2019斯坦福CS224n深度学习自然语言处理笔记(4)——反向传播与计算图


课程视频连接:《 深度学习与自然语言处理(4)
——————————————————————————

1. 矩阵梯度下降及一些小贴士

1.1 梯度下降

还是上节课的梯度下降,我们首先回顾一下:
s W = δ z w = δ w W x + b \frac{\partial s}{\partial \bold W}=\delta\frac{\partial \bold z}{\partial \bold w}=\delta\frac{\partial }{\partial \bold w}\bold W \bold x+\bold b

这样在计算 W i j W_{ij} 时一般时采用如下过程,从中就可以发现,其实需要参与运算的部分不多,其中 W i j W_{ij} 表示的是第i个输出的第j个输入上的权重:
z i W i j = W i j W i x + b i = W i j k = 1 d W i k x k = x j \frac{\partial z_i}{\partial W_{ij}}=\frac{\partial }{\partial W_{ij}}\bold W_i \bold x+b_i=\frac{\partial }{\partial W_{ij}}\sum^d_{k=1} W_{ik}x_k =x_j

1.2注意事项

  1. 仔细定义你的变量并追踪他们的维度变化。
  2. 链式法则要注意,注意链式法则的每一步求导。
  3. 对于softmax函数,首先考虑微分正确类型的函数,其次再考虑微分错误类型的函数。
  4. 如果你对矩阵计算比较困惑,你可以计算出每一个元素的偏导。
  5. 使用维度控制,当你计算每一步时,应当从理论上控制每一步中每一个运算的矩阵长宽对应。

1.3 窗口模型中的梯度下降

就像我们上节课所说的那样,在命名实体识别中,使用窗口模型来预测当前窗口中心词是否是实体。在梯度下降更新时,我们对于窗口中每一个词进行更新,如果这个词出现过两次,那么它会被更新2次。
x w i n d o w = [ x m u s e u m s x i n x P a r i s x a r e x a m a z i n g ] \bold x_{window}=[\bold x_{museums} \bold x_{in} \bold x_{Paris} \bold x_{are} \bold x_{amazing}]
更新时如下:
δ w i n d o w = [ x m u s e u m s x i n x P a r i s x a r e x a m a z i n g ] \delta_{window}=\begin{bmatrix} \nabla \bold x_{museums} \\ \nabla \bold x_{in} \\ \nabla \bold x_{Paris} \\ \nabla \bold x_{are} \\ \nabla \bold x_{amazing} \end{bmatrix}

使用窗口模型可以帮助我们命名实体识别,例如当我们出现 x i n x_{in} 出现在中心词前,那么中心词很有可能就是一个表示地点的实体。

1.4 使用词向量的陷阱

如果我们重新训练我们的词向量在我们的任务中,如果在训练集和测试集中的单词不能够也很好的覆盖,那么可能会出现词汇改变不同步现象。例如在预训练语料中存在TV,telly和Television,这三个词时同义词,其向量位置也很一致。
在这里插入图片描述
但是,TV和telly出现在训练集中,television只出现在测试集中,那么很有可能出现词向量更新不同步现象。
在这里插入图片描述
这种情况对于分类来讲时非常不好的。
所以我们在进行训练时,该怎么做?
Q1我是否应该使用预训练的词向量模型?
答案是在多数情况下当然是这样。因为预训练模型会拥有更多的先验知识,这意味着词向量不仅知道关于你的训练数据中的含义,还知道的更多。那如果数据集中包含极大的数据例如10亿词汇(一般会出现在机器翻译中,常用语言如中文和英文的双语语料很容易找到这么多数据),这时候可能词向量并不够使用,那么更好的建议是随机初始化后再进行训练。
Q2在使用预训练的词向量模型时,我是否应该微调它?
回答时如果你拥有一个小训练集,那么应该固定词向量。如果你同样拥有一个大的数据集,那么你可以进行微调词向量在你的任务中。

2. 计算图模型与反向传播

这里我们详细讲述计算图模型与反向传播过程,这两个是所有深度学习框架的基石。

回到刚开始的例子,我们仍然以此为例:
s = u T h s=\bold u^T \bold h
h = f ( z ) \bold h=f(\bold z)
z = W x + b \bold z=\bold W \bold x +\bold b
x ( i n p u t ) \bold x (input)
这四个可以以计算图模型展示出来,其中资源节点是输入,内部结点是操作。
在这里插入图片描述
在计算图模型中,数据是以流动的形式完成计算,按照数据流动的方向,我们分为前向传播和反向传播两个部分。

所谓的前向传播则是按照给定的输入,从左到右依次计算即可,就像我们正常的计算一样。

而反向传播,则是神经网络的独特之处,它自右向左依次求偏导后将误差传播回各个部分以更新参数(图中蓝色箭头所指)。
在这里插入图片描述
这些偏导我们在上节课中都已经讲过了,根据链式法则求导即可。

2.1 单个结点的反向传播

单个节点的梯度分为3个部分,上游梯度(upstream),局部梯度(local)和下游梯度(downstream),他们之间的关系是:
d o w n s t r e a m = u p s t e a m × l o c a l downstream=upsteam \times local
在这里插入图片描述
对于有多个输入而言,则是分别计算其偏导即可。
在这里插入图片描述

2.2 一个具体的例子

这里以一个具体的例子来详细描述一下如何进行反向传播。
原式为:
f ( x , y , z ) = ( x + y ) m a x ( y , z ) f(x,y,z)=(x+y)max(y,z)
根据链式法则,该式就可以转换为以下3个式子:
a = x + y a=x+y
b = m a x ( y , z ) b=max(y,z)
f = a b f=ab
其计算图如下所示,其中x=1,y=2,z=3:
在这里插入图片描述
其局部梯度分别为:
a x = 1 , a y = 1 \frac{\partial a}{\partial x}=1,\frac{\partial a}{\partial y}=1
b y = 1 ( y > z ) = 1 , b z = 1 ( z > y ) = 0 \frac{\partial b}{\partial y}=1(y>z)=1,\frac{\partial b}{\partial z}=1(z>y)=0
f a = b = 2 , f b = a = 3 \frac{\partial f}{\partial a}=b=2,\frac{\partial f}{\partial b}=a=3
根据我们刚才单个节点的公式,可以获得以下梯度:
d o w n s t r e a m = u p s t e a m × l o c a l downstream=upsteam \times local
在这里插入图片描述
在计算梯度时遵循一下3个法则:

  1. 如果遇到像y一样的时候,它的梯度有2个来源,甚至多个来源时,将这些梯度进行相加获得总梯度。
  2. 如果遇到像y和z这种二选一的时候,则只需传播已选中的节点梯度。
  3. 所有的相互连接的梯度都使用乘法进行传播。

还是像我们之前说的那样,运用链式法则,我们会尽可能的减少重复计算量。

2.3 自动求导

一般的,计算图模型中我们需要对于节点的运算进行求导。对于简单的表达式,都会自动的求导。而如果是比较复杂的式子,可能需要你手动指出该式的导数。下面我们使用具体的例子来说明如何求导。
一般的一个计算图模型的代码如下所示:

class ComputationalGraph(object):
	#...
	def forward(inputs):
		# 1. [pass inputs to input gates...]
		# 2. forward the computational graph:
		for gate in self.graph.nodes_topologically_sorted():
			gate.forward()
		return loss # the finnal gate in the graph outputs the loss
	def backward():
		for gate in reversed(self.graph.nodes_topologically_sorted())):
			gate.backward()
		return inputs_gradients

这里的代码有些部分可能比较难懂,不过没关系,我们会在接下来的Keras源码分析中进行一一解答。
其中一个gate的形式如下所示:

class MultiplyGate(object):
	def forward(x,y):
		z=x*y
		self.x=x #must keep these around!
		self.y=y
		return z
	def backward(dz):
		dx=self.y * dz # [dz/dz * dL/dz]
		dy=self.x * dz # [dz/dy * dL/dz]
		return [dx, dy]

最后友情提醒的是,尽管我们使用了数学手段实现了反向传播过程,但是我们仍然需要来检验我们是否做的正确。一般的,对于一个较小的 h 0.0001 h\approx0.0001 ,其导数计算可以约等于如下公式:
f ( x ) f ( x + h ) f ( x h ) 2 h f'(x)\approx \frac{f(x+h)-f(x-h)}{2h}
当然,这只是一种验证手段,在早期的时候,人们都是这样做的。但是现在人们都不会这样做了,因为人们早已经证明这个是正确的,并且编写了如Tensorflow和Pytorch这种库能够进行调用。

现在的深度学习框架都已经可以自动求梯度,如果考虑Keras,完全可以连一些基本的计算图模型都不需要掌握就可进行实验。那么我们为什么要了解这些关于梯度的细节呢?

  1. 可以更好的理解整个架构。
  2. 可以更好的调试和改进模型。
  3. 对于后来的教程如RNN的时候还会有所提及。

3.其他一些注意事项

3.1 正则项

在我们的模型汇中,有大量的参数,这时候,需要使用正则项来防止过拟合遐想的发生。所谓的过拟合现象指的是当我们的训练错误不断减少时,测试错误则是先下降后上升,如图所示。
在这里插入图片描述
解决这个方法通常使用正则项,一般的使用L1正则和L2正则,L1正则项取决于保留较少且更重要的参数,L2正则项取决于保留较小的参数,使得模型泛化性更好。

有很多参考说L1相较于L2更好,这里考虑的是最终解的形式,一般的,L1具有稀疏解,它可以找到更加重要的参数和特征。但是正是因为L1具有稀疏解,因此它并没有解析解,需要依靠稀疏算法进行求解,这就使得L1的算法并没有L2算法更加高效。其他更多关于L1和L2的细节请看《L1正则和L2正则比较》。
L1正则公式如下:
J ( θ ) = 1 N i = 1 N l o g ( e f y i c = 1 C e f c ) + λ k θ k J(\theta)=\frac{1}{N}\sum_{i=1}^N-log(\frac{e^{f_{y_i}}}{\sum_{c=1}^Ce^{f_c}})+\lambda\sum_k|\theta_k|
L2正则公式如下:
J ( θ ) = 1 N i = 1 N l o g ( e f y i c = 1 C e f c ) + λ k θ k 2 J(\theta)=\frac{1}{N}\sum_{i=1}^N-log(\frac{e^{f_{y_i}}}{\sum_{c=1}^Ce^{f_c}})+\lambda\sum_k\theta^2_k

3.2 向量矩阵化

使用矩阵化可以使用数学方法在GPU/CPU中分布式加速运算,一般的都可以达到10倍以上的加速效果。其向量矩阵化运算具体细节详见《向量化运算》。

3.3 激活函数

在上一讲中,我们就叙述了为什么要使用激活函数。在此处,我们列举一些常见的激活函数。
sigmoid激活函数
f ( z ) = 1 1 + e z f(z)=\frac{1}{1+e^{-z}}
tanh激活函数
t a n h ( z ) = 2 s i g m o i d ( 2 z ) 1 tanh(z)=2sigmoid(2z)-1
t a n h ( z ) = e z e z e x + e x tanh(z)=\frac{e^z-e^{-z}}{e^x+e^{-x}}
硬tanh激活函数
H a r d t a h n h ( z ) = { 1 i f z < 1 z i f 1 z 1 1 i f x > 1 Hardtahnh(z)=\begin{cases} -1 &if z<-1 \\ z & if -1 \leq z \leq 1\\ 1 &if x>1 \end{cases}
ReLU函数
r e c t ( z ) = m a x ( z , 0 ) rect(z)=max(z,0)
还要Leakly ReLU和Parametric ReLU等,尽管sigmoid和tanh等在某些情况下仍然被使用,但是一般的我们使用ReLu函数作为常用函数。具体的图像和详细的解析可以看《常用激活函数总结》。

3.4 优化器

在初始化参数时,一般使用随机初始化,但是不能够太大或者太小。

在选择优化器时,一般使用SGD即可,其他情况下可用的优化器包括(Adagrad, RMSprop, Adam,SparseAdam)等,现在常用的主要是Adam,详情可以看《优化器详解》。

优化器和学习率相关。一般的优化器如SGD不会对学习率进行改变,此时学习率是一个较小的值如lr=0.001。太大有可能会发散或根本不收敛。太小的话可能没办法尽早的收敛。

我们手动可以调节学习率,使得在刚开始时学习率大一些,因为此时我们离最终目标可能会很远,然后随着不断逼近目标,我们减小我们的学习率(这有点像打高尔夫球)。而其他的一些优化器如Adam等都会对于学习率进行动态调节。

发布了232 篇原创文章 · 获赞 547 · 访问量 51万+

猜你喜欢

转载自blog.csdn.net/qq_35082030/article/details/89287711