LightningAI 的首席数据科学家Akshay(https://x.com/akshay_pachaar)做了六张图解释Transformer,相当清晰明了。
一、Embeddings(词嵌入)
词嵌入是使用一组数字对每个token(大约一个词)进行有意义的表示。
这种嵌入是我们作为语言模型的输入提供的,即下面过程:
原始文本 → Tokenization(标记化) → Embeddings(词嵌入) → 模型
标记化(Tokenization)
将文本分解为单词或子词。
- 输入句子 "I love Tennis."
- 标记化将这个句子分解为更小的单位,称为标记(tokens)。在这个例子中,标记化过程将句子分解为四个标记:"I"、"Love"、"Tennis" 和 "."
词嵌入(Embeddings)
将这些单词或子词转换为数值向量,以便进行进一步的计算和分析。
- 每个标记被映射到一个高维向量。这个向量是一个数值数组,表示标记的特征。在图中,"I"、"Love"、"Tennis" 和 "." 分别被映射到不同的向量。
- 这些向量的具体数值在图中以蓝色表示,例如 "I" 对应的向量是 [0.89, 0.45, 0.67, ..., 0.32, 0.04]。
二、捕捉文本的上下文和含义
语言建模的核心思想是理解语言中的结构和模式。
通过对句子中的单词(tokens)建模,我们可以捕捉文本的上下文和含义。
语言模型在处理自然语言时,必须理解整个句子的上下文,才能正确理解句子中各个部分之间的关系。
我们看看句子中箭头的连接:
“网球”(Tennis)与“拉斐尔·纳达尔”(Rafael Nadal)之间的关联。
拉斐尔·纳达尔是一位著名的网球运动员,所以句子表达了讲述者对网球的热爱以及对拉斐尔·纳达尔的崇拜之间的联系。
为了理解这句话,语言模型需要意识到“网球”和“拉斐尔·纳达尔”之间的关系。讲述者提到“网球”时,紧接着谈论了“拉斐尔·纳达尔”,表示他对这位网球明星的喜爱。语言模型在处理这样的句子时,需要捕捉这种关系,理解讲述者是在表达对网球和纳达尔的共同喜爱。
在自然语言处理中,语言模型必须能够识别和理解词语之间的上下文关系,以准确地解读句子含义。这也表明了语言模型需要具备上下文理解的能力,不仅仅是逐词处理。
三、Attention
自我关注(Attention)是一种帮助建立这些关系的通信机制,表达为概率分数。
每个token都会给自己最高分,并根据它们的相关性给其他tokens分数。
您可以将其视为一个有向图(Directed Graph)。
左边的矩阵展示了每个单词对其他单词(包括自己)的注意力概率分数。
这些分数表示一个单词应该多大程度上关注自己和邻近的单词。例如,“I”对自己的注意力分数是0.7,对“love”的注意力分数是0.2,依此类推。
右边的图则是将左边的矩阵可视化成了一个有向图。
每个节点代表一个单词,边上的权重表示注意力分数。例如,从“I”到“love”的边权重是0.2,从“I”到“tennis”的边权重是0.06。图中的箭头表示注意力的方向和大小,帮助我们更直观地理解注意力机制是如何在单词之间分配注意力的。
四、注意力机制中的KQV
我们必须理解 3 个关键术语:
- 查询向量 Keys
- 关键向量 Queries
- 价值向量 Values
这些向量是通过将输入嵌入乘以三个可训练的权重矩阵而创建的。
输入令牌嵌入(Input Token Embedding):
图的左下角有一个橙色矩形,标记为“\(X\)”,表示输入令牌的嵌入向量。
可训练权重矩阵(Trainable Weight Matrices):
图中有三个不同颜色的矩阵:紫色的 \(\_\)、青色的 \(\_\) 和橙色的 \(\_\) 。这些是可训练的权重矩阵,分别用于生成查询(Query)、键(Key)和值(Value)。
查询(Query):
输入向量 $$ 乘以权重矩阵 \(\_\) 得到查询向量 $$(用紫色矩形表示),公式为 \(= \cdot \_Q\) 。
查询向量表示输入令牌在寻找什么。
键(Key):
输入向量 $$ 乘以权重矩阵 \(/_\) 得到键向量 $$(用青色矩形表示),公式为 \(K=X \cdot W\_K\) 。
键向量表示输入令牌包含什么信息。
值(Value):
输入向量 $$ 乘以权重矩阵 \(\_\) 得到值向量 $$(用橙色矩形表示),公式为 \(V=X \cdot W\_V\) 。
值向量表示输入令牌将提供什么信息。
这些向量 $$、$$ 和 $$ 是自注意力机制中非常重要的组成部分,它们用于计算注意力权重(每个输入令牌与其他令牌之间的相关性),从而实现上下文信息的捕获(决定每个输入令牌在上下文中的重要性和关联性)。
五、自注意力(Self-Attention)机制
让我们更全面地了解输入嵌入是如何与KQV结合以获得实际的注意力分数的。
获取KQV后,我们将它们合并以创建一组新的上下文感知嵌入。
输入嵌入(Input Embeddings):
图中左侧的橙色矩阵表示输入的嵌入向量,这里有三个词:"I"、"love" 和 "tennis"。
注意力头(Attention Head):
自注意力机制依赖于三个不同的权重矩阵:查询(Query,W_Q)、键(Key,W_K)和值(Value,W_V)。
查询矩阵(Query Matrix,Q):
输入嵌入与查询权重矩阵 W_Q 相乘,生成查询矩阵 Q(图中紫色矩阵)。
公式为 \(Q = X * W\_Q\)。
键矩阵(Key Matrix,K):
输入嵌入与键权重矩阵 W_K 相乘,生成键矩阵 K(图中蓝色矩阵)。
公式为 \(K = X * W\_K\)。
值矩阵(Value Matrix,V):
输入嵌入与值权重矩阵 W_V 相乘,生成值矩阵 V(图中黄色矩阵)。
公式为 \(V = X * W\_V\)。
注意力得分(Attention Scores):
查询矩阵 Q 与键矩阵 K 的转置相乘,再除以一个缩放因子(通常是键矩阵的维度的平方根 \(d_k\)),得到注意力得分 S。
公式为 \(S = softmax(Q * K\_T / sqrt(d_k))\)。
图中灰色矩阵展示了计算后的注意力得分。
注意力输出(Attention Output):
注意力得分矩阵 S 与值矩阵 V 相乘,生成最终的注意力输出(图中绿色矩阵)。这个输出包含了输入词汇在上下文中的信息,即“上下文感知嵌入(Context aware Embeddings)”。
自注意力机制通过计算查询、键和值之间的相似性来决定输入序列中每个词的重要性,从而生成包含上下文信息的嵌入表示。这个机制在自然语言处理中非常有效,因为它可以捕捉到序列中词与词之间的关系。
PyTorch 实现
使用 PyTorch,可以清晰看到实现:
import torch
import torch.nn as nn
from torch.nn import functional as F
class SelfAttention(nn.Module):
""" 单头自注意力机制 """
def __init__(self, head_size):
super().__init__()
# 初始化key、query和value的线性层,且不使用偏置项
self.key = nn.Linear(n_embd, head_size, bias=False)
self.query = nn.Linear(n_embd, head_size, bias=False)
self.value = nn.Linear(n_embd, head_size, bias=False)
# 注册一个缓冲区,用于存储一个下三角矩阵,
# 这个矩阵将用于掩蔽未来的标记,以防止模型在训练时看到未来的信息。
self.register_buffer('tril', torch.tril(torch.ones(block_size, block_size)))
# 初始化Dropout层,防止过拟合。
# Dropout层会在训练过程中随机将部分神经元的输出设为0,从而减少模型对特定神经元的依赖,增强模型的泛化能力。
self.dropout = nn.Dropout(dropout)
def forward(self, x):
B, T, C = x.shape # 批量大小,序列长度,嵌入维度
# 计算key、query和value矩阵
k = self.key(x)
q = self.query(x)
# 计算注意力得分
wei = q @ k.transpose(-2, -1) * k.shape[-1]**-0.5 # 按照头大小的平方根进行缩放
wei = F.softmax(wei, dim=-1) # 应用softmax获取注意力权重
# 将注意力权重应用于value矩阵
v = self.value(x)
out = wei @ v # 值的加权和
return out
总结
通过Akshay的数据科学家图解,我们可以清晰地理解从词嵌入开始,经过标记化、生成向量,到自注意力机制中KQV的生成和注意力得分的计算,再到最终的上下文感知嵌入的创建全过程。
这种系统性的解释不仅帮助我们理解语言模型的内部工作原理,也展示了Transformer在自然语言处理中捕捉词与词之间关系的强大能力。