最新の文脈 を入れ、1トークン ずつ追加します。temperature 0は最大点、それ以外は確率 抽選。top_k 0は全候補です。
seedは再現 用。改行 か上限で止まり、新しい文章だけ返します。学習 時のQ:/A:形式を使います。
生成ループ — 1トークンずつつけていく(自己回帰)
generate は、この順序を決めたトークン数だけ繰り返します。
これまでのトークンから最後の context(12)個だけを切り出し、forward に入れます。
返ってきた点数のリスト のうち、最後の位置の点数 [-1] だけを使います。それが次のトークンの点数です。
決めた方法(貪欲・温度・top_k)でトークンを1つ選びます。
選んだトークンを ids の後ろにつけます。次の周はこのトークンまで含めた文章を見ます。
選んだトークンが改行なら止まります。
自分の出した結果をもう一度入力に使うので、自己回帰 (autoregressive)生成といいます。一度選んだトークンは戻さないので、前で間違えると後ろがその間違いに引きずられます。
生成は重み を読むだけです。ところが XSLM の forward は学習時と同じ Value の計算をするので、使わない計算グラフ まで作ります。PyTorch は torch.no_grad() の中で生成してグラフを作らず、model.eval() で dropout をオフにします。
貪欲選択 — temperature 0
temperature を0にすると、いつも点数が最も高いトークンを選びます(greedy 、argmax )。同じ入力なら、いつも同じ答えになります。
長所は予測できることです。モデル が何を最もそれらしいと思っているかをそのまま見せるので、学習できたかを確かめるのに便利です。短所は、同じ句を繰り返しやすいことです。"is warn is warn is warn" のように一度ループに入ると、毎回同じトークンが最も高くなり、抜け出せません。だから会話ではランダムさを少し混ぜます。
温度 — 確率分布をとがらせる、または平らにする
temperature(T)は、点数を softmax する前に T で割ります。確率は exp(点数 ÷ T) に比例します。
T < 1: 点数の差が広がり、分布がとがります。高いトークンがより頻繁に引かれます。
T = 1: モデルが出した確率そのままです。
T > 1: 点数の差が縮まり、分布が平らになります。低いトークンも頻繁に引かれ、でたらめになります。
下の実験は、点数 warm 2.0、hot 1.0、blue 0.2、run −1.0 に対して温度を変えます。warm の確率は T 0.5 で 0.858、1.0 で 0.632、2.0 で 0.447 に変わり、run は 0.002 から 0.1 に上がります。
generate.py も同じ計算をします。候補の点数から最も大きい値を引き(数値の安定)、T で割って exp した値を重みとして random.choices に渡します。重みは合計が1でなくてもよく、choices が比率どおりに引きます。ふつうは 0.7〜0.9 がよく使われ、0.1〜0.5 は保守的で、1.0 以上はでたらめになりやすいです。
AIに聞いてみる
自分で確かめる lab/temperature.py
import math
logits = {'warm': 2.0, 'hot': 1.0, 'blue': 0.2, 'run': -1.0}
for t in [0.5, 1.0, 2.0]:
weights = {w: math.exp(s / t) for w, s in logits.items()}
total = sum(weights.values())
print('T =', t, {w: round(v / total, 3) for w, v in weights.items()})
print('top_k=2 keeps', sorted(logits, key=logits.get, reverse=True)[:2])
cd ~/xslm
mkdir -p lab
micro lab/temperature.py
python3 -m lab.temperature
実行結果
T = 0.5 {'warm': 0.858, 'hot': 0.116, 'blue': 0.023, 'run': 0.002}
T = 1.0 {'warm': 0.632, 'hot': 0.232, 'blue': 0.104, 'run': 0.031}
T = 2.0 {'warm': 0.447, 'hot': 0.271, 'blue': 0.182, 'run': 0.1}
top_k=2 keeps ['warm', 'hot']
top-k と top-p — 候補を減らす
温度を使っても、とても低いトークンたちを合わせるとかなり大きな確率になることがあります。語彙が5万個なら、1つ1つは小さくても、すべて足すと無視できません。そこで引く前に候補を減らします。
top-k: 点数の上位 k 個だけを残し、残りは捨てます。XSLM の top_k 8 は、語彙40個のうち8個だけを候補にします。0なら減らしません。実験の top_k=2 は warm・hot だけを残します。
top-p(nucleus): 確率が高い順に足していき、合計が p(例: 0.9)になるまでだけを残します。モデルが確信しているときは候補が数個、迷っているときは多くなり、状況に合わせて変わります。XSLM にはありませんが、実際のサービスでよく使います。
語彙が数万個あるモデルでは、top_k 40〜60 がよく使われます。(温度 0.7、top_k 30)は正確に、(0.9, 50)はバランスよく、(1.1, 100)は創造的に使う組み合わせです。語彙が40個の XSLM では、8くらいが似た比率です。
AIに聞いてみる
seed — ランダムな選択を再現する
temperature が0より大きいと、random.choices がトークンをランダムに引きます。generate は呼ばれるたびに random.Random(seed) で新しい生成器を作るので、同じチェックポイント・プロンプト ・温度・top_k・seed なら、いつも同じ文章が出ます。
下の実験は、重み 5:3:2 で a・b・c を12回引きます。seed 7 は2回とも同じ行が出て、seed 8 は違う行が出ます。重みが大きい a が最も多く出ますが、順序は seed が決めます。
seed は結果を再現する番号であって、良い答えを保証する番号ではありません。設定を比べるときは seed を固定して1つだけを変え、結果が運なのかを見るには seed をいくつも変えてみます。
自分で確かめる lab/seed.py
import random
for seed in [7, 7, 8]:
rng = random.Random(seed)
print(seed, ''.join(rng.choices('abc', weights=[5, 3, 2])[0] for _ in range(12)))
cd ~/xslm
mkdir -p lab
micro lab/seed.py
python3 -m lab.seed
実行結果
7 aababaabaaaa
7 aababaabaaaa
8 acabaacabaaa
止まる条件 — 改行と最大トークン数
生成は2つの場合に止まります。
改行トークンを引いたとき: 学習資料では答えの1行が改行で終わっていたので、モデルが「答えが終わった」と判断したのです。改行は結果に入れません。
要求されたトークン数(基本24、最大64)を満たしたとき: モデルが終わりを出せなくても、果てしなく書かないように防ぎます。
結果には、プロンプトを除いて新しく作ったトークンだけを decode して返します。最初のトークンから改行が引かれると、空の答えになります。
実際のモデルは、EOS 特殊トークンや決めた停止の印(例: [/ANSWER])で止まり、その後ろは切り捨てます。トークンを作るたびにすぐ画面へ送るストリーミング もよく使われます。
プロンプトの形式 — 学習した形と空白まで同じに
モデルは、学習資料で見た形の後に来る文章だけを上手に続けます。だから生成するときに入れる文章(プロンプト)は、学習の形式とまったく同じでなければなりません。XSLM の基本プロンプトは、"Q: hi" の次の行に "A: " です。
空白1つも大事です。学習資料の "A: " は1トークン(30番)ですが、最後の空白を抜いて "A:" にすると "A"(5番)と ":"(4番)の2トークンになります。この2トークンがこう並んだことは学習資料に一度もないので、モデルは初めて見る状況で答えを始めます。デスクトップで学習率 0.1 で3,000回学習したモデルに sun を聞いてみると、"A: " で終えると "te sun is warn is warn…" のように学習の文に似た文章を、"A:" で終えると "ore" のようなかけらだけを出しました。
形式が長いなら、答えの書き出しまで入れてあげます。例えば [ANSWER] の次に "要約:" と "-" まで入れると、モデルはすぐに要約のリストを続けて書きます。
答えがおかしいとき — 原因と実験
小さなモデルの答えがおかしい原因を、よくある順に見ます。
学習不足 : 基本の20回は確認用です。損失が 3.7 付近なら、モデルはまだほとんど何も知りません。
資料不足: 12組では形式をまねるのがやっとで、初めて見る質問には答えられません。
形式の不一致: プロンプトが学習の形式と違うか、知らない文字(�)が混ざっています。
文脈不足: context 12トークンを超える前の部分は見えません。
サンプリング の設定: 繰り返すなら温度・top_k を少し上げ、でたらめなら下げます。
ランダム性を上げても、足りない学習は解決しません。試す価値のある実験はこうです。新しいチェックポイントで学習率を 0.1 に変え、--steps を数百回ずつに分けて学習しながら、同じ質問を temperature 0 で聞いて、答えがどう変わるかを記録します。上で見た "te sun is warn…" は、学習の文 "the sun is warm." をまね始めたものの、まだ暗記すらできていない段階です。
入力して実験
generate.pyを書き、同じ重みで温度、候補数、seedを一つずつ変えます。
cd ~/xslm
micro generate.py
python3 generate.py --temperature 0 --top-k 1
python3 generate.py --temperature 0.8 --top-k 8 --seed 7
python3 generate.py --temperature 1.2 --top-k 0 --seed 8
ターミナルで実習する
generate.py 手入力する完全なソース
構造と重みを復元し、文脈を切って反復抽選します。
import argparse
import json
import math
import random
from pathlib import Path
from model import GPT
from tokenizer import BPE
def load():
saved = json.loads(Path('checkpoints/model.json').read_text())
cfg = saved['settings']
tokenizer = BPE(**saved['tokenizer'])
model = GPT(len(tokenizer.vocab), cfg['width'], cfg['context'], cfg['seed'])
model.load(saved['weights'])
return model, tokenizer, cfg
def generate(model, tokenizer, prompt, tokens=24, temperature=0.8, top_k=8, seed=7):
if not prompt or tokens < 1 or temperature < 0 or not math.isfinite(temperature) or top_k < 0:
raise ValueError('invalid sampling parameters')
ids, result, rng = tokenizer.encode(prompt), [], random.Random(seed)
for _ in range(tokens):
logits = model.forward(ids[-model.context:])[-1]
ranked = sorted(range(len(logits)), key=lambda i: logits[i].data, reverse=True)
if temperature == 0:
token = ranked[0]
else:
candidates = ranked[:top_k] if top_k else ranked
peak = logits[candidates[0]].data
weights = [math.exp((logits[i].data - peak) / temperature) for i in candidates]
token = rng.choices(candidates, weights=weights)[0]
ids.append(token)
if tokenizer.vocab[token] == '\n':
break
result.append(token)
return tokenizer.decode(result)
if __name__ == '__main__':
model, tokenizer, cfg = load()
parser = argparse.ArgumentParser()
parser.add_argument('--prompt', default='Q: hi\nA: ')
parser.add_argument('--tokens', type=int, default=cfg['tokens'])
parser.add_argument('--temperature', type=float, default=0.8)
parser.add_argument('--top-k', type=int, default=8)
parser.add_argument('--seed', type=int, default=cfg['seed'])
args = parser.parse_args()
assert 1 <= args.tokens <= cfg['max_tokens']
print(generate(model, tokenizer, args.prompt, args.tokens,
args.temperature, args.top_k, args.seed))
コードを1行ずつ読み解く
generate.py load — チェックポイントからモデルを復元する
def load():
saved = json.loads(Path('checkpoints/model.json').read_text())
cfg = saved['settings']
tokenizer = BPE(**saved['tokenizer'])
model = GPT(len(tokenizer.vocab), cfg['width'], cfg['context'], cfg['seed'])
model.load(saved['weights'])
return model, tokenizer, cfg
model.json 1つから設定とトークナイザー まで読むので、settings.json や tokenizer.json があとで変わっても、学習したときと同じモデルを作ります。同じ大きさの GPT を作ってから(最初の値はすぐ上書きされる)、load で学習した数字を入れます。
generate.py 入力の検査と準備
def generate(model, tokenizer, prompt, tokens=24, temperature=0.8, top_k=8, seed=7):
if not prompt or tokens < 1 or temperature < 0 or not math.isfinite(temperature) or top_k < 0:
raise ValueError('invalid sampling parameters')
ids, result, rng = tokenizer.encode(prompt), [], random.Random(seed)
空のプロンプト、1より小さいトークン数、負・無限大・NaN の温度、負の top_k は ValueError で拒否します。math.isfinite は inf・nan なら偽です。プロンプトを番号に変え、新しく作ったトークンを集める result と、今回の生成だけの乱数生成器 rng を作ります。
generate.py 次のトークンの点数を求める
for _ in range(tokens):
logits = model.forward(ids[-model.context:])[-1]
ranked = sorted(range(len(logits)), key=lambda i: logits[i].data, reverse=True)
ids[-model.context:] で最後の12トークンだけを入れ、forward の結果の最後の行 [-1] だけを使います。ranked は語彙の番号を点数の高い順に並べ替えたリストで、ranked[0] が最も高いトークンです。
generate.py トークンを1つ選ぶ
if temperature == 0:
token = ranked[0]
else:
candidates = ranked[:top_k] if top_k else ranked
peak = logits[candidates[0]].data
weights = [math.exp((logits[i].data - peak) / temperature) for i in candidates]
token = rng.choices(candidates, weights=weights)[0]
temperature が0なら、最も高いトークンを選びます。そうでなければ top_k 個(0ならすべて)を候補に残し、最も大きい点数 peak を引いてから temperature で割って exp した値を重みにし、rng.choices で1つを引きます。choices はリストを返すので、[0] で取り出します。
generate.py つけて、止めて、返す
ids.append(token)
if tokenizer.vocab[token] == '\n':
break
result.append(token)
return tokenizer.decode(result)
選んだトークンを ids につけ、次の周の入力に入れます。そのトークンが改行なら結果に入れずに止まり、そうでなければ result に集めます。繰り返し が終わると result だけを decode して返すので、プロンプトは結果に入りません。
generate.py コマンドラインでの実行
if __name__ == '__main__':
model, tokenizer, cfg = load()
parser = argparse.ArgumentParser()
parser.add_argument('--prompt', default='Q: hi\nA: ')
parser.add_argument('--tokens', type=int, default=cfg['tokens'])
parser.add_argument('--temperature', type=float, default=0.8)
parser.add_argument('--top-k', type=int, default=8)
parser.add_argument('--seed', type=int, default=cfg['seed'])
args = parser.parse_args()
assert 1 <= args.tokens <= cfg['max_tokens']
print(generate(model, tokenizer, args.prompt, args.tokens,
args.temperature, args.top_k, args.seed))
直接実行すると、チェックポイントを読み、--prompt・--tokens・--temperature・--top-k・--seed オプションを受け取ります。argparse は --top-k を args.top_k に変えてくれます(- が _ に)。トークン数が 1〜max_tokens(64) かを assert で確かめてから、生成した文章を表示します。シェル で改行を含むプロンプトを渡すには、--prompt "$(printf 'Q: cat\nA: ')" のように printf を使います。
理解を確認
同じ入力と設定なら再現できます。最初が改行なら空の回答もあります。
困ったとき 不自然な結果は小規模学習では普通です。形式・未知文字・loss を確認します。
よくある質問
同じ質問に、なぜ毎回同じ答えが出るのですか?
seed が同じだからです。generate は要求ごとに同じ seed で生成器を新しく作ります。違う答えを見るには、--seed を変えるか、Runner の要求 JSON に "seed" を別の整数で入れてください。temperature が0なら、seed に関係なくいつも同じです。
温度を上げると賢くなりますか?
違います。温度は、すでに計算された確率から選ぶ方法だけを変えます。モデルが正解に低い確率を与えているなら、温度をどう変えても正解はあまり出ません。賢さは学習(資料・大きさ・回数)が決めます。
空の答えが出ます。
最初のトークンとして改行が引かれ、すぐに止まった場合です。学習資料では "A: " の直後に改行することはないので、学習が足りないか、プロンプトの形式が違うときに起きます。プロンプトの最後が "A: " かを確かめ、学習を増やすか、seed・temperature を変えてみてください。