3、模型量化入门:为什么需要量化?量化原理(FP32到INT8)、训练后量化(Post-training Quantization)、量化感知训练(QAT)简介

好,咱们进入第三讲。说实话,量化这个话题,是很多嵌入式AI新手的第一道坎。我记得刚入行那会儿,看到“量化”两个字,第一反应是——这玩意儿是不是把模型给“压缩”了?其实没那么玄乎。

今天我就带你把这层窗户纸捅破。咱们从三个角度聊:为什么非得量化FP32到INT8到底发生了什么、以及两种主流量化方法——训练后量化(PTQ)和量化感知训练(QAT)。

3.1 为什么需要量化?——MCU的“内存焦虑”

你想想看,一个典型的TinyML模型,比如MobileNet V1,FP32版本大概4.3MB。听起来不大?但很多MCU的Flash只有512KB甚至256KB。4.3MB塞进去?门都没有。

更关键的是RAM。FP32模型推理时,中间激活值动不动就几百KB。而Cortex-M4内核的MCU,SRAM通常只有128KB~256KB。跑一次推理,内存直接爆掉。

我遇到过最惨的一次,是在STM32F407上部署一个语音关键词识别模型。模型本身1.2MB,Flash勉强够。但一跑推理,SRAM直接溢出,系统反复重启。后来量化到INT8,模型体积降到300KB,RAM占用也砍了一半。嗯,这才跑起来。

所以量化解决的核心问题就三个:

  • 模型体积缩小:FP32到INT8,直接缩到1/4
  • 内存占用降低:中间激活值也变小了
  • 推理速度提升:INT8运算比FP32快2~4倍(很多MCU有硬件加速)

核心结论:量化不是可选项,而是MCU部署的必选项。不量化,你连模型都塞不进去。

3.2 量化原理:FP32到INT8,到底发生了什么?

说白了,量化就是把一个高精度的数值范围,映射到一个低精度的数值范围。FP32是32位浮点数,范围大约是±3.4×10³⁸。INT8是8位有符号整数,范围只有-128到127。

怎么映射?核心公式就一个:

q = round(r / S) + Z

其中:

  • r:原始FP32值
  • q:量化后的INT8值
  • S:缩放因子(Scale),FP32的小数
  • Z:零点偏移(Zero Point),INT8的整数

反过来,反量化公式:

r = (q - Z) × S

举个例子。假设我们有一组FP32权重,范围是[-1.0, 1.0]。要量化到INT8的[-128, 127]:

S = (1.0 - (-1.0)) / (127 - (-128)) = 2.0 / 255 ≈ 0.00784
Z = round(0 - (-1.0) / 0.00784) = round(127.55) = 128

那么FP32的0.5,量化后就是:

q = round(0.5 / 0.00784) + 128 = round(63.78) + 128 = 64 + 128 = 192

你看,就这么简单。但这里有个坑——精度损失。因为round操作会引入误差。我刚开始做量化时,总觉得误差无所谓。直到有一次,一个分类模型量化后准确率从95%掉到了82%。排查了半天,发现是某个层的权重范围特别大,导致量化步长S太大,精度损失严重。

避坑指南:量化误差主要来自两个地方——截断误差(超出INT8范围的值被截断)和舍入误差(round操作)。对于权重分布特别不均匀的层,建议单独统计范围,不要用全局统计。

3.3 训练后量化(Post-training Quantization)

PTQ,顾名思义,就是模型训练完之后再做量化。这是最省事的方法,也是我推荐新手入门的方式。

具体流程:

  1. 训练一个FP32模型(正常训练流程)
  2. 准备一小部分校准数据(通常几百张图片或几十段音频)
  3. 用校准数据跑一遍推理,统计每层的激活值范围(min, max)
  4. 根据统计结果计算每层的S和Z
  5. 将权重和激活值都量化到INT8

在TFLite Micro中,PTQ的实现非常简单:

import tensorflow as tf

# 加载FP32模型
converter = tf.lite.TFLiteConverter.from_saved_model('model_fp32')

# 设置量化优化
converter.optimizations = [tf.lite.Optimize.DEFAULT]

# 提供校准数据集(代表性数据)
def representative_dataset():
    for _ in range(100):
        data = np.random.randn(1, 224, 224, 3).astype(np.float32)
        yield [data]

converter.representative_dataset = representative_dataset

# 转换到INT8
tflite_model = converter.convert()

这段代码里,representative_dataset是关键。它提供的数据要能代表真实推理时的输入分布。我见过有人随便传了一堆噪声数据,结果量化后模型直接废了。

我的经验:校准数据最好从训练集或验证集中随机抽取,数量不用太多,200~500个样本就够。多了反而可能过拟合到校准集上。

PTQ的优点很明显:快、简单、不需要重新训练。但缺点也很突出:精度损失可能较大,尤其是对激活值范围敏感的网络(比如MobileNet系列)。

3.4 量化感知训练(Quantization-aware Training)

QAT,说白了就是在训练过程中模拟量化误差,让模型学会“忍受”量化带来的精度损失。

具体做法:

  1. 在FP32训练的前向传播中,插入伪量化节点(Fake Quantization)
  2. 这些节点模拟量化-反量化过程(即:FP32→INT8→FP32)
  3. 反向传播时,梯度仍然用FP32计算(因为量化操作不可导)
  4. 训练完成后,移除伪量化节点,导出真正的INT8模型

在TFLite中,QAT的实现需要先训练一个带伪量化节点的模型:

import tensorflow_model_optimization as tfmot

# 在原始模型上应用QAT
quantize_model = tfmot.quantization.keras.quantize_model

# 假设model是你的FP32模型
qat_model = quantize_model(model)

# 正常训练(学习率可以调小一点)
qat_model.compile(optimizer='adam', loss='categorical_crossentropy')
qat_model.fit(x_train, y_train, epochs=5)

# 导出TFLite模型
converter = tf.lite.TFLiteConverter.from_keras_model(qat_model)
converter.optimizations = [tf.lite.Optimize.DEFAULT]
tflite_model = converter.convert()

QAT的精度通常比PTQ高1%~3%。我做过一个对比实验:在CIFAR-10上,ResNet-20的PTQ精度掉了2.1%,而QAT只掉了0.3%。

什么时候用QAT? 当PTQ精度损失超过可接受范围(比如>1%),或者模型对量化特别敏感(比如有大量BatchNorm层)时,建议上QAT。

但QAT也有代价:训练时间变长(因为前向传播多了量化模拟),而且需要重新训练。如果你的项目时间紧,PTQ够用就别折腾QAT。

3.5 PTQ vs QAT:怎么选?

对比维度 PTQ(训练后量化) QAT(量化感知训练)
实现难度 低(几行代码搞定) 中(需要修改训练流程)
精度损失 较大(通常0.5%~3%) 较小(通常0.1%~1%)
训练时间 不需要额外训练 需要额外训练(约1.5倍时间)
适用场景 快速原型、精度要求不高 精度敏感、生产部署
校准数据需求 需要少量校准数据 不需要(训练时已学习)

我个人习惯是:先跑PTQ看看精度。如果掉点超过1%,再考虑QAT。毕竟QAT要重新训练,时间成本摆在那。

嗯,这一讲就到这。量化入门其实不难,关键是理解“映射”的本质。下一讲咱们会深入TFLite Micro的量化工具链,手把手教你如何把量化后的模型部署到MCU上。

课后小作业:找一个你手头的FP32模型,用PTQ量化到INT8,对比一下模型体积和推理精度的变化。你会发现,量化后的模型体积只有原来的1/4,但精度可能只掉了不到1%。这就是量化的魅力。