☰
Transformers模型保存与加载的八种模式详解
2026/10/13 1:28:42 网站建设 项目流程

1. 这不是“存个文件”那么简单:为什么 Transformers 模型的保存与加载值得单独讲八次

你有没有试过训练完一个模型,兴冲冲地torch.save(model, 'model.pth'),结果第二天torch.load('model.pth')时报错AttributeError: 'BertModel' object has no attribute 'classifier'?或者更糟——模型能加载,但预测结果完全不对,loss 飙高,像没训过一样?我第一次遇到这种问题时,在实验室熬到凌晨三点,反复核对代码,最后发现只是少了一行model.eval()。这根本不是“保存/加载”两个函数的事,这是在和整个模型架构、状态字典、配置文件、分词器、甚至随机数生成器打一场精密配合的协同战。

“Transformers 保存并加载模型 | 八”这个标题里的“八”,绝不是凑数。它代表的是八种常见却极易踩坑的场景:从最基础的save_pretrained()/from_pretrained()组合,到只保存/加载模型权重(.bin)、只保存/加载配置(.json)、只保存/加载分词器(tokenizer.json),再到混合保存(权重+配置+分词器打包)、跨框架加载(PyTorch 模型加载进 TensorFlow 环境)、断点续训专用保存(含优化器状态、学习率调度器、epoch 数)、以及最隐蔽也最致命的——仅保存模型结构而不保存权重,或反之。每一种,背后都对应着不同的工程目标:是部署上线?是团队协作复现?是中断后继续训练?还是做模型蒸馏的教师模型?选错方式,轻则浪费几小时调试,重则让三天的训练白费。

核心关键词就三个:save_pretrained、from_pretrained、state_dict。但它们不是孤立的 API,而是一套工作流的锚点。save_pretrained不是把 Python 对象“快照”下来,而是把模型拆解成三类独立资产:1)可序列化的模型权重(二进制.bin文件);2)人类可读的模型结构定义(JSON 格式config.json);3)文本处理逻辑(tokenizer.json或vocab.txt)。from_pretrained则是反向操作,它先读config.json构建空壳模型,再加载.bin填充参数,最后加载分词器完成端到端链路。而state_dict是 PyTorch 的底层机制,它只管模型参数(nn.Parameter)和缓冲区(buffer)的键值对映射,不包含任何结构信息——这就是为什么直接torch.save(model.state_dict())后,你必须手动用完全相同的代码重新构建模型类,否则torch.load()出来的只是一堆数字,没有“意义”。

适合谁看?如果你正在用 Hugging Face Transformers 做项目,无论是微调 BERT 做文本分类,还是用 Llama-2 做对话生成,只要你的流程里有“训练完要保存”或“别人给了你一个模型要跑起来”,这篇就是为你写的。它不讲论文,不推公式,只讲你在终端里敲下的每一行命令、在代码里写的每一个load调用,背后到底发生了什么,以及为什么你昨天那个“看起来一模一样”的加载代码,今天会报错。

2. 保存与加载的本质:三件套缺一不可,但你可以按需组合

2.1 为什么必须是“三件套”?——模型、配置、分词器的职责分离

想象你要寄一台组装好的台式机给朋友。你不能只寄主板(模型权重),因为朋友不知道这主板支持什么 CPU(config.json定义了隐藏层维度、注意力头数、激活函数类型等)。你也不能只寄说明书(config.json),因为没硬件,说明书只是废纸。更不能只寄键盘(分词器),因为键盘打不出字,得有主板和 CPU 配合才能运行程序。Hugging Face 的设计哲学正是如此:解耦。模型权重(.bin)是“肌肉”,负责计算;配置文件(config.json)是“基因图谱”,定义了肌肉怎么长、多大强度;分词器(tokenizer.json)是“翻译官”,把人类语言转成模型能懂的数字 ID。

我做过一个对比实验:用AutoModel.from_pretrained("bert-base-chinese")加载官方模型,然后分别尝试:

  • 只删掉config.json:from_pretrained直接报错OSError: Can't load config for 'path/to/dir'. Make sure the config file is present.;
  • 只删掉pytorch_model.bin:报错OSError: Unable to load weights from pytorch checkpoint for 'BertModel';
  • 只删掉tokenizer.json:模型能加载,但tokenizer.encode("你好")会失败,因为找不到词汇表。

这证明三者是强依赖关系。但“强依赖”不等于“必须同时存在”。比如你做模型蒸馏,只需要教师模型的前向推理能力,那config.json和pytorch_model.bin就够了,分词器可以另配;又比如你做超参搜索,需要快速保存/恢复优化器状态,那state_dict+optimizer.state_dict()就是核心,config.json反而是冗余的。

2.2 八种模式的底层逻辑:按目标选择“最小必要集合”

所谓“八种模式”,本质是围绕三个核心资产(权重、配置、分词器)和两个附加状态(优化器、随机种子)的排列组合。我们来逐个拆解其适用场景和底层原理:

  1. 标准全量保存/加载(推荐新手)
    model.save_pretrained("my_model_dir")+AutoModel.from_pretrained("my_model_dir")
    ✅ 优点:全自动,三件套齐全,开箱即用。
    ❌ 缺点:目录体积大(pytorch_model.bin单文件常达 400MB+),不适合频繁保存。

  2. 仅权重保存/加载(部署精简)
    torch.save(model.state_dict(), "weights.bin")+model.load_state_dict(torch.load("weights.bin"))
    ✅ 优点:文件极小(无元数据),加载快。
    ❌ 缺点:必须保证加载时的模型类定义(class MyBertModel(BertModel):...)与保存时完全一致,包括所有__init__参数和子模块命名。我曾因一个dropout=0.1写成dropout=0.2,导致load_state_dict报size mismatch错误,查了两小时。

  3. 仅配置保存/加载(架构复用)
    config.save_pretrained("my_config_dir")+AutoConfig.from_pretrained("my_config_dir")
    ✅ 优点:config.json仅几 KB,可版本控制(Git)。
    ❌ 缺点:纯文本,无法执行,需配合权重使用。

  4. 仅分词器保存/加载(NLP 流水线解耦)
    tokenizer.save_pretrained("my_tokenizer_dir")+AutoTokenizer.from_pretrained("my_tokenizer_dir")
    ✅ 优点:分词逻辑独立,可为不同模型共享同一套分词器(如多个任务共用bert-base-chinese分词器)。
    ❌ 缺点:tokenizer.json里存的是词汇表映射,不包含模型结构,无法单独推理。

  5. 混合保存(自定义打包)
    手动将config.json、pytorch_model.bin、tokenizer.json复制到同一目录,再用from_pretrained加载。
    ✅ 优点:灵活,可剔除不需要的文件(如删除special_tokens_map.json节省空间)。
    ❌ 缺点:易出错,需确保文件名严格匹配(Hugging Face 默认要求pytorch_model.bin,写成model.bin就加载失败)。

  6. 跨框架加载(PyTorch ↔ TensorFlow)
    TFBertModel.from_pretrained("bert-base-chinese", from_pt=True)
    ✅ 优点:无需转换工具,一行代码搞定。
    ❌ 缺点:仅支持官方支持的模型(BERT、RoBERTa 等),且转换过程有精度损失(FP32 → TF 的 float32 表示差异)。

  7. 断点续训保存(含优化器状态)

    torch.save({ 'epoch': epoch, 'model_state_dict': model.state_dict(), 'optimizer_state_dict': optimizer.state_dict(), 'scheduler_state_dict': scheduler.state_dict(), 'loss': loss, }, "checkpoint.pth")

    ✅ 优点:恢复训练时,连学习率、当前 epoch 都精准还原。
    ❌ 缺点:optimizer.state_dict()包含大量内部缓存(如 Adam 的exp_avg),文件比纯权重大 2-3 倍。

  8. 结构+权重分离保存(高级调试)
    model.config.to_json_file("config.json")+torch.save(model.state_dict(), "weights.bin")
    ✅ 优点:调试时可单独修改config.json(如把num_hidden_layers从 12 改成 6),再加载新权重验证效果。
    ❌ 缺点:config.json修改后,若权重维度不匹配(如层数变少但权重仍是 12 层),load_state_dict(strict=True)会直接报错。

提示:strict=False参数是救命稻草。当load_state_dict报Missing key(s) in state_dict或Unexpected key(s) in state_dict时,加strict=False可跳过不匹配的键,只加载能对上的部分。但这只是临时方案,长期来看,必须保证配置与权重的拓扑一致性。

3. 实操全流程:从零开始保存一个微调后的 BERT 模型,并在另一台机器上完美加载

3.1 场景设定:一个真实的微调任务

假设你在某高校的 NLP 实验室,任务是微调bert-base-chinese在一个中文新闻标题分类数据集(体育、财经、娱乐三类)上。你用TrainerAPI 训练了 3 个 epoch,现在要保存模型用于后续部署。这不是玩具 demo,而是要交付给工程组集成到线上服务的正式模型。

第一步:确认训练环境与依赖版本
这是最容易被忽略的“隐形前提”。我在某次跨团队交付中,因对方环境transformers==4.25.0,而我的是4.30.0,导致from_pretrained加载时报KeyError: 'layer_norm_eps'—— 新版 config 多了一个字段,旧版代码不认识。解决方案只有两个:要么统一版本(推荐),要么在保存时导出兼容格式(见 3.3 节)。所以,先执行:

pip list | grep transformers # 输出:transformers 4.30.0 python -c "import torch; print(torch.__version__)" # 输出:2.0.1+cu118

并将这两行写入requirements.txt,随模型一起交付。

第二步:标准全量保存(安全第一)
训练完成后,不要急着torch.save,而是用 Hugging Face 官方推荐方式:

from transformers import AutoModel, AutoTokenizer # 假设 model 是训练好的 AutoModelForSequenceClassification model.save_pretrained("./my_news_classifier") # 保存模型权重+config tokenizer.save_pretrained("./my_news_classifier") # 保存分词器

执行后,./my_news_classifier目录下会生成:

├── config.json # 模型结构定义 ├── pytorch_model.bin # 模型权重(420MB) ├── tokenizer_config.json # 分词器配置 ├── tokenizer.json # 分词器词汇表(12MB) ├── special_tokens_map.json # 特殊 token 映射(如 [CLS], [SEP]) └── vocab.txt # 词表文本(备选格式)

注意:pytorch_model.bin是 PyTorch 格式,如果工程组用 TensorFlow,这里就要换成TFBertModel并用save_pretrained(..., save_format="tf")。

第三步:在目标机器上加载并验证
交付给工程组的,不只是一个文件夹,而是一份可执行的验证脚本。我习惯提供verify_load.py:

from transformers import AutoModelForSequenceClassification, AutoTokenizer import torch # 1. 加载(自动识别目录内容) model = AutoModelForSequenceClassification.from_pretrained("./my_news_classifier") tokenizer = AutoTokenizer.from_pretrained("./my_news_classifier") # 2. 关键!设置为评估模式 model.eval() # 3. 构造测试输入(必须和训练时一致) text = "国足客场1-0战胜越南队" inputs = tokenizer(text, return_tensors="pt", truncation=True, padding=True, max_length=128) # 4. 前向推理 with torch.no_grad(): outputs = model(**inputs) predictions = torch.nn.functional.softmax(outputs.logits, dim=-1) predicted_class = torch.argmax(predictions, dim=-1).item() print(f"输入: {text}") print(f"预测类别: {predicted_class} (概率: {predictions[0][predicted_class].item():.4f})") # 输出应为 0(体育类),且概率 > 0.95

这个脚本必须能在目标机器上python verify_load.py一键跑通。如果失败,错误信息就是第一手调试依据。

第四步:生产环境精简(可选)
线上服务不需要special_tokens_map.json和tokenizer_config.json的全部字段。我通常会:

  • 删除special_tokens_map.json(tokenizer初始化时会从tokenizer.json自动推导);
  • 将tokenizer.json中的added_tokens字段精简,只保留实际用到的特殊 token;
  • 用torch.quantization.quantize_dynamic对模型做动态量化,pytorch_model.bin体积可减少 50%,推理速度提升 20%。

注意:量化后的模型必须用torch.jit.trace或torch.jit.script导出为 TorchScript,再用torch.jit.load加载,不能再用from_pretrained。这是另一个技术栈,此处不展开。

4. 常见问题与排查技巧实录:那些让我熬夜的 Bug 和解决方案

4.1 “KeyError: 'xxx'” —— 配置与权重的版本战争

现象:from_pretrained("my_model")报错KeyError: 'layer_norm_eps'或KeyError: 'relative_attention_max_distance'。
原因:config.json里定义了新字段,但当前transformers版本的模型类代码里没有这个属性。这是 Hugging Face 版本迭代的典型问题。新版config.json向后兼容,但旧版代码不向前兼容。
排查思路:

  1. cat ./my_model/config.json | head -20查看 config 是否有新字段;
  2. pip show transformers查看本地版本;
  3. 对照 Hugging Face 官方文档 确认该字段是哪个版本引入的。
    终极方案:
  • 升级本地transformers(pip install --upgrade transformers);
  • 或降级模型 config:用旧版代码打开 config,删掉不认识的字段,再config.to_json_file("config.json")重存。例如,layer_norm_eps在 v4.28+ 引入,v4.25 需要删掉它。

4.2 “size mismatch for xxx.weight” —— 权重形状的无声背叛

现象:model.load_state_dict(torch.load("weights.bin"))报错size mismatch for bert.encoder.layer.0.attention.self.query.weight: copying a param with shape torch.Size([768, 768]) from checkpoint, the shape in current model is torch.Size([768, 1024])。
原因:模型结构变了。可能是:

  • 你改了config.hidden_size(如从 768 改成 1024),但权重文件还是旧的;
  • 或用了prune_heads剪枝,但加载时没传pruned_heads参数。
    排查技巧:
  • 用torch.load("weights.bin", map_location="cpu").keys()列出所有权重键;
  • 用model.state_dict().keys()列出当前模型键;
  • 用set(weights_keys) - set(model_keys)找出多出来的键,set(model_keys) - set(weights_keys)找出缺失的键。
    修复步骤:
  1. 确认config.json的hidden_size、num_attention_heads等关键参数;
  2. 如果参数不一致,必须重新训练,或用model.resize_token_embeddings()等 API 动态调整(仅适用于嵌入层)。

4.3 “CUDA out of memory” —— 加载时的显存幻觉

现象:from_pretrained执行到一半,GPU 显存爆满,报CUDA out of memory。
真相:这不是加载的问题,是from_pretrained默认在 GPU 上构建模型。AutoModel.from_pretrained会先model = BertModel(config),这个空模型就占了显存,再model.load_state_dict()加载权重,显存翻倍。
解决方案:

  • 方法一(推荐):强制 CPU 加载,再移入 GPU
    model = AutoModel.from_pretrained("my_model", device_map="cpu") # 全程在 CPU model = model.to("cuda:0") # 一次性搬入 GPU
  • 方法二:用low_cpu_mem_usage=True(v4.20+)
    model = AutoModel.from_pretrained("my_model", low_cpu_mem_usage=True)
    此参数会跳过在 CPU 创建完整模型的过程,直接从磁盘流式加载权重到 GPU,显存占用降低 40%。

4.4 “The model class X was not found” —— 自定义模型的迷失

现象:你写了class MyCustomBert(BertModel): ...,保存后from_pretrained报错The model class MyCustomBert was not found。
原因:from_pretrained只认识 Hugging Face 官方注册的模型类(如BertModel,RobertaModel),不认识你的自定义类。
正确做法:

  1. 保存时,用model.save_pretrained保存官方基类权重;
  2. 加载时,先用BertModel.from_pretrained加载权重,再用你的自定义类包装:
    base_model = BertModel.from_pretrained("my_model") my_model = MyCustomBert(base_model.config) my_model.bert = base_model # 将官方模型作为子模块
    这样既复用训练好的权重,又保留自定义逻辑。

4.5 “Predictions are all random” —— 评估模式的遗忘

现象:模型能加载,model(**inputs)也能跑,但输出 logits 全是接近 0 的随机数,softmax 后概率分布均匀(如[0.33, 0.33, 0.34])。
原因:忘了model.eval()。训练时的Dropout和LayerNorm在train()模式下是随机的,eval()模式下才固定。
排查口诀:所有推理前,必加model.eval();所有训练前,必加model.train()。
防呆设计:在verify_load.py里,加载后立刻加一句:

assert not model.training, "Model is in training mode! Add model.eval()"

5. 进阶技巧与避坑心得:十年踩坑总结的七条铁律

5.1 铁律一:永远用save_pretrained/from_pretrained,除非你明确知道自己在做什么

我见过太多人用torch.save(model, "model.pt"),理由是“简单”。但torch.save保存的是 Python 对象的 pickle 序列化,它会把整个模型类、所有闭包变量、甚至当前工作目录路径都存进去。一旦换环境(Python 版本不同、模块路径不同),torch.load就会报ModuleNotFoundError。而save_pretrained只存纯数据(JSON、BIN),与环境解耦。这是 Hugging Face 成为行业标准的根本原因——可移植性。

5.2 铁律二:config.json是你的“宪法”,修改前必备份,修改后必验证

config.json里任何一个数字错了,都会导致灾难性后果。比如num_hidden_layers少写一个,模型就少一层,权重加载时直接size mismatch;hidden_size写错,整个前向传播的矩阵乘法维度都不对。我的做法是:每次修改config.json,都用git commit -m "config: update num_hidden_layers to 12",并写一条单元测试:

def test_config_consistency(): config = AutoConfig.from_pretrained("./my_model") assert config.num_hidden_layers == 12 assert config.hidden_size == 768 assert config.vocab_size == 21128

5.3 铁律三:分词器必须和模型“同源”,禁止混搭

有人为了“省事”,用AutoTokenizer.from_pretrained("bert-base-chinese")加载官方分词器,去处理自己微调的my_news_classifier模型。这很危险!因为微调时可能添加了新 token(如领域专有名词),而官方分词器不认识。结果就是tokenizer.encode("CBA")返回[101, 102](全是 [UNK]),模型输入全是 100,输出自然乱码。正确做法:分词器必须和模型一起保存、一起加载,哪怕它看起来“没变”。

5.4 铁律四:断点续训的 checkpoint,必须包含随机种子状态

Trainer的save_strategy="steps"会自动保存trainer_state.json,里面记录了seed。但如果你手动torch.save,很容易漏掉。我吃过亏:在 A 机器训练到 epoch 2,保存 checkpoint;在 B 机器加载后继续训练,结果因为torch.manual_seed()没同步,B 机器的Dropoutmask 和 A 机器完全不同,loss 曲线剧烈震荡。解决方案:在 checkpoint 字典里显式加入:

checkpoint = { 'model_state_dict': model.state_dict(), 'optimizer_state_dict': optimizer.state_dict(), 'seed': torch.initial_seed(), # 获取当前随机种子 }

5.5 铁律五:生产环境加载,必须做“冷启动”验证

不要相信“上次能跑,这次肯定能”。每次部署新模型,必须在空环境(Docker 容器)里,从pip install开始,完整走一遍from_pretrained→model.eval()→tokenizer.encode()→model(**inputs)→softmax全流程。我维护的 CI/CD 流水线里,有一条test_production_load用例,失败则阻断发布。这看似繁琐,但比线上事故后回滚节省 10 倍时间。

5.6 铁律六:模型文件权限,是运维的隐形门槛

pytorch_model.bin是二进制文件,Linux 下默认权限是644(所有者可读写,组和其他人只读)。但如果工程组用root用户运行服务,而模型文件是user:user所有,root可能因 umask 设置无法读取。我的做法:保存后立刻chmod 644 ./my_model/pytorch_model.bin,并写入部署文档:“请确保模型目录对服务用户有读取权限”。

5.7 铁律七:日志,是你唯一的证人

在verify_load.py里,我强制打印三行日志:

print(f"[INFO] Model config: {model.config.model_type}, hidden_size={model.config.hidden_size}") print(f"[INFO] Tokenizer vocab_size: {tokenizer.vocab_size}, max_len={tokenizer.model_max_length}") print(f"[INFO] Input shape: {inputs['input_ids'].shape}, device={inputs['input_ids'].device}")

这三行能在 90% 的加载失败场景里,第一时间定位问题:是模型类型不匹配?是分词器长度超限?还是张量没送到 GPU?日志不是摆设,是调试时的第一手证据。

我个人在实际操作中的体会是:模型保存与加载,70% 的时间花在环境一致性上,20% 花在配置校验上,只有 10% 是真正的代码问题。当你看到from_pretrained成功返回一个BertModel对象时,那不是结束,而是真正考验的开始——接下来的model.eval()、tokenizer.encode()、model(**inputs),每一步都是对“三件套”完整性的终极拷问。把这八种模式吃透,把七条铁律刻进本能,你就能在任何项目里,稳稳地把训练成果,变成可交付、可复现、可信赖的生产力。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询