4、模型剪枝与蒸馏:结构化剪枝与非结构化剪枝、知识蒸馏原理、剪枝后微调策略
好,咱们进入第四章。这一章聊的是模型瘦身的两个核心手段——剪枝和蒸馏。说白了,就是给模型“减负”,让它跑得更快,同时尽量不掉精度。
我在实际项目中遇到过不少这样的场景:模型在GPU上跑得飞快,一部署到手机或者边缘设备上,延迟直接飙到不可接受。这时候,剪枝和蒸馏就是我最先考虑的两把刀。
4.1 结构化剪枝 vs 非结构化剪枝
剪枝,顾名思义,就是把模型里不重要的连接或者神经元砍掉。但怎么砍,有两种思路。
4.1.1 非结构化剪枝:细粒度,但硬件不友好
非结构化剪枝,是对单个权重下手。它会找出那些绝对值很小的权重,直接置为零。这样做的好处是,理论上可以砍掉90%甚至更多的参数,模型大小能大幅缩减。
但问题来了——硬件不买账。你想想看,一个权重矩阵里,这里一个零,那里一个零,稀疏得毫无规律。CPU和GPU的矩阵乘法库(比如cuBLAS)是为密集矩阵优化的,遇到这种稀疏矩阵,反而跑不快。我试过一次,剪了80%的参数,推理速度几乎没变,甚至更慢了。
代码示例(PyTorch风格):
# 非结构化剪枝:将绝对值小于阈值的权重置零
import torch
import torch.nn.utils.prune as prune
model = ... # 你的模型
# 对某个卷积层进行非结构化剪枝,剪掉50%的权重
prune.l1_unstructured(model.conv1, name='weight', amount=0.5)
# 此时 model.conv1.weight 中有一半是零
4.1.2 结构化剪枝:粗粒度,但加速明显
结构化剪枝就不一样了。它砍的是整个“结构”——比如一个卷积核(channel)、一个神经元、甚至一整层。砍完之后,矩阵还是密集的,只是尺寸变小了。
举个例子,一个卷积层原来有64个输出通道,你剪掉16个,剩下48个。那这层输出的特征图就变小了,后续层的计算量也跟着降。硬件跑起来,那是实打实的加速。
我个人习惯优先用结构化剪枝。虽然它砍得“粗”,精度损失可能比非结构化大一点,但换来的是实实在在的延迟降低。在实时系统里,延迟是硬指标,精度差一点可以微调补回来。
代码示例:
# 结构化剪枝:按通道重要性剪枝
def prune_channel(conv_layer, keep_ratio):
# 计算每个输出通道的L2范数
weight = conv_layer.weight.data # shape: [out_channels, in_channels, k, k]
l2_norm = torch.norm(weight.view(weight.size(0), -1), dim=1)
# 保留范数最大的 keep_ratio 个通道
num_keep = int(weight.size(0) * keep_ratio)
_, indices = torch.topk(l2_norm, num_keep)
# 只保留这些通道
conv_layer.weight.data = weight[indices]
# 别忘了处理bias
if conv_layer.bias is not None:
conv_layer.bias.data = conv_layer.bias[indices]
4.2 知识蒸馏原理
知识蒸馏,说白了就是“大模型教小模型”。大模型(教师网络)学到的知识,通过软标签(soft label)传递给小模型(学生网络)。
为什么软标签有用?你想想看,一个分类任务,硬标签是“猫=1,狗=0”。但教师网络输出的软概率可能是“猫=0.9,狗=0.08,兔子=0.02”。这个分布里包含了教师对类别间相似性的理解——猫和狗更像,猫和兔子差得远。学生网络学到这个分布,比只学硬标签要丰富得多。
我记得有一次做语音唤醒词识别,教师模型有500万参数,学生模型只有50万。直接训练学生模型,准确率只有85%。用蒸馏训练,准确率提到了92%。嗯,差距就是这么明显。
蒸馏的损失函数通常有两部分:
- 硬损失:学生输出与真实标签的交叉熵
- 软损失:学生输出与教师软标签的KL散度
公式长这样:
Loss = α * HardLoss(y_student, y_true) + (1-α) * SoftLoss(y_student, y_teacher, temperature)
这里的 temperature 是个超参数。温度越高,软标签的分布越平滑,学生能学到更多“暗知识”。我一般把温度设在4到8之间,效果比较稳。
4.3 剪枝后微调策略
剪枝完,模型精度肯定会掉。这时候不能直接部署,得微调(fine-tune)。微调不是从头训练,而是用原来的训练数据,再训几个epoch,把精度拉回来。
这里有几个关键点:
- 学习率要小:剪枝后的模型参数已经接近最优解了,学习率太大容易跑偏。我一般用原学习率的十分之一。
- 先剪枝再蒸馏:我的习惯是,先对教师模型做剪枝,得到一个“瘦身版教师”,再用这个瘦身教师去蒸馏学生模型。这样学生模型学到的知识更“接地气”。
- 迭代式剪枝:别想一次剪到位。我建议每次剪掉10%-20%的参数,然后微调几个epoch,看看精度恢复情况。再决定要不要继续剪。
| 策略 | 适用场景 | 我的建议 |
|---|---|---|
| 一次性剪枝 + 微调 | 模型较大,剪枝比例不高(<30%) | 简单粗暴,适合快速验证 |
| 迭代剪枝 + 微调 | 需要高压缩比(>50%) | 效果好,但耗时较长 |
| 剪枝 + 蒸馏联合 | 学生模型很小,精度要求高 | 我最常用的方案,推荐 |
嗯,这一章的内容就这些。剪枝和蒸馏,一个负责“砍”,一个负责“教”,搭配起来用,效果1+1>2。下一章我们会聊量化,那是另一个让模型跑得更快的利器。