12.1. 优化与深度学习
在 Colab 中打开 Notebook
在 Colab 中打开 Notebook
在 Colab 中打开 Notebook
在 Colab 中打开 Notebook
在 SageMaker Studio Lab 中打开 Notebook

在本节中,我们将讨论优化与深度学习之间的关系,以及在深度学习中使用优化的挑战。对于一个深度学习问题,我们通常会先定义一个*损失函数*。有了损失函数,我们就可以使用优化算法来尝试最小化损失。在优化中,损失函数通常被称为优化问题的*目标函数*。根据传统和惯例,大多数优化算法都关注于*最小化*。如果我们 कभी需要最大化一个目标,有一个简单的解决方案:只需将目标的符号翻转即可。

12.1.1. 优化的目标

尽管优化为深度学习提供了一种最小化损失函数的方法,但本质上,优化和深度学习的目标是根本不同的。前者主要关注最小化一个目标,而后者则关注在给定有限数据量的情况下找到一个合适的模型。在 3.6节中,我们详细讨论了这两个目标之间的差异。例如,训练误差和泛化误差通常是不同的:由于优化算法的目标函数通常是基于训练数据集的损失函数,因此优化的目标是减少训练误差。然而,深度学习(或更广泛地说,统计推断)的目标是减少泛化误差。为了实现后者,除了使用优化算法来减少训练误差外,我们还需要注意过拟合。

%matplotlib inline
import numpy as np
import torch
from mpl_toolkits import mplot3d
from d2l import torch as d2l
%matplotlib inline
from mpl_toolkits import mplot3d
from mxnet import np, npx
from d2l import mxnet as d2l

npx.set_np()
%matplotlib inline
import numpy as np
import tensorflow as tf
from mpl_toolkits import mplot3d
from d2l import tensorflow as d2l

为了说明上述不同的目标,让我们考虑经验风险和风险。如 4.7.3.1节所述,经验风险是训练数据集上的平均损失,而风险是整个数据群体上的预期损失。下面我们定义两个函数:风险函数 f 和经验风险函数 g。假设我们只有有限数量的训练数据。因此,这里的 g 不如 f 平滑。

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

def g(x):
    return f(x) + 0.2 * torch.cos(5 * np.pi * x)
def f(x):
    return x * np.cos(np.pi * x)

def g(x):
    return f(x) + 0.2 * np.cos(5 * np.pi * x)
def f(x):
    return x * tf.cos(np.pi * x)

def g(x):
    return f(x) + 0.2 * tf.cos(5 * np.pi * x)

下图说明了训练数据集上经验风险的最小值可能与风险(泛化误差)的最小值位于不同的位置。

def annotate(text, xy, xytext):  #@save
    d2l.plt.gca().annotate(text, xy=xy, xytext=xytext,
                           arrowprops=dict(arrowstyle='->'))

x = torch.arange(0.5, 1.5, 0.01)
d2l.set_figsize((4.5, 2.5))
d2l.plot(x, [f(x), g(x)], 'x', 'risk')
annotate('min of\nempirical risk', (1.0, -1.2), (0.5, -1.1))
annotate('min of risk', (1.1, -1.05), (0.95, -0.5))
../_images/output_optimization-intro_70d214_27_0.svg
def annotate(text, xy, xytext):  #@save
    d2l.plt.gca().annotate(text, xy=xy, xytext=xytext,
                           arrowprops=dict(arrowstyle='->'))

x = np.arange(0.5, 1.5, 0.01)
d2l.set_figsize((4.5, 2.5))
d2l.plot(x, [f(x), g(x)], 'x', 'risk')
annotate('min of\nempirical risk', (1.0, -1.2), (0.5, -1.1))
annotate('min of risk', (1.1, -1.05), (0.95, -0.5))
[22:08:11] ../src/storage/storage.cc:196: Using Pooled (Naive) StorageManager for CPU
../_images/output_optimization-intro_70d214_30_1.svg
def annotate(text, xy, xytext):  #@save
    d2l.plt.gca().annotate(text, xy=xy, xytext=xytext,
                           arrowprops=dict(arrowstyle='->'))

x = tf.range(0.5, 1.5, 0.01)
d2l.set_figsize((4.5, 2.5))
d2l.plot(x, [f(x), g(x)], 'x', 'risk')
annotate('min of\nempirical risk', (1.0, -1.2), (0.5, -1.1))
annotate('min of risk', (1.1, -1.05), (0.95, -0.5))
../_images/output_optimization-intro_70d214_33_0.svg

12.1.2. 深度学习中的优化挑战

在本章中,我们将特别关注优化算法在最小化目标函数方面的性能,而不是模型的泛化误差。在 3.1节中,我们区分了优化问题中的解析解和数值解。在深度学习中,大多数目标函数都很复杂,没有解析解。相反,我们必须使用数值优化算法。本章中的优化算法都属于这一类。

深度学习优化中存在许多挑战。其中一些最棘手的问题是局部最小值、鞍点和梯度消失。让我们来看看它们。

12.1.2.1. 局部最小值

对于任何目标函数 \(f(x)\),如果 \(x\) 处的 \(f(x)\) 值小于 \(x\) 附近任何其他点的 \(f(x)\) 值,那么 \(f(x)\) 可能是一个局部最小值。如果 \(x\) 处的 \(f(x)\) 值是整个定义域上目标函数的最小值,那么 \(f(x)\) 是全局最小值。

例如,给定函数

(12.1.1)\[f(x) = x \cdot \textrm{cos}(\pi x) \textrm{ for } -1.0 \leq x \leq 2.0,\]

我们可以近似该函数的局部最小值和全局最小值。

x = torch.arange(-1.0, 2.0, 0.01)
d2l.plot(x, [f(x), ], 'x', 'f(x)')
annotate('local minimum', (-0.3, -0.25), (-0.77, -1.0))
annotate('global minimum', (1.1, -0.95), (0.6, 0.8))
../_images/output_optimization-intro_70d214_39_0.svg
x = np.arange(-1.0, 2.0, 0.01)
d2l.plot(x, [f(x), ], 'x', 'f(x)')
annotate('local minimum', (-0.3, -0.25), (-0.77, -1.0))
annotate('global minimum', (1.1, -0.95), (0.6, 0.8))
../_images/output_optimization-intro_70d214_42_0.svg
x = tf.range(-1.0, 2.0, 0.01)
d2l.plot(x, [f(x), ], 'x', 'f(x)')
annotate('local minimum', (-0.3, -0.25), (-0.77, -1.0))
annotate('global minimum', (1.1, -0.95), (0.6, 0.8))
../_images/output_optimization-intro_70d214_45_0.svg

深度学习模型的目标函数通常有许多局部最优解。当优化问题的数值解接近局部最优解时,最终迭代得到的数值解可能只在*局部*最小化目标函数,而不是*全局*最小化,因为目标函数解的梯度接近或变为零。只有一定程度的噪声才可能将参数从局部最小值中“敲”出来。事实上,这是小批量随机梯度下降的一个有益特性,其中小批量上梯度的自然变化能够将参数从局部最小值中移出。

12.1.2.2. 鞍点

除了局部最小值,鞍点是梯度消失的另一个原因。*鞍点*是函数所有梯度都消失但既不是全局最小值也不是局部最小值的位置。考虑函数 \(f(x) = x^3\)。它的一阶和二阶导数在 \(x=0\) 时都为零。优化可能会在这一点上停滞,即使它不是一个最小值。

x = torch.arange(-2.0, 2.0, 0.01)
d2l.plot(x, [x**3], 'x', 'f(x)')
annotate('saddle point', (0, -0.2), (-0.52, -5.0))
../_images/output_optimization-intro_70d214_51_0.svg
x = np.arange(-2.0, 2.0, 0.01)
d2l.plot(x, [x**3], 'x', 'f(x)')
annotate('saddle point', (0, -0.2), (-0.52, -5.0))
../_images/output_optimization-intro_70d214_54_0.svg
x = tf.range(-2.0, 2.0, 0.01)
d2l.plot(x, [x**3], 'x', 'f(x)')
annotate('saddle point', (0, -0.2), (-0.52, -5.0))
../_images/output_optimization-intro_70d214_57_0.svg

高维空间中的鞍点更加隐蔽,如下面的例子所示。考虑函数 \(f(x, y) = x^2 - y^2\)。它的鞍点在 \((0, 0)\)。这一点对于 \(y\) 来说是最大值,对于 \(x\) 来说是最小值。而且,它*看起来*像一个马鞍,这个数学性质也因此得名。

x, y = torch.meshgrid(
    torch.linspace(-1.0, 1.0, 101), torch.linspace(-1.0, 1.0, 101))
z = x**2 - y**2

ax = d2l.plt.figure().add_subplot(111, projection='3d')
ax.plot_wireframe(x, y, z, **{'rstride': 10, 'cstride': 10})
ax.plot([0], [0], [0], 'rx')
ticks = [-1, 0, 1]
d2l.plt.xticks(ticks)
d2l.plt.yticks(ticks)
ax.set_zticks(ticks)
d2l.plt.xlabel('x')
d2l.plt.ylabel('y');
../_images/output_optimization-intro_70d214_63_0.svg
x, y = np.meshgrid(
    np.linspace(-1.0, 1.0, 101), np.linspace(-1.0, 1.0, 101))
z = x**2 - y**2

ax = d2l.plt.figure().add_subplot(111, projection='3d')
ax.plot_wireframe(x.asnumpy(), y.asnumpy(), z.asnumpy(),
                  **{'rstride': 10, 'cstride': 10})
ax.plot([0], [0], [0], 'rx')
ticks = [-1, 0, 1]
d2l.plt.xticks(ticks)
d2l.plt.yticks(ticks)
ax.set_zticks(ticks)
d2l.plt.xlabel('x')
d2l.plt.ylabel('y');
../_images/output_optimization-intro_70d214_66_0.svg
x, y = tf.meshgrid(
    tf.linspace(-1.0, 1.0, 101), tf.linspace(-1.0, 1.0, 101))
z = x**2 - y**2

ax = d2l.plt.figure().add_subplot(111, projection='3d')
ax.plot_wireframe(x, y, z, **{'rstride': 10, 'cstride': 10})
ax.plot([0], [0], [0], 'rx')
ticks = [-1, 0, 1]
d2l.plt.xticks(ticks)
d2l.plt.yticks(ticks)
ax.set_zticks(ticks)
d2l.plt.xlabel('x')
d2l.plt.ylabel('y');
../_images/output_optimization-intro_70d214_69_0.svg

我们假设一个函数的输入是 \(k\) 维向量,输出是标量,所以它的Hessian矩阵将有 \(k\) 个特征值。在函数梯度为零的位置,函数的解可能是局部最小值、局部最大值或鞍点。

  • 当函数在梯度为零位置的Hessian矩阵的特征值全部为正时,我们得到函数的局部最小值。

  • 当函数在梯度为零位置的Hessian矩阵的特征值全部为负时,我们得到函数的局部最大值。

  • 当函数在梯度为零位置的Hessian矩阵的特征值有正有负时,我们得到函数的鞍点。

对于高维问题,至少*一些*特征值为负的可能性相当高。这使得鞍点比局部最小值更有可能出现。在下一节介绍凸性时,我们将讨论这种情况的一些例外。简而言之,凸函数是那些Hessian矩阵的特征值永不为负的函数。然而,遗憾的是,大多数深度学习问题都不属于这一类。尽管如此,它仍然是研究优化算法的一个很好的工具。

12.1.2.3. 梯度消失

可能最棘手的问题是梯度消失。回想一下我们在 5.1.2节中常用的激活函数及其导数。例如,假设我们想要最小化函数 \(f(x) = \tanh(x)\),并且我们恰好从 \(x = 4\) 开始。正如我们所见,\(f\) 的梯度接近于零。更具体地说,\(f'(x) = 1 - \tanh^2(x)\),因此 \(f'(4) = 0.0013\)。因此,在我们取得进展之前,优化会停滞很长时间。这被证明是引入ReLU激活函数之前训练深度学习模型相当棘手的原因之一。

x = torch.arange(-2.0, 5.0, 0.01)
d2l.plot(x, [torch.tanh(x)], 'x', 'f(x)')
annotate('vanishing gradient', (4, 1), (2, 0.0))
../_images/output_optimization-intro_70d214_75_0.svg
x = np.arange(-2.0, 5.0, 0.01)
d2l.plot(x, [np.tanh(x)], 'x', 'f(x)')
annotate('vanishing gradient', (4, 1), (2, 0.0))
../_images/output_optimization-intro_70d214_78_0.svg
x = tf.range(-2.0, 5.0, 0.01)
d2l.plot(x, [tf.tanh(x)], 'x', 'f(x)')
annotate('vanishing gradient', (4, 1), (2, 0.0))
../_images/output_optimization-intro_70d214_81_0.svg

正如我们所看到的,深度学习的优化充满了挑战。幸运的是,有一系列强大的算法表现良好,即使对于初学者也很容易使用。此外,并不一定需要找到*最佳*解。局部最优解甚至其近似解仍然非常有用。

12.1.3. 小结

  • 最小化训练误差*并不能*保证我们找到最小化泛化误差的最佳参数集。

  • 优化问题可能有许多局部最小值。

  • 问题可能存在更多的鞍点,因为问题通常不是凸的。

  • 梯度消失可能导致优化停滞。通常对问题进行重新参数化会有所帮助。良好的参数初始化也可能是有益的。

12.1.4. 练习

  1. 考虑一个简单的多层感知机,它有一个隐藏层,比如说隐藏层有 \(d\) 个维度和一个输出。证明对于任何局部最小值,至少存在 \(d!\) 个表现完全相同的等价解。

  2. 假设我们有一个对称随机矩阵 \(\mathbf{M}\),其中元素 \(M_{ij} = M_{ji}\) 各自从某个概率分布 \(p_{ij}\) 中抽取。此外,假设 \(p_{ij}(x) = p_{ij}(-x)\),即分布是对称的(详情参见例如 Wigner (1958))。

    1. 证明特征值的分布也是对称的。也就是说,对于任何特征向量 \(\mathbf{v}\),其关联的特征值 \(\lambda\) 满足 \(P(\lambda > 0) = P(\lambda < 0)\)

    2. 为什么以上*并不*意味着 \(P(\lambda > 0) = 0.5\)

  3. 你能想到深度学习优化中还涉及哪些其他挑战吗?

  4. 假设你想在一个(真实的)马鞍上平衡一个(真实的)球。

    1. 为什么这很难?

    2. 你能否将这种效应也用于优化算法?