Chapter 3: Transformer を組み立てる残りの部品
前章では、Transformer の心臓部 Attention(注意機構) を、概念から実装まで一気通貫でつかみました。ただし前章はずっと「単語はすでにベクトルになっている」ことを前提にしていました。
でも、よく考えると Attention だけでは Transformer は動きません。たとえば——
- そもそも テキスト(文字列)をどうやってベクトルにするの?
- Attention は全単語を一気に見るけど、「何番目の単語か」という順番の情報はどこから来るの?
- Attention の出力を、もっと深く加工する部品は要らないの?
- 層をたくさん積んだとき、学習がちゃんと進むように支える仕組みは?
この章では、これらの疑問に答える Attention 以外の5つの部品を、初学者でも分かるように図解しながら厚めに解説します。
- トークン埋め込み(埋め込みベクトル)… テキストを意味のあるベクトルに変える入り口
- 位置エンコーディング … 「何番目か」という順番の情報を足す
- フィードフォワード層(FFN)… 各単語を個別に深く加工する
- スキップ接続(残差接続)… 層を深く積んでも学習が進むようにする近道
- レイヤー正規化 … 値のスケールを整えて学習を安定させる
そして最後に、Attention とこの5部品を全部組み合わせて、1つの Transformer ブロックを完成させます。
5つの部品は、どれも「Transformer ブロックという1つの装置の、それぞれの部品」です。バラバラに覚えるのではなく、「この部品はブロックのどこに、何のために付くのか」を意識すると一気に頭に入ります。各節の最後に、必ず全体像のどこに効くのかを示します。
0. 全体像:5つの部品はブロックのどこにいる?
細かい話に入る前に、まず完成形(地図)を見ておきましょう。下の図が、これから組み立てる Transformer ブロック1個の中身です。前章の Attention(マルチヘッドアテンション)は中央にいて、この章で学ぶ5部品がそれを取り囲んでいます。
データは上から下へ流れます。テキスト → ①埋め込みでベクトル化 → ②位置情報を足す → ブロックの中で(Attention → ④⑤で整える → ③FFN → ④⑤で整える)→ 出力、という流れです。この図を「地図」として、これから各部品を1つずつ訪ねていきましょう。
Transformer は、上の点線で囲ったブロックを何個も積み重ねてできています(GPT 系では数十段)。1段目の出力が2段目の入力になり……とくり返すことで、単純な部品の組み合わせから深い表現力が生まれます。①②(埋め込み・位置)は最初に1回だけ、③〜⑤を含むブロックは N 回くり返す、という構造です。
1. トークン埋め込み(埋め込みベクトル)
1.1 直感:単語を「意味を持つ数のリスト」に変える
コンピュータは「猫」という文字そのものを計算できません。足し算も内積もできるのは数だけです。そこで、まず各単語にベクトル(数のリスト)を割り当てます。これが トークン埋め込み(token embedding) です。
では、どんなベクトルを割り当てればよいでしょう? 一番素朴なのは、単語に番号を振って、その番号の場所だけ 1 にする方法です(ワンホット表現)。たとえば語彙が「猫・犬・走る・寝る」の4語なら、
- 猫 →
- 犬 →
ですが、これには2つの大問題があります。
- 意味が入っていない。 猫と犬は似た生き物なのに、ベクトルとしては全くの無関係(内積を取るとゼロ)。
- 巨大すぎる。 実際の語彙は数万語なので、1単語が数万次元のスカスカなベクトルになってしまう。
そこで Transformer では、各単語に 「短くて、中身がぎっしり詰まった(密な)、意味を反映したベクトル」 を割り当てます。たとえば次元を4にして、
- 猫 →
- 犬 → (猫と近い!)
- 走る → (動詞なので離れている)
こうすると「猫と犬は近い」という意味が、ベクトルの近さ(前章の内積・コサイン類似度!)として表現できます。
ここでは分かりやすさのために「単語」と言っていますが、正確には トークン(token) という単位を使います。トークンは「単語より少し細かい、文字列の断片」で、たとえば "playing" が "play" + "ing" のように分割されることもあります。テキストをトークンに分ける仕組み(トークナイザー)は今後の章で扱います。この章では「トークン ≒ 単語」と思って大丈夫です。
1.2 定義:埋め込み行列から「行を引く」だけ
埋め込みの正体は、実はとてもシンプルです。1枚の大きな表(行列)から、単語の番号で行を1本引いてくるだけです。
語彙のサイズを (語の総数)、埋め込みの次元を とすると、埋め込み行列 は
という大きさの行列です。 番目のトークン(トークン ID が )の埋め込みベクトルは、この の 第 行そのものです。
つまり「埋め込み」という処理は、難しい計算ではなく表引き(ルックアップ)です。そして表 の中身(各単語のベクトル)は、最初はランダムな値で、学習を通じて少しずつ意味のある値に調整されていきます。ここがワンホットとの決定的な違いで、埋め込みは「学習で賢くなる」のです。
「1.1 で出てきたワンホットと、この埋め込みは別物なの?」と思うかもしれません。答えは 「計算のうえではつながっているが、意味を持つのは埋め込み側」 です。
実は、ワンホット表現と埋め込み行列 の掛け算は、ルックアップ(行引き)とまったく同じ結果になります。たとえば「猫」(ID = 3)のワンホット に を掛けると、他の行は が掛かって消え、 の4行目だけが残ります。
ここでワンホットの長さは語彙サイズ に等しく、語彙が4語なら長さ4、数万語なら数万次元のスカスカなベクトルです。埋め込み行列 は の大きさなので、掛け算は次元がぴったりつながります。
埋め込みが「短くてぎっしり」なのは、この 次元から 次元への圧縮でもあるのです( は数万でも、 は数百〜数千ほど)。
つまり「埋め込みはワンホットから作られる」とも言えます。ただし大事なのは、意味のある数値を持っているのは の側だということ。ワンホットは と しか持たず、「 のどの行を選ぶか」を指すスイッチにすぎません。猫と犬が似たベクトルになるのも、学習で賢くなるのも、すべて の働きです。だから実装では掛け算を省いて、最初から行を引きます(→ 1.4 節)。
1.3 具体例:実際に表を引いてみる
語彙4語、 の小さな例で考えます。埋め込み行列を
とすると、「猫」(ID = 3)の埋め込みは第3行(0始まり)なので 、「犬」(ID = 2)は です。猫と犬の行が似ていて、走る・寝るの行とは離れている——これが「意味をベクトルで表す」ということです。
1.4 コード:NumPy と PyTorch
NumPy なら、ルックアップは単なる行のインデックス参照です。
import numpy as np
# 埋め込み行列 E(語彙4語 × d_model=4)。本来は学習で決まるが、ここでは固定値。
E = np.array([
[-0.6, 0.9, 0.1, 0.5], # 0: 走る
[-0.5, 0.8, 0.0, 0.4], # 1: 寝る
[ 0.7, 0.3, -0.4, 0.2], # 2: 犬
[ 0.8, 0.2, -0.5, 0.1], # 3: 猫
])
# 文「猫 が 走る」をトークン ID 列にしたもの(「が」は省略して 2語と仮定)
token_ids = np.array([3, 0]) # 猫=3, 走る=0
embedded = E[token_ids] # ← これだけ! 各 ID の行をまとめて引く
print(embedded)
# [[ 0.8 0.2 -0.5 0.1] ← 猫
# [-0.6 0.9 0.1 0.5]] ← 走る
PyTorch には専用の層 nn.Embedding があります。中身は同じ「行を引く表」ですが、学習で表の中身が更新される点が大事です。
import torch
import torch.nn as nn
vocab_size, d_model = 4, 4
embedding = nn.Embedding(vocab_size, d_model) # V×d_model の表を内部に持つ(最初はランダム)
token_ids = torch.tensor([3, 0]) # 猫=3, 走る=0
embedded = embedding(token_ids) # 各 ID の行を引く
print(embedded.shape) # torch.Size([2, 4]) = 2トークン × 4次元
nn.Linear ではなく nn.Embedding?ワンホットベクトルに重み行列を掛け算すると、結果は「掛けた行列の1行を取り出す」のと同じになります(他の成分は 0 が掛かって消えるため)。つまり埋め込みはワンホット × 重み行列と数学的には等価です。でも、わざわざ巨大なワンホットを作って掛け算するのは無駄なので、nn.Embedding は「掛け算をスキップして、最初から行を引く」効率的なショートカットになっています。
1.5 つながり:これが Transformer の入口
トークン埋め込みは、全体像の図の ① にあたります。テキストをトークン ID の列に変え、各 ID を埋め込み行列で引くことで、「トークン数 × 」の行列が手に入ります。これがモデルへの入力で、前章で「すでにベクトルになっている」と仮定していたものの正体です。
ただし、この時点ではまだ「順番」の情報が入っていません。「猫 が 走る」と「走る が 猫」を区別できないのです。それを解決するのが、次の位置エンコーディングです。
2. 位置エンコーディング
2.1 直感:Attention は「順番」を見ていない
前章で見たように、Attention は全単語ペアの関連度を一気に計算します。並列に計算できるのが Transformer の強みでした。でも、これには思わぬ落とし穴があります。Attention の計算には「単語の順番」がどこにも入っていないのです。
どういうことか、極端な例で確かめましょう。「猫 が 犬 を 追う」と「犬 が 猫 を 追う」は、まったく意味が逆です。でも Attention にとっては、入力は「猫・が・犬・を・追う という単語の集まり」でしかなく、並び順をシャッフルしても結果が変わりません(単語ベクトルの集合としては同じだから)。これでは「誰が誰を追うのか」が分からず、言語モデルとして致命的です。
RNN(前章 2.1)は単語を1つずつ順番に読むので、順番が自然に入っていました。Transformer はその「順番に読む」のをやめて並列化した代わりに、順番の情報を失ってしまったのです。だから「順番を表す情報」を、人間が明示的に与えてあげる必要があります。これが位置エンコーディングの役目です。
2.2 アイデア:埋め込みに「位置を表すベクトル」を足す
解決策はシンプルです。各単語の埋め込みベクトルに、「その単語が何番目にあるか」を表すベクトルを足し込みます。これを 位置エンコーディング(positional encoding, PE) と呼びます。
足し算で混ぜるだけ、というのがポイントです。こうすると、同じ「猫」でも 0 番目にある猫と 2 番目にある猫で少し違うベクトルになり、Attention が「順番」を区別できるようになります。
2.3 定義:sin と cos の波で位置を表す
では、 の中身——「位置を表すベクトル」はどう作るのでしょう? 元の論文では、異なる波長の sin と cos の波を使います。位置を 、ベクトルの次元番号を 、ベクトル全体の長さを とすると、
記号を1つずつ渡します。 は「何番目の単語か」(0, 1, 2, …)。 と は「ベクトルの何番目の成分か」を偶数・奇数に分けたもので、偶数番目には sin、奇数番目には cosを入れます。分母の は、成分番号 が大きいほど大きくなる数で、これが波の波長(ゆっくり変化するか、速く変化するか)を決めます。
ここで使う sin(サイン)と cos(コサイン)は、Chapter 1 で学んだ三角関数そのものです。値が から の間を波打つ関数、というイメージで十分です。難しい公式は要りません。
2.4 なぜ sin・cos なのか:波長の違う「ものさし」を並べる
「位置を表すだけなら をそのまま入れればいいのでは?」と思うかもしれません。でも、それだと長い文( が何百にもなる)で値が際限なく大きくなり、学習が不安定になります。sin・cos は必ず 〜 に収まるので、その心配がありません。
そして本質は、「波長の違う波を何本も束ねる」ところにあります。下の図のように、ベクトルの先頭の成分は速く変化する波(短い波長)、後ろの成分ほどゆっくり変化する波(長い波長)になっています。
これは時計の 秒針・分針・時針にそっくりです。秒針(速い波)だけでは1分後に同じ位置に戻ってしまいますが、分針・時針(遅い波)と組み合わせれば、どの瞬間も一意に表せます。同じように、波長の違う sin・cos を束ねると、どの位置 も世界に1つだけのパターンになり、しかも「2つ隣」「5つ先」といった相対的な位置関係も波のズレとして自然に表現できます。
「束ねる」をもう少し具体的に見てみましょう。ある位置 に注目し、波長の違う各波の「その位置での高さ」を1個ずつ読み取って縦に積むと、その位置だけのベクトル(=位置の「番地」)ができます。これが「束ねる」の正体です。
たとえ速い波の値がたまたま近い2つの位置でも、遅い波の値が違うので、束ねたベクトル全体としてはどの位置も必ず別物になります。これが「組み合わせれば一意に表せる」ということです。 と のビットを組み合わせて数を一意に表す2進数の、なめらかな(sin・cos 版の)番地表記だと思うと分かりやすいでしょう。
この は理論的に導かれた値ではなく、論文の著者が選んだハイパーパラメータ(手で決める設定値)です。役割は「いちばん速い波と、いちばん遅い波の波長の比」を決めること。分母 に成分番号 を端から端まで入れてみると、
- 先頭の成分()… 分母は 、波長はいちばん短い
- 最後の成分()… 分母は 、波長はいちばん長い
となり、波長が から約 まで、なめらかに広がります。なぜこんなに長い波まで用意するかというと、いちばん遅い波の波長が、扱う文の長さよりずっと長くないと、波が1周してしまって「位置 」と「位置 波長」が同じパターンになり区別できなくなるから(秒針が1分で一周してしまうのと同じ)。数千トークンの文でも1周しないよう、余裕を持った大きめの値として が選ばれています。
別の値( や など)でも動きますし、論文も「これが最適」とは述べていません。最近の長文脈モデル(RoPE など)では、この 相当の値をあえて大きくして、より長い文を扱えるようにするチューニングも行われます。「波長の範囲を決めるツマミ」だと分かっていれば、こうした発展も腑に落ちます。
2.5 具体例と実装
で、位置 の位置エンコーディングを計算してみます。NumPy で書くと次のとおりです。
import numpy as np
def positional_encoding(seq_len, d_model):
pos = np.arange(seq_len)[:, None] # 位置 0,1,2,... を縦に並べる(列ベクトル)
i = np.arange(d_model)[None, :] # 次元番号 0,1,2,...(横に並べる)
# 分母 10000^(2i/d_model)。偶数・奇数で同じ波長を共有するため i//2 を使う
denom = np.power(10000, (2 * (i // 2)) / d_model)
angle = pos / denom # 角度(pos ÷ 波長)
pe = np.zeros((seq_len, d_model))
pe[:, 0::2] = np.sin(angle[:, 0::2]) # 偶数番目の成分 → sin
pe[:, 1::2] = np.cos(angle[:, 1::2]) # 奇数番目の成分 → cos
return pe
print(np.round(positional_encoding(3, 4), 3))
# [[ 0. 1. 0. 1. ] ← pos=0: sin0=0, cos0=1
# [ 0.841 0.54 0.01 1. ] ← pos=1
# [ 0.909 -0.416 0.02 1. ]] ← pos=2
pos = 0 の行が [0, 1, 0, 1] になるのは、、 だからです。位置が進むにつれて各成分が波打って変化し、行(=位置)ごとに違うパターンになっているのが分かります。
数字で「速い波・遅い波」を読み解く
もう少し位置を増やして、 から までの値を並べてみます()。 では、次元 0・1 が「速い波ペア」(分母 )、次元 2・3 が「遅い波ペア」(分母 )になります。
| 位置 | 次元0(速い sin) | 次元1(速い cos) | 次元2(遅い sin) | 次元3(遅い cos) |
|---|---|---|---|---|
| 0 | 0.000 | 1.000 | 0.000 | 1.000 |
| 1 | 0.841 | 0.540 | 0.010 | 1.000 |
| 2 | 0.909 | −0.416 | 0.020 | 1.000 |
| 3 | 0.141 | −0.990 | 0.030 | 1.000 |
| 4 | −0.757 | −0.654 | 0.040 | 0.999 |
この表が、まさに前の図の「束ねたベクトル」を数字にしたものです。2つのペアの動きを比べると、役割の違いがハッキリ見えます。
- 速い波ペア(次元 0・1)は1歩ごとに大きく動く。 と激しく上下します。波長が なので、約6歩で1周してしまうほど速い。だから隣り合う位置を細かく区別するのが得意です(裏を返すと、これだけだと約6歩で値がかぶる)。
- 遅い波ペア(次元 2・3)はほとんど動かない。 と、ジリジリ増えるだけ。波長が もあるので、数百歩かけてゆっくり一周します。だから遠く離れた位置を区別するのに効きます。
そして大事なのは、どの行(位置)も、4つの数の組み合わせが完全に別物だということ。速い波の値がたまたま近い位置が出てきても、遅い波の値がズレているので、ベクトル全体としては必ず一意になります。これが「速い波〜遅い波を束ねて、各位置に世界で1つの番地を振る」ということの、数字での正体です。
ここでは見やすさのため (波のペアは2組)にしていますが、実際のモデルでは は数百〜数千あり、波のペアもその半分だけ並びます。速い波から遅い波まで何十・何百種類もの「ものさし」が用意されるので、何千トークンの長い文でも、各位置にしっかり固有の番地を振れるわけです。
実際にモデルへ入れるときは、この位置エンコーディングを埋め込みに足すだけです。
# 埋め込み(2.5節までの E[token_ids])と同じ形にして足す
embedded = np.array([[ 0.8, 0.2, -0.5, 0.1], # 猫(0番目)
[-0.6, 0.9, 0.1, 0.5]]) # 走る(1番目)
pe = positional_encoding(seq_len=2, d_model=4)
model_input = embedded + pe # ← 意味 + 位置
PyTorch でも考え方は同じで、計算した位置エンコーディングを入力に加算します(実装では register_buffer で固定テーブルとして持つのが定番です)。
ここで紹介した sin・cos の式は固定(学習しない)方式です。これとは別に、位置ごとのベクトルを埋め込みと同じように学習で獲得する方式(learned positional embedding)もあり、GPT 系など多くのモデルで使われています。さらに新しいモデルでは、回転を使う RoPE など発展形も登場しています。いずれも「順番の情報を足す」という目的は同じです。
2.6 つながり:これでブロックに入る準備が整う
位置エンコーディングは全体像の ② です。①の埋め込み(意味)に②(位置)を足すことで、「意味」と「順番」の両方を持った入力ベクトルが完成し、ようやく Transformer ブロックの中(Attention)に送り込めます。
ここからは「ブロックの中身」の話に移ります。Attention は前章で学んだので、次はその出力を加工する フィードフォワード層です。
3. フィードフォワード層(FFN)
3.1 直感:Attention が「混ぜる」役、FFN が「考える」役
Attention の役割をひとことで言うと、「単語どうしを見渡して、関連する情報を混ぜ合わせる」ことでした。出力は Value の加重和——つまり、ほかの単語のベクトルを足し合わせたものです。
でも、よく考えると Attention の中心は足し算(加重和)です。足し算だけをいくら重ねても、表現できる関係には限界があります(数学的には「線形」の範囲を超えられません)。そこで、混ぜ合わせたあとの各単語ベクトルを、1つずつ取り出して、もっと複雑に加工する部品が必要になります。それが フィードフォワード層(Feed-Forward Network, FFN) です。
役割分担をイメージで言うと、こうです。
- Attention … 教室で「周りの人の意見を聞いて回る」(情報を集めて混ぜる)
- FFN … 集めた意見を持ち帰って「自分の頭の中で1人でじっくり考える」(各単語を個別に深く加工する)
ここで大事なのは、FFN は 各単語(各位置)に対して、まったく同じ処理を独立に適用するという点です。0番目の単語も、5番目の単語も、同じ FFN を通ります。お隣の単語を覗き見ることはしません(混ぜるのは Attention の仕事だから)。
3.2 定義:広げて → 非線形 → 戻す
FFN の中身は、実はとてもシンプルな2層のニューラルネットです。式で書くと、
です。順番に見ていきましょう。 は1つの単語のベクトル(長さ )。
- 広げる: で、いったん大きな次元 に引き伸ばす(典型的には )。
- 非線形をかける: は ReLU という関数で、「負の値を 0 にする」だけのシンプルな操作。これが「非線形性」を生み、足し算だけでは作れない複雑な変換を可能にします。
- 戻す: で元の次元 に縮める。
は重み行列、 はバイアス(下駄ばき)で、すべて学習で決まります。
は ReLU(Rectified Linear Unit) と呼ばれ、「入力が負なら 0、正ならそのまま通す」という関数です。グラフにすると、原点で折れ曲がった「レ」の字の形になります。この折れ曲がりこそが非線形性の源で、これがあるおかげでネットワークは曲線的な複雑な関係を学べます。最近の LLM では、ReLU を滑らかにした GELU という関数がよく使われますが、「広げる → 非線形 → 戻す」という骨格は同じです。
なぜこれで賢くなるのか:質問して、選別して、組み合わせ直す
「広げて → 非線形 → 戻す」が何をしているのかを、もう一段かみくだきます。キーワードは「質問と選別」です。
① 広げる = 質問をたくさん並べる。 で得られる中間ベクトルの各成分 は、実は
という内積です。内積は「似ているほど大きくなる」量でした(Chapter 1)。つまり の各列は「この単語、〇〇っぽい?」という質問ベクトルで、 はその質問への回答(似ている度合い)です。「動物っぽい?」「動作を表す?」「否定文の中にいる?」——そんな質問係が 人並んでいて(質問の中身は学習が勝手に決めます)、一斉に をチェックする。 から に広げるのは、質問の数を増やして多角的に調べるためです。
② ReLU =「いいえ」を捨てる選別。 回答が負(=似ていない・当てはまらない)の質問を に切り捨てます。すると入力によって「生き残る質問」が変わる。動物の話なら質問1が生き残り、動作の話なら質問4が生き残る——つまり、入力の内容に応じて実質的に違う変換が適用されるのです。
③ 戻す =「はい」の回答を組み合わせる。 は、生き残った回答を「じゃあ出力のこの方向を強めよう」と合成して、 次元の新しい表現に翻訳し直します。
では、途中の ReLU を抜いたらどうなるでしょう? 行列積には結合法則があるので、
と先に を1枚の行列 にまとめられてしまい、広げた意味が完全に消えます。線形変換をいくら重ねても1枚の線形変換と同じ——直線をいくら足しても直線のまま、ということです。ReLU という「選別」が挟まって初めて、この式変形が成り立たなくなり、入力ごとに違う回路が動く表現力が生まれます。「広げて → 非線形 → 戻す」の3点セットは、どれか1つ欠けても機能しないのです。
Attention が「他の単語から情報を集める」係だとすると、FFN は「集めた情報を自分の知識と照らして解釈し直す」係です。実際、近年の研究では、「東京 → 日本の首都」のような事実知識の多くが FFN の重み( の質問と の合成ルール)に書き込まれていることが示唆されています。パラメータ数でも FFN は Transformer 全体の約3分の2を占める、堂々たる主役級の部品です。
3.3 具体例:手で1ステップ追う
、 の小さな例で、1単語ぶんを計算してみます。入力 、
とすると、。
ここで ReLU(負を 0 に)を通すと 。負だった2つの成分が消えました。これが「非線形に情報を選別する」イメージです。このあと で2次元に戻すと、加工済みの新しいベクトルが得られます。
3.4 コード:NumPy と PyTorch
import numpy as np
def relu(z):
return np.maximum(0, z)
def ffn(x, W1, b1, W2, b2):
h = relu(x @ W1 + b1) # ① 広げて ② ReLU で非線形
return h @ W2 + b2 # ③ 元の次元に戻す
x = np.array([[1.0, -2.0]]) # 1トークン(d_model=2)
W1 = np.array([[1, 0, -1, 2],
[0, 1, 1, -1]], dtype=float) # d_model=2 → d_ff=4
b1 = np.zeros(4)
W2 = np.random.randn(4, 2) * 0.1 # d_ff=4 → d_model=2
b2 = np.zeros(2)
print(ffn(x, W1, b1, W2, b2).shape) # (1, 2) ← 入力と同じ d_model
PyTorch では nn.Linear を2つ重ねるだけです。
import torch
import torch.nn as nn
class FeedForward(nn.Module):
def __init__(self, d_model, d_ff):
super().__init__()
self.fc1 = nn.Linear(d_model, d_ff) # 広げる
self.fc2 = nn.Linear(d_ff, d_model) # 戻す
self.act = nn.ReLU() # 非線形(実際は nn.GELU() も多い)
def forward(self, x):
return self.fc2(self.act(self.fc1(x)))
ff = FeedForward(d_model=4, d_ff=16)
x = torch.randn(2, 3, 4) # バッチ2 × 3トークン × 4次元
print(ff(x).shape) # torch.Size([2, 3, 4]) 形は変わらない
入力と出力で形(次元)が変わらないことに注目してください。だからブロックの中にそのまま差し込めます。
3.5 つながり:ブロックを「賢く」する非線形パート
FFN は全体像の ③ で、Attention のすぐ後ろに置かれます。「Attention で混ぜる → FFN で深く加工する」のワンセットが、Transformer ブロックの中核的な計算です。
ここまでで「混ぜる(Attention)」「加工する(FFN)」という2つの主処理がそろいました。ところが、こうした層を何十段も積み重ねると、学習がうまく進まなくなるという別の問題が出てきます。それを支えるのが、残る2部品——スキップ接続とレイヤー正規化です。
4. スキップ接続(残差接続)
4.1 直感:層を「飛び越える近道」を作る
Transformer は強くなるために、ブロックを何十段も積み重ねます。ところが、層を深くするほど学習が難しくなる、という根深い問題があります。理由は 勾配消失です。
学習では、出力の誤差を逆向きにたどって各層の重みを調整します(この「たどる量」を勾配と呼びます)。層が深いと、この勾配が入口に届くまでに何度も掛け算され、だんだん小さくなってほぼ 0 になってしまうことがあります。勾配が 0 だと「どう直せばいいか」の信号が届かず、入口に近い層が学習できません。
これを解決するのが スキップ接続(skip connection)、別名 残差接続(residual connection) です。アイデアは拍子抜けするほど単純で、層の入力 を、その層の出力にそのまま足すだけです。
ここで は層の処理(Attention や FFN)です。 がそのまま出力に現れる「近道(バイパス)」ができるのがポイントです。
4.2 なぜこれが効くのか:2つのうれしいこと
スキップ接続が効く理由は、大きく2つあります。
① 勾配の「高速道路」ができる。 出力に がそのまま含まれるので、逆向きにたどる勾配も、層 を通らずに近道をまっすぐ入口まで届けるルートを持ちます。途中で 0 に薄まらないので、深いネットでも入口の層まで学習信号が届きます。
② 「差分」だけ学べばよくなる。 式を見ると、層 は「出力そのもの」ではなく「入力からの変化分(残差)」 を学べばよいことになります。もし「この層では何もしないのが最適」なら、(つまり出力 )にすればよく、これは簡単に学習できます。何もしないことすら難しかった深いネットに、「とりあえず素通り」という安全なスタート地点を与えるイメージです。
は「出力から入力を引いた残り」なので、残差(residual) と呼ばれます。層に「ゼロから正解を作れ」と要求する代わりに、「いまの に、どんな修正を加えるか」だけを学ばせる——これが残差接続の発想です。画像認識の ResNet で大成功し、Transformer にも受け継がれました。
4.3 具体例とコード
数値はとても簡単です。入力 、層の出力 なら、スキップ接続つきの出力は単に足すだけ:
import numpy as np
x = np.array([1.0, 2.0])
Fx = np.array([0.1, -0.3]) # 層(Attention や FFN)の出力
y = x + Fx # ← スキップ接続はこの足し算だけ
print(y) # [1.1 1.7]
PyTorch でも、層を呼んだ結果に入力を足すだけです。
import torch.nn as nn
class ResidualBlock(nn.Module):
def __init__(self, sublayer):
super().__init__()
self.sublayer = sublayer # Attention や FFN を入れる
def forward(self, x):
return x + self.sublayer(x) # y = x + F(x)
入力と出力の次元が同じでないと足せないことに注意してください。FFN(3節)が「入力と同じ で出力する」ように作られていたのは、まさにこのスキップ接続で足し合わせるためでもあります。
4.4 つながり:ブロックを深く積むための土台
スキップ接続は全体像の ④ で、Attention と FFN それぞれを「近道つき」で包みます。つまり実際のブロックでは、、 のように使われます。これがあるおかげで、Transformer は何十段も積み重ねても学習できるのです。
ただし、足し算をくり返すと値がどんどん大きくなったり、層によってスケールがバラバラになったりします。その暴れを抑える最後の部品が、レイヤー正規化です。
5. レイヤー正規化
5.1 直感:ベクトルの「目盛り」をそろえる
ここまでの部品を通ると、ベクトルの値は層ごとにバラバラなスケールになります。ある層では のように大きく暴れ、別の層では のように小さくしぼむ——こうしたムラがあると、学習がとても不安定になります(値が大きすぎて発散したり、小さすぎて進まなかったり)。
そこで、各ベクトルを通すたびに 「平均 0・ばらつき 1」に整え直すのが レイヤー正規化(Layer Normalization, LayerNorm) です。身長と体重のように単位がバラバラな数値を「偏差値」に直して比べやすくするのと、まったく同じ発想です。値の位置(平均)と広がり(ばらつき)の両方をそろえることで、どの層でも計算が安定した範囲で進むようになります。
5.2 定義:平均を引いて、ばらつきで割る
1本のベクトル (長さ )に対して、レイヤー正規化は次のように計算します。まず、そのベクトルの中の平均 とばらつき(分散 )を求めます。
そして、各成分から平均を引き、ばらつきの平方根で割って整えます。
記号を渡します。 の部分が正規化の本体で、「平均を 0 に、ばらつきを 1 に」する操作です。(イプシロン)は くらいの極小の数で、ばらつきが 0 のときにゼロ割りを防ぐためのお守りです。 は成分ごとの掛け算を表します。
最後の (ガンマ)と (ベータ)は学習で決まるパラメータで、「整えたあと、もう一度どれくらい伸縮・移動させるか」を調整します。これにより「正規化はするけれど、必要なら少し崩す」という柔軟さをモデルに残しています。
正規化には バッチ正規化(BatchNorm) という似た手法もありますが、平均・ばらつきを取る方向が違います。レイヤー正規化は「1つのベクトルの中(特徴の方向)」で計算するので、他のサンプルや文の長さに影響されません。だから、文ごとに長さが変わる言語データと相性がよく、Transformer ではレイヤー正規化が使われます。
5.3 具体例:手で計算してみる
を正規化してみます(、、 は無視)。
- 平均:
- 分散:、標準偏差
- 正規化:
平均が 0 になり()、左右対称のそろった値になりました。元の「2, 4, 6」がどんなスケールでも、こうして決まった範囲に収まります。
5.4 コード:NumPy と PyTorch
import numpy as np
def layer_norm(x, gamma, beta, eps=1e-5):
mu = x.mean(axis=-1, keepdims=True) # 最後の次元(特徴方向)で平均
var = x.var(axis=-1, keepdims=True) # 同じ方向でばらつき
x_hat = (x - mu) / np.sqrt(var + eps) # 平均0・ばらつき1に整える
return gamma * x_hat + beta # 学習パラメータで伸縮・移動
x = np.array([[2.0, 4.0, 6.0]])
print(np.round(layer_norm(x, gamma=1.0, beta=0.0), 2)) # [[-1.22 0. 1.22]]
PyTorch には nn.LayerNorm があり、gamma・beta を内部に持って自動で学習します。
import torch
import torch.nn as nn
ln = nn.LayerNorm(4) # d_model=4 の最後の次元を正規化
x = torch.randn(2, 3, 4) # バッチ2 × 3トークン × 4次元
print(ln(x).shape) # torch.Size([2, 3, 4]) 形は変わらない
5.5 つながり:「Add & Norm」として効く
レイヤー正規化は全体像の ⑤ で、スキップ接続(④)とペアで使われます。Attention や FFN の直後に「スキップ接続で足して(Add)、レイヤー正規化で整える(Norm)」がワンセットで入り、合わせて Add & Norm と呼ばれます。これで値が暴れず、深いネットでも安定して学習できます。
レイヤー正規化を「足し算の後」に置くか「層に入る前」に置くかで2流派あります。元論文は後(Post-LN:)でしたが、深いモデルでは学習が不安定になりやすいため、最近の LLM の多くは前(Pre-LN:)を採用しています。次節の組み立てでは、まず分かりやすい Post-LN の形で示します。
これで5つの部品がすべてそろいました。いよいよ全部を組み合わせて、1つの Transformer ブロックを完成させましょう!
6. 全部組み合わせて Transformer ブロック完成!
6.1 データの旅を最初から最後までたどる
5つの部品と前章の Attention がそろったので、ついに テキストが Transformer を通り抜ける流れを、最初から最後まで一気にたどれます。下の図が、その完成形です。
旅の流れを言葉でまとめると、こうです。
- テキストをトークンに分け、①埋め込みで各トークンをベクトルにする
- ②位置エンコーディングを足して「順番」の情報を入れる
- ここからブロックの中:マルチヘッドアテンションで単語どうしの情報を混ぜる
- その出力を ④スキップ接続で入力に足し、⑤レイヤー正規化で整える(Add & Norm)
- ③FFNで各単語を個別に深く加工する
- また ④スキップ接続 + ⑤レイヤー正規化で整える
- できあがった出力を、次の同じ形のブロックへ……これを N 回くり返す
6.2 式でまとめる
入力 (埋め込み+位置エンコーディング済み)に対して、1つの Transformer ブロックは次のように書けます(分かりやすい Post-LN の形)。
1行目が「Attention → Add & Norm」、2行目が「FFN → Add & Norm」です。 がスキップ接続、 がレイヤー正規化。この章で学んだ部品が、そのまま式の中に並んでいるのが見て取れます。
6.3 コード:1ブロックを PyTorch で組む
前章の nn.MultiheadAttention と、この章で作った FFN・スキップ接続・レイヤー正規化を組み合わせると、1つのブロックがそのまま書けます。
import torch
import torch.nn as nn
class TransformerBlock(nn.Module):
def __init__(self, d_model, n_heads, d_ff):
super().__init__()
self.attn = nn.MultiheadAttention(d_model, n_heads, batch_first=True) # 前章
self.ffn = nn.Sequential( # ③ FFN
nn.Linear(d_model, d_ff),
nn.ReLU(),
nn.Linear(d_ff, d_model),
)
self.norm1 = nn.LayerNorm(d_model) # ⑤ レイヤー正規化
self.norm2 = nn.LayerNorm(d_model)
def forward(self, x):
# Attention → ④スキップ接続で足す → ⑤正規化
attn_out, _ = self.attn(x, x, x)
x = self.norm1(x + attn_out) # x + F(x) が ④ スキップ接続
# FFN → ④スキップ接続で足す → ⑤正規化
x = self.norm2(x + self.ffn(x))
return x
block = TransformerBlock(d_model=4, n_heads=2, d_ff=16)
x = torch.randn(1, 3, 4) # バッチ1 × 3トークン × 4次元
print(block(x).shape) # torch.Size([1, 3, 4]) 入力と同じ形!
出力の形が入力とまったく同じ([1, 3, 4])なのが重要です。形が変わらないからこそ、このブロックを何個でも積み重ねられるのです。
# ブロックを N 個積めば、それがもう Transformer の本体
n_layers = 6
transformer = nn.Sequential(*[
TransformerBlock(d_model=4, n_heads=2, d_ff=16) for _ in range(n_layers)
])
print(transformer(x).shape) # torch.Size([1, 3, 4])
ここで組んだブロックを数十段積み、入口に①②(埋め込み・位置)、出口に「次の単語を予測する層」を付ければ、構造としては GPT 系の言語モデルとほぼ同じです。残るのは「大量のテキストでどう学習するか」だけ——その学習の話は、今後の章で扱います。あなたはもう、Transformer の部品も組み立て方も理解できています。
6.4 どの数字を「誰が」決めるのか
この章にはたくさんの行列や数値が登場しました。「 や の中身は、いったいどこから来るの?」と気になった人のために、登場人物を決め方で3種類に整理しておきます。
| 種類 | この2章で登場したもの | 決め方 |
|---|---|---|
| 学習で決まるパラメータ | 埋め込み行列 、FFN の 、Attention の 、LayerNorm の | 最初はランダムな値。学習中に予測の誤差を減らす方向へ、少しずつ調整され続ける |
| 人間が決めるハイパーパラメータ | 、( など)、ヘッド数 、ブロック数 、位置エンコーディングの | 設計者が手で選ぶ。モデルの「箱の大きさ・形」を決める |
| 固定の計算(学習しない) | ReLU、softmax、sin・cos の位置エンコーディング、スキップ接続の足し算 | ただの決まった関数。中に調整される数値を持たない |
ポイントは、人間が決めるのは「箱の大きさ」までだということです。たとえば FFN の を「 の行列にする」と決めるのは人間ですが、その約100万個の数字に何が入るかは100%学習まかせです。学習は「次の単語を予測 → 外れたら、誤差を逆向きにたどって(4節で登場した勾配です)全パラメータを誤差が減る方向に微調整」という単純なループを、膨大なテキストに対してひたすらくり返すだけ。その積み重ねの副産物として、 の列は「役に立つ質問」(3.2 節)に、埋め込み の行は「意味を映すベクトル」(1.2 節)に、勝手に育っていきます。役に立つ重みを持つほど予測ミスが減るので、自然とそうなるのです。
この「学習のループを実際にどう回すのか」が、まさに今後の章で扱うテーマです。
7. この章のまとめ
この章では、前章の Attention を取り囲む 5つの部品を学び、最後に全部を組み立てて Transformer ブロックを完成させました。
5つの部品
- ① トークン埋め込み:テキストを「意味を持つ密なベクトル」に変える入り口。正体は埋め込み行列から ID で行を引くルックアップで、表の中身は学習で賢くなる。
- ② 位置エンコーディング:並列計算で失われた「順番」の情報を、波長の違う sin・cos の波として埋め込みに足し込む。これで「猫が犬を追う」と「犬が猫を追う」を区別できる。
- ③ フィードフォワード層(FFN):Attention が「混ぜる」役なら、FFN は各単語を個別に「深く加工する」役。広げて → ReLU で非線形 → 戻すの2層ネットで、表現力を生む。
- ④ スキップ接続(残差接続): という近道で、勾配の高速道路を作り「差分だけ学べばいい」状態にする。深く積んでも学習できる土台。
- ⑤ レイヤー正規化:ベクトルを平均0・ばらつき1に整え、値の暴れを抑えて学習を安定させる。④とペアで Add & Norm として使う。
組み立て
- 1つの Transformer ブロックは 、。
- 入力と出力の形が同じなので、ブロックを N 段積み重ねられる。これが Transformer の本体。
- 入口に①②、出口に予測層を付け、大量のテキストで学習させれば——構造は GPT 系とほぼ同じ。
前章と本章で、Transformer のアーキテクチャ(部品と組み立て)は一通りつかめました。残る大きなピースは2つ——テキストをどうトークン(数)に分けるか(トークナイザー)と、大量のデータでどう学習させるか(学習ループ)です。これらは今後の章で扱っていきます。お疲れさまでした!🎉