当前位置:   article > 正文

LLM - DataCollatorForLanguageModeling 样本生成 by transformers

datacollatorforlanguagemodeling

目录

一.引言

二.生成样本 By API

1.样本处理样式

2.DataCollatorForLanguageModeling

2.1 样本准备

2.2 API 生成

三.生成样本 By DIY

1.样本准备

2.data_colloator 实现

3.使用自定义 data_colloator

四.总结


一.引言

前面我们讲了 Baichuan7B 的 lora 微调步骤,我们在 QA 基础上构建了样本集并训练,但是细心的同学肯定发现我们原始样本中只给出了 input_ids 但是没有给出 labels,本文我们简单看下 data_collator 如何生成样本 label 并自定义实现一个简单的 data_collator。

二.生成样本 By API

1.样本处理样式

  1. def preprocess(tokenizer, config, example, max_seq_length, prompt_key, target_key):
  2. prompt = example[prompt_key]
  3. target = example[target_key]
  4. prompt_ids = tokenizer.encode(prompt, max_length=max_seq_length, truncation=True)
  5. target_ids = tokenizer.encode(target, max_length=max_seq_length, truncation=True, add_special_tokens=False)
  6. # 最终还是将 instruction 的输入输出都拼在一起,使用经典的 causal-LM 的 next word prediction 方式来训练
  7. input_ids = prompt_ids + target_ids + [config.eos_token_id] # EOS 用于标识句子结束
  8. return {"input_ids": input_ids, "seq_len": len(prompt_ids)}

首先加载 tokenizer 对 Q 和 A 分别 token 获取对应 Q_ids 和 A_ids:

  1. Q: 请计算:39 * 0 = 什么?
  2. A: 这是简单的乘法运算,39乘以0得到的是0
  3. TokenQ: [31106, 15875, 77, 57, 53, 31159, 53, 60, 58, 31135, 6787, 6775, 81]
  4. TokenA: [31106, 3908, 14313, 31640, 31257, 31481, 31742, 72, 57, 53, 31640, 31187, 53, 60, 58, 9323, 31178, 52, 79, 54, 59, 56]

获取 ids 后,会直接将 Q_ids A_ids 连接并在尾部增加 eos_token_id 标识当前句子结束,这里 eos_token_id = 2,合并后的 input_ids 如下:

  1. input_ids = [31106, 15875, 77, 57, 53, 31159, 53, 60, 58, 31135, 6787, 6775, 81, 31106, 3908, 14313, 31640, 31257, 31481,
  2. 31742, 72, 57, 53, 31640, 31187, 53, 60, 58, 9323, 31178, 52, 79, 54, 59, 56, 2]

除此之外,json 里还用 seq_len 记录了 prompt_ids 即 Q_ids 的长度:

json = {"input_ids": input_ids, "seq_len": 13}

2.DataCollatorForLanguageModeling

data_collator = DataCollatorForLanguageModeling(tokenizer=tokenizer,mlm=False)
  1. # 初始化 trainer, 此处报错: NotImplementedError: Cannot copy out of meta tensor; no data!
  2. trainer = ModifiedTrainer(
  3. model=model,
  4. train_dataset=dataset,
  5. args=training_args,
  6. callbacks=[TensorBoardCallback(writer)], # 回调将训练情况写入 tensorboard
  7. data_collator=data_collator,
  8. )
  9. trainer.train()

上文中我们把 json 样本生成的 Dataset 和 DataCollatorForLanguageModeling 直接传给了 Trainer 训练,但是样本中并没有 labels,于是就在想没有 labels 怎么梯度回传,下面用 DataCollatorForLanguageModeling API 看下 data_collator 使用后生成的数据样式。

2.1 样本准备

这里我直接将第一步 tokenizer 后的样本手动生成两个 json 测试后续流程:

  1. # p: [31106, 15875, 77, 57, 53, 31159, 53, 60, 58, 31135, 6787, 6775, 81]
  2. # t: [31106, 3908, 14313, 31640, 31257, 31481, 31742, 72, 57, 53, 31640, 31187, 53, 60, 58, 9323, 31178, 52, 79, 54, 59, 56]
  3. sample1 = [31106, 15875, 77, 57, 53, 31159, 53, 60, 58, 31135, 6787, 6775, 81, 31106, 3908, 14313, 31640, 31257, 31481,
  4. 31742, 72, 57, 53, 31640, 31187, 53, 60, 58, 9323, 31178, 52, 79, 54, 59, 56, 2]
  5. # p: [31106, 33370, 5629, 16927, 54, 56, 31179, 11002, 72, 31526, 31342, 8835, 31221, 31423, 7156, 55, 31396, 31222, 33370, 31604, 72, 31415, 31396, 31222, 33370, 1231, 31221, 31195, 6851, 11002, 75]
  6. # t: [31106, 33370, 5629, 16927, 54, 56, 31179, 11002, 72, 8835, 31221, 31423, 55, 31396, 31222, 33370, 31604, 72, 2926, 31415, 31396, 31222, 33370, 1231, 31221, 11254, 11002, 31380, 1522, 31482, 11002, 31380, 31640, 31187, 31222, 33370, 31135, 31396, 31380, 73, 5, 54, 56, 34399, 55, 31191, 60, 5, 31415, 31396, 31222, 33370, 1231, 31221, 31195, 60, 31179, 11002, 73, 2122, 72, 6787, 31161, 31415, 31396, 31222, 33370, 1231, 31221, 31195, 60, 31179, 11002, 73]
  7. sample2 = [31106, 33370, 5629, 16927, 54, 56, 31179, 11002, 72, 31526, 31342, 8835, 31221, 31423, 7156, 55, 31396,
  8. 31222, 33370, 31604, 72, 31415, 31396, 31222, 33370, 1231, 31221, 31195, 6851, 11002, 75, 31106, 33370, 5629,
  9. 16927, 54, 56, 31179, 11002, 72, 8835, 31221, 31423, 55, 31396, 31222, 33370, 31604, 72, 2926, 31415, 31396,
  10. 31222, 33370, 1231, 31221, 11254, 11002, 31380, 1522, 31482, 11002, 31380, 31640, 31187, 31222, 33370, 31135,
  11. 31396, 31380, 73, 5, 54, 56, 34399, 55, 31191, 60, 5, 31415, 31396, 31222, 33370, 1231, 31221, 31195, 60,
  12. 31179, 11002, 73, 2122, 72, 6787, 31161, 31415, 31396, 31222, 33370, 1231, 31221, 31195, 60, 31179, 11002,
  13. 73, 2]
  14. json1 = {"input_ids": sample1, "seq_len": 13}
  15. json2 = {"input_ids": sample2, "seq_len": 31}
  16. features = [json1, json2]

每个 json 的 input_ids 都遵循 Q_ids + A_ids + [SEP] 即 Prompt_ids + Target_ids + [SEP] 的规则生成。

2.2 API 生成
  1. from transformers import AutoTokenizer
  2. from transformers import DataCollatorForLanguageModeling
  3. model_checkpoint = "/model/baichuan-7B"
  4. tokenizer = AutoTokenizer.from_pretrained(model_checkpoint, trust_remote_code=True)
  5. tokenizer.pad_token = tokenizer.unk_token
  6. data_collator = DataCollatorForLanguageModeling(tokenizer=tokenizer,mlm=False)
  7. features = [json1, json2]
  8. batch = data_collator(features)
  9. print(batch)

调用 API 生成一个 Batch 对应的大 json 共包含 4 个 key:

• attention_mask

对于基于 mask 的语言任务例如 Bert,data_collator 可以帮助生成掩码标签。对于 sample1 其长度较短,所以后面的 PAD_TOKEN 部分的 mask 均为 0,而 sample2 的长度为所有样本中最长的且小于 max_length,所以其 mask 均为 1。

  1. 'attention_mask': tensor([[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
  2. 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0,
  3. 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
  4. 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
  5. 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
  6. [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
  7. 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
  8. 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
  9. 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
  10. 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]])

• input_ids

与上面同理,data_collator 会就算当前 batch 样本中的 max_length,对于不足 max_length 的样本进行补齐的 padding 操作,所以 sample1 的样本补了很多 padding,sample2 正常。

  1. 'input_ids': tensor([[31106, 15875, 77, 57, 53, 31159, 53, 60, 58, 31135,
  2. 6787, 6775, 81, 31106, 3908, 14313, 31640, 31257, 31481, 31742,
  3. 72, 57, 53, 31640, 31187, 53, 60, 58, 9323, 31178,
  4. 52, 79, 54, 59, 56, 2, 0, 0, 0, 0,
  5. 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
  6. 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
  7. 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
  8. 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
  9. 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
  10. 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
  11. 0, 0, 0, 0, 0, 0],
  12. [31106, 33370, 5629, 16927, 54, 56, 31179, 11002, 72, 31526,
  13. 31342, 8835, 31221, 31423, 7156, 55, 31396, 31222, 33370, 31604,
  14. 72, 31415, 31396, 31222, 33370, 1231, 31221, 31195, 6851, 11002,
  15. 75, 31106, 33370, 5629, 16927, 54, 56, 31179, 11002, 72,
  16. 8835, 31221, 31423, 55, 31396, 31222, 33370, 31604, 72, 2926,
  17. 31415, 31396, 31222, 33370, 1231, 31221, 11254, 11002, 31380, 1522,
  18. 31482, 11002, 31380, 31640, 31187, 31222, 33370, 31135, 31396, 31380,
  19. 73, 5, 54, 56, 34399, 55, 31191, 60, 5, 31415,
  20. 31396, 31222, 33370, 1231, 31221, 31195, 60, 31179, 11002, 73,
  21. 2122, 72, 6787, 31161, 31415, 31396, 31222, 33370, 1231, 31221,
  22. 31195, 60, 31179, 11002, 73, 2]])

Tips:

这里 pad 的 0 和 mask 的 0 需要区分,这里 padding 为 0 是因为我们定义了 pad_token = unk_token,而 unk_token = <unk> 经过 tokenizer encode 后得到的是 [0]。

  1. code=tokenizer.encode(tokenizer.pad_token, max_length=10000, truncation=True)
  2. print('pad', tokenizer.unk_token, 'token', code)
  3. => pad <unk> token [0]

• seq_len

这个很好理解,一个 batch 内的多个样本,每个样本的 prompt_id 的长度即 Q_id 的长度。这个长度主要用于 batch 内判断 longest 最长的序列是多长,从而对短的样本进行 padding,保证进入深度模型网络的样本维度一致。

'seq_len': tensor([13, 31])

• labels

  1. 'labels': tensor([[31106, 15875, 77, 57, 53, 31159, 53, 60, 58, 31135,
  2. 6787, 6775, 81, 31106, 3908, 14313, 31640, 31257, 31481, 31742,
  3. 72, 57, 53, 31640, 31187, 53, 60, 58, 9323, 31178,
  4. 52, 79, 54, 59, 56, 2, -100, -100, -100, -100,
  5. -100, -100, -100, -100, -100, -100, -100, -100, -100, -100,
  6. -100, -100, -100, -100, -100, -100, -100, -100, -100, -100,
  7. -100, -100, -100, -100, -100, -100, -100, -100, -100, -100,
  8. -100, -100, -100, -100, -100, -100, -100, -100, -100, -100,
  9. -100, -100, -100, -100, -100, -100, -100, -100, -100, -100,
  10. -100, -100, -100, -100, -100, -100, -100, -100, -100, -100,
  11. -100, -100, -100, -100, -100, -100],
  12. [31106, 33370, 5629, 16927, 54, 56, 31179, 11002, 72, 31526,
  13. 31342, 8835, 31221, 31423, 7156, 55, 31396, 31222, 33370, 31604,
  14. 72, 31415, 31396, 31222, 33370, 1231, 31221, 31195, 6851, 11002,
  15. 75, 31106, 33370, 5629, 16927, 54, 56, 31179, 11002, 72,
  16. 8835, 31221, 31423, 55, 31396, 31222, 33370, 31604, 72, 2926,
  17. 31415, 31396, 31222, 33370, 1231, 31221, 11254, 11002, 31380, 1522,
  18. 31482, 11002, 31380, 31640, 31187, 31222, 33370, 31135, 31396, 31380,
  19. 73, 5, 54, 56, 34399, 55, 31191, 60, 5, 31415,
  20. 31396, 31222, 33370, 1231, 31221, 31195, 60, 31179, 11002, 73,
  21. 2122, 72, 6787, 31161, 31415, 31396, 31222, 33370, 1231, 31221,
  22. 31195, 60, 31179, 11002, 73, 2]])}

最后一个 key 是 labels,这里也可以解决我们前面的疑惑了,为什么样本里只有 input_ids 和 seq_len,但是经过 data_collator 处理送到 trainer 可以正常训练,这里的 label 对应的是 QA 里 A 的 token ids 即 target ids,以第一个 sample 为例,第一个 sample 的 A ids 为:

[31106, 3908, 14313, 31640, 31257, 31481, 31742, 72, 57, 53, 31640, 31187, 53, 60, 58, 9323, 31178, 52, 79, 54, 59, 56] + [2]

最后的 [2] 与前面 <unk> 类似,eos_token 后编码为 [2]:

  1. code=tokenizer.encode(tokenizer.eos_token, max_length=10000, truncation=True)
  2. print('eos', tokenizer.eos_token, 'token', code)
  3. => eos </s> token [2]

三.生成样本 By DIY

上面使用 Transformer API 实现了 {"input_ids":  xxx, "seq_len": xxx} 形式的数据解析与样本生成,下面我们参考上面逻辑自定义一版 data_collator。首先整理一下思路:

input_ids = Q_ids + A_ids + [SEP]_ids

seq_len = Q_ids.length

labels =  if (max_length) A_ids else A_ids + padding * n

根据 A_ids 的长度判断是否是 longest,然后决定是否补齐 padding 从而生成对应 Label。

1.样本准备

  1. # p: [31106, 15875, 77, 57, 53, 31159, 53, 60, 58, 31135, 6787, 6775, 81]
  2. # t: [31106, 3908, 14313, 31640, 31257, 31481, 31742, 72, 57, 53, 31640, 31187, 53, 60, 58, 9323, 31178, 52, 79, 54, 59, 56]
  3. sample1 = [31106, 15875, 77, 57, 53, 31159, 53, 60, 58, 31135, 6787, 6775, 81, 31106, 3908, 14313, 31640, 31257, 31481,
  4. 31742, 72, 57, 53, 31640, 31187, 53, 60, 58, 9323, 31178, 52, 79, 54, 59, 56, 2]
  5. # p: [31106, 33370, 5629, 16927, 54, 56, 31179, 11002, 72, 31526, 31342, 8835, 31221, 31423, 7156, 55, 31396, 31222, 33370, 31604, 72, 31415, 31396, 31222, 33370, 1231, 31221, 31195, 6851, 11002, 75]
  6. # t: [31106, 33370, 5629, 16927, 54, 56, 31179, 11002, 72, 8835, 31221, 31423, 55, 31396, 31222, 33370, 31604, 72, 2926, 31415, 31396, 31222, 33370, 1231, 31221, 11254, 11002, 31380, 1522, 31482, 11002, 31380, 31640, 31187, 31222, 33370, 31135, 31396, 31380, 73, 5, 54, 56, 34399, 55, 31191, 60, 5, 31415, 31396, 31222, 33370, 1231, 31221, 31195, 60, 31179, 11002, 73, 2122, 72, 6787, 31161, 31415, 31396, 31222, 33370, 1231, 31221, 31195, 60, 31179, 11002, 73]
  7. sample2 = [31106, 33370, 5629, 16927, 54, 56, 31179, 11002, 72, 31526, 31342, 8835, 31221, 31423, 7156, 55, 31396,
  8. 31222, 33370, 31604, 72, 31415, 31396, 31222, 33370, 1231, 31221, 31195, 6851, 11002, 75, 31106, 33370, 5629,
  9. 16927, 54, 56, 31179, 11002, 72, 8835, 31221, 31423, 55, 31396, 31222, 33370, 31604, 72, 2926, 31415, 31396,
  10. 31222, 33370, 1231, 31221, 11254, 11002, 31380, 1522, 31482, 11002, 31380, 31640, 31187, 31222, 33370, 31135,
  11. 31396, 31380, 73, 5, 54, 56, 34399, 55, 31191, 60, 5, 31415, 31396, 31222, 33370, 1231, 31221, 31195, 60,
  12. 31179, 11002, 73, 2122, 72, 6787, 31161, 31415, 31396, 31222, 33370, 1231, 31221, 31195, 60, 31179, 11002,
  13. 73, 2]
  14. json1 = {"input_ids": sample1, "seq_len": 13}
  15. json2 = {"input_ids": sample2, "seq_len": 31}
  16. features = [json1, json2]

样本继续使用上面构造的 json1 和 json2 并统一放到 features 的 list 中。

2.data_colloator 实现

  1. def data_collator(features: list) -> dict:
  2. # 序列长度: [36, 106]
  3. len_ids = [len(feature["input_ids"]) for feature in features]
  4. # 取最长的序列长度: 106
  5. longest = max(len_ids)
  6. input_ids = []
  7. labels_list = []
  8. # 降序排列
  9. for ids_l, feature in sorted(zip(len_ids, features), key=lambda x: -x[0]):
  10. ids = feature["input_ids"] # tokenIds
  11. seq_len = feature["seq_len"] # seqLen
  12. # len(prompt) x [-100] + Target + [longest - len(prompt)] * [-100]
  13. labels = ([-100] * seq_len + ids[seq_len:] + [-100] * (longest - ids_l))
  14. ids = ids + [pad_token_id] * (longest - ids_l)
  15. _ids = torch.LongTensor(ids)
  16. labels_list.append(torch.LongTensor(labels))
  17. input_ids.append(_ids)
  18. # tensor([[], []])
  19. input_ids = torch.stack(input_ids)
  20. labels = torch.stack(labels_list)
  21. return {
  22. "input_ids": input_ids,
  23. "labels": labels,
  24. }

data_collator 的逻辑主要是这一句:

  1. # len(prompt) x [-100] + Target + [longest - len(prompt)] * [-100]
  2. labels = ([-100] * seq_len + ids[seq_len:] + [-100] * (longest - ids_l))

即将 Q mask 进行掩码,A 即 Target 保持不变,最后根据 longest 的长度决定是否 padding,这里手动指定了 pad_token_id = 0,实际代码中可以使用 tokenizer 自动指定:

tokenizer.pad_token_id

除此之外,这里和上面 API 生成还有一个区别是是否 mask Q_ids,API 把 Q_ids 全部去掉,上面方法保留了 Q_ids 的位置,但是使用 -100 进行了 mask,实际场景下二者没有区别,只不过 DIY 的 labels 如果 Q_ids 很长则会占用很多无关空间。

3.使用自定义 data_colloator

  1. trainer = ModifiedTrainer(
  2. model=model,
  3. train_dataset=dataset,
  4. args=training_args,
  5. callbacks=[TensorBoardCallback(writer)], # 回调将训练情况写入 tensorboard
  6. data_collator=data_collator,
  7. )

定义好 Trainer 后,将上面定一个 data_collator 传给 data_collator 参数即可,不过常规情况下我们使用 API 即可,如果自己对样本和 label 的构建有自定义需求,则可以采用后者 DIY 的形式。

四.总结

经过上面的分析,对于样本的处理和 label 的生成流程我们大致清晰了,下面解释下上面的样本如何应用在 LM 大语言模型中以及自己理解的这样构造的含义。

input 为 QA,output 为 A。Input 和 Output 分别在 Embedding lookup 获取输入向量,添加 position 向量后进入 Multi-Head Attention,首先经过 Q、K、V 的 Linear 映射转换,随后经过 Encoder 和 Decoder 的 Transformer 结构,最终通过一个 Linear + Softmax 输出预测概率 logits,输出概率 logits 与 A_ids 对应的 multi_hot 向量计算 batch loss 并回传,这里会忽略 [-100] 的掩码,更完整的 loss 计算逻辑大家可以参考 model.loss 的实现。

  1. # 根据输入计算 Loss
  2. def compute_loss(self, model, inputs, return_outputs=False):
  3. return model(
  4. input_ids=inputs["input_ids"],
  5. labels=inputs["labels"],
  6. ).loss

一般情况下,这里 QA 也分别代表 Prompt 和 Target,上面这种样本和 label 的构造方式意在学习当给定 Prompt 的前提提示下,模型能够预测得到 Target,即语言生成。如果想要学习其他不同的模式,大家也可以根据需求自定义修改上面的 data_collator。

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

闽ICP备14008679号