简介
PEFT
PEFT(Parameter-Efficient Fine-Tuning),用于有效地使大型预训练模型适应各种下游应用程序,而无需微调模型的所有参数。
PEFT 方法仅对少量(额外)模型参数进行微调,显著降低了计算和存储成本,同时产生的性能可与完全微调的模型相媲美。
| task | model | method |
|---|---|---|
| causal language modeling,因果语言建模 | gpt2 | lora, prefix tuning, p-tuning, prompt tuning, IA3 |
| causal language modeling,因果语言建模 | gpt-neo | lora, prefix tuning, p-tuning, prompt tuning, IA3 |
| causal language modeling,因果语言建模 | gpt-j | lora, prefix tuning, p-tuning, prompt tuning, IA3 |
| causal language modeling,因果语言建模 | gpt-neox-20b | lora, prefix tuning, p-tuning, prompt tuning, IA3 |
| causal language modeling,因果语言建模 | bloom | lora, prefix tuning, p-tuning, prompt tuning, IA3 |
| causal language modeling,因果语言建模 | opt | lora, prefix tuning, p-tuning, prompt tuning, IA3 |
| causal language modeling,因果语言建模 | llama | lora, prefix tuning, p-tuning, prompt tuning, IA3 |
| causal language modeling,因果语言建模 | chatglm | lora, prefix tuning, p-tuning, prompt tuning, IA3 |
| causal language modeling,因果语言建模 | mistral | lora |
| conditional generation,条件生成 | t5 | lora, prefix tuning, p-tuning, prompt tuning, IA3 |
| conditional generation,条件生成 | bart | lora, prefix tuning, p-tuning, prompt tuning, IA3 |
| sequence classification,序列分类 | bert | lora, prefix tuning, p-tuning, prompt tuning, IA3 |
| sequence classification,序列分类 | roberta | lora, prefix tuning, p-tuning, prompt tuning, IA3 |
| sequence classification,序列分类 | deberta | lora, p-tuning, prompt tuning |
| sequence classification,序列分类 | deberta-v2 | lora, p-tuning, prompt tuning |
| sequence classification,序列分类 | gpt-2 | lora, prefix tuning, p-tuning, prompt tuning |
| sequence classification,序列分类 | gpt-neo | lora, prefix tuning, p-tuning, prompt tuning |
| sequence classification,序列分类 | gpt-j | lora, prefix tuning, p-tuning, prompt tuning |
| sequence classification,序列分类 | bloom | lora, prefix tuning, p-tuning, prompt tuning |
| sequence classification,序列分类 | opt | lora, prefix tuning, p-tuning, prompt tuning |
| token classification,令牌分类 | bert | lora, prefix tuning |
| token classification,令牌分类 | roberta | lora, prefix tuning |
| token classification,令牌分类 | deberta | lora |
| token classification,令牌分类 | deberta-v2 | lora |
| token classification,令牌分类 | gpt-2 | lora, prefix tuning |
| token classification,令牌分类 | gpt-neo | lora, prefix tuning |
| token classification,令牌分类 | gpt-j | lora, prefix tuning |
| token classification,令牌分类 | bloom | lora, prefix tuning |
| token classification,令牌分类 | opt | lora, prefix tuning |
| text-to-image,文生图 | stable diffusion | lora, loha, lokr |
| image classification,图像分类 | vit | lora |
| image classification,图像分类 | swin | lora |
| image-to-text,图生文 | blip-2 | lora |
| semantic segmentation,语义分割 | segformer | lora |
快速教程
传统的范式是为每个下游任务微调模型的所有参数,但由于常见的大语言模型中的参数量庞大,全部参数进行微调并不现实。
训练较少量的参数或使用低秩适应(LoRA)等方法来减少训练参数的数量会更有效。
a. 加载微调模型
每个 PEFT 方法都由一个 PeftConfig 类定义,该类存储了用于构建 PeftModel 的所有重要参数。
示例:使用 LoRA 进行训练,创建 LoraConfig 类并指定以下参数,并进行加载:
task_type:要训练的任务(在本例中为 Sequence-to-Sequence 语言建模)inference_mode:是否使用模型进行推理r:低秩矩阵的维度lora_alpha:低秩矩阵的比例因子lora_dropout:LoRA 层的 dropout 概率
# 创建PeftConfig对象
from peft import LoraConfig, TaskType
peft_config = LoraConfig(task_type=TaskType.SEQ_2_SEQ_LM, inference_mode=False, r=8, lora_alpha=32, lora_dropout=0.1)
# 加载要微调的模型
from transformers import AutoModelForSeq2SeqLM
model = AutoModelForSeq2SeqLM.from_pretrained("bigscience/mt0-large")
# 使用get_peft_model()函数包装要微调的模型,结合PeftConfig对象,创建PeftModel
from peft import get_peft_model
model = get_peft_model(model, peft_config)
model.print_trainable_parameters()
# 输出:可训练参数的数量只占原模型的0.19%
"output: trainable params: 2359296 || all params: 1231940608 || trainable%: 0.19151053100118282"
b. 训练
# 使用Transformers的Trainer、Accelerate或PyTorch训练模型
# 创建TrainingArguments对象
training_args = TrainingArguments(
output_dir="your-name/bigscience/mt0-large-lora",
learning_rate=1e-3,
per_device_train_batch_size=32,
per_device_eval_batch_size=32,
num_train_epochs=2,
weight_decay=0.01,
eval_strategy="epoch",
save_strategy="epoch",
load_best_model_at_end=True,
)
# 将模型、训练参数、数据集、分词器和任何其他必要的组件传递给 Trainer,然后调用 train 开始训练。
trainer = Trainer(
model=model,
args=training_args,
train_dataset=tokenized_datasets["train"],
eval_dataset=tokenized_datasets["test"],
tokenizer=tokenizer,
data_collator=data_collator,
compute_metrics=compute_metrics,
)
trainer.train()
c. 保存模型
# 保存模型
model.save_pretrained("output_dir")
# 推送至Hub仓库
from huggingface_hub import notebook_login
notebook_login()
model.push_to_hub("your-name/bigscience/mt0-large-lora")
d. 推理
# 使用AutoPeftModel类和from_pretrained方法加载PEFT训练的模型
from peft import AutoPeftModelForCausalLM
from transformers import AutoTokenizer
import torch
model = AutoPeftModelForCausalLM.from_pretrained("ybelkada/opt-350m-lora")
tokenizer = AutoTokenizer.from_pretrained("facebook/opt-350m")
model = model.to("cuda")
model.eval()
inputs = tokenizer("Preheat the oven to 350 degrees and place the cookie dough", return_tensors="pt")
outputs = model.generate(input_ids=inputs["input_ids"].to("cuda"), max_new_tokens=50)
print(tokenizer.batch_decode(outputs.detach().cpu().numpy(), skip_special_tokens=True)[0])
"Preheat the oven to 350 degrees and place the cookie dough in the center of the oven. In a large bowl, combine the flour, baking powder, baking soda, salt, and cinnamon. In a separate bowl, combine the egg yolks, sugar, and vanilla."
如果未明确指定下游任务,可以使用 AutoPeftModel 类加载无头模型:
from peft import AutoPeftModel
model = AutoPeftModel.from_pretrained("smangrul/openai-whisper-large-v2-LORA-colab")
步骤总结:
- 为 PEFT 方法准备
PeftConfig对象 - 使用
get_peft_model()方法,使用PeftConfig对象和预训练模型,创建PeftModel对象
安装
a. 通过 pip 安装 PEFT
pip install peft
b. 通过源码安装
pip install git+https://github.com/huggingface/peft
c. 通过源码安装(测试版)
git clone https://github.com/huggingface/peft
cd peft
pip install -e .[test]
配置
PEFT 配置完成后,可以使用多种训练框架完成后续微调工作。
PEFT 方法类型分为:soft prompting(软提示), matrix decomposition(矩阵分解), adapters(适配器),用于减少可训练参数量。
a. PEFT 配置:指定微调使用的 PEFT 方法
示例 1:通过 PeftConfig 进行配置
LoRA 参数配置如下:
{
"base_model_name_or_path": "facebook/opt-350m",
"bias": "none",
"fan_in_fan_out": false,
"inference_mode": true,
"init_lora_weights": true,
"layers_pattern": null,
"layers_to_transform": null,
"lora_alpha": 32,
"lora_dropout": 0.05,
"modules_to_save": null,
"peft_type": "LORA",
"r": 16,
"revision": null,
"target_modules": [
"q_proj",
"v_proj"
],
"task_type": "CAUSAL_LM"
}
其中:
base_model_name_or_path: base model to apply LoRA topeft_type: PEFT method typetarget_modules: model modules to apply LoRA to (query and value projection layers)task_type: type of task to train model on
初始化 LoraConfig 创建训练配置:
from peft import LoraConfig, TaskType
lora_config = LoraConfig(
r=16,
target_modules=["q_proj", "v_proj"],
task_type=TaskType.CAUSAL_LM,
lora_alpha=32,
lora_dropout=0.05
)
示例 2:通过 PromptEncoderConfig 创建配置
p-tuning 参数配置如下:
{
"base_model_name_or_path": "roberta-large",
"encoder_dropout": 0.0,
"encoder_hidden_size": 128,
"encoder_num_layers": 2,
"encoder_reparameterization_type": "MLP",
"inference_mode": true,
"num_attention_heads": 16,
"num_layers": 24,
"num_transformer_submodules": 1,
"num_virtual_tokens": 20,
"peft_type": "P_TUNING",
"task_type": "SEQ_CLS",
"token_dim": 1024
}
其中:
base_model_name_or_path: base model to apply p-tuning topeft_type: PEFT method typetask_type: type of task to train model on
初始化 PromptEncoderConfig 创建训练配置:
from peft import PromptEncoderConfig, TaskType
p_tuning_config = PromptEncoderConfig(
encoder_reparameterization_type="MLP",
encoder_hidden_size=128,
num_attention_heads=16,
num_layers=24,
num_transformer_submodules=1,
num_virtual_tokens=20,
token_dim=1024,
task_type=TaskType.SEQ_CLS
)
b. PEFT 模型
有了 PEFT 配置,可以用于任何预训练模型以创建 PeftModel:
# 加载预训练模型
from transformers import AutoModelForCausalLM
model = AutoModelForCausalLM.from_pretrained("facebook/opt-350m")
# 使用get_peft_model()函数,创建微调模型
from peft import get_peft_model
lora_model = get_peft_model(model, lora_config)
lora_model.print_trainable_parameters()
# 输出:查看可训练的参数量,占比0.47%
"trainable params: 1,572,864 || all params: 332,769,280 || trainable%: 0.472659014678278"
注意:使用
get_peft_model()方法,会就地修改基础模型,如果需要应用新的配置参数,必须先执行unload()卸载模型,或者重新初始化模型,确保加载的预训练模型是全新未修改的状态。
使用 Transformers 的 Trainer 训练模型:
# 使用Transformers的Trainer、Accelerate或PyTorch训练模型
# 创建TrainingArguments对象
training_args = TrainingArguments(
output_dir="your-name/opt-350m-lora",
learning_rate=1e-3,
per_device_train_batch_size=32,
per_device_eval_batch_size=32,
num_train_epochs=2,
weight_decay=0.01,
eval_strategy="epoch",
save_strategy="epoch",
load_best_model_at_end=True,
)
# 将模型、训练参数、数据集、分词器和任何其他必要的组件传递给 Trainer,然后调用 train 开始训练。
trainer = Trainer(
model=lora_model,
args=training_args,
train_dataset=tokenized_datasets["train"],
eval_dataset=tokenized_datasets["test"],
tokenizer=tokenizer,
data_collator=data_collator,
compute_metrics=compute_metrics,
)
trainer.train()
模型保存与加载:
# 模型保存
lora_model.save_pretrained("your-name/opt-350m-lora")
# 模型推送至远程
lora_model.push_to_hub("your-name/opt-350m-lora")
# 要加载 PeftModel 进行推理,需要提供用于创建它的 PeftConfig 以及训练它的基本模型。
from peft import PeftModel, PeftConfig
config = PeftConfig.from_pretrained("ybelkada/opt-350m-lora")
model = AutoModelForCausalLM.from_pretrained(config.base_model_name_or_path)
lora_model = PeftModel.from_pretrained(model, "ybelkada/opt-350m-lora")
# 默认情况下,PeftModel 设置为推理,但如果想进一步训练适配器,可以设置 is_trainable=True。
集成
PEFT 的主要优点之一是 PEFT 方法生成的适配器文件比原始模型小得多,这使得管理和使用多个适配器变得非常容易。
只需加载指定任务进行微调的新适配器,即可将一个预训练的基础模型用于多个任务。
也可以将多个适配器与文本到图像扩散模型结合使用,以创建新的效果。
a. Diffusers 集成
Diffusers 是一个生成式 AI 库,用于使用扩散模型从文本或图像创建图像和视频。
LoRA 是一种扩散模型训练方法,可以非常快速地训练和共享扩散模型以生成新样式的图像。
Diffusers 使用 PEFT 库来管理不同的推理适配器。
加载基本模型,加载适配器,使用 load_lora_weights 方法进行推理(adapter_name 是用户自定义的适配器名称,后续方便用户调整适配器状态):
import torch
from diffusers import DiffusionPipeline
pipeline = DiffusionPipeline.from_pretrained("stabilityai/stable-diffusion-xl-base-1.0", torch_dtype=torch.float16).to("cuda")
pipeline.load_lora_weights(
"peft-internal-testing/artificialguybr__3DRedmond-V1",
weight_name="3DRedmond-3DRenderStyle-3DRenderAF.safetensors",
adapter_name="3d"
)
image = pipeline("sushi rolls shaped like kawaii cat faces").images[0]
image
加载另一个 LoRA 模型,使用 adapter_name 命名这个适配器,然后使用 set_adapters 方法,将其设置为当前活动的适配器:
pipeline.load_lora_weights(
"ostris/super-cereal-sdxl-lora",
weight_name="cereal_box_sdxl_v1.safetensors",
adapter_name="cereal"
)
pipeline.set_adapters("cereal")
image = pipeline("sushi rolls shaped like kawaii cat faces").images[0]
image
最后,调用 disable_lora 方法恢复基础模型:
pipeline.disable_lora()
b. Transformers 集成
Transformers 负责加载一系列预训练模型,PEFT 负载预训练模型微调。
加载基础预训练模型进行训练:
from transformers import AutoModelForCausalLM
model = AutoModelForCausalLM.from_pretrained("facebook/opt-350m")
添加适配器配置以指定如何调整模型参数,调用 add_adapter() 方法将配置添加到基础模型:
from peft import LoraConfig
peft_config = LoraConfig(
lora_alpha=16,
lora_dropout=0.1,
r=64,
bias="none",
task_type="CAUSAL_LM"
)
model.add_adapter(peft_config)
# 后续使用Transformers的Trainer训练模型,代码略
# 仍然以这里的facebook/opt-350m模型为例,经过微调得到新的微调模型peft-internal-testing/opt-350m-lora
加载新的微调模型进行推理,AutoModel 会使用 PEFT 将适配器权重和配置文件,加载到基本预训练模型中:
from transformers import AutoModelForCausalLM
model = AutoModelForCausalLM.from_pretrained("peft-internal-testing/opt-350m-lora")
或者使用 Pipeline 加载新的微调模型:
from transformers import pipeline
model = pipeline("text-generation", "peft-internal-testing/opt-350m-lora")
print(model("Hello World"))
对比或使用多个适配器效果:
如果需要对比或使用多个适配器效果,可以调用 add_adapter() 方法将适配器配置添加到基本模型。
注意:添加的适配器类型必须相同(例如,不能混合使用 LoRA 和 LoHa 适配器)。
from transformers import AutoModelForCausalLM
from peft import LoraConfig
model = AutoModelForCausalLM.from_pretrained("facebook/opt-350m")
model.add_adapter(lora_config_1, adapter_name="adapter_1")
# 再次调用add_adapter()方法将新适配器连接到基本模型
model.add_adapter(lora_config_2, adapter_name="adapter_2")
# 然后调用set_adapter()方法设置当前活动的适配器
model.set_adapter("adapter_1")
output = model.generate(**inputs)
print(tokenizer.decode(output_disabled[0], skip_special_tokens=True))
禁用与启用适配器:
# 如需禁用适配器,需要调用disable_adapter()方法
model.disable_adapters()
# 如需再次启用适配器,需要调用enable_adapter()方法
model.enable_adapters()
PEFT 指南
基于提示的方法
提示可以描述任务或提供希望模型学习的任务示例。
软提示方法无需手动创建这些提示,而是将可学习的参数添加到嵌入向量中,这些参数可以针对特定任务进行优化,同时保持预训练模型的参数处于冻结状态。
PEFT 库支持多种类型的提示方法(p-tuning、prefix-tuning、prompt-tuning)。
示例:展示如何使用软提示方法训练因果语言模型,判断推文是否为投诉。
安装库:
pip install -q peft transformers datasets
使用 load_dataset 加载数据集,创建 text_label 列:
from datasets import load_dataset
ds = load_dataset("ought/raft", "twitter_complaints")
classes = [k.replace("_", " ") for k in ds["train"].features["Label"].names]
ds = ds.map(
lambda x: {"text_label": [classes[label] for label in x["Label"]]},
batched=True,
num_proc=1,
)
ds["train"][0]
# 输出:
{"Tweet text": "@HMRCcustomers No this is my first job", "ID": 0, "Label": 2, "text_label": "no complaint"}
加载分词器,确定填充令牌,确定分词标签的最大长度:
from transformers import AutoTokenizer
tokenizer = AutoTokenizer.from_pretrained("bigscience/bloomz-560m")
if tokenizer.pad_token_id is None:
tokenizer.pad_token_id = tokenizer.eos_token_id
target_max_length = max([len(tokenizer(class_label)["input_ids"]) for class_label in classes])
print(target_max_length)
创建预处理函数,标记文本和标签,填充每个批次中的输入和标签,创建注意力掩码,序列截断到 max_length。将输入转换为 PyTorch 张量:
import torch
max_length = 64
def preprocess_function(examples, text_column="Tweet text", label_column="text_label"):
batch_size = len(examples[text_column])
inputs = [f"{text_column} : {x} Label : " for x in examples[text_column]]
targets = [str(x) for x in examples[label_column]]
model_inputs = tokenizer(inputs)
labels = tokenizer(targets)
classes = [k.replace("_", " ") for k in ds["train"].features["Label"].names]
for i in range(batch_size):
sample_input_ids = model_inputs["input_ids"][i]
label_input_ids = labels["input_ids"][i]
model_inputs["input_ids"][i] = [tokenizer.pad_token_id] * (max_length - len(sample_input_ids)) + sample_input_ids
model_inputs["attention_mask"][i] = [0] * (max_length - len(sample_input_ids)) + model_inputs["attention_mask"][i]
labels["input_ids"][i] = [-100] * (max_length - len(label_input_ids)) + label_input_ids
model_inputs["input_ids"][i] = torch.tensor(model_inputs["input_ids"][i][:max_length])
model_inputs["attention_mask"][i] = torch.tensor(model_inputs["attention_mask"][i][:max_length])
labels["input_ids"][i] = torch.tensor(labels["input_ids"][i][:max_length])
model_inputs["labels"] = labels["input_ids"]
return model_inputs
使用 map 函数将预处理函数应用于整个数据集,删除无关的列:
processed_ds = ds.map(
preprocess_function,
batched=True,
num_proc=1,
remove_columns=ds["train"].column_names,
load_from_cache_file=False,
desc="Running tokenizer on dataset",
)
创建训练和验证数据集,如果数据集样本位于 CPU 上,可以设置 pin_memory=True 加快训练期间向 GPU 的数据传输速度:
from torch.utils.data import DataLoader
from transformers import default_data_collator
train_ds = processed_ds["train"]
eval_ds = processed_ds["test"]
batch_size = 16
train_dataloader = DataLoader(train_ds, shuffle=True, collate_fn=default_data_collator, batch_size=batch_size, pin_memory=True)
eval_dataloader = DataLoader(eval_ds, collate_fn=default_data_collator, batch_size=batch_size, pin_memory=True)
加载预训练模型,用作软提示方法的基础模型:
from transformers import AutoModelForCausalLM
model = AutoModelForCausalLM.from_pretrained("bigscience/bloomz-560m")
PEFT 配置方式:
1. p-tuning 配置:添加一个可训练的嵌入张量,提示标记可以添加到输入序列中的任何位置:
from peft import PromptEncoderConfig, get_peft_model
peft_config = PromptEncoderConfig(task_type="CAUSAL_LM", num_virtual_tokens=20, encoder_hidden_size=128)
model = get_peft_model(model, peft_config)
model.print_trainable_parameters()
"trainable params: 300,288 || all params: 559,514,880 || trainable%: 0.05366935013417338"
2. prefix-tuning 配置:前缀优化在所有模型层中添加特定于任务的参数,这些参数由单独的前馈网络进行优化:
from peft import PrefixTuningConfig, get_peft_model
peft_config = PrefixTuningConfig(task_type="CAUSAL_LM", num_virtual_tokens=20)
model = get_peft_model(model, peft_config)
model.print_trainable_parameters()
"trainable params: 983,040 || all params: 560,197,632 || trainable%: 0.1754809274167014"
3. prompt-tuning 配置:提示调优是将所有任务表示为生成任务,将特定于任务的提示添加到输入中:
from peft import PromptTuningConfig, PromptTuningInit, get_peft_model
prompt_tuning_init_text = "Classify if the tweet is a complaint or no complaint.\n"
peft_config = PromptTuningConfig(
task_type="CAUSAL_LM",
prompt_tuning_init=PromptTuningInit.TEXT,
num_virtual_tokens=len(tokenizer(prompt_tuning_init_text)["input_ids"]),
prompt_tuning_init_text=prompt_tuning_init_text,
tokenizer_name_or_path="bigscience/bloomz-560m",
)
model = get_peft_model(model, peft_config)
model.print_trainable_parameters()
"trainable params: 8,192 || all params: 559,222,784 || trainable%: 0.0014648902430985358"
定义优化器和学习率调度器:
from transformers import get_linear_schedule_with_warmup
lr = 3e-2
num_epochs = 50
optimizer = torch.optim.AdamW(model.parameters(), lr=lr)
lr_scheduler = get_linear_schedule_with_warmup(
optimizer=optimizer,
num_warmup_steps=0,
num_training_steps=(len(train_dataloader) * num_epochs),
)
训练循环:
from tqdm import tqdm
device = "cuda"
model = model.to(device)
for epoch in range(num_epochs):
model.train()
total_loss = 0
for step, batch in enumerate(tqdm(train_dataloader)):
batch = {k: v.to(device) for k, v in batch.items()}
outputs = model(**batch)
loss = outputs.loss
total_loss += loss.detach().float()
loss.backward()
optimizer.step()
lr_scheduler.step()
optimizer.zero_grad()
model.eval()
eval_loss = 0
eval_preds = []
for step, batch in enumerate(tqdm(eval_dataloader)):
batch = {k: v.to(device) for k, v in batch.items()}
with torch.no_grad():
outputs = model(**batch)
loss = outputs.loss
eval_loss += loss.detach().float()
eval_preds.extend(
tokenizer.batch_decode(torch.argmax(outputs.logits, -1).detach().cpu().numpy(), skip_special_tokens=True)
)
eval_epoch_loss = eval_loss / len(eval_dataloader)
eval_ppl = torch.exp(eval_epoch_loss)
train_epoch_loss = total_loss / len(train_dataloader)
train_ppl = torch.exp(train_epoch_loss)
print(f"{epoch=}: {train_ppl=} {train_epoch_loss=} {eval_ppl=} {eval_epoch_loss=}")
上传模型:
from huggingface_hub import notebook_login
account = <your-hf-account-name>
peft_model_id = f"{account}/bloomz-560-m-peft-method"
model.push_to_hub(peft_model_id)
模型加载,进行推理:
from peft import AutoPeftModelForCausalLM
model = AutoPeftModelForCausalLM.from_pretrained("peft_model_id").to("cuda")
tokenizer = AutoTokenizer.from_pretrained("bigscience/bloomz-560m")
i = 15
inputs = tokenizer(f'{text_column} : {ds["test"][i]["Tweet text"]} Label : ', return_tensors="pt")
print(ds["test"][i]["Tweet text"])
"@NYTsupport i have complained a dozen times & yet my papers are still thrown FAR from my door. Why is this so hard to resolve?"
# 调用generate方法生成预测的分类标签
with torch.no_grad():
inputs = {k: v.to(device) for k, v in inputs.items()}
outputs = model.generate(input_ids=inputs["input_ids"], max_new_tokens=10)
print(tokenizer.batch_decode(outputs.detach().cpu().numpy(), skip_special_tokens=True))
"['Tweet text : @NYTsupport i have complained a dozen times & yet my papers are still thrown FAR from my door. Why is this so hard to resolve? Label : complaint']"
提示调优方法的参数说明
a. p-tuning 方法:PromptEncoderConfig 参数
| 参数名 | 类型/默认值 | 默认值 | 含义说明 | 影响范围 |
|---|---|---|---|---|
num_virtual_tokens | int | None(必填) | 虚拟提示词(virtual tokens)的数量,即连续提示向量的长度。 | 决定提示向量的维度(如 10 表示 10 个连续向量)。 |
token_dim | int | None(必填) | 每个提示向量的隐藏层维度,需与预训练模型的隐藏层维度一致(如 768)。 | 必须等于 PLM 的 hidden_size,否则会报错。 |
num_transformer_submodules | int | 自动推断(可选) | 需要插入提示的 Transformer 子模块数量(如 GPT-2 有 1 个,T5 有 2 个编码器-解码器)。 | 影响提示向量插入的层数(通常自动推断)。 |
encoder_hidden_size | int | token_dim(可选) | 提示编码器(如 LSTM/MLP)的隐藏层维度(仅当使用 PromptEncoder 时生效)。 | 若未设置,默认等于 token_dim。 |
encoder_num_layers | int | 2 | 提示编码器的层数(如 LSTM 的层数)。 | 更深的编码器可能捕获更复杂模式,但会增加参数量。 |
encoder_dropout | float | 0.0 | 提示编码器的 Dropout 率。 | 防止过拟合(仅在训练时生效)。 |
task_type | str | None(必填) | 任务类型(如 SEQ_CLS, CAUSAL_LM)。 | 决定提示插入的位置(如分类任务拼接到输入,生成任务拼接到每层注意力)。 |
prompt_tuning_init | str | ”RANDOM” | 提示向量的初始化方式:“RANDOM”:随机初始化;“TEXT”:从特定文本的嵌入初始化。 | 影响训练稳定性(文本初始化可能收敛更快)。 |
prompt_tuning_init_text | str | None(可选) | 当 prompt_tuning_init="TEXT" 时,用于初始化提示向量的文本(如 “Classify”)。 | 需与任务相关,文本会被分词并取前 num_virtual_tokens 个 token 的嵌入。 |
b. prefix-tuning 方法:PrefixTuningConfig 参数
| 参数名 | 类型 | 默认值 | 含义 | 影响范围 |
|---|---|---|---|---|
num_virtual_tokens | int | None(必填) | 前缀的虚拟 token 数量(即前缀长度 l) | 决定前缀向量的序列长度(l × hidden_size) |
encoder_hidden_size | int | None | 生成前缀向量的编码器隐藏层维度(若使用 MLP/LSTM) | 当使用编码器时,指定中间层维度 |
prefix_projection | bool | False | 是否使用投影网络(如 MLP)生成前缀向量 | True 时通过小网络生成前缀;False 直接优化原始向量 |
task_specific_params | Dict | None | 任务特定的参数配置(如不同任务的前缀长度) | 多任务学习时可定制不同任务的前缀 |
inference_mode | bool | False | 是否在推理时冻结前缀 | True 时前缀不可训练,仅用于推理 |
num_attention_heads | int | None | 注意力头的数量(需与 PLM 一致) | 确保前缀向量与模型注意力头匹配 |
num_layers | int | None | 应用前缀的 Transformer 层数 | 控制前缀插入的深度(如仅顶层或所有层) |
hidden_size | int | None | 前缀向量的隐藏层维度(需与 PLM 一致) | 通常等于 PLM 的 hidden_size(如 768) |
prefix_dropout | float | 0.0 | 前缀向量的 Dropout 率 | 防止过拟合,随机置零部分前缀 |
c. prompt-tuning 方法:PromptTuningConfig 参数
| 参数名 | 类型 | 默认值 | 含义 | 影响范围 |
|---|---|---|---|---|
peft_type | str | ”PROMPT_TUNING” | 指定 PEFT 方法类型(固定为 “PROMPT_TUNING”)。 | 全局配置标识,不可修改。 |
task_type | str | None | 任务类型(如 “SEQ_CLS”、“CAUSAL_LM”),需手动指定。 | 决定模型如何处理输入(分类、生成等)。 |
num_virtual_tokens | int | None | 连续提示(prompt)的虚拟 token 数量(如 10)。 | 提示向量的长度,影响模型输入的拼接维度。 |
token_dim | int | None | 每个虚拟 token 的嵌入维度(通常与 PLM 的 hidden_size 一致)。 | 需匹配预训练模型的隐藏层维度(如 768 for BERT-base)。 |
num_transformer_submodules | int | None | 子模块数量(如 GPT-2 为 1,T5 为 2 因编码器-解码器结构)。 | 决定提示向量插入的位置(编码器/解码器/两者)。 |
num_attention_heads | int | None | 注意力头数(需与 PLM 一致,如 12 for BERT-base)。 | 影响多头注意力中提示向量的分配方式。 |
prompt_tuning_init | str | ”RANDOM” | 提示向量初始化方式(“RANDOM” 或 “TEXT”)。 | “RANDOM”:随机初始化;“TEXT”:从具体文本的嵌入初始化(需 prompt_tuning_init_text)。 |
prompt_tuning_init_text | str | None | 用于初始化提示向量的文本(需 prompt_tuning_init="TEXT")。 | 文本会被分词并取其嵌入作为初始值(如 “Classify this sentence:”)。 |
tokenizer_name_or_path | str | None | 分词器名称/路径(仅当 prompt_tuning_init="TEXT" 时需指定)。 | 确保文本初始化时与模型的分词方式一致。 |
inference_mode | bool | False | 是否冻结整个 PLM(仅训练提示向量)。 | True:完全冻结 PLM;False:可配合其他微调方法(如 LoRA)。 |
LoRA 方法
LoRA 方法经常向大型模型中插入(通常在注意力块后)较小的可训练矩阵。
这些矩阵是 delta 权重矩阵的低秩分解,可在微调期间学习。
预训练模型的原始权重矩阵被冻结,在训练期间仅更新较小的矩阵。
这减少了可训练参数的数量,减少了内存使用和训练时间。
将权重矩阵表示为低秩分解:
- 低秩适应(LoRA)最常见
- 低秩 Hadamard 积(LoHa)
- 低秩 Kronecker 积(LoKr)
- 自适应低秩适应(AdaLoRA)
示例:展示如何使用低秩分解方法快速训练图像分类模型,已识别图像中显示的食物类别。
安装库:
pip install -q peft transformers datasets
加载数据集:
from datasets import load_dataset
ds = load_dataset("food101")
创建 label2id 和 id2label 字典将标签索引和标签进行相互转化:
labels = ds["train"].features["label"].names
label2id, id2label = dict(), dict()
for i, label in enumerate(labels):
label2id[label] = i
id2label[i] = label
id2label[2]
"baklava"
加载图像处理器:
from transformers import AutoImageProcessor
image_processor = AutoImageProcessor.from_pretrained("google/vit-base-patch16-224-in21k")
使用图像处理器准备一些数据增强和像素缩放的转换函数:
from torchvision.transforms import (
CenterCrop,
Compose,
Normalize,
RandomHorizontalFlip,
RandomResizedCrop,
Resize,
ToTensor,
)
normalize = Normalize(mean=image_processor.image_mean, std=image_processor.image_std)
train_transforms = Compose(
[
RandomResizedCrop(image_processor.size["height"]),
RandomHorizontalFlip(),
ToTensor(),
normalize,
]
)
val_transforms = Compose(
[
Resize(image_processor.size["height"]),
CenterCrop(image_processor.size["height"]),
ToTensor(),
normalize,
]
)
def preprocess_train(example_batch):
example_batch["pixel_values"] = [train_transforms(image.convert("RGB")) for image in example_batch["image"]]
return example_batch
def preprocess_val(example_batch):
example_batch["pixel_values"] = [val_transforms(image.convert("RGB")) for image in example_batch["image"]]
return example_batch
定义训练和验证数据集,使用 set_transform 函数动态切换:
train_ds = ds["train"]
val_ds = ds["validation"]
train_ds.set_transform(preprocess_train)
val_ds.set_transform(preprocess_val)
创建数据整理器,创建训练和验证数据,并将标签转换为 torch.tensor 对象:
import torch
def collate_fn(examples):
pixel_values = torch.stack([example["pixel_values"] for example in examples])
labels = torch.tensor([example["label"] for example in examples])
return {"pixel_values": pixel_values, "labels": labels}
加载模型,如果要微调已微调的检查点,可以传递 ignore_mismatched_sizes=True 参数:
from transformers import AutoModelForImageClassification, TrainingArguments, Trainer
model = AutoModelForImageClassification.from_pretrained(
"google/vit-base-patch16-224-in21k",
label2id=label2id,
id2label=id2label,
ignore_mismatched_sizes=True,
)
PEFT 的四种配置方式:
1. LoRA 配置:LoRA 将权重更新矩阵分解为两个较小的矩阵,这些低秩矩阵的大小由其 rank 或 r 决定。较高的 rank 或 r 意味着模型有较多的参数需要训练,这也意味着模型具有更多的学习能力:
from peft import LoraConfig, get_peft_model
config = LoraConfig(
r=16,
lora_alpha=16,
target_modules=["query", "value"],
lora_dropout=0.1,
bias="none",
modules_to_save=["classifier"],
)
model = get_peft_model(model, config)
model.print_trainable_parameters()
"trainable params: 667,493 || all params: 86,543,818 || trainable%: 0.7712775047664294"
2. LoHa 配置:LoHa 将权重更新矩阵分解为四个较小的矩阵,每对较小的矩阵与 Hadamard 积组合。与 LoRA 相比,这允许权重更新矩阵保持相同数量的可训练参数,但具有更高的排名(与 LoRA 的 2*r 相比,LoHA 为 r^2)。较小矩阵的大小由其秩或 r 决定:
from peft import LoHaConfig, get_peft_model
config = LoHaConfig(
r=16,
alpha=16,
target_modules=["query", "value"],
module_dropout=0.1,
modules_to_save=["classifier"],
)
model = get_peft_model(model, config)
model.print_trainable_parameters()
"trainable params: 1,257,317 || all params: 87,133,642 || trainable%: 1.4429753779831676"
3. LoKr 配置:LoKr 将权重更新矩阵表示为 Kronecker 积的分解,创建一个能够保留原始权重矩阵秩的块矩阵。较小矩阵的大小由其 rank 或 r 决定:
from peft import LoKrConfig, get_peft_model
config = LoKrConfig(
r=16,
alpha=16,
target_modules=["query", "value"],
module_dropout=0.1,
modules_to_save=["classifier"],
)
model = get_peft_model(model, config)
model.print_trainable_parameters()
"trainable params: 116,069 || all params: 87,172,042 || trainable%: 0.13314934162033282"
4. AdaLoRA 配置:AdaLoRA 通过为重要的权重矩阵分配更多参数并修剪不太重要的参数来有效管理 LoRA 参数计算。相比之下,LoRA 在所有模块之间均匀分布参数,这种算法可以控制矩阵的平均所需秩或 r,以及使用 target_module 将 AdaLoRA 应用于哪些模块:
from peft import AdaLoraConfig, get_peft_model
config = AdaLoraConfig(
r=8,
init_r=12,
tinit=200,
tfinal=1000,
deltaT=10,
target_modules=["query", "value"],
modules_to_save=["classifier"],
)
model = get_peft_model(model, config)
model.print_trainable_parameters()
"trainable params: 520,325 || all params: 87,614,722 || trainable%: 0.5938785036606062"
训练参数设置:
from transformers import TrainingArguments, Trainer
account = "stevhliu"
peft_model_id = f"{account}/google/vit-base-patch16-224-in21k-lora"
batch_size = 128
args = TrainingArguments(
peft_model_id,
remove_unused_columns=False,
eval_strategy="epoch",
save_strategy="epoch",
learning_rate=5e-3,
per_device_train_batch_size=batch_size,
gradient_accumulation_steps=4,
per_device_eval_batch_size=batch_size,
fp16=True,
num_train_epochs=5,
logging_steps=10,
load_best_model_at_end=True,
label_names=["labels"],
)
开始训练:
trainer = Trainer(
model,
args,
train_dataset=train_ds,
eval_dataset=val_ds,
tokenizer=image_processor,
data_collator=collate_fn,
)
trainer.train()
共享模型:
from huggingface_hub import notebook_login
notebook_login()
model.push_to_hub(peft_model_id)
加载模型,进行推理:
from peft import PeftConfig, PeftModel
from transformers import AutoImageProcessor
from PIL import Image
import requests
config = PeftConfig.from_pretrained("stevhliu/vit-base-patch16-224-in21k-lora")
model = AutoModelForImageClassification.from_pretrained(
config.base_model_name_or_path,
label2id=label2id,
id2label=id2label,
ignore_mismatched_sizes=True,
)
model = PeftModel.from_pretrained(model, "stevhliu/vit-base-patch16-224-in21k-lora")
url = "https://huggingface.co/datasets/sayakpaul/sample-datasets/resolve/main/beignets.jpeg"
image = Image.open(requests.get(url, stream=True).raw)
image
将图像转换为 RGB 并返回 PyTorch 张量:
encoding = image_processor(image.convert("RGB"), return_tensors="pt")
运行模型并返回预测的类:
with torch.no_grad():
outputs = model(**encoding)
logits = outputs.logits
predicted_class_idx = logits.argmax(-1).item()
print("Predicted class:", model.config.id2label[predicted_class_idx])
"Predicted class: beignets"
IA3
IA3 将模型的激活(自注意力、编码器-解码器注意力块中对键和值、位置前馈网络的中间激活)乘以三个学习向量。
这种 PEFT 方法引入的可训练参数量比 LoRA 还要少,LoRA 引入的是权重矩阵而不是向量。原始模型参数保持冻结状态,仅更新这些学习向量。
示例:展示如何使用 IA3 训练序列到序列模型,用于生成财经新闻的情绪标签。
加载数据:
from datasets import load_dataset
ds = load_dataset("financial_phrasebank", "sentences_allagree")
ds = ds["train"].train_test_split(test_size=0.1)
ds["validation"] = ds["test"]
del ds["test"]
classes = ds["train"].features["label"].names
ds = ds.map(
lambda x: {"text_label": [classes[label] for label in x["label"]]},
batched=True,
num_proc=1,
)
ds["train"][0]
{'sentence': 'It will be operated by Nokia , and supported by its Nokia NetAct network and service management system .',
'label': 1,
'text_label': 'neutral'}
加载分词器,定义预处理函数:
from transformers import AutoTokenizer
text_column = "sentence"
label_column = "text_label"
max_length = 128
tokenizer = AutoTokenizer.from_pretrained("bigscience/mt0-large")
def preprocess_function(examples):
inputs = examples[text_column]
targets = examples[label_column]
model_inputs = tokenizer(inputs, max_length=max_length, padding="max_length", truncation=True, return_tensors="pt")
labels = tokenizer(targets, max_length=3, padding="max_length", truncation=True, return_tensors="pt")
labels = labels["input_ids"]
labels[labels == tokenizer.pad_token_id] = -100
model_inputs["labels"] = labels
return model_inputs
使用 map 将预处理函数应用于整个数据集:
processed_ds = ds.map(
preprocess_function,
batched=True,
num_proc=1,
remove_columns=ds["train"].column_names,
load_from_cache_file=False,
desc="Running tokenizer on dataset",
)
创建训练和验证数据集,设置 pin_memory=True,以便在数据集样本位于 CPU 上时,在训练期间加快向 GPU 的数据传输速度:
from torch.utils.data import DataLoader
from transformers import default_data_collator
train_ds = processed_ds["train"]
eval_ds = processed_ds["validation"]
batch_size = 8
train_dataloader = DataLoader(
train_ds, shuffle=True, collate_fn=default_data_collator, batch_size=batch_size, pin_memory=True
)
eval_dataloader = DataLoader(eval_ds, collate_fn=default_data_collator, batch_size=batch_size, pin_memory=True)
PEFT 配置:创建一个 IA3Config 对象:
from peft import IA3Config, get_peft_model
peft_config = IA3Config(task_type="SEQ_2_SEQ_LM")
model = get_peft_model(model, peft_config)
model.print_trainable_parameters()
"trainable params: 282,624 || all params: 1,229,863,936 || trainable%: 0.022980103060766553"
设置优化器和学习率调度器:
import torch
from transformers import get_linear_schedule_with_warmup
lr = 8e-3
num_epochs = 3
optimizer = torch.optim.AdamW(model.parameters(), lr=lr)
lr_scheduler = get_linear_schedule_with_warmup(
optimizer=optimizer,
num_warmup_steps=0,
num_training_steps=(len(train_dataloader) * num_epochs),
)
训练循环:
from tqdm import tqdm
device = "cuda"
model = model.to(device)
for epoch in range(num_epochs):
model.train()
total_loss = 0
for step, batch in enumerate(tqdm(train_dataloader)):
batch = {k: v.to(device) for k, v in batch.items()}
outputs = model(**batch)
loss = outputs.loss
total_loss += loss.detach().float()
loss.backward()
optimizer.step()
lr_scheduler.step()
optimizer.zero_grad()
model.eval()
eval_loss = 0
eval_preds = []
for step, batch in enumerate(tqdm(eval_dataloader)):
batch = {k: v.to(device) for k, v in batch.items()}
with torch.no_grad():
outputs = model(**batch)
loss = outputs.loss
eval_loss += loss.detach().float()
eval_preds.extend(
tokenizer.batch_decode(torch.argmax(outputs.logits, -1).detach().cpu().numpy(), skip_special_tokens=True)
)
eval_epoch_loss = eval_loss / len(eval_dataloader)
eval_ppl = torch.exp(eval_epoch_loss)
train_epoch_loss = total_loss / len(train_dataloader)
train_ppl = torch.exp(train_epoch_loss)
print(f"{epoch=}: {train_ppl=} {train_epoch_loss=} {eval_ppl=} {eval_epoch_loss=}")
共享模型:
from huggingface_hub import notebook_login
account = <your-hf-account-name>
peft_model_id = f"{account}/mt0-large-ia3"
model.push_to_hub(peft_model_id)
加载模型:
from peft import AutoPeftModelForSeq2SeqLM
model = AutoPeftModelForSeq2SeqLM.from_pretrained("<your-hf-account-name>/mt0-large-ia3").to("cuda")
tokenizer = AutoTokenizer.from_pretrained("bigscience/mt0-large")
i = 15
inputs = tokenizer(ds["validation"][text_column][i], return_tensors="pt")
print(ds["validation"][text_column][i])
"The robust growth was the result of the inclusion of clothing chain Lindex in the Group in December 2007 ."
模型推理:
with torch.no_grad():
inputs = {k: v.to(device) for k, v in inputs.items()}
outputs = model.generate(input_ids=inputs["input_ids"], max_new_tokens=10)
print(tokenizer.batch_decode(outputs.detach().cpu().numpy(), skip_special_tokens=True))
['positive']
开发者指南
模型合并
模型合并是指将多个预训练模型组合成一个模型。
a. 合并方法
PEFT 进行模型合并的方法:
- TIES - TrIm、Elect 和 Merge(TIES)是合并模型的三步方法。首先,修剪冗余参数,然后将冲突的符号解析为聚合向量,最后对符号与聚合符号相同的参数进行平均。此方法考虑到某些值(冗余和符号不一致)可能会降低合并模型中的性能。
- DARE - Drop And REscale 是一种可用于为其他模型合并方法(如 TIES)做准备的方法。它的工作原理是根据丢弃率随机放置参数并重新缩放剩余参数。这有助于减少多个模型之间冗余和潜在干扰参数的数量。
示例:合并三个微调的 TinyLlama/TinyLlama-1.1B-intermediate-step-1431k-3T 模型:tinyllama_lora_nobots、tinyllama_lora_sql 和 tinyllama_lora_adcopy。
加载基本模型,使用 load_adapter() 方法加载适配器并分配名称:
from peft import PeftConfig, PeftModel
from transformers import AutoModelForCausalLM, AutoTokenizer
import torch
config = PeftConfig.from_pretrained("smangrul/tinyllama_lora_norobots")
model = AutoModelForCausalLM.from_pretrained(config.base_model_name_or_path, load_in_4bit=True, device_map="auto").eval()
tokenizer = AutoTokenizer.from_pretrained("smangrul/tinyllama_lora_norobots")
model.config.vocab_size = 32005
model.resize_token_embeddings(32005)
model = PeftModel.from_pretrained(model, "smangrul/tinyllama_lora_norobots", adapter_name="norobots")
_ = model.load_adapter("smangrul/tinyllama_lora_sql", adapter_name="sql")
_ = model.load_adapter("smangrul/tinyllama_lora_adcopy", adapter_name="adcopy")
合并方法示例:
1. 使用 TIES 方法:
adapters = ["norobots", "adcopy", "sql"]
weights = [2.0, 1.0, 1.0]
adapter_name = "merge"
density = 0.2
model.add_weighted_adapter(adapters, weights, adapter_name, combination_type="ties", density=density)
2. 使用 DARE 方法:
adapters = ["norobots", "adcopy", "sql"]
weights = [2.0, 0.3, 0.7]
adapter_name = "merge"
density = 0.2
model.add_weighted_adapter(adapters, weights, adapter_name, combination_type="dare_ties", density=density)
set_adapter() 方法将新合并的模型设置为活动模型:
model.set_adapter("merge")
现在可以将合并模型来进行对话、编写广告文案或 SQL 查询。
对话示例:
messages = [
{"role": "user", "content": "Write an essay about Generative AI."},
]
text = tokenizer.apply_chat_template(messages, add_generation_prompt=True, tokenize=False)
inputs = tokenizer(text, return_tensors="pt")
inputs = {k: v.to("cuda") for k, v in inputs.items()}
outputs = model.generate(**inputs, max_new_tokens=256, do_sample=True, top_p=0.95, temperature=0.2, repetition_penalty=1.2, eos_token_id=tokenizer.eos_token_id)
print(tokenizer.decode(outputs[0]))
编写文案示例:
messages = [
{"role": "system", "content": "Create a text ad given the following product and description."},
{"role": "user", "content": "Product: Sony PS5 PlayStation Console\nDescription: The PS5 console unleashes new gaming possibilities that you never anticipated."},
]
text = tokenizer.apply_chat_template(messages, add_generation_prompt=True, tokenize=False)
inputs = tokenizer(text, return_tensors="pt")
inputs = {k: v.to("cuda") for k, v in inputs.items()}
outputs = model.generate(**inputs, max_new_tokens=128, do_sample=True, top_p=0.95, temperature=0.2, repetition_penalty=1.2, eos_token_id=tokenizer.eos_token_id)
print(tokenizer.decode(outputs[0]))
SQL 查询示例:
text = """Table: 2-11365528-2
Columns: ['Team', 'Head Coach', 'President', 'Home Ground', 'Location']
Natural Query: Who is the Head Coach of the team whose President is Mario Volarevic?
SQL Query:"""
inputs = tokenizer(text, return_tensors="pt")
inputs = {k: v.to("cuda") for k, v in inputs.items()}
outputs = model.generate(**inputs, max_new_tokens=64, repetition_penalty=1.1, eos_token_id=tokenizer("</s>").input_ids[-1])
print(tokenizer.decode(outputs[0]))
b. IA3 模型
IA3 模型有助于适配器的线性合并。要在 IA3 模型中合并适配器,可以使用 IA3Model 类中的 add_weighted_adapter 方法。此方法类似于 LoraModel 中使用的 add_weighted_adapter 方法,主要区别在于没有 combination_type 参数。
将三个 IA3 适配器合并到模型中:
adapters = ["adapter1", "adapter2", "adapter3"]
weights = [0.4, 0.3, 0.3]
adapter_name = "merge"
model.add_weighted_adapter(adapters, weights, adapter_name)
# 将合并模型设置为活动模型
model.set_adapter("merge")
量化
量化是通过使用较少位的数据,达到减少模型占用内存、加速模型推理的目的。
量化的方法:
- 优化使用 AWQ 算法量化的模型权重
- 使用 GPTQ 算法独立量化权重矩阵的每一行
- 使用 BitSandbytes 库量化为 8 位和 4 位精度
- 使用 AQLM 算法进行量化,精度低至 2 位
注意:在量化模型后,通常不会针对下游任务对其进行进一步训练,因为由于权重和激活的精度较低,训练可能不稳定。
但是由于 PEFT 方法只添加了额外的可训练参数,因此可以在 PEFT 适配器顶部训练量化模型!
将量化与 PEFT 相结合是一种很好的策略,甚至可以在单个 GPU 上训练最大的模型。例如,QLoRA 是一种将模型量化为 4 位,然后使用 LoRA 对其进行训练的方法。此方法允许您在单个 48GB GPU 上微调 65B 参数模型。
a. 量化模型
bitsandbytes 是 Transformers 集成的量化库。可以将模型量化为 8 位或 4 位,并通过配置 BitsAndBytesConfig 类来启用许多其他选项:
- 设置
load_in_4bit=True以在加载模型时将模型量化为 4 位 - 将
bnb_4bit_quant_type="nf4"设置为对从正态分布初始化的权重使用特殊的 4 位数据类型 - 设置为
bnb_4bit_use_double_quant=True使用嵌套量化方案来量化已量化的权重 - 设置为
bnb_4bit_compute_dtype=torch.bfloat16使用 bfloat16 以加快计算速度
import torch
from transformers import BitsAndBytesConfig
config = BitsAndBytesConfig(
load_in_4bit=True,
bnb_4bit_quant_type="nf4",
bnb_4bit_use_double_quant=True,
bnb_4bit_compute_dtype=torch.bfloat16,
)
将 config 传递给 from_pretrained 方法:
from transformers import AutoModelForCausalLM
model = AutoModelForCausalLM.from_pretrained("mistralai/Mistral-7B-v0.1", quantization_config=config)
预处理量化模型:
from peft import prepare_model_for_kbit_training
model = prepare_model_for_kbit_training(model)
b. LoRAConfig
创建 LoraConfig 配置:
from peft import LoraConfig
config = LoraConfig(
r=16,
lora_alpha=8,
target_modules=["q_proj", "k_proj", "v_proj", "o_proj"],
lora_dropout=0.05,
bias="none",
task_type="CAUSAL_LM"
)
使用量化模型创建 PeftModel:
from peft import get_peft_model
model = get_peft_model(model, config)
i. LoftQ 初始化
LoftQ 初始化 LoRA 权重,以便将量化误差降至最低,并且可以在训练量化模型时提高性能。
一般来说,为了使 LoftQ 发挥最佳效果,建议以尽可能多的 LoRA 为目标层,以便应用 LoftQ。举个极端的例子,指定所有 LoRA 为目标层,通过 LoraConfig(..., target_modules="all-linear") 可能会产生最佳结果。
此外,当使用 4 位量化时,应该在量化配置中使用 nf4 作为量化类型,即 BitsAndBytesConfig(load_in_4bit=True, bnb_4bit_quant_type="nf4")。
ii. QLoRA-style 训练
QLoRA 为 transformer 架构中的所有线性层添加了可训练的权重。由于这些线性层的属性名称可能因架构而异,因此将 target_modules 设置为 "all-linear" 以将 LoRA 添加到所有线性层:
config = LoraConfig(target_modules="all-linear", ...)
c. GPTQ 量化
量化后训练,PEFT 可以同时使用 GPTQModel。
安装库:
pip install gptqmodel --no-build-isolation
from transformers import AutoModelForCausalLM, AutoTokenizer, GPTQConfig
model_id = "facebook/opt-125m"
tokenizer = AutoTokenizer.from_pretrained(model_id)
gptq_config = GPTQConfig(bits=4, group_size=128, dataset="wikitext2", tokenizer=tokenizer)
quantized_model = AutoModelForCausalLM.from_pretrained(model_id, device_map="auto", quantization_config=gptq_config)
# save quantized model
quantized_model.save_pretrained("./opt-125m-gptq")
tokenizer.save_pretrained("./opt-125m-gptq")
量化后,可以继续使用 PEFT API 对 GPTQ 模型进行后训练。
d. AQLM 量化
语言模型的加性量化(AQLM)是一种大型语言模型压缩方法。它将多个权重量化在一起,并利用它们之间的相互依赖关系。AQLM 将 8-16 个权重的组表示为多个向量代码的总和。这使得它可以将模型压缩到低至 2 位,而精度损失相当低。
由于 AQLM 量化过程的计算成本很高,因此建议使用预量化模型。可用模型的部分列表可以在官方 aqlm 存储库中找到。
这些模型支持 LoRA 适配器调整。要优化量化模型,您需要安装 aqlm 推理库:
pip install aqlm>=1.0.2
微调的 LoRA 适配器应单独保存,因为无法将它们与 AQLM 量化权重合并。
加性量化模型加载:
quantized_model = AutoModelForCausalLM.from_pretrained(
"BlackSamorez/Mixtral-8x7b-AQLM-2Bit-1x16-hf-test-dispatch",
torch_dtype="auto", device_map="auto", low_cpu_mem_usage=True,
)
peft_config = LoraConfig(...)
quantized_model = get_peft_model(quantized_model, peft_config)
e. EETQ 量化
对 EETQ 量化模型执行 LoRA 微调。EETQ 包提供了简单有效的方法来执行 8 位量化,据称比 LLM.int8() 算法更快。
量化模型配置与加载:
import torch
from transformers import EetqConfig
config = EetqConfig("int8")
from transformers import AutoModelForCausalLM
model = AutoModelForCausalLM.from_pretrained("mistralai/Mistral-7B-v0.1", quantization_config=config)
PEFT 模型配置与加载:
from peft import LoraConfig, get_peft_model
config = LoraConfig(
r=16,
lora_alpha=8,
target_modules=["q_proj", "k_proj", "v_proj", "o_proj"],
lora_dropout=0.05,
bias="none",
task_type="CAUSAL_LM"
)
model = get_peft_model(model, config)
f. HQQ 量化
使用大型机器学习模型的二次型量化(HQQ)的模型支持 LoRA 适配器调整。
使用 Transformers 加载模型:
from transformers import HqqConfig, AutoModelForCausalLM
quant_config = HqqConfig(nbits=4, group_size=64)
quantized_model = AutoModelForCausalLM.from_pretrained(save_dir_or_hfhub, device_map=device_map, quantization_config=quant_config)
peft_config = LoraConfig(...)
quantized_model = get_peft_model(quantized_model, peft_config)
g. torchao(PyTorch Architecture Optimization)PyTorch 架构优化
PEFT 对使用 torchao 进行量化的模型进行 int8 量化。
加载模型:
from peft import LoraConfig, get_peft_model
from transformers import AutoModelForCausalLM, TorchAoConfig
model_id = ...
quantization_config = TorchAoConfig(quant_type="int8_weight_only")
base_model = AutoModelForCausalLM.from_pretrained(model_id, quantization_config=quantization_config)
peft_config = LoraConfig(...)
model = get_peft_model(base_model, peft_config)
h. 支持量化的其他 PEFT 方法
除了 LoRA 之外,还有以下 PEFT 方法支持量化:
- VeRA(支持 bitsandbytes 量化)
- AdaLoRA(支持 bitsandbytes 和 GPTQ 量化)
- IA3(支持 bitsandbytes 量化)
LoRA
LoRA 是一种低秩分解方法,用于减少可训练参数的数量,从而加快了大型模型的微调速度并使用了更少的内存。
a. 初始化方法
LoRA 权重的初始化由 LoraConfig 中的参数 init_lora_weights 控制。
默认情况下,PEFT 对权重 A 使用 Kaiming-uniform 初始化 LoRA 权重,对权重 B 使用 0 初始化 LoRA 权重。
也可以传递 init_lora_weights="gaussian",这会使用高斯分布初始化权重 A,将权重 B 初始化为零:
from peft import LoraConfig
config = LoraConfig(init_lora_weights="gaussian", ...)
当调试和测试时,可以设置 init_lora_weights=False:
from peft import LoraConfig
config = LoraConfig(init_lora_weights=False, ...)
i. PiSSA
PiSSA 使用主奇异值和奇异向量初始化 LoRA 适配器。这种简单的修改使 PiSSA 能够比 LoRA 更快地收敛,并最终获得卓越的性能。此外,与 QLoRA 相比,PiSSA 减少了量化误差,从而进一步增强了性能。
将初始化方法配置为 "pissa":
from peft import LoraConfig
config = LoraConfig(init_lora_weights="pissa", ...)
或者执行快速 SVD,初始化速度更快:
lora_config = LoraConfig(init_lora_weights="pissa_niter_[number of iters]", ...)
ii. CorDA
CorDA 从权重分解构建任务感知型 LoRA 适配器,该权重分解由要学习的下游任务上下文(指令预览模式,IPM)或要维护的知识(知识保留模式,KPM)。
KPM 不仅在微调任务上取得了比 LoRA 更好的性能,而且还减轻了对预先训练的世界知识的灾难性遗忘。当不考虑保留预训练知识时,IPM 受到青睐,因为它可以进一步加速收敛并提高微调性能。
将初始化方法配置为 "corda",并指定 IPM 或 KPM 的模式以及用于收集协方差矩阵的数据集:
@torch.no_grad()
def run_model():
# Assume `model` and `dataset` is in context...
model.eval()
for batch in dataset:
model(**batch)
corda_config = CordaConfig(
corda_method="kpm",
)
lora_config = LoraConfig(
init_lora_weights="corda",
corda_config=corda_config,
)
preprocess_corda(model, lora_config, run_model=run_model)
peft_model = get_peft_model(model, lora_config)
iii. OLoRA
OLoRA 利用 QR 分解来初始化 LoRA 适配器。OLoRA 通过其 QR 分解的因子来转换模型的基本权重,即,在对权重进行任何训练之前,它会改变权重。这种方法显著提高了稳定性,加快了收敛速度,并最终实现了卓越的性能。
传递一个附加选项即可使用 OLoRA:
from peft import LoraConfig
config = LoraConfig(init_lora_weights="olora", ...)
iv. EVA
EVA 对每一层的输入激活执行 SVD,并使用右奇异向量来初始化 LoRA 权重。因此,它是一个数据驱动的初始化方案。此外,EVA 根据层的”解释方差比”(从 SVD 分析得出的指标)自适应地在层之间分配排名。
设置 init_lora_weights="eva":
from peft import LoraConfig, EvaConfig
peft_config = LoraConfig(
init_lora_weights = "eva",
eva_config = EvaConfig(rho = 2.0),
...
)
建议在 GPU 上执行 EVA 初始化,因为它要快得多。要优化 EVA 的可用内存量,您可以在 get_peft_model() 中使用 low_cpu_mem_usage 标志:
peft_model = get_peft_model(model, peft_config, low_cpu_mem_usage=True)
初始化 EVA 权重:
initialize_lora_eva_weights(peft_model, dataloader)
v. LoftQ
1) 标准方法
在量化 QLoRA 训练的基础模型时,请考虑使用 LoftQ 初始化,这已被证明可以在训练量化模型时提高性能。其理念是初始化 LoRA 权重,以便将量化误差降至最低。
一般来说,为了使 LoftQ 发挥最佳效果,建议以尽可能多的 LoRA 为目标层,因为那些未为目标的层无法应用 LoftQ。这意味着通过 LoraConfig(..., target_modules="all-linear") 很可能会产生最佳结果。此外,当使用 4 位量化时,您应该在量化配置中使用 nf4 作为量化类型,即 BitsAndBytesConfig(load_in_4bit=True, bnb_4bit_quant_type="nf4")。
2) 简便方法
应用 LoftQ 初始化的一种更简单但更受限的方法是使用 replace_lora_weights_loftq 函数。这将量化的 PEFT 模型作为输入,并将 LoRA 权重替换为 LoftQ 初始化的权重:
from peft import replace_lora_weights_loftq
from transformers import BitsAndBytesConfig
bnb_config = BitsAndBytesConfig(load_in_4bit=True, ...)
base_model = AutoModelForCausalLM.from_pretrained(..., quantization_config=bnb_config)
# note: don't pass init_lora_weights="loftq" or loftq_config!
lora_config = LoraConfig(task_type="CAUSAL_LM")
peft_model = get_peft_model(base_model, lora_config)
replace_lora_weights_loftq(peft_model)
replace_lora_weights_loftq 还允许您传递回调参数,以便更好地控制应该修改或不修改哪些图层,这可以从经验上大大改善结果。
replace_lora_weights_loftq 仅实现 LoftQ 的一个迭代步骤。这意味着仅更新 LoRA 权重,而不是迭代更新 LoRA 权重和量化的基本模型权重。这可能会导致性能降低,但其优点是我们可以使用从基本模型派生的原始量化权重,而不必保留修改后的量化权重的额外副本。这种权衡是否值得取决于用例。
目前,replace_lora_weights_loftq 具有以下其他限制:
- 模型文件必须存储为 safetensors 文件。
- 仅支持 bitsandbytes 4 位量化。
vi. 排序稳定的 LoRA(Rank-stabilized LoRA)
初始化 LoraConfig 的另一种方法是使用排序稳定的 LoRA(rsLoRA)方法。LoRA 体系结构在每次正向传递期间通过一个固定标量缩放每个适配器,该标量在初始化时设置,并取决于等级 r。标量在原始实现中由 lora_alpha/r 给出,但 rsLoRA 使用 lora_alpha/math.sqrt(r) 来稳定适配器并增加使用更高 r 的性能潜力。
from peft import LoraConfig
config = LoraConfig(use_rslora=True, ...)
vii. 权重分解低秩适配(Weight-Decomposed Low-Rank Adaptation,DoRA)
该技术将权重的更新分解为两部分,即大小和方向。方向由普通 LoRA 处理,而幅度由单独的可学习参数处理。这可以提高 LoRA 的性能,尤其是在低等级时。
from peft import LoraConfig
config = LoraConfig(use_dora=True, ...)
如果模型的某些部分或 DoRA 适配器被卸载到 CPU,则可以通过在 config.runtime_config 中使用 ephemeral_gpu_offload=True 来获得显著的加速,但代价是一些临时(短暂的)VRAM 开销:
from peft import LoraConfig, LoraRuntimeConfig
config = LoraConfig(use_dora=True, runtime_config=LoraRuntimeConfig(ephemeral_gpu_offload=True), ...)
带有 DoRA 适配器的 PeftModel 也可以使用 from_pretrained 方法和 load_adapter 方法加载 ephemeral_gpu_offload=True 标志:
from peft import PeftModel
model = PeftModel.from_pretrained(base_model, peft_model_id, ephemeral_gpu_offload=True)
viii. QLoRA-style 训练
PEFT 中的默认 LoRA 设置将可训练权重添加到每个注意力块的 query 和 value 层。但是 QLoRA 将可训练权重添加到 transformer 模型的所有线性层,可以提供与完全微调模型相同的性能。要将 LoRA 应用于所有线性层,就像在 QLoRA 中一样,请设置 target_modules="all-linear"(比按名称指定单个模块更容易,名称可能因架构而异)。
config = LoraConfig(target_modules="all-linear", ...)
ix. 使用 LoRA 实现内存高效的层复制
用于提高模型性能的一种方法是通过复制模型中的层来扩展模型,以从给定大小的预训练模型构建更大的模型。例如,将 7B 模型增加到 10B 模型,如 SOLAR 论文中所述。PEFT LoRA 以内存高效的方式支持这种扩展,支持在层复制后使用附加到层的 LoRA 适配器进行进一步微调。复制的层不会占用额外的内存,因为它们共享底层权重,因此唯一需要的额外内存是适配器权重的内存。要使用此功能,您需要使用 layer_replication 参数创建配置。
config = LoraConfig(layer_replication=[[0,4], [2,5]], ...)
假设原始模型有 5 个图层 [0, 1, 2, 3, 4],这将创建一个具有 7 个图层的模型,排列为 [0, 1, 2, 3, 3, 4]。这遵循 mergekit 传递合并约定,其中指定为 start inclusive 和 end exclusive 元组的层序列被堆叠以构建最终模型。最终模型中的每个层都有自己独特的 LoRA 适配器集。
x. 对排名和 Alpha 的精细控制(缩放)
默认情况下,所有以 LoRA 为目标的图层将具有相同的等级 r 和相同的 lora_alpha(这决定了 LoRA 缩放),具体取决于 LoraConfig 中指定的内容。但是,在相同情况下,您可能希望为不同的图层指示不同的值。这可以通过将 rank_pattern 和 alpha_pattern 参数传递给 LoraConfig 来实现。这些参数应该是字典,键是图层名称,值是 rank/alpha 值。键可以是正则表达式(regex)。rank_pattern 和 alpha_pattern 中未明确提及的所有 LoRA 图层都将采用默认的 r 和 lora_alpha 值。
假设有模型结构如下:
print(model)
# 输出
Outer(
(foo): Linear(...)
(module): Middle(
(foo): Linear(...)
(foobar): Linear(...)
(module): Inner(
(foo): Linear(...)
(barfoo): Linear(...)
)
)
)
说明:
rank_pattern={"foo": 42}将精确匹配所有 3 个foo层。foobar和barfoo都不匹配。rank_pattern={"^foo": 42}将只匹配模型的foo层,但module.foo和module.module.foo都不会匹配。这是因为在使用正则表达式时^表示”字符串的开头”,只有foo以 “foo” 开头,其他层名称都有前缀。rank_pattern={"^module.foo": 42}出于同样的原因,仅匹配module.foo,但不匹配module.module.foo。rank_pattern={"module.foo": 42}匹配module.foo和module.module.foo,但不匹配foo。rank_pattern={"^foo": 42, "^module.module.foo": 55}分别匹配foo和module.module.foo,但不匹配module.foo。- 无需指示
$来标记结束,因为这是由 PEFT 自动添加的。
b. 优化器
LoRA 训练可以选择包括特殊用途的优化器。目前唯一的此类优化器是 LoRA+。
i. LoRA+:优化的 LoRA
可以使用 LoRA+ 进行优化,LoRA+ 对适配器矩阵 A 和 B 使用不同的学习率,显示可以将微调速度提高多达 2 倍,将性能提高 1-2%。
from peft import LoraConfig, get_peft_model
from peft.optimizers import create_loraplus_optimizer
from transformers import Trainer
import bitsandbytes as bnb
base_model = ...
config = LoraConfig(...)
model = get_peft_model(base_model, config)
optimizer = create_loraplus_optimizer(
model=model,
optimizer_cls=bnb.optim.Adam8bit,
lr=5e-5,
loraplus_lr_ratio=16,
)
scheduler = None
...
trainer = Trainer(
...,
optimizers=(optimizer, scheduler),
)
c. 使用 LoRA 高效训练 token
有时,不仅需要更改某些层的权重,还需要添加新的令牌。对于较大的模型,这可能是一项耗费内存的工作。PEFT LoRA 适配器支持 trainable_token_indices 参数,该参数允许使用 LoRA 微调其他令牌以及微调特定层。此方法仅训练您指定的令牌,并保持所有其他令牌不变。这样可以节省内存,并且不会丢弃现有 token 嵌入的学习上下文。
# for layer 'embed_tokens'
config = LoraConfig(trainable_token_indices=[idx_1, idx_2, ...], ...)
# specific embedding layer
config = LoraConfig(trainable_token_indices={'emb_tokens': [idx_1, idx_2, ...]}, ...)
展示如何向模型添加新标记,以及如何将其与模型中的其他层一起训练:
from transformers import AutoTokenizer, AutoModelForCausalLM
from peft import get_peft_model, LoraConfig
base_model = AutoModelForCausalLM.from_pretrained("mistralai/Mistral-7B-v0.1")
tokenizer = AutoTokenizer.from_pretrained("mistralai/Mistral-7B-v0.1")
# we define our new tokens and add them to the tokenizer as special tokens
special_tokens = ['<|start_think|>', '<|stop_think|>']
tokenizer.add_special_tokens({'additional_special_tokens': special_tokens})
# make room for new tokens in the embedding matrix if it isn't big enough already
base_model.resize_token_embeddings(max(len(tokenizer), base_model.model.embed_tokens.num_embeddings))
# typical LoRA config with `trainable_token_indices` targeting embedding layer `embed_tokens`
# and specifically our new tokens we just added
lora_config = LoraConfig(
target_modules='all-linear',
trainable_token_indices={'embed_tokens': tokenizer.convert_tokens_to_ids(special_tokens)},
)
peft_model = get_peft_model(base_model, lora_config)
# proceed to train the model like normal
[...]
d. 将 LoRA 权重合并到基础模型中
虽然 LoRA 的训练体积明显更小且速度更快,但由于单独加载基础模型和 LoRA 适配器,您可能会在推理过程中遇到延迟问题。要消除延迟,请使用 merge_and_unload() 函数将适配器权重与基本模型合并。这允许您将新合并的模型用作独立模型。merge_and_unload() 函数不会将适配器权重保留在内存中。
LoRA 适配器合并:
from transformers import AutoModelForCausalLM
from peft import PeftModel
base_model = AutoModelForCausalLM.from_pretrained("mistralai/Mistral-7B-v0.1")
peft_model_id = "alignment-handbook/zephyr-7b-sft-lora"
model = PeftModel.from_pretrained(base_model, peft_model_id)
model.merge_and_unload()
如果需要保留权重的副本,以便以后可以取消合并适配器或删除并加载不同的权重,则应改用 merge_adapter() 函数。可以选择使用 unmerge_adapter() 返回基本模型:
from transformers import AutoModelForCausalLM
from peft import PeftModel
base_model = AutoModelForCausalLM.from_pretrained("mistralai/Mistral-7B-v0.1")
peft_model_id = "alignment-handbook/zephyr-7b-sft-lora"
model = PeftModel.from_pretrained(base_model, peft_model_id)
model.merge_adapter()
# unmerge the LoRA layers from the base model
model.unmerge_adapter()
add_weighted_adapter() 函数可用于根据 weights 参数中提供的用户提供的加权方案将多个 LoRA 合并到新适配器中。
加载模型:
from transformers import AutoModelForCausalLM
from peft import PeftModel
import torch
base_model = AutoModelForCausalLM.from_pretrained(
"mistralai/Mistral-7B-v0.1", torch_dtype=torch.float16, device_map="auto"
)
加载适配器,两个适配器进行合并:
peft_model_id = "alignment-handbook/zephyr-7b-sft-lora"
model = PeftModel.from_pretrained(base_model, peft_model_id, adapter_name="sft")
weighted_adapter_name = "sft-dpo"
model.load_adapter("alignment-handbook/zephyr-7b-dpo-lora", adapter_name="dpo")
model.add_weighted_adapter(
adapters=["sft", "dpo"],
weights=[0.7, 0.3],
adapter_name=weighted_adapter_name,
combination_type="linear"
)
model.set_adapter(weighted_adapter_name)
执行推理:
tokenizer = AutoTokenizer.from_pretrained("mistralai/Mistral-7B-v0.1")
prompt = "Hey, are you conscious? Can you talk to me?"
inputs = tokenizer(prompt, return_tensors="pt")
inputs = {k: v.to("cuda") for k, v in inputs.items()}
with torch.no_grad():
generate_ids = model.generate(**inputs, max_length=30)
outputs = tokenizer.batch_decode(generate_ids, skip_special_tokens=True, clean_up_tokenization_spaces=False)[0]
print(outputs)
e. 加载适配器
使用 load_adapter() 将适配器加载到预训练模型上,这对于尝试权重未合并的不同适配器非常有用。使用 set_adapter() 函数设置活动适配器权重:
from transformers import AutoModelForCausalLM
from peft import PeftModel
base_model = AutoModelForCausalLM.from_pretrained("mistralai/Mistral-7B-v0.1")
peft_model_id = "alignment-handbook/zephyr-7b-sft-lora"
model = PeftModel.from_pretrained(base_model, peft_model_id)
# load different adapter
model.load_adapter("alignment-handbook/zephyr-7b-dpo-lora", adapter_name="dpo")
# set adapter as active
model.set_adapter("dpo")
返回基本模型,可以使用 unload() 卸载所有 LoRA 模块,或使用 delete_adapter() 完全删除适配器:
# unload adapter
model.unload()
# delete adapter
model.delete_adapter("dpo")
f. 在同一批次中使用不同的 LoRA 适配器进行推理
通常,每个推理批次都必须在 PEFT 中使用相同的适配器。这有时可能很烦人,因为我们可能有包含旨在与不同 LoRA 适配器一起使用的样品的批次。
例如,我们可以有一个在英语中运行良好的基本模型和另外两个 LoRA 适配器,一个用于法语,一个用于德语。通常,我们必须拆分我们的批次,以便每个批次只包含其中一种语言的样本,我们不能在同一批次中组合不同的语言。
可以使用 adapter_name 参数在同一批次中混合不同的 LoRA 适配器。
首先,让我们加载基本模型 English 和两个适配器 French 和 German:
from transformers import AutoTokenizer, AutoModelForCausalLM
from peft import PeftModel
model_id = ...
tokenizer = AutoTokenizer.from_pretrained(model_id)
model = AutoModelForCausalLM.from_pretrained(model_id)
# load the LoRA adapter for French
peft_model = PeftModel.from_pretrained(model, <path>, adapter_name="adapter_fr")
# next, load the LoRA adapter for German
peft_model.load_adapter(<path>, adapter_name="adapter_de")
我们希望在包含所有三种语言的示例上生成文本:前三个示例是英语,接下来三个是法语,后三个是德语。我们可以使用 adapter_names 参数来指定每个样本使用哪个适配器。
由于我们的基本模型用于英语,因此我们对这些样本使用特殊字符串 "__base__"。指示法语 LoRA 微调的适配器名称,在本例中为 "adapter_fr",指示德国 LoRA 微调的适配器名称,在本例中为 "adapter_de"。
这样,可以在单个批次中使用基本模型和两个适配器:
inputs = tokenizer(
[
"Hello, my dog is cute",
"Hello, my cat is awesome",
"Hello, my fish is great",
"Salut, mon chien est mignon",
"Salut, mon chat est génial",
"Salut, mon poisson est super",
"Hallo, mein Hund ist süß",
"Hallo, meine Katze ist toll",
"Hallo, mein Fisch ist großartig",
],
return_tensors="pt",
padding=True,
)
adapter_names = [
"__base__", "__base__", "__base__",
"adapter_fr", "adapter_fr", "adapter_fr",
"adapter_de", "adapter_de", "adapter_de",
]
output = peft_model.generate(**inputs, adapter_names=adapter_names, max_new_tokens=20)
请注意,这里的顺序并不重要,即批次中的样品不需要像上面的例子那样按适配器分组。我们只需要确保 adapter_names 参数与样本正确对齐。
使用此功能有一些缺点,即:
- 仅适用于推理,不适用于训练。
- 使用
with model.disable_adapter()上下文禁用适配器优先于adapter_names。 - 当某些适配器权重使用
merge_adapter方法与基本权重合并时,无法传递adapter_names。请先调用model.unmerge_adapter()取消合并所有适配器。 - 此功能目前不适用于 DoRA,因此如果您想使用它,请在
LoraConfig中设置use_dora=False。 modules_to_save功能目前仅支持 Linear、Embedding、Conv2d 和 Conv1d 类型的图层。- 使用
adapter_names进行推理会产生预期开销,尤其是在批处理中不同适配器的数量很高时。这是因为批量大小实际上减少了每个适配器的样本数。如果运行时性能是您的首要任务,请尝试增加批处理大小。 - 尽量避免在同一批次中出现大量不同的适配器,最好使用同构批次。这可以通过使用相同的适配器缓冲样本来实现,并且仅使用少量不同的适配器执行推理。
自定义模型
本节演示将 LoRA 应用于多层感知器、timm 库中的计算机视觉模型或新的 Transformers 架构。
a. 多层感知器
定义多层感知器模型:
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)
此模型中有一些线性层可以使用 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_trainable_parameters()
# prints trainable params: 56,164 || all params: 4,100,164 || trainable%: 1.369798866581922
b. TIMM 模型
安装 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" 应该可以完成这项工作。
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_trainable_parameters()
# prints trainable params: 1,064,454 || all params: 56,467,974 || trainable%: 1.88505789139876
适配器注入
使用 PEFT,您可以将可训练适配器注入任何 torch 模块,从而允许您使用适配器方法,而无需依赖 PEFT 中的建模类。
目前,PEFT 支持将 LoRA、AdaLoRA 和 IA3 注入到模型中,因为对于这些适配器,模型的就地修改就足以对其进行微调。
何时应该注入适配器:
| 时机 | 限制 |
|---|---|
| 模型就地修改,保留所有原始属性和方法 | 从 Hugging Face 手动编写 from_pretrained 和 save_pretrained 实用程序函数以保存和加载适配器 |
| 适用于任何分割模块和模式 | 不适用于 PeftModel 提供的任何实用程序方法,例如禁用和合并适配器 |
a. 创建新的 PEFT 模型
要执行适配器注入,请使用 inject_adapter_in_model() 方法。此方法接受 3 个参数,即 PEFT 配置、模型、可选的适配器名称。
如果使用不同的适配器名称多次调用 inject_adapter_in_model() 也可以将多个适配器连接到模型。
例如,要将 LoRA 适配器注入 DummyModel 模块的线性子模块:
import torch
from peft import inject_adapter_in_model, LoraConfig
class DummyModel(torch.nn.Module):
def __init__(self):
super().__init__()
self.embedding = torch.nn.Embedding(10, 10)
self.linear = torch.nn.Linear(10, 10)
self.lm_head = torch.nn.Linear(10, 10)
def forward(self, input_ids):
x = self.embedding(input_ids)
x = self.linear(x)
x = self.lm_head(x)
return x
lora_config = LoraConfig(
lora_alpha=16,
lora_dropout=0.1,
r=64,
bias="none",
target_modules=["linear"],
)
model = DummyModel()
model = inject_adapter_in_model(lora_config, model)
dummy_inputs = torch.LongTensor([[0, 1, 2, 3, 4, 5, 6, 7]])
dummy_outputs = model(dummy_inputs)
打印模型以查看适配器是否已正确注入:
DummyModel(
(embedding): Embedding(10, 10)
(linear): Linear(
in_features=10, out_features=10, bias=True
(lora_dropout): ModuleDict(
(default): Dropout(p=0.1, inplace=False)
)
(lora_A): ModuleDict(
(default): Linear(in_features=10, out_features=64, bias=False)
)
(lora_B): ModuleDict(
(default): Linear(in_features=64, out_features=10, bias=False)
)
(lora_embedding_A): ParameterDict()
(lora_embedding_B): ParameterDict()
)
(lm_head): Linear(in_features=10, out_features=10, bias=True)
)
b. 保存模型
要仅保存适配器,请使用 get_peft_model_state_dict() 函数:
from peft import get_peft_model_state_dict
peft_state_dict = get_peft_model_state_dict(model)
print(peft_state_dict)
否则,model.state_dict() 返回模型的完整 state dict。
c. 加载模型
加载保存的 state_dict 后,可以使用 set_peft_model_state_dict() 函数应用它:
from peft import set_peft_model_state_dict
model = DummyModel()
model = inject_adapter_in_model(lora_config, model)
outcome = set_peft_model_state_dict(model, peft_state_dict)
# check that there were no wrong keys
print(outcome.unexpected_keys)
如果注入适配器很慢,或者你需要加载大量的适配器,你可以使用一个优化,允许在元设备上创建一个”空”适配器,并且只在调用 set_peft_model_state_dict() 时用实际权重填充权重。
为此,请将 low_cpu_mem_usage=True 传递给 inject_adapter_in_model() 和 set_peft_model_state_dict():
model = DummyModel()
model = inject_adapter_in_model(lora_config, model, low_cpu_mem_usage=True)
print(model.linear.lora_A["default"].weight.device.type == "meta") # should be True
set_peft_model_state_dict(model, peft_state_dict, low_cpu_mem_usage=True)
print(model.linear.lora_A["default"].weight.device.type == "cpu") # should be True
混合适配器类型
通常,不能在 PEFT 中混合不同类型的适配器。例如,可以使用两个不同的 LoRA 适配器(可以具有不同的配置选项)创建 PEFT 模型,但不能将 LoRA 和 LoHa 适配器组合在一起。
但是,对于 PeftMixedModel,只要适配器类型兼容,就可以正常工作。允许混合适配器类型的主要目的是组合经过训练的适配器进行推理。虽然可以训练混合适配器模型,但这尚未经过测试,因此不建议这样做。
要将不同的适配器类型加载到 PEFT 模型中,请使用 PeftMixedModel 而不是 PeftModel:
from peft import PeftMixedModel
base_model = ... # load the base model, e.g. from transformers
# load first adapter, which will be called "default"
peft_model = PeftMixedModel.from_pretrained(base_model, <path_to_adapter1>)
peft_model.load_adapter(<path_to_adapter2>, adapter_name="other")
peft_model.set_adapter(["default", "other"])
激活两个适配器都需要 set_adapter() 方法,否则只有第一个适配器处于活动状态。您可以通过重复调用 add_adapter() 来不断添加更多适配器。
PeftMixedModel 不支持保存和加载混合适配器。适配器应该已经经过训练,加载模型需要每次都运行一个脚本。
Tips 技巧:
- 并非所有适配器类型都可以组合使用。有关兼容类型的列表,请参阅
peft.tuners.mixed.COMPATIBLE_TUNER_TYPES。如果尝试组合不兼容的适配器类型,将引发错误。 - 可以混合使用多个相同类型的适配器,这对于组合具有非常不同配置的适配器很有用。
- 如果要组合许多不同的适配器,最有效的方法是连续添加相同的适配器类型。例如,按此顺序添加 LoRA1、LoRA2、LoHa1、LoHa2,而不是 LoRA1、LoHa1、LoRA2 和 LoHa2。虽然 order 会影响输出,但本身没有最佳 order,因此最好选择最快的 order。
Accelerate 集成
DeepSpeed
DeepSpeed 是专为十亿参数的大型模型的分布式训练而设计的库。核心是零冗余优化器(ZeRO),用于跨数据并行进程对优化器状态(ZeRO-1)、优化器状态+梯度(ZeRO-2)、参数(ZeRO-3)进行分片。大大减少了内存使用量,将训练的模型规模扩展到十亿参数量级以上。为了释放更多的内存,ZeRO-Offload 通过在优化期间利用 CPU 资源来减少 GPU 计算和内存占用。
ZeRO-1 和 ZeRO-2 针对训练进行优化,ZeRO-3 针对推理进行优化。
示例:PEFT + DeepSpeed ZeRO-3,多 GPU 微调
配置:
首先运行以下命令,使用 Accelerate 创建 DeepSpeed 配置文件。--config_file 标志允许您将配置文件保存到特定位置,否则它会在 Accelerate 缓存中保存为 default_config.yaml 文件:
accelerate config --config_file deepspeed_config.yaml
系统将询问一些有关您的设置的问题,并配置以下参数。在此示例中,使用 ZeRO-3,因此请确保选择这些选项:
zero_stage: [0] Disabled, [1] optimizer state partitioning, [2] optimizer+gradient state partitioning and [3] optimizer+gradient+parameter partitioninggradient_accumulation_steps: Number of training steps to accumulate gradients before averaging and applying them. Pass the same value as you would pass via cmd argument else you will encounter mismatch error.gradient_clipping: Enable gradient clipping with value. Don’t set this as you will be passing it via cmd arguments.offload_optimizer_device: [none] Disable optimizer offloading, [cpu] offload optimizer to CPU, [nvme] offload optimizer to NVMe SSD. Only applicable with ZeRO >= Stage-2. Set this asnoneas don’t want to enable offloading.offload_param_device: [none] Disable parameter offloading, [cpu] offload parameters to CPU, [nvme] offload parameters to NVMe SSD. Only applicable with ZeRO Stage-3. Set this asnoneas don’t want to enable offloading.zero3_init_flag: Decides whether to enabledeepspeed.zero.Initfor constructing massive models. Only applicable with ZeRO Stage-3. Set this toTrue.zero3_save_16bit_model: Decides whether to save 16-bit model weights when using ZeRO Stage-3. Set this toTrue.mixed_precision:nofor FP32 training,fp16for FP16 mixed-precision training andbf16for BF16 mixed-precision training. Set this toTrue.
完成操作后,deepspeed_config.yaml 文件中包含以下内容:
compute_environment: LOCAL_MACHINE
debug: false
deepspeed_config:
deepspeed_multinode_launcher: standard
gradient_accumulation_steps: 4
offload_optimizer_device: none
offload_param_device: none
zero3_init_flag: true
zero3_save_16bit_model: true
zero_stage: 3
distributed_type: DEEPSPEED
downcast_bf16: 'no'
machine_rank: 0
main_training_function: main
mixed_precision: bf16
num_machines: 1
num_processes: 8
rdzv_backend: static
same_network: true
tpu_env: []
tpu_use_cluster: false
tpu_use_sudo: false
use_cpu: false
启动命令,创建启动脚本 run_peft_deepspeed.sh:
accelerate launch --config_file "configs/deepspeed_config.yaml" train.py \
--seed 100 \
--model_name_or_path "meta-llama/Llama-2-70b-hf" \
--dataset_name "smangrul/ultrachat-10k-chatml" \
--chat_template_format "chatml" \
--add_special_tokens False \
--append_concat_token False \
--splits "train,test" \
--max_seq_len 2048 \
--num_train_epochs 1 \
--logging_steps 5 \
--log_level "info" \
--logging_strategy "steps" \
--eval_strategy "epoch" \
--save_strategy "epoch" \
--push_to_hub \
--hub_private_repo True \
--hub_strategy "every_save" \
--bf16 True \
--packing True \
--learning_rate 1e-4 \
--lr_scheduler_type "cosine" \
--weight_decay 1e-4 \
--warmup_ratio 0.0 \
--max_grad_norm 1.0 \
--output_dir "llama-sft-lora-deepspeed" \
--per_device_train_batch_size 8 \
--per_device_eval_batch_size 8 \
--gradient_accumulation_steps 4 \
--gradient_checkpointing True \
--use_reentrant False \
--dataset_text_field "content" \
--use_flash_attn True \
--use_peft_lora True \
--lora_r 8 \
--lora_alpha 16 \
--lora_dropout 0.1 \
--lora_target_modules "all-linear" \
--use_4bit_quantization False
注意,使用 rank=8、alpha=16 的 LoRA,并针对所有线性层。我们正在传递 deepspeed 配置文件,并在 ultrachat 数据集的子集上微调 70B Llama 模型。
训练器设置:
说明:
- 由于 DeepSpeed 配置已传递,因此使用 DeepSpeed 进行分布式训练
SFTTrainer类使用 peft 配置创建 PEFT 模型- 调用
train()方法时,SFTTrainer会在内部使用 Accelerate 准备模型 - 优化器和训练器使用 DeepSpeed 配置来创建 DeepSpeed 引擎,然后进行训练
训练部分的主要代码:
trainer = SFTTrainer(
model=model,
tokenizer=tokenizer,
args=training_args,
train_dataset=train_dataset,
eval_dataset=eval_dataset,
peft_config=peft_config,
)
trainer.accelerator.print(f"{trainer.model}")
# train
checkpoint = None
if training_args.resume_from_checkpoint is not None:
checkpoint = training_args.resume_from_checkpoint
trainer.train(resume_from_checkpoint=checkpoint)
# saving final model
trainer.save_model()
内存使用情况:2 个 GPU,每个 GPU 消耗内存 64G。
示例:PEFT QLoRA + DeepSpeed ZeRO3,多 GPU 上微调大模型
项目依赖:bitsandbytes>=0.43.3、accelerate>=1.0.1、transformers>4.44.2、trl>0.11.4 和 peft>0.13.0。
使用 Accelerate 配置时,将 zero3_init_flag 设置为 True。
使用 Accelerate 生成配置文件 deepspeed_config_z3_qlora.yaml:
compute_environment: LOCAL_MACHINE
debug: false
deepspeed_config:
deepspeed_multinode_launcher: standard
offload_optimizer_device: none
offload_param_device: none
zero3_init_flag: true
zero3_save_16bit_model: true
zero_stage: 3
distributed_type: DEEPSPEED
downcast_bf16: 'no'
machine_rank: 0
main_training_function: main
mixed_precision: bf16
num_machines: 1
num_processes: 2
rdzv_backend: static
same_network: true
tpu_env: []
tpu_use_cluster: false
tpu_use_sudo: false
use_cpu: false
启动命令,使用启动脚本 run_peft_qlora_deepspeed_stage3.sh:
accelerate launch --config_file "configs/deepspeed_config_z3_qlora.yaml" train.py \
--seed 100 \
--model_name_or_path "meta-llama/Llama-2-70b-hf" \
--dataset_name "smangrul/ultrachat-10k-chatml" \
--chat_template_format "chatml" \
--add_special_tokens False \
--append_concat_token False \
--splits "train,test" \
--max_seq_len 2048 \
--num_train_epochs 1 \
--logging_steps 5 \
--log_level "info" \
--logging_strategy "steps" \
--eval_strategy "epoch" \
--save_strategy "epoch" \
--push_to_hub \
--hub_private_repo True \
--hub_strategy "every_save" \
--bf16 True \
--packing True \
--learning_rate 1e-4 \
--lr_scheduler_type "cosine" \
--weight_decay 1e-4 \
--warmup_ratio 0.0 \
--max_grad_norm 1.0 \
--output_dir "llama-sft-qlora-dsz3" \
--per_device_train_batch_size 2 \
--per_device_eval_batch_size 2 \
--gradient_accumulation_steps 2 \
--gradient_checkpointing True \
--use_reentrant True \
--dataset_text_field "content" \
--use_flash_attn True \
--use_peft_lora True \
--lora_r 8 \
--lora_alpha 16 \
--lora_dropout 0.1 \
--lora_target_modules "all-linear" \
--use_4bit_quantization True \
--use_nested_quant True \
--bnb_4bit_compute_dtype "bfloat16" \
--bnb_4bit_quant_storage_dtype "bfloat16"
请注意 bnb_4bit_quant_storage_dtype 传递的新参数,该参数表示用于打包 4 位参数的数据类型。例如,当它设置为 bfloat16 时,32/4 = 8 个 4 位参数在量化后打包在一起。
训练代码的更改:
...
bnb_config = BitsAndBytesConfig(
load_in_4bit=args.use_4bit_quantization,
bnb_4bit_quant_type=args.bnb_4bit_quant_type,
bnb_4bit_compute_dtype=compute_dtype,
bnb_4bit_use_double_quant=args.use_nested_quant,
bnb_4bit_quant_storage=quant_storage_dtype,
)
...
model = AutoModelForCausalLM.from_pretrained(
args.model_name_or_path,
quantization_config=bnb_config,
trust_remote_code=True,
attn_implementation="flash_attention_2" if args.use_flash_attn else "eager",
torch_dtype=quant_storage_dtype or torch.float32,
)
注意:AutoModelForCausalLM 的 torch_dtype 与 bnb_4bit_quant_storage 数据类型相同。其他一切都由 Trainer 和 TRL 处理。
内存使用情况:每个 GPU 消耗内存为 36.6G。
示例:PEFT + DeepSpeed ZeRO-3 + CPU 卸载,在单 GPU 上微调大模型
配置:
使用 Accelerate 创建 DeepSpeed 配置文件。--config_file 标志允许您将配置文件保存到特定位置,否则它会在 Accelerate 缓存中保存为 default_config.yaml 文件:
accelerate config --config_file ds_zero3_cpu.yaml
系统将询问一些有关设置的问题,并配置以下参数。在此示例中,将使用 ZeRO-3 和 CPU-Offload,因此请确保选择这些选项:
zero_stage: [0] Disabled, [1] optimizer state partitioning, [2] optimizer+gradient state partitioning and [3] optimizer+gradient+parameter partitioninggradient_accumulation_steps: Number of training steps to accumulate gradients before averaging and applying them.gradient_clipping: Enable gradient clipping with value.offload_optimizer_device: [none] Disable optimizer offloading, [cpu] offload optimizer to CPU, [nvme] offload optimizer to NVMe SSD. Only applicable with ZeRO >= Stage-2.offload_param_device: [none] Disable parameter offloading, [cpu] offload parameters to CPU, [nvme] offload parameters to NVMe SSD. Only applicable with ZeRO Stage-3.zero3_init_flag: Decides whether to enabledeepspeed.zero.Initfor constructing massive models. Only applicable with ZeRO Stage-3.zero3_save_16bit_model: Decides whether to save 16-bit model weights when using ZeRO Stage-3.mixed_precision:nofor FP32 training,fp16for FP16 mixed-precision training andbf16for BF16 mixed-precision training.
配置文件如下所示:zero_stage 设置为 3,offload_optimizer_device 和 offload_param_device 设置为 cpu:
compute_environment: LOCAL_MACHINE
deepspeed_config:
gradient_accumulation_steps: 1
gradient_clipping: 1.0
offload_optimizer_device: cpu
offload_param_device: cpu
zero3_init_flag: true
zero3_save_16bit_model: true
zero_stage: 3
distributed_type: DEEPSPEED
downcast_bf16: 'no'
dynamo_backend: 'NO'
fsdp_config: {}
machine_rank: 0
main_training_function: main
megatron_lm_config: {}
mixed_precision: 'no'
num_machines: 1
num_processes: 1
rdzv_backend: static
same_network: true
use_cpu: false
初始化 Accelerator、LoraConfig 对象:
def main():
accelerator = Accelerator()
model_name_or_path = "facebook/bart-large"
dataset_name = "twitter_complaints"
peft_config = LoraConfig(
task_type=TaskType.SEQ_2_SEQ_LM, inference_mode=False, r=8, lora_alpha=32, lora_dropout=0.1
)
创建 PeftModel:
model = AutoModelForSeq2SeqLM.from_pretrained(model_name_or_path)
model = get_peft_model(model, peft_config)
将训练相关的对象传递给 Accelerate:
model, train_dataloader, eval_dataloader, test_dataloader, optimizer, lr_scheduler = accelerator.prepare(
model, train_dataloader, eval_dataloader, test_dataloader, optimizer, lr_scheduler
)
检查 accelerator 中是否使用了 DeepSpeed 插件,如果插件存在,则继续检查是否使用了 ZeRO-3:
is_ds_zero_3 = False
if getattr(accelerator.state, "deepspeed_plugin", None):
is_ds_zero_3 = accelerator.state.deepspeed_plugin.zero_stage == 3
在训练循环中,通常的 loss.backward() 被 Accelerate 的 backward 替换,后者根据您的配置使用正确的 backward() 方法:
for epoch in range(num_epochs):
with TorchTracemalloc() as tracemalloc:
model.train()
total_loss = 0
for step, batch in enumerate(tqdm(train_dataloader)):
outputs = model(**batch)
loss = outputs.loss
total_loss += loss.detach().float()
accelerator.backward(loss)
optimizer.step()
lr_scheduler.step()
optimizer.zero_grad()
启动训练脚本,需要使用 --config_file 将配置文件路径传递给启动器:
accelerate launch --config_file ds_zero3_cpu.yaml examples/peft_lora_seq2seq_accelerate_ds_zero3_offload.py
输出日志:跟踪训练期间内存使用情况:
GPU Memory before entering the train : 1916
GPU Memory consumed at the end of the train (end-begin): 66
GPU Peak Memory consumed during the train (max-begin): 7488
GPU Total Peak Memory consumed during the train (max): 9404
CPU Memory before entering the train : 19411
CPU Memory consumed at the end of the train (end-begin): 0
CPU Peak Memory consumed during the train (max-begin): 0
CPU Total Peak Memory consumed during the train (max): 19411
epoch=4: train_ppl=tensor(1.0705, device='cuda:0') train_epoch_loss=tensor(0.0681, device='cuda:0')
100%|████████████████████████████████████████████████████████████████████████████| 7/7 [00:27<00:00, 3.92s/it]
GPU Memory before entering the eval : 1982
GPU Memory consumed at the end of the eval (end-begin): -66
GPU Peak Memory consumed during the eval (max-begin): 672
GPU Total Peak Memory consumed during the eval (max): 2654
CPU Memory before entering the eval : 19411
CPU Memory consumed at the end of the eval (end-begin): 0
CPU Peak Memory consumed during the eval (max-begin): 0
CPU Total Peak Memory consumed during the eval (max): 19411
accuracy=100.0
eval_preds[:10]=['no complaint', 'no complaint', 'complaint', 'complaint', 'no complaint', 'no complaint', 'no complaint', 'complaint', 'complaint', 'no complaint']
dataset['train'][label_column][:10]=['no complaint', 'no complaint', 'complaint', 'complaint', 'no complaint', 'no complaint', 'no complaint', 'complaint', 'complaint', 'no complaint']
完全分片数据并行
完全分片数据并行(FSDP)是为对高达 1T 参数的大型预训练模型进行分布式训练而开发的。FSDP 通过在数据并行进程之间对模型参数、梯度和优化器状态进行分片来实现这一点,它还可以将分片的模型参数卸载到 CPU。FSDP 提供的内存效率允许您将训练扩展到更大的批处理或模型大小。
示例:PEFT + FSDP
配置: 首先运行以下命令,使用 Accelerate 创建 FSDP 配置文件。--config_file 标志允许将配置文件保存到特定位置,否则会在 Accelerate 缓存中保存为 default_config.yaml 文件:
accelerate config --config_file fsdp_config.yaml
完成此操作后,生成配置文件 fsdp_config.yaml:
compute_environment: LOCAL_MACHINE
debug: false
distributed_type: FSDP
downcast_bf16: 'no'
fsdp_config:
fsdp_auto_wrap_policy: TRANSFORMER_BASED_WRAP
fsdp_backward_prefetch: BACKWARD_PRE
fsdp_cpu_ram_efficient_loading: true
fsdp_forward_prefetch: false
fsdp_offload_params: false
fsdp_sharding_strategy: FULL_SHARD
fsdp_state_dict_type: SHARDED_STATE_DICT
fsdp_sync_module_states: true
fsdp_use_orig_params: false
machine_rank: 0
main_training_function: main
mixed_precision: bf16
num_machines: 1
num_processes: 8
rdzv_backend: static
same_network: true
tpu_env: []
tpu_use_cluster: false
tpu_use_sudo: false
use_cpu: false
启动命令:创建 run_peft_fsdp.sh 脚本并启动微调:
accelerate launch --config_file "configs/fsdp_config.yaml" train.py \
--seed 100 \
--model_name_or_path "meta-llama/Llama-2-70b-hf" \
--dataset_name "smangrul/ultrachat-10k-chatml" \
--chat_template_format "chatml" \
--add_special_tokens False \
--append_concat_token False \
--splits "train,test" \
--max_seq_len 2048 \
--num_train_epochs 1 \
--logging_steps 5 \
--log_level "info" \
--logging_strategy "steps" \
--eval_strategy "epoch" \
--save_strategy "epoch" \
--push_to_hub \
--hub_private_repo True \
--hub_strategy "every_save" \
--bf16 True \
--packing True \
--learning_rate 1e-4 \
--lr_scheduler_type "cosine" \
--weight_decay 1e-4 \
--warmup_ratio 0.0 \
--max_grad_norm 1.0 \
--output_dir "llama-sft-lora-fsdp" \
--per_device_train_batch_size 8 \
--per_device_eval_batch_size 8 \
--gradient_accumulation_steps 4 \
--gradient_checkpointing True \
--use_reentrant False \
--dataset_text_field "content" \
--use_flash_attn True \
--use_peft_lora True \
--lora_r 8 \
--lora_alpha 16 \
--lora_dropout 0.1 \
--lora_target_modules "all-linear" \
--use_4bit_quantization False
由于 FSDP 配置已传递,因此该脚本使用 FSDP 进行分布式训练。SFTTrainer 类使用 peft 配置创建 PEFT 模型。当调用 trainer.train() 时,Trainer 会在内部使用 Accelerate 来准备模型。优化器和训练器使用 FSDP 配置来创建 FSDP 包装模型,然后进行训练。
主要代码:
# trainer
trainer = SFTTrainer(
model=model,
tokenizer=tokenizer,
args=training_args,
train_dataset=train_dataset,
eval_dataset=eval_dataset,
peft_config=peft_config,
)
trainer.accelerator.print(f"{trainer.model}")
if model_args.use_peft_lora:
# handle PEFT+FSDP case
trainer.model.print_trainable_parameters()
if getattr(trainer.accelerator.state, "fsdp_plugin", None):
from peft.utils.other import fsdp_auto_wrap_policy
fsdp_plugin = trainer.accelerator.state.fsdp_plugin
fsdp_plugin.auto_wrap_policy = fsdp_auto_wrap_policy(trainer.model)
# train
checkpoint = None
if training_args.resume_from_checkpoint is not None:
checkpoint = training_args.resume_from_checkpoint
trainer.train(resume_from_checkpoint=checkpoint)
# saving final model
if trainer.is_fsdp_enabled:
trainer.accelerator.state.fsdp_plugin.set_state_dict_type("FULL_STATE_DICT")
trainer.save_model()
目前将 FSDP 与 PEFT 一起使用时需要注意的一点是,use_orig_params 需要为 False 才能实现 GPU 内存节省。
由于 use_orig_params=False,FSDP 的自动包装策略需要更改,以便可训练和不可训练的参数分别包装。这是通过下面的代码 snippet 完成的,该代码使用 fsdp_auto_wrap_policy PEFT 的 util 函数:
if getattr(trainer.accelerator.state, "fsdp_plugin", None):
from peft.utils.other import fsdp_auto_wrap_policy
fsdp_plugin = trainer.accelerator.state.fsdp_plugin
fsdp_plugin.auto_wrap_policy = fsdp_auto_wrap_policy(trainer.model)
内存使用情况:每个 GPU 消耗内存为 72-80GB。
示例:PEFT QLoRA + FSDP,多 GPU 微调
在家训练 70b 语言模型。
依赖:bitsandbytes>=0.43.3、accelerate>=1.0.1、transformers>4.44.2、trl>0.11.4 和 peft>0.13.0。
使用 Accelerate 配置时,我们需要设置 fsdp_cpu_ram_efficient_loading=true、fsdp_use_orig_params=false 和 fsdp_offload_params=true(cpu offloading)。不使用 accelerate 启动器时,您可以交替设置环境变量 export FSDP_CPU_RAM_EFFICIENT_LOADING=true。
在这里,我们将使用 accelerate config,下面是可以在 fsdp_config_qlora.yaml 中找到的配置:
compute_environment: LOCAL_MACHINE
debug: false
distributed_type: FSDP
downcast_bf16: 'no'
fsdp_config:
fsdp_auto_wrap_policy: TRANSFORMER_BASED_WRAP
fsdp_backward_prefetch: BACKWARD_PRE
fsdp_cpu_ram_efficient_loading: true
fsdp_forward_prefetch: false
fsdp_offload_params: true
fsdp_sharding_strategy: FULL_SHARD
fsdp_state_dict_type: SHARDED_STATE_DICT
fsdp_sync_module_states: true
fsdp_use_orig_params: false
machine_rank: 0
main_training_function: main
mixed_precision: 'no'
num_machines: 1
num_processes: 2
rdzv_backend: static
same_network: true
tpu_env: []
tpu_use_cluster: false
tpu_use_sudo: false
use_cpu: false
启动命令,创建启动脚本 run_peft_qlora_fsdp.sh:
accelerate launch --config_file "configs/fsdp_config_qlora.yaml" train.py \
--seed 100 \
--model_name_or_path "meta-llama/Llama-2-70b-hf" \
--dataset_name "smangrul/ultrachat-10k-chatml" \
--chat_template_format "chatml" \
--add_special_tokens False \
--append_concat_token False \
--splits "train,test" \
--max_seq_len 2048 \
--num_train_epochs 1 \
--logging_steps 5 \
--log_level "info" \
--logging_strategy "steps" \
--eval_strategy "epoch" \
--save_strategy "epoch" \
--push_to_hub \
--hub_private_repo True \
--hub_strategy "every_save" \
--bf16 True \
--packing True \
--learning_rate 1e-4 \
--lr_scheduler_type "cosine" \
--weight_decay 1e-4 \
--warmup_ratio 0.0 \
--max_grad_norm 1.0 \
--output_dir "llama-sft-qlora-fsdp" \
--per_device_train_batch_size 2 \
--per_device_eval_batch_size 2 \
--gradient_accumulation_steps 2 \
--gradient_checkpointing True \
--use_reentrant True \
--dataset_text_field "content" \
--use_flash_attn True \
--use_peft_lora True \
--lora_r 8 \
--lora_alpha 16 \
--lora_dropout 0.1 \
--lora_target_modules "all-linear" \
--use_4bit_quantization True \
--use_nested_quant True \
--bnb_4bit_compute_dtype "bfloat16" \
--bnb_4bit_quant_storage_dtype "bfloat16"
训练代码的主要更改:
...
bnb_config = BitsAndBytesConfig(
load_in_4bit=args.use_4bit_quantization,
bnb_4bit_quant_type=args.bnb_4bit_quant_type,
bnb_4bit_compute_dtype=compute_dtype,
bnb_4bit_use_double_quant=args.use_nested_quant,
bnb_4bit_quant_storage=quant_storage_dtype,
)
...
model = AutoModelForCausalLM.from_pretrained(
args.model_name_or_path,
quantization_config=bnb_config,
trust_remote_code=True,
attn_implementation="flash_attention_2" if args.use_flash_attn else "eager",
torch_dtype=quant_storage_dtype or torch.float32,
)
内存使用情况:每个 GPU 消耗内存 19.6GB,CPU RAM 使用 107GB,禁用 CPU 卸载时,每个 GPU 内存使用 35.6GB。
概念指南
适配器
基于适配器的方法在冻结的预训练模型的注意力和全连接层之后添加额外的可训练参数,以减少内存使用并加快训练速度。
该方法因适配器而异,它可能只是一个额外的附加层,也可能将权重更新 ∆W 表示为权重矩阵的低秩分解。
无论哪种方式,适配器通常都很小,但表现出与完全微调的模型相当的性能,并且能够使用更少的资源训练更大的模型。
以下简要概述 PEFT 支持的适配器方法:
a. 低秩适应(Low-Rank Adaptation,LoRA)
LoRA 表示通过低秩分解使用两个较小矩阵(称为更新矩阵)的权重更新 ∆W,训练这些较小的更新矩阵以适应新数据,同时保持较低的可训练参数量。
原始权重矩阵保持冻结状态,不进行更新。为了产生最终结果,原始权重和额外的调整权重被组合在一起。还可以将适配器权重与基本模型合并,以消除推理延迟。
这种方法有很多优点:
- LoRA 通过大幅减少可训练参数的数量来提高微调的效率。
- 原始预训练权重保持冻结状态,这意味着您可以在它们之上构建多个轻量级且可移植的 LoRA 模型,用于各种下游任务。
- LoRA 与其他参数高效的方法正交,可以与其中许多方法结合使用。
- 使用 LoRA 微调的模型的性能与完全微调的模型的性能相当。
原则上,LoRA 可以应用于神经网络中权重矩阵的任何子集,以减少可训练参数的数量。然而,为了简单和进一步的参数效率,LoRA 通常只应用于 Transformer 模型中的注意力块。LoRA 模型中生成的可训练参数数量取决于更新矩阵的大小,这主要由秩 r 和原始权重矩阵的形状决定。
b. LoRA 专家混合(Mixture of LoRA Experts,X-LoRA)
X-LoRA 是 LoRA 的专家混合方法,它通过使用密集或稀疏门控来动态激活 LoRA 专家。LoRA 专家和基础模型在训练期间被冻结,导致参数计数较低,因为只需要训练门控层。特别是,门控层输出缩放(取决于配置)在层和令牌级别上是精细的。
此外,在推理过程中,X-LoRA 会动态激活 LoRA 适配器以调用知识并有效地混合它们:对于每个步骤,X-LoRA 要求基本模型运行两次:第一次,在没有任何 LoRA 适配器的情况下获得隐藏状态,其次,隐藏状态用于计算应用于 LoRA 适配器的缩放,然后再次运行模型。第二次运行的输出是模型步骤的结果。最终,由于双前向传递方案,X-LoRA 允许模型反思其知识,并动态地重新配置架构。
c. 低秩 Hadamard(Low-Rank Hadamard Product,LoHa)
低秩分解可能会影响性能,因为权重更新仅限于低秩空间,这可能会限制模型的表达能力。但是,您不一定希望使用更大的秩,因为它会增加可训练参数的数量。为了解决这个问题,LoHa(一种最初为计算机视觉开发的方法)被应用于扩散模型,其中生成不同图像的能力是一个重要的考虑因素。
LoHa 使用 Hadamard 乘积(元素乘积)而不是矩阵乘积。∆W 由四个较小的矩阵表示,而不是像 LoRA 中那样由两个较小的矩阵表示,并且每对这些低秩矩阵都与 Hadamard 积组合。因此,∆W 可以具有相同数量的可训练参数,但具有更高的秩和表现度。
d. 低秩 Kronecker(Low-Rank Kronecker Product,LoKr)
LoKr 与 LoRA 和 LoHa 非常相似,它也主要应用于扩散模型,但也可以将其与其他模型类型一起使用。LoKr 将矩阵乘积替换为 Kronecker 乘积。Kronecker 乘积分解会创建一个块矩阵,该矩阵保留原始权重矩阵的秩。Kronecker 乘积的另一个优点是可以通过堆叠矩阵列来矢量化。这可以加快该过程,因为避免了完全重建 ∆W。
e. 正交微调(Orthogonal Finetuning,OFT)
OFT 是一种主要侧重于在微调模型中保留预训练模型的生成性能的方法。它试图在一个层中的所有成对神经元之间保持相同的余弦相似性(超球能量),因为这可以更好地捕获神经元之间的语义信息。这意味着 OFT 更有能力保留主题,并且更适合可控生成(类似于 ControlNet)。
OFT 通过学习神经元的正交变换来保持它们之间的余弦相似性不变,从而保留超球能量。在实践中,这意味着将正交矩阵的矩阵乘积与预训练的权重矩阵相乘。但是,为了实现参数效率,正交矩阵表示为具有秩 r 块的块对角矩阵。LoRA 减少了具有低秩结构的可训练参数的数量,而 OFT 减少了具有稀疏块对角矩阵结构的可训练参数的数量。
f. 正交蝶形(Orthogonal Butterfly,BOFT)
BOFT 是一种主要侧重于在微调模型中保留预训练模型的生成性能的方法。它试图在一个层中的所有成对神经元之间保持相同的余弦相似性(超球能量),因为这可以更好地捕获神经元之间的语义信息。这意味着 BOFT 更有能力保留主题,并且更适合可控生成(类似于 ControlNet)。
BOFT 通过学习神经元的正交变换来保持它们之间的余弦相似性不变,从而保留超球能量。在实践中,这意味着将正交矩阵的矩阵乘积与预训练的权重矩阵相乘。但是,为了实现参数效率,正交矩阵表示为具有秩 r 块的块对角矩阵。LoRA 减少了具有低秩结构的可训练参数的数量,而 BOFT 减少了具有稀疏块对角矩阵结构的可训练参数的数量。
g. 自适应低秩适应(Adaptive Low-Rank Adaptation,AdaLoRA)
AdaLoRA 通过为更适合任务的重要权重矩阵分配更多参数(换句话说,更高等级的 r)并修剪不太重要的权重矩阵来管理从 LoRA 引入的参数预算。排名由类似于奇异值分解(SVD)的方法控制。∆W 使用两个正交矩阵和一个包含奇异值的对角矩阵进行参数化。这种参数化方法避免了迭代应用 SVD,因为 SVD 的计算成本很高。基于此方法,∆W 的排名将根据重要性分数进行调整。∆W 分为多个三元组,每个三元组根据其对模型性能的贡献进行评分。重要性分数低的三元组被修剪,而重要性分数高的三元组被保留以供微调。
使用 AdaLoRA 进行培训分为三个阶段:初始阶段、预算阶段和最终阶段。在初始阶段,不应用预算,因此不会触及等级。在预算阶段,应用上述过程,并根据预算重新分配 rank,旨在为更重要的 adapters 提供更多的 rank,而为不太重要的层提供更少的 rank。当到达最后阶段时,预算已经结束,等级将被重新分配,但我们可能会继续使用重新分配的等级进行一段时间的训练,以进一步提高绩效。
h. Llama-适配器(Llama-Adapter)
Llama-Adapter 是一种将 Llama 改编成指令跟随模型的方法。为了帮助模型适应指令跟踪,适配器使用 52K 指令输出数据集进行训练。
一组可学习的适应提示以输入指令标记为前缀。这些入到模型的上层,因为最好使用预训练模型的更高级别语义进行学习。作为 input 前缀的 instruction-output 标记指导适应提示生成上下文响应。
为了避免向令牌添加干扰,适配器使用零初始化 attention。最重要的是,适配器添加了一个可学习的门控因子(以零初始化),以便在训练期间逐步向模型添加信息。这可以防止新学习的指令压倒模型的预训练知识。
i. Householder Reflection Adaptation(HRA)
HRA 提供了将 LoRA 与 OFT 联系起来的新视角,这意味着它可以利用这两种策略的优势,降低参数和计算成本,同时减少训练前知识的损失。
HRA 构建了一系列可训练的 Householder 反射(HR)。由于 Householder 反射矩阵是正交矩阵,而正交矩阵的乘积也是正交矩阵,因此 HRA 满足正交微调(OFT)的理论保证。同时,通过重写公式,HRA 也可以被视为低秩微调适配器。
r 越高,参数的可训练性就越强,从而获得更大的模型容量和更好的性能。此外,由于链结构,HR 平面的正交性影响了 HRA 的容量和规律性。为了实现模型容量和规律性之间的权衡,在损失函数中添加了 HR 平面的正交正则化器。权重 λ 可以控制正则化器的强度。
j. Bone
Block-Affine Adaptation(Bone)通过将原始权重划分为多个子空间,这些子空间共享一个矩阵进行权重更新,Bone 通过要求将可训练矩阵初始化为零来简化流程,从而消除了像某些 LoRA 变体那样进行复杂初始化的需要。与 LoRA 相比,Bone 显著减少了内存使用并实现了更快的计算速度。
直观地说,Bone 中单个可训练矩阵的形状与 lora_B 一致,因此 Bone 中的 r 参数比 LoRA 中的 r 小(in_feature * r)。
Bat 是由 Bone 和”Weight Guide”组成的新结构。您可以通过设置 init_weights == "bat" 来使用它。Bat 通过使用块分组方法减少了可训练参数的数量,权重 W 的块计算有效地促进了原始权重中的信息交换,增强了微调时的数据拟合能力。该实验提到通过 b(块大小)控制可训练参数的大小,类似于 LoRA 中的 r(等级)。为了在 PEFT 内保持一致性,我们也将 b 命名为 r。
注意:Bat 的
r(b)是特殊的,要求权重 W 满足in_features % r == 0和out_features % r == 0条件。此外,当in_features == out_features且 Bone-r 等于 LoRA-r 时,Bone 的可训练参数数量仅为 LoRA 的一半。
Bat 目前存在一些问题:它比 LoRA 慢,并且需要 checkpoint 来解决由于中间值而导致的内存使用过多问题,这进一步降低了训练速度。
软提示
训练大型预训练语言模型非常耗时且计算密集。随着它们的规模不断扩大,人们对更有效的训练方法(例如提示)越来越感兴趣。Prompting 通过包含描述任务的文本提示甚至演示任务示例,为特定下游任务准备冻结的预训练模型。通过提示,您可以避免为每个下游任务完全训练单独的模型,而是使用相同的冻结预训练模型。这要容易得多,因为您可以将同一模型用于多个不同的任务,并且训练和存储较小的提示参数集比训练模型的所有参数要高效得多。
提示方法分为两类:
- 硬提示是带有离散输入标记的手动文本提示;缺点是创建一个好的提示需要付出很多努力
- 软提示是与可针对数据集优化的输入嵌入连接的可学习张量;缺点是它们不是人类可读的,因为你没有将这些”虚拟标记”与真实单词的嵌入匹配
简要概述了 PEFT 中包含的软提示方法:提示调优、前缀调优、P 调优和多任务提示调优。
提示调优(Prompt tuning):
Prompt tuning 是为 T5 模型上的文本分类任务开发的,所有下游任务都作为文本生成任务进行转换。例如,序列分类通常为文本序列分配单个类标签。通过将其强制转换为文本生成任务,将生成构成类标签的标记。提示将作为一系列标记添加到输入中。通常,模型参数是固定的,这意味着提示标记也由模型参数固定。
Prompt tuning 背后的关键思想是 prompt token 有自己的参数,这些参数是独立更新的。这意味着您可以保持预训练模型的参数处于冻结状态,并且只更新提示标记嵌入的梯度。结果与训练整个模型的传统方法相当,并且随着模型大小的增加,提示 tuning 性能会扩展。
前缀优化(Prefix tuning):
前缀优化专为 GPT 模型上的自然语言生成(NLG)任务而设计。它与 prompt tuning 非常相似;前缀优化还会在输入之前预置一系列特定于任务的向量,这些向量可以进行训练和更新,同时保持预训练模型的其余参数处于冻结状态。
主要区别在于前缀参数入到所有模型层中,而 prompt tuning 只将 prompt 参数添加到模型输入嵌入中。前缀参数也通过单独的前馈网络(FFN)进行优化,而不是直接在软提示上进行训练,因为它会导致不稳定并损害性能。更新软提示后,FFN 将被丢弃。
因此,作者发现,尽管参数少了 1000 倍,但前缀优化的性能与完全微调模型相当,并且在低数据设置中表现得更好。
P 调优(P-tuning):
P-tuning 专为自然语言理解(NLU)任务和所有语言模型而设计。它是软提示方法的另一种变体;P-tuning 还添加了一个可训练的嵌入张量,可以对其进行优化以找到更好的提示,并使用提示编码器(双向长短期记忆网络或 LSTM)来优化提示参数。不过,与前缀调整不同的是:
- 提示标记可以插入输入序列中的任何位置,并且不仅限于开头
- 提示标记仅添加到 Input,而不是将它们添加到模型的每一层
- 引入锚点标记可以提高性能,因为它们指示输入序列中组件的特征
结果表明,P-tuning 比手动制作提示更有效,并且它使类似 GPT 的模型能够在 NLU 任务上与类似 BERT 的模型竞争。
多任务提示调优(Multitask prompt tuning):
多任务提示调优(MPT)从多个任务类型的数据中学习单个提示,这些任务类型可以共享给不同的目标任务。其他现有方法为每个需要检索或聚合以适应目标任务的任务学习单独的软提示。MPT 由两个阶段组成:
- 源训练 - 对于每个任务,其软提示被分解为特定于任务的向量。将特定于任务的向量相乘以形成另一个矩阵 W,并在 W 和共享提示矩阵 P 之间使用 Hadamard 乘积来生成特定于任务的提示矩阵。特定于任务的提示被提炼成一个在所有任务之间共享的提示矩阵。此提示通过多任务训练进行训练。
- 目标适应 - 为了使单个提示适应目标任务,目标提示被初始化并表示为共享提示矩阵和特定于任务的低秩提示矩阵的 Hadamard 乘积。
上下文感知提示调优(Context-Aware Prompt Tuning,CPT):
上下文感知提示调优(CPT)旨在通过仅优化上下文嵌入来增强小样本分类。这种方法结合了上下文学习(ICL)、提示调整(PT)和对抗性优化的思想,专注于使模型适应既高效又有效。在 CPT 中,只有特定的上下文标记嵌入被优化,而模型的其余部分保持冻结状态。为了防止过拟合并保持稳定性,CPT 使用受控扰动来限制在定义范围内允许对上下文嵌入的更改。此外,为了解决近因偏差现象(上下文末尾的示例往往优先于早期示例),CPT 应用了衰减损失因子。
IA3
为了提高微调的效率,IA3(Infused Adapter by Inhibiting and Amplifying Inner Activations)使用学习到的向量重新调整内部激活。这些学习到的向量被注入到典型的基于 transformer 的架构中的 attention 和 feedforward 模块中。这些学习到的向量是微调过程中唯一可训练的参数,因此原始权重保持冻结状态。处理学习到的向量(而不是对 LoRA 等权重矩阵的学习到的低秩更新)可以使可训练参数的数量大大减少。
与 LoRA 类似,IA3 具有许多相同的优点:
- IA3 通过大幅减少可训练参数的数量来提高微调的效率。(对于 T0,IA3 模型只有大约 0.01% 的可训练参数,而即使是 LoRA 也有 > 0.1%)
- 原始的预训练权重保持冻结状态,这意味着您可以在它们之上构建多个轻量级且可移植的 IA3 模型,用于各种下游任务。
- 使用 IA3 微调的模型的性能与完全微调的模型的性能相当。
- IA3 不会增加任何推理延迟,因为适配器权重可以与基本模型合并。
原则上,IA3 可以应用于神经网络中权重矩阵的任何子集,以减少可训练参数的数量。在作者的实现之后,IA3 权重被添加到 Transformer 模型的 key、value 和 feedforward 层。具体来说,对于 transformer 模型,IA3 权重被添加到键层和值层的输出中,以及每个 transformer 模块中第二个前馈层的输入中。
给定用于注入 IA3 参数的目标层,可以根据权重矩阵的大小确定可训练参数的数量。
使用 IA3 微调模型:
- 实例化基础模型。
- 创建配置(
IA3Config),您可以在其中定义特定于 IA3 的参数。 - 用
get_peft_model()包装基本模型以获得可训练的PeftModel。 - 像通常训练基本模型一样训练
PeftModel。
IA3Config 允许通过以下参数将 IA3 应用于基础模型:
target_modules:用于应用 IA3 向量的模块(例如,注意力块)。feedforward_modules:在target_modules中被视为前馈层的模块列表。虽然学习到的向量与注意力块的输出激活相乘,但向量与经典前馈层的输入相乘。请注意,feedforward_modules必须是target_modules的子集。modules_to_save:除 IA3 层外,要设置为可训练并保存在最终检查点中的模块列表。这些通常包括为微调任务随机初始化的 model 的自定义 head。
示例用法:
# 对于序列分类的任务,初始化 Llama 模型的 IA3 配置
peft_config = IA3Config(
task_type=TaskType.SEQ_CLS, target_modules=["k_proj", "v_proj", "down_proj"], feedforward_modules=["down_proj"]
)
API 参考
- 自动 PEFT 模型
- PEFT 模型
- PEFT 类型
- 配置
- 优化
适配器
- AdaLoRA
- IA3
- Llama-Adapter
- LoHa
- LoKr
- LoRA
- X-LoRA
- LyCORIS
- Multitask Prompt Tuning
- OFT
- BOFT
- Polytropon
- P-tuning
- Prefix tuning
- Prompt tuning
- Layernorm tuning
- VeRA
- FourierFT
- VB-LoRA
- HRA
- CPT
- Bone
- Trainable Tokens