梯度下降
尽管梯度下降(gradient descent)很少直接用于深度学习,但了解它是理解下一节随机梯度下降算法的关键。例如,由于学习率过大,优化问题可能会发散,这种现象早已在梯度下降中出现。同样地,预处理(preconditioning)是梯度下降中的一种常用技术,还被沿用到更高级的算法中。让我们从简单的一维梯度下降开始
1 - 一维梯度下降
%matplotlib inline
import numpy as np
import torch
from d2l import torch as d2l
def f(x): # 目标函数
return x ** 2
def f_grad(x): # 目标函数的梯度(导数)
return 2 * x
接下来,我们使用x = 10作为初始值,并假设 η = 0.2 \eta=0.2 η=0.2。使用梯度下降法迭代x共10次,我们可以得到,x的值最终将接近最优解
def gd(eta,f_grad):
x = 10.0
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
results = gd(0.2,f_grad)
epoch 10,x: 0.060466
对进行x优化的过程可以绘制如下
def show_trace(results,f):
n = max(abs(min(results)),abs(max(results)))
f_line = torch.arange(-n,n,0.01)
d2l.set_figsize()
d2l.plot([f_line, results], [[f(x) for x in f_line], [f(x) for x in results]], 'x', 'f(x)', fmts=['-', '-o'])
show_trace(results,f)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mW4T0Pxz-1663162129056)(https://yingziimage.oss-cn-beijing.aliyuncs.com/img/202209142121652.svg)]
学习率
学习率(learning rate)决定⽬标函数能否收敛到局部最⼩值,以及何时收敛到最⼩值。学习率η可由算法设计者设置。请注意,如果我们使⽤的学习率太⼩,将导致x的更新⾮常缓慢,需要更多的迭代。例如,考虑同⼀优化问题中η = 0.05的进度。如下所⽰,尽管经过了10个步骤,我们仍然离最优解很远
show_trace(gd(0.05, f_grad), f)
epoch 10,x: 3.486784
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jvWKYOtW-1663162129057)(https://yingziimage.oss-cn-beijing.aliyuncs.com/img/202209142121653.svg)]
show_trace(gd(1.1, f_grad), f)
epoch 10,x: 61.917364
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-giSKaps3-1663162129057)(https://yingziimage.oss-cn-beijing.aliyuncs.com/img/202209142121654.svg)]
局部最小值
为了演⽰⾮凸函数的梯度下降,考虑函数f(x) = x · cos(cx),其中c为某常数。这个函数有⽆穷多个局部最⼩值。根据我们选择的学习率,我们最终可能只会得到许多解的⼀个。下⾯的例⼦说明了(不切实际的)⾼学习率如何导致较差的局部最⼩值
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)
epoch 10,x: -1.528166
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Svj3FZpW-1663162129057)(https://yingziimage.oss-cn-beijing.aliyuncs.com/img/202209142121655.svg)]
2 - 多元梯度下降
def train_2d(trainer,steps=20,f_grad=None):
"""用定制的训练机优化2D目标函数"""
# 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): #@save
"""显⽰优化过程中2D变量的轨迹"""
d2l.set_figsize()
d2l.plt.plot(*zip(*results), '-o', color='#ff7f0e')
x1, x2 = torch.meshgrid(torch.arange(-5.5, 1.0, 0.1),torch.arange(-3.0, 1.0, 0.1))
d2l.plt.contour(x1, x2, f(x1, x2), colors='#1f77b4')
d2l.plt.xlabel('x1')
d2l.plt.ylabel('x2')
接下来,我们观察学习率η = 0.1时优化变量x的轨迹。可以看到,经过20步之后,x的值接近其位于[0, 0]的最⼩值。虽然进展相当顺利,但相当缓慢
def f_2d(x1, x2): # ⽬标函数
return x1 ** 2 + 2 * x2 ** 2
def f_2d_grad(x1, x2): # ⽬标函数的梯度
return (2 * x1, 4 * x2)
def gd_2d(x1, x2, s1, s2, f_grad):
g1, g2 = f_grad(x1, x2)
return (x1 - eta * g1, x2 - eta * g2, 0, 0)
eta = 0.1
show_trace_2d(f_2d, train_2d(gd_2d, f_grad=f_2d_grad))
epoch 20,x1:-0.057646,x2: -0.000073
C:\Users\20919\anaconda3\envs\d2l\lib\site-packages\torch\functional.py:478: UserWarning: torch.meshgrid: in an upcoming release, it will be required to pass the indexing argument. (Triggered internally at C:\actions-runner\_work\pytorch\pytorch\builder\windows\pytorch\aten\src\ATen\native\TensorShape.cpp:2895.)
return _VF.meshgrid(tensors, **kwargs) # type: ignore[attr-defined]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-TXfOFbE1-1663162129058)(https://yingziimage.oss-cn-beijing.aliyuncs.com/img/202209142121656.svg)]
3 - 自适应方法
正如我们在 11.3.1节中所看到的,选择“恰到好处”的学习率η是很棘⼿的。如果我们把它选得太⼩,就没有什么进展;如果太⼤,得到的解就会振荡,甚⾄可能发散。如果我们可以⾃动确定η,或者完全不必选择学习率,会怎么样?除了考虑⽬标函数的值和梯度、还考虑它的曲率的⼆阶⽅法可以帮我们解决这个问题。虽然由于计算代价的原因,这些⽅法不能直接应⽤于深度学习,但它们为如何设计⾼级优化算法提供了有⽤的思维直觉,这些算法可以模拟下⾯概述的算法的许多理想特性
牛顿法
c = torch.tensor(0.5)
def f(x): # 目标函数
return torch.cosh(c * x)
def f_grad(x): # 目标函数的梯度
return c * torch.sinh(c * x)
def f_hess(x): # 目标函数的Hessian
return c ** 2 * torch.cosh(c * x)
def newton(eta=1):
x = 10.0
results = [x]
for i in range(10):
x -= eta * f_grad(x) / f_hess(x)
results.append(float(x))
print('epoch 10, x:', x)
return results
show_trace(newton(),f)
epoch 10, x: tensor(0.)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xYOQ98s3-1663162129058)(https://yingziimage.oss-cn-beijing.aliyuncs.com/img/202209142121657.svg)]
现在让我们考虑⼀个⾮凸函数,⽐如f(x) = x cos(cx),c为某些常数。请注意在⽜顿法中,我们最终将除以Hessian。这意味着如果⼆阶导数是负的,f的值可能会趋于增加。这是这个算法的致命缺陷!让我们看看实践中会发⽣什么
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)
def f_hess(x): # ⽬标函数的Hessian
return - 2 * c * torch.sin(c * x) - x * c**2 * torch.cos(c * x)
show_trace(newton(), f)
epoch 10, x: tensor(26.8341)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mTTOa5ah-1663162129059)(https://yingziimage.oss-cn-beijing.aliyuncs.com/img/202209142121658.svg)]
这发⽣了惊⼈的错误。我们怎样才能修正它?⼀种⽅法是⽤取Hessian的绝对值来修正,另⼀个策略是重新引⼊学习率。这似乎违背了初衷,但不完全是——拥有⼆阶信息可以使我们在曲率较⼤时保持谨慎,⽽在⽬标函数较平坦时则采⽤较⼤的学习率。让我们看看在学习率稍⼩的情况下它是如何⽣效的,⽐如η =0.5。如我们所⻅,我们有了⼀个相当⾼效的算法
show_trace(newton(0.5), f)
epoch 10, x: tensor(7.2699)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5j6dqmMl-1663162129059)(https://yingziimage.oss-cn-beijing.aliyuncs.com/img/202209142121659.svg)]
收敛性分析
预处理
梯度下降和线搜索
4 - 小结
- 学习率的⼤⼩很重要:学习率太⼤会使模型发散,学习率太⼩会没有进展
- 梯度下降会可能陷⼊局部极⼩值,⽽得不到全局最⼩值
- 在⾼维模型中,调整学习率是很复杂的
- 预处理有助于调节⽐例
- ⽜顿法在凸问题中⼀旦开始正常⼯作,速度就会快得多
- 对于⾮凸问题,不要不作任何调整就使⽤⽜顿法