手撕Attention
第一重境界
最简单的计算 Attention(Q, K, V) = softmax(Q*K.T/sqrt(hidden_dim))*V
import math
import torch
import torch.nn as nn
class SelfAttentionV1(nn.Module):
def __init__(self, hidden_dim:int= 768) -> None:
super().__init__()
self.hidden_dim = hidden_dim
self.query_proj = nn.Linear(hidden_dim, hidden_dim)
self.key_proj = nn.Linear(hidden_dim, hidden_dim)
self.value_proj = nn.Linear(hidden_dim, hidden_dim)
def forward(self, X):
# X: (batch_size, seq_len, hidden_dim)
Q = self.query_proj(X)
K = self.key_proj(X)
V = self.value_proj(X) # Q, K, V: (batch_size, seq_len, hidden_dim)
attention_weight = torch.softmax(Q @ K.transpose(-2, -1) / math.sqrt(self.hidden_dim), dim=-1) # K变成 (batch_size, hidden_dim, seq_len), attention_weight: (batch_size, seq_len, seq_len)
output = attention_weight @ V # output: (batch_size, seq_len, hidden_dim)
return output
第二重境界
用一个大矩阵同时生成Q, K, V
# 大投影矩阵
class SelfAttentionV2(nn.Module):
def __init__(self, hidden_dim:int= 768) -> None:
super().__init__()
self.hidden_dim = hidden_dim
self.proj = nn.Linear(hidden_dim, hidden_dim*3) # 用一个proj,提高速度
def forward(self, X):
QKV = self.proj(X) # QKV: (batch_size, seq_len, hidden_dim*3)
Q, K, V = torch.split(QKV, self.hidden_dim, dim=-1)
attention_weight = torch.softmax(Q @ K.transpose(-2, -1) / math.sqrt(self.hidden_dim), dim=-1)
output = attention_weight @ V
return output
第三重境界
attention_mask作用:把短句子多余的部分mask掉
attention_mask作用于Q*K.T/sqrt(hidden_dim)之后
dropout作用于softmax(Q*K.T/sqrt(hidden_dim))之后,因为dropout是对概率进行操作的
# 1. drop_out位置
# 2. attention_mask
class SelfAttentionV3(nn.Module):
def __init__(self, hidden_dim:int= 768, dropout_rate=0.1) -> None:
super().__init__()
self.hidden_dim = hidden_dim
self.proj = nn.Linear(hidden_dim, hidden_dim*3)
self.attention_dropout = nn.Dropout(dropout_rate)
def forward(self, X, attention_mask = None): # attention_mask: (batch_size, seq_len, seq_len)
QKV = self.proj(X)
Q, K, V = torch.split(QKV, self.hidden_dim, dim=-1)
attention_weight = Q @ K.transpose(-2, -1) / math.sqrt(self.hidden_dim)
if attention_mask is not None:
attention_weight = attention_weight.masked_fill(attention_mask==0, float('-inf')) # 在Q@K.T之后,softmax之前加attention_mask
attention_weight = torch.softmax(attention_weight, dim=-1)
attention_weight = self.attention_dropout(attention_weight) # 对softmax之后的attention_weight做dropout
output = attention_weight @ V
return output
输出:
if __name__ == "__main__":
X = torch.randn(3, 2, 4)
net1 = SelfAttentionV1(hidden_dim=4)
net2 = SelfAttentionV2(hidden_dim=4)
output1 = net1(X)
output2 = net2(X)
print(output1.shape)
print(output2.shape)
X = torch.randn(3, 4, 2)
mask = torch.tensor([
[1, 1, 1, 0],
[1, 1, 0, 0],
[1, 0, 0, 0],
]) # (batch_size, seq_len)
mask = mask.unsqueeze(dim=1).repeat(1, 4, 1) # repeat的参数(重复1次, 重复4次, 重复1次) (batch_size, seq_len, seq_len)
net3 = SelfAttentionV3(hidden_dim=2)
output3 = net3(X)
print(output3.shape)
第四重境界 Multi-Head Attention
Multi-Head Attention:将hidden_dim分成多个头,每个头独立计算attention,最后合并结果
class SelfAttentionV4(nn.Module):
def __init__(self, hidden_dim:int= 768, num_heads=0, dropout_rate=0.1) -> None:
super().__init__()
self.hidden_dim = hidden_dim
self.num_heads = num_heads
self.head_dim = hidden_dim // num_heads
self.proj = nn.Linear(hidden_dim, hidden_dim*3)
self.out_proj = nn.Linear(hidden_dim, hidden_dim)
self.attention_dropout = nn.Dropout(dropout_rate)
def forward(self, X, attention_mask = None):
batch_size, seq_len, _ = X.shape
QKV = self.proj(X)
Q, K, V = torch.split(QKV, self.hidden_dim, dim=-1)
# 重塑为多头格式: (batch_size, seq_len, num_heads, head_dim)
Q = Q.view(batch_size, seq_len, self.num_heads, self.head_dim)
K = K.view(batch_size, seq_len, self.num_heads, self.head_dim)
V = V.view(batch_size, seq_len, self.num_heads, self.head_dim)
# 转置为: (batch_size, num_heads, seq_len, head_dim)
Q = Q.transpose(1, 2)
K = K.transpose(1, 2)
V = V.transpose(1, 2)
attention_weight = Q @ K.transpose(-2, -1) / math.sqrt(self.hidden_dim)
if attention_mask is not None:
# 扩展mask维度以匹配多头: (batch_size, 1, seq_len, seq_len)
attention_mask = attention_mask.unsqueeze(1)
attention_weight = attention_weight.masked_fill(attention_mask==0, float('-inf'))
attention_weight = torch.softmax(attention_weight, dim=-1)
attention_weight = self.attention_dropout(attention_weight)
output = attention_weight @ V
# 合并多头结果
output = output.transpose(1, 2) # (batch_size, seq_len, num_heads, head_dim)
output = output.contiguous().view(batch_size, seq_len, self.hidden_dim) # (batch_size, seq_len, hidden_dim)
return output
# 测试第四重境界:Multi-Head Attention
X = torch.randn(2, 8, 12) # (batch_size=2, seq_len=8, hidden_dim=12)
mask = torch.ones(2, 8, 8) # 全1的mask,表示所有位置都有效
net4 = SelfAttentionV4(hidden_dim=12, num_heads=4) # 12维分成4个头,每个头3维
output4 = net4(X, attention_mask=mask)
print(f"Multi-Head Attention output shape: {output4.shape}") # torch.Size([2, 8, 12])
评论区已延迟加载滑到评论区附近时会自动加载,也可以现在手动打开。



