ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

YOLOv1核心原理与复现指南:单阶段目标检测的起点

YOLOv1核心原理与复现指南:单阶段目标检测的起点 如果你现在打开YOLOv8的源码再回头看YOLOv1会忍不住觉得它简单得像玩具没有anchor、没有FPN、没有CSPDarknet连BatchNorm都用得小心翼翼。但2016年这篇论文发表的时候目标检测圈正卡在“跑得动的不准准的跑不动”这个尴尬节点上。YOLOv1直接把检测当成回归问题来做用一张448×448的图像一次前向就同时输出所有边界框和类别概率在当时把检测速度拉到了45 FPS一举改变了整个领域的技术路线。这篇文章不是论文翻译而是我结合论文和实际复现踩坑的完整解读。我会从它解决的问题出发把网格划分、网络结构、损失函数、训练推理这些核心细节拆开讲透最后再聊一聊自己动手实现时最容易翻车的几个点。无论你是刚接触目标检测想找入口还是已经用过YOLOv5/YOLOv8想回头补基础这篇都能帮上忙。1. 从两阶段到单阶段YOLOv1在2016年解决了什么1.1 当时的主流检测器慢在哪在YOLOv1之前目标检测的主流方案是R-CNN家族。R-CNN的思路是先通过Selective Search等算法在图像上挖出大约2000个候选区域然后把这些候选区域逐个缩放、挨个送进CNN提取特征最后再用SVM和回归器做分类、精修框。这个流程的精度确实高但慢也是真的——2000个候选区域要过2000次网络一张图在GPU上也要好几秒CPU上更是能跑到几十秒一张。后来的Fast R-CNN做了一个重要改进把整张图先过一次CNN生成共享特征图再用ROI Pooling从特征图上抠出每个候选区域对应的特征规避了“每个候选区域都过一遍网络”的浪费。但问题也随之而来候选区域的生成还是靠Selective Search这个CPU算法它本身是独立于网络的耗时依然不低整体离实时差得很远。Faster R-CNN后来用Region Proposal NetworkRPN把候选框生成也塞进了网络速度有所提升但依然是“先提框、再分类”的两阶段结构pipeline复杂部署和训练都不太省心。在这种背景下大家其实已经意识到能不能绕开候选区域一步到位直接输出所有目标的位置和类别YOLOv1就是对这个问题的正面回答。1.2 YOLOv1的核心提法检测就是回归YOLOv1的全称是You Only Look Once字面意思就是“你只看一次”。它把目标检测重新定义成一个端到端的回归问题输入一张图像经过一个单一神经网络直接输出边界框坐标、框内目标的置信度以及目标属于各个类别的概率。这和两阶段检测器的本质区别在哪里两阶段是“先找可能含目标的位置再判断这些位置是什么”本质上是两步走YOLOv1则是把整张图均匀划分成网格让每个网格直接负责“我这个区域里有没有目标、目标在哪、目标是什么”所有预测都在一次前向里完成。它没有显式的候选区域提取过程也没有独立的区域分类器整个系统从输入到输出是“one piece”。这种设计带来的最大好处是速度和全局信息。速度上所有计算天然共享一张图一次推理就能出全部结果架构简单到可以藏在实时的视频流里。全局信息上YOLOv1在预测每个目标时能看到整张图的上下文不像滑窗或候选区域那样容易只见局部所以背景误检率明显更低。1.3 一个网格、一个边界框、一组类别的极简哲学YOLOv1的预测逻辑可以浓缩成一句话把图像切成S×S个网格每个网格负责预测中心点落在该网格内的目标。论文里S取7所以输入图被分成7×7共49个网格。每个网格要做两件事一是预测B个边界框论文里B2每个框由位置和大小信息组成二是预测C个类别概率VOC数据集上C20。边界框的置信度表示“这个框里有没有目标以及有目标的话框得有多准”类别概率则表示“如果这个网格里有目标它属于哪个类别”。最后把所有预测拼起来就得到了一个S×S×(B×5C)的张量也就是7×7×30。这个张量是YOLOv1所有魔法发生的地方下一节我就重点拆它。你也可以把这种思路理解成把整张图像当作一个巨大的标签网络的任务就是把图像“翻译”成一张固定大小的预测表不需要再做任何后处理来生成候选框。2. 输入输出的几何直觉7×7×30张量是怎么来的2.1 把图片切成网格后每个格子要干什么我先用一个简化例子说明。假设输入图是448×448网络最后输出的是7×7的特征图那么原图中每64×64个像素就对应一个网格。如果某个目标的中心点落在第(3,5)个网格内那么这个网格就是所谓的“responsible”也就是由它来负责预测这个目标。这里有个关键细节不是目标所在的整个区域都由网格负责只看中心点落在哪个网格里。一个目标即使跨了好几个网格最终也只会分配给它中心点所在的那个网格。这个规则从训练标注就开始用后面所有坐标损失、类别损失都围绕“目标中心落在哪个网格”来划分。每个网格预测B个边界框论文B2但并不是两个框都去拟合目标。训练时我们会分别计算两个预测框和真实目标框的IoUIoU更高的那个框被选为“负责”预测该目标的框另一个框如果和该目标匹配就也算正样本但整体上最终落地时每个目标只有最优的那个框真正参与定位回归。这个设计我后面讲损失函数时会再展开。2.2 30维输出怎么拆55207×7×30张量里的30维由三个部分构成每个格子预测B2个边界框每个框有5个值x, y, w, h, confidence所以是2×510再加上C20个类别概率。于是总维度就是5×22030。这30个数依次的含义是前5个第一个边界框的x、y、w、h、confidence接着5个第二个边界框的x、y、w、h、confidence最后20个该网格内目标属于20个类别的条件概率Pr(Class_i|Object)。注意类别概率是网格共享的不是每个框一套。也就是说YOLOv1里一个网格最多只能输出一组类别概率这是它在设计上的重要约束。如果一个网格里同时有猫和狗的中心点YOLOv1只能二选一这是它后来被YOLOv2改进的动机之一。2.3 训练时的编码与预测值的关系要理解x、y、w、h的含义必须搞清它们的归一化方式。YOLOv1里x和y是目标中心点相对当前网格左上角的偏移用网格宽高归一化所以通常在0到1之间。w和h是目标宽度和高度相对整张图像宽高的比例也在0到1之间。confidence是预测框与真实框的IoU乘以一个“是否有目标”的指示值。为什么要这样编码因为网络不可能直接回归像素值几百像素那样数值尺度太大、不同尺寸的目标差异也大网络很难学到稳定的映射。归一化之后所有输出都落在0到1这个量级配合最后的线性激活层网络只需要拟合一个相对平滑的分布收敛难度会低很多。这里有个容易混淆的点YOLOv1的x和y是相对网格的偏移不是相对整张图片的坐标。也就是说网络输出的是“目标中心离这个网格左上角多远”而不是“目标中心在整张图的绝对位置”。训练时我们要先把真实框的中心点换算到网格坐标系里再计算损失这和很多现在用anchor的YOLO版本不一样后面复现部分我专门讲这个坑。3. 网络结构、预训练与448×448的取舍3.1 24层卷积2层全连接的结构细节YOLOv1的主干网络受GoogLeNet启发但没有直接用Inception模块而是用1×1卷积做通道压缩、紧跟3×3卷积做特征提取这样能在控制计算量的同时加深网络。整体结构是24个卷积层后面接2个全连接层。具体展开的话前20个卷积层先用于ImageNet分类预训练这20层负责从原始像素中提取通用视觉特征在进入检测任务时后面接上4个卷积层和2个全连接层。最后的全连接层输出4096维再接一个全连接层把维度压缩到7×7×30最后reshape成空间上的7×7×30张量。激活函数上YOLOv1除最后一层使用线性激活外中间层统一使用Leaky ReLUslope参数alpha0.1。为什么不用普通ReLU因为Leaky ReLU在负半轴保留了一个很小的梯度能减少神经元“死亡”问题让训练更稳定这个小细节后来被无数检测模型继承。另外YOLOv1还推出了一个轻量版叫Fast YOLO卷积层从24层减到9层速度可以冲到150 FPS但精度降了不少mAP大约52.7。这说明当时作者已经意识到“速度-精度”是检测模型绕不开的权衡而这个权衡一直到今天都是YOLO系列的主线。3.2 为什么输入必须是448×448全连接层的代价YOLOv1的网络最后是两层全连接而全连接层的输入尺寸是固定的这就导致模型输入必须固定成448×448。你不能像后来的全卷积检测器那样任意尺寸输入、任意尺寸输出。448×448这个数字是从哪来的预训练时用的是ImageNet的标准224×224检测阶段为了提高定位精度把输入翻倍成448×448相当于在保留预训练权重的基础上把特征图分辨率提高了一倍。这样可以保留更多小目标的空间细节代价是计算量增加了约4倍但换来的是检测能力的显著提升。如果你现在自己动手复现YOLOv1建议老老实实把输入resize到448×448。因为一旦改动输入分辨率网络输出的7×7网格对应的原图感受野和空间粒度都会变训练超参数也需要从头调整并不划算。想要多尺度输入那是YOLOv2引入anchor和全卷积之后的事情了。3.3 预训练分两步ImageNet分类、检测微调YOLOv1的训练策略非常经典也是后来不少检测模型的标准做法。第一步用前20层卷积加一个平均池化层和一个全连接层在ImageNet 1000类分类任务上预训练。这个阶段大约跑了一周top-5准确率能到88%证明这20层卷积确实学到了通用的视觉特征。第二步把分类用的全连接层换成检测用的结构也就是补上剩下的4个卷积层和2个全连接层然后切换到448×448输入在VOC数据集上微调。这一步之所以有效是因为分类任务和检测任务在低中层级特征上是共享的边缘、纹理、形状这些基础特征不需要从头学微调只需要让高层的语义特征适应“边界框回归多类别分类”这个新目标。这种“分类预训练检测微调”的思路本质上是一种迁移学习。放到今天看似乎很常规但在2016年YOLOv1把这件事做成了一套明确可复制的流程对后来者影响很大。很多论文和开源库都沿用了类似思路先用大规模分类数据初始化再微调到检测任务上。4. 损失函数设计里的门道4.1 定位损失宽高开根号是为了小框YOLOv1的损失函数用的是一个统一形式的sum-squared error均方误差分为坐标损失、置信度损失和分类损失三块。最初的坐标损失长这样对于“负责”预测目标的网格i中的第j个框计算中心坐标和宽高的平方误差[ \lambda_{coord}\sum_{i0}^{S^2}\sum_{j0}^{B} 1_{ij}^{obj}\left[(x_i-\hat{x}i)^2(y_i-\hat{y}i)^2\right] ] [ \lambda{coord}\sum{i0}^{S^2}\sum_{j0}^{B} 1_{ij}^{obj}\left[(\sqrt{w_i}-\sqrt{\hat{w}_i})^2(\sqrt{h_i}-\sqrt{\hat{h}_i})^2\right] ]这里(1_{ij}^{obj})表示第i个网格的第j个预测框是否负责这个目标。注意第二行对宽高先开根号再算平方误差这是YOLOv1损失函数里最出名的一个细节。为什么要开根号因为大框和小框对同样大小的偏差容忍度是不同的。举个例子一个宽度为200像素的框偏差10像素相对误差只有5%但一个宽度为20像素的框偏差10像素相对误差就是50%。如果不处理大框的误差天然会主导梯度小框的定位精度就很难学上去。开根号之后大框的数值变化被压缩、小框的数值变化被放大相当于变相给小框损失加了更大的权重让网络更重视小目标的边界框精度。这是很朴素但很有效的小目标优化思想。4.2 置信度损失有无目标的权重差异置信度损失分两块有目标和无目标。[ \sum_{i0}^{S^2}\sum_{j0}^{B} 1_{ij}^{obj}(C_i-\hat{C}_i)^2 ] [\lambda_{noobj}\sum_{i0}^{S^2}\sum_{j0}^{B} 1_{ij}^{noobj}(C_i-\hat{C}_i)^2 ]有目标的部分只对那些真实分配给目标的预测框计算。无目标的部分是所有没有分配到目标的预测框也就是绝大多数网格里的框。这里必须设置一个(\lambda_{noobj})论文取0.5。为什么不是1因为一张7×7图像里真正包含目标的网格通常只有几个其余全是背景。如果背景框的置信度损失权重和前景一样大网络很容易走极端把所有confidence都预测成0因为“大部分框本来就没有目标”这样总和误差反而最小。0.5的权重是为了抑制这种背景主导但又不至于完全忽略背景让网络保留一定的判别能力。在训练之初你大概率会观察到置信度预测值整体偏低这是正常的。因为绝大多数网格没有目标网络在尽量压低这些负样本的confidence。关键是让有目标的网格里负责人的那个框能慢慢“顶出来”最终达到“有目标时confidence接近IoU无目标时confidence接近0”的状态。4.3 分类损失与每个格子只能给一个类别的限制分类损失本身很简单[ \sum_{i0}^{S^2} 1_i^{obj}\sum_{c\in classes}(p_i(c)-\hat{p}_i(c))^2 ]只对存在目标中心点的网格i计算(1_i^{obj})表示第i个网格是否有目标。注意YOLOv1没有用交叉熵而是继续用平方误差这是为了和整个sum-squared error的框架保持一致优化起来也简单。但这个简单设计背后有一个明显短板每个网格只预测一组类别概率所以不管B2个框是不是对应两个不同的目标最终类别预测只能是一个。如果两个目标中心点都落在同一个网格里哪怕一个在左上、一个在右下YOLOv1也只能输出其中一类。这个“一格一类”的限制直接导致YOLOv1在密集小目标场景下表现不佳。YOLOv2后来引入anchor并让每个anchor独立预测类别部分解决的正是这个问题。5. 训练、推理与评估从数据集到最终mAP5.1 准备VOC格式数据标注框怎么归一化想在YOLOv1上训练自己的数据集第一步是把标注转成YOLO风格。虽然YOLOv1有Darknet原生格式但你可以先用LabelImg或CVAT这类工具打标然后统一转成YOLO格式的txt文件。每张图片对应一个同名txt文件每一行代表一个目标格式为class_id x_center y_center width height这里x_center、y_center是相对图像宽高的中心点坐标width、height也是相对图像的宽高比例全部归一化到0到1。LabelImg保存VOC格式XML时你可以直接用官方脚本转成上述格式CVAT导出时也有现成的YOLO格式选项比较省事。数据做完之后只是完成了上游准备。真正进入YOLOv1训练前还需要把标注从“相对整张图的中心坐标”转换成“相对网格左上角的偏移”这一步我会在复现坑里详细讲这里先提醒一句很多刚接触的人会在这里算错最后发现loss能降但框的位置完全不对。5.2 训练超参数与学习率调法论文里YOLOv1在PASCAL VOC 2007和2012的训练验证集上训练大约135个epoch。batch size为64momentum为0.9weight decay为0.0005初始学习率调度比较特殊第一个epoch学习率从0.001缓慢升到0.01也就是现在常说的warmup。接下来75个epoch学习率固定在0.01。再之后30个epoch学习率降到0.001。最后30个epoch学习率降到0.0001。为什么要warmup因为训练刚开始时网络权重来自预训练突然给一个很大的学习率可能破坏已经学到的特征。用一个epoch慢慢把学习率推上去能让模型平稳过渡到检测任务。这个细节我第一次训练时没在意结果前几个batch loss直接飞到数百换成warmup之后才稳定下来。数据增强方面YOLOv1用了随机缩放和平移最大偏移量约为原图尺寸的20%同时会在HSV色彩空间随机调整曝光和饱和度上限大约是1.5倍。另外在全连接层后加了dropout概率0.5用于减轻过拟合。这些手段放到今天看不算多但在当时已经能明显提升泛化能力。5.3 推理时阈值过滤与NMSYOLOv1推理非常直观。网络输出7×7×30后先把每个框的confidence和该网格的类别概率相乘得到每一个框针对每个类别的最终得分[ \text{score} P(\text{Class}_i|\text{Object}) \times \text{confidence} ]这个score表示“这个框里是某类目标、且框得准不准”的综合置信度。得到score后先按阈值过滤掉低分框论文在VOC上常用0.2左右剩下的框再按类别分别做NMS非极大值抑制。NMS的逻辑是同一类别的多个预测框如果重叠度很高就只保留得分最高的那个。具体做法是先按score排序取最高分的框删除所有与它IoU超过某个阈值比如0.5的其他框然后重复这个过程。这个流程看似简单但NMS的阈值很影响结果。阈值设得太高会残留大量重复框设得太低又可能把挨得近的同类目标误删。YOLOv1因为每个网格只预测一个类别同一个目标通常只有一两个框竞争NMS压力不大所以阈值可以稍微宽松一些。5.4 用mAP给YOLOv1打分45 FPS和63.4 mAP在PASCAL VOC 2007测试集上YOLOv1取得了63.4的mAP在Titan X GPU上达到45 FPS。Fast YOLO则是52.7 mAP速度150 FPS。作为对比当时的R-CNN达到66.0 mAP但速度只有0.5 FPSFast R-CNN的mAP是70.0速度依然远低于实时。从数字能看出YOLOv1在当时并不是精度最高的它的卖点是“把速度拉到了可实用的级别”。63.4的mAP虽然在数字上落后Fast R-CNN约7个点但45 FPS意味着它可以实时处理视频流这是质变。很多应用场景比如监控、机器人、自动驾驶的初步感知之前根本不敢用CNN检测器YOLOv1让这些方向第一次有了尝试的可能。mAP计算本身用的是VOC的AP平均方式对每个类别分别算Precision-Recall曲线下的面积再取平均。这个指标对训练和部署都很重要建议在复现时自己实现一遍能帮你深刻理解置信度阈值怎么影响precision和recall的取舍。6. 自己动手复现YOLOv1原理之外的坑6.1 坐标系编码x,y的偏移量到底怎么算我复现YOLOv1时踩的第一个大坑就是坐标编码。网络预测的x和y不是“相对整张图的中心点”而是“相对当前网格左上角的偏移”。但训练标签里目标中心点坐标通常是相对整张图归一化的。所以你得先算清楚假设目标中心点归一化坐标是(tx, ty)当前所在网格的列号是col、行号是row从0开始范围0到6网络预测的x、y目标值应该是[ x tx \times 7 - col ] [ y ty \times 7 - row ]也就是说如果目标中心落在第3行第5列网格的正中间那么(ty, tx)大约对应(3.5/7, 5.5/7)经过转换后y0.5, x0.5。网络预测的x、y就应该逼近0.5。推理时做反向操作把预测的x加上col再除以7就能还原成相对整张图的中心点坐标。很多初学代码的人直接拿归一化的中心点去算损失训练出来框全部偏移到网格左上角这就是坐标编码没搞对。即使公式看似简单一旦网格索引搞错整个系统都会不可用。6.2 空白网格是置信度训练的主角另一个坑是置信度的初始化和正负样本不均衡。由于7×7的网格里大部分区域是背景训练初期模型会疯狂把所有confidence往0推。这时候损失函数里(\lambda_{noobj}0.5)就成了唯一屏障。实际操作中我建议训练刚开始时观察无目标网格的confidence均值如果下降太快且太快趋于0要考虑是否(\lambda_{noobj})设太大。有目标网格中“负责框”的confidence训练早期可以允许它先升高到0.5左右再慢慢逼近IoU。如果你看到它长期在0.2以下大概率是学习率太低或者正样本分配逻辑写错了。准备训练数据时尽量保证每个batch里包含足够多的正样本网格否则网络会被背景淹没。YOLOv1没有focal loss处理正负样本不均衡全靠权重和采样。你可以在实现时自己控制数据顺序让每批图片里目标不要过于稀疏这个技巧能明显加速收敛。6.3 小目标检测差为什么是结构性缺陷YOLOv1对密集小目标的检测效果差这不是训练不充分而是结构层面的限制。主要原因有三个第一输入448×448经过多层卷积和下采样后最后输出的7×7特征图上每个网格感受野对应原图64×64像素的区域。如果目标比这个区域小很多能用于判别它的特征就很有限网络很难精确定位。第二每个网格最多只能预测B2个框且只能有一组类别概率。目标稍微密一点两个目标中心落在同一个网格系统就会直接超载。第三YOLOv1没有anchor也没有多尺度特征融合它对不同宽高比和尺度的目标泛化能力弱全靠卷积层自动适应。这和后来的YOLOv2用anchor、YOLOv3用FPN相比差距非常明显。所以如果你要在无人机视角、小目标密集场景里做检测YOLOv1基本不太能用。它的历史价值更多在于验证了“单阶段实时检测可行”这个方向真正的工程实用性要到YOLOv2和YOLOv3才逐渐成熟。6.4 从YOLOv1到YOLOv2YOLOv1的边界在哪复现过YOLOv1之后你会更明白YOLOv2的改进点为什么那么精准。YOLOv2做了几件关键的事移除了全连接层改成全卷积结构输入尺寸不再固定。引入anchor框机制每个网格预测更多候选框类别预测从“网格共享”变成“anchor独立”解决了“一格一类”的限制。加入BatchNorm训练更稳定收敛更快。多尺度训练让网络适应不同输入分辨率。把输入分辨率提升到416×416并去掉了最后一个池化层让特征图分辨率从7×7变成13×13小目标检测能力明显增强。这些改进每一项都精准打在YOLOv1的痛点上。所以学习YOLOv1时不要只背结论最好把“YOLOv1为什么这样设计”“它哪里不行”一起搞清楚这样再看YOLOv2、YOLOv3的一系列演进会顺理成章得多。7. YOLOv1真正留给后来者的遗产7.1 不是精度最高但开启实时检测YOLOv1今天看有很多不足mAP不如同期的两阶段方法小目标检测弱对密集场景也力不从心但它定义了“实时目标检测”这条技术路线。Fast R-CNN再准跑不到实时许多实际应用就落不了地YOLOv1用45 FPS第一次让实时检测成为可能让行业开始认真关注“如何在速度与精度之间做设计”而不仅仅是追求更高的mAP。这种“先定义问题再设计最简方案”的思路对整个检测领域的影响比那63.4 mAP大得多。直到现在YOLO系列的每个版本都还在沿用YOLOv1确立的单阶段、网格化预测、统一损失函数的基本框架。无论你后续用哪个版本的YOLO回到YOLOv1打地基都会让你对它们的设计选择有更深的体会。7.2 从YOLOv1入手理解YOLO系列的学习路径如果你刚入门目标检测我会很推荐用YOLOv1做第一个实践项目原因很简单它结构直接没有太多花哨模块几十行核心代码就能跑通。你可以在它上面亲手验证损失函数、坐标编码、NMS这些基础概念然后再逐步向YOLOv5、YOLOv8这些工程化更强的模型迁移。建议的学习顺序是先把YOLOv1的论文原文读一遍尤其聚焦网格划分和损失函数部分然后照着开源PyTorch实现把数据流和loss函数一行行跑通最后换到自己的小数据集上训练对比检测效果再去找YOLOv2和YOLOv3的改进论文逐项对比“哪里改了、为什么改”。一圈走下来你对目标检测的理解会比直接上手YOLOv8深得多。我自己当年也是从YOLOv1开始入门的印象最深的是第一次用自己训练出来的模型检测一张街景图看到那些框在视频流里稳稳跟住行人心里那种满足感至今还记得。那之后我再换到YOLOv3、YOLOv5感觉都不是从零学起而是在一个已有的框架里升级认知成本低了很多。如果你也想把目标检测的地基打扎实YOLOv1是一个绕不开、也值得绕回去好好看一眼的起点。
返回列表