优化策略
- 1. 数据增强:
- 1.2. 超参数修改
- 1.2 添加新的数据增强方法,YOLOv5集成Albumentations。
- 2. 调整输入尺寸:
- 2.1. 尝试调整输入图像的尺寸
- 3. 调整学习率和优化器:
- 3.1. 采用学习率调度策略,例如余弦退火调度。
- 3.2. 尝试不同的优化器,如Adam、SGD等,并调整学习率和动量参数。
- 3.3. 添加激活函数
- 4. 调整损失函数:
- 4.1. 使用 Focal loss
- 4.2.添加损失函数
- 5. 调整网络架构:
- 5.1. 详细操作步骤:
- 5.1.1. 增加网络深度和宽度:
- 5.1.2. 引入注意力机制:
- 5.2. 其他内容待补充
- 6. 正负样本平衡:
- 7. 模型剪枝:
- 8. 模型蒸馏:
- 8.1.载入teacher模型
- 9. 集成学习:
优化 YOLOv5 模型以提高精度可以涉及多个方面的调整和改进。下面是一些建议:
深度学习的五个步骤: 数据 -> 模型 -> 损失 -> 优化器 -> 迭代训练。
1. 数据增强:
- 增加数据集的多样性
包括更多的背景、不同光照条件、尺度变化等。
在数据集中添加更多具有不同背景的图像,以确保模型在各种环境中都能正常工作。 引入具有不同光照条件的图像,使模型能够适应不同的亮度和阴影。
包含具有不同尺度和比例的目标,以确保模型对各种目标尺寸的适应性。
- 尝试使用不同的数据增强技术
例如旋转、缩放、裁剪、亮度调整等,以提高模型对于各种场景的适应性。
1.2. 超参数修改
在train.py脚本中可以看到,训练时默认的超参数(Hyperparameter)在ROOT / 'data/hyps/hyp.scratch-low.yaml'
这个位置的这个hyp.scratch-low.yaml
文件中。
可以看到这个文件夹中还有其他的有关超参数设置的文件。
hyp.scratch-low.yaml(数据增强低)
hyp.scratch-mdeia.yaml(数据增强中)
hyp.scratch-high.yaml(数据增强高)
以hyp.scratch-low.yaml(数据增强低)为例,进行介绍。
# See tutorials for hyperparameter evolution https://github.com/ultralytics/yolov5#tutorials
#最初的学习率。学习率的初始值,通常使用SGD时为0.01,使用Adam时为0.001
lr0: 0.01 # 初始学习率 (SGD=1E-2, Adam=1E-3)
# 最终学习率,或者可以说是最小的学习率【一般学习率调度器会随着epoch增加衰减的】
#OneCycleLR 学习率调度器中的最终学习率(lr0 * lrf)
lrf: 0.01 # 循环学习率 (lr0 * lrf) # final OneCycleLR learning rate (lr0 * lrf)
#Momentum是为解决SGD中出现的曲折摆动问题,如图所示,“之”字形的上下摆动,降低了损失函数到达最低点的速度。
#此情况下,若想减少摆动浮动,只能采用比较小的learning rate,这同样将导致寻优的速度较低。而Momentum就是为解决此问题而来。动量可以理解为参数更新的惯性,类似于小球滚动的惯性。它通过维护一个动量向量,记录之前梯度方向的加权平均值,并将其用于参数更新,可以加快训练速度,提高模型稳定性。较小的动量可以使更新方向更加平稳较大的动量可以加速参数更新。
momentum: 0.937 #学习率动量 SGD momentum/Adam beta1
#权重衰减(weight decay)是一种常用的应对过拟合的方法,其等价于L2范数正则化(regularization)。
#正则化通过为模型损失函数添加惩罚项使得学出的模型参数较小,通常接近于0。
#权重衰减旨在减少模型的复杂性,防止过拟合。值越大,正则化强度越大,模型泛化能力会更好。但是太大会导致模型欠拟合。
weight_decay: 0.0005 # 权重衰减系数 optimizer weight decay 5e-4
#热身,让模型先热热身,熟悉熟悉数据,学习率要小,相当于只是去看看,还没正式训练呢,学习率太高说不定就学跑偏了。
#在训练深度学习模型时,有时候需要先使用一个较小的学习率来进行预热,以避免在初始阶段出现不稳定的梯度或损失的情况。
#warmup_epochs就是控制预热的epoch数,也就是在训练的前几个epoch使用较小的学习率,使得模型能够更快地收敛到稳定的状态。
#在预热阶段结束后,学习率会逐渐增加到初始设定的学习率,然后继续按照设定的学习率进行训练。
warmup_epochs: 3.0 # 预热学习 warmup epochs (fractions ok)
#与warmup_epochs相对应设定的参数,简单来说,就是热身训练那几个epochs的动量设置
warmup_momentum: 0.8 # 预热学习动量 warmup initial momentum
#与warmup_epochs相对应设定的参数,指在学习率warm-up期间,偏置(bias)的学习率所使用的初始学习率。
#在训练深度神经网络时,通常采用较小的学习率进行初始化,以便网络能够逐渐适应数据集的特征。
#当学习率较小时,偏置的学习率通常要比权重的学习率更高,以保证网络在早期学习阶段能够更快地适应数据集的偏置。
warmup_bias_lr: 0.1 #预热初始学习率 warmup initial bias lr
#box cls obj 这三者是YOLOv5中的三大损失
#目标框损失
#权重box通常指的是bounding box,即目标的边框。在yolov5中,是一个超参数,控制bounding box loss的权重。它影响了网络如何调整检测边界框的位置和大小,以更好地匹配目标。
#较大的box值将使网络更注重优化边界框。较小的值将使其更加注重类别分类。
box: 0.05 # iou损失系数 box loss gain
#分类损失权重
#在目标检测中,cls 通常表示分类损失。YoloV5中使用的是交叉熵损失,它度量的是目标属于每个类别的概率分布与实际分布之间的差异。
#这个损失用于调整网络的权重,使网络能够更准确地预测目标的类别。
#cls值越大,表示越注重分类损失的影响。
cls: 0.5 # cls损失系数 cls loss gain
#置信度损失权重
#YOLOv5 模型的 obj 指代对象的存在损失权重,用于指定正负样本的权重。
#这个权重被用来平衡正负样本对于训练的贡献,避免模型偏向于训练样本数较多的类别,从而提高模型的性能。
#当该参数值较大时,模型对对象的存在的关注程度就会更高。
obj: 1.0 #有无物体系数 obj loss gain (scale with pixels)
#cls_pw、obj_pw 是主要用于调控正样本权重的超参数
#分类损失的二元交叉熵BCE损失中正样本的权重
#cls_pw就是用于控制分类损失函数中正样本权重的参数。
#在某些情况下,数据集中的正负样本不平衡,导致分类损失函数中的负样本权重占比较大,这可能会导致模型对正样本的预测能力下降。
#可以通过增加正样本的权重来平衡分类损失函数,从而提高模型对正样本的预测能力。
#默认为1.0,表示正样本权重和负样本权重相等。
#值设置的越大,则正样本权重越大。
cls_pw: 1.0 #正样本权重 cls BCELoss positive_weight
#置信度损失的二元交叉熵BCE损失中正样本的权重
#如果正样本数量较少,YOLOv5会增加正样本的权重,以便更多地关注正样本,提高检测准确率。
#如果正样本数量较多,YOLOv5会减小正样本的权重,以便更多地关注负样本,减少误检率。
obj_pw: 1.0 #有无物体BCELoss正样本权重 obj BCELoss positive_weight
#Iou阈值,用于预测框和真实框之间的匹配。
#IoU 是指检测框(预测框)和真实框之间的交并比。当预测框和真实框之间的 IoU 大于 iou_t 时,视为检测正确,否则视为检测错误。
#比如,iuo_t设置为0.5,只有预测框和真实框之间的Iou大于0.5才会视为正确检测。
iou_t: 0.20 # IoU训练时的阈值 IoU training threshold
#在进行不同类型目标检测的时候可以适当调整这个参数,对于模型精准匹配和收敛会有帮助
#比如:检测车辆对象就可以设置大点,检测安全帽、无人机这类对象就可以设置小点
#anchor的阈值,用于筛选anchor。跟iou_t道理一样
#这个参数就是控制哪些anchor boxes会被用来匹配目标。anchor_t越小,则越容易匹配更小的目标,anchor_t越大,则越容易匹配更大的目标
#对于一些小的目标,因为它们的大小与anchor的大小差别较大,需要使用比较小的anchor box进行匹配,此时设置一个小的anchor_t可以让这些小目标更容易被匹配上。
#而对于一些大目标,它们的大小与anchor大小的差别较小,可以使用相对较大的anchor进行匹配,此时设置一个大的anchor_t可以使得大目标更容易被匹配。
anchor_t: 4.0 # anchor的长宽比(长:宽 = 4:1) anchor-multiple threshold
# anchors: 3 # anchors per output layer (0 to ignore)
#主要用于focalloss,fl_gamma 是 Focal Loss 中的一个参数,用于调整损失函数的加权。
#fl_gamma 就是控制难以分类样本权重的参数,其值越大,模型对难以分类样本的关注程度越高,对易于分类的样本关注程度越低。
-----------------------------------------------------------------
#以下系数是数据增强系数,包括颜色空间和图片空间
fl_gamma: 0.0 # focal loss gamma (efficientDet default gamma=1.5)
#颜色亮度,色调(Hue)、饱和度(Saturation)
# 表示图像HSV颜色空间的增强参数。
#hsv_h表示色调,取值范围都是[0, 1],可以尝试不同的取值来进行比较。值越大,强度越大
hsv_h: 0.015 # 色调 image HSV-Hue augmentation (fraction)
#表示图像HSV颜色空间的增强参数。
#hsv_s表示饱和度,取值范围都是[0, 1],可以尝试不同的取值来进行比较。值越大,强度越大
hsv_s: 1.0 #饱和度 image HSV-Saturation augmentation (fraction)
#表示图像HSV颜色空间的增强参数。
#hsv_v表示明度,取值范围都是[0, 1],可以尝试不同的取值来进行比较。值越大,强度越大
hsv_v: 0.4 #亮度 image HSV-Value augmentation (fraction)
#图像旋转。用于控制图像旋转的增强参数
#数据增强的程度越大,可以提高模型的泛化能力,但也会增加训练时间和计算资源的消耗。
degrees: 0.0 #旋转角度 (+/- deg) image rotation (+/- deg)
#图像平移。用于控制图像平移的增强参数
#数据增强的程度越大,可以提高模型的泛化能力,但也会增加训练时间和计算资源的消耗。
translate: 0.1 # 平移 image translation (+/- fraction)
#图像仿射变换的缩放比例。用于控制图像缩放的增强参数
#数据增强的程度越大,可以提高模型的泛化能力,但也会增加训练时间和计算资源的消耗。
scale: 0.5 #图像缩放 image scale (+/- gain)
#设置裁剪的仿射矩阵系数。用于控制图像扭曲的增强参数
#数据增强的程度越大,可以提高模型的泛化能力,但也会增加训练时间和计算资源的消耗。
shear: 0.0 # 图像剪切 image shear (+/- deg)
#透视变换。是数据增强中的一种,它会对图像进行透视变换,使得图像看起来像是从不同的角度拍摄的。
#透视变换可以改变图像中物体的形状和位置,因此能够增加模型的鲁棒性和泛化能力。
#参数的值表示透视变换的程度,取值范围是0到0.001。
#当值为0时,表示不进行透视变换。当值为0.001时,表示进行最大程度的透视变换。
#通常情况下,可以将其设置为一个比较小的值,例如0.0005,以增加模型的泛化能力,同时不会对图像造成过大的扭曲。
perspective: 0.0 # 透明度 image perspective (+/- fraction), range 0-0.001
#上下翻转。用于控制图像上下翻转的增强概率
#它们的取值范围是从 0 到 1,表示翻转的概率
#flipud设置为 0.5 表示有 50% 的概率对图像进行上下翻转,设置为 0 表示不进行翻转,设置为 1 表示始终进行上下翻转
flipud: 0.0 #进行上下翻转概率 image flip up-down (probability)
#左右翻转。用于控制图像左右翻转的增强概率
#它们的取值范围是从 0 到 1,表示翻转的概率
#fliplr设置为 0.5 表示有 50% 的概率对图像进行左右翻转,设置为 0 表示不进行翻转,设置为 1 表示始终进行左右翻转
fliplr: 0.5 #进行左右翻转概率 image flip left-right (probability)
#用于控制数据增强的方式,可以用来增强训练集的多样性,从而提高模型的泛化性能。
#以一定的概率将四张图片拼接成一张,增强了模型对多物体的感知能力和位置估计能力,取值一般在0.1~1.0之间
mosaic: 1.0 #进行Mosaic概率 image mosaic (probability)
#用于控制数据增强的方式,可以用来增强训练集的多样性,从而提高模型的泛化性能。
#mixup: 以一定的概率对两张图片进行线性混合,增强了模型对物体形状和纹理的学习能力,取值一般在0.0~0.5之间
mixup: 0.0 #进行图像混叠概率(即,多张图像重叠在一起) image mixup (probability)#在mosaic启用时,才可以启用
#用于控制数据增强的方式,可以用来增强训练集的多样性,从而提高模型的泛化性能。
#copy_paste: 以一定的概率将一张图片的一部分复制到另一张图片上,增强了模型对物体的位置和尺度变化的鲁棒性。
#取值一般在0.0~0.5之间.这些取值表示概率,比如mosaic参数值为1,表示使用mosaic数据增强概率为100%,值为0.5则表示概率为50%
copy_paste: 0.0 # segment copy-paste (probability) # 在mosaic启用时,才可以启用
这里面很多都是进行数据增强的超参数,直接在这里面修改就可以进行想要的数据增强。通过train.py脚本训练的时候就会自己进行数据增强了。
1.2 添加新的数据增强方法,YOLOv5集成Albumentations。
2. 调整输入尺寸:
2.1. 尝试调整输入图像的尺寸
有时候更大的输入尺寸能够提高模型的精度,但需要权衡计算成本。在不同的输入尺寸下进行训练,找到最适合您任务的尺寸。
python train.py --img 640 --batch 16 --epochs 5 --data data.yaml --weights yolov5s.pt --workers 0
在用yolov5的train.py脚本训练的时候。可以用–img来调整输入图像的尺寸。这里的–img可以用’–imgsz’, ‘–img’, '–img-size’这些来替代。参考yolov5的train.py脚本中的参数设置。
3. 调整学习率和优化器:
3.1. 采用学习率调度策略,例如余弦退火调度。
修改学习率可以在ROOT / 'data/hyps/hyp.scratch-low.yaml'
文件中修改参数lr0
lr0: 0.01 # initial learning rate (SGD=1E-2, Adam=1E-3)
学习率的大小在训练深度学习模型中是一个关键的超参数,它直接影响模型的收敛速度和训练效果。不同大小的学习率适用于不同的情况,并具有各自的优点和缺点。
学习率较大的情况:
- 优点:
- 快速收敛: 大学习率使得模型参数更快地更新,因此有助于快速收敛到一个较优解。
- 跳出局部极小值:大学习率有助于模型跳出局部最小值,更有可能找到全局最小值。
- 适用场景:
- 较大数据集: 在大型数据集上,使用较大学习率可能更为有效,因为更多的数据可以提供更可靠的梯度信息。
- 预训练模型:在使用预训练模型进行微调时,较大学习率通常是常见的选择,以便更快地调整模型权重。
- 注意事项:
- 稳定性问题: 过大的学习率可能导致训练不稳定,甚至无法收敛。需要搭配合适的学习率调整策略,如学习率衰减或余弦退火,以提高训练稳定性。
学习率较小的情况:
- 优点:
- 稳定收敛: 较小学习率能够稳定地使模型收敛,减小收敛过程中的不稳定性。
- 更精细调整:> 较小学习率使得模型参数更新更为精细,有助于更精准地调整到最优解。
- 适用场景:
- 小数据集: 在小型数据集上,较小学习率通常更适用,因为数据量较小,不容易提供足够的梯度信息。
- 复杂任务:在处理复杂任务时,较小学习率可能更有助于模型更精细地学习复杂的特征。
- 注意事项:
- 训练时间: 较小学习率可能导致训练时间过长,因此需要在训练时间和模型性能之间进行权衡。
- 局部最小值:较小学习率可能使模型更容易陷入局部最小值,因此需要更谨慎地选择初始值。
在实践中,通常需要进行学习率调整的实验,以找到适合具体任务和数据集的学习率大小。此外,常用的学习率调整策略,如余弦退火调度或学习率衰减,有助于在训练过程中自适应地调整学习率。
3.2. 尝试不同的优化器,如Adam、SGD等,并调整学习率和动量参数。
- 尝试其他优化器,例如 Adam、SGD,以及它们的不同变体。某些优化器可能对不同的任务和模型结构更有效。
在用yolov5的train.py脚本训练的时候。可以用–optimizer参数来选择优化器。这里可选的优化器有’SGD’, ‘Adam’, ‘AdamW’。
这三个优化器(SGD、Adam、AdamW)在深度学习中都是常用的优化算法,它们各自有一些优点和适用范围。以下是它们的简要介绍:
SGD(Stochastic Gradient Descent):
优点: 相对简单,易于理解和实现。 在某些情况下,SGD 在凸优化问题中能够找到全局最优解。
适用范围: 较小数据集。 较简单的模型。
Adam(Adaptive Moment Estimation):
优点: 自适应学习率,能够针对每个参数调整学习率,适应不同参数的梯度变化。 具有动量项,有助于加速收敛。对于大多数情况,Adam表现良好,是一种强大的优化器。
适用范围: 广泛用于各种深度学习任务。 大规模数据集和复杂模型。需要更智能的学习率调整。
AdamW:
优点: 是Adam的一种改进版本,特别解决了Adam在权重衰减方面的一些问题。 使用“weight decay”(权重衰减)而不是L2正则化,更稳定。
适用范围: 类似于Adam,适用于大规模数据集和复杂模型。在存在大量参数(例如BERT等预训练模型)的情况下,AdamW可能表现更好。
3.3. 添加激活函数
- 添加方法
第一步:损失函数定义到YOLOv7或者YOLOv5的utils/activations.py中。
class GELU(nn.Module):
def __init__(self):
super(GELU, self).__init__()
def forward(self, x):
return 0.5 * x * (1 + torch.tanh(np.sqrt(2 / np.pi) * (x + 0.044715 * torch.pow(x, 3))))
第二步:common.py构建模块。重构Conv模块。
from utils.activations import GELU
class Conv(nn.Module):
# Standard convolution
def __init__(self, c1, c2, k=1, s=1, p=None, g=1, act=True): # ch_in, ch_out, kernel, stride, padding, groups
super(Conv, self).__init__()
#.conv = nn.Conv2d(c1, c2, k, s, autopad(k, p),dilation=1, groups=g, bias=False)#空洞卷积
self.conv = nn.Conv2d(c1, c2, k, s, autopad(k, p), groups=g, bias=False)
self.bn = nn.BatchNorm2d(c2)
self.act = GELU() if act is True else (act if isinstance(act, nn.Module) else nn.Identity())
def forward(self, x):
return self.act(self.bn(self.conv(x)))
def forward_fuse(self, x):
return self.act(self.conv(x))
第三步:将train.py中改为本文的yaml文件即可,开始训练,即可将原Conv中的激活函数改为GELU函数。
添加其他的激活函数也是同样的方法。
4. 调整损失函数:
解决问题:YOLOv5采用CIOU损失函数,优点:CIOU就是在DIOU的基础上增加了检测框尺度的loss,增加了长和宽的loss,这样预测框就会更加的符合真实框。但是缺点是:1. 纵横比描述的是相对值,存在一定的模糊 2. 未考虑难易样本的平衡问题。针对以上问题,采用EIOU的方法。原理:
CIOU Loss虽然考虑了边界框回归的重叠面积、中心点距离、纵横比。但是通过其公式中的v反映的纵横比的差异,而不是宽高分别与其置信度的真实差异,所以有时会阻碍模型有效的优化相似性。针对这一问题,有学者在CIOU的基础上将纵横比拆开,提出了EIOU Loss,并且加入Focal聚焦优质的锚框,该方法出自于2021年的一篇文章《Focal and Efficient IOU Loss for Accurate Bounding Box Regression》
尝试不同的损失函数,如Focal Loss等,以适应不同的任务需求。
4.1. 使用 Focal loss
在util/loss.py中,computeloss类用于计算损失函数
# Focal loss
g = h['fl_gamma'] # focal loss gamma
if g > 0:
BCEcls, BCEobj = FocalLoss(BCEcls, g), FocalLoss(BCEobj, g)
调参即可
fl_gamma
4.2.添加损失函数
方 法:
第一步修改general.py,增加SIOU。
elif EIoU:
w=(w1-w2)*(w1-w2)
h=(h1-h2)*(h1-h2)
return iou-(rho2/c2+w/(cw**2)+h/(ch**2))#EIOU 2021.12.29
第二步:将loss.py中边框位置回归损失函数改为eiou。
iou = bbox_iou(pbox.T, tbox[i], x1y1x2y2=False, EIoU=True) # iou(prediction, target)
添加其他损失函数类似。
5. 调整网络架构:
- 网络深度增加:尝试更深的网络结构,可以增加卷积层或模块的数量,但要注意过深可能导致训练和推理时间增加。
- 引入先进模块或注意力机制: 使用更先进的模块或注意力机制,如SENet、CBAM等,以提高网络学习到的特征的表示能力。
5.1. 详细操作步骤:
5.1.1. 增加网络深度和宽度:
- 打开YOLOv5的配置文件,一般是yolov5/models/yolov5.yaml。
- 在该文件中找到backbone部分,通常是CSPDarknet,可以看到depth_multiple和width_multiple参数。
- 增加网络深度可以通过增加depth_multiple参数的值,例如,从默认的0.33调整到0.67。该参数决定了网络的深度倍数。
- 您还可以调整width_multiple,控制网络的宽度倍数,但请注意增加宽度可能会增加计算成本。
- 保存配置文件后,重新运行训练脚本,例如train.py,确保新的配置生效。
优势:更深的网络结构通常能够提取更复杂的特征,提高模型的表征能力,有助于处理复杂场景。
注意:深层网络可能导致训练和推理时间增加,需要权衡性能和效率。
引入注意力机制:
5.1.2. 引入注意力机制:
- 打开models/common.py文件,这是YOLOv5中包含通用模块定义的文件。
- 导入您想要引入的注意力机制模块的定义,例如SENet或CBAM。确保这些模块与YOLOv5的结构兼容。
- 在YOLOv5的主干网络结构中,找到适当的位置插入这些模块。通常,在CSPDarknet结构中的卷积块后插入是一个不错的选择。
- 保存文件后,重新运行训练脚本,以应用新的注意力机制。
优势:SENet、CBAM等注意力机制能够增强模型对重要特征的关注,提升模型性能。
注意:引入过多的注意力机制可能导致模型复杂性上升,需根据任务和计算资源进行评估。
通过这些操作,你可以探索更深的网络结构和引入先进的模块,不断观察训练和验证指标,找到适合任务的最佳组合。
5.2. 其他内容待补充
6. 正负样本平衡:
在目标检测模型的训练中,负样本是指图像中不包含任何感兴趣目标的区域或物体。在YOLOv5或其他目标检测模型的训练中,引入负样本的主要目的是帮助模型学习正确地区分目标与背景,从而提高模型的准确性和泛化能力。
以下是为什么要加入负样本的一些原因:
- 背景学习: 负样本有助于模型学习背景的特征,使其能够正确地将目标与周围环境区分开。这有助于提高模型对目标的检测准确性,避免将背景错误地识别为目标。
- 增加鲁棒性: 加入负样本有助于提高模型的鲁棒性,使其能够在复杂场景中更好地应对噪声和干扰。通过引入各种背景和非目标区域,模型可以更好地适应各种环境。
- 避免假正例: 如果训练集中没有足够的负样本,模型可能会过于自信地认为所有区域都包含目标,导致假正例的增加。负样本的存在帮助模型更好地理解目标的上下文,降低错误检测的风险。
- 平衡类别分布: 为了保持训练集中正负样本的平衡,负样本的引入有助于避免模型对某个类别过度训练,从而提高整体性能。
- 避免过拟合: 如果负样本不足,模型可能会过于关注正样本,导致在训练集上表现很好,但在新数据上泛化能力差,产生过拟合问题。
负样本的数量:通常建议与正样本数量相当,或者至少要有一定比例的平衡。这是为了避免模型对某个类别过度训练,从而提高整体的模型性能。
7. 模型剪枝:
通过剪枝来裁剪我们的模型,达到在精度损失不大的情况下,提高模型速度的目的。
考虑对模型进行剪枝,去除冗余参数,以提高推理速度和减少模型的计算负担。
1.首先使用train.py进行正常训练:
python train.py --weights yolov5s.pt --adam --epochs 100
2.然后稀疏训练:
python train_sparsity.py --st --sr 0.0001 --weights yolov5s.pt --adam --epochs 100
sr的选择需要根据数据集调整,可以通过观察tensorboard的map,gamma变化直方图等选择。 在run/train/exp*/目录下:
3.训练完成后进行剪枝:
python prune.py --weights runs/train/exp1/weights/last.pt --percent 0.5 --cfg models/yolov5s.yaml
4.裁剪比例percent根据效果调整,可以从小到大试。注意cfg的模型文件需要和weights对应上,否则会出现运行prune 过程中出现键值不对应的问题,裁剪完成会保存对应的模型pruned_model.pt。
微调:
python finetune_pruned.py --weights pruned_model.pt --adam --epochs 100
8. 模型蒸馏:
考虑使用模型量化技术,将模型参数和激活值量化为较低位数,从而减小模型的内存占用和提高推理速度。
对于提高检测器的性能,我们除了可以从增加数据、修改模型结构、修改loss等模型本身的角度出发外,深度学习领域还有一个方式—蒸馏。简单的说,蒸馏就是让性能更强的模型(teacher, 参数量更大)来指导性能更弱student模型,从而提高student模型的性能。
蒸馏的方式有很多种,比较简单暴力的比如直接让student模型来拟合teacher模型的输出特征图,当然蒸馏也不是万能的,毕竟student模型和teacher模型的参数量有差距,student模型不一定能很好的学习teacher的知识,对于自己的任务有没有作用也需要尝试。
8.1.载入teacher模型
蒸馏首先需要有一个teacher模型,这个teacher模型一般和student同样结构,只是参数量更大、层数更多。比如对于yolov5,可以尝试用yolov5m来蒸馏yolov5s。
train.py增加一个命令行参数:
parser.add_argument("--teacher-weights", type=str, default="", help="initial weights path")
在train函数中载入teacher weights,过程与原有的载入过程类似,注意,DP或者DDP模型也要对teacher模型做对应的处理。
# teacher model
if opt.teacher_weights:
teacher_weights = opt.teacher_weights
# with torch_distributed_zero_first(rank):
# teacher_weights = attempt_download(teacher_weights) # download if not found locally
teacher_model = Model(teacher_weights, ch=3, nc=nc).to(device) # create
# load state_dict
ckpt = torch.load(teacher_weights, map_location=device) # load checkpoint
state_dict = ckpt["model"].float().state_dict() # to FP32
teacher_model.load_state_dict(state_dict, strict=True) # load
#set to eval
teacher_model.eval()
#set IDetect to train mode
# teacher_model.model[-1].train()
logger.info(f"Load teacher model from {teacher_weights}") # report
# DP mode
if cuda and rank == -1 and torch.cuda.device_count() > 1:
model = torch.nn.DataParallel(model)
if opt.teacher_weights:
teacher_model = torch.nn.DataParallel(teacher_model)
# SyncBatchNorm
if opt.sync_bn and cuda and rank != -1:
model = torch.nn.SyncBatchNorm.convert_sync_batchnorm(model).to(device)
logger.info("Using SyncBatchNorm()")
if opt.teacher_weights:
teacher_model = torch.nn.SyncBatchNorm.convert_sync_batchnorm(teacher_model).to(device)
teacher模型不进行梯度计算,因此:
if opt.teacher_weights:
for param in teacher_model.parameters():
param.requires_grad = False
蒸馏Loss:
蒸馏loss是计算teacher模型的一层或者多层与student的对应层的相似度,监督student模型向teacher模型靠近。对于yolov7,可以去监督三个特征层。
参考FGD的开源代码,我们在loss.py中增加一个FeatureLoss类, 参数暂时使用默认:
class FeatureLoss(nn.Module):
"""PyTorch version of `Feature Distillation for General Detectors`
Args:
student_channels(int): Number of channels in the student's feature map.
teacher_channels(int): Number of channels in the teacher's feature map.
temp (float, optional): Temperature coefficient. Defaults to 0.5.
name (str): the loss name of the layer
alpha_fgd (float, optional): Weight of fg_loss. Defaults to 0.001
beta_fgd (float, optional): Weight of bg_loss. Defaults to 0.0005
gamma_fgd (float, optional): Weight of mask_loss. Defaults to 0.0005
lambda_fgd (float, optional): Weight of relation_loss. Defaults to 0.000005
"""
def __init__(self,
student_channels,
teacher_channels,
temp=0.5,
alpha_fgd=0.001,
beta_fgd=0.0005,
gamma_fgd=0.001,
lambda_fgd=0.000005,
):
super(FeatureLoss, self).__init__()
self.temp = temp
self.alpha_fgd = alpha_fgd
self.beta_fgd = beta_fgd
self.gamma_fgd = gamma_fgd
self.lambda_fgd = lambda_fgd
if student_channels != teacher_channels:
self.align = nn.Conv2d(student_channels, teacher_channels, kernel_size=1, stride=1, padding=0)
else:
self.align = None
self.conv_mask_s = nn.Conv2d(teacher_channels, 1, kernel_size=1)
self.conv_mask_t = nn.Conv2d(teacher_channels, 1, kernel_size=1)
self.channel_add_conv_s = nn.Sequential(
nn.Conv2d(teacher_channels, teacher_channels//2, kernel_size=1),
nn.LayerNorm([teacher_channels//2, 1, 1]),
nn.ReLU(inplace=True), # yapf: disable
nn.Conv2d(teacher_channels//2, teacher_channels, kernel_size=1))
self.channel_add_conv_t = nn.Sequential(
nn.Conv2d(teacher_channels, teacher_channels//2, kernel_size=1),
nn.LayerNorm([teacher_channels//2, 1, 1]),
nn.ReLU(inplace=True), # yapf: disable
nn.Conv2d(teacher_channels//2, teacher_channels, kernel_size=1))
self.reset_parameters()
def forward(self,
preds_S,
preds_T,
gt_bboxes,
img_metas):
"""Forward function.
Args:
preds_S(Tensor): Bs*C*H*W, student's feature map
preds_T(Tensor): Bs*C*H*W, teacher's feature map
gt_bboxes(tuple): Bs*[nt*4], pixel decimal: (tl_x, tl_y, br_x, br_y)
img_metas (list[dict]): Meta information of each image, e.g.,
image size, scaling factor, etc.
"""
assert preds_S.shape[-2:] == preds_T.shape[-2:], 'the output dim of teacher and student differ'
device = gt_bboxes.device
self.to(device)
if self.align is not None:
preds_S = self.align(preds_S)
N,C,H,W = preds_S.shape
S_attention_t, C_attention_t = self.get_attention(preds_T, self.temp)
S_attention_s, C_attention_s = self.get_attention(preds_S, self.temp)
Mask_fg = torch.zeros_like(S_attention_t)
# Mask_bg = torch.ones_like(S_attention_t)
wmin,wmax,hmin,hmax = [],[],[],[]
img_h, img_w = img_metas
bboxes = gt_bboxes[:,2:6]
#xywh2xyxy
bboxes = xywh2xyxy(bboxes)
new_boxxes = torch.ones_like(bboxes)
new_boxxes[:, 0] = torch.floor(bboxes[:, 0]*W)
new_boxxes[:, 2] = torch.ceil(bboxes[:, 2]*W)
new_boxxes[:, 1] = torch.floor(bboxes[:, 1]*H)
new_boxxes[:, 3] = torch.ceil(bboxes[:, 3]*H)
#to int
new_boxxes = new_boxxes.int()
for i in range(N):
new_boxxes_i = new_boxxes[torch.where(gt_bboxes[:,0]==i)]
wmin.append(new_boxxes_i[:, 0])
wmax.append(new_boxxes_i[:, 2])
hmin.append(new_boxxes_i[:, 1])
hmax.append(new_boxxes_i[:, 3])
area = 1.0/(hmax[i].view(1,-1)+1-hmin[i].view(1,-1))/(wmax[i].view(1,-1)+1-wmin[i].view(1,-1))
for j in range(len(new_boxxes_i)):
Mask_fg[i][hmin[i][j]:hmax[i][j]+1, wmin[i][j]:wmax[i][j]+1] = \
torch.maximum(Mask_fg[i][hmin[i][j]:hmax[i][j]+1, wmin[i][j]:wmax[i][j]+1], area[0][j])
Mask_bg = torch.where(Mask_fg > 0, 0., 1.)
Mask_bg_sum = torch.sum(Mask_bg, dim=(1,2))
Mask_bg[Mask_bg_sum>0] /= Mask_bg_sum[Mask_bg_sum>0].unsqueeze(1).unsqueeze(2)
fg_loss, bg_loss = self.get_fea_loss(preds_S, preds_T, Mask_fg, Mask_bg,
C_attention_s, C_attention_t, S_attention_s, S_attention_t)
mask_loss = self.get_mask_loss(C_attention_s, C_attention_t, S_attention_s, S_attention_t)
rela_loss = self.get_rela_loss(preds_S, preds_T)
loss = self.alpha_fgd * fg_loss + self.beta_fgd * bg_loss \
+ self.gamma_fgd * mask_loss + self.lambda_fgd * rela_loss
return loss, loss.detach()
def get_attention(self, preds, temp):
""" preds: Bs*C*W*H """
N, C, H, W= preds.shape
value = torch.abs(preds)
# Bs*W*H
fea_map = value.mean(axis=1, keepdim=True)
S_attention = (H * W * F.softmax((fea_map/temp).view(N,-1), dim=1)).view(N, H, W)
# Bs*C
channel_map = value.mean(axis=2,keepdim=False).mean(axis=2,keepdim=False)
C_attention = C * F.softmax(channel_map/temp, dim=1)
return S_attention, C_attention
def get_fea_loss(self, preds_S, preds_T, Mask_fg, Mask_bg, C_s, C_t, S_s, S_t):
loss_mse = nn.MSELoss(reduction='sum')
Mask_fg = Mask_fg.unsqueeze(dim=1)
Mask_bg = Mask_bg.unsqueeze(dim=1)
C_t = C_t.unsqueeze(dim=-1)
C_t = C_t.unsqueeze(dim=-1)
S_t = S_t.unsqueeze(dim=1)
fea_t= torch.mul(preds_T, torch.sqrt(S_t))
fea_t = torch.mul(fea_t, torch.sqrt(C_t))
fg_fea_t = torch.mul(fea_t, torch.sqrt(Mask_fg))
bg_fea_t = torch.mul(fea_t, torch.sqrt(Mask_bg))
fea_s = torch.mul(preds_S, torch.sqrt(S_t))
fea_s = torch.mul(fea_s, torch.sqrt(C_t))
fg_fea_s = torch.mul(fea_s, torch.sqrt(Mask_fg))
bg_fea_s = torch.mul(fea_s, torch.sqrt(Mask_bg))
fg_loss = loss_mse(fg_fea_s, fg_fea_t)/len(Mask_fg)
bg_loss = loss_mse(bg_fea_s, bg_fea_t)/len(Mask_bg)
return fg_loss, bg_loss
def get_mask_loss(self, C_s, C_t, S_s, S_t):
mask_loss = torch.sum(torch.abs((C_s-C_t)))/len(C_s) + torch.sum(torch.abs((S_s-S_t)))/len(S_s)
return mask_loss
def spatial_pool(self, x, in_type):
batch, channel, width, height = x.size()
input_x = x
# [N, C, H * W]
input_x = input_x.view(batch, channel, height * width)
# [N, 1, C, H * W]
input_x = input_x.unsqueeze(1)
# [N, 1, H, W]
if in_type == 0:
context_mask = self.conv_mask_s(x)
else:
context_mask = self.conv_mask_t(x)
# [N, 1, H * W]
context_mask = context_mask.view(batch, 1, height * width)
# [N, 1, H * W]
context_mask = F.softmax(context_mask, dim=2)
# [N, 1, H * W, 1]
context_mask = context_mask.unsqueeze(-1)
# [N, 1, C, 1]
context = torch.matmul(input_x, context_mask)
# [N, C, 1, 1]
context = context.view(batch, channel, 1, 1)
return context
def get_rela_loss(self, preds_S, preds_T):
loss_mse = nn.MSELoss(reduction='sum')
context_s = self.spatial_pool(preds_S, 0)
context_t = self.spatial_pool(preds_T, 1)
out_s = preds_S
out_t = preds_T
channel_add_s = self.channel_add_conv_s(context_s)
out_s = out_s + channel_add_s
channel_add_t = self.channel_add_conv_t(context_t)
out_t = out_t + channel_add_t
rela_loss = loss_mse(out_s, out_t)/len(out_s)
return rela_loss
def last_zero_init(self, m):
if isinstance(m, nn.Sequential):
constant_init(m[-1], val=0)
else:
constant_init(m, val=0)
def reset_parameters(self):
kaiming_init(self.conv_mask_s, mode='fan_in')
kaiming_init(self.conv_mask_t, mode='fan_in')
self.conv_mask_s.inited = True
self.conv_mask_t.inited = True
self.last_zero_init(self.channel_add_conv_s)
self.last_zero_init(self.channel_add_conv_t)
其中hyp[‘xxx_kd_layers’]是用于指定我们要蒸馏的层序号。
为了提取出我们需要的层的特征图,我们还需要对模型推理的代码进行修改,这个放在下一篇,这一篇先把主要流程过一遍。
蒸馏训练
与普通loss一样,在训练中,首先计算蒸馏loss, 然后进行反向传播,区别只是计算蒸馏loss时需要使用teacher模型也对数据进行推理。
if opt.teacher_weights:
pred, features = model(imgs, extra_features = student_kd_layers) # forward
_, teacher_features = teacher_model(imgs, extra_features = teacher_kd_layers)
if "loss_ota" not in hyp or hyp["loss_ota"] == 1 and epoch >= ota_start:
loss, loss_items = compute_loss_ota(
pred, targets.to(device), imgs
)
else:
loss, loss_items = compute_loss(
pred, targets.to(device)
) # loss scaled by batch_size
# kd loss
loss_items = torch.cat((loss_items[0].unsqueeze(0), loss_items[1].unsqueeze(0), loss_items[2].unsqueeze(0), torch.zeros(1, device=device), loss_items[3].unsqueeze(0)))
loss_items[-1]*=imgs.shape[0]
for i in range(len(features)):
feature = features[i]
teacher_feature = teacher_features[i]
kd_loss, kd_loss_item = kd_losses[i](feature, teacher_feature, targets.to(device), [imgsz,imgsz])
loss += kd_loss
loss_items[3] += kd_loss_item
loss_items[4] += kd_loss_item
在这里,我们将kd_loss累加到了loss上。计算出总的loss,其他就与普通训练一样了。
9. 集成学习:
尝试使用集成学习,结合多个模型的预测结果,可以提高模型的鲁棒性和精度。
在组装之前,我们建立单个模型的基线性能。此命令在 COCO val2017 上以图像大小 640 像素测试
- 正常测试
YOLOv5x。 yolov5x.pt 是目前最大、最准确的模型。其他选项包括 yolov5s.pt 、 yolov5m.pt 和 yolov5l.pt ,或者您拥有训练自定义数据集的检查点 ./weights/best.pt 。
python val.py --weights yolov5x.pt --data coco.yaml --img 640 --half
输出:
val: data=./data/coco.yaml, weights=['yolov5x.pt'], batch_size=32, imgsz=640, conf_thres=0.001, iou_thres=0.65, task=val, device=, single_cls=False, augment=False, verbose=False, save_txt=False, save_hybrid=False, save_conf=False, save_json=True, project=runs/val, name=exp, exist_ok=False, half=True YOLOv5 🚀 v5.0-267-g6a3ee7c torch
1.9.0+cu102 CUDA:0 (Tesla P100-PCIE-16GB, 16280.875MB)
Fusing layers... Model Summary: 476 layers, 87730285 parameters, 0 gradients
val: Scanning '../datasets/coco/val2017' images and labels...4952 found, 48 missing, 0 empty, 0 corrupted: 100% 5000/5000 [00:01<00:00,
2846.03it/s] val: New cache created: ../datasets/coco/val2017.cache
Class Images Labels P R mAP@.5 mAP@.5:.95: 100% 157/157 [02:30<00:00, 1.05it/s]
all 5000 36335 0.746 0.626 0.68 0.49 Speed: 0.1ms pre-process, 22.4ms inference, 1.4ms NMS per image at shape (32, 3, 640, 640) # <--- baseline speed
Evaluating pycocotools mAP... saving runs/val/exp/yolov5x_predictions.json... ... Average Precision (AP) @[ IoU=0.50:0.95 | area= all | maxDets=100 ] = 0.504 # <--- baseline mAP Average Precision (AP) @[ IoU=0.50 | area= all | maxDets=100 ] = 0.688
……
- 集成测试
在测试和推理时,只需将额外的模型附加到任何现有 val.py 或 detect.py 命令中的 --weights 参数中,就可以将多个预训练模型集成在一起。此示例一起测试 2 个模型的集合:
- YOLOv5x
- YOLOv5l6
python val.py --weights yolov5x.pt yolov5l6.pt --data coco.yaml --img 640 --half
输出:
val: data=./data/coco.yaml, weights=['yolov5x.pt', 'yolov5l6.pt'], batch_size=32, imgsz=640, conf_thres=0.001, iou_thres=0.6, task=val, device=, single_cls=False, augment=False, verbose=False, save_txt=False, save_hybrid=False, save_conf=False, save_json=True, project=runs/val, name=exp, exist_ok=False, half=True
YOLOv5 🚀 v5.0-267-g6a3ee7c torch 1.9.0+cu102 CUDA:0 (Tesla P100-PCIE-16GB, 16280.875MB)
Fusing layers...
Model Summary: 476 layers, 87730285 parameters, 0 gradients # Model 1
Fusing layers...
Model Summary: 501 layers, 77218620 parameters, 0 gradients # Model 2
Ensemble created with ['yolov5x.pt', 'yolov5l6.pt'] # Ensemble notice
val: Scanning '../datasets/coco/val2017.cache' images and labels... 4952 found, 48 missing, 0 empty, 0 corrupted: 100% 5000/5000 [00:00<00:00, 49695545.02it/s]
Class Images Labels P R mAP@.5 mAP@.5:.95: 100% 157/157 [03:58<00:00, 1.52s/it]
all 5000 36335 0.747 0.637 0.692 0.502
Speed: 0.1ms pre-process, 39.5ms inference, 2.0ms NMS per image at shape (32, 3, 640, 640) # <--- ensemble speed
Evaluating pycocotools mAP... saving runs/val/exp3/yolov5x_predictions.json...
...
Average Precision (AP) @[ IoU=0.50:0.95 | area= all | maxDets=100 ] = 0.515 # <--- ensemble mAP
Average Precision (AP) @[ IoU=0.50 | area= all | maxDets=100 ] = 0.699
Average Precision (AP) @[ IoU=0.75 | area= all | maxDets=100 ] = 0.557
……
在实施这些建议时,请在验证集上进行评估,以确保模型在提高精度的同时不会过拟合或造成其他问题。