单片机_简单AI模型训练与部署__从0到0.9

IDE: CLion        STM32CubeMX

MCU: STM32F407VET6

CubeAI:10.0.0

Build System: CMake

一、简介

        以求知为导向,从问题到寻求问题解决的方法,以兴趣驱动学习。虽从0,但不到1,剩下的那一小步将由你迈出。

        本篇主要目的是体验一次完整的简单AI模型部署流程,从数据采集到模型创建与训练,再到部署单片机上。所谓部署其实就是把模型转换为C语言代码,添加到我们的程序中。

        选定的训练方向也非常简单,既不是手势、语音识别这些有一定难度的模型,也不是使用一些第三方的预训练模型,而是简简单单地从“调包”开始,搭建一个判断输入数字大小的小模型。比如判断输入数据是否小于24。最终生成的C代码其内存、存储占用均为10KB,也可缩减至不到1KB

        既然是“从零开始”,那么就不需要介绍太多复杂的术语解释,但一些基本的概念还是需要了解的。该篇最主要的目的就是体验,不需要知道太多为什么,真正上手实践后,再自行学习

二、流程

1,STM32CubeMX的AI插件

        想要触碰一个未曾熟知的领域,最重要的是要搜集信息,了解要做什么、怎么做,然后简单体验一番。那么第一步来了,我们的问题很简单,stm32单片机上怎么跑AI。带着这个问题我们使用搜索引擎可以得到一些博客,什么模型搭建、部署什么的可能也听不大懂。

        但从这些博客里我们可以找到一个共同点,那就是都使用到了STM32CubeMX,虽然AI模型相关的不太懂,但这个工具软件可太熟了。

① X-CUBE-AI工具

        从这里我们可以获取另一个关键点——就目前所获取的信息来看,stm32单片机上跑AI应是依赖STM32CubeMX的AI插件的。

        新建一个STM32Cube的工程,我们可以看到,在中间件和软件包这里有一个X-Cube-AI的工具(插件)

点击后,会弹出这样的界面,可以看到目前最新版是10.0.0,点击Install进行安装

然后展开,勾选Core,并选择Application Template(应用模板),最后点击Ok

等待它下载扩展包,不过速度方面很是着急,大概几百KB/s

下载完成后,再点击X-CUBE_AI就可以进入配置项了

可以点击右上角的Add network来添加神经网络了

②开启CRC

        此外,如果想要部署的AI能正常运行,那么必须要开启CRC

③文档(重要)

        搜索手册文档,如图第一个就是我们要下载的文档

        X-CUBE-AI入门指南下载链接

        手册可以说是非常详细,且是友好的中文❤️,省去了大量的时间。如果你看完了本篇博客,知道了大概的开发流程,那么想要再深入一步,就可以反复翻阅最新的指导手册,种种不解之处或许就迎刃而解了

        这个指南有个奇怪之处,目录是放在最后几页的


 

2,AI模型

①何为AI模型

        当你兴冲冲地使用CubeMX上的AI插件时,你可能会发现,缺少一件东西——AI模型

        回想前面回答,DS说过要准备模型

        虽然网上相关博客有不少,什么手势识别模型、神经网络算法等等,但几乎没有几个是直接给你一个AI模型用的或者教你搭建一个模型,要么介绍各种算法充斥种种专业术语什么的,要么是到官网的github或者云盘上找。总之对于初学者而言,是有一些麻烦的

        这属于“会了不难,难了不会”,也就是有一定门槛。那么我们接下来就继续发挥“不会就问”的精神

……

        从这个问答,我们知道神经网络模型是有多种格式的,这一点在CubeAI插件里可以找到

        同时,DS也指明了一条道路,让我们自己训练一个轻量级模型。不过呢,它举的这些例子更偏向实际应用,我们还是以“判断一个数字是否小于24”为我们训练AI模型的目的,模型格式选择Keras/TensorFlow Lite(后面你会理解为什么这样选择)

②如何训练一个模型

        继续向DS提问

        从回答中可以看出,确定问题后,我们只需要收集数据训练模型即可。无论是收集数据(初印象自然是Excel),还是训练模型(使用python编程),都是非常清晰的过程。对于我们本次的目的,需要把数归类为小于24和不小于24,那么可定义为二分类问题(顾名思义,分成两个类别),毕竟有“小于24”和“不小于24”这两个类别。

③数据采集

        当你对回答中的一些概念含混不清的时候,你可以把自己的理解、猜测主动反问给DS。无论你的想法是否正确,你都将被引向一条正确的道路

        回答里,DS提供了一个很好的想法,那就是生成模拟数据,对于我们的此次目的来说,这无疑是很方便的

        既然确定了步骤,不妨问问更细节的一些东西,比如

……

        以我们当前要解决的问题,自然是选择CSV文件。打开CSV文件后你会发现这跟Excel里常见表格大差不差,第一行是标签,通过英文逗号隔开,每个标签下都是一列数据。

        下图中可以看出,左边的就是我们要输入的值(布尔值),右边就是我们需要给数据进行“人工标注”的类别,1表示小于24,0表示不小于24

        训练AI模型时,会用你收集到的这个数据集训练。以图中这个数据为例(二分类问题),所谓训练过程就是给它左边的输入数据,让AI模型输出,然后与数据集中的右边的输出对比,来判断AI预测的结果,之后AI再不断调整权重、参数什么的,让下次预测更加精准。

        知道了这些信息后,你可以自行收集数据。比如一些波形数据,图像相关的什么矩阵、像素、灰度之类的等等

④训练模型

        此处的“训练模型”是训练一个模型中的一个步骤,与“采集数据”同级。此外,我们需要清楚,STM32上跑的这个AI模型其实是神经网络模型,它们之间的包含关系是这样的:

        AI(人工智能)>  机器学习 > 深度学习 >  神经网络模型(非传统神经网络)

        而在神经网络中,我们常常能见到下面这种形式的结构图,也就是神经网络的“层”。它一共分为输入层、隐藏层和输出层。输入层很好理解,就是输入数据的层,输出层自然就是输出结果的层。隐藏层就是对数据进行某种不可描述的事情的层,也就是我们常常说神经网络模型是个“黑盒子”,即输入可知,输出也可知,但你不知道隐藏层究竟对数据做了多少次不可描述的操作。

        那么自然,隐藏层可以包含很多层,比如CNN(卷积层)、RNN等,你也许经常听到它们的名称。没听过也没关系,遇到这些陌生术语,一带而过即可,用几遍就熟悉了。这些所谓的“层”,其实就是对数据的一次操作。

        OK,那么我们回归主题,前面回答中出现过Keras TensorFlow Lite,除了是神经网络的格式外,它们也分别对应一种神经网络框架(你也可以理解为库或包)。我们此时使用的是Keras框架,为什么呢?因为Keras可以让你快速搭建自己的模型结构,没有复杂的操作,只需一味地向括号里添加你所希望的层。

        之前,STM32CubeMX.AI对Keras格式的支持其实并不算好,一些高级层CubeAI暂时无法将其转换为C代码,转换时就会表现为一些奇怪的提示错误。即使是它支持的层,比如CNN,直接输出为.keras格式,那么转换时仍可能会出现不支持的情况。

        具体支持的层有哪些,需要对照手册

        好在模型格式之间可以相互转换,例如将 Keras 模型(.h5SavedModel)转换为 TensorFlow Lite(.tflite)格式,而 CubeAI 对这种服务于嵌入式的框架的支持是很好的。

⑤推理

        推理,也就是预测,就是输入数据让AI模型输出预测的结果。说白了就是使用训练好的AI模型

三、采集数据

        这里不说怎么安装Pycharm什么的,因为这是最基本的能力


        正如前面所言,我们的训练目的是判断输入数据是否小于24。现在我们把训练目的具体化:在0-100内,判断出这个数据是否小于24。那么我们就需要生成这样的标注数据,为了保真,还得让数据随机起来,且小于24的和大于24的概率还不同,以增加些许难度。

        这个数据可以使用python生成,那么怎么写python脚本呢?从变量命名、标识符开始学一遍python?那大可不必,我们不需要从头开始学习python(因为你至少已掌握了C语言),只需要知道怎么让DS生成正确的代码即可。

        代码和它的注释见多了,并且尝试修改代码,需要什么操作就查什么,慢慢就会熟悉的

我想要随机生成0到100的数据,小于24的概率为0.56,并且标记为True,不小于24的标记为False,用作AI的训练数据集,最后保存为csv文件,那么怎么使用python实现?

        DS生成的代码大多数情况下可用,如果出错,那么就把报错信息或者调试信息给它,提供给它需求,让它不断修改代码直至生成可用的代码,这里不展开细节了。下面就是可用的python代码,如果你是新安装的pycharm,那么可能会提示安装一些库,光标放在报错的地方就会提示安装对应的包,点击即可

        如果安装软件包的过程可能会有些漫长,换源即可。可用的python代码如下,其中小于24的概率不建议调得太高或者太低,不然就成了“非平衡二分类问题”。举个例子,如果小于24的概率调到了0.05,那么意味不管输入什么数据全部蒙“不小于24”的概率都可以达到0.95,那么AI模型就根本不想进步了。需要用一些方法,比如调整权重、欠采样等来解决

import random
import pandas as pd

# 设置参数
num_samples = 1000  # 总样本量(可自行调整)
low_prob = 0.56  # 小于24的概率
true_threshold = 24  # 分类阈值
file_name = 'Data/simulated_dataset.csv'

# 生成数据集
data = []
for _ in range(num_samples):
    # 生成0-1之间的随机数决定区间
    if random.random() < low_prob:
        # 生成0-23的整数(包含23)
        num = random.randint(0, 23)
    else:
        # 生成24-100的整数(包含24和100)
        num = random.randint(24, 100)

    # 添加标签
    label = num < true_threshold
    data.append((num, label))

# 转换为DataFrame
df = pd.DataFrame(data, columns=['Number', 'Label'])

# 保存CSV文件
df.to_csv(file_name, index=False)

# 验证分布比例
true_count = df['Label'].sum()
false_count = len(df) - true_count
print(f"True比例: {true_count / len(df):.2%} ({true_count}条)")
print(f"False比例: {false_count / len(df):.2%} ({false_count}条)")

        正确执行完后,指定目录下就会有一个simulated_data.csv文件。

        运行时要注意,运行的是不是当前文件

四、训练模型

1,训练模型代码

        听着虽然有些不明觉厉,其实这里我们只用非常简单且有限的步骤,因为Keras框架已经帮我们做好了绝大部分工作了。

        问询你可以描述得更加具体。不得不说DS太体贴了,前面提问时说过“我是初学者”,它生成代码时也没忘记,注释可谓是非常详细

        ……

        既如此,让它改进代码

……

        总之,多次让AI改进后,可以得到了一份可以训练刚才数据的代码

# -*- coding: utf-8 -*-
import matplotlib
import pandas as pd
import numpy as np
import tensorflow as tf
import matplotlib.pyplot as plt
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
from keras.src.layers import Dense


matplotlib.use('TkAgg')  # 或者 'Qt5Agg' 设置matplotlib后端为TkAgg,不然pycharm绘图会报错

# 设置随机种子确保可重复性
np.random.seed(42)
tf.random.set_seed(42)

# ---------------------------
# 1. 加载数据与预处理
# ---------------------------
# 从CSV文件加载数据
df = pd.read_csv('./Data/simulated_dataset.csv')  # 替换为你的CSV文件路径
X = df[['Number']].values  # 输入特征
y = df['Label'].values  # 标签(0或1)

# 数据标准化
scaler = StandardScaler()
X_scaled = scaler.fit_transform(X)

# 分割数据集
X_train, X_test, y_train, y_test = train_test_split(
    X_scaled, y, test_size=0.2, random_state=42
)

# ---------------------------
# 2. 构建Keras模型
# ---------------------------
model = tf.keras.Sequential([
    Dense(8, activation='relu', input_shape=(1,)),
    Dense(1, activation='sigmoid')
])

model.compile(
    optimizer='adam',
    loss='binary_crossentropy',
    metrics=['accuracy']
)

# ---------------------------
# 3. 训练模型并可视化
# ---------------------------
history = model.fit(
    X_train, y_train,
    epochs=300,
    batch_size=32,
    validation_split=0.1,
    verbose=1
)

# 绘制训练过程曲线
plt.figure(figsize=(12, 4))

# 准确率曲线
plt.subplot(1, 2, 1)
plt.plot(history.history['accuracy'], label='Training Accuracy')
plt.plot(history.history['val_accuracy'], label='Validation Accuracy')
plt.title('Model Accuracy')
plt.xlabel('Epochs')
plt.ylabel('Accuracy')
plt.legend()

# 损失曲线
plt.subplot(1, 2, 2)
plt.plot(history.history['loss'], label='Training Loss')
plt.plot(history.history['val_loss'], label='Validation Loss')
plt.title('Model Loss')
plt.xlabel('Epochs')
plt.ylabel('Loss')


plt.tight_layout()
plt.show()

# ---------------------------
# 4. 评估与保存模型
# ---------------------------
# 保存模型为TensorFlow Lite格式(修正部分)
converter = tf.lite.TFLiteConverter.from_keras_model(model)
tflite_model = converter.convert()  # 先转换模型

# 将模型写入文件(注意扩展名应为.tflite)
model.save('model/number_classifier.h5')
with open('model/number_classifier.tflite', 'wb') as f:  # 使用相对路径
    f.write(tflite_model)
print("\n模型已保存为 number_classifier.tflite")

# 测试集评估
test_loss, test_acc = model.evaluate(X_test, y_test, verbose=0)
print(f"\n测试集准确率: {test_acc:.2%}")
print(f"测试集损失: {test_loss:.4f}")

2,简单分析代码

        这个代码,有时候即便你安装了所有软件包可能还会冒红线,可以先不用管它

码,由于有注释的帮助,这些步骤非常清晰明了。下面代码在个别地方可能与上面不同

①加载数据

②创建模型

        这里面可以看到两个函数,也就是两层神经网络,你想要增加,就再添加一个函数即可。事实上它表示了三层神经网络,输入层在input_shape那里,只不过由于只需输入一个数,因此输入层只有一个节点。

        隐藏层只有一个Dense(全通层),里面有8个节点,激活函数是relu(这个现在应该可以自行查询哦)。

        输出层只有一个节点,虽然是二分类问题,但事实上,一个节点激活和不激活就已经表示两种状态了。因此,只需要一个节点即可,而sigmoid这个激活函数常见用于二分类问题的输出层。

        常用的隐藏层比如CNN和Dense,层越多,理论上训练效果更好,但容易过拟合并且占用算力资源也会增加。

③编译模型

④训练模型

        这里只要注意这两个参数即可,一个是epochs,另一个是batch_size。前者是训练轮数,后者是每批次训练的数据量

⑤评估模型

        这里其实就是图形化显示训练的结果,什么准确率、损失什么的。准确率分为训练准确率和验证准确率,因为在训练过程中并不是拿出所有数据来训练,不然那就相当于拿着答案做题,正确率自然是百分百。因此,需要拿一部分题来训练,另一部分题来检验,一般这个比例为0.7:0.3

        这就自然而言牵扯出一个概念,那就是过拟合——训练准确率很高,但是验证准确率很低,这就相当于做自己熟悉的题千百次,准确率相当高,但是遇到陌生题型时却难以变通,准确率很低。

⑥保存模型

3,开始训练

①训练效果

既然这样,我们看看运行之后是什么样子的

如果这是你的第一个AI模型,成就感满满是不是

从图中可以看到随着训练轮次增加,准确率也逐渐增加,最后逐渐收敛

        我们单看某一次的,可以发现正确率已经达到0.9940了,因为问题比较简单嘛

②改进模型

        从训练结果来看,训练批次长,需要达到一百余次才渐渐稳定到令人满意地收敛。此外,准确率收敛在0.994(99.4%),仍有0.6%的优化空间。我们可以有以下优化方法:

调整模型结构

        添加更多的节点、更多的层,更换激活函数以增强非线性能力

model = tf.keras.Sequential([
    Dense(16, activation='swish', input_shape=(1,)),  # 增强非线性能力
    Dense(8, activation='swish'),
    Dense(1, activation='sigmoid')
])

        可以看出,训练到120次左右,无论是训练还是验证准确率都达到了1.00,效果十分的夸张

优化训练策略

        比如调整权重、更换损失函数、早停法、显示设置学习率等等。设置学习率与调整模型结构一样,不能太高或者太低。太高容易错过最优解,步子跨太大了。太低呢容易陷入局部最优解,也就是沉浸于短期利益中,跳不出来,看不到远方。

        一般可以10倍10倍的升或者降

model.compile(
    optimizer=tf.keras.optimizers.Adam(learning_rate=0.001),  # 显式设置学习率
    loss='binary_crossentropy',
    metrics=['accuracy']
)

调低的情况下(0.00001),收敛轮次变长

比较合适的情况下(0.01)

调高的情况下(0.1),有可能不收敛(前面乱糟糟的,如果没挺过去,后面也会乱糟糟的)

增强数据质量

        在阈值24附近生成更多样本,相当于增大生存压力。或者清洗数据,剔除一些错误或者噪声数据。

……
优化的代码
# -*- coding: utf-8 -*-
import matplotlib
import pandas as pd
import numpy as np
import tensorflow as tf
import matplotlib.pyplot as plt
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
from keras.src.layers import Dense

matplotlib.use('TkAgg')  # 或者 'Qt5Agg' 设置matplotlib后端为TkAgg,不然pycharm绘图会报错
# 设置 matplotlib 使用的字体
plt.rcParams['font.sans-serif'] = ['SimHei']  # 用来正常显示中文标签
plt.rcParams['axes.unicode_minus'] = False  # 用来正常显示负号

# 设置随机种子确保可重复性
np.random.seed(42)
tf.random.set_seed(42)

# ---------------------------
# 1. 加载数据与预处理
# ---------------------------
# 从CSV文件加载数据
df = pd.read_csv('./Data/simulated_dataset.csv')  # 替换为你的CSV文件路径
X = df[['Number']].values  # 输入特征
y = df['Label'].values  # 标签(0或1)

# 数据标准化
scaler = StandardScaler()
X_scaled = scaler.fit_transform(X)

# 分割数据集
X_train, X_test, y_train, y_test = train_test_split(
    X_scaled, y, test_size=0.2, random_state=42
)

# ---------------------------
# 2. 构建Keras模型
# ---------------------------
model = tf.keras.Sequential([
    Dense(16, activation='swish', input_shape=(1,)),  # 增强非线性能力
    Dense(8, activation='swish'),
    Dense(1, activation='sigmoid')
])

model.compile(
    optimizer=tf.keras.optimizers.Adam(learning_rate=0.01),  # 显式设置学习率
    loss='binary_crossentropy',
    metrics=['accuracy']
)

# ---------------------------
# 3. 训练模型并可视化
# ---------------------------
history = model.fit(
    X_train, y_train,
    epochs=150,
    batch_size=32,
    validation_split=0.1,
    verbose=1
)

# 绘制训练过程曲线
plt.figure(figsize=(12, 4))

# 准确率曲线
plt.subplot(1, 2, 1)
plt.plot(history.history['accuracy'], label='训练准确率')
plt.plot(history.history['val_accuracy'], label='验证准确率')
plt.title('模型准确率')
plt.xlabel('训练轮次')
plt.ylabel('准确率')
plt.legend()

# 损失曲线
plt.subplot(1, 2, 2)
plt.plot(history.history['loss'], label='训练损失')
plt.plot(history.history['val_loss'], label='验证损失')
plt.title('模型损失')
plt.xlabel('训练轮次')
plt.ylabel('损失')


plt.tight_layout()
plt.show()

# ---------------------------
# 4. 评估与保存模型
# ---------------------------
# 保存模型为TensorFlow Lite格式(修正部分)
converter = tf.lite.TFLiteConverter.from_keras_model(model)
tflite_model = converter.convert()  # 先转换模型

# 将模型写入文件(注意扩展名应为.tflite)
model.save('model/number_classifier.keras')
with open('model/number_classifier.tflite', 'wb') as f:  # 使用相对路径
    f.write(tflite_model)
print("\n模型已保存为 number_classifier.tflite")

# 测试集评估
test_loss, test_acc = model.evaluate(X_test, y_test, verbose=0)
print(f"\n测试集准确率: {test_acc:.2%}")
print(f"测试集损失: {test_loss:.4f}")

③注意事项

         此外,TensorFlow的新版只能用CPU进行训练,想要使用GPU训练,那么就需要在Conda环境里下载旧版的TensorFlow。只不过以当前训练而言,使用CPU足矣。

        运行过程中图片会阻塞程序

五、部署推理

1,生成模拟数据

        既然模型已经训练完毕,接下来我们就可以在本地部署,然后进行推理看看效果怎么样。(加载训练好的模型,然后输入数据,看看AI的输出是什么样的)

        不过在此之前我们还要生成100个模拟数据,当做实际中的数据,用于验证模型的推理效果。生成这100个数据,我可以通过原先的模拟数据生成代码微调一些参数,来生成实际模拟数据。

2,推理预测

然后就是推理(预测)了

# -*- coding: utf-8 -*-
import matplotlib
import numpy as np
import matplotlib.pyplot as plt
import pandas as pd
from sklearn.metrics import accuracy_score, confusion_matrix, roc_curve, auc
from sklearn.preprocessing import StandardScaler
import tensorflow as tf
matplotlib.use('TkAgg')  # 或者 'Qt5Agg' 设置matplotlib后端为TkAgg,不然pycharm绘图会报错

# 设置 matplotlib 使用的字体
plt.rcParams['font.sans-serif'] = ['SimHei']  # 用来正常显示中文标签
plt.rcParams['axes.unicode_minus'] = False  # 用来正常显示负号

# ---------------------------
# 1. 加载数据与预处理
# ---------------------------
# 加载测试数据集
df = pd.read_csv('./Data/simulated_dataset_valid.csv')
X = df[['Number']].values
y_true = df['Label'].values

# 必须使用训练时的scaler对象!假设已保存训练时的scaler
# 如果未保存,需要重新拟合(但会引入数据泄漏,不推荐)
scaler = StandardScaler()
X_scaled = scaler.fit_transform(X)  # 实际部署时应加载训练时的scaler参数

# ---------------------------
# 2. 加载TFLite模型
# ---------------------------
interpreter = tf.lite.Interpreter(model_path='model/number_classifier.tflite')
interpreter.allocate_tensors()

# 获取输入输出张量信息
input_details = interpreter.get_input_details()
output_details = interpreter.get_output_details()

# ---------------------------
# 3. 批量推理并评估
# ---------------------------
def batch_predict(X):
    y_pred = []
    for sample in X:
        # 预处理输入
        sample = sample.reshape(1, -1).astype(np.float32)
        # 设置输入
        interpreter.set_tensor(input_details[0]['index'], sample)
        # 运行推理
        interpreter.invoke()
        # 获取输出
        pred = interpreter.get_tensor(output_details[0]['index'])[0][0]
        y_pred.append(pred)
    return np.array(y_pred)

y_prob = batch_predict(X_scaled)
y_pred = (y_prob > 0.5).astype(int)  # 将概率转换为0/1预测

# ---------------------------
# 4. 可视化评估结果
# ---------------------------
plt.figure(figsize=(15, 5))

# 子图1:真实值与预测值对比散点图
plt.subplot(1, 3, 1)
plt.scatter(X, y_true, c='blue', label='真实值', alpha=0.6)
plt.scatter(X, y_pred, c='red', marker='x', label='预测值', alpha=0.6)
plt.axvline(x=24, color='green', linestyle='--', label='理论阈值24')
plt.xlabel('输入数值')
plt.ylabel('类别')
plt.title('真实值与预测值对比')
plt.legend()

# 子图2:混淆矩阵
plt.subplot(1, 3, 2)
cm = confusion_matrix(y_true, y_pred)
plt.imshow(cm, interpolation='nearest', cmap=plt.cm.Blues)
plt.title('混淆矩阵')
plt.colorbar()
tick_marks = [0, 1]
plt.xticks(tick_marks, ['False', 'True'])
plt.yticks(tick_marks, ['False', 'True'])
plt.xlabel('预测标签')
plt.ylabel('真实标签')
for i in range(2):
    for j in range(2):
        plt.text(j, i, cm[i][j], ha='center', va='center', color='white' if cm[i][j] > cm.max()/2 else 'black')

# 子图3:ROC曲线
plt.subplot(1, 3, 3)
fpr, tpr, _ = roc_curve(y_true, y_prob)
roc_auc = auc(fpr, tpr)
plt.plot(fpr, tpr, color='darkorange', lw=2, label=f'ROC曲线 (AUC = {roc_auc:.2f})')
plt.plot([0, 1], [0, 1], color='navy', lw=2, linestyle='--')
plt.xlim([0.0, 1.0])
plt.ylim([0.0, 1.05])
plt.xlabel('假正率')
plt.ylabel('真正率')
plt.title('ROC曲线')
plt.legend(loc='lower right')

plt.tight_layout()
plt.show()

# ---------------------------
# 5. 打印评估指标
# ---------------------------
test_accuracy = accuracy_score(y_true, y_pred)
print(f"\n测试集准确率: {test_accuracy:.2%}")
print(f"混淆矩阵:\n{cm}")

# ---------------------------
# 6. 单样本推理演示
# ---------------------------
def predict_single_number(num):
    # 数据预处理
    scaled_num = scaler.transform([[num]])
    # 推理
    interpreter.set_tensor(input_details[0]['index'], scaled_num.astype(np.float32))
    interpreter.invoke()
    prob = interpreter.get_tensor(output_details[0]['index'])[0][0]
    prediction = prob > 0.5
    actual_label = num < 24
    # 可视化结果
    plt.figure(figsize=(6, 4))
    plt.bar(['预测概率'], [prob], color='skyblue')
    plt.axhline(y=0.5, color='red', linestyle='--', label='决策阈值')
    plt.ylim(0, 1)
    plt.title(f'输入值: {num}\n预测结果: {prediction} (正确性: {prediction == actual_label})')
    plt.legend()
    plt.show()
    return prob

# 示例:手动输入数值测试
while True:
    try:
        user_input = input("\n输入测试数值(输入 'q' 退出): ")
        if user_input.lower() == 'q':
            break
        num = float(user_input)
        predict_single_number(num)
    except:
        print("请输入有效数字!")

  我们可以看到推理后的结果是非常哇塞的,这根本就难不倒它嘛

关闭图像后,下面的终端还开着,可以输入数字来测试一下,按输入q或者直接按停止运行键即可退出

六、加载模型

        既然前面已经把模型训练了出来,接下来就可以把模型部署到单片机上了。不过部署模型时可能会遇到下面问题,即长路径错误,建议配置一下。

  • 在注册表编辑器中,找到路径 HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\FileSystem
  • 修改或添加 LongPathsEnabled

    • 在 FileSystem 文件夹中查找名为 LongPathsEnabled 的DWORD (32-bit) 值。
    • 如果存在,双击它并将其值设置为 1
    • 如果不存在,右键点击 FileSystem 文件夹,选择 新建 -> DWORD (32-bit) 值,命名为 LongPathsEnabled,然后将其值设置为 1
  • 然后重启

1,生成C代码 

①添加模型

       在STM32Cube的X-CUBE-AI插件里,选择TFLite格式,并选择前面我们保存的模型

②分析模型

        选择后,点击分析即可

分析成功后应如下,能显示分析后的Flash和Ram

        我们可以看到最后一句,输入和输出都是4字节

根据手册,生成的C模型或者说C代码,其实只有float32_t类型,并且仅支持简单的张量输入和输出

③张量

        张量是里面的术语,这里强调的是数据的布局,毕竟无论是图像处理还是别的什么,数据输入与输出总得有东西存储起来。在C语言里,那么就是多维数组。

        对于我们此次实验,用到的自然是一维张量,由于只用一个批次,那么自然是一维数组

④生成的文件

        回到正题,既然模型经X-CUBE-AI分析成功了,那么就生成代码,此处使用的是STM32CubeIDE

生成的代码工程中,我们需要重点关注下面这两个目录

        Middlewares目录下有我们需要的AI静态库,X-CUBE-AI目录里是应用级模板。前者相当于是封装好的HAL库,后者相当于是我们自己要编写的BSP或者应用。前者自然是不能改,也改不了,后者我们可以自由更改。

2,移植库

        如果你移植很熟练的话,这一步也没什么难度。这里要做的事只有一件,就是把生成的代码文件移动到我们的工程里。不过要说明的是,我的工程是cmake工程,移动文件后,只需要修改CMakelists就行了。如果是Keil或者IAR的话,需要手动图形化添加这些文件。

①内核运行时库 

       内核运行时库指的就是下面这个后缀名为.a的静态库

        移植时,需要把这个AI目录移动到自己工程里,这里建议保持跟Cube工程类似的组织结构,方便后续添加其他扩展

        如果是CMake工程,那么在CMakelists里需要添加下面语句来链接静态库(疑惑的话可以多问问DS)

target_link_libraries(libai INTERFACE :NetworkRuntime910_CM4_GCC.a)
target_link_libraries(libai PRIVATE libdrivers)

        如果是IAR或者Keil,库的头文件目录ST/AI/Inc不要忘记包含了(或者说头文件),头文件比较多,可以按Ctrl + A全部选中

②应用级模板

        X-CUBE-AI这个移植就随意一点了,把里面文件放在你认为合适的目录里即可,比如你可以放在与Middleware同级的新建目录AI

        与前面一致,源文件和头文件不要忘了添加

③CRC

        把生成的CRC源文件和头文件都复制到自己的工程里

3,使用

        最初选择软件包时,由于勾选了应用模板,生成的自然是前面看到的应用级模板,也就是给你一个使用的模板供参考

①初印象

        这些文件中,我们只要考虑这两个即可。从下面的图中,可以看到app_x-cube-ai.c中有个头文件包含错误,也就是找不到main.h。这个根据自己需要改即可,由于本工程没有main.h,那么这个头文件包含去掉即可

        那么在这么多函数里我们要找哪个呢?使用STM32CubeMX的经验告诉我们,应该重点关注带有MX_前缀的函数。从这两个函数的名称可以看出,一个是初始化的示范,另一个是使用AI推理的示范

②简单分析

MX_X_CUBE_AI_Init

        我们先观察初始化函数,可以看到这个函数的初始化过程其实就两步,使用printf打印提示信息和调用ai_boostrap这个函数

        那么进入这个函数,会发现它是一个静态函数,也就是说这个函数只在这个模板文件里存在,那么自然就可以随便修改。

        其内部充斥着大量条件预编译指令,但细看就可以拆解为下面几个部分:

ai网络初始化、获取ai网络的输入和输出缓冲区指针、拷贝那两个缓冲区

        怎么拷贝呢?以输入缓冲区为例,从形式上来看,好像是把ai_input赋值给外部的缓冲区data_ins,嗯?这是不是反了呢?

        实则不然,因为它们都是指针,这句代码实际上是把内部缓冲区指针给这个外部缓冲区指针。没错,data_ins是个指针数组,用来保存内部缓冲区的指针,我们可以通过data_ins指针数组里的指针来间接访问内部缓冲区,进而达到向内部缓冲区读取或写入数据的目的

我们接着进入ai_network_inputs_get函数,先是检查ai网络句柄(其实就是个指针),然后再调用一个函数来返回输入缓冲区的指针

        继续进入ai_platform_inputs_get函数里,发现进不去,而是进入了这个函数声明里。显然,这是之前移植的那个AI内核运行时库的API(应用程序编程接口),也就是ST提供给你的函数,与HAL没什么区别,直接调用即可。

        从这可以看出,这个AI应用级模板的套路其实就是调用那个运行时库的API,做了一些检查机制并提供一层层的封装。需要注意的是,不同版本的X-CUBE-AI拓展包提供的API不一定相同

MX_X_CUBE_AI_Process

        接着来看这个处理函数,这个处理过程非常地自然。上面这一块显然是进行AI推理的部分,下面这一块是判断AI推理结果的部分

        先看AI推理的部分,这里使用一个do while循环,显然是处理不同批次的数据。对于每一批次,我们可以从注释看到三步走的过程——先调用acquire_and_process_data

获取并处理输入数据,然后调用ai_run来推理,最后再调用post_process函数处理输出结果

        那么我们看看这三个函数在干嘛,先看acquire_and_process_data。你会发现它其实是空函数,不过注释里已经有了很明显的注释,它的意思是把数据放到data这个数组(缓冲区)里,比如传感器数据、从ADC里获取的数据等,只不过是以指针的形式

那么ai_run在干嘛呢,从图下可以看出,它根据ai网络句柄、输入输出缓冲区来进行推理

 

        那么post_process呢,从名称上来看,它的作用是进行后处理,也就是下面注释提到的“处理推理结果”

③添加代码

        对代码进行简单分析后,那么我们想使用AI推理的步骤就很清晰了。首先是CRC初始化和AI初始化,既然MX_CRC_InitMX_X_CUBE_AI_Init做得很完善了,那么直接调用它即可。

        接着是对输入数据进行推理,由于MX_X_CUBE_AI_Process完全是个空模板,那么就要求我们对其进行一些修改。

        出于测试(省事)的目的,我们不需要做那么多检查,也不用那么多层封装。因此只需要输入数据、处理数据、输出数据这三个简单步骤。为此,我们可以编写一个这样的函数供调用。

        

        先搭建一个空函数,在它旁边即可

头文件里声明它

搭建一个基本的三步走

        前面提到,数据输入只需要对外部缓冲区指针data_ins操作即可,同时手册里也提到,生成的C模型目前只支持float32_t类型,也就是说给我们看到的这个ai_i8类型(singed char)实际上与我们要输入的数据类型无关

        那也就是说,我们需要让输入数据以float的形式进行存储,那么需要进行强制转换。索引是0也很好理解,因为我们只有一个批次一个输入数据

        数据输出自然是返回data_outs里的数据,并且也要以float32_t的形式读取出来

那么最终的代码就是

// 推理数据的代码
float ai_process_data(const float data)
{
  // 数据输入
  *(float *)data_ins[0] = data;

  // 进行ai推理
  ai_run();

  // 数据输出
  return *(float *)data_outs[0];
}
标准化   

     but!!有一个非常容易忽略的问题,如果训练模型时对输入数据进行了标准化(StandardScaler),那么部署在stm32上的输入数据也要标准化,不然输出数据可能完全不对。以本次为例,输入大于0的数,输出数据就会全部变成0.00

// 推理数据的代码
float ai_process_data(const float data)
{
  // 数据输入
  // *(float *)data_ins[0] = data;
  *(float *)data_ins[0] = (data - 24.5f) / 5.0f;// 复现标准化


  // 进行ai推理
  ai_run();

  // 数据输出
  return *(float *)data_outs[0];
}

④测试

        为了测试一下部署的AI模型能否工作,这里写了一个简单的测试代码。

        app_init和background_handler都是封装过的函数,前者是初始化各种驱动的,后者是后台任务,也就是死循环

        打印串口使用的是ITM,仅通过ST-Link仿真器即可打印信息到CLion上。当然,你也可以使用其他手段,比如直接从调试栏观察变量、使用USB转TTL等

参考博客:STM32使用ITM调试_通过仿真器实现串口打印_stm32 itm-CSDN博客

先在GDB窗口加载gdb脚本(脚本在上面博客里)

然后在终端输入TCP命令,连接端口(其实可以直接编写ps1脚本一步到位)

运行后可以看到,能进行正常的ai推理

由于训练时用的是整型数据,并且没有在24附近进行强化训练,所以模型在24附近没那么准

但其他地方很准,非常符合预期

七、跨越

        AI模型转为C代码可以不使用CubeMX的这个插件,但那样占用可能会很高,性能无法有效利用。对于STM32平台,无论是操作的便捷性还是针对STM32的性能优化,使用官方的无疑是比较合适的。如果是其他单片机,那么直接使用由TensorFlow Lite训练的模型转换的C代码,占用也不会很夸张,因为它专门用于嵌入式平台。

        至于神经网络、CNN、LVTM、图像识别什么的,从0.9到,就需要靠自己主动学习了。

八、日志

2025.3.21
        感谢大家的支持,经一些同志的反馈,文章部分内容有些含糊,且版本有些落后(这是无可避免的)。为此,更新了一下部分内容,并添加了一些信息提示。

        当初写下本篇时,最好用的免费大语言模型还是通义2.5,现在有了DeepSeek,那么辅助学习就更加简单准确了。于是,把当初通义回答的图片替换为了DeepSeek,后文中DeepSeek就简称为DS。

        今晚先更新训练代码的部分,明晚再更新部署过程

评论 7
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值