百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 编程字典 > 正文

Transformer到底是什么

toyiye 2024-06-21 12:39 14 浏览 0 评论

明显说的不是这个

赋予动机

当我还是个孩子的时候,我就对技术及其带来的创新着迷。一个特别感兴趣的领域是人工智能(AI)的基础,以及它的子集,机器学习(ML)。带着这种新的热情,我了解了计算机视觉 (CV) 以及计算机如何通过构建卷积神经网络 (CNN) 进行观察。我获得了自动驾驶汽车如何导航的直觉,因为我训练了强化学习(RL)模型。现在,我有动力去发现计算机是如何模拟语言的,因为我偶然发现了“注意力是你所需要的一切”的论文。

深入研究这篇论文后,它奠定的基础以及它对深度学习未来的影响给我留下了深刻的印象。这篇论文提出的细微差别是变革性的,并为强大的大型语言模型(LLM)铺平了道路,例如:GPT-2和3,BERT,XLNET,编码伴侣GitHub Copilot和非常著名的ChatGPT。 在我写这篇文章的时候,LLM 已经在我们的工作流程中占据了先例,减少了花在研究上的时间,帮助我们调试代码,有时还做功课......通过这些可能性和我的好奇心,我接受了构建负责它们的架构的挑战:变形金刚

承担这项任务非常困难,并且花了几个月的时间完善我对该主题的知识,但构建原始的序列到序列转换器让我对 LLM 及其在 AI 中的重要性有了更深入的了解。最后,我希望不仅能向你展示如何建造变形金刚,还希望能给你这个项目带来同样的热情。

先决条件

鉴于这个主题相对先进,我假设你有一些编程经验,并且你对人工智能背后的技术方面有一个基本的了解。因为我们将使用 PyTorch,所以建议你也有一些使用此 python 框架的背景知识。接下来,我们将介绍很多概念和术语;如果您不熟悉,我在下面留下了复习,以帮助您理解我们将讨论的一些主题。

  1. 机器学习 (ML)
  2. 神经网络 (NN)
  3. 深度学习 (DL)
  4. 自然语言处理 (NLP)
  5. 神经机器翻译 (NMT)

Transformer解释

首先,Transformer 是一个深度神经网络,它学习输入序列(源)和输出序列(目标)之间的关系,用于各种序列到序列任务,例如语言翻译。它使用标记的隐藏表示(嵌入)、标记在序列中的位置(位置编码)、标记之间的上下文相关性(注意力机制)、非线性关系(位置前馈网络)和其他一些深度学习技术(例如归一化、正则化等)来执行此任务。这种方法不仅使 Transformer 成为 NLP 任务的最先进 (SOTA) 架构,而且还解决了旧 NLP 架构(如 RNN)的一些缺陷。

注意:为了将来参考,我交替使用“标记”和“单词”,但两者的含义不同。标记可以被认为是一个单词或子单词(例如,“learning”被标记为标记“learn”和“ing”)。如果您对什么是标记化感到好奇,可以从 huggingface nlp 课程中了解更多信息。

编码器-解码器架构

在 NMT 中,编码器-解码器架构是一种常见的实现,用于将输入序列转换为用于转换的隐藏表示。在 Transformer 中,编码器采用长度为 n 的标记 xn 的输入序列,并将其编码为隐藏的表示 zn。接下来,解码器获取编码器 zn 的输出,并将其解码为长度为 m 的输出标记 ym 序列。此过程是自动回归的,因此解码器根据编码器输出 zn 上下文化的信息和之前预测的信息 ym?? 一次预测一个标记。

Transformer 架构(左边是编码器,右边是解码器)

嵌入

嵌入是将由标记组成的序列转换为隐藏表示的重要部分。简单地说,嵌入采用一系列单词,并将每个单词映射到一系列最能描述该单词的值中。每个单词的值由嵌入权重分配,这些权重可能会随着嵌入层学习整个词汇表中单词之间的关系而改变(本文中有关嵌入的更多信息)。

在“Attention Is All You Need”论文中,序列中的每个标记都将嵌入到 512 个值的向量中。(即 dm = 512)。展望未来,dm 是一个超参数,它定义了 Transformer 隐藏表示的维度。在实际论文中,它被引用为d型,但为了简单起见,我采用了我的符号。您将看到此超参数在整个 Transformer 中重复使用,随着您阅读本博客的进行,这将更有意义。

import torch.nn as nn  
  
class Embeddings(nn.Module):  
  
    def __init__(self, vocab_size, dm, pad_id) -> None:  
        super().__init__()  
        self.embedding = nn.Embedding(vocab_size, dm, padding_idx=pad_id)  
  
    def forward(self, x):  
        # inshape: x - (batch_size, seq_len)  
  
        # embed tokens to dm | shape: out - (batch_size, seq_len, dm)  
        out = self.embedding(x)  
        return out

位置编码

现在有一种方法可以理解输入序列中标记的含义,我们现在必须在位置上将它们相互关联。这很重要,因为我们不会说“星球大战是有史以来最伟大的电影系列”与“Wars greatest Stars the franchise movie of is greatest time all.”具有相同的含义,因此需要位置编码来捕获序列中标记的顺序。

由于 Transformer 模型不使用先前架构中的递归来定位关联标记,因此正弦模式(即正弦和余弦函数)用于编码序列中标记的位置。

位置编码函数

编码首先根据序列的最大长度和模型的隐藏维度创建。继续,上面的等式允许通过正弦曲线映射令牌的位置。pos 表示单词在标记化文本序列中的位置或索引,i 表示位置的隐藏表示的索引,d model 是模型的隐藏维度;我们之前说过 dm = 512

如果使用正弦函数绘制位置,则由于函数的波浪行为,不同的位置将具有不同的编码。虽然,一些具有不同位置的代币可能会获得相同的编码,因为正弦波重复。为了抵消这一点,i 用于为单个位置输出多个正弦波(频率),从而允许序列中每个标记的唯一编码。从本质上讲,根据其值是偶数还是奇数来创建交替的正弦波和余弦波,从而使模型在训练过程中更容易地“按相对位置参与”(本文中的视觉效果和更详细的解释)。

在处理输入时,位置编码器对嵌入和相应的位置编码求和,以捕获序列中标记的含义和顺序。

import torch  
import torch.nn as nn  
import numpy as np  
  
class PositionalEncoder(nn.Module):  
  
    def __init__(self, dm, maxlen, dropout=0.1, scale=True) -> None:  
        super().__init__()  
        self.dm = dm  
        self.drop = nn.Dropout(dropout)  
        self.scale = scale  
  
        # shape: pos - (maxlen, 1) dim - (dm, )  
        pos = torch.arange(maxlen).float().unsqueeze(1)  
        dim = torch.arange(dm).float()  
  
        # apply pos / (10000^2*i / dm) -> use sin for even indices & cosine for odd indices  
        values = pos / torch.pow(1e4, 2 * torch.div(dim, 2, rounding_mode="floor") / dm)  
        encodings = torch.where(dim.long() % 2 == 0, torch.sin(values), torch.cos(values))  
  
        # reshape: encodings - (1, maxlen, dm)  
        encodings = encodings.unsqueeze(0)  
          
        # register encodings w/o grad  
        self.register_buffer("pos_encodings", encodings)  
  
    def forward(self, embeddings):  
        # inshape: embeddings - (batch_size, seq_len, dm)  
  
        # scale embeddings (if applicable)  
        if self.scale:  
            embeddings = embeddings * np.sqrt(self.dm)  
        # sum embeddings w/ respective positonal encodings | shape: embeddings - (batch_size, seq_len, dm)  
        seq_len = embeddings.size(1)  
        embeddings = embeddings + self.pos_encodings[:, :seq_len]  
        # drop neurons | out - (batch_size, seq_len, dm)  
        out = self.drop(embeddings)  
        return out

注意:Transformer 中的常见做法是,不仅要按模型隐藏维度的平方根缩放嵌入以进行归一化,还要通过使用 dropout 删除 10% 的值来对其进行正则化(即 scale = sqrt(dm) 和 dropout = 0.1)。这些组件的添加有助于模型的学习能力,并防止其在训练期间过度拟合。

在解释了嵌入和位置编码之后,我们可以更深入地了解允许 Transformer 理解上下文信息的主要子层:注意力机制

注意力

注意力是一种机制,它采用查询和键值对,并根据查询和键之间的相似性对值应用权重。从某种意义上说,注意力机制允许 Transformer 学习如何将序列置于上下文中,以及如何“翻译”这些上下文化的序列。在 NMT 中,注意力有许多不同的实现,但在 Transformer 中,使用了缩放的点积注意力

缩放点-产品关注

缩放点积注意力图及功能

在缩放的点积注意力中,我们计算查询 (Q) 和键 (K) 之间的点积,两者都具有维度 dk。这将产生查询和键之间的相似性。接下来,相似性按模型隐藏维度的平方根进行缩放;这是防止在反向传播过程中由于在此操作之后应用 SoftMax 而可能发生的梯度递减的必要步骤。在将 softmax 应用于缩放的相似性后,我们得到了注意力权重。最后,我们在注意力权重和具有维度 dv 的值 (V) 之间进行矩阵乘法。此步骤的结果是传递序列的上下文向量。

import torch  
import torch.nn as nn  
import numpy as np  
  
class ScaledDotProductAttention(nn.Module):  
  
    def __init__(self, dk, dropout=None) -> None:  
        super().__init__()  
        self.dk = dk  
        self.drop = nn.Dropout(dropout)  
        self.softmax = nn.Softmax(dim=-1)  
          
    def forward(self, q, k, v, mask=None):  
        # inputs are projected | shape: q - (batch_size, *n_head, q_len, dk) k - (batch_size, *n_head, k_len, dk)  v - (batch_size, *n_head, k_len, dv)  
  
        # compute dot prod w/ q & k then scale | shape: similarities - (batch_size, *n_head, q_len, k_len)  
        similarities = torch.matmul(q, k.transpose(-2, -1)) / np.sqrt(self.dk)  
  
        # apply mask (if required)  
        if mask is not None:  
            mask = mask.unsqueeze(1) # for multi-head attention  
            similarities = similarities.masked_fill(mask == 0,-1e9)  
  
        # compute attention weights | shape: attention - (batch_size, *n_head, q_len, k_len)  
        attention = self.softmax(similarities)  
        # drop attention weights  
        attention = self.drop(attention)  
  
        # compute context given v | shape: context - (batch_size, *n_head, q_len, dv)  
        context = torch.matmul(attention, v)  
        return context, attention

注意:在我的实现中,我使用一个 dropout 层来正则化注意力权重,然后再将它们与值矩阵相乘。

缩放的点积注意力允许 Transformer 以给定的顺序评估令牌相对于彼此的重要性,同时并行进行。事实证明,这种并行能力对于该架构组件来说是一个巨大的优势,因为以前的架构都停留在顺序处理上。不知何故,“注意力就是你所需要的一切”论文的作者找到了通过多头注意力来利用这个组件的更多性能的方法。

多头注意力

多头注意力图

多头注意力是一个子层,通过将查询和键值对拆分为多个注意力头,同时执行缩放的点积注意力的多次计算。在此实现中,具有维度 dk 的查询和键,以及维度为 dv 的值,都是投影 h 倍,其中“h”表示注意头的数量。查询和键值对的投影由三个可学习的权重矩阵创建: Wq用于查询,Wk 用于键,Wv 用于值。查询和键值对的投影允许 Transformer 处理不同位置的多个子空间。

一旦投影并拆分为注意力头,就会在每个头内的查询和键值对之间同时计算缩放的点积注意力。接下来,将拆分为多个头的合成上下文向量连接在一起以形成单个上下文向量;与模型的尺寸相匹配。现在统一了,上下文向量使用不同的可学习权重矩阵进行投影,表示为 W?。此操作的结果将输出查询和键值对的最终上下文向量。

为了简化下面发生的事情,多头注意力让变形金刚从不同的角度查看序列的不同部分,从而提高了注意力的有效性。

class MultiHeadAttention(nn.Module):  
  
    def __init__(self, dm, dk, dv, nhead, bias=False, dropout=None) -> None:  
        super().__init__()  
        if dm % nhead != 0:  
            raise ValueError("Embedding dimensions (dm) must be evenly divisble by number of heads (nhead)")  
        self.dm = dm  
        self.dk = dk  
        self.dv = dv  
        self.nhead = nhead  
        self.wq = nn.Linear(dm, dk * nhead, bias=bias)  
        self.wk = nn.Linear(dm, dk * nhead, bias=bias)  
        self.wv = nn.Linear(dm, dv * nhead, bias=bias)  
        self.wo = nn.Linear(dv * nhead, dm)  
        self.scaled_dot_prod_attn = ScaledDotProductAttention(dk, dropout=dropout)  
  
    def forward(self, q, k, v, mask=None):  
        # inshape: q - (batch_size, q_len, dm) k & v - (batch_size, k_len, dm)  
        batch_size, q_len, k_len = q.size(0), q.size(1), k.size(1)  
  
        # linear projections into heads | shape: q - (batch_size, nhead, q_len, dk) k - (batch_size, nhead, k_len, dk) v - (batch_size, nhead, k_len, dv)  
        q = self.wq(q).view(batch_size, q_len, self.nhead, self.dk).transpose(1, 2)  
        k = self.wk(k).view(batch_size, k_len, self.nhead, self.dk).transpose(1, 2)  
        v = self.wv(v).view(batch_size, k_len, self.nhead, self.dv).transpose(1, 2)  
  
        # get context & attn weights | shape: attention - (batch_size, nhead, q_len, k_len) context - (batch_size, nhead, q_len, dv)  
        context, attention = self.scaled_dot_prod_attn(q, k, v, mask=mask)  
  
        # concat heads | shape: context - (batch_size, q_len, dm)  
        context = context.transpose(1, 2).contiguous().view(batch_size, q_len, self.dm)  
  
        # project context vector | shape: context - (batch_size, q_len, dm)  
        context = self.wo(context)  
        return context, attention

注意:用于投影查询和键值对的可学习权重矩阵没有偏差(即偏差 = False)。

填充掩码和无峰值后续掩码

您可能已经发现了掩码在我们的 PyTorch 实现中的应用,用于计算缩放的点积注意力。掩码对于注意力机制至关重要,并在两个特定用例中发挥作用:

  1. 确保在计算注意力时忽略序列中的填充位置。
  2. 防止解码器在训练期间预测单词时获得不公平的优势。

忽略填充

为了实现并行和高效的训练,序列被批处理在一起,其中批处理中的所有序列必须具有相同的序列长度。由于并非所有序列在训练数据中都具有完全相同的长度,因此我们将序列填充到相同的长度,从而允许将它们批处理在一起。由于填充对序列的上下文含义为零,因此我们忽略了有填充的嵌入值;这正是填充蒙版的作用。

def generate_pad_mask(seq, pad_id):  
    # inshape: seq - (batch_size, seq_len)  
  
    # mark non-pad True & pad False   
    mask = (seq != pad_id).unsqueeze(-2)  
    # outshape: mask - (batch_size, 1, seq_len)  
    return mask

当蒙版应用于缩放的点积注意力时,有填充的位置被标记为 False,被填充为一个非常大的负数(例如 -1,000,000,000)。一旦应用softmax来获得注意力权重,这些值将非常微不足道,以至于对于填充位置,更新权重的梯度可以忽略不计。简单地说,在对序列进行上下文化时,将忽略填充位置。

无峰值后续掩码

在训练阶段,无峰值后续掩码是必要的,因为它们确保解码器不会关注序列的后续位置,而是关注它已经在序列中预测的位置。通俗地说,它确保解码器学会从给定句子中已经预测的单词中连续(即一个接一个)预测每个单词,而不是向前预测同一句子中的单词。

这是通过制作一个 l x l 矩阵来实现的,其中 l 是序列的长度。矩阵的行表示解码器可以在“时间步长”上处理的序列的位置,列表示标记在序列中的位置。标记为 True 的职位可以处理,而标记为 False 的职位则不能。

import torch  
  
def generate_nopeak_pad_mask(trg, pad_id):  
    # inshape: trg - (batch_size, trg_len)  
  
    # create pad mask (True = no pad False = pad) | shape: trg_mask - (batch_size, 1, trg_len)  
    trg_mask = generate_pad_mask(trg, pad_id)  
    # create subsequent mask | shape: trg_nopeak_mask - (1, trg_len, trg_len)  
    trg_len = trg.size(1)  
    trg_nopeak_mask = torch.triu(torch.ones((1, trg_len, trg_len)) == 1)  
    trg_nopeak_mask = trg_nopeak_mask.transpose(1, 2)  
    # combine pad & subsequent mask shape  
    trg_mask = trg_mask & trg_nopeak_mask  
    # outshape: trg_mask - (batch_size, trg_len, trg_len)  
    return trg_mask

由于填充规则仍然适用,因此无论解码器是否可以处理该位置,无峰值后续掩码都会与相应的填充掩码组合(逻辑上和),以将填充的位置保留为 False。从那里开始,当应用softmax时,遵循相同的原则,基本上否定了对解码器中后续位置和填充位置的关注。

标记化序列的张量示例(pad token id = 0)

填充掩码,例如张量

无峰值后续掩码,例如张量

def generate_masks(src, trg, pad_id):  
    # inshape: src - (batch_size, src_len) trg - (batch_size, trg_len)  
  
    # create pad mask for src (True = no pad False = pad)  
    src_mask = generate_pad_mask(src, pad_id)  
    # generate pad nopeak mask for trg  
    trg_mask = generate_nopeak_pad_mask(trg, pad_id)  
    # outshape: src_mask - (batch_size, 1, src_len) trg_mask - (batch_size, trg_len, trg_len)  
    return src_mask, trg_mask

注意:此代码段为源序列和目标序列生成所需的掩码。

位置前馈网络

随着大部分肮脏工作的到来,我们可以探索位置前馈网络。该子层是进一步提高 Transformer 学习能力的关键。

位置前馈网络功能

前馈网络由两个可学习的权重矩阵 W?W? 组成,两者之间有一个 ReLU 激活。两个矩阵的维度均由模型的隐藏维度和网络的指定维度定义。使用“Attention Is All You Need”论文中的参数,矩阵的尺寸分别为 512x2048 和 2048x512(即 dff = 2048)。

位置前馈网络对于编码器和解码器模块都是必不可少的,因为它对注意力模块进行了参数化。如果没有它,传递到后续层中注意力模块的上下文向量将只是被“重新平均”,从而阻碍模型的学习能力。因此,有必要包含它,以允许更多的模型功能来学习数据中的复杂模式(有关其实现的更多信息,请点击此处)。

import torch.nn as nn  
  
class FeedForwardNetwork(nn.Module):  
  
    def __init__(self, dm, dff, dropout=0.1) -> None:  
        super().__init__()  
        self.w1 = nn.Linear(dm, dff)  
        self.w2 = nn.Linear(dff, dm)  
        self.relu = nn.ReLU(inplace=False)  
        self.drop = nn.Dropout(dropout)  
  
    def forward(self, x):  
        # inshape: x - (batch_size, seq_len, dm)  
          
        # first linear transform with ReLU | shape: x - (batch_size, seq_len, dff)  
        x = self.relu(self.w1(x))  
        # drop neurons  
        x = self.drop(x)  
        # second linear transform | shape: out - (batch_size, seq_len, dm)  
        out = self.w2(x)  
        return out

注意:在我的实现中,我在第二次线性变换之前删除神经元,以帮助泛化并减少训练期间网络过拟合的机会。

图层归一化

通过 PyTorch 的层归一化公式

最后,但并非最不重要的一点是,我们有层归一化模块(LayerNorm)。图层归一化是一种用于根据均值和方差对输入要素进行归一化的技术。在训练过程中,层归一化模块使用 gamma (γ) 进行缩放,然后使用 beta (β) 来移动特征的均值和方差。gamma 和 beta 都是可学习的参数,当模块试图稳定均值和方差时,它们可能会进行调整。在 Transformer 中,被归一化的特征是标记化序列的各种隐藏表示。

Encoder 和 Decoder 模块中集成了层归一化功能,具有多种优势。首先,它可以在训练过程中稳定梯度,从而提高学习性能。它还使收敛速度更快,从而大大缩短了训练时间。最后,它的存在可能会在推理过程中引入更好的泛化(这是一篇深入研究层归一化的研究论文,以便进一步理解)。

import torch  
import torch.nn as nn  
  
class Norm(nn.Module):  
  
    def __init__(self, dm, eps=1e-6):  
        super().__init__()  
        self.gamma = nn.Parameter(torch.ones(dm))  
        self.beta = nn.Parameter(torch.zeros(dm))  
        self.eps = eps  
  
    def forward(self, x: torch.Tensor):  
        # inshape: x - (batch_size, seq_len, dm)  
  
        # calc mean & variance (along dm)  
        mean = x.mean(dim=-1, keepdim=True)  
        var = x.var(dim=-1, unbiased=True, keepdim=True)  
        # normalize, scale & shift | shape: out - (batch_size, seq_len, dm)  
        norm = (x - mean) / torch.sqrt(var + self.eps)  
        out = norm * self.gamma + self.beta  
        return out

描述完所有子层和模块后,我们可以同时创建编码器和解码器。


编码器块

编码器块

在“注意力就是你所需要的一切”论文中,编码器使用多种组件来有效运行。它的主要组成部分是多头注意力和位置前馈网络子层。最重要的是,丢弃、残差连接和层归一化用于生成子层的最终输出。

Residual Connections

In the Transformer, all sublayers have an output shape identical to the dimensions of the model (d? = 512) which is intended to allow for residual connections. Residual connections are a key technique found in both the Encoder and Decoder blocks of the Transformer. They function as shortcuts for gradients between sublayers, preventing information from being lost during back-propagation. Since summation is a linear operation, gradients passing through residual connections will be unimpeded during back-propagation, even if some sublayers produce small gradients. Residual connections also serve to keep information consistent with the original inputs of sublayers. In multi-head attention, inputs are arbitrarily permuted which alters their original representation. Residual connections, pretty much, help sublayers ‘remember’ what their original inputs were. This ensures sublayer ouputs computed genuinely come from their original inputs and not from permuted alterations (further explanation).

Dropout

Dropout works by ignoring a fraction of inputs (i.e. setting their value to zero), meaning the model is forced to learn different representations of inputs independently. This regularization can make the model less prone to overfitting, and increase its robustness when generalizing to unseen inputs during inference (you can find out more about dropout from this research paper).

Sublayer Output

Residual connections and dropout are used to generate the final output of a sublayer. The function that describes this output before it’s passed to another can be defined by the pseudocode below:

output = LayerNorm(x + dropout(Sublayer(x)))

Pivoting back to the Encoder block, an input sequence (source) is embedded then positionally encoded. Following that, the result is passed to the multi-head attention sublayer where the context vector is computed. Dropout is then applied to the context vector, at which it is then summed with the original input of the multi-head attention sublayer via a residual connection. Lastly, the sum is normalized and passed as a new input for the position-wise feed-forward network.

For the position-wise feed-forward network, the same process is repeated, except the input is passed through the feed-forward network instead of the multi-head attention sublayer. This generates the final output of the Encoder block, which will later be used as an input in the Decoder block for Encoder-Decoder attention.

import torch.nn as nn  
from embedding import Embeddings  
from pos_encoder import PositionalEncoder  
from attention import MultiHeadAttention  
from norm import Norm  
from feedforward import FeedForwardNetwork  
  
class EncoderLayer(nn.Module):  
  
    def __init__(self, dm, dk, dv, nhead, dff, bias=False, dropout=0.1, eps=1e-6) -> None:  
        super().__init__()  
        self.multihead = MultiHeadAttention(dm, dk, dv, nhead, bias=bias, dropout=dropout)  
        self.feedforward = FeedForwardNetwork(dm, dff, dropout=dropout)  
        self.norm1 = Norm(dm, eps=eps)  
        self.norm2 = Norm(dm, eps=eps)  
        self.drop1 = nn.Dropout(dropout)  
        self.drop2 = nn.Dropout(dropout)  
  
    def forward(self, src, src_mask=None):  
        # inshape: src - (batch_size, src_len, dm)  
  
        # get context | shape - x_out (batch_size, src_len, dm)  
        x = src  
        x_out, attn = self.multihead(x, x, x, mask=src_mask)  
        # drop neurons  
        x_out = self.drop1(x_out)  
        # add & norm (residual connections) | shape: x - (batch_size, src_len, dm)  
        x = self.norm1(x + x_out)  
  
        # linear transforms | shape: x_out (batch_size, src_len, dm)  
        x_out = self.feedforward(x)   
        # drop neurons  
        x_out = self.drop2(x_out)  
        # add & norm (residual connections) | shape: out - (batch_size, src_len, dm)  
        out = self.norm2(x + x_out)  
        return out, attn  
  
class Encoder(nn.Module):  
  
    def __init__(self, vocab_size, maxlen, pad_id, dm, dk, dv, nhead, dff, layers=6, bias=False,   
                 dropout=0.1, eps=1e-6, scale=True) -> None:  
        super().__init__()  
        self.embeddings = Embeddings(vocab_size, dm, pad_id)  
        self.pos_encodings = PositionalEncoder(dm, maxlen, dropout=dropout, scale=scale)  
        self.stack = nn.ModuleList([EncoderLayer(dm, dk, dv, nhead, dff, bias=bias, dropout=dropout, eps=eps)   
                                    for l in range(layers)])  
  
    def forward(self, src, src_mask=None):  
        # inshape: src - (batch_size, src_len, dm) src_mask - (batch_size, 1, src_len)  
  
        # embeddings + positional encodings | shape: x - (batch_size, src_len, dm)  
        x = self.embeddings(src)  
        x = self.pos_encodings(x)  
        # pass src through stack of encoders (out of layer is in for next)  
        for encoder in self.stack:  
            x, attn = encoder(x, src_mask=src_mask)  
        # shape: out - (batch_size, src_len, dm)  
        out = x  
        return out, attn

注意:编码器块可以堆叠多次,其中前一个块的输出是下一个块的输入。高潮,或者更准确地说,这些块的堆叠以及源嵌入和位置编码,是编码器的全部。在“Attention Is All You Need”论文中,基本模型有一堆 6 个(即 N = 6)。

解码器模块

解码器模块

Decoder 模块与 Encoder 模块非常相似,因为它嵌入并对其输入进行位置编码,使用相同的子层输出方程(参见残差连接和丢弃部分),并使用按位置前馈网络作为其最终子层。但是,它采用了遮罩的多头注意力,然后是我们之前提到的编码器-解码器注意力。

屏蔽的多头注意力类似于 Encoder 模块中的多头注意力。不同之处在于,它应用了一个无峰值的后续掩码(请参阅填充掩码和无峰值后续掩码部分),以防止解码器在学习生成输出序列(目标)时过早预测标记或“作弊”。

当计算出被屏蔽的多头注意力时,上下文向量将传递到下一个多头注意力子层,用于编码器-解码器注意力。在此实例中,从屏蔽的多头注意力生成的上下文向量用作查询,而编码器的输出用于键值对。此步骤教模型如何将源序列“翻译”为目标序列。最后,从编码器-解码器注意力计算出的上下文向量通过按位置前馈网络传递,从而创建解码器模块的最终输出。

import torch.nn as nn  
from embedding import Embeddings  
from pos_encoder import PositionalEncoder  
from attention import MultiHeadAttention  
from norm import Norm  
from feedforward import FeedForwardNetwork  
  
class DecoderLayer(nn.Module):  
  
    def __init__(self, dm, dk, dv, nhead, dff, bias=False, dropout=0.1, eps=1e-6) -> None:  
        super().__init__()  
        self.maskmultihead = MultiHeadAttention(dm, dk, dv, nhead, bias=bias, dropout=dropout)  
        self.multihead = MultiHeadAttention(dm, dk, dv, nhead, bias=bias, dropout=dropout)  
        self.feedforward = FeedForwardNetwork(dm, dff, dropout=dropout)  
        self.norm1 = Norm(dm, eps=eps)  
        self.norm2 = Norm(dm, eps=eps)  
        self.norm3 = Norm(dm, eps=eps)  
        self.drop1 = nn.Dropout(dropout)  
        self.drop2 = nn.Dropout(dropout)  
        self.drop3 = nn.Dropout(dropout)  
  
    def forward(self, src, trg, src_mask=None, trg_mask=None):  
        # inshape: src - (batch_size src_len, dm) trg - (batch_size, trg_len, dm) \  
        # src_mask - (batch_size, 1 src_len) trg_mask - (batch_size trg_len, trg_len)/(batch_size, 1 , trg_len)  
  
        # calc masked context | shape: x_out - (batch_size, trg_len, dm)  
        x = trg  
        x_out, attn1 = self.maskmultihead(x, x, x, mask=trg_mask)  
        # drop neurons  
        x_out = self.drop1(x_out)  
        # add & norm (residual connections) | shape: x - (batch_size, trg_len, dm)  
        x = self.norm1(x + x_out)  
  
        # calc context | shape: x_out - (batch_size, trg_len, dm)  
        x_out, attn2 = self.multihead(x, src, src, mask=src_mask)  
        # drop neurons  
        x_out = self.drop2(x_out)  
        # add & norm (residual connections) | shape: x - (batch_size, trg_len, dm)  
        x = self.norm2(x + x_out)  
  
        # calc linear transforms | shape: x_out - (batch_size, trg_len, dm)  
        x_out = self.feedforward(x)  
        # drop neurons  
        x_out = self.drop3(x_out)  
        # add & norm (residual connections) | shape: out - (batch_size, trg_len, dm)  
        out = self.norm3(x + x_out)  
        return out, attn1, attn2  
      
class Decoder(nn.Module):  
  
    def __init__(self, vocab_size, maxlen, pad_id, dm, dk, dv, nhead, dff, layers=6, bias=False,   
                 dropout=0.1, eps=1e-6, scale=True) -> None:  
        super().__init__()  
        self.embeddings = Embeddings(vocab_size, dm, pad_id)  
        self.pos_encodings = PositionalEncoder(dm, maxlen, dropout=dropout, scale=scale)  
        self.stack = nn.ModuleList([DecoderLayer(dm, dk, dv, nhead, dff, bias=bias, dropout=dropout, eps=eps)   
                                    for l in range(layers)])  
          
    def forward(self, src, trg, src_mask=None, trg_mask=None):  
        # inshape: src - (batch_size, src_len, dm) trg - (batch_size, trg_len, dm)  
  
        # embeddings + positional encodings | shape: x - (batch_size, trg_len, dm)  
        x = self.embeddings(trg)  
        x = self.pos_encodings(x)  
        # pass src & trg through stack of decoders (out of layer is in for next)  
        for decoder in self.stack:  
            x, attn1, attn2 = decoder(src, x, src_mask=src_mask, trg_mask=trg_mask)  
        out = x  
        return out, attn1, attn2

注意:与编码器模块类似,解码器模块可以堆叠,也可以与目标嵌入和位置编码配对,以形成解码器的整体。原始论文使用N = 6的堆栈。

现在,与解码器的输出没有太大关系,因为它只是它的最终隐藏表示。由于我们的目标是生成一个向量,其中每个位置都包含目标词汇表中每个单词的概率列表,因此隐藏的表示被转换了。

线性变换和 Softmax

线性变换和softmax应用

线性变换

首先,解码器输出需要从模型维度的连续向量空间转换为目标词汇表的表示。这可以通过添加一个可学习的线性层来实现,该线性层既具有模型的维度,又具有目标词汇表中的标记数(即 dm x Vt,其中 Vt 是目标词汇表中的标记数)。

Softmax的

下一步是为序列中的每个位置在目标词汇表上创建概率分布。这可以通过沿目标词汇表维度在变换后的向量上计算 softmax 轻松实现。一旦应用,它就会生成一个序列,其中每个位置对应于目标词汇表中每个单词的概率列表,即预测的输出标记概率。

把它们放在一起

随着隐藏的细节和复杂性的讨论,我们终于可以开始将每一块拼图放在一起来构建变形金刚。

import torch.nn as nn  
from encoder import Encoder  
from decoder import Decoder  
  
class Transformer(nn.Module):  
      
    def __init__(self, vocab_enc, vocab_dec, maxlen, pad_id, dm=512, dk=64, dv=64, nhead=8, layers=6,   
                dff=2048, bias=False, dropout=0.1, eps=1e-6, scale=True) -> None:  
        super().__init__()  
        self.encoder = Encoder(vocab_enc, maxlen, pad_id, dm, dk, dv, nhead, dff,   
                        layers=layers, bias=bias, dropout=dropout, eps=eps, scale=scale)            
        self.decoder = Decoder(vocab_dec, maxlen, pad_id, dm, dk, dv, nhead, dff,   
                        layers=layers, bias=bias, dropout=dropout, eps=eps, scale=scale)  
        self.linear = nn.Linear(dm, vocab_dec)  
        self.maxlen = maxlen  
        self.pad_id = pad_id  
        self.apply(xavier_init)  
  
    def forward(self, src, trg, src_mask=None, trg_mask=None):  
        # inshape: src - (batch_size, src_len) trg - (batch_size, trg_len)\  
        # src_mask - (batch_size, 1, src_len) trg_mask - (batch_size, 1, trg_len, trg_len)  
          
        # encode embeddings | shape: e_out - (batch_size, src_len, dm)  
        e_out, attn = self.encoder(src, src_mask=src_mask)  
  
        # decode embeddings | shape: d_out - (batch_size, trg_len, dm)  
        d_out, attn, attn = self.decoder(e_out, trg, src_mask=src_mask, trg_mask=trg_mask)  
        # linear transform decoder output | shape: out - (batch_size, trg_len, vocab_size)  
        out = self.linear(d_out)  
        return out  
  
def xavier_init(module):  
    if hasattr(module, "weight") and module.weight.dim() > 1:  
        init.xavier_uniform_(module.weight.data)

注意:我想指出的是,在我们的代码中转换解码器输出后,没有应用softmax。原因是因为在 PyTorch 中计算损失时,用于训练 Transformer 的损失函数 cross-entropy loss 会为您应用 softmax。此外,Xavier 权重初始化用于阻止梯度消失和爆炸,并为模型提供一个良好的起点,以便在训练期间收敛(可以从本文中找到有关权重初始化的进一步直觉)。


训练

我们无法真正用 Transformer 做任何事情,除非我们用一些数据来训练它来做一些翻译。下面是一个通用的训练函数,它使用自定义的 Pytorch DataLoader、Optimizer 和可选的所需设备(例如用于并行 GPU 计算的“cuda”)来训练您的 Transformer 并在一定数量的 epoch 内对其进行训练。

import numpy as np  
import torch.nn as nn  
from utils.functional import generate_masks  
  
def train(dataloader, model, optimizer, epochs=1000, device=None):  
    # setup  
    model.train()  
    m = len(dataloader)  
    cross_entropy = nn.CrossEntropyLoss(ignore_index=model.pad_id)  
    losses = []  
  
    # train over epochs  
    print("Training Started")  
    for epoch in range(epochs):  
        accum_loss = 0 # reset accumulative loss  
        for inputs, labels in dataloader:  
            # get src & trg  
            src, trg, out = inputs, labels[:, :-1], labels[:, 1:] # shape: src - (batch_size, src_len) trg & out - (batch_size, trg_len)  
            src, trg, out = src.long(), trg.long(), out.long()  
            # generate the masks  
            src_mask, trg_mask = generate_masks(src, trg, model.pad_id)  
            # move to device   
            src, trg, out = src.to(device), trg.to(device), out.to(device)  
            src_mask, trg_mask = src_mask.to(device), trg_mask.to(device)  
  
            # zero the grad  
            optimizer.zero_grad()  
            # get pred & reshape outputs  
            pred = model(src, trg, src_mask=src_mask, trg_mask=trg_mask) # shape: pred - (batch_size, seq_len, vocab_size)  
            pred, out = pred.contiguous().view(-1, pred.size(-1)), out.contiguous().view(-1) # shape: pred - (batch_size * seq_len, vocab_size) out - (batch_size * seq_len)  
            # calc grad & update model params  
            loss = cross_entropy(pred, out)  
            loss.backward()  
            optimizer.step()  
            # accumulate loss over time  
            accum_loss += loss.item()  
  
        # get epoch loss & keep track  
        epoch_loss = accum_loss / m  
        losses.append(epoch_loss)  
        print(f"Epoch {epoch + 1} Complete | Loss: {epoch_loss:.4f}")  
  
    # calc avg train loss  
    loss = np.mean(losses).item()  
    print(f"Training Complete | Average Loss: {loss:.4f}")  
    return loss

实验

在我的实验中,我训练了 Transformer 执行英语到德语的翻译。我使用 torchtext 版本 0.4.0 中的 Multi30k 数据集训练和评估了该模型。对于配置和超参数,我复制了“Attention Is All You Need”论文的基本模型中的设置。我使用相同的 Adam 优化器,初始学习率为 0.00001(即 lr = 1e-5),beta? = 0.9,beta? = 0.98。 我还包括一个调度程序,如果测试损失稳定了 10 个 epoch,它会将学习率降低 10%。最后,我使用波束宽度为 3 的波束搜索在推理过程中解码令牌。

我还使用许多其他模块、工具和超参数来帮助模型在训练期间查看其性能。如果有兴趣,您可以在我的 GitHub 存储库中找到我的完整实现。

结果

在 Lambda Cloud 的 Nvidia A10 GPU 上训练模型超过 1000 个周期后,与原始“注意力就是您所需要的一切”论文中的基础 Transformer 相比,我能够获得卓越的性能。

Transformer 模型的训练片段

对于我的模型,Multi30k 数据集的平均训练损失为 1.2493,平均测试损失为 2.5804,最佳 BLEU(双语评估替补)得分为 25.7。与“注意力就是你所需要的”论文中概述的类似任务(具体为25.8)中评估的Transformer的结果相比,这个结果的delta为0.1。

训练后 Transformer 的指标性能(红色为训练损失,蓝色为测试损失)


结论

我们不仅一步一步地演练了 Transformer 模型,而且还使用 PyTorch 构建了一个模型,并且我们成功地训练了该模型,在对英语到德语的翻译数据集进行评估时,该模型取得了可观的性能。

说到这里,我希望我能够帮助您处理 Transformer 的复杂性,同时阐明它的功能,使其成为构建 LLM 的可行架构。完成这项工作后,我要感谢您对本文的考虑,对于未来的工作,我计划更深入地研究仅解码器的 Transformer 模型,其中最著名的是在 ChatGPT 中发现的。

相关推荐

为何越来越多的编程语言使用JSON(为什么编程)

JSON是JavascriptObjectNotation的缩写,意思是Javascript对象表示法,是一种易于人类阅读和对编程友好的文本数据传递方法,是JavaScript语言规范定义的一个子...

何时在数据库中使用 JSON(数据库用json格式存储)

在本文中,您将了解何时应考虑将JSON数据类型添加到表中以及何时应避免使用它们。每天?分享?最新?软件?开发?,Devops,敏捷?,测试?以及?项目?管理?最新?,最热门?的?文章?,每天?花?...

MySQL 从零开始:05 数据类型(mysql数据类型有哪些,并举例)

前面的讲解中已经接触到了表的创建,表的创建是对字段的声明,比如:上述语句声明了字段的名称、类型、所占空间、默认值和是否可以为空等信息。其中的int、varchar、char和decimal都...

JSON对象花样进阶(json格式对象)

一、引言在现代Web开发中,JSON(JavaScriptObjectNotation)已经成为数据交换的标准格式。无论是从前端向后端发送数据,还是从后端接收数据,JSON都是不可或缺的一部分。...

深入理解 JSON 和 Form-data(json和formdata提交区别)

在讨论现代网络开发与API设计的语境下,理解客户端和服务器间如何有效且可靠地交换数据变得尤为关键。这里,特别值得关注的是两种主流数据格式:...

JSON 语法(json 语法 priority)

JSON语法是JavaScript语法的子集。JSON语法规则JSON语法是JavaScript对象表示法语法的子集。数据在名称/值对中数据由逗号分隔花括号保存对象方括号保存数组JS...

JSON语法详解(json的语法规则)

JSON语法规则JSON语法是JavaScript对象表示法语法的子集。数据在名称/值对中数据由逗号分隔大括号保存对象中括号保存数组注意:json的key是字符串,且必须是双引号,不能是单引号...

MySQL JSON数据类型操作(mysql的json)

概述mysql自5.7.8版本开始,就支持了json结构的数据存储和查询,这表明了mysql也在不断的学习和增加nosql数据库的有点。但mysql毕竟是关系型数据库,在处理json这种非结构化的数据...

JSON的数据模式(json数据格式示例)

像XML模式一样,JSON数据格式也有Schema,这是一个基于JSON格式的规范。JSON模式也以JSON格式编写。它用于验证JSON数据。JSON模式示例以下代码显示了基本的JSON模式。{"...

前端学习——JSON格式详解(后端json格式)

JSON(JavaScriptObjectNotation)是一种轻量级的数据交换格式。易于人阅读和编写。同时也易于机器解析和生成。它基于JavaScriptProgrammingLa...

什么是 JSON:详解 JSON 及其优势(什么叫json)

现在程序员还有谁不知道JSON吗?无论对于前端还是后端,JSON都是一种常见的数据格式。那么JSON到底是什么呢?JSON的定义...

PostgreSQL JSON 类型:处理结构化数据

PostgreSQL提供JSON类型,以存储结构化数据。JSON是一种开放的数据格式,可用于存储各种类型的值。什么是JSON类型?JSON类型表示JSON(JavaScriptO...

JavaScript:JSON、三种包装类(javascript 包)

JOSN:我们希望可以将一个对象在不同的语言中进行传递,以达到通信的目的,最佳方式就是将一个对象转换为字符串的形式JSON(JavaScriptObjectNotation)-JS的对象表示法...

Python数据分析 只要1分钟 教你玩转JSON 全程干货

Json简介:Json,全名JavaScriptObjectNotation,JSON(JavaScriptObjectNotation(记号、标记))是一种轻量级的数据交换格式。它基于J...

比较一下JSON与XML两种数据格式?(json和xml哪个好)

JSON(JavaScriptObjectNotation)和XML(eXtensibleMarkupLanguage)是在日常开发中比较常用的两种数据格式,它们主要的作用就是用来进行数据的传...

取消回复欢迎 发表评论:

请填写验证码