节前,我们组织了一场算法岗技术&面试讨论会,邀请了一些互联网大厂朋友、参加社招和校招面试的同学,针对大模型技术趋势、大模型落地项目经验分享、新手如何入门算法岗、该如何备战、面试常考点分享等热门话题进行了深入的讨论。
合集在这里:《大模型面试宝典》(2024版) 正式发布!
上篇文章讲了怎么利用预训练让英文大语言模型支持中文,详情:大模型面试准备(十二):怎样利用预训练方法让英文大语言模型可以很好的支持中文?
本篇文章再来介绍下对中文进行指令微调。喜欢本文记得收藏、关注、点赞。文末提供技术交流群,欢迎围观。
一、为什么要进行指令微调
上篇讲过的继续预训练之后,我们应该对数据处理到训练、预测的整个流程有所了解,其实,指令微调的过程基本上也是差不多的。
指令微调也叫有监督微调(SFT),是使用高质量的任务相关的数据来训练模型,使得模型具备指令理解能力和上下文理解能力,进而使得模型的输出更符合任务需求或者人类偏好,比如使得模型能够更好的完成开放领域问题、阅读理解、翻译、生成代码等
我们在选择好一个要进行指令微调的大语言模型之后,比如chatglm、llama、bloom等,要想使用它,得了解三个方面:输入数据的格式、tokenization、模型的使用方式,接下来我们逐一来看。
二、指令微调数据如何处理
一般情况下我们要在模型的官方代码上找到数据输入的那部分,或者说找到其它的一些开源的项目里面关于数据预处理的部分。这里找一份小的数据集,将这部分单独拿出来运行一下,看一下输出是什么,返回的结果是什么。
比如一般看一下input_ids里面的特殊标记,labels是怎么构造的。举个例子,cpm-bee在forward里面需要额外传入span和length,与一般的只需要传入input_ids和labels不同。
这里我们看下chatglm的数据格式是怎么样的,在test_dataset.py里面:
import logging
import os
from dataclasses import dataclass
from typing import Optional, Dict, Sequence, Union, List
import datasets
import torch
import logging
from datasets import load_dataset, concatenate_datasets
import copy
import transformers
import random
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: Union[List[str],str],
tokenizer: transformers.PreTrainedTokenizer,
max_seq_length: int, data_cache_dir = None,
preprocessing_num_workers = None,
):
def tokenization(examples):
sources = []
targets = []
# prompt = PROMPT_TEMPLATE
for instruction, input, output in zip(examples['instruct'],examples['query'],examples['answer']):
if input is not None and input !="":
instruction = instruction+'\n'+input
# source = prompt.format_map({'instruction': instruction})
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)
print(tokenized_targets)
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)
results = {'input_ids':all_input_ids, 'labels': all_labels}
return results
logging.warning("building dataset...")
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)
logger.info(f'training datasets-{file} has been loaded from disk')
except Exception:
print(file)
raw_dataset = load_dataset("json", data_files=file, cache_dir=cache_path)
print(raw_dataset)
tokenization_func = tokenization
tokenized_dataset = raw_dataset.map(
tokenization_func,
batched=True,
num_proc=preprocessing_num_workers,
remove_columns=["instruct","query","answer"],
keep_in_memory=False,
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'])
all_datasets = concatenate_datasets(all_datasets)
return all_datasets
@dataclass
class DataCollatorForSupervisedDataset(object):
"""Collate examples for supervised fine-tuning."""
tokenizer: transformers.PreTrainedTokenizer
def __call__(self, instances: Sequence[Dict]) -> Dict[str, torch.Tensor]:
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,
)
if __name__ == "__main__":
from transformers import AutoModelForCausalLM, AutoTokenizer
tokenizer = AutoTokenizer.from_pretrained("model_hub/chatglm-6b", trust_remote_code=True)
all_datasets = buid_instruction_dataset(["data/msra/train.txt"], tokenizer, max_seq_length=256)
print(all_datasets[0])
data_collator = DataCollatorForSupervisedDataset(tokenizer=tokenizer)
data = data_collator(all_datasets[:2])
print(data)
指令数据一般由三部分组成:instruction(instruct)、input(query)、output(answer),分别表示提示指令、文本、返回的结果。构造的时候一般是instruction和input进行拼接,当然input可能是为空的,最终对output进行预测。需要注意的是,除了instruction之外,可能还有特殊的prompt,不同模型的prompt是不一样的,比如:
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"),
}
本文使用data/msra/train.txt里面数据的格式,一行为一条样本(一个{}内),样本示例如下:
{
"instruct": "你现在是一个实体识别模型,你需要提取文本里面的人名、地名、机构名,如果存在结果,返回'实体_实体类型',不同实体间用\n分隔。如果没有结果,回答'没有'。",
"query": "文本:一位郑州学人说,越秀学术讲座对郑州学界而言堪称功德之举。",
"answer": "郑州_地名\n越秀_机构名"
}
我们在构造的时候最好像之前预训练模型那样构造样本。
接下来再讲讲input_ids和labels。
假设我们现在有样本:我爱北京天安门,你喜欢什么?分词之后得到[“我”, “爱”, “北京”, “天安门”, “你”, “喜欢”, “什么”, “?”],之后转换为token_id,[12, 112, 122324, 22323, 23, 2346, 1233, 545]。
假设output为:我喜欢故宫。转换为token_id:[12, 2346, 654],一般情况下,output前后会被标识,比如bos_token_id和eos_token_id,假设分别为1和2,那么我们样本的输入就是:[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],长度和input_ids保持一致。
有人可能会疑惑,不是说是根据上一个字预测下一个字吗? 怎么是自己预测自己。这是因为一般的模型内部在前向计算的时候已经帮我们处理了:input_ids = input_ids[-1] labels=labels[1:]。
-100是表示在计算损失的时候不考虑标签为-100的位置。如果还设置了文本最大长度,则input_ids后面用pad_token_id进行填充,需要注意可能有的模型的tokenization中pad_token为None,需要自己去设置一个,可以和eos_token_id一样。而标签需要用-100进行填充。
针对于chatglm,除了上述说明的外,它还有一个额外的[gMASK]标记。而它的输入为:
# instruction为instruction + input
# [gmask]等标记转换为id,这里直接展示
input_ids = instruction_ids + [gmask] + <sop> + output_ids + <eop>
# +1是[gmask]
-100 * len(instruction_ids + 1) + <sop> + output_ids + <eop>
所以说不同模型的输入构造可能不大一样,需要注意:
-
特殊标记的使用;
-
除了input_ids和labels,是否需要额外的输入;
-
有的模型内部是帮你自动转换labels和input_ids计算损失,有的没有转换,可能需要自己手动转换,比如cpm-bee。
三、指令微调tokenization如何构建
Tokenization也很重要,我们一般可以先探索一下,在test_tokenizer.py中:
from transformers import AutoTokenizer, AutoModel
tokenizer = AutoTokenizer.from_pretrained("model_hub/chatglm-6b", trust_remote_code=True)
text = "我爱北京天安门"
print(tokenizer(text))
print(tokenizer.convert_ids_to_tokens([18060, 12247, 14949]))
print(tokenizer.decode([18060, 12247, 14949]))
# 打印特殊 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: ", tokenizer.bos_token_id)
print("EOS token: ", tokenizer.eos_token_id)
print("PAD token: ", tokenizer.pad_token_id)
print("UNK token: ", tokenizer.unk_token_id)
print(tokenizer.decode([130004,
67470, 24, 83049, 4, 76699, 24, 83049, 4, 67357,
65065, 24, 83049, 4, 64484, 68137, 63940, 24, 64539,
63972, 4, 69670, 72232, 69023, 24, 83049, 4, 64372,
64149, 24, 83049, 4, 63855, 24, 83049, 130005]))
# 这个是chatglm特有的。
input_ids = tokenizer.build_inputs_with_special_tokens([1], [2])
print(input_ids)
我们要注意看一下特殊标记是否为空,其它的话一些编码、解码、分词、tokenizer(文本)返回什么(input_ids、attention_mask)之类的,可以根据自己的需要进行尝试。
四、指令微调模型如何构建
模型构建方式的话,一般使用的是AutoTenizer和AutoModelForCausalLM,但有的模型可能这么加载会报错。比如LLaMA的加载方式就是:LlamaForCausalLM和LlamaTokenizer。针对于ChatGLM的话,加载方式为:AutoTenizer和AutoModel,但需要注意的是其加载的时候设置了trust_remote_code=True,该参数会根据映射找到真正使用的模型文件。下载好模型权重后,我们可以根据情况先看看效果,在test_model.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)
五、指令微调训练
现在我们可以进行指令微调训练了,训练代码在run_clm_sft_with_peft.py里,训练脚本如下(具体一些参数含义可以查看上一篇文章):
torchrun --nnodes 1 --nproc_per_node 1 run_clm_sft_with_peft.py \
--deepspeed ds_zero2_no_offoad.json \
--model_name_or_path model_hub/chatglm-6b \
--tokenizer_name_or_path model_hub/chatglm-6b \
--dataset_dir data/msra/ \
--per_device_train_batch_size 8 \
--per_device_eval_batch_size 8 \
--do_train \
--seed $RANDOM \
--fp16 \
--num_train_epochs 3 \
--learning_rate 3e-5 \
--warmup_ratio 0.01 \
--weight_decay 0 \
--logging_strategy steps \
--logging_steps 10 \
--save_strategy steps \
--save_total_limit 3 \
--save_steps 200 \
--gradient_accumulation_steps 1 \
--preprocessing_num_workers 8 \
--max_seq_length 256 \
--output_dir output_dir \
--overwrite_output_dir \
--ddp_timeout 30000 \
--logging_first_step True \
--lora_rank 8 \
--lora_alpha 32 \
--trainable query_key_value \
--lora_dropout 0.05 \
--torch_dtype float16 \
--gradient_checkpointing \
--ddp_find_unused_parameters False
六、使用指令微调模型进行预测
训练完成后可以使用test_sft_model.py进行预测:
import os
import torch
from transformers import AutoTokenizer, AutoModel
from peft import PeftModel
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()
model_vocab_size = model.get_output_embeddings().weight.size(0)
model.resize_token_embeddings(len(tokenizer))
model = PeftModel.from_pretrained(model, os.path.join("output_dir", "adapter_model"))
model.cuda()
model.eval()
response, history = model.chat(tokenizer, "你好", history=[])
print(response)
response, history = model.chat(tokenizer, "晚上睡不着应该怎么办", history=[])
print(response)
response, history = model.chat(tokenizer, "你现在是一个实体识别模型,你需要提取文本里面的人名、地名、机构名,如果存在结果,返回'实体_实体类型',不同实体间用\n分隔。如果没有结果,回答'没有'。文本:我们是受到郑振铎先生、阿英先生著作的启示,从个人条件出发,瞄准现代出版史研究的空白,重点集藏解放区、国民党毁禁出版物。", history=[])
print(response)
七、其他
其它一些,比如lora的可训练的层怎么定义,可以使用fin_lora_names.py进行查看。
另外在SFT之后其实应该还有对齐这部分,就是对模型的输出进行规范,比如使用奖励模型+基于人类反馈的强化学习等,这里就不作展开了。
技术交流群
前沿技术资讯、算法交流、求职内推、算法竞赛、面试交流(校招、社招、实习)等、与 10000+来自港科大、北大、清华、中科院、CMU、腾讯、百度等名校名企开发者互动交流~
我们建了大模型算法岗技术与面试交流群, 想要进交流群、需要源码&资料、提升技术的同学,可以直接加微信号:mlc2040。加的时候备注一下:研究方向 +学校/公司+CSDN,即可。然后就可以拉你进群了。
方式①、微信搜索公众号:机器学习社区,后台回复:加群
方式②、添加微信号:mlc2040,备注:技术交流
用通俗易懂方式讲解系列
- 《大模型面试宝典》(2024版) 正式发布!
- 《大模型实战宝典》(2024版)正式发布!
- 大模型面试准备(一):LLM主流结构和训练目标、构建流程
- 大模型面试准备(二):LLM容易被忽略的Tokenizer与Embedding
- 大模型面试准备(三):聊一聊大模型的幻觉问题
- 大模型面试准备(四):大模型面试必会的位置编码
- 大模型面试准备(五):图解 Transformer 最关键模块 MHA
- 大模型面试准备(六):一文讲透生成式预训练模型 GPT、GPT2、GPT3
- 大模型面试准备(七):ChatGPT 的内核 InstructGPT 详细解读
- 大模型面试准备(八):一文详解国产大模型导师 LLaMA v1和v2
- 大模型面试准备(九):简单透彻理解MoE
- 大模型面试准备(十):大模型数据处理方法及优秀的开源数据介绍
- 大模型面试准备(十一):怎样让英文大语言模型可以很好的支持中文?
- 大模型面试准备(十二):怎样利用预训练方法让英文大语言模型可以很好的支持中文?
参考链接:
https://github.com/taishan1994/chinese_llm_sft。