Mxnet (26): 梯度下降(GD&SGD)

五迷三道 提交于 2020-10-03 05:28:01

这里我们介绍梯度下降的基本概念。尽管很少实际中很少用到,但是了解梯度下降有益于理解随机梯度下降算法的关键。例如,由于学习率过高,优化问题可能会有所不同,这在梯度下降中也会出现。同样预处理是梯度下降中的常用技术。我们先从简单的入手。

1 一维梯度下降

一维梯度下降是一个很好的例子,可以用于理解梯度下降算法如何减小目标函数的值。对于连续可微的实值函数 f : R → R f: \mathbb{R} \rightarrow \mathbb{R} f:RR。使用泰勒展开公式:
f ( x + ϵ ) = f ( x ) + ϵ f ′ ( x ) + O ( ϵ 2 ) f(x + \epsilon) = f(x) + \epsilon f'(x) + \mathcal{O}(\epsilon^2) f(x+ϵ)=f(x)+ϵf(x)+O(ϵ2)

也就是说, f ( x + ϵ ) f(x+\epsilon) f(x+ϵ)近似为 f ( x ) f(x) f(x) 以及在 x x x处的一阶导数 f ′ ( x ) f'(x) f(x)之和。假设 ϵ \epsilon ϵ 向梯度相反移动将会减少 f f f是合理的。为了简化,我们选择固定步长 η > 0 \eta > 0 η>0以及 ϵ = − η f ′ ( x ) \epsilon = -\eta f'(x) ϵ=ηf(x)。然后带入到泰勒展开中:

f ( x − η f ′ ( x ) ) = f ( x ) − η f ′ 2 ( x ) + O ( η 2 f ′ 2 ( x ) ) . f(x - \eta f'(x)) = f(x) - \eta f'^2(x) + \mathcal{O}(\eta^2 f'^2(x)). f(xηf(x))=f(x)ηf2(x)+O(η2f2(x)).

假设 f ′ ( x ) ≠ 0 f'(x) \neq 0 f(x)=0 一直成立,因为 η f ′ 2 ( x ) > 0 \eta f'^2(x)>0 ηf2(x)>0,将会起作用。如果选择的 η \eta η足够小的话,可以得到:

f ( x − η f ′ ( x ) ) ⪅ f ( x ) f(x - \eta f'(x)) \lessapprox f(x) f(xηf(x))f(x)

也就是说,我们通过

x ← x − η f ′ ( x ) x \leftarrow x - \eta f'(x) xxηf(x)

来迭代 x x x, f ( x ) f(x) f(x)函数的值也许会减小。在选择一个初始值 x x x 和一个常数 η > 0 \eta > 0 η>0然后一直迭代 x x x 直到达到停止条件,如, 当 ∣ f ′ ( x ) ∣ |f'(x)| f(x)足够小或是迭代次数达到一定值。

为了简单起见我们选择 f ( x ) = x 2 f(x)=x^2 f(x)=x2 作为目标函数来解释如何实现梯度下降。虽然我们知道 x = 0 x=0 x=0 f ( x ) f(x) f(x)为的最小值, 我们将使用一个简单的函数来观察 x x x的变化。

from d2l import mxnet as d2l
from mxnet import npx, np
import plotly.graph_objs as go
npx.set_np()

f = lambda x:x**2
gradf = lambda x: 2*x

我们使用 x=10 作为初始值并假设 η=0.2 。使用梯度下降进行迭代 x 10次。

def gd(eta):
    x = 10.0
    result = [x]
    for i in range(10):
        x -= eta*gradf(x)
        result.append(float(x))
        print(f'[epoch {i+1}]  x: {x:.3f}')
    return result

res = gd(0.2)

在这里插入图片描述

使用plotly做可视化:

def show_trace(res):
    n = max(abs(min(res)), abs(max(res)))
    x = np.arange(-n, n, 0.01)
    fig = go.Figure()
    fig.add_trace(go.Scatter(x=x.tolist(), y=f(x).tolist() , mode='lines'))
    fig.add_trace(go.Scatter(x=res, y=f(np.array(res)).tolist() , mode='lines+markers', marker={
   
   'size':8}))
    fig.update_layout(width=500, height = 380, xaxis_title='x', yaxis_title='f(x)')
    fig.show()
    
show_trace(res)

在这里插入图片描述

1.1 学习率

学习率 η 可以由算法设计者设置。如果我们使用的学习率太小,将导致 x 更新非常缓慢,需要更多迭代才能获得更好的解决方案。为了说明在这种情况下会发生什么,请考虑针对 η=0.05 。如我们所见,即使经过10个步骤,我们仍然离最佳解决方案还差得很远。

show_trace(gd(0.05))

在这里插入图片描述

相反,如果我们使用过高的学习率, ∣ η f ′ ( x ) ∣ \left|\eta f'(x)\right| ηf(x) 对于一阶泰勒展开式而言,可能太大。 就是说 O ( η 2 f ′ 2 ( x ) ) \mathcal{O}(\eta^2 f'^2(x)) O(η2f2(x))的影响更大。 在这种情况下,我们不能保证 x x x 能够降低 f ( x ) f(x) f(x)。例如, 当我们将学习率设置为 η = 1.1 \eta=1.1 η=1.1, x x x 越过了 x = 0 x=0 x=0 并且逐步发散。

show_trace(gd(1.1))

在这里插入图片描述

1.2 局部极小值

请考虑以下情况: f ( x ) = x ⋅ cos ⁡ c x f(x) = x \cdot \cos c x f(x)=xcoscx。此函数具有无限多个局部最小值。下面展示了高学习率导致较差的局部最小值。

c = np.array(0.15 * np.pi)
f = lambda x: x * np.cos(c * x)
gradf = lambda x: np.cos(c * x) - c * x * np.sin(c * x)
show_trace(gd(2))

在这里插入图片描述

2.2 多元梯度下降

为了了解算法在实际中如何工作,这里构造了一个函数 f ( x ) = x 1 2 + 2 x 2 2 f(\mathbf{x})=x_1^2+2x_2^2 f(x)=x12+2x22 以及一个二维向量 x = [ x 1 , x 2 ] ⊤ \mathbf{x} = [x_1, x_2]^\top x=[x1,x2]作为输入,标量作为输出。梯度由下式给出 ∇ f ( x ) = [ 2 x 1 , 4 x 2 ] ⊤ \nabla f(\mathbf{x}) = [2x_1, 4x_2]^\top f(x)=[2x1,4x2]。我们将观察到轨迹 x \mathbf{x} x从初始位置 [ − 5 , − 2 ] [-5, -2] [5,2]开始下降。

def train_2d(trainer, steps=20):  
    """优化二维目标"""
    # s1和s2是内部状态变量
    x1, x2, s1, s2 = -5, -2, 0, 0
    results = [[x1], [x2]]
    for i in range(steps):
        x1, x2, s1, s2 = trainer(x1, x2, s1, s2)
        results[0].append(x1)
        results[1].append(x2)
    return results

def show_trace_2d(f, results): 
    """2D可视化"""
    fig = go.Figure()
    x1, x2 = np.meshgrid(np.arange(-5.5, 1.0, 0.1),  np.arange(-3.0, 1.0, 0.1))
    fig.add_trace(go.Contour(x=x1[0].tolist(), y=x2.T[0].tolist(), z=f(x1, x2).tolist(),showscale=False , colorscale='YlGnBu'))
    fig.add_trace(go.Scatter(x=results[0], y=results[1] , mode='lines+markers', marker={
   
   'size':8}))
    fig.update_layout(width=500, height = 380, xaxis_title='x1', yaxis_title='x2')
    fig.show()

接下来,我们观察优化变量的轨迹 x x x 学习率 η = 0.1 η=0.1 η=0.1 。我们可以看到,经过20个步骤, x x x 在接近其最小值 [ 0 , 0 ] [0,0] [0,0] 。尽管进展相当缓慢,但进展还是相当不错的。

f = lambda x1, x2: x1 ** 2 + 2 * x2 ** 2  # 目的函数
gradf = lambda x1, x2: (2 * x1, 4 * x2)  # 辅助函数

def gd(x1, x2, s1, s2):
    (g1, g2) = gradf(x1, x2)  # 计算梯度
    return (x1 - eta * g1, x2 - eta * g2, 0, 0)  # 更新

eta = 0.1
show_trace_2d(f, train_2d(gd))

在这里插入图片描述

3.随机梯度下降

3.1 随机梯度更新

在深度学习中,目标函数通常是训练数据集中每个示例的损失函数的平均值。 假设 n n n 个样本 以 f i ( x ) f_i(\mathbf{x}) fi(x) 为损失函数的数据集, 索引 i i i, 参数向量 x \mathbf{x} x, 我们的目标函数如下:

f ( x ) = 1 n ∑ i = 1 n f i ( x ) . f(\mathbf{x}) = \frac{1}{n} \sum_{i = 1}^n f_i(\mathbf{x}). f(x)=n1i=1nfi(x).

目标函数在 x \mathbf{x} x的梯度计算如下:

∇ f ( x ) = 1 n ∑ i = 1 n ∇ f i ( x ) . \nabla f(\mathbf{x}) = \frac{1}{n} \sum_{i = 1}^n \nabla f_i(\mathbf{x}). f(x)=n1i=1nfi(x).

如果使用梯度下降,则每个自变量迭代的计算成本为 O ( n ) \mathcal{O}(n) O(n), 随着 n n n线性增加。 当模型训练数据集很大时,每次迭代的梯度下降成本将非常高。

随机梯度下降 (SGD) 降低了每次迭代的计算成本. 在随机梯度下降的每次迭代中, 我们统一采样了一个索引 i ∈ { 1 , … , n } i\in\{1,\ldots, n\} i{ 1,,n} 来获取数据, 并且计算梯度 ∇ f i ( x ) \nabla f_i(\mathbf{x}) fi(x) 来更新 x \mathbf{x} x:

x ← x − η ∇ f i ( x ) . \mathbf{x} \leftarrow \mathbf{x} - \eta \nabla f_i(\mathbf{x}). xxηfi(x).

η \eta η 是学习速率。 每次迭代的计算成本从 O ( n ) \mathcal{O}(n) O(n) 下降到常数 O ( 1 ) \mathcal{O}(1) O(1). 随机梯度 ∇ f i ( x ) \nabla f_i(\mathbf{x}) fi(x) 是梯度 ∇ f ( x ) \nabla f(\mathbf{x}) f(x)的无偏估计

E i ∇ f i ( x ) = 1 n ∑ i = 1 n ∇ f i ( x ) = ∇ f ( 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}). Eifi(x)=n1i=1nfi(x)=f(x).

平均而言,随机梯度是对梯度的良好估计。

现在,我们将通过向梯度中添加平均值为0,方差为1的随机噪声来模拟SGD,将其与梯度下降进行比较。

f = lambda x1, x2: x1 ** 2 + 2 * x2 ** 2  # 目标函数
gradf = lambda x1, x2: (2 * x1, 4 * x2)  # 辅助函数

trans = lambda x: x.tolist()[0]

def sgd(x1, x2, s1, s2):
    global lr  # 学习率调度
    (g1, g2) = gradf(x1, x2)
    # Simulate noisy gradient
    g1 += np.random.normal(0.0, 1, (1,))
    g2 += np.random.normal(0.0, 1, (1,))
    eta_t = eta * lr()  # 时间t的学习率
    return (trans(x1 - eta_t * g1), trans(x2 - eta_t * g2), 0, 0)  # 更新变量

eta = 0.1
lr = (lambda: 1)  # 保持学习率
show_trace_2d(f, train_2d(sgd, steps=50))

在这里插入图片描述

由于梯度增加了随机性,SGD中变量的轨迹比上一节中在梯度下降中观察到的更加不稳定。这是由于实验所添加的噪声使模拟的随机梯度的准确度下降。在实际中,这些噪声通常指训练数据集中的无意义的干扰。

3.2 动态学习率

使用随时间变化的学习率函数 η ( t ) \eta(t) η(t)可以增加控制优化算法收敛的复杂性。需要搞清楚以何种速度衰减 η \eta η 。如果太快, 将会过早停止优化,如果太慢,就会浪费大量时间。有一些用于调整的基本策略:

η ( t ) = η i  if  t i ≤ t ≤ t i + 1 分 段 常 数 η ( t ) = η 0 ⋅ e − λ t 指 数 η ( t ) = η 0 ⋅ ( β t + 1 ) − α 多 项 式 \begin{aligned} \eta(t) & = \eta_i \text{ if } t_i \leq t \leq t_{i+1} && \mathrm{分段常数} \\ \eta(t) & = \eta_0 \cdot e^{-\lambda t} && \mathrm{指数} \\ \eta(t) & = \eta_0 \cdot (\beta t + 1)^{-\alpha} && \mathrm{多项式} \end{aligned} η(t)η(t)η(t)=ηi if titti+1=η0eλt=η0(βt+1)α

在第一种情况下,例如,每当优化进度停滞时,我们都会降低学习率。这是训练深度网络的常用策略。另外,我们可以通过指数衰减更加主动地降低它。不幸的是,这导致算法收敛之前过早停止。一个流行的选择是多项式衰减 以 α = 0.5 \alpha = 0.5 α=0.5

import math
def exponential():
    global ctr
    ctr += 1
    return math.exp(-0.1 * ctr)

ctr = 1
lr = exponential  # 设置动态更新的学习率
show_trace_2d(f, train_2d(sgd, steps=1000))

在这里插入图片描述

跟预期的相同,参数的差异明显减小。但是,这是以无法收敛到最佳解决方案为代价。即使经过1000步,我们仍然离最佳解决方案还很远。实际上,该算法根本无法收敛。另一方面,如果我们使用多项式衰减,则学习率衰减与步数的平方根成反比,则收敛是好的。

def polynomial():
    global ctr
    ctr += 1
    return (1 + 0.1 * ctr)**(-0.5)

ctr = 1
lr = polynomial  
show_trace_2d(f, train_2d(sgd, steps=50))

在这里插入图片描述

4.参考

https://d2l.ai/chapter_optimization/gd.html

5.代码

github

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!