基于《大规模语言模型:从理论到实践(第2版)》第5章 指令微调
爆款小标题:指令数据怎么建、LoRA 怎么配?原书第5章指令微调与高效微调精讲
为什么这一节重要
预训练得到的基座模型「会说话、有知识」,但未必会按人的指令格式输出、也未必在「多个合理答案」中选人类更喜欢的那一个。指令微调(SFT) 就是通过高质量的「指令—输入—输出」数据,教会模型遵循指令、适应对话与任务格式;而参数高效微调(如 LoRA) 让我们在有限显存下只训练少量参数就能获得不错效果。本节基于原书第 5 章,把指令微调的目标与数据形式、LoRA 的原理与配置、以及长上下文扩展的常见思路讲清,并给出数据构建与 LoRA 调参的实战要点。
学习目标
学完本节,你将能够:
- 说清指令微调:说明 SFT 的目标、典型数据形式(指令/输入/输出)与训练流程(损失在哪算、如何构造输入序列)。
- 掌握 LoRA:解释 LoRA 如何在线性层旁路加入低秩矩阵 A、B,为何只训 A/B 就能逼近全量微调、推理时如何合并;能根据基座与任务初步选择 rank、alpha、target_modules。
- 了解长上下文扩展:说出位置编码插值、NTK、YaRN 等常见做法的大致思路,以及何时需配合长上下文数据微调。
一、指令微调的目标与数据形式(原书第 5 章)
目标:让模型在给定「指令(及可选输入)」时,生成符合任务要求、格式稳定、且与人类期望一致的「输出」。与预训练不同,这里的数据是有监督的:每条样本包含明确的指令、输入(可选)与期望输出。
典型数据格式:原书第 5 章采用的形态可概括为:
- instruction:任务描述或角色设定(如「你是一个客服助手,请根据知识库回答用户问题。」)
- input:可选,用户输入或上下文(如用户问题、检索到的段落)
- output:期望模型生成的内容(如标准回答、结构化 JSON)
训练时,把 instruction(+ input)与 output 按对话或模板拼成序列,对 output 部分计算语言建模损失(交叉熵),instruction 与 input 部分通常不参与损失计算(或 mask 掉),这样模型主要学习「在给定指令与输入下生成正确输出」。
数据来源与质量:原书强调,指令数据可来自人工标注、模型生成后筛选、蒸馏(用大模型为小模型生成)以及课程学习(从易到难)。质量与多样性共同决定指令遵循与泛化:低质量或重复过多的数据会拉低表现;建议覆盖真实用户问法、负例与边界 case,避免只做模板式数据。
数据格式示例(JSON 行格式):
{"instruction": "你是一个客服助手,请根据知识库回答用户问题。", "input": "请问退货政策是什么?", "output": "根据我们的政策,商品签收后 7 天内可无理由退货..."}
{"instruction": "请将下列文本翻译成英文。", "input": "今天天气不错。", "output": "The weather is nice today."}
训练时拼接为:<instruction>\n{instruction}\n\n<input>\n{input}\n\n<output>\n{output},仅对 <output> 之后的部分计算 loss;若无 input,可省略对应字段。
二、LoRA 的原理与使用(原书第 5 章高效微调)
动机:全量微调需要更新全部参数,显存与优化器状态占用大;对于大模型,单卡或少量卡上很难做全量微调。参数高效微调(PEFT) 只更新少量参数,在保持效果的前提下大幅降低显存与训练成本。
LoRA 做法:对线性层 (h = Wx),不直接更新 (W),而是引入旁路:(h = Wx + (B A) x),其中 (A \in \mathbb{R}^{r \times d_{\text{in}}}),(B \in \mathbb{R}^{d_{\text{out}} \times r}),(r \ll d) 为低秩。训练时冻结 (W),只训练 (A) 和 (B);推理时可将 (W' = W + BA) 合并回单矩阵,从而不增加推理延迟。原书第 5 章对 LoRA 的数学与实现有说明。
参数量:若原线性层为 (d_{\text{out}} \times d_{\text{in}}),LoRA 新增参数量为 (r \cdot (d_{\text{in}} + d_{\text{out}}))。例如 4096×4096 的层、rank=8,则新增 8×(4096+4096)=65536 个参数,远小于原层的 4096×4096≈1677 万。
超参:rank (r) 控制表达能力,过小可能欠拟合、过大接近全量微调且易过拟合;alpha 常与 rank 配合,用于缩放 LoRA 输出(等价于学习率与 rank 的联合调节);target_modules 指定对哪些层加 LoRA(如只对 attention 的 q/v、或只对 FFN)。建议先用小规模数据与验证集做 rank、alpha、target_modules 的扫描,再放大。
三、长上下文扩展(原书第 5 章及相关)
问题:基座多在 2K、4K 或 8K 长度上训练,业务若需要 32K、128K 等更长上下文,直接推理往往效果差或显存爆。原因包括:位置编码在训练长度外泛化差、注意力计算复杂度随序列长度平方增长、以及模型可能未在长序列上学习到有效利用长程信息的能力。
常见思路:
- 位置编码插值:把位置索引线性缩放,使「物理长度」落在训练过的范围内;简单但可能损失长程精度,尤其对 RoPE 等相对位置编码,需注意缩放方式。
- NTK-aware 插值:在 RoPE 等编码下,对不同维度做不同缩放,以兼顾短序列与长序列;相比线性插值在长序列上的表现通常更好。
- YaRN 等:在 RoPE 基础上做更细的插值或外推策略,不少长上下文模型(如 LLongMA、超过 32K 的 LLaMA 变体)采用。
有时还需配合长上下文数据微调:在更长序列(如 32K)上构造指令数据做 SFT,使模型「学会利用」长上下文(如检索到多段文档后综合回答)。仅做位置插值而不做长上下文微调,模型可能仍难以有效利用超长输入中的信息。原书第 5 章对上下文扩展有简要讨论,更多细节可参考相关论文与实现。
四、案例:LoRA 训练实操步骤
完整可运行代码(依赖:pip install transformers peft datasets accelerate;使用 Qwen2-0.5B,单卡可跑):
import os
from datasets import Dataset
from transformers import AutoModelForCausalLM, AutoTokenizer, TrainingArguments, Trainer, DataCollatorForLanguageModeling
from peft import LoraConfig, get_peft_model, TaskType
def prepare_data():
data = [{"instruction": "你是一个客服助手。", "input": "退货政策是什么?", "output": "7天内可无理由退货。"}, {"instruction": "请简洁回答。", "input": "远程办公几天?", "output": "每周可远程2天。"}]
return [{**d, "id": i} for i, d in enumerate(data * 10)]
def format_conv(item, tokenizer):
prompt = f"<instruction>\n{item['instruction']}\n\n<input>\n{item['input']}\n\n<output>\n"
return {"text": prompt + item["output"] + tokenizer.eos_token, "prompt_len": len(tokenizer.encode(prompt))}
def main():
model_name, output_dir = "Qwen/Qwen2-0.5B-Instruct", "./lora_output"
os.makedirs(output_dir, exist_ok=True)
tokenizer = AutoTokenizer.from_pretrained(model_name, trust_remote_code=True)
model = AutoModelForCausalLM.from_pretrained(model_name, trust_remote_code=True, torch_dtype="auto", device_map="auto" if __import__("torch").cuda.is_available() else None)
model = get_peft_model(model, LoraConfig(r=8, lora_alpha=16, target_modules=["q_proj", "v_proj"], lora_dropout=0.05, bias="none", task_type=TaskType.CAUSAL_LM))
raw = prepare_data()
texts = []; prompt_lens = []
for item in raw:
o = format_conv(item, tokenizer)
texts.append(o["text"]); prompt_lens.append(o["prompt_len"])
ds = Dataset.from_dict({"text": texts, "prompt_len": prompt_lens})
def tokenize(examples):
return tokenizer(examples["text"], truncation=True, max_length=512, padding="max_length", return_tensors=None)
ds = ds.map(tokenize, batched=True, remove_columns=["text"])
def set_labels(examples):
labels = []
for i, ids in enumerate(examples["input_ids"]):
pl = examples["prompt_len"][i]
ll = [-100] * pl + ids[pl:]
ll = (ll + [-100] * len(ids))[:len(ids)]
labels.append(ll)
examples["labels"] = labels
return examples
ds = ds.map(set_labels, batched=True, remove_columns=["prompt_len"])
trainer = Trainer(model=model, args=TrainingArguments(output_dir=output_dir, per_device_train_batch_size=2, gradient_accumulation_steps=2, num_train_epochs=1, learning_rate=5e-5, logging_steps=2, save_strategy="no", fp16=__import__("torch").cuda.is_available()), train_dataset=ds, data_collator=DataCollatorForLanguageModeling(tokenizer=tokenizer, mlm=False))
trainer.train()
model.save_pretrained(output_dir); tokenizer.save_pretrained(output_dir)
if __name__ == "__main__":
main()
r:rank;lora_alpha:缩放因子;target_modules:对 q、v 加 LoRA。合并可用model.merge_and_unload()。
五、工程实战要点
1. 指令数据要覆盖真实分布与边界
避免只做「模板式」指令(如千篇一律的「请回答以下问题」);要包含真实用户问法、多轮与单轮、负例(如不应回答的问题)与边界 case(如超长输入、敏感话题)。可用抽样或小模型做质量与多样性检查。
2. LoRA 先小规模实验再放大
先用几百到几千条数据、小 rank(如 8)、单卡或少量卡跑通流程,观察验证集 loss 与人工抽检;再调 rank、alpha、target_modules,最后再放大数据与规模。避免一上来就大 rank、全模块、海量数据,导致调参成本高且难定位问题。
3. 推理时合并 LoRA 以省显存与延迟
若部署环境支持,将训练好的 LoRA 与基座合并为单权重再推理,可省去运行时加载 LoRA 的显存与计算;若需要动态切换多套 LoRA,则保留分离形式。
六、常见误区与避坑指南
误区一:指令数据越多越好
低质量与重复指令会拉低指令遵循与泛化。避坑:质量优先、多样性其次;可做去重与质量过滤,并留验证集监控过拟合。
误区二:LoRA rank 随便设
rank 过小表达能力不足,过大易过拟合且接近全量微调。避坑:用验证集做 rank 与 alpha 的扫描;常见从 8、16、32 试起。
误区三:忽略「指令」与「输出」的对应关系
若指令与输出不匹配(如指令说「只输出是/否」但输出是长段落),模型会学乱。避坑:数据构造时严格保证指令与输出格式一致,并做抽样检查。
七、小结与衔接
本节基于原书第 5 章梳理了指令微调的目标与数据形式、LoRA 的原理与参数量及超参选择、以及长上下文扩展的常见思路;并给出了指令数据覆盖真实与边界、LoRA 先小规模实验、推理时合并等工程要点。下一节将进入强化学习与人类反馈(RLHF):奖励模型、PPO 与 RLOO/GRPO 等,理解「对齐」阶段如何塑造模型偏好与安全(原书第 6 章)。
课后思考题
- 设计一个最小可用的「指令微调」数据格式(字段与示例),并说明为什么需要「指令」和「输出」两个部分。
- 若基座某线性层为 4096×4096,LoRA rank=8,则该层新增可训练参数数量是多少?与全量微调该层参数量如何比较?