""" 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. @karpathy """ import os # os.path.exists import math # math.log, math.exp import random # random.seed, random.choices, random.gauss, random.shuffle random.seed(42) # Let there be order among chaos # Let there be a Dataset `docs`: list[str] of documents (e.g. a list of names) if not os.path.exists('input.txt'): import urllib.request names_url = 'https://raw.githubusercontent.com/karpathy/makemore/988aa59/names.txt' urllib.request.urlretrieve(names_url, 'input.txt') docs = [line.strip() for line in open('input.txt') if line.strip()] random.shuffle(docs) print(f"num docs: {len(docs)}") # Let there be a Tokenizer to translate strings to sequences of integers ("tokens") and back uchars = sorted(set(''.join(docs))) # unique characters in the dataset become token ids 0..n-1 BOS = len(uchars) # token id for a special Beginning of Sequence (BOS) token vocab_size = len(uchars) + 1 # total number of unique tokens, +1 is for BOS print(f"vocab size: {vocab_size}") # Let there be Autograd to recursively apply the chain rule through a computation graph class Value: __slots__ = ('data', 'grad', '_children', '_local_grads') # Python optimization for memory usage def __init__(self, data, children=(), local_grads=()): self.data = data # scalar value of this node calculated during forward pass self.grad = 0 # derivative of the loss w.r.t. this node, calculated in backward pass self._children = children # children of this node in the computation graph self._local_grads = local_grads # local derivative of this node w.r.t. its children def __add__(self, other): other = other if isinstance(other, Value) else Value(other) return Value(self.data + other.data, (self, other), (1, 1)) def __mul__(self, other): other = other if isinstance(other, Value) else Value(other) return Value(self.data * other.data, (self, other), (other.data, self.data)) def __pow__(self, other): return Value(self.data**other, (self,), (other * self.data**(other-1),)) def log(self): return Value(math.log(self.data), (self,), (1/self.data,)) def exp(self): return Value(math.exp(self.data), (self,), (math.exp(self.data),)) def relu(self): return Value(max(0, self.data), (self,), (float(self.data > 0),)) def __neg__(self): return self * -1 def __radd__(self, other): return self + other def __sub__(self, other): return self + (-other) def __rsub__(self, other): return other + (-self) def __rmul__(self, other): return self * other def __truediv__(self, other): return self * other**-1 def __rtruediv__(self, other): return other * self**-1 def backward(self): topo = [] visited = set() def build_topo(v): if v not in visited: visited.add(v) for child in v._children: build_topo(child) topo.append(v) build_topo(self) self.grad = 1 for v in reversed(topo): for child, local_grad in zip(v._children, v._local_grads): child.grad += local_grad * v.grad # ===== [追加] 途中の数値を表示するための設定と関数 ===== TRACE_STEP = 0 # [追加] 何ステップ目(0始まり)の学習を表示するか TRACE = None # [追加] 表示中は記録用の辞書になる PAUSE = True # [追加] True: Enter で1段ずつ進む def pause(): global PAUSE if PAUSE: a = input(" ---- [Enter] 次へ / [a+Enter] 最後まで止まらずに表示 ---- ") if a.strip().lower() == 'a': PAUSE = False print() def val(x): return x.data if isinstance(x, Value) else x def show(title, M, rows, cols, fmt="{:+.3f}", w=7, mask=None): """行列 M(リストのリスト)を行ラベル・列ラベル付きで2次元に表示する""" print(f"■ {title} ({len(M)} 行 × {len(M[0])} 列)") lw = max(len(r) for r in rows) + 1 print(" " * lw + "".join(f"{c:>{w}}" for c in cols)) for i, (r, row) in enumerate(zip(rows, M)): cells = [] for j, x in enumerate(row): if mask is not None and mask(i, j): cells.append(f"{'·':>{w}}") else: cells.append(f"{fmt.format(val(x)):>{w}}") print(f"{r:<{lw}}" + "".join(cells)) print() def T(M): return [list(c) for c in zip(*M)] # Initialize the parameters, to store the knowledge of the model n_layer = 1 # depth of the transformer neural network (number of layers) n_embd = 16 # width of the network (embedding dimension) block_size = 16 # maximum context length of the attention window (note: the longest name is 15 characters) n_head = 4 # number of attention heads head_dim = n_embd // n_head # derived dimension of each head matrix = lambda nout, nin, std=0.08: [[Value(random.gauss(0, std)) for _ in range(nin)] for _ in range(nout)] state_dict = {'wte': matrix(vocab_size, n_embd), 'wpe': matrix(block_size, n_embd), 'lm_head': matrix(vocab_size, n_embd)} for i in range(n_layer): state_dict[f'layer{i}.attn_wq'] = matrix(n_embd, n_embd) state_dict[f'layer{i}.attn_wk'] = matrix(n_embd, n_embd) state_dict[f'layer{i}.attn_wv'] = matrix(n_embd, n_embd) state_dict[f'layer{i}.attn_wo'] = matrix(n_embd, n_embd) state_dict[f'layer{i}.mlp_fc1'] = matrix(4 * n_embd, n_embd) state_dict[f'layer{i}.mlp_fc2'] = matrix(n_embd, 4 * n_embd) params = [p for mat in state_dict.values() for row in mat for p in row] # flatten params into a single list[Value] print(f"num params: {len(params)}") # Define the model architecture: a function mapping tokens and parameters to logits over what comes next # Follow GPT-2, blessed among the GPTs, with minor differences: layernorm -> rmsnorm, no biases, GeLU -> ReLU def linear(x, w): return [sum(wi * xi for wi, xi in zip(wo, x)) for wo in w] def softmax(logits): max_val = max(val.data for val in logits) exps = [(val - max_val).exp() for val in logits] total = sum(exps) return [e / total for e in exps] 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] def gpt(token_id, pos_id, keys, values): tok_emb = state_dict['wte'][token_id] # token embedding pos_emb = state_dict['wpe'][pos_id] # position embedding x = [t + p for t, p in zip(tok_emb, pos_emb)] # joint token and position embedding x = rmsnorm(x) # note: not redundant due to backward pass via the residual connection if TRACE is not None: # [追加] TRACE['tok'].append(tok_emb); TRACE['pos'].append(pos_emb); TRACE['x0'].append(x) for li in range(n_layer): # 1) Multi-head Attention block x_residual = x x = rmsnorm(x) q = linear(x, state_dict[f'layer{li}.attn_wq']) k = linear(x, state_dict[f'layer{li}.attn_wk']) v = linear(x, state_dict[f'layer{li}.attn_wv']) if TRACE is not None: # [追加] TRACE['xn1'].append(x); TRACE['q'].append(q); TRACE['k'].append(k); TRACE['v'].append(v) keys[li].append(k) values[li].append(v) x_attn = [] for h in range(n_head): hs = h * head_dim q_h = q[hs:hs+head_dim] k_h = [ki[hs:hs+head_dim] for ki in keys[li]] v_h = [vi[hs:hs+head_dim] for vi in values[li]] 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) if TRACE is not None: # [追加] TRACE['score'][h].append(attn_logits); TRACE['weight'][h].append(attn_weights) head_out = [sum(attn_weights[t] * v_h[t][j] for t in range(len(v_h))) for j in range(head_dim)] x_attn.extend(head_out) x = linear(x_attn, state_dict[f'layer{li}.attn_wo']) if TRACE is not None: # [追加] TRACE['wo'].append(x) x = [a + b for a, b in zip(x, x_residual)] # 2) MLP block if TRACE is not None: # [追加] TRACE['attn_cat'].append(x_attn); TRACE['x1'].append(x) x_residual = x x = rmsnorm(x) x = linear(x, state_dict[f'layer{li}.mlp_fc1']) x = [xi.relu() for xi in x] if TRACE is not None: # [追加] TRACE['hidden'].append(x) x = linear(x, state_dict[f'layer{li}.mlp_fc2']) if TRACE is not None: # [追加] TRACE['fc2'].append(x) x = [a + b for a, b in zip(x, x_residual)] logits = linear(x, state_dict['lm_head']) if TRACE is not None: # [追加] TRACE['x2'].append(x) return logits # ===== [追加] 表示の本体 ===== def lab(t): return 'BOS' if t == BOS else uchars[t] def show_forward(step, doc, tokens, n, loss): P = [f"{i}:{lab(tokens[i])}" for i in range(n)] # 行ラベル=位置:入力の文字 D = [f"d{j}" for j in range(n_embd)] print("\n" + "=" * 70) print(f"学習ステップ {step+1} の計算を表示します(学習率などは元のまま)") print("=" * 70 + "\n") print(f"■ 0. 入力:この1ステップで使う名前 = '{doc}'") print(" トークン番号 :", tokens) print(" 入力 → 正解(次の文字):") for i in range(n): print(f" 位置{i}: {lab(tokens[i]):>3} → {lab(tokens[i+1])}") print(" 各行は1つの位置(位置番号:その位置に入力された文字)を表します。\n") pause() show("1a. トークン埋め込み wte の行(文字ごとのベクトル)", TRACE['tok'], P, D); pause() show("1b. 位置埋め込み wpe の行(位置ごとのベクトル)", TRACE['pos'], P, D); pause() show("1c. 和を RMSNorm したもの=ブロックへの入力 x", TRACE['x0'], P, D); pause() show("2a. クエリ q = Wq·RMSNorm(x)", TRACE['q'], P, D); pause() show("2b. キー k = Wk·RMSNorm(x)", TRACE['k'], P, D); pause() show("2c. バリュー v = Wv·RMSNorm(x)", TRACE['v'], P, D) print(f" ※ 16次元を4次元ずつに分け,d0–d3 がヘッド0,d4–d7 がヘッド1,… に使われます。\n"); pause() for h in range(n_head): S = [row + [0.0] * (n - len(row)) for row in TRACE['score'][h]] W = [row + [0.0] * (n - len(row)) for row in TRACE['weight'][h]] fut = lambda i, j: j > i print(f"--- ヘッド{h}(次元 d{h*head_dim}–d{h*head_dim+head_dim-1})---") show(f"3a. 注意の点数 q·k/√{head_dim}(行=クエリの位置,列=キーの位置)", S, P, P, mask=fut) show("3b. 注意の重み softmax(点数)(各行の和=1)", W, P, P, fmt="{:.3f}", mask=fut) print(" ※ '·' はまだ計算されていない未来の位置(キーがまだ存在しない)です。\n") pause() show("4a. 4ヘッドの出力をつないだもの(重みでバリューを混ぜたもの)", TRACE['attn_cat'], P, D); pause() show("4b. Wo を掛けたもの", TRACE['wo'], P, D); pause() show("4c. 残差接続:4b + ブロックへの入力 x(1c)=注意ブロックの出力", TRACE['x1'], P, D); pause() H = [f"h{j}" for j in range(4 * n_embd)] show("5a. MLP の隠れ層 ReLU(W1·RMSNorm(x))(64次元なので転置:行=成分,列=位置)", T(TRACE['hidden']), H, P, fmt="{:.3f}", mask=lambda i, j: val(TRACE['hidden'][j][i]) == 0) print(" ※ '·' は ReLU で 0 になった成分です。\n"); pause() show("5b. W2 を掛けたもの", TRACE['fc2'], P, D); pause() show("5c. 残差接続:5b + 4c = MLP ブロックの出力(最終層の出力)", TRACE['x2'], P, D); pause() V = [lab(i) for i in range(vocab_size)] show("6a. ロジット(次の文字の点数)(転置:行=文字,列=位置)", T(TRACE['logits']), V, P); pause() show("6b. 確率 softmax(ロジット)(転置:行=文字,列=位置,各列の和=1)", T(TRACE['probs']), V, P, fmt="{:.3f}") print("■ 6c. 正解の文字の確率と損失 −ln p") for i in range(n): tgt = tokens[i + 1] print(f" 位置{i}: 正解 {lab(tgt):>3} p = {val(TRACE['probs'][i][tgt]):.4f} −ln p = {val(TRACE['loss'][i]):.4f}") print(f" 損失(平均) L = {loss.data:.4f} (参考:でたらめに当てた場合 ln {vocab_size} = {math.log(vocab_size):.4f})\n") pause() def show_backward(loss): D = [f"d{j}" for j in range(n_embd)] print("■ 7. 逆伝播 loss.backward() が終わりました。各パラメータの .grad に ∂L/∂w が入っています。\n") G = [[p.grad for p in row] for row in state_dict['layer0.attn_wq']] show("7a. Wq の勾配 ∂L/∂Wq(行=q の成分,列=入力の成分)", G, [f"q{j}" for j in range(n_embd)], D, fmt="{:+.4f}", w=8) pause() def show_update(before, lr_t, step): D = [f"d{j}" for j in range(n_embd)] R = [f"q{j}" for j in range(n_embd)] after = [[p.data for p in row] for row in state_dict['layer0.attn_wq']] diff = [[a - b for a, b in zip(ra, rb)] for ra, rb in zip(after, before['layer0.attn_wq'])] print(f"■ 8. Adam による更新(このステップの学習率 {lr_t:.5f})\n") show("8a. 更新前の Wq", before['layer0.attn_wq'], R, D); pause() show("8b. 更新量(更新後 − 更新前)", diff, R, D, fmt="{:+.4f}", w=8) if step == 0: print(" ※ 最初のステップでは,Adam の補正後の値が m̂/√v̂ = 勾配の符号 になるため,") print(" 更新量はどれも ±学習率 になります(勾配の大きさによらず,向きだけが効く)。\n") pause() show("8c. 更新後の Wq", after, R, D) print("これで学習の1ステップが終わりです。以降は元のプログラムどおり,表示せずに学習を続けます。\n") pause() # Let there be Adam, the blessed optimizer and its buffers learning_rate, beta1, beta2, eps_adam = 0.01, 0.85, 0.99, 1e-8 m = [0.0] * len(params) # first moment buffer v = [0.0] * len(params) # second moment buffer # Repeat in sequence num_steps = 1000 # number of training steps for step in range(num_steps): # Take single document, tokenize it, surround it with BOS special token on both sides doc = docs[step % len(docs)] tokens = [BOS] + [uchars.index(ch) for ch in doc] + [BOS] n = min(block_size, len(tokens) - 1) if step == TRACE_STEP: # [追加] 記録の開始 TRACE = {key: [] for key in ['tok','pos','x0','xn1','q','k','v','attn_cat','wo','x1','hidden','fc2','x2','logits','probs','loss']} TRACE['score'] = [[] for _ in range(n_head)]; TRACE['weight'] = [[] for _ in range(n_head)] # Forward the token sequence through the model, building up the computation graph all the way to the loss keys, values = [[] for _ in range(n_layer)], [[] for _ in range(n_layer)] losses = [] for pos_id in range(n): token_id, target_id = tokens[pos_id], tokens[pos_id + 1] logits = gpt(token_id, pos_id, keys, values) probs = softmax(logits) loss_t = -probs[target_id].log() if TRACE is not None: # [追加] TRACE['logits'].append(logits); TRACE['probs'].append(probs); TRACE['loss'].append(loss_t) losses.append(loss_t) loss = (1 / n) * sum(losses) # final average loss over the document sequence. May yours be low. if TRACE is not None: # [追加] 順伝播の途中の値を表示 show_forward(step, doc, tokens, n, loss) # Backward the loss, calculating the gradients with respect to all model parameters loss.backward() if TRACE is not None: # [追加] 勾配を表示し,更新前の値を控える show_backward(loss) before = {name: [[p.data for p in row] for row in state_dict[name]] for name in ('layer0.attn_wq',)} # Adam optimizer update: update the model parameters based on the corresponding gradients lr_t = learning_rate * (1 - step / num_steps) # linear learning rate decay for i, p in enumerate(params): m[i] = beta1 * m[i] + (1 - beta1) * p.grad v[i] = beta2 * v[i] + (1 - beta2) * p.grad ** 2 m_hat = m[i] / (1 - beta1 ** (step + 1)) v_hat = v[i] / (1 - beta2 ** (step + 1)) p.data -= lr_t * m_hat / (v_hat ** 0.5 + eps_adam) p.grad = 0 if TRACE is not None: # [追加] 更新の結果を表示して記録を終える show_update(before, lr_t, step) TRACE = None print(f"step {step+1:4d} / {num_steps:4d} | loss {loss.data:.4f}", end='\r') # Inference: may the model babble back to us temperature = 0.5 # in (0, 1], control the "creativity" of generated text, low to high print("\n--- inference (new, hallucinated names) ---") for sample_idx in range(20): keys, values = [[] for _ in range(n_layer)], [[] for _ in range(n_layer)] token_id = BOS sample = [] for pos_id in range(block_size): logits = gpt(token_id, pos_id, keys, values) probs = softmax([l / temperature for l in logits]) token_id = random.choices(range(vocab_size), weights=[p.data for p in probs])[0] if token_id == BOS: break sample.append(uchars[token_id]) print(f"sample {sample_idx+1:2d}: {''.join(sample)}")