动手学深度学习-预备知识4-微积分

291 阅读4分钟

大约2500年前,古希腊人通过将多边形分成小三角形,再把这些三角形的面积加起来,找到了计算多边形面积的方法。为了计算像圆这种曲线形状的面积,他们发明了“内接多边形”的方法:在曲线内画一个多边形,随着边数越来越多,多边形就越来越接近圆。这种方法叫做逼近法。

polygon-circle.svg

图4.1 用逼近法求圆的面积

实际上,逼近法就是积分的雏形。几千年后,另一种微积分分支——微分,也被发明了。微分主要用于解决优化问题,简单来说,就是找到让事情做到最好的方法。在深度学习中,优化问题随处可见。

在深度学习里,我们通过“训练”模型,不断调整它的参数,让模型在看到更多数据后表现得越来越好。通常,表现更好意味着我们在最小化损失函数,也就是一个用来衡量模型表现有多差的分数。最终,我们希望模型在从未见过的数据上也能有良好表现,但训练过程只能让模型适应已知数据。

因此,训练模型可以分为两个核心问题:

  • 优化:通过调整模型参数,使它更好地拟合已有数据;
  • 泛化:通过数学理论和经验,确保模型在未见过的数据上也能有好表现。

为了更好理解后续的优化问题和方法,本文提供了一个简明的微分基础教程,帮助读者快速掌握深度学习中的常用微分知识。

1. 导数和微分

我们首先讨论如何计算导数,这是几乎所有深度学习优化算法的核心步骤。在深度学习中,通常选择对于模型参数可微的损失函数。简单来说,对于每个参数,我们希望知道当这个参数增加或减少一个极小的量时,损失函数的变化速度。

假设我们有一个函数 𝑓(𝑥)𝑓(𝑥),其输入和输出都是标量。如果这个函数的导数存在,那么导数的定义可以用下面的公式表示:

f(x)=limh0f(x+h)f(x)hf'(x) = \lim_{h \to 0} \frac{f(x+h) - f(x)}{h}

如果这个极限存在,我们就说 𝑓(𝑥)𝑓(𝑥) 在 𝑥 处是可微的。若函数在某个区间的每个点上都是可微的,则称该函数在该区间内是可微的。导数 𝑓(𝑥)𝑓'(𝑥) 可以解释为 𝑓(𝑥)𝑓(𝑥) 在 𝑥 处的瞬时变化率,即当 ℎ 很小时,𝑓(𝑥)𝑓(𝑥) 随着 𝑥 的变化速度。

1.1 实验:计算导数

为了更好地理解导数,假设我们定义一个简单的函数:f(x)=3x24xf(x) = 3x^2 - 4x

def f(x):
    return 3 * x ** 2 - 4 * x

现在,我们通过减小 ℎ 值,来计算数值导数。我们将使用以下代码进行实验:

def numerical_lim(f, x, h):
    return (f(x + h) - f(x)) / h


h = 0.1
for i in range(5):
    print(f'h={h:.5f}, numerical limit={numerical_lim(f, 1, h):.5f}')
    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

从上面的结果可以看出,随着 h 越来越小,数值结果越来越接近 22,这说明当 h0ℎ→0 时,导数的精确值为 22

1.2 导数符号

对于导数,以下符号是等价的:

f(x)=ddxf(x)=dfdxf'(x) = \frac{d}{dx}f(x) = \frac{df}{dx}

其中,ddx\frac{d}{dx}dfdx\frac{df}{dx} 都是微分运算符,表示对 𝑥 求导数。为了求出常见函数的导数,可以使用以下规则:

  • 常数的导数为 0。
  • 幂函数的导数:ddxxn=nxn1\frac{d}{dx}x^n=n x^{n-1}(其中 𝑛 是实数)。

1.3 微分法则

若 𝑓(𝑥) 和 𝑔(𝑥) 都是可微的函数,且 𝑐 是常数,那么以下法则适用:

  • 常数相乘法则ddx[cf(x)]=cf(x)\frac{d}{dx} [c \cdot f(x)] = c \cdot f'(x)

f(x)=3x2f(x) = 3x^2,其中 c=3c = 3,那么导数为:f(x)=3ddx(x2)=32x=6xf'(x) = 3 \cdot \frac{d}{dx}(x^2) = 3 \cdot 2x = 6x

  • 加法法则ddx[f(x)+g(x)]=f(x)+g(x)\frac{d}{dx} [f(x) + g(x)] = f'(x) + g'(x)

f(x)=x2f(x) = x^2g(x)=2xg(x) = 2x,那么导数为:(f(x)+g(x))=ddx(x2)+ddx(2x)=2x+2(f(x) + g(x))' = \frac{d}{dx}(x^2) + \frac{d}{dx}(2x) = 2x + 2

  • 乘法法则ddx[f(x)g(x)]=f(x)g(x)+f(x)g(x)\frac{d}{dx} [f(x) \cdot g(x)] = f'(x) \cdot g(x) + f(x) \cdot g'(x)

f(x)=x2f(x) = x^2g(x)=3xg(x) = 3x,那么导数为:

(f(x)g(x))=ddx(x2)3x+x2ddx(3x)=2x3x+x23=6x2+3x2=9x2(f(x)g(x))' = \frac{d}{dx}(x^2) \cdot 3x + x^2 \cdot \frac{d}{dx}(3x) = 2x \cdot 3x + x^2 \cdot 3 = 6x^2 + 3x^2 = 9x^2
  • 除法法则ddx[f(x)g(x)]=f(x)g(x)f(x)g(x)g(x)2\frac{d}{dx} \left[\frac{f(x)}{g(x)}\right] = \frac{f'(x) \cdot g(x) - f(x) \cdot g'(x)}{g(x)^2}

f(x)=x2f(x) = x^2g(x)=x+1g(x) = x+1,那么导数为:

(x2x+1)=2x(x+1)x2(1)(x+1)2=2x2+2xx2(x+1)2=x2+2x(x+1)2\left(\frac{x^2}{x+1}\right)' = \frac{2x(x+1) - x^2(1)}{(x+1)^2} = \frac{2x^2 + 2x - x^2}{(x+1)^2} = \frac{x^2 + 2x}{(x+1)^2}

1.4 导数的可视化

我们需要定义几个函数,保存在一个独立的包d2l中,以后无须重新定义就可以直接调用它们(例如,d2l.use_svg_display())。

import sys
from matplotlib import pyplot as plt
from matplotlib_inline import backend_inline

# 设置中文字体(SimHei 是黑体字)
plt.rcParams['font.sans-serif'] = ['SimHei']  # 使用黑体
plt.rcParams['axes.unicode_minus'] = False  # 解决负号 '-' 显示成方块的问题
d2l = sys.modules[__name__]


def use_svg_display():  # @save
    """使用svg格式在Jupyter中显示绘图"""
    backend_inline.set_matplotlib_formats('svg')


# 我们定义set_figsize函数来设置图表大小。
# 注意,这里可以直接使用d2l.plt,因为导入语句 from matplotlib import pyplot as plt已标记为保存到d2l包中。
def set_figsize(figsize=(3.5, 2.5)):  # @save
    """设置matplotlib的图表大小"""
    use_svg_display()
    d2l.plt.rcParams['figure.figsize'] = figsize


# 下面的set_axes函数用于设置由matplotlib生成图表的轴的属性。
def set_axes(axes, xlabel, ylabel, xlim, ylim, xscale, yscale, legend):
    """设置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函数来简洁地绘制多条曲线,因为我们需要在整个书中可视化许多曲线。
def plot(X, Y=None, xlabel=None, ylabel=None, legend=None, xlim=None,
         ylim=None, xscale='linear', yscale='linear',
         fmts=('-', 'm--', 'g-.', 'r:'), figsize=(3.5, 2.5), axes=None):
    """绘制数据点"""
    if legend is None:
        legend = []

    set_figsize(figsize)
    axes = axes if axes else d2l.plt.gca()

    # 如果X有一个轴,输出True
    def has_one_axis(X):
        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)
    axes.cla()
    for x, y, fmt in zip(X, Y, fmts):
        if len(x):
            axes.plot(x, y, fmt)
        else:
            axes.plot(y, fmt)
    set_axes(axes, xlabel, ylabel, xlim, ylim, xscale, yscale, legend)
    plt.show()

为了更好地理解导数的几何意义,我们可以绘制函数 f(x)f(x) 及其在 x=1x=1处的切线y=2x3y=2x-3。切线的斜率就是该点的导数值。使用 matplotlib 绘图库,我们可以创建以下可视化:

import numpy as np
import d2l


def f(x):
    return 3 * x ** 2 - 4 * x


x = np.arange(0, 3, 0.1)
d2l.plot(x, [f(x), 2 * x - 3], 'x', 'f(x)', legend=['f(x)', 'Tangent line(切线) (x=1)'])

image.png

2. 偏导数

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

y=f(x1,x2,,xn)y=f(x_1, x_2, \ldots, x_n) 是一个具有 nn 个变量的函数。关于第 ii 个参数 xix_i 的偏导数(partial derivative)为:

yxi=limΔxi0f(x1,,xi+Δxi,,xn)f(x1,,xi,,xn)Δxi\frac{\partial y}{\partial x_i} = \lim_{\Delta x_i \to 0} \frac{f(x_1, \ldots, x_i + \Delta x_i, \ldots, x_n) - f(x_1, \ldots, x_i, \ldots, x_n)}{\Delta x_i}

为了计算 fxi\frac{\partial f}{\partial x_i},我们可以简单地将 x1,x2,,xi1,xi+1,,xnx_1, x_2, \ldots, x_{i-1}, x_{i+1}, \ldots, x_n 看作常数,并计算 ff 关于 xix_i 的导数。对于偏导数的表示,以下是等价的(三种不同的表示方式):

yxi=fxi=xif(x)=Dxif(x)\frac{\partial y}{\partial x_i}=\frac{\partial f}{\partial x_i} = \frac{\partial}{\partial x_i} f(x) = D_{x_i} f(x)

3. 梯度

我们可以连结一个多元函数对其所有变量的偏导数,以得到该函数的梯度gradient)向量。具体而言,设函数 ff 的输入是一个 𝑛 维向量 x=[x1,x2,,xn]T\mathbf{x}=[x_1,x_2,…,x_n]^T,并且输出是一个标量。函数 𝑓𝑓 相对于 𝑥𝑥 的梯度是一个包含 𝑛 个偏导数的向量

g=xf(x)=[f(x)x1,f(x)x2,,f(x)xn]\mathbf{g} = \nabla_{\mathbf{x}} f(\mathbf{x}) = \left[ \frac{\partial f(\mathbf{x})}{\partial x_1}, \frac{\partial f(\mathbf{x})}{\partial x_2}, \dots, \frac{\partial f(\mathbf{x})}{\partial x_n} \right]

其中 xf(x)∇xf(x) 通常在没有歧义时被简化为 f∇f

假设 x=[x1,x2,,xn]\mathbf{x}=[x1,x2,…,xn]nn 维向量,在微分多元函数时经常使用以下规则:

  • 对于所有 ARm×n\mathbf{A} \in \mathbb{R}^{m \times n} ,都有 xAx=AT\nabla_x \mathbf{A}_x = \mathbf{A}^T
  • 对于所有 ARm×n\mathbf{A} \in \mathbb{R}^{m \times n} ,都有 xxTAx=A\nabla_x \mathbf{x}^T \mathbf{A}_x = \mathbf{A}
  • 对于所有 ARm×n\mathbf{A} \in \mathbb{R}^{m \times n} ,都有 xxTAx=(A+AT)x\nabla_x \mathbf{x}^T \mathbf{A}_x = (\mathbf{A} + \mathbf{A}^T) \mathbf{x}
  • xx2=xxTx=2x\nabla_x \| \mathbf{x} \|^2 = \nabla_x \mathbf{x}^T \mathbf{x} = 2\mathbf{x }

4. 链式法则

然而,上面方法可能很难找到梯度。这是因为在深度学习中,多元函数通常是复合(composite)的,所以难以应用上述任何规则来微分这些函数。幸运的是,链式法则可以被用来微分复合函数。 让我们先考虑单变量函数。假设函数 y=f(u)y=f(u)u=g(x)u=g(x) 都是可微的,根据链式法则:

dydx=dydududx\frac{d_y}{d_x} = \frac{d_y}{d_u} \frac{d_u}{d_x}

现在考虑一个更一般的场景,即函数具有任意数量的变量的情况。 假设可微分函数 yy 有变量 u1,u2,...,umu_1,u_2,...,u_m,其中每个可微分函数 uiu_i都有变量 x1,x2,...,xnx_1,x_2,...,x_n。 注意,yyx1,x2,...,xnx_1,x_2,...,x_n的函数。对于任意i=1,2,...,ni = 1,2,...,n,链式法则给出:

yxi=yu1u1xi+yu2u2xi++yununxi\frac{\partial_y}{\partial x_i} = \frac{\partial_y}{\partial u_1} \frac{\partial {u_1}}{\partial x_i} + \frac{\partial_y}{\partial u_2} \frac{\partial {u_2}}{\partial x_i} + \dots + \frac{\partial_y}{\partial u_n} \frac{\partial {u_n}}{\partial x_i}

雅可比矩阵(Jacobian Matrix)是向量值函数的一阶偏导数矩阵,它描述了一个向量函数在某一点处的局部变化特性。简单来说,它可以看作是对多元函数的偏导数进行组织和排列的一种方式。

定义

设有一个从 Rn\mathbb{R}^nRm\mathbb{R}^m 的向量值函数:

f(x)=[f1(x)f2(x)fm(x)]\mathbf{f}(\mathbf{x}) = \begin{bmatrix} f_1(\mathbf{x}) \\ f_2(\mathbf{x}) \\ \vdots \\ f_m(\mathbf{x}) \end{bmatrix}

其中x=[x1x2xn]\mathbf{x} = \begin{bmatrix} x_1 & x_2 & \dots & x_n \end{bmatrix}^\topRn\mathbb{R}^n 中的输入变量,而 f(x)\mathbf{f(x)}Rm\mathbb{R}^m 中的输出向量。

雅可比矩阵是函数 𝑓\mathbf{𝑓} 对变量 𝑥\mathbf{𝑥} 的所有偏导数组成的一个矩阵。其形式为:

Jf(x)=[f1x1f1x2f1xnf2x1f2x2f2xnfmx1fmx2fmxn]\mathbf{J}_{\mathbf{f}}(\mathbf{x}) = \begin{bmatrix} \frac{\partial f_1}{\partial x_1} & \frac{\partial f_1}{\partial x_2} & \dots & \frac{\partial f_1}{\partial x_n} \\ \frac{\partial f_2}{\partial x_1} & \frac{\partial f_2}{\partial x_2} & \dots & \frac{\partial f_2}{\partial x_n} \\ \vdots & \vdots & \ddots & \vdots \\ \frac{\partial f_m}{\partial x_1} & \frac{\partial f_m}{\partial x_2} & \dots & \frac{\partial f_m}{\partial x_n} \end{bmatrix}
雅可比矩阵的作用
  • 线性近似:雅可比矩阵可以用来线性近似非线性函数的局部变化。函数在某一点的雅可比矩阵描述了它在该点附近的线性近似。
  • 牛顿法:在多元非线性方程求解中,牛顿法依赖于雅可比矩阵来更新解的近似值。
  • 机器学习与神经网络:在反向传播算法中,雅可比矩阵用于计算梯度,以调整模型的参数。

4.1 平方函数的复合

f(x)=(2x+3)2f(x) = (2x + 3)^2,我们可以将其看作 f(x)=u2f(x) = u^2,其中 u=2x+3u = 2x + 3。这是一个复合函数,外层是平方函数,内层是线性函数。

推导过程:
  1. 定义外层和内层函数:
  • 外层函数:f(u)=u2f(u) = u^2
  • 内层函数:u(x)=2x+3u(x) = 2x + 3
  1. 求内层函数的导数:

u(x)=ddx(2x+3)=2u'(x) = \frac{d}{dx}(2x + 3) = 2

  1. 求外层函数对内层的导数:

f(u)=ddu(u2)=2uf'(u) = \frac{d}{du}(u^2) = 2u

  1. 应用链式法则:

f(x)=f(u)u(x)=2u2=4(2x+3)f'(x) = f'(u) \cdot u'(x) = 2u \cdot 2 = 4(2x + 3)

因此,f(x)=(2x+3)2f(x) = (2x + 3)^2 的导数为 f(x)=4(2x+3)f'(x) = 4(2x + 3)

4.2 三角函数的复合

f(x)=sin(3x)f(x) = \sin(3x),我们可以将其看作 f(x)=sin(u)f(x) = \sin(u),其中 u=3xu = 3x。这是一个三角函数的复合。

推导过程:
  1. 定义外层和内层函数:
  • 外层函数:f(u)=sin(u)f(u) = \sin(u)
  • 内层函数:u(x)=3xu(x) = 3x
  1. 求内层函数的导数:

u(x)=ddx(3x)=3u'(x) = \frac{d}{dx}(3x) = 3

  1. 求外层函数对内层的导数:

f(u)=ddu(sin(u))=cos(u)f'(u) = \frac{d}{du}(\sin(u)) = \cos(u)

  1. 应用链式法则:

f(x)=f(u)u(x)=cos(u)3=3cos(3x)f'(x) = f'(u) \cdot u'(x) = \cos(u) \cdot 3 = 3\cos(3x)

因此,f(x)=sin(3x)f(x) = \sin(3x) 的导数为 f(x)=3cos(3x)f'(x) = 3\cos(3x)

4.3 指数函数的复合

f(x)=e5xf(x) = e^{5x},我们可以将其看作 f(x)=euf(x) = e^u,其中 u=5xu = 5x。这是一个指数函数的复合。

推导过程:
  1. 定义外层和内层函数:
  • 外层函数:f(u)=euf(u) = e^u
  • 内层函数:u(x)=5xu(x) = 5x
  1. 求内层函数的导数:

u(x)=ddx(5x)=5u'(x) = \frac{d}{dx}(5x) = 5

  1. 求外层函数对内层的导数:

f(u)=ddu(eu)=euf'(u) = \frac{d}{du}(e^u) = e^u

  1. 应用链式法则:

f(x)=f(u)u(x)=eu5=5e5xf'(x) = f'(u) \cdot u'(x) = e^u \cdot 5 = 5e^{5x}

因此,f(x)=e5xf(x) = e^{5x} 的导数为 f(x)=5e5xf'(x) = 5e^{5x}

4.4 向量链式法则

假设有如下的函数:

f(x)=g(u(x))f(\mathbf{x}) = g(\mathbf{u}(\mathbf{x}))

其中,x=[x1 x2]\mathbf{x} = \begin{bmatrix} x_1 \ x_2 \end{bmatrix} 是二维向量,u(x)=[u1(x) u2(x)]\mathbf{u}(\mathbf{x}) = \begin{bmatrix} u_1(\mathbf{x}) \ u_2(\mathbf{x}) \end{bmatrix} 也是二维向量。

已知:

u1(x)=x12+x22u_1(\mathbf{x}) = x_1^2 + x_2^2
u2(x)=x1+x2u_2(\mathbf{x}) = x_1 + x_2

g(u)=u1+u2g(\mathbf{u}) = u_1 + u_2 是标量函数。

要求:对 f(x)f(\mathbf{x}) 关于 x\mathbf{x} 求导。

推导过程
  1. 定义外层函数 g(u)g(\mathbf{u}) 和内层函数 u(x)\mathbf{u}(\mathbf{x})
  • 外层函数:g(u)=u1+u2g(\mathbf{u}) = u_1 + u_2
  • 内层函数:u(x)=[u1(x) u2(x)]\mathbf{u}(\mathbf{x}) = \begin{bmatrix} u_1(\mathbf{x}) \ u_2(\mathbf{x}) \end{bmatrix}
  1. 求内层函数的雅可比矩阵:

雅可比矩阵 Ju(x)\mathbf{J}_\mathbf{u}(\mathbf{x})u\mathbf{u}x\mathbf{x} 的偏导数矩阵:

Ju(x)=[u1x1u1x2u2x1u2x2]\mathbf{J}_\mathbf{u}(\mathbf{x}) = \begin{bmatrix} \frac{\partial u_1}{\partial x_1} & \frac{\partial u_1}{\partial x_2} \\ \frac{\partial u_2}{\partial x_1} & \frac{\partial u_2}{\partial x_2} \end{bmatrix}

首先计算:

u1x1=2x1,u1x2=2x2\frac{\partial u_1}{\partial x_1} = 2x_1, \quad \frac{\partial u_1}{\partial x_2} = 2x_2
u2x1=1,u2x2=1\frac{\partial u_2}{\partial x_1} = 1, \quad \frac{\partial u_2}{\partial x_2} = 1

因此,雅可比矩阵为:

Ju(x)=[2x12x211]\mathbf{J}_\mathbf{u}(\mathbf{x}) = \begin{bmatrix} 2x_1 & 2x_2 \\ 1 & 1 \end{bmatrix}
  1. 求外层函数对 u\mathbf{u} 的导数:

g(u)=u1+u2g(\mathbf{u}) = u_1 + u_2,因此对 u\mathbf{u} 求导得到梯度向量:

ug(u)=[gu1gu2]=[11]\nabla_{\mathbf{u}} g(\mathbf{u}) = \begin{bmatrix} \frac{\partial g}{\partial u_1} \\ \frac{\partial g}{\partial u_2} \end{bmatrix} = \begin{bmatrix} 1 \\ 1 \end{bmatrix}
  1. 应用链式法则:

根据链式法则,f(x)f(\mathbf{x}) 的梯度 xf(x)\nabla_{\mathbf{x}} f(\mathbf{x}) 可以表示为:

xf(x)=Ju(x)Tug(u)\nabla_{\mathbf{x}} f(\mathbf{x}) = \mathbf{J}_\mathbf{u}(\mathbf{x})^T \nabla_{\mathbf{u}} g(\mathbf{u})

将雅可比矩阵 Ju(x)\mathbf{J}_\mathbf{u}(\mathbf{x})ug(u)\nabla_{\mathbf{u}} g(\mathbf{u}) 代入公式:

xf(x)=[2x112x21][11]\nabla_{\mathbf{x}} f(\mathbf{x}) = \begin{bmatrix} 2x_1 & 1 \\ 2x_2 & 1 \end{bmatrix} \begin{bmatrix} 1 \\ 1 \end{bmatrix}

进行矩阵乘法计算:

xf(x)=[2x11+112x21+11]=[2x1+12x2+1]\nabla_{\mathbf{x}} f(\mathbf{x}) = \begin{bmatrix} 2x_1 \cdot 1 + 1 \cdot 1 \\ 2x_2 \cdot 1 + 1 \cdot 1 \end{bmatrix} = \begin{bmatrix} 2x_1 + 1 \\ 2x_2 + 1 \end{bmatrix}

最终结果为:

xf(x)=[2x1+12x2+1]\nabla_{\mathbf{x}} f(\mathbf{x}) = \begin{bmatrix} 2x_1 + 1 \\ 2x_2 + 1 \end{bmatrix}