http://zh.d2l.ai/chapter_attention-mechanisms/transformer.html
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是过去的东西,那么在哪里存放了呢
本节的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的话,除了最后一个时间步,前面时间步的后续计算就出错了,因为它们会对“未来”产生注意力。当然由于最终我只取最后一个时间步的结果,所以结果依旧会正确,但这就没必要再多此一举了。
大佬们,我想问下transformer是监督学习还是无监督学习呀,老师讲的transformer似乎没有用到label,只是用到了X
这个预测步骤的代码,和seq2seq类似,都写错代码了。作者们可以修改下吗
这个预测步骤的代码,可以这么改吗?
预测时,由于batch_size=num_step=1, 计算position_embedding时,都只使用了第一行。这丧失了位置编码的优点。
你的方法可行,但是预测每一个字,都要把前面所有的字全部输入,如果句子很长,计算效率低。可以这样改吗,预测第一个字所用的position embedding用位置矩阵的第一行,预测第二个字用位置矩阵的第二行,依次类推,这样可行吗?
X2 = self.attention1(X, X, X, dec_valid_lens)
这个有点问题,预测时,按照上面同学的思路,比如当前是第三个时间步,q=k=v=前三个时间步,但是官方说是,当前时间步的输入是q, 前面已经生成的词元是k和v, 这里我觉得后者是对的。
当然可行,我只是想尽量做出小的修改,使得逻辑正确即可。而且主要的问题在于predict_seq2seq函数我不能修改,它已经被嵌入到d2l模块中了,因此我也必须顺应这个函数所期待的模型运行方式。
注意我在TransformerDecoder最后有一个这个修改:
if not self.training:
return self.dense(X)[:, -1:, :], state
这里官方的实现方法其实是比较绕的,因为当前时间步它只送入一个时间步的数据,官方把前面时间步的中间数据作为返回值与传入参数保存着,而我的实现方式比较直接,把初始数据作为网络实例状态保存,所以这里我和官方的代码肯定是不一样的。其实我们二者等效,我的也没有错误。
也许比较符合官方api的做法是我把实例状态也改为用state[2]来传递,不过效果都一样。
是否也可以修改predict seq2seq为把所有之前预测的东西一起输入进去?这样positional encoding就可以正常工作了?
这样当然最好……不过predict_seq2seq函数首次出现是在9.7节,并且作者提示它是#save了的,也就是说在pip安装的d2l模块里定义了,已经属于api的一部分了,在现在的10.7节我们是直接从d2l模块调用的predict_seq2seq函数,所以再修改它不太方便。
就我的理解,代码是没有错的。
- 训练时,对完整的输入添加位置编码,Decoder拟合的是已经加入序列信息的embedding,针对query输出的后序预测就包含了Decoder对位置的学习;
- 预测时,初始添加位置编码送入的是开始符号(完全生成)或者已有的半截句子对应的embedding(部分生成)。在完全生成时,确实送入的只有1时间步的位置编码,但这是正常的,此时的开始符号添加了位置信息和训练阶段一致,已经足够了。后续生成序列的位置信息由Decoder自行预测,且已包含在预测输出中了,因为训练时拟合的样本就是包含序列信息的。
很抱歉,我对时序任务了解的不多,很多词汇也未必专业,可能会造成阅读障碍。只是一点浅薄的看法,欢迎指正。
你需要把d2l中的源码改一下,torch.py文件中read_data_mnt函数的打开形式加上encoding=‘utf-8’