モジュール 09 · 第 4 回

完全な GPT を組み立てる

アテンション、フィードフォワードネットワーク、残差接続、LayerNorm を Transformer ブロックに組み立て、いくつか重ね、埋め込みと出力層を加えれば、完全な GPT になります。gpt.py を一節ずつ読み、160 万個のパラメータがどこにあるかを数え上げます。

  • 約 50 分
  • 難易度:上級
  • 検証:2026-09-15 torch 2.14、CPU、乱数シード固定

コードと実行結果は実際に動かしたときのまま載せているため、コメントと出力は中国語です。

前の二つの課で部品を作りました。アテンションで各文字が前の文字から情報を集められるようになり、マルチヘッドでいくつもの種類の情報を同時に集められるようになり、位置の埋め込みで順序がわかるようになりました。この課では部品を組み立てて、完全な GPT にします。

完全なコードは code/09-transformer/gpt.py にあり、150 行足らずです。構造は GPT-2 と同じで、ただずっと小さいだけです。この後の二つの課の学習と生成は、どちらもこれを使います。

python inspect_gpt.py

全体の構造

  词元编号
     │
  词元嵌入 + 位置嵌入
     │
  ┌──────────────────────────┐
  │  LayerNorm → 多头注意力   │ ─┐
  │        + ←────────────────── ┘ 残差
  │  LayerNorm → 前馈网络     │ ─┐
  │        + ←────────────────── ┘ 残差
  └──────────────────────────┘
     │   (这样的块叠 4 层)
  LayerNorm
     │
  输出层:对词表里每个字打分

下から上へ見ていきます。各文字はまずベクトルに変わり(トークンの埋め込みと位置の埋め込み)、同じ構造の Transformer ブロックをいくつか通り、最後に出力層が次の文字を採点します。ブロックの中のアテンションはすでに書きました。ほかに新しいものが三つあります。フィードフォワードネットワーク、残差接続、LayerNorm です。

フィードフォワードネットワーク:各文字が自分で考える

アテンションは文字と文字の間で情報を伝える役目です。情報を集め終えたら、各文字はそれぞれ自分で「消化」する必要があり、それがフィードフォワードネットワークです。

self.mlp = nn.Sequential(  # 前馈网络:先放大 4 倍,过激活函数,再缩回来
    nn.Linear(cfg.n_embd, 4 * cfg.n_embd),
    nn.GELU(),
    nn.Linear(4 * cfg.n_embd, cfg.n_embd),
    nn.Dropout(cfg.dropout),
)

これはモジュール 08 で見た最も普通の 2 層のネットワークそのもので、128 次元を 512 次元に広げ、活性化関数を通し、また 128 次元に戻します。GELU は ReLU によく似た活性化関数で、ただ 0 の付近がなめらかな曲線になっています。GPT-2 が使っているのがこれです。

フィードフォワードネットワークは位置ごとに別々に計算し、位置どうしは互いに影響しません。ですから Transformer ブロックの分担はこうです。アテンションが「交流」を、フィードフォワードネットワークが「思考」を担当します。後で見るとおり、モデルのパラメータの大部分は実はフィードフォワードネットワークにあります。

残差接続:元のものを少しだけ変える

def forward(self, x, cache=None):
    x = x + self.attn(self.ln1(x), cache)  # 残差连接:在原来的基础上加一点修改
    x = x + self.mlp(self.ln2(x))
    return x

ここが x = self.attn(x) ではなく x = x + ... になっている点に注目してください。アテンションとフィードフォワードネットワークが計算するのは新しいベクトルではなく、元のベクトルへの「変更量」で、それを元のベクトルに足し戻します。これを残差接続と呼びます。

なぜこうするのでしょうか。モジュール 08 第 3 課で扱ったとおり、誤差逆伝播法では勾配を 1 層ずつ掛け戻していきます。層が多いと勾配は何度も掛け合わされ、とても小さく(学習が進まない)なるか、とても大きく(学習が爆発する)なりやすいのです。残差接続があれば、x + f(x)x についての微分には必ず 1 が含まれるので、勾配は「足し算」の道に沿って前の層へまっすぐ伝わり、各層の変換を通り抜ける必要がありません。これが、数十層、数百層の深いネットワークを学習させられる鍵の一つです。

別の見方もできます。モデル全体には入力から出力までまっすぐ通じる「幹線道路」が一本あり、各ブロックはそこに少しずつ何かを付け加えているだけなのです。

LayerNorm:値を安定させる

モジュール 08 第 2 課で見たとおり、入力の値の範囲が大きく違うと学習が難しくなります。1 層ずつ計算していくと、ベクトルの中の数もどんどん大きく、あるいは小さくなっていきます。LayerNorm は各サブ層の前で、各ベクトルを平均 0、標準偏差 1 に調整し、それから学習可能な二組のパラメータを掛けて足します(適切な範囲はモデル自身に決めさせます)。

これはモジュール 08 の標準化と同じ考え方で、ただ位置ごとのベクトルに別々に行い、しかもネットワークの途中で何度も行います。

LayerNorm がアテンションとフィードフォワードネットワークのに置かれている(self.attn(self.ln1(x)))点に注目してください。後ではありません。これを Pre-LN と呼び、GPT-2 以降のモデルはほぼすべてこの配置で、学習がより安定します。最後の出力の前にも LayerNorm がもう一つあります(ln_f)。

入力と出力

self.tok_emb = nn.Embedding(cfg.vocab_size, cfg.n_embd)
self.pos_emb = nn.Embedding(cfg.block_size, cfg.n_embd)
...
self.head = nn.Linear(cfg.n_embd, cfg.vocab_size, bias=False)
self.head.weight = self.tok_emb.weight  # 输出层和词元嵌入共用一套参数,GPT-2 也是这样做的

nn.Embedding はただの表で、i 行目が i 番目の文字のベクトルです。トークンの埋め込みの表は 6289 行(語彙表の大きさ)、位置の埋め込みの表は 128 行(最大 128 個の位置を扱う)です。

出力層は 128 次元のベクトルを 6289 個のスコアに変え、文字一つにつき一つです。ここには小さな工夫が一つあります。出力層とトークンの埋め込みが同じ行列を共有しているのです。トークンの埋め込みは「文字」を「ベクトル」に変え、出力層は逆に「ベクトル」がどの「文字」に最も似ているかを判定するので、同じパラメータを使うのは理にかなっていて、しかもパラメータを大きく節約できます。

順方向の計算がこれらをつなげます。

def forward(self, idx, targets=None, caches=None, start=0):
    B, T = idx.shape
    pos = torch.arange(start, start + T, device=idx.device)
    x = self.drop(self.tok_emb(idx) + self.pos_emb(pos))
    for i, block in enumerate(self.blocks):
        x = block(x, None if caches is None else caches[i])
    logits = self.head(self.ln_f(x))  # (B, T, 词表大小):每个位置对下一个词元的打分
    loss = None
    if targets is not None:
        loss = F.cross_entropy(logits.reshape(-1, logits.size(-1)), targets.reshape(-1))
    return logits, loss

cachesstart は第 6 課の KV キャッシュのためのもので、今は無視してください。損失はモジュール 08 第 5 課の交差エントロピーそのものです。どの位置も「6289 個の文字から一つを選ぶ」分類問題です。

== 2. 一批数据流过模型时的形状
  输入的词元编号        (2, 6)
  嵌入之后              (2, 6, 128)
  经过 4 个块之后        (2, 6, 128)
  输出层                (2, 6, 6289):每个位置对 6289 个字各打一个分

詩 2 句、各 6 文字を入れると (2, 6) です。埋め込みの後、各文字は 128 次元のベクトルになります。4 つのブロックを通っても形はまったく変わらず、これこそブロックをいくつでも重ねられる理由です。最後に各位置が 6289 個のスコアを得ます。

出力はすべての位置にあることに注目してください。6 文字の文章は、学習のときに「次の文字を当てる」問題を同時に 6 問提供します。1 番目の位置は「白」を見て「日」を当て、2 番目の位置は「白日」を見て「依」を当てる……。因果マスクがあるので、どの問題も答えを覗けません。これで学習の効率がずっと上がります。

パラメータはどこにあるか

== 1. 参数都在哪里(词表 6289 个字符)
  词元嵌入   6289 × 128 = 804,992(输出层和它共用,不另算)
  位置嵌入   128 × 128 = 16,384
  每个块     198,272:注意力 66,048,前馈网络 131,712,两个 LayerNorm 512
  4 个块共  793,088
  合计       1,614,720,其中词元嵌入占 50%

合計 161 万個のパラメータです。自分で計算してみることもできます。

  • アテンション:qkv は 128×384 に 384 個のバイアス、proj は 128×128 に 128 で、合計 66048。
  • フィードフォワードネットワーク:128×512 に 512、512×128 に 128 で、合計 131712。アテンションの 2 倍です。
  • LayerNorm:それぞれ 128 個のスケールと 128 個のシフトがあり、二つで 512。

私たちの小さなモデルでは、パラメータの半分がトークンの埋め込みの表にあります。語彙表に 6289 文字あるのに、ベクトルは 128 次元しかないからです。LLM では比率がまったく違います。ベクトルの次元は数千、層は数十あり、ブロックの中のパラメータが埋め込みよりはるかに多いのです。各ブロックの中では、フィードフォワードネットワークがおよそ 3 分の 2、アテンションが 3 分の 1 を占め、この比率は LLM でもほぼ同じです。

学習の前に

== 3. 没训练过的模型,损失应该接近随便猜
  损失 8.775,ln(6289) = 8.747
  下一个字最可能是:骠蔫潮涨湄(随机初始化,毫无道理)

学習していないモデルは 6289 個の文字をほぼ同じに採点するので、でたらめに当てるのと同じで、交差エントロピーは ln(6289) = 8.747 になるはずです。実際は 8.775 で、とても近い値です。モジュール 08 第 5 課で言ったとおり、学習開始時の損失がこの数からかけ離れていたら、たいていどこかの書き間違いです。モデルを組み立てたら、最初にすべきチェックがこれです。

モデルのパラメータの初期値は std=0.02 の正規分布からランダムに取っているので(_init メソッド)、最初はどの文字のスコアもほぼ同じになり、損失はでたらめに当てた場合に近くなります。

GPT と本物の LLM は何が違うか

この GPT の構造は GPT-2 とほぼ同じです。2026 年 9 月時点で、主流のオープンソース LLM はこの骨組みにいくつかの改良を加えていて、よくあるものは次のとおりです。

  • 位置の情報に、前の課で触れた回転位置エンコーディング(RoPE)を使う。
  • LayerNorm を、計算がより簡単な RMSNorm に替える。
  • フィードフォワードネットワークを「ゲート」付きの構造(たとえば SwiGLU)に替える。
  • 複数のアテンションヘッドで K と V を共有する(グループ化クエリアテンション)。次の課の KV キャッシュが占めるメモリを減らせる。
  • フィードフォワードネットワークを多くの「エキスパート」に替え、各文字はそのうちのいくつかだけを使う(混合エキスパート、MoE)。DeepSeek のモデルはこの構造です。

しかし骨組みは変わっていません。埋め込み、いくつかの「アテンション + フィードフォワードネットワーク」のブロック、残差接続、正規化、出力層。この 150 行の GPT を理解すれば、こうしたモデルのコードを読んだとき、大部分に見覚えがあるはずです。

練習問題

  1. n_layer を 8 に、n_embd を 256 に変えると、パラメータはいくつになりますか。トークンの埋め込みの占める割合はどうなりますか。先にこの課の式で計算してから、実行して確かめてください。
  2. Block.forward の二つの残差接続を取り除いて(x = self.attn(self.ln1(x)))、次の課の train.py で 300 ステップ学習させ、元の損失と比べてください。
  3. 出力層とトークンの埋め込みでパラメータを共有しているあの 1 行を削除すると、パラメータはどれだけ増えますか。

確認テスト

1. Transformer ブロックにはどんな部分がありますか?アテンションとフィードフォワードネットワークの分担は何ですか?

二つのサブ層、マルチヘッドの因果的自己アテンションとフィードフォワードネットワークがあり、各サブ層の前に LayerNorm が一つあり、外側を残差接続が包んでいます。アテンションは違う位置の間で情報を伝え、フィードフォワードネットワークは位置ごとに別々に計算して、集めた情報を処理します。

2. 残差接続が、とても深いネットワークの学習を助けられるのはなぜですか?

残差接続は各サブ層の出力を入力に足し戻すので、x + f(x) の x についての微分には必ず 1 が含まれます。誤差逆伝播法のとき、勾配はこのまっすぐな経路に沿って前の層に伝わり、何層も掛け合わされてとても小さくなったり大きくなったりしません。

3. 学習していないモデルの損失が ln(語彙表の大きさ) に近くなるはずなのはなぜですか?

ランダムに初期化したパラメータは小さいので、モデルはどの文字もほぼ同じに採点し、softmax 後の各文字の確率はおよそ 1/語彙表の大きさになります。交差エントロピーは -log(正しい文字の確率) なので、-log(1/語彙表の大きさ) = ln(語彙表の大きさ) になります。

質問と議論

このレッスンでつまずいたところは、ここで質問してください。他の人の質問に答えるのも歓迎です。

質問で 3 ポイント、回答で 6 ポイント。審査を通過すると公開されます。

議論を読み込んでいます…