2.4. 微积分¶ 在 SageMaker Studio Lab 中打开 Notebook
在很长一段时间里,如何计算圆的面积一直是个谜。后来,在古希腊,数学家阿基米德想出了一个巧妙的办法,即在一个圆的内部画一系列顶点数不断增加的多边形(图 2.4.1)。对于一个有\(n\)个顶点的多边形,我们可以得到\(n\)个三角形。当我们更精细地分割圆时,每个三角形的高都接近半径\(r\)。同时,它的底边接近\(2 \pi r/n\),因为对于大量的顶点,弧长和弦长之间的比率接近1。因此,多边形的面积接近\(n \cdot r \cdot \frac{1}{2} (2 \pi r/n) = \pi r^2\)。
图 2.4.1 将圆的面积计算为一个极限过程。¶
这种极限过程是微分学和积分学的基础。前者可以告诉我们如何通过操纵函数的参数来增加或减少函数的值。这对于我们在深度学习中面临的优化问题很有用,在这些问题中,我们反复更新参数以减少损失函数。优化解决了如何使我们的模型拟合训练数据的问题,而微积分是其关键的先决条件。但是,不要忘记我们的最终目标是在以前未见过的数据上表现良好。这个问题被称为泛化,将是其他章节的重点。
%matplotlib inline
import numpy as np
from matplotlib_inline import backend_inline
from d2l import torch as d2l
%matplotlib inline
from matplotlib_inline import backend_inline
from mxnet import np, npx
from d2l import mxnet as d2l
npx.set_np()
%matplotlib inline
import numpy as np
from matplotlib_inline import backend_inline
from d2l import jax as d2l
No GPU/TPU found, falling back to CPU. (Set TF_CPP_MIN_LOG_LEVEL=0 and rerun for more info.)
%matplotlib inline
import numpy as np
from matplotlib_inline import backend_inline
from d2l import tensorflow as d2l
2.4.1. 导数和微分¶
简单地说,导数是函数相对于其参数变化的速率。导数可以告诉我们,如果我们将每个参数增加或减少一个极小的量,损失函数会以多快的速度增加或减少。形式上,对于从标量映射到标量的函数\(f: \mathbb{R} \rightarrow \mathbb{R}\),\(f\)在点\(x\)的导数定义为
右边的这个项叫做极限,它告诉我们当一个指定的变量接近一个特定值时,一个表达式的值会发生什么。这个极限告诉我们,当我们将扰动\(h\)的大小缩小到零时,扰动\(h\)与函数值变化\(f(x + h) - f(x)\)之间的比率收敛到什么值。
当\(f'(x)\)存在时,称\(f\)在\(x\)处是可微的;当\(f'(x)\)在某个集合(例如,区间\([a,b]\))上的所有\(x\)都存在时,我们说\(f\)在该集合上是可微的。并非所有函数都是可微的,包括许多我们希望优化的函数,例如准确率和受试者工作特征曲线下面积(AUC)。然而,由于计算损失的导数是几乎所有训练深度神经网络算法中的关键一步,我们通常会优化一个可微的代理函数来代替。
我们可以将导数\(f'(x)\)解释为\(f(x)\)相对于\(x\)的瞬时变化率。让我们通过一个例子来建立一些直观的认识。定义\(u = f(x) = 3x^2-4x\)。
def f(x):
return 3 * x ** 2 - 4 * x
def f(x):
return 3 * x ** 2 - 4 * x
def f(x):
return 3 * x ** 2 - 4 * x
def f(x):
return 3 * x ** 2 - 4 * x
设置\(x=1\),我们看到当\(h\)趋近于\(0\)时,\(\frac{f(x+h) - f(x)}{h}\)接近\(2\)。虽然这个实验缺乏数学证明的严谨性,但我们可以很快看到,\(f'(1) = 2\)确实成立。
for h in 10.0**np.arange(-1, -6, -1):
print(f'h={h:.5f}, numerical limit={(f(1+h)-f(1))/h:.5f}')
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
for h in 10.0**np.arange(-1, -6, -1):
print(f'h={h:.5f}, numerical limit={(f(1+h)-f(1))/h:.5f}')
h=0.10000, numerical limit=2.30000
h=0.01000, numerical limit=2.02999
h=0.00100, numerical limit=2.00295
h=0.00010, numerical limit=2.00033
h=0.00001, numerical limit=2.00272
[21:50:15] ../src/storage/storage.cc:196: Using Pooled (Naive) StorageManager for CPU
for h in 10.0**np.arange(-1, -6, -1):
print(f'h={h:.5f}, numerical limit={(f(1+h)-f(1))/h:.5f}')
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
for h in 10.0**np.arange(-1, -6, -1):
print(f'h={h:.5f}, numerical limit={(f(1+h)-f(1))/h:.5f}')
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)\),以下表达式是等价的:
其中符号\(\frac{d}{dx}\)和\(D\)是微分算子。下面,我们列出一些常见函数的导数:
由可微函数复合而成的函数通常本身也是可微的。以下规则对于处理任意可微函数\(f\)和\(g\)以及常数\(C\)的复合函数非常有用。
利用这些规则,我们可以求出\(3 x^2 - 4x\)的导数:
代入\(x = 1\)表明,在这个位置,导数确实等于\(2\)。注意,导数告诉我们函数在特定位置的斜率。
2.4.2. 可视化工具¶
我们可以使用matplotlib
库来可视化函数的斜率。我们需要定义几个函数。顾名思义,use_svg_display
告诉matplotlib
以SVG格式输出图形,以获得更清晰的图像。注释#@save
是一个特殊的修饰符,它允许我们将任何函数、类或其他代码块保存到d2l
包中,这样我们以后就可以通过d2l.use_svg_display()
等方式调用它,而无需重复代码。
def use_svg_display(): #@save
"""Use the svg format to display a plot in Jupyter."""
backend_inline.set_matplotlib_formats('svg')
方便的是,我们可以用set_figsize
设置图形大小。由于导入语句from matplotlib import pyplot as plt
在d2l
包中被标记为#@save
,我们可以调用d2l.plt
。
def set_figsize(figsize=(3.5, 2.5)): #@save
"""Set the figure size for matplotlib."""
use_svg_display()
d2l.plt.rcParams['figure.figsize'] = figsize
set_axes
函数可以将坐标轴与属性关联起来,包括标签、范围和刻度。
#@save
def set_axes(axes, xlabel, ylabel, xlim, ylim, xscale, yscale, legend):
"""Set the axes for matplotlib."""
axes.set_xlabel(xlabel), axes.set_ylabel(ylabel)
axes.set_xscale(xscale), axes.set_yscale(yscale)
axes.set_xlim(xlim), axes.set_ylim(ylim)
if legend:
axes.legend(legend)
axes.grid()
有了这三个函数,我们可以定义一个plot
函数来叠加多条曲线。这里的大部分代码只是为了确保输入的大小和形状匹配。
#@save
def plot(X, Y=None, xlabel=None, ylabel=None, legend=[], xlim=None,
ylim=None, xscale='linear', yscale='linear',
fmts=('-', 'm--', 'g-.', 'r:'), figsize=(3.5, 2.5), axes=None):
"""Plot data points."""
def has_one_axis(X): # True if X (tensor or list) has 1 axis
return (hasattr(X, "ndim") and X.ndim == 1 or isinstance(X, list)
and not hasattr(X[0], "__len__"))
if has_one_axis(X): X = [X]
if Y is None:
X, Y = [[]] * len(X), X
elif has_one_axis(Y):
Y = [Y]
if len(X) != len(Y):
X = X * len(Y)
set_figsize(figsize)
if axes is None:
axes = d2l.plt.gca()
axes.cla()
for x, y, fmt in zip(X, Y, fmts):
axes.plot(x,y,fmt) if len(x) else axes.plot(y,fmt)
set_axes(axes, xlabel, ylabel, xlim, ylim, xscale, yscale, legend)
现在我们可以绘制函数\(u = f(x)\)及其在\(x=1\)处的切线\(y = 2x - 3\),其中系数\(2\)是切线的斜率。
x = np.arange(0, 3, 0.1)
plot(x, [f(x), 2 * x - 3], 'x', 'f(x)', legend=['f(x)', 'Tangent line (x=1)'])
x = np.arange(0, 3, 0.1)
plot(x, [f(x), 2 * x - 3], 'x', 'f(x)', legend=['f(x)', 'Tangent line (x=1)'])
x = np.arange(0, 3, 0.1)
plot(x, [f(x), 2 * x - 3], 'x', 'f(x)', legend=['f(x)', 'Tangent line (x=1)'])
x = np.arange(0, 3, 0.1)
plot(x, [f(x), 2 * x - 3], 'x', 'f(x)', legend=['f(x)', 'Tangent line (x=1)'])
2.4.3. 偏导数和梯度¶
到目前为止,我们一直在对只有一个变量的函数进行微分。在深度学习中,我们还需要处理包含许多变量的函数。我们简要介绍适用于这类多元函数的导数概念。
设\(y = f(x_1, x_2, \ldots, x_n)\)是一个有\(n\)个变量的函数。\(y\)关于其第\(i^\textrm{th}\)个参数\(x_i\)的偏导数是
要计算\(\frac{\partial y}{\partial x_i}\),我们可以将\(x_1, \ldots, x_{i-1}, x_{i+1}, \ldots, x_n\)视为常数,然后计算\(y\)关于\(x_i\)的导数。以下关于偏导数的表示法都很常见,并且都表示相同的意思:
我们可以将一个多元函数对其所有变量的偏导数连接起来,得到一个向量,这个向量被称为该函数的梯度。假设函数\(f: \mathbb{R}^n \rightarrow \mathbb{R}\)的输入是一个\(n\)维向量\(\mathbf{x} = [x_1, x_2, \ldots, x_n]^\top\),输出是一个标量。函数\(f\)关于\(\mathbf{x}\)的梯度是一个由\(n\)个偏导数组成的向量:
在没有歧义的情况下,\(\nabla_{\mathbf{x}} f(\mathbf{x})\)通常被替换为\(\nabla f(\mathbf{x})\)。以下规则在微分多元函数时很有用:
对于所有\(\mathbf{A} \in \mathbb{R}^{m \times n}\),我们有\(\nabla_{\mathbf{x}} \mathbf{A} \mathbf{x} = \mathbf{A}^\top\)和\(\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} \|_\textrm{F}^2 = 2\mathbf{X}\)。
2.4.4. 链式法则¶
在深度学习中,我们关心的梯度通常很难计算,因为我们处理的是深度嵌套的函数(函数的函数(函数的函数……))。幸运的是,链式法则可以解决这个问题。回到单变量函数,假设\(y = f(g(x))\),并且其基础函数\(y=f(u)\)和\(u=g(x)\)都是可微的。链式法则规定:
回到多元函数,假设\(y = f(\mathbf{u})\)有变量\(u_1, u_2, \ldots, u_m\),其中每个\(u_i = g_i(\mathbf{x})\)都有变量\(x_1, x_2, \ldots, x_n\),即\(\mathbf{u} = g(\mathbf{x})\)。那么链式法则规定:
其中\(\mathbf{A} \in \mathbb{R}^{n \times m}\)是一个矩阵,它包含向量\(\mathbf{u}\)关于向量\(\mathbf{x}\)的导数。因此,评估梯度需要计算一个向量-矩阵乘积。这是线性代数成为构建深度学习系统不可或缺的基石的关键原因之一。
2.4.5. 讨论¶
虽然我们只是触及了一个深奥主题的皮毛,但一些概念已经浮出水面:首先,微分的复合规则可以常规地应用,使我们能够自动计算梯度。这个任务不需要创造力,因此我们可以将我们的认知能力集中在其他地方。其次,计算向量值函数的导数需要我们在从输出到输入追踪变量的依赖关系图时进行矩阵乘法。特别是,当我们在评估一个函数时,这个图是向前遍历的,而当我们在计算梯度时,是向后遍历的。后面的章节将正式介绍反向传播,这是一种应用链式法则的计算过程。
从优化的角度来看,梯度允许我们确定如何移动模型的参数以降低损失,本书中使用的优化算法的每一步都需要计算梯度。
2.4.6. 练习¶
到目前为止,我们都想当然地接受了导数规则。请使用定义和极限证明 (i) \(f(x) = c\), (ii) \(f(x) = x^n\), (iii) \(f(x) = e^x\) 和 (iv) \(f(x) = \log x\) 的性质。
同样地,从第一性原理证明乘法、加法和除法法则。
证明常数倍法则可作为乘法法则的一个特例。
计算\(f(x) = x^x\)的导数。
对于某个\(x\),\(f'(x) = 0\)意味着什么?给出一个函数\(f\)和一个位置\(x\)的例子,说明这种情况可能成立。
绘制函数\(y = f(x) = x^3 - \frac{1}{x}\)并绘制其在\(x = 1\)处的切线。
求函数\(f(\mathbf{x}) = 3x_1^2 + 5e^{x_2}\)的梯度。
函数\(f(\mathbf{x}) = \|\mathbf{x}\|_2\)的梯度是什么?当\(\mathbf{x} = \mathbf{0}\)时会发生什么?
你能写出当\(u = f(x, y, z)\)且\(x = x(a, b)\)、\(y = y(a, b)\)和\(z = z(a, b)\)时的链式法则吗?
给定一个可逆函数\(f(x)\),计算其逆函数\(f^{-1}(x)\)的导数。这里我们有\(f^{-1}(f(x)) = x\),反之亦然\(f(f^{-1}(y)) = y\)。提示:在你的推导中利用这些性质。