第 6 章 · 神经网络

Speech and Language Processing · Daniel Jurafsky & James H. Martin · 中文翻译稿
覆盖:神经单元 · XOR 与多层网络 · 前馈神经网络 · 嵌入作为输入 · 损失函数与训练 · 计算图与反向传播
第三版草稿 · 2026 年 1 月 6 日版本

6   神经网络

“当单元数量很大时,这类机器可以表现出非常复杂的行为。”
     Alan Turing (1948), “Intelligent Machines”, 第 6 页

神经网络是语言处理的一种基础计算工具,而且历史很早。它们被称为“神经”,是因为其源头可以追溯到 McCulloch–Pitts 神经元(McCulloch and Pitts, 1943):这是一种对生物神经元的简化模型,把神经元看成一种可以用命题逻辑描述的计算元素。但现代语言处理中的神经网络已经不再依赖这些早期的生物学灵感。

相反,现代神经网络是由许多小计算单元构成的网络;每个单元接收一个输入值向量,并产生一个输出值。本章介绍用于分类的神经网络。我们将介绍的架构称为前馈网络(feedforward network),因为计算会从一层单元迭代地流向下一层单元。现代神经网络的使用常称为深度学习(deep learning),因为现代网络通常很深,也就是具有许多层。

神经网络与逻辑回归共享大量数学基础。但神经网络是比逻辑回归更强大的分类器;事实上,一个最小神经网络(严格说,是一个带有单个“隐藏层”的网络)可以被证明能够学习任意函数。

神经网络分类器与逻辑回归还有另一个不同点。使用逻辑回归时,我们通过基于领域知识设计大量丰富的特征模板,把回归分类器应用到许多不同任务。使用神经网络时,更常见的做法是避免大多数人工设计的丰富特征,而是构建以原始词元为输入的神经网络,让模型在学习分类的过程中自行诱导特征。第 5 章已经在嵌入中看到这种表示学习的例子;等开始研究深层 transformer 网络时,我们还会看到更多例子。很深的网络特别擅长表示学习。因此,对于有足够数据可自动学习特征的任务,深度神经网络是合适的工具。

本章将介绍作为分类器的前馈网络:先使用人工构造特征,再使用第 5 章学过的嵌入。在后续章节中,我们会介绍许多其他神经模型,最重要的是 transformer 和注意力(第 8 章),也包括循环神经网络(第 13 章)和卷积神经网络(第 15 章)。下一章还会介绍神经大型语言模型这一范式。

6.1   单元

神经网络的基本构件是单个计算单元。一个单元接收一组实值数字作为输入,对它们进行某种计算,并产生一个输出。

从核心上看,神经单元对输入进行加权求和,并在这个和中加入一个额外项,称为偏置项(bias term)。给定一组输入 $x_1,\ldots,x_n$,一个单元有一组对应的权重 $w_1,\ldots,w_n$ 和一个偏置 $b$,因此加权和 $z$ 可以表示为:

📐 公式 6.1
$$ z = b + \sum_i w_i x_i \tag{6.1} $$

通常,用向量记号表达这个加权和会更方便;回忆线性代数,向量本质上只是一个数字列表或数组。因此,我们会用权重向量 $\boldsymbol{w}$、标量偏置 $b$ 和输入向量 $\boldsymbol{x}$ 来谈论 $z$,并用方便的点积替代求和:

📐 公式 6.2
$$ z = \boldsymbol{w}\cdot\boldsymbol{x} + b \tag{6.2} $$

按照式 (6.2) 的定义,$z$ 只是一个实值数字。

最后,神经单元不会直接把 $z$ 这个关于 $\boldsymbol{x}$ 的线性函数作为输出,而是把非线性函数 $f$ 应用于 $z$。我们把这个函数的输出称为该单元的激活值(activation value),记作 $a$。由于此处只建模单个单元,节点的激活实际上就是网络的最终输出,通常记作 $y$。因此 $y$ 的值定义为:

$$ y = a = f(z) $$

下面会讨论三种常用非线性函数 $f$(sigmoid、tanh、修正线性单元 ReLU),但从教学上说,先从第 4 章见过的 sigmoid 函数开始最方便:

📐 公式 6.3
$$ y = \sigma(z) = \frac{1}{1+e^{-z}} \tag{6.3} $$

sigmoid(见图 6.1)有一些优点:它把输出映射到 $(0,1)$ 区间,这有助于把离群值压向 0 或 1。它还是可微的,而正如第 4.15 节所见,这对学习会很有用。

图 6.1
图 6.1 sigmoid 函数接收一个实值,并把它映射到 $(0,1)$ 区间。它在 0 附近几乎是线性的,但离群值会被压向 0 或 1。

把式 (6.2) 代入式 (6.3),即可得到神经单元的输出:

📐 公式 6.4
$$ y = \sigma(\boldsymbol{w}\cdot\boldsymbol{x} + b) = \frac{1}{1+\exp(-(\boldsymbol{w}\cdot\boldsymbol{x}+b))} \tag{6.4} $$

图 6.2 给出了基本神经单元的最终示意图。在这个例子中,单元接收 3 个输入值 $x_1,x_2,x_3$,计算加权和:分别用权重 $w_1,w_2,w_3$ 乘以各输入值,再加入偏置项 $b$,最后把所得和传入 sigmoid 函数,得到一个介于 0 和 1 之间的数。

图 6.2
图 6.2 一个神经单元,接收 3 个输入 $x_1,x_2,x_3$(以及偏置 $b$;图中把它表示为固定为 $+1$ 的输入上的权重),并产生输出 $y$。图中还标出了一些方便的中间变量:求和输出 $z$,以及 sigmoid 的输出 $a$。在这个例子中,单元输出 $y$ 与 $a$ 相同;但在更深的网络中,我们会保留 $y$ 表示整个网络的最终输出,而用 $a$ 表示单个节点的激活。

我们通过一个例子建立直觉。假设某个单元有如下权重向量和偏置:

$$ \boldsymbol{w}=[0.2,0.3,0.9]\qquad b=0.5 $$

如果输入向量为:

$$ \boldsymbol{x}=[0.5,0.6,0.1] $$

则输出 $y$ 为:

$$ y = \sigma(\boldsymbol{w}\cdot\boldsymbol{x}+b) = \frac{1}{1+e^{-(\boldsymbol{w}\cdot\boldsymbol{x}+b)}} = \frac{1}{1+e^{-(.5\cdot.2 + .6\cdot.3 + .1\cdot.9 + .5)}} = \frac{1}{1+e^{-0.87}} = .70 $$

实践中,sigmoid 并不常用作激活函数。一个非常相似但几乎总是更好的函数是图 6.3a 所示的 tanh 函数;tanh 是 sigmoid 的一种变体,取值范围从 $-1$ 到 $+1$:

📐 公式 6.5
$$ y = \tanh(z) = \frac{e^z - e^{-z}}{e^z + e^{-z}} \tag{6.5} $$

最简单、也许也是最常用的激活函数是修正线性单元(rectified linear unit),也称为 ReLU,如图 6.3b 所示。当 $z$ 为正时,它就是 $z$ 本身;否则为 0:

📐 公式 6.6
$$ y = \mathrm{ReLU}(z) = \max(z,0) \tag{6.6} $$
图 6.3
图 6.3 tanh 和 ReLU 激活函数。

这些激活函数具有不同性质,因而适用于不同语言应用或网络架构。例如,tanh 函数具有平滑可微、并把离群值映射向均值的良好性质。另一方面,修正线性函数的优点来自它非常接近线性。在 sigmoid 或 tanh 函数中,$z$ 的很大取值会使 $y$ 进入饱和状态,也就是极其接近 1,并且导数极其接近 0。零导数会给学习带来问题,因为正如第 6.6 节将看到的,我们会通过把误差信号向后传播来训练网络,并在网络的每一层乘以梯度(偏导数);几乎为 0 的梯度会使误差信号越来越小,直到小到无法用于训练,这就是梯度消失问题。ReLU 没有这个问题,因为当 $z$ 很大时,ReLU 的导数是 1,而不是非常接近 0。

6.2   XOR 问题

在神经网络历史的早期,人们就意识到:神经网络的力量,和启发它们的真实神经元一样,来自把这些单元组合成更大的网络。

Minsky 和 Papert(1969)证明单个神经单元无法计算某些非常简单的输入函数,这是说明多层网络必要性的一个极巧妙例子。考虑计算两个输入上的基本逻辑函数,如 AND、OR 和 XOR。作为提醒,下面给出这些函数的真值表:

AND
$x_1$$x_2$$y$
000
010
100
111
OR
$x_1$$x_2$$y$
000
011
101
111
XOR
$x_1$$x_2$$y$
000
011
101
110

这个例子最早是针对感知机(perceptron)展示的。感知机是一种非常简单的神经单元,它有二值输出,并把一种非常简单的阶跃函数作为非线性激活函数。感知机输出 $y$ 为 0 或 1,计算方式如下(使用与式 (6.2) 相同的权重 $\boldsymbol{w}$、输入 $\boldsymbol{x}$ 和偏置 $b$):

📐 公式 6.7
$$ y = \begin{cases} 0, & \text{if } \boldsymbol{w}\cdot\boldsymbol{x} + b \leq 0\\ 1, & \text{if } \boldsymbol{w}\cdot\boldsymbol{x} + b > 0 \end{cases} \tag{6.7} $$

构建一个能计算二值输入的逻辑 AND 和 OR 函数的感知机很容易;图 6.4 给出了所需权重。

图 6.4
图 6.4 用于计算逻辑函数的感知机权重 $\boldsymbol{w}$ 和偏置 $b$。输入显示为 $x_1$ 和 $x_2$,偏置显示为一个值为 $+1$ 的特殊节点,并乘以偏置权重 $b$。(a) 逻辑 AND,权重 $w_1=1$、$w_2=1$,偏置权重 $b=-1$。(b) 逻辑 OR,权重 $w_1=1$、$w_2=1$,偏置权重 $b=0$。这些权重/偏置只是能实现这些函数的无穷多组可能权重和偏置之一。

然而,事实证明不可能构建一个感知机来计算逻辑 XOR!值得花一点时间亲自尝试一下。

这个重要结果背后的直觉依赖于这样一个事实:感知机是一个线性分类器。对于二维输入 $x_1$ 和 $x_2$,感知机方程 $w_1 x_1 + w_2 x_2 + b = 0$ 是一条直线的方程。(把它写成标准线性形式即可看出:$x_2 = (-w_1/w_2) x_1 + (-b/w_2)$。)这条线在二维空间中充当决策边界:线一侧的所有输入被分配输出 0,另一侧的所有输入点被分配输出 1。如果输入多于 2 个,决策边界就从直线变成超平面,但思想相同,都是把空间分成两个类别。

图 6.5 展示了可能的逻辑输入(00、01、10、11),以及 AND 和 OR 分类器的一组可能参数所画出的直线。注意,对于 XOR,没有办法画出一条直线,把正例(01 和 10)与负例(00 和 11)分开。我们说 XOR 不是线性可分函数。当然,我们可以画一条曲线边界或其他函数边界,但不能只用一条直线。

图 6.5
图 6.5 AND、OR 和 XOR 函数,横轴为输入 $x_1$,纵轴为输入 $x_2$。实心圆表示感知机输出为 1,空心圆表示感知机输出为 0。不存在一条直线能够正确分开 XOR 的两个类别。图样式改自 Russell and Norvig (2002)。

6.2.1   解决方案:神经网络

虽然 XOR 函数不能由单个感知机计算,但可以由分层的感知机单元网络计算。这里不使用简单感知机网络来展示,而是按照 Goodfellow et al. (2016),用两层基于 ReLU 的单元来计算 XOR。图 6.6 展示了输入被两层神经单元处理的情形。中间层(称为 $h$)有两个单元,输出层(称为 $y$)有一个单元。图中给出一组权重和偏置,使网络能够正确计算 XOR 函数。

我们来看输入 $\boldsymbol{x}=[0,0]$ 时发生了什么。把每个输入值乘以相应权重、求和、再加偏置 $b$,得到向量 $[0,-1]$;然后应用修正线性变换,得到 $h$ 层输出 $[0,0]$。接着再一次乘以权重、求和、加偏置(本例中为 0),得到值 0。读者可以自行计算其余 3 个可能输入对,验证所得 $y$ 值对于输入 $[0,1]$ 和 $[1,0]$ 为 1,对于 $[0,0]$ 和 $[1,1]$ 为 0。

图 6.6
图 6.6 Goodfellow et al. (2016) 的 XOR 解法。图中有三个位于两层中的 ReLU 单元;我们把它们称为 $h_1$、$h_2$($h$ 表示“隐藏层”)和 $y_1$。同前,箭头上的数字表示每个单元的权重 $\boldsymbol{w}$,偏置 $b$ 表示为固定为 $+1$ 的单元上的权重,偏置权重/单元用灰色表示。

观察中间结果,也就是两个隐藏节点 $h_1$ 和 $h_2$ 的输出,同样很有启发。上一段已经说明,输入 $\boldsymbol{x}=[0,0]$ 时,$h$ 向量是 $[0,0]$。图 6.7b 展示了全部 4 个输入的 $h$ 层取值。注意,输入点 $\boldsymbol{x}=[0,1]$ 和 $\boldsymbol{x}=[1,0]$(这两个是 XOR 输出为 1 的情况)的隐藏表示被合并到同一个点 $\boldsymbol{h}=[1,0]$。这种合并使得 XOR 的正例和负例容易线性分开。换言之,我们可以把网络的隐藏层看作在形成输入的某种表示。

在这个例子中,我们只是指定了图 6.6 中的权重。但在真实例子中,神经网络的权重会用第 6.6 节将介绍的误差反向传播算法自动学习。这意味着隐藏层会学习形成有用的表示。神经网络能够自动学习输入的有用表示,这一直觉是它们的关键优势之一,后续章节将反复回到这一点。

图 6.7
图 6.7 隐藏层为输入形成新的表示。(b) 展示了隐藏层表示 $\boldsymbol{h}$,并与 (a) 中的原始输入表示 $\boldsymbol{x}$ 对比。注意,输入点 $[0,1]$ 已经与输入点 $[1,0]$ 折叠到一起,使得 XOR 的正例和负例可以线性分开。改自 Goodfellow et al. (2016)。

6.3   前馈神经网络

现在我们更形式化地介绍最简单的一类神经网络:前馈网络。前馈网络是一个多层网络,其中单元之间没有环;每一层单元的输出传递给下一层更高层的单元,没有输出传回较低层。(第 13 章会介绍带环的网络,称为循环神经网络。)

由于历史原因,多层网络,尤其是前馈网络,有时称为多层感知机(multi-layer perceptrons, MLP)。从技术上说这是个误称,因为现代多层网络中的单元并不是感知机(感知机的激活函数是简单阶跃函数,而现代网络由带有许多种非线性函数的单元组成,如 ReLU 和 sigmoid),但这个名称后来沿用了下来。

简单前馈网络有三类节点:输入单元、隐藏单元和输出单元。

图 6.8 给出了一幅示意图。输入层 $\boldsymbol{x}$ 是由简单标量值组成的向量,就像图 6.2 中看到的那样。

神经网络的核心是由隐藏单元 $h_i$ 构成的隐藏层 $\boldsymbol{h}$;每个隐藏单元都是第 6.1 节所述的神经单元,先对输入进行加权求和,再应用非线性函数。在标准架构中,每一层都是全连接的,意思是每一层中的每个单元都以此前一层所有单元的输出为输入,相邻两层中每一对单元之间都有连接。因此,每个隐藏单元都会对所有输入单元求和。

回忆单个隐藏单元的参数是一组权重向量和一个偏置。我们把每个单元 $i$ 的权重向量和偏置组合起来,用一个权重矩阵 $W$ 和一个偏置向量 $\boldsymbol{b}$ 表示整个隐藏层的参数(见图 6.8)。权重矩阵 $W$ 的每个元素 $W_{ji}$ 表示从第 $i$ 个输入单元 $x_i$ 到第 $j$ 个隐藏单元 $h_j$ 的连接权重。

用单个矩阵 $W$ 表示整层权重的优势是:前馈网络中隐藏层的计算现在可以用简单矩阵运算非常高效地完成。事实上,计算只有三步:把权重矩阵乘以输入向量 $\boldsymbol{x}$,加上偏置向量 $\boldsymbol{b}$,并应用激活函数 $g$(例如上面定义的 sigmoid、tanh 或 ReLU)。

图 6.8
图 6.8 一个简单的 2 层前馈网络,包含一个隐藏层、一个输出层和一个输入层(在计数层数时,通常不把输入层算入)。

因此,隐藏层输出向量 $\boldsymbol{h}$ 如下(本例中使用 sigmoid 函数 $\sigma$ 作为激活函数):

📐 公式 6.8
$$ \boldsymbol{h} = \sigma(W\boldsymbol{x} + \boldsymbol{b}) \tag{6.8} $$

注意,这里我们把 $\sigma$ 函数应用于向量,而在式 (6.3) 中它应用于标量。因此,我们允许 $\sigma(\cdot)$,以及任何激活函数 $g(\cdot)$,逐元素地应用于一个向量;也就是说 $g([z_1,z_2,z_3])=[g(z_1),g(z_2),g(z_3)]$。

下面引入一些常量来表示这些向量和矩阵的维度。我们把输入层称为网络的第 0 层,并用 $n_0$ 表示输入数量,因此 $\boldsymbol{x}$ 是维度为 $n_0$ 的实数向量,更形式化地说,$\boldsymbol{x}\in\mathbb{R}^{n_0}$,是维度为 $[n_0\times 1]$ 的列向量。把隐藏层称为第 1 层,输出层称为第 2 层。隐藏层的维度为 $n_1$,因此 $\boldsymbol{h}\in\mathbb{R}^{n_1}$,同时 $\boldsymbol{b}\in\mathbb{R}^{n_1}$(因为每个隐藏单元可以有不同偏置值)。权重矩阵 $W$ 的维度为 $W\in\mathbb{R}^{n_1\times n_0}$,即 $[n_1\times n_0]$。

花一点时间确认:式 (6.8) 中的矩阵乘法会把每个 $h_j$ 计算为

$$ \sigma\!\left(\sum_{i=1}^{n_0} W_{ji} x_i + b_j\right). $$

正如第 6.2 节所见,得到的值 $\boldsymbol{h}$(既表示 hidden,也可理解为 hypothesis)形成了输入的一种表示。输出层的作用是接收这个新表示 $\boldsymbol{h}$ 并计算最终输出。这个输出可以是一个实值数,但在许多情况下,网络的目标是做某种分类决策,因此我们将重点讨论分类情形。

如果做二分类任务,如情感分类,我们可能有一个输出节点,它的标量值 $y$ 是正面情感相对于负面情感的概率。如果做多项分类,例如分配词性标注,我们可能为每个可能词性设置一个输出节点,其输出值是该词性的概率,并且所有输出节点的值必须和为 1。因此,输出层是一个向量 $\boldsymbol{y}$,给出输出节点上的概率分布。

下面看看它是如何发生的。和隐藏层一样,输出层也有一个权重矩阵(称为 $U$),但有些模型在输出层不包含偏置向量 $\boldsymbol{b}$,所以本例中为简化起见去掉输出层偏置向量。权重矩阵乘以其输入向量 $\boldsymbol{h}$,产生中间输出 $\boldsymbol{z}$:

$$ \boldsymbol{z} = U\boldsymbol{h} $$

有 $n_2$ 个输出节点,因此 $\boldsymbol{z}\in\mathbb{R}^{n_2}$,权重矩阵 $U$ 的维度为 $U\in\mathbb{R}^{n_2\times n_1}$,元素 $U_{ij}$ 是隐藏层第 $j$ 个单元到输出层第 $i$ 个单元的权重。

然而,$\boldsymbol{z}$ 不能作为分类器输出,因为它是一个实值向量,而分类所需的是一个概率向量。我们需要一个方便的函数来归一化实值向量,也就是把它转换成编码概率分布的向量(所有数位于 0 和 1 之间,且总和为 1):这就是第 4 章第 79 页见过的 softmax 函数。更一般地,对于任何维度为 $d$ 的向量 $\boldsymbol{z}$,softmax 定义为:

📐 公式 6.9
$$ \mathrm{softmax}(z_i) = \frac{\exp(z_i)}{\sum_{j=1}^{d}\exp(z_j)} \qquad 1\leq i\leq d \tag{6.9} $$

例如,给定向量

📐 公式 6.10
$$ \boldsymbol{z}=[0.6,\ 1.1,\ -1.5,\ 1.2,\ 3.2,\ -1.1], \tag{6.10} $$

softmax 会把它归一化为一个概率分布(这里四舍五入显示):

📐 公式 6.11
$$ \mathrm{softmax}(\boldsymbol{z}) = [0.055,\ 0.090,\ 0.0067,\ 0.10,\ 0.74,\ 0.010] \tag{6.11} $$

你也许还记得,在第 4 章多项逻辑回归版本中,我们曾用 softmax 从实值数字向量(由权重乘以特征并求和而得)创建概率分布。

这意味着我们可以把带有一个隐藏层的神经网络分类器理解为:先构建向量 $\boldsymbol{h}$,它是输入的隐藏层表示;然后在网络于 $\boldsymbol{h}$ 中发展出来的特征上运行标准多项逻辑回归。相比之下,第 4 章中的特征主要是通过特征模板人工设计的。因此,神经网络类似于多项逻辑回归,但有三点不同:(a) 它有很多层,因为深度神经网络像是一层接一层的逻辑回归分类器;(b) 这些中间层可以有许多可能的激活函数(tanh、ReLU、sigmoid),而不只是 sigmoid(尽管为了方便,我们会继续用 $\sigma$ 表示任意激活函数);(c) 特征不是由特征模板形成,而是由网络的前面各层自行诱导出特征表示。

下面是带有单个隐藏层的前馈网络的最终方程。它接收输入向量 $\boldsymbol{x}$,输出概率分布 $\hat{\boldsymbol{y}}$,并由权重矩阵 $W$、$U$ 和偏置向量 $\boldsymbol{b}$ 参数化:

📐 公式 6.12
$$ \begin{aligned} \boldsymbol{h} &= \sigma(W\boldsymbol{x} + \boldsymbol{b})\\ \boldsymbol{z} &= U\boldsymbol{h}\\ \hat{\boldsymbol{y}} &= \mathrm{softmax}(\boldsymbol{z}) \end{aligned} \tag{6.12} $$

为了记住所有变量的形状:$\boldsymbol{x}\in\mathbb{R}^{n_0}$,$\boldsymbol{h}\in\mathbb{R}^{n_1}$,$\boldsymbol{b}\in\mathbb{R}^{n_1}$,$W\in\mathbb{R}^{n_1\times n_0}$,$U\in\mathbb{R}^{n_2\times n_1}$,输出向量 $\boldsymbol{y}\in\mathbb{R}^{n_2}$。我们把这个网络称为 2 层网络(按传统,在编号层数时不计输入层,但计输出层)。按照这个术语,逻辑回归是 1 层网络。

6.3.1   前馈网络的更多细节

现在设置一些记号,以便更容易讨论深度大于 2 的更深网络。我们用方括号上标表示层号,从输入层 0 开始。因此,$W^{[1]}$ 表示第一个隐藏层的权重矩阵,$\boldsymbol{b}^{[1]}$ 表示第一个隐藏层的偏置向量。$n_j$ 表示第 $j$ 层的单元数。用 $g(\cdot)$ 表示激活函数;中间层通常是 ReLU 或 tanh,输出层通常是 softmax。用 $\boldsymbol{a}^{[i]}$ 表示第 $i$ 层的输出,用 $\boldsymbol{z}^{[i]}$ 表示上一层输出、权重和偏置的组合 $W^{[i]}\boldsymbol{a}^{[i-1]}+\boldsymbol{b}^{[i]}$。第 0 层是输入层,所以我们更一般地把输入 $\boldsymbol{x}$ 记作 $\boldsymbol{a}^{[0]}$。

于是,式 (6.12) 中的 2 层网络可以重新表示为:

📐 公式 6.13
$$ \begin{aligned} \boldsymbol{z}^{[1]} &= W^{[1]}\boldsymbol{a}^{[0]} + \boldsymbol{b}^{[1]}\\ \boldsymbol{a}^{[1]} &= g^{[1]}(\boldsymbol{z}^{[1]})\\ \boldsymbol{z}^{[2]} &= W^{[2]}\boldsymbol{a}^{[1]} + \boldsymbol{b}^{[2]}\\ \boldsymbol{a}^{[2]} &= g^{[2]}(\boldsymbol{z}^{[2]})\\ \hat{\boldsymbol{y}} &= \boldsymbol{a}^{[2]} \end{aligned} \tag{6.13} $$

注意,在这种记号下,每一层完成的计算方程都是相同的。因此,给定输入向量 $\boldsymbol{a}^{[0]}$,在 $n$ 层前馈网络中计算前向步骤的算法很简单:

for i in 1,...,n
    z[i] = W[i] a[i-1] + b[i]
    a[i] = g[i](z[i])
yhat = a[n]

通常,给最终 softmax 之前的最后一组激活命名会很有用。因此,不管网络有多少层,我们通常把最终向量 $\boldsymbol{z}^{[n]}$ 中未归一化的值,也就是最终 softmax 之前的分数向量,称为 logits(见式 4.7)。

非线性激活函数的必要性

我们在神经网络每一层使用非线性激活函数的原因之一是:如果不使用,所得网络就与单层网络完全等价。原因如下。想象这样一个完全由线性层构成的网络的前两层:

$$ \boldsymbol{z}^{[1]} = W^{[1]}\boldsymbol{x} + \boldsymbol{b}^{[1]} $$ $$ \boldsymbol{z}^{[2]} = W^{[2]}\boldsymbol{z}^{[1]} + \boldsymbol{b}^{[2]} $$

可以把网络正在计算的函数改写为:

📐 公式 6.14
$$ \begin{aligned} \boldsymbol{z}^{[2]} &= W^{[2]}\boldsymbol{z}^{[1]} + \boldsymbol{b}^{[2]}\\ &= W^{[2]}(W^{[1]}\boldsymbol{x} + \boldsymbol{b}^{[1]}) + \boldsymbol{b}^{[2]}\\ &= W^{[2]}W^{[1]}\boldsymbol{x} + W^{[2]}\boldsymbol{b}^{[1]} + \boldsymbol{b}^{[2]}\\ &= W'\boldsymbol{x} + \boldsymbol{b}' \end{aligned} \tag{6.14} $$

这可以推广到任意层数。因此,没有非线性激活函数,多层网络就只是带有另一组权重的单层网络的记号变体,我们会失去多层网络的全部表示能力。

替换偏置单元

在描述网络时,我们有时会使用一种略微简化的记号,它表示完全相同的函数,但不显式提及偏置节点 $b$。相反,我们在每一层加入一个哑节点 $a_0$,其值总是 1。因此第 0 层(输入层)会有哑节点 $a_0^{[0]}=1$,第 1 层会有 $a_0^{[1]}=1$,依此类推。这个哑节点仍有一个关联权重,而该权重就表示偏置值 $b$。例如,原来有:

📐 公式 6.15
$$ \boldsymbol{h} = \sigma(W\boldsymbol{x} + \boldsymbol{b}) \tag{6.15} $$

我们会写成:

📐 公式 6.16
$$ \boldsymbol{h} = \sigma(W\boldsymbol{x}) \tag{6.16} $$

但现在,向量 $\boldsymbol{x}$ 不再只有 $n_0$ 个值 $\boldsymbol{x}=x_1,\ldots,x_{n_0}$,而是有 $n_0+1$ 个值,加入新的第 0 个哑值 $x_0=1$:$\boldsymbol{x}=x_0,\ldots,x_{n_0}$。原先每个 $h_j$ 的计算为:

📐 公式 6.17
$$ h_j = \sigma\!\left(\sum_{i=1}^{n_0} W_{ji} x_i + b_j\right), \tag{6.17} $$

现在改为:

📐 公式 6.18
$$ h_j = \sigma\!\left(\sum_{i=0}^{n_0} W_{ji} x_i\right), \tag{6.18} $$

其中 $W_{j0}$ 取代了原来的 $b_j$。图 6.9 给出了可视化。

图 6.9
图 6.9 用 $x_0$ 替换偏置节点(a 中所示),如 (b) 所示。

在第 6.6 节讲学习算法时,我们会继续把偏置显示为 $\boldsymbol{b}$;但本书后续大多数图和一些方程会使用这种不显式写出偏置项的简化记号。

6.4   用于 NLP 分类的前馈网络

现在看看如何把前馈网络应用到 NLP 分类任务。实践中,简单前馈网络并不是我们进行文本分类的主要方法;真实应用中会使用更复杂的架构,比如第 9 章的 BERT transformer。尽管如此,观察一个前馈网络文本分类器可以帮助我们引入贯穿本书后续章节的重要思想,包括嵌入矩阵、表示池化和表示学习。

但在引入这些思想之前,先从一个分类器开始,它只对第 4 章的情感分类器做最小修改。和那些分类器一样,我们接收人工构造特征,把它们送入分类器,并产生类别概率。唯一不同的是,分类器不再是逻辑回归,而是神经网络。

6.4.1   使用人工构造特征的神经网络分类器

先构建一个简单的 2 层情感分类器:取第 4 章的逻辑回归分类器(它对应于 1 层网络),只加一个隐藏层。输入元素 $x_i$ 可以是图 4.2 中的标量特征,例如 $x_1=\mathrm{count}(\text{words}\in\mathrm{doc})$,$x_2=\mathrm{count}(\text{positive lexicon words}\in\mathrm{doc})$,$x_3=1$ if “no” $\in\mathrm{doc}$,依此类推,共 $d$ 个特征。输出层 $\hat{\boldsymbol{y}}$ 可以有两个节点(分别对应正面和负面),也可以有 3 个节点(正面、负面、中性),此时 $\hat{y}_1$ 是正面情感的估计概率,$\hat{y}_2$ 是负面概率,$\hat{y}_3$ 是中性概率。所得方程正是上面看到的 2 层网络方程(同往常一样,我们继续用 $\sigma$ 表示任意非线性函数,无论是 sigmoid、ReLU 还是其他函数):

📐 公式 6.19
$$ \begin{aligned} \boldsymbol{x} &= [x_1,x_2,\ldots,x_d]\quad(\text{每个 }x_i\text{ 都是人工设计特征})\\ \boldsymbol{h} &= \sigma(W\boldsymbol{x} + \boldsymbol{b})\\ \boldsymbol{z} &= U\boldsymbol{h}\\ \hat{\boldsymbol{y}} &= \mathrm{softmax}(\boldsymbol{z}) \end{aligned} \tag{6.19} $$

图 6.10 勾勒了这个架构。如前所述,给逻辑回归分类器加入这个隐藏层,使网络能够表示特征之间的非线性交互。仅这一点就可能得到更好的情感分类器。

图 6.10
图 6.10 使用输入文本的传统人工构造特征进行情感分析的前馈网络。

6.4.2   向量化以并行化推理

式 (6.19) 展示了如何分类单个样例 $\boldsymbol{x}$;但实践中,我们希望高效分类整个测试集中的 $m$ 个样例。做法是像逻辑回归中那样向量化整个过程:不再用循环逐个处理样例,而是用矩阵乘法一次性完成整个测试集的计算。首先,把每个输入 $\boldsymbol{x}$ 的输入特征向量打包成一个输入矩阵 $X$,其中第 $i$ 行是由输入样例 $\boldsymbol{x}^{(i)}$ 的特征组成的行向量(即向量 $\boldsymbol{x}^{(i)}$)。如果输入特征向量维度是 $d$,则 $X$ 的形状为 $[m\times d]$。

因为现在每个输入被建模为行向量而不是列向量,所以还需要稍微修改式 (6.19)。$X$ 的形状是 $[m\times d]$,$W$ 的形状是 $[d_h\times d]$,因此我们要调整 $X$ 与 $W$ 的乘法顺序,并转置 $W$,使它们能正确相乘并得到形状为 $[m\times d_h]$ 的矩阵 $H$。[注 1]

式 (6.19) 中形状为 $[1\times d_h]$ 的偏置向量 $\boldsymbol{b}$ 现在必须复制成形状为 $[m\times d_h]$ 的矩阵。下一步同样需要调整顺序并转置 $U$。最后,输出矩阵 $\hat{Y}$ 的形状为 $[m\times 3]$(更一般地说为 $[m\times d_o]$,其中 $d_o$ 是输出类别数),输出矩阵 $\hat{Y}$ 的第 $i$ 行就是输出向量 $\hat{\boldsymbol{y}}^{(i)}$。下面是计算整个测试集输出类别分布的最终方程:

📐 公式 6.20
$$ \begin{aligned} H &= \sigma(XW^{\top} + \boldsymbol{b})\\ Z &= HU^{\top}\\ \hat{Y} &= \mathrm{softmax}(Z) \end{aligned} \tag{6.20} $$

本书中,我们有时会看到 $WX+\boldsymbol{b}$ 这样的顺序,有时会看到 $XW+\boldsymbol{b}$。因此,在任何给定方程中,都必须非常清楚参与运算的权重矩阵形状。

6.5   作为神经网络分类器输入的嵌入

人工构造特征是设计分类器的传统方式,但 NLP 中神经网络的大多数应用不会使用人工工程化特征作为输入。相反,我们利用深度学习从数据中学习特征的能力,把词元表示为嵌入。本节中,每个词元用第 5 章学过如何计算的静态 word2vec 或 GloVe 嵌入来表示。所谓静态嵌入,是指每个词元由一个固定向量表示:我们训练一次,然后把它放入一个大词典中。需要引用该词元时,就从词典中取出它的嵌入。

然而,当我们把神经模型应用于语言建模任务时(第 8 章会看到),情况会更复杂;我们会使用一种更强大的嵌入,称为上下文嵌入。同一个词每次出现在不同上下文中,上下文嵌入都不同。此外,我们还会让网络在词预测任务中学习这些嵌入。

现在回到上面的文本分类领域,但使用静态嵌入作为特征,而不是人工设计特征。我们关注推理阶段:此时我们已经为所有输入词元学到了嵌入。嵌入是一个维度为 $d$ 的向量,表示输入词元。存储这些静态嵌入的词典就是嵌入矩阵 $E$。嵌入矩阵的每一行把词表 $V$ 中的一个词元表示为维度为 $d$ 的行向量。由于 $E$ 为词表中每个 $|V|$ 词元都有一行,所以 $E$ 的形状为 $[|V|\times d]$。只要我们用嵌入作为神经 NLP 系统的输入,包括接下来几章将介绍的基于 transformer 的大型语言模型,嵌入矩阵 $E$ 都会发挥作用。

给定一个输入词元串,例如 dessert was great,我们首先把词元转换成词表索引(这些索引是在最初用 BPE 或 SentencePiece 对输入分词时创建的)。因此,dessert was great 的表示可能是 $\boldsymbol{w}=[3,9824,226]$。接着,我们通过索引从 $E$ 中选择对应的行(第 3 行、第 9824 行、第 226 行)。

另一种理解从嵌入矩阵中选择词元嵌入的方式,是把输入词元表示为形状 $[1\times |V|]$ 的独热向量(one-hot vector),也就是词表中每个词有一个维度。回忆独热向量:所有元素都是 0,只有一个元素为 1;这个元素对应词在词表中的索引。因此,如果单词 “dessert” 在词表中的索引为 3,那么 $x_3=1$,且对所有 $i\neq 3$,$x_i=0$,如下所示:

$$ [0\ \ 0\ \ 1\ \ 0\ \ 0\ \ 0\ \ 0\ \ \ldots\ \ 0\ \ 0\ \ 0\ \ 0] $$ $$ 1\quad 2\quad 3\quad 4\quad 5\quad 6\quad 7\quad \ldots\quad\ldots\quad |V| $$

乘以一个只有一个非零元素 $x_i=1$ 的独热向量,只是选出词 $i$ 的相关行向量,从而得到词 $i$ 的嵌入,如图 6.11 所示。

图 6.11
图 6.11 通过把嵌入矩阵 $E$ 与索引 3 处为 1 的独热向量相乘,选择单词 $V_3$ 的嵌入向量。

可以把这个想法扩展到整个输入词元序列:用一个独热向量矩阵表示,每个输入位置有一个独热向量,如图 6.12 所示。

图 6.12
图 6.12 通过把对应于 $W$ 的独热矩阵乘以嵌入矩阵 $E$,为输入词元 id 序列 $W$ 选择嵌入矩阵。

现在,我们需要把这个由 $N$ 个 $[1\times d]$ 嵌入组成的输入(表示 $N$ 个词元的窗口)分类为单个类别(如正面或负面)。

有两种常见方式把嵌入传给分类器:拼接和池化。第一种方式是把形状为 $[N\times d]$ 的输入重新整形,把所有输入向量拼接成一个形状为 $[1\times dN]$ 的很长向量。然后把这个输入传给分类器,让它做决策。这提供了大量信息,代价是网络会相当大。第二种方式是把 $N$ 个嵌入池化(pool)成单个嵌入,然后把这个池化后的单个嵌入传给分类器。池化得到的信息少于原始所有嵌入包含的信息,但优点是小而高效;对于那些不太关心原始词序的任务尤其有用。下面各举一个例子:情感任务中的池化,以及语言建模任务中的拼接。

为情感分类池化输入嵌入

先看池化如何用于情感分类任务。池化的直觉是:对情感而言,输入词的确切位置(像 great 这样的词是第一个词还是第二个词?)不如词本身的身份重要。

池化函数是把一组嵌入转换成单个嵌入的方法。例如,对于包含 $N$ 个输入词/词元 $w_1,\ldots,w_N$ 的文本,我们希望把 $N$ 个行嵌入 $e(w_1),\ldots,e(w_N)$(每个维度为 $d$)转换成同样维度为 $d$ 的单个嵌入。

池化有多种方式。最简单的是均值池化:把嵌入求和,再除以 $N$ 得到均值:

📐 公式 6.21
$$ \boldsymbol{x}_{\mathrm{mean}} = \frac{1}{N}\sum_{i=1}^{N} e(w_i) \tag{6.21} $$

假设使用均值池化,该分类器的方程如下:

📐 公式 6.22
$$ \begin{aligned} \boldsymbol{x} &= \mathrm{mean}(e(w_1), e(w_2), \ldots, e(w_n))\\ \boldsymbol{h} &= \sigma(\boldsymbol{x}W + \boldsymbol{b})\\ \boldsymbol{z} &= \boldsymbol{h}U\\ \hat{\boldsymbol{y}} &= \mathrm{softmax}(\boldsymbol{z}) \end{aligned} \tag{6.22} $$

图 6.13 勾勒了该架构,并给出了所有相关矩阵的形状。

图 6.13
图 6.13 使用输入词的池化嵌入进行情感分析的前馈网络。每个时间步,网络为每个上下文词计算一个 $d$ 维嵌入(通过把独热向量乘以嵌入矩阵 $E$),并池化得到的 $N$ 个嵌入,得到一个表示上下文窗口的单个嵌入,即层 $e$。

还有很多其他池化选项,例如最大池化(max-pooling)。这种情况下,对每个维度,在所有输入上取逐元素最大值。一组 $N$ 个向量的逐元素最大值,是一个新向量,其第 $k$ 个元素是所有 $N$ 个向量第 $k$ 个元素中的最大值。

为语言建模拼接输入嵌入

在情感分析中,我们看到如何给定 $N$ 个输入词元窗口,先把这些词元嵌入池化成单个嵌入向量,再生成一个输出向量,其中包含三个类别(正面、负面、中性)的概率。

现在考虑语言建模:根据先前词预测即将出现的词。在这个任务中,我们仍给定同样的 $N$ 个输入词元窗口,但任务变为预测该窗口之后应该接着出现的下一个词元。下面勾勒一个简单前馈神经语言模型,依据 Bengio et al. (2003) 首次提出的算法。前馈语言模型引入了大型语言建模的许多重要概念,第 7 章和第 8 章会回到这些概念。

与第 3 章的 n-gram 语言模型相比,神经语言模型有很多优势。神经语言模型能够处理长得多的历史,能够在相似词的上下文之间更好泛化,在词预测上也准确得多。另一方面,神经网络语言模型更慢、更复杂、训练需要大量能量,并且比 n-gram 模型更不易解释;因此,对某些较小任务来说,n-gram 语言模型仍然是合适工具。

前馈神经语言模型是一种前馈网络:在时间 $t$,它接收若干先前词($w_{t-1},w_{t-2}$ 等)的表示作为输入,并输出可能下一个词上的概率分布。因此,和 n-gram LM 一样,前馈神经 LM 用前 $N-1$ 个词近似整个先前上下文给定时词的概率 $P(w_t|w_{1:t-1})$:

📐 公式 6.23
$$ P(w_t|w_1,\ldots,w_{t-1}) \approx P(w_t|w_{t-N+1},\ldots,w_{t-1}) \tag{6.23} $$

下面的例子使用 4-gram 示例,因此会展示一个用于估计 $P(w_t=i|w_{t-3},w_{t-2},w_{t-1})$ 的神经网络。

神经语言模型用这些先前上下文词的嵌入表示词,而不是像 n-gram 语言模型那样只用词身份。使用嵌入使神经语言模型能更好泛化到未见数据。例如,假设训练中见过下面这个句子:

I have to make sure that the cat gets fed.

但从未见过单词 “dog” 后面跟着 “gets fed”。测试集中有前缀:

I forgot to make sure that the dog gets

下一个词是什么?n-gram 语言模型会在 “that the cat gets” 后预测 “fed”,但不会在 “that the dog gets” 后这样预测。而神经 LM 知道 “cat” 和 “dog” 具有相似嵌入,因此即使看到 “dog”,也能从 “cat” 上下文泛化,并给 “fed” 分配足够高的概率。

图 6.14
图 6.14 前馈神经语言模型中的前向推理。在每个时间步 $t$,网络为 $N=3$ 个上下文词元中的每一个计算 $d$ 维嵌入(通过把独热向量乘以嵌入矩阵 $E$),并把三者拼接得到嵌入层 $e$。这个嵌入 $e$ 乘以权重矩阵 $W$,随后逐元素应用激活函数得到隐藏层 $h$,再乘以另一个权重矩阵 $U$。softmax 层在每个输出节点 $i$ 处预测下一个词 $w_t$ 是词表词 $V_i$ 的概率。图中把上下文窗口大小 $N$ 设为 3 只是为了适合页面;实践中,语言建模需要长得多的上下文。

这个预测任务需要一个表达 $|V|$ 个概率的输出向量:每个可能下一个词元都有一个概率值。词表可能包含 60,000 到 300,000 个词元,因此语言建模任务的输出向量远长于 3。语言建模的另一个不同点是:我们不再池化 $N$ 个输入词元的嵌入以创建单个嵌入,而是把输入拼接成一个很长的输入向量。为了预测下一个词元,知道每个先前词元以及它们的顺序会有帮助。

图 6.14 展示了语言建模任务,为了适合页面,它只画了非常短的上下文窗口 $N=3$。这 3 个嵌入向量被拼接成 $e$,即嵌入层。$e$ 乘以权重矩阵 $W$ 得到隐藏层,再乘以另一个权重矩阵 $U$ 得到输出层,其 softmax 给出词上的概率分布。例如,输出节点 42 的值 $y_{42}$,就是下一个词 $w_t$ 是 $V_{42}$ 的概率;在我们的例子中,词表中索引为 42 的词是 “fish”。

给定每个输入上下文词的独热输入向量,窗口大小为 3 的简单前馈神经语言模型方程如下:

📐 公式 6.24
$$ \begin{aligned} \boldsymbol{e} &= [E\boldsymbol{x}_{t-3};\ E\boldsymbol{x}_{t-2};\ E\boldsymbol{x}_{t-1}]\\ \boldsymbol{h} &= \sigma(W\boldsymbol{e} + \boldsymbol{b})\\ \boldsymbol{z} &= U\boldsymbol{h}\\ \hat{\boldsymbol{y}} &= \mathrm{softmax}(\boldsymbol{z}) \end{aligned} \tag{6.24} $$

注意,这里用分号表示向量拼接,因此通过拼接三个上下文向量的 3 个嵌入来形成嵌入层 $\boldsymbol{e}$。

在第 7 章和第 8 章介绍 transformer 语言模型时,我们会回到用神经网络做语言建模这一思想。

6.6   训练神经网络

前馈神经网络是监督机器学习的一个实例,其中每个观测 $\boldsymbol{x}$ 的正确输出 $\boldsymbol{y}$ 已知。系统通过式 (6.13) 产生 $\hat{\boldsymbol{y}}$,也就是系统对真实 $\boldsymbol{y}$ 的估计。训练过程的目标,是为每一层 $i$ 学习参数 $W^{[i]}$ 和 $\boldsymbol{b}^{[i]}$,使每个训练观测的 $\hat{\boldsymbol{y}}$ 尽可能接近真实 $\boldsymbol{y}$。

一般来说,这一切都基于第 4 章为逻辑回归介绍的方法,因此读者在继续之前应熟悉那一章。我们将在简单通用网络上探索算法,而不是在专门为情感分类或语言建模设计的网络上探索。

首先,需要一个损失函数,用于建模系统输出和标准输出之间的距离。常见做法是使用逻辑回归中使用的损失函数,即交叉熵损失。

第二,为了找到最小化这个损失函数的参数,我们会使用第 4 章介绍的梯度下降优化算法。

第三,梯度下降要求知道损失函数的梯度,也就是包含损失函数对每个参数偏导数的向量。在逻辑回归中,对于每个观测,我们可以直接计算损失函数对单个 $w$ 或 $b$ 的导数。但对于神经网络,网络有许多层、数百万参数;当损失只附着在网络最后面某层时,很难直接看出如何计算损失对第 1 层某个权重的偏导数。如何把损失沿所有这些中间层分解出来?答案是称为误差反向传播(error backpropagation)或反向微分(backward differentiation)的算法。

6.6.1   损失函数

神经网络中使用的交叉熵损失与逻辑回归中见过的相同。如果神经网络被用作二分类器,并在最后一层使用 sigmoid,那么损失函数就是式 (4.19) 中看到的同一个逻辑回归损失:

📐 公式 6.25
$$ L_{\mathrm{CE}}(\hat{y},y) = -\log p(y|\boldsymbol{x}) = -\left[y\log\hat{y} + (1-y)\log(1-\hat{y})\right] \tag{6.25} $$

如果使用网络分类到 3 个或更多类别,损失函数就与第 4 章第 82 页看到的多项回归损失完全相同。为方便起见,这里简要总结说明。首先,当类别多于 2 个时,需要把 $\boldsymbol{y}$ 和 $\hat{\boldsymbol{y}}$ 都表示为向量。假设我们做硬分类,其中只有一个类别是正确类别。真实标签 $\boldsymbol{y}$ 是一个有 $K$ 个元素的向量,每个元素对应一个类别;如果正确类别是 $c$,则 $y_c=1$,$\boldsymbol{y}$ 的所有其他元素为 0。回忆这种只有一个值等于 1、其余为 0 的向量称为独热向量。分类器会产生一个估计向量 $\hat{\boldsymbol{y}}$,也有 $K$ 个元素,其中每个元素 $\hat{y}_k$ 表示估计概率 $p(y_k=1|\boldsymbol{x})$。

单个样例 $\boldsymbol{x}$ 的损失函数,是 $K$ 个输出类别的对数之和的负值,每一项按其概率 $y_k$ 加权:

📐 公式 6.26
$$ L_{\mathrm{CE}}(\hat{\boldsymbol{y}}, \boldsymbol{y}) = -\sum_{k=1}^{K} y_k \log \hat{y}_k \tag{6.26} $$

可以进一步简化这个方程。先用函数 $1\{\}$ 重写方程;条件为真时它取 1,否则取 0。这样更明显:式 (6.26) 中求和项除了对应真实类别的那一项(此时 $y_k=1$)之外都会是 0:

$$ L_{\mathrm{CE}}(\hat{\boldsymbol{y}},\boldsymbol{y}) = -\sum_{k=1}^{K} 1\{y_k=1\}\log\hat{y}_k $$

换言之,交叉熵损失就是正确类别所对应输出概率的负对数,因此也称为负对数似然损失

📐 公式 6.27
$$ L_{\mathrm{CE}}(\hat{\boldsymbol{y}},\boldsymbol{y}) = -\log \hat{y}_c \qquad(\text{其中 }c\text{ 是正确类别}) \tag{6.27} $$

代入式 (6.9) 中的 softmax 公式,并令 $K$ 为类别数:

📐 公式 6.28
$$ L_{\mathrm{CE}}(\hat{\boldsymbol{y}},\boldsymbol{y}) = -\log \frac{\exp(z_c)}{\sum_{j=1}^{K}\exp(z_j)} \qquad(\text{其中 }c\text{ 是正确类别}) \tag{6.28} $$

思考一下负对数概率为何可作为损失函数。完美分类器会给正确类别 $i$ 分配概率 1,给所有错误类别分配概率 0。这意味着 $p(\hat{y}_i)$ 越高(越接近 1),分类器越好;$p(\hat{y}_i)$ 越低(越接近 0),分类器越差。这个概率的负对数是一个很漂亮的损失度量,因为它从 0(1 的负对数,没有损失)到无穷大(0 的负对数,无穷损失)。这个损失函数还确保:当正确答案的概率被最大化时,所有错误答案的概率会被最小化;由于它们总和为 1,正确答案概率的任何增加都来自错误答案概率的减少。

输出向量 $\hat{\boldsymbol{y}}$ 的类别数 $K$ 可以小也可以大。任务可能是三分类情感分析,此时类别可能是正面、负面和中性。或者,如果任务是判断一个词的词性(即它是名词、动词还是形容词等),那么 $K$ 是标注集中的可能词性集合(第 17 章会定义一个含 17 个词性的标注集)。如果任务是语言建模,分类器试图预测下一个词,那么类别集合就是词集合,可能有 50,000 或 100,000 个。

6.6.2   计算梯度

如何计算这个损失函数的梯度?计算梯度需要损失函数对每个参数的偏导数。对于只有一个权重层且 sigmoid 输出的网络(也就是逻辑回归),可以直接使用式 (6.29) 中的损失导数(并在第 4.15 节推导过):

📐 公式 6.29
$$ \begin{aligned} \frac{\partial L_{\mathrm{CE}}(\hat{y},y)}{\partial w_j} &= (\hat{y}-y)x_j\\ &= (\sigma(\boldsymbol{w}\cdot\boldsymbol{x}+b) - y)x_j \end{aligned} \tag{6.29} $$

或者,对于只有一个权重层且 softmax 输出的网络(即多项逻辑回归),可以使用式 (4.41) 中的 softmax 损失导数。下面针对特定权重 $w_k$ 和输入 $x_i$ 展示:

📐 公式 6.30
$$ \begin{aligned} \frac{\partial L_{\mathrm{CE}}(\hat{\boldsymbol{y}},\boldsymbol{y})}{\partial w_{k,i}} &= -(y_k - \hat{y}_k)x_i\\ &= -(y_k - p(y_k=1|\boldsymbol{x}))x_i\\ &= -\left(y_k - \frac{\exp(\boldsymbol{w}_k\cdot\boldsymbol{x}+b_k)}{\sum_{j=1}^{K}\exp(\boldsymbol{w}_j\cdot\boldsymbol{x}+b_j)}\right)x_i \end{aligned} \tag{6.30} $$

但这些导数只给出一个权重层的正确更新:最后一层!对于深度网络,计算每个权重的梯度要复杂得多,因为我们要计算损失对网络很早期层中权重参数的导数,而损失只在网络最后端计算。

计算这个梯度的解决方案是称为误差反向传播backprop 的算法(Rumelhart et al., 1986)。虽然 backprop 是专门为神经网络发明的,但它其实等同于一种更一般的过程,称为反向微分,而反向微分依赖于计算图的概念。下一小节看看它如何工作。

6.6.3   计算图

计算图是对数学表达式计算过程的一种表示。在计算图中,计算被分解为若干独立操作,每个操作被建模为图中的一个节点。

考虑计算函数 $L(a,b,c) = c(a+2b)$。如果把每个组成性的加法和乘法操作显式写出,并给中间输出加上名称($d$ 和 $e$),所得计算序列为:

$$ d = 2*b $$ $$ e = a + d $$ $$ L = c*e $$

现在可以把它表示为一张图:每个操作对应一个节点,有向边表示每个操作的输出作为下一步操作的输入,如图 6.15 所示。计算图最简单的用途,是在给定输入时计算函数值。图中假设输入 $a=3$、$b=1$、$c=-2$,并展示了前向传播计算结果 $L(3,1,-2)=-10$。在计算图的前向传播(forward pass)中,我们从左到右应用每个操作,把每个计算的输出作为下一个节点的输入。

图 6.15
图 6.15 函数 $L(a,b,c) = c(a+2b)$ 的计算图,输入节点取值为 $a=3$、$b=1$、$c=-2$,展示了 $L$ 的前向传播计算。

6.6.4   计算图上的反向微分

计算图的重要性来自反向传播(backward pass),它用于计算权重更新所需的导数。在这个例子中,我们的目标是计算输出函数 $L$ 对每个输入变量的导数,即 $\frac{\partial L}{\partial a}$、$\frac{\partial L}{\partial b}$ 和 $\frac{\partial L}{\partial c}$。导数 $\frac{\partial L}{\partial a}$ 告诉我们,$a$ 的一个小变化会对 $L$ 造成多大影响。

反向微分使用微积分中的链式法则,因此先回顾一下。假设我们计算复合函数 $f(x) = u(v(x))$ 的导数。$f(x)$ 的导数等于 $u(x)$ 对 $v(x)$ 的导数,乘以 $v(x)$ 对 $x$ 的导数:

📐 公式 6.31
$$ \frac{df}{dx} = \frac{du}{dv}\cdot\frac{dv}{dx} \tag{6.31} $$

链式法则可以扩展到多于两个函数。如果计算复合函数 $f(x) = u(v(w(x)))$ 的导数,则:

📐 公式 6.32
$$ \frac{df}{dx} = \frac{du}{dv}\cdot\frac{dv}{dw}\cdot\frac{dw}{dx} \tag{6.32} $$

反向微分的直觉是,把梯度从最终节点传回图中的所有节点。图 6.16 展示了某个节点 $e$ 上的部分反向计算。每个节点接收从右侧父节点传入的上游梯度;对于自己的每个输入,它计算局部梯度(输出相对于输入的梯度),并用链式法则把两者相乘,得到传给更早节点的下游梯度。

图 6.16
图 6.16 每个节点(如此处的 $e$)接收一个上游梯度,乘以局部梯度(输出相对于输入的梯度),并用链式法则计算传给先前节点的下游梯度。如果一个节点有多个输入,它可以有多个局部梯度。

现在计算所需的 3 个导数。由于在计算图中 $L = ce$,可以直接计算导数 $\frac{\partial L}{\partial c}$:

📐 公式 6.33
$$ \frac{\partial L}{\partial c} = e \tag{6.33} $$

另外两个需要使用链式法则:

📐 公式 6.34
$$ \begin{aligned} \frac{\partial L}{\partial a} &= \frac{\partial L}{\partial e}\frac{\partial e}{\partial a}\\ \frac{\partial L}{\partial b} &= \frac{\partial L}{\partial e}\frac{\partial e}{\partial d}\frac{\partial d}{\partial b} \end{aligned} \tag{6.34} $$

式 (6.34) 和式 (6.33) 因此需要五个中间导数:$\frac{\partial L}{\partial e}$、$\frac{\partial L}{\partial c}$、$\frac{\partial e}{\partial a}$、$\frac{\partial e}{\partial d}$ 和 $\frac{\partial d}{\partial b}$。它们如下(利用了求和导数等于导数之和这一事实):

$$ \begin{aligned} L = ce: &\quad \frac{\partial L}{\partial e} = c,\quad \frac{\partial L}{\partial c} = e\\ e = a + d: &\quad \frac{\partial e}{\partial a} = 1,\quad \frac{\partial e}{\partial d} = 1\\ d = 2b: &\quad \frac{\partial d}{\partial b} = 2 \end{aligned} $$

在反向传播中,我们沿着图中的每条边从右向左计算这些偏导,并像上面那样使用链式法则。因此,我们先计算从节点 $L$ 出发的下游梯度,即 $\frac{\partial L}{\partial e}$ 和 $\frac{\partial L}{\partial c}$。对于节点 $e$,我们把这个上游梯度 $\frac{\partial L}{\partial e}$ 乘以局部梯度(输出相对于输入的梯度)$\frac{\partial e}{\partial d}$,得到发回节点 $d$ 的输出 $\frac{\partial L}{\partial d}$。依此类推,直到把图一直标注到所有输入变量。前向传播已经方便地计算出了求这些导数所需的前向中间变量值(如 $d$ 和 $e$)。图 6.17 展示了反向传播。

图 6.17
图 6.17 函数 $L(a,b,c) = c(a+2b)$ 的计算图,展示了计算 $\frac{\partial L}{\partial a}$、$\frac{\partial L}{\partial b}$ 和 $\frac{\partial L}{\partial c}$ 的反向传播过程。

神经网络的反向微分

当然,真实神经网络的计算图要复杂得多。图 6.18 展示了一个 2 层神经网络的示例计算图,其中 $n_0=2$、$n_1=2$、$n_2=1$;为简化起见,假设是二分类,因此使用 sigmoid 输出单元。该计算图正在计算的函数为:

📐 公式 6.35
$$ \begin{aligned} \boldsymbol{z}^{[1]} &= W^{[1]}\boldsymbol{x} + \boldsymbol{b}^{[1]}\\ \boldsymbol{a}^{[1]} &= \mathrm{ReLU}(\boldsymbol{z}^{[1]})\\ \boldsymbol{z}^{[2]} &= W^{[2]}\boldsymbol{a}^{[1]} + \boldsymbol{b}^{[2]}\\ \boldsymbol{a}^{[2]} &= \sigma(\boldsymbol{z}^{[2]})\\ \hat{y} &= \boldsymbol{a}^{[2]} \end{aligned} \tag{6.35} $$

对于反向传播,我们还需要计算损失 $L$。式 (6.25) 中二值 sigmoid 输出的损失函数为:

📐 公式 6.36
$$ L_{\mathrm{CE}}(\hat{y},y) = -\left[y\log\hat{y} + (1-y)\log(1-\hat{y})\right] \tag{6.36} $$

我们的输出 $\hat{y} = a^{[2]}$,因此可以改写为:

📐 公式 6.37
$$ L_{\mathrm{CE}}(a^{[2]},y) = -\left[y\log a^{[2]} + (1-y)\log(1-a^{[2]})\right] \tag{6.37} $$
图 6.18
图 6.18 一个简单 2 层神经网络(即 1 个隐藏层)的示例计算图,包含两个输入单元和两个隐藏单元。图中略微调整了记号,以避免节点中的方程过长:只标出正在计算的函数和所得变量名。因此,节点 $w_{11}^{[1]}$ 右侧的 $*$ 表示 $w_{11}^{[1]}$ 要乘以 $x_1$,而节点 $z_1^{[1]}=+$ 表示 $z_1^{[1]}$ 的值由流入它的三个节点(两个乘积项和偏置项 $b_i^{[1]}$)求和得到。

需要更新的权重(也就是我们需要知道损失函数偏导数的权重)在图中以蓝绿色显示。为了进行反向传播,需要知道图中所有函数的导数。第 4.15 节已经见过 sigmoid $\sigma$ 的导数:

📐 公式 6.38
$$ \frac{d\sigma(z)}{dz} = \sigma(z)(1 - \sigma(z)) \tag{6.38} $$

还需要其他激活函数的导数。tanh 的导数为:

📐 公式 6.39
$$ \frac{d\tanh(z)}{dz} = 1 - \tanh^2(z) \tag{6.39} $$

ReLU 的导数为:[注 2]

📐 公式 6.40
$$ \frac{d\,\mathrm{ReLU}(z)}{dz} = \begin{cases} 0, & \text{for } z < 0\\ 1, & \text{for } z \geq 0 \end{cases} \tag{6.40} $$

下面给出计算的开头:计算损失函数 $L$ 对 $z$ 的导数,即 $\frac{\partial L}{\partial z}$(其余计算留作读者练习)。由链式法则:

📐 公式 6.41
$$ \frac{\partial L}{\partial z} = \frac{\partial L}{\partial a^{[2]}}\frac{\partial a^{[2]}}{\partial z} \tag{6.41} $$

先计算 $\frac{\partial L}{\partial a^{[2]}}$,对重复如下的式 (6.37) 求导:

$$ L_{\mathrm{CE}}(a^{[2]},y) = -\left[y\log a^{[2]} + (1-y)\log(1-a^{[2]})\right] $$
📐 公式 6.42
$$ \begin{aligned} \frac{\partial L}{\partial a^{[2]}} &= -\left(y\,\frac{\partial\log(a^{[2]})}{\partial a^{[2]}} + (1-y)\,\frac{\partial\log(1-a^{[2]})}{\partial a^{[2]}}\right)\\ &= -\left(y\,\frac{1}{a^{[2]}} + (1-y)\,\frac{1}{1-a^{[2]}}(-1)\right)\\ &= -\left(\frac{y}{a^{[2]}} + \frac{y-1}{1-a^{[2]}}\right) \end{aligned} \tag{6.42} $$

接着,根据 sigmoid 的导数:

$$ \frac{\partial a^{[2]}}{\partial z} = a^{[2]}(1 - a^{[2]}) $$

最后使用链式法则:

📐 公式 6.43
$$ \begin{aligned} \frac{\partial L}{\partial z} &= \frac{\partial L}{\partial a^{[2]}}\frac{\partial a^{[2]}}{\partial z}\\ &= -\left(\frac{y}{a^{[2]}} + \frac{y-1}{1-a^{[2]}}\right) a^{[2]}(1-a^{[2]})\\ &= a^{[2]} - y \end{aligned} \tag{6.43} $$

继续进行梯度的反向计算(下一步把梯度传过 $b_1^{[2]}$ 和两个乘积节点,依此类推,传回所有蓝绿色节点)留作读者练习。

6.6.5   学习的更多细节

神经网络中的优化是非凸优化问题,比逻辑回归更复杂;由于这一点以及其他原因,成功学习有许多最佳实践。

对于逻辑回归,可以用所有权重和偏置都为 0 来初始化梯度下降。相比之下,在神经网络中,需要用较小的随机数初始化权重。把输入值归一化为均值 0、方差 1 也很有帮助。

为了防止过拟合,会使用各种正则化形式。其中最重要的一种是 dropout:训练期间随机从网络中丢弃一些单元及其连接(Hinton et al. 2012, Srivastava et al. 2014)。在训练的每次迭代中(每当更新参数时;如果使用小批量梯度下降,就是每个 mini-batch),我们反复选择一个概率 $p$,并对每个单元以概率 $p$ 把它的输出替换为零(同时重新归一化该层其余输出)。

调节超参数也很重要。神经网络的参数是权重 $W$ 和偏置 $\boldsymbol{b}$;它们由梯度下降学习得到。超参数则由算法设计者选择;最佳值是在开发集上调节的,而不是通过在训练集上用梯度下降学习得到。超参数包括学习率 $\eta$、mini-batch 大小、模型架构(层数、每层隐藏节点数、激活函数选择)、如何正则化,等等。梯度下降本身也有许多架构变体,如 Adam(Kingma and Ba, 2015)。

最后,大多数现代神经网络都是用计算图形式体系构建的。这些形式体系使梯度计算以及在基于向量的 GPU(图形处理单元)上的并行化变得简单而自然。PyTorch(Paszke et al., 2017)和 TensorFlow(Abadi et al., 2015)是其中最流行的两个。感兴趣的读者可以查阅神经网络教材了解更多细节;本章末尾给出一些建议。

6.7   小结

历史注记

神经网络的起源在 20 世纪 40 年代的 McCulloch–Pitts 神经元(McCulloch and Pitts, 1943):它把生物神经元简化为一种可以用命题逻辑描述的计算元素。到 20 世纪 50 年代末和 60 年代初,许多实验室(包括 Cornell 的 Frank Rosenblatt 和 Stanford 的 Bernard Widrow)开展了神经网络研究;这一阶段出现了感知机(Rosenblatt, 1958),并把阈值转换为偏置,这一记号沿用至今(Widrow and Hoff, 1960)。

在单个感知机单元被证明不能建模像 XOR 这样简单的函数后,神经网络领域进入低潮(Minsky and Papert, 1969)。此后两个十年仍有少量工作继续进行,但该领域的重大复兴直到 20 世纪 80 年代才到来,当时误差反向传播等构建更深网络的实用工具开始普及(Rumelhart et al., 1986)。在 20 世纪 80 年代,人们发展出大量神经网络及相关架构,尤其用于心理学和认知科学应用(Rumelhart and McClelland 1986b, McClelland and Elman 1986, Rumelhart and McClelland 1986a, Elman 1990)。当时常使用连接主义(connectionist)或并行分布式处理这类术语(Feldman and Ballard 1982, Smolensky 1988)。这一时期发展出的许多原则和技术是现代工作的基础,包括分布式表示(Hinton, 1986)、循环网络(Elman, 1990)以及用张量表示组合性(Smolensky, 1990)。

到 20 世纪 90 年代,更大的神经网络开始应用于许多实际语言处理任务,例如手写识别(LeCun et al. 1989)和语音识别(Morgan and Bourlard 1990)。到 21 世纪初,计算机硬件的改进以及优化和训练技术的进步,使训练更大更深的网络成为可能,并产生了现代术语 deep learning(Hinton et al. 2006, Bengio et al. 2007)。第 13 章和第 15 章会覆盖更多相关历史。

关于神经网络,有不少优秀书籍,包括 Goodfellow et al. (2016) 和 Nielsen (2015)。