ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

第23章:YOLOv5 骨干网络替换为 ShuffleNetV2

第23章:YOLOv5 骨干网络替换为 ShuffleNetV2

YOLOv5 骨干网络替换为 ShuffleNetV2:从源码到落地的完整技术拆解

本博客基于yolov5更换骨干网络为shufflenet项目真实代码撰写。所有模块定义、网络结构、通道数与 stride 均取自models/common.py、models/yolo.py、models/yolov5s_shufflenet.yaml原文

目录

  1. 为什么要换骨干:YOLOv5 在端侧部署的瓶颈
  2. ShuffleNetV2 为什么"轻"且"快":四条设计准则
  3. 源码拆解:common.py 新增的 3 个核心模块
  4. 模型解析器如何"认识"新模块(yolo.py)
  5. 新骨干网络结构逐层解析(yolov5s_shufflenet.yaml)
  6. 从零复现的改造步骤与训练
  7. 效果预期、代价与注意事项
  8. 小结

一、为什么要换骨干:YOLOv5 在端侧部署的瓶颈

YOLOv5s 默认骨干由Focus(切片下采样)、C3(CSP Bottleneck)与SPPF(空间金字塔池化)堆叠构成,精度高、生态成熟,但端侧存在两个结构性痛点:

  • 计算与参数量偏高:C3的 bottleneck 含两条并行的1x1 → 3x3路径,特征复用充分但 FLOPs 高;
  • 深层通道膨胀:骨干后段通道涨到 512/1024,对低算力设备的内存带宽(MAC)极不友好——而推理延迟恰恰由 MAC 主导,而非纸面 FLOPs。

因此部署到移动端 App、边缘盒子、嵌入式板卡时,工程上更倾向替换为面向速度设计的轻量骨干。ShuffleNetV2 正是这类骨干的代表。

二、ShuffleNetV2 为什么"轻"且"快":四条设计准则

ShuffleNetV2 作者提出关键观点:FLOPs 低 ≠ 实际跑得快。真实速度由内存访问成本(MAC)、并行度、算子碎片化与元素级操作共同决定。四条准则及在本项目代码中的落点:

准则内容项目代码中的落点
① 平衡通道输入/输出通道相等时 MAC 最小branch_features = oup // 2,两分支通道对半
② 慎用分组卷积分组数过高抬高 MAC仅 DW 卷积处分组,1x1 用普通卷积
③ 减少碎片化分支不宜过多每单元固定 2 分支(stride=1 时切分)
④ 减少元素级操作ReLU、add 越少越好仅保留必要 ReLU,无额外 add

最核心的机制是channel_shuffle(通道重排):把分组卷积后"各分支自说自话"的特征在通道维交叉打乱,强制信息在分组间流动,在保证效率的同时不损失表征能力。

三、源码拆解:common.py 新增的 3 个核心模块

3.1CRM—— 下采样入口(Conv + ReLU + MaxPool)

# Conv+Relu+MaxPool class CRM(nn.Module): def __init__(self, c1, c2, k=3, s=2): super(CRM, self).__init__() self.conv1 = nn.Sequential( nn.Conv2d(c1, c2, k, s, padding=1, bias=False), nn.BatchNorm2d(c2), nn.ReLU(inplace=True), ) self.mp = nn.MaxPool2d(kernel_size=3, stride=2, padding=1) def forward(self, x): res = self.mp(self.conv1(x)) return res

取代原版Focus作为输入下采样层:一个Conv3x3 stride=2+ 一个MaxPool3x3 stride=2,总下采样倍数 = 2×2 = 4,对应P2/4特征图。算子更简单、硬件友好,stride 节奏保持不变。

3.2channel_shuffle—— 通道重排

def channel_shuffle(x, groups): batchsize, num_channels, height, width = x.data.size() channels_per_group = num_channels // groups # (B, C, H, W) -> (B, g, c_per_g, H, W) x = x.view(batchsize, groups, channels_per_group, height, width) x = torch.transpose(x, 1, 2).contiguous() # (B, c_per_g, g, H, W) # 展平回 (B, C, H, W),实现分组间通道交叉 x = x.view(batchsize, -1, height, width) return x

将通道C重排为g × (C/g),转置后再展开——相邻分组的通道被交错打散,使下一次分组卷积能看到跨组信息。本项目固定groups=2。

3.3InvertedResidual—— 核心倒残差单元

class InvertedResidual(nn.Module): def __init__(self, inp, oup, stride): super(InvertedResidual, self).__init__() if not (1 <= stride <= 3): raise ValueError('illegal stride value') self.stride = stride branch_features = oup // 2 assert (self.stride != 1) or (inp == branch_features << 1) if self.stride > 1: self.branch1 = nn.Sequential( # 下采样旁路:3x3 DW(stride) -> 1x1 self.depthwise_conv(inp, inp, kernel_size=3, stride=self.stride, padding=1), nn.BatchNorm2d(inp), nn.Conv2d(inp, branch_features, kernel_size=1, stride=1, padding=0, bias=False), nn.BatchNorm2d(branch_features), nn.ReLU(inplace=True), ) self.branch2 = nn.Sequential( # 主分支:1x1 -> 3x3 DW -> 1x1 nn.Conv2d(inp if (self.stride > 1) else branch_features, branch_features, kernel_size=1, stride=1, padding=0, bias=False), nn.BatchNorm2d(branch_features), nn.ReLU(inplace=True), self.depthwise_conv(branch_features, branch_features, kernel_size=3, stride=self.stride, padding=1), nn.BatchNorm2d(branch_features), nn.Conv2d(branch_features, branch_features, kernel_size=1, stride=1, padding=0, bias=False), nn.BatchNorm2d(branch_features), nn.ReLU(inplace=True), ) @staticmethod def depthwise_conv(i, o, kernel_size, stride=1, padding=0, bias=False): return nn.Conv2d(i, o, kernel_size, stride, padding, bias=bias, groups=i) def forward(self, x): if self.stride == 1: x1, x2 = x.chunk(2, dim=1) # 通道对半切分 out = torch.cat((x1, self.branch2(x2)), dim=1) # 一半直连,一半过主分支 else: out = torch.cat((self.branch1(x), self.branch2(x)), dim=1) # 双分支均处理 out = channel_shuffle(out, 2) # 通道重排 return out

两种 stride,两种结构:

  • stride = 1(不降采样):输入通道对半切 →x1直接短路、x2走1x1 → 3x3 DW → 1x1→ 两半拼接 →channel_shuffle。正是准则①"平衡通道"的直接体现——一半恒等映射、一半特征变换,信息复用且省算力。
  • stride > 1(下采样):两个分支都做特征提取,branch1(3x3 DW stride + 1x1)负责降分辨率,branch2负责主特征,最后拼接 + 重排。

边界条件:stride=1 时断言inp == branch_features << 1(即inp == oup),保证通道对半切分合法;stride 仅允许 1~3,超范围raise ValueError。

四、模型解析器如何"认识"新模块(yolo.py)

光有模块还不够,parse_model必须知道通道推导规则。改动两处:

# yolo.py 顶部导入 CRM, InvertedResidual, # 引入 shufflenet # parse_model 的可推导模块集合中加入两者 CRM, InvertedResidual

InvertedResidual构造签名(inp, oup, stride)与Conv家族一致(c1, c2 = ch[f], args[0])。注册后 YAML 中[InvertedResidual, [128, 2]]即解析为"输入通道继承上一层、输出 128、stride 2",再经make_divisible(c2 * gw, ch_mul)归一化(此处width_multiple=1,通道保持原值)。

五、新骨干网络结构逐层解析(yolov5s_shufflenet.yaml)

替换后骨干完全去掉Focus/C3/SPPF,仅保留 7 层:

backbone: [ [-1, 1, CRM, [32]], # 0-P2/4 conv3x3 + maxpool,stride=4 [-1, 1, InvertedResidual, [128, 2]], # 1 128ch,stride 4→8 [-1, 3, InvertedResidual, [128, 1]], # 2-P3/8 3× 不降采样单元 [-1, 1, InvertedResidual, [256, 2]], # 3 256ch,stride 8→16 [-1, 7, InvertedResidual, [256, 1]], # 4-P4/16 7× 不降采样单元 [-1, 1, InvertedResidual, [512, 2]], # 5 512ch,stride 16→32 [-1, 3, InvertedResidual, [512, 1]], # 6-P5/32 3× 不降采样单元 ]
层模块输出通道stride输出特征图检测尺度
0CRM324P2/4—
1InvertedResidual ×11288——
2InvertedResidual ×31288P3/8小目标
3InvertedResidual ×125616——
4InvertedResidual ×725616P4/16中目标
5InvertedResidual ×151232——
6InvertedResidual ×351232P5/32大目标

head部分原样沿用 YOLOv5 v6.0 的 PANet(1x1 通道对齐 → 上采样 → Concat → C3 融合),最后由Detect在 P3/P4/P5 三层输出。改造面被严格控制在 backbone。

nc: 20 # 类别数改为 20(按你的数据集) depth_multiple: 1 # 深度不缩放,模块重复次数按 yaml 原值 width_multiple: 1 # 宽度不缩放,通道数按 yaml 原值

六、从零复现的改造步骤与训练

  1. models/common.py:文件末尾追加CRM、channel_shuffle、InvertedResidual。
  2. models/yolo.py:顶部 import 和parse_model白名单注册CRM, InvertedResidual。
  3. 新建models/yolov5s_shufflenet.yaml:按第五节结构编写,nc改类别数。
  4. 训练:
python train.py --cfg models/yolov5s_shufflenet.yaml \ --data your_dataset.yaml \ --epochs 300 --batch-size 16 --imgsz 640 \ --device 0

注:仓库中train.py、utils/dataloaders.py也出现与 shufflenet 相关字段(推测为标签/推理适配),如训练报错请优先核对这两处改动。

七、效果预期、代价与注意事项

可预期的收益(需实测):参数与 FLOPs 显著下降;推理延迟降低;模型体积更小利于部署与量化。

必须警惕的代价:精度通常下降(小目标召回率可能降低);通道固定失去缩放灵活性;需重新调参(降学习率、增强数据增强)。

指标原始 YOLOv5sShuffleNetV2 骨干版
骨干结构Focus + C3×3 + SPPFCRM + InvertedResidual
骨干计算强度高(全量卷积 + CSP)低(分组 + 通道对半)
参数 / FLOPs基线预期显著降低(待实测)
mAP@0.5基线预期略降(待实测)
推理延迟基线预期降低(待实测)

八、小结

这是典型的**“换骨干实现模型轻量化”**工程:用CRM替代Focus做下采样,用InvertedResidual(通道对半 + channel shuffle)替代C3堆叠,保留 PANet 检测头,改动最小化。适合想把手头 YOLOv5 部署到低算力设备、又不想重写 pipeline 的开发者。

吃透这段代码的关键,是理解 ShuffleNetV2 的四条速度设计准则与channel_shuffle 的通道交叉机制——抓住这两点,这套替换逻辑可推广到任何基于Focus/C3的 YOLO 变体上。


下面是配套的网络结构可视化图(直接渲染):

下载链接:YOLOv5 骨干网络替换为 ShuffleNetV2

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进