「彼は銀行に___を預けた」— この空欄に入る単語は何でしょうか。多くの人は「お金」と即答するはずです。私たちは空欄の左側だけでなく、「預けた」という右側の文脈も同時に使って正解を推測しています。マスク言語モデル(Masked Language Model, MLM)は、まさにこの「穴埋め問題」を大規模テキストで繰り返すことにより、言語の深い構造を学習する手法です。
2018年にGoogleが発表したBERT(Bidirectional Encoder Representations from Transformers)は、MLMを事前学習の中核に据えることで、自然言語処理のベンチマークを一斉に塗り替えました。MLMによって獲得された文脈表現は、以下のような幅広いタスクの基盤となります。
- 質問応答(SQuAD): 文書中から回答を抽出する
- 感情分析(SST-2): 文章のポジティブ/ネガティブを判定する
- 固有表現抽出(NER): 人名・地名・組織名をテキストから特定する
- 文の類似度判定(STS-B): 2つの文がどれほど近い意味かをスコア化する
「なぜ穴埋め問題を解くだけで、これほど多様なタスクに転用できる表現が得られるのか?」— 本記事ではこの問いに、数式とコードの両面から答えていきます。
本記事の内容
- 自己回帰型言語モデルとマスク型言語モデルの違い
- MLMのマスク戦略と目的関数の数学的定式化
- MLMの学習ダイナミクスと表現学習としての解釈
- NSP(Next Sentence Prediction)との組み合わせと限界
- Whole Word Masking、SpanBERT、ELECTRAなどの改良手法
- PyTorchでの簡易MLMモデルのスクラッチ実装
- Hugging Face を使ったfill-maskパイプラインの実践
前提知識
この記事を読む前に、以下の記事を読んでおくと理解が深まります。
言語モデルの2つのパラダイム
言語モデルとは、端的に言えば「テキストに確率を割り当てるモデル」です。しかしその確率の求め方には、大きく分けて2つのアプローチがあります。まず「自分より前の単語だけ」を見て次の単語を予測する方法と、「前後両方の単語」を見て穴埋めを行う方法です。それぞれの考え方を見ていきましょう。
自己回帰型言語モデル(Causal Language Model, CLM)
自己回帰型言語モデルは、文を左から右へ順番に生成する方法です。GPTシリーズがこのアプローチの代表です。
文 $\bm{x} = (x_1, x_2, \ldots, x_T)$ の同時確率を、連鎖律で次のように分解します。
$$ P(\bm{x}) = \prod_{t=1}^{T} P(x_t \mid x_1, x_2, \ldots, x_{t-1}) $$
各ステップで「これまでに生成したトークン列」を条件として、次のトークンの確率分布を計算します。この形式は、文の生成には自然に適合します。小説の続きを書く、コードを補完するといった「左から右へ流れる」タスクは、まさに自己回帰型の得意分野です。
しかしここで重要な制約があります。時刻 $t$ でのトークン予測には、$x_1, \ldots, x_{t-1}$ という左側の文脈のみが使われます。$x_{t+1}$ 以降の情報、つまり右側の文脈は一切参照できません。
マスク型言語モデル(Masked Language Model, MLM)
一方、MLMは文の一部を隠し(マスクし)、残りの文脈全体を使ってマスクされたトークンを予測します。BERTシリーズがこのアプローチの代表です。
入力文のトークン集合からランダムにいくつかのトークンを選びマスク集合 $\mathcal{M}$ とし、マスクされていないトークン $\bm{x}_{\setminus \mathcal{M}}$ を条件としてマスクされたトークンを予測します。
$$ P(x_i \mid \bm{x}_{\setminus \mathcal{M}}; \theta), \quad i \in \mathcal{M} $$
ここで決定的に重要なのは、$\bm{x}_{\setminus \mathcal{M}}$ にはマスク位置の左右両方のトークンが含まれるという点です。
なぜ双方向が重要なのか
具体例で考えてみましょう。「彼は銀行にを預けた」という文では、空欄に入る単語は「お金」です。しかし「彼は銀行にを見つけた」であれば、答えは「巣穴」「宝物」など全く別のものになるかもしれません。
自己回帰型モデルでは、「彼は銀行に」という左側の文脈だけで次の単語を予測しなければなりません。「銀行」が金融機関なのか川の土手(bank)なのかは、この時点では曖昧です。一方MLMでは、「預けた」という右側の文脈も同時に参照できるため、「銀行 = 金融機関」「空欄 = お金」という推論が容易になります。
この双方向性は、文の理解(分類、抽出、質問応答)において特に大きなアドバンテージをもたらします。ある単語の意味を正確に把握するには、その単語の前後両方の文脈が必要だからです。
ただし、双方向性にはトレードオフもあります。自己回帰型モデルのように「次のトークンを順に生成する」というタスクには直接使えません。BERTが文生成には向かず、GPTが文理解には相対的に弱かった(少なくとも当初は)のは、このパラダイムの違いに起因します。
ここまでで、MLMが「双方向文脈を活用する」という点で自己回帰型モデルと根本的に異なることがわかりました。では、MLMは具体的にどのようなルールでトークンをマスクし、どのような目的関数で学習するのでしょうか。次のセクションでは、BERTにおけるMLMの数学的な定式化を詳しく見ていきます。
MLMの数学的定式化
MLMの核心は「何を、どのようにマスクし、何を最適化するか」に集約されます。ここではBERTの原論文に基づいて、マスク戦略と目的関数を厳密に定式化します。
マスク戦略: 15%の選択と80/10/10ルール
入力トークン列 $\bm{x} = (x_1, x_2, \ldots, x_T)$ から、全トークンの15% をランダムに選択してマスク対象集合 $\mathcal{M}$ とします。ここで $|\mathcal{M}| \approx 0.15T$ です。
なぜ15%なのでしょうか。マスク率が高すぎると、残りの文脈だけでは予測に必要な情報が不足します。逆にマスク率が低すぎると、1つのサンプルから得られる学習信号が少なくなり、学習効率が落ちます。BERTの著者らは実験的に15%が良いバランスであることを見出しました。
マスク対象に選ばれたトークン $x_i \; (i \in \mathcal{M})$ に対して、以下の3通りの処理をランダムに適用します。
| 確率 | 処理 | 具体例(元: “Tokyo”) |
|---|---|---|
| 80% | [MASK] トークンに置換 |
[MASK] |
| 10% | ランダムな別トークンに置換 | "apple" |
| 10% | 元のトークンのまま | "Tokyo" |
この操作を数式で書くと、マスク後のトークン $\tilde{x}_i$ は次のようになります。
$$ \tilde{x}_i = \begin{cases} \texttt{[MASK]} & \text{確率 } 0.80 \\ x_{\text{random}} & \text{確率 } 0.10 \\ x_i & \text{確率 } 0.10 \end{cases} \quad (i \in \mathcal{M}) $$
ここで $x_{\text{random}}$ は語彙 $\mathcal{V}$ から一様ランダムに選んだトークンです。
なぜ80/10/10の比率なのか
この一見奇妙な比率には、重要な技術的理由があります。
もし100%を[MASK]に置換したら何が起きるかを考えましょう。事前学習時、モデルは入力中に[MASK]トークンを大量に見ます。しかしファインチューニング時や推論時には、入力文に[MASK]は一切含まれません。これにより、事前学習時と推論時で入力の分布にミスマッチが生じます。モデルは「[MASK]がある文」の処理には長けていても、「普通の文」に対する汎化性能が劣化する恐れがあります。
- 80%を
[MASK]に: モデルがマスクされたトークンを文脈から予測する能力を主に学習します。これがMLMの主目的です - 10%をランダムトークンに: モデルは「この位置のトークンは正しいのか?」を常に判断する必要があり、各位置の表現に実際のトークン情報を反映させるようになります。これにより、
[MASK]が存在しない通常の入力に対してもロバストな表現が得られます - 10%をそのままに: ファインチューニング時の入力分布(
[MASK]がない通常テキスト)に近づけるバイアス補正の役割を果たします。また、モデルが「この位置は変更されていない」と学習し、観測されたトークンの表現をそのまま出力にコピーする能力も維持します
ランダム置換のわずか10%が表現を壊すのではないかという懸念は、実際にはほとんど影響しないことが実験で確認されています。全トークンの15%のうちの10%、つまり全体の1.5%しかランダム置換されないため、文脈情報全体に与える影響は軽微です。
目的関数
MLMの目的関数は、マスクされた各位置のトークンの負の対数尤度の和です。マスク後の入力列 $\tilde{\bm{x}}$ をTransformer Encoderに通して得られる隠れ表現を $\bm{h}_i$ とします。
まず、Transformer Encoderの最終層の出力 $\bm{h}_i \in \mathbb{R}^{d}$ を語彙サイズ $|\mathcal{V}|$ のロジットに変換します。この変換はMLMヘッドと呼ばれ、以下の処理を行います。
$$ \bm{z}_i = \bm{W}_o \, \text{GeLU}(\bm{W}_h \bm{h}_i + \bm{b}_h) + \bm{b}_o $$
ここで $\bm{W}_h \in \mathbb{R}^{d \times d}$、$\bm{W}_o \in \mathbb{R}^{|\mathcal{V}| \times d}$ は学習可能なパラメータ、GeLU は活性化関数です。BERTでは $\bm{W}_o$ に入力の埋め込み行列の転置を重み共有(weight tying)で用いることが多いです。
ロジット $\bm{z}_i$ にソフトマックスを適用して、位置 $i$ における各トークンの予測確率を得ます。
$$ P(x_i = w \mid \tilde{\bm{x}}; \theta) = \frac{\exp(z_{i,w})}{\sum_{w’ \in \mathcal{V}} \exp(z_{i,w’})} $$
ここで $z_{i,w}$ はトークン $w$ に対応するロジットの成分です。
MLMの損失関数は、マスクされた全位置にわたる交差エントロピーの合計です。
$$ \mathcal{L}_{\text{MLM}} = -\sum_{i \in \mathcal{M}} \log P(x_i \mid \tilde{\bm{x}}; \theta) $$
この式が意味するのは、「マスクされた各位置について、正解トークンに割り当てられる確率をできるだけ大きくせよ」ということです。モデルパラメータ $\theta$(Transformer Encoderの重み + MLMヘッドの重み)は、この損失を最小化するように更新されます。
重要な点として、損失の計算はマスクされた位置 $i \in \mathcal{M}$ に対してのみ行われます。マスクされていない位置のトークンについては、予測も損失計算も行いません。これは、マスクされていないトークンは自明にそのまま出力すればよいため、学習信号としての価値が低いからです。
ここまでで、MLMの「何を最適化するか」が明確になりました。しかし、この穴埋め問題をひたすら解くことで、モデルの内部ではいったい何が起きているのでしょうか。次のセクションでは、MLMの学習過程でモデルがどのような知識を獲得するのかを掘り下げます。
MLMの学習ダイナミクス
MLMは何を学んでいるのか
穴埋め問題を大量に解くだけで、なぜ汎用的な言語表現が得られるのか — これは直感的には不思議に思えるかもしれません。しかし、マスクされたトークンを正確に予測するために、モデルは以下の3種類の知識を暗黙的に獲得する必要があります。
構文的知識(Syntax)
「The cat ___ on the mat」の空欄を埋めるには、主語 “cat” が三人称単数であること、この位置には動詞が入ること、時制は現在形が適切であることを理解しなければなりません。つまりMLMの学習を通じて、品詞、語順、係り受けといった統語的な規則が内部表現にエンコードされます。Hewitt & Manning (2019) の研究では、BERTの隠れ表現に対して単純な線形変換を適用するだけで構文木が復元できることが示されており、これはMLMの学習が統語構造を自然に捉えていることの証拠です。
意味的知識(Semantics)
「彼はコーヒーを___で飲んだ」に対して「カップ」「マグカップ」は適切ですが、「皿」や「鍋」は不自然です。正解を予測するためには、単語間の意味的な関係(上位下位関係、同義関係、共起関係)を学ぶ必要があります。その結果、MLMで学習された埋め込みでは、意味の近い単語がベクトル空間上で近くに配置されます。
世界知識(World Knowledge)
「東京は___の首都である」に正解するには「日本」という事実を知っている必要があります。Petroni et al. (2019) のLAMA(LAnguage Model Analysis)プローブでは、BERTが穴埋め形式で問いかけるだけで、関係データベースやWikipedia知識に匹敵する正答率を示すことが確認されています。MLMの学習は、訓練コーパスに含まれる事実関係を暗黙的にパラメータにエンコードする効果を持つのです。
MLMの表現学習としての解釈
MLMは、タスクとしては単なる穴埋め予測ですが、表現学習の観点からは分布仮説(distributional hypothesis)の強力な実装と見なせます。分布仮説とは、「ある単語の意味は、その単語が出現する文脈によって決まる」という言語学の考え方です。
MLMでは、トークン $x_i$ をマスクし、周囲の文脈 $\bm{x}_{\setminus \mathcal{M}}$ から $x_i$ を復元するようにモデルを訓練します。このとき、Transformer Encoderの各層は文脈情報を統合し、マスク位置に「文脈を要約したベクトル」を出力する必要があります。この出力ベクトル $\bm{h}_i$ こそが、文脈化された単語埋め込み(contextualized embedding)です。
つまり、MLMの損失関数を最小化する過程で、副産物としてモデルは各トークンに対して文脈全体を反映した高品質なベクトル表現を生成するようになります。この表現がファインチューニングで下流タスクに活用されるのです。
CBoW(Continuous Bag of Words)との類似と違い
MLMの「周囲の文脈から中心の単語を予測する」というアイデアは、Word2VecのCBoWモデルと非常に似ています。CBoWでは、ウィンドウサイズ $k$ 内の周囲の単語の埋め込みを平均して中心単語を予測します。
$$ P(w_t \mid w_{t-k}, \ldots, w_{t-1}, w_{t+1}, \ldots, w_{t+k}) $$
両者の類似点は明確です。いずれも「文脈から対象の単語を予測する」という自己教師あり学習です。しかし、MLMにはCBoWと比較して以下の重要な違いがあります。
文脈の範囲: CBoWは固定長ウィンドウ(通常5〜10トークン)の局所的な文脈を見ますが、MLMはTransformerのSelf-Attentionを通じて系列全体(BERTでは最大512トークン)の文脈を参照できます。遠く離れた位置にある手がかりも活用可能です。
文脈の処理方法: CBoWは周囲の単語ベクトルを単純に平均します。語順の情報は失われ、”dog bites man” と “man bites dog” は同じ文脈表現になります。一方、MLMはTransformer Encoderの位置エンコーディングとSelf-Attentionにより、語順と長距離依存関係を精密に捉えます。
表現の性質: CBoWは各単語に1つの静的なベクトルを割り当てます(”bank” は金融でも川岸でも同じベクトル)。MLMは文脈に応じて異なるベクトルを生成するため、多義語の区別が可能です。「銀行に預けた」の「銀行」と「川の銀行に座った」の「銀行」は異なるベクトルになります。
学習のスケール: CBoWは浅いネットワーク(1層)で効率的に大規模コーパスを処理します。MLMはTransformerの深い層(BERTは12〜24層)により、より高次の言語パターンを学習できますが、計算コストは大幅に増大します。
このように、MLMはCBoWの「文脈から単語を予測する」という基本思想を、Transformerの強力な文脈処理能力で拡張したものと位置づけることができます。
MLMがBERTの事前学習の核であることはわかりましたが、オリジナルのBERTではMLMと並んでもう1つの事前学習タスクが使われていました。次のセクションでは、そのもう1つのタスクであるNSP(Next Sentence Prediction)を見ていきます。
NSP(Next Sentence Prediction)との組み合わせ
BERTオリジナルのNSPの仕組み
BERTの事前学習では、MLMに加えてNSP(Next Sentence Prediction)という二値分類タスクも同時に学習します。このタスクは、2つの文が元のテキストで連続していたか否かを判定するものです。
具体的には、訓練データの作成時に以下の手順で文のペアを構築します。
- 50%の確率: 実際に隣接する2文(文A, 文B)を使い、ラベル
IsNextを付与 - 50%の確率: 文Aに対してコーパスからランダムに選んだ無関係な文を文Bとし、ラベル
NotNextを付与
入力は [CLS] 文A [SEP] 文B [SEP] の形式で、[CLS] トークンの最終層出力に線形分類器を適用して IsNext / NotNext を予測します。
$$ P(\text{IsNext} \mid \bm{h}_{\texttt{[CLS]}}) = \sigma(\bm{w}_{\text{NSP}}^\top \bm{h}_{\texttt{[CLS]}} + b_{\text{NSP}}) $$
ここで $\sigma$ はシグモイド関数、$\bm{w}_{\text{NSP}}$ と $b_{\text{NSP}}$ は学習可能なパラメータです。
NSPの損失関数は標準的な二値交差エントロピーです。
$$ \mathcal{L}_{\text{NSP}} = -\left[ y \log P(\text{IsNext}) + (1-y) \log(1 – P(\text{IsNext})) \right] $$
ここで $y \in \{0, 1\}$ はラベルです。
BERTの全体の事前学習損失は、MLMとNSPの和として定義されます。
$$ \mathcal{L}_{\text{BERT}} = \mathcal{L}_{\text{MLM}} + \mathcal{L}_{\text{NSP}} $$
NSPの導入目的は、文間の整合性や論理的なつながりを理解させることでした。質問応答(質問と回答の対応)や自然言語推論(前提と仮説の関係)など、文ペアを入力とするタスクの性能向上が期待されていました。
NSPの限界 — RoBERTaで廃止された理由
しかし、その後の研究でNSPの有効性には疑問が呈されました。Facebook AI(現Meta AI)が2019年に発表したRoBERTaでは、NSPを完全に廃止して以下の変更を行い、BERTを大幅に上回る性能を達成しました。
NSPが機能しなかった主な理由は以下の通りです。
タスクが簡単すぎる: ランダムに選ばれた文ペアは、多くの場合トピックが全く異なるため、NotNextの判定が容易です。モデルは文間のトピックの一致/不一致を見るだけで高い精度が出せてしまい、「論理的な連続性」を深く学習する動機がありません。
MLMとの干渉: NSPの学習信号がMLMの表現学習を阻害する可能性が指摘されています。NSPのために[CLS]トークンの表現が文ペア分類に特化し、個々のトークンの文脈表現の質が低下するという仮説です。
データ効率の低下: NSPの50%のNotNextサンプルは、異なる文書からランダムに選ばれるため、モデルが長い文脈を学習する機会を減らしてしまいます。
RoBERTaでは、NSPを廃止する代わりに、1つの文書から連続するテキストをできるだけ長く入力に使うFull-Sentences戦略を採用しました。これにより、モデルはより長い文脈のパターンを学習でき、MLMの効果を最大限に引き出すことができたのです。
後続の研究では、NSPの代わりにSOP(Sentence Order Prediction)を導入したALBERTが提案されています。SOPは同じ文書内の2つの連続文の順序が正しいか逆かを判定するタスクで、NSPよりも困難かつ有用な学習信号を提供します。
MLMの基本形とその組み合わせ方がわかったところで、次はMLMそのものの改良バリエーションを見ていきましょう。トークン単位のマスクでは捉えきれない言語構造を学習するために、さまざまな拡張が提案されています。
MLMの改良バリエーション
BERTのオリジナルのMLMは「サブワードトークン単位でランダムにマスクする」というシンプルな戦略でしたが、言語には単語やフレーズといったトークンより大きな単位の構造があります。この構造を効果的に学習するため、いくつかの重要な改良手法が提案されてきました。
Whole Word Masking(WWM)
BERTのトークナイザ(WordPiece)は、語彙にない単語をサブワードに分割します。たとえば “playing” は “play” と “##ing” の2トークンに分割されます。オリジナルのMLMでは “##ing” だけがマスクされることがあり、この場合 “play” が見えている状態で “##ing” を予測するのは極めて容易です。モデルは文脈の理解ではなく、単なるサブワードの補完を学習してしまいます。
Whole Word Maskingでは、ある単語のサブワードの一部がマスク対象に選ばれた場合、その単語の全サブワードを同時にマスクします。”playing” をマスクする場合、”play” と “##ing” の両方が [MASK] に置換されます。
これにより、モデルは「play + ##ing → playing」という表面的なパターンではなく、前後の文脈から “playing” という単語全体の意味を推測する必要があるため、より深い文脈理解が促進されます。Google はBERTの学習済みモデルのWWM版を公開しており、特に中国語など形態素の分割が重要な言語で大きな改善が報告されています。
Span Masking(SpanBERT)
SpanBERT(Joshi et al., 2020)は、個別のトークンではなく連続するスパン(区間)をマスクする手法です。マスクするスパンの長さは幾何分布からサンプリングされます。
$$ P(\text{span length} = k) = p \cdot (1-p)^{k-1}, \quad k = 1, 2, \ldots $$
ここで $p$ はパラメータ(SpanBERTでは $p = 0.2$、平均スパン長3.8トークン)です。
SpanBERTの特徴的な点は、マスクされたスパン内の各トークンの予測に、スパンの境界トークン(スパンの直前と直後のトークン)の表現を積極的に活用するSpan Boundary Objective(SBO)を導入したことです。
位置 $i$(スパン内)のトークンを予測する際、通常のMLM出力 $\bm{h}_i$ に加えて、スパンの左境界トークンの表現 $\bm{h}_{\text{start}-1}$、右境界トークンの表現 $\bm{h}_{\text{end}+1}$、およびスパン内での相対位置埋め込み $\bm{p}_{i – \text{start} + 1}$ を用いて予測を行います。
$$ \bm{y}_i = f(\bm{h}_{\text{start}-1}, \bm{h}_{\text{end}+1}, \bm{p}_{i – \text{start}+1}) $$
ここで $f$ は2層のフィードフォワードネットワークです。
この設計により、モデルは「スパンの境界の表現に、スパン内部の情報を凝縮する」ことを学びます。これは、固有表現抽出や質問応答のように、テキスト中の特定のスパンを識別するタスクと親和性が高く、SpanBERTは抽出型質問応答や共参照解析で大きな改善を示しました。
なお、SpanBERTではNSPを廃止し、単一文のみを入力としています。これはRoBERTaの知見と一致する設計判断です。
ELECTRA — Replaced Token Detection
ELECTRA(Clark et al., 2020)は、MLMとは根本的に異なるアプローチを採る手法で、MLMの非効率性を解決するために設計されました。
MLMの重要な非効率性は、損失計算が全トークンの15%(マスクされたトークン)に対してのみ行われる点です。残りの85%のトークンからは学習信号が得られません。
ELECTRAは、Replaced Token Detection(RTD)というタスクで全トークンから学習信号を得ます。アーキテクチャは2つのネットワークから成るGAN風の構造です。
生成器(Generator): 小さなMLMモデルで、マスクされたトークンの代替候補を生成します。
識別器(Discriminator): 入力文の各トークンが「本物(オリジナル)」か「偽物(生成器が置換したもの)」かを二値分類します。
学習の流れは以下の通りです。
- 入力文のトークンの一部をマスクする
- 生成器がマスクされた位置のトークンを予測・生成する
- マスク位置に生成されたトークンを埋め込み、「一見自然だが一部が改変された文」を作る
- 識別器が全トークンに対して「本物か偽物か」を判定する
識別器の損失関数は以下のように表されます。
$$ \mathcal{L}_{\text{RTD}} = -\sum_{t=1}^{T} \left[ \mathbb{1}(x_t = \hat{x}_t) \log D(\tilde{\bm{x}}, t) + \mathbb{1}(x_t \neq \hat{x}_t) \log(1 – D(\tilde{\bm{x}}, t)) \right] $$
ここで $D(\tilde{\bm{x}}, t)$ は位置 $t$ のトークンがオリジナルである確率の予測値、$\hat{x}_t$ は生成器が出力したトークンです。
ELECTRAの大きな利点は、全てのトークン(100%)から学習信号が得られるため、同じ計算量でもMLMより遥かに効率的に学習が進む点です。特に小規模モデルや限られた計算資源での学習で、BERTを大幅に上回る性能を示しました。
下流タスクで使用するのは識別器のネットワークで、ファインチューニング時には生成器は不要です。
ここまでで、MLMの理論的な全体像 — 基本形、改良手法、そして代替アプローチ — を俯瞰しました。理論を深く理解するには手を動かすことが一番です。次のセクションでは、PyTorchを使ってMLMの学習パイプラインをゼロから実装していきます。
PyTorchでの実装
ここでは、MLMの仕組みを実際にコードで確認します。小規模なTransformerモデルを構築し、簡単なコーパスでMLMの学習を行い、マスク予測が正しく機能することを確かめます。
マスク処理の実装
まず、MLMの核心であるマスク処理を実装します。入力トークン列に対して、15%のトークンを選択し、80/10/10ルールでマスク・置換・維持を行う関数です。
import torch
import torch.nn as nn
import torch.nn.functional as F
import numpy as np
import random
def create_mlm_data(token_ids, vocab_size, mask_token_id,
mask_prob=0.15, special_token_ids=None):
"""
MLMのマスク処理を行う
Parameters
----------
token_ids : list[int]
入力トークンIDのリスト
vocab_size : int
語彙サイズ
mask_token_id : int
[MASK]トークンのID
mask_prob : float
マスクする割合(デフォルト15%)
special_token_ids : set[int] or None
マスクしない特殊トークンのID集合([CLS], [SEP]等)
Returns
-------
masked_ids : list[int]
マスク後のトークンIDリスト
labels : list[int]
正解ラベル(マスクされていない位置は-100)
"""
if special_token_ids is None:
special_token_ids = set()
masked_ids = list(token_ids)
labels = [-100] * len(token_ids) # -100はPyTorchのCrossEntropyLossで無視される
# マスク候補のインデックス(特殊トークンを除く)
candidate_indices = [
i for i, tid in enumerate(token_ids)
if tid not in special_token_ids
]
# 15%をランダムに選択
num_to_mask = max(1, int(len(candidate_indices) * mask_prob))
masked_indices = random.sample(candidate_indices, num_to_mask)
for idx in masked_indices:
labels[idx] = token_ids[idx] # 正解ラベルを記録
rand_val = random.random()
if rand_val < 0.8:
# 80%: [MASK]に置換
masked_ids[idx] = mask_token_id
elif rand_val < 0.9:
# 10%: ランダムトークンに置換
masked_ids[idx] = random.randint(0, vocab_size - 1)
# 残り10%: そのまま(何もしない)
return masked_ids, labels
この関数のポイントは、ラベル配列でマスクされていない位置を-100に設定している点です。PyTorchのnn.CrossEntropyLossはデフォルトでignore_index=-100を持っており、この値を持つ位置の損失を自動的に無視します。これにより、損失計算がマスクされた位置のみに限定されます。
実際にマスク処理を動かして、入力がどのように変換されるかを確認してみましょう。
# 簡易的な語彙の定義
vocab = {
"[PAD]": 0, "[CLS]": 1, "[SEP]": 2, "[MASK]": 3,
"彼": 4, "は": 5, "大学": 6, "で": 7,
"数学": 8, "を": 9, "勉強": 10, "し": 11,
"た": 12, "毎日": 13, "図書館": 14, "に": 15,
"通っ": 16, "て": 17, "い": 18,
}
id_to_token = {v: k for k, v in vocab.items()}
vocab_size = len(vocab)
mask_token_id = vocab["[MASK]"]
special_token_ids = {vocab["[PAD]"], vocab["[CLS]"], vocab["[SEP]"]}
# 入力文: [CLS] 彼 は 大学 で 数学 を 勉強 し た [SEP]
input_ids = [1, 4, 5, 6, 7, 8, 9, 10, 11, 12, 2]
# マスク処理を実行
random.seed(42)
masked_ids, labels = create_mlm_data(
input_ids, vocab_size, mask_token_id,
mask_prob=0.15, special_token_ids=special_token_ids
)
print("元の入力 :", [id_to_token[i] for i in input_ids])
print("マスク後 :", [id_to_token.get(i, f"ID={i}") for i in masked_ids])
print("ラベル :", [id_to_token.get(i, "---") if i != -100 else "---" for i in labels])
実行すると、入力の11トークン(うち特殊トークン2個を除く9トークン)から15%にあたる1〜2トークンがマスク対象に選ばれ、80%の確率で[MASK]に、10%でランダムトークンに、10%でそのままに置換されていることが確認できます。ラベル配列では、マスクされた位置のみに元のトークンIDが記録され、残りは---(-100)となっています。これが学習時の「正解」として使われます。
簡易MLMモデルの実装
次に、小規模なTransformer Encoderの上にMLMヘッドを載せた学習可能なモデルを構築します。
class TransformerMLM(nn.Module):
"""
簡易的なTransformer Encoder + MLMヘッド
"""
def __init__(self, vocab_size, d_model=128, nhead=4,
num_layers=2, dim_feedforward=256, max_len=64, dropout=0.1):
super().__init__()
self.d_model = d_model
# トークン埋め込み + 位置エンコーディング
self.token_embedding = nn.Embedding(vocab_size, d_model)
self.position_embedding = nn.Embedding(max_len, d_model)
self.layer_norm = nn.LayerNorm(d_model)
self.dropout = nn.Dropout(dropout)
# Transformer Encoder
encoder_layer = nn.TransformerEncoderLayer(
d_model=d_model, nhead=nhead,
dim_feedforward=dim_feedforward,
dropout=dropout, batch_first=True,
activation='gelu'
)
self.transformer_encoder = nn.TransformerEncoder(
encoder_layer, num_layers=num_layers
)
# MLMヘッド: 隠れ表現 → 語彙サイズのロジット
self.mlm_head = nn.Sequential(
nn.Linear(d_model, d_model),
nn.GELU(),
nn.LayerNorm(d_model),
nn.Linear(d_model, vocab_size)
)
def forward(self, input_ids, padding_mask=None):
"""
Parameters
----------
input_ids : Tensor [batch_size, seq_len]
padding_mask : Tensor [batch_size, seq_len], Trueがパディング位置
Returns
-------
logits : Tensor [batch_size, seq_len, vocab_size]
"""
seq_len = input_ids.size(1)
positions = torch.arange(seq_len, device=input_ids.device).unsqueeze(0)
# 埋め込みの合成
x = self.token_embedding(input_ids) + self.position_embedding(positions)
x = self.layer_norm(x)
x = self.dropout(x)
# Transformer Encoder
x = self.transformer_encoder(x, src_key_padding_mask=padding_mask)
# MLMヘッド
logits = self.mlm_head(x)
return logits
このモデルの構造は、入力トークンIDを受け取り、トークン埋め込みと位置埋め込みを加算してTransformer Encoderに通し、最終層の出力をMLMヘッドで語彙サイズのロジットに変換するというものです。BERTの構造を簡略化していますが、MLMの本質的な仕組みはそのまま反映されています。
小さなコーパスでの学習実験
それでは、このモデルを小さなコーパスで実際に学習させてみましょう。学習データとして短い日本語文を複数用意し、MLMタスクで訓練します。
import matplotlib.pyplot as plt
# 簡易語彙(拡張版)
vocab = {
"[PAD]": 0, "[CLS]": 1, "[SEP]": 2, "[MASK]": 3,
"彼": 4, "は": 5, "大学": 6, "で": 7, "数学": 8, "を": 9,
"勉強": 10, "し": 11, "た": 12, "彼女": 13, "物理": 14,
"毎日": 15, "図書館": 16, "に": 17, "通っ": 18, "て": 19,
"い": 20, "私": 21, "化学": 22, "研究": 23, "室": 24,
"実験": 25, "教授": 26, "論文": 27, "書い": 28,
"学生": 29, "講義": 30, "聞い": 31,
}
id_to_token = {v: k for k, v in vocab.items()}
vocab_size = len(vocab)
mask_token_id = vocab["[MASK]"]
special_ids = {0, 1, 2}
# 訓練コーパス(トークンIDのリスト)
corpus = [
[1, 4, 5, 6, 7, 8, 9, 10, 11, 12, 2], # [CLS]彼は大学で数学を勉強した[SEP]
[1, 13, 5, 6, 7, 14, 9, 10, 11, 12, 2], # [CLS]彼女は大学で物理を勉強した[SEP]
[1, 4, 5, 15, 16, 17, 18, 19, 20, 12, 2], # [CLS]彼は毎日図書館に通っていた[SEP]
[1, 21, 5, 22, 9, 10, 11, 12, 2, 0, 0], # [CLS]私は化学を勉強した[SEP][PAD][PAD]
[1, 26, 5, 27, 9, 28, 12, 2, 0, 0, 0], # [CLS]教授は論文を書いた[SEP][PAD][PAD][PAD]
[1, 29, 5, 30, 9, 31, 12, 2, 0, 0, 0], # [CLS]学生は講義を聞いた[SEP][PAD][PAD][PAD]
[1, 4, 5, 23, 24, 7, 25, 9, 11, 12, 2], # [CLS]彼は研究室で実験をした[SEP]
[1, 13, 5, 16, 7, 27, 9, 28, 12, 2, 0], # [CLS]彼女は図書館で論文を書いた[SEP][PAD]
]
# パディングマスクの作成
def make_padding_mask(ids_list, pad_id=0):
return torch.tensor([[tid == pad_id for tid in ids] for ids in ids_list])
# モデルの初期化
torch.manual_seed(42)
model = TransformerMLM(vocab_size=vocab_size, d_model=64, nhead=4,
num_layers=2, dim_feedforward=128, max_len=16)
optimizer = torch.optim.Adam(model.parameters(), lr=1e-3)
criterion = nn.CrossEntropyLoss(ignore_index=-100)
# 学習ループ
num_epochs = 300
losses = []
for epoch in range(num_epochs):
epoch_loss = 0.0
random.shuffle(corpus)
for sentence in corpus:
# マスク処理
masked_ids, labels = create_mlm_data(
sentence, vocab_size, mask_token_id,
mask_prob=0.20, special_token_ids=special_ids
)
input_tensor = torch.tensor([masked_ids])
label_tensor = torch.tensor([labels])
pad_mask = make_padding_mask([masked_ids])
# 順伝播
logits = model(input_tensor, padding_mask=pad_mask)
# 損失計算([batch_size * seq_len, vocab_size]に変形)
loss = criterion(logits.view(-1, vocab_size), label_tensor.view(-1))
# 逆伝播
optimizer.zero_grad()
loss.backward()
optimizer.step()
epoch_loss += loss.item()
avg_loss = epoch_loss / len(corpus)
losses.append(avg_loss)
if (epoch + 1) % 50 == 0:
print(f"Epoch {epoch+1:3d}/{num_epochs} | Loss: {avg_loss:.4f}")
# 学習曲線のプロット
plt.figure(figsize=(8, 4))
plt.plot(losses, color='#00bcd4', linewidth=1.5)
plt.xlabel("Epoch")
plt.ylabel("MLM Loss")
plt.title("MLM Training Loss Curve")
plt.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
学習曲線を見ると、損失が最初の50エポック程度で急速に減少し、その後も緩やかに下がり続けていることがわかります。損失の減少は、モデルがマスクされたトークンを文脈から正しく予測する能力を徐々に獲得していることを示しています。コーパスが非常に小規模(8文)であるため過学習が起きやすいですが、MLMの学習メカニズムの動作確認としては十分です。
マスク予測の結果を確認
学習したモデルを使って、マスクされた位置のトークンを予測させてみましょう。
model.eval()
# テスト文: 「彼は大学で[MASK]を勉強した」
test_sentence = [1, 4, 5, 6, 7, 3, 9, 10, 11, 12, 2] # 3 = [MASK]
mask_pos = 5 # [MASK]の位置
print("入力:", [id_to_token.get(i, f"?{i}") for i in test_sentence])
print(f"マスク位置: {mask_pos} (元のトークン: 数学)")
print()
with torch.no_grad():
input_tensor = torch.tensor([test_sentence])
pad_mask = make_padding_mask([test_sentence])
logits = model(input_tensor, padding_mask=pad_mask)
# マスク位置の予測確率
probs = F.softmax(logits[0, mask_pos], dim=-1)
top5_probs, top5_ids = torch.topk(probs, 5)
print("予測Top-5:")
for prob, tid in zip(top5_probs, top5_ids):
token = id_to_token.get(tid.item(), f"ID={tid.item()}")
print(f" {token:6s} : {prob.item():.4f}")
# 別のテスト: 「彼女は大学で[MASK]を勉強した」
test2 = [1, 13, 5, 6, 7, 3, 9, 10, 11, 12, 2]
print("\n入力:", [id_to_token.get(i, f"?{i}") for i in test2])
print(f"マスク位置: {mask_pos}")
with torch.no_grad():
input_tensor2 = torch.tensor([test2])
pad_mask2 = make_padding_mask([test2])
logits2 = model(input_tensor2, padding_mask=pad_mask2)
probs2 = F.softmax(logits2[0, mask_pos], dim=-1)
top5_probs2, top5_ids2 = torch.topk(probs2, 5)
print("予測Top-5:")
for prob, tid in zip(top5_probs2, top5_ids2):
token = id_to_token.get(tid.item(), f"ID={tid.item()}")
print(f" {token:6s} : {prob.item():.4f}")
予測結果を見ると、「彼は大学で[MASK]を勉強した」という文では「数学」や「物理」「化学」といった教科名が高い確率で予測されていることがわかります。訓練コーパスに「彼は大学で数学を勉強した」という文が含まれているため、「数学」が最も高い確率となるのは自然です。「彼女は〜」に変えた場合にも同様の傾向が見られますが、コーパス中の「彼女は大学で物理を勉強した」の影響で「物理」のスコアが相対的に上がることが期待されます。このように、たった8文のコーパスでも、MLMは文脈に応じた単語の予測を学習できていることが確認できます。
続いて、マスク位置ごとの予測精度を可視化してみましょう。
# 全コーパスに対するマスク予測精度の集計
model.eval()
correct = 0
total = 0
position_correct = {}
position_total = {}
random.seed(123)
for _ in range(100): # 100回繰り返して統計を取る
for sentence in corpus:
masked_ids, labels = create_mlm_data(
sentence, vocab_size, mask_token_id,
mask_prob=0.20, special_token_ids=special_ids
)
with torch.no_grad():
input_tensor = torch.tensor([masked_ids])
pad_mask = make_padding_mask([masked_ids])
logits = model(input_tensor, padding_mask=pad_mask)
preds = logits.argmax(dim=-1)[0]
for i, label in enumerate(labels):
if label != -100:
total += 1
position_total[i] = position_total.get(i, 0) + 1
if preds[i].item() == label:
correct += 1
position_correct[i] = position_correct.get(i, 0) + 1
print(f"全体のマスク予測精度: {correct}/{total} = {correct/total:.2%}")
# 位置別精度の可視化
positions = sorted(position_total.keys())
accuracies = [position_correct.get(p, 0) / position_total[p] for p in positions]
plt.figure(figsize=(8, 4))
bars = plt.bar(positions, accuracies, color='#00bcd4', alpha=0.8, edgecolor='white')
plt.xlabel("Token Position")
plt.ylabel("Prediction Accuracy")
plt.title("MLM Prediction Accuracy by Position")
plt.ylim(0, 1.05)
plt.xticks(positions)
plt.grid(axis='y', alpha=0.3)
plt.tight_layout()
plt.show()
この可視化から、位置によって予測精度にばらつきがあることが読み取れます。助詞(「は」「を」「で」など)が出現する位置は、文法的にほぼ一意に定まるため高い精度になる傾向があります。一方、内容語(名詞や動詞)の位置は複数の候補がありうるため、精度は相対的に低くなります。しかし小規模コーパスでの実験とはいえ、全体的に高い精度が出ていれば、MLMの学習が正しく機能していることの証拠です。
ここまでのスクラッチ実装で、MLMの内部動作を詳細に理解できました。次のセクションでは、実用的なMLMの利用方法として、Hugging Faceの学習済みモデルを使ったfill-maskパイプラインを試します。
Hugging Face を使ったMLMの実践
スクラッチ実装で仕組みを理解したところで、次は大規模コーパスで事前学習された本格的なモデルを使ってMLMの威力を体感してみましょう。Hugging Face Transformersライブラリのfill-maskパイプラインを使えば、わずか数行のコードでBERTのマスク予測を実行できます。
from transformers import pipeline
# fill-maskパイプラインの構築(日本語BERTを使用)
fill_mask = pipeline("fill-mask", model="cl-tohoku/bert-base-japanese-whole-word-masking")
# テスト1: 学術的な文脈
result1 = fill_mask("彼は大学で[MASK]を勉強した。")
print("=== 「彼は大学で[MASK]を勉強した。」 ===")
for r in result1[:5]:
print(f" {r['token_str']:8s} (score: {r['score']:.4f})")
# テスト2: 文脈による予測の変化
result2 = fill_mask("東京は[MASK]の首都です。")
print("\n=== 「東京は[MASK]の首都です。」 ===")
for r in result2[:5]:
print(f" {r['token_str']:8s} (score: {r['score']:.4f})")
# テスト3: 双方向文脈の効果を確認
result3a = fill_mask("彼は銀行に[MASK]を預けた。")
result3b = fill_mask("彼は[MASK]の銀行に就職した。")
print("\n=== 「彼は銀行に[MASK]を預けた。」 ===")
for r in result3a[:5]:
print(f" {r['token_str']:8s} (score: {r['score']:.4f})")
print("\n=== 「彼は[MASK]の銀行に就職した。」 ===")
for r in result3b[:5]:
print(f" {r['token_str']:8s} (score: {r['score']:.4f})")
この実行結果から、いくつかの重要な観察が得られます。
第一に、「彼は大学で[MASK]を勉強した」に対しては「法律」「経済」「医学」「数学」など、大学で学ぶ典型的な科目が高スコアで予測されます。これは、大規模コーパスでの事前学習により「大学で○○を勉強する」という文脈パターンが幅広く学習されていることを示します。
第二に、「東京は[MASK]の首都です」に対して「日本」が圧倒的に高いスコアで返されます。これはMLMが事実関係(世界知識)をパラメータ内にエンコードしている証拠です。
第三に、「銀行に[MASK]を預けた」と「[MASK]の銀行に就職した」で予測結果が大きく異なることが確認できます。前者では「お金」「金」などの金融関連の語が、後者では地名や組織を表す語が上位に来るはずです。これは双方向文脈によって同じ[MASK]位置でも、左右の文脈に応じて全く異なる予測が行われることを示しており、MLMの双方向性の本質が見て取れます。
# テスト4: 構文的知識の確認
results_syntax = fill_mask("この問題は非常に[MASK]。")
print("=== 「この問題は非常に[MASK]。」 ===")
for r in results_syntax[:5]:
print(f" {r['token_str']:8s} (score: {r['score']:.4f})")
「この問題は非常に[MASK]。」に対しては、「難しい」「重要」「複雑」など、「非常に」の後に自然に続く形容詞が高スコアで予測されます。「非常に」は程度副詞であり、その後には形容詞(あるいは形容動詞)が来るという構文的知識と、「問題」という主語と共起しやすい形容詞の意味的知識が同時に反映されています。
このように、大規模コーパスで訓練されたBERTのMLMは、構文・意味・世界知識の3つの側面を統合した予測を行います。fill-maskパイプラインは、MLMで獲得した知識の質を手軽に確認できる便利なツールです。
まとめ
本記事では、BERTの事前学習の核心であるマスク言語モデル(MLM)について、理論と実装の両面から解説しました。
- MLMの基本思想: 入力トークンの15%をマスクし、双方向の文脈から予測することで、文の前後両方の情報を活用した深い言語表現を学習する
- マスク戦略(80/10/10ルール): 事前学習とファインチューニングの間の入力分布ミスマッチを緩和するための工夫であり、80%を
[MASK]に、10%をランダムトークンに、10%をそのままにする - 目的関数: マスクされた位置の負の対数尤度を最小化する交差エントロピー損失で学習が行われる
- 学習で獲得される知識: 構文的知識(品詞、語順)、意味的知識(同義語、共起)、世界知識(事実関係)の3層
- NSPの限界: BERTオリジナルのNSPはタスクが簡単すぎるため、RoBERTaで廃止された
- 改良手法: Whole Word Masking、SpanBERT(スパン単位のマスク)、ELECTRA(全トークンからの学習信号)など、MLMの弱点を補う多様な手法が提案されている
- PyTorchでの実装: マスク処理、Transformer Encoder + MLMヘッドの構築、学習ループ、予測結果の確認を一通り実装した
- Hugging Faceでの実践: 事前学習済みBERTのfill-maskパイプラインにより、双方向文脈による高精度な予測を確認した
MLMは「穴埋め問題を解く」という直感的なタスクでありながら、言語の構造を驚くほど深く捉える強力な事前学習手法です。その成功は、Transformerの双方向的な文脈統合能力と、自己教師あり学習による大規模データの効率的活用の賜物といえます。
次のステップとして、以下の記事もぜひ参考にしてください。