【論文解説】Tokenizationとは?SentencePiece論文からLLMが文章をtokenに分割する仕組みを解説

SentencePieceが学習済みtokenizerモデルとしてEncode/Decodeに使われる流れ

Tokenizationとは?SentencePiece論文からLLMが文章をtokenに分割する仕組みを解説

スポンサーリンク

3文要約

Tokenizationで文字列がサブワードとtoken ID列に変換されLLMへ入力される流れ

Tokenization(文章をモデルが扱える単位に分割する処理)は、 LLM(大規模言語モデル)の入力文字列を、 サブワードとtoken ID列へ変換する入口です。

SentencePiece論文は、 pre-tokenization(事前に単語境界で分割する処理)に依存せず、 raw text(言語別の単語分割をかける前の文字列)から、 サブワードtokenizerを学習できる技術を提案しました。

特に、空白を通常の記号として扱うlossless tokenization (正規化後の文字列を復元できるトークン化)と、 正規化ルールまで含む自己完結したtokenizerモデルが重要です。

これらは、 現在の多言語LLMを理解するうえでも大事な視点です。

論文情報

項目 内容
論文タイトル SentencePiece: A simple and language independent subword tokenizer and detokenizer for Neural Text Processing
著者 Taku Kudo, John Richardson
公開日 2018年8月19日
採択 EMNLP 2018 Demo Paper
分野 Computation and Language
arXiv SentencePiece: A simple and language independent subword tokenizer and detokenizer for Neural Text Processing
DOI 10.48550/arXiv.1808.06226
実装 google/sentencepiece

Tokenizationとは何か

Tokenizationは処理であり、SentencePiece modelはその処理に使う学習済みファイルであることを示す図

Tokenizationは、文章をモデルが扱えるtokenへ分割し、必要に応じてtoken IDへ変換する処理です。

ここで混乱しやすいのは、「処理」と「モデル」という言葉です。

Tokenization自体は処理名です。

一方で、SentencePieceでは、 その処理に使う語彙、ID、正規化ルール、 BPE/Unigramの設定などを学習します。

そして、それらを .model ファイルとして保存します。

つまり、SentencePieceは、 「tokenizationを実行するシステム」と、 「学習済みtokenizerモデルファイル」の両方に関わる技術です。

SentencePieceが学習済みtokenizerモデルとしてEncode/Decodeに使われる流れ
用語 意味
Tokenization 文字列をtokenへ分割し、token IDへ変換する処理
Tokenizer Tokenizationを行うプログラムや設定一式
SentencePiece tokenizerを学習・実行するための技術と実装
SentencePiece model 語彙、ID、正規化ルール、モデルタイプを含む学習済みtokenizerファイル
token ID列 tokenの文字数ではなく、語彙表上の整数IDの並び

たとえば、 語彙表に ▁New -> 482, ▁York -> 901 と登録されているとします。

この場合、New York["▁New", "▁York"] というpiece列を経て、 [482, 901] というtoken ID列になります。

LLMは、この整数ID列をEmbedding(IDをベクトルへ変換する層)へ渡して計算します。

なぜ単語単位では足りないのか

単語単位の弱点とサブワード単位の利点を比較した図

単語単位のtokenizerは直感的ですが、未知語、固有名詞、活用、表記ゆれに弱くなります。

方法 強み 課題
文字単位 未知語に強い token列が長くなりやすい
単語単位 意味のまとまりを扱いやすい 未知語や語彙サイズが問題になる
サブワード単位 未知語に比較的強く、token数も抑えやすい 分割結果が直感とずれることがある

たとえば、tokenization, tokenizer, tokenize は関連語ですが、単語単位では別々の語彙になりがちです。

サブワードなら、token, ization, izer のような部品として扱えます。

日本語ではさらに重要です。

英語は空白で単語境界がある程度分かります。

しかし、日本語や中国語のようなnon-segmented languages (空白で単語境界が明示されない言語)では、 そもそも「先にどこで単語を切るか」が言語依存の問題になります。

SentencePiece論文の強みは、 ここをpre-tokenizationへ依存させない点です。

raw textから直接サブワードモデルを学習できます。

既存手法の課題

従来のpre-tokenization依存とSentencePieceのraw text入力を比較した図

SentencePiece以前にも、 BPE(Byte Pair Encoding、頻出する隣接記号を順に結合する圧縮由来の分割手法)や、 subword-nmtのようなサブワード分割ツールは使われていました。

subword-nmtは、NMT向けにBPEを使う代表的なツールです。

ただし、多くのツールは入力がすでに単語列へ分かれていることを前提にしていました。

課題 内容
言語依存 英語はMoses、日本語はKyTeaのように言語ごとの前処理が必要になる
再現性 前処理ツールのバージョンや設定差で結果が変わりうる
多言語化の難しさ 言語ごとに前処理を管理する必要がある
可逆性の問題 token列から元の空白や記号配置を戻せない場合がある

SentencePieceは、raw textを入力にします。

さらに、空白も通常の記号として扱います。

これにより、この前処理依存を減らします。

日本語ではどう処理されるのか

SentencePieceでは、日本語も英語もUnicode文字列として扱います。

英語のように空白で分かれている言語では、空白を のような記号として保持します。

日本語のように空白がない言語では、文字や頻出する文字列の並びから、BPEまたはUnigramがサブワードを学習します。

たとえば、説明用に単純化すると次のようなイメージです。

入力: 私は機械学習が好きです

候補となる分割例:
[私] [は] [機械学習] [が] [好き] [です]
[私は] [機械] [学習] [が] [好きです]
[私] [は] [機械] [学習] [が] [好き] [です]

実際の分割は、 学習corpus(学習用テキスト集合)でどの文字列がどれくらい頻出するか、 語彙サイズ、モデルタイプによって変わります。

このため、日本語専用の形態素解析器に依存せず、 多言語でも同じ枠組みを使えます。

ただし、これは「日本語の単語境界を必ず正しく理解する」という意味ではありません。

あくまで、 サブワードとして有用な文字列単位をデータから学習するという意味です。

Lossless tokenization:Normalize後を復元する

SentencePiece論文の重要な設計がlossless tokenizationです。

論文で狙う関係は次です。

Decode(Encode(Normalize(text))) = Normalize(text)

ここで復元されるのは、raw textそのものではなくNormalize後の文字列です。

Normalize、Encode、Decodeで正規化後の文字列が復元される流れ

たとえば、全角英数字を半角へ正規化する設定があるとします。

text:
Hello world.

Normalize(text):
Hello world.

Encode(Normalize(text)):
[▁Hello] [▁world] [.]

Decode(...):
Hello world.

この場合、Decode(...)Hello world. へ戻ります。

元の Hello world. には戻りません。

この違いを押さえると、「正規化後の文字列を復元できる」という意味が分かりやすくなります。

BPEとsubword-nmtの基本

BPEが文字から始めて頻出ペアを結合していく流れ

BPEは、最初に文字単位で文を表します。

そのうえで、 corpus内で頻出する隣接ペアを順に結合して語彙を作る手法です。

corpus: low, lower, lowest

初期状態:
l o w
l o w e r
l o w e s t

頻出ペア l + o を結合:
lo w
lo w e r
lo w e s t

次に lo + w を結合:
low
low e r
low e s t

この結果、low のような頻出文字列が1tokenになりやすくなります。

未知語が来ても、 文字や短いサブワードへ分解できます。

そのため、単語単位より扱いやすくなります。

subword-nmtは、このBPEをNMT向けに使う代表的な実装です。

SentencePiece論文では、 subword-nmtのようなツールが「事前に単語分割されたテキスト」を前提にしがちな点を、 課題として扱っています。

BPEとUnigram Language Modelの違い

SentencePieceは、 BPEとUnigram Language Model (候補サブワード集合から文を生成する確率モデル)の両方を実装しています。

ただし、BPEを行ってからUnigramへ入力するわけではありません。

tokenizerモデルを学習するときに、 どちらのモデルタイプを使うかを選びます。

BPEとUnigramは直列ではなく選択式のモデルタイプであることを示す図
観点 BPE Unigram Language Model
基本発想 頻出する隣接ペアを順に結合する 候補サブワードの確率モデルとして扱う
学習の見方 ボトムアップに語彙を作る 大きめの候補から不要な語彙を削る
分割の性質 決定的になりやすい 複数分割を確率的に扱いやすい
subword regularization 直接は扱いにくい サンプリングと相性がよい
直感 圧縮に近い 生成モデルに近い

Unigram Language Modelでは、文がサブワード列から生成される確率を考えます。

文 $X$ に対して、 ある分割 $x = (x_1, x_2, …, x_M)$ の確率は、 簡略化すると次のように書けます。

P(x) = \prod_{i=1}^{M} p(x_i)

たとえば "New York" なら、 次のような複数の分割候補がありえます。

候補A: [▁New] [▁York]
候補B: [▁Ne] [w] [▁York]
候補C: [▁New] [▁Yo] [rk]

推論時はもっとも確率が高い分割を選ぶことが多いです。

一方、学習時には複数候補から確率的にサンプリングします。

同じ文字列でも、 少し異なる分割を見せることがあります。

これがsubword regularizationです。

Unigram Language Modelの学習・データ拡張的なテクニックとして理解すると、 分かりやすいです。

Unigram Language Modelで複数の分割候補を評価・サンプリングする流れ

語彙サイズと推論コスト

Tokenizationでよく出てくる設定が語彙サイズです。

語彙サイズは、tokenizerが扱うtokenの種類数です。

語彙サイズが大きいと、 頻出する長い表現を1tokenとして登録しやすくなります。

そのため、同じ文章でもtoken列は短くなりやすいです。

一方で、語彙サイズが大きいほど、 Embedding行列や出力層のサイズは大きくなります。

語彙サイズとtoken数が推論コストやEmbedding/出力層サイズに影響する流れ
語彙サイズ メリット デメリット
小さい 未知語に強く、語彙表が軽い 1文あたりのtoken数が増えやすい
大きい 短いtoken列になりやすい 語彙表、Embedding、出力層が重くなる
中程度 実用上のバランスを取りやすい 言語・ドメインごとの調整が必要

token数が増えると、LLMが処理する系列長が伸びます。

系列長が伸びると、文脈長を多く消費します。

また、Attention(文中のtoken同士の関係を計算する仕組み)の計算対象も増えます。

そのため、推論レイテンシ、メモリ、API課金などに影響します。

特に日本語は、英語のように空白で単語境界が示されません。

そのため、tokenizerが細かく分割しすぎると、 同じ意味を表すのに多くのtokenが必要になります。

コードは、snake_casecamelCase、括弧、ドット、インデント、 API名などが意味を持ちます。

たとえば get_user_profile() が、 get, _, user, _, profile, (, ) のように適切に分かれる場合があります。

一方で、識別子の途中で不自然に割れる場合もあります。

この違いによって、 モデルが構文や命名規則を扱う難しさが変わります。

実験結果の要約

論文では、KFTT (Kyoto Free Translation Task、京都関連テキストを用いた英日・日英翻訳データセット)を使います。

このデータセットで、 word modelとSentencePieceを比較しています。

主な結果は次の通りです。

比較 論文で示された傾向
Word model vs SentencePiece SentencePieceのサブワード分割はword modelよりBLEUが高い傾向
pre-tokenizationあり vs なし raw textから直接学習しても同等程度、条件によってはpre-tokenizationなしが有利
日本語処理 non-segmented languagesでは、raw textからの教師なし分割が有効に働く可能性
処理速度 raw Japanese textに対するsegmentationでsubword-nmtより大幅に高速な設定が報告されている

注意点として、この実験はNMTでの検証です。

現在のLLM全般の性能をそのまま保証するものではありません。

ただし、言語依存の前処理を減らすという設計思想は、 現在でも重要です。

tokenizerをモデルファイルとして再現可能に管理する点も、 実務で効いてきます。

LLM実装者が見るべきポイント

実装者目線では、Tokenizationは単なる前処理ではありません。

モデルの入力長、コスト、未知語耐性、再現性、 入出力の安定性に関わります。

NFKC、BOS、EOSなどtokenizer周辺の専門用語をまとめたチートシート
観点 確認ポイント
正規化 NFKC、lowercase、独自ルールの有無
空白処理 連続空白、改行、タブをどう扱うか
特殊token BOS、EOS、PAD、UNK、会話テンプレート用token
語彙サイズ ドメイン語彙、コード、日本語でtoken数が増えすぎないか
再現性 tokenizerモデル、正規化ルール、ライブラリバージョンを固定できるか
detokenization 生成後に余計な空白や文字化けが出ないか

特に、Fine-tuningやRAG(検索で外部文書を補う生成方式)では、 学習時・推論時・評価時で同じtokenizerを使うことが重要です。

tokenizerが変わると、同じ文字列でもtoken ID列が変わります。

Embeddingや出力層の語彙対応も変わります。

そのため、原則としてtokenizerだけを気軽に差し替えることはできません。

実装例:BPEとUnigramを比較する

以下は、SentencePieceのAPIを使った最小例です。

同じcorpusからBPEモデルとUnigramモデルを学習し、 同じ文章を分割します。

実行には sentencepiece パッケージが必要です。

import logging
import tempfile
from pathlib import Path
from typing import Literal

import sentencepiece as spm

logger = logging.getLogger(__name__)

ModelType = Literal["bpe", "unigram"]


def train_sentencepiece_model(corpus: list[str], model_type: ModelType, vocab_size: int) -> Path:
    """Train a tiny SentencePiece model for comparing BPE and Unigram.

    Args:
        corpus: Training sentences used as the raw text corpus.
        model_type: SentencePiece model type. Use "bpe" or "unigram".
        vocab_size: Target vocabulary size for the tokenizer model.

    Returns:
        Path to the trained `.model` file.

    Raises:
        ValueError: If corpus is empty or vocab_size is too small.
        RuntimeError: If SentencePiece training fails.

    Examples:
        >>> path = train_sentencepiece_model(["I like New York"], "bpe", 32)
        >>> path.suffix
        '.model'
    """
    if not corpus:
        raise ValueError("corpus must not be empty")
    if vocab_size < 16:
        raise ValueError("vocab_size must be at least 16 for this demo")

    work_dir = Path(tempfile.mkdtemp(prefix=f"spm_{model_type}_"))
    corpus_path = work_dir / "corpus.txt"
    corpus_path.write_text("\n".join(corpus), encoding="utf-8")
    prefix = work_dir / model_type

    logger.info("train SentencePiece model: type=%s vocab_size=%d", model_type, vocab_size)
    # BPEとUnigramの差を見たいので、同じcorpusと語彙サイズでmodel_typeだけを変える。
    spm.SentencePieceTrainer.train(
        input=str(corpus_path),
        model_prefix=str(prefix),
        model_type=model_type,
        vocab_size=vocab_size,
        character_coverage=1.0,
        bos_id=-1,
        eos_id=-1,
        pad_id=-1,
    )
    return prefix.with_suffix(".model")


def encode_with_sentencepiece(model_path: Path, text: str) -> tuple[list[str], list[int]]:
    """Encode text into subword pieces and token IDs with a SentencePiece model.

    Args:
        model_path: Path to a trained SentencePiece `.model` file.
        text: Input text to tokenize.

    Returns:
        A tuple of `(pieces, token_ids)`.

    Raises:
        FileNotFoundError: If model_path does not exist.
        RuntimeError: If the model cannot be loaded.

    Examples:
        >>> pieces, ids = encode_with_sentencepiece(Path("bpe.model"), "I like New York")
        >>> isinstance(pieces, list) and isinstance(ids, list)
        True
    """
    if not model_path.exists():
        raise FileNotFoundError(model_path)

    processor = spm.SentencePieceProcessor(model_file=str(model_path))
    pieces = processor.encode(text, out_type=str)
    token_ids = processor.encode(text, out_type=int)
    logger.info("encoded text: chars=%d pieces=%d", len(text), len(pieces))
    logger.debug("pieces=%s token_ids=%s", pieces, token_ids)
    return pieces, token_ids


def sample_unigram_segmentations(model_path: Path, text: str, samples: int = 3) -> list[list[str]]:
    """Sample multiple Unigram segmentations for the same text.

    Args:
        model_path: Path to a Unigram SentencePiece model.
        text: Input text to tokenize.
        samples: Number of sampled segmentations.

    Returns:
        A list of sampled piece sequences.

    Raises:
        ValueError: If samples is not positive.
        FileNotFoundError: If model_path does not exist.

    Examples:
        >>> sample_unigram_segmentations(Path("unigram.model"), "New York", 2)
        [['▁New', '▁York'], ['▁Ne', 'w', '▁York']]
    """
    if samples <= 0:
        raise ValueError("samples must be positive")
    if not model_path.exists():
        raise FileNotFoundError(model_path)

    processor = spm.SentencePieceProcessor(model_file=str(model_path))
    # Unigramは複数の分割候補を持てるため、学習時の揺らぎを再現しやすい。
    return [
        processor.sample_encode_as_pieces(text, nbest_size=-1, alpha=0.5)
        for _ in range(samples)
    ]

このコードでは、BPEとUnigramを直列に組み合わせていません。

同じcorpusに対して、 model_type だけを変えた2つのtokenizerモデルを作ります。

これにより、分割の違いを比較できます。

よくある誤解

誤解 正確な見方
tokenは単語と同じ tokenは単語、単語の一部、記号、空白を含む単位になりうる
token IDは文字数である token IDは語彙表上の整数インデックス
日本語は文字数が短いので安い tokenizerによっては日本語のtoken数が増え、コストや文脈長を消費しやすい
tokenizerは後から差し替えられる 語彙IDとEmbeddingが対応しているため、通常はモデルとセットで扱う
正規化は細かい前処理にすぎない 表記ゆれ、再現性、復元結果に影響する重要な処理
BPEとUnigramを直列に使う SentencePieceでは基本的にモデルタイプとしてどちらかを選ぶ

技術的な新規性

SentencePiece論文の新規性は、単に「サブワード分割を実装した」ことではありません。

既存のNMTパイプラインには、 言語依存のpre-tokenizationが残っていました。

SentencePiece論文では、 それをできるだけモデル化された処理へ寄せた点が重要です。

新規性 なぜ重要か
raw textから直接サブワードモデルを学習 non-segmented languagesでも同じ枠組みで扱える
lossless tokenization detokenizationの曖昧さを減らし、生成結果を扱いやすくする
正規化ルール込みの自己完結モデル 実験・本番環境での再現性を高める
C++/Python/TensorFlow API オフライン前処理だけでなくオンザフライ処理に組み込みやすい
BPEとUnigramの両対応 用途に応じて分割アルゴリズムを選べる

今後の展望

現在のLLMでは、SentencePiece以外にも、 byte-level BPE、WordPiece、TikToken系の実装などが使われています。

つまり、tokenizerにはさまざまな種類があります。

ただし、根本的な論点は大きく変わっていません。

どの単位で文章を分けるかは、 モデルの学習効率、推論コスト、マルチリンガル性能、 コード処理、長文処理に影響します。

今後は、次のような観点がさらに重要になると考えられます。

観点 内容
多言語公平性 特定言語だけtoken数が極端に多くならない設計
コード・数式対応 プログラムや数式を壊しにくい分割
長文処理 文脈長を無駄遣いしないtokenization
モデル統合 tokenizer、chat template、special tokenの一体管理
byte fallback 未知文字や絵文字を安定して扱う仕組み

まとめ

Tokenizationは、LLMの一番入口にある処理です。

しかし、単なる文字列分割ではありません。

モデルが世界をどの粒度の記号列として見るかを決める、重要な設計要素です。

SentencePiece論文は、 raw textから言語非依存にサブワードモデルを学習する設計を示しました。

また、空白を含めて可逆に扱い、 正規化ルールまでモデルファイルへ含めます。

これにより、 NMTや多言語処理で問題になりがちな言語依存のpre-tokenizationを減らせます。

その結果、 再現性の高いtokenizationを実現しやすくなります。

LLMを使う側にとっても、 tokenizerはコスト、文脈長、Fine-tuning、RAG、評価に関わる基礎部品です。

「文字数」ではなく「token数」で考えることが、LLMアプリケーション設計の第一歩になります。

関連技術

技術 関係
BPE 頻出ペアを結合してサブワード語彙を作る
subword-nmt NMTでBPEを使う代表的なサブワード分割ツール
Unigram Language Model サブワード分割を確率モデルとして扱う
WordPiece BERT系でよく知られるサブワード分割
Embedding token IDをベクトルへ変換する次の処理
Context Length token数で測られる文脈長

次に読むべき記事

次はEmbeddingです。

Tokenizationで得られたtoken IDは、そのまま意味を持つ数値ではありません。

Embedding層を通すことで、token IDはモデルが計算できるベクトルへ変換されます。

コメント

タイトルとURLをコピーしました