3、量化核心概念:定点数与浮点数、量化尺度(scale)与零点(zero point)、对称量化与非对称量化、量化精度(int8/uint8/int16)

在进入 TensorFlow Lite 训练后量化(PTQ)实战之前,必须先建立一套精确的“数值语言”。深度学习模型在训练时几乎全部使用 32 位浮点数(FP32)来存储权重和激活值,而部署到移动端、边缘设备或嵌入式芯片时,FP32 的存储开销和计算延迟往往难以承受。量化的本质,就是找到一种从 FP32 到低比特整数(如 int8)的映射规则,同时尽量保留原始张量的信息。本章将围绕四个核心概念展开:定点数与浮点数的区别、量化尺度(scale)与零点(zero point)的数学含义、对称量化与非对称量化的适用场景,以及 int8/uint8/int16 三种精度的选型依据。理解这些概念,是后续掌握 TensorFlow Lite 转换 API、校准数据集选择和精度调优的基石。

量化核心概念关系图 浮点数 (FP32) 动态范围大,精度高 存储 4 字节,计算慢 训练阶段标准格式 → 部署时需压缩 定点数 (int8) 动态范围有限,精度低 存储 1 字节,计算快 推理阶段高效格式 → 需量化映射 量化映射公式 q = round(r / scale) + zero_point r = (q - zero_point) × scale scale: 缩放因子(浮点数) zero_point: 零点偏移(整数) 对称量化 zero_point = 0 适合权重(分布近似对称) 实现简单,计算高效 非对称量化 zero_point ≠ 0 适合激活值(分布偏置) 精度更高,但多一次偏移运算 精度选择 int8: 通用首选 uint8: 旧硬件兼容 | int16: 高精度场景

3.1 定点数与浮点数:从连续世界到离散世界的映射

浮点数(Floating Point)用科学计数法表示实数,由符号位、指数位和尾数位组成,其动态范围极大,可以表示从 1e-38 到 3.4e38 的数值(FP32)。这种灵活性让训练过程中的梯度更新和激活值传播变得非常稳定。然而,浮点运算需要专门的硬件单元(FPU),在低功耗嵌入式芯片上,FPU 要么不存在,要么速度远低于整数运算单元。定点数(Fixed Point)则是一种将数值范围预先固定好的表示方法,例如 int8 只能表示 -128 到 127 的整数。量化并不是简单地把浮点数四舍五入成整数,而是需要先确定一个缩放因子(scale),将浮点数的实际范围映射到整数范围内。

核心原理:量化过程可以看作是一个“归一化 + 取整”的过程。假设一个张量的取值范围是 [r_min, r_max],我们需要找到一个 scale 和 zero_point,使得浮点数 r 映射到整数 q 后,误差最小。公式为:q = round(r / scale) + zero_point。其中 scale 是浮点数,表示每个整数单位代表的实际数值大小;zero_point 是整数,表示浮点数 0 映射到哪个整数。

在实际操作中,判断一个张量适合用定点数还是浮点数,主要看两个指标:动态范围和精度需求。如果张量的数值分布非常集中(例如某个卷积层的权重在 -0.1 到 0.1 之间),用 FP32 存储会造成大量浪费,此时用 int8 配合合适的 scale 就能很好地覆盖。反之,如果张量存在极端离群值(outlier),直接量化会导致整体精度急剧下降。

属性浮点数 (FP32)定点数 (int8)
表示范围约 ±3.4e38-128 ~ 127
存储大小4 字节1 字节
计算速度慢(需 FPU)快(整数 ALU)
精度高(约 7 位有效数字)低(约 2-3 位有效数字)
适用场景训练、高精度推理边缘部署、实时推理

一个常见的误区是认为“量化就是简单地除以最大值再乘以 127”。这种做法忽略了零点偏移,会导致浮点数 0 在量化后不再对应整数 0,从而在 padding 或 ReLU 等操作中引入偏差。正确的做法必须显式地计算 zero_point,确保浮点 0 被精确映射到某个整数,这样在推理时,任何与 0 相关的运算(如 padding 填充)都能保持一致性。

3.2 量化尺度(scale)与零点(zero point):映射的数学基础

scale 和 zero_point 是量化参数的核心。scale 是一个浮点数,它决定了量化后的“步长”。例如,如果一个张量的范围是 [0.0, 6.0],使用 int8 非对称量化,那么 scale = (6.0 - 0.0) / (255 - 0) ≈ 0.02353。这意味着量化后的每个整数单位代表 0.02353 的浮点数值。zero_point 则是浮点值 0 对应的整数。在上述例子中,zero_point = 0,因为浮点 0 恰好落在整数 0 上。但如果张量范围是 [-3.0, 3.0],使用 int8 对称量化,scale = 3.0 / 127 ≈ 0.02362,zero_point = 0。

判断 scale 和 zero_point 是否设置合理,可以通过一个简单的“往返测试”来验证:将浮点数 r 量化得到 q,再反量化得到 r',计算 r 与 r' 的误差。如果误差在可接受范围内(例如 < 1%),则参数合理。在 TensorFlow Lite 中,转换器会自动计算这些参数,但理解其原理有助于手动调试。

# 伪代码:手动计算 scale 和 zero_point
def compute_quant_params(r_min, r_max, num_bits=8):
    q_min = -(1 << (num_bits - 1))  # int8: -128
    q_max = (1 << (num_bits - 1)) - 1  # int8: 127
    scale = (r_max - r_min) / (q_max - q_min)
    zero_point = q_min - round(r_min / scale)
    # 确保 zero_point 在 [q_min, q_max] 范围内
    zero_point = max(q_min, min(q_max, zero_point))
    return scale, zero_point

实战中,TensorFlow Lite 的 representative_dataset 会收集激活值的分布,然后通过最小化 KL 散度或均方误差来确定最优的 scale 和 zero_point。这里有一个关键点:scale 和 zero_point 是逐张量(per-tensor)还是逐通道(per-channel)的? 对于权重,通常使用 per-channel 量化,即每个输出通道有自己的 scale 和 zero_point,这样可以显著减少量化误差。对于激活值,由于计算复杂,通常使用 per-tensor 量化。

经验提醒:当你在 TensorFlow Lite 中看到精度下降明显时,优先检查权重是否启用了 per-channel 量化。默认情况下,TFLite 对权重使用 per-channel,但对激活值使用 per-tensor。如果激活值分布极不均匀,可以考虑使用 int16 激活值量化,它提供了更大的动态范围。

3.3 对称量化与非对称量化:如何选择映射策略

对称量化强制 zero_point = 0,即浮点数的 0 映射到整数的 0。这种方式的优点是计算简单,因为反量化公式变为 r = q × scale,不需要额外的加法运算。对称量化非常适合权重,因为训练后的权重分布通常近似于以 0 为中心的高斯分布,正负范围大致对称。然而,对于激活值(例如 ReLU 的输出),其分布往往是非负的,且集中在某个正区间内。如果使用对称量化,会导致一半的整数范围(负数部分)被浪费,从而降低精度。

非对称量化允许 zero_point 不为 0,可以充分利用整个整数范围。例如,ReLU 输出的激活值范围是 [0.0, 6.0],使用非对称量化可以将 [0, 255] 全部用于表示正数,scale 更小,精度更高。但代价是推理时需要多一次加法运算(q - zero_point)。在 TensorFlow Lite 中,默认对激活值使用非对称量化,对权重使用对称量化(per-channel)。

对比维度对称量化非对称量化
zero_point固定为 0可调,通常非 0
计算复杂度低(无偏移加法)高(需处理偏移)
适用张量权重(分布对称)激活值(分布偏置)
精度表现对偏置分布浪费精度充分利用整数范围
TFLite 默认权重激活值

一个典型的实战场景是:假设你有一个 MobileNetV2 模型,其第一个卷积层的权重分布范围是 [-0.5, 0.5],而激活值经过 ReLU6 后范围是 [0.0, 6.0]。如果对两者都使用对称量化,权重的 scale = 0.5/127 ≈ 0.00394,激活值的 scale = 6.0/127 ≈ 0.0472。激活值的量化步长过大,会导致信息丢失。改用非对称量化后,激活值的 scale = 6.0/255 ≈ 0.0235,精度提升了一倍。这就是为什么 TFLite 默认对激活值采用非对称量化的原因。

风险提示:不要盲目地对所有层使用非对称量化。对于某些硬件加速器(如 NPU),对称量化可以简化硬件设计,提高吞吐量。如果你的部署目标硬件只支持对称量化,那么你需要接受激活值精度损失,或者通过调整网络结构(如加入 BN 层)来使激活值分布更对称。

3.4 量化精度:int8、uint8 与 int16 的选型策略

TensorFlow Lite 支持多种量化精度,最常用的是 int8(有符号 8 位整数),范围 -128 到 127。int8 是大多数边缘设备的首选,因为它存储小、计算快,且精度损失通常可以控制在 1-2% 以内。uint8(无符号 8 位整数)范围 0 到 255,在早期的 TensorFlow Lite 版本中较为常见,主要用于兼容一些旧的硬件 DSP。uint8 的优点是能表示更大的正数范围,但缺点是无法直接表示负数,因此通常需要配合 zero_point 进行偏移。

int16 量化则提供了更高的精度,其范围是 -32768 到 32767。int16 的 scale 比 int8 小 256 倍,因此量化误差显著降低。但代价是存储翻倍(2 字节),计算速度也慢于 int8。int16 通常用于对精度要求较高的场景,例如语音识别模型或超分辨率模型。在 TensorFlow Lite 中,可以通过设置 inference_input_type 和 inference_output_type 来指定量化精度。

# TensorFlow Lite 转换器设置量化精度
converter = tf.lite.TFLiteConverter.from_keras_model(model)
converter.optimizations = [tf.lite.Optimize.DEFAULT]
converter.representative_dataset = representative_dataset_gen
converter.target_spec.supported_ops = [tf.lite.OpsSet.TFLITE_BUILTINS_INT8]
converter.inference_input_type = tf.int8  # 或 tf.uint8 / tf.int16
converter.inference_output_type = tf.int8
tflite_model = converter.convert()

选择精度的判断流程如下:首先,尝试 int8 量化,评估精度损失。如果精度损失小于 1%,则直接采用 int8。如果精度损失在 1-5% 之间,可以尝试 int16 激活值量化(权重仍用 int8),或者使用 per-channel 量化。如果精度损失超过 5%,则需要考虑量化感知训练(QAT),或者检查是否有离群值需要裁剪。

案例拆解:假设我们有一个用于工业缺陷检测的 CNN 模型,输入为 224x224x3 的 RGB 图像。原始 FP32 模型在验证集上的 mAP 为 0.92。使用 int8 量化后,mAP 降至 0.87,损失 5.4%,不可接受。分析发现,模型的最后一层全连接层的激活值分布存在少量极端值(最大达到 12.0),而大部分值集中在 [0, 2.0] 区间。使用 int8 非对称量化时,scale = 12.0/255 ≈ 0.047,导致 [0, 2.0] 区间内的量化步长过大。解决方案:将最后一层改为 int16 量化(仅该层),其他层保持 int8。重新评估后,mAP 回升至 0.91,模型大小仅增加 2%。

另一个常见误区是认为 uint8 比 int8 精度更高,因为 255 比 127 大。实际上,精度取决于 scale 的大小,而不是整数的绝对值。如果浮点范围是 [-1.0, 1.0],int8 的 scale = 2.0/255 ≈ 0.00784,而 uint8 需要将范围偏移到 [0, 2.0],scale = 2.0/255 ≈ 0.00784,两者精度相同。uint8 的唯一优势是能表示更大的正数范围,但代价是无法直接表示负数,需要额外的偏移逻辑。