零基础入门深度学习(7) - 递归神经网络

往期回顾

在前面的文章中,咱们介绍了循环神经网络,它能够用来处理包含序列结构的信息。然而,除此以外,信息每每还存在着诸如树结构、图结构等更复杂的结构。对于这种复杂的结构,循环神经网络就无能为力了。本文介绍一种更为强大、复杂的神经网络:递归神经网络 (Recursive Neural Network, RNN),以及它的训练算法BPTS (Back Propagation Through Structure)。顾名思义,递归神经网络(巧合的是,它的缩写和循环神经网络同样,也是RNN)能够处理诸如树、图这样的递归结构。在文章的最后,咱们将实现一个递归神经网络,并介绍它的几个应用场景。node

 

递归神经网络是啥

由于神经网络的输入层单元个数是固定的,所以必须用循环或者递归的方式来处理长度可变的输入。循环神经网络实现了前者,经过将长度不定的输入分割为等长度的小块,而后再依次的输入到网络中,从而实现了神经网络对变长输入的处理。一个典型的例子是,当咱们处理一句话的时候,咱们能够把一句话看做是词组成的序列,而后,每次向循环神经网络输入一个词,如此循环直至整句话输入完毕,循环神经网络将产生对应的输出。如此,咱们就能处理任意长度的句子了。入下图所示:python

然而,有时候把句子看作是词的序列是不够的,好比下面这句话『两个外语学院的学生』:git

上图显示了这句话的两个不一样的语法解析树。能够看出来这句话有歧义,不一样的语法解析树则对应了不一样的意思。一个是『两个外语学院的/学生』,也就是学生可能有许多,但他们来自于两所外语学校;另外一个是『两个/外语学院的学生』,也就是只有两个学生,他们是外语学院的。为了可以让模型区分出两个不一样的意思,咱们的模型必须可以按照树结构去处理信息,而不是序列,这就是递归神经网络的做用。当面对按照树/图结构处理信息更有效的任务时,递归神经网络一般都会得到不错的结果。github

递归神经网络能够把一个树/图结构信息编码为一个向量,也就是把信息映射到一个语义向量空间中。这个语义向量空间知足某类性质,好比语义类似的向量距离更近。也就是说,若是两句话(尽管内容不一样)它的意思是类似的,那么把它们分别编码后的两个向量的距离也相近;反之,若是两句话的意思大相径庭,那么编码后向量的距离则很远。以下图所示:算法

从上图咱们能够看到,递归神经网络将全部的词、句都映射到一个2维向量空间中。句子『the country of my birth』和句子『the place where I was born』的意思是很是接近的,因此表示它们的两个向量在向量空间中的距离很近。另外两个词『Germany』和『France』由于表示的都是地点,它们的向量与上面两句话的向量的距离,就比另外两个表示时间的词『Monday』和『Tuesday』的向量的距离近得多。这样,经过向量的距离,就获得了一种语义的表示。数组

上图还显示了天然语言可组合的性质:词能够组成句、句能够组成段落、段落能够组成篇章,而更高层的语义取决于底层的语义以及它们的组合方式。递归神经网络是一种表示学习,它能够将词、句、段、篇按照他们的语义映射到同一个向量空间中,也就是把可组合(树/图结构)的信息表示为一个个有意义的向量。好比上面这个例子,递归神经网络把句子"the country of my birth"表示为二维向量[1,5]。有了这个『编码器』以后,咱们就能够以这些有意义的向量为基础去完成更高级的任务(好比情感分析等)。以下图所示,递归神经网络在作情感分析时,能够比较好的处理否认句,这是赛过其余一些模型的:网络

在上图中,蓝色表示正面评价,红色表示负面评价。每一个节点是一个向量,这个向量表达了以它为根的子树的情感评价。好比"intelligent humor"是正面评价,而"care about cleverness wit or any other kind of intelligent humor"是中性评价。咱们能够看到,模型可以正确的处理doesn't的含义,将正面评价转变为负面评价。dom

尽管递归神经网络具备更为强大的表示能力,可是在实际应用中并不太流行。其中一个主要缘由是,递归神经网络的输入是树/图结构,而这种结构须要花费不少人工去标注。想象一下,若是咱们用循环神经网络处理句子,那么咱们能够直接把句子做为输入。然而,若是咱们用递归神经网络处理句子,咱们就必须把每一个句子标注为语法解析树的形式,这无疑要花费很是大的精力。不少时候,相对于递归神经网络可以带来的性能提高,这个投入是不太划算的。python2.7

咱们已经基本了解了递归神经网络是作什么用的,接下来,咱们将探讨它的算法细节。ide

 

递归神经网络的前向计算

接下来,咱们详细介绍一下递归神经网络是如何处理树/图结构的信息的。在这里,咱们以处理树型信息为例进行介绍。

递归神经网络的输入是两个子节点(也能够是多个),输出就是将这两个子节点编码后产生的父节点,父节点的维度和每一个子节点是相同的。以下图所示:

分别是表示两个子节点的向量,是表示父节点的向量。子节点和父节点组成一个全链接神经网络,也就是子节点的每一个神经元都和父节点的每一个神经元两两相连。咱们用矩阵表示这些链接上的权重,它的维度将是,其中,表示每一个节点的维度。父节点的计算公式能够写成:

 

 

 

在上式中,tanh是激活函数(固然也能够用其它的激活函数),是偏置项,它也是一个维度为的向量。若是读过前面的文章,相信你们已经很是熟悉这些计算了,在此不作过多的解释了。

而后,咱们把产生的父节点的向量和其余子节点的向量再次做为网络的输入,再次产生它们的父节点。如此递归下去,直至整棵树处理完毕。最终,咱们将获得根节点的向量,咱们能够认为它是对整棵树的表示,这样咱们就实现了把树映射为一个向量。在下图中,咱们使用递归神经网络处理一棵树,最终获得的向量,就是对整棵树的表示:

举个例子,咱们使用递归神将网络将『两个外语学校的学生』映射为一个向量,以下图所示:

最后获得的向量就是对整个句子『两个外语学校的学生』的表示。因为整个结构是递归的,不只仅是根节点,事实上每一个节点都是以其为根的子树的表示。好比,在左边的这棵树中,向量是短语『外语学院的学生』的表示,而向量是短语『外语学院的』的表示。

式1就是递归神经网络的前向计算算法。它和全链接神经网络的计算没有什么区别,只是在输入的过程当中须要根据输入的树结构依次输入每一个子节点。

须要特别注意的是,递归神经网络的权重和偏置项在全部的节点都是共享的。

 

递归神经网络的训练

递归神经网络的训练算法和循环神经网络相似,二者不一样之处在于,前者须要将残差从根节点反向传播到各个子节点,然后者是将残差从当前时刻反向传播到初始时刻

下面,咱们介绍适用于递归神经网络的训练算法,也就是BPTS算法。

 

偏差项的传递

首先,咱们先推导将偏差从父节点传递到子节点的公式,以下图:

定义为偏差函数E相对于父节点的加权输入的导数,即:

 

 

 

 

是父节点的加权输入,则

 

在上述式子里,都是向量,而是矩阵。为了看清楚它们的关系,咱们将其展开:

 

 

 

 

在上面的公式中,表示父节点p的第i个份量;表示子节点的第i个份量;表示子节点的第i个份量;表示子节点的第k个份量到父节点p的第i个份量的的权重。根据上面展开后的矩阵乘法形式,咱们不难看出,对于子节点来讲,它会影响父节点全部的份量。所以,咱们求偏差函数E对的导数时,必须用到全导数公式,也就是:

 

 

 

 

有了上式,咱们就能够把它表示为矩阵形式,从而获得一个向量化表达:

 

 

 

 

其中,矩阵是从矩阵W中提取部分元素组成的矩阵。其单元为:

 

 

 

 

上式看上去可能会让人晕菜,从下图,咱们能够直观的看到究竟是啥。首先咱们把W矩阵拆分为两个矩阵,以下图所示:

显然,子矩阵分别对应子节点的到父节点权重。则矩阵为:

 

 

 

 

也就是说,将偏差项反向传递到相应子节点的矩阵就是其对应权重矩阵的转置。

如今,咱们设是子节点的加权输入,是子节点c的激活函数,则:

 

 

 

 

这样,咱们获得:

 

 

 

 

若是咱们将不一样子节点对应的偏差项链接成一个向量。那么,上式能够写成:

 

 

 

式2就是将偏差项从父节点传递到其子节点的公式。注意,上式中的也是将两个子节点的加权输入连在一块儿的向量。

有了传递一层的公式,咱们就不难写出逐层传递的公式。

上图是在树型结构中反向传递偏差项的全景图,反复应用式2,在已知的状况下,咱们不难算出为:

 

 

 

 

在上面的公式中,表示取向量属于节点p的部分。

 

权重梯度的计算

根据加权输入的计算公式:

 

 

 

 

其中,表示第l层的父节点的加权输入,表示第l层的子节点。是权重矩阵,是偏置项。将其展开可得:

 

 

 

 

那么,咱们能够求得偏差函数在第l层对权重的梯度为:

 

 

 

 

上式是针对一个权重项的公式,如今须要把它扩展为对全部的权重项的公式。咱们能够把上式写成矩阵的形式(在下面的公式中,m=2n):

 

 

 

式3就是第l层权重项的梯度计算公式。咱们知道,因为权重是在全部层共享的,因此和循环神经网络同样,递归神经网络的最终的权重梯度是各个层权重梯度之和。即:

 

 

 

由于循环神经网络的证实过程已经在零基础入门深度学习(4) - 卷积神经网络一文中给出,所以,递归神经网络『为何最终梯度是各层梯度之和』的证实就留给读者自行完成啦。

接下来,咱们求偏置项的梯度计算公式。先计算偏差函数对第l层偏置项的梯度:

 

 

 

 

把上式扩展为矩阵的形式:

 

 

 

式5是第l层偏置项的梯度,那么最终的偏置项梯度是各个层偏置项梯度之和,即:

 

 

 

 

权重更新

若是使用梯度降低优化算法,那么权重更新公式为:

 

 

 

 

其中,是学习速率常数。把式4带入到上式,便可完成权重的更新。同理,偏置项的更新公式为:

 

 

 

 

把式6带入到上式,便可完成偏置项的更新。

这就是递归神经网络的训练算法BPTS。因为咱们有了前面几篇文章的基础,相信读者们理解BPTS算法也会比较容易。

 

递归神经网络的实现

完整代码请参考GitHub: https://github.com/hanbt/learn_dl/blob/master/recursive.py (python2.7)

如今,咱们实现一个处理树型结构的递归神经网络。

在文件的开头,加入以下代码:

 
  1. #!/usr/bin/env python
  2. # -*- coding: UTF-8 -*-
  3. import numpy as np
  4. from cnn import IdentityActivator

上述四行代码很是简单,没有什么须要解释的。IdentityActivator激活函数是在咱们介绍卷积神经网络时写的,如今引用一下它。

咱们首先定义一个树节点结构,这样,咱们就能够用它保存卷积神经网络生成的整棵树:

 
  1. class TreeNode(object):
  2. def __init__(self, data, children=[], children_data=[]):
  3. self.parent = None
  4. self.children = children
  5. self.children_data = children_data
  6. self.data = data
  7. for child in children:
  8. child.parent = self

接下来,咱们把递归神经网络的实现代码都放在RecursiveLayer类中,下面是这个类的构造函数:

 
  1. # 递归神经网络实现
  2. class RecursiveLayer(object):
  3. def __init__(self, node_width, child_count,
  4. activator, learning_rate):
  5. '''
  6. 递归神经网络构造函数
  7. node_width: 表示每一个节点的向量的维度
  8. child_count: 每一个父节点有几个子节点
  9. activator: 激活函数对象
  10. learning_rate: 梯度降低算法学习率
  11. '''
  12. self.node_width = node_width
  13. self.child_count = child_count
  14. self.activator = activator
  15. self.learning_rate = learning_rate
  16. # 权重数组W
  17. self.W = np.random.uniform(-1e-4, 1e-4,
  18. (node_width, node_width * child_count))
  19. # 偏置项b
  20. self.b = np.zeros((node_width, 1))
  21. # 递归神经网络生成的树的根节点
  22. self.root = None

下面是前向计算的实现:

 
  1. def forward(self, *children):
  2. '''
  3. 前向计算
  4. '''
  5. children_data = self.concatenate(children)
  6. parent_data = self.activator.forward(
  7. np.dot(self.W, children_data) + self.b
  8. )
  9. self.root = TreeNode(parent_data, children
  10. , children_data)

forward函数接收一系列的树节点对象做为输入,而后,递归神经网络将这些树节点做为子节点,并计算它们的父节点。最后,将计算的父节点保存在self.root变量中。

上面用到的concatenate函数,是将各个子节点中的数据拼接成一个长向量,其代码以下:

 
  1. def concatenate(self, tree_nodes):
  2. '''
  3. 将各个树节点中的数据拼接成一个长向量
  4. '''
  5. concat = np.zeros((0,1))
  6. for node in tree_nodes:
  7. concat = np.concatenate((concat, node.data))
  8. return concat

下面是反向传播算法BPTS的实现:

 
  1. def backward(self, parent_delta):
  2. '''
  3. BPTS反向传播算法
  4. '''
  5. self.calc_delta(parent_delta, self.root)
  6. self.W_grad, self.b_grad = self.calc_gradient(self.root)
  7. def calc_delta(self, parent_delta, parent):
  8. '''
  9. 计算每一个节点的delta
  10. '''
  11. parent.delta = parent_delta
  12. if parent.children:
  13. # 根据式2计算每一个子节点的delta
  14. children_delta = np.dot(self.W.T, parent_delta) * (
  15. self.activator.backward(parent.children_data)
  16. )
  17. # slices = [(子节点编号,子节点delta起始位置,子节点delta结束位置)]
  18. slices = [(i, i * self.node_width,
  19. (i + 1) * self.node_width)
  20. for i in range(self.child_count)]
  21. # 针对每一个子节点,递归调用calc_delta函数
  22. for s in slices:
  23. self.calc_delta(children_delta[s[1]:s[2]],
  24. parent.children[s[0]])
  25. def calc_gradient(self, parent):
  26. '''
  27. 计算每一个节点权重的梯度,并将它们求和,获得最终的梯度
  28. '''
  29. W_grad = np.zeros((self.node_width,
  30. self.node_width * self.child_count))
  31. b_grad = np.zeros((self.node_width, 1))
  32. if not parent.children:
  33. return W_grad, b_grad
  34. parent.W_grad = np.dot(parent.delta, parent.children_data.T)
  35. parent.b_grad = parent.delta
  36. W_grad += parent.W_grad
  37. b_grad += parent.b_grad
  38. for child in parent.children:
  39. W, b = self.calc_gradient(child)
  40. W_grad += W
  41. b_grad += b
  42. return W_grad, b_grad

在上述算法中,calc_delta函数和calc_gradient函数分别计算各个节点的偏差项以及最终的梯度。它们都采用递归算法,先序遍历整个树,并逐一完成每一个节点的计算。

下面是梯度降低算法的实现(没有weight decay),这个很是简单:

 
  1. def update(self):
  2. '''
  3. 使用SGD算法更新权重
  4. '''
  5. self.W -= self.learning_rate * self.W_grad
  6. self.b -= self.learning_rate * self.b_grad

以上就是递归神经网络的实现,总共100行左右,和上一篇文章的LSTM相比简单多了。

最后,咱们用梯度检查来验证程序的正确性:

 
  1. def gradient_check():
  2. '''
  3. 梯度检查
  4. '''
  5. # 设计一个偏差函数,取全部节点输出项之和
  6. error_function = lambda o: o.sum()
  7. rnn = RecursiveLayer(2, 2, IdentityActivator(), 1e-3)
  8. # 计算forward值
  9. x, d = data_set()
  10. rnn.forward(x[0], x[1])
  11. rnn.forward(rnn.root, x[2])
  12. # 求取sensitivity map
  13. sensitivity_array = np.ones((rnn.node_width, 1),
  14. dtype=np.float64)
  15. # 计算梯度
  16. rnn.backward(sensitivity_array)
  17. # 检查梯度
  18. epsilon = 10e-4
  19. for i in range(rnn.W.shape[0]):
  20. for j in range(rnn.W.shape[1]):
  21. rnn.W[i,j] += epsilon
  22. rnn.reset_state()
  23. rnn.forward(x[0], x[1])
  24. rnn.forward(rnn.root, x[2])
  25. err1 = error_function(rnn.root.data)
  26. rnn.W[i,j] -= 2*epsilon
  27. rnn.reset_state()
  28. rnn.forward(x[0], x[1])
  29. rnn.forward(rnn.root, x[2])
  30. err2 = error_function(rnn.root.data)
  31. expect_grad = (err1 - err2) / (2 * epsilon)
  32. rnn.W[i,j] += epsilon
  33. print 'weights(%d,%d): expected - actural %.4e - %.4e' % (
  34. i, j, expect_grad, rnn.W_grad[i,j])
  35. return rnn

下面是梯度检查的结果,彻底正确,OH YEAH!

 

递归神经网络的应用

 

天然语言和天然场景解析

在天然语言处理任务中,若是咱们可以实现一个解析器,将天然语言解析为语法树,那么毫无疑问,这将大大提高咱们对天然语言的处理能力。解析器以下所示:

能够看出,递归神经网络可以完成句子的语法分析,并产生一个语法解析树。

除了天然语言以外,天然场景也具备可组合的性质。所以,咱们能够用相似的模型完成天然场景的解析,以下图所示:

两种不一样的场景,能够用相同的递归神经网络模型来实现。咱们以第一个场景,天然语言解析为例。

咱们但愿将一句话逐字输入到神经网络中,而后,神经网络返回一个解析好的树。为了作到这一点,咱们须要给神经网络再加上一层,负责打分。分数越高,说明两个子节点结合更加紧密,分数越低,说明两个子节点结合更松散。以下图所示:

一旦这个打分函数训练好了(也就是矩阵U的各项值变为合适的值),咱们就能够利用贪心算法来实现句子的解析。第一步,咱们先将词按照顺序两两输入神经网络,获得第一组打分:

咱们发现,如今分数最高的是第一组,The cat,说明它们的结合是最紧密的。这样,咱们能够先将它们组合为一个节点。而后,再次两两计算相邻子节点的打分:

如今,分数最高的是最后一组,the mat。因而,咱们将它们组合为一个节点,再两两计算相邻节点的打分:

这时,咱们发现最高的分数是on the mat,把它们组合为一个节点,继续两两计算相邻节点的打分......最终,咱们就可以获得整个解析树:

如今,咱们困惑这样牛逼的打分函数score是怎样训练出来的呢?咱们须要定义一个目标函数。这里,咱们使用Max-Margin目标函数。它的定义以下:

 

 

 

 

在上式中,分别表示第i个训练样本的输入和标签,注意这里的标签是一棵解析树。就是打分函数s对第i个训练样本的打分。由于训练样本的标签确定是正确的,咱们但愿s对它的打分越高越好,也就是越大越好。是全部可能的解析树的集合,而则是对某个可能的解析树的打分。是对错误的惩罚。也就是说,若是某个解析树和标签是同样的,那么为0,若是网络的输出错的越离谱,那么惩罚项的值就越高。表示全部树里面最高得分。在这里,惩罚项至关于Margin,也就是咱们虽然但愿打分函数s对正确的树打分比对错误的树打分高,但也不要高过Margin的值。咱们优化,使目标函数取最小值,即:

 

 

 

 

下面是惩罚函数的定义:

 

 

 

 

上式中,N(y)是树y节点的集合;subTree(d)是以d为节点的子树。上式的含义是,若是以d为节点的子树没有出如今标签中,那么函数值+1。最终,惩罚函数的值,是树y中没有出如今树中的子树的个数,再乘上一个系数k。其实也就是关于两棵树差别的一个度量。

是对一个样本最终的打分,它是对树y每一个节点打分的总和。

 

 

 

 

具体细节,读者能够查阅『参考资料3』的论文。

 

小结

咱们在系列文章中已经介绍的全链接神经网络、卷积神经网络、循环神经网络和递归神经网络,在训练时都使用了监督学习(Supervised Learning)做为训练方法。在监督学习中,每一个训练样本既包括输入特征,也包括标记,即样本。然而,不少状况下,咱们没法得到形如的样本,这时,咱们就不能采用监督学习的方法。在接下来的几篇文章中,咱们重点介绍另一种学习方法:加强学习(Reinforcement Learning)。在了解加强学习的主要算法以后,咱们还将介绍著名的围棋软件AlphaGo,它是一个把监督学习和加强学习进行完美结合的案例。

 

参考资料

  1. CS224d: Deep Learning for Natural Language Processing
  2. Learning Task-Dependent Distributed Representations by Back Propagation Through Structure
  3. Parsing Natural Scenes and Natural Language with Recursive Neural Networks
  4. 转载自:https://zybuluo.com/hanbingtao/note/626300
相关文章
相关标签/搜索