动手学深度学习 7.2 梯度下降和随机梯度下降

前言

从零开始学习ai文章系列计划是个人在《动手学深度学习》和《磨菇书》两本书的学习中的个人笔记,文章也会以课本中的章节分开,即每个章节一片笔记。我会尽量的把主要内容以及遇到的难点进行记录与解决,如果哪里有错误的欢迎指正。或者不清晰的可以直接查看原文部分。

《动手学深度学习》原文(课本):https://tangshusen.me/Dive-into-DL-PyTorch/#/

《动手学深度学习》代码:https://github.com/ShusenTang/Dive-into-DL-PyTorch

(由于有时候公式太多,可能会直接贴图片)


在本节中,我们将介绍梯度下降(gradient descent)的工作原理。虽然梯度下降在深度学习中很少被直接使用,但理解梯度的意义以及沿着梯度反方向更新自变量可能降低目标函数值的原因是学习后续优化算法的基础。

随后,我们将引出随机梯度下降(stochastic gradient descent)。

本篇中解释说明了为什么 减去梯度可以减少损失函数。以及随机梯度为什么可行的原理

1. 一维梯度下降

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

假设连续可导的函数 \(f:R→R\) 的输入和输出都是标量。给定绝对值足够小的数 \(ϵ\),根据泰勒展开公式,我们得到以下的近似: \[ f(x+ϵ) ≈ f(x) + ϵf'(x) \] 这里 \(f'(x)\) 是函数\(f\)\(x\)处的梯度。一维函数的梯度是一个标量,也称为导数

接下来,找到一个常数\(η>0\),使得\(∣ηf'(x)∣\)足够小,那么可以将\(ϵ\)替换为\(−ηf'(x)\)并得到 \[ f(x−ηf'(x)) ≈ f(x) − ηf'(x)^2 \] 如果导数\(f'(x)≠0\)那么,那么\(ηf'(x)^2>0\),所以 \[ f(x−ηf'(x)) ≲ f(x) \] 这意味着,如果通过

\[ x \leftarrow x−ηf'(x) \] 来迭代\(x\),函数\(f(x)\)的值可能会降低。

因此在梯度下降中,我们先选取一个初始值\(x\)和常数\(η>0\),然后不断通过上式来迭代\(x\),直到达到停止条件,例如\(f'(x)^2\)的值已足够小或迭代次数已达到某个值。

可能懵逼的同学看完后还是有点懵逼,我简单说一下。

存在 \(输入x\)\(输出f(x)\) ,然后计算出当前的导数 \(ηf'(x)\)

倘若我们 将 \(原本的x\)\(x−ηf'(x)\) 替代的话,我们得到 \(f(x−ηf'(x))\)

根据之前的公式,\(f(x−ηf'(x)) ≲ f(x)\) 此时替换后算出的结果必定不会比原来大。

那么在实际中,这个\(f(x)\) 就是我们的损失函数,x就是我们网络的各种参数 \(\theta\) ,在求导过程中,我们正向计算的图片等输入在此时都是常数。那么我们将原本的参数 \(\theta\)\(\theta−ηf'(\theta)\) 替换,那么我们就能让损失函数降低或者不变。

(记住前提是 \(∣ηf'(x)∣\)足够小,公式才成立 )

也就是直觉中导数正负指明x的前进方向,然后减去导数就是往反方向(值变小的方向)走的理论依据。

下面我们以目标函数 \(f(x)=x^2\) 为例来看一看梯度下降是如何工作的。虽然我们知道最小化f(x)的解为x=0,这里依然使用这个简单函数来观察x是如何被迭代的。

使用 \(x=10\) 作为初始值,并设 \(η=0.2\) 。使用梯度下降对x迭代10次,可见最终x的值较接近最优解。

1
2
3
4
5
6
7
8
9
10
def gd(eta):
x = 10
results = [x]
for i in range(10):
x -= eta * 2 * x # f(x) = x * x的导数为f'(x) = 2 * x
results.append(x)
print('epoch 10, x:', x)
return results

res = gd(0.2)

可以看到x在10轮后非常接近于0 (\(f(x)\) 为最小值时的\(x\)

下面将绘制出自变量x的迭代轨迹。

1
2
3
4
5
6
7
8
9
10
def show_trace(res):
n = max(abs(min(res)), abs(max(res)), 10)
f_line = np.arange(-n, n, 0.1)
d2l.set_figsize()
d2l.plt.plot(f_line, [x * x for x in f_line])
d2l.plt.plot(res, [x * x for x in res], '-o')
d2l.plt.xlabel('x')
d2l.plt.ylabel('f(x)')
d2l.plt.show()
show_trace(res)

2. 学习率

上述梯度下降算法中的正数 \(η\) 通常叫作学习率。这是一个超参数,需要人工设定。

如果使用过小的学习率,会导致x更新缓慢从而需要更多的迭代才能得到较好的解。

下面展示使用学习率 \(η=0.05\) 时自变量x的迭代轨迹。可见,同样迭代10次后,当学习率过小时,最终x的值依然与最优解存在较大偏差。

1
show_trace(gd(0.05))

如果使用过大的学习率,∣ηf′(x)∣ 可能会过大从而使前面提到的一阶泰勒展开公式不再成立:这时我们无法保证迭代x会降低f(x)的值。

举个例子,当设学习率η=1.1时,可以看到x不断越过(overshoot)最优解x=0并逐渐发散。

1
show_trace(gd(1.1))

3. 多维梯度下降

在了解了一维梯度下降之后,我们再考虑一种更广义的情况:目标函数的输入为向量,输出为标量。假设 \(目标函数f\) 的输入是一个d维向量 \(x=[x_1,x_2,…,x_d]^⊤\)。目标函数f(x)有关x的梯度是一个由d个偏导数组成的向量

为表示简洁,我们用 \(∇f(x)\) 代替 \(∇_xf(x)\)

梯度中每个偏导数元素 \(∂f(x)/∂x_i\) 代表着 f 在x有关输入\(x_i\) 的变化率。

为了测量f沿着 \(单位向量u\)(即 \(∥u∥=1\) )方向上的变化率,在多元微积分中,我们定义f在x上沿着u方向的方向导数为

依据方向导数性质,以上方向导数可以改写为

方向导数 \(D_uf(x)\) 给出了 \(f\)\(x\) 上沿着所有可能方向的变化率。为了最小化 \(f\),我们希望找到 \(f\) 能被降低最快的方向。因此,我们可以通过单位向量 \(u\) 来最小化方向导数 \(D_uf(x)\)

由于

其中 \(θ\) 为梯度 \(∇f(x)\) 和单位向量 \(u\) 之间的夹角

\(θ=\pi\) 时,\(cos(θ)\) 取得最小值−1。因此,\(u\) 在梯度方向 \(∇f(x)\) 的相反方向时方向导数 \(D_uf(x)\) 被最小化。因此,我们可能通过梯度下降算法来不断降低目标函数 \(f\) 的值: \[ x \leftarrow x−η∇f(x) \] 同样,其中 \(η\)(取正数)称作学习率。

下面我们构造一个输入为二维向量 \(x=[x_1,x_2]^⊤\) 和输出为标量的 目标函数 \(f(x)=x_1^2+2x_2^2\) 。那么,梯度为 \(∇f(x)=[2x_1,4x_2]^⊤\)

我们将观察梯度下降从初始位置 \([−5,−2]\) 开始对自变量 \(x\) 的迭代轨迹。

我们先定义两个辅助函数,

第一个函数使用给定的自变量更新函数,从初始位置 \([−5,−2]\) 开始迭代自变量\(x\)共20次,

第二个函数对自变量x的迭代轨迹进行可视化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
def train_2d(trainer):  # 本函数将保存在d2lzh_pytorch包中方便以后使用
x1, x2, s1, s2 = -5, -2, 0, 0 # s1和s2是自变量状态,本章后续几节会使用
results = [(x1, x2)]
for i in range(20):
x1, x2, s1, s2 = trainer(x1, x2, s1, s2)
results.append((x1, x2))
print('epoch %d, x1 %f, x2 %f' % (i + 1, x1, x2))
return results

def show_trace_2d(f, results): # 本函数将保存在d2lzh_pytorch包中方便以后使用
d2l.plt.plot(*zip(*results), '-o', color='#ff7f0e')
x1, x2 = np.meshgrid(np.arange(-5.5, 1.0, 0.1), np.arange(-3.0, 1.0, 0.1))
cs = d2l.plt.contour(x1, x2, f(x1, x2), colors='#1f77b4')
d2l.plt.clabel(cs) # 给每条等高线加上数值标签
d2l.plt.xlabel('x1')
d2l.plt.ylabel('x2')
d2l.plt.show()

然后,观察学习率为0.1时自变量的迭代轨迹。使用梯度下降对自变量x迭代20次后,可见最终x的值较接近最优解[0,0]。

1
2
3
4
5
6
7
8
eta = 0.1
def f_2d(x1, x2): # 目标函数
return x1 ** 2 + 2 * x2 ** 2

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

show_trace_2d(f_2d, train_2d(gd_2d))

我们清晰的看到每次 \(x_1,x_2\) 更新后,\(f(x_1,x_2)\) 的值就会越小

4. 随机梯度下降

在深度学习里,目标函数通常是训练数据集中有关各个样本的损失函数的平均。设 \(f_i(x)\) 是有关索引为 \(i\) 的训练数据样本的损失函数,\(n\) 是训练数据样本数,\(x\) 是模型的参数向量,那么目标函数定义为

目标函数在 \(x\) 处的梯度计算为

如果使用梯度下降,每次自变量迭代的计算开销为O(n),它随着n线性增长。因此,当训练数据样本数很大时,梯度下降每次迭代的计算开销很高。

随机梯度下降(stochastic gradient descent,SGD)减少了每次迭代的计算开销。在随机梯度下降的每次迭代中,我们随机均匀采样的一个样本索引 \(i∈{1,…,n}\) ,并计算梯度 \(∇f_i(x)\) 来迭代 \(x\)

这里是意思是随机挑选1个样本然后计算他的梯度来迭代x,而并不是跟之前一样n个样本的全部导数计算出来后更新

\[ x \leftarrow x−η∇f'(x) \]

这里 \(η\) 同样是学习率。可以看到每次迭代的计算开销从梯度下降的O(n)降到了常数O(1)。值得强调的是,随机梯度 \(∇f_i(x)\) 是对梯度 \(∇f(x)\) 的无偏估计

这意味着,平均来说,随机梯度是对梯度的一个良好的估计。

下面我们通过在梯度中添加均值为0的随机噪声来模拟随机梯度下降,以此来比较它与梯度下降的区别。

1
2
3
4
5
def sgd_2d(x1, x2, s1, s2):
return (x1 - eta * (2 * x1 + np.random.normal(0.1)),
x2 - eta * (4 * x2 + np.random.normal(0.1)), 0, 0)

show_trace_2d(f_2d, train_2d(sgd_2d))

可以看到,随机梯度下降中自变量的迭代轨迹相对于梯度下降中的来说更为曲折。这是由于实验所添加的噪声使模拟的随机梯度的准确度下降。在实际中,这些噪声通常指训练数据集中的无意义的干扰。