介绍
本节我们使用两个循环神经网络的编码器和解码器, 并将其应用于序列到序列(sequence to sequence,seq2seq)类的学习任务。遵循编码器-解码器架构的设计原则, 循环神经网络编码器使用长度可变的序列作为输入, 将其转换为固定形状的隐状态。 换言之,输入序列的信息被编码到循环神经网络编码器的隐状态中。
结构
首先,我们使用上一节提到的编码器-解码器结构,其中编码器使用一个双隐层的门控循环单元构成的循环神经网络(链接均为我之前发布的博客笔记,seq2seq是基于之前这几节的内容的)。而解码器使用一个双隐层的门控循环单元构成的循环神经网络,后接一个全连接层。
编码器作用
编码器通过循环神经网络,将每个时间步的输入X和上一时间步的隐藏状态进行处理生成下一时间步的隐状态,即。之后再通过编码操作把每个时间步的隐状态转化为上下文变量,即。
解码器作用
解码器通过先前的输出序列和上下文变量c共同决定当前时间步输出,概率为,解码器隐状态的更新操作为。
代码实现
引入库
import collections
import math
from mxnet import autograd, gluon, init, np, npx
from mxnet.gluon import nn, rnn
from d2l import mxnet as d2l
npx.set_np()
编码器的实现
对编码器的代码进行解释:在对数据集进行处理后,形成的是一个三维的数据,size=(batch_size,num_steps,vocab_size),经过嵌入层的处理后,转换为size=(batch_size,num_steps,embed_size)。之后对第一维度和第二维度进行交换,使得size=(num_steps,batch_size,embed_size),在之前的RNN中我们已经知道第一维度应该是时间步数(这样RNN可以沿着时间步继续走下去),通过维度转换,使得第一维度成为时间步,需要注意的是此时不能使用X.T直接进行转置,因为我们只希望转换前两个维度,并不希望对整个矩阵进行变换。接下来的操作与RNN一致,获得RNN的输出与状态(需要注意的是,此时的RNN是一个GRU)。
class Seq2SeqEncoder(d2l.Encoder):
def __init__(self, vocab_size, embed_size, num_hiddens, num_layers,
dropout=0, **kwargs):
super(Seq2SeqEncoder, self).__init__(**kwargs)
# 嵌入层
self.embedding = nn.Embedding(vocab_size, embed_size)
self.rnn = rnn.GRU(num_hiddens, num_layers, dropout=dropout)
def forward(self, X, *args):
# 输出'X'的形状:(batch_size,num_steps,embed_size)
X = self.embedding(X)
# 在循环神经网络模型中,第一个轴对应于时间步
X = X.swapaxes(0, 1)
state = self.rnn.begin_state(batch_size=X.shape[1], ctx=X.ctx)
output, state = self.rnn(X, state)
# output的形状:(num_steps,batch_size,num_hiddens)
# state的形状:(num_layers,batch_size,num_hiddens)
return output, state
解码器的实现
上下文变量与输入进行拼接(concatenate)操作,使得每一个时间步读取对应的上下文变量和输入。解码器使用一个全连接层进行Softmax运算产生输出。
需要注意的是,在编码器的返回变量中,output和state共同构成一个元组。在init_state()中,通过对编码器的输出索引第二个元素得到所需要的状态state。在前向计算中,获得最后一个层的状态,作为上下文变量,将上下文变量context与输入进行连接,一起输入到门控循环单元GRU中。
class Seq2SeqDecoder(d2l.Decoder):
def __init__(self, vocab_size, embed_size, num_hiddens, num_layers,
dropout=0, **kwargs):
super(Seq2SeqDecoder, self).__init__(**kwargs)
self.embedding = nn.Embedding(vocab_size, embed_size)
self.rnn = rnn.GRU(num_hiddens, num_layers, dropout=dropout)
self.dense = nn.Dense(vocab_size, flatten=False)
def init_state(self, enc_outputs, *args):
return enc_outputs[1]
def forward(self, X, state):
# 输出'X'的形状:(batch_size,num_steps,embed_size)
X = self.embedding(X).swapaxes(0, 1)
# context的形状:(batch_size,num_hiddens)
context = state[0][-1]
# 广播context,使其具有与X相同的num_steps
context = np.broadcast_to(context, (
X.shape[0], context.shape[0], context.shape[1]))
X_and_context = np.concatenate((X, context), 2)
output, state = self.rnn(X_and_context, state)
output = self.dense(output).swapaxes(0, 1)
# output的形状:(batch_size,num_steps,vocab_size)
# state的形状:(num_layers,batch_size,num_hiddens)
return output, state
实例化解码器测试输出大小
decoder = Seq2SeqDecoder(vocab_size=10, embed_size=8, num_hiddens=16,
num_layers=2)
decoder.initialize()
state = decoder.init_state(encoder(X))
output, state = decoder(X, state)
output.shape, len(state), state[0].shape
((4, 7, 10), 1, (2, 4, 16))
疑问
其实我这里有一个地方感到不解,在解码器解码时,之前编码器的state应为一个三维数组,怎么会索引[0][-1]之后出现一个二维数组?难道哪里把它封装成元组了吗?
应该的确是这样,因为根据测试输出大小时输出的state长度为1,说明解码器对state直接进行了包装,使得state[0]才是真正的状态,但我没有想到这是哪个操作进行的。