Skip to content
Published on

LLMをゼロから作ってみる — スタンフォードCS336流の学習ロードマップ

Authors

はじめに — なぜ今、ゼロから作るのか

2026年上半期、Hacker NewsとGeekNewsで最も安定して上位に登場する教育コンテンツがあります。スタンフォードのCS336、Language Modeling from Scratchです。講義資料と課題はすべて公開されており、受講生は外部ライブラリの高レベル抽象に頼らず、トークナイザー、トランスフォーマー、学習ループ、推論エンジンをすべて自分の手で実装します。

興味深いのはそのタイミングです。2026年はAIコーディングエージェントが当たり前になった年です。Claude CodeやCodexのようなエージェントが数時間規模の自律タスクをこなし、プロンプトエンジニアリングという言葉はコンテキストエンジニアリングやループエンジニアリングというキーワードに置き換わりつつあります。自分でモデルを作る必要性がかつてないほど薄れて見える時代に、むしろゼロから作る講義が爆発的な人気を集めているのです。

本記事ではCS336流のカリキュラムを段階ごとに解剖し、それぞれの段階で何を学ぶのか、API時代になぜそれが重要なのか、そして独学でも追える20週間プランを整理します。

カリキュラム全体を一望する

CS336流カリキュラムは大きく6本の柱で構成されます。

+------------------+    +------------------+    +--------------------+
| 1. トークナイザー  | -> | 2. アーキテクチャ  | -> | 3. 学習インフラ      |
|  BPEをゼロから    |    |  Transformer     |    |  データ / 分散学習   |
+------------------+    +------------------+    +--------------------+
                                                        |
                                                        v
+------------------+    +------------------+    +--------------------+
| 6. 推論最適化     | <- | 5. アライメント    | <- | 4. スケーリング則    |
|  KV cache        |    |  SFT/RLHF/DPO    |    |  予算配分           |
+------------------+    +------------------+    +--------------------+

順序が重要です。トークナイザーがなければデータはなく、アーキテクチャがなければ学習する対象がなく、インフラがなければ意味のある規模で学習できません。スケーリング則は予算の使い道を教えてくれ、アライメントはベースモデルを役立つものにし、推論最適化は作ったモデルを実際にサービングできるようにします。

ステージ1 — トークナイザー:BPEを自分で実装する

すべてのLLMの入り口はトークナイザーです。CS336の最初の課題もBPE(Byte Pair Encoding)トークナイザーをゼロから実装することです。概念は単純で、最も頻繁に現れる隣接バイトペアを繰り返しマージして語彙を育てていきます。

from collections import Counter

def get_pair_counts(corpus):
    """トークン列のリストから隣接ペアの頻度を数える。"""
    counts = Counter()
    for tokens in corpus:
        for a, b in zip(tokens, tokens[1:]):
            counts[(a, b)] += 1
    return counts

def merge_pair(corpus, pair, new_token):
    """corpus全体でpairをnew_tokenにマージする。"""
    merged = []
    for tokens in corpus:
        out, i = [], 0
        while i < len(tokens):
            if i < len(tokens) - 1 and (tokens[i], tokens[i + 1]) == pair:
                out.append(new_token)
                i += 2
            else:
                out.append(tokens[i])
                i += 1
        merged.append(out)
    return merged

def train_bpe(text, vocab_size):
    # バイトレベルから開始:基本語彙は256個
    corpus = [list(text.encode("utf-8"))]
    merges = {}
    next_id = 256
    while next_id < vocab_size:
        counts = get_pair_counts(corpus)
        if not counts:
            break
        best = counts.most_common(1)[0][0]
        merges[best] = next_id
        corpus = merge_pair(corpus, best, next_id)
        next_id += 1
    return merges

この素朴な実装を実際のコーパスで走らせると、すぐに最初の教訓を得ます。遅すぎるのです。数ギガバイトのコーパスでマージのたびに全体を再スキャンすると数日かかります。そこで優先度付きキューと転置インデックスによる差分更新を実装することになり、その過程でトークナイザーが単なる前処理ではなく、それ自体がシステムエンジニアリングの問題であることを体感します。

自分で実装して学べることは次の通りです。

  • バイトレベルBPEがUnicodeの問題(日本語、絵文字)をどう回避するか
  • 語彙サイズがシーケンス長と埋め込みテーブルサイズに与えるトレードオフ
  • なぜ数字や空白の扱いのルールひとつが下流タスクの性能を左右するのか

ステージ2 — トランスフォーマー:アテンションをコードで理解する

数式で見ると難しく感じるセルフアテンションも、コードで見れば行列積が数回あるだけです。スケールドドット積アテンションの本質をPyTorchで表現するとこうなります。

import math
import torch
import torch.nn as nn
import torch.nn.functional as F

class CausalSelfAttention(nn.Module):
    def __init__(self, d_model, n_head):
        super().__init__()
        assert d_model % n_head == 0
        self.n_head = n_head
        self.head_dim = d_model // n_head
        self.qkv = nn.Linear(d_model, 3 * d_model, bias=False)
        self.proj = nn.Linear(d_model, d_model, bias=False)

    def forward(self, x):
        B, T, C = x.shape
        q, k, v = self.qkv(x).split(C, dim=2)
        # (B, T, C) -> (B, n_head, T, head_dim)
        q = q.view(B, T, self.n_head, self.head_dim).transpose(1, 2)
        k = k.view(B, T, self.n_head, self.head_dim).transpose(1, 2)
        v = v.view(B, T, self.n_head, self.head_dim).transpose(1, 2)

        # アテンションスコア = Q @ K^T / sqrt(head_dim)
        scores = q @ k.transpose(-2, -1) / math.sqrt(self.head_dim)
        # 因果マスク:未来のトークンを見ないように
        mask = torch.triu(torch.ones(T, T, device=x.device), diagonal=1).bool()
        scores = scores.masked_fill(mask, float("-inf"))
        attn = F.softmax(scores, dim=-1)

        out = attn @ v  # (B, n_head, T, head_dim)
        out = out.transpose(1, 2).contiguous().view(B, T, C)
        return self.proj(out)

ここにRMSNorm、SwiGLUフィードフォワード、RoPE(回転位置埋め込み)を加えれば、2026年基準の標準的なデコーダーブロックが完成します。自分で作って初めて体感できるポイントがあります。

  • 因果マスクの一行が言語モデルとエンコーダーを分ける決定的な違いであること
  • pre-normとpost-normの違いが学習の安定性に与える影響
  • アテンションのメモリ使用量がシーケンス長の二乗に比例することを、OOMエラーで身をもって確認する経験

CS336はここで止まらず、FlashAttentionのアイデア(タイリングによってアテンション行列をメモリ上に作らない)までTritonカーネルで実装させます。この段階でGPUのメモリ階層に対する感覚が育ちます。

ステージ3 — 学習インフラ:データパイプラインと分散学習

モデルのコードは全作業の2割にすぎません。残りはデータとインフラです。

データパイプライン

Webクロールデータ(Common Crawl系)を学習可能なトークンストリームに変える過程は、それ自体がひとつのパイプラインです。

生のHTML
   |  テキスト抽出(ボイラープレート除去)
   v
言語フィルタリング(fastText分類器)
   |
   v
品質フィルタリング(ヒューリスティック + 分類器)
   |
   v
重複除去(MinHash / 完全一致)
   |
   v
トークン化 + シャッフル + 固定長チャンク
   |
   v
学習用バイナリシャード (.bin)

重複除去ひとつとってもMinHash LSHを実装する必要があり、品質フィルタの閾値ひとつで最終的なモデル性能が数パーセント変わります。データの仕事がモデルアーキテクチャより性能に大きく効くというのが、ここ数年の一貫した教訓です。

分散学習の基礎

単一GPUを超えた瞬間、分散学習が必要になります。押さえるべき核心概念は3つです。

手法何を分割するか通信コスト適した状況
DDP(データ並列)バッチ勾配のall-reduceモデルがGPU1枚に載るとき
FSDP / ZeROパラメータ+オプティマイザ状態all-gather, reduce-scatterモデルが1枚に載らないとき
テンソル並列行列積そのものレイヤーごとのall-reduce超大規模モデル、高速インターコネクト

ミニマルなDDP学習ループは次の通りです。

import torch.distributed as dist
from torch.nn.parallel import DistributedDataParallel as DDP

dist.init_process_group(backend="nccl")
rank = dist.get_rank()
torch.cuda.set_device(rank)

model = GPT(config).cuda(rank)
model = DDP(model, device_ids=[rank])
optimizer = torch.optim.AdamW(model.parameters(), lr=3e-4, weight_decay=0.1)

for step, batch in enumerate(loader):
    x, y = batch
    x, y = x.cuda(rank), y.cuda(rank)
    logits = model(x)
    loss = F.cross_entropy(logits.view(-1, logits.size(-1)), y.view(-1))
    loss.backward()
    torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0)
    optimizer.step()
    optimizer.zero_grad(set_to_none=True)

実際に回してみると、学習曲線が発散する経験を必ずします。学習率ウォームアップ、勾配クリッピング、bf16混合精度といった仕組みがなぜ存在するのかを、身体で学ぶ段階です。

ステージ4 — スケーリング則:予算をどこに使うか

スケーリング則は、計算予算が与えられたときにモデルサイズとデータ量をどう配分するかを教えてくれる経験則です。Chinchilla論文の核心的な結論は、パラメータ数と学習トークン数をおよそ1対20の比率にするのが計算最適だというものです。

def chinchilla_optimal(compute_budget_flops):
    """6ND近似を使ったおおまかなcompute-optimal配分。
    C = 6 * N * D、D = 20 * N を仮定。"""
    # C = 6 * N * (20 * N) = 120 * N^2
    n_params = (compute_budget_flops / 120) ** 0.5
    n_tokens = 20 * n_params
    return n_params, n_tokens

# 例:A100を8枚で2週間(およそ1.2e21 FLOPsを仮定)
params, tokens = chinchilla_optimal(1.2e21)
print(f"params: {params/1e9:.2f}B, tokens: {tokens/1e9:.0f}B")
# およそ3Bパラメータ、63Bトークン規模

小規模な実験でスケーリング曲線を自分で描いてみるのがCS336の醍醐味です。1M、10M、100Mパラメータのモデルを同じデータで学習させ、損失を両対数グラフにプロットすると、ほぼ直線が現れます。この直線を外挿してより大きなモデルの性能を予測するのは、フロンティアラボが実際にやっていることの縮図です。

注意すべきは、Chinchilla最適はあくまで学習計算だけの最適だという点です。推論コストまで考慮すると、小さいモデルを長く学習させる(オーバートレーニング)方が総コストで有利な場合が多く、実際、最近の公開モデルはChinchilla比率よりはるかに多くのトークンで学習されています。

ステージ5 — アライメント:SFT、RLHF、DPO

プリトレーニングを終えたベースモデルは、次トークン予測器にすぎません。指示に従わせるにはアライメントの段階が必要です。

ベースモデル
   |
   v
SFT(教師あり微調整)
   |  高品質な指示-応答ペアで学習
   v
選好最適化(RLHF または DPO)
   |  人間が好む応答の方向へ調整
   v
最終的なアシスタントモデル
  • SFTは単純です。指示-応答ペアを作り、通常の言語モデル損失で微調整します。データ品質がすべてです。
  • RLHFは報酬モデルを別に学習させ、PPOのような強化学習でポリシーを最適化します。強力ですが、実装難度と不安定性が高い手法です。
  • DPOは報酬モデルと強化学習ループをなくし、選好ペア(選ばれた応答と棄却された応答)で直接ポリシーを最適化します。

DPO損失の核心をコードで見るとこうなります。

def dpo_loss(policy_chosen_logps, policy_rejected_logps,
             ref_chosen_logps, ref_rejected_logps, beta=0.1):
    """選ばれた応答と棄却された応答の対数確率差を、
    参照モデル比で広げる方向に学習する。"""
    chosen_ratio = policy_chosen_logps - ref_chosen_logps
    rejected_ratio = policy_rejected_logps - ref_rejected_logps
    logits = beta * (chosen_ratio - rejected_ratio)
    return -F.logsigmoid(logits).mean()

自分で実装してみると、アライメントは魔法ではなく損失関数設計の問題であること、そしてベースモデルの品質と選好データの品質が結果の上限を決めることがわかります。

ステージ6 — 推論最適化:まずはKV cacheから

学習済みモデルをサービングした瞬間、まったく別のエンジニアリング問題が始まります。自己回帰生成はトークンを1つ作るたびに全シーケンスを再計算する構造ですが、KV cacheがこれを解決します。

class KVCache:
    def __init__(self, batch, n_head, max_len, head_dim, device):
        shape = (batch, n_head, max_len, head_dim)
        self.k = torch.zeros(shape, device=device, dtype=torch.bfloat16)
        self.v = torch.zeros(shape, device=device, dtype=torch.bfloat16)
        self.pos = 0

    def update(self, k_new, v_new):
        t = k_new.size(2)
        self.k[:, :, self.pos:self.pos + t] = k_new
        self.v[:, :, self.pos:self.pos + t] = v_new
        self.pos += t
        return self.k[:, :, :self.pos], self.v[:, :, :self.pos]

キャッシュを付けると、生成ステップの計算量はシーケンス長に対して二乗から線形に落ちます。代わりにメモリを消費します。このトレードオフを体感すると、GQA(クエリのグループ化)がなぜ登場したのか、vLLMのPagedAttentionがなぜ革新だったのかが自然に理解できます。推論最適化は別の記事で深掘りしますが、核心は、推論が学習とはまったく異なるボトルネック(メモリ帯域幅)を持つという点です。

自分で作って学べること vs API時代になぜ重要か

もっともな反論があります。エージェントがコードを書いてくれる時代に、なぜ手でアテンションを実装するのか。

第一に、デバッグと意思決定の質が変わります。コンテキスト長を伸ばすとコストがなぜ二乗で跳ね上がるのか、なぜトークナイザーのせいで数値計算に弱いタスクがあるのか、ファインチューニングとRAGのどちらを選ぶべきか。こうした実務判断は、内部構造を知っている人と知らない人とでまったく違うものになります。

第二に、AIエージェントを使いこなす能力そのものが内部理解に比例します。2026年の中核スキルとして浮上したコンテキストエンジニアリングは、結局のところモデルがコンテキストをどう消費するか(アテンション、位置埋め込み、KV cache)の理解の上でしか精緻になりません。

第三に、抽象化はいつか必ず漏れます(leaky abstraction)。APIの向こうに隠れたモデルも、トークナイザー境界、コンテキスト制限、サンプリングパラメータという形で内部構造を露呈します。ゼロから作った経験は、その漏れに出会ったとき慌てないための保険です。

どんな計算資源で始めるか

ゼロから学ぶ際の障壁としてGPUコストがよく挙げられますが、段階ごとに必要な計算資源は思ったより小さいものです。

段階必要な計算資源現実的な選択肢おおよその費用
トークナイザー、アテンション実装CPUで十分ローカルのノートPC無料
文字レベルLM、10MモデルGPU1枚(8GB VRAM)Colab無料/Pro、ローカルのゲーミングGPU無料〜月2千円程度
100MモデルのプリトレーニングGPU1枚(24GB VRAM)クラウドのスポットインスタンス数千円規模
1B級の実験、分散学習GPU2〜8枚時間課金のGPUクラウド実験あたり数万円
SFT、DPO(1B以下)GPU1枚(24GB VRAM)LoRA活用でさらに小さくも可能数千円規模

最初の8週間は実質無料で進められます。環境構築はこの程度で十分です。

# 仮想環境と主要な依存関係
python -m venv .venv
source .venv/bin/activate
pip install torch numpy tiktoken datasets wandb

# nanoGPTで最初の学習感覚をつかむ
git clone https://github.com/karpathy/nanoGPT.git
cd nanoGPT
python data/shakespeare_char/prepare.py
python train.py config/train_shakespeare_char.py

学習の進捗は最初からロギングツールで記録する習慣をつけるのがよいでしょう。損失曲線、学習率、勾配ノルムを毎実験残しておくと、後で発散の原因を追跡する際の大きな資産になります。

評価をサボらないこと

自分が作ったモデルは客観的に評価しなければなりません。最小限の評価セットアップは次の通りです。

@torch.no_grad()
def estimate_perplexity(model, loader, max_batches=50):
    model.eval()
    total_loss, n = 0.0, 0
    for i, (x, y) in enumerate(loader):
        if i >= max_batches:
            break
        logits = model(x.cuda())
        loss = F.cross_entropy(
            logits.view(-1, logits.size(-1)), y.cuda().view(-1)
        )
        total_loss += loss.item()
        n += 1
    model.train()
    return math.exp(total_loss / n)

perplexityは同じトークナイザーを使うモデル同士でしか比較できない点に注意してください。語彙が異なれば、トークンあたり損失の意味そのものが変わります。

20週間の学習プラン

週8〜10時間の投資を想定した独学プランです。

テーマ主な活動成果物
1環境構築とベースラインPyTorch、GPU環境、文字レベルLM文字レベルのバイグラムモデル
2BPEトークナイザー1素朴なBPE実装動く学習コード
3BPEトークナイザー2優先度キュー最適化、エンコード/デコード実用速度のトークナイザー
4アテンション基礎シングルヘッドアテンション、因果マスクアテンションモジュール
5トランスフォーマーブロックマルチヘッド、RMSNorm、SwiGLU、RoPEデコーダーブロック
6モデル全体の組み立てGPTクラス、重みの初期化10Mパラメータモデル
7学習ループAdamW、コサインスケジュール、クリッピング安定した学習曲線
8混合精度とプロファイリングbf16、torch.compile、スループット測定トークン/秒レポート
9データパイプライン1テキスト抽出、言語フィルタクリーニングスクリプト
10データパイプライン2MinHash重複除去、シャーディング学習用データセット
11中間プロジェクト100M級モデルのプリトレーニング自作ベースモデル
12スケーリング実験1M〜100Mの損失曲線フィッティングスケーリンググラフ
13分散学習1DDP、マルチGPU学習分散学習スクリプト
14分散学習2FSDPまたはZeROの概念と実験メモリ使用量の比較
15SFT指示データ構築、微調整指示に従うモデル
16DPO選好データ、DPO損失の実装アライメント済みモデル
17評価perplexity、ベンチマーク、LLM審判評価レポート
18推論最適化1KV cache、サンプリング実装自作生成エンジン
19推論最適化2バッチング、量子化の概念、速度測定レイテンシ/スループットレポート
20最終プロジェクトパイプライン全体の整理、ブログ執筆公開リポジトリ + 振り返り

ミニプロジェクトのアイデア

nanoGPTのような小さく完結したプロジェクトが、学習効率が最も高いです。

  1. ナノ・シェイクスピア:文字レベルモデルでシェイクスピア風テキストを生成。4時間で終わり、全体像がつかめます。
  2. 日本語BPEトークナイザー:日本語Wikipediaダンプで語彙32kのトークナイザーを作り、既存トークナイザーと圧縮率を比較。
  3. 自分だけのnanoGPTフォーク:RoPE、RMSNorm、SwiGLUを自分で追加し、オリジナルと損失曲線を比較。
  4. スケーリング・ミニラボ:3つのサイズのモデルで損失曲線をフィッティングし、10倍大きいモデルの損失を予測して実際に検証。
  5. DPO演習:公開選好データセットで1B以下のモデルをアライメントし、前後の応答をブラインド比較。
  6. ミニ推論サーバー:KV cacheと動的バッチングを備えた生成サーバーを自作し、同時リクエストのスループットを測定。

落とし穴と批判的視点

ゼロから学ぶアプローチにも罠があります。

  • 計算資源の幻想:自宅でフロンティア級モデルは作れません。目標は動作原理の体得であり、SOTAの再現ではありません。100M級のモデルでも学ぶべきことはすべて学べます。
  • 車輪の再発明への埋没:トークナイザー最適化に4週間使うのは本末転倒です。各段階は動くレベルで止めて次へ進む規律が必要です。
  • プロダクションコードとの距離:教育用の実装は単純化されています。実際のサービングにはvLLMのような実績あるエンジンを使うのが正解です。自作はそのエンジンを理解しチューニングするための土台です。
  • 履歴書効果の過大評価:ゼロから作った経験そのものより、その過程で生まれた文章とコードと測定結果が評価されます。成果物を公開する習慣が重要です。

おわりに

LLMをゼロから作ってみることは、2026年でも、いやむしろ2026年だからこそ、価値ある投資です。エージェントがコードを代わりに書いてくれる時代ほど、システムの底を知る人と知らない人の差は、抽象化レイヤーの下で静かに広がっていきます。CS336とnanoGPTという素晴らしい公開資料があるのですから、20週間プランの1週目から気軽に始めてみてください。文字レベルモデルが初めてそれらしい文章を吐き出す瞬間の感覚は、API呼び出しでは決して得られない種類のものです。

参考資料