利用Tensorflow构建RNN并对序列数据进行建模
对文本处理 处理任务的方法中,一般将TF-IDF 向量作为特征输入。显然的缺陷是:这种方法丢失了输入的文本序列中每个单词的顺序 。
对一般的前馈神经网络 ,比如CNN,通常接受一个定长的向量作为输入 。
CNN对文本数据建模的时候,输入变长的字符串或者单词串,然后通过滑动窗口加上池化的方式将原先的输入转换成为一个固定长度的向量表示,
这样做可以捕捉到原始文本中的一些局部特征,但是对于两个单词之间的依赖关系则很难被捕获学习到 。
循环神经网络RNN能很好地处理文本数据变长且有序的输入序列。
RNN模拟了人类阅读文章的顺序,从前到后阅读文中的每一个单词,将前面阅读到的有用信息编码到状态变量中去,从而拥有了一定的记忆能力,可以更好地理解后续文本。
本文包括以下方面的内容:
-
介绍序列数据(sequential data) -
介绍用于序列建模的RNN -
介绍长短时记忆网络LSTM -
介绍时间截断反向传播算法TBPTT -
使用Tensorflow构建多层RNN并用于序列建模 -
实验1:基于IMDB电影评论数据集,利用RNN进行情感分析 -
实验2:基于The mysterious island文本数据,利用RNN字符级语言建模与LSTM单元建模; -
使用梯度裁剪以应对梯度爆炸 -
介绍Transformer模型以及自注意力机制
from IPython.display import Image
%matplotlib inline
1.介绍序列数据(sequential data)
序列数据有时候也称为序列,这里首先讨论序列数据的一些特性,然后了解序列数据的表示,此后研究序列数据的各种建模类型。
1.1序列数据的顺序因素(order matters)
序列的独特之处在于序列中的元素以一定的顺序出现,彼此之间不是独立的 。
而对于经典的机器学习模型而言:
经典的监督学习、机器学习算法假设输入是独立和同分布(IID)数据,这意味着训练示例相互独立,具有相同的基础分布。在这方面,基于相互独立的假设,训练实例给出的顺序是无关的 。
处理序列数据的时候,此种假设就不成立了:
因为,序列数据的顺序、或者次序很重要。比如对于预测某只股票的市场价值数据。
例如,假设我们有n个训练示例,其中每个训练示例代表某一天某只股票的市场价值。如果我们的任务是预测未来三天的股票市场价值,那么以日期排序的顺序来考虑以前的股票价格来得出趋势是有意义的,而不是以随机的顺序利用这些训练例子。
序列数据与时间序列数据(time-series data):
时间序列数据是一种特殊类型的序列数据,其中每个样本示例都与 时间维度直接相关 。
在时间序列数据中,数据的采样是针对连续的时间戳进行的,因此时间维度决定了数据点之间的顺序。
另一方面,并非所有的序列数据都具有时间维度,例如文本数据或DNA数据。此类数据的示例是有序的,但不具有时间先后关系。但RNN实际上可以用于处理时间序列数据 。
1.2序列数据的表示(Representing sequences)
前面提到序列数据的order非常重要,因此需要找到一种办法,在机器学习模型中利用到这种顺序信息。
这里记序列数据为
?
x
(
1
)
,
x
(
2
)
,
…
,
x
(
T
)
?
\left\langle\boldsymbol{x}^{(1)}, \boldsymbol{x}^{(2)}, \ldots, \boldsymbol{x}^{(T)}\right\rangle
?x(1),x(2),…,x(T)?。这里的上标表明了实例之间的顺序。同时,序列的长度为
T
T
T。
对于时间序列数据,
x
(
t
)
x^{(t)}
x(t)始于特定时间段
t
t
t的样本点。
下图显示了一个时间序列数据的例子,其中输入特征与样本标签都遵循时间轴顺序,因此
x
,
y
x,y
x,y均为序列:
Image(filename='images/16_01.png', width=700)

正如已经提到的,迄今为止我们所涵盖的标准神经网络(NN)模型,如多层感知器(MLP)和用于图像数据的CNN,都假定训练示例彼此独立 ,故不包含顺序信息。
所以,这样的模型不具有对前面的训练示例的记忆。例如,样本经过前馈和反向传播步骤,并且权值的更新独立于训练样本的处理顺序。相比之下,RNN是
为序列建模而设计的,能够记住过去的信息并相应地处理新的事件,这在处理序列数据时是一个明显的优势。
1.3序列建模(sequence modeling)的不同类型
参考链接
该文章总结了常见的序列建模任务,这些任务依赖于输入和输出数据的关系类型:
Image(filename='images/16_02.png', width=700)

many-to-one:
输入数据是一个序列,但输出是固定大小fixed-size向量或者标量,而非序列。例如:情感分析中,输入是文本,输出是标签;
One-to-many:
输入数据是标准格式的,而非序列,但输出是序列。例如:图像字幕image captioning ,这里的输入为图像,输出为总结该图像内容的英语短语;
many-to-many:
输入和输出都是序列,这个类别可以根据输入和输出是否同步 可以进一步划分为两类。同步的many-to-many建模任务的一个例子:视频分类,视频中的每一帧都被标记;
不同步(也称为delayed)的many-to-many建模任务的一个例子:将一种语言翻译成另一种语言,例如将应为通过机器阅读和处理,得到另一种语言;
2.用于sequences建模的RNN
首先介绍RNN的典型结构,其中包括建模序列数据的递归或循环组件。然后,再介绍经典的RNN中神经元激活是如何计算的。
2.1理解RNN的循环机制(looping mechanism)
下图将对比标准前馈网络和RNN的网络结构:
Image(filename='images/16_03.png', width=700)

上图中各层次的神经单元都没有显示,整体的假设:输入层input layer
(
x
)
\boldsymbol{(x)}
(x),隐藏层hidden layer
(
h
)
\boldsymbol{(h)}
(h),输出层output layer
(
o
)
\boldsymbol{(o)}
(o),它们都是矩阵,且均包含很多的神经单元。
2.2确定RNN的输出类型
这种通用的RNN体系结构可以对应于上述输入为序列的两种建模类型:many-to-many、many-to-one;
在Tensorflow Keras API中,通过将参数return_sequences设置为True or False,就可以实现指定循环层关于返回一个序列作为输出或者仅使用最后一个输出。
在标准的前馈网络中:信息从输入层流向隐含层,再从隐含层流向输出层。
在RNN中:隐藏层接收当前时间步长输入层的输入,且接收前一个时间步长的输入层的输入。因此隐藏层中相邻时间步长的信息流动使得网络对过去的事件信息具有了记忆。
这种信息流通常以循环的形式显示,在图符号 中也被称为递归边recurrent edge ,这就是这种通用RNN体系结构得名的原因。
类似于多层感知机,RNN可以由多个隐藏层组成。通常将带有一个隐藏层的RNN称为单层RNN。
Image(filename='images/16_04.png', width=700)

注意,在第一个时间步长
t
=
0
t=0
t=0,隐藏单元被初始化为0,或者是被初始化为较小的随机值 。
结合上述多层RNN,信息流动情况总结如下:
layer = 1: 隐藏层表示为
h
1
(
t
)
\boldsymbol{h}_{1}^{(t)}
h1(t)?,它从输入数据点
x
(
t
)
\boldsymbol{x}^{(t)}
x(t),以及同一层中的隐藏层,且是在前面的时间步长,接收输入
h
1
(
t
?
1
)
\boldsymbol{h}_1^{(t-1)}
h1(t?1)?;
layer = 2: 隐藏层表示为
h
2
(
t
)
\boldsymbol{h}_{2}^{(t)}
h2(t)?, 它从下面一层当前时间步长
o
1
(
t
)
\boldsymbol{o}_1^{(t)}
o1(t)?的输出接收其输入,并从上一个时间步长
h
2
(
t
?
1
)
\boldsymbol{h}_2^{(t-1)}
h2(t?1)?接收其自身的隐层值;
2.3RNN网络中activations的计算
RNN网络结构中的每个有向边都和一个权重矩阵相关联。这些权重不依赖于时间t, 在时间轴上是共享的 。单隐层RNN中的权重矩阵如下:
W
x
h
\boldsymbol{W}_{x h}
Wxh? : 也就是输入,
x
(
t
)
\boldsymbol{x}^{(t)}
x(t), 和隐藏层,
h
\boldsymbol{h}
h 之间的权重矩阵;
W
h
h
\boldsymbol{W}_{h h}
Whh? : 循环边之间的权重矩阵;
W
h
o
\boldsymbol{W}_{h o}
Who? : 隐层和输出层之间的权重矩阵;
Image(filename='images/16_05.png', width=700)

在一些实现中,矩阵
W
x
h
\boldsymbol{W}_{x h}
Wxh? 和矩阵
W
h
h
\boldsymbol{W}_{h h}
Whh?会被组合连接到一起,形成一个整体。表示形式如下;
W
h
=
[
W
x
h
;
W
h
h
]
\boldsymbol{W}_{h}=\left[\boldsymbol{W}_{x h} ; \boldsymbol{W}_{h h}\right]
Wh?=[Wxh?;Whh?]
计算网络的激活类似于其他标准多层感知机和其他类型的前馈神经网络。
对于隐含层,网络的输入(净活性值),
Z
h
\boldsymbol{Z}_h
Zh?的计算是通过输入特征与权值的线性组合计算得来的:
z
h
(
t
)
=
W
x
h
x
(
t
)
+
W
h
h
h
(
t
?
1
)
+
b
h
\mathbf{z}_{h}^{(t)}=\boldsymbol{W}_{x h} \boldsymbol{x}^{(t)}+\boldsymbol{W}_{h h} \boldsymbol{h}^{(t-1)}+\boldsymbol{b}_{h}
zh(t)?=Wxh?x(t)+Whh?h(t?1)+bh?
则隐层在时间步长
t
t
t处的活性值计算如下:
h
(
t
)
=
?
h
(
z
h
(
t
)
)
=
?
h
(
W
x
h
x
(
t
)
+
W
h
h
h
(
t
?
1
)
+
b
h
)
\boldsymbol{h}^{(t)}=\phi_{h}\left(\mathbf{z}_{h}^{(t)}\right)=\phi_{h}\left(\boldsymbol{W}_{x h} \boldsymbol{x}^{(t)}+\boldsymbol{W}_{h h} \boldsymbol{h}^{(t-1)}+\boldsymbol{b}_{h}\right)
h(t)=?h?(zh(t)?)=?h?(Wxh?x(t)+Whh?h(t?1)+bh?)
其中,
b
h
\boldsymbol{b}_{h}
bh?为隐层单元的偏置向量,
?
h
\phi_{h}
?h?为隐层的激活函数。
将上述权重矩阵写成组合形式,则隐层活性值计算如下:
h
(
t
)
=
?
h
(
[
W
x
h
;
W
h
h
]
[
x
(
t
)
h
(
t
?
1
)
]
+
b
h
)
\boldsymbol{h}^{(t)}=\phi_{h}\left(\left[\boldsymbol{W}_{x h} ; \boldsymbol{W}_{h h}\right]\left[\begin{array}{c} \boldsymbol{x}^{(t)} \\ \boldsymbol{h}^{(t-1)} \end{array}\right]+\boldsymbol{b}_{h}\right)
h(t)=?h?([Wxh?;Whh?][x(t)h(t?1)?]+bh?)
则,输出单元的activations计算如下:
o
(
t
)
=
?
o
(
W
h
o
h
(
t
)
+
b
o
)
\boldsymbol{o}^{(t)}=\phi_{o}\left(\boldsymbol{W}_{h o} \boldsymbol{h}^{(t)}+\boldsymbol{b}_{o}\right)
o(t)=?o?(Who?h(t)+bo?)
上述计算过程的几何表示如下:
Image(filename='images/16_06.png', width=700)

2.4使用基于时间的反向传播BPTT训练RNN
BPTT相关文献。
梯度的推导,基本的思想为:整体的损失
L
L
L为从时间步长
t
t
t到时间步长
T
T
T的损失累积和:
L
=
∑
t
=
1
T
L
(
t
)
L=\sum_{t=1}^{T} L^{(t)}
L=t=1∑T?L(t)
由于在
t
t
t时刻的损失依赖于前面从
1
:
t
1:t
1:t时间步长的隐层单元,梯度计算如下:
?
L
(
t
)
?
W
h
h
=
?
L
(
t
)
?
o
(
t
)
×
?
o
(
t
)
?
h
(
t
)
×
(
∑
k
=
1
t
?
h
(
t
)
?
h
(
k
)
×
?
h
(
k
)
?
W
h
h
)
\frac{\partial L^{(t)}}{\partial \boldsymbol{W}_{h h}}=\frac{\partial L^{(t)}}{\partial \boldsymbol{o}^{(t)}} \times \frac{\partial \boldsymbol{o}^{(t)}}{\partial \boldsymbol{h}^{(t)}} \times\left(\sum_{k=1}^{t} \frac{\partial \boldsymbol{h}^{(t)}}{\partial \boldsymbol{h}^{(k)}} \times \frac{\partial \boldsymbol{h}^{(k)}}{\partial \boldsymbol{W}_{h h}}\right)
?Whh??L(t)?=?o(t)?L(t)?×?h(t)?o(t)?×(k=1∑t??h(k)?h(t)?×?Whh??h(k)?)
其中,
?
h
(
t
)
?
h
(
k
)
\frac{\partial \boldsymbol{h}^{(t)}}{\partial \boldsymbol{h}^{(k)}}
?h(k)?h(t)?的计算为相邻时间步的连乘积:
?
h
(
t
)
?
h
(
k
)
=
∏
i
=
k
+
1
t
?
h
(
i
)
?
h
(
i
?
1
)
\frac{\partial \boldsymbol{h}^{(t)}}{\partial \boldsymbol{h}^{(k)}}=\prod_{i=k+1}^{t} \frac{\partial \boldsymbol{h}^{(i)}}{\partial \boldsymbol{h}^{(i-1)}}
?h(k)?h(t)?=i=k+1∏t??h(i?1)?h(i)?
3.隐层循环和输出层循环
除了上面的隐含层具有循环或递归性质的循环网络,还有另一种模型。即循环连接来自输出层 ,这种情况下,输出层的上一个时间步长的活性值
o
(
t
?
1
)
\boldsymbol{o}^{(t-1)}
o(t?1)可以通过两种方式计算得到:
权重矩阵的表示:
W
h
h
\boldsymbol{W}_{hh}
Whh?代表–hidden-to-hidden;
W
o
h
\boldsymbol{W}_{oh}
Woh?代表–output-to-hidden;
W
o
o
\boldsymbol{W}_{oo}
Woo?代表–output-to-output。有适合也整体记作:
W
r
e
c
\boldsymbol{W}_{rec}
Wrec?
Image(filename='images/16_07.png', width=700)

import tensorflow as tf
tf.random.set_seed(1)
"""
通过SimpleRNN定义一个递归层,输入形状为(None, None, 5)。第一个维度为batch维度;第二个维度对应序列,使用None表示可变序列长度;
最后一个维度对应的是特征。
"""
rnn_layer = tf.keras.layers.SimpleRNN(
units=2, use_bias=True,
return_sequences=True)
rnn_layer.build(input_shape=(None, None, 5))
w_xh, w_oo, b_h = rnn_layer.weights
print('W_xh shape:', w_xh.shape)
print('W_oo shape:', w_oo.shape)
print('b_h shape:', b_h.shape)
W_xh shape: (5, 2)
W_oo shape: (2, 2)
b_h shape: (2,)
这里设置return_sequencts=True, 因此最后的输出仍为sequence,且对应于
?
o
(
0
)
,
o
(
1
)
,
o
(
2
)
?
\left\langle\boldsymbol{o}^{(0)}, \boldsymbol{o}^{(1)}, \boldsymbol{o}^{(2)}\right\rangle
?o(0),o(1),o(2)?
x_seq = tf.convert_to_tensor(
[[1.0]*5, [2.0]*5, [3.0]*5],
dtype=tf.float32)
"""
在rnn_layer上调用前向传递,在每个时间步上计算输出
"""
output = rnn_layer(tf.reshape(x_seq, shape=(1, 3, 5)))
out_man = []
for t in range(len(x_seq)):
xt = tf.reshape(x_seq[t], (1, 5))
print('Time step {} =>'.format(t))
print(' Input :', xt.numpy())
ht = tf.matmul(xt, w_xh) + b_h
print(' Hidden :', ht.numpy())
if t>0:
prev_o = out_man[t-1]
else:
prev_o = tf.zeros(shape=(ht.shape))
ot = ht + tf.matmul(prev_o, w_oo)
ot = tf.math.tanh(ot)
out_man.append(ot)
print(' Output (manual) :', ot.numpy())
print(' SimpleRNN output:'.format(t), output[0][t].numpy())
print()
Time step 0 =>
Input : [[1. 1. 1. 1. 1.]]
Hidden : [[0.41464037 0.96012145]]
Output (manual) : [[0.39240566 0.744331 ]]
SimpleRNN output: [0.39240566 0.744331 ]
Time step 1 =>
Input : [[2. 2. 2. 2. 2.]]
Hidden : [[0.82928073 1.9202429 ]]
Output (manual) : [[0.8011651 0.9912947]]
SimpleRNN output: [0.8011651 0.9912947]
Time step 2 =>
Input : [[3. 3. 3. 3. 3.]]
Hidden : [[1.243921 2.8803644]]
Output (manual) : [[0.9546827 0.999307 ]]
SimpleRNN output: [0.9546827 0.999307 ]
4.长距离学习的挑战
前面提到的BPTT引入了一些挑战,即梯度消失和梯度爆炸 。
相关博文连接–苏建林
Image(filename='images/16_08.png', width=700)

$ \frac{\partial \boldsymbol{h}^{(t)}}{\partial \boldsymbol{h}^{(k)}}
实
际
上
包
含
了
实际上包含了
实际上包含了t-1$项的连乘积,如果绝对值都小于1,则结果趋近于0,绝对值都大于1,则结果就非常大。
实践中,通常有以下的方法用于处理上述问题:
使用梯度裁剪来指定一个截止值或者阈值,并将此阈值赋给超过这个值的梯度值。
TBPTT只是限制了信号在每次前向传递后,可以反向传播的时间步长。
虽然梯度裁剪和TBPTT都可以用于解决梯度爆炸问题,但截断限制了梯度有效反向传播和适当更新权值的步数 。
但是LSTM在处理上述问题的时候,更为成功。
5.LSTM(Long Short-Term Memory cells )
LSTM的提出是为了克服梯度消失问题 。
LSTM的构造块儿是一个存储单元,本质上代表或替代了标准RNN的隐藏层。
几何表示如下;
Image(filename='images/16_09.png', width=700)

前一个时间步长中的单元状态
C
(
t
?
1
)
\boldsymbol{C}^{(t-1)}
C(t?1)被修改为当前时间步长
C
(
t
)
\boldsymbol{C}^{(t)}
C(t)的单元状态cell state;
这一过程没有与任何的权重因子相乘。这个存储单元中的信息流由几个门控单元控制。
上图中的
⊙
\odot
⊙表示元素级别的乘积;
上图中的
⊕
\oplus
⊕表示元素级别的加和;
x
(
t
)
\boldsymbol{x}^{(t)}
x(t)表示
t
t
t时刻的输入数据;
h
(
t
?
1
)
\boldsymbol{h}^{(t-1)}
h(t?1)表示
t
?
1
t-1
t?1时刻的隐层单元;
遗忘门、输入门、输出门、内部记忆单元使用sigmoid函数或者tanh函数,此外包括一组权值表示;
LSTM中有三种门控,分别如下:
- forget gate遗忘门(
f
t
\boldsymbol{f}_t
ft?):允许存储单元在不无限增长的情况下重置单元状态。即决定了哪些信息允许通过,哪些信息被遗忘。
计算过程如下:
f
t
=
σ
(
W
x
f
x
(
t
)
+
W
h
f
h
(
t
?
1
)
+
b
f
)
\boldsymbol{f}_{t}=\sigma\left(\boldsymbol{W}_{x f} \boldsymbol{x}^{(t)}+\boldsymbol{W}_{h f} \boldsymbol{h}^{(t-1)}+\boldsymbol{b}_{f}\right)
ft?=σ(Wxf?x(t)+Whf?h(t?1)+bf?)
- input gate(
i
t
\boldsymbol{i}_t
it?)和candidate value(
(
C
~
t
)
\left(\widetilde{\boldsymbol{C}}_{t}\right)
(C
t?))负责更新单元状态;
计算过程如下:
i
t
=
σ
(
W
x
i
x
(
t
)
+
W
h
i
h
(
t
?
1
)
+
b
i
)
C
~
t
=
tanh
?
(
W
x
c
x
(
t
)
+
W
h
c
h
(
t
?
1
)
+
b
c
)
\begin{gathered} \boldsymbol{i}_{t}=\sigma\left(\boldsymbol{W}_{x i} \boldsymbol{x}^{(t)}+\boldsymbol{W}_{h i} \boldsymbol{h}^{(t-1)}+\boldsymbol{b}_{i}\right) \\ \widetilde{\boldsymbol{C}}_{t}=\tanh \left(\boldsymbol{W}_{x c} \boldsymbol{x}^{(t)}+\boldsymbol{W}_{h c} \boldsymbol{h}^{(t-1)}+\boldsymbol{b}_{c}\right) \end{gathered}
it?=σ(Wxi?x(t)+Whi?h(t?1)+bi?)C
t?=tanh(Wxc?x(t)+Whc?h(t?1)+bc?)?
第
t
t
t时刻的单元状态计算如下:
C
(
t
)
=
(
C
(
t
?
1
)
⊙
f
t
)
⊕
(
i
t
⊙
C
~
t
)
\boldsymbol{C}^{(t)}=\left(\boldsymbol{C}^{(t-1)} \odot \boldsymbol{f}_{t}\right) \oplus\left(\boldsymbol{i}_{t} \odot \widetilde{\boldsymbol{C}}_{t}\right)
C(t)=(C(t?1)⊙ft?)⊕(it?⊙C
t?)
- output gate(
o
t
\boldsymbol{o}_t
ot?):决定了如何更新隐层单元的值
计算过程如下:
o
t
=
σ
(
W
x
o
x
(
t
)
+
W
h
o
h
(
t
?
1
)
+
b
o
)
\boldsymbol{o}_{t}=\sigma\left(\boldsymbol{W}_{x o} \boldsymbol{x}^{(t)}+\boldsymbol{W}_{h o} \boldsymbol{h}^{(t-1)}+\boldsymbol{b}_{o}\right)
ot?=σ(Wxo?x(t)+Who?h(t?1)+bo?)
这样一来,当前时间步的隐藏单元计算如下:
h
(
t
)
=
o
t
⊙
tanh
?
(
C
(
t
)
)
\boldsymbol{h}^{(t)}=\boldsymbol{o}_{t} \odot \tanh \left(\boldsymbol{C}^{(t)}\right)
h(t)=ot?⊙tanh(C(t))
实际上Tensorflow已经在优化的包装器函数中实现了上述结构,因此就可以实现轻松地定义LSTM单元。
6.使用TensorFlow构建RNN
6.1Project one: 基于 IMDb movie reviews数据进行情感分析
准备电影评论数据:
import tensorflow as tf
import tensorflow_datasets as tfds
import numpy as np
import pandas as pd
import os
import gzip
import shutil
with gzip.open('../ch08/movie_data.csv.gz', 'rb') as f_in, open('movie_data.csv', 'wb') as f_out:
shutil.copyfileobj(f_in, f_out)
df = pd.read_csv('movie_data.csv', encoding='utf-8')
df.tail()
| review | sentiment |
---|
49995 | OK, lets start with the best. the building. al... | 0 |
---|
49996 | The British 'heritage film' industry is out of... | 0 |
---|
49997 | I don't even know where to begin on this one. ... | 0 |
---|
49998 | Richard Tyler is a little boy who is scared of... | 0 |
---|
49999 | I waited long to watch this movie. Also becaus... | 1 |
---|
在将数据输入给模型之前,需要有以下几个预处理步骤:
-
创建Tensorflow数据集对象,并将其划分为训练集、测试集、验证集; -
识别训练数据集中的唯一的单词; -
将唯一的单词映射到一个唯一的整数,并将评论文本编码为已编码的整数(每个唯一单词的索引) -
将数据集划分为mini-batch,作为模型的输入
target = df.pop('sentiment')
ds_raw = tf.data.Dataset.from_tensor_slices(
(df.values, target.values))
for ex in ds_raw.take(3):
tf.print(ex[0].numpy()[0][:50], ex[1])
b'In 1974, the teenager Martha Moxley (Maggie Grace)' 1
b'OK... so... I really like Kris Kristofferson and h' 0
b'***SPOILER*** Do not read this, if you think about' 0
数据集划分:整个数据集包含50000个样本
-
前25000个样本用于验证(包含测试数据集) -
接下来的20000条用于训练 -
5000条用于验证
tf.random.set_seed(1)
ds_raw = ds_raw.shuffle(
50000, reshuffle_each_iteration=False)
ds_raw_test = ds_raw.take(25000)
ds_raw_train_valid = ds_raw.skip(25000)
ds_raw_train = ds_raw_train_valid.take(20000)
ds_raw_valid = ds_raw_train_valid.skip(20000)
为了准备输入到NN的数据,需要将其编码为数值,如上步骤2,3;
首先在训练数据集中找到唯一的单词(标记–takens),这里为了更加高效,使用的是Counter类;
下面的代码中,实例化一个Counter对象–token_counts,该对象将手机唯一的单词的词频。
这里仅仅对唯一的单词感兴趣,不需要单词计数。tensorflow_datasets提供了一个Tokenizer类,用于将文本拆分为单词
from collections import Counter
tokenizer = tfds.features.text.Tokenizer()
token_counts = Counter()
for example in ds_raw_train:
tokens = tokenizer.tokenize(example[0].numpy()[0])
token_counts.update(tokens)
print('Vocab-size:', len(token_counts))
Vocab-size: 87007
encoder = tfds.features.text.TokenTextEncoder(token_counts)
example_str = 'This is an example!'
encoder.encode(example_str)
[232, 9, 270, 1123]
验证集或测试集中有一些单词tokens没有出现在训练数据集合中,因此没有包含在上述映射中。
如果有
q
q
q个单词,这里为87007,那么所有以前没有见过的tokens将被分配给
q
+
1
q+1
q+1.
也就是说,索引
q
+
1
q+1
q+1是为了未知单词而保留的。
def encode(text_tensor, label):
text = text_tensor.numpy()[0]
encoded_text = encoder.encode(text)
return encoded_text, label
def encode_map_fn(text, label):
return tf.py_function(encode, inp=[text, label],
Tout=(tf.int64, tf.int64))
ds_train = ds_raw_train.map(encode_map_fn)
ds_valid = ds_raw_valid.map(encode_map_fn)
ds_test = ds_raw_test.map(encode_map_fn)
tf.random.set_seed(1)
for example in ds_train.shuffle(1000).take(5):
print('Sequence length:', example[0].shape)
example
Sequence length: (24,)
Sequence length: (179,)
Sequence length: (262,)
Sequence length: (535,)
Sequence length: (130,)
(<tf.Tensor: shape=(130,), dtype=int64, numpy=
array([ 579, 1296, 32, 425, 40, 763, 9267, 65, 280,
308, 6, 481, 155, 473, 2, 3, 684, 9,
781, 176, 959, 730, 3917, 67, 9905, 13, 277,
24, 35, 371, 16368, 6, 14, 17231, 29, 187,
1651, 489, 503, 480, 143, 32, 270, 5851, 2402,
13, 3592, 3443, 425, 3313, 256, 257, 1577, 117,
8, 698, 270, 564, 56, 8, 42, 7517, 2629,
820, 25, 60, 79, 343, 32, 645, 14, 528,
241, 32, 1980, 8, 56, 8, 42, 1364, 573,
5183, 43, 12, 3870, 32, 312, 642, 251, 1401,
17232, 8, 698, 257, 750, 2, 9, 76, 235,
8, 42, 235, 840, 666, 258, 17233, 419, 32,
17234, 585, 420, 840, 25, 40, 13, 14, 198,
266, 623, 173, 179, 4103, 216, 25, 616, 14185,
186, 35, 16250, 120], dtype=int64)>,
<tf.Tensor: shape=(), dtype=int64, numpy=0>)
- batch() vs. padded_batch()
BATCH_SIZE = 32
train_data = all_encoded_data.batch(BATCH_SIZE)
next(iter(train_data))
ds_subset = ds_train.take(8)
for example in ds_subset:
print('Individual size:', example[0].shape)
ds_batched = ds_subset.padded_batch(
4, padded_shapes=([-1], []))
for batch in ds_batched:
print('Batch dimension:', batch[0].shape)
Individual size: (119,)
Individual size: (688,)
Individual size: (308,)
Individual size: (204,)
Individual size: (326,)
Individual size: (240,)
Individual size: (127,)
Individual size: (453,)
Batch dimension: (4, 688)
Batch dimension: (4, 453)
train_data = ds_train.padded_batch(
32, padded_shapes=([-1],[]))
valid_data = ds_valid.padded_batch(
32, padded_shapes=([-1],[]))
test_data = ds_test.padded_batch(
32, padded_shapes=([-1],[]))
6.2Embedding layers for sentence encoding
-
input_dim : number of words, i.e. maximum integer index + 1. -
output_dim : -
input_length : the length of (padded) sequence
- for example,
'This is an example' -> [0, 0, 0, 0, 0, 0, 3, 1, 8, 9] => input_lenght is 10 -
When calling the layer, takes integr values as input, the embedding layer convert each interger into float vector of size [output_dim]
- If input shape is
[BATCH_SIZE] , output shape will be [BATCH_SIZE, output_dim] - If input shape is
[BATCH_SIZE, 10] , output shape will be [BATCH_SIZE, 10, output_dim]
在上一步的数据准备过程中,生成了相同长度的序列。这些序列的元素是与唯一单词的索引相对应的整数。这些单词索引可以通过几种不同的方式转换为输入
特征。一种简单的方法是应用独热码编码将索引转换为0和1的向量。然后,每个单词将被映射到一个向量,该向量的大小是整个数据集中唯一单词的数量。考虑
到独特单词的数量(词汇表的大小)可能在
1
0
4
,
1
0
5
10^4,10^5
104,105(也将是我们的输入特征的数量)的数量级,在这些特征上训练的模型可能会受到维度灾难的影响。此外,这些特
征非常稀疏,因为除了当前值之外,所有特征都是零。
另一种方法是将每个单词映射到具有实值元素(不一定是整数)的固定大小的向量 。与独热码
不同,可以使用有限大小的向量来表示无限数量的实数。(理论上,可以从给定区间提取无限实数,例如[-1,1]。)
Image(filename='images/16_10.png', width=700)

from tensorflow.keras.layers import Embedding
model = tf.keras.Sequential()
model.add(Embedding(input_dim=100,
output_dim=6,
input_length=20,
name='embed-layer'))
model.summary()
Model: "sequential"
_________________________________________________________________
Layer (type) Output Shape Param #
=================================================================
embed-layer (Embedding) (None, 20, 6) 600
=================================================================
Total params: 600
Trainable params: 600
Non-trainable params: 0
_________________________________________________________________
6.3建立RNN
"""
创建RNN模型,输入维度为1000,输出维度为32----嵌入层;
添加两个类型为SimpleRNN的循环层。
添加一个非循环全连接层作为输出层,将返回一个单一的输出值作为预测;
"""
from tensorflow.keras import Sequential
from tensorflow.keras.layers import Embedding
from tensorflow.keras.layers import SimpleRNN
from tensorflow.keras.layers import Dense
model = Sequential()
model.add(Embedding(1000, 32))
model.add(SimpleRNN(32, return_sequences=True))
model.add(SimpleRNN(32))
model.add(Dense(1))
model.summary()
Model: "sequential_1"
_________________________________________________________________
Layer (type) Output Shape Param #
=================================================================
embedding_1 (Embedding) (None, None, 32) 32000
_________________________________________________________________
simple_rnn_1 (SimpleRNN) (None, None, 32) 2080
_________________________________________________________________
simple_rnn_2 (SimpleRNN) (None, 32) 2080
_________________________________________________________________
dense (Dense) (None, 1) 33
=================================================================
Total params: 36,193
Trainable params: 36,193
Non-trainable params: 0
_________________________________________________________________
from tensorflow.keras.layers import LSTM
model = Sequential()
model.add(Embedding(10000, 32))
model.add(LSTM(32, return_sequences=True))
model.add(LSTM(32))
model.add(Dense(1))
model.summary()
Model: "sequential_2"
_________________________________________________________________
Layer (type) Output Shape Param #
=================================================================
embedding_2 (Embedding) (None, None, 32) 320000
_________________________________________________________________
lstm (LSTM) (None, None, 32) 8320
_________________________________________________________________
lstm_1 (LSTM) (None, 32) 8320
_________________________________________________________________
dense_1 (Dense) (None, 1) 33
=================================================================
Total params: 336,673
Trainable params: 336,673
Non-trainable params: 0
_________________________________________________________________
from tensorflow.keras.layers import GRU
model = Sequential()
model.add(Embedding(10000, 32))
model.add(GRU(32, return_sequences=True))
model.add(GRU(32))
model.add(Dense(1))
model.summary()
Model: "sequential_3"
_________________________________________________________________
Layer (type) Output Shape Param #
=================================================================
embedding_3 (Embedding) (None, None, 32) 320000
_________________________________________________________________
gru (GRU) (None, None, 32) 6336
_________________________________________________________________
gru_1 (GRU) (None, 32) 6336
_________________________________________________________________
dense_2 (Dense) (None, 1) 33
=================================================================
Total params: 332,705
Trainable params: 332,705
Non-trainable params: 0
_________________________________________________________________
6.4建立情感分析任务的RNN模型
由于这里有非常长的序列,所以使用LSTM层来说明长期影响。此外,将把LSTM层放在双向包装器中,这将使递归层从两个方向(从开始到结束,以及相反方向)通过输入序列
embedding_dim = 20
vocab_size = len(token_counts) + 2
tf.random.set_seed(1)
bi_lstm_model = tf.keras.Sequential([
tf.keras.layers.Embedding(
input_dim=vocab_size,
output_dim=embedding_dim,
name='embed-layer'),
tf.keras.layers.Bidirectional(
tf.keras.layers.LSTM(64, name='lstm-layer'),
name='bidir-lstm'),
tf.keras.layers.Dense(64, activation='relu'),
tf.keras.layers.Dense(1, activation='sigmoid')
])
bi_lstm_model.summary()
bi_lstm_model.compile(
optimizer=tf.keras.optimizers.Adam(1e-3),
loss=tf.keras.losses.BinaryCrossentropy(from_logits=False),
metrics=['accuracy'])
history = bi_lstm_model.fit(
train_data,
validation_data=valid_data,
epochs=10)
test_results= bi_lstm_model.evaluate(test_data)
print('Test Acc.: {:.2f}%'.format(test_results[1]*100))
if not os.path.exists('models'):
os.mkdir('models')
bi_lstm_model.save('models/Bidir-LSTM-full-length-seq.h5')
- Trying SimpleRNN with short sequences
def preprocess_datasets(
ds_raw_train,
ds_raw_valid,
ds_raw_test,
max_seq_length=None,
batch_size=32):
tokenizer = tfds.features.text.Tokenizer()
token_counts = Counter()
for example in ds_raw_train:
tokens = tokenizer.tokenize(example[0].numpy()[0])
if max_seq_length is not None:
tokens = tokens[-max_seq_length:]
token_counts.update(tokens)
print('Vocab-size:', len(token_counts))
encoder = tfds.features.text.TokenTextEncoder(token_counts)
def encode(text_tensor, label):
text = text_tensor.numpy()[0]
encoded_text = encoder.encode(text)
if max_seq_length is not None:
encoded_text = encoded_text[-max_seq_length:]
return encoded_text, label
def encode_map_fn(text, label):
return tf.py_function(encode, inp=[text, label],
Tout=(tf.int64, tf.int64))
ds_train = ds_raw_train.map(encode_map_fn)
ds_valid = ds_raw_valid.map(encode_map_fn)
ds_test = ds_raw_test.map(encode_map_fn)
train_data = ds_train.padded_batch(
batch_size, padded_shapes=([-1],[]))
valid_data = ds_valid.padded_batch(
batch_size, padded_shapes=([-1],[]))
test_data = ds_test.padded_batch(
batch_size, padded_shapes=([-1],[]))
return (train_data, valid_data,
test_data, len(token_counts))
def build_rnn_model(embedding_dim, vocab_size,
recurrent_type='SimpleRNN',
n_recurrent_units=64,
n_recurrent_layers=1,
bidirectional=True):
tf.random.set_seed(1)
model = tf.keras.Sequential()
model.add(
Embedding(
input_dim=vocab_size,
output_dim=embedding_dim,
name='embed-layer')
)
for i in range(n_recurrent_layers):
return_sequences = (i < n_recurrent_layers-1)
if recurrent_type == 'SimpleRNN':
recurrent_layer = SimpleRNN(
units=n_recurrent_units,
return_sequences=return_sequences,
name='simprnn-layer-{}'.format(i))
elif recurrent_type == 'LSTM':
recurrent_layer = LSTM(
units=n_recurrent_units,
return_sequences=return_sequences,
name='lstm-layer-{}'.format(i))
elif recurrent_type == 'GRU':
recurrent_layer = GRU(
units=n_recurrent_units,
return_sequences=return_sequences,
name='gru-layer-{}'.format(i))
if bidirectional:
recurrent_layer = Bidirectional(
recurrent_layer, name='bidir-'+recurrent_layer.name)
model.add(recurrent_layer)
model.add(tf.keras.layers.Dense(64, activation='relu'))
model.add(tf.keras.layers.Dense(1, activation='sigmoid'))
return model
from tensorflow.keras.layers import Bidirectional
batch_size = 32
embedding_dim = 20
max_seq_length = 100
train_data, valid_data, test_data, n = preprocess_datasets(
ds_raw_train, ds_raw_valid, ds_raw_test,
max_seq_length=max_seq_length,
batch_size=batch_size
)
vocab_size = n + 2
rnn_model = build_rnn_model(
embedding_dim, vocab_size,
recurrent_type='SimpleRNN',
n_recurrent_units=64,
n_recurrent_layers=1,
bidirectional=True)
rnn_model.summary()
Vocab-size: 58063
rnn_model.compile(optimizer=tf.keras.optimizers.Adam(1e-3),
loss=tf.keras.losses.BinaryCrossentropy(from_logits=False),
metrics=['accuracy'])
history = rnn_model.fit(
train_data,
validation_data=valid_data,
epochs=10)
results = rnn_model.evaluate(test_data)
print('Test Acc.: {:.2f}%'.format(results[1]*100))
附
A – 使用tensorflow_datasets获取数据
imdb_bldr = tfds.builder('imdb_reviews')
print(imdb_bldr.info)
imdb_bldr.download_and_prepare()
datasets = imdb_bldr.as_dataset(shuffle_files=False)
datasets.keys()
tfds.core.DatasetInfo(
name='imdb_reviews',
version=1.0.0,
description='Large Movie Review Dataset.
This is a dataset for binary sentiment classification containing substantially more data than previous benchmark datasets. We provide a set of 25,000 highly polar movie reviews for training, and 25,000 for testing. There is additional unlabeled data for use as well.',
homepage='http://ai.stanford.edu/~amaas/data/sentiment/',
features=FeaturesDict({
'label': ClassLabel(shape=(), dtype=tf.int64, num_classes=2),
'text': Text(shape=(), dtype=tf.string),
}),
total_num_examples=100000,
splits={
'test': 25000,
'train': 25000,
'unsupervised': 50000,
},
supervised_keys=('text', 'label'),
citation="""@InProceedings{maas-EtAl:2011:ACL-HLT2011,
author = {Maas, Andrew L. and Daly, Raymond E. and Pham, Peter T. and Huang, Dan and Ng, Andrew Y. and Potts, Christopher},
title = {Learning Word Vectors for Sentiment Analysis},
booktitle = {Proceedings of the 49th Annual Meeting of the Association for Computational Linguistics: Human Language Technologies},
month = {June},
year = {2011},
address = {Portland, Oregon, USA},
publisher = {Association for Computational Linguistics},
pages = {142--150},
url = {http://www.aclweb.org/anthology/P11-1015}
}""",
redistribution_info=,
)
[1mDownloading and preparing dataset imdb_reviews/plain_text/1.0.0 (download: 80.23 MiB, generated: Unknown size, total: 80.23 MiB) to C:\Users\xiaoyao\tensorflow_datasets\imdb_reviews\plain_text\1.0.0...[0m
Dl Completed...: 0 url [00:00, ? url/s]
Dl Size...: 0 MiB [00:00, ? MiB/s]
0 examples [00:00, ? examples/s]
Shuffling and writing examples to C:\Users\xiaoyao\tensorflow_datasets\imdb_reviews\plain_text\1.0.0.incomplete21S3Q3\imdb_reviews-train.tfrecord
0%| | 0/25000 [00:00<?, ? examples/s]
0 examples [00:00, ? examples/s]
Shuffling and writing examples to C:\Users\xiaoyao\tensorflow_datasets\imdb_reviews\plain_text\1.0.0.incomplete21S3Q3\imdb_reviews-test.tfrecord
0%| | 0/25000 [00:00<?, ? examples/s]
0 examples [00:00, ? examples/s]
Shuffling and writing examples to C:\Users\xiaoyao\tensorflow_datasets\imdb_reviews\plain_text\1.0.0.incomplete21S3Q3\imdb_reviews-unsupervised.tfrecord
0%| | 0/50000 [00:00<?, ? examples/s]
[1mDataset imdb_reviews downloaded and prepared to C:\Users\xiaoyao\tensorflow_datasets\imdb_reviews\plain_text\1.0.0. Subsequent calls will reuse this data.[0m
dict_keys(['test', 'train', 'unsupervised'])
imdb_train = datasets['train']
imdb_train = datasets['test']
B – Tokenizer and Encoder
tfds.features.text.Tokenizer : https://www.tensorflow.org/datasets/api_docs/python/tfds/features/text/Tokenizertfds.features.text.TokenTextEncoder : https://www.tensorflow.org/datasets/api_docs/python/tfds/features/text/TokenTextEncoder
vocab_set = {'a', 'b', 'c', 'd'}
encoder = tfds.features.text.TokenTextEncoder(vocab_set)
print(encoder)
print(encoder.encode(b'a b c d, , : .'))
print(encoder.encode(b'a b c d e f g h i z'))
<TokenTextEncoder vocab_size=6>
[3, 2, 4, 1]
[3, 2, 4, 1, 5, 5, 5, 5, 5, 5]
C – Text Pre-processing with Keras
TOP_K = 200
MAX_LEN = 10
tokenizer = tf.keras.preprocessing.text.Tokenizer(num_words=TOP_K)
tokenizer.fit_on_texts(['this is an example', 'je suis en forme '])
sequences = tokenizer.texts_to_sequences(['this is an example', 'je suis en forme '])
print(sequences)
tf.keras.preprocessing.sequence.pad_sequences(sequences, maxlen=MAX_LEN)
[[1, 2, 3, 4], [5, 6, 7, 8]]
array([[0, 0, 0, 0, 0, 0, 1, 2, 3, 4],
[0, 0, 0, 0, 0, 0, 5, 6, 7, 8]])
TOP_K = 20000
MAX_LEN = 500
tokenizer = tf.keras.preprocessing.text.Tokenizer(num_words=TOP_K)
tokenizer.fit_on_texts(
[example['text'].numpy().decode('utf-8')
for example in imdb_train])
x_train = tokenizer.texts_to_sequences(
[example['text'].numpy().decode('utf-8')
for example in imdb_train])
print(len(x_train))
x_train_padded = tf.keras.preprocessing.sequence.pad_sequences(
x_train, maxlen=MAX_LEN)
print(x_train_padded.shape)
25000
(25000, 500)
D – Embedding
from tensorflow.keras.layers import Embedding
tf.random.set_seed(1)
embed = Embedding(input_dim=100, output_dim=4)
inp_arr = np.array([1, 98, 5, 6, 67, 45])
tf.print(embed(inp_arr))
tf.print(embed(inp_arr).shape)
tf.print(embed(np.array([1])))
[[-0.0208060984 0.0142502077 0.0475785471 -0.00649005175]
[-0.00420691818 -0.0375086069 -0.00477621704 0.00311584398]
[0.028728161 -0.0440448038 -0.0428906195 -0.019158531]
[-0.0248817336 0.0408470519 -0.00285203382 -0.0257614851]
[0.0443614833 0.00331580639 0.043055404 -0.011118304]
[-0.0281324144 0.00720113516 0.0192188732 -0.0186921246]]
TensorShape([6, 4])
[[-0.0208060984 0.0142502077 0.0475785471 -0.00649005175]]
|