Level 1概要役割と処理の流れ
何をする機能か
2 つの処理をまとめたものです。
- 残差接続(Add): サブ層(Attention や Feed-Forward)の入力を、その出力にそのまま足す
- Layer Normalization(Norm): 足した結果を、各トークンの特徴次元方向に正規化する
入力も出力も [B, S, D] で、形は変わりません。
図の中でこのブロックが何度も出てくるのは、各サブ層のあとに必ず適用されるためです。
なぜ必要か
残差接続は、深いネットワークの学習を成立させるための仕組みです。 層を重ねるほど勾配が小さくなって学習が進まなくなりますが、 足し算の経路があると勾配がそこを通って下層まで届きます。 また「サブ層は差分だけを学習すればよい」形になるため、層を増やしても性能が落ちにくくなります。
LayerNorm は、層を通るたびに値の大きさが偏るのを抑えます。 値の分布が層ごとに大きく変わると学習率の調整が難しくなりますが、 毎層そろえておけば安定します。
全体の中でいつ呼ばれるか
Encoder では「Self-Attention のあと」と「Feed-Forward のあと」の 2 回、 Decoder では「Masked Self-Attention のあと」「Cross Attention のあと」「Feed-Forward のあと」の 3 回です。 図で Encoder に 2 個、Decoder に 3 個描かれているのはこのためです。
Level 2内部処理Tensor形状・内部ステップ・設計上の意味
前段から受け取るもの
2 つの [B, S, D] を受け取ります。
residual: サブ層に入る前の値sublayer_output: サブ層の出力
図の茶色い破線が、residual を迂回させている経路です。
後段へ渡すもの
hidden_states [B, S, D]。次のサブ層、または次の層へ渡されます。
LayerNorm が正規化する方向
ここが誤解の多いところです。正規化は特徴次元 D の方向に対して行われます。
- バッチ方向(
B)でも系列方向(S)でもありません - つまり、トークン1個ぶんの
D次元ベクトルごとに、平均 0・分散 1 にそろえます - そのあと、学習可能なパラメータ (スケール)と (シフト)を掛けて足します
があるため、出力の平均が 0、分散が 1 のままとは限りません。 「値を 0〜1 に収める処理」ではない点に注意してください。
LayerNorm 前
LayerNorm 後
例文での処理
共通例文
The keys on the table are lost.
概念上のトークン列(S = 8): [The, keys, on, the, table, are, lost, .]
Self-Attention のあと、位置1の keys のベクトルには周囲の情報が混ざっています。
残差接続はここで、Attention に入る前の keys 自身の情報を足し戻します。
- The
- keys
- on
- the
- table
- are
- lost
- .
これにより、次の 2 つが両立します。
keysは「テーブルの上にある、失くした複数個のもの」という文脈情報を得る- 同時に、
keysがkeysであるという元の情報も失わない
残差接続がないと、層を通るたびに元の語の情報が薄まっていきます。
Post-Norm と Pre-Norm
適用する順序に 2 つの流儀があり、混同しやすい重要な違いです。
| Post-Norm(原典 Transformer) | Pre-Norm(近年の主流) | |
|---|---|---|
| 式 | ||
| 正規化の位置 | サブ層のあと(残差を足したあと) | サブ層の前(残差は正規化しない) |
| 学習の安定性 | 深くすると不安定。Warmup が必須 | 深くしても安定しやすい |
| 採用例 | 原典 Transformer、BERT | GPT-2 以降、Llama など |
このサイトの図は原典に合わせた Post-Norm です。
図では「サブ層 → Add & Norm」の順に並んでいます。
一方、現代の LLM のコードを読むと、LayerNorm がサブ層の手前に置かれていることが多いはずです。
同じ「Add & Norm」という名前でも位置が違う点に注意してください。
Pre-Norm では、残差の経路に正規化が挟まらないため、 入力から出力まで何も掛からない加算の道ができます。これが深い層での安定性につながります。
EncoderとDecoderでの違い
処理内容は同じです。呼ばれる回数だけが異なります(Encoder は 2 回、Decoder は 3 回)。
入力と出力
入力
| 項目名 | データの意味 | データ型 | Tensor形状 | 記号の意味 | 具体例 | 次の渡し先 |
|---|---|---|---|---|---|---|
residual | サブ層に入る前の値(迂回してきた経路) | float32 Tensor | [B, S, D] | B = バッチサイズ(同時に処理する文の数) / S = 入力系列長(Encoder側のトークン数) / D = モデル内部のベクトル次元(d_model) | [1, 8, 512] | 加算 |
sublayer_output | Attention や Feed-Forward の出力 | float32 Tensor | [B, S, D] | B = バッチサイズ(同時に処理する文の数) / S = 入力系列長(Encoder側のトークン数) / D = モデル内部のベクトル次元(d_model) | [1, 8, 512] | 加算 |
出力
| 項目名 | データの意味 | データ型 | Tensor形状 | 記号の意味 | 具体例 | 次の渡し先 |
|---|---|---|---|---|---|---|
hidden_states | 残差を加算し、特徴次元方向に正規化した値 | float32 Tensor | [B, S, D] | B = バッチサイズ(同時に処理する文の数) / S = 入力系列長(Encoder側のトークン数) / D = モデル内部のベクトル次元(d_model) | [1, 8, 512](形は変わらない) | 次のサブ層、または次の層 |
B- バッチサイズ(同時に処理する文の数)
S- 入力系列長(Encoder側のトークン数)
D- モデル内部のベクトル次元(d_model)
このサイトでは Tensor 形状を PyTorch の `batch_first=True` 相当、つまり `[B, S, D]`(バッチ次元が先頭)で統一しています。実装によっては `[S, B, D]` を採用するものもあります。
学習対象のパラメータは [D] と [D] の 2 本だけで、非常に軽量です。
よくある誤解
Level 3数式・Python数式・疑似実装・計算量・実装差
数式
残差接続と Post-Norm:
LayerNorm 本体は、 番目の要素について次のように計算します ( ではなく特徴次元 の中で統計を取ります)。
はゼロ除算を避けるための小さな値( 程度)です。
Pre-Norm の場合は次のようになります。
Pythonで表したインターフェース
import torch
from torch import Tensor, nn
class LayerNorm(nn.Module):
"""特徴次元 D の方向に正規化する。"""
def __init__(self, d_model: int, eps: float = 1e-5) -> None:
super().__init__()
self.gamma = nn.Parameter(torch.ones(d_model)) # [D] スケール
self.beta = nn.Parameter(torch.zeros(d_model)) # [D] シフト
self.eps = eps
def forward(self, x: Tensor) -> Tensor:
"""
Args:
x: [B, S, D]
Returns:
[B, S, D]。最後の次元(D)の中で平均0・分散1にそろえたあと、γ・βを適用。
"""
mean = x.mean(dim=-1, keepdim=True) # [B, S, 1]
var = x.var(dim=-1, keepdim=True, unbiased=False)# [B, S, 1]
normalized = (x - mean) / torch.sqrt(var + self.eps)
return self.gamma * normalized + self.beta # [B, S, D]
def add_and_norm(residual: Tensor, sublayer_output: Tensor, norm: LayerNorm) -> Tensor:
"""残差を加算し、Layer Normalization を適用する(原典の Post-Norm)。
Args:
residual: [B, S, D]。サブ層に入る前の値。
sublayer_output: [B, S, D]。サブ層の出力。
norm: LayerNorm 層。
Returns:
[B, S, D]
"""
return norm(residual + sublayer_output)
def pre_norm_block(x: Tensor, sublayer, norm: LayerNorm) -> Tensor:
"""近年主流の Pre-Norm 版。正規化してからサブ層へ入れ、残差は素通しする。"""
return x + sublayer(norm(x))
# 最小使用例
B, S, D = 1, 8, 512
norm = LayerNorm(d_model=D)
residual = torch.randn(B, S, D) # Attention に入る前の値
sublayer_output = torch.randn(B, S, D) # Attention の出力
out = add_and_norm(residual, sublayer_output, norm)
print(out.shape) # torch.Size([1, 8, 512]) == [B, S, D]
# 正規化は D 方向。位置ごとに平均がほぼ0(γ=1, β=0 の初期状態の場合)
print(out[0, 0].mean().abs() < 1e-5) # tensor(True)この例で単純化している点
- Dropout(原典ではサブ層出力に適用してから加算)を省略しています。
- nn.LayerNorm を使わず、計算内容がわかる形で書いています。
- Pre-Norm 版は下に別関数として示しています。
型と Tensor 形状の見え方を掴むための擬似実装です。そのまま学習・推論に使えるものではありません。
計算量
です。Attention や Feed-Forward に比べると小さいですが、 要素ごとの読み書きが多くメモリ帯域を使うため、 実行時間に占める割合は計算量の比率より大きくなることがあります。
実装差と派生
- RMSNorm: 平均の減算を省き、二乗平均平方根だけで割る方式。 も持ちません。計算が軽く、Llama などで採用されています。
- Dropout の位置: 原典ではサブ層の出力に Dropout を適用してから加算します。
- Sandwich Norm / DeepNorm: 非常に深いモデルを安定させるための変種です。
- 最終層の LayerNorm: Pre-Norm 構成では、最後のブロックの出力にもう一度 LayerNorm を掛けてから出力ヘッドへ渡すのが一般的です(Post-Norm では不要)。