DeepLearning-BadNeuralNetwork-2

概述

  这次是来填之前BadNeuralNetwork的坑的。随着对于深度DL不断学习,我对其的了解也逐步加深,在此基础上我继续进行上次没有进行完全的试验,这次对于解决的上次的问题又加深了一步。

理论支持

激活函数

  目前的激活函数主要包括sigmoid,tanh,relu,leaky_relu,elu等。
  其中sigmoid函数具有明显的缺点:第一点为存在两个较大的饱和区,梯度为0,无法进行优化,第二点为非zero-mean的,整体函数x轴以上,输出值都大于0,经过简单的推导可以发现,优化的方向为系数乘$x$,并且$x$的各值为上一层的sigmoid的输出,故都大于0,因此$x$指向第一象限,因此优化的方向大大的受限。第三点为计算exp时计算成本高。tanh在sigmoid的基础上进行改进,为zero-mean的,但是仍存在较大的饱和区。
  relu函数的优势是大大减小了饱和区,仅在$x<0$的时候梯度为0,但是其存在的缺点是如果样本点都满足$y=wx+b<0$,则由于梯度为0,无法优化,称为“”dead“,实际的体现就是此层的神经元输出为0,在训练的过程中如果daad的神经元过多,势必效果不好。因此产生了leaky_relu,其优势是不存在饱和,在$x<0$的时候也可以进行优化,但是其对于噪声比较敏感,而ELU则对于rule进行了一定的改进,但是也存在了饱和区,其优势是对于噪声并不像leaky_relu一样敏感。
  对于激活函数的选择,老师的建议是,对于CNN优先选择relu,如果效果不好再试一下leaky_relu和ELU,还不好的话再试一下tanh,sigmoid一般就不用了。对于LSTM,则优先选择tanh。
  老师还提供了一种检验激活函数活性的方法,即对于每一个输出层统计输出为0的神经元的直方图,并进行分析。可以基于此方法考虑是否需要更换激活函数。

数据预处理

  一般的步骤是进行均值归零的操作,即样本点减去平均值,这样做的好处防止值较大不方便优化;如果多个输入的动态范围差别较大的话,则进行方差优化,即样本除以标准差,这样可以使得不同输入的影响基本一致。对于数据预处理,老师的一点经验是:处理不处理影响很大,怎么处理的的差别不是很大。

参数初始化

  首先的是,各参数一定不能设定为相同值,因为如果初始化为相同的值,则训练的神经元其实是完全相同的,这样训练多个神经元则没有意义。因此参数应该进行随机初始化。但是也不是随便随机就可以的,例如对于tanh来说,如果参数初始化过小,则使得乘系数后值过小,梯度接近0;如果参数初始化过大,则直接使值掉入饱和区中,梯度仍为0。因此选择合适的随机值也很关键

问题分析

回顾

  之前我训练了一个隐层层数为(20,5,2)的小网络,想让其学习$2*(x-2)^2+2$的函数,但是学习的过程中经常出现结果为直线的情况。我上次认为是陷入了局部最优或者鞍点。其实要证明这个观点还是比较困难的,并且维度较高时,可视化并不方便。所以这次我想从Rule活性的角度进行分析。

重现

  首先对于问题进行重现,这次使用pytorch对于网络进行重建,样本与数据预处理如下:

1
2
3
4
5
6
7
def getDataSet():
x = torch.range(-3000,5000)
y = 2*torch.pow((x-1),2) + 2

#data preprocessing
x = (x+3000)/8000
y = (y-2)/50000000

  网络构建如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Net(nn.Module):

def __init__(self):
super(Net,self).__init__()
self.fc1 = nn.Linear(1,20)
self.fc2 = nn.Linear(20,5)
self.fc3 = nn.Linear(5,2)
self.fc4 = nn.Linear(2,1)

def forward(self,x):
x = F.relu(self.fc1(x))
x = F.relu(self.fc2(x))
x = F.relu(self.fc3(x))
x = F.sigmoid(self.fc4(x))
return x

  进行训练时,上次的问题果然出现。

分析

  输出loss如下,发现其较早就在较大的loss值时收敛了:

1
2
3
4
5
6
7
8
9
10
[1,   100] loss: 7.373
[1, 200] loss: 7.050
[1, 300] loss: 7.050
[1, 400] loss: 7.050
[1, 500] loss: 7.050
[1, 600] loss: 7.050
[1, 700] loss: 7.050
[1, 800] loss: 7.050
[1, 900] loss: 7.050
[1, 1000] loss: 7.050

  输出网络倒数第二层发现网络倒数第二层在迭代几次之后,基本输出全部变为0,因此有较多的relu神经元dead,部分输出如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
Variable containing:
0 0
0 0
0 0

0 0
0 0
0 0
[torch.FloatTensor of size 8001x2]

Variable containing:
0 0
0 0
0 0

0 0
0 0
0 0
[torch.FloatTensor of size 8001x2]

Variable containing:
0 0
0 0
0 0

0 0
0 0
0 0
[torch.FloatTensor of size 8001x2]

  可以判断网络过早收敛是由于过多relu失活造成的。

网络改进

  我先尝试了relu,leaky_relu,elu,发现三种效果均不理想。然后尝试找了下小网络进行函数学习的文章,在一篇训练效果不错的文章中我发现其训练的模型为(9,9,9,2),使用的函数为sigmoid。我先尝试将网络模型改为(9,9,9,2),但是训练效果并灭有明显的提升,之后我将激活函数全部使用sigmoid:

1
2
3
4
5
6
def forward(self,x):
x = F.sigmoid(self.fc1(x))
x = F.sigmoid(self.fc2(x))
x = F.sigmoid(self.fc3(x))
x = F.sigmoid(self.fc4(x))
return x

  发现网络的loss可以持续递减,并且在第一次迭代的5000-6000次左右的时候,基本就会收敛,其中一次的输出如下:

1
2
3
4
5
6
7
8
9
10
11
[1,  4000] loss: 0.009
[1, 4100] loss: 0.009
[1, 4200] loss: 0.009
[1, 4300] loss: 0.009
[1, 4400] loss: 0.009
[1, 4500] loss: 0.009
[1, 4600] loss: 0.009
[1, 4700] loss: 0.008
[1, 4800] loss: 0.008
[1, 4900] loss: 0.008
[1, 5000] loss: 0.008

  基本在5000次左右的时候就已经有较好的训练效果,效果如图:
photo1

总结

  经过这次试验,可以确定之前网络训练效果不好的原因是因为激活函数选择不合适造成的。同时也体现出,对于一些简单的传统神经网络来说,sigmoid函数的效果还是要比relu要好的。
  在这次试验中,出现了一个较大的问题,是一开始forwark内第一层报错,说数据类型不正确,但是我输入的类型为variable没有错误,因此排查的很长时间错误没有找到原因,最后发现data_x的size为8000,然后我利用

1
2
data_x = data_x.view(8000,1)
data_y = data_y.view(8000,1)

  将size更正为80001,输出正常。通过这次教训,下次对于一维的数据一定要将其维度定为n1。
  通过这次试验我也意识到DL的基础理论对于训练网络的重要性,没有弄清楚这些的话,即使调参也会没有明确的方向性。。