AutoModel 是 Hugging Face Transformers 里我最常用的类,没有之一。不管是加载别人训练好的开源模型,还是把本地 checkpoint 塞进 pipeline,一句AutoModel.from_pretrained()就搞定了。但等你自己开始训练自定义模型、想用同一套加载逻辑把模型存下来再读回去时,多半会撞上一堆报错:Unrecognized model type、Could not find modeling file、size mismatch,每一行都让人头大。
这篇文章就围绕“AutoModel 怎么保存与加载自定义模型”这件事,把机制拆开讲清楚,然后给你一套能直接落地复制的方案。适合这类场景的读者:你在某个内部项目里改过注意力结构,或者给模型加了自定义 head,又或者只是想把一个非官方结构的模型用统一方式加载。我的目标是,你看完之后能自己写一个模型文件、注册到 AutoModel、保存成 checkpoint,再顺利读回来,不靠网上零碎的帖子拼凑。
1. 先搞清楚 AutoModel 到底是怎么“认人”的
1.1 AutoModel 不是魔法,它只是一个“路由表”
很多人在自定义模型加载失败时,第一反应是去改 Transformers 源码,加一个 if 分支。这个方向一开始就偏了。AutoModel的本质不是模型实现,而是一个工厂:它通过配置文件里的字段,找到应该实例化哪个类,然后调用那个类的from_pretrained方法完成加载。
具体来说,AutoModel.from_pretrained()会先读取目标目录下的config.json,从里面拿到model_type或architectures字段,再到注册表里查对应的模型类。你自定义的模型如果不在这个注册表里,它自然不认识。这就像打车软件派单:乘客输入的终点是model_type,司机池子是注册表,乘客要去的具体地址才是你的模型类。你只告诉它目的地,它能派单的前提是系统里得有这个司机。
所以要让 AutoModel 认出自定义模型,本质上只有两条路:要么把你的模型类注册进它的路由表,要么在config.json里给出一份“导航地图”(auto_map),让它按图索骥找到你的代码。两条路不冲突,可以搭配使用。
1.2 保存自定义模型时需要哪些“身家证明”
既然 AutoModel 是靠 config 找类,那么“保存模型”就远不止把state_dict存下来这么简单。一个能被 AutoModel 重新加载的自定义模型,目录里至少要有这几样东西:
config.json:模型的元信息文件,也是 AutoModel 的导航入口。pytorch_model.bin或model.safetensors:权重文件。modeling_my_model.py:模型类的定义代码,AutoModel 需要执行它才能拿到你的类。- 可能还有
tokenizer、preprocessor_config.json等,取决于你的任务类型。
很多人只保存了权重,然后拿一个全新的模型类去load_state_dict,也能跑,但代价是加载逻辑散落在代码里:每个使用方都要知道“模型类是什么、参数怎么传”。这不叫“能用 AutoModel 保存加载”,叫“碰巧能跑”。如果你想让团队里任何一个人拿到这个目录就能训练、推理、部署,那么 config、代码、权重三者必须齐全,并且路径关系稳定。
2. 动手之前先搭好自定义模型的目录与配置
2.1 一种简单可靠的目录结构
先说结论,我建议你在项目里用下面这种结构放自定义模型相关的文件:
my_project/ ├── modeling_my_model.py ├── configuration_my_model.py └── save_and_load_demo.py为什么把配置类和模型类拆开?因为config和model的职责不同:config负责记录超参和元信息,model负责定义网络结构。如果你把它们写在同一个文件里,不是不行,但加载时auto_map的路径写法会稍微绕一点。拆开之后,职责清晰,也方便你后续把模型推到某个模型仓库时按社区惯例管理。
实际保存时,生成的目标目录结构大概是这样:
my_custom_model/ ├── config.json ├── modeling_my_model.py ├── configuration_my_model.py ├── model.safetensors └── vocab.json这里的modeling_my_model.py和configuration_my_model.py是你在项目里写的原始文件,save_pretrained会帮你把它们复制到输出目录,至少它会为 remote code 模式做这一步。
2.2 configuration_my_model.py 里必须写清楚什么
你的配置类应该继承PretrainedConfig,并且要显式设置model_type。这个字段的值,就是 AutoModel 以后识别你的模型的钥匙。比如:
from transformers import PretrainedConfig class MyModelConfig(PretrainedConfig): model_type = "my_model" def __init__( self, vocab_size=32000, hidden_size=768, num_hidden_layers=12, num_attention_heads=12, intermediate_size=3072, max_position_embeddings=1024, initializer_range=0.02, **kwargs, ): super().__init__(**kwargs) self.vocab_size = vocab_size self.hidden_size = hidden_size self.num_hidden_layers = num_hidden_layers self.num_attention_heads = num_attention_heads self.intermediate_size = intermediate_size self.max_position_embeddings = max_position_embeddings self.initializer_range = initializer_range这里有一个很多新手会漏掉的点:model_type一定要在__init__外定义,或者说至少保证它作为类属性存在。因为save_pretrained在写出config.json时,会读取这个属性。如果你的model_type只通过kwargs传入而不在类上声明,序列化时可能丢掉它,加载的时候就抓瞎。
另外,kwargs里一定要调用super().__init__(**kwargs)。很多PretrainedConfig的内置字段(torch_dtype、transformers_version、_name_or_path等)都需要经过父类初始化才能正确落盘。漏掉这一行,from_pretrained可能静默丢失某些元信息,后排查起来很麻烦。
2.3 modeling 文件里必须包含的三个关键接口
自定义模型文件的核心是你自己的nn.Module实现,但要能被 AutoModel 用起来,还缺三样东西:
- 模型类本身,通常命名如
MyModel。 - 必须在文件顶部
from transformers import PreTrainedModel,并继承PreTrainedModel而不是nn.Module。 - 必须在类里定义
config_class = MyModelConfig,并在__init__里调用super().__init__(config)。
原因不复杂:PreTrainedModel是 Transformers 给所有模型提供的基础设施,save_pretrained、from_pretrained、push_to_hub、generate这些方法全都依赖它。你如果直接继承nn.Module,那么AutoModel.from_pretrained即使找到了你的类,调用的也是父类的通用加载逻辑,很多预处理和后处理逻辑对不上,迟早出问题。
模型的前向函数返回值建议保持 Transformers 的惯例:返回一个包含loss和logits的对象,或者直接返回ModelOutput。这不是强制要求,但如果你后面接 Trainer、pipeline,或者想统一用AutoModelForSequenceClassification之类的类做封装,返回值结构一致可以省掉一堆兼容代码。
3. 保存与导出的完整流程
3.1 用 save_pretrained 保存模型的完整动作
假设你已经训练好了一个自定义模型model,对应的配置是config。保存时不要只调用torch.save(model.state_dict(), ...),那会把 config 和代码文件丢掉。正确的做法是:
model.save_pretrained("./my_custom_model") model.config.save_pretrained("./my_custom_model")如果你把config绑定到了模型上(也就是model.config = config),第一个调用就会自动把 config 写进去,第二个调用可以省略。但显式写下config.save_pretrained有个额外好处:你可以临时修改 config 里的某些字段再保存,比如把max_position_embeddings调低,或者把architectures字段改成你想要的名字。
safetensors是另一个建议。从某个版本开始,save_pretrained默认会尝试保存为model.safetensors。如果你更习惯用老的 bin 格式,也可以显式传入safe_serialization=False。我个人的建议是:多设备推理、生产环境优先用 safetensors,加载更快,也避免 pickle 反序列化带来的安全隐患。你的加载代码并不需要关心它是 bin 还是 safetensors,AutoModel 会自动识别。
保存完之后,建议打开config.json检查一眼。重点看这几个字段在不在:
model_type:值等于你在 config 类里定义的字符串。architectures:值是模型类的名字数组,比如["MyModel"]。transformers_version:用于追踪版本兼容性。
这些字段里,model_type是 AutoModel 认路的关键,architectures是给库内注册表用的。两个都别丢。
3.2 把自定义模型注册进 AutoModel 的两种方式
方式一:使用auto_map远程代码。这种方式需要在config.json里显式声明,让 AutoModel 知道去哪个文件、找哪个类。下面是一个示例配置:
{ "model_type": "my_model", "auto_map": { "AutoConfig": "configuration_my_model.MyModelConfig", "AutoModel": "modeling_my_model.MyModel" } }然后加载时,你必须传trust_remote_code=True,并指定config所在的目录:
from transformers import AutoConfig, AutoModel config = AutoConfig.from_pretrained("./my_custom_model", trust_remote_code=True) model = AutoModel.from_pretrained("./my_custom_model", config=config, trust_remote_code=True)注意auto_map里配置的路径,是相对于加载路径的模块路径,不是 Python 包路径。也就是说,如果config.json在my_custom_model目录下,它会去my_custom_model/modeling_my_model.py和my_custom_model/configuration_my_model.py里找对应类,而不是去 site-packages 里找。这很关键,很多人在这踩坑。
方式二:直接使用register类方法。如果你只是在自己项目里用,不想每次加载都传trust_remote_code,可以直接注册:
from transformers import AutoConfig, AutoModel AutoConfig.register("my_model", MyModelConfig) AutoModel.register(MyModelConfig, MyModel)注册之后,AutoModel 的路由表里就有你了。这时加载模型就不需要trust_remote_code,也不需要auto_map,只要目录里有config.json和权重文件就行。缺点是这个注册动作必须在加载前执行过,其他使用者也得在你的环境里先跑一遍注册代码。
两种方式怎么选?我的判断是:如果你要把模型发布给其他人用,或者放到模型仓库,用auto_map更规范。如果你只是在本地训练和推理循环里复用,用register更简单,少了一层文件映射的复杂度。多数内部项目,register已经足够了。
3.3 加载时的参数到底该怎么传
AutoModel.from_pretrained里最常被忽视的两个参数是trust_remote_code和strict。
trust_remote_code的含义是允许 AutoModel 加载并执行自定义代码文件。这就像一个“可执行文件”许可:你确认目录里的.py文件是可信的,才会去执行。本地加载自己写的代码,问题不大。但如果你下载了一个陌生人的模型并且它的 config 里有auto_map,执行它之前务必检查代码内容。这是社区里反复强调的安全意识,但实践里真的有人不看。
strict参数决定load_state_dict是否要求权重 key 完全匹配。默认True,也就是模型代码定义的层和权重文件里的 key 必须一一对应。自定义模型改结构后加载老权重,或者加载了和 config 不一致的 checkpoint,最常见的报错就是size mismatch和unexpected key。如果你确实想部分加载,可以传strict=False,但要明确这可能导致你模型里有随机初始化的层,语义上是“加载了,但没完全加载”。
一个值得养成的习惯是加载后做一次输出验证:用同一个输入跑一次旧模型和新加载的模型,对比输出张量是否一致。很多静默错误(比如 config 里的hidden_size写错了,导致权重被广播式复用)都是靠这种 sanity check 暴露的。
4. 实操示例:从自定义实现到 AutoModel 完整加载
4.1 示例模型设计
下面是我在一个模拟项目里用过的例子,结构很简单:一个 embedding 层、一个 Transformer encoder 层、一个分类头。为了让这个例子有区分度,我没用现成的BertModel直接拼,而是写了一个自定义初始化方式的模块。
# modeling_my_model.py import torch import torch.nn as nn from transformers import PreTrainedModel from configuration_my_model import MyModelConfig class MyModel(PreTrainedModel): config_class = MyModelConfig def __init__(self, config): super().__init__(config) self.embedding = nn.Embedding(config.vocab_size, config.hidden_size) self.encoder = nn.TransformerEncoder( nn.TransformerEncoderLayer( d_model=config.hidden_size, nhead=config.num_attention_heads, dim_feedforward=config.intermediate_size, batch_first=True, ), num_layers=config.num_hidden_layers, ) self.classifier = nn.Linear(config.hidden_size, 2) self.post_init() def forward(self, input_ids, attention_mask=None, labels=None): hidden = self.embedding(input_ids) hidden = self.encoder(hidden) pooled = hidden.mean(dim=1) logits = self.classifier(pooled) loss = None if labels is not None: loss = nn.functional.cross_entropy(logits, labels) return {"loss": loss, "logits": logits}注意这里调用了post_init(),这个方法会在初始化后做权重初始化并绑定config。如果你需要新的随机初始化策略,也应该在post_init前设置好。
4.2 训练并保存 checkpoint
接下来是保存流程。为了体现“完整”,我把 tokenizer、config、模型三个部分拆开来演示:
from transformers import AutoTokenizer from configuration_my_model import MyModelConfig from modeling_my_model import MyModel config = MyModelConfig( vocab_size=1000, hidden_size=128, num_hidden_layers=2, num_attention_heads=4, intermediate_size=512, max_position_embeddings=64, ) model = MyModel(config) tokenizer = AutoTokenizer.from_pretrained("bert-base-uncased") output_dir = "./my_custom_model" model.save_pretrained(output_dir, safe_serialization=True) model.config.save_pretrained(output_dir) tokenizer.save_pretrained(output_dir).save_pretrained之后,output_dir的config.json里应该能看到model_type="my_model"。如果你的模型类没有自动带出这个字段,或者你想让其他人在本地不执行注册代码也能加载,就在config.json里手动补一份auto_map。注意,save_pretrained本身不会帮你生成auto_map,这是需要你主动加的。
4.3 用 AutoModel 加载并做一致性验证
接下来的加载演示分两个层级。先是最朴素的:目录里没有auto_map,但代码已经在当前 Python 进程里注册好了。
from transformers import AutoConfig, AutoModel AutoConfig.register("my_model", MyModelConfig) AutoModel.register(MyModelConfig, MyModel) config = AutoConfig.from_pretrained("./my_custom_model") model = AutoModel.from_pretrained("./my_custom_model", config=config) model.eval() with torch.no_grad(): logits = model(torch.tensor([[1, 2, 3]]))["logits"] print(logits.shape) # torch.Size([1, 2])如果目录里有auto_map,也简单:
config = AutoConfig.from_pretrained("./my_custom_model", trust_remote_code=True) model = AutoModel.from_pretrained("./my_custom_model", config=config, trust_remote_code=True)两种方式加载之后,模型输出应该完全一致。如果输出不一致,说明 class 路径或者 config 序列化环节出了问题。排查时先看config.json里的model_type和代码里定义的是否一致,再看architectures字段里的类名和代码里的类名是否一致。
5. 常见问题与排查技巧实录
5.1 典型报错速查表
我做了一张小表,按我自己的踩坑频率排序。不一定全面,但覆盖了 80% 的自定义模型加载问题。
| 报错特征 | 常见原因 | 解决办法 |
|---|---|---|
Unrecognized model type | model_type没有注册 | 调用AutoConfig.register,或在config.json里写auto_map |
Could not find modeling file | auto_map路径配置错误 | 确认路径是相对 checkpoint 目录的模块路径,并确认.py文件已被复制到目录 |
ImportError: cannot import name | 类名和auto_map里的名字不一致 | 打开目标目录下的.py文件,核对类名 |
size mismatch for embedding.weight | config 参数和权重维度不一致 | 检查vocab_size、hidden_size等是否保存时与加载时一致 |
Unexpected key(s) in state_dict | 模型结构和权重 key 对不上 | 加载时传strict=False确认缺失层,或重新初始化对齐的结构 |
| 加载成功但输出明显异常 | config 参数字段名错误,导致多个层复用相同参数 | 打印 config 和模型参数量,逐个核对字段名 |
trust_remote_code=True后仍然报错 | 当前 Transformers 版本过旧,不支持该参数 | 升级 Transformers,或改用register方式 |
OSError: model.safetensors is not a valid file | 手动改名导致的文件格式不匹配 | 用save_pretrained(safe_serialization=True)重新保存 |
5.2 一条能快速定位问题的排查路径
我以前遇到问题,习惯从最外层往里走。现在推荐的排查路径是:
- 先看
config.json是否存在,且内容完整。缺了这个文件,AutoModel 连路由都没法做。 - 再看
model_type是否等于你 config 类里定义的字符串。这一个字段错了,后面所有的报错都是白看。 - 看目录里有没有对应的
.py文件。如果你用了auto_map,save_pretrained不会自动把所有 py 文件都复制过去,你要么手动复制,要么保存时指定model_file_name和config_file_name。 - 看权重文件是否存在,并检查它和 config 的参数维度是否匹配。最简单的方式是直接用
torch.load读一下,观察 key 和 shape。 - 最后一步再去看
AutoModel的报错堆栈。多数情况下,到这一步会发现前面某个环节漏了。
5.3 集成到训练框架时的额外注意点
很多人不是单独跑上面的 demo,而是把自定义模型塞进Trainer或自己的训练循环里。这里有几个坑,我逐一说明。
第一,Trainer在保存 checkpoint 时,会调用model.save_pretrained(save_directory)。如果你的模型文件里没有带上configuration_my_model.py和modeling_my_model.py,那么拿这个 checkpoint 给别人加载时就会有挫败感。解决办法是在保存前,把这两个.py文件复制到输出目录。或者更优雅一点,重写save_pretrained,让它在保存 config 和权重之前先把源文件复制过去。
第二,多卡device_map加载时,AutoModel.from_pretrained的device_map="auto"会触发模型拆分。自定义模型如果内部结构不是标准的nn.Module嵌套,或者某些层用了不可分片的实现,拆分时可能报错。这种情况我要么退回device_map=None单卡加载,要么把自定义算子改成可分片的形式。
第三,如果你自定义模型里包含随机 dropout、或者非确定性采样层,跨版本加载后重新训练,指标可能对不上。保存时最好把transformers_version字段留下来,加载时也确认代码文件版本和保存时一致。这个字段PretrainedConfig会自动记录,但如果你手动构造 config 并且不调用save_pretrained,就可能缺失。
5.4 我踩过的几次坑
第一次踩坑是我把config_class写错位置,放在了文件底部而不是模型类里。加载时AutoModel.from_pretrained找到了类,但拿不到 config 类,于是退回到默认的PretrainedConfig,一堆参数全部丢失。这种问题不会立刻报错,而是等到模型前向计算时才发现维度全乱了,排查起来格外痛苦。现在我的习惯是:写完modeling文件后,先单独跑一个“保存再加载”的最小测试,确认 config 字段一致,再继续往下写训练逻辑。
第二次踩坑是auto_map路径写错。我把路径写成了my_custom_model/modeling_my_model.MyModel,但实际config.json就在my_custom_model目录下,它加载时会把目录名也拼接进去,结果自然找不到。后来我统一约定:auto_map里只写相对于 checkpoint 目录的模块名和类名,不写目录前缀。这个约定沿用至今,没再出过类似问题。
第三次踩坑比较隐蔽:模型里用了自定义的激活函数,但这个函数写在训练脚本里,没放进modeling_my_model.py。结果保存时一切正常,一旦换一台机器用AutoModel.from_pretrained加载,直接报NameError。从那以后,我要求所有模型相关代码(包括自定义初始化、激活函数、损失函数)全部放回模型文件里,保持目录自包含。
最后分享一点个人体会
做了几年的模型训练和部署,我最大的感受是:加载逻辑这件事,越早统一越好。训练时顺手把 config、模型文件、tokenizer 一起保存,比事后补档案省心太多。AutoModel 这套设计虽然初看有些绕,但它的价值在于“约定优于配置”:只要你的文件结构和字段命名符合它的约定,它就能像加载官方模型一样加载你的自定义模型,这样团队协作、模型上线、后续迭代都会顺很多。如果你也在做类似的事情,建议从最小用例开始,先把保存加载闭环跑通,再逐渐加复杂度,这套体系会给你带来稳定的复现体验。