4、模型准备与转换(上):Keras训练全连接网络与X-CUBE-AI转换
好,咱们正式开始动手了。这一章的目标很明确:在PC上训练一个能识别手写数字的神经网络,然后把它变成STM32能跑的模型。听起来有点绕,其实就两步——先训练,再转换。
我个人习惯把模型准备阶段叫做“地基工程”。地基没打好,后面部署时各种踩坑。你想想看,一个在电脑上准确率99%的模型,转到单片机上可能直接崩掉,或者精度掉到没法看。为什么?很多时候就是转换这一步出了问题。
4.1 为什么选MNIST + 全连接网络?
MNIST数据集,说白了就是一堆28x28像素的手写数字图片,0到9共10类。每个像素值0到255,灰度图。这个数据集在嵌入式部署里特别适合做“Hello World”——网络小、数据简单、验证方便。
全连接网络(Dense Network)呢?结构最简单,没有卷积、没有循环,就是一层层神经元全连起来。我刚开始做嵌入式AI时,第一个项目就是拿全连接网络做手势识别。当时觉得“这么简单的网络,转换肯定没问题”,结果卡在输入输出格式上折腾了两天。嗯,这些坑咱们今天一并填上。
4.2 用Keras训练MNIST分类网络
Keras是TensorFlow的高层API,写起来很顺手。咱们直接上代码,边写边解释。
4.2.1 数据加载与预处理
import tensorflow as tf
from tensorflow import keras
from tensorflow.keras import layers
import numpy as np
# 加载MNIST数据集
(x_train, y_train), (x_test, y_test) = keras.datasets.mnist.load_data()
# 归一化:把0-255的像素值缩放到0-1之间
x_train = x_train.astype('float32') / 255.0
x_test = x_test.astype('float32') / 255.0
# 展平:把28x28的二维图像变成一维向量(784个特征)
x_train = x_train.reshape(-1, 784)
x_test = x_test.reshape(-1, 784)
# 标签转one-hot编码
y_train = keras.utils.to_categorical(y_train, 10)
y_test = keras.utils.to_categorical(y_test, 10)
print(f'训练集形状: {x_train.shape}')
print(f'测试集形状: {x_test.shape}')
这里有个细节要注意:归一化。我曾经见过有人直接把原始像素值喂给网络,结果训练时梯度爆炸,损失值直接飞上天。归一化到[0,1]区间,能让训练更稳定,收敛更快。
4.2.2 构建模型
model = keras.Sequential([
layers.Dense(128, activation='relu', input_shape=(784,)),
layers.Dense(64, activation='relu'),
layers.Dense(10, activation='softmax')
])
model.summary()
这个网络结构很简单:输入层784个神经元(对应28x28像素),中间两个隐藏层(128和64个神经元),输出层10个神经元(对应0-9十个数字)。激活函数用ReLU,输出层用Softmax做概率分布。
4.2.3 编译与训练
model.compile(
optimizer='adam',
loss='categorical_crossentropy',
metrics=['accuracy']
)
history = model.fit(
x_train, y_train,
batch_size=128,
epochs=10,
validation_data=(x_test, y_test)
)
训练过程大概几十秒到一两分钟,看你的CPU性能。训练结束后,测试集准确率通常在98%左右。这个精度对于演示完全够用了。
4.2.4 评估与保存
# 评估模型
test_loss, test_acc = model.evaluate(x_test, y_test)
print(f'测试集准确率: {test_acc:.4f}')
# 保存为.h5文件
model.save('mnist_fc_model.h5')
print('模型已保存为 mnist_fc_model.h5')
保存成.h5文件,这是Keras的标准格式。X-CUBE-AI可以直接读取这个文件进行转换。
4.3 模型转换:X-CUBE-AI实战
模型训练好了,接下来就是把它变成STM32能理解的东西。X-CUBE-AI是ST官方提供的工具,能把训练好的模型转换成针对STM32优化的C代码。
4.3.1 安装X-CUBE-AI
有两种方式:
- 作为STM32CubeMX的扩展包:在CubeMX的软件包管理器里安装X-CUBE-AI
- 独立命令行工具:下载X-CUBE-AI的独立包,用命令行操作
我个人推荐用CubeMX的方式,图形界面操作更直观,适合初学者。命令行方式适合自动化流程,后面咱们再讲。
4.3.2 在CubeMX中配置模型转换
打开STM32CubeMX,新建一个项目,选好你的STM32型号(比如STM32F407VG)。然后:
- 点击 Software Packs → Select Components
- 找到 X-CUBE-AI,勾选并选择版本
- 在 Pinout & Configuration 标签页,找到 Software Packs → X-CUBE-AI → Core
- 在 Model 选项卡里,点击 Add,选择你的
mnist_fc_model.h5文件
加载成功后,你会看到模型的结构信息:输入形状、输出形状、各层参数等。这时候可以点击 Analyze 按钮,工具会估算模型在目标芯片上的RAM和Flash占用。
| 指标 | 估算值(STM32F407) | 说明 |
|---|---|---|
| Flash占用 | 约 120 KB | 权重+代码 |
| RAM占用 | 约 30 KB | 输入输出缓冲区+中间激活 |
| 单次推理时间 | 约 5-10 ms | 取决于主频和优化等级 |
4.3.3 生成代码
分析通过后,点击 Generate Code。CubeMX会生成一个完整的工程,里面包含了:
network.c/network.h:模型定义和推理函数network_data.c:权重数据(以常量数组形式存储)ai_platform_interface.c:平台适配层
生成的代码可以直接编译烧录到STM32上。调用推理函数只需要三步:
// 1. 定义输入输出缓冲区
float input[784];
float output[10];
// 2. 填充输入数据(比如从摄像头或串口获取的图像数据)
// ... 这里省略数据采集代码 ...
// 3. 执行推理
ai_network_run(input, output);
// 4. 处理输出结果
int predicted_class = argmax(output, 10);
就这么简单。你不需要关心底层怎么实现矩阵乘法、怎么加载权重,X-CUBE-AI都帮你封装好了。
4.4 转换过程中的常见问题
我整理了几个实际项目中经常遇到的问题,你遇到了可以直接对照排查:
| 问题现象 | 可能原因 | 解决方案 |
|---|---|---|
| 模型加载失败 | Keras版本不兼容 | 使用TF 2.4~2.10重新保存模型 |
| RAM超限 | 模型太大或缓冲区分配不当 | 减小网络规模,或使用优化等级-O2 |
| 推理结果全为0 | 输入数据未归一化 | 确保输入值在[0,1]范围内 |
| 推理结果全为1 | Softmax输出未正确解析 | 检查输出缓冲区大小是否为10个float |
model = keras.models.load_model('xxx.h5') 检查一下模型结构,确保没有多余的前处理层。
4.5 本章知识体系
下面这张图帮你理清整个流程:
这张图把整个流程分成三个阶段:训练、导出、转换。每个阶段都有明确的输入输出和注意事项。你跟着这个流程走,基本不会出大问题。
4.6 小结
这一章咱们完成了两件大事:
- 训练了一个全连接网络,在MNIST上达到98%+的准确率
- 用X-CUBE-AI把它转换成了STM32可执行的C代码
转换后的代码可以直接集成到你的STM32工程里。下一章咱们会真正在硬件上跑起来,看看推理效果如何。不过在那之前,我建议你先自己动手走一遍这个流程——训练、保存、转换,每一步都亲手做一次。遇到问题不要慌,对照上面的表格排查,基本都能解决。