3. 模型优化基础:量化、剪枝与蒸馏
模型优化,说白了就是给大模型「减负」。你想想看,一个几百亿参数的模型,直接扔到生产环境里做高并发推理,成本扛不住,延迟也扛不住。我这些年经手过的项目,十有八九都绕不开这三板斧:量化、剪枝、蒸馏。
今天咱们就把这三样东西掰开揉碎了讲。不只是讲原理,我会把我在实际项目中踩过的坑、试过的有效方法,一并交代清楚。
核心观点:模型优化的本质,是在「模型大小」和「推理精度」之间找平衡点。没有银弹,只有 trade-off。
3.1 量化:用更少的比特表示参数
量化是最直接、见效最快的优化手段。它的思路很简单:把模型参数从高精度浮点数(比如 FP32)映射到低精度整数(比如 INT8)。
为什么能这么做?因为神经网络对噪声有一定容忍度。参数从 32 位降到 8 位,丢失的那点精度,很多时候模型自己就消化了。
3.1.1 量化类型对比
| 量化类型 | 精度 | 模型大小缩减 | 推理加速比 | 适用场景 |
|---|---|---|---|---|
| FP32 → FP16 | 几乎无损 | 50% | 1.5x - 2x | GPU 推理,兼容性好 |
| FP32 → INT8 | 轻微损失 | 75% | 2x - 4x | CPU/GPU 推理,需校准 |
| FP32 → INT4 | 明显损失 | 87.5% | 3x - 6x | 边缘设备,需重训练 |
我个人习惯,线上服务优先上 FP16,几乎零成本。如果对延迟有硬性要求,再考虑 INT8。
3.1.2 量化感知训练 vs 训练后量化
这里有个关键选择:什么时候做量化?
- 训练后量化(PTQ):模型训练完,直接转。快,但精度损失可能大。
- 量化感知训练(QAT):训练过程中模拟量化效果,让模型提前适应。精度更好,但需要重新训练。
我的经验:如果模型本身比较大(比如 7B 以上),PTQ 通常够用。小模型或者对精度敏感的任务,老老实实上 QAT。
我曾经在一个 NLP 意图识别项目里,直接上 PTQ 转 INT8,结果准确率掉了 3 个点。后来换成 QAT,只掉了 0.5 个点。嗯,这个教训挺深刻的。
3.1.3 代码示例:PyTorch 量化
import torch
import torch.quantization as quant
# 训练后量化(PTQ)
model = load_model()
model.eval()
model.qconfig = quant.get_default_qconfig('fbgemm')
quant.prepare(model, inplace=True)
quant.convert(model, inplace=True)
# 量化感知训练(QAT)
model.qconfig = quant.get_default_qat_qconfig('fbgemm')
quant.prepare_qat(model, inplace=True)
# 正常训练循环...
quant.convert(model, inplace=True)
注意:量化后的模型,算子不一定都能在目标硬件上跑。一定要做端到端的 benchmark,别只看模型大小。
3.2 剪枝:去掉不重要的连接
剪枝的思路更直接:把模型里「不重要」的参数砍掉。神经网络里有很多冗余连接,去掉它们,模型变小,推理变快。
为什么会这样?你想想看,训练好的模型,很多权重值接近零。这些权重对最终输出的贡献微乎其微,留着纯属浪费。
3.2.1 剪枝策略
- 非结构化剪枝:把绝对值小的权重直接置零。简单,但稀疏矩阵在硬件上不好加速。
- 结构化剪枝:整行、整列、整个通道地砍。对硬件友好,但精度损失可能更大。
- 迭代剪枝:剪一点,训练恢复,再剪一点。效果最好,但耗时最长。
我记得有一次做图像分类模型优化,用非结构化剪枝砍掉了 60% 的参数,模型大小降下来了,但推理速度几乎没变。后来换成结构化剪枝,砍掉 30% 的通道,速度直接翻倍。所以,剪枝一定要考虑硬件特性。
3.2.2 剪枝流程
- 训练一个基线模型
- 评估每个权重/通道的重要性(比如用 L1 范数)
- 剪掉不重要的部分
- 微调恢复精度
- 重复步骤 2-4,直到达到目标
3.2.3 代码示例:基于权重大小的剪枝
import torch.nn.utils.prune as prune
# 对卷积层做 L1 非结构化剪枝,砍掉 30% 的权重
prune.l1_unstructured(module, name='weight', amount=0.3)
# 移除剪枝掩码,固化权重
prune.remove(module, 'weight')
建议:剪枝和量化可以组合使用。先剪枝,再量化,效果往往 1+1 > 2。
3.3 蒸馏:让学生模型模仿老师
知识蒸馏,是我个人觉得最有意思的优化方法。它不直接动大模型,而是训练一个小模型(学生)去模仿大模型(老师)的行为。
为什么小模型能学到大模型的能力?因为老师模型输出的概率分布里,包含了丰富的「暗知识」。比如分类猫和狗,老师不仅告诉学生「这是猫」,还告诉它「猫和老虎有点像,和狗不太像」。这些信息,硬标签给不了。
3.3.1 蒸馏的关键要素
| 要素 | 说明 |
|---|---|
| 老师模型 | 大而准,通常是 ensemble 或超大模型 |
| 学生模型 | 小而快,结构可以完全不同 |
| 温度参数 T | 控制概率分布的平滑程度,T 越大,暗知识越明显 |
| 蒸馏损失 | KL 散度 + 硬标签交叉熵,加权组合 |
3.3.2 蒸馏损失函数
import torch.nn.functional as F
def distillation_loss(student_logits, teacher_logits, labels, T=4.0, alpha=0.7):
# 软标签损失:KL 散度
soft_loss = F.kl_div(
F.log_softmax(student_logits / T, dim=-1),
F.softmax(teacher_logits / T, dim=-1),
reduction='batchmean'
) * (T * T)
# 硬标签损失:交叉熵
hard_loss = F.cross_entropy(student_logits, labels)
# 加权组合
return alpha * soft_loss + (1 - alpha) * hard_loss
关键点:温度 T 和权重 alpha 是超参数。我一般从 T=4、alpha=0.7 开始调。T 太小,暗知识出不来;T 太大,学生学到的全是噪声。
3.3.3 蒸馏的变体
- 离线蒸馏:先跑完老师,存下 logits,再训练学生。省资源,但老师不能更新。
- 在线蒸馏:老师和学生同时训练,互相学习。效果好,但训练成本高。
- 自蒸馏:自己当自己的老师。同一个模型,深层的输出教浅层的。
我曾经在一个推荐系统项目里用过自蒸馏。模型太大,线上延迟扛不住。用自蒸馏把深层知识教给浅层,模型小了 40%,AUC 只掉了 0.1%。嗯,这个方案后来成了团队的标配。
3.4 三种方法的对比与选择
| 方法 | 优势 | 劣势 | 最佳场景 |
|---|---|---|---|
| 量化 | 见效快,几乎不改模型结构 | 对硬件有依赖,INT8 精度可能下降 | GPU/CPU 推理,延迟敏感 |
| 剪枝 | 直接减小模型体积 | 非结构化剪枝加速效果有限 | 模型过大,需要压缩存储 |
| 蒸馏 | 可以换更小的模型结构 | 需要额外训练,流程复杂 | 有充足训练资源,追求极致加速 |
你想想看,实际项目中怎么选?我的建议是:
- 先试量化,成本最低
- 量化不够,加剪枝
- 还不行,上蒸馏
- 终极方案:量化 + 剪枝 + 蒸馏,三管齐下
避坑指南:我曾经在一个项目里同时上了三种优化,结果精度崩了。后来发现是剪枝和蒸馏的节奏没配合好。建议先做蒸馏,再剪枝,最后量化。顺序很重要。
3.5 本章知识体系
下面这张图,把三种优化方法的核心逻辑串起来了。你可以看到,它们最终都指向同一个目标:在精度可接受的范围内,让模型跑得更快、更省资源。
这张图把三种方法的关系理清楚了。量化、剪枝、蒸馏,各有各的适用场景,也各有各的坑。实际项目中,我建议你从量化入手,快速拿到收益,再根据瓶颈决定要不要上剪枝或蒸馏。
好了,这一章的内容就到这里。三种方法都讲透了,代码也给出来了,剩下的就是你在自己的项目里动手试试。记住,没有最好的方法,只有最适合你场景的方法。