4、量化基础:定点数表示、Q7/Q15格式、量化参数(scale/offset)对全连接计算的影响

CMSIS-NN 面向 ARM Cortex-M 系列处理器,这类内核通常不具备硬件浮点单元(FPU)或仅有单精度 FPU,且内存带宽与容量受限。若直接使用 float32 执行全连接层计算,不仅速度慢,而且内存占用高。因此,CMSIS-NN 将神经网络权重与激活值量化为定点数,核心是 Q7(int8_t) 与 Q15(int16_t) 两种格式。理解定点数表示、Q 格式语义以及 scale/offset 量化参数,是掌握全连接层优化(如 arm_fully_connected_q7 与 arm_fully_connected_q15)的前提。本章将从数学定义、内存布局、计算链路三个维度展开。

量化全连接计算链路与参数映射 浮点模型 y = W·x + b float32 权重/激活 量化参数 scale_w, offset_w scale_x, offset_x scale_y, offset_y 定点表示 Q7 / Q15 整数 + 小数位 无浮点运算 量化输出 Q7/Q15 反量化可选 内存访问模式(以 Q7 全连接层为例) 输入 x[N] 权重 W[M×N] 累加器 int32 偏置 b[M] 输出 y[M] • 权重按 [输出通道][输入通道] 顺序存储,保证内层循环连续读取 • 输入向量在每次输出通道计算时被重复读取,需利用缓存或 SIMD 重排 • 累加器使用 int32 避免溢出,最后通过 scale/offset 量化为 Q7/Q15 • 偏置以 int32 或 int16 存储,在累加结束后一次性加入

4.1 定点数表示与 Q 格式

定点数是一种在固定位宽内同时表达整数部分和小数部分的数值表示方法。与浮点数不同,定点数的小数点位置是固定的,因此不需要指数位来动态调整数值范围。在 CMSIS-NN 中,最常用的是 Q7 和 Q15 格式,分别对应 8 位和 16 位有符号定点数。

Q 格式的数学定义:Qm.n 表示总位宽为 m+n 位,其中 m 位为整数部分(含符号位),n 位为小数部分。例如 Q7 格式通常指 Q0.7,即 1 位符号位 + 7 位小数位,表示范围为 [-1, 1 - 2⁻⁷],精度为 2⁻⁷ ≈ 0.0078125。Q15 格式为 Q0.15,范围 [-1, 1 - 2⁻¹⁵],精度约 3.05×10⁻⁵。

实际数值与定点整数的转换关系为:

real_value = q_format_integer × 2⁻ⁿ

其中 n 为小数位个数。例如 Q15 格式中,整数 16384 表示 16384 × 2⁻¹⁵ = 0.5。这种表示法的优势在于:乘法运算可以直接使用整数乘法指令,加法使用整数加法指令,无需浮点运算单元。

关键点:CMSIS-NN 的 Q7/Q15 并非严格限定为 Q0.7/Q0.15。实际使用时,权重和激活的数值范围可能超出 [-1, 1],因此需要配合 scale 参数进行缩放。Q 格式只定义了小数位数量,而 scale 定义了每个量化步长代表的实际数值大小。

4.2 量化参数:scale 与 offset 的数学语义

量化参数建立了浮点实数域与定点整数域之间的映射关系。对于任意浮点值 r,其量化后的定点整数 q 满足:

q = round(r / scale) + offset

反量化为:

r = (q - offset) × scale

其中 scale(缩放因子)是一个正浮点数,表示量化步长;offset(零点偏移)是一个整数,表示浮点值 0 对应的量化整数。这种非对称量化方式能够更好地适配 ReLU 等非负激活函数,减少量化误差。

参数 符号 数据类型 作用 典型值
权重缩放 scale_w float32 将权重浮点值映射到 Q7/Q15 整数 0.001 ~ 0.1
权重偏移 offset_w int32 权重零点对齐 0(对称量化)或非零
输入缩放 scale_x float32 将输入激活映射到定点格式 0.01 ~ 0.5
输入偏移 offset_x int32 输入零点偏移 0 或 128(uint8 输入)
输出缩放 scale_y float32 将累加结果映射到输出定点格式 由训练后量化统计决定
输出偏移 offset_y int32 输出零点偏移 0 或 128

在全连接层计算中,浮点运算 y = W·x + b 被量化为以下定点运算链路:

  1. 输入量化:将浮点输入 x 转换为 Q7/Q15 整数 x_q,即 x_q = round(x / scale_x) + offset_x。
  2. 权重量化:权重 W 在训练后离线量化为 W_q,存储为 Q7/Q15 格式。
  3. 整数乘累加:计算 acc = Σ(W_q[i][j] × x_q[j]),累加器使用 int32 或 int64 以避免中间溢出。
  4. 偏置加入:acc += b_q[i],其中 b_q 为量化后的偏置(通常以 int32 存储)。
  5. 反量化与再量化:将累加结果转换为输出定点格式,y_q = round(acc × (scale_w × scale_x / scale_y)) + offset_y。
工程经验:在 CMSIS-NN 的 arm_fully_connected_q7 函数中,权重和输入均以 Q7 格式存储,但累加器使用 int32。最终输出前会调用 arm_nn_requantize 函数,将 int32 累加结果乘以一个由 scale 计算出的乘数因子,再右移实现定点反量化。该乘数因子通常被预计算为 multiplier 和 shift 两个整数,避免浮点运算。

4.3 量化对全连接计算的影响分析

量化参数的选择直接影响全连接层的计算精度、内存占用和推理速度。以下是三个维度的具体影响:

(1)精度影响:scale 过大会导致量化步长增大,舍入误差增加;scale 过小则可能超出 Q7/Q15 的表示范围,产生截断。offset 设置不当会导致零点偏移,使得小数值被映射到错误的整数区间。对于全连接层,权重和输入的动态范围差异较大时,使用单一 scale 会显著增加量化误差,此时应考虑按通道(per-channel)量化。

(2)内存与带宽影响:Q7 格式每个权重仅占 1 字节,Q15 占 2 字节,相比 float32 分别减少 75% 和 50% 的内存占用。全连接层的权重矩阵通常较大(例如 512×512),内存缩减直接降低了从 Flash 加载权重的时间。但需要注意,Q7 格式的权重矩阵在计算时可能需要扩展为 int16 或 int32 进行乘累加,这会增加寄存器压力。

(3)计算效率影响:CMSIS-NN 利用 ARM Cortex-M 的 SIMD(单指令多数据)指令,如 SMLAD(有符号乘加双字)和 SMLALD,可以在单条指令内完成两个 16 位乘法并累加到 32 位结果。Q7 格式配合 __SMLAD 指令,一次可处理两个权重-输入对,大幅提升吞吐量。而 Q15 格式则使用 SMLAL 指令,一次处理一个乘法,但精度更高。

风险提示:量化参数不匹配是 CMSIS-NN 部署中最常见的错误。例如,训练时使用 per-tensor 量化,但推理时误用了 per-channel 的 scale;或者输入数据的 scale 与模型训练时不一致,导致输出严重偏离。务必在部署前使用校准数据集验证量化参数的正确性。

4.4 案例拆解:Q7 全连接层的量化参数推导

假设我们有一个训练好的全连接层,输入维度 N=4,输出维度 M=3。浮点权重和输入如下:

权重 W(float32):
[[ 0.25, -0.50,  0.75, -0.10],
 [ 0.60,  0.30, -0.20,  0.40],
 [-0.35,  0.15,  0.55, -0.25]]

输入 x(float32):
[0.80, -0.60, 0.40, 0.20]

偏置 b(float32):
[0.10, -0.05, 0.00]

第一步:确定量化参数。统计权重的值最大值为 0.75,输入值最大值为 0.80。为了充分利用 Q7 的 [-1, 1) 范围,我们选择对称量化(offset=0):

scale_w = 0.75 / 127 ≈ 0.00591
scale_x = 0.80 / 127 ≈ 0.00630

第二步:量化权重和输入。使用 q = round(r / scale):

W_q = [[42, -85, 127, -17],
       [102, 51, -34, 68],
       [-59, 25, 93, -42]]

x_q = [127, -95, 63, 32]

第三步:整数乘累加。以第一个输出神经元为例:

acc = 42×127 + (-85)×(-95) + 127×63 + (-17)×32
    = 5334 + 8075 + 8001 - 544
    = 20866

第四步:加入偏置并反量化。偏置 b=0.10 量化为 b_q = 0.10 / (scale_w × scale_x) ≈ 0.10 / (0.00591×0.00630) ≈ 2687。累加后:

acc_total = 20866 + 2687 = 23553
y_q = round(acc_total × (scale_w × scale_x / scale_y))

假设输出 scale_y = 0.01,则 y_q = round(23553 × 0.0000372 / 0.01) = round(87.6) = 88。反量化得到浮点输出 y = 88 × 0.01 = 0.88,而原始浮点计算结果为 0.25×0.8 + (-0.5)×(-0.6) + 0.75×0.4 + (-0.1)×0.2 + 0.1 = 0.2 + 0.3 + 0.3 - 0.02 + 0.1 = 0.88,完全一致。

复盘与经验:本案例中量化误差为零,是因为数值恰好能被 scale 整除。实际场景中必然存在舍入误差。若发现量化后精度下降明显,可考虑:① 使用 per-channel 量化;② 增大位宽至 Q15;③ 在训练阶段引入量化感知训练(QAT),让模型适应量化噪声。

4.5 常见误区与总结

误区一:认为 Q7 格式的数值范围一定是 [-1, 1)。实际上,Q7 只是定义了 8 位有符号整数的存储格式,具体表示的数值范围由 scale 决定。通过调整 scale,Q7 可以表示任意范围的数据,只是精度随之变化。

误区二:忽略 offset 的作用。对于 ReLU 后的激活值(非负),使用对称量化(offset=0)会浪费一半的表示范围。此时应使用非对称量化,将 offset 设为 0 或 128,以充分利用 8 位的全部 256 个码值。

误区三:在 CMSIS-NN 中手动计算 scale 乘积。CMSIS-NN 提供了 arm_nn_requantize 函数,内部使用 64 位乘法和移位操作完成高效的定点反量化。开发者应传入预计算的 multiplier 和 shift 参数,而不是在运行时进行浮点乘法。

误区四:忽视内存对齐。CMSIS-NN 的 Q7 权重矩阵要求按 4 字节对齐,以便使用 SIMD 指令加载 4 个 int8 数据。如果权重数组未对齐,会导致总线错误或性能下降。使用 __ALIGNED(4) 或编译器对齐属性可以避免此问题。

总结:量化参数 scale/offset 是全连接层定点计算的核心元数据。它们不仅决定了数值映射的精度,还影响内存布局和指令选择。在 CMSIS-NN 中,Q7 格式配合 SIMD 指令可实现 2-4 倍的性能提升,但前提是量化参数正确、内存对齐合理、反量化路径高效。建议在部署前使用校准数据集统计权重和激活的数值分布,选择最优的 scale/offset,并通过端到端精度验证确保模型质量。