高度なデコーディング戦略 — Contrastive Search・Repetition Penaltyなど最新の生成制御手法

ChatGPTやClaude、Llamaなどの大規模言語モデル(LLM)を使ったことがある方なら、「同じプロンプトでも、モデルの設定次第で出力がまったく変わる」という経験があるのではないでしょうか。Temperature や Top-p サンプリングは広く知られていますが、それだけでは解決できない問題があります。たとえば、モデルが同じフレーズを何度も繰り返してしまう「退化(degeneration)」や、多様性を上げようとすると意味不明な文が混ざる「品質と多様性のトレードオフ」です。

これらの問題に対処するため、近年さまざまな高度なデコーディング戦略が提案されています。Repetition Penalty は繰り返しを直接抑制し、Contrastive Search は品質と多様性を同時に最適化し、Typical Decoding は情報理論に基づいて「典型的な」トークンだけを選びます。さらに Mirostat は、生成中に Perplexity を一定に保つよう動的にサンプリングを調整するという、ユニークなアプローチをとります。

本記事の内容

  • 既存デコーディング手法の課題整理
  • Repetition Penalty と Frequency / Presence Penalty の数式と実装
  • Contrastive Search による品質と多様性の同時最適化
  • Typical Decoding の情報理論的な基礎
  • η-sampling と min-p sampling
  • Mirostat — Perplexity 目標による動的サンプリング
  • Python での各戦略のスクラッチ実装と比較実験

LLMデコーディング戦略の分類概念マップ

上図は本記事で扱うデコーディング戦略の全体像を示したものです。大きく「繰り返し抑制」「候補集合フィルタ」「品質+多様性の統合」「動的制御」の4カテゴリに整理でき、それぞれ異なる問題意識から設計されています。記事を読み進める際に、現在どのカテゴリの手法を学んでいるかを意識すると理解が深まります。

前提知識

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

画像なし
Temperature・Top-k・Top-pサンプリングを比較して理解する
LLMの基本的なサンプリング手法であるTemperature、Top-k、Top-pの数式と実装を解説します。
画像なし
ビームサーチの理論とPython実装
決定論的なテキスト生成手法であるビームサーチのアルゴリズムと実装を解説します。
画像なし
因果言語モデル(CLM)の仕組み
自己回帰による言語モデルの基本構造と学習方法を解説します。
言語モデルとPerplexity
言語モデルの評価指標であるPerplexityの定義と解釈を解説します。

既存デコーディング手法の課題

退化問題(Degeneration)

言語モデルの生成において最も深刻な問題の一つが退化(degeneration)です。退化とは、モデルが同じフレーズや文構造を延々と繰り返してしまう現象を指します。

たとえば、貪欲法(Greedy Decoding)で長文を生成すると、しばしば次のような出力に陥ります。

The cat sat on the mat. The cat sat on the mat. The cat sat on the mat...

これは貪欲法に限った問題ではありません。ビームサーチでも、ビーム幅を大きくすると逆に退化が起きやすくなることが知られています。Holtzman et al. (2020) は、人間が書くテキストのトークン確率分布と、言語モデルが高確率と判断するトークンの分布が体系的にずれていることを示しました。人間は必ずしも最高確率のトークンを選んでいるわけではなく、むしろ「驚きすぎず、退屈すぎない」程度のトークンを自然に使い分けています。

Temperature・Top-p の限界

Temperature・Top-k・Top-pサンプリングは多様性を制御する基本的な手法ですが、以下の限界があります。

Temperature の問題: – 温度を下げすぎると繰り返しが発生する – 温度を上げすぎると意味不明なトークンが混ざる – 最適な温度はコンテキストに依存し、固定値では対応しきれない

Top-p(Nucleus Sampling)の問題: – 分布が非常に鋭い場合(1トークンが支配的な場合)、Top-p でも事実上貪欲法になる – 分布が非常に平坦な場合、多数の低確率トークンが候補に入り品質が下がる – 「繰り返し」に対する直接的な抑制機構がない

品質と多様性のトレードオフ

理想的なテキスト生成には、2つの相反する性質が求められます。

  1. 品質(Coherence): 文法的に正しく、意味的に一貫したテキスト
  2. 多様性(Diversity): 退屈でない、バリエーション豊かなテキスト

既存手法の多くは、この2つをトレードオフの関係として扱い、一方を上げると他方が下がるという構造的限界を持っています。本記事で紹介する高度なデコーディング戦略は、このトレードオフをより賢く扱うことを目指しています。

それでは、まず繰り返し問題に直接対処する Repetition Penalty から見ていきましょう。

Temperatureがトークン確率分布の鋭さに与える影響

上図は Temperature を $0.3 \sim 1.5$ と変化させたときのトークン確率分布を示しています。低温では特定のトークンに確率が集中して「鋭い」分布になり、高温では分布が平坦化してエントロピーが増大します。この鋭さの変化こそが Temperature の本質的な作用で、後述する各手法はいずれもこの基本的な確率操作に工夫を加えたものと見なせます。

Repetition Penalty

直感的な理解

Repetition Penalty は名前の通り、「すでに生成されたトークンが再び選ばれにくくなるよう、ペナルティをかける」手法です。日常的なアナロジーで言えば、カラオケで同じ曲を連続で歌わないよう「さっき歌った曲は選曲リストの優先度を下げる」ルールを設けるようなものです。

この手法は Keskar et al. (2019) の “CTRL: A Conditional Transformer Language Model for Controllable Generation” で提案されました。仕組みは単純ですが、退化を効果的に抑制できるため、HuggingFace Transformers をはじめ多くのライブラリに標準実装されています。

数式の定義

語彙 $\mathcal{V}$ に対してモデルが出力するロジットを $z_i$($i \in \mathcal{V}$)とします。これまでに生成されたトークンの集合を $G$ としたとき、Repetition Penalty パラメータ $\theta > 0$ を用いて、ロジットを次のように変換します。

$$ z_i’ = \begin{cases} z_i / \theta & \text{if } i \in G \text{ and } z_i > 0 \\ z_i \cdot \theta & \text{if } i \in G \text{ and } z_i < 0 \\ z_i & \text{if } i \notin G \end{cases} $$

この変換が何をしているか、直感的に理解しましょう。$\theta > 1$(典型的には $1.0 \sim 2.0$)の場合を考えます。

  • 正のロジット $z_i > 0$ のトークンが $G$ に含まれる場合: $z_i / \theta$ により値が小さくなります。つまり、「すでに使ったトークンの魅力度を下げる」操作です。
  • 負のロジット $z_i < 0$ のトークンが $G$ に含まれる場合: $z_i \cdot \theta$ により値がさらに負方向に大きくなります。つまり、「もともと選ばれにくいトークンをさらに選ばれにくくする」操作です。

どちらの場合もロジットの絶対値を $0$ から遠ざける方向に動かすため、結果としてペナルティ対象トークンの選択確率が下がります。

$\theta = 1$ のときはペナルティなし(元のロジットのまま)、$\theta > 1$ でペナルティが強くなります。

ペナルティの効果の数学的分析

ペナルティが確率にどう影響するかを定量的に見てみましょう。2つのトークン $i \in G$(すでに生成済み)と $j \notin G$(未生成)を比較します。ともに正のロジットを持つ場合、ペナルティ適用後の確率比は次のようになります。

元の確率比:

$$ \frac{P(i)}{P(j)} = \frac{\exp(z_i)}{\exp(z_j)} = \exp(z_i – z_j) $$

ペナルティ適用後の確率比:

$$ \frac{P'(i)}{P'(j)} = \frac{\exp(z_i / \theta)}{\exp(z_j)} = \exp\left(\frac{z_i}{\theta} – z_j\right) $$

$\theta > 1$ のとき $z_i / \theta < z_i$ なので、ペナルティ適用後の確率比は元より小さくなり、生成済みトークン $i$ の相対的な選択確率が低下します。

この方法は単純ですが、一つ欠点があります。トークンが1回出現しても10回出現しても同じペナルティがかかるため、出現頻度を考慮できないのです。この問題に対処するのが、次に紹介する Frequency Penalty と Presence Penalty です。

Frequency Penalty と Presence Penalty

2つのペナルティの違い

OpenAI の GPT API で広く使われている Frequency Penalty と Presence Penalty は、繰り返し制御のもう一つのアプローチです。Repetition Penalty がロジットを乗除で変換するのに対し、こちらはロジットに加減算でペナルティを適用します。

直感的には次のように区別できます。

  • Frequency Penalty: 「たくさん使った単語ほど、次に使いにくくする」— 出現回数に比例したペナルティ
  • Presence Penalty: 「一度でも使った単語は、次に使いにくくする」— 出現の有無に基づく一律のペナルティ

数式の定義

これまでに生成されたトークン列で、トークン $i$ が出現した回数を $c_i$ とします。Frequency Penalty パラメータ $\alpha_f$ と Presence Penalty パラメータ $\alpha_p$ を用いて、ロジット $z_i$ を次のように変換します。

$$ z_i’ = z_i – \alpha_f \cdot c_i – \alpha_p \cdot \mathbb{1}[c_i > 0] $$

ここで $\mathbb{1}[c_i > 0]$ は指示関数で、トークン $i$ が少なくとも1回出現していれば $1$、そうでなければ $0$ を返します。

各パラメータの意味を整理しましょう。

パラメータ 範囲 効果
$\alpha_f$ (Frequency) $[-2, 2]$ $c_i$ に比例してロジットを減算。正の値で繰り返し抑制
$\alpha_p$ (Presence) $[-2, 2]$ 出現済みトークンのロジットを一律減算。正の値で話題の多様性を促進

Frequency Penalty はトークンの出現回数 $c_i$ に比例するため、頻繁に出現するトークンほど強くペナルティを受けます。「the」「is」のような機能語が過剰に使われるのを防ぐのに効果的です。

Presence Penalty は出現の有無のみを見るため、1回出現しても10回出現しても同じペナルティです。これは新しいトピックへの切り替えを促す効果があり、創造的なテキスト生成に向いています。

Repetition Penalty との比較

3つのペナルティ手法を比較すると、次のようになります。

手法 操作 出現回数の考慮 主な用途
Repetition Penalty 乗除($\times$ or $\div$) なし(有無のみ) 一般的な繰り返し抑制
Frequency Penalty 加減算 あり($c_i$ に比例) 高頻度トークンの抑制
Presence Penalty 加減算 なし(有無のみ) 話題の多様性促進

いずれのペナルティも「すでに出たトークンを避ける」という後処理的なアプローチです。しかし、テキストの品質と多様性を根本的に両立させるには、もっと構造的なアプローチが必要です。次に紹介する Contrastive Search は、まさにその問題に正面から取り組みます。

Contrastive Search

なぜ新しい探索手法が必要か

Su et al. (2022) は、”A Contrastive Framework for Neural Text Generation” において、既存のデコーディング手法の共通の限界を指摘しました。Temperature や Top-p などのサンプリング手法は確率分布の「形」を調整するだけであり、生成されるトークンの表現空間(embedding space)上での多様性を考慮していません。

これはどういうことでしょうか。言語モデルの内部では、各トークンは高次元ベクトル(隠れ状態)として表現されています。意味的に似たトークンは、この空間上で近い位置にあります。確率だけを見てトークンを選ぶと、連続するトークンが表現空間上で非常に近くなり、結果として意味的に冗長なテキストが生成されてしまいます。これが退化の本質的な原因です。

Contrastive Search は「確率が高く、かつ、直前のコンテキストと表現空間上で適度に離れているトークン」を選ぶことで、品質と多様性を同時に最適化します。

数式の定義

Contrastive Search では、次のトークン $y_t$ を以下のスコア関数を最大化するトークンとして選びます。

$$ y_t = \argmax_{v \in V^{(k)}} \Big\{ (1 – \alpha) \cdot P_\text{model}(v \mid \bm{y}_{

この式の各要素を分解して見ていきましょう。

$V^{(k)}$: モデルの確率分布で上位 $k$ 個のトークン候補集合です。全語彙を探索すると計算コストが大きいため、まず確率上位 $k$ 個に絞ります。

第1項: $(1 – \alpha) \cdot P_\text{model}(v \mid \bm{y}_{ — これはモデルの予測確率そのものです。確率が高いトークンほどスコアが高くなります。これが品質を担保する項です。

第2項: $\alpha \cdot \max_{j} \text{sim}(\bm{h}_v, \bm{h}_{y_j})$ — これは候補トークン $v$ の隠れ状態 $\bm{h}_v$ と、これまでに生成された全トークンの隠れ状態 $\bm{h}_{y_j}$ との最大コサイン類似度です。この値が大きいということは、過去の文脈と意味的に「被っている」ことを意味します。この項は減算されるので、過去と似ているトークンほどスコアが下がる——つまり多様性を促す項として機能します。

ここで $\text{sim}(\bm{h}_v, \bm{h}_{y_j})$ はコサイン類似度です。

$$ \text{sim}(\bm{h}_v, \bm{h}_{y_j}) = \frac{\bm{h}_v \cdot \bm{h}_{y_j}}{\|\bm{h}_v\| \|\bm{h}_{y_j}\|} $$

ハイパーパラメータ $\alpha$ の役割

パラメータ $\alpha \in [0, 1]$ は品質と多様性のバランスを制御します。

  • $\alpha = 0$: 第2項が消え、純粋に確率最大のトークンを選ぶ(貪欲法と同等)
  • $\alpha = 1$: 第1項が消え、過去の文脈と最も異なるトークンを選ぶ(意味は通らなくなる)
  • $\alpha \approx 0.6$: 元論文で推奨される値。品質を保ちながら適度な多様性を実現

$k$ は候補集合の大きさで、典型的には $k = 5 \sim 10$ が使われます。$k$ が小さすぎると多様性が制限され、大きすぎると低確率の不適切なトークンが混入するリスクがあります。

Contrastive Search の利点

Contrastive Search の特筆すべき点は、確率的ではなく決定論的であることです。温度やサンプリングのランダム性に依存しないため、同じ入力に対して常に同じ出力が得られます。それでいて、表現空間上の類似度による制約が多様性を担保するため、貪欲法のような退化は起きにくくなっています。

ただし、各ステップで全候補トークンの隠れ状態を計算し、過去の全トークンとの類似度を求める必要があるため、計算コストは通常のサンプリングより高くなります。

次に、情報理論に基づくもう一つのエレガントなアプローチ、Typical Decoding を見ていきましょう。

Typical Decoding

情報理論からの発想

Typical Decoding(Meister et al., 2023)は、情報理論の「典型集合(typical set)」という概念に基づく手法です。

まずは直感的に理解しましょう。人間が文章を書くとき、毎回「最も予測可能な単語」を選ぶわけではありません。かといって、完全に予測不可能な単語を使うわけでもありません。実際には、「ちょうどよい程度に予測可能な」単語を自然に使い分けています。

情報理論の言葉で言えば、人間が書くテキストは確率分布のエントロピーに近い情報量を持つトークンで構成されている、ということです。この考え方を形式化したのが Typical Decoding です。

エントロピーと情報量の復習

トークン $v$ の情報量(自己情報量)は次のように定義されます。

$$ I(v) = -\log_2 P(v \mid \bm{y}_{

確率が高いトークンほど情報量が小さく(「予測しやすい」)、確率が低いトークンほど情報量が大きい(「驚きが大きい」)ことを表します。

条件付きエントロピー(期待情報量)は、分布全体の「平均的な驚き度」を表します。

$$ H_t = -\sum_{v \in \mathcal{V}} P(v \mid \bm{y}_{

Typical Decoding の定義

Typical Decoding は、各トークンの情報量がエントロピー $H_t$ に「近い」かどうかで候補を絞ります。

まず、各トークン $v$ について、情報量とエントロピーの差の絶対値を計算します。

$$ \delta(v) = \left| I(v) – H_t \right| = \left| -\log_2 P(v \mid \bm{y}_{

この $\delta(v)$ が小さいトークンは「典型的」——つまり、確率分布の平均的な驚き度に近い情報量を持つトークンです。

次に、$\delta(v)$ が小さい順にトークンをソートし、確率の累積和がしきい値 $\tau$(典型的には $0.2 \sim 0.95$)を超えるまでのトークン集合 $\mathcal{T}_\tau$ を構成します。

$$ \mathcal{T}_\tau = \text{smallest set such that} \sum_{v \in \mathcal{T}_\tau} P(v \mid \bm{y}_{

ただし、$\mathcal{T}_\tau$ のトークンは $\delta(v)$ の昇順に追加されます。最後に、$\mathcal{T}_\tau$ 内のトークンの確率を再正規化し、そこからサンプリングします。

Top-p との違い

Typical Decoding と Top-p サンプリングは、どちらも動的にトークン候補集合を構成するという点で似ていますが、候補の選び方が根本的に異なります。

  • Top-p: 確率が高い順にトークンを追加(高確率トークンを優先)
  • Typical Decoding: エントロピーに近い情報量を持つ順にトークンを追加(「典型的」なトークンを優先)

この違いは、確率分布が非常に鋭い場合に顕著になります。Top-p は最高確率のトークンをほぼ確定的に選んでしまいますが、Typical Decoding はそのトークンの情報量がエントロピーからかけ離れていれば(つまり「予測しやすすぎる」場合は)候補から外す可能性があります。これにより、退化を防ぎつつ自然な多様性を維持できます。

Typical Decoding は情報理論に根差した美しいアプローチですが、実用上は他のサンプリング手法と組み合わせて使われることも多いです。次に、より実用的な2つの手法——η-sampling と min-p sampling——を紹介します。

η-sampling と min-p sampling

η-sampling

η-sampling(イータサンプリング)は Hewitt et al. (2022) が提案した手法で、Typical Decoding の考え方をより直接的に実装したものです。

η-sampling では、エントロピー $H_t$ から動的にしきい値を計算し、確率がそのしきい値を下回るトークンを除外します。

$$ \eta_t = \min\left(\epsilon, \sqrt{\epsilon} \cdot e^{-H_t}\right) $$

ここで $\epsilon$ は小さな正の定数(ハイパーパラメータ)です。この式の意味を考えてみましょう。

エントロピー $H_t$ が大きい(分布が平坦な)とき、$e^{-H_t}$ は小さくなり、しきい値 $\eta_t$ も小さくなります。つまり、確率分布が不確実なときはより多くのトークンを候補に残します。逆に、エントロピーが小さい(分布が鋭い)ときはしきい値が大きくなり、候補が少数に絞られます。

この後、確率 $P(v \mid \bm{y}_{

$$ \mathcal{C}_t = \{v \in \mathcal{V} \mid P(v \mid \bm{y}_{

min-p sampling

min-p sampling は、Top-p とは逆のアプローチで候補集合を構成する手法です。実装が非常にシンプルでありながら、効果的に低品質トークンを除外できます。

アイデアは単純です。最大確率トークンの確率 $p_\text{max}$ に対して、ある比率 $p_\text{base}$ を掛けた値を下回るトークンを除外します。

$$ \mathcal{C}_t = \{v \in \mathcal{V} \mid P(v \mid \bm{y}_{

ここで $p_\text{max} = \max_{v \in \mathcal{V}} P(v \mid \bm{y}_{

$p_\text{base}$ は典型的には $0.05 \sim 0.2$ 程度に設定されます。たとえば $p_\text{base} = 0.1$ のとき、最大確率が $0.6$ なら $0.06$ 未満の確率のトークンが除外され、最大確率が $0.02$ なら $0.002$ 未満の確率のトークンが除外されます。

min-p の魅力は、分布の形に自動的に適応することです。分布が鋭いときは厳しくフィルタリングし、平坦なときは多くの候補を残します。この適応性は Top-p にはない特徴です。Top-p では、$p = 0.9$ と固定すると、分布の形に関係なく累積確率が $0.9$ になるまでトークンを含めてしまいます。

η-sampling と min-p の比較

特徴 η-sampling min-p sampling
しきい値の基準 エントロピーから計算 最大確率からの相対比率
適応性 エントロピーに連動 最大確率に連動
実装の複雑さ エントロピー計算が必要 最大確率のみで済む
ハイパーパラメータ $\epsilon$(小さな定数) $p_\text{base}$(比率)

どちらも「コンテキストに応じて動的に候補を絞る」という思想を共有していますが、min-p の方が実装が簡単で直感的に理解しやすいため、実用上は min-p が広く採用される傾向にあります。

ここまでの手法はすべて「各ステップで候補を絞る」アプローチでした。次に紹介する Mirostat は、発想を根本的に変え、「生成全体の Perplexity を一定に保つ」というグローバルな視点から動的にサンプリングを制御します。

Mirostat — Perplexity 目標の動的サンプリング

制御理論からの着想

Mirostat(Basu et al., 2021)は、これまでの手法とは根本的に異なるアプローチをとります。名前の由来はギリシャ語で「一定に保つ」を意味する miro + stat です。

人間が書くテキストの Perplexity は、文章のジャンルやスタイルによってある程度一定の範囲に収まる傾向があります。たとえば、技術文書はPerplexityが低く(予測しやすい)、詩や小説は高い(予測しにくい)傾向があります。

Mirostat のアイデアは、目標 Perplexity $\tau$ を設定し、生成されるテキストの Perplexity がその目標に追従するよう、サンプリングのパラメータを動的に調整するというものです。これは制御工学におけるフィードバック制御の発想そのものです。

Perplexity と surprisal の関係

まず、Perplexity と surprisal(自己情報量)の関係を整理しましょう。

テキストの Perplexity は、各トークンの surprisal の指数平均として定義されます。局所的には、ステップ $t$ での surprisal は次のようになります。

$$ s_t = -\log_2 P(y_t \mid \bm{y}_{

目標 Perplexity を $\tau$ とすると、目標 surprisal は次のようになります。

$$ s^* = \log_2 \tau $$

Mirostat は、各ステップの surprisal $s_t$ が目標 $s^*$ に近づくよう制御します。

Mirostat のアルゴリズム

Mirostat には v1 と v2 の2つのバージョンがあります。ここでは、より広く使われている Mirostat v2 を解説します。

Mirostat v2 は、各ステップで動的にしきい値 $\mu_t$ を更新し、このしきい値を用いて Top-k のような候補フィルタリングを行います。

ステップ1: 候補のフィルタリング

surprisal がしきい値 $\mu_t$ 以下のトークンを候補に残します。

$$ \mathcal{C}_t = \{v \in \mathcal{V} \mid -\log_2 P(v \mid \bm{y}_{

surprisal $\leq \mu_t$ ということは、確率 $\geq 2^{-\mu_t}$ のトークンだけが候補に残るということです。$\mu_t$ が大きいほど多くのトークンが候補に入り(多様な生成)、小さいほど少数の高確率トークンに限定されます(保守的な生成)。

ステップ2: サンプリング

$\mathcal{C}_t$ 内のトークンの確率を再正規化し、そこからサンプリングしてトークン $y_t$ を選びます。

ステップ3: しきい値の更新

選ばれたトークン $y_t$ の surprisal $s_t$ と目標 surprisal $s^*$ の差に基づいて、しきい値 $\mu_t$ を更新します。

$$ \mu_{t+1} = \mu_t + \eta (s^* – s_t) $$

ここで $\eta$ は学習率で、制御の追従速度を決めます。この更新則は非常に直感的です。

  • $s_t > s^*$(生成されたトークンの surprisal が目標より大きい = 予測しにくいトークンが選ばれた)のとき: $\mu_{t+1} < \mu_t$ となり、次のステップではしきい値が下がります。つまり、候補がより高確率トークンに絞られ、より「予測しやすい」トークンが選ばれやすくなります。
  • $s_t < s^*$(予測しやすいトークンが選ばれた)のとき: $\mu_{t+1} > \mu_t$ となり、候補が広がり、より多様なトークンが選ばれやすくなります。

このフィードバック機構により、生成テキスト全体の Perplexity が目標 $\tau$ の近傍に維持されます。

Mirostat のハイパーパラメータ

パラメータ 意味 典型的な値
$\tau$ 目標 Perplexity $3 \sim 8$(タスク依存)
$\eta$ 学習率(追従速度) $0.1$(デフォルト)
$\mu_0$ 初期しきい値 $2 \cdot \log_2 \tau$(推奨)

$\tau$ が小さいほど保守的で予測しやすい(退屈な)テキストが生成され、大きいほど多様で予測しにくい(時に不自然な)テキストが生成されます。しかし、Temperature とは異なり、Mirostat は生成の進行中に自動的にバランスを調整するため、固定パラメータで起きがちな「途中から退化する」「途中から破綻する」という問題が緩和されます。

ここまでで、各デコーディング戦略の理論的背景を理解しました。次に、これらをすべて Python で実装し、実際に動作を比較してみましょう。

Python での各戦略の実装

共通のセットアップ

まず、すべての手法で共通して使うユーティリティを準備します。ここでは、簡単なロジットベクトルを使って各手法の挙動を確認するため、実際の言語モデルは使わず、合成的なロジット分布でシミュレーションします。

import numpy as np
import matplotlib.pyplot as plt
from scipy.special import softmax

np.random.seed(42)

def create_logits(vocab_size=50, spike_idx=None, spike_val=5.0):
    """テスト用のロジットベクトルを生成"""
    logits = np.random.randn(vocab_size)
    if spike_idx is not None:
        logits[spike_idx] = spike_val
    return logits

# 基本的なロジット(語彙サイズ50)
logits = create_logits(vocab_size=50, spike_idx=0, spike_val=5.0)
probs = softmax(logits)

print(f"語彙サイズ: {len(logits)}")
print(f"最大確率トークン: index={np.argmax(probs)}, prob={probs[np.argmax(probs)]:.4f}")
print(f"エントロピー: {-np.sum(probs * np.log2(probs + 1e-12)):.4f} bits")

このコードでは、語彙サイズ50のロジットベクトルを生成しています。インデックス0のトークンに高いロジット値(5.0)を設定し、1つのトークンが支配的な分布を作っています。エントロピーは分布の不確実さの度合いを表し、後の Typical Decoding や Mirostat で重要な役割を果たします。

Repetition Penalty の実装

import numpy as np
from scipy.special import softmax

def apply_repetition_penalty(logits, generated_tokens, theta=1.2):
    """
    Repetition Penalty を適用する

    Parameters:
        logits: ロジットベクトル (numpy array)
        generated_tokens: 生成済みトークンのリスト
        theta: ペナルティパラメータ (> 1 でペナルティ強化)

    Returns:
        ペナルティ適用後のロジット
    """
    logits_penalized = logits.copy()
    for token_id in set(generated_tokens):
        if logits_penalized[token_id] > 0:
            logits_penalized[token_id] /= theta
        else:
            logits_penalized[token_id] *= theta
    return logits_penalized

# テスト: トークン0と3がすでに生成済みとする
logits = create_logits(vocab_size=50, spike_idx=0, spike_val=5.0)
generated = [0, 3, 0, 5, 0]  # トークン0が3回出現

# 異なるペナルティ強度での比較
thetas = [1.0, 1.2, 1.5, 2.0]
fig, axes = plt.subplots(1, len(thetas), figsize=(16, 4))

for ax, theta in zip(axes, thetas):
    penalized = apply_repetition_penalty(logits, generated, theta=theta)
    probs_pen = softmax(penalized)
    top_k = 10
    top_indices = np.argsort(probs_pen)[-top_k:][::-1]

    colors = ['#e74c3c' if idx in set(generated) else '#3498db'
              for idx in top_indices]
    ax.barh(range(top_k), probs_pen[top_indices], color=colors)
    ax.set_yticks(range(top_k))
    ax.set_yticklabels([f'Token {i}' for i in top_indices])
    ax.set_xlabel('Probability')
    ax.set_title(f'θ = {theta}')
    ax.invert_yaxis()

plt.suptitle('Repetition Penalty: Effect of θ', fontsize=14)
plt.tight_layout()
plt.show()

上のグラフから、ペナルティパラメータ $\theta$ の効果が視覚的に確認できます。$\theta = 1.0$(ペナルティなし)ではトークン0が圧倒的に高い確率を持っていますが、$\theta$ を大きくするにつれて生成済みトークン(赤色のバー)の確率が低下し、未生成トークン(青色のバー)の相対的な確率が上昇しています。$\theta = 2.0$ では、もともと支配的だったトークン0の確率が大幅に下がり、他のトークンとの差が縮まっていることがわかります。

Repetition Penaltyのthetaによる繰り返し抑制効果

上図では赤いバーが「生成済みトークン」、青いバーが「未生成トークン」を示しています。$\theta = 1.0$ ではトークン0が圧倒的に高い確率を占めていますが、$\theta$ を上げるにつれて赤いバーが縮まり、他のトークンが浮上してくる様子が一目でわかります。$\theta = 2.0$ では生成済みトークン3本の確率が大幅に抑制され、多様なトークンへの分散が実現されています。

Frequency Penalty と Presence Penalty の実装

import numpy as np
from scipy.special import softmax

def apply_frequency_presence_penalty(logits, token_counts, alpha_f=0.0, alpha_p=0.0):
    """
    Frequency Penalty と Presence Penalty を適用する

    Parameters:
        logits: ロジットベクトル
        token_counts: 各トークンの出現回数 (dict: token_id -> count)
        alpha_f: Frequency Penalty パラメータ
        alpha_p: Presence Penalty パラメータ

    Returns:
        ペナルティ適用後のロジット
    """
    logits_penalized = logits.copy()
    for token_id, count in token_counts.items():
        logits_penalized[token_id] -= alpha_f * count
        if count > 0:
            logits_penalized[token_id] -= alpha_p
    return logits_penalized

# テスト: トークンの出現回数
logits = create_logits(vocab_size=50, spike_idx=0, spike_val=5.0)
token_counts = {0: 5, 3: 2, 5: 1, 8: 3}

# Frequency vs Presence の比較
fig, axes = plt.subplots(1, 3, figsize=(15, 5))

configs = [
    {'alpha_f': 0.5, 'alpha_p': 0.0, 'title': 'Frequency Only (α_f=0.5)'},
    {'alpha_f': 0.0, 'alpha_p': 1.0, 'title': 'Presence Only (α_p=1.0)'},
    {'alpha_f': 0.3, 'alpha_p': 0.5, 'title': 'Combined (α_f=0.3, α_p=0.5)'},
]

for ax, config in zip(axes, configs):
    penalized = apply_frequency_presence_penalty(
        logits, token_counts,
        alpha_f=config['alpha_f'], alpha_p=config['alpha_p']
    )
    probs_pen = softmax(penalized)

    # 出現済みトークンと未出現トークンの確率を比較
    appeared = list(token_counts.keys())
    appeared_probs = [probs_pen[i] for i in appeared]
    appeared_labels = [f'Token {i}\n(count={token_counts[i]})' for i in appeared]

    colors = plt.cm.Reds(np.linspace(0.3, 0.9, len(appeared)))
    ax.bar(range(len(appeared)), appeared_probs, color=colors)
    ax.set_xticks(range(len(appeared)))
    ax.set_xticklabels(appeared_labels, fontsize=9)
    ax.set_ylabel('Probability')
    ax.set_title(config['title'])

plt.suptitle('Frequency vs Presence Penalty on Previously Generated Tokens', fontsize=13)
plt.tight_layout()
plt.show()

このグラフから、Frequency Penalty と Presence Penalty の違いが明確に読み取れます。Frequency Only(左)では、出現回数が多いトークン(Token 0: 5回)ほど確率が大きく低下しています。一方、Presence Only(中央)では出現回数に関係なく一律にペナルティがかかるため、1回出現のトークン(Token 5)も5回出現のトークン(Token 0)も同程度の減衰を受けています。Combined(右)は両者のバランスをとり、出現頻度を考慮しつつ全体的に多様性を促進する挙動を示しています。

Frequency PenaltyとPresence Penaltyの出現頻度の扱い方の違い

上図のグレーが「ペナルティ前」、色付きが「ペナルティ後」の確率を示しています。Frequency のみ(左)ではペナルティ前後の差が出現回数に比例して開いており、5回出現のトークン0が最も大きく抑制されています。Presence のみ(中央)は1回でも5回でも同じ幅だけ縮小されており、出現回数に依存しない一律抑制を確認できます。

Contrastive Search の実装

import numpy as np
from scipy.special import softmax

def contrastive_search(logits, hidden_states_candidates, hidden_states_context,
                       alpha=0.6, k=10):
    """
    Contrastive Search のスコアを計算する

    Parameters:
        logits: ロジットベクトル
        hidden_states_candidates: 候補トークンの隠れ状態 (k, d)
        hidden_states_context: 過去の生成トークンの隠れ状態 (t, d)
        alpha: 品質と多様性のバランスパラメータ
        k: 候補トークン数

    Returns:
        選択されたトークンのインデックス, スコア配列
    """
    probs = softmax(logits)
    top_k_indices = np.argsort(probs)[-k:][::-1]
    top_k_probs = probs[top_k_indices]

    # コサイン類似度の計算
    def cosine_sim(a, b):
        return np.dot(a, b) / (np.linalg.norm(a) * np.linalg.norm(b) + 1e-8)

    scores = []
    for i, (idx, prob) in enumerate(zip(top_k_indices, top_k_probs)):
        # 各候補と過去の全トークンとの最大コサイン類似度
        max_sim = max(
            cosine_sim(hidden_states_candidates[i], h)
            for h in hidden_states_context
        )
        # Contrastive Score
        score = (1 - alpha) * prob - alpha * max_sim
        scores.append(score)

    scores = np.array(scores)
    best_idx = np.argmax(scores)
    return top_k_indices[best_idx], scores, top_k_indices

# シミュレーション: 合成的な隠れ状態でテスト
np.random.seed(42)
hidden_dim = 64
vocab_size = 50
k = 10

logits = create_logits(vocab_size=vocab_size, spike_idx=0, spike_val=5.0)
probs = softmax(logits)
top_k_indices = np.argsort(probs)[-k:][::-1]

# 候補トークンの隠れ状態
h_candidates = np.random.randn(k, hidden_dim)
# 過去の生成トークンの隠れ状態(Token 0に近いベクトルを含む)
h_context = np.random.randn(5, hidden_dim)
h_context[0] = h_candidates[0] + np.random.randn(hidden_dim) * 0.1  # Token 0に類似

# 異なるαでの比較
alphas = [0.0, 0.3, 0.6, 0.9]
fig, axes = plt.subplots(2, 2, figsize=(12, 10))

for ax, alpha in zip(axes.ravel(), alphas):
    selected, scores, indices = contrastive_search(
        logits, h_candidates, h_context, alpha=alpha, k=k
    )

    colors = ['#e74c3c' if idx == selected else '#3498db' for idx in indices]
    ax.barh(range(k), scores, color=colors)
    ax.set_yticks(range(k))
    ax.set_yticklabels([f'Token {i}' for i in indices])
    ax.set_xlabel('Contrastive Score')
    ax.set_title(f'α = {alpha} (selected: Token {selected})')
    ax.invert_yaxis()
    ax.axvline(x=0, color='gray', linestyle='--', alpha=0.5)

plt.suptitle('Contrastive Search: Effect of α on Token Selection', fontsize=14)
plt.tight_layout()
plt.show()

この可視化から、$\alpha$ の変化が選択に与える影響が明確にわかります。$\alpha = 0$(左上)では純粋に確率最大のトークン(Token 0)が選ばれますが、$\alpha$ を上げるにつれて、過去のコンテキストと類似度が高いToken 0のスコアが低下し、他のトークンが選ばれるようになります。$\alpha = 0.6$(推奨値)では、確率はそこそこ高いが過去の文脈と異なるトークンが選ばれており、品質と多様性のバランスがとれていることが確認できます。$\alpha = 0.9$ ではほぼ多様性のみが重視され、確率の低いトークンが選ばれるリスクがあります。

Contrastive SearchのalphaによるトークンスコアのQ-D分解

上図では緑のバーが「品質項 $(1-\alpha) \cdot P$」、赤のバーが「多様性ペナルティ項 $-\alpha \cdot \text{sim}$」を示しており、金色の縦線が最終的に選ばれるトークンを表しています。$\alpha = 0$ では品質項だけが残りトークン0が選ばれていますが、$\alpha = 0.6$ では多様性ペナルティによってトークン0のネットスコアが抑制され、文脈と異なる別のトークンが浮上しています。品質と多様性の2項を視覚的に分解することで、Contrastive Search が単純なサンプリングとは根本的に異なる選択機構であることが確認できます。

Typical Decoding の実装

import numpy as np
from scipy.special import softmax

def typical_decoding(logits, tau=0.95, temperature=1.0):
    """
    Typical Decoding を適用する

    Parameters:
        logits: ロジットベクトル
        tau: 累積確率のしきい値
        temperature: Temperature パラメータ

    Returns:
        サンプリング結果のトークンインデックス, フィルタ後の確率分布
    """
    probs = softmax(logits / temperature)

    # エントロピーの計算
    entropy = -np.sum(probs * np.log2(probs + 1e-12))

    # 各トークンの情報量
    info = -np.log2(probs + 1e-12)

    # エントロピーとの差の絶対値
    delta = np.abs(info - entropy)

    # δが小さい順にソート
    sorted_indices = np.argsort(delta)

    # 累積確率がτを超えるまでトークンを追加
    cumsum = 0.0
    typical_set = []
    for idx in sorted_indices:
        typical_set.append(idx)
        cumsum += probs[idx]
        if cumsum >= tau:
            break

    # 再正規化
    typical_probs = np.zeros_like(probs)
    for idx in typical_set:
        typical_probs[idx] = probs[idx]
    typical_probs /= typical_probs.sum()

    # サンプリング
    sampled = np.random.choice(len(logits), p=typical_probs)
    return sampled, typical_probs, entropy, delta

# Typical Decoding vs Top-p の比較
np.random.seed(42)
logits = create_logits(vocab_size=50, spike_idx=0, spike_val=5.0)
probs_original = softmax(logits)

# Top-p (Nucleus) サンプリング
def top_p_filter(probs, p=0.95):
    sorted_indices = np.argsort(probs)[::-1]
    cumsum = np.cumsum(probs[sorted_indices])
    cutoff = np.searchsorted(cumsum, p) + 1
    selected = sorted_indices[:cutoff]
    filtered = np.zeros_like(probs)
    for idx in selected:
        filtered[idx] = probs[idx]
    filtered /= filtered.sum()
    return filtered

top_p_probs = top_p_filter(probs_original, p=0.95)
_, typical_probs, entropy, delta = typical_decoding(logits, tau=0.95)

fig, axes = plt.subplots(1, 3, figsize=(18, 5))

# 元の分布
top_n = 15
sorted_idx = np.argsort(probs_original)[-top_n:][::-1]

axes[0].bar(range(top_n), probs_original[sorted_idx], color='#3498db')
axes[0].set_xticks(range(top_n))
axes[0].set_xticklabels([f'T{i}' for i in sorted_idx], fontsize=8)
axes[0].set_ylabel('Probability')
axes[0].set_title('Original Distribution')

# Top-p
top_p_sorted = top_p_probs[sorted_idx]
colors_tp = ['#2ecc71' if p > 0 else '#bdc3c7' for p in top_p_sorted]
axes[1].bar(range(top_n), top_p_sorted, color=colors_tp)
axes[1].set_xticks(range(top_n))
axes[1].set_xticklabels([f'T{i}' for i in sorted_idx], fontsize=8)
axes[1].set_ylabel('Probability')
axes[1].set_title(f'Top-p (p=0.95)\n{np.sum(top_p_probs > 0)} tokens selected')

# Typical Decoding
typical_sorted = typical_probs[sorted_idx]
colors_td = ['#e74c3c' if p > 0 else '#bdc3c7' for p in typical_sorted]
axes[2].bar(range(top_n), typical_sorted, color=colors_td)
axes[2].set_xticks(range(top_n))
axes[2].set_xticklabels([f'T{i}' for i in sorted_idx], fontsize=8)
axes[2].set_ylabel('Probability')
axes[2].set_title(f'Typical Decoding (τ=0.95)\n{np.sum(typical_probs > 0)} tokens, H={entropy:.2f} bits')

plt.suptitle('Top-p vs Typical Decoding: Which Tokens Are Selected?', fontsize=14)
plt.tight_layout()
plt.show()

この比較可視化には重要な違いが表れています。Top-p(中央、緑)は確率が高い順にトークンを選んでいるため、最高確率のToken 0が圧倒的に支配的です。一方、Typical Decoding(右、赤)はエントロピーに近い情報量を持つトークンを優先するため、Token 0のような「予測しやすすぎる」トークンの確率が相対的に下がり、中程度の確率を持つ複数のトークンに分散されています。これが Typical Decoding の本質的な特徴——「典型的な驚き度」のトークンを選好する——を視覚的に示しています。

Top-pとTypical Decodingの候補トークン選び方の違い

上図では左が元の分布、中央が Top-p 適用後(緑)、右が Typical Decoding 適用後(赤)です。Top-p は確率の高いトークンを上から詰めるため、T0の支配が続いています。一方 Typical Decoding ではT0の確率が相対的に小さくなり、中位の確率帯のトークンが選ばれやすくなっています。選択方針の違いが再正規化後の分布の「形」にはっきり現れている点に注目してください。

min-p sampling の実装

import numpy as np
from scipy.special import softmax

def min_p_sampling(logits, p_base=0.1, temperature=1.0):
    """
    min-p sampling を適用する

    Parameters:
        logits: ロジットベクトル
        p_base: 最大確率に対する比率しきい値
        temperature: Temperature パラメータ

    Returns:
        サンプリング結果, フィルタ後の確率分布
    """
    probs = softmax(logits / temperature)
    p_max = np.max(probs)
    threshold = p_base * p_max

    # しきい値以上のトークンだけ残す
    filtered = np.where(probs >= threshold, probs, 0.0)
    filtered /= filtered.sum()

    sampled = np.random.choice(len(logits), p=filtered)
    return sampled, filtered, threshold

def eta_sampling(logits, epsilon=0.001, temperature=1.0):
    """
    η-sampling を適用する

    Parameters:
        logits: ロジットベクトル
        epsilon: しきい値計算用の定数
        temperature: Temperature パラメータ

    Returns:
        サンプリング結果, フィルタ後の確率分布
    """
    probs = softmax(logits / temperature)
    entropy = -np.sum(probs * np.log2(probs + 1e-12))

    # 動的しきい値の計算
    eta = min(epsilon, np.sqrt(epsilon) * np.exp(-entropy))

    # しきい値以上のトークンだけ残す
    filtered = np.where(probs >= eta, probs, 0.0)
    if filtered.sum() == 0:
        filtered = probs.copy()
    filtered /= filtered.sum()

    sampled = np.random.choice(len(logits), p=filtered)
    return sampled, filtered, eta

# 異なる分布形状での min-p の適応性を確認
np.random.seed(42)
fig, axes = plt.subplots(2, 3, figsize=(18, 10))

# 鋭い分布と平坦な分布を作成
distributions = [
    ("Sharp (spike=8.0)", create_logits(50, spike_idx=0, spike_val=8.0)),
    ("Medium (spike=4.0)", create_logits(50, spike_idx=0, spike_val=4.0)),
    ("Flat (no spike)", np.random.randn(50) * 0.5),
]

for col, (name, logits_test) in enumerate(distributions):
    probs_test = softmax(logits_test)
    _, min_p_probs, thresh = min_p_sampling(logits_test, p_base=0.1)

    top_n = 15
    sorted_idx = np.argsort(probs_test)[-top_n:][::-1]

    # 元の分布
    axes[0][col].bar(range(top_n), probs_test[sorted_idx], color='#3498db', alpha=0.7)
    axes[0][col].set_title(f'{name}\nOriginal')
    axes[0][col].set_xticks(range(top_n))
    axes[0][col].set_xticklabels([f'T{i}' for i in sorted_idx], fontsize=7)

    # min-p 適用後
    min_p_sorted = min_p_probs[sorted_idx]
    colors = ['#2ecc71' if p > 0 else '#bdc3c7' for p in min_p_sorted]
    axes[1][col].bar(range(top_n), min_p_sorted, color=colors, alpha=0.7)
    n_selected = np.sum(min_p_probs > 0)
    axes[1][col].set_title(f'min-p (p_base=0.1)\n{n_selected} tokens, thresh={thresh:.4f}')
    axes[1][col].set_xticks(range(top_n))
    axes[1][col].set_xticklabels([f'T{i}' for i in sorted_idx], fontsize=7)

for ax_row in axes:
    for ax in ax_row:
        ax.set_ylabel('Probability')

plt.suptitle('min-p Sampling: Adaptive Behavior Across Distribution Shapes', fontsize=14)
plt.tight_layout()
plt.show()

この実験結果から、min-p sampling の適応性が明確にわかります。鋭い分布(左)ではしきい値が高くなり、わずか数個のトークンだけが候補に残ります。中程度の分布(中央)では適度な数のトークンが選ばれ、平坦な分布(右)ではしきい値が低くなるため多数のトークンが候補に残ります。つまり、min-p は分布の形状に自動的に適応し、鋭いときは品質を重視、平坦なときは多様性を許容するという合理的な挙動を示しています。

min-p Samplingの分布形状への自動適応フィルタリング

上図の上段が「元の分布」、下段が「min-p 適用後」の分布です。鋭い分布(左列)ではオレンジのバーが1本だけ残り、絞り込みが非常に厳しいことがわかります。平坦な分布(右列)では下段のオレンジのバーが多数残っており、多様なトークンに選択肢が広がっています。Top-p が常に累積確率一定でフィルタするのに対し、min-p は「最大確率との相対比」を基準にするため、このような適応的な挙動が実現できることを視覚的に確認できます。

Mirostat v2 の実装

import numpy as np
from scipy.special import softmax

def mirostat_v2_step(logits, mu, tau=5.0, eta=0.1):
    """
    Mirostat v2 の1ステップを実行する

    Parameters:
        logits: ロジットベクトル
        mu: 現在のsurprisalしきい値
        tau: 目標Perplexity
        eta: 学習率

    Returns:
        サンプリングされたトークン, 更新後のmu, surprisal
    """
    probs = softmax(logits)

    # 目標surprisal
    target_surprise = np.log2(tau)

    # surprisal がμ以下のトークンを候補にする
    surprisals = -np.log2(probs + 1e-12)
    mask = surprisals <= mu

    if not np.any(mask):
        # 候補がない場合、最高確率トークンだけを候補にする
        mask = np.zeros_like(probs, dtype=bool)
        mask[np.argmax(probs)] = True

    filtered_probs = np.where(mask, probs, 0.0)
    filtered_probs /= filtered_probs.sum()

    # サンプリング
    sampled = np.random.choice(len(logits), p=filtered_probs)
    sampled_surprise = -np.log2(probs[sampled] + 1e-12)

    # μの更新
    mu_new = mu + eta * (target_surprise - sampled_surprise)

    return sampled, mu_new, sampled_surprise

def simulate_mirostat(n_steps=100, tau=5.0, eta=0.1, vocab_size=50):
    """Mirostat v2 の動的サンプリングをシミュレーション"""
    mu = 2 * np.log2(tau)  # 初期しきい値

    surprisals = []
    mus = []
    n_candidates = []

    for step in range(n_steps):
        # 各ステップでランダムなロジットを生成(異なるコンテキストをシミュレート)
        logits = np.random.randn(vocab_size) * (1.5 + 0.5 * np.sin(step / 10))
        # 少しのスパイクを入れる
        spike = np.random.randint(0, vocab_size)
        logits[spike] += np.random.uniform(2.0, 6.0)

        probs = softmax(logits)
        surp = -np.log2(probs + 1e-12)
        n_cand = np.sum(surp <= mu)
        n_candidates.append(n_cand)

        sampled, mu, sampled_surprise = mirostat_v2_step(logits, mu, tau=tau, eta=eta)

        surprisals.append(sampled_surprise)
        mus.append(mu)

    return surprisals, mus, n_candidates

# シミュレーション実行
np.random.seed(42)
taus = [3.0, 5.0, 8.0]
fig, axes = plt.subplots(3, 1, figsize=(14, 12))

for tau in taus:
    surprisals, mus, n_cands = simulate_mirostat(n_steps=200, tau=tau, eta=0.1)
    target = np.log2(tau)

    axes[0].plot(surprisals, alpha=0.6, label=f'τ={tau}')
    axes[0].axhline(y=target, linestyle='--', alpha=0.4)

    axes[1].plot(mus, alpha=0.6, label=f'τ={tau}')

    axes[2].plot(n_cands, alpha=0.6, label=f'τ={tau}')

axes[0].set_ylabel('Surprisal (bits)')
axes[0].set_title('Sampled Token Surprisal Over Time')
axes[0].legend()
axes[0].set_xlabel('Step')

axes[1].set_ylabel('μ (threshold)')
axes[1].set_title('Dynamic Threshold μ Over Time')
axes[1].legend()
axes[1].set_xlabel('Step')

axes[2].set_ylabel('# Candidates')
axes[2].set_title('Number of Candidate Tokens Over Time')
axes[2].legend()
axes[2].set_xlabel('Step')

plt.suptitle('Mirostat v2: Dynamic Sampling Across Target Perplexities', fontsize=14)
plt.tight_layout()
plt.show()

この3段のグラフから、Mirostat のフィードバック制御がどのように機能しているかが読み取れます。

上段の surprisal グラフでは、各目標 Perplexity $\tau$ に対応する目標 surprisal(破線)の周辺で、実際のサンプリング surprisal が振動していることがわかります。$\tau = 3$(保守的)では低い surprisal に集中し、$\tau = 8$(多様)ではより高い surprisal まで許容されています。

中段のしきい値 $\mu$ のグラフでは、フィードバック制御によって $\mu$ が動的に調整される様子が見えます。surprisal が目標を超えると $\mu$ が下がり(候補を絞る)、目標を下回ると $\mu$ が上がる(候補を広げる)というフィードバックが繰り返されています。

下段の候補数グラフでは、$\mu$ の変化に連動して候補トークン数が変動していることが確認できます。$\tau$ が大きいほど平均的な候補数が多く、多様なトークンからサンプリングしていることがわかります。

Mirostat v2フィードバック制御のダイナミクス

上図は200ステップのシミュレーション結果で、青・緑・赤がそれぞれ $\tau = 3, 5, 8$ に対応しています。上段では各 $\tau$ の目標 surprisal(破線)の近くに実値が収束しており、フィードバック制御が有効に機能していることが確認できます。中段の $\mu$ が上下に調整される様子と、下段の候補トークン数が連動して変化する様子は、制御工学のフィードバックループそのものを可視化しています。

比較実験

全手法の統一比較

最後に、すべてのデコーディング戦略を統一的な条件で比較します。100ステップの生成をシミュレーションし、各手法が生成するトークンの多様性と surprisal の分布を比較します。

import numpy as np
from scipy.special import softmax
from collections import Counter

np.random.seed(42)

def simulate_generation(method, n_steps=200, vocab_size=50):
    """各手法での生成をシミュレーション"""
    generated_tokens = []
    surprisals = []

    # Mirostat用の状態
    mu = 2 * np.log2(5.0)

    for step in range(n_steps):
        # ステップごとにロジットを変化させる(文脈の変化をシミュレート)
        base_logits = np.random.randn(vocab_size) * 1.5
        # いくつかのトークンにスパイクを与える
        for spike_idx in np.random.choice(vocab_size, size=3, replace=False):
            base_logits[spike_idx] += np.random.uniform(2.0, 5.0)

        probs = softmax(base_logits)

        if method == 'greedy':
            token = np.argmax(probs)

        elif method == 'temperature':
            probs_t = softmax(base_logits / 0.8)
            token = np.random.choice(vocab_size, p=probs_t)

        elif method == 'top_p':
            sorted_idx = np.argsort(probs)[::-1]
            cumsum = np.cumsum(probs[sorted_idx])
            cutoff = np.searchsorted(cumsum, 0.9) + 1
            selected = sorted_idx[:cutoff]
            sel_probs = probs[selected] / probs[selected].sum()
            token = np.random.choice(selected, p=sel_probs)

        elif method == 'rep_penalty':
            pen_logits = apply_repetition_penalty(base_logits, generated_tokens, theta=1.3)
            pen_probs = softmax(pen_logits)
            token = np.random.choice(vocab_size, p=pen_probs)

        elif method == 'typical':
            token, _, _, _ = typical_decoding(base_logits, tau=0.9)

        elif method == 'min_p':
            token, _, _ = min_p_sampling(base_logits, p_base=0.1)

        elif method == 'mirostat':
            token, mu, _ = mirostat_v2_step(base_logits, mu, tau=5.0, eta=0.1)

        generated_tokens.append(token)
        token_surprise = -np.log2(probs[token] + 1e-12)
        surprisals.append(token_surprise)

    return generated_tokens, surprisals

# 全手法のシミュレーション
methods = ['greedy', 'temperature', 'top_p', 'rep_penalty', 'typical', 'min_p', 'mirostat']
method_labels = ['Greedy', 'Temperature\n(T=0.8)', 'Top-p\n(p=0.9)',
                 'Rep. Penalty\n(θ=1.3)', 'Typical\n(τ=0.9)', 'min-p\n(p=0.1)',
                 'Mirostat\n(τ=5)']

results = {}
for method in methods:
    tokens, surprisals = simulate_generation(method, n_steps=200)
    unique_tokens = len(set(tokens))
    avg_surprise = np.mean(surprisals)
    std_surprise = np.std(surprisals)
    results[method] = {
        'tokens': tokens, 'surprisals': surprisals,
        'unique': unique_tokens, 'avg_surprise': avg_surprise,
        'std_surprise': std_surprise
    }

# 可視化
fig, axes = plt.subplots(2, 2, figsize=(16, 12))

# 1. Surprisal分布の箱ひげ図
surprise_data = [results[m]['surprisals'] for m in methods]
bp = axes[0][0].boxplot(surprise_data, labels=method_labels, patch_artist=True)
colors_box = ['#e74c3c', '#f39c12', '#2ecc71', '#3498db', '#9b59b6', '#1abc9c', '#e67e22']
for patch, color in zip(bp['boxes'], colors_box):
    patch.set_facecolor(color)
    patch.set_alpha(0.7)
axes[0][0].set_ylabel('Surprisal (bits)')
axes[0][0].set_title('Surprisal Distribution by Method')
axes[0][0].tick_params(axis='x', labelsize=8)

# 2. ユニークトークン数
unique_counts = [results[m]['unique'] for m in methods]
axes[0][1].bar(range(len(methods)), unique_counts, color=colors_box, alpha=0.7)
axes[0][1].set_xticks(range(len(methods)))
axes[0][1].set_xticklabels(method_labels, fontsize=8)
axes[0][1].set_ylabel('Unique Tokens')
axes[0][1].set_title('Token Diversity (Unique Tokens in 200 Steps)')

# 3. 平均Surprisal vs 標準偏差(品質-多様性プロット)
for i, method in enumerate(methods):
    axes[1][0].scatter(
        results[method]['avg_surprise'],
        results[method]['std_surprise'],
        color=colors_box[i], s=150, zorder=5, edgecolors='black', linewidth=1
    )
    axes[1][0].annotate(
        method_labels[i].replace('\n', ' '),
        (results[method]['avg_surprise'], results[method]['std_surprise']),
        textcoords="offset points", xytext=(10, 5), fontsize=8
    )
axes[1][0].set_xlabel('Mean Surprisal (bits)')
axes[1][0].set_ylabel('Std Surprisal (bits)')
axes[1][0].set_title('Quality-Diversity Trade-off')

# 4. トークン頻度分布(上位20トークン)
for i, method in enumerate(methods):
    counter = Counter(results[method]['tokens'])
    top20 = counter.most_common(20)
    freqs = [c for _, c in top20]
    axes[1][1].plot(range(len(freqs)), freqs, 'o-', color=colors_box[i],
                     alpha=0.7, label=method_labels[i].replace('\n', ' '), markersize=4)
axes[1][1].set_xlabel('Token Rank')
axes[1][1].set_ylabel('Frequency')
axes[1][1].set_title('Token Frequency Distribution (Top 20)')
axes[1][1].legend(fontsize=7, loc='upper right')

plt.suptitle('Comprehensive Comparison of Decoding Strategies', fontsize=14)
plt.tight_layout()
plt.show()

この4つのグラフから、各デコーディング戦略の特性を総合的に読み取ることができます。

Surprisal分布(左上): Greedy は surprisal が最も低く集中しており(常に高確率トークンを選択)、分散も小さいです。Mirostat は目標 Perplexity の周辺に制御されているため、比較的安定した surprisal を示しています。Typical Decoding はエントロピー近傍に集中する特徴的なパターンを持ちます。

トークン多様性(右上): Repetition Penalty と Typical Decoding は多様性が高い傾向にあります。Greedy は当然ながら最も多様性が低く、同じトークンが繰り返し選ばれています。

品質-多様性トレードオフ(左下): この散布図が最も重要です。理想的な手法は、平均 surprisal が低すぎず高すぎず(x軸の中間)、かつ標準偏差も適度(y軸の中間)な位置にあるべきです。Mirostat や Typical Decoding は、このバランスのとれた領域に位置していることが多いです。

トークン頻度分布(右下): Greedy は急激なべき乗則的分布(少数のトークンが支配的)を示しますが、Repetition Penalty や Typical Decoding はより平坦な分布になっています。これは人間が書くテキストのトークン分布に近い傾向です。

全デコーディング手法の比較Surprisal分布・多様性・トレードオフ

上図の左パネルの箱ひげ図では、Greedy の外れ値が最も少なく(常に高確率トークンのみ選択)、Repetition Penalty や min-p は中央値が中程度で分散も適度なことが確認できます。中央パネルのユニークトークン数では、各手法の多様性の差が一目瞭然です。右パネルの品質-多様性散布図で、Mirostat と Typical Decoding が左下の「保守すぎず多様すぎない」領域に位置していることが、両手法の設計思想を裏付けています。

各手法の特性まとめ

import numpy as np
import matplotlib.pyplot as plt

# 各手法の特性を5段階でスコアリング(主観的評価 + シミュレーション結果)
categories = ['Diversity', 'Quality', 'Adaptivity', 'Simplicity', 'Determinism']
n_cats = len(categories)

method_scores = {
    'Greedy':          [1, 4, 1, 5, 5],
    'Temperature':     [3, 3, 1, 5, 1],
    'Top-p':           [3, 4, 2, 4, 1],
    'Rep. Penalty':    [4, 3, 2, 4, 1],
    'Typical Dec.':    [4, 4, 3, 3, 1],
    'min-p':           [4, 4, 4, 4, 1],
    'Contrastive':     [4, 5, 3, 2, 5],
    'Mirostat':        [4, 4, 5, 3, 1],
}

angles = np.linspace(0, 2 * np.pi, n_cats, endpoint=False).tolist()
angles += angles[:1]

fig, axes = plt.subplots(2, 4, figsize=(20, 10), subplot_kw=dict(polar=True))
colors_radar = ['#e74c3c', '#f39c12', '#2ecc71', '#3498db',
                '#9b59b6', '#1abc9c', '#e67e22', '#c0392b']

for idx, (name, scores) in enumerate(method_scores.items()):
    ax = axes[idx // 4][idx % 4]
    values = scores + scores[:1]
    ax.fill(angles, values, alpha=0.25, color=colors_radar[idx])
    ax.plot(angles, values, 'o-', color=colors_radar[idx], linewidth=2)
    ax.set_xticks(angles[:-1])
    ax.set_xticklabels(categories, fontsize=8)
    ax.set_ylim(0, 5)
    ax.set_title(name, fontsize=11, fontweight='bold', pad=15)
    ax.set_yticks([1, 2, 3, 4, 5])
    ax.set_yticklabels(['1', '2', '3', '4', '5'], fontsize=7)

plt.suptitle('Decoding Strategy Characteristics (Radar Chart)', fontsize=14)
plt.tight_layout()
plt.show()

このレーダーチャートは、各デコーディング戦略の長所と短所を一目で把握するのに役立ちます。Diversity(多様性)、Quality(品質)、Adaptivity(状況適応性)、Simplicity(実装の簡潔さ)、Determinism(決定論性)の5軸で評価しています。

Contrastive Search は品質と決定論性に優れますが、計算コスト(Simplicityの低さ)がトレードオフです。Mirostat は適応性が最も高く、生成中に動的にバランスを調整できる唯一の手法です。min-p は全体的にバランスが取れており、実装も比較的簡単なため、実用上の汎用性が高いことがわかります。

デコーディング戦略の特性レーダーチャート5軸評価

上図のレーダーチャートでは、8手法それぞれの「形」を比較することで特性の違いが直感的にわかります。Greedy は品質・決定論性の2軸だけが突出した「尖った」形、Mirostat は適応性が突出した形です。min-p のチャートが最も「均整のとれた五角形」に近く、実用上の汎用性の高さを示しています。タスクに応じて「どの軸を重視するか」を基準に手法を選ぶ際の羅針盤として活用してください。

実践的なガイドライン

ここまで多数の手法を紹介してきましたが、実際にどれを使えばよいのでしょうか。タスクに応じた推奨の組み合わせを以下に示します。

タスク別の推奨設定

正確性重視のタスク(コード生成、質問応答、翻訳など): – Contrastive Search($\alpha = 0.6$, $k = 5$)または低 Temperature($T = 0.2 \sim 0.5$)+ min-p($p_\text{base} = 0.1$) – 理由: 品質を最優先しつつ、退化を防ぐ必要があるため

創造性重視のタスク(ストーリー生成、ブレインストーミングなど): – Temperature($T = 0.8 \sim 1.2$)+ Top-p($p = 0.9$)+ Frequency Penalty($\alpha_f = 0.3$) – 理由: 多様性を高めつつ、頻出単語の繰り返しを防ぐため

長文生成(記事、レポートなど): – Mirostat($\tau = 5.0$, $\eta = 0.1$)+ Repetition Penalty($\theta = 1.2$) – 理由: 長い生成でも品質と多様性のバランスを動的に維持する必要があるため

対話システム: – min-p($p_\text{base} = 0.05$)+ Presence Penalty($\alpha_p = 0.5$)+ 適度な Temperature($T = 0.7$) – 理由: 自然な応答の多様性と、話題の繰り返し防止のバランスをとるため

手法の組み合わせ

実際の運用では、これらの手法を単独で使うことは少なく、複数の手法を組み合わせて使います。一般的な適用順序は次の通りです。

  1. Temperature — ロジットのスケーリング(分布の鋭さ調整)
  2. Repetition / Frequency / Presence Penalty — 生成済みトークンのロジット修正
  3. Top-k / Top-p / min-p / Typical / η-sampling — 候補トークンのフィルタリング
  4. サンプリング — フィルタ後の分布からトークンを選択

Contrastive Search と Mirostat は、それぞれ独立したデコーディングフレームワークとして機能するため、通常は他のフィルタリング手法とは組み合わせません。

まとめ

本記事では、LLM のテキスト生成における高度なデコーディング戦略を解説しました。

  • Repetition Penalty は生成済みトークンのロジットを乗除で変換し、繰り返しを直接抑制します。Frequency Penalty と Presence Penalty は加減算による類似のアプローチで、出現回数の考慮有無が異なります
  • Contrastive Search は確率と表現空間上の類似度を組み合わせたスコア関数で、品質と多様性を決定論的に同時最適化する手法です。$\alpha = 0.6$ が推奨値として知られています
  • Typical Decoding は情報理論の典型集合に基づき、エントロピーに近い情報量を持つ「典型的な」トークンを優先的に選ぶ手法です
  • η-samplingmin-p sampling は、コンテキストに応じて動的にしきい値を調整する実用的な手法です。特に min-p はシンプルかつ効果的で、実用上の採用が広がっています
  • Mirostat は目標 Perplexity に追従するフィードバック制御により、生成全体を通じて品質と多様性のバランスを動的に維持します

これらの手法は単独で使うだけでなく、組み合わせて使うことで相乗効果を得られます。重要なのは、タスクの性質に応じて適切な手法とパラメータを選ぶことです。

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

画像なし
ビームサーチの理論とPython実装
決定論的なデコーディング手法であるビームサーチのアルゴリズムと実装を解説します。本記事の高度なサンプリング系手法と対比することで、品質と多様性のトレードオフがより明確になります。
画像なし
ソフトマックス関数の定義・性質・数値安定な実装
各デコーディング手法の基盤となるソフトマックス関数の定義・数値安定実装・ボルツマン分布との関係を解説します。Temperature の仕組みを数式レベルで理解するのに必須の知識です。