Transformer学习记录:输入层

229 阅读7分钟

初识Transformer

Transformer模型的作⽤:基于seq2seq架构的transformer模型可以完成NLP领域研究的典型任务, 如机器翻译, ⽂本⽣成等,同时⼜可以构建预训练语⾔模型,⽤于不同任务的迁移学习。

Transformer总体架构图(图片来源:网络) image.png

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

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

输⼊部分包含:

  • 源⽂本嵌⼊层及其位置编码器
  • ⽬标⽂本嵌⼊层及其位置编码器

image.png

输出部分包含:

  • 线性层
  • softmax层

image.png

编码器部分:

  • 由N个编码器层堆叠⽽成
  • 每个编码器层由两个⼦层连接结构组成
  • 第⼀个⼦层连接结构包括⼀个多头⾃注意⼒⼦层和规范化层以及⼀个残差连接
  • 第⼆个⼦层连接结构包括⼀个前馈全连接⼦层和规范化层以及⼀个残差连接

image.png

解码器部分:

  • 每个解码器层由三个⼦层连接结构组成
  • 第⼀个⼦层连接结构包括⼀个多头⾃注意⼒⼦层和规范化层以及⼀个残差连接
  • 第⼆个⼦层连接结构包括⼀个多头注意⼒⼦层和规范化层以及⼀个残差连接
  • 第三个⼦层连接结构包括⼀个前馈全连接⼦层和规范化层以及⼀个残差连接

image.png

输入部分实现

输⼊部分包含:

  • 源⽂本嵌⼊层及其位置编码器
  • ⽬标⽂本嵌⼊层及其位置编码器

文本嵌入的作用:⽆论是源⽂本嵌⼊还是⽬标⽂本嵌⼊,都是为了将⽂本中词汇的数字表示转变为向量表示, 希望在这样的⾼维空间捕捉词汇间的关系。

简单实现:

class Embeddings(nn.Module):
    def __init__(self, d_model, vocab):
        """
        @param d_model: 词嵌入的维度
        @param vocab:词表大小
        """
        super(Embeddings, self).__init__()
        # 定义Embedding层
        self.lut = nn.Embedding(vocab, d_model)
        self.d_model = d_model

    def forward(self, x):
        """
        @param x: 输入进模型的文本通过词汇映射后的数字张量
        """
        return self.lut(x) * math.sqrt(self.d_model)
# 词嵌入维度是512维
d_model = 512
# 词表大小是1000
vocab = 1000
x = torch.LongTensor([[100,2,421,508],[491,998,1,221]])
emb = Embeddings(d_model, vocab)
embr = emb(x)
print("embr:", embr)
print("embr:", embr.shape)

image.png

三维数组理解:假设你有一个书架,书架上有两层(即第一个维度为2),每层有4本书(第二个维度为4),每本书有512页(第三个维度为512)。这样,我们就可以用 [2, 4, 512] 来描述这个书架上的书籍布局。

四维数组理解:假设你有一个四维张量 [8, 3, 128, 128],可以这样理解:

  • 8:表示批处理中有8张图像。
  • 3:表示每张图像是彩色图像,有3个通道(RGB)。
  • 128:表示每张图像的高度为128像素。
  • 128:表示每张图像的宽度为128像素。

五位数组理解:假设你有一个五维张量 [8, 10, 3, 128, 128],可以这样理解:

  • 8:表示批处理中有8个样本。
  • 10:表示每个样本有10个时间步(例如,每段视频有10帧)。
  • 3:表示每帧图像是彩色图像,有3个通道(RGB)。
  • 128:表示每帧图像的高度为128像素。
  • 128:表示每帧图像的宽度为128像素。

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

简单实现:

class PositionalEncoding(nn.Module):
    def __init__(self, d_model, dropout, max_len=5000):
    """
    @param d_model: 词嵌⼊维度
    @param dropout: 置0⽐率,
    @param max_len: 句子最大长度,默认为5000
    """
    super(PositionalEncoding, self).__init__()
    self.dropout = nn.Dropout(p=dropout)
    初始化⼀个位置编码矩阵, 它是⼀个0阵,矩阵的⼤⼩是max_len x d_model.
    pe = torch.zeros(max_len, d_model)
    # 初始化⼀个绝对位置矩阵, 在我们这⾥,词汇的绝对位置就是⽤它的索引去表示.
    # 所以我们⾸先使⽤arange⽅法获得⼀个连续⾃然数向量,然后再使⽤unsqueeze⽅法拓展向量维度使其成为矩阵,
    # ⼜因为参数传的是1,代表矩阵拓展的位置,会使向量变成⼀个max_len x 1 的矩阵,
    positon = torch.arange(0, max_len).unsqueeze(1)
    # 绝对位置矩阵初始化之后,接下来就是考虑如何将这些位置信息加⼊到位置编码矩阵中,
    # 最简单思路就是先将max_len x 1的绝对位置矩阵, 变换成max_len x d_model形状,然后覆盖原来的初始位置编码矩阵即可,
    # 要做这种矩阵变换,就需要⼀个1xd_model形状的变换矩阵div_term,我们对这个变换矩阵的要求除了形状外,
    # 还希望它能够将⾃然数的绝对位置编码缩放成⾜够⼩的数字,有助于在之后的梯度下降过程中更快的收敛. 这样我们就可以开始初始化这个变换矩阵了.
    # ⾸先使⽤arange获得⼀个⾃然数矩阵, 我们这⾥并没有按照预计的⼀样初始化⼀个1xd_model的矩阵,
    # ⽽是有了⼀个跳跃,只初始化了⼀半即1xd_model/2 的矩阵。 为什么是⼀半呢,其实这⾥并不是真正意义上的初始化了⼀半的矩阵,
    # 我们可以把它看作是初始化了两次,⽽每次初始化的变换矩阵会做不同的处理,第⼀次初始化的变换矩阵分布在正弦波上, 第⼆次初始化的变换矩阵分布在余弦波上,
    # 并把这两个矩阵分别填充在位置编码矩阵的偶数和奇数位置上,组成最终的位置编码矩阵
    div_term = torch.exp(torch.arange(0, d_model, 2) * -(math.log(10000.0) / d_model))
    pe[:, 0::2] = torch.sin(positon * div_term)
    pe[:, 1::2] = torch.cos(positon * div_term)
    # 这样我们就得到了位置编码矩阵pe, pe现在还只是⼀个⼆维矩阵,要想和embedding的输出(⼀个三维张量)相加,
    # 就必须拓展⼀个维度,所以这⾥使⽤unsqueeze拓展维度.
    pe = pe.unsqueeze(0)
    拓展后的维度变成了[1,max_len,d_model]
    '''
    向模块添加持久缓冲区。
    这通常用于注册不应被视为模型参数的缓冲区。例如,pe不是一个参数,而是持久状态的一部分。
    缓冲区可以使用给定的名称作为属性访问。
    说明:
    应该就是在内存中定义一个常量,同时,模型保存和加载的时候可以写入和读出
    '''
    self.register_buffer('pe', pe)
    #forward函数的参数是x, 表示⽂本序列的词嵌⼊表示
    def forward(self, x):
    # 在相加之前我们对pe做⼀些适配⼯作, 将这个三维张量的第⼆维也就是句⼦最⼤⻓度的那⼀维将切⽚到与输⼊的x的第⼆维相同即x.size(1),
    # 因为我们默认max_len为5000⼀般来讲实在太⼤了,很难有⼀条句⼦包含5000个词汇,所以要进⾏与输⼊张量的适配.
    # 最后使⽤Variable进⾏封装,使其与x的样式相同,但是它是不需要进⾏梯度求解的,因此把requires_grad设置成false.
        x = x + Variable(self.pe[:, :x.size(1)], requires_grad=False)
        return self.dropout(x)

unsqueeze函数:

x = torch.tensor([1, 2, 3, 4])
print(torch.unsqueeze(x, 0))
print(torch.unsqueeze(x, 0).shape)
print(torch.unsqueeze(x, 1))
print(torch.unsqueeze(x, 1).shape)

image.png

嵌入图示理解: 绘制词汇向量中特征的分布曲线

import matplotlib.pyplot as plt
import numpy as np

plt.figure(figsize=(15, 5))
pe = PositionalEncoding(20, 0)
y = pe(Variable(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]])
plt.show()

查看4、5、6、7列的词汇向量分布曲线,奇数列图像正弦分布,偶数列图像预先分布,符合位置嵌入的位置编码矩阵的策略 image.png