梯度下降原理
直覺理解
想像你站在一座山的某個點,蒙著眼睛要走到山谷最低點。你會怎麼做?
- 用腳感受哪個方向是向下坡
- 往那個方向跨一步
- 重複直到感覺不到下坡
這就是梯度下降!
- 梯度 = 最陡的上坡方向
- 負梯度 = 最陡的下坡方向(我們要走的方向)
- 學習率 = 每一步的大小
\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$):可能震盪甚至發散
收斂判斷
實務上何時停止迭代?
- 梯度接近零:$|\nabla L(w)| < "epsilon$(精準但昂貴)
- 損失不再下降:$|L_{t+1} - L_t| < "epsilon$(最實用)
- 固定次數:達到最大迭代次數(簡單但可能不夠)
實戰:二維視覺化
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 不降時才減 | 不知道該設多少時 |
實際建議
- 先從 0.01 開始,觀察 loss 曲線
- 如果 loss 震盪 → 降學習率(0.001)
- 如果 loss 下降太慢 → 升學習率(0.1)
- 使用 Cosine Annealing + Warm Restart 通常是安全牌
下一章預告:Momentum 與 Adam
學習率策略解決了「每一步走多大」。下一章的 Momentum 和 Adam 解決了「往哪個方向走」——透過慣性和自適應學習率加速收斂。