更新时间:2024-09-14 GMT+08:00
分享

训练的数据集预处理说明

llama2-13b举例,使用训练作业运行:0_pl_pretrain_13b.sh训练脚本后,脚本检查是否已经完成数据集预处理

如果已完成数据集预处理,则直接执行预训练任务。如果未进行数据集预处理,则会自动执行 scripts/llama2/1_preprocess_data.sh

预训练数据集预处理参数说明

预训练数据集预处理脚本 scripts/llama2/1_preprocess_data.sh 中的具体参数如下:

  • --input:原始数据集的存放路径。
  • --output-prefix:处理后的数据集保存路径+数据集名称(例如:alpaca_gpt4_data)。
  • --tokenizer-type:tokenizer的类型,可选项有['BertWordPieceLowerCase','BertWordPieceCase','GPT2BPETokenizer','PretrainedFromHF'],一般为PretrainedFromHF。
  • --tokenizer-name-or-path:tokenizer的存放路径,与HF权重存放在一个文件夹下。
  • --seq-length:要处理的最大seq length。
  • --workers:设置数据处理使用执行卡数量 / 启动的工作进程数。
  • --log-interval:是一个用于设置日志输出间隔的参数,表示输出日志的频率。在训练大规模模型时,可以通过设置这个参数来控制日志的输出。

输出数据预处理结果路径:

训练完成后,以 llama2-13b 为例,输出数据路径为:/home/ma-user/work/llm_train/processed_for_input/llama2-13b/data/pretrain/

微调数据集预处理参数说明

微调包含SFT和LoRA微调。数据集预处理脚本参数说明如下:

  • --input:原始数据集的存放路径。支持 .parquet \ .csv \ .json \ .jsonl \ .txt \ .arrow 格式。
  • --output-prefix:处理后的数据集保存路径+数据集名称(例如:alpaca_gpt4_data)
  • --tokenizer-type:tokenizer的类型,可选项有['BertWordPieceLowerCase','BertWordPieceCase','GPT2BPETokenizer','PretrainedFromHF'],一般为PretrainedFromHF。
  • --tokenizer-name-or-path:tokenizer的存放路径,与HF权重存放在一个文件夹下。
  • --handler-name:生成数据集的用途,这里是生成的指令数据集,用于微调。
    • GeneralPretrainHandler:默认。用于预训练时的数据预处理过程中,将数据集根据key值进行简单的过滤。
    • GeneralInstructionHandler:用于sft、lora微调时的数据预处理过程中,会对数据集full_prompt中的user_prompt进行mask操作。
  • --seq-length:要处理的最大seq length。
  • --workers:设置数据处理使用执行卡数量 / 启动的工作进程数。
  • --log-interval:是一个用于设置日志输出间隔的参数,表示输出日志的频率。在训练大规模模型时,可以通过设置这个参数来控制日志的输出。

输出数据预处理结果路径:

训练完成后,以llama2-13b为例,输出数据路径为:/home/ma-user/work/llm_train/processed_for_input/llama2-13b/data/fintune/

handler-name参数说明

数据集预处理中 --handler-name 都会传递参数,用于构建实际处理数据的hanler对象,并根据handler对象对数据集进行解析。文件路径在:ModelLink/modellink/data/data_handler.py。

  • 基类BaseDatasetHandler解析

    data_handler的基类是BaseDatasetHandler,其核心函数是serialize_to_disk:

    def serialize_to_disk(self):        
        """save idx and bin to disk"""        
        startup_start = time.time()
        if not self.tokenized_dataset:
            self.tokenized_dataset = self.get_tokenized_data()
        output_bin_files = {}
        output_idx_files = {}
        builders = {}
        level = "document"
        if self.args.split_sentences: 
           level = "sentence"
        logger.info("Vocab size: %s", self.tokenizer.vocab_size)
        logger.info("Output prefix: %s", self.args.output_prefix)
        for key in self.args.json_keys:
            ## 写入磁盘
    • 先调用self.get_tokenized_data()对数据集进行encode
    • self.get_tokenized_data()中调用self._filter方法处理每一个sample
    • self._filter在基类中未定义,需要各个子类针对目标数据集格式进行实现

    所有handler依据实际数据集实现self._filter方法,处理原始数据集中的单一sample,其余方法复用基类的实现。

  • GeneralPretrainHandler解析

    GeneralPretrainHandler是处理预训练数据集的一个类,继承自BaseDatasetHandler,实现对alpaca格式预训练数据集的处理。

    def _filter(self, sample):
        sample = self._pre_process(sample)
        for key in self.args.json_keys:
            text = sample[key]
            doc_ids = []
            for sentence in self.splitter.tokenize(text):
                if len(sentence) > 0:
                    sentence_ids = self._tokenize(sentence)
                    doc_ids.append(sentence_ids)
            if len(doc_ids) > 0 and self.args.append_eod:
                doc_ids[-1]['input_ids'].append(self.tokenizer.eod)
                doc_ids[-1]['attention_mask'].append(1)
                doc_ids[-1]['labels'].append(self.tokenizer.eod)
            sample[key] = doc_ids
            # for now, only input_ids are saved
            sample[key] = list(map(lambda x: x['input_ids'], sample[key]))
        return sample
    • 支持的是预训练数据风格,会根据参数args.json_keys的设置,从数据集中找到对应关键字的文本内容。例如本案例中提供的 train-00000-of-00001-a09b74b3ef9c3b56.parquet 预训练数据集的关键字为“text”,格式如下:
      [
         {"text": "document"},
         {"other keys": "optional content"}
      ]
    • 训练数据构造:在 _filter 函数中会根据关键字将内容提取后,直接使用模型特定的tokenizer分词器,将对应关键字中的内容进行预处理。最后实际用于训练的内容如下:
      doc_ids[-1]['input_ids'].append(self.tokenizer.eod)
    • 推理prompt构造:由于 GeneralPretrainHandler 中未定义 prompt 格式用于训练,因此在推理时可直接自定义 prompt 内容用于推理。
  • GeneralInstructionHandler解析

    GeneralInstructionHandler是处理微调数据集的一个基本类,继承自BaseDatasetHandler,实现对alpaca格式微调数据集的处理。

    def _filter(self, sample):
        messages = self._format_msg(sample)
        full_prompt = self.prompter.generate_training_prompt(messages)
        tokenized_full_prompt = self._tokenize(full_prompt)
        if self.args.append_eod:
            tokenized_full_prompt["input_ids"].append(self.tokenizer.eod)
            tokenized_full_prompt["attention_mask"].append(1)
            tokenized_full_prompt["labels"].append(self.tokenizer.eod)
        if not self.train_on_inputs:
            user_prompt = full_prompt.rsplit(self.prompter.template.assistant_token, maxsplit=1)[0] + \
                self.prompter.template.assistant_token + "\n"
            tokenized_user_prompt = self._tokenize(user_prompt)
            user_prompt_len = len(tokenized_user_prompt["input_ids"])
            tokenized_full_prompt["labels"][:user_prompt_len] = [self.ignored_label] * user_prompt_len
        for key in self.args.json_keys:
            tokenized_full_prompt[key] = [tokenized_full_prompt[key]]
        return tokenized_full_prompt
    • 本案例中 alpaca_gpt4_data.json 数据集包含有以下字段:
      • instruction:描述模型应执行的任务。指令中的每一条都是唯一的。
      • input:任务的可选上下文或输入。instruction 对应的内容会与 input 对应的内容拼接后作为指令,即指令为 instruction\ninput。
      • output:生成的指令的答案。
      [
       {
           "instruction": "指令(必填)",
           "input": "输入(选填)",
           "output": "模型回答(必填)",
       } 
      ]
    • 训练数据构造:在 _filter 函数中会使用 Alpaca 微调指令的模板 self.prompter 将数据集中 instruction、input、output 关键字的内容进行拼接,并用于训练。拼接方式如下,其中 {instruction}、{input}、{output} 分别对应数据集中 instruction、input、output 关键字的内容。
      "Below is an instruction that describes a task, paired with an input that provides further context. \n Write a response that appropriately completes the request. \n Please note that you need to think through your response logically and step by step."
      
      "### Instruction:"
      {instruction} + "\n" + {input}
      
      "### Response:"
      {output}
    • 推理prompt构造:通过微调训练后进行推理时,同样需要根据训练时的prompt模板来构造prompt内容。prompt拼接格式如下,其中 {instruction} 为用户推理测试时输入的内容。
      "Below is an instruction that describes a task, paired with an input that provides further context. \n Write a response that appropriately completes the request. \n Please note that you need to think through your response logically and step by step."
      
      "### Instruction:"
      {instruction}
      
      "### Response:"

  • MOSSMultiTurnHandler解析
    MOSSMultiTurnHandler是处理微调数据集的一个类,继承自GeneralInstructionHandler,实现对moss格式微调数据集的处理。
    def _filter(self, sample):
        input_ids, labels = [], []
        for turn in sample["chat"].values():
           if not turn:
               continue
           user = turn["Human"].replace("<eoh>", "").replace("<|Human|>: ", "").strip()
           assistant = turn["MOSS"].replace("<|MOSS|>:", "").replace("<eom>", "").strip()
           user_ids = self._unwrapped_tokenizer.encode(user) 
           assistant_ids = self._unwrapped_tokenizer.encode(assistant)
           input_ids += self.user_token + user_ids + self.assistant_token + assistant_ids
           labels += [self._unwrapped_tokenizer.eos_token_id] + self.ignored_index * len(user_ids) + self.ignored_index + assistant_ids
        input_ids.append(self._unwrapped_tokenizer.eos_token_id)
        labels.append(self._unwrapped_tokenizer.eos_token_id)
        attention_mask = [1 for _ in range(len(input_ids))]
        return {            
            "input_ids": [input_ids],            
            "attention_mask": [attention_mask],            
            "labels": [labels]        
        }
    1. moss原始数据集是一个多轮对话的jsonl,filter的输入就是其中的一行
    2. 循环处理其中的单轮对话
    3. 在单轮对话中
      1. 对user和assiant的文本进行清洗
      2. 分别encode处理后的文本,获得对应的token序列,user_ids和assiantant_ids
      3. input_ids是user_ids和assiantant_ids的拼接
      4. labels与input_ids对应,用-100替换user_ids的token,只保留assiantant_ids
    4. attention_mask是和input_ids等长的全1序列
    5. 返回input_ids\attention_mask\labels的字典
    6. 处理完单一sample

    注:labels中用-100填充的地方,表示会被loss_mask给mask掉

    • 训练数据构造:在 _filter 函数中会读取 MOSS 数据集的“Human”和“MOSS”字段的文本内容,并将内容中"<|Human|>: "、"<|MOSS|>:"、"<eom>"字符串去除。随后将“Human”和“MOSS”的文本内容进行拼接,拼接方式如下,其中 {Human}、{MOSS}分别对应Human、MOSS关键字的内容。
      user_token = [195]
      assistant_token = [196]
      input_ids = user_token + {Human} + assistant_token + {MOSS}
    • 推理prompt构造:MOSSMultiTurnHandler 中未定义 prompt 格式用于训练,因此在推理时可直接自定义 prompt 内容用于推理。

  • MOSSInstructionHandler解析
    def _filter(self, sample):
        messages = []
        tokenized_chats = []
        for turn in sample["chat"].values():
            if not turn:
                continue
            user = turn["Human"].replace("<eoh>", "").replace("<|Human|>: ", "").strip()
            assistant = turn["MOSS"].replace("<|MOSS|>:", "").replace("<eom>", "").strip()
            messages.append(dict(role=self.prompter.user_role, content=user))
            messages.append(dict(role=self.prompter.assistant_role, content=assistant))
            full_prompt = self.prompter.generate_training_prompt(messages)
            tokenized_full_prompt = self._tokenize(full_prompt)
            if not self.train_on_inputs:
                user_prompt = full_prompt.rsplit(self.prompter.template.assistant_token, maxsplit=1)[0] + \
                              self.prompter.template.assistant_token + "\n"
                tokenized_user_prompt = self._tokenize(user_prompt)
                user_prompt_len = len(tokenized_user_prompt["input_ids"])
                tokenized_full_prompt["labels"] = [-100] * user_prompt_len + tokenized_full_prompt["labels"][user_prompt_len:]
            tokenized_chats.append(tokenized_full_prompt)
        for key in self.args.json_keys:
            sample[key] = [chat[key] for chat in tokenized_chats]
        return sample
    • 训练数据构造:在 _filter 函数中同样会读取 MOSS 数据集的“Human”和“MOSS”字段的文本内容,并将内容中"<|Human|>: "、"<|MOSS|>:"、"<eom>"字符串去除。随后将“Human”和“MOSS”的文本内容进行拼接,拼接方式如下,其中 {Human}、{MOSS}分别对应Human、MOSS关键字的内容。
      "Below is an instruction that describes a task, paired with an input that provides further context. \n Write a response that appropriately completes the request. \n Please note that you need to think through your response logically and step by step."
      
      "### Instruction:"
      {Human}
      
      "### Response:"
      {MOSS}
    • 推理prompt构造:通过微调训练后进行推理时,同样需要根据训练时的prompt模板来构造prompt内容。prompt拼接格式如下,其中 {Human} 为用户推理测试时输入的内容。
      "Below is an instruction that describes a task, paired with an input that provides further context. \n Write a response that appropriately completes the request. \n Please note that you need to think through your response logically and step by step."
      
      "### Instruction:"
      {Human}
      
      "### Response:"

  • 自定义handler

    参考MOSSMultiTurnHandler的实现,继承想要的通用的父类,实现_filter方法,然后在数据预处理的参数里指定自己的handler名称即可

用户自定义执行数据处理脚本修改参数说明

如果用户要自定义数据处理脚本并且单独执行,同样以llama2为例。

  • 方法一:用户可打开scripts/llama2/1_preprocess_data.sh脚本,将执行的python命令复制下来,修改环境变量的值。在Notebook进入到 /home/ma-user/work/llm_train/AscendSpeed/ModelLink 路径中,再执行python命令。
  • 方法二:用户在Notebook中直接编辑scripts/llama2/1_preprocess_data.sh脚本,自定义环境变量的值,并在脚本的首行中添加 cd /home/ma-user/work/llm_train/AscendSpeed/ModelLink 命令,随后在Notebook中运行该脚本。

其中环境变量详细介绍如下:

表1 数据预处理中的环境变量

环境变量

示例

参数说明

RUN_TYPE

pretrain、sft、lora

数据预处理区分:

预训练场景下数据预处理,默认参数:pretrain

微调场景下数据预处理,默认:sft / lora

ORIGINAL_TRAIN_DATA_PATH

/home/ma-user/work/training_data/finetune/moss_LossCompare.jsonl

原始数据集的存放路径。

TOKENIZER_PATH

/home/ma-user/work/model/llama-2-13b-chat-hf

tokenizer的存放路径,与HF权重存放在一个文件夹下。请根据实际规划修改。

PROCESSED_DATA_PREFIX

/home/ma-user/work/llm_train/processed_for_input/llama2-13b/data/pretrain/alpaca

处理后的数据集保存路径+数据集前缀。

TOKENIZER_TYPE

PretrainedFromHF

可选项有:['BertWordPieceLowerCase','BertWordPieceCase','GPT2BPETokenizer','PretrainedFromHF'],一般为 PretrainedFromHF 。

SEQ_LEN

4096

要处理的最大seq length。脚本会检测超出SEQ_LEN长度的数据,并打印log。

相关文档