
GPTをゼロから作って理解する完全ガイド
GPTをゼロから作って理解する完全ガイド ChatGPTを毎日使っているのに、「中身がどうなっているか全くわからない」という方は多いのではないでしょうか。ブラックボックスとして使うだけでは、プロンプトエンジニアリングの限界に気づかなかったり、AIツールの選定で迷ったりすることがあります。 OpenAIの元研究員で「nanoGPT」の作者でもあるAndrej Karpathyは、GPTをゼロからコードで実装する方法を丁寧に解説しています。本記事では、その学習アプローチをもとに、Transformerの核心を自分の手で実装しながら理解するためのロードマップをお届けします。 なぜ「作って理解する」アプローチが最強なのか AIの論文を読んでも、「なんとなくわかった気がする」で終わってしまう経験はありませんか?理論の理解だけでは、実際のモデル動作やトラブルシューティングには限界があります。 Karpathyが提唱する**「spelled out(手順を声に出して説明しながら実装する)」**アプローチの強みは次の3点です。 理解の穴を即座に発見できる:コードが動かない=理解が不完全な箇所 直感的な数値感覚が身につく:テンソルの形状変化を目で追える 論文と実装のギャップを埋められる:「Attention is All You Need」の数式がコードに対応する 小さなモデルでも本物のGPTと同じ原理で動く点が、この学習法の最大のメリットです。 実装の全体像:7つのステップ GPTのスクラッチ実装は、大きく以下のステップで進みます。 ステップ1:データの準備とトークン化 まず、テキストデータ(たとえばシェイクスピア全集)を読み込み、文字レベルのトークン化を行います。 1 2 3 4 5 6 7 8 9 10 11 12 # テキストを読み込み、ユニークな文字の語彙を作成 with open('input.txt', 'r', encoding='utf-8') as f: text = f.read() chars = sorted(list(set(text))) vocab_size = len(chars) # 文字→整数、整数→文字の変換辞書 stoi = { ch:i for i,ch in enumerate(chars) } itos = { i:ch for i,ch in enumerate(chars) } encode = lambda s: [stoi[c] for c in s] decode = lambda l: ''.join([itos[i] for i in l]) ここで重要なのは、トークン化の粒度がモデルの語彙サイズと学習効率に直結するという点。実際のGPT-4はByte Pair Encoding(BPE)という手法で数万規模の語彙を扱っています。 ステップ2:バイグラムモデルで最初のベースラインを作る 最初からTransformerを実装するのではなく、まずバイグラム言語モデル(前の1文字だけを見て次の文字を予測する)をベースラインとして構築します。これにより、言語モデルの損失計算や生成ロジックの「型」を習得できます。 ステップ3:セルフアテンションの核心を実装する GPTの心臓部がセルフアテンションです。Karpathyは4段階で段階的に実装を発展させていきます。 for loopによる素朴な平均化(過去のトークンを単純平均) 行列積による高速化(同じ計算をベクトル演算で) Softmaxの導入(重みの正規化) Key・Query・Valueによる本物のアテンション 最終的なセルフアテンションの計算式は次のようになります。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 # 1ヘッドのセルフアテンション class Head(nn.Module): def __init__(self, head_size): super().__init__() self.key = nn.Linear(n_embd, head_size, bias=False) self.query = nn.Linear(n_embd, head_size, bias=False) self.value = nn.Linear(n_embd, head_size, bias=False) self.register_buffer('tril', torch.tril(torch.ones(block_size, block_size))) def forward(self, x): B, T, C = x.shape k = self.key(x) # (B, T, head_size) q = self.query(x) # (B, T, head_size) # アテンションスコアの計算(スケーリングあり) wei = q @ k.transpose(-2, -1) * k.shape[-1]**-0.5 wei = wei.masked_fill(self.tril[:T, :T] == 0, float('-inf')) wei = F.softmax(wei, dim=-1) v = self.value(x) return wei @ v **「なぜsqrt(head_size)で割るのか?」**という疑問が生まれたら、それが深い理解への入口です。答えは「head_sizeが大きいとdot productの分散が増大し、softmaxが極端に尖ってしまうから」。こういった「なぜ?」をコードと数式で確認できるのが、スクラッチ実装の醍醐味です。 ...
