基于HuggingFace训练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 ['▁']
核心疑问
- Trainer类或DataCollator是否会自动添加EOS token?
- 添加EOS token的最佳实践是什么?应该在何处添加?
- 代码中还有哪些需要检查或奇怪的地方(比如索引为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

