英文大语言模型中文指令微调实战指南
Part1 前言
在之前的继续预训练(Continual Pre-training)讲解中,我们已经对从数据处理到训练、预测的整个流程有了基本了解。实际上,指令微调(Instruction Fine-tuning, SFT)的流程与预训练类似,但在数据构造和模型适配上有所不同。
当我们选择好一个大语言模型后,例如 ChatGLM、LLaMA、Bloom 等,要想使用它进行中文指令微调,必须深入理解三个方面:输入数据的格式、Tokenization 机制、以及模型的使用方式。本文将基于 Chinese-LLaMA-Alpaca 项目的训练代码,详细拆解指令微调的核心步骤。
Part2 数据构造
数据的输入是微调的基础。一般情况下,我们需要在模型的官方代码中找到数据输入的部分,或者参考其他开源项目的数据预处理逻辑。建议先找一份小的数据集,单独运行预处理脚本,观察输出结果,特别是 input_ids 中的特殊标记和 labels 的构造方式。
2.1 数据格式规范
指令数据通常由三部分组成:
- Instruction (instruct): 提示指令,描述任务。
- Input (query): 文本输入,可选,为空时仅依赖指令。
- Output (answer): 返回的结果,即模型需要生成的内容。
构造样本时,一般将 Instruction 和 Input 拼接作为 Prompt,最终对 Output 进行预测。需要注意的是,不同模型对 Prompt 的格式要求不同,例如 LLaMA 系列常用 Alpaca 格式,而 ChatGLM 有特定的对话模板。
PROMPT_DICT = {
"chatglm_input": "{instruction}{input}",
"alpaca_input": (
"Below is an instruction that describes a task. "
"Write a response that appropriately completes the request.\n\n"
"### Instruction:\n{instruction}{input}\n\n### Response: "
),
"bloom_input": "Human: \n{instruction}{input}\n\nAssistant: \n",
}
2.2 Token ID 与 Labels 构建
假设我们有样本:我爱北京天安门,你喜欢什么?,分词后得到 token 序列,转换为 token_id。对于 Output 我喜欢故宫,同样转换为 token_id。
一般情况下,Output 前后会被标识符包裹,如 bos_token_id (开始) 和 eos_token_id (结束)。样本的 input_ids 结构如下:
[Instruction_Tokens] + [bos_token_id] + [Output_Tokens] + [eos_token_id]
关于 labels 的构建,关键在于损失计算的范围。我们只希望模型学习生成 Output 部分,而不需要为 Instruction 部分计算损失。因此,Instruction 部分的 labels 应填充为 -100(IGNORE_INDEX),Output 部分的 labels 则为对应的 token_id。
示例:
- Input IDs:
[12, 112, ..., 545, 1, 12, 2346, 654, 2]
- Labels:
[-100, -100, ..., -100, 1, 12, 2346, 654, 2]
这里 -100 表示在计算交叉熵损失时忽略该位置。如果设置了最大序列长度,input_ids 后面用 pad_token_id 填充,标签则继续用 -100 填充。
针对 ChatGLM,除了上述说明外,它还有一个额外的 [gMASK] 标记。其输入构造可能如下:
input_ids = instruction_ids + [gmask] + sop_ids + output_ids + eop_ids
labels = [-100] * len(instruction_ids + gmask + sop_ids) + output_ids + eop_ids
不同模型的输入构造差异较大,需特别注意:
- 特殊标记的使用(如 bos, eos, pad, gmask)。
- 是否需要额外输入(如 attention_mask, position_ids)。
- 模型内部是否自动转换 labels 计算损失,有的模型(如 CPM-Bee)可能需要手动处理。
Part3 Tokenization 详解
Tokenization 是将文本转换为模型可理解的数字序列的过程。在微调前,务必探索清楚目标模型的 tokenizer 行为。
3.1 特殊标记检查
加载 tokenizer 后,应检查以下属性是否为空或有效:
bos_token, eos_token: 句首/句尾标记。
pad_token: 填充标记。若为 None,需手动指定(通常设为 eos_token)。
unk_token: 未知字符标记。
from transformers import AutoTokenizer
tokenizer = AutoTokenizer.from_pretrained("model_hub/chatglm-6b", trust_remote_code=True)
print("BOS token:", tokenizer.bos_token)
print("EOS token:", tokenizer.eos_token)
print("PAD token:", tokenizer.pad_token)
3.2 编码与解码测试
通过实际文本验证分词效果,确保中文支持正常,且特殊标记被正确识别。
text = "我爱北京天安门"
print(tokenizer(text))
print(tokenizer.convert_ids_to_tokens([18060, 12247, 14949]))
print(tokenizer.decode([18060, 12247, 14949]))
注意某些模型(如 ChatGLM)特有的 build_inputs_with_special_tokens 方法,用于处理多轮对话或特定结构的输入。
Part4 模型加载与推理
模型加载方式因框架而异。大多数现代模型支持 AutoTokenizer 和 AutoModelForCausalLM,但部分模型(如 LLaMA 早期版本)需要专用类。
4.1 加载配置
对于 ChatGLM 等包含自定义代码的模型,加载时必须设置 trust_remote_code=True。这会允许加载器执行远程代码以找到正确的模型文件(如 modeling_chatglm.py)。
from transformers import AutoTokenizer, AutoModel
tokenizer = AutoTokenizer.from_pretrained("model_hub/chatglm-6b", trust_remote_code=True)
model = AutoModel.from_pretrained("model_hub/chatglm-6b", trust_remote_code=True).half().cuda()
model = model.eval()
response, history = model.chat(tokenizer, "你好", history=[])
print(response)
response, history = model.chat(tokenizer, "晚上睡不着应该怎么办", history=history)
print(response)
4.2 显存优化
加载模型时,建议使用 .half() 或 .bfloat16() 进行半精度加载,以减少显存占用并加速推理。若显存不足,可考虑量化加载(如 bitsandbytes)。
Part5 训练配置与优化
在实际微调训练中,结合高效的库可以显著提升训练速度和稳定性。
5.1 核心库集成
- DeepSpeed: 用于分布式训练和 ZeRO 优化,大幅降低显存需求。
- Transformers: Hugging Face 提供的标准接口。
- PEFT (LoRA): 参数高效微调技术,冻结主干网络,仅训练少量适配器参数。
- Datasets: 高效加载和处理大规模数据集。
5.2 LoRA 参数配置
使用 LoRA 时,关键参数包括:
r: 低秩矩阵的维度,通常取 8, 16, 32。
lora_alpha: 缩放系数,通常设为 r 的倍数。
target_modules: 需要应用 LoRA 的线性层,如 q_proj, v_proj。
from peft import LoraConfig, get_peft_model
peft_config = LoraConfig(
r=16,
lora_alpha=32,
target_modules=["q_proj", "k_proj", "v_proj", "o_proj"],
lora_dropout=0.1,
bias="none",
task_type="CAUSAL_LM"
)
5.3 DeepSpeed 配置
DeepSpeed 配置文件 (deepspeed_config.json) 定义了优化策略。常见的 ZeRO 阶段包括:
- ZeRO-1: 优化器状态分片。
- ZeRO-2: 优化器状态 + 梯度分片。
- ZeRO-3: 优化器状态 + 梯度 + 参数分片(最省显存)。
5.4 训练循环
使用 transformers.Trainer 可以简化训练过程。需定义 TrainingArguments,指定学习率、batch size、epoch 数及保存策略。
from transformers import TrainingArguments
training_args = TrainingArguments(
output_dir="./output",
per_device_train_batch_size=4,
gradient_accumulation_steps=4,
learning_rate=2e-5,
num_train_epochs=3,
fp16=True,
logging_steps=10,
save_strategy="epoch",
deepspeed="ds_config.json"
)
Part6 评估与部署
SFT 之后通常还有对齐(Alignment)阶段,如使用奖励模型 + PPO 进行人类反馈强化学习(RLHF),但这超出了基础指令微调的范围。
6.1 评估指标
- 困惑度 (Perplexity): 衡量模型对测试集数据的拟合程度。
- BLEU/ROUGE: 传统 NLP 任务指标,适用于生成任务对比。
- 人工评估: 邀请专家对回复质量打分,是最可靠的评估方式。
6.2 后续方向
完成微调后,建议关注 LangChain 等框架,以便将模型集成到实际应用中。LangChain 提供了丰富的链式调用能力,可以连接知识库、API 等外部资源,构建复杂的 Agent 系统。
Part7 总结
本文详细讲解了如何让英文大语言模型支持中文指令微调。核心步骤包括:
- 数据准备: 遵循特定格式,构造 input_ids 和 labels。
- Tokenization: 确认特殊标记和分词逻辑。
- 模型加载: 正确处理 remote code 和精度。
- 训练优化: 利用 LoRA 和 DeepSpeed 提升效率。
通过上述流程,开发者可以有效地将通用大模型转化为具备特定领域知识或语言能力的专用模型。