1. 为什么YOLOv4的cfg文件值得单独拿出来聊
搞过目标检测的人都知道,YOLOv4这个模型在工业界和学术界都算是一个分水岭式的存在。它不像后来的那些端到端模型那样把配置全部藏在代码里,而是把大量核心参数暴露在一个.cfg文本文件中。这个设计看起来有点“复古”,但实际用起来你会发现,它给了你极大的控制权——你可以通过改几行文本,就调整整个网络的深度、宽度、特征融合方式,甚至推理时的后处理逻辑。
我见过太多人拿到YOLOv4的cfg文件之后,直接改几个classes和filters就开跑,结果训练出来的模型要么不收敛,要么精度差得离谱。问题就出在:cfg文件里的参数不是孤立的,它们之间存在强耦合关系。比如你改了classes,就必须同步改所有检测头前面的filters;你调了width和height,就得重新考虑anchors的尺度分布;你动了max_batches,学习率调度策略也得跟着变。
这篇文章就是要把这些耦合关系一条条拆开讲清楚。我会从cfg文件的整体结构入手,逐块解释每个参数的实际含义和调整逻辑,然后给出完整的训练流程和参数配置方案,最后分享一些我在实际项目中踩过的坑和总结出来的排查技巧。无论你是刚接触YOLOv4的新手,还是已经跑过几轮训练但效果不理想的老手,应该都能从中找到有用的东西。
2. cfg文件整体结构与核心模块拆解
2.1 从[net]段开始:全局参数的锚定作用
[net]段是整个cfg文件的全局控制中心,它定义了输入尺寸、训练策略、数据增强方式等顶层参数。很多人容易忽略的是,这里的每一个参数都会向下影响到后续所有层的实际行为。
先看最基础的几个:
[net] batch=64 subdivisions=16 width=608 height=608 channels=3batch和subdivisions的关系是:实际每次送入GPU的样本数是batch/subdivisions。比如上面这个配置,实际每次前向传播只处理4张图。这个设计是为了在显存有限的情况下模拟大批量训练的效果——梯度会累积subdivisions次后再统一更新。我个人的经验是,如果你用的是单卡12GB显存,batch=64, subdivisions=16是一个比较稳妥的起点;如果显存更小,可以调到batch=32, subdivisions=16,但不要再往下压了,否则BN层的统计量会不稳定。
width和height必须是32的倍数,这是因为YOLOv4的下采样总倍数是32。如果你填了608,经过5次下采样后特征图尺寸依次是304、152、76、38、19,都是整数。如果你填了600,那就会出现小数,网络会直接报错。这一点在自定义输入尺寸时一定要记住。
接下来是训练相关的参数:
learning_rate=0.0013 burn_in=1000 max_batches=500500 policy=steps steps=400000,450000 scales=.1,.1learning_rate的设定和max_batches是联动的。YOLOv4官方推荐的是max_batches = classes * 2000,但最少不低于6000。比如你有3个类,那就是6000;有20个类,那就是40000。steps是两个衰减点,分别设在max_batches的80%和90%位置,每次衰减为原来的0.1倍。这个策略在大多数数据集上都能work,但如果你用的是小数据集(比如几千张图),我建议把max_batches降到8000到10000左右,steps相应调整到6400和7200,否则训练到后面会严重过拟合。
还有一个容易被忽视的参数是burn_in。它控制的是前1000次迭代内学习率从0线性上升到learning_rate的过程。这个机制在训练初期非常关键,可以防止随机初始化权重被过大的梯度直接带偏。如果你发现训练loss在前几百次迭代就爆炸,大概率就是burn_in设得太小或者learning_rate设得太高。
2.2 [convolutional]段:卷积层的参数细节
YOLOv4的骨干网络用了大量卷积层,每个[convolutional]段控制一个卷积块的行为。这里面的参数看起来简单,但组合起来有很多讲究。
[convolutional] batch_normalize=1 filters=32 size=3 stride=1 pad=1 activation=mishbatch_normalize=1表示这个卷积层后面跟着BN层。在YOLOv4中,除了最后的检测头卷积,几乎所有卷积层都开启了BN。BN的作用是加速收敛和防止过拟合,但在推理阶段会被融合进卷积权重中,所以不会增加额外计算量。
filters是输出通道数,size是卷积核大小,stride是步长,pad=1表示padding方式为same(输出尺寸不变)。这里有一个经验法则:当size=3时,pad=1;当size=1时,pad=0。如果你写错了,网络虽然能跑,但特征图尺寸会意外变化,导致后续的route和shortcut层维度不匹配。
activation在YOLOv4中主要是mish和leaky两种。Mish激活函数是YOLOv4的一个亮点,它在负值区域不是完全截断,而是有一个平滑的过渡,实测在深层网络中比Leaky ReLU的梯度流动更顺畅。但Mish的计算量比Leaky ReLU大不少,如果你在边缘设备上部署,可以考虑把骨干网络中的Mish换成Leaky,精度损失通常在1%以内,但推理速度能提升15%左右。
2.3 [route]和[shortcut]段:特征融合的关键
YOLOv4之所以能在速度和精度之间取得很好的平衡,很大程度上归功于它的特征融合设计。[route]层负责将不同尺度的特征图拼接或相加,[shortcut]层负责残差连接。
[route] layers=-1,-3这个配置表示将当前层的前一层和前第三层在通道维度上拼接。注意是拼接不是相加,所以输出通道数是两层通道数之和。这里有一个常见的坑:如果你在route层之前改了某一层的filters,那route之后的卷积层filters也必须同步修改,否则通道数对不上。
[shortcut] from=-3 activation=linearshortcut层实现的是残差相加,要求输入和输出的通道数完全一致。在CSPDarknet中,shortcut被大量使用来构建跨阶段残差连接。如果你把某个shortcut的from参数改错了,网络会直接报维度不匹配的错误。
2.4 [yolo]段:检测头的核心参数
[yolo]段是YOLOv4最核心的部分,它定义了锚框、损失函数权重、数据增强策略等。
[yolo] mask=0,1,2 anchors=12,16, 19,36, 40,28, 36,75, 76,55, 72,146, 142,110, 192,243, 459,401 classes=80 num=9 jitter=.3 ignore_thresh=.7 truth_thresh=1 random=1 scale_x_y=1.05anchors是9组预定义的锚框尺寸,mask决定当前检测头使用哪三组。YOLOv4有三个检测头,分别对应19x19、38x38、76x76的特征图,每个头使用不同尺度的锚框。如果你训练自己的数据集,第一步就是用k-means聚类重新计算anchors。我通常会用gen_anchors.py脚本跑一遍,把得到的9组值替换掉默认值。这里有一个细节:聚类时的num_clusters要设成9,但如果你用的是更小的输入尺寸(比如416),锚框的绝对尺寸也要按比例缩小。
ignore_thresh=.7这个参数控制的是负样本的判定阈值。当某个预测框与真实框的IoU大于0.7但又不是最佳匹配时,它会被忽略,不参与损失计算。这个设计是为了防止那些“模棱两可”的预测框产生过大的梯度。如果你发现模型对密集小目标检测效果不好,可以适当降低这个值到0.5左右,让更多预测框参与训练。
scale_x_y=1.05是YOLOv4的一个小改进,它通过缩放sigmoid输出的范围来让中心点坐标的预测更稳定。这个值不要改太大,1.05到1.1之间比较合适,改大了会导致中心点偏移。
3. 训练流程与参数配置实战
3.1 数据准备与配置文件生成
在开始训练之前,你需要准备好三样东西:图像文件、标注文件(通常是YOLO格式的txt)、以及数据配置文件。
标注文件的格式是每行一个目标:class_id center_x center_y width height,所有坐标都归一化到0到1之间。这里有一个新手常犯的错误:把像素坐标直接写进去了。记住一定要除以图像宽高做归一化。
数据配置文件obj.data的内容如下:
classes= 3 train = data/train.txt valid = data/valid.txt names = data/obj.names backup = backup/train.txt和valid.txt里每行是一张图像的绝对路径。obj.names里每行是一个类别名称,顺序要和标注文件里的class_id对应。
然后就是修改cfg文件。假设你有3个类,需要改三个地方:
第一,所有[yolo]段前面的[convolutional]层的filters改成(classes + 5) * 3 = 24。注意是每个yolo段前面那个卷积层,不是所有卷积层。
第二,所有[yolo]段的classes改成3。
第三,如果你重新聚类了anchors,把anchors和mask替换掉。
我建议在改之前先备份一份原始cfg,改完之后用darknet的partial命令加载一下,看看有没有报错。这个命令会解析cfg文件并打印网络结构,如果有维度不匹配的问题会直接提示。
3.2 关键参数的计算与选择
训练YOLOv4时,有几个参数需要根据你的数据集规模和硬件条件来计算,不能直接抄默认值。
max_batches的计算:官方公式是classes * 2000,但不低于6000。如果你有3个类,那就是6000;有10个类,那就是20000。但如果你只有几百张图,我建议把max_batches控制在4000到6000之间,同时把steps设在max_batches的80%和90%位置。比如max_batches=6000,那steps=4800,5400。
learning_rate的选择:默认的0.0013适用于batch=64, subdivisions=16的情况。如果你把batch调小了,学习率也要按比例缩小。比如batch=32,学习率可以设成0.00065。但不要低于0.0001,否则收敛会非常慢。
width和height的选择:如果你的目标以小物体为主,建议用608x608;如果以中大物体为主,416x416就够用了。输入尺寸越大,显存占用和推理时间都会显著增加。我实测下来,608比416的mAP大概高2到3个点,但推理速度慢了一倍。
anchors的聚类:用k-means对训练集的标注框做聚类,num_clusters=9。聚类时的距离度量用1-IoU比欧式距离更合理。得到9组锚框后,按面积从小到大排序,前3组给19x19的检测头,中间3组给38x38,后3组给76x76。这个分配顺序不能乱,因为大特征图负责检测小物体,需要小锚框。
3.3 启动训练与监控指标
启动训练的命令很简单:
./darknet detector train data/obj.data cfg/yolov4-custom.cfg yolov4.conv.137 -mapyolov4.conv.137是预训练权重,只包含骨干网络的前137层。用预训练权重可以显著加快收敛速度,尤其是在小数据集上。如果你没有这个文件,也可以从零开始训练,但需要更多的迭代次数。
训练过程中,终端会输出每一轮的loss和mAP。这里有几个关键指标需要关注:
avg loss:平均损失,应该随着训练逐渐下降。如果loss震荡很厉害,说明学习率太高或者batch太小。class loss:分类损失,如果这个值一直不降,可能是类别标注有问题。objectness loss:目标置信度损失,如果这个值很高,说明模型很难区分前景和背景。mAP@0.5:IoU阈值为0.5时的平均精度,这是最直观的指标。
我通常会在训练到max_batches的50%时,用验证集跑一次评估。如果mAP已经达到预期,可以提前停止;如果还差得远,可能需要调整学习率或者检查数据质量。
3.4 学习率调度与burn_in的实际影响
学习率调度策略对最终精度的影响非常大。YOLOv4默认用的是step decay,在steps指定的迭代次数处将学习率乘以scales。这个策略简单有效,但有一个问题:衰减点之后学习率骤降,模型可能会陷入局部最优。
我在实际项目中试过用cosine decay替代step decay,具体做法是把policy改成cosine,然后去掉steps和scales。Cosine decay让学习率平滑下降,在训练后期能更细致地搜索参数空间。实测下来,cosine decay比step decay的mAP高0.5到1个点,但训练时间会稍微长一点。
burn_in的作用是在训练初期让学习率从0线性上升到设定值。这个机制对于随机初始化的层(比如检测头)非常重要。如果你用的是预训练权重,骨干网络已经收敛得不错了,burn_in可以设小一点,比如500;如果是从零开始训练,建议设到1000甚至2000。
4. 常见问题与排查技巧实录
4.1 训练不收敛或loss爆炸
这是最常见的问题,通常有以下几个原因:
学习率太高:如果你用的是默认的0.0013但batch只有16,那学习率相对就太高了。解决办法是按比例缩小学习率,或者增大batch。
数据标注有问题:我遇到过好几次,标注文件里的坐标没有归一化,导致loss直接飞到nan。检查方法是随机抽几张图,用可视化工具把标注框画出来,看看是否和实际目标对齐。
anchors不匹配:如果你没有重新聚类anchors,默认的anchors可能和你的目标尺寸差异很大。这会导致正样本匹配数量极少,模型学不到东西。解决办法是用k-means重新计算。
BN层统计量不稳定:如果subdivisions设得太大,实际batch size太小,BN层的均值和方差估计会很不准确。建议实际batch size不要低于4。
4.2 mAP上不去或过拟合
训练到后面mAP不升反降,这是典型的过拟合。解决办法有:
- 增加数据增强:YOLOv4默认开启了mosaic、cutmix、翻转、缩放等增强,你可以进一步调大
jitter值,或者加入随机旋转。 - 减小模型复杂度:把
width和height调小,或者减少某些层的filters。 - 提前停止:在验证集mAP开始下降时停止训练。
- 增加正则化:调大
ignore_thresh,或者加入dropout层。
如果mAP从一开始就很低,那可能是欠拟合。检查一下max_batches是否够大,学习率是否太低,或者预训练权重是否加载成功。
4.3 显存不足与batch调整
显存不足的报错通常是CUDA out of memory。解决办法有:
- 减小
batch和subdivisions,保持实际batch size不变。 - 减小
width和height。 - 把
random=1改成random=0,关闭多尺度训练。 - 使用混合精度训练(需要改代码)。
我个人的经验是,12GB显存跑608x608、batch=64、subdivisions=16是刚好够用的。如果你要跑更大的输入尺寸,建议上多卡或者用梯度累积。
4.4 检测结果框重叠或漏检
如果推理时发现大量重叠框,说明NMS的IoU阈值设得太高。YOLOv4默认的NMS阈值是0.45,你可以在推理命令里用-thresh参数调整。如果漏检严重,可能是confidence threshold设得太高,试着降到0.2左右看看。
还有一个容易被忽视的问题是scale_x_y参数。如果你把它改得太大,中心点预测会变得过于激进,导致框的位置偏移。建议保持在1.05到1.1之间。
4.5 常见问题速查表
| 问题现象 | 可能原因 | 排查方法 | 解决方案 |
|---|---|---|---|
| loss为nan | 学习率过高、标注未归一化 | 检查前100轮loss曲线 | 降低学习率、重新检查标注 |
| mAP始终为0 | anchors不匹配、类别数配置错误 | 检查cfg中classes和filters | 重新聚类anchors、修正filters |
| 显存溢出 | batch或输入尺寸过大 | 查看nvidia-smi | 减小batch、降低输入尺寸 |
| 训练后期过拟合 | max_batches过大、数据量不足 | 对比训练集和验证集loss | 提前停止、增加数据增强 |
| 检测框大量重叠 | NMS阈值过高 | 可视化推理结果 | 降低NMS阈值到0.4左右 |
| 小目标漏检严重 | 输入尺寸太小、anchors偏大 | 分析小目标占比 | 增大输入尺寸、重新聚类小锚框 |
5. 参数调优的进阶思路与个人经验
5.1 从cfg层面做消融实验
如果你想系统性地提升模型精度,可以在cfg层面做消融实验。比如:
- 把所有的
mish换成leaky,看精度和速度的变化。 - 去掉某些
shortcut层,看模型是否还能收敛。 - 调整
route层的拼接方式,比如从拼接改成相加。 - 修改
scale_x_y的值,看中心点预测的稳定性。
我做过一组实验,把CSPDarknet中的部分Mish换成Leaky,在COCO数据集上mAP只掉了0.8个点,但推理速度提升了18%。这个trade-off在很多实际场景中是值得的。
5.2 多尺度训练的实际效果
random=1开启多尺度训练后,每10个batch会随机改变输入尺寸。这个策略能显著提升模型对不同尺度目标的适应能力。但有一个副作用:训练时间会变长,因为每次改变尺寸都需要重新分配显存。
我的建议是,如果你的数据集里目标尺度变化很大,一定要开启多尺度训练。如果目标尺度比较单一,可以关掉它来节省时间。
5.3 迁移学习中的参数冻结策略
如果你用的是预训练权重,可以在训练初期冻结骨干网络,只训练检测头。具体做法是把骨干网络部分的learning_rate设成0,或者用stopbackward层截断梯度。等检测头收敛后,再解冻骨干网络做微调。
这个策略在小数据集上特别有效,可以防止预训练权重被少量数据带偏。我通常会在前1000次迭代冻结骨干,然后解冻再训练到max_batches。
5.4 cfg文件的版本管理
最后分享一个容易被忽视但很重要的点:cfg文件一定要做版本管理。每次修改参数后,保存一份带注释的副本,记录改了什么、为什么改、效果如何。我见过太多人改来改去最后忘了哪个版本效果最好,只能从头再来。
我自己的做法是用git管理cfg文件,每次实验开一个分支,commit message里写清楚参数变化和对应的mAP。这样即使过了几个月,也能快速回溯到最佳配置。
训练YOLOv4这件事,说到底就是和cfg文件里的几百个参数打交道。理解每个参数的含义和它们之间的耦合关系,比盲目调参要高效得多。希望这篇内容能帮你少走一些弯路,把模型精度真正提上去。