模組 08 · 第 2 課

梯度下降

不再盲目地試,而是每一步都算出往哪個方向改參數、改多少。從斜率講起理解導數,手算梯度,寫出梯度下降的迴圈,再親眼看看學習率太小、太大和資料沒有標準化時會發生什麼。

  • 約 45 分鐘
  • 難度:入門
  • 實測:2026-09-14 numpy 2.5,固定隨機種子

程式碼和執行結果保留原樣(簡體中文),與實際執行時完全一致。

上一課用網格搜尋找到了最好的直線,但也看到了它的死路:參數一多,要試的組合就是天文數字。

想像你蒙著眼睛站在一座山的山坡上,要走到山谷的最低點。你看不見整座山,但能用腳感覺到腳下的地面往哪邊傾斜。那就往下坡的方向邁一小步,再感覺一下,再邁一步。一直走下去,最後就到了谷底。

這就是梯度下降(gradient descent)。"山"是損失,你站的位置是當前的參數,"往哪邊傾斜"就是梯度。

導數:就是斜率

先只看一個參數 w,固定 b = 0。把 w 往右挪一點點,損失會變多少?變化量除以挪動的距離,就是損失在這一點的斜率。用數值的辦法直接量一量:

def mse(w, b, x=area, y=price):
    return np.mean((w * x + b - y) ** 2)


w, b = 1.0, 0.0
for h in [0.1, 0.01, 0.001]:
    slope = (mse(w + h, b) - mse(w, b)) / h
    print(f"  w 往右挪 {h:<6},损失变化 / 挪动距离 = {slope:10.2f}")
== 1. 在 w=1.0, b=0 这一点,损失对 w 的斜率
  w 往右挪 0.1   ,损失变化 / 挪动距离 =   -7300.21
  w 往右挪 0.01  ,损失变化 / 挪动距离 =   -8256.23
  w 往右挪 0.001 ,损失变化 / 挪动距离 =   -8351.83

挪動的距離越小,量出來的斜率越穩定,逐漸接近一個固定的值。這個"挪動距離無限小時的斜率",就是導數。它告訴我們兩件事:

  • 符號:這裡是負數,意思是 w 往右挪(變大),損失會變小。所以應該把 w 調大。
  • 大小:絕對值很大,意思是這裡的坡很陡,損失對 w 非常敏感。

梯度:每個參數一個導數

我們有兩個參數,wb。對每個參數分別求導(求 w 的導數時把 b 當成常數,反過來也一樣),得到的兩個數合在一起,叫做梯度

用數值的辦法量太慢,也不精確。好在均方誤差的導數可以直接寫出公式。損失是 平均((w×x + b - y)²),按求導的規則:

损失对 w 的导数 = 平均( 2 × (w×x + b - y) × x )
损失对 b 的导数 = 平均( 2 × (w×x + b - y) )

不需要記這個推導,只要知道它就是"把上面量斜率的過程,用代數一次算準"。寫成程式碼:

def gradients(w, b, x=area, y=price):
    error = w * x + b - y
    dw = np.mean(2 * error * x)  # 损失对 w 的导数
    db = np.mean(2 * error)  # 损失对 b 的导数
    return dw, db
== 2. 公式算出的梯度:dw=-8362.45,db=-79.98

dw = -8362.45,和上面數值量出來的結果(挪動 0.001 時是 -8351.83)非常接近。這是一個好習慣:自己推導的梯度公式,用數值的辦法核對一下,能發現很多錯誤。下一課寫反向傳播時,我們還會這樣做。

梯度下降的迴圈

梯度指向損失增加最快的方向。我們要讓損失減小,就往梯度的反方向走一小步:

w = w - 学习率 × dw
b = b - 学习率 × db

學習率(learning rate)控制每一步邁多大。從 w = 0, b = 0 出發,重複這一步:

def descend(lr, steps, x=area, y=price, report=()):
    """从 w=0, b=0 出发走 steps 步。发散时返回 None。"""
    w, b = 0.0, 0.0
    for step in range(1, steps + 1):
        dw, db = gradients(w, b, x, y)
        w -= lr * dw
        b -= lr * db
        if abs(w) > 1e6:  # 越走越远,已经发散了
            print(f"  第 {step} 步:w 已经变成 {w:.3g},发散了")
            return None
        if step in report:
            print(f"  第 {step:5d} 步:w={w:7.4f} b={b:7.3f} 损失 {mse(w, b, x, y):10.2f}")
    return w, b

這就是訓練的全部核心邏輯。第 09 模組訓練 GPT 時,迴圈的結構一模一樣,只是參數多了、算梯度的辦法換成了下一課的反向傳播。

走一走:沒那麼順利

用學習率 0.00005 走 2 萬步:

== 3. 学习率 0.00005,走 20000 步
  第     1 步:w= 1.4804 b=  0.014 损失     244.38
  第    10 步:w= 1.3935 b=  0.014 损失     164.22
  第   100 步:w= 1.3934 b=  0.027 损失     164.18
  第  1000 步:w= 1.3922 b=  0.155 损失     163.81
  第  5000 步:w= 1.3871 b=  0.711 损失     162.27
  第 20000 步:w= 1.3695 b=  2.615 损失     157.43

第一步,損失就從幾千降到了 244;10 步之後降到 164。然後就幾乎不動了:走了 2 萬步,損失才降到 157,而上一課網格搜尋找到的最低損失是 143。b 更是慢得出奇,2 萬步只從 0 挪到了 2.6,離最好的值(約 14.5)還很遠。

為什麼會這樣?下面兩個實驗會揭開原因。

學習率:太小太慢,太大就炸

換幾個學習率,都走 1000 步:

== 4. 不同的学习率(都走 1000 步)
  学习率 1e-05  :w=1.3932 b=0.041 损失 164.14
  学习率 5e-05  :w=1.3922 b=0.155 损失 163.81
  学习率 9e-05  :w=1.3912 b=0.268 损失 163.50
  第 115 步:w 已经变成 1.03e+06,发散了
  学习率 0.0001 :发散

學習率越大,走得越快,b 挪得越遠。可學習率從 0.00009 提高到 0.0001,只差了一點點,就徹底炸了:第 115 步,w 變成了一百多萬。

發散是這樣發生的:w 方向的坡太陡(上面量到斜率是 -8362),步子稍微大一點,一步就跨過了谷底,落到對面更高的地方;那裡的坡更陡,下一步跨得更遠……每一步都比上一步離谷底更遠,最後越來越大,直到數值溢位。

問題的根源在於:w 方向的坡太陡,b 方向的坡太緩dw 是 -8362,db 只有 -80,差了一百倍。學習率要小到不讓 w 發散,b 就只能挪得極慢。一個學習率沒法同時照顧兩個方向。

標準化:讓每個方向的坡差不多陡

w 方向的坡為什麼這麼陡?因為面積的數值很大,幾十到一百多平方米。梯度公式裡 dw = 平均(2 × 误差 × x)x 越大,dw 就越大。

辦法是把面積換算成"均值為 0、標準差為 1"的數,這叫標準化

mean, std = area.mean(), area.std()
area_n = (area - mean) / std

標準化之後,面積的數值都在 -2 到 2 左右,wb 兩個方向的坡差不多陡了。這時學習率可以放心地用 0.1,只走 100 步:

== 5. 面积标准化之后(均值 98.0,标准差 32.1),学习率 0.1 只走 100 步
  第     1 步:w= 8.0605 b= 27.588 损失   13360.27
  第    10 步:w=35.9749 b=123.129 损失     381.15
  第    50 步:w=40.3018 b=137.939 损失     143.05
  第   100 步:w=40.3023 b=137.941 损失     143.05
  换算回原单位:w=1.2571 b=14.806

50 步就到了最低點,損失 143.05,比上一課網格搜尋的 143.1 還低一點(網格搜尋每隔 0.01 才試一次,最好的點可能落在兩個格子之間)。換算回原來的單位,w = 1.2571, b = 14.806

之前 2 萬步都做不到的事,標準化之後 50 步就完成了。這是訓練神經網路時一個非常實用的經驗:輸入的數值範圍要差不多。以後你會看到的各種"歸一化"(LayerNorm、BatchNorm),出發點都和這裡一樣。

小結一下梯度下降

  • 損失對每個參數的導數合起來叫梯度,它指向損失增加最快的方向。
  • 每一步都往梯度的反方向走一小步,步長由學習率決定。
  • 學習率太小,走得慢;太大,會發散。它是訓練中最需要調的一個數。
  • 輸入的數值範圍差別很大時,訓練會很難調,先做標準化。

我們的直線只有兩個參數,梯度公式可以手推。可一個神經網路有成百上千個參數,一層套一層,手推每個參數的導數是不可能的。下一課講怎麼讓計算機自動算出所有參數的梯度:反向傳播。

練習

  1. 在標準化的資料上,把學習率改成 0.5、1.0、1.1,各走 100 步,看看結果。臨界的學習率大約是多少?
  2. 不做標準化,只用學習率 0.00009,要走多少步,損失才能降到 144 以下?估算一下,再執行驗證。
  3. 數值求導時,h 越小越好嗎?把 h 改成 1e-101e-15,看看量出來的斜率。結果是不是反而變差了?想一想為什麼(提示:計算機裡的小數精度是有限的)。

自測

1. 梯度下降為什麼要往梯度的"反方向"走?

梯度指向損失增加最快的方向。我們的目標是讓損失減小,所以要往相反的方向走。

2. 學習率只從 0.00009 提高到 0.0001,訓練就發散了。發生了什麼?

在 w 方向,損失的坡非常陡。學習率稍微大一點,一步就跨過了最低點,落到對面更高的地方,那裡的坡更陡,下一步跨得更遠。每一步都比上一步離最低點更遠,最後數值越來越大,直到溢位。

3. 為什麼對面積做了標準化之後,訓練快了幾百倍?

沒有標準化時,面積的數值很大,w 方向的梯度比 b 方向大了一百倍。學習率只能遷就陡的 w 方向,設得很小,結果 b 方向走得極慢。標準化後兩個方向的坡差不多陡,可以用大得多的學習率,兩個參數都能很快走到最低點。

提問與討論

這一課沒看懂的地方,在這裡問。看到別人的問題,也歡迎你來回答。

提問 +3 點,回答別人 +6 點。內容經審核後公開。

正在載入討論…