一文要約: デコード戦略とは、語彙全体の確率分布から単一のトークンを選ぶ変換です。貪欲はピークを選び、サンプリングはサイコロを振り、Temperature は曲線を変形し、Top-k/Top-p は裾を刈り取ります。
B.1 デコード戦略が重要な理由
B.1.1 ロジットからトークンへ
Transformer デコーダーのすべての順伝播は同じ場所で終わります。語彙内の各トークンに一つのエントリを持つロジットベクトルです。
logits = [2.1, -0.5, 1.3, 0.8, ..., -1.2] # 長さ = vocab_size
各値は正規化されていないスコアです。サンプリングを行う前に、Softmax を通じて確率分布に変換します。
ステップ 1 の Softmax は固定です。ステップ 2 — その分布からトークンを選ぶ — がデコード戦略の領域です。 この付録では実践的なすべての選択肢を網羅します。
B.1.2 戦略のトレードオフ一覧
| 戦略 | 決定論的 | 多様性 | リスク |
|---|---|---|---|
| 貪欲 | 高 | 低 | 繰り返し・単調さ |
| ランダムサンプリング | 低 | 高 | 非整合なトークン |
| Top-K | 中 | 中 | 固定 K が分布形状と合わない |
| Top-P | 中 | 中 | 実際はバランスが取れている |
| Beam Search | 高 | 低 | 高コスト、安全だが退屈 |
B.2 貪欲デコード
B.2.1 仕組み
最もシンプルな戦略: 毎ステップ最高確率のトークンを選ぶ。
import torch
import torch.nn.functional as F
def greedy_decode(logits: torch.Tensor) -> int:
"""
logits: shape (vocab_size,) の 1-D テンソル
最もロジットが高いトークン ID を返す。
"""
return torch.argmax(logits).item()
ここでは Softmax さえ不要です — Softmax は単調なので、ロジットに対する argmax と確率に対する argmax は同じ結果を返します。
B.2.2 計算例
プロンプト: "The agent opened a pull"
ステップ 1:
logits → softmax → {request: 0.51, comment: 0.22, merge: 0.12, ...}
貪欲が選ぶ: "request"
プロンプト: "The agent opened a pull request"
ステップ 2:
logits → softmax → {and: 0.34, .": 0.29, to: 0.18, ...}
貪欲が選ぶ: "and"
最終出力: "The agent opened a pull request and ..."
同じ入力は常に同じ出力を生みます。ランダム性なし。
B.2.3 貪欲を使うとき
適している用途:
- 単一の正しい答えが必要なコード補完
- 事実検索や構造化された情報抽出
- ダウンストリームのコードが出力を確定的にパースするパイプライン
適していない用途:
- 創作生成 — 高確率の溝にはまると貪欲は酷くループする
- レスポンスの多様性が重要な対話
- 局所的に最良のトークンが大局的に最良のシーケンスではないタスク
B.2.4 貪欲の失敗モード: 繰り返し
貪欲は常に分布のモードに従うため、次のようなループに陥ることがあります。
"The function returns the value. The function returns the value. The function..."
これはモデルのバグではありません — デコードの選択のバグです。B.9 節の繰り返しペナルティはこれを壊すために存在します。
B.3 ランダムサンプリング
B.3.1 仕組み
argmax を取る代わりに、確率分布全体からランダムに 1 トークンを引く。
import torch
import torch.nn.functional as F
def random_sample(logits: torch.Tensor) -> int:
"""
logits: shape (vocab_size,) の 1-D テンソル
Softmax 分布からサンプリングしたトークン ID を返す。
"""
probs = F.softmax(logits, dim=-1)
return torch.multinomial(probs, num_samples=1).item()
B.3.2 計算例
probs = {request: 0.40, comment: 0.30, review: 0.15, merge: 0.10, issue: 0.05}
実行ごとに異なるトークンが返ってくる可能性があります:
- "request" (40% の確率)
- "comment" (30% の確率)
- "review" (15% の確率)
- "merge" (10% の確率)
- "issue" ( 5% の確率)
同じプロンプトでも呼び出しごとに異なる補完が生成できます。
B.3.3 トレードオフ
利点:
- 多様で変化のある出力
- 決定論的な繰り返しループを回避
- 次に何が来るかについてのモデルの不確実性全体を反映
欠点:
- 低確率のトークンが選ばれ、非整合なテキストが生成される可能性がある
- ランダムシードを固定しないと出力が再現できない
- 修正なしの生のサンプリングは本番ではほぼ使わない; ほぼ常に Temperature・Top-k・Top-p と組み合わせる
B.4 Temperature
B.4.1 仕組み
Temperature は Softmax 前にロジットを再スケールし、分布を尖らせたり平坦にしたりします。
import torch
import torch.nn.functional as F
def softmax_with_temperature(logits: torch.Tensor, temperature: float) -> torch.Tensor:
"""
temperature > 1 → より平坦な分布(よりランダム)
temperature < 1 → より尖った分布(より決定論的)
temperature → 0 → 貪欲と等価(argmax)
"""
scaled_logits = logits / temperature
return F.softmax(scaled_logits, dim=-1)
数学はシンプルです: 指数をとる前に全ロジットを T で割ります。高い T はロジット間の差を抑制し、低い T はそれを増幅します。
B.4.2 数値表
固定ロジット配列 [2.0, 1.0, 0.5] を使用:
| Temperature | Softmax 結果(近似) | 特性 |
|---|---|---|
| 0.1 | [1.00, 0.00, 0.00] | ほぼ決定論的 |
| 0.5 | [0.84, 0.11, 0.04] | トップトークンへの強い選好 |
| 1.0 | [0.63, 0.23, 0.14] | 生のモデル分布 |
| 2.0 | [0.48, 0.29, 0.23] | 明らかに平坦 |
| 10.0 | [0.36, 0.33, 0.31] | ほぼ一様 |
注意: 値は
softmax(logits / T)を小数点 2 桁に丸めたものです。
直感:
- T < 1: モデルがより決定論的になる — 高確率トークンがさらに多くの確率質量を得る
- T = 1: 変化なし; 生の分布が保持される
- T > 1: モデルがより探索的になる — 低確率トークンがより大きなスライスを得る
B.4.3 Temperature の選び方
| Temperature | 効果 | 典型的な用途 |
|---|---|---|
| 0(または → 0) | 貪欲、完全決定論的 | コード生成、構造化抽出 |
| 0.1 〜 0.3 | 非常に確信度が高い | 事実 Q&A、検索 |
| 0.5 〜 0.7 | 変化を伴う確信 | 一般的なアシスタント対話 |
| 0.8 〜 1.0 | バランス | 品質ガードレール付きの創作 |
| 1.0 〜 1.5 | 探索的 | ブレインストーミング、物語の下書き |
| > 1.5 | 非常にランダム | 実験的のみ — 出力がよく劣化する |
B.4.4 Temperature = 0
T がゼロに近づくにつれてスケールされたロジットは ±∞ に発散し、Softmax は argmax の点質量に崩壊します。多くの API が temperature=0 を貪欲デコードの正確な同義語として扱います。
B.5 Top-K サンプリング
B.5.1 仕組み
最高ロジットの K トークンだけを残し、残りをゼロにして、残ったトークンからサンプリングします。
import torch
import torch.nn.functional as F
def top_k_sample(logits: torch.Tensor, k: int, temperature: float = 1.0) -> int:
"""
logits: shape (vocab_size,) の 1-D テンソル
k: 残すトップトークン数
temperature: 保持されたトークンに対する Softmax の前に適用
"""
# 1. 上位 k 番目のロジット値(閾値)を見つける
top_k_values, top_k_indices = torch.topk(logits, k)
# 2. Temperature を適用し、Top-k のみで正規化
top_k_probs = F.softmax(top_k_values / temperature, dim=-1)
# 3. 縮小された分布からサンプリング
sampled_pos = torch.multinomial(top_k_probs, num_samples=1).item()
return top_k_indices[sampled_pos].item()
B.5.2 計算例
全分布:
A: 0.40, B: 0.30, C: 0.15, D: 0.08, E: 0.05, F: 0.02
K=3 の Top-K:
保持: A: 0.40, B: 0.30, C: 0.15
再正規化: A: 0.47, B: 0.35, C: 0.18
{A, B, C} からのみサンプリング — D, E, F は完全に除外。
B.5.3 K の選び方
| K の値 | 効果 |
|---|---|
| 1 | 貪欲と等価 |
| 10 〜 50 | 一般的な本番範囲 |
| 100+ | 制約なしサンプリングに近づく |
目安: K = 40 〜 50 は多くのオープンソース推論スタック(LLaMA、Mistral のデフォルト)のデフォルトです。
B.5.4 Top-K の限界
K は固定数ですが、分布の形状はトークンの位置によって大きく変わります。極端な 2 例を見てみましょう。
ピーク型の分布:
A: 0.95, B: 0.03, C: 0.01, D: 0.005, ...
K=50 では合計確率 < 0.05 の 49 トークンを保持してしまう。
それらの裾のトークンはそもそも候補にすべきではない。
フラットな分布:
A: 0.10, B: 0.09, C: 0.08, D: 0.08, E: 0.07, ...
K=50 では完全に合理的な代替トークンを切り捨てる可能性がある。
固定 K は分布のエントロピーに適応できません。それが Top-P が解決しようとした問題です。
B.6 Top-P(ニュークリアス)サンプリング
B.6.1 仕組み
Top-P は累積確率が P に達する最小のトークンセットを保持し、そのセットからサンプリングします。候補セットのサイズは自動的に適応します。
import torch
import torch.nn.functional as F
def top_p_sample(logits: torch.Tensor, p: float, temperature: float = 1.0) -> int:
"""
logits: shape (vocab_size,) の 1-D テンソル
p: 累積確率の閾値(例: 0.9)
temperature: Softmax の前に適用
"""
# 1. Temperature を適用して確率を計算
probs = F.softmax(logits / temperature, dim=-1)
# 2. 降順にソート
sorted_probs, sorted_indices = torch.sort(probs, descending=True)
# 3. 累積和
cumulative_probs = torch.cumsum(sorted_probs, dim=-1)
# 4. 累積値が p 以上になる最初のインデックスを見つける
# p を超えるトークンを必ず含めるために 1 つシフト
cutoff_mask = (cumulative_probs - sorted_probs) >= p
sorted_probs[cutoff_mask] = 0.0
# 5. 再正規化してサンプリング
sorted_probs = sorted_probs / sorted_probs.sum()
sampled_pos = torch.multinomial(sorted_probs, num_samples=1).item()
return sorted_indices[sampled_pos].item()
B.6.2 計算例
ソートされた分布:
A: 0.40, B: 0.30, C: 0.15, D: 0.08, E: 0.05, F: 0.02
累積:
0.40, 0.70, 0.85, 0.93, 0.98, 1.00
Top-P = 0.90:
A, B, C, D を保持(累積 0.93 ≥ 0.90)。
ニュークリアスサイズ = 4。
B.6.3 Top-P は分布の形状に適応する
Top-K にはできないことです。
ピーク型の分布:
A: 0.95, B: 0.03, ...
Top-P = 0.90 → ニュークリアス = {A} のみ(サイズ 1)
フラットな分布:
A: 0.10, B: 0.09, C: 0.08, ...
Top-P = 0.90 → ニュークリアス = ~15 トークン(サイズが自動調整)
モデルが確信しているときはニュークリアスが縮小し、不確かなときは拡大します。それが正しい振る舞いです。
B.6.4 P の選び方
| P の値 | 効果 |
|---|---|
| 0.1 〜 0.5 | 小さいニュークリアス、保守的 |
| 0.8 〜 0.95 | 標準的な本番範囲 |
| 1.0 | 制約なしサンプリングと等価 |
目安: P = 0.9 または P = 0.95 がほとんどの本番設定のデフォルトです。ニュークリアスサンプリングを導入した Holtzman et al. 2019 の論文は、オープンエンド生成に P = 0.95 を推奨していました。
B.7 Beam Search
B.7.1 仕組み
各ステップで 1 つのトークンに確定するのではなく、Beam Search は B 個の候補シーケンスを並行して管理します — これが「ビーム」です。毎ステップ、各ビームはすべての可能な次のトークンで拡張され、(累積対数確率で)上位 B のシーケンスだけが生き残ります。
B.7.2 アルゴリズム
import math
from typing import List, Tuple
def beam_search(
model,
prompt: List[int],
beam_width: int,
max_length: int,
alpha: float = 0.6, # 長さ正規化指数
) -> List[int]:
"""
長さ正規化された Beam Search で最高スコアのシーケンスを返す。
model: トークン ID を受け取り、語彙上の対数確率を返す呼び出し可能オブジェクト。
"""
# 各ビームは(シーケンス, 累積対数確率)のタプル
beams: List[Tuple[List[int], float]] = [(prompt[:], 0.0)]
completed: List[Tuple[List[int], float]] = []
for step in range(max_length):
all_candidates: List[Tuple[List[int], float]] = []
for seq, score in beams:
log_probs = model(seq) # shape: (vocab_size,)
# 拡張: すべての可能な次のトークンを考慮
for token_id, lp in enumerate(log_probs):
new_seq = seq + [token_id]
new_score = score + lp
all_candidates.append((new_seq, new_score))
# ランキング前に長さ正規化を適用
def length_normalized_score(candidate):
seq, score = candidate
length = len(seq) - len(prompt)
return score / (max(length, 1) ** alpha)
all_candidates.sort(key=length_normalized_score, reverse=True)
beams = all_candidates[:beam_width]
# 先頭シーケンスを返す
return beams[0][0]
B.7.3 長さ正規化
補正なしでは、Beam Search は短いシーケンスを優遇します。各ステップで確率(< 1)を乗算するため、長いシーケンスは生の累積スコアが低くなるからです。標準的な修正方法:
ここで が一般的なデフォルト(Google のオリジナルニューラル機械翻訳システムで使用)。高い α は長さをより積極的にペナルティします。
B.7.4 Beam Search が適切なとき
強く適している:
- 機械翻訳(ビーム幅 4〜6 が標準)
- 抽象的な要約
- 既知の文法を持つ構造化生成(制約付きデコード)
適していない:
- オープンエンドのチャットや指示に従うタスク — Beam Search はすべてのリスクを避けた流暢だが退屈な出力を生む
- 複数の生成間で多様性が重要なタスク
現代の LLM サービングスタック(vLLM、TGI、llama.cpp)はチャットモデルに Beam Search を使いません。Top-P + Temperature サンプリングを使います。Beam Search は出力に単一の客観的に正しい形がある NLP パイプラインに残っています。
B.8 組み合わせ戦略
B.8.1 2 つの一般的な組み合わせ
実際にはほぼ常に複数の戦略を組み合わせます。2 つの標準パターンを示します。
import torch
import torch.nn.functional as F
# --- ヘルパー: Top-K フィルター ---
def top_k_filter(logits: torch.Tensor, k: int) -> torch.Tensor:
"""上位 k 以外のすべてのロジットをゼロにする。"""
if k <= 0:
return logits
top_k_values = torch.topk(logits, k).values
threshold = top_k_values[-1]
return torch.where(logits >= threshold, logits, torch.full_like(logits, float('-inf')))
# --- ヘルパー: Top-P フィルター ---
def top_p_filter(logits: torch.Tensor, p: float) -> torch.Tensor:
"""ニュークリアスの外側のすべてのロジットをゼロにする。"""
probs = F.softmax(logits, dim=-1)
sorted_probs, sorted_indices = torch.sort(probs, descending=True)
cumulative = torch.cumsum(sorted_probs, dim=-1)
mask = (cumulative - sorted_probs) >= p
sorted_probs[mask] = 0.0
# 元の順序に戻す
filtered = torch.zeros_like(probs)
filtered.scatter_(0, sorted_indices, sorted_probs)
return torch.log(filtered + 1e-10) # ロジット空間に戻す(近似)
# --- パターン 1: Top-P + Temperature ---
def decode_top_p_temperature(
logits: torch.Tensor,
p: float = 0.9,
temperature: float = 0.7,
) -> int:
"""標準的なオープンエンド生成の設定。"""
filtered = top_p_filter(logits, p)
probs = F.softmax(filtered / temperature, dim=-1)
return torch.multinomial(probs, num_samples=1).item()
# --- パターン 2: Top-K + Top-P + Temperature ---
def decode_top_k_top_p_temperature(
logits: torch.Tensor,
k: int = 50,
p: float = 0.9,
temperature: float = 0.7,
) -> int:
"""Hugging Face generate() デフォルトスタイルの設定。"""
# 最初に Top-k で絞り、次にニュークリアス閾値を適用し、Temperature をかける
filtered = top_k_filter(logits, k)
filtered = top_p_filter(filtered, p)
probs = F.softmax(filtered / temperature, dim=-1)
return torch.multinomial(probs, num_samples=1).item()
順序が重要です: Top-K を先に、次に Top-P。これにより Top-K が候補の上限を設け、Top-P はニュークリアスを小さくすることしかできず、大きくすることはできません。
B.8.2 実際のデフォルト設定
異なる推論スタックは異なるデフォルトを使います。最初に API にアクセスするときに遭遇するデファクトの設定を示します。
| スタック / API | temperature | top_p | top_k | 備考 |
|---|---|---|---|---|
| OpenAI API(chat) | 1.0 | 1.0 | — | 両方のデフォルトは「オフ」; ユーザーが調整することが期待される |
| Anthropic Claude API | 1.0 | — | — | デフォルトは Temperature 1 で実質的に貪欲に近い |
Hugging Face generate() | 1.0 | 0.9 | 50 | Top-K + Top-P 両方がデフォルトで有効 |
| llama.cpp | 0.8 | 0.95 | 40 | ローカル使用のための保守的なデフォルト |
| LLaMA リファレンス推論 | 0.6 | 0.9 | — | Meta の Llama 3 公開デフォルト |
| Mistral リファレンス | 0.7 | 1.0 | 50 | Top-K 有効、Top-P デフォルトオフ |
B.8.3 タスク別の推奨スタート設定
| タスク | Temperature | Top-P | Top-K | 備考 |
|---|---|---|---|---|
| コード生成 | 0.2 | 0.95 | — | 低い T、広いニュークリアス |
| 事実 Q&A | 0 または 0.3 | — | — | 貪欲またはほぼ貪欲 |
| 指示に従う | 0.7 | 0.9 | — | 標準的なアシスタント設定 |
| 創作 | 0.9 〜 1.0 | 0.9 | — | モデルに探索させる |
| ブレインストーミング | 1.0 〜 1.2 | 0.95 | — | より高い Temperature、より広いニュークリアス |
| 機械翻訳 | beam_width=4, α=0.6 | — | — | Beam Search がまだ好まれる |
B.9 繰り返しペナルティ
B.9.1 問題
LLM はループに陥ることがあります。
"The agent opened the pull request. The agent opened the pull request.
The agent opened the pull request. The agent..."
これは貪欲デコードの失敗モードですが、高確率のトークンが直前に生成されたトークンと同じ場合にサンプリングでも発生します。修正方法は、すでに出現したトークンにペナルティをかけることです。
B.9.2 実装
import torch
def apply_repetition_penalty(
logits: torch.Tensor,
generated_token_ids: list,
penalty: float = 1.2,
) -> torch.Tensor:
"""
すでに生成された各トークンのロジットを減らす。
正のロジットを持つトークンはペナルティで除算(減少させる)。
負のロジットを持つトークンはペナルティで乗算(より負にする)。
penalty = 1.0 → 変化なし。
penalty = 1.2 → 一般的なデフォルト(確率質量が約 17% 減少)。
"""
logits = logits.clone()
for token_id in set(generated_token_ids):
if logits[token_id] > 0:
logits[token_id] /= penalty
else:
logits[token_id] *= penalty
return logits
set() による重複排除により、頻度は関係ありません — トークンが 1 回出現しても 10 回出現しても同じペナルティが適用されます。頻度に敏感な振る舞いには B.11 を参照してください。
B.10 プレゼンスペナルティ
B.10.1 何をするか
プレゼンスペナルティは、生成されたテキストのどこかに出現したすべてのトークンに、出現回数にかかわらず一定の減算を適用します。
import torch
def apply_presence_penalty(
logits: torch.Tensor,
generated_token_ids: list,
presence_penalty: float = 0.6,
) -> torch.Tensor:
"""
少なくとも一度見られたすべてのトークンのロジットから presence_penalty を引く。
presence_penalty = 0 → 効果なし
presence_penalty = 0.6 → 一般的なデフォルト(OpenAI API)
presence_penalty = 2.0 → 繰り返されたトークンの強い回避
"""
logits = logits.clone()
for token_id in set(generated_token_ids):
logits[token_id] -= presence_penalty
return logits
これは OpenAI API の presence_penalty パラメータです。プレゼンスペナルティはモデルに新しいトピックを導入するよう促します — 繰り返し回数に比例してスケールしません。
B.11 頻度ペナルティ
B.11.1 何をするか
頻度ペナルティは、すでに各トークンが出現した回数に比例したペナルティを適用します。トークンが繰り返されるほど、より強く抑制されます。
import torch
from collections import Counter
def apply_frequency_penalty(
logits: torch.Tensor,
generated_token_ids: list,
frequency_penalty: float = 0.5,
) -> torch.Tensor:
"""
各トークンのロジットから frequency_penalty * count[token] を引く。
frequency_penalty = 0 → 効果なし
frequency_penalty = 0.5 → 一般的なデフォルト
frequency_penalty = 2.0 → 繰り返しトークンの強い抑制
"""
logits = logits.clone()
token_counts = Counter(generated_token_ids)
for token_id, count in token_counts.items():
logits[token_id] -= frequency_penalty * count
return logits
これは OpenAI API の frequency_penalty パラメータです。プレゼンスペナルティとは異なり、頻度ペナルティはトークンが出現するたびに差し引き続けます — 10 回見られたトークンは 1 回見られたトークンの 10 倍のペナルティを受けます。
B.11.2 ペナルティパラメータリファレンス
| パラメータ | 範囲 | 0 のときの効果 | 一般的なデフォルト |
|---|---|---|---|
repetition_penalty | 1.0 〜 1.5 | 変化なし(乗算的) | 1.2 |
presence_penalty | 0 〜 2.0 | 変化なし | 0.6 |
frequency_penalty | 0 〜 2.0 | 変化なし | 0.5 |
3 つすべてを組み合わせることができます。実際には、プレゼンスペナルティと頻度ペナルティは API 向け製品でより一般的で、繰り返しペナルティはオープンソースのローカル推論スタックでより一般的です。
B.12 付録まとめ
| 戦略 | 核心のアイデア | 使うとき |
|---|---|---|
| 貪欲 | 毎ステップ argmax | コード、決定論的タスク |
| ランダムサンプリング | 多項分布のドロー | 単独ではほぼ使わない; 他の方法のベース |
| Temperature | Softmax 前に 1/T でロジットをスケール | 常に使う; まずこれを調整 |
| Top-K | 上位 K 候補を保持 | 保険でTop-Pと組み合わせる |
| Top-P | 累積 P でニュークリアスを保持 | Top-K 単独より好まれる |
| Beam Search | B 個の最良シーケンスを追跡 | 翻訳、制約付き生成 |
| 繰り返しペナルティ | 見られたトークンへの乗算ペナルティ | ローカル推論のデフォルト |
| プレゼンスペナルティ | 見られたトークンへの一定減算 | API レベルのトピック多様性 |
| 頻度ペナルティ | 見られたトークンへの回数スケール減算 | API レベルの繰り返し制御 |
参考文献
- The Curious Case of Neural Text Degeneration (Holtzman et al., 2019) — ニュークリアス(Top-P)サンプリングを導入し、劣化問題に名前をつけた論文
- Hierarchical Neural Story Generation (Fan et al., 2018) — ストーリー生成に適用されたオリジナルの Top-K サンプリング
- CTRL: A Conditional Transformer Language Model for Controllable Generation (Keskar et al., 2019) — 制御可能な生成のコンテキストで繰り返しペナルティを議論
デコードの決定はプロダクトの表面です。サービングスタックに付属するデフォルトは、ユーザーへのモデルの感触を静かに形成します。付録 C では最もよく出る質問に答えます。