tensorflow2.0下对多层双向循环神经网络api的输出测试

问题背景 1、将单词的index序列输入embedding层编码成嵌入表示
2、将单词的嵌入序列输入由RNN构成的编码器进行编码
那么RNN编码器的输出的格式是怎么样的呢?在网上我们可以看到很多序列模型用到了双向的RNN,并堆叠了多层构成了多层的双向RNN。但是我们有时候也是需要中间层的状态的,通常的做法是需要另外构造一个model进行输出,这显然是不自由的。
所以这次我们自己直接构造一个多层双向的RNN来检测他的输出结果到底是什么。这次测试针对的版本是tensorflow2.0,由于2.0版本的eager计算方式和自动图更新,所以下面都采用面向对象来编程。
1 导包

import numpy as np import tensorflow as tf import tensorflow.keras as keras import tensorflow.keras.layers as layerstf.__version__ '2.2.0'

需要编码的句子
word_id = tf.convert_to_tensor([[1, 2, 0], [1, 0, 0]], dtype=tf.int64)

2 构造嵌入层
class Embedding(keras.Model): def __init__(self, input_size, output_size, weights=None): super(Embedding, self).__init__() if weights is not None: self.embedding = layers.Embedding(input_size, output_size, embeddings_initializer=keras.initializers.constant(weights), mask_zero=True) else: self.embedding = layers.Embedding(input_size, output_size, mask_zero=True)def call(self, x):# [batch, len] return self.embedding(x)# [batch, len, output_size]

这个嵌入层类主要封装了layers.Embedding(input_size, output_size, embeddings_initializer=keras.initializers.constant(weights), mask_zero=True),这个api涉及的几个参数:
  1. 第一个参数为词汇表的维度
  2. 第二个参数为词嵌入维度
  3. embeddings_initializer为权值初始化函数,如果有预训练的词嵌入,可以通过这个传入
  4. mask_zero这个参数实现了对index=0的单词的mask,我们通常把pad的符号设置为词汇表中的index=0,于是它产生一个mask并向后传递,在RNN中防止对句子中多余的pad符号进行解码。在tensorflow1.x中是通过在tf.nn.dynamic_rnn()这个api中传入一个encoder_len实现的,在pytorch中torch.nn.utils.rnn.pack_padded_sequence也起到了相同的作用。这个mask只会在RNN编码时起作用,并不会把pad的词嵌入变成全0,原来是什么就是什么。
# 嵌入层测试 weights = np.array([[1, 2, 3, 4, 5], [2, 3, 4, 5, 6], [3, 4, 5, 6, 7]], dtype=np.float64) embedding = Embedding(3, 5, weights)# (num_vocab, embedding_size) word_embed = embedding(word_id)# [batch, seq, embedding_size]word_embed

3 构造编码器
class Encoder(keras.Model): def __init__(self, rnn_type,# rnn类型 input_size, output_size, num_layers,# rnn层数 bidirectional=False, return_sequences=True): super(Encoder, self).__init__() assert rnn_type in ['GRU', 'LSTM'] if bidirectional: assert output_size % 2 == 0 if bidirectional: self.num_directions = 2 else: self.num_directions = 1 units = int(output_size / self.num_directions) if rnn_type == 'GRU': rnnCell = [getattr(keras.layers, 'GRUCell')(units) for _ in range(num_layers)] else: rnnCell = [getattr(keras.layers, 'LSTMCell')(units) for _ in range(num_layers)] self.rnn = keras.layers.RNN(rnnCell, input_shape=(None, None, input_size), return_sequences=return_sequences, return_state=True) self.rnn_type = rnn_type self.num_layers = num_layers if bidirectional: self.rnn = keras.layers.Bidirectional(self.rnn, merge_mode='concat') self.bidirectional = bidirectional self.return_sequences = return_sequencesdef call(self, x):# [batch, timesteps, input_dim] return self.rnn(x)

构造方法:
  1. 通过rnnCell = [getattr(layers, 'GRUCell')(units) for _ in range(num_layers)]rnnCell = [getattr(layers, 'LSTMCell')(units) for _ in range(num_layers)]获得num_layers层RNN单元列表
  2. 通过rnn = layers.RNN(rnnCell, input_shape=(None, None, input_size), return_sequences=True, return_state=True)传入RNN单元列表构造一个多层的RNN,return_sequences=True代表输出每个时间步的输出,而不是最后一个时间步的输出,return_state=True代表返回RNN状态,False的话就不返回状态了。
  3. 通过rnn = layers.Bidirectional(self.rnn, merge_mode='concat')加上双向的装饰器,merge_mode='concat'代表通过拼接方式产生输出
下面开始对输出进行测试,对这部分没兴趣的可以直接看第6部分的结论。
4 实验参数设置
# num_vocab = 3 # embedding_size = 5 # batch_size = 2 # encoder_len = 3 # num_units = 10 # num_layers = 2

5 实验结果 单向多层GRU return_sequences=True
encoder_gru_seq = Encoder('GRU', 5, 10, 2, False, True) encoder_gru_seq(word_embed) [, , ]

输出包含3部分:
  • outputs[0]: shape=(2, 3, 10) (batch_size, encoder_len, num_units), 每个时间步的输出
  • outputs[1]: shape=(2, 10) (batch_size, num_units), 第一层的状态
  • outputs[2]: shape=(2, 10) (batch_size, num_units), 第二层的状态, 这里因为只有两层, 所以是每个样本最后一个字符的输出
  • outputs[N]: 如果有第N层, 则为第N层的状态
通过查看output[0]的数据我们也能发现受到嵌入层mask的作用,pad部分的编码结果和句子结束时状态是一样的,只是向后复制了。
return_sequences=True
encoder_gru = Encoder('GRU', 5, 10, 2, False, False) encoder_gru(word_embed) [, , ]

输出包含3部分
  • outputs[0]: shape=(2, 10) (batch_size, encoder_len, num_units), 最后一个
    时间步的输出
  • outputs[1]: shape=(2, 10) (batch_size, num_units), 第一层的状态
  • outputs[2]: shape=(2, 10) (batch_size, num_units), 第二层的状态, 这里因为只有两层, 所以是每个样本最后一个字符的输出, 和outputs[0]一致.
  • outputs[N]: 如果有第N层, 则为第N层的状态
双向多层GRU return_sequences=True
encoder_bigru_seq = Encoder('GRU', 5, 10, 2, True, True) encoder_bigru_seq(word_embed) [, , , , ]

输出包含5部分:
  • outputs[0]: shape=(2, 3, 10) (batch_size, encoder_len, num_units), 每个时间步的输出
  • outputs[1]: shape=(2, 5) (batch_size, num_units//2), 正向第1层的状态
  • outputs[2]: shape=(2, 5) (batch_size, num_units//2), 正向第2层的状态
  • outputs[3]: shape=(2, 5) (batch_size, num_units//2), 反向第1层的状态
  • outputs[4]: shape=(2, 5) (batch_size, num_units//2), 反向第2层的状态
先正向从第1层到最后1层, 然后再反向. 另外需要注意的是, 从结果来看正向第2层的状态和反向第2层的状态的拼接和输出还是存在略微的差异?
return_sequences=False
encoder_bigru = Encoder('GRU', 5, 10, 2, True, False) encoder_bigru(word_embed) [, , , , ]

单向多层LSTM return_sequences=True
encoder_lstm_seq = Encoder('LSTM', 5, 10, 2, False, True) encoder_lstm_seq(word_embed) [, [, ], [, ]]

输出包含3部分:
  • outputs[0]: shape=(2, 3, 10) (batch_size, encoder_len, num_units), 每个时间步的输出
  • outputs[1]: shape=[(2, 10), (2, 10)] [(batch_size, num_units), (batch_size, num_units)], 正向第1层的状态(h, c), h为输出(短时记忆), c为lstm的长时记忆
  • outputs[2]: shape=[(2, 10), (2, 10)] [(batch_size, num_units), (batch_size, num_units)], 正向第2层的状态(h, c), h为输出(短时记忆), c为lstm的长时记忆, 这里的h也是句子最后一个单词的输出.
return_sequences=False
encoder_lstm = Encoder('LSTM', 5, 10, 2, False, False) encoder_lstm(word_embed) [, [, ], [, ]]

输出包含3部分:
  • outputs[0]: shape=(2, 10) (batch_size, encoder_len, num_units), 最后1个时间步的输出
  • 【tensorflow2.0下对多层双向循环神经网络api的输出测试】outputs[1]: shape=[(2, 10), (2, 10)] [(batch_size, num_units), (batch_size, num_units)], 正向第1层的状态(h, c), h为输出(短时记忆), c为lstm的长时记忆
  • outputs[2]: shape=[(2, 10), (2, 10)] [(batch_size, num_units), (batch_size, num_units)], 正向第2层的状态(h, c), h为输出(短时记忆), c为lstm的长时记忆, 这里的h也是句子最后一个单词的输出.
双向多层LSTM return_sequences=True
encoder_bilstm_seq = Encoder('LSTM', 5, 10, 2, True, True) encoder_bilstm_seq(word_embed) [, [, ], [, ], [, ], [, ]]

输出包含5部分:
  • outputs[0]: shape=(2, 3, 10) (batch_size, encoder_len, num_units), 最后1个时间步的输出
  • outputs[1]: shape=[(2, 5), (2, 5)] [(batch_size, num_units//2), (batch_size, num_units//2)], 正向第1层的状态(h, c), h为输出(短时记忆), c为lstm的长时记忆
  • outputs[2]: shape=[(2, 5), (2, 5)] [(batch_size, num_units//2), (batch_size, num_units//2)], 正向第2层的状态(h, c), h为输出(短时记忆), c为lstm的长时记忆
  • outputs[3]: shape=[(2, 5), (2, 5)] [(batch_size, num_units//2), (batch_size, num_units//2)], 反向第1层的状态(h, c), h为输出(短时记忆), c为lstm的长时记忆
  • outputs[4]: shape=[(2, 5), (2, 5)] [(batch_size, num_units//2), (batch_size, num_units//2)], 反向第2层的状态(h, c), h为输出(短时记忆), c为lstm的长时记忆
先正向从第1层到最后1层, 然后再反向. 另外需要注意的是, 从结果来看正向第2层的状态的h和反向第2层的状态的h拼接和输出还是存在略微的差异?
return_sequences=False
encoder_bilstm = Encoder('LSTM', 5, 10, 2, True, False) encoder_bilstm(word_embed) [, [, ], [, ], [, ], [, ]]

输出包含5部分:
  • outputs[0]: shape=(2, 3, 10) (batch_size, encoder_len, num_units), 最后1个时间步的输出
  • outputs[1]: shape=[(2, 5), (2, 5)] [(batch_size, num_units//2), (batch_size, num_units//2)], 正向第1层的状态(h, c), h为输出(短时记忆), c为lstm的长时记忆
  • outputs[2]: shape=[(2, 5), (2, 5)] [(batch_size, num_units//2), (batch_size, num_units//2)], 正向第2层的状态(h, c), h为输出(短时记忆), c为lstm的长时记忆
  • outputs[3]: shape=[(2, 5), (2, 5)] [(batch_size, num_units//2), (batch_size, num_units//2)], 反向第1层的状态(h, c), h为输出(短时记忆), c为lstm的长时记忆
  • outputs[4]: shape=[(2, 5), (2, 5)] [(batch_size, num_units//2), (batch_size, num_units//2)], 反向第2层的状态(h, c), h为输出(短时记忆), c为lstm的长时记忆
先正向从第1层到最后1层, 然后再反向. 另外需要注意的是, 从结果来看正向第2层的状态h和反向第2层的状态h的拼接和输出是完全一致的, 这里和return_sequences=True的情况不一样
结论
  1. outputs[0] 在return_sequences=True时是每个时间步的输出, 在return_sequences=False时是最后1个时间步的输出.
  2. 其他状态的输出(outputs[1-N])按先正向1到最后1层排序, 如果有反向再按反向1到最后一层排序.
  3. 如果是lstm, 每层状态是[h, c](短时记忆/输出, 长时记忆)
  4. 如果return_sequences=True, 输出的状态正向和反向不会拼接(在merge_mode='concat'设置下), 如果return_sequences=False, 输出的状态正向和反向会直接拼接好.
  5. 当然如果return_state=False, 除了输出就没有其他东西了.

    推荐阅读