LLM 微调实战:用 LoRA/QLoRA 打造你的领域专家模型

📅 2026/6/26 6:44:03
LLM 微调实战:用 LoRA/QLoRA 打造你的领域专家模型
本文是「AI 应用开发进阶实战」系列的扩展篇假设你已了解基础概念直接进入工程实战。一、什么时候该微调什么时候不该微调很多团队上来就想微调但微调不是银弹。先看决策树你的需求 ├─ 模型已有该知识只是表现不稳定 → Prompt Engineering ├─ 需要特定格式输出JSON/SQL/代码→ Few-shot Function Calling ├─ 需要注入专有领域知识 → RAG本系列第1篇 ├─ 需要改变模型行为风格 → 微调 └─ 既要知识又要风格 → RAG 微调微调的典型场景客服机器人需要特定的语气、话术模板代码审查需要特定的审查维度和输出格式医疗问诊需要严谨的追问逻辑文本分类/实体抽取传统 NLP 任务用 LLM 替代不该微调的场景数据量 100 条高质量样本知识频繁更新用 RAG 更合适只是想要 JSON 输出Function Calling 就够二、LoRA 原理30 秒版全量微调 7B 模型需要 ~56GB 显存。LoRA 的核心洞察全量微调更新 W (d×k) → 参数量 d×k LoRA冻结 W只训练两个小矩阵 A (d×r) 和 B (r×k) W W α·A·B 参数量d×r r×kr 通常 8~64远小于 d 和 k 显存7B 模型用 LoRA 只需 ~12GB消费级显卡就行QLoRA 进一步把原始模型量化到 4-bit显存降到 ~6GB。三、环境搭建pipinstalltransformers datasets peft bitsandbytes accelerate trl wandb关键库库作用transformers模型加载、推理datasets数据集加载和处理peftLoRA/QLoRA 实现bitsandbytes4-bit 量化trlSFT/DPO/RLHF 训练循环accelerate分布式训练四、数据准备格式和质量比数量重要4.1 对话格式# 标准 ShareGPT 格式推荐training_data[{conversations:[{role:system,content:你是资深 Python 代码审查专家...},{role:user,content:帮我审查这段代码},{role:assistant,content:发现以下问题\n1. ...}]},# ...更多样本]4.2 数据质量检查脚本importjsonfromtransformersimportAutoTokenizer tokenizerAutoTokenizer.from_pretrained(Qwen/Qwen2.5-7B-Instruct)defvalidate_dataset(filepath:str,max_length:int4096):检查数据集质量withopen(filepath,r,encodingutf-8)asf:datajson.load(f)stats{total:len(data),too_long:0,empty:0,no_system:0}lengths[]foritemindata:convsitem.get(conversations,[])ifnotconvs:stats[empty]1continueifconvs[0].get(role)!system:stats[no_system]1# 计算 token 长度texttokenizer.apply_chat_template(convs,tokenizeFalse)tokenslen(tokenizer.encode(text))lengths.append(tokens)iftokensmax_length:stats[too_long]1print(f总样本:{stats[total]})print(f超长({max_length}tokens):{stats[too_long]}({stats[too_long]/stats[total]*100:.1f}%))print(f空样本:{stats[empty]})print(f无 system prompt:{stats[no_system]})print(f平均长度:{sum(lengths)/len(lengths):.0f}tokens)print(f中位数长度:{sorted(lengths)[len(lengths)//2]}tokens)returnstats validate_dataset(./data/training_data.json)4.3 数据增强技巧defaugment_dataset(data:list,multiplier:int3)-list:用 LLM 生成变体扩充数据集fromopenaiimportOpenAI clientOpenAI()augmentedlist(data)# 保留原始数据foritemindata[:50]:# 取前 50 条做增强for_inrange(multiplier-1):responseclient.chat.completions.create(modelgpt-4o-mini,messages[{role:system,content:改写以下对话样本保持相同的任务类型和难度 但改变具体的代码示例、问题描述和答案措辞。保持 system prompt 不变。},{role:user,content:json.dumps(item,ensure_asciiFalse)}])try:new_itemjson.loads(response.choices[0].message.content)augmented.append(new_item)except:passprint(f原始:{len(data)}→ 增强后:{len(augmented)})returnaugmented五、QLoRA 微调完整代码# finetune.pyimporttorchfromdatasetsimportload_datasetfromtransformersimport(AutoTokenizer,AutoModelForCausalLM,BitsAndBytesConfig,TrainingArguments,TrainerCallback,)frompeftimportLoraConfig,get_peft_model,prepare_model_for_kbit_trainingfromtrlimportSFTTrainerimportjsonimportosclassFineTuner:QLoRA 微调流水线def__init__(self,model_name:strQwen/Qwen2.5-7B-Instruct,output_dir:str./lora-output,):self.model_namemodel_name self.output_diroutput_dir# 4-bit 量化配置self.bnb_configBitsAndBytesConfig(load_in_4bitTrue,bnb_4bit_quant_typenf4,# NormalFloat4效果最好bnb_4bit_compute_dtypetorch.bfloat16,bnb_4bit_use_double_quantTrue,# 双重量化再省 0.4 bit)# LoRA 配置self.lora_configLoraConfig(r16,# rank越大容量越大但越慢lora_alpha32,# 缩放因子通常 r 的 2 倍target_modules[# 要训练的模块Qwen 系列q_proj,k_proj,v_proj,o_proj,gate_proj,up_proj,down_proj,],lora_dropout0.05,# 正则化biasnone,task_typeCAUSAL_LM,)defload_model(self):加载量化模型print(fLoading{self.model_name}...)modelAutoModelForCausalLM.from_pretrained(self.model_name,quantization_configself.bnb_config,device_mapauto,# 自动分配层到 GPUtrust_remote_codeTrue,torch_dtypetorch.bfloat16,attn_implementationflash_attention_2,# Flash Attention)tokenizerAutoTokenizer.from_pretrained(self.model_name,trust_remote_codeTrue,padding_sideright,)# 设置 pad_tokeniftokenizer.pad_tokenisNone:tokenizer.pad_tokentokenizer.eos_token# 准备 k-bit 训练modelprepare_model_for_kbit_training(model)# 注入 LoRAmodelget_peft_model(model,self.lora_config)model.print_trainable_parameters()# 输出: trainable params: ~40M || all params: ~7B || trainable%: 0.6%returnmodel,tokenizerdeftrain(self,data_path:str,num_epochs:int3):执行训练model,tokenizerself.load_model()# 加载数据datasetload_dataset(json,data_filesdata_path,splittrain)# 格式化函数defformat_conversation(example):convsexample[conversations]return{text:tokenizer.apply_chat_template(convs,tokenizeFalse,add_generation_promptFalse)}datasetdataset.map(format_conversation)# 训练参数training_argsTrainingArguments(output_dirself.output_dir,num_train_epochsnum_epochs,per_device_train_batch_size4,gradient_accumulation_steps4,# 有效 batch_size16gradient_checkpointingTrue,# 省显存optimpaged_adamw_8bit,# 8-bit 优化器learning_rate2e-4,lr_scheduler_typecosine,warmup_ratio0.03,logging_steps10,save_steps200,save_total_limit3,# 只保留最后 3 个 checkpointfp16False,bf16True,max_grad_norm0.3,report_towandb,# 可选用 WB 监控run_nameflora-{self.model_name.split(/)[-1]},)# SFT TrainertrainerSFTTrainer(modelmodel,argstraining_args,train_datasetdataset,tokenizertokenizer,max_seq_length4096,dataset_text_fieldtext,packingFalse,# False 更稳定)# 开始训练print(f\n开始训练{num_epochs}epochs...)trainer.train()# 保存最终模型final_pathos.path.join(self.output_dir,final)trainer.save_model(final_path)tokenizer.save_pretrained(final_path)print(f\n模型保存到:{final_path})returnfinal_path# 使用 if__name____main__:tunerFineTuner(model_nameQwen/Qwen2.5-7B-Instruct,output_dir./my-code-reviewer-lora,)model_pathtuner.train(data_path./data/code_review_data.json,num_epochs3,)六、训练监控看懂 Loss 曲线正常训练 Loss │ ╲ │ ╲___ │ ╲___ ← 趋于平稳 │ ╲___ └─────────────────→ Step 说明学习率合适收敛正常 过拟合 Loss │ ╲ │ ╲___ │ ╲___ ← train loss 还在降 │ eval: ─────╱ ← eval loss 开始上升 └─────────────────→ Step 对策减少 epoch、增加 lora_dropout、减少 rank 欠拟合 Loss │ ╲___ │ ╲_________ ← 平了就再也不降了 └─────────────────→ Step 对策增加 epoch、提高学习率、增加 rank七、模型合并与推理LoRA 训练出来的是适配器adapter需要合并回基础模型才能直接用。# merge.pyimporttorchfromtransformersimportAutoModelForCausalLM,AutoTokenizerfrompeftimportPeftModeldefmerge_lora(base_model:str,lora_path:str,output_path:str,push_to_hub:boolFalse,):合并 LoRA 权重到基础模型print(f加载基础模型:{base_model})# 加载基础模型不需要量化modelAutoModelForCausalLM.from_pretrained(base_model,torch_dtypetorch.bfloat16,device_mapauto,trust_remote_codeTrue,)tokenizerAutoTokenizer.from_pretrained(base_model)# 加载 LoRA 权重print(f加载 LoRA:{lora_path})modelPeftModel.from_pretrained(model,lora_path)# 合并print(合并权重...)modelmodel.merge_and_unload()# 保存model.save_pretrained(output_path,safe_serializationTrue)tokenizer.save_pretrained(output_path)print(f✅ 合并完成:{output_path})# 可选推到 HuggingFace Hubifpush_to_hub:model.push_to_hub(output_path)tokenizer.push_to_hub(output_path)returnoutput_path# 推理测试deftest_model(model_path:str):测试微调后的模型fromtransformersimportpipeline pipepipeline(text-generation,modelmodel_path,torch_dtypetorch.bfloat16,device_mapauto,)test_cases[请审查以下 Python 代码的安全问题\npython\ndef login(user, pwd):\n query f\SELECT * FROM users WHERE name{user} AND pwd{pwd}\\n return db.execute(query)\n,用项目符号列出这段代码的三个性能问题,]fori,testinenumerate(test_cases):print(f\n{*50})print(fTest{i1}:{test[:60]}...)resultpipe(test,max_new_tokens512,temperature0.3,do_sampleTrue,)print(result[0][generated_text][-300:])# 使用merge_lora(base_modelQwen/Qwen2.5-7B-Instruct,lora_path./my-code-reviewer-lora/final,output_path./my-code-reviewer-merged,)八、实战经验与踩坑8.1 超参数速查表参数推荐值说明LoRA rank (r)8-32数据 1K 用 8 10K 用 32LoRA alphar×2rank 的 2 倍Learning rate1e-4 ~ 5e-4QLoRA 可以略高于全量微调Batch size (有效)16-64grad_accum × per_deviceEpochs1-5 1K 数据用 3-5 10K 用 1-2Max seq length2048-4096看你的数据分布lora_dropout0.05-0.1小数据集用 0.1 防过拟合8.2 常见报错CUDA out of memory → 减小 batch_size增大 gradient_accumulation_steps → 减小 max_seq_length → 去掉 flash_attention_2 loss is NaN → 降低 learning rate → 检查数据是否有空字符串或超长文本 → 尝试 fp32 代替 bf16 训练后模型输出乱码 → 检查 tokenizer 的 chat_template 是否正确 → 检查训练数据的 role 格式 → 合并模型时确保用了正确的 base model 训练太慢 → 开启 flash_attention_2需安装 flash-attn → 增大 batch_size → 使用 packingTrue注意可能导致 loss 异常8.3 评估微调效果defevaluate_finetune(base_model:str,finetuned_model:str,test_data:list):对比微调前后效果fromtransformersimportpipeline basepipeline(text-generation,modelbase_model,device_mapauto)finetunedpipeline(text-generation,modelfinetuned_model,device_mapauto)scores{base_win:0,finetuned_win:0,tie:0}foritemintest_data:promptitem[conversations][-2][content]# 最后一个 user 消息expecteditem[conversations][-1][content]# assistant 回复base_outbase(prompt,max_new_tokens256)[0][generated_text]ft_outfinetuned(prompt,max_new_tokens256)[0][generated_text]# 用 GPT-4 做裁判或人工评估judgeask_gpt4_judge(prompt,expected,base_out,ft_out)ifjudgebase:scores[base_win]1elifjudgefinetuned:scores[finetuned_win]1else:scores[tie]1print(f基准模型胜:{scores[base_win]})print(f微调模型胜:{scores[finetuned_win]})print(f平局:{scores[tie]})returnscores九、总结微调决策数据 100 条 需要风格改变 → LoRA/QLoRA 数据准备ShareGPT 格式 质量检查 可选增强 训练核心QLoRA(4bit) rank16 lr2e-4 3 epochs 效果验证GPT-4 裁判对比 人工抽检成本参考7B 模型1K 样本3 epochs显存~10GBQLoRA单卡 RTX 3090/4090时间~2 小时RTX 4090费用如果用云 GPUA10G约 $3-5微调不是什么高深技术工程上最难的是搞到高质量标注数据。100 条精心标注的数据远比 10000 条 AI 生成的劣质数据有用。