与依赖 OpenAI 的 GPT 等专有基础模型相比,微调大型语言模型(LLM)能带来诸多优势。想想看,推理成本降低 10 倍,每秒生成的令牌数提升 10 倍,还无需担忧 OpenAI 在其 API 背后进行的任何不透明操作。关于微调,大家的思考方向不应是如何超越 OpenAI 或替代 RAG,而应是如何在特定应用场景下保持同等性能的同时,大幅减少推理时间和成本。
可现实是普通开发者构建 RAG 应用时,对微调LLM的能力并不自信——训练数据难以收集,方法论难以理解,微调后的模型也难以评测。因此,微调成了LLM从业者的最佳维生素。你常会听到诸如“微调目前不是优先事项”、“我们会先尝试 RAG,必要时再转向微调”,以及经典的“已在路线图中”。但如果我告诉你,任何人都能在 2 小时内、免费、用不到 100 行代码开始微调LLM呢?与其在 RAG 和微调之间二选一,为何不两者兼得?
本文将展示如何使用 Hugging Face 的 transformers 库微调 LLaMA-3 8B 模型,并通过 DeepEval 评测微调后的模型,全程在 Google Colab 中完成。
什么是 LLaMA-3 以及微调?
LLaMA-3 是 Meta 推出的第二代开源LLM系列,采用优化的 Transformer 架构,提供 8B 和 70B 两种规模的模型,适用于各类 NLP 任务。尽管像 LLaMA-3 这样的预训练自回归模型能较好地预测序列中的下一个标记,但通过微调使其响应更符合人类预期仍是必要的。
机器学习中的微调是指在新数据上调整预训练模型的权重,通过在特定任务数据集上训练来提升任务表现,使其适应新输入。对于 LLaMA-3 的微调,则意味着提供一组指令和响应样本,采用指令调优技术将其转化为实用的助手。微调的优势显著——你知道吗?仅训练 LLaMA-3 8B 模型,Meta 就耗费了 130 万 GPU 小时。
微调有两种不同的形式:
- 监督微调(SFT):LLMs基于一组指令和响应进行微调。模型权重将被更新,以最小化生成输出与标注响应之间的差异。
- 人类反馈强化学习(RLHF):LLMs通过最大化奖励函数(使用近端策略优化算法或直接偏好优化(DPO)算法)进行训练。该技术利用人类对生成输出的评价反馈,从而捕捉更复杂的人类偏好,但易受人类反馈不一致性的影响。
本文将使用 SFT 对 LLaMA-3 8B 模型进行指令微调。
微调中的常见陷阱
训练数据不足
先前关于 RLHF 的声明揭示了一个至关重要的观点:在微调过程中,训练数据集的质量是最关键的因素。实际上,LIMA 论文表明,使用 1,000 个高质量样本对 65B 参数的 LLaMA(1)进行微调,其表现可超越 OpenAI 的 DaVinci003 模型。
再举一个例子,这是基于 14 万条 Slack 消息微调后的 gpt-3.5-turbo:
这相当滑稽,但或许只是因为它不是来自我的LLM。
使用了错误的提示模板
这实际上仅在你使用特定模型(如 LLaMA-2 的聊天模型)且该模型基于特定提示模板训练时才重要。简而言之,Meta 在训练 LLaMA-2 聊天模型时采用了以下模板格式,理想情况下,你的训练数据也需遵循此格式。
[s][INST] [[SYS]]
System prompt
[[/SYS]]
User prompt [/INST] Model answer [/s]
基于这些原因,我们将使用 mlabonne/guanaco-llama2–1k 数据集进行微调。这是一个包含1000条高质量指令-响应的数据集(源自timdettmers/openassistant-guanaco数据集),已按LLaMA-2的提示模板重新格式化。
LLaMA-3 微调分步指南
第一步——安装
首先,创建一个新的 Google Colab 笔记本。没错,我们将在 Colab 笔记本中完成所有操作。
接着,安装并导入所需的库:
!pip install transformers peft bitsandbytes trl deepeval
import os
import torch
from datasets import load_dataset
from transformers import (
AutoModelForCausalLM,
AutoTokenizer,
BitsAndBytesConfig,
TrainingArguments,
pipeline,
)
from peft import LoraConfig
from trl import SFTTrainer
这里,我们使用的是来自 Hugging Face 和 Confident AI 生态系统的库:
- transformers :用于加载模型、分词器等。
- peft : 执行参数高效微调
- bitsandbytes : 设置 4 位量化
- trl : 用于监督式微调
- deepeval : 评测微调后的LLM
步骤 2——量化设置
为优化 LLaMA-3 8B 微调过程中的 Colab 内存使用,我们采用 QLoRA(量化低秩近似技术)。以下是其核心原理解析:
- 4 位量化:QLoRA 通过仅用 4 比特表示权重(而非标准的 32 位浮点数),对预训练的 LLaMA-3 8B 模型进行压缩,显著降低了模型的内存占用。
- 冻结预训练模型:量化后,LLaMA-3 的绝大部分参数被冻结,确保微调期间不会直接更新核心模型。
- 低秩适配器:QLoRA 在模型架构中引入了轻量级、可训练的适配层。这些适配器捕获任务特定知识,而不会显著增加参数数量。
- 基于梯度的微调:在微调过程中,梯度流经冻结的 4 位量化模型,但仅用于更新低秩适配器内的参数。这种隔离优化极大降低了计算开销。
以下是原论文中 QLoRA 的视觉呈现。
在具体实现中,我们可以调用bitsandbytes库进行高效优化。
...
#################################
### Setup Quantization Config ###
#################################
compute_dtype = getattr(torch, "float16")
quant_config = BitsAndBytesConfig(
load_in_4bit=True,
bnb_4bit_quant_type="nf4",
bnb_4bit_compute_dtype=compute_dtype,
bnb_4bit_use_double_quant=False,
)
步骤 3——加载带有 QLoRA 配置的 LLaMA-3
本步骤操作简易:我们将直接从Hugging Face平台加载LLaMA-3 8B模型。
请注意,尽管 LLaMA-3 是开源的且在 Hugging Face 上可用,但你需要向 Meta 提交申请以获取访问权限,这一过程通常需要一周左右的时间。
...
#######################
### Load Base Model ###
#######################
base_model_name = "meta-llama/Meta-Llama-3-8B"
llama_3 = AutoModelForCausalLM.from_pretrained(
base_model_name,
quantization_config=quant_config,
device_map={"": 0}
)
步骤 4——加载分词器
当LLM读取文本时,首先需要将文本转换为可读格式。这一过程称为标记化,由标记器执行。
标记器通常设计为与其对应模型协同工作。复制以下代码以加载 LLaMA-3 的标记器:
...
######################
### Load Tokenizer ###
######################
tokenizer = AutoTokenizer.from_pretrained(
base_model_name,
trust_remote_code=True
)
tokenizer.pad_token = tokenizer.eos_token
tokenizer.padding_side = "right"
步骤 5——加载数据集
如前一节所述,鉴于 mlabonne/guanaco-llama2–1k 数据集的高质量数据标签及其与 LLaMA-3 提示模板的兼容性,我们将使用该数据集进行微调。
...
####################
### Load Dataset ###
####################
train_dataset_name = "mlabonne/guanaco-llama2-1k"
train_dataset = load_dataset(train_dataset_name, split="train")
步骤 6 — 为 PEFT 加载 LoRA 配置
我不想去分析 QLoRA 与 LoRA 之间的具体差异,但 LoRA 本质上是一个内存效率较低的 QLoRA 版本,因为它不使用量化技术,不过可能带来略微更高的准确度。(更多关于 LoRA 的信息可在此处阅读。)
在此步骤中,我们为参数高效微调(PEFT)配置 LoRA,它仅更新一小部分参数,与常规微调中更新所有模型参数的做法形成对比。
...
#########################################
### Load LoRA Configurations for PEFT ###
#########################################
peft_config = LoraConfig(
lora_alpha = 16
lora_dropout=0.1,
r=64,
bias="none",
task_type="CAUSAL_LM",
)
步骤 7 — 设置训练参数与 SFT 参数
我们即将完成,剩下的就是设置训练所需的参数以及为训练器配置监督微调(SFT)参数:
...
##############################
### Set Training Arguments ###
##############################
training_arguments = TrainingArguments(
output_dir="./tuning_results",
num_train_epochs=1,
per_device_train_batch_size=4,
gradient_accumulation_steps=1,
optim="paged_adamw_32bit",
save_steps=25,
logging_steps=25,
learning_rate=2e-4,
weight_decay=0.001,
fp16=False,
bf16=False,
max_grad_norm=0.3,
max_steps=-1,
warmup_ratio=0.03,
group_by_length=True,
lr_scheduler_type="constant"
)
##########################
### Set SFT Parameters ###
##########################
trainer = SFTTrainer(
model=llama_3,
train_dataset=train_dataset,
peft_config=peft_config,
dataset_text_field="text",
max_seq_length=None,
tokenizer=tokenizer,
args=training_arguments,
packing=False,
)
步骤 8——微调并保存模型
运行以下代码开始微调:
...
#######################
### Fine-Tune Model ###
#######################
trainer.train()
预计训练将持续约一小时,微调完成后,保存你的模型和分词器,即可立即开始测试微调模型的结果!
微调完成后,请保存模型和分词器,随后即可立即开始测试微调后的模型效果!
...
##################
### Save Model ###
##################
new_model = "tuned-llama-3-8b"
trainer.model.save_pretrained(new_model)
trainer.tokenizer.save_pretrained(new_model)
#################
### Try Model ###
#################
prompt = "What is a large language model?"
pipe = pipeline(
task="text-generation",
model=llama_3,
tokenizer=tokenizer,
max_length=200
)
result = pipe(f"[s][INST] {prompt} [/INST]")
print(result[0]['generated_text'])
使用 DeepEval 评测微调后的LLM
我知道你在想什么(或者说我希望我猜对了)。你期待看到类似在 TensorBoard 上展示的微调过程中损失值变化的图表,但幸运的是,我不会用这种“评测”方式来让你感到无聊。相反,我们将使用 DeepEval,一个针对LLMs的开源LLM评测框架。
由于我们对 LLaMA-3 8B 进行了微调,使其本质上能作为助手发挥作用,我们将基于三个指标评测模型:偏见性、毒性和实用性。在 DeepEval 中,这些指标通过LLMs进行评测,结合了细致的提示工程和 QAG、G-Eval 等框架。
首先,设置你的 OpenAI API 密钥并定义LLM评测指标:
%env OPENAI_API_KEY=your-openai-api-key
from deepeval.metrics import GEval, BiasMetric, ToxicityMetric
from deepeval.test_case import LLMTestCaseParams
helpfulness_metric = GEval(
name="Helpfulness",
criteria="Helpfulness - determine if how helpful the actual output is in response with the input.",
evaluation_params=[LLMTestCaseParams.INPUT, LLMTestCaseParams.ACTUAL_OUTPUT],
threshold=0.5
)
bias_metric = BiasMetric(threshold=0.5)
toxicity_metric = ToxicityMetric(threshold=0.5)
DeepEval 的指标返回一个分数(0-1)并给出评分理由。只有当计算分数达到阈值(根据指标不同,可能是最高或最低阈值)时,该指标才被视为成功。
若你好奇这些指标的具体实现方式,可前往探索⭐DeepEval开源代码库⭐,或系统学习如何为LLM评估指标打分的所有核心知识要点。
总结一下,通过使用 DeepEval 的合成数据生成器创建测试用例,列出你想要评测模型输出的输入列表:
from deepeval.synthesizer import Synthesizer
from deepeval.test_case import LLMTestCase
...
synthesizer = Synthesizer()
synthesizer.generate_goldens_from_docs(
# Generate queries from your documents
document_paths=['example_1.txt', 'example_2.docx', 'example_3.pdf'],
max_goldens_per_document=2
)
pipe = pipeline(
task="text-generation",
model=llama_3,
tokenizer=tokenizer,
max_length=200
)
test_cases = []
for golden in synthesizer.synthetic_goldens:
actual_output = pipe(f"[s][INST] {input} [/INST]")[0]['generated_text']
test_case = LLMTestCase(input=golden.input, acutal_output=actual_output)
test_cases.append(test_case)
为了简化,我们硬编码了输入,但你应该明白了。最后,使用你之前定义的LLM评测指标来创建并评测你的数据集:
from deepeval.dataset import EvaluationDataset
...
evaluation_dataset = EvaluationDataset(test_cases=test_cases)
evaluation_dataset.evaluate([bias_metric, helpfulness_metric, toxicity_metric])
搞定!恭喜你完成了本教程的学习。通过这一配置,你将能够添加更多指标和测试用例,进一步评测并迭代优化你微调过的 LLaMA-3 模型。
结论
在本文中,我们探讨了 LLaMA-3 是什么、为何需要微调及其涉及的内容,以及在微调过程中需注意的事项,包括使用正确的数据集,并将其格式化以适配基础模型训练时使用的提示模板。
我们还演示了如何利用Hugging Face生态系统,在Google Colab笔记本中借助QLoRA等量化技术无缝开展模型微调。最后,我们展示了如何通过DeepEval ⭐ 对微调后的模型进行系统评估。我们已为你完成了所有复杂的基础工作,并提供覆盖LLM微调评估全流程的完整解决方案体系。