怎麼讓英文大語言模型支持中文?(三)進行指令微調

前面已經講過:

怎麼讓英文大語言模型支持中文?(一)構建中文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([180601224714949]))
print(tokenizer.decode([180601224714949]))

# 打印特殊 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,  83049130005]))

# 這個是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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章