1.YOLOv5算法简介

YOLOv5主要由输入端、Backone、Neck以及Prediction四部分组成。其中:

(1) Backbone:在不同图像细粒度上聚合并形成图像特征的卷积神经网络。

(2) Neck:一系列混合和组合图像特征的网络层,并将图像特征传递到预测层。

(3) Head: 对图像特征进行预测,生成边界框和并预测类别。

检测框架:

image.png

2.原始YOLOv5模型

[原始]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# YOLOv5 head
head:
[[-1, 1, Conv, [512, 1, 1]],
[-1, 1, nn.Upsample, [None, 2, 'nearest']],
[[-1, 6], 1, Concat, [1]], # cat backbone P4
[-1, 3, C3, [512, False]], # 13

[-1, 1, Conv, [256, 1, 1]],
[-1, 1, nn.Upsample, [None, 2, 'nearest']],
[[-1, 4], 1, Concat, [1]], # cat backbone P3
[-1, 3, C3, [256, False]], # 17 (P3/8-small)

[-1, 1, Conv, [256, 3, 2]],
[[-1, 14], 1, Concat, [1]], # cat head P4
[-1, 3, C3, [512, False]], # 20 (P4/16-medium)

[-1, 1, Conv, [512, 3, 2]],
[[-1, 10], 1, Concat, [1]], # cat head P5
[-1, 3, C3, [1024, False]], # 23 (P5/32-large)

[[17, 20, 23], 1, Detect, [nc, anchors]], # Detect(P3, P4, P5)
]


若输入图像尺寸=640X640,

P3/8 对应的检测特征图大小为80X80,用于检测大小在8X8以上的目标。

P4/16对应的检测特征图大小为40X40,用于检测大小在16X16以上的目标。

P5/32对应的检测特征图大小为20X20,用于检测大小在32X32以上的目标。

3.增加小目标检测层

[魔改]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60

# parameters
nc: 1 # number of classes
depth_multiple: 0.33 # model depth multiple
width_multiple: 0.50 # layer channel multiple

# anchors
anchors:
- [5,6, 8,14, 15,11] #4
- [10,13, 16,30, 33,23] # P3/8
- [30,61, 62,45, 59,119] # P4/16
- [116,90, 156,198, 373,326] # P5/32

# YOLOv5 backbone
backbone:
# [from, number, module, args]
[[-1, 1, Focus, [64, 3]], # 0-P1/2
[-1, 1, Conv, [128, 3, 2]], # 1-P2/4
[-1, 3, BottleneckCSP, [128]], #160*160
[-1, 1, Conv, [256, 3, 2]], # 3-P3/8
[-1, 9, BottleneckCSP, [256]], #80*80
[-1, 1, Conv, [512, 3, 2]], # 5-P4/16
[-1, 9, BottleneckCSP, [512]], #40*40
[-1, 1, Conv, [1024, 3, 2]], # 7-P5/32
[-1, 1, SPP, [1024, [5, 9, 13]]],
[-1, 3, BottleneckCSP, [1024, False]], # 9 20*20
]

# YOLOv5 head
head:
[[-1, 1, Conv, [512, 1, 1]], #20*20
[-1, 1, nn.Upsample, [None, 2, 'nearest']], #40*40
[[-1, 6], 1, Concat, [1]], # cat backbone P4 40*40
[-1, 3, BottleneckCSP, [512, False]], # 13 40*40

[-1, 1, Conv, [512, 1, 1]], #40*40
[-1, 1, nn.Upsample, [None, 2, 'nearest']],
[[-1, 4], 1, Concat, [1]], # cat backbone P3 80*80
[-1, 3, BottleneckCSP, [512, False]], # 17 (P3/8-small) 80*80

[-1, 1, Conv, [256, 1, 1]], #18 80*80
[-1, 1, nn.Upsample, [None, 2, 'nearest']], #19 160*160
[[-1, 2], 1, Concat, [1]], #20 cat backbone p2 160*160
[-1, 3, BottleneckCSP, [256, False]], #21 160*160

[-1, 1, Conv, [256, 3, 2]], #22 80*80
[[-1, 18], 1, Concat, [1]], #23 80*80
[-1, 3, BottleneckCSP, [256, False]], #24 80*80

[-1, 1, Conv, [256, 3, 2]], #25 40*40
[[-1, 14], 1, Concat, [1]], # 26 cat head P4 40*40
[-1, 3, BottleneckCSP, [512, False]], # 27 (P4/16-medium) 40*40

[-1, 1, Conv, [512, 3, 2]], #28 20*20
[[-1, 10], 1, Concat, [1]], #29 cat head P5 #20*20
[-1, 3, BottleneckCSP, [1024, False]], # 30 (P5/32-large) 20*20

[[21, 24, 27, 30], 1, Detect, [nc, anchors]], # Detect(p2, P3, P4, P5)
]

上面的代码中根据不同yolov5版本进行适当修改,例如有的版本backbone第一行为[-1, 1, Conv, [64, 6, 2, 2]], # 0-P1/2,利用卷积取消了Focus,有些版本是C3替换了C2f,backbone第一行为[-1, 1, Conv, [64, 3, 2]] # 0-P1/2,其它yolo系列如yolov7与yolov8原理一样,之不过yolov5与yolov7许哟啊用到anchors,因此anchor也需要在原来基础上增加一个如上,yolov8则不需要,然后都是根据对应版本的原始yaml文件进行更改,只更改head部分即可。

# 新增加160X160的检测特征图,用于检测4X4以上的目标。

改进后,虽然计算量和检测速度有所增加,但对小目标的检测精度有明显改善。

这里给出其它yolov7与yolov8魔改的链接

YOLOv8/YOLOv7/YOLOv5系列算法改进【NO.6】增加小目标检测层,提高对小目标的检测效果_yolov5添加小目标检测层_人工智能算法研究院的博客-CSDN博客

anchor重新聚类方法
https://blog.csdn.net/qq_37541097/article/details/119647026

yolov5中聚类anchors代码讲解
如果你是直接使用yolov5的训练脚本,那么它会自动去计算下默认的anchors与你数据集中所有目标的best possible recall,如果小于0.98就会根据你自己数据集的目标去重新聚类生成anchors,反之使用默认的anchors。

下面代码是我根据yolov5中聚类anchors的代码简单修改得到的。基本流程不变,主要改动了三点:1.对代码做了些简化。2.把使用pytorch的地方都改成了numpy(感觉这样会更通用点,但numpy效率确实没有pytorch高)。3.作者默认使用的k-means方法是scipy包提供的,使用的是欧式距离。我自己改成了基于1-IOU(bboxes, anchors)距离的方法。当然我只是注释掉了作者原来的方法,如果想用自己把注释取消掉就行了。但在我使用测试过程中,还是基于1-IOU(bboxes, anchors)距离的方法会略好点。

完整代码链接:
https://github.com/WZMIAOMIAO/deep-learning-for-image-processing/tree/master/others_project/kmeans_anchors

其实在yolov5生成anchors中不仅仅使用了k-means聚类,还使用了Genetic Algorithm遗传算法,在k-means聚类的结果上进行mutation变异。接下来简单介绍下代码流程:

读取训练集中每张图片的wh以及所有bboxes的wh(这里是我自己写的脚本读取的PASCAL VOC数据)
将每张图片中wh的最大值等比例缩放到指定大小img_size,由于读取的bboxes是相对坐标所以不需要改动
将bboxes从相对坐标改成绝对坐标(乘以缩放后的wh)
筛选bboxes,保留wh都大于等于两个像素的bboxes
使用k-means聚类得到n个anchors
使用遗传算法随机对anchors的wh进行变异,如果变异后效果变得更好(使用anchor_fitness方法计算得到的fitness(适应度)进行评估)就将变异后的结果赋值给anchors,如果变异后效果变差就跳过,默认变异1000次。
将最终变异得到的anchors按照面积进行排序并返回

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
import random
import numpy as np
from tqdm import tqdm
from scipy.cluster.vq import kmeans

from read_voc import VOCDataSet
from yolo_kmeans import k_means, wh_iou


def anchor_fitness(k: np.ndarray, wh: np.ndarray, thr: float): # mutation fitness
r = wh[:, None] / k[None]
x = np.minimum(r, 1. / r).min(2) # ratio metric
# x = wh_iou(wh, k) # iou metric
best = x.max(1)
f = (best * (best > thr).astype(np.float32)).mean() # fitness
bpr = (best > thr).astype(np.float32).mean() # best possible recall
return f, bpr


def main(img_size=512, n=9, thr=0.25, gen=1000):
# 从数据集中读取所有图片的wh以及对应bboxes的wh
dataset = VOCDataSet(voc_root="/data", year="2012", txt_name="train.txt")
im_wh, boxes_wh = dataset.get_info()

# 最大边缩放到img_size
im_wh = np.array(im_wh, dtype=np.float32)
shapes = img_size * im_wh / im_wh.max(1, keepdims=True)
wh0 = np.concatenate([l * s for s, l in zip(shapes, boxes_wh)]) # wh

# Filter 过滤掉小目标
i = (wh0 < 3.0).any(1).sum()
if i:
print(f'WARNING: Extremely small objects found. {i} of {len(wh0)} labels are < 3 pixels in size.')
wh = wh0[(wh0 >= 2.0).any(1)] # 只保留wh都大于等于2个像素的box

# Kmeans calculation
# print(f'Running kmeans for {n} anchors on {len(wh)} points...')
# s = wh.std(0) # sigmas for whitening
# k, dist = kmeans(wh / s, n, iter=30) # points, mean distance
# assert len(k) == n, print(f'ERROR: scipy.cluster.vq.kmeans requested {n} points but returned only {len(k)}')
# k *= s
k = k_means(wh, n)

# 按面积排序
k = k[np.argsort(k.prod(1))] # sort small to large
f, bpr = anchor_fitness(k, wh, thr)
print("kmeans: " + " ".join([f"[{int(i[0])}, {int(i[1])}]" for i in k]))
print(f"fitness: {f:.5f}, best possible recall: {bpr:.5f}")

# Evolve
# 遗传算法(在kmeans的结果基础上变异mutation)
npr = np.random
f, sh, mp, s = anchor_fitness(k, wh, thr)[0], k.shape, 0.9, 0.1 # fitness, generations, mutation prob, sigma
pbar = tqdm(range(gen), desc=f'Evolving anchors with Genetic Algorithm:') # progress bar
for _ in pbar:
v = np.ones(sh)
while (v == 1).all(): # mutate until a change occurs (prevent duplicates)
v = ((npr.random(sh) < mp) * random.random() * npr.randn(*sh) * s + 1).clip(0.3, 3.0)
kg = (k.copy() * v).clip(min=2.0)
fg, bpr = anchor_fitness(kg, wh, thr)
if fg > f:
f, k = fg, kg.copy()
pbar.desc = f'Evolving anchors with Genetic Algorithm: fitness = {f:.4f}'

# 按面积排序
k = k[np.argsort(k.prod(1))] # sort small to large
print("genetic: " + " ".join([f"[{int(i[0])}, {int(i[1])}]" for i in k]))
print(f"fitness: {f:.5f}, best possible recall: {bpr:.5f}")


if __name__ == "__main__":
main()

聚类anchors需要注意的坑

有时使用自己聚类得到的anchors的效果反而变差了,此时你可以从以下几方面进行检查:

注意输入网络时训练的图片尺寸。这是个很重要的点,因为一般训练/验证时输入网络的图片尺寸是固定的,比如说640x640,那么图片在输入网络前一般会将最大边长缩放到640,同时图片中的bboxes也会进行缩放。所以在聚类anchors时需要使用相同的方式提前去缩放bboxes,否则聚类出来的anchors并不匹配。比如你的图片都是1280x1280大小的,假设bboxes都是100x100大小的,如果不去缩放bboxes,那么聚类得到的anchors差不多是在100x100附近。而实际训练网络时bboxes都已经缩放到50x50大小了,此时理想的anchors应该是50x50左右而不是100x100了。
如果使用预训练权重,不要冻结太多的权重。现在训练自己数据集时一般都是使用别人在coco等大型数据上预训练好的权重。而这些权重是基于coco等数据集上聚类得到的结果,并不是针对自己数据集聚类得到的。所以网络为了要适应新的anchors需要调整很多权重,如果你冻结了很多层(假设只去微调最后的预测器,其他权重全部冻结),那么得到的结果很大几率还没有之前的anchors好。当可训练的权重越来越多,一般使用自己数据集聚类得到的anchors会更好一点(前提是自己聚类的anchors是合理的)。