前面已經講過:
怎麼讓英文大語言模型支持中文?(一)構建中文tokenization
這裏是最後一部分了:怎麼讓英文大語言模型支持中文?(三)對預訓練模型進行指令微調。
代碼已上傳到github: chinese_llm_sft
Part1前言
在之前講過的繼續預訓練之後,我們應該對數據處理到訓練、預測的整個流程有所瞭解,其實,基本上過程是差不多的。我們在選擇好一個大語言模型之後。比如chatglm、llama、bloom等,要想使用它,得了解三個方面:輸入數據的格式、tokenization、模型的使用方式。接下來我們一一來看。本文主訓練代碼來自github:Chinese-LLaMA-Alpaca。
Part2數據
數據的輸入的話,一般情況下我們要在模型的官方代碼上找到數據輸入的那部分,或者說找到其它的一些開源的項目裏面關於數據預處理的部分。找一份小的數據集,將這部分單獨拿出來運行一下,看一下輸出是什麼。返回的結果是什麼。比如一般看一下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"),
}
我們在構造的時候最好想之前預訓練模型那樣構造樣本。
接下來再講講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。
Part3tokenization
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)之類的。可以根據自己的需要進行嘗試。
Part4模型
模型加載方式的話,一般使用的是AutoTenizer和AutoModelForCausalLM,但有的模型可能這麼加載會報錯。比如LLaMA的加載方式就是:LlamaForCausalLM和LlamaTokenizer,。針對於chatglm的話,加載方式爲:AutoTenizer和AutoModel,但需要注意的是其加載的時候設置了trust_remote_code=True,該參數會根據映射找到真正使用的模型文件,比如modeling_chatglm.py。下載好模型權重後,我們可以根據情況先看看效果,在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)
Part5其它
其它的一些就是結合一些庫的使用了,比如:
deepspeed transformers peft中使用的lora datasets加載數據
需要注意的是, 我們可以把數據拆分爲很多小文件放在一個文件夾下,然後遍歷文件夾裏面的數據,用datasets加載數據並進行並行處理後保存到磁盤上。如果中間發現處理數據有問題的話要先刪除掉保存的處理後的數據,再重新進行處理,否則的話就是直接加載保存的處理好的數據。
在SFT之後其實應該還有對齊這部分,就是對模型的輸出進行規範,比如使用獎勵模型+基於人類反饋的強化學習等,這裏就不作展開了。
最後,接下來的話終於要開始去好好了解下langchain了,一直都在關注這個但沒有好好地看下。
Part6參考
https://github.com/ymcui/Chinese-LLaMA-Alpaca