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)的前提。本章将从数学定义、内存布局、计算链路三个维度展开。
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。这种表示法的优势在于:乘法运算可以直接使用整数乘法指令,加法使用整数加法指令,无需浮点运算单元。
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 被量化为以下定点运算链路:
- 输入量化:将浮点输入 x 转换为 Q7/Q15 整数 x_q,即
x_q = round(x / scale_x) + offset_x。 - 权重量化:权重 W 在训练后离线量化为 W_q,存储为 Q7/Q15 格式。
- 整数乘累加:计算
acc = Σ(W_q[i][j] × x_q[j]),累加器使用 int32 或 int64 以避免中间溢出。 - 偏置加入:
acc += b_q[i],其中 b_q 为量化后的偏置(通常以 int32 存储)。 - 反量化与再量化:将累加结果转换为输出定点格式,
y_q = round(acc × (scale_w × scale_x / scale_y)) + offset_y。
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 指令,一次处理一个乘法,但精度更高。
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) 或编译器对齐属性可以避免此问题。