一、参数的更新
神经网络的学习的目的是找到使损失函数的值尽可能小的参数。这是寻找最优参数的问题,解决这个问题的过程称为最优化(optimization)。
SGD:
为了找到最优参数,我们将参数的梯度(导数)作为了线索。使用参数的梯度,沿梯度方向更新参数,并重复这个步骤多次,从而逐渐靠近最优参数,这个过程称为随机梯度下降法(stochastic gradient descent ),简称SGD。
←表示用右边的值更新左边的值。
SGD是一个简单的方法,不过比起胡乱地搜索参数空间,也算是“聪明”的方法。但是,根据不同的问题,也存在比SGD更加聪明的方法。
虽然SGD简单,并且容易实现,但是在解决某些问题时可能没有效率。这里,在指出SGD的缺点之际,我们来思考一下求下面这个函数的最小值的问题。
这个梯度的特征是,y轴方向上大,x轴方向上小。换句话说,就是y轴方向的坡度大,而x轴方向的坡度小。
1 class SGD:
2 def __init__(self, lr=0.01)
3 self.lr=lr
4 def update(self, params, grads):
5 for key in params.keys():
6 params[key]-=self.lr*grad[key]
这种形状的函数应用SGD。从(z, y))=(一7.0,2.0)处(初始值)开始搜索。基于SGD的最优化的更新路径:呈“之”字形朝最小值(0,0)移动,效率低。
右图的SGD呈“之”字形移动。这是一个相当低效的路径。也就是说,SGD的缺点是,如果函数的形状非均向( anisotropic),比如呈延伸状,搜索的路径就会非常低效。因此,我们需要比单纯朝梯度方向前进的SGD更聪明的方法。SGD低效的根本原因是,梯度的方向并没有指向最小值的方向。
Momentum:
Momentum是“动量”的意思,和物理有关。用数学式表示Momentum方法,如下所示。
和前面的SGD一样,W表示要更新的权重参数, 表示损失函数关于W的梯度,η 表示学习率。这里新出现了一个变量v,对应物理上的速度。
1 class Momentum:
2 def __init__(self, lr=0.01, momentum=0.9):
3 self.lr=lr
4 self.momentum=momentum
5 self.v=None
6 def update(self, params, grads):
7 if self.v is None:
8 self.v={}
9 for key,val in params.items():
10 self.v[key]=np.zeros_like(val)
11 for key in params.keys():
12 self.v[key]=self.momentum*self.v[key]-self.lr*grads[key]
13 params[key]+=self.v[key]
更新路径就像小球在碗中滚动一样。和SGD相比,我们发现“之”字形的“程度”减轻了。这是因为虽然α轴方向上受到的力非常小,但是一直在同一方向上受力,所以朝同一个方向会有一定的加速。反过来,虽然y轴方向上受到的力很大,但是因为交互地受到正方向和反方向的力,它们会互相抵消,所以y轴方向上的速度不稳定。因此,和SGD时的情形相比,可以更快地朝z轴方向靠近,减弱“之”字形的变动程度。
AdaGrad:
在关于学习率的有效技巧中,有一种被称为学习率衰减(learning ratedecay )的方法,即随着学习的进行,使学习率逐渐减小。实际上,一开始“多”学,然后逐渐“少”学的方法,在神经网络的学习中经常被使用。
逐渐减小学习率的想法,相当于将“全体”参数的学习率值一起降低。而AdaGrad进一步发展了这个想法,针对“一个一个”的参数,赋予其“定制”的值。
AdaGrad会为参数的每个元素适当地调整学习率,与此同时进行学习AdaGrad 的Ada来自英文单词Adaptive,即“适当的”的意思)。下面,让我们用数学式表示AdaGrad的更新方法。
现在来实现AdaGrad:
1 class AdaGrad:
2 def __init__(self, lr=0.01):
3 self.lr=lr
4 self.h=None
5 def update(self, params, grads):
6 if self.h is None:
7 self.h={}
8 for key,val in params.items():
9 self.h[key]=np.zeros_like(val)
10 for key,val in params.keys():
11 self.h[key]+=grads[key]*grads[key]
12 params[key]-=self.lr*grads[key]/(np.sqrt(self.h[key])+1e-7)
由图结果可知,函数的取值高效地向着最小值移动。由于y轴方向上的梯度较大,因此刚开始变动较大,但是后面会根据这个较大的变动按比例进行调整,减小更新的步伐。因此,y轴方向上的更新程度被减弱,“之”字形的变动程度有所衰减。
Adam:
Momentum参照小球在碗中滚动的物理规则进行移动,AdaGrad为参数的每个元素适当地调整更新步伐。如果将这两个方法融合在一起会怎么样?
Adam是2015年提出的新方法。它的理论有些复杂,直观地讲,就是融合了Momentum和 AdaGrad的方法。通过组合前面两个方法的优点,有望实现参数空间的高效搜索。此外,进行超参数的“偏置校正”也是Adam的特征。
我们以手写数字识别为例,比较前面介绍的SGD、Momentum ,AdaGrad、Adam这4种方法,并确认不同的方法在学习进展上有多大程度的差异。
二、权重的初始值
在神经网络的学习中,权重的初始值特别重要。实际上,设定什么样的权重初始值,经常关系到神经网络的学习能否成功。如果想减小权重的值,一开始就将初始值设为较小的值才是正途。
我们可以设置初始值像0.01*np.random.randn(10,100)这样,使用由高斯分布生成的值乘以0.01后得到的值(标准差为0.01的高斯分布)。
如果我们把权重初始值全部设为0以减小权重的值,会怎么样呢?从结论来说,将权重初始值设为0不是一个好主意。事实上,将权重初始值设为0的话,将无法正确讲行学习。
观察隐藏层的激活值(激活函数的输出数据的分布),可以获得很多启发。这里,我们来做一个简单的实验,观察权重初始值是如何影响隐藏层的激活值的分布的。这里要做的实验是,向一个5层神经网络(激活函数使用sigmoid函数)传入随机生成的输入数据,用直方图绘制各层激活值的数据分布。
1 import numpy as np
2 import matplotlib.pyplot as plt
3
4
5 def sigmoid(x):
6 return 1 / (1 + np.exp(-x))
7
8
9 def ReLU(x):
10 return np.maximum(0, x)
11
12
13 def tanh(x):
14 return np.tanh(x)
15
16 input_data = np.random.randn(1000, 100) # 1000个数据
17 node_num = 100 # 各隐藏层的节点(神经元)数
18 hidden_layer_size = 5 # 隐藏层有5层
19 activations = {} # 激活值的结果保存在这里
20
21 x = input_data
22
23 for i in range(hidden_layer_size):
24 if i != 0:
25 x = activations[i-1]
26
27 # 改变初始值进行实验!
28 w = np.random.randn(node_num, node_num) * 1
29 # w = np.random.randn(node_num, node_num) * 0.01
30 # w = np.random.randn(node_num, node_num) * np.sqrt(1.0 / node_num)
31 # w = np.random.randn(node_num, node_num) * np.sqrt(2.0 / node_num)
32
33
34 a = np.dot(x, w)
View Code
使用标准差为1 的高斯分布作为权重初始值时的各层激活值的分布
各层的激活值呈偏向0和1的分布。这里使用的sigmoid函数是S型函数,随着输出不断地靠近0(或者靠近1),它的导数的值逐渐接近0。因此,偏向О和1的数据分布会造成反向传播中梯度的值不断变小,最后消失。这个问题称为梯度消失(gradient vanishing)。层次加深的深度学习中,梯度消失的问题可能会更加严重。