Build a Large Language Model (From Scratch) · Chapter 5
在無標註資料上預訓練Pretraining on unlabeled data
第 4 章組好的 GPT 只會輸出亂碼——因為權重還是隨機的。這一章補上靈魂:先定義「生成得好不好」的數值指標(loss),再寫訓練迴圈實際預訓練它,接著用 temperature 與 top-k 控制生成的隨機性,最後把 OpenAI 訓練好的 GPT-2 權重直接載進我們的模型。頁面中的關鍵數值都與書中執行輸出一致(示意值會另行標明)。
本章定位:讓模型「學會講話」
整本書的三階段地圖裡,本章是第二階段:預訓練——把第一階段搭好的架構,變成一個能生成連貫文字的 foundation model:
資料處理 · attention · 架構 ✓→ STAGE 2(本章)
預訓練 pretraining訓練迴圈 · 模型評估 · 載入預訓練權重→ STAGE 3(ch6–7)
微調:分類器 / 助理
本章的 7 個步驟——會隨你往下讀自動打勾(也可以點擊跳到該節):
預訓練的邏輯一句話就講完:拿一段文字、遮住下一個字,讓模型猜;猜錯就把權重往「更會猜對」的方向調一點;重複幾百萬次。本章所有內容——loss、訓練迴圈、評估——都是在把這句話變成可以執行的程式。而「無標註資料」的妙處是:下一個字本身就是標準答案,不需要人工標記,網路上的任何文字都能拿來訓練。
🏋️ 訓練 training=forward 算預測 → 算 loss → backward 算梯度 → 更新權重——本章的主戲;💬 推論 inference=權重固定、只做 forward、一次吐一個字——5.1.1 的生成回顧與 5.3 的解碼策略屬於這邊。各節標題旁會掛上對應的標籤。
暖身:loss 是整章的方向盤
訓練要「往好的方向調」,前提是先能量化「現在有多差」。這個數字就是 loss(損失):愈小=模型把正確答案的機率估得愈高。先預告本章會看到的三個關鍵數字:
Using GPT to generate text(先讓 GPT 開口)💬 推論
"Every effort moves you"→tokenizer → GPTModel(未訓練)→ 解碼text_to_token_ids · generate_text_simple · token_ids_to_text→output:文字
(亂碼,預期結果)
先把第 4 章的模型架起來。設定和上一章幾乎相同,只改一個地方——點任一行看說明:
接著定義兩個貫穿全章的小工具:text_to_token_ids()(文字 → ID tensor,並用 unsqueeze(0) 補上 batch 維)與 token_ids_to_text()(反向)。按播放,看一段文字的完整往返:
tiktoken(gpt2)
→
6100, 345]])
形狀 [1, 4]
接龍 10 個 token
→
每輪 +1 個 ID
→
Calculating the text generation loss(生成文字的 loss)
+ targets [2, 3]→cross entropysoftmax → 取 target 位置機率 → log → 平均 → 取負→output:loss
一個數字(10.7940)
用兩句迷你例子把 loss 一步步算出來。inputs 是輸入、targets 是「右移一格」的正確答案(第 2 章 data loader 的老朋友)。滑過任一位置,看那個位置的模型視野與它該預測的答案:
訓練目標=把「正確答案格子」的機率推高
把 logits 過 softmax,每個位置得到一條「下一個字是誰」的機率分佈。書中用 7 個字的迷你詞彙表把這件事畫出來(數值為書中示意):黃框=target(正確答案)的格子,底色=該列 argmax(模型目前會選的字)。切換前後對照:
六步算出 cross entropy loss(書中真實數值)
對真正的 GPTModel(詞彙表 50257)做同樣的事。反向傳播需要一個 loss 函數來衡量「差多遠」——按「下一步」逐步推導:
# PyTorch 一行就把六步全包了(先把 batch 維攤平): logits_flat = logits.flatten(0, 1) # [2, 3, 50257] → [6, 50257] targets_flat = targets.flatten() # [2, 3] → [6] loss = torch.nn.functional.cross_entropy(logits_flat, targets_flat) # → tensor(10.7940) ✓ 和手算一致
Perplexity:loss 的白話翻譯
perplexity = exp(loss),可以直觀讀成「模型此刻等效在幾個字之間亂猜」。拖動 loss,看 perplexity 的爆炸性變化:
Training and validation set losses(訓練/驗證 loss)
20,479 字元 / 5,145 tokens→切 train / val → data loadermax_length=256 · stride=256 · batch_size=2→output:train_loss 與 val_loss
兩個監控指標
接著把 loss 從「兩句話」推廣到「整份資料集」。資料仍是第 2 章用過的公有領域短篇小說《The Verdict》:20,479 個字元、tokenize 後 5,145 個 token——小到筆電幾分鐘就能跑完一輪訓練。
只看訓練資料的 loss,就像只用「寫過的考古題」評估學生——背下來就能拿滿分。所以把資料切成兩份:訓練集(讓模型調權重用)和驗證集(模型從沒讀過、只用來測試)。兩個 loss 一起看,才能分辨模型是「真的學會語言」還是「把課文背起來」——這個對照正是本章結尾最大的伏筆。
拖動 train_ratio,看 5,145 個 token 怎麼被切成 256-token 的視窗、再兩兩組成 batch(書中用 0.9):
def calc_loss_batch(input_batch, target_batch, model, device):
input_batch = input_batch.to(device) # 有 GPU 就搬去 GPU
target_batch = target_batch.to(device)
logits = model(input_batch)
return torch.nn.functional.cross_entropy( # 上一節的六步,一行搞定
logits.flatten(0, 1), target_batch.flatten())
def calc_loss_loader(data_loader, model, device, num_batches=None):
... # 逐 batch 呼叫上面的函數、取平均
對還沒訓練的模型跑一次(with torch.no_grad()——只是量測、不用算梯度):
我們用 5,145 個 token 練習;Llama 2 7B 用了 2 兆個 token、184,320 個 A100 GPU 小時——照當時雲端價格估算約 69 萬美元。這就是為什麼 5.5 節「直接載入別人訓練好的權重」在實務上如此重要。
Training an LLM(訓練一個 LLM)🏋️ 訓練
(外加兩條 loss 曲線)
零件到齊,可以組訓練迴圈了。它就是把上方管線圖跑成迴圈:取 batch → forward 算 loss → backward 算梯度 → 更新權重,每 5 步量一次 train/val loss,每個 epoch 結束生成一段樣本讓你肉眼檢查。逐行走一遍:
lr=0.0004, weight_decay=0.1。實際訓練 10 個 epoch(書中真實輸出)
MacBook Air 上約 5 分鐘跑完。按「下一個 epoch」逐輪觀察:左邊是 loss 曲線(實心點=書中印出的值;空心點=依書中圖 5.12 的平滑內插),下面是該輪結束時模型生成的樣本——看著它從逗號進化成句子:
以教學目的,算:它證明了訓練迴圈正確、模型有能力學會語言結構。overfitting 嚴重是因為資料太小(5,145 tokens)卻跑了 10 個 epoch。真實 LLM 的做法相反:資料大到「每句話幾乎只看一次」(常常只訓練 1 個 epoch),自然不容易背起來。想試試大資料?書的附錄 D 提供用 Project Gutenberg 60,000 本書訓練的作法。
Decoding strategies(解碼策略:控制隨機性)💬 推論
[50257]→top-k 過濾 → ÷temperature → softmax → multinomial 抽樣取代原本的 argmax→output:下一個 token
(有隨機性、但不亂來)
剛訓練好的模型有個問題:generate_text_simple 用 argmax(永遠挑機率最高的字,稱為 greedy decoding),所以同一個開頭永遠生出同一句話——而且容易整段背出訓練集原文。解法:把「挑最大」改成「按機率抽籤」(torch.multinomial),再用兩個旋鈕控制抽籤的行為。
旋鈕①:temperature scaling(把分佈捏尖或壓平)
書中用 9 個字的迷你詞彙表示範。模型看到 "every effort moves you" 之後輸出這排 logits(下方數值),temperature 就是「把 logits 除以 T 再過 softmax」。拖動 T,機率分佈即時重算(真實計算):
旋鈕②:top-k sampling(把長尾直接剁掉)
高溫讓輸出多樣,但也讓 "every effort moves you pizza" 這種蠢話有 ~4% 的機會被抽中。top-k 的對策:只保留 logits 最高的 k 個字,其他一律設成 −∞——softmax 後機率變 0,永遠抽不到。拖動 k 看四個步驟(k=3 為書中例子):
組裝成新的 generate()
def generate(model, idx, max_new_tokens, context_size,
temperature=0.0, top_k=None, eos_id=None):
for _ in range(max_new_tokens):
idx_cond = idx[:, -context_size:]
with torch.no_grad():
logits = model(idx_cond)[:, -1, :]
if top_k is not None: # ① 先剁長尾
top_logits, _ = torch.topk(logits, top_k)
logits = torch.where(logits < top_logits[:, -1],
torch.tensor(float("-inf")).to(logits.device), logits)
if temperature > 0.0: # ② 再調溫、抽籤
probs = torch.softmax(logits / temperature, dim=-1)
idx_next = torch.multinomial(probs, num_samples=1)
else: # temperature=0 → 回到 argmax(greedy)
idx_next = torch.argmax(logits, dim=-1, keepdim=True)
if idx_next == eos_id: break # 遇到結束符提早收工
idx = torch.cat((idx, idx_next), dim=1)
return idx
temperature=0.0 完全關閉抽樣、回到確定性的 argmax。Loading and saving model weights(權重存讀)
下次不用重練
訓練很貴,練完(或練到一半)當然要存檔。PyTorch 的慣例是存 state_dict——一個「層名 → 權重 tensor」的字典。點下面 checkpoint 的兩把鑰匙,看各自裝了什麼、為什麼續訓時兩個都要存:
# 只做推論:存模型就夠 torch.save(model.state_dict(), "model.pth") model = GPTModel(GPT_CONFIG_124M) model.load_state_dict(torch.load("model.pth", map_location=device)) model.eval() # 推論前記得關 dropout! # 要續訓:模型 + optimizer 一起存 checkpoint = torch.load("model_and_optimizer.pth", map_location=device) model.load_state_dict(checkpoint["model_state_dict"]) optimizer = torch.optim.AdamW(model.parameters(), lr=5e-4, weight_decay=0.1) optimizer.load_state_dict(checkpoint["optimizer_state_dict"]) model.train();
Loading pretrained weights from OpenAI(載入 OpenAI 權重)
(TensorFlow checkpoint)→load_weights_into_gpt逐層對名字、檢查形狀、搬進我們的 GPTModel→output:會講人話的 GPT-2
(不用花 $690K)
OpenAI 公開了 GPT-2 系列的權重。用書中提供的 gpt_download.py 抓 124M 版本,會下載 7 個檔案:
四種尺寸,同一套架構——差別只在維度與層數(點選查看;粗體為對 124M 的改動):
載入前要把我們的 config 改回和 OpenAI 一致——點每一行看原因(都是前幾章埋的伏筆):
把權重「對名入座」:load_weights_into_gpt
OpenAI 的命名和我們不同,得逐層對應、搬運。輔助函數 assign(left, right) 會先檢查兩邊形狀一致才覆蓋——形狀不合就 raise,這是搬權重時最重要的保險。點一個部件,看它的對應關係:
驗收:它會講人話了
本章小結
- Loss=負平均對數機率(cross entropy):softmax 取 target 機率 → log → 平均 → 取負;未訓練約 10.98 ≈ ln(50257)。perplexity = exp(loss),讀作「等效在幾個字之間亂猜」。
- 資料切分:train 90% / val 10%;val loss 是唯一能誠實回答「有沒有真的學會」的指標。
- 訓練迴圈八步:epoch → batch → 清梯度 → forward 算 loss → backward → AdamW 更新 → 定期評估 → 生成樣本肉眼檢查。
- 實測 10 epochs:train 9.78 → 0.39、val 卡在 6.45——小資料多 epoch 必然 overfitting,模型逐字背出《The Verdict》。
- 解碼策略:multinomial 抽樣取代 argmax;temperature 把分佈捏尖(<1)或壓平(>1)、top-k 把長尾剁掉;T=0 回到 greedy。
- 存讀權重:state_dict 是「層名 → tensor」字典;續訓要連 AdamW 的 optimizer state 一起存。
- 載入 OpenAI 權重:config 改回 context 1024、qkv_bias=True;assign() 驗形狀、attention 的 c_attn 要 split 成 Q/K/V、out_head 與 wte 共用(weight tying)。
知識檢核
// 七題,作答後立即看解析
頁面中的計算皆為瀏覽器即時運算 · 快捷鍵 T 切換主題