4. 模型训练基础:使用Keras训练一个简单的分类或回归模型,并导出为SavedModel格式
在TFLite Micro的部署链路中,模型训练是决定最终推理效果的上游环节。虽然TFLite Micro面向的是资源受限的MCU,但其模型来源与常规TensorFlow流程一致:先使用Keras构建并训练模型,再通过TensorFlow生态工具链完成格式转换与量化。本章聚焦于这一起点——如何用Keras训练一个轻量级分类或回归模型,并将其导出为SavedModel格式。这一步骤看似基础,却直接决定了后续模型能否被TFLite Converter正确解析,以及量化后精度损失是否可控。
4.1 核心原理:为什么训练阶段就要考虑MCU部署
许多开发者习惯先训练一个“大而准”的模型,再考虑压缩和部署。但在TFLite Micro场景下,这种思路往往导致返工。MCU的Flash通常只有几十KB到几MB,RAM更是以KB为单位,主频多在几十MHz到两百MHz之间。这意味着模型参数量必须控制在数万到数十万级别,且推理时的中间特征图不能过大。
Keras作为TensorFlow的高层API,提供了从数据管道到训练循环的完整封装。其核心价值在于:声明式构建(用Layer对象堆叠网络结构)、自动微分(训练时自动计算梯度)、回调机制(在训练过程中监控指标、调整学习率)。对于MCU部署,Keras模型需要满足三个约束:参数量小、算子类型兼容TFLite Micro、数值范围适合后续定点量化。
SavedModel是TensorFlow推荐的模型导出格式。它是一个包含模型结构、权重和训练状态的目录,而非单一文件。TFLite Converter可以直接读取SavedModel目录,将其转换为FlatBuffer格式的.tflite文件。相比HDF5(.h5)格式,SavedModel保留了完整的签名(SignatureDef),使得转换工具能更准确地推断输入输出张量的名称和形状。
4.2 操作流程:从数据到SavedModel的完整步骤
下面以一个二分类任务(例如识别传感器数据中的异常模式)和一个回归任务(例如根据温度预测电阻值)为例,展示标准流程。整个流程分为五个步骤:数据准备、模型构建、编译配置、训练执行、导出验证。
步骤一:数据准备与预处理
MCU场景的数据通常来自传感器采样,特征维度不宜过高。以128个采样点为一帧,每帧对应一个标签。数据需要划分为训练集、验证集和测试集,比例建议为70%:15%:15%。归一化是必须的——对于8位量化,输入数据最好映射到[-1, 1]或[0, 1]区间,这能减少量化误差。
import numpy as np
from sklearn.model_selection import train_test_split
# 假设X为特征矩阵 (N, 128),y为标签 (N,)
X_train, X_temp, y_train, y_temp = train_test_split(
X, y, test_size=0.3, random_state=42, stratify=y
)
X_val, X_test, y_val, y_test = train_test_split(
X_temp, y_temp, test_size=0.5, random_state=42, stratify=y_temp
)
# 归一化:计算训练集均值和标准差
mean = X_train.mean(axis=0, keepdims=True)
std = X_train.std(axis=0, keepdims=True) + 1e-7
X_train = (X_train - mean) / std
X_val = (X_val - mean) / std
X_test = (X_test - mean) / std
步骤二:构建轻量级Keras模型
对于MCU部署,推荐使用Sequential模型,因为它结构简单、转换友好。以下是一个适用于二分类的1D卷积模型,参数量约3K,适合Cortex-M4级别的MCU。
import tensorflow as tf
from tensorflow.keras import layers
def build_classifier(input_shape=(128, 1)):
model = tf.keras.Sequential([
layers.Input(shape=input_shape),
layers.Conv1D(filters=8, kernel_size=3, strides=2, activation='relu'),
layers.MaxPooling1D(pool_size=2),
layers.Conv1D(filters=16, kernel_size=3, strides=2, activation='relu'),
layers.GlobalAveragePooling1D(),
layers.Dense(8, activation='relu'),
layers.Dense(1, activation='sigmoid') # 二分类输出
])
return model
model = build_classifier()
model.summary() # 打印参数量,确认在MCU可接受范围内
步骤三:编译配置
编译时需要指定优化器、损失函数和评估指标。对于MCU部署,Adam优化器训练收敛快,但推理时不需要优化器。损失函数的选择要匹配任务类型:二分类用binary_crossentropy,多分类用categorical_crossentropy,回归用mse或mae。
model.compile(
optimizer=tf.keras.optimizers.Adam(learning_rate=1e-3),
loss='binary_crossentropy',
metrics=['accuracy', tf.keras.metrics.AUC(name='auc')]
)
步骤四:训练执行与回调
训练时使用EarlyStopping防止过拟合,使用ReduceLROnPlateau在验证损失停滞时降低学习率。对于MCU场景,训练轮次不宜过多,一般20-50轮即可。
callbacks = [
tf.keras.callbacks.EarlyStopping(patience=5, restore_best_weights=True),
tf.keras.callbacks.ReduceLROnPlateau(factor=0.5, patience=3)
]
history = model.fit(
X_train, y_train,
validation_data=(X_val, y_val),
epochs=50,
batch_size=32,
callbacks=callbacks,
verbose=1
)
步骤五:导出为SavedModel
训练完成后,使用model.save()导出。注意指定save_format='tf'以生成SavedModel目录。导出前最好先评估测试集指标,确保模型质量达标。
# 测试集评估
test_loss, test_acc, test_auc = model.evaluate(X_test, y_test, verbose=0)
print(f"Test accuracy: {test_acc:.4f}, AUC: {test_auc:.4f}")
# 导出为SavedModel
model.save("sensor_classifier_saved_model", save_format="tf")
print("SavedModel exported successfully.")
4.3 案例拆解:温度-电阻回归模型的完整训练闭环
下面通过一个具体的回归案例,展示从数据生成到SavedModel导出的完整闭环。该案例模拟一个NTC热敏电阻的温度-电阻特性曲线拟合,后续将部署到MCU上进行实时温度计算。
背景与约束:某工业传感器节点需要根据ADC采集的电压值反推环境温度。NTC电阻的阻值与温度呈非线性关系,传统查表法占用Flash且精度有限。目标是用一个Keras回归模型拟合R-T曲线,模型参数量小于2KB,部署在STM32G0系列MCU上。
数据生成:使用Steinhart-Hart方程生成模拟数据,温度范围-20°C至80°C,加入1%的高斯噪声模拟测量误差。
import numpy as np
# Steinhart-Hart 方程参数
A, B, C = 1.129e-3, 2.341e-4, 8.767e-8
T = np.linspace(-20, 80, 1000) # 温度范围
T_k = T + 273.15 # 转换为开尔文
lnR = A + B/T_k + C/(T_k**3)
R = np.exp(lnR) * 10000 # 10k NTC
R_noisy = R * (1 + np.random.normal(0, 0.01, R.shape))
# 特征:电阻值(归一化),标签:温度
X = R_noisy.reshape(-1, 1)
y = T.reshape(-1, 1)
模型设计:由于输入输出都是单变量,使用3层全连接网络即可。隐藏层使用tanh激活函数(输出范围[-1,1],适合回归中间层),输出层为线性激活。
model = tf.keras.Sequential([
layers.Input(shape=(1,)),
layers.Dense(16, activation='tanh'),
layers.Dense(8, activation='tanh'),
layers.Dense(1) # 线性输出
])
model.compile(optimizer='adam', loss='mse', metrics=['mae'])
训练与验证:训练100轮后,测试集MAE达到0.35°C,满足±0.5°C的精度要求。导出SavedModel后,使用saved_model_cli工具验证签名。
# 命令行验证
# saved_model_cli show --dir ./ntc_regressor_saved_model --all
结果对比与复盘:该模型参数量为16×1+16+8×16+8+8×1+1 = 169个参数,加上偏置共约200个参数,占用Flash约800字节(float32)。后续量化到int8后,Flash占用降至约200字节。相比查表法(需要存储1000个float32点,约4KB),模型方法节省了95%的存储空间,且精度更高。
4.4 常见误区与总结复盘
在训练用于MCU部署的模型时,以下误区最为常见:
| 误区 | 后果 | 正确做法 |
|---|---|---|
| 使用过大的模型(如ResNet50) | Flash/RAM超限,无法部署 | 从最小模型开始,逐步增加容量 |
| 忽略输入归一化 | 量化后精度大幅下降 | 训练前固定归一化参数,推理时复用 |
| 使用ReLU作为回归输出激活 | 无法输出负值,预测偏差大 | 回归输出层用线性激活 |
| 训练后不评估直接导出 | 部署后才发现精度不达标 | 导出前在测试集上做最终验证 |
| 使用不支持的算子(如LSTM) | TFLite Converter转换失败 | 查阅TFLite Micro支持的算子列表 |
总结复盘:本章的核心要点可以归纳为“三个提前”——提前规划模型规模、提前设计输入输出范围、提前验证算子兼容性。SavedModel导出是连接训练与部署的桥梁,务必在导出后检查目录结构是否完整(包含saved_model.pb和variables/文件夹)。在下一阶段,我们将基于这个SavedModel进行TFLite转换和量化,届时模型的结构和数值范围将直接影响量化效果。
model.save('model.h5')保存HDF5格式。虽然Keras仍支持加载,但TFLite Converter对HDF5的支持已逐步弃用,且HDF5不包含完整的服务签名,可能导致转换时输入输出张量名称不明确。