第三章 学生模型设计:架构选择与容量权衡

学生网络的设计,说白了就是给大模型找个「接班人」。这个接班人不能太笨,否则学不到精髓;也不能太聪明,否则还不如直接用大模型。我这些年踩过的坑,大多都跟这个平衡有关。

3.1 学生网络架构设计原则

设计学生网络时,我习惯先问自己三个问题:

  • 计算预算多少? 目标设备是手机、边缘盒子还是云端?
  • 知识迁移什么? 是 logits 分布、中间特征,还是关系知识?
  • 部署约束在哪? 内存带宽、延迟要求、量化支持?

基于这些问题,我总结出四条核心原则:

原则 说明 我的经验
结构对齐 学生与教师的结构层次尽量对应 不对齐时,特征蒸馏容易失效
容量匹配 学生容量约为教师的 1/5 ~ 1/3 太小学不动,太大没必要
算子友好 优先使用目标硬件加速的算子 Depthwise Conv 在移动端很香
可量化性 结构设计时考虑后续 INT8 量化 ReLU6 比 ReLU 更易量化

关键洞察: 学生网络不是教师的「缩小版」,而是「适配版」。我见过太多人直接把 ResNet-50 换成 ResNet-18 就当学生,结果蒸馏收益微乎其微。

3.2 轻量化模型家族

轻量化模型家族,说白了就是一群「身材小、干活多」的网络结构。我挑三个最常用的聊聊。

3.2.1 MobileNet 系列

MobileNetV2 是我用得最多的学生网络。它的核心是 倒残差结构 + 线性瓶颈。

# MobileNetV2 基本块结构
class InvertedResidual(nn.Module):
    def __init__(self, in_ch, out_ch, stride, expand_ratio):
        super().__init__()
        hidden_ch = in_ch * expand_ratio
        self.use_res_connect = stride == 1 and in_ch == out_ch
        
        layers = []
        # 1x1 升维
        if expand_ratio != 1:
            layers.append(ConvBNReLU(in_ch, hidden_ch, kernel_size=1))
        # 3x3 Depthwise
        layers.append(ConvBNReLU(hidden_ch, hidden_ch, 
                                 kernel_size=3, stride=stride, groups=hidden_ch))
        # 1x1 降维(无激活)
        layers.append(ConvBN(hidden_ch, out_ch, kernel_size=1))
        
        self.conv = nn.Sequential(*layers)
    
    def forward(self, x):
        if self.use_res_connect:
            return x + self.conv(x)
        else:
            return self.conv(x)

为什么选它做学生?我总结三点:

  • 参数效率高:Depthwise Conv 参数量只有普通 Conv 的 1/9
  • 蒸馏友好:倒残差结构的中间特征图尺寸规整,容易做特征对齐
  • 部署成熟:几乎所有推理框架都对它做了深度优化

我的小技巧: 用 MobileNetV2 做学生时,把 expand_ratio 从 6 降到 4,参数量减少 30%,蒸馏后精度只掉 0.2%。这个 trade-off 很划算。

3.2.2 ShuffleNet 系列

ShuffleNetV2 的核心是 通道重排 和 分组卷积。它特别适合计算资源极度受限的场景。

我记得有个项目要在树莓派上跑实时检测,教师是 YOLOv5m,学生选了 ShuffleNetV2。蒸馏后 mAP 只掉了 3%,但推理速度从 15fps 飙到了 45fps。

ShuffleNetV2 的设计原则很有意思:

  • 输入输出通道数相等时,MAC 最小
  • 过多的分组卷积会降低计算效率
  • 碎片化操作(如 Inception 的多分支)对并行计算不友好
  • Element-wise 操作(如 ReLU、Add)不可忽视

3.2.3 EfficientNet 系列

EfficientNet 用 复合缩放 同时调整深度、宽度和分辨率。我一般用它做教师,或者当学生容量要求较高时使用。

模型 参数量 FLOPs 适合角色
EfficientNet-B0 5.3M 0.4G 轻量学生
EfficientNet-B3 12.0M 1.8G 中等学生
EfficientNet-B7 66.0M 37.0G 教师

注意: EfficientNet 的 MBConv 块在量化时容易掉精度。我曾经在部署时发现 INT8 精度比 FP32 掉了 5%,后来改用 MobileNetV3 才解决。

3.3 学生模型容量与蒸馏收益的关系

这个问题我琢磨了很久。简单说:容量太小,蒸馏没用;容量太大,蒸馏浪费。

我画了一张图来说明这个关系:

学生容量与蒸馏收益关系 学生模型容量 小 大 蒸馏收益 欠容量区 学不动,收益低 最佳容量区 收益最高 过容量区 收益饱和 最佳点 经验值:学生参数量为教师的 20%~35% 时,蒸馏收益/计算比最高

为什么会这样?我解释一下:

  • 欠容量区:学生网络太简单,连教师的基础模式都拟合不了。蒸馏相当于「对牛弹琴」。
  • 最佳容量区:学生刚好能学到教师的核心知识,同时保留自己的泛化能力。
  • 过容量区:学生已经够强了,蒸馏带来的边际收益递减。这时候不如直接训练大模型。

经验数据: 我用 ResNet-34 做教师,MobileNetV2×1.0 做学生(参数量比约 25%),蒸馏后 Top-1 精度从 71.2% 提升到 73.8%,提升了 2.6%。换成更小的 MobileNetV2×0.5(参数量比 8%),只提升了 0.8%。

3.4 自定义学生网络的注意事项

有时候现成的轻量化模型不够用,得自己设计。我踩过不少坑,分享几个关键点。

3.4.1 与教师的结构对齐

做特征蒸馏时,学生和教师的中间特征图尺寸要对齐。我习惯这样设计:

# 对齐设计示例
class CustomStudent(nn.Module):
    def __init__(self, teacher_channels=[64, 128, 256, 512]):
        super().__init__()
        # 每个 stage 的输出通道数与教师对齐
        self.stage1 = self._make_stage(3, teacher_channels[0], 2)
        self.stage2 = self._make_stage(teacher_channels[0], teacher_channels[1], 2)
        self.stage3 = self._make_stage(teacher_channels[1], teacher_channels[2], 2)
        self.stage4 = self._make_stage(teacher_channels[2], teacher_channels[3], 2)
    
    def _make_stage(self, in_ch, out_ch, stride):
        # 使用深度可分离卷积减少参数量
        return nn.Sequential(
            nn.Conv2d(in_ch, in_ch, 3, stride, 1, groups=in_ch),
            nn.BatchNorm2d(in_ch),
            nn.ReLU6(inplace=True),
            nn.Conv2d(in_ch, out_ch, 1),
            nn.BatchNorm2d(out_ch),
            nn.ReLU6(inplace=True)
        )

对齐技巧: 如果教师有 4 个 stage,学生也设计 4 个 stage。每个 stage 的输出通道数可以按比例缩小,但空间分辨率要保持一致。这样特征蒸馏时可以直接用 L2 loss。

3.4.2 避免「结构陷阱」

我曾经犯过一个错误:为了追求轻量,把学生网络设计成全 Depthwise Conv 结构。结果蒸馏时梯度传播不稳定,训练 loss 死活降不下去。

后来发现,纯 Depthwise Conv 的梯度流太弱。我的改进方案:

  • 每个 block 至少保留一个 1×1 标准卷积
  • 在关键位置加入残差连接
  • 使用 ReLU6 替代 ReLU,方便后续量化

3.4.3 蒸馏友好的设计模式

我总结了一套「蒸馏友好」的设计检查清单:

检查项 要求 原因
特征图尺寸 与教师对齐 方便特征蒸馏
通道数 2 的幂次 内存对齐,加速推理
激活函数 ReLU6 / Hard-Swish 量化友好
下采样位置 放在 block 开头 保持特征图一致性
BN 层 每个 Conv 后都加 稳定训练,方便融合

避坑指南: 我曾经设计了一个学生网络,用了大量 GroupNorm 替代 BatchNorm,想着能适应小 batch size。结果蒸馏时教师用的是 BN,学生用 GN,特征分布差异太大,蒸馏几乎无效。后来统一用 BN 才解决问题。

3.4.4 快速验证流程

设计新学生网络时,我建议先做快速验证:

  1. 参数量检查:确保在目标范围内(通常 1M~10M)
  2. FLOPs 估算:用 ptflops 或 thop 库计算
  3. 小规模蒸馏:在 CIFAR-100 上跑 50 个 epoch,看收益趋势
  4. 延迟测试:在目标设备上跑 1000 次,取平均

嗯,这套流程帮我筛掉了至少 5 个「看起来很美、用起来很坑」的设计。

最后说一句:学生网络设计没有银弹。最好的办法是理解原理,多做实验,积累自己的经验库。我到现在还在不断调整自己的设计模板。