RoBERTaの改良点と性能向上 — BERTの学習設定を最適化した設計判断

BERTのアーキテクチャを一切変えずに、学習の設定だけでどこまで性能を上げられるのか。これは2019年にFacebookAI(現Meta AI)の Liu et al. が投げかけた問いであり、その答えが RoBERTa(A Robustly Optimized BERT Pretraining Approach)です。

BERTが登場したとき、多くの研究者はアーキテクチャそのものの改良に目を向けました。層の数を増やす、注意機構を工夫する、外部知識を組み込むといった方向です。しかし Liu et al. は、「そもそもBERTは十分に学習されていなかったのではないか?」という根本的な疑問を提示しました。そして、学習レシピ(学習時間、バッチサイズ、データ量、マスキング戦略、事前学習タスクの取捨選択)を系統的に検証した結果、モデル構造をまったく変えることなく、GLUE、SQuAD、RACEといった主要ベンチマークでBERTを大幅に上回る性能を実現したのです。

この結果は、深層学習の研究と実務の両面で重要な教訓を含んでいます。

  • NLPベンチマークの改善: RoBERTaはGLUEリーダーボード上でBERTを大きく超え、当時最先端だったXLNetにも匹敵する性能を達成しました。アーキテクチャを変えずに達成したという事実が衝撃的です
  • 事前学習の最適化手法: 動的マスキング、NSPの廃止、大バッチ学習、データの増量という4つの改良は、後続モデル(ALBERT、DeBERTa、ELECTRAなど)の学習にも広く取り入れられました
  • 研究方法論への示唆: 新しいアーキテクチャを提案する前に、既存モデルの学習設定を十分に最適化しているかを確認すべきだという教訓は、NLPに限らず機械学習全般に通じます

本記事の内容

  • BERTの学習設定における「未学習」問題の分析
  • RoBERTaが導入した4つの改良(動的マスキング、NSP廃止、大バッチ、大規模データ)
  • 各改良の効果を定量的に確認する消去実験の解説
  • BERTとRoBERTaの設計比較(アーキテクチャは同一、学習レシピだけが異なる)
  • PyTorchとHugging Faceを用いた実装と性能比較

前提知識

この記事を読む前に、以下の記事を読んでおくと理解が深まります。

画像なし
BERTのアーキテクチャを解説
BERTのTransformer Encoderベースの構造と入力表現を解説します
画像なし
BERTのアーキテクチャと事前学習を解説
BERTの事前学習タスクMLMとNSPの目的関数を数式で導出します
画像なし
Masked Language Modelingの仕組みと実装
MLMの理論的背景と実装方法を解説します

BERTの「未学習」問題 — 何が足りなかったのか

BERTのオリジナル学習設定の振り返り

RoBERTaの改良を理解するには、まずBERTのオリジナルの学習設定を正確に把握する必要があります。Devlin et al. (2019) のBERTは、以下の設定で事前学習が行われました。

データ: BookCorpus(約8億単語)+ English Wikipedia(約25億単語)の合計約33億単語(約16GB)のテキストを使用しました。これは当時としては大規模でしたが、後から振り返ると限定的なデータ量です。

マスキング: テキストの前処理段階で、各入力シーケンスに対して10通りのマスクパターンを事前に作成しました。学習中は40エポックにわたって、これら10通りのマスクを繰り返し使います。つまり、各シーケンスは同じマスクパターンを4回ずつ見ることになります。これを静的マスキング(Static Masking)と呼びます。

事前学習タスク: MLM(Masked Language Model)と NSP(Next Sentence Prediction)の2つのタスクを同時に学習しました。損失関数は両者の和として定義されます。

$$ \mathcal{L}_{\text{BERT}} = \mathcal{L}_{\text{MLM}} + \mathcal{L}_{\text{NSP}} $$

MLM損失はマスクされたトークンの復元に関する交差エントロピーです。

$$ \mathcal{L}_{\text{MLM}} = -\sum_{i \in \mathcal{M}} \log P(x_i \mid \bm{x}_{\backslash \mathcal{M}}) $$

ここで $\mathcal{M}$ はマスクされたトークンのインデックス集合、$\bm{x}_{\backslash \mathcal{M}}$ はマスクされていないトークン列を表します。NSP損失は、2つの文が連続しているかどうかの2値分類の交差エントロピーです。

$$ \mathcal{L}_{\text{NSP}} = -\left[ y \log P(\text{IsNext} \mid \bm{h}_{\text{[CLS]}}) + (1-y) \log P(\text{NotNext} \mid \bm{h}_{\text{[CLS]}}) \right] $$

ここで $y \in \{0, 1\}$ は2つのセグメントが実際に連続しているかどうかのラベルです。

バッチサイズと学習ステップ: バッチサイズ256、学習ステップ数100万回(1M steps)で学習しました。最大系列長は128トークン(最初の90%のステップ)と512トークン(残りの10%)の2段階で訓練されました。

オプティマイザ: Adam($\beta_1=0.9$, $\beta_2=0.999$)に線形ウォームアップ付き学習率スケジュール(ピーク学習率 $1 \times 10^{-4}$)を使用しました。

Liu et al. (2019) の問題提起

Liu et al. は、BERTの学習設定に対して以下の4つの問題を指摘しました。

1. マスキングの固定化: BERTの静的マスキングでは、学習中に各入力が見るマスクパターンが10通りに制限されます。40エポックの学習で同じマスクを4回ずつ見ることは、モデルが特定のマスクパターンに過適合するリスクを生みます。

2. NSPタスクの有害性: NSPは、BERTの事前学習に貢献するどころか、性能を下げている可能性がありました。その理由は後のセクションで詳しく分析しますが、直感的に言えば、「2つの文が隣り合うかどうか」という問いが簡単すぎて、モデルに有意な言語理解を促さないのです。

3. バッチサイズの小ささ: 後の研究(特に、大規模分散学習の文脈で)で、より大きなバッチサイズが学習の安定性と最終的な性能を向上させることが示されていました。BERTのバッチサイズ256は、最適値からかなり離れていた可能性があります。

4. データ量と学習時間の不足: BERTの約16GBのデータと100万ステップの学習は、モデルのキャパシティ(特にBERT-Largeの3.4億パラメータ)を十分に活用するには不十分だった可能性があります。大規模モデルには大規模なデータと長い学習時間が必要だという直感は自然なものですが、BERTの論文ではこの点が十分に検証されていませんでした。

これらの問題を一つずつ検証し、最適な学習設定を見つけ出すことが、RoBERTa論文の中心的な貢献です。それでは、各改良を詳しく見ていきましょう。

RoBERTaの4つの改良点

RoBERTaの革新は、「新しいアーキテクチャ」ではなく「新しい学習レシピ」にあります。料理にたとえるなら、同じ食材と調理器具を使っていても、火加減や調理時間、下ごしらえの方法を変えるだけで料理の出来栄えが劇的に変わるのと同じです。以下の4つの改良を、一つずつ見ていきます。

4-1. 動的マスキング(Dynamic Masking)

BERTの静的マスキング

BERTの事前学習では、テキストを前処理する段階でマスキングを適用します。具体的には、各入力シーケンスに対して10通りの異なるマスクパターンを事前に作成し、ディスクに保存しておきます。学習ループでは、この保存済みのマスクパターンの中からランダムに1つを選んでモデルに入力します。

この方式では、40エポックの学習の間に各シーケンスが同じマスクパターンを約4回ずつ見ることになります。数値的に確認してみましょう。40エポック $\div$ 10パターン $= 4$ 回/パターンです。

静的マスキングの問題点は、モデルが「この文ではここがマスクされやすい」というパターンを記憶してしまう可能性があることです。たとえば、ある文で常に同じ10箇所の組み合わせでマスクが適用されるなら、モデルは文脈を深く理解するよりも、マスクパターンのショートカットを学習してしまうかもしれません。

RoBERTaの動的マスキング

RoBERTaの動的マスキング(Dynamic Masking)は、概念的にはきわめてシンプルです。事前にマスクを作成して保存するのではなく、毎回のミニバッチ入力時にマスクを新しく生成します。つまり、同じ入力シーケンスであっても、エポックが変わるたびに異なる位置がマスクされます。

これをデータ拡張の観点から考えると、静的マスキングでは各入力に対して最大10通りの「拡張されたデータ」しか存在しません。一方、動的マスキングでは、入力長 $n$ のシーケンスから $\lfloor 0.15n \rfloor$ 個のトークンをマスクする組み合わせは天文学的な数になります。長さ512のシーケンスでマスク率15%の場合、マスクするトークン数は約77個です。このとき、組み合わせの数は次のようになります。

$$ \binom{512}{77} \approx 10^{100} $$

この数は観測可能な宇宙の原子の数(約 $10^{80}$)をはるかに超えています。つまり、動的マスキングでは事実上無限のマスクパターンを利用できるのです。静的マスキングの10パターンとは比較にならない多様性です。

実験結果の比較

Liu et al. の実験では、動的マスキングの効果は比較的小さいものの、一貫して静的マスキングと同等かわずかに優れる結果が得られました。

マスキング方式 SQuAD 1.1 (F1) SQuAD 2.0 (F1) MNLI-m (Acc) SST-2 (Acc)
静的(参照実装) 88.1 77.0 84.3 92.5
動的 88.5 77.6 84.7 92.9

数値の差は大きくありませんが、動的マスキングには「コード実装が簡潔になる」という副次的なメリットもあります。前処理パイプラインでマスクを事前生成して保存する必要がなくなるため、データの準備が簡素化されます。

動的マスキングの効果は、それ単独では控えめです。しかし、RoBERTaの真価は4つの改良の組み合わせにあります。次に、より大きなインパクトを持つ改良であるNSPの廃止を見ていきます。

4-2. NSP(Next Sentence Prediction)の廃止

BERTのNSPの仕組みの復習

BERTでは、事前学習タスクの1つとしてNSP(Next Sentence Prediction)が使われていました。入力は2つのセグメント(segment A, segment B)で構成され、50%の確率で実際に連続する文ペアが、残り50%の確率でランダムに選ばれた無関係な文ペアが入力されます。モデルは [CLS] トークンの出力から、この2つのセグメントが連続しているかどうかを予測します。

BERTのオリジナル論文では、NSPが質問応答や自然言語推論といったタスクの性能向上に寄与すると報告されていました。2つの文の関係を理解する力が、下流タスクでの文ペア分類に役立つという理屈です。

NSPが有害だった理由

しかし、Liu et al. はNSPがむしろ有害であることを発見しました。その理由を直感的に考えてみましょう。

NSPの「ネガティブ例」(NotNextペア)は、異なるドキュメントからランダムに文を選んで作成されます。ここで重要な点は、異なるドキュメントの文は通常、まったく異なるトピックについて書かれているということです。たとえば、セグメントAが「太陽系の惑星の数は8個である」で、セグメントBが「本日の株式市場は大幅に下落した」だった場合、モデルはこのペアが連続していないことを判断するのにトピックの違いだけを見ればよく、文の論理的なつながりを理解する必要がありません。

つまり、NSPは文の間の論理的関係の判断ではなく、トピックの一致/不一致の判断に退化してしまいます。これはトピック分類タスクとしては簡単すぎて、言語理解の向上にはほとんど貢献しません。しかも、NSP用にセグメントAとセグメントBを短く区切って入力する制約が、文脈理解を阻害する可能性すらあります。

4つの入力フォーマットの比較

Liu et al. は、この問題を検証するために4つの入力フォーマットを比較実験しました。

1. SEGMENT-PAIR + NSP(BERTのオリジナル): 2つのセグメント(各セグメントは複数文を含みうる)をペアにし、NSPで学習します。入力の合計長は最大512トークンです。

2. SENTENCE-PAIR + NSP: 2つの「自然文」(1文ずつ)をペアにし、NSPで学習します。ただし、1文は通常512トークンよりはるかに短いため、入力全体が短くなり、バッチサイズを大きくして計算コストを揃える必要があります。

3. FULL-SENTENCES(NSPなし): 1つまたは複数のドキュメントから連続したテキストを取り出し、最大512トークンになるまで詰め込みます。ドキュメントの境界を越える場合は、セパレータトークンを挿入します。NSPは使いません。

4. DOC-SENTENCES(NSPなし): FULL-SENTENCESと似ていますが、ドキュメントの境界を越えないようにします。つまり、1つのドキュメントの中の連続テキストのみを使います。ドキュメントが短い場合はバッチサイズを動的に調整して、トータルのトークン数を揃えます。

実験結果は以下の通りです。

入力フォーマット SQuAD 1.1 (F1) SQuAD 2.0 (F1) MNLI-m (Acc) SST-2 (Acc)
SEGMENT-PAIR + NSP 88.1 77.0 84.3 92.5
SENTENCE-PAIR + NSP 86.9 75.2 82.9 92.2
FULL-SENTENCES 88.5 77.6 84.7 92.9
DOC-SENTENCES 88.7 78.1 84.9 93.0

ここから読み取れることは3つあります。

第一に、NSPを除去した方が性能が向上しています。FULL-SENTENCESとDOC-SENTENCESはともにNSPなしで、NSPありのSEGMENT-PAIRを上回っています。

第二に、入力を長くすることが重要です。SENTENCE-PAIR(1文ずつのペア)は全フォーマットの中で最も性能が低く、短い入力が不利であることを示しています。言語モデルは長い文脈から学ぶことが多いのです。

第三に、DOC-SENTENCESがわずかにFULL-SENTENCESを上回っていますが、バッチサイズの動的調整が必要になるため実装が複雑です。RoBERTaでは実装の簡潔さを優先し、FULL-SENTENCESを採用しました。

NSPの廃止は、RoBERTaの改良の中で最もインパクトの大きいものの一つです。しかし、学習の効率をさらに引き上げるには、最適化の観点からバッチサイズの検討も必要です。

4-3. より大きなバッチサイズ

BERTからRoBERTaへのバッチサイズの変遷

ニューラルネットワークの学習において、バッチサイズは性能と効率の両方に大きな影響を与えるハイパーパラメータです。日常的な例で考えると、試験勉強で一度に1問ずつ解答を確認するか、100問解いてからまとめて確認するかの違いに似ています。1問ずつ確認すれば即座にフィードバックが得られますが、ノイズが大きくなります。まとめて確認すれば安定した傾向が掴めますが、細かい修正が遅れます。

BERTのオリジナルはバッチサイズ256で学習されました。Liu et al. はこれを2K(2,048)と8K(8,192)に増やした場合の効果を検証しました。

大規模バッチ学習の理論的な背景を整理しておきましょう。確率的勾配降下法(SGD)で用いるミニバッチの勾配 $\bm{g}_B$ は、真の勾配 $\nabla \mathcal{L}$ の不偏推定量です。

$$ \mathbb{E}[\bm{g}_B] = \nabla \mathcal{L} $$

ミニバッチのサイズを $B$ とすると、勾配推定の分散は次のようにスケールします。

$$ \text{Var}(\bm{g}_B) = \frac{\sigma^2}{B} $$

ここで $\sigma^2$ は個々のサンプルの勾配の分散です。バッチサイズ $B$ を大きくすると、分散が $1/B$ に減少するため、勾配推定がより正確になります。つまり、各ステップでのパラメータ更新方向が真の降下方向により近くなるのです。

ただし、バッチサイズを大きくした場合、同じトークン数を処理するステップ数が減ります。これを補うために、学習率をバッチサイズに応じて調整する必要があります。線形スケーリング則(linear scaling rule)では、バッチサイズを $k$ 倍にしたら学習率も $k$ 倍にすることが推奨されています。

$$ \eta_{\text{new}} = k \cdot \eta_{\text{base}} $$

ただし、実際にはこの線形スケーリングが常に成り立つわけではなく、ウォームアップ期間の調整やLAMBオプティマイザのような工夫が必要になる場合もあります。

実験結果

Liu et al. は、バッチサイズを変えながら同じ計算量(同じトークン数を処理)で比較しました。

バッチサイズ ステップ数 処理トークン数 PPL(MLM) MNLI-m (Acc) SST-2 (Acc)
256 (BERT) 1M 約33B 3.99 84.7 92.7
2K 125K 約33B 3.68 85.2 93.7
8K 31K 約33B 3.77 84.5 93.4

バッチサイズ2Kでの学習が最もバランスの良い結果を示しました。バッチサイズ8Kでも競争力のある結果が得られていますが、2Kの方がやや安定しています。注目すべきは、バッチサイズ256よりも2Kや8Kの方がMLMのPerplexityが低く、下流タスクの性能も向上している点です。

さらに重要な実用面の利点として、大バッチは分散学習との相性が極めて良いということがあります。バッチサイズ8Kの学習は、複数のGPUに分散して並列に計算できるため、壁時計時間(実際にかかる時間)を大幅に短縮できます。ステップ数が31Kと少なくて済むことも、学習の高速化に直結します。

RoBERTaの最終設定では、バッチサイズ 8K が採用されました。2Kの方がやや性能が良い場合もありますが、分散学習の効率性と全体的な性能のバランスを考慮しての判断です。

バッチサイズの最適化に加えて、モデルが「見る」データの量と学習時間を増やすことも、RoBERTaの重要な改良です。次にその点を見ていきましょう。

4-4. より多くのデータとより長い学習

データ量の大幅な増加

BERTが使用したデータはBookCorpus(約8億単語)とEnglish Wikipedia(約25億単語)の合計約16GBでした。RoBERTaでは、これに加えて以下の3つのデータセットを追加しました。

CC-News(約76GB): Common Crawlから収集したニュース記事。2016年9月から2019年2月までの約6300万件の英語ニュース記事を含みます。

OpenWebText(約38GB): RedditのリンクからクロールしたWebページのテキスト。GPT-2の学習に使われたWebTextの再現データセットです。

Stories(約31GB): Common Crawlのサブセットから、物語風のテキスト(ストーリー)をフィルタリングして収集したものです。

これらを合わせると、合計約160GBのテキストデータになり、BERTの約10倍以上のデータ量です。

なぜデータ量を増やすことがこれほど重要なのでしょうか。言語の多様性を考えてみましょう。自然言語には膨大な表現パターンがあり、同じ意味でも文脈や分野によって全く異なる表現が使われます。BERTの16GBのデータには、Wikipedia の百科事典的な文体と BookCorpus の物語的な文体しか含まれていませんでした。RoBERTaの160GBには、ニュース記事、Web上の議論、物語など多様なジャンルが含まれるため、モデルはより幅広い言語パターンを学習できます。

大規模言語モデルの学習におけるスケーリング則の観点からも、データ量の重要性は裏付けられています。Kaplan et al. (2020) のスケーリング則によれば、モデルの性能はデータ量 $D$ に対してべき乗則に従います。

$$ L(D) \propto D^{-\alpha_D} $$

ここで $\alpha_D$ はデータに関するスケーリング指数です。この法則は、データ量を10倍にすれば性能が系統的に改善することを意味しています。

学習ステップ数の増加

データ量を増やすだけでなく、学習ステップ数も大幅に増加させました。BERTの100K ステップに対して、RoBERTaでは100K、300K、500K ステップの3段階で効果を検証しました。

学習ステップ SQuAD 1.1 (F1) SQuAD 2.0 (F1) MNLI-m (Acc) SST-2 (Acc)
100K 88.5 77.6 84.7 92.9
300K 90.4 80.6 86.6 94.2
500K 91.3 81.8 87.6 95.3

ステップ数を増やすほど、全てのベンチマークで一貫した性能向上が見られます。100Kから500Kへの増加で、SQuAD 1.1のF1スコアが2.8ポイント、MNLI-mの精度が2.9ポイント向上しています。この改善幅は、他の3つの改良(動的マスキング、NSP廃止、大バッチ)の効果を上回ります。

これは直感的にも理解できます。500Kステップ $\times$ バッチサイズ8K $\times$ 最大512トークン $\approx$ 2兆トークン以上をモデルが処理することになります。160GBのテキストを複数回通過することで、モデルは多様な言語パターンをより深く内在化できるのです。

重要なのは、500Kステップ時点でもまだ性能が飽和していないように見えることです。Liu et al. も論文中で「さらに長い学習が性能を改善する可能性がある」と述べています。これは、BERTが100Kステップで学習を打ち切ったことがいかに早計だったかを示唆しています。

以上の4つの改良を組み合わせたものが、RoBERTaの完全な学習レシピです。次に、BERTとRoBERTaのアーキテクチャを並べて比較し、「何が変わって何が変わらなかったのか」を明確にしましょう。

BERTとRoBERTaのアーキテクチャ比較

RoBERTaの最も注目すべき点は、モデルのアーキテクチャがBERT-Largeと完全に同一であることです。層の数、隠れ次元、注意ヘッドの数、活性化関数、正規化の位置など、モデル構造に関する一切の変更はありません。違いは学習設定だけです。

この事実を、表で明確に整理しておきましょう。

モデル構造(同一)

パラメータ BERT-Large RoBERTa
Encoder層数 $L$ 24 24
隠れ次元 $d_{\text{model}}$ 1024 1024
FFN内部次元 $d_{\text{ff}}$ 4096 4096
注意ヘッド数 $A$ 16 16
パラメータ数 355M 355M
活性化関数 GELU GELU
正規化 Layer Norm(Pre-LN) Layer Norm(Pre-LN)
位置埋め込み 学習可能(最大512) 学習可能(最大512)

学習設定(異なる)

設定項目 BERT RoBERTa
マスキング 静的(10パターン事前生成) 動的(毎バッチ生成)
事前学習タスク MLM + NSP MLMのみ(NSP廃止)
入力フォーマット Segment Pair Full Sentences
バッチサイズ 256 8K
学習ステップ 1M(前半128トークン、後半512トークン) 500K(全て512トークン)
学習率 $1 \times 10^{-4}$ $6 \times 10^{-4}$(大バッチに対応)
ウォームアップ 10K steps 30K steps
訓練データ BookCorpus + Wikipedia(16GB) + CC-News + OpenWebText + Stories(160GB)
トークナイザ WordPiece(30K語彙) BPE(50K語彙)
最大系列長 128→512(2段階) 512(全ステップ)

トークナイザについて補足すると、BERTはWordPieceを使用していましたが、RoBERTaはGPT-2で使われたByte-Pair Encoding(BPE)を採用しています。BPEの語彙サイズは50,265トークンで、BERTの30,522トークンよりも大きくなっています。バイトレベルのBPEを使うことで、未知語(UNK)トークンが不要になり、あらゆるテキストをトークン化できるという利点があります。

この表を見ると、RoBERTaの本質がよく分かります。モデル構造は1ビットも変わっていません。変わったのは、学習の「レシピ」— つまり、何を、どれだけ、どのように食べさせるか — だけです。

では、この学習レシピの変更が実際のベンチマークでどのような結果をもたらしたのかを確認しましょう。

実験結果

GLUE ベンチマークでの性能

GLUE(General Language Understanding Evaluation)は、自然言語理解の8つのタスクを含むベンチマークです。RoBERTaは、BERTだけでなく、アーキテクチャを変更したXLNetとも競争力のある結果を達成しました。

モデル MNLI-m QQP QNLI SST-2 CoLA STS-B MRPC RTE 平均
BERT-Large 86.6 91.3 92.3 93.2 60.6 90.0 88.0 70.4 84.1
XLNet-Large 89.8 91.8 93.9 95.6 63.6 91.8 89.2 83.8 87.4
RoBERTa 90.2 92.2 94.7 96.4 68.0 92.4 90.9 86.6 88.9

RoBERTaがGLUEの全てのタスクでBERT-Largeを上回っています。特に注目すべきは以下の点です。

MNLI-m(自然言語推論): BERTの86.6%からRoBERTaの90.2%へと3.6ポイント向上しています。文のペア間の意味的関係を判断するこのタスクでは、NSPの廃止とFULL-SENTENCES入力が特に効果的だったと考えられます。長い文脈から文の関係性をより深く学習できるようになったためです。

CoLA(言語的受容性判定): 60.6%から68.0%へと7.4ポイントの大幅な向上です。CoLAは文が文法的に正しいかどうかを判定するタスクであり、言語の微妙な構造的パターンの理解が必要です。大量のテキストデータと長い学習が、こうした繊細な言語知識の獲得に貢献しています。

RTE(テキスト含意関係認識): 70.4%から86.6%へと16.2ポイントもの向上です。RTEはGLUEの中で最も訓練データが少ないタスク(約2,500例)であり、事前学習の質がファインチューニングの結果に大きく影響します。RoBERTaの強力な事前学習が、少量のデータでの転移学習を飛躍的に改善したことがわかります。

SQuAD での性能

SQuAD(Stanford Question Answering Dataset)は、文章中から質問の答えを抽出する読解タスクです。

モデル SQuAD 1.1 (EM/F1) SQuAD 2.0 (EM/F1)
BERT-Large 84.1 / 90.9 79.0 / 81.8
XLNet-Large 89.0 / 94.5 86.1 / 88.8
RoBERTa 88.9 / 94.6 86.5 / 89.4

SQuAD 2.0ではXLNetをもわずかに上回り、SQuAD 1.1ではほぼ同等の性能を達成しています。SQuAD 2.0は「答えがない質問」も含むため、文脈の深い理解が必要な難しいタスクです。

RACE での性能

RACE(ReAding Comprehension from Examinations)は、中国の英語試験から収集された多肢選択式の読解タスクです。

モデル RACE-m (Acc) RACE-h (Acc) RACE (Acc)
BERT-Large 71.1 62.3 65.0
XLNet-Large 85.4 80.6 81.8
RoBERTa 86.5 81.3 83.2

RACEでも全てのサブタスクでBERTとXLNetを上回っています。RACE-mは中学レベル、RACE-hは高校レベルの問題で、特に高校レベルの問題での改善が大きいことがわかります。

消去実験(Ablation Study)の解説

Liu et al. の論文の大きな貢献は、各改良の効果を個別に検証する消去実験を丁寧に行ったことです。改良を一つずつ追加し、どの改良がどれだけ性能に寄与しているかを明確にしました。

実験は、BERT-Largeの学習設定から出発し、改良を順に適用する形で行われました。

設定 変更内容 MNLI-m SST-2
BERT-Large(再現) ベースライン 84.3 92.5
+ 動的マスキング 静的→動的 84.7 (+0.4) 92.9 (+0.4)
+ FULL-SENTENCES(NSP廃止) NSP除去 85.2 (+0.5) 93.4 (+0.5)
+ 大バッチ(8K) 256→8K 85.7 (+0.5) 93.8 (+0.4)
+ 大規模データ(160GB) 16GB→160GB 87.6 (+1.9) 95.3 (+1.5)

この表から、各改良の貢献度が明確に読み取れます。

動的マスキングは+0.4ポイントの改善で、効果は最も小さいものの、一貫してプラスに働いています。

NSPの廃止とFULL-SENTENCES入力は+0.5ポイントの改善です。NSPという「不要なタスク」を取り除き、より長い文脈を入力することで、MLMの学習効率が向上しています。

大バッチ(8K) は+0.5ポイントの改善です。勾配推定の精度向上が、学習の安定性と最終性能の両方に寄与しています。

大規模データ(160GB) は+1.9ポイントの改善で、最も大きな貢献をしています。データ量と学習ステップ数の増加は、他の改良を合わせたよりも大きなインパクトがあります。

この消去実験の結果は、「十分なデータで十分に長く学習すること」が事前学習の最も重要な要素であることを示唆しています。アーキテクチャの工夫や学習テクニックも重要ですが、スケール(データ量と計算量)の効果はそれらを上回るのです。

これらの実験結果から、研究者やエンジニアが学ぶべき教訓を次のセクションで整理しましょう。

RoBERTaから学べる教訓

教訓1: アーキテクチャ改良よりも学習レシピが重要なことがある

2019年当時、BERTの性能を超えるために多くの研究者がアーキテクチャの改変に注力していました。XLNetはTransformer-XLの相対位置エンコーディングと並べ替え言語モデルを導入し、SPANBERTはスパンマスキングという新しいマスク戦略を提案しました。

RoBERTaの結果は、これらのアーキテクチャ改変の効果の一部が、実はBERTの学習不足を補っていただけだった可能性を示唆しています。十分に最適化されたBERTは、アーキテクチャを変えたモデルと同等以上の性能を発揮したからです。

この教訓は、機械学習の実務においても重要です。新しいモデルを試す前に、まず既存モデルの学習設定(学習率、バッチサイズ、学習ステップ数、データ拡張など)を十分に最適化しているか確認すべきです。ハイパーパラメータのチューニングは地味な作業ですが、アーキテクチャの変更よりも大きな改善をもたらすことが少なくありません。

教訓2: 十分な学習の重要性

BERTの100Kステップという学習量は、BERT-Largeの3.5億パラメータにとって不十分でした。500Kステップでもまだ改善の余地があったことを考えると、大規模モデルの学習には想定以上の計算資源が必要です。

これは後のスケーリング則の研究(Kaplan et al., 2020; Chinchilla, 2022)とも一致しています。モデルサイズ $N$ に対して最適な学習トークン数 $D^*$ は次のように推定されています。

$$ D^* \approx 20N $$

BERT-Largeの3.5億パラメータに対して、最適なトークン数は約70億トークンです。BERTの学習で処理されたトークン数(約33億)はこの半分にも満たず、まさに「未学習」(undertrained)状態だったのです。RoBERTaの500Kステップ $\times$ 8Kバッチでの処理トークン数は2兆トークン以上で、最適量をはるかに超えていますが、多様なデータを複数回通過させることで、モデルは言語パターンをより深く定着させることができたと考えられます。

教訓3: データの質と量のバランス

RoBERTaのデータ戦略は「量で押す」アプローチに見えますが、実際にはデータの質にも注意が払われています。たとえば、CC-Newsはニュース記事という比較的高品質なテキストのみを含み、Storiesも物語風テキストにフィルタリングされています。無制限にWebテキストを追加するのではなく、品質を保ちながら量を増やすというバランスが重要です。

この点は、後のGPT-3やLLaMAの学習でも繰り返し確認されています。データの量を増やすことは重要ですが、低品質なデータ(機械生成テキスト、重複コンテンツ、HTMLタグ混じりのテキストなど)を混ぜると、かえって性能が低下することがあります。

教訓4: 実験の再現性と体系的検証

RoBERTaの論文のもう一つの重要な貢献は、実験の方法論そのものです。一つの改良を加えては効果を測定し、次の改良を加えるという丁寧な消去実験は、因果関係を特定するための科学的な手法です。

機械学習の研究では、複数の改良を同時に導入して「全体として性能が上がった」と報告することが少なくありません。しかし、どの改良がどれだけ効いているのかがわからなければ、他の研究者がその成果を活用することが難しくなります。RoBERTaの消去実験は、この点で模範的な研究事例といえます。

これらの教訓を頭に入れた上で、RoBERTaの改良をPythonで実装し、実際の動作を確認していきましょう。

PyTorchでの実装

動的マスキングの実装

まず、RoBERTaの動的マスキングをPyTorchで実装します。BERTの静的マスキングとの違いを実際にコードで確認しましょう。

import torch
import numpy as np
import matplotlib.pyplot as plt

# 再現性のためにシードを固定
torch.manual_seed(42)
np.random.seed(42)

def static_masking(input_ids, mask_prob=0.15, num_patterns=10, vocab_size=30522):
    """
    BERTの静的マスキング: 事前にnum_patterns通りのマスクを生成して保存
    """
    seq_len = len(input_ids)
    num_masks = int(seq_len * mask_prob)
    patterns = []

    for _ in range(num_patterns):
        # マスク位置をランダムに選択
        mask_positions = np.random.choice(seq_len, size=num_masks, replace=False)
        masked_input = input_ids.clone()
        labels = torch.full_like(input_ids, -100)  # -100はignore_index

        for pos in mask_positions:
            labels[pos] = input_ids[pos]
            rand = np.random.random()
            if rand < 0.8:
                masked_input[pos] = 103  # [MASK]トークンID
            elif rand < 0.9:
                masked_input[pos] = np.random.randint(0, vocab_size)
            # 残り10%はそのまま

        patterns.append((masked_input, labels))

    return patterns


def dynamic_masking(input_ids, mask_prob=0.15, vocab_size=30522):
    """
    RoBERTaの動的マスキング: 毎回新しいマスクを生成
    """
    seq_len = len(input_ids)
    num_masks = int(seq_len * mask_prob)

    # 毎回異なるマスク位置
    mask_positions = np.random.choice(seq_len, size=num_masks, replace=False)
    masked_input = input_ids.clone()
    labels = torch.full_like(input_ids, -100)

    for pos in mask_positions:
        labels[pos] = input_ids[pos]
        rand = np.random.random()
        if rand < 0.8:
            masked_input[pos] = 103  # [MASK]
        elif rand < 0.9:
            masked_input[pos] = np.random.randint(0, vocab_size)

    return masked_input, labels


# デモ: 同じ入力に対するマスクの多様性を比較
input_ids = torch.arange(20)  # 簡易的なトークン列 [0, 1, 2, ..., 19]

# 静的マスキング: 10通りのパターンを事前生成
static_patterns = static_masking(input_ids, mask_prob=0.15, num_patterns=10)

# 動的マスキング: 40回呼び出し(40エポック相当)
dynamic_results = [dynamic_masking(input_ids, mask_prob=0.15) for _ in range(40)]

# マスクされた位置の頻度を集計
fig, axes = plt.subplots(1, 2, figsize=(14, 5))

# 静的マスキング: 40エポック(10パターン × 4回ずつ)
static_mask_counts = np.zeros(20)
for epoch in range(40):
    pattern_idx = epoch % 10  # 10パターンを繰り返し使用
    masked_input, labels = static_patterns[pattern_idx]
    for pos in range(20):
        if labels[pos] != -100:
            static_mask_counts[pos] += 1

axes[0].bar(range(20), static_mask_counts, color='#2196F3', alpha=0.8)
axes[0].set_title('Static Masking (BERT)\n10 patterns x 4 repeats = 40 epochs', fontsize=12)
axes[0].set_xlabel('Token Position')
axes[0].set_ylabel('Mask Count (over 40 epochs)')
axes[0].set_ylim(0, 20)

# 動的マスキング: 40エポック(毎回異なるマスク)
dynamic_mask_counts = np.zeros(20)
for masked_input, labels in dynamic_results:
    for pos in range(20):
        if labels[pos] != -100:
            dynamic_mask_counts[pos] += 1

axes[1].bar(range(20), dynamic_mask_counts, color='#FF9800', alpha=0.8)
axes[1].set_title('Dynamic Masking (RoBERTa)\n40 unique masks = 40 epochs', fontsize=12)
axes[1].set_xlabel('Token Position')
axes[1].set_ylabel('Mask Count (over 40 epochs)')
axes[1].set_ylim(0, 20)

plt.tight_layout()
plt.savefig('static_vs_dynamic_masking.png', dpi=150, bbox_inches='tight')
plt.show()

# ユニークなマスクパターン数を表示
static_unique = len(set(
    tuple(labels[labels != -100].tolist()) for _, labels in static_patterns
))
dynamic_unique = len(set(
    tuple(labels[labels != -100].tolist()) for _, labels in dynamic_results
))
print(f"静的マスキング: {static_unique} ユニークパターン(10パターンを4回ずつ使用)")
print(f"動的マスキング: {dynamic_unique} ユニークパターン(40エポックで毎回異なる)")

上のコードでは、長さ20の入力シーケンスに対して、静的マスキング(10パターンを4回ずつ繰り返し)と動的マスキング(40回それぞれ異なるマスク)を比較しています。左の棒グラフ(静的マスキング)では、特定の位置が繰り返しマスクされ、他の位置はほとんどマスクされないという偏りが見られます。これは10パターンしかないことの直接的な影響です。一方、右の棒グラフ(動的マスキング)では、各位置のマスク回数がより均等に分布しています。これは毎回新しいマスクが生成されるため、確率的に全ての位置がまんべんなくマスクされるからです。この均等性が、モデルが入力の全ての位置で文脈理解を鍛えることにつながります。

FULL-SENTENCES 入力フォーマットの実装

次に、BERTのSEGMENT-PAIR入力とRoBERTaのFULL-SENTENCES入力の違いを実装で確認します。

import torch
import numpy as np

def bert_segment_pair_input(doc1_sentences, doc2_sentences, max_len=512):
    """
    BERTのSEGMENT-PAIR入力: 2つのセグメントをペアにして[SEP]で区切る
    NSP用に50%の確率でランダムペアを使用
    """
    # セグメントAとBを選択(簡略化のためdoc1, doc2から直接)
    seg_a = " ".join(doc1_sentences[:2])  # doc1から2文

    is_next = np.random.random() < 0.5
    if is_next:
        seg_b = " ".join(doc1_sentences[2:4])  # 実際の続き
        label = 1  # IsNext
    else:
        seg_b = " ".join(doc2_sentences[:2])  # ランダムな文
        label = 0  # NotNext

    # トークン列を構成: [CLS] segA [SEP] segB [SEP]
    tokens = ["[CLS]"] + seg_a.split()[:max_len//2] + ["[SEP]"] + seg_b.split()[:max_len//2] + ["[SEP]"]
    token_type_ids = [0] * (len(seg_a.split()[:max_len//2]) + 2) + [1] * (len(seg_b.split()[:max_len//2]) + 1)

    return {
        "tokens": tokens,
        "token_type_ids": token_type_ids[:len(tokens)],
        "nsp_label": label,
        "total_tokens": len(tokens)
    }


def roberta_full_sentences_input(documents, max_len=512):
    """
    RoBERTaのFULL-SENTENCES入力: ドキュメントから連続テキストを
    最大長まで詰め込む(ドキュメント境界を跨ぐ場合は</s>を挿入)
    """
    tokens = ["<s>"]  # RoBERTaのBOSトークン
    current_doc_idx = 0
    current_sent_idx = 0

    while len(tokens) < max_len and current_doc_idx < len(documents):
        doc = documents[current_doc_idx]
        while current_sent_idx < len(doc) and len(tokens) < max_len:
            sentence_tokens = doc[current_sent_idx].split()
            tokens.extend(sentence_tokens)
            current_sent_idx += 1

        # ドキュメント境界
        if current_doc_idx < len(documents) - 1 and len(tokens) < max_len:
            tokens.append("</s>")  # ドキュメント区切り
            current_doc_idx += 1
            current_sent_idx = 0

    tokens = tokens[:max_len]
    tokens.append("</s>")  # EOSトークン

    return {
        "tokens": tokens,
        "nsp_label": None,  # NSPなし
        "total_tokens": len(tokens)
    }


# デモデータ
doc1 = [
    "The cat sat on the mat",
    "It was a sunny day",
    "The birds were singing",
    "The flowers bloomed in the garden"
]
doc2 = [
    "Machine learning is a subset of artificial intelligence",
    "Neural networks are inspired by biological neurons",
    "Deep learning uses multiple layers of processing",
    "Transfer learning allows knowledge reuse"
]
doc3 = [
    "The solar system contains eight planets",
    "Jupiter is the largest planet",
    "Mars has a thin atmosphere"
]

# BERT方式
bert_input = bert_segment_pair_input(doc1, doc2)
print("=== BERT SEGMENT-PAIR 入力 ===")
print(f"トークン数: {bert_input['total_tokens']}")
print(f"NSPラベル: {'IsNext' if bert_input['nsp_label'] == 1 else 'NotNext'}")
print(f"トークン列: {' '.join(bert_input['tokens'][:30])}...")
print()

# RoBERTa方式
roberta_input = roberta_full_sentences_input([doc1, doc2, doc3], max_len=50)
print("=== RoBERTa FULL-SENTENCES 入力 ===")
print(f"トークン数: {roberta_input['total_tokens']}")
print(f"NSPラベル: {roberta_input['nsp_label']} (タスクなし)")
print(f"トークン列: {' '.join(roberta_input['tokens'][:30])}...")
print()

# 有効トークン数の比較
print("=== 有効トークン数の比較 ===")
print(f"BERT: セグメントA + セグメントB = 入力の一部のみ使用")
print(f"RoBERTa: 最大長まで連続テキストを詰め込み → より多くの文脈を学習")

このコードの出力を見ると、BERTのSEGMENT-PAIR入力では2つの短いセグメントがNSPラベル付きで構成されているのに対し、RoBERTaのFULL-SENTENCES入力では複数のドキュメントからテキストを連続的に詰め込み、NSPラベルが不要であることがわかります。RoBERTa方式では、最大長(512トークン)までテキストを詰め込むため、1つの入力で学習できる文脈情報の量がBERT方式よりも多くなります。特に、ドキュメント境界を </s> トークンで区切りつつも連続して配置することで、モデルは異なるドキュメント間の文脈切り替えにも対応できるようになります。

Hugging Face RoBERTaとBERTの出力比較

Hugging Faceのtransformersライブラリを使って、実際のBERTとRoBERTaの出力を比較してみましょう。

from transformers import BertTokenizer, BertModel
from transformers import RobertaTokenizer, RobertaModel
import torch
import numpy as np

# モデルとトークナイザの読み込み
bert_tokenizer = BertTokenizer.from_pretrained('bert-base-uncased')
bert_model = BertModel.from_pretrained('bert-base-uncased')

roberta_tokenizer = RobertaTokenizer.from_pretrained('roberta-base')
roberta_model = RobertaModel.from_pretrained('roberta-base')

# テスト文
sentences = [
    "The bank by the river was covered with wildflowers.",
    "She deposited money at the bank downtown.",
    "The stock market crashed dramatically last Tuesday."
]

print("=== BERTとRoBERTaの埋め込み比較 ===\n")

# 各文の[CLS]/<s>トークン埋め込みを取得
bert_embeddings = []
roberta_embeddings = []

for sent in sentences:
    # BERT
    bert_inputs = bert_tokenizer(sent, return_tensors='pt', padding=True, truncation=True)
    with torch.no_grad():
        bert_outputs = bert_model(**bert_inputs)
    bert_cls = bert_outputs.last_hidden_state[0, 0, :]  # [CLS]トークン
    bert_embeddings.append(bert_cls.numpy())

    # RoBERTa
    roberta_inputs = roberta_tokenizer(sent, return_tensors='pt', padding=True, truncation=True)
    with torch.no_grad():
        roberta_outputs = roberta_model(**roberta_inputs)
    roberta_cls = roberta_outputs.last_hidden_state[0, 0, :]  # <s>トークン
    roberta_embeddings.append(roberta_cls.numpy())

# コサイン類似度を計算
def cosine_similarity(a, b):
    return np.dot(a, b) / (np.linalg.norm(a) * np.linalg.norm(b))

print("文1: 川岸の bank")
print("文2: 銀行の bank")
print("文3: 株式市場(bank なし)\n")

print("--- BERT [CLS] 埋め込みのコサイン類似度 ---")
print(f"文1 vs 文2 (bank の多義語): {cosine_similarity(bert_embeddings[0], bert_embeddings[1]):.4f}")
print(f"文1 vs 文3 (異なるトピック): {cosine_similarity(bert_embeddings[0], bert_embeddings[2]):.4f}")
print(f"文2 vs 文3 (金融関連):      {cosine_similarity(bert_embeddings[1], bert_embeddings[2]):.4f}")

print()

print("--- RoBERTa <s> 埋め込みのコサイン類似度 ---")
print(f"文1 vs 文2 (bank の多義語): {cosine_similarity(roberta_embeddings[0], roberta_embeddings[1]):.4f}")
print(f"文1 vs 文3 (異なるトピック): {cosine_similarity(roberta_embeddings[0], roberta_embeddings[2]):.4f}")
print(f"文2 vs 文3 (金融関連):      {cosine_similarity(roberta_embeddings[1], roberta_embeddings[2]):.4f}")

この実験では、”bank” という多義語を含む2つの文(川岸としてのbank、銀行としてのbank)と、金融に関連するがbankを含まない文の3つを比較しています。BERTとRoBERTaそれぞれの [CLS]/<s> トークンの埋め込みのコサイン類似度を計算することで、各モデルが文の意味をどの程度正確に捉えているかを確認できます。RoBERTaでは、文2(銀行としてのbank)と文3(株式市場)の類似度がBERTよりも高くなり、金融というトピックの関連性をより的確に捉えている傾向が見られます。同時に、文1(川岸のbank)と文2(銀行のbank)の類似度がBERTよりも低くなれば、bankの異なる意味を適切に区別できていることを示します。

マスク予測の比較

BERTとRoBERTaのMLM能力をさらに直接的に比較してみましょう。

from transformers import pipeline
import matplotlib.pyplot as plt

# マスク埋めパイプラインの構築
bert_fill = pipeline('fill-mask', model='bert-base-uncased')
roberta_fill = pipeline('fill-mask', model='roberta-base')

# テストケース: 文脈から適切な単語を予測
test_cases = [
    {
        "bert_text": "The scientist published a [MASK] in the journal.",
        "roberta_text": "The scientist published a <mask> in the journal.",
        "expected": "paper",
        "description": "学術文脈"
    },
    {
        "bert_text": "The [MASK] orbits around the sun.",
        "roberta_text": "The <mask> orbits around the sun.",
        "expected": "earth",
        "description": "天文文脈"
    },
    {
        "bert_text": "The patient was diagnosed with a rare [MASK].",
        "roberta_text": "The patient was diagnosed with a rare <mask>.",
        "expected": "disease",
        "description": "医療文脈"
    }
]

print("=== BERT vs RoBERTa: マスク予測比較 ===\n")

for case in test_cases:
    print(f"[{case['description']}]")
    print(f"  期待される単語: {case['expected']}")

    bert_results = bert_fill(case['bert_text'])
    roberta_results = roberta_fill(case['roberta_text'])

    print(f"  BERT Top-3:")
    for r in bert_results[:3]:
        print(f"    {r['token_str']:15s} (スコア: {r['score']:.4f})")

    print(f"  RoBERTa Top-3:")
    for r in roberta_results[:3]:
        print(f"    {r['token_str']:15s} (スコア: {r['score']:.4f})")
    print()

マスク予測の比較結果を見ると、BERTとRoBERTaの両方が文脈に基づいた合理的な予測を行いますが、RoBERTaは一般により高い確信度(スコア)でTop-1予測を行う傾向があります。これは、RoBERTaがより多くのデータでより長く学習されていることに加え、動的マスキングによってより多様なマスクパターンで訓練されているためです。特に、専門的な文脈(医学用語や学術用語)での予測精度の差は、160GBの多様なコーパスで学習したRoBERTaの強みが発揮される場面です。

GLUEタスクでの性能比較の可視化

最後に、GLUEベンチマークでのBERTとRoBERTaの性能差を可視化します。

import matplotlib.pyplot as plt
import numpy as np

# GLUEベンチマークのスコア(論文から)
tasks = ['MNLI-m', 'QQP', 'QNLI', 'SST-2', 'CoLA', 'STS-B', 'MRPC', 'RTE']

bert_scores = [86.6, 91.3, 92.3, 93.2, 60.6, 90.0, 88.0, 70.4]
xlnet_scores = [89.8, 91.8, 93.9, 95.6, 63.6, 91.8, 89.2, 83.8]
roberta_scores = [90.2, 92.2, 94.7, 96.4, 68.0, 92.4, 90.9, 86.6]

x = np.arange(len(tasks))
width = 0.25

fig, ax = plt.subplots(figsize=(14, 7))

bars1 = ax.bar(x - width, bert_scores, width, label='BERT-Large', color='#42A5F5', alpha=0.85)
bars2 = ax.bar(x, xlnet_scores, width, label='XLNet-Large', color='#66BB6A', alpha=0.85)
bars3 = ax.bar(x + width, roberta_scores, width, label='RoBERTa', color='#FF7043', alpha=0.85)

ax.set_xlabel('GLUE Tasks', fontsize=13)
ax.set_ylabel('Score', fontsize=13)
ax.set_title('GLUE Benchmark: BERT vs XLNet vs RoBERTa', fontsize=15, fontweight='bold')
ax.set_xticks(x)
ax.set_xticklabels(tasks, fontsize=11)
ax.legend(fontsize=12)

# Y軸の範囲を調整して差が見やすいように
ax.set_ylim(55, 100)
ax.grid(axis='y', alpha=0.3)

# 各バーの上にスコアを表示
for bars in [bars1, bars2, bars3]:
    for bar in bars:
        height = bar.get_height()
        ax.annotate(f'{height:.1f}',
                    xy=(bar.get_x() + bar.get_width() / 2, height),
                    xytext=(0, 3),
                    textcoords="offset points",
                    ha='center', va='bottom', fontsize=8)

plt.tight_layout()
plt.savefig('glue_comparison.png', dpi=150, bbox_inches='tight')
plt.show()

このグラフから、RoBERTa(オレンジ)がGLUEの全8タスクでBERT-Large(青)を上回り、さらにアーキテクチャを変更したXLNet-Large(緑)をもほとんどのタスクで超えていることが一目で確認できます。特にCoLA(+7.4ポイント)とRTE(+16.2ポイント)での改善が顕著で、これらはデータが少ない小規模タスクです。事前学習の質が高いRoBERTaは、少量のファインチューニングデータでも優れた転移学習性能を発揮します。XLNetはTransformer-XLの相対位置エンコーディングや並べ替え言語モデルという新しいアーキテクチャ要素を導入していますが、学習レシピを最適化しただけのRoBERTaに全体として及ばないという結果は、「学習設定の最適化」の威力を雄弁に物語っています。

次に、消去実験の効果を可視化します。

import matplotlib.pyplot as plt
import numpy as np

# 消去実験のデータ(MNLI-mの精度)
ablation_labels = [
    'BERT-Large\n(Baseline)',
    '+ Dynamic\nMasking',
    '+ FULL-SENT\n(No NSP)',
    '+ Large Batch\n(8K)',
    '+ More Data\n(160GB)'
]
ablation_mnli = [84.3, 84.7, 85.2, 85.7, 87.6]
ablation_sst2 = [92.5, 92.9, 93.4, 93.8, 95.3]

# 各ステップでの改善幅
improvements_mnli = [0] + [ablation_mnli[i] - ablation_mnli[i-1] for i in range(1, len(ablation_mnli))]
improvements_sst2 = [0] + [ablation_sst2[i] - ablation_sst2[i-1] for i in range(1, len(ablation_sst2))]

fig, axes = plt.subplots(1, 2, figsize=(16, 6))

# MNLI-m の累積改善
colors = ['#90CAF9', '#42A5F5', '#1E88E5', '#1565C0', '#0D47A1']
axes[0].bar(range(len(ablation_labels)), ablation_mnli, color=colors, edgecolor='white', linewidth=1.5)
axes[0].set_xticks(range(len(ablation_labels)))
axes[0].set_xticklabels(ablation_labels, fontsize=9)
axes[0].set_ylabel('Accuracy (%)', fontsize=12)
axes[0].set_title('MNLI-m: Ablation Study', fontsize=13, fontweight='bold')
axes[0].set_ylim(83, 89)
axes[0].grid(axis='y', alpha=0.3)

# 改善幅を表示
for i, (val, imp) in enumerate(zip(ablation_mnli, improvements_mnli)):
    axes[0].text(i, val + 0.1, f'{val:.1f}', ha='center', fontsize=10, fontweight='bold')
    if imp > 0:
        axes[0].text(i, val - 0.3, f'+{imp:.1f}', ha='center', fontsize=9, color='red')

# SST-2 の累積改善
colors_sst = ['#FFCC80', '#FFA726', '#FB8C00', '#EF6C00', '#E65100']
axes[1].bar(range(len(ablation_labels)), ablation_sst2, color=colors_sst, edgecolor='white', linewidth=1.5)
axes[1].set_xticks(range(len(ablation_labels)))
axes[1].set_xticklabels(ablation_labels, fontsize=9)
axes[1].set_ylabel('Accuracy (%)', fontsize=12)
axes[1].set_title('SST-2: Ablation Study', fontsize=13, fontweight='bold')
axes[1].set_ylim(91.5, 96)
axes[1].grid(axis='y', alpha=0.3)

for i, (val, imp) in enumerate(zip(ablation_sst2, improvements_sst2)):
    axes[1].text(i, val + 0.05, f'{val:.1f}', ha='center', fontsize=10, fontweight='bold')
    if imp > 0:
        axes[1].text(i, val - 0.2, f'+{imp:.1f}', ha='center', fontsize=9, color='red')

plt.tight_layout()
plt.savefig('ablation_study.png', dpi=150, bbox_inches='tight')
plt.show()

# 各改良の貢献度を出力
print("=== 各改良の貢献度(MNLI-m) ===")
total_improvement = ablation_mnli[-1] - ablation_mnli[0]
for i in range(1, len(ablation_labels)):
    imp = improvements_mnli[i]
    pct = imp / total_improvement * 100
    print(f"{ablation_labels[i].replace(chr(10), ' ')}: +{imp:.1f} ({pct:.0f}%)")
print(f"合計改善: +{total_improvement:.1f}")

この消去実験の可視化からは、各改良の累積的な効果が明確に読み取れます。MNLI-mでは合計3.3ポイントの改善のうち、データ増量が+1.9ポイント(約58%)を占めており、最大の貢献をしています。動的マスキング(+0.4、約12%)、NSP廃止(+0.5、約15%)、大バッチ(+0.5、約15%)はそれぞれ控えめな改善ですが、いずれもプラスに寄与しており、4つの改良が相乗的に機能していることがわかります。SST-2でも同様のパターンが見られ、データ増量の効果が最も大きいという一貫した傾向が確認できます。

まとめ

本記事では、RoBERTaがBERTのアーキテクチャを一切変えずに、学習設定の最適化だけで大幅な性能向上を達成した仕組みについて解説しました。

  • 動的マスキング: 毎バッチ新しいマスクを生成し、事実上無限のマスクパターンによるデータ拡張効果を得る
  • NSPの廃止: 有害だったNSPタスクを除去し、FULL-SENTENCES入力で長い文脈から効率的にMLMを学習する
  • 大バッチ(8K): 勾配推定の精度を高め、分散学習との親和性も向上させる
  • 大規模データ(160GB)と長い学習(500Kステップ): 消去実験で最大の貢献を示し、「十分なデータで十分に学習する」ことの重要性を実証した
  • 研究方法論: 各改良の効果を丁寧な消去実験で個別に検証する姿勢は、機械学習研究の模範である

RoBERTaの教訓は、「新しいアーキテクチャを試す前に、既存モデルの学習設定を十分に最適化しているか確認すべき」という普遍的なメッセージです。この教訓は、後続のALBERT、ELECTRA、DeBERTaといったモデルの開発にも強い影響を与えました。

次のステップとして、以下の記事も参考にしてください。

画像なし
ALBERTとDistilBERT — BERTの軽量化手法
パラメータ共有やモデル蒸留によるBERTの軽量化手法を解説します
画像なし
ELECTRAのアーキテクチャと事前学習
Replaced Token Detectionによる効率的な事前学習手法を解説します
画像なし
DeBERTaのアーキテクチャと改良点
Disentangled AttentionとEnhanced Mask Decoderによる改良を解説します