参考 动手学深度学习-11.优化算法
AI辅助创作,部分内容因个人时间精力进行了取舍
梯度下降#
利用泰勒展开,\(f(x+\epsilon) = f(x) + \epsilon f'(x)+O(\epsilon ^ 2)\),由于我们的目的是求得另一个\(x_1\),使得\(f(x_1) \lt f(x)\),其中\(x_1=x+\epsilon\)
换言之就是让$\epsilon f'(x)<0$
固定步长为\(\eta>0\),取\(\epsilon=-\eta f'(x)\),代入可得\(\epsilon f'(x) = -\eta f'(x)^2 <0\)
1
2
3
4
| %matplotlib inline
import numpy as np
import torch
from d2l import torch as d2l
|
定义目标函数为\(f(x)=x^2\),目标函数导数为\(2x\)
1
2
3
4
| def f(x):
return x ** 2
def f_grad(x):
return 2 * x
|
使用\(x=10\)作为初始值,设\(\eta=0.2\),使用梯度下降法迭代\(10\)次
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
| import matplotlib.pyplot as plt
def gd(eta,f_grad):
x = 10
results = [x]
for i in range(10):
x -= eta * f_grad(x)
results.append(float(x))
print(f'epoch 10,x:{x:f}')
return results
_trace_history = []
def show_trace(results, f, title=None, max_cols=3, reset=False, show=True):
global _trace_history
if reset:
_trace_history = []
_trace_history.append((results, f, title))
if not show:
return
n = len(_trace_history)
cols = min(max_cols, n)
traces = _trace_history[-cols:]
fig, axes = plt.subplots(1, cols, figsize=(4.8 * cols, 3.6))
if cols == 1:
axes = [axes]
for i, (ax, (res, func, t)) in enumerate(zip(axes, traces), start=n - cols + 1):
bound = max(abs(min(res)), abs(max(res)))
f_line = torch.arange(-bound, bound, 0.01)
x_line = f_line.numpy()
y_line = [float(func(x)) for x in f_line]
x_res = [float(x) for x in res]
y_res = [float(func(x)) for x in res]
ax.plot(x_line, y_line, '-')
ax.plot(x_res, y_res, '-o')
ax.set_xlabel('x')
ax.set_ylabel('f(x)')
ax.set_title(t if t is not None else f'第{i}次轨迹')
ax.grid(alpha=0.3)
plt.tight_layout()
plt.show()
results = gd(0.2,f_grad)
show_trace(results, f, title='eta=0.2', reset=True)
|
epoch 10,x:7.295479
学习率决定目标函数能否收敛到最小值,以及决定收敛速度,学习率过低可能收敛过慢
1
| show_trace(gd(0.05,f_grad), f, title='eta=0.05')
|
epoch 10,x:3.486784
学习率过高可能导致高阶项无法忽视导致发散
1
| show_trace(gd(1.2,f_grad), f, title='eta=1.2')
|
epoch 10,x:289.254655
在非凸函数中,梯度下降并不保证找到全局最小值,它通常会收敛到“离初始点最近”的那个局部最小值或者鞍点
1
2
3
4
5
6
7
8
9
| c = torch.tensor(0.15 * np.pi)
def f(x):
return x * torch.cos(c * x)
def f_grad(x):
return torch.cos(c * x) - c * x * torch.sin(c * x)
show_trace(gd(2,f_grad), f, title='non-convex eta=2', reset=True, show=False)
show_trace(gd(0.2,f_grad), f, title='non-convex eta=0.2')
|
epoch 10,x:-1.528166
epoch 10,x:7.295479
多元梯度下降 \(\pmb{x}\leftarrow \pmb{x}-\eta\nabla f(\pmb{x})\)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
| def train_2d(trainer,steps=20,f_grad=None): #兼容后面,GD实际上没有用的s1,s2
x1,x2,s1,s2 = -5,-2,0,0
results = [(x1,x2)]
for i in range(steps):
if f_grad:
x1,x2,s1,s2 = trainer(x1,x2,s1,s2,f_grad)
else: #兼容后面
x1,x2,s1,s2 = trainer(x1,x2,s1,s2)
results.append((x1,x2))
print(f'epoch{i+1}, x1:{float(x1):f}, x2:{float(x2):f}')
return results
def show_trace_2d(f, results, title='2D optimization trace'):
x1_vals, x2_vals = zip(*results)
x1_min, x1_max = min(x1_vals), max(x1_vals)
x2_min, x2_max = min(x2_vals), max(x2_vals)
pad1 = max(1.0, (x1_max - x1_min) * 0.2)
pad2 = max(1.0, (x2_max - x2_min) * 0.2)
x1_grid = np.linspace(x1_min - pad1, x1_max + pad1, 200)
x2_grid = np.linspace(x2_min - pad2, x2_max + pad2, 200)
X1, X2 = np.meshgrid(x1_grid, x2_grid)
Z = f(X1, X2)
plt.figure(figsize=(6, 5))
contours = plt.contour(X1, X2, Z, levels=20, cmap='viridis')
plt.clabel(contours, inline=True, fontsize=8)
plt.plot(x1_vals, x2_vals, '-o', color='tab:red', linewidth=2, markersize=4, label='trajectory')
plt.scatter([x1_vals[0]], [x2_vals[0]], color='tab:blue', s=60, label='start')
plt.scatter([x1_vals[-1]], [x2_vals[-1]], color='tab:green', s=60, label='end')
plt.xlabel('x1')
plt.ylabel('x2')
plt.title(title)
plt.legend()
plt.grid(alpha=0.3)
plt.tight_layout()
plt.show()
|
1
2
3
4
5
6
7
8
9
10
11
12
13
| def f_2d(x1, x2):
return x1**2 + 2 * x2**2
def gd_2d(x1, x2, s1, s2, f_grad):
eta = 0.1
g1, g2 = f_grad(x1, x2)
return x1 - eta * g1, x2 - eta * g2, 0, 0
def f_2d_grad(x1, x2):
return 2 * x1, 4 * x2
res_2d = train_2d(gd_2d, steps=20, f_grad=f_2d_grad)
show_trace_2d(f_2d, res_2d, title='GD on f(x1,x2)=x1^2+2x2^2')
|
epoch20, x1:-0.057646, x2:-0.000073
学习率的选取决定学习的效果,一些自适应方法可能不能直接用于深度学习
随机梯度下降#
普通的梯度下降的损失函数是所有样本的和
$$L(\theta) = \sum_{i=1}^{N} L_i(\theta)$$
每次参数更新都需要重新计算\(N\)个样本梯度值和
$$\theta = \theta - \eta \cdot \nabla \left( \sum_{i=1}^{N} L_i(\theta) \right) = \theta - \eta \cdot \sum_{i=1}^{N} \nabla L_i(\theta)$$
当\(N\)的规模过大时,普通梯度下降的时空复杂度可能难以接受
既然计算所有\(N\)个样本太慢,那每次更新时,只随机抽取一小部分样本(甚至只有一个样本)来计算损失函数和估算梯度,会发生什么?
假设我们在每次迭代 \(t\) 时,从全部数据中随机均匀采样一个大小为 \(|B|\) 的子集 \(B_t\)(其中 \(B_t \subset \{1, ..., N\}\))。我们用这个子集的平均梯度来近似整体梯度:
$$\nabla L(\theta) \approx \frac{1}{|B|} \sum_{i \in B_t} \nabla L_i(\theta)$$
此时,参数更新公式变为:
$$\theta = \theta - \eta \cdot \frac{1}{|B|} \sum_{i \in B_t} \nabla L_i(\theta)$$
特别地,当 \(|B|=1\) 时,即每次只随机选一个样本 \(i\),这就是最原始的随机梯度下降(SGD):
$$\theta = \theta - \eta \cdot \nabla L_i(\theta)$$
那用部分估计整体靠谱吗?
$$E\left[ \frac{1}{|B|} \sum_{i \in B_t} \nabla L_i(\theta) \right] =\frac{1}{|B|} \sum_{i \in B_t}E\left[ \nabla L_i(\theta) \right] = \frac{1}{N} \sum_{i=1}^{N} E\left[ \nabla L_i(\theta) \right]= \frac{1}{N} \sum_{i=1}^{N} \nabla L_i(\theta) = \nabla L(\theta)$$
所以说随机梯度是对完整梯度的无偏估计
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| def sgd(x1,x2,s1,s2,f_grad):
g1,g2 = f_grad(x1,x2)
# 增加随机噪声,实际中只用部分样本计算的梯度自带噪声
g1 += torch.normal(0.0,1,(1,))
g2 += torch.normal(0.0,1,(1,))
eta_t = eta *lr()
return (x1-eta_t*g1,x2-eta_t*g2,0,0)
def constant_lr():
return 1
eta = 0.1
lr = constant_lr
res_2d = train_2d(sgd, steps=50, f_grad=f_2d_grad)
show_trace_2d(f_2d, res_2d, title='GD on f(x1,x2)=x1^2+2x2^2')
|
epoch50, x1:-0.401927, x2:-0.046643
尽管迭代了\(50\)次,效果看起来不如普通的梯度下降,那么剩下能改变的就是学习率了,固定的学习率无法兼具“启动快(此处不是鸡煲笑话),收敛稳”的效果。
为了解决这个矛盾,我们将学习率从常数 \(\eta\) 变为关于迭代步数 \(t\) 的函数 \(\eta_t\)。
SGD 的更新公式演变为:
$$\theta_{t+1} = \theta_t - \eta_t \cdot \nabla L_i(\theta_t)$$
在实际应用中,我们通常使用以下几种调度方式:
| 策略 | 公式示例 | 特点 |
|---|
| 指数衰减 | \(\eta_t = \eta_0 \cdot \gamma^t\) | 学习率随步数指数级下降,衰减速度快 |
| 分段常数 | 每 30 轮 \(\eta\) 除以 10 | 实现简单,便于手动控制关键阶段的精度 |
| 多项式衰减 | \(\eta_t = \eta_0 \cdot (1 - \frac{t}{T})^k\) | 随训练进度平滑下降,常用于深度学习 |
1
2
3
4
5
6
7
8
9
10
| import math
def exponential_lr():
global t
t += 1
return math.exp(-0.1 * t)
t = 1
lr = exponential_lr
res_2d = train_2d(sgd, steps=50, f_grad=f_2d_grad)
show_trace_2d(f_2d, res_2d, title='GD on f(x1,x2)=x1^2+2x2^2')
|
epoch50, x1:-0.788887, x2:0.064546
1
2
3
4
5
6
7
8
9
| def polynomial_lr():
global t
t += 1
return (1 + 0.1 * t) ** (-0.5)
t = 1
lr = polynomial_lr
res_2d = train_2d(sgd, steps=50, f_grad=f_2d_grad)
show_trace_2d(f_2d, res_2d, title='GD on f(x1,x2)=x1^2+2x2^2')
|
epoch50, x1:0.108062, x2:0.041646
随机梯度下降只用一个样本进行更新,这也带来了劣势。首先是向量运算无法充分利用 GPU 的矩阵并行计算能力,其次是单样本梯度估计的方差较大,导致收敛路径震荡剧烈。为解决这两个问题,实际使用时通常用小批量随机梯度下降
单个样本的方差是
$$\text{Var}(\hat{g}) = \text{Var}(g_i) = \sigma^2$$
估计量是 \(m\) 个样本梯度的平均值
$$\hat{g}_{\text{m}} = \frac{1}{m} \sum_{j=1}^{m} g_{i_j}$$
要计算这个平均值的方差,需要利用方差的两个性质,假设样本独立同分布:
- \(\text{Var}(cX) = c^2 \text{Var}(X)\)
- 若 \(X, Y\) 独立,\(\text{Var}(X+Y) = \text{Var}(X) + \text{Var}(Y)\)
推导过程如下:
$$
\begin{aligned}
\text{Var}(\hat{g}_{\text{m}}) &= \text{Var}\left( \frac{1}{m} \sum_{j=1}^{m} g_{i_j} \right) \\
&= \frac{1}{m^2} \text{Var}\left( \sum_{j=1}^{m} g_{i_j} \right) \quad \leftarrow \text{性质 1 } \\
&= \frac{1}{m^2} \sum_{j=1}^{m} \text{Var}(g_{i_j}) \quad \leftarrow \text{性质 2} \\
&= \frac{1}{m^2} \cdot m \cdot \sigma^2 \quad \leftarrow \text{共有 } m \text{ 个项,每项方差为 } \sigma^2 \\
&= \frac{\sigma^2}{m}
\end{aligned}
$$
所以说,当\(m\)增大时,梯度方差是降低的
动量法#
AdaGrad算法#
RMSProp算法#
Adadelta算法#
Adam算法#