Skip to content

英文大语言模型中文适配之指令微调

内容整理自学习笔记,仅供面试备考参考;不构成录用、培训或考试承诺。

1. 为什么需要对预训练模型进行指令微调?

在完成继续预训练之后,我们已了解从数据处理到训练、预测的整个流程。选择好大语言模型(如 ChatGLM、LLaMA、BLOOM 等)后,要使用它需要了解三个方面:

关键方面说明
输入数据的格式不同模型对输入数据的组织方式不同
Tokenization分词器的工作机制和特殊标记使用
模型的使用方式模型加载、前向计算、生成等

2. 指令微调数据如何处理?

2.1 数据处理思路

一般在模型的官方代码或开源项目中找到数据预处理部分,取一小部分数据集单独运行,查看输出结果。重点关注 input_ids 中的特殊标记和 labels 的构造方式。

例如 CPM-Bee 在 forward 中需要额外传入 spanlength,与一般模型只需 input_idslabels 不同。

2.2 指令数据格式

指令数据一般由三部分组成:

字段别名说明
instructioninstruct提示指令
inputquery输入文本(可为空)
outputanswer返回的结果

构造时一般将 instructioninput 拼接,最终对 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]

其中 1bos_token_id2eos_token_id

3.2 关键说明

  1. 自回归预测机制:模型内部前向计算时已处理 input_ids = input_ids[:-1]labels = labels[1:],即根据上一个 token 预测下一个 token
  2. IGNORE_INDEX = -100:在计算损失时不考虑标签为 -100 的位置
  3. 填充处理:如设置了最大长度,input_idspad_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_idslabels 外,部分模型需额外输入
Labels 转换有的模型内部自动转换,有的需手动转换(如 CPM-Bee)

5. 模型如何构建?

5.1 模型加载方式

模型加载方式说明
通用AutoTokenizer + AutoModelForCausalLM大多数模型适用
LLaMALlamaForCausalLM + LlamaTokenizer专用加载方式
ChatGLMAutoTokenizer + 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)等。