4、Scale处理核心:深入解析scale在卷积、全连接、池化等算子中的传递与融合规则,掌握scale的乘法与除法运算
在CMSIS-NN的Int8推理链路中,Scale(缩放因子)是连接浮点世界与定点世界的桥梁。当我们把权重和激活值量化为int8后,每一层的数学运算结果实际上是一个int32的累加值,这个值必须乘以一个浮点scale才能还原为真实的浮点数值范围,再经过下一轮的量化才能继续传递。Scale的处理绝非简单的“乘一下”或“除一下”,它涉及算子的融合、精度的取舍、以及不同算子间scale的传递规则。本章将深入剖析scale在卷积、全连接、池化等核心算子中的传递与融合机制,帮助你在实际部署中精准控制数值精度。
4.1 Scale的基本定义与量化参数体系
在CMSIS-NN中,每个张量(权重、激活、偏置)都关联一组量化参数:scale(缩放因子)和zero_point(零点)。对于int8对称量化,zero_point通常为0;非对称量化则需记录零点偏移。Scale的数学含义是:real_value = (int8_value - zero_point) × scale。在推理过程中,我们并不需要显式地将每个int8值转换为浮点,而是将scale的计算合并到算子内部的整数运算中。
以卷积层为例,假设输入激活的scale为s_in,权重的scale为s_w,偏置的scale为s_b。卷积的累加过程在int32域内完成,累加结果的scale为s_acc = s_in × s_w。偏置在加入累加器之前,需要先将其从s_b转换为s_acc,即bias_int32 = round(bias_float / s_acc)。这一转换通常在模型转换阶段(如TFLite转CMSIS-NN)完成,运行时无需额外计算。
4.2 卷积与全连接中的Scale传递与融合
卷积和全连接在数学本质上是相同的——都是乘累加运算。它们的scale处理规则完全一致。核心操作分为三步:反量化累加结果 → 应用激活函数 → 重新量化输出。但在实际部署中,这三步被融合为一个高效的整数运算序列。
标准流程:
- 累加阶段:int8输入与int8权重相乘,累加得到int32的
acc。此阶段不涉及scale运算。 - Scale调整阶段:计算
requant_scale = s_acc / s_out。这个值在编译期预先计算并存储为浮点数,运行时通过乘法近似实现。 - 重新量化阶段:
y_int8 = clip( round( acc × requant_scale + zero_out ), -128, 127 )。
CMSIS-NN提供了arm_convolve_s8和arm_fully_connected_s8等函数,它们内部使用arm_nn_requantize函数完成scale的乘法运算。该函数采用乘法+移位的方式近似浮点乘法,以避免昂贵的浮点运算。具体实现为:将requant_scale表示为m × 2^n的形式,其中m为int32的定点数,n为移位位数。这样,acc × requant_scale就变成了(acc × m) >> n,仅需一次乘法与一次移位。
| 算子类型 | 累加器Scale | 输出Scale | Requantize参数 | 备注 |
|---|---|---|---|---|
| 卷积 (3x3, stride=1) | s_in × s_w | s_out | s_in × s_w / s_out | 偏置需预先缩放至s_acc |
| 全连接 | s_in × s_w | s_out | s_in × s_w / s_out | 与卷积规则一致 |
| 深度可分离卷积 | s_in × s_w (depthwise) | s_out | s_in × s_w / s_out | 逐通道scale,需分别处理 |
requant_scale通常以int32的定点格式存储,并配合一个右移位数shift。例如,requant_scale = 0.00123可表示为m = 1032(放大2^20倍),shift = 20。这种表示法在ARM Cortex-M系列上执行效率极高。
4.3 池化算子的Scale处理:不变与除法
池化算子(最大池化、平均池化)的scale处理相对简单,但容易出错。最大池化直接取窗口内的最大值,不改变数值的scale,即输出scale等于输入scale。平均池化则需要对窗口内的值求和后除以窗口大小,这涉及到一个除法运算。
在CMSIS-NN中,平均池化的实现有两种策略:
- 策略一:浮点除法近似。将
1/window_size预先计算为定点数,然后通过乘法+移位实现除法。例如,对于2x2池化,1/4 = 0.25,可表示为m = 1 << 30,shift = 32,即sum × m >> 32。 - 策略二:整数除法。直接使用整数除法指令,但CMSIS-NN为了性能,通常采用策略一。
值得注意的是,平均池化的除法发生在int32累加域内,因此不需要额外的scale转换。输入scale在池化前后保持不变,因为池化操作本身不改变数据的物理含义,只是空间上的聚合。
4.4 Scale的融合规则:从算子到图优化
在实际的神经网络模型中,scale的传递往往涉及多个算子的融合。CMSIS-NN通过算子融合技术,将多个操作合并为一个高效的kernel,减少内存访问和计算开销。常见的融合模式包括:
- 卷积+偏置+ReLU融合:偏置在int32域内直接加到累加器上,ReLU通过clip操作实现,无需额外的scale转换。
- 卷积+残差连接融合:残差分支的scale需要与主分支的scale对齐。假设主分支输出scale为
s_main,残差分支输出scale为s_res,则残差分支的int8值需先乘以s_res / s_main再相加。这个比例因子在编译期计算并融合到残差分支的requantize参数中。 - 卷积+池化融合:池化紧跟卷积时,可以将池化的除法运算与卷积的requantize合并,减少一次中间数据的读写。
Scale融合的核心原则是:尽可能在int32域内完成所有需要scale的运算,避免中间结果的浮点转换。这要求我们在模型转换阶段(如使用TFLite Converter导出时)就完成scale的预计算和融合。
4.5 案例拆解:一个包含残差连接的卷积块
假设我们有一个残差块,主路径为卷积层(Conv1),残差路径为1x1卷积(Conv2),两者输出相加后经过ReLU。已知参数如下:
- Conv1: 输入scale=0.02, 权重scale=0.01, 输出scale=0.05
- Conv2: 输入scale=0.02, 权重scale=0.03, 输出scale=0.08
- 相加后输出scale=0.05(与主路径一致)
分析过程:
- Conv1的累加器scale = 0.02 × 0.01 = 0.0002。Requantize参数 = 0.0002 / 0.05 = 0.004。
- Conv2的累加器scale = 0.02 × 0.03 = 0.0006。Requantize参数 = 0.0006 / 0.08 = 0.0075。
- 相加时,Conv2的输出scale(0.08)需转换为Conv1的输出scale(0.05)。比例因子 = 0.08 / 0.05 = 1.6。因此,Conv2的输出int8值需乘以1.6后再与Conv1的输出相加。
实际操作:在CMSIS-NN中,我们不会显式地乘以1.6,而是将Conv2的requantize参数调整为0.0075 × 1.6 = 0.012,这样Conv2的输出直接就是scale=0.05的int8值,相加时无需额外转换。最终,相加后的结果经过ReLU clip到[0, 127](假设是ReLU6)即可。
| 步骤 | 操作 | Scale值 | 说明 |
|---|---|---|---|
| 1 | Conv1累加 | 0.0002 | int32累加器 |
| 2 | Conv1 requantize | 0.004 | 输出scale=0.05 |
| 3 | Conv2累加 | 0.0006 | int32累加器 |
| 4 | Conv2 requantize | 0.012 | 融合残差比例1.6 |
| 5 | 相加+ReLU | 0.05 | clip到[0,127] |
4.6 常见误区与精度风险控制
Scale处理中的错误往往不会导致程序崩溃,但会静默地降低模型精度。以下是几个高频误区:
- 误区一:忽略zero_point。非对称量化中,zero_point不为0,在requantize时必须加上zero_out,否则输出会整体偏移。
- 误区二:requantize参数溢出。当
requant_scale过大或过小时,定点表示可能溢出。CMSIS-NN要求m在int32范围内,且shift在[-31, 31]之间。若超出范围,需调整scale的表示方式。 - 误区三:池化后错误调整scale。如前所述,池化不改变scale,但有些开发者会误除以窗口大小。
- 误区四:多分支融合时未对齐scale。残差连接、concat等操作要求所有输入分支的scale一致,否则相加或拼接后数值范围混乱。
总结而言,Scale处理的核心在于理解“scale是张量的属性,而非算子的属性”。每个张量都有其固有的scale,算子的作用是在保持物理意义不变的前提下,将输入张量的scale转换为输出张量的scale。掌握这一思想,你就能灵活应对卷积、池化、残差等各种复杂结构中的scale传递与融合问题。