《深度学习入门:基于Python的理论与实现》第6章-参数的更新

声明:这是原书的读书笔记,原书中的图,实在太漂亮了,我忍不住全扣下来了。。。强推原书。

参数优化

神经网络的学习的目的是找到使损失函数的值尽可能小的参数。这是寻找最优参数的问题,解决这个问题的过程称为最优化。

1 随机梯度下降

一种最基本的方法是将参数的梯度作为线索,使用参数的梯度,沿梯度方向更新参数,并重复这个步骤多次,从而逐渐靠近最优参数,这个过程称为随机梯度下降(Stochastic Gradient Descent,SGD)。

1.1 探险家故事

SGD可能听的最多的就是,下山,找坡度最大的方向,所以这里就直接引用原文的故事:

《深度学习入门:基于Python的理论与实现》第6章-参数的更新_第1张图片

1.2 SGD

SGD的数学公式是:

W \gets W - \eta \frac{\partial L}{\partial W}

W为要更新权重参数,把损失函数关于W的梯度记为\frac{\partial L}{\partial W}\eta表示学习率,事先设好的值(超参数)。

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]

其中 lr 表示 learning rate,学习率。

1.3 SGD的缺点

SGD的实现非常简单。但在解决某些问题时可能没用效率。

例如,有这样一个目标函数:

f(x, y) = \frac{1}{20}x^2 + y^2

《深度学习入门:基于Python的理论与实现》第6章-参数的更新_第2张图片

对于这个式子,用图表示梯度的话,可得到下图:

《深度学习入门:基于Python的理论与实现》第6章-参数的更新_第3张图片

可以看到这个梯度的特征是,y轴方向上大,x轴方向上小。换句话说就是y轴方向坡度大,x轴方向坡度小。虽然该式最小值(x,y) = (0,0)。但并非处处梯度执行(0,0)。

对这样一个形状的函数应用SGD,例如从(x,y)=(-7.0,2.0)处开始搜索,结果如下:

《深度学习入门:基于Python的理论与实现》第6章-参数的更新_第4张图片

SGD会呈“之”字形移动。这是一个相当低效的路径。也就是说,当函数的形状是非均向,比如呈延伸性,搜索的路径就会非常低效。其根本原因是梯度的方向并没有指向最小值的方向。

2 Momentum

改进方法一。

Momentum是“动量”的意思,即物理里的那个质量和速度相乘得到的动量。

用数学式表示如下:

v \gets \alpha v - \eta \frac{\partial L}{\partial W}

W \gets W + v

和前面一样,W表示要更新的权重参数,\frac{\partial L}{\partial W}表示损失函数关于W的梯度,\eta表示学习率。这里出现了一个新变量v,对应物理上的速度。前一个式子表明了物体在梯度方向上受力,在这个力作用下,物体的速度增加这一物理法则。Momentum方法给人的感觉就像是小球在地面上滚动。

《深度学习入门:基于Python的理论与实现》第6章-参数的更新_第5张图片

式中有 \alpha v 这一项。在物体不受力时,该项承担使物体逐渐减速的任务(\alpha设定为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]

实例变量v会保存物体的速度。初始化时,v中什么都不存,但当第一次调用update()时,v会以字典型变量的形式保存于参数结构相同的数据。剩余的代码就是将前述两式表现出来。

此时解决前面SGD遇到的问题,

《深度学习入门:基于Python的理论与实现》第6章-参数的更新_第6张图片

可以发现“之”字形的程度减轻了。这是因为虽然x轴方向上受到的力非常小,但是一直在同一方向上受力,所以朝同一个方向上会有一定的加速。反过来,虽然在y轴上受到的力很大,但因为在不停做往返运动,加速效果会低效,所以y轴方向上速度不稳定。因此,和SGD的情形相比,可以更快地朝x轴方向靠近,减弱“之”字形的变动程度。

3 AdaGrad

在神经网络的学习中,学习率的值很重要,设置过大,容易导致发散,在最优值左右徘徊,而设置过小容易导致学习速度非常慢。在关于学习率的有效技巧中,有一种被称为学习率衰减(learning rate decay)的方法,即随着学习的进行,使学习率主键减小。

逐渐减小学习率的想法,相当于将“全体”参数的学习率值一起降低。而AdaGrad进一步发展了这个想法,针对“一个一个”的参数,赋予其“定制”的值。

AdaGrad会为参数的每个元素适当地调整学习率,与此同时进行学习(AdaGrad的Ada来自单词Adaptive,即“适当的”的意思)。

AdaGrad的数学表示:

h \gets h + \frac{\partial L}{\partial W}\odot \frac{\partial L}{\partial W}

w \gets w - \eta \frac{1}{\sqrt{h}} \frac{\partial L}{\partial W}

和前面的SGD一样,W表示要更新的权重参数,\frac{\partial L}{\partial W}表示损失函数关于W的梯度,\eta表示学习率。这里出现了新变量h,看第一个式子可知,其保存了以前所有梯度的平方和(\odot表示矩阵对应位置相乘)。然后,在更新参数时,通过乘以\frac{1}{\sqrt{h}},就可以调整学习的尺度。这意味着,参数的元素中变动较大(被大幅更新)的元素的学习率将变小。也就是说,可以按照参数的元素进行学习率衰减,使变动大的参数的学习率逐渐减小。

AdaGrad会记录过去所有梯度的平方和。因此,学习越深入,更新的幅度就越小。实际上,如果无止境的学习,h的值会越来越大,导致2式的后一部分趋于0,则参数将不再更新。为了改善这个问题,可以使用RMSProp方法。RMSProp方法不是将过去所有的梯度一视同仁地相加,而是逐渐地遗忘过去的梯度,在做加法运算时将新梯度的信息更多地反映出来。这种操作从专业上讲,称为“指数移动平均”,呈指数函数式地减小过去的梯度的尺度。

现在来实现AdaGrad。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用作除数的情况。在很多深度学习的框架中,这个微小值也可以设定为参数,但这里这个值是个固定值。

现在,尝试用AdaGrad解决前面SGD出现的问题,结果如下:

《深度学习入门:基于Python的理论与实现》第6章-参数的更新_第7张图片

可以看到,函数的值高效地向着最小值移动。由于y轴方向上的梯度较大,因此刚开始变动较大,但是后面会根据这个较大的变动按比例进行调整,减小更新的步伐。因此,y轴方向上的更新程度被减弱,“之”字形的变动程度进一步衰减。

4 Adam

Momentum参照小球在碗中滚动的物理规则进行移动,AdaGrad为参数的每个元素适当地调整更新步伐。Adam是将这两个方法融合在一起。

通过组合前面两个方法的优点,有望实现参数空间的高效搜索。此外,进行超参数的“偏置矫正”也是Adam的特征。

根据Adam论文中的描述Adam的数学表达如下:

t \gets t + 1

g_t \gets \frac{\partial L}{\partial W}

m_t \gets \beta_1 * m_{t-1} + (1-\beta_1) * g_t

v_t \gets \beta_2 * v_{t-1} + (1-\beta_2) * g_t^2

\hat{m_t} \gets m_t / (1-\beta_1^t)

\hat{v_t} \gets v_t / (1-\beta_2^t)

w \gets w - \eta * \hat{m_t} / (\sqrt{\hat{v_t}} + \varepsilon )

其中g_t就是目标函数对待更新参数w的梯度,\varepsilon就是前面的微小值1e-9。

这里有一个记录更新次数的参数t,表示第几次调用update函数。

原文中提到,Adam会设置3个超参数。一个是学习率,另外两个是一次momentum系数\beta_1和二次momentum系数\beta_2。根据论文,标准的设定值是\beta_1是0.9,\beta_2是0.999。

根据原书中代码实现方案,Adam的数学表达如下:

t \gets t + 1

\eta \gets \eta * \frac{\sqrt{1-\beta_2^t}}{1-\beta_1^t}

m \gets m + (1-\beta_1) * (\frac{\partial L}{\partial W} - m)

v \gets v + (1-\beta_2) * ((\frac{\partial L}{\partial W})^2 - v)

w \gets w - \eta * \frac{m}{\sqrt{v + \varepsilon}}

上述两种表示乍一看似乎很不一样,不过把第二种进行一下因式分解和参数代换,很容易可以和第一种方式对应上。我亲自推理了下,已经确保没有问题了。不过还是推荐去读下Adam论文,对该方法的解读。

下面是原书中代码的实现:

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)

然后尝试用Adam参数优化方法解决SGD遇到的问题,效果如下:

《深度学习入门:基于Python的理论与实现》第6章-参数的更新_第8张图片

5 更新方法选择

《深度学习入门:基于Python的理论与实现》第6章-参数的更新_第9张图片

如图所示,根据使用的方法不同,参数更新的路径也不同。只看这个图似乎AdaGrad是最好的,但要注意,结果会根据实际问题的变化而变化。而且,超参数的设置不同,结果也会发生变化。目前这4种方法各有各的特点,都有各自擅长解决的问题饿不擅长解决的问题。

还有很多其它参数优化方法,这里暂未提到。

你可能感兴趣的:(python,神经网络)