Run this notebook online:Binder or Colab: Colab

2.4. 微分

在2500年前,古希腊人把一个多边形分成三角形,并把它们的面积相加,才找到计算多边形面积的方法。 为了求出曲线形状(比如圆)的面积,古希腊人在这样的形状上刻内接多边形。如 fig_circle_area 所示,内接多边形的等长边越多,就越接近圆。这个过程也被称为 逼近法(method of exhaustion)。

用逼近法求圆的面积。 .. _fig_circle_area:

事实上,逼近法就是 积分(integral calculus)的起源,我们将在 sec_integral_calculus 中详细描述。2000 多年后,微积分的另一支,微分(differential calculus),被发明出来。在微分学最重要的应用是优化问题,即考虑如何把事情做到最好。正如在 Section 2.3.10.1 中讨论的那样,这种问题在深度学习中是无处不在的。

在深度学习中,我们“训练”模型,不断更新它们,使它们在看到越来越多的数据时变得越来越好。通常情况下,变得更好意味着最小化一个 损失函数(loss function),即一个衡量“我们的模型有多糟糕”这个问题的分数。这个问题比看上去要微妙得多。最终,我们真正关心的是生成一个能够在我们从未见过的数据上表现良好的模型。但我们只能将模型与我们实际能看到的数据相拟合。因此,我们可以将拟合模型的任务分解为两个关键问题:(1)优化(optimization):用模型拟合观测数据的过程;(2)泛化(generalization):数学原理和实践者的智慧,能够指导我们生成出有效性超出用于训练的数据集本身的模型。

为了帮助你在后面的章节中更好地理解优化问题和方法,这里我们对深度学习中常用的微分知识提供了一个非常简短的入门教程。

2.4.1. 导数和微分

我们首先讨论导数的计算,这是几乎所有深度学习优化算法的关键步骤。在深度学习中,我们通常选择对于模型参数可微的损失函数。简而言之,这意味着,对于每个参数, 如果我们把这个参数增加减少一个无穷小的量,我们可以知道损失会以多快的速度增加或减少,

假设我们有一个函数 \(f: \mathbb{R} \rightarrow \mathbb{R}\),其输入和输出都是标量。(:math:`f` 的 *导数* 被定义为)

(**

(2.4.1)\[f'(x) = \lim_{h \rightarrow 0} \frac{f(x+h) - f(x)}{h},\]

**) :label: eq_derivative

如果这个极限存在。如果\(f'(a)\)存在,则称\(f\)\(a\)处是可微(differentiable)的。如果 \(f\) 在一个区间内的每个数上都是可微的,则此函数在此区间中是可微的。我们可以将 eq_derivative 中的导数 \(f'(x)\) 解释为\(f(x)\)相对于 \(x\)瞬时(instantaneous) 变化率。所谓的瞬时变化率是基于\(x\)中的变化\(h\),且\(h\)接近\(0\)

为了更好地解释导数,让我们用一个例子来做实验。 (定义:math:`u = f(x) = 3x^2-4x`.)

注意:为了提高精度我们在本章中使用``Double``。而在深度学习当中通常使用``Float``

%load ../utils/djl-imports
%load ../utils/plot-utils
%load ../utils/Functions.java
NDManager manager = NDManager.newBaseManager();
Function<Double, Double> f = x -> (3 * Math.pow(x, 2) -4 * x);

通过令 \(x=1\) 并让 \(h\) 接近 \(0\)eq_derivative 中(:math:`frac{f(x+h) - f(x)}{h}` 的数值结果接近 :math:`2`)。虽然这个实验不是一个数学证明,但我们稍后会看到,当 \(x=1\)时,导数 \(u'\)\(2\)

public Double numericalLim(Function<Double, Double> f, double x, double h) {
    return (f.apply(x+h) - f.apply(x)) / h;
}

double h = 0.1;
for (int i=0; i < 5; i++) {
    System.out.println("h=" + String.format("%.5f", h) + ", numerical limit="
                       + String.format("%.5f", numericalLim(f, 1, h)));
    h *= 0.1;
}
h=0.10000, numerical limit=2.30000
h=0.01000, numerical limit=2.03000
h=0.00100, numerical limit=2.00300
h=0.00010, numerical limit=2.00030
h=0.00001, numerical limit=2.00003

让我们熟悉一下导数的几个等价符号。 给定 \(y = f(x)\),其中 \(x\)\(y\) 分别是函数 \(f\) 的自变量和因变量。以下表达式是等价的:

(2.4.2)\[f'(x) = y' = \frac{dy}{dx} = \frac{df}{dx} = \frac{d}{dx} f(x) = Df(x) = D_x f(x),\]

其中符号 \(\frac{d}{dx}\)\(D\)微分运算符,表示微分操作。我们可以使用以下规则来对常见函数求微分:

  • \(DC = 0\)\(C\) 是一个常数)

  • \(Dx^n = nx^{n-1}\)幂律(power rule), \(n\)是任意实数)

  • \(De^x = e^x\)

  • \(D\ln(x) = 1/x\)

为了微分一个由一些简单函数(如上面的常见函数)组成的函数,下面的法则使用起来很方便。 假设函数\(f\)\(g\)都是可微的,\(C\)是一个常数,我们有:

常数相乘法则

(2.4.3)\[\frac{d}{dx} [Cf(x)] = C \frac{d}{dx} f(x),\]

加法法则

(2.4.4)\[\frac{d}{dx} [f(x) + g(x)] = \frac{d}{dx} f(x) + \frac{d}{dx} g(x),\]

乘法法则

(2.4.5)\[\frac{d}{dx} [f(x)g(x)] = f(x) \frac{d}{dx} [g(x)] + g(x) \frac{d}{dx} [f(x)],\]

除法法则

(2.4.6)\[\frac{d}{dx} \left[\frac{f(x)}{g(x)}\right] = \frac{g(x) \frac{d}{dx} [f(x)] - f(x) \frac{d}{dx} [g(x)]}{[g(x)]^2}.\]

现在我们可以应用上述几个法则来计算 \(u' = f'(x) = 3 \frac{d}{dx} x^2-4\frac{d}{dx}x = 6x-4\)。因此,通过令 \(x = 1\) ,我们有 \(u' = 2\) :这一点得到了我们在本节前面的实验的支持,在这个实验中,数值结果接近\(2\)。当 \(x=1\) 时,此导数也是曲线 \(u = f(x)\) 切线的斜率。

为了对导数的这种解释进行可视化,我们将使用 plotly,这是一个中流行的绘图库。我们使用自定义的plotLineAndSegment()函数配置图表的属性,通过Tablsaw来控制plotly绘图。 plotLineAndSegment()函数接受三个数组作为输入,第一个数组是 x 轴数据,第二个数组是曲线的 y 轴数据,第三个数组是切线(虚线)的 y 轴数据。此外我们还需要提供曲线的名称, x/y轴的标签以及图表大小。通过plotLineAndSegment()函数,我们可以轻松的在同一图表中绘制多条曲线。 在本书后续的章节中,我们会使用类似的函数在Jupyter中可视化许多曲线。

public Figure plotLineAndSegment(double[] x, double[] y, double[] segment,
                                 String trace1Name, String trace2Name,
                                 String xLabel, String yLabel,
                                 int width, int height) {
    ScatterTrace trace = ScatterTrace.builder(x, y)
        .mode(ScatterTrace.Mode.LINE)
        .name(trace1Name)
        .build();

    ScatterTrace trace2 = ScatterTrace.builder(x, segment)
        .mode(ScatterTrace.Mode.LINE)
        .name(trace2Name)
        .build();

    Layout layout = Layout.builder()
        .height(height)
        .width(width)
        .showLegend(true)
        .xAxis(Axis.builder().title(xLabel).build())
        .yAxis(Axis.builder().title(yLabel).build())
        .build();

    return new Figure(layout, trace, trace2);
}

现在我们可以[绘制函数 :math:`u = f(x)` 及其在 :math:`x=1` 处的切线 :math:`y = 2x - 3`],其中系数\(2\)是切线的斜率。

NDArray X = manager.arange(0f, 3f, 0.1f, DataType.FLOAT64);
double[] x = X.toDoubleArray();

double[] fx = new double[x.length];
for (int i=0; i < x.length; i++) {
    fx[i] = f.apply(x[i]);
}


double[] fg = new double[x.length];
for (int i=0; i < x.length; i++) {
    fg[i] = 2*x[i]-3;
}

plotLineAndSegment(x, fx, fg, "f(x)", "Tangent line(x=1)", "x", "f(x)", 700, 500)

2.4.2. 偏导数

到目前为止,我们只讨论了仅含一个变量的函数的微分。在深度学习中,函数通常依赖于许多变量。因此,我们需要将微分的思想推广到这些 多元函数 (multivariate function)上。

\(y = f(x_1, x_2, \ldots, x_n)\) 是一个具有 \(n\) 个变量的函数。\(y\) 关于第\(i\) 个参数\(x_i\)偏导数(partial derivative)为:

(2.4.7)\[\frac{\partial y}{\partial x_i} = \lim_{h \rightarrow 0} \frac{f(x_1, \ldots, x_{i-1}, x_i+h, x_{i+1}, \ldots, x_n) - f(x_1, \ldots, x_i, \ldots, x_n)}{h}.\]

为了计算 \(\frac{\partial y}{\partial x_i}\),我们可以简单地将 \(x_1, \ldots, x_{i-1}, x_{i+1}, \ldots, x_n\) 看作常数,并计算 \(y\)关于\(x_i\) 的导数。对于偏导数的表示,以下是等价的:

(2.4.8)\[\frac{\partial y}{\partial x_i} = \frac{\partial f}{\partial x_i} = f_{x_i} = f_i = D_i f = D_{x_i} f.\]

2.4.3. 梯度

我们可以连结一个多元函数对其所有变量的偏导数,以得到该函数的梯度(gradient)向量。设函数 \(f: \mathbb{R}^n \rightarrow \mathbb{R}\) 的输入是一个 \(n\) 维向量 \(\mathbf{x} = [x_1, x_2, \ldots, x_n]^\top\),并且输出是一个标量。 函数\(f(\mathbf{x})\)相对于\(\mathbf{x}\)的梯度是一个包含\(n\)个偏导数的向量:

(2.4.9)\[\nabla_{\mathbf{x}} f(\mathbf{x}) = \bigg[\frac{\partial f(\mathbf{x})}{\partial x_1}, \frac{\partial f(\mathbf{x})}{\partial x_2}, \ldots, \frac{\partial f(\mathbf{x})}{\partial x_n}\bigg]^\top,\]

其中 \(\nabla_{\mathbf{x}} f(\mathbf{x})\) 通常在没有歧义时被 \(\nabla f(\mathbf{x})\) 取代。

假设\(\mathbf{x}\)\(n\)维向量,在微分多元函数时经常使用以下规则:

  • 对于所有\(\mathbf{A} \in \mathbb{R}^{m \times n}\),都有 \(\nabla_{\mathbf{x}} \mathbf{A} \mathbf{x} = \mathbf{A}^\top\)

  • 对于所有\(\mathbf{A} \in \mathbb{R}^{n \times m}\),都有 \(\nabla_{\mathbf{x}} \mathbf{x}^\top \mathbf{A} = \mathbf{A}\)

  • 对于所有\(\mathbf{A} \in \mathbb{R}^{n \times n}\),都有 \(\nabla_{\mathbf{x}} \mathbf{x}^\top \mathbf{A} \mathbf{x} = (\mathbf{A} + \mathbf{A}^\top)\mathbf{x}\)

  • \(\nabla_{\mathbf{x}} \|\mathbf{x} \|^2 = \nabla_{\mathbf{x}} \mathbf{x}^\top \mathbf{x} = 2\mathbf{x}\)

同样,对于任何矩阵 \(\mathbf{X}\),我们都有 \(\nabla_{\mathbf{X}} \|\mathbf{X} \|_F^2 = 2\mathbf{X}\)。正如我们之后将看到的,梯度对于设计深度学习中的优化算法有很大用处。

2.4.4. 链式法则

然而,上面方法可能很难找到梯度。 这是因为在深度学习中,多元函数通常是 复合(composite)的,所以我们可能没法应用上述任何规则来微分这些函数。 幸运的是,链式法则使我们能够微分复合函数。

让我们先考虑单变量函数。假设函数 \(y=f(u)\)\(u=g(x)\) 都是可微的,根据链式法则:

(2.4.10)\[\frac{dy}{dx} = \frac{dy}{du} \frac{du}{dx}.\]

现在让我们把注意力转向一个更一般的场景,即函数具有任意数量的变量的情况。假设可微分函数 \(y\) 有变量 \(u_1, u_2, \ldots, u_m\),其中每个可微分函数 \(u_i\) 都有变量 \(x_1, x_2, \ldots, x_n\)。注意,\(y\)\(x_1, x_2, \ldots, x_n\) 的函数。对于任意 \(i = 1, 2, \ldots, n\),链式法则给出:

(2.4.11)\[\frac{dy}{dx_i} = \frac{dy}{du_1} \frac{du_1}{dx_i} + \frac{dy}{du_2} \frac{du_2}{dx_i} + \cdots + \frac{dy}{du_m} \frac{du_m}{dx_i}\]

2.4.5. 小结

  • 微分和积分是微积分的两个分支,其中前者可以应用于深度学习中无处不在的优化问题。

  • 导数可以被解释为函数相对于其变量的瞬时变化率。它也是函数曲线的切线的斜率。

  • 梯度是一个向量,其分量是多变量函数相对于其所有变量的偏导数。

  • 链式法则使我们能够微分复合函数。

2.4.6. 练习

  1. 绘制函数\(y = f(x) = x^3 - \frac{1}{x}\)和其在\(x = 1\)处切线的图像。

  2. 求函数\(f(\mathbf{x}) = 3x_1^2 + 5e^{x_2}\)的梯度。

  3. 函数\(f(\mathbf{x}) = \|\mathbf{x}\|_2\)的梯度是什么?

  4. 你可以写出函数\(u = f(x, y, z)\),其中\(x = x(a, b)\)\(y = y(a, b)\)\(z = z(a, b)\)的链式法则吗?