22.3. 单变量微积分
在 Colab 中打开 Notebook
在 Colab 中打开 Notebook
在 Colab 中打开 Notebook
在 Colab 中打开 Notebook
在 SageMaker Studio Lab 中打开 Notebook

第 2.4 节中,我们看到了微分学的基本要素。本节将更深入地探讨微积分的基本原理,以及如何在机器学习的背景下理解和应用它。

22.3.1. 微分学

微分学本质上是研究函数在微小变化下的行为。为了理解为什么这对于深度学习如此核心,让我们来看一个例子。

假设我们有一个深度神经网络,为了方便起见,其权重被连接成一个单一的向量\(\mathbf{w} = (w_1, \ldots, w_n)\)。给定一个训练数据集,我们考虑神经网络在该数据集上的损失,我们将其写为\(\mathcal{L}(\mathbf{w})\)

这个函数非常复杂,它编码了给定架构的所有可能模型在该数据集上的性能,因此几乎不可能判断哪一组权重\(\mathbf{w}\)会使损失最小化。因此,在实践中,我们通常首先*随机*初始化我们的权重,然后迭代地朝着使损失下降最快的方向迈出小步。

接下来的问题表面上看起来并不简单:我们如何找到使权重下降最快的方向?为了深入探讨这个问题,让我们首先研究只有一个权重的情况:\(L(\mathbf{w}) = L(x)\),其中\(x\)是一个单一的实数值。

让我们取\(x\)并尝试理解当我们将它改变一个很小的量变成\(x + \epsilon\)时会发生什么。如果你希望更具体一些,可以想象一个像\(\epsilon = 0.0000001\)这样的数字。为了帮助我们可视化所发生的事情,让我们绘制一个示例函数\(f(x) = \sin(x^x)\)在区间\([0, 3]\)上的图像。

%matplotlib inline
import torch
from IPython import display
from d2l import torch as d2l

torch.pi = torch.acos(torch.zeros(1)).item() * 2  # Define pi in torch

# Plot a function in a normal range
x_big = torch.arange(0.01, 3.01, 0.01)
ys = torch.sin(x_big**x_big)
d2l.plot(x_big, ys, 'x', 'f(x)')
../_images/output_single-variable-calculus_47f3dd_3_0.svg
%matplotlib inline
from IPython import display
from mxnet import np, npx
from d2l import mxnet as d2l

npx.set_np()

# Plot a function in a normal range
x_big = np.arange(0.01, 3.01, 0.01)
ys = np.sin(x_big**x_big)
d2l.plot(x_big, ys, 'x', 'f(x)')
[21:56:22] ../src/storage/storage.cc:196: Using Pooled (Naive) StorageManager for CPU
../_images/output_single-variable-calculus_47f3dd_6_1.svg
%matplotlib inline
import tensorflow as tf
from IPython import display
from d2l import tensorflow as d2l

tf.pi = tf.acos(tf.zeros(1)).numpy() * 2  # Define pi in TensorFlow

# Plot a function in a normal range
x_big = tf.range(0.01, 3.01, 0.01)
ys = tf.sin(x_big**x_big)
d2l.plot(x_big, ys, 'x', 'f(x)')
../_images/output_single-variable-calculus_47f3dd_9_0.svg

在这样大的尺度上,函数的行为并不简单。然而,如果我们将范围缩小到像\([1.75, 2.25]\)这样更小的范围,我们会发现图像变得简单得多。

# Plot a the same function in a tiny range
x_med = torch.arange(1.75, 2.25, 0.001)
ys = torch.sin(x_med**x_med)
d2l.plot(x_med, ys, 'x', 'f(x)')
../_images/output_single-variable-calculus_47f3dd_15_0.svg
# Plot a the same function in a tiny range
x_med = np.arange(1.75, 2.25, 0.001)
ys = np.sin(x_med**x_med)
d2l.plot(x_med, ys, 'x', 'f(x)')
../_images/output_single-variable-calculus_47f3dd_18_0.svg
# Plot a the same function in a tiny range
x_med = tf.range(1.75, 2.25, 0.001)
ys = tf.sin(x_med**x_med)
d2l.plot(x_med, ys, 'x', 'f(x)')
../_images/output_single-variable-calculus_47f3dd_21_0.svg

再极端一点,如果我们放大到一个非常小的片段,行为会变得更加简单:它就是一条直线。

# Plot a the same function in a tiny range
x_small = torch.arange(2.0, 2.01, 0.0001)
ys = torch.sin(x_small**x_small)
d2l.plot(x_small, ys, 'x', 'f(x)')
../_images/output_single-variable-calculus_47f3dd_27_0.svg
# Plot a the same function in a tiny range
x_small = np.arange(2.0, 2.01, 0.0001)
ys = np.sin(x_small**x_small)
d2l.plot(x_small, ys, 'x', 'f(x)')
../_images/output_single-variable-calculus_47f3dd_30_0.svg
# Plot a the same function in a tiny range
x_small = tf.range(2.0, 2.01, 0.0001)
ys = tf.sin(x_small**x_small)
d2l.plot(x_small, ys, 'x', 'f(x)')
../_images/output_single-variable-calculus_47f3dd_33_0.svg

这是单变量微积分的关键观察:在足够小的范围内,常见函数的行为可以由一条直线来建模。这意味着对于大多数函数,我们可以合理地预期,当我们将函数的\(x\)值稍微改变一点时,输出\(f(x)\)也会相应地改变一点。我们唯一需要回答的问题是:“输出的变化量与输入的变化量相比有多大?是它的一半大?还是两倍大?”

因此,我们可以考虑函数输出的变化量与函数输入的微小变化量之比。我们可以将其正式写为:

(22.3.1)\[\frac{L(x+\epsilon) - L(x)}{(x+\epsilon) - x} = \frac{L(x+\epsilon) - L(x)}{\epsilon}.\]

这已经足够我们在代码中进行实验了。例如,假设我们知道\(L(x) = x^{2} + 1701(x-4)^3\),那么我们可以看到在\(x = 4\)这一点这个值有多大,如下所示。

# Define our function
def L(x):
    return x**2 + 1701*(x-4)**3

# Print the difference divided by epsilon for several epsilon
for epsilon in [0.1, 0.001, 0.0001, 0.00001]:
    print(f'epsilon = {epsilon:.5f} -> {(L(4+epsilon) - L(4)) / epsilon:.5f}')
epsilon = 0.10000 -> 25.11000
epsilon = 0.00100 -> 8.00270
epsilon = 0.00010 -> 8.00012
epsilon = 0.00001 -> 8.00001
# Define our function
def L(x):
    return x**2 + 1701*(x-4)**3

# Print the difference divided by epsilon for several epsilon
for epsilon in [0.1, 0.001, 0.0001, 0.00001]:
    print(f'epsilon = {epsilon:.5f} -> {(L(4+epsilon) - L(4)) / epsilon:.5f}')
epsilon = 0.10000 -> 25.11000
epsilon = 0.00100 -> 8.00270
epsilon = 0.00010 -> 8.00012
epsilon = 0.00001 -> 8.00001
# Define our function
def L(x):
    return x**2 + 1701*(x-4)**3

# Print the difference divided by epsilon for several epsilon
for epsilon in [0.1, 0.001, 0.0001, 0.00001]:
    print(f'epsilon = {epsilon:.5f} -> {(L(4+epsilon) - L(4)) / epsilon:.5f}')
epsilon = 0.10000 -> 25.11000
epsilon = 0.00100 -> 8.00270
epsilon = 0.00010 -> 8.00012
epsilon = 0.00001 -> 8.00001

现在,如果我们仔细观察,会发现这个数字的输出非常接近\(8\)。事实上,如果我们减小\(\epsilon\),我们会看到值越来越接近\(8\)。因此我们可以得出结论,我们所寻求的值(输入的变化引起输出变化的程度)在点\(x=4\)处应该是\(8\)。数学家编码这一事实的方式是

(22.3.2)\[\lim_{\epsilon \rightarrow 0}\frac{L(4+\epsilon) - L(4)}{\epsilon} = 8.\]

稍微离题谈点历史:在神经网络研究的最初几十年里,科学家们使用这种算法(*有限差分法*)来评估损失函数在微小扰动下的变化:只需改变权重,看看损失如何变化。这种方法计算效率低下,需要两次评估损失函数才能看到单个变量的变化如何影响损失。如果我们试图用这种方法处理哪怕只有区区几千个参数的网络,也需要在整个数据集上进行数千次网络评估!直到1986年,在Rumelhart等人(1988中引入的*反向传播算法*才解决了这个问题,它提供了一种方法,可以在与网络在数据集上进行一次预测相同的计算时间内,计算出*任何*权重的共同变化将如何改变损失。

回到我们的例子,这个值\(8\)对于不同的\(x\)值是不同的,所以将它定义为\(x\)的函数是有意义的。更正式地说,这个依赖于值的变化率被称为*导数*,写作

(22.3.3)\[\frac{df}{dx}(x) = \lim_{\epsilon \rightarrow 0}\frac{f(x+\epsilon) - f(x)}{\epsilon}.\]

不同的教科书会使用不同的导数记号。例如,下面所有的记号都表示同样的事情

(22.3.4)\[\frac{df}{dx} = \frac{d}{dx}f = f' = \nabla_xf = D_xf = f_x.\]

大多数作者会选择一种记号并坚持使用,但即使这样也不能保证。最好熟悉所有这些记号。在本文中,我们将使用记号\(\frac{df}{dx}\),除非我们想对一个复杂的表达式求导,在这种情况下,我们将使用\(\frac{d}{dx}f\)来写出像下面这样的表达式

(22.3.5)\[\frac{d}{dx}\left[x^4+\cos\left(\frac{x^2+1}{2x-1}\right)\right].\]

通常,直观地展开导数的定义(22.3.3)来理解当我们对\(x\)做一个微小改变时函数如何变化是很有用的

(22.3.6)\[\begin{split}\begin{aligned} \frac{df}{dx}(x) = \lim_{\epsilon \rightarrow 0}\frac{f(x+\epsilon) - f(x)}{\epsilon} & \implies \frac{df}{dx}(x) \approx \frac{f(x+\epsilon) - f(x)}{\epsilon} \\ & \implies \epsilon \frac{df}{dx}(x) \approx f(x+\epsilon) - f(x) \\ & \implies f(x+\epsilon) \approx f(x) + \epsilon \frac{df}{dx}(x). \end{aligned}\end{split}\]

最后一个方程值得特别指出。它告诉我们,如果你对任何函数做一个微小的输入改变,输出的变化量将是这个微小改变量乘以导数。

通过这种方式,我们可以将导数理解为一个比例因子,它告诉我们从输入的变化中可以得到多大的输出变化。

22.3.2. 微积分法则

我们现在转向理解如何计算一个显式函数的导数。一个完全正式的微积分处理会从第一性原理推导一切。我们在这里不会沉溺于这种诱惑,而是提供对常见法则的理解。

22.3.2.1. 常见导数

正如在 第 2.4 节中所见,计算导数时通常可以利用一系列法则将计算简化为几个核心函数。我们在这里重复它们以便参考。

  • 常数导数。 \(\frac{d}{dx}c = 0\)

  • 线性函数导数。 \(\frac{d}{dx}(ax) = a\)

  • 幂法则。 \(\frac{d}{dx}x^n = nx^{n-1}\)

  • 指数函数导数。 \(\frac{d}{dx}e^x = e^x\)

  • 对数函数导数。 \(\frac{d}{dx}\log(x) = \frac{1}{x}\)

22.3.2.2. 求导法则

如果每个导数都需要单独计算并存储在表格中,那么微分学将几乎是不可能的。数学的馈赠在于,我们可以推广上述导数,并计算更复杂的导数,例如求\(f(x) = \log\left(1+(x-1)^{10}\right)\)的导数。正如在 第 2.4 节中提到的,关键在于将函数以各种方式(最重要的是:求和、乘积和复合)组合时发生的情况进行编码。

  • 加法法则。 \(\frac{d}{dx}\left(g(x) + h(x)\right) = \frac{dg}{dx}(x) + \frac{dh}{dx}(x)\)

  • 乘法法则。 \(\frac{d}{dx}\left(g(x)\cdot h(x)\right) = g(x)\frac{dh}{dx}(x) + \frac{dg}{dx}(x)h(x)\)

  • 链式法则。 \(\frac{d}{dx}g(h(x)) = \frac{dg}{dh}(h(x))\cdot \frac{dh}{dx}(x)\)

让我们看看如何使用 (22.3.6)来理解这些法则。对于加法法则,考虑以下推理链:

(22.3.7)\[\begin{split}\begin{aligned} f(x+\epsilon) & = g(x+\epsilon) + h(x+\epsilon) \\ & \approx g(x) + \epsilon \frac{dg}{dx}(x) + h(x) + \epsilon \frac{dh}{dx}(x) \\ & = g(x) + h(x) + \epsilon\left(\frac{dg}{dx}(x) + \frac{dh}{dx}(x)\right) \\ & = f(x) + \epsilon\left(\frac{dg}{dx}(x) + \frac{dh}{dx}(x)\right). \end{aligned}\end{split}\]

通过将这个结果与\(f(x+\epsilon) \approx f(x) + \epsilon \frac{df}{dx}(x)\)这一事实进行比较,我们看到\(\frac{df}{dx}(x) = \frac{dg}{dx}(x) + \frac{dh}{dx}(x)\),正如所期望的。这里的直觉是:当我们改变输入\(x\)时,\(g\)\(h\)共同对输出的变化贡献了\(\frac{dg}{dx}(x)\)\(\frac{dh}{dx}(x)\)

乘积法则更为微妙,需要一个新的观察来处理这些表达式。我们将像以前一样使用 (22.3.6) 开始:

(22.3.8)\[\begin{split}\begin{aligned} f(x+\epsilon) & = g(x+\epsilon)\cdot h(x+\epsilon) \\ & \approx \left(g(x) + \epsilon \frac{dg}{dx}(x)\right)\cdot\left(h(x) + \epsilon \frac{dh}{dx}(x)\right) \\ & = g(x)\cdot h(x) + \epsilon\left(g(x)\frac{dh}{dx}(x) + \frac{dg}{dx}(x)h(x)\right) + \epsilon^2\frac{dg}{dx}(x)\frac{dh}{dx}(x) \\ & = f(x) + \epsilon\left(g(x)\frac{dh}{dx}(x) + \frac{dg}{dx}(x)h(x)\right) + \epsilon^2\frac{dg}{dx}(x)\frac{dh}{dx}(x). \\ \end{aligned}\end{split}\]

这类似于上面的计算,我们确实看到了我们的答案(\(\frac{df}{dx}(x) = g(x)\frac{dh}{dx}(x) + \frac{dg}{dx}(x)h(x)\))紧挨着\(\epsilon\),但存在那个大小为\(\epsilon^{2}\)的项的问题。我们称之为*高阶项*,因为\(\epsilon^2\)的幂高于\(\epsilon^1\)的幂。在后面的章节中,我们将看到我们有时需要跟踪这些项,但现在请注意,如果\(\epsilon = 0.0000001\),那么\(\epsilon^{2}= 0.0000000000001\),这要小得多。当我们让\(\epsilon \rightarrow 0\)时,我们可以安全地忽略高阶项。在本附录中,作为一个通用约定,我们将使用“\(\approx\)”来表示两个项在忽略高阶项的情况下是相等的。然而,如果我们希望更正式,我们可以检查差商:

(22.3.9)\[\frac{f(x+\epsilon) - f(x)}{\epsilon} = g(x)\frac{dh}{dx}(x) + \frac{dg}{dx}(x)h(x) + \epsilon \frac{dg}{dx}(x)\frac{dh}{dx}(x),\]

可以看到当\(\epsilon \rightarrow 0\)时,右边的项也趋于零。

最后,对于链式法则,我们可以再次像之前一样使用 (22.3.6),然后看到

(22.3.10)\[\begin{split}\begin{aligned} f(x+\epsilon) & = g(h(x+\epsilon)) \\ & \approx g\left(h(x) + \epsilon \frac{dh}{dx}(x)\right) \\ & \approx g(h(x)) + \epsilon \frac{dh}{dx}(x) \frac{dg}{dh}(h(x))\\ & = f(x) + \epsilon \frac{dg}{dh}(h(x))\frac{dh}{dx}(x), \end{aligned}\end{split}\]

在第二行中,我们将函数\(g\)视为其输入(\(h(x)\))被一个微小的量\(\epsilon \frac{dh}{dx}(x)\)所移动。

这些法则为我们提供了一套灵活的工具,可以计算几乎任何想要的表达式。例如,

(22.3.11)\[\begin{split}\begin{aligned} \frac{d}{dx}\left[\log\left(1+(x-1)^{10}\right)\right] & = \left(1+(x-1)^{10}\right)^{-1}\frac{d}{dx}\left[1+(x-1)^{10}\right]\\ & = \left(1+(x-1)^{10}\right)^{-1}\left(\frac{d}{dx}[1] + \frac{d}{dx}[(x-1)^{10}]\right) \\ & = \left(1+(x-1)^{10}\right)^{-1}\left(0 + 10(x-1)^9\frac{d}{dx}[x-1]\right) \\ & = 10\left(1+(x-1)^{10}\right)^{-1}(x-1)^9 \\ & = \frac{10(x-1)^9}{1+(x-1)^{10}}. \end{aligned}\end{split}\]

其中每一行都使用了以下规则

  1. 链式法则和对数函数的导数。

  2. 加法法则。

  3. 常数导数、链式法则和幂法则。

  4. 加法法则、线性函数导数、常数导数。

做完这个例子后,有两件事应该很清楚了:

  1. 任何我们可以用求和、乘积、常数、幂、指数和对数写出的函数,都可以通过遵循这些规则机械地计算出其导数。

  2. 让一个人来遵循这些规则可能会很繁琐且容易出错!

值得庆幸的是,这两个事实共同暗示了一条前进的道路:这是一个完美的机械化候选者!事实上,我们将在本节稍后重新讨论的反向传播正是如此。

22.3.2.3. 线性近似

在处理导数时,几何地解释上面使用的近似通常很有用。特别地,请注意方程

(22.3.12)\[f(x+\epsilon) \approx f(x) + \epsilon \frac{df}{dx}(x),\]

通过一条经过点\((x, f(x))\)且斜率为\(\frac{df}{dx}(x)\)的直线来近似\(f\)的值。通过这种方式,我们说导数给出了函数\(f\)的线性近似,如下图所示:

# Compute sin
xs = torch.arange(-torch.pi, torch.pi, 0.01)
plots = [torch.sin(xs)]

# Compute some linear approximations. Use d(sin(x))/dx = cos(x)
for x0 in [-1.5, 0.0, 2.0]:
    plots.append(torch.sin(torch.tensor(x0)) + (xs - x0) *
                 torch.cos(torch.tensor(x0)))

d2l.plot(xs, plots, 'x', 'f(x)', ylim=[-1.5, 1.5])
../_images/output_single-variable-calculus_47f3dd_51_0.svg
# Compute sin
xs = np.arange(-np.pi, np.pi, 0.01)
plots = [np.sin(xs)]

# Compute some linear approximations. Use d(sin(x)) / dx = cos(x)
for x0 in [-1.5, 0, 2]:
    plots.append(np.sin(x0) + (xs - x0) * np.cos(x0))

d2l.plot(xs, plots, 'x', 'f(x)', ylim=[-1.5, 1.5])
../_images/output_single-variable-calculus_47f3dd_54_0.svg
# Compute sin
xs = tf.range(-tf.pi, tf.pi, 0.01)
plots = [tf.sin(xs)]

# Compute some linear approximations. Use d(sin(x))/dx = cos(x)
for x0 in [-1.5, 0.0, 2.0]:
    plots.append(tf.sin(tf.constant(x0)) + (xs - x0) *
                 tf.cos(tf.constant(x0)))

d2l.plot(xs, plots, 'x', 'f(x)', ylim=[-1.5, 1.5])
../_images/output_single-variable-calculus_47f3dd_57_0.svg

22.3.2.4. 高阶导数

现在让我们做一件表面上看起来可能很奇怪的事情。取一个函数\(f\)并计算其导数\(\frac{df}{dx}\)。这给了我们\(f\)在任何点的变化率。

然而,导数\(\frac{df}{dx}\)本身也可以看作一个函数,所以没有什么能阻止我们计算\(\frac{df}{dx}\)的导数来得到\(\frac{d^2f}{dx^2} = \frac{df}{dx}\left(\frac{df}{dx}\right)\)。我们称之为\(f\)的二阶导数。这个函数是\(f\)的变化率的变化率,换句话说,就是变化率是如何变化的。我们可以任意次地应用导数来获得所谓的\(n\)阶导数。为了保持记号简洁,我们将\(n\)阶导数表示为

(22.3.13)\[f^{(n)}(x) = \frac{d^{n}f}{dx^{n}} = \left(\frac{d}{dx}\right)^{n} f.\]

让我们试着理解*为什么*这是一个有用的概念。下面,我们可视化了\(f^{(2)}(x)\), \(f^{(1)}(x)\), 和\(f(x)\)

首先,考虑二阶导数\(f^{(2)}(x)\)是正常数的情况。这意味着一阶导数的斜率为正。因此,一阶导数\(f^{(1)}(x)\)可能从负值开始,在某一点变为零,然后最终变为正值。这告诉我们原始函数\(f\)的斜率,因此,函数\(f\)本身是先减小,然后变平,再增加。换句话说,函数\(f\)向上弯曲,并有一个单一的最小值,如 图 22.3.1所示。

../_images/posSecDer.svg

图 22.3.1 如果我们假设二阶导数是正常数,那么一阶导数是增加的,这意味着函数本身有一个最小值。

其次,如果二阶导数是负常数,这意味着一阶导数是递减的。这意味着一阶导数可能从正值开始,在某一点变为零,然后变为负值。因此,函数\(f\)本身先增加,然后变平,再减少。换句话说,函数\(f\)向下弯曲,并有一个单一的最大值,如 图 22.3.2所示。

../_images/negSecDer.svg

图 22.3.2 如果我们假设二阶导数是负常数,那么一阶导数是递减的,这意味着函数本身有一个最大值。

第三,如果二阶导数始终为零,那么一阶导数将永远不会改变——它是一个常数!这意味着\(f\)以固定的速率增加(或减少),并且\(f\)本身是一条直线,如 图 22.3.3所示。

../_images/zeroSecDer.svg

图 22.3.3 如果我们假设二阶导数为零,那么一阶导数是常数,这意味着函数本身是一条直线。

总而言之,二阶导数可以被解释为描述函数\(f\)弯曲的方式。正的二阶导数导致向上弯曲,而负的二阶导数意味着\(f\)向下弯曲,零二阶导数意味着\(f\)根本不弯曲。

让我们更进一步。考虑函数\(g(x) = ax^{2}+ bx + c\)。我们可以计算出:

(22.3.14)\[\begin{split}\begin{aligned} \frac{dg}{dx}(x) & = 2ax + b \\ \frac{d^2g}{dx^2}(x) & = 2a. \end{aligned}\end{split}\]

如果我们心中有一个原始函数\(f(x)\),我们可以计算前两个导数,并找到\(a, b\)\(c\)的值,使它们与这个计算相匹配。与上一节我们看到一阶导数给出了用直线进行最佳近似类似,这种构造提供了用二次曲线进行最佳近似。让我们为\(f(x) = \sin(x)\)可视化这一点。

# Compute sin
xs = torch.arange(-torch.pi, torch.pi, 0.01)
plots = [torch.sin(xs)]

# Compute some quadratic approximations. Use d(sin(x)) / dx = cos(x)
for x0 in [-1.5, 0.0, 2.0]:
    plots.append(torch.sin(torch.tensor(x0)) + (xs - x0) *
                 torch.cos(torch.tensor(x0)) - (xs - x0)**2 *
                 torch.sin(torch.tensor(x0)) / 2)

d2l.plot(xs, plots, 'x', 'f(x)', ylim=[-1.5, 1.5])
../_images/output_single-variable-calculus_47f3dd_63_0.svg
# Compute sin
xs = np.arange(-np.pi, np.pi, 0.01)
plots = [np.sin(xs)]

# Compute some quadratic approximations. Use d(sin(x)) / dx = cos(x)
for x0 in [-1.5, 0, 2]:
    plots.append(np.sin(x0) + (xs - x0) * np.cos(x0) -
                              (xs - x0)**2 * np.sin(x0) / 2)

d2l.plot(xs, plots, 'x', 'f(x)', ylim=[-1.5, 1.5])
../_images/output_single-variable-calculus_47f3dd_66_0.svg
# Compute sin
xs = tf.range(-tf.pi, tf.pi, 0.01)
plots = [tf.sin(xs)]

# Compute some quadratic approximations. Use d(sin(x)) / dx = cos(x)
for x0 in [-1.5, 0.0, 2.0]:
    plots.append(tf.sin(tf.constant(x0)) + (xs - x0) *
                 tf.cos(tf.constant(x0)) - (xs - x0)**2 *
                 tf.sin(tf.constant(x0)) / 2)

d2l.plot(xs, plots, 'x', 'f(x)', ylim=[-1.5, 1.5])
../_images/output_single-variable-calculus_47f3dd_69_0.svg

我们将在下一节中将这个想法扩展到*泰勒级数*的概念。

22.3.2.5. 泰勒级数

泰勒级数提供了一种方法,如果我们给定函数在点\(x_0\)处的前\(n\)个导数的值,即\(\left\{ f(x_0), f^{(1)}(x_0), f^{(2)}(x_0), \ldots, f^{(n)}(x_0) \right\}\),就可以近似函数\(f(x)\)。其思想是找到一个\(n\)次多项式,使其在\(x_0\)处的所有给定导数都匹配。

我们在上一节看到了\(n=2\)的情况,经过一些代数运算可以得到

(22.3.15)\[f(x) \approx \frac{1}{2}\frac{d^2f}{dx^2}(x_0)(x-x_0)^{2}+ \frac{df}{dx}(x_0)(x-x_0) + f(x_0).\]

正如我们在上面看到的,分母中的\(2\)是为了抵消我们对\(x^2\)求两次导数时得到的\(2\),而其他项都为零。同样的逻辑适用于一阶导数和函数值本身。

如果我们将逻辑进一步推到\(n=3\),我们会得出结论

(22.3.16)\[f(x) \approx \frac{\frac{d^3f}{dx^3}(x_0)}{6}(x-x_0)^3 + \frac{\frac{d^2f}{dx^2}(x_0)}{2}(x-x_0)^{2}+ \frac{df}{dx}(x_0)(x-x_0) + f(x_0).\]

其中\(6 = 3 \times 2 = 3!\)来自于我们对\(x^3\)求三次导数时得到的常数。

此外,我们可以通过以下方式得到一个\(n\)次多项式

(22.3.17)\[P_n(x) = \sum_{i = 0}^{n} \frac{f^{(i)}(x_0)}{i!}(x-x_0)^{i}.\]

其中记号为

(22.3.18)\[f^{(n)}(x) = \frac{d^{n}f}{dx^{n}} = \left(\frac{d}{dx}\right)^{n} f.\]

实际上,\(P_n(x)\)可以被看作是我们的函数\(f(x)\)的最好的\(n\)次多项式近似。

虽然我们不会深入探讨上述近似的误差,但值得一提的是无限极限。在这种情况下,对于行为良好(称为实解析函数)的函数,如\(\cos(x)\)\(e^{x}\),我们可以写出无限项并精确地近似同一个函数

(22.3.19)\[f(x) = \sum_{n = 0}^\infty \frac{f^{(n)}(x_0)}{n!}(x-x_0)^{n}.\]

\(f(x) = e^{x}\)为例。由于\(e^{x}\)的导数是其本身,我们知道\(f^{(n)}(x) = e^{x}\)。因此,\(e^{x}\)可以通过在\(x_0 = 0\)处取泰勒级数来重构,即

(22.3.20)\[e^{x} = \sum_{n = 0}^\infty \frac{x^{n}}{n!} = 1 + x + \frac{x^2}{2} + \frac{x^3}{6} + \cdots.\]

让我们在代码中看看这是如何工作的,并观察增加泰勒近似的阶数如何使我们更接近期望的函数\(e^x\)

# Compute the exponential function
xs = torch.arange(0, 3, 0.01)
ys = torch.exp(xs)

# Compute a few Taylor series approximations
P1 = 1 + xs
P2 = 1 + xs + xs**2 / 2
P5 = 1 + xs + xs**2 / 2 + xs**3 / 6 + xs**4 / 24 + xs**5 / 120

d2l.plot(xs, [ys, P1, P2, P5], 'x', 'f(x)', legend=[
    "Exponential", "Degree 1 Taylor Series", "Degree 2 Taylor Series",
    "Degree 5 Taylor Series"])
../_images/output_single-variable-calculus_47f3dd_75_0.svg
# Compute the exponential function
xs = np.arange(0, 3, 0.01)
ys = np.exp(xs)

# Compute a few Taylor series approximations
P1 = 1 + xs
P2 = 1 + xs + xs**2 / 2
P5 = 1 + xs + xs**2 / 2 + xs**3 / 6 + xs**4 / 24 + xs**5 / 120

d2l.plot(xs, [ys, P1, P2, P5], 'x', 'f(x)', legend=[
    "Exponential", "Degree 1 Taylor Series", "Degree 2 Taylor Series",
    "Degree 5 Taylor Series"])
../_images/output_single-variable-calculus_47f3dd_78_0.svg
# Compute the exponential function
xs = tf.range(0, 3, 0.01)
ys = tf.exp(xs)

# Compute a few Taylor series approximations
P1 = 1 + xs
P2 = 1 + xs + xs**2 / 2
P5 = 1 + xs + xs**2 / 2 + xs**3 / 6 + xs**4 / 24 + xs**5 / 120

d2l.plot(xs, [ys, P1, P2, P5], 'x', 'f(x)', legend=[
    "Exponential", "Degree 1 Taylor Series", "Degree 2 Taylor Series",
    "Degree 5 Taylor Series"])
../_images/output_single-variable-calculus_47f3dd_81_0.svg

泰勒级数有两个主要应用

  1. 理论应用:当我们试图理解一个过于复杂的函数时,使用泰勒级数可以将其转化为我们可以直接处理的多项式。

  2. 数值应用:像\(e^{x}\)\(\cos(x)\)这样的函数对于机器来说很难计算。它们可以存储固定精度的值表(这通常也是这么做的),但这仍然留下了诸如“\(\cos(1)\)的第1000位小数是多少?”这样的问题。泰勒级数通常有助于回答这类问题。

22.3.3. 总结

  • 导数可以用来表示当我们对输入进行微小改变时函数如何变化。

  • 基本导数可以通过求导法则组合起来,以创建任意复杂的导数。

  • 导数可以迭代以获得二阶或更高阶的导数。每增加一阶,就能提供关于函数行为更精细的信息。

  • 利用单个数据样本的导数信息,我们可以通过泰勒级数得到的多项式来近似行为良好的函数。

22.3.4. 练习

  1. \(x^3-4x+1\)的导数是什么?

  2. \(\log(\frac{1}{x})\)的导数是什么?

  3. 正确或错误:如果\(f'(x) = 0\),那么\(f\)\(x\)处有最大值或最小值?

  4. 对于\(x\ge0\)(我们假设\(f\)\(f(0)\)处取极限值\(0\)),\(f(x) = x\log(x)\)的最小值在哪里?