Multi-Head Attention详解

在这里插入图片描述
文中大部分内容以及图片来自:https://medium.com/@hunter-j-phillips/multi-head-attention-7924371d477a

当使用 multi-head attention 时,通常d_key = d_value =(d_model / n_heads),其中n_heads是头的数量。研究人员称,通常使用平行注意层代替全尺寸性,因为该模型能够“关注来自不同位置的不同表示子空间的信息”。

通过线性层传递输入

计算注意力的第一步是获得Q、K和V张量;它们分别是查询张量、键张量和值张量。它们是通过采用位置编码的嵌入来计算的,它将被记为X,同时将张量传递给三个线性层,它们被记为Wq, Wk和Wv。这可以从上面的详细图像中看到。

  • Q = XWq
  • K = XWk
  • V = XWv
    为了理解乘法是如何发生的,最好将每个组件分解成这个形状:
  • X的大小为(batch_size, seq_length, d_model)。例如,一批32个序列的长度为10,嵌入为512,其形状为(32,10,512)。
  • Wq,Wk和Wv的大小为(d_model,d_model)。按照上面的示例,它们的形状为(512,512)。

因此,可以更好地理解乘法的输出。每个重量矩阵同时在批处理中 broadcast 每个序列,以创建Q,K和V张量。

  • Q = XWq | (batch_size, seq_length, d_model) x (d_model, d_model) = (batch_size, seq_length, d_model)
  • K = XWk | (batch_size, seq_length, d_model) x (d_model, d_model) = (batch_size, seq_length, d_model)
  • V = XWv | (batch_size, seq_length, d_model) x (d_model, d_model) = (batch_size, seq_length, d_model)

下面的图片显示了Q, K和V是如何出现的。每个紫色盒子代表一个序列,每个橙色盒子是序列中的一个 token 或单词。灰色椭圆表示每个token 的嵌入。
在这里插入图片描述

下面的代码加载了Positional Encoding和Embeddings类。

# convert the sequences to integers
sequences = ["I wonder what will come next!","This is a basic example paragraph.","Hello, what is a basic split?"]# tokenize the sequences
tokenized_sequences = [tokenize(seq) for seq in sequences]# index the sequences 
indexed_sequences = [[stoi[word] for word in seq] for seq in tokenized_sequences]# convert the sequences to a tensor
tensor_sequences = torch.tensor(indexed_sequences).long()# vocab size
vocab_size = len(stoi)# embedding dimensions
d_model = 8# create the embeddings
lut = Embeddings(vocab_size, d_model) # look-up table (lut)# create the positional encodings
pe = PositionalEncoding(d_model=d_model, dropout=0.1, max_length=10)# embed the sequence
embeddings = lut(tensor_sequences)# positionally encode the sequences
X = pe(embeddings)
tensor([[[-3.45, -1.34,  4.12, -3.33, -0.81, -1.93, -0.28,  8.25],[ 7.36, -1.09,  2.32,  1.52,  3.50,  1.42,  0.46, -0.95],[-2.26,  0.53, -1.02,  1.49, -3.97, -2.19,  2.86, -0.59],[-3.87, -2.02,  1.46,  6.78,  0.88,  1.08, -2.97,  1.45],[ 1.12, -2.09,  1.19,  3.87, -0.00,  3.73, -0.88,  1.12],[-0.35, -0.02,  3.98, -0.20,  7.05,  1.55,  0.00, -0.83]],[[-4.27,  0.17, -2.08,  0.94, -6.35,  1.99,  5.23,  5.18],[-0.00, -5.05, -7.19,  3.27,  1.49, -7.11, -0.59,  0.52],[ 0.54, -2.33, -1.10, -2.02, -0.88, -3.15,  0.38,  5.26],[ 0.87, -2.98,  2.67,  3.32,  1.16,  0.00,  1.74,  5.28],[-5.58, -2.09,  0.96, -2.05, -4.23,  2.11, -0.00,  0.61],[ 6.39,  2.15, -2.78,  2.45,  0.30,  1.58,  2.12,  3.20]],[[ 4.51, -1.22,  2.04,  3.48,  1.63,  3.42,  1.21,  2.33],[-2.34,  0.00, -1.13,  1.51, -3.99, -2.19,  2.86, -0.59],[-4.65, -6.12, -7.08,  3.26,  1.50, -7.11, -0.59,  0.52],[-0.32, -2.97, -0.99, -2.05, -0.87, -0.00,  0.39,  5.26],[-0.12, -2.61,  2.77,  3.28,  1.17,  0.00,  1.74,  5.28],[-5.64,  0.49,  2.32, -0.00, -0.44,  4.06,  3.33,  3.11]]],grad_fn=<MulBackward0>)

此时,嵌入序列X的形状为(3,6,8)。有3个序列,包含6个标记,具有8维嵌入。

Wq、Wk和Wv的线性层可以使用nn.Linear(d_model, d_model)来创建。这将创建一个(8,8)矩阵,该矩阵将在跨每个序列的乘法期间广播。

Wq = nn.Linear(d_model, d_model)          # query weights (8,8)
Wk = nn.Linear(d_model, d_model)          # key weights   (8,8)
Wv = nn.Linear(d_model, d_model)          # value weights (8,8)Wq.state_dict()['weight']
tensor([[ 0.19,  0.34, -0.12, -0.22,  0.26, -0.06,  0.12, -0.28],[ 0.09,  0.22,  0.32,  0.11,  0.21,  0.03, -0.35,  0.31],[-0.34, -0.21, -0.11,  0.34, -0.28,  0.03,  0.26, -0.22],[-0.35,  0.11,  0.17,  0.21, -0.19, -0.29,  0.22,  0.20],[ 0.19,  0.04, -0.07, -0.02,  0.01, -0.20,  0.30, -0.19],[ 0.23,  0.15,  0.22,  0.26,  0.17,  0.16,  0.23,  0.18],[ 0.01,  0.06, -0.31,  0.19,  0.22,  0.08,  0.15, -0.04],[-0.11,  0.24, -0.20,  0.26, -0.01, -0.14,  0.29, -0.32]])

Wq的权重如上图所示。Wk和Wv形状相同,但权重不同。当X穿过每一个线性层时,它保持它的形状,但是现在Q, K和V已经被权值转换成唯一的张量。

Q = Wq(X) # (3,6,8)x(broadcast 8,8) = (3,6,8)
K = Wk(X) # (3,6,8)x(broadcast 8,8) = (3,6,8)
V = Wv(X) # (3,6,8)x(broadcast 8,8) = (3,6,8)Q
tensor([# sequence 0[[-3.13,  2.71, -2.07,  3.54, -2.25, -0.26, -2.80, -4.31],[ 1.70,  1.63, -2.90, -2.90,  1.15,  3.01,  0.49, -1.14],[-0.69, -2.38,  3.00,  3.09,  0.97, -0.98, -0.10,  2.16],[-3.52,  2.08,  2.36,  2.16, -2.48,  0.58,  0.33, -0.26],[-1.99,  1.18,  0.64, -0.45, -1.32,  1.61,  0.28, -1.18],[ 1.66,  2.46, -2.39, -0.97, -0.47,  1.83,  0.36, -1.06]],# sequence 1[[-3.13, -2.43,  3.85,  4.34, -0.60, -0.03,  0.04,  0.62],[-0.82, -2.67,  1.82,  0.89,  1.30, -2.65,  2.01,  1.56],[-1.42,  0.11, -1.40,  1.36, -0.21, -0.87, -0.88, -2.24],[-2.70,  1.88, -0.10,  1.95, -0.75,  2.54, -0.14, -1.91],[-2.67, -1.58,  2.46,  1.93, -1.78, -2.44, -1.76, -1.23],[ 1.23,  0.78, -1.93, -1.12,  1.07,  2.98,  1.82,  0.18]],# sequence 2[[-0.71,  1.90, -1.12, -0.97, -0.23,  3.54,  0.65, -1.39],[-0.87, -2.54,  3.16,  3.04,  0.94, -1.10, -0.10,  2.07],[-2.06, -3.30,  3.63,  2.39,  0.38, -3.87,  1.86,  1.79],[-2.00,  0.02, -0.90,  0.68, -1.03, -0.63, -0.70, -2.77],[-2.76,  1.90,  0.14,  2.34, -0.93,  2.38, -0.17, -1.75],[-1.82,  0.15,  1.79,  2.87, -1.65,  0.97, -0.21, -0.54]]],grad_fn=<ViewBackward0>)

Q K和V都是这个形状。和前面一样,每个矩阵是一个序列,每一行都是由嵌入表示的 token。

把Q, K, V分成多头

通过创建Q,K和V张量,现在可以通过将D_Model的视图更改为 (n_heads, d_key) 将它们分为各自的头部。N_heads可以是任意数字,但是使用较大的嵌入时,通常要执行8、10或12。请记住,d_key = (d_model / n_heads)。
在这里插入图片描述
在前面的图像中,每个 token 在单个维度中包含d_model嵌入。现在,这个维度被分成行和列来创建一个矩阵;每行是一个包含键的头。这可以从上面的图片中看到。

每个张量的形状变成:

  • (batch_size, seq_length, d_model) → (batch_size, seq_length, n_heads, d_key)

假设示例中选择了四个heads,则(3,6,8)张量将被分成(3,6,4,2)张量,其中有3个序列,每个序列中有6个tokens,每个标记中有4个heads,每个正面中有2个元素。

这可以通过view来实现,它可以用来添加和设置每个维度的大小。由于每个示例的批大小或序列数量是相同的,因此可以设置批大小。同样,在每个张量中,头的数量和键的数量应该是恒定的。-1可用于表示剩余值,即序列长度。

batch_size = Q.size(0)   
n_heads = 4
d_key = d_model//n_heads # 8/4 = 2# query tensor | -1 = query_length | (3, 6, 8) -> (3, 6, 4, 2)
Q = Q.view(batch_size, -1, n_heads, d_key)# value tensor | -1 = key_length | (3, 6, 8) -> (3, 6, 4, 2) 
K = K.view(batch_size, -1, n_heads, d_key)# value tensor | -1 = value_length | (3, 6, 8) -> (3, 6, 4, 2) 
V = V.view(batch_size, -1, n_heads, d_key)                   Q

下面是Q张量的例子。这3个序列中的每一个都有6个tokens,每个标记是一个tokens,每个标记有4个head(行)和2个keys。

tensor([# sequence 0[[[-3.13,  2.71],[-2.07,  3.54],[-2.25, -0.26],[-2.80, -4.31]],[[ 1.70,  1.63],[-2.90, -2.90],[ 1.15,  3.01],[ 0.49, -1.14]],[[-0.69, -2.38],[ 3.00,  3.09],[ 0.97, -0.98],[-0.10,  2.16]],[[-3.52,  2.08],[ 2.36,  2.16],[-2.48,  0.58],[ 0.33, -0.26]],[[-1.99,  1.18],[ 0.64, -0.45],[-1.32,  1.61],[ 0.28, -1.18]],[[ 1.66,  2.46],[-2.39, -0.97],[-0.47,  1.83],[ 0.36, -1.06]]],# sequence 1[[[-3.13, -2.43],[ 3.85,  4.34],[-0.60, -0.03],[ 0.04,  0.62]],[[-0.82, -2.67],[ 1.82,  0.89],[ 1.30, -2.65],[ 2.01,  1.56]],[[-1.42,  0.11],[-1.40,  1.36],[-0.21, -0.87],[-0.88, -2.24]],[[-2.70,  1.88],[-0.10,  1.95],[-0.75,  2.54],[-0.14, -1.91]],[[-2.67, -1.58],[ 2.46,  1.93],[-1.78, -2.44],[-1.76, -1.23]],[[ 1.23,  0.78],[-1.93, -1.12],[ 1.07,  2.98],[ 1.82,  0.18]]],# sequence 2[[[-0.71,  1.90],[-1.12, -0.97],[-0.23,  3.54],[ 0.65, -1.39]],[[-0.87, -2.54],[ 3.16,  3.04],[ 0.94, -1.10],[-0.10,  2.07]],[[-2.06, -3.30],[ 3.63,  2.39],[ 0.38, -3.87],[ 1.86,  1.79]],[[-2.00,  0.02],[-0.90,  0.68],[-1.03, -0.63],[-0.70, -2.77]],[[-2.76,  1.90],[ 0.14,  2.34],[-0.93,  2.38],[-0.17, -1.75]],[[-1.82,  0.15],[ 1.79,  2.87],[-1.65,  0.97],[-0.21, -0.54]]]], grad_fn=<ViewBackward0>)

为了继续,最好将序列长度和n个头(第二次和第三次)调换成以下形状

  • (batch_size, seq_length, n_heads, d_key) → (batch_size, n_heads, seq_length, d_key)

现在,每个序列被分成n_heads,每个头接收seq_length长度token中的d_key个元素,而不是d_model个。这达到了研究人员在不同位置关注来自不同表示子空间的信息的目的。

这个张量的可视化如下图所示。每个序列是紫色的,每个头是灰色的。在头部中,每个标记是一行d_key元素。
在这里插入图片描述

回到前面的例子,Q张量将从(3,6,4,2)转置到(3,4,6,2)。这个张量现在将表示3个序列,每个序列分为n_heads= 4,每个头包含 seq_length= 6个tokens,每个tokens有一个 d_key = 2元素键。

本质上,每个头部都包含每个序列 tokens 的副本,但它只有一个 d_key= 2的元素表示,而不是完整的d_model= 8的元素表示。这意味着每个序列同时在n_head= 4个不同的子空间中表示。

下面的代码使用permute来切换每个张量的第二轴和第三轴。

# query tensor | (3, 6, 4, 2) -> (3, 4, 6, 2) 
Q = Q.permute(0, 2, 1, 3)
# key tensor | (3, 6, 4, 2) -> (3, 4, 6, 2) 
K = K.permute(0, 2, 1, 3)
# value tensor | (3, 6, 4, 2) -> (3, 4, 6, 2) 
V = V.permute(0, 2, 1, 3)Q
tensor([# sequence 0[[[-3.13,  2.71],[ 1.70,  1.63],[-0.69, -2.38],[-3.52,  2.08],[-1.99,  1.18],[ 1.66,  2.46]],[[-2.07,  3.54],[-2.90, -2.90],[ 3.00,  3.09],[ 2.36,  2.16],[ 0.64, -0.45],[-2.39, -0.97]],[[-2.25, -0.26],[ 1.15,  3.01],[ 0.97, -0.98],[-2.48,  0.58],[-1.32,  1.61],[-0.47,  1.83]],[[-2.80, -4.31],[ 0.49, -1.14],[-0.10,  2.16],[ 0.33, -0.26],[ 0.28, -1.18],[ 0.36, -1.06]]],# sequence 1[[[-3.13, -2.43],[-0.82, -2.67],[-1.42,  0.11],[-2.70,  1.88],[-2.67, -1.58],[ 1.23,  0.78]],[[ 3.85,  4.34],[ 1.82,  0.89],[-1.40,  1.36],[-0.10,  1.95],[ 2.46,  1.93],[-1.93, -1.12]],[[-0.60, -0.03],[ 1.30, -2.65],[-0.21, -0.87],[-0.75,  2.54],[-1.78, -2.44],[ 1.07,  2.98]],[[ 0.04,  0.62],[ 2.01,  1.56],[-0.88, -2.24],[-0.14, -1.91],[-1.76, -1.23],[ 1.82,  0.18]]],# sequence 2[[[-0.71,  1.90],[-0.87, -2.54],[-2.06, -3.30],[-2.00,  0.02],[-2.76,  1.90],[-1.82,  0.15]],[[-1.12, -0.97],[ 3.16,  3.04],[ 3.63,  2.39],[-0.90,  0.68],[ 0.14,  2.34],[ 1.79,  2.87]],[[-0.23,  3.54],[ 0.94, -1.10],[ 0.38, -3.87],[-1.03, -0.63],[-0.93,  2.38],[-1.65,  0.97]],[[ 0.65, -1.39],[-0.10,  2.07],[ 1.86,  1.79],[-0.70, -2.77],[-0.17, -1.75],[-0.21, -0.54]]]], grad_fn=<PermuteBackward0>)

虽然拥有完整的视图很好,但通过检查单个序列更容易理解。

很容易在这个序列中看到四个heads。每个头包含六行,这是 tokens,每行有两个元素,这是键。这显示了如何将序列拆分为四个子空间,以创建同一序列的不同表示。

# select the first sequence from the Query tensor
Q[0]
tensor([# head 0[[-3.13,  2.71],[ 1.70,  1.63],[-0.69, -2.38],[-3.52,  2.08],[-1.99,  1.18],[ 1.66,  2.46]],# head 1[[-2.07,  3.54],[-2.90, -2.90],[ 3.00,  3.09],[ 2.36,  2.16],[ 0.64, -0.45],[-2.39, -0.97]],# head 2[[-2.25, -0.26],[ 1.15,  3.01],[ 0.97, -0.98],[-2.48,  0.58],[-1.32,  1.61],[-0.47,  1.83]],# head 3[[-2.80, -4.31],[ 0.49, -1.14],[-0.10,  2.16],[ 0.33, -0.26],[ 0.28, -1.18],[ 0.36, -1.06]]], grad_fn=<SelectBackward0>)

计算注意力

在这里插入图片描述
将Q, K和V分成多个头,现在可以计算Q和K的标量点积。上面的等式表明,第一步是执行张量乘法。然而,K必须先转置。

接下来,为了清晰起见,每个张量的seq长度形状将通过其各自的张量,Q_length,K_length 或 V_length 来知道

  • Q has a shape of (batch_size, n_heads, Q_length, d_key)
  • K has a shape of (batch_size, n_heads, K_length, d_key)
  • V has a shape of (batch_size, n_heads, V_length, d_key)

K最右边的两个维度必须调换,以改变形状为(batch_size, n_heads, d_key, K_length)。

现在, Q K T QK^T QKT的输出是

  • (batch_size, n_heads, Q_length, d_key) x (batch_size, n_heads, d_key, K_length) = (batch_size, n_heads, Q_length, K_length)

每个张量中的相应序列将相互乘法。Q中的第一个序列将乘以K中的第一个序列,Q中的第二个序列与K中的第二个序列相乘。当这些序列相互相乘时,每个头将在相反的张量中与相应的头相乘。Q的第一个序列的第一个头将与K的第一个序列的第一个头相乘,Q的第一个序列的第二个头与K的第一个序列的第二个头相乘。在乘以这些头时,Q头中每个形状为(Q_length,d_key)的token与K头中的每个token相乘,形状为(d_key,K_length)。结果是一个(Q-length,K_length)矩阵,显示每个单词与包括自身在内的所有其他单词的强度。这就是“self-attention”这个名字的来源,因为模型通过将单词乘以另一个自身表示来发现哪些单词与序列最相关。

Q K T QK^T QKT由 d_key 缩放,以帮助使softmax函数在下一步的输出不那么集中在0和1附近。在未缩放分布中,接近0和1的值更接近分布的中间。

继续这个例子,缩放后的点积的输出形状为(3, 4, 6, 2) x (3, 4, 2, 6) = (3, 4, 6, 6)。

# calculate scaled dot product
scaled_dot_prod = torch.matmul(Q, K.permute(0, 1, 3, 2)) / math.sqrt(d_key) # (batch_size, n_heads, Q_length, K_length)

这个张量然后通过softmax函数来创建一个概率分布。请注意softmax是如何应用于每个头部中每个矩阵的每一行的。softmax维度可以设置为-1或3,因为两者都表示形状中最右边的维度,即键。

# apply softmax to get context for each token and others
attn_probs = torch.softmax(scaled_dot_prod, dim=-1) # (batch_size, n_heads, Q_length, K_length)

这些注意概率可以使用来自matplotlib的imshow可视化。可以在附录中找到同时显示序列的所有头部的函数,称为display_attention。白色更接近于1,黑色更接近于0。

# sequence 0
display_attention(["i", "wonder", "what", "will", "come", "next"], ["i", "wonder", "what", "will", "come", "next"], attn_probs[0], 4, 2, 2)

在这里插入图片描述

# sequence 1
display_attention(["this", "is", "a", "basic", "example", "paragraph"], ["this", "is", "a", "basic", "example", "paragraph"], attn_probs[1], 4, 2, 2)

在这里插入图片描述

# sequence 2
display_attention(["hello", "what", "is", "a", "basic", "split"], ["hello", "what", "is", "a", "basic", "split"], attn_probs[2], 4, 2, 2)

在这里插入图片描述

它们演示了每个query(row)和key(column)之间的关系。序列中单词之间的每个交集都代表了关系的强度。由于这些值是从随机权重生成的,因此它们目前没有显示任何有效的关系。下图展示了编码器经过训练后这些矩阵的样子。
在这里插入图片描述
计算出这些概率后,下一步是将它们与V张量相乘,以创建这些分布的总结。每个单词的上下文本质上是聚合的。
在这里插入图片描述

# multiply attention and values to get reweighted values
A = torch.matmul(attn_probs, V) # (batch_size, n_heads, Q_length, d_key)

下面是这个示例的每个步骤的图表。
在这里插入图片描述

这里到底发生了什么:好吧,Q和K都是同一序列的表示,分为不同头部的query和key组件。这计算了序列中每个单词与序列中所有其他单词之间的关系。这发生在 n_heads 子空间中。计算每个单词的query表示和每个单词的key表示之间的点积。这表示每个单词和其他单词之间的“强度”或“重量”。通过训练,这种力量将帮助模型理解哪些单词之间应该有更高的“权重”;这将表明哪些单词对上下文和预测最重要。再次强调,query与key相乘,以在每个token和序列中的所有其他token之间生成权重。

softmax张量中的每一行都表示一个token与同一序列中的其他token之间的关系。在V中,每一列是序列的一个表示。将两个张量相乘以重新加权值,并计算每个头或子空间中每个token的最重要上下文的摘要。

下面的图表显示了序列中单个头部的 self-attention。

在这里插入图片描述

Passing It Through the Output Layer

此时,在通过最后的线性层(即多头注意机制中的最后一层)之前,这些头部可以重新连接在一起。

串联将反转最初执行的分割。第一步是n_heads和Q_length的转置。第二步是将 n_heads 和 d_key 连接起来,得到 d_model。

一旦完成,A将具有 (batch_size, Q_length, d_model) 的形状。

# transpose from (3, 4, 6, 2) -> (3, 6, 4, 2)
A = A.permute(0, 2, 1, 3).contiguous()# reshape from (3, 6, 4, 2) -> (3, 6, 8) = (batch_size, Q_length, d_model)
A = A.view(batch_size, -1, n_heads*d_key)A 
tensor([[[ 0.41, -0.71,  0.63, -0.22,  0.79, -3.58,  0.11,  1.71],[-0.15,  0.93,  0.50, -0.40, -0.43, -1.36,  0.11,  1.64],[-1.05, -1.58, -0.14, -1.42,  0.12,  0.21, -0.54, -0.52],[ 0.31, -0.65, -0.17, -1.33,  0.84, -3.78, -0.02,  0.41],[ 0.58, -0.83, -0.56, -1.17,  0.83, -3.70,  0.11,  1.65],[-0.17,  0.99,  0.58, -0.32,  0.65, -3.14,  0.11,  1.61]],[[-1.08, -1.93, -1.62,  3.69,  0.62, -0.34, -1.88, -2.31],[-1.17, -1.84, -1.76,  1.62,  0.60, -0.40, -2.56, -1.59],[-1.29, -0.52, -0.89, -1.06,  0.31,  0.07,  0.90,  1.69],[-0.90, -0.07, -1.43,  1.97,  1.16, -1.30,  0.73,  1.51],[-1.09, -1.92, -1.61,  2.89, -0.21,  0.92,  0.55,  1.32],[-0.92, -1.14, -0.95, -1.66,  0.28, -0.70, -0.91, -0.78]],[[-0.27,  0.87, -1.54, -3.73,  1.00, -1.33, -0.80,  0.07],[-1.13, -1.86, -1.22,  0.61, -0.47,  0.15, -0.10, -3.30],[-1.04, -1.82, -1.48,  0.91, -0.70,  0.45, -1.37, -0.49],[-0.37,  0.57, -1.24, -1.56, -0.29,  0.44, -0.97,  0.25],[-0.22,  1.10, -0.89, -0.33,  1.02, -1.33, -0.80,  0.19],[-0.37,  0.62, -1.02,  0.15,  0.80, -1.09, -0.37, -0.42]]],grad_fn=<ViewBackward0>)

最后一步是让A通过Wo,它的形状为 (d_model, d_model)。同样,权重张量在批处理中的每个序列中广播。最后的输出保持其形状
在这里插入图片描述

Wo = nn.Linear(d_model, d_model)# (3, 6, 8) x (broadcast 8, 8) = (3, 6, 8)
output = Wo(A)              
tensor([[[-0.39, -0.45, -0.17,  0.18, -0.24, -1.68, -0.35, -0.56],[ 0.38,  0.02,  0.28, -0.42, -0.70, -0.81,  0.05,  0.03],[ 1.01, -0.72,  0.12,  0.18,  1.20, -0.29,  1.10, -0.59],[-0.50, -0.84, -0.07,  0.22,  0.49, -1.58,  0.13, -0.90],[-0.15, -0.95, -0.35,  0.17,  0.15, -1.65, -0.27, -0.79],[-0.47, -0.04,  0.15,  0.03, -0.83, -1.24, -0.04, -0.15]],[[-1.29, -0.85, -1.02,  1.56,  0.32, -0.08, -0.14,  0.40],[-0.45, -1.19, -0.70,  1.23,  0.75, -0.42,  0.46, -0.38],[ 1.33, -0.58, -0.34,  0.10, -0.13,  0.15,  0.44,  0.38],[-0.42, -0.32, -0.97,  0.89, -1.19,  0.01, -0.66,  1.11],[ 0.66, -0.75, -1.36,  0.73, -0.69,  0.47, -0.79,  1.29],[ 0.60, -1.03,  0.01,  0.29,  1.20, -0.50,  1.07, -0.78]],[[ 0.61, -0.66,  0.54, -0.06,  0.97, -0.68,  1.30, -1.08],[-0.22, -1.02, -0.38,  0.62,  1.46,  0.30,  0.74,  0.10],[ 0.67, -1.23, -0.65,  0.47,  0.58, -0.18,  0.31, -0.09],[ 0.94, -0.43,  0.30, -0.22,  0.40, -0.23,  0.78, -0.36],[-0.46, -0.03,  0.16,  0.37, -0.23, -0.55,  0.34, -0.11],[-0.54, -0.15, -0.03,  0.46, -0.06, -0.29,  0.26,  0.13]]],grad_fn=<ViewBackward0>)

该输出将传递到下一层,其中包括残差加法和layer normalization。这些将在后面的文章中讨论。

Multi-Head Attention in Transformers

在解释了多头注意力的每个组件之后,实现就很简单了,并且使用了前面列出的相同组件。唯一增加的是一个dropout层。

代码中有一个掩码的实现,但现在可以忽略它。它不会对实现之后的示例产生影响。当描述编码器和解码器时,将对此进行解释。

请注意,在这个实现中,Q、K和V张量是同时分割和排列的,这与上面的实现不同。

class MultiHeadAttention(nn.Module):def __init__(self, d_model: int = 512, n_heads: int = 8, dropout: float = 0.1):"""Args:d_model:      dimension of embeddingsn_heads:      number of self attention headsdropout:      probability of dropout occurring"""super().__init__()assert d_model % n_heads == 0            # ensure an even num of headsself.d_model = d_model                   # 512 dimself.n_heads = n_heads                   # 8 headsself.d_key = d_model // n_heads          # assume d_value equals d_key | 512/8=64self.Wq = nn.Linear(d_model, d_model)    # query weightsself.Wk = nn.Linear(d_model, d_model)    # key weightsself.Wv = nn.Linear(d_model, d_model)    # value weightsself.Wo = nn.Linear(d_model, d_model)    # output weightsself.dropout = nn.Dropout(p=dropout)     # initialize dropout layer  def forward(self, query: Tensor, key: Tensor, value: Tensor, mask: Tensor = None):"""Args:query:         query vector         (batch_size, q_length, d_model)key:           key vector           (batch_size, k_length, d_model)value:         value vector         (batch_size, s_length, d_model)mask:          mask for decoder     Returns:output:        attention values     (batch_size, q_length, d_model)attn_probs:    softmax scores       (batch_size, n_heads, q_length, k_length)"""batch_size = key.size(0)                  # calculate query, key, and value tensorsQ = self.Wq(query)                       # (32, 10, 512) x (512, 512) = (32, 10, 512)K = self.Wk(key)                         # (32, 10, 512) x (512, 512) = (32, 10, 512)V = self.Wv(value)                       # (32, 10, 512) x (512, 512) = (32, 10, 512)# split each tensor into n-heads to compute attention# query tensorQ = Q.view(batch_size,                   # (32, 10, 512) -> (32, 10, 8, 64) -1,                           # -1 = q_lengthself.n_heads,              self.d_key).permute(0, 2, 1, 3)         # (32, 10, 8, 64) -> (32, 8, 10, 64) = (batch_size, n_heads, q_length, d_key)# key tensorK = K.view(batch_size,                   # (32, 10, 512) -> (32, 10, 8, 64) -1,                           # -1 = k_lengthself.n_heads,              self.d_key).permute(0, 2, 1, 3)         # (32, 10, 8, 64) -> (32, 8, 10, 64) = (batch_size, n_heads, k_length, d_key)# value tensorV = V.view(batch_size,                   # (32, 10, 512) -> (32, 10, 8, 64) -1,                           # -1 = v_lengthself.n_heads, self.d_key).permute(0, 2, 1, 3)         # (32, 10, 8, 64) -> (32, 8, 10, 64) = (batch_size, n_heads, v_length, d_key)# computes attention# scaled dot product -> QK^{T}scaled_dot_prod = torch.matmul(Q,        # (32, 8, 10, 64) x (32, 8, 64, 10) -> (32, 8, 10, 10) = (batch_size, n_heads, q_length, k_length)K.permute(0, 1, 3, 2)) / math.sqrt(self.d_key)      # sqrt(64)# fill those positions of product as (-1e10) where mask positions are 0if mask is not None:scaled_dot_prod = scaled_dot_prod.masked_fill(mask == 0, -1e10)# apply softmax attn_probs = torch.softmax(scaled_dot_prod, dim=-1)# multiply by values to get attentionA = torch.matmul(self.dropout(attn_probs), V)       # (32, 8, 10, 10) x (32, 8, 10, 64) -> (32, 8, 10, 64)# (batch_size, n_heads, q_length, k_length) x (batch_size, n_heads, v_length, d_key) -> (batch_size, n_heads, q_length, d_key)# reshape attention back to (32, 10, 512)A = A.permute(0, 2, 1, 3).contiguous()              # (32, 8, 10, 64) -> (32, 10, 8, 64)A = A.view(batch_size, -1, self.n_heads*self.d_key) # (32, 10, 8, 64) -> (32, 10, 8*64) -> (32, 10, 512) = (batch_size, q_length, d_model)# push through the final weight layeroutput = self.Wo(A)                                 # (32, 10, 512) x (512, 512) = (32, 10, 512) return output, attn_probs                           # return attn_probs for visualization of the scores

现在,可以将它与嵌入层和位置编码层一起使用,以生成与本文类似的输出。将使用相同的示例,但将使用该类生成不同的输出。记住,这假设Embeddings和PositionalEncoding模块与MultiHeadAttention模块一起加载。

torch.set_printoptions(precision=2, sci_mode=False)# convert the sequences to integers
sequences = ["I wonder what will come next!","This is a basic example paragraph.","Hello, what is a basic split?"]# tokenize the sequences
tokenized_sequences = [tokenize(seq) for seq in sequences]# index the sequences 
indexed_sequences = [[stoi[word] for word in seq] for seq in tokenized_sequences]# convert the sequences to a tensor
tensor_sequences = torch.tensor(indexed_sequences).long()# vocab size
vocab_size = len(stoi)# embedding dimensions
d_model = 8# create the embeddings
lut = Embeddings(vocab_size, d_model) # look-up table (lut)# create the positional encodings
pe = PositionalEncoding(d_model=d_model, dropout=0.1, max_length=10)# embed the sequence
embeddings = lut(tensor_sequences)# positionally encode the sequences
X = pe(embeddings)# set the n_heads
n_heads = 4# create the attention layer
attention = MultiHeadAttention(d_model, n_heads, dropout=0.1)# pass X through the attention layer three times to create Q, K, and V
output, attn_probs = attention(X, X, X, mask=None)output

正如预期的那样,输出的形状与输入的形状相同,即(3,6,8)。

tensor([[[-0.54,  0.58, -0.86,  0.72,  0.73,  0.26,  0.22, -1.31],[-0.88, -0.50,  0.06, -1.04,  0.79,  0.05,  0.78, -1.34],[-2.34,  0.46,  0.84,  0.15,  1.22,  1.25,  1.99, -1.55],[-2.69,  0.17,  0.57,  0.20,  1.44,  1.89,  1.99, -1.95],[-0.00, -1.09,  0.21, -0.90,  1.34, -0.32, -0.30, -0.81],[-1.25, -0.88,  0.85, -0.05,  1.54,  0.11,  0.77, -1.59]],[[-0.36, -0.52, -0.66, -0.71, -0.46,  0.83,  0.68,  0.19],[-0.45, -0.04, -0.76, -0.12,  0.21,  1.05,  0.54, -0.12],[-0.97,  0.15, -0.32, -0.14, -0.07,  0.96,  1.07, -0.42],[ 0.06, -0.69, -0.71, -0.72,  0.04,  0.32,  0.20,  0.13],[-0.40,  0.14, -0.48,  0.36, -0.85,  0.72,  0.77,  0.45],[-0.17, -0.69, -0.45, -0.98, -0.15,  0.14,  0.52, -0.04]],[[ 0.57,  0.26, -0.24,  0.44,  0.08, -0.66, -0.37, -0.23],[-0.33,  0.75,  0.58,  0.06,  0.32, -0.63,  0.55, -0.10],[-0.50,  0.46, -0.64,  0.87,  0.65,  0.85,  0.29, -0.60],[ 1.54,  0.43,  1.51,  0.09, -0.19, -2.58, -0.84,  1.40],[ 1.46, -0.38, -0.51, -0.06,  0.04, -0.83, -1.10,  1.08],[-0.28,  1.85,  0.19,  1.38, -0.69, -0.01,  0.55, -0.11]]],grad_fn=<ViewBackward0>)

来自注意力的概率也可以使用注意力问题来预览。下面是第一个序列的heads的注意力分布。

display_attention(["i", "wonder", "what", "will", "come", "next"], ["i", "wonder", "what", "will", "come", "next"], attn_probs[0], 4, 2, 2)

在这里插入图片描述

Supplementary Images of Attention

在这里插入图片描述
这是多头注意力计算的另一种view。下图是如何在同一序列的不同表示之间计算softmax的示例。
在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/710611.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

01-Vue2 介绍与指令的使用

1. Vue核心 1.1. Vue简介 1.1.1. 官网 中文官网Vue.js - 渐进式 JavaScript 框架 | Vue.js (vuejs.org)https://cn.vuejs.org/ 英文官网Vue.js - The Progressive JavaScript Framework | Vue.js (vuejs.org)https://vuejs.org/ 1.1.2. 介绍与描述 VUE是构建于用户界面的渐进…

靶机渗透之sar

Name: Sar: 1Date release: 15 Feb 2020Author: LoveSeries: Sar Download: https://drive.google.com/open?id1AFAmM21AwiAEiVFUA0cSr_GeAYaxd3lQ 对于vulnhub中的靶机&#xff0c;我们都需先下载镜像&#xff0c;然后导入VM&#xff0c;并将网络连接改为NAT模式。首先我们…

UDP数据报套接字编程入门

目录 1.TCP和UDP的特点及区别 1.1TCP的特点 1.2UDP的特点 1.3区别 2.UDP Socket的api的介绍 2.1DatagramSocket API 2.2DatagramPacket API 3.回显客户端与服务器 3.1回显服务器 3.1.1UdpEchoServer类的创建 3.1.2服务器的运行方法start() 3.1.3main部分 3.1.4.完整…

C# CAD PaletteSet.Style各种外观和行为样式

ps.Style 是 Autodesk.AutoCAD.Windows.PaletteSet 类的一个属性&#xff0c;用于定义调色板集&#xff08;PaletteSet&#xff09;的各种外观和行为样式。它可以是 PaletteSetStyles 枚举类型的组合值 PaletteSetStyles 枚举中包含以下一些选项&#xff1a; NameEditable&am…

统计子矩阵

一、题目描述 P8783 [蓝桥杯 2022 省 B] 统计子矩阵 二、算法简析 2.1 二维前缀和 我们知道&#xff0c;只要确定了矩阵的左上顶点和右下顶点&#xff0c;一个矩阵就被固定了。因此&#xff0c;我们可以遍历这两个顶点&#xff0c;达到遍历所有子矩阵的目的&#xff0c;复杂…

在微服务整合dubbo,以为微服务版的若依为例

在微服务整合dubbo&#xff0c;以为微服务版的若依为例 一、环境二、整合过程1、父模块依赖2、生产者3、消费者 三、修改若依的服务调用方式为dubbo1、改造系统模块2、改造认证授权中心 四、整合过程遇到的问题1、出现循环引用2、出现依赖冲突3、启动出现端口号被占用4、出现某…

UVa11726 Crime Scene

题目链接 UVa11726 - Crime Scene 题意 给定n&#xff08;n≤100&#xff09;个物体&#xff0c;每个物体都是一个圆或者k&#xff08;k≤10&#xff09;边形&#xff0c;用长度尽量小的绳子把它们包围起来。 分析 孟加拉国Manzurur Rahman Khan (Sidky)大神出的难题&#xff…

MySQL 核心模块揭秘 | 07 期 | 二阶段提交 (1) prepare 阶段

二阶段提交的 prepare 阶段&#xff0c;binlog 和 InnoDB 各自会有哪些动作&#xff1f; 本文基于 MySQL 8.0.32 源码&#xff0c;存储引擎为 InnoDB。 1. 二阶段提交 二阶段提交&#xff0c;顾名思义&#xff0c;包含两个阶段&#xff0c;它们是&#xff1a; prepare 阶段。…

springboot-基础-eclipse配置+helloword示例

备份笔记。所有代码都是2019年测试通过的&#xff0c;如有问题请自行搜索解决&#xff01; 下一篇&#xff1a;springboot-基础-添加model和controller的简单例子常用注解含义 目录 配置helloword示例新建项目创建文件 配置 spring boot官方有定制版eclipse&#xff0c;也就是…

BUUCTF AWD-Test1

打开靶场是这个有些简陋的界面。 随便点点&#xff0c;找到这个东西。 看到ThinkPHP&#xff0c;思路瞬间清晰&#xff0c;老熟人了。这个就是ThinkPHP漏洞。根据版本我们去找一下poc。 /index.php/?sIndex/\think\View/display&content%22%3C?%3E%3C?php%20phpinfo();…

服务端向客户端推送数据的实现方案

在日常的开发中&#xff0c;我们经常能碰见服务端需要主动推送给客户端数据的业务场景&#xff0c;比如数据大屏的实时数据&#xff0c;比如消息中心的未读消息&#xff0c;比如聊天功能等等。 本文主要介绍SSE的使用场景和如何使用SSE。 服务端向客户端推送数据的实现方案有哪…

MySQL 自增列解析(Auto_increment)

MySQL数据库为列提供了一种自增属性&#xff0c;当列被定义为自增时。Insert语句对该列即使不提供值&#xff0c;MySQL也会自动为该列生成递增的唯一标识&#xff0c;因此这个特性广泛用于主键的自动生成。 一、自增列的用法 自增列具有自动生成序列值&#xff0c;整型&#…

MYSQL04高级_逻辑架构剖析、查询缓存、解析器、优化器、执行器、存储引擎

文章目录 ①. 逻辑架构剖析②. 服务层 - 查询缓存③. 服务层 - 解析器④. 服务层 - 优化器⑤. 服务层 - 执行器⑥. MySQL8执行原理 ①. 逻辑架构剖析 ①. 服务器处理客户端请求 ②. 连接层 系统(客户端)访问MySQL服务器前,做的第一件事就是建立TCP连接经过三次握手建立连接成…

Linux使用C语言实现通过互斥锁限制对共享资源的访问

互斥锁限制共享资源的访问 主线程中有两个线程&#xff0c;分别输出信息。 #include <stdio.h> #include <pthread.h> #include <unistd.h>int g_data0;void* fun1(void *arg) {printf("t1&#xff1a;%ld thread is create\n", (unsigned long)…

稀疏图带负边的全源最短路Johnson算法

BellmanFord算法 Johnson算法解决的问题 带负权的稀疏图的全源最短路 算法流程 重新设置的每条边的权重都大于或等于0&#xff0c;跑完Djikstra后得到的全源最短路&#xff0c;记得要还原&#xff0c;即&#xff1a;f(u,v) d(u,v) - h[u] h[v] 例题

45、WEB攻防——通用漏洞PHP反序列化POP链构造魔术方法原生类

文章目录 序列化&#xff1a;将java、php等代码中的对象转化为数组或字符串等格式。代表函数serialize()&#xff0c;将一个对象转换成一个字符&#xff1b;反序列化&#xff1a;将数组或字符串等格式还成对象。代表函数unserialize()&#xff0c;将字符串还原成一个对象。 P…

MWC 2024丨Smart Health搭载高通Aware平台—美格发布智能健康看护解决方案,开启健康管理新体验

2月29日&#xff0c;在MWC 2024世界移动通信大会上&#xff0c;全球领先的无线通信模组及解决方案提供商——美格智能正式发布了新一代Cat.1模组SLM336Q&#xff0c;是中低速物联网应用场景的高性价比之选。本次还发布了首款搭载高通Aware™平台的智能看护解决方案MC303&#x…

[万字长文] 从 Vue 3 的项目模板学习 tsconfig 配置

文章目录 一、tsconfig.json 的作用二、基本介绍三、Vue 3 的 tsconfig.json 的结构分析1. 总配置 tsconfig.json2. Web 侧 tsconfig.app.jsona. 继承基础配置b. 包含和排除的文件c. 编译器选项 3. 测试 tsconfig.vitest.jsona. 继承的基础配置b. 包含和排除的文件c. 编译器选项…

OD(13)之Mermaid饼图和象限图

OD(13)之Mermaid饼图和象限图使用详解 Author: Once Day Date: 2024年2月29日 漫漫长路才刚刚开始… 全系列文章可参考专栏: Mermaid使用指南_Once_day的博客-CSDN博客 参考文章: 关于 Mermaid | Mermaid 中文网 (nodejs.cn)Mermaid | Diagramming and charting tool‍‌⁡…

FPGA-学会使用vivado中的存储器资源RAM(IP核)

问题 信号源(例如ADC)以1us一个的速率产生12位的数据现要求获得连续1ms内的数据,通过串口以115200的波特率发到电脑。 分析 数据量是1000个 数据速率不匹配 数据内容未知 数据总数据量有限 数据的使用速度低于数据的产生速度 数据生产和消耗的位宽 数据量相对较…