Transformer

http://zh-v2.d2l.ai/chapter_attention-mechanisms/transformer.html


不太明白为什么,这里LayerNorm为什么会是[32],不应该是[10,32]吗,这样还是LayerNorm吗?

As explained in official documentation about LayerNorm’s normalized_shape parameter, cite it here:

If a single integer is used, it is treated as a singleton list, and this module will normalize over the last dimension which is expected to be of that specific size.

So it’s ok only [32] is passed here.

在后面的predict中,num_steps为1,而训练数据num_step为10,同一个网络,如果设置[10,32],进行predict时会报错

不太明白self.i是过去的东西,那么在哪里存放了呢

  1. 因为位置编码在-1到1之间,为什么embedding要乘以维度的平方根进行缩放,这样做的依据是什么????

本节的Transformer解码器预测部分有错误。

在预测时,predict_seq2seq函数中每次送入解码器的X都是1批量1时间步的,因此TransformerDecoder的forward函数第一步X = self.pos_encoding(self.embedding(X) * math.sqrt(self.num_hiddens))将永远使用第一位置,导致在预测时解码器的位置编码失效。

为了使运行逻辑正确需要进行一些修改,我对DecoderBlock类的forward函数和TransformerDecoder类的init_state及forward函数进行了少量修改修复了这个问题。大致思想是在预测时把之前每一步的预测结果拼接在一起保存在TransformerDecoder对象里,使得预测第t步目标时输入解码器的X是从第0步到第(t-1)步的张量,而最后输出的结果只取与(t-1)步对应的即可。

首先是DecoderBlock.forward删除了state[2]相关的代码,现在不需要使用使用state[2]维持状态:

class DecoderBlock(nn.Module):
    """解码器中第i个块"""
    def __init__(self, key_size, query_size, value_size, num_hiddens,
                 norm_shape, ffn_num_input, ffn_num_hiddens, num_heads,
                 dropout, i, **kwargs):
        super(DecoderBlock, self).__init__(**kwargs)
        self.i = i
        self.attention1 = d2l.MultiHeadAttention(
            key_size, query_size, value_size, num_hiddens, num_heads, dropout)
        self.addnorm1 = AddNorm(norm_shape, dropout)
        self.attention2 = d2l.MultiHeadAttention(
            key_size, query_size, value_size, num_hiddens, num_heads, dropout)
        self.addnorm2 = AddNorm(norm_shape, dropout)
        self.ffn = PositionWiseFFN(ffn_num_input, ffn_num_hiddens,
                                   num_hiddens)
        self.addnorm3 = AddNorm(norm_shape, dropout)

    def forward(self, X, state):
        enc_outputs, enc_valid_lens = state[0], state[1]
        
        batch_size, num_steps, _ = X.shape
        # dec_valid_lens的开头:(batch_size,num_steps),
        # 其中每一行是[1,2,...,num_steps]
        dec_valid_lens = torch.arange(
            1, num_steps + 1, device=X.device).repeat(batch_size, 1)

        # 自注意力
        X2 = self.attention1(X, X, X, dec_valid_lens)
        Y = self.addnorm1(X, X2)
        # 编码器-解码器注意力。
        # enc_outputs的开头:(batch_size,num_steps,num_hiddens)
        Y2 = self.attention2(Y, enc_outputs, enc_outputs, enc_valid_lens)
        Z = self.addnorm2(Y, Y2)
        return self.addnorm3(Z, self.ffn(Z)), state

之后是TransformerDecoder,init_state中增加一个记录之前预测结果的属性,并且不需要有state[2]。forward中当不处于training状态时要保存每一步的预测结果,并且返回时只取最后一个时间步的结果:

class TransformerDecoder(d2l.AttentionDecoder):
    def __init__(self, vocab_size, key_size, query_size, value_size,
                 num_hiddens, norm_shape, ffn_num_input, ffn_num_hiddens,
                 num_heads, num_layers, dropout, **kwargs):
        super(TransformerDecoder, self).__init__(**kwargs)
        self.num_hiddens = num_hiddens
        self.num_layers = num_layers
        self.embedding = nn.Embedding(vocab_size, num_hiddens)
        self.pos_encoding = d2l.PositionalEncoding(num_hiddens, dropout)
        self.blks = nn.Sequential()
        for i in range(num_layers):
            self.blks.add_module("block"+str(i),
                DecoderBlock(key_size, query_size, value_size, num_hiddens,
                             norm_shape, ffn_num_input, ffn_num_hiddens,
                             num_heads, dropout, i))
        self.dense = nn.Linear(num_hiddens, vocab_size)

    def init_state(self, enc_outputs, enc_valid_lens, *args):
        self.seqX = None
        return [enc_outputs, enc_valid_lens]

    def forward(self, X, state):
        if not self.training:
            self.seqX = X if self.seqX is None else torch.cat((self.seqX, X), dim=1)
            X = self.seqX

        X = self.pos_encoding(self.embedding(X) * math.sqrt(self.num_hiddens))
        self._attention_weights = [[None] * len(self.blks) for _ in range (2)]
        for i, blk in enumerate(self.blks):
            X, state = blk(X, state)
            # 解码器自注意力权重
            self._attention_weights[0][
                i] = blk.attention1.attention.attention_weights
            # “编码器-解码器”自注意力权重
            self._attention_weights[1][
                i] = blk.attention2.attention.attention_weights
        
        if not self.training:
            return self.dense(X)[:, -1:, :], state
        
        return self.dense(X), state

    @property
    def attention_weights(self):
        return self._attention_weights

这样修改之后,后面四个预测结果的bleu值都为1:

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

后面在观察解码器注意力时,要把dec_attention_weights_2d中的head[0]修改为head[-1],代表观察最后一个解码时间步的注意力。原先是预测时只记录了一个时间步的注意力所以就用的head[0],而按照上面的改法那么每个时间步的解码器注意力都记录下来了。

dec_attention_weights_2d = [head[-1].tolist()
                            for step in dec_attention_weight_seq
                            for attn in step for blk in attn for head in blk]

大佬,你好,你这写的很好,但是我想问一下,这里 dec_valid_len,这里为啥要用arange而不使用实际的长度(在训练的时候,可能得到dec_valid_len的)
dec_valid_lens = torch.arange(
1, num_steps + 1, device=X.device).repeat(batch_size, 1)

这里的dec_valid_len与样本中翻译后句子的有效长度没有关系,而是为了使得transformer解码器在第一个自注意力块中每个时间步的单词不要对“未来”产生注意力而设置的。

因此,在任何解码器时间步中,只有生成的词元才能用于解码器的自注意力计算中。为了在解码器中保留自回归的属性,其掩蔽自注意力设定了参数dec_valid_lens ,以便任何查询都只会与解码器中所有已经生成词元的位置(即直到该查询位置为止)进行注意力计算。

dec_valid_len的size是[batch_size, num_steps],dec_valid_len使得X(X的维度为:[batch_size, num_steps, num_hiddens])在进行自注意力计算时,每个batch的第1时间步的单词只能对第1个时间步的单词有注意力,第2时间步的单词只能对前2个时间步的单词有注意力,第N时间步的单词只能对前N个时间步的单词有注意力。

而训练时在解码器中并不必考虑样本中翻译后的句子的有效长度,因为预测出来不一样长的惩罚最终会体现在loss中(可以回看9.7节train_seq2seq函数),编码部分必须要考虑翻译前句子的有效长度是因为解码块的第二个部分:“编码器-解码器”注意力部分,它要对编码时间步产生注意力,这里不应该对无效长度的部分产生注意力,所以需要enc_valid_lens。

注意自注意力计算中虽然query, key, value都送入的是X,但是在query中,不同时间步代表的是不同次的查询。

queries的形状:(batch_size,查询的个数,d)
keys的形状:(batch_size,“键-值”对的个数,d)
values的形状:(batch_size,“键-值”对的个数,值的维度)

在原书代码DecoderBlock.forward中区分了训练状态和预测状态,是因为预测状态总是一个词一个词送入,绝不会看到“未来”,因此书中就设置了dec_valid_len=None。而我修改的代码没有再考虑这个,是因为我即使在预测时也总是送入至今为止预测的完整时间的X,设置为None的话,除了最后一个时间步,前面时间步的后续计算就出错了,因为它们会对“未来”产生注意力。当然由于最终我只取最后一个时间步的结果,所以结果依旧会正确,但这就没必要再多此一举了。