第8章 因果マスク:未来を見えなくする仕組み

こんにちは。ゆうせいです。

第7章では、スケーリングによってスコアの大きさが調整されることを確認しました。第8章では、そのスコア行列に対して、GPT-2のような文章生成モデル特有の処理を加えます。因果マスク(Causal Mask)と呼ばれるものです。Transformer Explainerでスコア行列を見ると、右上の三角形の領域が、他とは明らかに異なる表示になっていることに気づくはずです。多くの場合、その領域には極端に小さな値が並んでいます。なぜ、せっかく計算したスコアの半分近くを、わざわざ使えなくするのでしょうか。第8章では、その理由と実装方法を解説します。

因果マスクとは何か

因果マスクとは、あるトークンを処理する際に、それより後ろ(未来)にあるトークンへの注目を、強制的に遮断する仕組みです。

具体例で確認します。「The cat sat on the mat」という6トークンの文章があるとします。

3番目のトークン「sat」を処理するとき、参照してよいのは、1番目の「The」、2番目の「cat」、そして自分自身である3番目の「sat」までです。

4番目以降の「on」「the」「mat」は、参照してはいけません。

同様に、1番目の「The」を処理するときは、自分自身しか参照できません。6番目の「mat」を処理するときは、すべてのトークンを参照できます。

この制約を、スコア行列に対して適用するのが、因果マスクです。

なぜ未来を見てはいけないのか

学習と推論の一貫性を保つため

理由を理解するには、GPT-2がどのように学習し、どのように使われるかを確認する必要があります。

GPT-2は、次のトークンを予測するという課題で学習されています。「The cat sat on the」まで与えられたときに、次に来るのは「mat」である、と当てる訓練です。

実際に文章を生成するときも、同じ仕組みで動きます。ここまでの文章から次の1トークンを予測し、それを末尾に追加して、また次の1トークンを予測する、という繰り返しです。

つまり、推論時には、未来のトークンは物理的に存在しません。まだ生成されていないのですから、参照しようがないのです。

もし学習時に未来を見せてしまうと、深刻な問題が起きます。

「The cat sat on the ___」の空欄を予測する訓練において、正解である「mat」が見えている状態で訓練することになります。答えを見ながら答えを当てる訓練ですから、モデルは何も学習しません。

そして、推論時には未来が存在しないため、学習時とはまったく異なる状況に置かれます。学習した通りに動作できず、性能が著しく低下します。

この現象を、情報のリーク(漏洩)と呼びます。因果マスクは、このリークを防ぐための仕組みです。

因果という名前の意味

「因果」という言葉は、原因が結果に先行するという、時間の順序を指しています。

未来の出来事が過去に影響を与えることはありません。同様に、後ろのトークンが前のトークンの処理に影響を与えてはならない、という制約を表しています。

この方式は、他にも複数の呼び方があります。

Masked Self-Attention(マスク付き自己注意)と呼ばれることがあります。

Causal Attention(因果注意)と呼ばれることもあります。

一方向注意、あるいは単方向注意と呼ばれることもあります。

いずれも同じ仕組みを指していますので、資料によって呼び方が異なっていても、混乱しないでください。

GPTとBERTの決定的な違い

因果マスクの有無は、モデルの用途を根本的に分ける違いです。

GPT系:因果マスクあり

GPT-2をはじめとする文章生成モデルは、因果マスクを使います。

各トークンは、自分より前のトークンだけを参照します。この制約があるからこそ、文章を左から右へ、1トークンずつ生成できます。

用途は、文章生成、対話、続きの予測などです。

BERT系:因果マスクなし

BERTのようなモデルでは、因果マスクを使いません。すべてのトークンが、すべてのトークンを参照できます。

BERTの学習方法は、文中の一部のトークンを隠し、それを周囲から推測させるというものです。隠された箇所の前後、両方の文脈を使えることが利点になります。

用途は、文章の分類、固有表現の抽出、文の意味の理解などです。

使い分けの整理

項目GPT系(因果マスクあり)BERT系(因果マスクなし)
参照できる範囲自分より前のトークンのみすべてのトークン
主な学習方法次のトークンの予測隠されたトークンの復元
得意な用途文章生成、対話分類、意味理解
文章生成できる苦手
双方向の文脈理解苦手できる

新人エンジニアが両者を混同しやすい理由は、どちらもTransformerを使っているためです。基本構造は同じですが、因果マスクの有無という一点の違いが、まったく異なる性質のモデルを生んでいる、と押さえてください。

どのように実装されているのか

因果マスクの実装は、単純です。参照してはいけない位置のスコアを、極端に小さな値に置き換えます。

なぜ0ではなくマイナス無限大なのか

ここで、初学者が最もつまずく点があります。

「参照させたくないなら、スコアを0にすればよいのではないか」と考える人が多いのですが、それでは正しく機能しません。

理由は、次にSoftmax関数を通すからです。

Softmax関数では、各値の指数関数を計算します。スコアが0の場合、次のようになります。

e^0 = 1

0のままにすると、指数関数を通した値は1になります。他のスコアが小さい場合、この1という値は、相対的に大きな重みとして残ってしまいます。つまり、参照させたくないトークンに、それなりの注目が向いてしまうのです。

一方、スコアをマイナス無限大にすると、次のようになります。

e^{-\infty} = 0

指数関数を通した結果が0になり、確率も完全に0になります。これで初めて、参照が完全に遮断されます。

実装上はマイナス無限大に近い有限の値を使う

実際のプログラムでは、マイナス無限大という値を直接扱うと、計算エラーが発生することがあります。

そこで、実装上は、極端に小さな有限の値が使われます。たとえば、マイナス10000や、その環境で表現できる最小値に近い値です。

e^{-10000}

この値は、コンピュータの数値表現では0とみなされるほど小さくなります。実質的に、マイナス無限大と同じ効果が得られます。

マスク行列の形

マスクは、上三角行列の形をしています。

トークン数が4の場合、スコア行列に適用されるマスクは、次のような形になります。丸印が参照可能、バツ印が遮断される位置です。

列1列2列3列4
行1バツバツバツ
行2バツバツ
行3バツ
行4

第6章で確認したとおり、行はQueryを出す側、列はKeyを提示する側です。

行1は1番目のトークンであり、自分自身(列1)しか参照できません。

行4は4番目のトークンであり、すべてのトークンを参照できます。

対角線を含む左下の三角形が残り、対角線より右上の三角形が遮断される、という形です。

なお、この対角線の扱いには注意が必要です。対角線上、つまり自分自身への注目は、遮断されません。自分のQueryと自分のKeyとの内積は、有効な情報として使われます。

計算の無駄について

因果マスクを使う場合、スコア行列の右上の約半分は、計算しても捨てられることになります。

素朴に実装すると、この部分の内積計算が無駄になります。トークン数が多いほど、この無駄も大きくなります。

実際の高速な実装では、この無駄を省く工夫が行われています。FlashAttentionのような手法では、マスクされる領域の計算自体をスキップする最適化が含まれています。

Transformer Explainerで観察できること

Transformer Explainerでは、マスクが適用されたスコア行列が表示されます。

次の点を確認してみてください。

一つ目は、右上の三角形の領域が、他と明確に区別された表示になっているという点です。極端に小さな値が入っているか、あるいは空白として表示されているはずです。

二つ目は、対角線上の値は残っているという点です。自分自身への注目は遮断されません。

三つ目は、最初のトークンの行を見ると、自分自身の位置だけが残っているという点です。文頭のトークンは、参照できる過去がありません。

三つ目の点には、興味深い帰結があります。最初のトークンは、自分自身しか参照できないため、Softmaxを通すと、自分自身への注目が確率1になります。つまり、自己注意を通しても、実質的にほとんど何も変化しないということです。

初学者がつまずきやすい点

つまずき1:推論時にも毎回すべてを計算し直すと思ってしまう

文章を生成する際、1トークン生成するたびに、最初からすべての計算をやり直すのは、非常に無駄です。

因果マスクがあるおかげで、過去のトークンの計算結果は、新しいトークンが追加されても変化しません。3番目のトークンの処理は、4番目のトークンが追加されても、影響を受けないからです。

この性質を利用して、過去のKeyとValueを保存しておき、再利用する最適化が行われます。これをKVキャッシュと呼びます。

因果マスクは、制約であると同時に、この高速化を可能にする前提でもあるのです。因果マスクがなければ、KVキャッシュは成立しません。

つまずき2:マスクを適用する順序を間違える

処理の順序は、次の通りです。

内積の計算、スケーリング、マスクの適用、Softmax、という順です。

マスクは、Softmaxの前に適用しなければなりません。Softmaxの後にマスクを適用すると、確率の合計が1でなくなり、正しく機能しません。

また、スケーリングとマスクの順序については、どちらが先でも結果は同じです。マイナス無限大は、何で割ってもマイナス無限大だからです。実装によって順序が異なることがありますが、混乱しないでください。

つまずき3:パディングマスクと混同する

実務では、もう一種類のマスクが登場します。パディングマスクです。

複数の文章をまとめて処理する際、長さを揃えるために、短い文章の末尾に意味のない埋め草のトークン(パディング)を追加します。このパディング部分を無視するためのマスクが、パディングマスクです。

因果マスクは「未来を見せない」ためのもの、パディングマスクは「意味のない埋め草を見せない」ためのものです。目的がまったく異なります。

実際のモデルでは、両方のマスクが同時に適用されることがあります。

演習

演習1:マスク行列を書く

トークン数が5の場合の因果マスクを、表の形で書き出してください。参照可能な位置と、遮断される位置を明示してください。

また、遮断される位置の個数と、参照可能な位置の個数を、それぞれ数えてください。

演習2:マスクの有無で結果を比較する

import torch
import torch.nn.functional as F
import math

# 5トークンを想定したスコア行列(スケーリング済み)
torch.manual_seed(42)
scores = torch.randn(5, 5)

print("=== マスクなしのSoftmax ===")
probs_no_mask = F.softmax(scores, dim=-1)
print(probs_no_mask.numpy().round(3))

print("\n=== 因果マスクを作成 ===")
seq_len = 5
mask = torch.tril(torch.ones(seq_len, seq_len))
print(mask.numpy())

print("\n=== マスク適用後のスコア ===")
masked_scores = scores.masked_fill(mask == 0, float('-inf'))
print(masked_scores.numpy().round(3))

print("\n=== マスクありのSoftmax ===")
probs_masked = F.softmax(masked_scores, dim=-1)
print(probs_masked.numpy().round(3))

print("\n=== 各行の合計を確認 ===")
print(f"マスクなし: {probs_no_mask.sum(dim=-1).tolist()}")
print(f"マスクあり: {probs_masked.sum(dim=-1).tolist()}")

演習3:0でマスクした場合との比較

なぜマイナス無限大なのかを、数値で確認します。

import torch
import torch.nn.functional as F

scores = torch.tensor([[2.0, 1.5, -1.0, 0.5]])

print("=== マスクなし ===")
print(F.softmax(scores, dim=-1).numpy().round(4))

print("\n=== 後半2つを0でマスク(誤った方法) ===")
scores_zero = torch.tensor([[2.0, 1.5, 0.0, 0.0]])
print(F.softmax(scores_zero, dim=-1).numpy().round(4))

print("\n=== 後半2つをマイナス無限大でマスク(正しい方法) ===")
scores_inf = torch.tensor([[2.0, 1.5, float('-inf'), float('-inf')]])
print(F.softmax(scores_inf, dim=-1).numpy().round(4))

演習の解答例と解説

演習1の解答は、次の通りです。

列1列2列3列4列5
行1バツバツバツバツ
行2バツバツバツ
行3バツバツ
行4バツ
行5

参照可能な位置の個数は、1 + 2 + 3 + 4 + 5 = 15個です。

遮断される位置の個数は、全体の25個から15個を引いた、10個です。

一般に、トークン数がnのとき、参照可能な位置の個数は次の式で表されます。

\frac{n(n+1)}{2}

全体の約半分が遮断される、という関係が確認できます。

演習2では、マスクありの場合、上三角の位置の確率がすべて0になることが確認できます。また、各行の合計は、マスクの有無にかかわらず1になります。Softmaxは、残された要素だけで確率を再配分するためです。

演習3では、0でマスクした場合、その位置の確率が0にならないことが確認できます。他の値との相対関係によっては、無視できない大きさの確率が残ります。マイナス無限大でマスクした場合のみ、確率が完全に0になります。この比較が、マイナス無限大を使う理由を最も直接的に示しています。

講師向けの補足

この章では、「なぜ0ではだめなのか」という点に、必ず時間を割いてください。

多くの受講者は、直感的に0を思い浮かべます。その直感が誤りであることを、演習3のプログラムで数値として示すと、強い納得感が得られます。説明だけで済ませると、後で実装する際に、同じ誤りを繰り返しがちです。

また、GPTとBERTの違いを説明する際は、「どちらが優れているか」という比較にならないよう注意してください。用途が異なるだけであり、優劣ではありません。文章を生成したいのか、文章を理解したいのかによって、適した構造が異なる、という整理が正確です。

理解度を確認する問い

第8章の内容を、次の問いで確認してみてください。

因果マスクがない状態で次のトークンを予測する訓練を行うと、どのような問題が起きるでしょうか。

マスクする位置のスコアを0ではなくマイナス無限大にする理由を、Softmax関数の性質から説明してみてください。

GPT系のモデルとBERT系のモデルの、構造上の決定的な違いは何でしょうか。それぞれが得意とする用途は何でしょうか。

まとめ

因果マスクとは、あるトークンを処理する際に、それより後ろにあるトークンへの注目を強制的に遮断する仕組みです。

この仕組みが必要な理由は、GPT-2が次のトークンを予測する課題で学習されているためです。未来のトークンが見えた状態で訓練すると、答えを見ながら答えを当てることになり、学習が成立しません。また、推論時には未来のトークンが物理的に存在しないため、学習時と推論時で条件を一致させる必要があります。

実装は、参照してはいけない位置のスコアを、マイナス無限大に置き換えることで行われます。0ではなくマイナス無限大を使うのは、Softmax関数で指数関数を通した際に、0では値が1として残ってしまい、確率が0にならないためです。

マスクは上三角の形をしており、対角線上の自分自身への注目は遮断されません。全体のおよそ半分の位置が遮断されます。

因果マスクの有無は、GPT系とBERT系を分ける決定的な違いです。GPT系は文章生成に適し、BERT系は双方向の文脈を使った理解に適しています。

次の第9章では、いよいよSoftmax関数による確率への変換と、その確率を使ってValueを重み付きで合成する処理を扱います。ここまで計算してきたスコアが、実際にどのように使われて、文脈を反映したベクトルが作られるのかを解説します。

第9章に進む前に、Transformer Explainerでマスク適用後のスコア行列を確認し、右上の三角形がどのように表示されているかを見ておいてください。次章では、この行列が確率の行列へと変わる様子を観察することになります。

セイ・コンサルティング・グループでは新人エンジニア研修のアシスタント講師を募集しています。

投稿者プロフィール

山崎講師
山崎講師代表取締役
セイ・コンサルティング・グループ株式会社代表取締役。
岐阜県出身。
海外放浪の末、2000年創業、2004年会社設立。
IT企業向け人材育成研修歴業界歴20年以上。
すべての無駄を省いた費用対効果の高い「筋肉質」な研修を提供します!
この記事に間違い等ありましたらぜひお知らせください。

学生時代は趣味と実益を兼ねてリゾートバイトにいそしむ。長野県白馬村に始まり、志賀高原でのスキーインストラクター、沖縄石垣島、北海道トマム。高じてオーストラリアのゴールドコーストでツアーガイドなど。現在は野菜作りにはまっている。