《变形金刚百科全书:你需要了解的一切》

《变形金刚百科全书:掌握一切必备知识》

一切关于变形金刚的知识和实现方法

作者提供的图片

为什么要写另一篇变形金刚教程?

你可能已经听说过变形金刚,每个人都在谈论它,那为什么还要写一篇新文章呢?

好吧,我是一名研究员,这就要求我对使用的工具有很深入的了解(因为如果你不了解它们,你怎么能找出它们的问题并改进它们呢,对吧?)

随着我深入研究变形金刚的世界,我发现自己被大量资源淹没。然而,尽管阅读了所有这些,我对架构只有一般的了解,并留下一连串的疑问。

嵌入,图像经作者修改

从文本到向量-嵌入过程:想象一下,我们的输入是一个单词序列,比如“猫喝牛奶”。这个序列的长度被称为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,依此类推?”这种方法存在几个挑战:

  1. 多维性:每个标记用512个维度表示。仅仅增加一个值是不足以捕捉到这个复杂空间的。
  2. 归一化问题:理想情况下,我们希望的值位于-1和1之间。所以,直接添加大数(如对于长文本的+2000)会有问题。
  3. 序列长度相关性:直接递增不是与尺度无关的。对于一篇长文本,其中的位置可能是+5000,这个数字并不能真正反映标记在其所属句子中的相对位置。而一个单词的含义更多取决于它在句子中的相对位置,而不是它在文本中的绝对位置。

如果你学过数学,循环坐标的思想-具体来说,正弦和余弦函数-应该会让你感到熟悉。这些函数为我们提供了一种独特的编码位置的方式,满足我们的需求。

在我们的大小为(seq_len, d_model)的矩阵上,我们的目标是添加一个相同大小的另一个矩阵,即位置编码。

下面是核心概念:

  1. 对于每个标记,作者建议提供一对维度(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的联系,因为“她”和“她”可能指的是同一个实体。
    • 它与“给了”或“他”等其他单词的关联性较弱。

    深入技术讲解注意力机制

    缩放点积注意力,该图来自文章

    对于每个标记,我们生成三个向量:

    1. 查询(Q):

    直觉:将查询视为标记提出的“问题”。它表示当前单词,并尝试找出对其相关的序列的哪些部分重要。

    2. 键(K):

    直觉:可以将键视为序列中每个单词的“标识符”。当查询“提问”时,关键有助于“回答”,通过确定每个单词与查询的相关性。

    3. 值(V):

    直觉: 一旦确定了每个单词(通过其键)与查询的相关性,我们需要来自这些单词的实际信息或内容来帮助当前的令牌。这就是值的作用所在。 它代表每个单词的内容。

    Q、K、V是如何生成的?

    Q、K、V生成,图片来源是作者

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

    关注公式,文章中的图片

    让我们通过一个例子来说明:

    假设我们有一个查询,想要使用K和V来计算注意力的结果:

    Q、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):前馈神经网络:分别应用于每个位置的小型神经网络。
    Encoder Layer, residual connections, and Layer Norm,Image from article modified by author

    现在让我们编写代码:

    首先是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个词。这保证了令牌的顺序生成。
    因果掩码/前瞻掩码,图片由作者提供

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

    掩码,softmax 中的一种技巧,图片由作者提供

    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 模型的无与伦比的成功和普及可以归功于几个关键因素:

    1. 灵活性。Transformer 能够处理任何向量序列。这些向量可以是单词的嵌入向量。将这个概念转化到计算机视觉中可以将图像转换为不同的块,并将块展开为向量。在音频中,我们可以将音频分割成不同的片段并将其向量化。
    2. 通用性:Transformer 在最小程度的归纳偏差下,可以自由地捕捉数据中复杂而微妙的模式,从而使其能够学习和更好地泛化。
    3. 速度与效率:借助强大的 GPU 计算能力,Transformer 设计用于并行处理。

    感谢阅读!在你离开之前:

    你可以在我的 Transformer Github 仓库中运行这些实验。

    要获取更多精彩的教程,请查看我的AI 教程合集在 Github 上

    GitHub — FrancoisPorcher/awesome-ai-tutorials: 致力于让您成为数据科学专家的最佳 AI 教程合集!

    最佳 AI 教程合集,助您成为数据科学专家!— GitHub …

    github.com

    您应该订阅我的文章。 在这里订阅。

    如果您想要访问 VoAGI 上的高级文章,只需要支付每月 $5 的会员费用。如果您使用 我的链接进行注册,您将以无额外费用的方式支持我。

    如果您发现本文内容深入且有益,请考虑关注我并为更深入的内容点赞!您的支持将帮助我继续创作有助于我们共同理解的内容。

    参考资料

    进一步了解

    即使有了一份全面的指南,与Transformer有关的其他领域依然很多。以下是一些您可能想要探索的想法:

    • 位置编码:已经取得了显著的改进,您可以查看“相对位置编码”和“旋转位置嵌入(RoPE)”
    • 层归一化和与批归一化、组归一化的不同之处
    • 残差连接及其平滑梯度的效果
    • 对BERT的改进(Roberta、ELECTRA、Camembert)
    • 大型模型蒸馏为小型模型
    • Transformer在其他领域(主要是视觉和音频)中的应用
    • Transformer和图神经网络之间的联系