Multilayer Perceptrons
在第3节中,我们介绍了softmax回归(第3.4节),从头实现算法(第3.6节)和使用高级api(第3.7节),并训练分类器从低分辨率图像中识别10类服装。在此过程中,我们学习了如何处理数据,迫使我们的输出进入一个有效的概率分布,应用一个适当的损失函数,并使其相对于我们的模型参数最小。既然我们已经掌握了简单线性模型中的这些机制,我们就可以开始对深度神经网络的探索,这是本书主要关注的相对丰富的一类模型。
Hidden Layers
我们在3.1.1.1节中描述了仿射变换,它是一个加了偏差的线性变换。首先,回顾与我们的softmax回归示例相对应的模型架构,如图3.4.1所示。这个模型通过一个仿射变换将我们的输入直接映射到输出,然后是一个softmax操作。如果我们的标签真的通过仿射变换与输入数据相关,那么这种方法就足够了。但是仿射变换中的线性是一个很强的假设。
Linear Models May Go Wrong
例如,线性意味着较弱的单调性假设: 我们的特征的任何增加必须要么总是导致我们的模型输出的增加(如果相应的权值是正的),要么总是导致我们的模型输出的减少(如果相应的权值是负的)。有时候它是有用的。 例如,如果我们试图预测一个人是否会偿还贷款,我们可以合理地设想,在其他条件相同的情况下,收入较高的申请人总是比收入较低的申请人更有可能偿还贷款。虽然这种关系是单调的,但它很可能与偿还的概率不是线性相关的。收入从0万增加到5万的可能性比收入从100万增加到105万的可能性更大。处理这个问题的一种方法可能是对我们的数据进行预处理,使线性变得更合理,比如,使用收入的对数作为特征。
注意,我们可以很容易地举出违反单调性的例子。比如说,我们想根据体温来预测死亡的概率。对于体温超过37摄氏度(98.6华氏度)的人来说,温度越高风险越大。然而,对于体温低于37摄氏度的人来说,更高的温度意味着风险更低。在这种情况下,我们也可以通过一些巧妙的预处理来解决这个问题。也就是说,我们可以用距离37摄氏度的远近作为我们的特征。
但是如何对猫和狗的图像进行分类呢?增加位置(13,17)像素的强度是否总是增加(或总是减少)图像描绘狗的可能性。对线性模型的依赖对应着一个隐含的假设,即区分猫和狗的唯一要求是评估单个像素的亮度。这种方法注定要失败,因为在这个世界上,颠倒图像保留了类别。
然而,尽管这里的线性明显是荒谬的,与我们之前的例子相比,它并不明显,我们可以通过一个简单的预处理修复来解决这个问题。这是因为任何像素的重要性以复杂的方式取决于它的上下文(周围像素的值)。虽然我们的数据可能存在一种表示,它将考虑我们的特征之间的相关相互作用,在此基础上,一个线性模型将是合适的,但我们只是不知道如何手工计算它。通过深度神经网络,我们使用观测数据通过隐藏层和线性预测器共同学习表征。
Incorporating Hidden Layers
通过合并一个或多个隐藏层,我们可以克服线性模型的这些限制,处理更一般的一类函数。最简单的方法是将许多全连接的层堆叠在一起。每一层都将馈入其上一层,直到生成输出。我们可以把第$L−1$看作是我们的表征 并且 最后一层是我们的线性预测器。这种架构通常被称为多层感知器,通常缩写为MLP。下面,我们用示意图描述一个MLP(图4.1.1)。
图4.1.1 一个有5个隐藏单元隐藏层的MLP。
这个MLP有4个输入,3个输出,它的隐藏层包含5个隐藏单元。由于输入层不涉及任何计算,用这个网络产生输出需要同时实现隐藏层和输出层的计算;因此,该MLP中的层数为2。注意,这些层都是全连接的。每一个输入都会影响隐层中的每个神经元,而每一个隐层又会依次影响输出层中的每个神经元。然而,如第3.4.3节所述,具有全连接层的mlp的参数成本可能非常高,这启发了 在不改变输入或输出大小的情况下,可以在参数节约和模型效率之间进行权衡 [Zhang et al., 2021]
。
From Linear to Nonlinear
像之前一样, 通过矩阵 $X \in \mathbb{R}^{n \times d}$, 我们将 $n$ 个样本表示为一个 minibatch, 其中每个样本有 $d$ 个输入(feature)。 对于一个单隐藏层的MLP, 它的隐藏层有 $h$ 个隐藏单元, 隐藏层的输入表示为 $H \in \mathbb{R}^{n \times h}$, 它是隐藏表征。在数学化或者代码化 ,$H$ 也称为隐藏层变量或隐藏变量。因为隐藏层和输出层都是全连接的,所以我们有隐藏层的权值 $W^{(1)} \in \mathbb{R}^{d \times h}$ 以及偏差 $b^{(1)} \in \mathbb{R}^{1 \times h}$ 以及输出层权重 $W^{(2)} \in \mathbb{R}^{h \times q}$ 以及 偏差 $b^{(2)} \in \mathbb{R}^{1 \times q}$。 形式上, 我们计算一个隐藏层的输出 $O \in \mathbb{R}^{n \times q}$如下:
\[H = XW^{(1)} + b^{(1)} \\ O = HW^{(2)} + b^{(2)} \tag{4.1.1}\]注意,在添加了隐藏层之后,我们的模型现在需要我们跟踪和更新增加的参数集。那么我们得到了什么? 您可能会惊讶地发现,在上述定义的模型中,我们的麻烦没有得到任何好处!理由很简单。 上面的隐藏单元由输入的仿射函数给出,输出(pre-softmax)只是隐藏单元的仿射函数。仿射函数的仿射函数本身也是仿射函数。何况,我们的线性模型已经能够表示任何仿射函数。
我们可以通过证明对于任何值的权值,我们可以把隐藏层折叠起来,从形式上看这个等价性, 得到一个等效单层模型, 它的参数为 $W = W^{(1)}W^{(2)}$ 以及 $b = b^{(1)}W^{(2)} + b^{(2)}$
\[O = (XW^{(1)} + b^{(1)})W^{(2)} + b^{(2)} = XW^{(1)}W^{(2)} + b^{(1)}W^{(2)} + b^{(2)} = XW + b \tag{4.1.2}\]为了实现多层结构的潜力, 我们需要一个关键的成分: 非线性激活函数 $\sigma$ 应用于仿射变换后的每个隐藏单元。激活函数的输出(e.g., σ(⋅) )叫做 activations。通常,有了激活函数,就不可能再将我们的MLP分解成一个线性模型:
\[H = \sigma(XW^{(1)} + b^{(1)}) \\ O = HW^{(2)} + b^{(2)} \tag{4.1.3}\]因为 $X$ 中每行 对应 minibatch 中的一个样本, 乱用一些符号, 我们定义非线性 $\sigma$ 以按行的方式应用于其输入,即一次一个样本。注意,我们在3.4.5节中以同样的方式使用了softmax的表示法来表示按行操作。通常,我们应用于隐藏层的激活函数不仅是按行的,而且是按元素的。这意味着在计算完层的线性部分后,我们可以计算每个激活,而不需要查看其他隐藏单元的值。对于大多数激活函数都是这样。
为了构建更通用的mlp,我们可以继续堆叠这样的隐藏层, 比如 $H^{(1)} = \sigma_1(XW^{(1)} + b^{(1)})$ 以及 $H^{(2)} = \sigma_2(H^{(1)}W^{(2)} + b^{(2)})$,一个在另一个之上,创造出更具表征性的模型。
Universal Approximators
MLPs 可以通过其隐藏的神经元捕获输入之间的复杂交互作用,这些神经元依赖于每个输入的值。我们可以很容易地设计隐藏节点来执行任意的计算,例如,对一对输入进行基本的逻辑操作。此外,对于激活函数的某些选择,众所周知 MLPs 是通用近似器。即使是单一的隐藏层网络,只要有足够的节点(可能多得离谱)和正确的权值集,我们就可以对任何函数建模,尽管实际学习该函数是非常困难的。您可能认为神经网络有点像C编程语言。但是真正想出一个符合你的规范的程序是困难的部分。
此外,仅仅因为单一隐藏层网络可以学习任何函数,并不意味着您应该尝试用单一隐藏层网络来解决所有问题。实际上,通过使用更深(而不是更宽)的网络,我们可以更紧凑地近似许多函数。我们将在以后的几章中涉及更严格的论证。
Activation Functions
激活函数通过计算加权和和增加偏差的结果来决定一个神经元是否应该被激活。它们是将输入信号转换为输出信号的可微算子,但它们大多数都增加了非线性。由于激活函数是深度学习的基础,让我们简要介绍一些常见的激活函数。
1
2
3
%matplotlib inline
import torch
from d2l import torch as d2l
ReLU Function
由于实现简单且在各种预测任务上具有良好的性能,最流行的选择是 rectified linear unit(ReLU)。ReLU提供了一个非常简单的非线性变换。给定一个元素 $x$,函数被定义为该元素与 $0$ 的最大值:
\[\text{RELU}(x) = \max (x, 0) \tag{4.1.4}\]非形式化地讲,ReLU函数通过将相应的激活设置为0,只保留正元素,丢弃所有负元素。为了直观,我们可以画出函数。你可以看到,激活函数是分段线性的。
1
2
3
x = torch.arange(-8.0, 8.0, 0.1, requires_grad=True)
y = torch.relu(x)
d2l.plot(x.detach(), y.detach(), 'x', 'relu(x)', figsize=(5, 2.5))
当输入为负时,ReLU函数的导数为0,当输入为正时,ReLU函数的导数为1。注意,当输入的值恰好等于0时,ReLU函数是不可微分的。在这些情况下,我们默认为左边的导数,当输入为0时,导数为0。我们可以这样做,因为输入可能永远不会是零。有句古老的格言说,如果微妙的边界条件很重要,我们可能是在做(真正的)数学,而不是工程。这种传统智慧或许适用于此。我们在下面画出ReLU函数的导数。
1
2
y.backward(torch.ones_like(x), retain_graph=True)
d2l.plot(x.detach(), x.grad, 'x', 'grad of relu', figsize=(5, 2.5))
使用ReLU的原因是它的导数表现得特别好:它们要么消失,要么让参数通过。这使得优化表现得更好,并缓解了之前版本的神经网络所困扰的梯度消失问题。
注意,ReLU函数有很多变体,包括参数化的ReLU (pReLU)函数 [He et al., 2015]
。这种变化给ReLU增加了一个线性项,所以即使参数是负的,一些信息仍然可以传递出去。
Sigmoid Function
sigmoid函数转换其输入,其值位于定义域 $\mathbb{R}$ 内, 输出位于 $(0, 1)$内。因此,sigmoid通常被称为压缩函数:它将范围(-inf, inf)内的任何输入压缩为范围(0,1)内的某个值:
\[\text{sigmoid}(x) = \frac{1}{1 + \exp(-x)} \tag{4.1.6}\]在最早的神经网络中,科学家们对模拟生物神经元感兴趣,这些神经元要么会放电,要么不放电。因此,这个领域的先驱们,可以追溯到人工神经元的发明者McCulloch和Pitts,他们专注于阈值单元。阈值激活在输入低于某个阈值时取值0,在输入超过该阈值时取值1。
当注意力转向基于梯度的学习时,sigmoid函数是一个自然的选择,因为它是一个阈值单元的光滑、可微的近似值。当我们希望将输出解释为二进制分类问题的概率时(您可以将sigmoid看作softmax的一种特殊情况),sigmoid仍然作为输出单元上的激活函数被广泛使用。然而,sigmoid已经被更简单、更容易训练的ReLU所取代,用于隐藏层的大多数使用。在后面关于循环神经网络的章节中,我们将描述利用sigmoid单元来控制跨时间的信息流的架构。
下面,我们绘制 sigmoid 函数。注意,当输入接近于0时,sigmoid函数接近于一个线性变换。
1
2
y = torch.sigmoid(x)
d2l.plot(x.detach(), y.detach(), 'x', 'sigmoid(x)', figsize=(5, 2.5))
sigmoid 函数的导数由如下等式所示:
\[\frac{d}{dx} \text{sigmoid}(x) = \frac{\exp(-x)}{(1 + \exp(-x))^2} = \text{sigmoid}(x)(1 - \text{sigmoid}(x)) \tag{4.1.7}\]sigmoid 函数的导数绘制如下。 注意,当输入为0时,sigmoid函数的导数最大为0.25。当输入在任意方向上从0发散时,导数趋于0。
1
2
y = torch.sigmoid(x)
d2l.plot(x.detach(), y.detach(), 'x', 'sigmoid(x)', figsize=(5, 2.5))
Tanh Function
就像sigmoid函数一样,tanh(双曲正切)函数也压缩了它的输入,将它们转换为-1到1区间上的元素:
\[\text{tanh} = \frac{1 - \exp(-2x)}{1+\exp(-2x)} \tag{4.1.8}\]我们把tanh函数画在下面。注意当输入趋于0时,tanh函数趋于一个线性变换。虽然函数的形状类似于sigmoid函数,tanh函数关于坐标系原点对称。
1
2
y = torch.tanh(x)
d2l.plot(x.detach(), y.detach(), 'x', 'tanh(x)', figsize=(5, 2.5))
tanh函数的导数是:
\[\frac{d}{dx}\text{tanh}(x) = 1 - \text{tanh}^2(x) \tag{4.1.9}\]tanh函数的导数画在下面。当输入趋于0时,tanh函数的导数趋于最大值1。就像我们在sigmoid函数中看到的,当输入从0向任意方向移动时,tanh函数的导数趋于0。
1
2
3
4
# Clear out previous gradients.
x.grad.data.zero_()
y.backward(torch.ones_like(x), retain_graph=True)
d2l.plot(x.detach(), x.grad, 'x', 'grad of tanh', figsize=(5, 2.5))
总之,我们现在知道如何结合非线性来构建表达多层神经网络结构。你的知识已经使你掌握了一个类似于1990年从业人员的工具包。在某些方面,您比任何在90年代工作的人都有优势,因为您可以利用强大的开源深度学习框架,只用几行代码就可以快速构建模型。以前,训练这些网络需要研究人员编写数千行C语言和Fortran语言。
Summary
- MLP在输出层和输入层之间添加一个或多个全连接的隐藏层,并通过激活函数转换隐藏层的输出。
- 常用的激活函数有ReLU函数、sigmoid函数和tanh函数。
Exercises
1) 计算pReLU激活函数的导数。
2) 说明仅使用ReLU(或pReLU)的MLP构造了一个连续的分段线性函数。
3) 说明 $\text{tanh}(x) + 1 = 2 \text{sigmoid}(2x)$
4) 假设我们有一个适用于一次一个 minibatch 的非线性函数。 你预计这会导致哪些问题?