(microgpt の主要データ構造)= # microgpt の主要データ構造 ## この章で学ぶこと - 自動微分のための `Value` と計算グラフ - `state_dict` と `params`、行列の初期化 - {numref}`microgpt の構造` の **B3**・**B4** に対応するコード ## Value の直感(履歴つきのスカラー) **`Value` は自動微分の最小単位です。** スカラー 1 個に計算履歴を持たせ、`backward()` で勾配を流します。`data`(値)、`grad`(勾配)、`_children`(入力ノード)、`_local_grads`(局所勾配)をまとめて持ち、**この数字がどこから来たか**を覚えています。順伝播の計算を行うと同時にその履歴を記録し、`backward()` を呼ぶことで末端から根へ向かって勾配を足し戻すことができます。 `loss.backward()` は、計算の履歴を**トポロジカル順**に逆順にたどる処理です。`build_topo()` は依存を崩さない順に並べるための準備にすぎません。 ### 自動微分のための `Value` `Value` は、このファイルの土台です。PyTorch の `Tensor` のごく小さな対応物で、**スカラー(1 つの実数)だけ**を扱います。ベクトルや行列は「`Value` のリストのリスト」として表現します。 ```{literalinclude} ../../../src/part3/microgpt.py :language: python :lines: 53-98 :lineno-match: ``` **行ごとの意味** - **L53** `class Value:` — 自動微分可能なスカラーを表すクラス。 - **L54** `__slots__ = (...)` — インスタンス属性を固定し、インスタンスごとの `__dict__` を持たせない(大量ノード時の省メモリ)。 - **L56–L60** `__init__` — `data` に順伝播の値、`grad` は 0 から、`children` に入力ノードのタプル、`local_grads` に各入力に対する ∂(自分)/∂(入力)。 - **L62–L65** `__add__` — 相手を `Value` に揃え、和の値と局所微分 `(1, 1)` を記録した新ノードを返す。 - **L67–L70** `__mul__` — 積の値と、乗法の微分に対応する局所微分 `(other.data, self.data)` を記録。 - **L72** `__pow__` — 累乗と、その入力に対する微分係数を 1 行で定義。 - **L73–L75** `log` / `exp` / `relu` — 各活性化・非線形の順伝播値と、入力への局所微分(`log` は `1/x`、`relu` は `0` または `1`)。 - **L76–L82** 符号反転・減算・除算など — 既存の `+` と `*` などに還元する演算子オーバーロード。 - **L84–L98** `backward` — `build_topo` で子→親の順に並べたあと逆順に辿り、`self.grad = 1` から `child.grad += local_grad * v.grad` で連鎖律を適用。 要約すると、`__add__` や `__mul__` は新しい `Value` を返すと同時に**親子リンクと局所微分**を保存し、`backward()` は**末端から根へ**勾配を足し戻します(同じ `Value` が複数経路に現れる場合は `+=` で合算)。数式では次の 1 行に相当します。 ```text child.grad += local_grad * v.grad ``` 普通の `float` は値しか持ちませんが、`Value` は**計算グラフ**を保持するので、`loss` までつながったグラフから各パラメータの勾配を求められます。 #### 最小例で動きを追う 式だけだと抽象的なので、**スカラーが 2 個と乗算 1 回**だけの例で、順伝播と逆伝播を数で対応させます。上に掲げた `Value` クラス(`microgpt.py` と同じ定義)を読み込んだ前提の試用例です。 ```python a = Value(2.0) b = Value(3.0) c = a * b # 順伝播: c.data = 2 * 3 = 6 c.backward() # 「c を最後のスカラー」とみなし、∂c/∂a と ∂c/∂b を求める(学習時は c の代わりに loss) # backward 後: c.grad == 1 # 実装では末端で d(末端)/d(末端)=1 を立てる # backward 後: a.grad == 3.0 # ∂c/∂a = b.data # backward 後: b.grad == 2.0 # ∂c/∂b = a.data ``` 乗算では `_local_grads` が `(b.data, a.data)`、つまり `(3, 2)` です。`backward()` の先頭で `c.grad = 1` としたうえで、逆順に子へ流すと、 - `a.grad += 3 * c.grad = 3`(∂c/∂a = b と一致) - `b.grad += 2 * c.grad = 2`(∂c/∂b = a と一致) となり、手で計算した偏微分と一致します。学習では「最後のスカラー」が損失 `loss` になり、`loss.backward()` で **∂loss/∂(各パラメータ)** が同じ仕組みで `state_dict` 内の各 `Value.grad` に溜まります。 加算だけの例も対比用に置きます。 ```python x = Value(2.0) y = Value(3.0) s = x + y # 順伝播: s.data = 5。局所微分は (1, 1) s.backward() # backward 後: s.grad == 1 # backward 後: x.grad == 1.0 # ∂s/∂x = 1 # backward 後: y.grad == 1.0 # ∂s/∂y = 1 ``` つまり **順伝播で `data` を組み立て、逆伝播で `grad` を末端から根に向かって足し戻す**のが `Value` の動きです。モデル全体はこの繰り返しが長いだけです。 ### 重み 次は重みです。実装では辞書 `state_dict` にまとめられ、**各要素はすべて `Value` のスカラー**です。行列は「行のリストのリスト」として表現し、`matrix(nout, nin)` で `nout × nin` 個の `Value` をガウス乱数で初期化しています。 ```{literalinclude} ../../../src/part3/microgpt.py :language: python :lines: 103-121 :lineno-match: ``` **行ごとの意味** - **L103–L106** ハイパーパラメータ — 層数・埋め込み次元・最大文脈長・ヘッド数を定義する。 - **L107** `head_dim = n_embd // n_head` — 埋め込み次元をヘッド数で割り、各ヘッドの部分空間の次元にする。 - **L108** `matrix = lambda ...` — `nout` 行 `nin` 列の `Value` 行列を、標準偏差 `std` のガウス乱数で初期化するヘルパー。 - **L110** `state_dict = { ... }` — まず `wte`(語彙×埋め込み)、`wpe`(最大位置×埋め込み)、`lm_head`(語彙×埋め込み)の 3 つを登録する。 - **L111–L119** `for i in range(n_layer)` — 各層に Attention 用 4 行列(Q/K/V/O)と MLP 用 2 行列を追加。Attention は `n_embd × n_embd`、MLP は中間を 4 倍に広げる `4*n_embd × n_embd` と、その逆形状の `mlp_fc2`。 - **L120** `params = [...]` — `state_dict` 内のすべての行列を走査し、含まれる `Value` を**一次元リスト**に平坦化(Adam がこの順で更新する)。 - **L121** パラメータ数の表示。 意味づけの対応は次のとおりです。 - **`wte`**: 語彙サイズ `vocab_size` 行、`n_embd` 列。トークン ID から埋め込みベクトルを引く。 - **`wpe`**: 位置 `0 … block_size-1` まで `n_embd` 次元。文の先頭から何文字目かを表す。 - **`layer{i}.attn_wq` / `attn_wk` / `attn_wv` / `attn_wo`**: Query・Key・Value・Attention 出力への線形変換(いずれも `n_embd × n_embd`)。 - **`layer{i}.mlp_fc1` / `mlp_fc2`**: MLP の 2 層(中間 4 倍)。 - **`lm_head`**: 最終表現から語彙サイズ次元の logits へ写す `vocab_size × n_embd`。 学習で更新するのは `params` に並んだ `Value` だけです。Adam はこのリストと同じ順で勾配を読み、`Value.data` を更新します。 最初は小さな乱数で始めます。ここで大事なのは、**モデルは最初から賢いわけではない**ということです。学習しながら少しずつ文字のつながり方を覚えていきます。