Transformer简述和实现

Transformer

1、概述

(一)、诞生

自从2017年此文《Attention is All You Need》提出来Transformer后,便开启了大规模预训练的新时代,也在历史的长河中一举催生出了GPT、BERT这样的里程碑模型。

(二)、优势

相比之前占领市场的LSTM和GRU模型,Transformer有两个显著的优势:

  • Transformer能够利用分布式GPU进行并行训练,提升模型训练效率.
  • 在分析预测更长的文本时, 捕捉间隔较长的语义关联效果更好.

2、架构

(一)、模型作用

  • 基于seq2seq架构的transformer模型可以完成NLP领域研究的典型任务, 如机器翻译, 文本生成等. 同时又可以构建预训练语言模型,用于不同任务的迁移学习.
  • 在接下来的架构分析中, 我们将假设使用Transformer模型架构处理从一种语言文本到另一种语言文本的翻译工作, 因此很多命名方式遵循NLP中的规则. 比如: Embeddding层将称作文本嵌入层, Embedding层产生的张量称为词嵌入张量, 它的最后一维将称作词向量等.

(二)、总体架构图

在这里插入图片描述

transformer总体架构:输入部分,输出部分,编码器部分,解码器部分

3、输入部分

在这里插入图片描述

(一)、文本嵌入式作用

无论是源文本嵌入还是目标文本嵌入,都是为了将文本中词汇的数字表示转变为向量表示, 希望在这样的高维空间捕捉词汇间的关系

# 导入必备的工具包
import torch
# 预定义的网络层torch.nn, 工具开发者已经帮助我们开发好的一些常用层, 
# 比如,卷积层, lstm层, embedding层等, 不需要我们再重新造轮子.
import torch.nn as nn
# 数学计算工具包
import math
# torch中变量封装函数Variable.
from torch.autograd import Variable

# Embeddings类 实现思路分析
# 1 init函数 (self, d_model, vocab)
    # 设置类属性 定义词嵌入层 self.lut层
# 2 forward(x)函数
    # self.lut(x) * math.sqrt(self.d_model)
# 词嵌入类
class Embedding(nn.Module):
    def __init__(self, d_model, vocab_size):
        '''
        初始化embedding编码模型和词嵌入维度
        :param d_model: 词嵌入维度
        :param vocab_size: 词表大小
        '''
        super().__init__()
        # 初始化词嵌入对象
        self.lcut = nn.Embedding(vocab_size, d_model)
        # 初始化词嵌入维度
        self.d_model = d_model

    def forward(self, x):
        '''
        前向传播,对词表进行编码和放大
        :param x: 输入的原始信息
        :return: 放大后的编码
        '''
        # 对token进行编码并放大
        return self.lcut(x) * math.sqrt(self.d_model)

(二)、位置编码器的作用

因为在Transformer的编码器结构中, 所有输入的词语并行训练,并没有针对词汇位置信息的处理,因此需要在Embedding层后加入位置编码器,将词汇位置不同可能会产生不同语义的信息加入到词嵌入张量中, 以弥补位置信息的缺失。

为什么选择使用sin和cos来作为位置编码?

  • 绝对位置编码(0-512):冲淡文本嵌入信息,模型外推性差,当超出设置长度,无法表示位置。(ROPE alibi)
  • 对绝对位置编码进行归一化:512/512=1 511/512约=1,相邻token位置信息区别不大
  • 为什么不单独用一个三角函数:三角函数的周期,周期大,相邻token位置信息区别不大,周期小,间隔的token可能位置信息相似甚至相等
# 位置编码器类PositionalEncoding 实现思路分析
# 1 init函数  (self, d_model, dropout, max_len=5000)
#   super()函数 定义层self.dropout
#   定义位置编码矩阵pe  定义位置列-矩阵position 定义变化矩阵div_term
#   套公式div_term = torch.exp(torch.arange(0, d_model, 2) * -(math.log(10000.0)/d_model))
#   位置列-矩阵 * 变化矩阵 哈达码积my_matmulres
#   给pe矩阵偶数列奇数列赋值 pe[:, 0::2] pe[:, 1::2]
#   pe矩阵注册到模型缓冲区 pe.unsqueeze(0)三维 self.register_buffer('pe', pe)
# 2 forward(self, x) 返回self.dropout(x)
#   给x数据添加位置特征信息 x = x + Variable( self.pe[:,:x.size()[1]], requires_grad=False)

class PositionalEncoding(nn.Module):
    def __init__(self, d_model, dropout, max_len=5000):
        # 参数d_model 词嵌入维度 eg: 512个特征
        # 参数max_len 单词token个数 eg: 60个单词
        super(PositionalEncoding, self).__init__()

        # 定义dropout层
        self.dropout = nn.Dropout(p=dropout)

        # 思路:位置编码矩阵 + 特征矩阵 相当于给特征增加了位置信息
        # 定义位置编码矩阵PE eg pe[60, 512], 位置编码矩阵和特征矩阵形状是一样的
        pe = torch.zeros(max_len, d_model)

        # 定义位置列-矩阵position  数据形状[max_len,1] eg: [0,1,2,3,4...60]^T
        position = torch.arange(0, max_len).unsqueeze(1)# 保存绝对位置信息
        # print('position--->', position.shape, position)

        # 定义变化矩阵div_term [1,256]
        # torch.arange(start=1, end=512, 2)结果并不包含end。在start和end之间做一个等差数组 [0, 2, 4, 6 ... 510]
        div_term = torch.exp(torch.arange(0, d_model, 2) * -(math.log(10000.0) / d_model)) 

        # 位置列-矩阵 @ 变化矩阵 做矩阵运算 [60*1]@ [1*256] ==> 60 *256
        # 矩阵相乘也就是行列对应位置相乘再相加,其含义,给每一个列属性(列特征)增加位置编码信息
        my_matmulres = position * div_term
        # print('my_matmulres--->', my_matmulres.shape, my_matmulres)

        # 给位置编码矩阵奇数列,赋值sin曲线特征
        pe[:, 0::2] = torch.sin(my_matmulres)
        # 给位置编码矩阵偶数列,赋值cos曲线特征
        pe[:, 1::2] = torch.cos(my_matmulres)

        # 形状变化 [60,512]-->[1,60,512]
        pe = pe.unsqueeze(0)

        # 把pe位置编码矩阵 注册成模型的持久缓冲区buffer; 模型保存再加载时,可以根模型参数一样,一同被加载
        # 什么是buffer: 对模型效果有帮助的,但是却不是模型结构中超参数或者参数,不参与模型训练
        self.register_buffer('pe', pe)

    def forward(self, x):
        # 注意:输入的x形状2*4*512  pe是1*60*512 形状 如何进行相加
        # 只需按照x的单词个数 给特征增加位置信息
        x = x + Variable( self.pe[:,:x.size()[1]], requires_grad=False)
        return self.dropout(x)

4、编码器部分*

在这里插入图片描述

由N个编码器层堆叠而成

每个编码器层由两个子层连接结构组成

第一个子层连接结构包括一个多头自注意力子层和规范化层以及一个残差连接

第二个子层连接结构包括一个前馈全连接子层和规范化层以及一个残差连接

(一)、注意力机制

(1)、注意力计算

A t t e n t i o n ( Q , K , V ) = S o f t m a x ( Q ⋅ K T d k ⋅ V ) Attention(Q,K,V)=Softmax(\frac{Q·K^T}{\sqrt{d_k}}·V) Attention(Q,K,V)=Softmax(dk QKTV)

为什么除以根号下dk?

假设:输入x (2,4,512),分别经过三个线性层,Q、K、V (2,4,512)—> Q@K^T (2,4,4)
词嵌入维度 dk=512
前提假设 Q、K 满足标准正态分布,(均值)期望0,方差1, E(QK)=0, Var(QK)=dk, 把方差dk拉回到1,除以标准差(根号下dk)
可以解决:

  • 内部协变量偏移问题 输入输出最好是同分布的,好处是: 需要的data更少,训练时间更短
  • f = softmax(xi),f‘ = f(1-f)(i=j);f’ = -fi*fj (j!=i),dk越大,方差越大,数据越分散,大的很大,小的很小,再经过softmax,大的变成1,小的变成了0,类似onehot编码形式,把这些值带入softmax导数公式,梯度接近为0,梯度消失问题,模型可训练性非常差
(2)、实现
# 实现单头attn
def attention(query, key, value, dropout=None, mask=None):
    '''
    该函数实现单头注意力机制计算
    :param query:Q
    :param key:K
    :param value:V
    :param dropout:随机失活函数对象
    :param mask:掩码矩阵padding mask(遮蔽填充的0);subsquent mask(解码并行)
    :return:处理后的数据,权重概率矩阵
    '''
    # 拿到d_k,是一个头词嵌入的维度
    d_k = query.size(-1)
    # 先将Q·K^T,再除以sqrt(d_k)
    scores = torch.matmul(query, key.transpose(-2, -1)) / math.sqrt(d_k)
    # 使用掩码矩阵对原始信息进行遮掩。masked_fill->将目标数字替换为第二个参数。
    # 此处替换是因为如果是原始0,在softmax转换后分子为1,替换为一个负值后,分子e^i极小,几乎不对结果概率产生影响
    if mask is not None:
        scores = scores.masked_fill(mask == 0, -1e9)
    # 将上述的结果经过softmax得到权重概率矩阵
    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

(二)、多头注意力机制

(1)、作用

多头注意力机制的理解:1、代码;2、原理
a、注意力计算公式,(2,4,512)—》(2,4,8,64)—》(2,8,4,64)
b、512,划分成了8个细分子空间,每个子空间就是一个观察角度,特征抽取会更丰富更全面,盲人摸象
c、降低了计算复杂度,4*512*4*512,变成了 8 个 4*64*4*64

(2)、实现
# 克隆函数
def clones(module, N):
    '''
    复制N个目标对象module
    :param module: 目标module
    :param N: 复制N个
    :return: 返回一个内部有N个module对象列表
    '''
    # 对目标对象进行深拷贝,拷贝N份,再将放到到一个列表中输出
    return nn.ModuleList([copy.deepcopy(module) for _ in range(N)])


# 多头注意力机制类
class MutiHeadAttn(nn.Module):
    def __init__(self, head, embedding_dim, dropout=0.1):
        '''
        初始化多头注意力机制参数
        :param head: 多少头
        :param embedding_dim: 词向量长度
        :param dropout: 随机失活比例
        '''
        super().__init__()
        # 因为多头必须均分词向量,所以不能整除就不继续往下走
        assert embedding_dim % head == 0
        # 计算每个头的d_k
        self.d_k = embedding_dim // head
        # 复制头数
        self.head = head
        # 初始化注意力
        self.attn = None
        # 初始化随机失活对象
        self.dropout = nn.Dropout(dropout)
        # 初始化4个线性层,计算QKV使用3个,最后的x需要再经过1个
        self.linears = clones(nn.Linear(embedding_dim, embedding_dim), 4)

    def forward(self, query, key, value, mask=None):
        '''
        实现多头注意力机制,通过输入的QKV控制是自注意力机制还是一般注意力机制
        :param query: Q
        :param key: K
        :param value: V
        :param mask: 掩码矩阵
        :return: 返回多头处理后的x
        '''
        # 对掩码矩阵进行升维,为了和多头注意力矩阵处理
        if mask is not None:
            mask = mask.unsqueeze(0)
        # 获得batch_size
        batch_size = query.size(0)
        '''
        q,k,v 三者的维度都是 (batch_size, head, seq_len, d_k)  d_k就是分为多个头之后的维度 64
        transpose(1, 2) 是为了让 seq-len(代表的token)与 d_k 相邻 才有意义,同时方便计算
        此处的-1是为了动态适应句子长度,因为句子长度都不一样,用-1自适应更加灵活
        '''
        query, key, value = [model(x).view(batch_size, -1, self.head, self.d_k).transpose(1, 2)
                             for model, x in zip(self.linears, (query, key, value))]
        # 利用单头注意力函数计算处理后的数据
        x, self.attn = attention(query, key, value, mask=mask, dropout=self.dropout)
        # 先(batch_size, head, seq_len, d_k)-》(batch_size, seq_len, head, d_k)
        # 将处理后的内存合并,再将数据降维回三维
        x = x.transpose(1, 2).contiguous().view(batch_size, -1, self.head * self.d_k)
        # 使用第4个线性层处理原始数据
        return self.linears[-1](x)
(3)、小结
  • 多头注意力机制:
    • 每个头开始从词义层面分割输出的张量,也就是每个头都想获得一组Q,K,V进行注意力机制的计算,但是句子中的每个词的表示只获得一部分,也就是只分割了最后一维的词嵌入向量. 这就是所谓的多头.将每个头的获得的输入送到注意力机制中, 就形成了多头注意力机制.
  • 多头注意力机制的作用:
    • 这种结构设计能让每个注意力机制去优化每个词汇的不同特征部分,从而均衡同一种注意力机制可能产生的偏差,让词义拥有来自更多元的表达,实验表明可以从而提升模型效果.
  • 实现多头注意力机制的类: MultiHeadedAttention
    • 因为多头注意力机制中需要使用多个相同的线性层, 首先实现了克隆函数clones.
    • clones函数的输入是module,N,分别代表克隆的目标层,和克隆个数.
    • clones函数的输出是装有N个克隆层的Module列表.
    • 接着实现MultiHeadedAttention类, 它的初始化函数输入是h, d_model, dropout分别代表头数,词嵌入维度和置零比率.
    • 它的实例化对象输入是Q, K, V以及掩码张量mask.
    • 它的实例化对象输出是通过多头注意力机制处理的Q的注意力表示

(三)、前馈全连接层

(1)、作用

作用:考虑注意力机制可能对复杂过程的拟合程度不够, 通过增加两层网络来增强模型的能力

(2)、实现
# feed_forward前馈全连接层
class PositionwiseFeedForward(nn.Module):
    def __init__(self, d_model, d_ff, dropout=0.1):
        '''
        增加两层线性层来增加模型的拟合能力
        :param d_model: 词嵌入维度
        :param d_ff: 中间过度维度
        :param dropout: 随机失活率
        '''
        super().__init__()
        # 第一层线性层,将词向量维度转为中间过渡维度
        self.w1 = nn.Linear(d_model, d_ff)
        # 第二层线性层,将中间过度维度转为词向量维度
        self.w2 = nn.Linear(d_ff, d_model)
        # 随机失活率
        self.dropout = nn.Dropout(dropout)

    def forward(self, x):
        '''
        将数据经过两层线性层、relu激活函数和随机失活
        :param x: 上一层输入的数据
        :return: 经过线性层处理的数据
        '''
        # 线性层-》relu激活-》随机失活-》线性层
        return self.w2(self.dropout(F.relu(self.w1(x))))
(3)、小结
  • 前馈全连接层:
    • 在Transformer中前馈全连接层就是具有两层线性层的全连接网络.
  • 前馈全连接层的作用:
    • 考虑注意力机制可能对复杂过程的拟合程度不够, 通过增加两层网络来增强模型的能力.
  • 实现前馈全连接层的类: PositionwiseFeedForward
    • 它的实例化参数为d_model, d_ff, dropout, 分别代表词嵌入维度, 线性变换维度, 和置零比率.
    • 它的输入参数x, 表示上层的输出.
    • 它的输出是经过2层线性网络变换的特征表示

(四)、规划化层

(1)、作用

它是所有深层网络模型都需要的标准网络层,因为随着网络层数的增加,通过多层的计算后参数可能开始出现过大或过小的情况,这样可能会导致学习过程出现异常,模型可能收敛非常的慢. 因此都会在一定层数后接规范化层进行数值的规范化,使其特征数值在合理范围内.

norm的作用:

1、解决内部协变量偏移问题;

2、把数据分布拉回到激活函数的非饱和区

norm会不会丢失网络学到的参数:不会。

1、norm改变的是数据的空间分布,不改变数据内部的相对大小

2、增加了两个可学习参数:缩放系数、偏移系数,分别决定方差、期望,让模型自行决定缩放、偏移多少

(2)、实现
# 规范化层
class LayerNorm(nn.Module):
    def __init__(self, features, esp=1e-6):
        '''
        初始化规划化层的数据
        :param features: 词向量维度
        :param esp: 很小的值,防止在计算时数据违法
        '''
        super().__init__()
        # 词向量维度
        self.size = features
        # 将a2定义为一个随着模型更新的参数
        self.a2 = nn.Parameter(torch.ones(features))
        # 将b2定义为一个随着模型更新的参数
        self.b2 = nn.Parameter(torch.zeros(features))
        # 很小的值
        self.esp = esp

    def forward(self, x):
        '''
        规范化/标准化,相当于BN层
        :param x:
        :return:
        '''
        # 对数据的最后一维做均值计算
        mean = x.mean(-1, keepdim=True)
        # 对数据做标准差计算
        std = x.std(-1, keepdim=True)
        # 用均值和标准差对数据做标准化,再用反向传播的a2和b2做学习的比例和偏移
        return self.a2 * (x - mean) / (std + self.esp) + self.b2
(3)、小结
  • 规范化层的作用:
    • 它是所有深层网络模型都需要的标准网络层,因为随着网络层数的增加,通过多层的计算后参数可能开始出现过大或过小的情况,这样可能会导致学习过程出现异常,模型可能收敛非常的慢. 因此都会在一定层数后接规范化层进行数值的规范化,使其特征数值在合理范围内.
  • 实现规范化层的类: LayerNorm
    • 它的实例化参数有两个, features和eps,分别表示词嵌入特征大小,和一个足够小的数.
    • 它的输入参数x代表来自上一层的输出.
    • 它的输出就是经过规范化的特征表示

(五)、子层连接结构

(1)、概述

输入到每个子层以及规范化层的过程中,还使用了残差链接(跳跃连接),因此我们把这一部分结构整体叫做子层连接(代表子层及其链接结构),在每个编码器层中,都有两个子层,这两个子层加上周围的链接结构就形成了两个子层连接结构

(2)、实现
# 子层连接结构 子层(前馈全连接层 或者 注意力机制层)+ norm层 + 残差连接
# SublayerConnection实现思路分析
# 1 init函数  (self, size, dropout=0.1):
   # 定义self.norm层 self.dropout层, 其中LayerNorm(size)
# 2 forward(self, x, sublayer) 返回+以后的结果
    # 数据self.norm() -> sublayer()->self.dropout() + x

# 子层连接结构
class SublayerConnection(nn.Module):
    def __init__(self, size, dropout=0.1):
        '''
        初始化归一化对象,初始化随机失活比例
        :param size: 词嵌入维度
        :param dropout: 随机失活参数
        '''
        super().__init__()
        # 初始化归一化对象
        self.norm = LayerNorm(size)
        # 初始化随机失活对象
        self.dropout = nn.Dropout(dropout)

    def forward(self, x, sublayer):
        '''
        将原始数据和
        :param x: 原始数据(刚经过位置编码)
        :param sublayer: 处理数据对象,如多头注意力层
        :return: 处理后的数据
        '''
        # 残差连接,将原始数据与处理后的数据相加
        res = x + self.dropout(sublayer(self.norm(x)))
        # 方法二
        # res = x + self.dropout(self.norm(x.subtype(x)))
        # 返回处理后的数据
        return res
(3)、小结
  • 子层连接结构:
    • 如图所示,输入到每个子层以及规范化层的过程中,还使用了残差链接(跳跃连接),因此我们把这一部分结构整体叫做子层连接(代表子层及其链接结构), 在每个编码器层中,都有两个子层,这两个子层加上周围的链接结构就形成了两个子层连接结构.
  • 实现子层连接结构的类: SublayerConnection
    • 类的初始化函数输入参数是size, dropout, 分别代表词嵌入大小和置零比率.
    • 它的实例化对象输入参数是x, sublayer, 分别代表上一层输出以及子层的函数表示.
    • 它的输出就是通过子层连接结构处理的输出.

(六)、编码器层

(1)、作用

编码器的组成单元,每个编码器层完成一次对输入特征的提取过程,即编码过程

(2)、实现
# 编码器层
class EncoderLayer(nn.Module):
    def __init__(self, size, self_attn, ffn, dropout=0.1):
        '''
        初始化各类对象
        :param size: 词嵌入维度
        :param self_attn: 注意力层对象
        :param ffn: 前馈全连接层对象
        :param dropout: 随机失活参数
        '''
        super().__init__()
        # 初始化词嵌入维度
        self.size = size
        # 初始化注意力层对象
        self.self_attn = self_attn
        # 初始化前馈全连接层对象
        self.ffn = ffn
        # 复制两个子层连接结构
        self.sublayer = clones(SublayerConnection(size), 2)

    def forward(self, x, mask=None):
        '''
        构建一个完整的编码器层
        :param x: 经过位置编码后的数据
        :param mask: 掩码矩阵
        :return: 处理后的数据
        '''
        # 计算自注意力机制后(因为qkv原始数据相等),再经过一次残差连接后输出数据
        x = self.sublayer[0](x, lambda x: self.self_attn(x, x, x, mask))
        # 计算前馈全连接层后,经过一次残差连接后输出数据
        x = self.sublayer[1](x, self.ffn)
        return x
(3)、小结
  • 编码器层的作用:
    • 作为编码器的组成单元, 每个编码器层完成一次对输入的特征提取过程, 即编码过程.
  • 实现编码器层的类: EncoderLayer
    • 类的初始化函数共有4个, 别是size,其实就是我们词嵌入维度的大小. 第二个self_attn,之后我们将传入多头自注意力子层实例化对象, 并且是自注意力机制. 第三个是feed_froward, 之后我们将传入前馈全连接层实例化对象. 最后一个是置0比率dropout.
    • 实例化对象的输入参数有2个,x代表来自上一层的输出, mask代表掩码张量.
    • 它的输出代表经过整个编码层的特征表示

(七)、编码器

(1)、作用

对于输入进行指定的特征提取过程,也称为编码,由多个编码器层堆叠构成

(2)、实现
# 编码器
class Encoder(nn.Module):
    def __init__(self, layer, N):
        '''
        复制N份编码器模型
        :param layer: 编码器对象
        :param N: 复制多少个
        '''
        super().__init__()
        # 复制N个编码器对象,并放到一个列表中
        self.layers = clones(layer, N)
        # 定义规范化对象
        self.norm = LayerNorm(layer.size)

    def forward(self, x, mask):
        '''
        构建多个编码器层构成的编码器
        :param x: 经过位置编码后的数据
        :param mask: 掩码矩阵
        :return: 经过多层编码器层处理,再经过一层规范化的数据
        '''
        # 将数据在N个编码器层中串行处理
        for layer in self.layers:
            x = layer(x, mask)
        # 将上面处理的数据进行规范化
        return self.norm(x)
(3)、小结
  • 编码器的作用:
    • 编码器用于对输入进行指定的特征提取过程, 也称为编码, 由N个编码器层堆叠而成.
  • 实现编码器的类: Encoder
    • 类的初始化函数参数有两个,分别是layer和N,代表编码器层和编码器层的个数.
    • forward函数的输入参数也有两个, 和编码器层的forward相同, x代表上一层的输出, mask代码掩码张量.
    • 编码器类的输出就是Transformer中编码器的特征提取表示, 它将成为解码器的输入的一部分.

5、解码器部分

  • 由N个解码器层堆叠而成
  • 每个解码器层由三个子层连接结构组成
  • 第一个子层连接结构包括一个多头自注意力子层和规范化层以及一个残差连接
  • 第二个子层连接结构包括一个多头注意力子层和规范化层以及一个残差连接
  • 第三个子层连接结构包括一个前馈全连接子层和规范化层以及一个残差连接

(一)、掩码机制

(1)、概述

掩代表遮掩,码就是我们张量中的数值,它的尺寸不定,里面一般只有1和0的元素,代表位置被遮掩或者不被遮掩,至于是0位置被遮掩还是1位置被遮掩可以自定义,因此它的作用就是让另外一个张量中的一些数值被遮掩,也可以说被替换, 它的表现形式是一个张量

(2)、作用

在transformer中, 掩码张量的主要作用在应用attention(将在下一小节讲解)时,有一些生成的attention张量中的值计算有可能已知了未来信息而得到的,未来信息被看到是因为训练时会把整个输出结果都一次性进行Embedding,但是理论上解码器的的输出却不是一次就能产生最终结果的,而是一次次通过上一次结果综合得出的,因此,未来的信息可能被提前利用. 所以,我们会进行遮掩

(3)、实现
# 下三角矩阵作用: 生成字符时,希望模型不要使用当前字符和后面的字符。
    # 使用遮掩mask,防止未来的信息可能被提前利用
    # 实现方法: 1 - 上三角矩阵
# 函数 subsequent_mask 实现分析
# 产生上三角矩阵 np.triu(m=np.ones((1, size, size)), k=1).astype('uint8')
# 返回下三角矩阵 torch.from_numpy(1 - my_mask )
# 实现掩码张量
def subsquent_mask(seq_len):
    '''
    构造掩码矩阵,目的是在解码器预测时掩盖未来信息
    :param seq_len: 句子长,因为掩码步骤在Q*K^T后,所以矩阵大小就是句子长
    :return: 返回目标掩码矩阵
    '''
    # 定义掩码矩阵大小,1是因为可以广播,seq_len是Q*K^T后,矩阵大小就是句子长
    attn_shape = (1, seq_len, seq_len)
    # triu函数是保留右上半区
    # subsquent_mask = np.triu(np.ones(attn_shape), k=1).astype('uint8')
    # return torch.from_numpy(1 - subsquent_mask)
    # tril函数是保留左下半区
    subsquent_mask = np.tril(np.ones(attn_shape), k=0).astype('uint8')
    return torch.from_numpy(subsquent_mask)
(4)、小结
  • 掩码张量:
    • 掩代表遮掩,码就是我们张量中的数值,它的尺寸不定,里面一般只有1和0的元素,代表位置被遮掩或者不被遮掩,至于是0位置被遮掩还是1位置被遮掩可以自定义,因此它的作用就是让另外一个张量中的一些数值被遮掩, 也可以说被替换, 它的表现形式是一个张量.
  • 掩码张量的作用:
    • 在transformer中, 掩码张量的主要作用在应用attention(将在下一小节讲解)时,有一些生成的attetion张量中的值计算有可能已知量未来信息而得到的,未来信息被看到是因为训练时会把整个输出结果都一次性进行Embedding,但是理论上解码器的的输出却不是一次就能产生最终结果的,而是一次次通过上一次结果综合得出的,因此,未来的信息可能被提前利用. 所以,我们会进行遮掩. 关于解码器的有关知识将在后面的章节中讲解.
  • 实现生成向后遮掩的掩码张量函数: subsequent_mask
    • 它的输入是size, 代表掩码张量的大小.
    • 它的输出是一个最后两维形成1方阵的下三角阵.
    • 最后对生成的掩码张量进行了可视化分析, 更深一步理解了它的用途.

(二)、解码器层

(1)、作用

解码器的组成单元,每个解码器层根据给定的输入向目标进行特征提取操作,即解码过程。

(2)、实现
# 解码器层
class DecoderLayer(nn.Module):
    def __init__(self, size, self_attn, src_attn, ffn, dropout=0.1):
        '''
        构建出一个完整的解码器层所需的数据和类
        :param size: 词向量维度
        :param self_attn: 自注意力机制对象
        :param src_attn: 一般注意力机制对象
        :param ffn: 前馈全连接层对象
        :param dropout: 随机失活率
        '''
        super().__init__()
        # 初始化词向量维度
        self.size = size
        # 自注意力机制对象
        self.self_attn = self_attn
        # 一般注意力机制对象
        self.src_attn = src_attn
        # 前馈全连接层对象
        self.ffn = ffn
        # 克隆三份自连接结构层
        self.sublayer = clones(SublayerConnection(size), 3)

    def forward(self, x, memory, source_mask, target_mask):
        '''
        构建出一个完整的解码器层
        :param x: 目标语言经过位置编码后的数据
        :param memory: 中间语义张量c
        :param source_mask: 自有语言的掩码矩阵
        :param target_mask: 目标语言的掩码矩阵
        :return: 解码数据
        '''
        # 中间语义张量,也就是编码器最后输出的值
        m = memory
        # 输入目标语言带位置编码的数据,经过自注意力层,规范化层,残差连接
        x = self.sublayer[0](x, lambda x: self.self_attn(x, x, x, target_mask))
        # 将上面输出的数据作为Q输入,KV为编码器输出,计算一般注意力权重,在过规范化层,残差连接
        x = self.sublayer[1](x, lambda x: self.src_attn(x, m, m, source_mask))
        # 计算前馈全连接层后,经过一次残差连接后输出数据
        x = self.sublayer[2](x, self.ffn)
        return x
(3)、小结
  • 解码器层的作用:
    • 作为解码器的组成单元, 每个解码器层根据给定的输入向目标方向进行特征提取操作,即解码过程.
  • 实现解码器层的类: DecoderLayer
    • 类的初始化函数的参数有5个, 分别是size,代表词嵌入的维度大小, 同时也代表解码器层的尺寸,第二个是self_attn,多头自注意力对象,也就是说这个注意力机制需要Q=K=V,第三个是src_attn,多头注意力对象,这里Q!=K=V, 第四个是前馈全连接层对象,最后就是droupout置0比率.
    • forward函数的参数有4个,分别是来自上一层的输入x,来自编码器层的语义存储变量mermory, 以及源数据掩码张量和目标数据掩码张量.
    • 最终输出了由编码器输入和目标数据一同作用的特征提取结果.

(三)、解码器

(1)、作用

编码器的结果以及上一次预测的结果, 对下一次可能出现的’值’进行特征表示

(2)、实现
# 解码器
class Decoder(nn.Module):
    def __init__(self, layer, N):
        '''
        初始化出N个解码器层
        :param layer: 解码器层对象
        :param N: 需要几个解码器层
        '''
        super().__init__()
        # 复制出N个解码器层对象
        self.layers = clones(layer, N)
        # 规范化层对象
        self.norm = LayerNorm(layer.size)

    def forward(self, x, memory, source_mask, target_mask):
        '''
        构建多个解码器层构成的解码器
        :param x: 经过位置编码后的数据
        :param memory: 中间语义张量c,也就是编码器的输出
        :param source_mask: 源语言的掩码矩阵
        :param target_mask: 目标语言的掩码矩阵
        :return: 规划化后的预测矩阵
        '''
        # 经过N个解码器层,对数据进行解码
        for layer in self.layers:
            x = layer(x, memory, source_mask, target_mask)
        # 对数据进行规范化
        return self.norm(x)
(3)、小结
  • 解码器的作用:
    • 根据编码器的结果以及上一次预测的结果, 对下一次可能出现的’值’进行特征表示.
  • 实现解码器的类: Decoder
    • 类的初始化函数的参数有两个,第一个就是解码器层layer,第二个是解码器层的个数N.
    • forward函数中的参数有4个,x代表目标数据的嵌入表示,memory是编码器层的输出,src_mask, tgt_mask代表源数据和目标数据的掩码张量.
    • 输出解码过程的最终特征表示

6、输出部分

  • 线性层
  • softmax层
# 输出部分
class Generator(nn.Module):
    def __init__(self, d_model, vocab_size):
        '''
        初始化线性层
        :param d_model: 输入词嵌入维度
        :param vocab_size: 输出词嵌入维度
        '''
        super().__init__()
        # 初始化线性层
        self.project = nn.Linear(d_model, vocab_size)

    def forward(self, x):
        '''
        输出目标数据
        :param x: 编码器的输出数据
        :return: 最终输出数据
        '''
        # 将编码器输出的数据经过线性层后,对最后一维做softmax
        return F.log_softmax(self.project(x), dim=-1)

实现线性层和softmax层的类: Generator

  • 初始化函数的输入参数有两个, d_model代表词嵌入维度, vocab_size代表词表大小.
  • forward函数接受上一层的输出.
  • 最终获得经过线性层和softmax层处理的结果.

7、模型构建

(一)、实现编码器和解码器

# 编码器-解码器联合
class EncoderDecoder(nn.Module):
    def __init__(self, encoder, decoder, source_embed, target_embed, generator):
        '''
        初始化解码器、编码器、源输入、目标输入、输出对象
        :param encoder: 编码器对象
        :param decoder: 解码器对象
        :param source_embed: 源输入对象
        :param target_embed: 目标输入对象
        :param generator: 输出对象
        '''
        super().__init__()
        # 初始化编码器对象
        self.encoder = encoder
        # 初始化解码器对象
        self.decoder = decoder
        # 初始化源输入对象(包含位置编码)
        self.src_embed = source_embed
        # 初始化目标输入对象(包含位置编码)
        self.tgt_embed = target_embed
        # 初始化输出对象
        self.generator = generator

    def forward(self, source, target, source_mask, target_mask):
        '''
        将源数据和目标数据输入后解码
        :param source: 源数据
        :param target: 目标数据
        :param source_mask: 源数据掩码矩阵
        :param target_mask: 目标数据掩码矩阵
        :return: 解码后的数据
        '''
        # 编码器的输出作为解码器的QK,其他的和解码器一样,再将解码器输出给输出层,最终返回最终结果
        return self.generator(self.decode(self.encode(source, source_mask), source_mask, target, target_mask))

    def encode(self, source, source_mask):
        '''
        构造编码器
        :param source: 源数据
        :param source_mask: 源数据掩码矩阵
        :return: 编码后的数据
        '''
        # 先将原数据进行编码(包括位置编码和掩码),再进行编码
        return self.encoder(self.src_embed(source), source_mask)

    def decode(self, memory, source_mask, target, target_mask):
        '''
        构造解码器
        :param memory: 中间语义张量c,解码器的输出
        :param source_mask: 源数据掩码矩阵
        :param target: 目标数据
        :param target_mask: 目标数据掩码矩阵
        :return: 解码后的数据
        '''
        # 先将目标数据进行编码(包括位置编码和掩码),再进行解码
        return self.decoder(self.tgt_embed(target), memory, source_mask, target_mask)

(二)、实现组件模型

# 组建模型
def make_model(source_vocab, target_vocab, d_model=512, d_ff=2048, head=8, dropout=0.1, N=6):
    '''
    组建完整的transformer
    :param source_vocab: 原数据
    :param target_vocab: 目标数据
    :param d_model: 词嵌入维度
    :param d_ff: 中间过度维度
    :param head: 多头数量
    :param dropout: 失活比率
    :param N: 组成编码器和解码器的编码器层和解码器层个数
    :return: 模型
    '''
    # 深拷贝对象
    c = copy.deepcopy
    # 定义位置编码对象
    position = PositionalEncoding(d_model)
    # 定义多头注意力对象
    attn = MutiHeadAttn(head, d_model)
    # 定义前馈层对象
    ff = PositionwiseFeedForward(d_model, d_ff, dropout)
    # 定义解码编码器对象
    model = EncoderDecoder(
        # 输入编码器对象,由N个编码器层对象构成
        Encoder(EncoderLayer(d_model, c(attn), c(ff), dropout), N),
        # 输入解码器对象,由N个解码器层对象构成
        Decoder(DecoderLayer(d_model, c(attn), c(ff), dropout), N),
        # 源数据enbedding对象和位置编码对象
        nn.Sequential(Embedding(d_model, source_vocab), c(position)),
        # 目标数据enbedding对象和位置编码对象
        nn.Sequential(Embedding(d_model, target_vocab), c(position)),
        # 输出对象
        Generator(d_model, target_vocab)
    )
    # 遍历model中所有的迭代器,只要维度大于1的就进行xavier_uniform初始化,后面的_表示在原地址修改
    for p in model.parameters():
        if p.dim() > 1:
            nn.init.xavier_uniform_(p)
    # 返回模型
    return model

8、执行

if __name__ == '__main__':
    # 词嵌入维度
    d_model = 512
    # 中间过度维度
    d_ff = 1024
    # 源词表维度
    source_vocab = 1000
    # 目标词表维度
    target_vocab = 1000
    # 解码后词嵌入维度
    vocab_size = 5000
    # 多头注意力头数
    head = 8
    # 编码器/解码器内部编码器层/解码器层个数
    N = 6
    # 深拷贝对象
    c = copy.deepcopy
    # 多头一般注意力和多头自注意力对象
    src_attn = self_attn = MutiHeadAttn(head, d_model)
    # 前馈全连接层对象
    ffn = PositionwiseFeedForward(d_model, d_ff)
    # 编码器层对象
    el = EncoderLayer(d_model, c(self_attn), c(ffn))
    # 解码器层对象
    dl = DecoderLayer(d_model, self_attn, src_attn, ffn)
    # 编码器对象
    encoder = Encoder(el, N)
    # 解码器对象
    decoder = Decoder(dl, N)
    # 源数据词嵌入对象
    source_embed = nn.Embedding(source_vocab, d_model)
    # 目标数据词嵌入对象
    target_embed = nn.Embedding(target_vocab, d_model)
    # 输出对象
    gen = Generator(d_model, vocab_size)
    # 源数据和目标数据掩码矩阵
    source_mask = target_mask = subsquent_mask(4)
    # 源数据和目标数据
    source = target = torch.LongTensor([[1, 2, 3, 4], [6, 7, 8, 9]])  # (2,4)
    # 定义解码编码器对象
    ed = EncoderDecoder(encoder, decoder, source_embed, target_embed, gen)
    # 调用解码编码器对象
    ed_res = ed(source, target, source_mask, target_mask)
    print(ed_res)
    print(ed_res.shape)

    # 构建模型
    # model = make_model(source_vocab, target_vocab)
    # print(model)

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

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

相关文章

Astro 5.0 发布

Astro 5.0 发布! 使用 Astro Content Layer 可以从任何来源加载内容,并使用 Server Islands 将缓存的静态内容与动态个性化内容结合起来。 什么是 Astro Astro 是用于构建内容驱动网站(包括博客、营销和电子商务)的 Web 框架。…

数据结构与算法-03链表-04

链表与递归 在链表操作中移除、反转经常会用到递归实现。通过力扣案例理解链表常规操作中的递归实现。 移除数据 删除链表的节点 问题 LCR 136. 删除链表的节点 - 力扣(LeetCode) 问题描述 给定单向链表的头指针和一个要删除的节点的值,定…

Let up bring up a linux.part2 [十一]

之前的篇幅中我们已经将 Linux 内核 bringup 起来了,不知道大家有没有去尝试将根文件系统运行起来,今天我就带领大家完成这个事情,可以跟着下面的步骤一步步来完成: 在这里我们使用 busybox 构建 rootfs: 下载 busyb…

WEB开发: Node.js路由之由浅入深(一) - 全栈工程师入门

作为一个使用Node.js多年的开发者,我已经习惯于用Node.js写一些web应用来为工作服务,因为实现快速、部署简单、自定义强。今天我们一起来学习一个全栈工程师必备技能:web路由。(观看此文的前提是默认你已经装好nonde.js了&#xf…

新书速览|循序渐进Node.js企业级开发实践

《循序渐进Node.js企业级开发实践》 1 本书内容 《循序渐进Node.js企业级开发实践》结合作者多年一线开发实践,系统地介绍了Node.js技术栈及其在企业级开发中的应用。全书共分5部分,第1部分基础知识(第1~3章)&#xf…

二代证信息读写器安卓身份证手持终端pda

HT530是一款可满足不同应用需求的多功能身份证核验手持机。Android 10操作系统,搭载高性能8核心2.0G主频处理器,5.5寸高清大屏,1300万摄像头;内存2G16G,4G64G可选。条码扫描(扫描头可选)、可离线采集、读取…

Redis的高可用之哨兵模式

Redis哨兵主要是解决Redis主从同步时主数据库宕机问题,使其能够自动进行故障恢复,提高Redis系统的高可用性。 1. 哨兵的作用: 监控:哨兵通过心跳机制监控主库和从库的存活性。 选主:当主库宕机时,哨兵会选举出一个领…

2024最新版python+pycharm安装与配置(mac和window都有讲)

PS:这篇是对于初学者的pythonpycharm配置教程 ,配置完成后可以直接看我的python学习笔记来进行python全套学习目前正在持续更新。 目录 python以及pycharm的安装配置一、下载安装Python1、python环境检查2、系统环境检查3、python下载4、开始安装5、检查…

【css】基础(二)

本专栏内容为:前端专栏 记录学习前端,分为若干个子专栏,html js css vue等 💓博主csdn个人主页:小小unicorn ⏩专栏分类:css专栏 🚚代码仓库:小小unicorn的代码仓库🚚 &a…

OceanBase 的探索与实践

作者:来自 vivo 互联网数据库团队- Xu Shaohui 本文总结了目前我们遇到的痛点问题并通过 OceanBase 的技术方案解决了这些痛点问题,完整的描述了 OceanBase 的实施落地,通过迁移到 OceanBase 实践案例中遇到的问题与解决方案让大家能更好的了…

【开源免费】基于Vue和SpringBoot的服装生产管理系统(附论文)

博主说明:本文项目编号 T 066 ,文末自助获取源码 \color{red}{T066,文末自助获取源码} T066,文末自助获取源码 目录 一、系统介绍二、演示录屏三、启动教程四、功能截图五、文案资料5.1 选题背景5.2 国内外研究现状5.3 可行性分析…

租赁小程序的优势与应用场景解析

内容概要 租赁小程序,听起来是不是很酷?其实,它就是一个让你可以方便地租借各种高成本但用得不频繁的商品的平台。想象一下,当你需要租一件派对用的华丽小礼服,或是想体验一下超酷的运动器材,租赁小程序就…

MySQL 权限管理分配详解

MySQL 权限管理分配详解 MySQL权限系统的工作原理权限表的存取用户通过权限认证、进行权限分配的流程账号管理我们常用的授权all privileges到底有哪些权限呢?以及带来的安全隐患有哪些?创建账户的时候最好分配指定的权限,这样子安全也高管理…

使用C#开发VTK笔记(一)-VTK开发环境搭建

一.使用C#开发VTK的背景 因为C#开发的友好性,一直都比较习惯于从C#开发程序。而长期以来,都希望有一个稳定可靠的三位工程数模的开发演示平台,经过多次对比之后,感觉VTK和OpenCasCade这两个开源项目是比较好的,但它们都是用C++编写的,我用C#形式开发,只能找到发布的C#组…

React 组件中 State 的定义、使用及正确更新方式

​🌈个人主页:前端青山 🔥系列专栏:React篇 🔖人终将被年少不可得之物困其一生 依旧青山,本期给大家带来React篇专栏内容React 组件中 State 的定义、使用及正确更新方式 前言 在 React 应用开发中,state …

长沙市的科技查新机构有哪些

中南大学图书馆科技查新站: 中南大学图书馆科技查新站成立于2003年12月,中南大学图书馆科技查新站作为教育部首批批准的科技查新工作站之一,具备了在全国范围内开展科技查新工作的专业资质。 长沙理工大学科技查新工作站: 长沙理…

数组 - 八皇后 - 困难

************* C topic: 面试题 08.12. 八皇后 - 力扣(LeetCode) ************* Good morning, gays, Fridary angin and try the hard to celebrate. Inspect the topic: This topic I can understand it in a second. And I do rethink a movie, …

IDEA的service窗口中启动类是灰色且容易消失

大家在学习Spring Cloud的过程中,随着项目的深入,会分出很多个微服务,当我们的服务数量大于等于三个的时候,IDEA会给我们的服务整理起来,类似于这样 但是当我们的微服务数量达到5个以上的时候,再启动服务的时候,服务的启动类就会变成灰色,而且还容易丢失 解决方法 我们按住…

threejs相机辅助对象cameraHelper

为指定相机创建一个辅助对象,显示这个相机的视锥。 想要在场景里面显示相机的视锥,需要创建两个相机。 举个例子,场景中有个相机A,想要显示相机A的视锥,那么需要一个相机B,把B放在A的后面,两个…