强拆YOLO_V3
这里拆解的代码主要来自GitHub eriklindernoren/PyTorch-YOLOv3。
ultralytics版也很流行,但eriklindernoren版更适合初学者。
1.模型可视化
1.1 yolov3各layer可视化
模型通过netron可视化,并稍作整理后显示如下。
别看很复杂,其实主体部分是backbone darknet_53;
darknet可以理解为一堆res_layer,再加上若干下采样块;
其余部分就是直接为yolo_layer服务的,第一个yolo_layer的输入直接为下采样32倍后的feature map,后两个yolo_layer是上采样2倍的结果与前面相同尺寸特征的融合;这有没有点unet的意思?
其中,Darknet_53用于目标检测的部分见下图红框,即前52个卷积层:
此结果和csdn博客yolo系列之yolo v3【深度解析】给出的结构(见下图)完全一致。
paddlepaddle目标检测文档中给出的更简约,也更直观的图如下:
1.2 yolov3-spp各layer可视化
对比可得,yolov3-spp只是在第一个yolo_layer之前加了个spp单元,同时多了一组conv+LeaklyRelu。
在eriklindernoren的实现版本中,spp由三个maxpool+1个shortcut进行concatenate实现。
三个maxpool分别为:
- nn.MaxPool2d(kernel_size=5, stride=1, padding=2)
- nn.MaxPool2d(kernel_size=9, stride=1, padding=4)
- nn.MaxPool2d(kernel_size=13, stride=1, padding=6)
由 o = i − k + 2 ∗ p s + 1 o=\cfrac{i-k+2*p}{s}+1 o=si−k+2∗p+1,知:输入输出size不变,可以直接进行spatial concatenate。
1.3 有趣的话题:YOLO_V3与UNet
UNet,特别是以resnet为backbone的unet,可以说与YOLO_V3是非常相似的。
下图仅为展示,不表示实际的卷积层个数。
Yolo_V3中,下采样完全通过常规卷积实现。
2.pred_box位置回归为哪般?
上图来自Christopher Bourez’s blog Bounding box object detectors,此图描绘位置回归,比原文中的图片还要形象。
paper中的公式:
b x = σ ( t x ) + c x b y = σ ( t y ) + c y b w = p w ⋅ e t w b h = p h ⋅ e t h b_x=\sigma(t_x)+c_x\\b_y=\sigma(t_y)+c_y\\b_w=p_w\cdot e^{t_w}\\b_h=p_h \cdot e^{t_h} bx=σ(tx)+cxby=σ(ty)+cybw=pw⋅etwbh=ph⋅eth
相应代码:
x = torch.sigmoid(prediction[..., 0])
y = torch.sigmoid(prediction[..., 1])
w = prediction[..., 2] # Width
h = prediction[..., 3] # Height
pred_boxes[..., 0] = x.data + self.grid_x # b_x=/sigma(t_x)+c_x
pred_boxes[..., 1] = y.data + self.grid_y # b_y=/sigma(t_y)+c_y
# b_w=p_w*\exp(t_w)
pred_boxes[..., 2] = torch.exp(w.data) * self.anchor_w
# b_h=p_h*\exp(t_h)
pred_boxes[..., 3] = torch.exp(h.data) * self.anchor_h
prediction[…, 0]到prediction[…, 4]是模型输出的初始 x 0 , y 0 , w 0 , h 0 x_0,y_0,w_0,h_0 x0,y0,w0,h0。这个信息里还没有anchor的尺寸信息。
回归过程,首先是对 x 0 , y 0 x_0,y_0 x0,y0进行 σ \sigma σ操作,再分别加上 c x , c y c_x,c_y cx,cy,即代码中的self.grid_x,self.grid_y 。
这是啥,其实就是当前进行预测的这个gird的坐标信息。这个grid是指feature_map,如原图416*416,分别缩小了32,16,8倍,则 c x , c y c_x,c_y cx,cy分别为0~13, 0~26 及 0~52。
然后是对 w , h w,h w,h的回归。公式里的 p w , p h p_w,p_h pw,ph,即self.anchor_w,self.anchor_h即所用anchor的w,h信息。 p w , p h p_w,p_h pw,ph也相应的是anchor宽/高经过缩放到当前feature map上的w,h。
下面是一组对比,对于 ( x , y ) (x,y) (x,y)必须首先将它放回到对应的尺寸,这里只对比w,h回归前后pred box的差异,另外这里只显示与target box最匹配grid的三个pred box。
从上图可以理解下,feature map越小,anchor相对越大,便于检测大目标;feature map越大,anchor相对越小,便于检测小目标。
图片如何画网格,使用的是plt.plot,可参考anchor box之 SSD default boxes及GitHub 。
只显示最佳grid最match的anchor,位置回归后的结果(由于数据预处理及权重随机,所以与上图不要联系在一起),见下图:
3.使用kmeans聚类生成新的一组anchor
kmeans是一种常见的聚类方法,其原理简介可见一文GET Kmeans、DBSCAN、GMM、谱聚类Spectral clustering 算法。
其实现步骤可简单概括为:
- 1.给定聚类类别个数k;
- 2.确定初始聚类中心,如从数据中随机选择k个样本作为初始中心;
- 3.确定某种距离规则,如常用的欧氏距离,按照至k个中心距离最小的原则将所有数据聚成k类;
- 4.重新计算k类的中心(如取各类样本各维度的均值);
- 5.重复3~4,直至各类中心不再发生变化或变化极小。
而yolov3作者进行聚类时(见yolov2),没有使用常用的欧氏距离,而是:
d ( b o x , c e n t r o i d ) = 1 − I O U ( b o x , c e n t r o i d ) \qquad\qquad d(box,centroid)=1-IOU(box,centroid) d(box,centroid)=1−IOU(box,centroid)
与target box的长宽与anchor的长宽越接近,二者IOU越大非常切合。
参考代码见yolov3_anchor kmeans python实现。
这里需要理解的一点是:
从label文件夹中获取某图片target box的长和宽要除以该图片的长和宽,得到的长和宽都在(0,1)范围。
然后聚类出来的k个类心也是一样,在(0,1)范围。
最后输出的anchors的长宽,要乘上设置的图片尺寸中长和宽,如(416,416)。这样才得到了我们需要的anchors。
这时就可以替换掉配置文件中的默认anchor,进行训练了。
4.关于在图片显示中加入bbox
bbox显示在图片中是object detection的必备工具。
一种实现方法已经在图像增强 imgaug中介绍过了,使用imgaug确实很赞。
还有一种用cv2实现,收在cv2画图操作中,应该是ultralytics版中get到的。
今天再介绍一种:使用老朋友matplotlib。这在eriklindernoren版detect.py代码中有介绍。
同样还是使用安全帽数据集中的一张图片。
使用matplotlib显示bbox代码如下:
from PIL import Image
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.patches as patches
from matplotlib.ticker import NullLocator
img_path = r"D:\1.jpg"
boxes = [[60, 66, 910, 1108]]
img = np.array(Image.open(img_path))
plt.figure()
fig, ax = plt.subplots(1)
ax.imshow(img)
for [x1, y1, x2, y2] in boxes:
box_w = x2 - x1
box_h = y2 - y1
# Create a Rectangle patch
bbox = patches.Rectangle(
(x1, y1), box_w, box_h, linewidth=1, edgecolor='b', facecolor="none")
# Add the bbox to the plot
ax.add_patch(bbox)
# Add label
plt.text(
x1,
y1,
s='hat',
color="white",
verticalalignment="top",
bbox={
"color": "black", "pad": 0},
)
# Save generated image with detections
plt.axis("off")
plt.gca().xaxis.set_major_locator(NullLocator())
plt.gca().yaxis.set_major_locator(NullLocator())
plt.show()
结果为:
可见依然还是一个字:帅!
参考文献
[1] https://github.com/eriklindernoren/PyTorch-YOLOv3
[2] yolo系列之yolo v3【深度解析】
[3] yolov3_anchor kmeans python实现
[4] Bounding box object detectors: understanding YOLO, You Look Only Once
[5]https://www.paddlepaddle.org.cn/tutorials/projectdetail/783903#anchor-19