第24章 microgpt の推論ループ

24.1 この章で学ぶこと

この章で学ぶ内容は以下のとおりです。

表24-1: この章で学ぶこと

学ぶこと

ポイント

temperature の効果

logits を温度で割ってから softmax にかける。小さいほど鋭い分布

サンプリング

random.choices で確率分布に比例してトークンを 1 つ選ぶ

終了条件

BOS トークンが出たら系列終了(文頭に戻ってきた、つまり文が終わった)

対応ブロック

第18章 microgpt の構造 B7(推論ループ)

24.2 推論(生成)

推論ループは、学習で得た重みを使って新しいテキストを生成する部分です。 学習が「正解と比べて重みを直す」往復だったのに対し、推論は重みを固定したまま、BOS から始めて次の 1 文字を予測し、それを入力に足してまた予測するという繰り返しです。

具体的には、各位置で gpt を呼んで次トークンの logits を求め、temperature で調整した確率分布から 1 文字をサンプリングして系列に追加します。 BOS が出るか最大長に達したら 1 本の生成を終え、これを 20 回繰り返して 20 個の名前を作ります。 学習データには無い、それらしい新しい名前が出てくるのが、このループの見どころです。

推論ループの全体像を次の図に示します。

../_images/inference_overview.jpg

図24-1: 推論(生成)の全体像

token_id(最初は BOS)から gpt で logits を得て、temperature で割り、softmax で確率分布にし、random.choices で 1 文字をサンプルする、という流れを繰り返します。 サンプルしたトークンを次の入力に戻し、BOS が出る(または block_size に達する)まで系列を伸ばします。 例えば BOS a n n a BOS"anna" という 1 本が完成します。

学習ループとの主な差分を次の表にまとめます。

観点

学習ループ(B6)

推論ループ(B7)

重みの更新

Adam で更新あり

更新なし(固定)

logits の扱い

softmax から損失計算

temperature 付き softmax

次トークン

ラベルと比較して損失

random.choices でサンプル

まず推論(生成)ブロック全体(microgpt.py の L238〜L251)を引用します。 続けてブロック図と各ブロックの概要を置き、そのあと行ごとの意味を列挙します。

238temperature = 0.5  # (0,1]の範囲。低いほど確定的、高いほど多様な出力
239print("\n--- inference (new, hallucinated names) ---")
240for sample_idx in range(20):
241    keys, values = [[] for _ in range(n_layer)], [[] for _ in range(n_layer)]
242    token_id = BOS  # BOSから開始
243    sample = []
244    for pos_id in range(block_size):
245        logits = gpt(token_id, pos_id, keys, values)
246        probs = softmax([l / temperature for l in logits])  # temperatureで分布を調整
247        token_id = random.choices(range(vocab_size), weights=[p.data for p in probs])[0]  # 確率的サンプリング
248        if token_id == BOS:
249            break  # BOSが来たら終端
250        sample.append(uchars[token_id])
251    print(f"sample {sample_idx+1:2d}: {''.join(sample)}")

24.3 推論ループ内のブロック図と概要

次の図は、上記ソースの制御の流れを (1)〜(7) の項番号に対応させたものです。 外側は 20 本の独立したサンプル、内側は pos_id による 1 文字ずつの伸長です。

temperature(logits を割る係数)推論開始の見出しを printfor sample_idx in range(20)keys/values を層ごとに空で初期化token_id = BOS、sample = []for pos_id: gpt → logits/temperature → softmax → random.choices1 サンプル分の文字列を print 次の pos(BOS まで)1 系列が終わったら次の sample_idx

図24-2: 推論ループのブロック

行ごとの意味

  • L238 temperature = 0.5:logits を softmax に渡す前に割る係数です。(0, 1] の範囲を想定し、小さいほど分布が鋭くなって確定的(無難)に、大きいほど分布が平坦になって多様(冒険的)になります。

  • L239 print(...):推論開始の見出し(--- inference (new, hallucinated names) ---)を表示します。「hallucinated(幻覚)」とあるとおり、学習データには無い新しい名前をモデルが作り出します。

  • L240 for sample_idx in range(20):独立した 20 本のサンプルを生成します。1 本ごとに以降の初期化からやり直すので、サンプル同士は互いに影響しません。

  • L241 keys, values = ...:層ごとの K/V キャッシュを空リストで初期化します。サンプルごとにキャッシュを空にするのは、前のサンプルの文脈を引きずらず、毎回まっさらな状態から系列を生成するためです。

  • L242 token_id = BOS:最初の入力トークンは文頭記号 BOS です。文の始まりをモデルに伝え、ここから次の 1 文字を予測させます。

  • L243 sample = []:生成した文字を溜めるリストです(BOS 以外の文字だけが入ります)。

  • L244 for pos_id in range(block_size):位置を 0 から進めながら、最大 block_size 文字まで 1 文字ずつ生成します(内側の伸長ループ)。

  • L245 logits = gpt(...):現在のトークンと位置で順伝播し、語彙サイズぶんの logits(次トークンの生スコア)を得ます。学習時と同じ gpt 関数を、重みを更新せずに呼ぶだけです。

  • L246 probs = softmax([l / temperature for l in logits]):各 logit を temperature で割ってから softmax し、確率分布に変換します。

  • L247 token_id = random.choices(...):その確率分布に比例した頻度でトークンを 1 つ選びます(最大値を選ぶのではなく確率的にサンプルする)。

  • L248〜L249 if token_id == BOS: breakBOS が出たら文の終わりとみなし、この系列を打ち切ります。

  • L250 sample.append(uchars[token_id])BOS でなければ、トークン ID を uchars で文字に戻して sample に追加します。

  • L251 print(...):生成した文字列(sample を連結したもの)を表示します。

BOS から始めて、次の文字を 1 文字ずつ選んでいきます。 温度による分布の調整は、次の 24.4 で詳しく見ます。

24.4 temperature の役割

temperature は、生成の確からしさを調整する値です。 softmax に渡す前に各 logit を temperature で割るだけですが、これが分布の形を変えます。

  • temperature が小さい(例: 0.5):logit の差が拡大され、softmax 後の分布が鋭くなります。最も確率の高いトークンが選ばれやすくなり、出力は無難で安定しますが、同じような結果に偏りがちです。

  • temperature = 1.0:logit をそのまま使う標準の softmax です。

  • temperature が大きい(例: 1.5):logit の差が圧縮され、分布が平坦になります。低確率のトークンも選ばれやすくなり、多様だが崩れやすい出力になります。

本コードの既定値は 0.5 で、人名らしさを保ちつつ多少のばらつきを出すバランスです。

注釈

temperature を 0 に近づけると、実質的に「常に最大確率のトークンを選ぶ」貪欲法(greedy)に近づきます。 逆に大きくしすぎると、ほぼ一様分布になり、文字の並びがランダムに崩れます。

24.5 サンプリングと終了条件

学習ループと推論ループの最大の違いは、logits の使い方にあります。 学習では正解ラベルと比べて損失を計算しましたが、推論では確率分布からサンプリングして次トークンを決めます。

サンプリングでは、最大確率のトークンを選ぶのではなく、確率に比例した頻度でトークンを選びます。 random.choices(range(vocab_size), weights=...) がこれを行います。 常に最大確率のトークンを選ぶ貪欲法では、同じ入力から毎回同じ出力しか得られません。 確率的にサンプルすることで、同じ BOS から始めても 20 本それぞれ異なる名前が生成されます。

生成が止まる条件は次の 2 つです。

  1. BOS が出力された:文頭と文末の特別トークンが選ばれたら、文が完結したとみなして系列を打ち切ります(L248〜L249)。学習時に文末へ BOS を付けたことが、ここで終わりを知らせる役割を果たします。

  2. block_size に達したBOS が出ないまま最大長に到達したら、内側ループが終了して打ち切ります。

この往復をデータの流れとして示すと、次のようになります。

生成ループgptsoftmax+sample 現在の系列で順伝播次トークンの logitstemperature を反映した確率トークンを一つ選び系列へ終了条件まで繰り返し

図24-3: 生成フェーズのデータの往復

学習時よりシンプルな往復です。 図24-2 のブロック図が制御の流れを、この図がデータの往復を表します。 重みは固定のまま、gpt でサンプルを得て系列に追加する処理を、BOS か最大長まで繰り返すだけです。