Run this notebook online:Binder or Colab: Colab

11.1. 优化和深度学习

在本节中,我们将讨论优化与深度学习之间的关系以及在深度学习中使用优化的挑战。对于深度学习问题,我们通常会先定义损失函数。一旦我们有了损失函数,我们就可以使用优化算法来尝试最小化损失。在优化中,损失函数通常被称为优化问题的目标函数。按照传统和惯则,大多数优化算法都关注的是最小化。如果我们需要最大化目标,那么有一个简单的解决方案:在目标函数前加负号即可。

11.1.1. 优化的目标

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

%load ../utils/djl-imports
%load ../utils/plot-utils
import org.apache.commons.lang3.ArrayUtils;

下图说明,训练数据集的最低经验风险可能与最低风险(泛化误差)不同。

// Saved in Functions class for later use
public float[] callFunc(float[] x, Function<Float, Float> func) {
    float[] y = new float[x.length];
    for (int i = 0; i < x.length; i++) {
        y[i] = func.apply(x[i]);
    }
    return y;
}

Function<Float, Float> f = x -> x * (float)Math.cos(Math.PI * x);

Function<Float, Float> g = x -> f.apply(x) + 0.2f * (float)Math.cos(5 * Math.PI * x);

NDManager manager = NDManager.newBaseManager();

NDArray X = manager.arange(0.5f, 1.5f, 0.01f);
float[] x = X.toFloatArray();
float[] fx = callFunc(x, f);
float[] gx = callFunc(x, g);

String[] grouping = new String[x.length * 2];
for (int i = 0; i < x.length; i++) {
    grouping[i] = "Expected Risk";
    grouping[i + x.length] = "Empirical Risk";
}

Table data = Table.create("Data")
    .addColumns(
        FloatColumn.create("x", ArrayUtils.addAll(x, x)),
        FloatColumn.create("risk", ArrayUtils.addAll(fx, gx)),
        StringColumn.create("grouping", grouping)
    );

LinePlot.create("Risk", data, "x", "risk", "grouping");

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

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

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

11.1.2.1. 局部最小值

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

例如,给定函数

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

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

NDArray X = manager.arange(-1.0f, 2.0f, 0.01f);
float[] x = X.toFloatArray();
float[] fx = callFunc(x, f);

Table data = Table.create("Data")
.addColumns(
    FloatColumn.create("x", x),
    FloatColumn.create("f(x)", fx)
);

LinePlot.create("x * cos(pi * x)", data, "x", "f(x)");

深度学习模型的目标函数通常有许多局部最优解。当优化问题的数值解接近局部最优值时,随着目标函数解的梯度接近或变为零,通过最终迭代获得的数值解可能仅使目标函数局部最优,而不是全局最优。只有一定程度的噪声可能会使参数超出局部最小值。事实上,这是小批量随机梯度下降的有利特性之一,在这种情况下,小批量上梯度的自然变化能够将参数从局部极小值中移出。

11.1.2.2. 鞍点

除了局部最小值之外,鞍点也是梯度消失的另一个原因。鞍点(saddle point)是指函数的所有梯度都消失但既不是全局最小值也不是局部最小值的任何位置。考虑这个函数\(f(x) = x^3\)。它的一阶和二阶导数在\(x=0\)时消失。这时优化可能会停止,尽管它不是最小值。

Function<Float, Float> cube = x -> x * x * x;

NDArray X = manager.arange(-2.0f, 2.0f, 0.01f);
float[] x = X.toFloatArray();
float[] fx = callFunc(x, cube);

Table data = Table.create("Data")
.addColumns(
    FloatColumn.create("x", x),
    FloatColumn.create("f(x)", fx)
);

LinePlot.create("x^3", data, "x", "f(x)");

如下例所示,较高维度的鞍点甚至更加隐蔽。考虑这个函数\(f(x, y) = x^2 - y^2\)。它的鞍点为\((0, 0)\)。这是关于\(y\)的最大值,也是关于\(x\)的最小值。此外,它看起来像马鞍,这就是这个数学属性的名字由来。

https://djl-ai.s3.amazonaws.com/resources/d2l-java/higher_dim_saddle.svg

Fig. 11.1.1 Higher Dimension Saddle.

我们假设函数的输入是\(k\)维向量,其输出是标量,因此其Hessian矩阵(也称黑塞矩阵)将有\(k\)特征值(参考online appendix on eigendecompositions。函数的解决方案可以是局部最小值、局部最大值或函数梯度为零的位置处的鞍点:

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

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

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

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

11.1.2.3. 梯度消失

可能遇到的最隐蔽的问题是梯度消失。回想一下我们在 subsec_activation-functions中常用的激活函数及其衍生函数。例如,假设我们想最小化函数\(f(x) = \tanh(x)\),然后我们恰好从\(x = 4\)开始。正如我们所看到的那样,\(f\)的梯度接近零。更具体地说,\(f'(x) = 1 - \tanh^2(x)\),因此是\(f'(4) = 0.0013\)。因此,在我们取得进展之前,优化将会停滞很长一段时间。事实证明,这是在引入ReLU激活函数之前训练深度学习模型相当棘手的原因之一。

Function<Float, Float> tanh = x -> (float)Math.tanh(x);

NDArray X = manager.arange(-2.0f, 5.0f, 0.01f);
float[] x = X.toFloatArray();
float[] fx = callFunc(x, tanh);

Table data = Table.create("Data")
.addColumns(
    FloatColumn.create("x", x),
    FloatColumn.create("f(x)", fx)
);

LinePlot.create("tanh", data, "x", "f(x)");

正如我们所看到的那样,深度学习的优化充满挑战。幸运的是,有一系列强大的算法表现良好,即使对于初学者也很容易使用。此外,没有必要找到最佳解决方案。局部最优解或其近似解仍然非常有用。

11.1.3. 小结

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

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

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

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

11.1.4. 练习

  1. 考虑一个简单的的MLP,它有一个隐藏层,比如,隐藏层中维度为\(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)\)的概率为\(P(\lambda > 0) = P(\lambda < 0)\)

    2. 为什么以上没有暗示\(P(\lambda > 0) = 0.5\)

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

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

    1. 为什么这很难?

    2. 你也能利用这种效应来优化算法吗?