梯度下降原理

直覺理解

想像你站在一座山的某個點,蒙著眼睛要走到山谷最低點。你會怎麼做?

  1. 用腳感受哪個方向是向下坡
  2. 往那個方向跨一步
  3. 重複直到感覺不到下坡

這就是梯度下降!

  • 梯度 = 最陡的上坡方向
  • 負梯度 = 最陡的下坡方向(我們要走的方向)
  • 學習率 = 每一步的大小

\begin{aligned} w_{t+1} = w_t - \eta \cdot \nabla L(w_t) \end{aligned}

其中 $\eta$ 是學習率,$\nabla L(w_t)$ 是損失函數在 $w_t$ 處的梯度。

從零實作

import numpy as np

def gradient_descent(gradient_func, initial_w, lr=0.1, n_steps=100):
    """
    gradient_func: 梯度函數
    initial_w: 初始參數
    lr: 學習率
    """
    w = initial_w
    history = [w]
    
    for i in range(n_steps):
        grad = gradient_func(w)
        w = w - lr * grad
        history.append(w.copy())
    
    return w, history

Vibe Prompt

🔥 詠唱:「請幫我視覺化梯度下降在 f(x,y)=x²+2y² 上的收斂過程,用等高線圖畫出參數軌跡。」


深入理解:梯度計算與收斂分析

梯度計算方式

梯度下降有幾種梯度計算方式,各有取捨:

| 方式 | 計算公式 | 特點 | |------|---------|------| | 數值微分 | $\frac{df}{dx} \approx \frac{f(x+h)-f(x-h)}{2h}$ | 簡單但慢,適合驗證 | | 解析微分 | 手推公式後直接計算 | 精準快速,需手動推導 | | 自動微分 | 計算圖鏈式法則 | 框架主流,精準且通用 |

本章使用解析微分,因為我們的目標函數 $f(x,y) = x^2 + 2y^2$ 的梯度很容易推導:

$$\nabla f = \begin{bmatrix} \frac{\partial f}{\partial x} \ \frac{\partial f}{\partial y} \end{bmatrix} = \begin{bmatrix} 2x \ 4y \end{bmatrix}$$

學習率的影響

學習率 $"eta$ 是梯度下降最重要的超參數:

  • 太小 ($"eta = 0.001$):收斂極慢,需要大量迭代
  • 適中 ($"eta = 0.1$):平穩收斂,典型選擇
  • 太大 ($"eta = 1.0$):可能震盪甚至發散

收斂判斷

實務上何時停止迭代?

  1. 梯度接近零:$|\nabla L(w)| < "epsilon$(精準但昂貴)
  2. 損失不再下降:$|L_{t+1} - L_t| < "epsilon$(最實用)
  3. 固定次數:達到最大迭代次數(簡單但可能不夠)

實戰:二維視覺化

import numpy as np
import matplotlib.pyplot as plt

# 定義函數與梯度
def f(x, y):
    return x**2 + 2*y**2

def grad_f(x, y):
    return np.array([2*x, 4*y])

# 執行梯度下降
def gd_2d(start, lr=0.1, steps=50):
    path = [start]
    w = np.array(start)
    for _ in range(steps):
        w = w - lr * grad_f(*w)
        path.append(w.copy())
    return np.array(path)

# 比較不同學習率
starts = [(3, 4)]
lrs = [0.01, 0.1, 0.5]

# 畫出等高線與軌跡
x_range = np.linspace(-4, 4, 100)
y_range = np.linspace(-4, 4, 100)
X, Y = np.meshgrid(x_range, y_range)
Z = f(X, Y)

plt.figure(figsize=(12, 4))
for i, lr in enumerate(lrs):
    plt.subplot(1, 3, i+1)
    plt.contour(X, Y, Z, levels=20, cmap='viridis')
    path = gd_2d((3, 4), lr=lr, steps=50)
    plt.plot(path[:, 0], path[:, 1], 'r.-', markersize=5)
    plt.plot(path[0, 0], path[0, 1], 'go', markersize=8, label='Start')
    plt.plot(path[-1, 0], path[-1, 1], 'r*', markersize=12, label='End')
    plt.title(f'Learning Rate = {lr}')
    plt.xlabel('x'), plt.ylabel('y')
    plt.legend()
plt.tight_layout()
plt.show()

print(f"lr=0.01 終點: {path[-1]}, 50 步後仍未收斂")
print(f"lr=0.1  終點: 接近 (0,0),平穩收斂")
print(f"lr=0.5  終點: 可能震盪,在狹長山谷中尤其明顯")

梯度下降的三大變體

| 變體 | 每次使用的資料量 | 優點 | 缺點 | |------|:---------------:|------|------| | Batch GD | 全部資料 | 方向穩定,保證收斂 | 大資料集超慢 | | SGD | 1 筆資料 | 快,能逃脫局部極小 | 方向雜訊大 | | Mini-Batch | 32-256 筆 | 平衡速度與穩定 | 需調整 batch size |

下一章將深入 Momentum 與 Adam,解決標準 GD 的振盪問題!


關鍵要點

  • ✅ 梯度下降 = 沿負梯度方向迭代更新參數
  • ✅ 學習率決定收斂速度與穩定性,是最重要的超參數
  • ✅ $f(x) = x^2$ 類的凸函數保證收斂到全局最小值
  • ✅ 狹長山谷(條件數大)會導致振盪,需要 Momentum 解決
  • ✅ 數值微分適合驗證,解析微分適合實作,自動微分最通用


學習率策略:讓訓練更有效率

固定學習率不是好做法。實務上會動態調整學習率來加速收斂。

常見的學習率排程

| 策略 | 做法 | 適合場景 | |:----|:----|:--------| | Step Decay | 每 N 個 epoch 減半 | CV 訓練 | | Exponential Decay | η = η₀ × e^(-k×t) | 平滑衰減 | | Cosine Annealing | 餘弦週期性調整 | 搭配重啟(Restart) | | Reduce on Plateau | loss 不降時才減 | 不知道該設多少時 |

實際建議

  1. 先從 0.01 開始,觀察 loss 曲線
  2. 如果 loss 震盪 → 降學習率(0.001)
  3. 如果 loss 下降太慢 → 升學習率(0.1)
  4. 使用 Cosine Annealing + Warm Restart 通常是安全牌

下一章預告:Momentum 與 Adam

學習率策略解決了「每一步走多大」。下一章的 Momentum 和 Adam 解決了「往哪個方向走」——透過慣性和自適應學習率加速收斂。

會員專屬免費教學

本章節為註冊會員專屬的免費開放內容!請先登入或註冊會員,即可立即解鎖閱讀。

立即登入 / 註冊