当前位置:   article > 正文

LLM - 数据处理之 Process Dataset For LLM With PT、SFT、RM_llm sft

llm sft

目录

一.引言

二.PT 数据流程

1.数据样式

2.生成代码

3.数据生成

三.SFT 数据流程

1.数据样式

2.生成代码

3.数据生成

四.RM 数据流程

1.生成逻辑

2.RM 模型测试

五.总结


一.引言

上篇文章 LLM - 批量加载 dataset 并合并介绍了如何加载多个文件并合成一个 dataset 数据集,本文基于上文生成的 dataset,生成后续任务所需的样式,这里任务分下面三个类型:

- pt 即 pretrain,预训练

- sft 即 Supervised fine tuning ,有监督的微调

- rm 即 reward model,奖励模型

Tips:

本文数据集与代码主要参考 Github LLaMA-Efficient-Tuning

二.PT 数据流程

pretrain 的目的是对模型预训练,使得模型具备基础的知识。这里我们可以把知识理解为记忆面包,统统一股脑喂给模型即可。

1.数据样式

以 wiki_demo.txt 为例,其中每一行信息就是我们上面说到的记忆面包的知识。PT 也是一种自学习的方法,就像之前版本给出的那样,PT 的样本 source 和 target 是一样的。

2.生成代码

  1. def preprocess_pretrain_dataset(examples: Dict[str, List[Any]]) -> Dict[str, Any]:
  2. # build grouped texts with format `X1 X2 X3 ...`
  3. if isinstance(getattr(tokenizer, "tokenizer", None), tiktoken.Encoding):
  4. token_kwargs = dict(allowed_special="all") # for tiktoken tokenizer (Qwen)
  5. else:
  6. token_kwargs = dict(add_special_tokens=True)
  7. if hasattr(tokenizer, "add_bos_token") and hasattr(tokenizer, "add_eos_token"):
  8. setattr(tokenizer, "add_bos_token", True) # for LLaMA tokenizer
  9. setattr(tokenizer, "add_eos_token", True)
  10. tokenized_examples = tokenizer(examples["instruction"], **token_kwargs)
  11. concatenated_examples = {k: list(chain(*tokenized_examples[k])) for k in tokenized_examples.keys()}
  12. total_length = len(concatenated_examples[list(concatenated_examples.keys())[0]])
  13. block_size = 512
  14. # we drop the small remainder, and if the total_length < block_size, we exclude this batch
  15. total_length = (total_length // block_size) * block_size
  16. # split by chunks of max_source_length
  17. result = {
  18. k: [t[i: i + block_size] for i in range(0, total_length, block_size)]
  19. for k, t in concatenated_examples.items()
  20. }
  21. return result

- examples: Dict[str, List[Any]]

examples 为我们加载文件数据得到的 dataset,这里默认 pretrain 要训练的知识存储在每条 json 的 instruction 字段中。

- tokenized_examples

通过 tokenizer 批量 tokenzier 文本能得到 input_ids,形式大概是 List[List] 嵌套。这里 Baichuan 模型可以不需要 bos_token_id 和 eos_token_id,LLaMA 需要 bos_token_id 和 eos_token_id。

- concatenated_examples

通过 chain 将 input_ids 与 attention_mask 都连在一起。

- result

将上面 chain 整合在一起的 input_ids 按照 block 为尺寸进行包装,并存储至 results 中。result 中的 input_ids 中每个 list 保持 block 的大小。 

 

3.数据生成

  1. import os.path
  2. from datasets import load_dataset, concatenate_datasets, interleave_datasets
  3. from typing import TYPE_CHECKING, Any, Dict, Generator, List, Literal, Union, Tuple
  4. from transformers import GPT2Tokenizer
  5. from itertools import chain
  6. import tiktoken
  7. tokenizer = GPT2Tokenizer.from_pretrained("gpt2")
  8. if __name__ == '__main__':
  9. # 多文件地址
  10. base_path = "/Users/LLaMA-Efficient-Tuning-main/data"
  11. data_files = ['alpaca_data_zh_51k.json', 'alpaca_gpt4_data_zh.json']
  12. strategy = 'concat'
  13. train_dataset = getBatchDataSet(base_path, data_files, strategy)
  14. kwargs = dict(
  15. num_proc=4,
  16. load_from_cache_file=False,
  17. desc="Running tokenizer on dataset"
  18. )
  19. stage = 'pt'
  20. if stage == "pt":
  21. train_dataset = train_dataset.filter(lambda example: example["instruction"])
  22. preprocess_function = preprocess_pretrain_dataset
  23. column_names = list(next(iter(train_dataset)).keys())
  24. dataset = train_dataset.select(range(1024)).map(
  25. preprocess_function,
  26. batched=True,
  27. remove_columns=column_names,
  28. **kwargs
  29. )
  30. limit = 5
  31. if stage == "pt":
  32. for source in dataset.select(range(limit))['input_ids']:
  33. print(len(source), source)

为了快速演示 demo,我们对原始数据集进行了 select 1024 的限制,并且在最后取 5 条数据打印,这里 tokenizer 为了便捷我们继续使用 GPT-2 进行测试:

这里 block_size = max_source_length = max_target_length = 512。

三.SFT 数据流程

1.数据样式

以 aplace_gpt4_data_zh.json 为例,SFT 有监督微调的数据以 QA 的形式提供,其中 instruction 可以作为 prompt 使用即 Q,如果 instrution 和 input 都有值,则逻辑会把二者拼接在一起作为统一个 Source。output 为 A ,在有监督微调的情况下,前面的 Prompt 会以 mask 的形式进行遮蔽,从而构造 label_ids。

 

2.生成代码

  1. def preprocess_supervised_dataset(examples: Dict[str, List[Any]]) -> Dict[str, Any]:
  2. # build inputs with format `<bos> X Y <eos>` and labels with format `<ignore> ... <ignore> Y <eos>`
  3. # for multi turn examples, we only mask the prompt part in each prompt-response pair.
  4. model_inputs = {"input_ids": [], "attention_mask": [], "labels": []}
  5. max_source_length = 512
  6. max_target_length = 512
  7. max_length = max_source_length + max_target_length
  8. r"""
  9. Encodes formatted inputs to pairs of token ids.
  10. Turn 0: bos + prefix + sep + query resp + eos
  11. Turn t: sep + bos + query resp + eos
  12. """
  13. # special tokens
  14. bos_ids = [tokenizer.bos_token_id]
  15. eos_ids = [tokenizer.eos_token_id]
  16. sep_ids = [tokenizer.sep_token_id]
  17. print("bos_id: %s eos_id: %s sep_id: %s" % (
  18. str(tokenizer.bos_token_id), str(tokenizer.eos_token_id), str(tokenizer.sep_token_id)))
  19. # 开始标记
  20. is_start = True
  21. # token 配置
  22. token_kwargs = dict(add_special_tokens=False)
  23. for query, response, history, system in construct_example(examples):
  24. input_ids, labels = [], []
  25. prefix = "{{system}}"
  26. prefix_ids = [] + tokenizer.encode(prefix, **kwargs)
  27. if is_start:
  28. if len(prefix_ids) != 0: # has prefix
  29. prefix_ids = bos_ids + prefix_ids + sep_ids
  30. else:
  31. prefix_ids = bos_ids
  32. # 重置
  33. is_start = False
  34. else:
  35. prefix_ids = sep_ids + bos_ids
  36. source_ids = prefix_ids + tokenizer.encode(query, **token_kwargs)
  37. target_ids = tokenizer.encode(response, **token_kwargs) + eos_ids
  38. if len(source_ids) > max_source_length:
  39. source_ids = source_ids[:max_source_length]
  40. if len(target_ids) > max_target_length:
  41. target_ids = target_ids[:max_target_length]
  42. if len(input_ids) + len(source_ids) + len(target_ids) > max_length:
  43. break
  44. source_mask = [IGNORE_INDEX] * len(source_ids)
  45. input_ids += source_ids + target_ids
  46. labels += source_mask + target_ids
  47. model_inputs["input_ids"].append(input_ids)
  48. model_inputs["attention_mask"].append([1] * len(input_ids))
  49. model_inputs["labels"].append(labels)
  50. return model_inputs

这里的生成代码相对 PT 较长一些,这里博主按块的形式分解:

- Init Model Input

input_ids 和对应的 labels 即为有监督微调的监督部分,其次定义了 max_source_length 和 max_target_length,这个数值和不同模型支持的最大长度也有关系。

- Special Token

定义特殊的分割 token_id,同时需要注意第一个样本开头包含 bos。如果包含自定义的 template 模板或者前缀,也可以结合 sep 添加到 souce 中。

- Encode Query And Response

根据是否 start 以及是否包含 prefix,获取 prefix_ids,常规情况下,prefix_ids 只包含一个 bos_token_id,后续的样本需要在  bos_token_id 前增加一个 sep_token_id。source_id 通过 prefix_ids 和 query [instruction] token 得到的 ids 拼接而成,target_ids 通过 response [output] token 得到的 ids 和 eos_ids 拼接而成。

- construct_example

上面生成 token_ids 使用 construcj_example 对原始数据做了第一道处理,可以看到对应的 key 的关系。同时也看到如果有 input 输入,query 会将 input 拼接到一起构成新的 query。由于这里我们并未用到 history 和 system [框架里对应模板],所以这两个 None 可以忽略。

- get Model Input

首先根据 init model input 的 max_length 相关定义对得到的 source_ids 和 target_ids 进行截断,并为 labels 构建 source_mask 将 source 部分遮蔽,其中 IGNORE_INDEX 为 -100。最终全部添加至 model_input 即可。

3.数据生成

  1. if __name__ == '__main__':
  2. # 多文件地址
  3. base_path = "/Users/LLaMA-Efficient-Tuning-main/data"
  4. data_files = ['alpaca_data_zh_51k.json', 'alpaca_gpt4_data_zh.json']
  5. strategy = 'concat'
  6. train_dataset = getBatchDataSet(base_path, data_files, strategy)
  7. kwargs = dict(
  8. num_proc=4,
  9. load_from_cache_file=False,
  10. desc="Running tokenizer on dataset"
  11. )
  12. stage = 'pt'
  13. if stage == "pt":
  14. train_dataset = train_dataset.filter(lambda example: example["instruction"])
  15. preprocess_function = preprocess_pretrain_dataset
  16. elif stage == 'sft':
  17. train_dataset = train_dataset.filter(lambda example: example["instruction"] and example["output"])
  18. preprocess_function = preprocess_supervised_dataset
  19. else:
  20. raise ValueError("UnKnown process stage")
  21. column_names = list(next(iter(train_dataset)).keys())
  22. dataset = train_dataset.select(range(1024)).map(
  23. preprocess_function,
  24. batched=True,
  25. remove_columns=column_names,
  26. **kwargs
  27. )
  28. limit = 5
  29. if stage == "pt":
  30. for source in dataset.select(range(limit))['input_ids']:
  31. print(len(source), source)
  32. else:
  33. for source, target in zip(dataset.select(range(limit))['input_ids'], dataset.select(range(5))['labels']):
  34. print(source, target)

添加 stage == 'sft' 的逻辑,下图为 GPT2-Tokenizer 对应的 special token:

 最终的样例数据:

四.RM 数据流程

1.生成逻辑

RM 模型用于针对给定的 QA 对进行评分,因此训练 RM 模型需要传给模型好的回复和不好的回复,这里我们主要看代码的逻辑,不再输出测试。

  1. def preprocess_pairwise_dataset(examples):
  2. # build input pairs with format `<bos> X`, `Y1 <eos>` and `Y2 <eos>`
  3. model_inputs = {"prompt_ids": [], "chosen_ids": [], "rejected_ids": []}
  4. for query, response, history, system in construct_example(examples):
  5. prompt_ids, chosen_ids = template.encode_oneturn(tokenizer, query, response[0], history, system)
  6. _, rejected_ids = template.encode_oneturn(tokenizer, query, response[1], history, system)
  7. if len(prompt_ids) > data_args.max_source_length:
  8. prompt_ids = prompt_ids[:data_args.max_source_length]
  9. if len(chosen_ids) > data_args.max_target_length:
  10. chosen_ids = chosen_ids[:data_args.max_target_length]
  11. if len(rejected_ids) > data_args.max_target_length:
  12. rejected_ids = rejected_ids[:data_args.max_target_length]
  13. model_inputs["prompt_ids"].append(prompt_ids)
  14. model_inputs["chosen_ids"].append(chosen_ids)
  15. model_inputs["rejected_ids"].append(rejected_ids)
  16. return model_inputs

和前面 sft 的逻辑比较相似,sft 只有 source 和 target,这里 prompt 相当于 source,chosen_ids 相当于 positive_target 的 token ids,rejected_ids 相当于 negative_target 的 token ids,最终全部添加至 model_inputs 即可。原始样本的格式我们虽然未给出,但是大家可以自行构建,只需要在 sft 的基础上增加 bad case 即可,不过这一步需要有正负情感的数据标注。

2.RM 模型测试

Ziya 类模型提供原生的 Reward Model,下面我们展示下其工作流程:

  1. import torch
  2. from transformers import AutoModelForSequenceClassification,LlamaTokenizer
  3. reward_model = AutoModelForSequenceClassification.from_pretrained("/data2/ziya/Ziya-LLaMA-7B-Reward", trust_remote_code=True)
  4. reward_model = reward_model.eval().half().cuda()
  5. lora_reward_model = AutoModelForSequenceClassification.from_pretrained("./MergeZiya", trust_remote_code=True)
  6. lora_reward_model = lora_reward_model.eval().half().cuda()
  7. tokenizer = LlamaTokenizer.from_pretrained("/data2/ziya/Ziya-LLaMA-7B-Reward",add_eos_token=True)
  8. prefix_user = "Human:"
  9. prefix_bot = "\n\nAssistant:"
  10. query = "列举一种空气污染。"
  11. response = "一种常见的空气污染源是化石燃料的燃烧产生的尾气排放,包括来自汽车、卡车、飞机、火车和工业厂房的废气排放。这会导致大气中的二>氧化硫、氮氧化物、一氧化碳、臭氧和颗粒物(例如灰尘和烟雾)等污染物含量增加,对人类健康和环境造成不利影响。"
  12. text = prefix_user + query + prefix_bot+response
  13. batch = tokenizer(text, return_tensors="pt", padding=True, truncation=True, max_length=1024)
  14. with torch.no_grad():
  15. reward = reward_model(batch['input_ids'].cuda(), attention_mask = batch['attention_mask'].cuda())
  16. print("ori:",reward.item())
  17. reward = lora_reward_model(batch['input_ids'].cuda(), attention_mask = batch['attention_mask'].cuda())
  18. print("lora:",reward.item())

最终会打印一个 0-1 之间的值代表二者的相关性。

五.总结

经过前面的 Dataset 生成与本文的 PreProcess 预处理,我们已经得到可训的 Dataset,后续我们继续熟悉框架代码,看看 trainer 如何构建,整个训练任务怎么进行。

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/码创造者/article/detail/835743
推荐阅读
相关标签
  

闽ICP备14008679号