69 ポイント 投稿者 GN⁺ 2026-02-17 | 1件のコメント | WhatsAppで共有
  • karpathyが公開したアートプロジェクト。200行の単一ファイルで外部依存なしにGPT全体のアルゴリズムを実装
  • 本番LLMとの違いは規模と効率性だけであり、中核は同じ。このコードを理解すればGPTのアルゴリズム的本質を理解したことになる
  • データセット、トークナイザー、autogradエンジン、GPT-2類似のTransformerアーキテクチャAdamオプティマイザ、学習・推論ループまで含む
  • micrograd、makemore、nanogptなど既存プロジェクトと10年にわたるLLM単純化作業の集大成であり、これ以上単純化できない最小形としてGPTの本質を収めている
  • 32,000件の名前データを学習してもっともらしい新しい名前を生成し、すべての計算をスカラー単位のautogradで直接実行
  • 学習過程は損失計算 → 逆伝播 → Adam更新で構成され、約1分以内で実行可能

microgpt概要

  • microgptは200行のPythonスクリプトで、GPTモデルの学習と推論の過程を完全に実装
    • 外部ライブラリなしでデータセット、トークナイザー、autograd、モデル、オプティマイザ、学習ループをすべて含む
  • 既存のmicrograd、makemore、nanogptなどのプロジェクトを統合し、単一ファイルに整理
  • 「これ以上単純化できない」レベルまでアルゴリズムの中核だけを残した実装
  • 全コードはGitHub GistウェブページGoogle Colabで提供

データセット構成

  • 大規模言語モデルの燃料はテキストデータストリームであり、本番環境ではインターネット上のウェブページを使うが、microgptでは32,000件の名前を1行ずつ収めた単純な例を使用
  • 各名前は1つの"文書"として扱われ、モデルはデータ内の統計的パターンを学習して、似た新しい文書を生成することを目標とする
  • 学習完了後、モデルは"kamon"、"karai"、"vialan"のようなもっともらしい新しい名前を"幻覚(hallucinate)"する
  • ChatGPTの観点でも、ユーザーとの対話は"少し変わった形の文書"にすぎず、プロンプトで文書を初期化するとモデルの応答は統計的な文書補完に当たる

トークナイザー

  • ニューラルネットワークは文字ではなく数値で動作するため、テキストを整数トークンID列に変換し、再び復元する方法が必要
  • tiktoken(GPT-4で使用)のような本番用トークナイザーは効率のため文字チャンク単位で動作するが、最も単純なトークナイザーはデータセット内の各固有文字に1つの整数を割り当てる
  • 小文字a-zを並べて各文字にインデックスとしてIDを付与し、整数値そのものに意味はなく、各トークンは別個の離散シンボル
  • BOS(Beginning of Sequence) 特殊トークンを追加して「新しい文書が開始/終了した」ことを知らせ、"emma"は [BOS, e, m, m, a, BOS] でラップされる
  • 最終語彙サイズは27個(小文字26個 + BOS 1個)

自動微分(Autograd)

  • ニューラルネットワークの学習には勾配が必要。各パラメータについて「この値を少し上げると損失は上がるのか、下がるのか、どれくらいか?」を知る必要がある
  • 計算グラフは多くの入力(モデルパラメータと入力トークン)を持つが、単一スカラー出力である損失(loss) に収束する
  • 逆伝播(Backpropagation) は出力から始めてグラフを逆方向にたどり、微積分の連鎖律に依存してすべての入力に対する損失の勾配を計算
  • Valueクラスとして実装。各Valueは単一のスカラー(.data)を包み、どのように計算されたかを追跡
    • 加算、乗算などの演算時に新しいValueが入力(_children)と、その演算の局所導関数(_local_grads) を記憶
    • 例: __mul__ は ∂(a·b)/∂a=b、∂(a·b)/∂b=a を記録
  • サポートする演算ブロック: 加算、乗算、累乗、log、exp、ReLU
  • backward() メソッドが逆位相順序でグラフを走査し、各段階で連鎖律を適用
    • 損失ノードで self.grad = 1 から開始(∂L/∂L=1)
    • 局所勾配を経路に沿って掛け合わせながらパラメータまで伝播
  • +=で累積(代入ではない)。グラフが分岐すると各分岐から独立して勾配が流れ込み、合算されなければならない(多変数連鎖律の結果)
  • PyTorchの.backward()アルゴリズム的には同一だが、テンソルの代わりにスカラー単位で動作するため、はるかに単純な一方で効率は低い

パラメータ初期化

  • パラメータはモデルの知識であり、ランダムに始まり、学習中に繰り返し最適化される浮動小数点数の大規模な集合
  • ガウス分布から小さなランダム値で初期化
  • state_dict として命名された行列群で構成: 埋め込みテーブルアテンション重みMLP重み最終出力プロジェクション
  • ハイパーパラメータ設定:
    • n_embd = 16: 埋め込み次元
    • n_head = 4: アテンションヘッド数
    • n_layer = 1: レイヤー数
    • block_size = 16: 最大シーケンス長
  • 小型モデル基準で4,192個のパラメータ(GPT-2は16億個、現代のLLMは数千億個)

アーキテクチャ

  • モデルアーキテクチャは状態を持たない関数: トークン、位置、パラメータ、前の位置のキャッシュ済みキー/値を受け取り、次のトークンに対する**ロジット(スコア)**を返す
  • GPT-2に従いつつ少し単純化: RMSNorm(LayerNormの代わり)、バイアスなし、ReLU(GeLUの代わり)
  • ヘルパー関数

    • linear: 行列とベクトルの積により、重み行列の各行に対して1つの内積を計算する、ニューラルネットワークの基本構成要素である学習済み線形変換
    • softmax: 生のスコア(ロジット)を確率分布に変換し、すべての値が[0,1]の範囲に入り合計が1になるようにする、数値安定性のために先に最大値を引く
    • rmsnorm: ベクトルの二乗平均平方根が1になるように再スケーリングし、活性化がネットワークを通る中で大きくなったり小さくなったりするのを防ぎ、学習を安定化させる
  • モデル構造

    • 埋め込み: トークンIDと位置IDがそれぞれの埋め込みテーブル(wte, wpe)の行を参照し、2つのベクトルを足し合わせることで、トークンが何であるかとシーケンス内のどこにあるかを同時にエンコードする
      • 現代のLLMは位置埋め込みを省略し、RoPEのような相対位置ベースの手法を使うことが多い
    • アテンションブロック: 現在のトークンをQ(クエリ)、K(キー)、V(バリュー)の3つのベクトルに射影する
      • クエリ: 「自分が探しているものは?」、キー: 「自分が持っているものは?」、バリュー: 「選ばれたら何を提供するか?」
      • 例: "emma"で2つ目の"m"が次を予測するとき、「直前にどんな母音があったか?」のようなクエリを学習でき、先頭の"e"がこのクエリによく一致して高いアテンション重みを得る
      • キーとバリューはKVキャッシュに追加され、以前の位置を参照できる
      • 各アテンションヘッドは、クエリとキャッシュ済みのすべてのキーの間の内積(√d_headでスケーリング)を計算し、softmaxでアテンション重みを得て、キャッシュ済みバリューの加重和を計算する
      • すべてのヘッド出力を連結して attn_wo に射影する
      • アテンションブロックは、位置tのトークンが過去0..t-1のトークンを「見る」ことができる唯一の場所であり、アテンションはトークン間の通信メカニズム
    • MLPブロック: 2層のフィードフォワードネットワーク: 埋め込み次元の4倍に拡張 → ReLUを適用 → 再び縮小
      • 位置ごとの「思考」の大部分がここで行われる
      • アテンションと異なり、時刻tにおいて完全にローカルな計算
      • Transformerは通信(アテンション)と計算(MLP)を交互に配置する
    • 残差接続: アテンションブロックとMLPブロックはどちらも出力を入力に加え戻す
      • 勾配がネットワークを直接通過できるようにし、深いモデルでも学習可能にする
    • 出力: 最終隠れ状態を lm_head で語彙サイズに射影し、トークンごとに1つのロジットを生成する(ここでは27個の数値)、ロジットが高いほどそのトークンが次に来る可能性が高い
    • KVキャッシュの特異点: 学習中にKVキャッシュを使うのはまれだが、microgptは一度に1トークンしか処理しないため明示的に構築する必要があり、キャッシュされたキーとバリューは計算グラフ内の生きたValueノードとして逆伝播の対象になる

学習ループ

  • 学習ループは繰り返し: (1) 文書を選択 → (2) トークンに対してモデルを順伝播実行 → (3) 損失を計算 → (4) 逆伝播で勾配を取得 → (5) パラメータを更新
  • トークン化

    • 各学習ステップで1つの文書を選び、両端をBOSで囲む: "emma" → [BOS, e, m, m, a, BOS]
    • モデルの目標は、以前のトークンが与えられたときに各次トークンを予測すること
  • 順伝播と損失

    • トークンを1つずつモデルに入力しながらKVキャッシュを構築する
    • 各位置でモデルは27個のロジットを出力し、softmaxで確率に変換する
    • 各位置の損失は、正しい次トークンの負の対数確率: −log p(target) で、これをクロスエントロピー損失と呼ぶ
    • 損失は、モデルが実際に来るものにどれだけ驚いたかを測る: 確率1.0を割り当てれば損失は0、確率が0に近ければ損失は+∞
    • 文書全体の各位置の損失を平均して単一のスカラー損失を得る
  • 逆伝播

    • loss.backward() を1回呼ぶだけで、計算グラフ全体に対する逆伝播が実行される
    • その後、各パラメータの .grad が、損失を減らすためにどう変更すべきかを示す
  • Adamオプティマイザ

    • 単純な勾配降下(p.data -= lr * p.grad)の代わりにAdamを使う
    • パラメータごとに2つの移動平均を維持する:
      • m: 最近の勾配の平均(モメンタム)
      • v: 最近の勾配二乗の平均(パラメータごとの学習率適応)
    • m_hat, v_hat は、0で初期化されたmとvに対するバイアス補正
    • 学習率は学習中に線形に減少する
    • 更新後は .grad = 0 で初期化する
  • 学習結果

    • 1,000ステップの間に損失は約3.3(27トークンからのランダム推測: −log(1/27)≈3.3)から約2.37まで低下
    • 低いほど良く、最小は0(完全予測)なのでまだ改善の余地はあるが、モデルが名前の統計的パターンを学習していることは明らか

推論

  • 学習完了後はモデルから新しい名前をサンプリングでき、パラメータを固定したうえで順伝播をループ実行し、各生成トークンを次の入力としてフィードバックする
  • サンプリング過程

    • 各サンプルをBOSトークンで開始する(「新しい名前の開始」)
    • モデルが27個のロジットを生成 → 確率に変換 → その確率に従ってランダムに1つのトークンをサンプリング
    • そのトークンを次の入力としてフィードバックし、モデルが再びBOSを生成して「終了」するか、最大シーケンス長に達するまで繰り返す
  • 温度(Temperature)

    • softmaxの前にロジットを温度で割る
    • 温度1.0: モデルが学習した分布からそのままサンプリング
    • 低い温度(例: 0.5): 分布を鋭くし、モデルがより保守的に上位候補を選びやすくする
    • 温度0に近い: 常に最も確率の高い単一トークンを選ぶ(貪欲デコーディング
    • 高い温度: 分布を平坦化し、より多様だが一貫性の低い出力になる

実行方法

  • Pythonだけで動作(pip install不要、依存関係なし): python train.py
  • MacBookで約1分
  • 各ステップで損失を出力: 約3.3(ランダム)から約2.37へ低下
  • 学習完了後はそれらしく見える新しい名前を生成: "kamon", "ann", "karai" など
  • Google Colabノートブックでも実行可能で、Geminiに質問できる
  • 別のデータセットを試したり、num_steps を増やして長く学習させたり、モデルサイズを大きくしてより良い結果を得たりできる

コードの進行段階

ファイル 追加内容
train0.py バイグラムのカウントテーブル — ニューラルネットワークなし、勾配なし
train1.py MLP + 手動勾配(数値的・解析的)+ SGD
train2.py Autograd(Valueクラス)— 手動勾配を置き換え
train3.py 位置埋め込み + 単一ヘッドアテンション + rmsnorm + 残差
train4.py マルチヘッドアテンション + レイヤーループ — 完全なGPTアーキテクチャ
train5.py Adamオプティマイザ — これが train.py
  • build_microgpt.py GistのRevisionsで、すべてのバージョンと各ステップ間のdiffを確認できる

本番LLMとの違い

  • microgptには、GPTの学習と実行における完全なアルゴリズム的本質が含まれており、ChatGPTのような本番LLMとの違いは、コアアルゴリズムを変えるものではなく、それを大規模に動かすための要素にある
  • データ

    • 32Kの短い名前の代わりに、数兆個のインターネットテキストトークン(Webページ、書籍、コードなど)で学習する
    • データの重複除去、品質フィルタリング、ドメイン間の慎重な混合
  • トークナイザー

    • 単一文字の代わりに、BPE (Byte Pair Encoding) のようなサブワードトークナイザーを使う
    • 頻繁に一緒に現れる文字列を単一トークンに結合し、"the" のような一般的な単語は単一トークン、まれな単語は断片に分割する
    • 約100Kトークンの語彙により、位置ごとにより多くの内容を見られるため、はるかに効率的
  • Autograd

    • 純粋なPythonのスカラーValueオブジェクトの代わりに、テンソル(大規模な多次元数値配列)を使い、毎秒数十億回の浮動小数点演算を実行するGPU/TPU上で動作する
    • PyTorchがテンソルに対するautogradを処理し、FlashAttentionのようなCUDAカーネルが複数の演算を融合する
    • 数学は同じで、多数のスカラーが並列処理される
  • アーキテクチャ

    • microgpt: 4,192個のパラメータ、GPT-4級モデル: 数千億個
    • 全体として非常によく似たTransformerニューラルネットワークだが、はるかに幅広く(埋め込み次元10,000+)、はるかに深い(100+レイヤー)
    • 追加のレゴブロック的な部品や順序変更:
      • RoPE(回転位置埋め込み)— 学習された位置埋め込みの代わり
      • GQA(Grouped Query Attention)— KVキャッシュサイズを削減
      • ゲート付き線形活性化 — ReLUの代わり
      • MoE(Mixture of Experts)レイヤー
    • 残差ストリームの上でAttention(通信)とMLP(計算)が交互に現れる中核構造はよく保たれている
  • 学習

    • ステップごとに1つの文書ではなく、大規模バッチ(ステップごとに数百万トークン)、勾配蓄積、混合精度(float16/bfloat16)、慎重なハイパーパラメータ調整を使う
    • フロンティアモデルの学習には、数千台のGPUを数か月間動かす
  • 最適化

    • microgpt: Adam + 単純な線形学習率減衰
    • 大規模では最適化は独立した専門分野であり、低精度化(bfloat16, fp8)や大規模GPUクラスターでの学習が行われる
    • オプティマイザ設定(学習率、重み減衰、ベータパラメータ、ウォームアップ/減衰スケジュール)は精密な調整が必要で、正しい値はモデルサイズ、バッチサイズ、データセット構成によって異なる
    • スケーリング則(例: Chinchilla)は、固定された計算予算をモデルサイズと学習トークン数の間でどう配分するかを導く
    • 大規模では、これらの詳細を誤ると数百万ドル分の計算資源を無駄にする可能性があり、チームは本番の学習実行前に広範な小規模実験を行う
  • 後学習(Post-training)

    • 学習から得られるベースモデル("事前学習"モデル)は、文書補完器であってチャットボットではない
    • ChatGPTにする過程は2段階:
      • SFT(教師ありファインチューニング): 文書をキュレーションされた対話に置き換えて学習を継続し、アルゴリズム的な変化はない
      • RL(強化学習): モデルが応答を生成 → 採点する(人間、"審判"モデル、アルゴリズム)→ そのフィードバックで学習する
    • 本質的には依然として文書に対して学習しているが、今度はその文書がモデル自身から出たトークンで構成される
  • 推論

    • 数百万人のユーザーにモデルを提供するには、専用のエンジニアリングスタックが必要: リクエストのバッチング、KVキャッシュの管理とページング(vLLMなど)、高速化のための投機的デコーディング、メモリ削減のための量子化(int8/int4で実行)、複数GPUへのモデル分散
    • 本質的には依然としてシーケンスの次のトークンを予測しているが、より速くするためのエンジニアリングに多大な努力が払われている

FAQ

  • モデルは何かを「理解」しているのか?

    • 哲学的な問いではあるが、機械的に見れば魔法は起きていない
    • モデルは入力トークンを次のトークンに対する確率分布へ写像する大きな数学関数である
    • 学習中、パラメータは正しい次トークンの確率をより高くするよう調整される
    • それを「理解」と呼ぶかは人それぞれだが、メカニズムは上の200行に完全に収まっている
  • なぜ動作するのか?

    • モデルには何千もの調整可能なパラメータがあり、オプティマイザが各ステップで損失を下げるよう少しずつ動かしていく
    • 多くのステップを経ることで、パラメータはデータの統計的規則性を捉える値へと安定する
    • 名前の場合なら、子音で始まることが多い、"qu" は一緒に現れやすい、子音が3つ連続するのはまれ、など
    • モデルは明示的なルールではなく、これを反映する確率分布を学習する
  • ChatGPTとどう関係しているのか?

    • ChatGPTは、この同じコアループ(次トークン予測、サンプリング、反復)を大規模に拡張し、さらに対話型にするための後学習を追加したもの
    • チャット時には、システムプロンプト、ユーザーメッセージ、応答はすべてシーケンスのトークンにすぎない
    • モデルは、microgptが名前を補完するのとまったく同じように、一度に1トークンずつ文書を補完する
  • 「幻覚」とは何か?

    • モデルは確率分布からサンプリングしてトークンを生成する
    • 真実という概念を持たず、学習データに照らして統計的にもっともらしいシーケンスだけを知っている
    • microgptが "karia" のような名前を「幻覚」するのは、ChatGPTが誤った事実を自信ありげに語るのと同じ現象である
    • どちらも実在しないもっともらしく聞こえる補完にすぎない
  • なぜこんなに遅いのか?

    • microgptは純粋なPythonで一度に1つのスカラーを処理するため、単一の学習ステップに数秒かかる
    • GPUでは同じ数学が数百万のスカラーを並列処理し、桁違いに高速で実行される
  • より良い名前を生成させられるか?

    • 可能: より長く学習する(num_steps を増やす)、モデルサイズを大きくする(n_embd, n_layer, n_head)、より大きなデータセットを使う
    • 大規模でも重要な同じ調整要素である
  • データセットを変えたら?

    • モデルはデータ内のあらゆるパターンを学習する
    • 都市名、ポケモン名、英単語、短い詩のファイルに置き換えれば、代わりにそれらを生成するよう学習する
    • それ以外のコードは変更不要

1件のコメント

 
mhj5730 2026-02-19

良い記事をありがとうございます。