PEFT documentation
自定义模型
自定义模型
一些微调技术,例如 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