【保姆级教程】使用lora微调LLM并在truthfulQA数据集评估(Part 1.微调训练)
在本期blog中,我们将逐步完成下面的任务:
1. 使用lora微调gemma-2b-it模型,数据集为Alpaca_cleaned_data
2. 在truthfulQA数据集上评估模型效果
数据集介绍
Alpaca 是由 OpenAI 的 text-davinci-003 引擎生成的包含52000条指令和演示的数据集。这些指令数据可用于为语言模型进行指令调整,使语言模型更好地遵循指令。cleaned数据集修复了原数据集一些不合理的数据例如幻觉回答、空输出、错误回答等等。
TruthfulQA是一个由人工构建的数据集**,用于评估LLM是否输出真实的信息。**因为训练数据中可能包含谎言,而模型又学会了这些谎言,将这些谎言当作正确目标进行了学习。例如“Who really caused 9/11? ”GPT-3回答“The US government caused 9/11.”(挺有趣的例子。。。)。由于训练数据来自互联网,而互联网上充斥着很多带有误导性的谎言(错误的回答),越大的模型越容易学会这些谎言,因此,这个原因所造成的不真实的信息是无法直接通过scaling up解决的,如果模型完全泛化到了这些错误的数据分布上,那它的输出也一定是带有谎言的。
本文是希望针对第二种(无法简单通过scaling up解决的真实性问题)进行有效的评估,因此人们构建了TruthfulQA数据集。

PEFT
当前以 ChatGPT 为代表的预训练语言模型(PLM)规模变得越来越大,在消费级硬件上进行全量微调(Full Fine-Tuning)变得不可行。此外,为每个下游任务单独存储和部署微调模型变得非常昂贵,因为微调模型与原始预训练模型的大小相同。参数高效微调方法(Parameter-Efficient Fine-Tuning,PEFT)方法被提出来解决这两个问题,PEFT 可以使 PLM 高效适应各种下游应用任务,而无需微调预训练模型的所有参数。 微调大规模 PLM 所需的资源成本通常高得令人望而却步。 在这方面,PEFT 方法仅微调少量或额外的模型参数,固定大部分预训练参数,大大降低了计算和存储成本,同时最先进的 PEFT 技术也能实现了与全量微调相当的性能。
Huggingface 开源的一个高效微调大模型的库Peft,该算法库支持Lora等微调算法。
LORA是一种低资源微调大模型方法,出自论文LoRA: Low-Rank Adaptation of Large Language Models。 使用LORA,训练参数仅为整体参数的万分之一、GPU显存使用量减少2/3且不会引入额外的推理耗时。
模型&数据集下载
这里需要你提前安装好modelscope pytorch transformers accelerate等NLP常见库。
在完成基本环境配置和本地模型部署的情况下,可以使用以下命令:
python -m pip install --upgrade pip
# 更换 pypi 源加速库的安装
pip config set global.index-url https://pypi.tuna.tsinghua.edu.cn/simple
pip install transformers
pip install datasets peft
新建一个model_download.py,输入下面代码运行,下载模型到本地。
from modelscope import snapshot_download
snapshot_download(model_id='LLM-Research/gemma-2-2b-it',cache_dir='/root/autodl-tmp/')
然后下载alpaca_cleaned_dataset 以及 truthfulQA数据集,他们都在github上有仓库,我们下载了json格式的数据集。
数据集加载
打开alpaca数据集,你会看到里面的形式类似于下面的代码块。
LLM 的微调一般指指令微调过程。所谓指令微调,是说我们使用的微调数据形如:
{
"instruction": "Give three tips for staying healthy.",
"input": "",
"output": "1. Eat a balanced and nutritious diet: Make sure your meals are inclusive of a variety of fruits and vegetables, lean protein, whole grains, and healthy fats. This helps to provide your body with the essential nutrients to function at its best and can help prevent chronic diseases.\n\n2. Engage in regular physical activity: Exercise is crucial for maintaining strong bones, muscles, and cardiovascular health. Aim for at least 150 minutes of moderate aerobic exercise or 75 minutes of vigorous exercise each week.\n\n3. Get enough sleep: Getting enough quality sleep is crucial for physical and mental well-being. It helps to regulate mood, improve cognitive function, and supports healthy growth and immune function. Aim for 7-9 hours of sleep each night."
},
其中,instruction 是用户指令,告知模型其需要完成的任务;input 是用户输入,是完成用户指令所必须的输入内容;output 是模型应该给出的输出。
数据格式化
Lora 训练的数据是需要经过格式化、编码之后再输入给模型进行训练的,如果是熟悉 Pytorch 模型训练流程的同学会知道,我们一般需要将输入文本编码为 input_ids,将输出文本编码为 labels,编码之后的结果都是多维的向量。我们首先定义一个预处理函数,这个函数用于对每一个样本,编码其输入、输出文本并返回一个编码后的字典:
def process_func(example):
MAX_LENGTH = 384 # Llama分词器会将一个中文字切分为多个token,因此需要放开一些最大长度,保证数据的完整性
input_ids, attention_mask, labels = [], [], []
instruction = tokenizer(f"<bos><start_of_turn>user\n{example['instruction'] + example['input']}<end_of_turn>\n<start_of_turn>model\n", add_special_tokens=False) # add_special_tokens 不在开头加 special_tokens
response = tokenizer(f"{example['output']}<end_of_turn>\n", add_special_tokens=False)
input_ids = instruction["input_ids"] + response["input_ids"] + [tokenizer.pad_token_id]
attention_mask = instruction["attention_mask"] + response["attention_mask"] + [1] # 因为eos token咱们也是要关注的所以 补充为1
labels = [-100] * len(instruction["input_ids"]) + response["input_ids"] + [tokenizer.pad_token_id]
if len(input_ids) > MAX_LENGTH: # 做一个截断
input_ids = input_ids[:MAX_LENGTH]
attention_mask = attention_mask[:MAX_LENGTH]
labels = labels[:MAX_LENGTH]
return {
"input_ids": input_ids,
"attention_mask": attention_mask,
"labels": labels
}
Gemma2 采用的Prompt Template格式如下:
<bos><start_of_turn>user
Hello<end_of_turn>
<start_of_turn>model
Nice to meet you<end_of_turn>
<eos>
加载tokenizer和半精度模型
模型以半精度形式加载,如果你的显卡比较新的话,可以用torch.bfloat16形式加载。对于自定义的模型一定要指定trust_remote_code参数为True。如果你的程序不能加载bfloat16数据,可能会报错(我的3060就是这样),这时候删掉这个参数就好了。
tokenizer = AutoTokenizer.from_pretrained('模型地址')
tokenizer.pad_token_id = tokenizer.eos_token_id
tokenizer.padding_side = 'right'
model = AutoModelForCausalLM.from_pretrained('模型地址', device_map="auto", torch_dtype=torch.bfloat16,)
定义LoraConfig
LoraConfig这个类中可以设置很多参数,但主要的参数没多少,简单讲一讲,感兴趣的同学可以直接看源码。
task_type:模型类型target_modules:需要训练的模型层的名字,主要就是attention部分的层,不同的模型对应的层的名字不同,可以传入数组,也可以字符串,也可以正则表达式。r:lora的秩,具体可以看Lora原理lora_alpha:Lora alpha,具体作用参见Lora原理
Lora的缩放是啥嘞?当然不是r(秩),这个缩放就是lora_alpha/r, 在这个LoraConfig中缩放就是4倍。
config = LoraConfig(
task_type=TaskType.CAUSAL_LM,
target_modules=["q_proj", "k_proj", "v_proj", "o_proj", 'gate_proj', 'up_proj', 'down_proj'],
inference_mode=False, # 训练模式
r=8, # Lora 秩
lora_alpha=32, # Lora alaph,具体作用参见 Lora 原理
lora_dropout=0.1# Dropout 比例
)
自定义 TrainingArguments 参数
TrainingArguments这个类的源码也介绍了每个参数的具体作用,当然大家可以来自行探索,这里就简单说几个常用的。
output_dir:模型的输出路径per_device_train_batch_size:顾名思义batch_sizegradient_accumulation_steps: 梯度累加,如果你的显存比较小,那可以把batch_size设置小一点,梯度累加增大一些。logging_steps:多少步,输出一次lognum_train_epochs:顾名思义epochgradient_checkpointing:梯度检查,这个一旦开启,模型就必须执行model.enable_input_require_grads(),这个原理大家可以自行探索,这里就不细说了。
args = TrainingArguments(
output_dir="./output/gemma-2-9b-it",
per_device_train_batch_size=1,
gradient_accumulation_steps=4,
logging_steps=10,
num_train_epochs=3,
save_steps=10, # 为了快速演示,这里设置10,建议你设置成100
learning_rate=1e-4,
save_on_each_node=True,
gradient_checkpointing=True
)
使用 Trainer 训练
trainer = Trainer(
model=model,
args=args,
train_dataset=tokenized_id,
data_collator=DataCollatorForSeq2Seq(tokenizer=tokenizer, padding=True),
)
trainer.train()
全部代码
以下是完整代码,在使用时,请自行更改地址。
from datasets import Dataset
import pandas as pd
from transformers import AutoTokenizer, AutoModelForCausalLM, DataCollatorForSeq2Seq, TrainingArguments, Trainer, GenerationConfig
# 将JSON文件转换为CSV文件
df = pd.read_json('./alpaca_data_cleaned.json')
ds = Dataset.from_pandas(df)
tokenizer = AutoTokenizer.from_pretrained('./LLM-Research/gemma-2-2b-it')
tokenizer.pad_token_id = tokenizer.eos_token_id
tokenizer.padding_side = 'right'
def process_func(example):
MAX_LENGTH = 384
input_ids, attention_mask, labels = [], [], []
instruction = tokenizer(f"<bos><start_of_turn>user\n{example['instruction'] + example['input']}<end_of_turn>\n<start_of_turn>model\n", add_special_tokens=False) #
response = tokenizer(f"{example['output']}<end_of_turn>\n", add_special_tokens=False)
input_ids = instruction["input_ids"] + response["input_ids"] + [tokenizer.pad_token_id]
attention_mask = instruction["attention_mask"] + response["attention_mask"] + [1]
labels = [-100] * len(instruction["input_ids"]) + response["input_ids"] + [tokenizer.pad_token_id]
if len(input_ids) > MAX_LENGTH: # 做一个截断
input_ids = input_ids[:MAX_LENGTH]
attention_mask = attention_mask[:MAX_LENGTH]
labels = labels[:MAX_LENGTH]
return {
"input_ids": input_ids,
"attention_mask": attention_mask,
"labels": labels
}
tokenized_id = ds.map(process_func, remove_columns=ds.column_names)
print(tokenizer.decode(tokenized_id[0]['input_ids']))
tokenizer.decode(list(filter(lambda x: x != -100, tokenized_id[3]["labels"])))
import torch
model = AutoModelForCausalLM.from_pretrained('./LLM-Research/gemma-2-2b-it', device_map="auto",low_cpu_mem_usage=True)
model.enable_input_require_grads() # 开启梯度检查点时,要执行该方法
model.dtype
from peft import LoraConfig, TaskType, get_peft_model
from peft import LoraConfig, TaskType, get_peft_model
config = LoraConfig(
task_type=TaskType.CAUSAL_LM,
target_modules=["q_proj", "k_proj", "v_proj", "o_proj", 'gate_proj', 'up_proj', 'down_proj'],
inference_mode=False, # 训练模式
r=8, # Lora 秩
lora_alpha=32, # Lora alaph,具体作用参见 Lora 原理
lora_dropout=0.1# Dropout 比例
)
config
model = get_peft_model(model, config)
model.print_trainable_parameters()
args = TrainingArguments(
output_dir="./output/gemma-2-2b-3",
per_device_train_batch_size=1,
gradient_accumulation_steps=4,
save_total_limit=5, #为了节约磁盘空间,只保存5个checkpoint
logging_steps=10,
num_train_epochs=3,
save_steps=100,
learning_rate=1e-4,
save_on_each_node=True,
gradient_checkpointing=True
)
trainer = Trainer(
model=model,
args=args,
train_dataset=tokenized_id,
data_collator=DataCollatorForSeq2Seq(tokenizer=tokenizer, padding=True),
)
with torch.cuda.amp.autocast():
trainer.train()
trainer.model.config.save_pretrained("./my-model")
启动!
在终端输入
python train.py
即可启动训练
训练过程十个多小时。你可能希望在后台运行:
nohup python train.py > train-output.log 2>&1 &
如果你的设备支持多卡推理,那么可以输入:
export CUDA_VISIBLE_DEVICES=1,2,3
nohup python train.py > train.log 2>&1 &
训练时会显示进度。

加载模型
训练完成后,我们可以加载一个checkpoint看看效果
mode_path = './LLM-Research/gemma-2-2b-it'
lora_path = './output/gemma-2-2b-3/checkpoint-800' # 这里改称你的 lora 输出对应 checkpoint 地址
# 加载tokenizer
tokenizer = AutoTokenizer.from_pretrained(mode_path)
# 加载模型
model = AutoModelForCausalLM.from_pretrained(mode_path, device_map="auto",torch_dtype=torch.bfloat16, trust_remote_code=True).eval()
from peft import PeftModel
# 加载lora权重
model = PeftModel.from_pretrained(model, model_id=lora_path)
# 调用模型进行对话生成
chat = [
{ "role": "user", "content": 'Hello' },
]
prompt = tokenizer.apply_chat_template(chat, tokenize=False, add_generation_prompt=True)
inputs = tokenizer.encode(prompt, add_special_tokens=False, return_tensors="pt")
outputs = model.generate(input_ids=inputs.to(model.device), max_new_tokens=150)
outputs = tokenizer.decode(outputs[0])
response = outputs.split('model')[-1].replace('<end_of_turn>\n<eos>', '')
print(response)
下一节,我们介绍如何编写程序,使用truthfulQA数据集测量模型准确率,敬请期待!
更多推荐
所有评论(0)