基于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 mathclass 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) # 储存位置编码的Tensorfor 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 mathclass MultiHeadAttention(nn.Module):def __init__(self, d_model, heads, dropout=0.1):super().__init__()self.d_model = d_modelself.d_k = d_model // heads  # 确保每个头的维度加起来等于模型的总维度self.h = headsself.q_linear = nn.Linear(d_model, d_model)  # 线性变换,将输入维度调整为d_modelself.v_linear = nn.Linear(d_model, d_model)  # 线性变换,将输入维度调整为d_modelself.k_linear = nn.Linear(d_model, d_model)  # 线性变换,将输入维度调整为d_modelself.dropout = nn.Dropout(dropout)  # Dropout层,用于随机置零一部分输入self.out = nn.Linear(d_model, d_model)  # 线性变换,将输入维度调整为d_modeldef 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 计算后为0if 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 outputdef 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的维度# 计算attentionscores = 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 Fclass FeedForward(nn.Module):def __init__(self, d_model, d_ff=2048, dropout=0.1):super(FeedForward, self).__init__()# d_ff默认为2048self.linear_1 = nn.Linear(d_model, d_ff)  # 线性变换,将输入维度调整为d_ffself.dropout = nn.Dropout(dropout)  # Dropout层,用于随机置零一部分输入self.linear_2 = nn.Linear(d_ff, d_model)  # 线性变换,将输入维度调整为d_modeldef forward(self, x):x = self.dropout(F.relu(self.linear_1(x)))  # 使用ReLU激活函数并应用dropoutx = 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 nnclass NormLayer(nn.Module):def __init__(self, d_model, eps=1e-6):super().__init__()self.size = d_modelself.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 Fclass 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 xclass Encorder(nn.Module):def __init__(self, vocab_size, d_model, N, heads, dropout):super().__init__()self.N = Nself.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 Fclass DecoderLayer(nn.Module):def __init__(self, d_model, heads, dropout=0.1):super(DecoderLayer, self).__init__()self.norm_1 = Norm(d_model)  # 归一化层 1self.norm_2 = Norm(d_model)  # 归一化层 2self.norm_3 = Norm(d_model)  # 归一化层 3self.dropout_1 = nn.Dropout(dropout)  # 丢弃层 1self.dropout_2 = nn.Dropout(dropout)  # 丢弃层 2self.dropout_3 = nn.Dropout(dropout)  # 丢弃层 3self.attn1 = MultiHeadAttention(heads, d_model, dropout=dropout)  # 注意力机制 1self.attn2 = MultiHeadAttention(heads, d_model, dropout=dropout)  # 注意力机制 2self.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 xclass Decoder(nn.Module):def __init__(self, vocab_size, d_model, N, heads, dropout=0.1):super().__init__()self.N = Nself.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 nnclass 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.mzph.cn/news/600402.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

fmincon函数求解非线性超越方程的学习记录

最近的算法中用到了fmincon函数,寻找多变量非线性方程最小值的函数;因此学习一下; fmincon函数的基础语法如下所示: fmincon函数是为了求解下列方程的最小值; b 和 beq 是向量,A 和 Aeq 是矩阵&#xff0c…

2024年最新51单片机+Proteus嵌入式开发入门实战完整版教程

我们为什么要学嵌入式开发? 嵌入式系统是一种专为特定任务或特定应用设计的计算机系统。与通用计算机系统不同,嵌入式系统通常具有更小的体积、更低的功耗和更强的可靠性。由于这些特点,嵌入式系统广泛应用于工业控制、医疗设备、智能家居、…

【C++进阶04】STL中map、set、multimap、multiset的介绍及使用

一、关联式容器 vector/list/deque… 这些容器统称为序列式容器 因为其底层为线性序列的数据结构 里面存储的是元素本身 map/set… 这些容器统称为关联式容器 关联式容器也是用来存储数据的 与序列式容器不同的是 其里面存储的是<key, value>结构的键值对 在数据检索时…

JVM工作原理与实战(七):类的生命周期-初始化阶段

专栏导航 JVM工作原理与实战 RabbitMQ入门指南 从零开始了解大数据 目录 专栏导航 前言 一、类的生命周期 1.加载&#xff08;Loading&#xff09; 2.连接&#xff08;Linking&#xff09; 3.初始化&#xff08;Initialization&#xff09; 4.使用&#xff08;Using&…

canvas文字设置(含最大宽度)的示例

查看专栏目录 canvas示例教程100专栏&#xff0c;提供canvas的基础知识&#xff0c;高级动画&#xff0c;相关应用扩展等信息。canvas作为html的一部分&#xff0c;是图像图标地图可视化的一个重要的基础&#xff0c;学好了canvas&#xff0c;在其他的一些应用上将会起到非常重…

Linux tty命令教程:掌握Linux终端的使用(附案例详解和注意事项)

Linux tty命令介绍 tty命令在Linux中是一个简单的实用程序&#xff0c;用于检查连接到标准输入的终端。tty是“teletypewriter”的缩写&#xff0c;但通常被称为终端&#xff0c;它允许您通过将数据&#xff08;您的输入&#xff09;传递给系统并显示系统产生的输出来与系统进…

springboot + vue 前后端加密传输 RSA互相加解密、加签验签、密钥对生成

参考 二、关于PKCS#1和PKCS#8格式密钥 由于Java非对称加解密、加验签都是采用PKCS#8格式的密钥&#xff0c;PKCS#1格式的密钥跑不通&#xff0c;这里先简单介绍一下两者的区别。 1、简介 PKCS#1和PKCS#8是两个不同的数字证书标准。 PKCS#1是一个公钥加密标准&#xff0c;它…

【解决|三方工具】导入 XChart 后提示丢失关于 TMPPro 工具引用

开发平台&#xff1a;Unity 2021 版本 插件版本&#xff1a;XChart 3.0&#xff1a;官方文档 - https://github.com/XCharts-Team/XCharts   问题描述 导入 XChart 插件至 Unity 中出现 目录&#xff1a;Component、Theme 等提示丢失 TMPPro&#xff08;TextMeshPro 工具&…

微信小程序的5种打开页面方式

wx.navigateTo&#xff1a;打开新页面 使用 wx.navigateTo 可以打开一个新的页面&#xff0c;新页面会被放置在当前页面的上层。 wx.navigateTo({url: /pages/nextPage/nextPage });wx.redirectTo&#xff1a;关闭当前页面&#xff0c;打开新页面 使用 wx.redirectTo 关闭当…

2024年,Web3技术的“iPhone时刻”会出现吗?

出品&#xff5c;欧科云链研究院 关于2024年的Web3市场&#xff0c;大多数人目前是持乐观态度的。随着宏观政策稳定&#xff0c;美国和香港地区不断释放加密资产现货ETF的利好&#xff0c;叠加铭文热潮及减半周期临近&#xff0c;市场对Web3及加密资产的兴趣正持续上涨。年后的…

[蓝桥杯学习]树的直径与重心

树的直径 定义 为什么不直接说&#xff08;u,v&#xff09;是两个叶子&#xff0c;可能有如下情况&#xff1a; 这是一条链&#xff0c;且u为根&#xff0c;但&#xff0c;度数为1 下面这个情况是不经过根的。 求解方法 如果设根u的深度为0时&#xff0c;直径就是深度dep[v]…

代码随想录刷题第三十八天| 理论基础 ● 509. 斐波那契数 ● 70. 爬楼梯 ● 746. 使用最小花费爬楼梯

代码随想录刷题第三十八天 动态规划基础理论 斐波那契数 (LC 509) 题目思路&#xff1a; 代码实现&#xff1a; class Solution:def fib(self, n: int) -> int:if n<1: return ndp [0 for _ in range(n1)]dp[1] 1for i in range(2, n1):dp[i] dp[i-1]dp[i-2] …

教学/直播/会议触摸一体机定制_基于展锐T820安卓核心板方案

触控一体机是一种集先进的触摸屏、工控和计算机技术于一体的设备。它取代了传统的键盘鼠标输入功能&#xff0c;广泛应用于教学、培训、工业、会议、直播、高新科技展示等领域。触摸一体机的应用提升了教学、会议和展示的互动性和信息交流。 触摸一体机方案基于国产6nm旗舰芯片…

【Spring实战】23 Spring Actuator 常用的自定义

文章目录 1. 自定义健康指示器2. 自定义端点3. 自定义端点路径4. 自定义 Actuator 端点的访问权限5. 启动服务6. 访问自定义的 custom 端点总结 Spring Actuator 是 Spring 框架的一个模块&#xff0c;为开发人员提供了一套强大的监控和管理功能。上一篇 【Spring实战】22 Spri…

1.5 CHALLENGES IN PARALLEL PROGRAMMING

是什么让并行编程变得困难&#xff1f;有人曾经说过&#xff0c;如果你不关心性能&#xff0c;并行编程很容易。你可以在一小时内编写一个并行程序。但是&#xff0c;如果你不在乎性能&#xff0c;为什么要费心写一个并行程序呢&#xff1f; 这本书解决了在并行编程中实现高性…

C语言全面学习基础阶段01—C生万物

如何学好 C 语言 1. 鼓励你&#xff0c;为你叫好。 C 生万物 编程之本 长远 IT 职业发展的首选 C 语言是母体语言&#xff0c;是人机交互接近底层的桥梁 学会 C/C &#xff0c;相当于掌握技术核心 知识点一竿子打通。 IT 行业&#xff0c;一般每 10 年就有一次变革 40 年间&a…

汽车信息安全--芯片厂、OEM安全启动汇总(2)

目录 1.STM32 X-CUBE-SBSFU 2.小米澎湃OS安全启动 3.小结 在汽车信息安全--芯片厂、OEM安全启动汇总-CSDN博客,我们描述了芯驰E3的安全启动机制,接下来我们继续看其他芯片、OEM等安全启动机制。 1.STM32 X-CUBE-SBSFU 该产品全称Secure Boot and Secure

5.2 Android BCC环境搭建(eadb版 上)

写在前面 eadb即eBPF Android Debug Bridge,它是基于adeb的重构。后者曾随aosp 10发布在platform/external目录下。 一,root权限 这里再HighLight下,当前整个专栏都是基于开发环境来展开的,也就是Android设备需要具有root权限。因此该专栏下每一篇博客都是默认了当前开发…

科锐16位汇编学习笔记01汇编基础和debug使用

为什么学习16位汇编&#xff1f; 16位操作指令最多能够操作两个字节&#xff0c;且更能够体现出与硬件的交互。16位下的指令和32位汇编的指令差不多。16位汇编的指令在32位一样使用.要学好汇编必须要了解一点点硬件知识,16汇编是直接操作硬件,32位汇编指令跟硬件隔离了 硬件运…

UICollection Compositional Layout全详解

本文字数&#xff1a;8325字 预计阅读时间&#xff1a;45分钟 01 Collection View Layout全详解 UICollectionView在iOS中是构建复杂布局的强大工具。iOS13中引入的 UICollectionViewCompositionalLayout为创建自定义布局提供了全新的可能性。本文将深入探讨Compositional Lay…