3. TensorFlow模型基础:回顾Keras Sequential/Functional API、SavedModel格式和Concrete Function的基本概念与区别

在深入TFLite Micro转换工具链之前,必须先厘清TensorFlow模型在PC端训练完成后的几种存在形态。很多开发者习惯性地将“训练好的模型”等同于“一个.h5文件”或“一个.pb文件”,但在TensorFlow 2.x的生态中,模型从构建到部署要经历代码定义、训练态、导出态和推理态四个阶段。Keras API负责前两个阶段,SavedModel是标准交换格式,而Concrete Function则是将计算图固化为可执行实体的关键桥梁。本章将系统梳理这三者的定位、结构差异与转换关系,为后续学习TFLite Micro的量化与转换打下坚实基础。

Keras API 层 SavedModel 格式层 Concrete Function 层 Sequential API 线性堆叠,适合前馈网络 Functional API 图结构,支持多输入/输出 Model 子类化 自定义前向传播逻辑 训练态:权重 + 优化器 model.fit() / model.save() assets/ 外部资源(词表、字典) variables/ 权重与变量检查点 saved_model.pb 图结构 + 元数据(协议缓冲区) 导出态:语言无关 tf.saved_model.save() FunctionSpec 输入签名定义 Graph 固化后的计算图(无Python) Outputs 张量输出定义 推理态:可执行实体 get_concrete_function() 导出 导出 加载 加载 Keras定义 → SavedModel交换 → Concrete Function执行,是TensorFlow 2.x模型部署的核心链路

上图展示了从Keras API到SavedModel再到Concrete Function的完整链路。Keras层负责模型的定义与训练,SavedModel层提供语言无关的持久化格式,而Concrete Function层则是将计算图固化为可被底层运行时直接调用的执行单元。下面我们逐一深入剖析。

3.1 Keras Sequential与Functional API:模型定义的两大范式

核心原理:从“线性堆叠”到“图结构”

Keras Sequential API是最直观的模型构建方式,它假设网络结构是一个简单的层叠栈:每一层只有一个输入张量和一个输出张量,数据从输入层依次流经各隐藏层到达输出层。这种范式适用于绝大多数前馈神经网络,如全连接网络、卷积神经网络(CNN)的典型堆叠结构。其内部实现实际上是将层列表包装成一个tf.keras.Sequential实例,在调用build()时自动推断输入形状。

Functional API则提供了更灵活的图结构定义方式。它允许层实例被显式调用,从而构建出具有分支、合并、多输入、多输出的计算图。例如,一个模型可以同时接收图像特征和文本特征,分别经过不同的子网络后拼接,再输出多个预测结果。Functional API的核心思想是“层即函数”,通过函数式调用来描述张量之间的流动关系。

判断方法:如何选择API

维度Sequential APIFunctional API
网络拓扑线性堆叠,无分支任意有向无环图(DAG)
多输入/输出不支持原生支持
共享层不支持支持(同一层实例被多次调用)
调试难度低,结构清晰中等,需关注张量形状
适用场景标准CNN/MLP多模态、残差连接、Inception等

选择原则很简单:如果模型是“一条直线走到底”,用Sequential;只要出现任何分支、合并或共享层,就必须使用Functional API。Model子类化虽然提供了最大的自由度,但会牺牲SavedModel导出的便利性,在TFLite Micro部署场景中应尽量避免。

案例拆解:一个简单的Functional模型定义

import tensorflow as tf

# Functional API 定义多输入模型
input_a = tf.keras.Input(shape=(32,), name='sensor_a')
input_b = tf.keras.Input(shape=(64,), name='sensor_b')

dense_a = tf.keras.layers.Dense(16, activation='relu')(input_a)
dense_b = tf.keras.layers.Dense(16, activation='relu')(input_b)

merged = tf.keras.layers.Concatenate()([dense_a, dense_b])
output = tf.keras.layers.Dense(1, activation='sigmoid', name='output')(merged)

model = tf.keras.Model(inputs=[input_a, input_b], outputs=output)
model.compile(optimizer='adam', loss='binary_crossentropy')

这个模型接收两个传感器输入,分别经过独立的Dense层后拼接,最终输出一个二分类概率。在训练完成后,该模型可以通过model.save('sensor_model.h5')保存,但更推荐使用SavedModel格式。

经验提醒:在定义模型时,务必为每一层和输入输出指定清晰的name参数。这些名称在后续的Concrete Function签名和TFLite转换中会成为张量标识符,良好的命名习惯能大幅减少调试时间。

3.2 SavedModel格式:跨平台的标准交换格式

核心原理:目录即模型

SavedModel是TensorFlow官方推荐的模型持久化格式,它不是一个单一文件,而是一个包含多个子目录和文件的目录结构。其设计目标是实现“语言无关、环境无关”的模型交换。一个标准的SavedModel目录包含以下内容:

  • saved_model.pb:协议缓冲区文件,存储了计算图的MetaGraph定义,包括图结构、集合(collections)、签名定义等。
  • variables/:包含训练得到的权重和变量,通常以检查点(checkpoint)格式存储,分为variables.index和variables.data-*文件。
  • assets/:可选目录,存放模型运行所需的外部资源文件,如词汇表、预训练嵌入等。

SavedModel的核心优势在于其自包含性和版本兼容性。它不依赖原始的Python代码,任何支持TensorFlow的运行时(包括TFLite、TensorFlow Serving、TFLite Micro)都可以加载并执行。

操作流程:导出与加载

# 导出SavedModel
tf.saved_model.save(model, 'saved_model_dir/')

# 加载SavedModel
loaded_model = tf.saved_model.load('saved_model_dir/')
# 查看可用签名
print(list(loaded_model.signatures.keys()))  # 输出: ['serving_default']

在导出时,TensorFlow会自动生成一个默认的serving签名,该签名基于模型的输入输出张量名称。如果模型有多个输入或需要自定义签名,可以在导出时通过signatures参数显式指定。

常见误区:混淆.h5与SavedModel

风险说明:很多开发者习惯使用model.save('model.h5')保存Keras模型。H5格式虽然方便,但它仅包含模型结构和权重,不包含完整的计算图元数据,且对自定义层、自定义损失函数的支持较差。在TFLite Micro转换链路中,强烈建议使用SavedModel作为中间格式,因为它保留了完整的签名信息,这是后续生成Concrete Function的必要条件。

3.3 Concrete Function:从“图定义”到“可执行实体”

核心原理:固化输入签名

Concrete Function是TensorFlow 2.x中tf.function机制的核心产物。当我们使用@tf.function装饰一个Python函数时,TensorFlow会将其追踪(trace)为计算图。但tf.function本身是“多态的”——它可以根据不同的输入形状或数据类型生成不同的底层图。而Concrete Function则是将某一特定输入签名下的图固化下来,形成一个独立的、可序列化的执行单元。

从技术角度看,Concrete Function包含三个关键部分:FunctionSpec(输入签名定义)、Graph(固化后的计算图,不包含任何Python控制流)、Outputs(输出张量的定义)。它可以通过get_concrete_function()方法从SavedModel中提取,也可以直接从tf.function对象创建。

操作流程:从SavedModel获取Concrete Function

# 加载SavedModel
loaded = tf.saved_model.load('saved_model_dir/')

# 获取默认的serving签名对应的Concrete Function
infer_func = loaded.signatures['serving_default']

# 查看输入输出张量的具体信息
print(infer_func.structured_input_signature)
print(infer_func.structured_outputs)

# 直接调用Concrete Function进行推理
import numpy as np
input_data = tf.constant(np.random.rand(1, 32).astype(np.float32))
result = infer_func(input_data)
print(result['output'].numpy())

在TFLite Micro转换中,我们通常需要将Concrete Function的输入输出张量信息作为转换器的输入输出规范。例如,使用TFLiteConverter时,可以指定converter.concrete_func = infer_func来明确转换的边界。

案例拆解:一个完整的转换前验证流程

假设我们有一个训练好的Keras模型,需要验证其能否被正确转换为TFLite格式。以下是标准流程:

  1. 使用Functional API定义模型并训练。
  2. 导出为SavedModel:tf.saved_model.save(model, 'tmp/saved_model')。
  3. 加载并获取Concrete Function:loaded = tf.saved_model.load('tmp/saved_model'),然后cf = loaded.signatures['serving_default']。
  4. 使用随机数据测试Concrete Function的输出,确保与原始Keras模型输出一致。
  5. 将Concrete Function传递给TFLiteConverter进行转换。

这个流程中,第4步至关重要。很多转换失败的问题(如不支持的算子、形状不匹配)都可以在步骤4提前暴露,而不是等到TFLite转换时才报错。

常见误区与总结复盘

核心要点回顾:
  • Keras Sequential/Functional API是模型定义的“源代码”,它们描述的是计算逻辑,而非可执行实体。
  • SavedModel是“编译后的可移植文件”,它包含了图结构、权重和签名,但不包含Python代码。
  • Concrete Function是“特定输入签名下的可执行图”,它是TFLite转换的直接输入。
  • 三者关系:Keras定义 → SavedModel导出 → Concrete Function提取 → TFLite转换。

一个常见的错误是试图直接将Keras模型对象传递给TFLiteConverter。虽然TFLiteConverter也支持直接接收Keras模型,但在复杂模型(如包含预处理逻辑、自定义层)时,这种方式容易丢失关键信息。正确的做法是始终通过SavedModel和Concrete Function链路进行转换,这样能确保所有签名和元数据都被完整保留。

另一个值得注意的细节是:Concrete Function的输入张量形状必须是静态的(即固定维度)。如果模型在训练时使用了动态形状(如None维度),在获取Concrete Function时必须指定具体的形状,否则TFLite转换会因无法确定输入大小而失败。在实际部署中,建议在模型定义时就固定输入形状,或者在获取Concrete Function时显式传入tf.TensorSpec。

最后,理解这三者的区别对于调试TFLite Micro的转换问题至关重要。当转换报错时,首先要判断错误发生在哪一层:是Keras模型定义的问题(如使用了不支持的层),还是SavedModel导出时签名不完整,还是Concrete Function提取时形状不匹配。这种分层排查的思路,能帮助开发者快速定位问题根源,避免在工具链的迷宫中浪费时间。