1. 模型保存:一个看似简单却暗藏玄机的操作
刚入行做机器学习项目的时候,我总觉得模型训练是最核心、最耗时的部分,至于保存模型,不就是一行
model.save()
的事吗?直到有一次,我花了三天三夜训练好的一个图像分类模型,因为一个简单的保存操作失误,导致整个模型文件损坏,无法加载,更别提部署了。那一刻我才深刻体会到,模型保存与加载绝非小事,它直接关系到你数日甚至数周劳动成果的“生死存亡”。
今天,我们就来彻底聊聊在 Python 中,如何将训练好的模型妥善地保存下来。这里的“妥善”包含两层核心诉求:第一,保存下来的模型要能够被稳定、正确地加载并使用,进行预测或推理;第二,更进阶一点,保存的模型要能保留完整的训练状态,以便未来可以无缝地继续训练,而不是从头开始。这不仅仅是调用一个 API,更涉及到对框架机制、文件格式和工程实践的理解。无论你用的是 TensorFlow、PyTorch 还是 Scikit-learn,背后的逻辑和踩坑点都值得细细品味。
2. 为什么不能简单地“存参数”?理解模型保存的层次
在深入代码之前,我们必须先搞清楚,当我们说“保存模型”时,我们到底在保存什么。新手最容易犯的错误就是只保存了模型的权重参数,这就像只保存了一本书的正文内容,却丢掉了目录、章节标题和页码。当你想接着读或者查找特定内容时,会非常困难。
2.1 模型架构 vs. 模型参数
这是最根本的区分。
模型架构
定义了模型的“骨架”或“蓝图”,例如这是一个有几层的神经网络,每层是什么类型(全连接层、卷积层),层与层之间如何连接。在代码中,这通常体现为你定义的类(如
class MyModel(nn.Module):
)或顺序构建的层。
模型参数
(或称权重和偏置)则是这个骨架上的“血肉”,是模型通过训练从数据中学到的具体数值。只保存参数,你必须有完全相同的架构代码才能将其加载回去。
2.2 优化器状态与训练元数据
当你需要“继续训练”时,这就至关重要了。优化器(如 SGD、Adam)内部维护着一些状态变量,例如动量(Momentum)的缓存、自适应学习率(如 Adam 中的 m 和 v 向量)。如果只保存模型参数,优化器状态会被重置,继续训练时相当于从一个“冷启动”的状态开始,可能会破坏训练动态,导致损失震荡甚至发散。此外,当前的 epoch 数、学习率调度器的状态等,都属于应被保存的训练元数据。
2.3 词汇表、归一化参数与自定义代码
对于处理序列数据(如 NLP)的模型,词到索引的映射(词汇表)必须和模型一起保存。对于预处理步骤,比如特征标准化(StandardScaler)的均值(mean_)和方差(scale_)也必须保存,以确保新数据在预测时得到与训练时一致的变换。更复杂的情况是,如果你的模型包含自定义的层、损失函数或指标,这些代码本身也需要以某种形式被记录或打包,否则加载时 Python 会找不到对应的类定义。
理解了这些层次,我们就能明白,不同的保存方法实际上是在不同层次上做文章。接下来,我们将针对三大主流框架,看看它们如何应对这些需求。
3. PyTorch 的保存哲学:极致的灵活性与明确的责任划分
PyTorch 的设计哲学是“显式优于隐式”,这在模型保存上体现得淋漓尽致。它提供了基础的工具,但如何组织、保存哪些内容,很大程度上交给了开发者。这带来了灵活性,也要求更高的认知。
3.1 基础保存与加载:
state_dict
的核心地位
PyTorch 推荐的标准方式是保存模型的
state_dict
。这是一个 Python 字典对象,它将模型每一层映射到其参数张量。注意,它不包含模型架构本身。
import torch
import torch.nn as nn
# 假设我们有一个简单的模型
class SimpleNN(nn.Module):
def __init__(self):
super().__init__()
self.fc1 = nn.Linear(10, 5)
self.fc2 = nn.Linear(5, 1)
def forward(self, x):
return self.fc2(torch.relu(self.fc1(x)))
model = SimpleNN()
# ... 训练过程 ...
# 保存 state_dict
torch.save(model.state_dict(), 'model_weights.pth')
# 要加载它,你必须先重新实例化完全相同的模型架构
loaded_model = SimpleNN()
loaded_model.load_state_dict(torch.load('model_weights.pth'))
loaded_model.eval() # 切换到评估模式
注意 :
torch.save默认使用 Python 的 pickle 序列化。这意味着被保存的对象(如state_dict中的自定义类)必须可以在加载时的环境中被导入。这也是潜在的安全风险,切勿加载来源不可信的.pth文件。
3.2 实现“继续训练”:保存检查点(Checkpoint)
为了实现继续训练,我们需要构建一个检查点字典,打包所有必要状态。
# 在训练循环的某个epoch结束时保存检查点
checkpoint = {
'epoch': epoch,
'model_state_dict': model.state_dict(),
'optimizer_state_dict': optimizer.state_dict(),
'loss': loss,
# 还可以保存学习率调度器状态
'scheduler_state_dict': scheduler.state_dict() if scheduler else None,
# 保存其他自定义信息,如最佳验证精度
'best_val_acc': best_val_acc,
}
torch.save(checkpoint, f'checkpoint_epoch_{epoch}.pth')
加载检查点并继续训练的代码如下:
# 初始化模型、优化器(架构必须与保存时一致)
model = SimpleNN()
optimizer = torch.optim.Adam(model.parameters())
scheduler = torch.optim.lr_scheduler.StepLR(optimizer, step_size=30)
start_epoch = 0
best_val_acc = 0.0
# 加载检查点
checkpoint = torch.load('checkpoint_epoch_10.pth')
model.load_state_dict(checkpoint['model_state_dict'])
optimizer.load_state_dict(checkpoint['optimizer_state_dict'])
if checkpoint['scheduler_state_dict']:
scheduler.load_state_dict(checkpoint['scheduler_state_dict'])
start_epoch = checkpoint['epoch'] + 1 # 从下一轮开始
best_val_acc = checkpoint['best_val_acc']
loss = checkpoint['loss']
model.train() # 切换到训练模式
# 继续训练循环...
3.3 陷阱与最佳实践
-
设备映射问题
:如果你在 GPU 上训练并保存,然后在只有 CPU 的环境中加载,需要设置
map_location参数:torch.load('model.pth', map_location=torch.device('cpu'))。反之亦然。 -
自定义类的序列化
:如果模型包含自定义的
nn.Module子类,确保该类定义在加载脚本中可用。一种更稳健的做法是将类定义放在独立的模块文件中,然后导入。 -
保存整个模型(不推荐)
:你可以使用
torch.save(model, 'entire_model.pth')。这会通过 pickle 保存整个模型对象(包括架构)。但这种方法非常脆弱,强烈不推荐用于长期存储或分享,因为它与保存时特定的文件路径和代码环境紧密耦合,极易在加载时失败。 -
版本兼容性
:PyTorch 的
state_dict在不同版本间通常保持稳定,但并非绝对。跨大版本加载时需谨慎测试。
4. TensorFlow/Keras 的保存之道:一体化方案与格式选择
TensorFlow 2.x 及其高阶 API Keras 提供了更多“开箱即用”的一体化保存方案,对新手更友好,但同时也支持细粒度控制。
4.1 SavedModel 格式:工业级部署的标准
这是 TensorFlow 推荐的格式,适用于部署到 TensorFlow Serving、TFLite、TensorFlow.js 等平台。它保存了完整的计算图、权重以及必要的元数据。
import tensorflow as tf
model = tf.keras.Sequential([...]) # 构建你的模型
# ... 编译和训练模型 ...
# 保存为 SavedModel 格式
model.save('my_saved_model') # 注意,这是一个文件夹,不是单个文件
# 加载
loaded_model = tf.keras.models.load_model('my_saved_model')
# 加载后可以直接预测
predictions = loaded_model.predict(new_data)
SavedModel 文件夹内部包含
saved_model.pb
(计算图定义)和
variables/
目录(权重)。这种方式加载的模型是“即用型”的,无需重新编译。
4.2 Keras
.h5
格式与继续训练
Keras 也支持传统的 HDF5 格式(
.h5
或
.keras
后缀),它可以保存模型架构、权重和优化器状态。
# 保存模型(包含优化器状态,以便继续训练)
model.save('my_model.h5') # 或 'my_model.keras'
# 加载模型(包含所有训练状态)
loaded_model = tf.keras.models.load_model('my_model.h5')
# 此时模型处于训练结束时的状态,可以直接继续训练
loaded_model.fit(more_training_data, epochs=5)
注意 :
.h5格式在保存自定义层、损失或指标时,有时需要提供custom_objects参数来指定自定义类。而较新的.keras格式(基于 Keras v3 的保存格式)在这方面通常更健壮。
4.3 仅保存权重与仅保存架构
有时我们需要将架构和权重分开管理。
# 仅保存权重
model.save_weights('my_model_weights.h5')
# 仅保存架构为 JSON 文件
model_json = model.to_json()
with open('model_architecture.json', 'w') as json_file:
json_file.write(model_json)
# 从JSON加载架构,然后加载权重
with open('model_architecture.json', 'r') as json_file:
loaded_model_json = json_file.read()
new_model = tf.keras.models.model_from_json(loaded_model_json)
new_model.load_weights('my_model_weights.h5')
new_model.compile(...) # 需要重新编译优化器、损失函数等
4.4 TensorFlow 的检查点与
tf.train.Checkpoint
对于更底层的控制,或者需要保存非 Keras 对象(如自定义优化器)的状态,可以使用 TensorFlow 的检查点 API。
# 创建检查点对象并关联要保存的对象
checkpoint = tf.train.Checkpoint(optimizer=optimizer, model=model)
# 保存
checkpoint.save('my_training_checkpoints/ckpt')
# 加载最新检查点
latest_checkpoint = tf.train.latest_checkpoint('my_training_checkpoints/')
checkpoint.restore(latest_checkpoint)
# 此时 model 和 optimizer 的状态都已恢复
这种方式非常灵活,可以保存任何
tf.train.Checkpointable
对象的状态,是构建复杂训练循环时的利器。
5. Scikit-learn 及其他传统机器学习库:使用
pickle
或
joblib
对于 Scikit-learn、XGBoost、LightGBM 等模型,标准做法是使用 Python 的
pickle
模块或其优化版本
joblib
。
5.1 使用
joblib
的优势
joblib
在保存包含大型 numpy 数组的对象(如大多数机器学习模型)时,通常比
pickle
更高效。
from sklearn.ensemble import RandomForestClassifier
from sklearn.externals import joblib # 注意:新版本中需 `import joblib`
model = RandomForestClassifier(n_estimators=100)
model.fit(X_train, y_train)
# 保存模型
joblib.dump(model, 'random_forest_model.joblib')
# 加载模型
loaded_model = joblib.load('random_forest_model.joblib')
predictions = loaded_model.predict(X_test)
5.2 保存完整的机器学习流水线
在实际项目中,我们很少只保存一个孤立的模型。通常需要保存包括数据预处理(如标准化、编码)在内的完整流水线。
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler
from sklearn.svm import SVC
# 创建一个包含预处理和分类器的流水线
pipeline = Pipeline([
('scaler', StandardScaler()),
('svc', SVC(probability=True))
])
pipeline.fit(X_train, y_train)
# 保存整个流水线
joblib.dump(pipeline, 'full_pipeline.joblib')
# 加载时,预处理和预测一步到位
loaded_pipeline = joblib.load('full_pipeline.joblib')
# 直接对新数据(原始数据)进行预测,流水线内部会自动进行标准化
predictions = loaded_pipeline.predict(new_raw_data)
这是非常重要且实用的模式,确保了训练和预测阶段数据变换的一致性。
5.3 关于“继续训练”的局限性
需要明确的是,Scikit-learn 的许多模型(如
RandomForestClassifier
,
SVM
)本身并不直接支持“增量学习”或“在线学习”。像
partial_fit
这样的方法只适用于部分算法(如
SGDClassifier
)。对于不支持增量学习的模型,所谓的“继续训练”通常意味着用新旧数据合并后的全集重新训练一个新模型。因此,保存这类模型主要是为了部署和推理,而非中断后继续训练。
6. 工程化实践:超越单文件保存
当项目从实验走向生产,从个人开发走向团队协作时,简单的文件保存就不够用了。我们需要考虑版本化、元数据和可复现性。
6.1 模型版本管理与实验追踪
手动命名文件如
model_v1_final_final.pth
是灾难的开始。应该使用专门的工具:
-
MLflow
:其
mlflow.pytorch.log_model()或mlflow.sklearn.log_model()不仅能保存模型文件,还能自动记录代码版本、参数、指标和环境依赖,形成一个完整的“模型运行”记录。 -
Weights & Biases (W&B)
:类似地,
wandb.log_artifact()可以将模型文件作为制品保存,并与训练实验关联。 - DVC (Data Version Control) :可以将模型文件视为数据,用 Git 管理其元信息,而将实际的大文件存储到云存储(S3, GCS等),实现模型的版本控制。
6.2 模型卡片与元数据
保存模型文件的同时,应该创建一个简单的元数据文件(如
meta.json
),记录:
{
"model_type": "BERTForSequenceClassification",
"framework": "pytorch",
"framework_version": "2.1.0",
"training_dataset": "sst-2",
"training_script_hash": "abc123...",
"author": "your_name",
"created_date": "2023-10-27",
"performance_metrics": {"accuracy": 0.92, "f1": 0.91},
"preprocessing_steps": "tokenization with bert-base-uncased tokenizer"
}
这能极大提高模型的可维护性和可理解性。
6.3 容器化与环境固化
最彻底的保存是保存整个运行环境。使用 Docker 将模型、推理代码和所有依赖项打包成一个镜像。这确保了模型在任何地方都能以完全相同的方式运行,彻底解决了“在我机器上好好的”这个问题。对于 PyTorch 或 TensorFlow 模型,可以结合 TorchServe 或 TensorFlow Serving 的 Docker 镜像来构建标准的模型服务容器。
7. 常见陷阱排查与实战心得
根据我多年的踩坑经验,这里有几个高频问题及其解决方案:
-
“模型加载后预测结果全是乱码或一致值”
-
可能原因A
:忘记调用
model.eval()(PyTorch)或忘记设置training=False(某些框架的自定义层)。这会导致 Dropout、BatchNorm 等层在预测时仍处于训练模式,引入随机性。 -
可能原因B
:数据预处理不一致。加载模型进行预测时,输入数据必须经过与训练时完全相同的归一化、裁剪、编码等操作。务必保存预处理器(如 Scikit-learn 的
StandardScaler)。 - 排查 :用训练集中的一个 batch 重新过一遍模型,对比加载前后输出是否一致。从数据端开始逐层检查。
-
可能原因A
:忘记调用
-
“AttributeError: Can‘t get attribute ‘MyCustomLayer’...”
-
原因
:PyTorch 保存整个模型(
torch.save(model, ...))时,pickle 需要能找到类定义。如果类定义在 Notebook 或临时脚本中,加载时就会失败。 -
解决
:始终坚持保存
state_dict。将自定义层、损失函数等代码放在项目模块中(如models/layers.py),通过 import 使用。
-
原因
:PyTorch 保存整个模型(
-
“继续训练时损失突然飙升或震荡”
- 可能原因 :优化器状态未正确保存/加载。特别是使用 Adam 等带有动量的优化器,其内部状态对训练轨迹影响很大。
-
检查
:确认检查点字典中包含了
optimizer_state_dict,并且加载后成功恢复了状态。可以打印优化器状态中的某个参数(如optimizer.param_groups[0]['momentum_buffer'])在保存前后的值来验证。
-
模型文件过大
-
对策
:对于 PyTorch,可以考虑在保存前将模型权重转换为半精度(FP16):
torch.save(model.half().state_dict(), ...)。加载时再转换回来。对于部署,可以使用框架提供的工具(如 TensorRT, ONNX)对模型进行剪枝、量化,显著减少体积和提升推理速度。
-
对策
:对于 PyTorch,可以考虑在保存前将模型权重转换为半精度(FP16):
-
安全警告:永远不要
torch.load或pickle.load来源不可信的文件- 原因 :反序列化过程可以执行任意代码,这是严重的安全漏洞。
- 最佳实践 :只加载自己或可信来源生成的模型文件。考虑使用更安全的序列化格式(如 ONNX, Safetensors),或从安全的模型仓库加载。
模型保存是机器学习工程化的基石。它连接了实验与部署,沟通了研发与生产。花时间建立一套规范、可靠的模型保存与加载流程,其长期回报远大于在模型损坏时付出的调试成本。从今天起,像重视训练一样重视你的模型保存操作吧。
403



被折叠的 条评论
为什么被折叠?



