ARTICLE DETAIL

资讯详情

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

YOLOv1深入解析:从原理到复现的目标检测入门指南

YOLOv1深入解析:从原理到复现的目标检测入门指南 在做目标检测相关的项目时YOLO几乎是绕不开的名字。从2016年YOLOv1发布到现在这个系列已经迭代了多个版本但很多初学者直接上手YOLOv5、YOLOv8时往往只学会了调用接口对检测原理反而是一团浆糊。我个人的建议是如果要真正理解YOLO家族最好还是从YOLOv1开始啃因为它的思想最纯粹——把目标检测当成一个回归问题一次前向推理直接把边界框和类别全预测出来。这篇文章就围绕YOLOv1展开从设计思路、网络结构、损失函数到训练推理的细节逐个环节拆开讲清楚顺带把我实际复现和调试时踩过的坑也一并写出来给正准备入门目标检测的同学做参考。1. 目标检测到底在解决什么问题要搞懂YOLOv1得先退一步看看目标检测这个任务本身。检测任务和分类任务最大的区别在于分类只需要回答“图里有什么”而检测还要回答“这些东西在哪个位置”。说得更具体一点一张图喂给模型之后我们希望输出的是一组结构化的结果每个目标都有一个类别标签还有一个尽量紧贴目标的矩形框通常用四个坐标值来表示比如中心点坐标加宽高或者左上角加右下角坐标。在YOLOv1出现之前主流方案是两阶段检测代表作是R-CNN系列。这类方法的思路是先在前端生成一堆候选区域再用分类网络逐个判断这些区域里有没有目标、是什么目标。思路很直观但问题也明显候选区域可能有上千个每个都要过一遍网络速度自然快不起来。我记得最早用R-CNN跑一张图CPU上要几十秒GPU上也要好几秒这在很多真实场景里根本没法用。后来虽然有Fast R-CNN、Faster R-CNN在速度上做了不少优化但“先提候选框、再分类”这个总体框架没变推理耗时依然偏高。YOLOv1把这个问题换了个角度来思考能不能不要“先找候选框”这一步直接把图像像素映射到检测结果作者Redmon等人的答案是用一个单次前向传播的卷积网络把整张图作为输入输出张量里直接编码所有目标的边界框坐标、置信度和类别概率。这个过程整个就是一次回归所以YOLO的原始论文标题叫You Only Look Once意思是你看一眼就够了不需要来回扫两遍甚至更多遍。这种设计思路换来的是极其夸张的速度提升。当年的GPU上YOLOv1的实时版本能做到每秒45帧快速版本甚至能到每秒155帧左右而当时的Faster R-CNN大概只有每秒5到7帧。代价也很明显YOLOv1在精度上比Faster R-CNN差了一截尤其是在小目标和重叠目标的场景下经常翻车。不过它确实开创了一条新路线后续YOLO系列所有版本本质上都是在V1这个“单阶段、一次前向”的框架上不断打补丁、做优化。2. YOLOv1的核心设计思路2.1 把整张图切成网格每个格子自己管自己YOLOv1最关键的设计决策是把输入图像划分成S×S的网格。论文里取的是S7也就是输入图像被切成了49个格子。这里的“格子”不是像素级别的滑动窗口它更像是一种责任划分每个网格单元负责预测那些“中心点落在这个格子内部”的目标。我给初学者解释这个机制的时候经常用小区物业来类比。一栋楼有49个单元每个单元自己负责管理自己门前的那片区域。如果有人的中心点正好站在你的单元门口那这个目标的责任就归你了。这种按中心点归属的规则很重要后面损失函数计算、正样本分配都要用到它。但这里必须注意一个网格并不是只输出一个物体的信息。YOLOv1设计成每个网格能预测B个边界框论文里取B2同时还要预测这个网格属于各个类别的概率VOC数据集上类别数C20。所以最终的输出张量尺寸就是S×S×(B×5C)代入S7、B2、C20结果就是7×7×30。这个30维的向量里前10维是2个预测框各自的信息每个框对应5个数值分别是中心坐标x和y、宽度w、高度h、置信度confidence后面20维是这个网格对20个类别的条件类别概率。2.2 边界框的表示与编码方式YOLOv1对边界框的编码方式很有讲究也是初学者最容易理解错的地方。它没有直接预测框的绝对像素坐标而是做了归一化和相对化处理。中心坐标x和y是相对于所在网格左上角的偏移量并且除以了网格本身的尺寸所以取值范围在0到1之间。宽度w和高度h则是相对于整张输入图像尺寸的比例同样归一化到0到1之间。这样做的好处很明显数值范围被限制在0到1之间后模型输出的预测值天然就是有界的配合sigmoid之类的激活函数训练起来更稳定。如果你直接让模型去预测绝对的像素值比如中心点可能在几百到几千像素之间浮动那损失函数的数值尺度会非常大而且不同尺寸的输入图片预测值的分布差异也很大网络很难收敛。这里有个细节我觉得值得拿出来说x和y在V1版本里其实没有用sigmoid做额外约束虽然理论上它应该在0到1之间但YOLOv1的网络在全连接输出层之后直接就是回归值没有做数值截断。实际运行中模型偶尔会预测出略小于0或者略大于1的坐标值这个在工程上问题不大后处理时clip一下就行。真正把这个bug修掉是YOLOv2的事情V3之后对中心坐标做了logistic激活约束这也是V1设计上比较粗糙的一个点。2.3 置信度的含义不是单纯的“有没有目标”YOLOv1里每个边界框都带一个置信度confidence这个值的定义是Pr(Object)×IoU(pred, truth)可以理解为“这个框内部有目标吗”乘以“预测框贴得准不准”。如果网格里没有目标置信度的理想值就是0如果有目标置信度应该等于预测框和真实框的交并比IoU。我在和不少初学者交流的时候发现有人会把这个置信度和类别概率混淆。实际上YOLOv1里两者是分开的置信度负责量化“框的质量”类别概率负责回答“框里到底是什么”。推理的时候最终每个框的得分是类别概率乘以置信度得到的是一个综合了类别信心和定位精度的分数这个分数用于后续的阈值筛选和非极大值抑制。2.4 类别概率是网格级别的不是框级别的YOLOv1里还有一处容易忽略但很重要的设计类别预测实际上是按网格来做的不是按边界框来做的。每个网格只输出一个20维的类别概率向量而不是每个框都带一个20维的类别向量。这也就是说在模型看来同一个网格内的两个预测框共享同一份类别信息。这种设计在比较空旷的目标分布下问题不大但如果两个不同类别的目标中心同时落在一个网格内模型就处理不了了因为类别只有一份怎么分都不对。所以YOLOv1的容量上限其实很受限制最多只能预测49×298个框而且每个网格最多只能预测出一个有意义的类别。这也注定了它在密集场景和多尺度目标场景下表现不好。但它让模型结构变得非常简单输出张量一次性全部给出速度和简洁性拉满。3. 网络结构与前向推理细节3.1 主体结构从GoogLeNet起步YOLOv1的骨干网络受到了GoogLeNet的启发整体是24层卷积加2层全连接。这里有个小细节作者没有直接照搬GoogLeNet的Inception模块而是用1×1卷积加3×3卷积的组合来模拟类似Inception的感受野增强效果。1×1卷积主要用来做跨通道的信息融合和降维能显著减少参数量和计算量。网络前20层是在ImageNet分类任务上预训练过的输入分辨率是224×224。但检测任务需要更细粒度的空间信息所以作者在预训练完成之后把输入分辨率提高到了448×448并且在后面又加了4层卷积和2层全连接。这种先在分类上预训练、再在检测上微调的策略在当时几乎是标配能明显加快收敛速度同时对最终精度也有正向帮助。从参数量的角度看YOLOv1其实并不算“轻量”模型。它在448×448输入下FP32模型大概有250MB左右的大小当时的GPU跑起来问题不大但放到嵌入式设备上就有点吃力了。这和现在动辄只有几兆的轻量化检测模型相比体重确实超标但在2016年这个参数规模已属于合理水平。3.2 输出张量是怎么组织起来的最后一层全连接输出的是4096维的特征向量然后被reshape成7×7×30的张量。这个张量可以理解为49个网格每个网格对应一个30维的向量。30维里的前10维属于两个预测框每个框5个值x、y、w、h、confidence后20维是VOC的类别概率。我在实际阅读源码的时候特别喜欢把输出张量打印出来看。比如输入一张猫的图片某个网格的30维向量可能是0.52、0.31、0.78、0.66、0.92、0.48、0.29、0.81、0.63、0.11、一堆0.001之类的概率值。其中第一个框的confidence明显高第二个框的confidence很低这表示模型认为这个网格里有一个目标而且第一个框预测得比较准第二个框基本可以不看了。实践中大部分预测框的confidence都会非常低绝大多数都是负样本最后有效框就是少数那些分数比较高的。3.3 两次全连接对空间信息的破坏从今天的眼光来看YOLOv1网络结构有个比较大的设计缺陷它用了两层全连接层来输出最终结果。全连接层会把前面的二维空间特征全部拉平破坏空间位置关系导致模型对目标的定位能力受限。这也是YOLOv1在小目标和密集场景下表现差的结构性原因之一。后来YOLOv2专门针对这个问题做了改造把全连接层全部去掉改成了全卷积结构再引入anchor机制让空间信息得以保留。所以你在看YOLOv1源码时留意到的那个“4096维全连接”的设计在后来的版本里被彻底抛弃了。我们学习V1时要知道这个点的缺陷但也不必过于批判因为这是当年计算资源和设计思路下的合理选择。3.4 激活函数和归一化YOLOv1在卷积层之后使用了Leaky ReLU作为激活函数斜率为0.1。为什么不用普通的ReLU因为ReLU在负数区域的梯度是0一旦神经元输出为负它将永远无法更新这就是所谓的“神经元死亡”。Leaky ReLU在负数区域保留了一个很小的梯度能让信息流动更顺畅。## 4. 损失函数YOLOv1的灵魂4.1 简单粗暴的求和平方误差YOLOv1的损失函数是整个算法的灵魂理解了这个部分你基本就理解了大半的YOLO。原文用的是sum-squared error也就是把所有预测误差的平方加起来。这种选择的好处是简单反向传播时梯度计算容易坏处是它把所有误差项一视同仁不同尺度的误差之间没有天然区分。为此作者在损失函数里手动加权重、加条件按不同部分分别计算。完整来说损失函数由三大部分组成坐标预测损失、置信度预测损失和类别预测损失。这三部分的权重并不相同作者人为设定了λcoord5来加大坐标误差的惩罚同时用λnoobj0.5来降低没有目标的网格的置信度惩罚。4.2 坐标损失宽高为什么要开根号坐标损失只针对“负责检测目标”的边界框来算。所谓“负责”指的是在某个网格的B个预测框中和真实框的IoU最大的那个框。这个负责框要预测坐标x、y、w、h其损失计算方式如下对于中心坐标误差直接算平方差对于宽高误差先对预测值和真实值分别开平方根再算平方差。为什么要对宽高开根号这是一个非常巧妙的细节。如果不做开根号同样大小的绝对误差放在大目标和小目标上对最终IoU的影响是完全不同的。举个例子一个宽度为100像素的大目标预测成105像素IoU掉不了多少但一个宽度为10像素的小目标预测成15像素IoU可能就直接崩了。如果直接用宽度差来算损失模型会觉得“反正误差都是5像素差不多”于是不重视小目标的宽高预测。而开根号之后小目标宽度从10变成15根号差是3.87−3.160.71大目标宽度从100变成105根号差是10.25−100.25小目标的误差被放大了模型就会更努力地去学小目标的位置。这是个非常经典的处理手段至今在不少检测框架里还能看到类似的思路。4.3 置信度损失有目标和无目标分开处理置信度损失分成两部分。第一部分是“有目标”的框即负责检测的框它的置信度目标值是真实框的IoU值所以损失是预测置信度与该IoU的平方差。第二部分是“无目标”的框包括非负责的框以及整个网格里都没有目标时的那两个框它们的置信度目标值是0所以损失平方差直接用预测置信度来计算。这里有个比较关键且容易踩坑的点无目标框的数量远多于有目标框的数量。一张图里49个网格98个框真正有目标的可能就两三个剩下的全是背景。如果对负样本的惩罚和对正样本一样重模型会倾向于把所有框的置信度都预测成0反正惩罚也不大。作者用λnoobj0.5压低负样本的权重但说实话这个压低幅度在极端不平衡的条件下仍然不够。我在训练YOLOv1的时候观察到的置信度输出经常偏大背景框也给出0.3、0.4的置信度这导致后处理时阈值要调得比较高才能滤干净。4.4 类别损失与整体函数形态类别损失只跟网格是否包含目标有关也就是只有那些“包含目标中心点”的网格才计算类别概率误差。注意整份损失函数中类别损失是没有加权系数的权重是1。在实际运行中坐标损失因为被λcoord5放大往往占据主导地位这本身也是作者有意为之——先把框定位学准再去学分类。整体损失函数看起来像一个巨大的分段函数不同情况下取不同的计算分支代码里会写很多条件判断。从工程实现上讲YOLOv1的损失函数计算并不复杂但绝对不是一个简单的MSE一算就完事。你需要先计算每个真实框对应哪个网格、哪个负责框然后针对不同情况构建一个mask矩阵把需要计算损失的项挑出来。如果mask构建错了损失值会乱跳模型也不会收敛。我见过不少自己复现YOLOv1的同学报出来的loss要么不降要么直接NaN多半问题就出在mask没构对。5. 训练细节与推理流程5.1 数据标注格式与预处理训练YOLOv1时数据集标注需要转换成规定的格式。以VOC数据集为例每张图对应一个XML文件里面记录了每个目标的类别和边界框坐标左上角xmin、ymin右下角xmax、ymax。在送入模型之前需要做几件事把坐标转换成中心点加宽高的形式除以图像宽高完成归一化再根据中心点算出它落在哪个网格。对输入图像YOLOv1会统一resize到448×448。这里要注意如果你的原图不是正方形直接拉伸会改变目标的宽高比例影响检测精度。我当时实验的时候对比过直接拉伸和先letterbox再填充的效果发现letterbox方式对宽高比极端的图片更友好。但YOLOv1论文原文其实没有明确用letterbox很多复现代码直接resize效果也能接受只是你需要知道这里存在一个质量取舍。打标环节如果你用的是LabelImg导出时可以设置成YOLO格式这样拿到的就是归一化后的类别id、中心点x、中心点y、宽度w、高度h跟YOLOv1训练所需的格式基本一致。需要注意LabelImg导出YOLO格式时坐标是按图片原始尺寸归一化的你仍然要在训练时把它转换到7×7的网格体系下也就是x乘以7取整得到网格索引偏移量x×7−floor(x×7)才是预测目标值。5.2 从零训练还是预训练微调前面提到YOLOv1的前20层先在ImageNet上预训练。这在当年的算力条件下非常重要因为检测数据集规模远小于分类数据集如果从零开始训练模型很容易过拟合而且收敛很慢。即使到今天你如果想在自定义数据集上重新训练一个YOLOv1模型我仍然建议沿用这个策略先找一个大一点的分类数据集做预训练或者直接加载作者发布的权重再在你的检测数据上微调。我的经验是在数据量只有几千张的情况下从零训练YOLOv1的效果会惨不忍睹mAP可能只有从预训练权重微调的一半不到。训练时的超参数设置论文里给出的参考是epoch数约135batch size为64初始学习率0.001后续在75轮和105轮时降低学习率动量0.9权重衰减0.0005。数据集增强方面用了随机缩放、平移、调整曝光度和饱和度等手段。这些参数在GTX 1080级别的显卡上大概要训练几天放到现在用更好的GPU会快很多。5.3 推理阶段的后处理推理阶段模型前向传播得到7×7×30的输出张量后还不能直接画框。原始输出的98个框里真正有效的非常少而且存在大量互相重叠的重复检测必须做后处理才能得到最终结果。具体流程如下。先将每个边界框的置信度与对应网格的类别概率相乘得到每个框在各类别上的最终得分。公式上是score_{ij} class_prob_i × confidence_j其中i是类别索引j是边界框索引。得到98×20的得分矩阵后对每个类别独立处理先筛掉低于设定阈值的检测框再在这个类别内部做非极大值抑制NMS。NMS的操作逻辑是把所有得分从高到低排序选最高分的框作为保留框然后删除所有与它IoU大于指定阈值通常设0.5的其他框重复直到没有候选框为止。这一步的作用是消除同一目标上的多个重复框。如果不做NMS最终画出来的检测效果会是一坨框叠在一起完全没法看。NMS的IoU阈值怎么定很有讲究。设得太低比如0.3容易把离得近的不同目标误删掉一个设得太高比如0.7同一目标上可能存在多个重叠框无法消除。我在项目中一般从0.45到0.5这个区间开始调。还有一个容易被忽略的坑对不同类别分别做NMS还是跨类别一起做YOLOv1常用的做法是按类别分别做因为理论上同一个目标不会同时属于两个类别但实际中如果两个不同类别的目标高度重叠跨类别NMS会把其中一个误删。所以更稳妥的做法是分类别做NMS。5.4 判断检测好坏的指标mAP和IoU训练和评估检测模型离不开IoU、Precision、Recall、mAP这几个指标。IoU就是预测框和真实框的交集面积除以并集面积用来衡量两个框的贴合程度。一般约定IoU大于0.5算检测成功但这个阈值是可以调的比如COCO数据集上会统计多个IoU阈值下的平均精度。mAP的计算大致是按类别分别算出不同置信度阈值下的Precision和Recall曲线再求曲线下面积得到AP然后对所有类别的AP取平均。写YOLOv1的论文时作者用的就是VOC 2007的mAP指标。在VOC 2007测试集上YOLOv1的mAP约63.4%Faster R-CNN大约是73.2%同期另一个单阶段方法SSD300约72.1%。可以看到YOLOv1在精度上确实落后不少但速度优势非常明显。6. 复现YOLOv1时踩过的坑6.1 正样本分配时的IoU计算陷阱YOLOv1的正样本选择机制是对每个真实框找到它中心点落入的网格在这个网格的两个预测框中挑一个与真实框IoU最大的框作为负责框。问题在于训练初期网络权重是随机或预训练的预测框位置乱七八糟两个框跟真实框的IoU可能都很低甚至都是0。如果IoU为0计算置信度目标值时就是0负责框的置信度目标为0模型就学不到“这里有目标”的信号。解决办法是在训练一段时间后动态计算IoU并更新负责框不能一开始就固定。有个我常用的技巧是在构建训练批次时将“最佳IoU匹配”的计算放在batch内部实时进行而不是预处理时一次性算好。因为预处理时网络还没训练预测框完全无意义。这个点看似小但直接决定了模型能不能正常收敛。6.2 Loss不下降或者NaN的排查思路如果训练了几个epochloss完全没有下降趋势先检查三样东西输入图像的归一化是否正确、标注坐标是否越界、正样本mask是否全为0。我遇到最多的就是标注框的坐标没有归一化好导致计算损失时目标值动辄几百上千梯度爆炸loss直接NaN。再有一个常见问题就是学习率设置不合理。YOLOv1用的优化器是带动量的SGD学习率0.001起步如果你换成了Adam而且学习率不调整loss很可能会上下剧烈波动。此外batch size太小时置信度分支的梯度估计方差会比较大也容易训练不稳。我当时的经验是batch size至少32起步最好64或更高。6.3 小目标和密集目标的灾难现场如果你拿YOLOv1去检测密集的行人或者远处的车辆效果堪称灾难。原因是7×7的网格太粗糙一张448×448的图里每个网格对应的原图区域是64×64像素如果一个目标占的像素区域远小于这个尺寸它的特征很可能在卷积过程中被稀释或混合到背景里。而且同一网格只能预测一个类别、最多两个框这从根本上限制了它对密集场景的处理能力。这里有一个实际工程里的应对办法在训练时将目标裁剪放大或者对图像做多尺度训练让目标在图上占据更大的网格区域。另一个办法是干脆换用更高分辨率的输入比如训练时用608甚至更高但YOLOv1全连接层的存在使得输入尺寸必须固定所以改输入尺寸就得重新训练整个网络。这也是后来全卷积多尺度版本能通吃不同分辨率的原因。6.4 全连接层带来的固定尺寸限制YOLOv1因为用了全连接层输入尺寸被死死钉在448×448。这一点在真实部署中很麻烦因为摄像头来的流分辨率未必是正方形或者你希望用小分辨率加快速度、用大分辨率提升小目标召回但V1做不到弹性调整。你做推理前必须把任意尺寸的输入resize成448×448这会丢失很多原始信息。如果只是学习原理这个问题可以忽略。如果是为了实际项目我的建议是用YOLOv3或之后的版本作为生产模型YOLOv1更适合作为理解单阶段检测器工作流程的教学模型。不过理解V1的缺陷恰恰能帮你理解后续版本为什么要引入anchor、为什么要做多尺度预测、为什么要用特征金字塔——所有优化都像是对症下药。7. YOLOv1对后续版本的启示7.1 从全局回归到先验锚框YOLOv1那98个框是完全自由预测的分布不可控模型要同时学“哪里有目标”“目标在哪”“目标多大”三个问题压力很大。于是YOLOv2引入了anchor先验让模型在预设好的先验框基础上做微调训练难度大幅下降。这个思路如今已经成了检测模型的标准配置即使是当时并行的SSD也同样使用了anchor机制。7.2 多尺度特征的引入YOLOv1只在7×7的单一尺度特征图上做预测分辨率太低小目标很难有出头之日。到了YOLOv3作者引入了类似FPN的特征金字塔结构在三个不同尺度的特征图上分别做预测分别对应大目标、中目标和小目标。这是YOLO系列在小目标检测能力上的一次大飞跃。如果你以后跑YOLOv8做项目感受最大的提升之一也来自多尺度预测。7.3 损失函数的精细化YOLOv1的求和平方误差在今天看来相当粗糙后来YOLOv2使用了带obj/nonobj加权的交叉熵损失YOLOv3用了BCE损失加置信度与类别解耦再后来YOLOv4开始引入CIoU损失等更先进的框回归损失函数每一步都是在弥补V1损失函数设计上的不足。回归损失从简单的L2进化到IoU Loss、GIoU、DIoU、CIoU甚至EIoU核心诉求都是“让框回归得更精准”。可以说YOLOv1是一颗种子后面所有YOLO版本都在它的框架下不断精修。学透V1再去看V2到V8的变化基本就是看作者如何一步步修补V1的缺陷整个系列的演进逻辑会非常清晰。8. 从YOLOv1出发的项目实践建议如果你现在准备做一个目标检测相关的毕业设计或者小项目我建议不要直接生啃YOLOv8的训练脚本。先花两三周时间手工复现一个简化版的YOLOv1跑通训练和推理你会发现后面用任何检测框架都事半功倍。我自己当年就是这么干的复现YOLOv1之后对anchor、NMS、mAP这些概念的理解明显比直接调库的同学扎实很多。具体复现时我建议从以下几个方面入手首先用PyTorch搭建网络结构前20层可以先不加载ImageNet预训练权重用随机初始化在少量数据上跑通前向和反向然后构造一批简单的合成数据比如纯色背景上放几个不同颜色和形状的矩形验证损失函数能否正常下降接着用VOC中的两三个类别做一个小规模训练看看mAP能否有基本保障最后再尝试加载官方权重进行完整训练。这样分步走排查问题的范围会小很多。关于环境配置YOLOv1本身非常简单依赖就是PyTorch或TensorFlow加OpenCV、NumPy不需要什么特殊的库。如果你用的是AMD显卡也可以直接用CPU跑小数据集做验证YOLOv1的前向计算量不算大CPU上跑几轮训练还是可以接受的。真正想训练到收敛级别的模型再考虑合适的GPU环境。数据集准备方面如果你需要标注自己的数据LabelImg和CVAT都支持导出YOLO格式。标注时请一定记得检测框尽量贴合目标边缘框和真实目标之间不要留太多背景否则模型会学歪。还有一个容易被忽略的点标注框的最小尺寸不要太极端如果一堆标注框只有几个像素模型很难学这些样本最好直接删掉不要硬留着拖后腿。最后再说一个小技巧训练时定期保存checkpoint并且每几个epoch就用验证集跑一次推理可视化看看模型到底学出了什么。我见过不少同学在那里干等loss降到多少结果loss降了但框乱飞这种时候可视化比看数字有用得多。YOLOv1虽然老了但用来练手和建立目标检测的直觉依然是个非常靠谱的选择。
返回列表