行业资讯

大模型之基于PEFT的SFT微调实战篇

发布时间:2026/7/30 10:21:07
大模型之基于PEFT的SFT微调实战篇 1、核心知识点VIP1PEFT LoRA插入target modules的方法它不会修改磁盘上原始模型代码/权重文件全部是运行时内存层面动态修改模型对象。对于q_proj、k_proj、v_proj、o_proj原始是普通nn.Linear比如q_proj nn.Linear(hidden_size, num_heads * head_dim)磁盘上的模型文件 (.bin/.safetensors) 保存的就是这些 Linear 的 weight、bias。对于get_peft_model(model, lora_config)操作PEFT 内部逻辑遍历模型所有子模块递归寻找名字匹配target_modules的层q_proj/k_proj/v_proj/o_proj把原来的torch.nn.Linear对象用一个LoraLinear包装类原地替换掉内存替换保存原始 Linear 权重不改动、冻结新增两套小矩阵lora_A、lora_B。内存中替换过程 原来 q_proj nn.Linear(in_dim, out_dim) 替换后 q_proj LoraLinear( 原始的nn.Linear(冻结), lora_A, lora_B )前向传播同时跑两路基座输出 LoRA 分支输出相加。反向传播只更新 A、B基座 Wbase 梯度关闭不更新。2CausalLM交叉熵Loss计算流程1. 因果LLM训练模型每个位置输出词表维度logits2. 内部自动错位用第 i 位置logits预测 i1 的真实token3. 对每个位置先对该位置完整词表logits做softmax归一化得到词表上的概率分布取出真实标签对应的预测概率4. 每个有效token计算负对数似然labels-100的token直接跳过不参与计算5. 整个batch把所有有效token的loss求和除以batch全部有效token总数得到最终标量loss6. loss.backward回传梯度更新LoRA参数7. 训练阶段不采样生成token直接基于logits概率计算交叉熵3采样/解码方法1. 贪心搜索每一步直接选取当前概率最大 token速度快输出容易重复2.beam‑search 束搜索维护 num_beams 条候选序列选择整体概率最优序列输出通顺但多样性不足3. 随机采样 基于 softmax 概率做随机采样temperature 控制随机程度top‑p 核采样保留累积概率 p 的候选集是对话模型主流解码方式。4SFT交叉熵逐token计算的问题在 PEFT 做 SFT 训练时交叉熵是逐 token 计算负对数似然。假如模型输出的回答和真实标签 ground‑truth 语义相近但句式、表达方式差别很大token 序列并不相同是不是就会产生很大的 loss为什么会出现这种现象该问题如何缓解【回答】PEFT‑SFT 用的是 token‑level 交叉熵损失只比对 token ID不理解语义。即使模型输出语义接近、表达方式差异很大只要 token 序列和 ground‑truth 不一致对应位置负对数损失就会很大。这是 SFT 天然短板训练阶段强制字面对齐但推理阶段通过采样可以输出同义改写。SFT loss 代表对训练集文本的拟合程度不等于语义效果loss 太低容易发生过拟合背诵原文。RLHF/DPO 偏好对齐就是用来缓解该问题不再把单条 ground‑truth 当作唯一标准答案而是学习回答之间相对优劣容纳多种合理表达方式。注意 DPO 依旧基于 token 概率不是直接计算语义向量相似度。2、环境准备AutoDL1modelscope相关pip install modelscope下载Qwen2.5-7B-Instruct模型modelscope download --model Qwen/Qwen2.5-7B-Instruct2安装相关依赖包pip install transformers peft accelerate datasets trl bitsandbytes sentencepiece wandb3测试qwen2.5模型from modelscope import AutoModelForCausalLM, AutoTokenizer model_name Qwen/Qwen2.5-7B-Instruct model AutoModelForCausalLM.from_pretrained( model_name, torch_dtypeauto, device_mapauto ) tokenizer AutoTokenizer.from_pretrained(model_name) def chat_infer(messages, max_new_tokens512, temperature0.7, top_p0.8): :param messages: 对话list [{role:system/user/assistant, content:str}] :return: model回复字符串 # 套用模型对话模板推理务必 add_generation_promptTrue text tokenizer.apply_chat_template( messages, tokenizeFalse, add_generation_promptTrue ) print(f\n 模型输入完整模板文本 \n{text}\n 模型输出 \n) model_inputs tokenizer([text], return_tensorspt).to(model.device) input_len model_inputs.input_ids.shape[1] generated_ids model.generate( **model_inputs, max_new_tokensmax_new_tokens, temperaturetemperature, top_ptop_p, do_sampleTrue, ) # 切掉输入部分只保留新生成token output_ids generated_ids[:, input_len:] response tokenizer.batch_decode(output_ids, skip_special_tokensTrue)[0] return response if __name__ __main__: prompt Give me a short introduction to large language model. messages [ {role: system, content: You are Qwen, created by Alibaba Cloud. You are a helpful assistant.}, {role: user, content: prompt} ] resp chat_infer(messages) print(resp)显示结果3、准备SFT数据集医疗问诊数据_SFT格式数据示例{instruction: 你好医生我不知道自己怀孕的情况下感冒了输了利巴韦林和头孢我怀孕有四十多天了请问我可以要这小孩吗, input: , output: 首先请您放心感冒时使用利巴韦林和头孢是常规治疗方式一般不会对胎儿产生影响。不过如果您确实怀孕了建议您尽快去医院进行产前检查让医生评估胎儿的情况确定是否可以继续妊娠。如果胎儿的发育和健康状况正常您可以选择继续妊娠如果存在风险或者胎儿异常您可以考虑终止妊娠。最好在专业医生的指导下做出决策。}这个是Alpaca格式instruction/input/output不能直接进行训练。我们的SFT需要的格式是{messages: [system,user,assistant]} ChatML 格式需要转换成Qwen要求的message格式{ messages: [ {role:system,content:你是专业的医疗咨询助手请给出严谨、客观的医疗建议提醒用户以线下医生诊断为准。}, {role:user,content:你好医生我不知道自己怀孕的情况下感冒了输了利巴韦林和头孢我怀孕有四十多天了请问我可以要这小孩吗}, {role:assistant,content:首先请您放心感冒时使用利巴韦林和头孢是常规治疗方式……做出决策。} ] }格式转换脚本convert_medical.pyimport json src_file rC:\Users\benbe\Downloads\med_zh\med_dev_zh.json sys_text 你是专业的医疗咨询助手回答仅供科普参考不能替代执业医师面诊诊疗请遵从线下医生的专业意见。 out_list [] with open(src_file,r,encodingutf-8) as f: for line in f: line line.strip() if not line: continue d json.loads(line) user_content d[instruction] # 如果input不为空拼接到用户提问后面 if d.get(input,).strip(): user_content \n d[input].strip() new_item { messages:[ {role:system,content:sys_text}, {role:user,content:user_content}, {role:assistant,content:d[output]} ] } out_list.append(new_item) # 8:2划分训练集、验证集 split_pos int(len(out_list)*0.8) with open(train.jsonl,w,encodingutf-8) as fw: for x in out_list[:split_pos]: fw.write(json.dumps(x,ensure_asciiFalse)\n) with open(val.jsonl,w,encodingutf-8) as fw: for x in out_list[split_pos:]: fw.write(json.dumps(x,ensure_asciiFalse)\n) print(f总样本:{len(out_list)}训练集{split_pos}条验证集{len(out_list)-split_pos}条)拿出一条转化后的样本进行测试确认模板格式是否满足基座模型的格式要求测试脚本from transformers import AutoTokenizer tokenizerAutoTokenizer.from_pretrained(/root/autodl-tmp/models/Qwen2.5-7B-Instruct,trust_remote_codeTrue) sample {messages: [{role: system, content: 你是专业的医疗咨询助手回答仅供科普参考不能替代执业医师面诊诊疗请遵从线下医生的专业意见。}, {role: user, content: 哺乳后胸太小自己个字较高体重偏瘦胸部太平}, {role: assistant, content: 哺乳后胸部变小是正常的生理现象因为哺乳会导致乳房内的脂肪组织减少。如果您希望增大胸部可以考虑以下方法\n\n1. 进行胸部锻炼如俯卧撑、卧推等可以增强胸肌使胸部看起来更丰满。\n\n2. 保持良好的饮食习惯摄入足够的蛋白质、脂肪和维生素帮助胸部增加脂肪组织。\n\n3. 考虑使用胸部增大产品如丰胸霜、胸部按摩等。\n\n4. 如果您认为自己的胸部大小影响了自信心可以考虑进行整形手术如隆胸手术。\n\n另外身高和体重偏瘦也可能会影响胸部的大小。如果您希望增加体重可以适当增加饮食量选择高蛋白、高热量的食物并进行适当的运动锻炼增加肌肉量。}]} text tokenizer.apply_chat_template(sample[messages],tokenizeFalse,add_generation_promptFalse) print(text)输出rootautodl-container-be044ebe99-a8225ede:~/autodl-tmp/codes# python test_sample_convert.py |im_start|system 你是专业的医疗咨询助手回答仅供科普参考不能替代执业医师面诊诊疗请遵从线下医生的专业意见。|im_end| |im_start|user 哺乳后胸太小自己个字较高体重偏瘦胸部太平|im_end| |im_start|assistant 哺乳后胸部变小是正常的生理现象因为哺乳会导致乳房内的脂肪组织减少。如果您希望增大胸部可以考虑以下方法 1. 进行胸部锻炼如俯卧撑、卧推等可以增强胸肌使胸部看起来更丰满。 2. 保持良好的饮食习惯摄入足够的蛋白质、脂肪和维生素帮助胸部增加脂肪组织。 3. 考虑使用胸部增大产品如丰胸霜、胸部按摩等。 4. 如果您认为自己的胸部大小影响了自信心可以考虑进行整形手术如隆胸手术。 另外身高和体重偏瘦也可能会影响胸部的大小。如果您希望增加体重可以适当增加饮食量选择高蛋白、高热量的食物并进行适当的运动锻炼增加肌肉量。|im_end|打印结果可以看到 |im_start|system、|im_start|user、|im_start|assistant标记格式正确就可以喂给 SFTTrainer 训练。4、PEFT-QLoRA SFT训练训练代码import torch from datasets import load_dataset from transformers import ( AutoModelForCausalLM, AutoTokenizer, BitsAndBytesConfig, TrainingArguments, Trainer ) from peft import LoraConfig, get_peft_model # 配置项 model_name /root/autodl-tmp/models/Qwen2.5-7B-Instruct train_data_path /root/autodl-tmp/datas/med_sft/med_train.jsonl val_data_path /root/autodl-tmp/datas/med_sft/med_val.jsonl output_dir ./lora-med-ckpt lora_save_path ./lora-med-adapter MAX_LEN 2048 # QLoRA 4bit量化 bnb_config BitsAndBytesConfig( load_in_4bitTrue, bnb_4bit_use_double_quantTrue, bnb_4bit_quant_typenf4, bnb_4bit_compute_dtypetorch.bfloat16 ) tokenizer AutoTokenizer.from_pretrained(model_name, trust_remote_codeTrue) tokenizer.pad_token tokenizer.eos_token tokenizer.padding_side right tokenizer.model_max_length MAX_LEN model AutoModelForCausalLM.from_pretrained( model_name, quantization_configbnb_config, device_mapauto, trust_remote_codeTrue, torch_dtypetorch.bfloat16 ) model.gradient_checkpointing_enable() # PEFT LoRA配置 Qwen2.5 lora_config LoraConfig( r16, lora_alpha32, target_modules[q_proj, k_proj, v_proj, o_proj], lora_dropout0.05, biasnone, task_typeCAUSAL_LM ) model get_peft_model(model, lora_config) model.print_trainable_parameters() # 加载数据集 train_ds load_dataset(json, data_filestrain_data_path, splittrain) val_ds load_dataset(json, data_filesval_data_path, splittrain) RESPONSE_TPL |im_start|assistant\n tpl_ids tokenizer(RESPONSE_TPL, add_special_tokensFalse)[input_ids] def process_fn(sample): # 1. apply chat template full_text tokenizer.apply_chat_template( sample[messages], tokenizeFalse, add_generation_promptFalse ) # 2. tokenize out tokenizer( full_text, truncationTrue, max_lengthMAX_LEN, paddingmax_length ) input_ids out[input_ids] attention_mask out[attention_mask] labels input_ids.copy() # 3. 找到assistant模板位置前面全部置-100不计算loss for i in range(len(input_ids) - len(tpl_ids) 1): if input_ids[i:ilen(tpl_ids)] tpl_ids: for pos in range(i len(tpl_ids)): labels[pos] -100 break return {input_ids:input_ids, attention_mask:attention_mask, labels:labels} train_ds train_ds.map(process_fn) val_ds val_ds.map(process_fn) # 设置格式只保留模型需要的key train_ds.set_format(torch, columns[input_ids,attention_mask,labels]) val_ds.set_format(torch, columns[input_ids,attention_mask,labels]) training_args TrainingArguments( output_diroutput_dir, per_device_train_batch_size2, gradient_accumulation_steps4, per_device_eval_batch_size2, learning_rate2e-5, num_train_epochs2, logging_steps10, save_steps100, eval_steps100, fp16True, optimpaged_adamw_8bit, report_to[], eval_strategysteps, save_total_limit3, load_best_model_at_endTrue, metric_for_best_modeleval_loss, ) # ✅ 使用原生Trainer不再使用SFTTrainer trainer Trainer( modelmodel, argstraining_args, train_datasettrain_ds, eval_datasetval_ds, ) trainer.train() # 保存LoRA适配器 trainer.model.save_pretrained(lora_save_path) tokenizer.save_pretrained(lora_save_path) print(f✅LoRA适配器保存完成{lora_save_path})代码解读1. 导入库AutoModelForCausalLM因果大模型加载对话生成模型peftLoRA轻量化微调库2. 配置项lora_save_path最终产出只有LoRA小权重不是完整模型体积几十MB推理时要挂载基座模型使用MAX_LEN2048单条样本最大token长度3. QLoRA 4bit量化配置BitsAndBytesConfigQLoRA核心把7B模型压缩到4bit加载大幅降低显存占用。权重存储 4bit运算 bf16兼顾显存和训练效果。load_in_4bitTrue以 4bit 加载模型权重bnb_4bit_use_double_quantTrue二次量化进一步省显存。权重只量化一次到 NF4二次量化对象是scale 缩放因子。bnb_4bit_quant_typenf4Normalized‑float4大模型微调推荐量化类型效果优于普通 fp4。NF4为非均匀格点量化。bnb_4bit_compute_dtypetorch.bfloat16计算的时候转回 bfloat16 精度做前向 / 反向传播保证训练精度权重存储是 4bit。4. Tokenizer初始化padding_sideright向右 padding因果语言模型必须右补零左 padding 会干扰生成5. 加载基座模型开启梯度检查点quantization_configbnb_config启用 4bit QLoRA 加载torch_dtypetorch.bfloat16模型运算精度 bf16。Ampere架构的显卡硬件原生支持BF16矩阵乘法Tensor Core速度很快。gradient_checkpointing_enable()梯度检查点以时间换显存节省大量显存代价训练速度会变慢一点。等同于deepspeed的激活检查点。核心思想不保存中间激活值反向传播的时候重新跑一遍前向重新算出需要的激活以计算时间换取显存。6. LoRA配置r16LoRA秩越大拟合能力越强参数量越大target_modules[q_proj,k_proj,v_proj,o_proj]Qwen2.5 注意力层要加 LoRA 的模块只对注意力做 LoRAmlp 不动。7. 加载数据集RESPONSE_TPL |im_start|assistant\nQwen ChatML 标记注意末尾换行符不能丢。tpl_ids把 assistant 开头标记转为 token id后续用来定位回答起始位置。8. process_fn单样本预处理函数apply_chat_template把messages[system,user,assistant]按照 Qwen ChatML 模板拼接完整字符串。add_generation_promptFalse训练阶段关闭推理时要 True训练样本已经包含完整 assistant 回答。|im_start|assistant\n这个字符串正是add_generation_promptTrue追加的那一段文本add_generation_prompt控制是否在对话模板末尾追加 assistant 起始标记|im_start|assistant\n。训练数据集已经包含完整 assistant 回答设置 False推理仅有 systemuser设置 True用来引导模型从 assistant 位置开始续写输出。attention_mask注意力层控制哪些 token 可以互相看见0 屏蔽 padding遍历 token 序列匹配|im_start|assistant\n对应的 id 片段该标记之前所有 token label 赋值为‑100。PyTorch CrossEntropyLoss 特性label-100 会自动忽略该位置不计入损失。也就是systemuser|im_start|assistant\n这些 token 不参与 loss只有 assistant 后面的回答文字参与 loss 更新权重。9. TrainingArguments 训练超参gradient_accumulation_steps4梯度累积每 4 步更新一次权重实际等效 batch_size 2 *4 8显存不足调小 batch调大累积步数。metric_for_best_modeleval_loss以验证集 loss 作为评判好坏标准总结QLoRA权重存储 4bit计算 bf16gradient_checkpoint 进一步省显存代价速度下降。Loss 掩码必须把 system/user 部分 label-100仅 assistant 回答算 loss模板末尾换行不可丢否则匹配失败掩码失效。LoRA 只训练 q/k/v/o基座完全冻结可训练参数占比 0.1‑0.3%。load_best_model_at_end非常重要自动选 eval_loss 最优模型对抗医疗小数据集过拟合。5、运行结果【运行环境3090 24G 2卡】1运行日志rootautodl-container-be044ebe99-a8225ede:~/autodl-tmp/codes# python train_med_sft.py [transformers] torch_dtype is deprecated! Use dtype instead! Loading weights: 100%|███████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 339/339 [00:0200:00, 130.71it/s] trainable params: 10,092,544 || all params: 7,625,709,056 || trainable%: 0.1323 {loss: 9.681, grad_norm: 40.3, learning_rate: 1.972e-05, epoch: 0.04} {loss: 3.945, grad_norm: 38.34, learning_rate: 1.936e-05, epoch: 0.08} {loss: 1.344, grad_norm: 9.147, learning_rate: 1.896e-05, epoch: 0.12} 6%|██████████▏ | 32/500 [09:302:19:11, 17.85s/it] {loss: 0.3227, grad_norm: 1.623, learning_rate: 1.856e-05, epoch: 0.16} 9%|███████████████ | 47/500 [13:572:14:28, 17.81s/it] {loss: 0.1129, grad_norm: 0.5479, learning_rate: 1.816e-05, epoch: 0.2} {loss: 0.08933, grad_norm: 0.2654, learning_rate: 1.776e-05, epoch: 0.24} {loss: 0.0747, grad_norm: 0.2404, learning_rate: 1.736e-05, epoch: 0.28} {loss: 0.06829, grad_norm: 0.06401, learning_rate: 1.696e-05, epoch: 0.32} {loss: 0.0762, grad_norm: 0.07949, learning_rate: 1.656e-05, epoch: 0.36} {loss: 0.0701, grad_norm: 0.05436, learning_rate: 1.616e-05, epoch: 0.4} {eval_loss: 0.06643, eval_runtime: 358.6, eval_samples_per_second: 1.394, eval_steps_per_second: 0.697, epoch: 0.4} {loss: 0.06733, grad_norm: 0.04594, learning_rate: 1.576e-05, epoch: 0.44} {loss: 0.06793, grad_norm: 0.04274, learning_rate: 1.536e-05, epoch: 0.48} {loss: 0.06278, grad_norm: 0.07219, learning_rate: 1.496e-05, epoch: 0.52} {loss: 0.06734, grad_norm: 0.04364, learning_rate: 1.456e-05, epoch: 0.56} {loss: 0.06567, grad_norm: 0.04943, learning_rate: 1.416e-05, epoch: 0.6} {loss: 0.0649, grad_norm: 0.04054, learning_rate: 1.376e-05, epoch: 0.64} {loss: 0.06346, grad_norm: 0.1199, learning_rate: 1.336e-05, epoch: 0.68} {loss: 0.06456, grad_norm: 0.04834, learning_rate: 1.296e-05, epoch: 0.72} {loss: 0.06505, grad_norm: 0.04073, learning_rate: 1.256e-05, epoch: 0.76} {loss: 0.06118, grad_norm: 0.03702, learning_rate: 1.216e-05, epoch: 0.8} {eval_loss: 0.06243, eval_runtime: 358.7, eval_samples_per_second: 1.394, eval_steps_per_second: 0.697, epoch: 0.8} 42%|█████████████████████████████████████████████████████████████████▎ | 208/500 [1:13:412:09:49, 26.68s/it] {loss: 0.06143, grad_norm: 0.04803, learning_rate: 1.176e-05, epoch: 0.84} {loss: 0.06285, grad_norm: 0.05561, learning_rate: 1.136e-05, epoch: 0.88} {loss: 0.0619, grad_norm: 0.0374, learning_rate: 1.096e-05, epoch: 0.92} {loss: 0.06739, grad_norm: 0.0474, learning_rate: 1.056e-05, epoch: 0.96} {loss: 0.05766, grad_norm: 0.04297, learning_rate: 1.016e-05, epoch: 1} 51%|███████████████████████████████████████████████████████████████████████████████▊ | 254/500 [1:27:201:13:04, 17.82s/it] {loss: 0.05751, grad_norm: 0.07466, learning_rate: 9.76e-06, epoch: 1.04} {loss: 0.05832, grad_norm: 0.04197, learning_rate: 9.36e-06, epoch: 1.08} {loss: 0.06342, grad_norm: 0.0499, learning_rate: 8.96e-06, epoch: 1.12} {loss: 0.06125, grad_norm: 0.04082, learning_rate: 8.56e-06, epoch: 1.16} {loss: 0.05576, grad_norm: 0.04777, learning_rate: 8.16e-06, epoch: 1.2} {eval_loss: 0.06149, eval_runtime: 358.7, eval_samples_per_second: 1.394, eval_steps_per_second: 0.697, epoch: 1.2} {loss: 0.0637, grad_norm: 0.05606, learning_rate: 7.76e-06, epoch: 1.24} {loss: 0.05827, grad_norm: 0.04508, learning_rate: 7.36e-06, epoch: 1.28} {loss: 0.06752, grad_norm: 0.06225, learning_rate: 6.96e-06, epoch: 1.32} {loss: 0.06071, grad_norm: 0.04637, learning_rate: 6.56e-06, epoch: 1.36} {loss: 0.0603, grad_norm: 0.05329, learning_rate: 6.16e-06, epoch: 1.4} {loss: 0.06119, grad_norm: 0.04567, learning_rate: 5.76e-06, epoch: 1.44} {loss: 0.05596, grad_norm: 0.0464, learning_rate: 5.36e-06, epoch: 1.48} {loss: 0.06642, grad_norm: 0.04317, learning_rate: 4.96e-06, epoch: 1.52} {loss: 0.05975, grad_norm: 0.05931, learning_rate: 4.56e-06, epoch: 1.56} {loss: 0.06347, grad_norm: 0.0577, learning_rate: 4.16e-06, epoch: 1.6} {eval_loss: 0.06129, eval_runtime: 358.8, eval_samples_per_second: 1.394, eval_steps_per_second: 0.697, epoch: 1.6} {loss: 0.06557, grad_norm: 0.05656, learning_rate: 3.76e-06, epoch: 1.64} {loss: 0.06327, grad_norm: 0.06272, learning_rate: 3.36e-06, epoch: 1.68} {loss: 0.06514, grad_norm: 0.04247, learning_rate: 2.96e-06, epoch: 1.72} {loss: 0.0689, grad_norm: 0.05644, learning_rate: 2.56e-06, epoch: 1.76} 88%|████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████▏ | 441/500 [2:34:5117:31, 17.83s/it] {loss: 0.06636, grad_norm: 0.0459, learning_rate: 2.16e-06, epoch: 1.8} {loss: 0.06554, grad_norm: 0.05689, learning_rate: 1.76e-06, epoch: 1.84} {loss: 0.05621, grad_norm: 0.04422, learning_rate: 1.36e-06, epoch: 1.88} {loss: 0.06446, grad_norm: 0.06602, learning_rate: 9.6e-07, epoch: 1.92} {loss: 0.05942, grad_norm: 0.04792, learning_rate: 5.6e-07, epoch: 1.96} {loss: 0.06235, grad_norm: 0.05635, learning_rate: 1.6e-07, epoch: 2} {eval_loss: 0.06112, eval_runtime: 358.8, eval_samples_per_second: 1.394, eval_steps_per_second: 0.697, epoch: 2} {train_runtime: 1.07e04, train_samples_per_second: 0.374, train_steps_per_second: 0.047, train_loss: 0.3659, epoch: 2} 100%|███████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 500/500 [2:58:2200:00, 21.41s/it] ✅LoRA适配器保存完成./lora-med-adapter2grad_norm它是梯度范数把所有可训练参数LoRA_A, LoRA_B梯度拼接之后的 L2‑范数。把全部可训练参数的梯度摊平拼成一个超大一维向量计算 L2含义grad_norm越大梯度整体幅度大参数更新步子大grad_norm越小梯度整体很小更新微弱接近收敛grad_norm ≈ 0几乎不更新模型不再学东西。3结果目录lora-med-adapter解读rootautodl-container-be044ebe99-a8225ede:~/autodl-tmp/codes/lora-med-adapter# ls README.md adapter_config.json adapter_model.safetensors chat_template.jinja tokenizer.json tokenizer_config.json1adapter_config.json核心配置内容示例peft_type: LORAtarget_modules: [q_proj,k_proj,v_proj,o_proj]r、lora_alpha、lora_dropout、bias、task_type记录你训练时全部 LoRA 超参。PeftModel.from_pretrained(base_model, 这个文件夹)会读取该配置知道哪些层要包装 LoraLinear。没有这个文件peft 无法加载适配器。2adapter_model.safetensorsLoRA 权重本体真正保存训练出来的 lora_A、lora_B 矩阵只有 LoRA 小权重不含基座 Qwen 权重。safetensors 安全格式防止恶意代码。里面 key 类似model.layers.0.self_attn.q_proj.lora_A.default.weightmodel.layers.0.self_attn.q_proj.lora_B.default.weight训练得到的可训练参数全部在这里39MB 。基座原始权重仍然在你原始 Qwen 模型目录。3chat_template.jinjajinja2 模板文件存储ChatML 对话模板。tokenizer.apply_chat_template()底层读取这个 jinja 模板把 messages 数组转成模型输入文本。⚠️重要DPO、推理、RM 训练必须使用完全一致 chat_template。加载 tokenizer 的时候会自动读取这个 jinja如果丢失模板会错乱训练 / 推理效果暴跌。4tokenizer_config.json分词器配置pad_token、eos_token、bos_tokenspecial token 映射max_context 等。保存分词器的各种配置参数。5tokenizer.json真正的分词词典、BPE/Byte‑Pair 编码规则vocab 合并表。决定怎么把文字转 input_ids。6、推理测试代码from peft import PeftModel from transformers import AutoModelForCausalLM, AutoTokenizer base_model_name /root/autodl-tmp/models/Qwen2.5-7B-Instruct lora_path ./lora-med-adapter base_model AutoModelForCausalLM.from_pretrained( base_model_name, torch_dtypeauto, device_mapauto, trust_remote_codeTrue ) model PeftModel.from_pretrained(base_model, lora_path) tokenizer AutoTokenizer.from_pretrained(base_model_name, trust_remote_codeTrue) def chat_infer(messages, max_new_tokens512, temperature0.7, top_p0.8): text tokenizer.apply_chat_template( messages, tokenizeFalse, add_generation_promptTrue ) print(f\n输入模板\n{text}\n输出\n) model_inputs tokenizer([text], return_tensorspt).to(model.device) input_len model_inputs.input_ids.shape[1] generated_ids model.generate( **model_inputs, max_new_tokensmax_new_tokens, temperaturetemperature, top_ptop_p, do_sampleTrue ) output_ids generated_ids[:, input_len:] resp tokenizer.batch_decode(output_ids, skip_special_tokensTrue)[0] return resp if __name__ __main__: # 测试一条医疗提问尽量用训练集没见过的问题 test_msg [ {role: system, content: 你是专业的医疗咨询助手回答仅供科普参考不能替代执业医师面诊诊疗请遵从线下医生的专业意见。}, {role: user, content:怀孕40多天不知情下输了利巴韦林和头孢孩子能不能要} ] print(chat_infer(test_msg))运行结果rootautodl-container-be044ebe99-a8225ede:~/autodl-tmp/codes/sft# python test_med_lora.py Loading weights: 100%|████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 339/339 [00:0300:00, 91.42it/s] 输入模板 |im_start|system 你是专业的医疗咨询助手回答仅供科普参考不能替代执业医师面诊诊疗请遵从线下医生的专业意见。|im_end| |im_start|user 怀孕40多天不知情下输了利巴韦林和头孢孩子能不能要|im_end| |im_start|assistant 输出 利巴韦林和头孢类抗生素都属于对胎儿有潜在危害的药物。如果您在怀孕初期已经使用了这些药物建议您尽快咨询医生以了解可能的风险以及如何降低风险。 一般来说如果怀孕早期使用了这些药物医生可能会建议您进行产前检查以确定胎儿是否受到药物的影响。如果您选择继续妊娠医生可能会建议您进行更多的产前检查以确保胎儿的健康。