简介:面向目标检测初学者和YOLOv8使用者的网络结构图解教程,从整体到细节逐步拆解网络设计,核心解决“看懂结构但画不出图、对不上代码”的痛点。文档先对Backbone、Neck、Head三部分作整体说明,再分模块讲解卷积层、C2f、SPPF、上采样、Concat及检测头的功能与连接关系;同时对照yolov8.yaml和PyTorch源码,帮助读者将配置文件、代码实现与网络图一一对应。绘制方面,文档给出使用Visio、Graphviz、Netron等工具绘制结构图的完整流程,覆盖Backbone、Neck、Head的分步画法、标注规范与美化技巧。包内仅含1个doc文档,压缩包约53KB,内容集中,便于随查随用。目前已有2307人学习,非常适用于目标检测入门、网络结构分析、论文插图绘制等场景。文档还对比YOLOv5说明C2f的改进,并讨论注意力机制、轻量化骨干等优化方向,帮助读者在理解结构基础上根据具体任务做调整与实验。
1. 看了一天源码,还不如先画一张YOLOv8结构图
很多同学第一次接触 YOLOv8,第一反应是打开官方仓库把模型代码从头到尾读一遍,结果读了一个下午,能说清楚“输入进了模型之后到底经过了哪些层”的人少之又少。我见过太多人卡在同一个地方:训练能跑、demo 也能出框,但一旦要改网络结构——比如把某个 C2f 删掉、把 SPPF 换掉、给 Neck 加一层——就完全不知道从哪里下手。这时候与其继续盯着代码,不如先把网络结构图画出来。结构图不只是一张给汇报用的插图,它是你理解模型、定位模块、动手改造的地图。这篇文章我会把我自己画 YOLOv8 结构图的方法、参数和踩过的坑完整讲一遍,新手能照着画,熟手也能拿去对照检查自己的理解有没有偏差。
2. 拆开YOLOv8的骨架:Backbone、Neck、Head到底怎么连接
2.1 先读懂结构文件:一张表看懂模块职责
YOLOv8 的网络结构在官方仓库里是用一个结构描述文件(扩展名为 yaml 的配置文件)定义的,而不是在 Python 代码里硬编码每一层。这个文件里写了三件事:Backbone 有哪些模块、Neck 有哪些模块、Head 输出几个尺度的检测结果。结构图要画得准,第一步就是把这份文件里的模块名和实际作用对应起来。下面这张表是我每次画图前必看的对照表,模块名以常见版本的结构文件为准。
| 结构部分 | 模块名(按出现顺序) | 主要作用 |
|---|---|---|
| Backbone | CBS、C2f、SPPF | 提取特征,逐层降低分辨率,最终输出多层特征图 |
| Neck | CBS、Upsample、Concat、C2f、CBS(下采样) | 融合不同尺度的特征,形成 PAN 结构 |
| Head | Detect(三个分支) | 在每个尺度上预测框位置和类别 |
这里有个容易被忽略的点:结构文件里的模块名是“简写”,真正的实现是 Python 类。比如 CBS 是 Conv + BatchNorm + SiLU 的封装,C2f 是一个包含多个 Bottleneck 的 CSP 结构模块,SPPF 是空间金字塔池化的快速版本。画结构图时如果你只写“C2f”不展开内部细节,这张图是能用的;如果你想给别人讲清楚每个模块为什么这么设计,就需要把 C2f、SPPF 单独拆开画。我一般会先画一张“粗粒度”的总体连接图,再针对关键模块画“细粒度”的内部展开图,两张图配合使用。
2.2 张量尺寸流转:从640x640到三路输出
结构图的核心不是方框,而是方框之间的连线,以及线上流动的张量形状。YOLOv8 默认输入是 640x640x3 的 RGB 图像,经过 Backbone 后会在三个不同尺度上输出特征图,分别记为 P3、P4、P5。这三个名字在结构图里必须标清楚,否则后续解读 Neck 和 Head 会全乱。
| 输出层级 | 特征图尺寸 | 通道数 | 对应输入图像上的下采样倍数 |
|---|---|---|---|
| P3 | 80x80 | 256 | 8 倍下采样 |
| P4 | 40x40 | 512 | 16 倍下采样 |
| P5 | 20x20 | 1024 | 32 倍下采样 |
这几个数字不是靠记忆背下来的,而是跟着卷积的 stride 推出来的。Backbone 前面几个 CBS 模块里,凡是 stride=2 的卷积都会让特征图宽高减半。输入 640,过第一个 stride=2 变成 320,再过第二个 stride=2 变成 160,继续下去就会得到 80、40、20 这一串尺寸。画结构图时,我习惯在每个模块旁边标注“输出尺寸 + 通道数”,而不是只写模块名。这样一张图看完,整个模型的计算路径也就清楚了。
2.3 手工画结构图的五个步骤
如果你不想一开始就写代码画图,可以先在纸上或者白板上手工画,步骤固定下来之后会很快。第一步,画输入框,标注 640x640x3。第二步,沿着 Backbone 顺序画出所有 CBS 和 C2f,每画一个在旁边写下输出尺寸和通道数。第三步,标出 P3、P4、P5 三个分支点,这三个点分别引出三条线进入 Neck。第四步,在 Neck 里按“先上采样融合,再下采样融合”的顺序连线,注意 Concat 模块要有两条输入线。第五步,把 Neck 的三个输出分别连到 Detect 的三个分支,标注每个分支的输出通道数。画完这五步,一张能用的结构图就出来了。手工画一遍的最大价值是强迫你自己把每一层的输入输出走一遍,很多“以为懂了其实没懂”的细节会在这个过程中暴露出来。
3. 结构图的核心部件:C2f、SPPF和解耦头的内部细节
3.1 C2f:结构图里出现最多的模块
C2f 是 YOLOv8 的 Backbone 和 Neck 里出现频率最高的模块,结构图如果只画一个方块写“C2f”,信息量是不够的。C2f 全称是 CSP Bottleneck with 2 convolutions,它的设计思路是:输入先经过一个 1x1 卷积把通道分成两路,一路直接走、另一路经过若干个 Bottleneck 子模块,最后把多路结果在通道维度上拼接起来。这里的“拼接”是 Concat,而不是相加,画图时一定要区分。
C2f 有两个关键参数:一个是输出通道数,另一个是 Bottleneck 的重复次数。在结构文件里,C2f 的写法类似C2f(128, 3),前半是通道数,后半是重复次数。有一个常见误区是以为重复次数越大输出通道数越大,其实不会,因为每个 Bottleneck 内部的卷积会自己管理通道数,最后 Concat 完之后还会有一个 1x1 卷积把通道数还原到设定值。画图时我建议把 C2f 画成一个“大框套小框”的形式,大框里画两到三个 Bottleneck 方块,并标注重复次数 n,这样别人一眼就能看出这是个可堆叠的结构。
3.2 SPPF:一个“快版”SPP,画图时别画错
SPPF 在结构图里通常出现在 Backbone 的最后一层,负责在固定分辨率下聚合多尺度的感受野信息。它的全称是 Spatial Pyramid Pooling - Fast,和早期 YOLO 系列里的 SPP 作用类似,但是实现方式做了简化。SPP 是并行做几个不同尺寸的池化再拼接,比如 5x5、9x9、13x13;SPPF 则改成串行:先做一个 5x5 的最大池化,再把结果继续做 5x5 池化,再做一次 5x5 池化,最后把原始输入和三次池化结果全部拼接起来。
画 SPPF 时最常犯的错误是把它画成三个并行的池化分支。实际上它是串行的,第二次池化作用的对象是第一次池化的输出,第三次池化作用的对象是第二次池化的输出,这种串行结构让计算量明显下降,但效果上和并行 SPP 基本一致。SPPF 内部通道也很有特点:输入的 1024 通道先被一个 1x1 卷积压缩成 512,池化过程中始终保持 512,最后 Concat 得到 2048,再经过一个 1x1 卷积还原到 1024。我把这个通道变化画成一条“先收窄再还原”的路径,结构图的信息量就上来了。
3.3 解耦检测头:输出通道数是怎么算出来的
YOLOv8 的 Head 是解耦头,也就是说分类和回归各走各的分支。在结构图上,Detect 不是一个简单的方框,而是三个分支,分别对应 P3、P4、P5。每个分支内部又分成分类支路和回归支路,二者共享一部分卷积,之后各自输出。
回归支路的输出通道数是固定的,由reg_max决定。以常见配置为例,reg_max=16,每个框需要预测 4 个位置参数,所以回归支路的输出是 4 乘 16 等于 64。分类支路的输出通道数是数据集类别数,比如检测 80 个类别时输出就是 80。所以一个检测头的总输出是 64 加 80 等于 144。这个数字经常出现在结构文件的 Head 部分,很多人不知道它是怎么来的,画图时直接把 144 写成卷积输出,不对,它其实是两条支路拼出来的。画图时把分类支路和回归支路分开画,再标注拼接操作,这张图就严谨了。
3.4 结构图对应源码:从哪里找到每个部件
结构图画完之后,建议把它和源码对应一遍,确认每个方块在代码里都能找到实现。CBS 对应的是卷积、批归一化、SiLU 激活的组合;C2f 对应的是那个包含多个 Bottleneck 的类;SPPF 对应的是串行最大池化的实现;Detect 对应的是检测头类。我习惯在结构图的每个方块旁边写上源码里的类名,这样后续想改结构时,能直接定位到要改的代码位置。
这里有一个很实用的小技巧:官方源码里通常会有一份结构文件,里面每一行的格式是“模块名: 参数”。你可以在读结构文件时把每一行的模块名标上序号,然后在结构图里也标同样的序号,两边一一对应。这样排查问题时,你可以顺着序号快速跳转到对应代码,不用每次都从头搜索。我自己的经验是,做过一次完整的“结构文件到结构图再到源码”的映射之后,再去看模型训练日志里的层名,基本就能直接读懂。
4. 用Python把结构图画成矢量图:可直接跑的绘图脚本
4.1 绘图脚本:matplotlib版
手工画图适合理思路,但要做汇报材料或者写技术文档时,还是需要一张干净的矢量图。我一般用 matplotlib 直接画,不需要额外安装 graphviz 之类的工具。下面这个脚本是我常用的简化版,它能把 YOLOv8 的 Backbone、Neck、Head 按区域画出来,并自动编号。代码可以直接保存运行,注意提前安装好依赖。
import matplotlib.pyplot as plt from matplotlib.patches import FancyBboxPatch, FancyArrowPatch def draw_module(ax, x, y, w, h, text, fc): """画一个带圆角的模块方块。fc 控制填充颜色。""" box = FancyBboxPatch((x, y), w, h, boxstyle="round,pad=0.02", linewidth=1.2, edgecolor="#2d2d2d", facecolor=fc) ax.add_patch(box) ax.text(x + w / 2, y + h / 2, text, ha="center", va="center", fontsize=8) def draw_arrow(ax, x1, y1, x2, y2): """画一条带箭头的连线,表示张量流向。""" arrow = FancyArrowPatch((x1, y1), (x2, y2), arrowstyle="-|>", mutation_scale=14, linewidth=1.1, color="#2d2d2d") ax.add_patch(arrow) # 画布初始化 fig, ax = plt.subplots(figsize=(16, 10)) ax.set_xlim(0, 24) ax.set_ylim(0, 14) ax.axis("off") # Backbone 上层(从左到右) backbone_top = [ "Input 640x640x3", "CBS k3 s2 c64", "C2f c64 n3", "CBS k3 s2 c128", "C2f c128 n6", "CBS k3 s2 c256" ] # Backbone 下层(从右到左连接) backbone_bottom = [ "C2f c256 n6", "CBS k3 s2 c512", "C2f c512 n3", "SPPF c1024" ] # 绘制 Backbone 上层 x_pos = 1.0 for name in backbone_top: draw_module(ax, x_pos, 10.5, 2.0, 0.9, name, "#dbe9f6") x_pos += 2.4 # 绘制 Backbone 下层 x_pos = 1.0 for name in backbone_bottom: draw_module(ax, x_pos, 8.2, 2.0, 0.9, name, "#cfe5cf") x_pos += 2.4 # Neck 模块(示意) neck_names = [ "CBS 1x1 c512", "Upsample x2", "Concat", "C2f c512 n3", "CBS 1x1 c256", "Upsample x2", "Concat", "C2f c256 n3", "CBS 3x3 s2 c256", "Concat", "C2f c512 n3", "CBS 3x3 s2 c512", "Concat", "C2f c1024 n3" ] x_pos = 1.0 for name in neck_names: draw_module(ax, x_pos, 5.2, 1.7, 0.8, name, "#f5e2c8") x_pos += 1.9 # Head 三个分支 detect_names = ["Detect P3", "Detect P4", "Detect P5"] x_pos = 1.0 for name in detect_names: draw_module(ax, x_pos, 2.2, 2.2, 0.8, name, "#f2cccc") x_pos += 2.6 # 主干连线:Backbone 上层末尾 -> Backbone 下层开头 draw_arrow(ax, 13.0, 10.5, 13.0, 9.1) # 示意连线:Backbone 上层第二个 C2f 输出(P3)连到 Neck draw_arrow(ax, 2.2, 10.5, 2.2, 6.0) # 示意连线:Backbone 下层第二个 C2f 输出(P4)连到 Neck draw_arrow(ax, 8.2, 8.2, 8.2, 6.0) # 示意连线:Backbone 下层 SPPF 输出(P5)连到 Neck draw_arrow(ax, 13.0, 8.2, 13.0, 6.0) plt.savefig("yolov8_structure.png", dpi=150, bbox_inches="tight") plt.show()这个脚本的运行效果是一张按三行区域排布的结构图:上行是 Backbone 前半段,中行是 Backbone 后半段与 Neck,下行是检测头。三个区域的颜色不同,方便区分职责。注释里写清楚了每个函数的作用,后面要扩展也非常容易,比如给每个模块加上输出尺寸标注。
4.2 参数说明与样式调整
脚本里的几个关键参数会影响成图效果。figsize=(16, 10)控制整张图的宽高比例,如果你的模块名比较长,建议把宽度调大,比如 18,防止文字互相挤压。fontsize=8是模块内文字的默认字号,当模块数量多、方块变窄时,可以降到 7;如果模块少,可以调到 10,图更清晰。boxstyle="round,pad=0.02"让方块带圆角,这是个人审美问题,不需要在意。dpi=150决定导出 PNG 的清晰度,如果后续要放大打印,建议提到 300。
三个区域的填充颜色通过facecolor控制,我习惯用蓝、绿、橙分别表示 Backbone、Neck、Head,这样读者一眼能看出模块属于哪个部分。draw_arrow函数里的mutation_scale控制箭头大小。连线坐标是手动指定的,这也是这个脚本最需要耐心的地方:一旦模块坐标变了,箭头坐标必须跟着改。我的做法是先运行脚本,看输出的图里哪些箭头位置不对,再逐个微调坐标,反复两三轮就稳了。
4.3 画完怎么验证结构图没画错
结构图画完后,验证比画图更重要。我一般用三个方法。第一个方法是从头顺着连线走一遍,确认每个模块至少有一条输入和一条输出线。第二个方法是拿结构文件里的通道数逐层核对,从 640x640x3 开始,每过一个模块就把输出通道数更新一次,最后看 P3、P4、P5 的通道数是不是 256、512、1024。第三个方法是打印模型的每一层输出形状,和结构图上标的形状对比。最后这个方法最严格,能同时验证你对结构文件的理解和对张量流转的理解。只要这三个检查都通过,这张图就可以放心拿去用了。
5. 画YOLOv8结构图的常见坑:五个从翻车里总结的注意点
5.1 把yaml直接当成连接图,漏掉Concat的来路
现象:照着结构文件逐行画图,画到 Concat 时只画了一条输入线,因为结构文件里 Concat 那行看起来只有一个来源。 原因:结构文件里 Concat 的输入往往来自前面某个模块的输出,但文件名里不会直接列出这个“来自哪里”,而是靠代码里的路由逻辑决定的。没理解路由,就会漏画一条线。 解决:画 Concat 前,先在上一步的特征图列表里找到两个输入分别是谁。我习惯在结构文件旁边标注“本层输入来自哪一层”,再动手画。这条坑几乎每个新手都会踩,好在这个检查很简单。
5.2 把C2f的通道数当成一路直通,不知道内部是拼接
现象:画出来的 C2f 是一个单进单出的方块,输入 128 通道,输出 128 通道,看起来像是一个普通卷积。 原因:C2f 内部有分流、Bottleneck 堆叠、Concat 三个步骤,只看输入输出形状确实和普通卷积一样,但如果把它当成普通卷积,你就无法理解为什么 C2f 能提升梯度流动。 解决:至少在结构图的图例里注明“C2f 内部包含分流和拼接”,或者单独画一张 C2f 内部展开图。展开图不需要画所有 Bottleneck,只画两个方块并标注“重复 n 次”就够了。
5.3 主干画对了,颈部方向却画反了
现象:Backbone 画得完全正确,Neck 里却把“先上采样再融合”画成了“先下采样再融合”,整张图看着别扭但说不出哪里错。 原因:PAN 结构有两条通路,一条从深层往浅层做上采样融合,另一条再从浅层往深层做下采样融合。只记住“特征融合”却忘记方向,就会画反。 解决:在 Neck 区域画两个明确的大箭头,一个朝上(上采样路径),一个朝下(下采样路径),中间用 Concat 作为分界点。这样方向问题在视觉上就避开了。
5.4 P3/P4/P5的对应关系搞混,导致输出尺寸对不上
现象:结构图画完,检测头三个分支标注的输入尺寸分别是 80、40、20,但和 Backbone 的实际输出对应不上。 原因:P3 是较小的下采样倍数、对应较大的特征图 80x80;P5 是最大的下采样倍数、对应最小的特征图 20x20。数字越大特征图越小,这个反直觉关系特别容易记反。 解决:画图时用“下采样倍数”而不是“层号”来命名,比如标注“P3 / 8x down / 80x80”,三个信息同时出现,任何两个对上都算验证通过。
5.5 拿着旧结构的理解画新结构,图上出现“CBL”这种残留
现象:结构图上出现以前旧版 YOLO 的结构名,比如 CBL(Conv + BatchNorm + LeakyReLU),但 YOLOv8 用的是 CBS(Conv + BatchNorm + SiLU)。 原因:很多同学是从旧版迁移过来的,画图时肌肉记忆把激活函数写成了旧名字。 解决:画图前先确认每个简写对应的激活函数,CBS 里的“S”代表 SiLU,不要沿用旧习惯。这种小错误不影响整体结构理解,但在给别人讲的时候会明显露怯。
6. 拿着结构图去改模型:一个改C2f参数的完整例子
6.1 改n=3为n=2,输出通道为什么不变
结构图最直接的用途就是指导改模型。比如我想让模型更轻量,打算把 Backbone 里某个 C2f 的重复次数从 3 改成 2。在结构文件里,这个改动只是把 C2f 的参数n=3改成n=2。但是改完之前,你必须先理解一件事:这个改动不会改变输出通道数。因为 C2f 的输出通道由最后的 1x1 卷积决定,而那个卷积的输入是多个分支 Concat 的结果,分支数量不会因为 n 变了就变。换句话说,你把 n 从 3 改成 2,只是让内部 Bottleneck 少了两个,整体输入输出形状不变。
这个结论在结构图上是一目了然的。如果你没有结构图,很容易改完参数之后对着训练日志里的 shape 发愣:明明改了结构,为什么输出没变?现在你知道了,这不算翻车,而是 C2f 的设计本来如此。改完之后需要关注的是模型参数量和计算量变化,而不是输出形状变化。
6.2 改完先核对结构图再训练
改动任何结构参数之后,不要急着开训练,先做一次形状核对。我用的是一个小技巧:加载模型后把每一层的输出形状打印出来,拿它和结构图上标注的形状逐一对比。如果 C2f 的重复次数变化没有引起任何形状变化,说明结构图标注是对的;如果某层的形状变了,结构图上也要同步更新,否则下次看图的人会以为模型有 bug。这套“先核对再训练”的习惯能节省大量排查时间。
6.3 值得投入这个方向吗
画结构图这件事,看起来花时间,实际上是把未来所有调试时间压缩在前期的一次性投入里。我自己的习惯是,改网络结构之前先花半小时把结构图和形状表更新好,改完之后再花十分钟核对,这比之后出了问题对着报错日志猜原因要快得多。如果你打算长期做模型结构方面的工作,我建议把结构图当作代码的一部分来维护,每次改动都同步更新。这个方向绝对值得投入,它不解决某一个具体 bug,但它能让你在所有结构相关的工作里都不再瞎猜。希望这篇教程能帮到你,画图的过程本身就是最好的学习过程。
本文还有配套的精品资源,点击获取