Skip to content

YOLO 原理和结构概述

给一张图片,给定目标物体,要求从照片中检测出目标物体——计算机视觉中检测和分割是长久而热门的问题。

伟大的 RCNN

RCNN 是将 CNN 方法引入目标检测任务的伟大算法,深度学习时代第一个重要的目标检测算法。

R-CNN (Regions with CNN features)

工作流程

  1. 使用 Selective Search 生成约 2000 个候选区域
  2. 将每个区域缩放到固定尺寸
  3. 使用 CNN 提取特征
  4. 使用 SVM 分类
  5. 使用回归器精修边界框

⚠️ 存在的问题

  • 训练过程分为多个阶段
  • 特征提取重复计算
  • 速度慢(测试时间约 47s/张)
  • 需要大量磁盘空间存储特征

Fast R-CNN

Fast R-CNN 对 R-CNN 进行了重要改进。

主要创新

  1. 共享卷积计算
  2. ROI Pooling 层
  3. 多任务损失函数(分类 + 回归)

✅ 改进效果

  • 训练和测试速度提升
  • 精度提升
  • 单阶段训练
  • 不需要磁盘存储

⚠️ 仍存在的问题

  • 仍依赖 Selective Search
  • 候选区域生成是瓶颈(~2s/张)

Faster R-CNN

Faster R-CNN 通过引入 RPN 网络,实现了端到端的目标检测。

核心创新

  1. 区域建议网络(RPN)
  2. Anchor 机制
  3. 特征共享

✅ 优势

  • 检测速度显著提升(~0.2s/张)
  • 可端到端训练
  • 精度进一步提升

⚠️ 局限性

  • 仍是两阶段方法
  • 实时性有限
  • 网络结构复杂

YOLO 的原理

YOLO 就是 You Only Look Once

Bounding Box

如果让你去用矩形框框出目标,那么这个矩形框的信息怎么储存呢?我们使用 bounding box,bbox 有很多种,YOLO 里的 bbox 以如下参数定位矩形框:

  • cx, cy:中心点的横纵坐标
  • w, h:矩形框的宽和高,但是注意这里用的是归一化的策略,也就是占比——如果矩形框高 300,整幅图片高 600,那么 h 就是 0.5
  • c:置信度

YOLO 的创新点

之前的目标检测都难以摆脱遍历图像和其它计算量大的算法。YOLO 团队就想:你分类器输出的是向量,那我直接换成 (x,y,w,h,c),把问题转化为回归问题,直接回归出 bbox 的位置不就好了?

于是 YOLO 就把图片分割成 S×S 个 grid,每个 grid 大小相等。传统的思路就是每个框只能识别一个物体,而且物体必须在 grid 内,YOLO 放低了这个要求,只要求物体的中心在 grid 内。第一次预测的效果可能是这样的:

image

image

那么问题来了,每个 grid 都有自己的框,这样才能扫描整个图像,但框太多了完全没有重点和效果。这个时候就要用到 IOU(交并比)了:

两个框分别为 B1,B2,它们的交并比为:

IOU=B1B2B1B2

置信度 confidence=Pr(obj)×IOUtruthpred,其中 Pr(obj) 表示一个 grid 有物体的概率,用来预测有多大把握这个物体在某个 grid 里。

如果使用聚类,检测左边瓶子的 grid 里选择 confidence 最大的,其它都不保留。这确实是删去多余框的办法,但问题是如果目标很小,一个框里有多个目标,两个目标物体本身也很近,容易被识别成一个物体。

这就要使用**非极大值抑制(NMS)**了。如果两个框重合度很高,大概率是一个目标,那就选择 confidence 较大的框,一旦两个框的 IOU 大于了我们设置的阈值,就保留置信度大的。

对于小目标检测,YOLO 给出的方法很直白:每个 grid 生成两个框,一个负责大目标,一个负责小目标。

这是它的损失函数:

λcoordi=0S2j=0B1ijobj[(xix^i)2+(yiy^i)2]+λcoordi=0S2j=0B1ijobj[(wiw^i)2+(hih^i)2]+i=0S2j=0B1ijobj(CiC^i)2

置信度误差(边框内有对象)

+λnoobji=0S2j=0B1ijnoobj(CiC^i)2

置信度误差(边框内无对象)

+i=0S21iobjcclasses(pi(c)p^i(c))2

然后 YOLOv1 被刊物拒稿了……

现代 YOLO 的结构

  • YOLOv8
  • MMYOLO

image

Backbone

YOLO 负责特征提取的部分,YOLOv8 后 C2f 模块替代了 C3 模块,人们可以手动替换这些模块,只需要在源文件里定义模块,然后在 .yaml 文件中写好 backbone 即可:

yaml
backbone:
  # [from, repeats, module, args]
  - [-1, 1, Conv, [64, 3, 2]]       # 0-P1/2 第0层,-1代表将上层的输入作为本层的输入。第0层的输入是640*640*3的图像。Conv代表卷积层,相应的参数:64代表输出通道数,3代表卷积核大小k,2代表stride步长。
  - [-1, 1, Conv, [128, 3, 2]]      # 1-P2/4 第1层,本层和上一层是一样的操作(128代表输出通道数,3代表卷积核大小k,2代表stride步长)
  - [-1, 3, C2f, [128, True]]       # 第2层,本层是C2f模块,3代表本层重复3次。128代表输出通道数,True表示Bottleneck有shortcut。
  - [-1, 1, Conv, [256, 3, 2]]      # 3-P3/8 第3层,进行卷积操作(256代表输出通道数,3代表卷积核大小k,2代表stride步长),输出特征图尺寸为80*80*256(卷积的参数都没变,所以都是长宽变成原来的1/2,和之前一样),特征图的长宽已经变成输入图像的1/8。
  - [-1, 6, C2f, [256, True]]       # 第4层,本层是C2f模块,6代表本层重复6次。256代表输出通道数,True表示Bottleneck有shortcut。经过这层之后,特征图尺寸依旧是80*80*256。
  - [-1, 1, Conv, [512, 3, 2]]      # 5-P4/16 第5层,进行卷积操作(512代表输出通道数,3代表卷积核大小k,2代表stride步长),输出特征图尺寸为40*40*512(卷积的参数都没变,所以都是长宽变成原来的1/2,和之前一样),特征图的长宽已经变成输入图像的1/16。
  - [-1, 6, C2f, [512, True]]       # 第6层,本层是C2f模块,6代表本层重复6次。512代表输出通道数,True表示Bottleneck有shortcut。经过这层之后,特征图尺寸依旧是40*40*512。
  - [-1, 1, Conv, [1024, 3, 2]]     # 7-P5/32 第7层,进行卷积操作(1024代表输出通道数,3代表卷积核大小k,2代表stride步长),输出特征图尺寸为20*20*1024(卷积的参数都没变,所以都是长宽变成原来的1/2,和之前一样),特征图的长宽已经变成输入图像的1/32。
  - [-1, 3, C2f, [1024, True]]       # 第8层,本层是C2f模块,3代表本层重复3次。1024代表输出通道数,True表示Bottleneck有shortcut。经过这层之后,特征图尺寸依旧是20*20*1024。
  - [-1, 1, SPPF, [1024, 5]]         # 9 第9层,本层是快速空间金字塔池化层(SPPF)。1024代表输出通道数,5代表池化核大小k。结合模块结构图和代码可以看出,最后concat得到的特征图尺寸是20*20*(512*4),经过一次Conv得到20*20*1024。

比如说可以在其中插入注意力模块(SE、CBAM)等。

Neck

负责多尺度特征融合。PAN-FPN 结构。

image

yaml
head:
  - [-1, 1, nn.Upsample, [None, 2, 'nearest']]   # 第10层,本层是上采样层。-1代表将上层的输出作为本层的输入。None代表上采样的size(输出尺寸)不指定。2代表scale_factor=2,表示输出的尺寸是输入尺寸的2倍。nearest代表使用的上采样算法为最近邻插值算法。经过这层之后,特征图的长和宽变成原来的两倍,通道数不变,所以最终尺寸为40*40*1024。
  - [[-1, 6], 1, Concat, [1]]                    # cat backbone P4 第11层,本层是concat层,[-1,6]代表将上层和第6层的输出作为本层的输入。[1]代表concat拼接的维度是1。从上面的分析可知,上层的输出尺寸是40*40*1024,第6层的输出是40*40*512,最终本层的输出尺寸为40*40*1536。
  - [-1, 3, C2f, [512]]                          # 12 第12层,本层是C2f模块,3代表本层重复3次。512代表输出通道数。与Backbone中C2f不同的是,此处的C2f的bottleneck模块的shortcut=False。
  - [-1, 1, nn.Upsample, [None, 2, 'nearest']]   # 第13层,本层也是上采样层(参考第10层)。经过这层之后,特征图的长和宽变成原来的两倍,通道数不变,所以最终尺寸为80*80*512。
  - [[-1, 4], 1, Concat, [1]]                    # cat backbone P3 第14层,本层是concat层,[-1,4]代表将上层和第4层的输出作为本层的输入。[1]代表concat拼接的维度是1。从上面的分析可知,上层的输出尺寸是80*80*512,第4层的输出是80*80*256,最终本层的输出尺寸为80*80*768。
  - [-1, 3, C2f, [256]]                          # 15 (P3/8-small) 第15层,本层是C2f模块,3代表本层重复3次。256代表输出通道数。经过这层之后,特征图尺寸变为80*80*256,特征图的长宽已经变成输入图像的1/8。
  - [-1, 1, Conv, [256, 3, 2]]                   # 第16层,进行卷积操作(256代表输出通道数,3代表卷积核大小k,2代表stride步长),输出特征图尺寸为40*40*256(卷积的参数都没变,所以都是长宽变成原来的1/2,和之前一样)。
  - [[-1, 12], 1, Concat, [1]]                   # cat head P4 第17层,本层是concat层,[-1, 12]代表将上层和第12层的输出作为本层的输入。[1]代表concat拼接的维度是1。从上面的分析可知,上层的输出尺寸是40*40*256,第12层的输出是40*40*512,最终本层的输出尺寸为40*40*768。
  - [-1, 3, C2f, [512]]                          # 18 (P4/16-medium) 第18层,本层是C2f模块,3代表本层重复3次。512代表输出通道数。经过这层之后,特征图尺寸变为40*40*512,特征图的长宽已经变成输入图像的1/16。
  - [-1, 1, Conv, [512, 3, 2]]                   # 第19层,进行卷积操作(512代表输出通道数,3代表卷积核大小k,2代表stride步长),输出特征图尺寸为20*20*512(卷积的参数都没变,所以都是长宽变成原来的1/2,和之前一样)。
  - [[-1, 9], 1, Concat, [1]]                     # cat head P5 第20层,本层是concat层,[-1, 9]代表将上层和第9层的输出作为本层的输入。[1]代表concat拼接的维度是1。从上面的分析可知,上层的输出尺寸是20*20*512,第9层的输出是20*20*1024,最终本层的输出尺寸为20*20*1536。
  - [-1, 3, C2f, [1024]]                         # 21 (P5/32-large) 第21层,本层是C2f模块,3代表本层重复3次。1024代表输出通道数。经过这层之后,特征图尺寸变为20*20*1024,特征图的长宽已经变成输入图像的1/32。
  # 以上是Neck部分,YOLO里Neck和head都在head里。
  - [[15, 18, 21], 1, Detect, [nc]]              # Detect(P3, P4, P5) 第22层,本层是Detect层,[15, 18, 21]代表将第15、18、21层的输出(分别是80*80*256、40*40*512、20*20*1024)作为本层的输入。nc是数据集的类别数。

执行最终检测任务,包含一个检测头和一个分类头。.yaml 文件中的 Detect 层部分:

yaml
- [[15, 18, 21], 1, Detect, [nc]]  # Detect(P3, P4, P5) 本层是Detect层,[15, 18, 21]代表将第15、18、21层的输出(分别是80*80*256、40*40*512、20*20*1024)作为本层的输入。nc是数据集的类别数。