自己动手微调大模型:使用PEFT库对Qwen进行指令微调
一、为什么需要微调通用大模型虽然能力强大但在特定领域往往表现不佳——它不懂专业术语不熟悉特定业务场景也不会按你期望的格式输出答案。监督微调Supervised Fine-Tuning, SFT就是解决这个问题的方法通过少量高质量的指令数据让模型学会理解和遵循特定任务的指令。但大模型参数量动辄数十亿全参数微调需要极高的硬件成本。以7B模型为例全量微调需要保存优化器状态Adam优化器需要保存参数的动量和方差额外占用8倍参数量的显存普通开发者根本负担不起。LoRALow-Rank Adaptation提供了一条出路。它冻结原始模型权重只训练极少的额外参数可训练参数量减少99%以上显存占用也大幅降低。本文就带大家使用Hugging Face的PEFT库对Qwen模型进行LoRA指令微调。二、环境准备建议使用Python 3.10及以上版本并创建独立的虚拟环境conda create-nqwen_lorapython3.10conda activate qwen_lora pipinstalltorch2.0.0 transformers4.37.0\datasets2.18.0 peft0.10.0 accelerate0.26.0\bitsandbytes硬件要求7B模型LoRA微调建议24GB及以上显存如果显存不足可选择更小的模型如1.5B版本或使用QLoRA4-bit量化微调。三、数据集准备指令微调的数据通常采用如下格式{instruction:回答以下用户问题仅输出答案。,input:高血压应该注意什么,output:高血压患者应注意控制盐分摄入、保持规律运动、定期监测血压。}其中instruction是任务指令input是用户输入output是期望模型给出的回答。Qwen模型使用ChatML格式的对话模板输入格式为|im_start|system 系统提示词|im_end| |im_start|user 用户问题|im_end| |im_start|assistant 模型回答|im_end|数据预处理函数fromtransformersimportAutoTokenizer tokenizerAutoTokenizer.from_pretrained(Qwen/Qwen-7B-Chat,trust_remote_codeTrue)tokenizer.pad_token_idtokenizer.eos_token_iddefprocess_func(example):MAX_LENGTH512# 构建system promptsystem_prompt你是一个专业的医疗问答助手。# 拼接instruction和inputuser_contentexample[instruction]ifexample[input]:user_content\nexample[input]# 构造对话格式instruction_texttokenizer(f|im_start|system\n{system_prompt}|im_end|\nf|im_start|user\n{user_content}|im_end|\nf|im_start|assistant\n,add_special_tokensFalse)response_texttokenizer(example[output]|im_end|\n,add_special_tokensFalse)input_idsinstruction_text[input_ids]response_text[input_ids]attention_maskinstruction_text[attention_mask]response_text[attention_mask]# labels: 只计算assistant部分的损失system和user部分用-100掩码labels[-100]*len(instruction_text[input_ids])response_text[input_ids]# 截断iflen(input_ids)MAX_LENGTH:input_idsinput_ids[:MAX_LENGTH]attention_maskattention_mask[:MAX_LENGTH]labelslabels[:MAX_LENGTH]return{input_ids:input_ids,attention_mask:attention_mask,labels:labels}labels中值为-100的位置在计算交叉熵损失时会被忽略因此只有助手回答部分的token参与梯度更新用户问题和系统提示不参与损失计算。四、加载模型并配置LoRA4.1 加载基座模型importtorchfromtransformersimportAutoModelForCausalLMfrompeftimportLoraConfig,get_peft_model,TaskType model_pathQwen/Qwen-7B-ChatmodelAutoModelForCausalLM.from_pretrained(model_path,torch_dtypetorch.bfloat16,# 混合精度节省显存device_mapauto,trust_remote_codeTrue)tokenizerAutoTokenizer.from_pretrained(model_path,trust_remote_codeTrue)tokenizer.pad_token_idtokenizer.eos_token_id4.2 配置LoRAlora_configLoraConfig(task_typeTaskType.CAUSAL_LM,target_modules[c_attn,c_proj,w1,w2],# Qwen-7B的注意力层名称r8,# LoRA秩通常4-16之间lora_alpha32,# 缩放因子有效学习率约为alpha/rlora_dropout0.1,# Dropout防止过拟合biasnone,inference_modeFalse)modelget_peft_model(model,lora_config)model.print_trainable_parameters()输出类似trainable params: 4,390,912 || all params: 500,000,000 || trainable%: 0.884.3 LoRA参数说明r秩低秩矩阵的维度。r越大模型容量越高但参数也越多4-16能覆盖绝大多数场景。lora_alpha缩放因子。LoRA更新的有效学习率相当于alpha/r一般设alpha2r是不错的起点。target_modules决定LoRA挂载到哪些层。注意力层的q_proj和v_proj是最常见的选择。五、训练配置与启动5.1 加载数据集fromdatasetsimportload_datasetfromtransformersimportDataCollatorForSeq2Seq# 加载医疗问答数据集datasetload_dataset(shibing624/medical,splittrain)datasetdataset.select(range(1000))# 取前1000条用于演示# 应用预处理tokenized_datasetdataset.map(process_func,remove_columnsdataset.column_names)data_collatorDataCollatorForSeq2Seq(tokenizertokenizer,modelmodel,paddingTrue,label_pad_token_id-100)5.2 训练参数fromtransformersimportTrainingArguments,Trainer training_argsTrainingArguments(output_dir./output/qwen-lora-medical,per_device_train_batch_size4,gradient_accumulation_steps4,# 显存不足时可增大此值learning_rate1e-4,num_train_epochs3,logging_steps50,save_steps100,evaluation_strategyno,fp16True,# 混合精度gradient_checkpointingTrue,# 用计算换显存save_total_limit2,remove_unused_columnsFalse,warmup_steps100,)关键参数解读per_device_train_batch_size每张卡的batch大小根据显存调整gradient_accumulation_steps梯度累积步数有效batch batch_size × 累积步数gradient_checkpointing用30%额外计算换取显存建议开启fp16混合精度训练显存节省约50%5.3 启动训练trainerTrainer(modelmodel,argstraining_args,train_datasettokenized_dataset,data_collatordata_collator,)trainer.train()# 保存LoRA适配器model.save_pretrained(./output/qwen-lora-final)tokenizer.save_pretrained(./output/qwen-lora-final)六、推理验证训练完成后加载LoRA权重进行推理并与基座模型对比效果importgcfrompeftimportPeftModeldefload_lora_model(lora_pathNone):base_modelAutoModelForCausalLM.from_pretrained(model_path,torch_dtypetorch.bfloat16,device_mapauto,trust_remote_codeTrue)iflora_path:modelPeftModel.from_pretrained(base_model,lora_path)else:modelbase_modelreturnmodeldefgenerate_response(model,user_input,system_prompt你是一个专业的医疗问答助手。):messages[{role:system,content:system_prompt},{role:user,content:user_input}]texttokenizer.apply_chat_template(messages,tokenizeFalse,add_generation_promptTrue)inputstokenizer(text,return_tensorspt).to(model.device)withtorch.no_grad():outputsmodel.generate(**inputs,max_new_tokens256,temperature0.7,top_p0.9,do_sampleTrue)returntokenizer.decode(outputs[0],skip_special_tokensTrue)# 基座模型base_modelload_lora_model()print(基座模型回答,generate_response(base_model,高血压应该注意什么))delbase_model gc.collect()torch.cuda.empty_cache()# LoRA模型lora_modelload_lora_model(./output/qwen-lora-final)print(LoRA微调后回答,generate_response(lora_model,高血压应该注意什么))七、常见问题1. 显存不足OOM减小per_device_train_batch_size到2或1增大gradient_accumulation_steps或使用QLoRA4-bit量化进一步压缩。2. 训练loss不下降检查labels中是否只保留了assistant部分参与损失计算尝试降低学习率至1e-4以下。3. target_modules如何选择不同模型的注意力层名称不同。Qwen-7B-Chat使用[c_attn, c_proj, w1, w2]Qwen1.5及更新版本一般使用[q_proj, k_proj, v_proj, o_proj]。可通过model.named_parameters()查看模型层名称确认。八、结语LoRA让大模型微调不再是A100的专属。通过PEFT库只需几行配置就能将可训练参数压缩到1%以下在消费级显卡上完成对Qwen等大模型的指令微调。本文从数据准备、模型加载到训练推理提供了完整的代码实现所有脚本均可直接运行。进阶方向包括尝试不同秩r4/8/16的对比实验、使用QLoRA进一步降低显存门槛、或在更多垂直领域数据上验证微调效果。