第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 序列化,文件紧凑且解析高效,非常适合资源受限的嵌入式环境。
支持的层类型包括:
- 卷积层:Conv2D、DepthwiseConv2D、Conv2DTranspose
- 池化层:MaxPooling2D、AveragePooling2D、GlobalAveragePooling2D
- 全连接层:Dense(支持 ReLU、Softmax、Sigmoid、HardSwish 等激活函数)
- 归一化层:BatchNormalization(自动融合到前一层卷积中)
- 元素运算:Add、Mul、Concatenate
- 形状操作:Reshape、Flatten、Squeeze、ExpandDims
转换方式:通过 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 支持要点:
- 支持 ONNX opset 版本 11~16
- 支持静态形状模型(动态 batch 维度需固定为 1)
- 支持算子:Conv、Relu、MaxPool、AveragePool、Gemm、BatchNormalization、Softmax、Reshape、Flatten、Concat、Add 等
- 不支持动态控制流(如 if/while 节点)
- 不支持未注册的自定义算子
3.1.3 Keras (.h5)
Keras H5 格式可以直接被 STM32Cube.AI 解析,底层会自动将其转换为内部表示。适合使用 tf.keras 构建和训练模型的工作流,无需额外的格式转换步骤。
注意事项:
- 推荐使用 tf.keras(TensorFlow 2.x 内置),而非独立 keras 包
- 自定义层需要提供序列化支持(get_config 方法)
- Functional API 和 Sequential API 均完整支持
- Subclassing API 支持有限,建议先转换为 Functional 模型
- 训练专用层(Dropout 等)在推理时自动禁用
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 从训练框架导出模型
导出模型时需要注意以下关键事项:
- 固定输入维度:MCU推理的输入形状必须在导出时确定,通常为
[1, H, W, C](TFLite 使用 NHWC 格式)或 [1, C, H, W](ONNX 使用 NCHW 格式)
- 移除训练专用层:Dropout、BatchNormalization(训练模式)等仅在训练时使用的层需要在导出前处理。BatchNormalization 的统计量应融合到卷积层参数中
- 统一激活函数:确保激活函数使用支持的类型。某些框架特有的激活函数(如 PyTorch 的 SiLU/Swish)可能需要替换为 ReLU 或 HardSwish
- 检查量化兼容性:如果计划使用量化模型,导出时应使用量化感知训练(QAT)或后训练量化(PTQ)的正确流程
- 命名输入输出节点:为模型的输入和输出张量指定明确的名称,便于后续在 C 代码中引用
3.2.2 STM32Cube.AI 转换引擎
STM32Cube.AI 的模型转换引擎执行以下核心步骤:
- 模型解析(Parse):读取模型文件,构建内部计算图表示(Network Graph)。此阶段会验证所有算子的兼容性,报告不支持的操作
- 图优化(Graph Optimization):执行算子融合(如 Conv+BN+ReLU 融合为单个算子)、常量折叠、冗余节点消除等优化,显著减少推理时的计算量和内存访问
- 内存规划(Memory Planning):根据目标 MCU 的内存层次(内部 SRAM、外部 SDRAM、Flash),分配张量缓冲区,最大化内存复用,最小化 RAM 占用
- 代码生成(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_2D | Conv | CONV2D | CMSIS-NN arm_convolve_s8 |
| DEPTHWISE_CONV_2D | Conv (group=N) | DWCONV2D | arm_depthwise_conv_s8 |
| FULLY_CONNECTED | Gemm | DENSE | arm_fully_connected_s8 |
| MAX_POOL_2D | MaxPool | MAXPOOL | arm_max_pool_s8 |
| AVERAGE_POOL_2D | AveragePool | AVGPOOL | arm_avgpool_s8 |
| SOFTMAX | Softmax | SOFTMAX | arm_softmax_s8 |
| RESIZE_BILINEAR | Resize | RESIZE | arm_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.transpose 或 np.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 数据增强策略
嵌入式场景下训练数据往往有限,数据增强是提升模型泛化能力的有效手段:
传感器数据增强
- 时间扭曲(Time Warping):对时间轴进行轻微拉伸或压缩,模拟不同的运动速度
- 噪声注入(Noise Injection):添加高斯噪声模拟传感器噪声,信噪比通常设为 20~40dB
- 信号翻转(Signal Flipping):对加速度计各轴取反,模拟传感器安装方向变化
- 窗口滑动(Window Shifting):在时间窗口内随机偏移几个采样点
- 幅度缩放(Amplitude Scaling):随机缩放信号幅度(0.8~1.2倍),模拟不同距离或力度
图像数据增强
- 旋转(Rotation):小角度旋转(±15°),模拟物体方向变化
- 翻转(Flip):水平翻转(注意:仅在语义不变时使用,如猫狗分类可用,文字识别不可用)
- 亮度/对比度调整:模拟不同光照条件
- 裁剪与缩放(Crop & Resize):模拟不同距离和视角
- 颜色抖动(Color Jitter):随机调整色调、饱和度
3.3.4 输入尺寸调整与通道顺序
MCU 上的模型输入尺寸通常远小于云端模型,需要根据任务需求和资源限制合理选择:
- 图像分类:32x32 或 64x64(而非云端的 224x224)
- 关键词检测:MFCC 特征 40x49 或 128x32
- 振动异常检测:加速度计窗口 128x3 或 256x3
通道顺序: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 量化时,需要提供校准数据集来确定量化参数(缩放因子和零点)。校准数据集的选择原则:
- 代表性:应覆盖实际部署时会遇到的所有场景和输入分布
- 多样性:包含所有类别的样本,避免类别不平衡导致量化偏差
- 规模适中:通常 100~500 个样本即可,过多不会显著提升精度但会增加校准时间
- 无异常值:排除明显错误或不合理的数据样本,异常值会严重影响量化范围
- 真实分布:最好从实际传感器采集,而非纯合成数据
3.4 预处理管道设计
在 MCU 上部署 AI 推理时,预处理管道的设计直接影响系统的实时性能和资源利用率。合理的管道设计需要在精度、速度和内存之间取得平衡。
3.4.1 传感器数据预处理
加速度计/陀螺仪数据
典型的 IMU 数据预处理流程:
- 重采样:统一采样率(如 100Hz),处理丢包和抖动
- 滤波:低通滤波去除高频噪声(截止频率根据应用设定,如手势识别 20Hz)
- 分段加窗:将连续数据流切分为固定长度的窗口(如 1秒=100采样点),窗口间可有重叠
- 归一化:将物理量(g, deg/s)映射到模型期望的输入范围
- 特征提取(可选):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;
}
}
音频数据预处理
音频数据通常需要转换为频谱特征后再输入模型:
- VAD(语音活动检测):检测有效语音段,减少无效推理
- 分帧加窗:25ms 帧长,10ms 帧移,汉明窗
- FFT 变换:将时域信号转为频域
- Mel 滤波器组:映射到 Mel 频率尺度(通常 40 个滤波器)
- 对数变换:取对数压缩动态范围
- DCT(可选):得到 MFCC 特征(通常取前 13 个系数)
在 Cortex-M4/M7 上,可以使用 CMSIS-DSP 库中的 arm_rfft_f32 和 arm_mel_filterbank_f32 函数高效实现上述流程。
3.4.2 图像数据预处理
MCU 上的图像预处理流程(以摄像头输入为例):
- 图像采集:从 DCMI/摄像头接口获取原始图像(通常为 RGB565 或 YUV422 格式)
- 颜色空间转换:RGB565 -> RGB888(如模型需要 RGB 输入)
- 缩放(Resize):使用硬件 JPEG 解码器或软件双线性插值缩放到模型输入尺寸
- 裁剪(Crop):中心裁剪或感兴趣区域(ROI)裁剪
- 归一化:像素值从 [0, 255] 映射到 [0, 1] 或 [-1, 1]
- 通道重排:确保 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 上的文本处理应用较少,但在某些场景(如本地关键词理解、简单意图识别)中仍有需求:
- 分词(Tokenization):使用字节对编码(BPE)或字符级分词,将文本拆分为 token 序列
- 词表映射:将每个 token 映射为整数 ID(词表大小通常限制在 1000~5000 以节省内存)
- 填充/截断:将序列长度固定为模型输入长度(如 32 或 64)
- 嵌入查找(Embedding Lookup):将 token ID 转换为嵌入向量。嵌入表可存储在 Flash 中
内存警告:Transformer 类模型的嵌入表和自注意力机制对 MCU 资源消耗极大。建议在 MCU 上使用简化模型(如 1D-CNN 文本分类),或将嵌入计算放在预处理阶段离线完成。
3.4.4 在MCU上实现预处理的策略
策略一:离线预处理
在 PC 端或上位机完成所有预处理,MCU 只接收处理好的数据。适合数据量小、实时性要求不高的场景(如离线数据记录分析)。
优点:MCU 代码简单,推理延迟低。缺点:依赖外部处理,不适合实时流数据。
策略二:在线预处理
在 MCU 上实时执行预处理。需要使用 CMSIS-DSP 等优化库确保处理速度满足实时要求。
优点:完全自主运行,低延迟。缺点:占用 CPU 资源,需要仔细优化代码。
策略三:硬件加速预处理
利用 MCU 的硬件外设加速预处理:
- 使用 DMA 进行数据搬运,减少 CPU 干预
- 使用硬件滤波器(如 SAI 接口的数字滤波器)处理音频
- 使用 DCMI + DMA 直接传输图像数据到内存
- 使用 STM32 的 FMAC(定点数学加速器)执行滤波运算
- 使用 JPEG 硬件编解码器加速图像缩放
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 中并合理对齐,可以显著提升吞吐量。
本章小结
- TFLite 是 STM32Cube.AI 支持最完善的格式,推荐作为首选导入方式
- PyTorch 模型需要先转换为 ONNX 格式再导入,注意固定 batch 维度
- 模型转换包含解析、图优化、内存规划、代码生成四个阶段
- 数据归一化参数必须从训练集计算,并在 MCU 端保持一致
- 数据增强是提升小数据集模型泛化能力的有效手段
- 预处理管道设计需要综合考虑精度、实时性和资源约束
- 双缓冲和 DMA 是实现高效流水线的关键技术
章节测验
完成以下10道选择题,检验你对本章知识的掌握程度。选择你认为正确的答案后点击"提交答案"。