复古数据科学:测试 YOLO 的第一个版本

让我们回到8年前

使用YOLO进行对象检测,作者:Image by author

数据科学的世界不断变化。通常情况下,我们看不到这些变化,因为它们缓慢进行,但是经过一段时间,很容易回顾过去,看到景观已经发生了巨大的变化。仅在10年前处于进步前沿的工具和库,今天可能完全被遗忘。

YOLO(You Only Look Once)是一种流行的对象检测库。其第一版是在2015年发布的。YOLO工作速度快,提供良好的结果,并且预训练模型是公开可用的。该模型很快变得流行起来,该项目现在仍在不断改进。这使我们有机会看到数据科学工具和库在多年中的演变。在本文中,我将测试不同的YOLO版本,从第一个V1到最新的V8。

为了进行进一步的测试,我将使用OpenCV YOLO教程中的一个图像:

测试图像,来源 © https://opencv-tutorial.readthedocs.io

想要自己重现结果的读者可以打开该链接并下载原始图像。

让我们开始吧。

YOLO V1..V3

关于YOLO的第一篇论文“You Only Look Once: Unified, Real-Time Object Detection”于2015年发布。令人惊讶的是,YOLO v1仍可供下载。正如原始论文的作者之一Mr.Redmon所写的那样,他保留了这个版本“出于历史目的”,这确实很好。但是今天我们能否运行它呢?该模型以两个文件的形式分布。配置文件“yolo.cfg”包含有关神经网络模型的详细信息:

[net]batch=1height=448width=448channels=3momentum=0.9decay=0.0005...[convolutional]batch_normalize=1filters=64size=7stride=2pad=1activation=leaky

第二个文件“yolov1.weights”,顾名思义,包含预训练模型的权重。

这种格式不是来自PyTorch或Keras。事实证明,该模型是使用C语言编写的开源神经网络框架Darknet创建的。该项目仍然可在GitHub上找到,但它看起来被遗弃了。在撰写本文时,有164个拉取请求和1794个未解决的问题;最后一次提交是在2018年,之后只更改了README.md(这可能就是这个项目在现代数字世界中的死亡方式)。

原始的Darknet项目被遗弃了,这是一个坏消息。好消息是,readNetFromDarknet方法仍然可在OpenCV中使用,甚至在最新的OpenCV版本中也可用。因此,我们可以轻松地尝试使用现代Python环境加载原始的YOLO v1模型:

import cv2model = cv2.dnn.readNetFromDarknet("yolo.cfg", "yolov1.weights")

遗憾的是,它没有起作用;我只得到了一个错误:

darknet_io.cpp:902: error: (-212:Parsing error) Unknown layer type: local in function 'ReadDarknetFromCfgStream'

结果“yolo.cfg”有一层名为“local”的层,OpenCV不支持,我也不知道是否有解决方法。无论如何,YOLO v2配置中不再有此层,可以在OpenCV中成功加载该模型:

import cv2
model = cv2.dnn.readNetFromDarknet("yolov2.cfg", "yolov2.weights")

使用该模型并不像我们想象的那么容易。首先,我们需要找到模型的输出层:

ln = model.getLayerNames()
output_layers = [ln[i - 1] for i in model.getUnconnectedOutLayers()]

然后我们需要加载图像并将其转换为模型可以理解的二进制格式:

img = cv2.imread('horse.jpg')
H, W = img.shape[:2]
blob = cv2.dnn.blobFromImage(img, 1/255.0, (608, 608), swapRB=True, crop=False)

最后,我们可以进行前向传播。 “前向”方法将运行计算并返回请求的层输出:

model.setInput(blob)
outputs = model.forward(output_layers)

进行前向传播很简单,但解析输出可能有点棘手。该模型将输出85维特征向量,其中前4个数字表示对象矩形,第5个数字是对象存在的概率,最后80个数字包含模型训练的80个类别的概率信息。有了这些信息,我们可以在原始图像上绘制标签:

threshold = 0.5
boxes, confidences, class_ids = [], [], []
# 获取所有框和标签
for output in outputs:
    for detection in output:
        scores = detection[5:]
        class_id = np.argmax(scores)
        confidence = scores[class_id]
        if confidence > threshold:
            center_x, center_y = int(detection[0] * W), int(detection[1] * H)
            width, height = int(detection[2] * W), int(detection[3] * H)
            left = center_x - width//2
            top = center_y - height//2
            boxes.append([left, top, width, height])
            class_ids.append(class_id)
            confidences.append(float(confidence))
# 使用非最大抑制将框合并在一起
indices = cv2.dnn.NMSBoxes(boxes, confidences, 0.5, 0.4)
# 所有 COCO 类别
classes = "person;bicycle;car;motorbike;aeroplane;bus;train;truck;boat;traffic light;fire hydrant;stop sign;parking meter;bench;bird;" \
          "cat;dog;horse;sheep;cow;elephant;bear;zebra;giraffe;backpack;umbrella;handbag;tie;suitcase;frisbee;skis;snowboard;sports ball;kite;" \
          "baseball bat;baseball glove;skateboard;surfboard;tennis racket;bottle;wine glass;cup;fork;knife;spoon;bowl;banana;apple;sandwich;" \
          "orange;broccoli;carrot;hot dog;pizza;donut;cake;chair;sofa;pottedplant;bed;diningtable;toilet;tvmonitor;laptop;mouse;remote;keyboard;" \
          "cell phone;microwave;oven;toaster;sink;refrigerator;book;clock;vase;scissors;teddy bear;hair dryer;toothbrush".split(";")
# 在图像上绘制矩形
colors = np.random.randint(0, 255, size=(len(classes), 3), dtype='uint8')
for i in indices.flatten():
    x, y, w, h = boxes[i]
    color = [int(c) for c in colors[class_ids[i]]]
    cv2.rectangle(img, (x, y), (x + w, y + h), color, 2)
    text = f"{classes[class_ids[i]]}: {confidences[i]:.2f}"
    cv2.putText(img, text, (x + 2, y - 6), cv2.FONT_HERSHEY_COMPLEX, 0.5, color, 1)
# 显示
cv2.imshow('window', img)
cv2.waitKey(0)
cv2.destroyAllWindows()

这里使用 np.argmax 找到具有最大概率的类别ID。 YOLO 模型使用 COCO(上下文中的常见对象,知识共享署名4.0许可)数据集进行训练,并为了简单起见,我直接将所有80个标签名称放在了代码中。 我还使用了 OpenCV NMSBoxes 方法将嵌入式矩形合并在一起。

最终结果如下:

作者提供的YOLO v2结果图

我们成功地在现代环境下运行了一个2016年发布的模型!

下一个版本,YOLO v3,在两年后的2018年发布,我们也可以使用同样的代码运行它(权重和配置文件在网上可用)。正如作者在论文中写的那样,新模型更加准确,我们可以轻松验证:

作者提供的YOLO v3结果图

确实,一个V3模型能够在同一图像上找到更多的物体。那些对技术细节感兴趣的读者可以阅读这篇2018年的TDS文章。

YOLO V5..V7

我们可以看到,使用readNetFromDarknet方法加载的模型是有效的,但所需的代码相当“低级”和繁琐。OpenCV开发人员决定使生活更轻松,在2019年的4.1.2版本中添加了一个新的DetectionModel类。我们可以这样加载YOLO模型;一般逻辑保持不变,但所需的代码量要小得多。该模型直接返回类ID、置信度值和矩形,只需一个方法调用:

import cv2model = cv2.dnn_DetectionModel("yolov7.cfg", "yolov7.weights")model.setInputParams(size=(640, 640), scale=1/255, mean=(127.5, 127.5, 127.5), swapRB=True)class_ids, confidences, boxes = model.detect(img, confThreshold=0.5)# 使用非最大值抑制将框组合在一起indices = cv2.dnn.NMSBoxes(boxes, confidences, 0.5, 0.4)# 所有COCO类别classes = "person;bicycle;car;motorbike;aeroplane;bus;train;truck;boat;traffic light;fire hydrant;stop sign;parking meter;bench;bird;" \          "cat;dog;horse;sheep;cow;elephant;bear;zebra;giraffe;backpack;umbrella;handbag;tie;suitcase;frisbee;skis;snowboard;sports ball;kite;" \          "baseball bat;baseball glove;skateboard;surfboard;tennis racket;bottle;wine glass;cup;fork;knife;spoon;bowl;banana;apple;sandwich;" \          "orange;broccoli;carrot;hot dog;pizza;donut;cake;chair;sofa;pottedplant;bed;diningtable;toilet;tvmonitor;laptop;mouse;remote;keyboard;" \          "cell phone;microwave;oven;toaster;sink;refrigerator;book;clock;vase;scissors;teddy bear;hair dryer;toothbrush".split(";")# 在图像上绘制矩形colors = np.random.randint(0, 255, size=(len(classes), 3), dtype='uint8')for i in indices.flatten():    x, y, w, h = boxes[i]    color = [int(c) for c in colors[class_ids[i]]]    cv2.rectangle(img, (x, y), (x + w, y + h), color, 2)    text = f"{classes[class_ids[i]]}: {confidences[i]:.2f}"    cv2.putText(img, text, (x, y - 5), cv2.FONT_HERSHEY_SIMPLEX, 0.5, color, 1)# 显示cv2.imshow('window', img)cv2.waitKey(0)cv2.destroyAllWindows()

我们可以看到,从模型输出中提取框和置信度值所需的所有低级代码现在不再需要。

运行YOLO v7的结果总体上相同,但是围绕马的矩形看起来更加精确:

作者提供的YOLO v7结果图

YOLO V8

第8版于2023年发布,因此我在撰写本文时至少不能认为它是“复古”的。但是为了对比结果,让我们看看现在运行YOLO所需的代码:

from ultralytics import YOLOimport supervision as svmodel = YOLO('yolov8m.pt')results = model.predict(source=img, save=False, save_txt=False, verbose=False)detections = sv.Detections.from_yolov8(results[0])# Create list of labelslabels = []for ind, class_id in enumerate(detections.class_id):    labels.append(f"{model.model.names[class_id]}: {detections.confidence[ind]:.2f}")# Draw rectangles on imagebox_annotator = sv.BoxAnnotator(thickness=2, text_thickness=1, text_scale=0.4)box_annotator.annotate(scene=img, detections=detections, labels=labels)# Showcv2.imshow('window', img)cv2.waitKey(0)cv2.destroyAllWindows()

我们可以看到,代码变得更加紧凑。我们不需要考虑数据集标签名称(模型提供“名称”属性)或如何在图像上绘制矩形和标签(有一个特殊的BoxAnnotator类)。我们甚至不需要再下载模型权重。该库将自动为我们完成。与2016年相比,2023年的程序从大约50行缩减到了大约5行!这显然是一个不错的改进,现代开发人员不需要再了解前向传播或输出级别格式。该模型只是一个带有某些“魔术”的黑盒子。这是好还是坏?我不知道 🙂

至于结果本身,它更或多或少相似:

YOLO v8 results, Image by author

该模型运行良好,至少在我的计算机上,与v7相比,计算速度有所提高,可能是由于更好地利用了GPU。

结论

在本文中,我们能够测试几乎所有从2016年到2023年推出的YOLO模型。乍一看,尝试运行一个发布了近10年的模型可能看起来是浪费时间。但对我来说,在执行这些测试时学到了很多东西:

  • 看到流行的数据科学工具和库如何逐年发展是有趣的。从低级代码向高级方法的趋势,这些方法可以做任何事情,甚至在执行之前下载预训练模型(至少目前还没有要求订阅密钥,但是谁知道10年后会发生什么?),这一趋势非常明显。这是好是坏?这是一个有趣而开放的问题。
  • 了解OpenCV“本地”能够运行深度学习模型非常重要。这使得神经网络模型不仅可以在大型框架(如PyTorch或Keras)中使用,而且可以在纯Python甚至C ++应用程序中使用。并非每个应用程序都在具有几乎无限资源的云中运行。物联网市场正在增长,这对于在低功率设备(如机器人,监控摄像机或智能门铃)上运行神经网络尤其重要。

在下一篇文章中,我将更详细地测试它,并展示如何在低功率板(如树莓派)上运行YOLO v8,并且我们将能够测试Python和C ++版本。敬请关注。

如果您喜欢这篇文章,请订阅小猪AI,您将获得通知,当我的新文章发布时,以及其他作者的成千上万的故事的完整访问权限。