🐷

tokenizer.pad_token = tokenizer.eos_tokenって問題ないの?

に公開

はじめに

LLM の Fine-Tuning の実装を見ていると、以下のように pad_token として eos_token を使っている実装をちらほら見かけます。

tokenizer.pad_token = tokenizer.eos_token

私はこれを見た時、「pad_token と eos_token に同じトークンを使うと、何か問題が起きそうじゃないか🧐?」と思い、色々調べたのでその結果をまとめておきます。

結論

大半のケースでは tokenizer.pad_token = tokenizer.eos_token として問題はありません。

ただし以下 2 点の前提が成り立っている時に限定されます。(普通に学習を行なっている場合は成立するものです。)

  • attention_mask について、パディングトークンの位置を 0 にしておくこと。
  • 損失計算について、パディングトークンを除外しておくこと(labels を -100 などに設定する)。

もしトークンを厳密に分けたい場合は、新しい [PAD] トークンを追加して語彙を拡張することで実現できます。ただし、埋め込み行列のサイズが変わることで、読み込み時に手間がかかる点に注意してください。以下のような実装で拡張が可能です。

tokenizer.add_special_tokens({"pad_token": "[PAD]"})
model.resize_token_embeddings(len(tokenizer))

一方、「なぜ pad_token が無いモデルがあるのか?」については、明確な理由が分かりませんでした。

なぜ pad_token として eos_token を設定する必要があるのか?

新たに、SFT などで、パディングを行うようなコードを利用する際、pad_token が設定されている必要があります。例えば、以下のような分類を行う Fine-Tuning をする場合、DataCollatorWithPadding で padding を行うためのトークンが必要となります。

import torch
from datasets import Dataset
from transformers import (
    AutoTokenizer,
    AutoModelForSequenceClassification,
    DataCollatorWithPadding,
    TrainingArguments,
    Trainer,
)

MODEL_NAME = "meta-llama/Llama-3.2-1B-Instruct"

tokenizer = AutoTokenizer.from_pretrained(MODEL_NAME, use_fast=True)
model = AutoModelForSequenceClassification.from_pretrained(
    MODEL_NAME,
    num_labels=2,
    torch_dtype="auto",
    trust_remote_code=True,
)

# ① #
tokenizer.pad_token_id = tokenizer.eos_token_id
model.config.pad_token_id = tokenizer.pad_token_id
### 

ds = Dataset.from_dict(
    {
        "text": [
            "I love this movie. Fantastic!",
            "Terrible experience... never again.",
            "It was okay, not great.",
            "Brilliant acting and strong plot.",
        ],
        "labels": [1, 0, 0, 1],
    }
)

def preprocess(example):
    tokenized = tokenizer(
        example["text"],
        truncation=True,
        max_length=128,
    )
    return tokenized

tok_ds = ds.map(preprocess, remove_columns=["text"])

collator = DataCollatorWithPadding(
    tokenizer=tokenizer,
    return_tensors="pt",
)

training_args = TrainingArguments(
    output_dir="./ckpt-cls",
    learning_rate=1e-6,
    num_train_epochs=1,
    logging_steps=1,
    report_to="none"
)

trainer = Trainer(
    model=model,
    args=training_args,
    train_dataset=tok_ds,
    data_collator=collator,
)

trainer.train()

①の部分のコードが重要となります。このコードがないと、以下のような Error が表示されます。

ValueError: Asking to pad but the tokenizer does not have a padding token. Please select a token to use as `pad_token` `(tokenizer.pad_token = tokenizer.eos_token e.g.)` or add a new pad token via `tokenizer.add_special_tokens({'pad_token': '[PAD]'})`.

Error メッセージにて、tokenizer.pad_token = tokenizer.eos_token をしろと表示されますね。

モデルごとの pad / eos トークン一覧

LLM を利用する場合、以下のようなコードで、tokenizer と model を読み込むかと思います。

from transformers import AutoModelForCausalLM, AutoTokenizer

model_name = "meta-llama/Llama-3.2-1B-Instruct"

tokenizer = AutoTokenizer.from_pretrained(model_name)
model = AutoModelForCausalLM.from_pretrained(model_name)

tokenizer と model にはそれぞれ pad_token と eos_token が設定されています。以下の実装で確認することができます。

print(tokenizer.pad_token)
print(tokenizer.eos_token)

print(model.config.pad_token_id)
print(model.config.eos_token_id)

model.config.pad_token_idmodel.config.eos_token_idの返却値 はトークン ID(例: 128001)なので、トークン文字列を知りたい場合は tokenizer.decode の実施必要です。

主要な LLM モデルについて、tokenizer と model の pad_token と eos_token を確認した結果を以下の表にまとめます。

モデル名 tokenizer.pad_token tokenizer.eos_token model.pad_token model.eos_token
meta-llama/Llama-3.2-1B-Instruct None <|eot_id|> None ['<|end_of_text|>', '<|eom_id|>', '<|eot_id|>']
Qwen/Qwen2.5-3B-Instruct <|endoftext|> <|im_end|> None ['<|im_end|>']
Qwen/Qwen3-0.6B <|endoftext|> <|im_end|> None ['<|im_end|>']
google/gemma-2-2b-it <pad> <eos> ['<pad>'] ['<eos>', '<end_of_turn>']
google/gemma-3-4b-it <pad> <eos> None ['<eos>', '<end_of_turn>']
microsoft/Phi-4-mini-instruct <|endoftext|> <|endoftext|> ['<|endoftext|>'] ['<|endoftext|>']

表から分かることは以下です。

  • meta-llama/Llama-3.2-1B-Instruct について、tokenizer, model ともに、pad_token が None である。
  • Qwen/Qwen2.5-3B-Instruct, Qwen/Qwen3-0.6B, google/gemma-3-4b-it について、tokenizer には pad_token と eos_token が設定されているが、model には pad_token が設定されていない。
  • google/gemma-2-2b-it は、tokenizer, model ともに、pad_token, eos_token にそれぞれ異なるトークンが設定されている。
  • microsoft/Phi-4-mini-instruct は、pad_token, eos_token として同じトークンが設定されている。

なぜ pad_token が無いモデルがあるのか?

モデルを実際に学習したチームや、学習の際の手法によって事情が異なりそうです。そのため、予想も含めながら私の理解を記載します。もしより明確な理由が分かる方いたら教えてください。

まず、LLM は 事前学習 → SFT → RLHF/DPO/GRPO と学習が進みます。

モデルに pad_token が無い理由は、それぞれの段階でパディングをどう扱うかに起因します。以下、フェーズ別に整理します。

事前学習
多くの実装は シーケンス・パッキング(固定長ブロック:例 4 096 token)で学習します。
連結後のブロック長は常に一定なので、モデル内部でパディングを挿入する必要がありません。つまり、pad_token は必要ありません。

SFT(Supervised Fine-Tuning)
SFT では <|user|> … <|assistant|> … と言う質問と応答のデータを、固定長の 1 本のシーケンスに連結して学習する packing と言う手法があります。この手法では、pad_token が必要ありません。よって、packing を利用して学習されたモデルには、pad_token が付与されない可能性があります。

RLHF/DPO/GRPO
ポリシー/報酬モデルとも「1 プロンプト → 1 生成」を扱う点は同じですが、生成長はサンプルごとに異なるため、バッチ計算の直前にパディングが必須になります。そのため、RLHF を行なったモデルについては、pad_token が必須のように感じます。一方、RLHF を行なっている meta-llama/Llama-3.2-1B-Instructに pad_token が設定されていないため、その理由に関しては分かりませんでした。

まとめると、事前学習では、pad_token が必要ありません。SFT では packing を行うことで、pad_token を設定しないで実施することが可能です。RLHF/DPO/GRPO では、pad_token が必要そうに見えます。一方、RLHF を行なっている meta-llama/Llama-3.2-1B-Instructに対し、pad_token が設定されていない理由はよく分かりませんでした。

pad_token として eos_token を利用しても問題ないか?

ほぼ問題ありません。理由は 2 つあります。

Attention 計算で無視される
DataCollatorWithPadding などは、パディング位置の attention_mask を 0 に設定します。0 になった位置は無視されるため、eos_token を同じトークンを pad_token として利用しても学習や推論に影響はありません。

損失計算から除外される
生成の場合はラベル (labels) をコピーしたのち、パディング位置を -100 に置換する実装が一般的です。SFTTrainer は自動で実施されます。CrossEntropyLoss はラベル -100 を無視するため、パディングが学習に影響しません。

一方、自分で実装した場合 attention_mask を付け忘れたり、labels の -100 対応をし忘れるとEOS トークンの確率が変わるため、問題があります。正しく設定できているか確認するようにしましょう。

pad_token を独立させたい場合

pad_token と eos_token を分けたい場合は、次のようにトークンを追加します。

tokenizer.add_special_tokens({"pad_token": "[PAD]"})
model.resize_token_embeddings(len(tokenizer))

ただし、この手法だと、語彙サイズが 1 つ増えるため、埋め込み行列が拡張されます。埋め込み行列が拡張されると、既存のチェックポイントを読み込めなくなり、重みの共有やマージ時に問題が起こりやすくなります。

多くのプロジェクトで tokenizer.pad_token = tokenizer.eos_token の流用が実用的とされるのは、この煩雑さを回避することが理由かと考えます。

終わりに

tokenizer.pad_token = tokenizer.eos_tokenという実装は、直感的に違和感はありますが、よくよく考えると問題がないことが多い実装であることが分かりました。個人的には、pad_token と eos_token は明確に分けたいですが、モデルの事前学習やSFT、RLHFにおいてパディングが必要ないのであれば、設定されていないのも仕方ないと思いました。むしろ、ダウンストリームタスク側がパディングをしなくて済むように発展する必要があるのかもしれないですね。

pad_token と eos_token の確認を行なったコード

https://colab.research.google.com/drive/1FKiJgkt28s_nEFxd-JE9xumk1Qe72tQo?usp=sharing

参考ページ

https://discuss.huggingface.co/t/how-to-set-the-pad-token-for-meta-llama-llama-3-models/103418

https://discuss.huggingface.co/t/why-does-the-falcon-qlora-tutorial-code-use-eos-token-as-pad-token/45954

Discussion