第一次跑 Qwen2 指令微调的时候,我心里预期很朴素:找一批问答数据,跑通脚本,看 loss 下降,然后得到一个“听话”的模型。结果真等训练结束才发现,能跑通只是开始,模型在验证集上的表现、对指令格式的敏感度、甚至数据顺序都会带来完全不一样的体验。这篇文章就是想把整个 Qwen2 指令微调的入门过程拆开讲清楚:从“为什么要微调”到数据准备、LoRA 参数配置、推理验证,再到我实际踩过的坑。适合刚接触大模型微调、想让模型按特定格式回答问题的开发者,也适合已经跑过开源推理代码、但对训练侧还比较陌生的朋友。1. 动手前先想清楚:指令微调到底在解决什么问题很多同学上来就搜训练脚本,这没错,但我更建议先弄明白你要解决的是哪一层的问题。因为指令微调不是万能的,它只在特定范围内有效。1.1 预训练模型和“会用”之间的差距Qwen2 这类开源底座模型,预训练阶段做的事情本质上是“续写”。它的优化目标是根据前文预测下一个 token,所以它能生成通顺的文本、掌握大量知识,但不等同于会“回答问题”。你直接拿底座模型做推理,输入“什么是注意力机制?”,它可能反问一句“你在说什么?”或者继续输出一段百科性质的阅读理解,而不是给你一个清爽的答案。这不是模型笨,是它的训练目标里根本没有“指令-回复”这种对齐结构。指令微调的目的,就是在这套纯文本续写能力之上,通过一批“指令-回答”数据,引导模型学会一种新的交互模式。这个过程不改变模型的知识量,更多是改变输出的行为习惯。提示:判断你需要的到底是知识注入还是行为对齐,是开始之前最重要的决策。如果你想要模型学会新领域知识,微调不是最优解;如果你只是想要它按固定格式输出、学会某种语气,指令微调刚好对症。1.2 指令微调适合哪些场景,不适合哪些我梳理了一下,指令微调最适合的场景有三类。第一类是固定格式输出。比如你要做一个客服助手,所有回答都要求分“问题原因、处理步骤、注意事项”三段。底座模型做不到每次都严格按这个结构来,但几千条样例微调之后,格式稳定性会明显提升。第二类是特定风格的对话。比如模型需要模仿你写周报的语气、带点幽默感,或者用简洁口语回答问题,这些都属于“输出分布”的迁移,微调非常管用。第三类是私有知识库的问答入口。注意,微调并不能让模型记住大量新知识,但可以让模型在拿到检索片段之后,只依据片段内容作答、不乱编。这种“行为约束”恰恰靠指令微调建立。不适合的场景也有。如果你想给模型注入新的技能,比如教它做复杂的数学推理,纯靠指令微调很难。推理能力更多依赖基座本身的水平,LoRA 能激发一部分潜力,但无法无中生有。另外,超长文本、多模态能力这类问题也不是指令微调能解决的,那是模型结构层面的事。1.3 为什么选 Qwen2 起步选 Qwen2 作为入门对象,理由其实很务实。一是文档和社区生态成熟,模型权重的下载、推理、微调工具链都比较完善。遇到问题搜索一下基本都能找到同类情况。二是尺寸选择灵活。你可以从 0.5B 这种小尺寸开始验证流程,跑通了再切换到 7B 甚至更大的版本。这点对入门非常友好。三是它对中文的支持比较自然。如果你最终要做中文垂类场景,基座模型本身的“语感”会直接影响微调效果。Qwen2 在中英文混合指令上的表现稳定,不容易出现“中文问、英文答”的割裂感。我个人的建议是:先拿 0.5B 或 1.5B 把全流程跑通,确认数据格式、训练参数和推理链路都没问题,再上 7B 跑正式实验。这样省时省钱,排查问题也快得多。2. 环境搭建与数据准备:决定后续效果的第一道坎这一步看着不起眼,但我在实际项目中见过太多“训练没问题、效果一团糟”的案例,根源都在数据。2.1 依赖选型与硬件预估先讲环境。微调 Qwen2 的主流方案是使用开源的 PEFT 库搭配 Transformers,训练框架可以用 PyTorch 原生的 Trainer,也可以用更上层的封装。对入门来说,直接基于 Transformers 的 Trainer 就够了,少一层封装,出了问题好定位。基础的依赖大概是这样:Python 3.10 左右transformers 4.40 以上peftacceleratebitsandbytes(量化相关)torch 2.1 以上硬件方面,如果你用 LoRA 微调 7B 模型,一张 24GB 显存的消费级显卡可以勉强跑,但序列长度得控制。用 1.5B 模型的话,16GB 显存也够用。提示:显存不够优先考虑缩短 max_length,其次才是换更小的模型。很多人一上来就上 2048 长度,还没开始训练就 OOM,其实先跑通流程比追求长文本更重要。2.2 数据格式与清洗指令微调的数据格式,最常见的两种是 ChatML 风格和 Alpaca 风格。Qwen2 在预训练阶段使用 ChatML 格式的处理方式,微调时最好延续这种风格,这样模型不用花额外的能力去适应新的指令模板。我自己习惯把数据结构化成一个包含conversations字段的列表,每条对话形如:{ "conversations": [ {"role": "user", "content": "用一句话解释什么是进程和线程"}, {"role": "assistant", "content": "进程是操作系统分配资源的基本单位,线程是进程内部独立执行的子任务。"} ] }有一点要特别注意:如果你用的是 Transformers 的 Trainer 加 DataCollator,需要把这种多轮对话结构转成模型可以接受的输入。最简单的方式是使用apply_chat_template,把对话转成包含特殊 token 的文本序列。数据清洗我认为有四个必须检查的点。第一,回答里不能出现“作为 AI 模型,我不能……”这种万能话术,除非业务场景真的需要。第二,所有指令和回答的格式要统一,标点、换行、语气都要检查一遍。第三,去除明显重复的样本,重复数据会让模型对某些模式过度记忆。第四,检查是否存在“指令与回答不匹配”的情况,这个比什么都致命。我见过一个案例,某同学准备了两千条数据,训练结束后模型经常答非所问。排查了很久才发现,数据里大约 15% 的回答是从其他领域直接复制过来的,模型学着学着就把错误对应关系学进去了。2.3 数据量级的选择关于数据量,很多人喜欢追求“越多越好”,但实际上指令微调不是堆量就有用。对入门项目来说,质量过关的前提下:单一任务、格式固定:300-500 条即可看到明显效果多任务、需要保持通用能力:2000-5000 条比较合理真正接近生产级的 Chat 模型:数万条是另一个故事我建议第一次实验先准备 500 条左右的高质量数据,跑通流程并观察效果,再逐步扩充。这样就算数据有问题,也能快速定位,而不是等到训了一夜之后才发现数据格式错了一半。如果手头数据不够,可以有限度地使用合成数据。比如用一个表现较好的模型对你的原始材料生成多个角度的问答对,再人工筛选。合成的数据需要注意多样性,不要只换表述方式而保留完全一样的信息结构,否则模型学到的只是表面句式。3. LoRA 微调:用最小成本实现参数更新环境没问题、数据也备好后,就进入训练环节了。这也是大多数人最关心的一步。3.1 为什么优先 LoRA刚开始接触微调时,很多人会纠结一个问题:到底做全参微调还是 LoRA?我的答案是:入门阶段无脑选 LoRA。理由不是全参微调不好,而是 LoRA 用最小的代价让你跑通整个链路。全参微调意味着更新模型的所有权重。7B 模型如果用 float16 做全参微调,优化器状态带来的显存开销非常大,个人电脑基本扛不住。LoRA 的思路是冻结原始权重,只在每层 Transformer 的 Attention 部分注入低秩矩阵,训练时只更新这些新增参数。这样做的好处有两个。一是显存占用大幅下降。二是可插拔。训练完成后你可以把 LoRA 权重单独保存,推理时动态加载,不用保留训练状态。多个 LoRA 权重还可以针对不同任务切换,相当于一个底座模型配多个“小脑”。3.2 关键参数配置与训练脚本下面我给出一个基于 PEFT 的 LoRA 训练脚本主配置。这不是唯一答案,但至少是我验证过能稳定跑通的组合。from peft import LoraConfig, get_peft_model from transformers import AutoModelForCausalLM, AutoTokenizer, Trainer, TrainingArguments model_path = "Qwen/Qwen2-1.5B-Instruct" tokenizer = AutoTokenizer.from_pretrained(model_path, trust_remote_code=True) model = AutoModelForCausalLM.from_pretrained( model_path, torch_dtype="auto", device_map="auto" ) lora_config = LoraConfig( r=16, lora_alpha=32, target_modules=["q_proj", "k_proj", "v_proj", "o_proj"], lora_dropout=0.05, bias="none", task_type="CAUSAL_LM" ) model = get_peft_model(model, lora_config) model.print_trainable_parameters() training_args = TrainingArguments( output_dir="./qwen2-lora-output", per_device_train_batch_size=2, gradient_accumulation_steps=8, learning_rate=2e-4, num_train_epochs=3, logging_steps=10, save_steps=100, save_total_limit=2, fp16=True, remove_unused_columns=False )几个参数我想展开说一下。r代表低秩矩阵的秩,决定了新注入参数的规模。r=8适合简单任务,r=16是比较通用的起点。lora_alpha简单理解成缩放系数,一般设为r的两倍。target_modules这段值得细看。不同模型的模块名不一样,Qwen2 系列里q_proj、k_proj、v_proj、o_proj是 attention 部分的四个投影层。如果只改 Attention 层效果不够,还可以考虑把 MLP 层也纳入。但我的经验是,先只动 Attention 层,效果稳定又快。learning_rate和batch_size是互相影响的。LoRA 训练通常使用比全参微调更高的学习率,2e-4 附近是常用区间。学习率太高模型会学歪,太低则需要更多步数才能看到效果。提示:remove_unused_columns=False这一项容易忽略。因为你的数据集里除了文本列之外可能还有一些元信息,如果默认让 Trainer 去除未使用列,可能把构造好的输入也一起删掉,导致训练报错。3.3 训练过程中的损失观察训练启动后,不要只盯着 loss 数值看。我习惯记录 loss 曲线的走势,同时关注它在不同阶段的下降节奏。正常情况是前几百步 loss 快速下降,随后进入平台期。如果你发现 loss 下降极其缓慢,先检查学习率;如果 loss 出现突然跳升,考虑数据里是否存在异常样本。更要紧的是:训练 loss 只能告诉你模型是否拟合了这批数据,不能告诉你它的泛化能力。一个常见误区是把训练 loss 压到 0.3 以下就觉得大功告成。实际上 LoRA 训练很少需要把训练 loss 压到极低,否则基本意味着过拟合。训练过程中每隔一段时间就手动存一个 checkpoint,我至少会存三份:训练后期、训练中段、以及一个接近收敛的点。后面做效果对比时会非常有用。4. 合并权重与推理验证:别急着庆祝训练结束不是冲刺终点,真正的验证才刚刚开始。4.1 模型的保存与合并LoRA 训练结束后,你手里拿到的是训练器输出的 checkpoint,其中只包含适配器权重,不是完整模型。有两种使用方式。一种是直接保存适配器权重,推理时加载原模型再加 LoRA:from peft import PeftModel base_model = AutoModelForCausalLM.from_pretrained(model_path) model = PeftModel.from_pretrained(base_model, "./qwen2-lora-output/checkpoint-500")另一种是先把适配器权重合并到原模型中,导出成一个完整的模型文件。合并方式很简单:merged_model = model.merge_and_unload() merged_model.save_pretrained("./qwen2-lora-merged") tokenizer.save_pretrained("./qwen2-lora-merged")合并导出之后,你就可以把模型当成一个普通底座模型来推理了,部署起来更方便。但要注意,合并后的模型体积会变大,因为原始参数全部被保存。4.2 指令遵循能力验证推理验证阶段,我强烈建议不要只准备两三道题测一下就觉得万事大吉。我通常会把验证集分成几类,分别测试:直接复现:与训练数据格式很像的题目,看模型是否学会了格式简单泛化:同样领域、但没出现过的题目,看模型是否理解任务边界测试:刻意问一些训练阶段没有覆盖的角度,看模型会不会乱答直接复现表现好是应该的,泛化表现好才是微调真正起效的证据。边界测试如果表现不理想也不用慌,这是正常现象,关键是判断模型的输出是“知识性错误”还是“行为模式错误”。前者可以接受,说明它只是没见过这个知识;后者需要警惕,说明它对指令的理解出现了偏差。4.3 过拟合检查与简单评测过拟合是 LoRA 微调最容易出现的问题,尤其数据量少的时候。判断方法很简单:把训练集里出现过的题目原封不动丢给模型,如果回答和训练数据一模一样、甚至一个标点都不差,这就是过拟合信号。同时按 emotion 判断一下:如果模型在未见过的新问题上也能保持格式稳定,说明行为对齐学得不错;如果新问题上彻底胡言乱语,则要考虑数据量不足或学习率过高。另外,如果你微调的时候给原始模型添加了特殊 token 或修改了模板,验证时千万要记得同样处理。我见过一个开发者在训练时自定义了一套对话前缀,推理时却用官方默认模板,结果模型输出没有灵魂,因为输入分布和训练分布完全对不上了。5. 跑完一轮之后的踩坑记录与调优路线最后这部分,我把自己实际踩过坑和总结出的调优路线放在一起说。5.1 高频踩坑点第一个高频坑是“模板不一致”。训练时用的 ChatML 模板、pad token 设置、以及 newline 处理,如果和推理时不统一,效果会千差万别。每次都把训练和推理放在同一个 tokenizer 下完成,能省很多排查时间。第二个坑是“把 LoRA 参数焊死”。很多人直接照搬别人的配置,从来不看任务难度。简单分类任务用 r=16 三个 epoch 可能已经完全过拟合;长对话生成任务可能还需要更大秩和更长训练。参数是服务于数据和任务的,不是越统一越好。第三个坑是“只看 loss 不看成例”。训练 loss 好看,但推理时模型总是回复“抱歉,我不太明白你的问题”,这是典型的“学过但对不上”。问题往往出在数据分布太窄,或者数据里缺少负例。可以适量加入一些“无法回答”的类型样本,让模型学会拒绝,而不是硬答。第四,硬件相关。很多人训练到一半爆显存,第一反应是调小 batch size。但有时候梯度累积步数没同步调整,有效 batch size 突然变小,模型效果会漂。我在训练配置里习惯把per_device_train_batch_size和gradient_accumulation_steps绑定在一起调整,保证有效 batch size 不变。5.2 后续的进阶方向入门流程跑通一遍后,接下去的优化方向有几条比较好走。数据层面,可以做负例筛选。把验证集里模型答错的样本重新清洗、修正,再放回训练集。这种方法往往比加更多新数据更有效,因为错误的样本直接影响模型行为。训练层面,可以尝试在 LoRA 基础上结合不同层级的模块,比如把 MLP 层也纳入低秩更新。有人觉得效果提升有限,但在某些领域任务上,肉眼可见地降低幻觉频率。推理侧也有优化空间。同一套 LoRA 权重,在不同采样参数下的表现可能天差地别。很多“微调无效”的结论,其实是temperature和top_p没调好,模型在随机采样模式下显得不稳定。我个人实践下来最大的体会是:指令微调的项目里,数据整理时间和调参时间的比例,通常会超过 7:3。技术方案本身不复杂,真正的复杂度在于理解模型当前的问题、构造合适的数据、设计有效的验证方法,再一点一点逼近你想要的行为模式。如果你已经能稳定地把 Qwen2 微调跑通,接下来真正拉开差距的,可能就不再是训练脚本里的参数,而是你对数据、任务和模型三者边界的理解了。
第一次跑 Qwen2 指令微调的时候,我心里预期很朴素:找一批问答数据,跑通脚本,看 loss 下降,然后得到一个“听话”的模型。结果真等训练结束才发现,能跑通只是开始,模型在验证集上的表现、对指令格式的敏感度、甚至数据顺序都会带来完全不一样的体验。这篇文章就是想把整个 Qwen2 指令微调的入门过程拆开讲清楚:从“为什么要微调”到数据准备、LoRA 参数配置、推理验证,再到我实际踩过的坑。适合刚接触大模型微调、想让模型按特定格式回答问题的开发者,也适合已经跑过开源推理代码、但对训练侧还比较陌生的朋友。1. 动手前先想清楚:指令微调到底在解决什么问题很多同学上来就搜训练脚本,这没错,但我更建议先弄明白你要解决的是哪一层的问题。因为指令微调不是万能的,它只在特定范围内有效。1.1 预训练模型和“会用”之间的差距Qwen2 这类开源底座模型,预训练阶段做的事情本质上是“续写”。它的优化目标是根据前文预测下一个 token,所以它能生成通顺的文本、掌握大量知识,但不等同于会“回答问题”。你直接拿底座模型做推理,输入“什么是注意力机制?”,它可能反问一句“你在说什么?”或者继续输出一段百科性质的阅读理解,而不是给你一个清爽的答案。这不是模型笨,是它的训练目标里根本没有“指令-回复”这种对齐结构。指令微调的目的,就是在这套纯文本续写能力之上,通过一批“指令-回答”数据,引导模型学会一种新的交互模式。这个过程不改变模型的知识量,更多是改变输出的行为习惯。提示:判断你需要的到底是知识注入还是行为对齐,是开始之前最重要的决策。如果你想要模型学会新领域知识,微调不是最优解;如果你只是想要它按固定格式输出、学会某种语气,指令微调刚好对症。1.2 指令微调适合哪些场景,不适合哪些我梳理了一下,指令微调最适合的场景有三类。第一类是固定格式输出。比如你要做一个客服助手,所有回答都要求分“问题原因、处理步骤、注意事项”三段。底座模型做不到每次都严格按这个结构来,但几千条样例微调之后,格式稳定性会明显提升。第二类是特定风格的对话。比如模型需要模仿你写周报的语气、带点幽默感,或者用简洁口语回答问题,这些都属于“输出分布”的迁移,微调非常管用。第三类是私有知识库的问答入口。注意,微调并不能让模型记住大量新知识,但可以让模型在拿到检索片段之后,只依据片段内容作答、不乱编。这种“行为约束”恰恰靠指令微调建立。不适合的场景也有。如果你想给模型注入新的技能,比如教它做复杂的数学推理,纯靠指令微调很难。推理能力更多依赖基座本身的水平,LoRA 能激发一部分潜力,但无法无中生有。另外,超长文本、多模态能力这类问题也不是指令微调能解决的,那是模型结构层面的事。1.3 为什么选 Qwen2 起步选 Qwen2 作为入门对象,理由其实很务实。一是文档和社区生态成熟,模型权重的下载、推理、微调工具链都比较完善。遇到问题搜索一下基本都能找到同类情况。二是尺寸选择灵活。你可以从 0.5B 这种小尺寸开始验证流程,跑通了再切换到 7B 甚至更大的版本。这点对入门非常友好。三是它对中文的支持比较自然。如果你最终要做中文垂类场景,基座模型本身的“语感”会直接影响微调效果。Qwen2 在中英文混合指令上的表现稳定,不容易出现“中文问、英文答”的割裂感。我个人的建议是:先拿 0.5B 或 1.5B 把全流程跑通,确认数据格式、训练参数和推理链路都没问题,再上 7B 跑正式实验。这样省时省钱,排查问题也快得多。2. 环境搭建与数据准备:决定后续效果的第一道坎这一步看着不起眼,但我在实际项目中见过太多“训练没问题、效果一团糟”的案例,根源都在数据。2.1 依赖选型与硬件预估先讲环境。微调 Qwen2 的主流方案是使用开源的 PEFT 库搭配 Transformers,训练框架可以用 PyTorch 原生的 Trainer,也可以用更上层的封装。对入门来说,直接基于 Transformers 的 Trainer 就够了,少一层封装,出了问题好定位。基础的依赖大概是这样:Python 3.10 左右transformers 4.40 以上peftacceleratebitsandbytes(量化相关)torch 2.1 以上硬件方面,如果你用 LoRA 微调 7B 模型,一张 24GB 显存的消费级显卡可以勉强跑,但序列长度得控制。用 1.5B 模型的话,16GB 显存也够用。提示:显存不够优先考虑缩短 max_length,其次才是换更小的模型。很多人一上来就上 2048 长度,还没开始训练就 OOM,其实先跑通流程比追求长文本更重要。2.2 数据格式与清洗指令微调的数据格式,最常见的两种是 ChatML 风格和 Alpaca 风格。Qwen2 在预训练阶段使用 ChatML 格式的处理方式,微调时最好延续这种风格,这样模型不用花额外的能力去适应新的指令模板。我自己习惯把数据结构化成一个包含conversations字段的列表,每条对话形如:{ "conversations": [ {"role": "user", "content": "用一句话解释什么是进程和线程"}, {"role": "assistant", "content": "进程是操作系统分配资源的基本单位,线程是进程内部独立执行的子任务。"} ] }有一点要特别注意:如果你用的是 Transformers 的 Trainer 加 DataCollator,需要把这种多轮对话结构转成模型可以接受的输入。最简单的方式是使用apply_chat_template,把对话转成包含特殊 token 的文本序列。数据清洗我认为有四个必须检查的点。第一,回答里不能出现“作为 AI 模型,我不能……”这种万能话术,除非业务场景真的需要。第二,所有指令和回答的格式要统一,标点、换行、语气都要检查一遍。第三,去除明显重复的样本,重复数据会让模型对某些模式过度记忆。第四,检查是否存在“指令与回答不匹配”的情况,这个比什么都致命。我见过一个案例,某同学准备了两千条数据,训练结束后模型经常答非所问。排查了很久才发现,数据里大约 15% 的回答是从其他领域直接复制过来的,模型学着学着就把错误对应关系学进去了。2.3 数据量级的选择关于数据量,很多人喜欢追求“越多越好”,但实际上指令微调不是堆量就有用。对入门项目来说,质量过关的前提下:单一任务、格式固定:300-500 条即可看到明显效果多任务、需要保持通用能力:2000-5000 条比较合理真正接近生产级的 Chat 模型:数万条是另一个故事我建议第一次实验先准备 500 条左右的高质量数据,跑通流程并观察效果,再逐步扩充。这样就算数据有问题,也能快速定位,而不是等到训了一夜之后才发现数据格式错了一半。如果手头数据不够,可以有限度地使用合成数据。比如用一个表现较好的模型对你的原始材料生成多个角度的问答对,再人工筛选。合成的数据需要注意多样性,不要只换表述方式而保留完全一样的信息结构,否则模型学到的只是表面句式。3. LoRA 微调:用最小成本实现参数更新环境没问题、数据也备好后,就进入训练环节了。这也是大多数人最关心的一步。3.1 为什么优先 LoRA刚开始接触微调时,很多人会纠结一个问题:到底做全参微调还是 LoRA?我的答案是:入门阶段无脑选 LoRA。理由不是全参微调不好,而是 LoRA 用最小的代价让你跑通整个链路。全参微调意味着更新模型的所有权重。7B 模型如果用 float16 做全参微调,优化器状态带来的显存开销非常大,个人电脑基本扛不住。LoRA 的思路是冻结原始权重,只在每层 Transformer 的 Attention 部分注入低秩矩阵,训练时只更新这些新增参数。这样做的好处有两个。一是显存占用大幅下降。二是可插拔。训练完成后你可以把 LoRA 权重单独保存,推理时动态加载,不用保留训练状态。多个 LoRA 权重还可以针对不同任务切换,相当于一个底座模型配多个“小脑”。3.2 关键参数配置与训练脚本下面我给出一个基于 PEFT 的 LoRA 训练脚本主配置。这不是唯一答案,但至少是我验证过能稳定跑通的组合。from peft import LoraConfig, get_peft_model from transformers import AutoModelForCausalLM, AutoTokenizer, Trainer, TrainingArguments model_path = "Qwen/Qwen2-1.5B-Instruct" tokenizer = AutoTokenizer.from_pretrained(model_path, trust_remote_code=True) model = AutoModelForCausalLM.from_pretrained( model_path, torch_dtype="auto", device_map="auto" ) lora_config = LoraConfig( r=16, lora_alpha=32, target_modules=["q_proj", "k_proj", "v_proj", "o_proj"], lora_dropout=0.05, bias="none", task_type="CAUSAL_LM" ) model = get_peft_model(model, lora_config) model.print_trainable_parameters() training_args = TrainingArguments( output_dir="./qwen2-lora-output", per_device_train_batch_size=2, gradient_accumulation_steps=8, learning_rate=2e-4, num_train_epochs=3, logging_steps=10, save_steps=100, save_total_limit=2, fp16=True, remove_unused_columns=False )几个参数我想展开说一下。r代表低秩矩阵的秩,决定了新注入参数的规模。r=8适合简单任务,r=16是比较通用的起点。lora_alpha简单理解成缩放系数,一般设为r的两倍。target_modules这段值得细看。不同模型的模块名不一样,Qwen2 系列里q_proj、k_proj、v_proj、o_proj是 attention 部分的四个投影层。如果只改 Attention 层效果不够,还可以考虑把 MLP 层也纳入。但我的经验是,先只动 Attention 层,效果稳定又快。learning_rate和batch_size是互相影响的。LoRA 训练通常使用比全参微调更高的学习率,2e-4 附近是常用区间。学习率太高模型会学歪,太低则需要更多步数才能看到效果。提示:remove_unused_columns=False这一项容易忽略。因为你的数据集里除了文本列之外可能还有一些元信息,如果默认让 Trainer 去除未使用列,可能把构造好的输入也一起删掉,导致训练报错。3.3 训练过程中的损失观察训练启动后,不要只盯着 loss 数值看。我习惯记录 loss 曲线的走势,同时关注它在不同阶段的下降节奏。正常情况是前几百步 loss 快速下降,随后进入平台期。如果你发现 loss 下降极其缓慢,先检查学习率;如果 loss 出现突然跳升,考虑数据里是否存在异常样本。更要紧的是:训练 loss 只能告诉你模型是否拟合了这批数据,不能告诉你它的泛化能力。一个常见误区是把训练 loss 压到 0.3 以下就觉得大功告成。实际上 LoRA 训练很少需要把训练 loss 压到极低,否则基本意味着过拟合。训练过程中每隔一段时间就手动存一个 checkpoint,我至少会存三份:训练后期、训练中段、以及一个接近收敛的点。后面做效果对比时会非常有用。4. 合并权重与推理验证:别急着庆祝训练结束不是冲刺终点,真正的验证才刚刚开始。4.1 模型的保存与合并LoRA 训练结束后,你手里拿到的是训练器输出的 checkpoint,其中只包含适配器权重,不是完整模型。有两种使用方式。一种是直接保存适配器权重,推理时加载原模型再加 LoRA:from peft import PeftModel base_model = AutoModelForCausalLM.from_pretrained(model_path) model = PeftModel.from_pretrained(base_model, "./qwen2-lora-output/checkpoint-500")另一种是先把适配器权重合并到原模型中,导出成一个完整的模型文件。合并方式很简单:merged_model = model.merge_and_unload() merged_model.save_pretrained("./qwen2-lora-merged") tokenizer.save_pretrained("./qwen2-lora-merged")合并导出之后,你就可以把模型当成一个普通底座模型来推理了,部署起来更方便。但要注意,合并后的模型体积会变大,因为原始参数全部被保存。4.2 指令遵循能力验证推理验证阶段,我强烈建议不要只准备两三道题测一下就觉得万事大吉。我通常会把验证集分成几类,分别测试:直接复现:与训练数据格式很像的题目,看模型是否学会了格式简单泛化:同样领域、但没出现过的题目,看模型是否理解任务边界测试:刻意问一些训练阶段没有覆盖的角度,看模型会不会乱答直接复现表现好是应该的,泛化表现好才是微调真正起效的证据。边界测试如果表现不理想也不用慌,这是正常现象,关键是判断模型的输出是“知识性错误”还是“行为模式错误”。前者可以接受,说明它只是没见过这个知识;后者需要警惕,说明它对指令的理解出现了偏差。4.3 过拟合检查与简单评测过拟合是 LoRA 微调最容易出现的问题,尤其数据量少的时候。判断方法很简单:把训练集里出现过的题目原封不动丢给模型,如果回答和训练数据一模一样、甚至一个标点都不差,这就是过拟合信号。同时按 emotion 判断一下:如果模型在未见过的新问题上也能保持格式稳定,说明行为对齐学得不错;如果新问题上彻底胡言乱语,则要考虑数据量不足或学习率过高。另外,如果你微调的时候给原始模型添加了特殊 token 或修改了模板,验证时千万要记得同样处理。我见过一个开发者在训练时自定义了一套对话前缀,推理时却用官方默认模板,结果模型输出没有灵魂,因为输入分布和训练分布完全对不上了。5. 跑完一轮之后的踩坑记录与调优路线最后这部分,我把自己实际踩过坑和总结出的调优路线放在一起说。5.1 高频踩坑点第一个高频坑是“模板不一致”。训练时用的 ChatML 模板、pad token 设置、以及 newline 处理,如果和推理时不统一,效果会千差万别。每次都把训练和推理放在同一个 tokenizer 下完成,能省很多排查时间。第二个坑是“把 LoRA 参数焊死”。很多人直接照搬别人的配置,从来不看任务难度。简单分类任务用 r=16 三个 epoch 可能已经完全过拟合;长对话生成任务可能还需要更大秩和更长训练。参数是服务于数据和任务的,不是越统一越好。第三个坑是“只看 loss 不看成例”。训练 loss 好看,但推理时模型总是回复“抱歉,我不太明白你的问题”,这是典型的“学过但对不上”。问题往往出在数据分布太窄,或者数据里缺少负例。可以适量加入一些“无法回答”的类型样本,让模型学会拒绝,而不是硬答。第四,硬件相关。很多人训练到一半爆显存,第一反应是调小 batch size。但有时候梯度累积步数没同步调整,有效 batch size 突然变小,模型效果会漂。我在训练配置里习惯把per_device_train_batch_size和gradient_accumulation_steps绑定在一起调整,保证有效 batch size 不变。5.2 后续的进阶方向入门流程跑通一遍后,接下去的优化方向有几条比较好走。数据层面,可以做负例筛选。把验证集里模型答错的样本重新清洗、修正,再放回训练集。这种方法往往比加更多新数据更有效,因为错误的样本直接影响模型行为。训练层面,可以尝试在 LoRA 基础上结合不同层级的模块,比如把 MLP 层也纳入低秩更新。有人觉得效果提升有限,但在某些领域任务上,肉眼可见地降低幻觉频率。推理侧也有优化空间。同一套 LoRA 权重,在不同采样参数下的表现可能天差地别。很多“微调无效”的结论,其实是temperature和top_p没调好,模型在随机采样模式下显得不稳定。我个人实践下来最大的体会是:指令微调的项目里,数据整理时间和调参时间的比例,通常会超过 7:3。技术方案本身不复杂,真正的复杂度在于理解模型当前的问题、构造合适的数据、设计有效的验证方法,再一点一点逼近你想要的行为模式。如果你已经能稳定地把 Qwen2 微调跑通,接下来真正拉开差距的,可能就不再是训练脚本里的参数,而是你对数据、任务和模型三者边界的理解了。