优化策略

  • 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. 增加数据集的多样性
    包括更多的背景、不同光照条件、尺度变化等。

在数据集中添加更多具有不同背景的图像,以确保模型在各种环境中都能正常工作。 引入具有不同光照条件的图像,使模型能够适应不同的亮度和阴影。
包含具有不同尺度和比例的目标,以确保模型对各种目标尺寸的适应性。

  1. 尝试使用不同的数据增强技术

例如旋转、缩放、裁剪、亮度调整等,以提高模型对于各种场景的适应性。

1.2. 超参数修改

在train.py脚本中可以看到,训练时默认的超参数(Hyperparameter)在ROOT / 'data/hyps/hyp.scratch-low.yaml'这个位置的这个hyp.scratch-low.yaml文件中。

目标检测中小目标一般是多少百分比_计算机视觉


可以看到这个文件夹中还有其他的有关超参数设置的文件。

目标检测中小目标一般是多少百分比_计算机视觉_02

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脚本中的参数设置。

目标检测中小目标一般是多少百分比_人工智能_03

3. 调整学习率和优化器:

3.1. 采用学习率调度策略,例如余弦退火调度。

修改学习率可以在ROOT / 'data/hyps/hyp.scratch-low.yaml'文件中修改参数lr0

lr0: 0.01  # initial learning rate (SGD=1E-2, Adam=1E-3)

学习率的大小在训练深度学习模型中是一个关键的超参数,它直接影响模型的收敛速度和训练效果。不同大小的学习率适用于不同的情况,并具有各自的优点和缺点。

学习率较大的情况:

  1. 优点
  • 快速收敛: 大学习率使得模型参数更快地更新,因此有助于快速收敛到一个较优解。
  • 跳出局部极小值:大学习率有助于模型跳出局部最小值,更有可能找到全局最小值。
  1. 适用场景
  • 较大数据集: 在大型数据集上,使用较大学习率可能更为有效,因为更多的数据可以提供更可靠的梯度信息。
  • 预训练模型:在使用预训练模型进行微调时,较大学习率通常是常见的选择,以便更快地调整模型权重。
  1. 注意事项
  • 稳定性问题: 过大的学习率可能导致训练不稳定,甚至无法收敛。需要搭配合适的学习率调整策略,如学习率衰减或余弦退火,以提高训练稳定性。

学习率较小的情况:

  1. 优点
  • 稳定收敛: 较小学习率能够稳定地使模型收敛,减小收敛过程中的不稳定性。
  • 更精细调整:> 较小学习率使得模型参数更新更为精细,有助于更精准地调整到最优解。
  1. 适用场景
  • 小数据集: 在小型数据集上,较小学习率通常更适用,因为数据量较小,不容易提供足够的梯度信息。
  • 复杂任务:在处理复杂任务时,较小学习率可能更有助于模型更精细地学习复杂的特征。
  1. 注意事项
  • 训练时间: 较小学习率可能导致训练时间过长,因此需要在训练时间和模型性能之间进行权衡。
  • 局部最小值:较小学习率可能使模型更容易陷入局部最小值,因此需要更谨慎地选择初始值。

在实践中,通常需要进行学习率调整的实验,以找到适合具体任务和数据集的学习率大小。此外,常用的学习率调整策略,如余弦退火调度或学习率衰减,有助于在训练过程中自适应地调整学习率。

3.2. 尝试不同的优化器,如Adam、SGD等,并调整学习率和动量参数。

  1. 尝试其他优化器,例如 Adam、SGD,以及它们的不同变体。某些优化器可能对不同的任务和模型结构更有效。

在用yolov5的train.py脚本训练的时候。可以用–optimizer参数来选择优化器。这里可选的优化器有’SGD’, ‘Adam’, ‘AdamW’。

目标检测中小目标一般是多少百分比_计算机视觉_04

这三个优化器(SGD、Adam、AdamW)在深度学习中都是常用的优化算法,它们各自有一些优点和适用范围。以下是它们的简要介绍:

SGD(Stochastic Gradient Descent):

优点: 相对简单,易于理解和实现。 在某些情况下,SGD 在凸优化问题中能够找到全局最优解。
适用范围: 较小数据集。 较简单的模型。

Adam(Adaptive Moment Estimation):

优点: 自适应学习率,能够针对每个参数调整学习率,适应不同参数的梯度变化。 具有动量项,有助于加速收敛。对于大多数情况,Adam表现良好,是一种强大的优化器。
适用范围: 广泛用于各种深度学习任务。 大规模数据集和复杂模型。需要更智能的学习率调整。

AdamW:

优点: 是Adam的一种改进版本,特别解决了Adam在权重衰减方面的一些问题。 使用“weight decay”(权重衰减)而不是L2正则化,更稳定。
适用范围: 类似于Adam,适用于大规模数据集和复杂模型。在存在大量参数(例如BERT等预训练模型)的情况下,AdamW可能表现更好。

3.3. 添加激活函数

  1. 添加方法

第一步:损失函数定义到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等,以提高网络学习到的特征的表示能力。

目标检测中小目标一般是多少百分比_YOLO_05

5.1. 详细操作步骤:

5.1.1. 增加网络深度和宽度:

  1. 打开YOLOv5的配置文件,一般是yolov5/models/yolov5.yaml。
  2. 在该文件中找到backbone部分,通常是CSPDarknet,可以看到depth_multiple和width_multiple参数。
  3. 增加网络深度可以通过增加depth_multiple参数的值,例如,从默认的0.33调整到0.67。该参数决定了网络的深度倍数。
  4. 您还可以调整width_multiple,控制网络的宽度倍数,但请注意增加宽度可能会增加计算成本。
  5. 保存配置文件后,重新运行训练脚本,例如train.py,确保新的配置生效。

优势:更深的网络结构通常能够提取更复杂的特征,提高模型的表征能力,有助于处理复杂场景。
注意:深层网络可能导致训练和推理时间增加,需要权衡性能和效率。
引入注意力机制:

5.1.2. 引入注意力机制:

  1. 打开models/common.py文件,这是YOLOv5中包含通用模块定义的文件。
  2. 导入您想要引入的注意力机制模块的定义,例如SENet或CBAM。确保这些模块与YOLOv5的结构兼容。
  3. 在YOLOv5的主干网络结构中,找到适当的位置插入这些模块。通常,在CSPDarknet结构中的卷积块后插入是一个不错的选择。
  4. 保存文件后,重新运行训练脚本,以应用新的注意力机制。

优势:SENet、CBAM等注意力机制能够增强模型对重要特征的关注,提升模型性能。
注意:引入过多的注意力机制可能导致模型复杂性上升,需根据任务和计算资源进行评估。
通过这些操作,你可以探索更深的网络结构和引入先进的模块,不断观察训练和验证指标,找到适合任务的最佳组合。

5.2. 其他内容待补充

6. 正负样本平衡:

在目标检测模型的训练中,负样本是指图像中不包含任何感兴趣目标的区域或物体。在YOLOv5或其他目标检测模型的训练中,引入负样本的主要目的是帮助模型学习正确地区分目标与背景,从而提高模型的准确性和泛化能力。

以下是为什么要加入负样本的一些原因:

  1. 背景学习: 负样本有助于模型学习背景的特征,使其能够正确地将目标与周围环境区分开。这有助于提高模型对目标的检测准确性,避免将背景错误地识别为目标。
  2. 增加鲁棒性: 加入负样本有助于提高模型的鲁棒性,使其能够在复杂场景中更好地应对噪声和干扰。通过引入各种背景和非目标区域,模型可以更好地适应各种环境。
  3. 避免假正例: 如果训练集中没有足够的负样本,模型可能会过于自信地认为所有区域都包含目标,导致假正例的增加。负样本的存在帮助模型更好地理解目标的上下文,降低错误检测的风险。
  4. 平衡类别分布: 为了保持训练集中正负样本的平衡,负样本的引入有助于避免模型对某个类别过度训练,从而提高整体性能。
  5. 避免过拟合: 如果负样本不足,模型可能会过于关注正样本,导致在训练集上表现很好,但在新数据上泛化能力差,产生过拟合问题。

负样本的数量:通常建议与正样本数量相当,或者至少要有一定比例的平衡。这是为了避免模型对某个类别过度训练,从而提高整体的模型性能。

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 像素测试

  1. 正常测试
    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
 ……
  1. 集成测试
    在测试和推理时,只需将额外的模型附加到任何现有 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
 ……

在实施这些建议时,请在验证集上进行评估,以确保模型在提高精度的同时不会过拟合或造成其他问题。