Level 1概要役割と処理の流れ
何をする機能か
確率分布 [B, 1, V] から、実際に出力する1 個のトークンを選びます。
そして、選んだトークンを入力系列の末尾に足し、Decoder をもう一度実行します。
この繰り返しが「文章生成」の実体です。 図で「選択後、再び入力」と書かれているループがこれにあたります。
選び方には次のような方法があります。
- 貪欲法(greedy): 最も確率の高いトークンを選ぶ。毎回同じ結果になる
- サンプリング: 確率に従ってランダムに選ぶ。多様な出力になる
- top-k: 上位 k 個に絞ってからサンプリング
- top-p(nucleus): 累積確率が p に達するまでの候補に絞ってサンプリング
- ビームサーチ: 複数の候補系列を並行して保持し、系列全体のスコアが高いものを選ぶ
なぜ必要か
モデルが直接出せるのは「次の 1 トークンの確率分布」だけです。 文章を作るには、そこから 1 個を確定させ、その結果を踏まえて次を予測する、 という手順を繰り返す必要があります。
この「1 つ前の出力が次の入力になる」性質を 自己回帰(autoregressive) と呼びます。
全体の中でいつ呼ばれるか
推論(生成)のときだけ、1 トークンごとに実行されます。 学習時には行いません。学習では全位置の正解が最初からわかっているため、 選択もループも不要です(右シフト を参照)。
Level 2内部処理Tensor形状・内部ステップ・設計上の意味
前段から受け取るもの
Softmax が出力した probabilities。
推論時は最後の位置だけが必要なので [B, 1, V] です。
後段へ渡すもの
next_token_id [B, 1]。これは 2 か所へ渡されます。
- 出力バッファ(最終的な生成結果の一部になる)
- Decoder の入力の末尾(次のステップの入力になる)
2 のループがあるため、この工程だけは「後段」が前段へ戻ります。
例文での処理
共通例文
The keys on the table are lost.
概念上のトークン列(S = 8): [The, keys, on, the, table, are, lost, .]
The keys on the table まで生成済みで、次を選ぶ場面です。
- The
- keys
- on
- the
- table
| 候補 | 確率 | greedy | temperature=0.8 のサンプリング |
|---|---|---|---|
are | 0.94 | 選ばれる | ほぼ選ばれる |
is | 0.042 | — | まれに選ばれる |
were | 0.019 | — | まれに選ばれる |
are が選ばれると、系列は The keys on the table are になり、T は 6 になります。
そしてもう一度 Decoder 全体を実行して、次は lost を予測します。
- The
- keys
- on
- the
- table
- are
この繰り返しは、終了トークン(</s> など)が選ばれるか、
最大長に達するまで続きます。
学習時と推論時の違い
このページの内容は推論時のみです。両者の違いをもう一度整理します。
| 学習時 | 推論時 | |
|---|---|---|
| Decoder の実行回数 | 1 回(全位置を並列に計算) | 生成トークン数ぶん繰り返す |
| 次の入力 | 正解トークン(Teacher Forcing) | 自分が選んだトークン |
| 選択処理 | なし(全語彙の確率で損失を計算) | あり(greedy / サンプリング等) |
| 誤りの影響 | 次の位置には影響しない | 次以降の入力が汚染される |
| 並列性 | 高い(GPU を使い切れる) | 低い(逐次実行がボトルネック) |
推論が遅いのは、この逐次性が原因です。 1 トークンごとに Decoder を通す必要があり、系列長ぶんの往復が発生します。
推論を速くする仕組み
- KV Cache: 過去の位置の Key・Value を再計算せず使い回す。 詳しくは マスク付き自己注意 を参照
- 最後の位置だけを Linear に通す:
[B, T, V]ではなく[B, 1, V]にする - 投機的デコーディング: 小さいモデルで複数トークンを先読みし、 大きいモデルでまとめて検証する
EncoderとDecoderでの違い
この工程は Decoder 側(出力ヘッド)だけにあります。 Encoder-only モデル(BERT など)は系列を生成しないため、このループを持ちません。
Encoder–Decoder 構成では、生成ループの中で Encoder は再実行しません。入力文は変わらないので、 Encoder出力 は 1 回計算すれば使い回せます。
入力と出力
入力
| 項目名 | データの意味 | データ型 | Tensor形状 | 記号の意味 | 具体例 | 次の渡し先 |
|---|---|---|---|---|---|---|
probabilities | 最後の位置における次トークンの確率分布 | float32 Tensor | [B, 1, V] | B = バッチサイズ(同時に処理する文の数) / V = 語彙数(Tokenizer が扱うトークンの総数) | [1, 1, 32000] | 選択(greedy / top-k / top-p) |
generated_ids | これまでに生成したトークン列 | int64 Tensor | [B, T] | B = バッチサイズ(同時に処理する文の数) / T = 出力系列長(Decoder側のトークン数) | [1, 5] | 末尾に next_token_id を連結 |
出力
| 項目名 | データの意味 | データ型 | Tensor形状 | 記号の意味 | 具体例 | 次の渡し先 |
|---|---|---|---|---|---|---|
next_token_id | 実際に出力する 1 トークンのID | int64 Tensor | [B, 1] | B = バッチサイズ(同時に処理する文の数) | [[10]](are のID) | 出力バッファ、および Decoder 入力の末尾 |
generated_ids | 1 トークン伸びた系列 | int64 Tensor | [B, T+1] | B = バッチサイズ(同時に処理する文の数) | [1, 6] | 次ステップの Decoder 入力 |
B- バッチサイズ(同時に処理する文の数)
V- 語彙数(Tokenizer が扱うトークンの総数)
T- 出力系列長(Decoder側のトークン数)
このサイトでは Tensor 形状を PyTorch の `batch_first=True` 相当、つまり `[B, S, D]`(バッチ次元が先頭)で統一しています。実装によっては `[S, B, D]` を採用するものもあります。
よくある誤解
Level 3数式・Python数式・疑似実装・計算量・実装差
数式
系列 を生成する確率は、条件付き確率の積に分解できます。
各ステップで計算しているのは、この右辺の 1 項 です。
貪欲法は各ステップで局所的に最大のものを選びます。
これは系列全体の確率を最大化する保証がありません。 ビームサーチは幅 の候補を保持して、系列全体のスコアを近似的に最大化します。
top-p(nucleus)サンプリングは、確率の高い順に並べて累積が を超えるまでの集合 に絞り、その中で再正規化してサンプリングします。
Pythonで表したインターフェース
from typing import Callable, Optional
import torch
from torch import Tensor
def select_next_token(
probabilities: Tensor, # [B, V]
top_k: Optional[int] = None,
top_p: Optional[float] = None,
greedy: bool = False,
) -> Tensor:
"""確率分布から次の 1 トークンを選ぶ。
Args:
probabilities: [B, V]。合計が 1 の確率分布。
top_k: 上位 k 個に絞る。None なら絞らない。
top_p: 累積確率が p に達するまでに絞る。None なら絞らない。
greedy: True なら最大確率のトークンを決定的に選ぶ。
Returns:
[B, 1] int64。選ばれたトークンID。
"""
if greedy:
return probabilities.argmax(dim=-1, keepdim=True)
probs = probabilities.clone()
if top_k is not None:
threshold = probs.topk(top_k, dim=-1).values[:, -1:]
probs = torch.where(probs < threshold, torch.zeros_like(probs), probs)
if top_p is not None:
sorted_probs, sorted_idx = probs.sort(dim=-1, descending=True)
cumulative = sorted_probs.cumsum(dim=-1)
# 累積が p を超えた「後ろ」を落とす(先頭1つは必ず残す)
remove = cumulative - sorted_probs > top_p
sorted_probs[remove] = 0.0
probs = torch.zeros_like(probs).scatter(-1, sorted_idx, sorted_probs)
probs = probs / probs.sum(dim=-1, keepdim=True)
return torch.multinomial(probs, num_samples=1) # [B, 1]
def generate(
model: Callable[[Tensor], Tensor], # [B, T] -> logits [B, T, V]
prompt_ids: Tensor, # [B, T0]
max_new_tokens: int,
eos_id: int,
temperature: float = 1.0,
top_p: Optional[float] = 0.9,
) -> Tensor:
"""自己回帰的に系列を生成する。
Returns:
[B, T0 + n] int64。プロンプトを含む生成結果。
"""
generated = prompt_ids
for _ in range(max_new_tokens):
logits = model(generated) # [B, T, V]
last_logits = logits[:, -1, :] / temperature # [B, V]
probabilities = last_logits.softmax(dim=-1) # [B, V]
next_token_id = select_next_token(probabilities, top_p=top_p) # [B, 1]
generated = torch.cat([generated, next_token_id], dim=1) # [B, T+1]
if bool((next_token_id == eos_id).all()):
break
return generated
# 最小使用例(model はダミー。are 相当のIDを高確率で返す)
V = 6
ARE_ID, EOS_ID = 0, 5
def dummy_model(ids: Tensor) -> Tensor:
batch, length = ids.shape
logits = torch.full((batch, length, V), -5.0)
logits[:, :, ARE_ID] = 8.0
return logits
prompt = torch.tensor([[1, 2, 3, 4]]) # [B=1, T0=4]
out = generate(dummy_model, prompt, max_new_tokens=3, eos_id=EOS_ID, top_p=0.9)
print(prompt.shape, out.shape) # torch.Size([1, 4]) torch.Size([1, 7])
print(out[0, 4:].tolist()) # [0, 0, 0] (are が繰り返し選ばれる)この例で単純化している点
- KV Cache を使わず、毎ステップ系列全体を通す素朴な実装にしています(動きを追いやすくするため)。
- ビームサーチ、繰り返しペナルティ、終了条件の細かい制御は省略しています。
- model は [B, T] を受け取り logits [B, T, V] を返すものとしています。
型と Tensor 形状の見え方を掴むための擬似実装です。そのまま学習・推論に使えるものではありません。
計算量
1 ステップあたりのコストは、KV Cache の有無で大きく変わります。
| 1 ステップの計算量 | n トークン生成の合計 | |
|---|---|---|
| Cache なし | 程度に膨らむ | |
| Cache あり |
実運用では KV Cache が前提です。 その場合、生成速度はメモリ帯域(重みと Cache の読み出し)で決まることが多く、 「計算量」よりも「1 トークンあたり何バイト読むか」が支配的になります。
生成パラメータの整理
| パラメータ | 効果 | 典型値 |
|---|---|---|
temperature | 分布の鋭さ。小さいほど決定的 | 0.0〜1.0 |
top_k | 上位 k 個に限定 | 40〜100 |
top_p | 累積確率 p までに限定 | 0.9〜0.95 |
repetition_penalty | 既出トークンの logit を下げ、繰り返しを抑える | 1.0〜1.2 |
max_new_tokens | 生成の上限 | 用途による |
temperature=0 と greedy は実質的に同じ挙動になります
(実装では 0 除算を避けるため、内部で greedy に切り替えます)。
Decoder-only LLM での違い
- プロンプトの処理(prefill)と生成(decode)の 2 段階に分かれます。 prefill は全位置を並列に計算できるため高速で、decode が逐次的で遅い部分です。
- Encoder がないため、Cross Attention はありません。 プロンプトは同じ系列の前半として Masked Self-Attention で参照されます。
- 生成ループの構造そのものは、原典の Decoder とまったく同じです。