4、Scale处理核心:深入解析scale在卷积、全连接、池化等算子中的传递与融合规则,掌握scale的乘法与除法运算

在CMSIS-NN的Int8推理链路中,Scale(缩放因子)是连接浮点世界与定点世界的桥梁。当我们把权重和激活值量化为int8后,每一层的数学运算结果实际上是一个int32的累加值,这个值必须乘以一个浮点scale才能还原为真实的浮点数值范围,再经过下一轮的量化才能继续传递。Scale的处理绝非简单的“乘一下”或“除一下”,它涉及算子的融合、精度的取舍、以及不同算子间scale的传递规则。本章将深入剖析scale在卷积、全连接、池化等核心算子中的传递与融合机制,帮助你在实际部署中精准控制数值精度。

Scale传递与融合规则总览 输入激活 scale_in, zero_in 卷积 / 全连接 acc = Σ(w·x) + b scale_w · scale_in int32 累加器 scale_acc = scale_w·scale_in 输出 int8 scale_out = scale_acc / scale_out 池化(Max/Avg) Max: scale不变 Avg: scale不变 Scale 融合规则 Requantize 融合 acc × scale_acc / scale_out Bias 融合 bias × scale_acc 激活函数融合 ReLU: clip(0, 255) 残差连接融合 scale 对齐相加 核心公式: 1. 卷积/全连接: y_int8 = clip( round( (acc_int32 × scale_w × scale_in) / scale_out + zero_out ) ) 2. 池化: y_int8 = acc_int32(Max直接取最大值,Avg累加后除以池化窗口大小) 3. 残差相加: y = clip( round( x1 × (s1/s_out) + x2 × (s2/s_out) + zero_out ) )

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)完成,运行时无需额外计算。

关键点:Scale的传递本质上是数值范围的缩放。每一层的输出scale决定了下一层输入scale,而权重scale则决定了累加器的scale。理解这一链条,是掌握scale融合的前提。

4.2 卷积与全连接中的Scale传递与融合

卷积和全连接在数学本质上是相同的——都是乘累加运算。它们的scale处理规则完全一致。核心操作分为三步:反量化累加结果 → 应用激活函数 → 重新量化输出。但在实际部署中,这三步被融合为一个高效的整数运算序列。

标准流程:

  1. 累加阶段:int8输入与int8权重相乘,累加得到int32的acc。此阶段不涉及scale运算。
  2. Scale调整阶段:计算requant_scale = s_acc / s_out。这个值在编译期预先计算并存储为浮点数,运行时通过乘法近似实现。
  3. 重新量化阶段: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,需分别处理
实战经验:在CMSIS-NN中,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在池化前后保持不变,因为池化操作本身不改变数据的物理含义,只是空间上的聚合。

常见误区:有些开发者误以为平均池化需要将输出scale除以窗口大小。实际上,池化操作在int32域内完成除法,输出scale与输入scale保持一致。如果错误地调整scale,会导致输出数值范围错误,进而影响后续层的精度。

4.4 Scale的融合规则:从算子到图优化

在实际的神经网络模型中,scale的传递往往涉及多个算子的融合。CMSIS-NN通过算子融合技术,将多个操作合并为一个高效的kernel,减少内存访问和计算开销。常见的融合模式包括:

  1. 卷积+偏置+ReLU融合:偏置在int32域内直接加到累加器上,ReLU通过clip操作实现,无需额外的scale转换。
  2. 卷积+残差连接融合:残差分支的scale需要与主分支的scale对齐。假设主分支输出scale为s_main,残差分支输出scale为s_res,则残差分支的int8值需先乘以s_res / s_main再相加。这个比例因子在编译期计算并融合到残差分支的requantize参数中。
  3. 卷积+池化融合:池化紧跟卷积时,可以将池化的除法运算与卷积的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(与主路径一致)

分析过程:

  1. Conv1的累加器scale = 0.02 × 0.01 = 0.0002。Requantize参数 = 0.0002 / 0.05 = 0.004。
  2. Conv2的累加器scale = 0.02 × 0.03 = 0.0006。Requantize参数 = 0.0006 / 0.08 = 0.0075。
  3. 相加时,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]
复盘要点:通过将残差比例因子融合到requantize参数中,我们避免了额外的乘法运算,且保证了相加时两个分支的scale一致。这种融合策略在CMSIS-NN中被称为“scale对齐”,是图优化的重要环节。

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一致,否则相加或拼接后数值范围混乱。
风险提示:在Cortex-M系列等资源受限的MCU上,浮点运算代价高昂。CMSIS-NN的requantize使用定点乘法+移位近似浮点除法,这会产生约0.1%~0.5%的数值误差。对于深层网络,误差可能累积。建议在模型转换后,使用仿真工具(如CMSIS-NN的Python模拟器)对比浮点模型与定点模型的输出差异,确保精度损失在可接受范围内。

总结而言,Scale处理的核心在于理解“scale是张量的属性,而非算子的属性”。每个张量都有其固有的scale,算子的作用是在保持物理意义不变的前提下,将输入张量的scale转换为输出张量的scale。掌握这一思想,你就能灵活应对卷积、池化、残差等各种复杂结构中的scale传递与融合问题。