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 layers

tf.__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_sequences


    def 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个时间步的输出

  • 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, 除了输出就没有其他东西了.

你可能感兴趣的:(tensorflow2.0下对多层双向循环神经网络api的输出测试)