一文要約: 訓練ではシーケンス全体を見てパラメータをパラレルに更新しますが、推論ではプロンプトしか見えず、トークンを1つずつ生成しなければなりません。このAutoregressiveな制約こそが、GPTが一貫したテキストを生み出す理由であり、推論がレイテンシのボトルネックになる理由でもあります。
16.1 訓練と推論: 本質的な違い
16.1.1 並べて比較する
| 訓練 | 推論 | |
|---|---|---|
| 目的 | パラメータを学習する | テキストを生成する |
| 入力 | テキストのシーケンス全体 | 最初のプロンプト |
| 正解(ターゲット) | 既知 (各位置の次のトークン) | 未知 (予測する必要がある) |
| 処理方法 | パラレル (一回のフォワードパスで全位置) | シリアル (1パスにつき1トークン) |
| パラメータ更新 | あり | なし |
16.1.2 なぜこの違いが生じるのか
訓練時:
- 完全なテキストが手元にあります。例: "The agent opened a pull request for review"
- 各位置で次に来る正しいトークンが分かっています
- 全位置のロスを同時に計算できます
推論時:
- プロンプトしかありません: "The agent opened a pull request"
- 次に何が来るかは分かりません
- 1トークンを予測し、それを観測してから次を予測する必要があります
この非対称性 — 答えを知っているか否か — が、すべての説明になります。
16.2 訓練の詳細
16.2.1 Teacher Forcing
訓練では Teacher Forcing という手法を使います。
Input: The agent opened a pull request
Target: agent opened a pull request for
入力は元のシーケンスです。ターゲットは元のシーケンスを 1位置だけ右にシフトしたもの です。
すべての位置が同時に、自身の後続トークンを予測しています:
- 位置0 ("The") → "agent" を予測
- 位置1 ("agent") → "opened" を予測
- 位置2 ("opened") → "a" を予測
- ...
- 位置5 ("request") → "for" を予測
16.2.2 パラレル計算
入力もターゲットもすべて既知なので、シーケンス全体を一回のフォワードパスで計算できます:
# 訓練ステップの概略
def train_step(model, input_ids, target_ids):
# シーケンス全体に対して一回のフォワードパス
logits = model(input_ids) # [batch, seq_len, vocab_size]
# 全位置のロスを同時に計算
loss = F.cross_entropy(
logits.view(-1, vocab_size),
target_ids.view(-1)
)
# 逆伝播
loss.backward()
optimizer.step()
一回のフォワードパスで、全位置を処理します。これが訓練を計算的に効率的にしている要因です。
16.2.3 Causal Maskの役割
訓練ではシーケンス全体を一度に見ますが、各位置はそれより前にある位置にしかAttentionを向けることが許されません。未来は不可視のままでなければなりません。
これが第15章で扱った Causal Mask です:
位置0が見るのは: [The, -, -, -, -, -]
位置1が見るのは: [The, agent, -, -, -, -]
位置2が見るのは: [The, agent, opened, -, -, -]
...
このマスクは、推論時に存在する情報制約と同じものを訓練時にも強制します。これがなければ訓練はカンニングになってしまいます。位置3が自分のターゲットを見てしまい、ロスが自明に低くなってしまうからです。
16.3 推論の詳細
16.3.1 Autoregressive生成
推論時、モデルは1回のフォワードパスにつき1トークンを生成します:
プロンプト: "The agent opened a pull request"
|
モデルのフォワードパス
|
出力分布: [for=18%, to=12%, ...]
|
"for" をサンプリング
|
新しい入力: "The agent opened a pull request for"
|
モデルのフォワードパス
|
出力分布: [review=34%, approval=15%, ...]
|
"review" をサンプリング
これが Autoregressive 生成です。各ステップの出力が次のステップの入力になります。
これは線路を一区画ずつ敷きながら走る列車にたとえられます。次の一区画が敷かれるまで列車は前進できず、その一区画はそれまでに敷かれたすべての区画の上にしか置けません。
16.3.2 ステップごとの例
プロンプト "The agent opened a pull request" から生成する例:
ステップ1:
Input: "The agent opened a pull request"
Predict: "for"
ステップ2:
Input: "The agent opened a pull request for"
Predict: "review"
ステップ3:
Input: "The agent opened a pull request for review"
Predict: "."
ステップ4:
Input: "The agent opened a pull request for review."
Predict: <end-of-sequence> または次の文の始まり
これは、モデルがend-of-sequenceトークンを生成するか、max_new_tokens に達するまで続きます。
16.3.3 コンテキスト長と切り詰め
すべてのモデルには最大コンテキスト長があります。例えば context_length = 2048 などです。実行中のシーケンスがこの上限を超えると、モデルは切り詰めを行います:
直近の context_length 個のトークンだけを保持
それ以前のトークンは破棄
これが、すべてのLLM APIで遭遇する「コンテキストウィンドウ」の制限です。モデルは何かを心理的に「忘れた」わけではなく、単にそれらが今回のフォワードパスの入力に存在しなかった、というだけのことです。
16.4 推論コード
16.4.1 基本的な推論ループ
# 推論の概略
def generate(model, prompt_ids, max_new_tokens=50):
"""
Autoregressiveなテキスト生成。
Args:
model: GPTモデル
prompt_ids: 初期プロンプトのトークンID [1, seq_len]
max_new_tokens: 生成する新しいトークンの最大数
"""
model.eval() # インファレンスモードに切り替え (Dropoutを無効化)
generated = prompt_ids.clone()
for _ in range(max_new_tokens):
# 最大コンテキスト長を超えたら切り詰める
input_ids = generated[:, -max_len:]
# フォワードパス
with torch.no_grad(): # 勾配計算を行わない
logits = model(input_ids)
# 最後の位置のlogitsだけを取る
next_token_logits = logits[:, -1, :] # [1, vocab_size]
# Greedy: 確率最大のトークンを選ぶ
next_token = torch.argmax(next_token_logits, dim=-1, keepdim=True)
# シーケンスに追加
generated = torch.cat([generated, next_token], dim=1)
# end-of-sequenceで停止
if next_token.item() == eos_token_id:
break
return generated
16.4.2 重要なポイント
model.eval(): モデルを評価モードにして、Dropoutを無効化し出力を安定させますtorch.no_grad(): 勾配の追跡をスキップしてメモリを節約します- 最後の位置だけを取る:
logits[:, -1, :]— 次のトークンを予測するのは最終位置だけです - ループ: 新しいトークンが追加されるたびにシーケンスが1位置だけ伸びます
16.5 PaddingとバッチInference
16.5.1 Paddingとは何か
実運用システムでは複数のリクエストをパラレルに処理します。プロンプトごとに長さが異なるので、シーケンスを同じ長さに Padding します:
リクエスト1: "The agent opened a pull request" (6トークン)
リクエスト2: "Summarize" (1トークン)
長さ6にPadding後:
リクエスト1: "The agent opened a pull request"
リクエスト2: "<pad> <pad> <pad> <pad> <pad> Summarize"
16.5.2 Paddingの扱い
Attention計算ではPad位置を無視する必要があります:
# attention_mask: 1 = 実トークン, 0 = padding
attention_mask = (input_ids != pad_token_id).long()
# pad位置のAttention重みはゼロになる
モデルはマスクを見て、Paddingトークンに注意を向けないように学習します。Pad位置の出力は破棄されます。
16.6 訓練と推論: 計算量の比較
16.6.1 データの流れ
訓練時:
シーケンス全体 [seq_len]
| 一回のフォワードパス
全位置の予測 [seq_len, vocab]
| ターゲットと比較
ロス
| 逆伝播
パラメータ更新
推論時:
プロンプト [n]
| フォワードパス
位置 n+1 を予測
| トークンをサンプリング
新しいシーケンス [n+1]
| フォワードパス
位置 n+2 を予測
| トークンをサンプリング
... (完了までループ)
16.6.2 効率の比較
| 訓練 | 推論 | |
|---|---|---|
| シーケンスあたりのフォワードパス数 | 1 | N (N = 生成長) |
| 並列性 | 高 (全位置を一度に) | 低 (定義上シリアル) |
| ボトルネック | メモリ (勾配の保存) | レイテンシ (フォワードパスの繰り返し) |
これが、推論にKV Cacheのような最適化技術が必要となる理由です — 第22章で詳しく扱います。
16.6.3 Dropoutの挙動
| 訓練 | 推論 | |
|---|---|---|
| Dropout | 有効 (ランダムに落とす) | 無効 |
| 理由 | 過学習を防ぐ | 安定した決定論的出力を保証 |
model.train() # Dropout 有効
model.eval() # Dropout 無効 — サンプリング前に忘れずに
訓練モードのままにされていたせいで、呼び出すたびにモデルの出力が変わってしまうというバグを何度も見たことがあります。model.eval() はたった1行で、コストはゼロです。
16.7 デコーディング戦略
16.7.1 次のトークンをどう選ぶか
語彙上の確率分布が与えられたとき、どのトークンを次に選ぶべきでしょうか?
Greedy Decoding:
next_token = torch.argmax(probs, dim=-1) # 常に最大確率を選ぶ
- 決定論的で高速
- 反復的で無難なテキストに偏りがち
Sampling:
next_token = torch.multinomial(probs, num_samples=1) # 分布からサンプリング
- より創造的で多様
- 時に支離滅裂なシーケンスを生成することがある
Top-K Sampling:
# 上位K個のトークンからのみサンプリング
top_k_probs, top_k_indices = torch.topk(probs, k=50)
next_token = top_k_indices[torch.multinomial(top_k_probs, 1)]
Top-P (Nucleus) Sampling:
# 累積確率が P 以上になる最小のトークン集合からサンプリング
sorted_probs, sorted_indices = torch.sort(probs, descending=True)
cumsum = torch.cumsum(sorted_probs, dim=-1)
mask = cumsum <= 0.9 # P = 0.9
# このマスク内でサンプリング
16.7.2 Temperature
Temperatureは分布の鋭さを制御します:
probs = F.softmax(logits / temperature, dim=-1)
- T < 1: 集中した分布、より決定論的
- T = 1: 標準分布
- T > 1: 平坦な分布、よりランダム
ほとんどの実運用LLM APIはtemperatureをパラメータとして公開しています。デフォルトは通常1.0かそれに近い値です。
16.8 なぜAutoregressiveなのか
16.8.1 言語には順序がある
"The agent approved the pull request" と "The pull request approved the agent" は同じ単語を使っていますが、まったく異なる意味になります。言語は順序に依存します。
Autoregressive生成はそれを保ちます:
- 各トークンはそれ以前のすべてのトークンに条件付けられている
- 生成されたテキストは直前までの内容と整合性がとれている
- コンテキストが伸びるにつれて、モデルは生成の途中で適応できる
16.8.2 非Autoregressiveなモデル
すべてのトークンをパラレルに生成しようとするモデルもあります。これらは一般に、Autoregressiveなモデルよりも品質の低い出力を出します。特にオープンエンドな生成タスクで顕著です。
理由はトークン間の強い依存関係です。"the" を生成すると、その文脈で "agent" が何を意味するかについて具体的なヒントが得られます。パラレル生成ではこの依存関係を見落としてしまいます。
GPT-4、Claude、LLaMA、Gemini — すべてAutoregressiveです。レイテンシのコストはあれど、このパターンは十分に堅牢であることが証明されており、この分野はそれを手放していません。
16.9 章のまとめ
16.9.1 中核となる比較
| 観点 | 訓練 | 推論 |
|---|---|---|
| ターゲットは既知? | はい | いいえ |
| 処理方法 | パラレル | シリアル |
| フォワードパス数 | 1シーケンスあたり1回 | 1シーケンスあたりN回 |
| Dropout | オン | オフ |
| パラメータ更新 | あり | なし |
16.9.2 Autoregressive生成
プロンプト -> トークン1を予測 -> 追加 -> トークン2を予測 -> 追加 -> ...
各ステップは以前のすべてのトークンに依存します。この依存関係こそが、出力に一貫性をもたらすと同時に、推論を遅くする要因です。
16.9.3 中核的な洞察
訓練は答えが既知なのでパラレル化できますが、推論は新しい各トークンが以前のすべてのトークンに依存するためシリアルでなければなりません。このシリアルな制約こそが推論レイテンシの主要なボトルネックであり、KV Cacheが重要になるまさにその理由です。
章のチェックリスト
この章を終えたら、以下ができるようになっているはずです:
- 訓練と推論の本質的な違いを一文で説明できる
- Autoregressive生成をステップごとに記述できる
- なぜシーケンスあたりの推論が訓練より遅いのかを説明できる
- 少なくとも3つのデコーディング戦略を挙げ、それぞれをいつ使うかを説明できる
次の章でお会いしましょう
これが訓練と推論の違いです。図を見ずに「なぜ訓練はパラレルで推論はシリアルなのか」を説明できるなら、第17章へ進む準備は整っています。
Autoregressive生成では、出力1つあたりフォワードパスをN回走らせることになります — 効率は重要です。しかしKV Cacheのような最適化技術に進む前に、もう一つ固めておきたい訓練の概念があります。それが 学習率(Learning Rate) です。第17章では、それが何をするのか、なぜ実務上もっとも重要なハイパーパラメータなのか、そして当てずっぽうではなくどう設定すればよいのかを解説します。
ここまでお付き合いいただきありがとうございました。次章でまたお会いしましょう。