adam-scratch.md 6.1 KB
Newer Older
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 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72
# Adam --- 从0开始

Adam是一个组合了[动量法](momentum-scratch.md)[RMSProp](rmsprop-scratch.md)的优化算法。



## Adam算法

Adam算法会使用一个动量变量$\mathbf{v}$和一个RMSProp中梯度按元素平方的指数加权移动平均变量$\mathbf{s}$,并将它们中每个元素初始化为0。在每次迭代中,首先计算[小批量梯度](gd-sgd-scratch.md) $\mathbf{g}$,并递增迭代次数

$$t := t + 1$$

然后对梯度做指数加权移动平均并计算动量变量$\mathbf{v}$:

$$\mathbf{v} := \beta_1 \mathbf{v} + (1 - \beta_1) \mathbf{g} $$


该梯度按元素平方后做指数加权移动平均并计算$\mathbf{s}$:

$$\mathbf{s} := \beta_2 \mathbf{s} + (1 - \beta_2) \mathbf{g} \odot \mathbf{g} $$


在Adam算法里,为了减轻$\mathbf{v}$和$\mathbf{s}$被初始化为0在迭代初期对计算指数加权移动平均的影响,我们做下面的偏差修正:

$$\hat{\mathbf{v}} := \frac{\mathbf{v}}{1 - \beta_1^t} $$



$$\hat{\mathbf{s}} := \frac{\mathbf{s}}{1 - \beta_2^t} $$



可以看到,当$0 \leq \beta_1, \beta_2 < 1$时(算法作者建议分别设为0.9和0.999),当迭代后期$t$较大时,偏差修正几乎就不再有影响。我们使用以上偏差修正后的动量变量和RMSProp中梯度按元素平方的指数加权移动平均变量,将模型参数中每个元素的学习率通过按元素操作重新调整一下:

$$\mathbf{g}^\prime := \frac{\eta \hat{\mathbf{v}}}{\sqrt{\hat{\mathbf{s}} + \epsilon}} $$

其中$\eta$是初始学习率,$\epsilon$是为了维持数值稳定性而添加的常数,例如$10^{-8}$。和Adagrad一样,模型参数中每个元素都分别拥有自己的学习率。

同样地,最后的参数迭代步骤与小批量随机梯度下降类似。只是这里梯度前的学习率已经被调整过了:

$$\mathbf{x} := \mathbf{x} - \mathbf{g}^\prime $$


## Adam的实现


Adam的实现很简单。我们只需要把上面的数学公式翻译成代码。

```{.python .input}
# Adam。
def adam(params, vs, sqrs, lr, batch_size, t):
    beta1 = 0.9
    beta2 = 0.999
    eps_stable = 1e-8
    for param, v, sqr in zip(params, vs, sqrs):      
        g = param.grad / batch_size
        v[:] = beta1 * v + (1. - beta1) * g
        sqr[:] = beta2 * sqr + (1. - beta2) * nd.square(g)
        v_bias_corr = v / (1. - beta1 ** t)
        sqr_bias_corr = sqr / (1. - beta2 ** t)
        div = lr * v_bias_corr / (nd.sqrt(sqr_bias_corr) + eps_stable)        
        param[:] = param - div
```

## 实验

实验中,我们以线性回归为例。其中真实参数`w`为[2, -3.4],`b`为4.2。我们把算法中基于指数加权移动平均的变量初始化为和参数形状相同的零张量。

```{.python .input  n=1}
import mxnet as mx
from mxnet import autograd
from mxnet import gluon
A
Aston Zhang 已提交
73
from mxnet import nd
74 75
import random

A
Aston Zhang 已提交
76
# 为方便比较同一优化算法的从零开始实现和Gluon实现,将输出保持确定。
77 78 79 80 81 82 83 84 85 86 87 88
mx.random.seed(1)
random.seed(1)

# 生成数据集。
num_inputs = 2
num_examples = 1000
true_w = [2, -3.4]
true_b = 4.2
X = nd.random_normal(scale=1, shape=(num_examples, num_inputs))
y = true_w[0] * X[:, 0] + true_w[1] * X[:, 1] + true_b
y += .01 * nd.random_normal(scale=1, shape=y.shape)

A
Aston Zhang 已提交
89 90 91 92 93 94 95 96 97 98 99
# 初始化模型参数。
def init_params():
    w = nd.random_normal(scale=1, shape=(num_inputs, 1))
    b = nd.zeros(shape=(1,))
    params = [w, b]
    sqrs = []
    for param in params:
        param.attach_grad()
        # 把梯度按元素平方的指数加权移动平均变量初始化为和参数形状相同的零张量。
        sqrs.append(param.zeros_like())
    return params, sqrs
100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119

# 初始化模型参数。
def init_params():
    w = nd.random_normal(scale=1, shape=(num_inputs, 1))
    b = nd.zeros(shape=(1,))
    params = [w, b]
    vs = []
    sqrs = []
    for param in params:
        param.attach_grad()
        # 把算法中基于指数加权移动平均的变量初始化为和参数形状相同的零张量。
        vs.append(param.zeros_like())
        sqrs.append(param.zeros_like())
    return params, vs, sqrs
```

接下来定义训练函数。当epoch大于2时(epoch从1开始计数),学习率以自乘0.1的方式自我衰减。训练函数的period参数说明,每次采样过该数目的数据点后,记录当前目标函数值用于作图。例如,当period和batch_size都为10时,每次迭代后均会记录目标函数值。

```{.python .input  n=2}
%matplotlib inline
A
Aston Zhang 已提交
120
%config InlineBackend.figure_format = 'retina'
121 122 123 124
import matplotlib as mpl
import matplotlib.pyplot as plt
import numpy as np

A
Aston Zhang 已提交
125 126 127 128 129 130
import sys
sys.path.append('..')
import utils

net = utils.linreg
squared_loss = utils.squared_loss
131

A
Aston Zhang 已提交
132 133 134 135 136
def optimize(batch_size, lr, num_epochs, log_interval):
    [w, b], vs, sqrs = init_params()
    y_vals = [nd.mean(squared_loss(net(X, w, b), y)).asnumpy()]
    print('batch size', batch_size)
    
137
    t = 0
A
Aston Zhang 已提交
138 139 140
    for epoch in range(1, num_epochs + 1):
        for batch_i, features, label in utils.data_iter(
            batch_size, num_examples, random, X, y):
141
            with autograd.record():
A
Aston Zhang 已提交
142 143
                output = net(features, w, b)
                loss = squared_loss(output, label)
144 145 146 147
            loss.backward()
            # 必须在调用Adam前。
            t += 1
            adam([w, b], vs, sqrs, lr, batch_size, t)
A
Aston Zhang 已提交
148 149 150 151 152
            if batch_i * batch_size % log_interval == 0:
                y_vals.append(
                    nd.mean(squared_loss(net(X, w, b), y)).asnumpy())
        print('epoch %d, learning rate %f, loss %.4e' % 
              (epoch, lr, y_vals[-1]))
153 154
    print('w:', np.reshape(w.asnumpy(), (1, -1)), 
          'b:', b.asnumpy()[0], '\n')
A
Aston Zhang 已提交
155 156 157
    x_vals = np.linspace(0, num_epochs, len(y_vals), endpoint=True)
    utils.set_fig_size(mpl)
    plt.semilogy(x_vals, y_vals)
158 159 160 161 162 163 164 165
    plt.xlabel('epoch')
    plt.ylabel('loss')
    plt.show()
```

使用Adam,最终学到的参数值与真实值较接近。

```{.python .input  n=3}
A
Aston Zhang 已提交
166
optimize(batch_size=10, lr=0.1, num_epochs=3, log_interval=10)
167 168 169 170 171 172 173 174 175 176 177
```

## 结论

* Adam组合了动量法和RMSProp。


## 练习

* 你是怎样理解Adam算法中的偏差修正项的?

S
Sheng Zha 已提交
178
**吐槽和讨论欢迎点**[这里](https://discuss.gluon.ai/t/topic/2279)