3. TensorFlow Lite基础:模型量化原理、TFLite模型格式、模型转换工具、算子支持列表
好,咱们进入第三章。这一章我打算跟你聊聊TensorFlow Lite,简称TFLite。说实话,在嵌入式AI这个圈子里混久了,你会发现TFLite几乎是个绕不开的坎儿。为什么?因为STM32CubeAI也好,其他MCU推理框架也好,底层吃的模型格式,绝大多数都是TFLite。
我个人习惯把TFLite看作是TensorFlow的「瘦身版」。它专门为移动端和嵌入式设备设计。你想想看,一个在服务器上跑得欢的ResNet-50,模型文件动辄上百兆,直接塞进STM32?那是不可能的。所以,我们需要一套方法,把模型变小、变快、变省电。这套方法的核心,就是今天要讲的——模型量化。
3.1 模型量化原理:从浮点到整数的「降维打击」
量化,说白了就是把模型里的权重和激活值,从32位浮点数(float32)变成8位整数(int8)甚至更低位宽。为什么要这么做?两个原因:省空间、提速度。
我举个例子。一个float32的数值,占4个字节。换成int8,只占1个字节。模型大小直接缩到四分之一。我在项目里遇到过,一个原本需要2MB Flash的模型,量化后只要512KB,刚好塞进一颗低成本的芯片里。那种感觉,嗯,挺爽的。
但量化不是简单的截断。它背后有一套映射逻辑。我们通常用「缩放因子」和「零点」来建立浮点数和整数之间的映射关系。公式长这样:
int8_value = round(float32_value / scale) + zero_point
反过来,推理时再把整数还原成浮点数:
float32_value = (int8_value - zero_point) * scale
这里的scale和zero_point,就是量化参数。它们是在量化过程中计算出来的。你可能会问:「精度会不会损失?」会,但通常可控。尤其是在8位量化下,对于大多数嵌入式应用场景,精度损失在1%-2%以内,肉眼几乎看不出来。
3.2 TFLite模型格式:.tflite文件里到底装了啥?
TFLite模型的后缀是.tflite。它本质上是一个FlatBuffer格式的二进制文件。FlatBuffer是Google搞的一种高效序列化库,特点是解析速度快、不需要额外的内存分配。这一点对嵌入式设备来说,非常友好。
一个.tflite文件里,主要包含这几部分:
- 模型结构:定义了网络的层、连接方式、输入输出张量。
- 权重数据:量化后的权重值,通常是int8或uint8。
- 量化参数:每个张量对应的scale和zero_point。
- 算子列表:模型用到的所有操作,比如Conv2D、DepthwiseConv2D、FullyConnected等。
我记得第一次手动解析.tflite文件时,用了一个叫Netron的可视化工具。打开一看,整个网络结构一目了然。你可以看到每一层的输入输出形状、量化参数、甚至算子的具体配置。建议你也试试。
3.3 模型转换工具:从SavedModel到.tflite
把训练好的TensorFlow模型转成TFLite格式,靠的是TFLite Converter。这个工具是TensorFlow官方提供的,支持从SavedModel、Keras H5、以及具体函数(concrete function)进行转换。
我常用的转换流程是这样的:
- 训练好模型,保存为SavedModel格式。
- 用TFLite Converter加载模型。
- 设置量化选项(比如默认的float32量化,或者指定int8量化)。
- 运行转换,生成.tflite文件。
代码示例:
import tensorflow as tf
# 加载训练好的模型
model = tf.saved_model.load('path/to/saved_model')
# 创建转换器
converter = tf.lite.TFLiteConverter.from_saved_model('path/to/saved_model')
# 设置量化优化
converter.optimizations = [tf.lite.Optimize.DEFAULT]
# 如果需要int8量化,还需要提供代表性数据集
def representative_dataset():
for _ in range(100):
data = np.random.randn(1, 224, 224, 3).astype(np.float32)
yield [data]
converter.representative_dataset = representative_dataset
converter.target_spec.supported_ops = [tf.lite.OpsSet.TFLITE_BUILTINS_INT8]
# 转换并保存
tflite_model = converter.convert()
with open('model_quantized.tflite', 'wb') as f:
f.write(tflite_model)
3.4 算子支持列表:哪些操作能在MCU上跑?
不是所有TensorFlow算子都能被TFLite支持,更不是所有TFLite算子都能在STM32上跑。这里有个「支持链」的概念。
首先,TFLite本身有一个内置算子列表。常用的比如:
| 算子名称 | 说明 | MCU支持情况 |
|---|---|---|
| CONV_2D | 标准二维卷积 | 支持(int8) |
| DEPTHWISE_CONV_2D | 深度可分离卷积 | 支持(int8) |
| FULLY_CONNECTED | 全连接层 | 支持(int8) |
| AVERAGE_POOL_2D | 平均池化 | 支持 |
| MAX_POOL_2D | 最大池化 | 支持 |
| SOFTMAX | Softmax激活 | 支持 |
| RESHAPE | 张量变形 | 支持 |
| ADD | 逐元素加法 | 支持 |
但要注意,像LSTM、GRU这类循环神经网络算子,虽然TFLite支持,但在STM32上跑起来会比较吃力。我建议尽量用CNN结构,或者用TFLite Micro的算子列表做一次预检。
STM32CubeAI在导入模型时,会做一次算子兼容性检查。如果遇到不支持的算子,它会报错并告诉你哪个算子出了问题。这时候你有两个选择:要么修改模型结构,用支持的算子替代;要么自己实现这个算子的C代码,注册到推理引擎里。
嗯,这一章的内容差不多就这些。模型量化、TFLite格式、转换工具、算子支持,这四个点串起来,就是你从训练好的模型到嵌入式部署的完整路径。下一章,我们会把这些知识落地到STM32上,真正跑一个TFLite模型看看效果。