📋 このステップで学ぶこと
Self-Attentionの位置情報欠如問題
Positional Encoding(位置エンコーディング)の必要性
正弦波・余弦波による位置埋め込みの数式と計算
Feed-Forward Network(FFN)の役割と構造
残差結合(Residual Connection)の重要性
Layer Normalizationの仕組み
完全なEncoder層の実装
練習問題: 4問
💻 実行環境について
このステップのコードはGoogle Colab で実行できます。
PyTorchは最初から入っているので、追加インストールは不要です。
📍 1. 位置情報の必要性
STEP 13で学んだSelf-Attentionは強力ですが、
単語の順序(位置情報)を考慮しない という根本的な問題があります。
1-1. Self-Attentionの致命的な問題
⚠️ 順序を無視してしまう問題
Self-Attentionは「どの単語がどの単語と関連するか」は計算できますが、
「どの順番で並んでいるか」は全く考慮しません。
【実験】同じ単語を並び替えてみる
文1: “The cat sat on the mat”
(猫がマットの上に座った)
文2: “The mat sat on the cat”
(マットが猫の上に座った – 意味が逆!)
文3: “cat The mat on sat the”
(文法的に意味不明)
【Self-Attentionの計算結果】
Self-Attentionは各単語間の関連度を計算:
・”cat” ↔ “sat” の関連度
・”cat” ↔ “mat” の関連度
・”on” ↔ “the” の関連度
…
問題:
上記3つの文は、単語の集合が同じなので
Self-Attentionの計算結果は同じになってしまう!
文1の “cat” と 文2の “cat” は、
どちらも同じAttention Weightを持つ
→ 意味が全然違うのに区別できない!
1-2. なぜ位置情報が重要なのか
【例1: 主語と目的語の判断】
“Dog bites man” (犬が人を噛んだ – 日常的な出来事)
“Man bites dog” (人が犬を噛んだ – ニュース!)
位置によって:
・最初の名詞 = 主語(動作をする側)
・最後の名詞 = 目的語(動作を受ける側)
位置情報なしでは:
“Dog”, “bites”, “man” という単語の集合
→ 誰が誰を噛んだか判断できない
【例2: 否定の位置】
“I do not like cats”(私は猫が好きではない)
“I like not do cats”(文法的に意味不明)
“not”の位置が重要:
・動詞の直前 → 正しい否定
・それ以外 → 文法エラー
【例3: 形容詞の修飾対象】
“The big red ball”
・”big” と “red” は “ball” を修飾
・位置で修飾関係が決まる
“The red big ball”
・自然な語順ではないが許容される
・位置が意味に影響
【結論】
言語では位置が意味を決定する重要な要素
→ Self-Attentionには位置情報を追加する必要がある
1-3. RNN/LSTMとの比較
【なぜRNN/LSTMは位置情報を持つのか】
RNN/LSTMの処理:
時刻1: “The” → h₁
時刻2: “cat” → h₂ = f(h₁, “cat”) ← h₁の情報を含む
時刻3: “sat” → h₃ = f(h₂, “sat”) ← h₂の情報を含む
時刻4: “on” → h₄ = f(h₃, “on”) ← h₃の情報を含む
…
ポイント:
・逐次処理により、どの時刻で処理されたか暗黙的に分かる
・h₃ には「3番目に処理された」という情報が含まれる
・位置情報が自然に組み込まれる
【Self-Attentionの処理】
Self-Attentionの処理:
┌─────┬─────┬─────┬─────┐
│ The │ cat │ sat │ on │ ← 全て同時に処理
└─────┴─────┴─────┴─────┘
ポイント:
・全単語を同時に処理(並列化の利点)
・「何番目に現れたか」の情報がない
・明示的に位置情報を追加する必要がある
【解決策: Positional Encoding】
各単語の埋め込みに「位置を表すベクトル」を加算
入力 = 単語埋め込み + 位置エンコーディング
“cat”(位置1) の入力 ≠ “cat”(位置5) の入力
→ 同じ単語でも位置が違えば異なる表現に!
💡 まとめ: 位置情報が必要な理由
Self-Attentionは単語の順序を考慮しない
言語では位置が意味を決定する重要な要素
RNN/LSTMは逐次処理により暗黙的に位置情報を持つ
Self-Attentionには明示的な位置情報の追加が必要
🌊 2. Positional Encoding(位置エンコーディング)
Positional Encoding は、各単語の埋め込みベクトルに
位置を表すベクトル を加算することで位置情報を追加します。
2-1. 基本的なアイデア
【Positional Encodingの仕組み】
入力文: “I love you” (3単語)
ステップ1: 各単語を埋め込みベクトルに変換
“I” → [0.5, -0.3, 0.8, …, 0.2] (512次元)
“love” → [0.1, 0.7, -0.2, …, 0.9] (512次元)
“you” → [0.3, 0.4, 0.6, …, -0.1] (512次元)
ステップ2: 各位置のPositional Encodingを計算
位置0 → PE(0) = [0.0, 1.0, 0.0, …, 0.5] (512次元)
位置1 → PE(1) = [0.84, 0.54, 0.01, …, 0.3] (512次元)
位置2 → PE(2) = [0.91, -0.42, 0.02, …, 0.1] (512次元)
ステップ3: 埋め込み + 位置エンコーディング(加算!)
“I”(位置0) = [0.5, -0.3, 0.8, …] + [0.0, 1.0, 0.0, …]
= [0.5, 0.7, 0.8, …]
“love”(位置1) = [0.1, 0.7, -0.2, …] + [0.84, 0.54, 0.01, …]
= [0.94, 1.24, -0.19, …]
“you”(位置2) = [0.3, 0.4, 0.6, …] + [0.91, -0.42, 0.02, …]
= [1.21, -0.02, 0.62, …]
【重要なポイント】
・結合(concatenate)ではなく加算(add)
・次元数は変わらない(512のまま)
・同じ単語でも位置が違えば異なるベクトルになる
2-2. 正弦波・余弦波による位置エンコーディング
💡 なぜ正弦波と余弦波を使うのか?
Transformerの原論文では、sin(正弦波) とcos(余弦波) を
使った位置エンコーディングを提案しています。
単純な位置番号(0, 1, 2…)ではなく、周期関数を使う理由があります。
【位置エンコーディングの公式】
PE(pos, 2i) = sin(pos / 10000^(2i/d_model))
PE(pos, 2i+1) = cos(pos / 10000^(2i/d_model))
【パラメータの意味】
pos : 単語の位置(0, 1, 2, 3, …)
i : 次元のペア番号(0, 1, 2, …, d_model/2 – 1)
d_model: モデルの次元数(例: 512)
【ルール】
偶数次元(0, 2, 4, …): sin関数を使用
奇数次元(1, 3, 5, …): cos関数を使用
【周波数の違い】
i=0 の場合: pos / 10000^0 = pos / 1 = pos
→ 周波数が高い(細かく変化)
i=255 (d_model=512の場合): pos / 10000^1 = pos / 10000
→ 周波数が低い(ゆっくり変化)
低次元: 高周波(短い周期)→ 近い位置の違いを表現
高次元: 低周波(長い周期)→ 遠い位置の違いを表現
2-3. 計算例(手計算で理解)
【具体的な計算例】
設定: d_model = 4(簡略化のため)
位置: 0, 1, 2 の3つを計算
■ 位置0 のPositional Encoding
———————————
次元0(偶数、i=0):
PE(0, 0) = sin(0 / 10000^(0/4))
= sin(0 / 10000^0)
= sin(0 / 1)
= sin(0)
= 0.0
次元1(奇数、i=0):
PE(0, 1) = cos(0 / 10000^(0/4))
= cos(0 / 1)
= cos(0)
= 1.0
次元2(偶数、i=1):
PE(0, 2) = sin(0 / 10000^(2/4))
= sin(0 / 10000^0.5)
= sin(0 / 100)
= sin(0)
= 0.0
次元3(奇数、i=1):
PE(0, 3) = cos(0 / 10000^(2/4))
= cos(0 / 100)
= cos(0)
= 1.0
PE(0) = [0.0, 1.0, 0.0, 1.0]
■ 位置1 のPositional Encoding
———————————
次元0:
PE(1, 0) = sin(1 / 1) = sin(1) ≈ 0.841
次元1:
PE(1, 1) = cos(1 / 1) = cos(1) ≈ 0.540
次元2:
PE(1, 2) = sin(1 / 100) = sin(0.01) ≈ 0.010
次元3:
PE(1, 3) = cos(1 / 100) = cos(0.01) ≈ 0.9999
PE(1) = [0.841, 0.540, 0.010, 0.9999]
■ 位置2 のPositional Encoding
———————————
次元0:
PE(2, 0) = sin(2 / 1) = sin(2) ≈ 0.909
次元1:
PE(2, 1) = cos(2 / 1) = cos(2) ≈ -0.416
次元2:
PE(2, 2) = sin(2 / 100) = sin(0.02) ≈ 0.020
次元3:
PE(2, 3) = cos(2 / 100) = cos(0.02) ≈ 0.9998
PE(2) = [0.909, -0.416, 0.020, 0.9998]
【観察ポイント】
・各位置で異なるパターン
・低次元(0, 1): 大きく変化(高周波)
・高次元(2, 3): ゆっくり変化(低周波)
・位置が違えば必ず異なるベクトルになる
2-4. 正弦波・余弦波を使う4つの理由
💡 なぜsin/cosを使うのか?
相対位置を学習しやすい
三角関数の加法定理により、PE(pos+k)はPE(pos)の線形結合で表せる
sin(α+β) = sin(α)cos(β) + cos(α)sin(β)
→ モデルは「何単語離れているか」を学習しやすい
任意の長さに対応
数式で決まるため、訓練時より長い系列も処理可能
学習可能な埋め込みは訓練時の長さに制限される
パラメータ不要
学習するパラメータがない → メモリ効率が良い
多様な時間スケール
低次元: 短周期 → 近い位置の違い
高次元: 長周期 → 遠い位置の違い
2-5. PyTorchでの実装
ステップ1:基本構造の定義
import torch
import torch.nn as nn
import math
class PositionalEncoding(nn.Module):
“””正弦波・余弦波による位置エンコーディング”””
def __init__(self, d_model, max_len=5000, dropout=0.1):
“””
Args:
d_model: モデルの次元数(例: 512)
max_len: 最大系列長(例: 5000)
dropout: ドロップアウト率
“””
super(PositionalEncoding, self).__init__()
self.dropout = nn.Dropout(p=dropout)
ステップ2:位置エンコーディングの計算
# 位置エンコーディングを格納するテンソル
# pe: (max_len, d_model)
pe = torch.zeros(max_len, d_model)
# 位置インデックス: [[0], [1], [2], …, [max_len-1]]
# position: (max_len, 1)
position = torch.arange(0, max_len, dtype=torch.float).unsqueeze(1)
# 分母の計算: 10000^(2i/d_model)
# 対数空間で計算(数値安定性のため)
# exp(2i * (-log(10000) / d_model)) = 10000^(-2i/d_model) = 1/10000^(2i/d_model)
div_term = torch.exp(
torch.arange(0, d_model, 2).float() * (-math.log(10000.0) / d_model)
)
# div_term: (d_model/2,) = (256,) if d_model=512
ステップ3:sin/cosの適用とバッファ登録
# 偶数次元にsin、奇数次元にcosを適用
# pe[:, 0::2] は 0, 2, 4, … 番目の次元
# pe[:, 1::2] は 1, 3, 5, … 番目の次元
pe[:, 0::2] = torch.sin(position * div_term) # 偶数次元
pe[:, 1::2] = torch.cos(position * div_term) # 奇数次元
# バッチ次元を追加: (1, max_len, d_model)
pe = pe.unsqueeze(0)
# register_buffer: 学習対象外だがモデルと一緒に保存される
# 学習時に更新されない定数として扱う
self.register_buffer(‘pe’, pe)
ステップ4:forward関数
def forward(self, x):
“””
Args:
x: 入力テンソル (batch, seq_len, d_model)
Returns:
位置エンコーディングを加算した結果 (batch, seq_len, d_model)
“””
# x.size(1) は系列長
# self.pe[:, :x.size(1), :] で必要な長さ分だけ取得
x = x + self.pe[:, :x.size(1), :]
return self.dropout(x)
完成コード(コピー用)
※コードが長いため、横スクロールできます。
import torch
import torch.nn as nn
import math
class PositionalEncoding(nn.Module):
“””正弦波・余弦波による位置エンコーディング”””
def __init__(self, d_model, max_len=5000, dropout=0.1):
super(PositionalEncoding, self).__init__()
self.dropout = nn.Dropout(p=dropout)
pe = torch.zeros(max_len, d_model)
position = torch.arange(0, max_len, dtype=torch.float).unsqueeze(1)
div_term = torch.exp(torch.arange(0, d_model, 2).float() * (-math.log(10000.0) / d_model))
pe[:, 0::2] = torch.sin(position * div_term)
pe[:, 1::2] = torch.cos(position * div_term)
pe = pe.unsqueeze(0)
self.register_buffer(‘pe’, pe)
def forward(self, x):
x = x + self.pe[:, :x.size(1), :]
return self.dropout(x)
動作確認
# 使用例
d_model = 512
max_len = 5000
pos_encoding = PositionalEncoding(d_model, max_len)
# 入力データを作成
batch_size = 2
seq_len = 10
x = torch.randn(batch_size, seq_len, d_model)
# 位置エンコーディングを加算
output = pos_encoding(x)
print(f”入力形状: {x.shape}”)
print(f”出力形状: {output.shape}”)
print(f”PE形状: {pos_encoding.pe.shape}”)
print(f”\n位置0のPE(最初の10次元):”)
print(pos_encoding.pe[0, 0, :10].numpy().round(3))
print(f”\n位置1のPE(最初の10次元):”)
print(pos_encoding.pe[0, 1, :10].numpy().round(3))
実行結果:
入力形状: torch.Size([2, 10, 512])
出力形状: torch.Size([2, 10, 512])
PE形状: torch.Size([1, 5000, 512])
位置0のPE(最初の10次元):
[ 0. 1. 0. 1. 0. 1. 0. 1. 0. 1. ]
位置1のPE(最初の10次元):
[ 0.841 0.54 0.811 0.585 0.778 0.628 0.745 0.667 0.71 0.704]
🔲 3. Feed-Forward Network(FFN)
Transformerの各層では、Self-Attentionの後に
Feed-Forward Network(FFN) を適用します。
これは2層の全結合ネットワークです。
3-1. FFNとは何か
【Feed-Forward Networkの構造】
FFN(x) = ReLU(x × W₁ + b₁) × W₂ + b₂
【パラメータ】
d_model = 512 (入力・出力の次元)
d_ff = 2048 (中間層の次元、通常 d_model × 4)
W₁: (512, 2048) – 第1層の重み
b₁: (2048,) – 第1層のバイアス
W₂: (2048, 512) – 第2層の重み
b₂: (512,) – 第2層のバイアス
【処理の流れ】
入力: (batch, seq_len, 512)
↓
第1層(線形変換): x × W₁ + b₁
↓
中間: (batch, seq_len, 2048) ← 次元が4倍に拡大!
↓
ReLU(活性化関数): max(0, x)
↓
中間: (batch, seq_len, 2048) ← 負の値が0にクリップ
↓
第2層(線形変換): x × W₂ + b₂
↓
出力: (batch, seq_len, 512) ← 元の次元に戻る
【なぜ次元を拡大するのか?】
・中間層で2048次元に拡大
・より豊かな特徴空間で処理
・ボトルネックを避ける
・最後に512次元に戻す
3-2. “Position-wise”の意味
💡 Position-wise(位置ごと)の意味
FFNは各位置(単語)に対して独立に 同じネットワークを適用します。
単語間の情報交換はありません。
【Position-wiseの動作】
入力: “The cat sat” (3単語)
Self-Attentionの後:
[h_The, h_cat, h_sat] (各512次元)
FFNの適用:
h_The → FFN → 出力_The (独立に処理)
h_cat → FFN → 出力_cat (同じFFNを使用)
h_sat → FFN → 出力_sat (同じFFNを使用)
【ポイント】
・3つの単語に同じFFNを適用(重みを共有)
・各単語は独立に処理される
・単語間の相互作用はない(Self-Attentionで済ませている)
【Self-Attentionとの役割分担】
Self-Attention:
・「どの単語がどの単語と関連するか」を計算
・全単語を見る(グローバル)
・位置間の相互作用
FFN:
・「各単語の表現をどう変換するか」を計算
・1単語だけを見る(ローカル)
・位置ごとの独立した処理
この2つが交互に繰り返されることで、
文脈を理解しつつ、各単語の表現を豊かにする
3-3. なぜFFNが必要なのか
【FFNが必要な理由】
■ 問題: Self-Attentionはほぼ線形
Self-Attentionの計算:
1. QK^T(行列積)- 線形
2. softmax – 非線形だが、値を変換するだけ
3. × V(行列積)- 線形
→ 全体として「ほぼ線形的」な操作
→ 複雑なパターンを学習できない
■ 解決: FFNで非線形性を追加
FFNの計算:
1. x × W₁ + b₁ – 線形
2. ReLU – 非線形!(負の値を0に)
3. x × W₂ + b₂ – 線形
→ ReLUにより非線形性が追加される
→ 複雑な関数を近似できるようになる
■ Universal Approximation Theorem
「十分に広い中間層を持つ2層のニューラルネットワークは、
任意の連続関数を近似できる」
d_ff = 2048(4倍に拡大)
→ 十分な表現力を確保
→ 複雑なパターンを学習可能
■ 実験結果
FFNなし: BLEU 24.5
FFNあり: BLEU 28.4
→ 約4ポイントの向上!
→ FFNは不可欠な要素
3-4. PyTorchでの実装
class PositionwiseFeedForward(nn.Module):
“””Position-wise Feed-Forward Network”””
def __init__(self, d_model, d_ff, dropout=0.1):
“””
Args:
d_model: 入力・出力の次元(例: 512)
d_ff: 中間層の次元(例: 2048)
dropout: ドロップアウト率
“””
super(PositionwiseFeedForward, self).__init__()
# 第1層: d_model → d_ff
self.w_1 = nn.Linear(d_model, d_ff)
# 第2層: d_ff → d_model
self.w_2 = nn.Linear(d_ff, d_model)
self.dropout = nn.Dropout(dropout)
def forward(self, x):
“””
Args:
x: 入力 (batch, seq_len, d_model)
Returns:
出力 (batch, seq_len, d_model)
“””
# 第1層 + ReLU
x = self.w_1(x) # (batch, seq_len, d_ff)
x = torch.relu(x) # ReLUで非線形変換
x = self.dropout(x)
# 第2層
x = self.w_2(x) # (batch, seq_len, d_model)
x = self.dropout(x)
return x
動作確認
# 使用例
d_model = 512
d_ff = 2048
ffn = PositionwiseFeedForward(d_model, d_ff)
# 入力
batch_size = 2
seq_len = 5
x = torch.randn(batch_size, seq_len, d_model)
# FFN適用
output = ffn(x)
print(f”入力形状: {x.shape}”)
print(f”出力形状: {output.shape}”)
# パラメータ数
params = sum(p.numel() for p in ffn.parameters())
print(f”\nパラメータ数: {params:,}”)
print(f”内訳:”)
print(f” W1: {d_model} × {d_ff} = {d_model * d_ff:,}”)
print(f” b1: {d_ff:,}”)
print(f” W2: {d_ff} × {d_model} = {d_ff * d_model:,}”)
print(f” b2: {d_model}”)
実行結果:
入力形状: torch.Size([2, 5, 512])
出力形状: torch.Size([2, 5, 512])
パラメータ数: 2,099,712
内訳:
W1: 512 × 2048 = 1,048,576
b1: 2,048
W2: 2048 × 512 = 1,048,576
b2: 512
📊 パラメータ数の内訳
FFNはTransformer層のパラメータの約2/3 を占めます。
Multi-Head Attention: 約100万パラメータ
FFN: 約200万パラメータ
合計: 約300万パラメータ/層
🔗 4. 残差結合とLayer Normalization
Transformerでは、各サブ層の後に
残差結合(Residual Connection) と
Layer Normalization を適用します。
4-1. 残差結合(Residual Connection)とは
【残差結合の仕組み】
通常のネットワーク:
入力 x → サブ層 → 出力 = Sublayer(x)
残差結合あり:
入力 x → サブ層 → 加算 → 出力 = x + Sublayer(x)
└──────────────┘
スキップ接続
【図解】
x ────────────────┐
│ │
↓ │
┌─────────┐ │
│ Sublayer │ │ スキップ(バイパス)
└─────────┘ │
│ │
↓ │
Sublayer(x) │
│ │
↓ ↓
╋ ←───────────────╋ 加算
│
↓
output = x + Sublayer(x)
【具体例】
入力: x = [1.0, 2.0, 3.0, 4.0]
サブ層の出力: Sublayer(x) = [0.1, -0.2, 0.3, -0.1]
残差結合後:
output = x + Sublayer(x)
= [1.0, 2.0, 3.0, 4.0] + [0.1, -0.2, 0.3, -0.1]
= [1.1, 1.8, 3.3, 3.9]
→ 元の入力 x の情報が保存されている!
4-2. なぜ残差結合が必要なのか
⚠️ 深いネットワークの問題: 勾配消失
層が深くなると、誤差逆伝播時に勾配が徐々に小さくなり、
学習が進まなくなる「勾配消失問題」が起きます。
【勾配消失問題】
6層のネットワーク(残差結合なし):
y = f₆(f₅(f₄(f₃(f₂(f₁(x))))))
誤差逆伝播での勾配:
∂L/∂x = ∂L/∂f₆ × ∂f₆/∂f₅ × ∂f₅/∂f₄ × ∂f₄/∂f₃ × ∂f₃/∂f₂ × ∂f₂/∂f₁ × ∂f₁/∂x
問題:
・6つの微分の「積」
・各微分が1未満だと、積がどんどん小さくなる
・例: 0.5 × 0.5 × 0.5 × 0.5 × 0.5 × 0.5 = 0.016
・勾配が0に近づく → 学習が進まない
【残差結合による解決】
残差結合ありの場合:
y = x + f₆(x + f₅(x + f₄(x + f₃(x + f₂(x + f₁(x))))))
勾配の計算:
∂y/∂x = 1 + ∂f₆/∂x + ∂f₅/∂x + … + ∂f₁/∂x
ポイント:
・「1」の項が常に存在!
・勾配が0になることはない
・直接的な勾配の流れが確保される
・深い層でも学習可能
【もう一つの利点: 恒等写像の学習】
最悪の場合:
Sublayer(x) = 0 を学習すれば
output = x + 0 = x
→ 入力をそのまま出力(恒等写像)
→ 学習が進まなくても悪化しない
→ 安全に層を深くできる
4-3. Layer Normalizationとは
【Layer Normalizationの計算】
LayerNorm(x) = γ × ((x – μ) / σ) + β
μ: 平均(各サンプル、各位置で計算)
σ: 標準偏差
γ: スケールパラメータ(学習可能)
β: シフトパラメータ(学習可能)
【計算例】
入力: x = [2, 4, 6, 8] (1つの位置、4次元)
ステップ1: 平均を計算
μ = (2 + 4 + 6 + 8) / 4 = 5
ステップ2: 標準偏差を計算
分散 = ((2-5)² + (4-5)² + (6-5)² + (8-5)²) / 4
= (9 + 1 + 1 + 9) / 4 = 5
σ = √5 ≈ 2.236
ステップ3: 正規化
(x – μ) / σ = [(2-5), (4-5), (6-5), (8-5)] / 2.236
= [-3, -1, 1, 3] / 2.236
= [-1.34, -0.45, 0.45, 1.34]
ステップ4: スケール・シフト(γ=1, β=0の場合)
出力 = [-1.34, -0.45, 0.45, 1.34]
【結果】
・平均が0に正規化
・標準偏差が1に正規化
・γ、βで必要に応じて調整可能
4-4. なぜLayer Normalizationを使うのか
【Batch Normalization vs Layer Normalization】
■ Batch Normalization(CNNで一般的)
・バッチ方向に正規化
・バッチサイズが小さいと不安定
・系列長が可変だと使いにくい
・推論時に学習時の統計を使用(複雑)
■ Layer Normalization(NLPで標準)
・特徴量方向に正規化
・バッチサイズに依存しない
・系列長が可変でもOK
・推論時も同じ計算(シンプル)
【NLPでLayer Normを使う理由】
理由1: 系列長が可変
・「Hello」(1単語) と「Hello, how are you」(4単語)
・系列長が異なるデータを同じバッチで処理
・Batch Normだと系列長の統計がずれる
理由2: バッチサイズの影響
・GPUメモリの制約で小さいバッチも使う
・Batch Normは小さいバッチで不安定
・Layer Normはバッチサイズ1でもOK
理由3: シンプルさ
・推論時も学習時と同じ計算
・Moving Averageを保存する必要なし
4-5. 残差結合 + Layer Normの実装
class SublayerConnection(nn.Module):
“””残差結合 + Layer Normalization”””
def __init__(self, d_model, dropout=0.1):
“””
Args:
d_model: モデルの次元数
dropout: ドロップアウト率
“””
super(SublayerConnection, self).__init__()
# Layer Normalization
self.norm = nn.LayerNorm(d_model)
# Dropout
self.dropout = nn.Dropout(dropout)
def forward(self, x, sublayer):
“””
Args:
x: 入力 (batch, seq_len, d_model)
sublayer: サブ層(関数として渡す)
Returns:
出力 (batch, seq_len, d_model)
“””
# Pre-LN方式: Layer Normを先に適用
# output = x + Dropout(Sublayer(LayerNorm(x)))
return x + self.dropout(sublayer(self.norm(x)))
動作確認
# 使用例
d_model = 512
sublayer_conn = SublayerConnection(d_model)
# ダミーのサブ層(線形変換)
sublayer = nn.Linear(d_model, d_model)
# 入力
x = torch.randn(2, 5, d_model)
# 残差結合 + Layer Norm
output = sublayer_conn(x, sublayer)
print(f”入力形状: {x.shape}”)
print(f”出力形状: {output.shape}”)
# 残差結合の効果を確認
print(f”\n入力の最初の5要素: {x[0, 0, :5].detach().numpy().round(3)}”)
print(f”出力の最初の5要素: {output[0, 0, :5].detach().numpy().round(3)}”)
print(f”→ 元の入力の情報が保存されている”)
実行結果:
入力形状: torch.Size([2, 5, 512])
出力形状: torch.Size([2, 5, 512])
入力の最初の5要素: [ 0.423 -1.203 0.872 0.156 -0.634]
出力の最初の5要素: [ 0.512 -1.089 0.923 0.198 -0.589]
→ 元の入力の情報が保存されている
🏗️ 5. Encoder層の完全実装
ここまで学んだ要素を全て組み合わせて、完全なEncoder層を実装します。
5-1. Encoder層の構造
【1つのEncoder層の構造】
入力: (batch, seq_len, d_model)
│
▼
┌─────────────────────────────────────┐
│ 1. Multi-Head Self-Attention │
│ ・各単語が他の全単語を見る │
│ ・文脈を理解 │
└────────────────┬────────────────────┘
│
▼
┌─────────────────────────────────────┐
│ 2. 残差結合 + Layer Norm │
│ ・勾配消失を防ぐ │
│ ・学習を安定化 │
└────────────────┬────────────────────┘
│
▼
┌─────────────────────────────────────┐
│ 3. Feed-Forward Network │
│ ・各位置の表現を変換 │
│ ・非線形性を追加 │
└────────────────┬────────────────────┘
│
▼
┌─────────────────────────────────────┐
│ 4. 残差結合 + Layer Norm │
│ ・再び安定化 │
└────────────────┬────────────────────┘
│
▼
出力: (batch, seq_len, d_model)
これを6回繰り返す(6層)
5-2. Encoder層の実装
まず、STEP 13で作成したMulti-Head Attentionを使用します。
import torch
import torch.nn as nn
import torch.nn.functional as F
import math
import copy
# Multi-Head Attention(STEP 13から)
class MultiHeadAttention(nn.Module):
def __init__(self, d_model, n_heads, dropout=0.1):
super(MultiHeadAttention, self).__init__()
assert d_model % n_heads == 0
self.d_model = d_model
self.n_heads = n_heads
self.d_k = d_model // n_heads
self.W_Q = nn.Linear(d_model, d_model)
self.W_K = nn.Linear(d_model, d_model)
self.W_V = nn.Linear(d_model, d_model)
self.W_O = nn.Linear(d_model, d_model)
self.dropout = nn.Dropout(dropout)
def forward(self, Q, K, V, mask=None):
batch_size = Q.size(0)
Q = self.W_Q(Q).view(batch_size, -1, self.n_heads, self.d_k).transpose(1, 2)
K = self.W_K(K).view(batch_size, -1, self.n_heads, self.d_k).transpose(1, 2)
V = self.W_V(V).view(batch_size, -1, self.n_heads, self.d_k).transpose(1, 2)
scores = torch.matmul(Q, K.transpose(-2, -1)) / math.sqrt(self.d_k)
if mask is not None:
scores = scores.masked_fill(mask == 0, -1e9)
attention_weights = F.softmax(scores, dim=-1)
attention_weights = self.dropout(attention_weights)
output = torch.matmul(attention_weights, V)
output = output.transpose(1, 2).contiguous().view(batch_size, -1, self.d_model)
output = self.W_O(output)
return output, attention_weights
Encoder層の実装
class EncoderLayer(nn.Module):
“””Transformerの1つのEncoder層”””
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(EncoderLayer, self).__init__()
# Multi-Head Self-Attention
self.self_attn = MultiHeadAttention(d_model, n_heads, dropout)
# Feed-Forward Network
self.feed_forward = PositionwiseFeedForward(d_model, d_ff, dropout)
# Layer Normalization(2つ)
self.norm1 = nn.LayerNorm(d_model)
self.norm2 = nn.LayerNorm(d_model)
# Dropout
self.dropout = nn.Dropout(dropout)
def forward(self, x, mask=None):
“””
Args:
x: 入力 (batch, seq_len, d_model)
mask: マスク(オプション)
Returns:
出力 (batch, seq_len, d_model)
“””
# 1. Multi-Head Self-Attention + 残差結合 + Layer Norm
attn_output, _ = self.self_attn(x, x, x, mask)
x = self.norm1(x + self.dropout(attn_output))
# 2. Feed-Forward + 残差結合 + Layer Norm
ff_output = self.feed_forward(x)
x = self.norm2(x + self.dropout(ff_output))
return x
5-3. 複数のEncoder層を積む
class Encoder(nn.Module):
“””N層のEncoder”””
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(Encoder, self).__init__()
# N層のEncoder層
self.layers = nn.ModuleList([
EncoderLayer(d_model, n_heads, d_ff, dropout)
for _ in range(n_layers)
])
# 最後のLayer Norm
self.norm = nn.LayerNorm(d_model)
def forward(self, x, mask=None):
“””
Args:
x: 入力 (batch, seq_len, d_model)
mask: マスク
Returns:
出力 (batch, seq_len, d_model)
“””
# 各層を順番に通す
for layer in self.layers:
x = layer(x, mask)
# 最後にLayer Norm
return self.norm(x)
5-4. 動作確認
# パラメータ設定
d_model = 512 # モデル次元
n_heads = 8 # ヘッド数
d_ff = 2048 # FFN中間層
n_layers = 6 # 層数
dropout = 0.1
# モデル作成
encoder = Encoder(d_model, n_heads, d_ff, n_layers, dropout)
pos_encoding = PositionalEncoding(d_model)
# 入力データ
batch_size = 2
seq_len = 10
x = torch.randn(batch_size, seq_len, d_model)
# 位置エンコーディングを追加
x = pos_encoding(x)
# Encoderを通す
output = encoder(x)
print(f”入力形状: {x.shape}”)
print(f”出力形状: {output.shape}”)
# パラメータ数
total_params = sum(p.numel() for p in encoder.parameters())
print(f”\n総パラメータ数: {total_params:,}”)
print(f”層あたり: {total_params // n_layers:,}”)
実行結果:
入力形状: torch.Size([2, 10, 512])
出力形状: torch.Size([2, 10, 512])
総パラメータ数: 18,897,408
層あたり: 3,149,568
💡 Encoder層のまとめ
Positional Encoding : 位置情報を加算
Multi-Head Self-Attention : 文脈を理解
残差結合 + Layer Norm : 学習を安定化
Feed-Forward Network : 非線形変換で表現を豊かに
これを6層繰り返す : 約1900万パラメータ
📝 練習問題
このステップで学んだ内容を確認しましょう。
問題1:位置情報の必要性
Self-Attentionに位置情報が必要な理由 は何ですか?
計算を高速化するため
Self-Attentionは単語の順序を考慮しないため
パラメータ数を削減するため
メモリ使用量を削減するため
解答を見る
正解:b
位置情報が必要なのはSelf-Attentionが単語の順序を考慮しない からです。
問題点:
“The cat sat on the mat” と “The mat sat on the cat”
単語の集合が同じなので、Self-Attentionでは同じ計算結果
しかし意味は全く違う!
解決策: Positional Encodingで位置情報を追加
問題2:正弦波・余弦波の利点
正弦波・余弦波による位置エンコーディングの利点 として正しくない ものはどれですか?
任意の長さの系列に対応できる
学習不要(パラメータなし)
相対位置を学習しやすい
学習可能な埋め込みより常に高精度
解答を見る
正解:d
正弦波が常に高精度 とは限りません。実験では両者ほぼ同等の性能です。
正弦波の利点(正しいもの):
a: 数式で決まるので任意の長さに対応可能 ✅
b: パラメータなしでメモリ効率が良い ✅
c: 三角関数の性質で相対位置を学習しやすい ✅
実際の使用: BERT、GPTは学習可能な埋め込みを使用していますが、性能はほぼ同等です。
問題3:FFNの役割
Position-wise Feed-Forward Network の役割として正しい ものはどれですか?
単語間の関係を学習する
各位置の表現を非線形変換で豊かにする
位置情報を埋め込む
Attention Weightを計算する
解答を見る
正解:b
FFNは各位置の表現を非線形変換で豊かにする 役割です。
FFNの特徴:
Position-wise: 各位置に独立に同じネットワークを適用
ReLUで非線形性を追加
512 → 2048 → 512(次元を拡大・縮小)
役割分担:
Self-Attention: 位置間の関係を学習(a、dの役割)
FFN: 各位置の表現を変換(b)
Positional Encoding: 位置情報を埋め込む(c)
問題4:残差結合
残差結合(Residual Connection) の主な利点は何ですか?
パラメータ数を削減する
計算を高速化する
勾配消失を防ぎ、深い層でも学習可能にする
メモリ使用量を削減する
解答を見る
正解:c
残差結合の主な利点は勾配消失を防ぎ、深い層でも学習可能にする ことです。
残差結合の仕組み:
output = x + Sublayer(x)
元の入力xを加算(スキップ接続)
利点:
勾配が直接流れる経路ができる
∂output/∂x = 1 + ∂Sublayer/∂x
「1」の項が常にある → 勾配消失しない
6層以上の深いネットワークでも学習可能
📝 STEP 14 のまとめ
✅ このステップで学んだこと
位置情報の必要性: Self-Attentionは順序を考慮しない
Positional Encoding: 正弦波・余弦波で位置を埋め込む
Feed-Forward Network: 位置ごとの非線形変換
残差結合: 勾配消失を防ぐスキップ接続
Layer Normalization: 学習の安定化
完全なEncoder層: 全ての要素を統合
💡 Encoder層の全体像
入力 → Positional Encoding
→ Multi-Head Self-Attention → 残差 + Layer Norm
→ Feed-Forward Network → 残差 + Layer Norm
→ 出力
これを6層繰り返す → 約1900万パラメータ
🎯 次のステップの準備
次のSTEP 15では、Transformer全体の実装 を完成させます!
学ぶ内容:
Decoder層の実装
Masked Self-Attention(未来を見ない)
Cross-Attention(EncoderとDecoderの接続)
完全なTransformerモデル