第16章 microgpt とは何か(出典と位置づけ)
16.1 この章で学ぶこと
この章で学ぶ内容は以下のとおりです。
学ぶこと |
ポイント |
|---|---|
microgpt の目的 |
依存ゼロの標準ライブラリだけで GPT 型モデルを学習・推論する教育実装 |
公開者と一次ソース |
Andrej Karpathy が GitHub Gist と解説ページで公開 |
本書での使い方 |
|
16.2 microgpt とは
microgpt は、PyTorch などの外部ライブラリに頼らず、標準ライブラリだけで GPT 型の言語モデルを学習と推論できるようにした、単一ファイルの Python 実装です。
ファイル先頭の説明文がその本質を一言で表しています。
The most atomic way to train and run inference for a GPT in pure, dependency-free Python.
This file is the complete algorithm. Everything else is just efficiency.
この 2 文を噛み砕くと次のようになります。
“The most atomic way”(これ以上削れない最小構成):これ以上分解できないほど小さく、かつ完全であることを意味します。 大きなフレームワークが提供する抽象化を剥ぎ取り、GPT の本質だけを残した実装です。
“pure, dependency-free Python”(純粋・依存ゼロの Python):import math、import struct、import os など標準ライブラリのみで動作します。
NumPy の行列演算も PyTorch の自動微分も使わず、すべてのロジックを Python のリストと基本演算で実装します。
“This file is the complete algorithm. Everything else is just efficiency.”:GPT の学習と推論のアルゴリズムとして必要なものはこの 1 ファイルに完結しています。 PyTorch や CUDA が追加するのは速度であってアルゴリズムそのものではない、という宣言です。
microgpt に含まれるもの
図16-2: microgpt.py の構成
特徴 |
詳細 |
|---|---|
依存ゼロ |
|
単一ファイル |
学習から推論まで 1 ファイルに収まる(251 行) |
教育目的 |
大規模フレームワークの裏側で起きていることを「読み切れる分量」で追える |
実務向きではない |
速度・拡張性・本番運用は考慮しない。アルゴリズムの骨格だけを示す |
Mojo 移植の出発点 |
依存がないため、標準ライブラリなしの Mojo にそのまま対応させやすい |
16.3 誰が公開しているか
公開者は Andrej Karpathy です。
深層学習と言語モデルの教育コンテンツで広く知られており、ファイル内のクレジット(@karpathy)からも確認できます。
Andrej Karpathy について
Karpathy はスタンフォード大学で深層学習(CS231n)を担当し、OpenAI の創業メンバーの一人、Tesla の AI ディレクターを経て、2024 年からは独立して教育コンテンツの発信に注力しています。 「複雑なものを最小の実装で理解させる」スタイルの教育実装を多数公開しており、microgpt はその系譜に属します。
実装名 |
内容 |
|---|---|
micrograd |
自動微分エンジンを 100 行で実装 |
minbpe |
BPE トークナイザーを最小実装 |
nanoGPT |
GPT の学習実装(NumPy と PyTorch 使用) |
microgpt |
GPT の学習と推論を標準ライブラリのみで実装 |
microgpt は nanoGPT をさらに小さくし、外部依存をゼロにしたバージョンです。 nanoGPT で GPU 高速化のために PyTorch が必要だった部分を、純粋な Python の演算に置き換えています。
一次ソースと本書内のファイル
リソース |
場所 |
|---|---|
ソースコード(Gist) |
https://gist.github.com/karpathy/8627fe009c40f57531cb18360106ce95 |
解説ページ |
https://karpathy.ai/microgpt.html |
本書プロジェクト内 |
|
本書では src/part3/microgpt.py を一次ソースとして扱います。
行番号はこのファイルに対応しており、microgpt.py を開きながら各章を読み進める構成になっています。
16.4 本書での位置づけ
本書では microgpt を「読んで理解する → Mojo に移植する → 高速化する」という 3 段階で扱います。
学習の流れ
図16-4: 本書の学習の流れ(第3部の読み進め方)
章 |
何をするか |
|---|---|
第17章(仕様) |
入出力・学習と生成の流れを俯瞰する。「何が入って何が出るか」を先に把握する |
第18章(構造) |
コード全体を 7 ブロック(B1〜B7)として地図化する。どこに何があるかを先に知ることで、細部を読むときに迷子にならない |
第19〜24章(コードリーディング) |
B1(前処理)から B7(推論ループ)まで、ブロックごとに行単位でコードを読む |
第25章(Mojo 移植) |
|
第26〜28章(MAX 統合) |
MAX(Modular AI eXecution platform)を使って Mojo 実装をさらに高速化する |
第29〜30章(比較) |
同じアルゴリズムを PyTorch・MLX で実装し、設計思想・速度・コード量を比較する |
なぜ Python 実装を先に読むか
Mojo への移植を始める前に Python 実装を丁寧に読む理由は 2 つあります。
アルゴリズムと実装を分離して理解する:GPT の仕組み(アルゴリズム)と Mojo の言語機能(実装手段)を混同しないようにするため。Python コードでアルゴリズムを先に把握し、その後「Mojo ではこう書く」というマッピングに集中できます。
依存ゼロの対称性:microgpt は NumPy も PyTorch も使わないため、Mojo への移植でも「対応するライブラリがない」という問題が生じません。
math.exp(x)→ Mojo のmath.exp(x)のように 1 対 1 に近い形で対応できます。
16.5 microgpt.py の全文
本書が一次ソースとして扱う microgpt.py の全文を以下に示します。学習と推論のすべてがこの 1 ファイルに収まっている点に注目してください。今の段階では細部を理解する必要はありません。「これだけの分量で GPT が動く」という全体の規模感をつかんでおくことが目的です。
第19章以降では、このコードを 7 ブロック(B1〜B7)に分け、行番号をたどりながら 1 つずつ読み解いていきます。
注記:本書版で原典に加えた変更点
以下のコードは、読者の理解を助けるために一次ソース(16.3 のリソース一覧で示した Gist)へ次の変更を加えています。いずれも原文とは一致しません。
コメントの日本語化:原典の英語コメントを日本語に翻訳し、必要に応じて補足を加えています。
説明用 docstring の追加:
linear・softmax・rmsnorm・gptなどの関数に、処理内容を説明する docstring を筆者が追加しています。学習ログの表示形式の調整:原典は学習の進捗を同じ行に上書き表示しますが、本書版では各ステップのログが残るよう改行表示に変更しています(出力の見た目だけの違いで、計算結果には影響しません)。
これら以外のアルゴリズム(GPT 本体・自動微分・Adam・トークナイザー)は原典のままで、変更していません。
1"""
2The most atomic way to train and run inference for a GPT in pure, dependency-free Python.
3This file is the complete algorithm.
4Everything else is just efficiency.
5
6@karpathy
7"""
8
9# =============================================================================
10# 最小GPTの概要
11# =============================================================================
12# 本ファイルは、依存関係なしの純粋なPythonで実装された最小限のGPTである。
13# 構成要素:
14# 1. データセット: 名前リストなどのテキスト
15# 2. トークナイザー: 文字→整数IDの変換
16# 3. Value: 自動微分(Autograd)の計算グラフ
17# 4. GPTモデル: 1層のTransformer(Attention + MLP)
18# 5. Adam最適化: パラメータ更新
19# 6. 推論: 次のトークンを確率的に生成
20# =============================================================================
21
22import os # os.path.exists
23import math # math.log, math.exp
24import random # random.seed, random.choices, random.gauss, random.shuffle
25random.seed(42) # 再現性のため乱数シードを固定
26
27# -----------------------------------------------------------------------------
28# データセットの準備
29# -----------------------------------------------------------------------------
30# docs: 文書のリスト(例: 名前のリスト)。各行が1つの文書
31if not os.path.exists('input.txt'):
32 import urllib.request
33 names_url = 'https://raw.githubusercontent.com/karpathy/makemore/988aa59/names.txt'
34 urllib.request.urlretrieve(names_url, 'input.txt')
35docs = [line.strip() for line in open('input.txt') if line.strip()]
36random.shuffle(docs) # 学習時の順序をランダム化
37print(f"num docs: {len(docs)}")
38
39# -----------------------------------------------------------------------------
40# トークナイザー
41# -----------------------------------------------------------------------------
42# 文字列を整数列(トークンID)に変換する。文字ベースのトークナイザー。
43uchars = sorted(set(''.join(docs))) # データセット内の全ユニーク文字 → ID 0..n-1
44BOS = len(uchars) # 文の開始/終了を示す特殊トークン(Beginning of Sequence)
45vocab_size = len(uchars) + 1 # 語彙サイズ(文字数 + BOS)
46print(f"vocab size: {vocab_size}")
47
48# -----------------------------------------------------------------------------
49# Value: 自動微分(Autograd)の実装
50# -----------------------------------------------------------------------------
51# 計算グラフを構築し、backward()で連鎖律により勾配を逆伝播する。
52# PyTorchのTensorの最小版。スカラー値のみを扱う。
53class Value:
54 __slots__ = ('data', 'grad', '_children', '_local_grads') # メモリ最適化
55
56 def __init__(self, data, children=(), local_grads=()):
57 self.data = data # 順伝播で計算されたスカラー値
58 self.grad = 0 # 損失に対するこのノードの勾配(逆伝播で計算)
59 self._children = children # 計算グラフ上の子ノード
60 self._local_grads = local_grads # 子ノードに対する局所勾配(連鎖律用)
61
62 def __add__(self, other):
63 # 加算: d(a+b)/da=1, d(a+b)/db=1
64 other = other if isinstance(other, Value) else Value(other)
65 return Value(self.data + other.data, (self, other), (1, 1))
66
67 def __mul__(self, other):
68 # 乗算: d(a*b)/da=b, d(a*b)/db=a
69 other = other if isinstance(other, Value) else Value(other)
70 return Value(self.data * other.data, (self, other), (other.data, self.data))
71
72 def __pow__(self, other): return Value(self.data**other, (self,), (other * self.data**(other-1),))
73 def log(self): return Value(math.log(self.data), (self,), (1/self.data,))
74 def exp(self): return Value(math.exp(self.data), (self,), (math.exp(self.data),))
75 def relu(self): return Value(max(0, self.data), (self,), (float(self.data > 0),)) # ReLU: max(0,x)
76 def __neg__(self): return self * -1
77 def __radd__(self, other): return self + other
78 def __sub__(self, other): return self + (-other)
79 def __rsub__(self, other): return other + (-self)
80 def __rmul__(self, other): return self * other
81 def __truediv__(self, other): return self * other**-1
82 def __rtruediv__(self, other): return other * self**-1
83
84 def backward(self):
85 # トポロジカルソートで計算グラフを逆順に辿り、連鎖律で勾配を伝播
86 topo = []
87 visited = set()
88 def build_topo(v):
89 if v not in visited:
90 visited.add(v)
91 for child in v._children:
92 build_topo(child)
93 topo.append(v)
94 build_topo(self)
95 self.grad = 1 # 損失自身の勾配は1(dL/dL=1)
96 for v in reversed(topo):
97 for child, local_grad in zip(v._children, v._local_grads):
98 child.grad += local_grad * v.grad # 連鎖律: ∂L/∂child += (∂v/∂child) * (∂L/∂v)
99
100# -----------------------------------------------------------------------------
101# モデルパラメータの初期化
102# -----------------------------------------------------------------------------
103n_layer = 1 # Transformerの層数(深さ)
104n_embd = 16 # 埋め込み次元(ネットワークの幅)
105block_size = 16 # コンテキスト長の上限(注意窓の最大長。最長名前は15文字)
106n_head = 4 # マルチヘッドアテンションのヘッド数
107head_dim = n_embd // n_head # 各ヘッドの次元(n_embdをn_headで分割)
108matrix = lambda nout, nin, std=0.08: [[Value(random.gauss(0, std)) for _ in range(nin)] for _ in range(nout)]
109# パラメータ辞書: wte=トークン埋め込み, wpe=位置埋め込み, lm_head=言語モデル出力層
110state_dict = {'wte': matrix(vocab_size, n_embd), 'wpe': matrix(block_size, n_embd), 'lm_head': matrix(vocab_size, n_embd)}
111for i in range(n_layer):
112 # Attention: Q/K/V/O の4つの線形変換(Query, Key, Value, Output)
113 state_dict[f'layer{i}.attn_wq'] = matrix(n_embd, n_embd)
114 state_dict[f'layer{i}.attn_wk'] = matrix(n_embd, n_embd)
115 state_dict[f'layer{i}.attn_wv'] = matrix(n_embd, n_embd)
116 state_dict[f'layer{i}.attn_wo'] = matrix(n_embd, n_embd)
117 # MLP: 2層の全結合(中間層は4倍に拡張)
118 state_dict[f'layer{i}.mlp_fc1'] = matrix(4 * n_embd, n_embd)
119 state_dict[f'layer{i}.mlp_fc2'] = matrix(n_embd, 4 * n_embd)
120params = [p for mat in state_dict.values() for row in mat for p in row] # 全パラメータを1次元リストに平坦化
121print(f"num params: {len(params)}")
122
123# -----------------------------------------------------------------------------
124# モデルアーキテクチャ
125# -----------------------------------------------------------------------------
126# トークン列とパラメータから「次に来るトークン」のlogitsを出力する関数。
127# GPT-2に準拠。相違点: LayerNorm→RMSNorm、バイアスなし、GeLU→ReLU
128
129def linear(x, w):
130 """線形変換: y = Wx。入力xと重みwから出力ベクトルを計算"""
131 return [sum(wi * xi for wi, xi in zip(wo, x)) for wo in w]
132
133def softmax(logits):
134 """ソフトマックス: logitsを確率分布に変換。数値安定性のためmaxで引いてからexp"""
135 max_val = max(val.data for val in logits)
136 exps = [(val - max_val).exp() for val in logits]
137 total = sum(exps)
138 return [e / total for e in exps]
139
140def rmsnorm(x):
141 """RMSNorm: 二乗平均の平方根で正規化。LayerNormの簡略版(平均を引かない)"""
142 ms = sum(xi * xi for xi in x) / len(x)
143 scale = (ms + 1e-5) ** -0.5
144 return [xi * scale for xi in x]
145
146def gpt(token_id, pos_id, keys, values):
147 """
148 GPTの順伝播。現在のトークンと位置から、次トークンのlogitsを出力。
149 keys, values: 過去のK/Vをキャッシュ(推論時の効率化、学習時は因果マスク用)
150 """
151 tok_emb = state_dict['wte'][token_id] # トークン埋め込み
152 pos_emb = state_dict['wpe'][pos_id] # 位置埋め込み
153 x = [t + p for t, p in zip(tok_emb, pos_emb)] # トークン+位置の結合埋め込み
154 x = rmsnorm(x) # 初期正規化(残差接続経由で勾配が流れるため冗長ではない)
155
156 for li in range(n_layer):
157 # --- 1) マルチヘッドアテンションブロック ---
158 x_residual = x
159 x = rmsnorm(x)
160 q = linear(x, state_dict[f'layer{li}.attn_wq']) # Query
161 k = linear(x, state_dict[f'layer{li}.attn_wk']) # Key
162 v = linear(x, state_dict[f'layer{li}.attn_wv']) # Value
163 keys[li].append(k)
164 values[li].append(v)
165 x_attn = []
166 for h in range(n_head):
167 hs = h * head_dim
168 q_h = q[hs:hs+head_dim]
169 k_h = [ki[hs:hs+head_dim] for ki in keys[li]]
170 v_h = [vi[hs:hs+head_dim] for vi in values[li]]
171 # スケール付き内積: attn = softmax(QK^T / sqrt(d_k))
172 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))]
173 attn_weights = softmax(attn_logits)
174 # 重み付き和: output = attn @ V
175 head_out = [sum(attn_weights[t] * v_h[t][j] for t in range(len(v_h))) for j in range(head_dim)]
176 x_attn.extend(head_out)
177 x = linear(x_attn, state_dict[f'layer{li}.attn_wo']) # ヘッド結合
178 x = [a + b for a, b in zip(x, x_residual)] # 残差接続
179
180 # --- 2) MLPブロック ---
181 x_residual = x
182 x = rmsnorm(x)
183 x = linear(x, state_dict[f'layer{li}.mlp_fc1'])
184 x = [xi.relu() for xi in x]
185 x = linear(x, state_dict[f'layer{li}.mlp_fc2'])
186 x = [a + b for a, b in zip(x, x_residual)] # 残差接続
187
188 logits = linear(x, state_dict['lm_head']) # 語彙サイズ次元のlogits
189 return logits
190
191# -----------------------------------------------------------------------------
192# Adam最適化のバッファ
193# -----------------------------------------------------------------------------
194learning_rate, beta1, beta2, eps_adam = 0.01, 0.85, 0.99, 1e-8
195m = [0.0] * len(params) # 第1モーメント(勾配の移動平均)
196v = [0.0] * len(params) # 第2モーメント(勾配の二乗の移動平均)
197
198# -----------------------------------------------------------------------------
199# 学習ループ
200# -----------------------------------------------------------------------------
201num_steps = 1000 # 学習ステップ数
202for step in range(num_steps):
203
204 # 1文書を取得し、トークン化。前後にBOSを付与
205 doc = docs[step % len(docs)]
206 tokens = [BOS] + [uchars.index(ch) for ch in doc] + [BOS]
207 n = min(block_size, len(tokens) - 1) # 予測する位置数
208
209 # 順伝播: 各位置で次トークンを予測し、負の対数尤度(交差エントロピー)を計算
210 keys, values = [[] for _ in range(n_layer)], [[] for _ in range(n_layer)]
211 losses = []
212 for pos_id in range(n):
213 token_id, target_id = tokens[pos_id], tokens[pos_id + 1]
214 logits = gpt(token_id, pos_id, keys, values)
215 probs = softmax(logits)
216 loss_t = -probs[target_id].log() # 正解トークンの負の対数尤度
217 losses.append(loss_t)
218 loss = (1 / n) * sum(losses) # 文書全体の平均損失
219
220 # 逆伝播: 全パラメータに対する勾配を計算
221 loss.backward()
222
223 # Adam更新: 勾配に基づいてパラメータを更新
224 lr_t = learning_rate * (1 - step / num_steps) # 線形学習率減衰
225 for i, p in enumerate(params):
226 m[i] = beta1 * m[i] + (1 - beta1) * p.grad
227 v[i] = beta2 * v[i] + (1 - beta2) * p.grad ** 2
228 m_hat = m[i] / (1 - beta1 ** (step + 1)) # バイアス補正
229 v_hat = v[i] / (1 - beta2 ** (step + 1))
230 p.data -= lr_t * m_hat / (v_hat ** 0.5 + eps_adam) # Adam更新式
231 p.grad = 0 # 次ステップ用に勾配をリセット
232
233 print(f"step {step+1:4d} / {num_steps:4d} | loss {loss.data:.4f}")
234
235# -----------------------------------------------------------------------------
236# 推論(生成)
237# -----------------------------------------------------------------------------
238temperature = 0.5 # (0,1]の範囲。低いほど確定的、高いほど多様な出力
239print("\n--- inference (new, hallucinated names) ---")
240for sample_idx in range(20):
241 keys, values = [[] for _ in range(n_layer)], [[] for _ in range(n_layer)]
242 token_id = BOS # BOSから開始
243 sample = []
244 for pos_id in range(block_size):
245 logits = gpt(token_id, pos_id, keys, values)
246 probs = softmax([l / temperature for l in logits]) # temperatureで分布を調整
247 token_id = random.choices(range(vocab_size), weights=[p.data for p in probs])[0] # 確率的サンプリング
248 if token_id == BOS:
249 break # BOSが来たら終端
250 sample.append(uchars[token_id])
251 print(f"sample {sample_idx+1:2d}: {''.join(sample)}")
16.6 まとめ
本章で扱った内容を以下にまとめます。
要素 |
内容 |
|---|---|
microgpt |
標準ライブラリのみで学習・推論まで行う教育向け単一ファイル実装 |
公開者 |
Andrej Karpathy(GitHub Gist + karpathy.ai で公開) |
本書での役割 |
Python で GPT を理解してから Mojo へ移植するための出発点 |