• Transformer学习


    第一章:Transformer背景介绍

    1.1 Transformer的诞生

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

    论文地址: https:/arxiv.org/pdf/1810.04805.pdf

    而在BERT中发挥重要作用的结构就是Transformer,之后又相继出现XLNET,rOBERT等模型击败了BERT,但是他们的核心没有变,仍然是:Transformer。

    1.2 Transformer的优势

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

    (1)Transformer能够利用分布式GPU进行并行训练,提升模型训练效率;

    (2)在分析预测更长的文本时,捕捉间隔较长的语义关联效果更好。

    1.3 Transformer的市场

    在著名的SOTA机器翻译榜单上,几乎所有排名靠前的模型都使用Transformer。

    WMT2014 English-German Benchmark (Machine Translation) | Papers With Code

    其基本上可以看作是工业界的风钧标,市场空间自然不必多说!

    第二章:Transformer架构解析

    2.1 认识Transformer架构

    (1)学习目标:

    • 了解Transformer模型的作用.
    • 了解Transformer总体架构图中各个组成部分的名称

    (2)Transformer模型的作用:

    基于seq2seq架构的transformer模型可以完成NLP领域研究的典型任务,如机器翻译,文本生成等,同时又可以构建预训练语言模型,用于不同任务的迁移学习

    (3)声明:

    在接下来的架构分析中,我们将假设使用Transformer模型架构处理从一种语言文本到另一种语言文本的翻译工作,因此很多命名方式遵循NLP中的规则。比如: Embeddding层将称作文本嵌入层,Embedding层产生的张量称为词嵌入张量,它的最后一维将称作词向量等。

    (4)Transformer总体架构图:

    (5)Transformer总体架构可分为四个部分:

    • 输入部分
    • 输出部分
    • 编码器部分
    • 解码器部分

    (6)输入部分包含:

    • 源文本嵌入层及其位置编码器
    • 目标文本嵌入层及其位置编码器

    (7) 输出部分包含:

    • 线性层
    • softmax层

    (8) 编码器部分:

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

    (9)解码器部分:

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

     (10)小节总结:

    本节学习了Transformer模型的作用:

    • 基于seq2seq架构的transformer模型可以完成NLP领域研究的典型任务,如机器翻译文本生成等。同时又可以构建预训练语言模型,用于不同任务的迁移学习

    Transformer总体架构可分为四个部分:

    • 输入部分
    • 输出部分
    • 编码器部分
    • 解码器部分

    输入部分包含:

    • 源文本嵌入层及其位置编码器
    • 目标文本嵌入层及其位置编码器

    输出部分包含:

    • 线性层
    • softmax处理器

    编码器部分:

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

    解码器部分:

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

    学习目标:

    • 了解文本嵌入层和位置编码的作用
    • 掌握文本嵌入层和位置编码的实现过程

    输入部分包含:

    • 源变本嵌入层及其位置编码器
    • 目标文本嵌入层及其位置编码器

    文本嵌入层的作用:

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

    文本嵌入层的代码分析:

    • 导入包
    1. import torch
    2. import torch.nn as nn
    3. import torch.nn.functional as F
    4. from torch.autograd import Variable
    5. import math
    6. import matplotlib.pyplot as plt
    7. import numpy as np
    8. import copy

    文本嵌入层:

    1. # 构建Embedding类来实现文本嵌入层
    2. class Embeddings(nn.Module):
    3. def __init__(self,d_model,vocab):
    4. # dmodel: 词嵌入的维度
    5. # vocab: 词表的大小
    6. super(Embeddings,self).__init__()
    7. # 定义Embedding层
    8. self.lut = nn.Embedding(vocab,d_model)
    9. # 将参数传入类中
    10. self.d_model = d_model
    11. def forward(self,x):
    12. # x: 代表输入进模型的文本通过词汇映射后的数字张量
    13. return self.lut(x)* math.sqrt(self.d_model)
    14. # 调用类
    15. d_model=512
    16. vocab = 1000
    17. x = Variable(torch.LongTensor([[100,2,421,508],[491,998,1,221]]))
    18. emb = Embeddings(d_model, vocab)
    19. embr = emb(x)
    20. print("embr:", embr)
    21. print(embr.shape)

    位置编码器的作用:

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

    位置编码器代码分析:

    1. class PositionalEncoding(nn.Module):
    2. """ 定义位置编码器类, 我们同样把它看做一个层, 因此会继承nn.Module """
    3. def __init__(self, d_model, dropout, max_len=5000):
    4. """
    5. 位置编码器类的初始化函数, 共有三个参数, 分别是:
    6. :param d_model: 词嵌入维度
    7. :param dropout: 置0比率
    8. :param max_len: 每个句子的最大长度
    9. """
    10. super(PositionalEncoding, self).__init__()
    11. # 实例化nn中预定义的Dropout层, 并将dropout传入其中, 获得对象self.dropout
    12. self.dropout = nn.Dropout(p=dropout)
    13. # 初始化一个位置编码矩阵, 它是一个0阵, 矩阵的大小是max_len x d_model.
    14. pe = torch.zeros(max_len, d_model)
    15. # 初始化一个绝对位置矩阵, 在我们这里, 词汇的绝对位置就是用它的索引去表示.
    16. # 所以我们首先使用arange方法获得一个连续自然数向量, 然后再使用unsqueeze方法拓展向量维度使其成为矩阵,
    17. # 又因为参数传的是1, 代表矩阵拓展的位置, 会使向量变成一个max_len x 1 的矩阵,
    18. position = torch.arange(0, max_len).unsqueeze(1)
    19. # 绝对位置矩阵初始化之后, 接下来就是考虑如何将这些位置信息加入到位置编码矩阵中,
    20. # 最简单思路就是先将max_len x 1的绝对位置矩阵, 变换成max_len x d_model形状, 然后覆盖原来的初始位置编码矩阵即可,
    21. # 要做这种矩阵变换, 就需要一个1xd_model形状的变换矩阵div_term, 我们对这个变换矩阵的要求除了形状外,
    22. # 还希望它能够将自然数的绝对位置编码缩放成足够小的数字, 有助于在之后的梯度下降过程中更快的收敛. 这样我们就可以开始初始化这个变换矩阵了.
    23. # 首先使用arange获得一个自然数矩阵, 但是细心的同学们会发现, 我们这里并没有按照预计的一样初始化一个1xd_model的矩阵,
    24. # 而是有了一个跳跃, 只初始化了一半即1xd_model/2 的矩阵. 为什么是一半呢, 其实这里并不是真正意义上的初始化了一半的矩阵,
    25. # 我们可以把它看作是初始化了两次, 而每次初始化的变换矩阵会做不同的处理, 第一次初始化的变换矩阵分布在正弦波上, 第二次初始化的变换矩阵分布在余弦波上,
    26. # 并把这两个矩阵分别填充在位置编码矩阵的偶数和奇数位置上, 组成最终的位置编码矩阵.
    27. div_term = torch.exp(torch.arange(0, d_model, 2) * -(math.log(10000.0) / d_model))
    28. pe[:, 0::2] = torch.sin(position * div_term)
    29. pe[:, 1::2] = torch.cos(position * div_term)
    30. # 这样我们就得到了位置编码矩阵pe, pe现在还只是一个二维矩阵, 要想和embedding的输出(一个三维张量)相加,
    31. # 就必须拓展一个维度, 所以这里使用unsqueeze拓展维度.
    32. pe = pe.unsqueeze(0)
    33. # 最后把pe位置编码矩阵注册成模型的buffer, 什么是buffer呢,
    34. # 我们把它认为是对模型效果有帮助的, 但是却不是模型结构中超参数或者参数, 不需要随着优化步骤进行更新的增益对象.
    35. # 注册之后我们就可以在模型保存后重加载时和模型结构与参数一同被加载.
    36. self.register_buffer('pe', pe)
    37. def forward(self, x):
    38. """forward函数的参数是x, 表示文本序列的词嵌入表示"""
    39. # 在相加之前我们对pe做一些适配工作, 将这个三维张量的第二维也就是句子最大长度的那一维将切片到与输入的x的第二维相同即x.size(1),
    40. # 因为我们默认max_len为5000一般来讲实在太大了, 很难有一条句子包含5000个词汇, 所以要进行与输入张量的适配.
    41. # 最后使用Variable进行封装, 使其与x的样式相同, 但是它是不需要进行梯度求解的, 因此把requires_grad设置成false.
    42. x = x + Variable(self.pe[:, :x.size(1)],
    43. requires_grad=False)
    44. # 最后使用self.dropout对象进行'丢弃'操作, 并返回结果.
    45. return self.dropout(x)
    46. # 调用类
    47. # 词嵌入维度是512维
    48. d_model = 512
    49. # 置0比率为0.1
    50. dropout = 0.1
    51. # 句子最大长度
    52. max_len=60
    53. # 输入x是Embedding层的输出的张量, 形状是2 x 4 x 512
    54. x = embr
    55. pe = PositionalEncoding(d_model, dropout, max_len)
    56. pe_result = pe(x)
    57. print("pe_result:", pe_result)
    58. # 形状是2 x 4 x 512
    59. print("pe_result shape:", pe_result.shape)

    绘制词汇向量中特征的分布曲线:

    1. import matplotlib.pyplot as plt
    2. # 创建一张15 x 5大小的画布
    3. plt.figure(figsize=(15, 5))
    4. # 实例化PositionalEncoding类得到pe对象, 输入参数是20和0
    5. pe = PositionalEncoding(20, 0)
    6. # 然后向pe传入被Variable封装的tensor, 这样pe会直接执行forward函数,
    7. # 且这个tensor里的数值都是0, 被处理后相当于位置编码张量
    8. y = pe(Variable(torch.zeros(1, 100, 20)))
    9. # 然后定义画布的横纵坐标, 横坐标到100的长度, 纵坐标是某一个词汇中的某维特征在不同长度下对应的值
    10. # 因为总共有20维之多, 我们这里只查看4,5,6,7维的值.
    11. plt.plot(np.arange(100), y[0, :, 4:8].data.numpy())
    12. # 在画布上填写维度提示信息
    13. plt.legend(["dim %d"%p for p in [4,5,6,7]])

    输出效果:

    输出效果分析:

    • 每条颜色的曲线代表某一个词汇中的特征在不同位置的含义。
    • 保证同一词汇随着所在位置不同它对应位置嵌入向量会发生变化。
    • 正弦波和余弦波的值域范围都是1到-1这又很好的控制了嵌入数值的大小, 有助于梯度的快速计算。

    小节总结

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

    • 学习并实现了文本嵌入层的类:Embeddings
      • 初始化函数以d_model: 词嵌入维度, 和vocab:词汇总数为参数,内部主要使用了nn中的预定层Embedding进行词嵌入。
      • 在forward函数中,将输入x传入到Embedding的实例化对象中, 然后乘以一个根号下d_model进行缩放, 控制数值大小。
      • 它的输出是文本嵌入后的结果。

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

    • 学习并实现了位置编码器的类: PositionalEncoding
      • 初始化函数以d_model, dropout,max_len为参数,分别代表d_model: 词嵌入维度, dropout:置0比率, max_len:每个句子的最大长度。
      • forward函数中的输入参数为x, 是Embedding层的输出。
      • 最终输出一个加入了位置编码信息的词嵌入张量。

    • 实现了绘制词汇向量中特征的分布曲线:
      • 保证同一词汇随着所在位置不同它对应位置嵌入向量会发生变化。
      • 正弦波和余弦波的值域范围都是1到-1, 这又很好的控制了嵌入数值的大小, 有助于梯度的快速计算。

     2.3 编码器部分实现 

    学习目标

    • 了解编码器中各个组成部分的作用。
    • 掌握编码器中各个组成部分的实现过程。

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

     2.3.1 掩码张量

    • 了解什么是掩码张量以及它的作用
    • 掌握生成掩码张量的实现过程

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

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

    • 生成掩码张量的代码分析:
    1. def subsequent_mask(size):
    2. """
    3. 构建掩码张量的函数
    4. :param size: 代表掩码张量最后两个维度, 形成一个方阵
    5. :return:
    6. """
    7. # 在函数中, 首先定义掩码张量的形状
    8. attn_shape = (1, size, size)
    9. # 使用 np.ones() 先构建一个全1的张量, 然后利用 np.triu() 形成上三角阵
    10. # 最后为了节约空间, 再使其中的数据类型变为无符号8位整形unit8
    11. subsequent_mask = np.triu(np.ones(attn_shape), k=1).astype('uint8')
    12. # 最后将numpy类型转化为torch中的tensor, 内部做一个1 - 的操作,
    13. # 其实是做了一个三角阵的反转, subsequent_mask中的每个元素都会被1减,
    14. # 如果是0, subsequent_mask中的该位置由0变成1
    15. # 如果是1, subsequent_mask中的该位置由1变成0
    16. # 得到一个下三角矩阵
    17. return torch.from_numpy(1 - subsequent_mask)
    18. # 生成的掩码张量的最后两维的大小
    19. size = 5
    20. sm = subsequent_mask(size)
    21. print("sm:", sm)

     可视化展示:


     效果分析:

    • 通过观察可视化方阵, 黄色是1的部分, 这里代表被遮掩, 紫色代表没有被遮掩的信息, 横坐标代表目标词汇的位置, 纵坐标代表可查看的位置;
    • 我们看到, 在0的位置我们一看望过去都是黄色的, 都被遮住了,1的位置一眼望过去还是黄色,说明第一次词还没有产生, 从第二个位置看过去, 就能看到位置1的词, 其他位置看不到, 以此类推。

    总结:

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

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

    • 学习并实现了生成向后遮掩的掩码张量函数: subsequent_mask
      • 它的输入是size,代表掩码张量的大小。
      • 它的输出是一个最后两维形成1方阵的下三角阵。
      • 最后对生成的掩码张量进行了可视化分析, 更深一步理解了它的用途。

    2.3.2 注意力机制
    • 学习目标:
      • 了解什么是注意力计算规则和注意力机制。
      • 掌握注意力计算规则的实现过程。

    • 什么是注意力:
      • 我们观察事物时,之所以能够快速判断一种事物(当然允许判断是错误的),是因为我们大脑能够很快把注意力放在事物最具有辨识度的部分从而作出判断,而并非是从头到尾的观察一遍事物后,才能有判断结果。正是基于这样的理论,就产生了注意力机制。

    • 什么是注意力计算规则:
      • 它需要三个输入:Q(query), K(key),V(value), 然后通过公式得到注意力的计算结果, 这个结果代表query在key和value作用下的表示。 而这个具体的计算规则有很多种, 这里只介绍我们用到的这一种。

    • 我们这里使用的注意力的计算规则:


    • Q, K, V的比喻解释:
    1. 假如我们有一个问题: 给出一段文本,使用一些关键词对它进行描述!
    2. 为了方便统一正确答案,这道题可能预先已经给大家写出了一些关键词作为提示。其中这些给出的提示就可以看作是key, 而整个的文本信息就相当于是query,value的含义则更抽象,可以比作是你看到这段文本信息后,脑子里浮现的答案信息。这里我们又假设大家最开始都不是很聪明,第一次看到这段文本后脑子里基本上浮现的信息就只有提示这些信息,因此key与value基本是相同的,但是随着我们对这个问题的深入理解,通过我们的思考脑子里想起来的东西原来越多,并且能够开始对我们query也就是这段文本,提取关键信息进行表示。这就是注意力作用的过程, 通过这个过程,
    3. 我们最终脑子里的value发生了变化,
    4. 根据提示key生成了query的关键词表示方法,也就是另外一种特征表示方法。
    5. 刚刚我们说到key和value一般情况下默认是相同,与query是不同的,这种是我们一般的注意力输入形式,
    6. 但有一种特殊情况,就是我们query与key和value相同,这种情况我们称为自注意力机制,就如同我们的刚刚的例子,使用一般注意力机制,是使用不同于给定文本的关键词表示它。 而自注意力机制, 需要用给定文本自身来表达自己,也就是说你需要从给定文本中抽取关键词来表述它, 相当于对文本自身的一次特征提取。

    • 什么是注意力机制:
      • 注意力机制是注意力计算规则能够应用的深度学习网络的载体, 除了注意力计算规则外, 还包括一些必要的全连接层以及相关张量处理, 使其与应用网络融为一体。 使用自注意力计算规则的注意力机制称为自注意力机制。

    • 注意力机制在网络中实现的图形表示:

    • 注意力计算规则的代码分析:
    1. def attention(query, key, value, mask=None, dropout=None):
    2. """
    3. 注意力机制的实现
    4. :param query: 输入张量
    5. :param key: 输入张量
    6. :param value: 输入张量
    7. :param mask: 掩码张量
    8. :param dropout: 传入的nn.Dropout层的实例化对象, 默认为None
    9. :return:
    10. """
    11. # 在函数中, 首先将query的最后一个维度提取出来, 一般情况下就等同于词嵌入维度, 命名为d_k
    12. d_k = query.size(-1)
    13. # 按照注意力公式, 将query与key的转置相乘, 这里面key是将最后两个维度进行转置, 再除以缩放系数根号下d_k, 这种计算方法也称为缩放点积注意力计算
    14. # 得到注意力得分张量scores
    15. scores = torch.matmul(query, key.transpose(-2, -1)) / math.sqrt(d_k)
    16. # 接着判断是否使用掩码张量
    17. if mask is not None:
    18. # 使用tensor的masked_fill方法, 将掩码张量和scores张量每个位置一一比较, 如果掩码张量处为0
    19. # 则对应的scores张量用-1e9这个值来替换, 如下演示
    20. scores = scores.masked_fill(mask == 0, -1e9)
    21. # 对scores的最后一维进行softmax操作, 使用F.softmax方法, 第一个参数是softmax对象, 第二个是目标维度
    22. # 这样获得最终的注意力张量
    23. p_attn = F.softmax(scores, dim=-1)
    24. # 之后判断是否使用dropout进行随机置0
    25. if dropout is not None:
    26. # 将p_attn传入dropout对象中进行'丢弃'处理
    27. p_attn = dropout(p_attn)
    28. # 最后, 根据公式将p_attn与value张量相乘获得最终的query注意力表示, 同时返回注意力张量
    29. return torch.matmul(p_attn, value), p_attn
    30. # 带有mask的输入参数
    31. query = key = value = pe_result
    32. # 令mask为一个2*4*4的零张量
    33. mask = Variable(torch.zeros(2, 4, 4))
    34. attn, p_attn = attention(query, key, value, mask=mask)
    35. print("attn:", attn)
    36. print("p_attn:", p_attn)

    总结:

    • 学习了什么是注意力:
      • 我们观察事物时,之所以能够快速判断一种事物(当然允许判断是错误的), 是因为我们大脑能够很快把注意力放在事物最具有辨识度的部分从而作出判断,而并非是从头到尾的观察一遍事物后,才能有判断结果. 正是基于这样的理论,就产生了注意力机制。

    • 什么是注意力计算规则:
      • 它需要三个指定的输入Q(query), K(key), V(value),然后通过公式得到注意力的计算结果,这个结果代表query在key和value作用下的表示。 而这个具体的计算规则有很多种, 我这里只介绍我们用到的这一种。

    • 学习了Q,K,V的比喻解释:
      • Q是一段准备被概括的文本; K是给出的提示; V是大脑中的对提示K的延伸。
      • 当Q=K=V时, 称作自注意力机制。

    • 什么是注意力机制:
      • 注意力机制是注意力计算规则能够应用的深度学习网络的载体, 除了注意力计算规则外, 还包括一些必要的全连接层以及相关张量处理,使其与应用网络融为一体。 使用自注意力计算规则的注意力机制称为自注意力机制。

    • 学习并实现了注意力计算规则的函数: attention
      • 它的输入就是Q,K,V以及mask和dropout, mask用于掩码, dropout用于随机置0。
      • 它的输出有两个, query的注意力表示以及注意力张量。

    2.2.3 多头注意力机制
    • 学习目标:
      • 了解多头注意力机制的作用。
      • 掌握多头注意力机制的实现过程。

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

    • 多头注意力机制结构图:


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

    • 多头注意力机制的代码实现:
    1. # 用于深度拷贝的copy工具包
    2. import copy
    3. # 首先需要定义克隆函数, 因为在多头注意力机制的实现中, 用到多个结构相同的线性层.
    4. # 我们将使用clone函数将他们一同初始化在一个网络层列表对象中. 之后的结构中也会用到该函数.
    5. def clones(module, N):
    6. """
    7. 用于生成相同网络层的克隆函数
    8. :param module: module表示要克隆的目标网络层
    9. :param N: N代表需要克隆的数量
    10. :return:
    11. """
    12. # 在函数中, 我们通过for循环对module进行N次深度拷贝, 使其每个module成为独立的层,
    13. # 然后将其放在nn.ModuleList类型的列表中存放.
    14. return nn.ModuleList([copy.deepcopy(module) for _ in range(N)])
    15. class MultiHeadedAttention(nn.Module):
    16. """ 实现多头注意力机制 """
    17. def __init__(self, head, embedding_dim, dropout=0.1):
    18. """
    19. 在类的初始化时, 会传入三个参数:
    20. :param head: head代表头数
    21. :param embedding_dim: embedding_dim代表词嵌入的维度
    22. :param dropout: dropout代表进行dropout操作时置零的比率, 默认是0.1
    23. """
    24. super(MultiHeadedAttention, self).__init__()
    25. # 要确认的一个事实: 多头的数量head需要整除词嵌入的维度embedding_dim
    26. # 在函数中, 首先使用了一个测试中常用的assert语句, 判断head是否能被d_model整除,
    27. # 这是因为我们之后要给每个头分配等量的词特征. 也就是embedding_dim/head个.
    28. assert embedding_dim % head == 0
    29. # 得到每个头获得的词向量的维度d_k
    30. self.d_k = embedding_dim // head
    31. # 传入头数h
    32. self.head = head
    33. # 然后获得线性层对象,通过nn的Linear实例化,它的内部变换矩阵是embedding_dim * embedding_dim,然后使用clones函数克隆四个,
    34. # 为什么是四个呢, 这是因为在多头注意力中,Q,K,V各需要一个, 最后拼接的矩阵还需要一个, 因此一共是四个.
    35. self.linears = clones(nn.Linear(embedding_dim, embedding_dim), 4)
    36. # self.attn为None, 它代表最后得到的注意力张量, 现在还没有结果所以为None.
    37. self.attn = None
    38. # 最后就是一个self.dropout对象, 它通过nn中的Dropout实例化而来, 置零比率为传进来的参数dropout.
    39. self.dropout = nn.Dropout(p=dropout)
    40. def forward(self, query, key, value, mask=None):
    41. """
    42. 前向逻辑函数, 它的输入参数有四:
    43. :param query: Q
    44. :param key: K
    45. :param value: V
    46. :param mask: mask掩码张量,默认是None
    47. :return:
    48. """
    49. # 如果存在掩码张量mask
    50. if mask is not None:
    51. # 使用unsqueeze拓展维度
    52. mask = mask.unsqueeze(0)
    53. # 接着,我们获得一个batch_size的变量,他是query尺寸的第1个数字,代表有多少条样本.
    54. batch_size = query.size(0)
    55. # 之后就进入多头处理环节
    56. # 首先利用zip将输入QKV与三个线性层组到一起, 然后使用for循环, 将输入QKV分别传到线性层中,
    57. # 做完线性变换后, 开始为每个头分割输入, 这里使用view方法对线性变换的结果进行维度重塑, 多加了一个维度h, 代表头数,
    58. # 这样就意味着每个头可以获得一部分词特征组成的句子, 其中的-1代表自适应维度,
    59. # 计算机会根据这种变换自动计算这里的值. 然后对第二维和第三维进行转置操作,
    60. # 为了让代表句子长度维度和词向量维度能够相邻, 这样注意力机制才能找到词义与句子位置的关系,
    61. # 从attention函数中可以看到, 利用的是原始输入的倒数第一和第二维. 这样我们就得到了每个头的输入.
    62. query, key, value = [model(x).view(batch_size, -1, self.head, self.d_k).transpose(1, 2)
    63. for model, x in zip(self.linears, (query, key, value))]
    64. # 得到每个头的输入后,接下来就是将他们传入到attention中,
    65. # 这里直接调用我们之前实现的attention函数.同时也将mask和dropout传入其中.
    66. x, self.attn = attention(query, key, value, mask=mask, dropout=self.dropout)
    67. # 通过多头注意力计算后, 我们就得到了每个头计算结果组成的4维张量, 我们需要将其转换为输入的形状以方便后续的计算,
    68. # 因此这里开始进行第一步处理环节的逆操作, 先对第二和第三维进行转置, 然后使用contiguous方法,
    69. # 这个方法的作用就是能够让转置后的张量应用view方法, 否则将无法直接使用,
    70. # 所以, 下一步就是使用view重塑形状, 变成和输入形状相同, self.head * self.d_k = embedding_dim.
    71. x = x.transpose(1, 2).contiguous().view(batch_size, -1, self.head * self.d_k)
    72. # 最后使用线性层列表中的最后一个线性层对输入进行线性变换得到最终的多头注意力结构的输出.
    73. return self.linears[-1](x)
    74. # 头数head
    75. head = 8
    76. # 词嵌入维度embedding_dim
    77. embedding_dim = 512
    78. # 置零比率dropout
    79. dropout = 0.2
    80. # 假设输入的Q,K,V仍然相等
    81. query = value = key = pe_result
    82. # 输入的掩码张量mask
    83. mask = Variable(torch.zeros(8, 4, 4))
    84. mha = MultiHeadedAttention(head, embedding_dim, dropout)
    85. mha_result = mha(query, key, value, mask)
    86. print(mha_result)
    87. print(mha_result.shape)

     多头注意力机制总结:

    • 学习了什么是多头注意力机制:
      • 每个头开始从词义层面分割输出的张量,也就是每个头都想获得一组Q,K,V进行注意力机制的计算,但是句子中的每个词的表示只获得一部分,也就是只分割了最后一维的词嵌入向量。这就是所谓的多头。将每个头的获得的输入送到注意力机制中, 就形成了多头注意力机制。

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

    • 学习并实现了多头注意力机制的类: MultiHeadedAttention
      • 它的实例化对象输出是通过多头注意力机制处理的Q的注意力表示。
      • 它的实例化对象输入是Q, K, V以及掩码张量mask。
      • 接着实现MultiHeadedAttention类, 它的初始化函数输入是h, d_model,dropout分别代表头数,词嵌入维度和置零比率。
      • clones函数的输出是装有N个克隆层的Module列表。
      • clones函数的输入是module,N,分别代表克隆的目标层,和克隆个数。
      • 因为多头注意力机制中需要使用多个相同的线性层, 首先实现了克隆函数clones。

  • 相关阅读:
    Wireshark数据包分析——时间盲注/延时注入攻击
    java数据类型-简介
    面试题: Spring AOP是如何实现的? 它和AspectJ有什么区别?
    C#/.NET学习值得推荐的在线论坛和技术社区
    分布式数据库(笔记)
    SAP 限制物料类型在BOM组件中简介
    基本分段存储管理方式(分段,段表,地址转换以及与分页管理对比)
    C++仿函数真好用
    [毕业设计源码]基于微信小程序的校园二手交易系统
    第六章 Scala if..else与循环
  • 原文地址:https://blog.csdn.net/weixin_42038527/article/details/136272582