创建自定义模型¶
🤗 Transformers 库设计得非常易于扩展。每个模型都在仓库的一个子文件夹中完全编码,没有任何抽象,因此你可以轻松地复制一个模型文件并根据需要进行调整。
如果你要编写一个全新的模型,从头开始可能会更容易。在这个教程中,我们将展示如何编写一个自定义模型及其配置,以便在 Transformers 中使用,并说明如何与社区分享这个模型(包括它依赖的代码),即使它不在 🤗 Transformers 库中。我们将基于 Transformers 框架扩展功能,加入你自己的钩子和自定义代码。
为了说明这些步骤,我们将以 ResNet 模型为例,通过包装 timm 库 中的 ResNet 类,使其符合 PreTrainedModel 的规范。
编写自定义配置¶
在深入模型之前,我们先来编写模型的配置。模型的配置对象将包含构建模型所需的所有必要信息。正如我们将在下一节中看到的,模型在初始化时只能接受一个 config 对象,因此我们需要确保这个对象尽可能完整。
transformers 库中的模型通常遵循这样的约定:它们在 __init__ 方法中接受一个 config 对象,然后将整个 config 传递给模型的子层,而不是将 config 对象拆分成多个参数分别传递给子层。以这种方式编写模型可以简化代码,确保任何超参数都有一个明确的“唯一来源”,并且也更容易重用 transformers 中其他模型的代码。
在我们的示例中,我们将取 ResNet 类的一些参数进行调整。不同的配置将给我们带来不同类型的 ResNets。我们检查一些参数的有效性后,将这些参数存储起来。
from transformers import PretrainedConfig
from typing import List
class ResnetConfig(PretrainedConfig):
model_type = "resnet"
def __init__(
self,
block_type="bottleneck",
layers: List[int] = [3, 4, 6, 3],
num_classes: int = 1000,
input_channels: int = 3,
cardinality: int = 1,
base_width: int = 64,
stem_width: int = 64,
stem_type: str = "",
avg_down: bool = False,
**kwargs,
):
if block_type not in ["basic", "bottleneck"]:
raise ValueError(f"`block_type` must be 'basic' or 'bottleneck', got {block_type}.")
if stem_type not in ["", "deep", "deep-tiered"]:
raise ValueError(f"`stem_type` must be '', 'deep' or 'deep-tiered', got {stem_type}.")
self.block_type = block_type
self.layers = layers
self.num_classes = num_classes
self.input_channels = input_channels
self.cardinality = cardinality
self.base_width = base_width
self.stem_width = stem_width
self.stem_type = stem_type
self.avg_down = avg_down
super().__init__(**kwargs)
编写自定义配置时有三个重要的点需要注意:
- 必须继承自
PretrainedConfig, - 你的
PretrainedConfig的__init__方法必须接受任何关键字参数(kwargs), - 这些
kwargs需要传递给父类的__init__方法。
继承 PretrainedConfig 是为了确保你能获得 🤗 Transformers 库的所有功能,而其他两个约束则是因为 PretrainedConfig 有更多的字段,而不仅仅是你设置的那些字段。当使用 from_pretrained 方法重新加载配置时,这些字段需要被你的配置接受并传递给父类。
为你的配置定义一个 model_type(例如 model_type="resnet")不是强制的,除非你想将你的模型注册到自动类(见最后一节)。
完成这些后,你可以像库中的其他模型配置一样轻松创建和保存你的配置。以下是创建并保存一个 resnet50d 配置的方法:
resnet50d_config = ResnetConfig(block_type="bottleneck", stem_width=32, stem_type="deep", avg_down=True)
resnet50d_config.save_pretrained("custom-resnet")
这将在 custom-resnet 文件夹中保存一个名为 config.json 的文件。然后你可以使用 from_pretrained 方法重新加载你的配置:
resnet50d_config = ResnetConfig.from_pretrained("custom-resnet")
你也可以使用 PretrainedConfig 类的其他方法,比如 push_to_hub() 来直接将配置上传到 Hub。
编写自定义模型¶
现在我们有了 ResNet 配置,可以继续编写模型了。实际上我们将编写两个模型:一个用于提取一批图像的隐藏特征(类似于 BertModel),另一个适用于图像分类(类似于 BertForSequenceClassification)。
如前所述,为了保持简单,我们只会编写一个松散的模型包装器。在编写这个类之前,我们需要做的是建立一个块类型和实际块类之间的映射。然后,通过将所有内容传递给 ResNet 类来定义模型:
from transformers import PreTrainedModel
from timm.models.resnet import BasicBlock, Bottleneck, ResNet
from .configuration_resnet import ResnetConfig
BLOCK_MAPPING = {"basic": BasicBlock, "bottleneck": Bottleneck}
class ResnetModel(PreTrainedModel):
config_class = ResnetConfig
def __init__(self, config):
super().__init__(config)
block_layer = BLOCK_MAPPING[config.block_type]
self.model = ResNet(
block_layer,
config.layers,
num_classes=config.num_classes,
in_chans=config.input_channels,
cardinality=config.cardinality,
base_width=config.base_width,
stem_width=config.stem_width,
stem_type=config.stem_type,
avg_down=config.avg_down,
)
def forward(self, tensor):
return self.model.forward_features(tensor)
对于将用于图像分类的模型,我们只需更改 forward 方法:
import torch
class ResnetModelForImageClassification(PreTrainedModel):
config_class = ResnetConfig
def __init__(self, config):
super().__init__(config)
block_layer = BLOCK_MAPPING[config.block_type]
self.model = ResNet(
block_layer,
config.layers,
num_classes=config.num_classes,
in_chans=config.input_channels,
cardinality=config.cardinality,
base_width=config.base_width,
stem_width=config.stem_width,
stem_type=config.stem_type,
avg_down=config.avg_down,
)
def forward(self, tensor, labels=None):
logits = self.model(tensor)
if labels is not None:
loss = torch.nn.functional.cross_entropy(logits, labels)
return {"loss": loss, "logits": logits}
return {"logits": logits}
在这两种情况下,注意我们是如何继承自 PreTrainedModel 并在初始化时调用父类的 config(类似于编写一个普通的 torch.nn.Module)。设置 config_class 这一行不是强制的,除非你想将你的模型注册到自动类(见最后一节)。
如果你的模型与库中的某个模型非常相似,你可以重用该模型相同的配置。
你的模型可以返回任何你想要的内容,但像我们在 ResnetModelForImageClassification 中那样返回一个包含损失(当提供标签时)的字典,可以使你的模型直接在 Trainer 类中使用。使用其他输出格式也是可以的,只要你计划使用自己的训练循环或另一个库进行训练。
现在我们有了模型类,让我们创建一个实例:
resnet50d = ResnetModelForImageClassification(resnet50d_config)
同样,你可以使用 PreTrainedModel 类的任何方法,比如 save_pretrained() 或 push_to_hub()。我们将在下一节中使用后者,并介绍如何将模型权重和代码一起推送。但首先,让我们加载一些预训练的权重到我们的模型中。
在你自己的用例中,你可能需要在自己的数据上训练自定义模型。为了快速完成本教程,我们将使用预训练的 resnet50d。由于我们的模型只是一个包装器,因此转移这些权重会非常容易:
import timm
pretrained_model = timm.create_model("resnet50d", pretrained=True)
resnet50d.model.load_state_dict(pretrained_model.state_dict())
现在让我们看看如何确保在调用 save_pretrained() 或 push_to_hub() 时,模型的代码也会被保存。
将带有自定义代码的模型注册到自动类¶
如果你正在编写一个扩展 🤗 Transformers 的库,你可能希望扩展自动类以包含你自己的模型。这与将代码推送到 Hub 不同,因为用户需要导入你的库才能使用自定义模型(而不是从 Hub 自动下载模型代码)。
只要你的配置有一个不同于现有模型类型的 model_type 属性,并且你的模型类具有正确的 config_class 属性,你就可以将它们添加到自动类中,如下所示:
from transformers import AutoConfig, AutoModel, AutoModelForImageClassification
AutoConfig.register("resnet", ResnetConfig)
AutoModel.register(ResnetConfig, ResnetModel)
AutoModelForImageClassification.register(ResnetConfig, ResnetModelForImageClassification)
请注意,当你将自定义配置注册到 AutoConfig 时,使用的第一个参数需要与你的自定义配置的 model_type 匹配,而在将自定义模型注册到任何自动模型类时,使用的第一个参数需要与这些模型的 config_class 匹配。
将代码推送到 Hub¶
此 API 是实验性的,可能会有一些小幅的破坏性变更。
首先,确保你的模型完全定义在一个 .py 文件中。它可以依赖于同一目录中的其他文件的相对导入(目前还不支持子模块)。在我们的示例中,我们将在当前工作目录中的 resnet_model 文件夹中定义一个 modeling_resnet.py 文件和一个 configuration_resnet.py 文件。配置文件包含 ResnetConfig 的代码,建模文件包含 ResnetModel 和 ResnetModelForImageClassification 的代码。
__init__.py 可以为空,只是为了确保 Python 检测到 resnet_model 可以作为一个模块使用。
如果从库中复制建模文件,你需要将文件顶部的所有相对导入替换为从 transformers 包中导入。
请注意,你可以重用(或子类化)现有的配置/模型。
要与社区共享你的模型,请按照以下步骤操作:首先从新创建的文件中导入 ResNet 模型和配置:
from resnet_model.configuration_resnet import ResnetConfig
from resnet_model.modeling_resnet import ResnetModel, ResnetModelForImageClassification
然后你需要告诉库,当你使用 save_pretrained 方法时,希望复制这些对象的代码文件,并正确注册它们到给定的自动类(尤其是对于模型),只需运行:
ResnetConfig.register_for_auto_class()
ResnetModel.register_for_auto_class("AutoModel")
ResnetModelForImageClassification.register_for_auto_class("AutoModelForImageClassification")
注意,对于配置不需要指定自动类(只有一个自动类 AutoConfig),但对于模型则不同。你的自定义模型可能适合多种不同的任务,因此你需要指定哪个自动类是你的模型的正确类。
如果你希望复制代码文件,请使用 register_for_auto_class()。如果你更喜欢从其他仓库使用 Hub 上的代码,则不必调用它。在有多个自动类的情况下,你可以直接修改 config.json,使用以下结构:
接下来,我们像之前一样创建配置和模型:
resnet50d_config = ResnetConfig(block_type="bottleneck", stem_width=32, stem_type="deep", avg_down=True)
resnet50d = ResnetModelForImageClassification(resnet50d_config)
pretrained_model = timm.create_model("resnet50d", pretrained=True)
resnet50d.model.load_state_dict(pretrained_model.state_dict())
现在要将模型推送到 Hub,请确保你已登录。可以在终端中运行:
huggingface-cli login
或者在笔记本中运行:
from huggingface_hub import notebook_login
notebook_login()
然后你可以将其推送到你自己的命名空间(或你是成员的组织):
resnet50d.push_to_hub("custom-resnet50d")
除了模型权重和 JSON 格式的配置外,这还将 custom-resnet50d 文件夹中的建模和配置 .py 文件复制并上传到 Hub。你可以在 模型库 中查看结果。
有关更多关于推送到 Hub 的方法的信息,请参阅 共享教程。
使用带有自定义代码的模型¶
你可以使用自动类和 from_pretrained 方法来使用任何带有自定义代码文件的配置、模型或分词器。上传到 Hub 的所有文件和代码都会进行恶意软件扫描(更多详细信息请参阅 Hub 安全 文档),但你仍然应该审查模型代码和作者,以避免在机器上执行恶意代码。设置 trust_remote_code=True 以使用带有自定义代码的模型:
from transformers import AutoModelForImageClassification
model = AutoModelForImageClassification.from_pretrained("sgugger/custom-resnet50d", trust_remote_code=True)
强烈建议传递一个 revision 参数并设置一个提交哈希值,以确保模型作者没有更新代码包含恶意的新行(除非你完全信任模型的作者)。
commit_hash = "ed94a7c6247d8aedce4647f00f20de6875b5b292"
model = AutoModelForImageClassification.from_pretrained(
"sgugger/custom-resnet50d", trust_remote_code=True, revision=commit_hash
)
请注意,当在 Hub 上浏览模型仓库的提交历史时,有一个按钮可以方便地复制任何提交的哈希值。