YOLOv5 骨干网络替换为 ShuffleNetV2:从源码到落地的完整技术拆解
本博客基于
yolov5更换骨干网络为shufflenet项目真实代码撰写。所有模块定义、网络结构、通道数与 stride 均取自models/common.py、models/yolo.py、models/yolov5s_shufflenet.yaml原文
目录
- 为什么要换骨干:YOLOv5 在端侧部署的瓶颈
- ShuffleNetV2 为什么"轻"且"快":四条设计准则
- 源码拆解:common.py 新增的 3 个核心模块
- 模型解析器如何"认识"新模块(yolo.py)
- 新骨干网络结构逐层解析(yolov5s_shufflenet.yaml)
- 从零复现的改造步骤与训练
- 效果预期、代价与注意事项
- 小结
一、为什么要换骨干: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, InvertedResidualInvertedResidual构造签名(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 | 输出特征图 | 检测尺度 |
|---|---|---|---|---|---|
| 0 | CRM | 32 | 4 | P2/4 | — |
| 1 | InvertedResidual ×1 | 128 | 8 | — | — |
| 2 | InvertedResidual ×3 | 128 | 8 | P3/8 | 小目标 |
| 3 | InvertedResidual ×1 | 256 | 16 | — | — |
| 4 | InvertedResidual ×7 | 256 | 16 | P4/16 | 中目标 |
| 5 | InvertedResidual ×1 | 512 | 32 | — | — |
| 6 | InvertedResidual ×3 | 512 | 32 | P5/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 原值六、从零复现的改造步骤与训练
models/common.py:文件末尾追加CRM、channel_shuffle、InvertedResidual。models/yolo.py:顶部 import 和parse_model白名单注册CRM, InvertedResidual。- 新建
models/yolov5s_shufflenet.yaml:按第五节结构编写,nc改类别数。 - 训练:
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 显著下降;推理延迟降低;模型体积更小利于部署与量化。
必须警惕的代价:精度通常下降(小目标召回率可能降低);通道固定失去缩放灵活性;需重新调参(降学习率、增强数据增强)。
| 指标 | 原始 YOLOv5s | ShuffleNetV2 骨干版 |
|---|---|---|
| 骨干结构 | Focus + C3×3 + SPPF | CRM + InvertedResidual |
| 骨干计算强度 | 高(全量卷积 + CSP) | 低(分组 + 通道对半) |
| 参数 / FLOPs | 基线 | 预期显著降低(待实测) |
| mAP@0.5 | 基线 | 预期略降(待实测) |
| 推理延迟 | 基线 | 预期降低(待实测) |
八、小结
这是典型的**“换骨干实现模型轻量化”**工程:用CRM替代Focus做下采样,用InvertedResidual(通道对半 + channel shuffle)替代C3堆叠,保留 PANet 检测头,改动最小化。适合想把手头 YOLOv5 部署到低算力设备、又不想重写 pipeline 的开发者。
吃透这段代码的关键,是理解 ShuffleNetV2 的四条速度设计准则与channel_shuffle 的通道交叉机制——抓住这两点,这套替换逻辑可推广到任何基于Focus/C3的 YOLO 变体上。
下面是配套的网络结构可视化图(直接渲染):
下载链接:YOLOv5 骨干网络替换为 ShuffleNetV2