pytorch实现梯度下降、随机梯度下降-图像直观展示

深度学习与优化算法原理

优化函数与深度学习

  • 在一个深度学习问题中,通常需要预先定义一个损失函数。
  • 有了损失函数以后,使用优化算法试图将其最小化。

在优化中,这样的损失函数通常被称作优化问题的目标函数(objective function)。

  • 依据惯例,优化算法通常只考虑最小化目标函数。
  • 其实,任何最大化问题都可以很容易地转化为最小化问题,只需令目标函数的相反数为新的目标函数即可

虽然优化为深度学习提供了最小化损失函数的方法,但本质上,优化与深度学习的目标是有区别的。

训练误差和泛化误差是不同的:

  • 优化算法的目标函数通常是一个基于训练数据集的损失函数,优化的目标在于降低训练误差。
  • 而深度学习的目标在于降低泛化误差。为了降低泛化误差,除了使用优化算法降低训练误差以外,还需要注意应对过拟合

深度学习优化的问题

深度学习中绝大多数目标函数都很复杂。因此,很多优化问题并不存在解析解,而需要使用基于数值方法的优化算法找到近似解,即数值解。为了求得最小化目标函数的数值解,我们将通过优化算法有限次迭代模型参数来尽可能降低损失函数的值

常见的优化在深度学习中的挑战,有局部最小值和鞍点等。

局部最小值

对于目标函数 f ( x ) f(x) f(x),如果 f ( x ) f(x) f(x) x x x上的值比在 x x x邻近的其他点的值更小,那么 f ( x ) f(x) f(x)可能是一个局部最小值(local minimum)。如果 f ( x ) f(x) f(x) x x x上的值是目标函数在整个定义域上的最小值,那么 f ( x ) f(x) f(x)是全局最小值(global minimum)。

举个例子,给定函数

f ( x ) = x ⋅ cos ( π x ) , − 1.0 ≤ x ≤ 2.0 , f(x) = x \cdot \text{cos}(\pi x), \qquad -1.0 \leq x \leq 2.0, f(x)=xcos(πx),1.0x2.0,

我们可以大致找出该函数的局部最小值和全局最小值的位置。需要注意的是,图中箭头所指示的只是大致位置。

%matplotlib inline
import sys
sys.path.append("..") 
from mpl_toolkits import mplot3d # 三维画图
from matplotlib import pyplot as plt
import numpy as np

def f(x):
    return x * np.cos(np.pi * x)

def set_figsize(figsize=(3.5, 2.5)):
    use_svg_display()
    # 设置图的尺寸
    plt.rcParams['figure.figsize'] = figsize


set_figsize((4.5, 2.5))
x = np.arange(-1.0, 2.0, 0.1)
fig,  = plt.plot(x, f(x))
fig.axes.annotate('local minimum', xy=(-0.3, -0.25), xytext=(-0.77, -1.0),
                  arrowprops=dict(arrowstyle='->'))
fig.axes.annotate('global minimum', xy=(1.1, -0.95), xytext=(0.6, 0.8),
                  arrowprops=dict(arrowstyle='->'))
plt.xlabel('x')
plt.ylabel('f(x)');

深度学习模型的目标函数可能有若干局部最优值。当一个优化问题的数值解在局部最优解附近时

  • 由于目标函数有关解的梯度接近或变成零
  • 最终迭代求得的数值解可能只令目标函数局部最小化而非全局最小化

鞍点

梯度接近或变成零可能是由于当前解在局部最优解附近造成的,另一种可能性是当前解在鞍点(saddle point)附近。

例如,给定函数

f ( x ) = x 3 , f(x) = x^3, f(x)=x3,

我们可以找出该函数的鞍点位置。

x = np.arange(-2.0, 2.0, 0.1)
fig, = plt.plot(x, x**3)
fig.axes.annotate('saddle point', xy=(0, -0.2), xytext=(-0.52, -5.0),
                  arrowprops=dict(arrowstyle='->'))
plt.xlabel('x')
plt.ylabel('f(x)');

又例如下面这个定义在二维空间的函数的例子,

f ( x , y ) = x 2 − y 2 . f(x, y) = x^2 - y^2. f(x,y)=x2y2.

我们可以找出该函数的鞍点位置。

x, y = np.mgrid[-1: 1: 31j, -1: 1: 31j]
z = x**2 - y**2

ax = plt.figure().add_subplot(111, projection='3d')
ax.plot_wireframe(x, y, z, **{'rstride': 2, 'cstride': 2})
ax.plot([0], [0], [0], 'rx')
ticks = [-1,  0, 1]
plt.xticks(ticks)
plt.yticks(ticks)
ax.set_zticks(ticks)
plt.xlabel('x')
plt.ylabel('y');
  • 该函数看起来像一个马鞍,而鞍点恰好是马鞍上可坐区域的中心
  • 在图的鞍点位置,目标函数在 x x x轴方向上是局部最小值,但在 y y y轴方向上是局部最大值。

假设一个函数的输入为 k k k维向量,输出为标量,那么它的海森矩阵(Hessian matrix)有 k k k个特征值。该函数在梯度为0的位置上可能是局部最小值、局部最大值或者鞍点。

  • 当函数的海森矩阵在梯度为零的位置上的特征值全为正时,该函数得到局部最小值。
  • 当函数的海森矩阵在梯度为零的位置上的特征值全为负时,该函数得到局部最大值。
  • 当函数的海森矩阵在梯度为零的位置上的特征值有正有负时,该函数得到鞍点。

随机矩阵理论告诉我们,对于一个大的高斯随机矩阵来说,任一特征值是正或者是负的概率都是0.5。那么,以上第一种情况的概率为 0. 5 k 0.5^k 0.5k。由于深度学习模型参数通常都是高维的( k k k很大),目标函数的鞍点通常比局部最小值更常见。

在深度学习中,虽然找到目标函数的全局最优解很难,但这并非是必要的。

梯度下降和随机梯度下降

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

一维梯度下降

先以简单的一维梯度下降为例。

  • 假设连续可导的函数 f : R → R f: \mathbb{R} \rightarrow \mathbb{R} f:RR的输入和输出都是标量
  • 给定绝对值足够小的数 ϵ \epsilon ϵ

根据泰勒展开公式,我们得到以下的近似:

f ( x + ϵ ) ≈ f ( x ) + ϵ f ′ ( x ) . f(x + \epsilon) \approx f(x) + \epsilon f'(x) . f(x+ϵ)f(x)+ϵf(x).

这里 f ′ ( x ) f'(x) f(x)是函数 f f f x x x处的梯度。一维函数的梯度是一个标量,也称导数。

接下来,找到一个常数 η > 0 \eta > 0 η>0,使得 ∣ η f ′ ( x ) ∣ \left|\eta f'(x)\right| ηf(x)足够小,那么可以将 ϵ \epsilon ϵ替换为 − η f ′ ( x ) -\eta f'(x) ηf(x)并得到

f ( x − η f ′ ( x ) ) ≈ f ( x ) − η f ′ ( x ) 2 . f(x - \eta f'(x)) \approx f(x) - \eta f'(x)^2. f(xηf(x))f(x)ηf(x)2.

如果导数 f ′ ( x ) ≠ 0 f'(x) \neq 0 f(x)=0,那么 η f ′ ( x ) 2 > 0 \eta f'(x)^2>0 ηf(x)2>0,所以

f ( x − η f ′ ( x ) ) ≲ f ( x ) . f(x - \eta f'(x)) \lesssim 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 ) 2 f'(x)^2 f(x)2的值已足够小或迭代次数已达到某个值。

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

%matplotlib inline
import numpy as np
import torch
import math
import sys
sys.path.append("..") 

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 x=10 x=10作为初始值,并设 η = 0.2 \eta=0.2 η=0.2。使用梯度下降对 x x x迭代10次,可见最终 x x x的值较接近最优解。

绘制出自变量 x x x的迭代轨迹:

def show_trace(res):
    n = max(abs(min(res)), abs(max(res)), 10)
    f_line = np.arange(-n, n, 0.1)
    set_figsize()
    plt.plot(f_line, [x * x for x in f_line])
    plt.plot(res, [x * x for x in res], '-o')
    plt.xlabel('x')
    plt.ylabel('f(x)')

show_trace(res)

学习率

上述梯度下降算法中的正数 η \eta η通常叫作学习率。这是一个超参数,需要人工设定。如果使用过小的学习率,会导致 x x x更新缓慢从而需要更多的迭代才能得到较好的解。

下面展示使用学习率 η = 0.05 \eta=0.05 η=0.05时自变量 x x x的迭代轨迹

show_trace(gd(0.05))

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

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

show_trace(gd(1.1))

维度增加:多维梯度下降

在了解了一维梯度下降之后,我们再考虑一种更广义的情况:

  • 目标函数的输入为向量
  • 输出为标量。

假设目标函数 f : R d → R f: \mathbb{R}^d \rightarrow \mathbb{R} f:RdR的输入是一个 d d d维向量 x = [ x 1 , x 2 , … , x d ] ⊤ \boldsymbol{x} = [x_1, x_2, \ldots, x_d]^\top x=[x1,x2,,xd]。目标函数 f ( x ) f(\boldsymbol{x}) f(x)有关 x \boldsymbol{x} x的梯度是一个由 d d d个偏导数组成的向量:

∇ x f ( x ) = [ ∂ f ( x ) ∂ x 1 , ∂ f ( x ) ∂ x 2 , … , ∂ f ( x ) ∂ x d ] ⊤ . \nabla_{\boldsymbol{x}} f(\boldsymbol{x}) = \bigg[\frac{\partial f(\boldsymbol{x})}{\partial x_1}, \frac{\partial f(\boldsymbol{x})}{\partial x_2}, \ldots, \frac{\partial f(\boldsymbol{x})}{\partial x_d}\bigg]^\top. xf(x)=[x1f(x),x2f(x),,xdf(x)].

为表示简洁,我们用 ∇ f ( x ) \nabla f(\boldsymbol{x}) f(x)代替 ∇ x f ( x ) \nabla_{\boldsymbol{x}} f(\boldsymbol{x}) xf(x)。梯度中每个偏导数元素 ∂ f ( x ) / ∂ x i \partial f(\boldsymbol{x})/\partial x_i f(x)/xi代表着 f f f x \boldsymbol{x} x有关输入 x i x_i xi的变化率。为了测量 f f f沿着单位向量 u \boldsymbol{u} u(即 ∣ u ∣ = 1 |\boldsymbol{u}|=1 u=1)方向上的变化率,在多元微积分中,我们定义 f f f x \boldsymbol{x} x上沿着 u \boldsymbol{u} u方向的方向导数为

D u f ( x ) = lim ⁡ h → 0 f ( x + h u ) − f ( x ) h . \text{D}_{\boldsymbol{u}} f(\boldsymbol{x}) = \lim_{h \rightarrow 0} \frac{f(\boldsymbol{x} + h_ \boldsymbol{u}) - f(\boldsymbol{x})}{h}. Duf(x)=h0limhf(x+hu)f(x).

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

D u f ( x ) = ∇ f ( x ) ⋅ u ( 向 量 相 乘 ) \text{D}_{\boldsymbol{u}} f(\boldsymbol{x}) = \nabla f(\boldsymbol{x}) \cdot \boldsymbol{u}(向量相乘) Duf(x)=f(x)u()

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

由于 D u f ( x ) = ∣ ∇ f ( x ) ∣ ⋅ ∣ u ∣ ⋅ cos ( θ ) = ∣ ∇ f ( x ) ∣ ⋅ cos ( θ ) \text{D}_{\boldsymbol{u}} f(\boldsymbol{x}) = |\nabla f(\boldsymbol{x})| \cdot |\boldsymbol{u}| \cdot \text{cos} (\theta) = |\nabla f(\boldsymbol{x})| \cdot \text{cos} (\theta) Duf(x)=f(x)ucos(θ)=f(x)cos(θ), 其中 θ \theta θ为梯度 ∇ f ( x ) \nabla f(\boldsymbol{x}) f(x)和单位向量 u \boldsymbol{u} u之间的夹角,当 θ = π \theta = \pi θ=π时, cos ( θ ) \text{cos}(\theta) cos(θ)取得最小值 − 1 -1 1。因此,当 u \boldsymbol{u} u在梯度方向 ∇ f ( x ) \nabla f(\boldsymbol{x}) f(x)的相反方向时,方向导数 D u f ( x ) \text{D}_{\boldsymbol{u}} f(\boldsymbol{x}) Duf(x)被最小化。因此,我们可能通过梯度下降算法来不断降低目标函数 f f f的值:

x ← x − η ∇ f ( x ) . \boldsymbol{x} \leftarrow \boldsymbol{x} - \eta \nabla f(\boldsymbol{x}). xxηf(x).

同样,其中 η \eta η(取正数)称作学习率。

下面我们构造一个目标函数

  • 输入为二维向量 x = [ x 1 , x 2 ] ⊤ \boldsymbol{x} = [x_1, x_2]^\top x=[x1,x2]
  • 输出为标量
  • f ( x ) = x 1 2 + 2 x 2 2 f(\boldsymbol{x})=x_1^2+2x_2^2 f(x)=x12+2x22

那么,梯度 ∇ f ( x ) = [ 2 x 1 , 4 x 2 ] ⊤ \nabla f(\boldsymbol{x}) = [2x_1, 4x_2]^\top f(x)=[2x1,4x2]。我们将观察梯度下降从初始位置 [ − 5 , − 2 ] [-5,-2] [5,2]开始对自变量 x \boldsymbol{x} x的迭代轨迹。

先定义两个辅助函数,第一个函数使用给定的自变量更新函数,从初始位置 [ − 5 , − 2 ] [-5,-2] [5,2]开始迭代自变量 x \boldsymbol{x} x共20次,第二个函数对自变量 x \boldsymbol{x} x的迭代轨迹进行可视化。

def train_2d(trainer):  
    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):  
    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))
    plt.contour(x1, x2, f(x1, x2), colors='#1f77b4')
    plt.xlabel('x1')
    plt.ylabel('x2')

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

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))

随机梯度下降

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

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

目标函数在 x \boldsymbol{x} x处的梯度计算为

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

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

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

x ← x − η ∇ f i ( x ) . \boldsymbol{x} \leftarrow \boldsymbol{x} - \eta \nabla f_i(\boldsymbol{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(\boldsymbol{x}) fi(x)是对梯度 ∇ f ( x ) \nabla f(\boldsymbol{x}) f(x)的无偏估计:

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

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

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

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))

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

  • 0
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
PyTorch实现随机梯度下降(SGD)的方式是通过optim包中的SGD优化器。具体来说,可以按照以下步骤实现随机梯度下降: 1. 导入必要的库函数 ```python import torch import torch.optim as optim ``` 2. 定义模型和损失函数 ```python model = YourModel() loss_fn = YourLossFunction() ``` 3. 定义优化器 ```python optimizer = optim.SGD(model.parameters(), lr=learning_rate) ``` 其中,`model.parameters()`表示获取模型中的所有可训练参数,`lr`表示学习率。 4. 进行训练 ```python for epoch in range(num_epochs): for batch_idx, (data, target) in enumerate(train_loader): optimizer.zero_grad() # 每个batch之前需要将梯度清零 output = model(data) loss = loss_fn(output, target) loss.backward() optimizer.step() ``` 其中,`train_loader`表示数据集的数据加载器,`optimizer.zero_grad()`表示清除梯度,`loss.backward()`表示计算损失函数的梯度,`optimizer.step()`表示更新模型参数。 5. 在测试集上进行测试 ```python model.eval() with torch.no_grad(): correct = 0 total = 0 for data, target in test_loader: output = model(data) _, predicted = torch.max(output.data, 1) total += target.size(0) correct += (predicted == target).sum().item() print('Accuracy of the network on the test images: %d %%' % (100 * correct / total)) ``` 其中,`model.eval()`表示将模型切换为测试模式,`torch.no_grad()`表示关闭梯度计算,`torch.max()`表示获取输出张量中最大值的索引。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值