为了小论文之跟着李沐学AI(二十)

Encoder和Decoder

其实现在发现其实LSTM,GRU就是一个Encoder和Decoder的例子。我们把最后的全链接层想象成解码前,前面的RNN网络想象成编码器。是不是有那味儿了。

class Encoder(nn.Module):
    """编码器-解码器结构的基本编码器接口。"""
    def __init__(self, **kwargs):
        super(Encoder, self).__init__(**kwargs)

    def forward(self, X, *args):
        raise NotImplementedError
    #编码器你看,其实跟之前的网络是相似的,没什么太大的差别,给一个X输出一个东西  

Encoder,其实乍一看,你会发现和我们之前的网络结构没有什么本质上的区别

class Decoder(nn.Module):
    """编码器-解码器结构的基本解码器接口。"""
    def __init__(self, **kwargs):
        super(Decoder, self).__init__(**kwargs)

    def init_state(self, enc_outputs, *args):
        raise NotImplementedError

    def forward(self, X, state):
        raise NotImplementedError

# 解码器就有点不同了,首先他会根据encoder传过来的输入,更新自己的状态
# 其次,他也有自己的输入X,这个就是自己很奇妙的地方,就是他有自己的输入

Decoder也只是略有不同。首先,他需要初始化一个隐藏状态,这个隐藏状态是来自于Encoder的输出的。其次,对于Deocder,他是有一个自己的输入的。

class EncoderDecoder(nn.Module):
  def __init__(self,encoder,decoder,**kwargs):
    super(EncoderDecoder,self).__init__(**kwargs)
    self.encoder = encoder
    self.decoder = decoder
  
  def forward(self,enc_X,dec_X,*args):
    enc_outputs = self.encoder(enc_X,*args)
    dec_state = self.decoder.init_state(enc_outputs,*args)
    return self.decoder(dec_X,dec_state)

再写一个类包装它的话,就是这样。

现在我们真正来实现一个Seq2Seq的模型

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'的形状:(`batch_size`, `num_steps`, `embed_size`)
        X = self.embedding(X)
        # 在循环神经网络模型中,第一个轴对应于时间步
        X = X.permute(1, 0, 2)
        # 如果未提及状态,则默认为0
        output, state = self.rnn(X)
        # `output`的形状: (`num_steps`, `batch_size`, `num_hiddens`)
        # `state[0]`的形状: (`num_layers`, `batch_size`, `num_hiddens`)
        return output, state

在这个Encoder部分。首先我们会用nn.Embedding去嵌入我们的输入
再用一个rnn去获得我们的输出
记得我们的X输入,需要调整它的维度,让时间步在前面

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'的形状:(`batch_size`, `num_steps`, `embed_size`)
        X = self.embedding(X).permute(1, 0, 2)
        # 广播`context`,使其具有与`X`相同的`num_steps`
        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)
        # `output`的形状: (`batch_size`, `num_steps`, `vocab_size`)
        # `state[0]`的形状: (`num_layers`, `batch_size`, `num_hiddens`)
        return output, state

其实真正有意思的代码都在decoder部分,来看我分析。
首先我们要明确,H是一个声明东西,之前我误解的把他理解为一个 hiddenshiddens的矩阵,不是这样的,它是一个batch_size * hiddens的矩阵。对应到每一个batch(可以理解为每一句话),都是一个向量。这个向量怎么来的。看我分析,首先对于我们的输入
比如 4 * 7,4表示batch_size 7表示表示timestep
首先我们会进行一个embedding ,假设我们的embedding层是一个 vocab_size为10,embed_size为8的一个层。
那么它就会变成4 * 7 8的一个3d矩阵。
如果我们要把这个东西输入到rnn的网络中
我们会先进行维度的置换,因为我们想要的是每一个时间步的迭代更新,而不是,一个batch_size的迭代更新对吗
变成一个7
4
8的矩阵后,在一个rnn模型中(不是GRU或者LSTM)
会有Whh 一个 embedding * hiddens的矩阵(或略Bias) 在这里就是一个816(假设hiddens是16)的。经过点乘变成一个(每一次迭代,只会进去 28) 216这样一个矩阵(用T表示)(这是一个时间步的迭代)
与此同时,会初始化一个H ,这个H的维数是一个 batchsize
hidddens的矩阵,也就是216的矩阵用H表示 H + T就是我们想要新的隐藏层H,(如果根据这个H 216 的H,再加上一个输出层 16*8,这里为什么是8,这个输出层跟着什么东西,全部看你想要获得一个什么东西,比如在这里,我想做一个文本的预测,做个文本的纠错问题,我希望它的输出还是一个词,那这个词只会来自于vocab字典中,所以我们映射成embedding的形状,到最后我们知道它是一个什么词对吗!

分析完了这么多,我们正式来看一下decoder,其实对于前面的理解并不难,难的是GRU的这里变成了embed_size + num_hiddens为什么是这样的,下面来看我分析。
我们定位到forward函数中,之前的循环神经网络,decoder部分我们直接套了一个hiddens * embedding的一个全连接层,但是我们不满足于H带出来的输出,我们希望是H + X进行输出,怎么做,进行维度上的concat,在这里,你可以看我们的X 是一个 4 * 7 8的矩阵 4表示我们的batchsize 7表示我们的timestep 8 表示我们的embeddingsize,我在embeddingsize这个维度concat了,其实是在embedding的维数上进行一个concat,让x的信息,让两个隐藏层的信息进去,这样就相当于带了两个隐藏层的信息和自己的embedding,。然后再进入一个rnn,然后出来的向量是一个batchsziehiddien,在进入一个全连接层,返回自己的vocab

就是我们整一个程序不是只有神经网络这个部分的,数据的预处理,预测都是很关键的

# 现在开始定义我们的损失函数

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

X = torch.tensor([[1, 2, 3], [4, 5, 6]])
sequence_mask(X, torch.tensor([1, 2]))

X.shape

我们知道,在我们的输入里面,有很多很多不必要的东西,比如PAD BOS EOS这些表示占位,表示起始结束的符号,之后我们会有一个记录位置的函数,就是跟这个函数进行搭配,放着这些占位符起作用。

class MaskedSoftmaxCELoss(nn.CrossEntropyLoss):
    """带遮蔽的softmax交叉熵损失函数"""
    # `pred` 的形状:(`batch_size`, `num_steps`, `vocab_size`)
    # `label` 的形状:(`batch_size`, `num_steps`)
    # `valid_len` 的形状:(`batch_size`,)
    def forward(self, pred, label, valid_len):
        weights = torch.ones_like(label)
        weights = sequence_mask(weights, valid_len)
        self.reduction='none'
        unweighted_loss = super(MaskedSoftmaxCELoss, self).forward(
            pred.permute(0, 2, 1), label)
        weighted_loss = (unweighted_loss * weights).mean(dim=1)
        return weighted_loss

重新定义我们的损失函数!

def train_seq2seq(net, data_iter, lr, num_epochs, tgt_vocab, device):
    """训练序列到序列模型。"""
    # net 我们定义好的网络
    # data_iter 数据集
    # lr学习率
    # num_epochs 会学习几次
    # tgt_vocab,目标字典
    # 装置
    def xavier_init_weights(m):
        if type(m) == nn.Linear:
            nn.init.xavier_uniform_(m.weight)
        if type(m) == nn.GRU:
            for param in m._flat_weights_names:
                if "weight" in param:
                    nn.init.xavier_uniform_(m._parameters[param])
    # 用xavier初始化权重,这样初始化的权重,计算出来的方差比较固定,且均值为零

    net.apply(xavier_init_weights)
    net.to(device)
    optimizer = torch.optim.Adam(net.parameters(), lr=lr)
    loss = MaskedSoftmaxCELoss()
    net.train()
    animator = d2l.Animator(xlabel='epoch', ylabel='loss',
                     xlim=[10, num_epochs])
    # 到这里都没什么可以说的,比较简单和平常套路相同
    for epoch in range(num_epochs):
        timer = d2l.Timer() # 创建一个计时器
        metric = d2l.Accumulator(2)  # 训练损失总和,词元数量
        for batch in data_iter:
            X, X_valid_len, Y, Y_valid_len = [x.to(device) for x in batch]
            #
            bos = torch.tensor([tgt_vocab['<bos>']] * Y.shape[0],
                          device=device).reshape(-1, 1) # 相当于拉成一个一列向量
            dec_input = torch.cat([bos, Y[:, :-1]], 1)  # 这里他把Y中每一个最后一个元素都换成了Bos吗,是这个意思嘛
            Y_hat, _ = net(X, dec_input, X_valid_len)
            l = loss(Y_hat, Y, Y_valid_len)
            l.sum().backward()      # 损失函数的标量进行“反传”
            d2l.grad_clipping(net, 1)
            num_tokens = Y_valid_len.sum()
            optimizer.step()
            with torch.no_grad():
                metric.add(l.sum(), num_tokens)
        if (epoch + 1) % 10 == 0:
            animator.add(epoch + 1, (metric[0] / metric[1],))
    print(f'loss {metric[0] / metric[1]:.3f}, {metric[1] / timer.stop():.1f} '
        f'tokens/sec on {str(device)}')

这个函数可以说是精华,来看我分析
# net 我们定义好的网络
# data_iter 数据集
# lr学习率
# num_epochs 会学习几次
# tgt_vocab,目标字典
# 装置

xavier_init_weight用xavier初始化权重,这样初始化的权重,计算出来的方差比较固定,且均值为零

首先获得数据,我们之道
X X_valid_len是一个词表和有效长度(英文)
Y Y_valid_len是一个词表和有效长度(法文)
然后把 y的其实状态改变,变成一个bos,其实状态
然后开始进行一个预测,然后进行梯度的更新

  • 12
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值