跳转至

第 6 章:深度前馈网络(Deep Feedforward Networks)

6.1 示例:学习 XOR(Example: Learning XOR)

深度前馈网络(deep feedforward network),也常称为前馈神经网络或多层感知机(MLP),是深度学习最具代表性的模型。其目标是逼近某个目标函数 \(f^*(x)\):在分类问题里 \(y = f^*(x)\) 把输入 \(x\) 映射到类别 \(y\),前馈网络用 \(f(x; \theta)\) 学习参数 \(\theta\) 得到最优逼近。之所以称为"前馈",是因为信息流是单向的——从 \(x\) 经中间计算到 \(y\),没有把模型输出回送的反馈连接;带反馈连接的版本则属于循环神经网络(第 10 章)。前馈网络在工业界极其重要:用于照片物体识别的卷积网络就是一类专门化的前馈网络;它们也是通往循环网络的概念阶梯,循环网络驱动很多自然语言应用。

前馈神经网络称为"网络"是因为它们通常由多个不同函数复合表示,对应一个描述函数如何复合的有向无环图。链式结构 \(f(x) = f^{(3)}(f^{(2)}(f^{(1)}(x)))\) 是最常用的神经网络结构:\(f^{(1)}\) 称为第一层,\(f^{(2)}\) 称为第二层,等等;链的总长度给出模型的深度,"深度学习"之名由此而来。最终层称为输出层。训练时驱动 \(f(x)\) 匹配 \(f^*(x)\):训练数据提供 \(f^*(x)\) 在不同训练点处的有噪近似样本,每个样本 \(x\) 配有标签 \(y \approx f^*(x)\)。训练样本直接规定输出层在每个 \(x\) 处必须产生接近 \(y\) 的值;其他层的行为不直接由训练数据规定,学习算法必须决定如何使用这些层以产生所需输出,但训练数据并不指定每个个体层应做什么。因为训练数据没有给出这些层各自期望的输出,它们被称为隐藏层(hidden layer)。

之所以称为"神经",是因为结构上受神经科学松散启发:每个隐藏层通常是向量化的,其维度决定模型宽度;每元素可类比一个神经元在并行运作,每单元接收来自其他许多单元的输入并计算其激活值。使用许多层向量化表示的思想受神经科学启发;选择 \(f^{(i)}(x)\) 也松散受生物神经元计算函数的神经科学观察指导。但现代神经网络研究受许多数学和工程学科指导,目标不是完美建模大脑;最好把前馈网络视为实现统计泛化的"函数逼近机器",偶尔借鉴我们对大脑的认识,而非脑功能模型。

理解前馈网络的一条路径是从线性模型出发并考虑如何克服其局限。线性模型(线性回归、逻辑回归)很吸引人,因为可以闭式或凸优化高效可靠地拟合。但线性模型的明显缺陷是模型容量被限制为线性函数——因此模型无法理解任意两个输入变量之间的交互。线性模型可以扩展为:通过把线性模型不直接应用于 \(x\) 而应用于变换后的输入 \(\phi(x)\)\(\phi\) 是非线性变换,等价地用 5.7.2 节描述的核技巧隐式应用 \(\phi\) 映射来获得非线性学习算法。问题在于如何选择映射 \(\phi\):选项一是用非常通用的 \(\phi\)(如基于 RBF 核的核机隐式使用的无限维 \(\phi\)),\(\phi(x)\) 维数足够高时总能有足够容量拟合训练集,但泛化到测试集往往仍差;极通用特征映射通常仅基于局部平滑性原则,不编码足够先验信息解决高级问题。选项二是手工工程 \(\phi\),深度学习出现前的主流做法,要求每个独立任务数十年人力投入,且不同领域(语音识别、计算机视觉)之间迁移很少。选项三是深度学习的策略——学习 \(\phi\):模型 \(y = f(x; \theta, w) = \phi(x; \theta)^\top w\),参数 \(\theta\) 用于从宽泛函数类中学习 \(\phi\),参数 \(w\) 用于从 \(\phi(x)\) 映射到所需输出;这是深度前馈网络的例子,\(\phi\) 定义一个隐藏层。三种选项中这是唯一放弃训练问题凸性的,但收益远超代价:参数化表示为 \(\phi(x; \theta)\),用优化算法找对应好表示的 \(\theta\)。该方法通过使用非常宽泛的 \(\phi(x; \theta)\) 族可获得第一种方法的高通用性,也可获得第二种方法的优势——人类从业者可通过设计他们预期表现好的 \(\phi(x; \theta)\) 族来编码知识以帮助泛化,优势是人只需找正确的通用函数族而非精确正确的函数。

通过学习特征改进模型的一般原则超出本章描述的前馈网络,是贯穿全书深度学习的反复出现主题,适用于全书后续描述的所有模型。前馈网络是该原则在学习确定性映射(无反馈连接)从 \(x\)\(y\) 上的应用;后续模型将该原则应用于学习随机映射、带反馈函数、单向量上概率分布。本章以一个简单的前馈网络例子开始;接着处理部署前馈网络所需的每个设计决策:训练前馈网络需要做与线性模型相同的设计决策(选择优化器、代价函数、输出单元形式),回顾这些基于梯度学习的基础,然后面对前馈网络独有的设计决策。前馈网络引入了隐藏层概念,这要求我们选择用于计算隐藏层值的激活函数;还须设计网络架构,包括网络应含多少层、层之间如何连接、每层含多少单元。深度神经网络学习需要计算复杂函数的梯度,本章介绍反向传播算法及其现代推广,可用于高效计算这些梯度;最后以历史视角收尾。

XOR("异或")函数是二元值 \(x_1\)\(x_2\) 上的操作:当恰好其中一个二元值等于 1 时返回 1,否则返回 0;它提供目标函数 \(y = f^*(x)\) 作为我们想要学习的东西。模型提供 \(y = f(x; \theta)\),学习算法调整参数 \(\theta\) 使 \(f\) 尽可能接近 \(f^*\)。此简单例子不关心统计泛化,希望网络在四点 \(X = \{[0,0]^\top, [0,1]^\top, [1,0]^\top, [1,1]^\top\}\) 上表现正确,将在所有四点上训练网络,挑战仅是拟合训练集。我们可以把问题视为回归问题并用均方误差损失 \(J(\theta) = \frac{1}{4}\sum_{x \in X}(f^*(x) - f(x; \theta))^2\);MSE 在此仅用于简化数学,实践应用中 MSE 通常不是建模二元数据的合适代价函数,更合适的方法在 6.2.2.2 节描述。

选择线性模型 \(f(x; w, b) = x^\top w + b\) 时可用正规方程闭式求解 \(J(\theta)\),解得 \(w = 0, b = 1/2\),线性模型到处输出 0.5。原因是线性模型不能表示 XOR 函数(图 6.1 解释)。解决方法是让模型学习不同的特征空间使线性模型能表示解。引入一个非常简单的单隐藏层前馈网络,含两个隐藏单元,\(h = f^{(1)}(x; W, c)\) 是隐藏单元向量,第二层把这些值用作输入。输出层仍是线性回归模型,但应用于 \(h\) 而非 \(x\),整个模型是 \(f(x; W, c, w, b) = f^{(2)}(f^{(1)}(x))\)。若 \(f^{(1)}\) 仍为线性,则整个前馈网络仍是输入的线性函数(因为 \(f^{(1)}(x) = W^\top x\)\(f^{(2)}(h) = h^\top w\) 复合给出 \(f(x) = w^\top W^\top x = x^\top w'\) 其中 \(w' = Ww\)),所以必须用非线性函数描述特征。大多数神经网络用"由学习参数控制的仿射变换 + 固定非线性函数(激活函数)"两步实现此策略:\(h = g(W^\top x + c)\)\(W\) 提供线性变换权重,\(c\) 提供偏置。现代神经网络默认推荐用修正线性单元(ReLU,Jarrett et al., 2009; Nair and Hinton, 2010; Glorot et al., 2011a),定义为激活函数 \(g(z) = \max\{0, z\}\)(图 6.3)。

完整网络可写为 \(f(x; W, c, w, b) = w^\top \max\{0, W^\top x + c\} + b\)。给出一组具体解:\(W = \begin{pmatrix}1 & 1 \\ 1 & 1\end{pmatrix}\)\(c = \begin{pmatrix}0 \\ -1\end{pmatrix}\)\(w = \begin{pmatrix}1 \\ -2\end{pmatrix}\)\(b = 0\)。设计矩阵 \(X\) 含所有四点的二元输入,逐行:\(\begin{pmatrix}0 & 0 \\ 0 & 1 \\ 1 & 0 \\ 1 & 1\end{pmatrix}\)。第一步 \(XW = \begin{pmatrix}0 & 0 \\ 1 & 1 \\ 1 & 1 \\ 2 & 2\end{pmatrix}\),加偏置向量 \(c\) 后得 \(\begin{pmatrix}0 & -1 \\ 1 & 0 \\ 1 & 0 \\ 2 & 1\end{pmatrix}\);此时所有例子落在斜率 1 的直线上,沿此线输出需从 0 升到 1 再降回 0,线性模型不能实现。施加修正线性变换后 \(h = \begin{pmatrix}0 & 0 \\ 1 & 0 \\ 1 & 0 \\ 2 & 1\end{pmatrix}\),转换了样例间关系——它们不再在单一直线上,而是落在图 6.1 展示的特征空间,线性模型可在此空间解决问题。最后乘以权重向量 \(w\)\(\begin{pmatrix}0 \\ 1 \\ 1 \\ 0\end{pmatrix}\),神经网络获得批中每个样例的正确解。

此处简单指定了解并证明其得到零误差;真实场景中可能有数十亿模型参数和数十亿训练样本,不能像这样猜解;基于梯度的优化算法可找产生很小误差的参数。所述 XOR 问题的解是损失函数的全局最小,梯度下降能收敛到此点;梯度下降也能找到其他等价解,收敛点依赖参数初始值;实践中梯度下降通常找不到如此干净、易懂、整数值的解。

6.2 基于梯度的学习(Gradient-Based Learning)

设计并训练神经网络与用梯度下降训练任何其他机器学习模型差别不大——神经网络的设计方法可视为第 5 章描述的"模型 + 代价 + 优化器"框架在非线性模型族上的直接应用。5.10 节描述了如何通过指定优化过程、代价函数、模型族来构建机器学习算法,本节将此框架应用于神经网络这一非凸、非线性模型族。线性模型与神经网络之间最大的差别是神经网络的非线性使大多数有趣损失函数成为非凸。这意味着神经网络通常用迭代的、基于梯度的优化器训练,这些优化器仅把代价函数驱动到很低的值——而不是用训练线性回归的线性方程求解器,或用训练逻辑回归/SVM 的带全局收敛保证的凸优化算法。凸优化从任意初始参数都能收敛(理论上——实践上很稳健但可能遇到数值问题);SGD 应用于非凸损失函数则没有此保证,并且对参数初值敏感——不同初值可能收敛到显著不同的局部极小甚至鞍点,因此实践中需要多次随机重启或专门设计的初始化方案。对前馈神经网络而言,初始化所有权重重置为小随机值尤其重要,这是打破对称性(使不同单元学到不同特征)的必要条件——若所有权重初始化为相同的非零值,则前向传播中每层所有单元接收相同输入、计算相同输出;反向传播中每层所有单元的梯度也相同,导致权重更新后仍保持相同,整个网络退化为单有效单元。偏置可初始化为零或小的正值(小的正值如 0.1 对 ReLU 类激活函数特别有利,可使单元在初始化时即处于激活状态,让梯度能够流通)。训练前馈网络和几乎所有其他深度模型的迭代基于梯度的优化算法将在第 8 章详细描述,参数初始化尤其在 8.4 节讨论。

当前只需理解训练算法几乎总是基于以某种方式用梯度下降代价函数这一核心原则;具体算法是 4.3 节介绍的梯度下降和 5.9 节介绍的随机梯度下降思想上的改进与精化——具体地说,大多数现代深度学习训练算法是 SGD 的改进,包括动量、Adam、RMSProp 等,将在第 8 章展开。

当然也可以用梯度下降训练线性回归和支持向量机,训练集极大时这很常见——例如对亿级训练样本闭式解不可行但 SGD 只需几次遍历即可获得不错模型。从这个角度看,训练神经网络与训练其他任何模型差别不大;神经网络梯度计算稍复杂,但仍可高效精确地完成。6.5 节描述如何用反向传播算法及反向传播的现代推广获得梯度——这一方法在概念上对任意可微函数都成立,不仅限于神经网络,可应用于任何可微计算图。与其他机器学习模型一样,应用基于梯度的学习须选择代价函数,须选择如何表示模型输出;下面 6.2.1 和 6.2.2 节专门在神经网络场景下重新审视这些设计考虑,并揭示它们与具体输出单元形式之间的紧密耦合——具体地说,6.2.1 讨论两类典型代价函数(最大似然类 + 条件统计量类),6.2.2 讨论三类最常用输出单元(线性、sigmoid、softmax)以及更一般的输出类型。本节奠定后续 6.3-6.5 节展开隐藏单元、架构设计、反向传播算法的概念基础。

6.2.1.1 用最大似然学习条件分布(Learning Conditional Distributions with Maximum Likelihood)

深度神经网络设计的一个重要方面是代价函数的选择。幸运的是,神经网络代价函数与其他参数模型(线性模型)大体相同。大多数情况下,我们的参数模型定义分布 \(p(y|x; \theta)\),简单采用最大似然原理——用训练数据与模型预测之间的交叉熵作为代价函数,该交叉熵自动包含对 \(\log p_{\text{model}}\) 的负期望。有时用更简单的方法:不预测 \(y\) 上完整概率分布,而仅预测 \(y\) 在给定 \(x\) 条件下的某统计量(如均值、中位数、分位数等);此时可使用专门损失函数训练这些估计的预测器,例如下面 6.2.1.2 节将讨论的均方误差和平均绝对误差就是典型例子。训练神经网络的完整代价函数通常把本节介绍的主代价函数之一与一个正则化项结合:5.2.2 节在线性模型中见过的权重衰减方法可直接用于深度神经网络,也是最流行的正则化策略之一;神经网络更高级的正则化策略留到第 7 章详细介绍。本节先讨论现代神经网络最常用的最大似然框架(本小节),再讨论学习单个条件统计量(6.2.1.2),最后在 6.2.2 节回到这些代价函数与具体输出单元形式之间的紧密耦合。

大多数现代神经网络用最大似然训练,代价函数就是负对数似然,等价于训练数据与模型分布之间的交叉熵:\(J(\theta) = -\mathbb{E}_{x, y \sim \hat{p}_{\text{data}}} \log p_{\text{model}}(y|x)\)。代价函数的具体形式随模型而变,取决于 \(\log p_{\text{model}}\) 的具体形式;展开该式时常产生与模型参数无关的项可丢弃。例如若 \(p_{\text{model}}(y|x) = \mathcal{N}(y; f(x; \theta), I)\),可恢复均方误差代价 \(J(\theta) = \frac{1}{2}\mathbb{E}_{x, y \sim \hat{p}_{\text{data}}} \|y - f(x; \theta)\|^2 + \text{const}\),相差 \(1/2\) 缩放因子和不含 \(\theta\) 的项;丢弃的常数基于高斯分布的方差,本情形下未把方差参数化。5.5.1 节讨论过这一等价性在均值为高斯时成立;值得注意的是这一等价性对预测高斯均值的任意 \(f(x; \theta)\) 都成立,而不仅限于线性模型——意味着只要把输出分布选为高斯,均方误差就自动是负对数似然,无需专门设计。

从最大似然推导代价函数的最大优点是免去了为每个模型设计代价函数的负担:指定模型 \(p(y|x)\) 就自动确定代价函数 \(\log p(y|x)\)。神经网络设计反复出现的主题是:代价函数的梯度必须足够大且可预测,才能成为学习算法的好向导。饱和(变得很平)的函数会破坏此目标——它们使梯度变得极小,这在很多情形下是隐藏单元或输出单元激活函数饱和造成的。负对数似然有助于避免此问题:很多输出单元涉及 \(\exp\) 函数,其自变量极负时会饱和;负对数似然代价函数中的 \(\log\) 函数抵消了某些输出单元的 \(\exp\),因此即便 \(\exp\) 已经饱和,损失对参数的梯度仍可通过 \(\log\) 提供的导数获得有意义的下降方向。代价函数与输出单元选择间的相互作用将在 6.2.2 节展开讨论。

用于最大似然估计的交叉熵代价有一个不寻常的特性——应用于实践中常用模型时通常无最小值。对离散输出变量,大多数模型参数化方式使其无法表示概率 0 或 1,但可任意接近(逻辑回归即此类)。对实值输出变量,若模型能控制输出分布密度(例如学习高斯输出分布方差参数),则可对正确训练集输出赋予极高密度,使交叉熵趋近负无穷。第 7 章介绍的正则化技术提供多种方式修改学习问题,阻止模型以这种方式无限制获益。

6.2.1.2 学习条件统计量(Learning Conditional Statistics)

有时不学完整条件分布 \(p(y|x; \theta)\),而只学 \(y\) 在给定 \(x\) 条件下的某统计量,例如希望预测 \(y\) 均值,或预测 \(y\) 的某分位数(用于预测区间)等。这种设计选择有很多实际动机——例如对噪声数据进行鲁棒预测时希望预测中位数而非均值(中位数对离群点更鲁棒),对不确定度敏感的决策问题希望预测分位数(如风险管理的 VaR),对某些回归任务希望预测条件期望(由应用本身决定)。若用足够强大的神经网络,可把神经网络视为能表示宽类函数(受连续性、有界性等特征限制而非特定参数形式约束)的函数族——这种"宽类"足够宽到包含任意连续函数(详见 6.4.1 节通用逼近定理),这意味着我们可以自由选择希望预测的统计量类型,并保证神经网络能近似实现该统计量到任意精度。从这个视角,代价函数可被视为"泛函"(functional)——从函数到实数的映射——学习可视为选择函数而非仅选择参数;这与仅在固定参数形式中搜索参数值的传统参数化统计学习有概念上的区别。可设计代价泛函使其最小值出现在所希望函数处(例如映射 \(x\)\(y\) 在给定 \(x\) 条件下的期望),这正是变分法在最优控制、最优传输等领域发挥核心作用的地方。解关于函数的优化问题需要变分法(calculus of variations,19.4.2 节),理解本章内容不必理解变分法;只需知道变分法可推导两个重要结果,这两个结果告诉我们哪些代价函数对应哪些"最优"统计量。

第一个结果:解优化问题 \(f^* = \arg\min_f \mathbb{E}_{x, y \sim p_{\text{data}}} \|y - f(x)\|^2\) 得到 \(f^*(x) = \mathbb{E}_{y \sim p_{\text{data}}(y|x)}[y]\)(式 6.14-6.15),只要该函数落在我们优化的类中(且为正定核的可表示函数等温和条件下)。换言之若能在真实数据生成分布上训练无穷多样本,最小化均方误差代价函数给出对每个 \(x\) 预测 \(y\) 均值的函数。这一结果的直觉是显然的——均方误差惩罚大误差,因此最优预测是使期望平方误差最小化的条件均值;但变分法严格证明这一直觉确实对任意函数族成立。不同代价函数给出不同统计量:\(f^* = \arg\min_f \mathbb{E}_{x, y \sim p_{\text{data}}} \|y - f(x)\|_1\) 的解给出对每个 \(x\)\(y\) 中位数(式 6.16,只要该函数可被函数族描述),此代价常称平均绝对误差。这个结果在概念上很重要——它告诉我们"优化什么代价"等价于"预测什么统计量",这给设计损失函数提供了原则性指导。类似地,优化 pinball loss 可得条件分位数。

遗憾的是,均方误差和平均绝对误差与基于梯度的优化配合时往往表现不佳:一些会饱和的输出单元与这些代价函数结合时产生极小梯度(例如把 sigmoid 输出与均方误差结合时,sigmoid 在极值处饱和导致梯度消失,但 sigmoid 输出与交叉熵结合则没有此问题——见 6.2.2.2 节详细讨论)。这是交叉熵代价函数比均方误差/平均绝对误差更受欢迎的原因之一,即便不需估计完整分布 \(p(y|x)\)——交叉熵对饱和更鲁棒。但这不是说均方误差和平均绝对误差毫无用处——它们在非饱和输出(如线性输出、足够大的 softmax 输出不会饱和于单类的情形)下仍表现良好,且语义上对应"预测均值"和"预测中位数"这种统计意义清晰的量,因此当应用明确要求某统计量时仍可使用——例如对右偏分布的中位数回归常用于经济学和医学建模。

6.2.2.1 用于高斯输出分布的线性单元(Linear Units for Gaussian Output Distributions)

代价函数选择与输出单元选择紧密耦合——给定代价函数的形式,选择如何表示输出决定了交叉熵函数的具体形态。大多数情况下我们简单用数据分布与模型分布之间的交叉熵,输出表示方式则决定交叉熵函数的具体形式。可用作输出的任何神经网络单元也可用作隐藏单元;本节聚焦它们作为输出的使用,但原则上也可内部使用,6.3 节会详细讨论作为隐藏单元的使用。整个 6.2.2 节假设前馈网络提供由 \(h = f(x; \theta)\) 定义的隐藏特征;输出层作用是提供从这些特征到完成网络任务的进一步变换。下面依次介绍三种最常用的输出单元(线性单元本小节、sigmoid 单元 6.2.2.2、softmax 单元 6.2.2.3)以及"其他输出类型"(6.2.2.4),展示每种单元如何与具体代价函数结合,以及它们在概率建模上的角色。

一种简单的输出单元是基于无非线性仿射变换的输出单元,通常称为线性单元——它本质上就是第 2-3 章介绍的线性回归模型作为神经网络的输出层。给定特征 \(h\),线性输出层产生向量 \(\hat{y} = W^\top h + b\),其中 \(W\) 是权重矩阵、\(b\) 是偏置向量;这种仿射变换保留了输入的尺度(\(\hat{y}\) 可以是任意实数),适合需要无界连续输出的回归任务。线性输出层常用于产生条件高斯分布的均值 \(p(y|x) = \mathcal{N}(y; \hat{y}, I)\)(式 6.17 给出此条件高斯形式,其中协方差为恒等矩阵即各向同性高斯),此时最大化对数似然等价于最小化均方误差(这一等价性来自 6.2.1.1 节讨论的高斯-均方误差对应关系)。最大似然框架使学习高斯协方差、或使高斯协方差成为输入函数(异方差模型)变得直接,但协方差须约束为对所有输入正定;用线性输出层难以满足这种约束(因为线性输出 \(\hat{y}\) 可以是任意实数,无法自然产生正定矩阵所需的正值约束),所以通常用其他输出单元参数化协方差(6.2.2.4 节会展开)。因为线性单元不饱和——\(\hat{y}\) 关于 \(h\)\(W\)\(b\) 的梯度不存在饱和问题,\(\partial \hat{y}/\partial W = h\)\(h\) 有限时梯度有界——对基于梯度的优化算法几乎不构成困难,可与多种优化算法(包括 L-BFGS、共轭梯度等需要精确梯度信息的算法)配合使用。

线性单元也常被加在隐藏层上——此时它们起到线性降维或低秩分解的作用(详见 6.3.3 节);但在线性回归等需要实数输出的任务中,线性输出是首选。线性输出单元的一个重要细节是它们产生无界的连续输出,但某些任务需要"软"的概率输出(如伯努利概率、分类概率),此时需要 sigmoid 或 softmax 输出单元(6.2.2.2 和 6.2.2.3 节)。从概率建模角度看,线性输出 + 高斯条件分布假设是最简单的"神经概率模型"形式——神经网络只负责学习高斯均值函数 \(\mu(x) = W^\top h + b\),方差假设为已知(恒等矩阵);这一假设的局限性是只能建模对称单峰的条件分布,无法处理多峰、偏斜或异方差情形,这些情形需要更复杂的输出单元(6.2.2.4 节将讨论高斯混合等);线性单元作为高斯均值函数也常作为更大概率图模型的组件,详见第 16 章结构化概率模型和第 19 章近似推断等更一般的概率模型设置中使用;这些是线性输出单元概念的自然延伸,也使线性输出单元在现代深度学习概率模型(如变分自编码器、扩散模型)中仍是基础构件。

6.2.2.2 用于伯努利输出分布的 sigmoid 单元(Sigmoid Units for Bernoulli Output Distributions)

很多任务要求预测二元变量 \(y\) 值(两类分类问题可由此构造),最大似然方法是定义给定 \(x\) 条件下 \(y\) 上的伯努利分布。伯努利分布由单数定义,神经网络只需预测 \(P(y=1|x)\);此数须落在区间 \([0, 1]\) 内(合法概率约束)。满足该约束需要细致设计:若用线性单元再阈值截断得 \(P(y=1|x) = \max\{0, \min\{1, w^\top h + b\}\}\)(式 6.18),确实定义合法条件分布,但用梯度下降不能有效训练——一旦 \(w^\top h + b\) 跑出单位区间(即 \(w^\top h + b > 1\)\(< 0\)),模型输出对参数的梯度就是 0(因为 max/min 操作的饱和段梯度为 0);梯度为 0 通常是问题,因为学习算法失去如何改进相应参数的指引——算法无法判断"现在模型过于自信 +0.5"与"现在模型过于自信 +1000"之间的差别,因为两者损失/参数梯度都是 0。更好的方法是使用 sigmoid 输出单元配合最大似然,确保模型出错时总有强梯度。

sigmoid 输出单元定义为 \(\hat{y} = \sigma(w^\top h + b)\)(式 6.19),其中 \(\sigma\) 是 3.10 节描述的 logistic sigmoid 函数 \(\sigma(x) = 1/(1+e^{-x})\)。可把 sigmoid 输出单元视为两部分:先用线性层计算 \(z = w^\top h + b\),再用 sigmoid 激活函数把 \(z\) 转换为概率。为说明如何用 \(z\) 定义 \(y\) 的概率分布,先构造未归一化概率分布 \(\tilde{P}(y)\)(其和不为 1),再除以适当常数得合法概率分布。假设未归一化对数概率对 \(y\)\(z\) 线性(\(\log \tilde{P}(y) = yz\),式 6.20),指数化得未归一化概率 \(\tilde{P}(y) = \exp(yz)\)(式 6.21),归一化(除以两类的指数和)得 \(P(y) = \exp(yz)/\sum_{y'} \exp(y' z) = \sigma((2y-1)z)\)(式 6.22 和 6.23)——由 \(z\) 的 sigmoid 变换控制的伯努利分布。基于指数化和归一化的概率分布在统计建模文献中很常见,定义这种二元变量分布的 \(z\) 称为 logit。

在 log-空间预测概率与最大似然学习自然配合——最大似然所用代价 \(-\log P(y|x)\) 中的 \(\log\) 抵消 sigmoid 的 \(\exp\),否则 sigmoid 饱和会阻碍基于梯度学习取得进展。sigmoid 参数化伯努利的最大似然损失函数为 \(J(\theta) = -\log P(y|x) = -\log \sigma((2y-1)z) = \zeta((1-2y)z)\)(式 6.24-6.26),其中 \(\zeta\) 是 softplus 函数 \(\zeta(x) = \log(1+e^x)\)。该推导利用 3.10 节关于 sigmoid 与 softplus 关系的性质:\(\log \sigma(x) = -\zeta(-x)\)。把损失改写为 softplus 形式后可见:损失只在 \((1-2y)z\) 极负时饱和——即只在模型已经答对时(\(y=1\)\(z\) 极正,或 \(y=0\)\(z\) 极负)饱和,因为此时模型输出 \(\sigma((2y-1)z)\) 已接近 1,损失接近 0,梯度自然接近 0。当 \(z\) 取错符号时,softplus 自变量 \((1-2y)z\) 化简为 \(|z|\);当 \(|z|\) 变大而 \(z\) 符号错误时,softplus 渐近返回其自变量 \(|z|\),对 \(z\) 的导数渐近为 \(\text{sign}(z)\),因此 \(z\) 极度错误时 softplus 完全不缩小梯度——基于梯度的学习可借此迅速纠正错误 \(z\)

当用其他损失函数(如均方误差)时,损失可能在 \(\sigma(z)\) 饱和时饱和(sigmoid 在 \(z\) 极负时饱和到 0、\(z\) 极正时饱和到 1),无论模型答案对错,梯度都可能小到对学习无用——例如即使模型答错(\(y=1\)\(z\) 极负使 \(\sigma(z) \approx 0\)),损失是 \((\sigma(z) - y)^2 \approx 1\),但损失对 \(z\) 的梯度 \(\partial (\sigma(z)-y)^2 / \partial z = 2(\sigma(z)-y) \sigma'(z)\) 包含 \(\sigma'(z) \approx 0\) 因子,梯度消失。这是为什么训练 sigmoid 输出单元几乎总是首选最大似然。sigmoid 的对数在分析上总是定义且有限的,因为 sigmoid 返回开区间 \((0, 1)\) 的值而不用闭区间 \([0, 1]\)——后者在边界取 0 或 1 时对数为 \(-\infty\)。软件实现中为避免数值问题最好把负对数似然写为 \(z\) 的函数而非 \(\hat{y} = \sigma(z)\) 的函数——sigmoid 下溢到 0 时对 \(\hat{y}\) 取对数会得负无穷,但 \(\zeta((1-2y)z)\) 即使 \(z\) 极负也只是渐近为 0 而非负无穷。

当用其他损失函数(如均方误差)时,损失可能在 \(\sigma(z)\) 饱和时饱和(sigmoid 在 \(z\) 极负时饱和到 0、\(z\) 极正时饱和到 1),无论模型答案对错,梯度都可能小到对学习无用。这是为什么训练 sigmoid 输出单元几乎总是首选最大似然。sigmoid 的对数在分析上总是定义且有限的,因为 sigmoid 返回开区间 \((0, 1)\) 的值而不用闭区间 \([0, 1]\);软件实现中为避免数值问题最好把负对数似然写为 \(z\) 的函数而非 \(\hat{y} = \sigma(z)\) 的函数——sigmoid 下溢到 0 时对 \(\hat{y}\) 取对数会得负无穷。

6.2.2.3 用于多项输出分布的 softmax 单元(Softmax Units for Multinoulli Output Distributions)

任何时候希望表示具有 \(n\) 个可能值的离散变量上的概率分布,都可使用 softmax 函数,可视为 sigmoid(二元变量上概率分布表示)向多元情形的推广。softmax 最常用作分类器输出,表示 \(n\) 个不同类上的概率分布;偶尔也用于模型内部希望模型在内部变量上从 \(n\) 种选项中做选择。

对二元变量,希望产生单数 \(\hat{y} = P(y=1|x)\)。因为此数需落在 \([0, 1]\),且希望其对数在基于梯度的对数似然优化中表现良好,我们改为预测 \(z = \log \tilde{P}(y=1|x)\),指数化并归一化得由 sigmoid 函数控制的伯努利分布。推广到有 \(n\) 个值的离散变量,需产生向量 \(\hat{y}\) 其中 \(\hat{y}_i = P(y=i|x)\),要求每 \(\hat{y}_i\)\([0, 1]\) 内且整个向量和为 1(合法概率分布)。对伯努利有效的方法同样推广到多项分布:先由线性层预测未归一化对数概率 \(z = W^\top h + b\)\(z_i = \log \tilde{P}(y=i|x)\)),softmax 再指数化并归一化 \(z\) 得所需 \(\hat{y}\),形式上 \(\text{softmax}(z)_i = \frac{\exp(z_i)}{\sum_j \exp(z_j)}\)

与 logistic sigmoid 一样,用 \(\exp\) 函数在最大对数似然训练 softmax 输出目标 \(y\) 时效果很好,因为对数似然中的 \(\log\) 抵消 softmax 的 \(\exp\)\(\log \text{softmax}(z)_i = z_i - \log \sum_j \exp(z_j)\)。式 6.30 第一项表明输入 \(z_i\) 总是对代价函数有直接贡献;此项不饱和,所以即使 \(z_i\) 对第二项的贡献变得极小,学习仍能进行。最大化对数似然时第一项鼓励把 \(z_i\) 推高、第二项鼓励把所有 \(z\) 推低。直观上 \(\log \sum_j \exp(z_j)\) 可粗略近似为 \(\max_j z_j\)\(\exp(z_k)\)\(z_k\) 明显小于 \(\max_j z_j\) 时不显著),由此得直观:负对数似然代价函数总是强烈惩罚最活跃的错误预测。若正确答案已有最大输入到 softmax,则 \(-z_i\) 项与 \(\log \sum_j \exp(z_j) \approx \max_j z_j = z_i\) 项大致抵消,该样本对总训练代价贡献小;总代价由尚未正确分类的样本主导。

至此只讨论了单样本。整体上,未正则化的最大似然会驱动模型学习使 softmax 预测训练集中观察到的每种结果出现频次的参数:\(\text{softmax}(z^{(x; \theta)})_i \approx \frac{\sum_{j=1}^m \mathbb{1}_{y^{(j)}=i, x^{(j)}=x}}{\sum_{j=1}^m \mathbb{1}_{x^{(j)}=x}}\)。因最大似然是一致估计量,只要模型族能表示训练分布则这一点必然成立;实践中有限模型容量和不完美优化使模型只能近似这些频次。

很多非对数似然目标函数与 softmax 配合不好。具体而言,不用 \(\log\) 抵消 softmax 的 \(\exp\) 的目标函数会在 \(\exp\) 自变量极负时学不到东西导致梯度消失;平方误差对 softmax 单元是差的损失函数,在模型做出高置信度错误预测时也可能失败(Bridle, 1990)。要理解这些其他损失函数为何失败需检查 softmax 函数本身。softmax 激活与 sigmoid 一样会饱和:sigmoid 是单输出在自变量极负/极正时饱和,softmax 有多输出当输入值间差异极大时输出值会饱和;softmax 饱和时基于 softmax 的很多代价函数也会饱和,除非它们能反转该饱和激活函数。

softmax 函数有不平凡性质:softmax 输出对所有输入加同一标量不变,\(\text{softmax}(z) = \text{softmax}(z + c)\)。利用此性质可推导数值稳定变体:\(\text{softmax}(z) = \text{softmax}(z - \max_i z_i)\),即使 \(z\) 含极大或极负的数也能仅有小数值误差地计算 softmax。检查数值稳定变体可见 softmax 函数由其参数偏离 \(\max_i z_i\) 的程度驱动。\(\text{softmax}(z)_i\) 在对应输入最大(\(z_i = \max_i z_i\))且 \(z_i\) 远大于其他输入时饱和到 1;\(\text{softmax}(z)_i\)\(z_i\) 非最大且最大值远大于它时饱和到 0,是 sigmoid 饱和方式的推广,若损失函数未设计补偿则引起类似学习困难。

softmax 的输入 \(z\) 可由两种方式产生。最常见是让神经网络前一层输出 \(z\) 的每个元素(用线性层 \(z = W^\top h + b\));这种方法实际上对分布是过参数化——\(n\) 个输出和为 1 的约束意味着仅需 \(n-1\) 个参数,第 \(n\) 个值的概率可由前 \(n-1\) 个概率从 1 减去得到。我们可要求 \(z\) 的某一元素固定(例如 \(z_n=0\))——这正是 sigmoid 单元在做的:\(P(y=1|x) = \sigma(z)\) 等价于 \(P(y=1|x) = \text{softmax}(z)_1\),其中 \(z\) 是二维且 \(z_1=0\)\(n-1\) 参数和 \(n\) 参数两种 softmax 方法能描述同一组概率分布,但学习动态不同;实践中用哪种差别不大,实现过参数化版本更简单。

从神经科学视角看,把 softmax 视为参与单元间一种竞争是有趣的:softmax 输出和总为 1,一个单元值增加必然对应其他单元值减少,类似于皮层中邻近神经元被认为存在的侧向抑制。当差异极大时变成 winner-take-all 形式(一个输出接近 1、其余接近 0)。"softmax" 名字有时令人困惑——该函数与 arg max 比与 max 更接近;"soft" 来自 softmax 连续可微,而 arg max 函数(结果用 one-hot 向量表示)既不连续也不可微;softmax 是 arg max 的"软化"版本,max 函数的对应软版本是 \(\text{softmax}(z)^\top z\)。也许称 softmax 为 "softargmax" 更贴切,但当前名称已成既定约定。

6.2.2.4 其他输出类型(Other Output Types)

线性、sigmoid、softmax 输出单元是最常见的。神经网络可推广到几乎任何希望类型的输出层;最大似然原理为几乎任何输出层设计好的代价函数提供指南。一般地,若定义条件分布 \(p(y|x; \theta)\),最大似然建议用 \(-\log p(y|x; \theta)\) 作为代价函数。可把神经网络视为表示函数 \(f(x; \theta)\)\(f\) 的输出不是 \(y\) 的直接预测;相反 \(f(x; \theta) = \omega\) 提供 \(y\) 上分布的参数,损失函数可解释为 \(-\log p(y; \omega(x))\)

例如希望学习给定 \(x\)\(y\) 的条件高斯方差。简单情形下方差 \(\sigma^2\) 为常数,则有闭式表达(最大似然方差估计量即观测值与期望值之差的经验平方均值)。一种计算更昂贵但无需写特例代码的方法是把方差作为分布 \(p(y|x)\) 的属性之一由 \(\omega = f(x; \theta)\) 控制,负对数似然 \(-\log p(y; \omega(x))\) 将提供含适当项的代价函数使优化过程能逐步学习方差。简单情形下标准差不依赖输入时,可在网络中新增一个参数直接复制到 \(\omega\)——此新参数可以是 \(\sigma\) 本身,或代表 \(\sigma^2\) 的参数 \(v\),或代表 \(1/\sigma^2\) 的参数 \(\beta\),取决于如何参数化分布。我们可能希望模型对不同 \(x\) 预测不同的 \(y\) 方差,这称为异方差模型(heteroscedastic model);异方差情形下只需把方差作为 \(f(x; \theta)\) 输出值之一即可。典型做法是用精度(precision)而非方差参数化高斯分布(式 3.22),多元情形最常用对角精度矩阵 \(\text{diag}(\beta)\),这种参数化与梯度下降配合良好——对数似然公式只涉及乘以 \(\beta_i\) 和加 \(\log \beta_i\)(乘、加、对数的梯度表现良好)。相比之下用方差参数化输出会涉及除法(除法在零附近变任意陡,极大梯度通常导致不稳定);用标准差参数化则对数似然仍涉及除法且涉及平方(平方运算的梯度在零附近可能消失,使学习被平方的参数变难)。

无论用标准差、方差还是精度,都必须保证高斯协方差矩阵正定;精度矩阵的特征值是协方差矩阵特征值的倒数,等价于保证精度矩阵正定。若用对角矩阵或对角矩阵的标量倍,则模型输出只需满足正性。假设 \(a\) 是模型用于决定对角精度的原始激活,可用 softplus 函数获得正精度向量 \(\beta = \zeta(a)\);此策略对使用方差/标准差/对角矩阵的标量倍同样适用。很少学习比对角矩阵结构更丰富的协方差或精度矩阵。若协方差是满的且依赖输入,则必须选择能保证预测协方差矩阵正定性的参数化方法(可通过写 \(\Sigma(x) = B(x)B(x)^\top\)\(B\) 为无约束方阵)。一个实际问题是矩阵满秩时计算似然昂贵——\(d \times d\) 矩阵需 \(O(d^3)\) 计算量来求 \(\Sigma(x)\) 的行列式和逆(或等价地更常见地,对 \(B(x)\) 做特征分解)。

常常需要进行多峰回归(multimodal regression),即预测 \(y\) 的实值在给定 \(x\) 的条件分布 \(p(y|x)\)\(y\) 空间可有多个峰;高斯混合是输出的自然表示(Jacobs et al., 1991; Bishop, 1994);以高斯混合为输出的神经网络常称为混合密度网络(mixture density network)。具有 \(n\) 个分量的高斯混合输出由条件概率分布 \(p(y|x) = \sum_{i=1}^n p(c=i|x) \mathcal{N}(y; \mu^{(i)}(x), \Sigma^{(i)}(x))\) 定义。神经网络必须有三种输出:定义 \(p(c=i|x)\) 的向量、为所有 \(i\) 提供 \(\mu^{(i)}(x)\) 的矩阵、为所有 \(i\) 提供 \(\Sigma^{(i)}(x)\) 的张量。这些输出须满足不同约束:

  1. 混合分量 \(p(c=i|x)\):在隐变量 \(c\)\(c\) 在数据中不观测到:给定 \(x\) 和目标 \(y\) 不可能确切知道哪个高斯分量生成了 \(y\),但可想象 \(y\) 是从中挑一个生成,把那次未观测选择作为随机变量)所关联的 \(n\) 个不同分量上形成多项分布,通常通过对 \(n\) 维向量做 softmax 得到,保证输出为正且和为 1。
  2. 均值 \(\mu^{(i)}(x)\):表示第 \(i\) 个高斯分量关联的中心或均值,无约束(这些输出单元通常完全无非线性);若 \(y\)\(d\) 维向量,则网络须输出 \(n \times d\) 矩阵含所有 \(n\)\(d\) 维向量。用最大似然学习这些均值比学习单输出模式分布的均值稍复杂——只想更新实际产生观测的那个分量的均值;实践中并不知道每个观测由哪个分量产生。负对数似然表达式自然地用每个分量产生该样本的概率为该样本对各分量损失的贡献加权。
  3. 协方差 \(\Sigma^{(i)}(x)\):指定每个分量 \(i\) 的协方差矩阵。与学习单高斯分量时一样,通常用对角矩阵避免计算行列式。与学习混合的均值一样,最大似然因需对每个样本对每个混合分量分配部分责任而复杂;只要给定混合模型下负对数似然规范正确,梯度下降会自动遵循正确流程。

有报告指出条件高斯混合(神经网络输出)的基于梯度的优化可能不可靠,部分因为有除以方差的操作(数值上不稳定——当某样本方差变小时会产生极大梯度)。一种解决方案是梯度截断(10.11.1 节),另一种是启发式缩放梯度(Murray and Larochelle, 2014)。高斯混合输出在语音(Schuster, 1999)或物理对象运动(Graves, 2013)的生成模型中特别有效;混合密度策略为网络提供表示多个输出模式并控制输出方差的方式,这对在这些实值域获得高质量至关重要。图 6.4 展示了混合密度网络的例子。

一般地,我们可能希望对包含更多变量的更大向量 \(y\) 建模,并对这些输出变量施加越来越丰富的结构。例如希望神经网络输出构成句子的字符序列。这些情形下仍可对模型 \(p(y; \omega(x))\) 应用最大似然原理,但用于描述 \(y\) 的模型会变得复杂得超出本章范围。第 10 章介绍如何用循环神经网络定义这种序列上的模型,第 III 部分描述对任意概率分布建模的高级技术。

6.3 隐藏单元(Hidden Units)

至此我们聚焦神经网络设计选择中与大多数用基于梯度优化训练的参数模型共有的部分(代价函数、输出单元)。现在转向前馈神经网络独有的问题:如何选择隐藏层中使用的隐藏单元类型。隐藏单元设计是极其活跃的研究领域,尚无定型的指导性理论原则——不像代价函数选择有最大似然/交叉熵这种"通用且有理论支撑"的默认,也不像输出单元选择有 sigmoid/softmax 这种"由概率建模决定"的清晰规则,隐藏单元设计更多是经验性的。修正线性单元(ReLU)是隐藏单元的绝佳默认选择,还有很多其他可用类型(如 sigmoid、tanh、maxout、各种 RBF 变体等);虽然 ReLU 通常是可接受的选项,但很难确定何时用哪种——例如循环网络中常用 tanh 而非 ReLU(因为 ReLU 的零梯度可能导致时间步间的信号丢失),自编码器中常用 sigmoid 或 tanh(因为需要平滑的有界激活)。这里描述每种隐藏单元类型背后的基本直觉,可帮助决定何时尝试哪种;通常不可能提前预测哪种最好——设计过程是试错、直觉某种隐藏单元可能表现良好、然后用该隐藏单元训练网络并在验证集评估性能。

有些隐藏单元在所有输入点并不都可微——例如修正线性函数 \(g(z) = \max\{0, z\}\)\(z = 0\) 处不可微。这看似使其无法用于基于梯度学习算法,但实践中梯度下降对这些模型仍表现得足够好。一定程度上是因为神经网络训练算法通常不会真正到达代价函数的局部极小点,而只是大幅降低其值(如图 4.3 所示),这些思想将在第 8 章进一步描述——具体地说,深度学习中的"收敛"通常指损失下降到某阈值附近,而非真正的最优点。因为我们不期望训练实际到达梯度为 0 的点,最小点对应梯度未定义点也是可接受的。不可微的隐藏单元通常只在少数点不可微。一般 \(g(z)\)\(z\) 处有左导数(\(z\) 左侧的斜率)和右导数(\(z\) 右侧的斜率),仅当两者都定义且相等时函数才在 \(z\) 处可微。神经网络上下文所用函数通常都有定义的左导数和右导数。\(g(z) = \max\{0, z\}\)\(z = 0\) 处的左导数为 0、右导数为 1。神经网络训练的软件实现通常返回某一侧的单侧导数而非报告导数未定义或抛错;这可由以下观察启发性地正当化——在数字计算机上基于梯度的优化本身就受数值误差影响,当函数被要求计算 \(g(0)\) 时底层真值极不可能真的是 0,它很可能是某个被舍入到 0 的小值 \(\epsilon\)。某些上下文有理论上更优雅的正当化,但这些通常不适用于神经网络训练。要点是实践中可安全忽略下面所描述隐藏单元激活函数的不可微性。

除非另作说明,大多数隐藏单元可描述为:接受输入向量 \(x\),计算仿射变换 \(z = W^\top x + b\),然后施加逐元素的非线性函数 \(g(z)\)\(h_i = g(z_i)\) 对每个 \(i\) 独立计算)。大多数隐藏单元彼此的区别仅在于激活函数 \(g(z)\) 形式的选择——这种"统一结构 + 多种激活"的视角反映了神经网络模块化设计哲学:仿射变换处理线性部分,激活函数引入非线性并决定单元的"行为模式"。理解隐藏单元设计的核心是理解不同激活函数 \(g\) 对梯度流通、信息保留、稀疏性、计算成本等的影响——这也是下面 6.3.1-6.3.3 节的组织逻辑。

6.3.1 修正线性单元及其推广(Rectified Linear Units and Their Generalizations)

修正线性单元使用激活函数 \(g(z) = \max\{0, z\}\)。修正线性单元易于优化,因为与线性单元非常相似:唯一区别是修正线性单元在其定义域的半边输出 0。这使得只要单元激活时通过修正线性单元的导数就保持很大;梯度不仅大而且一致。修正运算的二阶导数几乎处处为 0,修正运算的导数在单元激活处都为 1。这意味着梯度方向对学习比那些引入二阶效应的激活函数有用得多。修正线性单元通常用在仿射变换之上:\(h = g(W^\top x + b)\)。初始化仿射变换参数时一个好的实践是把 \(b\) 的所有元素设为小的正值(如 0.1),这使修正线性单元很可能对训练集中大多数输入在初始化时即为激活状态,从而允许导数通过。

存在几种修正线性单元的推广;这些推广大多与修正线性单元表现相当,偶尔更好。修正线性单元的一个缺点是对激活为 0 的样本无法通过基于梯度的方法学习,多种推广保证它们处处都收到梯度。三种基于在 \(z_i < 0\) 时使用非零斜率 \(\alpha_i\) 的推广:\(h_i = g(z, \alpha)_i = \max(0, z_i) + \alpha_i \min(0, z_i)\)。绝对值修正固定 \(\alpha_i = -1\)\(g(z) = |z|\),用于图像物体识别(Jarrett et al., 2009),在那种场合寻求对输入光照极性反转不变的特性是有意义的;其他推广更广泛适用。leaky ReLU(Maas et al., 2013)把 \(\alpha_i\) 固定为 0.01 这样的小值;参数化 ReLU 或 PReLU(He et al., 2015)把 \(\alpha_i\) 视为可学习参数。

Maxout 单元(Goodfellow et al., 2013a)进一步推广修正线性单元。maxout 单元不施加逐元素函数 \(g(z)\),而是把 \(z\) 分成大小为 \(k\) 的组,每组输出该组中的最大元素:\(g(z)_i = \max_{j \in G(i)} z_j\),其中 \(G(i)\) 是第 \(i\) 组的输入下标集 \(\{(i-1)k+1, \ldots, ik\}\)。这提供学习对输入 \(x\) 空间中多方向响应的分段线性函数的方法。一个 maxout 单元可学习具有多达 \(k\) 段的凸分段线性函数,因此可被视为在学激活函数本身而不只是单元间关系。\(k\) 足够大时,maxout 单元能以任意精度近似任何凸函数;具体地,具有两段的 maxout 层可学实现与传统修正线性激活函数、绝对值修正函数、leaky 或参数化 ReLU 同样关于 \(x\) 的函数,或学实现完全不同的函数。maxout 层当然与其他层类型参数化方式不同,所以即使在 maxout 学实现与其他层类型相同 \(x\) 函数的情形学习动态也会不同。

每个 maxout 单元现由 \(k\) 个权重向量而非仅一个参数化,所以 maxout 单元通常比修正线性单元需要更多正则化;在训练集大、每单元段数低时不需正则化也能工作良好(Cai et al., 2013)。Maxout 单元还有一些其他优点——某些情形下通过要求更少参数可获得统计和计算上的优势:具体地,若 \(n\) 个不同线性滤波器捕捉的特征可通过在每组 \(k\) 个特征上取 max 而无信息损失地汇总,则下一层可仅用 \(k\) 倍少的权重。因每个单元由多个滤波器驱动,maxout 单元具有冗余性,有助于抵抗一种称为灾难性遗忘(catastrophic forgetting)的现象——在该现象中神经网络会忘记过去训练的任务(Goodfellow et al., 2014a)。

修正线性单元及所有这些推广都基于"模型行为越接近线性越容易优化"的原则。这一"用线性行为获得更易优化"的通用原则也适用于深度线性网络以外的其他上下文。循环网络可从序列学习并产生状态和输出序列;训练它们时需要通过几个时间步传播信息,当涉及一些线性计算(其方向导数幅度接近 1)时这容易得多。表现最好的循环网络架构之一 LSTM 通过求和——一种特别直接的此类线性激活——在时间上传播信息。这在 10.10 节进一步讨论。

6.3.2 逻辑 sigmoid 和双曲正切(Logistic Sigmoid and Hyperbolic Tangent)

在修正线性单元引入之前,大多数神经网络使用逻辑 sigmoid 激活函数 \(g(z) = \sigma(z)\)(式 6.38)或双曲正切激活函数 \(g(z) = \tanh(z)\)(式 6.39)。这两个激活函数密切相关,因为 \(\tanh(z) = 2\sigma(2z) - 1\)——这意味着 \(\tanh\) 实际上是 \(\sigma\) 的简单仿射变换,反之亦然;从函数族角度它们表达能力相同,但训练动态略有差别。

我们已经把 sigmoid 单元作为输出单元见过,用于预测二元变量为 1 的概率(6.2.2.2 节)。与分段线性单元不同,sigmoid 单元在其定义域的大部分都会饱和——\(z\) 极正时饱和到高值、\(z\) 极负时饱和到低值、仅在 \(z\) 接近 0 时对输入强敏感(具体地说,sigmoid 的导数 \(\sigma'(z) = \sigma(z)(1-\sigma(z))\)\(|z| > 4\) 时已小于 0.01,梯度近乎消失)。sigmoid 单元的广泛饱和可能使基于梯度的学习非常困难——深度网络中 sigmoid 隐藏单元的饱和是导致"梯度消失"问题的核心原因之一(详见第 8 章优化讨论)。因此它们在前馈网络中作为隐藏单元的用途现在不被鼓励;它们作为输出单元与基于梯度的学习兼容——只要有合适的代价函数(负对数似然)能抵消输出层 sigmoid 的饱和(6.2.2.2 节)。

当必须用 sigmoid 激活函数时,双曲正切激活函数通常比逻辑 sigmoid 表现更好。它更接近恒等函数,因为 \(\tanh(0) = 0\)\(\sigma(0) = 1/2\)——这一中心化性质使训练过程中单元输出更可能是零中心的(zero-centered),这对梯度计算有利:若单元输出总是正(如 \(\sigma\) 总是 \(\in (0, 1)\)),则反向传播中权重梯度 \(g h^{(k-1)\top}\) 的符号完全由 \(g\) 决定,可能出现 zig-zag 现象。因 \(\tanh\) 在 0 附近类似恒等函数,训练深度神经网络 \(\hat{y} = w^\top \tanh(U^\top \tanh(V^\top x))\) 在网络激活可保持小时类似训练线性模型 \(\hat{y} = |w^\top U^\top V^\top x|\),使 \(\tanh\) 网络的训练更容易——这意味着对于相对浅的网络,\(\tanh\) 隐藏单元的优化行为接近线性,更易用梯度方法。sigmoid 激活函数在前馈网络之外的其他设定中更常见:循环网络、很多概率模型、一些自编码器有排除使用分段线性激活函数并使 sigmoid 单元尽管有饱和缺点但更有吸引力的额外要求——例如循环网络中的门控机制(将在第 10 章详细讨论的 LSTM/GRU 等)需要 sigmoid 来产生 0-1 之间的"门"值;自编码器的瓶颈层需要 sigmoid 来约束编码到 \([0, 1]^k\) 区间。这种"循环网络/概率模型需要 sigmoid"的实践原因常是"sigmoid 的 0-1 输出对概率语义自然对应",而非训练便利性——sigmoid 在循环网络时间步间仍可能饱和,但门控机制(如 LSTM 中的输入门/遗忘门)的特定设计使其在训练中保持适度激活。从历史视角看,sigmoid 隐藏单元在 1980-90 年代是前馈网络的事实标准(彼时"小网络"假设成立,sigmoid 表现优于 ReLU),但 2010 年后随着网络规模增大和深度增加,sigmoid 隐藏单元的梯度消失问题变得突出,逐渐被 ReLU 及其推广取代——这一历史转变在 6.6 节历史笔记中会进一步讨论。从纯经验角度,sigmoid 隐藏单元的另一个常被忽略的缺点是其输出非零中心——这导致反向传播中权重更新有系统性的"同向漂移"倾向,使训练较慢;ReLU 单元对正值输入梯度为 1,因此无此问题。但 0-1 之外的负值仍可能饱和(sigmoid 关于 0 中心则 t 端均饱和),这是 sigmoid 隐藏单元的固有问题。实践中,若坚持要用 sigmoid 类激活函数,可考虑参数化初始化(让大部分单元在初始时处于 0 附近以避免立即进入饱和区)和批归一化(详见第 8 章)等技术辅助训练。

6.3.3 其他隐藏单元(Other Hidden Units)

还有许多其他可能的隐藏单元类型,但使用频率较低——上文讨论的 ReLU 及其推广(6.3.1)已覆盖大多数实际应用,一般无需考虑这些其他类型。一般地,很多不同的可微函数表现完全良好;很多未发表的激活函数与流行的表现一样好。作为具体例子,作者在 MNIST 数据集上用 \(h = \cos(Wx + b)\) 测试前馈网络,得到低于 1% 的错误率,与用更传统激活函数获得的结果具有竞争力——这说明在 MNIST 这种相对简单任务上"激活函数选择"可能不是决定性因素;但在更复杂任务(如 ImageNet 大规模图像分类)上不同激活函数的差异会显著得多。研究和开发新技术时通常测试许多不同激活函数并发现标准实践的几种变体表现相当。这意味着新隐藏单元类型通常只在被清晰证明能提供显著改进时才会发表;与已知类型大致相当的新隐藏单元类型则常见得无趣。列出文献中出现的所有隐藏单元类型是不现实的,下面突出几个特别有用且与众不同的。

一种可能是不用任何激活 \(g(z)\),也可视为用恒等函数 \(g(z) = z\) 作为激活函数。我们已经见过线性单元作为神经网络输出有用;它也可用作隐藏单元。若神经网络每层都仅由线性变换组成,则整个网络将是线性的——这意味着无论多深都等价于一个线性变换。然而对部分神经网络层为纯线性也是可接受的。考虑 \(n\) 输入 \(p\) 输出的神经网络层 \(h = g(W^\top x + b)\),可用两层替换,一层用权重矩阵 \(U\)、另一层用权重矩阵 \(V\)。若第一层无激活函数,则实际是基于 \(W\) 对原层权重矩阵做低秩分解。分解方法计算 \(h = g(V^\top U^\top x + b)\);若 \(U\) 产生 \(q\) 输出,则 \(U\)\(V\) 一起仅含 \((n + p)q\) 参数,而 \(W\)\(np\) 参数——\(q\) 小时这可显著节省参数(例如 \(n = p = 100, q = 10\)\((n+p)q = 2000\) vs \(np = 10000\),节省 5 倍)。代价是约束线性变换为低秩(秩至多 \(q\)),但这些低秩关系通常足够——实践中很多有用的"层"实际近似低秩。线性隐藏单元因此为减少网络参数数提供了一种有效方式,也常用作自动编码器中的瓶颈层(详见第 14 章)。

Softmax 单元是另一种通常作为输出使用的单元(6.2.2.3 节)但有时也用作隐藏单元。softmax 单元自然地表示具有 \(k\) 个可能值的离散变量上的概率分布,所以可作为一种"软开关"使用——在某一状态下趋近 one-hot 向量(一个接近 1、其余接近 0),等价于一个离散选择。这类隐藏单元通常只在更高级的显式学习操作记忆的架构中使用,10.12 节描述——例如神经图灵机中的"读取权重"由 softmax 单元产生,决定从记忆的哪些位置读取;类似地注意力机制(将在第 12 章详细讨论)中的注意力权重也常用 softmax 产生,决定从输入序列的哪些位置获取信息。

其他几种合理的常见隐藏单元类型包括:

  • 径向基函数(RBF)单元:\(h_i = \exp\left(-\frac{1}{\sigma_i^2}\|W_{:,i} - x\|^2\right)\)。该函数在 \(x\) 接近模板 \(W_{:,i}\) 时更活跃;因对大多数 \(x\) 饱和到 0,可能难以优化。
  • Softplus:\(g(a) = \zeta(a) = \log(1 + e^a)\),是修正函数的平滑版本,由 Dugas et al.(2001)提出用于函数逼近、由 Nair and Hinton(2010)用于无向概率模型的条件分布。Glorot et al.(2011a)比较了 softplus 和修正函数,发现后者结果更好。softplus 的使用总体上不被鼓励——softplus 表明隐藏单元类型的性能可能非常反直觉:人们可能预期它由于处处可微或饱和更不完全而优于修正函数,但经验上并非如此。
  • Hard tanh:形状类似 \(\tanh\) 和修正函数但与后者不同,hard tanh 有界 \(g(a) = \max(-1, \min(1, a))\),由 Collobert(2004)提出。

隐藏单元设计仍是活跃研究领域,许多有用的隐藏单元类型仍有待发现。

6.4 架构设计(Architecture Design)

神经网络设计的另一个关键考量是确定架构。"架构"指网络的整体结构:应有多少单元、这些单元应如何相互连接、这些连接应形成什么拓扑结构。大多数神经网络被组织为称为"层"的单元组,每层是一组同时操作的单元(同一层内单元通常接收相同输入并相互独立计算激活);大多数神经网络架构把这些层安排为链式结构,每一层是前一层的函数,整个网络因此可表示为输入到输出的复合函数 \(f(x) = f^{(L)}(\ldots f^{(2)}(f^{(1)}(x))\ldots)\),这种链式结构是实践中最常见也最易优化的网络拓扑,其分析简洁性(每层可视为对前一层表示的进一步精化)使其成为理论分析(如深度学习理论的"信息瓶颈"框架、"优化景观"分析)和工程实现(按层序组织代码、自然支持残差连接等变体)的共同基础。在该结构中第一层为 \(h^{(1)} = g^{(1)}(W^{(1)\top} x + b^{(1)})\),第二层为 \(h^{(2)} = g^{(2)}(W^{(2)\top} h^{(1)} + b^{(2)})\),以此类推——式 6.40 和 6.41 给出前两层的形式,其中 \(W^{(i)}\) 是第 \(i\) 层的权重矩阵、\(b^{(i)}\) 是偏置向量、\(g^{(i)}\) 是激活函数。

在这些链式架构中,主要的架构考量是选择网络深度(层数 \(L\))和每层宽度(每层单元数)。这两个超参数的选择对模型性能有重大影响,但目前缺乏闭式最优解——实践中需通过超参数搜索(grid search、random search 或贝叶斯优化)结合交叉验证或验证集错误指导来确定。值得注意的是,深度学习中的"超参数"概念比传统机器学习更加重要——网络架构本身(深度、宽度、每层类型)就是超参数的一部分,其选择空间比 SVM 的 C 和 gamma 大得多;这使得深度学习模型调参成为独立的研究/工程子领域;本书第 11 章"实践方法论"专门讨论超参数选择,包括学习率、批量大小、优化器选择等训练超参数和深度、宽度等架构超参数,这些选择对最终模型性能有巨大影响,且选择之间常存在复杂的相互依赖——例如学习率过大可能要求更小的批量大小,深度增加可能要求更小的学习率以避免梯度爆炸;批量大小增加可能要求更大的学习率以保持更新幅度。架构选择本身也存在权衡——更深的网络通常带来更好的泛化(6.4.1 节"深度优势"),但训练难度增加;更宽的网络单层表示能力更强,但参数总量随宽度平方增长(对全连接层)容易过拟合。稍后会看到,即使一个隐藏层的网络也足以拟合训练集(通用逼近定理,6.4.1 节);更深的网络通常能使用少得多的每层单元和少得多的参数并常泛化到测试集,但也常更难优化(更复杂的损失景观、梯度消失/爆炸等)。任务理想网络架构必须通过受验证集错误指导的实验找到——目前没有闭式规则能从任务描述直接得出最佳架构,但经验上对许多任务有一些合理默认起点(如图像任务用 VGG/ResNet 类深度卷积网络,序列任务用 RNN/Transformer 类架构等),这些默认起点已通过多年实践被验证为有效的起点。

6.4.1 通用逼近性质与深度(Universal Approximation Properties and Depth)

线性模型(通过矩阵乘法把特征映射到输出)按定义只能表示线性函数。它的优点是易于训练,因为应用于线性模型时很多损失函数产生凸优化问题。不幸的是我们常希望学非线性函数。乍看可能认为学非线性函数需要为我们想学的非线性种类设计专门模型族。幸运的是带隐藏层的前馈网络提供了通用逼近框架。具体地,通用逼近定理(Hornik et al., 1989; Cybenko, 1989)陈述:带线性输出层和至少一个使用任何"压缩"激活函数(如逻辑 sigmoid 激活函数)的隐藏层的前馈网络,只要给定足够多隐藏单元,就能以任何大于零的期望误差逼近从一有限维空间到另一有限维空间的任何 Borel 可测函数。前馈网络的导数也能以任意精度逼近函数的导数(Hornik et al., 1990)。Borel 可测性概念超出本书范围;对我们来说,\(\mathbb{R}^n\) 上任何闭有界子集上的连续函数都是 Borel 可测的,因此可被神经网络逼近。神经网络也可逼近从任何有限维离散空间到另一空间的任何函数。原始定理首次以在极负和极正自变量都饱和的激活函数单元形式陈述,通用逼近定理也已为更广泛的激活函数类证明,包括现在常用的修正线性单元(Leshno et al., 1993)。

通用逼近定理意味着无论我们想学什么函数,都知道大 MLP 能表示该函数。但我们不能保证训练算法能学该函数——即使 MLP 能表示该函数,学习仍可能因两种不同原因失败。首先,训练用的优化算法可能找不到对应于所希望函数的参数值。其次,训练算法可能因过拟合而选错函数。回想 5.2.1 节:"没有免费午餐"定理表明没有普遍优越的机器学习算法。前馈网络为表示函数提供了通用系统——给定一函数就存在逼近它的一前馈网络,但不存在检查具体样本训练集并选一能泛化到训练集外点的函数的通用程序。

通用逼近定理说存在足够大的网络能达到我们希望的任何精度,但定理没说这个网络会有多大。Barron(1993)给出一些逼近宽类函数所需单层网络大小的界。不幸的是最坏情况下可能需要指数数量的隐藏单元(可能每隐藏单元对应一个需区分的输入配置)。这在二元情形最容易看出:向量 \(v \in \{0, 1\}^n\) 上的可能二元函数数为 \(2^{2^n}\),选其中一函数需 \(2^n\) 比特,原则上需要 \(O(2^n)\) 个自由度。

总结:单隐藏层前馈网络足以表示任何函数,但该层可能大到不切实际且可能学不会、泛化不好。很多情形下用更深模型能减少表示所希望函数所需单元数并能减少泛化误差量。存在这样的函数族:它们能被深度大于某值 \(d\) 的架构高效逼近,但若深度限制为 \(\leq d\) 则需大得多的模型;很多情形下浅模型所需隐藏单元数随 \(n\) 指数增长。这些结果最初针对不近似连续可微神经网络的模型(用于机器学习)证明,但此后已推广到这些模型:最初结果针对逻辑门电路(Håstad, 1986);后续工作推广到非负权重的线性阈值单元(Håstad and Goldmann, 1991; Hajnal et al., 1993),再推广到具有连续值激活的网络(Maass, 1992; Maass et al., 1994)。许多现代神经网络使用修正线性单元。Leshno et al.(1993)证明带包括修正线性单元在内宽类非多项式激活函数的浅网络有通用逼近性质,但这些结果不解决深度或效率问题——它们只说明足够宽的修正线性网络能表示任何函数。Montufar et al.(2014)表明深度修正网络可表示的函数可能需浅(单隐藏层)网络的指数数量隐藏单元;更精确地说,他们证明分段线性网络(可由修正非线性或 maxout 单元得到)能表示的区域数随网络深度指数增长。图 6.5 直观展示带绝对值修正的网络如何围绕某隐藏单元的输入相对于该隐藏单元计算的函数创建镜像:每个隐藏单元指定在输入空间中哪里"折叠"以创建镜像响应(绝对值非线性两侧);通过复合这些折叠操作,我们得到指数大的分段线性区域数,可捕获所有种类的规律(例如重复)模式。

更精确地说,Montufar et al.(2014)主要定理陈述:由 \(d\) 输入、深度 \(l\)、每层 \(n\) 单元的深度修正网络切出的线性区域数为 \(O\left(\binom{n}{d}^{d(l-1)/d}\right)\),即随深度 \(l\) 指数增长。对每单元 \(k\) 滤波器的 maxout 网络,区域数为 \(O\left(k^{(l-1)+d}\right)\)。当然,没有保证我们在机器学习(特别是 AI)应用中想学的函数种类共享这样的性质。

我们也可能出于统计原因选深度模型。任何时候选具体机器学习算法时,都隐式地声明了关于"算法应学何种函数"的一组先验信念。选深度模型编码了一种很一般的信念:我们想学的函数应涉及多个更简单函数的复合。从表示学习视角可解释为:学习问题由发现一组可变化底层因子组成,这些因子本身又可由更简单的可变化底层因子描述。或者,可把使用深度架构解释为表达一种信念:想学的函数是由多步组成的计算机程序,每步使用前一步的输出;这些中间输出不一定是变化因子,但可类比于网络用于组织其内部处理的计数器或指针。经验上对各种任务更大的深度似乎确实带来更好的泛化(Bengio et al., 2007; Erhan et al., 2009; Bengio, 2009; Mesnil et al., 2011; Ciresan et al., 2012; Krizhevsky et al., 2012; Sermanet et al., 2013; Farabet et al., 2013; Couprie et al., 2013; Kahou et al., 2013; Goodfellow et al., 2014d; Szegedy et al., 2014a)。图 6.6 和图 6.7 给出其中一些经验结果的例子。这表明使用深度架构确实表达了关于模型学习函数空间的有用先验。

6.4.2 其他架构考量(Other Architectural Considerations)

至此我们把神经网络描述为简单的层链,主要考量是网络深度和每层宽度。实践中神经网络表现出大得多的多样性。很多神经网络架构是为具体任务开发的;用于计算机视觉的专门架构——卷积网络——在第 9 章描述,它们利用图像的平移不变性和局部性,使用局部连接和权重共享,把 \(n \times p\) 的全连接参数 \(W\) 替换为少量卷积核。前馈网络也可推广为用于序列处理的循环神经网络,在第 10 章描述,它们有自己的架构考量(时间步间共享权重、隐藏状态作为记忆等)。一般地,层不必以链式连接,即使这是最常见的做法——很多架构构建一条主链但加入额外架构特征到其中,例如从层 \(i\) 到层 \(i+2\) 或更高的跳跃连接(skip connection)。这些跳跃连接使梯度更容易从输出层流向更接近输入的层——这一观察是 ResNet(He et al., 2015)成功的关键:通过在残差块中引入从输入到输出的恒等跳跃,深度可达数百甚至上千层而不出现严重梯度消失。

架构设计的另一个关键考量是如何准确地把一对层彼此相连。在默认的神经网络层(由矩阵 \(W\) 的线性变换描述)中,每个输入单元连接到每个输出单元——这称为"全连接",对应 \(W\) 为稠密矩阵。前方章节中的很多专门网络有更少的连接,所以输入层每个单元仅连接到输出层一个小子集——这称为"稀疏连接"。这些减少连接数的策略减少了参数数量和评估网络所需计算量(稠密连接的参数和计算量是 \(O(\text{input} \times \text{output})\),稀疏连接可降至 \(O(k \cdot \text{input})\) 其中 \(k\) 是每个输入单元的连接数),但常高度依赖问题——例如第 9 章描述的卷积网络使用对计算机视觉问题非常有效的专门稀疏连接模式(局部感受野 + 权重共享),第 10 章描述的循环网络在时间步间使用权重共享,本章中很难给出关于通用神经网络架构的更多具体建议,后续章节开发对不同应用领域已发现效果良好的具体架构策略。

图 6.6 显示在 Goodfellow et al.(2014d)转录地址照片多位数实验中,测试精度随深度(3-11 层)增加而单调提升;图 6.7 是控制实验,证明这种提升不是因为参数总量增加——把网络做得更宽并不带来同样提升,浅模型在约 2000 万参数时过拟合,深模型能受益于 6000 万以上参数。这说明深度模型表达关于函数空间的有用偏好:函数应由多个简单函数复合而成,要么学一个由更简单表示组成的表示(如由边定义的角),要么学一个含顺序依赖步骤的程序(先定位一组对象,再从彼此分割,再识别它们)。

架构设计的另一个关键考量是如何准确地把一对层彼此相连。在默认的神经网络层(由矩阵 \(W\) 的线性变换描述)中,每个输入单元连接到每个输出单元。前方章节中的很多专门网络有更少的连接,所以输入层每个单元仅连接到输出层一个小子集。这些减少连接数的策略减少了参数数量和评估网络所需计算量,但常高度依赖问题。例如第 9 章描述的卷积网络使用对计算机视觉问题非常有效的专门稀疏连接模式。本章中很难给出关于通用神经网络架构的更多具体建议,后续章节开发对不同应用领域已发现效果良好的具体架构策略。图 6.6 显示在 Goodfellow et al.(2014d)转录地址照片多位数实验中,测试精度随深度(3-11 层)增加而单调提升;图 6.7 是控制实验,证明这种提升不是因为参数总量增加——把网络做得更宽并不带来同样提升,浅模型在约 2000 万参数时过拟合,深模型能受益于 6000 万以上参数。这说明深度模型表达关于函数空间的有用偏好:函数应由多个简单函数复合而成,要么学一个由更简单表示组成的表示(如由边定义的角),要么学一个含顺序依赖步骤的程序(先定位一组对象,再从彼此分割,再识别它们)。

6.5 反向传播和其他微分算法(Back-Propagation and Other Differentiation Algorithms)

当我们用前馈神经网络接受输入 \(x\) 并产生输出 \(\hat{y}\) 时,信息通过网络向前流动。输入 \(x\) 提供初始信息,然后向上传播到每层隐藏单元,最后产生 \(\hat{y}\),这称为前向传播(forward propagation)。训练时,前向传播可持续进行直到产生标量代价 \(J(\theta)\)——代价 \(J(\theta)\) 度量模型预测 \(\hat{y}\) 与真实标签 \(y\) 之间的差距(加上可能的正则化项,例如 5.2.2 节的权重衰减);训练的目标是找使 \(J(\theta)\) 最小化的参数 \(\theta\)。这意味着我们需要 \(\nabla_\theta J(\theta)\)——代价关于参数的梯度;该梯度的计算正是反向传播的核心任务——没有高效的梯度计算,基于梯度的优化就无法在大型神经网络(数百万至数十亿参数)上实际运行——这正是 6.5 节展开反向传播及其现代推广的核心动机。6.5 节的剩余部分(6.5.1-6.5.10)将系统介绍反向传播的图语言、链式法则、算法骨架、广义形式、MLP 训练实例、工程复杂性、与自动微分理论的关系、高阶导数等。本节先厘清反向传播的"是什么"(梯度计算)和"不是什么"(不是整个学习算法),再介绍具体的图语言、链式法则、算法骨架、广义形式——这一路线图也是 6.5 节各小节的组织逻辑,可与第 5 章的算法三要素框架对照理解——6.5 节展开的是优化要素中"如何高效计算梯度"这一子问题。反向传播算法(back-propagation algorithm,Rumelhart et al., 1986a),常简称为 backprop,允许代价信息然后通过网络向后流动以计算梯度——即把"代价关于每层输出的导数"逐层从输出向输入方向回传。计算梯度的解析表达是直接的(只需应用链式法则),但数值评估这种解析表达可能计算上昂贵;反向传播算法用一种简单而廉价的过程完成这一工作,其关键思想是避免对同一子表达式做重复计算——后向求值顺序与每条边的局部导数结合即可获得完整梯度。

"反向传播"一词常被误解为指多层神经网络的整个学习算法——实际上反向传播仅指计算梯度的方法;另一种算法(如随机梯度下降)用此梯度执行学习。此外反向传播常被误解为专属于多层神经网络,但原则上它能计算任何函数的导数(对某些函数正确反应是报告函数导数未定义)。具体地,我们将描述如何对任意函数 \(f\) 计算梯度 \(\nabla_x f(x, y)\),其中 \(x\) 是希望求导的变量集,\(y\) 是函数输入但不需要其导数的另一组变量。学习算法中最常需要的梯度是代价函数关于参数的梯度 \(\nabla_\theta J(\theta)\)。很多机器学习任务涉及计算其他导数,作为学习过程的一部分或分析学到的模型;反向传播算法也可应用于这些任务,不限于计算代价函数关于参数的梯度——这一普适性是反向传播作为自动微分基础算法的关键。

6.5.1 计算图(Computational Graphs)

至此我们用相对非形式化的图语言讨论神经网络。为更精确描述反向传播算法,更精确的计算图语言是有帮助的。可有很多方式把计算形式化为图,每种方式各有优劣——例如有的图语言用节点表示"操作"而非"变量",有的允许多输出操作,有的把图限制为树(有唯一父节点);本书选择最简单和最通用的形式。我们用图中每个节点表示一个变量;该变量可以是标量、向量、矩阵、张量甚至其他类型的变量(字符串、稀疏矩阵、Python 对象等)——这种灵活性让同一图语言既能表示简单数学表达式也能表示复杂机器学习管道。要形式化图,还需引入"操作"(operation)的概念——操作是一个或多个变量的简单函数,对应神经网络中的加法、乘法、sigmoid、矩阵乘法、卷积等基本计算单元。我们的图语言伴有一组允许操作;比这组操作更复杂的函数可由组合许多操作描述——例如神经网络的前向传播就是很多操作的深度复合。不失一般性,我们定义一个操作只返回单输出变量;这不损失一般性因为输出变量可有多个分量(如向量、矩阵、张量都是单输出变量,只是其"多个分量"在数据结构内)。反向传播的软件实现通常支持多输出操作(一次返回多个独立张量),但我们在描述中避免这一情形因为它引入很多不重要的细节。

若变量 \(y\) 由对变量 \(x\) 应用操作计算得到,我们画一条从 \(x\)\(y\) 的有向边;有时在输出节点标注所施加操作的名称(如 \(x \xrightarrow{\times} z\)),其他时候在操作从上下文清楚时省略该标签(如 \(x \to z\))。计算图的好处是把所有计算"分解到原子操作层",使反向传播只需对每条边计算该操作的局部导数;这正是 6.5.6 节"一般反向传播"的关键思想。计算图的例子见图 6.8:图 (a) 用 × 操作计算 \(z = xy\);图 (b) 是逻辑回归预测 \(\hat{y} = \sigma(x^\top w + b)\),代数表达式中无名字的中间表达式在图中需要名字,我们简单把第 \(i\) 个这样的变量命名为 \(u^{(i)}\);图 (c) 是表达式 \(H = \max\{0, XW + b\}\) 的计算图,对含小批量输入 \(X\) 的设计矩阵计算修正线性单元激活的设计矩阵 \(H\),其中 matmul 操作施加于 \(X\)\(W\) 之上得到 \(U^{(1)}\),+ 操作加上偏置 \(b\) 得到 \(U^{(2)}\),relu 操作施加逐元素 max 得 \(H\);图 (d) 显示对变量施加多个操作的例子——线性回归模型的权重 \(w\) 既用于做预测 \(\hat{y}\) 又用于权重衰减惩罚 \(\lambda \sum_i w_i^2\),意味着 \(w\) 在图中对应多个输出节点——这一"一个变量被多个操作使用"的情形对反向传播很重要:因为 \(\partial J / \partial w\) 需要把两个分支的梯度求和(来自 \(\hat{y}\) 的贡献和来自正则项 \(\lambda \sum_i w_i^2\) 的贡献),这正是 6.5.6 节"一般反向传播"中"对到达同一节点的多条路径求和"的实例——通过引入"图的"概念,使我们能统一处理前向-反向计算路径并避免重复子表达式的指数爆炸问题。计算图这一抽象也让深度学习框架的 API 设计变得自然:用户用基本操作(PyTorch 的 torch.matmul/torch.relu 等)搭建计算图,框架自动追踪依赖关系并实现反向传播——无需用户手动求导,这大大降低了深度学习应用的工程门槛——研究者无需再为每个新模型手动推导并实现梯度公式——这是深度学习在 2010 年代得以广泛应用的关键工程基础——没有自动微分,每个新模型需要数周手动推导和实现梯度;有了自动微分,几小时即可上手实验。

6.5.2 微积分链式法则(Chain Rule of Calculus)

微积分链式法则(不要与概率链式法则混淆——后者是 \(P(A, B) = P(A) P(B|A)\),前者是 \((f \circ g)'(x) = f'(g(x)) g'(x)\))用于计算由其他已知导数的函数复合而成的函数的导数。反向传播是一种以特定运算顺序计算链式法则的算法,这种顺序极为高效——避免了朴素链式法则展开的指数爆炸。设 \(x\) 为实数,\(f\)\(g\) 都为实数到实数的函数;设 \(y = g(x)\)\(z = f(g(x)) = f(y)\),则链式法则陈述 \(\frac{dz}{dx} = \frac{dz}{dy} \frac{dy}{dx}\)(式 6.44)。这意味着 \(x\) 的微小变化 \(\delta x\) 引起 \(y\) 的变化 \(\delta y = (dy/dx) \delta x\),进而引起 \(z\) 的变化 \(\delta z = (dz/dy) \delta y = (dz/dy)(dy/dx) \delta x\),所以 \(dz/dx\) 是两个局部导数的乘积。

可推广到非标量情形:设 \(x \in \mathbb{R}^m\)\(y \in \mathbb{R}^n\)\(g\)\(\mathbb{R}^m\) 映射到 \(\mathbb{R}^n\)\(f\)\(\mathbb{R}^n\) 映射到 \(\mathbb{R}\);若 \(y = g(x)\)\(z = f(y)\),则 \(\frac{\partial z}{\partial x_i} = \sum_j \frac{\partial z}{\partial y_j} \frac{\partial y_j}{\partial x_i}\)(式 6.45)——\(z\) 关于 \(x\) 的每个分量的偏导数是关于 \(y\) 的所有分量的偏导数之和,每项是相应局部导数的乘积。用向量记号可等价地写为 \(\nabla_x z = \left(\frac{\partial y}{\partial x}\right)^\top \nabla_y z\)(式 6.46),其中 \(\frac{\partial y}{\partial x}\)\(g\)\(n \times m\) 雅可比矩阵(每项 \((i, j)\)\(\partial y_i / \partial x_j\))。

由此看到变量 \(x\) 的梯度可通过将雅可比矩阵 \(\frac{\partial y}{\partial x}\) 乘以梯度 \(\nabla_y z\) 获得——更精确地说是雅可比的转置乘以梯度。反向传播算法由对图中每个操作执行这种"雅可比-梯度乘积"组成:每个操作知道自己的雅可比(在反向传播语境下就是 bprop 规则),把上游传来的梯度乘以该操作对应的雅可比就得到下游梯度。通常我们不把反向传播算法仅用于向量,而用于任意维度的张量;概念上这与向量的反向传播完全相同,唯一区别是数如何排列为网格以形成张量。我们可以想象在运行反向传播前把每个张量展平为向量、计算向量值梯度、再把梯度整形回张量。在这种重新排列的视角下,反向传播仍然只是雅可比乘以梯度——只是雅可比现在是对"展平向量"而言的,但具体计算上每个操作只需知道自己关于其输入的局部导数。

为表示值 \(z\) 关于张量 \(X\) 的梯度,我们写 \(\nabla_X z\),就如 \(X\) 是向量一样。\(X\) 的下标现在有多个坐标——例如 3-D 张量由三坐标索引;可用单变量 \(i\) 表示完整下标元组以抽象掉这一点——对所有可能下标元组 \(i\)\((\nabla_X z)_i\) 给出 \(\frac{\partial z}{\partial X_i}\),与对所有进入向量的整数下标 \(i\)\((\nabla_x z)_i\) 给出 \(\frac{\partial z}{\partial x_i}\) 相同。使用该记号可写张量的链式法则:若 \(Y = g(X)\)\(z = f(Y)\),则 \(\nabla_X z = \sum_j (\nabla_X Y_j) \frac{\partial z}{\partial Y_j}\)(式 6.47)——这是 6.5.6 节"一般反向传播"中每个操作的 bprop 规则需要实现的数学形式——具体地说,op.bprop(inputs, X, G) 形式上需要实现的就是 \(\sum_i (\nabla_X \text{op.f(inputs)}_i) G_i\),即上游梯度 \(G\) 关于该操作输出被乘以雅可比后求和——这与"Jacobian-vector product"(JVP)或"vector-Jacobian product"(VJP)的计算完全等价,是自动微分理论的核心对象;其数值实现方式(forward mode vs reverse mode)直接决定了反向传播的算法形态;下面 6.5.9 节会进一步讨论反向模式与前向模式的对比和自动微分理论。值得注意的是链式法则的"链"既可从输入端开始逐层向输出端传播(forward mode),也可从输出端开始逐层向输入端传播(reverse mode,反向传播所用)。对单输出(如损失函数)多输入(如模型参数)的情形,reverse mode 显著更高效;对单输入多输出情形,forward mode 相对更高效。这正是为什么神经网络训练(单损失函数对百万级参数的梯度)几乎总是用 reverse mode(即反向传播)的原因——单输出多输入的情形下 reverse mode 的计算量与 forward mode 相比为 1 : m(m 是输入数),对大 m 极其高效。Jacobian 矩阵本身可能极大(\(n \times m\),对大 m 内存和时间都昂贵),但 reverse mode 一次只计算 \(\nabla_x z\)(一个 \(m\) 维向量),不需要显式构造 Jacobian 矩阵——这正是反向传播算法的工程实用性所在,使训练大模型成为可能——这一性质是反向传播成为深度学习训练算法的根本原因。

6.5.3 递归应用链式法则获得反向传播(Recursively Applying the Chain Rule to Obtain Backprop)

用链式法则直接写下标量关于计算该标量的计算图中任何节点的梯度的代数表达式是简单的。但在计算机中实际评估该表达式引入一些额外考虑。具体地说,许多子表达式可能在梯度整体表达式中重复出现多次;任何计算梯度的过程都需选择存储这些子表达式还是多次重新计算它们。图 6.9 给出这些重复子表达式如何产生的例子。在某些情形下计算同一子表达式两次仅仅是浪费;对复杂图而言,可能有指数多的这种浪费计算,使链式规则的朴素实现不可行。在其他情形下计算同一子表达式两次可能是以更高运行时间为代价减少内存消耗的有效方法。

我们先以直接指定实际梯度计算的反向传播算法版本开始(算法 6.2 与相关前向计算的算法 6.1),按实际执行的顺序并根据链式规则的递归应用。也可直接执行这些计算或把算法的描述视为对执行梯度计算的计算图的符号化规范。然而这一表述并未明确说明执行梯度计算的符号图的操纵和构造——这种表述在 6.5.6 节给出,使用算法 6.5,也在节内把推广到含任意张量的节点。

首先考虑描述如何计算单标量 \(u^{(n)}\)(例如一个训练样本的损失)的计算图。该标量是我们希望获得其梯度的量,关于 \(n_i\) 个输入节点 \(u^{(1)}\)\(u^{(n_i)}\):换言之希望计算 \(\frac{\partial u^{(n)}}{\partial u^{(i)}}\) 对所有 \(i \in \{1, 2, \ldots, n_i\}\)。在把反向传播应用于为梯度下降计算参数梯度的应用中,\(u^{(n)}\) 将是一个样本或一个小批量的代价,\(u^{(1)}\)\(u^{(n_i)}\) 对应模型参数。

我们假设图节点已排序使我们可以一个接一个地计算它们的输出,从 \(u^{(n_i+1)}\) 开始直到 \(u^{(n)}\)。如算法 6.1 定义,每个节点 \(u^{(i)}\) 与一个操作 \(f^{(i)}\) 关联,通过评估函数 \(u^{(i)} = f^{(i)}(A^{(i)})\) 计算,其中 \(A^{(i)}\)\(u^{(i)}\) 所有父节点的集合。该算法规定前向传播计算,可放入图 \(G\)。为执行反向传播,可构造一个依赖于 \(G\) 的计算图,并向其增加额外一组节点。这些节点构成子图 \(B\)\(G\) 中每节点对应一个。\(B\) 中计算按 \(G\) 中计算顺序的逆序进行,\(B\) 的每节点计算与前向图节点 \(u^{(i)}\) 关联的导数 \(\frac{\partial u^{(n)}}{\partial u^{(i)}}\),通过相对标量输出 \(u^{(n)}\) 的链式法则完成:

\[ \frac{\partial u^{(n)}}{\partial u^{(j)}} = \sum_{i: j \in \text{Pa}(u^{(i)})} \frac{\partial u^{(n)}}{\partial u^{(i)}} \frac{\partial u^{(i)}}{\partial u^{(j)}} \]

如算法 6.2 所规定。子图 \(B\) 恰好为 \(G\) 中从节点 \(u^{(j)}\) 到节点 \(u^{(i)}\) 的每条边包含一条边。从 \(u^{(j)}\)\(u^{(i)}\) 的边与计算 \(\frac{\partial u^{(i)}}{\partial u^{(j)}}\) 关联。另外,对每节点执行点积,介于已计算的关于 \(u^{(j)}\) 子节点 \(u^{(i)}\) 的梯度与含这些子节点 \(u^{(i)}\) 的偏导数 \(\frac{\partial u^{(i)}}{\partial u^{(j)}}\) 的向量之间。总之执行反向传播所需计算量与 \(G\) 中边数线性相关,每条边的计算对应计算一个偏导数(一个节点关于其一个父节点)以及执行一次乘法和一次加法。下面把这一分析推广到张量值节点,这只是把多个标量值组合在同节点并支持更高效实现的方式。

反向传播算法设计为不关心内存地减少公共子表达式数——具体地说,它对图中每节点执行大约一次雅可比乘积。这可从反向传播(算法 6.2)每条边访问一次以获得关联偏导数 \(\frac{\partial u^{(i)}}{\partial u^{(j)}}\) 的事实看出。因此反向传播避免了重复子表达式的指数爆炸。然而其他算法可通过在计算图上执行简化避免更多子表达式,或可通过重新计算而非存储某些子表达式来节省内存。描述完反向传播算法本身后我们会回到这些思想。

在具有 \(n\) 节点的图中计算梯度永远不会执行超过 \(O(n^2)\) 次操作或存储超过 \(O(n^2)\) 次操作的输出。这里我们计算的是计算图中的操作而非底层硬件执行的个体操作,所以记住每次操作的运行时间可能高度可变很重要——例如两个各含百万分量的矩阵相乘可能在图中对应单次操作。计算梯度至多需 \(O(n^2)\) 次操作,因为前向传播阶段最坏执行原图的所有 \(n\) 个节点(取决于我们想计算的值,可能不需执行整个图)。反向传播算法对原图每条边增加一个雅可比-向量积,该积应用 \(O(1)\) 节点表示。因为计算图是有向无环图它最多有 \(O(n^2)\) 条边。对实践中常用的图情况更好——大多数神经网络代价函数大致是链式结构,使反向传播有 \(O(n)\) 成本。这远好于朴素方法(可能需执行指数多节点)。这种潜在指数成本可通过对递归链式法则(式 6.49)非递归地展开和重写看出:

\[ \frac{\partial u^{(n)}}{\partial u^{(j)}} = \sum_{\text{path } (u^{(\pi_1)}, u^{(\pi_2)}, \ldots, u^{(\pi_t)}) \text{ from } \pi_1 = j \text{ to } \pi_t = n} \prod_{k=2}^t \frac{\partial u^{(\pi_k)}}{\partial u^{(\pi_{k-1})}} \]

因从节点 \(j\) 到节点 \(n\) 的路径数可随路径长度指数增长,上式中项数(这些路径数)可随前向传播图的深度指数增长;这种大成本会因 \(\frac{\partial u^{(i)}}{\partial u^{(j)}}\) 的同一计算被多次重做而产生。为避免这种重计算,可把反向传播视为利用存储中间结果 \(\frac{\partial u^{(n)}}{\partial u^{(i)}}\) 的填表算法:图中每节点都有表中对应槽存储该节点梯度;通过按顺序填入这些表项,反向传播避免重复许多公共子表达式。这种填表策略有时称为动态规划。

6.5.4 全连接 MLP 中的反向传播计算(Back-Propagation Computation in Fully-Connected MLP)

为澄清上述反向传播计算的定义,让我们考虑与全连接多层 MLP 关联的具体图。算法 6.3 先展示前向传播,将参数映射到与单(输入,目标)训练样本 \((x, y)\) 关联的有监督损失 \(L(\hat{y}, y)\)\(\hat{y}\)\(x\) 输入时神经网络的输出——这一单样本设定简化了伪代码,实践中应使用小批量。算法 6.4 然后展示对该图应用反向传播算法所做的对应计算——从输出层开始逐层向输入层反向计算每层激活上的梯度,再由激活梯度得到每层参数(权重和偏置)的梯度。算法 6.3 和 6.4 是为简单和直接理解而选择的演示;然而它们专门针对一个具体问题(全连接 MLP + 单样本 + 简单损失),不能直接处理卷积网络、循环网络等更复杂架构,也不能直接处理多输出雅可比等情形,也不展示一些工程实现的关键技巧(如多输出操作的批量反向传播、in-place 操作节省内存等)——例如算法 6.4 中"将层输出梯度转换为对前非线性激活梯度"对 \(f\) 逐元素时是逐元素乘,但对卷积等多输出操作则需用专门的 bprop 规则(卷积的 bprop 是转置卷积)。现代软件实现基于 6.5.6 节描述的反向传播广义形式,通过显式操纵表示符号化计算的数据结构可适应任何计算图。

算法 6.3(典型深度神经网络前向传播与代价函数计算):损失 \(L(\hat{y}, y)\) 依赖输出 \(\hat{y}\) 和目标 \(y\)(损失函数例子见 6.2.1.1)。为获得总代价 \(J\),损失可与正则化项 \(\Omega(\theta)\) 相加,其中 \(\theta\) 含所有参数(权重和偏置)。算法 6.4 展示如何计算 \(J\) 关于参数 \(W\)\(b\) 的梯度。为简单起见该演示仅使用单输入样本 \(x\);实际应用应使用小批量(更多真实演示见 6.5.7)。需要:网络深度 \(l\)\(W^{(i)}, i \in \{1, \ldots, l\}\) 权重矩阵;\(b^{(i)}, i \in \{1, \ldots, l\}\) 偏置参数;\(x\) 输入;\(y\) 目标输出。算法骨架:\(h^{(0)} = x\);对 \(k = 1, \ldots, l\)\(a^{(k)} = b^{(k)} + W^{(k)} h^{(k-1)}\)\(h^{(k)} = f(a^{(k)})\)\(\hat{y} = h^{(l)}\)\(J = L(\hat{y}, y) + \lambda \Omega(\theta)\)

算法 6.3 的核心是把网络视为有 \(l\) 层的链式复合:\(h^{(0)} = x\)(输入),对每层 \(k\) 先做仿射变换 \(a^{(k)} = W^{(k)} h^{(k-1)} + b^{(k)}\),再施加激活函数 \(h^{(k)} = f(a^{(k)})\),最后用 \(h^{(l)}\) 作为输出 \(\hat{y}\)。这种"仿射变换 + 逐元素非线性"的统一模式是几乎所有现代神经网络的共同骨架——卷积网络把 \(W^{(k)} h^{(k-1)}\) 替换为卷积运算,循环网络把 \(h^{(k-1)}\) 替换为前一时间步的隐藏状态,但仿射变换 + 非线性的两阶段结构不变。

算法 6.4 的反向计算伪代码则从输出层开始,逐层向输入层回传梯度——其关键思想是把"层输出梯度 \(g\)"转换为"层前非线性激活梯度"(对逐元素 \(f\) 是逐元素乘 \(g \odot f'(a^{(k)})\)),再由此得到该层参数(\(W^{(k)}, b^{(k)}\))上的梯度 \(\nabla_{W^{(k)}} J = g h^{(k-1)\top} + \lambda \nabla_{W^{(k)}} \Omega(\theta)\)\(\nabla_{b^{(k)}} J = g + \lambda \nabla_{b^{(k)}} \Omega(\theta)\),最后由 \(W^{(k)\top} g\) 把梯度传播到下一更低层的激活 \(h^{(k-1)}\)。这一算法的关键观察是:每层的局部计算只依赖该层的激活 \(a^{(k)}\)\(h^{(k-1)}\) 和从上一层回传的梯度 \(g\)——这种"局部性"使每层 bprop 可独立实现(这也是 6.5.6 节"一般反向传播"中"每操作实现自己的 bprop"原则的具体体现)。

需要强调的是算法 6.3-6.4 只是为教学目的构造的最小例子——它对单输入样本、单损失项、链式架构作了简化假设。实际应用需要扩展到:(1)小批量输入(设计矩阵 \(X\) 替代单向量 \(x\),所有操作改为批形式);(2)多种损失函数(通过 cross_entropy、mse_loss 等特定 bprop 规则支持);(3)多种层类型(卷积层、池化层、归一化层等各有特定 bprop 规则)。这些扩展由现代深度学习框架(PyTorch、TensorFlow、JAX 等)通过模块化设计自动处理。算法 6.3 和 6.4 的另一个重要观察是"前向计算的所有中间结果必须保留":具体地说,\(h^{(k)}\)(每层的激活输出)和 \(a^{(k)}\)(每层的仿射变换结果)都需要在反向传播时可用,因为反向计算 \(\nabla_{W^{(k)}} J\)\(\nabla_{b^{(k)}} J\) 都需要这些值。这意味着训练一个 \(l\) 层网络的前向传播阶段不仅要计算输出 \(\hat{y}\),还要保存所有中间激活——这是为什么反向传播的内存成本大致与网络"激活总量"成正比,而不是与输出大小成正比。从另一个角度看,算法 6.3-6.4 描述的是"模型 + 优化器"框架中"优化器"如何利用"模型"的导数信息——这是深度学习区别于传统优化(如 SVM 的对偶问题求解)的关键:传统优化通常利用问题的特殊结构(如稀疏性、低秩性)获得高效解,而神经网络的反向传播不依赖任何特殊结构,对任意可微复合函数都通用。

6.5.5 符号到符号的导数(Symbol-to-Symbol Derivatives)

代数表达式和计算图都作用于符号——或称没有具体值的变量。这些基于代数和图的表示称为符号表示。当我们实际使用或训练神经网络时必须给这些符号赋具体值:把网络输入符号 \(x\) 替换为具体数值如 \([1.2, 3.765, -1.8]^\top\)

反向传播的某些方法接受计算图和图输入的一组数值,然后返回描述该输入值处梯度的一组数值——称为"符号到数"微分(symbol-to-number differentiation),是 Torch(Collobert et al., 2011b)和 Caffe(Jia, 2013)等库使用的方法。另一种方法是接受计算图并向图添加提供所希望导数符号描述的额外节点——这是 Theano(Bergstra et al., 2010; Bastien et al., 2012)和 TensorFlow(Abadi et al., 2015)使用的方法。图 6.10 展示该方法工作原理的例子。该方法的主要优点是导数以与原始表达式相同的语言描述;因为导数只是另一计算图,可再次运行反向传播对导数求导以得到高阶导数(高阶导数计算在 6.5.10 节描述)。

我们将使用后一种方法并以构造导数的计算图方式描述反向传播算法。图任何子集可在之后用具体数值评估——这让我们避免指定每操作应何时被计算;通用图求值引擎可在每节点的父节点值可用时立即评估该节点。基于符号到符号方法的描述包含基于符号到数方法:基于符号到数方法可被理解为执行与基于符号到符号方法构建的图中完全相同的计算;关键区别是基于符号到数方法不暴露该图。

算法 6.4(算法 6.3 深度神经网络的反向计算)除输入 \(x\) 外还使用目标 \(y\)。该计算产生每层 \(k\) 激活 \(a^{(k)}\) 上的梯度,从输出层开始向后到第一个隐藏层。从这些梯度(可解释为每层输出应如何变化以减少误差的指示)可获得每层参数的梯度。权重和偏置上的梯度可立即作为随机梯度更新的一部分(计算完梯度后立即执行更新)或与其他基于梯度的优化方法一起使用。算法骨架:前向计算后,计算输出层上的梯度 \(g \leftarrow \nabla_{\hat{y}} J = \nabla_{\hat{y}} L(\hat{y}, y)\);对 \(k = l, l-1, \ldots, 1\):把层输出上的梯度转换为对前非线性激活的梯度(\(f\) 逐元素时是逐元素乘)\(g \leftarrow \nabla_{a^{(k)}} J = g \odot f'(a^{(k)})\);计算权重和偏置上的梯度(含需要的正则化项)\(\nabla_{b^{(k)}} J = g + \lambda \nabla_{b^{(k)}} \Omega(\theta)\)\(\nabla_{W^{(k)}} J = g h^{(k-1)\top} + \lambda \nabla_{W^{(k)}} \Omega(\theta)\);传播梯度到下一较低层隐藏单元的激活 \(g \leftarrow \nabla_{h^{(k-1)}} J = W^{(k)\top} g\)

注:上面"算法 6.4"伪代码的实际位置是 6.5.4 节末尾,但因编辑调整显示在 6.5.5 节;读者可将其视为 6.5.4 节的延续。两种"符号到符号"和"符号到数"方法各有优势——符号到符号方法的导数仍是计算图,可再次反向传播(二阶、三阶、...),适合需要高阶导数的场景;符号到数方法则更轻量,适合只需一阶梯度且对计算开销敏感的部署场景。本节后续将基于符号到符号方法展开,这是现代深度学习框架的主流选择。符号到符号方法的核心技术是"对导数再求导"——这要求反向传播算法本身能够处理"导数作为新输入"的情形,6.5.6 节的一般反向传播将提供这种能力的形式骨架。

6.5.6 一般反向传播(General Back-Propagation)

反向传播算法很简单。要计算某标量 \(z\) 关于图上一祖先 \(x\) 的梯度,我们从观察 \(z\) 关于自身的梯度由 \(\frac{dz}{dz} = 1\) 给出开始。然后可通过将当前梯度乘以产生 \(z\) 的操作的雅可比矩阵,计算 \(z\) 关于 \(z\) 在图中每个父节点的梯度;以这种方式继续乘以雅可比向后通过图直到达到 \(x\)。对任何可通过从 \(z\) 向后经过两条或多条路径到达的节点,我们简单地把从不同路径到达的梯度在该节点求和。

更形式地,\(G\) 中每节点对应一变量。为达最大一般性,我们把该变量描述为张量 \(V\);张量一般可有任何维数,包含标量、向量、矩阵。每变量 \(V\) 与以下子例程关联:

  • get_operation(V):返回计算 \(V\) 的操作,由计算图中进入 \(V\) 的边表示。例如可能有代表矩阵乘法操作的 Python 或 C++ 类,以及 get_operation 函数;假设有一个通过矩阵乘法创建的变量 \(C = AB\),则 get_operation(V) 返回相应 C++ 类的实例的指针。
  • get_consumers(V, G):返回 \(G\)\(V\) 的子节点变量列表。
  • get_inputs(V, G):返回 \(G\)\(V\) 的父节点变量列表。

每操作 op 也与一 bprop 操作关联。该 bprop 操作可计算式 6.47 描述的雅可比-向量积;这就是反向传播算法能够实现巨大一般性的方式。每操作负责知道如何通过其参与的图中边反向传播。例如可用矩阵乘法操作创建变量 \(C = AB\)。假设 \(z\) 关于 \(C\) 的梯度由 \(G\) 给出。矩阵乘法操作负责定义两个反向传播规则,每输入参数一个。若调用 bprop 方法请求给定输出上梯度为 \(G\) 时关于 \(A\) 的梯度,则矩阵乘法操作的 bprop 方法必须声明关于 \(A\) 的梯度由 \(GB^\top\) 给出;同样若调用 bprop 方法请求关于 \(B\) 的梯度,则矩阵操作负责实现 bprop 方法并指明所希望梯度由 \(A^\top G\) 给出。反向传播算法本身不需要知道任何求导规则;只需用正确参数调用每操作的 bprop 规则。形式上 op.bprop(inputs, X, G) 必须返回 \(\sum_i (\nabla_X \text{op.f(inputs)}_i) G_i\),这正是式 6.47 表达的链式法则的实现——inputs 是提供给操作的输入列表,op.f 是操作实现的数学函数,\(X\) 是希望计算梯度的输入,\(G\) 是操作输出上的梯度。

op.bprop 方法应始终假设其所有输入彼此不同,即使它们并非如此——例如若 mul 操作符被传入 \(x\) 的两个副本以计算 \(x^2\),op.bprop 方法仍应返回 \(x\) 作为关于两个输入的导数;反向传播算法稍后将这两个参数相加得到 \(2x\),即关于 \(x\) 的正确总导数。

反向传播的软件实现通常同时提供操作和它们的 bprop 方法,使深度学习软件库的用户能够反向传播通过用常见操作(矩阵乘法、指数、对数等)构建的图。构建反向传播新实现的软件工程师或需要向现有库添加自己操作的高级用户通常必须为任何新操作手动推导 op.bprop 方法。

反向传播算法在算法 6.5 中形式化描述:最外层骨架部分做简单的设置和清理工作,大部分重要工作发生在算法 6.6 的 build_grad 子例程中。需要:\(T\)(必须计算其梯度的目标变量集合);\(G\)(计算图);\(z\)(要被微分的变量)。算法骨架:\(G'\) 剪枝为仅含 \(z\) 的祖先和 \(T\) 节点的子孙的图;初始化 grad_table(关联张量与其梯度的数据结构);\(\text{grad\_table}[z] \leftarrow 1\);对 \(V \in T\)\(\text{build\_grad}(V, G, G', \text{grad\_table})\);返回 grad_table 限于 \(T\)

6.5.2 节中我们解释过反向传播的开发是为了避免在链式法则中多次计算同一子表达式。朴素算法可能因这些重复子表达式有指数运行时间。现在我们已经具体说明了反向传播算法,可以理解其计算成本。假设每操作评估有大致相同成本,那么我们可按所执行操作数分析计算成本。记住这里我们把操作称为计算图的基本单位,它可能实际包含非常多的算术操作(例如我们可能有把矩阵乘法视为单次操作的图)。在具有 \(n\) 节点的图中计算梯度永远不会执行超过 \(O(n^2)\) 次操作或存储超过 \(O(n^2)\) 次操作的输出。这里我们计算的是计算图中的操作而非底层硬件执行的个体操作,所以记住每次操作的运行时间可能高度可变很重要——例如两个各含百万分量的矩阵相乘可能对应图中单次操作。计算梯度至多需 \(O(n^2)\) 次操作,因为前向传播阶段最坏执行原图的所有 \(n\) 个节点(取决于我们想计算的值,可能不需执行整个图)。反向传播算法对原图每条边增加一个雅可比-向量积,该积应用 \(O(1)\) 节点表示。因为计算图是有向无环图它最多有 \(O(n^2)\) 条边。对实践中常用的图情况更好——大多数神经网络代价函数大致是链式结构,使反向传播有 \(O(n)\) 成本。这远好于朴素方法(可能需执行指数多节点)。这种潜在指数成本可通过对递归链式法则(式 6.49)非递归地展开和重写看出:因为从节点 \(j\) 到节点 \(n\) 的路径数可随这些路径长度指数增长,上式中项数(即这些路径数)可随前向传播图的深度指数增长。这种大成本会因 \(\frac{\partial u^{(i)}}{\partial u^{(j)}}\) 的同一计算被多次重做而产生。为避免这种重计算,可把反向传播视为利用存储中间结果 \(\frac{\partial u^{(n)}}{\partial u^{(i)}}\) 的填表算法——图中每节点都有表中对应槽存储该节点梯度;通过按顺序填入这些表项,反向传播避免重复许多公共子表达式。这种填表策略有时称为动态规划。

算法 6.6(build_grad 子例程)由算法 6.5 定义的反向传播算法调用。子例程为:若 \(V\) 已在 grad_table 中则返回 grad_table[\(V\)];否则对 \(V\)\(G'\) 中每子节点 \(C\):取 op 为 \(C\) 的操作;递归调用 build_grad(\(C\), \(G\), \(G'\), grad_table) 获得 \(D\);计算 \(G^{(i)} \leftarrow \text{op.bprop}(\text{get\_inputs}(C, G'), V, D)\);最后 \(G \leftarrow \sum_i G^{(i)}\),存 \(\text{grad\_table}[V] = G\),将 \(G\) 和创建它的操作插入 \(G\),返回 \(G\)

6.5.7 示例:MLP 训练的反向传播(Example: Back-Propagation for MLP Training)

作为例子,我们逐步介绍反向传播算法在训练多层感知机中的使用。这里我们开发一个非常简单的单隐藏层多层感知机;为训练该模型我们将使用小批量随机梯度下降。反向传播算法用于计算单小批量上代价的梯度。具体地,我们用训练集中的小批量样本,格式化为设计矩阵 \(X\) 和关联类标签向量 \(y\)。网络计算隐藏特征层 \(H = \max\{0, XW^{(1)}\}\);为简化呈现不在该模型中使用偏置,假设我们的图语言包含能逐元素计算 \(\max\{0, Z\}\) 的 relu 操作。类上未归一化对数概率的预测由 \(HW^{(2)}\) 给出;假设我们的图语言包含计算这些未归一化对数概率定义的概率分布与目标 \(y\) 之间交叉熵的 cross_entropy 操作。所得交叉熵定义代价 \(J_{\text{MLE}}\),最小化该交叉熵执行分类器的最大似然估计。然而为使该例子更真实,我们也加入正则化项。总代价

\[ J = J_{\text{MLE}} + \lambda \left(\sum_{i, j} (W_{i, j}^{(1)})^2 + \sum_{i, j} (W_{i, j}^{(2)})^2\right) \]

由交叉熵和系数 \(\lambda\) 的权重衰减项组成。计算图在图 6.11 中说明。

该例子的梯度计算图大到画或读都繁琐,这演示了反向传播算法的一个优点——它能自动生成对软件工程师来说虽直接但繁琐的梯度。我们可大致通过看图 6.11 中的前向传播图跟踪反向传播算法的行为。为训练我们希望同时计算 \(\nabla_{W^{(1)}} J\)\(\nabla_{W^{(2)}} J\)。从 \(J\) 向权重回走有两条不同路径:一条通过交叉熵代价,一条通过权重衰减代价。权重衰减代价相对简单——它总是对 \(W^{(i)}\) 的梯度贡献 \(2\lambda W^{(i)}\)

通过交叉熵代价的另一路径稍微复杂。设 \(G\) 是 cross_entropy 操作提供的关于未归一化对数概率 \(U^{(2)}\) 的梯度。反向传播算法现在需要探索两个不同分支。在较短分支上,它用矩阵乘法操作的第二个参数的反向传播规则把 \(H^\top G\) 加到 \(W^{(2)}\) 的梯度。另一分支对应沿网络向下延伸的更长链:首先反向传播算法用矩阵乘法操作的第一个参数的反向传播规则计算 \(\nabla_H J = GW^{(2)\top}\)。接着 relu 操作用其反向传播规则将对应 \(U^{(1)}\) 小于 0 那些分量的梯度清零,设结果为 \(G'\)。反向传播算法的最后一步是用 matmul 操作第二个参数的反向传播规则把 \(X^\top G'\) 加到 \(W^{(1)}\) 的梯度。

这些梯度计算完后,梯度下降算法或其他优化算法有责任用这些梯度更新参数。对 MLP 而言,计算成本由矩阵乘法成本主导。在前向传播阶段,我们乘以每个权重矩阵,产生 \(O(w)\) 乘加,其中 \(w\) 是权重数。在反向传播阶段,我们乘以每个权重矩阵的转置,计算成本相同。算法的主要内存成本是需存储隐藏层非线性的输入;该值从被计算之时存储直到反向传播回到同一点,因此内存成本为 \(O(m n_h)\),其中 \(m\) 是小批量中样本数,\(n_h\) 是隐藏单元数——对大模型这可能成为瓶颈,需要梯度检查点(gradient checkpointing)等内存优化技术(用更多计算换更少内存——通过在前向传播时丢弃中间激活、在反向传播时重新计算它们,内存可降至 \(O(L)\) 数量级,代价是计算量约翻倍)。对训练超大规模模型(如 GPT-3 类),这种"计算换内存"策略常是唯一可行的训练方案,使训练数万亿参数模型成为可能。从事大规模训练的研究者和工程师通常需要同时管理显存容量、计算吞吐量、checkpoint 频率和重新计算策略等,是一个独立的工程子领域,需要深厚的分布式系统和 GPU 编程功底。

6.5.8 复杂性(Complications)

这里我们对反向传播算法的描述比实际使用的实现简单。如上所述,我们把操作定义限制为返回单张量的函数。大多数软件实现需支持能返回多个张量的操作——例如若希望同时计算张量中的最大值及该值的下标,最好在单次内存访问中计算两者,所以最高效的做法是把该过程实现为带两个输出的单操作。我们未描述如何控制反向传播的内存消耗。反向传播常涉及将多个张量相加;朴素方法会分别计算这些张量,然后在第二步将它们全部相加——这种朴素方法有过高的内存瓶颈,可通过维护单缓冲器并在每个值被计算时将其加到该缓冲器来避免。真实世界反向传播实现也需处理各种数据类型,如 32 位浮点、64 位浮点和整数值;处理每种类型的策略需特别小心设计。一些操作具有未定义梯度,跟踪这些情形并确定用户所请求梯度是否未定义是重要的。还有各种其他技术细节使真实世界微分更复杂。这些技术细节并非不可逾越,本章已描述计算导数所需的关键知识工具,但意识到还有更多微妙之处存在是重要的——例如 NaN/Inf 处理(数值不稳定时如何优雅退化)、跨设备通信(多 GPU/TPU 训练时梯度如何跨设备同步和聚合)、即时编译(XLA/TorchScript 等把计算图编译为优化代码以加速);这些工程细节虽不影响"反向传播可计算梯度"这一根本结论,但实际决定了一个深度学习框架能否高效训练数十亿参数的大模型。在反向传播的具体实现层面,框架须处理一个变量被多个操作使用时的引用计数、in-place 操作的梯度传播正确性、稀疏张量的特殊优化、动态控制流(if/for)的可微处理等大量工程细节——这些是"计算图 + 自动微分"框架比"手动实现反向传播"复杂度更高的根本原因,也是"框架开发者"作为一个专业角色的存在理由——这与"应用研究者"形成分工。从事深度学习框架开发(PyTorch 核心、TensorFlow 核心、JAX 等)需要深厚的自动微分理论功底和工程实现能力,是深度学习生态系统的核心基础设施提供者。

6.5.9 深度学习社区之外的微分(Differentiation outside the Deep Learning Community)

深度学习社区在某种程度上与更广的计算机科学社区是隔离的,并主要发展了自己关于如何执行微分的文化态度。更一般地,自动微分(automatic differentiation)领域关注如何用算法计算导数。这里描述的反向传播算法只是自动微分方法之一,是称为反向模式累积(reverse mode accumulation)的更广技术类的特例。其他方法以不同顺序评估链式法则的子表达式;一般地,确定导致最低计算成本的评估顺序是困难的问题。找到计算梯度的最优操作序列是 NP 完全的(Naumann, 2008),意指可能需要将代数表达式化简为最便宜形式。

例如假设有变量 \(p_1, p_2, \ldots, p_n\) 表示概率,变量 \(z_1, z_2, \ldots, z_n\) 表示未归一化对数概率。假设定义 \(q_i = \frac{\exp(z_i)}{\sum_i \exp(z_i)}\),其中用指数化、求和和除法操作构建 softmax 函数,构造交叉熵损失 \(J = -\sum_i p_i \log q_i\)。人类数学家可观察到 \(J\) 关于 \(z_i\) 的导数形式非常简单:\(q_i - p_i\)。反向传播算法不能以这种方式化简梯度,而会显式地把梯度传播通过原图中所有对数和指数操作。一些软件库如 Theano(Bergstra et al., 2010; Bastien et al., 2012)能执行某种代数替换以改进纯反向传播算法提出的图。

\(G\) 有单输出节点且每偏导数 \(\frac{\partial u^{(i)}}{\partial u^{(j)}}\) 可用常数计算量计算时,反向传播保证梯度计算的计算数与前向计算的计算数同阶:算法 6.2 中每局部偏导数 \(\frac{\partial u^{(i)}}{\partial u^{(j)}}\) 沿递归链式法则(式 6.49)只需被计算一次,并伴随一次关联的乘法和加法。整体计算因此是 \(O(\#\text{edges})\)。然而可通过化简反向传播构建的计算图进一步降低,这是 NP 完全任务。Theano 和 TensorFlow 等实现使用基于匹配已知化简模式的启发式方法以迭代地尝试化简图。我们仅对标量输出的梯度计算定义了反向传播,但反向传播可扩展到计算雅可比矩阵(图中 \(k\) 个不同标量节点的,或包含 \(k\) 个值的张量值节点)。朴素实现可能需要 \(k\) 倍多计算:对原前向图中每标量内部节点,朴素实现计算 \(k\) 个梯度而非单梯度。当图输出数大于输入数时,有时更倾向于使用另一种自动微分形式——前向模式累积(forward mode accumulation)。例如前向模式计算曾被提议用于在循环网络中实时计算梯度(Williams and Zipser, 1989);这也避免存储整图的值和梯度的需要,以计算效率为代价换取内存。前向模式和反向模式之间的关系类似于左乘 vs 右乘一个矩阵序列,如 \(ABCD\),其中矩阵可视为雅可比矩阵——例如若 \(D\) 是列向量而 \(A\) 有很多行,这对应单输出多输入的图,从末尾开始向左乘只需矩阵-向量积,对应反向模式;相反若从左开始乘则涉及一系列矩阵-矩阵乘积,使整个计算昂贵得多;然而若 \(A\) 的行数少于 \(D\) 的列数,从左向右乘(对应前向模式)则更便宜。

在机器学习之外的很多社区中,更常见的是实现直接作用于传统编程语言代码(如 Python 或 C 代码)的微分软件,并自动生成对这些语言所写函数求导的程序。在深度学习社区中,计算图通常由专门库创建的显式数据结构表示。专门方法有需要库开发者为每操作定义 bprop 方法并限制库用户仅使用已定义操作的缺点;然而专门方法也有允许为每操作开发定制的反向传播规则以让开发者以自动过程显然无法复制的方式改进速度或稳定性的优点。

反向传播因此不是计算梯度的唯一方式或最优方式,但它是非常实用的方法,继续很好地服务于深度学习社区。未来随着深度学习从业者更了解自动微分更广领域的进展,深度网络的微分技术可能改进。

6.5.10 高阶导数(Higher-Order Derivatives)

一些软件框架支持使用高阶导数。在深度学习软件框架中至少包括 Theano 和 TensorFlow;这些库用与描述原始被微分函数相同类型的数据结构描述导数表达式,意味着符号微分机制可被应用于导数。在深度学习上下文中,计算标量函数的单二阶导数是罕见的——通常对 Hessian 矩阵的性质感兴趣。若有函数 \(f: \mathbb{R}^n \to \mathbb{R}\),则 Hessian 矩阵大小为 \(n \times n\);在典型深度学习应用中,\(n\) 将是模型参数数,可轻易达数十亿,完整 Hessian 矩阵因此甚至表示都不切实际。

代替显式计算 Hessian,典型深度学习方法是使用 Krylov 方法。Krylov 方法是一组执行各种操作(如近似求逆矩阵或找其特征向量/特征值近似)的迭代技术,仅使用矩阵-向量积操作。为在 Hessian 上使用 Krylov 方法,我们只需能计算 Hessian 矩阵 \(H\) 与任意向量 \(v\) 的乘积。直接做这件事的技术(Christianson, 1992)是计算 \(Hv = \nabla_x ((\nabla_x f(x))^\top v)\),该表达式中两个梯度计算可被适当软件库自动计算;注意外层梯度表达式对内层梯度表达式的函数求梯度。若 \(v\) 本身由计算图产生,重要的是指明自动微分软件不应微分通过产生 \(v\) 的图。

虽然计算 Hessian 通常不可取,但用 Hessian-向量积仍可能:只需对所有 \(i = 1, \ldots, n\) 计算 \(He^{(i)}\),其中 \(e^{(i)}\) 是第 \(i\) 个元素为 1、其余元素都为 0 的 one-hot 向量。但需 \(n\) 次 Hessian-向量积计算,每次都需要一次额外的反向传播通过计算图,开销约为 \(n\) 倍前向-反向计算——对大模型仍可能不切实际;实践中通常用截断 Newton 方法或随机近似(如 Hessian-free 优化、L-BFGS)代替精确 Hessian 计算。在深度学习语境下,Hessian 矩阵还用于诊断优化景观(如监控 Hessian 特征值分布可判断是否处于尖锐极小或平坦极小),但仍通常通过 Hessian-向量积而非显式构造 Hessian 实现;这一事实也说明了"符号到符号"反向传播框架相比"符号到数"框架的工程优势——同样的计算图既能求一阶导数又能求 Hessian-向量积。从工程实现角度看,深度学习框架的二阶自动微分能力(如 PyTorch 的 functorch、JAX 的 hessian transformation)正是基于对导数再求导这一原理——用户用同一个计算图既可求一阶梯度又可求 Hessian-向量积,无需手动推导二阶导数。在基于能量的模型(如 EBM)和物理信息神经网络(PINN)中,Hessian 信息甚至是模型方程的一部分——例如 PINN 用自动微分计算 PDE 残差中的二阶导数,本质上是计算网络输出关于输入的 Hessian 向量积。本节中"二阶导数"和"Hessian"两个概念在深度学习语境下常被混用,但严格说 Hessian 是 \(f\) 关于其输入的二阶偏导数矩阵,而二阶导数是对 Hessian 矩阵中的某个元素;从计算角度两者都可通过类似技术实现。"高阶导数"在某些场景的物理学信息机器学习和元学习(meta-learning)中扮演重要角色,Hessian 矩阵的迹和行列式可作为损失景观复杂度的量化指标,也常用于模型可解释性研究(如 Hessian 特征向量对应最敏感参数方向)。从更广的视角看,高阶导数反映了损失函数在参数空间的局部几何——零阶(函数值)给出当前位置的损失,一阶(梯度)给出上升/下降方向,二阶(Hessian)给出局部曲率信息,三阶及以上给出更精细的几何信息,但实践中很少直接使用。Hessian 特征值分析常用于评估模型平坦最小值 vs 尖锐最小值——后者泛化能力差,前者更好。这一观察也启发了若干优化算法设计。

6.6 历史笔记(Historical Notes)

前馈网络可被视为基于用梯度下降最小化函数逼近中的误差的高效非线性函数逼近器。从这个角度看,现代前馈网络是几个世纪以来一般函数逼近任务进展的顶峰。构成反向传播算法基础的链式法则在 17 世纪被发明(Leibniz, 1676; L'Hôpital, 1696)。微积分和代数长期以来被用于以闭式解优化问题,但梯度下降作为迭代逼近优化问题解的技术直到 19 世纪才被引入(Cauchy 1847 提出的"最速下降法"是现代梯度下降的雏形)(Cauchy, 1847)。

从 1940 年代开始,这些函数逼近技术被用于激发如感知机之类的机器学习模型。然而最早模型基于线性模型;包括 Marvin Minsky 在内的批评者指出了线性模型族的若干缺陷,如其不能学 XOR 函数,这导致了针对整个神经网络方法的反弹。学非线性函数需要发展多层感知机和通过这种模型计算梯度的方法。基于动态规划的链式法则的有效应用在 1960 和 1970 年代开始出现,大多用于控制应用(Kelley, 1960; Bryson and Denham, 1961; Dreyfus, 1962; Bryson and Ho, 1969; Dreyfus, 1973)但也用于灵敏度分析(Linnainmaa, 1976)。Werbos(1981)提议将这些技术应用于人工神经网络的训练;该思想在被以不同方式独立再发现后最终在实践中被发展(LeCun, 1985; Parker, 1985; Rumelhart et al., 1986a)。《Parallel Distributed Processing》一书在其中一个章节(Rumelhart et al., 1986b)中展示了一些反向传播首批成功实验的结果,对反向传播的推广贡献很大并启动了多层神经网络研究中一段非常活跃的时期。然而该书作者特别是 Rumelhart 和 Hinton 提出的思想远超反向传播;它们包含关于认知和学习几个中心方面的可能计算实现的重要思想,这些思想以"连接主义"(connectionism)知名,因为该思想流派把神经元之间的连接作为学习和记忆的位置而看重。这些思想特别包括分布式表示(distributed representation)的概念(Hinton et al., 1986)。

跟随反向传播的成功,神经网络研究获得流行并在 1990 年代早期达到顶峰。之后其他机器学习技术变得更流行,直到 2006 年开始的现代深度学习复兴。现代前馈网络背后的核心思想自 1980 年代以来没有实质改变;同样的反向传播算法和同样的梯度下降方法仍在使用。1986 至 2015 年神经网络性能的大多数改进可归因于两个因素。首先,更大的数据集减少了统计泛化对神经网络的挑战程度。其次,因更强大的计算机和更好的软件基础设施,神经网络变得大得多。然而少量算法变化也明显改善了神经网络的性能。

这些算法变化之一是用交叉熵损失函数族替代均方误差。均方误差在 1980 和 1990 年代流行,但随着思想在统计学社区和机器学习社区之间的传播,逐渐被交叉熵损失和最大似然原理替代。交叉熵损失的使用大大改善了 sigmoid 和 softmax 输出模型的性能,这些模型之前在使用均方误差损失时遭受饱和和学习缓慢之苦。

另一个大大改善前馈网络性能的主要算法变化是用分段线性隐藏单元(如修正线性单元)替代 sigmoid 隐藏单元。使用 \(\max\{0, z\}\) 函数的修正最早在早期神经网络模型中引入,至少可追溯到 Cognitron 和 Neocognitron(Fukushima, 1975, 1980)。这些早期模型未使用修正线性单元,而对非线性函数应用修正;尽管修正早期流行,修正在 1980 年代大体上被 sigmoid 替代,可能因为 sigmoid 在神经网络很小时表现更好。截至 2000 年代早期,修正线性单元因某种近乎迷信的信念——激活函数有不可微点必须避免——而被回避;这种情况在 2009 年左右开始改变。Jarrett et al.(2009)观察到"在神经网络架构设计的几个不同因素中,使用修正非线性是改善识别系统性能的最重要因素"。

对小数据集,Jarrett et al.(2009)观察到使用修正非线性比学隐藏层权重更重要——随机权重足以通过修正线性网络传播有用信息,使顶部分类器层能学如何把不同特征向量映射到类身份。当数据更多时,学习开始提取足够有用知识以超过随机选择参数的性能。Glorot et al.(2011a)表明学习在深度修正线性网络中比在激活函数有曲率或双侧饱和的深度网络中容易得多。

修正线性单元在历史上也有趣,因为它们表明神经科学继续对深度学习算法的发展有影响。Glorot et al.(2011a)从生物考量激发修正线性单元——半修正非线性意在捕捉生物神经元的这些性质:(1)对某些输入,生物神经元完全失活;(2)对某些输入,生物神经元的输出与其输入成正比;(3)大多数时间生物神经元运行在不活跃的区间(它们应有稀疏激活)。

当现代深度学习的复兴在 2006 年开始时,前馈网络继续名声不佳。从约 2006-2012 年,广泛认为前馈网络除非由其他模型(如概率模型)辅助否则表现不好。今天已知有了合适资源和工程实践前馈网络表现得很好;今天基于梯度的前馈网络学习被用作开发概率模型(如变分自编码器和生成对抗网络,第 20 章描述)的工具。2006 年时社区用无监督学习支持监督学习,现在反讽地用监督学习支持无监督学习则更常见。

前馈网络仍有未实现的潜力。未来我们预期它们将被应用于更多任务,且优化算法和模型设计的进展将进一步提高它们的性能。本章主要描述神经网络族模型;在后续章节中,我们转向如何使用这些模型——如何正则化和训练它们。

本章个人批注

第 6 章是 Part II "深度网络:现代实践"的开篇,也是 Goodfellow 等人把读者从 Part I 的"应用数学与机器学习基础"(线性模型、凸优化、概率论、信息论、数值计算、机器学习基础)正式带入"非凸非线性世界"的分水岭。这一章与第 5 章末尾"为什么需要深度学习"的论证(局部模板匹配的局限、平滑先验不够、流形假设)形成精确对接:5.11 节给出的"用 O(k) 样本定义 O(2^k) 区域"的指数增益承诺正是本章 6.4.1 节 Montufar et al. 2014 定理"深度修正网络切出的线性区域数随深度指数增长"的精确数学化。我读到 6.4.1 时第一反应是把它与 5.11.2 节的棋盘格比喻对照——一个是几何直觉(深度通过"折叠"产生指数多区域),一个是组合论证(分布式表示通过组合底层特征产生指数多函数);两者殊途同归,都是分布式表示为何能突破参数共享瓶颈的核心论点。

本章结构上是教科书式"问题驱动 + 算法分治"的典范。6.1 节用 XOR 这个玩具例子引出"为什么需要非线性"和"为什么需要隐藏层",并通过展示图 6.1 中"线性模型在原始 x 空间无法表示 XOR"与"神经网络在学习的 h 空间可用线性模型表示"的对照把"学习表示"这一深度学习的核心动机可视化。6.2 节把训练问题分拆为代价函数(6.2.1)和输出单元(6.2.2),并在 6.2.2.2 / 6.2.2.3 中详细推导出 sigmoid 单元、softmax 单元的负对数似然推导,以及为何最大似然与 cross-entropy 是"唯一不与饱和效应合谋"的损失函数——这一节是我读到时感到"啊原来如此"最多的部分,作者把"log 抵消 exp"这一工程经验提升到了数学上不可避免的必然性:当损失用 \(-\log P(y|x)\) 形式时,sigmoid 的 \(\sigma\)\(\exp\) 在损失中被 \(\log\) 抵消,所以 \(|z|\) 很大但符号错时损失不会饱和(式 6.26 给出 softplus 形式),而是渐近返回 \(|z|\)——这意味着梯度可任意大以纠正错误。6.3 节讨论隐藏单元,作者给出"ReLU 是默认"的实用建议并解释为什么(导数大且一致、第二导数为 0、行为接近线性),同时坦承"无可争议的设计原则仍不存在"——这种诚实比"最优答案"更可贵。

6.4 节"架构设计"的两个小节是我觉得读者最易忽视但极其重要的内容。6.4.1 节的通用逼近定理本身并不令人意外(线性输出 + 一个隐藏层 + "压缩"激活即足以逼近任意 Borel 可测函数),但作者紧接着强调两点工程现实:表示能力 ≠ 可学性("no free lunch")、最坏情况下所需隐藏单元数指数大(\(2^n\) 个二元函数需要 \(O(2^n)\) 个自由度)。这一节埋下了第 7 章正则化、第 8 章优化、第 11 章实践方法论的伏笔。6.4.2 节提到跳跃连接(从层 \(i\) 到层 \(i+2\) 或更高)使梯度更易流动——这是 ResNet 思想的最早教科书式伏笔,本书写于 2016 年,ResNet 已存在(He et al., 2015)但还没出现在主章正文里。

6.5 节反向传播是本章最具技术深度的一节,也是我作为计算生物力学研究者最有共鸣的——FEniCS 的自动微分机制正是反向传播的工程化身。6.5.1 节把神经网络用计算图形式化为节点(变量)和有向边(操作)是个干净抽象,6.5.2 节把链式法则从标量推广到向量和张量很自然(雅可比-向量积),6.5.3 节的"算法 6.1 + 6.2"则是计算图上前向/反向的伪代码骨架。6.5.4 节的全连接 MLP 伪代码是后续所有实现的"参考架构",6.5.5 节把反向传播区分为"符号到数"(Torch/Caffe 风格)和"符号到符号"(Theano/TensorFlow 风格)——后者的关键优势是"对导数再求导"即得高阶导数,我做有限元灵敏度分析时正是依赖这一性质(Hessian-向量积 6.5.10 节)。6.5.6 节给出"通用反向传播"的算法 6.5 + 6.6 是任意计算图上反向传播的形式骨架——核心思想是把"每操作"作为 bprop 的实施者:每操作知道如何反向传播通过自己(矩阵乘法 bprop 规则 \(GB^\top\)\(A^\top G\)),反向传播算法本身不需要知道任何求导规则,只需调用每操作的 bprop 规则。6.5.7 节 MLP 训练例子给出含权重衰减的完整计算图路径追踪,对理解"梯度从代价 \(J\) 回流到参数 \(W^{(1)}, W^{(2)}\) 经过两条路径(cross-entropy 路径 + weight decay 路径)"很有帮助——这条路径的精确数学化正是 PyTorch/TensorFlow 自动微分系统每天执行的数百万次操作。

6.5.9 节"深度学习社区之外的微分"是有趣的视角:反向传播(reverse mode accumulation)只是自动微分的一种特例,对输入少输出多的图更适合用 forward mode;找到最优求导顺序本身是 NP 完全问题(Naumann, 2008)。这一节让我意识到深度学习社区某种程度上"重新发明"了 1970-80 年代控制理论和灵敏度分析中的求导技术,Werbos(1981)把这些技术引入神经网络训练是关键的"翻译"步骤。6.5.10 节给出 Hessian-向量积的 Christianson(1992)公式:\(Hv = \nabla_x ((\nabla_x f(x))^\top v)\),正是工程上对"参数数十亿的神经网络如何避免显式构造 Hessian"这一问题的标准答案——这与 FEniCS 中用 adjoint 方法求灵敏度系数的思想一脉相承。

6.6 节历史笔记最让我意外的两点:(1)链式法则 1676 年由 Leibniz 发明,梯度下降 1847 年由 Cauchy 提出,神经网络用动态规划做反向传播在 1960-70 年代控制理论界就出现(Kelley 1960, Bryson and Ho 1969),Werbos 1981 年才提议用于人工神经网络——意味着神经网络"自己重新发明"的技术在控制理论中已存在 20 年;(2)ReLU 在 Fukushima 1975/1980 的 Cognitron/Neocognitron 中就以"对非线性函数施加修正"的形式存在,1980 年代被 sigmoid 替代的原因是"小网络时 sigmoid 表现更好",2009 年 Jarrett et al. 才把它"重新发现"为深度网络的默认激活函数。这段历史说明很多所谓"现代深度学习突破"实际上是老思想的轮回(当然,承认这一点并不贬低现代工作的价值——把它工程化、规模化、使之成为主流是 2006-2015 这十年的贡献)。

我对本章的疑问主要有两个:(1)6.2.2.3 末尾说 sigmoid 单元是 softmax 在 \(n=2\) 时的特例(\(P(y=1|x) = \text{softmax}(z)_1\) with \(z_1=0\)),那么为什么作者要分开两小节讲?是因为"sigmoid 用于二元分类"和"softmax 用于多元分类"在工程实现上有历史分离的代码路径,还是因为这种分解有理论意义?我目前的理解是后者(softmax 推导"为何用 cross-entropy"的工作机制对二元和多元是相同的,过参数化 vs 限制版 softmax 的学习动态差异也是相通的),但这一联系值得在讲义中更显式地给出。(2)6.4.1 末尾说"使用深度架构编码了一种相信目标函数由多个更简单函数复合的先验",但这种先验如何形式化为 PAC-Bayes 框架或类似理论框架?本书第 5 章给出了机器学习理论基础,本章却把"深度架构的归纳偏置"只放在描述性语言层面,缺少对深度架构泛化能力的理论保证。深度学习理论(第 6.4.1 节末尾的引用 Bengio et al., 2007; Montufar et al., 2014; 等)似乎是经验性的,这是深度学习理论目前的真实状态。

与上下章的衔接(一段话)

第 6 章处于 Part I(应用数学与机器学习基础)和 Part II(深度网络:现代实践)的分界点。它的前半部分(6.1-6.2.2)继续 Part I 建立的"基于梯度的学习 + 概率/统计框架"思路,把代价函数选择、最大似然、交叉熵这些 Part I 概念正式应用到神经网络的输出层;它的后半部分(6.3-6.5)则开启 Part II 的"深度网络"主题——隐藏单元选择(6.3)、架构设计(6.4)、反向传播算法(6.5)。第 5 章末尾 5.11 节给出的"为什么需要深度学习"论证(局部模板匹配失败、平滑先验不够、流形假设)正是本章 6.4.1 节"深度修正网络切出指数多线性区域"的动机来源:5.11.2 棋盘格比喻的数学化版本就是 Montufar et al. 2014 定理。本章末尾 6.6 节历史笔记为 6.1-6.5 节所有技术选择提供时间维度(链式法则 17 世纪、感知机 1940s、反向传播 1986、ReLU 复兴 2009、现代深度学习 2006-),并预告下一章"训练和使用这些模型"——这正是 Part II 第 7 章"正则化"和第 8 章"优化"的入口。