参考
https://www.ylkz.life/deeplearning/p12158901/
https://zhuanlan.zhihu.com/p/396221959
模型结构
Input Embedding
将文本中词汇的数字表示转变为向量表示, 希望得到其在高维空间中的特征表示向量。
# 导入必备的工具包
import torch
import torch.nn as nn
import math
from torch.autograd import Variable# 定义Embeddings类来实现文本嵌入层,这里s说明代表两个一模一样的嵌入层, 他们共享参数.
class Embeddings(nn.Module):def __init__(self, d_model, vocab):"""类的初始化函数,有两个参数, d_model: 指词嵌入的维度, vocab: 指词表的大小"""super(Embeddings, self).__init__()# 调用nn中的预定义层Embedding, 获得一个词嵌入对象self.lutself.lut = nn.Embedding(vocab, d_model)# 将d_model传入类中self.d_model = d_modeldef forward(self, x):# 将x传给self.lut并与根号下self.d_model相乘作为结果返回return self.lut(x) * math.sqrt(self.d_model)
Positional Encoding
在Transformer的编码器结构中, 并没有针对词汇位置信息的处理,因此需要在Embedding层后加入位置编码器,将词汇位置不同可能会产生不同语义的信息加入到词嵌入张量中, 以弥补位置信息的缺失.
# 定义位置编码器类
class PositionalEncoding(nn.Module):def __init__(self, d_model, dropout, max_len=5000):"""位置编码器类的初始化函数, 共有三个参数分别是d_model: 词嵌入维度 dropout: 置0比率max_len: 每个句子的最大长度"""super(PositionalEncoding, self).__init__()# 实例化nn中预定义的Dropout层, 并将dropout传入其中, 获得对象self.dropoutself.dropout = nn.Dropout(p=dropout)# 初始化一个位置编码矩阵, 它是一个0阵,矩阵的大小是max_len x d_model.pe = torch.zeros(max_len, d_model)# 初始化一个绝对位置矩阵, 在这里,词汇的绝对位置用它的索引表示. # 首先使用arange方法获得一个连续自然数向量,然后再使用unsqueeze方法拓展向量维度使其成为矩阵, position = torch.arange(0, max_len).unsqueeze(1)# 绝对位置矩阵初始化之后,接下来就是考虑如何将这些位置信息加入到位置编码矩阵中,# 最简单思路就是先将max_len x 1的绝对位置矩阵, 变换成max_len x d_model形状,然后覆盖原来的初始位置编码矩阵即可, # 要做这种矩阵变换,就需要一个1xd_model形状的变换矩阵div_term,我们对这个变换矩阵的要求除了形状外,# 还希望它能够将自然数的绝对位置编码缩放成足够小的数字,有助于在之后的梯度下降过程中更快的收敛. 这样我们就可以开始初始化这个变换矩阵了 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)# 使用unsqueeze拓展维度.pe = pe.unsqueeze(0)# 最后把pe位置编码矩阵注册成模型的bufferself.register_buffer('pe', pe)def forward(self, x):"""forward函数的参数是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)# 最后使用self.dropout对象进行'丢弃'操作, 并返回结果.return self.dropout(x)
掩码张量
只有1和0的元素,代表位置被遮掩或者不被遮掩,至于是0位置被遮掩还是1位置被遮掩可以自定义,因此它的作用就是让另外一个张量中的一些数值被遮掩,也可以说被替换, 它的表现形式是一个张量
在transformer中, 掩码张量的主要作用在应用attention,有一些生成的attention张量中的值计算有可能已知了未来信息而得到的,未来信息被看到是因为训练时会把整个输出结果都一次性进行Embedding,但是理论上解码器的的输出却不是一次就能产生最终结果的,而是一次次通过上一次结果综合得出的,因此,未来的信息可能被提前利用. 所以,需要进行遮掩
原理实现
>>> atten_data=torch.tensor([[4,2,3,4,5],[6,7,8,9,10],[11,12,13,14,15],[16,17,18,19,20]])
>>> data
tensor([[ 4, 2, 3, 4, 5],[ 6, 7, 8, 9, 10],[11, 12, 13, 14, 15],[16, 17, 18, 19, 20]])>>> mask=np.triu([[1,1,1,1,1],[1,1,1,1,1],[1,1,1,1,1],[1,1,1,1,1]],k=1)
>>> mask=1-mask
>>> mask
array([[1, 0, 0, 0, 0],[1, 1, 0, 0, 0],[1, 1, 1, 0, 0],[1, 1, 1, 1, 0]])>>> data=data.masked_fill(mask==0,-1e9)
>>> data
tensor([[ 4, -1000000000, -1000000000, -1000000000, -1000000000],[ 6, 7, -1000000000, -1000000000, -1000000000],[ 11, 12, 13, -1000000000, -1000000000],[ 16, 17, 18, 19, -1000000000]])
transformer中
多层Transformer
在实际使用种常常使用多层transformer结构(原论文6层)
在多层Transformer中,多层编码器先对输入序列进行编码,然后得到最后一个Encoder的输出Memory;解码器先通过Masked Multi-Head Attention对输入序列进行编码,然后将输出结果同Memory通过Encoder-Decoder Attention后得到第1层解码器的输出;接着再将第1层Decoder的输出通过Masked Multi-Head Attention进行编码,接着将编码后的结果同Memory通过Encoder-Decoder Attention后得到第2层解码器的输出,以此类推得到最后一个Decoder的输出。
值得注意的是,在多层Transformer的解码过程中,每一个Decoder在Encoder-Decoder Attention中所使用的Memory均是同一个。