外观
英文大语言模型中文适配之指令微调
内容整理自学习笔记,仅供面试备考参考;不构成录用、培训或考试承诺。
1. 为什么需要对预训练模型进行指令微调?
在完成继续预训练之后,我们已了解从数据处理到训练、预测的整个流程。选择好大语言模型(如 ChatGLM、LLaMA、BLOOM 等)后,要使用它需要了解三个方面:
| 关键方面 | 说明 |
|---|---|
| 输入数据的格式 | 不同模型对输入数据的组织方式不同 |
| Tokenization | 分词器的工作机制和特殊标记使用 |
| 模型的使用方式 | 模型加载、前向计算、生成等 |
2. 指令微调数据如何处理?
2.1 数据处理思路
一般在模型的官方代码或开源项目中找到数据预处理部分,取一小部分数据集单独运行,查看输出结果。重点关注 input_ids 中的特殊标记和 labels 的构造方式。
例如 CPM-Bee 在 forward 中需要额外传入
span和length,与一般模型只需input_ids和labels不同。
2.2 指令数据格式
指令数据一般由三部分组成:
| 字段 | 别名 | 说明 |
|---|---|---|
instruction | instruct | 提示指令 |
input | query | 输入文本(可为空) |
output | answer | 返回的结果 |
构造时一般将 instruction 和 input 拼接,最终对 output 进行预测。不同模型的 prompt 模板不同:
python
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.3 ChatGLM 数据处理代码
python
import logging
import os
from dataclasses import dataclass
from typing import Optional, Dict, Sequence, Union, List
import datasets
import torch
from datasets import load_dataset, concatenate_datasets
import transformers
IGNORE_INDEX = -100
logger = logging.getLogger('__name__')
PROMPT_TEMPLATE = (
"Below is an instruction that describes a task. "
"Write a response that appropriately completes the request.\n\n"
"### Instruction:\n{instruction}\n\n### Response: "
)
def buid_instruction_dataset(data_path, tokenizer, max_seq_length,
data_cache_dir=None, preprocessing_num_workers=None):
def tokenization(examples):
sources = []
targets = []
for instruction, input, output in zip(
examples['instruct'], examples['query'], examples['answer']
):
if input is not None and input != "":
instruction = instruction + '\n' + input
source = instruction
target = f"{tokenizer.bos_token}{output}{tokenizer.eos_token}"
sources.append(source)
targets.append(target)
tokenized_sources = tokenizer(sources, return_attention_mask=False, add_special_tokens=False)
tokenized_targets = tokenizer(targets, return_attention_mask=False, add_special_tokens=False)
all_input_ids = []
all_labels = []
for s, t in zip(tokenized_sources['input_ids'], tokenized_targets['input_ids']):
s = s + [tokenizer.gmask_token_id]
input_ids = torch.LongTensor(s + t)[:max_seq_length]
labels = torch.LongTensor([IGNORE_INDEX] * len(s) + t)[:max_seq_length]
assert len(input_ids) == len(labels)
all_input_ids.append(input_ids)
all_labels.append(labels)
return {'input_ids': all_input_ids, 'labels': all_labels}
all_datasets = []
if not isinstance(data_path, (list, tuple)):
data_path = [data_path]
for file in data_path:
if data_cache_dir is None:
data_cache_dir = str(os.path.dirname(file))
cache_path = os.path.join(data_cache_dir, os.path.basename(file).split('.')[0])
os.makedirs(cache_path, exist_ok=True)
try:
processed_dataset = datasets.load_from_disk(cache_path)
except Exception:
raw_dataset = load_dataset("json", data_files=file, cache_dir=cache_path)
tokenized_dataset = raw_dataset.map(
tokenization,
batched=True,
num_proc=preprocessing_num_workers,
remove_columns=["instruct", "query", "answer"],
desc="preprocessing on dataset",
)
processed_dataset = tokenized_dataset
processed_dataset.save_to_disk(cache_path)
processed_dataset.set_format('torch')
all_datasets.append(processed_dataset['train'])
return concatenate_datasets(all_datasets)2.4 DataCollator 实现
python
@dataclass
class DataCollatorForSupervisedDataset(object):
"""Collate examples for supervised fine-tuning."""
tokenizer: transformers.PreTrainedTokenizer
def __call__(self, instances):
input_ids = instances["input_ids"]
labels = instances["labels"]
input_ids = torch.nn.utils.rnn.pad_sequence(
input_ids, batch_first=True, padding_value=self.tokenizer.pad_token_id
)
labels = torch.nn.utils.rnn.pad_sequence(labels, batch_first=True, padding_value=-100)
return dict(input_ids=input_ids, labels=labels)3. input_ids 和 labels 如何构建?
3.1 构建示例
假设有样本:
- 输入:我爱北京天安门,你喜欢什么? → 分词后 token_id:
[12, 112, 122324, 22323, 23, 2346, 1233, 545] - 输出:我喜欢故宫 → token_id:
[12, 2346, 654]
构建结果如下:
| 序列 | 内容 |
|---|---|
| input_ids | [12, 112, 122324, 22323, 23, 2346, 1233, 545] + [1] + [12, 2346, 654] + [2] |
| labels | [-100, -100, -100, -100, -100, -100, -100, -100, 1, 12, 2346, 654, 2] |
其中
1为bos_token_id,2为eos_token_id。
3.2 关键说明
- 自回归预测机制:模型内部前向计算时已处理
input_ids = input_ids[:-1],labels = labels[1:],即根据上一个 token 预测下一个 token - IGNORE_INDEX = -100:在计算损失时不考虑标签为
-100的位置 - 填充处理:如设置了最大长度,
input_ids用pad_token_id填充,labels用-100填充
注意:部分模型的
pad_token可能为None,需自行设置(可与eos_token_id相同)。
3.3 ChatGLM 的特殊处理
ChatGLM 额外有 [gMASK] 标记,其输入构造为:
input_ids = instruction_ids + [gmask] + <sop> + output_ids + <eop>
labels = -100 * len(instruction_ids + 1) + <sop> + output_ids + <eop>4. Tokenization 如何构建?
在 test_tokenizer.py 中探索 tokenizer:
python
from transformers import AutoTokenizer, AutoModel
tokenizer = AutoTokenizer.from_pretrained("model_hub/chatglm-6b", trust_remote_code=True)
text = "我爱北京天安门"
print(tokenizer(text))
# 打印特殊 token
print("BOS token:", tokenizer.bos_token)
print("EOS token:", tokenizer.eos_token)
print("PAD token:", tokenizer.pad_token)
print("UNK token:", tokenizer.unk_token)
# 打印特殊 token_id
print("BOS token_id:", tokenizer.bos_token_id)
print("EOS token_id:", tokenizer.eos_token_id)
print("PAD token_id:", tokenizer.pad_token_id)
print("UNK token_id:", tokenizer.unk_token_id)
# 构建带特殊标记的输入
input_ids = tokenizer.build_inputs_with_special_tokens([1], [2])
print(input_ids)Tokenization 注意事项
| 注意事项 | 说明 |
|---|---|
| 特殊标记的使用 | 不同模型使用的特殊标记不同 |
| 额外输入 | 除 input_ids 和 labels 外,部分模型需额外输入 |
| Labels 转换 | 有的模型内部自动转换,有的需手动转换(如 CPM-Bee) |
5. 模型如何构建?
5.1 模型加载方式
| 模型 | 加载方式 | 说明 |
|---|---|---|
| 通用 | AutoTokenizer + AutoModelForCausalLM | 大多数模型适用 |
| LLaMA | LlamaForCausalLM + LlamaTokenizer | 专用加载方式 |
| ChatGLM | AutoTokenizer + AutoModel | 需设置 trust_remote_code=True |
5.2 ChatGLM 模型加载与对话
python
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)
trust_remote_code=True会根据映射找到真正使用的模型文件(如modeling_chatglm.py)。
6. 是否可以结合其他库使用?
指令微调过程中可结合以下库使用:
| 库 | 用途 |
|---|---|
| DeepSpeed | 分布式训练优化,降低显存占用 |
| Transformers | 模型加载与推理 |
| PEFT (LoRA) | 参数高效微调 |
| Datasets | 数据加载与并行处理 |
数据处理技巧:可将数据拆分为多个小文件放在文件夹下,遍历文件夹用
datasets加载并并行处理后保存到磁盘。如果发现数据处理有问题,需先删除已保存的缓存数据再重新处理。
SFT 之后还有对齐(Alignment)环节,即对模型输出进行规范,如使用奖励模型 + 基于人类反馈的强化学习(RLHF)等。