2、基础知识:TensorFlow计算图、会话与图模式、训练与推理的区别、模型文件格式(pb、pbtxt、ckpt、SavedModel)

在进入TFLite Micro的模型简化流程之前,必须先厘清TensorFlow框架中几个最核心、也最容易混淆的基础概念。TFLite Micro的转换工具链(如TFLite Converter、FlatBuffer解析器)本质上是在“图”层面做优化,而不是在Python层面对张量做运算。因此,理解计算图(Graph)、会话(Session)与图模式(Graph Mode/Eager Mode)的差异,理解训练与推理在算子、精度、内存管理上的本质区别,以及掌握pb、pbtxt、ckpt、SavedModel这四种文件格式的定位与适用场景,是后续进行图冻结、常量折叠和算子规整的前提。本章将围绕这四个知识点展开,帮助你在模型转换前建立正确的“图思维”。

TensorFlow 模型生命周期与文件格式关系图 训练阶段(Eager/Graph) 变量(Variable) 优化器(Optimizer) 损失函数(Loss) 反向传播算子 推理阶段(Graph) 常量(Const) 前向算子(Conv/FC/ReLU) 无反向/无优化器 内存可静态规划 图冻结 + 常量折叠 变量→常量,去除训练节点 ckpt(检查点) 变量值+元图 训练中间产物 pb / pbtxt GraphDef 序列化 冻结后常用 SavedModel 完整模型包 含签名+资产 TFLite FlatBuffer 最终部署格式 供Micro解释器加载 TFLite Converter 统一入口:接受 SavedModel / Frozen pb / Keras H5 输出 .tflite(FlatBuffer),供 TFLite Micro 编译器进一步优化

2.1 核心原理:计算图、会话与图模式

TensorFlow 1.x 的核心抽象是“计算图”(Graph)。计算图是一个有向无环图(DAG),节点(Node)代表算子(Op),边(Edge)代表张量(Tensor)的数据流动。在 TensorFlow 1.x 中,构建图与执行图是分离的:先用 Python API 构建图结构,然后通过“会话”(Session)将图部署到设备(CPU/GPU/TPU)上执行。会话负责分配资源、管理变量生命周期、执行图中的算子。

TensorFlow 2.x 默认启用了“动态图模式”(Eager Execution),即每行 Python 代码立即执行并返回数值,不再需要显式创建 Session。这种模式对调试和原型开发非常友好,但牺牲了部分静态优化能力。为了兼顾性能和部署需求,TensorFlow 2.x 提供了 tf.function 装饰器,它可以将 Python 函数编译为静态计算图(AutoGraph),从而在底层恢复图优化能力。

理解图模式的关键在于:图模式(Graph Mode)是静态的、可优化的、可序列化的;而 Eager 模式是动态的、直观的、不可直接部署的。TFLite Micro 只接受静态图(或由静态图转换而来的 FlatBuffer),因为嵌入式设备没有 Python 解释器,也没有动态图执行引擎。因此,模型转换前的第一步,就是确保模型处于“图模式”或可转换为图模式的状态。

核心要点:在 TensorFlow 2.x 中,即使你使用 Keras 高层 API 训练模型,底层仍然会通过 tf.function 生成计算图。Keras 模型的 model.save() 或 tf.saved_model.save() 都会将图结构序列化到磁盘。因此,你不需要手动构建 Graph 和 Session,但必须理解图的存在——因为后续的冻结、折叠、算子规整都是在图层面进行的。

2.2 判断方法:如何确认模型处于可转换状态

在动手转换之前,你需要确认模型是否满足以下三个条件:

  1. 模型已训练完成:所有变量(Variable)已收敛,不再需要更新。
  2. 模型已处于静态图状态:可以通过 tf.function 包装或直接使用 Keras 模型。
  3. 模型输入输出签名明确:TFLite Converter 需要知道输入张量的形状和数据类型。

一个快速验证方法是使用 tf.saved_model.save() 保存模型,然后用 saved_model_cli 工具检查签名:

# 保存模型
tf.saved_model.save(model, "exported_model")

# 命令行检查签名
saved_model_cli show --dir exported_model --all

如果输出中显示明确的 serving_default 签名,并且输入输出张量名称清晰,说明模型已具备转换条件。如果模型包含自定义层或自定义损失函数,需要确保这些自定义算子已注册到 TensorFlow 的算子库中,否则转换器无法识别。

2.3 训练与推理的区别:为什么必须冻结图

训练和推理在计算图层面有本质区别。训练图包含:变量(Variable)、优化器(Optimizer)、损失函数(Loss)、反向传播算子(如 GradientDescent、ApplyAdam)、批量归一化的滑动平均更新节点等。而推理图只需要:输入占位符、前向计算算子(卷积、全连接、激活函数)、以及常量权重。

“图冻结”(Graph Freezing)就是将训练图中的所有变量转换为常量(Const),并删除与训练相关的节点(如优化器、损失、反向传播)。冻结后的图不再包含任何可变状态,可以直接序列化为 pb 文件,供 TFLite Converter 使用。

对比维度 训练图 推理图(冻结后)
变量类型 Variable(可更新) Const(不可变)
算子类型 前向 + 反向 + 优化器 仅前向算子
内存管理 动态分配,需保存梯度 静态规划,无梯度
批量归一化 使用 batch 统计量 使用滑动平均的固定值
Dropout 随机失活 恒等映射(无随机性)
部署适用性 不可部署 可直接转换 TFLite

在 TFLite Micro 场景中,冻结图尤为重要。因为 Micro 运行时没有变量管理机制,所有权重必须作为常量嵌入到 FlatBuffer 中。如果直接转换未冻结的模型,转换器会报错或生成包含 Variable 节点的无效图。

常见陷阱:很多初学者在训练后直接调用 model.save('model.h5'),然后尝试用 TFLiteConverter 转换。Keras 的 H5 文件虽然包含权重和结构,但内部可能仍保留训练相关的元数据。建议使用 tf.saved_model.save() 或先调用 model.trainable = False 再导出,确保图被冻结。

2.4 模型文件格式:pb、pbtxt、ckpt、SavedModel 的定位

TensorFlow 生态中存在多种文件格式,它们服务于不同的生命周期阶段。理解这些格式的区别,能帮助你避免在转换过程中选错输入文件。

2.4.1 ckpt(Checkpoint)

ckpt 是训练过程中的检查点文件,通常包含 .data(变量值)、.index(索引)和 .meta(图结构)三个部分。它主要用于训练中断后的恢复,不适合直接用于推理或转换。ckpt 中的变量是“活”的,需要重新构建图结构才能加载。

2.4.2 pb / pbtxt(GraphDef)

pb(Protocol Buffer 二进制)和 pbtxt(文本格式)是 GraphDef 的序列化表示。GraphDef 只包含图结构(节点、边、属性),不包含变量值。冻结后的 pb 文件(即 Frozen GraphDef)将变量值以 Const 节点的形式嵌入图中,是 TFLite Converter 最常用的输入格式之一。pbtxt 是 pb 的可读版本,用于调试,但体积较大,不适合部署。

2.4.3 SavedModel

SavedModel 是 TensorFlow 2.x 推荐的完整模型格式。它是一个目录,包含 saved_model.pb(图结构+签名)、variables/(变量值)、assets/(附加资源)。SavedModel 自带签名信息,转换器可以直接读取输入输出张量名称,无需手动指定。对于 TFLite Micro 转换,推荐优先使用 SavedModel 作为输入。

格式 内容 是否含变量值 是否含签名 推荐用途
ckpt 变量值 + 元图 是 否 训练恢复
pb(冻结) 图结构 + 常量权重 是(以Const形式) 否 TFLite 转换
pbtxt 图结构(文本) 否 否 调试查看
SavedModel 图 + 变量 + 签名 + 资产 是 是 部署/转换首选

2.5 案例拆解:从 Keras 模型到可转换的冻结图

下面通过一个具体案例,演示如何将一个训练好的 Keras 模型转换为 TFLite Converter 可接受的格式。假设我们有一个简单的 MNIST 分类模型:

import tensorflow as tf

# 构建模型
model = tf.keras.Sequential([
    tf.keras.layers.Input(shape=(28, 28, 1)),
    tf.keras.layers.Conv2D(8, 3, activation='relu'),
    tf.keras.layers.MaxPooling2D(),
    tf.keras.layers.Flatten(),
    tf.keras.layers.Dense(10, activation='softmax')
])

# 训练(省略数据加载)
model.compile(optimizer='adam', loss='sparse_categorical_crossentropy')
# model.fit(x_train, y_train, epochs=5)

# 方法1:直接保存为 SavedModel(推荐)
tf.saved_model.save(model, "mnist_savedmodel")

# 方法2:保存为 H5,再手动冻结
model.save("mnist.h5")
# 加载 H5 并重新导出为 SavedModel
loaded = tf.keras.models.load_model("mnist.h5")
tf.saved_model.save(loaded, "mnist_frozen_savedmodel")

转换时,使用 TFLiteConverter 读取 SavedModel:

converter = tf.lite.TFLiteConverter.from_saved_model("mnist_savedmodel")
tflite_model = converter.convert()
with open("mnist.tflite", "wb") as f:
    f.write(tflite_model)

如果转换过程中出现“Variable”相关错误,说明图未冻结。此时需要检查模型中是否包含 BatchNormalization 层(其内部有滑动平均变量)或自定义训练层。解决方案是:在导出前调用 model.trainable = False,并确保所有 BatchNorm 层已设置为推理模式(model.layers[i].trainable = False)。

经验提醒:对于包含 BatchNorm 的模型,冻结时务必确认其 training=False 参数。在 Keras 中,可以通过 model.predict(x[:1]) 触发一次前向传播,让 BatchNorm 使用滑动均值,然后再导出。否则,转换后的 TFLite 模型在推理时可能产生错误的归一化结果。

2.6 常见误区与总结复盘

在模型转换前的准备阶段,以下误区最为常见:

  • 误区一:混淆 Eager 与 Graph。在 Eager 模式下直接调用 converter.convert() 会报错,因为 Eager 张量不是图节点。必须通过 tf.function 或 Keras 模型封装。
  • 误区二:忽略输入形状。TFLite Converter 需要明确的输入形状。如果模型使用动态形状(如 None),转换器会报错或生成低效的图。建议在模型第一层指定固定形状。
  • 误区三:使用未冻结的 ckpt 直接转换。ckpt 不是图文件,无法直接输入 Converter。必须先重建图、加载权重、再导出为 SavedModel 或冻结 pb。
  • 误区四:认为 pb 一定比 SavedModel 好。对于 TFLite Micro,两者都可以,但 SavedModel 包含签名,转换时更少出错。pb 需要手动指定输入输出张量名称,容易写错。