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;
}

编译与运行

  1. 在STM32CubeIDE中新建C项目,将model_weights.h和上述C代码放入Core/Src目录。
  2. 添加CMSIS-NN库:在.ioc文件中启用DSP库,并将CMSIS-NN源码(CMSIS/NN/Source)加入编译路径。
  3. 编译并烧录到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模式(将输出缓冲区复用为输入)。