第24章 microgpt の推論ループ
24.1 この章で学ぶこと
この章で学ぶ内容は以下のとおりです。
学ぶこと |
ポイント |
|---|---|
temperature の効果 |
logits を温度で割ってから softmax にかける。小さいほど鋭い分布 |
サンプリング |
|
終了条件 |
|
対応ブロック |
第18章 microgpt の構造 B7(推論ループ) |
24.2 推論(生成)
推論ループは、学習で得た重みを使って新しいテキストを生成する部分です。
学習が「正解と比べて重みを直す」往復だったのに対し、推論は重みを固定したまま、BOS から始めて次の 1 文字を予測し、それを入力に足してまた予測するという繰り返しです。
具体的には、各位置で gpt を呼んで次トークンの logits を求め、temperature で調整した確率分布から 1 文字をサンプリングして系列に追加します。
BOS が出るか最大長に達したら 1 本の生成を終え、これを 20 回繰り返して 20 個の名前を作ります。
学習データには無い、それらしい新しい名前が出てくるのが、このループの見どころです。
推論ループの全体像を次の図に示します。
図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 の扱い |
|
|
次トークン |
ラベルと比較して損失 |
|
まず推論(生成)ブロック全体(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 文字ずつの伸長です。
図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: break:BOSが出たら文の終わりとみなし、この系列を打ち切ります。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 つです。
BOSが出力された:文頭と文末の特別トークンが選ばれたら、文が完結したとみなして系列を打ち切ります(L248〜L249)。学習時に文末へBOSを付けたことが、ここで終わりを知らせる役割を果たします。block_sizeに達した:BOSが出ないまま最大長に到達したら、内側ループが終了して打ち切ります。
この往復をデータの流れとして示すと、次のようになります。
図24-3: 生成フェーズのデータの往復
学習時よりシンプルな往復です。
図24-2 のブロック図が制御の流れを、この図がデータの往復を表します。
重みは固定のまま、gpt でサンプルを得て系列に追加する処理を、BOS か最大長まで繰り返すだけです。