注意力机制的输入和输出
flyfish
一、注意力机制概述
点积的定义
对于两个向量
a
\mathbf{a}
a 和
b
\mathbf{b}
b,它们的点积定义为:
a
⋅
b
=
∑
i
=
1
n
a
i
b
i
\mathbf{a} \cdot \mathbf{b} = \sum_{i=1}^{n} a_i b_i
a⋅b=i=1∑naibi
其中,
a
i
a_i
ai 和
b
i
b_i
bi 分别是向量
a
\mathbf{a}
a 和
b
\mathbf{b}
b 的第
i
i
i 个分量,
n
n
n 是向量的维度。
点积的几何意义
点积不仅是一个代数运算,还有明确的几何意义。具体来说,点积可以表示为:
a
⋅
b
=
∥
a
∥
∥
b
∥
cos
(
θ
)
\mathbf{a} \cdot \mathbf{b} = \|\mathbf{a}\| \|\mathbf{b}\| \cos(\theta)
a⋅b=∥a∥∥b∥cos(θ)
其中,
∥
a
∥
\|\mathbf{a}\|
∥a∥ 和
∥
b
∥
\|\mathbf{b}\|
∥b∥ 分别是向量
a
\mathbf{a}
a 和
b
\mathbf{b}
b 的模(长度),
θ
\theta
θ 是两个向量之间的夹角。
点积与向量相似度
- 方向相似度:
- 当 θ = 0 \theta = 0 θ=0 时, cos ( θ ) = 1 \cos(\theta) = 1 cos(θ)=1,点积达到最大值,表明两个向量完全同向。
- 当 θ = 9 0 ∘ \theta = 90^\circ θ=90∘ 时, cos ( θ ) = 0 \cos(\theta) = 0 cos(θ)=0,点积为 0,表明两个向量正交(垂直)。
- 当 θ = 18 0 ∘ \theta = 180^\circ θ=180∘ 时, cos ( θ ) = − 1 \cos(\theta) = -1 cos(θ)=−1,点积达到最小值,表明两个向量完全反向。
点积(Dot Product)可以反映两个向量在方向上的相似度。点积的结果越大,表明两个向量在方向上越相似。
2. 归一化点积:
- 为了消除向量长度的影响,通常使用归一化的点积(即余弦相似度)来衡量向量的方向相似度:
Cosine Similarity = a ⋅ b ∥ a ∥ ∥ b ∥ \text{Cosine Similarity} = \frac{\mathbf{a} \cdot \mathbf{b}}{\|\mathbf{a}\| \|\mathbf{b}\|} Cosine Similarity=∥a∥∥b∥a⋅b - 余弦相似度的取值范围是 [ − 1 , 1 ] [-1, 1] [−1,1],值越接近 1 表明两个向量越相似,值越接近 -1 表明两个向量越相反,值为 0 表明两个向量正交。
在注意力机制中的应用
在自注意力机制中,点积用于计算查询向量(Query)和键向量(Key)之间的相似度。具体步骤如下:
-
生成查询、键和值:
- 通过线性变换生成查询向量
Q
Q
Q、键向量
K
K
K 和值向量
V
V
V:
Q = W Q ⋅ H Q = W_Q \cdot H Q=WQ⋅H
K = W K ⋅ H K = W_K \cdot H K=WK⋅H
V = W V ⋅ H V = W_V \cdot H V=WV⋅H
其中, H H H 是输入的隐藏状态, W Q W_Q WQ、 W K W_K WK 和 W V W_V WV 是线性变换的权重矩阵。
- 通过线性变换生成查询向量
Q
Q
Q、键向量
K
K
K 和值向量
V
V
V:
-
计算点积:
- 计算查询向量
Q
Q
Q 和键向量
K
K
K 之间的点积:
Scores = Q ⋅ K T \text{Scores} = Q \cdot K^T Scores=Q⋅KT - 为了防止数值不稳定,通常会对点积结果进行缩放:
Scores = Q ⋅ K T d k \text{Scores} = \frac{Q \cdot K^T}{\sqrt{d_k}} Scores=dkQ⋅KT
其中, d k d_k dk 是键向量的维度。
- 计算查询向量
Q
Q
Q 和键向量
K
K
K 之间的点积:
-
应用 Softmax:
- 应用 Softmax 函数将点积结果转换为注意力权重:
Attention Weights = softmax ( Scores ) \text{Attention Weights} = \text{softmax}(\text{Scores}) Attention Weights=softmax(Scores)
- 应用 Softmax 函数将点积结果转换为注意力权重:
-
加权求和:
- 使用注意力权重对值向量
V
V
V 进行加权求和,得到最终的上下文向量:
Context = Attention Weights ⋅ V \text{Context} = \text{Attention Weights} \cdot V Context=Attention Weights⋅V
- 使用注意力权重对值向量
V
V
V 进行加权求和,得到最终的上下文向量:
示例代码
以下是一个简单的示例代码,展示了如何计算查询向量和键向量之间的点积:
import torch
# 定义查询向量 Q 和键向量 K
# Q 和 K 都是 2x2 的张量,表示两个查询向量和两个键向量
Q = torch.tensor([[1.0, 2.0], [3.0, 4.0]]) # 查询向量
K = torch.tensor([[2.0, 3.0], [4.0, 5.0]]) # 键向量
# 计算点积
# 通过矩阵乘法计算查询向量 Q 和键向量 K 的转置 K.T 之间的点积
# 结果是一个 2x2 的矩阵,表示每个查询向量与每个键向量之间的相似度
scores = torch.matmul(Q, K.T)
print(f"Scores (Dot Product): {scores}")
# 缩放点积结果
# 为了防止数值不稳定,通常会对点积结果进行缩放
# 缩放因子是键向量维度 d_k 的平方根
d_k = K.shape[-1] # 获取键向量的维度
scaled_scores = scores / torch.sqrt(torch.tensor(d_k, dtype=torch.float32))
print(f"Scaled Scores: {scaled_scores}")
# 应用 Softmax
# 使用 Softmax 函数将缩放后的点积结果转换为注意力权重
# Softmax 函数将每个元素转换为概率值,使得所有元素的和为 1
attention_weights = torch.softmax(scaled_scores, dim=-1)
print(f"Attention Weights: {attention_weights}")
# 假设值向量 V
# V 是 2x2 的张量,表示两个值向量
V = torch.tensor([[0.1, 0.2], [0.3, 0.4]])
# 加权求和
# 使用注意力权重对值向量 V 进行加权求和,得到最终的上下文向量
# 结果是一个 2x2 的矩阵,表示每个查询向量对应的上下文向量
context = torch.matmul(attention_weights, V)
print(f"Context: {context}")
输出
Scores (Dot Product): tensor([[ 8., 14.],
[18., 32.]])
Scaled Scores: tensor([[ 5.6569, 9.8995],
[12.7279, 22.6274]])
Attention Weights: tensor([[1.4166e-02, 9.8583e-01],
[5.0198e-05, 9.9995e-01]])
Context: tensor([[0.2972, 0.3972],
[0.3000, 0.4000]])
输出解释
-
点积:
Scores (Dot Product)
是查询向量 Q Q Q 和键向量 K K K 之间的点积结果。Scaled Scores
是缩放后的点积结果,用于防止数值不稳定。
-
注意力权重:
Attention Weights
是通过 Softmax 函数将点积结果转换为注意力权重。
-
上下文向量:
Context
是使用注意力权重对值向量 V V V 进行加权求和得到的最终上下文向量。
二、注意力机制的输入
1. 来源
注意力机制的输入通常是隐藏状态(Hidden States),这些隐藏状态的来源较为多样,主要有以下几种常见情况:
(1)词嵌入(Word Embeddings)
- 生成过程:在自然语言处理任务中,首先要对输入文本进行处理,将文本中的单词转换为向量表示,这就是词嵌入的过程。通常会使用预训练的词嵌入模型或者在当前任务中专门训练的嵌入层来完成这个转换。例如,假设我们有一个词汇表,预训练的词嵌入模型会学习到词汇表中每个单词与一个固定维度向量空间中的向量的映射关系。当输入一个句子时,模型会根据这个映射将句子中的每个单词替换为对应的向量,这样就得到了一个词嵌入向量序列。比如,对于句子“我爱自然”,经过词嵌入后可能得到一个形状为(句子长度,嵌入维度)的向量序列,若考虑批量处理(假设批量大小为1),整体形状可能为(1,句子长度,嵌入维度)。
- 作用:词嵌入向量能够捕捉单词的语义信息,相较于原始的单词文本,这种向量表示在后续的注意力机制计算以及整个模型的处理中更便于进行数学运算和特征提取。
(2)前一层的输出
- 以多层神经网络为例:在多层的深度学习模型架构中,比如在Transformer模型中,每层都可能包含注意力机制。对于某一层的注意力机制来说,它所接收的输入往往就是上一层的输出。以Transformer解码器为例,解码器通常有多层结构,第二层的注意力机制的输入就是第一层解码器输出的隐藏状态。这些隐藏状态已经包含了上一层对输入信息初步处理后的一些特征信息,通过将其作为下一层注意力机制的输入,下一层能够在此基础上进一步挖掘序列内部的相关性等重要信息,实现对输入信息的逐步深入处理。
- 优势:这种层层传递的方式使得模型能够逐步精炼和优化对输入信息的理解,通过每一层的注意力机制不断调整对不同部分信息的关注程度,从而更好地捕捉输入数据的复杂结构和语义关系。
2. 预处理(以自然语言处理为例)
在自然语言处理任务中,当输入文本作为注意力机制输入的来源时,通常还需要经过一些预处理步骤,主要包括分词和嵌入层处理:
(1)分词
- 操作方式:使用专门的分词器将输入文本按照一定的规则分割成一个个的token(可以是单词、子词等形式)。例如,对于输入文本“我爱自然”,可能会被分割成“我”“爱”“自然”三个token。不同的分词器可能有不同的分词策略,比如基于词汇表的分词、基于规则的分词、基于机器学习的分词等。
- 目的:将连续的文本转换为离散的token序列,以便后续能够通过嵌入层将每个token转换为向量表示,同时也方便在模型中对文本进行处理和分析。
(2)嵌入层
- 具体转换:创建一个嵌入层(如在PyTorch中使用
nn.Embedding
类来创建),需要指定词汇表大小(vocab_size)和嵌入维度(embed_dim)。嵌入层内部维护着一个可学习的权重矩阵,其维度为(vocab_size,embed_dim)。当输入经过分词得到的token ID序列时(每个token都有一个唯一的ID),嵌入层会根据这些ID作为索引,从权重矩阵中取出对应的行向量,这些行向量就是相应token的嵌入向量。将所有token的嵌入向量组合起来,就得到了一个形状为(批量大小,序列长度,嵌入维度)的张量,也就是初始的隐藏状态。例如,假设我们有一个词汇表大小为100,嵌入维度为50的嵌入层,输入的token ID序列为[1, 2, 3](假设批量大小为1),那么通过嵌入层处理后,会得到一个形状为(1,3,50)的张量作为初始隐藏状态。 - 作用:将离散的token序列转换为连续的向量空间表示,使得文本中的单词或token能够在向量空间中进行数学运算和特征提取,为后续的注意力机制提供合适的输入形式,同时也能够捕捉单词之间的语义关系。
三、注意力机制内部处理过程
1. 查询(Query)、键(Key)和值(Value)向量的生成
- 线性投影:当接收到输入的隐藏状态后,注意力机制首先会通过一系列线性层将输入隐藏状态分别投影到查询(Query)、键(Key)和值(Value)向量空间。以常见的多头自注意力机制为例,假设输入隐藏状态的维度为
hidden_size
,头的数量为num_heads
,则每个头对应的维度为head_dim = hidden_size // num_heads
。会创建三个线性层q_proj
、k_proj
和v_proj
,它们的输入维度均为hidden_size
,输出维度也为hidden_size
(在一些实现中,k_proj
和v_proj
的输出维度可能为head_dim
,这里以常见的输出维度为hidden_size
为例)。通过以下公式生成查询、键和值向量:- 查询向量
Q
:Q = q_proj(hidden_states)
,然后将其形状调整为(batch_size, seq_len, num_heads, head_dim)
并通过transpose(1, 2)
操作将头的维度移到第二个位置,方便后续计算。 - 键向量
K
:K = k_proj(hidden_states)
,同样进行形状调整和转置操作,与查询向量的处理方式类似。 - 值向量
V
:V = v_proj(hidden_states)
,也进行相应的形状调整和转置操作。
- 查询向量
- 作用:查询向量主要用于衡量当前位置与其他位置的相关性程度;键向量是与查询向量配合,用于生成注意力分数,这个分数反映了输入序列中不同位置之间的关联强度;值向量是实际携带信息的载体,在通过注意力分数加权后,这些信息会被聚合到每个位置,从而得到包含序列全局信息的输出。
2. 注意力分数的计算
- 点积运算及缩放:计算查询向量
Q
和键向量K
的点积来得到注意力分数。具体计算公式为:scores = torch.matmul(Q, K.transpose(-2, -1)) * self.scale
,其中self.scale
是一个缩放因子,通常为每个头维度的平方根的倒数(即scale = (head_dim) ** -0.5
)。通过点积运算得到的原始分数可能会因为数值过大或过小而影响后续的Softmax操作和权重分配,所以需要进行缩放操作,以优化注意力分数的分布,使得Softmax操作能够更有效地计算出合理的注意力权重。 - 目的:通过计算注意力分数,能够反映出输入序列中每个位置与其他位置之间的相关性程度,为后续确定每个位置应该分配多少注意力权重提供依据。
3. 注意力权重的确定
- Softmax函数应用:对计算得到的注意力分数
scores
应用Softmax函数,得到每个位置相对于其他位置的注意力权重attention_probs
。具体计算公式为:attention_probs = torch.softmax(scores, dim=-1)
。Softmax函数会将注意力分数转换为概率分布,使得每个位置的权重之和为1,这样就明确了每个位置在当前序列中相对于其他位置的相对重要性,即确定了应该对每个位置分配多少注意力。 - 随机失活(Dropout)处理(可选):在一些实现中,为了防止过拟合,会在得到注意力权重后对其进行随机失活处理。例如,通过定义一个
Dropout
层,设置一定的丢弃概率(如dropout = 0.1
),然后将得到的注意力权重attention_probs
通过该Dropout
层进行处理,得到处理后的注意力权重。这样可以在训练过程中增加模型的泛化能力,使得模型不过于依赖某些特定的注意力权重组合。
4. 加权求和与信息聚合
- 加权求和操作:将得到的注意力权重
attention_probs
与值向量V
进行矩阵乘法,实现加权求和,得到一个中间结果。具体计算公式为:context = torch.matmul(attention_probs, V)
。这个操作的目的是根据每个位置的注意力权重,对值向量进行加权,将不同位置的值向量按照它们与当前位置的相关性权重进行求和,从而将序列中各个位置的信息按照相关性进行重新组合,聚合到每个位置。 - 形状调整:为了将加权求和得到的中间结果恢复到与输入隐藏状态相似的形状,以便后续进行输出投影等操作,需要对中间结果进行形状调整。通常会先通过
transpose(1, 2)
操作将头的维度移回原来的位置,然后通过contiguous().view(batch_size, seq_len, -1)
将结果调整为(batch_size, seq_len, hidden_size)
的形式,这里的-1
表示自动根据前面的维度计算出该位置的维度值,实际上就是将各个头的结果合并起来,恢复到与输入向量相同的维度形式。
四、注意力机制的输出
- 输出投影:经过加权求和与形状调整后的中间结果还需要通过一个线性层(如在多头自注意力机制中通常会有一个
o_proj
线性层)进行输出投影,将其投影回与输入隐藏状态相同的维度空间,得到最终的输出。具体计算公式为:output = o_proj(context)
。这个最终输出就是注意力机制经过一系列处理后得到的结果,它综合了输入序列中各个位置的信息,并且根据位置之间的相关性进行了重新组合,包含了对输入信息更深入的理解和处理结果。 - 形状与意义:最终输出的形状通常与输入隐藏状态的形状相同,例如在上述示例中,如果输入隐藏状态的形状为
(batch_size, seq_len, hidden_size)
,那么经过注意力机制处理后的输出形状也为(batch_size, seq_len, hidden_size)
。这个输出可以作为后续模型层的输入,继续参与到整个模型的处理流程中,比如在Transformer架构中,注意力机制的输出会被传递到下一层进行进一步的处理,如前馈神经网络层等,从而逐步构建起对输入信息的更深入的理解和处理体系。
import torch
import torch.nn as nn
# 定义分词器和嵌入层
# 定义一个简单的分词器类
class SimpleTokenizer:
def __init__(self, vocab):
"""
初始化分词器
Args:
vocab (list): 词汇表,是一个包含所有可能出现的单词或符号的列表
"""
self.vocab = vocab
# 创建一个字典,将词汇表中的每个单词映射到一个唯一的整数ID
self.token_to_id = {token: idx for idx, token in enumerate(vocab)}
def tokenize(self, text):
"""
将输入的文本进行分词,并将每个词转换为对应的ID
Args:
text (str): 要分词的输入文本
Returns:
list: 包含输入文本中每个词对应的ID的列表
"""
# 对输入文本按空格进行分割,得到单词列表
words = text.split()
# 遍历单词列表,通过之前创建的字典将每个单词转换为对应的ID
return [self.token_to_id[token] for token in words]
# 定义嵌入层类,用于将分词后的ID转换为向量表示
class EmbeddingLayer(nn.Module):
def __init__(self, vocab_size, embed_dim):
"""
初始化嵌入层
Args:
vocab_size (int): 词汇表的大小,即不同单词或符号的总数
embed_dim (int): 嵌入向量的维度,也就是每个单词将被转换为的向量的维度
"""
super(EmbeddingLayer, self).__init__()
# 创建一个嵌入层,它会根据词汇表大小和嵌入维度创建一个可学习的权重矩阵
self.embedding = nn.Embedding(vocab_size, embed_dim)
def forward(self, token_ids):
"""
前向传播函数,用于将输入的token IDs转换为嵌入向量
Args:
token_ids (torch.Tensor): 包含分词后单词对应的ID的张量,形状通常为 (batch_size, sequence_length)
Returns:
torch.Tensor: 转换后的嵌入向量张量,形状为 (batch_size, sequence_length, embed_dim)
"""
return self.embedding(token_ids)
# 定义自注意力机制
# 定义自注意力机制类,它是整个代码的核心部分,用于处理序列数据中的相关性
class SelfAttention(nn.Module):
def __init__(self, hidden_size, num_heads, dropout=0.1):
"""
初始化自注意力机制
Args:
hidden_size (int): 隐藏状态的维度,也就是输入向量的维度,它决定了后续很多线性变换的输入和输出维度
num_heads (int): 多头自注意力机制中的头的数量,通过使用多个头可以从不同角度捕捉输入序列的信息
dropout (int): 用于在训练过程中防止过拟合的丢弃概率,默认值为0.1
"""
super(SelfAttention, self).__init__()
self.hidden_size = hidden_size
self.num_heads = num_heads
# 计算每个头对应的维度,通过将隐藏状态维度除以头的数量得到
self.head_dim = hidden_size // num_heads
# 创建四个线性层,用于将输入向量投影到不同的空间
self.q_proj = nn.Linear(hidden_size, hidden_size, bias=True)
self.k_proj = nn.Linear(hidden_size, hidden_size, bias=True)
self.v_proj = nn.Linear(hidden_size, hidden_size, bias=True)
self.o_proj = nn.Linear(hidden_size, hidden_size, bias=False)
self.dropout = nn.Dropout(dropout)
# 计算一个缩放因子,用于在计算注意力得分时进行缩放,其值为每个头维度的平方根的倒数
self.scale = (self.head_dim) ** -0.5
def forward(self, hidden_states):
"""
前向传播函数,实现自注意力机制的核心计算逻辑
Args:
hidden_states (torch.Tensor): 输入的隐藏状态张量,形状为 (batch_size, sequence_length, hidden_size)
Returns:
torch.Tensor: 经过自注意力机制处理后的输出张量,形状为 (batch_size, sequence_length, hidden_size)
"""
batch_size, seq_len, _ = hidden_states.size()
# 投影到 Q, K, V 并分割成多个头
# 通过q_proj线性层对隐藏状态进行投影,得到查询向量Q,并调整形状以便后续处理
Q = self.q_proj(hidden_states).view(batch_size, seq_len, self.num_heads, self.head_dim).transpose(1, 2)
# 通过k_proj线性层对隐藏状态进行投影,得到键向量K,并调整形状以便后续处理
K = self.k_proj(hidden_states).view(batch_size, seq_len, self.num_heads, self.head_dim).transpose(1, 2)
# 通过v_proj线性层对隐藏状态进行投影,得到值向量V,并调整形状以便后续处理
V = self.v_proj(hidden_states).view(batch_size, seq_len, self.num_heads, self.head_dim).transpose(1, 2)
# 计算注意力得分
# 通过矩阵乘法计算查询向量Q和键向量K的转置的乘积,再乘以缩放因子self.scale得到注意力得分
scores = torch.matmul(Q, K.transpose(-2, -1)) * self.scale
# 应用 Softmax
# 对注意力得分应用Softmax函数,得到每个位置相对于其他位置的注意力权重
attention_probs = torch.softmax(scores, dim=-1)
# 对得到的注意力权重进行随机丢弃操作,以防止过拟合
attention_probs = self.dropout(attention_probs)
# 加权求和
# 将注意力权重与值向量V进行矩阵乘法,实现加权求和,得到一个中间结果
context = torch.matmul(attention_probs, V).transpose(1, 2).contiguous().view(batch_size, seq_len, -1)
# 输出投影
# 通过o_proj线性层对加权求和得到的中间结果进行投影,得到最终的输出
output = self.o_proj(context)
return output
# 示例
# 定义词汇表
vocab = ["<pad>", "hello", "world", "how", "are", "you", "?"]
# 创建分词器实例
tokenizer = SimpleTokenizer(vocab)
# 创建嵌入层实例,词汇表大小为词汇表的长度,嵌入维度为1536
embedding_layer = EmbeddingLayer(len(vocab), 1536)
# 创建自注意力机制实例,隐藏状态维度为1536,头的数量为16
self_attention = SelfAttention(1536, 16)
# 输入文本
text = "hello world how are you?"
# 对输入文本进行分词,并将每个词转换为对应的ID
token_ids = tokenizer.tokenize(text)
# 将token IDs转换为张量,并增加一个批量维度(这里假设批量大小为1)
token_ids = torch.tensor([token_ids])
# 嵌入层
# 将token IDs通过嵌入层转换为嵌入向量,得到初始的隐藏状态
hidden_states = embedding_layer(token_ids)
print(f"Initial Hidden States shape: {hidden_states.shape}")
# 自注意力机制
# 将初始的隐藏状态作为输入传递给自注意力机制,得到处理后的输出
output = self_attention(hidden_states)
print(f"Output shape: {output.shape}")