DLAI-Transformers-大模型工作原理笔记-全-
DLAI Transformers 大模型工作原理笔记(全)
001:课程介绍与历史背景 🧠

在本节课中,我们将要学习Transformer架构及其核心——注意力机制的基本概念与发展历史。我们将了解注意力机制如何从机器翻译任务中诞生,并最终演变为驱动现代大语言模型的关键技术。
课程概述
欢迎来到由Josh Tmer(StatQuest在线教育平台CEO)讲授的《PyTorch中的注意力机制与Transformer概念与代码》课程。本课程将讲解注意力机制,这一最终催生了Transformer架构的关键技术突破。你将学习这些思想如何随时间发展、其工作原理以及如何实现。Transformer架构和注意力算法在大型语言模型的发展中极为重要。
注意力机制的起源
上一节我们介绍了课程的整体目标,本节中我们来看看注意力机制诞生的背景。
2014年,许多研究人员致力于机器翻译任务,例如将英语句子翻译成法语。一种非常基础的方法是取每个英语单词,并查找该单词对应的法语翻译。
但这种方法效果不佳。例如,英语和法语中的词序可能不同。在示例中,英语句子以“the European economic area was”开头,但法语中的词序发生了变化。句子的长度也可能不同。这个三词的英语句子“they arrive later”在法语中是五个词。


为了应对这些挑战,两个研究小组——蒙特利尔大学的Yoshua Bengio小组和斯坦福大学的Chris Manning小组——独立提出了相似的方法,并发明了注意力机制。幻灯片上引用了这两篇论文。
编码器-解码器机制
为了解决上述问题,研究人员发现编码器-解码器机制对翻译是有效的。
编码器一次处理一个单词,并为每个单词生成一个输出向量。早期的方法会生成一个单一的密集向量来表示整个句子的含义。但在这些新论文中,每个单词的向量被保留下来,并提供给解码器使用。
这些针对每个单词的密集向量捕捉了单词在句子上下文中的含义。今天,我们可能会称之为上下文嵌入,因为嵌入不仅取决于单词本身,还取决于其周围的单词(即上下文)。

一旦输入句子被转换为向量,解码器便将这些向量作为输入。解码器会一次生成一个单词,以从英语输入中产生法语输出。
早期注意力机制的工作原理
现在,这是重要的部分。解码器有一种加权机制,或者说关注或注意每个输入单词(实际上是每个输入词嵌入)的方式。这种关注是独立进行的,基于输入的位置以及生成输出的当前位置。
例如,当翻译开始时,模型试图生成第一个法语单词,它可能会最关注输入中的第一个英语单词。然而,对于第二个单词,在这个例子中,由于法语词序的变化,它将关注第四个输入向量,即“area”对应的向量。模型持续地对翻译该步骤最相关的单词进行加权或关注。这为我们提供了注意力机制的早期形式。

Transformer架构的诞生
仅仅几年后,在2017年,由Ashish Vaswani、Noam Shazeer、Niki Parmar、Jakob Uszkoreit、Llion Jones、Aidan Gomez、Lukasz Kaiser(他曾在DeepLearning.AI和AI Fund任教)等人撰写的论文《Attention Is All You Need》发表。这篇论文来自我(吴恩达)的前团队——Google Brain团队,它引入了Transformer架构和更通用的注意力形式(Josh今天将详细描述)。该架构专门设计为能够高度扩展地使用GPU。Aidan告诉我,当时设计这个架构时,所有设计选择的首要标准是“这能在GPU上扩展吗?”,这被证明是一个伟大的决定。
这篇论文也研究了机器翻译,所描述的模型同样包含一个编码器和一个解码器。编码器单次前向传播为输入句子创建上下文嵌入。解码器然后一次生成一个单词。每个生成的输出都会被反馈给解码器作为下一步的输入,这样在生成下一个单词时,它就知道已经生成了哪些之前的单词。
Transformer的影响与演变
编码器模型后来成为BERT算法的基础。BERT代表来自Transformer的双向编码器表示,它进而成为今天几乎所有用于RAG或推荐应用创建嵌入向量的嵌入模型的基础。

解码器模型此后被用作OpenAI构建的GPT(生成式预训练Transformer)系列大型语言模型的基础,你可能在ChatGPT中使用过它。这个解码器也是大多数其他流行模型的基础,例如来自Anthropic、Google、Mistral和Meta的模型。原始论文仅使用了6层注意力,而例如Meta的Llama 3 405B模型有126层,但基本架构仍然相同。
课程安排
以下是本课程的结构安排:
我们将从描述Transformer和注意力背后的主要思想开始,然后继续学习注意力的矩阵运算和代码实现。


接着,你将学习自注意力、掩码自注意力之间的区别,并完成PyTorch实现。然后,你将学习吴恩达刚刚描述的编码器-解码器架构的细节,以及多头注意力。

致谢与花絮

许多人协助了本课程的制作。我要感谢Jeff Lutwig、Eper Ggami和Harun Salami。


(花絮对话)
嘿,Andrew,面具是怎么回事?哦,我以为你要讲掩码自注意力,我想我可以试着演示一下。嗯,显然我是在“自注意力”。但也许我们俩之间的这段对话,我们可以称之为“交叉注意力”?是的。

本节课中我们一起学习了Transformer和注意力机制的起源、核心思想及其在自然语言处理发展史上的关键作用。从早期机器翻译中的编码器-解码器与简单注意力,到2017年革命性的《Attention Is All You Need》论文提出可扩展的Transformer架构,我们看到了这项技术如何成为现代BERT、GPT等大语言模型的基石。下一节,我们将深入探讨注意力机制的具体算法与数学原理。
002:Transformer与注意力机制的核心思想 🧠
在本节课中,我们将学习Transformer模型及其核心组件——注意力机制的基本概念。我们将了解Transformer如何将文本转换为数字、如何保持词语顺序,以及如何建立词语之间的关系。


词嵌入:将词语转换为数字
上一节我们介绍了Transformer的总体概念,本节中我们来看看它的第一个核心组件:词嵌入。
Transformer是一种神经网络,而神经网络只能处理数字输入。因此,我们需要将输入的词语、词片段或符号(统称为“词元”)转换为数字。这个过程就是词嵌入。
例如,如果输入是“tell me about pizza”,词嵌入层会将其转换为一系列数字。
# 词嵌入的简化概念:将词语映射为向量
word_to_vector = {
"tell": [0.1, 0.2, ...],
"me": [0.3, 0.4, ...],
"about": [0.5, 0.6, ...],
"pizza": [0.7, 0.8, ...]
}

位置编码:追踪词语顺序
现在我们已经理解了词嵌入的目的,接下来让我们讨论位置编码,它有助于追踪词语的顺序。
词语的顺序对句子的含义至关重要。例如,“Squatch eats pizza”和“Pizza eats Squatch”使用了完全相同的词语,但含义截然不同。因此,追踪词语顺序非常重要。
有多种方法可以实现位置编码,但具体细节超出了本课的范围。目前,你只需要知道位置编码有助于模型记住词语在句子中的位置。

# 位置编码的简化概念:为每个位置添加一个独特的向量
position_encoding = {
1: [pe1_1, pe1_2, ...], # 第一个词的位置编码
2: [pe2_1, pe2_2, ...], # 第二个词的位置编码
...
}
# 最终输入 = 词嵌入向量 + 位置编码向量
注意力机制:建立词语间的关系
现在我们知道位置编码有助于追踪词语顺序,接下来让我们谈谈Transformer如何通过注意力机制建立词语之间的关系。

例如,在句子“The pizza came out of the oven, and it tasted good”中,“it”这个词可能指代“pizza”,也可能指代“oven”。显然,“it”应该正确地与“pizza”关联起来。
好消息是,Transformer拥有一种称为“注意力”的机制,可以正确地将“it”与“pizza”关联起来。注意力有多种类型,我们将从最基本的自注意力开始描述。
自注意力机制的工作原理是计算句子中每个词与所有其他词(包括其自身)的相似度。例如,它会计算第一个词“The”与句子中所有其他词的相似度,并对句子中的每个词都进行这样的计算。
一旦计算出相似度分数,它们就会被用来决定Transformer如何编码每个词。例如,如果在大量关于披萨的句子中,“it”更常与“pizza”相关联,那么“pizza”的高相似度分数将导致它对“it”的编码产生更大的影响。
以下是自注意力计算过程的简化步骤:
- 为每个词元创建查询向量、键向量和值向量。
- 计算查询向量与所有键向量的点积,得到相似度分数。
- 对相似度分数应用Softmax函数,将其转换为权重(总和为1)。
- 用这些权重对所有的值向量进行加权求和,得到该位置的输出。
# 自注意力的核心计算(简化示意)
# 假设 Q, K, V 分别是查询、键、值矩阵
attention_scores = Q @ K.T # 计算相似度
attention_weights = softmax(attention_scores) # 转换为权重
output = attention_weights @ V # 加权求和得到输出
总结
本节课中,我们一起学习了Transformer的三个基本构建模块的核心思想:
- 词嵌入:将输入文本中的词元转换为数字向量,以便神经网络处理。
- 位置编码:为词嵌入向量添加位置信息,帮助模型追踪词语在句子中的顺序。
- 注意力机制:通过计算词语间的相似度,建立并编码词语之间的关系,使模型能够理解上下文。

理解这三个部分是如何协同工作的,是掌握Transformer模型的基础。在接下来的课程中,我们将深入探讨它们的实现细节。
003:自注意力计算的矩阵运算


在本节课中,我们将逐步学习计算自注意力所需的矩阵运算。你将了解公式的工作原理及其背后的原因。让我们开始吧。
概述
自注意力计算公式初看可能有些令人望而生畏,但别担心,我们会将其分解成易于理解的小部分。我们将从理解查询(Q)、键(K)和值(V)这三个核心变量开始,然后逐步推导出完整的计算过程。
理解查询、键和值
首先,让我们看看这些变量。Q代表查询,K代表键,V代表值。这些术语来源于数据库领域。

为了理解它们,让我们先看一个数据库的例子。


想象一个酒店客人数据库,它将每位客人的姓氏与房间号配对。



现在,假设Stat sququach在柜台工作一晚,我以姓氏“Starmer”登记入住。

然而,sququach没有正确拼写我的姓氏“Starmer”,而是在电脑里输入了“stammer”。

现在,电脑需要找出数据库中哪个姓氏最接近squatch输入的内容。

在数据库术语中,sququaash输入的内容,即搜索词,被称为查询。而数据库中我们正在搜索的实际姓名则是键。
因此,电脑将查询与数据库中的所有键进行比较,并对每个键进行排名。在这个例子中,查询“stammer”最接近键“Starmer”。于是电脑返回我的房间号537。


在数据库术语中,我们称房间号为值。
总结一下数据库术语:
- 查询是我们用来搜索数据库的东西。
- 电脑计算查询与数据库中所有键之间的相似度。
- 值是数据库作为搜索结果返回的内容。

回到自注意力公式,我们现在对Q、K和V变量指的是什么有了更好的理解。

在Transformer中确定查询、键和值
上一节我们介绍了查询、键和值的数据库概念。本节中,我们来看看在Transformer的上下文中如何确定它们。
首先,请记住自注意力计算的是每个词与自身以及所有其他词之间的相似度。



自注意力为句子中的每个词计算这些相似度。这意味着我们需要为每个词计算一个查询和一个键。就像我们在数据库例子中看到的,每个键都需要返回一个值。


为了让我们的例子足够小,以便于手工计算,让我们使用提示词:“write a poem”。

就像我们在前一课看到的,Transformer做的第一件事是将提示中的每个词转换为词嵌入。


然后,Transformer将位置编码添加到词嵌入中,得到这些代表提示中每个词的编码数字。在这个简单的例子中,我们只用两个数字来代表提示中的每个词。然而,更常见的做法是使用512个或更多数字来代表每个词。
无论如何,为了为每个词创建查询,我们将编码堆叠在一个矩阵中。


然后将该矩阵乘以一个2x2的查询权重矩阵,从而为每个词计算两个查询数字。
注意,我们用一个2x2矩阵乘以编码值,因为我们从每个词两个编码值开始。一个2x2矩阵允许我们最终得到每个词两个查询数字。

相反,如果我们从每个词512个词嵌入开始,因此每个词有512个编码值,那么常见的做法是使用一个512x512的矩阵来为每个词创建512个查询数字。
也就是说,你必须遵循的唯一规则是矩阵运算必须是可行的。
另外,我想指出,我用转置符号标记了查询权重矩阵。这是因为PyTorch打印权重的方式要求我们在进行数学运算前将其转置,才能使计算正确。

现在,我们通过将编码值乘以一个2x2的键权重矩阵来创建键。


我们通过将编码值乘以一个2x2的值权重矩阵来创建值。



现在,我们有了每个标记的查询、键和值。



我们可以用它们来计算自注意力。

计算自注意力分数
上一节我们得到了每个词的查询、键和值矩阵。本节中,我们开始计算自注意力分数。
我们首先将查询矩阵Q乘以键矩阵K的转置。等等,为什么在做这个乘法时需要转置K呢?



在这个具体案例中,明显的原因是如果不转置K,乘法就无法进行。那么Q第一行的两个数字可以乘以K第一列的前两个数字,但底部的数字就会被排除在外。所以在这种情况下,出于技术原因,不转置K就进行乘法是个坏主意。


然而,转置K实际上有一个更重要的原因。为了理解它,让我们一步一步地进行乘法运算。

我们从Q的第一行(单词“write”的查询)和K转置的第一列(单词“write”的键)开始。矩阵乘法给出这些乘积的和,即-0.09。
这种将成对的对应数字相乘然后相加的过程,就像我们在这里所做的,被称为计算点积。所以-0.09是单词“write”的查询和键的点积。
点积可以作为两个事物之间相似度的未缩放度量。这个度量与所谓的余弦相似度密切相关。主要区别在于,余弦相似度将点积缩放到-1和1之间,而点积相似度则不进行缩放。
因此,-0.09是单词“write”的查询和键之间的未缩放相似度。同样地,单词“write”的查询和单词“a”的键之间的未缩放点积相似度是0.06。单词“write”的查询和单词“poem”的键之间的未缩放点积相似度是-0.61。

同样地,我们计算单词“a”的查询与所有键之间的未缩放点积相似度,以及单词“poem”的查询与所有键之间的未缩放点积相似度。
因此,通过将Q乘以K的转置,我们最终得到了每个词所有可能的查询和键组合之间的未缩放点积相似度。


接下来,我们用D_sub_K(键矩阵的维度)的平方根来缩放点积相似度。D_sub_K是键矩阵的维度。在这个例子中,维度指的是我们每个标记拥有的值的数量,即2。
所以我们用2的平方根缩放每个点积相似度,这样就得到了一个缩放后的点积相似度矩阵。
注意,仅用每个标记值数量的平方根进行缩放,并不会以任何系统性的方式缩放点积相似度。



尽管如此,即使在这种有限的缩放下,Transformer的原始作者表示它提高了性能。

接下来,我们对缩放后的点积相似度矩阵的每一行取softmax。



对每一行取softmax后,我们得到这些新的行。注意,softmax函数使得每一行的和为1。


因此,我们可以将这些值视为标记之间关系的总结。例如,单词“write”与自身有36%的相似度,与“a”有40%的相似度,与“poem”有24%的相似度。



现在,让我们把这些新行重新组合成一个矩阵。

计算自注意力的最后一步是将这些百分比乘以矩阵V中的值。
为了确切理解为什么进行这个乘法,让我们一步一步来看。

当我们将第一行的百分比乘以V的第一列时,我们计算了单词“write”的第一个值的36%,加上单词“a”的第一个值的40%,再加上单词“poem”的第一个值的24%。这样就得到了1.0,即单词“write”的第一个自注意力分数。
换句话说,从softmax函数出来的百分比告诉我们,每个词对任何给定词的最终编码应该有多大影响。

同样地,我们缩放第二列的值,得到单词“write”的第二个自注意力分数。

然后,我们用单词“a”的百分比缩放值,得到“a”的自注意力分数,再用单词“poem”的百分比缩放值,得到“poem”的自注意力分数。
最终,我们计算出了每个输入标记的自注意力分数。



总结
本节课中,我们一起学习了自注意力计算的完整矩阵运算过程。
自注意力公式可能看起来令人生畏,但它所做的只是计算所有词之间的缩放点积相似度,用softmax函数将这些缩放后的相似度转换为百分比,然后使用这些百分比来缩放值,从而成为每个词的自注意力分数。


核心计算过程可以总结为以下公式:
自注意力(Q, K, V) = softmax( (Q * K^T) / sqrt(d_k) ) * V
其中:
Q是查询矩阵K是键矩阵V是值矩阵d_k是键向量的维度softmax是归一化指数函数

通过这个过程,模型能够动态地关注输入序列中不同部分的信息,这是Transformer架构强大能力的基础。
004:使用PyTorch实现自注意力机制


概述
在本节课中,我们将学习如何使用PyTorch编写一个实现自注意力机制的类。我们将通过代码演示如何创建查询、键和值矩阵,计算注意力分数,并验证计算结果的正确性。
导入必要的库
首先,我们需要导入PyTorch及其相关模块,以便创建张量、定义神经网络层并使用辅助函数。
import torch
import torch.nn as nn
import torch.nn.functional as F
定义自注意力类
接下来,我们定义一个名为 SelfAttention 的类,它继承自 nn.Module。这是PyTorch中所有神经网络模块的基类。
class SelfAttention(nn.Module):
def __init__(self, d_model, row_idx=0, col_idx=1):
super().__init__()
self.d_model = d_model
self.row_idx = row_idx
self.col_idx = col_idx
self.W_q = nn.Linear(d_model, d_model, bias=False)
self.W_k = nn.Linear(d_model, d_model, bias=False)
self.W_v = nn.Linear(d_model, d_model, bias=False)
初始化方法详解
在 __init__ 方法中,我们接收以下参数:
- d_model:模型的维度,即每个词嵌入向量的长度。
- row_idx 和 col_idx:用于调整数据索引的便利参数,通常用于处理批次数据。
我们使用 nn.Linear 创建三个线性层,分别用于生成查询、键和值矩阵。每个线性层的输入和输出维度都设置为 d_model,并且不添加偏置项,这与原始Transformer论文的设计一致。
前向传播方法
现在,我们为 SelfAttention 类添加 forward 方法,用于计算每个词的自注意力分数。
def forward(self, token_encodings):
Q = self.W_q(token_encodings)
K = self.W_k(token_encodings)
V = self.W_v(token_encodings)
similarities = torch.matmul(Q, K.transpose(self.row_idx, self.col_idx))
scaled_similarities = similarities / torch.sqrt(torch.tensor(self.d_model, dtype=torch.float32))
attention_per = F.softmax(scaled_similarities, dim=-1)
attention_scores = torch.matmul(attention_per, V)
return attention_scores
前向传播步骤解析
以下是计算自注意力的具体步骤:
- 计算查询、键和值矩阵:将输入的词编码分别通过三个线性层,得到查询矩阵 Q、键矩阵 K 和值矩阵 V。
- 计算相似度:使用矩阵乘法计算查询和键之间的相似度,公式为:
similarities = Q * K^T - 缩放相似度:将相似度除以
sqrt(d_model)进行缩放,以防止梯度消失或爆炸。 - 应用Softmax函数:对缩放后的相似度应用Softmax函数,得到注意力权重矩阵
attention_per。 - 计算注意力分数:将注意力权重与值矩阵相乘,得到最终的注意力分数
attention_scores。
测试自注意力类
为了验证我们的实现是否正确,我们可以创建一个简单的测试用例。
torch.manual_seed(42)
encodings_matrix = torch.tensor([[1.0, 2.0], [3.0, 4.0], [5.0, 6.0]])
self_attention = SelfAttention(d_model=2, row_idx=0, col_idx=1)
attention_scores = self_attention(encodings_matrix)
print("注意力分数矩阵:")
print(attention_scores)
验证计算过程
为了确保计算正确,我们可以手动检查权重矩阵和中间结果。
以下是打印权重矩阵的代码:
print("查询权重矩阵 W_q:")
print(self_attention.W_q.weight.T)
print("键权重矩阵 W_k:")
print(self_attention.W_k.weight.T)
print("值权重矩阵 W_v:")
print(self_attention.W_v.weight.T)
通过比较手动计算的结果与类输出的结果,我们可以验证自注意力机制的正确性。

总结
在本节课中,我们一起学习了如何使用PyTorch实现自注意力机制。我们定义了一个 SelfAttention 类,它能够计算查询、键和值矩阵,并通过缩放点积注意力机制生成最终的注意力分数。通过测试和验证,我们确保了代码的正确性。掌握自注意力的实现是理解Transformer架构的重要一步。
005:自注意力与掩码自注意力
概述
在本节课中,我们将深入探讨自注意力与掩码自注意力机制的异同。虽然它们的差异看似细微,但对各自能解决的问题类型有着巨大影响。我们将从自注意力能实现的功能开始,逐步过渡到掩码自注意力,并理解它们为何适用于不同的任务。
从词嵌入到自注意力
上一节我们介绍了注意力的核心思想是帮助建立词与词之间的关系。现在,我们来看看自注意力机制如何工作,以及它为何强大。
首先,我们需要理解Transformer如何将词语转换为数字,这个过程称为词嵌入。一种简单的方法是为每个词分配一个随机数。例如,对于句子“Pizza is great.”,我们可以为每个词分配随机数。然而,这种方法存在问题:即使“great”和“awesome”含义相似、用法相近,它们也会被分配完全不同的数字。这意味着神经网络需要更多的复杂性和训练数据,因为学会处理“great”并不能帮助它正确处理“awesome”。
因此,理想的情况是,为用法相似的词语分配相似的数字,这样学会使用一个词就能同时帮助学会使用另一个词。同时,由于同一个词可以在不同语境中使用(例如,“great”可以表示正面意义,也可以用于讽刺的负面意义),为每个词分配多个数字(即一个向量)可能更好,这样神经网络就能更容易地适应不同的上下文。
构建简单的词嵌入网络
以下是构建一个简单词嵌入网络的步骤,用于处理“Pizza is great.”和“Pizza is awesome.”这两个句子:
- 为每个独特的词创建神经网络输入节点。
- 为每个词创建输出节点。
- 将所有输入连接到至少一个激活函数。在本例中,我们连接到两个激活函数。激活函数的数量决定了我们用来表示每个词的数字(即词嵌入向量)的维度。这里我们将得到两个数字。
- 为从输入到激活函数的连接添加权重。这些权重就是词嵌入值,初始化为随机数。
- 将激活函数连接到输出层(具体细节暂不深究)。
由于每个词都有两个嵌入值(分别对应顶部和底部的激活函数),我们可以将每个词绘制在一个二维图上,X轴是顶部嵌入值,Y轴是底部嵌入值。在初始随机权重下,词语“great”和“awesome”的分布可能并不相似。
训练词嵌入网络
训练的目标是让网络根据当前词预测下一个词。例如,我们希望输入“pizza”能预测出“is”,输入“is”能预测出“great”或“awesome”。通过使用反向传播等算法调整网络权重(即词嵌入值),最终训练后的词嵌入会使“great”和“awesome”在向量空间中彼此靠近,因为它们出现在相似的上下文中。
然而,仅预测下一个词提供的上下文信息有限。为了获得更好的词嵌入,我们可以使用更复杂的训练数据集和更长的上下文窗口。例如,使用“the pizza came out”四个词来预测下一个词“of”。但这种方法存在一个问题:它忽略了词序。对于神经网络来说,“the pizza came out of”和乱序的“pizza out came the of”输入是一样的,但它们的含义可能天差地别。
引入位置编码与自注意力
为了解决词序问题,Transformer引入了位置编码层。它在词嵌入向量中添加了位置信息,使模型能够区分词语的顺序。
随后是自注意力层。自注意力机制会考虑序列中的所有词(包括目标词之前和之后的词),来计算每个词与其他词的相关性。通过这种方式,我们得到了一种新的嵌入表示,通常称为上下文感知嵌入或语境化嵌入。
与仅聚类单个词的词嵌入相比,上下文感知嵌入可以帮助聚类相似的句子,甚至相似的文档。
仅编码器Transformer的应用
仅使用自注意力的Transformer被称为仅编码器Transformer。它们生成的上下文感知嵌入非常有用,可以用于多种任务:
以下是上下文感知嵌入的主要应用场景:
- 文本聚类:对句子或文档进行聚类分析。
- 情感分类:作为输入,接入一个普通的神经网络来分类文本的情感(如判断推特上关于披萨的评论是正面还是负面)。
- 特征输入:作为逻辑回归等分类模型的输入变量。
总之,仅编码器Transformer创建的上下文感知嵌入具有广泛的应用价值。
从自注意力到掩码自注意力
了解了仅编码器Transformer(使用自注意力)的强大功能后,我们来讨论另一种Transformer:仅解码器Transformer。
与仅编码器Transformer一样,仅解码器Transformer也从词嵌入和位置编码开始。但它不使用标准的自注意力,而是使用掩码自注意力。
核心区别:能否“向前看”
自注意力与掩码自注意力之间的最大区别在于:
- 自注意力:在计算某个词的注意力时,可以查看该词之前和之后的所有词。
- 掩码自注意力:在计算某个词的注意力时,只能查看该词之前的词,而忽略之后的词。这就像在注意力权重矩阵上应用了一个掩码,将“未来”信息屏蔽掉。
例如,对于句子“The pizza came out of the oven, and it tasted good.”:
- 计算“the”的自注意力时,会考虑它与序列中所有词(包括其后的词)的相似性。
- 计算“the”的掩码自注意力时,只考虑它与自身及之前词(本例中之前无词)的相似性,忽略其后所有词。
- 计算“it”的掩码自注意力时,只考虑“it”与“The”, “pizza”, “came”, “out”, “of”, “the”, “oven”, “and”的相似性,忽略其后的“tasted”和“good”。
仅解码器Transformer与生成任务
由于仅解码器Transformer使用掩码自注意力,永远无法“偷看”未来的词,因此它们可以被训练来出色地完成生成任务。
在训练时,我们可以给模型输入一个句子的前半部分(例如,到“it”为止),然后在训练过程中调整模型权重,直到它能够生成句子的剩余部分“tasted good”。这就是为什么像ChatGPT这样的仅解码器Transformer被称为生成模型,因为它被专门训练来根据提示生成后续文本。
因此,与创建上下文感知嵌入的仅编码器Transformer不同,仅解码器Transformer创建的是生成式输入,可以接入一个简单的神经网络来生成新的词元(tokens)。
总结
本节课我们一起学习了自注意力与掩码自注意力的核心区别及其影响:
- 自注意力可以查看目标词之前和之后的词,适用于需要理解完整上下文的任务(如文本分类、聚类),是仅编码器Transformer的核心。
- 掩码自注意力只能查看目标词之前的词,屏蔽未来信息,适用于文本生成任务,是仅解码器Transformer(如GPT系列)的核心。
这个相对较小的设计差异,深刻地决定了两种Transformer架构所能解决的问题类型。


附注:如果你好奇“仅编码器”和“仅解码器”这些名称的来源,请不要担心,我们将在后续课程中详细讲解。
006:计算掩码自注意力的矩阵运算


在本节课中,我们将逐步学习计算掩码自注意力所需的矩阵运算。你将同时了解其计算方法和背后的原理。

概述

上一节我们介绍了自注意力的矩阵运算公式。本节中,我们将探讨掩码自注意力的矩阵运算。好消息是,两者之间的唯一区别在于,我们在缩放后的相似度矩阵上添加了一个新的掩码矩阵 M。

计算查询、键和值矩阵

与之前一样,我们首先需要计算查询、键和值矩阵。

给定提示词“write a poem”,Transformer会创建词嵌入,然后加上位置编码,从而得到编码后的值。

接着,我们像之前一样计算查询矩阵 Q、键矩阵 K 和值矩阵 V。


计算掩码自注意力

现在,我们开始计算掩码自注意力。


首先,我们计算查询和键之间的相似度。
然后,对相似度进行缩放。
添加掩码矩阵
接下来,我们将掩码矩阵 M 加到缩放后的相似度矩阵上。

掩码的目的是防止任何词元在计算注意力时,包含其后面词元的信息。

以提示词“write a poem”为例:
- 第一个词元“write”应只包含自身,并在其注意力计算中屏蔽掉“a”和“poem”。
- 第二个词元“a”应包含自身和“write”,并屏蔽掉“poem”。
- 第三个词元“poem”应包含所有词元。
掩码通过一个矩阵来实现,该矩阵在我们希望保留的注意力计算值上加上0,在需要屏蔽的值上加上负无穷。

应用Softmax函数

加上0不会改变原值,因此这些缩放相似度保持不变。但加上负无穷会将对应的缩放相似度变为负无穷。

这意味着,当我们对每一行应用softmax函数时:
- 第一个词元“write”与自身的相似度为100%,与后面任何词元的相似度为0%。
- 同样,第二个词元“a”与后面词元“poem”的相似度为0%。
- 最后一个词元“poem”则与所有词元都有相似度。

计算最终输出
最后,我们将这些百分比乘以值矩阵 V:
- 第一个词元“write”的掩码自注意力值不包含其后面任何词元的信息。
- 第二个词元“a”的掩码自注意力值只包含“write”和“a”。
- 最后一个词元“poem”的掩码自注意力值包含所有词元。

总结

本节课中,我们一起学习了如何通过矩阵运算一步步计算掩码自注意力。核心在于在缩放相似度上添加一个掩码矩阵,以确保每个词元在生成输出时,只关注其自身及之前的词元,从而实现自回归生成。
007:使用PyTorch实现掩码自注意力


在本节课中,你将使用PyTorch编写一个实现掩码自注意力的类。然后,你将输入一些数据来运行它,并验证计算是否正确。让我们开始编码。

概述
在本节中,我们将学习如何使用PyTorch构建一个掩码自注意力模块。我们将定义一个类,它继承自nn.Module,并实现前向传播逻辑,其中包含对注意力分数应用掩码的关键步骤。我们将通过一个具体的例子来验证其功能。
导入必要的库
首先,我们需要导入PyTorch及其神经网络模块。
import torch
import torch.nn as nn
import torch.nn.functional as F
定义掩码自注意力类
接下来,我们定义一个名为MaskedSelfAttention的类,它继承自nn.Module。
class MaskedSelfAttention(nn.Module):
初始化方法
在__init__方法中,我们定义类的参数并初始化权重矩阵。
def __init__(self, d_model):
super().__init__()
# 创建查询、键和值的权重矩阵
self.W_q = nn.Linear(d_model, d_model) # 查询权重矩阵
self.W_k = nn.Linear(d_model, d_model) # 键权重矩阵
self.W_v = nn.Linear(d_model, d_model) # 值权重矩阵
d_model参数表示模型的维度,即每个词元嵌入向量的长度。我们使用nn.Linear层来创建查询、键和值的权重矩阵。
前向传播方法
forward方法是计算掩码自注意力的核心。它接收词元编码和一个可选的掩码矩阵。
def forward(self, x, mask=None):
# 计算查询、键和值
Q = self.W_q(x) # 查询矩阵
K = self.W_k(x) # 键矩阵
V = self.W_v(x) # 值矩阵
# 计算缩放点积注意力分数
attention_scores = torch.matmul(Q, K.transpose(-2, -1)) / (K.size(-1) ** 0.5)
# 如果提供了掩码,则应用掩码
if mask is not None:
# 将掩码中为True的位置填充为极小的负数
attention_scores = attention_scores.masked_fill(mask == 0, -1e9)
# 应用Softmax获取注意力权重
attention_weights = F.softmax(attention_scores, dim=-1)
# 计算加权和
output = torch.matmul(attention_weights, V)
return output
在前向传播中,我们首先通过权重矩阵计算查询、键和值。然后,我们计算查询和键之间的相似度,并进行缩放。如果提供了掩码,我们使用masked_fill方法将掩码中指定位置(通常对应未来词元)的注意力分数替换为一个极小的负数(如-1e9),这样在后续的Softmax步骤中,这些位置的概率就会接近零。最后,我们计算注意力权重与值的加权和,得到输出。
验证实现
现在,让我们通过一个例子来验证我们的实现是否正确。
设置随机种子
为了确保结果可复现,我们设置随机种子。
torch.manual_seed(42)
创建输入数据
假设我们有一个包含三个词元的提示,每个词元的编码维度为4。
# 假设的编码矩阵,形状为 (序列长度, d_model)
encodings = torch.randn(3, 4)
print("编码矩阵:")
print(encodings)
创建掩码
我们需要创建一个掩码来防止词元在计算注意力时“看到”未来的词元。对于长度为3的序列,我们创建一个下三角矩阵。
# 创建一个3x3的下三角掩码矩阵
mask = torch.tril(torch.ones(3, 3)).bool()
print("掩码矩阵:")
print(mask)
这个掩码矩阵的主对角线及以下元素为True,以上元素为False,确保了每个位置只能关注到自身及之前的位置。
实例化并运行模型
现在,我们实例化掩码自注意力类,并将编码和掩码输入。
# 实例化模型
d_model = 4
model = MaskedSelfAttention(d_model)
# 进行前向传播
output = model(encodings, mask)
print("掩码自注意力输出:")
print(output)
检查中间结果
为了深入理解,我们可以打印出权重矩阵和中间生成的查询、键、值矩阵。
print("查询权重矩阵:")
print(model.W_q.weight)
print("键权重矩阵:")
print(model.W_k.weight)
print("值权重矩阵:")
print(model.W_v.weight)
# 手动计算查询、键、值以验证
Q = model.W_q(encodings)
K = model.W_k(encodings)
V = model.W_v(encodings)
print("查询矩阵 Q:")
print(Q)
print("键矩阵 K:")
print(K)
print("值矩阵 V:")
print(V)
通过比较这些中间结果,你可以验证整个计算流程是否符合预期。

总结

在本节课中,我们一起学习了如何使用PyTorch实现一个掩码自注意力机制。我们定义了一个类,它能够计算查询、键和值,应用缩放点积注意力,并根据提供的掩码矩阵屏蔽掉未来的信息。最后,我们通过一个具体的例子验证了代码的正确性。理解并实现掩码自注意力是构建Transformer解码器的关键一步。
008:编码器-解码器注意力


在本节课中,我们将学习编码器-解码器注意力,并最终理解“仅编码器”和“仅解码器”Transformer名称的由来。

概述

到目前为止,我们已经了解了仅编码器Transformer如何使用自注意力来创建上下文感知的词嵌入。
回顾:仅编码器与仅解码器Transformer

上一节我们介绍了仅编码器Transformer的自注意力机制。它生成的上下文感知嵌入可以用于聚类相似的句子或文档,也可以作为分类模型的输入。这只是仅编码器Transformer能力的开端。



我们也看到了仅解码器Transformer如何使用掩码自注意力来创建生成式输入,以生成长串的新词元。



原始Transformer架构
然而,在仅编码器和仅解码器Transformer出现之前,第一个被创造出来的Transformer包含两个部分。
- 一个称为编码器的部分,它使用自注意力。
- 一个称为解码器的部分,它使用掩码自注意力。
编码器和解码器相互连接,以便它们能够计算一种称为编码器-解码器注意力的机制。

编码器-解码器注意力原理

编码器-解码器注意力使用编码器的输出来计算键(K)和值(V)矩阵。查询(Q)矩阵则从解码器生成的掩码自注意力输出中计算。
一旦查询、键和值矩阵计算完成,编码器-解码器注意力就像自注意力一样,使用点积相似度进行计算。
以下是注意力计算的通用公式:
Attention(Q, K, V) = softmax(QK^T / sqrt(d_k)) V

序列到序列模型的应用
最初的Transformer基于一种称为序列到序列或编码器-解码器的模型。这类模型旨在将一种语言(如英语)的文本翻译成另一种语言(如西班牙语)。

例如,输入句子“Pizza is great.”,编码器会计算其自注意力。解码器则利用编码器的输出计算编码器-解码器注意力,进而生成翻译“La pizza es genial.”。


“仅编码器”与“仅解码器”名称的由来
在第一个编码器-解码器Transformer发明后不久,研究人员意识到可以构建仅使用编码器部分来完成有趣任务的模型,这类模型被称为仅编码器Transformer。
同样,人们也很快发现可以仅使用解码器来生成文本(包括文本翻译),这类模型被称为仅解码器Transformer。

至此,我们知道了“仅编码器”和“仅解码器”名称的来源。

交叉注意力
我们也学习了第三种注意力类型:编码器-解码器注意力,它也被称为交叉注意力。

编码器-解码器注意力的核心在于灵活地计算查询、键和值矩阵的来源。它简单地要求我们能够灵活处理Q、K、V矩阵的计算方式。

现代应用:多模态模型
尽管这种序列到序列的模型风格在语言建模中已不再那么流行,但我们仍然在所谓的多模态模型中看到它的身影。

在多模态模型中,可能有一个在图像或声音上训练过的编码器。其生成的上下文感知嵌入可以通过编码器-解码器注意力输入到一个基于文本的解码器中,从而生成图像描述或响应音频提示。


总结

本节课中,我们一起学习了编码器-解码器注意力(交叉注意力)的工作原理。我们了解到它是如何连接原始Transformer中的编码器和解码器部分的,并由此理解了“仅编码器”和“仅解码器”Transformer命名的历史渊源。最后,我们还看到了这种注意力机制在现代多模态AI模型中的重要应用。
009:多头注意力机制 🧠

在本节课中,我们将要学习多头注意力机制。我们将了解它的用途以及它如何被整合到Transformer架构中。
概述

上一节我们介绍了注意力机制如何帮助建立输入中每个词与其他词之间的关系。对于简单的例子,我们之前看到的方法工作得很好。然而,为了在更长、更复杂的句子和段落中正确建立词与词之间的关系,我们可以同时多次对编码值应用注意力机制。
多头注意力机制的概念

每个注意力单元被称为一个“头”,并且每个头都有自己独立的权重集,用于计算查询、键和值。

当我们有多个头同时计算注意力时,我们称之为多头注意力。

例如,在这个例子中,我们使用了三个注意力头。



然而,在首次描述Transformer的论文中,作者使用了八个注意力头。
在我们的例子中,有三个头,每个头产生两个注意力值。



我们最终得到了六个注意力值。为了将输出数量减少到我们开始时编码值的原始数量(2个),我们只需将所有注意力值连接到一个具有两个输出的全连接层。



减少输出数量的另一种方法

另一种常用的减少输出数量的方法是修改值权重矩阵的形状。

到目前为止,我们使用了一个具有两列权重的矩阵。这产生了一个具有两列的值矩阵。

因此,每个注意力头有两个输出。

然而,如果我们只使用一列权重,那么值矩阵将只有一列。

每个头将只输出一个值。
在这种情况下,由于我们开始时有两个编码值,我们最多需要两个头来恢复到相同的数量。或者,我们可以将Transformer编码得更灵活,以适应这些变化。

总结

本节课中我们一起学习了多头注意力机制的核心思想。我们了解到,通过使用多个独立的注意力头,模型可以同时从不同的表示子空间捕获信息,从而更有效地处理复杂序列中的关系。多头注意力是Transformer架构强大能力的关键组成部分。
010:使用PyTorch实现编码器-解码器注意力与多头注意力
概述

在本节课中,我们将学习如何使用PyTorch编写一个类,该类能够实现自注意力、掩码自注意力以及编码器-解码器注意力。我们还将编写一个实现多头注意力的类。通过动手编码,我们将深入理解这些核心注意力机制的工作原理。
实现通用的注意力类
上一节我们介绍了注意力机制的基本概念,本节中我们来看看如何用代码实现一个通用的注意力类。
我们首先导入必要的库。

import torch
import torch.nn as nn
import torch.nn.functional as F
接下来,我们定义一个名为 Attention 的类,它继承自 nn.Module。这个类将实现我们学过的三种注意力类型:自注意力、掩码自注意力和编码器-解码器注意力。
__init__ 方法与之前编写的代码完全相同。
class Attention(nn.Module):
def __init__(self, d_model, d_k, d_v):
super(Attention, self).__init__()
self.d_k = d_k
self.W_q = nn.Linear(d_model, d_k)
self.W_k = nn.Linear(d_model, d_k)
self.W_v = nn.Linear(d_model, d_v)
forward 方法有两处关键变化。首先,我们现在可以为查询、键和值指定不同的输入编码矩阵。其次,我们将这些可能不同的编码矩阵传递给相应的权重矩阵以生成查询、键和值。其余部分保持不变。
def forward(self, encodings_q, encodings_k, encodings_v):
Q = self.W_q(encodings_q)
K = self.W_k(encodings_k)
V = self.W_v(encodings_v)
scores = torch.matmul(Q, K.transpose(-2, -1)) / torch.sqrt(torch.tensor(self.d_k, dtype=torch.float32))
attention_weights = F.softmax(scores, dim=-1)
output = torch.matmul(attention_weights, V)
return output
现在,让我们运行一些数据来验证它是否按预期工作。
我们使用与之前相同的令牌编码矩阵作为查询的输入。
encodings_q = torch.tensor([[1.0, 0.0], [0.0, 1.0], [1.0, 1.0]])
然后,我们创建用于生成键和值的编码矩阵。在这个例子中,为了使结果易于与之前的比较,我让这些编码值相同。
encodings_k = torch.tensor([[1.0, 0.0], [0.0, 1.0], [1.0, 1.0]])
encodings_v = torch.tensor([[1.0, 0.0], [0.0, 1.0], [1.0, 1.0]])
设置随机数种子以确保结果可复现。
torch.manual_seed(42)
使用之前相同的参数创建我们的注意力类对象。
attn = Attention(d_model=2, d_k=2, d_v=2)
最后,将编码矩阵传递给我们的注意力对象。
output = attn(encodings_q, encodings_k, encodings_v)
print(output)
运行后,我们将得到注意力值。如果得到不同的结果,可以像之前一样通过手动计算来验证。
实现多头注意力类
理解了基础注意力类的实现后,本节我们来看看如何构建更强大的多头注意力机制。
我们首先定义一个名为 MultiHeadAttention 的类,它也继承自 nn.Module。
在 __init__ 方法中,我们添加一个新的参数 num_heads,它表示我们想要的注意力头的数量。
class MultiHeadAttention(nn.Module):
def __init__(self, d_model, d_k, d_v, num_heads):
super(MultiHeadAttention, self).__init__()
self.num_heads = num_heads
与往常一样,接下来我们调用父类的 __init__ 方法。然后,我们使用一个 for 循环来创建 num_heads 个注意力对象。
每个创建的注意力对象都用相同的 d_model、d_k 和 d_v 值进行初始化,并将它们存储在一个名为 heads 的 ModuleList 中。ModuleList 顾名思义,是一个我们可以索引的模块列表。
self.heads = nn.ModuleList([Attention(d_model, d_k, d_v) for _ in range(num_heads)])
__init__ 方法中最后要做的是保存 d_v 参数,以便后续使用。
self.d_v = d_v
forward 方法接收编码矩阵,然后使用一个 for 循环将这些矩阵传递给每个注意力头。
每个头返回的注意力值随后被拼接起来并返回。
def forward(self, encodings_q, encodings_k, encodings_v):
head_outputs = []
for head in self.heads:
head_output = head(encodings_q, encodings_k, encodings_v)
head_outputs.append(head_output)
# 沿最后一个维度拼接所有头的输出
concatenated = torch.cat(head_outputs, dim=-1)
return concatenated
以上就是实现多头注意力的全部代码。
现在,让我们运行一些数据来确保它按预期工作。
首先设置随机数种子。
torch.manual_seed(42)
然后创建并初始化一个多头注意力对象。参数 d_model、d_k 和 d_v 与之前相同。我们将 num_heads 设置为 1,以查看是否能得到与之前单头注意力相同的结果。
multi_head_attn_1 = MultiHeadAttention(d_model=2, d_k=2, d_v=2, num_heads=1)
接着传入我们之前制作的编码矩阵。
output_1 = multi_head_attn_1(encodings_q, encodings_k, encodings_v)
print("Output with 1 head:\n", output_1)
我们应该得到与之前单头注意力相同的结果。
现在,让我们用两个头来做同样的事情。
重置随机数种子。
torch.manual_seed(42)
创建一个新的 num_heads 等于 2 的多头注意力对象。
multi_head_attn_2 = MultiHeadAttention(d_model=2, d_k=2, d_v=2, num_heads=2)
然后传递编码矩阵。
output_2 = multi_head_attn_2(encodings_q, encodings_k, encodings_v)
print("Output with 2 heads:\n", output_2)
我们将得到之前两倍数量的注意力值(因为输出维度是 d_v * num_heads)。
总结

本节课中,我们一起学习了如何使用PyTorch实现一个通用的注意力类,该类支持自注意力、掩码自注意力和编码器-解码器注意力。我们还实现了一个多头注意力类,它通过并行运行多个注意力头来捕获输入序列中不同子空间的信息。通过具体的代码示例和测试,我们验证了这些类的正确性,为后续构建完整的Transformer模型打下了坚实的基础。
011:总结


在本课程中,我们学习了三种类型的注意力机制:自注意力、掩码自注意力和编码器-解码器注意力。
我们深入探讨了自注意力与掩码自注意力的优缺点,并学习了在何种情况下应使用哪一种。此外,我们还动手编写了这三种注意力机制的代码,并验证了它们的功能符合预期。
我期待看到你们未来能独立构建出怎样的成果。

浙公网安备 33010602011771号