PyTorch/[PyTorch 学习笔记] 4.3 优化器
本章代码:
这篇文章主要介绍了 PyTorch 中的优化器,包括 3 个部分:优化器的概念、optimizer 的属性、optimizer 的方法。
优化器的概念
PyTorch 中的优化器是用于管理并更新模型中可学习参数的值,使得模型输出更加接近真实标签。
optimizer 的属性
PyTorch 中提供了 Optimizer 类,定义如下:
1 | class Optimizer(object): |
主要有 3 个属性
- defaults:优化器的超参数,如 weight_decay,momentum
- state:参数的缓存,如 momentum 中需要用到前几次的梯度,就缓存在这个变量中
- param_groups:管理的参数组,是一个 list,其中每个元素是字典,包括 momentum、lr、weight_decay、params 等。
- _step_count:记录更新 次数,在学习率调整中使用
optimizer 的方法
zero_grad():清空所管理参数的梯度。由于 PyTorch 的特性是张量的梯度不自动清零,因此每次反向传播之后都需要清空梯度。代码如下:
1
2
3
4
5
6
7def zero_grad(self):
r"""Clears the gradients of all optimized :class:`torch.Tensor` s."""
for group in self.param_groups:
for p in group['params']:
if p.grad is not None:
p.grad.detach_()
p.grad.zero_()step():执行一步梯度更新
add_param_group():添加参数组,主要代码如下:
1
2
3
4
5
6def add_param_group(self, param_group):
params = param_group['params']
if isinstance(params, torch.Tensor):
param_group['params'] = [params]
...
self.param_groups.append(param_group)state_dict():获取优化器当前状态信息字典
load_state_dict():加载状态信息字典,包括 state 、momentum_buffer 和 param_groups。主要用于模型的断点续训练。我们可以在每隔 50 个 epoch 就保存模型的 state_dict 到硬盘,在意外终止训练时,可以继续加载上次保存的状态,继续训练。代码如下:
1
2
3
4
5
6
7def state_dict(self):
r"""Returns the state of the optimizer as a :class:`dict`.
...
return {
'state': packed_state,
'param_groups': param_groups,
}
下面是代码示例:
step()
张量 weight 的形状为\(2 \times 2\),并设置梯度为 1,把 weight 传进优化器,学习率设置为 1,执行optimizer.step()
更新梯度,也就是所有的张量都减去 1。
1 | weight = torch.randn((2, 2), requires_grad=True) |
输出为:
1 | weight before step:tensor([[0.6614, 0.2669], |
zero_grad()
代码如下:
1 | print("weight before step:{}".format(weight.data)) |
输出为:
1 | weight before step:tensor([[0.6614, 0.2669], |
可以看到优化器的 param_groups 中存储的参数和 weight 的内存地址是一样的,所以优化器中保存的是参数的地址,而不是把参数复制到优化器中。
add_param_group()
向优化器中添加一组参数,代码如下:
1 | print("optimizer.param_groups is\n{}".format(optimizer.param_groups)) |
输出如下:
1 | optimizer.param_groups is |
state_dict()
首先进行 10 次反向传播更新,然后对比 state_dict 的变化。可以使用torch.save()
把 state_dict 保存到 pkl 文件中。
1 | optimizer = optim.SGD([weight], lr=0.1, momentum=0.9) |
输出为:
1 | state_dict before step: |
经过反向传播后,state_dict 中的字典保存了1976501036448
作为 key,这个 key 就是参数的内存地址。
load_state_dict()
上面保存了 state_dict 之后,可以先使用torch.load()
把加载到内存中,然后再使用load_state_dict()
加载到模型中,继续训练。代码如下:
1 | optimizer = optim.SGD([weight], lr=0.1, momentum=0.9) |
输出如下:
1 | state_dict before load state: |
学习率
学习率是影响损失函数收敛的重要因素,控制了梯度下降更新的步伐。下面构造一个损失函数\(y=(2x)^{2}\),\(x\)的初始值为 2,学习率设置为 1。
1 | iter_rec, loss_rec, x_rec = list(), list(), list() |
结果如下:
损失函数没有减少,而是增大,这时因为学习率太大,无法收敛,把学习率设置为 0.01 后,结果如下;
从上面可以看出,适当的学习率可以加快模型的收敛。
下面的代码是试验 10 个不同的学习率 ,[0.01, 0.5] 之间线性选择 10 个学习率,并比较损失函数的收敛情况
1 | iteration = 100 |
结果如下:
上面的结果表示在学习率较大时,损失函数越来越大,模型不能收敛。把学习率区间改为 [0.01, 0.2] 之后,结果如下:
这个损失函数在学习率为 0.125 时最快收敛,学习率为 0.01 收敛最慢。但是不同模型的最佳学习率不一样,无法事先知道,一般把学习率设置为比较小的数就可以了。
momentum 动量
momentum 动量的更新方法,不仅考虑当前的梯度,还会结合前面的梯度。
momentum 来源于指数加权平均:\(\mathrm{v}_{t}=\boldsymbol{\beta} * \boldsymbol{v}_{t-1}+(\mathbf{1}-\boldsymbol{\beta}) * \boldsymbol{\theta}_{t}\),其中\(v_{t-1}\)是上一个时刻的指数加权平均,\(\theta_{t}\)表示当前时刻的值,\(\beta\)是系数,一般小于 1。指数加权平均常用于时间序列求平均值。假设现在求得是 100 个时刻的指数加权平均,那么
\(\mathrm{v}_{100}=\boldsymbol{\beta} * \boldsymbol{v}_{99}+(\mathbf{1}-\boldsymbol{\beta}) * \boldsymbol{\theta}_{100}\) \(=(\mathbf{1}-\boldsymbol{\beta}) * \boldsymbol{\theta}_{100}+\boldsymbol{\beta} *\left(\boldsymbol{\beta} * \boldsymbol{v}_{98}+(\mathbf{1}-\boldsymbol{\beta}) * \boldsymbol{\theta}_{99}\right)\) \(=(\mathbf{1}-\boldsymbol{\beta}) * \boldsymbol{\theta}_{100}+(\mathbf{1}-\boldsymbol{\beta}) * \boldsymbol{\beta} * \boldsymbol{\theta}_{99}+\left(\boldsymbol{\beta}^{2} * \boldsymbol{v}_{98} \right)\)
\(=\sum_{i}^{N}(\mathbf{1}-\boldsymbol{\beta}) * \boldsymbol{\beta}^{i} * \boldsymbol{\theta}_{N-i}\)
从上式可以看到,由于\(\beta\)小于1,越前面时刻的\(\theta\),\(\beta\)的次方就越大,系数就越小。
\(\beta\) 可以理解为记忆周期,\(\beta\)越小,记忆周期越短,\(\beta\)越大,记忆周期越长。通常\(\beta\)设置为 0.9,那么 \(\frac{1}{1-\beta}=\frac{1}{1-0.9}=10\),表示更关注最近 10 天的数据。
下面代码展示了\(\beta=0.9\)的情况
1 | weights = exp_w_func(beta, time_list) |
结果为:
下面代码展示了不同的\(\beta\)取值情况
1 | beta_list = [0.98, 0.95, 0.9, 0.8] |
结果为:
\(\beta\)的值越大,记忆周期越长,就会更多考虑前面时刻的数值,因此越平缓。
在 PyTroch 中,momentum 的更新公式是:
\(v_{i}=m * v_{i-1}+g\left(w_{i}\right)\) \(w_{i+1}=w_{i}-l r * v_{i}\)
其中\(w_{i+1}\)表示第\(i+1\)次更新的参数,lr 表示学习率,\(v_{i}\)表示更新量,\(m\)表示 momentum 系数,\(g(w_{i})\)表示\(w_{i}\)的梯度。展开表示如下:
\(\begin{aligned} \boldsymbol{v}_{100} &=\boldsymbol{m} * \boldsymbol{v}_{99}+\boldsymbol{g}\left(\boldsymbol{w}_{100}\right) \\ &=\boldsymbol{g}\left(\boldsymbol{w}_{100}\right)+\boldsymbol{m} *\left(\boldsymbol{m} * \boldsymbol{v}_{98}+\boldsymbol{g}\left(\boldsymbol{w}_{99}\right)\right) \\ &=\boldsymbol{g}\left(\boldsymbol{w}_{100}\right)+\boldsymbol{m} * \boldsymbol{g}\left(\boldsymbol{w}_{99}\right)+\boldsymbol{m}^{2} * \boldsymbol{v}_{98} \\ &=\boldsymbol{g}\left(\boldsymbol{w}_{100}\right)+\boldsymbol{m} * \boldsymbol{g}\left(\boldsymbol{w}_{99}\right)+\boldsymbol{m}^{2} * \boldsymbol{g}\left(\boldsymbol{w}_{98}\right)+\boldsymbol{m}^{3} * \boldsymbol{v}_{97} \end{aligned}\)
下面的代码是构造一个损失函数\(y=(2x)^{2}\),\(x\)的初始值为 2,记录每一次梯度下降并画图,学习率使用 0.01 和 0.03,不适用 momentum。
1 | def func(x): |
结果为:
可以看到学习率为 0.3 时收敛更快。然后我们把学习率为 0.1 时,设置 momentum 为 0.9,结果如下:
虽然设置了 momentum,但是震荡收敛,这是由于 momentum 的值太大,每一次都考虑上一次的比例太多,可以把 momentum 设置为 0.63 后,结果如下:
可以看到设置适当的 momentum 后,学习率 0.1 的情况下收敛更快了。
下面介绍 PyTroch 所提供的 10 种优化器。
PyTroch 提供的 10 种优化器
optim.SGD
1 | optim.SGD(params, lr=<required parameter>, momentum=0, dampening=0, weight_decay=0, nesterov=False |
随机梯度下降法
主要参数:
- params:管理的参数组
- lr:初始学习率
- momentum:动量系数\(\beta\)
- weight_decay:L2 正则化系数
- nesterov:是否采用 NAG
optim.Adagrad
自适应学习率梯度下降法
optim.RMSprop
Adagrad 的改进
optim.Adadelta
optim.Adam
RMSProp 集合 Momentum,这个是目前最常用的优化器,因为它可以使用较大的初始学习率。
optim.Adamax
Adam 增加学习率上限
optim.SparseAdam
稀疏版的 Adam
optim.ASGD
随机平均梯度下降
optim.Rprop
弹性反向传播,这种优化器通常是在所有样本都一起训练,也就是 batchsize 为全部样本时使用。
optim.LBFGS
BFGS 在内存上的改进
参考资料
如果你觉得这篇文章对你有帮助,不妨点个赞,让我有更多动力写出好文章。
我的文章会首发在公众号上,欢迎扫码关注我的公众号张贤同学。