
如果你是第一次把 YOLOv5 跑通之后就急着去训练自己的数据集那大概率和我当初一样环境装了、代码 clone 了、训练也启动了但对网络内部到底长什么样基本是黑的。直到有一天我想换检测头、想剪枝、想弄懂为什么三个检测头输出通道都是 255 时才被迫老老实实把结构从头到尾拆了一遍。这篇博文就是那次拆解的笔记用文字版的图解尽量把从输入端到检测头的完整链路讲清楚。适合已经跑通 YOLOv5、但还没吃透结构的朋友也适合接下来要做自定义识别、模型部署或者调参的人参考。1. 先把YOLOv5的流水线画在脑子里1.1 三个部件分工YOLOv5 是单阶段目标检测网络整条数据流可以粗略分成三段Backbone 负责从原始图像里提特征Neck 负责把不同尺度的特征做融合Head 负责在融合后的特征图上输出框和类别。这个划分不是 YOLOv5 发明的但 YOLOv5 把每段的实现都做得很工程化。理解这段结构最简单的方式是把它想成一条工厂流水线Backbone 是原料粗加工车间把 640×640 的彩色图一步步变成越来越抽象、越来越薄的特征图Neck 是中间质检站把不同车间的半成品拿过来交叉比对确保小目标和大目标都有对应的特征信息Head 是最终出货口在三张不同尺寸的特征图上分别预测。1.2 特征图尺寸变化以YOLOv5s为例下面这张表是 YOLOv5sv6.0 版本Backbone 部分的特征图尺寸变化我建议把这张表抄在笔记本上后面看任何结构图都比对着看会顺很多。Backbone模块输出特征图size步长备注输入640×640×3-默认推理尺寸Stem Conv(6×6, s2)320×320×322早期版本这里是FocusConv(3×3, s2)160×160×644C3160×160×644第一个C3Conv(3×3, s2)80×80×1288往下到P3C380×80×1288P3特征来源Conv(3×3, s2)40×40×25616往下到P4C340×40×25616P4特征来源Conv(3×3, s2)20×20×51232C320×20×51232P5特征来源SPPF20×20×51232池化后通道不变这里 P3、P4、P5 是后续网络里反复出现的代号分别对应 8 倍、16 倍、32 倍下采样。P3 分辨率最高、感受野最小负责找小目标P5 分辨率最低、感受野最大负责找大目标P4 居中。把主链路用字符画出来就是这样的后面每一节都可以对着它看输入 640×640×3 │ ├─ Stem 6×6卷积(s2) → 320×320×32 │ 3×3卷积(s2) → 160×160×64 │ C3 → 160×160×64 │ 3×3卷积(s2) → 80×80×128 │ C3 → 80×80×128 ─────────────→ P3 │ 3×3卷积(s2) → 40×40×256 │ C3 → 40×40×256 ─────────────→ P4 │ 3×3卷积(s2) → 20×20×512 │ C3 → 20×20×512 │ SPPF → 20×20×512 ───────────→ P5 │ ├─ Neck(PANet) │ P5 → 1×1卷积降维 → 2倍上采样 │ → 与P4拼接 → C3 → P4 │ P4 → 1×1卷积降维 → 2倍上采样 │ → 与P3拼接 → C3 → P3 │ P3 → 3×3卷积(s2) → 与P4拼接 → C3 → P4 │ P4 → 3×3卷积(s2) → 与P5拼接 → C3 → P5 │ └─ Head P3(80×80) → 1×1卷积 → 输出 80×80×255 P4(40×40) → 1×1卷积 → 输出 40×40×255 P5(20×20) → 1×1卷积 → 输出 20×20×255注意这里写的是 COCO 的 80 类所以每个尺度的输出通道是 255。换成你自己的数据集这个数字会变具体怎么算在第 5 节讲。1.3 怎么画出自己的结构图想看图不一定要去网上找别人画的自己动手最快。YOLOv5 的模型定义都在models/yolo.py和models/common.py里结构本身写在models/yolov5s.yaml。加载模型后直接打印就能看到完整的层列表import torch from models.yolo import DetectionModel model DetectionModel(yolov5s.yaml, ch3, nc80) print(model)如果想可视化推荐两个方式一是把模型导出成 ONNX 后用 Netron 打开图形化界面看每个张量的流向非常直观二是在代码里调用torchviz生成计算图。我个人的习惯是导出 ONNX Netron因为可以看到每个中间层的输出形状排查维度对不上这类问题特别快。2. 输入端的设计Mosaic、自适应锚框和超参数很多人以为结构分析只需要看 Backbone 到 Head其实 YOLOv5 的输入端设计对最终精度影响非常大而且这部分同样写在结构定义和训练策略里。2.1 Mosaic增强Mosaic 的思路很简单每次训练读 4 张图经过随机缩放、裁剪、拼接后合成一张 640×640 的新图相当于在一个 batch 里同时看到了 4 张图的上下文。这样做的好处有两个第一小目标样本变多了因为缩放会让很多目标在拼接图里变小第二相当于隐式增大了 batch sizeBatchNorm 的统计量一次能看到 4 张图的分布训练更稳定。在 YOLOv5 的配置里Mosaic 不是全程开的。默认配置会在训练的最后 10 个 epoch 关闭因为拼接图会导致目标分布失真最后阶段需要回到真实分布上微调。很多人不知道这点总疑惑最后几轮 loss 为什么波动变大其实就是这个开关在切换。2.2 自适应锚框锚框是检测头预测的基础。YOLOv5 默认的锚框是针对 COCO 数据集算出来的三组框分别对应 P3、P4、P5 三个尺度。如果你换了一个完全不同的数据集比如水果识别或车牌识别目标宽高比和 COCO 差异很大默认锚框可能不是最优的。官方仓库提供了utils/autoanchor.py训练时会自动检查锚框质量。它会计算一个 Best Possible RecallBPR如果 BPR 0.98就会提示你重新计算锚框。所以训练自己的数据集时第一次跑训练命令看到类似 Autoanchor: 10.1 anchors/target, 0.997 Best Possible Recall (BPR) 的输出说明锚框还算合适如果 BPR 很低建议先跑一下重算脚本而不是直接开训。2.3 超参数文件中值得注意的参数YOLOv5 把训练超参数独立放在data/hyps/hyp.scratch-low.yaml里。这些参数不直接影响网络结构但对训练过程和最终精度的影响不亚于结构本身。参数默认值大致作用lr00.01初始学习率lrf0.01最终学习率 lr0 × lrfmomentum0.937SGD动量weight_decay0.0005权重衰减warmup_epochs3.0前3轮warmupbox0.05框回归损失权重cls0.5分类损失权重obj1.0置信度损失权重anchor_t4.0锚框匹配阈值hsv_h / hsv_s / hsv_v0.015 / 0.7 / 0.4HSV颜色增强幅度degrees0.0旋转增强translate0.1平移增强scale0.5缩放增强flipud0.0上下翻转概率fliplr0.5左右翻转概率mosaic1.0Mosaic概率mixup0.0Mixup概率默认值不一定适合所有场景。比如车牌识别里车牌通常长宽比很大翻转增强可以保留但垂直方向的角度增强可能引入过多语义错误的样本这时调小degrees会更稳。超参数的调整是另一个大话题这里先记住一个原则结构决定了网络能力的上限超参数决定你能多接近这个上限。3. Backbone拆解Focus、C3、SPPF各自的算盘Backbone 是 YOLOv5 最核心的部分早期版本里有 Focus 模块C3 和 SPPF 则一直保留。这三个模块的源码都不长但每个都有明确的设计意图。3.1 Focus切片在 v5.0 及以前YOLOv5 的第一个模块是 Focus。它对输入图做切片把一张 640×640×3 的图按像素位置的奇偶性切成 4 张 320×320×3 的子图再在通道维度拼成 320×320×12最后过一层 3×3 卷积得到 320×320×32。这么做的核心目的是降计算量。传统做法是直接用步长为 2 的卷积下采样而 Focus 通过切片先把空间分辨率降一半再做卷积时每个卷积核只需要处理更小的特征图。从感受野角度看Focus 3×3 卷积的效果和一层 6×6 步长 2 的卷积非常接近但工程实现上更灵活速度也更快。v6.0 之后官方把 Focus 换成了一层 6×6 步长 2 的普通卷积理由是这样做对 ONNX、TensorRT、CoreML 等部署框架更友好避免了切片操作在某些硬件上带来的额外开销。所以你在不同版本的 YOLOv5 里看到的第一个模块可能不一样这不是结构错了是版本差异。后面部署到 Jetson Nano 这类设备时我更推荐使用 v6.0 之后的版本少一层特殊算子转 TensorRT 时能少踩很多坑。3.2 C3模块C3 是 YOLOv5 里出现次数最多的模块Backbone 和 Neck 里到处都是。它源自 CSPNet 的思想核心就是一句话把特征图在通道维度分成两路一路走卷积和残差块另一路直接做 1×1 卷积最后把两路拼回去再用 1×1 卷积融合。为什么要这样分路因为在传统残差网络里梯度在反向传播时会在很多层之间重复传递存在大量冗余计算。CSP 把一部分特征直接抄近道送到后面让梯度有更短的传播路径同时减少重复计算。实测效果是同样计算量下CSP 结构能获得更好的精度训练时显存占用也更低。C3 内部有一个可选的shortcut参数。Backbone 里的 C3 默认开残差因为深层网络需要残差来稳定梯度Neck 里的 C3 默认关残差因为 Neck 结构本身不深没必要多一条跳连省一点算力是一点。这个细节在models/yolov5s.yaml里能直接看到C3 的参数比普通 Conv 多一个False。3.3 SPPFSPPF 是 SPP 的快速版。空间金字塔池化的本意是用不同尺寸的池化核去提取多尺度特征YOLOv5 靠三个并行的 5×5、9×9、13×13 最大池化做到这一点。SPPF 换了一种等价实现把三个 5×5 最大池化串起来每池化一次就把结果拼接一次。这里的关键是池化感受野的叠加规律两个 5×5 池化串联等效感受野是 9×9三个串联就是 13×13。所以 SPPF 用三个小池化核等效拿到了和 SPP 完全一样的三档感受野但并行变串行后计算量更小、结构更规整在 GPU 上跑起来更快。代码里 SPPF 也就几行class SPPF(nn.Module): def __init__(self, c1, c2, k5): super().__init__() c_ c1 // 2 self.cv1 Conv(c1, c_, 1, 1) self.cv2 Conv(c_ * 4, c2, 1, 1) self.m nn.MaxPool2d(kernel_sizek, stride1, paddingk // 2) def forward(self, x): x self.cv1(x) y1 self.m(x) y2 self.m(y1) y3 self.m(y2) return self.cv2(torch.cat([x, y1, y2, y3], 1))注意padding k // 2当 k5 时 pad2保证池化不改变特征图尺寸。如果你自己改 SPPF 的池化核大小这个 padding 一定要同步改否则后面所有 concat 维度都会错位。4. Neck自顶向下和自底向上的两次握手Neck 是 YOLOv5 结构里最容易看晕的部分因为它有两条路径分别是自顶向下和自底向上组合起来叫 PANet。4.1 FPN上半程先看自顶向下这条路径。流程是SPPF 输出的 P520×20先经过 1×1 卷积把通道降下来然后 2 倍上采样到 40×40和 Backbone 里 C3 输出的 P4 拼接再过一个 C3 融合得到新的 P4。类似地P4 再上采样到 80×80和 P3 拼接融合得到 P3。这条路径解决什么问题低层的高分辨率特征图P3、P4含有丰富的空间细节但语义信息弱高层的 P5 语义强但空间分辨率低。FPN 的自顶向下路径相当于把高层的语义信息分发给低层让低层特征既保留细节又具备语义判别能力。4.2 PAN下半程但光有 FPN 不够。自顶向下路径在传递语义时位置信息会逐渐丢失。PANet 补了自底向上这条路径P3 经过 3×3 步长 2 的卷积降到 40×40和 P4 拼接、融合得到 P4P4 再降到 20×20和 P5 拼接、融合得到 P5。用一句话概括FPN 让高层语义往下走PAN 让底层位置信息往上走两者在 Neck 里握手之后三个尺度的输出同时拥有较强的语义和空间信息。这也是 YOLOv5 小目标效果比早期 YOLO 系列好的重要原因之一。我在自己的图上画这两条路径时习惯用不同颜色笔画箭头自顶向下画蓝色自底向上画红色拼接点画成圆圈。画完一眼就能看清哪些层是信息汇合点调试特征可视化时基本每次都对着汇合点后面的层看。4.3 为什么neck里的C3不接残差很多人第一次看yolov5s.yaml会疑惑为什么 Backbone 的 C3 后面参数是[128]Neck 的 C3 后面却是[128, False]。这个False就是前面说的shortcut开关。Neck 里的模块很浅一条路径上通常只有两三个 C3梯度过深的顾虑不存在。关掉残差后每个 C3 内部就是一个直接的 1×1 → 3×3 → 1×1 主通路省去了捷径分支的加法和额外内存速度更快实测对精度基本无影响。这个细节说明 YOLOv5 在结构设计上很抠计算量该省的地方绝不手软。5. Detect Head解读255这个数字从哪来5.1 三个尺度的分工Detect Head 接收 Neck 输出的三个特征图P380×80、P440×40、P520×20。80×80 的每个格子对应原图 640×640 上的 8×8 像素区域适合检测小目标20×20 的每个格子对应 32×32 的区域适合检测大目标。每个格子上预设 3 个锚框每个锚框的输出是一个向量4 个框坐标中心点 x、y 和宽高 w、h、1 个目标置信度、以及每个类别的概率。所以 COCO 的 80 类单个锚框的输出维度是 4 1 80 853 个锚框就是 85 × 3 255。这就是三个检测头输出通道都是 255 的原因。尺度特征图size对应原图感受野适合目标P380×80小小目标P440×40中中目标P520×20大大目标如果你换成自定义数据集比如水果识别 nc6检测头最后的通道就是 3 × (4 1 6) 33车牌识别 nc1就是 3 × 6 18。模型配置文件yolov5s.yaml里 Detect 层的输出通道不需要手写它根据nc和anchors自动算。5.2 目标匹配anchor与gt怎么对齐有了预测训练时怎么把真实框分配给某个锚框YOLOv5 的匹配规则大致是对每个真实框计算它和所有尺度锚框的宽高比如果最大比值小于阈值anchor_t默认 4.0就认为这个锚框够格负责预测这个目标。匹配上的锚框要承担坐标回归、置信度和分类的学习任务没匹配上的就只学置信度背景。这也是为什么锚框质量很重要如果锚框和目标宽高比差异太大匹配阶段可能一个真实框都配不上模型学不到东西。训练自定义数据集时看到的 Autoanchor 提示本质就是在帮你检查这层匹配是否健康。5.3 损失构成box、cls、objYOLOv5 的总损失由三部分组成框回归损失默认使用 CIoU 损失同时考虑重叠面积、中心点距离和宽高比分类损失只有正样本参与用 BCEWithLogits置信度损失所有样本参与用 BCEWithLogits正样本的标签是由该位置锚框与真实框的 IoU 计算出来的。三个损失的权重分别在box、cls、obj超参数里控制。我调参时的经验是如果模型找得到框但分错类优先加大cls如果框的位置飘优先看 CIoU 的部分和锚框是否合理如果误检很多调高obj的权重或提高置信度阈值而不是盲目动结构。6. 同一套图纸五种规模n/s/m/l/x的尺寸密码YOLOv5 提供了 n、s、m、l、x 五种规模的模型它们的结构完全一样唯一的区别是两个缩放系数depth_multiple深度倍数和width_multiple宽度倍数。这两个值直接写在models/yolov5s.yaml这类文件顶部。6.1 depth_multiple与width_multipledepth_multiple控制 C3 内部残差块的数量。配置文件里每个 C3 后跟的 n如 3、6、9是基础重复次数最终重复次数是max(round(n × depth_multiple), 1)。width_multiple控制每层卷积的通道数最终通道数是int(基础通道数 × width_multiple)。五档配置的对应关系如下模型depth_multiplewidth_multiple参数量约典型用途YOLOv5n0.330.251.9MJetson Nano、树莓派YOLOv5s0.330.507.2M通用GPU推理YOLOv5m0.670.7521.2M精度优先YOLOv5l1.01.046.5M高精度YOLOv5x1.331.2586.7M追求极限精度我最初以为 s 和 m 差在层数实际看了结构才明白更多是通道数在差异。n → s 是宽度翻倍层数没变s → m 是宽度又涨了一半、深度翻倍。理解这个机制之后自定义模型规模就很简单想快就缩宽度想准就加深度或宽度但别超出显存。6.2 从结构看不同规模的适用场景在实际项目里我总结了一条选型原则先量任务难度再选规模。Nano 和 Small 适合算力紧张的边缘设备比如 Jetson Nano 上部署 YOLOv5sTensorRT FP16 大概能跑到实时。Medium 和 Large 适合服务端推理对帧率要求不高但要精度。X 一般只在打比赛或离线处理时用成本和收益比往往不如 l。结构上的另一个规律是网络越宽对小目标越友好网络越深对大目标的抽象能力越强。所以如果是水果识别这种物体较大、类别少的任务Small 完全够用如果是车牌识别这种需要看清小字、但目标本身结构规整的任务Small 加合适的输入分辨率通常效果就不错。7. 把结构知识用在实战里自定义数据、部署、小设备7.1 改nc后检测头通道数怎么变基于 YOLOv5 做水果识别或车牌识别时最常改的就是nc。你只需要把data/coco128.yaml这类数据配置里的nc改成自己的类别数把models/yolov5s.yaml里的nc同步改掉即可。训练时模型会根据 nc 自动计算检测头输出通道不需要手动改 255 那个数。这里最容易犯的错是只改数据配置、忘了改模型配置。两者 nc 不一致时模型加载权重会报 shape 不匹配或者训练时类别维度算错白白浪费一个上午。我的习惯是训练前先打印一下模型最后一层确认输出的第三个维度等于3 × (5 nc)。7.2 部署到Jetson Nano时哪里最耗算力部署和训练看结构的角度不一样。训练时关注显存和收敛速度部署时关注延迟和内存占用。在 Jetson Nano 这类设备上最耗时的往往是 Backbone 里的普通卷积其次是 Neck 里的上采样和 concat 操作。实际部署时我一般做三件事第一把模型换成 FP16 精度显存和延迟都能降不少第二考虑用 TensorRT 把模型转成 engine重点检查 Focus如果用了老版本和 Detect 层能不能被 TensorRT 算子覆盖第三评估是否需要裁剪输入分辨率。很多人一味追求高分辨率结果 640 升到 1280帧率掉一半精度提升却很有限这是性价比很低的做法。7.3 版本演进Focus为什么被6×6卷积替代前面提过v6.0 开始 YOLOv5 用 6×6 卷积替换 Focus。官方这样做的动机是Focus 切片操作在导出 ONNX 后会出现多个切片节点部分推理引擎对这些节点支持不友好量化时也容易掉精度。换成普通卷积后图的拓扑更干净跨框架兼容性大幅提升。如果你从官网下载的是最新代码直接就是 6×6 卷积版。如果你参考的老教程提到 Focus不必纠结两者的输入输出等价性足够高完全可以把新版结构图和老教程对照着看反而能加深理解。8. 拆结构时踩过的坑和最后留下的几条经验第一次完整拆完 YOLOv5 结构我踩了几个坑写在这里帮后来人省点时间。第一个坑是随意改通道数。有一次我想把 Backbone 第一个卷积从 32 改到 64 来提升性能结果 Neck 里所有 concat 的维度全对不上。YOLOv5 的通道数是全局联动的改一处就得顺着数据流把所有相关层都改一遍。除非你真理解每一层依赖关系否则建议通过width_multiple整体缩放而不是手工改单层。第二个坑是 SPPF 的 padding。这个前面提过池化核变大时没同步改 padding特征图尺寸对不上报错信息还藏得深。排查 concat 维度错误时先看每个分支的空间尺寸再看通道数。第三个坑发生在部署阶段。ONNX 导出后我发现 NMS 不在模型里才知道 YOLOv5 的 detect 层在推理时还要自己做坐标解码NMS 是后处理阶段单独实现的。所以部署时要把网络输出 → 解码 → NMS → 画框这一整条链都搬过去只导出模型是不够的。至于结构理解的实际收益我自己体会最深的一点是看懂结构之后再调参会变得有方向。以前遇到模型召回率低我只会加训练轮数现在我会先判断是不是锚框匹配有问题再去看是不是 P3 小目标分支的特征融合不足最后才动超参数。这种能定位问题在哪一层的能力比记住任何一张结构图都更有价值。如果你也打算深入 YOLOv5我的建议是别急着跑代码先花一个下午把本文这张结构图亲手画一遍画完你会发现后面所有训练、调参、部署的问题都变得好聊多了。