分享

这次终于能把Transformer搞懂了,包含概念和代码基础知识!

 汉无为 2024-04-06 发布于湖北

Transformers是一种新型神经网络,主要用于序列转换任务。

在本文中,我们将详细解释 Transformer ,以及如何在它是如何在 pytorch中 实现的。

序列转换是指将一个序列转换为另一个序列的任何任务。大多数具有竞争力的神经序列转换模型都具有 编码器-解码器 的结构。

那么,什么是编码器和解码器呢?

编码器将输入符号表示序列映射为连续表示序列,而解码器则一次生成一个符号,以产生输出符号序列。

在每个步骤中,模型是自回归的,即在生成下一个符号时,会将先前生成的符号作为额外的输入。

图片

Transformer的整体结构

标准的 Transformer 包含一个编码器和一个解码器。现在,让我们在代码中放置其结构的框架:

import copyimport torchimport mathimport torch.nn as nnfrom torch.nn.functional import log_softmax, pad

class EncoderDecoder(nn.Module):'''A standard Encoder-Decoder architecture. '''

def __init__(self, encoder, decoder, src_embed, tgt_embed):super(EncoderDecoder, self).__init__()self.encoder = encoderself.decoder = decoderself.src_embed = src_embedself.tgt_embed = tgt_embed

def forward(self, src, tgt, src_mask, tgt_mask):# Take in and process masked src and target sequences.return self.decode(self.encode(src, src_mask), src_mask, tgt, tgt_mask)

def encode(self, src, src_mask):# Pass input sequence i.e. src through encoderreturn self.encoder(self.src_embed(src), src_mask)

def decode(self, memory, src_mask, tgt, tgt_mask):# Memory is the query and key from encoderreturn self.decoder(self.tgt_embed(tgt), memory, src_mask, tgt_mask)

图片

编码器

在深度学习模型中的编码器部分,它的主要工作是将输入的序列(由一系列的令牌组成,如单词、字符或其他符号)转换为一个新的序列,这个新序列中的每个元素(即嵌入向量)都包含了原始序列中对应元素的上下文信息。

“上下文”在这里指的是一个元素(如单词)在序列中的位置以及它周围的元素。

例如,在句子“I like to eat apples.”中,

“apples”作为水果的上下文和在句子“I work at Apple Inc.”中作为公司的上下文是不同的。

因此,在将这两个句子输入到编码器时,编码器应该能够为“apples”这个词生成两个不同的嵌入向量,以反映其不同的上下文含义。

另外我为大家准备了一本自学 Transformer 的神书,

图片

这本书全面解析了 Transformers,涵盖 60 多个Transformer 架构和对应的知识及技巧,技术涵盖 语音、文本、时间序列和计算机视觉 等方向

图片

并且每种架构都包含实用提示和技巧以及如何在现实世界中使用它。

图片

Transformer 教程书和本文的完整代码一起打包好了,为了避免添加人数过多出现频繁,大家可以随便添加一个小助手,让她发给你。

图片

编码器块由N个编码器层组成(历史上通常为6层)。第一个编码器层接收输入嵌入,并添加位置嵌入。

在图中,位置嵌入用~符号表示,位置编码的添加仅发生在第一个编码器层中。

图片

编码器块

所有编码器层都有两个子层:

多头自注意力机制

简单的位置全连接前馈网络

每个编码器层接收一个嵌入序列,并将其通过这两个子层。

编码器层的输出嵌入与输入具有相同的大小,并传递给下一个编码器层。

最后,所有编码器层共同更新输入嵌入,使其包含序列的一些上下文信息。

每个编码器层使用跳跃连接(在图中用青色箭头表示)和层归一化(在图中显示为“add & norm”框)。

在深入编码器代码之前,我们先来解释这些组件。

图片

跳跃连接

跳跃连接,也被称为残差连接,将张量直接传递给下一层而不进行任何处理,并将其与已经处理过的张量相加。

跳跃连接是一种简单但非常有效的技术,用于缓解在训练深度神经网络时出现的梯度消失问题,并有助于模型更快地收敛。

这种技术最初在计算机视觉领域的ResNet中被引入。

图片

层归一化

“add & norm”框表示的是层归一化,它将批处理中的每个输入标准化为零均值和单位方差。

在文献中,有两种放置“Add & Norm”的方式:

如上面图像所示的后层归一化,即归一化发生在注意力机制之后;

是前层归一化,即“Add & Norm”放置在多头注意力机制之前。

前者在原始的Transformer论文中被使用,因为梯度发散,从头开始训练会很棘手,

但是,后者在训练过程中更为稳定,因此最为常用。

图片

位置嵌入

在使用Transformer模型处理序列数据时,我们不仅要考虑序列中每个令牌(通常是文本中的单词或字符)的语义信息,还要考虑到这些令牌在序列中的位置信息。

这是因为Transformer并行处理输入标记,而不是顺序处理;

因此,它不携带任何关于标记位置的信息,所以我们需要将这些信息嵌入到输入中。

位置嵌入是添加到令牌嵌入的标识符,因此它们的维度与令牌嵌入的维度相同。位置嵌入必须满足两个要求:

对于一个特定的位置,无论该位置的令牌是什么,位置嵌入都应该相同。因此,虽然序列可能会改变,但位置嵌入必须保持不变。

它们不应该太大,否则它们会主导语义相似性。

基于上述标准,我们不能使用非周期函数(例如线性函数)来从1到令牌总数对令牌进行索引。这样的函数会违反上面的第二个要求。

一个可行的选择是使用正弦和余弦函数。正弦和余弦函数周期性地返回一个在(-1,1)范围内的数值,且它们是有界的。

这些函数在任意位置都有定义,因此即使对于非常长的序列,所有令牌也能接收到位置嵌入。

此外,即使对于较大的数值,它们也具有较大的变化性。

这与sigmoid函数不同,sigmoid函数在较大值时会变得平坦。

正弦和余弦函数的缺点是,对于许多位置它们会重复相同的输出。

这是不理想的,因此我们可以给函数一个较低的频率(这样正弦和余弦函数就会被拉伸),这样即使对于最长的序列长度,数值也不会重复。

这样做的好处是具有线性函数在位置有界时的优点,但缺点是连续位置之间的差异很小。

我们希望不同的位置具有有意义的差异。因此,我们在位置嵌入的第一维上使用低频正弦函数。对于第二维,我们使用频率更高的余弦函数。

我们对所有维度重复这一操作:在正弦和余弦之间交替,并增加频率。

在数学上,对于位置pos和维度i,我们可以表示如下:

图片

class PositionalEncoding(nn.Module):# Implement the position encoding (PE) function.

def __init__(self, d_model, dropout, max_len=5000):super(PositionalEncoding, self).__init__()self.dropout = nn.Dropout(p=dropout)

# Compute the positional encodings once in log space.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):# adds token embedding to its position embeddingx = x + self.pe[:, : x.size(1)].requires_grad_(False)return self.dropout(x)

注:这种方法对文本非常有效。对于图像,人们使用相对位置嵌入。

频率是衡量水平拉伸的不同方式。在使用正弦函数时,频率是2π内发生的周期数。

例如,sin(3x)的频率是3,而sin(x/3)的频率是1/3。

图片

多头注意力

多头注意力机制由多个注意力头组成。每个注意力头都是一个机制,用于为序列中的每个元素分配不同数量的权重或“注意力”。

它通过将所有令牌嵌入的加权平均值更新每个令牌嵌入来实现这一点。

通过这种方式生成的嵌入被称为上下文嵌入。在加权平均值中的权重被称为注意力权重。

实现自注意力层有几种方法,其中最常用的方法是缩放点积注意力。

图片

缩放点积注意力

这种方法主要有三个步骤:

它将每个令牌嵌入投影为三个向量,称为查询、键和值。

这是通过对每个令牌嵌入应用三个独立的线性投影来实现的。

如果 T 是 d 维空间中的输入令牌嵌入,而 Wᵠ ∈ ℝᵠ、Wₖ ∈ ℝᵏ 和 Wᵥ ∈ ℝᵛ 是用于计算查询、键和值的投影矩阵,那么相应的嵌入将分别是:

图片

计算注意力得分

它通过点积方法测量查询和键之间的相似性,这种方法使用矩阵乘法可以非常高效地计算。

相似的查询和键将具有非常大的点积,而那些没有太多共同点的查询和键将几乎没有重叠。

这一步的输出被称为注意力得分。对于具有 n 个输入令牌的序列,存在一个相应的 n×n 注意力得分矩阵。

计算注意力权重。点积可能任意大,导致训练不稳定,因此在这一步中,我们通过除以 √dim 来缩小它们,然后应用 softmax 使它们的和等于 1。

如果我们不通过 √dim 缩小规模,我们应用的 softmax 可能会过早饱和。

softmax 的输出被称为注意力权重。

最后它使用注意力权重作为值嵌入的加权平均来更新令牌嵌入。

图片

在代码中

def attention(query, key, value, mask=None, dropout=None):'''Compute Scaled Dot Product Attention'''

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

拥有多个注意力头并组合它们的嵌入是有益的。多头注意力通过将不同表示子空间的信息连接起来,并通过线性投影传递它们,从而共同关注这些信息。

下面的图展示了单个注意力和多头注意力,它们接收 d_model 维度的令牌嵌入,并输出 d_model 维度的上下文嵌入。

请注意,由于 d_v 不需要与 d_model 相同,因此我们通过一个最终的线性层(表示为 W^0)将其投影回 d_model 维度。

图片

多头注意力

class MultiHeadedAttention(nn.Module):def __init__(self, h, d_model, dropout=0.1):# Take in model size and number of heads.super(MultiHeadedAttention, self).__init__()assert d_model % h == 0# We assume d_v always equals d_kself.d_k = d_model // hself.h = hself.linears = clones(nn.Linear(d_model, d_model), 4)self.attn = Noneself.dropout = nn.Dropout(p=dropout)

def forward(self, query, key, value, mask=None):if mask is not None:# Same mask applied to all h heads.mask = mask.unsqueeze(1)nbatches = query.size(0)

# Do all the linear projections in batch from d_model => h x d_kquery, 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))]

# Apply attention on all the projected vectors in batch.x, self.attn = attention(query, key, value, mask=mask, dropout=self.dropout)

# Concat using a view and apply a final linear.x = (x.transpose(1, 2).contiguous().view(nbatches, -1, self.h * self.d_k))return self.linears[-1](x)

图片

前馈层

这个子层只是一个简单的两层全连接神经网络,它独立地处理嵌入向量。这与将整个嵌入序列作为单个向量处理是相对的。

因此,这一层通常被称为位置感知的前馈层。

通常,第一层的隐藏大小为 4 d_model,并且大多使用 GELU 激活函数。大部分记忆发生在这里,当扩展模型时,这个维度通常也会扩展。

图片

解码器

解码器的工作是生成文本序列。在下图,我们看到了解码器块的图像,正如我们所看到的,它由多个解码器层组成。

每个解码器层都有两个多头注意力子层、一个位置感知的前馈层、残差连接和层归一化。

如我们所见,解码器块以一个作为分类器的线性层结束,并使用 softmax 来获取单词概率。

解码器是自回归的,它从一个起始令牌开始,并将之前输出的列表作为输入,以及包含来自输入的注意力信息的编码器输出。

当解码器生成一个令牌作为输出时,它就会停止解码。

图片

虽然前馈子层的行为类似于编码器中的对应子层,但两个多头注意力(MHA)子层与编码器中的MHA略有不同。

下面让我们看看它们的差异体现在哪里

1.带掩码的多头注意力:为了防止解码器查看未来的令牌,我们应用了前瞻掩码。

因此,解码器只允许关注输出序列中较早的位置。

在计算 softmax 之前和注意力机制中缩放得分之后添加掩码。

因此,它计算查询和键之间的缩放点积得分,然后添加前瞻掩码以掩盖未来的单词,然后计算它们的 softmax。

前瞻掩码是一个逐键的方阵,其下对角线为零,而上对角线被设置为−∞。

一旦对掩码得分进行 softmax,负无穷大值就会变为零,从而为未来的令牌留下零注意力得分。

这种掩码是在第一层多头注意力中计算注意力得分时唯一的差异。

2.是多头注意力:对于这一层,编码器的输出是查询和键,而第一层多头注意力层的输出是值。

这个过程将编码器的输入与解码器的输入匹配,允许解码器决定哪个编码器输入是相关并需要关注的。

第二层多头注意力的输出通过逐点前馈层进行进一步处理。

ef subsequent_mask(size):# Mask out subsequent positions.attn_shape = (1, size, size)subsequent_mask = torch.triu(torch.ones(attn_shape), diagonal=1).type(torch.uint8)return subsequent_mask == 0

class DecoderLayer(nn.Module):# Decoder is made of self-attn, src-attn, and feed forward (defined below)'

def __init__(self, size, self_attn, src_attn, feed_forward, dropout):super(DecoderLayer, self).__init__()self.size = sizeself.self_attn = self_attnself.src_attn = src_attnself.feed_forward = feed_forwardself.sublayer = clones(SublayerConnection(size, dropout), 3)

def forward(self, x, memory, src_mask, tgt_mask):# Follow Figure 1 (right) for connections.'m = memoryx = 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)

class Decoder(nn.Module):# Generic N layer decoder with masking.'

def __init__(self, layer, N):super(Decoder, self).__init__()self.layers = clones(layer, N)self.norm = 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的构建块——编码器和解码器。

我们查看了它们的内部架构,并解释了它们的组件,如跳跃连接、位置嵌入和注意力层。

我们还使用 PyTorch从头开始编写了它们的代码。

另外我为大家准备了一本自学 Transformer 的神书,

图片

这本书全面解析了 Transformers,涵盖 60 多个Transformer 架构和对应的知识及技巧,技术涵盖 语音、文本、时间序列和计算机视觉 等方向

图片

并且每种架构都包含实用提示和技巧以及如何在现实世界中使用它。

图片

Transformer 教程书和本文的完整代码一起打包好了,为了避免添加人数过多出现频繁,大家可以随便添加一个小助手,让她发给你。

图片

    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多