第3章 模型导入与预处理

本章导读:本章将详细介绍如何将训练好的AI模型导入STM32Cube.AI工具链,涵盖支持的框架与格式、模型转换流程、数据集准备与预处理方法,以及针对MCU平台的预处理管道设计。掌握这些知识是成功部署边缘AI应用的关键前提。

3.1 支持的AI框架与模型格式

STM32Cube.AI 作为ST官方提供的嵌入式AI工具,支持从多种主流深度学习框架中导入训练好的模型。理解各框架的支持程度和限制,是选择合适训练方案的第一步。

3.1.1 TensorFlow Lite (.tflite)

TensorFlow Lite 是 STM32Cube.AI 支持最完善的格式,也是官方推荐的首选导入方式。.tflite 格式采用 FlatBuffers 序列化,文件紧凑且解析高效,非常适合资源受限的嵌入式环境。

支持的层类型包括:

转换方式:通过 tf.lite.TFLiteConverter 从 SavedModel 或 Keras H5 模型导出。

# TensorFlow Lite 模型导出示例 import tensorflow as tf model = tf.keras.models.load_model('my_model.h5') converter = tf.lite.TFLiteConverter.from_keras_model(model) converter.optimizations = [tf.lite.Optimize.DEFAULT] tflite_model = converter.convert() with open('model.tflite', 'wb') as f: f.write(tflite_model)

3.1.2 ONNX (.onnx)

ONNX(Open Neural Network Exchange)是微软和Facebook联合推出的开放模型交换格式。STM32Cube.AI 从 7.x 版本开始增强了对 ONNX 的支持,使其成为跨框架导入的重要桥梁。无论你使用 PyTorch、MXNet 还是其他框架,都可以先导出为 ONNX 再导入。

ONNX 支持要点:

3.1.3 Keras (.h5)

Keras H5 格式可以直接被 STM32Cube.AI 解析,底层会自动将其转换为内部表示。适合使用 tf.keras 构建和训练模型的工作流,无需额外的格式转换步骤。

注意事项:

3.1.4 PyTorch 间接导入

STM32Cube.AI 不直接支持 PyTorch 的 .pt/.pth 格式。需要先将 PyTorch 模型导出为 ONNX 格式,再通过 ONNX 导入。这是 PyTorch 用户必须了解的间接路径。

# PyTorch -> ONNX 导出示例 import torch import torchvision.models as models model = models.mobilenet_v2(pretrained=True) model.eval() dummy_input = torch.randn(1, 3, 224, 224) torch.onnx.export( model, dummy_input, "model.onnx", export_params=True, opset_version=13, input_names=['input'], output_names=['output'], dynamic_axes={'input': {0: 'batch_size'}} )
注意:PyTorch 导出 ONNX 时,dynamic_axes 中的 batch_size 维度在导入 STM32Cube.AI 后必须固定为 1。MCU 推理始终以单样本方式进行。此外,某些 PyTorch 特有的操作(如 torch.jit)无法通过 ONNX 导出。

3.1.5 各框架支持对比

框架/格式支持层数量转换成功率推荐度备注
TFLite (.tflite)40+95%⭐⭐⭐⭐⭐首选格式,支持最完善
ONNX (.onnx)35+88%⭐⭐⭐⭐跨平台桥梁,PyTorch必用
Keras (.h5)38+92%⭐⭐⭐⭐TF生态直接导入
PyTorch (.pt)---需先转ONNX再导入
模型兼容性检查要点:在开始训练之前,建议先查阅 STM32Cube.AI 的 Release Notes 中的 "Supported Operators" 列表,确认所用算子均在支持范围内。同时关注目标 MCU 的 Flash 和 RAM 限制,避免模型过大无法部署。可以使用 stm32cubeai validate 命令在转换前检查模型兼容性。

3.2 模型转换流程

将训练好的模型转换为可在 STM32 上运行的 C 代码,是 STM32Cube.AI 的核心功能。理解转换流程的每个环节,有助于排查问题并优化最终生成的代码。

3.2.1 从训练框架导出模型

导出模型时需要注意以下关键事项:

  1. 固定输入维度:MCU推理的输入形状必须在导出时确定,通常为 [1, H, W, C](TFLite 使用 NHWC 格式)或 [1, C, H, W](ONNX 使用 NCHW 格式)
  2. 移除训练专用层:Dropout、BatchNormalization(训练模式)等仅在训练时使用的层需要在导出前处理。BatchNormalization 的统计量应融合到卷积层参数中
  3. 统一激活函数:确保激活函数使用支持的类型。某些框架特有的激活函数(如 PyTorch 的 SiLU/Swish)可能需要替换为 ReLU 或 HardSwish
  4. 检查量化兼容性:如果计划使用量化模型,导出时应使用量化感知训练(QAT)或后训练量化(PTQ)的正确流程
  5. 命名输入输出节点:为模型的输入和输出张量指定明确的名称,便于后续在 C 代码中引用

3.2.2 STM32Cube.AI 转换引擎

STM32Cube.AI 的模型转换引擎执行以下核心步骤:

  1. 模型解析(Parse):读取模型文件,构建内部计算图表示(Network Graph)。此阶段会验证所有算子的兼容性,报告不支持的操作
  2. 图优化(Graph Optimization):执行算子融合(如 Conv+BN+ReLU 融合为单个算子)、常量折叠、冗余节点消除等优化,显著减少推理时的计算量和内存访问
  3. 内存规划(Memory Planning):根据目标 MCU 的内存层次(内部 SRAM、外部 SDRAM、Flash),分配张量缓冲区,最大化内存复用,最小化 RAM 占用
  4. 代码生成(Code Generation):生成高度优化的 C 代码,包含网络参数(权重、偏置)的 C 数组和推理函数入口
# STM32Cube.AI CLI 转换命令示例 stm32cubeai generate \ --name my_network \ --model model.tflite \ --target-board STM32H7 \ --compression 1 \ --verbosity 1 # 输出文件: # - network.c / network.h (推理代码) # - network_data.c / .h (权重数据) # - network_config.h (配置宏定义)

3.2.3 转换过程中的算子映射

STM32Cube.AI 内部维护了一套算子映射表,将不同框架的算子统一映射到优化的 ARM Cortex-M 内核实现:

源算子 (TFLite)源算子 (ONNX)内部表示ARM 优化实现
CONV_2DConvCONV2DCMSIS-NN arm_convolve_s8
DEPTHWISE_CONV_2DConv (group=N)DWCONV2Darm_depthwise_conv_s8
FULLY_CONNECTEDGemmDENSEarm_fully_connected_s8
MAX_POOL_2DMaxPoolMAXPOOLarm_max_pool_s8
AVERAGE_POOL_2DAveragePoolAVGPOOLarm_avgpool_s8
SOFTMAXSoftmaxSOFTMAXarm_softmax_s8
RESIZE_BILINEARResizeRESIZEarm_resize_bilinear_s8

3.2.4 常见转换错误及解决方法

错误1:Unsupported operator

原因:模型中包含了 STM32Cube.AI 不支持的算子。

解决:替换为等效的支持算子组合,或使用更简单的模型架构。例如将 LayerNormalization 替换为 BatchNormalization。

错误2:Model too large for target

原因:模型参数量超过目标 MCU 的 Flash 容量。

解决:应用 INT8 量化(可将模型缩小4倍)、结构化剪枝或使用更小的模型架构(如 MobileNet 替代 ResNet)。

错误3:Memory allocation failed

原因:推理所需的激活内存超过 MCU RAM。

解决:启用内存复用优化(--compression 参数)、减小中间张量尺寸、使用外部 SDRAM。

错误4:Shape mismatch after conversion

原因:NHWC/NCHW 格式转换时维度处理不正确。

解决:确保导出时输入格式正确,TFLite 使用 NHWC,ONNX 使用 NCHW。使用 tf.transposenp.transpose 调整维度顺序。

3.2.5 模型验证

转换完成后,必须验证转换前后的模型输出一致性。这是确保部署可靠性的关键步骤:

import numpy as np import tensorflow as tf # 1. 准备相同的测试输入 test_input = np.random.randn(1, 128, 128, 3).astype(np.float32) # 2. 原始模型推理 interpreter = tf.lite.Interpreter(model_content=open('model.tflite','rb').read()) interpreter.allocate_tensors() input_details = interpreter.get_input_details() interpreter.set_tensor(input_details[0]['index'], test_input) interpreter.invoke() original_output = interpreter.get_tensor( interpreter.get_output_details()[0]['index']) # 3. 与 STM32 板端推理结果对比 # (通过串口或调试接口读取板端输出) board_output = read_board_output_via_serial() # 自定义函数 # 4. 计算误差 max_diff = np.max(np.abs(original_output - board_output)) print(f"最大绝对误差: {max_diff}") assert max_diff < 1e-3, "转换验证失败!误差超出阈值"
验证技巧:除了数值比较外,还应使用多组边界值输入(全零、全一、最大值、最小值)进行测试,确保量化后的模型在各种输入条件下都能正常工作。

3.3 数据集准备与预处理

高质量的训练数据和恰当的预处理是模型性能的基石。在嵌入式AI场景中,数据集的格式和预处理策略还需要考虑MCU端的部署约束。

3.3.1 训练数据集的格式要求

STM32Cube.AI 及相关的 NanoEdge AI Studio 支持以下数据格式:

C 数组格式

直接将数据嵌入 C 源码中,适合小型数据集或校准数据集:

/* 校准数据集 - C数组格式 */ const float calibration_data[NUM_SAMPLES][INPUT_SIZE] = { {0.12, -0.34, 0.56, ...}, /* 样本1 */ {0.78, -0.91, 0.23, ...}, /* 样本2 */ /* ... 更多样本 ... */ };

二进制文件 (.bin / .raw)

以原始二进制格式存储,文件紧凑,加载速度快。需要确保字节序(大端/小端)与目标平台一致:

# Python 生成二进制数据文件 import numpy as np data = np.load('sensor_data.npy') # shape: (1000, 128, 3) data = data.astype(np.float32) data.tofile('sensor_data.bin') # 在 MCU 端通过链接脚本将 .bin 放入指定 Flash 区域 # 或使用文件系统(FATFS)在运行时加载

3.3.2 数据归一化方法

归一化是预处理中最关键的步骤之一,直接影响模型的收敛速度和推理精度。

方法公式输出范围适用场景
Min-Max 归一化x' = (x - min) / (max - min)[0, 1] 或 [-1, 1]传感器数据、图像像素
Z-Score 标准化x' = (x - μ) / σ均值0,标准差1音频特征、时序信号
百分比归一化x' = x / max_abs[-1, 1]加速度计数据
对数变换x' = log(1 + x)[0, +inf)频谱能量特征
重要提示:归一化参数(min、max、均值、标准差)必须在训练集上计算,然后同时应用于训练集、验证集和测试集。在 MCU 端部署时,这些参数需要硬编码到预处理代码中,确保与训练时完全一致。

3.3.3 数据增强策略

嵌入式场景下训练数据往往有限,数据增强是提升模型泛化能力的有效手段:

传感器数据增强

图像数据增强

3.3.4 输入尺寸调整与通道顺序

MCU 上的模型输入尺寸通常远小于云端模型,需要根据任务需求和资源限制合理选择:

通道顺序:TFLite 使用 NHWC(高-宽-通道),ONNX 使用 NCHW(通道-高-宽)。在 Python 端预处理和 MCU 端数据输入时,必须确保通道顺序一致,否则会导致推理结果完全错误。

# 图像通道顺序转换 import numpy as np # ONNX 格式 (NCHW) -> TFLite 格式 (NHWC) img_nchw = np.random.randn(1, 3, 64, 64).astype(np.float32) img_nhwc = np.transpose(img_nchw, (0, 2, 3, 1)) # -> (1, 64, 64, 3) print(f"NCHW shape: {img_nchw.shape}") print(f"NHWC shape: {img_nhwc.shape}")

3.3.5 校准数据集的选择

当使用 INT8 量化时,需要提供校准数据集来确定量化参数(缩放因子和零点)。校准数据集的选择原则:

  1. 代表性:应覆盖实际部署时会遇到的所有场景和输入分布
  2. 多样性:包含所有类别的样本,避免类别不平衡导致量化偏差
  3. 规模适中:通常 100~500 个样本即可,过多不会显著提升精度但会增加校准时间
  4. 无异常值:排除明显错误或不合理的数据样本,异常值会严重影响量化范围
  5. 真实分布:最好从实际传感器采集,而非纯合成数据

3.4 预处理管道设计

在 MCU 上部署 AI 推理时,预处理管道的设计直接影响系统的实时性能和资源利用率。合理的管道设计需要在精度、速度和内存之间取得平衡。

3.4.1 传感器数据预处理

加速度计/陀螺仪数据

典型的 IMU 数据预处理流程:

  1. 重采样:统一采样率(如 100Hz),处理丢包和抖动
  2. 滤波:低通滤波去除高频噪声(截止频率根据应用设定,如手势识别 20Hz)
  3. 分段加窗:将连续数据流切分为固定长度的窗口(如 1秒=100采样点),窗口间可有重叠
  4. 归一化:将物理量(g, deg/s)映射到模型期望的输入范围
  5. 特征提取(可选):FFT 频谱特征、统计特征(均值、方差、峰值)等
/* MCU 端 IMU 预处理示例 */ void preprocess_imu(float *raw_data, int length, float *output) { /* 1. 低通滤波 (一阶 IIR) */ float alpha = 0.1f; float filtered[MAX_LEN]; filtered[0] = raw_data[0]; for (int i = 1; i < length; i++) { filtered[i] = alpha * raw_data[i] + (1 - alpha) * filtered[i-1]; } /* 2. Min-Max 归一化到 [-1, 1] */ float vmin = -4.0f, vmax = 4.0f; for (int i = 0; i < length; i++) { output[i] = 2.0f * (filtered[i] - vmin) / (vmax - vmin) - 1.0f; } }

音频数据预处理

音频数据通常需要转换为频谱特征后再输入模型:

  1. VAD(语音活动检测):检测有效语音段,减少无效推理
  2. 分帧加窗:25ms 帧长,10ms 帧移,汉明窗
  3. FFT 变换:将时域信号转为频域
  4. Mel 滤波器组:映射到 Mel 频率尺度(通常 40 个滤波器)
  5. 对数变换:取对数压缩动态范围
  6. DCT(可选):得到 MFCC 特征(通常取前 13 个系数)

在 Cortex-M4/M7 上,可以使用 CMSIS-DSP 库中的 arm_rfft_f32arm_mel_filterbank_f32 函数高效实现上述流程。

3.4.2 图像数据预处理

MCU 上的图像预处理流程(以摄像头输入为例):

  1. 图像采集:从 DCMI/摄像头接口获取原始图像(通常为 RGB565 或 YUV422 格式)
  2. 颜色空间转换:RGB565 -> RGB888(如模型需要 RGB 输入)
  3. 缩放(Resize):使用硬件 JPEG 解码器或软件双线性插值缩放到模型输入尺寸
  4. 裁剪(Crop):中心裁剪或感兴趣区域(ROI)裁剪
  5. 归一化:像素值从 [0, 255] 映射到 [0, 1] 或 [-1, 1]
  6. 通道重排:确保 RGB/BGR 顺序与模型训练时一致
/* 图像预处理关键步骤 */ void preprocess_image(uint16_t *rgb565_in, float *output, int src_w, int src_h, int dst_w, int dst_h) { for (int y = 0; y < dst_h; y++) { for (int x = 0; x < dst_w; x++) { /* 双线性插值缩放 */ float sx = (float)x * src_w / dst_w; float sy = (float)y * src_h / dst_h; /* RGB565 转 RGB888 并归一化 */ uint16_t c = rgb565_in[(int)sy * src_w + (int)sx]; int idx = (y * dst_w + x) * 3; output[idx+0] = ((c>>11)&0x1F) / 31.0f; output[idx+1] = ((c>>5)&0x3F) / 63.0f; output[idx+2] = (c&0x1F) / 31.0f; } } }

3.4.3 文本数据预处理

虽然 MCU 上的文本处理应用较少,但在某些场景(如本地关键词理解、简单意图识别)中仍有需求:

  1. 分词(Tokenization):使用字节对编码(BPE)或字符级分词,将文本拆分为 token 序列
  2. 词表映射:将每个 token 映射为整数 ID(词表大小通常限制在 1000~5000 以节省内存)
  3. 填充/截断:将序列长度固定为模型输入长度(如 32 或 64)
  4. 嵌入查找(Embedding Lookup):将 token ID 转换为嵌入向量。嵌入表可存储在 Flash 中
内存警告:Transformer 类模型的嵌入表和自注意力机制对 MCU 资源消耗极大。建议在 MCU 上使用简化模型(如 1D-CNN 文本分类),或将嵌入计算放在预处理阶段离线完成。

3.4.4 在MCU上实现预处理的策略

策略一:离线预处理

在 PC 端或上位机完成所有预处理,MCU 只接收处理好的数据。适合数据量小、实时性要求不高的场景(如离线数据记录分析)。

优点:MCU 代码简单,推理延迟低。缺点:依赖外部处理,不适合实时流数据。

策略二:在线预处理

在 MCU 上实时执行预处理。需要使用 CMSIS-DSP 等优化库确保处理速度满足实时要求。

优点:完全自主运行,低延迟。缺点:占用 CPU 资源,需要仔细优化代码。

策略三:硬件加速预处理

利用 MCU 的硬件外设加速预处理:

3.4.5 预处理与推理的流水线设计

为了实现高吞吐量的实时推理,可以采用双缓冲(Double Buffering)或环形缓冲区(Ring Buffer)实现预处理与推理的流水线并行:

/* 双缓冲流水线示例 */ #define BUF_SIZE 1024 float buf_a[BUF_SIZE]; // 缓冲区 A float buf_b[BUF_SIZE]; // 缓冲区 B void pipeline_loop(void) { float *fill_buf = buf_a; // 预处理写入 float *infer_buf = buf_b; // 推理读取 while (1) { /* 1. 预处理填充 fill_buf */ sensor_read_and_preprocess(fill_buf, BUF_SIZE); /* 2. 交换缓冲区指针 */ float *tmp = fill_buf; fill_buf = infer_buf; infer_buf = tmp; /* 3. 推理使用 infer_buf */ aiRun(infer_buf, output_buf); } }
性能提示:在 Cortex-M7(如 STM32H7)上,可以利用指令缓存(I-Cache)和数据缓存(D-Cache)进一步优化预处理和推理的性能。将权重数据放在 Flash 中并启用预取,将激活缓冲区放在 SRAM 中并合理对齐,可以显著提升吞吐量。

本章小结

章节测验

完成以下10道选择题,检验你对本章知识的掌握程度。选择你认为正确的答案后点击"提交答案"。