终于来到最后一期,也是目前NLP最常用的架构RNN,及其各种变种。具体来看,本次总结的主要内容:
- RNN的结构。循环神经网络的提出背景、优缺点。着重学习RNN的反向传播、RNN出现的问题(梯度问题、长期依赖问题)、BPTT算法。
- 双向RNN
- LSTM、GRU的结构、提出背景、优缺点。
- 针对梯度消失(LSTM等其他门控RNN)、梯度爆炸(梯度截断)的解决方案。
- Text-RNN的原理。
- 利用Text-RNN模型来进行文本分类
RNN
循环神经网络(Recurrent Neural Network,RNN)是用来建模序列化数据的一
种主流深度学习模型 [1]。传统的前馈神经网络一般的输入都是一个定长的向量,无法处理变长的序列信息,即使通过一些方法把序列处理成定长的向量,模型也很难捕捉序列中的长距离依赖关系。RNN则通过将神经元串行起来处理序列化的数据。由于每个神经元能用它的内部变量保存之前输入的序列信息,因此整个序列被浓缩成抽象的表示,并可以据此进行分类或生成新的序列。近年来,得益于计算能力的大幅提升和模型的改进,RNN在很多领域取得了突破性的进展——机器翻译、序列标注、图像描述、推荐系统、智能聊天机器人、自动作词作曲等。
下图展示一个典型的RNN的结构:
由图可见,一个长度为T的序列用循环神经网络建模,展开之后可以看作是一
个T层的前馈神经网络。其中,第t层的隐含状态
h
t
h_t
ht编码了序列中前t个输入的信息,可以通过当前的输入
x
t
x_t
xt和上一层神经网络的状态
h
t
−
1
h_{t−1}
ht−1计算得到;最后一层的状态
h
T
h_T
hT编码了整个序列的信息。以此为基础的结构可以应用于多种具体任务。例如,在
h
T
h_T
hT后面直接接一个Softmax层,输出文本所属类别的预测概率y,就可以实现文本分类。
h
t
h_t
ht和y的计算公式为:
n
e
t
t
=
U
x
t
+
W
h
t
−
1
net_t=Ux_t+Wh_{t−1}
nett=Uxt+Wht−1,
h
t
=
f
(
n
e
t
t
)
h_t=f(net_t)
ht=f(nett),
y
=
g
(
V
h
T
)
y=g(Vh_T)
y=g(VhT)
其中f和g为激活函数,U为输入层到隐含层的权重矩阵,W为隐含层从上一时刻到
下一时刻状态转移的权重矩阵。在文本分类任务中,f可以选取Tanh函数或者ReLU函数,g可以采用Softmax函数。
通过最小化损失误差(即输出的y与真实类别之间的距离),我们可以不断训
练网络,使得得到的循环神经网络可以准确地预测文本所属的类别,达到分类目
的。相比于卷积神经网络等前馈神经网络,循环神经网络由于具备对序列顺序信
息的刻画能力,往往能得到更准确的结果。
RNN 的变种有很多,从架构上来看,主要可以总结为一下4种:
循环神经网络的参数可以通过梯度下降方法来进行学习。循环神经网络中存在一个递归调用的函数f(·),因此其计算参数梯度的方式和前馈神经网络不太相同。在循环神经网络中主要有两种计算梯度的方式:随时间反向传播(BPTT)和实时循环学习(RTRL)算法。
随时间反向传播(Backpropagation Through Time,BPTT)算法的主要
思想是通过类似前馈神经网络的错误反向传播算法[2,3] 来进行计算
梯度。BPTT算法将循环神经网络看作是一个展开的多层前馈网络,其中“每一
层”对应循环网络中的“每个时刻”。这样,循环神经网络就可以按
按照前馈网络中的反向传播算法进行计算参数梯度。在“展开”的前馈网络中,
所有层的参数是共享的,因此参数的真实梯度是将所有“展开层”的参数梯度
之和。
因为参数U 和隐藏层在每个时刻k(1 ≤ k ≤ t) 的净输入
z
k
=
U
h
k
−
1
+
W
x
k
+
b
z_k = Uh_{k−1} + Wx_k + b
zk=Uhk−1+Wxk+b有关,因此第t 时刻损失的损失函数
L
t
L_t
Lt 关于参数
U
i
j
U_{ij}
Uij 的梯度为:
其中
∂
+
z
k
∂
U
i
j
\frac{\partial^+z_k}{\partial U_{ij}}
∂Uij∂+zk表示“直接”偏导数,即公式
z
k
=
U
h
k
−
1
+
W
x
k
+
b
z_k = Uh_{k−1} +Wx_k + b
zk=Uhk−1+Wxk+b中保持
h
k
−
1
h_{k−1}
hk−1 不变,对
U
i
j
U_{ij}
Uij 进行求偏导数,得到
其中
[
h
k
−
1
]
j
[h_{k−1}]j
[hk−1]j为第k −1 时刻隐状态的第j 维;
I
I
i
(
x
)
II_i(x)
IIi(x)除了第i 行值为x外,其余都为0 的向量。定义
δ
t
,
k
=
∂
L
t
z
k
\delta_{t,k} = \frac{\partial L_t}{z_k}
δt,k=zk∂Lt为第t 时刻的损失对第k 时刻隐藏神经层的净输入
z
k
z_k
zk 的导数,则:
公式合并,并写出矩阵形式得到:
∂
L
t
∂
U
=
∑
k
=
1
t
δ
t
,
k
h
k
−
1
T
\frac{\partial L_t}{\partial U}=\sum_{k=1}^t \delta_{t,k}h_{k-1}^T
∂U∂Lt=k=1∑tδt,khk−1T
BPTT由图表示可得:
与反向传播的BPTT算法不同的是,实时循环学习(Real-Time Recurrent
Learning,RTRL)是通过前向传播的方式来计算梯度,详细可参考文献[3]
双向RNN和LSTM
Bidirectional RNN(双向RNN)假设当前t的输出不仅仅和之前的序列有关,并且 还与之后的序列有关,例如:预测一个语句中缺失的词语那么需要根据上下文进 行预测;Bidirectional RNN是一个相对简单的RNNs,由两个RNNs上下叠加在 一起组成。输出由这两个RNNs的隐藏层的状态决定 [4]。 具体结构如图所示:
Long Short Term 网络—— 一般就叫做 LSTM ——是一种 RNN 特殊的类型,可以学习长期依赖信息。LSTM 由Hochreiter & Schmidhuber (1997)提出,并在近期被Alex Graves进行了改良和推广。在很多问题,LSTM 都取得相当巨大的成功,并得到了广泛的使用。
LSTM 通过刻意的设计来避免长期依赖问题。记住长期的信息在实践中是 LSTM 的默认行为,而非需要付出很大代价才能获得的能力!所有 RNN 都具有一种重复神经网络模块的链式的形式。在标准的 RNN 中,这个重复的模块只有一个非常简单的结构,例如一个 tanh 层。
LSTM 的关键就是细胞状态,水平线在图上方贯穿运行。
细胞状态类似于传送带。直接在整个链上运行,只有一些少量的线性交互。信息在上面流传保持不变会很容易。LSTM 有通过精心设计的称作为“门”的结构来去除或者增加信息到细胞状态的能力。门是一种让信息选择式通过的方法。他们包含一个 sigmoid 神经网络层和一个 pointwise 乘法操作。
门控循环单元(Gated Recurrent Unit,GRU)网络是一种比LSTM网络更加简单的循环神经网络。
在LSTM网络中,输入门和遗忘门是互补关系,用两个门比较冗余。GRU将输
入门与和遗忘门合并成一个门:更新门。同时,GRU也不引入额外的记忆单元,
直接在当前状态
h
t
h_t
ht 和历史状态
h
t
−
1
h_{t−1}
ht−1 之间引入线性依赖关系。具体结构如下:
梯度消失与梯度爆炸
循环神经网络在学习过程中的主要问题是长期依赖问题,
在BPTT算法中,将
δ
t
,
k
\delta_{t,k}
δt,k 展开得到:
δ
t
,
k
=
∏
i
=
k
t
−
1
(
d
i
a
g
(
f
′
(
z
i
)
)
U
T
)
δ
t
,
t
\delta_{t,k} = \prod_{i= k}^{t-1} (diag(f'(z_i))U^T)\delta_{t,t}
δt,k=i=k∏t−1(diag(f′(zi))UT)δt,t
如果定义
γ
≅
∣
∣
d
i
a
g
(
f
′
(
z
i
)
)
U
T
∣
∣
\gamma \cong || diag(f'(z_i))U^T||
γ≅∣∣diag(f′(zi))UT∣∣,则:
δ
t
,
k
=
γ
t
−
k
δ
t
,
t
\delta_{t,k} = \gamma^{t-k}\delta_{t,t}
δt,k=γt−kδt,t
若
γ
>
1
\gamma > 1
γ>1,当
t
−
k
→
∞
t−k → \infty
t−k→∞时,
γ
t
−
k
→
∞
\gamma^{t−k} → \infty
γt−k→∞,会造成系统不稳定,称为梯度爆炸问
题(Gradient Exploding Problem);相反,若
γ
<
1
\gamma < 1
γ<1,当
t
−
k
→
∞
t−k → \infty
t−k→∞时,
γ
t
−
k
→
0
\gamma^{t−k} → 0
γt−k→0,会出现和深度前馈神经网络类似的梯度消失问题(gradient vanishing problem)。
梯度爆炸的问题可以通过梯度裁剪来缓解,即当梯度的范式大于某个给定值
时,对梯度进行等比收缩。而梯度消失问题相对比较棘手,需要对模型本身进行
改进。深度残差网络是对前馈神经网络的改进,通过残差学习的方式缓解了梯度
消失的现象,从而使得我们能够学习到更深层的网络表示;而对于循环神经网络
来说,长短时记忆模型 LSTM及其变种门控循环单元(Gated recurrent unit,GRU)等模型通过加入门控机制,很大程度上弥补了梯度消失所带来的损失。
TextRNN
TextRNN的一般流程是1. embeddding layer, 2.Bi-LSTM layer, 3.concat output, 4.FC layer, 5.softmax:
其基本结构如图所示:
参考代码如下:
# 构建模型
class BiLSTM(object):
"""
Bi-LSTM 用于文本分类
"""
def __init__(self, config, wordEmbedding):
# 定义模型的输入
self.inputX = tf.placeholder(tf.int32, [None, config.sequenceLength], name="inputX")
self.inputY = tf.placeholder(tf.float32, [None, 1], name="inputY")
self.dropoutKeepProb = tf.placeholder(tf.float32, name="dropoutKeepProb")
# 定义l2损失
l2Loss = tf.constant(0.0)
# 词嵌入层
with tf.name_scope("embedding"):
# 利用预训练的词向量初始化词嵌入矩阵
self.W = tf.Variable(tf.cast(wordEmbedding, dtype=tf.float32, name="word2vec") ,name="W")
# 利用词嵌入矩阵将输入的数据中的词转换成词向量,维度[batch_size, sequence_length, embedding_size]
self.embeddedWords = tf.nn.embedding_lookup(self.W, self.inputX)
# 定义两层双向LSTM的模型结构
with tf.name_scope("Bi-LSTM"):
for idx, hiddenSize in enumerate(config.model.hiddenSizes):
with tf.name_scope("Bi-LSTM" + str(idx)):
# 定义前向LSTM结构
lstmFwCell = tf.nn.rnn_cell.DropoutWrapper(tf.nn.rnn_cell.LSTMCell(num_units=hiddenSize, state_is_tuple=True),
output_keep_prob=self.dropoutKeepProb)
# 定义反向LSTM结构
lstmBwCell = tf.nn.rnn_cell.DropoutWrapper(tf.nn.rnn_cell.LSTMCell(num_units=hiddenSize, state_is_tuple=True),
output_keep_prob=self.dropoutKeepProb)
# 采用动态rnn,可以动态的输入序列的长度,若没有输入,则取序列的全长
# outputs是一个元祖(output_fw, output_bw),其中两个元素的维度都是[batch_size, max_time, hidden_size],fw和bw的hidden_size一样
# self.current_state 是最终的状态,二元组(state_fw, state_bw),state_fw=[batch_size, s],s是一个元祖(h, c)
outputs, self.current_state = tf.nn.bidirectional_dynamic_rnn(lstmFwCell, lstmBwCell,
self.embeddedWords, dtype=tf.float32,
scope="bi-lstm" + str(idx))
# 对outputs中的fw和bw的结果拼接 [batch_size, time_step, hidden_size * 2]
self.embeddedWords = tf.concat(outputs, 2)
# 去除最后时间步的输出作为全连接的输入
finalOutput = self.embeddedWords[:, -1, :]
outputSize = config.model.hiddenSizes[-1] * 2 # 因为是双向LSTM,最终的输出值是fw和bw的拼接,因此要乘以2
output = tf.reshape(finalOutput, [-1, outputSize]) # reshape成全连接层的输入维度
# 全连接层的输出
with tf.name_scope("output"):
outputW = tf.get_variable(
"outputW",
shape=[outputSize, 1],
initializer=tf.contrib.layers.xavier_initializer())
outputB= tf.Variable(tf.constant(0.1, shape=[1]), name="outputB")
l2Loss += tf.nn.l2_loss(outputW)
l2Loss += tf.nn.l2_loss(outputB)
self.predictions = tf.nn.xw_plus_b(output, outputW, outputB, name="predictions")
self.binaryPreds = tf.cast(tf.greater_equal(self.predictions, 0.5), tf.float32, name="binaryPreds")
# 计算二元交叉熵损失
with tf.name_scope("loss"):
losses = tf.nn.sigmoid_cross_entropy_with_logits(logits=self.predictions, labels=self.inputY)
self.loss = tf.reduce_mean(losses) + config.model.l2RegLambda * l2Loss
参考文献
- 诸葛越,《百面机器学习》,人民邮电出版社
- Paul J Werbos. Backpropagation through time: what it does and how to do it.
Proceedings of the IEEE, 78(10):1550–1560,1990. - 邱锡鹏, 《神经网络和深度学习》
- DC童生,深度学习——RNN(2)双向RNN深度RNN几种变种,腾讯云(https://cloud.tencent.com/developer/article/1144238)
- wangduo, 理解 LSTM(Long Short-Term Memory, LSTM) 网络,博客园(https://www.cnblogs.com/wangduo/p/6773601.html?utm_source=itdadao&utm_medium=referral)