You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

基于HuggingFace训练LongT5时,如何添加EOS token?

关于LongT5训练中EOS Token添加及代码优化的疑问

我在用HuggingFace的Trainer类训练LongT5模型时,对EOS token的添加位置和是否自动添加感到困惑。我的数据集是<from>和<to>的文本对格式,分词器是自定义训练后用T5TokenizerFast加载的,目前发现分词器不会自动添加EOS token,训练后模型生成内容会一直到max_new_tokens上限才停止,怀疑是没加EOS导致的。

相关代码与输出

自定义分词器训练代码

tokenizer = SentencePieceUnigramTokenizer()
tokenizer.train_from_iterator(iterator=iterator, vocab_size=32_128, show_progress=True, unk_token="<unk>")

分词器加载代码

tokenizer = T5TokenizerFast(tokenizer_file="data-rb-25000/tokenizer.json",  
                            padding=True, bos_token="<s>", 
                            eos_token="</s>",unk_token="<unk>", 
                            pad_token="<pad>")

数据集预处理与过滤代码

MAX_SEQUENCE_LENGTH = 16_384 / 2

def preprocess_function(examples):
    inputs = tokenizer(
        examples['from'],
        truncation=False,  # Don't truncate yet
        padding=False,     # Don't pad yet
        return_length=True,
    )
    labels = tokenizer(
        examples['to'],
        truncation=False,
        padding=False,
        return_length=True,
    )

    inputs["input_length"] = inputs["length"]
    inputs["labels"] = labels["input_ids"]
    inputs["label_length"] = labels["length"]

    inputs.pop("length", None)

    return inputs

tokenized_data = dataset.map(preprocess_function, batched=True, remove_columns=dataset["train"].column_names)

def filter_function(example):
    return example['input_length'] <= MAX_SEQUENCE_LENGTH and example['label_length'] <= MAX_SEQUENCE_LENGTH

filtered_data = tokenized_data.filter(filter_function)

模型训练代码

from transformers import DataCollatorForSeq2Seq

data_collator = DataCollatorForSeq2Seq(tokenizer=tokenizer, model="google/long-t5-tglobal-base")

from transformers import AutoModelForSeq2SeqLM, AutoConfig

config = AutoConfig.from_pretrained(
    "google/long-t5-tglobal-base",
    vocab_size=len(tokenizer),
    pad_token_id=tokenizer.pad_token_id,
    eos_token_id=tokenizer.eos_token_id,
    decoder_start_token_id=tokenizer.pad_token_id,
)

model = AutoModelForSeq2SeqLM.from_config(config)

from transformers import GenerationConfig

generation_config = GenerationConfig.from_model_config(model.config)
generation_config._from_model_config = False
generation_config.max_new_tokens = 16_384

from transformers import Seq2SeqTrainer, Seq2SeqTrainingArguments

training_args = Seq2SeqTrainingArguments(
    output_dir="rb-25000-model",
    eval_strategy="epoch",
    save_strategy="epoch",
    learning_rate=2e-5,
    per_device_train_batch_size=1,
    per_device_eval_batch_size=1,
    gradient_accumulation_steps=16,
    gradient_checkpointing=True,
    weight_decay=0.01,
    save_total_limit=3,
    num_train_epochs=5,
    logging_steps=1,
    predict_with_generate=True,
    load_best_model_at_end=True,
    bf16=True,
)

trainer = Seq2SeqTrainer(
    model=model,
    args=training_args,
    train_dataset=filtered_data["train"],
    eval_dataset=filtered_data["test"],
    tokenizer=tokenizer,
    data_collator=data_collator,
    compute_metrics=compute_metrics,
    generation_config=generation_config,
)

trainer.train()

分词器测试代码与输出

测试代码:

inputs = tokenizer(['Hello world', 'Hello'], padding=True, truncation=True, max_length=100, return_tensors="pt")
labels = inputs["input_ids"]

print(labels)
print(tokenizer.convert_tokens_to_ids(['<s>'])[0])
print(tokenizer.convert_tokens_to_ids(['<pad>'])[0])
print(tokenizer.convert_tokens_to_ids(['<unk>'])[0])
print(tokenizer.convert_tokens_to_ids(['</s>'])[0])

print(tokenizer.convert_ids_to_tokens([1]))

输出:

tensor([[1, 10356, 1, 5056],
        [1, 10356, 16002, 16002]])
16000
16002
0
16001
['▁']

核心疑问

  1. Trainer类或DataCollator是否会自动添加EOS token?
  2. 添加EOS token的最佳实践是什么?应该在何处添加?
  3. 代码中还有哪些需要检查或奇怪的地方(比如索引为1的未知token)?

问题解答

1. EOS Token的自动添加逻辑

DataCollatorForSeq2Seq不会自动给输入或标签添加EOS token,它仅负责padding、处理标签的mask(将pad token对应位置设为-100)。Trainer本身也不会主动添加EOS,必须在数据预处理阶段手动处理。你观察到模型生成停不下来,确实是因为标签里没有EOS token,模型没学会用EOS终止生成。

2. 添加EOS Token的最佳实践

(1)预处理阶段给标签添加EOS

T5系列模型的训练逻辑是:输入不需要加EOS,但标签必须以EOS token结尾,这样模型才会学习在生成完目标文本后输出EOS,触发终止。

修改你的preprocess_function,在标签的input_ids末尾追加EOS token:

def preprocess_function(examples):
    inputs = tokenizer(
        examples['from'],
        truncation=False,
        padding=False,
        return_length=True,
    )
    labels = tokenizer(
        examples['to'],
        truncation=False,
        padding=False,
        return_length=True,
    )
    # 给每个标签的input_ids添加EOS token
    labels["input_ids"] = [ids + [tokenizer.eos_token_id] for ids in labels["input_ids"]]
    # 更新label_length,加1(EOS的长度)
    labels["length"] = [length + 1 for length in labels["length"]]

    inputs["input_length"] = inputs["length"]
    inputs["labels"] = labels["input_ids"]
    inputs["label_length"] = labels["length"]

    inputs.pop("length", None)

    return inputs

同时,过滤函数里的MAX_SEQUENCE_LENGTH要考虑EOS的位置,确保加上EOS后不超过上限(当前设置的是16384/2=8192,所以标签原长度要≤8191,加EOS后才≤8192)。

(2)输入是否需要加EOS?

T5模型的输入不需要加EOS,它的输入默认以<s>(BOS)开头,没有EOS的要求,所以输入部分保持现状即可。

3. 代码中的其他问题检查

(1)索引为1的未知token

这个token是▁,是SentencePiece分词器默认生成的空格/起始token。因为你自定义训练分词器时只指定了unk_token="<unk>",没有把<s>、</s>、<pad>加入训练词汇,所以这些特殊token是后续加载时手动添加的,而索引1被分词器训练时生成的▁占用了。

解决办法:在训练自定义分词器时,把所有特殊token一起传入,确保它们被加入词汇表:

special_tokens = ["<unk>", "<s>", "</s>", "<pad>"]
tokenizer = SentencePieceUnigramTokenizer()
tokenizer.train_from_iterator(
    iterator=iterator,
    vocab_size=32_128,
    show_progress=True,
    unk_token="<unk>",
    special_tokens=special_tokens
)

这样后续加载时,这些特殊token的id会和训练时一致,不会出现奇怪的占位token。

(2)Decoder起始token设置问题

你在config里设置了decoder_start_token_id=tokenizer.pad_token_id,但T5模型的decoder起始token应该是<s>(即tokenizer.bos_token_id),而不是pad token。这个错误会导致模型解码时起始输入错误,影响训练和生成效果,必须修正:

config = AutoConfig.from_pretrained(
    "google/long-t5-tglobal-base",
    vocab_size=len(tokenizer),
    pad_token_id=tokenizer.pad_token_id,
    eos_token_id=tokenizer.eos_token_id,
    decoder_start_token_id=tokenizer.bos_token_id,  # 改为bos_token_id
)

(3)MAX_SEQUENCE_LENGTH的类型问题

你设置MAX_SEQUENCE_LENGTH = 16_384 / 2,得到的是浮点数8192.0,过滤时和整数的input_length/label_length比较可能没问题,但建议改为整数:

MAX_SEQUENCE_LENGTH = 16_384 // 2  # 用整数除法得到8192

(4)预处理时的截断逻辑

你现在是先分词再过滤长度,但如果原文本分词后长度超过上限,过滤会直接丢弃样本。可以考虑在分词时直接截断,避免浪费数据:

inputs = tokenizer(
    examples['from'],
    truncation=True,
    max_length=int(MAX_SEQUENCE_LENGTH),
    padding=False,
    return_length=True,
)
labels = tokenizer(
    examples['to'],
    truncation=True,
    max_length=int(MAX_SEQUENCE_LENGTH) - 1,  # 留位置加EOS
    padding=False,
    return_length=True,
)

这样可以把超长文本截断到允许的长度,而不是直接丢弃。

内容的提问来源于stack exchange,提问作者gphilip

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.17 07:54:57