在前几章中,为了找到最优参数,我们将参数的梯度(导数)作为了线索。 使用参数的梯度,沿梯度方向更新参数,并重复这个步骤多次,从而逐渐靠 近最优参数,这个过程称为随机梯度下降法, 简称SGD。
虽然SGD简单,并且容易实现,但是在解决某些问题时可能没有效率。我们来思考一下求下面这个函数的最小值 的问题。
f
(
x
,
y
)
=
1
20
x
2
+
y
2
f(x,y) = \frac{1}{20}x^2+y^2
f(x,y)=201x2+y2
通过下图的梯度表示我们可以看出,该函数的梯度特征是在y轴方向上大,在x轴方向上小。换句话说,也就是在y轴方向上先更新到最小值处,x轴方向上后更新到最小值处。
下图为SGD的更新路径,SGD呈“之”字形移动。这是一个相当低效的路径。也就是说, SGD的缺点是,如果函数的形状非均向,比如呈延伸状,搜索 的路径就会非常低效。因此,我们需要比单纯朝梯度方向前进的SGD更聪 明的方法。SGD低效的根本原因是,梯度的方向并没有指向最小值的方向。
求函数最小值位置的代码
import numpy as np
def numerical_gradient(f, x):
h = 1e-4 # 0.0001
grad = np.zeros_like(x) # 生成和x形状相同的数组
for idx in range(x.size):
tmp_val = x[idx]
# f(x+h)的计算
x[idx] = tmp_val + h
fxh1 = f(x)
# f(x-h)的计算
x[idx] = tmp_val - h
fxh2 = f(x)
grad[idx] = (fxh1 - fxh2) / (2*h)
x[idx] = tmp_val # 还原值
return grad
def F(x):
return 1/20*x[0]*x[0]+x[1]*x[1]
x = np.array([-7.0,2.0])
for i in range(5000):
grad = numerical_gradient(F, x)
x = x - 0.01 * grad
print(x)
由代码可知,大概经历5000轮更新,更新到如下结果
Momentum是“动量”的意思,和物理有关。用数学式表示Momentum方 法,如下所示。
这里新出现了一个变量v,对应物理上的速度。 式(6.3)表示了物体在梯度方向上受力,在这个力的作用下,物体的速度增 加这一物理法则。式中有αv这一项。在物体不受任何力时,该项承担使物体逐渐减 速的任务(α设定为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]
求函数最小值位置的代码
import numpy as np
def numerical_gradient(f, x):
h = 1e-4 # 0.0001
grad = np.zeros_like(x) # 生成和x形状相同的数组
for idx in range(x.size):
tmp_val = x[idx]
# f(x+h)的计算
x[idx] = tmp_val + h
fxh1 = f(x)
# f(x-h)的计算
x[idx] = tmp_val - h
fxh2 = f(x)
grad[idx] = (fxh1 - fxh2) / (2*h)
x[idx] = tmp_val # 还原值
return grad
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]
return params
def F(x):
return 1/20*x[0]*x[0]+x[1]*x[1]
x = {}
grad = {}
x['W'] = np.array([-7.0,2.0])
notework = Momentum()
for i in range(400):
grad['W'] = numerical_gradient(F, x['W'])
print(notework.update(x, grad))
由代码可知,Momentum大约更新了400轮即更新到了以下结果,比SGD所需要的轮次更少。
由下图也可看出,“之”字形的“程度”减轻了。和SGD时的情形相比, 可以更快地朝x轴方向靠近,减弱“之”字形的变动程度。
在神经网络的学习中,学习率是非常的重要的。学习率过大过小都会影响我们的最后的结果。在有关学习率的技巧中,有一种叫做学习率衰减法即随着学习的进行,使学习率逐渐减小。实际上,一开始“多” 学,然后逐渐“少”学的方法,在神经网络的学习中经常被使用。
Adagrad优化算法被称为自适应学习率优化算法,之前我们讲的随机梯度下降对所有的参数都使用的固定的学习率进行参数更新,但是不同的参数梯度可能不一样,所以需要不同的学习率才能比较好的进行训练,但是这个事情又不能很好地被人为操作,所以 Adagrad 便能够帮助我们做这件事。
AdaGrad的更新方法
求函数最小值位置的代码
import numpy as np
def numerical_gradient(f, x):
h = 1e-4 # 0.0001
grad = np.zeros_like(x) # 生成和x形状相同的数组
for idx in range(x.size):
tmp_val = x[idx]
# f(x+h)的计算
x[idx] = tmp_val + h
fxh1 = f(x)
# f(x-h)的计算
x[idx] = tmp_val - h
fxh2 = f(x)
grad[idx] = (fxh1 - fxh2) / (2*h)
x[idx] = tmp_val # 还原值
return grad
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)
return params
def F(x):
return 1/20*x[0]*x[0]+x[1]*x[1]
x = {}
grad = {}
x['W'] = np.array([-7.0,2.0])
notework = AdaGrad()
for i in range(400000):
grad['W'] = numerical_gradient(F, x['W'])
print(notework.update(x, grad))
400000次更新后说到达的位置
由上可知,函数取值是较为高效的朝着最小值方向移动。由于y轴方 向上的梯度较大,因此刚开始变动较大,但是后面会根据这个较大的变动按 比例进行调整,减小更新的步伐。因此,需要更新更多的轮次才能到达最小值点。
Adam方法的基本思路是将Momentum和AdaGrad两种方法融合在一起。
Adam会设置 3个超参数。一个是学习率(论文中以α出现),另外两 个是一次momentum系数β1和二次momentum系数β2。根据论文, 标准的设定值是β1为 0.9,β2 为 0.999。设置了这些值后,大多数情 况下都能顺利运行。
在神经网络的学习中,权重的初始值特别重要。实际上,设定什么样的 权重初始值,经常关系到神经网络的学习能否成功。本节将介绍权重初始值 的推荐值,并通过实验确认神经网络的学习是否会快速进行。
其实,权重的初始值非但不能设置为0,设置成全部一样也是不行的。这是因为在误差反向传播法当中,如果权值的初始值都设置为一样的,第二层的所有权值都将会进行相同的更新,权值会被更新为相同的值,这使得神经网络拥有许多不同的权重的意义丧失了。为了防止“权重均一化” (严格地讲,是为了瓦解权重的对称结构),必须随机生成初始值。
观察隐藏层的激活值 A(激活函数的输出数据)的分布,可以获得很多启 发。这里,我们来做一个简单的实验,观察权重初始值是如何影响隐藏层的激活值的分布的。这里要做的实验是,向一个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,100)的数据
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 #此处乘的值极为标准差
z = np.dot(x,w)
a = sigmoid(z)
activations[i] = a
# a的形状为(1000,100),每一个值为(0,1)之间的数
# 下图绘制的为a中的元素大小分布图
for i, a in activations.items():
plt.subplot(1, len(activations), i+1) # 一行len(activations)列的图,当前为第i+1个
plt.title(str(i+1) + "-layer")
plt.hist(a.flatten(), 30, range=(0,1)) # 需要绘制的数组、分组数、范围
plt.show()
由上图可知,各层的激活值呈偏向0和1的分布。因为这里使用的是Sigmoid函数,反向传播时的局部倒数是y(1-y),随着输出不断地靠近0(或者靠近1),它的导数的值逐渐接近0。因此,偏向0和1的数据分布会造成反向传播中梯度的值不断变小,最后消失。这个问题称为梯度消失。层次加深的深度学习中,梯度消失的问题可能会更加严重。
下面,将权重的标准差设为0.01,进行相同的实验。实验的代码只需要把设定权重初始值的地方***1改为*0.01**
即可。使用标准差为0.01的高斯分布时,各层的激活值的分布 如图所示
由上图可知,这次呈现集中在0.5附近,因为不像刚才的例子那样偏向0和1,所 以不会发生梯度消失的问题。但是,激活值的分布有所偏向,说明在表现力上会有很大问题。因为如果有多个神经元都输出几乎相同的值,那它们就没有存在的意义了。比如,如果100个神经元都输出几乎相同的值,那么也可以由1个神经元来表达基本相同的事情。因此,激活值在 分布上有所偏向会出现“表现力受限”的问题。
直接上结论:初始值当使用标准差为 2 n \sqrt{\frac{2}{n}} n2的高斯分布
总结:当激活函数使用ReLU时,权重初始值使用He初始值( 2 n \sqrt{\frac{2}{n}} n2),当 激活函数为sigmoid或tanh等S型曲线函数时,初始值使用Xavier初始值( 1 n \sqrt{\frac{1}{n}} n1)。 这是目前的最佳实践。
有以上可知,当各层的激活值有了适当的广度,网络就可以进行顺利的学习。Batch Norm的思想上强制性的去调整激活值的分布。
Batch Norm优点如下:
Batch Norm的思路是调整各层的激活值分布使其拥有适当 的广度。为此,要向神经网络中插入对数据分布进行正规化的层,即Batch Normalization层
具体而言,就是进行使数据分布的均值为0、方差为1的 正规化。用数学式表示的话,如下所示:
这三个公式分别为求 均值、方差、正规化
将以上处理插入到 激活函数的前面(或者后面),可以减小数据分布的偏向。
Batch Norm层会对正规化后的数据进行缩放和平移的变换,用 数学式可以如下表示。
这里,γ和β是参数。一开始γ = 1,β = 0,然后再通过学习调整到合 适的值。
机器学习的问题中,过拟合是一个很常见的问题。过拟合指的是只能拟 合训练数据,但不能很好地拟合不包含在训练数据中的其他数据的状态。
发生过拟合的原因主要有以下两点:
权值衰减是一直以来经常被使用的一种抑制过拟合的方法。该方法通过 在学习的过程中对大的权重进行惩罚,来抑制过拟合。很多过拟合原本就是 因为权重参数取值过大才发生的。
表现在代码中:
前面所说的权值衰减的方法,在某种程度上可以抑制过拟合但是,如果网络的模型变得很复杂,只用权值衰减就难以应对了。在这种情况下,我们经常会使用Dropout]方法。
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置位1
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个结构相同(或者类似)的网络,分别进行 学习,测试时,以这 5个网络的输出的平均值作为答案。实验告诉我们,通过进行集成学习,神经网络的识别精度可以提高好几个百分点。 这个集成学习与 Dropout有密切的关系。这是因为可以将 Dropout 理解为,通过在学习过程中随机删除神经元,从而每一次都让不同 的模型进行学习。并且,推理时,通过对神经元的输出乘以删除比 例(比如,0.5等),可以取得模型的平均值。也就是说,可以理解成, Dropout将集成学习的效果(模拟地)通过一个网络实现了。
在神将网络中,超参数是指,比如各层的神经元数量、batch大小、参数更新时的学习率或权值衰减等。如果这些超参数没有设置合适的值,模型的性能就会很差。虽然超参数的取值非常重要,但是在决定超参数的过程中 一般会伴随很多的试错。本节将介绍尽可能高效地寻找超参数的值的方法。
之前,我们将数据分为训练数据和测试数据,训练数据用于参数的学习,测试数据用于评估模型的泛化能力。为了对超参数进行调整,还因该分出一部分称为验证数据,用于超参数的性能评估。
超参数优化过程:
步骤0:
设定超参数的范围。
weight_decay = 10 ** np.random.uniform(-8, -4) #10-8.10-4
lr = 10 ** np.random.uniform(-6, -2) #10-6,10-2
步骤1:
从设定的超参数范围中随机采样。
步骤2:
使用步骤1中采样到的超参数的值进行学习,通过验证数据评估识别精 度(但是要将epoch设置得很小)。
步骤3:
重复步骤1和步骤2(100次等),根据它们的识别精度的结果,缩小超参 数的范围。