2016年,Redmon等人把论文标题起得极其直白:You Only Look Once。这句话实际上是在宣告目标检测的一次范式级转向——把当时主流的两阶段检测框架(先提取候选区域、再逐区域分类)压缩成一次统一的单阶段整图回归:整张图喂进网络,坐标、大小、类别、置信度一次性全部输出,没有候选区域生成,没有二次分类。做检测的人看到这个方案的第一反应往往是“这也太狂了”,但回头看,正是这种“只看一次”的激进设计,精准踩中了两阶段检测的速度瓶颈,也奠定了之后所有YOLO版本乃至SSD、RetinaNet的基石。
这篇文章我会把YOLOv1从原理到实操完整拆一遍:先讲清楚两阶段到底慢在哪、YOLO为什么敢把检测变成一次回归,再深入网络结构、损失函数里那些容易被忽略的细节,最后给出一套能在PyCharm里跑通的环境搭建与推理代码,以及我实际复现时踩过的坑。适合刚入门目标检测、或者已经在用YOLOv5/YOLOv8但没弄懂v1根基的读者。
1. YOLOv1出现之前:两阶段检测的速度瓶颈在哪
1.1 从R-CNN到Faster R-CNN,精度一直在涨,速度一直没跟上
两阶段检测的套路,用一句话概括就是“先框出来,再认一认”。最早R-CNN的做法很暴力:先用Selective Search在图上生成大约2000个候选区域,然后把这2000个区域分别缩放、分别送进CNN提特征,每个区域还要过一遍SVM分类器和一个边界框回归器。这个流程精度不错,但块头太大——每张图相当于跑2000次前向,在当年的GPU上处理一张图要几十秒。
后来Fast R-CNN做了一件事:把整张图只过一遍卷积骨干网络,得到共享的特征图,再用RoI Pooling从特征图上抠出每个候选区域对应的特征块。这样卷积计算就共享了,但候选区域仍然依赖Selective Search,而且这个算法是在CPU上跑的,速度瓶颈从“卷积计算”转移到了“区域调度”。Faster R-CNN更进一步,用Region Proposal Network(RPN)把候选区域生成也塞进神经网络里学习,端到端训练成为可能,精度到了当时的最好水平。但代价是训练流程很重:RPN和检测头要交替训练,超参数多,推理时也是两段式串联。
生活化类比一下:两阶段检测像一个特别谨慎的顾客,进商场先把所有衣服都拿进试衣间,一件件试完才决定哪件值得买;而YOLO的思路是站在店门口扫一眼,凭整体印象直接说出“那排货架上一共九件商品,第三件是红色T恤”。
1.2 速度瓶颈的本质:计算冗余和两段式调度开销
两阶段为什么慢?很多人以为是卷积计算量太大,其实不完全是。Faster R-CNN在VOC2007上的mAP高达73.2,但Titan X上只有7 FPS;而同一时期YOLOv1的mAP是63.4,FPS却有45,两者相差六倍多。深层原因有两个:
第一,候选区域是“先猜一大批、再淘汰”,这个策略天然自带计算冗余。RPN每张图大概生成几百到几千个框,这些框里绝大多数是背景,可检测头还是得挨个处理;而YOLO把整张图划分成固定的7×7网格,每个格子最多输出两个框,不论图像内容复杂还是简单,计算量都是恒定的。
注意:恒定计算量既是YOLO速度稳定的原因,也是它面对密集目标时召回率上不去的伏笔。后面讲局限性时还会回到这一点。
第二,两段式流程意味着信息在两个模块之间“倒手”。Faster R-CNN先由RPN生成框,再把这些框对应的特征做RoI Pooling,最后才分类回归;YOLO则是把“定位”和“分类”统一在一个张量里同时回归。这里的关键不是“有没有共享特征图”,而是“要不要为每个框单独做一次后处理”。YOLO把后处理简化成了一次阈值筛选和一次NMS,整张图只过一遍网络,这就是单阶段整图回归最本质的速度优势。
用一张表格可以直观对比几个经典模型的精度和速度:
| 模型 | 框架 | 骨干网络 | mAP (VOC2007) | FPS (Titan X) |
|---|---|---|---|---|
| R-CNN | 两阶段 | AlexNet | 约48 | 约0.05 |
| Fast R-CNN | 两阶段 | VGG-16 | 70.0 | 约0.5 |
| Faster R-CNN | 两阶段 | VGG-16 | 73.2 | 约7 |
| YOLOv1 | 单阶段 | 自定义24层 | 63.4 | 45 |
| Fast YOLO | 单阶段 | 自定义9层 | 52.7 | 155 |
2. YOLOv1的设计哲学:目标检测就是一次回归
2.1 理解7×7网格:图片这样被切碎
YOLOv1把输入图片缩放到448×448,再逻辑上划分成S×S个网格,论文里S=7,也就是7×7一共49个格子。这里的“网格”不是真正切割图片做独立推理,而是一种空间上的“责任分配制度”:如果一个目标的中心点落在某个格子里,那么这个格子就负责预测这个目标。
这套逻辑很像城市的网格化治理:每个网格员只对辖区内的事件负责,事件归属的判断标准就是“中心点在哪”。为什么要按中心点而不是按面积重叠?因为如果按面积,两个网格会同时对一个目标负责,输出就会出现大量重复框,而且训练时的“该谁负责”会变得模糊不清。中心点规则简单粗暴,但极大地简化了标签分配。
每个格子预测什么呢?论文设为B=2个边界框,每个框是一个5维向量:中心点相对格子的偏移x,y,宽高相对整张图的比例w,h,外加一个置信度confidence。此外每个格子还负责预测C个类别的条件概率,VOC数据集C=20。所以每个格子输出的维度是B×5 + C = 2×5 + 20 = 30,整张图最终输出一个7×7×30的张量。
这里要特别强调一个很多新手会忽略的约束:每个格子虽然有2个候选框,但类别概率是格子级别的,不是框级别的。也就是说,两个框共享同一个20维类别概率。换句话说,一个格子最终只可能被判定为“一个目标实例”,即使它预测了两个框,NMS之后通常也只保留其中一个。因此YOLOv1理论上最多只能检测49个目标(实际还要被置信度阈值和NMS过滤掉很多)。
2.2 置信度不是“概率”,而是“有没有目标×框得准不准”
YOLO的每个边界框除了坐标,还带一个confidence值。论文里它的定义是:
confidence = Pr(Object) × IoU(pred, truth)
拆开看,它同时编码了两层信息:第一,这个框里到底有没有目标;第二,如果有,这个框和真实框的重合程度有多好。这两者相乘,使得置信度既惩罚“空框”,也惩罚“框歪了”。
训练时这个值的回归目标怎么定?如果某个格子里确实存在目标中心,那么负责预测的那个边界框的目标置信度就是它和真实框的IoU;如果格子里没有目标,置信度目标直接为0。由于绝大多数格子都是背景,训练时会出现严重的正负样本不平衡,所以损失函数里专门设了一个很小的权重λnoobj=0.5来削弱背景格子的梯度贡献,这一点到损失函数部分再展开。
推理时,最终每个框的分数不是直接用置信度,而是:
score = 类别条件概率 × 置信度 = Pr(Class_i | Object) × Pr(Object) × IoU
相乘之后的意义是:既要是这个类别,又要真的框住这个目标。这也是YOLO一个很实用的特性——背景误检率极低,Faster R-CNN在VOC2007上的背景误检率约13.6%,YOLO只有4.75%。
2.3 为什么是“回归”而不是“分类”
传统的目标检测把问题拆成“找出区域(疑似目标)”和“判定区域类别(是猫还是狗)”,本质上是分类逻辑。YOLO则把整个检测任务当作一个回归问题:输入是像素,输出是一个7×7×30的数值张量,目标值是从标注里编码出来的同样形状的张量,中间完全靠一个神经网络去拟合。
这样做最直接的收益是框架统一。分类任务通常用softmax交叉熵,回归任务用平方误差,两阶段检测要把这两套目标分开优化;YOLO为了“统一”,干脆连分类也用了平方误差。这种做法从信息论角度并不优雅,作者也承认“sum-squared error虽然简单,但对类别概率不一定是最优的”——但它带来了一个杀手锏级别的工程优势:整个模型只有一个网络、一个损失函数、一次反向传播,训练和部署都极其简单。
3. 网络结构与损失函数:YOLOv1的精髓都在细节里
3.1 骨架设计:24层卷积,参考GoogLeNet而不是VGG
YOLOv1的网络结构从今天看不算深:24层卷积层加2层全连接层。卷积骨架借鉴了GoogLeNet的思想——大量使用1×1卷积来压缩通道,再接3×3卷积提取特征。作者没有直接用VGG-16(当时图像分类的主流选择),原因是VGG卷积层多、计算量太大,而YOLO从设计目标上就需要“快”。
整个前20层先用224×224的ImageNet数据集做图像分类预训练,然后再把输入分辨率提升到448×448,在检测任务上微调。这里有两个关键细节:第一,预训练和检测任务的输入尺寸不一致,意味着微调时网络要适应更大的特征图分辨率;第二,作者只保留了前20层进行预训练,后面新增了4层卷积和2层全连接,这4层卷积是随机初始化的。这种“拿分类权重做初始化、再加层做检测”的做法,后来成为检测领域标准操作。
激活函数方面,最后一层输出层用线性激活(因为要输出连续坐标和置信度),其余层全部用Leaky ReLU,斜率为0.1。Leaky ReLU在这里的作用不是单纯解决“死神经元”,而是让网络对负区间的梯度不至于完全消失,这对坐标回归任务比较友好。
3.2 输出张量怎么解读:从7×7×30还原出目标框
想要真正搞懂YOLOv1,必须亲手把7×7×30的张量拆开一次。假设模型输出的tensor形状是1×30×7×7,对每个网格位置(i,j),那30个值是这样排列的:
- 前5个值:第1个边界框的 (x, y, w, h, confidence)
- 第6-10个值:第2个边界框的 (x, y, w, h, confidence)
- 第11-30个值:20个类别的条件概率
注意x,y是相对于格子左上角的偏移比例,范围0到1,不是绝对像素坐标;w,h是相对于整张输入图的比例,范围0到1。要还原成图片上的真实框,需要做两步换算:
x_abs = (column + x) × 原图宽度 / S y_abs = (row + y) × 原图高度 / S w_abs = w × 原图宽度 h_abs = h × 原图高度
这段看似简单,但我见很多人在复现时翻车,都是因为忘了“坐标在格子内、尺寸在整图内”这个设定。一个格子的列索引是column、行索引是row,x要加column,y要加row,分别再除以7。
推理时的完整流程是:前向拿到7×7×30张量之后,把每个格子的每个框都算出一个最终分数(类别概率×置信度),按某个阈值(比如0.3)筛掉低分框,最后在整张图上做一次按类别独立的NMS。这里“按类别独立”是必须的,否则猫和狗的框会互相抑制。由于每张图的77×30里总共只有98个候选框,NMS的计算量可以忽略不计,这也是YOLO后处理极快的原因。
3.3 损失函数详解:每一项权重为什么那样设
YOLOv1的损失函数是理解整篇论文的钥匙。它把所有误差统一成平方误差,公式拆开是5个部分:
中心点坐标误差:只计算“有目标”的格子,且只选择该格子内和真实框IoU最大的那个预测框参与回归,权重系数λcoord=5。
宽高误差:同样只算负责的那个框,但w和h先开根号再用平方误差。开根号是YOLO最精妙的设计之一:同样的绝对误差,对小框的惩罚比大框更重。因为IoU对尺寸敏感,同样偏差10像素,放在100像素的框上和无所谓,放在30像素的框上就会让IoU掉一大截。根号压缩了大框的误差区间,等于自动给“小框偏差”加了更高的权重。
有目标格子的置信度误差:目标是和真实框的IoU,直接平方误差。
无目标格子的置信度误差:目标为0,但权重只有λnoobj=0.5。这个0.5不是拍脑袋,而是因为一张自然图像里绝大多数格子都是背景,如果权重也设成1,模型会花大量梯度去学“这里什么都没有”,反而忽略少数真正有目标的位置。
分类误差:只有格子内存在目标才计算,直接对条件概率做平方误差。这里严格说并不符合概率分布的优化理论,但作者为了维持“整个模型就是一个回归器”的设计初衷,刻意放弃了交叉熵。
代码实现视角下的核心逻辑大概是这样的:
for i in range(S): for j in range(S): pred = output[i, j] # 30维 target = target_tensor[i, j] # 30维,由真实标注编码而来 # 1. 有目标才处理坐标 if target[10] == 1: # 假设20类的首位置放objectness标记 # 选择与GT IoU更大的框负责拟合 best_box = argmax(ious) xy_loss = lambda_coord * ((pred[best_box*5] - target[best_box*5])**2 + ...) wh_loss = lambda_coord * ((sqrt(pred[best_box*5+2]) - sqrt(target[best_box*5+2]))**2 + ...) cls_loss = sum((pred[10:] - target[10:])**2) conf_loss = (pred[best_box*5+4] - target[best_box*5+4])**2 else: conf_loss = lambda_noobj * sum((pred[b*5+4] - 0)**2 for b in range(2))“只选择IoU最大的那个框负责”这个规则也容易忽略。每个格子预测两个框,但只有和真实框IoU高的那个会参与坐标和置信度的反向传播,另一个框只在无目标的格子里被惩罚。这相当于网络在每个格子里“养了两个候选人”,训练时让更合适的那个出头,降低了单框定位不稳定的风险。
3.4 训练策略:动态学习率和多尺度增强
YOLOv1训练时的超参数设置,今天来看也很有参照价值:批大小64,动量0.9,权重衰减0.0005,学习率先设1e-3,训练前75轮,之后30轮提高到1e-2,最后30轮再降回1e-3。作者没用余弦退火这类花哨策略,就是简单粗暴的分阶段衰减,但对这个模型足够有效。
数据增强方面,YOLOv1用了随机缩放(原图尺寸的0.8到1.2倍)、随机平移、以及HSI空间的色彩抖动(饱和度上调1.5倍、曝光度上调1.5倍、色调偏移1.1倍)。这里强调一点,色彩抖动在目标检测里常常被当成可有可无的选项,但对YOLO这种“全图看一眼就输出”的模型来说,颜色分布的多样性会影响它对类别的判断,所以作者才刻意在三个色彩维度上同时扰动。
还有一个实操上的细节:训练时输入并不一直是448×448,作者还做了一个类似“多尺度”的操作,每隔一定批次随机调整输入尺寸(在320到640之间?)——严格来说,这是YOLOv2才明确提出的策略,v1论文里主要靠随机缩放和裁剪来增强。复现时不要混淆。
4. 实操复现:在PyCharm里跑通YOLOv1全流程
4.1 环境准备与依赖安装
YOLOv1原始实现是C语言写的Darknet框架,现在大部分人复现经典模型更习惯用PyTorch。我不建议你直接硬啃原始C代码来学习原理,但强烈建议至少跑通一个PyTorch版的前向和训练流程。环境准备可以这样做:
conda create -n yolo python=3.9 -y conda activate yolo pip install torch torchvision opencv-python numpy matplotlib tqdm pyyaml如果你习惯在PyCharm里写代码,记得创建好conda虚拟环境后,把项目的解释器指向这个环境的Python,而不是系统默认Python,否则依赖经常串台。这一步说出来像废话,但实际项目里因为环境指错导致“装了包却import不到”的案例我见过太多次。如果你用的是新版PyTorch,不需要额外装CUDA toolkit,直接装带CUDA的torch版本即可;如果是在NVIDIA AGX Orin这类Jetson设备上搭环境,则要注意JetPack版本与PyTorch官方预编译包的兼容性,通常建议用NVIDIA官方维护的container镜像,避免自己从源码编译时的坑。
我的建议是,学习原理阶段不要直接上全量VOC数据集,先下载一个只有十几张人车图片的demo数据,或者直接拿一张测试图跑推理。先把整条链路走通,再谈训练和调参。
4.2 推理代码:一步前向拿到结果
YOLOv1的前向特别“朴素”:没有锚框、没有特征金字塔,输入一张图,输出一个7×7×30的张量。写一个最小可用的推理脚本,核心就是拆张量加NMS:
import torch import numpy as np import cv2 def decode_predictions(output, S=7, B=2, C=20, conf_threshold=0.3, iou_threshold=0.4): # output shape: [1, 30, 7, 7] output = output.squeeze(0).permute(1, 2, 0).cpu().numpy() # [7, 7, 30] all_boxes = [] for row in range(S): for col in range(S): cell = output[row, col] class_probs = cell[B * 5:] cls_id = int(np.argmax(class_probs)) cls_score = class_probs[cls_id] for b in range(B): start = b * 5 x_offset = cell[start] y_offset = cell[start + 1] w_norm = cell[start + 2] h_norm = cell[start + 3] conf = cell[start + 4] score = conf * cls_score if score < conf_threshold: continue x_center_img = (col + x_offset) / S y_center_img = (row + y_offset) / S all_boxes.append([x_center_img, y_center_img, w_norm, h_norm, score, cls_id]) # 按类别独立的NMS return nms_per_class(all_boxes, iou_threshold) def nms_per_class(boxes, iou_threshold): keep = [] for cls in range(20): cls_boxes = [b for b in boxes if b[5] == cls] cls_boxes.sort(key=lambda x: -x[4]) while cls_boxes: best = cls_boxes.pop(0) keep.append(best) cls_boxes = [b for b in cls_boxes if compute_iou(best, b) < iou_threshold] return keep这段代码有几个值得注意的地方。score用的是“类别概率乘置信度”,不是直接用置信度,这正是论文里的最终目标分数。坐标x_center_img是在0到1之间归一化过的,可视化时别忘了乘以原图的宽和高,否则画出来的框全部挤在左上角。NMS的IoU阈值一般设在0.4到0.5之间,太小会导致重叠目标被误删,太大会出现大量重复框。
4.3 训练的小白友好路线:从一个人体小数据集开始
如果你要训练自己的YOLOv1,务必从小规模开始。我推荐的做法是:先用LabelImg或Roboflow标注大约200张包含person和chair两个类别的图片,导出成YOLO格式的txt标注,然后按下面的流程跑:
- 下载一个PyTorch实现的YOLOv1仓库,确认支持自定义数据集。
- 把标注文件按train/val分成两份,比例大概8:2。
- 修改配置文件里的类别数(改成2)、类别名称、输入尺寸448。
- 训练前先确保能正常加载一张图片的target张量,打印出来看一眼shape是不是7×7×30。
- 训练100到200轮,学习率1e-3,batch size根据显存调整。
我踩过最大的坑是target张量编码错误。YOLO格式标注里,每一行是“class x_center y_center w h”,注意x_center和y_center是相对于整张图的,而YOLOv1的loss里需要的是“相对于格子的偏移”。如果在构造target时忘了把x_center转换为格子内偏移(即 x_cell = x_center × S - floor(x_center × S)),训练时坐标损失永远无法收敛。另外,如果某个格子同时对应多个目标,要决定哪个目标占据该格子,最直接的规则是随机选一个或者按出现顺序覆盖,YOLOv1原论文对这种情况没有严格约定,实操中通常会选择IoU更高或面积更大的那个。
4.4 老模型的新酒:在AGX Orin上部署YOLOv1速度如何
YOLOv1的计算量大约8.5 GFLOPs,对比VGG-16的30多GFLOPs,放在今天的边缘设备上可以说是极其轻量。在NVIDIA AGX Orin上跑PyTorch版YOLOv1,即使不做TensorRT优化,也能轻松上几百FPS;如果转成TensorRT,单张448×448的推理时间能压到几毫秒。
当然,现实项目里没人会用v1去做产品。这里想表达的是:YOLOv1的结构简单,非常适合作为你在新硬件上验证工具链、测试部署流程的“最小模型”。比如你要在AGX Orin上搭建一套YOLO环境,拿v1做通明跑、转ONNX、转TensorRT的全链路,比直接上YOLOv8去调参容易得多。模型越小,定位问题就越简单,排错成本越低。
5. YOLOv1的硬伤与后续演进路线
5.1 四个绕不开的短板
YOLOv1是一次激进的简化,代价也很明显。第一个短板是每个格子只能输出一个类别,导致密集目标和重叠目标经常丢检,人群、货架上的商品、显微镜下的细胞这类场景基本是重灾区。第二个短板是小目标。7×7网格太粗,一个目标如果只有十几个像素,它所在格子的特征几乎被周围背景稀释光了,而且S×S的输出天然限定了检测粒度的上限。第三个短板是定位误差偏大。论文里的误差分析显示,YOLO的定位错误占比19.0%,而Fast R-CNN只有8.6%。这也是为什么YOLOv1的mAP比Faster R-CNN低近10个点的直接原因。第四个短板是比较隐蔽的:大框小框共享同样的绝对坐标误差,虽然开根号缓解了一部分,但本质上还是“一锅煮”的损失设计。
5.2 从v1到v8:YOLO系列是怎么一步步补齐短板的
YOLOv2主要做了四件事:引入Batch Normalization加速收敛;把全连接层去掉、引入锚框机制;增加多尺度训练;设计了一个更轻的Darknet-19骨干。锚框的引入让每个格子能预测多个不同尺度和长宽比的框,直接从结构上缓解了“一个格子只能有一个类别”的限制。
YOLOv3则把物体检测的尺度问题彻底打开:用特征金字塔结构,在三个不同分辨率的特征图上分别输出预测,小目标检测能力大幅提升;每个位置预测3个锚框,最终输出张量从单一尺度变成多尺度;分类头从softmax改成多个独立的logistic二分类器,支撑多标签场景;骨干换成了带残差连接的Darknet-53,深度加深但训练更容易。
后面的v4、v5、v8更多是在工程上做加法:CSP结构减少重复梯度、Mosaic数据增强提高小目标样本量、anchor-free分支简化后处理、实例分割分支扩展任务范围。但请注意,无论怎么改,YOLO的底层依然是“整图一次前向,输出张量里同时编码位置和类别”这个单阶段回归框架,而这个框架的开创者正是YOLOv1。
5.3 YOLO生态的跨界应用给我们的启发
YOLO家族早就超出了目标检测本身。YOLOv8官方就提供了seg(实例分割)、pose(姿态估计)、obb(旋转目标检测)等不同任务口径;社区里还有大量基于YOLO的二次开发。网络热词里提到的“基于YOLO目标检测+多模态AI分析的智慧交通事故检测分析系统”,本质上就是先用YOLO做车辆、行人、事故区域的定位,再交给大模型做语义理解和研判;“学生专注度检测”则通常是YOLO做头部和手势检测,再加行为时序分析;医疗影像里的肺结节检测也有不少团队尝试过YOLO系列,但这类场景对小目标和多尺度要求很高,实际项目更推荐YOLOv5/v8配合高分辨率输入和注意力机制,直接用v1基本行不通。
如果你现在要做一个实际落地的检测项目,我的建议是把YOLOv8作为首选基线,而不是费劲自己去改v1。但如果你想理解检测算法的演进逻辑、看懂论文里那些“为什么这么设计”,YOLOv1仍然是最好的教材,因为它的结构足够小、思想足够纯粹,你可以亲手把它的每个环节都复现一遍,而不是在一个几千万参数的黑盒里瞎猜。
6. 常见问题与避坑清单
6.1 问题速查表
| 症状 | 可能原因 | 排查方向 |
|---|---|---|
| 输出张量不是7×7×30 | 网格数、类别数、框数配置不一致 | 检查模型定义和配置文件 |
| 训练loss下降但mAP极低 | target编码错误,最常见是坐标忘了转格子内偏移 | 先可视化一张图的target和pred,逐格子打印对比 |
| NMS后空结果 | 置信度阈值太高,或模型未收敛 | 把阈值降到0.1查看原始响应 |
| 小目标完全检测不到 | v1的网格分辨率天然劣势 | 换v3+或提高输入分辨率;不要指望v1检测小目标 |
| 推理框全部挤在左上角 | 可视化时忘了把归一化坐标乘回原图尺寸 | 检查x_center_img乘以原图宽度 |
| 同一个目标输出大量重复框 | NMS的IoU阈值太松,或没有按类别独立做NMS | 调低iou_threshold到0.4,并确认按类别过滤 |
| 训练时显存爆掉 | 448×448输入加较大batch | 减小batch size,或先用320×320测试跑通 |
6.2 我的几条实操心得
复现经典模型的正确顺序,永远是“先单张图前向、再单batch训练、再全量训练”。很多新手急着在完整VOC上跑训练,结果跑了三天发现loss完全不降,最后定位到是target编码错误,白白浪费几十个小时GPU时间。我自己的习惯是写一个极小的可视化函数,把target张量解码出来的框画在原图上,跑一个batch就截图看一眼,如果框的位置和真实物体基本对得上,再开始正式训练。
YOLOv1的损失函数我强烈建议手写一遍,不要直接调现成仓库里的loss。手写的目的不在于节省那几行代码,而是让你真正理解“哪些项参与梯度、哪些项被掩码掉、为什么宽高要开根号”。我见过太多人用着YOLOv8却完全说不清loss里box_loss和cls_loss的分配逻辑,就是因为一开始建立在“会用API”而不是“懂原理”的基础上。
还有一个经验是:实验日志一定要记全参数。YOLO系列的超参数非常敏感,学习率、权重衰减、置信度权重、输入尺寸、类别数这些变量相互影响。我早期复现时试过改了一个数据增强参数,mAP直接掉了5个点,因为没有记录日志,整整排查了两天才找到原因。写实验记录不是科研人员的专利,任何想真正掌握一个模型的工程师都应该养成这个习惯。
最后分享一个小经验:如果你在PyTorch里复现YOLOv1,调试阶段把模型的输出层激活函数临时去掉(不改代码,只是把最后一层的输出打印出来),你会看到很多数值并不在0到1之间,这是正常的,因为回归任务没有像分类那样的softmax约束,网络可以输出任意实数值。这恰恰说明,检测模型的输出在训练初期就是一团乱糟糟的数字,所有“看起来合理的框”都是loss一点点约束出来的。理解了这一点,你就不会因为前几个epoch的预测结果惨不忍睹而怀疑代码写错了。