3.4RNN 层使用方法

Posted 炫云云

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了3.4RNN 层使用方法相关的知识,希望对你有一定的参考价值。


3.1意境级讲解循环神经网络RNN_炫云云-CSDN博客

3.2 LSTM和GRU循环神经网络

在介绍完循环神经网络的算法原理之后,我们来学习如何在 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+Wht1+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,[ht1])
对于 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]

image-20210613170259163
图 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 层使用方法的主要内容,如果未能解决你的问题,请参考以下文章

是否可以使用 CNN 的扁平层的输出作为 RNN 的输入?

RNN模型层的输入是如何工作的?

如何在 Keras 中为循环神经网络 (RNN) 使用嵌入层

使用Tensorflow后端的Keras LSTM RNN中令人费解的训练损失与纪元...行为的任何原因

深度学习原理与框架-递归神经网络-RNN_exmaple(代码) 1.rnn.BasicLSTMCell(构造基本网络) 2.tf.nn.dynamic_rnn(执行rnn网络) 3.tf.expa

RNN:连接层