第4章:权重量化(Quantization)——让模型瘦身还能跑得快

量化,说白了就是把模型里那些高精度的浮点数(FP32),换成低精度的整数(INT8、INT4)。为什么要这么干?因为浮点数在嵌入式设备上跑起来太慢了,而且占内存。我最早接触量化是在一个智能门锁项目上,芯片只有2MB的Flash,模型压缩前根本塞不进去。嗯,量化就是那个救场的技术。

4.1 量化原理:对称 vs 非对称

量化的核心就一句话:用更少的比特数表示原来的数值范围。但怎么映射,就有讲究了。

4.1.1 对称量化

对称量化,就是让0映射到0。权重和激活值的正负范围是对称的。公式很简单:

Q = round( x / scale )
scale = max(|x|) / (2^(b-1) - 1)

举个例子,FP32的权重范围是[-6.0, 6.0],量化到INT8(-127~127)。那么scale = 6.0 / 127 ≈ 0.0472。0.0映射到0,-6.0映射到-127,6.0映射到127。

我个人习惯:对称量化对权重效果不错,因为权重分布通常接近0对称。但激活值就不一定了,比如ReLU后的输出全是非负的,这时候用对称量化会浪费一半的表示范围。

4.1.2 非对称量化

非对称量化引入了零点(zero-point)的概念,可以偏移量化范围:

Q = round( x / scale ) + zero_point
scale = (max - min) / (2^b - 1)
zero_point = round( -min / scale )

这样,不管数据分布是正偏还是负偏,都能充分利用量化范围。我在一个语音唤醒模型上试过,激活值用非对称量化比对称量化精度高了0.3%,别小看这0.3%,在唤醒率上可能就是“听得见”和“听不见”的区别。

类型 优点 缺点 适用场景
对称量化 实现简单,计算快 非对称分布时浪费范围 权重、接近0对称的激活值
非对称量化 充分利用量化范围 需要额外存储零点 ReLU后的激活值、偏态分布

4.2 训练后量化(PTQ)—— 最快上手的方式

PTQ,就是模型训练完了,我再给它做量化。不需要重新训练,只需要一小部分校准数据。流程如下:

  1. 加载训练好的FP32模型
  2. 准备几百张校准图片(或几百条数据)
  3. 跑一遍前向推理,统计每层的min/max或直方图
  4. 根据统计结果计算scale和zero_point
  5. 把权重和激活值量化到INT8

代码示例(PyTorch + torch.quantization):

import torch
from torch.quantization import quantize_dynamic, QuantStub, DeQuantStub

# 定义模型时插入量化/反量化节点
class QuantModel(torch.nn.Module):
    def __init__(self):
        super().__init__()
        self.quant = QuantStub()
        self.conv = torch.nn.Conv2d(3, 64, 3)
        self.relu = torch.nn.ReLU()
        self.dequant = DeQuantStub()
    
    def forward(self, x):
        x = self.quant(x)
        x = self.conv(x)
        x = self.relu(x)
        x = self.dequant(x)
        return x

model = QuantModel()
model.eval()

# 准备校准数据(示例)
calibration_data = [torch.randn(1, 3, 224, 224) for _ in range(100)]

# 校准
model.qconfig = torch.quantization.get_default_qconfig('fbgemm')
torch.quantization.prepare(model, inplace=True)
for data in calibration_data:
    model(data)
torch.quantization.convert(model, inplace=True)

# 现在model已经是INT8量化模型了
print(model)

避坑指南:我曾经在一个项目里只用10张图片做校准,结果量化后精度掉了5%。后来改成200张,精度只掉了0.5%。校准数据量太少,统计出来的min/max不准确。建议至少100-500张,覆盖各种典型场景。

4.3 量化感知训练(QAT)—— 精度不够?那就练一练

PTQ虽然快,但有时候精度掉得厉害。尤其是小模型,比如MobileNetV2,量化后精度可能掉2-3%。这时候就得用QAT了。

QAT的原理:在训练过程中模拟量化误差,让模型自己学会适应。具体做法是在前向传播时插入fake_quant节点,把权重和激活值先量化再反量化,这样反向传播时梯度就能感知到量化误差。

import torch
from torch.quantization import FakeQuantize, MovingAverageMinMaxObserver

# 在训练中插入fake_quant
class QATModel(torch.nn.Module):
    def __init__(self):
        super().__init__()
        self.quant = torch.quantization.QuantStub()
        self.conv = torch.nn.Conv2d(3, 64, 3)
        self.relu = torch.nn.ReLU()
        self.dequant = torch.quantization.DeQuantStub()
    
    def forward(self, x):
        x = self.quant(x)
        x = self.conv(x)
        x = self.relu(x)
        x = self.dequant(x)
        return x

model = QATModel()
model.train()

# 配置QAT
model.qconfig = torch.quantization.get_default_qat_qconfig('fbgemm')
torch.quantization.prepare_qat(model, inplace=True)

# 正常训练循环
optimizer = torch.optim.SGD(model.parameters(), lr=0.001)
for epoch in range(10):
    for data, target in train_loader:
        optimizer.zero_grad()
        output = model(data)
        loss = torch.nn.functional.cross_entropy(output, target)
        loss.backward()
        optimizer.step()

# 训练完成后转为量化模型
model.eval()
torch.quantization.convert(model, inplace=True)

我建议:QAT的训练轮次不用太多,一般5-10个epoch就够了。因为模型已经收敛了,我们只是让它适应量化噪声。训练太多反而可能过拟合校准数据。

4.4 INT8/INT4量化实战

INT8是目前最主流的量化位宽,几乎所有的推理框架都支持。但INT4呢?它能把模型再压缩一半,但精度损失更大。

我在一个智能音箱项目上做过对比:

量化位宽 模型大小 推理速度(ms) 精度(Top-1)
FP32 45 MB 120 72.5%
INT8 11.5 MB 35 72.1%
INT4 6.2 MB 22 70.8%

INT8只掉了0.4%的精度,但INT4掉了1.7%。如果你的应用对精度要求极高,INT4要慎用。不过,如果模型本身很大(比如ResNet-152),INT4的精度损失反而会小一些,因为冗余度高。

注意:不是所有硬件都支持INT4。我在一个Cortex-M4芯片上试过,它只支持INT8的SIMD指令,INT4需要软件模拟,速度反而更慢。选型前一定要查清楚目标芯片的指令集支持情况。

4.5 精度损失如何补救

量化后精度掉了,怎么办?别慌,我有几个实战经验:

  • 混合精度量化:不是所有层都适合量化。比如第一层和最后一层,对精度影响最大,可以保留FP32。中间层用INT8。我在一个目标检测模型上试过,只把第一层和最后一层保留FP32,精度就恢复了1.2%。
  • 逐通道量化:默认是逐层量化(整个卷积层用一个scale),但不同通道的数值范围可能差异很大。逐通道量化(per-channel)给每个通道单独算scale,精度能提升0.3-0.5%。代价是计算量稍微增加。
  • 知识蒸馏+量化:先用大模型(Teacher)指导小模型(Student)训练,然后再量化。我在一个语音识别模型上试过,蒸馏后的模型量化精度比直接量化高了0.8%。
  • 量化后微调:量化完模型后,用少量数据再做几个epoch的微调。注意要冻结量化参数,只更新权重。这相当于让权重“适应”量化后的数值范围。

我曾经遇到一个极端情况:一个语义分割模型量化后,边界区域出现了大量噪点。后来发现是激活值的分布太宽,量化后细节丢失了。解决办法是把激活值做clip,限制到[-6, 6]范围内再量化,效果立竿见影。

4.6 知识体系总览

下面这张图总结了本章的核心逻辑,从量化原理到实战补救,一目了然:

权重量化知识体系 量化原理 对称量化(0映射到0) 非对称量化(引入零点) 量化方法 PTQ(训练后量化) QAT(量化感知训练) 混合精度/逐通道 精度补救:混合精度 | 知识蒸馏 | 量化后微调 | 激活值Clip

量化这条路,说白了就是用精度换速度。但怎么换得值,换得稳,就得靠经验了。我建议你从PTQ开始,如果精度达标就直接用。不达标再上QAT或混合精度。别一上来就搞复杂的,先跑通再说。


公众号:蓝海资料掘金营,微信deep3321