はじめに
Andrej Karpathyが2025年6月に公開したGistは、GPT(Generative Pre-trained Transformer)の訓練と推論を依存なしのPure Python 200行で実装したものだ。PyTorchもNumPyも使わない。すべてがmathとrandomだけで動く。
冒頭のコメントが本質を突いている。「This file is the complete algorithm. Everything else is just efficiency.」つまり、普段我々がPyTorchやJAXで書いている何千行ものコードの中で、アルゴリズムそのものはたったこれだけだということだ。
この記事では、このGistを上から順に追いながら、各コンポーネントが何をしているのかを解説する。
データセットとトークナイザー
コードはまずnames.txt(人名のリスト)をダウンロードし、文字単位のトークナイザーを構築する。
uchars = sorted(set(''.join(docs))) # 全ユニーク文字がトークンになる
BOS = len(uchars) # BOS (Beginning of Sequence) トークン
vocab_size = len(uchars) + 1
ここでのポイントは、BPE1のような複雑なトークナイザーを使わず、1文字 = 1トークンという最小構成にしていることだ。アルファベット26文字 + 特殊文字で vocab_size は27程度になる。
Autograd: 自動微分エンジン
次に登場するValueクラスが、このコード全体の土台となる自動微分エンジンだ。
class Value:
def __init__(self, data, children=(), local_grads=()):
self.data = data # スカラー値
self.grad = 0 # 損失に対するこのノードの勾配
self._children = children # 計算グラフ上の子ノード
self._local_grads = local_grads # 局所勾配
Valueは四則演算をオーバーロードしており、普通のPython式を書くだけで計算グラフが構築される。例えば a * b + c と書くと、__mul__と__add__が呼ばれて、裏で木構造が作られる。
backward()メソッドはトポロジカルソートで計算グラフを逆順に辿り、連鎖律(chain rule)で各パラメータの勾配を計算する。PyTorchのloss.backward()と同じことを、50行ほどで実現している。
Transformerアーキテクチャ
パラメータの初期化部分を見てみる。
n_layer = 1 # Transformer の層数
n_embd = 16 # 埋め込み次元
block_size = 16 # 最大コンテキスト長
n_head = 4 # アテンションヘッド数
head_dim = n_embd // n_head # ヘッドあたりの次元 = 4
GPT-2は n_layer=12, n_embd=768, n_head=12 だが、ここではミニチュア版として16次元、1層、4ヘッドで構成されている。人名生成というタスクには十分なサイズだ。
state_dictにはトークン埋め込み(wte)、位置埋め込み(wpe)、各層のAttentionとMLPの重み、そして最終出力層(lm_head)が格納される。
GPTのフォワードパス
gpt()関数がモデルの本体だ。入力は1つのトークンIDと位置IDで、出力は次トークンの確率分布(logits)。
処理の流れ:
- 埋め込み: トークン埋め込みと位置埋め込みを足し合わせる
- RMSNorm: 正規化(LayerNormの代わりにRMSNorm2を使用)
- Multi-Head Attention: Q, K, Vを計算し、ドット積アテンションを実行
- 残差結合: アテンション出力を入力に足す
- MLP: 2層のフィードフォワードネットワーク(ReLU活性化)
- 残差結合: MLP出力を入力に足す
- 出力:
lm_headで語彙サイズのlogitsに変換
def rmsnorm(x):
ms = sum(xi * xi for xi in x) / len(x)
scale = (ms + 1e-5) ** -0.5
return [xi * scale for xi in x]
このRMSNormは実質3行だ。ベクトルの二乗平均を取って、その逆数をスケールとして掛けるだけ。
アテンション機構
Multi-Head Attentionの核心部分を見てみよう。
attn_logits = [
sum(q_h[j] * k_h[t][j] for j in range(head_dim)) / head_dim**0.5
for t in range(len(k_h))
]
attn_weights = softmax(attn_logits)
head_out = [
sum(attn_weights[t] * v_h[t][j] for t in range(len(v_h)))
for j in range(head_dim)
]
行列演算ライブラリなしでやっているため、ドット積もsoftmaxもforループで書かれている。head_dim**0.5で割るのは「Scaled Dot-Product Attention」のスケーリングで、これがないとアテンションのlogitsが大きくなりすぎてsoftmaxが飽和する。
KV Cache3の仕組みもここに見える。keysとvaluesのリストに過去のK, Vを蓄積していくことで、推論時に再計算を避けている。
訓練ループとAdamオプティマイザ
learning_rate, beta1, beta2, eps_adam = 0.01, 0.85, 0.99, 1e-8
訓練ループは1000ステップで、各ステップで1つの名前(ドキュメント)を処理する。損失関数はクロスエントロピー(-log(prob[target]))の平均だ。
Adamオプティマイザの更新式も明示的に書かれている。一次モーメント(移動平均)と二次モーメント(二乗勾配の移動平均)を使ってパラメータを更新し、学習率は線形に減衰させている。
推論
最後に、訓練済みモデルで新しい名前を生成する。
temperature = 0.5
temperatureは生成の「創造性」を制御するパラメータだ。logitsをtemperatureで割ってからsoftmaxを取ることで、低い値ほど確率分布が尖り(高確率トークンが選ばれやすくなり)、高い値ほど平坦になる(ランダム性が増す)。
このコードから学べること
- Transformerは本質的にシンプル: 埋め込み → アテンション → MLP → 出力、の繰り返しに過ぎない
- 自動微分は連鎖律の再帰的適用:
backward()の実装は30行もない - GPUやPyTorchは効率のためのもの: アルゴリズム自体は標準ライブラリだけで記述できる
- スケーリングが全て: 同じアーキテクチャを巨大なデータと計算資源で訓練すると、ChatGPTになる