目录
接着
手磕实现 CNN卷积神经网络!《深度学习入门:基于Python的理论与实现》系列之三
继续学习笔记的记载。
(此笔记是基于《深度学习入门》这本书的重点知识梳理和汇总。)
本章主要内容:
-
寻找最优权重参数的最优化方法、权重参数的初始值、超参数的设定方法等。 -
应对过拟合,使用权值衰减、Dropout等正则化方法,并进行实现。 -
Batch Normalization方法
目的:
高效地进行神经网络(深度学习)的学习,提高识别精度。
一、参数的更新
神经网络的学习的目的:
找到使Loss函数的值尽可能小的参数。
即最优化:
寻找最优参数的问题
为了找到最优参数 ——> 梯度下降法
SGD:
SGD:随机梯度下降法(stochastic gradient descent)
定义:
使用参数的梯度,沿梯度方向更新参数,并重复这个步骤多次,从而逐渐靠近最优参数。
式(6.1)为SGD更新公式:
SGD的策略:
朝着当前所在位置的坡度最大的方向前进。
也就是每看一个example就更新,比GD更快。
SGD缺点:
- 在解决某些问题时可能没有效率
- SGD呈“之”字形移动。这是一个相当低效的路径。
- 如果函数的形状非均向(anisotropic),比如呈延伸状,就会非常低效。
-
SGD低效的根本原因是,梯度的方向并没有指向最小值的方向。
(所以李宏毅GD1中说要Feature Scaling特征缩放一下,就可以对着圆心更新,效率高)
其它更优于SGD的方法:
- Momentum:参照小球在碗中滚动的物理规则进行移动
- AdaGrad:为参数的每个元素适当地调整更新步伐。
- Adam:将上述两个方法融合在一起
1、Momentum
式(6.3/4)为Momentum更新公式:
新出现了一个变量v,对应物理上的速度。
式(6.3)表示了物体在梯度方向上受力;αv这一项:在物体不受任何力时,该项承担使物体逐渐减速的任务(α设定为0.9之类的值),对应物理上的地面摩擦或空气阻力。
Momentum方法就像是小球在碗中滚动。
优点:
- 和SGD相比,“之”字形的“程度”减轻了。
- 可以更快地朝x轴方向靠近,减弱“之”字形的变动程度。
Momentum的代码实现(源代码在 common/optimizer.py中)
class Momentum:
def __init__(self, lr=0.01, momentum=0.9): # momentum = α,设定为0.9
self.lr = lr
self.momentum = momentum
self.v = None # 初始化时,v中什么都不保存
# 第一次调用 update()时,v会以字典型变量的形式保存与参数结构相同的数据
def update(self, params, grads):
if self.v is None:
self.v = {} # 变量v 保存物体的速度
for key, val in params.items(): # params.items()为参数权重W
self.v[key] = np.zeros_like(val) # v会以字典的形式保存与参数W结构相同的数据
for key in params.keys():
self.v[key] = self.momentum*self.v[key] - self.lr*grads[key] # 式(6.3)
params[key] += self.v[key] # 式(6.4) W = W + v
2、AdaGrad
关于学习率的技巧:
学习率衰减(learning rate decay):
即随着学习的进行,使学习率逐渐减小。一开始“多”学,然后逐渐“少”学的方法,在神经网络的学习中经常被使用。
AdaGrad进一步发展了这个想法,针对“一个一个”的参数,赋予其“定制”的值。
式(6.5/6)为Adagrad更新公式:
变量h ——> 保存以前的所有梯度值的平方和
在更新参数时,通过除以 <过去所有梯度的均方根>,就可以调整学习的尺度(lr)。
参数的元素中变动较大(被大幅更新)的元素的学习率将变小。
即可以按参数的元素进行学习率衰减,使变动大的参数的学习率逐渐减小。
AdaGrad优点:
- 函数的取值高效地向着最小值移动
- 刚开始变动较大,但是后面会根据这个较大的变动按比例进行调整,减小更新的步伐。
- 因此,y轴方向上的更新程度被减弱,“之”字形的变动程度有所衰减。
学习越深入,更新的幅度就越小。如果无止境地学习,更新量就会变为 0,完全不再更新。为了改善这个问题,可以使用
RMSProp [7] 方法。
- 逐渐地遗忘过去的梯度,在做加法运算时将新梯度的信息更多地反映出来。
- 即指数移动平均:呈指数函数式地减小过去的梯度的尺度。
用python实现 AdaGrad,(源 代 码 在common/optimizer.py中)
class AdaGrad:
def __init__(self, lr=0.01):
self.lr = lr
self.h = None
# 第一次调用 update()时,h会以字典的形式保存与参数结构相同的数据
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) # h会以字典的形式保存与参数结构相同的数据
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)
# 防止除数为0
3、Adam
Adam优点:
通过组合前面两个方法的优点:
- 有望实现参数空间的高效搜索。
- 进行超参数的“偏置校正”
也像Momentun一样,像小球在碗中滚动。但是,Adam的小球左右摇晃的程度有所减轻。因为学习的更新程度被适当地调整了(Adagrad)。
Adam会设置3个超参数。
- 一个是学习率(论文中以α出现),
- 另外两个是一次momentum系数β1和二次momentum系数β2。
- 根据论文,标准的设定值是β1为0.9,β2 为0.999。设置了这些值后,大多数情况下都能顺利运行。
用python实现 Adam,(源 代 码 在common/optimizer.py中)
class Adam:
"""Adam (http://arxiv.org/abs/1412.6980v8)"""
def __init__(self, lr=0.001, beta1=0.9, beta2=0.999): # 一次momentum系数β1和二次momentum系数β2
self.lr = lr
self.beta1 = beta1
self.beta2 = beta2
self.iter = 0
self.m = None
self.v = None
def update(self, params, grads):
if self.m is None:
self.m, self.v = {}, {}
for key, val in params.items():
self.m[key] = np.zeros_like(val)
self.v[key] = np.zeros_like(val)
self.iter += 1
lr_t = self.lr * np.sqrt(1.0 - self.beta2**self.iter) / (1.0 - self.beta1**self.iter)
for key in params.keys():
#self.m[key] = self.beta1*self.m[key] + (1-self.beta1)*grads[key]
#self.v[key] = self.beta2*self.v[key] + (1-self.beta2)*(grads[key]**2)
self.m[key] += (1 - self.beta1) * (grads[key] - self.m[key])
self.v[key] += (1 - self.beta2) * (grads[key]**2 - self.v[key])
params[key] -= lr_t * self.m[key] / (np.sqrt(self.v[key]) + 1e-7)
#unbias_m += (1 - self.beta1) * (grads[key] - self.m[key]) # correct bias
#unbisa_b += (1 - self.beta2) * (grads[key]*grads[key] - self.v[key]) # correct bias
#params[key] += self.lr * unbias_m / (np.sqrt(unbisa_b) + 1e-7)
使用哪种更新方法呢?
比较一下这4种方法(源代码在 ch06/optimizer_compare_naive.py中)。
根据使用的方法不同,参数更新的路径也不同。
只看这个图的话,AdaGrad似乎是最好的,但
- 其实结果会根据要解决的问题而变。
- 超参数(学习率等)的设定值不同,结果也会发生变化。
这4种方法各有各的特点,各有擅长和不擅长(就像人一样,你能存粹的比较两个人谁好谁坏嘛)。
很多研究中至今仍在使用SGD。最近,很多研究人员都喜欢用Adam。本书将主要使用SGD或者Adam。
基于MNIST数据集的更新方法的比较
以手写数字识 别为例,比较SGD、Momentum、AdaGrad、Adam这4种更新方法(源代码在 ch06/optimizer_compare_mnist.py中)。
这个实验以一个5层神经网络为对象,其中每层有100个神经元。激活函数使用的是ReLU。
结论:
- 与SGD相比,其他3种方法学习得更快,AdaGrad的学习进行得稍微快一点。
- 不过实验结果会随学习率lr等超参数、神经网络的结构(几层深等)的不同而发生变化。
- 一般而言,与SGD相比,其他3种方法可以学习得更快,最终的识别精度也更高。
二、权重的初始值
接下来将介绍:
- 设定什么样的权重初始值(关系到神经网络的学习能否成功)
- 介绍权重初始值的推荐值:Xavier初始值
- 通过实验确认神经网络的学习是否会快速进行
为什么要设置权重初始值?
这关系到神经网络的学习能否成功进行下去。
权值衰减(weight decay):
如果想减小权重的值,一开始就将初始值设为较小的值最好。
实际上,我们会 设置 标准差(weitght_int_std)为0.01的高斯分布作为权值W的随机初始值。如下:
W = 0.01 * np.random.randn(10, 100)
W = weitght_int_std * np.random.randn(10, 100)
为什么不能直接设置初始值为0,这不是最小嘛?
不能设置为0,如果设置为0,直接把输入杀掉了不是吗。
为什么设置为随机初始值呢?
实际上,权重初始值不能设置为一样的值。
因为如果设置成一样的权重值,在误差反向传播法中,所有的权重值都会进行相同的更新。并拥有了许多重复的值,这使得神经网络拥有许多不同的权重的意义丧失了。为了防止“权重均一化”,
必须随机生成初始值,且不能为0。
隐藏层的激活值的分布
使用Xavier初始值:如果前一层的节点数为n,则初始值使用标准差为
1/√n
的高斯分布进行初始化。
为什么使用Xavier初始值?
为了使各层的激活值呈现出具有相同
广度
的分布。
因为通过在各层间传递多样性(具有广度)的数据,神经网络可以进行高效的学习。
反过来,如果传递的是有所偏向的数据,就会出现梯度消失或者“广度受限”的问题,导致学习可能无法顺利进行。
权重标准差为1的高斯分布,各层的激活值呈偏向0和1的分布。
sigmoid函数是S型函数,靠近0和靠近1的地方,它的导数逐渐接近0。导致梯度消失。如下图所示:(该实验的源代码在 ch06/weight_init_activation_histogram.py中)
权重初始值:标准差为0.01的高斯分布
,各层的激活值的分布集中在0.5附近,不会发生梯度消失,但是广度不够。因为如果有多个神经元都输出几乎相同的值,那它们就没有存在的意义了。如下图所示:
所以使用Xavier初始值,
各层的激活值呈现出具有广度的分布。
如下图所示,
权重初始值为:Xavier初始值;激活函数为Sigmoid
:
改善稍微歪斜的分布:
激活函数用tanh函数(双曲线函数)代替 sigmoid函数。
如下图所示:
众所周知,用作激活函数的函数最好具有关于原点对称的性质。
而tanh函数就是关于原点对称的S型函数。
ReLU的权重初始值
He 初始值使用标准差为
√(2/n)
的高斯分布。
(直观上)可以解释为,因为ReLU的负值区域为0,为了使它更有广度,所以需要2倍的系数。
总结:
激活函数/权重初始值 | sigmoid | tanh | ReLU |
---|---|---|---|
Xavier初始值 | ✔ | ✔ | |
He初始值 | ✔ |
- 当激活函数使用ReLU时,权重初始值使用He初始值;
- 当激活函数为 sigmoid或 tanh等S型曲线函数时,初始值使用Xavier初始值。
- 这是目前的最佳实践。
基于MNIST数据集的权重初始值的比较
采用不同的权重初始值:std = 0.01、Xavier初始值、He初始值进行实验(源代码在 ch06/weight_init_compare.py中)。得到的结果如下图所示:
这个实验神经网络有5层,每层有100个神经元,激活函数使用的是ReLU。
结论:
- std = 0.01时完全无法进行学习
- 权重初始值为Xavier初始值和He初始值时,学习进行得都很顺利。
- 并且He初始值学习进度更快一些。
综上,在神经网络的学习中,权重初始值非常重要。关系到能否成功得进行学习。
三、Batch Normalization
1、Batch Normalization的算法
是什么?
批归一化的算法。以进行学习时的mini-batch为单位,按mini-batch进行正规化。就是进行使数据分布的均值为0、方差为1的正规化。
干什么?方法的思路
为了使各层拥有适当的广度,“强制性”地调整激活值的分布。
即调整各层的激活值分布使其拥有适当的广度。
有什么优点?
- 可以使学习快速进行(可以增大学习率),训练得时候不用等那么久
- 不那么依赖初始值(对于初始值不那么神经质)
- 抑制过拟合(降低Dropout等的必要性)
Batch Norm层放在哪里?
通过将这个处理插入到激活函数的前面(或后面),可以减小数据分布的偏向。使数据分布更广。(近几年主要放激活函数后面多一点)
前面说了,Batch Norm,顾名思义,以进行学习时的mini-batch为单位,按mini-batch进行正规化。就是进行使数据分布的均值为0、方差为1的正规化。
用数学式表示的话,如下所示。
数学公式
接着,Batch Norm层会对正规化后的数据进行缩放和平移的变换,公式如下所示:
这个算法是神经网络上的正向传播,下图所示为Batch Norm正向传播的计算图:
Batch Norm的反向传播的推导有些复杂。不过如果使用图6-17的计算图来思考的话,Batch Norm的反向传播或许也能比较轻松地推导出来。Frederik Kratzert 的博客“Understanding the backward pass through Batch Normalization Layer”[13]里有详细说明,感兴趣可以参考一下。
2、Batch Normalization的评估
实验一:
使用 MNIST 数据集,观察使用Batch Norm层和不使用Batch Norm层时学习效果怎么样(源代码在 ch06/batch_norm_test.py中),结果如图6-18所示。
结论:
使用Batch Norm后,学习进行得更快了。
实验二:
给予不同的初始值,观察学习的效果。下图是权重初始值的标准差为各种不同的值时的学习过程图。
结论:
- 几乎所有的情况下都是使用Batch Norm时学习进行得更快
- 不使用Batch Norm的情况下,特别依赖权重初始值,如果选不好,学习将完全无法进行。
- 综上,通过使用Batch Norm,可以推动学习的进行。
- 并且,对权重初始值变得健壮(“对初始值健壮”表示不那么依赖初始值)。
四、正则化
什么是正则化?
为什么要正则化?
防止过拟合
发生过拟合的原因,主要有以下两个。
- 模型拥有大量参数、表现力强。
- 训练数据少。
抑制过拟合的技巧:
- 权值衰减:减小权重参数的值。(后续在 <怎么正则化?> 中会详细介绍)
- Dropout:在学习的过程中随机删除神经元。(模型复杂,只用权值衰减效果没那么好)
正则化项:假设有权重W = (w1, w2, . . . , wn)
- L1范数:各个元素的绝对值之和,相当于|w1| + |w2| + . . . + |wn|。
-
L2范数:各个元素的平方和,再开根号。
L∞范数:也叫Max范数,取各个元素的绝对值中最大的那一个。
怎么正则化?
1、使用权值衰减的方法:
利用正则化项L2范数,将权值衰减加上L2范数:½λW²。
- 将这个权值衰减的值加到损失函数中,亦能减少损失函数的值(这是神经网络学习的主要目的呀:减少损失函数的值!)
- λ是控制正则化强度的超参数。λ越大,对大的权重W抑制越多。
- ½的目的是将½λW²的求导结果变成λW。
- 在求权重梯度时,误差反向传播法的结果则要加上正则化项的导数λW。
做实验:故意造成过拟合
实验一:不使用权值衰减
训练数据的识别精度几乎是100%(可想而知,过拟合多严重啦!)
实验二:使用权值衰减,超参数 λ=0.1
2、使用Dropout的方法:
- 在学习的过程中随机删除神经元的方法。
- 即训练时,每传递一次数据,就会随机选择要删除的神经元。
-
测试时,虽然会传递所有的神经元信号,但是对于各个神经元的输出,要乘上训练时的删除比例后再输出。(
深度学习框架中不需要这一步
,因为训练时进行了恰当的计算)
Dropout的简单实现:
class Dropout:
def __init__(self, dropout_ratio=0.5): # dropout_ratio:删除神经元的比例
self.dropout_ratio = dropout_ratio
self.mask = None
def forward(self, x, train_flg=True):
if train_flg:
# 随机生成和x形状相同的数组,并和dropout_ratio相比较。>,mask=True;<,mask=False
self.mask = np.random.rand(*x.shape) > self.dropout_ratio
return x * self.mask # 返回Fasle的地方 x=0,即删除掉了神经元
else:
return x * (1.0 - self.dropout_ratio)
# 反向传播时的行为和ReLU相同。正向传播传递了神经元,反向传播按原样传递信号;正向传播没有传递神经元,反向传播信号将停在那里。
def backward(self, dout):
return dout * self.mask
做实验:
使用MNIST数据集进行验证,以确认Dropout的效果
前提:使用7层网络(每层有100个神经元,激活函数为ReLU)(挺深的网络)
实验一:没有使用Dropout
实验二:使用Dropout
下图为使用Dropout的网络,虽然有效抑制了过拟合,但是识别精度不是很高,这还和别的超参数的设置有关;比如下图dropout_rate=0.2;我再把dropout_rate=0.15做了一次实验。可以看到识别精度有较大改善。(图二)
总结:
- 通过使用Dropout,训练数据和测试数据的识别精度的差距变小了。
- 训练数据也没有到达100%的识别精度。
- 通过使用Dropout,即便是表现力强的网络(深网络),也可以抑制过拟合。
-
其实还可以通过改进其它超参数的值来抑制过拟合,而且能使网络的识别精度表现更好,继续看
<五、超参数的验证>
Dropout 类似于 集成学习
集成学习定义:让多个模型单独进行学习,推理时再取多个模型的输出的平均值。
即 有5个结构相同(或类似)的网络,分别进行学习;测试时,以这5个网络的输出的平均值作为答案。
通过集成学习,神经网络的识别精度可提高好几个百分点。
Dropout可以理解为,通过在学习过程中随机删除神经元,形成了不同的模型,从而每一次都让不同的模型进行学习。并且,推理时,通过对神经元的输出乘以删除比例(比如0.5等),可以取得模型的平均值。
所以Dropout相当于用一个网络随机删除神经元后变成了不同的几个网络,再进行学习。也就是集成学习的意思。
五、超参数的验证
- 本节介绍高效寻找超参数的值的方法:通过实验不断缩小超参数的范围
什么是超参数?(Hyper-Parameter)
比如各层的神经元数量、batch大小、参数更新时的学习率lr或权值衰减weigh decay等。
为什么要设置合适的超参数?
如果超参数没有设置合适的值,模型的性能就会很差。
- 训练数据用于参数(权重和偏置)的学习;
- 测试数据用于评估泛化能力;(比较理想的是只用一次测试数据评估,多次的话会导致数据过拟合测试数据)
- 还需要一笔验证数据(validation data):用于超参数的性能评估。
为什么不能用测试数据去评估超参数,从而选择合适的超参数?
会导致超参数的值被调整为只拟合测试数据。
验证数据哪里来?
如果数据集只有训练数据和测试数据,则需要自己事先从训练数据中抽一笔出来。比如从训练数据中事先分割出20%做验证数据。
而且,分割训练数据前,先打乱了输入数据和监督标签。这是因为数据集的数据可能存在偏向(比如,数据从“0”到“10”按顺序排列等)。
比如对于MNIST数据集,分割训练数据做验证数据的代码如下所示:
(x_train, t_train), (x_test, t_test) = load_mnist() # 载入MNIST数据集
# 打乱训练数据
x_train, t_train = shuffle_dataset(x_train, t_train)
# 分割出验证数据
validation_rate = 0.20 # 从训练数据中事先分割出20%做验证数据
validation_num = int(x_train.shape[0] * validation_rate) # x_train.shape[0]表示训练数据的数量(矩阵数据表示有几行)
# 比如有100笔训练数据,则validation_num=20
x_val = x_train[:validation_num]
t_val = t_train[:validation_num] # x_val,t_val 表示验证数据
x_train = x_train[validation_num:]
t_train = t_train[validation_num:] # 其余数据还给训练数据
使用验证数据观察超参数的最优化方法:不断尝试,减小最优超参数的取值范围,最终确定一个值。
超参数的最优化方法的步骤:
- 设定超参数的大致范围。(以“10 的阶乘”的尺度指定范围)
- 从设定的超参数范围中随机采样,随机选择一个超参数。(随机采样的搜索方式效果更好)(尽早放弃那些不符合逻辑的超参数)
- 使用步骤2中随机选择的超参数的值进行学习,通过验证数据评估识别精度(但是要将epoch设置得很小,从而缩短一次评估所需的时间)。
- 重复步骤2和步骤3(约100次等),根据它们的识别精度的结果,缩小超参数的范围。
- 反复进行上述操作,不断缩小超参数的范围,在缩小到一定程度时,从该范围中选出一个超参数的值。
如需更精炼的方法,可以使用
贝叶斯最优化
(Bayesian
optimization)。贝叶斯最优化能够更加严密、高效地进行最优化。详细内容请参考论文“Practical Bayesian
Optimization of Machine Learning Algorithms”[16]等。
超参数最优化的具体实现案例
- 使用MNIST数据集进行超参数的最优化。(源代码在 ch06/hyperparameter_optimization.py中。)
- 这里我们将 对 学习率 lr 和控制权值衰减强度的系数(下文称为“权值衰减系数”)weight decay 这两个超参数 进行优化。
步骤如下:
-
设定超参数的大致范围:权值衰减系数 weight decay 的初始范围为10
−8到10
−4,学习率 lr 的初始范围为10
−6到10
−2。 - 随机采样,随机选择一个超参数:超参数的随机采样的代码如下所示。
weight_decay = 10 ** np.random.uniform(-8, -4)
lr = 10 ** np.random.uniform(-6, -2)
- 再使用那些值进行学习。之后,多次使用各种超参数的值重复进行学习。(约100次等)
- 观察合乎逻辑的超参数在哪里。
学习结果图:
取出“Best-1”到“Best-5”中的超参数的值,缩小两个超参数的范围至 学习率lr = 0.001到0.01、权值衰减系数weight decay在10^−8到
10^−6之间时,学习可以顺利进行。
Best-1(val acc:0.77) | lr:0.008991967621142708, weight decay:5.425501025852778e-07
Best-2(val acc:0.77) | lr:0.006609255709609137, weight decay:8.124543461085908e-06
Best-3(val acc:0.76) | lr:0.007539029001714733, weight decay:1.2510664720534263e-06
Best-4(val acc:0.74) | lr:0.006670405931523085, weight decay:2.915136844957729e-05
Best-5(val acc:0.66) | lr:0.005631468995380081, weight decay:2.049732949146344e-07
在这个缩小的范围中,继续上述步骤,再缩小范围。最终选择一个范围内的超参数的值。
六、小结
本章我们介绍了神经网络的学习中的几个重要技巧。
- 参数的更新方法
- 权重初始值的赋值方法
- Batch Normalization
- Dropout等。
- 而且都在最先进的深度学习中被频繁使用。
总结
- 参数的更新方法,除 了 SGD 之 外,还 有 Momentum、AdaGrad、Adam等方法。(而且后三者往往比SGD效果好)
- 权重初始值的赋值方法对进行正确的学习非常重要。
- 作为权重初始值,Xavier初始值(用于Sigmoid和tanh)、He初始值(用于ReLU)等比较有效。
- 通过使用Batch Normalization,可以加速学习,并且对初始值变得健壮(不那么敏感)。
- 抑制过拟合的正则化技术有权值衰减、Dropout等。
- 通过验证数据,进行实验,来逐渐缩小超参数最优值的范围是最终选择超参数的一个有效方法。
——END——
注:如有细节处没有写到的,请继续精读《深度学习入门》,对小白来说真的是非常通俗易懂的深度学习入门书籍。(书中的例子主要是基于CV的)