Transformer架构解析

transfoermer简述

背景

Transformer是一种在自然语言处理领域中引起了革命性变革的模型架构。

它首次被提出于2017年的论文《Attention is All You Need》中,由Google的研究团队提出。

这篇论文开创了一种全新的模型架构,成为了许多自然语言处理任务的基础,如机器翻译、文本摘要、对话生成等。随后许多基于Transformer的变种模型也相继涌现,例如BERT、GPT等,进一步推动了自然语言处理领域的发展。

2018年10月,Google发出一篇论文《BERT: Pre-training of Deep Bidirectional Transformers for Language Understanding》, BERT模型横空出世, 并横扫NLP领域11项任务的最佳成绩!

BERT中发挥重要作用的结构就是Transformer!

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

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

RNN、LSTM、Transformer对长文本提取事物特征效果对比:

结论:

#1.rnn和lstm文本长度20~30之间效果下降显著
​
#2.transformer超过句子长度40以后也能保持较好效果

扩展阅读:

#1.SOTA模型
全称是state-of-the-art model,并不是特指某个具体的模型,而是指在该项研究任务中,目前最好最先进的模型。
​
#2.NLP常规任务划分
NLP是人工智能的子领域,它有两个核心任务:
NLU:Natural Language Understanding,自然语言理解。
NLG:Natural Language Generating,自然语言生成。

架构

Transformer的架构如下图所示:

Transformer架构分为四部分:

  • 输入部分

#1.源文本词嵌入层 + 位置编码器
#2.目标文本词嵌入层 + 位置编码器
  • 编码器部分

#1.由N个编码器层堆叠而成
#2.每个编码器由2个子层连接结构组成
#3.第一个子层连接结构包括普通的多头注意力机制 + 规范化层 + 残差连接
#4.第二个子层连接结构包括前馈全连接层 + 规范化层 + 残差连接

  • 解码器部分

#1.由N个解码器层堆叠而成
#2.每个解码器由3个子层连接结构组成
#3.第一个子层连接结构包括带掩码的多头注意力机制 + 规范化层 + 残差连接
#4.第二个子层连接结构包括普通的多头注意力机制 + 规范化层 + 残差连接
#5.第三个子层连接结构包括前馈全连接层 + 规范化层 + 残差连接

  • 输出部分

#1.线性层
#2.softmax层

输入部分

输入部分由两部分组成,词嵌入层和位置编码器

词嵌入层

就是一个Embedding层

目的是把文本数值化+张量化

代码如下

class Embeddings(nn.Module):
    def __init__(self, vocab, d_model):
        super().__init__()
        self.vocab = vocab
        self.d_model = d_model
        self.embedded = nn.Embedding(vocab, d_model)
        
    def forward(self, x):
        x = self.embedded(x)
        return x * math.sqrt(self.d_model)

位置编码器

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

作用:就是给文本添加位置信息。

  • 为什么要给文本添加位置信息?

词所在的位置不同,表示的含义可能不一样。

  • 什么是给文本添加位置信息?

给文本添加位置信息,准确地说,就是给文本的特征添加位置信息。

  • 如何添加位置信息?

引入Positional Encoding(PE,位置编码)矩阵,用特征矩阵加上PE矩阵,就相当于给文本添加了位置信息。

  • 如何构建PE矩阵?

位置矩阵由2个元素组成:行(类似于词表大小)、列(类似于维度)。

代码如下

class PositionalEncoding(nn.Module):
    def __init__(self, d_model, dropout, max_len=60):
        super().__init__()
        # dropout:随机失活的系数
        self.dropout = nn.Dropout(p=dropout)
        # 准备pe
        pe = torch.zeros(max_len, d_model) # [60,512]
        # 准备position
        position = torch.arange(0, max_len).unsqueeze(dim=1)
        # print("position-->", position) # [60,1]
        # 基于公式,实现位置编码
        # float():转换为小数
        _2i = torch.arange(0, d_model, 2).float()
        # print("_2i-->", _2i) # 一共256个
        # 对奇数位置赋值为sin函数的值
        pe[:, 0::2] = torch.sin(position / (10000 ** (_2i/d_model)))
        # 对偶数位置赋值为cos函数的值
        pe[:, 1::2] = torch.cos(position / (10000 ** (_2i/d_model)))
        # 对pe升维
        pe = pe.unsqueeze(dim=0) # [1,60,512]
        # 把pe注册到缓冲区
        self.register_buffer("pe", pe)
​
    def forward(self, x):
        # x就是Embedding后的数据,x的形状:[2,4,512]
        # 所以,截取的时候,只需要截取前4个token的位置编码即可
        x = x + self.pe[:, :x.shape[1]]
        return self.dropout(x)
​
​
绘制词向量中特征的分布曲线
def draw_pe_graph():
    # 初始化位置编码层对象
    pe = PositionalEncoding(d_model=20, dropout=0, max_len=100)
    # 准备数据x->【1,100,20】,1句话,100个 token,每个token使用20维度的向量表示
    x = torch.zeros(1, 100, 20)
    # 把x送给pe层
    y = pe(x)
    print("y-->", y.shape) # [1, 100, 20]
    # 画图
    plt.plot(np.arange(100), y[0, :, 4:8])
    # fontsize:字体大小
    plt.legend(["dim_%d" % p for p in [4,5,6,7]], fontsize='small')
    plt.show()

效果如上

  • 效果分析

  • 每条颜色的曲线代表某一个词汇中的特征在不同位置的含义

  • 保证同一词汇随着所在位置不同它对应位置嵌入向量会发生变化

  • 正弦波和余弦波的值域范围都是1到-1这又很好的控制了嵌入数值的大小, 有助于梯度的快速计算

编码器部分实现

编码器部分由六层编码器层组成,而每个编码器层由2个子连接层结构组成

第一个子连接层结构:多头自注意力(QKV相同)+规范化层+残差连接

第二个子连接层结构:前馈全连接层层+规范化层+残差连接

N为6

掩码张量

上三角矩阵:0组成的一个三角形,如下图:

下三角矩阵:0组成的一个三角形,如下图:

下三角矩阵的作用:

以模型解码为例,生成字符时,一个时间步一个时间步的解码。

使用掩码mask,比如:(0表示能看的见, 1表示被这遮掩) 希望模型不要使用当前字符和后面的字符。

也就是防止模型看到未来信息,用1给他遮掩住。

代码如下:

def dm_test_nptriu():
    # 测试产生上三角矩阵
    print(np.triu([[1, 1, 1, 1, 1],
                   [2, 2, 2, 2, 2],
                   [3, 3, 3, 3, 3],
                   [4, 4, 4, 4, 4],
                   [5, 5, 5, 5, 5]], k=1))
    print(np.triu([[1, 1, 1, 1, 1],
                   [2, 2, 2, 2, 2],
                   [3, 3, 3, 3, 3],
                   [4, 4, 4, 4, 4],
                   [5, 5, 5, 5, 5]], k=0))
    print(np.triu([[1, 1, 1, 1, 1],
                   [2, 2, 2, 2, 2],
                   [3, 3, 3, 3, 3],
                   [4, 4, 4, 4, 4],
                   [5, 5, 5, 5, 5]], k=-1))

# 生成掩码张量
def subsuquent_mask(size):
    # 生成上三角矩阵
    masked = np.triu(m=np.ones((size, size), dtype='uint8'), k=1)
    # print('masked-->\n', masked)
    # 生成下三角矩阵
    return torch.tensor(1 - masked)
​
​
def test_subsuquent_mask():
    size = 5
    masked = subsuquent_mask(5)
    print("masked-->\n", masked)
    # 掩码张量:作用在另外一个张量上。对另外一个张量进行掩码。
​
    # 绘图
    plt.imshow(subsuquent_mask(20))
    plt.show()

1:代表被遮掩(黄色部分)

0的位置向上看,全部被遮掩。1的位置向上看,只能看到一个信息。

自注意力机制

编码器使用的是自注意力机制,也就是Q=K=V。

已知:Q=K=V,他们的数据机构都是[2, 4, 512]。

问题:

  • 为什么Q要乘以K的转置?

因为不转置无法相乘。

  • 为什么要除以根号下d~k~?

控制方差,把方差拉回到0-1之间

代码如下:

def attention(query, key, value, mask=None, dropout=None):
    # 获取d_k,也就是维度,这里就是512
    d_k = query.shape[-1]
    # 基于自注意力计算公式,得到score
    scores = torch.matmul(query, key.transpose(-1, -2)) / math.sqrt(d_k)
    # print("scores-->", scores.shape)
​
    # 判断是否有mask
    if mask is not None:
        scores = scores.masked_fill(mask==0, -1e9)
​
    # 让score经过softmax,得到注意力权重
    p_attn = torch.softmax(scores, dim=-1)
​
    # 让p_attn经过dropout层
    if dropout is not None:
        p_attn = dropout(p_attn)
​
    # 返回结果
    return torch.matmul(p_attn, value), p_attn
​
​
def test_attention():
    vocab = 1000
    d_model = 512
    x = torch.tensor([[100, 2, 421, 508], [491, 998, 1, 221]])
    embed = Embeddings(vocab, d_model)
    x = embed(x)
    # 初始化PositionalEncoding对象
    dropout = 0.1
    pe = PositionalEncoding(d_model=d_model, dropout=dropout, max_len=60)
    # 让数据加上pe
    x = pe(x)
    # 准备query、key、value
    query = key = value = x
    # 不带掩码
    p_attn, attn_weights = attention(query, key, value)
    print("p_attn-->", p_attn, p_attn.shape)                          # [2, 4, 512]
    print("attn_weights-->", attn_weights, attn_weights.shape)        # [2, 4, 4]
    # 带掩码
    # mask = torch.zeros(2, 4, 4)
    # p_attn, attn_weights = attention(query, key, value, mask)
    # print("p_attn-->", p_attn, p_attn.shape)                          # [2, 4, 512]
    # print("attn_weights-->", attn_weights, attn_weights.shape)        # [2, 4, 4]

多头注意力机制

从多头注意力的结构图中,貌似这个所谓的多个头就是指多组线性变换层,其实并不是,我只有使用了一组线性变化层,即三个变换张量对Q,K,V分别进行线性变换,这些变换不会改变原有张量的尺寸,因此每个变换矩阵都是方阵,得到输出结果后,多头的作用才开始显现,每个头开始从词义层面分割输出的张量,也就是每个头都想获得一组Q,K,V进行注意力机制的计算,但是句子中的每个词的表示只获得一部分,也就是只分割了最后一维的词嵌入向量. 这就是所谓的多头,将每个头的获得的输入送到注意力机制中, 就形成多头注意力机制.

多头注意力机制的作用:这种结构设计能让每个注意力机制去优化每个词汇不同特征部分,从而均衡同一种注意力机制可能产生过的偏差,让词义拥有来自更多元的表达,实验表明可以提升模型效果

解释:

#1.线性变换:QKV分别输入到线性层
​
#2.view切分:特征做多头切分, 比如:256个特征切分成8个头,每个头64个特征 
​
#3.attention操作:通过attention函数进行多头特征提取 
​
#4.Concat操作:合并多头特征提取结果 
​
#5.线性层变换,最后的得到我们想要的数据形状

图解:

代码

def clones(model, N):
    # copy.deepcopy 深拷贝
    # Module
    return nn.ModuleList([copy.deepcopy(model) for _ in range(N)])
​
​
​
class MultiHeadAttention(nn.Module):
    def __init__(self, head, embedding_dim, dropout=0.1):
        super(MultiHeadAttention, self).__init__()
        # 断言,如果能整除,则代码继续,否则报错
        assert embedding_dim % head == 0
        # 属性
        # 多头:8个
        self.head = head
        # 每个头注意的维度数,也就是64
        self.d_k = embedding_dim // head
        # 注意力权重
        self.attn = None
        self.dropout = nn.Dropout(p=dropout)
        # 定义线性层
        # 4 = Q,K,V都要经过线性层(共3个线性层) + 最后一个线性层拼接所有多头维度
        self.linears = clones(nn.Linear(embedding_dim, embedding_dim), 4)
        # 打印多个线性层
        # print(self.linears[0])
        # print(self.linears[1])
        # print(self.linears[2])
        # print(self.linears[3])
        # print(self.linears[4]) # 报错
​
    def forward(self, query, key, value, mask=None):
        # query=key=value->[2,4,512]
        # 得到batch_size
        batch_size = query.shape[0]
        # 对mask升维
        if mask is not None:
            mask = mask.unsqueeze(0)
        # 让Q,K,V数据分别经过线性层
        # 这里的意思是,拉链后,线性层1,2,3,分别和query, key,value对应,就像
        """
        (self.linears[0], query)
        (self.linears[1], key)
        (self.linears[2], value) 
        """
        # 所以model对应linear1,2,3,x对应query,key, value,将x传入model(线性层)经过变换后,再进行reshaoe升维后交换位置得到2,8,4,512
        query, key, value = [model(x).reshape(batch_size, -1, self.head, self.d_k).transpose(1, 2)
                             for model,x in zip(self.linears, (query, key, value))]
        # print('query-->', query.shape)    # [2,8,4,64]
        # print('key-->', key.shape)        # [2,8,4,64]
        # print('value-->', value.shape)    # [2,8,4,64]
        # 把query、key,value送给注意力函数进行注意力计算
        x, self.attn = attention(query, key, value, mask=mask, dropout=self.dropout)
        # 把数据维度交换回来 # 2,4,8,512
        x = x.transpose(1, 2).reshape(batch_size, -1, self.head * self.d_k)
        # 让x经过最后一个线性层
        return self.linears[-1](x)

前馈全连接层
  • 在Transformer中前馈全连接层就是具有两层线性层的全连接网络.

  • 前馈全连接层的作用:

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

代码实现

class FeedForward(nn.Module):
    def __init__(self, d_model, d_ff, dropout=0.1):
        super().__init__()
        # 额外增加2个线性层,增强模型的表达能力
        #
        self.linear1 = nn.Linear(d_model, d_ff)
        self.linear2 = nn.Linear(d_ff, d_model)
        self.dropout = nn.Dropout(p=dropout)
​
    def forward(self, x):
        return self.linear2(self.dropout(torch.relu(self.linear1(x))))

规范化层

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

规范化层实际上是一种归一化的操作,旨在使输入的均值保持接近0,标准差保持接近1。

作用:

  • 防止梯度消失和梯度爆炸:在深度神经网络中,随着层数的增加,梯度很容易变得非常小(梯度消失)或非常大(梯度爆炸)。规范化层通过将输入数据归一化,可以缓解这一问题,使得梯度的传播更加稳定。

  • 加速训练过程:规范化层可使模型在训练时更快地收敛,加速整个训练过程

计算方式:

计算每个位置的所有特征维度上的均值和方差,然后对该位置的所有特征维度进行线性变换,以保证均值为0,方差为1,最后再进行缩放和平移操作,引入了可学习的参数(缩放因子和平移因子)。

为什么引入缩放因子和平移因子:

  • 为了学习不同批次之间的数据特征,能让模型有更好的泛化能力

  • 举个例子:每个批次有8个数,第1个批次有均值和方差,第2个批次有均值和方差。第n个批次也有均值和方差,可以学习所有数据的均值和方差,模型有更好的泛化能力

代码:

class LayerNorm(nn.Module):
    def __init__(self,features, eps=1e-6):
        super().__init__()
        # 封装2个参数,用于神经网络学习
        # y = ax+b
        self.a = nn.Parameter(torch.ones(features))
        self.b = nn.Parameter(torch.zeros(features))
        # eps记录属性
        self.eps = eps
​
    def forward(self, x):
        # 均值
        mean = x.mean(-1, keepdim=True)
        # 标准差
        std = x.std(-1, keepdim=True)
        # 封装为y函数
        return (self.a * (x - mean) / (std + self.eps) + self.b)

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

  • 子层连接结构图:

编码器层的子层连接结构:多头注意力/前馈全连接层 + 规范化层 + 残差连接

为了方便使用,我们将解码器层或者编码器层都用到的规范化层+残差连接封装成一个类

代码实现

class SublayerConnection(nn.Module):
    def __init__(self,size, dropout=0.1):
        super().__init__()
        # 规范化层
        self.norm = LayerNorm(size)
        # dropout层
        self.dropout = nn.Dropout(p=dropout)
​
    def forward(self, x, sublayer):
        # x数据
        # sublayer 子层连接结构对象,不同的子层连接结构,有可能是多头或者前馈全链接,它是一个函数入口
        # 先经过规范化层,再经过子层链接结构(前馈全链接或者多头注意力,再经过残差
        return x + self.dropout(sublayer(self.norm(x))) # x+提现了残差连接

编码器层

概念:是编码器的组成单元(编码部分的组成单元)。

作用:完成一次对输入数据特征的提取,即编码过程。

代码:

class Encoderlayer(nn.Module):
    def __init__(self, size, self_attn, feed_forward, dropout):
        super().__init__()
        self.size = size
        self.self_attn = self_attn
        self.feed_forward = feed_forward
        # 复制2个子层链接结构对象
        self.sublayer = clones(SublayerConnection(size, dropout), 2)
​
    def forward(self, x, mask):
        # 调用第一个子层链接结构
        x = self.sublayer[0](x, lambda x : self.self_attn(x, x, x, mask))
        # 调用第二个子层连接结构
        x = self.sublayer[1](x, lambda x: self.feed_forward(x))
        return x

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

  • 编码器的结构图

代码实现

class Encoder(nn.Module):
    def __init__(self, layer, N):
        super().__init__()
        self.layers = clones(layer, N)
        # layer :编码器层对象
        # layer.sizeL:从layer对象中拿到size属性
        self.norm = LayerNorm(layer.size)
​
    def forward(self, x, mask):
        # 遍历每个编码器层,让每一层的 编码器都经过x和mask
        for layer in self.layers:
            x = layer(x, mask)
            # 最后让x经过规范化层
        return self.norm(x)
​

解码器部分实现

结构:

其中解码器层的第二个子层连接结构的输入来自于编码器的输出,而且在图中可知,QKV不完全相同,所以是多头注意力机制,不是多头自注意力机制(QKV相同)

所以前面设计的MultiHeadAttention模块可以直接使用,仅仅是输入不同而已,但是所用的公式是同一个

所以,综上所知,解码器层的构建与编码器层的构建有一定相似度

解码器层

概念:是解码器的组成单元。

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

解码器层的组成:

第一个子层连接结构:多头自注意力机制+规范化层+残差连接

第二个子层连接结构:多头注意力机制+规范化层+残差连接

第三个子层连接结构:前馈全链接层+规范化层+残差连接

而编码器由6个编码器层组成

class DecoderLayer(nn.Module): # 编码器层
    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
        # 拷贝3个子层连接结构
        self.sublayer = clones(SublayerConnection(size, dropout), 3)
​
    def forward(self, x, memory, source_mask, target_mask):
        # x:解码器端经过词嵌入+位置编码后的结果
        # memory:编码器的输出
        # source_mask:编码器的掩码
        # target_mask:解码器的掩码
        # 先让数据经过解码器的带掩码的多头自注意力
        x = self.sublayer[0](x, lambda x : self.self_attn(x, x, x, target_mask))
        # 其次让数据经过普通注意力层
        x = self.sublayer[1](x, lambda x : self.src_attn(x, memory, memory, source_mask))
        # 最后让数据经过前馈全连接层
        return self.sublayer[2](x, lambda x : self.feed_forward(x))
    
class Decoder(nn.Module):
    def __init__(self, layer, N):
        super(Decoder, self).__init__()
        # 克隆多个解码器层
        self.layers = clones(layer, N)
        # 创建一个规范化层,用于最后的规范化输出
        self.norm = LayerNorm(features=layer.size)
​
    def forward(self, x, memory, source_mask, target_mask):
        for layer in self.layers:
            x = layer(x, memory, source_mask, target_mask)
        # 返回,加上规范化层
        return self.norm(x)

输出部分实现

输出部分包括线性层和softmax层:

  • 线性层:对上一步的结果进行指定维度变换,也就是转换维度的作用。

  • softmax层:转换概率分布,使最后一维的向量中的数字缩放到0-1的概率值域内, 并满足他们的和为1。

代码:

class Generator(nn.Module):
    def __init__(self, d_model, vocab_size):
        super(Generator, self).__init__()
        # d_model:维度数,这里就是512
        # vocab_size:目标文本词表大小
        self.linear = nn.Linear(d_model, vocab_size)
​
    def forward(self, x):
        # log_softmax等价于 nn.LogSoftmax()
        return torch.log_softmax(self.linear(x), dim=-1)

模型构建

我们已经完成了所有组成部分的实现,接下来实现完整的编码器-解码器结构。

完整代码如下

import copy
import torch
import torch.nn as nn
import math
import matplotlib.pyplot as plt
import numpy as np
​
# TODO 1.输入部分
# 词嵌入层
class Embeddings(nn.Module):
    def __init__(self, vocab, d_model):
        super().__init__()
        # 属性
        # vocab:词表大小
        # d_model:词嵌入维度,默认就是512
        # 在Transformer模型中,词嵌入维度,默认就是512
        self.vocab = vocab
        self.d_model = d_model
​
        # embedding层
        self.embedded = nn.Embedding(num_embeddings=vocab, embedding_dim=d_model)
​
    def forward(self, x):
        # 让x经过Embedding层,x就是输入的张量数据(数字)
        x = self.embedded(x)
        # 在Transformer中,Embedding的初始化默认使用的Xavier_uniform初始化,不是正态分布数据,
        # 为了让x保持数据处于正态分布,所以额外乘以 sqrt(d_model)
        return x * math.sqrt(self.d_model)
​
    
# 位置编码器
class PositionalEncoding(nn.Module):
    def __init__(self, d_model, dropout, max_len=60):
        super().__init__()
        # dropout:随机失活的系数
        self.dropout = nn.Dropout(p=dropout)
        # 准备pe
        pe = torch.zeros(max_len, d_model) # [60,512]
        # 准备position
        position = torch.arange(0, max_len).unsqueeze(dim=1)
        # print("position-->", position) # [60,1]
        # 基于公式,实现位置编码
        # float():转换为小数
        _2i = torch.arange(0, d_model, 2).float()
        # print("_2i-->", _2i) # 一共256个
        # 对奇数位置赋值为sin函数的值
        pe[:, 0::2] = torch.sin(position / (10000 ** (_2i/d_model)))
        # 对偶数位置赋值为cos函数的值
        pe[:, 1::2] = torch.cos(position / (10000 ** (_2i/d_model)))
        # 对pe升维
        pe = pe.unsqueeze(dim=0) # [1,60,512]
        # 把pe注册到缓冲区
        self.register_buffer("pe", pe)
​
    def forward(self, x):
        # x就是Embedding后的数据,x的形状:[2,4,512]
        # 所以,截取的时候,只需要截取前4个token的位置编码即可
        x = x + self.pe[:, :x.shape[1]]
        return self.dropout(x)
    
    
# 生成掩码张量
def subsuquent_mask(size):
    # 生成上三角矩阵
    masked = np.triu(m=np.ones((size, size), dtype='uint8'), k=1)
    # print('masked-->\n', masked)
    # 生成下三角矩阵
    return torch.tensor(1 - masked)
​
​
def test_subsuquent_mask():
    size = 5
    masked = subsuquent_mask(5)
    print("masked-->\n", masked)
    # 掩码张量:作用在另外一个张量上。对另外一个张量进行掩码。
​
    # 绘图
    plt.imshow(subsuquent_mask(20))
    plt.show()
    
    
# 注意力机制
def attention(query, key, value, mask=None, dropout=None):
    # 获取d_k,也就是维度,这里就是512
    d_k = query.shape[-1]
    # 基于自注意力计算公式,得到score
    scores = torch.matmul(query, key.transpose(-1, -2)) / math.sqrt(d_k)
    # print("scores-->", scores.shape)
​
    # 判断是否有mask
    if mask is not None:
        scores = scores.masked_fill(mask==0, -1e9)
​
    # 让score经过softmax,得到注意力权重
    p_attn = torch.softmax(scores, dim=-1)
​
    # 让p_attn经过dropout层
    if dropout is not None:
        p_attn = dropout(p_attn)
​
    # 返回结果
    return torch.matmul(p_attn, value), p_attn
​
​
​
​
# 多头注意力机制
def clones(model, N):
    # copy.deepcopy(x):深拷贝
    # ModuleList:模型列表,可以装多个模型,我们可以把它当做普通的Python列表来看待
    return nn.ModuleList([copy.deepcopy(model) for _ in range(N)])
​
​
class MultiHeadAttention(nn.Module):
    def __init__(self, head, embedding_dim, dropout=0.1):
        super(MultiHeadAttention, self).__init__()
        # 断言,如果能整除,则代码继续,否则报错
        assert embedding_dim % head == 0
        # 属性
        # 多头:8个
        self.head = head
        # 每个头注意的维度数,也就是64
        self.d_k = embedding_dim // head
        # 注意力权重
        self.attn = None
        self.dropout = nn.Dropout(p=dropout)
        # 定义线性层
        # 4 = Q,K,V都要经过线性层(共3个线性层) + 最后一个线性层拼接所有多头维度
        self.linears = clones(nn.Linear(embedding_dim, embedding_dim), 4)
        # 打印多个线性层
        # print(self.linears[0])
        # print(self.linears[1])
        # print(self.linears[2])
        # print(self.linears[3])
        # print(self.linears[4]) # 报错
​
    def forward(self, query, key, value, mask=None):
        # query=key=value->[2,4,512]
        # 得到batch_size
        batch_size = query.shape[0]
        # 对mask升维
        if mask is not None:
            mask = mask.unsqueeze(0)
        # 让Q,K,V数据分别经过线性层
        # 这里的意思是,拉链后,线性层1,2,3,分别和query, key,value对应,就像
        """
        (self.linears[0], query)
        (self.linears[1], key)
        (self.linears[2], value) 
        """
        # 所以model对应linear1,2,3,x对应query,key, value,将x传入model(线性层)经过变换后,再进行reshaoe升维后交换位置得到2,8,4,512
        query, key, value = [model(x).reshape(batch_size, -1, self.head, self.d_k).transpose(1, 2)
                             for model,x in zip(self.linears, (query, key, value))]
        # print('query-->', query.shape)    # [2,8,4,64]
        # print('key-->', key.shape)        # [2,8,4,64]
        # print('value-->', value.shape)    # [2,8,4,64]
        # 把query、key,value送给注意力函数进行注意力计算
        x, self.attn = attention(query, key, value, mask=mask, dropout=self.dropout)
        # 把数据维度交换回来 # 2,4,8,512
        x = x.transpose(1, 2).reshape(batch_size, -1, self.head * self.d_k)
        # 让x经过最后一个线性层
        return self.linears[-1](x)
    
    # 前馈全连接层
class FeedForward(nn.Module):
    def __init__(self, d_model, d_ff, dropout=0.1):
        super(FeedForward, self).__init__()
        # 额外增加2个线性层,增强模型的表达能力
        # 第一个线性层
        self.linear1 = nn.Linear(d_model, d_ff)
        # 第二个线性层
        self.linear2 = nn.Linear(d_ff, d_model)
        # 随机失活
        self.dropout = nn.Dropout(p=dropout)
​
    def forward(self, x):
        return self.linear2(self.dropout(torch.relu(self.linear1(x))))
    
    
# 规范化层,为了让数据保持同分布,可以减少训练时间,需要的数据也更少
class LayerNorm(nn.Module):
    def __init__(self, features, eps=1e-6):
        super(LayerNorm, self).__init__()
        # 封装2个参数,用于用于神经网络学习,经过Parameter封装后的参数可以随着神经网络的训练而更新。
        # 封装函数:y = ax + b
        self.a = nn.Parameter(torch.ones(features))
        self.b = nn.Parameter(torch.zeros(features))
        # eps纪录属性
        self.eps = eps
​
    def forward(self, x):
        # 均值
        mean = x.mean(dim=-1, keepdim=True)
        # 标准差
        std = x.std(dim=-1, keepdim=True)
        # 封装为y函数,# 这是标准化的公式 x-mean/方差
        return self.a * (x - mean) / (std + self.eps) + self.b
    
    
    
 # 解码器或者编码器都需要用到规范化+残差连接,将他封装到一个类中,子层连接结构
class SublayerConnection(nn.Module):
    def __init__(self, size, dropout=0.1):
        super(SublayerConnection, self).__init__()
        # 规范化层
        self.norm = LayerNorm(size)
        # dropout层
        self.dropout = nn.Dropout(p=dropout)
​
    def forward(self, x, sublayer):
        # x:数据
        # sublayer:子层连接结构对象,不同的子层连接结构,有可能是多头或者前馈全连接,它是一个函数入口地址(对象)
        # 先经过规范化层,再经过子层连接结构(前馈全连接或者多头注意力),再经过残差
        return x + self.dropout(sublayer(self.norm(x)))
    # 这里的 x + 就体现了残差连接
    
    
# 编码器层,每个编码器部分有6个编码器层组成
class EncoderLayer(nn.Module):
    def __init__(self, size, self_attn, feed_forward, dropout):
        super(EncoderLayer, self).__init__()
        # 属性
        self.size = size
        self.self_attn = self_attn
        self.feed_forward = feed_forward
        # 复制2个子层连接结构对象
        self.sublayer = clones(SublayerConnection(size, dropout), 2)
​
    def forward(self, x, mask):
        # 调用第1个子层连接结构
        x = self.sublayer[0](x, lambda x : self.self_attn(x, x, x, mask))
        # 调用第2个子层连接结构
        # x = self.sublayer[1](x, self.feed_forward)
        x = self.sublayer[1](x, lambda x : self.feed_forward(x))
        return x
    
    
    # 编码器
class Encoder(nn.Module):
    def __init__(self, layer, N):
        # layer:编码器层
        super(Encoder, self).__init__()
        self.layers = clones(layer, N)
        # layer:编码器层对象
        # layer.size:从layer对象中拿到size属性
        self.norm = LayerNorm(features=layer.size)
​
    def forward(self, x, mask):
        # 遍历每个编码器层,让每一层的编码器都经过x和mask
        for layer in self.layers:
            x = layer(x, mask)
        # 最后让x经过规范化层
        return self.norm(x)
    
​
# TODO 3.解码器部分
# 解码器层
class DecoderLayer(nn.Module):
    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
        # 拷贝3个子层连接结构
        self.sublayer = clones(SublayerConnection(size, dropout), 3)
​
    def forward(self, x, memory, source_mask, target_mask):
        # x:解码器端经过词嵌入+位置编码后的结果
        # memory:编码器的输出
        # source_mask:编码器的掩码
        # target_mask:解码器的掩码
        # 先让数据经过解码器的带掩码的多头自注意力
        x = self.sublayer[0](x, lambda x : self.self_attn(x, x, x, target_mask))
        # 其次让数据经过普通注意力层
        x = self.sublayer[1](x, lambda x : self.src_attn(x, memory, memory, source_mask))
        # 最后让数据经过前馈全连接层
        return self.sublayer[2](x, lambda x : self.feed_forward(x))
    
    # 解码器
class Decoder(nn.Module):
    def __init__(self, layer, N):
        super(Decoder, self).__init__()
        # 克隆多个解码器层
        self.layers = clones(layer, N)
        # 创建一个规范化层,用于最后的规范化输出
        self.norm = LayerNorm(features=layer.size)
​
    def forward(self, x, memory, source_mask, target_mask):
        for layer in self.layers:
            x = layer(x, memory, source_mask, target_mask)
        # 返回,加上规范化层
        return self.norm(x) 
    
    
# TODO 4.输出部分
class Generator(nn.Module):
    def __init__(self, d_model, vocab_size):
        super(Generator, self).__init__()
        # d_model:维度数,这里就是512
        # vocab_size:目标文本词表大小
        self.linear = nn.Linear(d_model, vocab_size)
​
    def forward(self, x):
        # log_softmax等价于 nn.LogSoftmax()
        return torch.log_softmax(self.linear(x), dim=-1)
​
​
def test_generator():
    vocab = 1000            # 源文本词表大小
    target_vocab = 2000     # 目标文本词表大小
    d_model = 512
    d_ff = 64
    N = 6
    c = copy.deepcopy
    x = torch.tensor([[100, 2, 421, 508], [491, 998, 1, 221]])
    embed = Embeddings(vocab, d_model)
    x = embed(x)
    # 初始化PositionalEncoding对象
    dropout = 0.1
    pe = PositionalEncoding(d_model=d_model, dropout=dropout, max_len=60)
    # 让数据加上pe
    x = pe(x)
    mha = MultiHeadAttention(head=8, embedding_dim=d_model)
    ff = FeedForward(d_model=d_model, d_ff=d_ff, dropout=dropout)
    # mask没有业务逻辑,只是测试代码是否能正常运行。
    mask = torch.zeros(8, 4, 4)
    # 实例化编码器层对象
    layer = EncoderLayer(size=d_model, self_attn=c(mha), feed_forward=c(ff), dropout=dropout)
    # 准备编码器对象
    encoder = Encoder(layer, N)
    # 让数据经过编码器
    memory = encoder(x, mask)
    # print("memory-->", memory.shape)        # 编码器的输出结果
​
    # 准备解码器的x,这个x是目标文本的词表数值化后的结果
    x = torch.tensor([[130, 234, 521, 598], [993, 938, 123, 261]])
    target_embed = Embeddings(target_vocab, d_model)
    target_x = target_embed(x)
    target_pe = PositionalEncoding(d_model=d_model, dropout=dropout, max_len=60)
    target_x = target_pe(target_x)
    # print("target_x-->", target_x.shape)    # 解码器的输入
​
    # 构建解码器层
    decoder_layer = DecoderLayer(size=d_model, self_attn=c(mha), src_attn=c(mha), feed_forward=c(ff), dropout=dropout)
    # 构建解码器对象
    decoder = Decoder(decoder_layer, N)
    # 把数据送给解码器
    decoder_result = decoder(target_x, memory, mask, mask)
    # print("decoder_result-->", decoder_result.shape)
    # 构建输出层对象
    gen = Generator(d_model, target_vocab)
    output = gen(decoder_result)
    print("output-->", output.shape)        # [2,4,2000]
    
    
if __name__ == '__main__':
    test_generator()

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值