网站建设基础服务,南京 百度 网站建设,做平面设计素材的哪个网站好,湛江市律师网站建设品牌1. 前置知识
1.1 YOLO 算法的基本思想 首先通过特征提取网络对输入图像提取特征#xff0c;得到一定大小的特征图#xff0c;比如 13x13#xff08;相当于416x416 图片大小#xff09;#xff0c;然后将输入图像分成 13x13 个 grid cells#xff1a;
YOLOv3/v4#xf…1. 前置知识
1.1 YOLO 算法的基本思想 首先通过特征提取网络对输入图像提取特征得到一定大小的特征图比如 13x13相当于416x416 图片大小然后将输入图像分成 13x13 个 grid cells
YOLOv3/v4如果 GT 中某个目标的中心坐标落在哪个 grid cell 中那么就由该 grid cell 来预测该目标。每个 grid cell 都会预测 3 个不同尺度的边界框。YOLOv5不同于 YOLOv3/v4其 GT 可以跨层预测即有些 bboxanchors在多个预测层都算正样本匹配数的正样本范围可以是 3-9 个。
预测得到的输出特征图有两个维度是提取到的特征的维度比如 13x13还有一个维度深度是 Bx(5C)其中
B 表示每个 grid cell 预测的边界框的数量YOLOv3/v4中是 3 个C 表示边界框的类别数没有背景类所以对于 VOC 数据集是 205 表示 4 个坐标信息和一个目标性得分objectness score
1.2 损失函数
Classification Loss 用于衡量模型对目标的分类准确性。计算方式通常使用交叉熵损失函数该函数衡量模型的分类输出与实际类别之间的差异。对于 YOLOv5每个目标都有一个对应的类别分类损失量化了模型对每个目标类别的分类准确性。 Localization Loss定位损失预测边界框与 GT 之间的误差 用于衡量模型对目标位置的预测准确性。YOLOv5 中采用的是均方差Mean Squared ErrorMSE损失函数衡量模型对目标边界框坐标的回归预测与实际边界框之间的差异。定位损失关注模型对目标位置的精确度希望模型能够准确地定位目标的边界框。 Confidence Loss置信度损失框的目标性 Objectness of the box 用于衡量模型对目标存在与否的预测准确性。YOLOv5 中采用的是二元交叉熵损失函数该函数衡量模型对目标存在概率的预测与实际目标存在的二元标签之间的差异。置信度损失考虑了模型对每个边界框的目标置信度以及是否包含目标的预测。该损失鼓励模型提高对包含目标的边界框的预测概率同时减小对不包含目标的边界框的预测概率。
总的损失函数 L o s s α × C l a s s i f i c a t i o n L o s s β × L o c a l i z a t i o n L o s s γ × C o n f i d e n c e L o s s \rm Loss \alpha \times Classification Loss \beta \times Localization Loss \gamma \times Confidence Loss Lossα×ClassificationLossβ×LocalizationLossγ×ConfidenceLoss
1.3 PyTorch2ONNX
Netron 对 .pt 格式的兼容性不好直接打卡无法显示整个网络。因此我们可以使用 YOLOv5 中的 models/export.py 脚本将 .pt 权重转换为 .onnx 格式再使用 Netron 打开就可以完整地查看 YOLOv5 的整体架构了。
python export.py \--weights weights/yolov5s.pt \--imgsz 640 \--batch-size 1 \--device cpu \--simplify \--include onnx详细可选参数见 export.py 文件 1.4 YOLOv5 模型结构图 图片来源霹雳吧啦Wz 2. 配置文件
在 models 中的 .yaml 文件是模型的配置文件
models
├── __init__.py
├── tf.py
├── yolo.py
├── yolov5l.yaml
├── yolov5m.yaml
├── yolov5n.yaml
├── yolov5s.yaml
└── yolov5x.yaml我们以 yolov5s.yaml 为例展开讲解。
2.1 模型深度系数 depth_multiple 和宽度系数 width_multiple
# Parameters
nc: 80 # number of classes | 类别数
depth_multiple: 0.33 # model depth multiple | 模型深度: 控制 BottleneckCSP 数
width_multiple: 0.50 # layer channel multiple | 模型宽度: 控制 Conv 通道个数卷积核数量depth_multiple 表示 BottleneckCSP、C3 等层缩放因子将所有的 BottleneckCSP、C3等 模块的 Bottleneck 子模块 乘上该参数得到最终的 Bottleneck 子模块个数width_multiple 表示卷积通道的缩放因子就是将配置里的 backbone 和 head 部分其实就是所有的有关 Conv 的通道都需要乘上该系数
通过 depth_multiple 和 width_multiple 参数可以实现不同复杂度的模型设计yolov5x、yolov5s、yolov5n、yolov5m、yolov5l。 BottleneckCSP 和 C3 的结构示意图 BotteleneckCSP 结构 BotteleneckCSP 图片来源: 深入浅出Yolo系列之Yolov5核心基础知识完整讲解 C3 结构 2.2 anchors | 先验框大小
anchors:- [10, 13, 16, 30, 33, 23] # P3/8- [30, 61, 62, 45, 59, 119] # P4/16- [116, 90, 156, 198, 373, 326] # P5/32上面就定义了三种尺寸的先验框的大小其中
P3/8P3 是层的名称8 表示此时特征图经过的下采样大小 → P3 特征图此时已经过了 8 倍下采样P4/16P4 特征图此时已经过了 16 倍下采样P5/32P5 特征图此时已经过了 32 倍下采样 在 YOLOv5 中P3 代表 Feature Pyramid Network (FPN) 的第三个级别。FPN 是一种用于目标检测的特征提取网络结构它通过在不同层级的特征图上应用卷积和上采样操作以获取具有不同尺度和语义信息的特征图。这些特征图可以用于检测不同大小的目标。 在这个模型配置文件中P3/8 表示 P3 层在输入图像上的缩放因子为 8。缩放因子指的是在输入图像上的每个像素点在 P3 层特征图上所对应的尺寸。通过这种缩放可以使得 P3 层特征图的尺寸相对于输入图像缩小 8 倍。这种缩放操作帮助模型捕获不同尺度的目标信息。 2.3 backbone
# YOLOv5 v6.0 backbone
backbone:# [from, number, module, args][[-1, 1, Conv, [64, 6, 2, 2]], # 0-P1/2[-1, 1, Conv, [128, 3, 2]], # 1-P2/4[-1, 3, C3, [128]],[-1, 1, Conv, [256, 3, 2]], # 3-P3/8[-1, 6, C3, [256]],[-1, 1, Conv, [512, 3, 2]], # 5-P4/16[-1, 9, C3, [512]],[-1, 1, Conv, [1024, 3, 2]], # 7-P5/32[-1, 3, C3, [1024]],[-1, 1, SPPF, [1024, 5]], # 9]首先第一行的备注信息已经告诉我们了这个 backbone 是 YOLOv5 和 YOLOv6 的 backbone。第二行中有对每一列的说明其中
from表示输入的来源。-1 表示前一层的输出作为输入。number表示重复使用该模块的次数。module表示使用的特征提取模块类型。args表示模块的参数 Conv 层输出通道数、卷积核大小、步幅和填充C3 层输出通道数SPPF 层表示输出通道数和池化的 kernel_size。 注意 在之前的版本v4.0中backbone 的第一层是一个 Focus 层但现在是一个卷积层。对于 C3 层而言如果重复了 3 次且 stride2那么只有第一个 C3 模块会进行两倍下采样剩下的两个 C3 模块不会进行下采样操作 〔与模型深度系数 depth_multiple 和宽度系数 width_multiple 的联系〕
前面说过了 depth_multiple 和 width_multiple 这两个参数的作用对于 YOLOv5-s 的 C3 层而言此时的 depth_multiple0.33那么第二列的 C3 层个数并不是实际上的数量实际上的数量还得乘上 depth_multiple
# YOLOv5 v6.0 backbone
backbone:# [from, number, module, args][[-1, 1, Conv, [64, 6, 2, 2]], # 0-P1/2[-1, 1, Conv, [128, 3, 2]], # 1-P2/4[-1, 3, C3, [128]], # 3*0.330.99 --------- 实际使用1个C3[-1, 1, Conv, [256, 3, 2]], # 3-P3/8[-1, 6, C3, [256]], # 6*0.331.98 --------- 实际使用2个C3[-1, 1, Conv, [512, 3, 2]], # 5-P4/16[-1, 9, C3, [512]], # 9*0.332.97 --------- 实际使用3个C3[-1, 1, Conv, [1024, 3, 2]], # 7-P5/32[-1, 3, C3, [1024]], # 3*0.330.99 --------- 实际使用1个C3[-1, 1, SPPF, [1024, 5]], # 9]Question这个计算是怎么进行的 Answer在 models/yolo.py 的 parse_model() 函数中有写
# 对 backbone 和 head 中的所有层进行遍历
for i, (f, n, m, args) in enumerate(d[backbone] d[head]): # f - from表示输入的来源。-1 表示前一层的输出作为输入。# n - number表示重复使用该模块的次数。# m - module表示使用的特征提取模块类型。# args表示模块的参数# 将字符串转换为对应的代码名称不懂的看一下 eval 函数m eval(m) if isinstance(m, str) else m # 遍历每一层的参数argsfor j, a in enumerate(args):# j: 参数的索引# a: 具体的参数with contextlib.suppress(NameError):# 将数字或字符长转换为代码args[j] eval(a) if isinstance(a, str) else a # eval strings# 先将所有的 number 乘上 深度系数n n_ max(round(n * gd), 1) if n 1 else n # depth gain这个根据向上取整的操作并确保结果至少为 1
那么对于 width_multiple 系数而言也是一样的在 YOLOv5s 中, width_multiple0.50
# YOLOv5 v6.0 backbone
backbone:# [from, number, module, args][[-1, 1, Conv, [64, 6, 2, 2]], # 0-P1/2 ---------- 64 * 0.5 32[-1, 1, Conv, [128, 3, 2]], # 1-P2/4 ---------- 128 * 0.5 64[-1, 3, C3, [128]], # ---------- 128 * 0.5 64[-1, 1, Conv, [256, 3, 2]], # 3-P3/8 ---------- 256 * 0.5 128[-1, 6, C3, [256]], # ---------- 256 * 0.5 128[-1, 1, Conv, [512, 3, 2]], # 5-P4/16 ---------- 512 * 0.5 256[-1, 9, C3, [512]], # ---------- 512 * 0.5 256[-1, 1, Conv, [1024, 3, 2]], # 7-P5/32 ---------- 1024* 0.5 512[-1, 3, C3, [1024]], # ---------- 1024* 0.5 512[-1, 1, SPPF, [1024, 5]], # 9 ---------- 1024* 0.5 512]意思就是说将所有的卷积层都乘上 width_multiple那我们看一下代码细节还是在 models/yolo.py - parse_model() 中
# 对 backbone 和 head 中的所有层进行遍历
for i, (f, n, m, args) in enumerate(d[backbone] d[head]): # f - from表示输入的来源。-1 表示前一层的输出作为输入。# n - number表示重复使用该模块的次数。# m - module表示使用的特征提取模块类型。# args表示模块的参数# 将字符串转换为对应的代码名称不懂的看一下 eval 函数m eval(m) if isinstance(m, str) else m # 遍历每一层的参数argsfor j, a in enumerate(args):# j: 参数的索引# a: 具体的参数with contextlib.suppress(NameError):# 将数字或字符长转换为代码args[j] eval(a) if isinstance(a, str) else a # eval strings# 先将所有的 number 乘上 深度系数n n_ max(round(n * gd), 1) if n 1 else n # depth gain# 判断当前模块是否在这个字典中if m in {Conv, # Conv BN SiLUGhostConv, # 华为在 GhostNet 中提出的Ghost卷积Bottleneck, # ResNet同款GhostBottleneck, # 将其中的3x3卷积替换为GhostConvSPP, # Spatial Pyramid PoolingSPPF, # SPP ConvDWConv, # 深度卷积MixConv2d, # 一种多尺度卷积层可以在不同尺度上进行卷积操作。它使用多个不同大小的卷积核对输入特征图进行卷积并将结果进行融合Focus, # 一种特征聚焦层用于减少计算量并增加感受野。它通过将输入特征图进行通道重排和降采样操作以获取更稠密和更大感受野的特征图CrossConv, # 一种交叉卷积层用于增加特征图的多样性。它使用不同大小的卷积核对输入特征图进行卷积并将结果进行融合BottleneckCSP, # 一种基于残差结构的卷积块由连续的Bottleneck模块和CSPCross Stage Partial结构组成用于构建深层网络提高特征提取能力C3, # 一种卷积块由三个连续的卷积层组成。它用于提取特征并增加网络的非线性能力C3TR, # C3TR是C3的变体它在C3的基础上添加了Transpose卷积操作。Transpose卷积用于将特征图的尺寸进行上采样C3SPP, # C3SPP是C3的变体它在C3的基础上添加了SPP操作。这样可以在不同尺度上对特征图进行池化并增加网络的感受野C3Ghost, # C3Ghost是一种基于C3模块的变体它使用GhostConv代替传统的卷积操作nn.ConvTranspose2d, # 转置卷积DWConvTranspose2d, # DWConvTranspose2d是深度可分离的转置卷积层用于进行上采样操作。它使用逐点卷积进行特征图的通道之间的信息整合以减少计算量C3x, # C3x是一种改进的C3模块它在C3的基础上添加了额外的操作如注意力机制或其他模块。这样可以进一步提高网络的性能}:c1, c2 ch[f], args[0] # c1: 卷积的输入通道数, c2: 卷积的输出通道数 | ch[f] 上一次的输出通道数即本层的输入通道数args[0]配置文件中想要的输出通道数if c2 ! no: # if not outputc2 make_divisible(c2 * gw, ch_mul) # 让输出通道数*width_multipleargs [c1, c2, *args[1:]] # 此时的c2已经是修改后的c2乘上width_multiple的c2了 | *args[1:]将其他非输出通道数的参数解包# 如果当前层是 BottleneckCSP, C3, C3TR, C3Ghost, C3x 中的一种这些结构都有 Bottleneck 结构if m in {BottleneckCSP, C3, C3TR, C3Ghost, C3x}:args.insert(2, n) # number of repeats | 需要让Bottleneck重复n次n 1 # 重置n其他层没有 Bottleneck 的模块不需要重复# 如果是BN层elif m is nn.BatchNorm2d:args [ch[f]] # 确定输出通道数# 如果是 Concat 层elif m is Concat:c2 sum(ch[x] for x in f) # Concat是按着通道维度进行的所以通道会增加2.4 head
# YOLOv5 v6.0 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)]Tips
列的定义和 backbone 是一样的不像 YOLOv3 那样作者区分了 Neck 和 Head。YOLOv5 的作者没有做出区分只有 Head所以在 Head 部分中包含了 PANet 和 Detect 部分。
QuestionConcat 怎么理解 Answer我们看下面的图。 这里的 Concat 就是把浅层的特征图与当前特征图进行拼接沿通道维度我们看一下源码在 models/common.py - Concat 中
class Concat(nn.Module):# Concatenate a list of tensors along dimensiondef __init__(self, dimension1):super().__init__()self.d dimensiondef forward(self, x):# 这里的 x 是一个 list所以可以有多个 Tensor 进行拼接return torch.cat(x, self.d)这里需要注意的其实就是 from即谁和谁拼接下面是解释
# YOLOv5 v6.0 backbone
backbone:# [from, number, module, args][[-1, 1, Conv, [64, 6, 2, 2]], # 0-P1/2[-1, 1, Conv, [128, 3, 2]], # 1-P2/4[-1, 3, C3, [128]], # 2[-1, 1, Conv, [256, 3, 2]], # 3-P3/8[-1, 6, C3, [256]], # 4[-1, 1, Conv, [512, 3, 2]], # 5-P4/16[-1, 9, C3, [512]], # 6[-1, 1, Conv, [1024, 3, 2]], # 7-P5/32[-1, 3, C3, [1024]], # 8[-1, 1, SPPF, [1024, 5]], # 9]# YOLOv5 v6.0 head
head: [[-1, 1, Conv, [512, 1, 1]], # 10[-1, 1, nn.Upsample, [None, 2, nearest]], # 11[[-1, 6], 1, Concat, [1]], # cat backbone P4 # 12[-1, 3, C3, [512, False]], # 13[-1, 1, Conv, [256, 1, 1]], # 14[-1, 1, nn.Upsample, [None, 2, nearest]], # 15[[-1, 4], 1, Concat, [1]], # cat backbone P3 # 16[-1, 3, C3, [256, False]], # 17 (P3/8-small)[-1, 1, Conv, [256, 3, 2]], # 18[[-1, 14], 1, Concat, [1]], # cat head P4 # 19[-1, 3, C3, [512, False]], # 20 (P4/16-medium)[-1, 1, Conv, [512, 3, 2]], # 21[[-1, 10], 1, Concat, [1]], # cat head P5 # 22[-1, 3, C3, [1024, False]], # 23 (P5/32-large)[[17, 20, 23], 1, Detect, [nc, anchors]], # Detect(P3, P4, P5) # 24]我们看第一个 Concat[[-1, 6], 1, Concat, [1]]
-1 表示上一层即 Concat 的前一层6 表示第 6 层即 backbone 中的 [-1, 9, C3, [512]]。
剩下的以此类推。 ⚠️ 这里的索引是从 0 开始的 在 Head 中P 其实对应的是检测头对应的输出层。比如说 P3 就是 8 倍下采样的输出层。我们常用的是 P3P4P5。为了捕获更小的目标我们可以使用 models/hub/yolov5-p2.yaml 这个模型 YOLOv5 模型架构图 2.5 不同规格模型配置
Modelsize(pixels)mAPval50-95mAPval50SpeedCPU b1(ms)SpeedV100 b1(ms)SpeedV100 b32(ms)params(M)FLOPs640 (B)YOLOv5n64028.045.7456.30.61.94.5YOLOv5s64037.456.8986.40.97.216.5YOLOv5m64045.464.12248.21.721.249.0YOLOv5l64049.067.343010.12.746.5109.1YOLOv5x64050.768.976612.14.886.7205.7YOLOv5n6128036.054.41538.12.13.24.6YOLOv5s6128044.863.73858.23.612.616.8YOLOv5m6128051.369.388711.16.835.750.0YOLOv5l6128053.771.3178415.810.576.8111.4YOLOv5x6 [TTA]1280153655.055.872.772.73136-26.2-19.4-140.7-209.8-
QuestionYOLOv5s 和 YOLOv5s6 有什么区别 Answer在YOLOv5中x6表示YOLOv5的最大版本并且具有更深和更宽的网络结构。可见 issue - What is the difference between YOLOv5s and YOLOv5s6? #12499
Question[TTA] 是什么 AnswerTTATest Time Augmentation是一种在测试时应用数据增强的技术。在目标检测任务中通常会在训练时应用数据增强如随机裁剪、旋转、缩放等来增加训练样本的多样性从而提高模型的鲁棒性和泛化能力。而在测试时为了进一步提高模型的性能可以应用一些额外的数据增强操作。TTA通过对输入图像进行多种增强操作生成多个预测结果并对这些结果进行综合以提高目标检测模型的性能。可见官方介绍文档 - 测试时间增强TTA
3. 网络架构
3.1 〔已废弃〕Focus
Focus 模块是 YOLOv5 中的一种卷积块主要用于减少计算量和参数数量并且能够保持较好的感受野。它通过将输入张量进行切分和重排来实现这一目标。目前最新的 YOLOv5 已不再使用该模块而是使用一个 kernel6, stride2, padding2 的 CBS 模块进行了替代。Focus 模块示意图如下所示。 CBS: Conv - BN - SiLU Focus 模块示意图 左边是原始输入Focus 模块会把数据切分为 4 份每份数据相当于是经过 2 倍下采样得到的然后再在 Channel 维度进行拼接最后再进行卷积操作。
我们看一下 Focus 模块的源码
class Focus(nn.Module):# Focus wh information into c-space | 将宽高信息聚焦到通道维度中def __init__(self, c1, c2, k1, s1, pNone, g1, actTrue): # ch_in, ch_out, kernel, stride, padding, groupssuper().__init__()self.conv Conv(c1 * 4, c2, k, s, p, g, actact)# self.contract Contract(gain2)def forward(self, x): # x(b,c,w,h) - y(b,4c,w/2,h/2)return self.conv(torch.cat((x[..., ::2, ::2], x[..., 1::2, ::2], x[..., ::2, 1::2], x[..., 1::2, 1::2]), 1))# return self.conv(self.contract(x))其中的 Conv 模块如下
class Conv(nn.Module):# Standard convolution with args(ch_in, ch_out, kernel, stride, padding, groups, dilation, activation)default_act nn.SiLU() # default activationdef __init__(self, c1, c2, k1, s1, pNone, g1, d1, actTrue):super().__init__()self.conv nn.Conv2d(c1, c2, k, s, autopad(k, p, d), groupsg, dilationd, biasFalse)self.bn nn.BatchNorm2d(c2)self.act self.default_act 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))⚠️ Note: Conv 模块使用的激活函数是 SiLU 而非 ReLU。
那我们使用 Focus 模块试一试。
import torch
import torch.nn as nn
import os
import sys
import platform
from pathlib import Path
FILE Path(__file__).resolve()
ROOT FILE.parents[1] # YOLOv5 root directory
if str(ROOT) not in sys.path:sys.path.append(str(ROOT)) # add ROOT to PATH
if platform.system() ! Windows:ROOT Path(os.path.relpath(ROOT, Path.cwd())) # relativefrom common import Convclass Focus(nn.Module):# Focus wh information into c-spacedef __init__(self, c1, c2, k1, s1, pNone, g1, actTrue): # ch_in, ch_out, kernel, stride, padding, groupssuper().__init__()self.conv Conv(c1 * 4, c2, k, s, p, g, actact)# self.contract Contract(gain2)def forward(self, x): # x(b,c,w,h) - y(b,4c,w/2,h/2)_concat torch.cat((x[..., ::2, ::2], x[..., 1::2, ::2], x[..., ::2, 1::2], x[..., 1::2, 1::2]), 1)_conv self.conv(_concat)print(f{_concat })print(f{_concat.shape })print(f{_concat.dtype }\n)# print(f{_conv })print(f{_conv.shape })print(f{_conv.dtype }\n)return _conv# return self.conv(self.contract(x))if __name__ __main__:# 创建tensorinput_tensor torch.tensor(data[[[[11, 12, 13, 14],[21, 22, 23, 24],[31, 32, 33, 34],[41, 42, 43, 44]]]], dtypetorch.float32)print(f{input_tensor })print(f{input_tensor.shape })print(f{input_tensor.dtype }\n)# 创建Focus子模块模型对象Sub_module Focus(1, 64).eval()# 前向推理output Sub_module(input_tensor)我们看一下输出
input_tensor tensor([[[[11., 12., 13., 14.],[21., 22., 23., 24.],[31., 32., 33., 34.],[41., 42., 43., 44.]]]])
input_tensor.shape torch.Size([1, 1, 4, 4])
input_tensor.dtype torch.float32_concat tensor([[[[11., 13.],[31., 33.]],[[21., 23.],[41., 43.]],[[12., 14.],[32., 34.]],[[22., 24.],[42., 44.]]]])
_concat.shape torch.Size([1, 4, 2, 2])
_concat.dtype torch.float32_conv.shape torch.Size([1, 64, 2, 2])
_conv.dtype torch.float32〔分析〕假设我们的输入如下
input_tensor tensor([[[[11., 12., 13., 14.],[21., 22., 23., 24.],[31., 32., 33., 34.],[41., 42., 43., 44.]]]])
input_tensor.shape torch.Size([1, 1, 4, 4])
input_tensor.dtype torch.float32那么经过 torch.cat((x[..., ::2, ::2], x[..., 1::2, ::2], x[..., ::2, 1::2], x[..., 1::2, 1::2]), 1) 之后变为
_concat tensor([[[[11., 13.],[31., 33.]],[[21., 23.],[41., 43.]],[[12., 14.],[32., 34.]],[[22., 24.],[42., 44.]]]])
_concat.shape torch.Size([1, 4, 2, 2])
_concat.dtype torch.float32之后再经过一个卷积self.conv(_concat)得到
_conv.shape torch.Size([1, 64, 2, 2])
_conv.dtype torch.float32可以看到我们的输入从原来的 [1, 1, 4, 4] 变为了 [1, 4, 2, 2]之后再通过一个 CBS 卷积得到 Focus 的最终输入 [1, 64, 2, 2]。
那假设我们的输入是 [1, 3, 256, 256]那么是怎么变化的呢结果如下
input_tensor.shape torch.Size([1, 3, 256, 256])
_concat.shape torch.Size([1, 12, 128, 128])
_conv.shape torch.Size([1, 64, 128, 128])#mermaid-svg-97Scy2aQX01TQ6wm {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#mermaid-svg-97Scy2aQX01TQ6wm .error-icon{fill:#552222;}#mermaid-svg-97Scy2aQX01TQ6wm .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-97Scy2aQX01TQ6wm .edge-thickness-normal{stroke-width:2px;}#mermaid-svg-97Scy2aQX01TQ6wm .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-97Scy2aQX01TQ6wm .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-97Scy2aQX01TQ6wm .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-97Scy2aQX01TQ6wm .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-97Scy2aQX01TQ6wm .marker{fill:#333333;stroke:#333333;}#mermaid-svg-97Scy2aQX01TQ6wm .marker.cross{stroke:#333333;}#mermaid-svg-97Scy2aQX01TQ6wm svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-97Scy2aQX01TQ6wm .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-97Scy2aQX01TQ6wm .cluster-label text{fill:#333;}#mermaid-svg-97Scy2aQX01TQ6wm .cluster-label span{color:#333;}#mermaid-svg-97Scy2aQX01TQ6wm .label text,#mermaid-svg-97Scy2aQX01TQ6wm span{fill:#333;color:#333;}#mermaid-svg-97Scy2aQX01TQ6wm .node rect,#mermaid-svg-97Scy2aQX01TQ6wm .node circle,#mermaid-svg-97Scy2aQX01TQ6wm .node ellipse,#mermaid-svg-97Scy2aQX01TQ6wm .node polygon,#mermaid-svg-97Scy2aQX01TQ6wm .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-97Scy2aQX01TQ6wm .node .label{text-align:center;}#mermaid-svg-97Scy2aQX01TQ6wm .node.clickable{cursor:pointer;}#mermaid-svg-97Scy2aQX01TQ6wm .arrowheadPath{fill:#333333;}#mermaid-svg-97Scy2aQX01TQ6wm .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-97Scy2aQX01TQ6wm .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-97Scy2aQX01TQ6wm .edgeLabel{background-color:#e8e8e8;text-align:center;}#mermaid-svg-97Scy2aQX01TQ6wm .edgeLabel rect{opacity:0.5;background-color:#e8e8e8;fill:#e8e8e8;}#mermaid-svg-97Scy2aQX01TQ6wm .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-97Scy2aQX01TQ6wm .cluster text{fill:#333;}#mermaid-svg-97Scy2aQX01TQ6wm .cluster span{color:#333;}#mermaid-svg-97Scy2aQX01TQ6wm div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-97Scy2aQX01TQ6wm :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} Concat CBS Image_1x3x256x256 1x12x128x128 1x64x128x128 总结
Focus 会对输入图片进行切片操作[N, C, H, W] - [N, C*4, H//2, W//2]之后通过一个卷积变成我们想要的 channel[N, C*4, H//2, W//2] - [N, C_out, H//2, W//2] 可能有同学比较好奇这个 Focus 模块出自哪篇论文其实并没有论文这是 YOLOv5 作者自己提出来的下面是他的解释 YOLOv5 Focus() Layer #3181 我收到了很多关于 YOLOv5 Focus 层的兴趣因此我在这里写了一个简短的文档。在将 YOLOv3 架构演进为 YOLOv5 时我自己创建了 Focus 层并没有采用其他来源的方法。Focus 层的主要目的是减少层的数量、减少参数、减少 FLOPS、减少 CUDA 内存同时最小程度地影响 mAP提高前向和后向推理速度。 YOLOv5 的 Focus 层用单一层替换了 YOLOv3 的前 3 层 在大量尝试和分析了替代 YOLOv3 输入层的不同设计后我最终选择了当前的 Focus 层设计。这些尝试包括对前向/后向/内存进行实时分析以及对完整的 300 个 Epoch 的 COCO 训练进行比较以确定其对 mAP 的影响。您可以使用 YOLOv5 的 profile() 函数很容易地对比 Focus 层和替代的 YOLOv3 原始层进行分析 # Profile
import torch.nn as nn
from models.common import Focus, Conv, Bottleneck
from utils.torch_utils import profile m1 Focus(3, 64, 3) # YOLOv5 Focus layer
m2 nn.Sequential(Conv(3, 32, 3, 1), Conv(32, 64, 3, 2), Bottleneck(64, 64)) # YOLOv3 first 3 layersresults profile(inputtorch.randn(16, 3, 640, 640), ops[m1, m2], n10, device0) # profile both 10 times at batch-size 16在 YOLOv5 Google Colab 笔记本中我得到了以下结果 YOLOv5 v5.0-405-gfad57c2 torch 1.9.0cu102 CUDA:0 (Tesla T4, 15109.75MB)Params GFLOPs GPU_mem (GB) forward (ms) backward (ms) input output7040 23.07 2.259 16.65 54.1 (16, 3, 640, 640) (16, 64, 320, 320) # Focus40160 140.7 7.522 77.86 331.9 (16, 3, 640, 640) (16, 64, 320, 320)Params GFLOPs GPU_mem (GB) forward (ms) backward (ms) input output7040 23.07 0.000 882.1 2029 (16, 3, 640, 640) (16, 64, 320, 320) # Focus40160 140.7 0.000 4513 8565 (16, 3, 640, 640) (16, 64, 320, 320)3.2 CSPNetCross Stage Partial Network
CSPNetCross Stage Partial Network跨阶段局部网络旨在提高模型的性能其的核心思想是在网络中引入 Cross Stage 信息传递以促进不同阶段之间的信息流动从而提高网络的感知能力。 将 CSPNet 应用于其他架构。CSPNet 也可以应用于 ResNet 和 ResNeXt这些架构如图 5 所示。由于只有一半的特征通道经过 Res(X)Blocks因此不再需要引入瓶颈层。这使得在浮点操作FLOPs固定时内存访问成本MAC的理论下限更低。 在 ResNet 中特征图会经过一系列的 Bottleneck 模块而在 CSPNet 中特征图会走两条支路Part 1 中会直接短路而 Part 2 中会经过 Bottleneck 模块之后在 Partial Transition 中进行融合。 根据论文 CSPNet: A New Backbone that can Enhance Learning Capability of CNN输入应该分为两个部分分别通过两个独立的分支进行处理。但是在你的实现中两个分支都使用相同的输入且没有进行任何分割。 YOLOv5 的作者也对其进行了回答 abhiagwl4262 是的输入并没有分割它们在这里用于两个地方我认为这与实际的 CSPNet 实现是一致的。 我们看一下 BottleneckCSP 的源码
class BottleneckCSP(nn.Module):# CSP Bottleneck https://github.com/WongKinYiu/CrossStagePartialNetworksdef __init__(self, c1, c2, n1, shortcutTrue, g1, e0.5): # ch_in, ch_out, number, shortcut, groups, expansionsuper().__init__()c_ int(c2 * e) # hidden channelsself.cv1 Conv(c1, c_, 1, 1) # CBSself.cv2 nn.Conv2d(c1, c_, 1, 1, biasFalse) # 普通卷积self.cv3 nn.Conv2d(c_, c_, 1, 1, biasFalse) # 普通卷积self.cv4 Conv(2 * c_, c2, 1, 1) # CBSself.bn nn.BatchNorm2d(2 * c_) # applied to cat(cv2, cv3)self.act nn.SiLU()self.m nn.Sequential(*(Bottleneck(c_, c_, shortcut, g, e1.0) for _ in range(n)))def forward(self, x):_conv1 self.cv1(x) # 经过 1x1 卷积CBS提升维度_m self.m(_conv1) # 经过一系列 Bottleneck 模块y1 self.cv3(_m) # 〔右边经过Bottleneck的分支〕再经过一个 1x1 普通卷积没有升维也没有降维: c_y2 self.cv2(x) # 〔左边不经过Bottleneck的分支〕对原始的输入用 1x1 普通卷积降为: c__concat torch.cat((y1, y2), 1) # 沿channel维度进行拼接: 2*c__bn self.bn(_concat) # 经过BN层_act self.act(_bn) # 经过SiLU层_conv4 self.cv4(_act) # 使用 1x1 卷积CBS对融合后的特征图进行降维: c2 c_outreturn _conv4可以看到y1 就是一个特征图经过普通的 Bottleneck 得到的y2 则只经过一个 1x1 卷积进行了通道维度对齐。
其实看了 BottleneckCSP 代码后我有一个疑问
y1 self.cv3(_m) # 〔右边经过Bottleneck的分支〕再经过一个 1x1 卷积没有升维也没有降维: c_这行代码有什么意义呢因为 1x1 卷积本身的参数量就非常少更何况 in_channel out_channel且 kernel_size1stride1这好像并没有做什么。让我们看一下 GPT 的回答 self.cv3 是一个具有 c_ 输入通道和 c_ 输出通道的 1x1 卷积层。这个操作应用于特征 _m并且不改变通道数。这一层的目的是引入非线性并允许网络从经过转换的特征中学习复杂的模式。 结果 y1 是网络右侧分支的输出经历了 bottleneck 模块的转换和额外的 1x1 卷积self.cv3。这个分支以一种捕捉复杂模式和经过 bottleneck 模块学到的相互作用的方式处理特征。 总之y1 self.cv3(_m) 的目的在于引入非线性并捕捉经过右侧分支 bottleneck 模块转换的特征中的复杂模式。两个分支的组合有助于提供丰富的信息以便进行后续处理。 这个回答看似有一定的道理但我感觉可能意义不大 。
3.3 C3CSP Bottleneck with 3 convolutions
现在的 YOLOv5 默认使用的 Bottleneck 模块并不是 BottleneckCSP 模块而是 C3 模块了下面是 C3 模块的源码
class C3(nn.Module):# CSP Bottleneck with 3 convolutionsdef __init__(self, c1, c2, n1, shortcutTrue, g1, e0.5): # ch_in, ch_out, number, shortcut, groups, expansionsuper().__init__()c_ int(c2 * e) # hidden channelsself.cv1 Conv(c1, c_, 1, 1) # CBSself.cv2 Conv(c1, c_, 1, 1) # CBSself.cv3 Conv(2 * c_, c2, 1) # CBS, optional actFReLU(c2)self.m nn.Sequential(*(Bottleneck(c_, c_, shortcut, g, e1.0) for _ in range(n)))def forward(self, x):_conv1 self.cv1(x) # 输入fmap进行1x1卷积CBS降维: c1 - c__m self.m(_conv1) # 降维的fmap经过bottleneck: c__conv2 self.cv2(x) # 输入fmap进行1x1卷积CBS降维: c1 - c__concat torch.cat((_m, _conv2), 1) # 沿channel维度进行拼接: 2*c__conv3 self.cv3(_concat) # 将融合的fmap经过1x1卷积CBS升维: 2*c_ - c2return _conv3可以看到
C3 模块中所有的卷积均为 CBSConv - BN - SiLU不像 BottleneckCSP 中除了 CBS 外还会使用普通卷积。还需要注意的是在 BottleneckCSP 中1x1 卷积用的是普通卷积而在 C3 模块中1x1 卷积用的是 CBS。去掉了令我感到疑惑的 1x1 卷积
除了上述 3 点外剩下的与 BottleneckCSP 是一致的。可以这么说C3 就是 BottleneckCSP 的高效版。 #mermaid-svg-GvSqHAZwP4GPuP4n {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#mermaid-svg-GvSqHAZwP4GPuP4n .error-icon{fill:#552222;}#mermaid-svg-GvSqHAZwP4GPuP4n .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-GvSqHAZwP4GPuP4n .edge-thickness-normal{stroke-width:2px;}#mermaid-svg-GvSqHAZwP4GPuP4n .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-GvSqHAZwP4GPuP4n .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-GvSqHAZwP4GPuP4n .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-GvSqHAZwP4GPuP4n .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-GvSqHAZwP4GPuP4n .marker{fill:#333333;stroke:#333333;}#mermaid-svg-GvSqHAZwP4GPuP4n .marker.cross{stroke:#333333;}#mermaid-svg-GvSqHAZwP4GPuP4n svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-GvSqHAZwP4GPuP4n .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-GvSqHAZwP4GPuP4n .cluster-label text{fill:#333;}#mermaid-svg-GvSqHAZwP4GPuP4n .cluster-label span{color:#333;}#mermaid-svg-GvSqHAZwP4GPuP4n .label text,#mermaid-svg-GvSqHAZwP4GPuP4n span{fill:#333;color:#333;}#mermaid-svg-GvSqHAZwP4GPuP4n .node rect,#mermaid-svg-GvSqHAZwP4GPuP4n .node circle,#mermaid-svg-GvSqHAZwP4GPuP4n .node ellipse,#mermaid-svg-GvSqHAZwP4GPuP4n .node polygon,#mermaid-svg-GvSqHAZwP4GPuP4n .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-GvSqHAZwP4GPuP4n .node .label{text-align:center;}#mermaid-svg-GvSqHAZwP4GPuP4n .node.clickable{cursor:pointer;}#mermaid-svg-GvSqHAZwP4GPuP4n .arrowheadPath{fill:#333333;}#mermaid-svg-GvSqHAZwP4GPuP4n .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-GvSqHAZwP4GPuP4n .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-GvSqHAZwP4GPuP4n .edgeLabel{background-color:#e8e8e8;text-align:center;}#mermaid-svg-GvSqHAZwP4GPuP4n .edgeLabel rect{opacity:0.5;background-color:#e8e8e8;fill:#e8e8e8;}#mermaid-svg-GvSqHAZwP4GPuP4n .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-GvSqHAZwP4GPuP4n .cluster text{fill:#333;}#mermaid-svg-GvSqHAZwP4GPuP4n .cluster span{color:#333;}#mermaid-svg-GvSqHAZwP4GPuP4n div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-GvSqHAZwP4GPuP4n :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 精简 BottleneckCSP C3 我们再看一篇论文Model Compression Methods for YOLOv5: A Review里面有这样一张图 图 3C3 和 BottleNeckCSP 模块的结构。使用 CSP 策略使 C3 模块通过残差块和稠密块加强信息流同时解决冗余梯度的问题。BottleNeck 块在 C3 和 BottleNeckCSP 中被使用并被标记为紫色它可以有两种配置 S / S ‾ S/\overline{S} S/S。这里 S S S 表示激活的快捷连接而 S ‾ \overline{S} S 表示没有任何跳跃连接的简单 BottleNeck。Backbone 中的 C3 块使用带有快捷连接的 BottleNecks而 Neck 中的 C3 块则不使用。 从上面的图片我们可以看到我们的说法遗漏了一个点
在 BottleneckCSP 中concat 之后会先经过一个 BN - SiLU 的结构最后再降维而在 C3 中没有这个 BN - SiLU 结构。
我们再看一篇论文 MC-YOLOv5: A Multi-Class Small Object Detection Algorithm (a) C3 的结构其输入为 H × W × C。C3 模块包含三个基本卷积层CBS和 n 个 Bottleneck 模块n 由配置文件和网络深度确定基本卷积的激活函数从 LeakyReLU 变为 SiLU。 (b) bottleneck-CSP 的结构其输入为 H × W × C。它由普通卷积、CBL 和 ResUnit 结构组成。 和我们的说法没有冲突那么我们可以再次总结我们的结论 —— C3 与 BottleneckCSP 的区别
C3 模块中所有的卷积均为 CBS包括 1x1 卷积删除了 Bottleneck 后的 1x1 卷积C3 删除了 concat 结构后的 BN - SiLU QuestionC3 模块为什么叫做 C3 Answer因为它的全称是CSP Bottleneck with 3 convolutions。
3.4 SPPSpatial Pyramid Pooling
在 YOLOv5 中SPPSpatial Pyramid Pooling是一种用于提取多尺度特征的技术它有助于网络对不同尺度的目标进行检测。SPP 通过在不同大小的网格上进行池化操作从而在不引入额外参数的情况下捕捉输入特征图的不同尺度上的语义信息。 将 SPP 块添加到 CSP 之上因为它显著增加了感受野分离出最重要的上下文特征并且几乎不会降低网络操作速度 —— YOLOv4 论文 在 YOLOv4-SPP 中进行了 5x5, 7x7, 13x13 的 MaxPooling而在 YOLOv5-SPP 中进行了 5x5, 9x9, 13x13 的 MaxPooling。通过 YOLOv4-SPP 中特征图变化可以看到在进行了 MaxPooling 后特征图的 shape 并没有发生变化。我们看一下 YOLOv5-SPP 的源码
class SPP(nn.Module):# Spatial Pyramid Pooling (SPP) layer https://arxiv.org/abs/1406.4729def __init__(self, c1, c2, k(5, 9, 13)):super().__init__()c_ c1 // 2 # hidden channelsself.cv1 Conv(c1, c_, 1, 1)self.cv2 Conv(c_ * (len(k) 1), c2, 1, 1) # 根据MaxPooling的个数自动调整假设有3个MaxPooling则314self.m nn.ModuleList([nn.MaxPool2d(kernel_sizex, stride1, paddingx // 2) for x in k])def forward(self, x):x self.cv1(x) # 先经过一个 1x1 卷积调整通道数_maxpools [m(x) for m in self.m] # 经过一些列MaxPooling_concat torch.cat([x] _maxpools, 1) # 将x与MaxPooling沿着通道维度拼接_conv2 self.cv2(_concat) # 最后经过一个1x1卷积调整通道数return _conv2不难理解需要注意的是
有 n 个 MaxPooling 层Concat 后维度就会 x(n1)SPP 中的池化层不会对特征图进行下采样
SPP 的流程图如下 YOLOv5-SPP 3.5 SPPFSpatial Pyramid Pooling with Fixed
SPPSpatial Pyramid Pooling和 SPPFSpatial Pyramid Pooling with Fixed都是在卷积神经网络CNN中使用的池化操作旨在处理不同尺寸的输入图像并生成固定大小的输出。 SPPSpatial Pyramid PoolingSPP 是由 Kaiming He 等人于 2014 年提出的主要用于解决卷积神经网络在处理不同尺寸的输入图像时所面临的问题。在传统的 CNN 中全连接层的输入大小是固定的但是输入图像的大小可能会有所不同。SPP 的目标是通过不同大小的池化窗口使网络能够接受不同尺寸的输入并生成固定长度的特征向量。 SPPFSpatial Pyramid Pooling with FixedSPPF 是在 SPP 的基础上进行改进的。SPPF 通过引入一个固定的金字塔级别pyramid level使得对输入图像的池化操作具有固定的感受野大小。这有助于在训练和推理中保持一致的输入特征大小。
总的来说SPP 是一种池化策略允许 CNN 处理不同尺寸的输入而 SPPF 是对 SPP 的一种改进引入了固定的金字塔级别以提高输入和输出的一致性。这两者都在图像识别和目标检测等任务中取得了一定的成功。
我们看一下 SPPF 的源码
class SPPF(nn.Module):# Spatial Pyramid Pooling - Fast (SPPF) layer for YOLOv5 by Glenn Jocherdef __init__(self, c1, c2, k5): # equivalent to SPP(k(5, 9, 13))super().__init__()c_ c1 // 2 # hidden channelsself.cv1 Conv(c1, c_, 1, 1)self.cv2 Conv(c_ * 4, c2, 1, 1) # 这里不再是按照MaxPooling的个数进行的而是固定为4self.m nn.MaxPool2d(kernel_sizek, stride1, paddingk // 2) # 这里的模块不再是一系列而是一个且kernel_size被固定了def forward(self, x):x self.cv1(x) # 先经过一个 1x1 卷积y1 self.m(x) # 经过一个 5x5 的MaxPoolingy2 self.m(y1) # 再经过一个 5x5 的MaxPooling_m self.m(y2) # 再再经过一个 5x5 的MaxPooling_concat torch.cat((x, y1, y2, _m), 1) # 将3个经过 MaxPooling 的和没有经过的沿着通道维度拼接_conv2 self.cv2(_concat) # 最后经过一个 1x1 卷积调整通道数return _conv2可以看到SPPF 跟 SPP 有很大的区别下面是 SPPF 的流程图 YOLOv5-SPP 放在一起看一下 YOLOv5-SPP v.s. YOLOv5-SPPF 可以看到SPPF 与 SPP 有了很大的不同
在 SPP 中经过 1x1 卷积的特征图 X \mathcal{X} X 会分为四条支路分别进入 shortcut、5x5-MaxPooling、9x9-MaxPooling 以及 13x13-MaxPooling之后四条支路的特征图会进行 Concat在 SPPF 中MaxPooling 的数量被固定为 4且 kernel_size 也被固定为 k经过 1x1 卷积的特征图 X \mathcal{X} X 会进入两条支路左边还是 shortcut右边则是顺序经过三个 5x5-MaxPooling每个 MaxPooling 都会分为两个分支一个进入 Concat另外一个进入下一个 5x5-MaxPooling。
SPPF 这样的操作可以得到和 SPP 一样的模型性能且计算量下降。 SPP 和 SPPF 参数量对比 import sys
sys.path.append(Learning-Notebook-Codes/ObjectDetection/YOLOv5/codes/yolov5-v7.0)
from torchsummary import summary
from models.common import SPP, SPPFspp SPP(c132, c23)
sppf SPPF(c132, c23)summary(spp, (32, 26, 26))
summary(sppf, (32, 26, 26))
----------------------------------------------------------------Layer (type) Output Shape Param #
Conv2d-1 [-1, 16, 26, 26] 512BatchNorm2d-2 [-1, 16, 26, 26] 32SiLU-3 [-1, 16, 26, 26] 0SiLU-4 [-1, 16, 26, 26] 0Conv-5 [-1, 16, 26, 26] 0MaxPool2d-6 [-1, 16, 26, 26] 0MaxPool2d-7 [-1, 16, 26, 26] 0MaxPool2d-8 [-1, 16, 26, 26] 0Conv2d-9 [-1, 3, 26, 26] 192BatchNorm2d-10 [-1, 3, 26, 26] 6SiLU-11 [-1, 3, 26, 26] 0SiLU-12 [-1, 3, 26, 26] 0Conv-13 [-1, 3, 26, 26] 0Total params: 742
Trainable params: 742
Non-trainable params: 0
----------------------------------------------------------------
Input size (MB): 0.08
Forward/backward pass size (MB): 0.74
Params size (MB): 0.00
Estimated Total Size (MB): 0.82
--------------------------------------------------------------------------------------------------------------------------------Layer (type) Output Shape Param #
Conv2d-1 [-1, 16, 26, 26] 512BatchNorm2d-2 [-1, 16, 26, 26] 32SiLU-3 [-1, 16, 26, 26] 0SiLU-4 [-1, 16, 26, 26] 0Conv-5 [-1, 16, 26, 26] 0MaxPool2d-6 [-1, 16, 26, 26] 0MaxPool2d-7 [-1, 16, 26, 26] 0MaxPool2d-8 [-1, 16, 26, 26] 0Conv2d-9 [-1, 3, 26, 26] 192BatchNorm2d-10 [-1, 3, 26, 26] 6SiLU-11 [-1, 3, 26, 26] 0SiLU-12 [-1, 3, 26, 26] 0Conv-13 [-1, 3, 26, 26] 0Total params: 742
Trainable params: 742
Non-trainable params: 0
----------------------------------------------------------------
Input size (MB): 0.08
Forward/backward pass size (MB): 0.74
Params size (MB): 0.00
Estimated Total Size (MB): 0.82
----------------------------------------------------------------我们发现二者是一样的因此直接暴力求解
import sys
sys.path.append(Learning-Notebook-Codes/ObjectDetection/YOLOv5/codes/yolov5-v7.0)
import torch
import time
from tqdm.rich import tqdm
from models.common import SPP, SPPFspp SPP(c11024, c21024)
sppf SPPF(c11024, c21024)input_tensor torch.randn(size[1, 1024, 20, 20])
times 200t1 time.time()
progress_bar tqdm(totaltimes, descSPP)
for _ in range(times):tmp spp(input_tensor)progress_bar.update()
progress_bar.close()
t2 time.time()progress_bar tqdm(totaltimes, descSPPF)
for _ in range(times):tmp sppf(input_tensor)progress_bar.update()
progress_bar.close()
t3 time.time()print(fSPP (average time): {(t2 - t1) / times:.4f}s)
print(fSPPF (average time): {(t3 - t2) / times:.4f}s)SPP (average time): 0.0429s
SPPF (average time): 0.0250s可以看到SPPF 的速度是 SPP 的 1.716 倍提升是非常明显的
3.6 PANetPath-Aggregation Network
PANetPath-Aggregation Network路径聚合网络是一种用于目标检测的深度神经网络架构旨在改善目标实例的多尺度特征表达。PANet 主要由两个关键组件组成自顶向下的路径传播Top-Down Path Propagation和自底向上的特征聚合Bottom-Up Feature Aggregation。 PANet 网络结构图 (a) FPN主干网络。 (b) 自底向上路径增强。 © 自适应特征池化。 (d) 区域提取分支。 (e) 全连接融合。 注意在(a)和(b)中为了简洁起见我们省略了特征图的通道维度。 自顶向下的路径传播Top-Down Path Propagation
PANet 通过自顶向下的路径传播从高层语义特征到低层细节特征帮助网络更好地理解目标的全局和局部上下文信息。这个传播路径使得网络能够通过多个层次的特征层次结合从而捕捉目标的多尺度信息。
自底向上的特征聚合Bottom-Up Feature Aggregation
为了更好地捕获底层特征的细节信息PANet 引入了自底向上的特征聚合机制。通过底层特征的横向传播网络能够聚合来自多个尺度的信息有助于提升对小目标或者细节的检测性能。
PANet 的设计使得网络能够充分利用多尺度信息从而提高目标检测任务的性能。该网络在 2018 年由北京大学的研究团队提出已经在许多目标检测竞赛和应用中取得了显著的成果。这种网络架构的灵活性和高效性使得它在处理不同尺度和复杂场景下的目标检测问题上表现出色。
在 YOLOv5 架构图中PANet 的示意图如下。 PANet 网络结构图 通过将拥有低中高层语义信息的特征图进行相互融合最终的预测特征图不仅拥有高级语义信息也有一定的中级和低级语义信息这样可以提高模型预测能力。
4. 损失函数
YOLOv5 损失函数包括三种
Classification Loss分类损失Localization Loss: 定位损失Anchor 与 GT 框之间的误差Confidence Loss: 置信度损失
总的损失是这三种损失的加权和。
4.1 分类损失
大多数分类器假设输出标签是互斥的对于猫狗分类数据集而言假设一张图片是“猫”那么就不能是“狗”例如 YOLOv1 和 YOLOv2在这些模型中通过使用 Softmax 函数将得分转换为总和为 1 的概率以表示每个类别的置信度。然而在 YOLOv3、YOLOv4 和 YOLOv5 之后引入了多标签分类的概念。举个例子YOLOv5 的输出标签可以是多标签的例如一个检测框可能同时包含“行人”和“儿童”这两个类别并不是互斥的。 Softmax 函数用于将一组实数转换为概率分布。给定输入向量 $ z [z_1, z_2, …, z_k] $Softmax 函数的输出 $ \sigma(z) $ 的计算公式如下 σ ( z ) i e z i ∑ j 1 k e z j \sigma(z)_i \frac{e^{z_i}}{\sum_{j1}^{k} e^{z_j}} σ(z)i∑j1kezjezi 其中 $ \sigma(z)_i $ 是 Softmax 函数的输出中的第 $ i $ 个元素。$ e $ 是自然对数的底。$ z_i $ 是输入向量 $ z $ 的第 $ i $ 个元素。$ \sum_{j1}^{k} e^{z_j} $ 是对所有输入向量的指数项的和。 Softmax 函数的目标是将输入向量 $ z $ 转换为一个概率分布使得输出的每个元素都在 0 到 1 之间且所有元素的和为 1。这通常用于多类别分类问题其中每个元素对应一个类别并且 Softmax 输出表示每个类别的概率。 需要注意的是在多标签分类中一个物体可能同时属于多个类别因此输出的标签不再通过 Softmax 函数进行处理。相反通常会使用 Sigmoid 函数对每个类别的得分进行独立的二分类处理。这样每个类别的输出都是一个介于 0 和 1 之间的概率值表示物体属于该类别的置信度。因此在多标签分类下输出类别的概率之和可以大于 1。
为了更清晰地说明多标签分类的实现方式假设有 N 个类别每个类别使用一个 Sigmoid 激活函数来产生一个范围在 0 到 1 之间的输出。对于每个类别如果输出值大于设定的阈值通常为 0.5则认为该物体属于该类别。这种独立的二分类方式允许一个检测框同时具有多个类别与互斥的单标签分类不同。 总的来说多标签分类通过使用多个独立的 Sigmoid 函数来实现每个函数对应一个类别这样就可以有效地处理一个物体属于多个类别的情况。
因此在计算分类损失时YOLOv3、YOLOv4、YOLOv5 对每个标签都使用 BCE 损失这样也降低了计算复杂度。
4.2 定位损失
4.2.1 IoU
边界框回归是许多 2D/3D 计算机视觉任务中最基本的组件之一。以前的方法通常使用 L 1 L_1 L1 和 L 2 L_2 L2 损失来度量边界框的预测误差但这些损失函数考虑的因素相对较少。一种改进的方法是使用 IoUIntersection over Union来度量边界框的定位损失。
IoU 考虑了预测边界框与真实边界框之间的重叠程度通过计算它们的交集与并集之间的比值。使用 IoU 作为损失函数的度量标准可以更准确地衡量边界框的位置和尺寸的预测精度尤其是在目标检测等任务中。这种方法对于提高模型的定位准确性和鲁棒性非常有效。 上图中绿色的框为 Ground Truth黑色的框为 Anchor那么 IoU 计算公式如下 I o U ∣ A ∩ B ∣ ∣ A ∪ B ∣ \mathrm{IoU} \frac{|A \cap B|}{|A \cup B|} IoU∣A∪B∣∣A∩B∣
我们看一下 IoU 的源码
def bbox_iou(box1, box2, xywhTrue, GIoUFalse, DIoUFalse, CIoUFalse, eps1e-7):box1: [1, 4]box2: [N, 4]xywh: 坐标格式为 xywhGIoU: 使用GIoUDIoU: 使用DIoUCIoU: 使用CIoU# Returns Intersection over Union (IoU) of box1(1,4) to box2(n,4)# Get the coordinates of bounding boxes# 将坐标转换为 xyxy 的格式# ⚠️ 坐标原点左上角if xywh: # transform from xywh to xyxy(x1, y1, w1, h1), (x2, y2, w2, h2) box1.chunk(4, -1), box2.chunk(4, -1)w1_, h1_, w2_, h2_ w1 / 2, h1 / 2, w2 / 2, h2 / 2b1_x1, b1_x2, b1_y1, b1_y2 x1 - w1_, x1 w1_, y1 - h1_, y1 h1_b2_x1, b2_x2, b2_y1, b2_y2 x2 - w2_, x2 w2_, y2 - h2_, y2 h2_else: # x1, y1, x2, y2 box1b1_x1, b1_y1, b1_x2, b1_y2 box1.chunk(4, -1) # b1_x1: x1列b2_x1, b2_y1, b2_x2, b2_y2 box2.chunk(4, -1)# 把w和h求出来w1, h1 b1_x2 - b1_x1, (b1_y2 - b1_y1).clamp(eps)w2, h2 b2_x2 - b2_x1, (b2_y2 - b2_y1).clamp(eps)# Intersection area# 求交集的面积# tensor1.minimum(tensor2): 两个相同shape的tensor进行逐元素比较inter_w (b1_x2.minimum(b2_x2) - b1_x1.maximum(b2_x1)).clamp(0) # 交集的宽度inter_h (b1_y2.minimum(b2_y2) - b1_y1.maximum(b2_y1)).clamp(0) # 交集的高度inter inter_w * inter_h # 交集的面积# Union Areaunion w1 * h1 w2 * h2 - inter eps# IoUiou inter / unionreturn iou # IoU对应的图片如下 IoU 示意图 torch.chunk(input, chunks, dim0): input: 要分割的输入张量。chunks: 分割的块数。dim: 沿着哪个维度进行分割默认为 0。 torch.clamp(input, min, max, outNone): 将输入张量的元素限制在指定范围内 torch.prod(input, dtypeNone): 用于计算输入张量中所有元素的乘积 b1_x2.minimum(b2_x2) 表示取 b1_x2 和 b2_x2 中的每个元素的最小值。这是一个逐元素的比较操作对于两个形状相同的张量它将返回一个新的张量其中每个元素都是对应位置上两个输入张量中较小的那个值。 4.2.2 IoU 存在的问题
虽然 IoU 可以度量两个框的重合度但也存在一定问题。当两个物体不重叠时IoU 的计算结果为 0这并不能直接反映出两个框的距离或定位误差。此外当 IoU 被用作损失函数时在两个框不重叠的情况下梯度也为 0这可能导致模型无法有效地进行优化尤其是在训练初期。
if |A ∩ B| 0:IoU(A, B) 04.2.3 IoU 推广GIoUGeneralized IoU
改进方法为了解决 IoU 在不重叠的情况下的问题一种常见的做法是推广 IoU 并确保满足以下条件
遵循与 IoU 相同的定义将比较对象的形状数据编码为区域属性。维持 IoU 的尺寸不变性确保新的指标在计算时不受对象尺寸变化的影响保持与 IoU 相关的特性。在重叠对象的情况下确保与 IoU 的强相关性新的指标在两个对象高度重叠时应该保持与 IoU 类似的表现以便保留其在目标检测等任务中的有效性。
在这个背景下一种被提出的改进是 Generalized IoUGIoU。GIoU 是一种更全面的边界框重叠度量它在计算两个边界框之间的重叠时不仅考虑了它们的交集和并集还考虑了它们的外接矩形最小闭包矩形。GIoU 被设计为在不同情况下都能提供更准确的重叠度量包括不重叠的情况。GIoU 具体操作如下
计算交集Intersection ∣ A ∩ B ∣ |A \cap B| ∣A∩B∣计算并集Union ∣ A ∪ B ∣ |A \cup B| ∣A∪B∣计算外接矩形的面积Bounding Box的最小闭包矩形 ∣ C ∣ |C| ∣C∣计算 GIoU G I o U I o U − ∣ C − ( A ∪ B ) ∣ ∣ C ∣ \mathrm{GIoU} \mathrm{IoU} - \frac{|C - (A \cup B) |}{|C|} GIoUIoU−∣C∣∣C−(A∪B)∣计算损失值 L G I o U 1 − G I o U \mathcal{L}_{\mathrm{GIoU}} 1 - \mathrm{GIoU} LGIoU1−GIoU GIoU 示例图 研究者们发现使用 GIoU 作为损失函数在目标检测等任务中能够取得更好的性能特别是在边界框回归方面。GIoU 的引入为解决 IoU 不足的问题提供了一个有效的方法。
我们看一下 GIoU 的源码
def bbox_iou(box1, box2, xywhTrue, GIoUFalse, DIoUFalse, CIoUFalse, eps1e-7):# Returns Intersection over Union (IoU) of box1(1,4) to box2(n,4)# Get the coordinates of bounding boxes# 将坐标转换为 xyxy 的格式# ⚠️ 坐标原点左上角if xywh: # transform from xywh to xyxy(x1, y1, w1, h1), (x2, y2, w2, h2) box1.chunk(4, -1), box2.chunk(4, -1)w1_, h1_, w2_, h2_ w1 / 2, h1 / 2, w2 / 2, h2 / 2b1_x1, b1_x2, b1_y1, b1_y2 x1 - w1_, x1 w1_, y1 - h1_, y1 h1_b2_x1, b2_x2, b2_y1, b2_y2 x2 - w2_, x2 w2_, y2 - h2_, y2 h2_else: # x1, y1, x2, y2 box1b1_x1, b1_y1, b1_x2, b1_y2 box1.chunk(4, -1) # b1_x1: x1列b2_x1, b2_y1, b2_x2, b2_y2 box2.chunk(4, -1)# 把w和h求出来w1, h1 b1_x2 - b1_x1, (b1_y2 - b1_y1).clamp(eps)w2, h2 b2_x2 - b2_x1, (b2_y2 - b2_y1).clamp(eps)# Intersection area# 求交集的面积# tensor1.minimum(tensor2): 两个相同shape的tensor进行逐元素比较inter_w (b1_x2.minimum(b2_x2) - b1_x1.maximum(b2_x1)).clamp(0) # 交集的宽度inter_h (b1_y2.minimum(b2_y2) - b1_y1.maximum(b2_y1)).clamp(0) # 交集的高度inter inter_w * inter_h # 交集的面积# Union Areaunion w1 * h1 w2 * h2 - inter eps# 先求一下普通的IoUiou inter / unionif CIoU or DIoU or GIoU:# GIoU https://arxiv.org/pdf/1902.09630.pdf# 求出最小外接矩形的宽度cw b1_x2.maximum(b2_x2) - b1_x1.minimum(b2_x1) # convex (smallest enclosing box) width# 求出最小外接矩形的高度ch b1_y2.maximum(b2_y2) - b1_y1.minimum(b2_y1) # convex height# 求出最小外接矩形的面积c_area cw * ch eps # convex area# 计算最终的 IoUiou iou - (c_area - union) / c_area return iou4.2.4 DIoUDistance IoU
Distance-IoU (DIoU) 是为了进一步改进边界框重叠度量而提出的。DIoU 是 GIoU 的一种改进形式它在 GIoU 的基础上引入了对边界框中心点之间距离的考虑。DIoU 的提出主要是为了解决 GIoU 中的一些问题尤其是在存在重叠但不完全匹配的情况下的不足。
GIoU 主要考虑了两个边界框的交集、并集以及外接矩形但在一些情况下GIoU 仍然可能受到边界框中心点之间距离的限制。在物体不完全对齐的情况下GIoU 可能会导致对中心点距离的过度敏感因此在一些实际场景中GIoU 可能表现不够稳健。
DIoU 引入了中心点之间的距离通过考虑中心点距离来纠正 GIoU 中的一些缺陷。DIoU 的计算包括了中心点距离的项以更全面地度量两个边界框之间的距离。DIoU 在实际目标检测任务中的性能提升主要体现在对不完全匹配目标的准确边界框回归上。 DIoU 示例图 Figure 5: DIoU loss for bounding box regression, where the normalized distance between central points can be directly minimized. c c c is the diagonal length of the smallest enclosing box covering two boxes, and d ρ ( b , b g t ) d \rho(\bold{b}, \bold{b}^{gt}) dρ(b,bgt) is the distance of central points of two boxes. 图5DIoU损失用于边界框回归其中可以直接最小化中心点之间的标准化距离。 c c c 是覆盖两个框的最小外接框的对角线长度 d ρ ( b , b g t ) d \rho(\bold{b}, \bold{b}^{gt}) dρ(b,bgt) 是两个框中心点的距离。 综合而言DIoU 的提出旨在弥补 GIoU 中对中心点距离的过度敏感的问题使得在处理实际场景中存在不完全匹配的目标时能够更加稳健。
我们看一下 DIoU 的源码
def bbox_iou(box1, box2, xywhTrue, GIoUFalse, DIoUFalse, CIoUFalse, eps1e-7):# Returns Intersection over Union (IoU) of box1(1,4) to box2(n,4)# Get the coordinates of bounding boxes# 将坐标转换为 xyxy 的格式# ⚠️ 坐标原点左上角if xywh: # transform from xywh to xyxy(x1, y1, w1, h1), (x2, y2, w2, h2) box1.chunk(4, -1), box2.chunk(4, -1)w1_, h1_, w2_, h2_ w1 / 2, h1 / 2, w2 / 2, h2 / 2b1_x1, b1_x2, b1_y1, b1_y2 x1 - w1_, x1 w1_, y1 - h1_, y1 h1_b2_x1, b2_x2, b2_y1, b2_y2 x2 - w2_, x2 w2_, y2 - h2_, y2 h2_else: # x1, y1, x2, y2 box1b1_x1, b1_y1, b1_x2, b1_y2 box1.chunk(4, -1) # b1_x1: x1列b2_x1, b2_y1, b2_x2, b2_y2 box2.chunk(4, -1)# 把w和h求出来w1, h1 b1_x2 - b1_x1, (b1_y2 - b1_y1).clamp(eps)w2, h2 b2_x2 - b2_x1, (b2_y2 - b2_y1).clamp(eps)# Intersection area# 求交集的面积# tensor1.minimum(tensor2): 两个相同shape的tensor进行逐元素比较inter_w (b1_x2.minimum(b2_x2) - b1_x1.maximum(b2_x1)).clamp(0) # 交集的宽度inter_h (b1_y2.minimum(b2_y2) - b1_y1.maximum(b2_y1)).clamp(0) # 交集的高度inter inter_w * inter_h # 交集的面积# Union Areaunion w1 * h1 w2 * h2 - inter eps# 先求一下普通的IoUiou inter / unionif CIoU or DIoU or GIoU:# GIoU https://arxiv.org/pdf/1902.09630.pdf# 求出最小外接矩形的宽度cw b1_x2.maximum(b2_x2) - b1_x1.minimum(b2_x1) # convex (smallest enclosing box) width# 求出最小外接矩形的高度ch b1_y2.maximum(b2_y2) - b1_y1.minimum(b2_y1) # convex height# Distance or Complete IoU https://arxiv.org/abs/1911.08287v1if CIoU or DIoU:# 使用勾股定理求出对角线的平方即c^2c² cw**2 ch**2 eps # convex diagonal squared | 凸对角线的平方# 求两个box中心点的平方看下面的图rho² ((b2_x1 b2_x2 - b1_x1 - b1_x2) ** 2 (b2_y1 b2_y2 - b1_y1 - b1_y2) ** 2) / 4 # center dist ** 2return iou - rho² / c² # DIoU还是拿下面这张图说事儿 DIoU 中心点求解公式 4.2.5 CIoUComplete IoU
CIoUComplete Intersection over Union是一种边界框Bounding Box的相似性度量方法它是 DIoUDistance Intersection over Union的改进版本。CIoU 主要用于目标检测任务中特别是在训练阶段作为损失函数的一部分。以下是DIoU的一些潜在缺陷 局限性 DIoU 主要关注中心点距离和最小外接矩形对角线距离相对于 CIoU 来说DIoU 的度量相对较为简化有时无法捕捉边界框之间的复杂关系。 对形状变化不敏感 DIoU 在处理边界框形状变化时可能不够敏感特别是对于不同形状和比例的目标DIoU 的相似性度量可能不够准确。 不全面 DIoU 缺乏对宽高比例差异的考虑而 CIoU 引入了宽高比例差异的项使得相似性度量更加全面。 收敛性较差 在某些情况下DIoU 可能在训练中收敛较慢而 CIoU 的改进设计有助于提高损失函数的收敛性。
总的来说CIoU 可以看作是对 DIoU 的一种扩展和改进以更全面、更准确地度量边界框之间的相似性。在实践中CIoU 的性能可能更好特别是在处理各种目标形状和尺寸差异的情况下。然而选择使用哪种方法通常取决于具体的任务和实验结果。
相比于 DIoUCIoU 还考虑了宽高比例差异即 CIoU 还考虑了两个边界框的宽高比例之差异。
CIoU 的计算公式如下 CIoU IoU − d 2 c 2 − α ⋅ v \text{CIoU} \text{IoU} - \frac{d^2}{c^2} - \alpha \cdot v CIoUIoU−c2d2−α⋅v
可以看到相比于 DIoUCIoU 其实只是在惩罚项上加了一项即 R CIoU ρ 2 ( b , b g t ) c 2 α v \mathcal{R}_{\text{CIoU}} \frac{\rho^2(\bold{b}, \bold{b}^{gt})}{c^2} \alpha v RCIoUc2ρ2(b,bgt)αv
其中 α \alpha α 是一个正的权衡参数而 v v v 衡量了两个 Box 宽高比的一致性 v 4 π 2 ( arctan w g t h g t − arctan w h ) 2 v \frac{4}{\pi^2}(\arctan \frac{w^{gt}}{h^{gt}} - \arctan\frac{w}{h})^2 vπ24(arctanhgtwgt−arctanhw)2
CIoU 的损失函数定义如下 L CIoU 1 − I o U ρ 2 ( b , b g t ) c 2 α v \mathcal{L}_{\text{CIoU}} 1 - \mathrm{IoU} \frac{\rho^2(\bold{b}, \bold{b}^{gt})}{c^2} \alpha v LCIoU1−IoUc2ρ2(b,bgt)αv
其中权衡参数 α \alpha α 被定义为 α v ( 1 − I o U ) v ′ \alpha \frac{v}{(1 - IoU) v} α(1−IoU)v′v
通过这个定义重叠区域因子在回归中被赋予更高的优先级特别是对于非重叠情况。
最后CIoU 损失的优化与 DIoU 损失相同只是需要明确关于宽度w和高度h的 v v v 梯度 ∂ v ∂ w 8 π 2 ( arctan w g t h g t − arctan w h ) × h w 2 h 2 ∂ v ∂ h 8 π 2 ( arctan w g t h g t − arctan w h ) × w w 2 h 2 (12) \frac{\partial v}{\partial w} \frac{8}{\pi^2}(\arctan \frac{w^{gt}}{h^{gt}} - \arctan \frac{w}{h}) \times \frac{h}{w^2 h^2} \\ \frac{\partial v}{\partial h} \frac{8}{\pi^2}(\arctan \frac{w^{gt}}{h^{gt}} - \arctan \frac{w}{h}) \times \frac{w}{w^2 h^2} \tag{12} ∂w∂vπ28(arctanhgtwgt−arctanhw)×w2h2h∂h∂vπ28(arctanhgtwgt−arctanhw)×w2h2w(12)
分母 w 2 h 2 w^2 h^2 w2h2 在 h h h 和 w w w 范围在 [0, 1] 的情况下通常是一个较小的值可能导致梯度爆炸。因此在我们的实现中为了稳定收敛分母 w 2 h 2 w^2h^2 w2h2 被简单地移除其中步长 1 w 2 h 2 \frac{1}{w^2h^2} w2h21 被替换为1梯度方向仍然与方程 (12) 一致。 IoU、GIoU、DIoU 的回归误差 图4在最终迭代 T T T即 E ( T , n ) E(T, n) E(T,n)可视化了 IoU、GIoU 和 DIoU 损失的回归误差对每个坐标 n n n。我们注意到 (a) 和 (b) 中的区域对应于良好的回归情况。可以看到IoU 损失在非重叠情况下有较大误差GIoU 损失在水平和垂直情况下有较大误差而我们的 DIoU 损失在所有地方都导致非常小的回归误差。
我们看一下 CIoU 的源码
def bbox_iou(box1, box2, xywhTrue, GIoUFalse, DIoUFalse, CIoUFalse, eps1e-7):# Returns Intersection over Union (IoU) of box1(1,4) to box2(n,4)# Get the coordinates of bounding boxes# 将坐标转换为 xyxy 的格式# ⚠️ 坐标原点左上角if xywh: # transform from xywh to xyxy(x1, y1, w1, h1), (x2, y2, w2, h2) box1.chunk(4, -1), box2.chunk(4, -1)w1_, h1_, w2_, h2_ w1 / 2, h1 / 2, w2 / 2, h2 / 2b1_x1, b1_x2, b1_y1, b1_y2 x1 - w1_, x1 w1_, y1 - h1_, y1 h1_b2_x1, b2_x2, b2_y1, b2_y2 x2 - w2_, x2 w2_, y2 - h2_, y2 h2_else: # x1, y1, x2, y2 box1b1_x1, b1_y1, b1_x2, b1_y2 box1.chunk(4, -1) # b1_x1: x1列b2_x1, b2_y1, b2_x2, b2_y2 box2.chunk(4, -1)# 把w和h求出来w1, h1 b1_x2 - b1_x1, (b1_y2 - b1_y1).clamp(eps)w2, h2 b2_x2 - b2_x1, (b2_y2 - b2_y1).clamp(eps)# Intersection area# 求交集的面积# tensor1.minimum(tensor2): 两个相同shape的tensor进行逐元素比较inter_w (b1_x2.minimum(b2_x2) - b1_x1.maximum(b2_x1)).clamp(0) # 交集的宽度inter_h (b1_y2.minimum(b2_y2) - b1_y1.maximum(b2_y1)).clamp(0) # 交集的高度inter inter_w * inter_h # 交集的面积# Union Areaunion w1 * h1 w2 * h2 - inter eps# 先求一下普通的IoUiou inter / unionif CIoU or DIoU or GIoU:# GIoU https://arxiv.org/pdf/1902.09630.pdf# 求出最小外接矩形的宽度cw b1_x2.maximum(b2_x2) - b1_x1.minimum(b2_x1) # convex (smallest enclosing box) width# 求出最小外接矩形的高度ch b1_y2.maximum(b2_y2) - b1_y1.minimum(b2_y1) # convex height# Distance or Complete IoU https://arxiv.org/abs/1911.08287v1if CIoU or DIoU:# 使用勾股定理求出对角线的平方即c^2c² cw**2 ch**2 eps # convex diagonal squared | 凸对角线的平方# 求两个box中心点的平方看下面的图rho² ((b2_x1 b2_x2 - b1_x1 - b1_x2) ** 2 (b2_y1 b2_y2 - b1_y1 - b1_y2) ** 2) / 4 # center dist ** 2# 使用CIoUif CIoU:# 宽高比trade-off parameterv (4 / math.pi**2) * (torch.atan(w2 / h2) - torch.atan(w1 / h1)).pow(2)# 当处于前向推理时with torch.no_grad():alpha v / (v - iou (1 eps))return iou - (rho2 / c2 v * alpha) # CIoUCIoU 的引入旨在提高边界框相似性度量的准确性使得在目标检测的训练中更好地指导模型的学习。
4.3 YOLOv5 目标框回归与跨网格匹配策略
4.3.1 Box 的格式
首先我们回顾一下 PASCAL VOC 的标注格式。如下图所示图片尺寸为 1000x654把左上角作为坐标原点 (0,0)。 那么我们可以得到 xyxy 格式的坐标
x1, y1 187, 21
x2, y2 403, 627除了这种 xyxy 格式的坐标还有一种常用的坐标格式为xywh 格式的坐标即
x x1 (x2 - x1) / 2 # 295.0
y y1 (y2 - y1) / 2 # 324.0上面的式子可以进行化简
x (x1 x2) / 2 # 295.0
y (y1 y2) / 2 # 324.0求出中心点坐标后我们需要求出宽度和高度
w x2 - x1 # 216
h y2 - y1 # 606同理如果我们知道 xywh 坐标如何求出 xyxy 坐标呢
x, y, w, h 295, 324, 216, 606x1 x - w / 2 # 187.0
y1 y - h / 2 # 21.0
x2 x w / 2 # 403.0
y2 y h / 2 # 627.0我们也可以将其写为函数
def xyxy2xywh(xyxy):将边界框坐标格式从 (x_min, y_min, x_max, y_max) 转换为 (x_center, y_center, width, height)x_min, y_min, x_max, y_max xyxyx_center (x_min x_max) / 2y_center (y_min y_max) / 2width x_max - x_minheight y_max - y_minreturn x_center, y_center, width, heightdef xywh2xyxy(xywh):将边界框坐标格式从 (x_center, y_center, width, height) 转换为 (x_min, y_min, x_max, y_max)x_center, y_center, width, height xywhx_min x_center - width / 2y_min y_center - height / 2x_max x_center width / 2y_max y_center height / 2return x_min, y_min, x_max, y_max我们看一下具体的标注格式内容 ?xml version1.0 encodingutf-8?annotationfolderimages/folderfilename000000000025.jpg/filenamesizewidth640/widthheight426/heightdepth3/depth/sizeobjectnamegiraffe/nameposeUnspecified/posetruncated0/truncateddifficult0/difficultbndboxxmin385.52992/xminymin60.030002999999994/yminxmax600.50016/xmaxymax357.19013700000005/ymax/bndbox/objectobjectnamegiraffe/nameposeUnspecified/posetruncated0/truncateddifficult0/difficultbndboxxmin53.01024000000001/xminymin356.49000599999994/yminxmax185.04032/xmaxymax411.68001/ymax/bndbox/object/annotation我们再看一下它对应的 .txt 标签
23 0.770336 0.489695 0.335891 0.697559
23 0.185977 0.901608 0.206297 0.129554
可以看到xml 中使用的标签格式是 xyxy而 yolo 中使用的是 xywh。
其中 YOLO 格式的列分别表示
class_id x y w h
23 0.770336 0.489695 0.335891 0.697559
23 0.185977 0.901608 0.206297 0.129554⚠️ YOLO 格式中使用的是归一化后的坐标即
x: box 中心点横坐标 / 图片宽度y: box 中心点横坐标 / 图片高度w: box 宽度 / 图片宽度h: box 高度 / 图片高度 YOLO 格式的坐标示意图 4.3.2 YOLOv3 和 YOLOv4 的目标框回归
先验框 Anchor 给出了目标宽高的初始值需要回归的是目标真实宽高与初始宽高的偏移量。具体做法如下
预测框中心点相对于对应网格左上角位置的相对偏移量为了将边界框中心点约束在当前网格中使用 sigmoid 函数处理偏移值使预测偏移量的值在 (0,1) 范围内 图2具有维度先验和位置预测的边界框。我们将边界框的宽度和高度预测为相对于聚类中心的偏移量。使用 sigmoid 函数我们预测边界框的中心坐标相对于滤波器应用位置的位置。该图毫不掩饰地自引用自 [15]。 图中
蓝色的框为预测框黑色的框为 GT
根据边界框模型会预测 4 个 offsets t x , t y , t w , t h t_x, t_y, t_w, t_h tx,ty,tw,th可以按照如下公式计算出边界框的实际位置和宽高 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 \begin{aligned} b_x \sigma(t_x) c_x \\ b_y \sigma(t_y) c_y \\ b_w p_w e^{t_w}\\ b_h p_h e^{t_h} \end{aligned} bxbybwbhσ(tx)cxσ(ty)cypwetwpheth
其中 t x t_x tx 和 t y t_y ty 是模型预测的相对于 Anchor 框或先验框的中心的偏移量。 σ \sigma σ 表示 sigmoid 函数它将预测的偏移量转换到 0 和 1 之间这样就可以用来表示相对于特征图feature map上的特定点的位置。 c x c_x cx 和 c y c_y cy 是 Anchor 框或先验框的中心坐标在特征图上的对应位置。 b x b_x bx 和 b y b_y by 是预测框的中心坐标。
对于宽度和高度公式稍有不同 p w p_w pw 和 p h p_h ph 是 Anchor 框或先验框的宽度和高度。 t w t_w tw 和 t h t_h th 是模型预测的相对于 Anchor 框的宽度和高度的偏移量。 e e e 是自然对数的底数用于指数化预测的偏移量这样可以保证宽度和高度始终为正数。 b w b_w bw 和 b h b_h bh 是预测框的宽度和高度。
总的来说这些公式描述了如何将模型的预测偏移量和尺寸变化应用于 Anchor 框以得到最终的预测边界框。 Question t x t_x tx 和 t y t_y ty 是相对于原图的偏移量吗
Answer不 t x t_x tx 和 t y t_y ty 不是相对于原图的偏移量。它们是相对于特定特征图feature map上的Anchor框或先验框中心的偏移量。 Question为什么要给 t x t_x tx 和 t y t_y ty 添加 σ \sigma σ 函数
Answer在 YOLOv5 等物体检测模型中给 t x t_x tx 和 t y t_y ty 添加 sigmoid 函数的原因是为了将预测的偏移量限制在 0 和 1 之间。这样做有以下几个原因
归一化sigmoid 函数将任何实数映射到 (0, 1) 区间内。在物体检测中我们需要预测边界框的中心相对于特定网格单元grid cell的位置。通过使用 sigmoid 函数我们可以确保预测的偏移量是归一化的即它们表示的是相对于网格单元的比例而不是绝对的坐标值。稳定训练在训练过程中模型的输出可能会非常大或非常小这可能导致数值不稳定例如梯度消失或爆炸。sigmoid 函数的输出是平滑的有助于减少这种不稳定性使得训练过程更加稳定。可解释性归一化的偏移量更具有可解释性因为它们直接表示边界框中心相对于网格单元的位置比例。例如如果一个网格单元的尺寸是 16x16 像素一个预测的 t x t_x tx 值为 0.5那么边界框中心的 x 坐标将位于该网格单元的中心。兼容性在 YOLOv5 中特征图上的每个点都对应于原始图像上的一个区域。通过使用 sigmoid 函数模型可以更容易地适应不同尺寸的输入图像因为偏移量是相对于网格单元的比例而不是绝对的像素值。 总之使用 sigmoid 函数是为了确保预测的边界框位置是归一化的、稳定的并且可以跨不同尺寸的图像进行泛化。 Question那么 t w t_w tw 和 t h t_h th 为什么要使用 e e e
Answer在 YOLOv5 等物体检测模型中 t w t_w tw 和 t h t_h th即预测的宽度和高度偏移量使用指数函数通常指的是自然指数函数 e x e^x ex的原因是为了保证预测的宽度和高度是正数并且可以取任意大的值。这样做有以下几个原因
非负值保证边界框的宽度和高度必须是正数。通过使用指数函数我们可以确保即使预测的偏移量 t w t_w tw 和 t h t_h th 是负数经过指数变换后的 b w b_w bw 和 b h b_h bh即最终的宽度和高度也是正数。尺度变换的灵活性自然指数函数 e x e^x ex 提供了一个连续且单调递增的变换这意味着小的偏移量会导致小的尺度变化而大的偏移量会导致大的尺度变化。这种变换的灵活性使得模型可以更好地适应不同大小和比例的物体。数值稳定性与 sigmoid 函数类似指数函数也有助于提高数值稳定性。在训练过程中模型可能会预测很大的负数作为宽度和高度的偏移量直接使用这些值可能会导致数值问题。指数函数可以平滑这些极端值从而提高训练的稳定性。避免限制如果使用其他函数如线性函数或 ReLU来变换宽度和高度的偏移量可能会引入不必要的限制例如宽度和高度的上限。指数函数没有这样的限制因此可以更准确地预测各种大小的边界框。
4.3.3 YOLOv5 的目标框回归
YOLOv5 的目标框回归计算公式如下 b x 2 σ ( t x ) − 0.5 c x b y 2 σ ( t y ) − 0.5 c y b w p w ( 2 σ ( t w ) ) 2 b h p h ( 2 σ ( t h ) ) 2 \begin{aligned} b_x 2\sigma(t_x) - 0.5 c_x \\ b_y 2\sigma(t_y) - 0.5 c_y \\ b_w p_w(2\sigma(t_w))^2 \\ b_h p_h(2\sigma(t_h))^2 \end{aligned} bxbybwbh2σ(tx)−0.5cx2σ(ty)−0.5cypw(2σ(tw))2ph(2σ(th))2
与 YOLOv3 不同是的YOLOv5 得到的所有预测值都需要经过 σ \sigma σ 函数而且 b x , b y , b w , b h b_x, b_y, b_w, b_h bx,by,bw,bh 的求解都与 YOLOv3 有着区别。那么 YOLOv5 这么做也是有自己的考量
原始的 YOLO/DarkNet 关于 Anchor 的公式存在严重的缺陷宽度和高度完全不受限制因为有一个指数的运算这可能会导致训练时的梯度失控、不稳定、NaN 损失等等并且最终无法完成训练。
对于 YOLOv5确保通过 Sigmoid 函数对模型的所有输出进行运算从而纠正该问题。这样的损失函数也会增加正样本的个数Anchor 的个数。
4.3.4 YOLOv5 跨网格匹配策略
YOLOv5 采用了跨邻域网格的匹配策略从而得到更多的正样本 Anchor这可以加速模型收敛具体做法为
从当前网格的上、下、左、右的四个网格中找到离目标中心点最近的两个网络再加上当前网格这样一共会有三个网格进行匹配。 这样除了目标中心点落在区域内的网格①外还会使用网格②和网格③。这样正样本 Anchor 从原来的 1 扩充为 3。
参考资料
YOLOv5入门到精通不愧是公认的讲的最好的【目标检测全套教程】同济大佬12小时带你从入门到进阶YOLO/目标检测/环境部署项目实战/Python/