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)した後、下流タスクにファインチューニングするという枠組みです。
- 事前学習: 大量のテキストデータで言語モデルの目的関数を最適化
- ファインチューニング: タスク固有のラベル付きデータで微調整
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 では、事前学習後にタスク固有のデータでファインチューニングを行います。分類タスクの場合 $$
\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-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 からテキストを生成する際、各ステップで次のトークンを選択する方法が重要です。 各ステップで最も確率の高いトークンを選択します。 $$
x_t = \arg\max_{w \in \mathcal{V}} P(w \mid x_{ 問題点: 局所最適に陥りやすく、反復的で退屈なテキストを生成する傾向があります。 ソフトマックスの温度パラメータ $\tau > 0$ を導入して確率分布を調整します。 $$
P_\tau(w \mid x_{ ここで $z_w$ はロジット(ソフトマックス前の値)です。 $\tau$ の効果を直感的に理解するため、2つのトークンの確率比を見ます。 $$
\frac{P_\tau(w_1)}{P_\tau(w_2)} = \exp\left(\frac{z_1 – z_2}{\tau}\right)
$$ $\tau$ が小さいほど、ロジットの差が増幅され、最も確率の高いトークンが選ばれやすくなります。 確率の上位 $k$ 個のトークンのみからサンプリングします。 $$
\mathcal{V}_k = \{w \in \mathcal{V} : \text{rank}(P(w \mid x_{ $$
P_{\text{top-}k}(w \mid x_{ 問題点: $k$ が固定であるため、確率分布の形状によっては不適切になります。鋭いピークを持つ分布では $k$ が大きすぎて低確率のトークンが選ばれ、平坦な分布では $k$ が小さすぎて多様性が不足します。 累積確率が $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$ が使われます。 自己回帰モデルは定義上 $P(x_1, \dots, x_T) = \prod P(x_t \mid x_{ BERT は各トークンが左右両方の文脈を同時に参照できるため、文の意味理解に必要な情報を直接利用できます。例えば「The man went to the bank to deposit money」という文では、”bank” の意味を判断するために右側の “deposit money” が重要ですが、GPT は左→右にしか見られないため、この情報を利用できません。 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$ は系列の連結を表します。 Few-shot Learning は重みの更新を伴わないため、モデルが事前学習で獲得した能力だけでタスクを解いていることになります。この能力がなぜ出現するかは活発な研究領域です。 本記事では、GPT のアーキテクチャと自己回帰生成について解説しました。 次のステップとして、以下の記事も参考にしてください。パープレキシティ
GPT-1 のファインチューニング
GPT-1 / 2 / 3 のスケーリング
GPT-1(2018)
GPT-2(2019)
GPT-3(2020)
スケーリング則
テキスト生成戦略の数学的定式化
1. Greedy Decoding
2. Temperature Sampling
3. Top-k Sampling
4. Top-p Sampling(Nucleus Sampling)
生成戦略の比較
戦略
多様性
品質
制御性
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 は生成に強いのか
なぜ BERT は理解に強いのか
In-context Learning / Few-shot Learning
定式化
設定
プロンプト
Zero-shot
タスクの説明のみ
One-shot
タスクの説明 + 1つの例
Few-shot
タスクの説明 + 数個の例
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()
まとめ