yolov5——训练策略
- 前言
- 1. 训练预热——Warmup
- 2. 自动调整锚定框——Autoanchor
- 3. 超参数进化——遗传算法调优(GA)
- 4. 冻结训练——Freeze training
- 5. 多尺度训练——multi-scale training
- 6. 加权图像策略
- 7. 矩形推理——Rectangular Inference
- 8. 标签平滑
- 9. 非极大值抑制——NMS
- 10. 早停止
- 11. 分布式训练
- 12. 跨卡同步BN
- 13. 断点训练
- 14.断点训练
前言
yolov5的训练策略big big丰富,这也是yolov5涨分厉害的reason,目前yolov5的使用量也是非常大的,官网的star已经23.5k了,无论是在迁移学习还是实际场景的应用都是非常广泛的。之前参加比赛,发现好几页的选手都在使用yolov5,确实有必要梳理一下,yolov5的训练策略。感觉这些策略对以后自己实验帮助会很大,所以要细嚼慢咽,好了,不说了,走起开吃!!!!!!!!!
1. 训练预热——Warmup
1.1 what是Warmup
众所周知学习率是一个非常重要的超参数,直接影响着网络训练的速度核收敛情况。通常情况下,网络开始训练之前,我们会随机初始化权重,设置学习率过大会导致模型振荡严重,学习率过小,网络收敛太慢。那这个时候该怎么做呢?是不是有人会说,我前面几十个或者几百个epoch学习率设置小一点,后面正常后,设置大一点呢,没错这就是最简单的Warmup。
1.2 why用Warmup
我们可以把Warmup的过程想成,模型最开始是一个小孩,学习率太大容易认识事物太绝对了,这个时候需要小的学习率,摸着石头过河,小心翼翼地学习,当他对事物有一定了解和积累,认知有了一定地水平,这个时候步子再迈大一点就没问题了。
1.3 常见Warmup类型
1. Constant Warmup
在前面100epoch里,学习率线性增加,大于100epoch以后保持不变,整个过程如下如所示:
2. Linner Warmup
在前面100epoch里,学习率线性增加,大于100epoch以后保持线性下降,整个过程如下如所示:
2. Cosine Warmup
在前面100epoch里,学习率线性增加,大于100epoch以后保持x余弦方式下降,整个过程如下如所示:
通常来说第三种Cosine Warmup使用地频率较多一点。
1.4 yolov5中的Warmup
1. 超参数设置
在yolov5中data/hyps/hyp.scratch-*.yaml三个文件中,都存在着warmup_epoch代表训练预热轮次,以这
hyp.scratch.scratch-med.yaml为例,如图超参数列表
2. 训练转化
nb表示训练的类别数,例如coco数据集80类,在超参数列表中warmup_epochs=3,则nw = 3 * 80 = 240,所以热身训练240epoch, 这里要注意的是最少热身训练100次,所以设施epoch的时候最好大于100epoch,要不然热身都还没有做完,运动就结束了。
3.预热训练开始
yolov5的预测训练从这里开始,超参数的初始值和变化范围data/hyps/hyp.scratch-*.yaml给出来了,计算过程就在这里。
2. 自动调整锚定框——Autoanchor
2.1 what是anchor
anchor是指预定义的框集合,其宽度和高度与数据集中对象的宽度和高度相匹配。预置的anchor包含在数据集中存在的对象大小的组合,这自然包括数据中存在的不同长宽比和比例。通常在图像中的每一个位置预置4-10个anchor。
训练目标检测网络的典型任务包括:生成anchor,搜索潜在anchor,将生成的anchor与可能的ground truth配对,将其余anchor分配给背景类别,然后进行sampling和训练。
推理过程就是对anchor的分类和回归,score大于阈值的anchor进一步做回归,小于阈值的作为背景舍弃,这样就得到了目标检测的结果。
2.2 why用anchor
目标检测可以理解为回归+分类,怎么样最好的完成这个任务呢,是不是想到了用锚框,首先预设一组不同尺度不同位置的固定参考框,覆盖几乎所有位置和尺度,每个参考框负责检测与其交并比大于阈值 (训练预设值,常用0.5或0.7) 的目标,anchor技术将问题转换为"这个固定参考框中有没有认识的目标,目标框偏离参考框多远",不再需要像传统的目标检测那样,挨个挨个不同大小的滑动,费时费力。正是anchor的出现把目标检测分为了anchor free和anchor base。
想要了解更多的anchor相关的知识可以查看连接:
目标检测Anchor的What/Where/When/Why/How
目标检测中的Anchor
2.1 yolov5默认锚定框
yolov5中预先设定了一下锚定框,这些锚框是针对coco数据集的,其他目标检测也适用,可以在models/yolov5.文件中查看,例如如图所示,这些框针对的图片大小是640640。这是默认的anchor大小。需要注意的是在目标检测任务中,一般使用大特征图上去检测小目标,因为大特征图含有更多小目标信息,因此大特征图上的anchor数值通常设置为小数值,小特征图检测大目标,因此小特征图上anchor数值设置较大。
2.2 yolov5自动锚框
在yolov5 中自动锚定框选项,训练开始前,会自动计算数据集标注信息针对默认锚定框的最佳召回率,当最佳召回率大于等于0.98时,则不需要更新锚定框;如果最佳召回率小于0.98,则需要重新计算符合此数据集的锚定框。
在parse_opt设置了默认自动计算锚框选项,如果不想自动计算,可以设置这个,建议不要改动。
在train.py中设置检查锚框是否符合要求,主要使用的函数是check_anchor。
check_anchor函数的流程大概是:先判断锚框是否符合要求(判断条件bpr / aat,大于0.98就不会更新),然后利用k-mean聚类更新锚框。
3. 超参数进化——遗传算法调优(GA)
3.1 what是GA
遗传算法是利用种群搜索技术将种群作为一组问题解,通过对当前种群施加类似生物遗传环境因素的选择、交叉、变异等一系列的遗传操作来产生新一代的种群,并逐步使种群优化到包含近似最优解的状态。
3.2 why用GA
遗传算法调优能够求出优化问题的全局最优解,优化结果与初始条件无关,算法独立于求解域,具有较强的鲁棒性,适合于求解复杂的优化问题,应用较为广泛。
3.3 yolov5超参数进化
yolov5使用遗传超参数进化,提供的默认参数是通过在COCO数据集上使用超参数进化得来的。由于超参数进化会耗费大量的资源和时间,如果默认参数训练出来的结果能满足你的使用,使用默认参数是不很nice的选择。yolov5/data/hyp.scratch-*.yaml有三个文件共大家选择,这里使用hyp.scratch-low.yaml
train.p文件中parse_opt函数可以设置是否经行超参数优化。
超参数进化开始,使用fitness寻求最优化值。
4. 冻结训练——Freeze training
4.1 what是冻结训练
冻结训练是迁移学习常用的方法,当我们在使用数据量不足的情况下,通常我们会选择公共数据集提供权重作为预训练权重,我们知道网络的backbone主要是用来提取特征用的,一般大型数据集训练好的权重主干特征提取能力是比较强的,这个时候我们只需要冻结主干网络,fine-tune后面层就可以了,不需要从头开始训练,大大减少了实践而且还提高了性能。
4.2 how弄冻结训练
冻结训练的优势不言而喻了,这里简单的提一下冻结训练的步骤好了,通常的做法是:
1.定义一个冻结层, 冻结之前的学习率和bs可以设置大一点。
2.设置不更新权重param.requires_grad = False
整个过程如下
# 冻结阶段训练参数,learning_rate和batch_size可以设置大一点 Freeze_Epoch = 100 Freeze_batch_size = 32 Freeze_lr = 1e-3 # 解冻阶段训练参数,learning_rate和batch_size设置小一点 UnFreeze_Epoch = 100 Unfreeze_batch_size = 16 Unfreeze_lr = 1e-4 # 可以加一个变量控制是否进行冻结训练 Freeze_Train = True # 冻结一部分进行训练 batch_size = Freeze_batch_size lr = Freeze_lr start_epoch = Init_Epoch end_epoch = Freeze_Epoch if Freeze_Train: for param in model.backbone.parameters(): param.requires_grad = False # 解冻后训练 batch_size = Unfreeze_batch_size lr = Unfreeze_lr start_epoch = Freeze_Epoch end_epoch = UnFreeze_Epoch if Freeze_Train: for param in model.backbone.parameters(): param.requires_grad = True
4.3 yolov5冻结训练
yolov5的train.py文件中提供了冻结训练选项,在parse_opt函数中
yolov5s.yaml文件中可以查看到0-9层是backbone,因此在设置冻结层的时候注意不能超过9
冻结训练开始部分代码
这里提yolov5冻结效果查看的网站Freezing Layers in YOLOv5
5. 多尺度训练——multi-scale training
5.1 what是multi-scale training
多尺度训练在比赛中经常可以看到他身影,是被证明了有效提高性能的方式。输入图片的尺寸对检测模型的性能影响很大,在基础网络部分常常会生成比原图小数十倍的特征图,导致小物体的特征描述不容易被检测网络捕捉。通过输入更大、更多尺寸的图片进行训练,能够在一定程度上提高检测模型对物体大小的鲁棒性。
知乎这里有个讨论:目标检测中的多尺度训练/测试
多尺度训练是指设置几种不同的图片输入尺度,训练时每隔一定iterations随机选取一种尺度训练。这样训练出来的模型鲁棒性强,其可以接受任意大小的图片作为输入,使用尺度小的图片测试速度会快些。
先了解更多多尺度训练/测试的方法可以查看这篇文章目标检测中的多尺度检测方法
5.2 yolov5多尺度训练
在train.py文件中提供了多尺度训练的选项
在train.py文件这里是多尺度训练开始的位置
6. 加权图像策略
6.1 图像加权策略
图像加权策略可以解决样本不平衡的,具体操作步骤图下:
根据样本种类分布使用图像调用频率不同的方法解决。
1、读取训练样本中的GT,保存为一个列表;
2、计算训练样本列表中不同类别个数,然后给每个类别按相应目标框数的倒数赋值,数目越多的种类权重越小,形成按种类的分布直方图;
3、对于训练数据列表,训练时按照类别权重筛选出每类的图像作为训练数据。使用random.choice(population, weights=None, *, cum_weights=None, k=1)
更改训练图像索引,可达到样本均衡的效果。
6.2 yolov5图像加权策略
在yolov5中的train.py文件中存在着图像加权策略选项,如函数parse_opt
在train.py文件训练部分,这个位置开始使用图像加权
获取类别权重的函数如下
7. 矩形推理——Rectangular Inference
在讲矩形推理之前我们需要先弄清楚方形推理,下面通过对比介绍这两个概念
7.1 方形推理 Square Inference
我们知道yolo系列的输入都是固定大小416*416,因为下采样32倍的原因,因此必须是32的倍数。但是我们输入的图片大小不一致,因此在输入网络之前需要统一做一个仿射变换,这个过程就是方形推理。具体操作代码如下:
def cv2_letterbox_image(image, expected_size): ih, iw = image.shape[0:2] ew, eh = expected_size scale = min(eh / ih, ew / iw) nh = int(ih * scale) nw = int(iw * scale) image = cv2.resize(image, (nw, nh), interpolation=cv2.INTER_CUBIC) top = (eh - nh) // 2 bottom = eh - nh - top left = (ew - nw) // 2 right = ew - nw - left new_img = cv2.copyMakeBorder(image, top, bottom, left, right, cv2.BORDER_CONSTANT) return new_img
效果
我们可以看到经过这种填充之后存在很多冗余信息,填充的部分太多,不利于我们的预测,提高了推理时间。Rectangular Training思路就是想要去掉这些冗余的部分,减少推理时间。下面先对比一下这两种推理方法的时间吧。
python3 detect.py # 416 square inference # 方形推理 Using CPU image 1/2 data/samples/bus.jpg: 416x416 1 handbags, 3 persons, 1 buss, Done. (0.999s) image 2/2 data/samples/zidane.jpg: 416x416 1 ties, 2 persons, Done. (1.008s) python3 detect.py # 416 rectangular inference # 矩形推理 Using CPU image 1/2 data/samples/bus.jpg: 416x320 1 handbags, 3 persons, 1 buss, Done. (0.767s) image 2/2 data/samples/zidane.jpg: 256x416 1 ties, 2 persons, Done. (0.632s)
7.2 矩形推理——Rectangular Inference
可以看出矩形推理时间可以减少37%,那他具体是怎么做到的呢,其实思路很简单就是就是保证长边是416,短边是32的倍数,但是倍数可以不相同。比如场边是416/32=13,短边可以是320/32=10、256/32=8。
代码
# Rectangular Training if self.rect: # Sort by aspect ratio s = self.shapes # wh ar = s[:, 1] / s[:, 0] # aspect ratio irect = ar.argsort() self.im_files = [self.im_files[i] for i in irect] self.label_files = [self.label_files[i] for i in irect] self.labels = [self.labels[i] for i in irect] self.shapes = s[irect] # wh ar = ar[irect] # Set training image shapes shapes = [[1, 1]] * nb for i in range(nb): ari = ar[bi == i] mini, maxi = ari.min(), ari.max() if maxi < 1: shapes[i] = [maxi, 1] elif mini > 1: shapes[i] = [1, 1 / mini] self.batch_shapes = np.ceil(np.array(shapes) * img_size / stride + pad).astype(np.int) * stride
效果
7.3 yolov5矩形训练
在train,py文件中有使用矩形训练的选项,默认情况是不适用矩形训练
开启矩形训练之后,可以在数据加载这里看到
在对数据预处理的文件里面,这个位置,有矩形处理的代码
8. 标签平滑
8.1 标签平滑——label smooth
在训练样本中,我们并不能保证所有sample都标注正确,如果某个样本标注错误,就可能产生负面印象,如果我们有办法“告诉”模型,样本的标签不一定正确,那么训练出来的模型对于少量的样本错误就会有“免疫力”采用随机化的标签作为训练数据时,损失函数有1-ε的概率与上面的式子相同,比如说告诉模型只有0.95概率是那个标签。
8.2 操作代码
在这里给大家两种标签平滑的方式
# label smoothing: 两个极端的值变得不那么极端 # 在训练数据太少, 不足表征所有样本的特征的情况下,会导致过拟合 # 用的是平滑后的label和softmax后的值做交叉熵,也就是给标签乘上一个平滑系数 import numpy as np import tensorflow as tf def SmoothOneHot(labels, classes, smooth=0.0): """ if smoothing == 0, it's one-hot method if 0 < smoothing < 1, it's smooth method """ assert 0 <= smooth < 1 labels *= 1- smooth labels += smooth/labels.shape[1] return labels if __name__ == "__main__": out = np.array([[4.0, 5.0, 10.0], [1.0, 5.0, 4.0], [1.0, 15.0, 4.0]]) y = np.array([[0.0, 0.0, 1.00], [0.0, 1.0, 0.0], [0.0, 1.00, 0.0]]) res1 = tf.losses.softmax_cross_entropy(onehot_labels=y, logits=out, label_smoothing=0) print("不使用标签平滑:", tf.Session().run(res1)) res2 = tf.losses.softmax_cross_entropy(onehot_labels=y, logits=out, label_smoothing=0.001) print("使用标签平滑:", tf.Session().run(res2)) # 自定义标签平滑 # new_onehot_labels = onehot_labels * (1 - label_smoothing) + label_smoothing / num_classes new_onehot_labels = y * (1 - 0.001) + 0.001 / 3 print("原始标签:", y) print("平滑处理后的标签:", new_onehot_labels) res3 = tf.losses.softmax_cross_entropy(onehot_labels=new_onehot_labels, logits=out, label_smoothing=0) print(tf.Session().run(res3))
8.3 yolov5标签平滑
在train.py文件中的这个位置可以根据自己的数据集分布特点设置平滑系数。
------------------------最近太忙了,剩下的后面在补--------------------------------------
9. 非极大值抑制——NMS
9.1 what是NMS
在目标检测过程中,同一目标会生成多个候选框,这些候选框之间是大量重叠的,要在大量的候选框之间最适合目标的框,就需要用到非极大抑制
9.2 how是NMS
非极大值抑制的操作步骤如下:
- 首先置信度得分进行排序
- 找到最大概率的候选框
- 计算置信度最高的边界框与其它候选框的IoU。
- 删除IoU大于阈值的边界框
- 重复上述过程,直至边界框列表为空。
下面提供两个python代码,帮助大家理解这个过程
import cv2 import numpy as np """ Non-max Suppression Algorithm @param list Object candidate bounding boxes @param list Confidence score of bounding boxes @param float IoU threshold @return Rest boxes after nms operation """ def nms(bounding_boxes, confidence_score, threshold): # If no bounding boxes, return empty list if len(bounding_boxes) == 0: return [], [] # Bounding boxes boxes = np.array(bounding_boxes) # coordinates of bounding boxes start_x = boxes[:, 0] start_y = boxes[:, 1] end_x = boxes[:, 2] end_y = boxes[:, 3] # Confidence scores of bounding boxes score = np.array(confidence_score) # Picked bounding boxes picked_boxes = [] picked_score = [] # Compute areas of bounding boxes areas = (end_x - start_x + 1) * (end_y - start_y + 1) # Sort by confidence score of bounding boxes order = np.argsort(score) # Iterate bounding boxes while order.size > 0: # The index of largest confidence score index = order[-1] # Pick the bounding box with largest confidence score picked_boxes.append(bounding_boxes[index]) picked_score.append(confidence_score[index]) # Compute ordinates of intersection-over-union(IOU) x1 = np.maximum(start_x[index], start_x[order[:-1]]) x2 = np.minimum(end_x[index], end_x[order[:-1]]) y1 = np.maximum(start_y[index], start_y[order[:-1]]) y2 = np.minimum(end_y[index], end_y[order[:-1]]) # Compute areas of intersection-over-union w = np.maximum(0.0, x2 - x1 + 1) h = np.maximum(0.0, y2 - y1 + 1) intersection = w * h # Compute the ratio between intersection and union ratio = intersection / (areas[index] + areas[order[:-1]] - intersection) left = np.where(ratio < threshold) order = order[left] return picked_boxes, picked_score # Image name image_name = './li.jpg' # Bounding boxes bounding_boxes = [(187, 82, 337, 317), (150, 67, 305, 282), (246, 121, 368, 304)] confidence_score = [0.9, 0.75, 0.8] # Read image image = cv2.imread(image_name) # Copy image as original org = image.copy() cv2.imshow('Original', org) # Draw parameters font = cv2.FONT_HERSHEY_SIMPLEX font_scale = 1 thickness = 2 # IoU threshold threshold = 0.4 # Draw bounding boxes and confidence score for (start_x, start_y, end_x, end_y), confidence in zip(bounding_boxes, confidence_score): (w, h), baseline = cv2.getTextSize(str(confidence), font, font_scale, thickness) cv2.rectangle(org, (start_x, start_y - (2 * baseline + 5)), (start_x + w, start_y), (0, 255, 255), -1) cv2.rectangle(org, (start_x, start_y), (end_x, end_y), (0, 255, 255), 2) cv2.putText(org, str(confidence), (start_x, start_y), font, font_scale, (0, 0, 0), thickness) # Run non-max suppression algorithm picked_boxes, picked_score = nms(bounding_boxes, confidence_score, threshold) # Draw bounding boxes and confidence score after non-maximum supression for (start_x, start_y, end_x, end_y), confidence in zip(picked_boxes, picked_score): (w, h), baseline = cv2.getTextSize(str(confidence), font, font_scale, thickness) cv2.rectangle(image, (start_x, start_y - (2 * baseline + 5)), (start_x + w, start_y), (0, 255, 255), -1) cv2.rectangle(image, (start_x, start_y), (end_x, end_y), (0, 255, 255), 2) cv2.putText(image, str(confidence), (start_x, start_y), font, font_scale, (0, 0, 0), thickness) print(org) # Show image cv2.imshow('Original', org) cv2.imshow('NMS', image) cv2.waitKey(0)
运行结果
代码2
import numpy as np def NMS(dets, thresh): #x1、y1、x2、y2、以及score赋值 # (x1、y1)(x2、y2)为box的左上和右下角标 x1 = dets[:, 0] y1 = dets[:, 1] x2 = dets[:, 2] y2 = dets[:, 3] scores = dets[:, 4] #每一个候选框的面积 areas = (x2 - x1 + 1) * (y2 - y1 + 1) #order是按照score降序排序的,得到的是排序的本来的索引,不是排完序的原数组 order = scores.argsort()[::-1] # ::-1表示逆序 temp = [] while order.size > 0: i = order[0] temp.append(i) #计算当前概率最大矩形框与其他矩形框的相交框的坐标 # 由于numpy的broadcast机制,得到的是向量 xx1 = np.maximum(x1[i], x1[order[1:]]) yy1 = np.minimum(y1[i], y1[order[1:]]) xx2 = np.minimum(x2[i], x2[order[1:]]) yy2 = np.maximum(y2[i], y2[order[1:]]) #计算相交框的面积,注意矩形框不相交时w或h算出来会是负数,需要用0代替 w = np.maximum(0.0, xx2 - xx1 + 1) h = np.maximum(0.0, yy2 - yy1 + 1) inter = w * h #计算重叠度IoU ovr = inter / (areas[i] + areas[order[1:]] - inter) #找到重叠度不高于阈值的矩形框索引 inds = np.where(ovr <= thresh)[0] #将order序列更新,由于前面得到的矩形框索引要比矩形框在原order序列中的索引小1,所以要把这个1加回来 order = order[inds + 1] return temp if __name__ == "__main__": dets = np.array([[310, 30, 420, 5, 0.6], [20, 20, 240, 210, 1], [70, 50, 260, 220, 0.8], [400, 280, 560, 360, 0.7]]) print(dets[:, 0]) # 设置阈值 thresh = 0.4 keep_dets = NMS(dets, thresh) # # 打印留下的框的索引 print(keep_dets) # # 打印留下的框的信息 # print(dets[keep_dets])