《动手学深度学习》学习笔记(七)

本文介绍了优化算法在深度学习中的应用,包括梯度下降、随机梯度下降、小批量随机梯度下降、动量法、AdaGrad、RMSProp、AdaDelta和Adam算法的原理、实现和特点。通过比较和总结,阐述了这些方法如何解决目标函数复杂性带来的挑战,以及如何适应不同维度梯度的动态调整.

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

第七章 优化算法

本文最后是梳理。可以直接看梳理。

一、优化与深度学习

在一个深度学习问题中,我们通常会预先定义一个损失函数。然后使用优化算法试图将其最小化。在优化中,这样的损失函数通常被称作优化问题的目标函数。

解析解数值解

深度学习模型的目标函数可能有若干局部最优值。当一个优化问题的数值解在局部最优解附近时,由于目标函数有关解的梯度接近或变成零,最终迭代求得的数值解可能只令目标函数局部最小化而非全局最小化。

由于深度学习模型参数通常都是高维的,目标函数的鞍点通常比局部最小值更常见。

二、梯度下降和随机梯度下降

学习率过大或过小都有问题。一个合适的学习率通常是需要通过多次实验找到的。

当训练数据集的样本较多时,梯度下降每次迭代的计算开销较大,因而随机梯度下降通常更受青睐。

三、小批量随机梯度下降

  • 在每一次迭代中,梯度下降使用整个训练数据集来计算梯度,因此它有时被称为批量梯度下降(batch gradient descent)。
  • 随机梯度下降在每次迭代中只随机采样一个样本来计算梯度。
  • 我们还可以在每轮迭代中随机均匀采样多个样本来组成一个小批量,然后使用这个小批量来计算梯度。小批量随机梯度下降。

小批量随机梯度下降随机均匀采样一个由训练数据样本索引组成的小批量\large B_{t}重复采样(sampling with replacement)或者不重复采样(sampling without replacement)。

\large g_{t} \leftarrow \bigtriangledown f_{B_{t}}(x_{t-1}) = \frac{1}{B}\sum _{i\in B_{t}}\bigtriangledown f_{i}(x_{t-1})

小批量随机梯度下降对自变量的迭代如下:

\large x_{t} \leftarrow x_{t-1} - \eta _{t}g_{t}

其中\large \eta _{t}为学习率,正数。

当批量大小为1时,该算法即为随机梯度下降;当批量大小等于训练数据样本数时,该算法即为梯度下降

  • 当批量较小时,每次迭代中使用的样本少,这会导致并行处理和内存使用效率变低。这使得在计算同样数目样本的情况下比使用更大批量时所花时间更多。
  • 当批量较大时,每个小批量梯度里可能含有更多的冗余信息。为了得到较好的解,批量较大时比批量较小时需要计算的样本数目可能更多,例如增大迭代周期数

1、小批量随机梯度下降从零开始实现:

%matplotlib inline
import numpy as np
import time
import torch
from torch import nn, optim
import sys
sys.path.append("./Dive-into-DL-PyTorch/code/")
import d2lzh_pytorch as d2l


def get_data_ch7():
    data = np.genfromtxt("./Dive-into-DL-PyTorch/data/airfoil_self_noise.dat", delimiter="\t")
    data = (data - data.mean(axis=0)) / data.std(axis=0)
    return torch.tensor(data[:1500, :-1], dtype=torch.float32), torch.tensor(data[:1500, -1], dtype=torch.float32)


features, labels = get_data_ch7()
print(features.shape)
print(labels.shape)
print(labels[:10])
print(features[0])

 我们将在训练函数里对各个小批量样本的损失求平均,因此优化算法里的梯度不需要除以批量大小。

def sgd(params, states, hyperparams):
    for p in params:
        p.data -= hyperparams['lr'] * p.grad.data

 训练函数:初始化一个线性回归模型。

# 本函数已保存在d2lzh_pytorch包中方便以后使用
def train_ch7(optimizer_fn, states, hyperparams, features, labels,
              batch_size=10, num_epochs=2):
    # 初始化模型
    net, loss = d2l.linreg, d2l.squared_loss

    w = torch.nn.Parameter(torch.tensor(np.random.normal(0, 0.01, size=(features.shape[1], 1)), dtype=torch.float32),
                           requires_grad=True)
    b = torch.nn.Parameter(torch.zeros(1, dtype=torch.float32), requires_grad=True)

    def eval_loss():
        return loss(net(features, w, b), labels).mean().item()

    ls = [eval_loss()]
    data_iter = torch.utils.data.DataLoader(
        torch.utils.data.TensorDataset(features, labels), batch_size, shuffle=True)

    for _ in range(num_epochs):
        start = time.time()
        for batch_i, (X, y) in enumerate(data_iter):
            l = loss(net(X, w, b), y).mean()  # 使用平均损失

            # 梯度清零
            if w.grad is not None:
                w.grad.data.zero_()
                b.grad.data.zero_()

            l.backward()
            optimizer_fn([w, b], states, hyperparams)  # 迭代模型参数
            if (batch_i + 1) * batch_size % 100 == 0:
                ls.append(eval_loss())  # 每100个样本记录下当前训练误差
    # 打印结果和作图
    print('loss: %f, %f sec per epoch' % (ls[-1], time.time() - start))
    d2l.set_figsize()
    d2l.plt.plot(np.linspace(0, num_epochs, len(ls)), ls)
    d2l.plt.xlabel('epoch')
    d2l.plt.ylabel('loss')

 当batch_size为1500时,为梯度下降。

def train_sgd(lr, batch_size, num_epochs=2):
    train_ch7(sgd, None, {'lr': lr}, features, labels, batch_size, num_epochs)

train_sgd(1, 1500, 6)

 当batch_size为1时,为随机梯度下降。

train_sgd(0.005, 1)

 当批量大小为10时,优化使用的是小批量随机梯度下降。

train_sgd(0.05, 10)

 2、简洁实现

# 本函数与原书不同的是这里第一个参数优化器函数而不是优化器的名字
# 例如: optimizer_fn=torch.optim.SGD, optimizer_hyperparams={"lr": 0.05}
def train_pytorch_ch7(optimizer_fn, optimizer_hyperparams, features, labels,
                    batch_size=10, num_epochs=2):
    # 初始化模型
    net = nn.Sequential(
        nn.Linear(features.shape[-1], 1)
    )
    loss = nn.MSELoss()
    optimizer = optimizer_fn(net.parameters(), **optimizer_hyperparams)

    def eval_loss():
        return loss(net(features).view(-1), labels).item() / 2

    ls = [eval_loss()]
    data_iter = torch.utils.data.DataLoader(
        torch.utils.data.TensorDataset(features, labels), batch_size, shuffle=True)

    for _ in range(num_epochs):
        start = time.time()
        for batch_i, (X, y) in enumerate(data_iter):
            # 除以2是为了和train_ch7保持一致, 因为squared_loss中除了2
            l = loss(net(X).view(-1), y) / 2 

            optimizer.zero_grad()
            l.backward()
            optimizer.step()
            if (batch_i + 1) * batch_size % 100 == 0:
                ls.append(eval_loss())
    # 打印结果和作图
    print('loss: %f, %f sec per epoch' % (ls[-1], time.time() - start))
    d2l.set_figsize()
    d2l.plt.plot(np.linspace(0, num_epochs, len(ls)), ls)
    d2l.plt.xlabel('epoch')
    d2l.plt.ylabel('loss')
train_pytorch_ch7(optim.SGD, {"lr": 0.05}, features, labels, 10)

 小结:

  • 小批量随机梯度每次随机均匀采样一个小批量的训练样本来计算梯度。
  • 在实际中,(小批量)随机梯度下降的学习率可以在迭代过程中自我衰减
  • 通常,小批量随机梯度在每个迭代周期的耗时介于梯度下降和随机梯度下降的耗时之间

四、动量法

1、梯度下降的问题

目标函数有关自变量的梯度代表了目标函数在自变量当前位置下降最快的方向。因此,梯度下降也叫作最陡下降(steepest descent)。在每次迭代中,梯度下降根据自变量当前位置,沿着当前位置的梯度更新自变量。然而,如果自变量的迭代方向仅仅取决于自变量当前位置,这可能会带来一些问题

考虑一个二维向量输入:\large x = [x_{1}, x_{2}]^{T}。目标函数为\large f(x) = 0.1x_{1}^{2} + 2x_{2}^{2}

%matplotlib inline
import sys
sys.path.append("..") 
import d2lzh_pytorch as d2l
import torch

eta = 0.4 # 学习率

def f_2d(x1, x2):
    return 0.1 * x1 ** 2 + 2 * x2 ** 2

def gd_2d(x1, x2, s1, s2):
    return (x1 - eta * 0.2 * x1, x2 - eta * 4 * x2, 0, 0)

d2l.show_trace_2d(f_2d, d2l.train_2d(gd_2d))

发现问题:同一位置上,目标函数在竖直方向(x2轴方向)比在水平方向(x1轴方向)的斜率的绝对值更大。 因此,给定学习率,梯度下降迭代自变量时会使自变量在竖直方向比在水平方向移动幅度更大。那么,我们需要一个较小的学习率从而避免自变量在竖直方向上越过目标函数最优解。然而,这会造成自变量在水平方向上朝最优解移动变慢。

2、动量法

动量法的提出是为了解决梯度下降的上述问题。

沿用上一节(小批量随机梯度下降)中时间步 t 的小批量随机梯度\large g_{t}的定义

\large g_{t} \leftarrow \bigtriangledown f_{B_{t}}(x_{t-1}) = \frac{1}{B}\sum _{i\in B_{t}}\bigtriangledown f_{i}(x_{t-1})

设时间步 t 的自变量为\large x_{t},学习率为\large \eta _{t}。在时间步0,动量法创建速度变量\large v_{0},并将其元素初始化成0。在时间步\large t> 0,动量法对每次迭代的步骤做如下修改:

\large v_{t} \leftarrow \gamma v_{t-1} + \eta _{t}g_{t}

\large x_{t} \leftarrow x_{t -1} - v_{t}

其中,动量超参数\large \gamma满足\large 0\leq \gamma < 1。当\large \gamma =0 时,动量法等价于小批量随机梯度下降。

在解释动量法的数学原理前,让我们先从实验中观察梯度下降在使用动量法后的迭代轨迹。

def momentum_2d(x1, x2, v1, v2):
    v1 = gamma * v1 + eta * 0.2 * x1
    v2 = gamma * v2 + eta * 4 * x2
    return x1 - v1, x2 - v2, v1, v2

eta, gamma = 0.4, 0.5
d2l.show_trace_2d(f_2d, d2l.train_2d(momentum_2d))

 动量法在竖直方向上的移动更加平滑,且在水平方向上更快逼近最优解。

 3、指数加权移动平均

为了从数学上理解动量法,让我们先解释一下指数加权移动平均(exponentially weighted moving average)。

给定超参数\large 0\leq \gamma < 1,当前时间步 t 的变量\large y_{t}是上一时间步t -1 的变量\large y_{t-1}和当前时间步另一变量\large x_{t}的线性组合:

\large y_{t} = \gamma y_{t-1} + (1-\gamma )x_{t}

因此,实际上,我们常常将\large y_{t}看做是对最近\large 1/(1-\gamma )个时间步的\large x_{t}值的加权平均。例如,当\large \gamma=0.95时,\large y_{t}可以被看作对最近20个时间步的\large x_{t}值的加权平均;当\large \gamma=0.9时,\large y_{t}可以被看作对最近10个时间步的\large x_{t}值的加权平均。而且,而且,离当前时间步t越近的\large x_{t}值获得的权重越大(越接近1)。

4、由指数加权移动平均理解动量法

现在,我们对动量法的速度变量做变形:

\large v_{t} \leftarrow \gamma v_{t-1} + \eta _{t}g_{t}

\large v_{t} \leftarrow \gamma v_{t-1} +(1-\gamma) (\tfrac{\eta _{t}}{1-\gamma }g_{t})

由指数加权移动平均的形式可得:速度变量\large v_{t}实际上对序列{\large {\eta _{t-i}g_{t-i}/(1-\gamma ): i=0, ..., 1/(1-\gamma)-1}}做了指数加权移动平均

换句话说,相比于小批量随机梯度下降,动量法在每个时间步的自变量更新量近似于将最近\large 1/(1-\gamma )个时间步的普通更新量(即学习率乘以梯度)做了指数加权移动平均后再除以\large 1-\gamma

所以,在动量法中,自变量在各个方向上的移动幅度不仅取决当前梯度还取决于过去的各个梯度在各个方向上是否一致。

在本节之前示例的优化问题中,所有梯度在水平方向上为正(向右),而在竖直方向上时正(向上)时负(向下)。这样,我们就可以使用较大的学习率从而使自变量向最优解更快移动。

5、从零开始实现

 相对于小批量随机梯度下降,动量法需要对每一个自变量维护一个同它一样形状的速度变量,且超参数里多了动量超参数momentum。实现中,我们将速度变量用更广义的状态变量states表示。

features, labels = d2l.get_data_ch7()

def init_momentum_states():
    v_w = torch.zeros((features.shape[1], 1), dtype=torch.float32)
    v_b = torch.zeros(1, dtype=torch.float32)
    return (v_w, v_b)

def sgd_momentum(params, states, hyperparams):
    for p, v in zip(params, states):
        v.data = hyperparams['momentum'] * v.data + hyperparams['lr'] * p.grad.data
        p.data -= v.data
d2l.train_ch7(sgd_momentum, init_momentum_states(),
              {'lr': 0.02, 'momentum': 0.5}, features, labels)

d2l.train_ch7(sgd_momentum, init_momentum_states(),
              {'lr': 0.02, 'momentum': 0.9}, features, labels)

 

可见目标函数值在后期迭代过程中的变化不够平滑。直觉上,10倍小批量梯度比2倍小批量梯度大了5倍,我们可以试着将学习率减小到原来的1/5。此时目标函数值在下降了一段时间后变化更加平滑。

d2l.train_ch7(sgd_momentum, init_momentum_states(),
              {'lr': 0.004, 'momentum': 0.9}, features, labels)

 

 

6、简洁实现

在PyTorch中,只需要通过参数momentum来指定动量超参数即可使用动量法。

d2l.train_pytorch_ch7(torch.optim.SGD, {'lr': 0.004, 'momentum': 0.9},
                    features, labels)

小结: 

  • 动量法使用了指数加权移动平均的思想。它将过去时间步的梯度做了加权平均且权重按时间步指数衰减。
  • 动量法使得相邻时间步的自变量更新在方向上更加一致。

五、AdaGrad算法

在之前介绍过的优化算法中,目标函数自变量的每一个元素在相同时间步都使用同一个学习率来自我迭代。

在7.4节(动量法)里我们看到当\large x_{1}\large x_{2}梯度值有较大差别时,需要选择足够小的学习率使得自变量在梯度值较大的维度上不发散。但这样会导致自变量在梯度值较小的维度上迭代过慢。动量法依赖指数加权移动平均使得自变量的更新方向更加一致从而降低发散的可能。本节我们介绍AdaGrad算法,它根据自变量在每个维度的梯度值的大小来调整各个维度上的学习率,从而避免统一的学习率难以适应所有维度的问题。

1、AdaGrad算法

AdaGrad算法会使用一个小批量随机梯度\large g_{t}按元素平方的累加变量\large s_{t}。在时间步0,AdaGrad将\large s_{0}中每个元素初始化为0。在时间步 t ,首先将小批量随机梯度​\large g_{t}按元素平方后累加到变量\large s_{t}

\large s_{t}\leftarrow s_{t-1} + g_{t}\odot g_{t}

其中\large \odot是按元素相乘。接着,我们将目标函数自变量中每个元素的学习率通过按元素运算重新调整一下:

\large x_{t}\leftarrow x_{t-1} - \frac{\eta }{\sqrt{s_{t}+\varepsilon }}\bigodot g_{t}

其中η是学习率,ϵ是为了维持数值稳定性而添加的常数,如\large 10^{-6}。这里开方、除法和乘法的运算都是按元素运算的。这些按元素运算使得目标函数自变量中每个元素分别拥有自己的学习率

2、AdaGrad的特点

\large x_{t}\leftarrow x_{t-1} - \frac{\eta }{\sqrt{s_{t}+\varepsilon }}\bigodot g_{t}

需要强调的是,小批量随机梯度按元素平方的累加变量\large s_{t}出现在学习率的分母项中。因此,

  • 如果目标函数有关自变量中某个元素的偏导数一直都较大,那么该元素的学习率将下降较快
  • 如果目标函数有关自变量中某个元素的偏导数一直都较小,那么该元素的学习率将下降较慢。

然而,由于累加变量\large s_{t}一直在累加按元素平方的梯度,自变量中每个元素的学习率在迭代过程中一直在降低(或不变)。所以,当学习率在迭代早期降得较快且当前解依然不佳时,AdaGrad算法在迭代后期由于学习率较小,可能较难找到一个有用的解

3、从零开始实现

仍然使用目标函数\large f(x) = 0.1x_{1}^{2} + 2x_{2}^{2}。由于\large s_{t}的累加效果使学习率不断衰减,自变量在迭代后期的移动幅度较小。

%matplotlib inline
import math
import torch
import sys
sys.path.append("..") 
import d2lzh_pytorch as d2l

def adagrad_2d(x1, x2, s1, s2):
    g1, g2, eps = 0.2 * x1, 4 * x2, 1e-6  # 前两项为自变量梯度
    s1 += g1 ** 2
    s2 += g2 ** 2
    x1 -= eta / math.sqrt(s1 + eps) * g1
    x2 -= eta / math.sqrt(s2 + eps) * g2
    return x1, x2, s1, s2

def f_2d(x1, x2):
    return 0.1 * x1 ** 2 + 2 * x2 ** 2

eta = 0.4
d2l.show_trace_2d(f_2d, d2l.train_2d(adagrad_2d))

 学习率增大到2

 4、简洁实现

d2l.train_pytorch_ch7(torch.optim.Adagrad, {'lr': 0.1}, features, labels)

 小结:

  • AdaGrad算法在迭代过程中不断调整学习率,并让目标函数自变量中每个元素都分别拥有自己的学习率

  • 使用AdaGrad算法时,自变量中每个元素的学习率在迭代过程中一直在降低(或不变)

六、RMSProp算法

上一节AdaGrad算法中,我们提到,因为调整学习率时分母上的变量\large \mathbf{s_{t}}一直在累加按元素平方的小批量随机梯度,所以目标函数自变量每个元素的学习率在迭代过程中一直在降低(或不变)。因此,当学习率在迭代早期降得较快当前解依然不佳时,AdaGrad算法在迭代后期由于学习率过小,可能较难找到一个有用的解。简言之:冲的太猛了。

1、算法

RMSProp算法对AdaGrad做了一点小小的修改。

动量法的优势在:指数加权移动平均法的使用。AdaGrad的优势,是梯度平方做累加变量至于分母。

如果结合起来,那么对累加变量的调整就是:

\large s_{t}\leftarrow \gamma s_{t-1} + (1-\gamma )g_{t}\odot g_{t}

\large x_{t}\leftarrow x_{t-1} - \frac{\eta }{\sqrt{s_{t}+\varepsilon }} \odot g_{t}

RMSProp算法和AdaGrad算法的不同在于,RMSProp算法使用了小批量随机梯度按元素平方指数加权移动平均来调整学习率。

%matplotlib inline
import math
import torch
import sys
sys.path.append("..") 
import d2lzh_pytorch as d2l

def rmsprop_2d(x1, x2, s1, s2):
    g1, g2, eps = 0.2 * x1, 4 * x2, 1e-6
    s1 = gamma * s1 + (1 - gamma) * g1 ** 2
    s2 = gamma * s2 + (1 - gamma) * g2 ** 2
    x1 -= eta / math.sqrt(s1 + eps) * g1
    x2 -= eta / math.sqrt(s2 + eps) * g2
    return x1, x2, s1, s2

def f_2d(x1, x2):
    return 0.1 * x1 ** 2 + 2 * x2 ** 2

eta, gamma = 0.4, 0.9
d2l.show_trace_2d(f_2d, d2l.train_2d(rmsprop_2d))

 

 2、从零实现:

features, labels = d2l.get_data_ch7()

def init_rmsprop_states():
    s_w = torch.zeros((features.shape[1], 1), dtype=torch.float32)
    s_b = torch.zeros(1, dtype=torch.float32)
    return (s_w, s_b)

def rmsprop(params, states, hyperparams):
    gamma, eps = hyperparams['gamma'], 1e-6
    for p, s in zip(params, states):
        s.data = gamma * s.data + (1 - gamma) * (p.grad.data)**2
        p.data -= hyperparams['lr'] * p.grad.data / torch.sqrt(s + eps)
d2l.train_ch7(rmsprop, init_rmsprop_states(), {'lr': 0.01, 'gamma': 0.9},
              features, labels)

 

 3、简洁实现:

d2l.train_pytorch_ch7(torch.optim.RMSprop, {'lr': 0.01, 'alpha': 0.9},
                    features, labels)

小结:  

  • RMSProp算法和AdaGrad算法的不同在于,RMSProp算法使用了小批量随机梯度按元素平方指数加权移动平均调整学习率

七、AdaDelta算法

除了RMSProp算法以外,另一个常用优化算法AdaDelta算法也针对AdaGrad算法在迭代后期较难找到有用解的问题做了改进:有趣的是:AdaDelta算法没有学习率这一超参数。

1、算法

同RMSProp类似,使用了小批量随机梯度\large g_{t}按元素平方的指数加权移动平均变量\large s_{t}

\large s_{t}\leftarrow \rho s_{t-1} + (1-\rho )g_{t}\odot g_{t}

与RMSProp不同的是:

AdaDelta算法还维护一个额外的状态变量\large \Delta x_{t}来计算自变量的变化量:

\large {g}'_{t} \leftarrow \sqrt{\frac{\Delta x_{t-1}+\varepsilon }{s_{t}+\varepsilon }}\odot g_{t}

\large \varepsilon是为了维持数据值稳定性而添加的常数,如\large 10^{-5}

接着更新自变量:

\large x_{t}\leftarrow x_{t-1} - {g}'_{t}

最后:我们使用\large \Delta x_{t}来记录自变量变化量\large {g}'_{t}按元素平方的指数加权移动平均:

\large \Delta x_{t} \leftarrow \rho\Delta x_{t-1}+ (1- \rho){g}'_{t}\odot {g}'_{t}

总之:如不考虑\large \varepsilon的影响,AdaDelta算法跟RMSProp算法的不同之处在于使用\large \sqrt{\Delta x_{t-1}}来替代学习率\large \eta

2、从零开始实现:

AdaDelta算法需要对每个自变量维护两个状态变量,\large s_{t}\large \Delta x_{t}。我们按AdaDelta算法中的公式实现该算法。

%matplotlib inline
import torch
import sys
sys.path.append("..") 
import d2lzh_pytorch as d2l

features, labels = d2l.get_data_ch7()

def init_adadelta_states():
    s_w, s_b = torch.zeros((features.shape[1], 1), dtype=torch.float32), torch.zeros(1, dtype=torch.float32)
    delta_w, delta_b = torch.zeros((features.shape[1], 1), dtype=torch.float32), torch.zeros(1, dtype=torch.float32)
    return ((s_w, delta_w), (s_b, delta_b))

def adadelta(params, states, hyperparams):
    rho, eps = hyperparams['rho'], 1e-5
    for p, (s, delta) in zip(params, states):
        s[:] = rho * s + (1 - rho) * (p.grad.data**2)
        g =  p.grad.data * torch.sqrt((delta + eps) / (s + eps))
        p.data -= g
        delta[:] = rho * delta + (1 - rho) * g * g
d2l.train_ch7(adadelta, init_adadelta_states(), {'rho': 0.9}, features, labels)

 3、简洁实现

d2l.train_pytorch_ch7(torch.optim.Adadelta, {'rho': 0.9}, features, labels)

 小结:

  • AdaDelta算法没有学习率超参数,它通过使用有关自变量更新量平方的指数加权移动平均的项来替代RMSProp算法中的学习率。

八、Adam算法

Adam算法在RMSProp算法基础上对小批量随机梯度也做了指数加权移动平均。

所以Adam算法可以看做是RMSProp算法与动量法的结合。

1、Adam算法

Adam算法使用了动量法中的动量变量\LARGE v_{t}和RMSProp算法中的小批量随机梯度按元素平方的指数加权移动平均变量\LARGE s_{t},并在时间步0将它们中每个元素初始化为0。给定超参数\large 0\leq \beta _{1}< 1 (算法作者建议设为0.9),时间步t的动量变量\LARGE v_{t}即小批量随机梯度\large g_{t}的指数加权移动平均:

\large v_{t} \leftarrow \beta _{1}v_{t-1} + (1-\beta_{1} )g_{t}

和RMSProp算法中的一样,给定超参数\large 0\leq \beta _{2}< 1(算法作者建议设为0.999),将小批量随机梯度按元素平方后做指数加权移动平均\LARGE s_{t}

\large s_{t} = \beta _{2}s_{t-1} + (1-\beta _{2})g_{t}\odot g_{t}

\LARGE v_{t}展开:

\large v_{t} = (1-\beta _{1}) \sum_{i=1}^{t}\beta _{1}^{t-i}g_{i}

将过去各时间步小批量随机梯度的权值相加:

\large (1-\beta _{1}) \sum_{i=1}^{t}\beta _{1}^{t-i} = 1-\beta _{1}^{t}

当t较小时,过去各时间步小批量随机梯度权值之和会较小。为了消除这种影响,对任意时间步t ,我们可以将\LARGE v_{t}再除以\large 1-\beta _{1}^{t},从而使过去各时间步小批量随机梯度权值之和为1。也叫作偏差修正。

我们对\LARGE v_{t}\LARGE s_{t}修正如下:

\large \hat{v}_{t} \leftarrow \frac{v_{t}}{1-\beta _{1}^{t}}

\large \hat{s}_{t} \leftarrow \frac{s_{t}}{1-\beta _{2}^{t}}

接下来,Adam算法使用偏差修正后的变量\large \hat{v}_{t}\large \hat{s}_{t},将模型参数中的每一个元素的学习率通过按元素平方运算重新调整:

\large {g}'_{t}\leftarrow \frac{\eta \hat{v}_{t}}{\sqrt{\hat{s}_{t}}+ \epsilon }

\large \eta为学习率,\large \epsilon是为了维持数值稳定性而添加的常数,如\large 10^{-8}。和AdaGrad算法、RMSProp算法以及AdaDelta算法一样,目标函数自变量中每个元素都分别拥有自己的学习率。

最后,使用\large {g}'_{t}迭代自变量:

\large x_{t} \leftarrow x_{t-1}-{g}'_{t}

2、从零开始实现

%matplotlib inline
import torch
import sys
sys.path.append("..") 
import d2lzh_pytorch as d2l

features, labels = d2l.get_data_ch7()

def init_adam_states():
    v_w, v_b = torch.zeros((features.shape[1], 1), dtype=torch.float32), torch.zeros(1, dtype=torch.float32)
    s_w, s_b = torch.zeros((features.shape[1], 1), dtype=torch.float32), torch.zeros(1, dtype=torch.float32)
    return ((v_w, s_w), (v_b, s_b))

def adam(params, states, hyperparams):
    beta1, beta2, eps = 0.9, 0.999, 1e-6
    for p, (v, s) in zip(params, states):
        v[:] = beta1 * v + (1 - beta1) * p.grad.data
        s[:] = beta2 * s + (1 - beta2) * p.grad.data**2
        v_bias_corr = v / (1 - beta1 ** hyperparams['t'])
        s_bias_corr = s / (1 - beta2 ** hyperparams['t'])
        p.data -= hyperparams['lr'] * v_bias_corr / (torch.sqrt(s_bias_corr) + eps)
    hyperparams['t'] += 1
d2l.train_ch7(adam, init_adam_states(), {'lr': 0.01, 't': 1}, features, labels)

 

3、简洁实现

d2l.train_pytorch_ch7(torch.optim.Adam, {'lr': 0.01}, features, labels)

 小结:

  • Adam算法在RMSProp算法的基础上对小批量随机梯度也做了指数加权移动平均

  • Adam算法使用了偏差修正


梳理:

模型通过训练数据来拟合模型参数,使得模型能够根据输入来正确输出。那么如果输出距离我们正确答案有差异。那么衡量差异的函数定义为目标函数(损失函数等)。我们希望目标函数为0。这样就表明我们模型完全正确输出我们的期望。但是因为特征维度的丰富,目标函数的解析解,不可求或者不好求。所以运用计算机强大的运算能力来求数值解,即:只要误差在可接受范围内,我们就认为这个解释我们的目标解。(水至清则无鱼嘛)。纯净物都是纯度百分之九十九点九九九九....。没有绝对之说。

我们求解数值解的方法,我们采用了迭代逼近的方法。一次次迭代x的值,使其逐渐逼近误差函数的最小值。就像求二分逼近法求开平方一样,不断迭代逼近正确值。但是这样速度太慢,为此我们考虑沿着反梯度方向可是实现最速下降。故引入了梯度下降法

但是计算梯度的时候对训练集的计算数量有了考量:所以有了三种梯度下降法

  1. 批量梯度下降:使用整个训练集来计算梯度。
  2. 随机梯度下降:使用随机采样的一个样本来计算梯度。
  3. 小批量随机梯度下降:随机均匀采样几个样本来组成一个小批量。然后对这个小批量计算梯度。

每次更新迭代x的公式:

梯度下降法:\large x\leftarrow x- \eta {f}'(x)

1、批量梯度下降法:\large x\leftarrow x- \eta \bigtriangledown f(x)

2、随机梯度下降法:\large x\leftarrow x- \eta \bigtriangledown f_{i}(x)

3、小批量随机梯度下降法:

\large g_{t} \leftarrow \bigtriangledown f_{B_{t}}(x_{t-1}) = \frac{1}{B}\sum _{i\in B_{t}}\bigtriangledown f_{i}(x_{t-1})

\large x_{t} \leftarrow x_{t-1} - \eta _{t}g_{t}

三者考量:

  • 批量梯度,一次性将所有数据全部计算梯度,运算量大,且总运算时间短。
  • 随机梯度偶然性大,且并行处理和内存使用效率低。总时间长。
  • 小批量随机梯度时间介于两者之间,
    • 当批量大时,每个小批量中可能含有更多的冗余信息,为了得到更好的解,需要增大迭代周期数。
    • 当批量小时,并行处理和内存使用率低。总运算时间长。

结论:目前大多使用小批量随机。


开始讲历史了:

有人觉得仅仅考虑沿梯度下降还不够。梯度下降也叫最陡下降,表示目标函数在自变量当前位置下降最快的方向。在当前位置下降,当前位置,所以,有人觉得你仅仅考虑当前位置不行的。

要考虑整体方向,运动有个惯性嘛,多看看历史,知道大势所趋,肯定不能只看当前位置,所以在自变量更新的时候考虑引入速度变量\large v_{t},有速度就有惯性。惯性越大,越势不可挡。

问:那么我只考虑当前位置有何不妥?

答:如原文中所写:在\large x_{1}维度和\large x_{2}维度,梯度下降两个方向移动幅度不同,所以导致我们没法统一学习率。移动幅度大的维度,我们希望学习率大一些,步幅越大,越迅速;移动幅度小的维度,我们希望学习率变动小一些,以免跳过跳出。对此解决方案:一是不同维度的自变量,不再统一学习率,各自学习各自的维度,就有了AdaGrad。二是每次自变量更新的学习率,通过历史梯度做加权平均,即学习率乘以梯度。也就是文中提到的指数加权移动平均。接下来我们一个个讲。

下文中\large g_{t} \leftarrow \bigtriangledown f_{B_{t}}(x_{t-1}) = \frac{1}{B}\sum _{i\in B_{t}}\bigtriangledown f_{i}(x_{t-1})。都是通过基于小样本随机梯度下降\large g_{t}

4、动量法:

所以有了动量法,不再仅仅考虑当前位置的梯度,自变量在各个方向上的移动幅度,不仅取决于当前梯度,还取决于过去的时间步各个梯度在各个方向(维度)上是否一致。更新原则如下:

\large v_{t} \leftarrow \gamma v_{t-1} + \eta _{t}g_{t}

\large x_{t} \leftarrow x_{t -1} - v_{t}

数学上用的是:指数加权移动平均

\large y_{t} = \gamma y_{t-1} + (1-\gamma )x_{t}

所以速度变量变换如下:

\large v_{t} \leftarrow \gamma v_{t-1} +(1-\gamma) (\tfrac{\eta _{t}}{1-\gamma }g_{t})

这样就能依靠 t 时刻之前的\large 1/(1-\gamma )个梯度来加权指导当前的更新值。我觉得凝练点称为:惯性干预

5、AdaGrad算法:

一问一答中,我们说了,之前每一个维度元素在相同时间步都使用同一学习率。如上图。这样\large \eta一刀切,各维度相互牵制,变换太过发散。

而且我们希望学习率随着距离目标越来越近,调整越来越精确,当然希望学习率逐渐精确。也就说越小。前面粗调,后面微操。

所以希望:每个维度的梯度都不同,每个维度都应有自己的学习率,且逐渐减小。所有就有了AdaGrad算法:

\large s_{t}\leftarrow s_{t-1} + g_{t}\odot g_{t}

\large x_{t}\leftarrow x_{t-1} - \frac{\eta }{\sqrt{s_{t}+\varepsilon }}\bigodot g_{t}

不同与动量法,有个速度惯性变量\large v_{t},它搞了个累加变量\large s_{t},累加的是什么呢?前面不说各个维度梯度变化幅度不同吗?那就把各自的差异累积起来。差异越大,指导意义越大,各自指导各自的。你梯度大,那你更新幅度大,你梯度小,你更新幅度小点。并把累加变量放在了分母项中。这样累加的越多,分母越大,整体越小,更新的幅度也能实现逐渐减小。

6、RMSProp算法

前面两个方法,一个是考虑前面惯性的影响,加入了对前面几步的考虑,采用了指数加权移动平均。一个是将各个维度分开累加梯度至于分母来更新。你肯定在想,组合起来

特别是AdaGrad中累加变量中:

\large s_{t}\leftarrow s_{t-1} + g_{t}\odot g_{t}

如果加入指数加权移动平均就好了。指数加权移动平均的形式为

\large y_{t} = \gamma y_{t-1} + (1-\gamma )x_{t}

所以我们把累加变量变换一下:

\large s_{t}\leftarrow \gamma s_{t-1} + (1-\gamma )g_{t}\odot g_{t}

然后自变量更新规则还是如之前那样:

\large x_{t}\leftarrow x_{t-1} - \frac{\eta }{\sqrt{s_{t}+\varepsilon }} \odot g_{t}

RMSProp算法和AdaGrad算法的不同在于,RMSProp算法使用了小批量随机梯度按元素平方指数加权移动平均来调整学习率。同时RMSProp还解决了AdaGrad算法在迭代后期较难找到有用解的问题。

7、AdaDelta算法

AdaDelta算法就更狠了,学习率不一致,那它就把学习率去掉了。但它多了一个对自变量变化量的考量\large \Delta x_{t}

1)首先还是梯度按元素平方的指数加权移动平均:

\large s_{t}\leftarrow \rho s_{t-1} + (1-\rho )g_{t}\odot g_{t}

2)根据上一步中的自变量变化量\large \Delta x_{t-1},计算自变量的更新值:

\large {g}'_{t} \leftarrow \sqrt{\frac{\Delta x_{t-1}+\varepsilon }{s_{t}+\varepsilon }}\odot g_{t}

3)更新自变量:

\large x_{t}\leftarrow x_{t-1} - {g}'_{t}

4)\large \Delta x_{t}记录自变量变化量\large {g}'_{t}按元素平方的指数加权移动平均,供下一个时刻更新使用。

\large \Delta x_{t} \leftarrow \rho\Delta x_{t-1}+ (1- \rho){g}'_{t}\odot {g}'_{t}

AdaDelta算法没有学习率超参数,它通过使用有关自变量更新量平方的指数加权移动平均的项来替代RMSProp算法中的学习率

8、Adam算法

我们采用指数加权移动平均时,可以将其迭代展开,将过去各时间步小批量随机梯度的权值相加,可以得到一个固定数。当t较小时,过去各时间步小批量随机梯度权值之和会较小。为了消除这样的影响,对于任意时间步t,我们可以将速度变量\large v_{t}和累加变量\large s_{t}除以这个权值之和,从而使过去各时间步小批量随机梯度权值之和为1。相当于做了一个归一化。这也叫作偏差修正

比如将\LARGE v_{t}展开:

\large v_{t} = (1-\beta _{1}) \sum_{i=1}^{t}\beta _{1}^{t-i}g_{i}

将过去各时间步小批量随机梯度的权值相加:

\large (1-\beta _{1}) \sum_{i=1}^{t}\beta _{1}^{t-i} = 1-\beta _{1}^{t}

类似\large s_{t}展开也如此。

首先指数加权移动平均速度变量和累加变量。

\large v_{t} \leftarrow \beta _{1}v_{t-1} + (1-\beta_{1} )g_{t}

\large s_{t} = \beta _{2}s_{t-1} + (1-\beta _{2})g_{t}\odot g_{t}

\large \beta _{1}作者建议值0.9,\large \beta _{2}作者建议值0.999。

然后:归一化,偏差修正:

\large \hat{v}_{t} \leftarrow \frac{v_{t}}{1-\beta _{1}^{t}}

\large \hat{s}_{t} \leftarrow \frac{s_{t}}{1-\beta _{2}^{t}}

接着:模型参数中每个元素的学习率重新调整:

\large {g}'_{t}\leftarrow \frac{\eta \hat{v}_{t}}{\sqrt{\hat{s}_{t}}+ \epsilon }

\large \eta为学习率,\large \epsilon是为了维持数值稳定性而添加的常数,如\large 10^{-8}

最后,使用\large {g}'_{t}迭代自变量:

\large x_{t} \leftarrow x_{t-1}-{g}'_{t}

和AdaGrad算法、RMSProp算法以及AdaDelta算法一样,目标函数自变量中每个元素都分别拥有自己的学习率。

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值