趋近智
使用参数高效微调(PEFT)技术对大型语言模型(LLM)进行微调,是一种常见且高效的优化手段,旨在提升其在特定RAG任务中的表现。此方法的目标是改进LLM根据检索阶段提供的上下文,严格地综合出准确且相关答案的能力,尤其适用于专门方面或需要特定回应风格的情形。
我们将进行数据准备、模型选择、应用LoRA(低秩适应)以及评估结果的过程,这一切都将以构建生产级别RAG系统的专家视角来完成。
完成本次实践后,您将能够:
在开始之前,请确保您拥有一个带有较新GPU的Python环境(强烈建议,以便获得合理的训练时间)。您将需要以下核心库:
torch: 用于张量运算和GPU支持。transformers: 来自Hugging Face,用于LLM模型和分词器。peft: 来自Hugging Face,用于参数高效微调技术,如LoRA。datasets: 来自Hugging Face,便于数据处理。accelerate: 用于简化分布式训练和混合精度(即使在单个GPU上,它也很有用)。bitsandbytes: 用于8位或4位量化(例如QLoRA),如果您想尝试进一步减少内存占用。您通常可以使用pip安装这些库:
pip install torch transformers peft datasets accelerate bitsandbytes
确保您的CUDA驱动和PyTorch安装与您的GPU兼容。
您的微调数据质量和结构对成功非常重要。对于RAG,您不仅仅是教授LLM通用知识;您是在教它基于所提供的文本进行推理。一个理想的数据集由三元组组成:(查询、检索到的上下文、基于上下文的理想答案)。
微调期间LLM的输入应模仿您在RAG系统中将使用的提示结构。一种常用格式是:
<s>[INST] Context: {retrieved_document_chunk}
Question: {user_query} [/INST]
Answer: {ideal_answer_based_on_context}</s>
<s> and </s>: 序列开始和结束标记。[INST] and [/INST]: 指令标签,常见于Llama和Mistral等模型。请根据您选择的基础模型的首选提示格式进行调整。{retrieved_document_chunk}: 您的检索器会提供的实际文本片段。{user_query}: 用户的提问。{ideal_answer_based_on_context}: 期望的输出。此答案必须仅从提供的retrieved_document_chunk中得出。避免需要外部知识的答案。示例数据点(JSONL格式):
{
"text": "<s>[INST] Context: Llama 2模型家族包含7B、13B和70B参数的版本。LoRA微调在使这些模型适应特定任务同时保持大多数权重冻结方面很有效。对于7B模型,LoRA秩(r)为8或16通常是一个好的起始点。 \nQuestion: Llama 2 7B建议使用哪个LoRA秩? [/INST]\n答案:对于Llama 2 7B模型,LoRA秩为8或16通常是微调的一个良好起始点。</s>"
}
制作高质量数据:
对于本次练习,您可以手动创建50-100个示例的小数据集,或者使用脚本从现有文档生成。将其保存为train.jsonl文件。
基础模型的选择取决于您的性能要求、计算预算和任务的复杂程度。Mistral-7B、Llama-2-7B或Gemma-7B等模型由于其强大的基础能力以及使用LoRA进行微调时可控的规模,是PEFT的很好起始点。
我们将使用LoRA。需要配置的LoRA参数有:
r: 更新矩阵的秩。较小的r表示可训练参数更少。常用值范围为4到64。lora_alpha: 一个缩放因子,通常设为r或2*r。target_modules: 指定Transformer中要应用LoRA的线性层(例如q_proj、v_proj、k_proj、o_proj)。识别这些层通常需要检查模型架构。lora_dropout: LoRA层的Dropout概率。bias: 是否使LoRA偏置项可训练(例如:“none”、“all”、“lora_only”)。我们假设使用像mistralai/Mistral-7B-Instruct-v0.1这样的模型。
这是一个使用Hugging Face库的Python脚本概述。
import torch
from datasets import load_dataset
from transformers import (
AutoModelForCausalLM,
AutoTokenizer,
BitsAndBytesConfig,
TrainingArguments,
)
from peft import LoraConfig, PeftModel, get_peft_model, prepare_model_for_kbit_training
from trl import SFTTrainer
# 1. 配置
model_name = "mistralai/Mistral-7B-Instruct-v0.1" # 或您选择的模型
dataset_path = "path/to/your/train.jsonl" # 您的JSONL文件
output_dir = "./results_rag_finetune"
lora_r = 16
lora_alpha = 32
lora_dropout = 0.05
# 用于QLoRA(4位量化)
use_4bit = True
bnb_4bit_quant_type = "nf4"
bnb_4bit_compute_dtype = torch.bfloat16 # 如果不支持bfloat16则使用torch.float16
# 2. 加载分词器和模型
tokenizer = AutoTokenizer.from_pretrained(model_name, trust_remote_code=True)
tokenizer.pad_token = tokenizer.eos_token # 常见做法
tokenizer.padding_side = "right"
if use_4bit:
bnb_config = BitsAndBytesConfig(
load_in_4bit=use_4bit,
bnb_4bit_quant_type=bnb_4bit_quant_type,
bnb_4bit_compute_dtype=bnb_4bit_compute_dtype,
bnb_4bit_use_double_quant=True, # 可选
)
model = AutoModelForCausalLM.from_pretrained(
model_name,
quantization_config=bnb_config,
device_map={"": 0} # 将模型加载到GPU 0
)
model = prepare_model_for_kbit_training(model)
else:
model = AutoModelForCausalLM.from_pretrained(
model_name,
device_map={"": 0} # 将模型加载到GPU 0
)
model.config.use_cache = False # 建议用于微调
model.config.pretraining_tp = 1 # 如果遇到此错误,请设置为1
# 3. LoRA配置
# 通过检查model.named_modules()或根据模型常识来查找目标模块
# 对于Mistral,常用目标是'q_proj', 'k_proj', 'v_proj', 'o_proj', 'gate_proj', 'up_proj', 'down_proj'
peft_config = LoraConfig(
r=lora_r,
lora_alpha=lora_alpha,
target_modules=["q_proj", "v_proj"], # 从小范围开始,如果需要再添加更多
lora_dropout=lora_dropout,
bias="none",
task_type="CAUSAL_LM",
)
model = get_peft_model(model, peft_config)
model.print_trainable_parameters() # 检查有多少参数是可训练的
# 4. 加载数据集
dataset = load_dataset("json", data_files=dataset_path, split="train")
# 5. 训练参数
training_arguments = TrainingArguments(
output_dir=output_dir,
per_device_train_batch_size=2, # 根据您的GPU显存调整
gradient_accumulation_steps=4, # 有效批量大小 = 2 * 4 = 8
optim="paged_adamw_32bit", # 如果不使用QLoRA,则为"adamw_torch"
save_steps=50, # 每50步保存检查点
logging_steps=10, # 记录训练进度
learning_rate=2e-4,
fp16=not use_4bit, # 如果不使用4位,则使用fp16
bf16=use_4bit and torch.cuda.is_bf16_supported(), # 如果是4位且支持,则使用bf16
max_grad_norm=0.3,
num_train_epochs=1, # 对于小型数据集,从1-3个周期开始
warmup_ratio=0.03,
group_by_length=True, # 通过对相似长度序列进行分组来加速训练
lr_scheduler_type="constant", # 或 "cosine"
report_to="tensorboard" # 或 "wandb"
)
# 6. 初始化训练器
trainer = SFTTrainer(
model=model,
train_dataset=dataset,
peft_config=peft_config,
dataset_text_field="text", # JSONL中包含完整提示的字段
max_seq_length=1024, # 根据您的上下文长度和显存调整
tokenizer=tokenizer,
args=training_arguments,
packing=False, # 如果要打包多个短序列,请设置为True
)
# 7. 开始训练
print("开始训练...")
trainer.train()
# 8. 保存微调后的适配器
adapter_output_dir = f"{output_dir}/final_adapter"
trainer.model.save_pretrained(adapter_output_dir)
tokenizer.save_pretrained(adapter_output_dir) # 保存分词器以保持一致性
print(f"微调后的适配器已保存到 {adapter_output_dir}")
训练要点:
per_device_train_batch_size、gradient_accumulation_steps、max_seq_length和量化设置(use_4bit),使其适应GPU的内存。target_modules: LoRA的target_modules选择会明显影响表现。通常需要进行实验。对于许多基于注意力的模型,定位查询、键、值和输出投影(q_proj、k_proj、v_proj、o_proj)是一个好的开始。某些架构也通过定位前馈网络层而受益。训练后,LoRA适配器(而非完整模型)会被保存。要将其用于推断:
from transformers import AutoModelForCausalLM, AutoTokenizer
from peft import PeftModel
import torch
base_model_name = "mistralai/Mistral-7B-Instruct-v0.1" # 相同的基础模型
adapter_path = "./results_rag_finetune/final_adapter" # 您保存的适配器路径
# 加载基础模型(推断时也可以量化)
# 用于4位推断:
# bnb_config = BitsAndBytesConfig(
# load_in_4bit=True,
# bnb_4bit_quant_type="nf4",
# bnb_4bit_compute_dtype=torch.bfloat16,
# bnb_4bit_use_double_quant=True,
# )
# base_model = AutoModelForCausalLM.from_pretrained(
# base_model_name,
# quantization_config=bnb_config,
# device_map={"": 0}
# )
# 或者不进行量化以获得全精度(显存占用更高)
base_model = AutoModelForCausalLM.from_pretrained(
base_model_name,
torch_dtype=torch.bfloat16, # 或 torch.float16
device_map={"": 0}
)
tokenizer = AutoTokenizer.from_pretrained(adapter_path) # 从适配器目录加载分词器
# 通过将适配器合并到基础模型来加载PEFT模型
model = PeftModel.from_pretrained(base_model, adapter_path)
model = model.eval() # 设置为评估模式
# 可选:将LoRA层与基础模型合并以加快推断速度
# 这会创建一个新模型,并且最初可能需要更多显存。
# model = model.merge_and_unload()
# print("LoRA层已合并。")
# 示例RAG风格提示
retrieved_context = "LoRA技术通过在现有层中插入可训练的低秩矩阵来调整大型预训练模型。与完全微调相比,这显著减少了可训练参数的数量,使其内存高效。"
user_query = "LoRA如何实现内存效率?"
prompt = f"<s>[INST] Context: {retrieved_context}\nQuestion: {user_query} [/INST]\nAnswer:"
inputs = tokenizer(prompt, return_tensors="pt").to(model.device)
# 生成回应
with torch.no_grad():
outputs = model.generate(
**inputs,
max_new_tokens=100,
do_sample=True,
temperature=0.7,
top_p=0.9,
pad_token_id=tokenizer.eos_token_id
)
response_text = tokenizer.decode(outputs[0], skip_special_tokens=True)
# 只提取生成的答案部分
answer_part = response_text.split("[/INST]\nAnswer:")[1].strip()
print(f"生成的答案: {answer_part}")
评估非常重要。像困惑度这样的标准LLM指标对于RAG来说是不足的。您需要评估:
比较分析: The最有效方式是显示改进的,是比较微调模型与基础模型在同一组(查询、上下文)对上的输出。
| 评估方面 | 基础模型行为(示例) | 微调模型行为(示例) |
|---|---|---|
| 忠实度 | 常引入外部知识或略微误解。 | 严格遵守所提供的文本。 |
| 幻觉 | 如果上下文稀疏或模棱两可,可能会编造细节。 | 更倾向于说明“无法回答”或谨慎地保持事实性。 |
| 相关性 | 可能过度总结或错过查询中的具体细节。 | 根据上下文更精确地针对查询。 |
| 风格/方面 | 通用语言。 | 采用微调数据中的术语/风格(如果存在)。 |
上表说明了潜在的改进。实际结果取决于数据质量、基础模型和调优情况。
对于大规模系统中的更系统化评估,可以考虑RAGAs等框架,它们提供忠实度、答案相关性和上下文相关性等指标。构建包含有挑战性RAG查询数据集的评估套件是一种最佳实践。
微调后的模型(无论是基础模型+LoRA适配器,还是合并后的模型)需要部署到您的LLM服务基础设施中(例如,vLLM、TGI、SageMaker等)。
merge_and_unload(),则将所得模型作为标准LLM部署。这有时可以提供略低的推断延迟,因为没有适配器逻辑开销,但您会失去轻松切换适配器的灵活性。考虑您的MLOps流程将如何处理再训练和部署新的适配器版本。与完全微调相比,PEFT方法明显简化了这一过程,因为适配器文件很小(兆字节而非千兆字节)。
本次动手练习展示了PEFT,特别是LoRA,如何成为一种强大工具,用于根据RAG系统的具体要求调整LLM。通过对强调上下文关联的数据进行微调,您可以明显改进RAG系统回应的忠实度和相关性。
专家实践者的后续步骤:
通过掌握这些技术,您可以构建高性能、可靠且高效的大规模分布式RAG系统,这些系统能够真正将LLM与外部知识结合使用。
简洁的语法。内置调试功能。从第一天起就可投入生产。
为 ApX 背后的 AI 系统而构建
这部分内容有帮助吗?
© 2026 ApX Machine Learning用心打造