3. 学生模型设计
学生网络的设计,说白了就是给大模型找个「接班人」。这个接班人不能太笨,否则学不到精髓;也不能太聪明,否则还不如直接用大模型。我这些年踩过的坑,大多都跟这个平衡有关。
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 学生模型容量与蒸馏收益的关系
这个问题我琢磨了很久。简单说:容量太小,蒸馏没用;容量太大,蒸馏浪费。
我画了一张图来说明这个关系:
为什么会这样?我解释一下:
- 欠容量区:学生网络太简单,连教师的基础模式都拟合不了。蒸馏相当于「对牛弹琴」。
- 最佳容量区:学生刚好能学到教师的核心知识,同时保留自己的泛化能力。
- 过容量区:学生已经够强了,蒸馏带来的边际收益递减。这时候不如直接训练大模型。
经验数据: 我用 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 快速验证流程
设计新学生网络时,我建议先做快速验证:
- 参数量检查:确保在目标范围内(通常 1M~10M)
- FLOPs 估算:用 ptflops 或 thop 库计算
- 小规模蒸馏:在 CIFAR-100 上跑 50 个 epoch,看收益趋势
- 延迟测试:在目标设备上跑 1000 次,取平均
嗯,这套流程帮我筛掉了至少 5 个「看起来很美、用起来很坑」的设计。
最后说一句:学生网络设计没有银弹。最好的办法是理解原理,多做实验,积累自己的经验库。我到现在还在不断调整自己的设计模板。