📋 このステップで学ぶこと
- Decoder層の詳細構造(3つのサブ層)
- Masked Self-Attention(未来を見ないマスク)
- Cross-Attention(EncoderとDecoderの接続)
- 完全なTransformerモデルの構築
- マスクの種類と使い分け(Padding、Look-ahead)
- 機械翻訳タスクでの訓練と推論
練習問題: 5問
💻 実行環境について
このステップのコードはGoogle Colabで実行できます。
PyTorchは最初から入っているので、追加インストールは不要です。
このステップでは、STEP 13、14で作成したコンポーネントを使用します。
🎭 1. Decoder層の構造
STEP 14でEncoder層を学びました。Decoder層はEncoder層に2つの新しい要素を追加した構造です。
ここでは、その違いと役割を理解しましょう。
1-1. Transformerの全体像(復習)
【Transformerの全体構造】
┌─────────────────────────────────────────────────────────┐
│ Transformer │
├─────────────────────────────────────────────────────────┤
│ │
│ 【入力側】 【出力側】 │
│ “I love you” “私 は あなた を …” │
│ ↓ ↓ │
│ ┌─────────┐ ┌─────────┐ │
│ │ Encoder │ ────────────→ │ Decoder │ │
│ │ (6層) │ encoder_output │ (6層) │ │
│ └─────────┘ └─────────┘ │
│ ↓ │
│ 次の単語を予測 │
│ │
└─────────────────────────────────────────────────────────┘
【処理の流れ】
1. Encoder: 入力文全体を一度に処理 → 文の表現を作成
2. Decoder: 1単語ずつ順番に生成 → Encoderの出力を参照
1-2. EncoderとDecoderの比較
【Encoder層の構造(復習)】
入力: x (batch, src_len, d_model)
│
▼
┌─────────────────────────────────────┐
│ サブ層1: Multi-Head Self-Attention │
│ ・全単語を同時に見る │
│ ・文脈を理解 │
└─────────────────────────────────────┘
│
├─ 残差結合: x = x + output
│
├─ Layer Norm
│
▼
┌─────────────────────────────────────┐
│ サブ層2: Feed-Forward Network │
│ ・各位置の表現を変換 │
│ ・非線形性を追加 │
└─────────────────────────────────────┘
│
├─ 残差結合 + Layer Norm
│
▼
出力: (batch, src_len, d_model)
合計: 2つのサブ層
【Decoder層の構造(新規)】
入力: x (batch, tgt_len, d_model)
Encoder出力: encoder_output (batch, src_len, d_model)
│
▼
┌─────────────────────────────────────┐
│ サブ層1: Masked Self-Attention │ ← 新規!
│ ・「未来を見ない」制約付き │
│ ・生成済みの単語だけを見る │
└─────────────────────────────────────┘
│
├─ 残差結合 + Layer Norm
│
▼
┌─────────────────────────────────────┐
│ サブ層2: Cross-Attention │ ← 新規!
│ ・Encoderの出力を参照 │
│ ・入力文のどこを見るか学習 │
└─────────────────────────────────────┘
│
├─ 残差結合 + Layer Norm
│
▼
┌─────────────────────────────────────┐
│ サブ層3: Feed-Forward Network │ ← Encoderと同じ
│ ・各位置の表現を変換 │
└─────────────────────────────────────┘
│
├─ 残差結合 + Layer Norm
│
▼
出力: (batch, tgt_len, d_model)
合計: 3つのサブ層(Encoderより1つ多い)
💡 Decoderに追加された2つの新要素
- Masked Self-Attention: 「未来を見ない」制約付きのSelf-Attention。訓練時のカンニングを防ぐ
- Cross-Attention: DecoderがEncoderの出力を参照する。翻訳時に「入力文のどこを見るか」を学習
1-3. 機械翻訳での役割
【機械翻訳の例】
入力(英語): “I love you”
出力(日本語): “私 は あなた を 愛し てい ます”
【ステップ1: Encoderが入力を処理】
“I love you” → Encoder(6層)→ encoder_output
encoder_output の内容:
h_I = [0.5, -0.3, 0.8, …, 0.2] (512次元)
h_love = [0.1, 0.7, -0.2, …, 0.9] (512次元)
h_you = [0.3, 0.4, 0.6, …, -0.1] (512次元)
各単語が文脈を考慮した表現に変換される
(”I”は主語、”love”は動詞、”you”は目的語という情報を含む)
【ステップ2: Decoderが1単語ずつ生成】
■ 時刻1: “私” を生成
入力: [<START>]
Masked Self-Attention:
・<START>だけを見る(これしかない)
・「文を始める」という情報
Cross-Attention:
・encoder_output を参照
・”I”に強く注目(主語 → 主語の対応)
・Attention Weights: I=0.8, love=0.1, you=0.1
FFN → 出力層 → “私” を予測
■ 時刻2: “は” を生成
入力: [<START>, 私]
Masked Self-Attention:
・<START>と”私”を見る
・”私”の後には助詞が来やすい
Cross-Attention:
・文法的要素(特定の単語に強く注目しない)
・Attention Weights: I=0.3, love=0.3, you=0.4
FFN → 出力層 → “は” を予測
■ 時刻3: “あなた” を生成
入力: [<START>, 私, は]
Masked Self-Attention:
・<START>、”私”、”は”を見る
・「私は〜」の後には目的語が来やすい
Cross-Attention:
・”you”に強く注目(目的語 → 目的語の対応)
・Attention Weights: I=0.1, love=0.1, you=0.8
FFN → 出力層 → “あなた” を予測
■ 以降も同様に続く…
“を” → “愛し” → “てい” → “ます” → <END>
【各サブ層の役割まとめ】
1. Masked Self-Attention:
「これまで生成した単語の関係」を理解
例: “私は〜”の後には何が来やすいか
2. Cross-Attention:
「入力文のどこを見るべきか」を決定
例: “あなた”を生成 → “you”に注目
3. FFN:
各位置の表現を非線形変換で豊かに
💡 Decoder層の3つのサブ層まとめ
- Masked Self-Attention: 生成中の出力系列内での関係を学習(未来は見ない)
- Cross-Attention: 入力系列(Encoder)と出力系列(Decoder)の関係を学習
- Feed-Forward Network: 各位置の表現を非線形変換で豊かに
🎭 2. Masked Self-Attention
Decoderは単語を1つずつ順番に生成します(自己回帰的生成)。
訓練時に「未来の単語」を見てしまうと、カンニングになってしまいます。
これを防ぐのがMasked Self-Attentionです。
2-1. なぜマスクが必要なのか
⚠️ 訓練時のカンニング問題
訓練時には正解の出力系列が分かっています。
マスクなしだと、未来の正解を見てしまい、正しく学習できません。
【問題: マスクなしの場合】
翻訳タスク:
入力: “I love you”
正解: “私 は あなた を 愛し てい ます”
訓練時のDecoder入力:
[<START>, 私, は, あなた, を, 愛し, てい, ます]
マスクなしのSelf-Attention:
時刻2で “は” を予測する際:
→ “あなた”, “を”, “愛し” … が全部見える!
→ 「次は”は”だな」と正解を見てしまう
→ これはカンニング!
【結果】
・訓練時: 正解を見ながら予測 → 損失が低い
・推論時: 未来がない状態で予測 → 失敗する
・訓練と推論で条件が違う → 汎化しない
【解決: マスクありの場合】
Masked Self-Attention:
時刻2で “は” を予測する際:
→ <START>, “私” だけが見える
→ “あなた”, “を”, “愛し” … は隠されている
→ 公平な条件で予測
【結果】
・訓練時: 過去だけを見て予測
・推論時: 過去だけを見て予測
・同じ条件 → 正しく汎化
2-2. Look-ahead Mask(先読みマスク)の仕組み
【Look-ahead Maskの作成】
系列長 n = 4 の場合(4単語)
位置: 0, 1, 2, 3
マスク行列(1=見える、0=見えない):
pos0 pos1 pos2 pos3
pos0 [ 1 0 0 0 ] ← 位置0は自分(0)だけ見える
pos1 [ 1 1 0 0 ] ← 位置1は0,1が見える
pos2 [ 1 1 1 0 ] ← 位置2は0,1,2が見える
pos3 [ 1 1 1 1 ] ← 位置3は全部見える
これは「下三角行列」(対角含む)
【具体例で理解】
出力系列: [<START>, 私, は, あなた]
位置0 位置1 位置2 位置3
位置1(”私”を生成)で見えるもの:
・位置0(<START>): ✓ 見える
・位置1(”私”): ✓ 見える(自分自身)
・位置2(”は”): ✗ 見えない(未来)
・位置3(”あなた”): ✗ 見えない(未来)
位置2(”は”を生成)で見えるもの:
・位置0(<START>): ✓ 見える
・位置1(”私”): ✓ 見える
・位置2(”は”): ✓ 見える(自分自身)
・位置3(”あなた”): ✗ 見えない(未来)
2-3. マスクの適用方法
【Attention Scoresへのマスク適用】
ステップ1: 通常のAttention Scores計算
QK^T / √d_k の結果:
pos0 pos1 pos2 pos3
pos0 [ 2.1 1.5 0.8 1.2]
pos1 [ 1.8 2.3 1.1 0.9]
pos2 [ 0.9 1.2 2.0 1.5]
pos3 [ 1.1 0.8 1.3 2.5]
ステップ2: マスクを適用(0の位置を-∞に)
pos0 pos1 pos2 pos3
pos0 [ 2.1 -∞ -∞ -∞ ]
pos1 [ 1.8 2.3 -∞ -∞ ]
pos2 [ 0.9 1.2 2.0 -∞ ]
pos3 [ 1.1 0.8 1.3 2.5]
ステップ3: Softmax適用
exp(-∞) = 0 なので、未来の位置は確率0に
pos0 pos1 pos2 pos3
pos0 [ 1.00 0.00 0.00 0.00] 合計=1.0
pos1 [ 0.38 0.62 0.00 0.00] 合計=1.0
pos2 [ 0.19 0.26 0.55 0.00] 合計=1.0
pos3 [ 0.18 0.13 0.22 0.47] 合計=1.0
→ 未来の位置が完全に0になった!
→ 各行の合計は1.0(確率分布)を維持
2-4. PyTorchでのLook-ahead Mask実装
import torch
def generate_square_subsequent_mask(sz):
“””
Look-ahead Mask(先読みマスク)を生成
Args:
sz: 系列長
Returns:
mask: (sz, sz) のマスク
True = 見える位置
False = 見えない位置(-∞にする)
“””
# torch.triu: 上三角行列を作成
# diagonal=1: 対角線の1つ上から(対角線は含まない)
# 結果: 対角線の上が1、それ以外が0
mask = torch.triu(torch.ones(sz, sz), diagonal=1)
# boolに変換(1→True、0→False)
mask = mask.bool()
# 反転(見えない位置をTrue→Falseに)
# ~演算子: NOT演算(TrueとFalseを反転)
return ~mask
動作確認
# マスクを生成
seq_len = 5
mask = generate_square_subsequent_mask(seq_len)
print(“Look-ahead Mask:”)
print(mask.int()) # True=1, False=0 で表示
print(“\n各位置で見える範囲:”)
for i in range(seq_len):
visible = [j for j in range(seq_len) if mask[i, j]]
print(f” 位置{i}: {visible}”)
実行結果:
Look-ahead Mask:
tensor([[1, 0, 0, 0, 0],
[1, 1, 0, 0, 0],
[1, 1, 1, 0, 0],
[1, 1, 1, 1, 0],
[1, 1, 1, 1, 1]])
各位置で見える範囲:
位置0: [0]
位置1: [0, 1]
位置2: [0, 1, 2]
位置3: [0, 1, 2, 3]
位置4: [0, 1, 2, 3, 4]
マスクをAttention Scoresに適用
def apply_mask(scores, mask):
“””
Attention Scoresにマスクを適用
Args:
scores: Attention Scores (batch, n_heads, seq_len, seq_len)
mask: マスク (seq_len, seq_len) True=見える、False=見えない
Returns:
masked_scores: マスク適用後のスコア
“””
# masked_fill: maskがFalseの位置を-infに置き換え
# float(‘-inf’): 負の無限大(Softmax後に0になる)
return scores.masked_fill(~mask, float(‘-inf’))
# テスト
scores = torch.randn(2, 8, 5, 5) # (batch=2, heads=8, seq_len=5, seq_len=5)
masked_scores = apply_mask(scores, mask)
# Softmax適用
attention_weights = torch.softmax(masked_scores, dim=-1)
print(“マスク適用後のAttention Weights(1サンプル、1ヘッド):”)
print(attention_weights[0, 0].detach().numpy().round(2))
print(“\n各行の合計:”, attention_weights[0, 0].sum(dim=-1).detach().numpy().round(2))
実行結果:
マスク適用後のAttention Weights(1サンプル、1ヘッド):
[[1. 0. 0. 0. 0. ]
[0.45 0.55 0. 0. 0. ]
[0.28 0.35 0.37 0. 0. ]
[0.22 0.18 0.31 0.29 0. ]
[0.15 0.23 0.19 0.21 0.22]]
各行の合計: [1. 1. 1. 1. 1.]
✅ 確認ポイント
- 未来の位置(右上の三角部分)が全て0になっている
- 各行の合計は1.0を維持(確率分布)
- 位置0は自分だけ(1.0)に注目
🔗 3. Cross-Attention
Cross-Attentionは、DecoderがEncoderの出力を参照する仕組みです。
Self-Attentionとの違いを理解しましょう。
3-1. Self-Attention vs Cross-Attention
【Self-Attention(復習)】
同じ系列内での注意
Query、Key、Value が全て同じ系列から来る:
Q = K = V = 同じ系列
Encoder内:
入力: “I love you”
Q, K, V: 全て “I love you” の表現
Decoder内(Masked):
生成中: “私 は”
Q, K, V: 全て “私 は” の表現
(ただし未来はマスク)
【Cross-Attention(新規)】
異なる系列間での注意
Query: Decoder(出力側)から
Key, Value: Encoder(入力側)から
機械翻訳の例:
Encoder出力: “I love you” の表現 → K, V
Decoder状態: “私 は” の表現 → Q
計算:
Attention(Q_decoder, K_encoder, V_encoder)
意味:
「”私 は”を生成する際に、
“I love you”のどこを見るべきか」を学習
【図解】
Self-Attention:
“私 は” ──┐
├──→ Attention ──→ 出力
“私 は” ──┘
(Q) (K,V)
↑同じ系列
Cross-Attention:
“私 は” ────────→ Q
↓
“I love you” ──→ K,V ──→ Attention ──→ 出力
↑異なる系列
3-2. Cross-Attentionの計算
【Cross-Attentionの計算手順】
入力:
Decoder状態: (batch, tgt_len, d_model) = (32, 10, 512)
Encoder出力: (batch, src_len, d_model) = (32, 15, 512)
ステップ1: Q, K, Vの計算
Q = Decoder状態 × W_Q → (32, 10, 512) ← Decoderから
K = Encoder出力 × W_K → (32, 15, 512) ← Encoderから
V = Encoder出力 × W_V → (32, 15, 512) ← Encoderから
ステップ2: Attention Scores
QK^T: (32, 10, 512) × (32, 512, 15) → (32, 10, 15)
形状の意味:
・10: 出力の位置数(何を生成中か)
・15: 入力の位置数(どこを見るか)
・各出力位置が、各入力位置との関連度を持つ
ステップ3: スケーリング + Softmax
softmax(QK^T / √d_k) → (32, 10, 15)
各行(出力位置ごと)で合計1.0の確率分布
ステップ4: Valueとの乗算
Attention Weights × V
(32, 10, 15) × (32, 15, 512) → (32, 10, 512)
入力の情報を重み付けして出力に渡す
【具体例】
入力: “I love you” (3単語)
出力生成中: “愛し” (位置5)
Cross-Attention計算:
Q: “愛し”の表現
K, V: “I”, “love”, “you”の表現
スコア計算:
“愛し” と “I” のスコア: 0.1
“愛し” と “love” のスコア: 0.8 ← 高い!
“愛し” と “you” のスコア: 0.1
Softmax後:
“I”: 0.12
“love”: 0.76 ← 強く注目
“you”: 0.12
出力:
0.12×V_I + 0.76×V_love + 0.12×V_you
→ “love”の情報を強く含む表現
→ “愛し”を生成するのに役立つ!
3-3. Cross-Attentionが学習する対応関係
💡 Cross-Attentionが学習すること
Cross-Attentionは「入力と出力の単語の対応関係」を学習します。
これは機械翻訳でいう「アライメント(対応付け)」に相当します。
【翻訳での対応関係の例】
入力: “I love you”
出力: “私 は あなた を 愛し てい ます”
学習される対応関係:
“私” → “I” に強く注目
“は” → 文法的要素(特定の単語に注目しない)
“あなた” → “you” に強く注目
“を” → 文法的要素
“愛し” → “love” に強く注目
“てい” → “love” に注目
“ます” → “love” に注目(丁寧語)
【可視化するとこうなる】
Cross-Attention Weights (Head 0):
I love you
私 [0.75 0.15 0.10] ← “I”に注目
は [0.30 0.30 0.40] ← 分散(文法)
あなた [0.10 0.10 0.80] ← “you”に注目
を [0.25 0.35 0.40] ← 分散
愛し [0.10 0.80 0.10] ← “love”に注目
てい [0.15 0.70 0.15] ← “love”に注目
ます [0.20 0.60 0.20] ← “love”に注目
→ 単語の対応関係が明確に学習される!
3-4. Cross-Attentionの実装
Cross-AttentionはMulti-Head Attentionと同じ構造ですが、
Q, K, Vの出所が異なります。
# STEP 13のMultiHeadAttentionを使用
# Q: Decoderから、K, V: Encoderから
class CrossAttention(nn.Module):
“””Cross-Attention(MultiHeadAttentionを再利用)”””
def __init__(self, d_model, n_heads, dropout=0.1):
super(CrossAttention, self).__init__()
# MultiHeadAttentionをそのまま使う
self.attention = MultiHeadAttention(d_model, n_heads, dropout)
def forward(self, decoder_state, encoder_output, mask=None):
“””
Args:
decoder_state: Decoderの状態 (batch, tgt_len, d_model) → Q
encoder_output: Encoderの出力 (batch, src_len, d_model) → K, V
mask: Padding用のマスク(オプション)
Returns:
output: (batch, tgt_len, d_model)
attention_weights: (batch, n_heads, tgt_len, src_len)
“””
# Q: Decoder, K: Encoder, V: Encoder
output, attention_weights = self.attention(
Q=decoder_state,
K=encoder_output,
V=encoder_output,
mask=mask
)
return output, attention_weights
動作確認
# Cross-Attentionのテスト
d_model = 512
n_heads = 8
cross_attn = CrossAttention(d_model, n_heads)
# Decoderの状態(生成中の10単語)
decoder_state = torch.randn(2, 10, d_model)
# Encoderの出力(入力文は15単語)
encoder_output = torch.randn(2, 15, d_model)
# Cross-Attention適用
output, weights = cross_attn(decoder_state, encoder_output)
print(f”Decoder状態: {decoder_state.shape}”)
print(f”Encoder出力: {encoder_output.shape}”)
print(f”Cross-Attention出力: {output.shape}”)
print(f”Attention Weights: {weights.shape}”)
print(f”\nWeightsの解釈:”)
print(f” バッチサイズ: {weights.shape[0]}”)
print(f” ヘッド数: {weights.shape[1]}”)
print(f” 出力位置数: {weights.shape[2]} (tgt_len)”)
print(f” 入力位置数: {weights.shape[3]} (src_len)”)
実行結果:
Decoder状態: torch.Size([2, 10, 512])
Encoder出力: torch.Size([2, 15, 512])
Cross-Attention出力: torch.Size([2, 10, 512])
Attention Weights: torch.Size([2, 8, 10, 15])
Weightsの解釈:
バッチサイズ: 2
ヘッド数: 8
出力位置数: 10 (tgt_len)
入力位置数: 15 (src_len)
🏗️ 4. Decoder層の完全実装
3つのサブ層(Masked Self-Attention、Cross-Attention、FFN)を
組み合わせてDecoder層を実装します。
4-1. Decoder層の構造図
【Decoder層の処理フロー】
入力: x (batch, tgt_len, d_model)
Encoder出力: encoder_output (batch, src_len, d_model)
│
▼
┌───────────────────────────────────────┐
│ サブ層1: Masked Self-Attention │
│ ・Q, K, V = x │
│ ・Look-ahead Maskを適用 │
│ ・未来を見ない │
└───────────────────────────────────────┘
│
├─ 残差結合: x = x + output
│
├─ Layer Norm
│
▼
┌───────────────────────────────────────┐
│ サブ層2: Cross-Attention │
│ ・Q = x (Decoder) │
│ ・K, V = encoder_output (Encoder) │
│ ・入力文のどこを見るか学習 │
└───────────────────────────────────────┘
│
├─ 残差結合: x = x + output
│
├─ Layer Norm
│
▼
┌───────────────────────────────────────┐
│ サブ層3: Feed-Forward Network │
│ ・位置ごとの非線形変換 │
│ ・512 → 2048 → 512 │
└───────────────────────────────────────┘
│
├─ 残差結合: x = x + output
│
├─ Layer Norm
│
▼
出力: x (batch, tgt_len, d_model)
4-2. Decoder層の実装
ステップ1:クラスの定義と初期化
class DecoderLayer(nn.Module):
“””Transformerの1つのDecoder層”””
def __init__(self, d_model, n_heads, d_ff, dropout=0.1):
“””
Args:
d_model: モデルの次元数(例: 512)
n_heads: ヘッド数(例: 8)
d_ff: FFNの中間層次元(例: 2048)
dropout: ドロップアウト率
“””
super(DecoderLayer, self).__init__()
# サブ層1: Masked Self-Attention
self.self_attn = MultiHeadAttention(d_model, n_heads, dropout)
# サブ層2: Cross-Attention
self.cross_attn = MultiHeadAttention(d_model, n_heads, dropout)
# サブ層3: Feed-Forward Network
self.feed_forward = PositionwiseFeedForward(d_model, d_ff, dropout)
# Layer Normalization(3つのサブ層それぞれに)
self.norm1 = nn.LayerNorm(d_model)
self.norm2 = nn.LayerNorm(d_model)
self.norm3 = nn.LayerNorm(d_model)
# Dropout
self.dropout = nn.Dropout(dropout)
self.d_model = d_model
ステップ2:forward関数
def forward(self, x, encoder_output, src_mask=None, tgt_mask=None):
“””
Args:
x: Decoder入力 (batch, tgt_len, d_model)
encoder_output: Encoder出力 (batch, src_len, d_model)
src_mask: 入力側のマスク(Padding用)
tgt_mask: 出力側のマスク(Look-ahead用)
Returns:
出力 (batch, tgt_len, d_model)
“””
# サブ層1: Masked Self-Attention
# Q, K, V 全て x から
attn_output, _ = self.self_attn(x, x, x, tgt_mask)
x = self.norm1(x + self.dropout(attn_output))
# サブ層2: Cross-Attention
# Q: x (Decoder), K, V: encoder_output (Encoder)
cross_output, _ = self.cross_attn(x, encoder_output, encoder_output, src_mask)
x = self.norm2(x + self.dropout(cross_output))
# サブ層3: Feed-Forward Network
ff_output = self.feed_forward(x)
x = self.norm3(x + self.dropout(ff_output))
return x
4-3. Decoder全体(N層)の実装
import copy
class Decoder(nn.Module):
“””N層のDecoder”””
def __init__(self, d_model, n_heads, d_ff, n_layers, dropout=0.1):
“””
Args:
d_model: モデル次元
n_heads: ヘッド数
d_ff: FFN中間層次元
n_layers: 層数(例: 6)
dropout: ドロップアウト率
“””
super(Decoder, self).__init__()
# N層のDecoder層
self.layers = nn.ModuleList([
DecoderLayer(d_model, n_heads, d_ff, dropout)
for _ in range(n_layers)
])
# 最後のLayer Norm
self.norm = nn.LayerNorm(d_model)
self.d_model = d_model
def forward(self, x, encoder_output, src_mask=None, tgt_mask=None):
“””
Args:
x: (batch, tgt_len, d_model)
encoder_output: (batch, src_len, d_model)
src_mask: 入力側マスク
tgt_mask: 出力側マスク(Look-ahead)
“””
# 各層を順番に通す
for layer in self.layers:
x = layer(x, encoder_output, src_mask, tgt_mask)
# 最後にLayer Norm
return self.norm(x)
動作確認
# Decoderのテスト
d_model = 512
n_heads = 8
d_ff = 2048
n_layers = 6
decoder = Decoder(d_model, n_heads, d_ff, n_layers)
# 入力
batch_size = 2
tgt_len = 10
src_len = 15
decoder_input = torch.randn(batch_size, tgt_len, d_model)
encoder_output = torch.randn(batch_size, src_len, d_model)
# マスク生成
tgt_mask = generate_square_subsequent_mask(tgt_len)
# Decoder適用
output = decoder(decoder_input, encoder_output, tgt_mask=tgt_mask)
print(f”Decoder入力: {decoder_input.shape}”)
print(f”Encoder出力: {encoder_output.shape}”)
print(f”Decoder出力: {output.shape}”)
# パラメータ数
params = sum(p.numel() for p in decoder.parameters())
print(f”\nDecoderのパラメータ数: {params:,}”)
print(f”(Encoderより多い:Cross-Attentionが追加されるため)”)
実行結果:
Decoder入力: torch.Size([2, 10, 512])
Encoder出力: torch.Size([2, 15, 512])
Decoder出力: torch.Size([2, 10, 512])
Decoderのパラメータ数: 25,198,080
(Encoderより多い:Cross-Attentionが追加されるため)
🎯 5. 完全なTransformerモデル
これまでの全てのコンポーネントを統合して、
完全なTransformerモデルを構築します。
5-1. Transformerの全体構造
【Transformerの全体構造】
入力: src (batch, src_len) – 入力文の単語ID
出力: tgt (batch, tgt_len) – 出力文の単語ID
┌─────────────────────────────────────────────────────────┐
│ Transformer │
├─────────────────────────────────────────────────────────┤
│ │
│ 【入力側】 【出力側】 │
│ │
│ src tgt │
│ ↓ ↓ │
│ Embedding Embedding │
│ ↓ ↓ │
│ Positional Encoding Positional Encoding │
│ ↓ ↓ │
│ ┌─────────┐ ┌─────────┐ │
│ │ Encoder │ │ Decoder │ │
│ │ (6層) │──────────────→ │ (6層) │ │
│ └─────────┘ encoder_output └─────────┘ │
│ ↓ │
│ Linear │
│ ↓ │
│ Softmax │
│ ↓ │
│ output (vocab_size) │
│ │
└─────────────────────────────────────────────────────────┘
5-2. Transformerの実装
※コードが長いため、横スクロールできます。
import math
import torch
import torch.nn as nn
class Transformer(nn.Module):
“””完全なTransformerモデル(Encoder-Decoder)”””
def __init__(self, src_vocab_size, tgt_vocab_size,
d_model=512, n_heads=8, n_layers=6,
d_ff=2048, dropout=0.1, max_len=5000):
“””
Args:
src_vocab_size: 入力側の語彙サイズ(例: 10000)
tgt_vocab_size: 出力側の語彙サイズ(例: 10000)
d_model: モデル次元(例: 512)
n_heads: ヘッド数(例: 8)
n_layers: 層数(例: 6)
d_ff: FFN中間層次元(例: 2048)
dropout: ドロップアウト率
max_len: 最大系列長
“””
super(Transformer, self).__init__()
# ========== 埋め込み層 ==========
# 入力側: 単語ID → d_model次元のベクトル
self.src_embedding = nn.Embedding(src_vocab_size, d_model)
# 出力側: 単語ID → d_model次元のベクトル
self.tgt_embedding = nn.Embedding(tgt_vocab_size, d_model)
# ========== 位置エンコーディング ==========
self.pos_encoding = PositionalEncoding(d_model, max_len, dropout)
# ========== Encoder ==========
self.encoder = Encoder(d_model, n_heads, d_ff, n_layers, dropout)
# ========== Decoder ==========
self.decoder = Decoder(d_model, n_heads, d_ff, n_layers, dropout)
# ========== 出力層 ==========
# d_model次元 → 語彙サイズ(各単語の確率)
self.fc_out = nn.Linear(d_model, tgt_vocab_size)
self.d_model = d_model
self.dropout = nn.Dropout(dropout)
# パラメータ初期化
self._init_parameters()
def _init_parameters(self):
“””Xavier初期化”””
for p in self.parameters():
if p.dim() > 1:
nn.init.xavier_uniform_(p)
def forward(self, src, tgt, src_mask=None, tgt_mask=None):
“””
Args:
src: 入力系列 (batch, src_len) – 単語ID
tgt: 出力系列 (batch, tgt_len) – 単語ID
src_mask: 入力側マスク(Padding用)
tgt_mask: 出力側マスク(Look-ahead用)
Returns:
output: (batch, tgt_len, tgt_vocab_size) – 各位置の単語確率
“””
# ========== Encoder ==========
# 埋め込み(√d_modelでスケーリング)
src_emb = self.src_embedding(src) * math.sqrt(self.d_model)
# 位置エンコーディングを加算
src_emb = self.pos_encoding(src_emb)
# Encoderを通す
encoder_output = self.encoder(src_emb, src_mask)
# ========== Decoder ==========
# 埋め込み
tgt_emb = self.tgt_embedding(tgt) * math.sqrt(self.d_model)
# 位置エンコーディング
tgt_emb = self.pos_encoding(tgt_emb)
# Decoderを通す
decoder_output = self.decoder(tgt_emb, encoder_output, src_mask, tgt_mask)
# ========== 出力層 ==========
# 語彙への射影
output = self.fc_out(decoder_output)
return output
def encode(self, src, src_mask=None):
“””Encoderのみ実行(推論時に使用)”””
src_emb = self.src_embedding(src) * math.sqrt(self.d_model)
src_emb = self.pos_encoding(src_emb)
return self.encoder(src_emb, src_mask)
def decode(self, tgt, encoder_output, src_mask=None, tgt_mask=None):
“””Decoderのみ実行(推論時に使用)”””
tgt_emb = self.tgt_embedding(tgt) * math.sqrt(self.d_model)
tgt_emb = self.pos_encoding(tgt_emb)
decoder_output = self.decoder(tgt_emb, encoder_output, src_mask, tgt_mask)
return self.fc_out(decoder_output)
動作確認
# モデルの作成
src_vocab_size = 10000 # 英語の語彙
tgt_vocab_size = 10000 # 日本語の語彙
model = Transformer(
src_vocab_size=src_vocab_size,
tgt_vocab_size=tgt_vocab_size,
d_model=512,
n_heads=8,
n_layers=6,
d_ff=2048,
dropout=0.1
)
# 入力データ(単語IDの列)
batch_size = 2
src_len = 15
tgt_len = 20
src = torch.randint(0, src_vocab_size, (batch_size, src_len))
tgt = torch.randint(0, tgt_vocab_size, (batch_size, tgt_len))
# マスク生成
tgt_mask = generate_square_subsequent_mask(tgt_len)
# Forward
output = model(src, tgt, tgt_mask=tgt_mask)
print(f”入力(src): {src.shape}”)
print(f”出力(tgt): {tgt.shape}”)
print(f”モデル出力: {output.shape}”)
print(f” → 各位置で{tgt_vocab_size}単語の確率を出力”)
# パラメータ数
total_params = sum(p.numel() for p in model.parameters())
print(f”\n総パラメータ数: {total_params:,}”)
実行結果:
入力(src): torch.Size([2, 15])
出力(tgt): torch.Size([2, 20])
モデル出力: torch.Size([2, 20, 10000])
→ 各位置で10000単語の確率を出力
総パラメータ数: 64,918,528
💡 パラメータ数の内訳(約6500万)
- 埋め込み層: (10000 + 10000) × 512 ≈ 1000万
- Encoder(6層): 約1900万
- Decoder(6層): 約2500万
- 出力層: 512 × 10000 ≈ 500万
🎓 6. 訓練と推論
Transformerモデルを機械翻訳タスクで訓練し、推論する方法を学びます。
6-1. 訓練の基本
【訓練の流れ】
1. 入力と正解を準備
入力(src): “I love you”
正解(tgt): “<START> 私 は あなた を 愛し てい ます <END>”
2. Decoderへの入力と教師ラベルを分ける
Decoder入力: “<START> 私 は あなた を 愛し てい ます”(最後を除く)
教師ラベル: “私 は あなた を 愛し てい ます <END>”(最初を除く)
3. Forward
output = model(src, decoder_input)
→ 各位置で次の単語の確率を予測
4. 損失計算
CrossEntropyLoss(output, 教師ラベル)
→ 予測と正解の差を計算
5. Backward
損失を逆伝播してパラメータを更新
【Teacher Forcing】
訓練時は「正解の単語」を次の入力として使う
(推論時は「予測した単語」を使う)
これにより:
・訓練が安定する
・収束が速い
6-2. 訓練コードの実装
import torch.optim as optim
# デバイス設定
device = torch.device(‘cuda’ if torch.cuda.is_available() else ‘cpu’)
model = model.to(device)
# Paddingトークンのインデックス
PAD_IDX = 0
# 損失関数(Paddingを無視)
criterion = nn.CrossEntropyLoss(ignore_index=PAD_IDX)
# 最適化器(Adam)
optimizer = optim.Adam(model.parameters(), lr=0.0001, betas=(0.9, 0.98), eps=1e-9)
def train_step(model, src, tgt, optimizer, criterion, device):
“””
1バッチの訓練
Args:
src: 入力系列 (batch, src_len)
tgt: 出力系列 (batch, tgt_len) – <START>〜<END>を含む
“””
model.train()
src = src.to(device)
tgt = tgt.to(device)
# Decoderの入力と教師ラベルを分ける
# 入力: <START>〜最後の1つ前
# ラベル: 最初の1つ〜<END>
tgt_input = tgt[:, :-1]
tgt_output = tgt[:, 1:]
# Look-aheadマスク生成
tgt_mask = generate_square_subsequent_mask(tgt_input.size(1)).to(device)
# Forward
optimizer.zero_grad()
output = model(src, tgt_input, tgt_mask=tgt_mask)
# 損失計算
# output: (batch, tgt_len-1, vocab_size)
# tgt_output: (batch, tgt_len-1)
# reshapeして2次元に
loss = criterion(
output.reshape(-1, output.size(-1)), # (batch * tgt_len-1, vocab_size)
tgt_output.reshape(-1) # (batch * tgt_len-1)
)
# Backward
loss.backward()
# 勾配クリッピング(勾配爆発を防ぐ)
torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)
# パラメータ更新
optimizer.step()
return loss.item()
6-3. 推論(Greedy Search)
【推論の流れ(Greedy Search)】
1. Encoderで入力文を処理
encoder_output = model.encode(src)
2. Decoderを1単語ずつ生成
初期入力: [<START>]
3. ループ
a. 現在の系列でDecoder実行
b. 最後の位置の出力から最も確率が高い単語を選択
c. その単語を系列に追加
d. <END>が出たら終了
【Greedy = 貪欲】
・各ステップで最も確率が高い単語を選ぶ
・シンプルだが最適とは限らない
・計算が速い
def greedy_decode(model, src, start_token, end_token, max_len=50, device=’cpu’):
“””
Greedy Searchによる生成
Args:
model: Transformerモデル
src: 入力系列 (1, src_len)
start_token: 開始トークンのID
end_token: 終了トークンのID
max_len: 最大生成長
device: デバイス
Returns:
generated: 生成された系列(単語IDのリスト)
“””
model.eval()
src = src.to(device)
# Encoderを1回だけ実行
with torch.no_grad():
encoder_output = model.encode(src)
# Decoderの初期入力
generated = [start_token]
for _ in range(max_len):
# 現在の系列をテンソルに
tgt = torch.LongTensor([generated]).to(device)
# Look-aheadマスク
tgt_mask = generate_square_subsequent_mask(tgt.size(1)).to(device)
# Decoder実行
with torch.no_grad():
output = model.decode(tgt, encoder_output, tgt_mask=tgt_mask)
# 最後の位置の予測
# output: (1, current_len, vocab_size)
next_word_logits = output[0, -1, :] # (vocab_size)
# 最も確率が高い単語を選択
next_word = next_word_logits.argmax().item()
# 終了トークンなら終了
if next_word == end_token:
break
# 系列に追加
generated.append(next_word)
return generated
# 使用例(実際にはデータローダーから取得)
START_TOKEN = 1 # <START>のID
END_TOKEN = 2 # <END>のID
# ダミーの入力
src = torch.randint(3, src_vocab_size, (1, 10))
# 生成
generated = greedy_decode(model, src, START_TOKEN, END_TOKEN, device=device)
print(f”生成された系列: {generated}”)
6-4. Beam Search(より良い生成)
【Beam Searchとは】
Greedy Searchの問題:
・各ステップで最も確率が高い単語を選ぶ
・局所最適解に陥りやすい
・「全体として良い系列」を逃す可能性
Beam Searchの解決:
・複数の候補(beam)を同時に探索
・beam_size個の候補を保持
・最終的に最も良い候補を選択
【例: beam_size = 3】
ステップ1: <START>
候補: [([“私”], 0.4), ([“彼”], 0.3), ([“彼女”], 0.2)]
ステップ2: 各候補を拡張
・”私” → “私 は” (0.4×0.5=0.20), “私 が” (0.4×0.3=0.12), …
・”彼” → “彼 は” (0.3×0.4=0.12), “彼 が” (0.3×0.3=0.09), …
・…
上位3つを選択:
候補: [([“私”, “は”], 0.20), ([“彼”, “は”], 0.12), ([“私”, “が”], 0.12)]
ステップ3: さらに拡張…
最終的に最もスコアが高い系列を選択
【利点】
・より良い翻訳結果を得やすい
・多様な候補を探索
【欠点】
・計算量がbeam_size倍
・beam_sizeが大きすぎると効果が薄れることも
📝 練習問題
このステップで学んだ内容を確認しましょう。
問題1:Masked Self-Attention
Masked Self-Attentionが必要な理由は何ですか?
- 計算を高速化するため
- 訓練時に未来の単語を見ないようにするため
- パラメータ数を削減するため
- メモリ使用量を削減するため
正解:b
Masked Self-Attentionは訓練時に未来の単語を見ないようにするために必要です。
問題:訓練時には正解の出力系列が見えている
- マスクなし: 「は」を予測する際に「あなた」「を」が見える(カンニング)
- マスクあり: 「は」を予測する際に過去(「私」)だけが見える(公平)
効果:訓練時と推論時で同じ条件になり、正しく汎化できる
問題2:Cross-Attention
Cross-Attentionにおける Query、Key、Value の出所は?
- 全て Decoder から
- 全て Encoder から
- Query: Decoder、Key&Value: Encoder
- Query: Encoder、Key&Value: Decoder
正解:c
Cross-AttentionではQuery はDecoder、Key と Value はEncoderから来ます。
意味:
- Query(Decoder): 「今生成している単語から見て…」
- Key, Value(Encoder): 「入力文のどこを見るべきか」
例:「愛し」を生成する際に、入力文の「love」に強く注目
問題3:Decoder層の構成
Decoder層のサブ層の正しい順序は?
- Self-Attention → FFN → Cross-Attention
- Cross-Attention → Self-Attention → FFN
- Masked Self-Attention → Cross-Attention → FFN
- FFN → Masked Self-Attention → Cross-Attention
正解:c
Decoder層の正しい順序はMasked Self-Attention → Cross-Attention → FFNです。
各サブ層の役割:
- Masked Self-Attention: 生成中の系列内での関係を学習(未来は見ない)
- Cross-Attention: 入力文のどこを見るかを学習
- FFN: 表現を非線形変換で豊かに
問題4:Look-ahead Mask
Look-ahead Maskの形状は?(系列長n=4の場合)
- 全て1の4×4行列
- 下三角行列(対角含む)
- 上三角行列(対角含む)
- 対角行列
正解:b
Look-ahead Maskは下三角行列(対角含む)です。
pos0 pos1 pos2 pos3
pos0 [ 1 0 0 0 ]
pos1 [ 1 1 0 0 ]
pos2 [ 1 1 1 0 ]
pos3 [ 1 1 1 1 ]
各位置は「自分と過去」だけを見ることができます。
問題5:Transformerのパラメータ
Transformer層で最もパラメータが多いのはどれ?
- Multi-Head Attention
- Feed-Forward Network
- Positional Encoding
- Layer Normalization
正解:b
Feed-Forward Networkが最もパラメータが多いです(層内の約2/3)。
パラメータ数の比較(d_model=512, d_ff=2048の場合):
- FFN: 512×2048 + 2048×512 ≈ 200万
- Multi-Head Attention: 4×512×512 ≈ 100万
- Layer Norm: 2×512 ≈ 1000(ごく少数)
- Positional Encoding: 0(パラメータなし)
📝 STEP 15 のまとめ
✅ このステップで学んだこと
- Decoder層:Masked Self-Attention + Cross-Attention + FFN の3サブ層
- Masked Self-Attention:未来を見ないLook-ahead Mask
- Cross-Attention:DecoderがEncoderを参照(Q: Decoder、K,V: Encoder)
- 完全なTransformer:Encoder-Decoderの統合、約6500万パラメータ
- 訓練と推論:Teacher Forcing、Greedy Search、Beam Search
🎉 Part 4完了!Transformerを完全マスター!
STEP 12〜15で、Transformerの全てを学びました!
習得したスキル:
- Self-Attention(Q, K, V)の仕組みと実装
- Multi-Head Attentionの構築
- Positional Encodingの理論と実装
- Feed-Forward Networkの役割
- 残差結合とLayer Normalization
- Masked Self-AttentionとLook-ahead Mask
- Cross-Attentionの実装
- 完全なEncoder-Decoder Transformerの構築
🎯 次のステップの準備
次のPart 5(STEP 16-19)では、事前学習モデルを学びます!
学ぶ内容:
- STEP 16: BERTの理論(Masked LM、NSP)
- STEP 17: BERTのファインチューニング
- STEP 18: GPTシリーズ(ChatGPTの仕組み)
- STEP 19: その他の事前学習モデル(RoBERTa、T5など)
現代NLPの核心技術へ進みましょう!