PEFT documentation

自定义模型

You are viewing main version, which requires installation from source. If you'd like regular pip install, checkout the latest stable version (v0.21.0).
Hugging Face's logo
Join the Hugging Face community

and get access to the augmented documentation experience

to get started

自定义模型

一些微调技术,例如 prompt tuning,是特定于语言模型的。这意味着在 🤗 PEFT 中,假设使用的是 🤗 Transformers 模型。然而,其他微调技术——如 LoRA——并不局限于特定的模型类型。

在本指南中,我们将了解如何将 LoRA 应用于多层感知机、timm 库中的计算机视觉模型,或新的 🤗 Transformers 架构。

多层感知机

假设我们想使用 LoRA 对多层感知机进行微调。以下是其定义:

from torch import nn


class MLP(nn.Module):
    def __init__(self, num_units_hidden=2000):
        super().__init__()
        self.seq = nn.Sequential(
            nn.Linear(20, num_units_hidden),
            nn.ReLU(),
            nn.Linear(num_units_hidden, num_units_hidden),
            nn.ReLU(),
            nn.Linear(num_units_hidden, 2),
            nn.LogSoftmax(dim=-1),
        )

    def forward(self, X):
        return self.seq(X)

这是一个结构简单的多层感知机,包含输入层、隐藏层和输出层。

对于这个示例,我们选择了数量极大的隐藏单元,以突出 PEFT 带来的效率提升,但这些提升与更贴近实际场景的示例是一致的。

该模型中有几个线性层可以通过 LoRA 进行调优。在处理常见的 🤗 Transformers 模型时,PEFT 会自动识别应应用 LoRA 的层,但在此情况下,需要用户自行选择要调优的层。 要确定待调优层的名称:

print([(n, type(m)) for n, m in MLP().named_modules()])

这应该会打印:

[('', __main__.MLP),
 ('seq', torch.nn.modules.container.Sequential),
 ('seq.0', torch.nn.modules.linear.Linear),
 ('seq.1', torch.nn.modules.activation.ReLU),
 ('seq.2', torch.nn.modules.linear.Linear),
 ('seq.3', torch.nn.modules.activation.ReLU),
 ('seq.4', torch.nn.modules.linear.Linear),
 ('seq.5', torch.nn.modules.activation.LogSoftmax)]

假设我们想对输入层和隐藏层应用 LoRA,它们分别是 'seq.0' 和 'seq.2'。此外,假设我们想在不使用 LoRA 的情况下更新输出层,那将是 'seq.4'。相应的配置为:

from peft import LoraConfig

config = LoraConfig(
    target_modules=["seq.0", "seq.2"],
    modules_to_save=["seq.4"],
)

这样,我们就可以创建 PEFT 模型并检查所训练参数的比例:

from peft import get_peft_model

model = MLP()
peft_model = get_peft_model(model, config)
peft_model.print_healthcheck()

最后,我们可以使用任意训练框架,或者自行编写训练循环,来训练peft_model。

如需完整示例,请查看此笔记本。

timm 模型

timm 库包含大量预训练的计算机视觉模型。这些模型也可以使用 PEFT 进行微调。下面我们来看看它在实践中是如何工作的。

首先,确保在 Python 环境中安装了 timm:

python -m pip install -U timm

接下来,我们加载一个用于图像分类任务的 timm 模型:

import timm

num_classes = ...
model_id = "timm/poolformer_m36.sail_in1k"
model = timm.create_model(model_id, pretrained=True, num_classes=num_classes)

同样,我们需要决定将 LoRA 应用于哪些层。由于 LoRA 支持 2D 卷积层,且这些层是该模型的主要构建模块,因此我们应该将 LoRA 应用于 2D 卷积层。为了确定这些层的名称,让我们查看所有层的名称:

print([(n, type(m)) for n, m in model.named_modules()])

这将打印一个非常长的列表,我们只展示前几项:

[('', timm.models.metaformer.MetaFormer),
 ('stem', timm.models.metaformer.Stem),
 ('stem.conv', torch.nn.modules.conv.Conv2d),
 ('stem.norm', torch.nn.modules.linear.Identity),
 ('stages', torch.nn.modules.container.Sequential),
 ('stages.0', timm.models.metaformer.MetaFormerStage),
 ('stages.0.downsample', torch.nn.modules.linear.Identity),
 ('stages.0.blocks', torch.nn.modules.container.Sequential),
 ('stages.0.blocks.0', timm.models.metaformer.MetaFormerBlock),
 ('stages.0.blocks.0.norm1', timm.layers.norm.GroupNorm1),
 ('stages.0.blocks.0.token_mixer', timm.models.metaformer.Pooling),
 ('stages.0.blocks.0.token_mixer.pool', torch.nn.modules.pooling.AvgPool2d),
 ('stages.0.blocks.0.drop_path1', torch.nn.modules.linear.Identity),
 ('stages.0.blocks.0.layer_scale1', timm.models.metaformer.Scale),
 ('stages.0.blocks.0.res_scale1', torch.nn.modules.linear.Identity),
 ('stages.0.blocks.0.norm2', timm.layers.norm.GroupNorm1),
 ('stages.0.blocks.0.mlp', timm.layers.mlp.Mlp),
 ('stages.0.blocks.0.mlp.fc1', torch.nn.modules.conv.Conv2d),
 ('stages.0.blocks.0.mlp.act', torch.nn.modules.activation.GELU),
 ('stages.0.blocks.0.mlp.drop1', torch.nn.modules.dropout.Dropout),
 ('stages.0.blocks.0.mlp.norm', torch.nn.modules.linear.Identity),
 ('stages.0.blocks.0.mlp.fc2', torch.nn.modules.conv.Conv2d),
 ('stages.0.blocks.0.mlp.drop2', torch.nn.modules.dropout.Dropout),
 ('stages.0.blocks.0.drop_path2', torch.nn.modules.linear.Identity),
 ('stages.0.blocks.0.layer_scale2', timm.models.metaformer.Scale),
 ('stages.0.blocks.0.res_scale2', torch.nn.modules.linear.Identity),
 ('stages.0.blocks.1', timm.models.metaformer.MetaFormerBlock),
 ('stages.0.blocks.1.norm1', timm.layers.norm.GroupNorm1),
 ('stages.0.blocks.1.token_mixer', timm.models.metaformer.Pooling),
 ('stages.0.blocks.1.token_mixer.pool', torch.nn.modules.pooling.AvgPool2d),
 ...
 ('head.global_pool.flatten', torch.nn.modules.linear.Identity),
 ('head.norm', timm.layers.norm.LayerNorm2d),
 ('head.flatten', torch.nn.modules.flatten.Flatten),
 ('head.drop', torch.nn.modules.linear.Identity),
 ('head.fc', torch.nn.modules.linear.Linear)]
 ]

仔细观察后,我们发现 2D 卷积层的名称类似于 "stages.0.blocks.0.mlp.fc1" 和 "stages.0.blocks.0.mlp.fc2"。我们如何精确匹配这些层名称?你可以编写一个 正则表达式 来匹配层名称。在我们的情况下,正则表达式 r".*\.mlp\.fc\d" 应该就能完成任务。

此外,与第一个示例一样,我们还应确保输出层(在本例中为分类头)也得到更新。查看上面打印的列表末尾,可以看到它的名称为 'head.fc'。基于此,我们的 LoRA 配置如下:

config = LoraConfig(target_modules=r".*\.mlp\.fc\d", modules_to_save=["head.fc"])

然后我们只需将基础模型和配置传入get_peft_model,即可创建 PEFT 模型:

peft_model = get_peft_model(model, config)
peft_model.print_healthcheck()

这表明我们只需训练不到 2% 的所有参数,从而带来巨大的效率提升。

如需完整示例,请查看此笔记本。

新的 Transformers 架构

当新的热门 transformers 架构发布时,我们会尽力快速将其添加到 PEFT 中。如果您遇到一个未开箱即用的 transformers 模型,请不要担心,只要配置正确,它通常仍然可以正常工作。具体来说,您需要识别出需要适配的层,并在初始化相应的配置类(例如 LoraConfig)时正确设置它们。以下是一些有助于完成此操作的提示。

作为第一步,查看现有模型以获取灵感是个好主意。你可以在 PEFT 仓库中的 constants.py 里找到它们。通常,你会发现一个使用相同名称的类似架构。例如,如果新的模型架构是 “mistral” 模型的变体,并且你想应用 LoRA,你可以看到 TRANSFORMERS_MODELS_TO_LORA_TARGET_MODULES_MAPPING 中 “mistral” 的条目包含 ["q_proj", "v_proj"]。这告诉你,对于 “mistral” 模型,LoRA 的 target_modules 应该是 ["q_proj", "v_proj"]:

from peft import LoraConfig, get_peft_model

my_mistral_model = ...
config = LoraConfig(
    target_modules=["q_proj", "v_proj"],
    ...,  # other LoRA arguments
)
peft_model = get_peft_model(my_mistral_model, config)

如果这没有帮助,请使用 named_modules 方法检查模型架构中现有的模块,并尝试识别注意力层,特别是 key、query 和 value 层。这些层通常具有诸如 c_attn、query、q_proj 等名称。key 层并不总是会被适配,理想情况下,您应检查将其包含在内是否能带来更好的性能。

此外,线性层也是常见的适配目标(例如,在 QLoRA 论文中,作者建议对它们也进行适配)。它们的名称中通常包含字符串 fc 或 dense。

如果您想向 PEFT 添加一个新模型,请在 constants.py 中创建一个条目,并在 repository 上发起一个拉取请求。别忘了同时更新 README。

验证参数和层

你可以通过几种方式来验证是否已正确地将 PEFT 方法应用于你的模型。

  • 运行 print_healthcheck() 以检查可训练参数占比和适配器状态。如果参数占比低于或高于预期,请通过打印模型来检查模型的 repr。这会显示模型中所有层类型的名称。确保只有预期的目标层被适配器层替换。例如,如果 LoRA 应用于 nn.Linear 层,则你应该只看到使用了 lora.Linear 层。
peft_model.print_healthcheck()
  • 查看适配层的另一种方法是使用 targeted_module_names 属性来列出每个被适配模块的名称。
print(peft_model.targeted_module_names)

不支持的模块类型

像 LoRA 这样的方法仅在目标模块受 PEFT 支持时才能生效。例如,可以将 LoRA 应用于 nn.Linear 和 nn.Conv2d 层,但不能应用于 nn.LSTM。如果您发现想要应用 PEFT 的层类不受支持,您可以:

  • 定义自定义映射以在 LoRA 中动态分发自定义模块
  • 打开一个 issue 并请求该功能,如果对此类模块的需求足够高,维护者将会实现它,或者指导您自行实现。

LoRA 中自定义模块动态调用的实验性支持

此功能目前处于实验阶段,可能会根据社区的反馈而发生变化。如果需求量大,我们将推出公开且稳定的 API。

PEFT 支持用于 LoRA 自定义模块类型的实验性 API。假设你有一个针对 LSTM 的 LoRA 实现。通常情况下,你无法让 PEFT 使用它,即使它在理论上可以与 PEFT 配合工作。然而,通过自定义层的动态分发,这是可以实现的。

实验性 API 目前看起来是这样的:

class MyLoraLSTMLayer:
    ...

base_model = ...  # load the base model that uses LSTMs

# add the LSTM layer names to target_modules
config = LoraConfig(..., target_modules=["lstm"])
# define a mapping from base layer type to LoRA layer type
custom_module_mapping = {nn.LSTM: MyLoraLSTMLayer}
# register the new mapping
config._register_custom_module(custom_module_mapping)
# after registration, create the PEFT model
peft_model = get_peft_model(base_model, config)
# do training

调用 get_peft_model() 时,由于 PEFT 无法识别目标模块类型,您会看到一条警告。在这种情况下,您可以忽略此警告。

通过提供自定义映射,PEFT 会首先将基础模型的层与自定义映射进行比对,若匹配成功,则分发到自定义的 LoRA 层类型。若未匹配,PEFT 会检查内置的 LoRA 层类型以寻找匹配项。

因此,该功能还可用于覆盖现有的分发逻辑,例如,如果您想使用自己的 LoRA 层来替代 PEFT 提供的 nn.Linear 层。

在创建自定义 LoRA 模块时,请遵循与现有 LoRA 模块相同的规则。需要考虑的一些重要约束条件:

  • 自定义模块应继承自 nn.Module 和 peft.tuners.lora.layer.LoraLayer。
  • 自定义模块的 __init__ 方法应包含位置参数 base_layer 和 adapter_name。在此之后,还有额外的 **kwargs,您可以自由使用或忽略。
  • 可学习参数应存储在 nn.ModuleDict 或 nn.ParameterDict 中,其中键对应于特定适配器的名称(请记住,一个模型可以同时拥有多个适配器)。
  • 这些可学习参数属性的名称应以 "lora_" 开头,例如 self.lora_new_param = ...。
  • 某些方法是可选的,例如,如果您想支持权重合并,则只需实现merge和unmerge。

目前,保存模型时,自定义模块的相关信息不会被持久化。加载模型时,您需要重新注册这些自定义模块。

# saving works as always and includes the parameters of the custom modules
peft_model.save_pretrained(<model-path>)

# loading the model later:
base_model = ...
# load the LoRA config that you saved earlier
config = LoraConfig.from_pretrained(<model-path>)
# register the custom module again, the same way as the first time
custom_module_mapping = {nn.LSTM: MyLoraLSTMLayer}
config._register_custom_module(custom_module_mapping)
# pass the config instance to from_pretrained:
peft_model = PeftModel.from_pretrained(model, tmp_path / "lora-custom-module", config=config)

如果您使用此功能并觉得它有用,或者在使用过程中遇到问题,请通过创建 issue 或 discussion 来告知我们。这有助于我们评估该功能的需求量,并在需求足够高时添加公共 API。

Update on GitHub