GPTのアーキテクチャと自己回帰生成を解説

GPT(Generative Pre-trained Transformer)は、OpenAI が提案した自己回帰型言語モデルです。Transformer の Decoder 部分を積層し、左から右への一方向の文脈に基づいてテキストを逐次的に生成します。GPT-1(2018年)から GPT-2(2019年)、GPT-3(2020年)へとスケーリングが進み、特に GPT-3 は1750億パラメータという巨大なモデルで、プロンプトに基づくタスク遂行能力(In-context Learning)を示し、大規模言語モデル(LLM)時代の幕を開けました。

BERT が双方向の Encoder で「理解」に優れるのに対し、GPT は自己回帰の Decoder で「生成」に優れるという対照的な特性を持ちます。本記事では、GPT のアーキテクチャを数式レベルで解説し、テキスト生成戦略の数学的定式化、BERT との比較を経て、Python で小規模な GPT をスクラッチ実装します。

本記事の内容

  • GPT の位置づけ(自己回帰型言語モデル)
  • Masked Self-Attention による Transformer Decoder のアーキテクチャ
  • 言語モデルの目的関数の定式化
  • 因果マスクの数学的定義と必要性
  • GPT-1 / 2 / 3 のスケーリング
  • テキスト生成戦略(Greedy / Top-k / Top-p / Temperature)の数学的定式化
  • BERT との比較(双方向 vs 自己回帰)
  • In-context Learning / Few-shot Learning の概要
  • Python で小規模 GPT のスクラッチ実装とテキスト生成デモ

前提知識

この記事を読む前に、以下の記事を読んでおくと理解が深まります。

GPT の位置づけ

自己回帰言語モデルとは

自己回帰言語モデル(autoregressive language model)は、テキスト系列 $\bm{x} = (x_1, x_2, \dots, x_T)$ の同時確率を、条件付き確率の積として分解します。

$$ P(\bm{x}) = P(x_1, x_2, \dots, x_T) = \prod_{t=1}^{T} P(x_t \mid x_1, \dots, x_{t-1}) $$

これは確率の連鎖律そのものであり、何の近似もありません。自己回帰モデルは、各ステップで「これまでの文脈から次のトークンを予測する」というタスクを学習します。

GPT の核心的なアイデア

GPT の核心は、この自己回帰言語モデルを Transformer の Decoder で実現し、大規模コーパスで事前学習(pre-training)した後、下流タスクにファインチューニングするという枠組みです。

  1. 事前学習: 大量のテキストデータで言語モデルの目的関数を最適化
  2. ファインチューニング: タスク固有のラベル付きデータで微調整

GPT-2 以降は、ファインチューニングなしで「プロンプト」だけでタスクを解く Zero-shot / Few-shot Learning が強調されるようになりました。

GPT のアーキテクチャ

全体構造

GPT は Transformer の Decoder のみ を使用します(原論文の Transformer における Encoder-Decoder 構造の Decoder 部分ではなく、Cross-Attention を除いた Decoder ブロック)。つまり、Masked Self-Attention + FFN の構成です。

モデル レイヤー数 $L$ 隠れ次元 $d_{\text{model}}$ ヘッド数 $A$ パラメータ数
GPT-1 12 768 12 117M
GPT-2 (Small) 12 768 12 117M
GPT-2 (Medium) 24 1024 16 345M
GPT-2 (Large) 36 1280 20 774M
GPT-2 (XL) 48 1600 25 1.5B
GPT-3 96 12288 96 175B

Masked Self-Attention(因果的自己注意)

GPT の自己注意機構は、各トークンが「未来のトークンを見ない」ように因果マスク(causal mask)を適用します。

入力系列の隠れ状態 $\bm{H}^{(l-1)} \in \mathbb{R}^{T \times d_{\text{model}}}$ に対して、Query, Key, Value を計算します。

$$ \bm{Q} = \bm{H}^{(l-1)} \bm{W}^Q, \quad \bm{K} = \bm{H}^{(l-1)} \bm{W}^K, \quad \bm{V} = \bm{H}^{(l-1)} \bm{W}^V $$

アテンションスコアの計算に因果マスクを適用します。

$$ \text{CausalAttention}(\bm{Q}, \bm{K}, \bm{V}) = \text{softmax}\left(\frac{\bm{Q}\bm{K}^\top}{\sqrt{d_k}} + \bm{M}\right) \bm{V} $$

因果マスクの数学的定義

因果マスク $\bm{M} \in \mathbb{R}^{T \times T}$ は以下のように定義されます。

$$ M_{ij} = \begin{cases} 0 & \text{if } i \geq j \quad (\text{現在以前のトークンを参照可能}) \\ -\infty & \text{if } i < j \quad (\text{未来のトークンを参照不可}) \end{cases} $$

行列形式で書くと

$$ \bm{M} = \begin{pmatrix} 0 & -\infty & -\infty & \cdots & -\infty \\ 0 & 0 & -\infty & \cdots & -\infty \\ 0 & 0 & 0 & \cdots & -\infty \\ \vdots & \vdots & \vdots & \ddots & \vdots \\ 0 & 0 & 0 & \cdots & 0 \end{pmatrix} $$

なぜ因果マスクが必要か

自己回帰言語モデルでは $P(x_t \mid x_1, \dots, x_{t-1})$ を学習します。もし位置 $t$ のトークンが位置 $t+1, t+2, \dots$ の情報を参照できてしまうと、「答えを見てから予測する」ことになり、学習が意味をなしません。

マスクを適用した後のアテンション重みを確認します。$\bm{S} = \bm{Q}\bm{K}^\top / \sqrt{d_k}$ として

$$ \text{softmax}(S_{ij} + M_{ij}) = \begin{cases} \frac{\exp(S_{ij})}{\sum_{k \leq i} \exp(S_{ik})} & \text{if } j \leq i \\ 0 & \text{if } j > i \end{cases} $$

$-\infty$ を加えることで $\exp(-\infty) = 0$ となり、未来のトークンへのアテンション重みが完全にゼロになります。

BERT のアテンションとの比較

BERT(Encoder)はマスクなしの Self-Attention を使用するため、各トークンが全位置を参照できます。

$$ \text{BERT}: \quad A_{ij} = \frac{\exp(S_{ij})}{\sum_{k=1}^{T} \exp(S_{ik})} \quad \text{for all } i, j $$

$$ \text{GPT}: \quad A_{ij} = \begin{cases} \frac{\exp(S_{ij})}{\sum_{k=1}^{i} \exp(S_{ik})} & \text{if } j \leq i \\ 0 & \text{otherwise} \end{cases} $$

Position-wise Feed-Forward Network

BERT と同様に、各位置に独立に適用される2層の全結合ネットワークです。

$$ \text{FFN}(\bm{x}) = \text{GELU}(\bm{x} \bm{W}_1 + \bm{b}_1) \bm{W}_2 + \bm{b}_2 $$

GPT-1 では GELU 活性化関数を使用しています(ReLU ではなく)。

レイヤーの構成

GPT-2 以降では、Pre-Norm(LayerNorm を各サブレイヤーの前に配置)が採用されています。

GPT-1(Post-Norm): $$ \bm{h}’ = \text{LayerNorm}(\bm{h} + \text{CausalAttention}(\bm{h})) $$ $$ \bm{h}^{(l)} = \text{LayerNorm}(\bm{h}’ + \text{FFN}(\bm{h}’)) $$

GPT-2(Pre-Norm): $$ \bm{h}’ = \bm{h} + \text{CausalAttention}(\text{LayerNorm}(\bm{h})) $$ $$ \bm{h}^{(l)} = \bm{h}’ + \text{FFN}(\text{LayerNorm}(\bm{h}’)) $$

Pre-Norm は深いネットワークでの学習を安定化させる効果があります。

言語モデルの目的関数

事前学習の目的関数

コーパス $\mathcal{D} = \{x_1, x_2, \dots, x_N\}$ が与えられたとき、GPT の事前学習の目的関数は対数尤度の最大化です。

$$ \begin{equation} \mathcal{L}_{\text{LM}} = -\frac{1}{T} \sum_{t=1}^{T} \log P(x_t \mid x_1, \dots, x_{t-1}; \bm{\theta}) \end{equation} $$

ここで $\bm{\theta}$ はモデルの全パラメータです。条件付き確率は最終層の隠れ状態を語彙に射影して得られます。

$$ P(x_t = w \mid x_{

ここで $\bm{h}_t^{(L)} \in \mathbb{R}^{d_{\text{model}}}$ は最終層の位置 $t$ の隠れ状態、$\bm{W}_{\text{vocab}} \in \mathbb{R}^{|\mathcal{V}| \times d_{\text{model}}}$ は語彙への射影行列です。

パープレキシティ

言語モデルの性能はパープレキシティ(perplexity)で評価されます。

$$ \text{PPL} = \exp\left(-\frac{1}{T} \sum_{t=1}^{T} \log P(x_t \mid x_{

パープレキシティは「各ステップで平均的にいくつの選択肢の中から正解を選んでいるか」を表します。PPL が低いほど、モデルの予測が正確であることを意味します。理想的なモデルでは PPL = 1(完全な予測)となります。

GPT-1 のファインチューニング

GPT-1 では、事前学習後にタスク固有のデータでファインチューニングを行います。分類タスクの場合

$$ \mathcal{L}_{\text{task}} = -\sum_{i} \log P(y_i \mid x_1^{(i)}, \dots, x_T^{(i)}) $$

全体の損失は言語モデルの損失との加重和です。

$$ \mathcal{L} = \mathcal{L}_{\text{task}} + \lambda \mathcal{L}_{\text{LM}} $$

$\lambda$ は補助的な言語モデル損失の重みで、これにより事前学習で獲得した言語知識が急速に失われることを防ぎます。

GPT-1 / 2 / 3 のスケーリング

GPT-1(2018)

  • 目的: 事前学習 + ファインチューニングの有効性を実証
  • データ: BookCorpus(約8億トークン)
  • 重要な貢献: Transformer を自己回帰言語モデルに適用する枠組みを確立

GPT-2(2019)

  • 目的: 十分に大きなモデルはファインチューニングなしでタスクを解けることを示す(Zero-shot)
  • データ: WebText(約80億トークン、Reddit で高評価を得たリンク先)
  • 変更点: Pre-Norm、バイトペア符号化(BPE)の改良、最大系列長を 512→1024 に拡張

GPT-3(2020)

  • 目的: スケーリングによる性能向上と In-context Learning の実証
  • データ: 約5000億トークン(Common Crawl、WebText、書籍、Wikipedia)
  • 規模: 1750億パラメータ
  • 重要な発見: モデルサイズが十分に大きくなると、プロンプトに例を含めるだけで(Few-shot)多様なタスクを解けるようになる

スケーリング則

GPT-3 の論文では、以下のスケーリング則(Kaplan et al., 2020 に基づく)が確認されました。

$$ \mathcal{L}(N) \propto N^{-\alpha_N}, \quad \mathcal{L}(D) \propto D^{-\alpha_D}, \quad \mathcal{L}(C) \propto C^{-\alpha_C} $$

ここで $N$ はパラメータ数、$D$ はデータ量、$C$ は計算量(FLOPs)であり、べき乗則に従って損失が減少します。$\alpha_N \approx 0.076$, $\alpha_D \approx 0.095$ 程度の値が報告されています。

テキスト生成戦略の数学的定式化

学習済みの GPT からテキストを生成する際、各ステップで次のトークンを選択する方法が重要です。

1. Greedy Decoding

各ステップで最も確率の高いトークンを選択します。

$$ x_t = \arg\max_{w \in \mathcal{V}} P(w \mid x_{

問題点: 局所最適に陥りやすく、反復的で退屈なテキストを生成する傾向があります。

2. Temperature Sampling

ソフトマックスの温度パラメータ $\tau > 0$ を導入して確率分布を調整します。

$$ P_\tau(w \mid x_{

ここで $z_w$ はロジット(ソフトマックス前の値)です。

  • $\tau \to 0$: 分布がピーキーになり、Greedy に近づく
  • $\tau = 1$: 元の確率分布
  • $\tau > 1$: 分布が平坦になり、多様性が増す

$\tau$ の効果を直感的に理解するため、2つのトークンの確率比を見ます。

$$ \frac{P_\tau(w_1)}{P_\tau(w_2)} = \exp\left(\frac{z_1 – z_2}{\tau}\right) $$

$\tau$ が小さいほど、ロジットの差が増幅され、最も確率の高いトークンが選ばれやすくなります。

3. Top-k Sampling

確率の上位 $k$ 個のトークンのみからサンプリングします。

$$ \mathcal{V}_k = \{w \in \mathcal{V} : \text{rank}(P(w \mid x_{

$$ P_{\text{top-}k}(w \mid x_{

問題点: $k$ が固定であるため、確率分布の形状によっては不適切になります。鋭いピークを持つ分布では $k$ が大きすぎて低確率のトークンが選ばれ、平坦な分布では $k$ が小さすぎて多様性が不足します。

4. Top-p Sampling(Nucleus Sampling)

累積確率が $p$ を超える最小のトークン集合からサンプリングします。

$$ \mathcal{V}_p = \text{smallest } \mathcal{V}’ \subseteq \mathcal{V} \text{ such that } \sum_{w \in \mathcal{V}’} P(w \mid x_{

$$ P_{\text{top-}p}(w \mid x_{

Top-p は確率分布の形状に適応的にサンプリング候補を調整できるため、Top-k の問題を解決します。典型的には $p = 0.9 \text{–} 0.95$ が使われます。

生成戦略の比較

戦略 多様性 品質 制御性
Greedy 最低 一定 なし
Temperature ($\tau < 1$) $\tau$ で調整
Temperature ($\tau > 1$) $\tau$ で調整
Top-k $k$ で調整
Top-p 適応的 $p$ で調整

BERT との比較

特性 BERT GPT
アーキテクチャ Transformer Encoder Transformer Decoder
文脈の方向 双方向 左→右(自己回帰)
事前学習タスク MLM + NSP 言語モデル
マスク なし(全位置参照可能) 因果マスク(未来を隠す)
得意なタスク 理解(分類、固有表現認識) 生成(テキスト生成、翻訳)
ファインチューニング タスク固有ヘッドを追加 GPT-1: ヘッド追加、GPT-2以降: プロンプト
スケーリング BERT-Large (340M) まで GPT-3 (175B) まで

なぜ GPT は生成に強いのか

自己回帰モデルは定義上 $P(x_1, \dots, x_T) = \prod P(x_t \mid x_{

なぜ BERT は理解に強いのか

BERT は各トークンが左右両方の文脈を同時に参照できるため、文の意味理解に必要な情報を直接利用できます。例えば「The man went to the bank to deposit money」という文では、”bank” の意味を判断するために右側の “deposit money” が重要ですが、GPT は左→右にしか見られないため、この情報を利用できません。

In-context Learning / Few-shot Learning

GPT-3 の重要な発見は、十分に大きなモデルがプロンプトに含まれる例だけでタスクを解けることです。

定式化

プロンプト $\bm{p}$ と入力 $\bm{x}$ を連結して出力 $\bm{y}$ を生成します。

$$ P(\bm{y} \mid \bm{x}) \approx P(\bm{y} \mid \bm{p} \oplus \bm{x}) $$

ここで $\oplus$ は系列の連結を表します。

設定 プロンプト
Zero-shot タスクの説明のみ
One-shot タスクの説明 + 1つの例
Few-shot タスクの説明 + 数個の例

Few-shot Learning は重みの更新を伴わないため、モデルが事前学習で獲得した能力だけでタスクを解いていることになります。この能力がなぜ出現するかは活発な研究領域です。

Python で小規模 GPT のスクラッチ実装

因果マスク付き Self-Attention の実装

import numpy as np
import matplotlib.pyplot as plt

class CausalSelfAttention:
    """因果マスク付き Self-Attention"""
    def __init__(self, d_model, n_heads):
        self.d_model = d_model
        self.n_heads = n_heads
        self.d_k = d_model // n_heads

        # Q, K, V, O の重み行列
        scale = np.sqrt(2.0 / d_model)
        self.W_q = np.random.randn(d_model, d_model) * scale
        self.W_k = np.random.randn(d_model, d_model) * scale
        self.W_v = np.random.randn(d_model, d_model) * scale
        self.W_o = np.random.randn(d_model, d_model) * scale

    def forward(self, x):
        """
        x: (seq_len, d_model)
        returns: (seq_len, d_model)
        """
        T, D = x.shape

        # Q, K, V の計算
        Q = x @ self.W_q  # (T, d_model)
        K = x @ self.W_k
        V = x @ self.W_v

        # Multi-Head に分割
        Q = Q.reshape(T, self.n_heads, self.d_k).transpose(1, 0, 2)  # (n_heads, T, d_k)
        K = K.reshape(T, self.n_heads, self.d_k).transpose(1, 0, 2)
        V = V.reshape(T, self.n_heads, self.d_k).transpose(1, 0, 2)

        # スケーリング付き内積アテンション
        scores = Q @ K.transpose(0, 2, 1) / np.sqrt(self.d_k)  # (n_heads, T, T)

        # 因果マスクの適用
        causal_mask = np.triu(np.full((T, T), -1e9), k=1)  # 上三角を -inf に
        scores = scores + causal_mask[np.newaxis, :, :]

        # ソフトマックス
        scores_max = np.max(scores, axis=-1, keepdims=True)
        exp_scores = np.exp(scores - scores_max)
        attn_weights = exp_scores / np.sum(exp_scores, axis=-1, keepdims=True)

        # 重み付き和
        out = attn_weights @ V  # (n_heads, T, d_k)

        # ヘッドの連結
        out = out.transpose(1, 0, 2).reshape(T, D)  # (T, d_model)

        # 出力射影
        out = out @ self.W_o

        return out, attn_weights

# 因果マスクの可視化
T = 8
causal_mask = np.triu(np.full((T, T), -np.inf), k=1)
exp_mask = np.where(causal_mask == 0, 1, 0)

plt.figure(figsize=(6, 5))
plt.imshow(exp_mask, cmap='Blues', interpolation='nearest')
plt.xlabel("Key Position (j)")
plt.ylabel("Query Position (i)")
plt.title("Causal Mask (1 = attend, 0 = masked)")
for i in range(T):
    for j in range(T):
        plt.text(j, i, str(exp_mask[i, j]), ha='center', va='center', fontsize=10)
plt.colorbar()
plt.tight_layout()
plt.show()

小規模 GPT モデルの実装

import numpy as np
import matplotlib.pyplot as plt

def gelu(x):
    """GELU 活性化関数"""
    return 0.5 * x * (1 + np.tanh(np.sqrt(2 / np.pi) * (x + 0.044715 * x**3)))

def softmax(x, axis=-1):
    """数値安定なソフトマックス"""
    x_max = np.max(x, axis=axis, keepdims=True)
    exp_x = np.exp(x - x_max)
    return exp_x / np.sum(exp_x, axis=axis, keepdims=True)

def layer_norm(x, gamma, beta, eps=1e-5):
    """レイヤー正規化"""
    mean = np.mean(x, axis=-1, keepdims=True)
    var = np.var(x, axis=-1, keepdims=True)
    return gamma * (x - mean) / np.sqrt(var + eps) + beta

class MiniGPT:
    """小規模 GPT の実装"""
    def __init__(self, vocab_size, d_model, n_heads, n_layers, max_seq_len):
        self.vocab_size = vocab_size
        self.d_model = d_model
        self.n_heads = n_heads
        self.n_layers = n_layers
        self.d_k = d_model // n_heads
        self.d_ff = 4 * d_model

        scale = 0.02

        # トークン埋め込み
        self.token_emb = np.random.randn(vocab_size, d_model) * scale
        # 位置埋め込み
        self.pos_emb = np.random.randn(max_seq_len, d_model) * scale

        # Transformer ブロックのパラメータ
        self.layers = []
        for _ in range(n_layers):
            layer = {
                # LayerNorm 1
                'ln1_gamma': np.ones(d_model),
                'ln1_beta': np.zeros(d_model),
                # Multi-Head Attention
                'W_q': np.random.randn(d_model, d_model) * scale,
                'W_k': np.random.randn(d_model, d_model) * scale,
                'W_v': np.random.randn(d_model, d_model) * scale,
                'W_o': np.random.randn(d_model, d_model) * scale,
                # LayerNorm 2
                'ln2_gamma': np.ones(d_model),
                'ln2_beta': np.zeros(d_model),
                # FFN
                'W1': np.random.randn(d_model, self.d_ff) * scale,
                'b1': np.zeros(self.d_ff),
                'W2': np.random.randn(self.d_ff, d_model) * scale,
                'b2': np.zeros(d_model),
            }
            self.layers.append(layer)

        # 最終レイヤー正規化
        self.ln_f_gamma = np.ones(d_model)
        self.ln_f_beta = np.zeros(d_model)

        # 出力射影(トークン埋め込みと重み共有)
        self.W_out = self.token_emb  # weight tying

    def causal_attention(self, x, layer_params):
        """因果マスク付き Multi-Head Self-Attention"""
        T, D = x.shape
        Q = x @ layer_params['W_q']
        K = x @ layer_params['W_k']
        V = x @ layer_params['W_v']

        # Multi-Head 分割
        Q = Q.reshape(T, self.n_heads, self.d_k).transpose(1, 0, 2)
        K = K.reshape(T, self.n_heads, self.d_k).transpose(1, 0, 2)
        V = V.reshape(T, self.n_heads, self.d_k).transpose(1, 0, 2)

        # アテンションスコア + 因果マスク
        scores = Q @ K.transpose(0, 2, 1) / np.sqrt(self.d_k)
        mask = np.triu(np.full((T, T), -1e9), k=1)
        scores = scores + mask[np.newaxis, :, :]

        attn = softmax(scores, axis=-1)
        out = attn @ V
        out = out.transpose(1, 0, 2).reshape(T, D)
        out = out @ layer_params['W_o']
        return out

    def ffn(self, x, layer_params):
        """Position-wise Feed-Forward Network"""
        h = gelu(x @ layer_params['W1'] + layer_params['b1'])
        return h @ layer_params['W2'] + layer_params['b2']

    def forward(self, token_ids):
        """
        token_ids: (seq_len,) の整数配列
        returns: (seq_len, vocab_size) のロジット
        """
        T = len(token_ids)

        # 埋め込み
        x = self.token_emb[token_ids] + self.pos_emb[:T]

        # Transformer ブロック(Pre-Norm)
        for layer_params in self.layers:
            # Self-Attention
            h = layer_norm(x, layer_params['ln1_gamma'], layer_params['ln1_beta'])
            x = x + self.causal_attention(h, layer_params)

            # FFN
            h = layer_norm(x, layer_params['ln2_gamma'], layer_params['ln2_beta'])
            x = x + self.ffn(h, layer_params)

        # 最終 LayerNorm
        x = layer_norm(x, self.ln_f_gamma, self.ln_f_beta)

        # ロジット
        logits = x @ self.W_out.T  # (T, vocab_size)
        return logits

    def generate(self, start_tokens, max_new_tokens, temperature=1.0,
                 top_k=None, top_p=None):
        """テキスト生成"""
        tokens = list(start_tokens)

        for _ in range(max_new_tokens):
            # 順伝播
            logits = self.forward(np.array(tokens))
            next_logits = logits[-1]  # 最後の位置のロジット

            # Temperature の適用
            next_logits = next_logits / temperature

            # 確率分布の計算
            probs = softmax(next_logits)

            # Top-k フィルタリング
            if top_k is not None:
                top_k_indices = np.argsort(probs)[-top_k:]
                mask = np.zeros_like(probs)
                mask[top_k_indices] = 1
                probs = probs * mask
                probs = probs / (probs.sum() + 1e-10)

            # Top-p (Nucleus) フィルタリング
            if top_p is not None:
                sorted_indices = np.argsort(probs)[::-1]
                sorted_probs = probs[sorted_indices]
                cumsum = np.cumsum(sorted_probs)
                cutoff_idx = np.searchsorted(cumsum, top_p) + 1
                allowed = sorted_indices[:cutoff_idx]
                mask = np.zeros_like(probs)
                mask[allowed] = 1
                probs = probs * mask
                probs = probs / (probs.sum() + 1e-10)

            # サンプリング
            next_token = np.random.choice(len(probs), p=probs)
            tokens.append(next_token)

        return tokens

# モデルの生成とテスト
np.random.seed(42)
vocab_size = 50
d_model = 32
n_heads = 4
n_layers = 2
max_seq_len = 64

model = MiniGPT(vocab_size, d_model, n_heads, n_layers, max_seq_len)

# 簡易トークナイザ
idx2token = {i: f"tok_{i}" for i in range(vocab_size)}
idx2token[0] = "<BOS>"
idx2token[1] = "<EOS>"

# テキスト生成のデモ
start = [0]  # <BOS> から開始
print("=== Greedy Decoding (temperature=0.1) ===")
generated = model.generate(start, max_new_tokens=10, temperature=0.1)
print("生成トークン:", [idx2token[t] for t in generated])

print("\n=== Temperature Sampling (temperature=1.0) ===")
generated = model.generate(start, max_new_tokens=10, temperature=1.0)
print("生成トークン:", [idx2token[t] for t in generated])

print("\n=== Top-k Sampling (k=5) ===")
generated = model.generate(start, max_new_tokens=10, temperature=1.0, top_k=5)
print("生成トークン:", [idx2token[t] for t in generated])

print("\n=== Top-p Sampling (p=0.9) ===")
generated = model.generate(start, max_new_tokens=10, temperature=1.0, top_p=0.9)
print("生成トークン:", [idx2token[t] for t in generated])

生成戦略の分布の可視化

import numpy as np
import matplotlib.pyplot as plt

# ロジットの例
np.random.seed(0)
logits = np.array([3.0, 2.5, 1.0, 0.5, 0.3, -0.5, -1.0, -2.0, -3.0, -4.0])
vocab = [f"w{i}" for i in range(len(logits))]

fig, axes = plt.subplots(2, 2, figsize=(12, 10))

# 1. Temperature の効果
for i, tau in enumerate([0.5, 1.0, 2.0]):
    probs = np.exp(logits / tau) / np.sum(np.exp(logits / tau))
    axes[0, 0].bar(np.arange(len(logits)) + i*0.25 - 0.25, probs, width=0.25,
                   label=f"T={tau}", alpha=0.8)
axes[0, 0].set_xticks(range(len(logits)))
axes[0, 0].set_xticklabels(vocab)
axes[0, 0].set_title("Temperature Scaling")
axes[0, 0].set_ylabel("Probability")
axes[0, 0].legend()

# 2. Top-k Sampling
probs_orig = np.exp(logits) / np.sum(np.exp(logits))
for i, k in enumerate([3, 5]):
    probs = probs_orig.copy()
    top_k_idx = np.argsort(probs)[-k:]
    mask = np.zeros_like(probs)
    mask[top_k_idx] = 1
    probs = probs * mask
    probs = probs / probs.sum()
    axes[0, 1].bar(np.arange(len(logits)) + i*0.3 - 0.15, probs, width=0.3,
                   label=f"k={k}", alpha=0.8)
axes[0, 1].set_xticks(range(len(logits)))
axes[0, 1].set_xticklabels(vocab)
axes[0, 1].set_title("Top-k Sampling")
axes[0, 1].set_ylabel("Probability")
axes[0, 1].legend()

# 3. Top-p Sampling
for i, p in enumerate([0.7, 0.9]):
    probs = probs_orig.copy()
    sorted_idx = np.argsort(probs)[::-1]
    sorted_probs = probs[sorted_idx]
    cumsum = np.cumsum(sorted_probs)
    cutoff = np.searchsorted(cumsum, p) + 1
    allowed = sorted_idx[:cutoff]
    mask = np.zeros_like(probs)
    mask[allowed] = 1
    probs = probs * mask
    probs = probs / probs.sum()
    axes[1, 0].bar(np.arange(len(logits)) + i*0.3 - 0.15, probs, width=0.3,
                   label=f"p={p}", alpha=0.8)
axes[1, 0].set_xticks(range(len(logits)))
axes[1, 0].set_xticklabels(vocab)
axes[1, 0].set_title("Top-p (Nucleus) Sampling")
axes[1, 0].set_ylabel("Probability")
axes[1, 0].legend()

# 4. 全戦略の比較
strategies = {
    "Greedy": np.eye(len(logits))[np.argmax(logits)],
    "T=1.0": probs_orig,
    "Top-k=3": None,
    "Top-p=0.9": None,
}
# Top-k=3
probs_topk = probs_orig.copy()
top3 = np.argsort(probs_topk)[-3:]
mask = np.zeros_like(probs_topk)
mask[top3] = 1
probs_topk = probs_topk * mask
strategies["Top-k=3"] = probs_topk / probs_topk.sum()

# Top-p=0.9
probs_topp = probs_orig.copy()
sorted_idx = np.argsort(probs_topp)[::-1]
cumsum = np.cumsum(probs_topp[sorted_idx])
cutoff = np.searchsorted(cumsum, 0.9) + 1
allowed = sorted_idx[:cutoff]
mask = np.zeros_like(probs_topp)
mask[allowed] = 1
probs_topp = probs_topp * mask
strategies["Top-p=0.9"] = probs_topp / probs_topp.sum()

x_pos = np.arange(len(logits))
width = 0.2
for i, (name, p) in enumerate(strategies.items()):
    axes[1, 1].bar(x_pos + i*width - 0.3, p, width=width, label=name, alpha=0.8)
axes[1, 1].set_xticks(range(len(logits)))
axes[1, 1].set_xticklabels(vocab)
axes[1, 1].set_title("Comparison of Strategies")
axes[1, 1].set_ylabel("Probability")
axes[1, 1].legend(fontsize=8)

plt.suptitle("Text Generation Strategies", fontsize=14, y=1.01)
plt.tight_layout()
plt.show()

まとめ

本記事では、GPT のアーキテクチャと自己回帰生成について解説しました。

  • 自己回帰言語モデル: テキストの同時確率を $P(\bm{x}) = \prod P(x_t \mid x_{
  • 因果マスク: 上三角部分を $-\infty$ にする行列 $\bm{M}$ により、各トークンが未来の情報を参照できないようにする仕組みを数学的に定義しました
  • スケーリング: GPT-1(117M)→ GPT-2(1.5B)→ GPT-3(175B)のスケーリングにより、損失がべき乗則に従って減少し、In-context Learning が出現することを確認しました
  • 生成戦略: Greedy, Temperature, Top-k, Top-p の各手法を数学的に定式化し、それぞれの特性を比較しました
  • BERT との比較: 双方向 Encoder(理解向き)vs 自己回帰 Decoder(生成向き)の本質的な違いを整理しました
  • 実装: Python で因果マスク付き Self-Attention と小規模 GPT をスクラッチ実装し、各種生成戦略でのテキスト生成デモを行いました

次のステップとして、以下の記事も参考にしてください。