《变形金刚百科全书:你需要了解的一切》
《变形金刚百科全书:掌握一切必备知识》
一切关于变形金刚的知识和实现方法

为什么要写另一篇变形金刚教程?
你可能已经听说过变形金刚,每个人都在谈论它,那为什么还要写一篇新文章呢?
好吧,我是一名研究员,这就要求我对使用的工具有很深入的了解(因为如果你不了解它们,你怎么能找出它们的问题并改进它们呢,对吧?)
随着我深入研究变形金刚的世界,我发现自己被大量资源淹没。然而,尽管阅读了所有这些,我对架构只有一般的了解,并留下一连串的疑问。

从文本到向量-嵌入过程:想象一下,我们的输入是一个单词序列,比如“猫喝牛奶”。这个序列的长度被称为seq_len。我们的即时任务是将这些词转换成模型可以理解的形式,具体来说就是向量。这就是嵌入器的作用。
每个单词都经历了一个转化过程,变成一个向量。这个转化过程被称为“嵌入”。每个向量或“嵌入”都有d_model = 512的大小。
那么,这个嵌入器到底是什么呢?在其核心,嵌入器是一个线性映射(矩阵),用E表示。你可以将其视为一个大小为(d_model, vocab_size)的矩阵,其中vocab_size是我们词汇的大小。
经过嵌入过程后,我们得到了一组大小为d_model的向量。理解这种格式很重要,因为它是一个经常出现的主题-你会在编码器输入、编码器输出等不同阶段看到它。
让我们来编写这部分代码:
class Embeddings(nn.Module): def __init__(self, d_model, vocab): super(Embeddings, self).__init__() self.lut = nn.Embedding(vocab, d_model) self.d_model = d_model def forward(self, x): return self.lut(x) * math.sqrt(self.d_model)
注意:我们乘以d_model是为了规范化(稍后解释)
注意2:我个人想知道是否可以使用预训练的嵌入器,或者至少从预训练的嵌入器开始,并进行微调。但实际上,嵌入是完全从零开始学习的,并且是随机初始化的。
位置编码
为什么需要位置编码?
在我们目前的设置中,我们拥有表示单词的向量列表。如果将其直接提供给变压器模型,那么就会缺少一个关键要素:单词的顺序。自然语言中的单词往往从其位置中获得意义。“John爱Mary”传达的情感与“Mary爱John”不同。为了确保我们的模型捕捉到这种顺序,我们引入了位置编码。
现在,你可能会想,“为什么不只是简单地按顺序递增,例如第一个词加1,第二个词加2,依此类推?”这种方法存在几个挑战:
- 多维性:每个标记用512个维度表示。仅仅增加一个值是不足以捕捉到这个复杂空间的。
- 归一化问题:理想情况下,我们希望的值位于-1和1之间。所以,直接添加大数(如对于长文本的+2000)会有问题。
- 序列长度相关性:直接递增不是与尺度无关的。对于一篇长文本,其中的位置可能是+5000,这个数字并不能真正反映标记在其所属句子中的相对位置。而一个单词的含义更多取决于它在句子中的相对位置,而不是它在文本中的绝对位置。
如果你学过数学,循环坐标的思想-具体来说,正弦和余弦函数-应该会让你感到熟悉。这些函数为我们提供了一种独特的编码位置的方式,满足我们的需求。
在我们的大小为(seq_len, d_model)的矩阵上,我们的目标是添加一个相同大小的另一个矩阵,即位置编码。
下面是核心概念:
- 对于每个标记,作者建议提供一对维度(2k)的<stron

这是以下图表的总结(但不要在此上过多思考)。关键是位置编码是一种数学函数,允许Transformer保持对句子中标记顺序的概念。这是一个非常活跃的研究领域。

class PositionalEncoding(nn.Module): "实现PE函数。" def __init__(self, d_model, dropout, max_len=5000): super(PositionalEncoding, self).__init__() self.dropout = nn.Dropout(p=dropout) # 在对数空间中计算位置编码。 pe = torch.zeros(max_len, d_model) position = torch.arange(0, max_len).unsqueeze(1) div_term = torch.exp( torch.arange(0, d_model, 2) * -(math.log(10000.0) / d_model) ) pe[:, 0::2] = torch.sin(position * div_term) pe[:, 1::2] = torch.cos(position * div_term) pe = pe.unsqueeze(0) self.register_buffer("pe", pe) def forward(self, x): x = x + self.pe[:, : x.size(1)].requires_grad_(False) return self.dropout(x)注意力机制(单头)
让我们深入了解Google论文的核心概念:注意力机制。
高层次直觉:
在其核心,注意力机制是向量/标记之间的一种通信机制。它允许模型在生成输出时专注于输入的特定部分。将其视为在输入数据的某些部分上聚光灯照射。这个“聚光灯”可以在更相关的部分上更亮(给予更多关注),在不相关的部分上更暗。
对于一个句子,注意力有助于确定单词之间的关系。在一个句子中,有些单词在意义或功能上密切相关,而其他单词则不是。注意力机制量化了这些关系。
例子:
考虑以下句子:“她把书给了他。”
如果我们关注“她”这个词,注意力机制可能确定以下事实:
- 它与“书”的关联性较强,因为“她”表示“书”的所有权。
- 它与“她”之间存在着VoAGI的联系,因为“她”和“她”可能指的是同一个实体。
- 它与“给了”或“他”等其他单词的关联性较弱。
深入技术讲解注意力机制

对于每个标记,我们生成三个向量:
- 查询(Q):
直觉:将查询视为标记提出的“问题”。它表示当前单词,并尝试找出对其相关的序列的哪些部分重要。
2. 键(K):
直觉:可以将键视为序列中每个单词的“标识符”。当查询“提问”时,关键有助于“回答”,通过确定每个单词与查询的相关性。
3. 值(V):
直觉: 一旦确定了每个单词(通过其键)与查询的相关性,我们需要来自这些单词的实际信息或内容来帮助当前的令牌。这就是值的作用所在。 它代表每个单词的内容。
Q、K、V是如何生成的?

查询和关键词之间的相似度是点积(衡量2个向量之间的相似度),除以该随机变量的标准差,以进行归一化处理。

让我们通过一个例子来说明:
假设我们有一个查询,想要使用K和V来计算注意力的结果:

现在让我们计算q1和关键词之间的相似度:

虽然数字3/2和1/8看起来相对接近,但是softmax函数的指数性质会放大它们的差异。

这种差异表明,q1与k1的关联性比与k2的关联性更为显著。
现在让我们看一下注意力的结果,即值的加权(注意力权重)组合

太棒了!对每个令牌(从q1到qn)重复此操作,得到n个向量的集合。
实际上,为了提高效率,此操作会矢量化为矩阵乘法。
让我们编写它:
def attention(query, key, value, mask=None, dropout=None): "计算'缩放点积注意力'" d_k = query.size(-1) scores = torch.matmul(query, key.transpose(-2, -1)) / math.sqrt(d_k) if mask is not None: scores = scores.masked_fill(mask == 0, -1e9) p_attn = scores.softmax(dim=-1) if dropout is not None: p_attn = dropout(p_attn) return torch.matmul(p_attn, value), p_attn多头注意力
单头注意力的问题是什么?
使用单头注意力方法时,每个令牌只能提出一个查询。一般而言,这意味着它与仅一个其他令牌建立了紧密的关系,因为softmax倾向于将一个值加权很重而将其他值减小到接近零。然而,当我们考虑语言和句子结构时,一个词通常与多个其他词有关联,而不仅仅是一个词。
为了解决这个限制,我们引入了多头注意力。核心思想是什么?让每个标记同时提出多个问题(查询),通过并行运行“h”次注意力过程来实现。原始的Transformer使用8个注意力头。

一旦我们获得了这8个注意力头的结果,我们将它们连接成一个矩阵。

编码这一步也很简单,我们只需小心处理维度:
class MultiHeadedAttention(nn.Module): def __init__(self, h, d_model, dropout=0.1): "接收模型大小和头数。" super(MultiHeadedAttention, self).__init__() assert d_model % h == 0 # 假设d_v始终等于d_k self.d_k = d_model // h self.h = h self.linears = clones(nn.Linear(d_model, d_model), 4) self.attn = None self.dropout = nn.Dropout(p=dropout) def forward(self, query, key, value, mask=None): "实现图2" if mask is not None: # 所有h个头都应用相同的掩码 mask = mask.unsqueeze(1) nbatches = query.size(0) # 1) 在整个批处理中,将所有线性投影从d_model => h x d_k query, key, value = [ lin(x).view(nbatches, -1, self.h, self.d_k).transpose(1, 2) for lin, x in zip(self.linears, (query, key, value)) ] # 2) 在整个批处理中应用注意力。 x, self.attn = attention(query, key, value, mask=mask, dropout=self.dropout) # 3) 使用视图来“连接”,并应用一个最终的线性层。 x = x.transpose(1, 2).contiguous().view(nbatches, -1, self.h * self.d_k) del query del key del value return self.linears[-1](x)现在你应该开始理解为什么Transformer如此强大,它充分利用了并行性。
组装Transformer的各个部分
从高层次而言,Transformer由3个元素组成:一个编码器,一个解码器和一个生成器

1. 编码器
- 目的:将输入序列转换为一个新序列(通常是较小维度),以捕捉原始数据的本质。
- 注意:如果你听说过BERT模型,它只使用了Transformer的这个编码部分。
2. 解码器
- 目的:使用编码器的输出序列生成一个输出序列。
- 注意:Transformer中的解码器与典型自动编码器的解码器不同。在Transformer中,解码器不仅考虑编码输出,还考虑到它已经生成的标记。
3. 生成器
- 目的:将向量转换为标记。它通过将向量投影到词汇表的大小,然后使用softmax函数选择最有可能的标记来实现这一目的。
让我们编写代码:
class EncoderDecoder(nn.Module): """ 标准的Encoder-Decoder架构。构成其他模型的基础。 """ def __init__(self, encoder, decoder, src_embed, tgt_embed, generator): super(EncoderDecoder, self).__init__() self.encoder = encoder self.decoder = decoder self.src_embed = src_embed self.tgt_embed = tgt_embed self.generator = generator def forward(self, src, tgt, src_mask, tgt_mask): "接受和处理带掩码的源和目标序列。" return self.decode(self.encode(src, src_mask), src_mask, tgt, tgt_mask) def encode(self, src, src_mask): return self.encoder(self.src_embed(src), src_mask) def decode(self, memory, src_mask, tgt, tgt_mask): return self.decoder(self.tgt_embed(tgt), memory, src_mask, tgt_mask)class Generator(nn.Module): "定义标准的线性+softmax生成步骤。" def __init__(self, d_model, vocab): super(Generator, self).__init__() self.proj = nn.Linear(d_model, vocab) def forward(self, x): return log_softmax(self.proj(x), dim=-1)这里有一个说明:“src”是指输入序列,“target”是指正在生成的序列。请记住,我们是以自回归的方式逐个标记生成输出,所以我们需要跟踪目标序列。
叠加编码器
Transformer的编码器不是仅仅一层。实际上,它是N层的堆叠。具体来说:
- 原始Transformer模型中的编码器由N=6个相同的层堆叠而成。
在编码器层内部,我们可以看到有两个非常相似的子层块((1)和(2)):一个残差连接后跟一个层归一化。
- 块 (1):自注意机制:帮助编码器在生成编码表示时专注于不同的输入单词。
- 块 (2):前馈神经网络:分别应用于每个位置的小型神经网络。

现在让我们编写代码:
首先是SublayerConnection:
我们遵循通用架构,可以通过“子层”更改为“自注意力”或“FFN”
class SublayerConnection(nn.Module): """ 一个残余连接后面跟着一个层归一化。 为了代码简洁性,规范是首先而不是最后运用的。 """ def __init__(self, size, dropout): super(SublayerConnection, self).__init__() self.norm = nn.LayerNorm(size) # 使用PyTorch的LayerNorm self.dropout = nn.Dropout(dropout) def forward(self, x, sublayer): "将残余连接应用于具有相同尺寸的任何子层。" return x + self.dropout(sublayer(self.norm(x)))现在我们可以定义完整的编码器层:
class EncoderLayer(nn.Module): "编码器由自注意力和前馈网络组成(在下面定义)" def __init__(self, size, self_attn, feed_forward, dropout): super(EncoderLayer, self).__init__() self.self_attn = self_attn self.feed_forward = feed_forward self.sublayer = clones(SublayerConnection(size, dropout), 2) self.size = size def forward(self, x, mask): # 自注意力,块1 x = self.sublayer[0](x, lambda x: self.self_attn(x, x, x, mask)) # 前馈网络,块2 x = self.sublayer[1](x, self.feed_forward) return x编码器层已经准备好,现在让我们将它们链在一起形成完整的编码器:
def clones(module, N): "生成N个相同的层" return nn.ModuleList([copy.deepcopy(module) for _ in range(N)])class Encoder(nn.Module): "主编码器是N层的堆叠" def __init__(self, layer, N): super(Encoder, self).__init__() self.layers = clones(layer, N) self.norm = nn.LayerNorm(layer.size) def forward(self, x, mask): "对输入(和掩码)依次通过每层。" for layer in self.layers: x = layer(x, mask) return self.norm(x)解码器
解码器和编码器一样,由多个相同的层堆叠而成。在原始Transformer模型中,这些层的数量通常为6层。
解码器与编码器有何不同?
第三个子层是为了与编码器进行交互而添加的:这就是交叉注意力
- 子层(1)与编码器相同。它是自注意力机制,意味着我们从输入到解码器的令牌中生成所有内容(Q、K、V)
- 子层(2)是新的通信机制:交叉注意力。之所以这样称呼,是因为我们使用(1)的输出来生成查询(Queries),并使用编码器的输出来生成键和值(K、V)。换句话说,要生成一个句子,我们必须同时关注解码器已经生成的内容(自注意力)以及在编码器中首次提出的要求(交叉注意力)
- 子层(3)与编码器中的相同。

现在让我们编码解码器层。如果你理解了编码器层中的机制,这个应该很简单。
class DecoderLayer(nn.Module): "解码器由自注意力、源注意力和前馈网络(下文定义)组成" def __init__(self, size, self_attn, src_attn, feed_forward, dropout): super(DecoderLayer, self).__init__() self.size = size self.self_attn = self_attn self.src_attn = src_attn self.feed_forward = feed_forward self.sublayer = clones(SublayerConnection(size, dropout), 3) def forward(self, x, memory, src_mask, tgt_mask): "遵循图1(右侧)进行连接。" m = memory x = self.sublayer[0](x, lambda x: self.self_attn(x, x, x, tgt_mask)) # 新子层(交叉注意力) x = self.sublayer[1](x, lambda x: self.src_attn(x, m, m, src_mask)) return self.sublayer[2](x, self.feed_forward)现在我们可以将N=6个解码器层连接起来形成解码器:
class Decoder(nn.Module): "带掩码的通用N层解码器。" def __init__(self, layer, N): super(Decoder, self).__init__() self.layers = clones(layer, N) self.norm = nn.LayerNorm(layer.size) def forward(self, x, memory, src_mask, tgt_mask): for layer in self.layers: x = layer(x, memory, src_mask, tgt_mask) return self.norm(x)到目前为止,你已经理解了Transformer的大约90%。还有一些细节:
Transformer模型细节
填充:
- 在典型的Transformer中,序列的最大长度是有限的(例如,“max_len=5000”)。这定义了模型可以处理的最长序列长度。
- 然而,现实世界中的句子长度可能会有所不同。为了处理较短的句子,我们使用填充。
- 填充是将特殊的“填充令牌”添加到批处理中的所有序列中,使它们的长度相等。

掩码
掩码确保在注意力计算过程中,某些令牌被忽略。
掩码适用于两种情况:
- 源掩码:由于我们在序列中添加了填充令牌,我们不希望模型关注这些无意义的令牌。因此,我们对其进行掩码。
- 目标掩码或向前掩码:在解码器中,当按顺序生成令牌时,每个令牌只应受到前面令牌的影响,而不应受到后续令牌的影响。例如,在生成句子的第5个词时,它不应该知道第6个词。这保证了令牌的顺序生成。

然后我们使用这个掩码来添加负无穷,以便相应的记号被忽略。以下示例应该能够澄清问题:

FFN: 前馈神经网络
- 在 Transformer 的图示中,“前馈”层有点误导人。它不仅仅是一个操作,而是一系列的操作。
- FFN 由两个线性层组成。有趣的是,输入数据可能是维度为
d_model=512的数据,首先被转换为更高的维度d_ff=2048,然后再映射回其原始维度(d_model=512)。 - 可以将这个过程想象为数据在操作中间被“扩展”,然后再“压缩”回其原始大小。

这个操作很容易实现:
class PositionwiseFeedForward(nn.Module): "实现前馈网络的方程." def __init__(self, d_model, d_ff, dropout=0.1): super(PositionwiseFeedForward, self).__init__() self.w_1 = nn.Linear(d_model, d_ff) self.w_2 = nn.Linear(d_ff, d_model) self.dropout = nn.Dropout(dropout) def forward(self, x): return self.w_2(self.dropout(self.w_1(x).relu()))结论
Transformer 模型的无与伦比的成功和普及可以归功于几个关键因素:
- 灵活性。Transformer 能够处理任何向量序列。这些向量可以是单词的嵌入向量。将这个概念转化到计算机视觉中可以将图像转换为不同的块,并将块展开为向量。在音频中,我们可以将音频分割成不同的片段并将其向量化。
- 通用性:Transformer 在最小程度的归纳偏差下,可以自由地捕捉数据中复杂而微妙的模式,从而使其能够学习和更好地泛化。
- 速度与效率:借助强大的 GPU 计算能力,Transformer 设计用于并行处理。
感谢阅读!在你离开之前:
你可以在我的 Transformer Github 仓库中运行这些实验。
要获取更多精彩的教程,请查看我的AI 教程合集在 Github 上
GitHub — FrancoisPorcher/awesome-ai-tutorials: 致力于让您成为数据科学专家的最佳 AI 教程合集!
最佳 AI 教程合集,助您成为数据科学专家!— GitHub …
github.com
您应该订阅我的文章。 在这里订阅。
如果您想要访问 VoAGI 上的高级文章,只需要支付每月 $5 的会员费用。如果您使用 我的链接进行注册,您将以无额外费用的方式支持我。
如果您发现本文内容深入且有益,请考虑关注我并为更深入的内容点赞!您的支持将帮助我继续创作有助于我们共同理解的内容。
参考资料
- 注意力机制就是你所需要的
- 加注解的Transformer(其中大部分代码受到他们的博客文章启发)
- Andrej Karpathy斯坦福演讲
进一步了解
即使有了一份全面的指南,与Transformer有关的其他领域依然很多。以下是一些您可能想要探索的想法:
- 位置编码:已经取得了显著的改进,您可以查看“相对位置编码”和“旋转位置嵌入(RoPE)”
- 层归一化和与批归一化、组归一化的不同之处
- 残差连接及其平滑梯度的效果
- 对BERT的改进(Roberta、ELECTRA、Camembert)
- 大型模型蒸馏为小型模型
- Transformer在其他领域(主要是视觉和音频)中的应用
- Transformer和图神经网络之间的联系



