Skip to content
Published on

自分だけのGPTを作る — nanoGPTでゼロから学習する言語モデル

Authors
  • Name
    Twitter
Build Your Own GPT

はじめに

ChatGPT、Claude、Gemini — 私たちが毎日使うAIの核心は**GPT(Generative Pre-trained Transformer)**アーキテクチャです。しかし、これを自分で作ったことはありますか?

このシリーズでは、Andrej KarpathyのnanoGPTをベースに、自宅のGPUで言語モデルをゼロから学習します。大規模モデルの縮小版ですが、原理はGPT-4と完全に同じです。

なぜ自分で作るべきか?

  • 論文を読むだけでは30%の理解、自分でコーディングすれば90%の理解
  • GPUサーバーがあるので実際に学習可能(GB10 128GB!)
  • 履歴書に「LLMをゼロから学習した経験」— 差別化ポイント
  • AI論文を読む際に「あ、この部分!」という直感が身につく

GPTアーキテクチャの核心

GPTはDecoder-only Transformerです。核心となる3要素:

1. トークン化(Tokenization)

テキストを数値に変換する最初のステップです。

# Character-levelトークナイザー(最もシンプル)
text = "hello world"
chars = sorted(list(set(text)))
# chars = [' ', 'd', 'e', 'h', 'l', 'o', 'r', 'w']

stoi = {ch: i for i, ch in enumerate(chars)}  # char → int
itos = {i: ch for i, ch in enumerate(chars)}  # int → char

encode = lambda s: [stoi[c] for c in s]
decode = lambda l: ''.join([itos[i] for i in l])

print(encode("hello"))  # [3, 2, 4, 4, 5]
print(decode([3, 2, 4, 4, 5]))  # "hello"

実際のGPTでは**BPE(Byte-Pair Encoding)**トークナイザーを使用します:

import tiktoken
enc = tiktoken.get_encoding("gpt2")
tokens = enc.encode("自分だけのGPTを作ろう!")
print(tokens)  # [171, 120, 230, 168, 245, ...]
print(len(tokens))  # ~15 tokens

2. Self-Attention(核心中の核心)

「各トークンが他のすべてのトークンにどれだけ注目するか」を学習します。

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

class SelfAttention(nn.Module):
    def __init__(self, embed_dim, head_dim):
        super().__init__()
        self.query = nn.Linear(embed_dim, head_dim, bias=False)
        self.key = nn.Linear(embed_dim, head_dim, bias=False)
        self.value = nn.Linear(embed_dim, head_dim, bias=False)

    def forward(self, x):
        B, T, C = x.shape
        q = self.query(x)  # (B, T, head_dim)
        k = self.key(x)    # (B, T, head_dim)
        v = self.value(x)  # (B, T, head_dim)

        # Attention scores
        weights = q @ k.transpose(-2, -1)  # (B, T, T)
        weights = weights * (C ** -0.5)     # Scale

        # Causal mask — 未来のトークンは見えない!
        mask = torch.tril(torch.ones(T, T))
        weights = weights.masked_fill(mask == 0, float('-inf'))

        weights = F.softmax(weights, dim=-1)
        out = weights @ v  # (B, T, head_dim)
        return out

核心的な直感: "The cat sat on the ___" の空欄を予測する際、"cat"と"sat"に高いattention weightが付与されます。

3. Transformer Block

class TransformerBlock(nn.Module):
    def __init__(self, embed_dim, num_heads):
        super().__init__()
        head_dim = embed_dim // num_heads
        self.heads = nn.ModuleList([
            SelfAttention(embed_dim, head_dim)
            for _ in range(num_heads)
        ])
        self.proj = nn.Linear(embed_dim, embed_dim)
        self.ffn = nn.Sequential(
            nn.Linear(embed_dim, 4 * embed_dim),
            nn.GELU(),
            nn.Linear(4 * embed_dim, embed_dim),
        )
        self.ln1 = nn.LayerNorm(embed_dim)
        self.ln2 = nn.LayerNorm(embed_dim)

    def forward(self, x):
        # Multi-Head Attention + Residual
        attn_out = torch.cat([h(self.ln1(x)) for h in self.heads], dim=-1)
        x = x + self.proj(attn_out)
        # Feed-Forward + Residual
        x = x + self.ffn(self.ln2(x))
        return x

完全なGPTモデル

class MicroGPT(nn.Module):
    def __init__(self, vocab_size, embed_dim=384, num_heads=6,
                 num_layers=6, block_size=256):
        super().__init__()
        self.token_emb = nn.Embedding(vocab_size, embed_dim)
        self.pos_emb = nn.Embedding(block_size, embed_dim)
        self.blocks = nn.Sequential(*[
            TransformerBlock(embed_dim, num_heads)
            for _ in range(num_layers)
        ])
        self.ln_f = nn.LayerNorm(embed_dim)
        self.head = nn.Linear(embed_dim, vocab_size)

    def forward(self, idx, targets=None):
        B, T = idx.shape
        tok_emb = self.token_emb(idx)          # (B, T, embed_dim)
        pos_emb = self.pos_emb(torch.arange(T)) # (T, embed_dim)
        x = tok_emb + pos_emb

        x = self.blocks(x)
        x = self.ln_f(x)
        logits = self.head(x)  # (B, T, vocab_size)

        loss = None
        if targets is not None:
            loss = F.cross_entropy(
                logits.view(-1, logits.size(-1)),
                targets.view(-1)
            )
        return logits, loss

    def generate(self, idx, max_new_tokens):
        for _ in range(max_new_tokens):
            logits, _ = self(idx[:, -256:])  # block_sizeの制限
            probs = F.softmax(logits[:, -1, :], dim=-1)
            next_token = torch.multinomial(probs, num_samples=1)
            idx = torch.cat([idx, next_token], dim=1)
        return idx

モデルサイズ比較:

モデルパラメータ数レイヤー学習時間(1 GPU)
私たちのMicroGPT10M6約30分
GPT-2 Small124M12数日
GPT-3175B96数千GPU日
GPT-4~1.8T(推定)??

実践: Shakespeareで学習する

データ準備

# spark01(GB10 128GB)で実行
cd ~/nanoGPT
python3 data/shakespeare_char/prepare.py
# → train: 1,003,854 tokens / val: 111,540 tokens

学習開始

python3 train.py config/train_shakespeare_char.py \
  --device=cuda \
  --max_iters=5000 \
  --eval_interval=500 \
  --log_interval=100

生成結果(5000イテレーション後)

ROMEO:
What say you to this? Let me not stay a whit;
And yet I feel the thing I have forgot
To take upon the honour of my word.

シェイクスピアスタイルのテキストをゼロから生成します!

次のステップ: 日本語GPT

  1. 日本語トークナイザーの学習(SentencePiece BPE)
  2. Wikipedia / ニュースデータの収集
  3. MicroGPT 500Mの学習(spark01 128GB)
  4. LoRAファインチューニングで対話型モデルへ

シリーズロードマップ

テーマ状態
1編nanoGPTでGPTを理解する(この記事)
2編日本語トークナイザーを作る🔜
3編500M日本語GPTの学習🔜
4編RLHFで対話型モデルを作る🔜
5編画像生成モデル(DDPM)をゼロから🔜

クイズ — 自分だけのGPT(クリックして確認!)

Q1. GPTはEncoderとDecoderのどちらの構造か? ||Decoder-only Transformer||

Q2. Self-AttentionにおけるCausal Maskの役割は? ||未来のトークンを見えないようにブロックする — autoregressiveな生成のために下三角行列でマスキング||

Q3. Attention score計算時にsqrt(d_k)で割る理由は? ||内積の値が大きくなるとsoftmaxが極端になりgradient vanishingが発生する — スケーリングで安定化||

Q4. BPEトークナイザーとCharacter-levelトークナイザーの長所と短所は? ||BPE: 語彙効率が良い(短いシーケンス)、ただし実装が複雑。Character-level: シンプルだがシーケンスが長くなり、長距離依存性の学習が困難||

Q5. Residual Connection(残差接続)がTransformerで重要な理由は? ||深いネットワークでgradientが消失しないよう元の入力を加算する — 学習の安定性と収束速度の向上||

Q6. Feed-Forward Networkで4倍に拡張してから再び縮小する理由は? ||非線形変換のための表現力確保 — より広い空間で特徴を抽出した後、元の次元に圧縮する||

Q7. 私たちのMicroGPTのパラメータ数は約いくつで、GPT-2 Smallとは何倍の差があるか? ||約10M、GPT-2 Small(124M)と約12倍の差||

Q8. next token predictionだけの学習で言語を理解できる理由は? ||次のトークンを正確に予測するには文法、意味、文脈、世界知識まで内在的に学習する必要がある — 圧縮こそが理解である||

参考資料 & GitHubリファレンス

ソースコード

原論文

動画講義

関連シリーズ & おすすめ記事