- 学习相关的技巧
- 5.1 权重最优化方法
- 5.1.1 随机梯度下降法(SGD)
- 5.1.2 动量法(Momentum)
- 5.1.3 自适应梯度法(AdaGrad)
- 5.1.4 动量学习率衰减方法(Adam)
- 5.2 权重的初始值
- 5.2.1 可以将权重初始值设置为零吗
- 6.2.3 隐藏层的激活值的分布
- 6.2.3.1 Xavier 初始值
- 6.2.3.2 ReLU的权重初始值
- 6.2.4 Batch Normalization
- 5.3 过拟合处理
- 5.3.1 权值衰减法
- 5.3.2 Dropout方法
- 5.4 超参数的验证
- 总结
前面已经系统了解了神经网络的工作原理,但是还有一些需要了解的其他细节点,本章将会学习除了梯度下降的其他权重最优化方法,权重初始值设定,超参数设定和过拟合处理方法等四个模块等内容。
5.1 权重最优化方法前面我们已经学习了随机梯度下降法(SGD,stochastic gradient descent)进行权重参数的优化,他理解简单,但是也有一些缺点,所以我们下面会再介绍Momntum,AdaGrad和Adam三种优化方法。
5.1.1 随机梯度下降法(SGD)随机梯度下降法的原理类似于贪心算法,他会找能使得梯度下降最大的方向进行下降,但是和贪心算法一样,他的这种最优解未必会是全局最优解,他的算法原理用数学表示为:
W
⟵
W
−
η
∂
L
∂
W
boldsymbol{W} longleftarrow boldsymbol{W} -eta { partial L over partial boldsymbol{W}}
W⟵W−η∂W∂L
实现代码和使用方法如下:
#实现代码
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]
#使用方法
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)
...
这种方法理解简单,实现也简单,通常简单的东西总会是有缺陷的,这个方法的缺点就是学习效率低,下面我们来看一个例子。
现在我们要求羡慕这个函数的最小值:
f
(
x
,
y
)
=
1
20
x
2
+
y
2
f(x,y)={1over 20}x^2+y^2
f(x,y)=201x2+y2
这个函数的图像和他的等高线如下:
他的登高线是椭圆型的,下面我们再给出他的梯度方向:
可以看到这个函数在
y
y
y轴方向上的梯度特别大,在
x
x
x轴方向上的梯度特别小。这就造成了一个问题,加入我们从初始位置
(
x
,
y
)
=
(
−
7.0
,
2.0
)
(x,y)=(-7.0,2.0)
(x,y)=(−7.0,2.0)开始进行随机梯度下降,那么这个下降就会在
y
y
y轴方向上大幅摆动,从而使效率下降,用图看的更直观:
这就是随机梯度下降的缺点,所以我们下面将会介绍其他几种最优化权重的方法,来解决这样的问题。
这种方法最好的理解方式是将前面提到的函数图像中扔一个小球,这个小球会不断滚向最低点:
数学表示这种方法为:
v
⟵
α
v
−
η
∂
L
∂
W
boldsymbol{v} longleftarrow alpha boldsymbol{v}-eta{ partial L over partial boldsymbol{W}}
v⟵αv−η∂W∂L
W
⟵
W
+
v
boldsymbol{W} longleftarrow boldsymbol{W}+boldsymbol{v}
W⟵W+v
他的代码实现为:
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]
他的工作效率比前面的随机梯度下降有明显的改进:
这种方法会在学习的过程中自动调整学习率也称为学习率衰减(learning rate decay)法,它随着学习的加深,学习率会不断的减小:
h
⟵
h
+
∂
L
∂
W
⊙
∂
L
∂
W
boldsymbol{h} longleftarrow boldsymbol{h}+{ partial L over partial boldsymbol{W}} odot { partial L over partial boldsymbol{W}}
h⟵h+∂W∂L⊙∂W∂L
W
⟵
W
−
η
1
h
∂
L
∂
W
boldsymbol{W} longleftarrow boldsymbol{W}-eta {1over sqrt {h}} { partial L over partial boldsymbol{W}}
W⟵W−ηh
1∂W∂L
这里出现了新参数
h
boldsymbol{h}
h,他是梯度的平方和,这意味着参数的元素中变动较大(被大幅更新)的元素的学习率将变小。也就是说,可以按参数的元素进行学习率衰减,使变动大的参数的学习率逐渐减小。
AdaGrad 会记录过去所有梯度的平方和。因此,学习越深入,更新的幅度就越小。实际上,如果无止境地学习,更新量就会变为 0,完全不再更新。为了改善这个问题,可以使用 RMSProp方法。RMSProp 方法并不是将过去所有的梯度一视同仁地相加,而是逐渐地遗忘过去的梯度,在做加法运算时将新梯度的信息更多地反映出来。这种操作从专业上讲,称为“指数移动平均”,呈指数函数式地减小过去的梯度的尺度。
这个算法的实现代码:
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)
用图表示他的工作情况:
这种方法是把动量法和学习率衰减法结合提出的一种方法,这个算法是2015年提出的相关工作原理需要参考原作者论文,他的工作图像:
Adam 会设置 3 个超参数。一个是学习率(论文中以 α 出现),另外两个是一次 momentum系数 β 1 beta_1 β1 和二次 momentum系数 β 2 beta_2 β2。根据论文,标准的设定值是 β 1 beta_1 β1 为 0.9, β 2 beta_2 β2为 0.999。设置了这些值后,大多数情况下都能顺利运行。
至此我们的4种优化方法就向大家介绍完了,这个四种方法根据不同的问题,优劣程度不同,但是相比较于SGD方法,其他方法要更好一些。
5.2 权重的初始值权重的初始值也是神经网络设计时的一个重要工作,因为权重的设置影响力网络学习的效率和学习是否成功。
5.2.1 可以将权重初始值设置为零吗前面我们在初始化权重时使用到了np.random.randn(10, 100)这种权重初始化方式,这种方式实际上是将权重设置为标准差为0.01的高斯分布。
但是设想我们是否可以简单粗暴将权重初始化直接设置为零,答案是不能的,因为反向传播时,由于正向传播时传来的都是相同的值,那么反向传播时可以参考以前学过的乘法反向传播规则,那么反向传播回去的又都是相同的值,这样由于多个神经元结果都是相同的,那么可以用一个神经元代替,那么神经网络高纬度的意义也就没了,所以不能初始化为零。
6.2.3 隐藏层的激活值的分布下面我们来研究一下权重初始化使用不同标准差的高斯分布时隐藏层激活函数值的分布情况。
假设:
向一个 5 层神经网络(激活函数使用 sigmoid 函数)传入随机生成的输入数据,用直方图绘制各层激活值的数据分布。
实验代码和结果如下:
import numpy as np
import matplotlib.pyplot as plt
def sigmoid(x):
return 1 / (1 + np.exp(-x))
x = np.random.randn(1000, 100) # 1000个数据
node_num = 100 # 各隐藏层的节点(神经元)数
hidden_layer_size = 5 # 隐藏层有5层
activations = {} # 激活值的结果保存在这里
for i in range(hidden_layer_size):
if i != 0:
x = activations[i-1]
w = np.random.randn(node_num, node_num) * 1#使用标准差为1的高斯分布
z = np.dot(x, w)
a = sigmoid(z) # sigmoid函数
activations[i] = a
# 绘制直方图
for i, a in activations.items():
plt.subplot(1, len(activations), i+1)
plt.title(str(i+1) + "-layer")
plt.hist(a.flatten(), 30, range=(0,1))
plt.show()
可以看到激活函数的值偏向于0和1,这种情况叫做表现力受限,同时这里使用到的是sigmoid激活函数,这个函数在结果趋向于0和1时,他们的倒数会接近于0,这样会导致反向传播中梯度不断减小直至消失的情况,从而无法继续进行学习,这就是梯度消失(gradient vanishing)。
如果更改初始化的标准差:
# w = np.random.randn(node_num, node_num) * 1 w = np.random.randn(node_num, node_num) * 0.01
那么得到的结果是:
可以看到还是表现力受限,这种结果是不能接受的,因为这和将权重初始值设置为0出现的问题一样这次呈集中在 0.5 附近的分布。因为不像刚才的例子那样偏向 0 和 1,所以不会发生梯度消失的问题。但是,激活值的分布有所偏向,说明在表现力上会有很大问题。为什么这么说呢?因为如果有多个神经元都输出几乎相同的值,那它们就没有存在的意义了。比如,如果 100 个神经元都输出几乎相同的值,那么也可以由 1 个神经元来表达基本相同的事情。因此,激活值在分布上有所偏向会出现“表现力受限”的问题。
有没有什么方法解决上面的问题呢,这里有两种权重初始化值Xavier 初始值和ReLU的权重初始值。
6.2.3.1 Xavier 初始值这种方法是Xavier Glorot 等人提出来的,他的核心思想是如果前一层的节点数为 n,则初始值使用标准差为
1
n
frac{1}{sqrt{n}}
n
1 的分布.
在代码上的变化是:
node_num = 100 # 前一层的节点数 w = np.random.randn(node_num, node_num) / np.sqrt(node_num)
改进后的激活值分布情况为:
可以看到这个激活函数值分布的宽度有了明显的改善。
6.2.3.2 ReLU的权重初始值后面的层的分布呈稍微歪斜的形状。如果用 tanh 函数(双曲线函数)代替 sigmoid 函数,这个稍微歪斜的问题就能得到改善。实际上,使用 tanh 函数后,会呈漂亮的吊钟型分布。tanh 函数和 sigmoid 函数同是 S 型曲线函数,但 tanh 函数是关于原点 (0, 0) 对称的 S 型曲线,而 sigmoid 函数是关于 (x, y)=(0, 0.5) 对称的 S 型曲线。众所周知,用作激活函数的函数最好具有关于原点对称的性质。
Xavier 初始值是以激活函数是线性函数为前提而推导出来的。因为 sigmoid 函数和 tanh 函数左右对称,且中央附近可以视作线性函数,所以适合使用 Xavier 初始值。但当激活函数使用 ReLU 时,一般推荐使用 ReLU 专用的初始值,也就是 Kaiming He 等人推荐的初始值,也称为“He 初始值”,他的工作原理是,当前一层的节点数为 n 时,He 初始值使用标准差为
2
n
sqrt{frac{2}{n}}
n2
的高斯分布。
对比使用ReLU激活函数不同初始化权值的激活值分布,可以看出使用标准差为0.01的高斯分布初始值出现了严重的表现力受限,初始值为 Xavier 初始值时的结果。在这种情况下,随着层的加深,偏向一点点变大。实际上,层加深后,激活值的偏向变大,学习时会出现梯度消失的问题。而当初始值为 He 初始值时,各层中分布的广度相同。由于即便层加深,数据的广度也能保持不变,因此逆向传播时,也会传递合适的值。
6.2.4 Batch Normalization总结一下,当激活函数使用 ReLU 时,权重初始值使用 He 初始值,当激活函数为 sigmoid 或 tanh 等 S 型曲线函数时,初始值使用 Xavier 初始值。这是目前的最佳实践。
前面学的是设置权重初始值,还有一种可以强制性调整权重初始值的方法,这就是Batch Normalization,比如前面学到的激活函数值图像如图:
可见0.8以后值就没有分布了,那么我们直接截取0.8以前的数据进行正则化,那么是不是激活函数分布就变广了,这就是这个方法的核心思想。
这种方法具有以下优点:
- 可以使学习快速进行(可以增大学习率)。
- 不那么依赖初始值(对于初始值不用那么神经质)。
- 抑制过拟合(降低 Dropout 等的必要性)。
他的工作需要增加一个Batch Norm 层:
他的具体方法就是以进行学习时的 mini-batch 为单位,按 mini-batch 进行正规化,使数据呈均值为 0、方差为 1 的正规化分布:
μ
B
⟵
1
m
∑
i
=
1
m
x
i
mu_B longleftarrow {1over m}sum_{i=1}^mx_i
μB⟵m1i=1∑mxi
σ
B
2
⟵
1
m
∑
i
=
1
m
(
x
i
−
μ
B
)
2
sigma_B^2 longleftarrow {1over m}sum_{i=1}^m(x_i-mu_B)^2
σB2⟵m1i=1∑m(xi−μB)2
x
^
i
⟵
x
i
−
μ
B
σ
B
2
+
ε
hat x_i longleftarrow {x_i-mu_B over sqrt {sigma_B^2+varepsilon}}
x^i⟵σB2+ε
xi−μB
这里对 mini-batch 的 m 个输入数据的集合 B=
{
x
1
,
x
2
⋯
,
x
m
}
{x_1,x_2cdots,x_m}
{x1,x2⋯,xm}求均值
μ
B
mu_B
μB 和方差
σ
B
2
sigma^2_B
σB2。然后,对输入数据进行均值为 0、方差为 1(合适的分布)的正规化。式中的
ε
varepsilon
ε是一个微小值(比如,10e-7 等),它是为了防止出现除以 0 的情况。
使用Batch Normalization和不使用该方法进行对比:
可以看到正确率明显升高。
造成过拟合主要有两个原因:
- 模型拥有大量参数、表现力强。
- 训练数据少。
这里,我们故意满足这两个条件,制造过拟合现象。为此,要从 MNIST 数据集原本的 60000 个训练数据中只选定 300 个,并且,为了增加网络的复杂度,使用 7 层网络(每层有 100 个神经元,激活函数为 ReLU),实现代码:
(x_train, t_train), (x_test, t_test) = load_mnist(normalize=True)
# 为了再现过拟合,减少学习数据
x_train = x_train[:300]
t_train = t_train[:300]
network = MultiLayerNet(input_size=784, hidden_size_list=[100, 100, 100,
100, 100, 100], output_size=10)
optimizer = SGD(lr=0.01) # 用学习率为0.01的SGD更新参数
max_epochs = 201
train_size = x_train.shape[0]
batch_size = 100
train_loss_list = []
train_acc_list = []
test_acc_list = []
iter_per_epoch = max(train_size / batch_size, 1)
epoch_cnt = 0
for i in range(1000000000):
batch_mask = np.random.choice(train_size, batch_size)
x_batch = x_train[batch_mask]
t_batch = t_train[batch_mask]
grads = network.gradient(x_batch, t_batch)
optimizer.update(network.params, grads)
if i % iter_per_epoch == 0:
train_acc = network.accuracy(x_train, t_train)
test_acc = network.accuracy(x_test, t_test)
train_acc_list.append(train_acc)
test_acc_list.append(test_acc)
epoch_cnt += 1
if epoch_cnt >= max_epochs:
break
我们来绘制精度:
)
可以看到数据在训练数据上表现很好,但是在测试数据上表现不行,这就是过拟合,处理过拟合有两种方法,一种是权值衰减法另一种方法是Dropout法。
很多过拟合产生的原因是因为权重参数取值过大,权值衰减法是在学习的过程中对大的权重进行惩罚,具体做法是在损失函数的后面加上平方范数(L2 范数),如果将权重记为 W boldsymbol{W} W,L2 范数的权值衰减就是 1 2 λ W 2 frac{1}{2}lambdaboldsymbol{W}^2 21λW2,其中 λ lambda λ是控制正则化强度的超参数。 λ lambda λ设置得越大,对大的权重施加的惩罚就越重。此外, 1 2 λ W 2 frac{1}{2}lambdaboldsymbol{W}^2 21λW2 开头的 1 2 frac{1}{2} 21 是用于将 1 2 λ W 2 frac{1}{2}lambdaboldsymbol{W}^2 21λW2 的求导结果变成 λ W lambdaboldsymbol{W} λW 的调整用常量。
对于所有权重,权值衰减方法都会为损失函数加上 1 2 λ W 2 frac{1}{2}lambdaboldsymbol{W}^2 21λW2。因此,在求权重梯度的计算中,要为之前的误差反向传播法的结果加上正则化项的导数 λ W lambdaboldsymbol{W} λW。
L2 范数相当于各个元素的平方和。用数学式表示的话,假设有权重 W = ( w 1 , w 2 , ⋯ , w n ) boldsymbol{W}=(w_1,w_2,cdots,w_n) W=(w1,w2,⋯,wn),则 L2 范数可用 w 1 2 + w 2 2 + ⋯ + w n 2 sqrt{w^2_1+w^2_2+cdots+w^2_n} w12+w22+⋯+wn2 计算出来。除了 L2 范数,还有 L1 范数、L ∞范数等。L1 范数是各个元素的绝对值之和,相当于 ∣ w 1 ∣ + ∣ w 2 ∣ + ⋯ + ∣ w n ∣ |w_1|+|w_2|+cdots+|w_n| ∣w1∣+∣w2∣+⋯+∣wn∣。L∞范数也称为 Max 范数,相当于各个元素的绝对值中最大的那一个。L2 范数、L1 范数、L∞范数都可以用作正则化项,它们各有各的特点,不过这里我们要实现的是比较常用的 L2 范数。
具体代码变化:
def loss(self, x, t):#损失函数时的变化
"""求损失函数
Parameters
----------
x : 输入数据
t : 教师标签
Returns
-------
损失函数的值
"""
y = self.predict(x)
weight_decay = 0
for idx in range(1, self.hidden_layer_num + 2):#计算衰减值
W = self.params['W' + str(idx)]
weight_decay += 0.5 * self.weight_decay_lambda * np.sum(W ** 2)
return self.last_layer.forward(y, t) + weight_decay
def gradient(self, x, t):
"""求梯度(误差反向传播法)
Parameters
----------
x : 输入数据
t : 教师标签
Returns
-------
具有各层的梯度的字典变量
grads['W1']、grads['W2']、...是各层的权重
grads['b1']、grads['b2']、...是各层的偏置
"""
# forward
self.loss(x, t)
# backward
dout = 1
dout = self.last_layer.backward(dout)
layers = list(self.layers.values())
layers.reverse()
for layer in layers:
dout = layer.backward(dout)
# 设定
grads = {}
for idx in range(1, self.hidden_layer_num+2):
grads['W' + str(idx)] = self.layers['Affine' + str(idx)].dW + self.weight_decay_lambda * self.layers['Affine' + str(idx)].W#在反向传播时的变化
grads['b' + str(idx)] = self.layers['Affine' + str(idx)].db
return grads
最后结果:
可以看到训练数据和测试数据的差距变小了。
这种方法是随机删除神经网络的神经元,训练时,每传递一次数据,就会随机选择要删除的神经元。然后,测试时,虽然会传递所有的神经元信号,但是对于各个神经元的输出,要乘上训练时的删除比例后再输出。
实现代码:
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
结果如下:
右边是使用了该方法的结果,可以看到有了明显的改进。
5.4 超参数的验证机器学习中经常使用集成学习。所谓集成学习,就是让多个模型单独进行学习,推理时再取多个模型的输出的平均值。用神经网络的语境来说,比如,准备 5 个结构相同(或者类似)的网络,分别进行学习,测试时,以这 5 个网络的输出的平均值作为答案。实验告诉我们,通过进行集成学习,神经网络的识别精度可以提高好几个百分点。这个集成学习与 Dropout 有密切的关系。这是因为可以将 Dropout 理解为,通过在学习过程中随机删除神经元,从而每一次都让不同的模型进行学习。并且,推理时,通过对神经元的输出乘以删除比例(比如,0.5 等),可以取得模型的平均值。也就是说,可以理解成,Dropout将集成学习的效果(模拟地)通过一个网络实现了。
神经网络中,除了权重和偏置等参数,超参数hyper-parameter也经常出现。这里所说的超参数是指,比如各层的神经元数量、batch 大小、参数更新时的学习率或权值衰减等。如果这些超参数没有设置合适的值,模型的性能就会很差。
但是调整超参数,要先从训练数据中分割出来一部分数据叫做验证数据这个数据不可以从测试数据中分,因为这会导致过拟合测试数据。
超参数的选择需要一定的经验,但是可以按照一定的流程来调超参数:
步骤 1
设定超参数的范围。
步骤 2
从设定的超参数范围中随机采样。
步骤 3
使用步骤 1 中采样到的超参数的值进行学习,通过验证数据评估识别精度(但是要将 epoch 设置得很小)。
步骤 4
重复步骤 1 和步骤 2(100 次等),根据它们的识别精度的结果,缩小超参数的范围。
总结这里介绍的超参数的最优化方法是实践性的方法。不过,这个方法与其说是科学方法,倒不如说有些实践者的经验的感觉。在超参数的最优化中,如果需要更精炼的方法,可以使用贝叶斯最优化(Bayesian optimization)。贝叶斯最优化运用以贝叶斯定理为中心的数学理论,能够更加严密、高效地进行最优化。详细内容请参考论文“Practical Bayesian Optimization of Machine Learning Algorithms” 等。
这一章,我们学习了权重参数的其他三种更新方法,也知道了SGD的缺点,后面我们又讲了两种设置初始化权重的方法(Xavier 初始值、He初始值等),同时介绍了一种强制转换的Batch Normalization方法,最后我们讲了如何处理过拟合,即权值衰减和Dropout方法,最后的最后,我们还说了如何设置超参数,至此我们神经网络的整体框架学完了,后面的部分看似不太重要,但是却也决定了网络是否高效和能学习成功。



