3.4RNN 层使用方法
Posted 炫云云
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了3.4RNN 层使用方法相关的知识,希望对你有一定的参考价值。
3.1意境级讲解循环神经网络RNN_炫云云-CSDN博客
在介绍完循环神经网络的算法原理之后,我们来学习如何在 TensorFlow 中实现 RNN 层。在 TensorFlow 中,可以通过 layers.SimpleRNNCell
来完成
h
t
=
f
(
U
x
t
+
W
h
t
−
1
+
b
)
(1)
\\Large \\color{green}{h_t=f(Ux_{t}+Wh_{t-1}+b)\\tag{1}}
ht=f(Uxt+Wht−1+b)(1)
计算。需要注意的是,在 TensorFlow 中,RNN 表示通用意义上的循环神经网络,对于基础循环神经网络,它一般叫做 SimpleRNN。SimpleRNN 与SimpleRNNCell 的区别在于,带 C e l l \\mathrm{Cell} Cell 的层仅仅是完成了一个时间步的前向运算,不带 C e l l \\mathrm{Cell} Cell 的层一般是基于 Cell 层实现的,它在内部已经完成了多个时间步的循环运算,因此使用起来更为方便快捷。
我们先介绍 SimpleRNNCell 的使用方法,再介绍 SimpleRNN层的使用方法。
from __future__ import absolute_import, division, print_function, unicode_literals
import collections
import matplotlib.pyplot as plt
import numpy as np
import os
import tensorflow as tf
from tensorflow.keras import layers, losses, optimizers, Sequential
from tensorflow import keras
SimpleRNNCell
以某输入特征长度 n = 4 n=4 n=4, Cell 隐藏向量特征长度 h = 3 h=3 h=3 为例,首先我们新建一个 SimpleRNNCell,不需要指定序列长度s,代码如下:
cell = layers.SimpleRNNCell(3) # 创建 RNN Cell,隐藏向量长度为 3
cell.build(input_shape=(None,4)) # 输出特征长度 n=4
cell.trainable_variables #打印 U, W, b 张量
[<tf.Variable 'kernel:0' shape=(4, 3) dtype=float32, numpy=
array([[-0.26086324, -0.0585714 , -0.85844785],
[-0.6060221 , 0.06823266, 0.73086584],
[-0.18024814, 0.60274863, -0.17478985],
[-0.04281795, -0.24752945, -0.18977386]], dtype=float32)>,
<tf.Variable 'recurrent_kernel:0' shape=(3, 3) dtype=float32, numpy=
array([[ 0.5762849 , -0.7962839 , 0.1839226 ],
[-0.6610643 , -0.3218719 , 0.67778486],
[-0.4805097 , -0.5121819 , -0.7118849 ]], dtype=float32)>,
<tf.Variable 'bias:0' shape=(3,) dtype=float32, numpy=array([0., 0., 0.], dtype=float32)>]
可以看到,SimpleRNNCell 内部维护了 3 个张量,kernel 变量即 U U U 张量,recurrent_kernel 变量即 W \\boldsymbol{W} W 张量,bias 变量即偏置 b \\boldsymbol{b} b 向量。但是 R N N \\mathrm{RNN} RNN 的 隐藏 状态向量 h h h并不由SimpleRNNCell 维护,需要用户自行初始化向量 h 0 \\boldsymbol{h}_{0} h0 并记录每个时间唯上的 h t \\boldsymbol{h}_{t} ht 。
通过调用
C
e
l
l
\\mathrm{Cell}
Cell 实例即可完成前向运算:
y
t
,
[
h
t
]
=
Cell
(
x
t
,
[
h
t
−
1
]
)
\\Large \\color{green}{\\boldsymbol{y}_{t},\\left[\\boldsymbol{h}_{t}\\right]=\\operatorname{Cell}\\left(\\boldsymbol{x}_{t},\\left[\\boldsymbol{h}_{t-1}\\right]\\right)}
yt,[ht]=Cell(xt,[ht−1])
对于 SimpleRNNCell 来说,
y
t
=
h
t
y_{t}=\\boldsymbol{h}_{t}
yt=ht, 并没有经过额外的线性层转换,是同一个对象:
[
h
t
]
\\left[\\boldsymbol{h}_{t}\\right]
[ht] 通过一个 List 包裹起来,这么设置是为了与
L
S
T
M
\\mathrm{LSTM}
LSTM 、GRU 等
R
N
N
\\mathrm{RNN}
RNN 变种格式统一。在循环 神经网络的初始化阶段,状态向量
h
0
\\boldsymbol{h}_{0}
h0 一般初始化为全 0 向量,例如:
# 初始化状态向量,用列表包裹,统一格式
h0 = [tf.zeros([4, 64])]
x = tf.random.normal([4, 80, 100]) # 生成输入张量,4 个 80 单词的句子
xt = x[:, 0, :] # 所有句子的第 1 个单词
# 构建输入特征 n=100,序列长度 s=80,状态长度=64 的 Cell
cell = layers.SimpleRNNCell(64)
out, h1 = cell(xt, h0) # 前向计算
print(out.shape, h1[0].shape)
(4, 64) (4, 64)
可以看到经过一个时间戳的计算后,输出和状态张量的 shape
都为
[
𝑏
,
h
]
[𝑏, ℎ]
[b,h] ,打印出这两者的
i
d
id
id 如下:
print(id(out), id(h1[0]))
1936928150456 1936928150456
两者 i d id id一致,即状态向量直接作为输出向量。对于长度为 s s s的训练来说,需要循环通过Cell类 s s s次才算完成一次网络层的前向运算。例如:
h = h0 # h 保存每个时间戳上的状态向量列表
# 在序列长度的维度解开输入,得到 xt:[b,n]
for xt in tf.unstack(x, axis=1):
out, h = cell(xt, h) # 前向计算,out 和 h 均被覆盖
# 最终输出可以聚合每个时间戳上的输出,也可以只取最后时间戳的输出
out = out
最后一个时间戳的输出变量 out 将作为网络的最终输出。实际上,也可以将每个时间戳上的输出保存,然后求和或者均值,将其作为网络的最终输出。
多层 SimpleRNNCell 网络
和卷积神经网络一样,循环神经网络虽然在时间轴上面展开了多次,但只能算一个网络层。通过在深度方向堆叠多个 Cell 类来实现深层卷积神经网络一样的效果,大大的提升网络的表达能力。但是和卷积神经网络动辄几十、上百的深度层数来比,循环神经网络很容易出现梯度弥散和梯度爆炸到现象,深层的循环神经网络训练起来非常困难,目前常见的循环神经网络模型层数一般控制在十层以内。
我们这里以两层的循环神经网络为例,介绍利用 Cell 方式构建多层 RNN 网络。首先新建两个 SimpleRNNCell 单元,代码如下:
x = tf.random.normal([4, 80, 100])
xt = x[:, 0, :] # 取第一个时间戳的输入 x0
# 构建 2 个 Cell,先 cell0,后 cell1,内存状态向量长度都为 64
cell0 = layers.SimpleRNNCell(64)
cell1 = layers.SimpleRNNCell(64)
h0 = [tf.zeros([4, 64])] # cell0 的初始状态向量
h1 = [tf.zeros([4, 64])] # cell1 的初始状态向量
out0, h0 = cell0(xt, h0)
out1, h1 = cell1(out0, h1)
在时间轴上面循环计算多次来实现整个网络的前向运算,每个时间戳上的输入 xt 首先通过第一层,得到输出 out0,再通过第二层,得到输出 out1,代码如下:
for xt in tf.unstack(x, axis=1):
# xt 作为输入,输出为out0
out0, h0 = cell0(xt, h0)
# 上一个cell的输出out0作为本cell的输入
out1, h1 = cell1(out0, h1)
上述方式先完成一个时间戳上的输入在所有层上的传播,再循环计算完所有时间戳上的输入。
但是大多数网络是分层进行训练的,即先完成输入在第一层上所有时间戳的计算,并保存第一层在所有时间戳上的输出列表,再计算第二层、第三层等的传播。代码如下:
# 保存上一层的所有时间戳上面的输出
middle_sequences = []
# 计算第一层的所有时间戳上的输出,并保存
for xt in tf.unstack(x, axis=1):
out0, h0 = cell0(xt, h0)
middle_sequences.append(out0)
# 计算第二层的所有时间戳上的输出
# 如果不是末层,需要保存所有时间戳上面的输出
for xt in middle_sequences:
out1, h1 = cell1(xt, h1)
使用这种方式的话,我们需要一个额外的 List 来保存上一层所有时间戳上面的状态信息:middle_sequences.append(out0)
。这两种方式效果相同,可以根据个人喜好选择编程风格。
需要注意的是,循环神经网络的每一层、每一个时间戳上面均有状态输出,那么对于后续任务来说,我们应该收集哪些状态输出最有效呢?一般来说,最末层 Cell 的状态有可能保存了高层的全局语义特征,因此一般使用最末层的输出作为后续任务网络的输入。更特别地,每层最后一个时间戳上的状态输出包含了整个序列的全局信息,如果只希望选用一个状态变量来完成后续任务,比如情感分类问题,一般选用最末层、最末时间戳的状态输出最为合适。
SimpleRNN 层
通过 SimpleRNNCell 层的使用,我们可以非常深入地理解循环神经网络前向运算的每个细节,但是在实际使用中,为了简便,不希望手动参与循环神经网络内部的计算过程,比如每一层的 状态向量的初始化,以及每一层在时间轴上展开的运算。通过 SimpleRNN层高层接口可以非常方便地帮助我们实现此目的。
比如我们要完成单层循环神经网络的前向运算,可以方便地实现如下:
layer = layers.SimpleRNN(64) # 创建状态向量长度为 64 的 SimpleRNN 层
x = tf.random.normal([4, 80, 100])
out = layer(x) # 和普通卷积网络一样,一行代码即可获得输出
out.shape
TensorShape([4, 64])
可以看到,通过 SimpleRNN 可以仅需一行代码即可完成整个前向运算过程,它默认返回最后一个时间戳上的输出。
如果希望返回所有时间戳上的输出列表,可以设置 return_sequences=True 参数,代码如下:
layer = layers.SimpleRNN(64, return_sequences=True)
out = layer(x)
out
<tf.Tensor: shape=(4, 80, 64), dtype=float32, numpy=
array([[[-0.7640995 , 0.29114813, -0.9395353 , ..., 0.7998332 ,
-0.8744245 , 0.05270826],
[-0.81423575, -0.80543303, 0.9387598 , ..., 0.72762984,
0.77148765, 0.66728926],
[-0.3563845 , 0.36993226, -0.98361504, ..., -0.14953893,
0.8894063 , 0.95787084],
...,
[-0.18223877, -0.08161224, 0.842399 , ..., 0.02058126,
0.9928785 , -0.33439025],
[ 0.62462914, -0.8390593 , 0.4640974 , ..., 0.6130682 ,
-0.80750656, -0.60346556],
[-0.37366417, -0.91101676, -0.8795699 , ..., -0.41510412,
-0.6854079 , -0.75581497]],
[[-0.5133586 , -0.8291532 , 0.9720653 , ..., -0.09188052,
0.05910752, -0.97738445],
[ 0.3955006 , -0.53642815, 0.69030464, ..., 0.39206386,
0.38519725, 0.23031585],
[ 0.5698126 , 0.8235152 , -0.9686071 , ..., -0.9065343 ,
0.965824 , 0.49484816],
...,
[-0.08619698, 0.95371157, -0.9168741 , ..., 0.72534055,
0.81172985, 0.99546957],
[ 0.9254669 , -0.7651368 , 0.99713796, ..., -0.13967742,
-0.26898682, 0.003392 ],
[-0.15479407, 0.95709383, -0.95395446, ..., -0.9576438 ,
0.99853796, -0.88316303]],
[[ 0.87729627, -0.6626161 , 0.26615268, ..., 0.14300938,
0.25743997, -0.7351425 ],
[ 0.5041899 , 0.08617769, 0.00582886, ..., -0.527421 ,
0.9809544 , 0.01493072],
[ 0.8543621 , -0.03570254, 0.71524394, ..., -0.7192558 ,
-0.92690545, -0.5497534 ],
...,
[ 0.47667795, 0.77006406, -0.8959127 , ..., -0.82292753,
-0.50869304, -0.70581955],
[ 0.43012512, -0.23496862, -0.08516458, ..., -0.4649777 ,
-0.9621672 , -0.4879089 ],
[-0.4852713 , 0.49230498, -0.21436137, ..., 0.44929373,
-0.1753316 , -0.9875434 ]],
[[-0.7808034 , -0.65544915, -0.49642462, ..., 0.3983099 ,
0.7190488 , 0.90666705],
[-0.3593413 , -0.85854506, -0.85261035, ..., 0.23371501,
-0.72413915, 0.21626596],
[ 0.6597075 , 0.98487127, 0.5982329 , ..., -0.87499446,
-0.99579674, 0.23364304],
...,
[ 0.510861 , 0.9102124 , -0.02208892, ..., 0.7702835 ,
-0.824872 , -0.99829495],
[-0.34481174, -0.48414457, 0.822184 , ..., -0.47846287,
0.8387454 , 0.54365987],
[-0.58193165, -0.3135157 , -0.8939082 , ..., 0.55660015,
0.97141373, 0.2149008 ]]], dtype=float32)>
可以看到,返回的输出张量 shape 为[4,80,64],中间维度的 80 即为时间戳维度。同样的,对于多层循环神经网络,我们可以通过堆叠多个 SimpleRNN 实现,如两层的网络,用法和普通的网络类似。例如
net = tf.keras.Sequential([ # 构建2层RNN网络
# 除最末层外,都需要返回所有时间戳的输出
layers.SimpleRNN(64, return_sequences=True),
layers.SimpleRNN(64),
])
out = net(x) # 前向计算
net.summary()
Model: "sequential_1"
_________________________________________________________________
Layer (type) Output Shape Param #
=================================================================
simple_rnn_4 (SimpleRNN) multiple 10560
_________________________________________________________________
simple_rnn_5 (SimpleRNN) multiple 8256
=================================================================
Total params: 18,816
Trainable params: 18,816
Non-trainable params: 0
_________________________________________________________________
每层都需要上一层在每个时间戳上面的状态输出,因此除了最末层以外,所有的 RNN 层都需要返回每个时间戳上面的状态输出,通过设置 return_sequences=True 来实现。可以看到,使用 SimpleRNN 层,与卷积神经网络的用法类似,非常简洁和高效。
RNN 情感分类问题实战
现在利用 RNN 网络来挑战情感分类问题。网络结构如图 1 1 1 所示,RNN 网络共两层,循环提取序列信号的语义特征,利用第 2 层 R N N \\mathrm{RNN} RNN 层的最后时间唯的状态向量 h s ( 2 ) \\boldsymbol{h}_{s}^{(2)} hs(2) 作为句子的全局语义特征表示,送入全连接层构成的分类网络 3,得到样本 x x x 为积极情感的概率 P ( x \\mathrm{P}(x P(x 为积极情感 ∣ x ) ∈ [ 0 , 1 ] \\mid x) \\in[0,1] ∣x)∈[0,1] 。
图
1
图1
图1
数据集
这里使用经典的 IMDB 影评数据集来完成情感分类任务。IMDB 影评数据集包含了50000 条用户评价,评价的标签分为消极和积极,其中 IMDB 评级<5 的用户评价标注为0,即消极;IMDB 评价>=7 的用户评价标注为 1,即积极。25000 条影评用于训练集,25,000 条用于测试集。
通过 tensorflow 提供的数据集 工具即可加载 IMDB 数据集,代码如下:
import tensorflow_datasets as tfds
# 通过调用tfds.load,我们将获得imdb_reviews数据集的数据
dataset, info = tfds.load('imdb_reviews', with_info=True, as_supervised=True)
# 其中25,000个样本用于训练,25,000个样本用于测试
train_dataset, test_dataset = dataset['train'], dataset['test']
# 训练数据和测试数据均包含了25,000个句子和相应的标签.我们需要采用分词器和padding工具来对句子做一些转换.首先我们定义一些列表,来存储训练数据和测试数据中的句子和标签
x_train = []
training_labels = []
x_test = []
testing_labels = []
# 现在我们遍历训练数据,将句子和标签放入列表中
for s, l in train_dataset:
x_train.append(str(s.numpy()))
training_labels.append(l.numpy())
for s, l in test_dataset:
x_test.append(str(s.numpy()))
testing_labels.append(l.numpy())
# 在训练时我们希望标签是NumPy数组的格式.所以我们需要通过以下的语句,对刚刚的标签列表进行转换
x_train = np.array(x_train)
x_test = np.array(x_test)
y_train = np.array(training_labels)
y_test = np.array(testing_labels)
# 打印输入的形状,标签的形状
print(x_train.shape, len(x_train[<以上是关于3.4RNN 层使用方法的主要内容,如果未能解决你的问题,请参考以下文章
如何在 Keras 中为循环神经网络 (RNN) 使用嵌入层
使用Tensorflow后端的Keras LSTM RNN中令人费解的训练损失与纪元...行为的任何原因
深度学习原理与框架-递归神经网络-RNN_exmaple(代码) 1.rnn.BasicLSTMCell(构造基本网络) 2.tf.nn.dynamic_rnn(执行rnn网络) 3.tf.expa