Transformer结构和注意力机制
如需要Transformer完整实现示例代码,可以参考阿正的梦工坊的博客:PyTorch从零开始实现Transformer
架构图
Transformer 作为大模型的最底层的技术,是我们必须知道的。和其他神经网络相比,有什么特点呢?
图片出处《人工智能注意力机制:体系、模型与算法剖析》。
Transformer 整体结构:
Transformer 编码器结构:
Transformer 解码器结构:
注意力机制
多头注意力机制(Multi - Head Attention)
在深度学习里,基于查询(Query,Q)、键(Key,K)、值(Value,V)的注意力机制是很常用的,像 Transformer 架构就大量使用了这种机制。下面我们会详细讲解如何用 Python 和 PyTorch 库来实现包含 QKV 的注意力机制。
原理概述
基于 QKV 的注意力机制的核心操作流程如下:
- 线性变换:将输入分别通过三个不同的线性层,得到查询(Q)、键(K)和值(V)。
- 计算注意力分数:通过计算查询(Q)和键(K)的点积,得到注意力分数。
- 缩放和归一化:对注意力分数进行缩放,然后使用 Softmax 函数将其转换为注意力权重。
- 加权求和:根据注意力权重对值(V)进行加权求和,得到最终的输出。
代码实现
import torch
import torch.nn as nn
class ScaledDotProductAttention(nn.Module):
def __init__(self, dropout=0.1):
super(ScaledDotProductAttention, self).__init__()
self.dropout = nn.Dropout(dropout)
def forward(self, query, key, value, mask=None):
# 计算 Q 和 K 的点积
d_k = query.size(-1)
scores = torch.matmul(query, key.transpose(-2, -1)) / torch.sqrt(torch.tensor(d_k, dtype=torch.float32))
if mask is not None:
scores = scores.masked_fill(mask == 0, -1e9)
# 使用 softmax 函数将分数转换为权重
attn_weights = torch.softmax(scores, dim=-1)
attn_weights = self.dropout(attn_weights)
# 根据权重对值进行加权求和
output = torch.matmul(attn_weights, value)
return output, attn_weights
class MultiHeadAttention(nn.Module):
def __init__(self, num_heads, input_dim, dropout=0.1):
super(MultiHeadAttention, self).__init__()
assert input_dim % num_heads == 0, "输入维度必须能被头的数量整除"
self.num_heads = num_heads
self.d_k = input_dim // num_heads
# 定义线性层来生成 Q、K、V
self.W_q = nn.Linear(input_dim, input_dim)
self.W_k = nn.Linear(input_dim, input_dim)
self.W_v = nn.Linear(input_dim, input_dim)
self.W_o = nn.Linear(input_dim, input_dim)
self.attention = ScaledDotProductAttention(dropout)
self.dropout = nn.Dropout(dropout)
def forward(self, query, key, value, mask=None):
batch_size = query.size(0)
# 线性变换得到 Q、K、V
Q = self.W_q(query)
K = self.W_k(key)
V = self.W_v(value)
# 分割多头
Q = Q.view(batch_size, -1, self.num_heads, self.d_k).transpose(1, 2)
K = K.view(batch_size, -1, self.num_heads, self.d_k).transpose(1, 2)
V = V.view(batch_size, -1, self.num_heads, self.d_k).transpose(1, 2)
if mask is not None:
mask = mask.unsqueeze(1)
# 计算多头注意力
output, attn_weights = self.attention(Q, K, V, mask)
# 合并多头
output = output.transpose(1, 2).contiguous().view(batch_size, -1, self.num_heads * self.d_k)
# 通过输出线性层
output = self.W_o(output)
output = self.dropout(output)
return output, attn_weights
# 测试代码
if __name__ == "__main__":
batch_size = 2
seq_len = 3
input_dim = 8
num_heads = 2
# 生成随机输入
query = torch.randn(batch_size, seq_len, input_dim)
key = torch.randn(batch_size, seq_len, input_dim)
value = torch.randn(batch_size, seq_len, input_dim)
# 创建多头注意力实例
multihead_attn = MultiHeadAttention(num_heads, input_dim)
# 前向传播
output, attn_weights = multihead_attn(query, key, value)
print("输出形状:", output.shape)
print("注意力权重形状:", attn_weights.shape)
代码解释
ScaledDotProductAttention
类:- 实现了缩放点积注意力机制,通过
forward
方法完成注意力分数的计算、缩放、归一化以及加权求和等操作。 mask
参数用于在某些场景下屏蔽部分输入,例如在解码器的自注意力机制中避免看到未来的信息。
- 实现了缩放点积注意力机制,通过
MultiHeadAttention
类:- 实现了多头注意力机制,通过多个独立的注意力头并行计算,最后将结果合并。
- 首先使用三个线性层
W_q
、W_k
、W_v
分别对输入进行线性变换得到 Q、K、V。 - 将 Q、K、V 分割成多个头,然后调用
ScaledDotProductAttention
类计算每个头的注意力。 - 最后将所有头的结果合并,并通过输出线性层
W_o
得到最终输出。
- 测试代码:
- 生成随机的查询、键和值输入。
- 创建
MultiHeadAttention
类的实例并进行前向传播,打印输出和注意力权重的形状。
复杂度分析
- 时间复杂度: O ( b a t c h _ s i z e × s e q _ l e n 2 × i n p u t _ d i m ) O(batch\_size \times seq\_len^2 \times input\_dim) O(batch_size×seq_len2×input_dim),主要开销在于计算注意力分数和加权求和。
- 空间复杂度: O ( b a t c h _ s i z e × s e q _ l e n 2 × n u m _ h e a d s ) O(batch\_size \times seq\_len^2 \times num\_heads) O(batch_size×seq_len2×num_heads),主要用于存储注意力权重。
通过上述代码,你可以实现一个包含 QKV 的注意力机制,并且可以方便地将其集成到更复杂的深度学习模型中。
什么是分割多头?
分割多头(Multi - Head Splitting)是多头注意力机制(Multi - Head Attention)里的一个关键步骤,下面从概念、目的、具体操作以及示例几个方面来详细解释。
概念
在深度学习尤其是 Transformer 架构中,多头注意力机制允许模型在不同的表示子空间里并行地关注输入序列的不同部分。分割多头就是把输入的查询(Q)、键(K)和值(V)张量分割成多个“头(heads)”,每个头在不同的低维子空间中独立地计算注意力,这样可以让模型捕捉到输入序列里多样化的特征和依赖关系。
目的
- 增强特征提取能力:不同的头能够学习到输入序列的不同方面的特征。例如,有的头可能更关注局部特征,有的头则可能更关注全局特征,从而提高模型的表达能力。
- 并行计算:多个头可以并行计算,这样能提高计算效率,尤其是在使用 GPU 进行计算时,能充分利用 GPU 的并行计算能力。
具体操作
假设输入的查询(Q)、键(K)和值(V)的形状都是 (batch_size, seq_len, input_dim)
,其中 batch_size
是批量大小,seq_len
是序列长度,input_dim
是输入的特征维度。并且设定多头注意力机制的头的数量为 num_heads
,且要求 input_dim
能被 num_heads
整除,每个头的特征维度 d_k = input_dim // num_heads
。
分割多头的具体步骤如下:
- 线性变换:通过三个线性层(
W_q
、W_k
、W_v
)分别对输入进行线性变换,得到 Q、K、V 张量,它们的形状仍然是(batch_size, seq_len, input_dim)
。 - 分割操作:将 Q、K、V 张量的最后一个维度(特征维度)分割成
num_heads
个维度为d_k
的子空间。具体代码如下:
import torch
batch_size = 2
seq_len = 3
input_dim = 8
num_heads = 2
d_k = input_dim // num_heads
# 假设已经得到 Q、K、V 张量
Q = torch.randn(batch_size, seq_len, input_dim)
K = torch.randn(batch_size, seq_len, input_dim)
V = torch.randn(batch_size, seq_len, input_dim)
# 分割多头
Q = Q.view(batch_size, -1, num_heads, d_k).transpose(1, 2)
K = K.view(batch_size, -1, num_heads, d_k).transpose(1, 2)
V = V.view(batch_size, -1, num_heads, d_k).transpose(1, 2)
print("分割后的 Q 形状:", Q.shape) # 输出: (batch_size, num_heads, seq_len, d_k)
在上述代码中,view
函数用于改变张量的形状,将特征维度分割成 num_heads
个 d_k
维度的子空间;transpose(1, 2)
函数用于交换第 1 维和第 2 维,使得形状变为 (batch_size, num_heads, seq_len, d_k)
,这样每个头就可以独立地进行注意力计算了。
后续操作
分割多头之后,每个头会独立地进行缩放点积注意力计算,得到各自的输出。最后,将所有头的输出在特征维度上合并,再通过一个线性层进行变换,得到最终的多头注意力输出。
总之,分割多头是多头注意力机制的重要组成部分,它通过将输入分割到多个低维子空间中,让模型能够更全面、更高效地捕捉输入序列的特征。
QKV 是什么?有物理意义吗?
一直没有搞懂QKV的作用,是否有物理意义呢。
查询(Q)、键(K)和值(V)是在注意力机制中人为定义的概念,并非物理现象,但这种定义有着坚实的数学和认知基础,在自然语言处理和计算机视觉等领域取得了很好的效果。下面从概念来源、工作原理、人为定义的优势几个方面详细解释为何使用 Q、K、V 能得到注意力。
概念来源
注意力机制的灵感来源于人类的认知系统。人类在处理信息时,会根据当前的任务和关注点,有选择性地关注输入信息的不同部分。例如,在阅读一篇文章时,我们会根据问题去关注文章中的特定段落。受此启发,研究者们在深度学习中引入了注意力机制,而 Q、K、V 就是为了模拟这种选择性关注过程而设计的。
工作原理
在基于 Q、K、V 的注意力机制中,Q、K、V 的具体作用和工作流程如下:
- 生成 Q、K、V:通常会将输入数据分别通过三个不同的线性变换得到 Q、K、V。例如,在自然语言处理中,输入可能是词向量序列,经过线性变换后得到每个词对应的 Q、K、V。
- 计算注意力分数:通过计算 Q 和 K 之间的相似度来得到注意力分数。常用的计算方式是点积运算,即计算 Q K T QK^T QKT。这一步的直观理解是,查询(Q)代表了当前需要关注的信息,键(K)则是输入信息的一种表示,通过计算 Q 和 K 的相似度,我们可以知道输入信息中哪些部分与当前查询更相关。
- 缩放和归一化:为了避免点积结果过大,通常会对注意力分数进行缩放,然后使用 Softmax 函数将其转换为概率分布(注意力权重)。这些权重表示了输入信息中各个部分的重要程度。
- 加权求和:根据得到的注意力权重,对值(V)进行加权求和,得到最终的注意力输出。值(V)是输入信息的另一种表示,通过加权求和,我们可以从输入信息中提取出与当前查询最相关的部分。
人为定义的优势
使用 Q、K、V 来计算注意力具有以下几个重要优势:
- 灵活性:通过不同的线性变换生成 Q、K、V,可以让模型学习到输入数据的不同表示,从而更好地捕捉数据中的复杂模式和关系。例如,在机器翻译任务中,不同的 Q、K、V 可以帮助模型关注源语言句子中不同部分与目标语言单词的对应关系。
- 可解释性:注意力权重可以直观地反映输入信息中各个部分的重要程度,这有助于我们理解模型的决策过程。例如,在图像识别任务中,注意力权重可以显示模型在识别图像时关注的区域。
- 并行计算:基于 Q、K、V 的注意力机制可以高效地进行并行计算,特别是在多头注意力机制中,多个头可以同时计算不同的注意力,大大提高了计算效率。
综上所述,虽然 Q、K、V 是人为定义的概念,但它们为注意力机制提供了一种有效的实现方式,能够帮助模型更好地处理和理解输入数据。
Transformer架构里,需要用到 dropout吗?
在Transformer架构中,通常会使用Dropout,它在提升模型性能、增强泛化能力等方面发挥着重要作用。以下从Dropout的作用、在Transformer中的具体应用位置两方面详细介绍:
Dropout的作用
- 防止过拟合:过拟合是指模型在训练数据上表现很好,但在未见过的测试数据上表现不佳。Dropout通过在训练过程中随机“丢弃”(将神经元的输出置为0)一部分神经元,使得模型不能过度依赖某些特定的神经元,从而迫使模型学习到更具泛化性的特征。例如,在处理自然语言处理任务时,训练数据可能存在一些局部的、偶然的特征模式,如果模型过度依赖这些模式,就会导致过拟合。Dropout可以有效避免这种情况。
- 增强模型的鲁棒性:随机丢弃神经元可以让模型在不同的子网络结构下进行训练,使得模型对输入数据的微小变化更加鲁棒。这意味着即使输入数据存在一些噪声或扰动,模型仍然能够保持较好的性能。
在Transformer中的具体应用位置
1. 多头注意力层之后
在多头注意力机制计算完注意力输出后,会应用Dropout。以下是代码示例(基于PyTorch):
import torch
import torch.nn as nn
class MultiHeadAttention(nn.Module):
def __init__(self, num_heads, input_dim, dropout=0.1):
super(MultiHeadAttention, self).__init__()
# 省略部分初始化代码
self.dropout = nn.Dropout(dropout)
def forward(self, query, key, value):
# 计算多头注意力输出
attn_output = ...
# 应用Dropout
attn_output = self.dropout(attn_output)
return attn_output
在这个位置使用Dropout可以防止多头注意力层的输出出现过拟合,使得模型在不同的注意力头组合下都能学习到有用的信息。
2. 前馈神经网络层之后
Transformer中的前馈神经网络(Feed - Forward Network)由两个线性层和一个激活函数组成,在第二个线性层的输出之后会应用Dropout:
class PositionwiseFeedForward(nn.Module):
def __init__(self, input_dim, ff_dim, dropout=0.1):
super(PositionwiseFeedForward, self).__init__()
self.w_1 = nn.Linear(input_dim, ff_dim)
self.w_2 = nn.Linear(ff_dim, input_dim)
self.dropout = nn.Dropout(dropout)
self.activation = nn.ReLU()
def forward(self, x):
output = self.w_2(self.activation(self.w_1(x)))
# 应用Dropout
output = self.dropout(output)
return output
这有助于防止前馈神经网络层的参数过拟合,使得模型能够学习到更通用的特征表示。
3. 残差连接之后
在Transformer中,多头注意力层和前馈神经网络层都使用了残差连接。在残差连接的输出经过层归一化(Layer Normalization)之后,也会应用Dropout,进一步增强模型的泛化能力。
综上所述,Dropout在Transformer架构中是一个重要的组件,它可以显著提升模型的性能和泛化能力。