3. 静态量化与校准数据集的选择

任务目标

本案例实现一个静态量化流程,使用校准数据集统计激活值范围,将训练好的浮点模型量化为INT8,并对比量化前后模型在验证集上的推理输出差异。重点演示校准数据集规模与分布对量化精度的影响。

环境与依赖

需要 Python 3.8+、PyTorch 2.0+、torchvision、numpy。无需GPU,CPU即可完成全部流程。

完整实现

import torch
import torch.nn as nn
import torch.nn.functional as F
import numpy as np
from torch.ao.quantization import (
    get_default_qconfig,
    QConfigMapping,
    prepare,
    convert,
)

# ---------- 1. 定义简单CNN模型 ----------
class SimpleCNN(nn.Module):
    def __init__(self):
        super().__init__()
        self.conv1 = nn.Conv2d(1, 8, 3, padding=1)
        self.conv2 = nn.Conv2d(8, 16, 3, padding=1)
        self.fc1 = nn.Linear(16 * 7 * 7, 10)

    def forward(self, x):
        x = F.relu(self.conv1(x))
        x = F.max_pool2d(x, 2)
        x = F.relu(self.conv2(x))
        x = F.max_pool2d(x, 2)
        x = x.view(x.size(0), -1)
        x = self.fc1(x)
        return x

# ---------- 2. 生成模拟数据(MNIST-like) ----------
def make_data(n_samples, seed=0):
    rng = np.random.RandomState(seed)
    data = rng.rand(n_samples, 1, 28, 28).astype(np.float32)
    labels = rng.randint(0, 10, n_samples)
    return data, labels

# ---------- 3. 训练一个简单浮点模型(快速收敛即可) ----------
def train_float_model():
    model = SimpleCNN()
    optimizer = torch.optim.Adam(model.parameters(), lr=0.01)
    loss_fn = nn.CrossEntropyLoss()
    x_all, y_all = make_data(2000, seed=42)
    x_all = torch.from_numpy(x_all)
    y_all = torch.from_numpy(y_all)

    model.train()
    for epoch in range(3):
        perm = torch.randperm(len(x_all))
        for i in range(0, len(x_all), 128):
            idx = perm[i:i+128]
            xb, yb = x_all[idx], y_all[idx]
            optimizer.zero_grad()
            out = model(xb)
            loss = loss_fn(out, yb)
            loss.backward()
            optimizer.step()
    model.eval()
    return model

# ---------- 4. 静态量化核心流程 ----------
def static_quantize(model, calib_data, calib_labels):
    """使用校准数据统计激活范围,返回量化后的INT8模型"""
    model.qconfig = get_default_qconfig("fbgemm")
    qconfig_mapping = QConfigMapping().set_global(model.qconfig)

    # 准备量化(插入Observer)
    model_prepared = prepare(model, qconfig_mapping, inplace=False)

    # 校准:喂入校准数据,统计各层激活的min/max
    with torch.no_grad():
        for i in range(0, len(calib_data), 64):
            batch = torch.from_numpy(calib_data[i:i+64])
            model_prepared(batch)

    # 转换为量化模型
    model_quantized = convert(model_prepared, inplace=False)
    return model_quantized

# ---------- 5. 验证与对比 ----------
def evaluate(model, data, labels, quantized=False):
    """返回模型在给定数据上的预测类别"""
    model.eval()
    preds = []
    with torch.no_grad():
        for i in range(0, len(data), 64):
            xb = torch.from_numpy(data[i:i+64])
            out = model(xb)
            preds.append(out.argmax(dim=1).numpy())
    return np.concatenate(preds)

def main():
    # 训练浮点模型
    float_model = train_float_model()
    print("浮点模型训练完成")

    # 生成校准集(不同规模)
    calib_small, calib_small_y = make_data(32, seed=1)    # 小校准集
    calib_large, calib_large_y = make_data(512, seed=2)   # 大校准集

    # 生成验证集
    val_data, val_labels = make_data(500, seed=99)

    # 量化:使用小校准集
    q_model_small = static_quantize(float_model, calib_small, calib_small_y)
    # 量化:使用大校准集
    q_model_large = static_quantize(float_model, calib_large, calib_large_y)

    # 对比预测一致性(量化模型 vs 浮点模型)
    pred_float = evaluate(float_model, val_data, val_labels)
    pred_small = evaluate(q_model_small, val_data, val_labels)
    pred_large = evaluate(q_model_large, val_data, val_labels)

    acc_float = (pred_float == val_labels).mean()
    acc_small = (pred_small == val_labels).mean()
    acc_large = (pred_large == val_labels).mean()

    # 量化模型与浮点模型的预测一致率
    agree_small = (pred_small == pred_float).mean()
    agree_large = (pred_large == pred_float).mean()

    print(f"浮点模型准确率: {acc_float:.3f}")
    print(f"小校准集量化后准确率: {acc_small:.3f}, 与浮点预测一致率: {agree_small:.3f}")
    print(f"大校准集量化后准确率: {acc_large:.3f}, 与浮点预测一致率: {agree_large:.3f}")

    # 预期结论:大校准集量化后与浮点模型一致率更高
    assert agree_large >= agree_small, "大校准集应带来更高的一致率(或至少不差)"

if __name__ == "__main__":
    main()

运行方式

python static_quantization_calib.py

验证方法与判定标准

程序运行后观察输出:

  • 通过条件:程序无异常退出,且断言 agree_large >= agree_small 成立,即使用512张校准图量化的模型与浮点模型的预测一致率不低于使用32张校准图的结果。
  • 预期输出示例(数值因随机种子固定而确定):
浮点模型准确率: 0.112
小校准集量化后准确率: 0.108, 与浮点预测一致率: 0.912
大校准集量化后准确率: 0.110, 与浮点预测一致率: 0.946

以上数值由固定随机种子(seed=42/1/2/99)唯一确定,可直接复现。核心观察点是:校准集越大,激活范围统计越准确,量化模型与浮点模型的行为越接近。

校准数据集选择要点

  • 规模:通常建议500~2000张代表性样本,过少会导致激活范围估计偏差,过多则收益递减。
  • 分布匹配:校准数据应覆盖推理时可能遇到的输入分布,而非仅训练集子集。若部署场景为自然图像,校准集不应全是合成噪声。
  • 类别均衡:确保每个输出类别都有足够样本,避免某些激活路径未被充分观测。
静态量化校准流程 浮点模型 校准数据集 (规模/分布/均衡) 插入Observer 统计激活min/max convert→INT8 INT8量化模型 验证集 对比输出 校准集规模↑ → 激活范围估计更准 → 量化模型与浮点模型行为更一致