翻訳、要約、感情分類、質問応答 — 自然言語処理(NLP)にはさまざまなタスクがありますが、これらをすべて同じ1つのフレームワークで解けるとしたらどうでしょうか? 従来のアプローチでは、分類タスクには分類ヘッドを、生成タスクにはデコーダを、とタスクごとに異なる出力層やアーキテクチャを設計する必要がありました。「タスクが10種類あれば10種類の出力設計が必要」という状況は、研究にも実務にも大きな負担です。
2019年にGoogleが発表したT5(Text-to-Text Transfer Transformer)は、この問題に対して驚くほどシンプルな解決策を提示しました。すべてのNLPタスクを「テキストを入力し、テキストを出力する」という統一フォーマットに変換するのです。翻訳なら "translate English to German: That is good" と入力すれば "Das ist gut" が出力される。分類なら "classify: This movie was great" と入力すれば "positive" というテキストが出力される。出力層のカスタマイズは一切不要です。
T5の設計思想を理解することは、以下のような場面で直接的に役立ちます。
- マルチタスク学習の設計: 1つのモデルで翻訳・要約・分類・QAを同時に学習させるシステムの構築
- 事前学習戦略の理解: Span Corruptionという独自の事前学習タスクと、その後のファインチューニングの流れ
- 最新モデルへの橋渡し: Flan-T5やmT5、UL2など、T5から派生した重要なモデル群を理解するための土台
- Encoder-Decoderの再評価: GPT系のDecoder-onlyモデルが主流の今、なぜEncoder-Decoderが有効な場面があるのかを理解する
さらに、T5の論文(Raffel et al., 2020, “Exploring the Limits of Transfer Learning with a Unified Text-to-Text Transformer”)は、単にモデルを提案するだけでなく、NLPの転移学習に関する体系的な実験をまとめた論文でもあります。アーキテクチャの比較、事前学習タスクの比較、データセットの設計、スケーリングの効果など、NLPの研究者が知るべき知見が凝縮されています。
本記事の内容
- Text-to-Textフレームワークの概念と設計思想
- T5のEncoder-Decoderアーキテクチャ(相対位置バイアス、Pre-Norm)
- Span Corruption事前学習の仕組みと目的関数
- C4データセットの設計とクリーニング方針
- Googleの体系的実験から得られた重要な知見
- T5の後継モデル(Flan-T5, mT5, UL2)
- PyTorchでの相対位置バイアスとSpan Corruptionの実装
- Hugging Face T5を使った推論デモ
前提知識
この記事を読む前に、以下の記事を読んでおくと理解が深まります。
BERTのマスク言語モデル(MLM)やGPTの自己回帰言語モデルの基本を理解していると、T5のSpan Corruptionがなぜそのように設計されたかが自然に理解できます。
それでは、T5の核心である「すべてをテキスト生成に統一する」というアイデアから見ていきましょう。
Text-to-Textフレームワークとは
「統一フォーマット」というアイデア
NLPには多種多様なタスクがあります。翻訳、要約、感情分類、質問応答、文法チェック、文の類似度判定 — これらは従来、それぞれ異なる形式で扱われてきました。分類タスクなら出力はクラスラベル(数値やカテゴリ)、翻訳なら出力はテキスト列、類似度判定なら出力はスコアという具合です。
この多様性は、研究者やエンジニアにとって厄介な問題を引き起こします。タスクごとに出力層を設計し、損失関数を選び、評価指標を決める必要があるからです。BERT系のモデルでは、事前学習済みのEncoderの上にタスク固有の「ヘッド」(線形層 + Softmax)を載せ替える方式が主流でしたが、これでも「どのようなヘッドを設計するか」はタスクごとに考える必要がありました。
T5のText-to-Textフレームワークは、この問題をエレガントに解決します。あらゆるNLPタスクを次の形式に統一するのです。
$$ \text{テキスト入力} \longrightarrow \text{テキスト出力} $$
入力テキストの先頭にタスクプレフィックス(task prefix)を付けることで、モデルに「何をすべきか」を伝えます。出力は常にテキストです。分類であっても、数値やカテゴリIDではなく、"positive" や "entailment" のようなテキスト文字列を出力させます。
具体例で理解する
Text-to-Textフレームワークの具体例を見てみましょう。
英独翻訳
入力: "translate English to German: That is good."
出力: "Das ist gut."
感情分類
入力: "sst2 sentence: This movie was absolutely wonderful."
出力: "positive"
文の要約
入力: "summarize: The quick brown fox jumps over the lazy dog. The dog was sleeping peacefully in the garden..."
出力: "A fox jumped over a sleeping dog in a garden."
質問応答(SQuAD形式)
入力: "question: What is the capital of France? context: France is a country in Western Europe. Its capital city is Paris."
出力: "Paris"
文の含意関係判定(NLI)
入力: "mnli premise: A man is playing guitar. hypothesis: Someone is making music."
出力: "entailment"
回帰タスク(STS-B: 文の類似度)
入力: "stsb sentence1: The cat sat on the mat. sentence2: A cat is sitting on a mat."
出力: "5.0"
最後の例が特に興味深いのは、回帰タスク(連続値の予測)すらもテキスト出力として扱っている点です。類似度スコアの 5.0 は数値ではなく、文字列 "5.0" として生成されます。一見非効率に思えますが、これによりすべてのタスクで同じモデル、同じ損失関数(トークン単位のクロスエントロピー)、同じデコーディング手順を使えるという大きな利点が生まれます。
BERTやGPTとの設計思想の違い
Text-to-Textフレームワークの位置付けをより明確にするために、BERTとGPTのアプローチと比較しましょう。
BERT(Encoder-only) は、入力テキストを双方向に読んで文脈表現を構築し、その表現の上にタスク固有のヘッドを載せる方式です。分類なら [CLS] トークンの表現に線形層を接続し、固有表現抽出なら各トークンの表現にラベル分類層を載せます。事前学習タスクはMLM(Masked Language Modeling)で、ランダムにマスクされたトークンを予測します。BERTは「理解」に特化したモデルであり、テキスト生成は不得意です。
GPT(Decoder-only) は、左から右へテキストを自己回帰的に生成するモデルです。次のトークンを予測することで学習し、プロンプトに続くテキストを生成できます。GPT-2以降は、タスクプレフィックスを使ってタスクを指定するアイデア(zero-shot / few-shot)が登場しましたが、アーキテクチャは一方向のDecoderのみであり、入力の双方向理解はできません。
T5(Encoder-Decoder) は、両者の利点を組み合わせます。Encoderが入力テキスト全体を双方向に理解し、Decoderがその理解を基にテキストを自己回帰的に生成します。これにより、「理解」と「生成」の両方が必要なタスク(翻訳、要約、QAなど)で優れた性能を発揮します。
| 特徴 | BERT | GPT | T5 |
|---|---|---|---|
| アーキテクチャ | Encoder-only | Decoder-only | Encoder-Decoder |
| 入力の処理 | 双方向 | 一方向(左→右) | 双方向(Encoder) |
| 出力の形式 | タスク固有ヘッド | テキスト生成 | テキスト生成 |
| 事前学習 | MLM + NSP | 自己回帰LM | Span Corruption |
| タスク適応 | ヘッド載せ替え | プロンプト設計 | Text-to-Text統一 |
| 得意分野 | 理解・分類 | 生成 | 理解+生成 |
T5の核心は、「出力を常にテキストにする」という割り切りです。分類の正解ラベルも "positive" という文字列にしてしまえば、分類も翻訳も要約も全て「テキスト生成」として同じパイプラインで処理できます。この統一性こそが、T5がマルチタスク学習やスケーリングで成功した理由の一つです。
ここまでで、T5が「何をするモデルか」が明確になりました。次に、このText-to-Textフレームワークを実現する具体的なアーキテクチャ — つまり「どのように実現するか」を見ていきましょう。
T5のアーキテクチャ
標準的なEncoder-Decoder Transformer
T5のアーキテクチャは、原論文(Vaswani et al., 2017)のTransformerとほぼ同じEncoder-Decoder構造を採用しています。革新的なアーキテクチャの提案ではなく、既存のEncoder-Decoderをそのまま使い、「何を入力し、何を出力するか」のフレームワークを変えたところにT5のポイントがあります。
処理の流れは以下のとおりです。
- 入力テキスト(タスクプレフィックス付き)をトークン化し、Encoderに入力する
- Encoderが入力系列全体を双方向Self-Attentionで処理し、文脈表現を生成する
- DecoderがEncoderの出力をCross-Attentionで参照しながら、出力テキストを自己回帰的に生成する
- 学習時はteacher forcing(正解の出力系列をDecoderに与える)を使用する
図式的に表すと以下のようになります。
入力: "translate English to German: That is good."
[トークン化]
↓
┌───────────────────────────┐
│ Encoder (双方向Self-Attn) │
│ × N layers │
└───────────────────────────┘
↓ 文脈表現
↓ (Cross-Attentionで参照)
┌──────────────────────────────┐
│ Decoder (因果Self-Attn │
│ + Cross-Attn) │
│ × N layers │
└──────────────────────────────┘
↓
[語彙上のSoftmax]
↓
出力: "Das ist gut."
ただし、オリジナルのTransformerからいくつかの変更点があります。これらの変更はそれぞれ明確な理由を持っています。
相対位置バイアス(Relative Position Bias)
オリジナルのTransformerでは、トークンの位置情報をsin/cos関数で計算した固定的な位置エンコーディングとして入力埋め込みに加算していました。T5では、この方式を使わず、相対位置バイアス(Relative Position Bias)を採用しています。
位置エンコーディングが「各トークンに絶対位置の情報を付与する」のに対し、相対位置バイアスは「2つのトークン間の距離に応じたバイアスをAttentionスコアに直接加算する」アプローチです。
直感的に言えば、これは次のような考え方です。文を読むとき、「3番目の単語と5番目の単語」という絶対的な位置よりも、「2つ隣の単語」という相対的な距離の方が言語的に重要な情報です。「The cat sat on the mat」という文で、”sat” と “cat” の関係は、文のどの位置に現れても「1つ隣」という関係は変わりません。相対位置バイアスは、この直感を直接モデルに組み込みます。
標準的なScaled Dot-Product Attentionでは、Attentionスコアを次のように計算します。
$$ \text{Attention}(\bm{Q}, \bm{K}, \bm{V}) = \text{softmax}\left(\frac{\bm{Q}\bm{K}^T}{\sqrt{d_k}}\right)\bm{V} $$
T5の相対位置バイアスでは、Softmaxの引数にバイアス項 $\bm{B}$ を加えます。
$$ \text{Attention}(\bm{Q}, \bm{K}, \bm{V}) = \text{softmax}\left(\frac{\bm{Q}\bm{K}^T}{\sqrt{d_k}} + \bm{B}\right)\bm{V} $$
ここで $\bm{B} \in \mathbb{R}^{n \times n}$ はバイアス行列で、$B_{ij}$ は位置 $i$ のトークンから位置 $j$ のトークンへのバイアスを表します。このバイアスは、相対位置 $i – j$ によって決まります。
しかし、相対位置をそのまま使うと、文が長くなるほどバイアスの種類が際限なく増えます。位置 $0$ と位置 $1000$ の距離は $1000$ ですが、距離 $999$ との区別がそこまで重要かというと、そうではありません。T5では、この問題をバケット化(bucketing)で解決します。
近い距離は細かく区別し、遠い距離は粗く区別するバケット化を行います。具体的には、距離 $0$ から $n_{\text{exact}}$(論文では $n_{\text{exact}}=16$ 前後に設定)までは1対1でバケットに割り当て、それ以降は対数スケールでバケットに割り当てます。この設計により、有限個のバケット数(T5-Baseでは32個)で、理論上任意の長さの系列を扱えます。
バケットインデックスを $b(i, j)$ とすると、バイアスは学習可能な埋め込みテーブルから取得されます。
$$ B_{ij} = \text{embedding}[b(i – j)] $$
この埋め込みテーブルはAttentionヘッドごとに独立しています。つまり、異なるヘッドが異なる距離パターンに注目できるようになっています。
もう一つ重要な点として、T5の相対位置バイアスは最初のEncoder層とDecoder層でのみ計算され、残りの層では同じバイアスが共有されます。これはパラメータ効率の観点からの設計選択です。
Pre-Norm(Pre-Layer Normalization)
オリジナルのTransformerでは、各サブレイヤー(Self-Attention や FFN)の後に残差接続とLayer Normalizationを適用していました(Post-Norm)。
$$ \text{Post-Norm: } \bm{x} + \text{LayerNorm}(\text{SubLayer}(\bm{x})) $$
T5では、SubLayerの前にLayer Normalizationを適用するPre-Normを採用しています。
$$ \text{Pre-Norm: } \bm{x} + \text{SubLayer}(\text{LayerNorm}(\bm{x})) $$
この違いは一見些細に見えますが、深いネットワークの学習安定性に大きく影響します。Pre-Normでは、残差接続のパスを通る勾配がLayer Normalizationを経由しないため、勾配がより安定して逆伝播できます。特に層数が多いモデル(T5-11Bは24層)では、この安定性が学習の成否を左右します。
また、T5のLayer NormalizationはRMSNorm(Root Mean Square Layer Normalization)に近い実装で、平均の引き算を省略しています。通常のLayer Normは $\hat{x} = \frac{x – \mu}{\sigma}$ と正規化しますが、T5では平均の中心化を行わず、RMSで割るだけです。
$$ \text{RMSNorm}(\bm{x}) = \frac{\bm{x}}{\text{RMS}(\bm{x})} \odot \bm{\gamma} $$
ここで、RMSは二乗平均平方根です。
$$ \text{RMS}(\bm{x}) = \sqrt{\frac{1}{d}\sum_{i=1}^{d}x_i^2} $$
この簡略化により、計算コストが僅かに削減されつつ、実質的な性能低下がないことが経験的に確認されています。
パラメータ構成
T5にはパラメータ共有のようなトリックは使われておらず、EncoderとDecoderは独立したパラメータを持ちます。これはAlbert(BERT系の軽量モデル)がEncoder内の全層でパラメータを共有したのとは対照的です。T5は「シンプルなアーキテクチャを大規模に訓練する」という方針を取っており、パラメータ効率よりも表現力を優先しています。
ただし、例外として入力埋め込み行列はEncoderとDecoderで共有されます。これは、入力と出力が同じ語彙空間を使うため、自然な設計選択です。Decoderの最終出力層(語彙への射影)でも同じ埋め込み行列が転置して再利用されます(weight tying)。
T5-Baseモデルの主要パラメータをまとめると、以下のようになります。
| パラメータ | T5-Base |
|---|---|
| Encoder層数 | 12 |
| Decoder層数 | 12 |
| 隠れ層の次元 $d_{\text{model}}$ | 768 |
| FFN内部次元 $d_{\text{ff}}$ | 3072 |
| Attentionヘッド数 | 12 |
| キー/バリューの次元 $d_k = d_v$ | 64 |
| 相対位置バケット数 | 32 |
| 最大相対位置距離 | 128 |
| 語彙サイズ | 32,128(SentencePiece) |
| 総パラメータ数 | 約220M |
ここまでで、T5のアーキテクチャの全体像が見えてきました。次に、このアーキテクチャをどのように事前学習するか — T5のもう一つの重要な要素であるSpan Corruption事前学習を見ていきましょう。
Span Corruption事前学習
マスクの粒度という問題
事前学習タスクの設計は、モデルの最終的な性能を大きく左右します。BERTのMLM(Masked Language Modeling)は、入力テキストのトークンをランダムに15%マスクし、そのトークンを予測する方式でした。この方法は強力ですが、1つ気になる点があります — マスクの粒度が1トークンに限定されている点です。
自然言語では、意味の単位はしばしば複数トークンにまたがります。「New York」「machine learning」「in order to」のようなフレーズは、個々のトークンではなくまとまりとして理解すべきです。1トークンずつ独立にマスクするMLMでは、こうしたフレーズレベルの理解を直接促す力が弱いと考えられます。
また、BERTのMLMには構造的な問題もあります。BERTはEncoder-onlyモデルなので、マスクされたトークンの予測は各位置で独立に行われます。つまり、マスクされたトークン同士の依存関係(例えば「New [MASK]」で「York」を予測する際に、近くの別のマスクの予測結果を参照すること)は直接モデル化されません。
T5のSpan Corruptionは、これらの問題に対処するために設計されました。
Span Corruptionの仕組み
Span Corruptionでは、入力テキストから連続するトークンのスパン(span) をランダムに選択し、そのスパン全体を1つのsentinelトークン(特殊トークン)で置換します。Decoderは、sentinelトークンとともにマスクされた部分を復元します。
具体例で見てみましょう。
元のテキスト:
"The quick brown fox jumps over the lazy dog in the park"
マスク後の入力(Encoderへ):
"The <extra_id_0> fox jumps <extra_id_1> the park"
ターゲット出力(Decoderが生成):
"<extra_id_0> quick brown <extra_id_1> over the lazy dog in"
ここで、<extra_id_0> と <extra_id_1> がsentinelトークンです。2つのスパン(”quick brown” と “over the lazy dog in”)がそれぞれ1つのsentinelトークンに置換されています。
Decoderの出力では、各sentinelトークンの後にそのスパンの元の内容が続きます。最後に文末トークンが付加されます。この形式により、Decoderは「どのsentinelがどのスパンに対応するか」を明示的に出力する必要があり、構造的な理解が促されます。
目的関数
Span Corruptionの目的関数は、標準的な条件付き言語モデリングの損失関数です。sentinelトークンを含むターゲット系列 $\bm{y} = (y_1, y_2, \dots, y_m)$ に対して、以下のクロスエントロピー損失を最小化します。
$$
\mathcal{L} = -\sum_{t=1}^{m} \log P(y_t \mid y_{ ここで $\bm{x}$ はsentinelトークンで置換された入力系列、$y_{ この損失関数はGPTの自己回帰損失と同じ形をしていますが、Encoderからの情報 $\bm{x}$ が条件に含まれている点が異なります。Decoderは、Encoderが入力系列から抽出した文脈表現をCross-Attentionで参照しながら、マスクされた部分を復元していきます。 Span Corruptionのハイパーパラメータとして、以下の2つが重要です。 スパン長は幾何分布からサンプリングされます。平均スパン長が3の場合、短いスパンが多く、長いスパンは稀になります。この設定は体系的な実験によって選ばれたもので、単一トークンのマスク(BERT式)やランダムな固定長マスクと比較して、下流タスクでの性能が最も良かった組み合わせです。 Span Corruptionと BERTのMLMの違いを整理しましょう。 特に重要なのは出力長の違いです。BERTのMLMでは入力と同じ長さの出力を生成しますが、T5のSpan Corruptionでは、マスクされた部分(+sentinelトークン)のみを生成すればよいため、Decoderの出力系列が大幅に短くなります。マスク率15%、平均スパン長3の場合、ターゲット系列は元のテキストの約15%程度の長さに圧縮されます。これは計算効率の面で大きな利点です — Decoderのself-attentionは $O(m^2)$ のコストがかかるため、出力長 $m$ が短いほど計算量が削減されます。 ここまでで事前学習のタスク設計が明らかになりました。次に、この事前学習で使用されるデータセット — T5のために特別に構築されたC4について見ていきましょう。 近年の大規模言語モデルの研究では、「モデルのアーキテクチャ」と「学習データの質と量」のどちらが性能に大きく影響するかという議論が盛んです。T5の論文は、この問いに対してデータの質が極めて重要であることを体系的に示しました。 T5の事前学習に使われたのは、C4(Colossal Clean Crawled Corpus) と呼ばれる、約750GBのクリーンなテキストデータセットです。これはCommon Crawl(ウェブ全体のクロールデータ)を出発点として、厳格なクリーニングを施したものです。 Common Crawlは膨大な量のウェブデータを含みますが、そのままでは品質が低く、言語モデルの学習に適しません。HTML内のボイラープレート、重複コンテンツ、卑猥な表現、コードスニペット、非自然言語テキストなど、大量のノイズが含まれています。 C4のクリーニングでは、以下のフィルタリングが適用されました。 これらのフィルタリングの結果、Common Crawlの膨大なデータが約750GBにまで絞り込まれました。この数字は大きく見えますが、元のCommon Crawlと比較すると大幅に圧縮されています。 T5の論文では、C4と他のデータソースを比較した実験も行われています。その結果、以下の知見が得られました。 この結果は、「ゴミを入れればゴミが出る(Garbage In, Garbage Out)」という原則がそのまま当てはまります。モデルのアーキテクチャが優れていても、学習データの質が低ければ性能は頭打ちになるのです。 C4の設計思想は、後続の多くのデータセット(The Pile、RefinedWeb、RedPajama等)に影響を与え、大規模言語モデルのデータ前処理のベースラインとなりました。 データセットの話はここまでにして、次はT5の論文のもう一つの大きな貢献 — NLPの転移学習に関する体系的な実験結果を見ていきましょう。 T5の論文は67ページにも及ぶ長大な論文で、単なるモデル提案ではなく、NLPの転移学習に関する包括的なサーベイ兼実験レポートという性格を持っています。「どのアーキテクチャが良いか」「どの事前学習タスクが良いか」「どのくらいのデータが必要か」「モデルを大きくすると何が起きるか」といった根本的な問いに、大規模な計算資源を投入して答えを出しています。 以下に、論文のSection 3から得られた重要な知見をまとめます。 T5の論文では、以下の3つのアーキテクチャ変種を公平な条件で比較しています。 比較の結果、以下のことが明らかになりました。 Encoder-Decoderが最も高性能でした。特に、EncoderとDecoderのパラメータを合計した場合でも、同じパラメータ数のDecoder-onlyモデルを上回りました。これは注目に値します。Encoder-Decoderモデルは、EncoderとDecoderにそれぞれ別のパラメータを持つため、合計パラメータ数が Decoder-onlyの2倍に近くなる場合がありますが、パラメータ数を揃えた比較でもEncoder-Decoderが優位でした。 直感的にこの結果を理解すると、入力の理解(Encoder)と出力の生成(Decoder)を別々のモジュールで担当させることで、それぞれが自分の役割に集中できるからと考えられます。Decoder-onlyモデルでは、入力の理解と出力の生成を同じパラメータで行わなければなりません。 Prefix LMはDecoder-onlyより良いが、Encoder-Decoderには及ばない結果でした。Prefix LMは入力部分で双方向Attentionを許可するため、入力の理解がDecoder-onlyより優れますが、EncoderとDecoderが構造的に分離されていないため、Encoder-Decoderの明確な役割分担には劣ります。 T5の論文では、以下の事前学習タスクを比較しています。 結果として、Span Corruptionが最も良い成績を記録しました。特に、BERT式MLMとSpan Corruptionの差は、「マスクの粒度(1トークン vs スパン)」と「出力の効率性(入力長 vs マスク部分のみ)」に起因すると考えられます。 Span Corruptionのマスク率と平均スパン長についても体系的に実験が行われ、マスク率15%、平均スパン長3の組み合わせが最適であることが示されました。マスク率を上げすぎるとEncoderに残る情報が少なくなりすぎ、下げすぎるとDecoderが学習するシグナルが少なくなります。平均スパン長については、短すぎると単語レベルの知識しか学べず、長すぎるとタスクが難しすぎて学習が非効率になります。 T5は5つのサイズで実験されており、モデルを大きくすることの効果を定量的に示しています。 この表から読み取れる興味深い点があります。T5-3BからT5-11Bへのスケールアップでは、層数と $d_{\text{model}}$ は変えず、FFN内部次元 $d_{\text{ff}}$ とヘッド数だけを増やしています。これは、モデルの「深さ」よりも「幅」を広げる方がスケーリング効率が良いという知見を反映しています。 実験の結果、モデルサイズを大きくするほど下流タスクの性能は一貫して向上しました。ただし、性能向上の幅は逓減します(対数スケールでの向上)。T5-11Bは当時のSuperGLUEベンチマークで人間のベースラインを初めて超えたモデルの一つとなりました。 これらの実験結果は、T5のモデル自体が評価されるだけでなく、NLPの転移学習全般に対する実用的なガイドラインとして広く参照されています。 T5の体系的な実験から多くの知見が得られましたが、T5の物語はここで終わりません。T5の設計を基盤として、さまざまな改良モデルが提案されています。次に、これらの後継モデルを概観しましょう。 Flan-T5(2022, Google)は、T5にInstruction Tuning(指示チューニング)を施したモデルです。Instruction Tuningとは、「入力に自然言語の指示を含め、その指示に従った出力を学習させる」ファインチューニング手法です。 T5のText-to-Textフレームワークでは、タスクプレフィックス(”translate English to German:” 等)が固定的なフォーマットでした。Flan-T5では、これをより自然な指示文に拡張します。 Flan-T5は1,800以上のタスクを473のデータセットからまとめ、それぞれに複数のInstruction Templateを用意してファインチューニングされました。この結果、T5と比較して以下の改善が見られました。 Flan-T5は、ChatGPTの登場以前に「指示に従うAI」の可能性を示した重要なモデルの一つであり、後のInstruction Tuning研究に大きな影響を与えました。 mT5(2021, Google)は、T5のアーキテクチャをそのまま多言語に拡張したモデルです。C4の多言語版であるmC4データセット(101言語をカバー)で事前学習されています。 mT5のアーキテクチャはT5と完全に同一で、変更点は以下の2つだけです。 mT5は、多言語のText-to-Textモデルとして、翻訳、多言語QA、多言語要約などのタスクで広く使われています。特に、低リソース言語(学習データが少ない言語)でも、高リソース言語からの知識転移により、一定の性能が得られることが示されました。 UL2(Unifying Language Learning Paradigms)(2022, Google)は、T5のSpan Corruptionをさらに一般化したモデルです。UL2の核心アイデアは、複数のデノイジングタスクを混合して事前学習することです。 具体的には、以下の3種類のデノイジングタスクを混合します。 これら3種類のデノイジングを混合することで、BERT的な理解力、GPT的な生成力、そして長距離の依存関係の把握を同時に学習できます。UL2は、Span Corruptionの「一つのタスクで事前学習する」というT5のアプローチを、「複数のタスクで多角的に学習する」方向に発展させたモデルと位置付けられます。 UL2の実験では、NLUタスク(理解)とNLGタスク(生成)の両方で、T5やGPT系のモデルを上回る性能が報告されています。 ここまでで、T5の理論的な全体像と、その後の発展が整理できました。次はいよいよ手を動かして、T5の主要コンポーネントをPyTorchで実装していきましょう。 まず、T5の特徴的な要素である相対位置バイアスをPyTorchで実装します。相対位置 $i – j$ をバケット化し、学習可能な埋め込みテーブルからバイアス値を取得する仕組みです。 このコードの動作を確認してみましょう。 左のヒートマップから、対角線付近(近い位置同士)ではバケットが細かく変化し、対角線から離れる(遠い位置同士)ほどバケットの変化が緩やかになる様子が確認できます。右のグラフは、相対位置をバケットインデックスに変換する関数の形状です。原点付近は線形に増加し(1対1の割り当て)、距離が大きくなると対数的に飽和していく特性が見て取れます。この「近い距離は精密に、遠い距離はざっくり」という設計が、有限個のバケットで長距離の位置関係を効率的に表現することを可能にしています。 次に、T5の事前学習で使われるSpan Corruptionの前処理を実装します。テキストをトークン列に変換した後、ランダムにスパンをマスクし、入力系列とターゲット系列を生成します。 実行すると、20トークンの入力から約15%(3トークン程度)がマスクされ、sentinelトークンで置換された入力系列と、sentinelトークン付きのターゲット系列が生成されます。入力系列は元のトークン列より短くなり(スパンが1つのsentinelに圧縮されるため)、ターゲット系列はマスクされた部分のみを含むため、元のテキスト全体よりも大幅に短くなります。これがSpan Corruptionの計算効率上の利点です。 T5のText-to-Textフレームワークでは、タスクプレフィックスを付けてモデルに入力します。以下に、主要なNLPタスクのフォーマッタを実装します。 出力を見ると、すべてのタスクが「テキスト入力 → テキスト出力」の統一フォーマットに変換されていることがわかります。タスクプレフィックスは固定的な文字列であり、モデルはこのプレフィックスを見て「今何をすべきか」を判断します。この設計のおかげで、新しいタスクを追加する際にモデルの構造を変更する必要がなく、プレフィックスの設計とファインチューニングデータの準備だけで対応できます。 最後に、Hugging Face Transformersライブラリを使って、事前学習済みのT5モデルで実際に推論を行うデモを実装します。要約、翻訳、分類の3つのタスクを1つのモデルで実行します。 このコードを実行すると、60Mパラメータの小さなT5-Smallモデルであっても、翻訳と要約のタスクに応答できることが確認できます。重要なのは、翻訳も要約もまったく同じモデル、同じ ただし、T5-Smallは60Mパラメータと小さいため、出力の品質は限定的です。T5-BaseやT5-Largeを使えば大幅に品質が向上しますが、メモリと計算時間のトレードオフがあります。実務では、タスクの要求精度とリソース制約に応じてモデルサイズを選択することになります。 続いて、T5のEncoder-Decoderの内部構造をより詳しく理解するために、Attentionの重みを可視化してみましょう。 8つのAttentionヘッドの重みを可視化すると、ヘッドによって注目パターンが大きく異なることが確認できます。あるヘッドは隣接トークンに強く注目し(局所的な文法構造の把握)、別のヘッドは離れたトークン間に注目する(長距離の意味的関係の把握)といったパターンが観察されます。特に “translate” や “English” のようなタスクプレフィックスのトークンと、本文のトークンの間のAttentionパターンは興味深く、モデルが「何をすべきか」の情報をどのように伝播させているかの手がかりを与えてくれます。これは、Attentionヘッドごとに異なる相対位置バイアスを学習していることの効果でもあります。 本記事では、T5(Text-to-Text Transfer Transformer)のアーキテクチャと設計思想について解説しました。 T5の最大の貢献は、「統一されたフレームワークの力」を示したことです。タスクごとに異なるアーキテクチャを設計するのではなく、シンプルなEncoder-DecoderにText-to-Textフォーマットを組み合わせ、大規模なデータで事前学習するというアプローチが、驚くほど広範なタスクで有効であることを証明しました。この思想は、その後のLLM研究(GPT-3のfew-shot learning、Flan系のinstruction tuning、PaLMのスケーリング)にも大きな影響を与えています。 次のステップとして、以下の記事も参考にしてください。ハイパーパラメータ
MLMとの違い
特徴
BERT MLM
T5 Span Corruption
マスク単位
1トークン
連続スパン(平均3トークン)
マスク率
15%
15%
出力方式
各位置で独立に予測
自己回帰的に生成
出力の依存関係
マスク間の依存なし
前のトークンの予測が次に影響
出力長
入力長と同じ
マスク部分のみ(短い)
sentinelトークン
なし
あり
アーキテクチャ
Encoder-only
Encoder-Decoder
C4(Colossal Clean Crawled Corpus)
データセットの重要性
クリーニングの方針
"Javascript must be enabled" のようなエラーメッセージページを排除するためのヒューリスティックですlangdetect ライブラリを使い、英語と判定されたページのみを保持(C4は英語データセット)データの質が性能に与える影響
Googleの体系的実験
論文の特異な位置付け
アーキテクチャの比較
事前学習タスクの比較
モデルサイズとスケーリング
モデル
パラメータ数
Encoder/Decoder層数
$d_{\text{model}}$
$d_{\text{ff}}$
ヘッド数
T5-Small
60M
6/6
512
2048
8
T5-Base
220M
12/12
768
3072
12
T5-Large
770M
24/24
1024
4096
16
T5-3B
3B
24/24
1024
16384
32
T5-11B
11B
24/24
1024
65536
128
その他の重要な知見
T5の後継モデル
Flan-T5(Instruction Tuning版)
T5のプレフィックス:
"summarize: {記事テキスト}"
Flan-T5の指示:
"Summarize the following article in 2-3 sentences: {記事テキスト}"
"Write a brief summary of the text below: {記事テキスト}"
"What are the key points of this article? {記事テキスト}"
mT5(多言語版)
UL2(Mixture of Denoisers)
PyTorchでの実装
相対位置バイアスの実装
import torch
import torch.nn as nn
import math
class T5RelativePositionBias(nn.Module):
"""T5の相対位置バイアスモジュール"""
def __init__(self, num_buckets=32, max_distance=128, num_heads=8):
super().__init__()
self.num_buckets = num_buckets
self.max_distance = max_distance
self.num_heads = num_heads
# ヘッドごとにバケット数分の学習可能なバイアス
self.relative_attention_bias = nn.Embedding(num_buckets, num_heads)
@staticmethod
def _relative_position_bucket(relative_position, num_buckets=32, max_distance=128):
"""
相対位置をバケットインデックスに変換する。
近い距離は細かく、遠い距離は対数スケールで粗く区別する。
"""
# 負の相対位置(前方参照)と正の相対位置(後方参照)を分ける
# 半分のバケットを負の方向に、半分を正の方向に割り当て
num_buckets_half = num_buckets // 2
# 符号に応じてオフセットを決定
# relative_position > 0 なら num_buckets_half を加算
offset = (relative_position > 0).long() * num_buckets_half
relative_position = relative_position.abs()
# 近い距離は1対1でバケットに割り当て
max_exact = num_buckets_half // 2 # 8
is_small = relative_position < max_exact
# 遠い距離は対数スケールでバケットに割り当て
relative_position_if_large = max_exact + (
torch.log(relative_position.float() / max_exact)
/ math.log(max_distance / max_exact)
* (num_buckets_half - max_exact)
).long()
# 最大バケットインデックスでクリップ
relative_position_if_large = torch.clamp(
relative_position_if_large, max=num_buckets_half - 1
)
# 近い距離はそのまま、遠い距離は対数バケットを使用
bucket = torch.where(is_small, relative_position, relative_position_if_large)
bucket = bucket + offset
return bucket
def forward(self, query_length, key_length, device=None):
"""バイアス行列を計算して返す"""
if device is None:
device = self.relative_attention_bias.weight.device
# 位置インデックスの生成
context_position = torch.arange(query_length, dtype=torch.long, device=device)[:, None]
memory_position = torch.arange(key_length, dtype=torch.long, device=device)[None, :]
# 相対位置: (query_length, key_length)
relative_position = memory_position - context_position
# バケット化: (query_length, key_length)
buckets = self._relative_position_bucket(
relative_position,
num_buckets=self.num_buckets,
max_distance=self.max_distance
)
# 埋め込みテーブルからバイアスを取得: (query_length, key_length, num_heads)
values = self.relative_attention_bias(buckets)
# (1, num_heads, query_length, key_length) に変形(バッチ次元を追加)
values = values.permute(2, 0, 1).unsqueeze(0)
return values
import torch
import matplotlib.pyplot as plt
# 相対位置バイアスモジュールの作成
bias_module = T5RelativePositionBias(num_buckets=32, max_distance=128, num_heads=8)
# バケット化の挙動を可視化
seq_len = 64
positions = torch.arange(seq_len)
relative_pos = positions[None, :] - positions[:, None]
buckets = T5RelativePositionBias._relative_position_bucket(relative_pos, num_buckets=32, max_distance=128)
fig, axes = plt.subplots(1, 2, figsize=(14, 5))
# バケットインデックスのヒートマップ
im0 = axes[0].imshow(buckets.numpy(), cmap='viridis', aspect='auto')
axes[0].set_xlabel('Key position')
axes[0].set_ylabel('Query position')
axes[0].set_title('Relative Position Bucket Indices')
plt.colorbar(im0, ax=axes[0])
# 1次元の相対位置 → バケットの対応
distances = torch.arange(-63, 64)
bucket_1d = T5RelativePositionBias._relative_position_bucket(distances, num_buckets=32, max_distance=128)
axes[1].plot(distances.numpy(), bucket_1d.numpy(), 'b-', linewidth=1.5)
axes[1].set_xlabel('Relative Position (i - j)')
axes[1].set_ylabel('Bucket Index')
axes[1].set_title('Relative Position to Bucket Mapping')
axes[1].grid(True, alpha=0.3)
axes[1].axvline(x=0, color='r', linestyle='--', alpha=0.5)
plt.tight_layout()
plt.savefig('t5_relative_position_buckets.png', dpi=150, bbox_inches='tight')
plt.show()
Span Corruptionの前処理実装
import torch
import numpy as np
def create_span_corruption(
token_ids,
corruption_rate=0.15,
mean_span_length=3,
sentinel_start_id=32099,
eos_id=1
):
"""
T5 Span Corruptionの前処理を行う。
Parameters:
token_ids: 元のトークンID列 (list or 1D array)
corruption_rate: マスクするトークンの割合
mean_span_length: スパンの平均長(幾何分布のパラメータ)
sentinel_start_id: sentinelトークンの開始ID(降順に使用)
eos_id: 終端トークンID
Returns:
input_ids: Encoderへの入力(sentinelで置換済み)
target_ids: Decoderのターゲット(sentinelとマスク内容)
"""
token_ids = np.array(token_ids)
length = len(token_ids)
# マスクするトークン数
num_to_mask = max(1, int(length * corruption_rate))
# 幾何分布でスパン長をサンプリング
# p = 1/mean_span_length として幾何分布からサンプル
p = 1.0 / mean_span_length
# スパンの開始位置とスパン長を決定
mask = np.zeros(length, dtype=bool)
masked_count = 0
span_infos = [] # (start, length) のリスト
while masked_count < num_to_mask:
# スパン長をサンプリング(最低1)
span_len = max(1, np.random.geometric(p))
span_len = min(span_len, num_to_mask - masked_count)
# 開始位置をランダムに選択(未マスク位置から)
available = np.where(~mask)[0]
if len(available) == 0:
break
start = np.random.choice(available)
end = min(start + span_len, length)
actual_len = end - start
# 既にマスクされている部分と重ならないように調整
if mask[start:end].any():
continue
mask[start:end] = True
span_infos.append((start, actual_len))
masked_count += actual_len
# スパンを開始位置でソート
span_infos.sort(key=lambda x: x[0])
# 入力系列の構築(マスクされたスパンをsentinelトークンに置換)
input_ids = []
target_ids = []
sentinel_id = sentinel_start_id
prev_end = 0
for span_start, span_len in span_infos:
# スパンの前の非マスク部分をそのまま追加
input_ids.extend(token_ids[prev_end:span_start].tolist())
# sentinelトークンを挿入
input_ids.append(sentinel_id)
# ターゲットにsentinelトークンとスパンの内容を追加
target_ids.append(sentinel_id)
target_ids.extend(token_ids[span_start:span_start + span_len].tolist())
sentinel_id -= 1 # 次のsentinelは1つ小さいIDを使用
prev_end = span_start + span_len
# 残りの非マスク部分を追加
input_ids.extend(token_ids[prev_end:].tolist())
# ターゲットの最後にEOSトークンを追加
target_ids.append(eos_id)
return input_ids, target_ids
# 動作確認
np.random.seed(42)
# 簡易的なトークン列(実際にはSentencePieceで生成される)
# 0-99のIDでダミートークンを作成
original_tokens = list(range(10, 30)) # 20トークンの文
print(f"元のトークン列 (長さ {len(original_tokens)}):")
print(original_tokens)
input_ids, target_ids = create_span_corruption(
original_tokens,
corruption_rate=0.15,
mean_span_length=3
)
print(f"\n入力系列 (長さ {len(input_ids)}):")
print(input_ids)
print(f"\nターゲット系列 (長さ {len(target_ids)}):")
print(target_ids)
Text-to-Textのフォーマッタ
def format_translation(source_text, source_lang="English", target_lang="German"):
"""翻訳タスクのフォーマット"""
return f"translate {source_lang} to {target_lang}: {source_text}"
def format_summarization(text):
"""要約タスクのフォーマット"""
return f"summarize: {text}"
def format_classification(text, dataset_name="sst2"):
"""分類タスクのフォーマット"""
return f"{dataset_name} sentence: {text}"
def format_qa(question, context):
"""質問応答タスクのフォーマット"""
return f"question: {question} context: {context}"
def format_nli(premise, hypothesis, dataset_name="mnli"):
"""自然言語推論タスクのフォーマット"""
return f"{dataset_name} premise: {premise} hypothesis: {hypothesis}"
def format_similarity(sentence1, sentence2, dataset_name="stsb"):
"""文類似度タスクのフォーマット"""
return f"{dataset_name} sentence1: {sentence1} sentence2: {sentence2}"
# 各タスクの入力例を表示
print("=== Text-to-Text フォーマットの例 ===\n")
print("【翻訳】")
print(format_translation("That is good."))
print("→ 期待出力: Das ist gut.\n")
print("【要約】")
print(format_summarization("The quick brown fox jumps over the lazy dog in the park."))
print("→ 期待出力: A fox jumped over a dog.\n")
print("【分類】")
print(format_classification("This movie was absolutely wonderful."))
print("→ 期待出力: positive\n")
print("【質問応答】")
print(format_qa("What is the capital of France?",
"France is a country in Western Europe. Its capital city is Paris."))
print("→ 期待出力: Paris\n")
print("【自然言語推論】")
print(format_nli("A man is playing guitar.", "Someone is making music."))
print("→ 期待出力: entailment\n")
print("【文類似度】")
print(format_similarity("The cat sat on the mat.", "A cat is sitting on a mat."))
print("→ 期待出力: 5.0")
Hugging Face T5を使った推論デモ
from transformers import T5Tokenizer, T5ForConditionalGeneration
# T5-Smallモデルとトークナイザの読み込み
model_name = "t5-small"
tokenizer = T5Tokenizer.from_pretrained(model_name)
model = T5ForConditionalGeneration.from_pretrained(model_name)
model.eval()
def t5_generate(input_text, max_length=128, num_beams=4):
"""T5で推論を実行する汎用関数"""
input_ids = tokenizer.encode(input_text, return_tensors="pt")
with torch.no_grad():
outputs = model.generate(
input_ids,
max_length=max_length,
num_beams=num_beams,
early_stopping=True
)
return tokenizer.decode(outputs[0], skip_special_tokens=True)
# --- タスク1: 英独翻訳 ---
print("=" * 60)
print("【タスク1: 英独翻訳】")
input_text = "translate English to German: The house is wonderful."
output = t5_generate(input_text)
print(f"入力: {input_text}")
print(f"出力: {output}")
# --- タスク2: 要約 ---
print("\n" + "=" * 60)
print("【タスク2: 要約】")
article = (
"summarize: Transfer learning, where a model is first pre-trained on a data-rich task "
"before being fine-tuned on a downstream task, has emerged as a powerful technique in "
"natural language processing. The effectiveness of transfer learning has given rise to "
"a diversity of approaches, methodology, and practice."
)
output = t5_generate(article, max_length=64)
print(f"入力: {article[:80]}...")
print(f"出力: {output}")
# --- タスク3: 英仏翻訳 ---
print("\n" + "=" * 60)
print("【タスク3: 英仏翻訳】")
input_text = "translate English to French: How are you today?"
output = t5_generate(input_text)
print(f"入力: {input_text}")
print(f"出力: {output}")
print("\n" + "=" * 60)
print(f"\nモデル: {model_name}")
print(f"パラメータ数: {sum(p.numel() for p in model.parameters()):,}")
generate() メソッドで処理されている点です。タスクプレフィックスを変えるだけで、モデルの振る舞いが切り替わります。これがText-to-Textフレームワークの力です。from transformers import T5Tokenizer, T5ForConditionalGeneration
import torch
import matplotlib.pyplot as plt
import numpy as np
# モデルの読み込み(Attention重みを出力するよう設定)
model_name = "t5-small"
tokenizer = T5Tokenizer.from_pretrained(model_name)
model = T5ForConditionalGeneration.from_pretrained(model_name, output_attentions=True)
model.eval()
# 翻訳タスクの入力
input_text = "translate English to German: The cat sits on the mat."
input_ids = tokenizer.encode(input_text, return_tensors="pt")
input_tokens = tokenizer.convert_ids_to_tokens(input_ids[0])
# 推論してAttention重みを取得
with torch.no_grad():
outputs = model.generate(
input_ids,
max_length=32,
num_beams=1,
output_attentions=True,
return_dict_in_generate=True
)
# Encoderの Self-Attention重みを取得(最終層、第1ヘッド)
encoder_outputs = model.encoder(input_ids, output_attentions=True)
encoder_attentions = encoder_outputs.attentions # (num_layers,) のタプル
# 最終層のAttention重み: (batch, num_heads, seq_len, seq_len)
last_layer_attn = encoder_attentions[-1][0] # バッチの最初の要素
fig, axes = plt.subplots(2, 4, figsize=(20, 10))
fig.suptitle("T5 Encoder Self-Attention Weights (Last Layer)", fontsize=14)
for head_idx in range(min(8, last_layer_attn.shape[0])):
ax = axes[head_idx // 4][head_idx % 4]
attn_weights = last_layer_attn[head_idx].numpy()
im = ax.imshow(attn_weights, cmap='Blues', vmin=0, vmax=1)
ax.set_title(f"Head {head_idx}")
ax.set_xticks(range(len(input_tokens)))
ax.set_yticks(range(len(input_tokens)))
ax.set_xticklabels(input_tokens, rotation=90, fontsize=7)
ax.set_yticklabels(input_tokens, fontsize=7)
plt.tight_layout()
plt.savefig('t5_encoder_attention.png', dpi=150, bbox_inches='tight')
plt.show()
まとめ