gd-sgd-scratch.md 13.0 KB
Newer Older
A
Aston Zhang 已提交
1
# 梯度下降和随机梯度下降——从0开始
2

A
till lr  
Aston Zhang 已提交
3
本节中,我们将介绍梯度下降(gradient descent)和随机梯度下降(stochastic gradient descent)算法。由于梯度下降是优化算法的核心部分,理解梯度的涵义十分重要。为了帮助读者深刻理解梯度,我们将从数学角度阐释梯度下降的意义。
4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21


## 一维梯度下降

我们先以简单的一维梯度下降为例,解释梯度下降算法可以降低目标函数值的原因。一维梯度是一个标量,也称导数。

假设函数$f: \mathbb{R} \rightarrow \mathbb{R}$的输入和输出都是标量。根据泰勒展开公式,我们得到

$$f(x + \epsilon) \approx f(x) + f'(x) \epsilon$$

假设$\eta$是一个常数,将$\epsilon$替换为$-\eta f'(x)$后,我们有

$$f(x - \eta f'(x)) \approx f(x) -  \eta f'(x)^2$$

如果$\eta$是一个很小的正数,那么

$$f(x - \eta f'(x)) \leq f(x)$$

A
till lr  
Aston Zhang 已提交
22
也就是说,如果目标函数$f(x)$当前的导数$f'(x) \neq 0$,按照
23

A
till lr  
Aston Zhang 已提交
24
$$x \leftarrow x - \eta f'(x)$$
25

A
till lr  
Aston Zhang 已提交
26
迭代自变量$x$可能会降低$f(x)$的值。由于导数$f'(x)$是梯度$\nabla_x f$在一维空间的特殊情况,上述迭代自变量$x$的方法也即一维空间的梯度下降。一维空间的梯度下降图7.2(左)所示,自变量$x$沿着梯度方向迭代。
27

A
till lr  
Aston Zhang 已提交
28
![梯度下降中,目标函数$f(x)$的自变量$x$(圆圈的横坐标)沿着梯度方向迭代](../img/gd_and_overshooting.svg)
29 30 31 32


## 学习率

A
Aston Zhang 已提交
33
上述梯度下降算法中的$\eta$叫做学习率。这是一个超参数,需要人工设定。学习率$\eta$要取正数。
A
Aston Zhang 已提交
34

A
till lr  
Aston Zhang 已提交
35
需要注意的是,学习率过大可能会造成自变量$x$越过(overshoot)目标函数$f(x)$的最优解,甚至发散。见图7.2(右)。
36

A
till lr  
Aston Zhang 已提交
37
然而,如果学习率过小,目标函数中自变量的迭代速度会过慢。实际中,一个合适的学习率通常是需要通过多次实验找到的。
38 39 40 41 42 43


## 多维梯度下降

现在我们考虑一个更广义的情况:目标函数的输入为向量,输出为标量。

A
Aston Zhang 已提交
44
假设目标函数$f: \mathbb{R}^d \rightarrow \mathbb{R}$的输入是一个$d$维向量$\mathbf{x} = [x_1, x_2, \ldots, x_d]^\top$。目标函数$f(\mathbf{x})$有关$\mathbf{x}$的梯度是一个由$d$个偏导数组成的向量:
45 46 47 48

$$\nabla_\mathbf{x} f(\mathbf{x}) = \bigg[\frac{\partial f(\mathbf{x})}{\partial x_1}, \frac{\partial f(\mathbf{x})}{\partial x_2}, \ldots, \frac{\partial f(\mathbf{x})}{\partial x_d}\bigg]^\top.$$


A
Aston Zhang 已提交
49
为表示简洁,我们用$\nabla f(\mathbf{x})$代替$\nabla_\mathbf{x} f(\mathbf{x})$。梯度中每个偏导数元素$\partial f(\mathbf{x})/\partial x_i$代表着$f$在$\mathbf{x}$有关输入$x_i$的变化率。为了测量$f$沿着单位向量$\mathbf{u}$方向上的变化率,在多元微积分中,我们定义$f$在$\mathbf{x}$上沿着$\mathbf{u}$方向的方向导数为
50

A
Aston Zhang 已提交
51
$$D_\mathbf{u} f(\mathbf{x}) = \lim_{h \rightarrow 0}  \frac{f(\mathbf{x} + h \mathbf{u}) - f(\mathbf{x})}{h}.$$
52

A
Aston Zhang 已提交
53
依据方向导数性质 [1,14.6节定理三],该方向导数可以改写为
54

A
Aston Zhang 已提交
55
$$D_\mathbf{u} f(\mathbf{x}) = \nabla f(\mathbf{x}) \cdot \mathbf{u}.$$
56

A
Aston Zhang 已提交
57
方向导数$D_\mathbf{u} f(\mathbf{x})$给出了$f$在$\mathbf{x}$上沿着所有可能方向的变化率。为了最小化$f$,我们希望找到$f$能被降低最快的方向。因此,我们可以通过单位向量$\mathbf{u}$来最小化方向导数$D_\mathbf{u} f(\mathbf{x})$。
58 59

由于$D_\mathbf{u} f(\mathbf{x}) = \|\nabla f(\mathbf{x})\| \cdot \|\mathbf{u}\|  \cdot \text{cos} (\theta) = \|\nabla f(\mathbf{x})\|  \cdot \text{cos} (\theta)$,
A
Aston Zhang 已提交
60
其中$\theta$为梯度$\nabla f(\mathbf{x})$和单位向量$\mathbf{u}$之间的夹角,当$\theta = \pi$,$\text{cos}(\theta)$取得最小值-1。因此,当$\mathbf{u}$在梯度方向$\nabla f(\mathbf{x})$的相反方向时,方向导数$D_\mathbf{u} f(\mathbf{x})$被最小化。所以,我们可能通过下面的梯度下降算法来不断降低目标函数$f$的值:
61

A
Aston Zhang 已提交
62
$$\mathbf{x} := \mathbf{x} - \eta \nabla f(\mathbf{x}).$$
63

A
Aston Zhang 已提交
64
相同地,其中$\eta$(取正数)称作学习率。
65 66 67 68 69 70 71

## 随机梯度下降

然而,当训练数据集很大时,梯度下降算法可能会难以使用。为了解释这个问题,考虑目标函数

$$f(\mathbf{x}) = \frac{1}{n} \sum_{i = 1}^n f_i(\mathbf{x}),$$

A
Aston Zhang 已提交
72
其中$f_i(\mathbf{x})$是有关索引为$i$的训练数据样本的损失函数,$n$是训练数据样本数。由于
73

A
Aston Zhang 已提交
74
$$\nabla f(\mathbf{x}) = \frac{1}{n} \sum_{i = 1}^n \nabla f_i(\mathbf{x}),$$
75

A
Aston Zhang 已提交
76
梯度下降每次迭代的计算开销随着$n$线性增长。因此,当训练数据样本数很大时,梯度下降每次迭代的计算开销很高。这时我们可以使用随机梯度下降。给定学习率$\eta$(取正数),在每次迭代时,随机梯度下降算法随机均匀采样$i$并计算$\nabla f_i(\mathbf{x})$来迭代$\mathbf{x}$:
77

A
Aston Zhang 已提交
78
$$\mathbf{x} := \mathbf{x} - \eta \nabla f_i(\mathbf{x}).$$
79 80


A
Aston Zhang 已提交
81 82 83 84 85 86
事实上,随机梯度$\nabla f_i(\mathbf{x})$是对梯度$\nabla f(\mathbf{x})$的无偏估计:

$$\mathbb{E}_i \nabla f_i(\mathbf{x}) = \frac{1}{n} \sum_{i = 1}^n \nabla f_i(\mathbf{x}) = \nabla f(\mathbf{x}).$$


## 小批量随机梯度下降
87

A
Aston Zhang 已提交
88
广义上,每一次迭代可以随机均匀采样一个由训练数据样本索引所组成的小批量(mini-batch)$\mathcal{B}$。类似地,我们可以使用
89 90 91

$$\nabla f_\mathcal{B}(\mathbf{x}) = \frac{1}{|\mathcal{B}|} \sum_{i \in \mathcal{B}}\nabla f_i(\mathbf{x})$$ 

A
Aston Zhang 已提交
92
来迭代$\mathbf{x}$:
93

A
Aston Zhang 已提交
94
$$\mathbf{x} := \mathbf{x} - \eta \nabla f_\mathcal{B}(\mathbf{x}).$$
95

A
Aston Zhang 已提交
96
在上式中,$|\mathcal{B}|$代表样本批量大小,$\eta$(取正数)称作学习率。同样,小批量随机梯度$\nabla f_\mathcal{B}(\mathbf{x})$也是对梯度$\nabla f(\mathbf{x})$的无偏估计:
97 98 99

$$\mathbb{E}_\mathcal{B} \nabla f_\mathcal{B}(\mathbf{x}) = \nabla f(\mathbf{x}).$$

A
Aston Zhang 已提交
100
这个算法叫做小批量随机梯度下降。该算法每次迭代的计算开销为$\mathcal{O}(|\mathcal{B}|)$。当批量大小为1时,该算法即随机梯度下降;当批量大小等于训练数据样本数,该算法即梯度下降。和学习率一样,批量大小也是一个超参数。当批量较小时,虽然每次迭代的计算开销较小,但计算机并行处理批量中各个样本的能力往往只得到较少利用。因此,当训练数据集的样本较少时,我们可以使用梯度下降;当样本较多时,我们可以使用小批量梯度下降并依据计算资源选择合适的批量大小。
101 102 103 104 105 106 107 108 109 110 111 112 113


## 算法实现和实验

我们只需要实现小批量随机梯度下降。当批量大小等于训练集大小时,该算法即为梯度下降;批量大小为1即为随机梯度下降。

```{.python .input  n=1}
# 小批量随机梯度下降。
def sgd(params, lr, batch_size):
    for param in params:
        param[:] = param - lr * param.grad / batch_size
```

A
gd sgd  
Aston Zhang 已提交
114
实验中,我们以之前介绍过的线性回归为例。我们使用权重`w`为[2, -3.4],偏差`b`为4.2的线性回归模型来生成数据集。数据集的样本数为1000。目标函数为平方损失函数。
115 116 117 118 119

```{.python .input  n=2}
import mxnet as mx
from mxnet import autograd
from mxnet import gluon
A
Aston Zhang 已提交
120
from mxnet import nd
A
Aston Zhang 已提交
121
import random
122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139

# 生成数据集。
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)

# 初始化模型参数。
def init_params():
    w = nd.random_normal(scale=1, shape=(num_inputs, 1))
    b = nd.zeros(shape=(1,))
    params = [w, b]
    for param in params:
        param.attach_grad()
    return params
A
Aston Zhang 已提交
140 141 142 143 144 145 146 147 148

# 线性回归模型。
def linreg(X, w, b): 
    return nd.dot(X, w) + b 

# 平方损失函数。
def squared_loss(yhat, y): 
    return (yhat - y.reshape(yhat.shape)) ** 2 / 2

A
gd sgd  
Aston Zhang 已提交
149
# 遍历数据集。
A
Aston Zhang 已提交
150 151 152
def data_iter(batch_size, num_examples, random, X, y): 
    idx = list(range(num_examples))
    random.shuffle(idx)
A
modify  
Aston Zhang 已提交
153
    for i in range(0, num_examples, batch_size):
A
Aston Zhang 已提交
154
        j = nd.array(idx[i: min(i + batch_size, num_examples)])
A
modify  
Aston Zhang 已提交
155
        yield X.take(j), y.take(j)
156 157
```

A
gd sgd  
Aston Zhang 已提交
158
下面我们描述一下优化函数`optimize`
A
Aston Zhang 已提交
159

A
gd sgd  
Aston Zhang 已提交
160 161 162
由于随机梯度的方差在迭代过程中无法减小,(小批量)随机梯度下降的学习率通常会采用自我衰减的方式。如此一来,学习率和随机梯度乘积的方差会衰减。实验中,当迭代周期(`epoch`)大于2时,(小批量)随机梯度下降的学习率在每个迭代周期开始时自乘0.1作自我衰减。而梯度下降在迭代过程中一直使用目标函数的真实梯度,无需自我衰减学习率。

在迭代过程中,每当`log_interval`个样本被采样过后,当前的目标函数值(`loss`)被记录下并用于作图。例如,当`batch_size``log_interval`都为10时,每次迭代后的目标函数值都被用来作图。
163 164 165

```{.python .input  n=3}
%matplotlib inline
A
Aston Zhang 已提交
166
%config InlineBackend.figure_format = 'retina'
A
Aston Zhang 已提交
167
import matplotlib as mpl
168 169
import matplotlib.pyplot as plt
import numpy as np
A
Aston Zhang 已提交
170 171 172
import sys
sys.path.append('..')
import utils
173

A
Aston Zhang 已提交
174 175
net = linreg
squared_loss = squared_loss
A
Aston Zhang 已提交
176

A
Aston Zhang 已提交
177
def optimize(batch_size, lr, num_epochs, log_interval, decay_epoch):
178
    w, b = init_params()
A
Aston Zhang 已提交
179 180 181
    y_vals = [nd.mean(squared_loss(net(X, w, b), y)).asnumpy()]
    print('batch size', batch_size)
    for epoch in range(1, num_epochs + 1):
182
        # 学习率自我衰减。
A
modify  
Aston Zhang 已提交
183
        if decay_epoch and epoch > decay_epoch:
184
            lr *= 0.1
A
modify  
Aston Zhang 已提交
185 186
        for batch_i, (features, label) in enumerate(
            data_iter(batch_size, num_examples, random, X, y)):
187
            with autograd.record():
A
Aston Zhang 已提交
188
                output = net(features, w, b)
A
Aston Zhang 已提交
189
                loss = squared_loss(output, label)
190 191
            loss.backward()
            sgd([w, b], lr, batch_size)
A
Aston Zhang 已提交
192
            if batch_i * batch_size % log_interval == 0:
193 194 195 196
                y_vals.append(squared_loss(net(X, w, b), y).mean().asnumpy())
        print('epoch %d, learning rate %f, loss %.4e' % (epoch, lr,
                                                         y_vals[-1]))
    # 为了便于打印,改变输出形状并转化成numpy数组。
A
Aston Zhang 已提交
197
    print('w:', w.reshape((1, -1)).asnumpy(), 'b:', b.asscalar(), '\n')
A
Aston Zhang 已提交
198
    x_vals = np.linspace(0, num_epochs, len(y_vals), endpoint=True)
A
Aston Zhang 已提交
199
    utils.set_fig_size(mpl)
A
Aston Zhang 已提交
200
    plt.semilogy(x_vals, y_vals)
201 202 203 204 205
    plt.xlabel('epoch')
    plt.ylabel('loss')
    plt.show()
```

A
gd sgd  
Aston Zhang 已提交
206
当批量大小为1时,优化使用的是随机梯度下降。在当前学习率下,目标函数值在早期快速下降后略有波动。这是由于随机梯度的方差在迭代过程中无法减小。当迭代周期大于2,学习率自我衰减后,目标函数值下降后较平稳。最终,优化所得的模型参数值`w``b`与它们的真实值[2, -3.4]和4.2较接近。
207 208

```{.python .input  n=4}
A
Aston Zhang 已提交
209
optimize(batch_size=1, lr=0.2, num_epochs=3, decay_epoch=2, log_interval=10)
210 211
```

A
gd sgd  
Aston Zhang 已提交
212 213 214
当批量大小为1000时,由于数据样本总数也是1000,优化使用的是梯度下降。梯度下降无需自我衰减学习率(`decay_epoch=None`)。最终,优化所得的模型参数值与它们的真实值较接近。

需要注意的是,梯度下降的1个迭代周期对模型参数只迭代1次。而随机梯度下降的批量大小为1,它在1个迭代周期对模型参数迭代了1000次。我们观察到,1个迭代周期后,梯度下降所得的目标函数值比随机梯度下降所得的目标函数值略大。而在3个迭代周期后,这两个算法所得的目标函数值很接近。
215 216

```{.python .input  n=5}
A
gd sgd  
Aston Zhang 已提交
217 218
optimize(batch_size=1000, lr=0.999, num_epochs=3, decay_epoch=None, 
         log_interval=1000)
219 220
```

A
gd sgd  
Aston Zhang 已提交
221
当批量大小为10时,由于数据样本总数也是1000,优化使用的是小批量随机梯度下降。最终,优化所得的模型参数值与它们的真实值较接近。
222 223

```{.python .input  n=6}
A
Aston Zhang 已提交
224
optimize(batch_size=10, lr=0.2, num_epochs=3, decay_epoch=2, log_interval=10)
225 226
```

A
gd sgd  
Aston Zhang 已提交
227 228
同样是批量大小为10,我们把学习率改大。这时我们观察到目标函数值不断增大,直到出现'nan'(not a number,非数)。
这是因为,过大的学习率造成了目标函数自变量越过最优解并发散。
229 230

```{.python .input  n=7}
A
Aston Zhang 已提交
231
optimize(batch_size=10, lr=5, num_epochs=3, decay_epoch=2, log_interval=10)
232 233
```

A
gd sgd  
Aston Zhang 已提交
234
同样是批量大小为10,我们把学习率改小。这时我们观察到目标函数值下降较慢,直到3个迭代周期也没能得到接近真实值的解。
235 236

```{.python .input  n=8}
A
gd sgd  
Aston Zhang 已提交
237 238
optimize(batch_size=10, lr=0.002, num_epochs=3, decay_epoch=2,
         log_interval=10)
239 240
```

A
Aston Zhang 已提交
241
## 小结
242 243

* 当训练数据较大,梯度下降每次迭代计算开销较大,因而(小批量)随机梯度下降更受青睐。
A
gd sgd  
Aston Zhang 已提交
244
* 学习率过大过小都有问题。一个合适的学习率通常是需要通过多次实验找到的。
245 246 247 248


## 练习

A
gd sgd  
Aston Zhang 已提交
249
* 运行本节中实验代码。比较一下随机梯度下降和梯度下降的运行时间。
250 251
* 梯度下降和随机梯度下降虽然看上去有效,但可能会有哪些问题?

A
gd sgd  
Aston Zhang 已提交
252
## 讨论
253

A
gd sgd  
Aston Zhang 已提交
254
欢迎扫码直达[本节内容讨论区](https://discuss.gluon.ai/t/topic/1877)
A
Aston Zhang 已提交
255

A
gd sgd  
Aston Zhang 已提交
256
![](../img/qr_gd-sgd-scratch.svg)
A
Aston Zhang 已提交
257 258


A
gd sgd  
Aston Zhang 已提交
259 260 261
## 参考文献

[1] J. Stewart. Calculus: Early Transcendentals (7th Edition). Brooks Cole. 2010.