建立隐藏层及损失函数
引用翻译:《动手学深度学习》
通过多类逻辑回归(也称为softmax回归),将服装图像分为10个可能的类别。
为了达到这个目的,我们必须学会如何处理数据,将我们的输出转化为一个有效的概率分布(通过 “softmax”)。
如何应用一个适当的损失函数,以及如何对我们的参数进行优化。现在,我们已经涵盖了这些初步的内容。
我们就可以把注意力集中在使用深度神经网络设计强大的模型这一更令人兴奋的事业,使用深度神经网络设计强大的模型。
一、隐藏层
回顾一下,对于线性回归和softmax回归。我们通过单一的线性变换将我们的输入直接映射到我们的输出.
如果我们的标签真的与我们的输入数据有近似的线性函数关系,那么这种方法将是完美的。但是,线性是一个强有力的假设。线性意味着,无论我们试图预测什么目标值,增加每一个输入的值都会推动输出的值上升或下降,而不考虑其他输入的值。
有时这是有道理的。例如,我们试图预测一个人是否会偿还贷款。我们可以合理地想象,在其他条件相同的情况下,收入较高的申请人比收入较低的申请人更有可能还款。在这些情况下,线性模型可能表现良好,甚至很难被打败。
但是,在FashionMNIST中对图像进行分类的情况如何?增加(13,17)位置的像素的强度是否应该总是增加该图像描绘的是一个口袋书的可能性?这似乎很荒谬,因为我们都知道,如果不考虑像素之间的相互作用,你就无法从图像中获得意义。
二、线性模型的问题
作为另一个案例,考虑尝试对图像进行分类,根据它们是否描绘了猫或狗的黑白图像进行分类。
如果我们使用一个线性模型,我们基本上会说,对于每个像素,增加它的值(使其更白),一定会增加该图像描述狗的概率
或一定会增加图像中的猫的概率。我们会做出一个荒唐的假设,即区分猫和狗的唯一要求是区分猫和狗的唯一要求就是评估它们有多亮。
这种方法在包含黑狗和黑猫的作品中是注定要失败的。既有黑狗又有黑猫,以及白狗和白猫。
要弄清图像中所描述的内容,通常需要允许我们的输入和输出之间有更复杂的关系。
因此,我们需要有能力发现模式的模型,这些模式可能是由许多特征之间的相互作用来表征的。
我们可以克服线性模型的这些限制,并处理一类更普遍的函数。通过加入一个或多个隐藏层,最简单的方法是将许多神经元层堆叠在一起。
每一层都会反馈到上面的一层,直到我们产生一个输出。这种结构通常被称为 多层感知器。通常缩写为MLP。
MLP的神经网络图看起来像这样。
上面的多层感知器有4个输入和3个输出,中间的隐藏层包含5个隐藏单元。由于输入层不涉及任何计算,构建这个网络将包括实现2层计算。输入层的神经元与隐藏层的输入完全相连。同样地,隐藏层的神经元也完全连接到输出层的神经元。
三、从线性到非线性
我们可以用数学符号写出定义这个单隐层MLP的计算结果,如下:
通过增加一层,我们增加了两组新的参数,但我们得到了什么交换?在上面定义的模型中,我们并没有为我们的麻烦取得任何成果!
我们的隐藏单元只是输入的线性函数,而输出(pre-softmax)也只是隐藏单元的线性函数。
一个线性函数的线性函数本身就是一个线性函数。
这意味着对于任何权重值,我们都可以用𝐖=𝐖2𝐖1和𝐛=𝐖2𝐛1+𝐛2来折叠出隐藏层,产生一个等效的单层模型。 和 .(线性函数的线性函数本身就是一个线性函数)
为了从多层结构中获得好处,我们需要另一种关键成分–非线性𝜎,在每一层的线性变换之后应用于每个隐藏单元。现在最流行的非线性选择是整流线性单元(ReLU)max(𝑥,0) 。在加入这些非线性之后,就不可能再合并层了。
显然,我们可以继续将这样的隐藏层堆叠起来,例如.
和
相互叠加,得到一个真正的多层次感知器。
多层感知器可以说明输入中复杂的相互作用,因为隐藏的神经元取决于每个输入的值。很容易设计出一个能进行任意计算的隐藏节点,例如,对其输入进行逻辑运算。此外,对于激活函数的某些选择,众所周知,多层感知器是通用近似器。这意味着,即使是单层神经网络,只要有足够的节点和正确的权重集,我们就可以对任何函数进行建模。实际上,学习这个函数才是最难的部分。
此外,单层网络可以学习任何函数并不意味着你应该尝试用单层网络解决所有的问题。事实证明,如果我们使用更深(相对于更宽)的神经网络,我们可以更紧凑地近似许多函数。我们将在随后的章节中更多地讨论数学问题,但现在让我们实际建立一个MLP。在这个例子中,我们将实现一个有两个隐藏层和一个输出层的多层感知器。
四、矢量化和小批量
如前所述,通过矩阵𝐗,我们表示一个小批量的输入。因此,从一个有两个隐藏层的MLP产生输出的计算可以表示为:
通过一些符号的滥用,我们定义非线性𝜎以逐行的方式应用于其输入,即一次一个观察。请注意,我们也是以同样的方式使用softmax的符号来表示逐行操作的。
通常情况下,就像本章一样,我们应用于隐藏层的激活函数不仅仅是按行计算,而是按组件计算。
这意味着在计算层的线性部分后,我们可以计算每个节点的激活,而不需要看其他隐藏单元的值。这对大多数激活函数来说都是如此(将在 :numref:chapter_batch_norm 中介绍的批量归一化操作是该规则的一个明显例外)。
五、激活函数
因为它们是深度学习的基础,在进一步讨论之前,让我们简单看看一些常见的激活函数。
1、ReLU函数
如上所述,最受欢迎的选择是整流线性单元(ReLU),因为它的实现简单,在训练中也很有效。ReLU提供了一个非常简单的非线性变换。给定元素𝑧,该函数被定义为该元素和0的最大值。
可以这样理解,ReLU函数只保留正元素,放弃负元素(将这些节点设置为0)。为了更好地了解它的样子,我们可以绘制它。为方便起见,我们定义了一个绘图函数xyplot来处理这些基础工作。
import sys
sys.path.insert(0, '..')
import numpy
import matplotlib.pyplot as plt
import torch
import torch.nn as nn
from torch.autograd import Variable
def xyplot(x_vals,y_vals,name):
x_vals=x_vals.detach().numpy() # we can't directly use var.numpy() because varibles might
y_vals=y_vals.detach().numpy() # already required grad.,thus using var.detach().numpy()
plt.plot(x_vals,y_vals)
plt.xlabel('x')
plt.ylabel(name+'(x)')
由于relu通常被用作激活函数,PyTorch支持relu函数作为一个基本的本地运算符。正如你所看到的,激活函数是片面的线性。
# 经过relu后,负的全为0,正的则保留
x=Variable(torch.arange(-8.0,8.0,0.1,dtype=torch.float32).reshape(int(16/0.1),1),requires_grad=True)
y=torch.nn.functional.relu(x) # 经过relu变换
print(x[1:5])
print(y[1:5])
xyplot(x,y,'relu')
tensor([[-7.9000],
[-7.8000],
[-7.7000],
[-7.6000]], grad_fn=<SliceBackward>)
tensor([[0.],
[0.],
[0.],
[0.]], grad_fn=<SliceBackward>)
当输入为负数时,ReLU函数的导数为0,当输入为正数时,ReLU函数的导数为1。注意,当输入的值正好等于0时,ReLU函数是不可微分的。有句老话说,如果微分的边界条件很重要,我们可能在做(真正的)数学,而不是工程。这一传统智慧可能适用于此。请看下面绘制的ReLU函数的导数。
当我们使用.backward()时,默认情况下是.backward(torch.Tensor([1])).当我们处理单一标量输入时,这很有用。但这里我们处理的是矢量输入,所以我们必须使用这个片段。
y.backward(torch.ones_like(x),retain_graph=True)
xyplot(x,x.grad,"grad of relu")
请注意,ReLU函数有很多变体,比如He等人的参数化ReLU(pReLU),2015年。这种变体在ReLU中增加了一个线性项,所以即使参数是负的,一些信息仍然可以通过。
使用ReLU的原因是它的导数表现得特别好–要么它们消失,要么它们只是让参数通过。这使得优化表现得更好,并减少了梯度消失的问题(后面会有更多介绍)。
2、Sigmoid函数
sigmoid函数将其在ℝ中取值的输入转化为区间(0,1)。由于这个原因,sigmoid通常被称为压扁函数:它将范围为(-inf, inf)的任何输入压扁为范围为(0,1)的某个值。
在最早的神经网络中,科学家们对模拟生物神经元感兴趣,这些神经元有的发射有的不发射。因此,这一领域的先驱们,一直追溯到20世纪40年代的McCulloch和Pitts,都专注于阈值化单元。阈值函数要么取值0(如果输入低于阈值),要么取值1(如果输入超过阈值)。
当注意力转移到基于梯度的学习时,sigmoid函数是一个自然的选择,因为它是一个平滑的、可分的阈值单元的近似值。
当我们想把输出解释为二元分类问题的概率时,sigmoid仍然是输出单元上常见的激活函数(你可以把sigmoid看作是softmax的一个特例),但sigmoid大多被更简单、更容易训练的ReLU所取代,大部分用于隐藏层。在 "循环神经网络 "中,将利用sigmoid单元来控制神经网络中的信息流,这要归功于它对0和1之间数值范围的转换能力。
下面绘制的sigmoid函数。当输入接近于0时,sigmoid函数接近于线性变换。
x=Variable(torch.arange(-8.0,8.0,0.1,dtype=torch.float32).reshape(int(16/0.1),1),requires_grad=True)
y=torch.sigmoid(x)
print(y[1:5])
xyplot(x,y,'sigmoid')
tensor([[0.0004],
[0.0004],
[0.0005],
[0.0005]], grad_fn=<SliceBackward>)
sigmoid函数的导数依据链式法则,sigmoid函数的导数:
下面是sigmoid函数的导数图。请注意,当输入为0时,sigmoid函数的导数达到最大值0.25。当输入从0向任何一个方向发散时,导数接近0。
# 越接近0则导数越大,越向两侧则越平缓
y.backward(torch.ones_like(x),retain_graph=True) # 这一步有何意义
print(x[1:5]) # x坐标
print(x.grad[1:5]) # 对x求导后结果
xyplot(x,x.grad,'grad of sigmoid') # 对x进行求导,得到sigmoid的导数图
tensor([[-7.9000],
[-7.8000],
[-7.7000],
[-7.6000]], grad_fn=<SliceBackward>)
tensor([[0.0019],
[0.0020],
[0.0023],
[0.0025]])
3、Tanh 函数
与sigmoid函数一样,tanh(双曲正切)函数也将其输入压扁,将它们转化为-1和1之间的元素。
我们绘制出tanh函数的打击。注意,当输入接近0时,tanh函数接近线性变换。虽然该函数的形状与sigmoid函数相似,但tanh函数表现出关于坐标系原点的点对称性。
x=Variable(torch.arange(-8.0,8.0,0.1,dtype=torch.float32).reshape(int(16/0.1),1),requires_grad=True)
y=torch.tanh(x)
xyplot(x,y,"tanh")
Tanh函数的导数是:
tanh函数的导数被绘制在下面,当输入接近0时,tanh函数的导数接近1的最大值。正如我们在sigmoid函数中看到的那样。当输入在任何一个方向上远离0时,tanh函数的导数接近0。
y.backward(torch.ones_like(x),retain_graph=True)
xyplot(x,x.grad,"grad of tanh")
综上所述,我们现在知道如何结合非线性因素来构建富有表现力的多层神经网络架构。顺便说一句,你现在的知识已经让你掌握了1990年左右的深度学习的技术水平。事实上,你比任何在90年代工作的人都有优势,因为你可以利用强大的开源深度学习框架,只用几行代码就可以快速建立模型。以前,让这些网络进行训练需要研究人员编写成千上万行的C和Fortran代码。
六、总结
- 多层感知器在输出层和输入层之间增加了一个或多个完全连接的隐藏层,并通过一个激活函数对隐藏层的输出进行转换。
- 常用的激活函数包括ReLU函数、sigmoid函数和tanh函数。
七、练习
1、计算tanh和pReLU激活函数的导数。
2、证明只使用ReLU(或pReLU)的多层感知器可以构造一个连续的片状线性函数。
3、证明tanh(𝑥)+1=2sigmoid(2𝑥)。
4、假设我们有一个多层感知器,各层之间没有非线性因素。特别是,假设我们有𝑑个输入维度,𝑑个输出维度,其中一个层只有𝑑/2个维度。说明这个网络的表现力(强大)不如单层感知器。
5、假设我们有一个非线性,一次适用于一个minibatch。你认为这将导致什么样的问题?