神经网络的学习的目的就是找到合适的参数使损失函数的值尽可能的小。这种寻找最优参数的过程就叫做最优化(optimization)。然而在深度神经网络中,参数的数量非常庞大,导致最优化的问题非常复杂。下面介绍四种常见的最优化的方法,并通过一个例子进行比较。
SGD即随机梯度下降法,这个方法通过梯度下降法更新参数,不过因为这里使用的数据是随机选择的min batch数据,所以称为随机梯度下降法。这里的“随机”指的是“随机选择的”的意思,因此随机梯度下降法是“对随机选择的数据进行的梯度下降法”。用数学式表示为:
这里把需要更新的权重参数记为W,把损失函数关于W的梯度记为。表示学习率,实际上会取0.001或0.0001这些事先决定好的值。这个式子表示,SGD是朝着梯度方向前进一小步的简单方法。现在将SGD实现为一个python类。
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]
Momentum是“动量”的意思,和物理有关。用数学式表示如下:
和前面SGD一样,W表示要更新的权重参数,表示损失函数关于W的梯度,表示学习率。这里新出现了一个变量v,对应物理上的速度。第一个式子表示物体在梯度方向上受力,在这个力的作用下,物体的速度增加这一法则。表示物理在不受任何力时,该项承担使物体逐渐减速的任务(设定为0.9之类的值),对应物理上的地面摩擦力或空气阻力。Momentum方法给人的感觉就像是小球在地面上滚动。
下面是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]
实例变量v会保存物体的速度。初始化时,v中什么都不保存,但当第一次调用update()时,v会以字典型变量的形式保存与参数结构相同的数据。
在神经网络的学习中,学习率(数学式中记为)的值很重要。学习率过小,会导致学习花费过多时间;学习率过大,则会导致学习发散而不能正确进行。
在关于学习率的有效技巧中,有一种被称为学习率衰减(learning rate decay)的方法,即随着学习的进行,使学习率逐渐减小。实际上,一开始“多”学,然后逐渐“少”学的方法,在神经网络的学习中经常被使用。
逐渐减少学习率的想法,相当于将“全体”参数的学习率值一起降低。而AdaGrad进一步发展了这个想法,针对“一个一个”参数,赋予其“定制” 的值。用数学式表示:
和前面SGD一样,W表示要更新的权重参数,表示损失函数关于W的梯度,表示学习率。这里新出现了一个变量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]
params[key] -= self.lr * grads[key] / (np.sqrt(self.h[key]) + 1e-7)
这里需要注意的是,最后加上的1e-7是为了防止self.h[key]中有0时,将0用作除数的情况。在很多深度学习框架中,这个微小值也可以设定为参数,但这里我们用的是1e-7这个固定值。
拓展:AdaGrad会记录过去所有梯度的平方和。因此,学习越深入,更新的幅度越小。实际上,如果无止境的学习,更新量就会变成0,完全不再更新。为了改善这个问题,可以使用RMSProp方法。RMSProp方法不是将过去的所有梯度一视同仁的相加,而是逐渐遗忘过去的梯度,在做加法运算时将新的梯度信息更多的反映出来。这种操作从专业上讲,称为“指数移动平均”,呈指数函数式地减小过去的梯度的尺度。
Momentum参照小球在碗中滚动的物理规则进行移动,AdaGrad为参数的每个元素适当的调整更新步伐。Adam就是将这两个方法融合在一起。这只是一个直观的说明,并不完全正确,详细内容可以参考作者的论文。Diederik Kingma and Jimmy Ba. Adam: A Method for Stochastic Optimization.
class Adam:
def __init__(self, lr=0.001, beta1=0.9, beta2=0.999):
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] += (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)
例:下面用这个四种方法寻找的最小值点。
从(x,y)=(-7.0,2.0)的位置开始搜索,用导数法求梯度,,
具体代码如下:
import numpy as np
import matplotlib.pyplot as plt
from collections import OrderedDict
from ch06.optimizer import *
# 原函数
def f(x, y):
return x ** 2 / 20.0 + y ** 2
# 求梯度
def df(x, y):
return x / 10.0, 2.0 * y
init_pos = (-7.0, 2.0) # 初始点
params = {}
params['x'], params['y'] = init_pos[0], init_pos[1]
grads = {}
grads['x'], grads['y'] = 0, 0
optimizers = OrderedDict()
optimizers["SGD"] = SGD(lr=0.95)
optimizers["Momentum"] = Momentum(lr=0.1)
optimizers["AdaGrad"] = AdaGrad(lr=1.5)
optimizers["Adam"] = Adam(lr=0.3)
idx = 1
for key in optimizers:
optimizer = optimizers[key]
x_history = []
y_history = []
params['x'], params['y'] = init_pos[0], init_pos[1]
for i in range(30):
x_history.append(params['x'])
y_history.append(params['y'])
grads['x'], grads['y'] = df(params['x'], params['y'])
optimizer.update(params, grads)
x = np.arange(-10, 10, 0.01)
y = np.arange(-5, 5, 0.01)
X, Y = np.meshgrid(x, y) # 生成网格点坐标矩阵
Z = f(X, Y)
# for simple contour line
mask = Z > 7
Z[mask] = 0
# plot
plt.subplot(2, 2, idx)
idx += 1
plt.plot(x_history, y_history, 'o-', color="red")
plt.contour(X, Y, Z)
plt.ylim(-10, 10)
plt.xlim(-10, 10)
plt.plot(0, 0, '+')
# colorbar()
# spring()
plt.title(key)
plt.xlabel("x")
plt.ylabel("y")
plt.show()
运行结果:
到目前为止,介绍了4种最优化的方法,那么哪种方法好呢。其实并不存在能在所有问题中表现都好的方法。这4种方法各有各的特点,都有各自擅长的领域。目前用的比较多的就是SGD和Adam.