第十七章:AI模型的“存档”艺术:揭秘.pt, .ckpt, .safetensors的江湖恩怨

AI保存格式

  • 前言:训练的终点,是"存档"的开始
  • [第一章:PyTorch的"传统艺能"------ torch.save 与 Pickle 协议](#第一章:PyTorch的“传统艺能”—— torch.save 与 Pickle 协议)
    • [1.1 两种存档策略:只存"灵魂" vs. "灵魂+躯体"一起存](#1.1 两种存档策略:只存“灵魂” vs. “灵魂+躯体”一起存)
    • [1.2 用代码实现两种存档与读档方式](#1.2 用代码实现两种存档与读档方式)
    • [1.3 【高能预警】Pickle的"阿喀琉斯之踵":恐怖的"代码注入"风险](#1.3 【高能预警】Pickle的“阿喀琉斯之踵”:恐怖的“代码注入”风险)
  • [第二章:新王登基 ------ safetensors 的崛起](#第二章:新王登基 —— safetensors 的崛起)
    • [2.1 为什么需要safetensors?------ 安全、快速、跨平台](#2.1 为什么需要safetensors?—— 安全、快速、跨平台)
    • [2.2 safetensors 的核心思想:数据与代码的"彻底分离"](#2.2 safetensors 的核心思想:数据与代码的“彻底分离”)
    • [2.3 用Hugging Face safetensors库进行安全的模型存取](#2.3 用Hugging Face safetensors库进行安全的模型存取)
  • [第三章:江湖别传 ------ .ckpt 与 .pth 的故事](#第三章:江湖别传 —— .ckpt 与 .pth 的故事)
    • [3.1 .ckpt (Checkpoint):不止是模型,更是"训练现场"的快照](#3.1 .ckpt (Checkpoint):不止是模型,更是“训练现场”的快照)
    • [3.2 .pth (PyTorch):一个"约定俗成"的旧爱](#3.2 .pth (PyTorch):一个“约定俗成”的旧爱)
  • [一张图看懂 三大主流格式的核心对比](#一张图看懂 三大主流格式的核心对比)
  • [Hugging Face Hub 的"自动转换":为什么你下载的模型越来越多是.safetensors?](#Hugging Face Hub 的“自动转换”:为什么你下载的模型越来越多是.safetensors?)
  • 总结与展望:成为一个"靠谱"的AI模型管理者

前言:训练的终点,是"存档"的开始

经过数小时的等待,看着Loss曲线缓缓下降,你终于训练好了一个表现出色的AI模型。此刻,这个模型的所有"智慧"------那亿万个经过优化的参数------都还静静地躺在你的内存(或显存)里。

一旦你关闭程序,或者电脑断电,这些耗费了巨大算力才学到的"知识"都将烟消云散。

模型持久化 (Model Persistence),也就是我们常说的"保存模型"或"存档",就是将这些内存中的"智慧",写入到硬盘文件中,以便未来可以随时加载回来,进行推理、继续训练或分享给他人。

但是,"存档"这件看似简单的事,背后却隐藏着格式的选择、效率的考量,甚至严重的安全风险。今天,我们就来当一次"AI档案管理员",学习如何正确地管理这些珍贵的"数字大脑"。

第一章:PyTorch的"传统艺能"------ torch.save 与 Pickle 协议

介绍PyTorch原生的两种保存方式,并揭示其背后pickle协议的巨大安全隐患。

1.1 两种存档策略:只存"灵魂" vs. "灵魂+躯体"一起存

只保存模型权重 (State Dict):

比喻:只保存一个人的"记忆和灵魂"(state_dict),不保存他的"身体结构"。

优点:文件小,灵活,安全。这是官方推荐的最佳实践。

缺点:加载时,你必须先拥有一个一模一样的"身体"(即模型的类定义),才能把"灵魂"加载进去。

保存整个模型:

比喻:"灵魂"和"身体结构"一起打包保存。

优点:方便,加载时不需要模型的类定义。

缺点:文件大,耦合性强,存在严重的安全风险。

1.2 用代码实现两种存档与读档方式

本节概括:我们将定义一个极简的神经网络,然后分别用"只存权重"和"存完整模型"两种方式进行保存和加载,让你直观地看到它们的区别。

代码实现

dart 复制代码
import torch
import torch.nn as nn
import os

# --- 0. 准备工作:创建一个文件夹和定义一个简单模型 ---
os.makedirs('models', exist_ok=True)

class SimpleModel(nn.Module):
    def __init__(self):
        super(SimpleModel, self).__init__()
        self.linear1 = nn.Linear(10, 20)
        self.relu = nn.ReLU()
        self.linear2 = nn.Linear(20, 5)

    def forward(self, x):
        return self.linear2(self.relu(self.linear1(x)))

# 实例化一个模型
model = SimpleModel()
print("--- 原始模型 (随机初始化权重) ---")
# 打印一个权重,看看它的初始值
print(model.linear1.weight[0, :5])


# ==========================================================
# 策略一:只保存"灵魂"(State Dict) - 官方推荐
# ==========================================================
print("\n--- 策略一:只保存权重 (State Dict) ---")

# 1. 保存
# model.state_dict() 会返回一个包含模型所有权重的字典
STATE_DICT_PATH = 'models/simple_model_weights.pt'
torch.save(model.state_dict(), STATE_DICT_PATH)
print(f"✅ 模型的权重已保存到: {STATE_DICT_PATH}")

# 2. 加载
# 首先,你需要重新创建一个一模一样的模型"躯体"
new_model_from_weights = SimpleModel()
# 然后,将保存的权重"灵魂"加载进去
new_model_from_weights.load_state_dict(torch.load(STATE_DICT_PATH))
# 设置为评估模式
new_model_from_weights.eval() 
print("✅ 权重已成功加载到新的模型实例中!")
print("--- 从权重加载的模型 ---")
print(new_model_from_weights.linear1.weight[0, :5])


# ==========================================================
# 策略二:保存"灵魂 + 躯体"(完整模型) - 不推荐
# ==========================================================
print("\n--- 策略二:保存完整模型 ---")

# 1. 保存
# 直接保存整个model对象
FULL_MODEL_PATH = 'models/simple_model_full.pt'
torch.save(model, FULL_MODEL_PATH)
print(f"✅ 完整的模型已保存到: {FULL_MODEL_PATH}")

# 2. 加载
# 直接加载文件,无需预先定义模型类
# (但前提是,你当前的代码环境能找到SimpleModel这个类的定义)
new_model_from_full = torch.load(FULL_MODEL_PATH)
new_model_from_full.eval()
print("✅ 完整的模型已成功加载!")
print("--- 从完整模型文件加载的模型 ---")
print(new_model_from_full.linear1.weight[0, :5])

print("\n🔍 对比发现,三种模型的权重完全一致,证明加载成功!")

【代码解读】

运行这段代码,你会清晰地看到:

策略一更"安全",因为它要求你必须拥有SimpleModel的源代码才能恢复模型。你清楚地知道自己正在加载什么。

策略二看起来更"方便",torch.load()一步到位。但它的代价是巨大的安全风险和较差的可移植性。

最佳实践:永远优先使用策略一(保存和加载state_dict)。

1.3 【高能预警】Pickle的"阿喀琉斯之踵":恐怖的"代码注入"风险

torch.save的底层,使用的是Python的pickle模块。pickle的强大之处在于,它不仅能序列化数据,还能序列化代码对象

这也带来了它最致命的弱点:一个恶意的.pt或.pth文件,可以在你加载它(torch.load)的时候,执行任意的、有害的计算机代码!

比如,一个黑客可以构造一个模型文件,你一加载,它就在后台悄悄地删除你的文件、或者上传你的私钥。

结论:永远不要加载来自你不信任来源的.pt或.pth文件!

第二章:新王登基 ------ safetensors 的崛起

介绍Hugging Face推出的safetensors格式,解释它为什么更安全、更快速,以及如何使用它。

为了解决Pickle的根本性安全问题,Hugging Face社区开发并力推一种新的模型序列化格式:.safetensors。

2.1 为什么需要safetensors?------ 安全、快速、跨平台

安全 (Safe):这是它最重要的特性。

快速 (Fast):它的加载速度,尤其是当模型只加载一部分(分片加载)时,远快于pickle。

零拷贝 (Zero-copy):加载时可以直接将硬盘上的数据映射到内存,无需额外的反序列化开销。

2.2 safetensors 的核心思想:数据与代码的"彻底分离"

safetensors的实现原理非常简单:

文件结构:一个.safetensors文件,只包含两部分:

一个JSON头部:用纯文本描述了所有张量的元信息(名字、形状、数据类型、在文件中的位置)。

一个二进制数据块:包含了所有Tensor的、紧凑排列的、原始的二进制字节数据。

加载过程:加载时,程序首先安全地解析JSON头部,了解模型结构,然后直接从文件中的指定位置,将二进制数据块加载到内存中,构建成Tensor。

整个过程中,没有任何执行任意代码的机会。 它只处理数据,不处理代码。

2.3 用Hugging Face safetensors库进行安全的模型存取

我们将使用与上一个案例相同的简单模型,但这次,我们用safetensors来保存和加载它的state_dict,体验其安全、高效的特性。

代码实现

dart 复制代码
import torch
import torch.nn as nn
import os
# 首先,需要安装safetensors库
# pip install safetensors
from safetensors.torch import save_file, load_file

# --- 0. 准备工作:复用我们之前的SimpleModel ---
os.makedirs('models', exist_ok=True)

class SimpleModel(nn.Module):
    def __init__(self):
        super(SimpleModel, self).__init__()
        self.linear1 = nn.Linear(10, 20)
        self.relu = nn.ReLU()
        self.linear2 = nn.Linear(20, 5)

    def forward(self, x):
        return self.linear2(self.relu(self.linear1(x)))

# 实例化一个模型
model_to_save_safely = SimpleModel()
print("--- 待保存的模型权重 (部分) ---")
print(model_to_save_safely.linear1.weight[0, :5])


# --- 1. 使用safetensors进行保存 ---
print("\n--- 使用 safetensors 保存 ---")

# safetensors只操作state_dict,符合最佳实践
weights = model_to_save_safely.state_dict()
SAFETENSORS_PATH = 'models/simple_model.safetensors'

# 调用 save_file 函数
save_file(weights, SAFETENSORS_PATH)
print(f"✅ 模型的权重已安全地保存到: {SAFETENSORS_PATH}")


# --- 2. 使用safetensors进行加载 ---
print("\n--- 使用 safetensors 加载 ---")

# 同样,先创建一个新的模型"躯体"
new_model_safe_load = SimpleModel()

# 调用 load_file 函数,它会返回一个state_dict
loaded_weights = load_file(SAFETENSORS_PATH)

# 将加载的权重应用到模型上
new_model_safe_load.load_state_dict(loaded_weights)
new_model_safe_load.eval()
print("✅ 权重已从.safetensors文件安全加载!")
print("--- 从safetensors加载的模型权重 (部分) ---")
print(new_model_safe_load.linear1.weight[0, :5])

print("\n🔍 对比发现,权重完全一致,我们完成了一次安全的模型存取!")

代码解读与优势分析】

你会发现,使用safetensors的流程和PyTorch官方推荐的"只存权重"策略几乎完全一样,学习成本极低。但它带来了根本性的改变:

绝对安全:load_file这个函数在设计上就不可能执行任何恶意代码。你可以放心地加载任何来源的.safetensors文件。

跨框架兼容:用PyTorch保存的.safetensors文件,可以被TensorFlow、JAX等其他框架直接加载,反之亦然。它是一种通用的"Tensor档案袋"。

加载飞快:对于巨大的模型文件(几十上百GB),safetensors的加载速度优势会变得极其明显,因为它避免了pickle缓慢的反序列化过程。

结论:在你的所有项目中,请养成使用safetensors.torch.save_file来替代torch.save保存权重的习惯。这是一个能让你受益终身的、专业且安全的工程实践。

第三章:江湖别传 ------ .ckpt 与 .pth 的故事

解释另外两种常见的文件后缀,让你对模型文件的"江湖"有一个更完整的认识。

3.1 .ckpt (Checkpoint):不止是模型,更是"训练现场"的快照

.ckpt或.checkpoint文件,通常不仅仅包含模型的权重。它是一个**"检查点"**文件,除了model_state_dict,往往还保存了:

optimizer_state_dict:优化器(如Adam)的状态,包含了动量等信息,以便能从断点处恢复训练。

epoch:当前训练到了第几个周期。

loss:当前的损失值等。

一句话:.ckpt是用于"恢复训练"的,而.pt或.safetensors通常是用于"推理部署"的最终产物。

3.2 .pth (PyTorch):一个"约定俗成"的旧爱

.pth是PyTorch早期教程中常用的文件后缀,它和.pt在技术上没有任何区别,都是使用pickle序列化的结果。只是一个社区的命名习惯问题。现在,.pt更为常见

一张图看懂 三大主流格式的核心对比

特性 .pt / .pth (Pickle) .safetensors .ckpt (Checkpoint)

核心技术 Python pickle JSON + Raw Binary Python pickle

安全性 🚨 极不安全 ✅ 非常安全 🚨 极不安全

加载速度 普通 ⚡️ 极快 (尤其是分片) 普通

主要用途 推理部署 (不推荐) 推理部署 (推荐) 恢复训练

内容物 权重 或 完整模型 通常只有权重 权重、优化器状态、训练进度等

Hugging Face Hub 的"自动转换":为什么你下载的模型越来越多是.safetensors?

你可能已经注意到,现在在Hugging Face Hub上下载模型时,即使作者只上传了.pt文件,Hub通常也会自动为你提供一个.safetensors的下载选项。

这是Hugging Face为了社区安全,在后台做的一项重要工作。它会自动加载用户上传的pickle文件,提取出其中的权重,然后用.safetensors格式重新保存一份。当你使用from_pretrained方法时,如果检测到有.safetensors文件,它会优先加载这个安全版本。

总结与展望:成为一个"靠谱"的AI模型管理者

恭喜你!今天你从一个模型的使用者,成长为了一名懂得如何安全、高效地管理和分发模型的"档案管理员"。

✨ 本章惊喜概括 ✨

你掌握了什么? 对应的技能/工具
理解了两种存档策略 ✅ 只存权重 vs. 存完整模型
洞悉了Pickle的风险 ✅ 警惕.pt文件的"代码注入"攻击
掌握了未来的标准 ✅ safetensors的安全与高效
分清了不同的"黑话" ✅ .ckpt用于恢复训练,.pth是旧称
在AI的开源世界里,模型的分享与协作是基石。而安全,则是这一切的底线。养成优先使用和分享.safetensors格式的习惯,不仅是对你自己负责,也是对整个社区负责。