BARTの事前学習と応用 — ノイズ除去自己符号化器としての設計と要約タスクへの活用

ニュース記事を3行に要約したい、議事録から決定事項だけを抽出したい — こうした「長い文章を短くまとめる」タスクは、自然言語処理のなかでも特に実用性の高い応用です。しかし、要約を正確に行うには、文章全体の意味を理解し、かつ自然な日本語で結果を生成する、2つの能力が同時に求められます。

BERTは文章全体を双方向に読むことで「理解」に優れますが、テキストを生成する能力を持ちません。GPTは流暢なテキストを「生成」できますが、左から右への一方向しか見ないため文脈の全体把握が苦手です。では、この2つの長所を1つのモデルに統合できないでしょうか?

2019年にFacebookAI(現Meta AI)のLewisらが発表したBART(Bidirectional and Auto-Regressive Transformers)は、まさにこの問いに答えるモデルです。BARTは、TransformerのEncoder-Decoder構造を採用し、Encoderで双方向の文脈理解を、Decoderで自己回帰的なテキスト生成を行います。さらに、ノイズ除去自己符号化器(Denoising Autoencoder)として事前学習を行うという独創的な設計により、入力テキストにさまざまなノイズを加え、元のテキストを復元するタスクを通じて、言語の深い構造を獲得します。

BARTのノイズ除去自己符号化の概念

BARTの事前学習は、上の図のように「元のテキストをわざと壊し、それをEncoder-Decoderで元に戻す」という流れで進みます。ノイズ関数で破損させたテキストをEncoderが双方向に読み、Decoderが自己回帰的に元の全文を復元します。復元結果と正解テキストのクロスエントロピーを最小化することで、モデルは言語の文法・意味・文脈依存を自然に身につけます。この「壊して直す」という発想が、本記事を貫く中心テーマです。

BARTの設計思想を理解すると、以下のような場面で直接的に役立ちます。

  • テキスト要約システムの設計: BARTはCNN/DailyMailなどの要約ベンチマークで当時の最高性能を達成し、要約タスクにおけるデファクトスタンダードとなりました
  • ノイズ除去事前学習の理解: テキストを意図的に壊して復元する学習手法は、後続のmBARTやPEGASUSにも継承される重要な設計パターンです
  • Encoder-Decoderモデルの位置づけ: T5と並んで、Encoder-Decoderが生成タスクでDecoder-onlyモデルと同等以上の性能を発揮できることを示した重要な事例です
  • 最新モデルの理論的基盤: mBART(多言語BART)やBARThez(フランス語BART)など、BARTの亜種を理解するための出発点になります

本記事の内容

  • BERTとGPTの「いいとこ取り」としてのBARTの設計思想
  • ノイズ除去自己符号化器(Denoising Autoencoder)の理論
  • 5つのノイズ関数(Token Masking / Token Deletion / Text Infilling / Sentence Permutation / Document Rotation)の詳細
  • Encoder-Decoderアーキテクチャの数学的定式化
  • 復元損失(Reconstruction Loss)の導出
  • ファインチューニング戦略(分類・要約・翻訳)
  • Pythonでの5つのノイズ関数のスクラッチ実装と可視化
  • Hugging Face BARTを使った要約デモ

前提知識

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

画像なし
Transformerアーキテクチャの全体像をわかりやすく解説
Encoder-Decoder構造、位置エンコーディング、残差接続とLayer Normalizationの仕組みを解説します。
画像なし
BERTのアーキテクチャと事前学習を解説
BERTの双方向エンコーダ、MLMとNSPの目的関数、ファインチューニング戦略を解説します。
画像なし
GPTのアーキテクチャと自己回帰生成を解説
GPTの自己回帰言語モデル、因果マスク、テキスト生成戦略の定式化を解説します。
画像なし
T5(Text-to-Text Transfer Transformer)の理論と設計思想を完全解説
すべてのNLPタスクをテキスト生成に統一するフレームワークの理論を解説します。

BERTのマスク言語モデル(MLM)とGPTの自己回帰言語モデルの仕組みを理解していると、BARTがなぜ「両者の統合」と呼ばれるのかが自然にわかります。

それでは、BARTの核心である「ノイズを加えて復元する」という事前学習の発想から見ていきましょう。

BARTの設計思想 — BERTとGPTの統合

BERTとGPTの限界

BARTの設計思想を理解するには、まずBERTとGPTが「何ができて、何ができないか」を整理する必要があります。

BERT(Encoder型) はTransformerのEncoderを積層し、入力系列の全トークンを双方向に参照できます。Masked Language Model(MLM)という事前学習タスクにより、マスクされたトークンの前後の文脈を同時に活用して予測します。この双方向性のおかげで、BERTは文分類、固有表現認識、質問応答などの「理解」タスクに優れます。しかし、BERTはEncoder-only構造であるため、テキストを逐次的に生成する能力を持ちません。要約や翻訳のような生成タスクには、そのまま適用できないのです。

GPT(Decoder型) はTransformerのDecoderを積層し、左から右への因果マスクにより自己回帰的にテキストを生成します。流暢で自然な文を生成する能力に優れますが、各トークンは自分より前のトークンしか参照できないという制約があります。つまり、入力テキスト全体を「見渡す」ことができないため、入力の意味を深く理解したうえで応答する条件付き生成タスクでは不利になります。

この状況をたとえるなら、BERTは「試験問題をすべて読んでから解答する優等生」、GPTは「前の行だけ見ながら次の行を書き続ける小説家」です。理想は、「全体を読み通したうえで、新しい文章を生成できる」モデルです。

BERT・GPT・BARTの位置づけ比較

この図は3つのモデルの構造的な違いを並べたものです。BERTはEncoderだけを積み、各トークンが双方向に参照し合うため理解に強い一方、テキストを生成できません。GPTはDecoderだけを積み、左から右への一方向で生成しますが全体を見渡せません。BARTはEncoderとDecoderの両方を持ち、さらにCross-Attentionで両者をつなぐことで、「双方向の理解」と「自己回帰の生成」を1つのモデルに同居させています。

BARTの解決策:Encoder-Decoder構造

BARTは、この問題に対してTransformerのEncoder-Decoder構造をそのまま使うという、ある意味で最も素直な解決策を採りました。

  • Encoder(BERT的): 入力テキストを双方向に読み、文脈を理解する
  • Decoder(GPT的): Encoderの出力を参照しながら、自己回帰的にテキストを生成する

つまり、BARTは「理解はBERT、生成はGPT」という役割分担をEncoder-Decoder構造で実現しています。この考え方自体は元祖Transformer(Vaswani et al., 2017)と同じですが、BARTの革新は事前学習の方法にあります。

BERTはランダムにトークンをマスクして予測するMLMで学習しました。GPTは次のトークンを予測する自己回帰で学習しました。BARTは、これらよりもはるかに多様な方法で入力テキストを「壊し」、元のテキストを復元するタスクで学習します。この「テキストを壊して復元する」という枠組みが、ノイズ除去自己符号化器(Denoising Autoencoder)です。

事前学習目的の比較

この図は3つの事前学習目的の「壊し方」と「予測対象」を対比したものです。BERTはマスク位置のトークンを前後文脈から当てるだけ、GPTは左のトークンだけを見て次を予測するだけです。これに対しBARTは、多様なノイズで壊した入力から元の全文を生成します。BARTがBERTのMLMを特別な一例(Token Maskingのみ)として含みつつ、より一般的な枠組みになっていることが見て取れます。

では、このノイズ除去自己符号化器とはどのような仕組みなのか、理論的な背景から見ていきましょう。

ノイズ除去自己符号化器(Denoising Autoencoder)

自己符号化器の考え方

ノイズ除去自己符号化器の仕組みを理解するには、まず自己符号化器(Autoencoder)の考え方を押さえておきましょう。

自己符号化器は、入力をいったん低次元の潜在表現(latent representation)に圧縮し、そこから元の入力を復元するモデルです。画像処理の文脈では、入力画像をエンコーダで圧縮し、デコーダで復元する構造としてよく知られています。入力と出力が同じものを再現しようとするので「自己(auto)符号化器」と呼ばれます。

しかし、単に入力をそのまま出力にコピーするだけでは、モデルは何も学びません。自己符号化器が有効な表現を学ぶためには、何らかの制約が必要です。古典的な自己符号化器では「潜在表現の次元を入力より小さくする」ことで情報のボトルネックを作りましたが、ノイズ除去自己符号化器ではもっと直接的なアプローチを取ります。

ノイズ除去の発想

ノイズ除去自己符号化器(Denoising Autoencoder; DAE)の発想はシンプルです。入力にノイズを加えてから、元の(ノイズのない)入力を復元するようにモデルを訓練します。

$$ \tilde{\bm{x}} = \text{corrupt}(\bm{x}) $$

$$ \hat{\bm{x}} = \text{Decoder}(\text{Encoder}(\tilde{\bm{x}})) $$

$$ \mathcal{L} = \text{loss}(\hat{\bm{x}}, \bm{x}) $$

ここで $\bm{x}$ が元のテキスト、$\tilde{\bm{x}}$ がノイズを加えたテキスト、$\hat{\bm{x}}$ がモデルの復元出力です。

この枠組みがなぜ有効なのでしょうか? ノイズが加わった不完全な入力から元のテキストを復元するには、モデルは言語の構造(文法、意味、文脈の依存関係)を深く理解する必要があるからです。たとえば、文中の単語が消えている場合、周囲の文脈から消えた単語を推測しなければなりません。文の順序がバラバラになっている場合、文の論理的なつながりを理解して正しい順序を復元しなければなりません。

BARTにおけるノイズ除去自己符号化器

BARTの事前学習は、この枠組みをテキストに適用したものです。元の文書 $\bm{x} = (x_1, x_2, \dots, x_n)$ にノイズ関数 $g$ を適用して壊れたテキスト $\tilde{\bm{x}} = g(\bm{x})$ を作り、Encoder-Decoderモデルで元のテキスト $\bm{x}$ を復元します。

重要なポイントは、BERTのMLMが「個々のトークンの穴埋め」であるのに対し、BARTは「テキスト全体の復元」を行うという点です。BERTではマスクされた位置のトークンだけを予測しますが、BARTのDecoderは元のテキスト全体を自己回帰的に生成します。これにより、BARTは単なる穴埋め能力だけでなく、テキスト全体の一貫性を保つ生成能力も同時に獲得します。

もう1つの重要な違いは、ノイズの多様性です。BERTのノイズはトークンのマスキング1種類ですが、BARTは5種類の異なるノイズ関数を用意しています。異なる種類のノイズを与えることで、モデルはさまざまな言語的知識(単語の意味、句の構造、文の順序、文書の構成)をバランスよく学習します。

それでは、BARTが採用する5つのノイズ関数を1つずつ見ていきましょう。

5つのノイズ関数

BARTの独創性の核心は、入力テキストを壊す方法(ノイズ関数)にあります。原論文では5つのノイズ関数が提案されており、それぞれが異なるタイプの言語知識をモデルに学ばせます。イメージとしては、試験勉強で「穴埋め問題」「並べ替え問題」「要約問題」など、さまざまな形式の問題に取り組むことで多面的な理解力を養うようなものです。

BARTの5つのノイズ関数

この図は同じ文 “The cat sat on the mat” に5種類のノイズを適用した結果を並べたものです。①トークンマスクは一部を [MASK] に置き換え、②トークン削除はトークン自体を消してトークン数を減らします。③スパン穴埋めは複数の連続トークンを1つの [MASK] にまとめます。④文の並べ替えは文単位で順序を入れ替え(色が文IDに対応)、⑤文書の回転は途中の位置を先頭にして全体を回します。同じ文でも壊し方がまったく異なることが視覚的にわかります。

1. Token Masking(トークンマスキング)

Token Maskingは、ランダムに選んだトークンを特殊トークン [MASK] に置き換えるノイズです。これはBERTのMLMと同じ操作です。

元のテキスト: "The cat sat on the mat"

ノイズ適用後: "The [MASK] sat on [MASK] mat"

モデルは周囲の文脈から、マスクされた位置に "cat""the" が入ることを予測する必要があります。この操作は単語レベルの意味理解を学ばせます。

2. Token Deletion(トークン削除)

Token Deletionは、ランダムに選んだトークンを入力から完全に削除するノイズです。Token Maskingとの違いは、マスクトークンという「目印」が残らない点です。

元のテキスト: "The cat sat on the mat"

ノイズ適用後: "The sat on mat"

マスキングでは [MASK] の位置を見れば「ここに何かがあった」とわかりますが、削除では「どこから何が消えたのか」すら分かりません。モデルは、消えたトークンの存在そのものを検出し、その位置と内容を推定する必要があるため、Token Maskingよりも高度な言語知識が求められます。

3. Text Infilling(テキスト穴埋め)

Text Infillingは、BARTのノイズ関数のなかで最も重要かつ独創的なものです。複数の連続するトークン(スパン)を1つの [MASK] トークンに置き換えます。スパンの長さはポアソン分布($\lambda = 3$)からサンプリングされます。

元のテキスト: "The cat sat on the mat in the room"

ノイズ適用後: "The [MASK] the mat [MASK] room"

この操作は T5 の Span Corruption に似ていますが、決定的な違いがあります。T5 ではマスクされたスパンごとに異なるセンチネルトークン(<extra_id_0>, <extra_id_1>, …)を使い、出力もセンチネルに対応するスパンだけを生成します。一方、BARTは全てのスパンを同じ [MASK] に置き換え、出力は元のテキスト全体を復元します。

ここで重要なのは、1つの [MASK] が0個以上の任意の数のトークンを表している可能性があるという点です。スパン長が0の場合は [MASK] が挿入されるだけで、実際には何も消えていません。つまりモデルは、各 [MASK] に対して「何トークン分の情報が失われているか」を文脈から推定する必要があります。これはToken Maskingの「1対1の置き換え」よりもはるかに困難であり、句構造や文法の理解を要求します。

4. Sentence Permutation(文の並べ替え)

Sentence Permutationは、文書内の文をランダムに並べ替えるノイズです。

元のテキスト: "A cat sat on the mat. It was a rainy day. The cat looked out the window."

ノイズ適用後: "The cat looked out the window. A cat sat on the mat. It was a rainy day."

このノイズは、個々の文はそのまま保存し、文の順序だけを崩します。元の文書を正しく復元するには、モデルは文同士の論理的なつながり(因果関係、時間の前後関係、代名詞の指示先)を理解する必要があります。

5. Document Rotation(文書の回転)

Document Rotationは、文書内のランダムな位置を先頭にして「回転」させるノイズです。

元のテキスト: "A B C D E F G"

ランダムにトークン D を選んだ場合: "D E F G A B C"

この操作は、文書の真の開始位置を見つけるタスクをモデルに課します。文書の冒頭には通常、トピックの導入や背景情報が置かれるという文書構造の知識を学ばせます。

ノイズ関数の組み合わせと効果

原論文の実験では、5つのノイズ関数を個別に評価した結果、Text Infillingが最も効果的であることが示されました。特に、Text InfillingとSentence Permutationを組み合わせた場合に、多くのタスクで最高性能が得られています。

各ノイズ関数がモデルに学ばせる能力をまとめると、以下のようになります。

ノイズ関数 操作 学ばせる能力
Token Masking トークンを [MASK] に置換 単語の意味理解
Token Deletion トークンを削除 欠損の検出と位置の推定
Text Infilling スパンを1つの [MASK] に置換 句構造・スパン長の推定
Sentence Permutation 文の順序をシャッフル 文間の論理的関係
Document Rotation 文書の開始位置を移動 文書構造の理解

ノイズ関数が学ばせる言語階層

この図は、各ノイズ関数が「言語のどの階層」を学ばせるかを整理したものです。Token Masking/Deletionは単語レベル、Text Infillingは句レベル、Sentence Permutationは文レベル、Document Rotationは文書レベルに対応します。BARTは単一のノイズに頼らず、単語から文書まで階層的に異なる構造を同時にカバーしているため、下流のさまざまなタスクにバランスよく適応できます。

このように、5つのノイズ関数はそれぞれ異なるレベルの言語知識を要求します。単語レベル(Token Masking/Deletion)、句レベル(Text Infilling)、文レベル(Sentence Permutation)、文書レベル(Document Rotation)と、階層的に異なる言語構造をカバーしている点がBARTの設計の巧みさです。

ノイズ関数の仕組みがわかったところで、次にBARTのEncoder-Decoderアーキテクチャの数学的な構造を見ていきましょう。

BARTのEncoder-Decoderアーキテクチャ

全体構造

BARTは、Vaswaniらの元祖Transformerと同じEncoder-Decoder構造を基本としつつ、いくつかの修正を加えています。

BARTのEncoder-Decoder構造

この図はBARTのデータフローを示しています。左のEncoderは破損入力の全トークンを双方向(マスクなし)に参照して文脈表現 $\bm{H}_{\text{enc}}$ を作ります。右のDecoderは因果マスクで左から右へ生成しつつ、赤い矢印で示したCross-AttentionによってEncoderの出力を参照します。つまりDecoderは「これまで生成したトークン」と「入力全体の理解」の両方を見ながら次トークンを決めます。この左右の役割分担が、BARTが理解と生成を両立できる仕組みの核心です。

項目 BART-Base BART-Large
Encoderレイヤー数 6 12
Decoderレイヤー数 6 12
隠れ次元 $d_{\text{model}}$ 768 1024
アテンションヘッド数 12 16
パラメータ数 約140M 約400M

BART-Baseのサイズ設定はBERT-Baseとほぼ同等であり、BART-LargeはBERT-Largeに対応します。これにより、BERTとの公平な性能比較が可能になっています。

GPT-2に基づく修正

BARTは、元祖Transformerに対してGPT-2で採用されたいくつかの修正を取り入れています。

  1. 活性化関数: ReLUの代わりにGeLU(Gaussian Error Linear Unit)を使用
  2. パラメータ初期化: $\mathcal{N}(0, 0.02)$ からの初期化
  3. Pre-Norm: 元祖TransformerのPost-Norm(サブレイヤーの出力後にLayerNorm)ではなく、サブレイヤーの入力前にLayerNormを適用するPre-Normを採用

GeLU活性化関数は以下のように定義されます。

$$ \text{GeLU}(x) = x \cdot \Phi(x) = x \cdot \frac{1}{2}\left[1 + \text{erf}\left(\frac{x}{\sqrt{2}}\right)\right] $$

ここで $\Phi(x)$ は標準正規分布の累積分布関数です。GeLUはReLUと異なり、$x = 0$ 付近でなめらかに0に遷移するため、勾配が不連続になる問題を回避できます。

Encoderの構造

BARTのEncoderは、BERTと同様にTransformer Encoderブロックを $L$ 層積層した構造です。

ノイズが加わった入力テキスト $\tilde{\bm{x}} = (\tilde{x}_1, \tilde{x}_2, \dots, \tilde{x}_M)$ に対して、まずトークン埋め込みと位置埋め込みを加算します。

$$ \bm{H}^{(0)} = \text{Embed}(\tilde{\bm{x}}) + \text{PE} $$

ここで $\text{Embed}(\tilde{\bm{x}}) \in \mathbb{R}^{M \times d_{\text{model}}}$ はトークン埋め込み、$\text{PE} \in \mathbb{R}^{M \times d_{\text{model}}}$ は学習可能な位置埋め込みです。BARTは正弦波位置エンコーディングではなく、学習可能な位置埋め込みを使用する点に注意してください。

各Encoderレイヤー $l = 1, \dots, L$ は、Multi-Head Self-AttentionとFeed-Forward Networkの2つのサブレイヤーで構成されます。Pre-Norm形式で書くと、

Self-Attentionサブレイヤーでは、まずLayerNormを適用してからAttentionを計算し、残差接続で入力と加算します。

$$ \bm{Z}^{(l)} = \bm{H}^{(l-1)} + \text{MultiHead}(\text{LN}(\bm{H}^{(l-1)})) $$

続いてFFNサブレイヤーでも同様に、LayerNormを適用してからFFNを通し、残差接続で加算します。

$$ \bm{H}^{(l)} = \bm{Z}^{(l)} + \text{FFN}(\text{LN}(\bm{Z}^{(l)})) $$

ここでFFNは2層のネットワークです。

$$ \text{FFN}(\bm{z}) = \text{GeLU}(\bm{z}\bm{W}_1 + \bm{b}_1)\bm{W}_2 + \bm{b}_2 $$

$\bm{W}_1 \in \mathbb{R}^{d_{\text{model}} \times d_{\text{ff}}}$, $\bm{W}_2 \in \mathbb{R}^{d_{\text{ff}} \times d_{\text{model}}}$ で、$d_{\text{ff}} = 4 \times d_{\text{model}}$ が標準設定です。

Encoderの最終出力 $\bm{H}^{(L)}_{\text{enc}} \in \mathbb{R}^{M \times d_{\text{model}}}$ は、ノイズ入力の文脈表現であり、これがDecoderに渡されます。

重要: BERTと同様に、EncoderのSelf-Attentionにはマスク(因果マスク)がありません。各トークンは入力系列の全トークンを双方向に参照できます。これがBARTの「理解力」の源泉です。

Decoderの構造

BARTのDecoderは、GPTと同様に自己回帰的に動作しますが、加えてEncoderの出力を参照するCross-Attention層を持つ点が異なります。

Decoderへの入力は、復元すべき元のテキスト $\bm{y} = (y_1, y_2, \dots, y_N)$ を1トークン右にシフトしたものです(教師強制)。

$$ \bm{S}^{(0)} = \text{Embed}(\bm{y}_{\text{shifted}}) + \text{PE}_{\text{dec}} $$

各Decoderレイヤー $l = 1, \dots, L$ は、3つのサブレイヤーで構成されます。

Masked Self-Attention(因果的自己注意)では、各トークンが自分より前のトークンだけを参照します。因果マスク $\bm{M}$ を適用します。

$$ \bm{A}^{(l)} = \bm{S}^{(l-1)} + \text{MaskedMultiHead}(\text{LN}(\bm{S}^{(l-1)})) $$

Cross-Attention(Encoder-Decoder Attention)では、DecoderのQueryがEncoderの最終出力をKey/Valueとして参照します。

$$ \bm{B}^{(l)} = \bm{A}^{(l)} + \text{CrossMultiHead}(\text{LN}(\bm{A}^{(l)}), \bm{H}^{(L)}_{\text{enc}}) $$

具体的には、Cross-AttentionのQuery, Key, Valueは次のように計算されます。

$$ \bm{Q} = \text{LN}(\bm{A}^{(l)})\bm{W}^Q, \quad \bm{K} = \bm{H}^{(L)}_{\text{enc}}\bm{W}^K, \quad \bm{V} = \bm{H}^{(L)}_{\text{enc}}\bm{W}^V $$

QueryはDecoderの現在の表現から、Key/ValueはEncoderの出力から作られます。これにより、Decoderは「今生成しようとしているトークンに対して、入力テキストのどの部分が関連しているか」をAttentionで判断できます。

FFN サブレイヤーはEncoderと同じ構造です。

$$ \bm{S}^{(l)} = \bm{B}^{(l)} + \text{FFN}(\text{LN}(\bm{B}^{(l)})) $$

Decoderの最終出力を語彙サイズの線形層に通し、Softmaxで次トークンの確率分布を得ます。

$$ P(y_t \mid y_{

ここで $\bm{W}_{\text{vocab}} \in \mathbb{R}^{d_{\text{model}} \times V}$ で、$V$ は語彙サイズです。

BERTとの構造的な違い

BARTとBERTは、ともにTransformerベースですが、いくつかの重要な構造的違いがあります。

特徴 BERT BART
構造 Encoder-only Encoder-Decoder
出力層 マスク位置のトークン予測 全トークンの自己回帰生成
Decoder なし 因果マスク + Cross-Attention
位置エンコーディング 学習可能 学習可能
Cross-Attention なし Decoderの各層に配置
生成能力 なし(判別モデル) あり(生成モデル)

BERTでは、Encoderの出力をそのまま分類ヘッドに接続して予測を行いますが、BARTではEncoderの出力はDecoderへの「入力情報」にすぎず、最終的な予測はDecoderが自己回帰的に行います。

アーキテクチャの全体像が見えたところで、BARTの事前学習を数学的に定式化していきましょう。

復元損失の導出

事前学習の目的関数

BARTの事前学習の目的は、ノイズが加わったテキスト $\tilde{\bm{x}}$ が与えられたとき、元のテキスト $\bm{x} = (x_1, x_2, \dots, x_N)$ を復元する確率を最大化することです。

Decoderは自己回帰的に動作するため、元のテキストの生成確率は条件付き確率の積として分解されます。

$$ P(\bm{x} \mid \tilde{\bm{x}}; \theta) = \prod_{t=1}^{N} P(x_t \mid x_1, \dots, x_{t-1}, \tilde{\bm{x}}; \theta) $$

ここで $\theta$ はモデルのパラメータ(Encoder + Decoder全体)です。

負の対数尤度

最大化したい対数尤度は次のように書けます。

$$ \log P(\bm{x} \mid \tilde{\bm{x}}; \theta) = \sum_{t=1}^{N} \log P(x_t \mid x_{

ここで $x_{

対数尤度を最大化することは、負の対数尤度を最小化することと同値ですので、損失関数は次のようになります。

$$ \mathcal{L}_{\text{BART}}(\theta) = -\sum_{t=1}^{N} \log P(x_t \mid x_{

各項の展開

各時刻 $t$ における条件付き確率は、Decoderの最終層の出力 $\bm{s}_t^{(L)}$ を語彙分布に射影して得られます。

$$ P(x_t = w \mid x_{

ここで $\bm{e}_w \in \mathbb{R}^{d_{\text{model}}}$ は単語 $w$ の出力埋め込みベクトルです。

この式を対数に変換すると、クロスエントロピー損失の形になります。

$$ -\log P(x_t \mid x_{

右辺第1項は正解トークン $x_t$ のスコア(大きいほど良い)、第2項はすべての語彙のスコアのログサムエクスポネンシャル(正規化定数)です。学習が進むと、正解トークンのスコアが相対的に高くなり、損失が減少します。

BERTのMLM損失との比較

BERTのMLM損失は、マスクされた位置 $\mathcal{M}$ のトークンだけを予測します。

$$ \mathcal{L}_{\text{BERT}} = -\sum_{t \in \mathcal{M}} \log P(x_t \mid \tilde{\bm{x}}; \theta) $$

ここで $\mathcal{M}$ はマスクされたトークンの位置の集合であり、入力全体の約15%です。

一方、BARTの損失は元のテキストの全トークンを対象とします。

$$ \mathcal{L}_{\text{BART}} = -\sum_{t=1}^{N} \log P(x_t \mid x_{

この違いには重要な含意があります。BERTはマスクされた15%のトークンだけから学習シグナルを得ますが、BARTは100%のトークンから学習シグナルを得ます。1つの訓練サンプルあたりの情報量がBARTのほうが多いため、学習効率の面で有利です。

復元損失の学習シグナル比較

この図は、1つのサンプルから得られる学習シグナルの量を比較したものです。左のBERT(MLM)では、マスクされた約15%の位置だけが損失計算の対象になり、残りのトークンは学習に寄与しません。右のBARTでは、Decoderが元のテキスト全体を生成するため、全トークンが損失に寄与します。同じ1サンプルでもBARTのほうがはるかに多くの教師信号を引き出せることが一目でわかります。

ミニバッチでの損失

実際の学習では、ミニバッチ $\mathcal{B} = \{(\bm{x}^{(1)}, \tilde{\bm{x}}^{(1)}), \dots, (\bm{x}^{(B)}, \tilde{\bm{x}}^{(B)})\}$ に対して損失を平均化します。

$$ \mathcal{L}_{\text{batch}} = -\frac{1}{B} \sum_{i=1}^{B} \frac{1}{N_i} \sum_{t=1}^{N_i} \log P(x_t^{(i)} \mid x_{

ここで $N_i$ は $i$ 番目のサンプルのトークン数であり、サンプルごとにトークン数で正規化しています。

復元損失の数学的な構造が明確になりました。次に、この事前学習済みモデルをどのように下流タスクに適用するか、ファインチューニング戦略を見ていきましょう。

ファインチューニング戦略

BARTの大きな強みは、Encoder-Decoder構造を持つことで、理解タスクと生成タスクの両方にファインチューニングできる柔軟性にあります。タスクの種類に応じて、BARTの異なる部分を活用します。

ファインチューニング戦略

この図は、代表的な2つのファインチューニングの流れを示しています。左の文分類では、入力をEncoderとDecoderの両方に通し、末尾トークン </s> の表現を分類ヘッドに渡してクラス確率を出します。右の要約では、長い文書をEncoderに入れ、DecoderがCross-Attentionで参照しながら短い要約を生成します。特に要約は、事前学習の「破損文から元の全文を復元する」流れと構造がそっくりであり、BARTが要約タスクに強い理由が直感的にわかります。

文分類タスク

文分類(感情分析、自然言語推論など)では、入力テキストをEncoderとDecoderの両方に入力し、Decoderの最終トークン位置の出力を分類ヘッドに接続します。

具体的には、入力テキストの末尾に特殊トークン </s>(BARTにおける文末トークン)を追加し、その位置に対応するDecoderの最終隠れ状態 $\bm{s}_{\text{last}}^{(L)}$ を線形層で分類します。

$$ P(c \mid \bm{x}) = \text{softmax}(\bm{W}_c \bm{s}_{\text{last}}^{(L)} + \bm{b}_c) $$

ここで $c$ はクラスラベル、$\bm{W}_c \in \mathbb{R}^{C \times d_{\text{model}}}$ は分類ヘッドの重みです。

なぜBERTのように [CLS] トークンではなく末尾トークンを使うのでしょうか? これはDecoderの因果マスクに起因します。Decoderの最終位置のトークンは、自己回帰的にそれまでの全てのトークンを参照できるため、入力全体の情報を集約した表現になります。BERTの [CLS] に相当する役割を果たすわけです。

要約・翻訳タスク(生成タスク)

要約や翻訳のような系列変換タスクは、BARTのEncoder-Decoder構造がそのまま適合します。入力テキストをEncoderに入力し、Decoderが出力テキストを自己回帰的に生成します。

要約タスクのファインチューニングでは、入力が元の文書、出力が要約文となるペアデータを用いて、通常のクロスエントロピー損失で学習します。

$$ \mathcal{L}_{\text{summ}} = -\sum_{t=1}^{N_{\text{out}}} \log P(y_t \mid y_{

ここで $\bm{x}$ は入力文書、$\bm{y} = (y_1, \dots, y_{N_{\text{out}}})$ は正解要約です。

事前学習の「壊されたテキストから元のテキストを復元する」タスクは、要約タスクの「長い文書から要約を生成する」タスクと構造的に類似しています。どちらも、入力情報を理解して、それに基づく新しいテキストを生成するという点で共通しています。この構造的な類似性が、BARTが要約タスクで特に優れた性能を示す理由の1つです。

機械翻訳タスク

機械翻訳にBARTを適用する際は、興味深い工夫が行われます。BARTのEncoderは英語で事前学習されていますが、翻訳では入力がソース言語(たとえばドイツ語)になります。

Lewis らは、BARTのEncoderの入力側に新しいEncoder(ランダム初期化)を追加するアプローチを提案しました。新しいEncoderがソース言語を処理し、その出力をBARTのEncoderの入力として与えます。学習は2段階で行われます。

  1. 第1段階: BARTのパラメータを固定し、新しいEncoderのみを学習する
  2. 第2段階: 全パラメータを小さい学習率でファインチューニングする

この2段階学習により、事前学習で獲得した英語の知識を壊すことなく、新しい言語の知識を統合できます。

BERTやGPTとの適用範囲の比較

各モデルが得意とするタスクの範囲を整理します。

タスク BERT GPT BART
文分類 得意 可能 得意
固有表現認識 得意 不得意 得意
質問応答(抽出型) 得意 可能 得意
テキスト要約 不可 可能 最も得意
機械翻訳 不可 可能 得意
テキスト生成 不可 最も得意 得意

BARTはBERTの得意な理解タスクとGPTの得意な生成タスクの両方をカバーできる汎用性を持っています。特に「入力を理解して出力を生成する」タイプのタスク(要約、翻訳、質問応答の回答生成)で、最も自然にEncoder-Decoder構造が活きます。

ここまでで、BARTの理論的な枠組みが一通りわかりました。次に、Pythonでノイズ関数を実装し、各ノイズがテキストをどのように変換するかを具体的に確認しましょう。

Pythonでのノイズ関数の実装

実装の方針

5つのノイズ関数をPythonでスクラッチ実装し、同じテキストに対して各ノイズを適用した結果を比較します。ここではトークナイザとして簡易的な空白分割を使用し、ノイズ関数のロジックに集中します。

import numpy as np
import matplotlib.pyplot as plt
from typing import List, Tuple

np.random.seed(42)

# サンプルテキスト(文単位で分割できるように "." で区切る)
sample_text = (
    "The Transformer architecture was introduced in 2017 . "
    "It relies on self-attention mechanisms . "
    "BERT uses the encoder part of the Transformer . "
    "GPT uses the decoder part of the Transformer . "
    "BART combines both encoder and decoder ."
)

# 簡易トークナイザ(空白分割)
def tokenize(text: str) -> List[str]:
    return text.strip().split()

def detokenize(tokens: List[str]) -> str:
    return " ".join(tokens)

tokens = tokenize(sample_text)
print(f"元のトークン数: {len(tokens)}")
print(f"元のテキスト:\n{sample_text.strip()}\n")

まず基本のトークン化関数を定義しました。実際のBARTではBPE(Byte Pair Encoding)トークナイザを使いますが、ノイズ関数の動作を理解するうえでは空白分割で十分です。

続いて、5つのノイズ関数を1つずつ実装していきます。

# === 1. Token Masking ===
def token_masking(tokens: List[str], mask_ratio: float = 0.15) -> List[str]:
    """ランダムにトークンを [MASK] に置き換える"""
    result = tokens.copy()
    n_mask = max(1, int(len(tokens) * mask_ratio))
    mask_indices = np.random.choice(len(tokens), size=n_mask, replace=False)
    for idx in mask_indices:
        result[idx] = "[MASK]"
    return result

# === 2. Token Deletion ===
def token_deletion(tokens: List[str], delete_ratio: float = 0.15) -> List[str]:
    """ランダムにトークンを削除する"""
    n_delete = max(1, int(len(tokens) * delete_ratio))
    delete_indices = set(np.random.choice(len(tokens), size=n_delete, replace=False))
    return [t for i, t in enumerate(tokens) if i not in delete_indices]

# === 3. Text Infilling ===
def text_infilling(tokens: List[str], mask_ratio: float = 0.15,
                   poisson_lambda: float = 3.0) -> List[str]:
    """スパンをポアソン分布の長さでサンプリングし、1つの [MASK] に置き換える"""
    n_to_mask = max(1, int(len(tokens) * mask_ratio))
    masked = 0
    mask_map = [False] * len(tokens)

    while masked < n_to_mask:
        # スパン長をポアソン分布からサンプリング(最低1)
        span_len = max(1, np.random.poisson(poisson_lambda))
        span_len = min(span_len, n_to_mask - masked, len(tokens))
        # スパンの開始位置をランダムに選択
        start = np.random.randint(0, max(1, len(tokens) - span_len + 1))
        for i in range(start, min(start + span_len, len(tokens))):
            if not mask_map[i]:
                mask_map[i] = True
                masked += 1

    # マスクされた連続区間を1つの [MASK] に置換
    result = []
    in_mask = False
    for i, t in enumerate(tokens):
        if mask_map[i]:
            if not in_mask:
                result.append("[MASK]")
                in_mask = True
        else:
            result.append(t)
            in_mask = False
    return result

# === 4. Sentence Permutation ===
def sentence_permutation(tokens: List[str], sep: str = ".") -> List[str]:
    """文("." で区切る)の順序をランダムにシャッフルする"""
    # 文に分割
    sentences = []
    current = []
    for t in tokens:
        current.append(t)
        if t == sep:
            sentences.append(current)
            current = []
    if current:
        sentences.append(current)

    # シャッフル
    perm = np.random.permutation(len(sentences))
    shuffled = []
    for idx in perm:
        shuffled.extend(sentences[idx])
    return shuffled

# === 5. Document Rotation ===
def document_rotation(tokens: List[str]) -> List[str]:
    """ランダムな位置を先頭にして文書を回転させる"""
    if len(tokens) <= 1:
        return tokens.copy()
    pivot = np.random.randint(1, len(tokens))
    return tokens[pivot:] + tokens[:pivot]

各ノイズ関数の結果を一覧で確認しましょう。

# 各ノイズ関数を適用して結果を表示
np.random.seed(42)

noise_functions = [
    ("Token Masking", token_masking),
    ("Token Deletion", token_deletion),
    ("Text Infilling", text_infilling),
    ("Sentence Permutation", sentence_permutation),
    ("Document Rotation", document_rotation),
]

results = {}
for name, func in noise_functions:
    np.random.seed(42)  # 再現性のためリセット
    noised = func(tokens)
    results[name] = noised
    print(f"=== {name} ===")
    print(f"トークン数: {len(tokens)} -> {len(noised)}")
    print(f"結果: {detokenize(noised)}\n")

上の出力から、各ノイズ関数がテキストをどのように変換するかが具体的にわかります。Token Maskingではトークン数が変わらず [MASK] に置換されるだけですが、Token Deletionではトークン数が減少します。Text Infillingでは複数トークンが1つの [MASK] に圧縮されるため、さらにトークン数が減ります。Sentence Permutationでは全トークンが保存されますが文の順序が変わり、Document Rotationでは文書の途中から始まるテキストになります。

これらの違いを可視化して、より直感的に理解しましょう。

fig, axes = plt.subplots(3, 2, figsize=(14, 12))
fig.suptitle("BART Noise Functions Comparison", fontsize=16, fontweight="bold")

# 元のテキストのトークン位置を色分けで可視化
colors_original = plt.cm.tab20(np.linspace(0, 1, len(tokens)))

# 各ノイズ関数の効果を棒グラフで表示
ax = axes[0, 0]
lengths = [len(tokens)] + [len(results[name]) for name, _ in noise_functions]
labels = ["Original"] + [name.replace(" ", "\n") for name, _ in noise_functions]
bar_colors = ["#2ecc71"] + ["#e74c3c"] * 5
ax.barh(range(len(lengths)), lengths, color=bar_colors, alpha=0.8, edgecolor="white")
ax.set_yticks(range(len(lengths)))
ax.set_yticklabels(labels, fontsize=9)
ax.set_xlabel("Number of Tokens")
ax.set_title("Token Count After Noise")
ax.invert_yaxis()
for i, v in enumerate(lengths):
    ax.text(v + 0.3, i, str(v), va="center", fontsize=10)

# Token Masking: マスク位置の可視化
ax = axes[0, 1]
np.random.seed(42)
masked_tokens = token_masking(tokens)
mask_positions = [1 if t == "[MASK]" else 0 for t in masked_tokens]
ax.bar(range(len(masked_tokens)), [1]*len(masked_tokens),
       color=["#e74c3c" if m else "#3498db" for m in mask_positions],
       alpha=0.8, edgecolor="white")
ax.set_xlabel("Token Position")
ax.set_title("Token Masking (red = [MASK])")
ax.set_yticks([])

# Token Deletion: 残ったトークンの可視化
ax = axes[1, 0]
np.random.seed(42)
deleted_tokens = token_deletion(tokens)
ax.bar(range(len(tokens)), [1]*len(tokens), color="#3498db", alpha=0.3,
       edgecolor="white", label="Original")
ax.bar(range(len(deleted_tokens)), [0.7]*len(deleted_tokens), color="#2ecc71",
       alpha=0.8, edgecolor="white", label="After Deletion")
ax.set_xlabel("Token Position")
ax.set_title("Token Deletion (green = remaining)")
ax.set_yticks([])
ax.legend(fontsize=9)

# Text Infilling: スパンマスクの可視化
ax = axes[1, 1]
np.random.seed(42)
infilled_tokens = text_infilling(tokens)
colors_infill = ["#e74c3c" if t == "[MASK]" else "#3498db" for t in infilled_tokens]
ax.bar(range(len(infilled_tokens)), [1]*len(infilled_tokens),
       color=colors_infill, alpha=0.8, edgecolor="white")
ax.set_xlabel("Token Position")
ax.set_title("Text Infilling (red = [MASK] replacing span)")
ax.set_yticks([])

# Sentence Permutation: 文の順序変化
ax = axes[2, 0]
np.random.seed(42)
perm_tokens = sentence_permutation(tokens)
# 文ごとに色を割り当て
original_sentences = sample_text.strip().split(" . ")
sent_colors_map = plt.cm.Set2(np.linspace(0, 1, len(original_sentences)))
token_sent_colors = []
sent_idx = 0
count = 0
for t in perm_tokens:
    token_sent_colors.append(sent_colors_map[sent_idx % len(sent_colors_map)])
    if t == ".":
        sent_idx += 1
ax.bar(range(len(perm_tokens)), [1]*len(perm_tokens),
       color=token_sent_colors, alpha=0.8, edgecolor="white")
ax.set_xlabel("Token Position")
ax.set_title("Sentence Permutation (color = sentence ID)")
ax.set_yticks([])

# Document Rotation: 回転位置の可視化
ax = axes[2, 1]
np.random.seed(42)
rotated_tokens = document_rotation(tokens)
pivot = len(tokens) - len(tokens)  # 概念的な可視化
colors_rot = ["#e74c3c" if i < len(rotated_tokens) - np.random.randint(1, len(tokens))
              else "#3498db" for i in range(len(rotated_tokens))]
# 実際のpivotを再計算
np.random.seed(42)
pivot_pos = np.random.randint(1, len(tokens))
colors_rot = ["#e74c3c" if i < len(tokens) - pivot_pos else "#3498db"
              for i in range(len(rotated_tokens))]
ax.bar(range(len(rotated_tokens)), [1]*len(rotated_tokens),
       color=colors_rot, alpha=0.8, edgecolor="white")
ax.set_xlabel("Token Position")
ax.set_title(f"Document Rotation (pivot at token {pivot_pos})")
ax.set_yticks([])

plt.tight_layout()
plt.savefig("bart_noise_functions.png", dpi=150, bbox_inches="tight")
plt.show()

上の可視化から、5つのノイズ関数がテキストに与える影響の違いが一目でわかります。左上のトークン数比較では、Token DeletionとText Infillingがトークン数を減少させるのに対し、Token MaskingとSentence Permutation/Document Rotationはトークン数を保存していることが確認できます。Token Maskingの赤い棒は [MASK] に置き換えられた位置を示しており、入力系列の約15%がマスクされています。Text Infillingでは、複数の連続トークンが1つの [MASK] に圧縮されるため、赤い棒の数がToken Maskingよりも少なくなります。Sentence Permutationでは各色が異なる文を表しており、文の順序がシャッフルされていることが色の並びから視覚的に確認できます。

Text Infillingのスパン長分布の可視化

Text Infillingで使われるポアソン分布のパラメータ $\lambda = 3$ が、スパン長にどのような分布をもたらすかを確認しましょう。

fig, axes = plt.subplots(1, 2, figsize=(12, 5))

# ポアソン分布 (lambda=3) の確率質量関数
ax = axes[0]
from scipy.stats import poisson
k = np.arange(0, 15)
pmf = poisson.pmf(k, mu=3)
ax.bar(k, pmf, color="#3498db", alpha=0.8, edgecolor="white")
ax.set_xlabel("Span Length $k$")
ax.set_ylabel("Probability $P(X=k)$")
ax.set_title("Poisson Distribution ($\\lambda=3$)")
ax.set_xticks(k)
for i, p in enumerate(pmf):
    if p > 0.01:
        ax.text(i, p + 0.005, f"{p:.3f}", ha="center", fontsize=8)

# 実際にサンプリングしたスパン長のヒストグラム
ax = axes[1]
np.random.seed(0)
span_lengths = np.random.poisson(lam=3, size=10000)
span_lengths = np.clip(span_lengths, 1, None)  # 最低1
ax.hist(span_lengths, bins=range(1, 16), density=True,
        color="#e74c3c", alpha=0.8, edgecolor="white", align="left")
ax.set_xlabel("Span Length")
ax.set_ylabel("Density")
ax.set_title("Sampled Span Lengths (clipped to min=1)")
ax.set_xticks(range(1, 15))

plt.tight_layout()
plt.savefig("bart_poisson_span.png", dpi=150, bbox_inches="tight")
plt.show()

ポアソン分布によるスパン長

左のグラフはポアソン分布 $\text{Poi}(\lambda=3)$ の理論的な確率質量関数を示しています。$k=3$ で最大確率(約0.224)を取り、$k=0$ から $k=8$ 程度の範囲に分布が集中しています。右のグラフは実際にサンプリングした10,000個のスパン長(最低値を1に制限)のヒストグラムで、理論分布とよく一致しています。$\lambda = 3$ という設定は、平均3トークンのスパンをマスクすることを意味し、短い句(2〜4語の名詞句や動詞句)程度の長さに相当します。これにより、モデルは句レベルの構造を学習しやすくなります。

次に、ノイズ関数の効果をより定量的に評価し、Hugging FaceのBARTモデルを使った実際の要約タスクを実装しましょう。

BARTの事前学習と復元のシミュレーション

実際のBARTの事前学習を完全に再現するには大規模なコーパスと計算資源が必要ですが、ノイズ関数→Encoder→Decoder→復元という流れの概念を、小さなスケールで確認しましょう。

import numpy as np
import matplotlib.pyplot as plt

# ノイズ適用率を変化させたときのトークン残存率を計算
mask_ratios = np.linspace(0.0, 0.5, 50)

# Token Masking: トークン数は変わらない([MASK]に置換されるだけ)
# Token Deletion: (1 - ratio) のトークンが残る
# Text Infilling: 概算として、マスク対象トークンのうちスパン数分の[MASK]が残る

token_masking_remaining = np.ones_like(mask_ratios)  # 常に100%(トークン数不変)
token_deletion_remaining = 1.0 - mask_ratios
# Text Infilling: マスクされるトークン数 / 平均スパン長 = スパン数
# 残りトークン = (1-ratio)*N + スパン数 の [MASK]
lambda_param = 3.0
infilling_remaining = (1.0 - mask_ratios) + mask_ratios / lambda_param

fig, ax = plt.subplots(figsize=(10, 6))

ax.plot(mask_ratios * 100, token_masking_remaining * 100,
        "o-", color="#3498db", label="Token Masking", markersize=3)
ax.plot(mask_ratios * 100, token_deletion_remaining * 100,
        "s-", color="#e74c3c", label="Token Deletion", markersize=3)
ax.plot(mask_ratios * 100, infilling_remaining * 100,
        "^-", color="#2ecc71", label="Text Infilling ($\\lambda=3$)", markersize=3)
ax.axhline(y=100, color="gray", linestyle="--", alpha=0.5)
ax.axvline(x=15, color="gray", linestyle=":", alpha=0.5, label="BART default (15%)")

ax.set_xlabel("Noise Ratio (%)", fontsize=12)
ax.set_ylabel("Remaining Token Ratio (%)", fontsize=12)
ax.set_title("Effect of Noise Ratio on Sequence Length", fontsize=14)
ax.legend(fontsize=11)
ax.set_xlim(0, 50)
ax.set_ylim(50, 105)
ax.grid(True, alpha=0.3)

plt.tight_layout()
plt.savefig("bart_noise_ratio.png", dpi=150, bbox_inches="tight")
plt.show()

ノイズ比率と系列長の変化

このグラフから、ノイズ比率に対する各ノイズ関数の入力系列長の変化が読み取れます。Token Masking(青)はノイズ比率に関わらず系列長が100%で一定です。これは [MASK] に置換するだけで系列の長さが変わらないためです。Token Deletion(赤)はノイズ比率に比例して系列長が線形に減少します。Text Infilling(緑)はToken Deletionより系列長の減少が緩やかです。これは、複数トークンのスパンが1つの [MASK] に置き換わるため、削除よりもトークンの残存率が高くなるためです。BARTのデフォルト設定(15%、灰色の点線)では、Text Infillingの系列長は約90%に保たれます。

この系列長の違いは計算コストにも影響します。EncoderのSelf-Attentionは入力長の2乗に比例するため、Token Deletionのように系列が短くなるノイズは、計算効率の面でも有利です。ただし、原論文の実験ではText Infillingが最も高いタスク性能を示しており、性能と効率のバランスが重要になります。

理論的な理解を深めたところで、最後にHugging FaceのBARTモデルを使った実際の要約タスクを実装しましょう。

Hugging Face BARTを使った要約デモ

BARTによるテキスト要約

Hugging Faceの transformers ライブラリを使えば、事前学習済みのBARTモデルを数行のコードで利用できます。ここでは facebook/bart-large-cnn(CNN/DailyMailデータセットでファインチューニング済み)を使って、英語のニュース記事を要約してみましょう。

from transformers import BartTokenizer, BartForConditionalGeneration

# BART-large-cnn(要約タスク用ファインチューニング済み)
model_name = "facebook/bart-large-cnn"
tokenizer = BartTokenizer.from_pretrained(model_name)
model = BartForConditionalGeneration.from_pretrained(model_name)

# 要約対象のテキスト
article = """
The tower is 324 metres (1,063 ft) tall, about the same height as an 81-storey
building, and the tallest structure in Paris. Its base is square, measuring 125
metres (410 ft) on each side. During its construction, the Eiffel Tower surpassed
the Washington Monument to become the tallest man-made structure in the world, a
title it held for 41 years until the Chrysler Building in New York City was
finished in 1930. It was the first structure to reach a height of 300 metres.
Due to the addition of a broadcasting aerial at the top of the tower in 1957,
it is now taller than the Chrysler Building by 5.2 metres (17 ft). Excluding
transmitters, the Eiffel Tower is the second tallest free-standing structure in
France after the Millau Viaduct.
"""

# トークン化
inputs = tokenizer(article, max_length=1024, return_tensors="pt", truncation=True)

print(f"入力トークン数: {inputs['input_ids'].shape[1]}")

# 要約生成
summary_ids = model.generate(
    inputs["input_ids"],
    num_beams=4,           # ビームサーチ(4ビーム)
    max_length=80,         # 最大出力長
    min_length=20,         # 最小出力長
    length_penalty=2.0,    # 長い出力にペナルティ
    early_stopping=True
)

summary = tokenizer.decode(summary_ids[0], skip_special_tokens=True)
print(f"\n=== 生成された要約 ===")
print(summary)
print(f"\n要約トークン数: {len(tokenizer.encode(summary))}")
print(f"圧縮率: {len(tokenizer.encode(summary)) / inputs['input_ids'].shape[1]:.2%}")

上のコードを実行すると、エッフェル塔に関する長い英文が数文に要約されます。入力テキストの約100トークン超が20〜30トークン程度に圧縮され、高さや歴史的な事実など重要な情報が保持された要約が生成されます。ビームサーチ(num_beams=4)を使うことで、貪欲法よりも品質の高い要約が得られます。length_penalty=2.0 は長い出力を抑制するパラメータで、値が大きいほど短い要約を生成する傾向があります。

BARTのEncoder出力の可視化

BARTのEncoderがテキストをどのような内部表現に変換しているかを、次元削減して可視化してみましょう。

import torch
import numpy as np
import matplotlib.pyplot as plt
from sklearn.decomposition import PCA
from transformers import BartTokenizer, BartModel

# BARTモデル(ベースモデル)
model_name = "facebook/bart-large"
tokenizer = BartTokenizer.from_pretrained(model_name)
model = BartModel.from_pretrained(model_name)
model.eval()

# 異なるカテゴリの文を用意
sentences = [
    # 科学系
    "The speed of light in vacuum is approximately 299792458 meters per second.",
    "Quantum mechanics describes the behavior of particles at atomic scales.",
    "DNA carries the genetic instructions for all living organisms.",
    # スポーツ系
    "The soccer World Cup is held every four years.",
    "Basketball requires teamwork and individual skill.",
    "Tennis matches can last several hours on clay courts.",
    # 技術系
    "Neural networks are inspired by the structure of biological brains.",
    "Cloud computing provides scalable resources on demand.",
    "Encryption algorithms protect sensitive data from unauthorized access.",
]

categories = ["Science"] * 3 + ["Sports"] * 3 + ["Technology"] * 3
colors_map = {"Science": "#3498db", "Sports": "#e74c3c", "Technology": "#2ecc71"}

# Encoder出力を取得
embeddings = []
with torch.no_grad():
    for sent in sentences:
        inputs = tokenizer(sent, return_tensors="pt", truncation=True)
        outputs = model.encoder(
            input_ids=inputs["input_ids"],
            attention_mask=inputs["attention_mask"]
        )
        # [CLS] 相当(先頭トークン)の表現を使用
        cls_embedding = outputs.last_hidden_state[:, 0, :].squeeze().numpy()
        embeddings.append(cls_embedding)

embeddings = np.array(embeddings)

# PCAで2次元に削減
pca = PCA(n_components=2)
embeddings_2d = pca.fit_transform(embeddings)

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

for cat in ["Science", "Sports", "Technology"]:
    mask = [c == cat for c in categories]
    ax.scatter(embeddings_2d[mask, 0], embeddings_2d[mask, 1],
               c=colors_map[cat], label=cat, s=120, alpha=0.8, edgecolors="white")

# ラベル付き
for i, sent in enumerate(sentences):
    short_label = sent[:40] + "..."
    ax.annotate(short_label, (embeddings_2d[i, 0], embeddings_2d[i, 1]),
                fontsize=7, alpha=0.7,
                xytext=(5, 5), textcoords="offset points")

ax.set_xlabel(f"PC1 ({pca.explained_variance_ratio_[0]:.1%} variance)")
ax.set_ylabel(f"PC2 ({pca.explained_variance_ratio_[1]:.1%} variance)")
ax.set_title("BART Encoder Representations (PCA)", fontsize=14)
ax.legend(fontsize=11)
ax.grid(True, alpha=0.3)

plt.tight_layout()
plt.savefig("bart_encoder_pca.png", dpi=150, bbox_inches="tight")
plt.show()

PCAによる2次元射影では、同じカテゴリの文が近くにクラスタリングされる傾向が観察されます。科学系の文(青)、スポーツ系の文(赤)、技術系の文(緑)がそれぞれまとまった領域に配置されることが期待されます。これは、BARTのEncoderが事前学習を通じて意味的に類似した文を近い表現にマッピングする能力を獲得していることを示しています。特に興味深いのは、「Neural networks are inspired by…」のような文が、科学と技術の中間に位置する可能性がある点です。ニューラルネットワークは生物学(科学)に着想を得た技術であり、BARTの表現がこうした意味的なニュアンスを捉えているかどうかを確認できます。

BARTモデルの構造の確認

最後に、BARTモデルの内部構造を確認して、これまで解説してきたアーキテクチャが実際のモデルにどのように実装されているかを見ましょう。

from transformers import BartForConditionalGeneration

model = BartForConditionalGeneration.from_pretrained("facebook/bart-base")

# モデル全体のパラメータ数
total_params = sum(p.numel() for p in model.parameters())
encoder_params = sum(p.numel() for p in model.model.encoder.parameters())
decoder_params = sum(p.numel() for p in model.model.decoder.parameters())
lm_head_params = sum(p.numel() for p in model.lm_head.parameters())

print("=== BART-Base モデル構造 ===")
print(f"総パラメータ数: {total_params:,}")
print(f"  Encoder: {encoder_params:,} ({encoder_params/total_params:.1%})")
print(f"  Decoder: {decoder_params:,} ({decoder_params/total_params:.1%})")
print(f"  LM Head: {lm_head_params:,} ({lm_head_params/total_params:.1%})")

print(f"\n=== Encoder構造 ===")
print(f"レイヤー数: {len(model.model.encoder.layers)}")
print(f"隠れ次元: {model.config.d_model}")
print(f"FFN次元: {model.config.encoder_ffn_dim}")
print(f"ヘッド数: {model.config.encoder_attention_heads}")

print(f"\n=== Decoder構造 ===")
print(f"レイヤー数: {len(model.model.decoder.layers)}")
print(f"FFN次元: {model.config.decoder_ffn_dim}")
print(f"ヘッド数: {model.config.decoder_attention_heads}")

# Decoderレイヤーの内部構造を確認
print(f"\n=== Decoder Layer 0 の構成要素 ===")
layer0 = model.model.decoder.layers[0]
for name, module in layer0.named_children():
    param_count = sum(p.numel() for p in module.parameters())
    print(f"  {name}: {param_count:,} params")

出力を確認すると、BART-Baseの総パラメータ数は約1.4億であり、EncoderとDecoderがほぼ同数のパラメータを持っていることがわかります。Decoderの各レイヤーには self_attn(Masked Self-Attention)、encoder_attn(Cross-Attention)、fc1/fc2(FFN)が含まれており、先ほど解説した3つのサブレイヤー構造と一致します。encoder_attn がCross-Attention層であり、これがBARTのDecoderがEncoderの出力を参照するための仕組みです。

# パラメータ分布を円グラフで可視化
fig, axes = plt.subplots(1, 2, figsize=(14, 6))

# モデル全体のパラメータ分布
ax = axes[0]
sizes = [encoder_params, decoder_params - lm_head_params, lm_head_params]
labels = [f"Encoder\n{encoder_params/1e6:.1f}M",
          f"Decoder\n{(decoder_params-lm_head_params)/1e6:.1f}M",
          f"LM Head\n{lm_head_params/1e6:.1f}M"]
colors = ["#3498db", "#e74c3c", "#2ecc71"]
ax.pie(sizes, labels=labels, colors=colors, autopct="%1.1f%%",
       startangle=90, textprops={"fontsize": 10})
ax.set_title("BART-Base Parameter Distribution", fontsize=13)

# Decoderレイヤーの内部構造
ax = axes[1]
layer0 = model.model.decoder.layers[0]
component_sizes = []
component_labels = []
component_colors = ["#3498db", "#9b59b6", "#e74c3c", "#f39c12", "#2ecc71",
                    "#1abc9c", "#e67e22", "#34495e"]
for i, (name, module) in enumerate(layer0.named_children()):
    pc = sum(p.numel() for p in module.parameters())
    component_sizes.append(pc)
    component_labels.append(f"{name}\n{pc/1e6:.2f}M")

ax.pie(component_sizes, labels=component_labels,
       colors=component_colors[:len(component_sizes)],
       autopct="%1.1f%%", startangle=90, textprops={"fontsize": 9})
ax.set_title("Decoder Layer 0 - Component Breakdown", fontsize=13)

plt.tight_layout()
plt.savefig("bart_params.png", dpi=150, bbox_inches="tight")
plt.show()

BART-Baseのパラメータ分布

左の円グラフから、BARTのパラメータがEncoder、Decoder、LM Headにどのように分配されているかがわかります。EncoderとDecoderはほぼ同規模ですが、DecoderはCross-Attention層を持つ分だけわずかにパラメータが多くなっています。右の円グラフでは、Decoderの1レイヤー内でFFN(fc1 + fc2)が最も多くのパラメータを占めていることがわかります。これはTransformerの一般的な傾向で、FFNの中間次元 $d_{\text{ff}} = 4 \times d_{\text{model}}$ が大きいため、Self-AttentionやCross-Attentionの射影層よりもパラメータが多くなります。

まとめ

本記事では、BARTの設計思想、アーキテクチャ、事前学習手法、ファインチューニング戦略について解説しました。

  • BERTとGPTの統合: BARTはTransformerのEncoder-Decoder構造を採用し、Encoderで双方向の文脈理解(BERT的)、Decoderで自己回帰的な生成(GPT的)を実現しています
  • ノイズ除去自己符号化器: 入力テキストにノイズを加え、元のテキストを復元する事前学習により、単語レベルから文書レベルまでの多様な言語知識を獲得します
  • 5つのノイズ関数: Token Masking, Token Deletion, Text Infilling, Sentence Permutation, Document Rotation の5種類のノイズが、それぞれ異なるレベルの言語構造を学習させます。特にText InfillingとSentence Permutationの組み合わせが最も効果的です
  • 復元損失: 元のテキスト全体を対象とするクロスエントロピー損失で学習し、BERTのMLM損失(マスク位置のみ)よりも多くの学習シグナルを各サンプルから得られます
  • 要約タスクでの強み: 「壊れたテキストから元のテキストを復元する」事前学習は、「長い文書から要約を生成する」タスクと構造的に類似しており、BARTは要約ベンチマークで優れた性能を示しました
  • 柔軟なファインチューニング: 理解タスク(分類、NER)と生成タスク(要約、翻訳)の両方に適用可能な汎用性を持っています

BARTは、Encoder-Decoderアーキテクチャの事前学習手法として、T5と並ぶ重要なマイルストーンです。T5が「すべてをText-to-Textに統一する」というフレームワーク面での革新を起こしたのに対し、BARTは「多様なノイズ関数で言語の多面的な構造を学ぶ」という事前学習手法の面での革新を起こしました。

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

画像なし
T5(Text-to-Text Transfer Transformer)の理論と設計思想を完全解説
BARTと並ぶEncoder-DecoderモデルであるT5の設計思想とSpan Corruption事前学習を解説します。
画像なし
BERTのアーキテクチャと事前学習を解説
BARTのEncoder側の基盤となるBERTの双方向エンコーダとMLMの仕組みを解説します。
画像なし
GPTのアーキテクチャと自己回帰生成を解説
BARTのDecoder側の基盤となるGPTの自己回帰言語モデルと因果マスクを解説します。