4、模型准备与转换(上):Keras训练全连接网络与X-CUBE-AI转换

好,咱们正式开始动手了。这一章的目标很明确:在PC上训练一个能识别手写数字的神经网络,然后把它变成STM32能跑的模型。听起来有点绕,其实就两步——先训练,再转换。

我个人习惯把模型准备阶段叫做“地基工程”。地基没打好,后面部署时各种踩坑。你想想看,一个在电脑上准确率99%的模型,转到单片机上可能直接崩掉,或者精度掉到没法看。为什么?很多时候就是转换这一步出了问题。

4.1 为什么选MNIST + 全连接网络?

MNIST数据集,说白了就是一堆28x28像素的手写数字图片,0到9共10类。每个像素值0到255,灰度图。这个数据集在嵌入式部署里特别适合做“Hello World”——网络小、数据简单、验证方便。

全连接网络(Dense Network)呢?结构最简单,没有卷积、没有循环,就是一层层神经元全连起来。我刚开始做嵌入式AI时,第一个项目就是拿全连接网络做手势识别。当时觉得“这么简单的网络,转换肯定没问题”,结果卡在输入输出格式上折腾了两天。嗯,这些坑咱们今天一并填上。

核心要点: 全连接网络虽然结构简单,但它是理解模型转换全流程的最佳起点。后面部署CNN、RNN时,很多原理是相通的。

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做概率分布。

我的建议: 隐藏层神经元数量不要贪多。128+64对于MNIST已经足够,再多反而容易过拟合,而且模型体积变大,STM32上跑不动。

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可以直接读取这个文件进行转换。

避坑指南: 我曾经遇到过保存的.h5文件在X-CUBE-AI里解析失败的情况。后来发现是Keras版本不兼容。建议使用TensorFlow 2.4~2.10之间的版本,太新或太旧都可能出问题。

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)。然后:

  1. 点击 Software Packs → Select Components
  2. 找到 X-CUBE-AI,勾选并选择版本
  3. 在 Pinout & Configuration 标签页,找到 Software Packs → X-CUBE-AI → Core
  4. 在 Model 选项卡里,点击 Add,选择你的 mnist_fc_model.h5 文件

加载成功后,你会看到模型的结构信息:输入形状、输出形状、各层参数等。这时候可以点击 Analyze 按钮,工具会估算模型在目标芯片上的RAM和Flash占用。

指标 估算值(STM32F407) 说明
Flash占用 约 120 KB 权重+代码
RAM占用 约 30 KB 输入输出缓冲区+中间激活
单次推理时间 约 5-10 ms 取决于主频和优化等级
关键点: 这个分析结果非常重要。如果Flash或RAM超了,你就得考虑换更大容量的芯片,或者压缩模型。咱们后面会讲模型压缩技巧。

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
我曾经踩过的坑: 有一次模型转换后推理结果总是错的,排查了两天才发现——我在训练时用了数据增强(Data Augmentation),但转换时忘了关闭。X-CUBE-AI不支持训练时的数据增强层,需要先导出不带增强层的模型。切记,转换前用 model = keras.models.load_model('xxx.h5') 检查一下模型结构,确保没有多余的前处理层。

4.5 本章知识体系

下面这张图帮你理清整个流程:

模型准备与转换流程 阶段1:训练 Keras构建+训练MNIST 阶段2:导出 保存为.h5文件 阶段3:转换 X-CUBE-AI生成C代码 训练子步骤 1. 加载MNIST数据集 2. 归一化+展平 3. 构建全连接网络 导出注意事项 • 检查Keras版本兼容性 • 移除数据增强层 • 确认输入输出形状 转换输出产物 • network.c / network.h • network_data.c(权重) • ai_platform_interface.c 💡 核心原则 训练环境(PC)与部署环境(STM32)分离,但数据格式必须一致。 归一化、输入形状、数据类型——任何一处不匹配,推理结果都会出错。

这张图把整个流程分成三个阶段:训练、导出、转换。每个阶段都有明确的输入输出和注意事项。你跟着这个流程走,基本不会出大问题。

4.6 小结

这一章咱们完成了两件大事:

  • 训练了一个全连接网络,在MNIST上达到98%+的准确率
  • 用X-CUBE-AI把它转换成了STM32可执行的C代码

转换后的代码可以直接集成到你的STM32工程里。下一章咱们会真正在硬件上跑起来,看看推理效果如何。不过在那之前,我建议你先自己动手走一遍这个流程——训练、保存、转换,每一步都亲手做一次。遇到问题不要慌,对照上面的表格排查,基本都能解决。

小练习: 试着把隐藏层改成256+128个神经元,看看模型体积和推理时间的变化。再试试把激活函数从ReLU换成tanh,看看准确率有什么不同。这些实验能帮你理解网络结构对部署的影响。