CONTENTS
- 六、与学习相关的技巧
- 6.1 参数的更新
- 6.2 权重的初始值
- 6.3 Batch Normalization
- 6.4 正则化
- 6.5 超参数的验证
六、与学习相关的技巧
6.1 参数的更新
神经网络的学习的目的是找到使损失函数的值尽可能小的参数。这是寻找最优参数的问题,解决这个问题的过程称为最优化(optimization)。
在之前我们是沿着梯度方向更新参数,不断重复从而逐渐靠近最优参数,这个过程称为随机梯度下降法(stochastic gradient descent),简称SGD,其公式如下:
将其实现为一个类如下:
class SGD:
def __init__(self, lr=0.01):
self.lr = lr
def update(self, params, grads):
for key in params.keys():
params[key] -= self.lr * grads[key]
参数params
和grads
(与之前的神经网络的实现一样)是字典型变量,按params['W1']
、grads['W1']
的形式,分别保存了权重参数和它们的梯度。
使用这个SGD类可以按如下方式进行神经网络参数的更新:
network = TwoLayerNet(...)
optimizer = SGD()
for i in range(10000):
...
x_batch, t_batch = get_mini_batch(...) # mini-batch
grads = network.gradient(x_batch, t_batch)
params = network.params
optimizer.update(params, grads)
...
虽然SGD简单,并且容易实现,但是在解决某些问题时可能没有效率。我们来思考一下求下面这个函数的最小值的问题:
对这种函数应用SGD。从 ( x , y ) = ( − 7.0 , 2.0 ) (x, y) = (−7.0, 2.0) (x,y)=(−7.0,2.0)处(初始值)开始搜索,结果如下图所示:
可以看到SGD呈“之”字形移动。这是一个相当低效的路径。也就是说,SGD的缺点是,如果函数的形状非均向(anisotropic),比如呈延伸状,搜索的路径就会非常低效。
接下来我们介绍Momentum方法,其公式如下:
这里新出现了一个变量 v v v,对应物理上的速度,该式表示了物体在梯度方向上受力。在物体不受任何力时, α v \alpha v αv项承担使物体逐渐减速的任务( α \alpha α设定为 0.9 0.9 0.9之类的值),对应物理上的地面摩擦或空气阻力。下面是Momentum的代码实现:
class Momentum:
def __init__(self, lr=0.01, momentum=0.9):
self.lr = lr
self.momentum = momentum
self.v = None
def update(self, params, grads):
if self.v is None:
self.v = {}
for key, val in params.items():
self.v[key] = np.zeros_like(val)
for key in params.keys():
self.v[key] = self.momentum * self.v[key] - self.lr * grads[key]
params[key] += self.v[key]
使用Momentum解决上式的最优化问题,结果如下图所示:
虽然 x x x轴方向上受到的力非常小,但是一直在同一方向上受力,所以朝同一个方向会有一定的加速。反过来,虽然 y y y轴方向上受到的力很大,但是因为交互地受到正方向和反方向的力,它们会互相抵消,所以 y y y轴方向上的速度不稳定。因此,和SGD时的情形相比,可以更快地朝 x x x轴方向靠近,减弱“之”字形的变动程度。
在神经网络的学习中,学习率(数学式中记为 η \eta η)的值很重要。学习率过小,会导致学习花费过多时间;反过来,学习率过大,则会导致学习发散而不能正确进行。
在关于学习率的有效技巧中,有一种被称为学习率衰减(learning rate decay)的方法,即随着学习的进行,使学习率逐渐减小。
AdaGrad方法进一步发展了这个想法,针对“一个一个”的参数,赋予其“定制”的值。其公式如下:
这里新出现了变量 h h h,它保存了以前的所有梯度值的平方和,在更新参数时,通过乘以 1 h \frac{1}{\sqrt h} h1,就可以调整学习的尺度。这意味着,参数的元素中变动较大(被大幅更新)的元素的学习率将变小。也就是说,可以按参数的元素进行学习率衰减,使变动大的参数的学习率逐渐减小。
AdaGrad的实现过程代码如下:
class AdaGrad:
def __init__(self, lr=0.01):
self.lr = lr
self.h = None
def update(self, params, grads):
if self.h is None:
self.h = {}
for key, val in params.items():
self.h[key] = np.zeros_like(val)
for key in params.keys():
self.h[key] += grads[key] * grads[key]
params[key] -= self.lr * grads[key] / (np.sqrt(self.h[key]) + 1e-7)
这里需要注意的是,最后一行加上了微小值
1
0
−
7
10^{-7}
10−7。这是为了防止当self.h[key]
中有
0
0
0时,将
0
0
0用作除数的情况。
使用AdaGrad解决之前的问题结果如下:
Momentum参照小球在碗中滚动的物理规则进行移动,AdaGrad为参数的每个元素适当地调整更新步伐。如果将这两个方法融合在一起即为Adam。其效果如下:
上面我们介绍了SGD、Momentum、AdaGrad、Adam这几种方法,那么用哪种方法好呢?非常遗憾,(目前)并不存在能在所有问题中都表现良好的方法。这几种方法各有各的特点,都有各自擅长解决的问题和不擅长解决的问题。
基于MNIST数据集的四种更新方法的比较结果如下:
6.2 权重的初始值
首先不能将权重初始值设为0,这使得神经网络拥有许多不同的权重的意义丧失了。为了防止“权重均一化”(严格地讲,是为了瓦解权重的对称结构),必须随机生成初始值。
我们向一个5层神经网络(激活函数使用 s i g m o i d sigmoid sigmoid函数)传入随机生成的输入数据,首先使用标准差为1的高斯分布生成的随机数据,用直方图绘制各层激活值的数据分布:
各层的激活值呈偏向0和1的分布。这里使用的 s i g m o i d sigmoid sigmoid函数是S型函数,随着输出不断地靠近0(或者靠近1),它的导数的值逐渐接近0。因此,偏向0和1的数据分布会造成反向传播中梯度的值不断变小,最后消失。这个问题称为梯度消失(gradient vanishing)。层次加深的深度学习中,梯度消失的问题可能会更加严重。
下面,将权重的标准差设为0.01,进行相同的实验,结果如下:
这次呈集中在0.5附近的分布。因为不像刚才的例子那样偏向0和1,所以不会发生梯度消失的问题。但是,激活值的分布有所偏向,说明在表现力上会有很大问题。
如果有多个神经元都输出几乎相同的值,那它们就没有存在的意义了。比如,如果100个神经元都输出几乎相同的值,那么也可以由1个神经元来表达基本相同的事情。因此,激活值在分布上有所偏向会出现“表现力受限”的问题。
6.3 Batch Normalization
Batch Normalization(简称Batch Norm)的思想是为了使各层拥有适当的广度,“强制性”地调整激活值的分布。
使用Batch Norm层的网络结构如下:
Batch Norm,顾名思义,以进行学习时的mini-batch为单位,按mini-batch进行正规化。具体而言,就是进行使数据分布的均值为0、方差为1的正规化。用数学式表示的话,如下所示:
这里对mini-batch的 m m m个输入数据的集合 B = x 1 , x 2 , . . . , x m B = {x1, x2, ... , xm} B=x1,x2,...,xm求均值 µ B µB µB和方差 。然后,对输入数据进行均值为0、方差为1(合适的分布)的正规化。上式中的 ε ε ε是一个微小值(比如, 1 0 − 7 10^{-7} 10−7等),它是为了防止出现除以0的情况。
接着,Batch Norm层会对正规化后的数据进行缩放和平移的变换,用数学式可以如下表示:
Batch Norm的计算图如下:
6.4 正则化
机器学习的问题中,过拟合是一个很常见的问题。过拟合指的是只能拟合训练数据,但不能很好地拟合不包含在训练数据中的其他数据的状态。
发生过拟合的原因,主要有以下两个:
- 模型拥有大量参数、表现力强。
- 训练数据少。
权值衰减是一直以来经常被使用的一种抑制过拟合的方法。该方法通过在学习的过程中对大的权重进行惩罚,来抑制过拟合。很多过拟合原本就是因为权重参数取值过大才发生的。
但是,如果网络的模型变得很复杂,只用权值衰减就难以应对了。在这种情况下,我们经常会使用 D r o p o u t Dropout Dropout方法。
D r o p o u t Dropout Dropout是一种在学习的过程中随机删除神经元的方法。训练时,随机选出隐藏层的神经元,然后将其删除,如下图所示:
实现代码如下:
class Dropout:
def __init__(self, dropout_ratio=0.5):
self.dropout_ratio = dropout_ratio
self.mask = None
def forward(self, x, train_flg=True):
if train_flg:
self.mask = np.random.rand(*x.shape) > self.dropout_ratio
return x * self.mask
else:
return x * (1.0 - self.dropout_ratio)
def backward(self, dout):
return dout * self.mask
每次正向传播时,self.mask
中都会以False
的形式保存要删除的神经元。self.mask
会随机生成和
x
x
x形状相同的数组,并将值比 dropout_ratio
大的元素设为True
。反向传播时的行为和
R
e
L
U
ReLU
ReLU相同。也就是说,正向传播时传递了信号的神经元,反向传播时按原样传递信号;正向传播时没有传递信号的神经元,反向传播时信号将停在那里。
6.5 超参数的验证
神经网络中,除了权重和偏置等参数,超参数(hyper-parameter)也经常出现。这里所说的超参数是指,比如各层的神经元数量、batch大小、参数更新时的学习率或权值衰减等。
不能使用测试数据评估超参数的性能。这一点非常重要,但也容易被忽视。因为如果使用测试数据调整超参数,超参数的值会对测试数据发生过拟合。换句话说,用测试数据确认超参数的值的“好坏”,就会导致超参数的值被调整为只拟合测试数据。这样的话,可能就会得到不能拟合其他数据、泛化能力低的模型。
因此,调整超参数时,必须使用超参数专用的确认数据。用于调整超参数的数据,一般称为验证数据(validation data)。
根据不同的数据集,有的会事先分成训练数据、验证数据、测试数据三部分,有的只分成训练数据和测试数据两部分,有的则不进行分割。在这种情况下,用户需要自行进行分割。
进行超参数的最优化时,逐渐缩小超参数的“好值”的存在范围非常重要。所谓逐渐缩小范围,是指一开始先大致设定一个范围,从这个范围中随机选出一个超参数(采样),用这个采样到的值进行识别精度的评估;然后,多次重复该操作,观察识别精度的结果,根据这个结果缩小超参数的“好值”的范围。通过重复这一操作,就可以逐渐确定超参数的合适范围。
下一节:【学习笔记】深度学习入门:基于Python的理论与实现-卷积神经网络。