2. 基于CMSIS-NN的全连接网络部署
任务目标
本案例将训练好的全连接网络(2层隐藏层,MNIST二分类演示)权重与偏置导出为C数组,在MCU上使用CMSIS-NN的arm_fully_connected_s8接口完成前向推理,最终输出分类结果(0或1)。案例聚焦“模型→C数组→CMSIS-NN推理”的最小可复现闭环,不涉及训练过程。
环境与依赖
- 硬件:任意支持CMSIS-NN的Cortex-M内核MCU(如STM32F746G-Discovery)
- 软件:STM32CubeIDE或Keil MDK,CMSIS-NN库(v4.5+)
- 模型来源:使用Python训练一个输入维度为20、输出为2类的全连接网络(本案例提供权重生成脚本)
模型参数导出(Python)
以下脚本生成C头文件model_weights.h,包含网络各层权重与偏置的int8量化数组。网络结构:输入20 → 隐藏层16(ReLU) → 输出层2。
import numpy as np
# 模拟训练好的权重(实际使用时替换为真实训练结果)
np.random.seed(0)
W1 = (np.random.randn(16, 20) * 0.5).astype(np.float32)
b1 = np.zeros(16, dtype=np.float32)
W2 = (np.random.randn(2, 16) * 0.5).astype(np.float32)
b2 = np.zeros(2, dtype=np.float32)
# 量化参数(scale, zero_point)
s1, z1 = 0.05, 0 # 隐藏层输出量化
s2, z2 = 0.02, 0 # 输出层量化
# 量化权重到int8
def quantize(w, scale):
return np.clip(np.round(w / scale), -128, 127).astype(np.int8)
W1_q = quantize(W1, 0.01) # 输入层scale=0.01
W2_q = quantize(W2, s1) # 隐藏层输出scale
b1_q = np.round(b1 / (0.01 * s1)).astype(np.int32)
b2_q = np.round(b2 / (s1 * s2)).astype(np.int32)
with open('model_weights.h', 'w') as f:
f.write('#ifndef MODEL_WEIGHTS_H\n#define MODEL_WEIGHTS_H\n\n')
f.write('#include <stdint.h>\n\n')
f.write('// 输入层权重 [16][20]\n')
f.write('const int8_t W1[16][20] = {\n')
for row in W1_q:
f.write(' {' + ', '.join(map(str, row)) + '},\n')
f.write('};\n\n')
f.write('const int32_t b1[16] = {' + ', '.join(map(str, b1_q)) + '};\n\n')
f.write('// 输出层权重 [2][16]\n')
f.write('const int8_t W2[2][16] = {\n')
for row in W2_q:
f.write(' {' + ', '.join(map(str, row)) + '},\n')
f.write('};\n\n')
f.write('const int32_t b2[2] = {' + ', '.join(map(str, b2_q)) + '};\n\n')
f.write('// 量化参数\n')
f.write('const float scale_in = 0.01f; // 输入量化scale\n')
f.write('const float scale_hidden = 0.05f;\n')
f.write('const float scale_out = 0.02f;\n')
f.write('const int32_t zero_point = 0;\n\n')
f.write('#endif\n')
print("model_weights.h 已生成")
MCU端C代码实现
以下代码完成:输入数据int8量化 → 两层全连接推理 → 输出反量化并比较得到分类结果。
#include "arm_nnfunctions.h"
#include "model_weights.h"
#include <stdio.h>
// 输入样本(示例:20维浮点特征,实际替换为传感器数据)
const float input_fp[20] = {
0.1f, -0.2f, 0.3f, 0.4f, -0.5f, 0.6f, -0.7f, 0.8f, 0.9f, -1.0f,
0.11f, -0.22f, 0.33f, 0.44f, -0.55f, 0.66f, -0.77f, 0.88f, 0.99f, -1.11f
};
int8_t input_q[20];
int8_t hidden_q[16];
int8_t output_q[2];
int32_t hidden_buf[16]; // 中间缓冲区
// 浮点转int8量化
void quantize_input(const float* src, int8_t* dst, int len, float scale) {
for (int i = 0; i < len; i++) {
dst[i] = (int8_t)(src[i] / scale);
}
}
// 反量化并找出最大值的索引
int dequantize_and_argmax(const int8_t* q_data, float scale, int len) {
float max_val = -1e9f;
int max_idx = 0;
for (int i = 0; i < len; i++) {
float val = q_data[i] * scale;
if (val > max_val) {
max_val = val;
max_idx = i;
}
}
return max_idx;
}
int main(void) {
// 1. 输入量化
quantize_input(input_fp, input_q, 20, scale_in);
// 2. 第一层全连接 + ReLU
arm_fully_connected_s8(input_q, W1[0], 20, 16, 0, 0,
scale_in, scale_hidden,
b1, hidden_buf, hidden_q, NULL);
// 3. 第二层全连接(无激活)
arm_fully_connected_s8(hidden_q, W2[0], 16, 2, 0, 0,
scale_hidden, scale_out,
b2, NULL, output_q, NULL);
// 4. 反量化并输出分类结果
int result = dequantize_and_argmax(output_q, scale_out, 2);
printf("分类结果: %d\n", result);
printf("原始输出: %d, %d\n", output_q[0], output_q[1]);
while(1);
return 0;
}
编译与运行
- 在STM32CubeIDE中新建C项目,将
model_weights.h和上述C代码放入Core/Src目录。 - 添加CMSIS-NN库:在
.ioc文件中启用DSP库,并将CMSIS-NN源码(CMSIS/NN/Source)加入编译路径。 - 编译并烧录到MCU,使用串口助手(115200-8-N-1)查看输出。
验证方法
通过串口观察输出:程序应打印分类结果: 0或1。为验证正确性,可将同一输入在PC上用Python运行原始浮点模型,比较分类结果是否一致。若一致,说明量化误差未影响分类正确性,部署成功。
预期输出示例(基于给定随机权重):
分类结果: 1,原始输出为两个int8值。实际数值取决于权重,但分类结果应与浮点模型一致。
常见问题
- 编译错误“未定义引用arm_fully_connected_s8”:确认CMSIS-NN源文件已加入编译,且包含
arm_nnfunctions.h。 - 输出恒为0:检查输入量化scale是否与训练时一致,权重导出时scale参数是否匹配。
- 内存不足:减小网络规模或使用
arm_fully_connected_s8的in-place模式(将输出缓冲区复用为输入)。