Keras 3 架构解析(八):模型持久化与部署系统——从模型序列化到跨后端推理
Keras 3 架构解析(八):模型持久化与部署系统——从模型序列化到跨后端推理
引言
在前七篇文章中,我们分别介绍了 Keras 3 的基础计算层、神经网络层、模型构建层、训练系统、数据处理系统、训练控制与监控系统,以及高性能计算系统。
这些部分共同构成了深度学习模型从数学定义到训练执行的完整技术体系。
基础计算层提供统一的张量运算能力;神经网络层通过 Layer 抽象管理计算逻辑与参数状态;模型构建层通过 Model 将多个计算组件组织成完整网络;训练系统通过损失函数、优化器和训练循环更新模型参数;数据处理系统负责持续提供训练样本;Callback 机制负责管理训练生命周期;高性能计算系统则通过混合精度、JIT 编译和分布式执行提高计算效率。
然而,模型完成训练并不意味着深度学习系统的工作已经结束。
一个经过训练的神经网络,其实际价值通常体现在后续使用过程中。例如,将模型加载到新的计算进程中进行推理,在训练中断后恢复模型状态,或者将模型部署到不依赖原始训练框架的生产环境。
为了完成这些任务,深度学习框架必须解决模型状态的持久化与计算程序的迁移问题。
这里存在两个不同的技术目标。
第一个目标是模型持久化(Model Persistence),即将模型的结构、参数及相关状态保存下来,使其能够在后续重新构建和使用。
第二个目标是模型部署(Model Deployment),即将训练完成的模型转换为适合目标运行环境的计算产物,并提供稳定的推理接口。
二者存在联系,但并不等价。
一个能够完整恢复训练状态的模型文件,未必能够直接被高性能推理引擎执行。相反,一个经过优化的推理模型,可能只包含执行预测所必需的计算图与参数,无法恢复原始训练过程。
Keras 3 通过模型序列化、权重保存、检查点管理和推理导出等机制,对这两类需求分别提供支持。
本文将从软件架构与计算执行两个角度出发,介绍 Keras 3 的模型持久化与部署系统,重点分析 .keras 原生模型格式、权重文件、检查点恢复、model.export()、SavedModel、ONNX、LiteRT,以及跨后端推理的兼容性边界。
一、模型持久化与模型部署的基本区别
1.1 模型并不只是参数矩阵
从数学角度看,一个神经网络可以表示为:
[
Y=f_\theta(X)
]
其中:
- (X):输入张量;
- (Y):输出张量;
- (f):模型的计算结构;
- (\theta):经过训练得到的参数。
如果只考虑推理任务,那么只需要保留足以计算 (f_\theta(X)) 的信息。
但完整的深度学习训练系统还包含其他状态。
例如,对于 Adam 优化器,训练过程中需要维护梯度的一阶矩和二阶矩估计。
因此,一个完整的训练状态可以概括为:
[
S_{\text{training}}
{
C,\theta,O,M,R,D
}
]
其中:
- (C):模型及训练配置;
- (\theta):模型参数及非训练状态;
- (O):优化器状态;
- (M):评价指标等训练状态;
- (R):随机数及其他运行状态;
- (D):数据迭代进度和相关外部状态。
这一表达式是概念性的,并不表示 Keras 的任何一种文件格式都会自动保存全部这些信息。
例如,.keras 文件能够保存模型结构、权重和相关优化器状态,但不会自动包含任意外部数据加载器的当前位置或所有自定义 Python 状态。
因此,需要区分模型持久化与完整实验状态持久化。
1.2 三种不同的保存目标
从工程需求来看,通常可以区分三种保存目标。
第一种:模型恢复
目标是重新创建模型对象,并恢复已经训练完成的参数。
这通常用于模型分析、继续开发、迁移学习或进一步训练。
第二种:训练恢复
目标是在发生中断之后,尽可能从此前的训练状态继续执行。
除模型参数外,还可能需要优化器状态、训练进度及其他上下文。
第三种:推理部署
目标是在独立运行环境中,根据输入数据稳定地产生预测结果。
此时,通常不需要保存优化器、损失函数或训练指标。
三种目标对应的技术选择并不相同。
| 目标 | 需要模型结构 | 需要权重 | 需要优化器状态 | 主要用途 |
|---|---|---|---|---|
| 完整模型恢复 | 是 | 是 | 继续训练时需要 | 开发与再训练 |
| 仅权重恢复 | 由外部提供 | 是 | 取决于任务 | 参数迁移 |
| 训练检查点 | 是或可重建 | 是 | 通常需要 | 故障恢复 |
| 推理部署 | 可执行计算表示 | 是 | 否 | 生产推理 |
其中,推理部署所需要的模型结构不一定以 Keras 的 Python 对象形式存在,也可以表示为 ONNX 计算图或其他推理运行时支持的程序形式。
1.3 保存与导出的架构关系
Keras 3 的相关机制可以概括为:
Keras Model
│
┌──────────┴───────────┐
│ │
▼ ▼
模型持久化 推理导出
│ │
┌─────┴─────┐ ┌───────┴────────┐
▼ ▼ ▼ ▼ ▼
.keras Weights SavedModel ONNX 其他格式
│ │ │ │ │
▼ ▼ ▼ ▼ ▼
Keras 加载 参数恢复 推理运行时与部署环境
│
▼
再训练或分析
这张图展示的是逻辑关系,而不是文件之间必须经过的转换顺序。
例如,Keras 可以从内存中的 Model 直接导出 ONNX,并不要求先保存为 .keras 文件。
这种分离是理解 Keras 3 持久化系统的基础。
二、Keras 原生模型格式:.keras
2.1 .keras 的设计目标
Keras 3 推荐使用 .keras 作为原生模型保存格式。
该格式主要用于保存完整的 Keras 模型对象及其相关状态。
例如:
model.save("model.keras")
重新加载:
import keras
model = keras.saving.load_model(
"model.keras"
)
对于能够正确序列化的模型,这种方式可以恢复模型结构和权重,并在相关训练配置存在时恢复优化器状态。
它是 Keras 3 中最直接的模型持久化接口。
2.2 .keras 文件的内部结构
默认情况下,保存到本地的 .keras 文件是一个 ZIP 归档。
其主要内容可以概括为:
model.keras
│
├── config.json
│
├── metadata.json
│
└── model.weights.h5
这里的结构代表常见的原生格式组成方式。实际归档还可能包含其他资产或状态文件。
其中:
config.json
用于记录模型和网络层的结构配置,包括各组件的类型、构造参数及连接关系等。
metadata.json
用于记录模型归档的相关元信息,例如 Keras 版本。
model.weights.h5
用于存储模型变量及其他相关状态。对于已经编译并具有优化器状态的模型,归档还可以包含优化器变量。
需要注意,.keras 文件并不保存自定义 Python 类的完整源代码。
因此,如果模型使用了开发者自行实现的 Layer、Model 或其他组件,重新加载时仍可能需要提供相应的 Python 定义。
2.3 模型保存的基本示例
首先构建一个简单模型:
import keras
from keras import layers
model = keras.Sequential([
keras.Input(shape=(16,)),
layers.Dense(
64,
activation="relu"
),
layers.Dense(1)
])
配置训练:
model.compile(
optimizer=keras.optimizers.Adam(
learning_rate=1e-3
),
loss="mse"
)
经过训练后:
model.fit(
x_train,
y_train,
epochs=10
)
可以直接保存:
model.save("regression.keras")
然后在另一个 Python 进程中加载:
restored_model = keras.saving.load_model(
"regression.keras"
)
如果保存时模型已完成编译,默认的 compile=True 会尝试恢复相应训练配置。
对于只需要执行推理的任务,可以使用:
restored_model = keras.saving.load_model(
"regression.keras",
compile=False
)
这样可以跳过模型编译配置的恢复。
对于只负责推理的应用程序,这种方式通常更加直接。
2.4 压缩归档与目录保存
较新的 Keras 3 API 还提供 zipped 参数,用于控制是否将模型保存为 ZIP 归档。
例如:
model.save(
"model.keras",
zipped=True
)
表示使用压缩归档。
也可以使用:
model.save(
"model_directory",
zipped=False
)
将模型保存为未压缩的目录结构。
这种形式可用于特定文件系统或模型仓库工作流。
但需要注意,未压缩的 .keras 目录并不等同于 TensorFlow SavedModel。
两者具有不同的内部格式和使用目的。
三、模型配置序列化与状态恢复
3.1 模型配置和模型权重是不同对象
假设某个 Dense 层具有:
layers.Dense(
units=64,
activation="relu"
)
其配置描述了:
- 输出特征数量;
- 激活函数;
- 是否使用偏置;
- 权重初始化方式;
- 其他层级配置。
但这些配置并不包含训练之后的具体权重数值。
例如,一个 Dense 层可能具有:
[
W\in\mathbb{R}^{16\times64}
]
其配置只决定权重矩阵的形状和初始化规则。
真正的参数状态则由矩阵中的 1,024 个数值及偏置等变量构成。
因此,模型的结构恢复与参数恢复是两个不同过程。
3.2 get_config() 与 from_config()
Keras 提供:
layer.get_config()
用于获取层的配置。
例如:
layer = keras.layers.Dense(
64,
activation="relu"
)
config = layer.get_config()
可以通过:
new_layer = keras.layers.Dense.from_config(
config
)
重新构建相同配置的层。
但此时,新层的权重并不自动等于原层经过训练的权重。
对于模型,也存在对应的结构序列化机制。
因此:
[
\text{Configuration}
\neq
\text{Weights}
]
这一点在模型复制和迁移学习中尤其重要。
3.3 clone_model() 不复制训练参数
Keras 提供:
new_model = keras.models.clone_model(
model
)
该方法主要根据模型配置重建相同的结构。
通常情况下,新模型会重新初始化参数。
如果希望同时复制模型当前权重,可以在结构兼容的前提下使用:
new_model.set_weights(
model.get_weights()
)
因此,clone_model() 与完整模型持久化并不是同一个概念。
前者主要用于创建结构副本,后者则用于保存和恢复模型状态。
四、仅保存模型权重
4.1 save_weights() 的使用场景
某些任务并不需要保存完整模型结构。
例如,模型结构已经由项目代码固定定义,后续只需要替换不同训练阶段的参数。
此时,可以只保存权重:
model.save_weights(
"model.weights.h5"
)
重新加载时:
model.load_weights(
"model.weights.h5"
)
这种方式通常适用于:
- 模型结构由代码独立维护;
- 迁移学习;
- 参数初始化实验;
- 模型微调;
- 多个相同结构模型之间的权重迁移。
但它也引入了一个重要约束。
加载权重的模型必须具有兼容的参数结构。
如果原模型和目标模型的参数形状不同,通常无法直接完成加载。
4.2 权重加载与网络拓扑
Keras 的权重加载机制通常根据网络结构和权重组织关系进行匹配。
例如:
def build_model():
return keras.Sequential([
keras.Input(shape=(16,)),
keras.layers.Dense(64),
keras.layers.Dense(1)
])
首先保存:
model_a = build_model()
model_a.save_weights(
"model.weights.h5"
)
然后创建新模型:
model_b = build_model()
model_b.load_weights(
"model.weights.h5"
)
由于两个模型具有相同的权重结构,因此可以恢复相应参数。
但如果将第二个模型改为:
keras.layers.Dense(128)
原有权重形状就可能不再匹配。
4.3 部分权重加载
Keras 允许使用:
model.load_weights(
"model.weights.h5",
skip_mismatch=True
)
跳过与当前模型不兼容的权重。
这种方式适合部分迁移学习场景。
例如,使用已经训练完成的特征提取网络,同时替换最后的预测层。
但 skip_mismatch=True 并不意味着 Keras 能够自动判断哪些参数在语义上适合迁移。
它主要处理权重结构不匹配的问题。
因此,加载完成后,应当检查哪些权重被成功恢复、哪些权重被跳过,而不能仅根据程序没有报错就认定参数迁移完全正确。
4.4 大模型的分片权重保存
对于参数规模较大的模型,单个权重文件可能非常庞大。
较新的 Keras 3 提供 max_shard_size 参数,可以将权重拆分为多个文件。
例如:
model.save_weights(
"large_model.weights.json",
max_shard_size=0.25
)
这里,max_shard_size 以 GB 为单位设置分片大小上限。
保存结果通常包含:
large_model.weights.json
large_model_00000.weights.h5
large_model_00001.weights.h5
large_model_00002.weights.h5
...
其中,JSON 文件保存分片元信息和权重映射关系,HDF5 文件保存实际数据。
加载时:
model.load_weights(
"large_model.weights.json"
)
需要注意,分片权重保存主要解决文件组织与存储管理问题,并不自动等价于模型并行或分布式参数分片。
将权重保存到多个磁盘文件,不意味着推理时会自动在多个 GPU 上分配这些权重。
五、自定义模型与自定义 Layer 的序列化
5.1 自定义组件的特殊问题
Keras 内置的 Dense、Conv2D 和 LSTM 等层,通常已经具备完整的配置序列化机制。
但自定义 Layer 可能包含开发者自行设计的构造参数、子层和内部状态。
例如:
class CustomLayer(keras.layers.Layer):
...
Keras 无法在所有情况下自动推断这个 Python 类应当如何重新创建。
因此,自定义组件需要明确提供可序列化的构造配置。
5.2 使用 get_config()
下面定义一个简单的特征缩放层。
import keras
from keras import ops
@keras.saving.register_keras_serializable(
package="Custom"
)
class FeatureScale(keras.layers.Layer):
def __init__(self, scale=1.0, **kwargs):
super().__init__(**kwargs)
self.scale = scale
def call(self, inputs):
return ops.multiply(
inputs,
self.scale
)
def get_config(self):
config = super().get_config()
config.update({
"scale": self.scale
})
return config
该层实现:
[
Y=sX
]
其中,(s) 是通过构造参数指定的缩放系数。
这里的 scale 是普通配置值,而不是通过梯度更新的可训练参数。
get_config() 的作用,是保存重新构建该层所需要的配置。
5.3 自定义对象注册
上述代码使用了:
@keras.saving.register_keras_serializable(
package="Custom"
)
该装饰器将自定义组件注册到 Keras 的序列化系统中。
当模型被保存时,Keras 可以记录该组件的注册名称。
在加载过程中,如果对应的类已经被注册,就可以根据注册信息恢复对象。
例如:
inputs = keras.Input(shape=(16,))
x = FeatureScale(scale=0.5)(inputs)
outputs = keras.layers.Dense(1)(x)
model = keras.Model(
inputs,
outputs
)
保存:
model.save(
"custom_model.keras"
)
加载:
restored_model = keras.saving.load_model(
"custom_model.keras"
)
需要注意,加载进程仍然需要能够访问自定义类的 Python 定义,并完成相应注册。
.keras 文件不会自动包含这个类的完整源代码。
5.4 复杂构造参数
如果自定义组件的构造参数包含另一个 Layer 或 Model,仅仅把对象直接放入配置字典通常不够。
例如:
self.sublayer = sublayer
这类对象通常需要使用:
keras.saving.serialize_keras_object()
进行序列化。
恢复时可以使用:
keras.saving.deserialize_keras_object()
如果默认的 from_config() 无法正确恢复嵌套组件,开发者就需要自行实现对应方法。
因此,自定义组件能否参与前向计算,与能否被完整序列化,是两个不同的能力。
5.5 自定义状态与外部资产
某些模型包含普通权重之外的其他状态。
例如:
- 自定义变量;
- 词表;
- 查找表;
- 特殊的构建参数;
- 自定义编译配置。
Keras 提供相应扩展接口:
| 接口 | 主要作用 |
|---|---|
save_own_variables() |
自定义变量保存 |
load_own_variables() |
自定义变量恢复 |
save_assets() |
保存额外资产 |
load_assets() |
加载额外资产 |
get_build_config() |
保存特殊构建配置 |
build_from_config() |
恢复特殊构建状态 |
get_compile_config() |
保存自定义编译配置 |
compile_from_config() |
恢复自定义编译配置 |
这些机制使复杂模型能够扩展 Keras 的原生持久化能力。
但它们并不是所有自定义 Layer 都必须实现的接口。
对于普通的参数化计算层,add_weight()、get_config() 和正确的对象注册通常已经足够。
六、训练检查点与断点恢复
6.1 检查点与完整模型保存的区别
在上一篇文章中,我们已经介绍了 ModelCheckpoint 和 BackupAndRestore。
从持久化系统的角度看,它们承担不同职责。
ModelCheckpoint 主要用于在训练过程中保存符合特定条件的模型状态。
BackupAndRestore 主要用于在训练意外中断后恢复训练进度。
因此,训练检查点不仅涉及模型参数,还可能涉及优化器内部状态和训练进度。
6.2 优化器状态为什么重要
以 Adam 优化器为例,其参数更新不仅依赖当前梯度,还依赖历史梯度的一阶矩和二阶矩估计:
[
m_t=
\beta_1m_{t-1}
+(1-\beta_1)g_t
]
[
v_t=
\beta_2v_{t-1}
+(1-\beta_2)g_t^2
]
因此,后续参数更新由以下状态共同决定:
[
(\theta_t,m_t,v_t,t)
]
如果只恢复模型参数:
[
\theta_t
]
而没有恢复:
[
m_t,v_t,t
]
即使继续使用相同的 Adam 配置,后续训练轨迹也可能不同。
因此,需要精确继续训练时,应优先使用能够保存优化器状态的机制。
但还必须强调,恢复优化器状态只是训练恢复的一部分。
如果数据采样顺序、随机数生成器状态或外部数据来源发生变化,训练轨迹仍可能与中断前不同。
6.3 ModelCheckpoint
例如:
checkpoint = keras.callbacks.ModelCheckpoint(
filepath="best_model.keras",
monitor="val_loss",
mode="min",
save_best_only=True
)
该 Callback 根据验证损失保存最佳模型。
这适合模型选择,但不等于完整的故障恢复策略。
例如,验证指标最好的 Epoch 可能早于训练中断时的 Epoch。
因此,从最佳模型重新开始训练,不一定能够恢复最近的训练状态。
6.4 BackupAndRestore
对于需要恢复最近训练进度的任务,可以使用:
backup = keras.callbacks.BackupAndRestore(
backup_dir="./backup",
save_freq="epoch"
)
它能够在训练中断后恢复适用的训练状态。
但需要注意,BackupAndRestore 不会自动重新启动已经终止的 Python 进程,也不会保存任意外部数据加载器的全部状态。
因此,正式训练系统仍然需要独立管理任务启动、数据版本和外部运行环境。
6.5 OrbaxCheckpoint:面向 JAX 分布式训练
对于使用 JAX 后端的较大规模训练,Keras 3 还提供 OrbaxCheckpoint。
Orbax 是 JAX 生态中的检查点管理工具。
从 Keras 3.14 开始,Keras 提供内置的:
keras.callbacks.OrbaxCheckpoint
例如:
checkpoint = keras.callbacks.OrbaxCheckpoint(
directory="./orbax_checkpoints",
max_to_keep=5,
save_freq="epoch"
)
该机制适合需要进行分布式状态保存的 JAX 训练任务。
其主要能力包括:
- 保存模型与优化器状态;
- 检查点保留数量管理;
- 异步保存;
- 多主机训练协调;
- 在支持的情况下恢复到不同的分片布局。
对于模型并行训练,权重可能分布在多个设备上,因此检查点保存需要正确处理分布式张量及其布局信息。
这与简单地将所有参数集中保存到单个文件的需求有所不同。
不过,OrbaxCheckpoint 并不是 Keras 所有计算后端通用的检查点实现。
它主要服务于 JAX 生态和相应的分布式训练机制。
七、模型导出:从训练对象到推理产物
7.1 为什么需要模型导出
.keras 文件主要服务于 Keras 模型的恢复。
但生产环境不一定需要安装完整的 Keras 训练框架。
例如,一个应用系统可能使用:
- TensorFlow Serving;
- ONNX Runtime;
- OpenVINO;
- LiteRT;
- PyTorch 的导出运行时;
- 其他专用推理引擎。
这些运行环境可能不支持直接加载 .keras 文件。
因此,需要将模型转换为目标运行时能够理解的表示形式。
这就是模型导出(Model Export)。
7.2 推理导出的数学目标
假设训练模型为:
[
Y=f_\theta(X)
]
导出过程希望生成一个独立的推理程序:
[
Y=g_{\theta’}(X)
]
其中,(g) 是目标运行时支持的计算表示。
在理想情况下:
[
g_{\theta’}(X)\approx f_\theta(X)
]
对于纯粹的格式转换,两者应在合理的浮点误差范围内保持数值一致。
如果导出过程中还进行了量化、算子替换或其他优化,则可能引入额外数值误差。
因此,模型导出的核心不只是生成文件,还包括验证:
导出后的推理程序是否仍然实现了预期的数学计算。
7.3 Model.export()
Keras 3 提供:
model.export()
作为统一的推理导出接口。
其主要形式为:
model.export(
filepath,
format="tf_saved_model"
)
按照当前 Keras 3 官方 API,主要支持以下导出格式:
| 格式参数 | 导出对象 | 主要应用 |
|---|---|---|
tf_saved_model |
TensorFlow SavedModel | TensorFlow Serving、TensorFlow 推理 |
onnx |
ONNX 模型 | 跨框架推理 |
openvino |
OpenVINO 模型产物 | OpenVINO 运行时 |
litert |
LiteRT / TFLite 模型 | 移动端与边缘设备 |
torch |
PyTorch ExportedProgram | PyTorch 导出运行时 |
需要注意,官方 API 列出某种导出格式,不代表所有计算后端、所有模型结构和全部算子都可以无条件导出。
实际支持情况还取决于:
- 当前 Keras 后端;
- 模型所使用的算子;
- 转换工具版本;
- 目标格式支持的操作集合;
- 输入形状与数据类型;
- 目标推理运行时的功能。
因此,模型导出应当被视为一个需要单独验证的工程阶段。
八、TensorFlow SavedModel
8.1 SavedModel 的基本定位
TensorFlow SavedModel 是 TensorFlow 生态中的模型保存与执行格式。
它可以保存执行推理所需要的计算函数、变量和相关资源。
对于模型部署而言,SavedModel 的重要特点是能够提供明确的可调用函数入口。
因此,推理服务不必重新执行原始的 Keras 模型构建代码,就可以调用已经导出的计算函数。
8.2 使用 model.export()
例如:
model.export(
"artifacts/saved_model",
format="tf_saved_model"
)
该操作会将模型导出到指定目录。
之后可以使用 TensorFlow 加载:
import tensorflow as tf
serving_model = tf.saved_model.load(
"artifacts/saved_model"
)
对于通过 Keras 默认导出方式生成的 SavedModel,通常可以使用:
outputs = serving_model.serve(
input_tensor
)
执行推理。
这里的 serve 是导出产物中的推理端点名称。
需要注意,端点名称属于模型导出接口的一部分。如果使用了不同的导出工具或自定义端点,实际名称可能不同。
8.3 SavedModel 与 .keras 的区别
两者的职责存在明显差异。
.keras 更适合恢复 Keras 的模型结构、权重和训练配置。
SavedModel 更强调将已经确定的计算函数提供给目标运行时执行。
因此:
model.save("model.keras")
与:
model.export(
"saved_model",
format="tf_saved_model"
)
并不是同一种操作。
在 Keras 3 中,不能通过直接将 model.save() 的目标路径改为普通目录,就将模型保存为 TensorFlow SavedModel。
需要使用相应的导出接口。
九、ExportArchive:自定义推理接口
9.1 为什么需要自定义导出接口
简单模型通常只需要一个推理入口。
例如:
[
Y=f_\theta(X)
]
但复杂应用可能需要多个推理函数。
例如:
- 输出最终预测结果;
- 输出中间特征;
- 根据不同输入结构执行不同任务;
- 使用不同的推理配置。
Keras 提供:
keras.export.ExportArchive
用于自定义 SavedModel 导出端点。
9.2 定义推理端点
例如:
import keras
archive = keras.export.ExportArchive()
archive.track(model)
archive.add_endpoint(
name="serve",
fn=lambda x: model(
x,
training=False
),
input_signature=[
keras.InputSpec(
shape=(None, 16),
dtype="float32"
)
]
)
archive.write_out(
"artifacts/custom_saved_model"
)
这里:
track():注册模型及相关资源;add_endpoint():定义推理函数;input_signature:定义输入张量的形状和数据类型;write_out():写出 SavedModel。
该模型的输入契约为:
[
X\in\mathbb{R}^{B\times16}
]
其中 (B) 是批大小。
9.3 多端点模型
可以进一步注册多个端点。
例如:
archive.add_endpoint(
name="predict",
fn=lambda x: model(
x,
training=False
),
input_signature=[
keras.InputSpec(
shape=(None, 16),
dtype="float32"
)
]
)
对于需要输出中间特征的模型,也可以将特征提取子模型注册为另一个端点。
这样,一个模型产物就可以提供多个具有明确输入输出契约的计算函数。
但需要注意,只有被正确跟踪的变量和资源,才能在导出后的计算函数中使用。
对于包含 Lookup Table、词表等特殊资源的模型,应当特别检查资源跟踪行为。
9.4 input_signature 的意义
输入签名是模型部署中的重要概念。
它描述推理函数接受什么类型的数据。
例如:
keras.InputSpec(
shape=(None, 60, 16),
dtype="float32"
)
表示输入是一个三维浮点张量。
其中:
- 第一维是批大小;
- 第二维是序列长度;
- 第三维是特征数量。
在这个示例中:
[
X\in\mathbb{R}^{B\times60\times16}
]
输入签名可以帮助运行时确定计算函数的接口。
但不同推理格式对动态维度的支持并不完全相同。
例如,某些导出路径要求使用固定形状,而不能保留任意动态维度。
因此,输入签名的选择需要与目标运行时的约束保持一致。
十、TFSMLayer:将 SavedModel 重新接入 Keras
10.1 SavedModel 不能直接通过 load_model() 恢复
Keras 3 有一个重要变化:
keras.saving.load_model() 不再将 TensorFlow SavedModel 当作原生 Keras 模型直接加载。
例如:
keras.saving.load_model(
"artifacts/saved_model"
)
对于普通 SavedModel 目录,并不是 Keras 3 支持的完整模型恢复方式。
如果需要重新使用 SavedModel,可以通过 TensorFlow 原生加载接口,或者在 TensorFlow 后端中使用 TFSMLayer。
10.2 TFSMLayer 的基本使用
例如:
import keras
layer = keras.layers.TFSMLayer(
"artifacts/saved_model",
call_endpoint="serve"
)
然后:
inputs = keras.Input(shape=(16,))
outputs = layer(inputs)
model = keras.Model(
inputs,
outputs
)
此时,原本导出的 SavedModel 被包装为一个 Keras Layer。
它可以重新参与 Keras 模型的组合。
10.3 TFSMLayer 的限制
TFSMLayer 重新加载的是保存的计算函数,而不是原始模型对象的完整 Python 结构。
因此,它不会自动恢复原始模型中的所有自定义方法和内部层级关系。
例如,原模型可能包含:
self.encoder
self.decoder
self.custom_method()
这些 Python 对象关系并不保证能够从 SavedModel 中恢复。
此外,TFSMLayer 属于 TensorFlow 专用层,应当在相应的后端环境中使用。
Keras 官方文档允许在适用情况下通过 TFSMLayer 继续微调其可训练变量,但这不意味着它等价于原始模型的完整恢复。
如果需要区分训练模式与推理模式,还必须在导出时保存对应端点,并通过 call_training_endpoint 等参数进行配置。
因此,TFSMLayer 的核心价值在于复用已经导出的计算函数,而不是替代 .keras 的完整模型持久化机制。
十一、ONNX:跨框架推理格式
11.1 ONNX 的基本定位
ONNX(Open Neural Network Exchange)是一种用于表示机器学习计算图的开放格式。
它通过标准化计算操作、张量和图结构,使模型能够在不同的软件环境中交换。
从架构角度看,ONNX 更接近一种中间表示,而不是完整的深度学习训练框架。
其主要内容包括:
- 计算图节点;
- 运算符;
- 输入输出信息;
- 张量形状和类型;
- 模型权重;
- 模型元信息。
因此,ONNX 文件通常可以脱离原始 Keras 模型类进行推理。
11.2 Keras 导出 ONNX
Keras 3 提供:
model.export(
"model.onnx",
format="onnx"
)
在支持的模型与后端组合下,该操作可以生成 ONNX 模型文件。
对于不同后端,导出过程可能使用不同的模型转换工具。
因此,导出 ONNX 可能需要额外安装相应依赖。
不能假定只安装 Keras,就已经安装了所有 ONNX 转换组件和推理运行时。
11.3 使用 ONNX Runtime 执行推理
ONNX Runtime 是支持执行 ONNX 模型的推理运行时之一。
例如:
import onnxruntime as ort
session = ort.InferenceSession(
"model.onnx"
)
可以获得模型输入信息:
inputs = session.get_inputs()
for item in inputs:
print(
item.name,
item.shape,
item.type
)
假设模型只有一个输入:
input_name = session.get_inputs()[0].name
然后准备输入数据:
import numpy as np
x = np.random.randn(
8,
16
).astype("float32")
执行推理:
outputs = session.run(
None,
{
input_name: x
}
)
返回的是对应模型输出的 NumPy 数组列表。
对于多输入模型,需要根据 ONNX 输入名称提供所有必要的输入数据。
11.4 ONNX 的主要优势
ONNX 的重要价值,是将模型定义与某一个 Python 深度学习框架解耦。
例如,训练可以在 Keras 与 JAX 后端中进行,而推理可以在支持 ONNX 的独立运行时中执行。
这使模型能够进入不同的应用环境。
例如:
Keras + JAX
│
▼
训练模型
│
▼
ONNX
│
┌──────┼──────┐
▼ ▼ ▼
Python C++ Rust
│ │ │
└──────┼──────┘
▼
ONNX Runtime
│
▼
模型推理
这里的 Rust、C++ 和 Python 代表不同的应用语言,而实际能否使用某种语言执行 ONNX,需要依赖对应的推理运行时及其接口。
因此,ONNX 的优势不在于模型自动获得更高性能,而在于提供相对独立的模型交换与执行表示。
11.5 ONNX 的兼容性限制
ONNX 并不保证能够表示所有深度学习框架中的计算操作。
如果模型包含自定义算子、特殊控制流或特定后端独有的功能,导出可能失败。
此外,ONNX 使用 Opset 版本定义相应的运算符规范。
不同的推理运行时可能支持不同的 Opset 和算子集合。
因此:
能够成功导出 ONNX,不等于目标推理环境一定能够正确执行该模型。
正式部署时,还需要验证:
- 目标运行时能够成功加载模型;
- 所有必要算子均受到支持;
- 输入输出结构正确;
- 数值结果满足误差要求;
- 推理性能满足应用目标。
十二、其他推理导出格式
12.1 OpenVINO
OpenVINO 是面向推理执行和优化的运行时体系。
Keras 3 提供:
model.export(
"openvino_model",
format="openvino"
)
在支持的配置下,可以将模型转换为 OpenVINO 能够使用的形式。
OpenVINO 主要适用于需要利用其支持的 CPU、GPU 及其他设备执行推理的场景。
不过,最终能否获得性能收益,取决于目标硬件、模型算子和推理配置。
不能仅因为模型被导出为 OpenVINO 格式,就认定其执行速度一定高于原始框架。
12.2 LiteRT
LiteRT 是 Google 面向设备端推理的运行时体系,其前身为 TensorFlow Lite。
Keras 3 支持:
model.export(
"model.tflite",
format="litert"
)
该格式主要服务于移动设备和边缘计算场景。
LiteRT 通常更加关注:
- 模型文件大小;
- 推理延迟;
- 设备内存消耗;
- 移动端兼容性;
- 低功耗运行;
- 模型量化。
但需要注意,LiteRT 导出仍然受到算子支持和后端转换路径的限制。
特别是根据 Keras 官方文档,使用 PyTorch 后端导出 LiteRT 时,动态输入形状存在明确限制。
如果模型需要在运行时处理不同序列长度,可能需要使用固定输入形状和 Padding 策略。
12.3 PyTorch ExportedProgram
Keras 3 还提供:
model.export(
"model.pt2",
format="torch"
)
用于生成 PyTorch ExportedProgram 形式的产物。
在适用的 PyTorch 环境中,可以通过:
import torch
program = torch.export.load(
"model.pt2"
)
重新加载导出程序。
然后通过相应的 PyTorch 接口进行执行。
需要注意,PyTorch ExportedProgram 与 Keras 的 .keras 完整模型不是同一种对象。
前者主要描述经过导出的计算程序,后者包含 Keras 的模型配置和相关状态。
12.4 导出格式的选择
不同部署场景应选择不同的目标格式。
| 目标场景 | 可优先考虑 |
|---|---|
| 继续使用 Keras 开发和训练 | .keras |
| 保留现有模型代码,仅恢复参数 | .weights.h5 |
| TensorFlow 生态推理服务 | SavedModel |
| 独立推理引擎、跨语言应用 | ONNX |
| OpenVINO 支持的硬件环境 | OpenVINO |
| 移动端与边缘设备 | LiteRT |
| PyTorch 导出执行体系 | PyTorch ExportedProgram |
这些选择不是排他性的。
同一个训练完成的模型,可以同时保存 .keras 文件和多个推理格式。
这使训练恢复与生产部署能够使用不同的模型产物。
十三、跨后端模型兼容性的边界
13.1 Keras 统一接口与跨后端兼容
Keras 3 的重要设计目标之一,是使相同的模型代码能够运行在不同计算后端上。
例如:
from keras import ops
def call(self, inputs):
return ops.matmul(
inputs,
self.kernel
)
这种使用 Keras Ops 的计算方式,不直接依赖 TensorFlow、JAX 或 PyTorch 的原生张量 API。
因此,更容易在不同后端之间复用。
但是,跨后端兼容需要满足一定条件。
例如:
- 模型使用跨后端兼容的 Keras API;
- 自定义组件具有正确的序列化配置;
- 不依赖目标后端不支持的计算操作;
- 输入张量结构符合要求;
- 相关组件能够正确恢复。
13.2 .keras 文件是否可以跨后端加载
对于使用 Keras 跨后端 API 构建的兼容模型,.keras 文件可以作为跨后端模型复用的载体。
例如,在一个进程中使用 TensorFlow 后端训练并保存模型,再在另一个进程中选择 JAX 后端加载。
但需要明确:
能够跨后端加载模型,不意味着可以保证完全一致的数值结果或无损恢复原始优化器的执行状态。
不同后端可能采用不同的:
- 算子实现;
- 浮点计算顺序;
- 编译策略;
- 随机数生成机制;
- 优化器执行细节;
- 设备内存和数据布局。
因此,跨后端迁移后应重新验证模型输出与训练行为。
13.3 推理产物与后端无关并不是绝对概念
例如,ONNX 通过统一计算图表示降低了应用对 Keras 的依赖。
但实际执行时,ONNX Runtime 仍然需要根据目标设备选择相应的 Execution Provider。
同样,SavedModel 可以不依赖原始 Keras Python 模型类,但仍需要能够执行相关 TensorFlow 计算的运行环境。
因此,应区分三个概念:
模型定义独立性
模型的数学结构能否脱离原始训练代码。
运行时独立性
模型是否需要原始训练框架的完整运行环境。
硬件可移植性
模型能否在不同 CPU、GPU 或其他加速器上执行。
这三个概念并不完全相同。
一个模型可以在结构上独立于 Keras,但仍依赖特定运行时。
也可能能够在多个硬件平台执行,但因为不同平台的算子实现不同而产生数值差异。
十四、模型导出后的数值一致性验证
14.1 为什么必须比较推理输出
模型格式转换通常涉及计算图重建、算子映射或数值类型转换。
即使转换成功,也需要验证导出结果是否与原模型一致。
假设原模型输出为:
[
Y_{\text{keras}}
]
导出模型输出为:
[
Y_{\text{export}}
]
可以计算最大绝对误差:
[
E_{\max}
\max_i
\left|
Y_{\text{keras},i}
Y_{\text{export},i}
\right|
]
也可以计算平均绝对误差:
[
E_{\text{mean}}
\frac{1}{N}
\sum_{i=1}^{N}
\left|
Y_{\text{keras},i}
Y_{\text{export},i}
\right|
]
这些指标可以帮助识别格式转换过程中的数值变化。
但需要注意,不同任务对误差的容忍程度不同。
对于普通回归任务,可能允许一定的浮点误差。
对于分类任务,还需要关注预测类别是否发生变化。
对于某些阈值决策模型,即使数值误差很小,也可能导致最终决策结果改变。
因此,数值一致性验证需要结合实际业务目标进行。
14.2 使用 NumPy 比较输出
例如:
import numpy as np
keras_output = model.predict(
x_test,
verbose=0
)
export_output = ort_session.run(
None,
{
input_name: x_test.astype("float32")
}
)[0]
np.testing.assert_allclose(
keras_output,
export_output,
rtol=1e-4,
atol=1e-5
)
这里使用了:
rtol:相对误差容忍度;atol:绝对误差容忍度。
这些阈值仅作为示例。
正式项目中,应根据模型数值精度、量化策略和业务需求确定适当的容忍范围。
14.3 浮点误差与计算顺序
浮点运算并不满足严格的实数代数性质。
例如:
[
(a+b)+c
]
与:
[
a+(b+c)
]
在有限精度浮点数中可能产生不同结果。
因此,不同运行时即使执行数学等价的计算图,也可能因为运算顺序不同而产生细微误差。
如果模型使用 BF16、FP16 或量化整数运算,这种差异可能进一步扩大。
因此,跨后端验证通常应采用合理的数值容忍度,而不是无条件要求逐位完全一致。
十五、完整实例:训练、保存、恢复与导出
下面通过一个完整示例,展示 Keras 模型从训练到推理部署的基本流程。
为简化依赖关系,使用 TensorFlow 作为 Keras 计算后端。
示例包含:
- 生成数据;
- 构建模型;
- 训练模型;
- 保存
.keras文件; - 重新加载并验证;
- 保存独立权重;
- 导出 SavedModel;
- 导出 ONNX;
- 使用 ONNX Runtime 验证推理结果。
其中,ONNX 导出和运行需要额外安装相应的转换依赖及 ONNX Runtime。
15.1 配置环境
import os
os.environ["KERAS_BACKEND"] = "tensorflow"
import numpy as np
import keras
from keras import layers
from pathlib import Path
keras.utils.set_random_seed(42)
15.2 生成训练数据
rng = np.random.default_rng(42)
x = rng.normal(
size=(3000, 16)
).astype("float32")
true_w = rng.normal(
size=(16, 1)
).astype("float32")
noise = rng.normal(
scale=0.1,
size=(3000, 1)
).astype("float32")
y = x @ true_w + noise
x_train = x[:2400]
y_train = y[:2400]
x_val = x[2400:]
y_val = y[2400:]
这里构建一个包含 16 个输入特征的回归任务。
15.3 构建模型
def build_model():
inputs = keras.Input(
shape=(16,),
name="features"
)
x = layers.Dense(
64,
activation="relu"
)(inputs)
x = layers.Dense(
32,
activation="relu"
)(x)
outputs = layers.Dense(
1,
name="prediction"
)(x)
return keras.Model(
inputs,
outputs
)
实例化:
model = build_model()
配置训练:
model.compile(
optimizer=keras.optimizers.Adam(
learning_rate=1e-3
),
loss="mse",
metrics=["mae"]
)
15.4 执行训练
model.fit(
x_train,
y_train,
validation_data=(x_val, y_val),
batch_size=64,
epochs=20,
verbose=2
)
完成训练后,模型已经拥有经过优化的参数。
15.5 创建保存目录
artifact_dir = Path("artifacts")
artifact_dir.mkdir(
parents=True,
exist_ok=True
)
15.6 保存完整 Keras 模型
model.save(
artifact_dir / "model.keras"
)
重新加载:
restored = keras.saving.load_model(
artifact_dir / "model.keras"
)
验证数值一致性:
x_sample = x_val[:32]
original_output = model.predict(
x_sample,
verbose=0
)
restored_output = restored.predict(
x_sample,
verbose=0
)
np.testing.assert_allclose(
original_output,
restored_output,
rtol=1e-5,
atol=1e-6
)
在相同执行环境下,正常保存与加载的模型应当能够在合理浮点误差范围内保持一致的推理输出。
15.7 保存并加载权重
单独保存:
model.save_weights(
artifact_dir / "model.weights.h5"
)
创建相同结构:
weights_model = build_model()
加载:
weights_model.load_weights(
artifact_dir / "model.weights.h5"
)
再次验证:
weights_output = weights_model.predict(
x_sample,
verbose=0
)
np.testing.assert_allclose(
original_output,
weights_output,
rtol=1e-5,
atol=1e-6
)
需要注意,weights_model 在加载权重后尚未调用 compile(),但这并不妨碍它执行前向推理。
15.8 导出 SavedModel
model.export(
artifact_dir / "saved_model",
format="tf_saved_model"
)
使用 TensorFlow 加载:
import tensorflow as tf
serving_model = tf.saved_model.load(
str(artifact_dir / "saved_model")
)
执行推理:
savedmodel_output = serving_model.serve(
tf.convert_to_tensor(x_sample)
)
转换为 NumPy:
savedmodel_output = savedmodel_output.numpy()
验证:
np.testing.assert_allclose(
original_output,
savedmodel_output,
rtol=1e-4,
atol=1e-5
)
15.9 导出 ONNX
model.export(
artifact_dir / "model.onnx",
format="onnx"
)
该步骤要求当前模型及所安装的 ONNX 转换依赖满足导出要求。
成功导出后:
import onnxruntime as ort
session = ort.InferenceSession(
str(artifact_dir / "model.onnx")
)
获取输入名称:
input_name = session.get_inputs()[0].name
执行推理:
onnx_output = session.run(
None,
{
input_name: x_sample
}
)[0]
验证:
np.testing.assert_allclose(
original_output,
onnx_output,
rtol=1e-4,
atol=1e-5
)
这一步是模型导出流程中非常重要的验证环节。
因为它不仅验证文件已经成功生成,也验证目标运行时能够执行模型,并产生符合预期的输出。
15.10 最终文件结构
整个示例可能形成:
artifacts/
│
├── model.keras
│
├── model.weights.h5
│
├── model.onnx
│
└── saved_model/
├── saved_model.pb
├── variables/
└── assets/
其中:
model.keras:用于完整 Keras 模型恢复;model.weights.h5:用于权重恢复;model.onnx:用于 ONNX 推理运行时;saved_model/:用于 TensorFlow 推理运行时。
同一个训练结果由此可以服务于不同的后续需求。
十六、模型部署中的性能问题
16.1 训练性能与推理性能并不相同
在上一篇文章中,我们已经讨论了训练吞吐量、MFU 和设备利用率等指标。
但推理部署具有不同的性能目标。
训练通常关注单位时间内能够处理多少样本,并需要执行前向传播、反向传播和参数更新。
推理通常只需要执行前向计算。
其主要性能指标包括:
| 指标 | 含义 |
|---|---|
| Latency | 单次推理请求的延迟 |
| Throughput | 单位时间内处理的请求或样本数量 |
| P50 Latency | 中位数推理延迟 |
| P95 / P99 Latency | 高分位推理延迟 |
| Peak Memory | 推理时峰值内存使用 |
| Model Load Time | 模型加载耗时 |
| Cold Start Time | 首次启动和准备推理的耗时 |
需要注意,推理吞吐量与推理延迟之间可能存在权衡。
例如,增加推理 Batch Size 往往能够提高设备吞吐量,但也可能增加单个请求等待批次构建的时间。
因此,不同应用可能具有不同的优化目标。
实时预测系统通常更关注尾延迟。
离线批量推理系统则可能更加关注总体吞吐量。
16.2 模型量化
模型量化(Quantization)是常见的推理优化技术。
其主要思想,是使用较低位宽的数值表示模型权重或激活。
例如,可以将部分 FP32 权重转换为 INT8。
从存储角度看,理想情况下:
[
M_{\text{INT8}}
\approx
\frac{1}{4}M_{\text{FP32}}
]
但实际文件和运行内存还包括量化比例、零点以及其他元信息,因此总占用不一定严格减少至四分之一。
量化可能降低模型体积和内存带宽需求,并在具备相应硬件支持的情况下提高推理性能。
但量化也可能引入额外数值误差。
因此,需要在模型大小、执行速度和预测质量之间进行权衡。
16.3 推理优化与格式转换
需要区分:
模型格式转换
将模型变为目标运行时能够加载的表示形式。
模型推理优化
通过量化、算子融合、计算图优化和硬件专用执行等方式提高性能。
两者可以同时发生,但并不是同一个过程。
例如,导出 ONNX 只是使模型获得 ONNX 格式。
是否使用特定硬件加速、是否启用图优化,以及是否执行量化,还取决于 ONNX Runtime 的配置和后续处理流程。
因此,不能简单将“成功导出”理解为“已经获得最优推理性能”。
十七、模型版本管理与部署契约
17.1 模型文件并不包含全部部署信息
即使某个 ONNX 模型可以独立执行,也不意味着应用系统已经具备正确使用它的全部信息。
例如,一个输入形状为:
[
(B,16)
]
的模型,并不能仅凭张量形状判断这 16 个特征分别代表什么。
也无法自动判断:
- 特征顺序;
- 特征单位;
- 缺失值处理方式;
- 标准化参数;
- 输入数据时间窗口;
- 输出标签含义;
- 输出阈值;
- 模型版本与数据版本。
这些信息属于模型的部署契约。
17.2 输入输出契约
一个推理系统应当明确规定:
[
X_{\text{input}}
\rightarrow
\text{Preprocessing}
\rightarrow
f_\theta
\rightarrow
\text{Postprocessing}
\rightarrow
Y_{\text{output}}
]
其中,预处理和后处理不一定全部包含在导出的计算图中。
例如,分类模型输出可能是 Logits。
如果业务系统要求概率,则可能需要额外执行 Sigmoid 或 Softmax。
如果业务系统要求离散类别,则可能还需要执行阈值判断或 Argmax。
因此,部署接口不能只描述模型的张量形状,还应说明输出的数学含义。
17.3 模型元信息
实际项目中,通常需要为模型产物保存元信息。
例如:
{
"model_name": "regression_model",
"model_version": "1.0.0",
"format": "onnx",
"input_name": "features",
"input_shape": [null, 16],
"input_dtype": "float32",
"output_name": "prediction",
"output_dtype": "float32",
"preprocessing_version": "1.0.0"
}
这里的 JSON 仅是一个示例,不属于 Keras 自动要求的标准部署元数据格式。
其中:
"input_shape": [null, 16]
表示输入具有动态批大小和固定的 16 维特征。
需要注意,元信息中的名称和形状应当以实际导出模型为准,不能简单假定 Keras 原始层名一定会成为 ONNX 的最终输入输出名称。
此外,正式实验通常还应记录:
- Keras 版本;
- 计算后端版本;
- 导出工具版本;
- 数据集版本;
- 模型配置;
- 权重文件校验值;
- 代码版本;
- 测试结果。
这些信息有助于建立模型产物与训练实验之间的可追溯关系。
17.4 模型更新与回滚
生产环境中的模型通常需要不断更新。
因此,部署系统不应只维护一个不断被覆盖的模型文件。
更合理的方式是为不同版本建立独立产物。
例如:
models/
│
├── v1/
│ ├── model.onnx
│ └── metadata.json
│
├── v2/
│ ├── model.onnx
│ └── metadata.json
│
└── v3/
├── model.onnx
└── metadata.json
这样可以保留历史模型,并支持在新版本出现问题时回滚。
需要注意,Keras 自身主要负责模型定义、保存和导出。
模型版本路由、流量切换、回滚与在线监控等部署管理能力,通常需要由外部系统实现。
十八、模型持久化与部署中的常见问题
18.1 将 .keras 视为通用推理格式
.keras 是 Keras 的原生模型格式。
它非常适合恢复模型结构和状态,但并不是所有推理引擎都支持直接加载。
因此,对于独立推理服务,应根据目标运行时选择相应的导出格式。
18.2 将 ONNX 视为完整训练检查点
ONNX 主要用于描述计算图和模型参数。
它并不以恢复完整 Keras 训练过程为主要目标。
因此,不能假定保存 ONNX 后,就能够恢复原来的 Adam 优化器状态、训练 Epoch 或数据迭代进度。
如果需要继续训练,应保留适当的训练检查点。
18.3 忽视自定义 Python 对象
Keras 原生文件不会自动保存所有自定义组件的源代码。
如果模型依赖自定义 Layer,但加载环境没有相应类定义,就可能出现反序列化错误。
因此,自定义组件应实现正确的配置序列化,并在模型恢复环境中提供对应代码。
18.4 关闭安全反序列化限制
Keras 的 load_model() 默认提供:
safe_mode=True
该参数用于限制某些不安全的反序列化行为。
例如:
keras.saving.load_model(
"model.keras",
safe_mode=False
)
可能允许执行存在安全风险的反序列化操作。
因此,不应为了绕过加载错误,就在不可信模型文件上直接关闭安全限制。
需要注意,safe_mode=True 本身也不应被视为能够安全执行任意不可信模型文件的完整沙箱。
正式部署系统仍然需要控制模型来源、文件完整性和运行权限。
18.5 忽视预处理一致性
模型训练时使用的标准化规则、特征顺序或词表,必须在推理阶段保持一致。
如果训练时使用:
[
z=\frac{x-\mu_{\text{train}}}{\sigma_{\text{train}}}
]
那么推理时通常也应使用相同的:
[
\mu_{\text{train}},
\sigma_{\text{train}}
]
而不是根据单个推理请求重新估计统计量。
否则,模型实际上接收到的是不同的数据表示。
这一问题可能导致预测结果异常,即使模型文件本身没有任何错误。
18.6 只检查导出成功,不检查数值结果
模型导出成功,只表示转换工具生成了目标文件。
它不能证明所有计算行为完全正确。
因此,至少应在一组具有代表性的输入上,比较导出前后模型的输出。
对于动态形状、多输入模型和特殊算子,还应覆盖不同输入条件。
18.7 假定跨后端输出完全一致
TensorFlow、JAX 和 PyTorch 的计算实现可能存在差异。
即使模型使用相同权重,也可能因浮点运算顺序或底层算子不同而产生误差。
因此,跨后端迁移应采用合理的误差容忍度,并验证最终任务指标。
十九、模型持久化与部署系统的架构意义
从整个 Keras 3 框架来看,模型持久化与部署系统承担着连接模型开发环境和实际应用环境的职责。
可以将其概括为:
Keras 训练环境
│
▼
Keras Model
│
┌──────────┴──────────┐
│ │
▼ ▼
模型持久化 推理导出
│ │
┌───────┴────────┐ ┌───────┴────────┐
▼ ▼ ▼ ▼
完整模型 权重文件 SavedModel ONNX 等
│ │ │ │
▼ ▼ ▼ ▼
模型恢复 参数恢复 TensorFlow 独立推理
│ │ Runtime Runtime
│ │ │ │
└────────┬───────┘ └───────┬────────┘
│ │
▼ ▼
继续训练 生产推理
模型分析 应用集成
其中,训练检查点可以被理解为模型持久化的进一步扩展,它还需要保存恢复训练所必需的相关状态。
从软件架构角度看,这一体系具有三个重要意义。
第一,模型定义与模型状态分离。
模型结构可以通过配置重新构建,参数则通过独立的状态保存机制恢复。
这使得模型复制、迁移学习和参数管理具有清晰的职责边界。
第二,训练环境与推理环境分离。
模型可以在 JAX、TensorFlow 或 PyTorch 等训练环境中完成优化,再根据目标运行时导出适当的推理产物。
这使生产应用不必无条件依赖原始训练程序。
第三,模型生命周期能够被独立管理。
训练检查点、最佳模型、推理产物和部署元信息可以分别保存,并建立明确的版本关系。
这使训练恢复、模型选择和生产部署能够采用不同的策略,而不需要由单一文件承担全部职责。
但这种分离也带来新的工程要求。
例如,不同格式之间需要进行数值一致性验证,模型版本需要与数据预处理版本关联,推理运行时需要支持相应的算子和数据类型。
因此,模型导出不应被视为训练完成后的一个简单文件写入操作,而应被视为独立的模型交付与验证过程。
结语
Keras 3 的模型持久化与部署系统,主要解决模型训练结果如何保存、恢复和迁移到实际推理环境的问题。
其核心机制可以分为三个层次。
第一层是模型序列化,通过 .keras 格式保存模型结构、参数和相关训练状态,使模型能够在 Keras 环境中重新构建。
第二层是状态管理,通过权重文件、ModelCheckpoint、BackupAndRestore 和 OrbaxCheckpoint 等机制,实现参数迁移与训练恢复。
第三层是推理导出,通过 model.export() 和 ExportArchive 等接口,将模型转换为 SavedModel、ONNX、OpenVINO、LiteRT 或 PyTorch ExportedProgram 等产物,使其能够在不同推理环境中运行。
这三个层次解决的是不同问题。
模型序列化主要关注 Keras 对象的恢复。
训练检查点主要关注计算状态的延续。
推理导出则主要关注模型计算函数在目标运行时中的可执行性。
因此,实际项目通常不应该只选择一种文件格式承担所有职责。
更合理的方式,是在训练结束后保留完整的 Keras 模型或相应检查点,同时生成符合部署环境要求的推理产物,并对导出结果进行独立验证。
从整个深度学习系统来看,训练完成后的模型不仅是一组已经学习到的参数,也是一项需要被持续管理的软件资产。
它具有明确的结构、版本、输入输出契约、数值行为和运行环境要求。
只有将这些因素纳入模型交付流程,才能使训练结果真正转化为可靠、可复现和可维护的推理系统。
至此,我们已经介绍了 Keras 3 的基础计算层、神经网络层、模型构建层、训练系统、数据处理系统、训练控制与监控系统、高性能计算系统,以及模型持久化与部署系统。
这八个部分共同构成了一个从模型定义、参数优化到推理交付的完整技术链路。
但 Keras 3 的能力并不局限于这些核心组件。
在实际深度学习开发中,开发者还可能需要使用预训练模型、超参数搜索、模型调试工具,以及其他建立在 Keras 之上的扩展库。
在下一篇文章中,我们将进一步介绍 Keras 的工具与扩展生态,重点分析 Keras Applications、KerasHub、KerasTuner 和相关工具在整体框架中的定位,以及如何在保持项目架构清晰的前提下合理使用这些扩展组件。
- 点赞
- 收藏
- 关注作者
评论(0)