跳到內容

step 01 / 19Part II 模型原理(選修)

S1 神經網路基礎

S0 全局地圖裡回答問題的模型是怎麼來的?Part II 模型原理(S1–S5)由下往上回答,這一章先回答最底層的問題:模型為什麼能「學」。答案是一個迴圈:定義一個衡量「錯多少」的 loss,算出每個參數該往哪個方向調、調多少(梯度),調一小步,重複幾千萬次。整章用「預測名字的下一個字元」當例子,因為這和 LLM 的訓練目標是同一件事,只是規模小到能在筆電上跑。

  • 能說出訓練迴圈的四步各做了什麼,並從「給正確答案的機率」估出 loss 與 perplexity。(講義與自我檢查)
  • 能用「倍率相乘」解釋 backprop,並看一條 loss 曲線指出可能的問題(學習率太大、初始化不好、overfitting)。(講義與自我檢查)
  • 從零寫出一個 scalar autograd,並用數值梯度驗證它是對的;親手畫出學習率太大時的 loss 曲線。(必做)
  • 訓練一個小型字元級語言模型,說出它的 loss 和 perplexity 代表什麼,並從 activation 分布圖找出飽和。(進階)
  • 用實驗追蹤工具比較多組超參數。(進階)

先看全貌。所有神經網路的訓練都是同一個迴圈:

  1. 前向(forward):把一筆資料送進模型,得到預測。
  2. 算 loss:用一個數字表示預測錯了多少。
  3. 反向(backward):算出每個參數的梯度,也就是「這個參數往上調一點,loss 會變大還是變小、變多快」。這一步叫 backprop。
  4. 更新:每個參數往讓 loss 變小的方向走一小步。怎麼走由 optimizer 決定。

回到第 1 步,訓練固定的步數,並定期用沒拿來訓練的 validation data 檢查 loss(→ 提前停止)。下面各節依序放大:先定義任務與 loss,再看梯度怎麼算、optimizer 怎麼用梯度,最後是訓練不順時怎麼診斷。

語言模型做的事是:給定前面的內容,輸出下一個單位的機率分布。LLM 的單位是 token(→ S2 Transformer 與 decoding),這一章的單位是字元。

例子:名字的下一個字元。 資料是約 3.2 萬個英文名字(makemore 的 names.txt)。字元集是 26 個小寫字母,加上一個代表「開頭 / 結尾」的符號 .,共 27 個。名字 emma 會拆成 5 題練習:

已知前文 要預測
. e
.e m
.em m
.emm a
.emma .(結束)

最簡單的模型:bigram。 只看前一個字元,用計數表估機率:數一數整份資料裡 m 後面接什麼。假設(示意數字)m 後面接 a 佔 40%、i 15%、e 15%、結束 10%、m 5%、其他字母合計 15%,模型就把這些比例當成預測的機率。

Loss:給正確答案的機率越低,罰越重。 每一題的 loss 是 −ln(模型給正確答案的機率),叫 cross-entropy loss。不用會推導,只要記住這張對照表:

給正確答案的機率 1 0.5 0.25 0.1 1/27 0.01
loss 0 0.69 1.39 2.30 3.30 4.61

emma 的第 3 題(m 後面接 m)模型只給 0.05,loss 是 −ln 0.05 ≈ 3.00;第 4 題(m 後面接 a)給 0.40,loss ≈ 0.92。整份資料的 loss 是所有題目的平均。1/27 那一欄很重要:27 個字元完全平均亂猜時,loss ≈ 3.30,這是一個還沒學任何東西的模型應有的起點。

Perplexity:換成「在幾個選項之間猶豫」。 Perplexity = e 的(平均 loss)次方。如果模型每一題都給正確答案 0.25,loss 是 1.39,perplexity 是 4,意思是模型的困惑程度相當於每次在 4 個字元之間平均亂猜。完全亂猜時 perplexity 是 27。Loss 的「差」對應 perplexity 的「比」:loss 從 2.0 降到 1.9,perplexity 從 7.39 降到 6.69(← 數學補給:對數尺度)。

從計數表到神經網路。 計數表只能看前一個字元;要看前 3 個,組合數變成 27³ 種,大多數組合在資料裡幾乎沒出現,計數不可靠。神經網路的做法是:

  • 每個字元對應一個學出來的小向量(例如 10 個數字),叫 embedding。意思相近的字元(例如母音)訓練後會有相近的向量。
  • 把前 3 個字元的 embedding 接起來(30 個數字),送進一層 hidden layer 加工,最後輸出 27 個分數,每個字元一個,叫 logit。
  • softmax 把 27 個分數轉成加總為 1 的機率:分數越高機率越高,分數差 1,機率差約 2.72 倍(2.72 就是 e,← 數學補給:softmax)。

一層在做什麼:加權總和,也就是矩陣乘法。 Hidden layer 裡的每個神經元做兩件事:把所有輸入各乘上自己的一個權重再加起來(再加一個常數,叫 bias),然後把這個加權總和送進 tanh,壓到 −1 到 1 之間。一層有很多個神經元,每個都有自己的一組權重;把這些權重一列一列排成一張表(矩陣),整層的加權總和就能一次算完,這個運算叫矩陣乘法。小例子:輸入是 3 個數字 [1, 2, 3],這一層有 2 個輸出,權重矩陣是 2 × 3:

對輸入 1 對輸入 2 對輸入 3 加權總和
輸出 1 1 0 −1 1×1 + 2×0 + 3×(−1) = −2
輸出 2 0.5 0.5 0.5 1×0.5 + 2×0.5 + 3×0.5 = 3

輸出是 [−2, 3]:每個輸出都是輸入的一組加權總和,矩陣的每一列就是一組權重。M1 makemore MLP 的 hidden 層就是 30 個輸入、200 個輸出的同一件事,輸出層是 200 個輸入、27 個 logit(這是進階題,選做;代號說明)。下一章的 Transformer 裡,大部分計算也都是這種矩陣乘法(→ S2 Transformer 與 decoding)。

這就是 M1 makemore MLP 要做的語言模型。模型裡所有的數字(embedding、每層的權重)都是參數,一開始是隨機的,靠下面幾節的迴圈慢慢調好。之後每一章的 LLM,本質上都是更大、更好的 next-token predictor。

容易誤會:loss 不會、也不該降到 0。 m 後面本來就可能接很多字母,資料本身有不確定性。在 bigram 這種只看一個字元的設定下,最好的模型也只能給出真實的比例,loss 有下限(實測 bigram 約 2.45)。訓練 loss 降到接近 0,通常代表模型在背答案(見 overfitting)。

準備動手做:公式、平滑化與 MLP 的形狀
  • 公式:softmax 是 pᵢ = e^(zᵢ) ÷ Σⱼ e^(zⱼ),zᵢ 是第 i 個字元的 logit。一份資料 N 題的 cross-entropy 是 L = −(1/N) Σ ln p(正確答案),perplexity = e^L。
  • 平滑化:計數表裡沒出現過的組合機率是 0,−ln 0 是無限大。常見做法是每格先加 1 再算比例(add-one smoothing)。
  • 一層線性 + softmax 等於計數表:把前一個字元表示成長度 27、只有一格是 1 的向量(one-hot),乘上一個 27 × 27 的權重矩陣,結果就是取出其中一列當 logits。用梯度下降訓練這個矩陣,會學到和計數表幾乎一樣的機率,所以 M1 makemore MLP 的步驟 2 兩個版本 val loss 應該接近。
  • MLP 的形狀(以 embedding 10 維、hidden 200 為例):embedding 表 27 × 10 = 270;hidden 層 30 × 200 權重加 200 個 bias = 6,200;輸出層 200 × 27 加 27 = 5,427;共 11,897 個參數。
  • 前文不足 3 個字元:用 . 補在前面,emma 的第一題是「... → e」。

梯度告訴每個參數「往哪調、調多快」。Backprop 是一次算出所有參數梯度的方法:把整個計算拆成小步,每一步只算自己的「放大倍率」,再從 loss 往回乘。

先用最笨的方法量梯度:把權重推一點,看 loss 怎麼變。 把 bigram 縮到最小:m 後面只有 a、m 兩個候選,而且這一題的正確答案是 a。模型只有一個權重 w,它是 a 的分數;m 的分數固定是 0。計算分兩步:

  • 第一步,w 經過 softmax 變成 a 的機率 p。兩個候選時 p = eʷ ÷ (eʷ + 1)。w = 0 時兩個分數一樣,p = 0.5。
  • 第二步,p 變成 loss = −ln p = 0.6931。

把 w 推到 0.01(代入上式,用計算機算):p 變成 0.5025,loss 變成 0.6882,少了約 0.0050。w 動 0.01、loss 動 −0.005,倍率約 −0.5,這就是 loss 對 w 的梯度。負號代表:w 往上調,loss 會變小。

Backprop:每一步各量各的,再相乘。 不必每次從頭推。分開量兩步的倍率:

步驟 推一點 結果變化 局部倍率
w → p w 從 0 到 0.01 p 從 0.5 到 0.5025 +0.25
p → loss p 從 0.5 到 0.501 loss 從 0.6931 到 0.6911 −2

兩個局部倍率相乘:0.25 × (−2) = −0.5,和直接量的一樣。道理很直觀:w 動一點,p 動 0.25 倍那麼多;p 每動一點,loss 又動 −2 倍,合起來就是 −0.5 倍。這就是 backprop:前向時記下每一步的運算,反向時從 loss 出發,把「後面傳回來的倍率 × 自己的局部倍率」一路傳到每個參數。模型再大,也只是步驟更多、分岔更多,每個小運算(加、乘、tanh……)只需要知道自己的局部倍率。

一個值被用兩次,兩條路的梯度要相加。 例如 y = x × x,x = 3。x 從兩條路影響 y,每條路的局部倍率都是另一邊的 x,也就是 3,所以 y 對 x 的梯度是 3 + 3 = 6。驗算:x 推到 3.01,y 從 9 變成 9.0601,倍率約 6.01。如果程式裡用「覆蓋」而不是「累加」,就會得到錯的 3。必做題特別要求你指出處理這件事的那一行。

為什麼不一直用「推一點」的方法。 M1 makemore MLP 的模型(embedding 10 維、hidden 200)約有 1.2 萬個參數,逐一推一下就要多跑 1.2 萬次前向;backprop 跑一次反向就拿到全部梯度,成本約是一次前向的兩倍。「推一點」的方法還是很有用:它是檢查 backprop 寫對沒有的標準方法,叫數值梯度檢查。

容易誤會:backprop 算的不是近似值。 它算出的是精確梯度,只有浮點誤差;「推一點」的數值梯度才是近似。兩者本來就有極小的差,所以驗收看相對誤差、不要求完全相等;差得明顯,通常是 backprop 寫錯。

深入一點:用連鎖律寫出來

上面的「局部倍率」就是導數,「相乘」就是連鎖律(← 數學補給)。

  • 兩個候選、一個分數 w 時,p = 1 ÷ (1 + e^(−w)),dp/dw = p(1 − p),w = 0 時是 0.25。
  • loss = −ln p,d loss / dp = −1/p,p = 0.5 時是 −2。
  • 連鎖律:d loss / dw = p(1 − p) × (−1/p) = p − 1 = −0.5。
  • 一般情況:softmax 加 cross-entropy 對每個 logit 的梯度是「預測機率 − 正確答案」,正確字元那格減 1,其他格是 0。所以實作時常把兩者合成一個運算,數值上也更穩定。
準備動手做:autograd 引擎的結構
  • 每個節點存:數值、梯度(初始為 0)、產生它的輸入節點,以及一個「把自己的梯度乘上局部導數、加到輸入節點梯度上」的函式。
  • 常用局部導數:a + b 對 a、b 都是 1;a × b 對 a 是 b、對 b 是 a;aⁿ(n 是常數)是 n·aⁿ⁻¹;eᵃ 是 eᵃ;tanh(a) 是 1 − tanh(a)²;a ÷ b 可以寫成 a × b⁻¹,不必另寫。
  • 反向的順序:對計算圖做拓樸排序,確保一個節點的所有使用者都處理完,才把它的梯度往前傳。輸出節點的梯度設為 1。
  • 梯度用 += 累加:這就是「用兩次要相加」。也因為累加,每個訓練步驟開始前要把梯度清回 0。
  • 中央差分:(f(x + h) − f(x − h)) ÷ 2h 比單邊的 (f(x + h) − f(x)) ÷ h 準得多,誤差和 h² 成正比而不是 h。float64 下 h = 1e-6 是常用的折衷。

Optimizer 決定拿到梯度之後怎麼走。最基本的是沿梯度反方向走一小步;現代 LLM 的預設是 AdamW,它讓每個參數有自己的步長。

梯度下降。 新的參數 = 舊的參數 − 學習率 × 梯度。學習率(learning rate)控制步長,是最重要的超參數。延續上一節,但這次看 m 後面的 4 題:3 題是 a、1 題是 m。平均 loss 在 w = 0 時是 0.693,用「推一點」量出梯度約 −0.25。

學習率 新的 w 新的平均 loss 結果
1 0 − 1 × (−0.25) = 0.25 0.638 變好
20 0 − 20 × (−0.25) = 5 1.257 一步走過頭,比原來還差

學習率 1 時重複這個迴圈,w 會停在約 1.10,這時 a 的機率是 0.75,正好等於資料裡的比例,平均 loss 是 0.562,不會再降(上一節說的下限)。學習率 20 時 a 的機率衝到 0.993,正確答案是 m 的那題,loss 變得很大。學習率太大的典型症狀就是 loss 震盪、上升,甚至變成 NaN。

SGD:每次只看一小批。 名字資料拆出約 22.8 萬題,每走一步都算全部太慢。實際做法是每次隨機抽一小批(mini-batch,例如 32 題)算梯度。這個梯度有雜訊,但便宜得多,可以走更多步,這就是 SGD(stochastic gradient descent)。

Adam:每個參數自己的步長。 不同參數的梯度大小可能差很多。假設參數 A 的梯度一直在 100 左右,參數 B 一直在 0.01 左右,學習率 0.001:SGD 讓 A 一步走 0.1、B 一步只走 0.00001,同一個學習率對 A 太大、對 B 太小。Adam 對每個參數記兩個移動平均:梯度的平均(決定方向,順便把雜訊平均掉),和梯度平方的平均(估計這個參數的梯度通常多大),走之前把步長除以後者的平方根(約等於這個參數梯度的典型大小)。結果 A、B 每步都走大約 0.001。所以上億個尺度差很多的參數能共用同一個學習率,對超參數也比較不敏感。

AdamW:把 weight decay 拆出來。 Weight decay 是每一步把參數往 0 拉一點(例如學習率 0.001、decay 0.1,每步把參數縮小 0.01%),防止參數長得太大,是一種正規化。原本的 Adam 把 weight decay 加進梯度裡,於是它也被「除以梯度大小」:梯度大的參數,被正規化得比較少。AdamW 把 weight decay 從梯度更新中分離出來,直接作用在參數上,每個參數都一致地生效。這是目前訓練 LLM 的預設選擇。

容易誤會:用了 Adam 不代表不用調學習率。 Adam 讓學習率比較好選,但它仍是第一個要調的超參數。學習率在訓練中通常也不是固定的,大規模訓練會先慢慢升、再逐步降(→ S3 Pretraining 與 scaling)。

深入一點:Adam 與 AdamW 的更新式

每一步,對每個參數 θ(梯度 g、學習率 η):

  • m ← β₁·m + (1 − β₁)·g(梯度的移動平均,也就是 momentum)
  • v ← β₂·v + (1 − β₂)·g²(梯度平方的移動平均)
  • 偏差修正:m̂ = m ÷ (1 − β₁ᵗ)、v̂ = v ÷ (1 − β₂ᵗ)。m、v 從 0 開始,前幾步會偏小,t 是步數。
  • Adam:θ ← θ − η·m̂ ÷ (√v̂ + ε)。加 L2 正規化時,是把 λθ 加進 g,於是它也被 √v̂ 除掉。
  • AdamW:θ ← θ − η·(m̂ ÷ (√v̂ + ε) + λθ),decay 項不經過 √v̂。

PyTorch 的預設值是 β₁ = 0.9、β₂ = 0.999、ε = 1e-8;AdamW 的 weight_decay 預設 0.01。

初始權重的大小決定訓練能不能順利開始。太大,訊號會飽和、梯度消失;太小,訊號一層層縮小。Normalization 層則在訓練過程中持續把數值拉回穩定的範圍。

第一個檢查:初始 loss。 還沒訓練的模型應該對 27 個字元差不多一視同仁,初始 loss 應該接近 ln 27 ≈ 3.30。如果第一步的 loss 是 15,代表模型給正確答案的機率只有 e⁻¹⁵,大約三百萬分之一:它沒學過任何東西,卻非常有把握地猜錯。原因通常是輸出層的初始權重太大,27 個分數差距很大,softmax 把幾乎所有機率給了某個隨機字元。修法是把輸出層的初始權重設得很小,讓 27 個分數接近,機率接近平均。這樣訓練的前幾百步不會浪費在「把過度自信壓下來」。

第二個檢查:tanh 有沒有飽和。 Hidden layer 常用 tanh 把數值壓進 −1 到 1 之間。輸入超過 ±3 時,輸出已經是 ±0.995,再推輸入一點,輸出幾乎不動,局部倍率只剩約 0.01。依 backprop 的規則,往回傳的梯度要乘上這個倍率,於是幾乎傳不回去,前面的參數學不到東西。M1 makemore MLP 用 |tanh| > 0.97 當飽和的標準:這時局部倍率不到 0.06。

為什麼會飽和?M1 makemore MLP 的 hidden 層每個神經元把 30 個輸入加權相加。如果每個輸入和權重的大小都在 1 左右,30 項的正負號隨機,大多互相抵消,所以加起來的典型大小只長到約 √30 ≈ 5.5,而不是 30;即使如此,也大部分落在 tanh 的平坦區。修法是依輸入數量縮小初始權重:乘上約 1/√30 ≈ 0.18,加總的典型大小就回到 1 左右。

Normalization 層。 初始化只管第一步,訓練中權重一直在變,每層輸入的尺度也跟著漂移。Normalization 層把每層的輸入重新調整成平均約 0、分散程度約 1,再乘上可學的縮放係數。BatchNorm 是對一個 batch 裡的同一個特徵做;LayerNorm 是對一筆資料自己的所有特徵做,Transformer 用的是後者,下一章會再換成更簡單的 RMSNorm(→ S2 Transformer 與 decoding)。M1 makemore MLP 不要求加 normalization,但網路一深就少不了它。

容易誤會:初始 loss 接近 3.30 不代表一切正常。 輸出層正確,hidden 層仍可能大量飽和。所以兩件事都要量:初始 loss,以及每層 activation 的分布(例如畫直方圖,看是不是大量堆在 ±1)。

準備動手做:初始化公式與診斷圖
  • 縮放公式:若每個輸入的分散程度約 1,權重用標準差 gain ÷ √fan_in 的隨機數,fan_in 是輸入數量。tanh 常用 gain = 5/3,ReLU 用 √2(Kaiming 初始化)。PyTorch 的 torch.nn.init.kaiming_normal_ 可以直接用。
  • BatchNorm 的麻煩:它用 batch 的平均與標準差,所以訓練時一筆資料的輸出會受同一批其他資料影響;推論時改用訓練中累積的平均值,model.train() / model.eval() 切換錯會得到奇怪的結果。LayerNorm 只看單筆資料,沒有這個問題。
  • 該畫的三張圖:每層 activation 的直方圖(是否堆在 ±1,或全是 0)、每層梯度的直方圖(各層大小是否差好幾個數量級)、每個參數「每步更新量 ÷ 參數本身大小」的比例。最後一個的常見經驗值約 1e-3:遠小於它代表學習率太小,遠大於它代表太大。

模型在訓練資料上越來越好、在沒看過的資料上卻變差,叫 overfitting:它開始背答案,而不是學規律。正規化(regularization)是各種防止背答案的做法。

怎麼看出來:把資料分成三份。 M1 makemore MLP 把名字切成 train / val / test(80 / 10 / 10)。Train 用來更新參數;val 用來比較設定、決定何時停;test 只在最後用一次,報告最終成績。只看 train loss 無法判斷模型好不好,因為模型可以把 2.6 萬個訓練名字背下來,loss 很低,卻產生不出新名字。

例子。 訓練 loss 一路從 3.30 降到 1.2,val loss 降到 2.0 後開始回升。val 回升的那一刻起,模型學到的東西只對訓練資料有用。這時有幾種對策:

  • 提前停止(early stopping):保留 val loss 最低的那個時間點的參數。最簡單,幾乎不花成本,通常先試。
  • Weight decay:上一節的「每步往 0 拉一點」,讓模型不容易靠很大的權重記住個別例子。
  • Dropout:訓練時每一步隨機關掉一部分神經元,模型不能依賴任何單一路徑。
  • 更多資料,或更小的模型:背不下來,就只能學規律。

容易誤會:train 和 val loss 有差距不一定是 overfitting。 兩者有點差距很正常;關鍵是 val loss 有沒有停止下降甚至回升。反過來,train 和 val 都很高而且很接近,是模型太小或還沒訓練夠(underfitting),這時加正規化只會更糟。

深入一點:dropout 與 test set 的規矩
  • Dropout 的細節:訓練時以機率 p 把每個 activation 設為 0,留下的除以 (1 − p),讓期望值不變;推論時不丟,也不縮放。大型 LLM 預訓練通常資料量夠大、只過資料一兩遍,很多模型直接不用 dropout。
  • Test set 只用一次:如果反覆看 test 成績來挑設定,test 就變成另一個 val,報出來的成績會偏樂觀。

訓練是實驗科學:同一份程式換一個學習率,結果可能天差地遠。所以要會讀 loss 曲線,並把每次執行記錄下來比較,而不是靠記憶。

Loss 曲線的常見形狀。

看到的曲線 最可能的原因 先試
第一步就遠高於 3.30 初始化讓模型過度自信 縮小輸出層初始權重
劇烈震盪、往上爬,或出現 NaN 學習率太大 學習率除以 3–10
從頭到尾幾乎是平的 學習率太小、梯度沒傳回去(飽和),或程式有 bug 先做下面的除錯步驟
下降但很抖 batch 太小,梯度雜訊大 加大 batch 或降學習率
train 持續降、val 回升 overfitting 見上一節

除錯的順序。 先讓模型在極少量資料上 overfit(例如只拿 1 個 batch 反覆訓練,loss 應該降到遠低於初始值,只拿幾個例子時可接近 0;降不下去代表程式有錯)→ 確認初始 loss 合理 → 調學習率 → 才開始調架構。順序反過來,會在一個有 bug 的程式上調好幾天的架構。

實驗追蹤。 實驗追蹤工具(例如 Weights & Biases)幫你記錄每次執行的超參數、loss 曲線、程式版本與隨機種子(seed),並排比較。例如 M1 makemore MLP 跑三組:學習率 0.1、0.03、0.01,其他都一樣,在同一個步數比較 val loss,就能看出哪組最好、好多少。

容易誤會:差一點點就是比較好。 同一組設定換一個 seed 重跑,val loss 也會有小差異。先重跑一兩次看自然的雜訊有多大,小於雜訊的差距不算數。比較時也要在相同步數下比,訓練較久的那組本來就容易佔便宜。

word2vec、GloVe 用「一起出現的字意思相近」的原理,為每個字學一個向量。它們是 embedding 想法的起點,但和 S2 Transformer 與 decoding 講的模型內部 token embedding、S9 檢索與 RAG 用的 sentence embedding 是不同的東西(見術語表)。知道歷史有助理解,不是後面各章的前置。

理解路線(不寫程式):做完「先試一下」就可以跳到自我檢查。

  1. 模型每一題都給正確答案 0.1 的機率。用講義的對照表,平均 loss 和 perplexity 各是多少?

    對照答案

    loss = −ln 0.1 ≈ 2.30,perplexity = e^2.30 ≈ 10:困惑程度相當於每次在 10 個字元之間平均亂猜。每題都給正確答案機率 p 時,perplexity 就是 1/p。

  2. 某個權重 w = 1,量到的梯度是 +0.4,學習率 0.5。走一步之後 w 是多少?loss 預期變大還是變小?

    對照答案

    1 − 0.5 × 0.4 = 0.8。梯度為正代表 w 往上調 loss 會變大,所以往下走;步長不太大時 loss 會變小。學習率太大時可能一步走過頭,反而變大。

目標:寫出一個能對任意 scalar 運算式做 backprop 的小引擎,並證明梯度是對的。

步驟

  1. 定義一個 Value 型別:存數值、梯度,以及產生它的運算與輸入。
  2. 支援 +、*、-、/、pow(常數次方)、exp、tanh。每個運算要寫出自己的局部導數。
  3. 實作 backward():對計算圖做拓樸排序,從輸出開始反向累加梯度。注意同一個節點被用兩次時,梯度要相加而不是覆蓋。
  4. 寫數值梯度檢查:對每個輸入加減 h = 1e-6,用中央差分估計梯度,和 backprop 結果比較。
  5. 用你的引擎組一個 2 層小網路(例如 2 → 8 → 1),用 SGD 擬合一個簡單資料集(例如 XOR 或 4–20 個點的二元分類),畫出 loss 曲線。
  6. 把學習率調大 10 倍以上再跑一次,畫在同一張圖上。

驗收

  • 至少 5 個隨機運算式(每個至少 6 個運算、包含重複使用同一變數)通過梯度檢查,相對誤差 |a − b| ÷ max(|a|, |b|, 1e-8) 小於 1e-5,或絕對誤差 |a − b| 小於 1e-7(a、b 是兩種方法算出的梯度;分母加下限,是因為梯度接近 0 時相對誤差會失真;梯度很小或運算式的值很大時,數值梯度本身就不準,所以另設絕對誤差的門檻)。
  • 小網路訓練 loss 單調下降(允許小幅震盪),最後在訓練資料上 100% 分類正確。
  • 兩條 loss 曲線並列,用一兩句話說明大學習率那條哪裡不對(震盪、發散或卡住)。
  • 能指出你的程式中哪一行處理「同一節點被用兩次」。

提示

  • TypeScript:一個 class 加上 closure 存局部導數即可,不需要任何套件;JS 的 number 是 float64,梯度檢查精度足夠。
  • Python:純 Python 就好;寫完可以用 PyTorch 的 requires_grad 對同一個運算式算一次,交叉比對。
  • 卡住時:

進階:模型專案 M1:makemore MLP 與訓練診斷

Section titled “進階:模型專案 M1:makemore MLP 與訓練診斷”

目標:在 names 資料集上訓練字元級語言模型,從 bigram 一路做到 MLP,並學會用數字診斷訓練。

步驟

  1. 從 karpathy/makemore 取得 names.txt,切成 train / val / test(80 / 10 / 10)。
  2. 做 bigram baseline:計數版與「一層線性 + softmax」的梯度下降版,兩者 val loss 應該接近。
  3. 做 MLP 版本:character embedding → 串接前 3 個字元 → hidden layer → softmax。先用 PyTorch tensor 寫,必做題的 scalar 引擎太慢。
  4. 修正初始化:讓初始 loss 接近 ln 27,並讓 tanh 不再大量飽和。畫出修正前後每層 activation 的直方圖。
  5. 用 W&B(或任何實驗追蹤工具)跑至少 3 組超參數(學習率、hidden 大小、embedding 維度),記錄 val loss。
  6. 從最好的模型 sample 20 個名字。

驗收

  • 報告 bigram 與 MLP 的 val loss 與 perplexity,MLP 的 val loss 比 bigram 低至少 0.15。
  • 初始 loss 在 3.30 ± 0.2 之內。
  • 在初始化時(第 0 步)量 hidden layer 中 |tanh| > 0.97 的比例:修正後要比修正前明顯下降(例如從 50% 以上降到 25% 以下);另外附上訓練結束時的比例(訓練後比例上升是正常的)。
  • 實驗追蹤頁面上至少 3 次執行,能指出哪組最好、差多少。

提示

  1. backprop 算出來的梯度是近似值還是精確值?數值梯度檢查又在檢查什麼?

    提示

    判斷重點:backprop 是精確值(只有浮點誤差),「推一點」的數值梯度才是近似;檢查的是 backprop 有沒有寫錯。常見錯法:要求兩者完全相等。回讀:參數該往哪裡調?

  2. 一個 27 類的分類模型,訓練第一步的 loss 是 15。你會先懷疑什麼?

    提示

    判斷重點:合理起點是 ln 27 ≈ 3.30;15 代表模型非常有把握地猜錯,先懷疑輸出層初始權重太大。常見錯法:先去調學習率或架構。回讀:訓練為什麼一開始就卡住?

  3. Adam 和 SGD 的差別是什麼?為什麼 LLM 訓練幾乎都用 AdamW?

    提示

    判斷重點:Adam 依每個參數梯度的典型大小調整步長;AdamW 再把 weight decay 從梯度更新中拆出來,讓每個參數一致地被正規化。常見錯法:以為用了 Adam 就不必調學習率。回讀:拿到梯度後怎麼走?

  4. 情境題:訓練 loss 一路降到 1.2,val loss 降到 2.0 後開始回升。你會怎麼處理?列出至少兩種做法,以及你會先試哪一個。

    提示

    判斷重點:這是 overfitting;對策有提前停止、weight decay、dropout、更多資料或更小的模型,提前停止成本最低、通常先試。常見錯法:繼續訓練到 train loss 更低。回讀:模型在背答案怎麼辦?

  5. Perplexity 從 8 降到 6,用白話說代表什麼?

    提示

    判斷重點:模型的困惑程度從「在 8 個選項間平均亂猜」降到「在 6 個之間」。常見錯法:把它當成 loss 的差;perplexity 要看比例,loss 才看差。回讀:模型要學什麼、怎麼打分?

  6. 不看講義,自己走一次「推一點」的例子:兩個候選、w = 0、正確答案是 a。分別量出 w → p 與 p → loss 的局部倍率,相乘後和直接推 w 量到的梯度比較。如果正確答案改成 m,梯度的正負號會怎麼變?

    提示

    判斷重點:0.25 × (−2) = −0.5,和直接量的一樣;答案改成 m 時,w 往上調會讓 loss 變大,梯度變成 +0.5。常見錯法:以為 p → loss 的倍率仍是 −2,其實這時 loss 看的是 m 的機率 1 − p。回讀:參數該往哪裡調?

  7. 為什麼 tanh 大量飽和時,前面幾層的參數幾乎學不到東西?用 backprop「倍率相乘」的說法解釋。

    提示

    判斷重點:飽和時 tanh 的局部倍率接近 0,往回傳的梯度要乘上它,前面的梯度跟著趨近 0。常見錯法:只說「數值太大」,沒連到倍率相乘。回讀:訓練為什麼一開始就卡住?

講義已經涵蓋這一章需要的內容。下面的資料適合想看得更深、或想跟著別人一行一行寫程式的人,都不是必讀。

  • karpathy/nn-zero-to-hero — 前 5 支影片:micrograd、makemore 的 bigram、MLP、activations / gradients / BatchNorm、手寫 backprop。和本章的必做題、M1 makemore MLP 一一對應,適合邊看邊打程式;第 6 支(WaveNet 式的階層架構)把 MLP 改成更深的結構,是走向 S2 Transformer 與 decoding 的過渡。系列 2024 後未更新,但這幾支講的基礎不會過時。
  • rasbt/LLMs-from-scratch — Appendix A(PyTorch 入門)與 Appendix D(訓練迴圈的 warmup、cosine decay、gradient clipping)。
  • MiniTorch(Cornell)— 把 autograd 擴充到 tensor 與平行運算,適合寫完必做題還想再深入的人。
  • Stanford CS224N — 前三講:word vectors、神經網路與 backprop 的數學推導,比講義的深入區塊更完整。

講義內容以 CC BY-NC-SA 4.0 授權:可以轉載與改作,但要標示出處、不得用於商業用途,改作也要用相同授權。引用的第三方內容與商標屬於原作者。