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 剪枝流程

  1. 训练一个基线模型
  2. 评估每个权重/通道的重要性(比如用 L1 范数)
  3. 剪掉不重要的部分
  4. 微调恢复精度
  5. 重复步骤 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 本章知识体系

下面这张图,把三种优化方法的核心逻辑串起来了。你可以看到,它们最终都指向同一个目标:在精度可接受的范围内,让模型跑得更快、更省资源。

模型优化知识体系 模型优化知识体系 模型优化目标 量化 剪枝 蒸馏 PTQ QAT 非结构化 结构化 离线蒸馏 在线蒸馏 核心原则 精度可接受的前提下,追求极致推理效率

这张图把三种方法的关系理清楚了。量化、剪枝、蒸馏,各有各的适用场景,也各有各的坑。实际项目中,我建议你从量化入手,快速拿到收益,再根据瓶颈决定要不要上剪枝或蒸馏。

好了,这一章的内容就到这里。三种方法都讲透了,代码也给出来了,剩下的就是你在自己的项目里动手试试。记住,没有最好的方法,只有最适合你场景的方法。