训练数据格式为<input,output>,为什么微调大模型时,模型所需的输入数据input_ids有时仅包含了input,而有时包含了input和output呢?

input_ids仅包含input

问题描述

input_ids = tokenizer.encode(input)  

适用场景

这种方法适用于那些只需要模型理解输入内容并预测输出的任务,如文本分类情感分析等。在这些任务中,模型的目标是基于输入文本来预测一个标签或类别,而不是生成与输入文本相关的输出文本。

input_ids包含input和output

问题描述

input_ids = tokenizer.encode(input+output)  

适用场景

这种方法适用于序列到序列的任务,如机器翻译文本摘要问答系统等。在这些任务中,模型需要根据输入文本生成相应的输出文本,因此input_ids需要包含完整的输入和输出序列,以便模型可以学习如何从输入映射到输出。

hugging face 代码示例

1. tokenizer分别处理input_ids、attention_mask、labels

数据格式:

{"input": "我爱中华", "output": "是的"}
{"input": "明天会更好", "output": "没错"}
{"input": "天气不错", "output": "阳光明媚"}

测试代码(只供理解,不为真实代码)

from transformers import (
    LlamaForCausalLM,
    LlamaTokenizer,
    LlamaConfig,
    default_data_collator,
)

tokenizer = LlamaTokenizer.from_pretrained(train_config.model_name)

# 逐行读取 JSONL 文件
with open(data_path, 'r', encoding='utf-8') as f:
    data = [json.loads(line.strip()) for line in f]

max_words = 1024

for item in data:
    input_text = item['input']
    output_text = item['output']
    # input部分单独编码
        input = torch.tensor(self.tokenizer.encode(input_text), dtype=torch.int64)
        print("input torch.tensor: ", input)
        print("input shape: ", input.shape)
        print("input shape[0]: ", input.shape[0])

        # input_ids: input、output拼接编码
        combined_text = input_text + output_text
        example = self.tokenizer.encode(combined_text)
        example.append(self.tokenizer.eos_token_id)    # 结束标记
        print("example encode: ", example)
        example = torch.tensor(example, dtype=torch.int64)
        print("example torch.tensor: ", example)
        print("example shape: ", example.shape)
        print("example shape[0]: ", example.shape[0])
        print("\n")

        # 统一长度
        padding = self.max_length - example.shape[0]
        print('padding: ', padding)
        if padding > 0:
            example = torch.cat((example, torch.zeros(padding, dtype=torch.int64) - 1))  # 不足补-1
        elif padding < 0:
            example = example[: self.max_words]  # 超出截断
        print("after padding, example torch.tensor: ", example)
        print("after padding, example shape: ", example.shape)
        print("after padding, example shape[0]: ", example.shape[0])

        # labels: 复制input_ids,input部分改为-1,output部分不变
        labels = copy.deepcopy(example)
        print("labels = copy.deepcopy(example): ", labels)
        labels[: len(input)] = -1
        print("labels tensor: ", labels)
        print("\n")
        # 将labels中为-1的置为-100
        label_mask = labels.ge(0)
        labels[~label_mask] = -100
        print("final labels tensor: ", labels)


        # attention_mask:
        example_mask = example.ge(0)    # ge(0): PyTorch 操作,表示 greater than or equal to 0(大于等于0), 如果值大于等于 0,结果是 True。 example_mask = tensor([True, True, False, False]),True 和 False 的值分别是 1 和 0
        print("example_mask ge(0): ", example_mask)
        example[~example_mask] = 0      # ~example_mask: 按位取反运算。 example_mask = tensor([True, True, 0, 0])=[1, 1, 0, 0]
        print("example[~example_mask]: ", example)
        example_mask = example_mask.float()
        print("example_mask: ", example_mask)
        print("\n")

        tokenized_ids = {
            "input_ids": example,
            "labels": labels,
            "attention_mask": example_mask,
        }

# 只供理解,不为真实代码
trainer = Trainer(
    model=model,
    args=args,
    train_dataset=tokenized_ids,
)

理解输出(input_ids、labels、attention_mask) 

dataset count:  3
input torch.tensor:  tensor([ 101, 1855,  100, 1746,  100,  102])
input shape:  torch.Size([6])
input shape[0]:  6
example encode:  [101, 1855, 100, 1746, 100, 100, 1916, 102]
example torch.tensor:  tensor([ 101, 1855,  100, 1746,  100,  100, 1916,  102])
example shape:  torch.Size([8])
example shape[0]:  8


padding:  8
after padding, example torch.tensor:  tensor([ 101, 1855,  100, 1746,  100,  100, 1916,  102,   -1,   -1,   -1,   -1,
          -1,   -1,   -1,   -1])
after padding, example shape:  torch.Size([16])
after padding, example shape[0]:  16
labels = copy.deepcopy(example):  tensor([ 101, 1855,  100, 1746,  100,  100, 1916,  102,   -1,   -1,   -1,   -1,
          -1,   -1,   -1,   -1])
labels tensor:  tensor([  -1,   -1,   -1,   -1,   -1,   -1, 1916,  102,   -1,   -1,   -1,   -1,
          -1,   -1,   -1,   -1])
final labels tensor:  tensor([  -100,   -100,   -100,   -100,   -100,   -100, 1916,  102,   -100,   -100,   -100,   -100,
          -100,   -100,   -100,   -100])


example_mask ge(0):  tensor([ True,  True,  True,  True,  True,  True,  True,  True, False, False,
        False, False, False, False, False, False])
example[~example_mask]:  tensor([ 101, 1855,  100, 1746,  100,  100, 1916,  102,    0,    0,    0,    0,
           0,    0,    0,    0])
example_mask:  tensor([1., 1., 1., 1., 1., 1., 1., 1., 0., 0., 0., 0., 0., 0., 0., 0.])

2. 自定义处理函数FineTuneDataset():tokenizer一次性处理input_ids、attention_mask、labels

import copy
import json

import torch
from torch.utils.data import Dataset
from transformers import BertTokenizer


class FineTuneDataset(Dataset):
    def __init__(self, dataset_config, tokenizer, max_words=1024):

        with open(data_path, 'r', encoding='utf-8') as f:
            self.data = [json.loads(line.strip()) for line in f]
        self.max_words = max_words
        self.tokenizer = tokenizer

    def __len__(self):
        return len(self.data)

    def __getitem__(self, index):
        IGNORE_INDEX = -100  # The default setting in CrossEntropyLoss

        item = self.data[index]

        prompt = item['input']

        # example = input+ output
        example = prompt + item["output"]
#        print(example)
        prompt = torch.tensor(self.tokenizer.encode(prompt), dtype=torch.int64)

        # example = input_ids + output_ids
        example = self.tokenizer.encode(example)    # input+output
#        print(example)
        # example = input_ids + output_ids + <eos>
        # example.append(self.tokenizer.eos_token_id)
        example = torch.tensor(example, dtype=torch.int64)
        padding = self.max_words - example.shape[0]
        # 用 -1 填充
        if padding > 0:
            example = torch.cat((example, torch.zeros(padding, dtype=torch.int64) - 1))
        # 截断
        elif padding < 0:
            example = example[: self.max_words]
        # labels = example
        labels = copy.deepcopy(example)

        # labels = input(-1) + output
        # 复制 example,并将 example 中与 prompt 对应的部分设置为 -1。这样,模型在训练时只会关注 output 部分作为标签,而忽略掉输入部分。
        labels[: len(prompt)] = -1

        # 创建一个 mask,用于标记 example 中不为 -1(即有效)的部分。
        example_mask = example.ge(0)

        # 创建一个 mask,用于标记 labels 中不为 IGNORE_INDEX(即有效)的部分。
        label_mask = labels.ge(0)

        # 将无效的部分(填充部分)置为 0,这样它们不会对损失计算产生影响。
        example[~example_mask] = 0
        labels[~label_mask] = IGNORE_INDEX
        example_mask = example_mask.float()
        label_mask = label_mask.float()

        return {
            "input_ids": example,   # example = input_ids + output_ids + <eos>
            "labels": labels,       # labels = input(-1) + output,输入部分被替换为 -1,只保留 output 部分作为目标标签。
            "attention_mask": example_mask,  # 返回一个 mask,指示哪些位置是有效的(即不是填充部分)。
        }

# 示例使用
if __name__ == '__main__':

    BERT_PATH = "./model/bert_base_uncased"
    tokenizer = BertTokenizer.from_pretrained(BERT_PATH)

    # 数据路径
    data_path = './train.jsonl'  # JSON 格式数据路径,包含 'input' 和 'output'

    # 创建数据集
    dataset = FineTuneDataset(data_path, tokenizer, max_words=16)
    print('dataset count: ', len(dataset))

    # 数据示例
    for data in dataset:
        print("Input IDs:", data['input_ids'])
        print("Attention Mask:", data['attention_mask'])
        print("Labels:", data['labels'])

3. tokenizer自动一次性处理input_ids、attention_mask、labels

attention_mask、input_ids和labels长度一样。

import json
from transformers import AutoModelForCausalLM, AutoTokenizer

# 加载 LLaMA 模型和分词器
model_name = "meta-llama/Llama-2-7b-hf"  # 替换为适合的模型
tokenizer = AutoTokenizer.from_pretrained(model_name)
model = AutoModelForCausalLM.from_pretrained(model_name, device_map="auto", load_in_8bit=True)

# 加载 JSONL 数据
dataset = load_dataset("json", data_files="path_to_your_file.jsonl")

# 数据预处理函数,tokenizer() 自动生成每条数据的input_ids、labels、attention_mask
def tokenizer_function(examples):
    return tokenizer(
        examples["input"]+examples["output"],
        padding="max_length",
        truncation=True,
        max_length=1024,
    )

# train_dataset[input_ids、labels、attention_mask]
train_dataset = dataset.map(tokenizer_function, batched=True, remove_columns=["input", "output"])

trainer = Trainer(
    model=model,
    args=training_args,
    train_dataset=train_dataset,
    data_collator=DataCollatorForSeq2Seq(tokenizer, padding=True),     # 数据批处理器
)

DataCollatorForSeq2Seq 是 Hugging Face transformers 库中提供的一个数据整理工具,专门为序列到序列(Seq2Seq)任务设计的。它的主要作用是动态地对输入和输出数据进行填充,并生成适合训练的 input_idslabelsattention_mask

Trainer中的train_dataset已经是经过token化了,为什么还需要data_collator来生成input_ids、labels 和 attention_mask。

为什么需要 data_collator

  1. 动态批处理
    虽然 train_dataset 已经包含了 input_idslabelsattention_mask,但这些是按样本粒度存储的,Trainer 需要以批次为单位将这些样本拼接成张量。如果不使用 data_collator,则必须手动编写批处理逻辑。

  2. 动态填充
    即使在 tokenizer_function 中指定了 padding="max_length",但通常不推荐对所有样本统一填充到 max_length,因为这会浪费内存和计算资源。

    • 使用 data_collator 可以根据每个批次中最长样本的长度动态调整填充长度。
    • 在大规模训练中,动态填充可以显著减少填充引入的计算开销。
  3. 目标序列(labels)的处理
    特别是在因果语言模型(如 LLaMA)中,labels 可能需要额外的填充值(如 -100)来避免填充值的部分参与损失计算。

    • 如果 tokenizer_function 没有处理好填充值(label_pad_token_id),data_collator 可以为目标序列动态填充正确的值。
  4. 简化批处理代码
    data_collator 提供了现成的功能,无需手动编写批处理逻辑。这使得代码更加简洁和易于维护。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值