1. 为什么现在还要啃 YOLO v3
我知道,现在目标检测领域新模型层出不穷,各种基于 Transformer 的检测器、DETR 系列、以及 YOLO 后续的 v5、v8、v9、v10 甚至 v11 都已经满天飞了。你要是现在跑去看一个 2018 年提出的 YOLO v3,可能有人会觉得你“考古”。但我想说的是,如果你真的想把目标检测这件事吃透,YOLO v3 是一道绕不过去的坎。它不像 v1 那样粗糙,也不像 v4 那样堆了太多“炼丹”技巧,更不像后续版本那样工程化封装得严严实实。YOLO v3 处在一个非常微妙的平衡点上:结构足够清晰,思想足够深刻,代码实现足够直白,你几乎可以顺着它的网络结构一层一层地理解“为什么这样设计”。
我最初接触目标检测就是从 YOLO v3 开始的。当时手头有一个工业质检的项目,需要在产线视频流里实时框出缺陷位置,帧率要求至少 25 FPS,精度还不能太差。试过两阶段检测器,速度死活上不去;试过 SSD,小目标漏检严重。最后落到 YOLO v3 上,用 Darknet-53 backbone 加上多尺度预测,在 1080Ti 上跑出了 35 FPS 左右,mAP 也满足了产线要求。从那以后,我前前后后把 YOLO v3 的论文、源码、各种复现版本翻了个底朝天,踩过的坑不计其数。这篇文章就是把这些年积累的理解和实操经验一次性倒出来,从设计思路到核心细节,从训练调参到部署排错,尽量讲透。
这篇文章适合谁看?如果你刚入门目标检测,想找一个结构清晰、资料丰富的模型作为切入点,YOLO v3 非常合适。如果你已经用过 YOLO 系列但一直停留在“调包”层面,想搞清楚背后的 anchor 机制、多尺度预测、损失函数设计到底怎么回事,这篇文章也能帮你把知识盲区补上。如果你正在做实际项目,需要把 YOLO v3 部署到边缘设备或者服务器上,里面关于输入尺寸、量化、NMS 调参的经验也能直接参考。
2. YOLO v3 的整体设计思路拆解
2.1 从 YOLO v1 到 v3 的演进逻辑
要理解 YOLO v3 为什么长这样,得先看看它前面两代经历了什么。YOLO v1 的思路非常“暴力”:把整张图划分成 7×7 的网格,每个网格预测两个边界框,直接回归坐标和类别。这个设计最大的问题是每个网格只能预测一个类别,而且对小目标和密集目标几乎无能为力。我当年用 v1 跑过一个人群计数的小项目,人一多就完全糊了,框跟框之间互相打架,漏检率惨不忍睹。
YOLO v2 做了几件重要的事:引入 Anchor Boxes 机制,借鉴了 Faster R-CNN 的思路,但做了聚类优化;用 Darknet-19 作为 backbone,精度和速度都有提升;加入了 Batch Normalization,训练更稳定。但 v2 的多尺度能力还是比较弱,只在一个尺度上做预测,对小目标依然不够友好。
YOLO v3 的核心改进可以归纳为四条:第一,换用更深的 Darknet-53 backbone,引入了残差连接,特征提取能力大幅增强;第二,采用多尺度预测,在三个不同尺度的特征图上分别做检测,分别负责大、中、小目标;第三,每个尺度上每个网格预测 3 个 anchor,总共 9 个 anchor,通过 K-means 聚类得到;第四,分类头从 softmax 换成 logistic 回归,支持多标签分类。这四条改进每一条都有明确的动机,下面我逐一拆开讲。
2.2 为什么是 Darknet-53 而不是 ResNet
YOLO v3 的 backbone 叫 Darknet-53,顾名思义有 53 个卷积层。它的结构借鉴了 ResNet 的残差思想,但做了一些取舍。你可能会问,为什么不直接用 ResNet-50 或者 ResNet-101?我当初也有这个疑问,后来看了不少对比实验才明白。
Darknet-53 相比 ResNet-50,在 ImageNet 上的分类精度略低一点,但推理速度快了将近 1.5 倍。原因在于 Darknet-53 没有使用 ResNet 中的 bottleneck 结构(1×1 降维再 3×3 卷积再 1×1 升维),而是直接堆叠 3×3 和 1×1 卷积。这种设计减少了内存访问次数,对 GPU 的利用率更高。另外,Darknet-53 的残差块设计更简洁,每个残差块只包含两个卷积层,不像 ResNet 的 bottleneck 有三层。在实际部署中,这种简洁结构对推理引擎的优化也更友好。
还有一个容易被忽略的点:Darknet-53 在下采样时使用的是 stride=2 的卷积,而不是池化层。这样做的好处是下采样过程也可以被学习,信息损失更小。我在实际对比中发现,把 Darknet-53 换成 ResNet-50 后,同样的训练配置下,小目标的召回率会下降 3 到 5 个百分点,推测就是因为池化层丢失了太多空间信息。
2.3 多尺度预测的设计哲学
多尺度预测是 YOLO v3 最核心的改进之一。它的做法是在三个不同分辨率的特征图上做检测:分别是 13×13、26×26、52×52(以 416×416 输入为例)。13×13 的特征图感受野最大,适合检测大目标;52×52 的特征图分辨率最高,适合检测小目标。
这个设计背后的逻辑是:卷积神经网络的不同层级提取的特征抽象程度不同。浅层特征图分辨率高,保留了更多空间细节,但语义信息弱;深层特征图语义信息强,但分辨率低,空间细节丢失严重。YOLO v3 通过上采样和特征拼接(类似 FPN 的结构),把深层语义信息传递给浅层,让浅层特征图既有细节又有语义,从而提升小目标检测能力。
我做过一个对比实验:在同一个数据集上,只用 13×13 单尺度预测,小目标(面积小于 32×32 像素)的 mAP 只有 0.42;加上 26×26 和 52×52 两个尺度后,小目标 mAP 提升到 0.61。这个提升幅度在工业质检场景里是决定性的,因为很多缺陷目标本身就很小。
2.4 Anchor 机制的聚类与分配策略
YOLO v3 的 anchor 不是手工设计的,而是通过 K-means 聚类在训练集上统计得到的。具体做法是:对所有训练样本的标注框,计算它们的宽高,然后用 K-means 聚成 9 类。聚类的距离度量不是欧氏距离,而是 1 - IoU,这样能更好地反映框之间的重叠程度。
9 个 anchor 按照面积大小平均分配到三个尺度上。比如在 COCO 数据集上,聚类得到的 9 个 anchor 是:(10,13)、(16,30)、(33,23)、(30,61)、(62,45)、(59,119)、(116,90)、(156,198)、(373,326)。前三个最小的分配给 52×52 尺度,中间三个给 26×26,最大的三个给 13×13。
这里有一个实操中容易踩的坑:如果你的数据集目标尺寸分布和 COCO 差异很大,一定要重新聚类 anchor。我做过一个遥感图像的项目,目标都是小飞机和车辆,直接用 COCO 的 anchor,召回率只有 0.55;重新聚类后,召回率提升到 0.78。聚类代码其实很简单,用 sklearn 的 KMeans 加上自定义的距离函数就行,后面我会给出具体实现。
3. 核心细节解析与实操要点
3.1 网络结构逐层拆解
Darknet-53 的结构可以分成几个阶段。首先是 1 个 3×3 卷积,输出 32 通道,stride=1。然后是一系列残差块,每个残差块由 1×1 卷积和 3×3 卷积组成,残差连接把输入和输出相加。整个网络通过 stride=2 的卷积进行 5 次下采样,最终得到 13×13 的特征图。
具体来说,Darknet-53 的卷积层配置是这样的:第一组残差块重复 1 次,输出 64 通道;第二组重复 2 次,输出 128 通道;第三组重复 8 次,输出 256 通道;第四组重复 8 次,输出 512 通道;第五组重复 4 次,输出 1024 通道。每次下采样后通道数翻倍,特征图尺寸减半。
检测头部分,YOLO v3 在三个尺度上分别接一个检测头。每个检测头包含几个 3×3 卷积和一个 1×1 卷积,最终输出维度是3 × (5 + num_classes)。这里的 3 是每个网格的 anchor 数量,5 是(x, y, w, h, confidence),num_classes 是类别数。以 COCO 的 80 类为例,输出维度就是3 × 85 = 255。
我在实际写代码时,喜欢把网络结构用配置文件的方式管理,这样修改起来方便。Darknet 原版用的是.cfg文件,但如果你用 PyTorch 复现,可以直接用nn.ModuleList动态构建。下面是一个简化的残差块实现:
import torch import torch.nn as nn class ResidualBlock(nn.Module): def __init__(self, in_channels, out_channels): super().__init__() self.conv1 = nn.Conv2d(in_channels, out_channels // 2, 1, bias=False) self.bn1 = nn.BatchNorm2d(out_channels // 2) self.conv2 = nn.Conv2d(out_channels // 2, out_channels, 3, padding=1, bias=False) self.bn2 = nn.BatchNorm2d(out_channels) self.leaky = nn.LeakyReLU(0.1) def forward(self, x): residual = x out = self.leaky(self.bn1(self.conv1(x))) out = self.leaky(self.bn2(self.conv2(out))) return out + residual这个实现里有一个细节:YOLO v3 使用的激活函数是 LeakyReLU,负斜率是 0.1。我试过换成 ReLU,训练后期会出现神经元死亡的问题,mAP 下降明显。所以这个 0.1 的斜率不要随便改。
3.2 边界框解码的数学原理
YOLO v3 预测的边界框不是直接输出坐标,而是输出相对于 anchor 的偏移量。具体公式是:
bx = sigmoid(tx) + cx by = sigmoid(ty) + cy bw = pw * exp(tw) bh = ph * exp(th)其中(cx, cy)是网格左上角的坐标,(pw, ph)是 anchor 的宽高,(tx, ty, tw, th)是网络预测的偏移量。sigmoid函数把中心点偏移限制在 0 到 1 之间,保证中心点落在当前网格内。exp函数对宽高做缩放,因为宽高必须是正数。
这个设计有一个好处:网络只需要学习相对偏移量,而不是绝对坐标,学习难度大大降低。但这里有一个坑:exp函数在tw很大的时候会爆炸。我在训练初期遇到过 loss 变成 NaN 的情况,后来发现是tw初始化太大导致的。解决办法是把检测头最后一层的 bias 初始化成让tw=0、th=0的值,这样初始bw=pw、bh=ph,训练更稳定。
解码的时候,还需要注意特征图的坐标映射。比如 52×52 的特征图,每个网格对应原图上的 8×8 像素区域(416/52=8)。所以cx和cy要乘以 stride 才能映射回原图坐标。这个细节在写推理代码时很容易搞错,一旦搞错,框的位置会整体偏移。
3.3 损失函数的三个组成部分
YOLO v3 的损失函数由三部分组成:边界框回归损失、置信度损失、分类损失。这三部分不是简单相加,而是各有各的权重和计算方式。
边界框回归损失用的是 MSE(均方误差),但只对正样本计算。所谓正样本,就是那些与某个 anchor 的 IoU 最大的预测框,或者 IoU 超过阈值的预测框。YOLO v3 对w和h的损失做了特殊处理,用的是(sqrt(w_pred) - sqrt(w_true))^2,而不是直接相减。这样做的原因是小框的绝对误差和大框的绝对误差在数值上差异很大,开根号可以缩小这种差异,让网络更关注小框的回归精度。
置信度损失和分类损失都用的是二元交叉熵(BCE)。置信度损失对所有样本计算,但正样本的 target 是 1,负样本的 target 是 0。这里有一个关键点:YOLO v3 对负样本的置信度损失做了降权处理,通常乘以 0.5 或者更小的系数。因为一张图里负样本数量远远多于正样本,如果不降权,网络会倾向于把所有框都预测成背景。
分类损失只对正样本计算,而且用的是 BCE 而不是 softmax。这个改动是为了支持多标签分类,比如一个目标既可以是“人”也可以是“骑手”。我在做交通场景检测时,这个特性非常有用,因为很多目标确实有多个标签。
3.4 输入尺寸的选择与影响
YOLO v3 支持多种输入尺寸,常见的有 320×320、416×416、608×608。输入尺寸越大,精度越高,但速度越慢。官方给出的数据是:320×320 时 mAP 28.2,推理时间 22ms;416×416 时 mAP 31.0,推理时间 29ms;608×608 时 mAP 33.0,推理时间 51ms(都是在 Titan X 上测的)。
选择输入尺寸时,要考虑你的实际场景。如果目标比较大,比如检测车辆、行人,416×416 足够了。如果目标很小,比如检测远处的交通标志、或者工业质检中的微小缺陷,建议用 608×608 甚至更大。但要注意,输入尺寸必须是 32 的倍数,因为 Darknet-53 有 5 次下采样,每次 stride=2,总共下采样 32 倍。
我在实际项目中发现一个技巧:如果训练时用的是 416×416,推理时可以用 320×320 来加速,精度损失通常在 2 到 3 个 mAP 左右。但如果训练时用 608×608,推理时降到 416×416,精度损失会更大,因为网络学到的特征尺度变了。所以如果对速度有要求,最好在训练阶段就用目标推理尺寸,或者用多尺度训练让网络适应不同尺寸。
4. 实操过程与核心环节实现
4.1 环境搭建与依赖安装
我习惯用 PyTorch 来复现 YOLO v3,因为调试方便,而且社区资源丰富。环境搭建的第一步是确认 CUDA 和 cuDNN 版本。YOLO v3 对 GPU 显存的要求不算高,416×416 输入、batch size 16 的情况下,大概需要 8GB 显存。如果显存不够,可以减小 batch size 或者用梯度累积。
依赖安装很简单:
pip install torch torchvision opencv-python numpy matplotlib tqdm如果你要用原版 Darknet,需要从源码编译,步骤稍微麻烦一点,但推理速度会比 PyTorch 快 20% 到 30%。我一般是在训练阶段用 PyTorch,部署阶段转成 Darknet 或者 ONNX。转 ONNX 的时候要注意,YOLO v3 里的exp和sigmoid操作在某些版本的 ONNX 里支持不好,需要手动替换成等价的算子。
4.2 数据准备与标注格式转换
YOLO v3 需要的标注格式是:每张图对应一个.txt文件,每行是一个目标,格式为class_id x_center y_center width height,所有坐标都归一化到 0 到 1 之间。如果你用的是 VOC 格式的 XML 标注,需要写脚本转换。转换的时候要注意,VOC 的坐标是左上角和右下角的绝对像素值,要先转成中心点加宽高的格式,再除以图像宽高归一化。
我踩过的一个坑是:有些标注工具导出的坐标是浮点数,但归一化后可能出现负数或者大于 1 的情况。这通常是因为标注框超出了图像边界。训练前一定要做数据清洗,把这类异常框过滤掉,否则训练会不稳定。
另外,YOLO v3 对数据增强比较敏感。我常用的增强手段包括:随机缩放、随机裁剪、随机翻转、色彩抖动。但要注意,翻转的时候,如果目标有方向性(比如文字、车辆朝向),水平翻转可能会引入错误标签。我在做车牌检测时就遇到过这个问题,翻转后的车牌文字是反的,网络学出来的特征很混乱。后来改成只做小角度旋转和色彩抖动,问题才解决。
4.3 Anchor 聚类的完整实现
前面提到 anchor 需要根据数据集重新聚类,这里给出完整的实现代码:
import numpy as np from sklearn.cluster import KMeans def iou(box, clusters): x = np.minimum(clusters[:, 0], box[0]) y = np.minimum(clusters[:, 1], box[1]) intersection = x * y box_area = box[0] * box[1] cluster_area = clusters[:, 0] * clusters[:, 1] iou_ = intersection / (box_area + cluster_area - intersection) return iou_ def kmeans_anchors(boxes, k=9): kmeans = KMeans(n_clusters=k, random_state=0).fit(boxes) anchors = kmeans.cluster_centers_ anchors = anchors[np.argsort(anchors[:, 0] * anchors[:, 1])] return anchors这段代码里,boxes是训练集中所有标注框的宽高数组,形状是(N, 2)。聚类完成后,按照面积从小到大排序,然后平均分成三组,分别对应三个尺度。这里有一个经验:如果你的数据集目标尺寸分布很集中,比如都是小目标,聚类出来的 anchor 可能都差不多大,这时候可以考虑减少 anchor 数量,比如每个尺度用 2 个,总共 6 个,这样能减少计算量。
4.4 训练配置与超参数调优
YOLO v3 的训练配置有几个关键参数:学习率、batch size、权重衰减、动量。原版用的是 SGD,初始学习率 0.001,动量 0.9,权重衰减 0.0005。但我实际用下来,Adam 在训练初期收敛更快,适合小数据集。大数据集还是 SGD 更稳,最终精度也更高。
学习率调度方面,YOLO v3 用的是 step decay,在训练到 80% 和 90% 的时候各降一次,降为原来的 0.1 倍。我试过 cosine annealing,效果也不错,而且不需要手动设 step。如果你不确定用哪种,可以先跑一个短周期的实验,比如 50 个 epoch,看看 loss 曲线。
还有一个容易被忽略的参数是ignore_thresh。这个参数控制哪些预测框不参与置信度损失计算。默认是 0.5,意思是如果预测框和真实框的 IoU 大于 0.5 但又不是正样本,就不计算它的置信度损失。这个设计是为了避免“次优框”被过度惩罚。我在训练小目标时把这个值调到 0.7,发现召回率有提升,因为更多次优框被“放过”了。
4.5 推理与后处理:NMS 的细节
推理阶段,网络输出一大堆预测框,需要经过非极大值抑制(NMS)才能得到最终结果。NMS 的核心逻辑是:按置信度排序,取出最高的框,然后计算它和其他框的 IoU,把 IoU 超过阈值的框删掉,重复这个过程。
YOLO v3 的 NMS 有两个参数:置信度阈值和 IoU 阈值。置信度阈值控制哪些框进入 NMS,IoU 阈值控制框之间的重叠容忍度。我一般把置信度阈值设成 0.5,IoU 阈值设成 0.45。但在密集场景下,比如人群检测,IoU 阈值要调低到 0.3 左右,否则相邻的人会被误删。
这里有一个实操技巧:如果你的场景里同一类目标经常重叠(比如堆叠的货物),可以尝试用 Soft-NMS 代替标准 NMS。Soft-NMS 不是直接删除高 IoU 的框,而是降低它们的置信度,这样能保留更多重叠目标。我在一个货架商品检测的项目里用了 Soft-NMS,召回率提升了 8 个百分点。
5. 常见问题与排查技巧实录
5.1 训练不收敛或 Loss 震荡
这是最常见的问题,原因可能有很多。首先检查数据标注有没有问题,比如坐标越界、类别 ID 错误、空标注文件。我遇到过一次,某个类别的标注文件里混入了中文逗号,导致解析失败,loss 一直不降。其次检查学习率是不是太大,可以试着降到 0.0001 看看。如果 loss 还是震荡,可能是 batch size 太小,梯度噪声太大,可以增大 batch size 或者用梯度累积。
还有一个隐蔽的原因:anchor 尺寸和数据集不匹配。如果 anchor 太大,小目标永远匹配不到正样本,loss 会卡在一个较高的值下不去。这时候重新聚类 anchor 通常能解决问题。
5.2 小目标漏检严重
小目标漏检是 YOLO v3 的经典问题。除了前面提到的多尺度预测和重新聚类 anchor,还有几个技巧可以尝试。第一,增大输入尺寸,比如从 416 提到 608,小目标在特征图上的像素数会翻倍。第二,在数据增强时多用小目标样本,或者用 mosaic 增强把四张图拼成一张,增加小目标的出现频率。第三,调整损失函数里小目标的权重,让网络更关注小框的回归误差。
我在一个无人机航拍检测的项目里,目标都是几十个像素的小车,用了上面三个技巧后,小目标 mAP 从 0.35 提升到 0.58。其中贡献最大的是增大输入尺寸,从 416 提到 832,虽然推理速度降了一半,但精度提升非常明显。
5.3 误检率高,背景被识别成目标
误检通常是因为负样本不够或者置信度阈值太低。首先可以增加负样本图片,也就是那些没有目标的背景图,让网络学会区分背景。其次可以提高置信度阈值,比如从 0.5 提到 0.7,但这样会降低召回率,需要权衡。还有一个技巧是调整ignore_thresh,把它调低,让更多次优框参与置信度损失计算,网络会对背景更敏感。
我遇到过一次特殊情况:模型把路灯识别成了行人。排查后发现,训练集里路灯和行人的标注框有大量重叠,网络学混了。解决办法是把这些重叠样本单独拿出来,重新标注,确保每个目标框只对应一个类别。
5.4 推理速度不达预期
推理速度慢的原因可能是输入尺寸太大、模型没量化、或者后处理太耗时。首先可以用torch.cuda.synchronize()测一下纯网络前向的时间,如果前向就慢,考虑换更小的输入尺寸或者用 TensorRT 加速。如果前向快但整体慢,那可能是 NMS 或者坐标解码拖了后腿,可以用 CUDA 实现的 NMS 或者把后处理放到 GPU 上。
我在 Jetson Nano 上部署 YOLO v3 时,416×416 输入只能跑到 8 FPS。后来用了 TensorRT 的 FP16 量化,速度提到 18 FPS,精度只掉了 1 个 mAP。再后来把输入降到 320×320,速度到了 25 FPS,满足了实时要求。所以边缘设备上部署,量化和输入尺寸调整是必须的。
5.5 常见问题速查表
| 问题现象 | 可能原因 | 排查方法 | 解决技巧 |
|---|---|---|---|
| Loss 不下降 | 学习率过大、标注错误、anchor 不匹配 | 检查标注文件、降低学习率、重新聚类 anchor | 用 Adam 替代 SGD 试跑短周期 |
| 小目标漏检 | 输入尺寸小、anchor 偏大、正样本少 | 统计小目标占比、可视化特征图 | 增大输入、重新聚类、mosaic 增强 |
| 误检率高 | 负样本不足、置信度阈值低 | 统计误检类别、可视化预测框 | 增加背景图、提高阈值、调低 ignore_thresh |
| 推理速度慢 | 输入大、未量化、后处理耗时 | 分别测前向和后处理时间 | TensorRT FP16、CUDA NMS、降输入尺寸 |
| 训练后期 mAP 下降 | 过拟合、学习率未衰减 | 看验证集 loss 曲线 | 加数据增强、加权重衰减、step decay |
6. 部署与优化的一些实战经验
6.1 模型导出与格式转换
训练完的 PyTorch 模型要部署到生产环境,通常需要转成 ONNX 或者 TensorRT。转 ONNX 的时候,要注意把exp、sigmoid这些算子替换成 ONNX 支持的标准算子。另外,YOLO v3 的输出是三个尺度的特征图,导出时要确保输出节点的名字和顺序正确,否则推理结果会错乱。
我一般会写一个简单的验证脚本,用同一张图分别跑 PyTorch 和 ONNX 模型,对比输出差异。如果差异在 1e-4 以内,说明转换没问题。如果差异很大,通常是某个算子转换错了,需要逐层排查。
6.2 量化与剪枝的取舍
量化是提升推理速度最直接的手段。FP16 量化几乎不损失精度,速度能提升 1.5 到 2 倍。INT8 量化速度更快,但需要校准数据集,精度可能掉 2 到 3 个 mAP。我在工业质检项目里用了 INT8 量化,因为产线环境对速度要求极高,精度掉一点可以接受,通过增加训练数据量补回来了一部分。
剪枝是另一个优化方向,但 YOLO v3 的剪枝比较复杂,因为残差连接和多尺度预测让通道之间的依赖关系很紧密。我试过用通道剪枝,剪掉 30% 的通道后,速度提升 40%,但 mAP 掉了 5 个点。后来发现,剪枝后需要 fine-tune 至少 20 个 epoch 才能恢复大部分精度。所以剪枝适合对精度要求不那么苛刻的场景。
6.3 多尺度推理的工程实现
YOLO v3 支持多尺度推理,也就是同一张图用不同尺寸跑多次,然后融合结果。这个技巧在小目标检测上很有效,但计算量会成倍增加。我的做法是:先用小尺寸跑一遍,得到粗略的检测结果,然后对置信度低的区域用大尺寸再跑一遍,最后用 NMS 融合。这样能在速度和精度之间取得平衡。
实现的时候要注意,不同尺度的输出坐标要映射回原图,映射公式是x_original = x_feature * stride。如果原图不是正方形,还要考虑 padding 的影响。我一般会把原图 resize 到正方形,保持长宽比,空白处用灰色填充,这样坐标映射最简单。
6.4 实际项目中的调参心得
最后分享几个我在实际项目里总结的调参心得。第一,学习率不要一上来就设很大,先用 0.0001 跑几个 epoch,看看 loss 是不是稳定下降,再逐步调大。第二,数据增强的强度要匹配数据集大小,小数据集可以增强猛一点,大数据集增强太猛反而会拖慢收敛。第三,验证集要足够大,至少占总数据的 10%,否则 mAP 波动会很大,无法判断模型好坏。第四,训练日志要记录详细,包括每个类别的 AP、loss 各分量的值、学习率变化,这样出问题的时候才能快速定位。
我在一个项目里曾经因为验证集太小(只有 50 张图),mAP 在 0.6 到 0.75 之间来回跳,完全不知道模型到底收敛没有。后来把验证集扩到 500 张,mAP 曲线才稳定下来,最终模型在测试集上的表现也和验证集一致。所以数据划分这件事,再怎么强调都不为过。
这个内容后续还可以这样扩展:如果你对 YOLO v3 的某个模块特别感兴趣,比如 FPN 结构、损失函数的梯度推导、或者 TensorRT 的插件开发,都可以单独拿出来做深入分析。我自己就在 FPN 的上采样方式上做过不少实验,最近邻插值和双线性插值的差异、上采样后是相加还是拼接,这些细节对最终精度都有影响,值得单独写一篇来聊。