如果你最近在折腾开源大模型,应该已经注意到,Qwen2 这个名字的出场率越来越高。它不是一个单一模型,而是一整个系列:既有能跑在消费级显卡上的小尺寸版本,也有能力接近头部商用模型的大参数版本。而“指令微调”这四个字,恰好是把一个通用模型改造成某个垂直领域、某套特殊输出格式“趁手工具”的核心手段。很多朋友问我,微调到底难不难?我的答案是:如果你只是想做一个能稳定按你的口吻和规则回答问题的助手,门槛没有想象中高。这篇文章不绕弯子,直接拆解我用 Qwen2 做指令微调的完整过程,包括训练数据怎么准备、LoRA 参数怎么配、模型怎么合并推理,以及那些文档里根本不会写清楚的坑。适合刚接触大模型微调、手头有个 12G 以上显存显卡的同学参考。
1. 微调前必须想清楚的事:指令微调到底在干什么
1.1 指令微调和预训练的本质区别
预训练阶段,模型其实只学会了一件事:给定上文,预测下一个 token。它可以续写新闻、接住半截小说,但你直接问它“明天上海天气怎么样”,它大概率会像写作文一样给你扯一段,而不是乖乖给你一个答案。这中间的差距,就是指令跟随能力不够。指令微调(SFT)要做的事情,就是塞给模型大量“人类指令 -> 期望回答”的配对样本,让它通过标准的语言建模损失,把这种问与答的行为模式固化下来。
很多人以为微调是让模型“变聪明”,这个理解需要纠正。微调更多是“改习惯、改风格、改领域口径”。Qwen2 这类开源基座模型本身已经具备不错的语言理解和推理能力,但它面对特定业务时是“一身力气无处使”:不会用你的行业术语、不会遵守你的答案框架、甚至会一本正经地编造产品功能。指令微调的作用,就是给它一本“岗位手册”,告诉它面对客户要怎样组织语言,回答技术问题要按什么结构展开,遇到知识盲区宁可说不知道也不要瞎编。
正因如此,微调之前必须先确认需求属于哪一类。如果你只想要一个能联网查资料、回答时效性问题的助手,那应该去做检索增强而不是微调;如果你要的是稳定输出 JSON、复刻某种官方客服语气、把企业内部文档里的规矩变成回答准则,那微调才是对症的方案。用一句话总结:预训练解决的是“会说话”,指令微调解决的是“懂规矩”。
1.2 为什么选 LoRA 而不是全参微调
先算一笔账。以 Qwen2 7B 为例,模型参数以 float16 存储时,权重本身就要占大约 14GB 显存。全参微调还要维护梯度、优化器状态,传统 AdamW 优化器在 fp16 下光状态就要占两倍模型大小,综合下来一张 24GB 的显卡都不一定喘得过气,消费级环境基本不用想。更不用说全参微调会把基座模型的通用知识冲得七零八落,几轮训练后模型可能连“苹果是水果”这种常识都答得小心翼翼,这就是灾难性遗忘。
LoRA 的思路完全不同。它先把原模型权重整个冻结,在旁边额外插入两个低秩小矩阵,训练时只更新这两个小矩阵。你可以把它理解为在一本字典旁边贴便利贴,原书一个字不改,你想补充的“方言释义”全写在便签上。推理时这两张小矩阵又能叠回原权重,等于只多花很少的存储和计算成本,就完成了一次针对性的行为改造。实际训练参数量通常只有总参数的 0.5% 到 2%,显存压力小得多,遗忘问题也轻得多。
QLoRA 则是在 LoRA 之上再加一步:把基座模型先用 4bit 量化压到更低显存占用,再往上挂 LoRA 矩阵。这样原本 7B 模型要占 14GB 的权重,压缩后只需要 5-6GB,入门门槛进一步降低。这也是为什么我说 12G 显存显卡也可以跑 7B 级别模型的 lora 微调,这在两年前是想都不敢想的事。至于 prefix-tuning、P-Tuning 这些方法,面对生成式大模型时效果稳定性普遍不如 LoRA,社区踩坑资料也不如 LoRA 丰富,入门阶段不用折腾。
1.3 前置条件与合理预期
动手之前,先把现实预期立好。指令微调不是炼丹显灵,它不能把模型完全没有接触过的专业知识凭空塞进头脑,更不可能靠几百条数据就造出一个行业专家。它的强项是改变模型的输出习惯和领域口径,比如让模型学会把回答限制在三个要点内、学会不暴露内部思维过程、学会遇到敏感问题先拒绝回答,这些“行为层”的问题它都很擅长。
硬件方面我的建议很直接:如果只是想跑通流程,选 Qwen2 0.5B 或 1.5B 版本,一张 8G 显存的卡也能完成 LoRA 训练;如果要做真实业务验证,7B 版本加 4bit 量化是性价比最高的组合。下面这张表是我在本地实测的大致参考,不同驱动和 CUDA 版本会有浮动:
| 配置级别 | 推荐模型 | 加载方式 | 训练显存参考 | 适合场景 |
|---|---|---|---|---|
| 入门低配 | Qwen2-1.5B | 4bit + LoRA | 8G-12G | 流程验证、小数据实验 |
| 主流单卡 | Qwen2-7B | 4bit + LoRA | 12G-16G | 小领域客服、格式改造 |
| 高配单卡 | Qwen2-7B | FP16 + LoRA | 20G-24G | 追求效果上限的正式微调 |
数据量方面,几百到几千条高质量的指令样本就足够完成一次行为改造。很多人一上来就追求几万条数据,反而容易把模型带到沟里去。记住一个口诀:指令微调是“小而准”的事情,不是“大而全”的事情。
2. 动手之前:环境准备与基础工具链
2.1 依赖清单与安装
环境是整个微调项目里最简单但也最容易翻车的环节。先确认 Python 版本在 3.10 以上,然后安装核心依赖:
pip install transformers peft accelerate bitsandbytes datasets这五个库各管一段:transformers 负责模型加载、分词器管理和训练器接口;peft 负责把 LoRA 配置注入到模型里;accelerate 负责处理设备映射,特别是多卡和 CPU offload 场景;bitsandbytes 是 4bit 量化的底层实现;datasets 用来管理训练集和测试集,支持高效的流式读取。
如果你打算跑 7B 模型追求更快训练速度,可以考虑装 flash-attn,它能把注意力部分的计算优化得非常好。但我要给你提个醒:这个库在 Linux 下编译顺畅,在 Windows 下经常因为编译工具链不一致而卡住,如果你的主要环境是 Windows,先不装它也完全能跑。另外,transformers 的版本不要太老,建议至少是 4.40 之后的版本,因为 Qwen2 架构的官方支持是在这个时间点附近才完整的,太老的版本可能压根找不到对应的模型结构。
这里有个容易忽略的环节:先跑nvidia-smi确认你的 CUDA 版本,再去安装对应版本的 torch。很多人直接把官方命令里的 torch 装进去,结果发现 CUDA 不匹配,训练时报错找不到可用的驱动。我自己的习惯是装完 torch 后马上执行一个小测试:
import torch print(torch.cuda.is_available()) print(torch.cuda.get_device_name(0))如果输出 False,说明 torch 和驱动之间没对齐,后面所有工作都白搭。
2.2 下载模型与本地目录
我不推荐在训练脚本里直接靠网络在线加载模型权重。原因很现实:训练中断、网络波动、连接不稳定,任何一个都会让你摸不清到底是代码问题还是下载问题。正确做法是先把模型权重完整下载到本地固定目录,之后再训练时只读本地文件。
目录结构大概长这样:
models/ └── qwen2-1.5b/ ├── config.json ├── model.safetensors.index.json ├── model-00001-of-00002.safetensors ├── model-00002-of-00002.safetensors ├── tokenizer.json ├── tokenizer_config.json └── vocab.json建议优先使用 safetensors 格式的权重,它比老式的 pytorch_model.bin 更安全,加载速度也更快,不会出现反序列化漏洞问题。模型下载完成后,先做一个最小冒烟测试:加载模型和分词器,跑一段最简单的生成,确认能正常输出文字再进入训练环节。这一步能帮你把“环境问题”和“训练问题”彻底切开。
2.3 几种常见硬件配置的经验参考
不同显卡跑相同的 LoRA 训练,体验差别很大。16G 显存稳定跑 7B 4bit 微调没有任何问题,只是 batch size 通常只能给到 1,靠梯度累积来弥补。24G 显存则可以稍微放开手脚,除了 LoRA 之外还能承担更长的上下文长度。如果你手头是 8G 显存的老卡,我建议别硬碰 7B,选 1.5B 版本,训练时间短,试错成本低,对理解整个微调流程完全够用。
还遇到过一个特殊情况:某个同学的机器是纯 CPU 环境,他问能不能微调 7B。我的回答是,能推理但训练会非常痛苦,一个 epoch 可能跑十几个小时。入门阶段建议放弃 CPU 训练,要么租一张云显卡,要么先降低到 0.5B 模型。
3. 指令数据集要怎么准备才算合格
3.1 开源社区最常见的三种数据格式
指令微调的数据格式没有那么多花活,最经典的就是三字段格式:instruction、input、output。instruction 放需要模型执行的指令,比如“请把下面的话改成客服话术”;input 放需要处理的附加输入,比如用户发来的原始消息;output 放理想回答。没有附加输入时,input 可以留空字符串。
我给一个直接可用的示例:
[ { "instruction": "请把下面这段话改写成正式的客服话术。", "input": "快递咋还没到,我都等烦了!", "output": "您好,非常抱歉让您久等了,我马上为您查询包裹的最新物流状态,预计会在几分钟内给您反馈。" }, { "instruction": "简述什么是RAG。", "input": "", "output": "RAG(检索增强生成)是一种让大模型在生成回答前先从外部知识库检索相关内容,再结合检索结果生成答案的方法。" } ]如果你要微调的是多轮对话能力,也可以使用 messages 字段组织对话历史,每条样本包含多组 role 和 content。但对入门者来说,我强烈建议先从单轮指令数据开始,把一条条“问题 -> 答案”理顺。单轮数据格式简单、拼接逻辑直观,跑通之后再升级到多轮,排查问题会轻松得多。
3.2 数据清洗与去重的实操方法
数据处理是决定微调效果的分水岭,但很多人把时间浪费在了花哨的数据增强上,忽略了最基础的脏数据问题。我在实际处理中通常会做三轮清洗。
第一轮是格式清洗。把所有文本里的前后空白、全角空格、异常换行、HTML标签清理掉。特别要注意那种“看起来正常但中间混了零宽字符”的样本,这类字符肉眼看不出来,但训练进模型后会导致生成时偶发乱码。第二轮是去重。指令微调数据里大量重复不仅浪费训练时间,还会直接把模型推向过拟合。简单的做法就是把 instruction 和 output 拼接后的字符串做哈希,用 set 去重。更精细一点,可以只对 instruction 做归一化去重,因为对用户问题来说,哪怕回答形式略有不同,只要指令一样,保留一条高质量回答就够了。
第三轮是抽检,这步最容易被忽略。清洗完成后,随机抽出 10 条样本逐条看,如果发现两条以上存在指令和答案不匹配、答案语义残缺、或者模型根本没法推理的情况,说明数据质量不过关,需要回炉。我个人的经验是:100 条人工校准过的数据,比 1000 条从网上批量抓回来的数据更能带来稳定的效果提升。数据质量和数量从来不是同等级的事。
3.3 数量、epoch 与过拟合的平衡
新手常见的第一反应是:数据越多越好,先凑五万条再说。但实际上指令微调更像是在教模型“行为习惯”,而不是机械地记住知识。一个垂直场景,比如公司内部 IT 支持问答,三五百条覆盖了常见问题的高质量样本,配合两三个 epoch,效果就已经能明显感觉到。反而数据量一大,标注质量参差不齐,模型容易被长尾噪声带偏。
那怎么判断过拟合还是没拟合透?我会固定一条验证集,大概 20 条和训练集同分布但不重复的问题,每结束一个 epoch 就人工看一遍输出。刚开始时模型的回答可能还带着通用味道,这是欠拟合;随着 epoch 增加,回答越来越贴近目标风格。但如果发现回答开始重复固定句式、把训练集里的名字拼音生硬套进新问题,那就是过拟合征兆,应该提前停止训练或降低数据里冗余样本的比例。
数据增强可以少量使用,但要有边界。比如你发现某个领域术语在样本里出现次数太少,可以写几个模板问题,用规则方式生成一批带术语的问法。这属于合理的补充。但不要拿大模型做大规模同义改写或反事实改写,那样生成的样本往往带着模型自身的老毛病,等于又把微调要解决的问题喂了回去。
4. 实战核心:用 LoRA 把 Qwen2 调成你要的样子
4.1 加载模型与分词器时的关键设置
一切环境就绪后,进入训练前的核心代码环节。先加载分词器和模型,我在 QLoRA 场景下会这样写:
import torch from transformers import AutoTokenizer, AutoModelForCausalLM, BitsAndBytesConfig model_dir = "./models/qwen2-1.5b" tokenizer = AutoTokenizer.from_pretrained(model_dir, use_fast=False) tokenizer.pad_token = tokenizer.eos_token bnb_config = BitsAndBytesConfig( load_in_4bit=True, bnb_4bit_quant_type="nf4", bnb_4bit_compute_dtype=torch.bfloat16, bnb_4bit_use_double_quant=True, ) model = AutoModelForCausalLM.from_pretrained( model_dir, quantization_config=bnb_config, device_map="auto", ) model.config.use_cache = False这里有几个细节值得展开。bnb_4bit_quant_type="nf4"代表使用 NormalFloat4 量化格式,相比传统 int4,nf4 对权重分布的拟合更好,微调效果损失更小。bnb_4bit_use_double_quant=True会对量化常数再做一层量化,进一步省显存,代价是加载时间稍长,但值得。device_map="auto"让 accelerate 自动把各层分配到可用设备上,单卡环境下体验最省心。
use_fast=False是我在 Qwen2 上踩过坑之后养成的习惯。有些 tokenizer 的 fast 版本在特殊 token 处理上会变得激进,导致训练时拼出的文本和你设想的模板不一致。为了稳定,我通常直接用慢速 tokenizer,性能差别在微调场景里可以忽略。至于use_cache=False,这是为了防止与 gradient checkpointing 冲突,同时也减少显存冗余。
模型加载完成后,可以先打印一下 tokenizer 的 chat_template,确认 Qwen2 自带的对话模板是否还在。这个模板决定了后面推理时消息能不能正确拼接,很多人会在这上面踩坑。
4.2 LoRA 配置:挂在哪些层、r 和 alpha 怎么选
模型加载好后,开始注入 LoRA 适配器。我的参考配置如下:
from peft import LoraConfig, get_peft_model lora_config = LoraConfig( r=16, lora_alpha=32, target_modules=[ "q_proj", "k_proj", "v_proj", "o_proj", "gate_proj", "up_proj", "down_proj" ], lora_dropout=0.05, bias="none", task_type="CAUSAL_LM", ) model = get_peft_model(model, lora_config) model.print_trainable_parameters()先解释 target_modules 为什么要挂这么多。Qwen2 是标准的 transformer 架构,q/k/v/o 是注意力投影矩阵,gate/up/down 是前馈网络里的全连接层。注意力层决定模型“关注什么”,前馈层决定模型“记住和表达什么”。指令微调既要改变响应模式,又要强化特定领域的表达,所以把这两类模块都挂上 LoRA 是最稳的选择。
r 是低秩矩阵的秩,决定 LoRA 可学习的维度上限。r=16 是社区认可度很高的默认值,效果稳定。r 越大可训练参数越多,但不代表效果一定更好,因为低秩本身是种正则化。lora_alpha 是缩放因子,实际生效的缩放比例是 alpha / r,我这里 alpha=32、r=16,缩放比例为 2。这个值可以保持默认,不需要频繁调整。
bias="none"的意思是,不训练模型里的偏置项。偏置项参数量很少,但对于 LoRA 来说,训练它往往带来不稳定,保持冻结更安全。lora_dropout=0.05是低秩矩阵前的随机失活,稍微提一点泛化。
执行print_trainable_parameters(),你会看到类似“trainable params: X / Y”的输出。比如 1.5B 模型,可训练参数大概在几十万到数百万级别,总占比非常低。看到这个数字,你对 LoRA 的“微调”就会有一个更直观的感受:动的地方真的不多,所以行为改造必须靠精准数据来引导。
4.3 数据处理与 Token 化:全量计算 loss 还是屏蔽指令
接下来把 JSON 数据转成模型能吃的训练样本。我先定义一个格式函数,把 instruction、input、output 拼成一段连续文本:
def format_example(ex): if ex.get("input"): text = ( f"指令:{ex['instruction']}\n" f"输入:{ex['input']}\n" f"回答:{ex['output']}" ) else: text = ( f"指令:{ex['instruction']}\n" f"回答:{ex['output']}" ) return {"text": text} dataset = dataset.map(format_example)拼接技巧在于换行符。指令和输入用 \n 分隔,回答前也放 \n,这样训练时模型读到“回答:”这个前缀后,自然会把后面的内容当作要生成的部分。如果你预留了 input 字段但内容为空,那就不要拼进去,避免模型学到“输入:”空着也正常。
Token 化环节有一个分歧点:到底要不要对指令部分做 label masking。标准做法是只对回答部分计算 loss,避免模型花费精力去“记忆”用户输入里的噪声。但入门阶段,很多人直接对整个拼接文本计算 loss,效果往往也还好,因为模型天然会把重心放在新内容生成上。如果你不想在这个细节上纠缠,可以先全量计算;如果 train loss 异常高,再回来考虑 masking。
处理时建议max_length=1024,超长样本截断,短样本不做 padding,让动态批处理自己解决。把padding=False设好,数据集内存占用会小很多。
4.4 训练参数怎么给才不容易翻车
训练参数我用下面这张表,是一个在 1.5B 和 7B 上都实测稳定过的组合:
| 参数名 | 推荐值 | 说明 |
|---|---|---|
| per_device_train_batch_size | 1 | 显存小的卡友好,依靠梯度累积 |
| gradient_accumulation_steps | 8 | 等效 batch size = 8 |
| learning_rate | 2e-4 | LoRA 常用学习率区间 |
| lr_scheduler_type | cosine | 训练后期平滑衰减 |
| warmup_ratio | 0.03 | 前 3% 步数预热 |
| num_train_epochs | 3 | 数据量小,3 轮足够 |
| logging_steps | 10 | 每 10 步打印一次 loss |
| save_strategy | epoch | 每个 epoch 保存一次检查点 |
| fp16 | True | 半精度混合训练 |
学习率是最容易出问题的参数。LoRA 只有极少数参数在训练,所以学习率通常比全参微调高一些,1e-4到2e-4是常见区间。如果你把通用大模型预训练时的3e-5拿来套,会发现 loss 死气沉沉,怎么训都不动。
启动训练也很简单:
from transformers import TrainingArguments, Trainer training_args = TrainingArguments( output_dir="./checkpoints/qwen2-lora", per_device_train_batch_size=1, gradient_accumulation_steps=8, learning_rate=2e-4, lr_scheduler_type="cosine", warmup_ratio=0.03, num_train_epochs=3, logging_steps=10, save_strategy="epoch", fp16=True, gradient_checkpointing=True, report_to="tensorboard", ) trainer = Trainer( model=model, args=training_args, train_dataset=dataset_tokenized, tokenizer=tokenizer, ) trainer.train()如果你只想跑通流程,这个组合非常省心。1.5B 模型几百条数据在消费级显卡上大约二三十分钟到一个小时就能训完;7B 模型可能要三四个小时。训练过程中盯着 loss 曲线看,下降趋势正常就说明数据、代码、参数都没大问题。
4.5 保存断点与 LoRA 适配器
训练结束后,保存的东西一定要分清。不要直接model.save_pretrained把整个大模型再存一遍,那样既慢又占磁盘,而且因为基座是 4bit 加载的,保存时反而可能出现量化格式不匹配的问题。正确的保存方式是:
model.save_pretrained("./output/qwen2-lora-ckpt") tokenizer.save_pretrained("./output/qwen2-lora-ckpt")这样保存的文件体积很小,几 MB 到几百 MB 不等,里面只有 LoRA 适配器的权重和配置。下次要用的时候,把 LoRA 适配器加载到同一个基座模型上,就能无缝恢复。这个习惯帮你节省大量磁盘空间,也方便同一个基座反复试验多个不同方向的微调。
训练中途如果断掉,Trainer 自动支持断点恢复。重新启动时加上resume_from_checkpoint=True,它会从最近一次保存的检查点继续训练。这个功能在长训练任务里非常实用,谁也不想因为一次断电回到起点。
5. 权重合并与效果检验
5.1 把 LoRA 权重合并回基座模型
微调完成并不代表能用顺手,因为 LoRA 适配器毕竟是一层“外挂”。要么在加载时通过 peft 接口直接加载适配器推理,要么把 LoRA 矩阵合并回基座模型,得到一个新的完整权重。两者都可以,但不同场景选择不同。如果你只是想快速验证效果,直接用 peft 热加载最方便;如果要部署到推理服务,或者把模型分发给别人,则需要合并成完整版本。
合并代码很直接:
from peft import PeftModel base_model = AutoModelForCausalLM.from_pretrained( "./models/qwen2-1.5b", torch_dtype=torch.bfloat16, device_map="auto", ) model = PeftModel.from_pretrained(base_model, "./output/qwen2-lora-ckpt") model = model.merge_and_unload() model.save_pretrained("./output/qwen2-merged") tokenizer = AutoTokenizer.from_pretrained("./output/qwen2-lora-ckpt") tokenizer.save_pretrained("./output/qwen2-merged")合并后的模型体积几乎和基座一样大,所以如果你的目标是频繁试验多个 LoRA 适配器,不合并直接用 PeftModel 加载才是正解。合并倒是有一个好处:推理阶段的底层代码不用感知 LoRA 的存在,方便接入各种标准推理框架。
5.2 推理模板与微调前后对比
推理阶段最容易翻车的是对话模板不一致。训练时你自己拼了“指令:\n输入:\n回答:”,如果推理时也手动拼,那直接用 base model 的generate就行。但如果走了apply_chat_template,就必须保证模板格式和训练数据一致。
我推荐直接用 Qwen2 自带的对话模板,因为它能正确控制 system、user、assistant 三种角色。推理代码如下:
from transformers import AutoModelForCausalLM, AutoTokenizer tokenizer = AutoTokenizer.from_pretrained("./output/qwen2-merged") model = AutoModelForCausalLM.from_pretrained( "./output/qwen2-merged", torch_dtype=torch.bfloat16, device_map="auto", ) messages = [ {"role": "system", "content": "你是一个耐心、简洁的客服助手。"}, {"role": "user", "content": "快递为什么还没发货?"}, ] text = tokenizer.apply_chat_template( messages, tokenize=False, add_generation_prompt=True, ) inputs = tokenizer([text], return_tensors="pt").to(model.device) outputs = model.generate( **inputs, max_new_tokens=512, do_sample=True, temperature=0.7, top_p=0.9, ) response = tokenizer.decode( outputs[0][inputs["input_ids"].shape[1]:], skip_special_tokens=True, ) print(response)对比测试一定要用训练时没见过的样本。如果你拿训练集里的同一句话去测,模型只是做了“背诵”,根本测不出真实水平。身边很多同学在做演示时用训练集样本,效果特别好,一上真实用户流量就露馅,这是数据集切分不严谨的结果。
5.3 快速评测的三种手段
正规评测可以用一堆自动化指标,但入门阶段我建议以人工评测为主。固定 20 条和训练集同分布但不重复的测试问题,微调前先跑一遍记录 baseline,微调后再跑一遍,对比输出变化。
评测时重点看三个维度:格式正确率、内容准确率、指令遵循率。比如你要模型统一输出“问题摘要 + 处理建议 + 安抚话术”三段式结构,那 20 条里有多少条严格遵守了结构,这就是格式正确率。内容准确率则要看是否出现事实性错误,比如把产品价格说错。指令遵循率检验模型有没有额外输出“好的,根据您的要求…”这类废话。
还可以做一个“骰子测试”:同一道问题分别用 temperature=0.2 和 temperature=0.8 跑两次,看输出稳定性。temperature 低时回答应该高度稳定,如果低温度下输出仍然前后矛盾,说明模型对这个问题的学习并不可靠。
6. 实战中躲不开的坑:排查手册与个人经验
6.1 显存溢出 OOM 怎么处理
训练时报CUDA out of memory,我见过太多次。第一反应不要加 batch size,先把per_device_train_batch_size降到 1,再去开gradient_checkpointing=True。这两步能把训练显存需求压得非常低。如果还是溢出,就把上下文长度从 1024 降到 512,或者换更小的模型版本。
还有一个很隐形的显存消耗点:输入拼接太长。如果数据里有几条特别长的样本,即便 batch size 是 1,单条样本也可能撑爆显存。用max_length截断是常用的兜底方案,但我建议在数据分析阶段先看长度分布,不要让超长样本处于不受控状态。特别长的样本要么拆分,要么截断,绝不能留下隐患。
6.2 loss 不下降或反复横跳
如果训练了十几个 step,loss 纹丝不动,先别急着调学习率,你要怀疑数据处理。最常见的问题是数据格式拼接错误,比如 output 被当成指令的一部分参与训练,模型学到的是“问题和答案一直在重复”。我的排查第一步是先搞一份最小数据,比如十条,把格式函数手动跑一遍,打印出拼接后的文本,肉眼看一下有没有错位。
第二步是单样本过拟合测试。把训练集截成一条样本,batch size=1,学习率调大到 5e-4,训练十几个 step,如果 loss 能快速下降到接近零,说明代码链路没问题,问题出在真实数据集的质量或量级上。如果一条样本都过拟合不了,那大概率是数据处理环节的bug,而不是训练参数的锅。
6.3 微调后通用能力变差怎么办
这是一个非常真实的体验:微调完处理目标任务确实稳了,但问它“1+1等于几”反倒支支吾吾。原因通常是三个:epoch 太多、领域数据占比过高、学习率过大。LoRA 虽然比全参微调温和,但本质还是在把模型往特定方向推,推过头自然把通用知识的权重压住了。
应对办法有三个。最直接的是减少 epoch,从 3 降到 1 或 2,在验证集上观察通用能力损失情况。第二是混入少量通用对话数据,比如把通用助手数据按照三比一或四比一的比例加进训练集,让模型保留基本对话习惯。第三是降低学习率,从 2e-4 降到 1e-4。这三个办法可以组合,效果通常立竿见影。
6.4 推理输出格式错乱、夹带前缀的问题
很多微调完的模型会生成“好的,根据您的要求,我为您整理如下”这类冗长前缀,或者在结尾带上<|im_end|>特殊 token。前者是因为训练数据里的回答前缀没有统一规范,模型学到了“啰嗦寒暄”的习惯;后者多半是推理时没有设置skip_special_tokens=True。
解决前者的办法是在数据清理阶段把 output 字段里的“帽子”摘干净,确保每条回答的第一句话都是业务内容,而不是寒暄。解决后者的办法更简单,直接把解码参数改为skip_special_tokens=True就行。如果模板拼接错误,比如训练时用了“回答:”推理时又用 chat template 换成了“assistant”,模型输出的风格也会飘。我的做法是训练前和推理前各打印一次拼接结果,保证保持一致。
最后补一个我自己的习惯:每次微调前,把基座模型的目录复制一份只读备份,LoRA 适配器分别命名保存。因为合并操作是不可逆的,想回到某个尝试版本,只能靠备份。这个习惯帮我省下了无数次重新下载模型的时间。入门阶段你会反复试验不同版本的数据和参数,提前做好备份和命名规范,比什么都管用。