Run this notebook online:\ |Binder| or Colab: |Colab|
.. |Binder| image:: https://mybinder.org/badge_logo.svg
:target: https://mybinder.org/v2/gh/deepjavalibrary/d2l-java/master?filepath=chapter_multilayer-perceptrons/mlp.ipynb
.. |Colab| image:: https://colab.research.google.com/assets/colab-badge.svg
:target: https://colab.research.google.com/github/deepjavalibrary/d2l-java/blob/colab/chapter_multilayer-perceptrons/mlp.ipynb
.. _sec_mlp:
多层感知机
==========
在 :numref:`chap_linear` 中,我们介绍了softmax回归(
:numref:`sec_softmax` ),然后我们从零开始实现softmax回归(
:numref:`sec_softmax_scratch` ),接着使用DJL 实现了算法(
:numref:`sec_softmax_concise`
),并训练分类器从低分辨率图像中识别10类服装。在这个过程中,我们学习了如何处理数据,将输出转换为有效的概率分布,并应用适当的损失函数,根据模型参数最小化损失。我们已经在简单的线性模型背景下掌握了这些知识,现在我们可以开始对深度神经网络的探索,这也是本书主要涉及的一类模型。
隐藏层
------
我们在
:numref:`subsec_linear_model`\ 中描述了仿射变换,它是一个带有偏置项的线性变换。首先,回想一下如
:numref:`fig_softmaxreg`\ 中所示的softmax回归的模型结构。该模型通过单个仿射变换将我们的输入直接映射到输出,然后进行softmax操作。如果我们的标签通过仿射变换后确实与我们的输入数据相关,那么这种方法就足够了。但是,仿射变换中的\ *线性*\ 是一个很强的假设。
线性模型可能会出错
~~~~~~~~~~~~~~~~~~
例如,线性意味着\ *单调*\ 假设:特征的任何增大都会导致模型输出增大(如果对应的权重为正),或者导致模型输出减少(如果对应的权重为负)。有时这是有道理的。例如,如果我们试图预测一个人是否会偿还贷款。我们可以认为,在其他条件不变的情况下,收入较高的申请人总是比收入较低的申请人更有可能偿还贷款。但是,虽然收入与还款概率存在单调性,但它们不是线性相关的。收入从0增加到5万,可能比从100万增加到105万带来更大的还款可能性。处理这一问题的一种方法是对我们的数据进行预处理,使线性变得更合理,如使用收入的对数作为我们的特征。
我们可以很容易地找出违反单调性的例子。例如,我们想要根据体温预测死亡率。对于体温高于37摄氏度的人来说,温度越高风险越大。然而,对于体温低于37摄氏度的人来说,温度越高风险就越低。在这种情况下,我们也可以通过一些巧妙的预处理来解决问题。例如,我们可以使用与37摄氏度的距离作为特征。
但是,如何对猫和狗的图像进行分类呢?增加位置(13,
17)处像素的强度是否总是增加(或总是降低)图像描绘狗的可能性?对线性模型的依赖对应于一个隐含的假设,即区分猫和狗的唯一要求是评估单个像素的强度。在一个倒置图像保留类别的世界里,这种方法注定会失败。
与我们前面的例子相比,这里的线性很荒谬,而且我们难以通过简单的预处理来解决这个问题。这是因为任何像素的重要性都以复杂的方式取决于该像素的上下文(周围像素的值)。我们的数据可能会有一种表示,这种表示会考虑到我们的特征之间的相关交互作用。在此表示的基础上建立一个线性模型可能会是合适的,但我们不知道如何手动计算这么一种表示。对于深度神经网络,我们使用观测数据来联合学习隐藏层表示和应用于该表示的线性预测器。
合并隐藏层
~~~~~~~~~~
我们可以通过合并一个或多个隐藏层来克服线性模型的限制,并处理更一般化的函数。要做到这一点,最简单的方法是将许多全连接层堆叠在一起。每一层都输出到上面的层,直到生成最后的输出。我们可以把前\ :math:`L-1`\ 层看作表示,把最后一层看作线性预测器。这种架构通常称为\ *多层感知机*\ (multilayer
perceptron),通常缩写为\ *MLP*\ 。下面,我们以图的方式描述了多层感知机(
:numref:`fig_mlp`\ )。
|一个单隐藏层的多层感知机,具有5个隐藏单元| .. _fig_mlp:
这个多层感知机有4个输入,3个输出,其隐藏层包含5个隐藏单元。输入层不涉及任何计算,因此使用此网络产生输出只需要实现隐藏层和输出层的计算;因此,这个多层感知机中的层数为2。注意,这两个层都是全连接的。每个输入都会影响隐藏层中的每个神经元,而隐藏层中的每个神经元又会影响输出层中的每个神经元。
然而,正如 :numref:`subsec_parameterization-cost-fc-layers`
所说,具有全连接层的多层感知机的参数开销可能会高得令人望而却步,
即使在不改变输入或输出大小的情况下,也可能促使在参数节约和模型有效性之间进行权衡
:cite:`Zhang.Tay.Zhang.ea.2021`\ 。
从线性到非线性
~~~~~~~~~~~~~~
跟之前的章节一样,我们通过矩阵\ :math:`\mathbf{X} \in \mathbb{R}^{n \times d}`\ 来表示\ :math:`n`\ 个样本的小批量,其中每个样本具有\ :math:`d`\ 个输入(特征)。对于具有\ :math:`h`\ 个隐藏单元的单隐藏层多层感知机,用\ :math:`\mathbf{H} \in \mathbb{R}^{n \times h}`\ 表示隐藏层的输出,称为\ *隐藏表示*\ (hidden
representations)。在数学或代码中,\ :math:`\mathbf{H}`\ 也被称为\ *隐藏层变量*\ (hidden-layer
variable)或\ *隐藏变量*\ (hidden variable)。
因为隐藏层和输出层都是全连接的,所以我们具有隐藏层权重\ :math:`\mathbf{W}^{(1)} \in \mathbb{R}^{d \times h}`\ 和隐藏层偏置\ :math:`\mathbf{b}^{(1)} \in \mathbb{R}^{1 \times h}`\ 以及输出层权重\ :math:`\mathbf{W}^{(2)} \in \mathbb{R}^{h \times q}`\ 和输出层偏置\ :math:`\mathbf{b}^{(2)} \in \mathbb{R}^{1 \times q}`\ 。形式上,我们按如下方式计算单隐藏层多层感知机的输出\ :math:`\mathbf{O} \in \mathbb{R}^{n \times q}`\ :
.. math::
\begin{aligned}
\mathbf{H} & = \mathbf{X} \mathbf{W}^{(1)} + \mathbf{b}^{(1)}, \\
\mathbf{O} & = \mathbf{H}\mathbf{W}^{(2)} + \mathbf{b}^{(2)}.
\end{aligned}
注意,在添加隐藏层之后,模型现在需要跟踪和更新额外的参数。可我们能从中得到什么好处呢?你可能会惊讶地发现:在上面定义的模型里,我们没有好处!原因很简单。上面的隐藏单元由输入的仿射函数给出,而输出(softmax操作前)只是隐藏单元的仿射函数。仿射函数的仿射函数本身就是仿射函数。但是我们之前的线性模型已经能够表示任何仿射函数。
我们可以证明这一等价性,即对于任意权重值,我们只需合并隐藏层,便可产生具有参数\ :math:`\mathbf{W} = \mathbf{W}^{(1)}\mathbf{W}^{(2)}`\ 和\ :math:`\mathbf{b} = \mathbf{b}^{(1)} \mathbf{W}^{(2)} + \mathbf{b}^{(2)}`\ 的等价单层模型:
.. math::
\mathbf{O} = (\mathbf{X} \mathbf{W}^{(1)} + \mathbf{b}^{(1)})\mathbf{W}^{(2)} + \mathbf{b}^{(2)} = \mathbf{X} \mathbf{W}^{(1)}\mathbf{W}^{(2)} + \mathbf{b}^{(1)} \mathbf{W}^{(2)} + \mathbf{b}^{(2)} = \mathbf{X} \mathbf{W} + \mathbf{b}.
为了发挥多层结构的潜力,我们还需要一个额外的关键要素:在仿射变换之后对每个隐藏单元应用非线性的\ *激活函数*\ (activation
function)\ :math:`\sigma`\ 。激活函数的输出(例如,\ :math:`\sigma(\cdot)`\ )被称为\ *激活值*\ (activations)。一般来说,有了激活函数,就不可能再将我们的多层感知机退化成线性模型:
.. math::
\begin{aligned}
\mathbf{H} & = \sigma(\mathbf{X} \mathbf{W}^{(1)} + \mathbf{b}^{(1)}), \\
\mathbf{O} & = \mathbf{H}\mathbf{W}^{(2)} + \mathbf{b}^{(2)}.\\
\end{aligned}
由于\ :math:`\mathbf{X}`\ 中的每一行对应于小批量中的一个样本,出于记号习惯的考量,我们定义非线性函数\ :math:`\sigma`\ 也以按行的方式作用于其输入,即一次计算一个样本。我们在
:numref:`subsec_softmax_vectorization`
中以相同的方式使用了softmax符号来表示按行操作。但是在本节中,我们应用于隐藏层的激活函数通常不仅仅是按行的,而且也是按元素。
这意味着在计算每一层的线性部分之后,我们可以计算每个激活值,而不需要查看其他隐藏单元所取的值。对于大多数激活函数都是这样。
为了构建更通用的多层感知机,我们可以继续堆叠这样的隐藏层,例如,\ :math:`\mathbf{H}^{(1)} = \sigma_1(\mathbf{X} \mathbf{W}^{(1)} + \mathbf{b}^{(1)})`\ 和\ :math:`\mathbf{H}^{(2)} = \sigma_2(\mathbf{H}^{(1)} \mathbf{W}^{(2)} + \mathbf{b}^{(2)})`\ ,一层叠一层,从而产生更有表达能力的模型。
通用近似定理
~~~~~~~~~~~~
多层感知机可以通过隐藏神经元捕捉到我们输入之间复杂的相互作用,这些神经元依赖于每个输入的值。我们可以很容易地设计隐藏节点来执行任意计算。例如,在一对输入上进行基本逻辑操作。多层感知机是通用近似器。即使是网络只有一个隐藏层,给定足够的神经元(可能非常多)和正确的权重,我们可以对任意函数建模,尽管实际中学习该函数是很困难的。你可能认为神经网络有点像C语言。C语言和任何其他现代编程语言一样,能够表达任何可计算的程序。但实际上,想出一个符合规范的程序才是最困难的部分。
而且,虽然一个单隐层网络能学习任何函数,但并不意味着应该尝试使用单隐藏层网络来解决所有问题。事实上,通过使用更深(而不是更广)的网络,我们可以更容易地逼近许多函数。我们将在后面的章节中进行更细致的讨论。
激活函数
--------
:label:``subsec:activation-functions``
激活函数通过计算加权和并加上偏置来确定神经元是否应该被激活。它们是将输入信号转换为输出的可微运算。大多数激活函数都是非线性的。由于激活函数是深度学习的基础,下面(\ **简要介绍一些常见的激活函数**)。
ReLU函数
~~~~~~~~
最受欢迎的选择是\ *线性整流单元*\ (Rectified linear
unit,\ *ReLU*\ ),因为它实现简单,同时在各种预测任务中表现良好。
[**ReLU提供了一种非常简单的非线性变换**]。
给定元素\ :math:`x`\ ,ReLU函数被定义为该元素与\ :math:`0`\ 的最大值:
(\ **
.. math:: \operatorname{ReLU}(x) = \max(x, 0).
**\ )
通俗地说,ReLU函数通过将相应的激活值设为0来仅保留正元素并丢弃所有负元素。为了直观感受一下,我们可以画出函数的曲线图。正如从图中所看到,激活函数是分段线性的。
.. |一个单隐藏层的多层感知机,具有5个隐藏单元| image:: http://d2l.ai/_images/mlp.svg
.. code:: java
%load ../utils/djl-imports
%load ../utils/plot-utils
.. code:: java
NDManager manager = NDManager.newBaseManager();
NDArray x = manager.arange(-8.0f, 8.0f, 0.1f);
x.setRequiresGradient(true);
NDArray y = Activation.relu(x);
// Converting the data into float arrays to render them in a plot.
float[] X = x.toFloatArray();
float[] Y = y.toFloatArray();
Table data = Table.create("Data").addColumns(
FloatColumn.create("X", X),
FloatColumn.create("relu(x)", Y)
);
render(LinePlot.create("", data, "x", "relu(X)"), "text/html");
.. raw:: html