基于PyTorch的Transformer组件实现

最近看了不少介绍LLM工作原理的文章,发现每一篇都会试图跟读者讲明白作为baseline的Transformer架构到底长啥样。但是好像比较少有代码实现的示例和具体的例子帮助理解。于是自己也想尝试着写一篇含有代码实现和具体例子解释的文章,希望能够给喜欢编程朋友带来一点点对于Transformer结构的启发😁。

整体架构和关键组件

注意力机制的提出极大提高了神经网络对序列相关信息的学习能力。Transformer结构正是完全基于注意力机制来完成对源语言序列和目标语言序列之间的全局依赖建模。它通过并行注意力计算来提取文本之间隐含的上下文信息,而不需要依赖递归结构或者时间步展开等方法。这给序列学习带来了巨大的改变。

Transformer的总体Architecture如下:

可以发现这里头主要有这么几个东西:

  • Position Encoding部分
  • Attention部分,包括Muiti-Head Attention和Masked Muiti-Head Attention
  • 前馈层部分,对应图中的Feed-Forward
  • 残差链接和归一化部分,对应图中的“Add&Norm”

接下来我尝试着去解释每个组件大致的功能,以及如何用基于Pytorch的代码实现这些模块

Position Encoding

在这里插入图片描述

原理

对于输入的文本序列,首先通过输入嵌入层将每个词语转换为其对应的向量表示。通常直接为每个词语创建一个向量表示。

由于Transfomer 模型不再使用基于循环的方式处理文本输入,序列中没有任何信息能够告诉模型词语之间的相对位置关系。在送入编码器端分析其上下文语义之前,一个非常关键的操作是在词嵌入中添加位置编码(Positional Encoding)这一特征。

具体来说,序列中每一个词语所在的位置都有一个向量。这一向量会与词语表示相应相加并送入到后续模块中做进一步处理。在训练的过程当中,模型会自动地学习到如何利用这部分位置信息。为了计算不同位置对应的编码,Transformer 模型使用不同频率的正余弦函数如下所示:

P E ( p o s , 2 i ) = s i n ( p o s 100002 i / d ) PE(pos, 2i) = sin(pos100002i/d ) PE(pos,2i)=sin(pos100002i/d)

P E ( p o s , 2 i + 1 ) = c o s ( p o s 100002 i / d ) PE(pos, 2i + 1) = cos(pos100002i/d ) PE(pos,2i+1)=cos(pos100002i/d)

其中,pos 表示词语所在的位置,2i 和2i+1 表示位置编码向量中的相应维度,d 则对应位置编码的总维度。

通过上面这种方式得到位置编码有这样几个优点:

  • 首先,正余弦函数的范围是在[-1,+1],导出的位置编码与原词嵌入相加不会使得结果偏离过远而损害原有词语的语义信息。
  • 其次,根据三角函数的基本性质,可以知道第pos+k 个位置的编码是第pos 个位置的编码的线性组合,这就意味着位置编码中包含着词语之间的距离信息(可以通过三角函数的变换得到)。
    对于输入的文本序列,首先通过输入嵌入层将每个词语转换为其对应的向量表示。通常直接为每个词语创建一个向量表示。

一个例子

举个例子说明这个:

假设我们有一个文本序列,包含四个词语:[我,爱,你,吗]。我们可以为每个词语分配一个编号:[0, 1, 2, 3]。然后我们可以根据上面的公式计算出每个词语所在位置的编码向量。假设位置编码的总维度是4,那么我们可以得到如下的矩阵:

P E = [ s i n ( 0 ) c o s ( 0 ) s i n ( 0 ) c o s ( 0 ) s i n ( 1 / 10000 ) c o s ( 1 / 10000 ) s i n ( 1 / 1000 0 2 ) c o s ( 1 / 1000 0 2 ) s i n ( 2 / 10000 ) c o s ( 2 / 10000 ) s i n ( 2 / 1000 0 2 ) c o s ( 2 / 1000 0 2 ) s i n ( 3 / 10000 ) c o s ( 3 / 10000 ) s i n ( 3 / 1000 0 2 ) c o s ( 3 / 1000 0 2 ) ] PE = \begin{bmatrix} sin(0) & cos(0) & sin(0) & cos(0) \\ sin(1/10000) & cos(1/10000) & sin(1/10000^2) & cos(1/10000^2) \\ sin(2/10000) & cos(2/10000) & sin(2/10000^2) & cos(2/10000^2) \\ sin(3/10000) & cos(3/10000) & sin(3/10000^2) & cos(3/10000^2) \end{bmatrix} PE= sin(0)sin(1/10000)sin(2/10000)sin(3/10000)cos(0)cos(1/10000)cos(2/10000)cos(3/10000)sin(0)sin(1/100002)sin(2/100002)sin(3/100002)cos(0)cos(1/100002)cos(2/100002)cos(3/100002)

接下来,我们可以将每个词语的向量表示与其对应的位置编码向量相加,得到新的词嵌入矩阵:

E = [ e 0 + P E 0 e 1 + P E 1 e 2 + P E 2 e 3 + P E 3 ] E = \begin{bmatrix} e_0 + PE_{0} \\ e_1 + PE_{1} \\ e_2 + PE_{2} \\ e_3 + PE_{3} \end{bmatrix} E= e0+PE0e1+PE1e2+PE2e3+PE3

其中, e i e_i ei 表示第 i 个词语的向量表示。这样,我们就为每个词语添加了位置信息,并且保证了不同位置的词语有不同的编码。

最后,我们将新的词嵌入矩阵送入编码器端进行后续处理。

代码

使用Pytorch 实现的位置编码参考代码如下所示:

from kiwisolver import Variable
import torch
import torch.nn as nn
import math

class PositionEncorder(nn.Module):
    def __init__(self, d_model, max_seq_len):
        super().__init__()
        self.d_model = d_model # 模型维度
        self.max_seq_len = max_seq_len # 序列的最大长度

        pe = torch.zeros(max_seq_len, d_model) # 储存位置编码的Tensor
        for pos in range(max_seq_len): # 遍历序列每个位置
            for i in range(0, d_model, 2): # 遍历每一个维度,步长为2(两个维度一组进行正余弦计算)
                pe[pos,i] = math.sin(pos / (10000 ** ((2 * i)/d_model)))
                pe[pos,i+1] = math.cos(pos / (10000 ** ((2 * (i+1))/d_model)))

        pe = pe.unsqueeze(0) # 添加了一个额外的维度,使其形状从(max_seq_len, d_model)变为(1, max_seq_len, d_model)
        self.register_buffer('pe', pe)

    def forward(self, x):
        x = x * math.sqrt(self.d_model) # 使得单词嵌入表示相对大一些
        seq_len = x.size(1)
        x  = x + Variable(self.pe[:,:seq_len], requires_grad=False).cuda() # 加上位置信息


MutiHead Self Attention

在这里插入图片描述

原理

自注意力

自注意力操作是基于Transformer 的机器翻译模型的核心操作,它能够在源语言的编码和目标语言的生成中有效地利用源语言、目标语言任意两个单词之间的依赖关系。给定由单词语义嵌入和其位置编码相加得到的输入表示。为了实现对上下文语义依赖的捕捉,进一步引入在自注意力机制中涉及到的三个要素:查询 q i q_i qi(Query),键 k i k_i ki(Key),值 v i vi vi(Value)。

在编码输入序列中每一个单词的表示的过程中,这三个要素用于计算上下文单词所对应的权重得分。直观地说,这些权重反映了在编码当前单词的表示时,对于上下文不同部分所需要的关注程度。通过三个线性变换 W Q ∈ R d × d q W^Q ∈ R^{d×d_q} WQRd×dq W K ∈ R d × d k W^K ∈ R^{d×d_k} WKRd×dk W V ∈ R d × d v W^V ∈ R^{d×d_v} WVRd×dv将输入序列中的每一个单词表示 x i x_i xi 转换为其对应的 q i ∈ R d k q_i ∈ R^{d_k} qiRdk k i ∈ R d k k_i ∈ R^{d_k} kiRdk v i ∈ R d v vi ∈ R^{d_v} viRdv 向量。
在这里插入图片描述

为了得到编码单词 x i x_i xi 时所需要关注的上下文信息,通过位置 i i i查询向量与其他位置的键向量做点积得到匹配分数 q i ⋅ k 1 , q i ⋅ k 2 , . . . , q i ⋅ k t q_i · k_1, q_i · k_2, ..., q_i · k_t qik1,qik2,...,qikt

为了防止过大的匹配分数在后续Softmax 计算过程中导致的梯度爆炸以及收敛效率差的问题,这些得分会除放缩因子 √ d √d d 以稳定优化。放缩后的得分经过Softmax 归一化为概率之后,与其他位置的值向量相乘来聚合希望关注的上下文信息,并最小化不相关信息的干扰。上述计算过程可以被形式化地表述如下:

Z = A t t e n t i o n ( Q , K , V ) = S o f t m a x ( Q K T / √ d ) V Z = Attention(Q,K,V ) = Softmax(QK^T/√d)V Z=Attention(Q,K,V)=Softmax(QKT/√d)V

其中 Q ∈ R L × d q Q ∈ R^{L×d_q} QRL×dq K ∈ R L × d k K ∈ R^{L×d_k} KRL×dk V ∈ R d × d v V ∈ R^{d×d_v} VRd×dv 分别表示输入序列中的不同单词的q, k, v 向量拼接组
成的矩阵,L 表示序列长度, Z ∈ R L × d v Z ∈ R^{L×d_v} ZRL×dv表示自注意力操作的输出

多头注意力

为了更好地利用自注意力机制来捕捉上下文信息的多样性,提出了多头自注意力(Multi-head Attention)的方法,它可以同时关注上下文的不同方面。其实就是每一个单词跟多组 W Q W^Q WQ W K W^K WK W V W^V WV 做乘法,映射到多个表征空间,其中每一个表征空间可能偏向于关注不同的上下文内容

一个例子

举个例子:

假设我们有一个句子:“I love to eat pizza.”

在多头自注意力机制中,我们可以通过使用多组独立的线性变换来创建多个注意力头。每个注意力头都有自己的权重矩阵 W Q W^Q WQ W K W^K WK W V W^V WV,用于将输入的单词映射到不同的表征空间。

假设我们使用两个注意力头,即 heads = 2。在这种情况下,我们将有两个独立的注意力头,每个头都有自己的权重矩阵。

第一个注意力头可能更关注动词和名词之间的关系,例如 “love” 和 “pizza” 之间的关系。在这个注意力头中,权重矩阵 W 1 Q W^Q_1 W1Q 可以将 “love” 映射到一个表征空间, W 1 K W^K_1 W1K 可以将 “pizza” 映射到另一个表征空间, W 1 V W^V_1 W1V 可以将 “pizza” 映射到另一个表征空间。这样,第一个注意力头可以关注句子中动词和名词之间的语义关联。

第二个注意力头可能更关注形容词和名词之间的关系,例如 “I” 和 “pizza” 之间的关系。在这个注意力头中,权重矩阵 W 2 Q W^Q_2 W2Q 可以将 “I” 映射到一个表征空间, W 2 K W^K_2 W2K 可以将 “pizza” 映射到另一个表征空间, W 2 V W^V_2 W2V 可以将 “pizza” 映射到另一个表征空间。这样,第二个注意力头可以关注句子中形容词和名词之间的语义关联。

所以说,通过使用多头自注意力机制,我们可以同时关注句子中不同方面的语义关系,从而捕捉到更丰富的上下文信息。每个注意力头都可以偏向于关注句子中不同的语义关联,从而提升模型的表征能力和语义理解能力。

代码

import torch
import torch.nn as nn
import torch.nn.functional as F
import math

class MultiHeadAttention(nn.Module):
    def __init__(self, d_model, heads, dropout=0.1):
        super().__init__()

        self.d_model = d_model
        self.d_k = d_model // heads  # 确保每个头的维度加起来等于模型的总维度
        self.h = heads

        self.q_linear = nn.Linear(d_model, d_model)  # 线性变换,将输入维度调整为d_model
        self.v_linear = nn.Linear(d_model, d_model)  # 线性变换,将输入维度调整为d_model
        self.k_linear = nn.Linear(d_model, d_model)  # 线性变换,将输入维度调整为d_model
        self.dropout = nn.Dropout(dropout)  # Dropout层,用于随机置零一部分输入
        self.out = nn.Linear(d_model, d_model)  # 线性变换,将输入维度调整为d_model

    def attention(q, k, v, d_k, mask=None, dropout=None):
        scores = torch.matmul(q, k.transpose(-2, -1)) / math.sqrt(d_k)  # q 和 k 点乘后除以根号d_k, 得到注意力分数

        # 掩盖掉那些为了填补长度增加的单元,使其通过softmax 计算后为0
        if mask is not None:
            mask = mask.unsqueeze(1)
            scores = scores.masked_fill(mask == 0, -1e9)

        scores = F.softmax(scores, dim=-1)  # 对注意力分数进行softmax操作,得到注意力权重

        if dropout is not None:
            scores = dropout(scores)  # 对注意力权重进行dropout操作

        output = torch.matmul(scores, v)  # 注意力权重与v相乘得到输出
        return output

    def forward(self, q, k, v, mask=None):

        bs = q.size(0)  # 获取batch size

        # 进行线性操作划分成h个头
        k = self.k_linear(k).view(bs, -1, self.h, self.d_k)  # 对k进行线性变换并调整维度形状
        q = self.q_linear(q).view(bs, -1, self.h, self.d_k)  # 对q进行线性变换并调整维度形状
        v = self.v_linear(v).view(bs, -1, self.h, self.d_k)  # 对v进行线性变换并调整维度形状

        # 调换维度顺序使得最后一个维度为单元长度
        k = k.transpose(1, 2)  # 转置k的维度
        q = q.transpose(1, 2)  # 转置q的维度
        v = v.transpose(1, 2)  # 转置v的维度

        # 计算attention
        scores = attention(q, k, v, self.d_k, mask, self.dropout)

        # Concat多个头
        concat = scores.transpose(1, 2).contiguous().view(bs, -1, self.d_model)  # 将多个头的结果拼接起来
        output = self.out(concat)  # 进行线性变换得到最终输出

        return output
        

前馈层

在这里插入图片描述

原理

前馈层将自注意力子层的输出作为输入,并通过一个包含Relu 激活函数的双层全连接网络对输入进行更加高级的非线性变换。这一非线性变换对模型最终的效果有着非常重要的作用。

代码

import torch
import torch.nn as nn
import torch.nn.functional as F

class FeedForward(nn.Module):
    def __init__(self, d_model, d_ff=2048, dropout=0.1):
        super(FeedForward, self).__init__()

        # d_ff默认为2048
        self.linear_1 = nn.Linear(d_model, d_ff)  # 线性变换,将输入维度调整为d_ff
        self.dropout = nn.Dropout(dropout)  # Dropout层,用于随机置零一部分输入
        self.linear_2 = nn.Linear(d_ff, d_model)  # 线性变换,将输入维度调整为d_model

    def forward(self, x):
        x = self.dropout(F.relu(self.linear_1(x)))  # 使用ReLU激活函数并应用dropout
        x = self.linear_2(x)  # 进行线性变换
        return x

残差连接和归一化

在这里插入图片描述

原理

由Transformer 架构构成的网络模型通常都是非常巨大。编码器和解码器都由许多层基本的Transformer 单元构成,每一层里面都包含复杂的非线性变换,这就导致模型的训练比较难。因此在Transformer 单元中进一步引入了残差连接与层归一化技术以进一步提升训练的稳定性。

  • 残差链接:残差链接是一种在每个子层(如多头自注意力层和前馈层)后添加输入与输出之和的方法。在Transformer模块,残差连接主要是指使用一条直连通道直接将对应子层的输入连接到输出上去,从而避免由于网络过深在优化过程中潜在的梯度消失问题,公式如下:

    x l + 1 = f ( x l ) + x l x_{l+1} = f(x_l) + x_l xl+1=f(xl)+xl

  • 层归一化:一种对每个子层的输出进行规范化处理的方法,用于将数据平移缩放到均值为0,方差为1 的标准分布。这样能让训练变得更加稳定

例子

层归一化:

假设一个Transformer模型的隐藏层维度为d=4,输入为一个三词的序列,即x=[x1,x2,x3],其中x1,x2,x3都是4维的向量。那么隐藏层的输出为h=[h1,h2,h3],也是一个三词的序列,其中h1,h2,h3也都是4维的向量。我们可以用矩阵形式表示h:

h = [ [ h 11 , h 12 , h 13 , h 14 ] , [ h 21 , h 22 , h 23 , h 24 ] , [ h 31 , h 32 , h 33 , h 34 ] ] h = [[h_{11}, h_{12}, h_{13}, h_{14}], [h_{21}, h_{22}, h_{23}, h_{24}], [h_{31}, h_{32}, h_{33}, h_{34}]] h=[[h11,h12,h13,h14],[h21,h22,h23,h24],[h31,h32,h33,h34]]

我们需要分别计算每个词向量的均值和方差,即:

m e a n i = 1 4 ∑ j = 1 4 h i j mean_i = \frac{1}{4} \sum_{j=1}^4 h_{ij} meani=41j=14hij

v a r i = 1 4 ∑ j = 1 4 ( h i j − m e a n i ) 2 var_i = \frac{1}{4} \sum_{j=1}^4 (h_{ij} - mean_i)^2 vari=41j=14(hijmeani)2

其中 i = 1 , 2 , 3 i = 1, 2, 3 i=1,2,3 分别表示第一个,第二个和第三个词。然后,我们用这些均值和方差对每个词向量进行归一化,即:

z i = h i − m e a n i v a r i + e p s z_i = \frac{h_i - mean_i}{\sqrt{var_i + eps}} zi=vari+eps himeani

其中 e p s eps eps 是一个很小的正数,用来避免除零错误。这样,我们就得到了归一化后的词向量矩阵:

z = [ [ z 1 ] , [ z 2 ] , [ z 3 ] ] z = [[z_1], [z_2], [z_3]] z=[[z1],[z2],[z3]]

这种归一化方法可以使每个词向量在不同的样本和层之间具有相同的尺度,从而减少了梯度消失或爆炸的风险,提高了模型的稳定性和效率。

残差连接

假设我们有一个由两层Transformer 单元组成的编码器,每个单元包含一个多头自注意力层和一个前馈层。输入序列为 x 0 , x 1 , . . . , x n x_0, x_1, ..., x_n x0,x1,...,xn,其中 x i x_i xi 是一个 d d d 维的向量。那么第一层Transformer 单元的输出为:

z 0 = MultiHead ( x 0 , x 0 , x 0 ) + x 0 z 1 = MultiHead ( x 1 , x 1 , x 1 ) + x 1 ⋮ z n = MultiHead ( x n , x n , x n ) + x n y 0 = FFN ( z 0 ) + z 0 y 1 = FFN ( z 1 ) + z 1 ⋮ y n = FFN ( z n ) + z n \begin{aligned} z_0 &= \text{MultiHead}(x_0, x_0, x_0) + x_0 \\ z_1 &= \text{MultiHead}(x_1, x_1, x_1) + x_1 \\ &\vdots \\ z_n &= \text{MultiHead}(x_n, x_n, x_n) + x_n \\ y_0 &= \text{FFN}(z_0) + z_0 \\ y_1 &= \text{FFN}(z_1) + z_1 \\ &\vdots \\ y_n &= \text{FFN}(z_n) + z_n \end{aligned} z0z1zny0y1yn=MultiHead(x0,x0,x0)+x0=MultiHead(x1,x1,x1)+x1=MultiHead(xn,xn,xn)+xn=FFN(z0)+z0=FFN(z1)+z1=FFN(zn)+zn

其中 MultiHead \text{MultiHead} MultiHead 是多头自注意力函数, FFN \text{FFN} FFN 是前馈网络函数。然后我们对每个 y i y_i yi 进行层归一化操作,得到第一层Transformer 单元的最终输出:

h 0 = LayerNorm ( y 0 ) h 1 = LayerNorm ( y 1 ) ⋮ h n = LayerNorm ( y n ) \begin{aligned} h_0 &= \text{LayerNorm}(y_0) \\ h_1 &= \text{LayerNorm}(y_1) \\ &\vdots \\ h_n &= \text{LayerNorm}(y_n) \end{aligned} h0h1hn=LayerNorm(y0)=LayerNorm(y1)=LayerNorm(yn)

其中 LayerNorm \text{LayerNorm} LayerNorm 是层归一化函数。接着我们将 h i h_i hi 作为第二层Transformer 单元的输入,重复上述过程,得到编码器的最终输出。

代码

import torch
import torch.nn as nn

class NormLayer(nn.Module):
    def __init__(self, d_model, eps=1e-6):
        super().__init__()
        self.size = d_model
        self.eps = eps

        # 两个可学习参数
        self.alpha = nn.Parameter(torch.ones(self.size))  # 可学习的缩放参数
        self.bias = nn.Parameter(torch.zeros(self.size))  # 可学习的偏置参数

    def forward(self, x):
        norm = self.alpha * (x - x.mean(dim=-1, keepdim=True)) \
        / (x.std(dim=-1, keepdim=True) + self.eps) + self.bias  # 归一化操作和残差连接
        return norm

开始搭建:Encorder & Deocrder

在这里插入图片描述

介绍完几个关键的组成部分,接下来就可以把他们搭建成Transformer的编码器和解码器了

编码器端的实现比较简单,就是用上面介绍的模块搭起来。解码器端就要复杂一些了。主要有两个不同的地方。

第一,解码器的每个Transformer 块里面,第一个自注意力子层要加一个注意力掩码,就是图中的掩码多头注意力(Masked Multi-Head Attention)部分。这是因为在翻译的时候,编码器端只是把源语言序列的信息编码好,这个序列是已经知道的,所以编码器只要考虑怎么把上下文语义信息融合好就行了。而解码器端要生成目标语言序列,这个过程是自回归的,也就是说,每生成一个单词,只能看到当前单词之前的目标语言序列,不能看到后面的。所以要加一个掩码,把后面的文本信息遮住,不让模型在训练的时候直接看到后面的文本序列,不然就没法训练好。

第二,解码器端还多了一个多头注意力模块,用交叉注意力(Cross-attention)方法,同时接收编码器端的输出和当前Transformer 块的前一个掩码注意力层的输出。查询是用解码器前一层的输出投影出来的,而键和值是用编码器的输出投影出来的。它的作用是在翻译的时候,为了生成合理的目标语言序列要看看源语言序列是什么。基于这样的编码器和解码器结构,源语言文本先经过编码器端的每个Transformer 块对它的上下文语义做一层层的提取,最后输出每个源语言单词相关的表示。解码器端按照自回归的方式生成目标语言文本,也就是说,在每个时间步t,根据编码器端输出的源语言文本表示,和前t − 1 个时刻生成的目标语言文本,生成当前时刻的目标语言单词。

代码如下:

  • Encorder部分:
import torch.nn as nn
import torch.nn.functional as F

class EncoderLayer(nn.Module):
    def __init__(self, d_model, n_heads, dropout=0.1):
        super().__init__()

        self.norm_1 = Norm(d_model)  # 第一个归一化层
        self.norm_2 = Norm(d_model)  # 第二个归一化层

        self.attn = MultiHeadAttention(d_model, n_heads, dropout=dropout)  # 多头注意力机制
        self.ff = FeedForward(d_model, dropout=dropout)  # 前馈神经网络

        self.dropout_1 = nn.Dropout(dropout)  # 第一个丢弃层
        self.dropout_2 = nn.Dropout(dropout)  # 第二个丢弃层

    def forward(self, x, mask):
        x2 = self.norm_1(x)  # 对输入进行归一化
        x = x + self.dropout_1(self.attn(x2, x2, x2, mask))  # 应用多头注意力机制和残差连接,并使用丢弃层
        x2 = self.norm_2(x)  # 对上一步的输出进行归一化
        x = x + self.dropout_2(self.ff(x2))  # 应用前馈神经网络和残差连接,并使用丢弃层

        return x

class Encorder(nn.Module):
    def __init__(self, vocab_size, d_model, N, heads, dropout):
        super().__init__()
        self.N = N
        self.embed = Embedder(vocab_size, d_model)  # 嵌入层
        self.pe = PositionalEncoder(d_model, dropout=dropout)  # 位置编码
        self.layers = get_clones(EncoderLayer(d_model, heads, dropout), N)  # 创建 N 个编码器层
        self.norm = Norm(d_model)  # 归一化层

    def forward(self, src, mask):
        x = self.embed(src)  # 对输入源序列进行嵌入
        x = self.pe(x)  # 对嵌入序列应用位置编码
        for i in range(self.N):
            x = self.layers[i](x, mask)  # 将序列通过每个编码器层

        return self.norm(x)  # 对最终输出进行归一化
  • Decorder部分:
import torch.nn as nn
import torch.nn.functional as F

class DecoderLayer(nn.Module):
    def __init__(self, d_model, heads, dropout=0.1):
        super(DecoderLayer, self).__init__()

        self.norm_1 = Norm(d_model)  # 归一化层 1
        self.norm_2 = Norm(d_model)  # 归一化层 2
        self.norm_3 = Norm(d_model)  # 归一化层 3

        self.dropout_1 = nn.Dropout(dropout)  # 丢弃层 1
        self.dropout_2 = nn.Dropout(dropout)  # 丢弃层 2
        self.dropout_3 = nn.Dropout(dropout)  # 丢弃层 3

        self.attn1 = MultiHeadAttention(heads, d_model, dropout=dropout)  # 注意力机制 1
        self.attn2 = MultiHeadAttention(heads, d_model, dropout=dropout)  # 注意力机制 2
        self.ff = FeedForward(d_model, dropout=dropout)  # 前馈神经网络

    def forward(self, x, e_outputs, src_mask, trg_mask):
        x2 = self.norm_1(x)  # 对输入进行归一化
        x = x + self.dropout_1(self.attn1(x2, x2, x2, trg_mask))  # 应用注意力机制 1 和残差连接,并使用丢弃层
        x2 = self.norm_2(x)  # 对上一步的输出进行归一化
        x = x + self.dropout_2(self.attn2(x2, e_outputs, e_outputs, src_mask))  # 应用注意力机制 2 和残差连接,并使用丢弃层
        x2 = self.norm_3(x)  # 对上一步的输出进行归一化
        x = x + self.dropout_3(self.ff(x2))  # 应用前馈神经网络和残差连接,并使用丢弃层
        return x

class Decoder(nn.Module):
    def __init__(self, vocab_size, d_model, N, heads, dropout=0.1):
        super().__init__()
        self.N = N
        self.embed = Embedder(vocab_size, d_model)  # 嵌入层
        self.ps = PositionalEncoder(d_model, dropout=dropout)  # 位置编码
        self.layers = get_clones(DecoderLayer(d_model, heads, dropout), N)  # 创建 N 个解码器层
        self.norm = Norm(d_model)  # 归一化层

    def forward(self, trg, e_outputs, src_mask, trg_mask):
        x = self.embed(trg)  # 对目标序列进行嵌入
        x = self.ps(x)  # 对嵌入序列应用位置编码
        for i in range(self.N):
            x = self.layers[i](x, e_outputs, src_mask, trg_mask)  # 将序列通过每个解码器层
        return self.norm(x)  # 对最终输出进行归一化
  • 最终的Transformer:
import torch.nn as nn

class Transformer(nn.Module):
    def __init__(self, src_vocab, trg_vocab, d_model, N, heads, dropout):
        super().__init__()
        self.encoder = Encoder(src_vocab, d_model, N, heads, dropout)
        self.decoder = Decoder(trg_vocab, d_model, N, heads, dropout)
        self.out = nn.Linear(d_model, trg_vocab)

    def forward(self, src, trg, src_mask, trg_mask):
        e_outputs = self.encoder(src, src_mask)
        d_output = self.decoder(trg, e_outputs, src_mask, trg_mask)
        output = self.out(d_output)
        return output

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/296143.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

金蝶Apusic应用服务器 loadTree JNDI注入漏洞

产品介绍 金蝶Apusic是一款企业级应用服务器,支持Java EE技术,适用于各种商业环境。 漏洞概述 由于金蝶Apusic应用服务器权限验证不当,使用较低JDK版本,导致攻击者可以向loadTree接口执行JNDI注入,远程加载恶意类&a…

C++学习day--25 俄罗斯方块游戏图像化开发

项目分析 项目演示、项目分析 启动页面 启动页面&#xff1a; 分析&#xff1a; 开发环境搭建 1&#xff09;安装vc2010, 或其他vs版本 2&#xff09;安装easyX图形库 代码实现: # include <stdio.h> # include <graphics.h> void welcome(void) { initgraph(55…

LINUX服务器防火墙nf_conntrack问题一例

一、故障现象 业务反馈服务异常,无法响应请求&#xff0c;从系统日志 dmesg 或 /var/log/messages 看到大量以下记录&#xff1a;kernel: nf_conntrack: table full, dropping packet. 二、问题分析 业务高峰期服务器访问量大&#xff0c;内核 netfilter 模块 conntrack 相关参…

听GPT 讲Rust源代码--compiler(19)

File: rust/compiler/rustc_target/src/spec/mips_unknown_linux_gnu.rs 该文件&#xff08;rust/compiler/rustc_target/src/spec/mips_unknown_linux_gnu.rs&#xff09;是Rust编译器针对MIPS架构上的Linux系统的目标描述文件。它的作用是定义了在这个目标上编译时的一些配置…

mysql之数据类型、建表以及约束

目录 一. CRUD 1.1 什么是crud 1.2 select(查询) 1.3 INSERT(新增) 1.4 UPDATE(修改&#xff09; 1.5 DELETE(删除) 二. 函数 2.1 常见函数 2.2 流程控制函数 2.3聚合函数 三. union与union all 3.1 union 3.2 union all 3.3 具体不同 3.4 结论 四、思维导图 一. CRUD 1.1…

建站指南,如何将拥有的域名自定义链接到wordpress

关于Dynadot Dynadot是通过ICANN认证的域名注册商&#xff0c;自2002年成立以来&#xff0c;服务于全球108个国家和地区的客户&#xff0c;为数以万计的客户提供简洁&#xff0c;优惠&#xff0c;安全的域名注册以及管理服务。 在Dynadot上&#xff0c;我们可已经账户中管理的…

OpenHarmony之消息机制实现

OpenHarmony之消息机制实现 背景 在之前的介绍&#xff08;OpenHarmony之HDF驱动框架&#xff09;中&#xff0c;了解到OpenHarmony的消息机制主要有以下两种&#xff1a; 用户态应用发送消息到驱动。用户态应用接收驱动主动上报事件。 下面我们分别来看看两种机制用户态的…

使用 dbgate 在 sealos 上完美管理 mysql pgsql 等数据库

先登录 sealos 创建数据库&#xff0c;可以创建个 pgsql: 再到模版市场启动 dbgate: 配置数据库的连接信息&#xff0c;即可搞定收工 sealos 以kubernetes为内核的云操作系统发行版&#xff0c;让云原生简单普及 laf 写代码像写博客一样简单&#xff0c;什么docker kubernete…

Linux系统使用超详细(七)~定时任务:crontab

目录 一、定时任务简介 1.1cron工具概述 1.2crontab命令简述 二、crontab安装与检查 2.1安装步骤 2.2查看crontab的状态 2.3查看crontab服务 三、crontab命令 3.1常用命令 3.2定时任务的写法 3.3实例 四、添加定时任务 4.1使用crontab -e命令 4.2创建一个独立的定…

银河麒麟Kylin-Server-V10-SP3使用ISO镜像搭建本地内网YUM/DNF源cdrom/http

机房服务器安装一般是内网环境&#xff0c;需要配置本地的YUM/DNF源。本文介绍通过ISO镜像搭建内网环境的UM/DNF源 准备工作&#xff1a; 提前准备好Kylin-Server-V10-SP3的ISO镜像文件。 本机IP地址&#xff1a;192.168.40.201 镜像存放目录/data/iso/Kylin-Server-V10-SP3-Ge…

编写一个弹跳小球的程序,小球在窗口中四处反弹(python)

import pygame import random# 初始化Pygame pygame.init()# 窗口尺寸 width 800 height 600# 创建窗口 screen pygame.display.set_mode((width, height)) pygame.display.set_caption("Bouncing Ball")# 小球初始位置和速度 ball_radius 20 ball_color (255, …

前端页面的生命周期

性能问题呈现给用户的感受往往就是简单而直接的&#xff1a;加载资源缓慢、运行过程卡顿或响应交互延迟等。而在前端工程师的眼中&#xff0c;从域名解析、TCP建立连接到HTTP的请求与响应&#xff0c;以及从资源请求、文件解析到关键渲染路径等&#xff0c;每一个环节都有可能因…

iOS苹果和Android安卓测试APP应用程序的区别差异

在移动应用开发中&#xff0c;测试是一个至关重要的环节。无论是iOS苹果还是Android安卓&#xff0c;测试APP应用程序都需要注意一些差异和细节。本文将详细介绍iOS和Android的测试差异&#xff0c;包括操作系统版本、设备适配、测试工具和测试策略&#xff0c;并回答一些新手容…

web3d-three.js场景设计器-TransformControls模型控制器

场景设计器-TransformControls 控制器 该控制器可以指定模型进入可控制模式-如图有三种控制方式 translate --移动模式 rotate -- 旋转模式 scale -- 缩放模式 方便布局过程中快捷对模型进行摆放操作。 引入方式 import { TransformControls } from three/examples/jsm/…

深入Pandas(二):高级数据处理技巧

文章目录 系列文章目录引言时间序列分析可视化示例 高级数据分析技术分组与聚合操作时间序列分析 高级数据操作数据合并与重塑示例&#xff1a;数据合并merge示例&#xff1a;数据合并concat示例&#xff1a;数据重塑 - 透视表 高级索引技巧 结论 系列文章目录 Python数据分析…

C++《异常》

前言&#xff1a;C有一套独立的异常处理机制,今天就来做详细的介绍try,catch这两个词等 在C语言中处理错误的方式和缺陷有&#xff1a; 返回错误码。 缺陷&#xff1a; 1.错误码不好设置&#xff0c;比如&#xff1a;除0操作&#xff0c;就不好返回错误码。如果返回一个数字&…

uniapp微信小程序投票系统实战 (SpringBoot2+vue3.2+element plus ) -小程序微信用户登录实现

锋哥原创的uniapp微信小程序投票系统实战&#xff1a; uniapp微信小程序投票系统实战课程 (SpringBoot2vue3.2element plus ) ( 火爆连载更新中... )_哔哩哔哩_bilibiliuniapp微信小程序投票系统实战课程 (SpringBoot2vue3.2element plus ) ( 火爆连载更新中... )共计21条视频…

LabVIEW在高级结构监测中的创新应用

LabVIEW在高级结构监测中的创新应用 LabVIEW作为一个强大的系统设计平台&#xff0c;其在基于BOTDA&#xff08;光时域反射分析&#xff09;技术的结构监测中发挥着核心作用。利用LabVIEW的高效数据处理能力和友好的用户界面&#xff0c;开发了一个先进的监测系统。该系统专门…

如何从格式化的 Windows 和 Mac 电脑硬盘恢复文件

格式化硬盘可为您提供全新的体验。它可以是硬盘驱动器定期维护的一部分&#xff0c;是清除不再使用的文件的一种方法&#xff0c;在某些情况下&#xff0c;它是处理逻辑损坏的万福玛利亚。但是&#xff0c;许多用户发现自己格式化了错误的分区或驱动器&#xff0c;或者后来意识…

箭头函数 - JavaScript的新宠儿

&#x1f4e2; 鸿蒙专栏&#xff1a;想学鸿蒙的&#xff0c;冲 &#x1f4e2; C语言专栏&#xff1a;想学C语言的&#xff0c;冲 &#x1f4e2; VUE专栏&#xff1a;想学VUE的&#xff0c;冲这里 &#x1f4e2; CSS专栏&#xff1a;想学CSS的&#xff0c;冲这里 &#x1f4…
最新文章