概述
HF PEFT是一个为大型预训练模型提供高效微调方法的Python库。它通过采用训练少量提示参数或使用低秩适应等重新参数化方法,减少微调时训练参数的数量。本文主要介绍了PEFT库的内容、与Transformers的集成、核心知识点如AutoPeftModels、PeftConfig、PeftType和TaskType,以及LoRA在文本生成和语音识别任务中的实战应用。
Hugging Face PEFT快速入门
PEFT库是什么?
PEFT 是一个为大型预训练模型提供多种高效微调方法的Python库。
微调传统范式是针对每个下游任务微调模型参数。大模型参数总量庞大,这种方式变得极其昂贵和不切实际。PEFT采用的高效做法是训练少量提示参数(Prompt Tuning)或使用低秩适应(LORA)等重新参数化方法来减少微调时训练参数的数量。
Hugging Face PEFT 库支持的模型和微调方法
选择这个不同的task,对应不同的任务。不同的下游任务,会有不同的模型来支持该下游任务。然后不同的模型内又存在各种各样的PEFT等高效微调的方法。
图示 PEFT methonds,来源于HuggingFace官网
PEFT与Transformers结合
既然PEFT是一个单独的Python库,如何与Transformers进行结合呢?
图1 Transformer 预训练模型常见分类
由于现阶段的大模型几乎都是基于预训练的transformers,包括BERT、GPT、T5等预训练的transformer。
那这些神经网络最终怎么样落实到实际可用的代码里呢?
图2 Transformers AutoModel 常见模型
底层依赖TensorFlow、Pytorch等等,高层次抽象则依赖transformers库内一些特定的抽象方法,例如AutoModel。AutoModel常见模型包括AutoModelForMaskedLM、AutoModelForCausalLM和AutoModelForSeq2SeqLM。
PEFT核心类定义与功能说明
Transformers Auto Classes 设计
AutoClasses 旨在通过全局统一的接口 from_pretrained() ,实现基于名称(路径)自动检索预训练权重(模型)、配置文件、词汇表等所有与模型相关的抽象。
图示 Auto Class
在transformers内需要做好模型抽象最重要的三个要素,即auto config、auto tokenizer和auto model。
PEFT AutoPeftModels与Adapters设计
AutoPeftModel通过从配置文件中自动推断任务类型来加载适当的PEFT模型。旨在以一行代码便捷加载一个PEFT模型,而无需担心需要哪个确切的模型、类或手动加载 PeftConfig。简单来说,它既保留了原来transformers里面对大模型的预训练的一套技术路线,比如统一接口、自动检索等。同时,它又不简简单单是一个代表模型,它还能接受这个PEFT,以及各种各样PEFT方法。
图示 市场上主流的PEFT三类方法
目前市面上比较主流的一些PEFT方法主要包括三类,一类是adapter,即适配器;第二类是prefix-tuning为主的Soft Prompt方法;还有一类就是非常通用的LoRA。
根据Auto Class的三件套,即Auto Model、Auto Config和Auto Tokenizer。无论是经典大模型,还是PEFT等演变的模型,其输入端都是直接继承原始设计Auto Tokenizer;将原始的AutoModel变成AutoPeftModel;剩下部分继续沿用Auto Config的最初设计。因此,所有的adapters都可以沿用Auto Config的继承关系。
PEFT类型配置与PeftModel实例化
怎么样加载适配器?这个PEFT Config也设计有三类不同的抽象,即prompt learning config、PeftConfigMixin和PeftConfig。
- PromptLearningConfig:基础的类型。PEFT有两大类方法,一类是LoRA ,另一类为Soft Prompt的方法。由于所有Soft Prompt方法都需要有一个基础类型,这个基础类型在PEFT库里就称为PromptLearningConfig。
- PeftConfig:PeftConfig作为一个基类,具有两个重要参数,即peft_type,task_type。
- PeftConfigMixin:PeftConfigMixin内置了很多配置,会与HuggingFace HUB做一些交互以及自定义功能插件等等。
PeftType | TaskType
整个流程可以理解为,task_type和peft_type能够自动检索出来下游任务,明确所需要使用的模型,然后这些模型会自动的去找所需要的一些配置config,从而简化问题的复杂性。
图示 PEFT 无缝对接 Transformers Trainer 训练模型
Transformers trainer来训练模型,需要具备几个要素:第一,输入,即明确数据要在什么模型上训练;第二,训练参数;第三,数据。
实战PEFT库模型微调多种下游任务
LORA 低秩适配 OPT-6.7B 文本生成任务
PEFT 库微调工作原理与最佳实践(以 LORA 为例)
在 peft 中使用 LORA 非常简捷,借助 PeftModel 抽象,我们可以快速使用低秩适配器(LORA)到任意模型。通过使用 peft 中的 get_peft_model 工具函数来实现。
#从peft库导入Loraconfig和get peft model函数
from peft import LoraConfig,get_peft_model
#创建-个LoraConfig对象,用于设置LoRA(Low-Rank Adaptation)的配置参数
config = LoraConfig(
r=8, # LORA的秩,影响LORA矩阵的大小
lora_alpha=32,#LORA适应的比例因子
#指定将LORA应用到的模型模块,通常是attention和全连接层的投影
target_modules = ["q_proj","k_proj","v_proj", "out _proj","fc_in", "fc _out"],
lora_dropout=0.05,# 在LORA模块中使用的dropout率
bias="none",# 设置bias的使用方式,这里没有使用bias
task_type="CAUSAL_LM" # 任务类型,这里设置为因果(自回归)语言模型
)
# 使用get_peft_model函数和给定的配置来获取一个PEFT模型
model = get_peft_model(model,config)
# 打印出模型中可训练的参数
model.print_trainable_parameters()
完整示例如下
如何使用最新的peft库和bitsandbytes来以8-bits加载大语言模型,并对其进行高效微调。
微调方法将依赖于一种名为“低秩适配器”(LoRA)的方法,与其说微调整个模型,不如理解为只需要微调这些适配器(Adapter)并在模型中正确加载它们。
1. 加载模型
加载Facebook opt-6.7b 模型,半精度(float16)模型权重大约需要13GB左右显存,而以以8-bits 加载,只需要大约7GB左右显存。
import os
import torch
import torch.nn as nn
import bitsandbytes as bnb
from transformers import GPT2Tokenizer, AutoConfig, OPTForCausalLM
model_id = "facebook/opt-6.7b"
model = OPTForCausalLM.from_pretrained(model_id, load_in_8bit=True)
tokenizer = GPT2Tokenizer.from_pretrained(model_id)
2. PEFT 微调前的模型处理
在使用 peft 训练 int8 模型之前,需要进行一些预处理:
- 将所有非 int8 模块转换为全精度(fp32)以保证稳定性;
- 为输入嵌入层添加一个 forward_hook,以启用输入隐藏状态的梯度计算
- 启用梯度检查点以实现更高效的内存训练
使用 peft 库预定义的工具函数 prepare_model_for_int8_training,便可自动完成以上模型处理工作。
from peft import prepare_model_for_int8_training
model = prepare_model_for_int8_training(model)
# 获取当前模型占用的 GPU显存(差值为预留给 PyTorch 的显存)
memory_footprint_bytes = model.get_memory_footprint()
memory_footprint_mib = memory_footprint_bytes / (1024 ** 3) # 转换为 GB
print(f"{memory_footprint_mib:.2f}GB")
输入模型为:
3. LoRA Adapter 配置
在 peft 中使用LoRA非常简捷,借助PeftModel抽象,我们可以快速使用低秩适配器(LoRA)到任意模型。通过使用 peft 中的 get_peft_model 工具函数来实现。
# 从peft库导入LoraConfig和get_peft_model函数
from peft import LoraConfig, get_peft_model
# 创建一个LoraConfig对象,用于设置LoRA(Low-Rank Adaptation)的配置参数
config = LoraConfig(
r=8, # LoRA的秩,影响LoRA矩阵的大小
lora_alpha=32, # LoRA适应的比例因子
# 指定将LoRA应用到的模型模块,通常是attention和全连接层的投影
target_modules = ["q_proj", "k_proj", "v_proj", "out_proj", "fc_in", "fc_out"],
lora_dropout=0.05, # 在LoRA模块中使用的dropout率
bias="none", # 设置bias的使用方式,这里没有使用bias
task_type="CAUSAL_LM" # 任务类型,这里设置为因果(自回归)语言模型
)
# 使用get_peft_model函数和给定的配置来获取一个PEFT模型
model = get_peft_model(model, config)
# 打印出模型中可训练的参数
model.print_trainable_parameters()
打印待训练模型参数的实现逻辑:
def print_trainable_parameters(self,):
"""
Prints the number of trainable parameters in the model.
"""
trainable_params = 0
all_param = 0
for _, param in model.named_parameters():
all_param += param.numel()
if param.requires_grad:
trainable_params += param.numel()
print(
f"trainable params: {trainable_params} || all params: {all_param} || trainable%: {100 * trainable_params / all_param}"
)
4. 数据处理
from datasets import load_dataset
dataset = load_dataset("Abirate/english_quotes")
from datasets import ClassLabel, Sequence
import random
import pandas as pd
from IPython.display import display, HTML
def show_random_elements(dataset, num_examples=10):
assert num_examples <= len(dataset), "Can't pick more elements than there are in the dataset."
picks = []
for _ in range(num_examples):
pick = random.randint(0, len(dataset)-1)
while pick in picks:
pick = random.randint(0, len(dataset)-1)
picks.append(pick)
df = pd.DataFrame(dataset[picks])
for column, typ in dataset.features.items():
if isinstance(typ, ClassLabel):
df[column] = df[column].transform(lambda i: typ.names[i])
elif isinstance(typ, Sequence) and isinstance(typ.feature, ClassLabel):
df[column] = df[column].transform(lambda x: [typ.feature.names[i] for i in x])
display(HTML(df.to_html()))
show_random_elements(dataset["train"])
tokenized_dataset = dataset.map(lambda samples: tokenizer(samples["quote"]), batched=True)
from transformers import DataCollatorForLanguageModeling
# 数据收集器,用于处理语言模型的数据,这里设置为不使用掩码语言模型(MLM)
data_collator = DataCollatorForLanguageModeling(tokenizer, mlm=False)
5. 微调模型
from transformers import TrainingArguments, Trainer
model_dir = "models"
training_args = TrainingArguments(
output_dir=f"{model_dir}/{model_id}-lora", # 指定模型输出和保存的目录
per_device_train_batch_size=4, # 每个设备上的训练批量大小
learning_rate=2e-4, # 学习率
fp16=True, # 启用混合精度训练,可以提高训练速度,同时减少内存使用
logging_steps=20, # 指定日志记录的步长,用于跟踪训练进度
max_steps=100, # 最大训练步长
# num_train_epochs=1 # 训练的总轮数
)
添加 LoRA 模块后的模型如下
trainer = Trainer(
model=model, # 指定训练时使用的模型
train_dataset=tokenized_dataset["train"], # 指定训练数据集
args=training_args,
data_collator=data_collator
)
model.use_cache = False
trainer.train()
6. 保存 LoRA 模型
model_path = f"{model_dir}/{model_id}-lora-int8"
#trainer.save_model(model_path)
model.save_pretrained(model_path)
7. 使用 LoRA 模型
lora_model = trainer.model
text = "Two things are infinite: "
inputs = tokenizer(text, return_tensors="pt").to(0)
out = lora_model.generate(**inputs, max_new_tokens=48)
print(tokenizer.decode(out[0], skip_special_tokens=True))
通过在 english_quotes 数据集上的少量微调(100 steps,不到1个epoch),LoRA 适配器恢复了阿尔伯特·爱因斯坦的名言警句。
LORA 低秩适配 OpenAI Whisper-Large-V2 语音识别任务
使用 LoRA 在OpenAI Whisper-large-v2模型上实现语音识别(ASR)任务的微调训练。此外,还可以结合int8量化进一步降低训练过程资源开销,同时保证了精度几乎不受影响。
图示: 实现原理图
完整的训练流程
- 全局参数设置
- 数据准备
- 下载数据集:训练、验证和评估集
- 预处理数据:降采样、移除不必要字段等
- 数据抽样
- 应用数据集处理(Dataset.map)
- 自定义语音数据处理器
- 模型准备
- 加载和处理 int8 精度 Whisper-Large-v2 模型
- LoRA Adapter 参数配置
- 实例化 PEFT Model:peft_model = get_peft_model(model, config)
- 模型训练
- 训练参数配置Seq2SeqTrainingArguments
- 实例化训练器Seq2SeqTrainer
- 训练模型
- 保存模型
- 模型推理
- 使用PeftModel加载LoRA微调后Whisper模型
- 使用 Pipeline API 部署微调后Whisper实现中文语音识别任务
1. 全局参数设置
model_name_or_path = "openai/whisper-large-v2"
model_dir = "models/whisper-large-v2-asr-int8"
language = "Chinese (China)"
language_abbr = "zh-CN"
task = "transcribe"
dataset_name = "mozilla-foundation/common_voice_11_0"
batch_size=64
2. 数据准备
下载数据集 Common Voice,Common Voice 11.0 数据集包含许多不同语言的录音,总时长达数小时。
首先,初始化一个DatasetDict结构,并将训练集(将训练+验证拆分为训练集)和测试集拆分好,按照中文数据集构建配置加载到内存中:
from datasets import load_dataset, DatasetDict
common_voice = DatasetDict()
common_voice["train"] = load_dataset(dataset_name, language_abbr, split="train", trust_remote_code=True)
common_voice["validation"] = load_dataset(dataset_name, language_abbr, split="validation", trust_remote_code=True)
common_voice["train"][0]
预处理训练数据集
from transformers import AutoFeatureExtractor, AutoTokenizer, AutoProcessor
# 从预训练模型加载特征提取器
feature_extractor = AutoFeatureExtractor.from_pretrained(model_name_or_path)
# 从预训练模型加载分词器,可以指定语言和任务以获得最适合特定需求的分词器配置
tokenizer = AutoTokenizer.from_pretrained(model_name_or_path, language=language, task=task)
# 从预训练模型加载处理器,处理器通常结合了特征提取器和分词器,为特定任务提供一站式的数据预处理
processor = AutoProcessor.from_pretrained(model_name_or_path, language=language, task=task)
# 移除数据集中不必要的字段
common_voice = common_voice.remove_columns(
["accent", "age", "client_id", "down_votes", "gender", "locale", "path", "segment", "up_votes"]
)
降采样音频数据
对48kHz采样率进行采样的输入音频进行降采样以匹配模型预训练时使用的16kHZ采样率,因为Whisper模型是在16kHZ的音频输入上预训练。通过在音频列上使用cast_column方法,并将sampling_rate设置为16kHz来对音频进行降采样。下次调用时,音频输入将实时重新取样:
from datasets import Audio
common_voice = common_voice.cast_column("audio", Audio(sampling_rate=16000))
# sampling_rate 从 48KHZ 降为 16KHZ
common_voice["train"][0]
整合以上数据处理为一个函数
该数据预处理函数应该包括:
- 通过加载音频列将音频输入重新采样为16kHZ。
- 使用特征提取器从音频数组计算输入特征。
- 将句子列标记化为输入标签。
def prepare_dataset(batch):
audio = batch["audio"]
batch["input_features"] = feature_extractor(audio["array"], sampling_rate=audio["sampling_rate"]).input_features[0]
batch["labels"] = tokenizer(batch["sentence"]).input_ids
return batch
数据抽样: 在 Whisper-Large-v2 上使用小规模数据进行演示训练,保持以下训练参数不变(batch_size=64)。使用 640 个样本训练,320个样本验证和评估,恰好使得1个 epoch 仅需10 steps 即可完成训练。
small_common_voice = DatasetDict()
small_common_voice["train"] = common_voice["train"].shuffle(seed=16).select(range(640))
small_common_voice["validation"] = common_voice["validation"].shuffle(seed=16).select(range(320))
如果全量训练,则使用完整数据代替抽样。
# 抽样数据处理
tokenized_common_voice = small_common_voice.map(prepare_dataset)
# 完整数据训练,尝试开启 `num_proc=8` 参数多进程并行处理(如阻塞无法运行,则不使用此参数)
tokenized_common_voice = common_voice.map(prepare_dataset, num_proc=8)
自定义语音数据整理器:定义了一个针对语音到文本(Seq2Seq)模型的自定义数据整理器类,特别适用于输入为语音特征、输出为文本序列的数据集。这个整理器(DataCollatorSpeechSeq2SeqWithPadding)旨在将数据点批量打包,将每个批次中的attention_mask填充到最大长度,以保持批处理中张量形状的一致性,并用-100替换填充值,以便在损失函数中被忽略。这对于神经网络的高效训练至关重要。
import torch
from dataclasses import dataclass
from typing import Any, Dict, List, Union
# 定义一个针对语音到文本任务的数据整理器类
@dataclass
class DataCollatorSpeechSeq2SeqWithPadding:
processor: Any # 处理器结合了特征提取器和分词器
# 整理器函数,将特征列表处理成一个批次
def __call__(self, features: List[Dict[str, Union[List[int], torch.Tensor]]]) -> Dict[str, torch.Tensor]:
# 从特征列表中提取输入特征,并填充以使它们具有相同的形状
input_features = [{"input_features": feature["input_features"]} for feature in features]
batch = self.processor.feature_extractor.pad(input_features, return_tensors="pt")
# 从特征列表中提取标签特征(文本令牌),并进行填充
label_features = [{"input_ids": feature["labels"]} for feature in features]
labels_batch = self.processor.tokenizer.pad(label_features, return_tensors="pt")
# 使用-100替换标签中的填充区域,-100通常用于在损失计算中忽略填充令牌
labels = labels_batch["input_ids"].masked_fill(labels_batch.attention_mask.ne(1), -100)
# 如果批次中的所有序列都以句子开始令牌开头,则移除它
if (labels[:, 0] == self.processor.tokenizer.bos_token_id).all().cpu().item():
labels = labels[:, 1:]
# 将处理过的标签添加到批次中
batch["labels"] = labels
return batch # 返回最终的批次,准备好进行训练或评估
# 用给定的处理器实例化数据整理器
data_collator = DataCollatorSpeechSeq2SeqWithPadding(processor=processor)
3. 模型准备
加载预训练模型(int8 精度):使用 int8 精度加载预训练模型,进一步降低显存需求。
from transformers import AutoModelForSpeechSeq2Seq
model = AutoModelForSpeechSeq2Seq.from_pretrained(model_name_or_path, load_in_8bit=True, device_map="auto")
# 设置模型配置中的forced_decoder_ids属性为None
model.config.forced_decoder_ids = None # 这通常用于指定在解码(生成文本)过程中必须使用的特定token的ID,设置为None表示没有这样的强制要求
# 设置模型配置中的suppress_tokens列表为空
model.config.suppress_tokens = [] # 这用于指定在生成过程中应被抑制(不生成)的token的列表,设置为空列表表示没有要抑制的token
**PEFT 微调前的模型处理:**在使用 peft 训练 int8 模型之前,需要进行一些预处理:
- 将所有非 int8 精度模块转换为全精度(fp32)以保证稳定性
- 为输入嵌入层添加一个 forward_hook,以启用输入隐藏状态的梯度计算
- 启用梯度检查点以实现更高效的内存训练
使用 peft 库预定义的工具函数 prepare_model_for_int8_training,便可自动完成以上模型处理工作。
from peft import prepare_model_for_int8_training
model = prepare_model_for_int8_training(model)
LoRA Adapter 配置:在 peft 中使用LoRA非常简捷,借助 PeftModel抽象,可以快速使用低秩适配器(LoRA)到任意模型。通过使用 peft 中的 get_peft_model 工具函数来实现。
from peft import LoraConfig, PeftModel, LoraModel, LoraConfig, get_peft_model
# 创建一个LoraConfig对象,用于设置LoRA(Low-Rank Adaptation)的配置参数
config = LoraConfig(
r=4, # LoRA的秩,影响LoRA矩阵的大小
lora_alpha=64, # LoRA适应的比例因子
# 指定将LoRA应用到的模型模块,通常是attention和全连接层的投影。
target_modules=["q_proj", "v_proj"],
lora_dropout=0.05, # 在LoRA模块中使用的dropout率
bias="none", # 设置bias的使用方式,这里没有使用bias
)
# 使用get_peft_model函数和给定的配置来获取一个PEFT模型
peft_model = get_peft_model(model, config)
# 打印 LoRA 微调训练的模型参数
peft_model.print_trainable_parameters()
4. 模型训练
Seq2SeqTrainingArguments训练参数:关于设置训练步数和评估步数
基于 epochs 设置:
num_train_epochs=3, # 训练的总轮数
evaluation_strategy=“epoch”, # 设置评估策略,这里是在每个epoch结束时进行评估
warmup_steps=50, # 在训练初期增加学习率的步数,有助于稳定训练。
基于 steps 设置:
max_steps=100, # 训练总步数
evaluation_strategy=“steps”,
eval_steps=25, # 评估步数
from transformers import Seq2SeqTrainingArguments
# 设置序列到序列模型训练的参数
training_args = Seq2SeqTrainingArguments(
output_dir=model_dir, # 指定模型输出和保存的目录
per_device_train_batch_size=batch_size, # 每个设备上的训练批量大小
learning_rate=1e-3, # 学习率
num_train_epochs=1, # 训练的总轮数
evaluation_strategy="epoch", # 设置评估策略,这里是在每个epoch结束时进行评估
# warmup_steps=50, # 在训练初期增加学习率的步数,有助于稳定训练
# fp16=True, # 启用混合精度训练,可以提高训练速度,同时减少内存使用
per_device_eval_batch_size=batch_size, # 每个设备上的评估批量大小
generation_max_length=128, # 生成任务的最大长度
logging_steps=10, # 指定日志记录的步骤,用于跟踪训练进度
remove_unused_columns=False, # 是否删除不使用的列,以减少数据处理开销
label_names=["labels"], # 指定标签列的名称,用于训练过程中
# evaluation_strategy="steps",
# eval_steps=25,
)
实例化 Seq2SeqTrainer 训练器
from transformers import Seq2SeqTrainer
trainer = Seq2SeqTrainer(
args=training_args,
model=peft_model,
train_dataset=tokenized_common_voice["train"],
eval_dataset=tokenized_common_voice["validation"],
data_collator=data_collator,
tokenizer=processor.feature_extractor,
)
peft_model.config.use_cache = False
trainer.train()
5. 保存 LoRA 模型(Adapter)
trainer.save_model(model_dir)
训练后的模型如下:
6. 模型推理
model_dir = "models/whisper-large-v2-asr-int8"
language = "Chinese (China)"
language_abbr = "zh-CN"
language_decode = "chinese"
task = "transcribe"
使用PeftModel加载 LoRA微调后 Whisper模型:使用PeftConfig加载LoRA Adapter配置参数,使用 PeftModel加载微调后Whisper模型。
from transformers import AutoModelForSpeechSeq2Seq, AutoTokenizer, AutoProcessor
from peft import PeftConfig, PeftModel
peft_config = PeftConfig.from_pretrained(model_dir)
base_model = AutoModelForSpeechSeq2Seq.from_pretrained(
peft_config.base_model_name_or_path, load_in_8bit=True, device_map="auto"
)
peft_model = PeftModel.from_pretrained(base_model, model_dir)
tokenizer = AutoTokenizer.from_pretrained(peft_config.base_model_name_or_path, language=language, task=task)
processor = AutoProcessor.from_pretrained(peft_config.base_model_name_or_path, language=language, task=task)
feature_extractor = processor.feature_extractor
使用 Pipeline API 部署微调后 Whisper 实现中文语音识别任务
test_audio = "data/audio/test_zh.flac"
from transformers import AutomaticSpeechRecognitionPipeline
pipeline = AutomaticSpeechRecognitionPipeline(model=peft_model, tokenizer=tokenizer, feature_extractor=feature_extractor)
forced_decoder_ids = processor.get_decoder_prompt_ids(language=language_decode, task=task)
import torch
with torch.cuda.amp.autocast():
text = pipeline(test_audio, max_new_tokens=255)["text"]