参数高效微调PEFT(四)快速入门(IA)3
- 我们已经了解了HuggingFace中peft库的几种高效微调方法。
参数高效微调PEFT(一)快速入门BitFit、Prompt Tuning、Prefix Tuning
参数高效微调PEFT(二)快速入门P-Tuning、P-Tuning V2
参数高效微调PEFT(三)快速入门LoRA、AdaLoRA
- 今天我们继续了解高效微调方法(IA)3。
1 (IA)3
- in-context learning(ICL)简单的说就是,在
冻结大模型参数的情况下
,在输入时,给定一些样本包含数据和标签,同时给一个待预测数据,由模型输出这条数据的预测值
。这个过程中模型的参数不发生变化。因此在应用到下游任务时,不需要更新参数,可以扩展到各种各样的任务场景。- 但这种模式使得,模型在每次推理过程中,都要处理一次prompts中的示例样本,这也是一种很大的计算消耗。
- 改变prompts中例子的顺序对最终预测效果的影响很大;
- 还有的研究发现,prompts中的例子数据和标签在没有正确配对的情况下,对带预测数据的预测结果影响不是很大。
- 这些现象说明in-context learning 背后的机制,以及如何使in-context learning效果更加鲁棒仍然有待研究。
- IA3论文主要对ICL和PEFT方法,在少样本场景下进行了严谨的实验对比,发现PEFT方法在取得很高精度的情况下,同时很大降低了计算消耗,可以替代ICL。
- PEFT极大地减少了训练和保存模型所需的内存和存储需求。PEFT可以显著提高计算效率,并同时实现比ICL更高的准确性。
- 此外,某些PEFT方法可以直接允许
混合任务的批处理
。- 例如,Prompt tuning只需将不同的prompt embeddings连接到批处理中的每个示例即可使单个模型执行多个任务。
混合任务的批处理
指的是在一个批量处理的数据集中同时包含多个不同类型的任务或问题。在机器学习领域,通常情况下,一个模型被设计用于解决特定的任务,比如分类、回归等。- 而混合任务的批处理则是指在同一个训练批次中包含了多种不同类型的任务,使得模型能够同时学习多种任务之间的相关性,从而提高泛化能力和效率。这种方法可以帮助模型更好地利用数据,加速训练过程,并提高模型在多任务学习和迁移学习方面的性能。
- 另一方面,重参数化模型的PEFT方法(例如LoRA)对于混合任务的批处理来说是昂贵或繁琐的。
- 此外,不同的PEFT方法增加了执行推断所需的计算和内存量。例如,adapters实际上在模型中添加了额外的(小型)层,导致计算成本和内存开销的增加。
1.1 (IA)3简介
-
论文地址:Few-Shot Parameter-Efficient Fine-Tuning is Better and Cheaper than In-Context Learning(2205)
-
论文创新点主要有两个:
- 提出了一个新的高效微调方法(IA)3
- 基于T0模型提出了T-Few,在下游任务中不需要对任务进行额外模型调整,即可进行少样本学习。
-
新的高效微调方法(IA)3
- 虽然prompt tuning以及prefix tuning等方法可以满足下游多个任务同批次进行,但是精度不够,而精度够的方法又不允许同批次多任务处理。因此作者开发一个新的PEFT方法—(IA)3。这种PEFT方法是对模型的
一些激活层进行抑制或放大
,也就是通过点乘一个向量的形式对模型的一部分参数进行加权
。 - 下图左侧展示了这些下游任务微调的小参数的添加位置,分别在attention机制中的Key向量和Value向量上,以及前馈神经网络的激活层后。
- 另外,有工作指出预训练这部分参数也可以进一步提高下游任务上的少样本以及零样本性能,因此作者也采纳了预训练的做法。
- 虽然prompt tuning以及prefix tuning等方法可以满足下游多个任务同批次进行,但是精度不够,而精度够的方法又不允许同批次多任务处理。因此作者开发一个新的PEFT方法—(IA)3。这种PEFT方法是对模型的
-
基于T0模型提出了T-Few
- 基于团队先前的工作T0,作者修改了损失函数以适应少样本学习的情况,称为T-Few,无需针对特定任务进行调整或修改即可应用于新任务。
- 所以在模型训练过程,作者使用了不同loss:即上图右侧的语言模型损失 L L M L_{LM} LLM, 负例似然损失 L U L L_{UL} LUL, 长度归一化损失 L L N L_{LN} LLN
1.2 (IA)3源码分析
(IA)3的初始化,和之前LoRA一样,重要的代码就是update_layer。
update_layer中,如果是feedforward,会初始化shape为(1, self.in_features)的向量
如果不是(如query_key_value),就初始化shape为(self.out_features, 1)的向量
# peft\tuners\ia3.py
def update_layer(self, adapter_name, init_ia3_weights):
# Actual trainable parameters
if self.is_feedforward:
weight = torch.randn((1, self.in_features))
else:
weight = torch.randn((self.out_features, 1))
self.ia3_l.update(nn.ParameterDict({adapter_name: nn.Parameter(weight)}))
if init_ia3_weights:
self.reset_ia3_parameters(adapter_name)
self.to(self.weight.device)
初始化后,就会将之前的module进行替换,替换后的Model如下:
PeftModelForCausalLM(
(base_model): IA3Model(
(model): BloomForCausalLM(
(transformer): BloomModel(
(word_embeddings): Embedding(250880, 64)
(word_embeddings_layernorm): LayerNorm((64,), eps=1e-05, elementwise_affine=True)
(h): ModuleList(
(0-1): 2 x BloomBlock(
(input_layernorm): LayerNorm((64,), eps=1e-05, elementwise_affine=True)
(self_attention): BloomAttention(
# 1、query_key_value模块添加IA3
(query_key_value): Linear(
in_features=64, out_features=192, bias=True
(ia3_l): ParameterDict( (default): Parameter containing: [torch.FloatTensor of size 192x1])
)
(dense): Linear(in_features=64, out_features=64, bias=True)
(attention_dropout): Dropout(p=0.0, inplace=False)
)
(post_attention_layernorm): LayerNorm((64,), eps=1e-05, elementwise_affine=True)
(mlp): BloomMLP(
(dense_h_to_4h): Linear(in_features=64, out_features=256, bias=True)
(gelu_impl): BloomGelu()
(dense_4h_to_h): Linear(
# 2、dense_4h_to_h模块添加IA3
in_features=256, out_features=64, bias=True
(ia3_l): ParameterDict( (default): Parameter containing: [torch.FloatTensor of size 1x256])
)
)
)
)
(ln_f): LayerNorm((64,), eps=1e-05, elementwise_affine=True)
)
(lm_head): Linear(in_features=64, out_features=250880, bias=False)
)
)
)
最终会调用peft\tuners\ia3.py
中Linear的前向传播(如下代码)。
-
如果是feedforward,ia3_l向量与
feedforward的输入相乘
,再进入F.linear -
如果不是,可学习向量ia3_l与
注意力块的输出result相乘
def forward(self, x: torch.Tensor):
previous_dtype = x.dtype
if self.active_adapter not in self.ia3_l.keys():
return F.linear(x, transpose(self.weight, self.fan_in_fan_out), bias=self.bias)
if self.disable_adapters:
if self.merged:
self.unmerge()
result = F.linear(x, transpose(self.weight, self.fan_in_fan_out), bias=self.bias)
elif not self.merged:
# 1、如果是feedforward,ia3_l向量与feedforward的【输入】相乘,再进入F.linear
if self.is_feedforward:
x = x.to(self.ia3_l[self.active_adapter].dtype)
interm = x * self.ia3_l[self.active_adapter].flatten()
result = F.linear(
interm.to(self.weight.dtype),
transpose(self.weight, self.fan_in_fan_out),
bias=self.bias,
)
else:
# 2、可学习向量ia3_l与注意力块的【输出】result相乘
result = F.linear(x, transpose(self.weight, self.fan_in_fan_out), bias=self.bias)
result = result.to(self.ia3_l[self.active_adapter].dtype) * self.ia3_l[self.active_adapter].flatten()
else:
result = F.linear(x, transpose(self.weight, self.fan_in_fan_out), bias=self.bias)
result = result.to(previous_dtype)
return result
1.3 (IA)3轻量微调bloom模型
同样,我们只需要在加载原模型后、配置训练器前加peft的代码即可。
from peft import IA3Config, TaskType, get_peft_model
config = IA3Config(task_type=TaskType.CAUSAL_LM,
# bloom默认为["query_key_value", "mlp.dense_4h_to_h"]
# 配置在TRANSFORMERS_MODELS_TO_IA3_TARGET_MODULES_MAPPING(peft\utils\other.py)
target_modules=["query_key_value", "mlp.dense_4h_to_h"],
inference_mode=False,
# bloom默认为["mlp.dense_4h_to_h"]
# 配置在TRANSFORMERS_MODELS_TO_IA3_FEEDFORWARD_MODULES_MAPPING(peft\utils\other.py)
feedforward_modules=["mlp.dense_4h_to_h"]
)
print(config)
model = get_peft_model(model, config)
# 打印可训练参数
model.print_trainable_parameters()
# trainable params: 172,032 || all params: 345,940,992 || trainable%: 0.04972871211515749
- IA3Config常用参数如下:
task_type
:指定任务类型。如:条件生成任务(SEQ_2_SEQ_LM),因果语言建模(CAUSAL_LM)等。inference_mode
:是否在推理模式下使用Peft模型。target_modules
:要替换为 IA3 的模块名称列表或模块名称的正则表达式feedforward_modules
:target_modules 中被视为前馈(feedforward)层的模块名称列表或模块名称的正则表达式。注意:可学习向量与注意力块的输出激活相乘,但与经典前馈层的输入相乘
。module_to_save
:除了 IA3 层之外要设置为可训练并保存在最终检查点中的模块列表。
- 配置训练器、模型训练及推理和参数高效微调PEFT(一)快速入门BitFit、Prompt Tuning、Prefix Tuning中2.1一样。
- 显存消耗情况:
(base) root@autodl-container-adbc11ae52-f2ebff02:~# nvidia-smi
+-----------------------------------------------------------------------------+
| NVIDIA-SMI 525.89.02 Driver Version: 525.89.02 CUDA Version: 12.0 |
|-------------------------------+----------------------+----------------------+
| GPU Name Persistence-M| Bus-Id Disp.A | Volatile Uncorr. ECC |
| Fan Temp Perf Pwr:Usage/Cap| Memory-Usage | GPU-Util Compute M. |
| | | MIG M. |
|===============================+======================+======================|
| 0 NVIDIA GeForce ... On | 00000000:B2:00.0 Off | N/A |
| 31% 53C P2 183W / 250W | 2836MiB / 11264MiB | 42% Default |
| | | N/A |
+-------------------------------+----------------------+----------------------+
+-----------------------------------------------------------------------------+
| Processes: |
| GPU GI CI PID Type Process name GPU Memory |
| ID ID Usage |
|=============================================================================|
+-----------------------------------------------------------------------------+
2 PEFT多适配器
2.1 自定义模型适配
如何为自定义的模型适配参数高效微调呢
import torch
from torch import nn
from peft import LoraConfig, get_peft_model, PeftModel
# 1、自定义模型
net1 = nn.Sequential(
nn.Linear(10, 10),
nn.ReLU(),
nn.Linear(10, 2)
)
# 2、打印参数名
for name, param in net1.named_parameters():
print(name)
# 0.weight
# 0.bias
# 2.weight
# 2.bias
# 3、这里对nn.Linear(10, 10)模块高效微调
# 利用target_modules指定要添加LoRA的目标模块(支持正则)
config = LoraConfig(target_modules=["0"])
model1 = get_peft_model(net1, config)
print(model1)
PeftModel(
(base_model): LoraModel(
(model): Sequential(
(0): Linear(
# 可以看到第0层,可以已经发生了替换
in_features=10, out_features=10, bias=True
(lora_dropout): ModuleDict(
(default): Identity()
)
(lora_A): ModuleDict(
(default): Linear(in_features=10, out_features=8, bias=False) # 秩默认为8
)
(lora_B): ModuleDict(
(default): Linear(in_features=8, out_features=10, bias=False)
)
(lora_embedding_A): ParameterDict()
(lora_embedding_B): ParameterDict()
)
(1): ReLU()
(2): Linear(in_features=10, out_features=2, bias=True)
)
)
)
2.2 多适配器加载与切换
一个主模型,多个适配器的情况如何使用
# 我们定义一个主模型main_net
main_net = nn.Sequential(
nn.Linear(10, 10),
nn.ReLU(),
nn.Linear(10, 2)
)
# 保存第1个适配器lora1
config1 = LoraConfig(target_modules=["0"])
model2 = get_peft_model(main_net, config1)
model2.save_pretrained("./lora1")
# 保存第2个适配器lora2
config2 = LoraConfig(target_modules=["2"])
model2 = get_peft_model(main_net, config2)
model2.save_pretrained("./lora2")
- 在加载第一个适配器时,可以通过
PeftModel.from_pretrained
方法并指定adapter_name
参数来给它命名。否则,将使用默认的适配器名称default
。 - 要加载另一个适配器,请使用 PeftModel 的 load_adapter() 方法,例如:
model.load_adapter(peft_model_path, adapter_name)
- 要切换适配器,请使用 PeftModel 的 set_adapter() 方法,例如:
model.set_adapter(adapter_name)
# 我们重新创建模型,加载适配器
main_net = nn.Sequential(
nn.Linear(10, 10),
nn.ReLU(),
nn.Linear(10, 2)
)
# 加载第一个适配器
main_model = PeftModel.from_pretrained(main_net, model_id="./lora1/", adapter_name="lora1")
for name, param in main_model.named_parameters():
if name in ["base_model.model.0.lora_A.lora1.weight", "base_model.model.0.lora_B.lora1.weight"]:
param.data = torch.ones_like(param) * 10
print(main_model.active_adapter)
print(main_model(torch.arange(0, 10).view(1, 10).float()))
lora1
tensor([[-18845.8906, 39025.6406]])
# 加载另一个适配器
main_model.load_adapter("./lora2/", adapter_name="lora2")
# 切换适配器前
print(main_model.active_adapter)
print(main_model(torch.arange(0, 10).view(1, 10).float()))
# 切换适配器后
main_model.set_adapter("lora2")
print(main_model.active_adapter)
print(main_model(torch.arange(0, 10).view(1, 10).float()))
lora1
tensor([[-18845.8906, 39025.6406]]) # 和lora1结果一致
lora2
tensor([[0.3642, 0.7926]])
2.3 禁用适配器
如何获取原始模型的输出结果
- 要禁用适配器,请使用上下文管理器 disable_adapter(),例如:
with model.disable_adapter()
# 切换适配器
main_model.set_adapter("lora1")
# 还是lora1结果 tensor([[-18845.8906, 39025.6406]])
print(main_model(torch.arange(0, 10).view(1, 10).float()))
# 获取原始结果
with main_model.disable_adapter():
# tensor([[0.3642, 0.7926]])
print(main_model(torch.arange(0, 10).view(1, 10).float()))