5.1. 多层感知机¶ 在 SageMaker Studio Lab 中打开 Notebook
在 第 4.1 节中,我们介绍了softmax回归,我们从零开始实现了该算法(第 4.4 节),并使用了高级API(第 4.5 节)。这使我们能够训练能够识别低分辨率图像中10类服装的分类器。在此过程中,我们学习了如何处理数据,将我们的输出强制转换为有效的概率分布,应用适当的损失函数,并根据模型的参数来最小化它。现在我们已经掌握了简单线性模型背景下的这些机制,我们可以开始探索深度神经网络,这是本书主要关注的相对丰富的模型类别。
%matplotlib inline
import torch
from d2l import torch as d2l
%matplotlib inline
from mxnet import autograd, np, npx
from d2l import mxnet as d2l
npx.set_np()
%matplotlib inline
import jax
from jax import grad
from jax import numpy as jnp
from jax import vmap
from d2l import jax as d2l
No GPU/TPU found, falling back to CPU. (Set TF_CPP_MIN_LOG_LEVEL=0 and rerun for more info.)
%matplotlib inline
import tensorflow as tf
from d2l import tensorflow as d2l
5.1.2. 激活函数¶
激活函数通过计算加权和并进一步加上偏置来决定神经元是否应该被激活。它们是将输入信号转换为输出的可微算子,同时它们中的大多数都增加了非线性。因为激活函数是深度学习的基础,让我们简要地回顾一些常见的激活函数。
5.1.2.1. ReLU 函数¶
最受欢迎的选择是修正线性单元(ReLU)(Nair and Hinton, 2010),这既因为它的实现简单,也因为它在各种预测任务上的良好性能。ReLU 提供了一个非常简单的非线性变换。给定一个元素 \(x\),该函数定义为该元素与 \(0\) 的最大值
通俗地说,ReLU函数只保留正元素,并通过将相应的激活设置为0来丢弃所有负元素。为了获得一些直观的理解,我们可以绘制该函数。如你所见,激活函数是分段线性的。
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))
x = np.arange(-8.0, 8.0, 0.1)
x.attach_grad()
with autograd.record():
y = npx.relu(x)
d2l.plot(x, y, 'x', 'relu(x)', figsize=(5, 2.5))
[21:54:14] ../src/storage/storage.cc:196: Using Pooled (Naive) StorageManager for CPU
x = jnp.arange(-8.0, 8.0, 0.1)
y = jax.nn.relu(x)
d2l.plot(x, y, 'x', 'relu(x)', figsize=(5, 2.5))
x = tf.Variable(tf.range(-8.0, 8.0, 0.1), dtype=tf.float32)
y = tf.nn.relu(x)
d2l.plot(x.numpy(), y.numpy(), 'x', 'relu(x)', figsize=(5, 2.5))
当输入为负时,ReLU函数的导数为0;当输入为正时,ReLU函数的导数为1。请注意,当输入值恰好等于0时,ReLU函数是不可微的。在这些情况下,我们默认使用左侧导数,并称当输入为0时导数为0。我们可以这样做,因为输入可能永远不会真正为零(数学家会说它在测度为零的集合上不可微)。有句老话说,如果细微的边界条件很重要,我们可能正在做(真正的)数学,而不是工程。这个传统智慧可能在这里适用,或者至少,我们没有在执行约束优化 (Mangasarian, 1965, Rockafellar, 1970)。下面我们绘制了ReLU函数的导数。
y.backward(torch.ones_like(x), retain_graph=True)
d2l.plot(x.detach(), x.grad, 'x', 'grad of relu', figsize=(5, 2.5))
y.backward()
d2l.plot(x, x.grad, 'x', 'grad of relu', figsize=(5, 2.5))
[21:54:14] ../src/base.cc:48: GPU context requested, but no GPUs found.
grad_relu = vmap(grad(jax.nn.relu))
d2l.plot(x, grad_relu(x), 'x', 'grad of relu', figsize=(5, 2.5))
with tf.GradientTape() as t:
y = tf.nn.relu(x)
d2l.plot(x.numpy(), t.gradient(y, x).numpy(), 'x', 'grad of relu',
figsize=(5, 2.5))
使用ReLU的原因是它的导数表现得特别好:要么消失,要么直接让参数通过。这使得优化过程表现得更好,并且缓解了困扰早期神经网络的梯度消失问题(稍后会详细介绍)。
请注意,ReLU函数有许多变体,包括参数化ReLU(pReLU)函数 (He et al., 2015)。这种变体为ReLU添加了一个线性项,所以即使参数为负,一些信息仍然可以通过
5.1.2.2. Sigmoid 函数¶
sigmoid 函数将值域在 \(\mathbb{R}\) 内的输入转换为在区间 (0, 1) 内的输出。因此,sigmoid 通常被称为挤压函数:它将范围在(-inf, inf)的任何输入挤压到范围在(0, 1)的某个值
在最早的神经网络中,科学家们对模拟生物神经元感兴趣,这些神经元要么激活要么不激活。因此,这个领域的先驱们,一直追溯到人工神经元的发明者McCulloch和Pitts,都专注于阈值单元 (McCulloch and Pitts, 1943)。阈值激活函数当其输入低于某个阈值时取值为0,当输入超过阈值时取值为1。
当注意力转向基于梯度的学习时,sigmoid函数是一个自然的选择,因为它是对阈值单元的光滑、可微的近似。当我们想将输出解释为二元分类问题的概率时,Sigmoid仍然被广泛用作输出单元的激活函数:你可以将sigmoid视为softmax的一个特例。然而,对于隐藏层中的大多数用途,sigmoid在很大程度上已经被更简单且更容易训练的ReLU所取代。这很大程度上是因为sigmoid对优化提出了挑战 (LeCun et al., 1998),因为它的梯度对于大的正负参数都会消失。这可能导致难以摆脱的平台期。尽管如此,sigmoid仍然很重要。在后续章节(例如,第 10.1 节)关于循环神经网络中,我们将描述利用sigmoid单元来控制信息在时间上传递的架构。
下面,我们绘制了sigmoid函数。请注意,当输入接近0时,sigmoid函数接近一个线性变换。
y = torch.sigmoid(x)
d2l.plot(x.detach(), y.detach(), 'x', 'sigmoid(x)', figsize=(5, 2.5))
with autograd.record():
y = npx.sigmoid(x)
d2l.plot(x, y, 'x', 'sigmoid(x)', figsize=(5, 2.5))
y = jax.nn.sigmoid(x)
d2l.plot(x, y, 'x', 'sigmoid(x)', figsize=(5, 2.5))
y = tf.nn.sigmoid(x)
d2l.plot(x.numpy(), y.numpy(), 'x', 'sigmoid(x)', figsize=(5, 2.5))
sigmoid函数的导数由以下方程给出
下面绘制了sigmoid函数的导数。请注意,当输入为0时,sigmoid函数的导数达到最大值0.25。当输入在任一方向上偏离0时,导数接近0。
# 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 sigmoid', figsize=(5, 2.5))
y.backward()
d2l.plot(x, x.grad, 'x', 'grad of sigmoid', figsize=(5, 2.5))
grad_sigmoid = vmap(grad(jax.nn.sigmoid))
d2l.plot(x, grad_sigmoid(x), 'x', 'grad of sigmoid', figsize=(5, 2.5))
with tf.GradientTape() as t:
y = tf.nn.sigmoid(x)
d2l.plot(x.numpy(), t.gradient(y, x).numpy(), 'x', 'grad of sigmoid',
figsize=(5, 2.5))
5.1.2.3. Tanh 函数¶
与sigmoid函数类似,tanh(双曲正切)函数也将其输入压缩,将它们转换为在 \(-1\) 和 \(1\) 之间的区间上的元素
我们在下面绘制了tanh函数。注意,当输入接近0时,tanh函数接近一个线性变换。虽然该函数的形状与sigmoid函数相似,但tanh函数关于坐标系原点表现出点对称性 (Kalman and Kwasny, 1992)。
y = torch.tanh(x)
d2l.plot(x.detach(), y.detach(), 'x', 'tanh(x)', figsize=(5, 2.5))
with autograd.record():
y = np.tanh(x)
d2l.plot(x, y, 'x', 'tanh(x)', figsize=(5, 2.5))
y = jax.nn.tanh(x)
d2l.plot(x, y, 'x', 'tanh(x)', figsize=(5, 2.5))
y = tf.nn.tanh(x)
d2l.plot(x.numpy(), y.numpy(), 'x', 'tanh(x)', figsize=(5, 2.5))
tanh函数的导数是
它被绘制在下面。当输入接近0时,tanh函数的导数接近最大值1。正如我们对sigmoid函数所看到的那样,当输入在任一方向上远离0时,tanh函数的导数接近0。
# 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))
y.backward()
d2l.plot(x, x.grad, 'x', 'grad of tanh', figsize=(5, 2.5))
grad_tanh = vmap(grad(jax.nn.tanh))
d2l.plot(x, grad_tanh(x), 'x', 'grad of tanh', figsize=(5, 2.5))
with tf.GradientTape() as t:
y = tf.nn.tanh(x)
d2l.plot(x.numpy(), t.gradient(y, x).numpy(), 'x', 'grad of tanh',
figsize=(5, 2.5))
5.1.3. 总结与讨论¶
我们现在知道如何结合非线性来构建富有表现力的多层神经网络架构。顺便说一句,你的知识已经让你掌握了与1990年左右的从业者类似的工具集。在某些方面,你比那时工作的任何人都更有优势,因为你可以利用强大的开源深度学习框架,仅用几行代码就能快速构建模型。以前,训练这些网络需要研究人员用C、Fortran甚至Lisp(在LeNet的情况下)显式地编写层和导数。
第二个好处是,ReLU比sigmoid或tanh函数更易于优化。可以说,这是过去十年帮助深度学习复兴的关键创新之一。不过,请注意,激活函数的研究并未停止。例如,Hendrycks and Gimpel (2016) 提出的GELU(高斯误差线性单元)激活函数 \(x \Phi(x)\)(其中 \(\Phi(x)\) 是标准高斯累积分布函数)和 Ramachandran et al. (2017) 提出的Swish激活函数 \(\sigma(x) = x \operatorname{sigmoid}(\beta x)\) 在许多情况下可以产生更好的准确性。
5.1.4. 练习¶
证明向一个线性深度网络(即没有非线性\(\sigma\)的网络)添加层永远不会增加网络的表达能力。举一个它实际上会降低表达能力的例子。
计算pReLU激活函数的导数。
计算Swish激活函数 \(x \operatorname{sigmoid}(\beta x)\) 的导数。
证明一个仅使用ReLU(或pReLU)的MLP构造了一个连续分段线性函数。
Sigmoid和tanh非常相似。
证明 \(\operatorname{tanh}(x) + 1 = 2 \operatorname{sigmoid}(2x)\)。
证明由两种非线性参数化的函数类是相同的。提示:仿射层也有偏置项。
假设我们有一个一次只应用于一个小批量的非线性,比如批量归一化 (Ioffe and Szegedy, 2015)。你预期这会引起什么样的问题?
给出一个sigmoid激活函数的梯度消失的例子。