既然我们已经在前面章节了解了LoRA和QLoRA的理论基础,现在让我们转向实际操作。本次动手练习将指导您使用LoRA和QLoRA技术对大型语言模型进行微调。您将直接体验这些方法的配置、训练过程的执行,并观察与完全微调相比的效率提升。我们假设您在具有合适GPU资源的环境中操作,并已安装了所需库,例如transformers、peft、accelerate、datasets和bitsandbytes。准备工作:环境和基础模型首先,确保您的环境配置正确。peft、accelerate和bitsandbytes库对于实现LoRA,尤其是QLoRA非常重要。我们将从加载预训练的基础模型开始。本次练习,我们选用meta-llama/Llama-2-7b-hf或类似大小的Transformer模型。访问某些模型可能需要通过Hugging Face Hub等平台进行身份验证。import torch from transformers import AutoModelForCausalLM, AutoTokenizer, BitsAndBytesConfig from peft import LoraConfig, get_peft_model, prepare_model_for_kbit_training from datasets import load_dataset import transformers # 定义基础模型ID model_id = "meta-llama/Llama-2-7b-hf" # 或另一个合适的模型 # 如果模型需要,请使用身份验证令牌 # from huggingface_hub import login # login() # 加载分词器 tokenizer = AutoTokenizer.from_pretrained(model_id) # 如果缺少填充令牌,则设置填充令牌 if tokenizer.pad_token is None: tokenizer.pad_token = tokenizer.eos_token # 加载示例数据集(例如,指令微调) # 将 'databricks/databricks-dolly-15k' 替换为您目标数据集 data = load_dataset("databricks/databricks-dolly-15k", split="train[:1000]") # 为提高速度使用子集 # 预处理数据 def format_instruction(sample): # 根据所选数据集结构调整格式 return f"""### 指令: {sample['instruction']} ### 上下文: {sample['context']} ### 回复: {sample['response']} """ data = data.map(lambda sample: tokenizer(format_instruction(sample), truncation=True, max_length=512, padding="max_length")) print("环境设置和数据准备完成。")此初始设置加载分词器和示例数据集,为微调过程做准备。特定的数据集和预处理函数(format_instruction)应根据您的目标任务(例如,摘要、问答、指令遵循)进行调整。使用LoRA进行微调LoRA通过在特定层(通常是注意力机制的线性投影)中引入低秩矩阵来调整模型。我们使用LoraConfig进行配置。LoRA配置LoraConfig对象定义了LoRA的应用方式:r: 更新矩阵的秩。较小的r意味着更少的训练参数。常见值范围为8到64。lora_alpha: LoRA更新的缩放因子,通常设置为2 * r。target_modules: 基础模型中将注入LoRA矩阵的模块名称列表(例如,注意力机制中查询和值投影的['q_proj', 'v_proj'])。lora_dropout: 应用于LoRA层的Dropout概率。bias: 指定偏差的处理方式('none'、'all'或'lora_only')。通常设置为'none'。task_type: 任务类型(例如,"CAUSAL_LM")。# 加载基础模型(确保VRAM充足) model = AutoModelForCausalLM.from_pretrained( model_id, device_map="auto", # 自动分布到可用GPU上 torch_dtype=torch.float16 # 使用float16以减少内存 ) # 定义LoRA配置 lora_config = LoraConfig( r=16, # 更新矩阵的秩 lora_alpha=32, # 缩放因子 alpha target_modules=["q_proj", "v_proj"], # 将LoRA应用于查询和值投影 lora_dropout=0.05, # Dropout概率 bias="none", # 不训练偏差 task_type="CAUSAL_LM" # 任务类型 ) # 使用PeftModel封装基础模型 model = get_peft_model(model, lora_config) # 打印可训练参数的百分比 model.print_trainable_parameters() # 示例输出:可训练参数:4,194,304 || 所有参数:6,742,609,920 || 可训练%:0.0622print_trainable_parameters方法提供关于参数效率的即时反馈。请注意可训练参数占总参数的百分比很小。训练LoRA Adapter我们现在可以使用transformers.Trainer进行训练。它自动处理PEFT模型,确保只更新LoRA参数。# 定义训练参数 training_args = transformers.TrainingArguments( output_dir="./lora_finetuned_model", per_device_train_batch_size=4, gradient_accumulation_steps=4, learning_rate=2e-4, num_train_epochs=1, # 根据数据集大小和收敛情况调整训练轮数 logging_steps=10, save_steps=50, fp16=True, # 使用混合精度训练 # 添加其他相关参数,如评估策略、权重衰减等 ) # 初始化Trainer trainer = transformers.Trainer( model=model, args=training_args, train_dataset=data, data_collator=transformers.DataCollatorForLanguageModeling(tokenizer, mlm=False), ) # 开始训练 print("开始LoRA微调...") trainer.train() print("LoRA微调完成。") # 保存训练好的LoRA adapter lora_adapter_path = "./lora_adapter" model.save_pretrained(lora_adapter_path) print(f"LoRA adapter 已保存到 {lora_adapter_path}")LoRA推理要进行推理,请加载原始基础模型,然后应用保存的LoRA adapter权重。from peft import PeftModel # 再次加载基础模型(如果尚未在内存中) base_model = AutoModelForCausalLM.from_pretrained( model_id, torch_dtype=torch.float16, device_map="auto" ) # 加载LoRA adapter model_with_adapter = PeftModel.from_pretrained(base_model, lora_adapter_path) model_with_adapter.eval() # 将模型设置为评估模式 # 推理示例 prompt = "### 指令:\nLoRA的主要优点是什么?\n\n### 回复:\n" inputs = tokenizer(prompt, return_tensors="pt").to(model_with_adapter.device) with torch.no_grad(): outputs = model_with_adapter.generate(**inputs, max_new_tokens=100) print("生成回复 (LoRA):") print(tokenizer.decode(outputs[0], skip_special_tokens=True))使用QLoRA进行微调QLoRA在LoRA的基础上,通过使用bitsandbytes将基础模型量化到4位精度。这大幅减少了加载和微调期间的内存占用,使得在消费级硬件上微调更大的模型成为可能。QLoRA配置和模型加载主要区别在于基础模型的加载方式。我们使用BitsAndBytesConfig来指定4位量化参数。# 定义量化配置 bnb_config = BitsAndBytesConfig( load_in_4bit=True, # 启用4位量化 bnb_4bit_quant_type="nf4", # 使用NF4(Normal Float 4)数据类型 bnb_4bit_compute_dtype=torch.bfloat16, # 计算数据类型以加快训练 bnb_4bit_use_double_quant=True, # 使用双重量化以额外节省内存 ) # 使用量化配置加载基础模型 qlora_model = AutoModelForCausalLM.from_pretrained( model_id, quantization_config=bnb_config, device_map="auto", # 对于量化模型的分布很重要 ) # 准备量化模型进行k位训练 qlora_model = prepare_model_for_kbit_training(qlora_model) # 定义LoRA配置(可以与之前相同) qlora_config = LoraConfig( r=16, lora_alpha=32, target_modules=["q_proj", "v_proj"], # 可能需要根据模型架构和观察到的稳定性进行调整 lora_dropout=0.05, bias="none", task_type="CAUSAL_LM" ) # 使用PeftModel封装量化模型 qlora_model = get_peft_model(qlora_model, qlora_config) # 打印可训练参数 qlora_model.print_trainable_parameters()prepare_model_for_kbit_training函数执行必要的调整,例如将层归一化和语言模型头部转换为float32以提高稳定性。LoraConfig保持相似,但现在应用于4位量化的基础模型。QLoRA训练和推理训练和推理过程与LoRA所用的相同。您可以重用相同的transformers.Trainer设置和推理代码。主要优点是在trainer.train()调用期间内存消耗大幅降低。# 重用或重新定义训练参数 qlora_training_args = transformers.TrainingArguments( output_dir="./qlora_finetuned_model", per_device_train_batch_size=4, # 由于内存使用量较低,可能会增加批量大小 gradient_accumulation_steps=4, learning_rate=2e-4, num_train_epochs=1, logging_steps=10, save_steps=50, fp16=False, # QLoRA 使用在BitsAndBytesConfig中指定的bf16计算类型 bf16=True, # 启用bf16训练 # 添加其他相关参数 ) # 为QLoRA初始化Trainer qlora_trainer = transformers.Trainer( model=qlora_model, args=qlora_training_args, train_dataset=data, data_collator=transformers.DataCollatorForLanguageModeling(tokenizer, mlm=False), ) # 开始QLoRA训练 print("开始QLoRA微调...") qlora_trainer.train() print("QLoRA微调完成。") # 保存QLoRA adapter qlora_adapter_path = "./qlora_adapter" qlora_model.save_pretrained(qlora_adapter_path) print(f"QLoRA adapter 已保存到 {qlora_adapter_path}") # 推理遵循与LoRA相同的模式,将adapter加载到量化的基础模型上 # 确保基础模型使用与训练时相同的BitsAndBytesConfig加载评估与分析使用两种方法训练adapter后,进行细致的评估很有必要。任务表现:使用任务特定指标(例如,语言模型的困惑度、摘要的ROUGE、分类的准确率)在保留的测试集上评估LoRA和QLoRA微调后的模型。将这些结果与基础模型的表现进行比较,如果可能,再与完全微调的模型进行比较。资源消耗:记录LoRA和QLoRA训练期间的峰值GPU内存使用量。注意训练时间差异。QLoRA应展现明显更低的内存需求。参数数量:比较两种配置中由print_trainable_parameters()报告的可训练参数数量。如果LoraConfig相同,它们应该相同,这表明QLoRA的内存节省来自基础模型量化,而非更少的adapter参数。考虑可视化这些权衡:{"layout": {"title": "PEFT方法对比(示例)", "xaxis": {"title": "方法"}, "yaxis": {"title": "峰值GPU显存 (GB)", "range": [0, 25]}, "yaxis2": {"title": "可训练参数 (%)", "overlaying": "y", "side": "right", "range": [0, 0.1], "showgrid": false}, "legend": {"x": 0.1, "y": 0.9}}, "data": [{"type": "bar", "name": "峰值显存 (GB)", "x": ["完全微调", "LoRA", "QLoRA"], "y": [40, 18, 10], "marker": {"color": "#228be6"}}, {"type": "scatter", "name": "可训练参数 (%)", "x": ["完全微调", "LoRA", "QLoRA"], "y": [100, 0.062, 0.062], "yaxis": "y2", "mode": "lines+markers", "line": {"color": "#f76707"}, "marker": {"symbol": "diamond", "size": 8}}]}示例性对比展示了完全微调、LoRA和QLoRA的潜在显存使用量和可训练参数百分比。实际值很大程度上取决于模型大小、硬件和批量大小。请注意,为了突出显示,此处完全微调的显存使用量超出了图表范围。讨论与考虑本次实践练习说明了LoRA和QLoRA在高效LLM微调中的实现。LoRA:与完全微调相比,提供了显著的参数效率,减少了计算和存储需求,同时通常保持高任务性能。QLoRA:通过量化基础模型,进一步大幅节省内存,使得在可用硬件上微调非常大的模型成为可能。然而,4位量化在某些复杂任务上可能引入轻微的性能下降。需要细致评估。在它们之间进行选择时,请考虑可用的硬件资源和所需的性能保真度。如果内存是主要限制,QLoRA是一个很好的选择。如果需要最大性能且内存允许,标准的LoRA(如果资源允许,甚至完全微调)可能是更优选择。超参数调整,特别是r、lora_alpha和学习率,对于两种技术优化结果仍然很重要。