本章介绍深度学习算法-循环神经网络,主要介绍循环神经网络的常见变种,包括 递归神经网络、双向循环神经网络 以及 深度循环神经网络。
递归神经网络 (Recursive Neural Network) 由 Pollack 于 1990 年引入,而 Bottou 在 2011 年描述了这类网络代表循环网络的潜在用途 学习推理
神经网络的输入层单元个数是固定的,因此必须用循环或者递归的方式来处理长度可变的输入,递归神经网络是代表循环网络的另一种扩展,它被构建为深的 树状结构 而不是 RNN 的链状结构,因为是不同类型的计算图。
与循环网络相比,递归网络的明显优势是
不过同样因为递归神经网络的输入是树/图结构,而这种结构需要花费很多人工去标注,导致其在行业应用中并不流行
因为神经网络的输入层单元个数是固定的,因此必须用 循环
或者 递归
的方式来处理 长度可变 的输入。循环神经网络实现了前者,通过将长度不定的输入分割为等长度的小块,然后再依次的输入到网络中,从而实现了神经网络对变长输入的处理。例如处理一句话的输入时,可以将其看作是不同词依照时间顺序组成的序列,然后每次向循环神经网络输入一个词,如此循环直至整句话输入完毕,循环神经网络将产生对应的输出,依此就能处理任意长度的输入
然而,有时候把句子看做是词的序列是不够的,比如这句话 『两个外语学院的学生』,可以看出这句话有明显歧义
为了使模型区分出两个不同的意思,递归网络必须能够按照 图/树结构 去处理信息,而不是序列,即通过两个不同的语法解析树则区别不同的语义
上例显示了自然语言可组合的性质,即词可以组成句、句可以组成段落、段落可以组成篇章,而更高层的语义取决于底层的语义以及不同组合方式
递归神经网络是一种 表示学习,它可以将词、句、段、篇按照他们的语义映射到同一个向量空间中,也就是把可组合图/数结构的信息表示为一个个有意义的向量,并以此为基础去完成更高级的任务,比如递归神经网络在做情感分析时,可以比较好的处理否定句
蓝色表示正面评价,白色是中性评价,红色表示负面评价
每个节点是一个向量,这个向量表达了以它为根的子树的情感评价比如 intelligent humor 是正面评价,而 care about cleverness wit or any other kind of intelligent humor 是中性评价
模型能够正确的处理 doesn’t 的含义,将正面评价转变为负面评价
尽管递归神经网络具有更为强大的表示能力,但是在实际应用中并不太流行。其中一个主要原因是,递归神经网络的图/树结构需要花费很多人工去标注
循环神经网络处理句子,可以直接把句子作为输入。然而,递归神经网络处理句子,就必须把每个句子标注为语法解析树的形式,这无疑要花费非常大的精力。很多时候,相对于递归神经网络能够带来的性能提升,这个投入是不太划算的
递归神经网络的输入是两个子节点,输出是将这两个子节点编码后产生的父节点,父节点的维度和每个子节点是相同的
C_1,C_2C1,C2 分别是表示两个子节点的向量,PP 是表示父节点的向量
子节点和父节点组成一个 全连接神经网络,也就是子节点的每个神经元都和父节点的每个神经元两两相连
矩阵 WW 表示这些连接上的权重,维度为 d \times 2dd×2d,其中,dd 表示每个节点的维度, 则父节点的计算公式可以写成
\qquad\qquad p = \tanh(W{C_1 \brack C_2} + b)p=tanh(W[C2C1]+b)
其中 \tanhtanh 是激活函数,bb 是偏置项,也是一个维度为 dd 的向量
将产生的父节点的向量和其他子节点的向量再次作为网络的输入,再次产生它们的父节点。如此递归下去,直至整棵树处理完毕。最终将得到 根节点 (Root) 的向量,可以认为它是对整棵树的表示,这样就实现了把树映射为一个向量
递归神经网络的反向传播算法 Back Propagtion Through Structure (BPTS) 和循环神经网络 BPTT 算法类似,两者不同之处在于,前者需要将残差 \deltaδ 从根节点反向传播到各个子节点,而后者是将残差 \deltaδ 从当前时刻 t^ktk 反向传播到初始时刻 t^1t1
设 \mathbf{net}_pnetp 是父节点的加权输入,则有
\qquad\qquad\mathbf{net}_p = W {C_1 \brack C_2} + bnetp=W[C2C1]+b
定义 \delta_pδp 为误差函数 EE 相对于父节点 PP 的加权输入 \mathbf{net}_pnetp 的导数,则有
\qquad\qquad\delta_p \stackrel{def}{=} \dfrac {\partial E}{\partial \mathbf{net}_p}δp=def∂netp∂E
因篇幅限制,在此不列举各项偏函数矩阵转换的过程,其梯度简化式为
\qquad\qquad
则其权重更新公式为
\qquad\qquad
# %load recursivenn.py import torch import torch.nn as nn import torch.nn.functional as F class RecursiveNN(nn.Module): def __init__(self, vocabSize, embedSize=100, numClasses=5): super(RecursiveNN, self).__init__() self.embedding = nn.Embedding(int(vocabSize), embedSize) self.W = nn.Linear(2*embedSize, embedSize, bias=True) self.projection = nn.Linear(embedSize, numClasses, bias=True) self.activation = F.relu self.nodeProbList = [] self.labelList = [] def traverse(self, node): if node.isLeaf(): currentNode = self.activation(self.embedding(torch.LongTensor([node.getLeafWord()]))) else: currentNode = self.activation(self.W(torch.cat((self.traverse(node.left()),self.traverse(node.right())),1))) self.nodeProbList.append(self.projection(currentNode)) self.labelList.append(torch.LongTensor([node.label()])) return currentNode def forward(self, x): self.nodeProbList = [] self.labelList = [] self.traverse(x) self.labelList = torch.cat(self.labelList) return torch.cat(self.nodeProbList)
第 06 章介绍的 RNN 网络隐含了一个假设,即时刻 tt 的状态只能由过去的输入序列 \mathcal{S} = \lbrace \vec{x}^{(1)}, \vec{x}^{(2)}, \dots, \vec{x}^{(t-1)}\rbraceS={x(1),x(2),…,x(t−1)},以及当前的输入 \vec{x}^{(t)}x(t) 来决定。但在实际应用中,网络输出 \vec{o}^{(t)}o(t) 可能依赖于整个输入序列
如语音识别任务中,当前语音对应的单词不仅取决于前面的单词,也取决于后面的单词。因为词与词之间存在 语义依赖
双向循环神经网络 (Bidirectional RNN, BiRNN) 就是为了解决这种双向依赖问题,它在需要双向信息的应用中非常成功,如 手写识别、语音识别 等。BiRNN 是一个相对简单的 RNNs,由两个 RNNs 上下叠加在一起组成,输出由这两个 RNNs 的隐藏层状态决定
顾名思义,BiRNN 结合时间上从序列 起点移动 的 RNN 和另一个时间上从 序列末尾 移动的 RNN
这允许输出单元 o^{(t)}o(t) 能够计算同时依赖于过去和未来且对时刻 tt 的输入值最敏感的表示,而不必指定 tt 周围固定大小的窗口,因此在每个点 tt,输出单元 o^{(t)}o(t) 可以受益于 \overrightarrow h^{(t)}h(t) 中关于过去的相关概要以及输出 \overleftarrow h^{(t)}h(t) 中关于未来的相关概要
给定时间步 tt 的小批量输入 X_t \in \Re^{n\times d}Xt∈ℜn×d (样本数为 nn,输入个数为 dd) 和隐藏层激活函数为 \phiϕ, 在 BiRNN 的模型架构中,设共有 hh 个隐藏单元,该时间步 tt 正向隐藏状态为 \overrightarrow h^{(t)} \in \Re^{n\times h}h(t)∈ℜn×h, 反向隐藏状态为 \overleftarrow h^{(t)} \in \Re^{n\times h}h(t)∈ℜn×h, 则有
\qquad\qquad\overleftarrow h^{(t)} = \phi(x_tW_{x,h}^{(f)} + \overleftarrow h^{(t-1)}W_{h,h}^{(f)} + b_h^{(f)})h(t)=ϕ(xtWx,h(f)+h(t−1)Wh,h(f)+bh(f))
\qquad\qquad\overrightarrow h^{(t)} = \phi(x_tW_{x,h}^{(b)} + \overrightarrow h^{(t+1)}W_{h,h}^{(b)} + b_h^{(b)})h(t)=ϕ(xtWx,h(b)+h(t+1)Wh,h(b)+bh(b))
其中 \lbrace W^{(f)}_{x,h}, W^{(b)}_{x,h}\rbrace \in \Re^{d \times h}, \lbrace W^{(f)}_{h,h}, W^{(b)}_{h,h}\rbrace \in \Re^{h \times h}{Wx,h(f),Wx,h(b)}∈ℜd×h,{Wh,h(f),Wh,h(b)}∈ℜh×h 和偏差 b^{(f)}_h \in \Re^{1 \times h}, b^{(b)}_h \in \Re^{1 \times h}bh(f)∈ℜ1×h,bh(b)∈ℜ1×h 均为模型参数
连结两个方向的隐藏状态 \lbrace \overrightarrow h^{(t)}, \overleftarrow h^{(t)}\rbrace{h(t),h(t)} 来得到隐藏状态 h^{(t)} \in \Re^{n \times 2h}h(t)∈ℜn×2h,并将其输入到输出层, 假设共有 qq 个输出单元个数,计算输出 o^{(t)} \in \Re^{n \times q}o(t)∈ℜn×q
\qquad\qquad o^{(t)} = h^{(t)} W_{h,q} + b_qo(t)=h(t)Wh,q+bq
其中权重 W_{h,q} \in \Re^{2h\times q}Wh,q∈ℜ2h×q 和偏差 b_q \in \Re^{1 \times q}bq∈ℜ1×q 为输出层的模型参数
BiRNN 可以使用 RNN 类似的算法来做训练,因为两个方向的神经元没有任何相互作用。然而反向传播时,由于不能同时更新输入和输出层,因此需要额外的过程
BiRNN 使用的模型是 nn.LSTM
, 在第 07 章中提到,LSTM 是一类可以处理长期依赖问题的特殊的 RNN,由 Hochreiter 和 Schmidhuber 于 1977 年提出,目前已有多种改进,且广泛用于各种各样的问题中
bidirectional=True
是表示该网路是一个双向的网络batch_first=True
因为 nn.lstm() 接受的数据输入是 [序列长度,batch,输入维数],使用 batch_first
可以将输入变成 [batch,序列长度,输入维数]# %load birnn.py import torch import torch.nn as nn class BiRNN(nn.Module): def __init__(self, input_size, hidden_size, num_layers, num_classes): super(BiRNN, self).__init__() self.hidden_size = hidden_size self.num_layers = num_layers self.lstm = nn.LSTM(input_size, hidden_size, num_layers, batch_first=True, bidirectional=True) self.fc = nn.Linear(hidden_size*2, num_classes) # 2 for bidirection def forward(self, x): # Set initial states h0 = torch.zeros(self.num_layers*2, x.size(0), self.hidden_size) # 2 for bidirection c0 = torch.zeros(self.num_layers*2, x.size(0), self.hidden_size) # Forward propagate LSTM out, _ = self.lstm(x, (h0, c0)) # out: tensor of shape (batch_size, seq_length, hidden_size*2) # Decode the hidden state of the last time step out = self.fc(out[:, -1, :]) return out
RNN 的计算大致可以分解为三种变换
这三个变换都是浅层的,即由一个仿射变换加一个激活函数组成, 事实上可以对这三种变换中引入 深度
通过将 RNN 的隐状态分为多层来引入深度:隐状态有两层 \vec{h}^{(t)}h(t) 和 \vec{z}^{(t)}z(t), 隐状态层中层次越高,对输入提取的概念越抽象
可知每一层便是一个循环神经网络,而下一层的循环神经网络的输出作为上一层的输入,依次进行迭代而成
深度循环神经网络的应用范围较少,对于单层的模型已经可以实现对序列模型的编码,而深层次模型会造成一定的 过拟合和梯度 问题,因此很少被应用
使用一个独立的 MLP 所生成的额外深度将导致从时间步 tt 到时间步 t+1t+1 的最短路径变得更长,这可能导致优化困难而破坏学习效果
类似 ResNet 的思想,在 隐状态-隐状态 的路径中引入跳跃连接,从而缓解最短路径变得更长的问题
开始实验