机器学习笔记:序列到序列学习[详细解释]

介绍

本节我们使用两个循环神经网络的编码器和解码器, 并将其应用于序列到序列(sequence to sequence,seq2seq)类的学习任务。遵循编码器-解码器架构的设计原则, 循环神经网络编码器使用长度可变的序列作为输入, 将其转换为固定形状的隐状态。 换言之,输入序列的信息被编码到循环神经网络编码器的隐状态中。

结构

首先,我们使用上一节提到的编码器-解码器结构,其中编码器使用一个双隐层的门控循环单元构成的循环神经网络(链接均为我之前发布的博客笔记,seq2seq是基于之前这几节的内容的)。而解码器使用一个双隐层的门控循环单元构成的循环神经网络,后接一个全连接层。

编码器作用

编码器通过循环神经网络,将每个时间步的输入X和上一时间步的隐藏状态进行处理生成下一时间步的隐状态,即H_t=f(X,H_{t-1})。之后再通过编码操作把每个时间步的隐状态转化为上下文变量c,即c=q(h_1,h_2,...,h_T)

解码器作用

解码器通过先前的输出序列和上下文变量c共同决定当前时间步输出,概率为P(y_t|y_1,y_2,...,y_{t-1},c),解码器隐状态的更新操作为s_t=g(s_{t-1},y_{t-1},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

解码器的实现

上下文变量c与输入y_t进行拼接(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]才是真正的状态,但我没有想到这是哪个操作进行的。

  • 20
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值