Machine Learning: An Algorithmic Perspective, Second Edition——Part 2
4. 多层感知器
我们感兴趣的大部分分体是非线性可分的——找到一种合适的转换特征的方式使问题线性可分;
神经网络中的学习与权重有关——为了进行更多的运算,就要加入更多的权重——① 加入后向的连接,以便于输出神经与输入联系起来——循环网络(recurrent network),已有研究,但并不经常使用;② 加入更多神经元——在输入节点与输出节点之间加入神经元,使神经网络更复杂;
加入额外的层会使神经网络更有用——解决XOR问题:
检查不同的输入结果可以看出上述网络可以实现XOR函数问题。
如何训练这种多层网络使得权重产生合适的正确目标——计算输出的误差——但不知道哪一个权重是错误的:在第一层还是在第二层;同时也不知道网络中间的神经元的正确激活是什么——隐藏层(hidden layer):不可能去检查并且直接修正它们的值——多层感知器MLP(Rumelhart、Hinton、MeClelland,1986);
MLP是最常用的神经网络之一,通常被视为“黑匣子”——人们在不了解其工作原理的情况下使用它,这通常会导致相当差的结果。
4.1 前向
训练MLP由两部分组成:① 前向(forward):对于给定的输入和当前的权重,计算输出;② 后向(backward):根据误差对权重进行更新——误差:表示输出和目标之间差别的函数;
前向——再现阶段:沿着网络计算每层神经元是否激活,并用与下一层的输入。
4.1.1 偏置
需要使每一个神经元包含一个偏置输入(-1),并且调整每个神经元的权重作为训练的一部分——在网络中的每个神经元(无论是隐藏层还是输出层)都有一个格外 的输入,且为定值。
4.2 后向:误差的反向传播
误差的反向传播(back-propagation of error):误差通过网络向后传播,是梯度下降(gradient descent)的一种形式——用数学来表述反向传播的性质。
在感知器中,改变权重以便于当目标认为一个神经元应该激活时这个神经元激活,而目标认为这个神经元不能激活时,它就不激活——为每一个神经元k选择预估误差函数Ek=yk-tk,并试图让其最小——此时网络只有一组权重,所以足以训练网络。
加入额外层(隐藏层)的权重,使得最小化误差难以安排——在尝试调整多层感知器的权重时,需要知道是哪一个权重引起的误差:可能是连接输入层和隐藏层的权重,或者是连接隐藏层和输出层的权重——对于更复杂的网络,在隐藏层中节点之间会有额外的权重,可以用相同的方法解决,但更难讨论,此处只考虑一层隐藏层。
感知器使用的误差函数为:
∑
k
=
1
N
E
k
=
∑
k
=
1
N
y
k
−
t
k
\sum\limits_{k=1}^{N}{{{E}_{k}}}=\sum\limits_{k=1}^{N}{{{y}_{k}}}-{{t}_{k}}
k=1∑NEk=k=1∑Nyk−tk,N为输出节点的数量,假设存在两个误差点:一个目标大于输出,一个输出大于目标,而这两个误差的大小相同,则和为0——算法认为没有误差——需要让所有的误差都具有相同的符号——平方和(sum-of square)误差函数:
上式的1/2是为了便于求导——对一个函数求导,得到函数的梯度,进而得到函数下降或减小最快的方向——对误差函数求导,得到误差的梯度——学习的目的是最小化误差,沿着误差函数下降的方向(负梯度方向)将会得到最小值——梯度下降法。
求导变量的选择——输入、决定是否要激活节点的激活函数、以及权重:在算法运行过程中前两个不在控制范围内,因此权重为求导的变量;
激活函数:采用阈值函数在阶跃点非连续且不可导——sigmoid函数:导数具有较好的形式。
常用形式(0~1):
双曲正切函数(-1~+1):
双曲正切函数导函数:
饱和(saturate):达到常量,可将双曲正切函数的饱和点,通过0.5(x+1)转换到(0~1)
根据上述内容,我们可以得到一个新形式的误差函数和一个新的激活函数来决定神经元是否激活——求导,以便改变权重时能使误差下降——改善网络的误差函数;
处理连接输出层的节点,更新完后,将沿着网络后向移动直到回到输入。
对于上述环节,存在两个问题:
① 对于输出神经元,不知道输入;
② 对于隐藏神经元,不知道目标;对于额外隐藏层,既不知道输入也不知道目标。
求导的链式法则(chain rule of differentiation):如果想要知道改变权重时误差如何改变,可以考虑改变权重的输入时误差如何改变,并且用它乘以改变权重时输入值的改变——可以吧输出节点的激活写成隐藏接地那的激活和输出权重的形式——传递误差计算,并且沿着网络后向移动到隐藏层,从而决定对于这些神经元来说其目标输出是什么。
根据权重计算误差的梯度,以便于改变权重,使其下降,从而使得误差变小。通过误差函数对权重求导来实现,但不能直接这样做,所以用链式法则对已知的数据信息求导。从而产生了两个不同的更新函数,一个对应一组去那种,并且仅仅应用它们沿着网络后向传递,从输出开始在输入结束。
4.2.1 多层感知器算法
假设有L个输入节点及偏置,M个隐藏节点及偏置,N个输出节点——输入和隐藏层之间有(L+1)M个权重,隐藏层和输出之间有(M+1)N个权重——所描述的算法可以具有任意数量的隐藏层,存在若干M值以及隐藏层之间的额外权重集——i, j, k 索引每层的节点,ι, ζ, κ用于固定索引。
使用了误差的反向传播的完整MLP训练算法总结:
- 输入向量被输入到输入节点;
- 输入沿着网络前向传递:
· 利用输入和第一层权重v决定隐藏节点是否激活。激活函数g(·)为上文的sigmoid函数;
· 利用这些神经元的输出和第二层权重w决定输出神经元是否激活; - 误差是网络的输出和目标之间的平方和;
- 这个误差沿网络后向传递,首先更新第二层权重,然后后向传播更新第一层权重。
多层感知器算法:
· 初始化:
- 用很小的随机数(正数或负数)初始化所有权重
· 训练: - 重复:
对每一个输入向量:
前向阶段:
① 计算隐藏层中每个神经元j的激活:
② 沿着网络直到到达输出层激活的神经元:
后向阶段:
① 计算输出的误差:
② 计算隐藏层的误差:
③ 更新输出层权重:
④ 更新隐藏层权重:
使输入向量的顺序随机化,以便于每次迭代都不会用相同的顺序训练网络 - 直到学习停止
· 回忆
- 使用上面训练部分的向前阶段
算法的核心权重更新计算可以实现为:
deltao = (targets-self.outputs)*self.outputs*(1.0-self.outputs)
deltah = self.hidden*(1.0-self.hidden)*(np.dot(deltao,np.transpose(self.'
weights2)))
updatew1 = np.zeros((np.shape(self.weights1)))
updatew2 = np.zeros((np.shape(self.weights2)))
updatew1 = eta*(np.dot(np.transpose(inputs),deltah[:,:-1]))
updatew2 = eta*(np.dot(np.transpose(self.hidden),deltao))
self.weights1 += updatew1
self.weights2 += updatew2
MLP学习XOR函数:
import numpy as np
import mlp
anddata = np.array([[0,0,0],[0,1,0],[1,0,0],[1,1,1]])
xordata = np.array([[0,0,0],[0,1,1],[1,0,1],[1,1,0]])
p = mlp.mlp(anddata[:,0:2],anddata[:,2:3],2)
p.mlptrain(anddata[:,0:2],anddata[:,2:3],0.25,1001)
p.confmat(anddata[:,0:2],anddata[:,2:3])
q = mlp.mlp(xordata[:,0:2],xordata[:,2:3],2)
q.mlptrain(xordata[:,0:2],xordata[:,2:3],0.25,5001)
q.confmat(xordata[:,0:2],xordata[:,2:3])
产生的输出为:
MLP学习XOR函数:
Iteration: 0 Error: 0.367917569871
Iteration: 1000 Error: 0.0204860723612
Confusion matrix is:
[[ 3. 0.]
[ 0. 1.]]
Percentage Correct: 100.0
Iteration: 0 Error: 0.515798627074
Iteration: 1000 Error: 0.499568173798
Iteration: 2000 Error: 0.498271692284
Iteration: 3000 Error: 0.480839047738
Iteration: 4000 Error: 0.382706753191
Iteration: 5000 Error: 0.0537169253359
Confusion matrix is:
[[ 2. 0.]
[ 0. 2.]]
Percentage Correct: 100.0
比起感知器,即使对于AND也需要迭代很多。所以更加复杂的网络的好处在于它的计算代价——使用更多的计算时间来拟合这些权重从而解决问题。
4.2.2 初始化权重
如果初始化权重值靠近1或-1(意味着值很大),那么对于sigmoid函数的输入也很可能会靠近±1,所以神经元的输出是0或1(sigmoid函数是饱和的,达到了最大值或最小值)。如果权重非常小(靠近0),那么输入也靠近0,所以神经元的输出仅仅是线性的,这样我们就得到一个线性模型。这两件事情对于最终的网络都是有用的,但是如果初始值在它们之间,就可以由网络自己来决定。
每个神经元从n个不同的地方得到输入(如果神经元在隐藏层中,则为输入节点;如果在输出层中,则为隐藏神经元)。如果我们把这些输入值看成都具有均匀的方差,那么对于神经元,典型的输入将是 w n w\sqrt{n} wn,这里w是权重的初始值。所以一个常用的技巧是设置权重在范围 − 1 / n < w < 1 / n -1/\sqrt{n}<w<1/\sqrt{n} −1/n<w<1/n之内(n为输入层节点数)。如果权重很大,则神经元的激活可能已经处于或接近0或1,这意味着梯度更小,因此学习速很慢,意味着小的β值(β<3.0)会更有效。
使用随机值进行初始化,以便每次运行时从不同的地方开始学习,并且保持它们都是相同的大小,因为我们想要所有的权重在同一时刻达到最后的值。这就是所谓的平均学习(uniform learning)
4.2.3 不同的输出激活函数
处理回归问题,需要连续输出——使用线性节点来代替输出神经元,也就是把输入加起来,并且把他们当做激活的——只修改输出节点,这些节点不再是神经元的模型,因为它们不再有激活或者不激活的特点;
Soft-max函数,使用1-of-N 输出编码——通过计算输入的指数来重新安排输出,并且除以所有神经元输入的和,以便激活加起来是1,并且所有的激活都分布于0到1之间:
如果改变激活函数,学习将会不同,对于线性激活函数:
其输出误差为:
对于soft-max激活,其输出误差为:
如果κ = K,则δκK = 1,否则为0。但如果修改误差函数为交叉熵(cross-entropy)形式:
输出误差与线性激活函数一致。
计算这些更新等式,需要计算被优化的误差函数,然后对其求导,代码如下:
# Difffferent types of output neurons
if self.outtype == ’linear’:
return outputs
elif self.outtype == ’logistic’:
return 1.0/(1.0+np.exp(-self.beta*outputs))
elif self.outtype == ’softmax’:
normalisers = np.sum(np.exp(outputs),axis=1)*np.ones((1,np.shape(outputs)[0]))
return np.transpose(np.transpose(np.exp(outputs))/normalisers)
else:
print "error"
4.2.4 顺序和批量训练
MLP是用于批量训练的算法。所有训练数据都提供给神经网络,计算平均的误差平方和,并且据此来更新权重。对于每次迭代(epoch,即里面所有训练数据)就只有一组权重更新——每一次得带只更新一次权重——权重沿着大多数输入想让它们移动的方向移动,而不是被每一个单独的输入推着走。批量的方法使得误差梯度的估计更精确,并且将会更快收敛到局部最小;
之前的算法是顺序(sequential)版本,即计算误差并且在每一个输入后更新权重,这在学习中并不能保重是有效的,但是使用循环会使得程序简单,因此更常用——收敛性不好,但有时会避开局部最小从而得到更好的解。
在顺序版本中吗,权重更新的顺序会很重要,因此需要在每次迭代时随机化输入向量顺序,从而提高算法的学习速度。采用np.random.shuffle():
np.random.shuffle(change)
inputs = inputs[change,:]
targets = targets[change,:]
4.2.5 局部最小
梯度下降法意味着实现了优化(optimisation):调整权重的值以便最小化误差函数——通过估计误差的梯度并且沿着梯度下降,以便到达局部最小(local minimum)。
由于不知道误差函数的图像,因此只能在当前所在的位置计算局部特征,也可以尝试几个不同的初始点,从而更有可能找到全局最小——常见方法。
4.2.6 利用冲量
通过改变当前点的前一个权重来加入一些贡献,进而用神经网络实现这个想法——冲量使得采用更小的学习速率成为可能,意味学习会更加稳定:
其中:t代表当前的更新,t-1 是前一个更新。最后一项
Δ
w
ζ
κ
t
−
1
\Delta w_{\zeta \kappa }^{t-1}
Δwζκt−1是权重的前一个更新(所以
Δ
w
ζ
κ
t
=
η
δ
0
(
κ
)
a
ζ
h
i
d
d
e
n
+
a
Δ
w
ζ
κ
t
−
1
\Delta w_{\zeta \kappa }^{t}=\eta {{\delta }_{0}}(\kappa )a_{\zeta }^{hidden}+a\Delta w_{\zeta \kappa }^{t-1}
Δwζκt=ηδ0(κ)aζhidden+aΔwζκt−1),0<a<1是冲量常量,通常取0.9,其代码为:
updatew1 = eta*(np.dot(np.transpose(inputs),deltah[:,:-1])) + '
momentum*updatew1
updatew2 = eta*(np.dot(np.transpose(hidden),deltao)) + momentum*updatew2
另一个可以加入的是权重衰减(weight decay),即随着迭代次数的增加而减少权重的大小,使得网络更接近于线性,并且只有与非线性学习有关的权重才应该变大——有时会产生糟糕的结果。
4.2.7 小批量和随机梯度下降
批量算法可以更好地估计最陡下降方向。
小批量(minibatch)方法的想法是通过以下方法找到两者之间一些好的中间点:将训练集分成随机批次,根据训练集的一个子集估计梯度,执行去那种更新,然后使用下一个子集估计一个新梯度,并将新梯度用于权重更新,持续进行下去直到使用了所有训练集。然后将训练集随机混合到新批次中,并进行下一次迭代。如果批次很小,那么梯度估计中的误差通常是合理的,因此优化有机会避免局部最小值,尽管要以错误方向前进为代价。
小批量思想的另一个更极端的版本是仅使用一个数据来估计算法每次迭代时的梯度,并从训练集中随机均匀地挑选该数据。因此,从训练集中选择单个输入向量,计算输出并以此计算该向量的误差,然后据此估计梯度,并更新权重。然后再选择新的随机输入向量(可以与前一个相同)并重复该过程——随机梯度下降。
4.2.8 其他改善方法
改善反向传播算法的收敛和行为:① 在算法进行的过程中减小学习速率:当权重是随机值时,网络只有在起初的时候才应该有大的权重改变;如果在以后还使用大的权重改变,那么就会报错;②包括误差函数对于权重的二阶导数的信息,使得学习有更好表现的方法,用来改善网络。
4.3 实践中的MLP
使用MLP来找到四类问题的解决方法:回归、分类、时间序列预测、数据压缩;
4.3.1 训练数据的量
对于单层隐藏层的MLP,有(L+1)M+(M+1)N个权重,这里L、M、N是相应的输入层、隐藏层和输出层节点数。额外的 +1 来自于偏置节点。
如果有更多的训练数据,学习效果就会更好,虽然算法学习所花的时间会变长,但是没有办法计算出所需数据的最小值是多少,因为这依赖于问题本身。对于大多数MLP,所使用的训练数据的数目至少是权重数目的10倍。
4.3.2 隐藏层的数目
隐藏层节点数量和隐藏层数量的选择——一般MLP学习最多需要两个隐藏层——用数学方式显示一个包含大量隐藏节点的隐藏层就足够了——通用近似定理;
然而没有理论来指导隐藏节点数量的选择:只能用不同隐藏节点数目来训练网络,并且选择给出最好结果的那一个。
用反向传播算法,在任何给定时间跟踪更新权重变得越来越难。但是不需要超过两层,即一个隐藏层和一个输出层——用局部sigmoid函数的线性组合来估计任何一个光滑的函数映射:
通过sigmoid函数的结合来产生脊状函数,通过脊状函数的结合产生有特定最大值的函数——将其结合起来并用其他神经层来转换,进而得到局部的映射——跃升函数——其线性组合将任何函数映射近似为任意精度。
MLP的学习可以被分成:a) 单个sigmoid神经元的输出;b) 单个输出的叠加,报考加入反向的图像进而得到“山”的形状;c) 以90°方向加入另一座“山”;d) 把跃升加在输出层,使图像更加尖锐。这样MLP就学习了一个局部的独立输入。
上图显示出两个隐藏层就以足够(sufficient),但是并不是必须(necessary)的:虽然可能需要任意数量的隐藏节点,但是一个隐藏层就足够了——通用近似定理(Universal Approximation Theorem)
两个隐藏层对于不同输入足够用于计算这些跃升函数,并且如果我们想要学习(估计)的函数是连续的,网络就可以计算。因此可以估计任意一个决策边界,而不仅仅像感知器计算的线性情形。
4.3.3 什么时候停止学习
大多数明显的选择都是不充分的:设置一些预定义的迭代数目T,运行算法直到面临过拟合的风险,或者还没有学习充分,仅在到达预先定义的最小误差时停止——验证集可以用来近视网络在当前学习阶段的泛化能力。如果把训练期间的平方和误差画出来,前几次训练迭代会减小的很快,然后随着学习算法通过微小的改变找到准确的局部最小值,减小的速度会慢下来——预先定义的一段时间训练网络,然后用验证集来估计网络的泛化能力,之后计算训练几次迭代,并且重复整个过程。某个阶段,验证集的误差将会再次增长,此时学习数据本身的噪点,这时需要停止训练——早期停止(early stopping)