【基于 PyTorch 的 Python 深度学习】8 注意力机制(4):PyTorch 实现(上)

news2024/10/6 10:42:25

前言

文章性质:学习笔记 📖

学习资料:吴茂贵《 Python 深度学习基于 PyTorch ( 第 2 版 ) 》【ISBN】978-7-111-71880-2

主要内容:根据学习资料撰写的学习笔记,该篇主要介绍了如何使用 PyTorch 实现 Transformer 。

代码链接:Python-DL-PyTorch2/pytorch-08/pytorch-08_01.ipynb

目录

前言

一、使用 PyTorch 实现 Transformer

1、Transformer 背景介绍

2、构建 Encoder-Decorder

3、构建编码器

4、构建解码器

5、构建多头注意力

6、构建前反馈神经网络层

7、预处理输入数据


一、使用 PyTorch 实现 Transformer

Transformer 的原理在前面已经分析得较为详细,本文重点介绍如何使用 PyTorch 来实现。我们将使用 PyTorch 1.0+ 版本完整实现 Transformer 架构,并用简单实例进行验证。本文代码参考了哈佛大学 OpenNMT 团队针对 Transformer 实现的代码。该代码是用 PyTorch 0.3.0 实现的,代码地址为:http://nlp.seas.harvard.edu/2018/04/03/attention.html 。

1、Transformer 背景介绍

目前主流的神经序列转换模型大都基于 Encoder-Decoder 架构。所谓序列转换模型就是把一个输入序列转换成另外一个输出序列,它们的长度可能不同。比如基于神经网络的机器翻译,输入是中文句子,输出是英语句子,这就是一个序列转换模型。类似的,文本摘要、对话等问题都可以看作序列转换问题。虽然这里主要关注机器翻译,但是任何输入是一个序列输出是另外一个序列的问题都可以使用 Encoder-Decoder 模型。

Transformer 使用编码器-解码器模型的架构,只不过它的编码器是由 N(6) 个 EncoderLayer 组成,每个 EncoderLayer 包含一个自注意力 SubLayer 层和一个全连接 SubLayer 层。而它的解码器也是由 N(6) 个 DecoderLayer 组成,每个 DecoderLayer 包含一个自注意力 SubLayer 层、一个注意力 SubLayer 层、一个全连接 SubLayer 层。

编码器 Encoder 把输入序列 ( x1, … , xn ) 映射或编码成一个连续的序列 z = ( z1, … , zn ) 。而解码器 Decoder 根据 z 来解码得到输出序列 y1, … , ym 。解码器 Decoder 是自回归的(Auto-Regressive),会把前一个时刻的输出作为当前时刻的输入。

2、构建 Encoder-Decorder

构建 编码器-解码器模型 的代码如下:

① 导入需要的库。

import numpy as np
import torch
import torch.nn as nn
import torch.nn.functional as F
import math, copy, time

import matplotlib.pyplot as plt
import seaborn
seaborn.set_context(context="talk")

%matplotlib inline

② 构建 Encoder-Decoder 架构。

class EncoderDecoder(nn.Module):
    """
    这是一个标准的 Encoder-Decoder 架构
    """
    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 
        # 解码器部分最后的 Linear+softmax
        self.generator = generator  
        
    def forward(self, src, tgt, src_mask, tgt_mask):
        # 接收并处理屏蔽 src 和目标序列,
        # 首先调用 encode 方法对输入进行编码,然后调用 decode 方法进行解码
        return self.decode(self.encode(src, src_mask), src_mask, tgt, tgt_mask)
    
    def encode(self, src, src_mask):
        # 传入参数包括 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 都使用了 掩码(Mask),即对某些值进行掩盖,使其在参数更新时不产生效果。

Transformer 模型里面涉及两种掩码,分别是 Padding Mask 和 Sequence Mask。

Padding Mask 在所有的 scaled dot-product attention 里都需要用到,而 Sequence Mask 只在 Decoder 的 self-attention 里用到。

(1)Padding Mask

什么是 Padding Mask 呢? 因为每个批次输入序列长度是不一样的,也就是说,我们要对输入序列进行对齐。具体来说,如果输入的序列较短,则在后面填充 0 ;如果输入的序列太长,则截取左边的内容,把多余的部分直接舍弃。因为这些填充的位置其实是没什么意义的,所以注意力机制不应该把注意力放在这些位置上,需要进行一些处理。 具体的做法是,把这些位置的值加上一个非常大的负数(负无穷),这样经过 softmax ,这些位置的概率就会接近 0 ! 而 Padding Mask 实际是一个张量,每个值都是一个布尔值,值为 false 的地方就是我们要进行处理的地方。

(2)Sequence Mask

前文也提到,Sequence Mask 是为了使解码器无法看见未来的信息。也就是说,对于一个序列,在 time_step 为 t 的时刻,解码输出应该只能依赖于 t 时刻之前的输出,而不能依赖 t 之后的输出。因此我们需要想办法把 t 之后的信息给隐藏起来。 那么具体怎么做呢?也很简单:生成一个上三角矩阵,上三角的值全为 0 ,然后把这个矩阵作用在每一个序列上。 对于解码器的自注意力,里面使用到的缩放的点积注意力 scaled dot-product attention,同时需要 Padding Mask 和 Sequence Mask 作为 attn_mask,具体实现就是两个 Mask 相加作为 attn_mask 。在其他情况下,attn_mask 一律等于 Padding Mask。

③ 创建 Generator 类。通过一个全连接层,再经过 log_softmax 函数的作用,使 Decoder 的输出成为概率值。

class Generator(nn.Module):
    """ 定义标准的一个全连接(linear)+ softmax
    根据解码器的隐状态输出一个词
    d_model 是解码器输出的大小,vocab 是词典大小 """
    def __init__(self, d_model, vocab):
        super(Generator, self).__init__()
        self.proj = nn.Linear(d_model, vocab)
    # 全连接再加上一个 softmax
    def forward(self, x):
        return F.log_softmax(self.proj(x), dim=-1)

3、构建编码器

前文提到,编码器 Encoder 是由 N 个相同结构的 Encoderlayer 堆积而成的,而每个 Encoderlayer 层又有两个子层。第一个是多头自注意力层,第二个比较简单,是按位置全连接的前馈网络。其间还有 LayerNorm 及残差连接等。编码器的结构如下图所示。

① 定义复制模块的函数。

首先定义 clones 函数,用于克隆相同的 EncoderLayer 。

def clones(module, N):
    " 克隆 N 个完全相同的 SubLayer,使用了 copy.deepcopy "
    return nn.ModuleList([copy.deepcopy(module) for _ in range(N)])

这里使用了 nn.ModuleList ,ModuleList 就像一个普通的 Python 的 List ,可以使用下标来访问它,其优点是传入的 ModuleList 的所有 Module 都会注册到 PyTorch 中,这样优化器 Optimizer 就能找到其中的参数,从而能够用梯度下降更新这些参数。

但是 nn.ModuleList 并不是 Module(的子类),因此它没有 forward 等方法,我们通常把它放到某个 Module 里。

② 定义编码器 Encoder 。

class Encoder(nn.Module):
    " Encoder 由 N 个 EncoderLayer 堆积而成 "
    def __init__(self, layer, N):
        super(Encoder, self).__init__()
        self.layers = clones(layer, N)
        self.norm = LayerNorm(layer.size)
        
    def forward(self, x, mask):
        " 对输入 (x, mask) 进行逐层处理 "
        for layer in self.layers:
            x = layer(x, mask)
        return self.norm(x) # N 个 EncoderLayer 处理完成之后还需要一个 LayerNorm

编码器就是 N 个 SubLayer 的栈,最后加上一个 LayerNorm 。

③ 定义 LayerNorm 。

class LayerNorm(nn.Module):
    " 构建一个 LayerNorm 模型 "
    def __init__(self, features, eps=1e-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, keepdim=True)
        std = x.std(-1, keepdim=True)
        return self.a_2 * (x - mean) / (std + self.eps) + self.b_2

论文中的处理过程如下:

x -> x + self-attention(x) -> layernorm(x + self-attention(x)) => y
y -> dense(y) -> y + dense(y) -> layernorm(y + dense(y)) => z(输入下一层) 

这里把 layernorm 层放到前面了,即处理过程如下:

x -> layernorm(x) -> self-attention(layernorm(x)) -> x + self-attention(layernorm(x)) => y
y -> layernorm(y) -> dense(layernorm(y)) -> y + dense(layernorm(y)) => z(输入下一层)

PyTorch 中各层权重的数据类型是 nn.Parameter ,而非 Tensor ,需对初始化后的参数(Tensor 型)进行类型转换。每个 Encoder 层又有两个子层,每个子层通过残差把每层的输入输出转换为新的输出。无论是自注意力还是全连接,都首先是 LayerNorm 层,然后是自注意力层 / 稠密层,然后是 dropout 层,最后是残差连接。这里把这个过程封装成 SubLayer Connection。

④ 定义 SublayerConnection 。

class SublayerConnection(nn.Module):
    """
    LayerNorm + SubLayer (Self-Attenion / Dense) + dropout + 残差连接
    为了简单,把 LayerNorm 放到了前面,而原始论文是把 LayerNorm 放在最后
    """
    def __init__(self, size, dropout):
        super(SublayerConnection, self).__init__()
        self.norm = LayerNorm(size)
        self.dropout = nn.Dropout(dropout)

    def forward(self, x, sublayer):
        # 将残差连接应用于具有相同大小的任何子层
        return x + self.dropout(sublayer(self.norm(x)))

⑤ 构建 EncoderLayer 。

class EncoderLayer(nn.Module):
    " Encoder 由 self_attn 和 feed_forward 构成 "
    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):
        " 实现正向传播功能 "
        x = self.sublayer[0](x, lambda x: self.self_attn(x, x, x, mask))
        return self.sublayer[1](x, self.feed_forward)

为实现代码的复用,这里把 self_attn 层和 feed_forward 层作为参数传入,只构造两个 SubLayer 。

forward 调用 sublayer[0](SubLayer 对象),最终会调用其 forward 方法,这个方法需要 2 个参数:输入 Tensor ,对象或函数。

self_attn 函数需要 4 个参数:Query 的输入,Key 的输入,Value 的输入和 Mask 。故使用 lambda 将其变成一个参数 x 的函数。

lambda x: self.self_attn(x, x, x, mask) ,这里可以将 Mask 看作已知数。

4、构建解码器

解码器由 N 个 DecoderLayer 堆积而成,参数 layer 是 DecoderLayer,也是一个调用对象,最终会调用 DecoderLayer.forward 方法,需要 4 个参数:输入 x ,Encoder 的输出 memory ,输入 Encoder 的 Mask(src_mask) ,输入 Decoder 的 Mask(tgt_mask) 。这里所有的 Decoder 的 forward 方法也需要这 4 个参数。解码器的结构如下图所示。

① 定义解码器 Decoder 。

class Decoder(nn.Module):
    " 构建 N 个完全相同的 Decoder 层"
    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)

② 定义 DecoderLayer 。

class DecoderLayer(nn.Module):
    " Decoder 包括 self-attn, src-attn 和 feed forward "
    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):
        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)

DecoderLayer 比 EncoderLayer 多了一个 src-attn 层,这层的输入来自 Encoder 的输出 memory 。

src-attn 和 self-attn 的实现相同,只不过使用的 Query 、Key 和 Value 输入不同。

普通注意力(src-attn)的 Query 由下层输入进来(来自 self-attn 的输出),Key 和 Value 是 Encoder 最后一层的输出 memory 。

自注意力的 Query ,Key 和 Value 都由下层输入进来。

③ 定义 subsequent_mask 函数。

Decoder 和 Encoder 有一个关键的不同:Decoder 在解码第 t 个时刻的时候只能使用 1, … , t 时刻的输入,而不能使用 t+1 时刻及其之后的输入。因此我们需要一个函数来产生一个掩码矩阵,代码如下:

def subsequent_mask(size):
    " Mask out subsequent positions. "
    attn_shape = (1, size, size)
    subsequent_mask = np.triu(np.ones(attn_shape), k=1).astype('uint8')
    return torch.from_numpy(subsequent_mask) == 0

我们看一下这个函数生成的一个简单样例,假设语句长度为 6 。

plt.figure(figsize=(5,5))
plt.imshow(subsequent_mask(6)[0])

运行结果如图 8-44 所示。

查看序列掩码情况:

subsequent_mask(6)[0]

我们发现它输出的是一个方阵,对角线和下面都是 True 。第一行只有第一列是 True ,它的意思是时刻 1 只能关注输入 1 ,第三行说明时刻 3 可以关注 { 1, 2, 3 } 而不能关注 { 4, 5, 6 } 的输入,因为在真正解码的时候,这是属于预测的信息。知道了这个函数的用途之后,上面的代码就很容易理解了。

5、构建多头注意力

构建多头注意力的过程类似于卷积神经网络中构建多通道的过程,目的都是提升模型的泛化能力。下面来看具体构建过程。

① 定义注意力 attention 。

注意力(包括自注意力和普通的注意力)可以看成一个函数,它的输入是 Query 、Key 、Value 和 Mask,输出是一个 Tensor 。其中输出是 Value 的加权平均,而权重由 Query 和 Key 计算得出。具体的计算公式如下所示:

Attention(Q,K,V)=Softmax(\frac{QK^T}{\sqrt{d_k}})V

具体实现代码如下:

def attention(query, key, value, mask=None, dropout=None):
    " 计算缩放的点积注意力 "
    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 = F.softmax(scores, dim = -1)
    if dropout is not None:
        p_attn = dropout(p_attn)
    return torch.matmul(p_attn, value), p_attn

上面的代码实现 \frac{QK^T}{\sqrt{d_k}}​ 和公式里稍微不同,这里的 QK 都是 4 维张量(Tensor),包括 batch 和 head 维度。torch.matmul 会对 Query 和 Key 的最后两维进行矩阵乘法,这样效率更高,如果我们要用标准的矩阵(2 维张量)乘法来实现,那么需要遍历 batch 和 head 两个维度。用一个具体例子跟踪一些不同张量的形状变化,然后对照公式就很容易理解。比如 Q 是 (30, 8, 33, 64) ,其中 30 是 batch 的个数,8 是 head 的个数,33 是序列长度,64 是每个时刻的特征数。KQ 的形状必须相同,而 V 可以不同,但在这里,其形状也是相同的。接下来是 scores.masked_fill(mask == 0, -1e9) ,用于把 mask=0 的得分变成一个很小的数,这样经过softmax 之后的概率就很接近零。self_attention 中的掩码主要是 Padding Mask 格式,与解码器 Decoder 中的掩码格式不同。再对 score 进行 softmax 函数计算,把得分变成概率 p_attn ,如果有 dropout 层,则对 p_attn 进行 dropout(原论文无 dropout 层)。最后把 p_attn 和 value 相乘。p_attn 是 ( 30, 8, 33, 33 ) ,value 是 ( 30, 8, 33, 64 ) ,只看后两堆 (33×33) × (33×64) = (33×64) 。

② 定义多头注意力 Multi-headed Attention 。

前面可视化部分介绍了如何将输入变成 QK 和 V ,对于每一个头都使用 3 个矩阵 W^Q​、W^K​、W^V​把输入转换成 QK 和 V 。然后分别用每一个头进行自注意力的计算,把 N 个头的输出拼接起来,与矩阵 W^Q​相乘。具体计算多头注意力的公式如下:

这里的映射是参数矩阵,其中,h = 8​ ,d_k=d_v=\frac{d_{model}}{h}=64​ 。

W_i^Q \in R^{d_{model}d_k} ,W_i^K \in R^{d_{model}d_k},W_i^V \in R^{d_{model}d_v},W_i^O \in R^{hd_vd_{model}}

详细的计算过程如下:

class MultiHeadedAttention(nn.Module):
    def __init__(self, h, d_model, dropout=0.1):
        " 传入 head 个数及 model 的维度 "
        super(MultiHeadedAttention, self).__init__()
        assert d_model % h == 0
        # 这里假设 d_v = 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(p=dropout)
        
    def forward(self, query, key, value, mask=None):
        " Implements Figure 2 "
        if mask is not None:
            # 相同的 mask 适应所有的 head
            mask = mask.unsqueeze(1)
        nbatches = query.size(0)
        
        # 1) 首先使用线性变换,然后把 d_model 分配给 h 个 head ,每个 head 为 d_k = d_model / h         
        query, key, value = \
            [l(x).view(nbatches, -1, self.h, self.d_k).transpose(1, 2)
             for l, x in zip(self.linears, (query, key, value))]
        
        # 2) 使用 attention 函数计算 scaled-dot-product-attention 
        x, self.attn = attention(query, key, value, mask=mask, dropout=self.dropout)
        
        # 3) 实现多头注意力,用 view 函数把 8 个 head 的 64 维向量拼接成一个 512 的向量
        # 然后再使用一个线性变换 (512, 521),shape 不变. 
        x = x.transpose(1, 2).contiguous().view(nbatches, -1, self.h * self.d_k)
        return self.linears[-1](x)

zip(self.linears, (query, key, value)) 是把 (self.linears[0], self.linears[1], self.linears[2]) 和 (query, key, value) 放到一起再进行遍历。

只看 self.linears[0] (query)。根据构造函数的定义,self.linears[0] 是一个 (512, 512) 的矩阵,而 query 是 (batch, time, 512) 。

相乘之后得到新的 query 还是 512 (d_model) 维的向量,接着用 view 把它变成 (batch, time, 8, 64) 。

再调用 transponse 将其变成 (batch, 8, time, 64),这是 attention 函数要求的 shape。

(batch, 8, time, 64) 分别对应 8 个 head ,每个 head 的 query 都是 64 维。

keyvalue 的运算完全相同,因此也分别得到 8 个 head 的 64 维的 key 和 64 维的 value 。

接着调用 attention 函数,得到 x 和 self.attn 。x 的 shape 是 (batch, 8, time, 64) ,而 attn 是 (batch, 8, time, time) 。

调用 x.transpose(1, 2) 把 x 变成 (batch, time, 8, 64) ,再将其变成 (batch, time, 512) ,即把 8 个 64 维的向量拼接成 512 的向量。

最后用 self.linears[-1] 对 x 进行线性变换,self.linears[-1] 是 (512, 512) ,因此最终的输出还是 (batch, time, 512) 。

最初构造了 4 个 (512, 512) 的矩阵,前 3 个用于对 query ,key 和 value 进行变换,最后一个用于变换 8 个 head 拼接后的向量。

多头注意力的应用:

Encoder 的 Self-Attention 层:query ,key 和 value 都是相同的值,来自下层的输入。掩码都是 1(当然填充的不算)。

Decoder 的 Self-Attention 层:query ,key 和 value 都是相同的值,来自下层的输入。但是掩码使得它不能访问未来的输入。

Encoder-Decoder 的普通 Attention 层:query 来自下层的输入,而 key 和 value 都是 Encoder 最后一层的输出,掩码都是 1 。

6、构建前反馈神经网络层

除了注意力子层之外,编码器和解码器中的每个层都包含一个完全连接的前馈网络( Feed Forward ),该网络包括两个线性转换,中间有一个 ReLU 激活函数,具体公式为:

FFN(x)=max(0,xW_1+b_1)W_2+b_2

全连接层的输入和输出都是 d_model(512) 维的,中间隐含单元的个数是 d_ff (2048) ,具体代码如下:

class PositionwiseFeedForward(nn.Module):
    " Implements FFN equation "
    def __init__(self, d_model, d_ff, dropout=0.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(F.relu(self.w_1(x))))

7、预处理输入数据

输入的词序列都是 ID 序列,我们需要嵌入 Embedding 。源语言和目标语言都需要 Embedding ,此外我们还需要一个线性变换把隐含变量变成输出概率,这可以通过前面的类 Generator 来实现。Transformer 模型的注意力机制并没有包含位置信息,即一句话中词语在不同的位置时,在 Transformer 中是没有区别的,这当然是不符合实际的。因此,在 Transformer 中引入位置信息相比卷积神经网络 CNN 、循环神经网络 RNN 等模型有更加重要的作用。原论文作者添加位置编码的方法是:构造一个与输入嵌入维度一样的矩阵,然后与输入嵌入相加得到 multi-head attention 的输入。预处理输入数据的过程如图 8-45 所示。

① 先把输入数据转换为输入嵌入,具体代码如下:

class 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):
    " 实现 PE 函数 "
    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):
        x = x + self.pe[:, :x.size(1)].clone().detach()
        return self.dropout(x)

注意这里调用了 Module.register_buffer 函数。这个函数的作用是创建一个 buffer ,比如把 pi 保存下来。register_buffer 通常用于保存一些模型参数之外的值,比如在 BatchNorm 中,我们需要保存 running_mean(Moving Average) ,它不是模型的参数(不是通过迭代学习的参数),但是模型会修改它,而且在预测的时候也要使用它。这里也是类似的,pe 是一个提前计算好的常量,在构造函数里并没有把 pe 保存到 self 里,但是在 forward 函数中可以直接使用 self.pe 。如果保存(序列化)模型到磁盘,PyTorch 框架将保存 buffer 里的数据到磁盘,这样在反序列化的时候就可以恢复它们。

③ 可视化编码 。

假设输入的 ID 序列长度为 10 ,如果输入转为嵌入之后是 (10, 512) ,那么位置编码的输出也是 (10, 512) 。上面公式中的 pos 就是位置 0~9 ,512 维的偶数维使用 sin 函数,而奇数维使用 cos 函数。这种位置编码的好处是:PE 可以表示为 PE+k 式的线性函数,这样网络就能容易地学到相对位置的关系。 

图 8-46 是一个示例,向量的大小 d_model=20 ,这里画出第 4 、5 、6 和 7 维的图像(下标从零开始),最大的位置是 100 。

可以看到它们都是正弦(余弦)函数,而且周期越来越长。具体实现代码如下:

## 语句长度为 100 ,这里假设 d_model=20,
plt.figure(figsize=(15, 5))
pe = PositionalEncoding(20, 0)  
y = pe.forward(torch.zeros(1, 100, 20))  
plt.plot(np.arange(100), y[0, :, 4:8].data.numpy())
plt.legend(["dim %d"%p for p in [4,5,6,7]])

④ 下面来看一个生成位置编码的简单示例,代码如下:

d_model, dropout, max_len = 512, 0, 5000
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)
print(pe.shape)
pe = pe.unsqueeze(0)
print(pe.shape)

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

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

相关文章

数据库系统概念(第八周 第一堂)(规范化关系数据库设计)(强推学习!!!)

目录 前言 E-R模型质量低的深层原因 数据依赖 函数依赖 主属性/非主属性 逻辑蕴含与闭包 Armstrongs Axiom 求解F闭包算法 求解属性集闭包算法 属性集闭包的作用 候选码求解理论和算法 候选码求解理论 无关属性 检验方法 正则覆盖 关系模式的设计 关系…

vcpkg安装opencv中的特殊问题记录(无法找到opencv_corexd.dll)

我是按照网上的vcpkg安装opencv方法进行的(比如这篇:从0开始在visual studio上安装opencv(超详细,针对小白)),但是中间出现了一些别人没有遇到的问题,虽然原因没有找到,但…

[自动驾驶 SoC]-3 英伟达Orin

NVIDIA Jetson AGX OrinTM series (资料来源:nvidia-jetson-agx-orin-technical-brief.pdf) 1 整体介绍 1) Orin SoC结构 Orin SoC,如下图所示,由一个NVIDIA Ampere architecture GPU, Arm Cortex-A78AE CPU, 下一代深度学习核视觉处理加速…

MicroPython+ESP32 C3开发上云

传感器PinI/O状态D412输出1开0关D513输出1开0关 概述 MicroPython是python3编程语言的精简实现,能够在资源非常有限的硬件上运行,如MCU微控制器Micropython的网络功能和计算功能很强大,有非常多的库可以使用,它为嵌入式开发带来了…

Windows NT 3.5程序员讲述微软标志性“3D管道”屏幕保护程序的起源故事

人们使用屏保程序来防止 CRT 显示器"烧毁",因为静态图像会永久损坏屏幕。像 3D Pipes 这样的屏保程序能在显示器处于非活动状态时为其提供动画效果,从而保护屏幕并延长其使用寿命。此外,它们还能在用户不使用电脑时为其提供可定制的…

盘点有趣的人工智能开源项目一

字幕导出 zh_recogn是一个专注于中文语音识别的字幕生成工具,基于魔塔社区Paraformer模型。它不仅支持音频文件,还能处理视频文件,输出标准的SRT字幕格式。这个项目提供了API接口和简单的用户界面,使得用户可以根据自己的需求灵活…

值得推荐的品牌维权控价方法

数据调查 全面了解线上各渠道(如淘宝、天猫、拼多多、京东、抖音、快手等)的低价情况,包括哪些是授权店低价、窜货或假货,为后续针对性治理提供依据。人工排查适用于链接不多的情况,链接数量庞大时利用系统监测更高效…

睿烨蜘蛛池福建官网下载

baidu搜索:如何联系八爪鱼SEO? baidu搜索:如何联系八爪鱼SEO? baidu搜索:如何联系八爪鱼SEO? 现在做站群程序的时候,由于百度、搜狗蜘蛛越来越少了,所以缓存也跟着减少,很多人都降低了服务器的配置,这个时候google蜘蛛却疯狂涌入,烦不胜烦…

Pulsar 社区周报 | No.2024-06-14 | 增强 Pulsar Broker 级别的监控指标

“ 各位热爱 Pulsar 的小伙伴们,Pulsar 社区周报更新啦!这里将记录 Pulsar 社区每周的重要更新,每周发布。 ” 本期主题:增强 Pulsar Broker 级别的监控指标 在 Pulsar 的当前度量指标框架中, pulsar_out_bytes_total …

AbMole带你探索细胞的“铁”门:Piezo1通道在椎间盘退变中的关键角色

在生物医学领域,铁是细胞功能不可或缺的元素,但铁的异常积累却可能成为细胞的“隐形杀手”。最近,一项发表在《Bone Research》上的研究,为我们揭开了铁代谢与椎间盘退变之间神秘联系的一角。这项研究不仅深化了我们对铁离子通道P…

[机器学习] Stable Diffusion初体验——基于深度学习通过神经网络的强大AI平台

文章目录 前言平台介绍 一.创建应用 Stable Diffusion WebUI初始化上传模型,VAE,lora 介绍sd模型,vae,lora模型进入应用文生图工作区调参区图生图 结语 前言 在这个信息爆炸的时代,AI技术正以前所未有的速度发展着。图…

Appium+python自动化(九)- 定位元素工具(义结金兰的两位异性兄弟)(超详解) 密码保护

宏哥微信粉丝群:https://bbs.csdn.net/topics/618423372 有兴趣的可以扫码加入 简介 环境搭建好了,其他方面的知识也准备的差不多了,那么我们就开始下一步元素定位,元素定位宏哥主要介绍如何使用uiautomatorviewer,通…

python-不定方程求解

[题目描述] 给定正整数 a,b,c。求不定方程axbyc 关于未知数 x 和 y 的所有非负整数解组数。输入: 一行,包含三个正整数 a,b,c,两个整数之间用单个空格隔开。每个数均不大于 1000。输出&#xff…

Python武器库开发-武器库篇之Redis未授权漏洞扫描器(五十七)

Python武器库开发-武器库篇之Redis未授权漏洞扫描器(五十七) Redis未授权访问漏洞简介以及危害 Redis是一个开源的内存数据库,具有高性能和可扩展性。然而,由于配置不当或者默认设置,Redis服务器可能会存在未授权访问的漏洞。 未授权访问漏…

股票交易系统

效果展示,如下动图: 首先简述一下股票交易规则: 买卖股票,股民可以自行选择股票的买入或卖出价格和股票的数量,但是用户不一定马上就交易成功,只有当股票价格低于买入价才有机会买入,高于卖出价…

用户运营(1):从“麦肯锡三层面法”看怎么定“用户运营策略”

麦肯锡三层面法是源自麦肯锡公司提出的一种战略规划框架,它基于对全球不同行业高速增长公司的研究,为企业提供了一个系统化的方法,可以让企业用来平衡短期业绩、中期增长机会与长期潜力的开发,确保企业持续增长与适应市场变化。以…

springcloud第4季 分布式事务seata作用服务搭建

一 seata作用 1.1 作用 二 seata服务端搭建 2.1 seata搭建 2.2.1 seata 服务端下载安装 下载地址: Seata-Server下载 | Apache Seata 截图如下: 2.2.2 使用mysql初始化seata所需表 1.下载脚本地址:incubator-seata/script/server/db/…

分组检测常用算法

目录 4.分组检测常用算法4.1 接收信号能量检测4.2 双滑动窗口分组检测4.3 采用前导结构进行分组检测 总结 微信公众号获取更多FPGA相关源码: 4.分组检测常用算法 常用的分组检测算法包括能量检测算法、双滑动窗口能量检测算法、以及利用训练序列的同步算法等。 …

MySQL8新特性实现无限层级依赖SQL查询

前言 看IT老齐视频,学到了一招MySQL8的新特性,特此记录一下,大家可以去看原视频: 【IT老齐173】学到就是赚到,利用MySQL8新特性实现无限层级依赖SQL查询 准备 MySQL至少需要8以上的版本哦! 1.创建表 C…

石墨消解仪 石墨炉加热 热传导率高 平均温差小

GS系列石墨消解仪是一款专为实验室加热设计制造的加热装置,可用于样品加热、培养、烘干。采用国际先进技术,具有消解快速、高效、节能、方便等优点,采用数字电路PID方式控制温度,更加准确。高纯优质石墨加热载体,表面喷…