CMSIS Quantization
Table of Contents
1. CMSIS Quantization
1.1. output_shift 与 bias_shift
1.1.1. dec_bit
dec_bit 指 float 最多可以左移 dec_bit 而不超过量化范围.
例如, 假设量化范围为 int8, 则 [-128, 127), 若 float 为 2.0, 则 dec_bit 为 5,因为 2.0 << 5 = 64, 而 2.0 << 6 = 128, 所以最大移位为 5
以 int8 量化为例, 计算 dec_bit 的公式为
\(dec\_bit(x)=7 - (np.ceil(np.log2(x)))\)
CMSIS 是对称量化, 且 scale 为 \(2^{dec\_bit(abs(x)_{max})}\)
需要注意的是后面提到的 scale, shift 等都是指 dec_bit, 而不是 \(2^{dec\_bit}\)
1.1.2. 计算 dense layer 的量化参数
实际上 dense layer 涉及到四个量化参数:
- fi: input scale
- fw: weight scale
- fo: output scale
- fb: bias scale
但 cmsis_nn api 要求提供两个参数做为量化参数:
- output_shift
- bias_shift
其中:
- \(output\_shift = fi+fw-fo\)
- \(bias\_shift = fi+fw-fb\)
之所以需要这两个参数是因为:
假设要计算 \(o=w*x+b\), w,x,b 是原始的浮点数, W,X,B,O 是分别量化的结果. 如何通过 W,X,B 得到 O?
并不能直接通过 \(O=W*X+B\), 因为各部分是分别量化的, scale 不同, 不能直接运算.
- B 与 W*X 相加时, 需要 B 左移 \(f_i+f_w-f_b\) 位变成和 W*X 相同的 scale
- \(W*X+(B\ll bias\_shift)\) 的结果需要先右移 \(f_i+f_w\) 得到浮点数, 再左移 \(f_o\) 得到 OUTPUT, 合并起来就是右移 output_shift
所以最终的过程是: \((W*X+ (B \ll bias\_shift)) \gg output\_shift\)
例如:
为便于理解, 把 scale 看作是 10 的幂 (而不是 2 的幂)
假设 input 为 0.1, weight 为 0.01, bias = 0.1.
未量化时 wx+b 的值为 0.001+0.1=0.101
设对 x/w/b/o 量化的 dec_bit 分别为 1/2/1/2; W/X/B/O 分别是 w/x/b/o 量化后的结果.
如何使用 W/X/B 得到 O?
\(wx+b=o \implies wx+b=(\frac{X}{10})*(\frac{W}{100})+\frac{B}{10}=\frac{XW}{1000}+\frac{B}{10}=\frac{XW+100B}{1000}=\frac{O}{100}\implies \frac{XW+100B}{10}=O\)
最后的式子显示于 B 需要左移 2 (fi+fw-fb), 且 output 需要右移 1 (fi+fw-fo) 才能最终得于 O
用 tflite 的量化模型进行推理时, 在 Prepare 阶段同样使用 output_shift, 不过名字叫 real_multiplier
Backlinks
Quantization (Quantization > Overview): 其中两个 layer 之间的 `DQ–Q` 是一个常量, 可以提前转换成近似的整数操作 (例如 cmsis 的 output_shift 和 tflite 的 real_multiplier)
1.2. weight 量化
根据 weight 本身的范围确定 weight 的 dec_bit, 然后令 W=w*(2**dec_bit) 即可
dec_bit = 7 - (np.ceil(np.log2(np.max([abs(min_weight), abs(max_weight)], axis=0))))
1.3. activiation 量化
使用一些参考输入, 确定每一层的输入输出的范围, 计算各自的 dec_bit 做为相应的 fi, fo, 配合 fw 可以获得 output_shift 和 bias_shift
1.4. 坑
keras layer activation
keras.Layers.Dense(16, activiation="softmax"), 这一层的输出会包含 softmax 的结果, 所以使用这一层的输出计算的量化参数并不是 dense layer 的参数.
解决的方法是把 softmax 做为单独的一层, 不要和 dense 写在一起
ReLU 导致的问题
假设网络结构为:
FC1 -> ReLU -> FC2
正常情况下, cmsis relu 的代码应该是这样的:
void relu(input, output_shift, ouput) { /* cmsis 是对称量化, 所以 input_bias=0 */ int input_bias = 0; if (input > input_bias) { return input >> output_shift } return 0 }
但实际上 cmsis 的 relu api 并不支持 output_shift 参数, 或者说 output_shift 为 0, 即 relu 的 output_scale 等于 input_scale (因为 output_shift=input_scale-output_scale)
所以 FC2 的 input_scale 应该直接使用 ReLU 的 input_scale, 而不能自己根据 FC2 input 范围来计算, 相当于量化时 relu 被 `pass throught`.
除了 ReLU, CMSIS 中的 average pooling, max pooling, 也有类似的问题 (softmax 是否有这个问题还不确定…)
Q: FC, CONV 为什么不像 ReLU 一样直接忽略 output_shift?
A: FC, CONV 会进行乘加, 结果会很快超出 int8 范围, output_shift 相当于是通过 Q(DQ(output_1, dec_bit_output_1), dec_bit_input_2) 保证输出在 int8 范围内. 但 ReLU, pooling 不存在这个问题
训练的越多越不准确…
最初的模型使用了 dropout 做正则化 (而没有使用 batch norm 或 l1/l2 regularizer), 导致训练的越多 weight 越大 (从 0.x 增加到 2.x), 而 weight 变大又导致 activiation 变大 (从几十增加到几千), 而 cmsis-nn 的 output_shift 只支持右移, 即它假设 activiation 总是在 [-128,128) 范围内, 当 activiation 远远大于这个范围时, 缺失的左移操作会导致结果不准确.
解决的方法:
- 使用 batch norm, 保证 activiation 在一定范围
- 使用 l1/l2 regularizer, 保证 weight 在较小的范围
1.5. Sample
1.5.1. keras 模型
def cnn(): inputs = keras.Input(shape=(FRAMES, DCT_COEFFICIENT_COUNT)) outputs = tf.expand_dims(inputs, axis=-1) outputs = layers.Conv2D( filters=28, kernel_size=[10, 4], strides=[1, 1], name="conv1" )(outputs) # outputs = layers.BatchNormalization()(outputs) outputs = layers.ReLU()(outputs) outputs = layers.Dropout(0.3)(outputs) outputs = layers.Conv2D( filters=30, kernel_size=[10, 4], strides=[2, 1], name="conv2" )(outputs) # outputs = layers.BatchNormalization()(outputs) outputs = layers.ReLU()(outputs) outputs = layers.Dropout(0.2)(outputs) outputs = layers.Flatten()(outputs) outputs = layers.Dense(16, name="dense1")(outputs) # outputs = layers.BatchNormalization()(outputs) outputs = layers.ReLU()(outputs) outputs = layers.Dropout(0.1)(outputs) outputs = layers.Dense(128, name="dense2")(outputs) outputs = layers.ReLU()(outputs) outputs = layers.Dense(len(WORDS), name="dense3")(outputs) outputs = layers.Softmax()(outputs) return keras.Model(inputs, outputs)
1.5.2. cmsis 模型
void CNN::run_nn(q7_t* in_data, q7_t* out_data) { arm_convolve_HWC_q7_basic_nonsquare( in_data, CONV1_IN_X, CONV1_IN_Y, CONV1_IN_CH, conv1_wt, CONV1_OUT_CH, CONV1_KERNEL_X, CONV1_KERNEL_Y, CONV1_PADDING_X, CONV1_PADDING_Y, CONV1_STRIDE_X, CONV1_STRIDE_Y, conv1_bias, CONV1_BIAS_SHIFT, CONV1_OUT_SHIFT, buffer1, CONV1_OUT_X, CONV1_OUT_Y, (q15_t*)buffer3, NULL); arm_relu_q7(buffer1, CONV1_OUT_X * CONV1_OUT_Y * CONV1_OUT_CH); arm_convolve_HWC_q7_basic_nonsquare( buffer1, CONV2_IN_X, CONV2_IN_Y, CONV2_IN_CH, conv2_wt, CONV2_OUT_CH, CONV2_KERNEL_X, CONV2_KERNEL_Y, CONV2_PADDING_X, CONV2_PADDING_Y, CONV2_STRIDE_X, CONV2_STRIDE_Y, conv2_bias, CONV2_BIAS_SHIFT, CONV2_OUT_SHIFT, buffer2, CONV2_OUT_X, CONV2_OUT_Y, (q15_t*)buffer3, NULL); arm_relu_q7(buffer2, CONV2_OUT_X * CONV2_OUT_Y * CONV2_OUT_CH); arm_fully_connected_q7( buffer2, dense1_wt, DENSE1_INPUT_DIM, DENSE1_OUTPUT_DIM, DENSE1_BIAS_SHIFT, DENSE1_OUT_SHIFT, dense1_bias, buffer1, (q15_t*)buffer3); arm_relu_q7(buffer1, DENSE1_OUTPUT_DIM); arm_fully_connected_q7( buffer1, dense2_wt, DENSE2_INPUT_DIM, DENSE2_OUTPUT_DIM, DENSE2_BIAS_SHIFT, DENSE2_OUT_SHIFT, dense2_bias, buffer2, (q15_t*)buffer3); arm_relu_q7(buffer2, DENSE2_OUTPUT_DIM); arm_fully_connected_q7( buffer2, dense3_wt, DENSE3_INPUT_DIM, DENSE3_OUTPUT_DIM, DENSE3_BIAS_SHIFT, DENSE3_OUT_SHIFT, dense3_bias, out_data, (q15_t*)buffer3); }
1.5.3. 量化
import tensorflow as tf import numpy as np from tensorflow import keras from tensorflow.keras import layers, losses, metrics, optimizers, models tf.keras.backend.clear_session() def collect_inference_statistics(model, ref_input): input = model.layers[0].output prev = model.layers[0] outputs = [] for layer in model.layers: if layer.name.startswith("conv") or layer.name.startswith("dense"): outputs.append(prev.output) outputs.append(layer.output) prev = layer model = keras.Model(input, outputs) max_value, min_value = [], [] for x in ref_input[0:2000]: value = [(i.numpy().min(), i.numpy().max()) for i in model(x)] min_value.append([v[0] for v in value]) max_value.append([v[1] for v in value]) min_value, max_value = ( np.asarray(min_value).min(axis=0), np.asarray(max_value).max(axis=0), ) shift = ( (7 - (np.ceil(np.log2(np.max([abs(min_value), abs(max_value)], axis=0))))) .astype(int) .tolist() ) return [(s[0], s[1]) for s in zip(shift[0::2], shift[1::2])] def quantize(model, shift, ref_input): KWS_CMSIS_NN = "model/kws_cmsis_nn.h" int_bits = int(np.ceil(np.log2(max(abs(ref_input.min()), abs(ref_input.max()))))) dec_bits = 7 - int_bits with open(KWS_CMSIS_NN, "w") as f: f.write("// -*- eval: (sw/large-file-mode); -*-\n") f.write("#define MFCC_DEC_BITS {}\n".format(dec_bits)) i = -1 for layer in model.layers: if layer.name.startswith("conv") or layer.name.startswith("dense"): i += 1 print(layer.name) fw = None for (t, value) in enumerate(layer.get_weights()): int_bits = int( np.ceil(np.log2(max(abs(value.min()), abs(value.max())))) ) dec_bits = 7 - int_bits q_value = np.round(value * 2 ** dec_bits) if t == 0: shift_name = layer.name + "_out" var_name = layer.name + "_weight" # fi+fw-fo fw = dec_bits shift_bits = shift[i][0] + fw - shift[i][1] else: shift_name = layer.name + "_bias" var_name = layer.name + "_bias" shift_bits = shift[i][0] + fw - dec_bits if shift_bits < 0: shift_bits = 0 with open(KWS_CMSIS_NN, "a") as f: f.write( "#define {}_SHIFT {}\n".format(shift_name.upper(), shift_bits) ) f.write("#define {} {{".format(var_name.upper())) if len(value.shape) == 4: # tf : HWNC # cmsis: CHWN q_value_t = np.transpose(q_value, (3, 0, 1, 2)) else: q_value_t = np.transpose(q_value) q_value_t.tofile(f, sep=", ", format="%d") f.write("}\n") if __name__ == "__main__": MODEL_PATH = "./model/kws" model = keras.models.load_model(MODEL_PATH) ref_input = np.load("./temp/test_x.npy") shift = collect_inference_statistics(model, ref_input) print(shift) quantize(model, shift, ref_input)
1.6. cmsis-nn 与 tflite 量化
tflite for mcu 底层支持 cmsis-nn, 但 tflite 的量化规范来自于 gemmlowp, 与前面描述的基于 2^n 的对称量化并不一致. 事实上, cmsis-nn 针对 tflite 的量化需求提供了另外一套 api
tensorflow/tensorflow/lite/micro/tools/make/downloads/cmsis/CMSIS/NN/README.md::Legacy vs TFL micro compliant APIs
Backlinks
Quantization (Quantization > Overview): scale 正常为浮点数, 但有些框架例如 CMSIS 会要求 scale 的值必须是 \(2^{n}\) 的形式, 这样可以避免浮点数乘法, 同时可以用移位代替整数乘法. tflite 会使用浮点数 scale, 但是可以用 rounding_doubling_high_mul 转换为整数乘法.
Quantization (Quantization > CMSIS Quantization): CMSIS Quantization