Andrej Karpathy「Let's build GPT」解説シリーズ 第4動画
はじめに
これまでに、GPTモデルをゼロから構築し、マルチGPUでの大規模学習パイプラインを完成させました。しかし、学習中の損失(loss)が下がっているだけでは、モデルが本当に「賢く」なっているのか、汎用的な能力を獲得しているのかは分かりません。
この最終回では、モデルの性能を客観的に測定するための評価(Evaluation)と検証(Validation)のプロセスを実装します。具体的には、以下の2つを学習ループに組み込みます。
- 検証データセット: 学習に使っていないデータ(検証セット)で定期的に損失を計算し、過学習(Overfitting)を監視します。
- HellaSwagベンチマーク: 常識的な文脈理解を必要とする質疑応答データセットを使い、モデルの実用的な性能を評価します。
なぜモデルの「評価」と「検証」が重要なのか?
なぜ評価と検証が重要なのでしょうか?
-
検証 (Validation): 学習データ(教科書)に対する成績(訓練損失)が良いのは当然ですが、模擬試験(検証データ)でも良い成績が取れなければ、未知の問題には対応できません。訓練損失は下がり続けているのに、検証損失が上がり始めたら、それはモデルが教科書を丸暗記しているだけの「過学習」のサインです。
-
評価 (Evaluation): モデルの能力測定です。HellaSwagのようなベンチマークは、「この文の続きとして、最も自然なのはどれ?」といった、単なる単語予測以上の、常識的な推論能力を問います。これにより、モデルがどれだけ人間のように自然な文章を理解し、生成できるかを測ることができます。
具体的な実装
1. 検証ループの実装
検証は、学習ループの中で定期的に(例: 250ステップごと)行います。重要なのは、検証中はモデルの重みを更新しないことです。
# 学習ループ内
if step % 250 == 0 or last_step:
# 1. モデルを評価モードに切り替え
model.eval()
val_loader.reset()
# 2. 勾配計算を無効化
with torch.no_grad():
val_loss_accum = 0.0
val_loss_steps = 20
for _ in range(val_loss_steps):
x, y = val_loader.next_batch()
x, y = x.to(device), y.to(device)
with torch.autocast(device_type=device_type, dtype=torch.bfloat16):
logits, loss = model(x, y)
loss = loss / val_loss_steps
val_loss_accum += loss.detach()
# DDP環境では、全GPUの損失を平均化
if ddp:
dist.all_reduce(val_loss_accum, op=dist.ReduceOp.AVG)
if master_process:
print(f"validation loss: {val_loss_accum.item():.4f}")
# 検証が終わったら、訓練モードに戻す
model.train()
ポイント:
-
model.eval()
: DropoutやBatchNormのような、訓練時と評価時で挙動が異なる層を正しく切り替えます。 -
torch.no_grad()
: このブロック内では勾配が計算されなくなり、メモリ使用量が削減され、計算が高速化します。 -
dist.all_reduce
: DDP環境では、各GPUで計算された検証損失を集約して平均を取る必要があります。
2. HellaSwagによるベンチマーク評価
HellaSwagは、文の続きを選択するタスクです。人間にとっては95%以上が正解できる簡単な問題ですが、AIにとっては自然な文脈を理解する必要があるため、非常に難しいとされています。
評価方法:
- コンテキスト(文脈)と4つの選択肢(ending)をそれぞれモデルに入力します。
- 各選択肢について、その選択肢部分だけの損失(不自然さ)を計算します。
- 4つの選択肢のうち、最も不自然さが小さい(=一番自然に見える)ものをモデルの予測とします。
- モデルの予測が正解ラベルと一致しているかを評価します。
def render_example(example):
"""
例題を辞書として受け取り、3つのtorchテンソルとしてレンダリング:
- tokens (文脈+完成のトークン、4xNサイズ、常に4つの候補があるため)
- mask (候補完成の領域で1、尤度を評価する場所)
- label (正解完成のインデックス、最高尤度を持つことを期待)
"""
ctx = example["ctx"]
label = example["label"]
endings = example["endings"]
# 全てのトークンを収集
ctx_tokens = enc.encode(ctx)
tok_rows = []
mask_rows = []
for end in endings:
end_tokens = enc.encode(" " + end) # GPT-2トークナイザーのため" "を前置
tok_rows.append(ctx_tokens + end_tokens)
mask_rows.append([0]*len(ctx_tokens) + [1]*len(end_tokens))
# 各行のトークン数が異なる可能性があるため、照合時に注意が必要
max_len = max(len(row) for row in tok_rows)
tokens = torch.zeros((4, max_len), dtype=torch.long)
mask = torch.zeros((4, max_len), dtype=torch.long)
for i, (tok_row, mask_row) in enumerate(zip(tok_rows, mask_rows)):
tokens[i, :len(tok_row)] = torch.tensor(tok_row)
mask[i, :len(mask_row)] = torch.tensor(mask_row)
return data, tokens, mask, label
# 評価ループ
if (step % 250 == 0 or last_step):
num_correct_norm = 0
num_total = 0
for i, example in enumerate(iterate_examples("val")):
# DDPの場合、各プロセスがデータの一部を処理
if i % ddp_world_size != ddp_rank:
continue
# データをトークン化し、モデルに入力
_, tokens, mask, label = render_example(example)
tokens = tokens.to(device)
mask = mask.to(device)
with torch.no_grad():
with torch.autocast(device_type=device_type, dtype=torch.bfloat16):
logits, loss = model(tokens)
# 最も尤もらしい選択肢を予測
pred_norm = get_most_likely_row(tokens, mask, logits)
num_total += 1
num_correct_norm += int(pred_norm == label)
# 全プロセスの結果を集計
if ddp:
# ... dist.all_reduceでnum_totalとnum_correct_normを合計 ...
acc_norm = num_correct_norm / num_total
if master_process:
print(f"HellaSwag accuracy: {acc_norm:.4f}")
GPT-2(124M)モデルのHellaSwag精度は30%前後であり、ランダム(25%)より少し良い程度です。これは、このタスクの難しさを示しています。
実験と検証
学習を進めると、以下のようなログが出力されます。
step 250 | loss: 3.012117 | lr 6.00e-04 | ...
validation loss: 3.2150
HellaSwag accuracy: 0.2851
---
step 500 | loss: 2.874321 | lr 6.00e-04 | ...
validation loss: 3.0520
HellaSwag accuracy: 0.3125
訓練損失と検証損失が共に順調に下がり、HellaSwagの正解率も少しずつ向上していることが分かります。これは、モデルが過学習することなく、汎用的な言語能力を学習している良い兆候です。
また、学習の各段階でモデルにテキストを生成させることで、その成長を定性的に確認することもできます。10000ループほど学習させたモデルの出力を見ると、以前よりは文法的に正しそうなテキストを生成することに成功しました。
# 学習済みモデルからのテキスト生成
# Hello, I'm a language model, and I've no idea how to turn my name into a character. When I'm working on a language model to create ......
まとめ
今回は、検証セットとHellaSwagベンチマークを導入し、学習中のモデルの性能を評価する方法を学びました。最終的なモデルの出力は私たちが普段使うChatGPTの出力とは大きく異なり、少し期待外れだったかもしれませんが、大規模言語モデルがどのようなメカニズムで動いているのかは理解できたと思います。
この「ゼロからGPTを作る」経験が、皆さんが今後、大規模言語モデルをより深く理解し、活用し、あるいは自ら作り出す上での確かな一歩となれば幸いです。
(この記事は研究室インターンで取り組みました:https://kojima-r.github.io/kojima/)