☰
YOLOv5网络结构深度拆解:从Backbone到Detect Head完整解析
2026/9/29 19:49:01 网站建设 项目流程

如果你是第一次把 YOLOv5 跑通之后就急着去训练自己的数据集,那大概率和我当初一样:环境装了、代码 clone 了、训练也启动了,但对网络内部到底长什么样,基本是黑的。直到有一天我想换检测头、想剪枝、想弄懂为什么三个检测头输出通道都是 255 时,才被迫老老实实把结构从头到尾拆了一遍。这篇博文就是那次拆解的笔记,用文字版的"图解"尽量把从输入端到检测头的完整链路讲清楚。适合已经跑通 YOLOv5、但还没吃透结构的朋友,也适合接下来要做自定义识别、模型部署或者调参的人参考。

1. 先把YOLOv5的流水线画在脑子里

1.1 三个部件分工

YOLOv5 是单阶段目标检测网络,整条数据流可以粗略分成三段:Backbone 负责从原始图像里提特征,Neck 负责把不同尺度的特征做融合,Head 负责在融合后的特征图上输出框和类别。这个划分不是 YOLOv5 发明的,但 YOLOv5 把每段的实现都做得很工程化。

理解这段结构,最简单的方式是把它想成一条工厂流水线:Backbone 是原料粗加工车间,把 640×640 的彩色图一步步变成越来越抽象、越来越"薄"的特征图;Neck 是中间质检站,把不同车间的半成品拿过来交叉比对,确保小目标和大目标都有对应的特征信息;Head 是最终出货口,在三张不同尺寸的特征图上分别预测。

1.2 特征图尺寸变化(以YOLOv5s为例)

下面这张表是 YOLOv5s(v6.0 版本)Backbone 部分的特征图尺寸变化,我建议把这张表抄在笔记本上,后面看任何结构图都比对着看,会顺很多。

Backbone模块输出特征图size步长备注
输入640×640×3-默认推理尺寸
Stem Conv(6×6, s=2)320×320×322早期版本这里是Focus
Conv(3×3, s=2)160×160×644
C3160×160×644第一个C3
Conv(3×3, s=2)80×80×1288往下到P3
C380×80×1288P3特征来源
Conv(3×3, s=2)40×40×25616往下到P4
C340×40×25616P4特征来源
Conv(3×3, s=2)20×20×51232
C320×20×51232P5特征来源
SPPF20×20×51232池化后通道不变

这里 P3、P4、P5 是后续网络里反复出现的代号,分别对应 8 倍、16 倍、32 倍下采样。P3 分辨率最高、感受野最小,负责找小目标;P5 分辨率最低、感受野最大,负责找大目标;P4 居中。

把主链路用字符画出来就是这样的,后面每一节都可以对着它看:

输入 640×640×3 │ ├─ Stem 6×6卷积(s=2) → 320×320×32 │ 3×3卷积(s=2) → 160×160×64 │ C3 → 160×160×64 │ 3×3卷积(s=2) → 80×80×128 │ C3 → 80×80×128 ─────────────→ P3 │ 3×3卷积(s=2) → 40×40×256 │ C3 → 40×40×256 ─────────────→ P4 │ 3×3卷积(s=2) → 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卷积(s=2) → 与P4'拼接 → C3 → P4'' │ P4'' → 3×3卷积(s=2) → 与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', ch=3, nc=80) print(model)

如果想可视化,推荐两个方式:一是把模型导出成 ONNX 后用 Netron 打开,图形化界面看每个张量的流向非常直观;二是在代码里调用torchviz生成计算图。我个人的习惯是导出 ONNX + Netron,因为可以看到每个中间层的输出形状,排查"维度对不上"这类问题特别快。

2. 输入端的设计:Mosaic、自适应锚框和超参数

很多人以为结构分析只需要看 Backbone 到 Head,其实 YOLOv5 的输入端设计对最终精度影响非常大,而且这部分同样写在结构定义和训练策略里。

2.1 Mosaic增强

Mosaic 的思路很简单:每次训练读 4 张图,经过随机缩放、裁剪、拼接后合成一张 640×640 的新图,相当于在一个 batch 里同时看到了 4 张图的上下文。这样做的好处有两个:第一,小目标样本变多了,因为缩放会让很多目标在拼接图里变小;第二,相当于隐式增大了 batch size,BatchNorm 的统计量一次能看到 4 张图的分布,训练更稳定。

在 YOLOv5 的配置里,Mosaic 不是全程开的。默认配置会在训练的最后 10 个 epoch 关闭,因为拼接图会导致目标分布失真,最后阶段需要回到真实分布上微调。很多人不知道这点,总疑惑最后几轮 loss 为什么波动变大,其实就是这个开关在切换。

2.2 自适应锚框

锚框是检测头预测的基础。YOLOv5 默认的锚框是针对 COCO 数据集算出来的三组框,分别对应 P3、P4、P5 三个尺度。如果你换了一个完全不同的数据集,比如水果识别或车牌识别,目标宽高比和 COCO 差异很大,默认锚框可能不是最优的。

官方仓库提供了utils/autoanchor.py,训练时会自动检查锚框质量。它会计算一个 Best Possible Recall(BPR),如果 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 × lrf
momentum0.937SGD动量
weight_decay0.0005权重衰减
warmup_epochs3.0前3轮warmup
box0.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 SPPF

SPPF 是 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, k=5): 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_size=k, stride=1, padding=k // 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,当 k=5 时 pad=2,保证池化不改变特征图尺寸。如果你自己改 SPPF 的池化核大小,这个 padding 一定要同步改,否则后面所有 concat 维度都会错位。

4. Neck:自顶向下和自底向上的两次握手

Neck 是 YOLOv5 结构里最容易看晕的部分,因为它有两条路径,分别是自顶向下和自底向上,组合起来叫 PANet。

4.1 FPN上半程

先看自顶向下这条路径。流程是:SPPF 输出的 P5(20×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' 拼接、融合得到 P4'';P4'' 再降到 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 输出的三个特征图:P3'(80×80)、P4''(40×40)、P5''(20×20)。80×80 的每个格子对应原图 640×640 上的 8×8 像素区域,适合检测小目标;20×20 的每个格子对应 32×32 的区域,适合检测大目标。

每个格子上预设 3 个锚框,每个锚框的输出是一个向量:4 个框坐标(中心点 x、y 和宽高 w、h)、1 个目标置信度、以及每个类别的概率。所以 COCO 的 80 类,单个锚框的输出维度是 4 + 1 + 80 = 85,3 个锚框就是 85 × 3 = 255。这就是三个检测头输出通道都是 255 的原因。

尺度特征图size对应原图感受野适合目标
P380×80小小目标
P440×40中中目标
P520×20大大目标

如果你换成自定义数据集,比如水果识别 nc=6,检测头最后的通道就是 3 × (4 + 1 + 6) = 33;车牌识别 nc=1,就是 3 × 6 = 18。模型配置文件yolov5s.yaml里 Detect 层的输出通道不需要手写,它根据nc和anchors自动算。

5.2 目标匹配:anchor与gt怎么对齐

有了预测,训练时怎么把真实框分配给某个锚框?YOLOv5 的匹配规则大致是:对每个真实框,计算它和所有尺度锚框的宽高比,如果最大比值小于阈值anchor_t(默认 4.0),就认为这个锚框"够格"负责预测这个目标。匹配上的锚框要承担坐标回归、置信度和分类的学习任务,没匹配上的就只学置信度(背景)。

这也是为什么锚框质量很重要:如果锚框和目标宽高比差异太大,匹配阶段可能一个真实框都配不上,模型学不到东西。训练自定义数据集时看到的 "Autoanchor" 提示,本质就是在帮你检查这层匹配是否健康。

5.3 损失构成:box、cls、obj

YOLOv5 的总损失由三部分组成:

  • 框回归损失:默认使用 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_multiple

depth_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 上部署 YOLOv5s,TensorRT 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,我的建议是别急着跑代码,先花一个下午把本文这张结构图亲手画一遍,画完你会发现后面所有训练、调参、部署的问题,都变得好聊多了。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询