STEP 15:Transformer全体の実装

🏗️ STEP 15: Transformer全体の実装

Encoder-Decoderを統合し、完全なTransformerモデルを構築します

📋 このステップで学ぶこと

  • 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つの新要素
  1. Masked Self-Attention: 「未来を見ない」制約付きのSelf-Attention。訓練時のカンニングを防ぐ
  2. 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つのサブ層まとめ
  1. Masked Self-Attention: 生成中の出力系列内での関係を学習(未来は見ない)
  2. Cross-Attention: 入力系列(Encoder)と出力系列(Decoder)の関係を学習
  3. 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が必要な理由は何ですか?

  1. 計算を高速化するため
  2. 訓練時に未来の単語を見ないようにするため
  3. パラメータ数を削減するため
  4. メモリ使用量を削減するため
正解:b

Masked Self-Attentionは訓練時に未来の単語を見ないようにするために必要です。

問題:訓練時には正解の出力系列が見えている

  • マスクなし: 「は」を予測する際に「あなた」「を」が見える(カンニング)
  • マスクあり: 「は」を予測する際に過去(「私」)だけが見える(公平)

効果:訓練時と推論時で同じ条件になり、正しく汎化できる

問題2:Cross-Attention

Cross-Attentionにおける Query、Key、Value の出所は?

  1. 全て Decoder から
  2. 全て Encoder から
  3. Query: Decoder、Key&Value: Encoder
  4. Query: Encoder、Key&Value: Decoder
正解:c

Cross-AttentionではQuery はDecoder、Key と Value はEncoderから来ます。

意味:

  • Query(Decoder): 「今生成している単語から見て…」
  • Key, Value(Encoder): 「入力文のどこを見るべきか」

例:「愛し」を生成する際に、入力文の「love」に強く注目

問題3:Decoder層の構成

Decoder層のサブ層の正しい順序は?

  1. Self-Attention → FFN → Cross-Attention
  2. Cross-Attention → Self-Attention → FFN
  3. Masked Self-Attention → Cross-Attention → FFN
  4. FFN → Masked Self-Attention → Cross-Attention
正解:c

Decoder層の正しい順序はMasked Self-Attention → Cross-Attention → FFNです。

各サブ層の役割:

  1. Masked Self-Attention: 生成中の系列内での関係を学習(未来は見ない)
  2. Cross-Attention: 入力文のどこを見るかを学習
  3. FFN: 表現を非線形変換で豊かに

問題4:Look-ahead Mask

Look-ahead Maskの形状は?(系列長n=4の場合)

  1. 全て1の4×4行列
  2. 下三角行列(対角含む)
  3. 上三角行列(対角含む)
  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層で最もパラメータが多いのはどれ?

  1. Multi-Head Attention
  2. Feed-Forward Network
  3. Positional Encoding
  4. 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の核心技術へ進みましょう!

📝

学習メモ

自然言語処理(NLP) - Step 15

📋 過去のメモ一覧
#artnasekai #学習メモ
LINE