Transformer 论文: 1706.attention is all you need!
唐宇迪解读transformer:transformer2021年前,从NLP活到CV的过程
综述:2110.Transformers in Vision: A Survey
代码讲解1: Transformer 模型详解及代码实现 - 进击的程序猿 - 知乎
代码讲解2:: Transformer代码解读(Pytorch) - 神洛的文章 - 知乎
一、 结构概述
图1 原论文总框图
输入:词向量(embedding)
位置编码模块:给输入的词的在全局的位置信息
多头注意力层:它是有多个单层注意力层构成
正则化层:
masked-head attention
图2 attention的计算方式(向量表示方法)
其他论文对其结构解读
2110.Transformers in Vision: A Survey
图2 自注意层如何使用在图像特征提取中
图3:Transformer模型的架构概述
该模型最初是为语言翻译任务开发的,其中一种语言的输入序列需要转换成另一种语言的输出序列。
Transformer编码器(中间行)对输入语言序列进行操作,并在将其传递给编码器块之前将其转换为嵌入。Transformer解码器(下一行)操作先前用翻译语言生成的输出和中间分支的编码输入序列,以输出输出序列中的下一个单词。先前的输出序列(用作解码器的输入)是通过将输出语句向右移动一个位置并在开始时附加句子开始标记来获得的。这种移位避免了模型学习简单地将解码器输入复制到输出。训练模型的基本事实是输出语言序列(没有任何右移)附加一个句子结束标记。由多头注意(顶行)和前馈层组成的块在编码器和解码器中重复N次。
图6 视觉transformer的架构
self-attention 代码
实现
from: Transformer 模型详解及代码实现 - 进击的程序猿 - 知乎
单层attention的实现
输入序列单词的 Embedding Vector 经过线性变换(Linear 层)得到 Q、K、V 三个向量,并将它们作为 Self-Attention 层的输入。假设输入序列的长度为 seq_len,则 Q、K 和 V 的形状为(seq_len,d_k),其中,
dk
表示每个词或向量的维度,也是 矩阵的列数。在论文中,输入给 Self-Attention 层的 Q、K、V 的向量维度是 64,
Embedding Vector 和 Encoder-Decoder 模块输入输出的维度都是 512。
class ScaleDotProductAttention(nn.Module):
def __init__(self, ):
super(ScaleDotProductAttention, self).__init__()
self.softmax = nn.Softmax(dim = -1)
def forward(self, Q, K, V, mask=None):
K_T = K.transpose(-1, -2) # 计算矩阵 K 的转置
d_k = Q.size(-1)
# 1, 计算 Q, K^T 矩阵的点积,再除以 sqrt(d_k) 得到注意力分数矩阵
scores = torch.matmul(Q, K_T) / math.sqrt(d_k)
# 2, 如果有掩码,则将注意力分数矩阵中对应掩码位置的值设为负无穷大
if mask is not None:
scores = scores.masked_fill(mask == 0, -1e9)
# 3, 对注意力分数矩阵按照最后一个维度进行 softmax 操作,得到注意力权重矩阵,值范围为 [0, 1]
attn_weights = self.softmax(scores)
# 4, 将注意力权重矩阵乘以 V,得到最终的输出矩阵
output = torch.matmul(attn_weights, V)
return output, attn_weights
# 创建 Q、K、V 三个张量
Q = torch.randn(5, 10, 64) # (batch_size, sequence_length, d_k)
K = torch.randn(5, 10, 64) # (batch_size, sequence_length, d_k)
V = torch.randn(5, 10, 64) # (batch_size, sequence_length, d_k)
# 创建 ScaleDotProductAttention 层
attention = ScaleDotProductAttention()
# 将 Q、K、V 三个张量传递给 ScaleDotProductAttention 层进行计算
output, attn_weights = attention(Q, K, V)
# 打印输出矩阵和注意力权重矩阵的形状
print(f"ScaleDotProductAttention output shape: {output.shape}") # torch.Size([5, 10, 64])
print(f"attn_weights shape: {attn_weights.shape}") # torch.Size([5, 10, 10])
唐宇迪的self-attention的计算讲解
构建3个矩阵来查询,当前词和其他词的关系,特征向量表示
Q K V是由可训练的矩阵计算得到
向量对应位点乘
最终的输出结果 scale softmax
整体计算流程总结
即 K Q 计算是加权权重值
多头注意力
相当于卷积里的多层
多头表达多个特征
具体的计算
多头注意力代码
from: Transformer 模型详解及代码实现 - 进击的程序猿 - 知乎
class MultiHeadAttention(nn.Module):
"""Multi-Head Attention Layer
Args:
d_model: Dimensions of the input embedding vector, equal to input and output dimensions of each head
n_head: number of heads, which is also the number of parallel attention layers
"""
def __init__(self, d_model, n_head):
super(MultiHeadAttention, self).__init__()
self.n_head = n_head
self.attention = ScaleDotProductAttention()
self.w_q = nn.Linear(d_model, d_model) # Q 线性变换层
self.w_k = nn.Linear(d_model, d_model) # K 线性变换层
self.w_v = nn.Linear(d_model, d_model) # V 线性变换层
self.fc = nn.Linear(d_model, d_model) # 输出线性变换层
def forward(self, q, k, v, mask=None):
# 1. dot product with weight matrices
q, k, v = self.w_q(q), self.w_k(k), self.w_v(v) # size is [batch_size, seq_len, d_model]
# 2, split by number of heads(n_head) # size is [batch_size, n_head, seq_len, d_model//n_head]
q, k, v = self.split(q), self.split(k), self.split(v)
# 3, compute attention
sa_output, attn_weights = self.attention(q, k, v, mask)
# 4, concat attention and linear transformation
concat_tensor = self.concat(sa_output)
mha_output = self.fc(concat_tensor)
return mha_output
def split(self, tensor):
"""
split tensor by number of head(n_head)
:param tensor: [batch_size, seq_len, d_model]
:return: [batch_size, n_head, seq_len, d_model//n_head], 输出矩阵是四维的,第二个维度是 head 维度
# 将 Q、K、V 通过 reshape 函数拆分为 n_head 个头
batch_size, seq_len, _ = q.shape
q = q.reshape(batch_size, seq_len, n_head, d_model // n_head)
k = k.reshape(batch_size, seq_len, n_head, d_model // n_head)
v = v.reshape(batch_size, seq_len, n_head, d_model // n_head)
"""
batch_size, seq_len, d_model = tensor.size()
d_tensor = d_model // self.n_head
split_tensor = tensor.view(batch_size, seq_len, self.n_head, d_tensor).transpose(1, 2)
# it is similar with group convolution (split by number of heads)
return split_tensor
def concat(self, sa_output):
""" merge multiple heads back together
Args:
sa_output: [batch_size, n_head, seq_len, d_tensor]
return: [batch_size, seq_len, d_model]
"""
batch_size, n_head, seq_len, d_tensor = sa_output.size()
d_model = n_head * d_tensor
concat_tensor = sa_output.transpose(1, 2).contiguous().view(batch_size, seq_len, d_model)
return concat_tensor
位置编码
实际的特征向量=token的词向量+位置编码
位置编码的代码
from : Transformer代码解读(Pytorch) - 神洛的文章 - 知乎
词嵌入之后紧接着就是位置编码,位置编码用以区分不同词以及同词不同特征之间的关系。代码中需要注意:X_只是初始化的矩阵,并不是输入进来的;完成位置编码之后会加一个dropout。另外,位置编码是最后加上去的,因此输入输出形状不变。
代码
Tensor = torch.Tensor
def positional_encoding(X, num_features, dropout_p=0.1, max_len=512) -> Tensor:
r'''
给输入加入位置编码
参数:
- num_features: 输入进来的维度
- dropout_p: dropout的概率,当其为非零时执行dropout
- max_len: 句子的最大长度,默认512
形状:
- 输入: [batch_size, seq_length, num_features]
- 输出: [batch_size, seq_length, num_features]
例子:
>>> X = torch.randn((2,4,10))
>>> X = positional_encoding(X, 10)
>>> print(X.shape)
>>> torch.Size([2, 4, 10])
'''
dropout = nn.Dropout(dropout_p)
P = torch.zeros((1,max_len,num_features))
X_ = torch.arange(max_len,dtype=torch.float32).reshape(-1,1) / torch.pow(
10000,
torch.arange(0,num_features,2,dtype=torch.float32) /num_features)
P[:,:,0::2] = torch.sin(X_)
P[:,:,1::2] = torch.cos(X_)
X = X + P[:,:X.shape[1],:].to(X.device)
return dropout(X)
# 位置编码例子
X = torch.randn((2,4,10))
X = positional_encoding(X, 10)
print(X.shape)
编码器其他细节
解码器
整体架构汇总