查看全课大纲(第 63 / 93 节)
  1. 1.计算机视觉概述
  2. 2.计算机视觉应用场景
  3. 3.困难与挑战
  4. 4.开源库介绍
  5. 5.图像显示与存储
  6. 6.CLAHE
  7. 7.腐蚀膨胀与开闭运算
  8. 8.滤波与边缘填充
  9. 9.各种滤波
  10. 10.图像金字塔
  11. 11.傅里叶变换
  12. 12.短时傅里叶变换与小波变换
  13. 13.颜色特征与颜色直方图
  14. 14.几何特征_边缘
  15. 15.几何特征-角点与斑点
  16. 16.局部特征-SIFT特征描述子
  17. 17.局部特征-SURF特征描述子
  18. 18.局部特征-ORB特征描述子
  19. 19.其他特征描述子
  20. 20.传统图像分割技术
  21. 21.GraphCut
  22. 22.GrabCut
  23. 23.人脸检测的传统方法
  24. 24.行人检测的传统方法HOG+SVM
  25. 25.行人检测DPM
  26. 26.人工神经网络的架构
  27. 27.目标函数与梯度下降
  28. 28.误差反向传播
  29. 29.可手算的神经网络
  30. 30.改进目标函数与克服梯度消散
  31. 31.神经网络之传统神经网络与深度网络的差异
  32. 32.各种梯度下降算法
  33. 33.如何选择合适的梯度下降算法
  34. 34.避免过适应
  35. 35.CNN的架构
  36. 36.CNN的误差反向传播_卷积层
  37. 37.CNN的误差反向传播_池化层
  38. 38.分类所用的数据集
  39. 39.Alexnet详解
  40. 40.VGG详解
  41. 41.GoogLeNet_v1
  42. 42.BatchNormalization
  43. 43.GoogLeNet_v3v4
  44. 44.ResNet
  45. 45.ResNeXt
  46. 46.CNN设计准则
  47. 47.图像检索与推土机距离
  48. 48.基于颜色相似性
  49. 49.基于形状相似性
  50. 50.基于纹理相似性
  51. 51.基于局部特征相似性
  52. 52.DL的二值哈希编码
  53. 53.高维空间中检索KDTree
  54. 54.高维空间中检索LSH
  55. 55.目标识别任务综述
  56. 56.RCNN
  57. 57.SPP-Net
  58. 58.Fast R-CNN
  59. 59.Faster R-CNN
  60. 60.R-CNN系列模型回顾
  61. 61.R-FCN的设计思路
  62. 62.R-FCN的实现
  63. 63.YOLOv1
  64. 64.YOLOv2_9000
  65. 65.YOLOv3
  66. 66.图像分割应用场景与数据集
  67. 67.上采样的实现方式
  68. 68.FCN网络架构的由来
  69. 69.FCN构建细节
  70. 70.DeepLabv1
  71. 71.CRF
  72. 72.DeepLabv2
  73. 73.DeepLabv3
  74. 74.特定的医学影像分割任务
  75. 75.U-Net
  76. 76.3D U-Net
  77. 77.V-Net
  78. 78.DenseNet
  79. 79.病理切片分析任务及数据集
  80. 80.病理切片分析
  81. 81.图说任务及其传统处理方法
  82. 82.深度语言模型与RNN
  83. 83.LSTM及其变种
  84. 84.NIC模型
  85. 85.SAT模型
  86. 86.图说的性能指标
  87. 87.判别模型与生成模型
  88. 88.KL散度与JS散度
  89. 89.VAE
  90. 90.GAN的基本原理
  91. 91.DCGAN与CGAN
  92. 92.Wasserstein GAN
  93. 93.SRGAN

YOLOv1

YOLO v1:单次检测的革命

小象实战讲义 · 基于深度学习的计算机视觉

在目标检测领域,R-CNN系列通过“区域提议+分类”的范式取得了巨大成功,但其“先提候选区域、再分类回归”的两阶段流程限制了速度。本节我们将介绍一种革命性的思想——YOLO(You Only Look Once),它将目标检测彻底重塑为一个单阶段的回归问题,实现了速度与精度的新平衡。学完本节,你将理解YOLO v1的核心设计、网络输出如何编码检测信息,以及其独特的损失函数构成。

💡 核心导读

  • 设计哲学:将目标检测从两阶段流水线(如RPN+分类)统一为端到端的回归任务,实现“只看一眼”。
  • 网格化预测:将输入图像划分为S×S的网格,每个网格负责预测中心落入其中的目标。
  • 输出张量结构:网络输出为S×S×(B×5+C)的张量,编码了边界框坐标、置信度及类别条件概率。
  • 损失函数:一个多任务损失,协调边界框坐标回归、置信度预测(含背景)和类别分类。
  • 后处理:通过非极大值抑制(NMS)分类别地筛选最终的检测框。

从“看多眼”到“看一眼”

回顾Faster R-CNN,其流程包含区域提议网络(RPN)生成候选框,再对每个候选框进行分类和回归。这本质上是“看多眼”的过程。YOLO的作者提出了一个根本性的问题:能否将候选框生成与目标识别合并为一个步骤?

YOLO的回答是肯定的。其基本思想是:将整个检测问题建模为一个回归问题。给定一张输入图像,网络直接预测图像中所有目标的边界框坐标、置信度以及类别概率。这意味着图像只需通过神经网络一次(“看一眼”),即可得到所有预测结果。

具体流程如下:

  1. 将输入图像Resize到固定尺寸(如448×448)。
  2. 将图像划分为S×S(例如7×7)的网格(grid cell)。
  3. 关键原则:某个目标的中心点落在哪个网格内,就由该网格负责预测该目标。
  4. 每个网格预测B个边界框(Bounding Box,YOLO v1中B=2),每个框预测5个值:中心坐标(x, y)、宽高(w, h)以及一个置信度(confidence)。
  5. 同时,每个网格还预测一组条件类别概率:P(Classi∣Object)P(\text{Class}_i | \text{Object}),即给定网格包含目标的前提下,该目标属于第i类的概率。
  6. 网络输出一个固定维度的张量,将所有预测信息打包。
  7. 最后,通过非极大值抑制(NMS)筛选出最终的检测框。

网络架构与输出编码

YOLO v1的网络结构借鉴了GoogLeNet(Inception v1)的设计。具体来说:

  • 骨干是受GoogLeNet(Inception v1)启发的自定义网络:用1×1降维卷积+3×3卷积的堆叠替代Inception模块,共24个卷积层。训练时先用其前20个卷积层(接均值池化和全连接层)在ImageNet 1000类上预训练,再把前20层迁移到检测网络,并新增4个卷积层和2个全连接层。
  • 在14×14×1024的特征图之后不再接全局平均池化,而是接新增的4个卷积层和2个全连接层。
  • 输入分辨率从224×224提升到448×448,以获得更精细的特征。
  • 激活函数使用LeakyReLU(负半轴斜率为0.1);在第一个全连接层之后接ratio=0.5的Dropout防止过拟合。

网络最终的输出是一个三维张量,其维度为 S×S×(B×5+C)S \times S \times (B \times 5 + C)。

以PASCAL VOC数据集(20个类别)和S=7,B=2为例:

  • S×S=7×7=49S \times S = 7 \times 7 = 49个网格。
  • B×5=2×5=10B \times 5 = 2 \times 5 = 10,代表每个网格预测的两个边界框的5个参数。
  • C=20C = 20,代表20个类别的条件概率。
  • 因此,输出张量维度为 7×7×307 \times 7 \times 30。

在这个30维向量中,(x,y)(x, y) 是目标中心相对于所在网格左上角的偏移,ww、hh 是相对于整幅图像宽高的比例;它们与置信度、20个类别概率一样,都被归一化到 [0,1][0,1]。

Faster R-CNN的RPN在每个位置预设9个锚框模板,YOLO v1则不用锚框,每个网格直接回归2个框。敢这样简化的前提是框的坐标仍要回归修正,初始框的形状不必很准;代价是一张图最多只有 7×7×2=987 \times 7 \times 2 = 98 个候选框,目标数超过98时必然漏检。

这个30维向量的长度对每个网格是固定的。我们可以通过代码来直观感受这个输出结构:

import numpy as np

# 模拟7x7网格中某个网格的30维输出:前10维是两个框,后20维是类别概率
np.random.seed(42)  # 固定随机种子,保证输出确定
# 两个边界框的 [x, y, w, h, confidence],均归一化到 [0, 1]
box_predictions = np.random.rand(10)
# 20个类别的条件概率,归一化后总和为1
class_logits = np.random.rand(20)
class_probs = class_logits / class_logits.sum()
grid_cell_prediction = np.concatenate([box_predictions, class_probs])

print("第一个网格(0,0)的30维预测向量(前15个值示例):")
print(np.round(grid_cell_prediction[:15], 4))
print("\n向量总长度:", len(grid_cell_prediction))
print("两个框的置信度:", np.round(grid_cell_prediction[[4, 9]], 4))
print("20个类别概率之和:", round(float(class_probs.sum()), 4))
print("结构解读:")
print("- 索引 0-4:   第一个边界框 [x, y, w, h, confidence]")
print("- 索引 5-9:   第二个边界框 [x, y, w, h, confidence]")
print("- 索引 10-29: 20个类别的条件概率 P(Class_i | Object)")
第一个网格(0,0)的30维预测向量(前15个值示例):
[0.3745 0.9507 0.732  0.5987 0.156  0.156  0.0581 0.8662 0.6011 0.7081
 0.0026 0.1219 0.1046 0.0267 0.0229]

向量总长度: 30
两个框的置信度: [0.156  0.7081]
20个类别概率之和: 1.0
结构解读:
- 索引 0-4:   第一个边界框 [x, y, w, h, confidence]
- 索引 5-9:   第二个边界框 [x, y, w, h, confidence]
- 索引 10-29: 20个类别的条件概率 P(Class_i | Object)

边界框置信度(Confidence) 的定义是YOLO的一个关键点。它反映了模型对该边界框的两方面信心:

  1. 该框内是否包含目标(Objectness)。
  2. 如果包含目标,预测的框与真实框(Ground Truth)的吻合程度。

用公式表示为: confidence=P(Object)×IoUpredtruth\text{confidence} = P(\text{Object}) \times \text{IoU}_{\text{pred}}^{\text{truth}} 其中,P(Object)P(\text{Object}) 是边界框包含目标的概率,IoUpredtruth\text{IoU}_{\text{pred}}^{\text{truth}} 是预测框与真实框的交并比。如果网格中不包含目标,则置信度应为0。

条件类别概率 P(Classi∣Object)P(\text{Class}_i | \text{Object}) 是针对网格的,而不是针对边界框的。这意味着一个网格只预测一组类别概率,被该网格负责的目标共享;因此同一网格内若出现不同类别的目标,只能表达其中一类。

在测试阶段,为了得到每个边界框对于某个类别的最终得分(Class-specific confidence score),需要将网格的条件类别概率与每个边界框的置信度相乘: Scorei=P(Classi∣Object)×confidence\text{Score}_{i} = P(\text{Class}_i | \text{Object}) \times \text{confidence} 这等价于: Scorei=P(Classi∣Object)×P(Object)×IoUpredtruth=P(Classi)×IoUpredtruth\text{Score}_{i} = P(\text{Class}_i | \text{Object}) \times P(\text{Object}) \times \text{IoU}_{\text{pred}}^{\text{truth}} = P(\text{Class}_i) \times \text{IoU}_{\text{pred}}^{\text{truth}} 这个得分同时衡量了类别概率和定位精度。

损失函数:多任务协调

YOLO使用一个统一的损失函数进行端到端训练,该损失函数是多个子损失的加权和: λcoord∑i=0S2∑j=0B1ijobj[(xi−x^i)2+(yi−y^i)2]+λcoord∑i=0S2∑j=0B1ijobj[(wi−w^i)2+(hi−h^i)2]+∑i=0S2∑j=0B1ijobj(Ci−C^i)2+λnoobj∑i=0S2∑j=0B1ijnoobj(Ci−C^i)2+∑i=0S21iobj∑c∈classes(pi(c)−p^i(c))2\begin{aligned} \lambda_{\text{coord}} &\sum_{i=0}^{S^2} \sum_{j=0}^{B} \mathbb{1}_{ij}^{\text{obj}} \left[ (x_i - \hat{x}_i)^2 + (y_i - \hat{y}_i)^2 \right] \\ + \lambda_{\text{coord}} &\sum_{i=0}^{S^2} \sum_{j=0}^{B} \mathbb{1}_{ij}^{\text{obj}} \left[ (\sqrt{w_i} - \sqrt{\hat{w}_i})^2 + (\sqrt{h_i} - \sqrt{\hat{h}_i})^2 \right] \\ + &\sum_{i=0}^{S^2} \sum_{j=0}^{B} \mathbb{1}_{ij}^{\text{obj}} (C_i - \hat{C}_i)^2 \\ + \lambda_{\text{noobj}} &\sum_{i=0}^{S^2} \sum_{j=0}^{B} \mathbb{1}_{ij}^{\text{noobj}} (C_i - \hat{C}_i)^2 \\ + &\sum_{i=0}^{S^2} \mathbb{1}_{i}^{\text{obj}} \sum_{c \in \text{classes}} (p_i(c) - \hat{p}_i(c))^2 \end{aligned}

让我们逐一解读:

  1. 坐标损失(前两行):只对负责预测目标的边界框(1ijobj=1\mathbb{1}_{ij}^{\text{obj}}=1)计算。λcoord\lambda_{\text{coord}}(论文中设为5)用于加大坐标损失的权重。
    • 第一行是中心坐标(x, y)的均方误差(MSE)。
    • 第二行是宽高(w, h)的MSE。对宽高取平方根是一个重要技巧:开方是凹函数,对大数值压缩更强,大目标的宽高误差不会因绝对数值大而主导损失,小目标的相对误差则被保留下来。
  2. 置信度损失(第三、四行):
    • 第三行:对包含目标的边界框,希望预测的置信度C^i\hat{C}_i接近其与真实框的IoU(即CiC_i)。
    • 第四行:对不包含目标的边界框(1ijnoobj=1\mathbb{1}_{ij}^{\text{noobj}}=1),希望其置信度C^i\hat{C}_i接近0。λnoobj\lambda_{\text{noobj}}(论文中设为0.5)用于降低这部分损失的权重,因为背景框数量远多于目标框。
  3. 类别损失(第五行):只对包含目标的网格(1iobj=1\mathbb{1}_{i}^{\text{obj}}=1)计算。这是网格条件类别概率的MSE损失。注意:类别损失与边界框无关,只与网格绑定。

这个损失函数巧妙地将定位、置信度(区分前景背景)和分类三个任务融合在一起,通过协调参数(λcoord\lambda_{\text{coord}}, λnoobj\lambda_{\text{noobj}})平衡各自的学习进度。

非极大值抑制(NMS)后处理

网络前向传播后,我们得到98个(7×7×2)边界框及其对应的各类别得分。直接输出会导致大量重叠的框。YOLO使用分类别的非极大值抑制(Class-specific NMS) 来筛选最终检测结果。

过程如下:

  1. 设置一个置信度阈值(如0.2),将所有边界框中所有类别的得分低于该阈值的置零。
  2. 对于每一个类别(如“狗”)独立进行: a. 找出所有边界框中对该类别得分最高的那个框。 b. 计算该框与剩余所有框的IoU。 c. 如果某个剩余框与当前最高分框的IoU超过设定阈值(如0.5),则认为它们检测的是同一个物体,将该剩余框的该类别的得分置零(注意:不是删除框,只是将该类得分归零)。 d. 在剩下的框中,重复步骤a-c,直到处理完所有框。
  3. 遍历所有边界框,对每个框取分数最高的类别;若该分数大于零,则输出该框及其类别和得分。

这个过程保证了对于图像中同一类别的多个实例(如多只狗),只要它们不重叠过度,都能被检测出来。

import numpy as np

# 模拟分类别NMS:3个预测框,只处理“dog”这一类的得分
boxes = np.array([[10, 20, 50, 60],    # [x1, y1, x2, y2]
                  [15, 25, 55, 65],
                  [200, 200, 250, 250]], dtype=float)
scores = np.array([0.90, 0.85, 0.80])  # 对应“dog”类别的得分
iou_threshold = 0.5

def compute_iou(box1, box2):
    # 计算两个矩形框的IoU
    x1 = max(box1[0], box2[0])
    y1 = max(box1[1], box2[1])
    x2 = min(box1[2], box2[2])
    y2 = min(box1[3], box2[3])
    inter_area = max(0.0, x2 - x1) * max(0.0, y2 - y1)
    area1 = (box1[2] - box1[0]) * (box1[3] - box1[1])
    area2 = (box2[2] - box2[0]) * (box2[3] - box2[1])
    union_area = area1 + area2 - inter_area
    return inter_area / union_area if union_area > 0 else 0.0

# NMS步骤:按得分从高到低处理,被抑制的框只把得分置0,不删除
scores = scores.copy()
kept = []
for i in np.argsort(-scores):
    if scores[i] == 0:
        continue
    kept.append(int(i))
    for j in np.argsort(-scores):
        if j == i or scores[j] == 0:
            continue
        iou = compute_iou(boxes[i], boxes[j])
        if iou > iou_threshold:
            print(f"框{i}(得分{scores[i]:.2f}) 抑制 框{j}(得分{scores[j]:.2f}),IoU={iou:.2f}")
            scores[j] = 0.0

print("保留的框:", kept)
print("保留框的得分:", scores[kept])
框0(得分0.90) 抑制 框1(得分0.85),IoU=0.62
保留的框: [0, 2]
保留框的得分: [0.9 0.8]

YOLO v1的特点与局限

优点:

  1. 速度快:单阶段端到端处理,在Titan X GPU上可达45 FPS,快速版可达155 FPS。
  2. 假阳性低:损失函数中专门包含对背景(无目标)置信度的惩罚项,使得模型对背景的误检较少。
  3. 全局推理:由于网络在整张图像上进行推理,它能够编码更多的上下文信息,对一些抽象或艺术化描绘的物体也有一定的识别能力。

不足:

  1. 整体精度与定位精度偏低:每个网格只预测两个边界框,且不使用预设锚框,框的形状完全由回归学习;坐标直接用MSE回归,不如Faster R-CNN基于锚框(Anchor)的回归精细,整体mAP也低于当时的SOTA(如Faster R-CNN)。
  2. 对小物体、密集物体检测效果差:这是最显著的缺点。一个网格只有一组类别概率、最多两个框:若多个不同类别的目标中心落入同一网格,必然只能保留其中一个;即使同类别,两个框也很难同时匹配到两个高IoU目标,因此鸟群、密集人群这类场景漏检严重。
  3. 召回率(Recall)相对较低:对不常见宽高比或尺度的物体,直接回归 ww、hh 的方式泛化能力有限。

📝 动手练一练

  1. 概念辨析:YOLO v1中,边界框的“置信度(confidence)”和网格的“条件类别概率”分别代表了什么信息?在计算某个边界框属于“猫”这个类别的最终得分时,为什么需要将两者相乘?
👉 点击查看参考答案
  • 边界框置信度:反映了该边界框内是否包含任何目标的概率,以及预测框与真实框的定位准确度(IoU)。公式为 confidence=P(Object)×IoUpredtruth\text{confidence} = P(\text{Object}) \times \text{IoU}_{\text{pred}}^{\text{truth}}。
  • 网格条件类别概率:反映了在该网格包含目标的前提下,这个目标属于某个特定类别(如“猫”)的概率,即 P(Class=猫∣Object)P(\text{Class}=\text{猫} | \text{Object})。

两者相乘: Score猫=P(Class=猫∣Object)×confidence=P(Class=猫∣Object)×P(Object)×IoU=P(Class=猫)×IoU\text{Score}_{\text{猫}} = P(\text{Class}=\text{猫} | \text{Object}) \times \text{confidence} = P(\text{Class}=\text{猫} | \text{Object}) \times P(\text{Object}) \times \text{IoU} = P(\text{Class}=\text{猫}) \times \text{IoU}。

这个最终得分同时表达了“这个框里是猫的可能性”和“这个框画得准不准”,是一个综合的评价指标,用于后续的非极大值抑制排序。

  1. 输出解析:假设YOLO v1在PASCAL VOC数据集上的输出是一个7×7×30的张量。对于位于第3行、第4列的网格,其30维向量的第24个元素(即0-based索引23)代表了什么信息?如果这个网格负责预测一只“狗”,那么网络需要从这个向量的哪些部分提取信息来构成最终的检测结果?
👉 点击查看参考答案
  • 第24个元素(0-based索引为23):这属于30维向量中后20个“条件类别概率”部分。索引10-29对应20个类别,索引23对应类别索引13(即从1数第14个类别)。需要查看PASCAL VOC的类别顺序才能确定具体类别,但可以确定它代表 P(Class14∣Object)P(\text{Class}_{14} | \text{Object})。

  • 构成“狗”的检测结果所需信息:

    1. 边界框:从该网格30维向量的索引0-4和5-9中,选择与真实狗目标IoU更高的那个边界框的5个参数 [x, y, w, h, confidence]。
    2. 类别概率:从索引10-29中找到“狗”类别对应的条件概率 P(Class=狗∣Object)P(\text{Class}=\text{狗} | \text{Object})。
    3. 计算最终得分:将选中的边界框的confidence与 P(Class=狗∣Object)P(\text{Class}=\text{狗} | \text{Object}) 相乘,得到该类别的最终得分。
    4. 后处理:这个得分会参与所有98个框中“狗”类别的非极大值抑制,胜出后即输出为检测结果,包含框坐标和“狗”的标签及得分。

本章小结

YOLO v1(2016年CVPR,Joseph Redmon等)是目标检测历史上一个里程碑式的工作。它打破了当时主流的“区域提议+分类”两阶段范式,勇敢地将检测任务重塑为一个单一的、端到端的回归问题。其“网格化预测”和“统一输出编码”的思想简洁而强大,为后续的单阶段检测器(如SSD、YOLO后续版本)奠定了基石。

尽管YOLO v1在定位精度和小物体检测上存在明显短板,但其在速度与精度之间取得的平衡,以及其全局推理的特性,使其在当时许多实时应用场景中脱颖而出。它深刻地启示我们:通过改变问题的建模方式,有时能带来性能上的突破性进展。

以今天的标准回看,YOLO v1已是历史技术:它的精度被YOLOv2/v3之后的YOLO系列、RetinaNet、DETR等大幅超越。今天学习它的价值,在于理解“网格+统一张量编码+多任务损失”这套单阶段检测的基本语言,而不是直接部署v1原模型。

📋 行动清单

  • 在纸上画一个7×7的网格,假设一个目标中心落在某个格子,理解“该格子负责预测此目标”的原则。
  • 手动推导一次YOLO v1损失函数的计算,理解λcoord\lambda_{\text{coord}}和λnoobj\lambda_{\text{noobj}}如何平衡不同损失项。
  • 思考:YOLO v1的“一个网格只有一组类别概率、最多两个框”的限制,在后续的YOLO v2/v3中是如何被改进的?(预习下一节)

—— 小象教研组