Transformer代码复现
提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档
文章目录
前言
提示:这里可以添加本文要记录的大概内容:
提示:以下是本篇文章正文内容,下面案例可供参考
一、输入部分的实现
1.输⼊部分包含:
源⽂本嵌⼊层及其位置编码器
⽬标⽂本嵌⼊层及其位置编码器
⽂本嵌⼊层的代码分析:
# 导入必备的工具包
import torch
# 预定义的网络层torch.nn,工具开发者已经帮助我们开发好的一些常用层,
# 比如,卷积层,lstm层,embedding层等,不需要我们再重新造轮子.
import torch.nn as nn
# 数学计算工具包
import math
# torch中变量封装函数Variable.
from torch.autograd import Variable
# 定义Embeddings类来实现文本嵌入层,这里s说明代表两个一模一样的嵌入层,他们共享参数.
# 该类继承nn.Module,这样就有标准层的一些功能,这里我们也可以理解为一种模式,我们自己实现的所有层都会这样去写.
class Embeddings(nn.Module):
def __init__(self, d_model, vocab):
"""类的初始化函数,有两个参数,d_model:指词嵌入的维度,vocab:指词表的大小."""
# 接着就是使用super的方式指明继承nn.Module的初始化函数,我们自己实现的所有层都会这样去写.
super(Embeddings, self).__init__()
# 之后就是调用nn中的预定义层Embedding,获得一个词嵌入对象self.lut
self.lut = nn.Embedding(vocab, d_model)
# 最后就是将d_model传入类中
self.d_model = d_model
def forward(self, x):
""" 参数x:因为Embedding层是首层,所以代表输入给模型的文本通过词汇映射后的张量"""
# 将x传给self.lut并与根号下self.d_model相乘作为结果返回
return self.lut(x) * math.sqrt(self.d_model)
位置编码器的代码分析:
# 定义位置编码器类, 我们同样把它看做⼀个层, 因此会继承nn.Module
class PositionalEncoding(nn.Module):
def __init__(self, d_model, dropout, max_len=5000):
super(PositionalEncoding, self).__init__()
self.dropout = nn.Dropout(p=dropout)
pe = torch.zeros(max_len, d_model)
position = torch.arange(0, max_len).unsqueeze(1)
# 绝对位置矩阵初始化之后,接下来就是考虑如何将这些位置信息加⼊到位置编码矩阵中,
# 最简单思路就是先将max_len x 1的绝对位置矩阵, 变换成max_len x d_model形状,然后覆盖原来的初始位置编码矩阵即可,
# 要做这种矩阵变换,就需要⼀个1xd_model形状的变换矩阵div_term,我们对这个变换矩阵的要求除了形状外,
# 还希望它能够将⾃然数的绝对位置编码缩放成⾜够⼩的数字,有助于在之后的梯度下降过程中更快的收敛. 这样我们就可以开始初始化这个变换矩阵了.
# ⽽是有了⼀个跳跃,只初始化了⼀半即1xd_model/2 的矩阵。 为什么是⼀半呢,其实这⾥并不是真正意义上的初始化了⼀半的矩阵,
# 我们可以把它看作是初始化了两次,⽽每次初始化的变换矩阵会做不同的处理,第⼀次初始化的变换矩阵分布在正弦波上, 第⼆次初始化的变换矩阵分布在余弦波上,
# 并把这两个矩阵分别填充在位置编码矩阵的偶数和奇数位置上,组成最终的位置编码矩
阵.
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)
# 这样我们就得到了位置编码矩阵pe, pe现在还只是⼀个⼆维矩阵,要想和embedding的输出(⼀个三维张量)相加,
# 就必须拓展⼀个维度,所以这⾥使⽤unsqueeze拓展维度.
pe = pe.unsqueeze(0)
self.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.编码器部分包含:
由N个编码器层堆叠⽽成
每个编码器层由两个⼦层连接结构组成
第⼀个⼦层连接结构包括⼀个多头⾃注意⼒⼦层和规范化层以及⼀个残差连接
第⼆个⼦层连接结构包括⼀个前馈全连接⼦层和规范化层以及⼀个残差连接
多头注意⼒机制的代码分析
class MultiHeadedAttention(nn.Module):
def __init__(self, head, embedding_dim, dropout=0.1):
"""
在类的初始化时,会传入三个参数,
head 代表头数,
embedding_dim 代表词嵌入的维度,
dropout 代表进行 dropout 操作时置 0 比率,默认是 0.1。
"""
super(MultiHeadedAttention, self).__init__()
# 在函数中,首先使用了一个测试中常用的 assert 语句,判断 head 是否能被 embedding_dim 整除,
# 这是因为我们之后要给每个头分配等量的词特征,也就是 embedding_dim / head 个。
assert embedding_dim % head == 0
# 得到每个头获得的分割词向量维度 d_k
self.d_k = embedding_dim // head
# 传入头数 head
self.head = head
# 然后获得线性层对象,通过 nn 的 Linear 实例化,它的内部变换矩阵是 embedding_dim x embedding_dim,
# 然后使用 clones 函数克隆四个,
# 为什么是四个呢,这是因为在多头注意力中,Q,K,V 各需要一个,最后拼接的矩阵还需要一个,因此一共是四个。
self.linears = clones(nn.Linear(embedding_dim, embedding_dim), 4)
# self.attn 为 None,它代表最后得到的注意力张量,现在还没有结果所以为 None。
self.attn = None
# 最后就是一个 self.dropout 对象,它通过 nn 中的 Dropout 实例化而来,置 0 比率为传进来的参数 dropout。
self.dropout = nn.Dropout(p=dropout)
def forward(self, query, key, value, mask=None):
"""
前向逻辑函数,它的输入参数有四个,前三个就是注意力机制需要的 Q, K, V,
最后一个是注意力机制中可能需要的 mask 掩码张量,默认是 None。
"""
# 如果存在掩码张量 mask
if mask is not None:
# 使用 unsqueeze 拓展维度
mask = mask.unsqueeze(0)
# 接着,我们获得一个 batch_size 的变量,它是 query 尺寸的第 1 个数字,代表有多少条样本。
batch_size = query.size(0)
# 之后就进入多头处理环节
# 首先利用 zip 将输入 QKV 与三个线性层组到一起,然后使用 for 循环,将输入 QKV 分别传到线性层中,
# 做完线性变换后,开始为每个头分割输入,这里使用 view 方法对线性变换的结果进行维度重塑,多加了一个维度 head,
# 这样就意味着每个头可以获得一部分词特征组成的句子,其中的 -1 代表自适应维度,
# 计算机会根据这种变换自动计算这里的值。然后对第二维和第三维进行转置操作,
# 为了让代表句子长度维度和词向量维度能够相邻,这样注意力机制才能找到词义与句子位置的关系,
# 从 attention 函数中可以看到,利用的是原始输入的倒数第一和第二维。这样我们就得到了每个头的输入。
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))
]
# 得到每个头的输入后,接下来就是将他们传入到 attention 中,
# 这里直接调用我们之前实现的 attention 函数。同时也将 mask 和 dropout 传入其中。
x, self.attn = attention(query, key, value, mask=mask, dropout=self.dropout)
# 通过多头注意力计算后,我们就得到了每个头计算结果组成的 4 维张量,我们需要将其转换为输入的形状以方便后续的计算,
# 因此这里开始进行第一步处理环节的逆操作,先对第二和第三维进行转置,然后使用 contiguous 方法,
# 这个方法的作用就是能够让转置后的张量应用 view 方法,否则将无法直接使用,
# 所以,下一步就是使用 view 重塑形状,变成和输入形状相同。
x = x.transpose(1, 2).contiguous().view(batch_size, -1, self.head * self.d_k)
# 最后使用线性层列表中的最后一个线性层对输入进行线性变换得到最终的多头注意力结构的输出。
return self.linears[-1](x)
前馈全连接层的代码分析
class PositionwiseFeedForward(nn.Module):
def __init__(self, d_model, d_ff, dropout=0.1):
"""初始化函数有三个输入参数分别是d_model,d_ff,和dropout=0.1,第一个是线性
层的输入维度也是第二个线性层的输出维度,
因为我们希望输入通过前馈全连接层后输入和输出的维度不变。第二个参数d_ff就是第
二个线性层的输入维度和第一个线性层的输出维度。
最后一个是dropout置0比率。"""
super(PositionwiseFeedForward, self).__init__()
# 首先按照我们预期使用nn实例化了两个线性层对象,self.w1和self.w2
# 它们的参数分别是d_model,d_ff和d_ff,d_model
self.w1 = nn.Linear(d_model, d_ff)
self.w2 = nn.Linear(d_ff, d_model)
# 然后使用nn的Dropout实例化了对象self.dropout
self.dropout = nn.Dropout(dropout)
def forward(self, x):
"""输入参数为x,代表来自上一层的输出"""
# 首先经过第一个线性层,然后使用Functional中relu函数进行激活,
# 之后再使用dropout进行随机置0,最后通过第二个线性层w2,返回最终结果。
return self.w2(self.dropout(F.relu(self.w1(x))))
规范化层的代码实现
class LayerNorm(nn.Module):
def __init__(self, features, eps=1e-6):
"""初始化函数有两个参数,一个是features,表示词嵌入的维度,
另一个是eps它是一个足够小的数,在规范化公式的分母中出现,
防止分母为0.默认是1e-6."""
super(LayerNorm, self).__init__()
# 根据features的形状初始化两个参数张量a2, 和b2,第一个初始化为1张量,
# 也就是里面的元素都是1,第二个初始化为0张量,也就是里面的元素都是0,这两个张量
# 就是规范化层的参数,
# 因为直接对上一层得到的结果做规范化公式计算,将改变结果的正常表征,因此就需要有
# 参数作为调节因子,
# 使其即能满足规范化要求,又能不改变针对目标的表征.最后使用nn.parameter封装,
# 代表他们是模型的参数。
self.a2 = nn.Parameter(torch.ones(features))
self.b2 = nn.Parameter(torch.zeros(features))
# 把eps传到类中
self.eps = eps
def forward(self, x):
"""输入参数x代表来自上一层的输出"""
# 在函数中,首先对输入变量x求其最后一个维度的均值,并保持输出维度与输入维度一致.
# 接着再求最后一个维度的标准差,然后就是根据规范化公式,用x减去均值除以标准差获
# 得规范化的结果,
# 最后对结果乘以我们的缩放参数,即a2,*号代表同型点乘,即对应位置进行乘法操作,
# 加上位移参数b2.返回即可.
mean = x.mean(-1, keepdim=True)
std = x.std(-1, keepdim=True)
return self.a2 * (x - mean) / (std + self.eps) + self.b2
三、解码器部分实现
1.由N个解码器层堆叠⽽成
每个解码器层由三个⼦层连接结构组成
第⼀个⼦层连接结构包括⼀个多头⾃注意⼒⼦层和规范化层以及⼀个残差连接
第⼆个⼦层连接结构包括⼀个多头注意⼒⼦层和规范化层以及⼀个残差连接
第三个⼦层连接结构包括⼀个前馈全连接⼦层和规范化层以及⼀个残差连接
四、输出实现
1.输出部分包含:
线性层
softmax层
总结
学习了Transformer模型的作⽤:
基于seq2seq架构的transformer模型可以完成NLP领域研究的典型任务, 如机器翻译, 文本⽣成等. 同时⼜可以构建预训练语⾔模型,⽤于不同任务的迁移学习.