深度学习
一、实验介绍
1.1 实验内容
深度学习
。
1.2 实验知识点
- 梯度消失问题
- 交叉熵损失函数
1.3 实验环境
- python 2.7
- numpy 1.12.1
- scipy 0.19.0
二、实验步骤
2.1 增加网络的深度
shallow.py
稍加修改,我们就可以得到一个深度神经网络,修改后的文件我们命令为deep.py
:
# encoding=utf-8
from layers import *
def main():
datalayer1 = Data('train.npy', 1024) # 用于训练,batch_size设置为1024
datalayer2 = Data('validate.npy', 10000) # 用于验证,所以设置batch_size为10000,一次性计算所有的样例
inner_layers = []
inner_layers.append(FullyConnect(17 * 17, 20))
inner_layers.append(Sigmoid())
inner_layers.append(FullyConnect(20, 26)) # 增加一个隐层
inner_layers.append(Sigmoid())
losslayer = QuadraticLoss()
accuracy = Accuracy()
for layer in inner_layers:
layer.lr = 1000.0 # 为所有中间层设置学习速率
epochs = 20
for i in range(epochs):
print 'epochs:', i
losssum = 0
iters = 0
while True:
data, pos = datalayer1.forward() # 从数据层取出数据
x, label = data
for layer in inner_layers: # 前向计算
x = layer.forward(x)
loss = losslayer.forward(x, label) # 调用损失层forward函数计算损失函数值
losssum += loss
iters += 1
d = losslayer.backward() # 调用损失层backward函数曾计算将要反向传播的梯度
for layer in inner_layers[::-1]: # 反向传播
d = layer.backward(d)
if pos == 0: # 一个epoch完成后进行准确率测试
data, _ = datalayer2.forward()
x, label = data
for layer in inner_layers:
x = layer.forward(x)
accu = accuracy.forward(x, label) # 调用准确率层forward()函数求出准确率
print 'loss:', losssum / iters
print 'accuracy:', accu
break
if __name__ == '__main__':
main()
我们只做了非常简单的修改,将原本的一层FullyConnect层改变成了两层,同时第一个FullyConnect层的输出有20个节点,这与第二个FullyConnect层的输入20个节点相匹配。这里中间网络层的节点数20也是一个需要我们手工设置的超参数。
2.2 深度学习的困难--梯度消失问题
python deep.py
运行上面的代码,你会发现我们的模型性能似乎不升反降了!上次实验我们的模型第一个epoch结束后就能达到0.4左右的准确率,可这次训练了好几个epoch之后准确率都还不到0.1, 20个epoch结束后,大概只有0.8左右的准确率。
不过其实如果你增大epochs(比如增大到100,但是这样比较耗时间),你会发现其实到最后,深度神经网络的准确率还是超过了浅层神经网络,大概能到0.97。最终准确率的提升验证了我们之前提到的深度神经网络具有更强的表达能力。
可是为什么一开始我们在验证集上的准确率增长的那么慢呢?难道说,我们的模型,“学习速率”变慢了?
lr
试试(比如增大到5000),准确率增长缓慢的问题会得到缓解,但和之前比起来还是慢了很多,为什么会这样呢?
要解释这个问题,我们再看看第一次实验中,梯度下降算法里给出的参数更新公式:
alpha
(对应代码里的lr
)控制学习速率,但同时也要注意到
也会影响每次参数更新的“步子”大小。由于我们这里的lr
没有变,那么一定是
变小了。
为什么变小了呢?你可以检查我们反向传播梯度的代码,尝试找出是哪个环节使得梯度变小。实际上,这里的主要问题出在Sigmoid层。Sigmoid层是这样反向传递梯度的:
def backward(self, d):
sig = self.sigmoid(self.x)
self.dx = d * sig * (1 - sig)
return self.dx # 反向传递梯度
对于从之前的网络层反向传递来的梯度,Sigmoid层会乘上其对于输入x的导数并再次反向传递。让我们看看sigmoid函数的导函数是什么样的:
我们看到,当输入为0时,sigmoid函数的导函数最大值为0.25,当输入数据的绝对值增大时,导函数值迅速减小,马上就会接近于0。
这便是导致我们的深度神经网络学习的更慢的“元凶”,由于我们这里多了一个Sigmoid层,梯度值经过Sigmoid层时,会减小很多,我们的梯度就像是“消失了”(vanishing gradient),导致我们的模型学习的非常慢。
实际上,梯度消失问题(或者说比这个问题更广泛的“梯度不稳定问题”),是导致难以训练深度神经网络的重要原因之一。
2.3 拯救消失的梯度--交叉熵损失函数
梯度消失问题(vanishing gradient problem)
的方法有很多,可以选择使用别的激活层替换掉Sigmoid层,但我们不打算介绍其他种类的激活层。在这里我们打算使用一种间接的,曲线救国的方式去解决梯度消失问题--使用交叉熵损失函数(cross-entropy loss)
替换掉平方损失函数。
2.3.1 什么是交叉熵
交叉熵这个概念来源于信息论,我们这里不打算深究交叉熵的深层含义,如果有兴趣你可以自己查阅相关资料。交叉熵损失函数公式如下:
公式的形式有些复杂,其中h(theta,x)代表我们的神经网络对输入数据x对应输出的预测,y代表输入数据的预期正确输出值,同时这里的log是以自然数e为底的对数。我们可以举几个例子去感受这个损失函数的性质,当y=1而h=0时,损失函数值为正无穷,这是合理的,因为我们的模型此时的预测是完全错误的。当y=1且h=1时,损失函数值为0, 即模型的预测是正确时,损失函数值为0。你可以多列举几组数据测试一下,感受一下交叉熵损失函数的特性。
2.3.2 对交叉熵损失函数求导
损失函数层计算损失函数对于该层输入的梯度,并反向传递回去,所以这里我们要对交叉熵损失函数求导。
交叉熵公式比较复杂,你可以自己尝试对它进行求导,求得的导函数为:
sigmoid(x)*(1-sigmoid(x))
,而sigmoid(x)*(1-sigmoid(x))
不就是h(thera,x)*(1-h(theta,x))
吗?所以损失函数层反向传递时先除以h(thera,x)*(1-h(theta,x))
,而Sigmoid层再乘以sigmoid(x)*(1-sigmoid(x))
,两者刚好相互抵消,于是我们的梯度就不会变小!!
这真是一个美妙的巧合,我在这里不得不感叹数学实在太美妙了。
2.3.3 编写交叉熵损失函数层
只要我们把之前的平方损失函数层替换成交叉熵损失函数层,应该就可以解决学习速率变低的问题。我们可以根据上面的公式编写出交叉熵损失函数层:
class CrossEntropyLoss:
def __init__(self):
pass
def forward(self, x, label):
self.x = x
self.label = np.zeros_like(x)
for a, b in zip(self.label, label):
a[b] = 1.0
self.loss = np.nan_to_num(-self.label * np.log(x) - ((1 - self.label) * np.log(1 - x))) # np.nan_to_num()避免log(0)得到负无穷的情况
self.loss = np.sum(self.loss) / x.shape[0]
return self.loss
def backward(self):
self.dx = (self.x - self.label) / self.x / (1 - self.x) # 分母会与Sigmoid层中的对应部分抵消
return self.dx
nan
,所以这里调用np.nan_to_num()避免这种情况。CrossEntropyLoss
已经包含在了上次实验的layers.py
文件中。
2.3.4 再次尝试深度神经网络
deep2.py
文件,修改代码如下:
# encoding=utf-8
from layers import *
def main():
datalayer1 = Data('train.npy', 1024) # 用于训练,batch_size设置为1024
datalayer2 = Data('validate.npy', 10000) # 用于验证,所以设置batch_size为10000,一次性计算所有的样例
inner_layers = []
inner_layers.append(FullyConnect(17 * 17, 20))
inner_layers.append(Sigmoid())
inner_layers.append(FullyConnect(20, 26))
inner_layers.append(Sigmoid())
losslayer = CrossEntropyLoss()
accuracy = Accuracy()
for layer in inner_layers:
layer.lr = 1.0 # 为所有中间层设置学习速率
epochs = 20
for i in range(epochs):
print 'epochs:', i
losssum = 0
iters = 0
while True:
data, pos = datalayer1.forward() # 从数据层取出数据
x, label = data
for layer in inner_layers: # 前向计算
x = layer.forward(x)
loss = losslayer.forward(x, label) # 调用损失层forward函数计算损失函数值
losssum += loss
iters += 1
d = losslayer.backward() # 调用损失层backward函数曾计算将要反向传播的梯度
for layer in inner_layers[::-1]: # 反向传播
d = layer.backward(d)
if pos == 0: # 一个epoch完成后进行准确率测试
data, _ = datalayer2.forward()
x, label = data
for layer in inner_layers:
x = layer.forward(x)
accu = accuracy.forward(x, label) # 调用准确率层forward()函数求出准确率
print 'loss:', losssum / iters
print 'accuracy:', accu
break
if __name__ == '__main__':
main()
在上次实验下载的代码包中已经包括了这段代码(在deep2.py中)。
这里只做了两处修改,一处是把QuadraticLoss
替换成了CrossEntropyLoss
,另一处是我们把学习速率调低到了1.0,因为这里我们的梯度不会像原来那样“消失”了,所以学习速率可以适当调低一点。python deep2.py
, 准确率在第一个epoch结束时就可以达到0.5左右,同时20个epoch结束后准确率可以达到0.98左右,实际上如果你多训练几个epoch,准确率可以超过0.99,就我自己测试而言,最高能到0.992,也就是说,10000张图片里面,只有80张图片的分类出现了错误。如果说,最开始0.9的准确率已经可以在一些实际场合运用的话,那0.992的准确率已经差不多达到人类的分类水准了,因为实际上验证图片中会有一些字母对于人来说都很难区分(比如一些"I"和“J”)。
当我第一次做出这个结果的时候,我非常非常的兴奋,深度学习的强大力量深深震撼了我。可以预见未来几年,深度学习的运用将会改变很多产业的格局,很多原本需要人来完成的事可以由机器替代我们去完成。甚至也许在有生之年,我们能够看到真正意义上的人工智能的出现(虽然到时候的智能算法不一定是深度学习)。所以朋友们,抓住这个机会吧!
2.4 尾声--更多的深度学习
本课程到这里就基本结束了。回想一下所有的实验,我们学习了深度学习领域的一些基本知识,编写出了一些神经网络层,用这些层组成了一个浅层神经网络并进一步将其改进为深度神经网络(我们的代码只包含模型训练和验证过程,你可以添加代码,将训练好的网络结构和参数保存下来,用到实际的项目中,比如识别图片验证码之类的)。
虽然我们最终达到了0.992的准确率,但实际上,我们的神经网络还是比较简陋(too simple),深度学习领域有很多其它类别的网络(比如卷积神经网络CNN, 循环神经网络RNN)在处理特定问题时有更强大的能力。我们的系列后续课程会进行介绍。
三、实验总结
作为最后一次实验,我们实现了一个真正意义上的深度神经网络,你以后可以自豪的说,你是做过“深度学习”的人了。
本次实验,我们学习了:
- Sigmoid层的导数很小是梯度消失问题的重要原因
- 交叉熵损失函数配合sigmoid激活函数可以避免梯度消失问题
- 损失函数如何选择也可以被视为一个超参数
- 深度学习领域还有其它种类的网络结构
四、课后作业
- [选做]深度学习中有很多种类的激活函数,请你查找相关资料了解。
- [选做]深度学习中有很多种类的损失函数,请你查找相关资料了解。
- [选做]请你添加将训练好的网络参数保存下来并运用于实际的图片字母识别的代码。