前言
感知机可以说是一个跨越了人工智能发展历史的模型,其提出之时引起了非常大的关注,但是随后,大家很快发现其存在严重的局限性,这也导致了AI的第一次寒冬。而经过十几年的停滞,多层感知机的出现解决了感知机存在的问题,并且至今仍在被广泛应用,甚至许多前沿的模型架构中都不乏MLP的身影,这也使得感知机成为了学习深度学习一定绕不开的模型,许多经典的深度学习课程与教材都会将感知机作为第一个模型讲解。本文将顺着感知机的发展历史,讲到目前被广泛使用的多层感知机,并介绍与此相关的一些问题,以及相关的一些技术。
注:本文会涉及到梯度下降与线性回归模型的部分内容,对此不了解的读者可以移步本站的梯度下降博客以及线性回归模型博客。
感知机的发展历史
神经元模型
1943年,心理学家McCulloch和数学家Pitts根据生物神经元的结构,提出了MP神经元模型,模型结构如下图

简单来说就是人类的神经元会接受其他神经元的信号,当信号强度超过某一个阈值之后,该神经元被激活。从数学上模拟这个过程就是,多个输入x1,x2,⋯,xp加权相加,这就是收到的信号强度,然后找一个分段函数,x>0给一个激活值,比如定义为1,否则不激活,比如定义为−1,因为这个函数模拟的是神经元判断信号强度是否达到激活阈值的过程,因此就叫激活函数。整个模型写出来就是
y=ϕ(w0+w1x1+⋯+wpxp)ϕ(x)={1−1x≥0x<0
由于模型只有一个输出,且激活函数会将输出从连续值映射到{−1,1},因此神经元模型被用于处理二分类问题,这与logistic回归中使用sigmoid函数将连续值映射到(0,1)上的设计是类似的,也可以说,神经元模型等价于换了激活函数的logistic回归。
最后,上面的模型结构图是详细版本,后续的模型随着结构的复杂化,一般都会省略偏置以及损失函数,如下图

感知机模型
在神经元模型的基础上,Frank Roseblatt于1957年提出感知机模型,其模型结构与神经元模型完全相同。
此时大家可能会疑惑,模型结构完全不变,这也能算提出一个新模型?其实不然,感知机的贡献在于其给出了一套机器自主学习参数的算法。生活在机器学习与深度学习盛行年代的我们,可能会先入为主的认为模型自主学习参数是一件很平常的事情,但这种操作最早就是由感知机模型提出的,我们来看看当年感知机模型所设计的算法。
感知机的训练方法为,首先初始化所有参数为0,接着取一个样本(x1,x2,⋯,xp,y),计算得到输出y^,如果yy^≤0,按下面的公式更新参数
wi←wi−y^xii=1,2,⋯,pw0←w0−y^
否则不更新参数,循环往复。
这个训练方法的想法也很简单,首先,如果预测值是正确的,那么不更新参数,如果预测值是错误的,例如正确值为1,预测值为−1,此时w0+w1x1+⋯+wpxp必然为负,我们希望其最终输出为1,那就是希望w0+w1x1+⋯+wpxp为正,反映到参数更新上就是我们希望经过参数更新后,w0+w1x1+⋯+wpxp的值变大,那只要所有的wixi与w0均变大,其和必然变大。综上,我们就得到了参数更新的方向,w0要上调,wi根据xi的正负选择上调或者下调,这样才能保证wixi是上调的嘛。现在,再回看上述参数更新公式,就很好理解了。反之亦然,大家可以自行验证。
看完感知机的训练算法,大家可能又会疑惑,模型训练不应该是先选个损失函数,再求解损失函数的最小值吗?而且这个算法怎么看着总有一种梯度下降的既视感?这又是生活在现在的我们先入为主了,在当时是没有把模型训练转化为求解损失函数最小值这种想法的,从算法的设计思路也可以看出来,作者当时就是从结果倒推设计出的这套算法。而看着像梯度下降其实是没错的,从我们现在的角度来看,感知机的这套训练算法等价于定义损失函数为L(w)=max(0,−yy^),使用随机梯度下降进行优化。
XOR问题
可以进行自主学习的感知机在当时引起了大量关注,不少人因此认为人工智能时代要来了。但是,1969年,Marvin Minsky和Seymour Papert在《Perceptrons》一书中指出,感知机无法解决线性不可分问题,例如XOR问题,即异或问题。XOR问题非常简单,假设有两个输入x1与x2,当两个输入正负相同时则为正类,反之则为负类,等价于对两个输出做异或运算,如下表
x1 |
x2 |
y |
+ |
+ |
1 |
+ |
− |
−1 |
− |
+ |
−1 |
− |
− |
1 |
一个最简单的XOR数据集只包含四个样本,如下图

其中,不同颜色代表不同类别。现在,我们需要训练一个感知机模型,用于将这四个样本分开。但是,这是做不到的,因为感知机模型本质上还是一个线性模型,因此,这个问题就相当于,找到一条直线,将上图中的四个点分开,很显然这是无法做到的。

XOR问题的解决思路
历史的车轮滚滚向前,在感知机模型被冷落了十几年之后,19世纪80年代,XOR问题也迎来了解决方法。思路很简单,既然一条直线无法将四个点分开,那么就用两个

如上图,如果我们有两个线性模型,那么就可以将XOR问题的四个样本完美的分开。我们来详细看看其决策过程,输入样本(x1,x2),如果其位于f1上,或者位于f2下,则该样本属于正类1,如果其位于f1下,同时位于f2上,则该样本属于负类−1。不难发现,上述决策过程需要将样本同时输入两个线性模型f1与f2,并且整合f1与f2的输出,得到分类结果。因此,我们就得到了下面的模型结构

构建两个线性模型f1与f2,其输出分别为h1与h2,再整合两个模型的输出得到最终输出y。从模型结构上来说,就是在感知机的基础上多加了一层中间层,这个中间层就被称为隐含层,而引入了隐含层的感知机就被称为多层感知机。
注:XOR问题只是线性不可分问题中的一个代表,因为其足够简单,只要能够解决XOR问题,那么其他的线性不可分问题也可以被解决。例如,对于多层感知机来说,通过增加隐含层的神经元数量,就可以解决更复杂的线性不可分问题。
多层感知机
感知机的发展历史就先告有一段落,现在我们来详细看看**多层感知机(Multi-Layer Perceptron,MLP)**模型,模型结构如下图

其中,输入层的节点数由数据的特征数决定,输出层的节点数由具体的问题类型决定,而隐含层的层数与每层的节点数均为超参数,一般均为人为设定。跟上文感知机的模型结构图相同,每一条连接线均代表一个可以学习的参数,且上述结构图中省略了偏置与损失函数。
注1:关于隐含层超参数的设定,初学者常常会困惑:隐含层的神经元数量为什么是256?调隐含层的超参数是玄学吗?实际上,隐含层超参数确实没有一套非常具体的调整方法,多以经验为主。一般隐含层的神经元数量会设置为2的整数次方,例如128,256,这主要是因为这样的设置有利于计算机的计算。而隐含层的层数不宜设置过深,过深的MLP在训练中会出现严重的梯度问题。而如果设置了多个隐含层,其神经元数量一般会呈递减,例如输入层神经元数量为100,输出层神经元数量为10,使用三个隐含层,其神经元数量可以分别设置为128,64,32,尽量避免神经元数量的大幅度变化。而从宏观层面看,更多的隐含层与神经元数量会带来更强的模型复杂度,因此,如果训练结果呈欠拟合,则增加隐含层层数与神经元数量,反之同理。这些基本就是隐含层超参数的调整方法了。
注2:多层感知机还有很多名字,例如前馈神经网络(Feedforward Neural Network,FNN),在神经网络中常常被叫做全连接层,稠密层,线性层,英文中会叫dense layer,大家基本都是按习惯叫法来,在各种资料中看到这些名称的时候知道跟MLP是一个东西就可以了。
注3:感知机模型扩展至多分类问题的方法与softmax回归是一样的OvR策略,将多层感知机的最后一层隐含层到输出层看作是一个softmax回归就很显然了。
激活函数的作用
在感知机模型以及logistic回归中,损失函数的作用是,在处理分类问题中,将连续值映射为离散值或概率值。而在多层感知机中,对于新引入的隐含层,是否需要使用损失函数?为什么要使用损失函数?这是我们这一小节要讲的问题。
先说结论,在多层感知机中,每一层隐含层都必须使用损失函数。至于原因,我们可以反过来想,如果隐含层不使用损失函数会带来什么问题?就用上面所展示的单隐含层的多层感知机,记x=(x1,x2,⋯,xp)T,h=(h1,h2,⋯,ym)T,y=(y1,y2,⋯,yk)T,三层的多层感知机会产生两个参数矩阵,以及两个偏置向量,这里我们选择将权重与偏置分开写,写在一个矩阵里也是没有问题的。从输入层到隐含层的参数矩阵记为W(1),形状为m×p,偏置向量记为w0(1),形状为m×1,从隐含层到输出层的参数矩阵记为W(2),形状为k×m,偏置向量记为w0(2),形状为k×1,在不使用损失函数的情况下
h=W(1)x+w0(1)y=W(2)h+w0(2)
将上式代入下式可以得到
y=W(2)(W(1)x+w0(1))+w0(2)=W(2)W(1)x+(W(2)w0(1)+w0(2))
不难发现,即使添加了隐含层,拟合出的模型依旧是一个线性模型,权重为W(2)W(1),偏置为W(2)w0(1)+w0(2),而线性模型是无法解决线性不可分问题的,这与我们引入隐含层的初衷不符。
到这里,我们就可以引出损失函数的另一个作用,为模型增加非线性性。由于单层的感知机是一个线性模型,无法解决线性不可分问题,因此我们选择使用添加隐含层的方法来解决,但是隐含层本身并不具有非线性性,所以隐含层需要使用激活函数来为提供非线性性,隐含层不使用激活函数的多层感知机会退化为单层感知机。
常用激活函数
现在,让我们来总结一下损失函数的作用,第一,在输出层中将连续值映射为离散值或概率值,第二,在隐含层中为模型提供非线性变换。基于此,我们可以得到损失函数需要满足的几个条件
- 非线性函数
- 连续可导,但也允许在少数点上不可导
- 函数尽可能简单
- 导函数值域在一个合适的区间内,不可过大也不可过小
上述条件中,前两个条件很好理解,用于提供非线性性的函数肯定是非线性的,而连续可导是为了方便模型使用梯度下降进行训练。至于后两个条件,在后文我们会详细讲到,现在,让我们来看几个最常见的损失函数。
sigmoid函数
首先是sigmoid函数,就是logistic回归中使用的激活函数,其表达式为
σ(x)=1+e−x1
这也是最经典的损失函数,满足了上述的所有要求,既可以用在隐含层也可以用在输出层,早期的神经网络模型基本均使用sigmoid函数作为损失函数,不过随着技术的发展,现在也逐渐被淘汰了,只会在二分类问题的输出层有所出场。
注:至于softmax函数,基本是为了输出层量身定制的,并不能用在隐含层上,一般做多分类问题的话,输出层就使用softmax函数,这里就顺带提一句了。
tanh函数
然后是tanh函数,这是一个反三角函数,其表达式为
tanh(x)=ex+e−xex−e−x
tanh函数可以看作放大并平移的sigmoid函数,由于其值域为(−1,1),因此一般不会将其用于输出层。提出使用tanh函数来替代sigmoid函数的初衷是为了解决sigmoid函数的导数值域太小的问题,详细原因会在后文关于模型训练的部分细讲。目前,tanh函数的出场空间也很小了,只在RNN中会有一定的使用。
ReLU函数
最后是ReLU(Rectified Linear Unit)函数,也被称为线性整流函数,这是目前使用最广泛的损失函数,没有之一。其表达式为
ReLU(x)={x0x≥0x<0=max(0,x)
这是一个非常简单的分段函数,而且在x=0处不可导,其值域也不可能被用于输出层上,理应不是一个很好的选择,但是用ReLU函数效果就是好,就这么简单,深度学习很多时候就是这样的。至于效果好的原因,目前普遍认为是因为其x≥0的部分导数固定为1,这对深层的神经网络的训练有很强的正面效果。当然ReLU函数也不是没有缺点,其x<0的部分导数固定为0,容易导致部分神经元参数一直得不到更新,即“神经元死亡”,也因此诞生了很多ReLU函数的变体用于缓解该问题,不过后续有dropout技术的出现,ReLU的这个缺点也不是非常明显了,因此目前对于大部分深度神经网络,损失函数的选择依旧是无脑选择ReLU。
注:其实还有大量的基于上述三个损失函数的变体,但是就笔者的个人经历来说,这些变体都属于只在书本上见过,从来没有在实际中用过,因此这里就不讲了,感兴趣的读者可以自行搜索学习。
损失函数与优化算法
最后,就是多层感知机的训练了,到了多层感知机的年代,已经有了将把模型训练转化为求解损失函数最小值的思路了,所以多层感知机的训练与求解线性回归的数值解使用的算法并无二致。
在损失函数的选用上,对于回归问题选用均方误差损失函数(MSE),表达式为
L(W)=i=1∑n(yi−y^)2
对于分类问题选用交叉熵损失函数,表达式为
L(W)=−i=1∑nj=1∑myi(j)lny^i(j)
在优化算法的选择上,均选用梯度下降算法及其变体。
补充:反向传播算法
注:反向传播算法母庸置疑是一个非常重要的算法,但是由于其作用是优化梯度的计算,就是让梯度下降跑得更快,而现在成熟的深度学习框架,如Pytorch,均实现了反向传播算法并将其细节隐藏起来,因此如果是以使用深度学习为目的进行学习,不了解反向传播算法并不影响使用深度学习模型,因此笔者选择将该部分放入补充。但是反向传播算法真的很重要,建议还是看一看。
在学习反向传播算法之前,我们肯定要先知道反向传播算法是干什么的。反向传播算法并不是什么新的优化算法,而是一种用于在深度神经网络中加速梯度计算的算法。上一小节我们就讲到,多层感知机依旧是使用梯度下降及其变体进行优化求解的,那这当中自然会产生大量的梯度计算,而感知机或者说神经网络的层数堆叠,除了带来参数量的剧增,梯度的计算复杂度也会随着增加,尤其是越靠近输入层的参数,其梯度计算越复杂。
而反向传播算法就是说,我们发现参数的梯度计算之间是存在相互依赖的,具体表现为层数靠前的参数的梯度计算会用到层数靠后的参数的梯度计算的中间结果,因此,我们在算梯度的时候,可以从后往前算,然后计算靠后层数的梯度时,将靠前层数需要用到的中间结果储存起来,传给靠前层数使用,这样就可以减少大量的重复计算,由于这种算法需要从后往前计算梯度,因此得名反向传播算法,相对的,计算模型输出的过程则被称之为前向传播。
总结一下就是,原来在梯度下降中算梯度的时候,就是按照每一个参数的梯度计算公式算,但是在深度神经网络中,梯度的计算公式会变得很复杂,计算量会变得很大,而这时候就有人发现,算梯度的过程中是存在很多重复计算的,因此设计了一套算法,然那些被重复计算的结果只需要算一遍,然后存起来复用,这样就大大减少了计算量,这就是反向传播算法。下面,我们会使用一个简单的多层感知机作为例子,从推导每一个参数的梯度计算公式,到发现重复计算的部分,再到最后的反向传播算法。
准备工作
现在,让我们从一个简单的多层感知机模型入手,模型结构与符号标识如下图

从图中可以看到,该模型是一个只有一个隐含层的多层感知机,每一层都只有两个神经元,由于隐含层与输出层都需要经过激活函数,因此使用两个符号分别表示,在隐含层中,用neti表示输入层加权求和的输出结果,用hi表示neti经过激活函数后的结果,例如
net1=w01(1)+w11(1)x1+w21(1)x2h1=σ(net1)
在输出层中同理,用outi与y^i进行分别表示。从输入层到隐含层的参数记为wij(1),其中i表示输入层的神经元编号,若i=0则表示偏置,j表示隐含层的神经元编号,例如从x1到h1的参数为w11(1),可以参考上述net1的计算式。同理,从隐含层到输出层的参数记为wij(2),其中i表示隐含层的神经元编号,j表示输出层的神经元编号。
最后,为简单起见,我们暂时不考虑小批量的情况,假设只有一个输入样本((x1,x2),(y1,y2)),模型输出值为(y^1,y^2),隐含层激活函数使用sigmoid函数,输出层激活函数使用softmax函数,损失函数使用交叉熵损失函数。
梯度计算式推导
准备工作结束,接下来我们尝试使用一般的方法来计算参数的梯度,这当中会大量的使用到链式法则。在此之前,我们先把损失函数以及激活函数的导数先算好,以便后续取用
L=−i=1∑2yilny^i,∂y^i∂L=−y^iyiσ(x)=1+e−x1,σ′(x)=σ(x)(1−σ(x))softmax(xi)=k∑exkexi,softmax′(xi)=softmax(xi)(1−softmax(xi))
我们先从靠后层数的参数开始计算,以参数w11(2)为例,涉及到该参数的前向传播计算有
out1=w01(2)+w11(2)h1+w21(2)h2y^1=softmax(out1)
根据链式法则
∂w11(2)∂L=∂y^1∂L∂out1∂y^1∂w11(2)∂out1=−y^1y1y^1(1−y^1)h1=−y1(1−y^1)h1
同理,其余wij(2)的计算方式是类似的,到这里,其实跟softmax回归中的梯度计算并无区别。接下来,我们来看看靠前层数的参数,以参数w11(1)为例,涉及到该参数的前向传播计算有
net1=w01(1)+w11(1)x1+w21(1)x2,h1=σ(net1)out1=w01(2)+w11(2)h1+w21(2)h2,y^1=softmax(out1)out2=w02(2)+w12(2)h1+w22(2)h2,y^2=softmax(out2)
这里我们可以看到,参数w11(1)跟x1与h1有关,而由于层数的堆叠,h1跟后续的所有层的所有中间结果都有关,这也就造成了计算复杂度的剧增,这里的使用的例子还算简单,大家可以尝试一下多加一些层,每一层多加一些神经元,那么除了输出层参数,其余层的参数推导出的梯度计算式都将巨长无比。言归正传,我们还是来看看参数w11(1)的梯度计算,同样根据链式法则
∂w11(1)∂L=∂h1∂L∂net1∂h1∂w11(1)∂net1=∂h1∂Lh1(1−h1)x1∂h1∂L=∂y^1∂L∂out1∂y^1∂h1∂out1+∂y^2∂L∂out2∂y^2∂h1∂out2=−y1(1−y^1)w11(2)−y2(1−y^2)w12(2)∂w11(1)∂L=(y1(1−y^1)w11(2)+y2(1−y^2)w12(2))h1(h1−1)x1
同理,其余wij(1)的计算方式也是类似的。总结一下就是
∂wij(2)∂L=∂y^j∂L∂outj∂y^j∂wij(2)∂outj∂wij(1)∂L=(k∑∂y^k∂L∂outk∂y^k∂hj∂outk)∂netj∂hj∂wij(1)∂netj
梯度计算优化
观察两层参数的计算式,我们不难发现有一些重复计算的部分,例如∂y^i∂L∂outi∂y^i,那我们不妨记
δi(2)=∂y^i∂L∂outi∂y^i
并且不难观察到
∂wij(2)∂outj=hi,i=0∂wij(2)∂outj=1,i=0
故wij(2)的计算式可以写作
∂wij(2)∂L=δi(2)hi,i=0∂wij(2)∂L=δi(2),i=0
于是我们可以换一种计算方法,先算出所有的δi(2),hi在前向传播中就已经计算完成了,直接拿来用,再根据上述计算式将两者相乘。对比原本的计算方法,即按照计算式以此计算每一个wij(2)的梯度,不难发现,原本的计算方法相当于将每一个δi(2)都重复计算了j+1次,而这还只是在计算所有wij(2)的梯度中节省下来的计算量。
我们继续来看wij(1)的计算式,类比wij(2)的处理方式,观察到
∂wij(1)∂netj=xi,i=0∂wij(1)∂netj=1,i=0
于是,我们也想将wij(1)的计算式转化为如下的形式
∂wij(1)∂L=δi(1)xi,i=0∂wij(1)∂L=δi(1),i=0
其中
δi(1)=(k∑∂y^k∂L∂outk∂y^k∂hi∂outk)∂neti∂hi
观察发现
∂y^k∂L∂outk∂y^k=δk(2)∂hi∂outk=wik(2)
而∂neti∂hi依损失函数而定,这里就记作
∂neti∂hi=ϕ′(neti)
故
δi(1)=(k∑δk(2)wik(2))ϕ′(neti)
到这里我们就可以发现,正如上文所说,靠前层数的梯度计算是会使用到靠后层数的梯度计算的中间结果的。到了这一层,对比原计算方法,单就δk(2)的计算次数减少的可就不是一星半点了。
最后,如果再加一层隐含层,梯度计算式会更复杂吗?尝试推导一下就会发现,如果是直接推导计算式,那确实会更复杂,而如果使用上述计算方法,其计算式形式是类似的,假设有一个δi(0)的话,那其计算式应该为
δi(0)=(k∑δk(1)wik(1))ϕ′(xi)
所以,上面提出的这套算法是可以被应用到任意层数的多层感知机中的,并且δ的计算是靠前层数依赖靠后层数,因此,这套算法的计算需要从后往前算,故得名反向传播算法。
反向传播算法
至此,我们已经通过一个简单的模型了解了反向传播算法是如何优化梯度计算的效率的,现在,我们就给出一般多层感知机模型下的反向传播算法,模型结构图与符号标识如下图

记输出层神经元数量为p,隐含层数量为s,每一层隐含层的神经元书数量为mi,每一层隐含层的两个结果,即经过激活函数前与经过激活函数后的结果,分别用netj(i)与hj(i)表示,输出层同理,用outi与y^i进行分别表示,参数则使用wij(l)表示,其中l表示参数从前往后的排序,i表示上一层的神经元编号,j表示下一层的神经元编号,损失函数使用L表示,隐含层的激活函数使用ϕi表示,输出层的激活函数使用ϕout表示。
则使用反向传播算法计算该多层感知机模型的梯度的计算公式为
∂wij(l)∂L=⎩⎪⎪⎨⎪⎪⎧δi(l)hiδi(l)xiδi(l)i=0, l=1i=0, l=1i=0δi(l)=⎩⎪⎨⎪⎧(k∑δk(l+1)wik(l+1))ϕl+1′(neti(l+1))∂y^i∂Lϕend′(neti(l+1))l=s+1l=s+1
正则化与丢弃法
数值稳定性与模型初始化
Pytorch代码实现