《动手学深度学习》Seq2Seq代码可能出错的原因及适当分析

关于沐神《动手学深度学习》Seq2Seq代码可能出错的原因及适当分析

先放训练结果:

沐神的 300个epoch:

go . => va au !, bleu 0.000
i lost . => j’ai perdu perdu ., bleu 0.783
he’s calm . => il est essaye il partie paresseux ., bleu 0.418
i’m home . => je suis chez tom chez triste pas pas pas , bleu 0.376

接下来修改的V1、V2版本 100个epoch

V1
go . => va !, bleu 1.000
i lost . => j’ai perdu ., bleu 1.000
he’s calm . => il est riche ., bleu 0.658
i’m home . => je suis chez moi ., bleu 1.000

V2
go . => va !, bleu 1.000
i lost . => j’ai perdu ., bleu 1.000
he’s calm . => il est ., bleu 0.658
i’m home . => je suis chez moi ., bleu 1.000

放一下所有修改前、修改后的代码:

解码器
【修正前】
class Seq2SeqDecoder(d2l.Decoder):
    def __init__(self, vocab_size, embed_size, num_hiddens, num_layers, dropout=0, **kwargs):
        super(Seq2SeqDecoder, self).__init__(**kwargs)
        # 解码器要有自己的embedding层,因为翻译一个英语一个法语
        self.embedding = nn.Embedding(vocab_size, embed_size)
        # 这里假设encoder隐藏层大小和decoder隐藏层大小是一样的
        self.rnn = nn.GRU(embed_size + num_hiddens, num_hiddens, num_layers, dropout=dropout)
        # 做一个vocab_size的分类
        self.dense = nn.Linear(num_hiddens, vocab_size)
        
    # enc的输出有两部分:outputs和state,只要state
    def init_state(self, enc_outputs, *args):
        return enc_outputs[1]
    
    # 如果没有上下文操作,那就是一个普通的rnn,没有什么区别。
    def forward(self, X, state):
        # 把时间步放到前面
        X = self.embedding(X).permute(1, 0, 2)
        '''
        上下文操作。这里state[-1]拿到的是“最右上角的”H(这个H融合和所有的信息)如果state是【2,4,16】的,那state[-1]就是【1,4,16】的。repeat重复时间步次。这样,每一个时间步都可以用到最后的H信息,与新的输入X做concat操作(这也是为什么解码器的self.rnn是ebd_size + num_hiddens的原因)。如果state[-1]是【1,4,16】,时间步是7,那重复完之后就是【7,4,16】的(7个时间步,4是batch_size,16是state隐藏单元的个数)。
       '''
        context = state[-1].repeat(X.shape[0], 1, 1)
        X_and_context = torch.cat((X, context), dim=2)
        output, state = self.rnn(X_and_context, state)
        # 再把维度调整回(batch_size, num_step, vocab_Size)
        output = self.dense(output).permute(1, 0, 2)
        return output, state
【修正后-V1】

修正原因见下面“训练部分”

class Seq2SeqDecoder(d2l.Decoder):
    def __init__(self, vocab_size, embed_size, num_hiddens, num_layers, dropout=0, **kwargs):
        super(Seq2SeqDecoder, self).__init__(**kwargs)
        # 解码器要有自己的embedding层,因为翻译一个英语一个法语
        self.embedding = nn.Embedding(vocab_size, embed_size)
        # 这里假设encoder隐藏层大小和decoder隐藏层大小是一样的
        self.rnn = nn.GRU(embed_size + num_hiddens, num_hiddens, num_layers, dropout=dropout)
        # 做一个vocab_size的分类
        self.dense = nn.Linear(num_hiddens, vocab_size)
        
    # enc的输出有两部分:outputs和state,只要state
    def init_state(self, enc_outputs, *args):
        return enc_outputs[1]
    
    # 如果没有上下文操作,那就是一个普通的rnn,没有什么区别。
    def forward(self, X, enc_state, state=None):
        # 把时间步放到前面
        X = self.embedding(X).permute(1, 0, 2)
         '''
        上下文操作。这里state[-1]拿到的是“最右上角的”H(这个H融合和所有的信息)如果state是【2,4,16】的,那state[-1]就是【1,4,16】的。repeat重复时间步次。这样,每一个时间步都可以用到最后的H信息,与新的输入X做concat操作(这也是为什么解码器的self.rnn是ebd_size + num_hiddens的原因)。如果state[-1]是【1,4,16】,时间步是7,那重复完之后就是【7,4,16】的(7个时间步,4是batch_size,16是state隐藏单元的个数)。
       '''
        context = enc_state[-1].repeat(X.shape[0], 1, 1)
        X_and_context = torch.cat((X, context), dim=2)
        output, state = self.rnn(X_and_context, state)
        # 再把维度调整回(batch_size, num_step, vocab_Size)
        output = self.dense(output).permute(1, 0, 2)
        return output, state
关键
# 关键部分:
def forward(self, X, enc_state, state=None):
    # X: (batch_size, num_step, emb_size)
    X = nn.Embedding(X).permute(1, 0, 2)
    context = enc_state[-1].repeat(X.shape[0], 1, 1) # (num_step, batch_size, num_hidden)
    X_and_Context = torch.cat((X, context), dim=2) # (num_step, batch_size, emb_size + num_hidden)
    # 如果state == None,那nn.GRU.forward中的第二个参数就是None,会自动生成(num_layer, batch_size, num_hiddens)的全0张量
    output, state = self.rnn(X_and_Context, state) # (num_step, batch_size, num_hidden)
    output = self.dense(output).permute(1, 0, 2)
    return output, state
【修正后-V2-My】

在V1的基础上,增加对这个类的修改:

#@save
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, dec_state)

修订前的预测部分代码】

#@save
def predict_seq2seq(net, src_sentence, src_vocab, tgt_vocab, num_steps,
                    device, save_attention_weights=False):
    """序列到序列模型的预测"""
    # 在预测时将net设置为评估模式
    net.eval()
    src_tokens = src_vocab[src_sentence.lower().split(' ')] + [
        src_vocab['<eos>']]
    enc_valid_len = torch.tensor([len(src_tokens)], device=device)
    src_tokens = d2l.truncate_pad(src_tokens, num_steps, src_vocab['<pad>'])
    # 添加批量轴
    enc_X = torch.unsqueeze(
        torch.tensor(src_tokens, dtype=torch.long, device=device), dim=0)
    enc_outputs = net.encoder(enc_X)
    dec_state = net.decoder.init_state(enc_outputs, enc_valid_len)
    # 添加批量轴
    dec_X = torch.unsqueeze(torch.tensor(
        [tgt_vocab['<bos>']], dtype=torch.long, device=device), dim=0)
    output_seq, attention_weight_seq = [], []
    for _ in range(num_steps):
        Y, dec_state = net.decoder(dec_X, dec_state)
        # 我们使用具有预测最高可能性的词元,作为解码器在下一时间步的输入
        dec_X = Y.argmax(dim=2)
        pred = dec_X.squeeze(dim=0).type(torch.int32).item()
        # 保存注意力权重(稍后讨论)
        if save_attention_weights:
            attention_weight_seq.append(net.decoder.attention_weights)
        # 一旦序列结束词元被预测,输出序列的生成就完成了
        if pred == tgt_vocab['<eos>']:
            break
        output_seq.append(pred)
    return ' '.join(tgt_vocab.to_tokens(output_seq)), attention_weight_seq

修订后代码】

#@save
def predict_seq2seq(net, src_sentence, src_vocab, tgt_vocab, num_steps,
                    device, save_attention_weights=False):
    """序列到序列模型的预测"""
    # 在预测时将net设置为评估模式
    net.eval()
    src_tokens = src_vocab[src_sentence.lower().split(' ')] + [
        src_vocab['<eos>']]
    enc_valid_len = torch.tensor([len(src_tokens)], device=device)
    src_tokens = d2l.truncate_pad(src_tokens, num_steps, src_vocab['<pad>'])
    # 添加批量轴
    enc_X = torch.unsqueeze(
        torch.tensor(src_tokens, dtype=torch.long, device=device), dim=0)
    enc_outputs = net.encoder(enc_X)
    dec_state = net.decoder.init_state(enc_outputs, enc_valid_len)
    # 添加批量轴
    dec_X = torch.unsqueeze(torch.tensor(
        [tgt_vocab['<bos>']], dtype=torch.long, device=device), dim=0)
    output_seq, attention_weight_seq = [], []
    state = None   # V2版本的修订代码这里换成 state = dec_state    
    for _ in range(num_steps):
        Y, state = net.decoder(dec_X, dec_state, state)
        # 我们使用具有预测最高可能性的词元,作为解码器在下一时间步的输入
        # dim=2是vocab维度
        dec_X = Y.argmax(dim=2)
        pred = dec_X.squeeze(dim=0).type(torch.int32).item()
        # 保存注意力权重(稍后讨论)
        if save_attention_weights:
            attention_weight_seq.append(net.decoder.attention_weights)
        # 一旦序列结束词元被预测,输出序列的生成就完成了
        if pred == tgt_vocab['<eos>']:
            break
        output_seq.append(pred)
    return ' '.join(tgt_vocab.to_tokens(output_seq)), attention_weight_seq

这里写一下自己学这部分的心得,以及整理下混乱的地方,以及回答为什么沐神的decoder也有问题。

引用:

孙笑川-7742的动态-哔哩哔哩 (bilibili.com)

9.7. 序列到序列学习(seq2seq) — 动手学深度学习 2.0.0-beta0 documentation (d2l.ai)——讨论区

首先是关于nn.RNN【nn.LSTM,nn.GRU同理】。

nn.RNN()初始化的时候需要的参数:(vocab_size,num_hiddens,num_layers)

而在调用net() 即forward方法的时候,需要传入的参数是X输入与state隐状态。

初始化隐状态state的时候需要的参数:(num_layers, batch_size, num_hiddens)

其次,train和predict有一个本质的区别在于:train的时候是已知num_step的,输入X是(batch_size, num_step)的,所以在decoder里调用self.rnn()其实只调用了一次

# 关键部分:
def forward(self, X, enc_state, state=None):
    # X: (batch_size, num_step, emb_size)
    X = nn.Embedding(X).permute(1, 0, 2)
    context = enc_state[-1].repeat(X.shape[0], 1, 1) # (num_step, batch_size, num_hidden)
    X_and_Context = torch.cat((X, context), dim=2) # (num_step, batch_size, emb_size + num_hidden)
    # 如果state == None,那nn.GRU.forward中的第二个参数就是None,会自动生成(num_layer, batch_size, num_hiddens)的全0张量
    output, state = self.rnn(X_and_Context, state) # (num_step, batch_size, num_hidden)
    output = self.dense(output).permute(1, 0, 2)
    return output, state

为什么这么说? 这里可以以手写的”从零开始实现rnn"中的计算函数来说:

我们已经permute了,把时间维度放到了第一维。在nn.GRU.forward调用的时候,内部其实是有一个for循环的【就像下面这样】,由于训练的时候已知时间步,所以隐状态在forward的时候是自动隐蔽的更新了:

# 计算。给一个小批量,将里面所有的时间步都算一遍,得到输出。
# input里包括所有的时间步(X_0到X_t),state是上一次运算的隐藏状态, params是可以学习的参数
def rnn(inputs, state, params):
    W_xh, W_hh, b_h, W_hq, b_q = params
    H, = state # 这里是一个tuple,但是只有一个元素
    outputs = []
    for X in inputs: # inputs是一个三维的矩阵:(时间步,batch_size, one_hot长),这样循环会按时间步分,所以前面要转置
        H = torch.tanh(torch.matmul(X, W_xh) + torch.matmal(H, W_hh) + b_h)
        Y = torch.matmul(H, W_hq) + b_q # Y是当前时间步预测下一个单词是谁,但是这里是一个for循环,所以要append
        outputs.append(Y)
    # cat之后是一个二维矩阵,可以认为是n个矩阵按照竖着摞起来的。列数还是vocab_size,行数是batch_size * 时间步数
    return torch.cat(outputs, dim=0), (H, )

对于修改版V1,一开始的时候,从0手写rnn也要对state做初始化全0操作(init_state函数), 如果第二个参数传None,不会影响初始化(而且在编码器的时候根本也没写state, 默认会初始化成0)。

对于修改版V2,在训练的时候传入的参数是两个相同的,效果和沐神的一样,解码器的隐状态初始化为编码器的输出。

所以对于训练来说,沐神代码【修订前】的也是可以的,因为:

 def forward(self, X, state):
        # 把时间步放到前面
        X = self.embedding(X).permute(1, 0, 2)
        '''
        上下文操作。这里state[-1]拿到的是“最右上角的”H(这个H融合和所有的信息)如果state是【2,4,16】的,那state[-1]就是【1,4,16】的。repeat重复时间步次。这样,每一个时间步都可以用到最后的H信息,与新的输入X做concat操作(这也是为什么解码器的self.rnn是ebd_size + num_hiddens的原因)。如果state[-1]是【1,4,16】,时间步是7,那重复完之后就是【7,4,16】的(7个时间步,4是batch_size,16是state隐藏单元的个数)。
       '''
        context = state[-1].repeat(X.shape[0], 1, 1)
        X_and_context = torch.cat((X, context), dim=2)
        output, state = self.rnn(X_and_context, state)
        # 再把维度调整回(batch_size, num_step, vocab_Size)
        output = self.dense(output).permute(1, 0, 2)
        return output, state

就算只有一个state,但是时间步那一重循环是在self.rnn.forward里做的,所以依然可以保证每次输入都是X和最后编码器的隐状态concat起来。【这里啰嗦一句,因为时间步在训练的时候是已知的,而且我们已经repeat过state[-1],so…】

但是对于训练来说就G了。

for _ in range(num_steps):
        Y, dec_state = net.decoder(dec_X, dec_state)
        # 我们使用具有预测最高可能性的词元,作为解码器在下一时间步的输入
        dec_X = Y.argmax(dim=2)
        pred = dec_X.squeeze(dim=0).type(torch.int32).item()
        # 保存注意力权重(稍后讨论)
        if save_attention_weights:
            attention_weight_seq.append(net.decoder.attention_weights)
        # 一旦序列结束词元被预测,输出序列的生成就完成了
        if pred == tgt_vocab['<eos>']:
            break
        output_seq.append(pred)
    return ' '.join(tgt_vocab.to_tokens(output_seq)), attention_weight_seq

这里因为是预测,不知道时间步是多长,只能自己写一个显式的for循环(而不是之前在self.rnn.forward里的隐式for循环), 所以net.decoder(也就是forward函数)可不止调用了一次!!

那在原来的decoder调用的时候,就G了,因为state显然是每次都在变化的…而这时候的可以看出dec_X(也就是输入)是batch_size = 1, num_step = 1的,所以每次X_and_context都不一样,根本就不是编码器的最终结果,而是上一次decoder的输出【 forward 函数里 context 与 rnn 的初始 hidden layer 耦合了(使用的都是输入参数 state )

所以要分开才行——enc_state应该干两件事:

1、初始化解码器的state;

2、每个输入都要加上(这里的加是concat,inception式而非resnet式)enc_state

所以我们要解耦合,搞两个变量来记录。【V1,V2效果都可以,但是我认为V2更好,更合理】:

 	state = dec_state   
    for _ in range(num_steps):
        Y, state = net.decoder(dec_X, dec_state, state)
        # 我们使用具有预测最高可能性的词元,作为解码器在下一时间步的输入
        # dim=2是vocab维度
        dec_X = Y.argmax(dim=2)
        pred = dec_X.squeeze(dim=0).type(torch.int32).item()
        # 保存注意力权重(稍后讨论)
        if save_attention_weights:
            attention_weight_seq.append(net.decoder.attention_weights)
        # 一旦序列结束词元被预测,输出序列的生成就完成了
        if pred == tgt_vocab['<eos>']:
            break
        output_seq.append(pred)
    return ' '.join(tgt_vocab.to_tokens(output_seq)), attention_weight_seq
  • 5
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值