从每一个计算细节了解 transformer

本文深入解析Transformer模型,探讨其如何解决LSTM的串行问题,提升训练效率。通过详细步骤,展示Transformer在NLP任务中的优势,包括多头注意力机制、位置编码、残差连接等关键组件。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

目前NLP领域最火的是各种各样的BERT。而BERT的核心则是更早出生的transformer。

今天就来理一理transformers,下面是transformer的原论文。

Attention is All You Need:https://arxiv.org/abs/1706.03762

 

首先是为什么要有transformer,在这之前我们训练seq2seq任务大多使用的是LSTM。效果也还不错,但是LSTM有一个瓶颈,就是串行问题,串行大大制约了训练速度。于是更高效的可并行训练的transformer就诞生了。

很多刚开始接触transformer的同学会问,为什么transformer比LSTM好,大概最主要的因素就是训练快吧。

训练快的结果就是可以在更大量的数据集上训练,于是大力出奇迹。同样的数据在同样复杂度的LSTM模型上不知道要训练到猴年马月了。

 

接下来就让我们一步步拨开transformer的外衣🙌,详细的看一看transformer的思路及计算细节。

光讲思路,没有计算细节对于新手来说是不够的,至少我在一开始学习的时候是不够的,看完思路之后仍然云里雾里。

 

模型结构

preview

以上是论文中的原图,整个模型分为两大部分,左边为Encoder,右边为Decoder。

本文以上下文预测作文案例进行讲解,比如下面的两句话,我们尝试用第一句来预测第二句。

第一话全部送入Encoder,然后从Decoder输出第二句话。

<s> Bob wanted to play a video game . </s>  
<s> He found the game he wanted online and ordered it . </s>

Encoder

Encoder主要由6个Layer构成,然后这6个Layer就构成了一个encoder,多个相同的encoder就构成了一个Encoder,也就是图中最左边的Nx,意思就是这个Encoder可以根据需要自定义多少个,各个小的encoder相互之间是串行的,transformer论文中是6个。

Decoder中也主要是这几个Layer构成,下面就先介绍一下这6个Layer,然后再讲多个encoder是如果工作的。

Input Embedding

这一层大家应该是比较熟悉的,LSTM中也离不开这一步,即将所有的单词都映射到一个词向量,这里假设是256.

假设原始数据是

X =(batch_size * seq_len)=(64 * 20),即一个batch为64, 每句话有20个单词。

那么经过这一层之后得到的输出为

X_e = (batch_size * seq_len * emb_dim)= (64 * 20 * 256)

Positional Encoding

为什么会多一个position encoding呢?因为在transformer的架构中并没有像LSTM一样一个词一个词串行训练的步骤,因此就缺少了词的先后关系,而NLP任务中,词的先后关系是至关重要的,因此就有了这一层。

而这一步是有成熟方法的。

用不同频率的sine和cosine函数直接计算。为什么是它,看了下面的效果就会理解了。

该方法中给偶数位置和奇数位置分别定义了sin和cos函数。pos为句子中当前词的位置,本文中则是0 - 19.

d_{model} 就是前面embedding层的向量长度,即128

下图是position encoding结构的可视化图,可以看到从0 - 19是由一个渐近的变化,这里利用了sin,cos函数的周期性规律,即表示出了不同位置的差异及关系。

 

img

这一层的输出Tensor shape和上面embeding是一样的,

X_p = (batch_size * seq_len * emb_dim)= (64 * 20 * 256)

然后将embedding的输出和position encoding的输出相加就得到了下一步的输入。

X_e_p = (batch_size * seq_len * emb_dim)= (64 * 20 * 256)

会有同学问为什么是相加不是拼接,相加能有效果吗?

因为后序的计算会把拼接也变成相加:

假设接下来做一个线性变换,相拼也会变成相加,具体后序更新。

 

Multi-Head Attention

这一步是transformer的核心了,它的作用就是替代LSTM,对整句话提取全局的词与词之间的关系等信息,是怎样的关系信息呢,比如途中,机器学习到了“it”指的是“animal”,而不是其他的名词如“street”。

 

img

在encoder中这一层叫做multi-head attention,即有多个attention,那么就先来将一个attention是怎么样的,我可以叫它self-attention更准确,主要是对第一句话进行充分理解,是通过寻找每一个词与这句话中所有词的关系来进行理解。

经过了前面embedding和position之后,我们拿到了一个词的综合Embedding,在下图中我们输入了Thinking、Machines两个词,然后得到了他们的Embedding向量 X1, X2

从第一个词开始寻找关系和所有词的关系,这里其实就是把当前词的向量与所有的向量依次点积就得到了与每个词的关系结果。由于我们还需要并行计算,所以把Embedding copy两份,这样就得到了图中的query 和 key 两个向量。

 

img

我们从第一个词开始,于是就计算q1 * k1, q1 *k2, 即得到了第一个词与第一个词和第二个词的关系分数,如下图。

img

既然得到了关系分数,即权重,而我们在计算权重时通常需要进行归一化,所以将上面得到的分数除以\sqrt{d_{k}},再做softmax。图中案例embedding维度是64,所以是除以8。这一步主要是为了保证梯度的稳定性,避免由于词向量维度大小的变化对训练造成剧烈波动。

img

得到归一化之的权重后,如果我们再把这个权重乘以每个词本来的Embedding再求和,就得到了一个新的第一个词的词向量z1,该词向量不仅有第一个词自己的信息还包含了其他所有词相关的信息。

这就是图中Values的作用,Values也是原始Embedding的copy。

img

 

对所有Querys都进行一次这样的计算,就完成了一次self-attention。

得到一个全新的

Z = (batch_size * seq_len * emb_dim)= (64 * 20 * 256)

img

那么Multi-Head Attention又是怎么来的呢,为了避免单个self-attention提取的信息有限,所以一次性多个self-attention并行进行。既然要并行就需要给每一个head一套embedding,所以这里加入了一个线性变换,使得Q,K,V可以变得更大,然后再平均拆分一部分给每一个head,最后将每一个head的到的Z向量进行合并。

img

 

 

原文中是有8个head,假设W的维度是512

经过W之后的X_w =(batch_size * seq_len * all_head_dim)=(64 * 20 * 512)

那么 经过W之后的 Q, K, V = (batch_size * seq_len * num_head * head_dim)= (64 * 20 * 8 * 64)

而每一个head得到的 q, k, v (batch_size * seq_len * head_dim)= (64 * 20 * 64)

最后得到的 Z =(batch_size * seq_len * all_head_dim)=(64 * 20 * 512)

 

Add & Norm

这一层做一个简单的Residual network残差网络,将X_wZ相加。残差网络在CNN中是应用很多的,如ResNet,Yolo,它是为了在深层网络中保证高效的梯度传递。

然后再增加一个LayerNorm归一化,即对每一个维度减去平均数,除以标准差。

 

这一步并未改变向量大小,仍然是

Z_a =(batch_size * seq_len * all_head_dim)=(64 * 20 * 512)

 

Feed Forward

然后再进行一个简单的线性变换,同时使用relu作为激活函数。

FFN=max(0,xW1+b1)W2+b2

这一步也并未改变向量大小,仍然是

Z_f =(batch_size * seq_len * all_head_dim)=(64 * 20 * 512)

 

Add & Normal

最后再做一次前面的残差操作,将Z_aZ_f加在一起,然后再归一化

 

至此,Encoder层就走完了,整个步骤如下图。然后就可以进入Decoder层了。

 

Decoder

Masked Multi-Head Attention

decoder比 encoder最主要是多了这一层,这一层大致和Multi-head attention 是一样的。

区别在于decoder是做预测,假设一句话20个词,那就是依次预测y_0, y_1, y_2, ... , y_19,当预测y_2时,只能看到y_0, y_1,所以计算y_2的与各词的权重分数时,只能得到与y_0, y_1的分数。所以就需要把y_2及以后的位置mask起来。

 

那么我只输入y_0, y_1的值不就完了,之际预测的时候是这样的,但是训练的时候是知道左右y的,为了避免训练时作弊,所以需要mask起来,达到更真实的训练效果。

我们通过构建一个如下的mask矩阵来起到mask的效果,将看不到的部分权重置为零 .

([[[``1``, ``0``, ``0``, ``0``, ``0``, ``0``, ``0``, ``0``, ``0``, ``0``],
 ``[``1``, ``1``, ``0``, ``0``, ``0``, ``0``, ``0``, ``0``, ``0``, ``0``],
 ``[``1``, ``1``, ``1``, ``0``, ``0``, ``0``, ``0``, ``0``, ``0``, ``0``],
 ``[``1``, ``1``, ``1``, ``1``, ``0``, ``0``, ``0``, ``0``, ``0``, ``0``],
 ``[``1``, ``1``, ``1``, ``1``, ``1``, ``0``, ``0``, ``0``, ``0``, ``0``],
 ``[``1``, ``1``, ``1``, ``1``, ``1``, ``1``, ``0``, ``0``, ``0``, ``0``],
 ``[``1``, ``1``, ``1``, ``1``, ``1``, ``1``, ``1``, ``0``, ``0``, ``0``],
 ``[``1``, ``1``, ``1``, ``1``, ``1``, ``1``, ``1``, ``1``, ``0``, ``0``],
 ``[``1``, ``1``, ``1``, ``1``, ``1``, ``1``, ``1``, ``1``, ``1``, ``0``],
 ``[``1``, ``1``, ``1``, ``1``, ``1``, ``1``, ``1``, ``1``, ``1``, ``1``]]])

Masked Multi-Head Attention 之后再来一个残差和归一化,

这一步得到一个y_i = (batch_size * 1 * all_head_dim)= (64 * 1 * 512)

我们需要依据Encoder得到的信息来预测,然后就可以把这一步得到的信息与 Encoder进行attention了。

 

 

计算方式一样,将y_i 与 Z_i每一个计算权重,然后求和得到一个新的向量。

只是这一步中Querys就是y了,Keys和Values是encoder的输出。

这一步的输出形状未改变为 y_i = (batch_size * 1 * all_head_dim)= (64 * 1 * 512)

再经过一个和Encoder中一样的Feed Forward及Add & Norm

最后得到一个最终的y_i = (batch_size * 1 * all_head_dim)= (64 * 1 * 512)

 

Final Liear and Softmax Layer

上面一部的到的一个512维的向量,而我们实际是要预测一个词,因此再进行一次线性变换 及 softmax归一化。

softmax的输出就是词表中每一个词的概率。假设词表大小是20000,

那么最后的输出就是 out = (batch_size * 1 * vocab_size) = (64 * 1* 20000)

img

 

Loss Function

既然是预测词,那么transformer的loss function就和其他模型一样是Crossentropy交叉熵。

 

模型效果对比

文章开头既然提到了LSTM与transformer的比较。

下面就把我自己做的实验拿来对比一下。

后序更新,

欢迎留言讨论

<think>嗯,用户想了解或实现一个简单的Transformer模型。好的,我得先回忆一下Transformer的基本结构。Transformer模型主要由编码器和解码器组成,每个部分都包含多个层。每个层里又有自注意力机制和前馈神经网络,对吧?对了,还有位置编码,因为Transformer不像RNN那样有顺序处理的能力,所以需要位置编码来注入序列的位置信息。 首先,用户可能需要一个简单的实现,所以代码不能太复杂。得用PyTorch这样的框架,因为它比较常用。然后,我需要拆解各个组件:自注意力、多头注意力、位置编码、编码器层、解码器层,最后整合成完整的Transformer。 自注意力部分的关键是计算Q、K、V矩阵,然后缩放点积注意力。记得要除以sqrt(d_k)来防止梯度消失。然后是多头注意力,把Q、K、V分成多个头,分别处理再合并。这里可能需要线性变换,把输入投影到不同的子空间。 位置编码的话,可以使用正弦和余弦函数,按照原始论文的方法生成。这部分需要生成一个足够大的位置编码矩阵,根据输入的序列长度和维度来调整。 编码器层应该包括自注意力、前馈网络、层归一化和残差连接。解码器层除了自注意力,还有编码器-解码器注意力,也就是交叉注意力。要注意解码器的自注意力需要掩码,防止看到未来的信息。 整合的时候,编码器和解码器各堆叠N层。最后通过线性层和softmax生成输出概率。 用户可能对各个组件的具体实现有疑问,比如如何实现掩码,或者位置编码的具体计算方式。代码里得注释清楚,变量名也要有意义。另外,测试一下各个部分是否正常工作,比如输入输出的维度是否正确。 还要考虑如何训练这个模型,可能需要定义损失函数和优化器。不过用户可能暂时只需要模型结构,所以先提供实现的代码,再解释每个部分的作用。 在解释的时候,要提到Transformer的优势,比如并行处理长序列,自注意力捕捉全局依赖等。同时引用相关的参考文献,比如原始论文和提到的站内引用,比如关于训练方法和位置前馈网络的部分。 最后,生成相关问题,帮助用户深入理解,比如自注意力的数学形式、位置编码的作用、训练时的注意事项等。这些问题应该覆盖模型结构、实现细节和应用场景,让用户有进一步学习的方向。 检查一下代码是否有语法错误,比如PyTorch的版本兼容性,张量操作是否正确。比如在生成位置编码时,是否正确使用了sin和cos函数,以及是否正确处理了奇偶索引。还有在多头注意力中,分割和合并头的操作是否用view和transpose正确实现。 确保每个组件都有清晰的注释,变量名符合惯例,比如d_model表示模型维度,n_heads表示头数。代码示例要尽量简洁,但功能完整,用户可以直接运行测试。 可能用户在实际应用中会遇到问题,比如维度不匹配,或者掩码设置错误,所以解释部分需要强调这些关键点。比如在解码器的自注意力中,如何使用triu生成上三角掩码矩阵,并应用到注意力得分上。 总之,整个思考过程需要从用户的需求出发,拆解Transformer的各个组件,逐步构建代码,并解释每个部分的作用和原理,同时引用相关文献支持,最后通过问题引导用户深入探索。</think>以下是一个简单Transformer模型的实现和原理解释,包含中文注释和关键代码段: ### 一、Transformer模型结构 Transformer由编码器和解码器组成,核心组件包括: 1. 自注意力机制(Self-Attention) 2. 位置前馈网络(Position-wise FFN) 3. 残差连接和层归一化[^3] 4. 位置编码(Positional Encoding) $$ \text{Attention}(Q,K,V) = \text{softmax}(\frac{QK^T}{\sqrt{d_k}})V $$ ### 二、关键组件实现(PyTorch) #### 1. 自注意力层 ```python import torch import torch.nn as nn class SelfAttention(nn.Module): def __init__(self, d_model, d_k): super().__init__() self.Wq = nn.Linear(d_model, d_k) self.Wk = nn.Linear(d_model, d_k) self.Wv = nn.Linear(d_model, d_k) def forward(self, x): Q = self.Wq(x) # [batch, seq_len, d_k] K = self.Wk(x) # [batch, seq_len, d_k] V = self.Wv(x) # [batch, seq_len, d_k] attn_scores = torch.matmul(Q, K.transpose(-2, -1)) / torch.sqrt(torch.tensor(Q.size(-1))) attn_weights = torch.softmax(attn_scores, dim=-1) return torch.matmul(attn_weights, V) ``` #### 2. 多头注意力 ```python class MultiHeadAttention(nn.Module): def __init__(self, d_model, n_heads): super().__init__() self.head_dim = d_model // n_heads self.Wo = nn.Linear(d_model, d_model) def forward(self, Q, K, V): # 分割为多个头 [batch, seq_len, n_heads, head_dim] Q = Q.view(*Q.shape[:-1], self.n_heads, self.head_dim) K = K.view(*K.shape[:-1], self.n_heads, self.head_dim) V = V.view(*V.shape[:-1], self.n_heads, self.head_dim) # 计算注意力并合并 attn_output = ... # 各头计算结果 return self.Wo(attn_output) ``` #### 3. 位置编码 ```python class PositionalEncoding(nn.Module): def __init__(self, d_model, max_len=5000): super().__init__() pe = torch.zeros(max_len, d_model) position = torch.arange(0, max_len, dtype=torch.float).unsqueeze(1) div_term = torch.exp(torch.arange(0, d_model, 2).float() * (-math.log(10000.0) / d_model)) pe[:, 0::2] = torch.sin(position * div_term) pe[:, 1::2] = torch.cos(position * div_term) self.register_buffer('pe', pe.unsqueeze(0)) def forward(self, x): return x + self.pe[:, :x.size(1)] ``` ### 三、完整模型架构 ```python class Transformer(nn.Module): def __init__(self, src_vocab, tgt_vocab, d_model=512, n_layers=6, n_heads=8): super().__init__() self.encoder = nn.ModuleList([ EncoderLayer(d_model, n_heads) for _ in range(n_layers) ]) self.decoder = nn.ModuleList([ DecoderLayer(d_model, n_heads) for _ in range(n_layers) ]) self.fc_out = nn.Linear(d_model, tgt_vocab) class EncoderLayer(nn.Module): def __init__(self, d_model, n_heads): super().__init__() self.self_attn = MultiHeadAttention(d_model, n_heads) self.ffn = PositionwiseFFN(d_model) self.norm1 = nn.LayerNorm(d_model) self.norm2 = nn.LayerNorm(d_model) def forward(self, x): attn_out = self.self_attn(x, x, x) x = self.norm1(x + attn_out) ffn_out = self.ffn(x) return self.norm2(x + ffn_out) ``` ### 四、关键特性说明 1. **并行计算**:相比RNN可并行处理整个序列[^2] 2. **长距离依赖**:自注意力机制直接建模任意位置关系 3. **位置感知**:通过位置编码注入序列顺序信息[^3] 4. **可扩展性**:通过堆叠多个层实现复杂模式学习[^4]
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值