Transformer架构详细解读(教程向)
说明本文内容多来自尚硅谷自然语言处理课程讲义图文并茂有图有公式内容质量很高在此表示感谢一、问题背景在大模型奠基之作Transformer出来之前传统的序列建模都是以RNNLSTM和GRU网络为主。这些网络虽然在一定程度上解决了自然语义序列信息建模困难的问题但是其局限性也十分明显每一个时间步的输出完全依赖于上一个时间步的隐藏状态这导致了这些网络无法对于长序列进行建模如果输入序列过长在隐藏状态的层层传递中最早的一个隐藏状态信息就可能丢失。尽管LSTM网络引入了遗忘门这一模块但是也无法彻底解决该问题此外RNN结构中梯度消失和梯度爆炸问题也十分棘手。Transformer架构的诞生从根本上完全摒弃了循环神经网路建模的思路使用Attention机制直接进行序列建模并通过一系列细节处理让模型近乎完美成为当下大模型的基石。二、模型整体结构模型整体上采取了编码器-解码器架构每个结构内采取Attention机制完整建模整个序列从而使模型有了并行处理序列的能力。整体模型架构如下编码器由两个子层组成分别是多头注意力子层和前馈神经网络子层FFN层解码器由三个子层组成分别是掩码mask自注意力子层、编码器解码器注意力子层和前馈神经网络子层FFN。编码器和解码器各N原始论文中6次堆叠形成整个架构。三、编码器详解3.1 多头自注意力子层自注意力机制Self-Attention是 Transformer 编码器的核心结构之一它的作用是在序列内部建立各位置之间的依赖关系使模型能够为每个位置生成融合全局信息的表示。之所以被称为“自”注意力是因为模型在计算每个位置的表示时所参考的信息全部来自同一个输入序列本身而不是来自另一个序列。3.1.1 自注意力计算过程自注意力机制的第一步是将输入序列中的每个位置表示映射为三个不同的向量分别是查询Query、键Key和值Value。这些向量的作用如下Query表示当前词的用于发起注意力匹配的向量Key表示序列中每个位置的内容标识用于与 Query 进行匹配Value表示该位置携带的信息用于加权汇总得到新的表示。自注意力的核心思想是每个位置用自身的 Query 向量与整个序列中所有位置的 Key 向量进行相关性计算从而得到注意力权重并据此对对应的 Value 向量加权汇总形成新的表示。其中Wq,Wk,Wv均为可学习的参数矩阵。完成 Query、Key、Value 向量的生成后模型会使用每个位置的 Query 向量与所有位置的 Key 向量进行相关性评分。具体计算公式如下其中dk是key向量的维度用于缩放点积的幅度。这个分数越大表示第 i 个位置越应该关注第 j 个位置的信息。对于整个序列可以通过矩阵运算一次性计算乘K矩阵转置即可所有位置之间的评分。在得到每个位置与所有位置之间的相关性评分后模型会使用 softmax 函数进行归一化确保每个位置对所有位置的关注程度之和为 1从而形成一个有效的加权分布。这里需要注意的是对于整个序列模型要做的是对之前得到的注意力评分矩阵的每一行进行softmax归一化。最后模型会根据注意力权重对所有位置的 Value 向量进行加权求和得到每个位置融合全局信息后的新表示。3.1.2 多头自注意力计算既然有了自注意力为什么又要弄一个多头自注意力这是因为自然语义中具有高度的复杂性例如“那只动物没有过马路因为它太累了”这一句话中包含了很多的逻辑关系“它”指代“那只动物”属于跨句的代指关系“因为”连接前后两个分句体现语义上的因果逻辑“过马路”构成动词短语属于固定的动宾结构。如果仅仅依靠Q,K,V向量计算序列中的上下文信息这是不够的要准确理解这类句子模型需要同时识别并建模多种层次和类型的依赖关系。但这些信息很难通过单一视角或一套注意力机制完整捕捉。为此Transformer 引入了多头注意力机制Multi-Head Attention。其核心思想是通过多组独立的 Query、Key、Value 投影让不同注意力头分别专注于不同的语义关系最后将各头的输出拼接融合。分别计算出各个头的自注意力然后使用cat多个输出矩阵按维度拼接再乘以Wo得到最终多头注意力的输出。3.2 前馈神经网络子层前馈神经网络Feed-Forward Network简称 FFN是 Transformer 编码器中每个子层的重要组成部分紧接在多头注意力子层之后。它通过对每个位置的表示进行逐位置、非线性的特征变换进一步提升模型对复杂语义的建模能力。一个标准的 FFN 子层包含两个线性变换和一个非线性激活函数中间通常使用 ReLU激活。其计算公式如下3.3 残差和归一化连接在 Transformer 的每个编码器层中每个子层包括自注意力子层和前馈神经网络子层其输出都要经过残差连接Residual Connection和层归一化Layer Normalization处理。这两者是深层神经网络中常用的结构用于缓解模型训练中的梯度消失、收敛困难等问题对于Transformer能够堆叠多层至关重要。残差连接大家比较熟悉就不多做介绍了。每个子层在残差连接之后都会进行层归一化Layer Normalization简称 LayerNorm。它的主要作用是规范输入序列中每个token的特征分布某个token的表示可能在不同维度上有较大数值差异提升模型训练的稳定性。该操作会将每个token的向量调整为均值为 0、方差为 1 的规范分布计算公式如下首先计算该向量在所有特征维度上的平均值然后计算向量各维度的标准差然后将每个特征值转换为均值为 0、方差为 1 的标准正态分布最后进行缩放和平移让模型可以学习在归一化后的基础上进行适当的调整保证归一化不会限制模型的表示能力四、位置编码Transformer 模型完全摒弃了 RNN 结构意味着它不再按顺序处理序列而是可以并行处理所有位置的信息。尽管这带来了显著的计算效率提升却也引发了一个问题Transformer 无法像 RNN 那样天然地捕捉词语之间的顺序关系。换句话说在没有额外机制的情况下Transformer 无法区分“猫吃鱼”和“鱼吃猫”这类语序不同但词汇相同的句子。为了解决这一问题Transformer 引入了一个关键机制——位置编码Positional Encoding。该机制为每个词引入一个表示其位置信息的向量并将其与对应的词向量相加作为模型输入的一部分。这样一来模型在处理每个词时既能获取词义信息也能感知其在句子中的位置从而具备对基本语序的理解能力。我们最容易想到的位置编码方法就是绝对位置编码使用绝对位置编号来表示每个词的位置例如第一个词用 0第二个词用 1依此类推...但是这样会造成严重的问题越靠后的 token 位置编码就越大若直接与词向量相加会造成数值倾斜让模型更关注位置而忽视词义。为缓解这一问题可以考虑将位置编号归一化为[0, 1]区间例如用pos/T表示位置其中T是句子长度。这种方式虽然使数值范围更平稳但也引入了一个严重的问题相同位置的词在不同长度句子中的位置编码不再一致。为了解决上述问题Transformer 使用了一种基于正弦sin和余弦cos函数的位置编码方式具体定义如下pos是当前词在序列中的位置i用于表示位置编码向量的维度索引2i表示偶数维2i1表示奇数维dmodel是词向量的维度大小。序列中的每个位置 pos 对应一个长度为dmodel的位置编码向量。该向量的偶数维度通过正弦函数生成奇数维度通过余弦函数生成如下图所示Transformer提出的这种编码方式不依赖任何可学习参数数值稳定并具备以下优势所有值都在[−1,1]范围内数值稳定编码方式固定、可预计算无需训练相同位置的编码在不同句子中保持一致编码之间具有数学规律便于模型在注意力机制中感知词语之间的相对位置关系。基于此当下大模型对于位置编码做了不同程度的改进最流行就是旋转位置编码RoPE。五、解码器详解Transformer 解码器的主要功能是根据编码器的输出逐步生成目标序列中的每一个词。其生成方式采用自回归机制autoregressive每一步的输入由此前已生成的所有词组成模型将输出一个与当前输入长度相同的序列表示。我们只取最后一个位置的输出作为当前步的预测结果。这一过程会不断重复直到生成特殊的结束标记 eos表示序列生成完成。5.1 掩码自注意力子层主要用于建模当前位置与前文词之间的依赖关系。为了在训练时模拟逐词生成的过程引入遮盖机制Mask限制每个位置只能关注它前面的词。由于 Transformer 不具备像 RNN 那样的隐藏状态传递机制无法在序列生成过程中保留上下文信息因此在生成每一个词时必须将此前已生成的所有词作为输入通过自注意力机制重新建模上下文关系以预测下一个词。此外从结构上看Transformer 编解码器都具有一个典型特性输入多少个词就输出多少个表示。需要注意的是在推理阶段我们只使用解码器最后一个位置的输出作为当前步的预测结果如果训练阶段也完全按照推理流程进行就必须将每个目标序列拆分成多个训练样本每个样本输入一段前文只预测一个词。这种方式虽然逻辑合理但训练效率极低完全无法利用 Transformer 并行计算的优势。为提升效率Transformer 采用了并行训练策略一次性输入完整目标序列同时预测每个位置的词。如下图所示但如果不加限制这种方式会让模型在预测每个位置时“看到”后面的词即提前访问未来信息破坏生成任务的因果结构。为解决这个问题解码器在自注意力机制中引入了遮盖机制Mask。该机制会在计算注意力时阻止模型访问当前位置之后的词只允许它依赖自身及前文的信息。这样即使在并行训练时模型也只能像逐词生成一样“看见”它应该看到的内容从而保持训练与推理阶段的一致性。如下图所示Mask 机制的实现非常简单只需将注意力得分矩阵中当前位置对其后续位置的评分设置为−∞如下图所示这样一来前面词与后文信息的注意力信息就会被负无穷遮住在经过 softmax 运算后这些位置的权重会趋近于 0。最终在加权求和时来自未来位置的信息几乎不会参与计算从而实现了“当前词只能看到它前面的词”的约束。十分巧妙5.2 编码器-解码器注意力子层该子层的主要作用是建模当前解码位置与源语言序列中各位置之间的依赖关系帮助模型在生成目标词时有效地参考输入内容相当于Seq2Seq模型中的注意力机制。编码器-解码器注意力的核心机制与前面讲过的自注意力机制完全一致区别仅在于Query 来自解码器当前的输入表示即当前生成状态Key和Value 来自编码器的输出表示即整个源序列的上下文。也就是说当前生成位置使用自己的Query去“询问”编码器输出中的哪些位置最相关。注意力机制会根据 Query 与所有 Key 的相似度为每个源位置分配一个权重然后用这些权重对 Value 进行加权求和得到当前生成词所需的上下文信息。这里Q,K,V不同源也就不是“自”注意力了。六、模型训练与推理Transformer 的训练与推理都基于自回归生成机制Autoregressive Generation模型逐步生成目标序列中的每一个词。然而在实现方式上训练与推理存在明显区别。6.1 模型训练训练时Transformer 将目标序列整体输入解码器并在每个位置同时进行预测。为防止模型“看到”后面的词破坏因果顺序解码器在自注意力机子层中引入了 遮盖机制Mask限制每个位置只能关注它前面的词。这种机制让模型在结构上模拟逐词生成但在实现上能充分利用并行计算大幅提升训练效率。6.2 模型推理推理时每一步都要重新输入整个已生成序列模型需要基于全量前文重新计算注意力分布决定下一个词的输出。整个过程必须顺序执行无法并行。推理阶段模型每一步都要重新输入当前已生成的全部词通过自注意力机制建模上下文关系预测下一个词。模型会基于完整前文重新计算注意力分布生成当前步的输出。由于每一步的输入依赖前一步结果整个过程必须顺序执行无法并行。每步输出的是一个词的概率分布最终生成结果也可使用不同的解码策略如贪心搜索、束搜索等。七、代码实现哈佛版本链接https://nlp.seas.harvard.edu/annotated-transformer/哈佛大学团队基于Pytorch实现了Transformer结构的完整代码为开源社区与学术研究做出了贡献。1、导入包import os from os.path import exists import torch import torch.nn as nn from torch.nn.functional import log_softmax, pad import math import copy import time from torch.optim.lr_scheduler import LambdaLR import pandas as pd import altair as alt from torchtext.data.functional import to_map_style_dataset from torch.utils.data import DataLoader from torchtext.vocab import build_vocab_from_iterator import torchtext.datasets as datasets import spacy import GPUtil import warnings from torch.utils.data.distributed import DistributedSampler import torch.distributed as dist import torch.multiprocessing as mp from torch.nn.parallel import DistributedDataParallel as DDP2、注意力计算def attention(query, key, value, maskNone, dropoutNone): 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_attnd_k取Q矩阵的最后一个维度也就是词向量维度一般来说是512。这里softmax是对最后一个维度做也就是前面提到的对每一行做归一化。3、多头注意力计算class MultiHeadedAttention(nn.Module): def __init__(self, h, d_model, dropout0.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_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(pdropout) def forward(self, query, key, value, maskNone): Implements Figure 2 if mask is not None: # Same mask applied to all h heads. mask mask.unsqueeze(1) nbatches query.size(0) # 1) Do all the linear projections in batch from 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) Apply attention on all the projected vectors in batch. x, self.attn attention( query, key, value, maskmask, dropoutself.dropout ) # 3) Concat using a view and apply a final linear. 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)头数h为8要求d_model%h0将 512 维拆分为 8 个 64 维的头Q,K,V矩阵形状为[batch_size,seq_len,d_model]view操作后变成[batch_size,seq_len,h,d_k]→transpose(1,2)后[batch_size,h,seq_len,d_k]。这里cat拼接使用的是view先transpose(1,2)回到[batch, seq_len, h, d_k]再view拼接为[batch, seq_len, d_model]。contiguous()的作用是PyTorch 中 transpose 后内存不连续需用该函数确保 view 正常执行。4、前馈神经网络class PositionwiseFeedForward(nn.Module): Implements FFN equation. def __init__(self, d_model, d_ff, dropout0.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()))5、位置编码首先是词向量嵌入Embeddingclass 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)然后是位置编码class PositionalEncoding(nn.Module): Implement the PE function. def __init__(self, d_model, dropout, max_len5000): super(PositionalEncoding, self).__init__() self.dropout nn.Dropout(pdropout) # 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): x x self.pe[:, : x.size(1)].requires_grad_(False) return self.dropout(x)max_len5000预计算最多 5000 个位置的编码覆盖大部分序列长度register_buffer(pe, pe)将 pe 注册为 “缓冲区”不会被优化器更新位置编码是固定的不训练requires_grad_(False)明确禁用梯度计算节省内存前向传播词嵌入x 位置编码pe再经过 Dropout。6、掩码def subsequent_mask(size): Mask out subsequent positions. attn_shape (1, size, size) subsequent_mask torch.triu(torch.ones(attn_shape), diagonal1).type( torch.uint8 ) return subsequent_mask 0输入形状[1, seq_len, seq_len]torch.triu构造上三角矩阵diagonal1表示对角线以上为1最后返回转换为布尔掩码True1表示可见False0表示屏蔽。7、架构层编码器class Encoder(nn.Module): Core encoder is a stack of N layers def __init__(self, layer, N): super(Encoder, self).__init__() self.layers clones(layer, N) self.norm LayerNorm(layer.size) def forward(self, x, mask): Pass the input (and mask) through each layer in turn. for layer in self.layers: x layer(x, mask) return self.norm(x)class EncoderLayer(nn.Module): Encoder is made up of self-attn and feed forward (defined below) 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): Follow Figure 1 (left) for connections. x self.sublayer[0](x, lambda x: self.self_attn(x, x, x, mask)) return self.sublayer[1](x, self.feed_forward)layer单个 EncoderLayer 实例N堆叠层数论文中 N6所有层共享同一个掩码src_mask用于屏蔽 padding token最终输出前做层归一化进一步稳定输出分布lambda x: self.self_attn(x,x,x,mask)将自注意力层封装为 “单参数函数”适配 SublayerConnection 的sublayer参数要求。解码器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)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 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): Follow Figure 1 (right) for connections. 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)memory编码器的输出源序列的编码结果src_mask源序列的掩码屏蔽 paddingtgt_mask目标序列的掩码屏蔽 padding 未来 token。编码器-解码器结构class EncoderDecoder(nn.Module): 标准的编码器-解码器架构是Transformer的基础 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 # 输出层线性softmax 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): # 解码目标序列嵌入 解码器前向传播依赖编码器输出memory return self.decoder(self.tgt_embed(tgt), memory, src_mask, tgt_mask)初始化参数encoder/decoder编码器 / 解码器的核心实例src_embed/tgt_embed源 / 目标序列的嵌入层词嵌入 位置编码generator最终输出层将解码器输出映射到目标词汇表。forward逻辑先通过encode得到源序列的编码结果memory再通过decode将memory和目标序列结合输出解码器结果src_mask/tgt_mask掩码屏蔽 padding / 未来 token。层归一化class LayerNorm(nn.Module): Construct a layernorm module (See citation for details). def __init__(self, features, eps1e-6): super(LayerNorm, self).__init__() self.a_2 nn.Parameter(torch.ones(features)) self.b_2 nn.Parameter(torch.zeros(features)) self.eps eps def forward(self, x): mean x.mean(-1, keepdimTrue) std x.std(-1, keepdimTrue) return self.a_2 * (x - mean) / (std self.eps) self.b_2残差连接class SublayerConnection(nn.Module): A residual connection followed by a layer norm. Note for code simplicity the norm is first as opposed to last. def __init__(self, size, dropout): super(SublayerConnection, self).__init__() self.norm LayerNorm(size) self.dropout nn.Dropout(dropout) def forward(self, x, sublayer): Apply residual connection to any sublayer with the same size. return x self.dropout(sublayer(self.norm(x)))8、一些工具函数克隆N层def clones(module, N): Produce N identical layers. return nn.ModuleList([copy.deepcopy(module) for _ in range(N)])class Generator(nn.Module): Define standard linear softmax generation step. 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)9、搭建模型def make_model( src_vocab, tgt_vocab, N6, d_model512, d_ff2048, h8, dropout0.1 ): Helper: Construct a model from hyperparameters. c copy.deepcopy attn MultiHeadedAttention(h, d_model) ff PositionwiseFeedForward(d_model, d_ff, dropout) position PositionalEncoding(d_model, dropout) model EncoderDecoder( Encoder(EncoderLayer(d_model, c(attn), c(ff), dropout), N), Decoder(DecoderLayer(d_model, c(attn), c(attn), c(ff), dropout), N), nn.Sequential(Embeddings(d_model, src_vocab), c(position)), nn.Sequential(Embeddings(d_model, tgt_vocab), c(position)), Generator(d_model, tgt_vocab), ) # This was important from their code. # Initialize parameters with Glorot / fan_avg. for p in model.parameters(): if p.dim() 1: nn.init.xavier_uniform_(p) return modelcopy.deepcopy每个层 / 组件都深拷贝确保参数独立比如编码器和解码器的注意力层不共享参数参数初始化xavier_uniform_Glorot 初始化适用于线性层使输入 / 输出的方差一致缓解梯度消失。
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2477264.html
如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!