写在前面,本人初次学习深度学习,本文用于记录和梳理知识点。
1 参数的更新
神经网络的学习的目的是找到使损失函数的值尽可能小的参数。这是寻找最优参数的问题,解决这个问题的过程称为最优化。
在前几章中,为了寻找最优参数,我们将参数的梯度(导数)作为了线索。使用参数的梯度,沿梯度方向更新参数,并重复这个步骤多次,从而逐渐靠近最优参数,这个过程称为随机梯度下降法,简称SGD。本文将介绍SGD、Momentum、AdaGrad、Adam四种更新参数的方法。
2 SGD
用数学式可以将SGD写成如下的式子(6.1):
SGD是朝着梯度方向只前进一定距离的简单方法。
实现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]
将SGD类应用在第五章两层神经网络实现程序中,替换成以下(其他更新方法一样):
#更新权重参数
# for key in ('W1','b1','W2','b2'):
# #不断的去更新权重参数,学习率可表示更新的幅度
# network.params[key] -= learning_rate * grad[key]
optimizer.update(params=network.params,grads=grad) #需初始定义 optimizer = SGD()
SGD的缺点
如图6-3所示,SGD呈“之”字形移动。这是相当低效的路径。也就是说,SGD的缺点是,如果函数的形状非均向,比如呈延伸状,搜索的路径就会非常低效。即SGD低效的根本原因是,梯度的方向并没有指向最小值的方向。
对于此类图的说明:这类图中的曲线被称为等高线,外圈高度越高,内圈高度越低,为找到最优参数,沿着梯度方向(最小值方向)更新参数,也就是说,向等高线越低的方向进行,其实可以将上图看做一个碗(后续的图也一样)。
3 Momentum
用数学式表示Momentum方法,如下所示:
新变量v对应物理上的速度。式(6.3)表示了物体在梯度方向上受力,在这个力的作用下,物体的速度增加这一物理法则。
在物体不受任何力时,阿尔法*v这一项承担使物体逐渐减速的任务(阿尔法值设定0.9之类的值),这一项对应物理上的地面摩擦或空气阻力。
Momentum代码实现:
import numpy as np
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(): #key:W、b val对应的参数数组
self.v[key] = np.zeros_like(val) #目的是构建一个与val同维度的数组,并初始化所有变量为零。
for key in params.keys():
#根据梯度grads更新v 再更新权重参数
self.v[key] = self.momentum * self.v[key] - self.lr * grads[key]
params[key] += self.v[key]
图6-5中,更新路径就像小球在碗中滚动一样。和SGD相比,我们发现“之”字形的“程度”减轻了,可以更快地朝x轴方向靠近,减弱“之”字形的变动程度。对于小球的方向,受x轴力与y轴方向的正或反向力影响。
4 AdaGrad
学习率衰减法:即随着学习的进行,使学习率逐渐减小。也就是说,一开始“多学”,然后逐渐“少”学的方法。逐渐减小学习率的想法,相当于将“全体”参数的学习率值一起降低。AdaGrad进一步发展了这个想法,为参数的每一个元素适当地调整学习率,同时进行学习。用数学式表示AdaGrad的更新方法:
变量h保存了以前的所有梯度值的平方和(6.5式中符号表示对应矩阵元素的乘法)。6.6式表示,更新参数,通过乘以1/更号h,调整学习的尺度。这意味着,参数的元素中变动比较大(被大幅更新)的元素的学习率将变小。也就是说,可以按参数的元素进行学习率衰减,使变动大的参数的学习率逐渐减小。
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] #式6-5
params[key] -= self.lr * grads[key] / (np.sqrt(self.h[key]) + 1e-7) #式6-6 1e-7防止当self.h[key]中有0时,将0用作除数的情况
由图6-6的结果所示,函数的取值高效地向着最小值移动。刚开始y轴方向上的梯度较大,所以变动较大,但是在后面会根据这个较大的变动按比例进行调整(不需要纠结比例问题),减小更新的步伐。因此,y轴方向上的更新程度被减弱,“之”字形的变动程度有所衰减。
5 Adam
Adam方法的基本思路:将Momentum参照小球在碗中滚动的物理规则进行移动和AdaGrad为参数的每一个元素适当地调整更新步伐,这两种方法相结合起来。
用数学式表示Adam如下:
Adam代码实现以及参数说明:
class Adam:
def __init__(self,lr = 0.001,beta1 = 0.9,beta2 = 0.999):
self.lr = lr #初始化学习率
#beta1、beta2控制移动平均的指数衰减率
self.beta1 = beta1
self.beta2 = beta2
self.iter = 0 #初始化步长
#m设定为第一个矩阵,v设定为第二个矩阵
self.m = None
self.v = None
def update(self,params,grads):
if self.m is None:
self.m,self.v = {},{}
#构建一个key(W1/W2/b1/b2)对应的与val同维度的数组,并初始化所有变量为零。
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] += (1.0 - self.beta1) * (grads[key] - self.m[key])
#(计算偏差校正的第二原始矩估计)
self.v[key] += (1.0 - self.beta2) * (grads[key]**2 - self.v[key])
#进行参数更新
params[key] -= lr_t * self.m[key] / (np.sqrt(self.v[key]) + 1e-7)
权重的初始值
关于权重的初始值的设定关系到神经网络的学习能否成功。
权值衰减:就是一种以减少权重参数的值为目的进行学习的方法。通过减小权重参数的值来抑制过拟合的发生。
如果想减小权重的值,一开始就应该将初始值设为较小的值,但是不能设为0,否则就无法进行学习,也就是说不能将权重初始值设定为一样的值。否则在误差反向传播中,所有的权重参数将会进行一样的更新,而权重参数不能均一化。
引入梯度消失概念:以激活函数sigmoid函数为例。
神经网络主要的训练方法是BP算法,BP算法的基础是导数的链式法则,也就是多个导数的乘积。sigmoid的导数最大为0.25,且大部分的激活值(激活函数的输出数据)都被推向两侧饱和区域(0和1区域),这就导致大部分数值经过sigmoid激活函数之后,其导数都非常的小,多个小于等于0.25的数值相乘,其运算结果很小。随着神经网络的层数的加深,最后更新浅层网络(前面的层)权重参数就基本不会有什么波动,也就没有将loss的信息传递到浅层网络,这样网络就无法训练学习。这样称之为梯度消失。
隐藏层的激活值的分布
以下将以sigmoid为激活函数,用不同的初始化权重参数查看激活值的分布。
实验以五层神经网络为例,代码如下:
import numpy as np
import matplotlib.pyplot as plt
def sigmoid(x):
return 1 / (1 + np.exp(-x))
def ReLU(x):
return np.maximum(0, x)
def tanh(x):
return np.tanh(x)
input_data = np.random.randn(1000, 100) # 1000个数据 x
node_num = 100 # 各隐藏层的节点(神经元)数
hidden_layer_size = 5 # 隐藏层有5层
activations = {} # 激活值的结果保存在这里
x = input_data
for i in range(hidden_layer_size):
if i != 0:
x = activations[i-1]
# 改变初始值进行实验!
w = np.random.randn(node_num, node_num) * 1 #
# w = np.random.randn(node_num, node_num) * 0.01
# w = np.random.randn(node_num, node_num) * np.sqrt(1.0 / node_num)
# w = np.random.randn(node_num, node_num) * np.sqrt(2.0 / node_num)
# print('w',w) #输出5个w权重参数矩阵 100*100
a = np.dot(x, w)
# 将激活函数的种类也改变,来进行实验!
z = sigmoid(a)
# z = ReLU(a)
# z = tanh(a)
activations[i] = z
# 绘制直方图
for i, a in activations.items():
#plt.subplot(nrows,ncols,index,**kwargs)
#参数说明:前面三个整数用于描述子图的位置信息,分别是行数、列数、索引值,子图将分布在行列的索引值位置上。
plt.subplot(1, len(activations), i+1)
plt.title(str(i+1) + "-layer") #plt.title()函数用于设置图像标题
if i != 0: plt.yticks([], []) #设置x或y轴对应显示的标签
# plt.xlim(0.1, 1)
# plt.ylim(0, 7000)
# 将一个大区间划分为等间隔的小区间,并统计每个区间上样本出现的频数之和。
#plt.hist(x,bins,range,...)
# 参数说明:x作直方图所用的数据,必须是一维数组;多维数组需要进行偏平化再作图;必选参数
#bins直方图的柱数,即要分的组数,默认为10
#range 元组类型或None,剔除较大和较小的离群值,如果None,则默认为(x.min(),x.max());即x轴的范围
plt.hist(a.flatten(), 30,range=(0,1))
plt.show()
使用不同标准差的高斯分布和Xavier初始值作为权重参数时,各层激活值的分布结果:
使用标准差为1的高斯分布
使用标准差为0.01的高斯分布作为权重参数
使用Xavier初始值作为权重初始值
结果分析:Xavier初始值是以激活函数是线性函数为前提而推导出来的。因为sigmoid 函数和 tanh 函数左右对称,且中央附近可以视作线性函数,所以适合使用Xavier初始值。
ReLU的权重初始值
当激活函数使用ReLU时,一般推荐使用ReLU专用的初始值,也称“He初始值”。当前一层的节点数为n时,He初始值使用标准差为根号下2/n的高斯分布。如图可知,当初始值为He初始值时,各层中分布的广度相同,由于即使层加深,数据的广度也能保持不变,因此逆向传播时,也会传递合适的值。
总结:当激活函数使用ReLU时,权重初始值使用He初始值,当激活函数是sigmoid或tanh等S型曲线函数时,初始值使用Xavier初始值。
基于MNIST数据集的权重初始值的比较
此处涉及全连接的多层神经网络,将会专门写一篇文章来概述,并且测试不同激活函数与不同权重初始值的关联影响。
此处主要是了解不同权重初始值在使用ReLU作为激活函数时对学习的影响。也算验证当激活函数使用ReLU时,权重初始值使用He初始值的实验。
横轴是学习的迭代次数,纵轴是损失函数的值。
这个实验室基于5层神经网络,每层有100个神经元,激活函数使用ReLU。当std为0.01时,完全无法学习,使用Xavier和He初始值时,学习进行的很顺利。