深入浅出“反向传播”(二)

111 阅读13分钟

背景

通过上一篇文章的学习,相信大家对偏导数、梯度、多元函数、损失函数和反向传播等基本概念有了基础的认识。接下来,我们就来具体聊聊关于反向传播的另一个更专业更通用的术语,即自动微分。在开始之前,你是否还记得微分、导数、偏导数、梯度这些名词?我们先来复习一遍。

基础概念

梯度、微分和导数是微积分中的核心概念,它们在数学、物理和机器学习中广泛应用。虽然三者密切相关,但侧重点和应用场景不同。以下从定义、几何意义和应用角度进行对比解析:

1. 导数(Derivative)

定义

  • 单变量函数:导数描述函数在某一点处的瞬时变化率。数学形式: f(x)=limh0f(x+h)f(x)hf'(x) = \lim_{h \to 0} \frac{f(x+h) - f(x)}{h}

  • 物理意义:速度、加速度、斜率等。

  • 几何意义:曲线在某点处切线的斜率。

关键点

  • 导数是标量,仅适用于单变量函数。

  • 导数存在条件:函数在该点连续且左导数等于右导数。

示例: 若f(x)=x2f(x) = x^2,则导数为f(x)=2xf'(x) = 2x。在 x=3x=3处,导数为 66,表示此时函数值以速率 6 增长。

2. 微分(Differential)

定义

  • 线性近似:微分是函数在某点处的局部线性近似,即用切线代替曲线。数学形式:df=f(x)dxdf = f'(x) \cdot dx

其中 dxdx 是自变量的微小变化,dfdf 是因变量的近似变化量。

  • 微分形式:微分是导数的代数表达,例如 dy=2xdxdy = 2x \, dx

关键点

  • 微分是一个线性映射(函数增量的最佳线性近似)。

  • 在多元函数中,微分发展为全微分(Total Differential),例如:df=fxdx+fydydf = \frac{\partial f}{\partial x}dx + \frac{\partial f}{\partial y}dy

示例: 若 f(x)=x2f(x) = x^2,当 x=3x=3dx=0.1dx=0.1 时,微分近似为:df=60.1=0.6df = 6 \cdot 0.1 = 0.6。实际增量f(3.1)f(3)=0.61f(3.1)-f(3)=0.61,误差仅为 0.01。

3. 梯度(Gradient)

定义

  • 多变量函数的导数:梯度是导数的高维推广,指向函数增长最快的方向。数学形式(对于f(x1,x2,...,xn)f(x_1, x_2, ..., x_n)):

    f=(fx1,fx2,...,fxn)\nabla f = \left( \frac{\partial f}{\partial x_1}, \frac{\partial f}{\partial x_2}, ..., \frac{\partial f}{\partial x_n} \right)

  • 几何意义:梯度是一个向量,其方向是函数在一点处的最速上升方向,模长是变化率的最大值。

关键点

  • 梯度是向量场,适用于多变量函数。

  • 负梯度方向是函数下降最快的方向,这是优化算法(如梯度下降法)的基础。

示例:

f(x,y)=x2+y2f(x, y) = x^2 + y^2,则梯度为:f=(2x,2y)\nabla f = (2x, 2y)

在点 (1,2)(1, 2) 处,梯度为 (2,4)(2, 4),表示从该点出发,沿方向 (2,4)(2,4) 函数值增长最快。

三者的联系与区别

在这里插入图片描述

实际应用

  1. 导数

    • 计算曲线的极值点(最大值/最小值)。

    • 牛顿法求方程的根。

  2. 微分

    • 误差分析(如泰勒展开的一阶近似)。

    • 微分方程建模(如 dy=kydxdy = k y \, dx)。

  1. 梯度

    • 机器学习:梯度下降法优化损失函数。

    • 物理场分析:电场、温度场的梯度描述场的变化。

    • 计算机图形学:法线方向计算(曲面的梯度方向)。

常见误区

  1. 导数 vs. 微分

    • 导数是变化率(比值),微分是线性近似(乘积)。

    • 例如,f(x)f'(x) 是导数值,df=f(x)dxdf = f'(x) dx 是微分。

  2. 梯度 vs. 偏导数

    • 梯度是所有偏导数组成的向量,而偏导数是沿某一坐标轴的变化率。

    • 梯度包含方向信息,偏导数是标量。

总结

  • 导数是单变量函数的瞬时变化率,微分是局部线性近似,梯度是多变量函数的最速上升方向。

  • 梯度是导数的向量推广,微分是导数的代数表达,三者共同构成微积分分析函数变化的核心工具。在机器学习和优化问题中,梯度尤其关键,直接指导模型参数的更新方向。

梯度下降

我们再回顾一下梯度下降的过程:

  1. 使用目标 [损失] 函数,计算模型在一组输入上的预测与这些输入的真实值之间的损失(误差)。

  2. 通过计算损失相对于模型每个参数的偏导数(梯度),找出参数对损失的影响。

  3. 通过将每个参数减去其相应的梯度并乘以一个称为学习率的超参数,朝着最小化损失的方向移动模型的参数。

  4. 清除所有梯度,并重复该过程,直到模型收敛,换句话说,直到模型不再改进并达到最佳性能;或者达到最大步长。

这个过程能够构建出强大的深度神经网络,但其中有一步很关键且复杂,那就是找到梯度以更新模型的参数。从数学的角度来看,我们如何对模型的参数求损失的导数?由此,我们引出自动微分(automatic differentiation)。

但是在介绍自动微分之前,我们先来了解两种替代方法,即数值微分和符号微分,以及它们为何无法满足基于梯度的神经网络的优化需求,以打消某些同学心中的疑惑。

数值微分(Numeric Differentiation)

在了解数值微分之前,我们需要大概了解下 泰勒展开, 因为它能帮助我们理解差分公式。

泰勒展开

泰勒展开是用多项式函数逼近光滑函数的一种数学工具,它将一个函数在某一点附近展开为无限项的多项式级数。泰勒展开的核心思想是:如果一个函数足够光滑(即无限可导),那么它在某一点附近的值可以用该点的函数值及其各阶导数的线性组合来近似表示。

1. 泰勒展开公式

f(x)f(x) 是一个在 x=ax = a 处无限可导的函数,则它在 x=ax = a 处的泰勒级数为:

f(x)=f(a)+f(a)(xa)+f(a)2!(xa)2+f(a)3!(xa)3+f(x) = f(a) + f'(a)(x-a) + \frac{f''(a)}{2!}(x-a)^2 + \frac{f'''(a)}{3!}(x-a)^3 + \cdots

或者写成求和形式:

f(x)=n=0f(n)(a)n!(xa)nf(x) = \sum_{n=0}^{\infty} \frac{f^{(n)}(a)}{n!} (x-a)^n

其中:

  • f(n)(a)f^{(n)}(a)f(x)f(x)x=ax = a 处的 第 nn 阶导数,

  • n!n!nn 的阶乘,

  • (xa)n(x-a)^n(xa)(x-a)nn 次幂。

有同学可能会疑惑,x=ax=a => xa=0x-a=0 => f(x)=f(a)f(x)=f(a), 啥情况?

是这样没错! 但泰勒级数的真正价值在于xax≠a时,用多项式近似 f(x)f(x), (xa)(x−a) 越小,逼近越精确。

这样 在 aa 附近就可以用低阶多项式近似复杂函数

特殊情况:麦克劳林展开(Maclaurin Series)

a=0a = 0 时,泰勒展开变为麦克劳林展开:

f(x)=f(0)+f(0)x+f(0)2!x2+f(0)3!x3+f(x) = f(0) + f'(0)x + \frac{f''(0)}{2!}x^2 + \frac{f'''(0)}{3!}x^3 + \cdots

2. 泰勒展开的直观理解

泰勒展开的核心思想是:

  1. 用多项式逼近函数:多项式计算简单,容易求导、积分,适合数值计算。

  2. 用导数信息构造近似:

    • 0 阶近似(常数):f(x)f(a)f(x) \approx f(a)(水平直线)

    • 1 阶近似(线性):f(x)f(a)+f(a)(xa)f(x) \approx f(a) + f'(a)(x-a)(切线)

    • 2 阶近似(抛物线):f(x)f(a)+f(a)(xa)+f(a)2(xa)2f(x) \approx f(a) + f'(a)(x-a) + \frac{f''(a)}{2}(x-a)^2

    • 高阶近似:增加更多项以提高精度。

示例:用泰勒展开近似exe^xx=0x=0附近

ex1+x+x22!+x33!+x44!+e^x \approx 1 + x + \frac{x^2}{2!} + \frac{x^3}{3!} + \frac{x^4}{4!} + \cdots

  • 0 阶近似:ex1e^x \approx 1(误差较大)

  • 1 阶近似:ex1+xe^x \approx 1 + x(直线)

  • 2 阶近似:ex1+x+x22e^x \approx 1 + x + \frac{x^2}{2}(抛物线)

  • 更高阶:逼近更精确。

3. 泰勒展开的余项(截断误差)

泰勒展开通常是无限级数,但在实际计算中我们只能取有限项(如 nn 阶近似),因此会引入截断误差(Truncation Error),即余项 Rn(x)R_n(x)

f(x)=k=0nf(k)(a)k!(xa)k泰勒多项式+Rn(x)余项f(x) = \underbrace{\sum_{k=0}^{n} \frac{f^{(k)}(a)}{k!}(x-a)^k}_{\text{泰勒多项式}} + \underbrace{R_n(x)}_{\text{余项}}

余项 Rn(x)R_n(x) 表示被忽略的高阶项的影响,常见形式有:

  1. 拉格朗日余项(Lagrange Remainder):

Rn(x)=f(n+1)(ξ)(n+1)!(xa)n+1,ξ(a,x)R_n(x) = \frac{f^{(n+1)}(\xi)}{(n+1)!}(x-a)^{n+1}, \quad \xi \in (a, x)

  • 其中 ξ\xiaaxx 之间的某个点。

  • 用于估计截断误差的上界。

  1. 佩亚诺余项(Peano Remainder):

Rn(x)=o((xa)n)R_n(x) = o((x-a)^n)

  • 表示余项比 (xa)n(x-a)^n 更快趋近于 0(当 xax \to a 时)。

4. 泰勒展开的收敛性

泰勒级数并不总是收敛到原函数,收敛性取决于:

  • 函数的解析性:函数是否无限可导。

  • 收敛半径:

    • 对于某些函数(如 11x\frac{1}{1-x}),泰勒级数只在 x<1|x| < 1 时收敛。

    • 对于 ex,sinx,cosxe^x, \sin x, \cos x,泰勒级数对所有实数 xx 收敛。

5. 常见函数的泰勒展开

在这里插入图片描述

截断误差(泰勒展开在差分中的应用)

在数值微分中,截断误差(Truncation Error)是指由于使用有限差分近似代替导数时,忽略泰勒级数展开中的高阶项而引入的误差。这种误差是数值方法本身固有的,与舍入误差(计算机浮点数精度限制)不同。

截断误差的来源

我们可以利用泰勒展开来近似计算导数(也就是我们接下来要提到的数值微分方法),例如,用以下差分公式近似一阶导数:

  1. 前向差分:

f(x)f(x+h)f(x)hf'(x)\approx\frac{f(x+h)-f(x)}{h}

  1. 后向差分:

f(x)f(x)f(xh)hf'(x)\approx\frac{f(x)-f(x-h)}{h}

  1. 中心差分:

f(x)f(x+h)f(xh)2hf'(x)\approx\frac{f(x+h)-f(x-h)}{2h}

这些公式的推导都可以基于泰勒级数展开。例如,对 f(x+h)f(x+h)f(xh)f(x-h) 展开:

f(x+h)=f(x)+hf(x)+h22f(x)+h36f(x)+f(x+h) = f(x) + h f'(x) + \frac{h^2}{2} f''(x) + \frac{h^3}{6} f'''(x) + \cdots

f(xh)=f(x)hf(x)+h22f(x)h36f(x)+f(x-h)=f(x)-hf'(x)+\frac{h^2}{2}f''(x)-\frac{h^3}{6}f'''(x)+\cdots

将泰勒展开代入差分公式后,截断误差就出现了,也就是被忽略的高阶项。

我们来代入看看:

  1. 前向/后向差分:

    • 通过泰勒展开可得:

    f(x+h)f(x)h=f(x)+h2f(x)+\frac{f(x+h)-f(x)}{h}=f'(x)+\frac{h}{2}f''(x)+\cdots

    • 截断误差的主项为 h2f(x)\frac{h}{2} f''(x),是 O(h)O(h)(一阶精度, 线性收敛)。
  2. 中心差分:

    • 利用泰勒展开相减消去偶数阶项:

    f(x+h)f(xh)2h=f(x)+h26f(x)+\frac{f(x+h)-f(x-h)}{2h}=f'(x)+\frac{h^2}{6} f'''(x)+\cdots

    • 截断误差的主项为 h26f(x)\frac{h^2}{6} f'''(x),是 O(h2)O(h^2)(二阶精度, 更加精确)。

关键点

  1. 步长hh的影响:

    • 截断误差随 hh 减小而减小,但过小的 hh 会放大舍入误差(如浮点精度限制)。

    • 存在一个最优 hh 平衡截断误差和舍入误差。

  2. 高阶差分公式:

    • 通过组合更多点的差分(如五点差分),可以进一步降低截断误差(例如 O(h4)O(h^4))。
  1. 函数光滑性:

    • 若函数的高阶导数不存在或不连续(如非光滑函数),截断误差的分析会失效。

极限求梯度?

接下来,进入正题,用数值微分来计算梯度。别着急,在这之前,我们先回顾下怎么通过使用极限定义来求导。

dfdx=limh0f(x+h)f(x)h\frac{df}{dx}=\lim_{ h\to 0}\frac{f(x + h)-f(x)}{h}

我们要计算函数f:RRf: \R \to \R 在输入 xx 处的导数,需要找到在 xx 处的切线斜率。切线斜率可以分解为函数在 xx 处的结果ffxx变化的比率。为了计算f(x)f(x)的变化量,我们基于两个输入来计算,一个是原始输入,另一个是输入加上一个小常数 hh ,从以上公式的分子中可以看出。接下来,我们将ff的变化量除以xx的变化量,也就是hh。当 hh 趋近于零时,我们能得到f(x)f(x)xx 处导数的近似值。

到这里,有人可能会觉得,那我直接给h赋值一个很小的数字,就能得到f(x)f(x)xx 处导数了吧?

No!Why?直接取极小的扰动值 hh 看似符合导数的极限定义,但在实际计算中会面临两大问题:

  • 舍入误差: 因为计算机精度有限,浮点数有最小步长限制(如单精度浮点数精度约为 10710^{-7},双精度约为 101610^{-16})。

  • 这种近似极限的方式其实缺乏误差阶数的理论指导, 没办法对误差进行量化。

这时,基于泰勒展开的差分法就应运而生了。

差分法求梯度

在实践中,神经网络对多维数组(通常称为张量)进行算术运算,而程序化地取极限是没有意义的(上面已经提到),因此我们舍弃极限并重写表达式以作用于网络的参数。

fθif(θ+hei)f(θ)h+O(h)\frac{\partial f}{\partial \theta_i}\approx\frac{f(\theta+h⋅e_i)−f(\theta)}{h}+O(h)

这个公式就是计算多元函数偏导数的前向差分法。

f:RnRf:R^n→R 是关于单个参数 θi\theta_i 的偏导数,来自参数向量 θ\theta 。符号 eie_i代表一个单位向量,其中第i个元素为1,而所有其他元素为零。计算 θi\theta_i 参数的偏导数就简单了,只需用 h0h\approx0 代入计算上述公式。在神经网络中, ff是损失函数,而 θ\theta是模型的参数。通过在模型的所有参数上计算上述公式,我们就能得到一次梯度下降所需要的梯度。

上面提到过前向差分法的截断误差O(h)O(h)较大,我们直接优化成中心差分法,如下:

fθif(θ+hei)f(θhei)2h+O(h2)\frac{\partial f}{\partial \theta_i}\approx\frac{f(\theta+h⋅e_i)−f(\theta-h⋅e_i)}{2h}+O(h^2)

中心差分是前向差分和后向差分的组合。通过从前向差分中减去后向差分并进行简化, O(h)O(h) 中的一阶误差项将相互抵消,留下二阶误差项作为主导。误差现在就与 hh 的平方成正比,这意味着如果 hh 减少一个数量级,误差将减少两个数量级。 在这里插入图片描述

图中展示了在x=2x=2处对cos(x)cos(x)的导数,使用h=0.5h=0.5进行差分方法(所有计算使用 32 位浮点数)。可以看到采用中心差分比前向和后向差分有更好的近似效果。另外,我们也能看到实际导数sin(2)−sin⁡(2)与因截断误差导致的近似导数之间的差异。

有人可能会说,那我们可以不断减少 hh,因为当 h0h\approx0 时,截断误差就不存在了。理论上来说,这确实能够消除我们在中心差分中的误差。但这样做会产生另一个副作用,上面其实也提到过好几次,就是会产生舍入误差。

舍入误差是由于计算机中数字表示的不准确性而引入的误差。比如 IEEE 754 标准在程序中使用单精度浮点数(float32)表示实数。神经网络在实际训练时依赖于这些表示方式。浮点数分配了固定的空间(在大多数情况下为 32 位),这限制了对任意大或小值的某些精度。联系上面的差分方法,如果hh变得太小,可能直接下溢为零,也就是丧失了数值的信息。

这样来看,当我们试图减小 hh 以减小截断误差时,我们反而增加了舍入误差。事实上,舍入误差与 hh 的取值大小成反比。例如,如果我们将 hh 减半,就会使舍入误差加倍。所以为了计算得到更加准确的梯度,这种截断误差和舍入误差之间的平衡就是我们需要考虑的一种权衡因素。 在这里插入图片描述

截断误差(Truncation)与舍入误差(Round-off):上图展示的是采样后的前向差分法(forward difference)和中心差分法(central difference)的误差,针对函数f(x)=(x10)2(3x23x+1)f(x)=(x−10)^2(3x^2−3x+1)hh使用单精度浮点值,范围为[107,1][10^{−7},1]。可以看出,刚开始,随着hh的减小,实际误差随着截断误差的减小而减小,但是当hh减小到一定程度后,舍入误差就开始占主导地位,导致实际误差反而随着舍入误差的增大而增大。

当然,有同学会说,那我们直接用更高精度的数据类型(例如 float64),但这会增加硬件约束,因为需要更多的内存和额外的计算,但这其实是一个完全不必要的权衡。另外,差分法还有一个问题:运行时的复杂性。

在输入向量长度为 nn 且输出为标量的情况下,我们需要 O(n)O(n) 次操作。如果我们有函数 f:RnRmf:R^n→R^m,我们大约需要O(mn)O(mn)次操作来计算梯度,这使得对于大值 mmnn 的计算就变得非常低效。考虑到梯度下降是一个应用于数百万甚至数十亿个参数的迭代过程,差分法明显无法适应。

那让我们可以转向另一种替代方法,即符号微分法。

欲知符号微分,且听下回分解!