本节从神经网络的发展着手,依次介绍激活函数、前向算法、损失函数、反向传播算法以及PyTorch中的数据处理,最后使用PyTorch解决一个iris数据集上的多分类问题。通过本节的学习,我们将对整个神经网络的流程有一个比较全面的认识。
1.神经元与神经网络
神经元最早是生物学上的概念,它是人脑中的最基本单元。人脑中含有大量的神经元,米粒大小的脑组织中就包含超过10000个神经元,不同的神经元之间相互连接,每个神经元与其他的神经元平均有6000个连接。一个神经元接收其他神经元传递过来的信息,通过某种方式处理后再传递给其他神经元。下图就是生物神经元的示意图。
一个神经元由细胞核、树突、轴突和轴突末梢等组成。其中树突有很多条,且含有不同的权重,主要用来接收从其他神经元传来的信息;接收到的信息在细胞整合后产生新的信息传递给其他神经元;而轴突只有一条,轴突末端有许多神经末梢,可以给其他神经元传递信息。神经末梢跟其他神经元的树突连接,从而传递信号,这个链接的位置在生物学上叫做“突触”。
在对人脑工作机理研究的基础上,1943年McCulloch和Pitts参考了生物神经元的结构,最早提出了人工神经元模型,即MP神经元模型。MP神经元从外部或者其他神经元接受输入信息,通过特定的计算得到输出结果。如下图所示,输入X1,X2,对应权重w1,w2,偏置b,通过加权求和代入f(z)函数中,得到输出Y。这个函数f(z)就是激活函数。人工神经元是人工神经网络中最基本的单元。
MP模型虽然简单,但却是构建神经网络的基础。神经网络(Neural Network,NN)是人工神经网络(Artificial Neural Network,ANN)的简称,由很多神经元组成。神经网络是对人脑工作机制的一种模仿。在MP模型中,权重的值都是预先设置的,因此不能学习。1949年Hebb提出了Hebb学习率,认为人脑神经细胞的突触上的强度是可以变化的。于是研究者开始考虑使用调整权值的方法来让机器学习。
1958年,Rosenblatt提出了由两层(输入层和输出层)神经元组成的神经网络,名叫“感知机”,如下图所示。从结构上,感知机把神经元中的输入变成了单独的神经元,成为输入单元。与神经元模型不同,感知机中的权重是通过训练得到的。感知机类似一个逻辑回归模型,可以做线性分类任务,是首个可以学习的人工神经网络。这为后面的学习算法奠定了基础,可以说感知机是神经网络的基石。但由于它只有一层功能神经单元,因此学习能力非常有限。Minsky在1969年出版了一本名为“Perceptron”的书,使用数学方法详细地证明了感知机的弱点,尤其是感知机对XOP(异或)这样的简单分类任务都无法解决。
感知机是前馈神经网络的一种,前馈神经网络实最早起也是最简单的一种人工神经网络。前馈神经网络包含多个神经元,被安排在不同的层,即输入层、隐含层、输出层,其中隐含层的个数有0个或多个。在前馈神经网络中,信息在神经元上的传播方向只有一个——向前,即从输入层经过隐含层到达 输出层,神经元之间没有循环结构。感知机就是没有隐含层的前馈神经网络。拥有一个或多个隐含层的前馈神经网络称为“多层感知机”(Multi Layer Perceotron,MLP),如下图所示。
多层感知机可以很好地解决非线性可分问题,我们通常将多层感知机这样的多层结构称为神经网络。
所谓神经网络的训练或者学习,其主要目的就是通过学习算法得到神经网络解决指定问题所需的参数。这里的参数包括各层神经元之间的连接权重以及偏置等。参数的确定需要神经网络通过训练样本和学习算法来迭代找到最优参数组合。说起神经网络的学习算法,不得不提其中最杰出、最成功的代表——反向传播算法。
2.激活函数
前面提到过在神经元中,输入信息通过一个非线性函数y=f(x)产生输出,这个函数决定哪些信息保留以传递给后面的神经元,这个函数就是激活函数(Activation Function),又被称为非线性函数(Nonlinearity Function),对于给定的输入,激活函数执行固定的数学运算得到输出结果,根据输出结果控制输入信息的保留程度。
激活函数要具有以下性质:
- 非线性:当激活函数是线性时,一个两层的神经网络基本上就可以表达所有的函数了,恒等函数f(x)=x不满足这个条件,如果MLP中使用恒等激活函数,那么整个神经网络跟单层的神经网络是等价的。为什么需要非线性呢?因为线性的叠加还是线性,而线性函数的表达能力有限,只能做线性可分的任务。对于线性不可分的更复杂的问题,比如说playground上的一些问题,线性不可分,所以需要用到非线性激活函数。
- 连续可微性:在训练神经网络的过程中,使用到了梯度下降,所以连续可微性是必要的。ReLU虽然不连续,但是也同样适合做激活函数。
- 值域是有限的:激活函数的值域是有限的时候,基于梯度下降的训练过程才能越来越稳定,因为特征表示受有限值的影响更加有效。
- 单调性:激活函数是单调的时候,单层的神经网络才能保证是凸函数。
- 具有单调导数的光滑函数:在某些情况下,这些已经被证明可以更好地概括。对这些性质的论证表名,这种激活函数与奥卡姆剃刀原理(简单有效原理)更加一致。
- 函数值和输入近似相等:满足这个条件的激活函数,当权重初始化成很小的随机数时,神经网络的训练将会很高效,如果不满足这个条件则需要很小心的初始化神经网络权重。
下面介绍几种常见的激活函数:Sigmoid、Tanh、Hard Tanh、ReLU、Softmax、LogSoftmax等。
1.Sigmoid
Sigmoid是一种很常用的非线性函数,其公式如下:
f ( x ) = δ ( x ) = 1 1 + e − x f(x)=\delta(x)=\frac{1}{1+e^{-x}} f(x)=δ(x)=1+e−x1
因其形状像S,又称S函数,将输入变量映射到(0,1)之间,对于特别大的输入,其输出结果是1;对于特别小的输入,其输出结果是0。早期在各类任务中应用广泛,但是现在只在某些特定的场合使用,因为它有自身的缺点:
- 梯度消失:从图形上可以看出,但输入变量特别大或者特别小的时候,函数曲线变化趋于平缓,也就是说函数的梯度变得越来越小,直到接近于0。这会导致经过神经元的信息很少。
- 非0均值:输出值不是0均值的,这样在后面的神经元上将得到非0均值的输入。如果进入神经元的数据时正的,在反向传播中权重上的梯度也永远是正的。这会导致权重梯度的更新呈现锯齿形态,这是不可取的。通过batch的权重和可能最终会得到不同的符号,可以得到缓解。比起梯度消息,这个问题不那么严重。
- 计算量大:因为其函数求导涉及除法,在神经网络的反向传播求梯度时,计算量很大。
PyTorch中Sigmoid的定义为torch.nn.Sigmoid,对输入的每个元素执行Sigmoid函数,输出的维度和输入的维度相同:
>>> import torch
>>> import torch.nn as nn
>>> import torch.autograd as autograd
>>> input_data = autograd.Variable(torch.randn(2))
>>> print(input_data)
tensor([-0.3765, 1.1710])
>>> m = nn.Sigmoid()
>>> print(m(input_data))
tensor([0.4070, 0.7633])
2.Tanh
Tanh是一个双曲三角函数,其公式如下:
f ( x ) = t a n h ( x ) = e x − e − x e x + e − x f(x)=tanh(x)=\frac{e^x-e^{-x}}{e^x+e^{-x}} f(x)=tanh(x)=ex+e−xex−e−x
从图像上可以看出,Tanh与Sigmoid不同,它将输入变量映射到(-1,1)之间,它是Sigmoid函数经过简单的变换得到的:
t a n h ( x ) = 2 δ ( 2 x ) − 1 tanh(x)=2\delta(2x)-1 tanh(x)=2δ(2x)−1
Tanh是0均值的,这一点要比Sigmoid好,所以实际应用中效果也会比Sigmoid好,但是它仍然没有解决梯度消失的问题,这点可以从图像上很清楚地看出来。
PyTorch中Tanh的定义:torch.nn.Tanh。输出的维度和输入的维度也是相同的:
>>> input_data = autograd.Variable(torch.randn(2))
>>> print(input_data)
tensor([ 0.5413, -0.8901])
>>> Tanh = nn.Tanh()
>>> print(Tanh(input_data))
tensor([ 0.4940, -0.7114])
注意:由于梯度消失的原因,不推荐在隐含层使用Sigmoid和Tanh函数,但是可以在输出层使用;如果有必要使用它们的时候,记住:Tanh要比Sigmoid好,因为Tanh是0均值的。
3.Hard Tanh
和Tanh类似,Hard Tanh同样把输入变量映射到(-1,1)之间,不同的是,映射的时候不再是通过公式计算,而是通过给定的阈值直接到达最终结果。标准的Hard Tanh把所有大于1的输入变成1,所有小于-1的输入变成-1,其他的输入不变:
KaTeX parse error: Unknown column alignment: * at position 39: … \begin{array}{*̲*lr**} …
PyTorch中Hard Tanh支持指定阈值min_val和max_val,以改变输出的最小值和最大值,比如说对于f=Hardtanh(-2,2)的输出,所有大于2的输入的输出都是2,所有小于-2的输入的输出都是-2,其他原样输出。
>>> input_data = autograd.Variable(torch.randn(2))
>>> print(input_data)
tensor([ 0.2704, -1.3050])
>>> Hardtanh = nn.Hardtanh()
>>> print(Hardtanh(input_data))
tensor([ 0.2704, -1.0000])
4.ReLU
线性整流函数(Rectified Linear Unit,ReLU)又称为修正线性单元。ReLU是一个分段函数,其公式如下:
f ( x ) = m a x ( 0 , x ) f(x)=max(0,x) f(x)=max(0,x)
ReLU做的事情很简单,大于0的数原样输出,小于0的数输出0。ReLU在0点处虽然连续不可导,但是也同样适合做激励函数。
ReLU的优点如下:
- 相对于Sigmoid、Tanh而言,ReLU更简单,只需要设置一个阈值就可以计算结果,不用复杂的运算。
- ReLU在随机梯度下降的训练中收敛会更快,原因是ReLU是非饱和的(non-saturating)。
ReLU在很多任务中都有出色的表现,是目前应用广泛的激活函数。但是它也不是十分完美的:ReLU单元很脆弱,以至于在训练过程中可能出现死亡现象,即经过一段时间的训练,一些神经元不再具有有效性,只会输出0,特别是使用较大的学习率的时候。如果发生这种情况,神经元的梯度将永远是0,不利于训练。
一个很大的梯度流过ReLU神经元,权重更新后,神经元就不会再对任何数据有效,如果这样,经过这个点的梯度将永远是0。也就是说,在训练过程中,ReLU单元会不可逆的死亡。如果学习率设置得太高,网络中会有40%可能是死亡的,即整个训练数据集中没有激活的神经元。设置一个合适的学习率可以减少这种情况的发生。
PyTorch中的ReLU函数有一个inplace参数,用于选择是否进行覆盖运算,默认为False。在PyTorch中应用ReLU:
>>> input_data = autograd.Variable(torch.randn(2))
>>> print(input_data)
tensor([ 0.8689, -1.1169])
>>> ReLU = nn.ReLU()
>>> print(ReLU(input_data))
tensor([0.8689, 0.0000])
ReLU的成功应用是在生物学的研究上。生物学研究表明:生物神经不是对所有的外界信息都做出反应,而是部分,即对一部分信息进行忽略,对应于输入信息小于0的情况。
5.ReLU的扩展
为了解决ReLU函数存在的问题,研究者提出了在ReLU基础上的优化方案。在基于ReLU的扩展中,主要思路是当输入是小于0的值时,不再一味地输出0,而是通过一个很小的斜率α的直线方程计算结果,根据α取值的不同可以分为以下几种方案。
(1)Leaky ReLU
使用参数α决定泄露(leak)的程度,就是输入值小于0时直线的斜率,α是固定的取值,而且很小,一般取值为0.01。这样可以保证在输入信息小于0的时候也有信息通过神经元,神经元不至于死亡。
Leaky ReLU函数公式为:
KaTeX parse error: Unknown column alignment: * at position 42: … \begin{array}{*̲*lr**} …
其中,α是一个很小的常数,比如α=0.01。
在PyTorch中,Leaky ReLU有两个参数:
- negative_slope:控制负斜率的角度,即公式中的alpha,默认值是1e-2。
- inplace:选择是否进行覆盖运算,默认值为False。
(2)Parametric ReLU
对于输入的每一个元素运用函数PReLU(x)=max(0,x)+α*min(0,x),这里的α是自学习的参数。当不带参数的调用时,nn.PReLU()在所有输入通道中使用同样的参数α;如果用nn.PReLU(nChannels)调用,α将应用到每个输入:
KaTeX parse error: Unknown column alignment: * at position 51: … \begin{array}{*̲*lr**} …
(3)Randomized ReLU
这是Leaky ReLU的random版本,即参数α是随机产生的,RReLU是在Kaggle的NDSB比赛中首次被提出的,其核心思想就是:在训练过程中,α是从一个高斯分布U(l,u)中随机生成的,然后在测试过程中进行修正。
以上三种ReLU扩展的比较:(这里的a_i就是α)
- ReLU,对小于0部分,直接置为0;
- Leaky ReLU,对小于0部分,进行这样的转换:y_i=a_i * x_i ,它的a_i是固定的;
- PReLU中的a_i 根据数据变化而变化;
- RReLU中的a_i是一个在一个给定的范围内随机抽取的值,这个值在测试环节就会固定下来。
(4)Exponential Linear Unit(ELU)
这是一个新的激活函数,效果比所有的ReLU的变形都要好,训练用的时间少,而且测试指标高。
KaTeX parse error: Unknown column alignment: * at position 55: … \begin{array}{*̲*lr**} …
关于ELU中参数α的选择,通常设置为1,当然也可以在实际应用中尝试其他的值,而且整个函数是平滑的,在x=0会加速梯度下降,因为在x=0处不用跳跃。
其缺点是,因为使用了指数,计算比ReLU系列的计算速度慢,但是训练时收敛快。
(5)Maxout
Maxout是Ian J.Goodfellow在2013年提出的。Maxout和Dropout结合后,在MNIST、CIFAR-10、CIFAR-100、SVHN这4个数据集上都取得了start-of-art的识别率。前面介绍的激活函数都是作用于输入信息的一个元素,输入信息之间是无关的。Maxout单元不是作用于每个元素的函数g(x),而是将x划分成具有k个值的组,然后输出其中一组最大的元素。Maxout有很强的拟合能力,在足够多隐藏层的情况下可以拟合任意的凸函数。
它具有ReLU函数的优点(不会饱和,计算简单),却没有ReLU函数的缺点(容易死亡),它的唯一缺点就是每个神经元都有k个权重,导致权重的总数大大增加。
PyTorch中还没有Maxout的实现,如果想尝试使用,可以参考一下PyTorch在GitHub上的一个例子。
6.Softmax
Softmax函数又称为归一化指数函数,是Sigmoid函数的一种推广。它能将一个含有任意实数的 K K K维向量 z z z压缩到另一个 K K K维向量 σ ( z ) \sigma(z) σ(z)中,返回的是每个互斥输出类别上的概率分布,使得每个元素的范围都在(0,1)之间,并且所有元素的和为1。公式如下:
f ( x i ) = e x p ( x i ) ∑ j e x p ( x j ) f(x_i)=\frac{exp(x_i)}{\sum_{j}exp(x_j)} f(xi)=∑jexp(xj)exp(xi)
跟数学中的max函数相比,max函数取一组数中的最大值,这样会导致较小的值永远不会被取到。Softmax很自然地表示具有K个可能值的离散随机变量的概率分布,所以可以用作一种开关,其中越大的数概率也就越大,Softmax和Maxout一样不是作用于单个神经网络中的每个x值,对n维输入张量运用Softmax函数,将张量的每个元素缩放到(0,1)之间,并且各个输出的和为1。Softmax一般用在网络的输出层,比如在多分类的输出值表示属于每个种类的概率。
PyTorch中关于Softmax的定义:
class torch.nn.Softmax(dim=None)
它接收一个dim参数,以指定计算Softmax的维度,在给定的维度上各个输出的和为1。例如:如果输入的是一个二维张量,dim=0时,表示按列计算Softmax值,每一列上的和为1;dim=1表示按行计算Softmax值,每一行上的和为1。使用Softmax必须显式给出dim,否则结果不确定。
在PyTorch中应用Softmax激活函数:
>>> input_data = autograd.Variable(torch.randn(2,3))
>>> print(input_data)
tensor([[-0.3776, 0.6697, 0.7186],
[-1.0892, 0.1614, 1.2264]])
>>> Softmax = nn.Softmax(dim=1)
>>> print(Softmax(input_data))
tensor([[0.1461, 0.4165, 0.4374],
[0.0684, 0.2388, 0.6928]])
7.LogSoftmax
在应用Softmax函数前,对输入应用对数函数,就是LogSoftmax,公式如下:
f ( x i ) = l o g e x p ( x i ) ∑ j e x p ( x j ) f(x_i)=log\frac{exp(x_i)}{\sum_{j}exp(x_j)} f(xi)=log∑jexp(xj)exp(xi)
PyTorch中的定义同样接受一个dim参数,含义和用法跟Softmax相同,这里不再赘述。
PyTorch中更多的预定义激活函数详见PyTorch的官方文档。
3.前向算法
当我们使用前馈神经网络接收输入 x x x,并产生输出 y ^ \hat y y^时,信息通过网络向前流动。输入 x x x提供初始信息,向输出的方向传播到每一层的神经元,并跟相应的权重做运算,最终产生输出 y ^ \hat y y^,这个过程称为前向传播(Forward Propagation)。
下面结合图示详细说明前向传播过程。
上图所示是一个只有4层的神经网络,其中第1层为输入层,第4层为输出层,第2层和第3层为隐含层。 x 1 x_1 x1和 x 2 x_2 x2是输入层中的两个神经元, h i k h_{i}^{k} hik表示第 k k k个隐含层中的第 i i i个神经