问题背景
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涉及的几个参数:
- 第一个参数为词汇表的维度
- 第二个参数为词嵌入维度
- embeddings_initializer为权值初始化函数,如果有预训练的词嵌入,可以通过这个传入
- 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)
构造方法:
- 通过
rnnCell = [getattr(layers, 'GRUCell')(units) for _ in range(num_layers)]
或rnnCell = [getattr(layers, 'LSTMCell')(units) for _ in range(num_layers)]
获得num_layers
层RNN单元列表 - 通过
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的话就不返回状态了。 - 通过
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
的情况不一样
结论
- outputs[0] 在
return_sequences=True
时是每个时间步的输出, 在return_sequences=False
时是最后1个时间步的输出. - 其他状态的输出(outputs[1-N])按先正向1到最后1层排序, 如果有反向再按反向1到最后一层排序.
- 如果是lstm, 每层状态是[h, c](短时记忆/输出, 长时记忆)
- 如果
return_sequences=True
, 输出的状态正向和反向不会拼接(在merge_mode='concat'
设置下), 如果return_sequences=False
, 输出的状态正向和反向会直接拼接好. - 当然如果
return_state=False
, 除了输出就没有其他东西了.