持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第8天,点击查看活动详情
前边刚说了encoder-decoder框架,也写了大体的代码思路。但是有思路不代表能写出东西来,今天就使用encoder-decoder来实现一个seq2seq。
import collections
import math
import torch
from torch import nn
from d2l import torch as d2l
常规操作导个包。
编码器
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 = nn.GRU(embed_size, num_hiddens, num_layers,dropout=dropout)
def forward(self, X, *args):
X = self.embedding(X)
X = X.permute(1, 0, 2)
output, state = self.rnn(X)
return output, state
定义一下seq2seq的encoder,在这里是继承了Encoder-Decoder框架 – 掘金 (juejin.cn)的encoder。
-
__init__
中先进行初始化,作为子类新增加了自己的embedding层,这里RNN层使用的是pytorch自带的GRU(门控循环单元)。 -
forward
定义前向传播:-
输出’X’的形状:(
batch_size
,num_steps
,embed_size
) 即第一维度是batch的大小,第二维度是时间步的长度,第三维度是embedding的大小。 -
第二句中,我们使用
permute
对X
的维度进行修改,即将其原来的前两个维度进行调换,改成时间步长度、批量大小、embedding大小。因为在循环神经网络模型中,第一个轴对应于时间步(pytorch内置的GRU的要求。当然可以修改的,但是那样写更麻烦,具体可以看文档GRU — PyTorch 1.11.0 documentation)。 -
第三步第四步是计算并返回encoder的隐状态。在这里需要注意一下,encoder是不需要输出的,他只需要将输入变为一个对应的矩阵即可。所以在这里所谓的
output
是所有时间步最后的隐状态,而state
是最后一个时间步隐状态。-
output
的形状: (num_steps
,batch_size
,num_hiddens
) -
state[0]
的形状: (num_layers
,batch_size
,num_hiddens
) -
这里解释一下为什么
output
叫最后的隐状态,因为可能涉及多层啊之类的,结果取得都是最后一层的,比如深度RNN,都是浅层向深层接续计算的。那为什么state
是最后时间步的隐状态,因为state是存储最后一个时间步所有的隐状态,有几层存几层。
-
-
encoder = Seq2SeqEncoder(vocab_size=10, embed_size=8, num_hiddens=16,num_layers=2)
encoder.eval()
X = torch.zeros((4, 7), dtype=torch.long)
output, state = encoder(X)
output.shape,state.shape
现在我们试一下子我们的encoder。
-
第一句设定默认的参数,假装我们预处理之后的字典长度为10,word embedding的大小为8,隐藏层大小为16,有俩层。
-
第二句是使用
eval()
将其转模式。这句不写也行的其实。 -
现在我们随便搞个X,假设大小为4*7的矩阵,用
torch.zeros
将其初始化为0。 -
然后我们取出来
output
和state
看一下他们俩的形状是啥。因为我是在jupyter notebook里写的,所以最后一句是个输出格式,不用print
,如果你们用别的IDE写的记得Print。
输出是:
(torch.Size([7, 4, 16]), torch.Size([2, 4, 16]))
自己对一下维度,是可以对上的。
解码器
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 = nn.GRU(embed_size + num_hiddens, num_hiddens, num_layers, dropout=dropout)
self.dense = nn.Linear(num_hiddens, vocab_size)
def init_state(self, enc_outputs, *args):
return enc_outputs[1]
def forward(self, X, state):
X = self.embedding(X).permute(1, 0, 2)
context = state[-1].repeat(X.shape[0], 1, 1)
X_and_context = torch.cat((X, context), 2)
output, state = self.rnn(X_and_context, state)
output = self.dense(output).permute(1, 0, 2)
return output, state
来看一下decoder部分。这个也是继承了上一篇文章里写的decoder。作为子类看看他都增加了什么新功能。
-
依旧从
__init__
开始说:-
这里也有个embedding,注意这里的embedding是decoder自己的,编码器解码器不共享词嵌入哦!
-
rnn这里依旧是使用GRU,但是注意这里第一项参数和编码器不一样了,这里是变成了
embed_size + num_hiddens
,这里待会看下边forward函数时候就懂了。 -
注意这里多了个
self.dense
,这个是decoder的输出。是真的输出!decoder是要做输出的!!!不要迷糊!
-
-
看一下
init_state
函数:注意这里参数列表写的是
enc_outputs
,应该能看出来是把encoder的outputs拿来做隐状态初始化的吧。
然后函数里是取enc_outputs[1]
,对照一下encoder,一共有两个输出,所有时间步最后一层的隐状态和最后一个时间步所有的隐状态。这里是取最后一个时间步的隐状态来进行初始化解码器的隐状态。
-
然后是
forward
函数。-
输出
X
的形状:(batch_size
,num_steps
,embed_size
),再对其进行维度的交换,将batch_size批量大小和num_steps时间步大小进行交换,使其符合pytorch内置GRU的参数要求。 -
第二步是取
state
的最后一项,对其广播生成context
,使其具有与X
相同的num_steps
。知不知道为什么取
state
的最后一项要用state[-1]
,因为我们有多少层是自己决定的,这里我们暂时是设置的2,所以虽然在这里例子里可以用state[1]
取得最后一项,但是如果换个层数就不成立了。 -
第三步
X_and_context
将解码器的输入X
和state
最后一个进行拼接。这里不是单纯的对其输入进行直接计算,而是结合编码器的结果进行计算。所以上边的GRU第一个参数变为了embed_size + num_hiddens
。 -
进行计算获得decoder的最后一层的所有隐状态和最后一个时间步的隐状态。
-
然后使用所有时间步最后的隐状态获得输出
dense
,并对其维度进行修改。-
output
的形状: (batch_size
,num_steps
,vocab_size
) -
state
的形状: (num_layers
,batch_size
,num_hiddens
)
-
-
最后将结果返回。
-
decoder = Seq2SeqDecoder(vocab_size=10, embed_size=8, num_hiddens=16,num_layers=2)
decoder.eval()
state = decoder.init_state(encoder(X))
output, state = decoder(X, state)
output.shape, state.shape
搞一下我们的decoder。
-
第一步传入参数。
-
第二步使用
.eval()
更改其模式。 -
第三步生成自己的初始隐状态。
-
第四步获取输出和最后一个时间步的所有的隐状态。
注意这里的
output
是真的输出,和encoder的性质不一样。 -
最后我们输出一下二者的维度,结果是:
(torch.Size([4, 7, 10]), torch.Size([2, 4, 16]))
mask
def sequence_mask(X, valid_len, value=0):
maxlen = X.size(1)
mask = torch.arange((maxlen), dtype=torch.float32,device=X.device)[None, :] < valid_len[:, None]
X[~mask] = value
return X
看一下这个mask函数,之前看过我其他文章的应该熟悉这个mask操作的。
-
先看传入的参数,
X
待mask的矩阵,valid_len
有效长度,需要传入一个向量,这个向量的长度要和X
的长度一样(不是样本长度,是几个样本)。value
默认是0,就是使用0进行mask,也可以更换为其他值。 -
maxlen
取X
的第一个维度,也就是有多少列。 -
初始化
mask
。这里对其进行了几步操作我们来分析一下。将长代码拆分开相当于两句。
-
mask = torch.arange((maxlen), dtype=torch.float32,device=X.device)
这里是先arange初始化mask向量。 -
mask = mask[None, :] < valid_len[:, None]
这个
[None,:]
有点类似于增加一个维度的torch.squeeze那种操作。回忆一下python的基础知识 :
-
a = b[:]
就是把b的所有内容复制给a, -
a = b[:,None]
就是给b增加一个维度,b原来的内容作为a的每行第一个元素。 -
a = b[None,:]
就是给b在前边增加一个维度,b原来的内容作为a的第一行。 -
在哪里增加None就是给哪里添加一个维度。
d = torch.arange(3) print(d) print(d[:,None]) print(d[None,:]) print(d[None,:,None])
输出为:
使用两个增加维度之后的矩阵作比较,得到
mask
,值的类型为布尔值。
-
-
-
之后对mask取反,之后使其等于value,也就是将不需要的值设置为0。
-
测试一下嗷
对于二维的:
X = torch.tensor([[1, 2, 3], [4, 5, 6]]) sequence_mask(X, torch.tensor([1, 2]))
对于三维的:
X = torch.ones(2, 3, 4) sequence_mask(X, torch.tensor([1, 2]), value=-1)
class MaskedSoftmaxCELoss(nn.CrossEntropyLoss):
def forward(self, pred, label, valid_len):
weights = torch.ones_like(label)
weights = sequence_mask(weights, valid_len)
self.reduction='none'
unweighted_loss = super().forward(pred.permute(0, 2, 1), label)
weighted_loss = (unweighted_loss * weights).mean(dim=1)
return weighted_loss
loss = MaskedSoftmaxCELoss()
好了上边那个mask函数其实就是为了搞一个带遮蔽效果的softmax。一些填充长度的值存在对softmax也没用,预测对了也没用,所以将其遮盖了。做法是搞一个参数weights
,保留的部分设置为1,其余部分设置为0。
pred
的形状:(batch_size
,num_steps
,vocab_size
)label
的形状:(batch_size
,num_steps
)valid_len
的形状:(batch_size
,)