最近又把《You Only Look Once: Unified, Real-Time Object Detection》这篇论文翻出来重读了一遍。做目标检测的人对YOLO v1应该都很熟但熟悉不代表吃透。这篇论文最核心的思想就是把目标检测当成一个回归问题输入一张完整图像经过一个卷积网络直接输出边界框、置信度和类别概率全程只看一次。这个思路在当年两阶段方法盛行时相当反直觉效果却立竿见影——在当时的GPU上能跑到45帧每秒让实时目标检测第一次有了一个足够简单、可端到端训练的基线。这篇笔记不打算逐段翻译论文而是从“为什么要这么设计”的角度把网格划分、输出张量、损失函数、训练技巧这几个关键点拆开讲清楚。无论你是刚接触检测的小白还是想复现经典模型的老手应该都能从里面找到一些值得琢磨的细节。1. 论文在解决什么问题1.1 目标检测为什么不是“分类加定位”这么简单在YOLO出现之前主流检测思路大概分两类一类是滑动窗口加分类器一类是候选区域加分类器。滑动窗口方法在图像上枚举不同位置和尺度然后用分类器判断窗口里有没有物体、是什么类别计算量巨大而且窗口数量很难控制。候选区域方法把检测分成两步先用一个模块生成一堆可能包含物体的区域再对每个区域做分类和边界框回归。这套流程精度不错但速度被区域生成和逐区域分类拖住了很难做到实时。更麻烦的是分类器看到的区域是裁剪出来的缺少物体周围的上下文信息容易把背景纹理误判成物体。YOLO v1想解决的问题很明确能不能用一个统一的模型一次前向把“哪里有物体、物体是什么、边界框在哪”全部输出这个问题的难点在于检测天然是变长输出而卷积网络的结构是固定的。分类任务输出一个固定长度向量就行但检测不知道图像里到底有几个物体。YOLO v1的策略是不预测任意数量的框而是把图像划分成固定网格每个网格最多负责几个框这样输出就是定长的也就能用一个普通的回归损失来训练整个网络。1.2 一次前向直接给答案的回归思路YOLO v1将输入图像划分成S×S的网格论文里S7。如果某个物体的中心落在某个网格内这个网格就负责预测它。每个网格要输出两类信息B个边界框以及对应的置信度还有该网格在所有类别上的概率分布。整个网络的输出张量是S×S×(B×5C)训练时拿它和一个同样维度的目标张量算损失。这个设计把检测的“多物体、变长”问题重新表述成“每个网格是否包含物体、包含什么物体”的固定网格预测问题从而绕开了变长输出。这种把所有任务压进一次前向的做法就是后来大家常说的单阶段检测。它没有显式的候选区域生成阶段也没有逐区域分类阶段。模型在全图上做推理时间复杂度只和网络前向计算相关因此能达到很高的帧率。和两阶段方法相比它牺牲了一点精度但换来的是流程极简和速度领先一个数量级。1.3 读这篇论文需要什么基础如果你正在学习目标检测这篇论文值得精读。读之前只需要对CNN分类任务有基本了解知道卷积、池化、全连接层、损失函数这些概念论文里的设定几乎都讲清楚了。如果你做工程YOLO v1本身的精度在今天已经算不上亮眼但很多思路比如网格划分、动态置信度标签、损失权重、NMS后处理在后来各种检测模型里到处都能看到影子。理解了YOLO v1再去看YOLO v2/v3、SSD、甚至一些anchor-free方法会发现很多设计是一脉相承的。2. 核心设计拆解网格、置信度与损失函数2.1 7×7网格与30维输出张量输入图像进入网络前会被缩放到448×448。经过卷积和全连接后最终输出是一个7×7×30的张量。7×7对应49个网格30的组成是每个网格预测2个边界框每个框有5个值x、y、w、h、置信度所以框相关部分是2×510个值剩余20个值是该网格在20个类别上的条件类别概率。302×520这是从头到尾都需要记住的维度关系。这里的x和y不是图像里的绝对坐标而是边界框中心相对当前网格左上角的偏移归一化到0到1之间w和h则是边界框相对整幅图像的宽高比同样归一化到0到1。归一化能让不同尺寸的物体在输出空间里有统一的尺度训练时更稳定。很多初学者直接把x、y当成图像坐标去还原结果所有框都跑到左上角去了。正确还原方式是把x加上网格索引再除以S。2.2 置信度到底在预测什么论文里置信度的定义是 Pr(Object) × IoU(pred, truth)。一个框的置信度包含两层含义这个框里有没有物体以及即使有物体预测框和真实框的重合程度有多高。如果网格里没有物体理想置信度是0。如果网格里有物体理想置信度应该等于预测框与真实框的IoU。注意这个IoU是动态变化的预测框在训练过程中一直在变所以监督目标不是固定不变的0或1。推理时置信度要和类别概率相乘得到最终的类别置信度分数Pr(Class_i | Object) × Pr(Object) × IoU。这样得到的分数既反映了物体类别的可能性又反映了位置准确度。后续做阈值过滤和NMS使用的都是这个分数。实际实现时先对输出张量解码得到每个框的位置、置信度和类别概率然后逐类别做NMS。如果不理解这个乘积的含义后处理就会做得莫名其妙。2.3 每个网格为什么允许预测多个框一个网格预测B2个框但在训练时每个物体只让一个框负责。论文的做法是计算该网格内两个预测框与真实框的IoU谁的IoU高谁就负责预测这个物体。另一个框即使靠得近也不承担坐标回归任务。这种“赢者通吃”的机制相当于在给两个框做隐式分工避免它们同时冲向同一个物体导致重复预测。但也要看到B2并不等于一个网格能输出两个不同物体。两个框共享同一组类别概率也就是说即使两个框都预测了物体它们也只能是同一类。这也是YOLO v1在密集场景下表现不佳的原因之一。后来YOLO v2把类别预测也改成每个框独立本质上就是在缓解这个限制。2.4 损失函数五个误差的加权和损失函数是YOLO v1里信息量最大的一部分用公式表示大概是这样的Loss λcoord * Σ 1_ij^obj * [(x_i - x_hat_i)^2 (y_i - y_hat_i)^2] λcoord * Σ 1_ij^obj * [(√w_i - √w_hat_i)^2 (√h_i - √h_hat_i)^2] Σ 1_i^obj * (C_i - C_hat_i)^2 λnoobj * Σ 1_i^noobj * (C_i - C_hat_i)^2 Σ 1_i^obj * Σ_c (p_i(c) - p_hat_i(c))^2符号说明一下1_ij^obj表示第i个网格的第j个预测框是否负责某个真实物体1_i^obj表示第i个网格内是否存在物体。第一项和第二项是负责物体的预测框的坐标损失第三项是有物体时的置信度损失第四项是没有物体时的置信度损失第五项是类别损失。每个网格只有一个真实类别标签所以类别损失只在有物体的网格计算而置信度损失无论网格有没有物体都要计算因为网络在空网格上也会给出置信度输出必须把它往0压。实际训练时绝大多数网格都是空的。如果无物体置信度损失的权重和坐标一样大模型很快就会学会把所有框都预测得很含糊以此压低无目标的损失这是训练里最常见的陷阱之一。论文把无物体置信度权重设成0.5坐标损失权重设成5是在平衡“找位置”和“找物体”两个目标。理解这个逻辑你改到自己的任务时才不会瞎抄数字。2.5 为什么坐标损失要开平方根这是很多人容易忽略的细节。损失函数里宽度和高度用的是(√w - √ŵ)²而不是直接用(w - ŵ)²。原因在于同样几像素的偏差对一个大框和一个小框来说影响完全不同。大框偏移几像素IoU可能掉得不多小框同样的绝对偏移IoU可能暴跌。如果直接用w、h的平方差大框的误差会主导梯度。开平方根之后函数斜率随着数值增大而减小相当于给小框的误差增加了权重让模型更认真地学习小目标的尺寸。打个比方一个身高1.8米的人测身高差2厘米无所谓一个刚出生的婴儿测量差2厘米就可能问题很大。模型训练的目标是让每个物体的框都准而不是让大框更准。YOLO v1的定位能力本来就弱这个设计至少表达了对小目标的重视虽然最终效果有限但思考方向是对的。3. 网络结构与训练细节3.1 主干网络24个卷积加2个全连接YOLO v1的主干网络借鉴了当时分类网络的成熟做法。整体由24个卷积层和2个全连接层组成大量使用1×1卷积做通道压缩再用3×3卷积提取特征。1×1卷积的作用是在降低通道数的同时保留空间信息既能减少计算量又能增加非线性表达。这个思路类似GoogLeNet的Inception结构但没有那么复杂的分支。全连接层的作用是把特征图映射成固定大小的输出张量也就是7×7×30。从现在的角度看24层卷积算不上深但放在当时的算力条件下已经是很重的模型。真正的计算瓶颈和参数大户其实在全连接层。全连接层把二维特征图展开成一维向量空间结构被强行压平参数量也很大。后续YOLO版本迅速抛弃全连接层改成全卷积加多尺度输出这正好印证了YOLO v1在结构上的时代局限。3.2 两阶段训练先分类预训练再检测微调论文采用分类预训练加检测微调的经典路线。先用前20层卷积接一个平均池化和一个全连接层在ImageNet分类任务上预训练输入分辨率是224×224。预训练的目的很直接大规模分类数据能让卷积层学到通用的视觉特征比如边缘、纹理、形状。检测数据集的规模通常小很多如果从零训练卷积层很难收敛到足够好的特征。预训练完成后去掉分类用的全连接层保留前20层卷积再追加4个卷积层和2个全连接层并把输入分辨率从224提高到448。为什么检测时要用更高的分辨率因为小目标检测需要更细的空间信息输入分辨率越高7×7网格的每一格能对应到更细的图像区域。但高分辨率输入也增加计算开销所以只在检测微调阶段使用预训练阶段用224可以省时间。3.3 训练超参数与数据增强论文的检测训练细节很具体batch size为64动量为0.9权重衰减为0.0005dropout为0.5激活函数使用带泄漏的线性整流单元。学习率采用分段调整第一个epoch做一个从0.001到0.01的线性warmup避免训练初期梯度过大把网络带偏之后用0.01训练75轮再用0.001训练30轮最后用0.0001训练30轮。这个warmup策略现在很常见但在当时的检测模型里并不多见。数据增强方面论文使用了随机缩放、随机平移和HSV色彩空间抖动。随机缩放和平移让模型对物体位置和尺度变化更鲁棒HSV抖动模拟光照变化让模型对颜色变化不过度敏感。没有用复杂的随机裁剪拼接而是用这些基础增强说明检测模型的训练对增强策略很敏感做过头反而会破坏物体中心落在网格内的假设。3.4 推理细节解码、过滤、NMS推理时输入一张448×448图像网络输出7×7×30张量。首先要做解码把x、y加上网格左上角的偏移再除以7还原成相对于原图的坐标w、h本来就是归一化值直接乘以图像宽高即可。每个网格有2个框49个网格一共产生98个框。这个数量很小后处理非常快。解码后每个框有置信度每个网格有20个类别概率。把每个框的置信度与该网格的20个类别概率逐项相乘得到该框对每个类别的分数。然后先按阈值过滤掉低分框再按类别分别做非极大值抑制。NMS的基本流程可以用一段很短的伪代码表达def nms(boxes, scores, threshold0.4): # 按分数从高到低排序 idx sorted(range(len(boxes)), keylambda i: scores[i], reverseTrue) keep [] while idx: i idx[0] keep.append(i) for j in idx[1:]: if iou(boxes[i], boxes[j]) threshold: idx.remove(j) idx idx[1:] return keep这段代码的逻辑是每一轮保留当前最高分的框把所有和它IoU过高的框删掉再处理剩下的框。NMS阈值通常设在0.4到0.5之间实际使用时要看具体场景来调。4. 效果、局限与适用场景4.1 实时性与全局上下文推理YOLO v1在当年的VOC 2007测试集上能在GPU上达到45 FPS的检测速度通过减少卷积层还能跑出更快的版本。相比之下同期主流方法的速度只有它的几十分之一。这个速度优势来自一次前向的设计没有候选区域生成没有逐区域缩放分类所有计算都集中在一次卷积网络前向中。除了快YOLO v1还有一个容易被忽略的优点它对图像全局上下文有感知。因为整个网络的输入是完整图像输出是全局网格不像两阶段方法那样只看裁剪出来的候选区域所以不容易把背景中的小块纹理误判成物体。论文里用艺术作品做了泛化测试在毕加索和人物艺术数据集上YOLO v1的表现明显好于当时基于区域的方法说明它学到的不是训练集特有的表面模式而是更通用的物体表示。4.2 小目标、密集目标与定位精度问题论文自己的错误分析显示YOLO v1最主要的错误来源是定位误差而不是分类误差。7×7的网格相当于把图像强行划分成49个区域如果一个格子里挤了两个物体或者物体只占了很小一块区域模型就很难同时给出准确的边界框。每个格子只能输出两个框两个框还共享一组类别概率进一步限制了密集目标的处理能力。这种限制在小目标和群体目标场景下特别明显。一群鸟、一排人、远处的小车这些在现实场景中非常常见的分布YOLO v1几乎无法正确处理。此外由于x、y是相对网格的偏移当物体中心落在网格边界附近时微小的预测误差就可能导致框偏移较大。这些局限在论文里都有清楚复盘后来YOLO v2通过引入锚框、更高分辨率、多尺度等机制逐一缓解也算是验证了v1的问题诊断方向是对的。4.3 和其他方法放一起看方法是否分阶段速度特性主要问题滑动窗口分类器不分阶段很慢窗口数量爆炸难以端到端R-CNN系列两阶段慢候选区域生成拖累速度YOLO v1单阶段快网格限制小目标弱这个对比想说明的是YOLO v1在当时选择了“牺牲部分精度换速度”的路线。它的精度不是最高的但速度领先一个数量级以上而且在很多场景下精度下降可以接受。这种权衡思路后来被SSD等模型继承用多尺度特征图进一步改善了小目标检测但单阶段的基本范式并没有改变。4.4 YOLO v1适合用在哪里如果你要做实时视频流检测、嵌入式端到端部署、或者希望用一个小而完整的检测模型快速验证想法YOLO v1的框架依然有参考价值。它的全卷积思路可以被替换成更轻量的骨干网络输出头也可以改造成更适合特定任务的格式。如果目标是小目标或密集场景那就别选它直接看后面的目标检测方案更实际。理解v1的意义更多在于它提供了一个极简的起点让很多概念有了具体的载体。5. 阅读和复现YOLO v1容易踩的坑5.1 搞错输出张量的维度顺序复现YOLO v1时最常出问题的是张量维度顺序。网络的输出可以理解成7×7×(2×520)具体排列上前10个值是第一个框的x、y、w、h、置信度以及第二个框的x、y、w、h、置信度后20个值是对应网格的类别概率。有的实现可能把类别概率放在前面或者把两个框独立成两个通道导致解码时坐标和类别对不上。阅读代码时建议先打印一次输出的形状和数值范围确认是哪一种排列再做解码。另一个容易出错的地方是坐标还原。x、y是归一化到0和1之间的网格内偏移还原公式应该是(grid_x x) / S而不是直接用x。w、h是相对整幅图的归一化还原成像素时要乘以图像宽高。如果忘记给x、y加上网格索引所有框都会堆到每个格子的左上角。5.2 置信度标签是动态IoU不是0和1训练时网络输出的置信度要逼近什么值很多人以为是“有物体就是1没物体就是0”。论文的设定更细致有物体时置信度目标等于当前预测框与真实框的IoU没有物体时目标为0。这意味着每个训练步骤里置信度的标签都会随着预测框的变化而变化。实现时需要在前向之后先计算IoU再拿着IoU去构建损失函数里的目标值。这个动态标签是非常关键的实现细节。如果直接把置信度标签设成0和1会让模型误以为只要框里大概有个物体置信度就必须很高从而忽略框的位置是否准确。动态IoU把“有没有物体”和“框得准不准”揉在同一个监督信号里这也是推理时置信度能和类别概率直接相乘的原因。5.3 NMS要按类别做而且要放到解码之后后处理里最容易被忽略的是类别维度。不同类别的物体可以合法地重叠在一起比如一个人戴着头盔人脸检测和头盔检测可能各自给出一个高置信度框这两个框重叠但分属不同类别NMS不能跨类别把它们抑制掉。正确做法是解码出所有候选框后对每一个类别单独排序、单独抑制。NMS的输入在一开始可能看起来数量很少但正因为少才更要注意阈值。同一个物体可能被相邻两个格子同时预测出来产生两个高IoU的框NMS的作用就是去掉这种重复框。阈值选得过大会留下大量重复框选得太小又可能把真正相邻的不同物体误删。实际项目中建议先在验证集上试几个阈值不要只靠经验定死。5.4 损失函数的权重不是随便设的λcoord5、λnoobj0.5这两个值背后有清晰的逻辑。图像中大部分区域是背景网格里没有物体的数量远超有物体的数量。如果不压低无物体置信度损失的权重模型会把所有框的置信度都降到很低的水平因为这样能让总体损失快速下降但检测就失效了。另一方面坐标误差对最终检测效果的影响比分类误差更直接所以用λcoord5把它放大。理解了这个逻辑你在改写成自己的检测任务时就知道该怎么调整权重而不是原封不动地抄数字。还要注意(√w-√ŵ)²的实现里开根号的对象是归一化后的宽高值。如果宽高已经缩放到0到1之间开根号后数值范围也在0到1之内直接计算没有问题。但如果在某些复现里宽高被还原成了像素值再开根号数值范围会完全不同损失量级也会变化和坐标项、置信度项的比例就失真了。6. 重读后的个人体会重读YOLO v1我最大的体会是它真正的贡献不是“一个能跑45帧的检测器”而是把目标检测重新定义成“回归问题”这个框架。这个框架让检测模型可以端到端训练让速度不再是瓶颈也让后来一系列单阶段模型有了可以吐槽、可以改进的基准。它的粗糙之处比如固定网格、共享类别概率、全连接层带来的参数浪费恰恰给后来者指明了改进方向。如果你打算自己复现我的建议是从损失函数入手。先别急着搭完整网络而是把7×7×30的输出张量、动态IoU、NMS这三块写清楚再套一个简单的骨干网络在公开数据集上跑一轮。这个过程会逼你弄清楚每个维度的含义比读十遍论文都有效。当你能从一张特征图里熟练地解出98个框再按类别做完NMSYOLO v1在你眼里就没有秘密了。最后再分享一个小技巧看论文公式时把每个符号的下标拆开对应到张量的具体维度上。YOLO v1里1_ij^obj、1_i^obj这些符号初看很绕但你只要记住“ij表示哪个网格的哪个框i表示哪个网格”整个损失函数就变成几行代码了。这种把公式翻译成张量操作的习惯对读后续的YOLO系列论文也非常有用。