3.8 模型保存与加载:把排程图存档 本节摘要:训练成果要交接给两个下游:继续训练的自己与推理上线的服务。本节讲清两种存法(只存权重、存全模型)与两种格式(Keras 原生格式、SavedModel 目录)的能力边界:权重文件小而快但必须先有结构代码,全模型存档自包含但耦合框架版本。选型的原则是"交付给谁"——给训练续档存权重,给部署与跨语言消费存 SavedModel,5.3 节的部署线全部以它为入口。 学习目标 阅读完本节,你应当能够: 区分权重存档与全模型存档的能力差异,按下游需求选对存法; 保存与加载 Keras 原生格式与 SavedModel 目录; 解释加载后必须重新 compile 才能继续训练的原因; 检查一个 SavedModel 目录的内容并核对签名。
本节摘要:训练成果要交接给两个下游:继续训练的自己与推理上线的服务。本节讲清两种存法(只存权重、存全模型)与两种格式(Keras 原生格式、SavedModel 目录)的能力边界:权重文件小而快但必须先有结构代码,全模型存档自包含但耦合框架版本。选型的原则是"交付给谁"——给训练续档存权重,给部署与跨语言消费存 SavedModel,5.3 节的部署线全部以它为入口。
阅读完本节,你应当能够:
存档的粒度有两条线。只存权重:把所有 Variable 的数值序列化,体积小、速度快,但加载端必须先有同结构的模型代码——适合"结构由代码定义、数值单独交接"的内部协作。存全模型:结构、权重、优化器状态、compile 配置一起打包,加载端一行 restore,不需要任何建模代码——适合交接给没有源码的一方,或推上部署线。
import tensorflow as tf import numpy as np from sklearn.datasets import fetch_california_housing housing = fetch_california_housing() X, y = housing.data.astype("float32"), housing.target.astype("float32") m = tf.keras.Sequential([tf.keras.layers.Input(shape=(8,)), tf.keras.layers.Dense(64, activation="relu"), tf.keras.layers.Dense(1)], name="house_net") m.compile(optimizer="adam", loss="mse") m.fit(X[:6000], y[:6000], epochs=3, batch_size=64, verbose=0) # 存法一:只存权重 m.save_weights("house.weights.h5") print("weights saved") # 输出:weights saved # 单个文件,只有参数数值 # 存法二:存全模型(Keras 原生格式) m.save("house_full.keras") print("full model saved") # 输出:full model saved # 单个 .keras 文件:结构加权重加优化器状态加 compile 配置
加载权重要求加载端先构造出结构完全一致的模型——层的类型、顺序、形状都要对上,然后 load_weights 按名字与顺序匹配。加载全模型则一行搞定,连 compile 状态都在:
# 姿势一:先建结构,再灌权重 reborn = tf.keras.Sequential([tf.keras.layers.Input(shape=(8,)), tf.keras.layers.Dense(64, activation="relu"), tf.keras.layers.Dense(1)]) reborn.load_weights("house.weights.h5") loss = reborn.evaluate(X[9000:10000], y[9000:10000], verbose=0) print(f"weights-only loss: {loss:.3f}") # 输出示例:weights-only loss: 0.581 # 结构对不上时这里直接报错——按形状逐层核对 # 姿势二:全模型恢复 full = tf.keras.models.load_model("house_full.keras") loss2 = full.evaluate(X[9000:10000], y[9000:10000], verbose=0) print(f"full-model loss: {loss2:.3f}") # 输出示例:full-model loss: 0.581 # 两路数值一致——存档的正确性就体现在这里
继续训练的关键动作:只存权重的档要重新 compile。权重文件里没有优化器状态与损失配置,加载后的模型对"怎么训练"一无所知;全模型档虽带 compile 配置,跨版本恢复后也建议重编译一次以确保行为明确。
reborn.compile(optimizer=tf.keras.optimizers.Adam(1e-4), loss="mse") h = reborn.fit(X[:6000], y[:6000], epochs=1, batch_size=64, verbose=0) print("recompiled and continued") # 输出:recompiled and continued # 注意 Adam 的动量状态是新的——从头热身,损失曲线可能先小跳一下
SavedModel 是目录格式:结构序列化、权重存成检查点、外加一份"签名表"声明可调用的函数与输入输出规格。它是 TensorFlow Serving、Lite 转换、tfjs 转换共同接受的入口格式——5.3 节的三条部署线全部从它出发。
# 导出 SavedModel 目录 m.export("serving_dir/house/1") print("savedmodel exported") # 输出:savedmodel exported # 目录内含结构、权重与默认服务签名 # 用 TFSMLayer 从 SavedModel 恢复为可推理层(结构与配置不回迁) reloaded = tf.keras.layers.TFSMLayer("serving_dir/house/1", call_endpoint="serving_default") sample = tf.constant(X[9000:9002]) out = reloaded(sample) print(out) # 输出示例: # {'output_0': <tf.Tensor: shape=(2, 1), dtype=float32, numpy=...>} # 返回是字典——服务签名的键名,部署端按名取值
SavedModel 与 .keras 全模型档的分工:前者是面向部署的"服务合同",签名固定、跨语言可调;后者是面向训练工作流的"工作台快照",连优化器状态都齐。存档文件名与目录只作为程序内的字符串字面量使用,部署阶段的规范做法是把导出目录交给版本化管理,按版本号递增。
| 下游 | 推荐存法 | 理由 |
|---|---|---|
| 断点续训 | checkpoint 存权重 | 需要重新 compile,体积小 |
| 交接给同事 | 全模型 .keras | 对方免源码一行恢复 |
| 上 Serving | SavedModel 目录 | 签名固定,服务化标准 |
| 转 Lite 或 tfjs | SavedModel 目录 | 转换器只认它 |
⚠️ 常见坑:给 Sequential 模型 load_weights 报形状不匹配,多半是两个 Sequential 的 Input 形状或层数不一致——权重按层序匹配,少一层或多一层全盘错位。给关键层起 name 再存档,加载端核对名单即可提前暴露。
💡 关键直觉:存档格式是为下游选的,不是为习惯选的。先问"这份档交给谁、在什么环境打开",再决定只存权重、全模型,还是导出 SavedModel。
第 3 章到站。第 4 章把装配对象换成各家族的网络结构。