我记起来做目标检测那阵子,最焦头烂额的阶段不是调参,而是数据标到凌晨三点发现标错了一百多张图。后来项目上了 YOLOv5,流程图和结构图在我脑子里过了无数遍,踩过的坑也一个没落下。这篇就跟大家从原理拆到实战,把 YOLOv5 从网络结构、训练配置到部署落地的完整链路说透,特别是那些文档里不写、但实际一跑就翻车的细节。
YOLOv5 是 Ultralytics 在 2020 年 6 月开源的目标检测框架,以速度快、精度高、部署友好著称。它解决的痛点是:多数科研模型停留在“精度不错但根本跑不动”的阶段,而 YOLOv5 在普通 GPU 上就能训练,在 CPU 甚至嵌入式设备上也能推理,几乎成了工业级目标检测的事实标准。这篇适合谁看?刚入门目标检测、想训练自己数据集的学生,或者在 Jeston Nano 这类边缘设备上做落地开发的工程师,都会从这里得到一张可直接照抄的路线图。
1. 整体架构设计思路:从输入端到输出端的全链路拆解
1.1 一条数据怎么在 YOLOv5 里走完全程
理解 YOLOv5 最快的方式,是跟着一条图片数据从头走到尾。输入一张 640x640 的图片,第一步不是进网络,而是先进一个数据增强流水线。这里有个很多人不知道的细节:Mosaic 增强会把 4 张图拼成一张,相当于把 batch size 翻了 4 倍,这也是 YOLOv5 在较小 batch 下依然能训出较好效果的底气之一。增强之后,图片会按比例缩放,剩余部分用灰度值 114 填充,而不是简单拉伸,这一步对保持目标长宽比例很关键。
数据进网络后,先经过 Backbone(主干网络)提取特征。YOLOv5s 的 Backbone 核心是 Focus 模块加 CSPNet 结构。Focus 模块做的操作很朴素——把图像的相邻像素切开来重组,比如把 640x640x3 变成 320x320x12,再用 32 个卷积核压成 320x320x32。这么做不是为了省参数,而是为了在不下采样的前提下扩大感受野,信息损失比直接 stride 卷积更小。CSPNet 则是把特征图分成两条路,一条走主卷积,一条直接过 shortcut,最后再拼起来。这种设计让梯度反向传播时有一条"高速公路",避免深层网络梯度消失,同时省内存。
特征提取完成后进入 Neck 部分,即 PANet 结构。这里做的事情是两个方向的特征融合:自顶向下把高层语义信息传给低层,让大目标更清晰;自底向上把低层纹理信息传给高层,让小目标也能被召回。这就是"FPN + PAN"双塔结构的设计逻辑。最后,融合后的特征被送到 Head 部分做预测。YOLOv5 的 Head 在不同尺寸的特征图上各做一次预测,分别是 80x80 的小目标分支、40x40 的中目标分支和 20x80 的大目标分支,每个分支上每个格子会预设 3 个 anchor。
1.2 为什么 YOLOv5 不用 Anchor-Free,反而继续保留 Anchor
YOLOv5 从一开始就选了 Anchor-Based 路线,这和当时很多号称"免锚"的模型形成鲜明对比。Anchor 可以理解为"预设的参考框",网络要做的事情不是直接预测目标的宽高,而是预测相对预设框的偏移量和缩放系数。打个比方,如果一个人站在你面前 10 米处,Anchor-Based 策略是"先猜他大概在 8~12 米位置,再去修正",Anchor-Free 则是"完全没有参考点,直接预测距离"。
有参考物其实是一种先验约束。COCO 数据集的标注框经过聚类之后,能得到比较集中的宽高模式,比如"人"往往是高大于宽,"汽车"往往接近方形。YOLOv5 在训练前会先对自定义数据集的标注框做 K-Means 聚类,重新计算这 3x3=9 个锚框的尺寸。这就是为什么官方预训练权重不能直接在自己的数据集上生效的原因之一——锚框不匹配,收敛很慢。
保留 Anchor 的另一个现实理由是部署友好。TensorRT、OpenVINO 这些推理引擎对 Anchor-Based 的算子优化得比较成熟,而一些 Anchor-Free 模型需要额外实现 DCN 等复杂算子,在 Jetson 这类设备上优化难度很高。这在工业落地上是个不能忽视的隐性成本。
2. 核心网络机制解析:你必须吃透的 4 个关键模块
2.1 CSPDarknet:YOLOv5 的主干核心为何能省内存
Backbone 用的 CSPDarknet 是整张网络最"省钱"的设计。它把输入特征图在通道维度上分成 two parts,一部分经过若干个 Bottleneck 堆叠,另一部分直接走一个 1x1 卷积保持通道数。最后将两条支路拼接起来再经过一个 1x1 卷积调整通道数。
这里有个极其容易理解错的细节:CSP 结构省内存不是因为"分叉"这个动作本身,而是因为网络最深处的特征只需要在"一条支路"上计算。另一条跳接支路不经过 Bottleneck,所以深层特征的通道数不会在每一层都被完整复制一遍。这直接降低了显存占用,也减少了梯度反传时的计算量。官方数据是 CSP 结构比普通 ResNet 风格的 Backbone 在同等精度下能缩减约 20%~30% 的浮点运算量。
实际测试中,同样的 2080Ti 显卡,用 YOLOv5s 训练自己数据集,显存占用能控制在 8GB 以内,而换用同等规模的 ResNet 系列做检测头,至少要多占 2~3GB。这个差距在服务器上不明显,但换到 Jetson Nano 或者树莓派上就是能用和不能用的区别。
2.2 SPPF:为什么能把 SPP 的池化层全部串起来
YOLOv5 在主干网络的最后接的是 SPPF 模块,和 YOLOv4 时代的 SPP 不同,它把三个 MaxPool 层全部串联起来,而不是并联。这里可能有人会有疑问:串联和并联的输出不是一样吗?如果你仔细算一下,一个 5x5 的 MaxPool 之后再做 5x5 MaxPool,等效感受野就是 9x9,再做一次就是 13x13。所以串联结构可以得到 5x5、9x9、13x13 三种不同尺度的特征,和并联结构表达能力一样,但计算量少了很多。
这个模块的作用是:通过多尺度池化,把主干输出的高维特征映射到多个感受野尺度上,从而增强网络对多尺寸目标的适应性。在后来的 YOLOv8 中,这个模块被保留了下来,也说明它的设计确实经得住检验。实际工程中我一般不建议随手改 SPPF 的池化核大小,因为太小会让深层特征失去全局信息,太大会显著增加计算量,默认 5 在绝大多数场景下是最平衡的配置。
2.3 Neck 的 CSP 设计何必这么重
PANet 是 YOLOv5 的 Neck 主体,它由两个 CSP 模块构成:一个做自顶向下的特征金字塔融合,另一个做自底向上的路径聚合。自顶向下的逻辑很好理解——深层特征分辨率低,但语义信息强,把它上采样后与浅层特征拼接,就能让浅层也"懂"目标是什么类别。自底向上的逻辑则相反:浅层分辨率高,空间信息精确,把它向后传播,能让高层分支在定位时不至于太"糊涂"。
这里有个工程上非常典型的坑:很多人在自定义数据集时,发现模型对小型目标的检测效果特别差,就在 Neck 里拼命堆小目标分支,比如把 160x160 的特征图也加进来。但这样做的直接后果是显存暴涨、训练速度骤降,而精度提升往往只有 1~2 个点。其实更值得先检查的是数据集的标注质量和小目标样本数量,而不是网络的宽度。
YOLOv5 的 Neck 在设计上倾向于轻量高效:它用的 CSP 结构比 Backbone 的浅、通道数更少,这保证了特征融合的代价不会超过主干提取的代价。记住一个原则:特征融合网络的宽度不应超过主干网络,否则会出现"融合处理不过来、特征被压缩"的情况。
2.4 Head 的预测逻辑:如何从 3 个分支得到最终结果
YOLOv5 的 Detection Head 在每个尺度的特征图上做三件事:分类、回归框坐标、回归置信度。以 COCO 80 类为例,每个锚框的输出维度是 5(坐标)+ 80(类别)+ 1(置信度),乘上 3 个锚框,每个格子的输出维度是 258。
推理阶段,网络输出的是带 anchor 的原始张量,需要做一次解码。坐标解码公式是:
bx = 2 * sigmoid(tx) - 0.5 + cx 表示中心点横坐标的偏移;by 同理。这里用 2 * sigmoid 而不是直接 sigmoid,是为了让中心点可以"跑出"当前格子一小段范围,增强靠近网格边界的预测能力。
宽高解码公式是: bw = pw * (2 * sigmoid(tw))^2,bh 同理。这里用平方项,是为了让小目标的宽高变化更加灵敏,不至于在数值上被大目标"淹没"。
做个简单计算你就明白了:当 tw=0 时,2*sigmoid(0)=1,此时 bw 正好等于预设 anchor 的宽度 pw,说明网络在初始状态下预测的就是锚框本身。这保证了训练初期 loss 的数值不会瞬间爆炸。解码之后,还要做 NMS(非极大值抑制),把同一目标上的多个候选框合并成一个。这一步是整条链路里最耗 CPU 的部分,部署时通常会改用 TensorRT 的 EfficientNMS 插件来加速。
3. 训练自己的数据集:完整流程及超参数详解
3.1 从零开始的数据准备与标注
训练 YOLOv5 的第一步是准备数据集。如果你要做的是水果识别,先去拍不同角度、不同光照、不同成熟度的水果照片,至少每类要 300 张以上;如果你要做车牌识别,那么蓝牌、绿牌、黄牌,单双层都要覆盖到。数据集的丰富度决定了模型上限,网络设计和训练技巧只是在逼近这个上限。
标注工具方面,个人最推荐 LabelImg 或 X-anyLabeling。前者老牌稳定,后者支持自动标注和交叉标注,配合 YOLOv5 的预训练模型做人工复核,可以节省一半以上的标注时间。标注结果推荐直接导出为 YOLO 格式,也就是每张图片对应一个同名的 txt 文件,每一行表示一个目标,格式是:
class_id center_x center_y width height
注意这里全部是归一化到 0~1 之间的相对坐标,不是像素坐标。我自己早期就吃过一次亏:用 LabelImg 导出了 Pascal VOC 格式,没转成 YOLO 格式,结果训练时反复报 no labels found。现在我会用 Ultralytics 附带的转换脚本统一处理。
数据集目录结构建议如下:
dataset/ ├── images/ │ ├── train/ │ └── val/ └── labels/ ├── train/ └── val/配套的 data yaml 文件内容如下:
train: dataset/images/train val: dataset/images/val nc: 2 names: ['apple', 'banana']一个我在实操中总结的经验:验证集的划分不要用随机抽样,而是按采集场景来分。比如同一棵树上的苹果,一部分进训练集一部分进验证集,这样验证结果会虚高。更严格的做法是整棵树、整块地的照片只进训练集或只进验证集,这样评估出来的泛化能力才真实。
3.2 超参数配置:lr、batch size 到底怎么调
YOLOv5 的超参数配置集中在 hyp.scratch-low.yaml 文件里,默认是 COCO 上炼丹出来的底座。很多教程直接告诉你"用默认超参就行",但实操中往往不是那么回事。先说最关键的两个参数:学习率和 batch size。
YOLOv5 用的是 SGD 优化器,初始学习率 lr0 默认 0.01。这个值在 COCO 这种超大、超复杂的数据集上是合理的,但在自定义的小数据集上,0.01 会导致 loss 震荡甚至发散。我自己的经验是:当训练集少于 5000 张图时,把 lr0 调到 0.001~0.005 之间,收敛明显更稳。很多人一上来就复现"官方最佳精度",却忽略了大模型在小数据上的不适应性。
batch size 的选择受限于显存,但很多新手的误区是可劲往上加。YOLOv5 的 loss 组成有三块:box_loss、cls_loss、obj_loss。batch 太大时,如果数据集里小目标多,obj_loss 反而会不稳定。这里提供一个我自己常用的方案:batch size 设为 16 起步,显存不够就降到 8,此时学习率也要等比降低,即 lr = 0.01 * (batch / 64)。这个线性缩放规则在 YOLOv5 的 SGD 优化器下基本好用。
3.3 训练命令与过程监控
训练命令可以直接从官方仓库 Copy 改参数,我自己常用的命令是:
python train.py \ --data data/custom.yaml \ --weights yolov5s.pt \ --img 640 \ --batch 16 \ --epochs 100 \ --cache \ --device 0这里 --cache 表示把图片预加载到内存,如果数据集总量不超过 10GB,推荐开启,训练速度能提升 30% 以上。如果显存紧张,可以在 --cache 后面改成 --cache disk,只把图片缓存到磁盘,也能减少 I/O 阻塞。
训练过程中,我会重点盯三个曲线:
- train/box_loss 和 val/box_loss:两者都下降则正常,若出现 val loss 回升而 train loss 继续下降,说明过拟合了。
- metrics/mAP_0.5 和 mAP_0.5:0.95:前者是粗粒度指标,IOU 大于 0.5 就算对;后者是细粒度指标,要 0.5 到 0.95 的平均,更考验定位精度。
- 出现 WARNING:Overfitting 或者 early stopping 建议,就得考虑加数据增强、加 dropout 或提前停止。
训练结束后,weights/best.pt 就是最推荐的模型文件,它保存的是在验证集上 mAP 最高的权重,而不是最后一个 epoch 的权重。这一点说过无数次但总有人踩:直接用 last.pt 做部署,结果精度不如预期,一查才知道 best.pt 早就不更新了。
4. 部署落地:从 PyTorch 到 TensorRT 的完整转换链路
4.1 导出为 ONNX 与 TensorRT 引擎
YOLOv5 训练完的模型是 PyTorch 格式的 .pt 文件,但真实项目部署几乎不会直接用 PyTorch 跑推理。最主流的部署路线是 PyTorch → ONNX → TensorRT(NVIDIA 平台)或 OpenVINO(Intel 平台)。
导出 ONNX 的命令很简单:
python export.py \ --weights best.pt \ --include onnx \ --opset 12 \ --dynamic这里 --dynamic 表示允许动态输入尺寸,但如果考虑部署稳定性,我建议直接固定输入尺寸为 640x640,性能更优且不容易出奇怪的尺寸不匹配问题。值得一提的是,在导出时 YOLOv5 会自动把 NMS 模块剥离,ONNX 模型只负责前向推理,NMS 留给部署框架实现。
TensorRT 的转换推荐直接用 trtexec 工具,命令如下:
trtexec --onnx=best.onnx \ --saveEngine=best.trt \ --fp16 \ --minShapes=images:1x3x640x640 \ --optShapes=images:1x3x640x640 \ --maxShapes=images:1x3x640x6404.2 Jetson Nano 上的部署实测:算力不够,优化来凑
Jetson Nano 只有 128 个 CUDA 核心,算力约 0.5 TFLOPS FP16,和桌面显卡完全不在一个量级。在这上面跑 YOLOv5s 原版 FP32,帧率大概只有 3~5 FPS,基本不可用。但做两步优化之后,可以稳定跑到 15~20 FPS。
第一步是转换成 TensorRT FP16 引擎,这一步大概能带来 2 倍提速。第二步是缩小输入尺寸,从 640x640 降到 416x416,虽然 mAP 会掉 1~2 个点,但推理速度能再翻一翻。如果你做的是车牌识别,车牌本身在画面里占比较大,缩到 416 影响不大。
还有一招是装 JetPack 4.6+ 后,使用 TensorRT 8.2 的 EfficientNMS 插件替换普通 NMS。这个插件把 NMS 直接编译进推理引擎,避免 CPU 和 GPU 之间频繁拷贝数据。实测这一项就能再省 2~4ms。最终单帧延迟可以控制在 50ms 以内,对于车牌识别、简单的物料分拣等应用场景,这个性能完全够用。
4.3 边端部署的内存与显存优化细节
在 Jetson 这类嵌入式设备上,显存优化比速度优化更需要优先考虑。TensorRT 默认会用固定显存池,如果和图形界面共用显存,可能会出现 Cuda Error: out of memory。解决方法是启动时加环境变量:
export CUDA_MODULE_LOADING=LAZY sudo nvpmodel -m 0另外,YOLOv5 的推理代码里有个容易忽略的 trick:把 cv2 读取的图像直接转成 RGB 再归一化,这一步如果用 numpy 实现会占 CPU 和内存。更推荐用 CUDA 上的 tensor 操作一次性完成,写法是:
img = torch.from_numpy(img).permute(2, 0, 1).float() img = img / 255.0 img = img.unsqueeze(0).to(device)这样省去了多次 numpy 拷贝,在小内存设备上非常见效。实测在 Jetson Nano 2GB 版本上,这么改之后内存占用能降低 15% 左右。
5. 实战场景扩展:水果识别与车牌识别的落地差异
5.1 基于 YOLOv5 的水果识别:数据增强优先于网络调参
水果识别这类任务,核心难点不在网络结构,而在数据多样性。YOLOv5 官方给的预训练权重是在 COCO 上训练的,其中已经包含了水果类别,迁移到自己的水果数据集时,即便只有几百张图,也能很快收敛到不错的效果。这背后是迁移学习的思维:网络低层已经学会了普遍适用的边缘、纹理、颜色特征,只需要微调高层语义映射。
在水果识别里,最有用的数据增强是 HSV 随机扰动,即随机调整色调、饱和度、明度。因为水果颜色在不同光照下差异极大,模型要学的是"颜色不变性",而不是死记硬背某种固定 RGB 值。实操中我会把 hsv_h 设为 0.02,hsv_s 设为 0.7,hsv_v 设为 0.5,效果立竿见影。
另一个关键点是叶片遮挡。果树上的水果往往被叶子挡掉一截,这会导致标注框里混入大量背景。针对这个,可以给标签加一点"不完全可见"类,或者用随机擦除增强模拟遮挡,YOLOv5 的 hyp 文件里没有直接对应项,需要自己在 dataloader 里改。
5.2 基于 YOLOv5 的车牌识别:检测与识别如何无缝配合
车牌识别项目一般分两段:先用 YOLOv5 把车牌区域从整图中框出来,再用 OCR 模型(如 LPRNet、PaddleOCR)做字符识别。这里检测精度的高低直接决定 OCR 的输入质量,所以比水果识别更依赖检测模型的稳定性。
车牌检测的一个独特难点是车牌尺寸在画面中差异极大。近处大车牌的像素宽度可能超过 400,远处小车牌可能只有 30 像素。应对方法有几个:一是用 YOLOv5m 或 YOLOv5l 而不是 s,因为大模型在极小目标上表现更好;二是保证训练集中包含多个距离尺度的样本;三是训练时开启多尺度训练,YOLOv5 默认会在每个 epoch 随机缩放输入尺寸,这一步能显著增强尺度适应性。
在部署上,车牌识别通常需要同时跑检测和 OCR 两个模型,Jetson Nano 上二合一性能会很紧张。我的做法是用 TensorRT 把检测模型和 OCR 模型都转成 FP16 引擎,然后做两阶段流水线处理,检测模型刚一帧输出,OCR 模型立刻吃进上一帧的车牌区域,通过双线程隐藏延迟,最终能做到整体端到端 15 FPS 以上,能满足停车场出入口这类低速场景的需求。
6. 常见问题与排查技巧实录
6.1 训练时提示 No labels found 的排查
这是新手最常遇到、也最容易劝退的报错。通常原因是标签文件目录和图片目录不匹配。先用这个命令验证标签和图片是否一一对应:
python -c " import os imgs = os.listdir('dataset/images/train') labels = os.listdir('dataset/labels/train') print(len(imgs), len(labels)) "如果两个数量差异巨大,多半是标注导出格式选错了,或者转换脚本没有把 Pascal VOC 的坐标归一化。还有一种情况是数据集里混入了没有标注信息的图片,YOLOv5 会直接忽略这些图片并打印提示,而不是中断训练。请务必检查 data yaml 的路径,YOLOv5 要求路径是相对 data yaml 所在目录的,不能写绝对路径前加 / 否则会在根目录找,这一点官方文档没有强调。
6.2 训练曲线正常但 mAP 很低的常见原因
训练过程看起来一切正常,loss 在下降,但 mAP 就是上不去,这类问题通常是数据标注不一致引起的。比如不同标注员对"水果"边界框的画法差异较大,有人框住了整个果实,有人只框住可见部分,这会导致模型学到"平均框",定位既不准确也不稳定。
另一类是类别不平衡问题。车牌识别场景里,"蓝牌"样本可能有 10 万张,"新能源绿牌"只有 2000 张,模型会严重偏向多数类。常规解法是给少数类加复制增强,或者用 YOLOv5 的 class weights 参数。后者可以在 data yaml 里这样配:
# class weights, 少样本类权重大 class_weights: [1.0, 5.0]此外,验证集的评估指标如果只看 mAP_0.5,很容易忽略定位精度的不足。实际部署中 IoU 0.5 的框可能偏大,导致后续 OCR 拿到的车牌区域含过多背景。一定要同时关注 mAP_0.5:0.95,它才是定位精度的真正体现。
6.3 漏检小目标的系统化排查思路
YOLOv5 默认针对 COCO 数据集设计,其中小目标占比不高,所以默认配置在极端小目标场景下表现一般。排查思路按照"数据—模型—后处理"三层递进:
数据层:统计训练集中目标面积占比,中位数如果低于 2%,说明小目标严重不足。此时优先做切片增强,即把大图切块后放大训练,这是见效最快的方法。
模型层:检查是否用了 P2 输出层。YOLOv5 支持 --p2 参数,能额外输出 160x160 的特征图专门检测小目标,代价是显存和延迟各增加 30% 左右。
后处理层:如果目标本身非常接近,NMS 的 IoU 阈值也调。默认的 IoU 阈值是 0.45,对于靠近的小目标,可以降到 0.3,避免两个靠近的框被 NMS 误删成一个。
6.4 显存不足怎么办:梯度累积与显存节约技巧
显存不足是最常见的训练报错,尤其是在 6GB 显存的卡上训练自定义数据集。第一反应是调小 batch size,很多人会直接降到 2 或者 1,但这会让 BatchNorm 的统计量不稳定。更优雅的解法是梯度累积:batch size 设为 8,但每训练 2 个 batch 做一次梯度更新,等效于 batch 16 的效果。
python train.py \ --batch 8 \ --accumulate 2另外,YOLOv5 的 --image-weights 参数可以根据训练中图片的 loss 动态给"难例"加权采样。这个功能对显存占用几乎没影响,但对小目标、复杂背景的样本提升明显,推荐在训练中后期开启。
如果显存实在紧张到 batch size 1 都跑不动,就得考虑降低输入分辨率。从 640 降到 512 通常 mAP 只掉 1~3 个点,但显存占用下降大约 35%。在数据集允许的前提下,这是一个性价比非常高的取舍。
7. 个人实操经验之谈
最后分享一个我多次踩坑后沉淀下来的经验:永远不要直接照搬官方仓库的默认配置到自己的项目里,但也不要一上来就魔改网络结构。YOLOv5 最强的设计恰恰是它的"中庸"——在精度、速度、显存占用、部署难度之间取得了极佳的平衡。多数实际项目,先用 YOLOv5s 从零到一打通全流程,再用 YOLOv5m 或 l 微调对比,远比一开始就研究如何在主干里加注意力机制更有价值。
我遇到过不少朋友问:"为什么我的 YOLOv5 精度总是达不到博客里的效果?"答案往往不在网络层,而在数据质量、超参数适配和验证集划分上。网络结构是显式的知识,数据治理是隐式的功夫,后者才是拉开差距的地方。如果你正卡在某个训练异常或部署报错上,不妨拿本文的排查清单对照一下,很可能就省下一整天的调试时间。