记录一下第十章:注意力机制。本来以为现代循环神经网络就已经是难度的极限了,没想到还有更可怕的啊啊啊。我这辈子和编码器-解码器过不去了。
第十章:注意力机制
10.1 注意力提示
这一节首先介绍了注意力这样一个概念,注意力是指在处理一系列东西(比如句子序列)时将注意力放在某个重点上。
然后运用到神经网络中,就是输入一个查询和一些键值对,通过注意力汇聚的形式就能将与查询匹配的键找出来,然后按匹配比例增加对应值的权重,作为输出。
输出一般和查询形状相同。
10.2 注意力汇聚
注意力汇聚动态分配权重来综合输入数据的不同部分,从而生成更有代表的输出。
注意力汇聚的基本公式是:
其中K是高斯核,是查询,是键值对。
比如取K为:
那么:
中间这个softmax函数之前提到过,是计算和的相似度的。
比如中有0、1、2、3,查询为2,那么对计算softmax就是,然后乘以对应的,接着对、......也都这么做,加起来就是查询x对应的值,可以看到越是接近查询(),softmax的值就越大,所以对应也越大。最后将所有值加起来时结果也会更偏向。
上面是不带参数的注意力汇聚,还有带参数的版本:
是一个实数,代表的是"惩罚因子",如果值查询越偏离某个键,那么输出也会越偏移这个键对应的值。
比如在上面的例子,w为1时,对0、1、2、3的查询2偏向y2,但w为2时,对于0、1、3的惩罚就会增加,于是输出的y值将会更偏向y2。
10.3 注意力评分函数
在之前的公式中:
最右边的a就是注意力评分函数。
在具体实现注意力评分机制之前,先实现一个掩蔽softmax操作,在之前进行自然语言处理中我们有时会添加填充词元,掩蔽softmax操作就是去除填充词元的影响:
python
#@save
def masked_softmax(X, valid_lens):
"""通过在最后一个轴上掩蔽元素来执行softmax操作"""
# X:3D张量,valid_lens:1D或2D张量
if valid_lens is None:
return nn.functional.softmax(X, dim=-1)
else:
shape = X.shape
if valid_lens.dim() == 1:
valid_lens = torch.repeat_interleave(valid_lens, shape[1])
else:
valid_lens = valid_lens.reshape(-1)
# 最后一轴上被掩蔽的元素使用一个非常大的负值替换,从而其softmax输出为0
X = d2l.sequence_mask(X.reshape(-1, shape[-1]), valid_lens,
value=-1e6)
return nn.functional.softmax(X.reshape(shape), dim=-1)
比如:
python
masked_softmax(torch.rand(2, 2, 4), torch.tensor([2, 3]))
输出就会是:
tensor([[[0.5980, 0.4020, 0.0000, 0.0000], [0.5548, 0.4452, 0.0000, 0.0000]], [[0.3716, 0.3926, 0.2358, 0.0000], [0.3455, 0.3337, 0.3208, 0.0000]]])
掩蔽了2、3列外的数字,只取有效长度计算softmax。
接下来介绍两种注意力评分函数。
10.3.2 加性注意力
加性注意力评分函数:
这种评分函数适用于查询q和键k大小不同的情况。
用参数W将其化为同一形状大小再相加,然后放入tanh激活函数,最后再乘以一个参数。
实现起来输入查询q、键k、隐藏层长度num_hiddens(以便将k和q都化为这个大小)、dropout(暂退法随机丢弃一些)。
python
#@save
class AdditiveAttention(nn.Module):
"""加性注意力"""
def __init__(self, key_size, query_size, num_hiddens, dropout, **kwargs):
super(AdditiveAttention, self).__init__(**kwargs)
self.W_k = nn.Linear(key_size, num_hiddens, bias=False)
self.W_q = nn.Linear(query_size, num_hiddens, bias=False)
self.w_v = nn.Linear(num_hiddens, 1, bias=False)
self.dropout = nn.Dropout(dropout)
def forward(self, queries, keys, values, valid_lens):
queries, keys = self.W_q(queries), self.W_k(keys)
# 在维度扩展后,
# queries的形状:(batch_size,查询的个数,1,num_hidden)
# key的形状:(batch_size,1,"键-值"对的个数,num_hiddens)
# 使用广播方式进行求和
features = queries.unsqueeze(2) + keys.unsqueeze(1)
features = torch.tanh(features)
# self.w_v仅有一个输出,因此从形状中移除最后那个维度。
# scores的形状:(batch_size,查询的个数,"键-值"对的个数)
scores = self.w_v(features).squeeze(-1)
self.attention_weights = masked_softmax(scores, valid_lens)
# values的形状:(batch_size,"键-值"对的个数,值的维度)
return torch.bmm(self.dropout(self.attention_weights), values)
要注意乘以w后q和k的形状也不是完全相同的,只是改变了当个查询和单个键的长度,查询数和键值对个数可能不同。
在相加的时候是广播求和,如代码所示,每一个查询会依次加每一个键。
还要注意这个attention_weights ,后面会非常频繁的出现,它代表softmax后的值,相当于每一个查询对每一个键值的匹配程度。
最后的Wv线性层作用是移除那个多余的维度。
softmax操作要去除无效的填充词元,有效长度代表的是key中的有效长度,实际使用时key可能是带填充词元的一句话,不一定全部是有效长度。
最后dropout暂退法后将attention_weights乘以value值就行。
这里查询q和输出大小不同,是因为要贴合值value大小,输出维度包含查询个数和value长度。
10.3.3 缩放点积注意力
如果查询和键长度相同,使用点积会更好,计算效率更高。
缩放点击注意力的评分函数如下:
代码实现也更好理解:
python
#@save
class DotProductAttention(nn.Module):
"""缩放点积注意力"""
def __init__(self, dropout, **kwargs):
super(DotProductAttention, self).__init__(**kwargs)
self.dropout = nn.Dropout(dropout)
# queries的形状:(batch_size,查询的个数,d)
# keys的形状:(batch_size,"键-值"对的个数,d)
# values的形状:(batch_size,"键-值"对的个数,值的维度)
# valid_lens的形状:(batch_size,)或者(batch_size,查询的个数)
def forward(self, queries, keys, values, valid_lens=None):
d = queries.shape[-1]
# 设置transpose_b=True为了交换keys的最后两个维度
scores = torch.bmm(queries, keys.transpose(1,2)) / math.sqrt(d)
self.attention_weights = masked_softmax(scores, valid_lens)
return torch.bmm(self.dropout(self.attention_weights), values)
这里查询q和键k的长度相同(不是查询数和键值对数相同),于是我们可以将键k维度互换,使两者可以相乘。
之后的处理和加性注意力没有区别,对w进行去除填充词元的softmax,最后暂退法后乘以value。
(对点积注意力没有要学习的参数不是很理解,去搜了一下,ai说点积注意力其实也有分别对于kqv的权重的,也就是说kqv在进行注意力汇聚之前还要进行一个线性变换)
10.4 Bahdanau注意力
这一节讲的是在编码器-解码器中间塞一个注意力机制。
之前说到,编码器输出包括循环神经网络的输出和最后一个时间步的状态(上下文变量),解码器的输入是上下文变量。
这里是将编码器循环神经网络的输出作为键值对,最后一层的上下文变量作为查询执行注意力机制。
写一个带注意力机制的解码器:
python
class Seq2SeqAttentionDecoder(AttentionDecoder):
def __init__(self, vocab_size, embed_size, num_hiddens, num_layers,
dropout=0, **kwargs):
super(Seq2SeqAttentionDecoder, self).__init__(**kwargs)
self.attention = d2l.AdditiveAttention(
num_hiddens, num_hiddens, num_hiddens, dropout)
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, enc_valid_lens, *args):
# outputs的形状为(batch_size,num_steps,num_hiddens).
# hidden_state的形状为(num_layers,batch_size,num_hiddens)
outputs, hidden_state = enc_outputs
return (outputs.permute(1, 0, 2), hidden_state, enc_valid_lens)
def forward(self, X, state):
# enc_outputs的形状为(batch_size,num_steps,num_hiddens).
# hidden_state的形状为(num_layers,batch_size,
# num_hiddens)
enc_outputs, hidden_state, enc_valid_lens = state
# 输出X的形状为(num_steps,batch_size,embed_size)
X = self.embedding(X).permute(1, 0, 2)
outputs, self._attention_weights = [], []
for x in X:
# query的形状为(batch_size,1,num_hiddens)
query = torch.unsqueeze(hidden_state[-1], dim=1)
# context的形状为(batch_size,1,num_hiddens)
context = self.attention(
query, enc_outputs, enc_outputs, enc_valid_lens)
# 在特征维度上连结
x = torch.cat((context, torch.unsqueeze(x, dim=1)), dim=-1)
# 将x变形为(1,batch_size,embed_size+num_hiddens)
out, hidden_state = self.rnn(x.permute(1, 0, 2), hidden_state)
outputs.append(out)
self._attention_weights.append(self.attention.attention_weights)
# 全连接层变换后,outputs的形状为
# (num_steps,batch_size,vocab_size)
outputs = self.dense(torch.cat(outputs, dim=0))
return outputs.permute(1, 0, 2), [enc_outputs, hidden_state,
enc_valid_lens]
@property
def attention_weights(self):
return self._attention_weights
这里注意,这个state不单是隐状态,而是包含了编码器输出、最后一步隐状态、有效长度。
像之前那样,编码器最后一步的隐状态被设为解码器的初始隐状态,之前编码器最后一步最后一层的隐状态和解码器的输入 X 连接,这里变成先通过注意力机制再连接。
引入注意力机制是因为需要从编码器输出中寻找当前隐状态需要注意的地方,键key和值value都是编码器的输入,查询是隐状态。
最后将X与查询的结果进行连接,然后扔进循环神经网络,记录结果和注意力的权重。
为了检测是否理解了这些操作和维度的变化,可以试着分析一下下面这个的输出:
python
encoder = d2l.Seq2SeqEncoder(vocab_size=10, embed_size=8, num_hiddens=16,
num_layers=2)
encoder.eval()
decoder = Seq2SeqAttentionDecoder(vocab_size=10, embed_size=8, num_hiddens=16,
num_layers=2)
decoder.eval()
X = torch.zeros((4, 7), dtype=torch.long) # (batch_size,num_steps)
state = decoder.init_state(encoder(X), None)
output, state = decoder(X, state)
output.shape, len(state), state[0].shape, len(state[1]), state[1][0].shape
最后一行是各个角色的大小形状,可以试着自己预估一下输出。
答案:
(torch.Size([4, 7, 10]), 3, torch.Size([4, 7, 16]), 2, torch.Size([4, 16]))
然后训练依旧是调用之前的编码器-解码器训练函数。
10.5 多头注意力
多头注意力是将一个注意力汇聚分成多个头head,最后将这多个头的结果进行连接。
具体计算方法如下:
计算每一个"头":
这里的f包括在加权后的qkv上截取h段,分别进行注意力的计算。
将每个"头"的结果连接:
代码:
python
#@save
class MultiHeadAttention(nn.Module):
"""多头注意力"""
def __init__(self, key_size, query_size, value_size, num_hiddens,
num_heads, dropout, bias=False, **kwargs):
super(MultiHeadAttention, self).__init__(**kwargs)
self.num_heads = num_heads
self.attention = d2l.DotProductAttention(dropout)
self.W_q = nn.Linear(query_size, num_hiddens, bias=bias)
self.W_k = nn.Linear(key_size, num_hiddens, bias=bias)
self.W_v = nn.Linear(value_size, num_hiddens, bias=bias)
self.W_o = nn.Linear(num_hiddens, num_hiddens, bias=bias)
def forward(self, queries, keys, values, valid_lens):
# queries,keys,values的形状:
# (batch_size,查询或者"键-值"对的个数,num_hiddens)
# valid_lens 的形状:
# (batch_size,)或(batch_size,查询的个数)
# 经过变换后,输出的queries,keys,values 的形状:
# (batch_size*num_heads,查询或者"键-值"对的个数,
# num_hiddens/num_heads)
queries = transpose_qkv(self.W_q(queries), self.num_heads)
keys = transpose_qkv(self.W_k(keys), self.num_heads)
values = transpose_qkv(self.W_v(values), self.num_heads)
if valid_lens is not None:
# 在轴0,将第一项(标量或者矢量)复制num_heads次,
# 然后如此复制第二项,然后诸如此类。
valid_lens = torch.repeat_interleave(
valid_lens, repeats=self.num_heads, dim=0)
# output的形状:(batch_size*num_heads,查询的个数,
# num_hiddens/num_heads)
output = self.attention(queries, keys, values, valid_lens)
# output_concat的形状:(batch_size,查询的个数,num_hiddens)
output_concat = transpose_output(output, self.num_heads)
return self.W_o(output_concat)
transpose_qkv是将qkv切分成h段,切出来的段与batch_size合并。transpose_output的操作与之相反。
因为长度被线性变化成相同的形状了,所以可以使用点积注意力。
python
#@save
def transpose_qkv(X, num_heads):
"""为了多注意力头的并行计算而变换形状"""
# 输入X的形状:(batch_size,查询或者"键-值"对的个数,num_hiddens)
# 输出X的形状:(batch_size,查询或者"键-值"对的个数,num_heads,
# num_hiddens/num_heads)
X = X.reshape(X.shape[0], X.shape[1], num_heads, -1)
# 输出X的形状:(batch_size,num_heads,查询或者"键-值"对的个数,
# num_hiddens/num_heads)
X = X.permute(0, 2, 1, 3)
# 最终输出的形状:(batch_size*num_heads,查询或者"键-值"对的个数,
# num_hiddens/num_heads)
return X.reshape(-1, X.shape[2], X.shape[3])
#@save
def transpose_output(X, num_heads):
"""逆转transpose_qkv函数的操作"""
X = X.reshape(-1, num_heads, X.shape[1], X.shape[2])
X = X.permute(0, 2, 1, 3)
return X.reshape(X.shape[0], X.shape[1], -1)
最后合并所有头h的结果,其实就是分割头的逆操作。
这一节的小节说多头注意力的知识来自相同的查询,不同的键值对,我想它说的是查询虽然被切分成了不同的头,但其实都来源于相同的查询;而键值对切分就成了不同键值对。
10.6 自注意力
自注意力是指查询q、键k、值v都相同的注意力汇聚。
它可以关注到每一个键值对生成输出。
比较卷积神经网络、循环神经网络和自注意力,发现自注意力和卷积神经网络同样具有可以并行计算的优点,它们不需要严格按顺序计算。
接着再介绍一个位置编码,为了在自注意力中引入位置的概念。
位置编码形状与原矩阵形状相同,每个元素由其位置决定。
比如第i行2j列的元素就是。
实现代码引入位置编码:
python
#@save
class PositionalEncoding(nn.Module):
"""位置编码"""
def __init__(self, num_hiddens, dropout, max_len=1000):
super(PositionalEncoding, self).__init__()
self.dropout = nn.Dropout(dropout)
# 创建一个足够长的P
self.P = torch.zeros((1, max_len, num_hiddens))
X = torch.arange(max_len, dtype=torch.float32).reshape(
-1, 1) / torch.pow(10000, torch.arange(
0, num_hiddens, 2, dtype=torch.float32) / num_hiddens)
self.P[:, :, 0::2] = torch.sin(X)
self.P[:, :, 1::2] = torch.cos(X)
def forward(self, X):
X = X + self.P[:, :X.shape[1], :].to(X.device)
return self.dropout(X)
初始化建立一个非常大的数组P,每个元素都是其位置编码。
其中X是,0::2意思是从0开始每一步加2,也就是0、2、4......
这样,P就记录了所有位置的位置编码,在前向计算中,X只要加上对应位置的P就行。
10.7 Transformer
这一章的压轴,果然有关于编码器-解码器架构的都不简单......
transformer模型是一种结合了多头自注意力、编码器-解码器架构、位置编码的模型,它可以在处理一个元素时兼顾序列中的其他元素。
它的架构如下:
可以看到,transformer的编码器和解码器都是由许多重复的块构成,这些块又包含了许多小块。
由于其中没有卷积神经网络和循环神经网络,所以需要进行位置编码引入元素位置的影响。
下面先来逐一分析这几个块:
**嵌入层和位置编码:**增加一个嵌入的维度,加上位置编码。
**多头注意力:**多头自注意力,将输入同时作为查询、键、值,采用多头加性注意力。
**加&规范化:**引入残差网络,将原本输入与多头自注意力输出拼接,然后使用层规范化,注意不是之前的批量规范化,层规范化能更好处理自然语言任务。
层规范化的初始化输入是输入X每一层的形状,利用这个保存不同层每个位置的均值和方差。
对比层规范化和批量规范化:
python
ln = nn.LayerNorm(2)
bn = nn.BatchNorm1d(2)
X = torch.tensor([[1, 2], [2, 3]], dtype=torch.float32)
# 在训练模式下计算X的均值和方差
print('layer norm:', ln(X), '\nbatch norm:', bn(X))
layer norm: tensor([[-1.0000, 1.0000], [-1.0000, 1.0000]], grad_fn=<NativeLayerNormBackward0>) batch norm: tensor([[-1.0000, -1.0000], [ 1.0000, 1.0000]], grad_fn=<NativeBatchNormBackward0>)
可见,规范化的维度不同。
层规范化是对所有批量的某一层一起规范化。
批量规范化是对所有层的某一批量规范化。
逐位前馈网络:
这里的前馈网络其实就是线性层。
在同一位置的权重相同,所以才叫基于位置,其实就是普通的两层线性层:
python
#@save
class PositionWiseFFN(nn.Module):
"""基于位置的前馈网络"""
def __init__(self, ffn_num_input, ffn_num_hiddens, ffn_num_outputs,
**kwargs):
super(PositionWiseFFN, self).__init__(**kwargs)
self.dense1 = nn.Linear(ffn_num_input, ffn_num_hiddens)
self.relu = nn.ReLU()
self.dense2 = nn.Linear(ffn_num_hiddens, ffn_num_outputs)
def forward(self, X):
return self.dense2(self.relu(self.dense1(X)))
接下来就可以实现编码器了。
先实现一个块,之前说过transformer编码器是由多个重复的块组成。
初始化输入有查询、键、值的大小(虽然其实都是一样的),线性层的输入隐藏层输出大小,头的数目。
前向传播输入嵌入后的文本,有效长度。
python
#@save
class EncoderBlock(nn.Module):
"""Transformer编码器块"""
def __init__(self, key_size, query_size, value_size, num_hiddens,
norm_shape, ffn_num_input, ffn_num_hiddens, num_heads,
dropout, use_bias=False, **kwargs):
super(EncoderBlock, self).__init__(**kwargs)
self.attention = d2l.MultiHeadAttention(
key_size, query_size, value_size, num_hiddens, num_heads, dropout,
use_bias)
self.addnorm1 = AddNorm(norm_shape, dropout)
self.ffn = PositionWiseFFN(
ffn_num_input, ffn_num_hiddens, num_hiddens)
self.addnorm2 = AddNorm(norm_shape, dropout)
def forward(self, X, valid_lens):
Y = self.addnorm1(X, self.attention(X, X, X, valid_lens))
return self.addnorm2(Y, self.ffn(Y))
按照之前那个结构图执行就行。
然后再来实现整个编码器:
初始化时多了一个嵌入层需要的词表大小和层数的参数,按照层数实现多个上面的块即可。
python
#@save
class TransformerEncoder(d2l.Encoder):
"""Transformer编码器"""
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, use_bias=False, **kwargs):
super(TransformerEncoder, self).__init__(**kwargs)
self.num_hiddens = num_hiddens
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),
EncoderBlock(key_size, query_size, value_size, num_hiddens,
norm_shape, ffn_num_input, ffn_num_hiddens,
num_heads, dropout, use_bias))
def forward(self, X, valid_lens, *args):
# 因为位置编码值在-1和1之间,
# 因此嵌入值乘以嵌入维度的平方根进行缩放,
# 然后再与位置编码相加。
X = self.pos_encoding(self.embedding(X) * math.sqrt(self.num_hiddens))
self.attention_weights = [None] * len(self.blks)
for i, blk in enumerate(self.blks):
X = blk(X, valid_lens)
self.attention_weights[
i] = blk.attention.attention.attention_weights
return X
前向传播时经过嵌入层后要进行缩放,因为位置编码较小。
接下来实现解码器。
同样是先实现一个块。
这里注意初始化要传入一个块序号,目的是储存状态state,因为
python
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]
# 训练阶段,输出序列的所有词元都在同一时间处理,
# 因此state[2][self.i]初始化为None。
# 预测阶段,输出序列是通过词元一个接着一个解码的,
# 因此state[2][self.i]包含着直到当前时间步第i个块解码的输出表示
if state[2][self.i] is None:
key_values = X
else:
key_values = torch.cat((state[2][self.i], X), axis=1)
state[2][self.i] = key_values
if self.training:
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)
else:
dec_valid_lens = None
# 自注意力
X2 = self.attention1(X, key_values, key_values, 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
在这里突然想到一个问题,就是这个"加法和规范化"层为什么要设置3个,理应没有可学习的参数,只要设置一个就行,去问ai才知道其实规范化是有权重和偏移两个参数的,在规范化后还要进行权重和偏移的操作。
这个前向传播也很有的推敲,虽然不是循环神经网络,但是解码器生成词元还是一个个生成的,所以输入的X是上一步的输出。(预测模式的话)
如果是训练模式,一股脑塞入全部X,输入的X就是全部的实际状态,所以直接将当前步的实际状态state[2][i]改为X。
如果是预测模式,一个个塞入上一步的X,当前状态state就要改为原本的state与传入的X合并,得到当前步即以前的状态。
如果state还是None(第一个时间步),那么直接传入X;如果不是第一个时间步,state就是上一个时间步的输入,再将这一个时间步的输入拼接上去即可。
注意训练模式的X是一起传入的,也就是不会有第二个第三个时间步了!所以注释才说训练模式的state是None。
并且如果是训练模式,有效长度就要被设为一个递增的数字,因为训练模式是直接传入全部时间步,不能让模型直接看到未来的数据。
这个state[2]代表的是已经预测的词元,作为掩蔽多头自注意力的键值对,其查询是解码器的输入;编码器的输出上下文变量作为另一个多头自注意力的键值对。
python
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):
return [enc_outputs, enc_valid_lens, [None] * self.num_layers]
def forward(self, X, state):
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
return self.dense(X), state
@property
def attention_weights(self):
return self._attention_weights
这里和编码器差不多,需要注意初始化状态state这个函数,state分别是编码器输出、编码器有效长度、解码器每层的输入。
总之好难............