直到最近,在Google Colab上免费使用单个GPU对7B模型进行微调是一种梦想。2023年5月23日,Tim Dettmers和他的团队提交了一篇关于微调量化大型语言模型的革命性论文[1]。
量化模型是一种权重数据类型低于其训练数据类型的模型。例如,如果您在32位浮点数中训练模型,然后将这些权重转换为较低的数据类型,如16/8/4位浮点数,以便对模型的性能几乎没有影响。
在这里我们不会过多讨论量化的理论,您可以参考Hugging-Face的出色博客文章[2][3]和Tim Dettmers本人的出色YouTube视频[4]来了解底层理论。
简而言之,QLora的含义是:
使用低秩适应矩阵(LoRA)对量化大型语言模型进行微调[5]
让我们直接进入代码:
数据准备
重要的是要理解,大型语言模型被设计为接受指令,这是在2021年的ACL论文[6]中首次引入的。这个想法很简单,我们给语言模型一个指令,它遵循指令并执行任务。因此,我们要对模型进行微调的数据集应该是以指令格式的,如果不是,我们可以进行转换。
常见的格式之一是指令格式。我们将使用Alpaca Prompt模板[7],其格式如下:
以下是描述任务的指令,以及提供进一步上下文的输入。编写一个适当完成请求的响应。
### 指令:
{instruction}
### 输入:
{input}
### 响应:
{response}
我们将使用SNLI数据集,这是一个包含2个句子及其之间关系(它们是矛盾、相互蕴含还是中性)的数据集。我们将使用它来使用LLAMAv2为句子生成矛盾。我们可以使用pandas简单地加载这个数据集。
import pandas as pd
df = pd.read_csv('snli_1.0_train_matched.csv')
df['gold_label'].value_counts().plot(kind='barh')
这里可以看到一些随机的矛盾示例。
df[df['gold_label'] == 'contradiction'].sample(10)[['sentence1', 'sentence2']]
现在我们可以创建一个小函数,仅接受矛盾句子并将数据集转换为指令格式。
def convert_to_format(row):
sentence1 = row['sentence1']
sentence2 = row['sentence2']ccccc
prompt = """以下是描述任务的指令,以及提供进一步上下文的输入。编写一个适当完成请求的响应。"""
instruction = """给出以下句子,您的任务是以json格式生成其否定形式"""
input = str(sentence1)
response = f"""```json
{{'orignal_sentence': '{sentence1}', 'generated_negation': '{sentence2}'}}
```
"""
if len(input.strip()) == 0: # prompt + 2 new lines + ###instruction + new line + input + new line + ###response
text = prompt + "\n\n### 指令:\n" + instruction + "\n### 响应:\n" + response
else:
text = prompt + "\n\n### 指令:\n" + instruction + "\n### 输入:\n" + input + "\n" + "\n### 响应:\n" + response
# 我们需要4个列用于自动训练,指令、输入、输出、文本
return pd.Series([instruction, input, response, text])
new_df = df[df['gold_label'] == 'contradiction'][['sentence1', 'sentence2']].apply(convert_to_format, axis=1)
new_df.columns = ['instruction', 'input', 'output', 'text']
new_df.to_csv('snli_instruct.csv', index=False)
这里是一个示例数据点的示例:
"以下是一条描述任务和提供进一步上下文的输入的指令。编写一个适当完成请求的响应。
### 指令:
给定以下句子,您的任务是以json格式生成其否定形式
### 输入:
在海滩上有一对夫妇和一个小男孩玩。
### 响应:
```json
{'orignal_sentence': '在海滩上有一对夫妇和一个小男孩玩。', 'generated_negation': '一对夫妇看着一个小女孩在海滩上自己玩。'}
```
现在我们有了正确格式的数据集,让我们从微调开始。在开始之前,让我们安装必要的软件包。我们将使用加速器、peft(参数高效微调)、与Hugging Face Bits和bytes和transformers组合。
!pip install -q accelerate==0.21.0 peft==0.4.0 bitsandbytes==0.40.2 transformers==4.31.0 trl==0.4.7
import os
import torch
from datasets import load_dataset
from transformers import (
AutoModelForCausalLM,
AutoTokenizer,
BitsAndBytesConfig,
HfArgumentParser,
TrainingArguments,
pipeline,
logging,
)
from peft import LoraConfig, PeftModel
from trl import SFTTrainer
您可以将格式化的数据集上传到驱动器并在Colab中加载它。
from google.colab import drive
import pandas as pd
drive.mount('/content/drive')
df = pd.read_csv('/content/drive/MyDrive/snli_instruct.csv')
您可以使用from_pandas
方法将其轻松转换为Hugging Face数据集格式,这对于训练模型很有帮助。
from datasets import Dataset
dataset = Dataset.from_pandas(df)
我们将使用由abhishek/llama-2-7b-hf-small-shards提供的预先量化的LLamav2模型。让我们在这里定义一些超参数和变量:
# 您要从Hugging Face hub训练的模型
model_name = "abhishek/llama-2-7b-hf-small-shards"
# 微调模型名称
new_model = "llama-2-contradictor"
################################################################################
# QLoRA参数
################################################################################
# LoRA注意力维度
lora_r = 64
# LoRA缩放的Alpha参数
lora_alpha = 16
# LoRA层的Dropout概率
lora_dropout = 0.1
################################################################################
# bitsandbytes参数
################################################################################
# 激活4位精度的基础模型加载
use_4bit = True
# 4位基础模型的计算dtype
bnb_4bit_compute_dtype = "float16"
# 量化类型(fp4或nf4)
bnb_4bit_quant_type = "nf4"
# 激活4位基础模型的嵌套量化(双量化)
use_nested_quant = False
################################################################################
# TrainingArguments参数
################################################################################
# 存储模型预测和检查点的输出目录
output_dir = "./results"
# 训练时的轮次数
num_train_epochs = 1
# 启用fp16/bf16训练(在A100上将bf16设置为True)
fp16 = False
bf16 = False
# 每个GPU的训练批次大小
per_device_train_batch_size = 4
# 每个GPU的评估批次大小
per_device_eval_batch_size = 4
# 累积梯度的更新步数
gradient_accumulation_steps = 1
# 启用梯度检查点
gradient_checkpointing = True
# 最大梯度规范(梯度裁剪)
max_grad_norm = 0.3
# 初始学习率(AdamW优化器)
learning_rate = 1e-5
# 应用于所有层(除了bias/LayerNorm权重)的权重衰减
weight_decay = 0.001
# 要使用的优化器
optim = "paged_adamw_32bit"
# 学习率调度程序
lr_scheduler_type = "cosine"
# 训练步骤数(覆盖num_train_epochs)
max_steps = -1
# 线性预热的步数比率(从0到学习率)
warmup_ratio = 0.03
# 将序列按相同长度分组
# 节省内存并显着加快训练速度
group_by_length = True
# 每X个更新步骤保存检查点
save_steps = 0
# 每X个更新步骤记录日志
logging_steps = 100
################################################################################
# SFT参数
################################################################################
# 要使用的最大序列长度
max_seq_length = None
# 将多个短示例打包到同一输入序列中以提高效率
packing = False
# 将整个模型加载到GPU 0上
device_map = {"": 0}
其中大多数都是相当直观的超参数,具有这些默认值。您始终可以参考文档获取更多详细信息。
现在我们可以简单地使用BitsAndBytesConfig类来创建4位微调的配置。
compute_dtype = getattr(torch, bnb_4bit_compute_dtype)
bnb_config = BitsAndBytesConfig(
load_in_4bit=use_4bit,
bnb_4bit_quant_type=bnb_4bit_quant_type,
bnb_4bit_compute_dtype=compute_dtype,
bnb_4bit_use_double_quant=use_nested_quant,
)
现在我们可以使用4位BitsAndBytesConfig和Fine-Tuning的tokenizer加载基础模型。
tokenizer = AutoTokenizer.from_pretrained(model_name, trust_remote_code=True)
tokenizer.pad_token = tokenizer.eos_token
tokenizer.padding_side = "right"
model = AutoModelForCausalLM.from_pretrained(
model_name,
quantization_config=bnb_config,
device_map=device_map
)
model.config.use_cache = False
model.config.pretraining_tp = 1
现在我们可以创建LoRA配置并设置训练参数。
# 加载LoRA配置
peft_config = LoraConfig(
lora_alpha=lora_alpha,
lora_dropout=lora_dropout,
r=lora_r,
bias="none",
task_type="CAUSAL_LM",
)
# 设置训练参数
training_arguments = TrainingArguments(
output_dir=output_dir,
num_train_epochs=num_train_epochs,
per_device_train_batch_size=per_device_train_batch_size,
gradient_accumulation_steps=gradient_accumulation_steps,
optim=optim,
save_steps=save_steps,
logging_steps=logging_steps,
learning_rate=learning_rate,
weight_decay=weight_decay,
fp16=fp16,
bf16=bf16,
max_grad_norm=max_grad_norm,
max_steps=max_steps,
warmup_ratio=warmup_ratio,
group_by_length=group_by_length,
lr_scheduler_type=lr_scheduler_type,
report_to="tensorboard"
)
现在我们可以简单地使用由HuggingFace提供的trl中的SFTTrainer来开始训练。
# 设置监督微调参数
trainer = SFTTrainer(
model=model,
train_dataset=dataset,
peft_config=peft_config,
dataset_text_field="text", # 这是数据集中的文本列
max_seq_length=max_seq_length,
tokenizer=tokenizer,
args=training_arguments,
packing=packing,
)
# 训练模型
trainer.train()
# 保存已训练的模型
trainer.model.save_pretrained(new_model)
这将开始根据您设置的轮数进行训练。训练完模型后,确保将其保存在驱动器中,以便您可以再次加载它(因为您需要重新启动colab中的会话)。您可以通过zip和mv命令将模型存储在驱动器中。
!zip -r llama-contradictor.zip results llama-contradictor
!mv llama-contradictor.zip /content/drive/MyDrive
现在当您重新启动Colab会话时,您可以将其重新移回到会话中。
!unzip /content/drive/MyDrive/llama-contradictor.zip -d .
您需要重新加载基础模型并将其与微调的LoRA矩阵合并。可以使用merge_and_unload()
函数来完成。
tokenizer = AutoTokenizer.from_pretrained(model_name, trust_remote_code=True)
tokenizer.pad_token = tokenizer.eos_token
tokenizer.padding_side = "right"
base_model = AutoModelForCausalLM.from_pretrained(
"abhishek/llama-2-7b-hf-small-shards",
low_cpu_mem_usage=True,
return_dict=True,
torch_dtype=torch.float16,
device_map={"": 0},
)
model = PeftModel.from_pretrained(base_model, '/content/llama-contradictor')
model = model.merge_and_unload()
pipe = pipeline(task="text-generation", model=model, tokenizer=tokenizer, max_length=200)
推理
您可以通过在上面定义的相同提示模板中简单地传入输入来测试您的模型。
prompt_template = """### 说明:
给定以下句子,您的任务是以json格式生成其否定形式。
### 输入:
{}
### 响应:
"""
sentence = "天气预报预测今天晴朗,气温约为30摄氏度,非常适合和朋友家人一起在海滩度过愉快的一天。"
input_sentence = prompt_template.format(sentence.strip())
result = pipe(input_sentence)
print(result)
输出
### 说明:
给定以下句子,您的任务是以json格式生成其否定形式。
### 输入:
天气预报预测今天晴朗,气温约为30摄氏度,非常适合和朋友家人一起在海滩度过愉快的一天。
### 响应:
```json
{
"sentence": "天气预报预测今天晴朗,气温约为30摄氏度,非常适合和朋友家人一起在海滩度过愉快的一天。",
"negation": "天气预报预测今天有雨,气温约为10摄氏度,不太适合和朋友家人一起在海滩度过愉快的一天。"
}
```
筛选有用的输出
在生成响应后,模型往往会继续预测,导致令牌数量超限。在这种情况下,您需要添加一个后处理函数,筛选出我们需要的JSON部分。这可以通过简单的正则表达式完成。
import re
import json
def format_results(s):
pattern = r'```json\n(.*?)\n```'
# 在字符串中找到所有JSON对象的出现
json_matches = re.findall(pattern, s, re.DOTALL)
if not json_matches:
# 尝试找到第二个模式
pattern = r'\{.*?"sentence":.*?"negation":.*?\}'
json_matches = re.findall(pattern, s)
# 返回找到的第一个JSON对象,如果没有找到匹配项,则返回None
return json.loads(json_matches[0]) if json_matches else None
这将给您所需的输出,而不是模型重复输出随机令牌。
总结
在本文中,您了解了QLora的基础知识,使用QLora在Colab上进行LLama v2模型的微调、指令微调以及来自Alpaca数据集的示例模板。这些内容可用于进一步指导模型。
参考资料
[1]: QLoRA: Efficient Finetuning of Quantized LLMs, 2023年5月23日,Tim Dettmers等
[2]: https://huggingface.co/blog/hf-bitsandbytes-integration
[3]: https://huggingface.co/blog/4bit-transformers-bitsandbytes
[4]: https://www.youtube.com/watch?v=y9PHWGOa8HA
[5]: https://arxiv.org/abs/2106.09685
[6]: https://aclanthology.org/2022.acl-long.244/
[7]: https://crfm.stanford.edu/2023/03/13/alpaca.html
[8]: @maximelabonne的Colab笔记本 https://colab.research.google.com/drive/1PEQyJO1-f6j0S_XJ8DV50NkpzasXkrzd?usp=sharing
Ahmad Anis是一位热情的机器学习工程师和研究员,目前在redbuffer.ai工作。除了日常工作外,Ahmad还积极参与机器学习社区。他担任Cohere for AI的区域负责人,这是一个致力于开放科学的非营利组织,并且是AWS社区建设者。Ahmad在Stackoverflow上是一位积极的贡献者,他拥有2300多个积分。他为许多著名的开源项目做出了贡献,包括OpenAI的Shap-E。