微调大型语言模型:定制 Llama 3 8B 应用
自 2022 年 11 月发布以来,ChatGPT 引发了关于大型语言模型(LLMs)和一般人工智能能力的广泛讨论。尽管像 GPT、Gemini 或 Claude 这样的工具非常强大,拥有数百甚至数千亿的参数,并在大量文本语料库上进行预训练,但它们并非万能。有些特定任务这些模型无法胜任。然而,我们并非没有解决这些任务的办法。我们可以利用小型开源模型的力量,将它们适应到我们的特定问题上。
本博客旨在简要概述一些较小的开源 LLMs,并解释两个关键的 LLM 微调概念:量化和 LoRA。此外,我们将介绍一些最受欢迎的微调库以及代码示例,以便您能快速将这些概念应用到您的用例中。
'小'型大型语言模型
微调 LLMs 可能代价昂贵,尤其是对于参数数量庞大的模型。根据经验法则,通常参数在 100 亿以下的模型可以进行微调而不会碰到显著的基础设施挑战。然而,对于像 Llama 3 70B 这样的大型模型,则需要大量资源。对一个 700 亿参数的模型如 Llama 3 进行微调大约需要 1.5TB 的 GPU 显存。为了直观比较,这个数量级的显存相当于一个大约有 20 块 Nvidia A100 组成的集群,每块有 80GB 的显存。假设硬件是可用的,这样的设置成本约为 40 万美元。
或者,人们可以使用云服务提供商,如 AWS、Azure 或 GCP,但这种方法同样成本高昂。例如,使用 AWS 上的一块 Nvidia A100 GPU 一小时的成本是 40 美元。如果你要在 20 个 GPU 上对 700 亿参数模型进行 5 天的微调,费用大约会是 10 万美元。
由于这些成本,大多数实践者主要使用参数少于 100 亿的较小 LLMs。这些模型可以更经济地训练,只需要 16GB 到 24GB 的显存(用于更大的批量大小和更快的训练)。例如,在 AWS 上使用一块 Nvidia A100 将 Mistral 7B 微调为塞尔维亚语,不到 10 小时就完成了,成本不到 20 美元。
当然,如果没有量化,特别是 4 位量化,一个 70 亿参数的模型仍然无法在这么大的显存中完成训练。
量化
如果使用完整的 32 位参数,我们仍然需要大量的显存来训练 LLM——大约需要 150GB,这对于个人开发者来说是一个巨大的数字。
量化通过将模型参数转换为低精度数据类型(如 8 位或 4 位)来提供解决方案,显著降低了内存消耗并提高了执行速度。概念很直接:所有可能的 32 位值都被映射到一个较小的有限值范围(例如,对于 8 位转换是 256)。这个过程可以被视为围绕几个固定点的高精度值分组,这些固定点代表了它们附近的值。
低秩适应(LoRA)
LoRA 是一种通过使用矩阵维数约简来更新模型权重的技术。这项技术尤其相关,因为广泛应用于 LLMs 的 Transformer 严重依赖矩阵。关于 LoRA 在低层次工作的详细解释可以在 Jay Alammar 的博客文章中找到。
在更新模型权重时,需要调整这些矩阵内的参数。从概念上讲,这种调整可以被视为将一个权重更新矩阵加到原始矩阵上:W' = W + ΔW。LoRA 引入了一种新颖的方法,通过将这个更新矩阵分解成两个较小的矩阵,当这两个矩阵相乘时,接近更新矩阵。在微调过程中,LoRA 不是创建然后分解更新矩阵,而是直接创建这两个较小的矩阵用于乘法运算。
下面几张图片中可以看到常规微调和使用 LoRA 进行微调之间的直观比较。
LoRA 的关键好处是,尽管近似稍微不那么精确,但它显著提高了内存和计算效率。例如,考虑一个有 1000x1000 参数的矩阵,总共有 100 万参数。通过使用分解后(略微不精确)的 1000x100 乘以 100x1000 矩阵的版本,参数数量减少到只有 2*100k,实现了 80% 的参数减少。
量化和 LoRA 通常结合使用,形成了所谓的 QLoRA。
Unsloth
如果我重新开始进行 LLM 微调,我会选择 Unsloth Python 库。Unsloth 提供了一系列针对 LLM 微调的优化,并支持包括 Mistral、Llama 3、Gemma 等在内的多种流行的 LLMs。例如,他们的免费层级包括了 12 种不同的针对 Mistral 的微调优化,提供了显著的 2.2 倍加速。
以下是使用 Unsloth 库微调 Llama 3 8B 的代码片段。所有这些代码块都取自 Unsloth 的 GitHub,完整的用于微调 Llama 3 8B 的笔记本可以在其官方仓库找到。
以 4 位精度导入模型
model, tokenizer = FastLanguageModel.from_pretrained(
model_name="unsloth/llama-3-8b-bnb-4bit",
max_seq_length=max_seq_length,
dtype=dtype,
load_in_4bit=load_in_4bit,
)
安装 LoRA
model = FastLanguageModel.get_peft_model(
model,
r=16,
target_modules=["q_proj", "k_proj", "v_proj", "o_proj",
"gate_proj", "up_proj", "down_proj",],
lora_alpha=16,
lora_dropout=0,
bias="none",
use_gradient_checkpointing="unsloth",
random_state=3407,
use_rslora=False,
loftq_config=None,
)
初始化 Hugging Face 的监督微调训练器
trainer = SFTTrainer(
model=model,
tokenizer=tokenizer,
train_dataset=dataset,
dataset_text_field="text",
max_seq_length=max_seq_length,
dataset_num_proc=2,
packing=False,
args=TrainingArguments(
per_device_train_batch_size=2,
gradient_accumulation_steps=4,
warmup_steps=5,
max_steps=60,
learning_rate=2e-4,
fp16=not torch.cuda.is_bf16_supported(),
bf16=torch.cuda.is_bf16_supported(),
logging_steps=1,
optim="adamw_8bit",
weight_decay=0.01,
lr_scheduler_type="linear",
seed=3407,
output_dir="outputs",
),
)
训练模型
trainer_stats = trainer.train()
监督微调训练器(SFT)
在预训练一个 LLM 之后,下一个关键步骤是监督微调。这个过程对于开发一个能够理解并生成连贯响应的模型至关重要,而不仅仅是完成句子。
像 Hugging Face 的 SFT(监督微调训练器)和 PEFT(参数高效微调),以及 Tim Dettmers 的 BitsAndBytes 等工具,极大地简化了将 LoRA、量化和微调等技术应用到模型的过程。这些库简化了高级优化方法的实施,使它们对开发者和研究人员更加易于访问和高效。
下面,你会注意到 Unsloth、SFT 和 ORPO 的代码非常相似。这种相似性源于这些库背后的基本思想大致相同,差异主要在于库本身以及可能的一些超参数。
以 4 位精度导入模型
model_id = "meta-llama/Meta-Llama-3-8B"
bnb_config = BitsAndBytesConfig(
load_in_4bit=True,
bnb_4bit_use_double_quant=True,
bnb_4bit_quant_type="nf4",
bnb_4bit_compute_dtype=torch.bfloat16 if use_flash_attention2 else torch.float16
)
model = AutoModelForCausalLM.from_pretrained(
model_id,
quantization_config=bnb_config,
use_cache=False,
device_map="auto",
token=os.environ["HF_TOKEN"],
attn_implementation="flash_attention_2" if use_flash_attention2 else "sdpa"
)
model.config.pretraining_tp = 1
tokenizer = AutoTokenizer.from_pretrained(
model_id,
token=os.environ["HF_TOKEN"],
)
tokenizer.pad_token = tokenizer.eos_token
tokenizer.padding_side = "right"
安装 LoRA
peft_config = LoraConfig(
lora_alpha=16,
lora_dropout=0.1,
r=64,
bias="none",
task_type="CAUSAL_LM",
target_modules=[
"q_proj",
"k_proj",
"v_proj",
"o_proj",
"gate_proj",
"up_proj",
"down_proj",
]
)
model = prepare_model_for_kbit_training(model)
初始化 Hugging Face 的监督微调训练器
args = TrainingArguments(
output_dir="mistral-int4-alpaca",
num_train_epochs=1,
per_device_train_batch_size=6 if use_flash_attention2 else 2,
gradient_accumulation_steps=4,
gradient_checkpointing=True,
optim="paged_adamw_8bit",
logging_steps=10,
save_strategy="epoch",
learning_rate=2e-4,
bf16=use_flash_attention2,
fp16=not use_flash_attention2,
tf32=use_flash_attention2,
max_grad_norm=0.3,
warmup_steps=5,
lr_scheduler_type="linear",
disable_tqdm=False,
report_to="none"
)
model = get_peft_model(model, peft_config)
trainer = SFTTrainer(
model=model,
train_dataset=dataset,
peft_config=peft_config,
max_seq_length=2048,
tokenizer=tokenizer,
packing=True,
formatting_func=format_instruction,
args=args,
)
训练模型
trainer.train()
优势比偏好优化(ORPO)
在这篇博客文章中,我们专注于大型语言模型(LLMs)的预训练和监督微调。然而,所有最先进的 LLMs 都经历了另一个关键步骤:偏好对齐。这一步骤发生在预训练和微调之后,你告知模型哪些生成的输出是可取的,哪些不是。流行的偏好对齐方法包括来自人类反馈的强化学习(RLHF)和直接偏好优化(DPO)。
一种名为优势比偏好优化(ORPO)的新方法于 2024 年 3 月出现,结合了监督微调和偏好对齐。
这里我们有一部分使用 ORPO 进行微调和偏好对齐的代码。完整代码可以在相关 Colab 笔记本中找到。
以 4 位精度导入模型
base_model = "meta-llama/Meta-Llama-3-8B"
new_model = "OrpoLlama-3-8B"
bnb_config = BitsAndBytesConfig(
load_in_4bit=True,
bnb_4bit_quant_type="nf4",
bnb_4bit_compute_dtype=torch_dtype,
bnb_4bit_use_double_quant=True,
)
tokenizer = AutoTokenizer.from_pretrained(base_model)
model = AutoModelForCausalLM.from_pretrained(
base_model,
quantization_config=bnb_config,
device_map="auto",
attn_implementation=attn_implementation
)
配置 LoRA
peft_config = LoraConfig(
r=16,
lora_alpha=32,
lora_dropout=0.05,
bias="none",
task_type="CAUSAL_LM",
target_modules=['up_proj', 'down_proj', 'gate_proj', 'k_proj', 'q_proj', 'v_proj', 'o_proj']
)
model = prepare_model_for_kbit_training(model)
初始化 Hugging Face 的 ORPO 训练器
orpo_args = ORPOConfig(
learning_rate=8e-6,
beta=0.1,
lr_scheduler_type="linear",
max_length=1024,
max_prompt_length=512,
per_device_train_batch_size=2,
per_device_eval_batch_size=2,
gradient_accumulation_steps=4,
optim="paged_adamw_8bit",
num_train_epochs=1,
evaluation_strategy="steps",
eval_steps=0.2,
logging_steps=1,
warmup_steps=10,
report_to="wandb",
output_dir="./results/",
)
trainer = ORPOTrainer(
model=model,
args=orpo_args,
train_dataset=dataset["train"],
eval_dataset=dataset["test"],
peft_config=peft_config,
tokenizer=tokenizer,
)
训练模型
trainer.train()
推理与部署
训练完成后,模型通常会保存为 LoRA 适配器权重。在实际应用中,您需要加载基础模型并结合这些适配器进行推理。
from peft import PeftModel
base_model = AutoModelForCausalLM.from_pretrained("meta-llama/Meta-Llama-3-8B")
model = PeftModel.from_pretrained(base_model, "path/to/your/lora_adapter")
inputs = tokenizer("请回答以下问题:", return_tensors="pt")
outputs = model.generate(**inputs, max_new_tokens=100)
print(tokenizer.decode(outputs[0]))
通过这种方式,您可以快速将微调后的模型集成到您的生产环境中,无需重新加载整个庞大的基础模型权重,从而节省内存并提高响应速度。
结论
尽管像 GPT、Gemini 或 Claude 这样的大型语言模型(LLMs)功能强大,但它们的大规模和资源需求使它们在许多任务中不切实际。为了解决这个问题,可以使用量化和低秩适应(LoRA)等技术对较小的开源 LLMs 进行微调和定制以满足特定需求。这些技术减少了内存消耗并提高了计算效率,使得训练模型更加经济,尤其是对于那些参数少于 100 亿的模型。
像 Unsloth、监督微调训练器(SFT)和优势比偏好优化(ORPO)这样的工具简化了微调过程,使其更加易于访问。例如,Unsloth 提供的优化可以显著加速训练,而 ORPO 结合了监督微调和偏好对齐,以提高模型性能。
通过利用这些技术和工具,开发人员和研究人员可以根据特定需求定制 LLMs,而无需承担与训练大型模型相关的高昂成本。这种方法使得高级语言模型的访问民主化,并在不同领域启用了广泛的应用。