【2】Transformer网络结构和模型代码详解(超详细)(Pytorch)

456 阅读22分钟

简介:仅个人学习总结。

1. Transformer结构

image.png

论文中encoder层由6个encoder(即N=6)堆叠在一起,decoder层也是6个叠加在一起。注意事项:每个encoder和decoder层只是架构一样,其参数的具体的值不一样,不能说每个encoder和decoder完全一样。

image.png

Transformer总体架构分为四个部分: 1.输入部分; 2.输出部分; 3.编码器部分; 4.解码器部分。

1.1 输入

1)输入部分包含:

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

image.png

### 1.1.1 位置编码 由于transformer并行运算所以输入的信息中没有位置信息,而在语音文本中,绝大部分都是有语序的。例如:我爱你,要是翻译成’“You love me”,那意思就完全不一样了。于是,就有了位置编码,其作用就是让输入数据携带位置信息,让模型能够找出位置特点。

位置编码的做法如下: 位置向量.jpg

Step1: embedding编码

假设embedding编码将每个词向量编程成512维,如图所示:

image.png

如上,如果有规定每次输入的x的长度,那么不足就直接使用padding用0填充。

Step2:  位置编码

位置编码公式为:

PE(pos,2i)=sin(pos/100002i/dmodel)PE(pos,2i+1)=cos(pos/100002i/dmodel)P E_{\left(p o s,2i\right)}=s i n\left(p o s/10000^{2i/d_{m o d e l}}\right)\\ P E_{\left(p o s,2i+1\right)}=c o s\left(p o s/10000^{2i/d_{m o d e l}}\right)
1/100002i/dmodel=elog100002i/dmodel=e2i/dmodellog1000=e2i(log1000/dmodel)1/10000^{2i/\mathrm{d_{model}}}=\mathrm{e}^{\log^{10000^{-2i/\mathrm{d_{model}}}}}=\mathrm{e}^{-2i/\mathrm{d_{model}}*\log^{1000}}=\mathrm{e}^{2i*(-\log^{1000}/\mathrm{d}_{model})}

其中pos是指当前词在句子中的位置,2i\2i+1是指向量维度的index,可以看出,在偶数位置,使用正弦编码,在奇数位置,使用余弦编码。例如对于“我爱你”中的“爱”位置编码如下:

image.png

***Step3:*  位置编码与embedding编码进行相加**

如图:

image.png

位置编码原理:

借助三角函数的积化和差公式可以将位置编码公式化为:

{PE(pos+k;2i)=PE(pos,2i)×PE(k,2i+1)+PE(pos,2i+1)×PE(k,2i)PE(pos+k,2i+1)=PE(pos,2i+1)×PE(k,2i+1)PE(pos,2i)×PE(k,2i)\begin{cases}PE(pos+k;2i)=PE(pos,2i)\times PE(k,2i+1)+PE(pos,2i+1)\times PE(k,2i)\\ PE(pos+k,2i+1)=PE(pos,2i+1)\times PE(k,2i+1)-PE(pos,2i)\times PE(k,2i)\end{cases}

对于“我爱你”,“你”的pos+k=2即(pos,k)可以取(0,2),(1,1),(2,0)的组合,我个人理解有两种:

  • pos表示位置,k表示距离。“我”的pos是0,在“你”的前两(k值)位;“爱”的pos为1,在“你”的前一位(即k=1);(2,0)表示自己的位置。
  • pos、k都表示位置

通过公式对于pos+k位置的位置向量某一维2i或者2i+1而言,可以表示为,pos位置与k位置的位置向量的2i与2i+1维的线性组合。这中线性组合意味着位置向量蕴含了相对位置的信息

代码:

# _*_ coding=utf-8_*_  
  
import math  
import torch  
import torch.nn as nn  
import numpy as np  
from torch.autograd import Variable  
import matplotlib.pyplot as plt  
  
  
class Embeddings(nn.Module):  
def __init__(self, d_model: int, vocab: int):  
super(Embeddings, self).__init__()  
"""  
d_model:词嵌入维度  
vocab:词表的大小  
"""  
# 将参数传入类中  
self.d_model = d_model  
# 定义Embedding层  
self.lut = nn.Embedding(vocab, d_model)  
  
def forward(self, x):  
"""  
向前传播,当传入该类实例化对象参数时,自动调用该类函数  
参数x:代表输入给模型文本通过词汇映射后的张量  
"""  
output = self.lut(x) * math.sqrt(self.d_model)  
return output  
  
  
# 构建位置编码的类  
class PositionalEncoding(nn.Module):  
def __init__(self, d_model: int, dropout: float, max_len: int = 5000):  
"""  
d_model:词嵌入维度  
dropout:代表Dropout层的置0比率  
max_len:每个句子的最大长度  
"""  
super(PositionalEncoding, self).__init__()  
# 实例化Dropout层  
self.dropout = nn.Dropout(p=dropout)  
  
# 初始化一个位置编码矩阵,大小是(max_len, d_model)  
pe = torch.zeros(max_len, d_model)  
# 初始化一个绝对位置矩阵,max_len*1  
position = torch.arange(0, max_len).unsqueeze(1)  
# 定义一个变换矩阵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)  
# 将二维张量扩充至三维张量  
pe = pe.unsqueeze(0)  
# 将位置编码矩阵注册成模型的buffer,这个buffer不是模型的参数,不跟随优化器同步更新  
# 注册成buffer后我们可以在模型保存后重新加载时,将这个位置编码器和模型参数一同记载  
  
self.register_buffer('pe', pe)  
  
def forward(self, x):  
"""  
x:本文序列词嵌入向量  
首先明确的pe编码太长了。将第二维度,也就是max_len对应的维度缩小x的句子同等长度  
"""  
x = x + Variable(self.pe[:, :x.size(1)], requires_grad=False)  
return self.dropout(x)  
  
  
if __name__ == '__main__':  
plt.figure(figsize=(15, 5))  
pe = PositionalEncoding(20, 0)  
x = pe(Variable(torch.zeros(1, 100, 20))) # 相当于只有位置编码  
plt.plot(np.arange(100), x[0, :, 4: 8].data.numpy())  
plt.legend(["dim %d" % p for p in range(4, 8)])  
plt.show()

结果:

Figure_1.png

表明:维度越高,其频率会降低,从公式也可以看出来。

代码简化:

class DataPreprocessing(nn.Module):  
def __init__(self, d_model: int, vocab: int, dropout: float, max_len: int = 5000):  
super(DataPreprocessing, self).__init__()  
self.d_model = d_model  
self.vocab = vocab  
self.dropout = dropout  
self.max_len = max_len  
  
self.dropout = nn.Dropout(p=self.dropout) # 实例化Dropout层  
self.lut = nn.Embedding(vocab, d_model) # 定义Embedding层  
  
pe = torch.zeros(self.max_len, self.d_model) # 位置编码矩阵:(max_len, d_model)  
position = torch.arange(0, self.max_len).unsqueeze(1) # 绝对位置矩阵, max_len*1  
div_len = torch.exp(torch.arange(0, self.d_model, 2) * -(math.log(10000.0) / self.d_model)) # 变换矩阵div_len  
# 将变换矩阵进行奇数,偶数分别赋值  
pe[:, 0::2] = torch.sin(position * div_len)  
pe[:, 1::2] = torch.cos(position * div_len)  
pe = pe.unsqueeze(0)  
# 将位置编码矩阵注册成模型的buffer,这个buffer不是模型的参数,不跟随优化器同步更新  
# 注册成buffer后我们可以在模型保存后重新加载时,将这个位置编码器和模型参数一同记载  
self.register_buffer('pe', pe)  
  
def forward(self, x):  
embedding_output = self.lut(x) * math.sqrt(self.d_model)  
output = embedding_output + self.pe[:, :embedding_output.size(1)]  
return self.dropout(output)

1.2 输出部分

输出部分包含:

  • 线形层
  • softmax层

image.png

Softmax函数可以将上一层的原始数据进行归一化,转化为一个(0,1)之间的数值,这些数值可以被当做概率分布,用来作为多分类的目标预测值。Softmax函数一般作为神经网络的最后一层,接受来自上一层网络的输入值,然后将其转化为概率。softmax函数:

Softmax(zi)=exp(zi)jexp(zj)\operatorname{Softmax}(z_i)=\frac{\exp(z_i)}{\sum_j\exp(z_j)}

指数在x轴正轴爆炸式地快速增长,如果zi z_{i}比较大,exp(zi)exp⁡( z_{i})也会非常大,得到的数值可能会溢出。溢出又分为下溢出(Underflow)和上溢出(Overflow)。计算机用一定长度的二进制表示数值,数值又被称为浮点数。当数值过小的时候,被四舍五入为0,这就是下溢出;当数值过大,超出了最大界限,就是上溢出。

会报错:


def softmax(x):
    return np.exp(x) / np.sum(np.exp(x), axis=0)

b = np.array([20, 300, 5000])
softmax(b)

RuntimeWarning: overflow encountered in exp return np.exp(x) / np.sum(np.exp(x), axis=0)

解决办法:

一个简单的办法是,先求得输入向量的最大值,然后所有向量都减去这个最大值:

M=max(z)M=max(z)
Softmax(zi)=exp(ziM)jexp(zjM)\operatorname{Softmax}(z_i)=\frac{\exp(z_i-M)}{\sum_j\exp(z_j-M)}

下图中,我们可以看到,Softmax将一个[2.0,1.0,0.1]的向量转化为了[0.7,0.2,0.1],而且各项之和为1。

image.png

当decoder层全部执行完毕后,在结尾再添加一个全连接层和softmax层,就可以把得到的向量映射为我们需要的词,假如我们的词典是1w个词,那最终softmax会输入1w个词的概率,概率值最大的对应的词就是我们最终的结果。

image.png

1.3 编码器和解码器部分

编码器部分:

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

image.png

解码器部分:

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

image.png

1.3.1 Self-Attention

公式:

Attention(Q,K,V)=softmax(QKTdk)V\operatorname{Attention}(Q,K,V)=\operatorname{softmax}(\frac{QK^T}{\sqrt{d_k}})V
  1. 首先,self-attention会计算出三个新的向量,在论文中,向量的维度是512维,我们把这三个向量分别称为Query、Key、Value,这三个向量是用embedding向量与一个矩阵相乘得到的结果,这个矩阵是随机初始化的,维度为(64,512)注意第二个维度需要和embedding的维度一样,其值在BP的过程中会一直进行更新,得到的这三个向量的维度是64。

image.png

  1. 计算self-attention的分数值,该分数值决定了当我们在某个位置encode一个词时,对输入句子的其他部分的关注程度。这个分数值的计算方法是Query与Key做点成,以下图为例,首先我们需要针对Thinking这个词,计算出其他词对于该词的一个分数值,首先是针对于自己本身即q1·k1,然后是针对于第二个词即q1·k2。

image.png

  1. 接下来,把点成的结果除以一个常数,这里我们除以8,这个值一般是采用上文提到的矩阵的第一个维度的开方即64的开方8,当然也可以选择其他的值,然后把得到的结果做一个softmax的计算。得到的结果即是每个词对于当前位置的词的相关性大小,当然,当前位置的词相关性肯定会会很大。

image.png

  1. 下一步就是把Value和softmax得到的值进行相乘,并相加,得到的结果即是self-attetion在当前节点的值。

image.png

这种通过 query 和 key 的相似性程度来确定 value 的权重分布的方法被称为scaled dot-product attention。可以看到 Scaled Dot-Product Attention 有个缩放因子dk\sqrt{d_{k}},为什么要加这个缩放因子呢?

如果dkd_k 很小, additive attention 和 dot-product attention 相差不大。
但是如果 dkd_k  很大,点乘的值很大,如果不做 scaling,结果就没有 additive attention 好。
另外,点乘结果过大,使得经过 softmax 之后的梯度很小,不利于反向传播,所以对结果进行 scaling。

image.png

image.png

1.3.2 Multi-Headed Attention

image.png

论文中表明,将模型分为多个头,形成多个子空间,可以让模型去关注不同方面的信息。上图中Multi-Head Attention 就是将 Scaled Dot-Product Attention 过程做 H 次,再把输出合并起来。

多头注意力机制的公式如下:

Qi=QWiQ,Ki=KWiK,Vi=VWiV,i=1,,8headi=Attention(Qi,Ki,Vi),i=1,,8MultiHead(Q,K,V)=Concact(head1,,head8)WO\begin{array}{l}Q_i=QW_i^Q,K_i=KW_i^K,V_i=VW_i^V,i=1,\ldots,8\\ \\ head_i=Attention(Q_i,K_i,V_i),i=1,\ldots,8\\ \\Multi Head(Q,K,V)=Concact(head_1,\ldots,head_8)W^O\end{array}

这里,我们假设

Q,K,VR512,WiQ,WiK,WiVR512×64,WOR512×512,headiR64Q,K,V\in R^{512},W_{i}^{Q},W_{i}^{K},W_{i}^{V}\in R^{512\times64},W^{O}\in R^{512\times512},h e a d_{i}\in R^{64}

① 输入句子 “tinking machine”

② 将句子进行 Tokenize 转换成 Word Embedding X

③ 将 X 切分成 8 份,并与权重WiW_i相乘,构成输入向量 WiXW_iX,形成 Qi,Ki,Vi,i=1,...,8Q_i,K_i,V_i,i=1,...,8

④ 计算 Attention 权重矩阵,zi=softmax(QiKiT/dk)Viz_i=softmax(Q_iK_i^T/\sqrt{d_k})V_i,最后将每个ziz_i合并形成ZiZ_i

⑤ 最后将 8 个头的结果 Zii,i=1,...,8Z_ii,i=1,...,8合并ZC=concact(Z1,...,Z8)Z_C=concact(Z_1,...,Z_8),点乘权重WOW_O,形成Z=ZCWOZ=Z_CW_O

图解:

  1. Linear。对一则广告标题Thinking Machines进行Multi-Head Attention计算,下图中矩阵X的第一行表示Thinking的词向量,第二行表示Machines的词向量。词向量维度为4。将X分别乘训练矩阵(维度4 * 3) W0Q,W0K,W0VW_0^Q,W_0^K,W_0^V得到(维度2*3)Q0.K0,V0Q_{0}.K_{0},V_{0},同理得到Q1.K1,V1Q_{1}.K_{1},V_{1}

image.png

  1. Scaled Dot-Product Attention。每个Head都进行attention计算,由于有8头attention,会得到8个不同的矩阵(维度2*3)。softmax对矩阵的每一行进行作用。(scale dot attention)

image.png

3. Concat + Linear。由于Multi-Head Attention后面可能紧跟前馈神经网络(或者RNN、CNN等),而这些网络接受的是单个矩阵向量,而不是8个矩阵。所以把8个矩阵连接在一起(维度2*(8* 3)=2* 24)然后再与一个矩阵(24* 4)相乘,最后压缩成一个矩阵(维度2*4)。步骤如下图所示:

image.png

图解:

image.png

1.3.3 normalization

Normalization有很多种,但是它们都有一个共同的目的,那就是把输入转化成均值为0方差为1的数据。我们在把数据送入激活函数之前进行normalization(归一化),因为我们不希望输入数据落在激活函数的饱和区。

归一化(Normalization)在机器学习和深度学习中扮演着重要的角色,它的主要作用包括:

  • 消除特征间的量纲差异:不同的特征往往具有不同的取值范围和分布,这会导致模型在训练过程中对各个特征的权重分配不平衡。通过对数据进行归一化,可以将不同特征的取值范围映射到统一的区间,使得模型更易于理解和学习。
  • 加速模型收敛:在使用梯度下降等优化算法进行模型训练时,如果特征间的差异较大,那么某些特征对模型的更新影响更大,而其他特征则可能被忽略。这样会导致模型收敛缓慢甚至无法收敛。通过归一化,可以使得各个特征的梯度大小相对均衡,加快了模型的收敛速度。
  • 提高模型的鲁棒性:在现实世界的数据集中,可能存在异常值或离群点,这些数据可能对模型产生较大的干扰。归一化可以对数据进行平滑处理,减小异常值对模型的影响,提高了模型的鲁棒性。
  • 防止过拟合:在一些机器学习和深度学习模型中,特征维度较多或取值范围差异较大时,容易出现过拟合问题。通过归一化可以减小特征之间的相关性,降低模型过拟合的风险。

Batch Normalization

计算公式:

BN(xi)=α×xiuBσB2+ϵ+βBN(x_i)=\alpha\times\frac{x_i-u_B}{\sqrt{\sigma_B^2+\epsilon}}+\beta

在自然语言处理(NLP)领域中,批量归一化(Batch Normalization,BN)虽然在计算机视觉等领域取得了很好的效果,但在NLP任务中存在一些缺点。下面详细介绍几个常见的缺点

  • 依赖于批次大小:BN的计算过程是基于批次数据的均值和方差的,因此批次大小会对BN的效果产生影响。当批次大小较小时,计算的均值和方差可能不够准确,导致归一化效果较差。此外,由于NLP任务中的序列长度差异,合适的批次大小选择也是一个挑战。
  • 序列长度差异:在NLP任务中,输入序列的长度通常是可变的,例如文本分类任务中的不同句子长度。由于BN是对每个批次进行归一化操作,当序列长度差异较大时,可能会导致BN层难以正常工作。长序列和短序列的均值和方差计算会有很大差异,这样会影响梯度的传播和模型的训练。
  • 特征分布偏移:BN的原理是通过对输入数据进行归一化,使其分布接近于标准正态分布。然而,某些情况下,输入数据的分布偏离了正态分布,例如长尾分布或异常值。这会导致BN的效果不佳,甚至可能引入噪声。
  • 对小样本数据不适用:在NLP任务中,如果训练集的样本数量较少,BN可能会因为计算的均值和方差不准确而导致模型性能下降。因此,在小样本场景下,需要谨慎使用BN,或者考虑使用其他归一化方法。

Layer normalization

计算公式:

LN(xi)=α×xiuLσL2+ϵ+βLN(x_i)=\alpha\times\frac{x_i-u_L}{\sqrt{\sigma_L^2+\epsilon}}+\beta

image.png

在NLP领域,与Batch Normalization (BN) 相比,Layer Normalization (LN) 具有以下优点:

  • 对序列和变长输入友好:在自然语言处理任务中,文本数据通常是序列形式的,且长度可能会变化。相比之下,BN适用于固定大小的batch数据,而LN对于不同长度的序列输入更加友好。LN的计算是基于每个样本在特定维度上进行归一化,因此可以适用于变长序列。
  • 不依赖于batch size:BN的计算依赖于batch内的样本均值和方差,因此在小批量训练或者使用较小的batch size时容易出现性能下降。而LN的计算是基于每个样本的特征维度进行归一化,因此不受batch size影响,并且在小批量训练中表现更稳定。
  • 更好地保留序列信息:BN在序列数据中不能保留序列的顺序信息,因为它对每个batch进行归一化处理。相比之下,LN是在同一序列中的不同位置上进行归一化,能够更好地保留序列中的顺序信息,有利于捕捉上下文相关的特征。
  • 更少的额外计算和存储开销:相对于BN,LN的计算量和存储开销较小。BN需要存储每个特征维度的均值和方差,并且在每个batch上进行计算。而LN只需计算每个样本在同一特征维度上的均值和方差,因此计算和存储开销更低。

残差

image.png

引入残差的目的:

根据反向传播的链式法则,

LX=LF(X)+XF(X)+XX\frac{\partial L}{\partial X}=\frac{\partial L}{\partial F_{(X)}+X}\frac{\partial F_{(X)}+X}{\partial X}
LX=LF(X)+X[1+F(X)relu(W1X)relu(W1X)X\frac{\partial L}{\partial X}=\frac{\partial L}{\partial F_{(X)}+X}[1+\frac{\partial F_{(X)}}{\partial relu(W_1X)}\frac{\partial relu(W_1X)}{\partial X}

上式中因为有1的存在,确保了梯度不会为0,不会出现梯度消失的现象。

mask

mask 表示掩码,它对某些值进行掩盖,使其在参数更新时不产生效果。Transformer 模型里面涉及两种 mask,分别是 padding mask 和 sequence mask。其中,padding mask 在所有的 scaled dot-product attention 里面都需要用到,而 sequence mask 只有在 decoder 的 self-attention 里面用到。

  1. padding mask

    什么是 padding mask 呢?因为每个批次输入序列长度是不一样的也就是说,我们要对输入序列进行对齐。具体来说,就是给在较短的序列后面填充 0。但是如果输入的序列太长,则是截取左边的内容,把多余的直接舍弃。因为这些填充的位置,其实是没什么意义的,所以我们的attention机制不应该把注意力放在这些位置上,所以我们需要进行一些处理。

    具体的做法是,把这些位置的值加上一个非常大的负数(负无穷),这样的话,经过 softmax,这些位置的概率就会接近0!

    而我们的 padding mask 实际上是一个张量,每个值都是一个Boolean,值为 false 的地方就是我们要进行处理的地方。

  2. Sequence mask

    sequence mask 是为了使得 decoder 不能看见未来的信息。也就是对于一个序列,在 time_step 为 t 的时刻,我们的解码输出应该只能依赖于 t 时刻之前的输出,而不能依赖 t 之后的输出。因此我们需要想一个办法,把 t 之后的信息给隐藏起来。

    那么具体怎么做呢?也很简单:产生一个上三角矩阵,上三角的值全为0。把这个矩阵作用在每一个序列上,就可以达到我们的目的

  • 对于 decoder 的 self-attention,里面使用到的 scaled dot-product attention,同时需要padding mask 和 sequence mask 作为 attn_mask,具体实现就是两个mask相加作为attn_mask。
  • 其他情况,attn_mask 一律等于 padding mas

2. 代码详解

个人编写,可能有不规范的地方

# _*_coding=utf-8_*_  
  
import math  
import copy  
import torch  
import torch.nn as nn  
import torch.nn.functional as F  
import argparse  
import numpy as np  
import matplotlib.pyplot as plt  
  
  
# 数据处理  
class DataPreprocessing(nn.Module):  
def __init__(self, d_model: int, vocab: int, dropout: float, max_len: int = 5000):  
super(DataPreprocessing, self).__init__()  
self.d_model = d_model  
self.vocab = vocab  
self.dropout = dropout  
self.max_len = max_len  
  
self.dropout = nn.Dropout(p=self.dropout) # 实例化Dropout层  
self.lut = nn.Embedding(vocab, d_model) # 定义Embedding层  
  
pe = torch.zeros(self.max_len, self.d_model) # 位置编码矩阵:(max_len, d_model)  
position = torch.arange(0, self.max_len).unsqueeze(1) # 绝对位置矩阵, max_len*1  
div_len = torch.exp(torch.arange(0, self.d_model, 2) * -(math.log(10000.0) / self.d_model)) # 变换矩阵div_len  
# 将变换矩阵进行奇数,偶数分别赋值  
pe[:, 0::2] = torch.sin(position * div_len)  
pe[:, 1::2] = torch.cos(position * div_len)  
pe = pe.unsqueeze(0)  
# 将位置编码矩阵注册成模型的buffer,这个buffer不是模型的参数,不跟随优化器同步更新  
# 注册成buffer后我们可以在模型保存后重新加载时,将这个位置编码器和模型参数一同记载  
self.register_buffer('pe', pe)  
  
def forward(self, x):  
embedding_output = self.lut(x) * math.sqrt(self.d_model)  
output = embedding_output + self.pe[:, :embedding_output.size(1)]  
return self.dropout(output)  
  
  
class MultiHeadAttention(nn.Module):  
def __init__(self, head: int, d_model: int, dropout=0.1, mask=None):  
"""  
:param head: 头数  
:param d_model: 词嵌入维度  
:param dropout:  
"""  
super(MultiHeadAttention, self).__init__()  
  
# 判断head是否能被此嵌入维度整除  
assert d_model % head == 0  
  
# 得到每个头获得的分割词向量维度d_K  
self.d_k = d_model // head  
  
self.head = head  
self.d_model = d_model  
self.mask = mask  
# 克隆获取四个全连接层对象,分别是Q、K、V及最终的输出线性层  
self.fc = MultiHeadAttention.clones(nn.Linear(d_model, d_model), 4)  
# self.attn为None,它代表最后得到的注意力张量,现在还没有结果所以为None  
self.attn = None  
# 最后就是一个self.dropout对象  
self.dropout = nn.Dropout(p=dropout)  
  
@staticmethod  
def attention(query, key, value, mask=None, dropout=None):  
"""  
:param query:Q  
:param key:K  
:param value:V  
:param mask: 掩码张量  
:param dropout: 置0比率  
:return: query在key和value作用下的表示  
"""  
# 获取query的最后一维的大小,一般情况下就等同于我们的词嵌入维度  
d_k = query.size(-1)  
  
# 按照注意力公式,将query与key转置相乘,这里面key是将最后两个维度进行转置,再除以缩放系数  
# 得到注意力的得分张量score  
scores = torch.matmul(query, key.transpose(-2, -1)) / math.sqrt(d_k)  
# 使用tensor的masked_fill方法,将掩码张量和scores张量每个位置一一比较  
# 如果掩码张量处为0 则对应的score张量用-inf替换  
if mask is not None:  
scores = scores.masked_fill(mask == 0, float('-inf'))  
p_attn = torch.softmax(scores, dim=-1) # 对scores进行softmax操作  
if dropout is not None:  
p_attn = dropout(p_attn) # 使用dropout进行正则化  
attended_representation = torch.matmul(p_attn, value) # 计算加权后的表示  
return attended_representation, p_attn  
  
@staticmethod  
def clones(module, n):  
"""  
生成相同的网络层的克隆函数  
:param module: 目标网络层  
:param n: 克隆数量  
"""  
return nn.ModuleList([copy.deepcopy(module) for _ in range(n)])  
  
def forward(self, query, key, value):  
# query, key, value是三个输入张量,mask代表掩码张量  
  
if self.mask is not None:  
# 扩展维度 代表多头中的第i个头  
mask = self.mask.unsqueeze(1)  
# 获取batch_size  
batch_size = query.size(0)  
  
# 首先利用zip将输入Q、K、V与三个全连接层组到一起,然后使用for循环,将输入Q、K、V分别传到线性层中  
query, key, value = [model(x).view(batch_size, -1, self.head, self.d_k).transpose(1, 2) for model, x in  
zip(self.fc, (query, key, value))]  
# 得到每个头的输出传入到attention中  
output, self.attn = MultiHeadAttention.attention(query, key, value, mask=self.mask, dropout=self.dropout)  
  
output = output.transpose(1, 2).contiguous().view(batch_size, -1, self.head * self.d_k)  
  
# 使用线性层列表中的最后一个线性层对输入进行线性计算  
return self.fc[-1](output)  
  
  
class FeedForward(nn.Module):  
def __init__(self, d_model, d_ff, dropout=0.1):  
"""  
:param d_model: 线性层输入维度  
:param d_ff: 线性层输出维度  
:param dropout: dropout  
"""  
super(FeedForward, self).__init__()  
self.fc1 = nn.Linear(d_model, d_ff)  
self.fc2 = nn.Linear(d_ff, d_model)  
self.dropout = nn.Dropout(dropout)  
  
def forward(self, x):  
return self.fc2(self.dropout(torch.relu(self.fc1(x))))  
  
  
class LayerNorm(nn.Module):  
def __init__(self, features: int, eps=1e-6):  
"""  
:param features: 代表词嵌入的维度  
:param eps: 在规范化公式的分母出现,防止分母为0  
"""  
super(LayerNorm, self).__init__()  
self.a2 = nn.Parameter(torch.ones(features))  
self.b2 = nn.Parameter(torch.zeros(features))  
self.eps = eps  
  
def forward(self, x):  
# 对输入变量x求其最后一个维度的均值,并保持输出维度与输入维度一致  
mean = x.mean(-1, keepdim=True)  
# 接着再求最后一个维度的标准差  
std = x.std(-1, keepdim=True)  
# 然后就是根据规范化公式,用x减去均值除以标准差获得规范化的结果  
# 最后对结果乘以我们的缩放参数,即a2,*号代表同型点乘,即对应位置进行乘法操作,加上位移参数  
return self.a2 * (x - mean) / (std + self.eps) + self.b2  
  
  
class ResConnect(nn.Module):  
def __init__(self, d_model: int, dropout=0.1):  
# d_model: 词嵌入维度的大小  
super(ResConnect, self).__init__()  
# 实例化规范化对象  
self.norm = LayerNorm(d_model)  
self.dropout = nn.Dropout(p=dropout)  
  
def forward(self, x, sublayer):  
"""  
接受上一个层或者子层的输入作为第一个参数  
将该子层连接中的子层函数作为第二个参数  
"""  
# 首先对输出进行规范化,然后将结果传给子层处理,之后再对子层进行dropout操作  
return x + self.dropout(sublayer(self.norm(x)))  
  
  
# 构建编码器  
class EncoderLayer(nn.Module):  
def __init__(self, d_model: int, d_ff: int, head: int, dropout: float, mask=None):  
"""  
:param d_model: 代表词嵌入的维度  
:param d_ff: 线性层输出维度  
:param head: 头数  
:param mask: 是否遮掩  
:param dropout: 置0比率  
"""  
super(EncoderLayer, self).__init__()  
# 将两个实例化对象和参数传入到类中  
self.d_model = d_model  
self.head = head  
self.mask = mask  
  
self.attention = MultiHeadAttention(head, d_model, dropout, mask)  
self.feed_forward = FeedForward(d_model, d_ff, dropout=dropout)  
  
# 编码器层中有两个自层结构,使用clones函数进行操作  
self.sublayer = MultiHeadAttention.clones(ResConnect(d_model, dropout), 2)  
  
def forward(self, x):  
x = self.sublayer[0](x, lambda x: self.attention(x, x, x))  
return self.sublayer[1](x, self.feed_forward)  
  
  
class Encoder(nn.Module):  
def __init__(self, d_model: int, d_ff: int, head: int, dropout: float, num, mask=None):  
"""  
:param d_model: 代表词嵌入的维度  
:param d_ff: 线性层输出维度  
:param head: 头数  
:param dropout: 置0比率  
:param num: 解码器层的个数  
"""  
super(Encoder, self).__init__()  
self.mask = mask  
self.encoder_layer = EncoderLayer(d_model=d_model, d_ff=d_ff, head=head, dropout=dropout, mask=mask)  
self.layers = MultiHeadAttention.clones(self.encoder_layer, num)  
self.norm = LayerNorm(d_model)  
  
def forward(self, x):  
for layer in self.layers:  
x = layer(x)  
return self.norm(x)  
  
  
class DecoderLayer(nn.Module):  
def __init__(self, d_model: int, d_ff: int, head: int, dropout: float, num: int, source_mask, target_mask):  
"""  
  
:param d_model: 代表词嵌入的维度  
:param d_ff: 线性层输出维度  
:param head: 头数  
:param dropout: 置0比率  
:param source_mask: 源数据掩码张量,为了遮掩对结果无用的数据  
:param target_mask: 目标数据掩码张量,解码时将未来信息进行遮掩  
:param num: 编码器层的个数  
"""  
super(DecoderLayer, self).__init__()  
self.d_model = d_model  
self.encoder = Encoder(d_model=d_model, d_ff=d_ff, head=head, dropout=dropout, num=num)  
self.feed_forward = FeedForward(d_model, d_ff, dropout) # 前馈神经网络对象  
self.attention_1 = MultiHeadAttention(head, d_model, dropout, target_mask) # 多头自注意机制对象  
self.attention_2 = MultiHeadAttention(head, d_model, dropout, source_mask) # 常规的注意力机制对象  
self.sublayer = MultiHeadAttention.clones(ResConnect(d_model, dropout), 3)  
  
def forward(self, x):  
m = self.encoder(x)  
# 采用target_mask  
x = self.sublayer[0](x, lambda x: self.attention_1(x, x, x))  
x = self.sublayer[1](x, lambda x: self.attention_2(x, m, m)) # 常规注意力机制,Q!=K=V  
return self.sublayer[2](x, self.feed_forward)  
  
  
class Decoder(nn.Module):  
def __init__(self, d_model: int, d_ff: int, head: int, dropout: float, num_encoder_layer: int,  
num_decoder_layer: int, source_mask, target_mask):  
"""  
:param d_model: 代表词嵌入的维度  
:param d_ff: 线性层输出维度  
:param head: 头数  
:param dropout: 置0比率  
:param source_mask: 源数据掩码张量  
:param target_mask: 目标数据掩码张量  
:param num_encoder_layer: 编码器层的个数  
:param num_decoder_layer: 解码器层的个数  
"""  
super(Decoder, self).__init__()  
# 调用解码层的实例化对象  
self.decoder_layer = DecoderLayer(d_model, d_ff, head, dropout, num_encoder_layer, source_mask, target_mask)  
self.layers = MultiHeadAttention.clones(self.decoder_layer, num_decoder_layer)  
self.norm = LayerNorm(d_model)  
  
def forward(self, x):  
for layer in self.layers:  
x = layer(x)  
return self.norm(x)  
  
  
class Generator(nn.Module):  
def __init__(self, vocab: int, d_model: int, d_ff: int, head: int, dropout: float, num_encoder_layer: int,  
num_decoder_layer: int, source_mask, target_mask):  
"""  
:param vocab: 代表词表大小  
:param d_model: 代表词嵌入的维度  
:param d_ff: 线性层输出维度  
:param head: 头数  
:param dropout: 置0比率  
:param source_mask: 源数据掩码张量  
:param target_mask: 目标数据掩码张量  
:param num_encoder_layer: 编码器层的个数  
:param num_decoder_layer: 解码器层的个数  
"""  
super(Generator, self).__init__()  
# 调用data_preprocessing实例化对象  
self.data_preprocessing = DataPreprocessing(d_model=d_model, vocab=vocab, dropout=dropout)  
# 调用decoder实例化对象  
self.decoder = Decoder(d_model=d_model, d_ff=d_ff, head=head, dropout=dropout,  
num_encoder_layer=num_encoder_layer,  
num_decoder_layer=num_decoder_layer,  
source_mask=source_mask, target_mask=target_mask)  
self.fc = nn.Linear(d_model, vocab)  
  
def forward(self, x):  
data_processing_output = self.data_preprocessing(x)  
decoder_output = self.decoder(data_processing_output)  
return F.log_softmax(self.fc(decoder_output), dim=-1)  
  
  
if __name__ == '__main__':  
parser = argparse.ArgumentParser()  
  
parser.add_argument('--vocab', type=int, default=1000, help='词表大小')  
parser.add_argument('--d_model', type=int, default=512, help='代表词嵌入的维度')  
parser.add_argument('--d_ff', type=int, default=64, help='线性层输出维度')  
parser.add_argument('--head', type=int, default=8, help='头数')  
parser.add_argument('--dropout', type=float, default=0.2, help='置0比率')  
parser.add_argument('--num_encoder_layer', type=int, default=6, help='编码器层的个数')  
parser.add_argument('--num_decoder_layer', type=int, default=6, help='解码器层的个数')  
parser.add_argument('--source_mask', default=torch.zeros(8, 4, 4).cuda(), help='源数据掩码张量')  
parser.add_argument('--target_mask', default=torch.zeros(8, 4, 4).cuda(), help='目标数据掩码张量')  
  
args = parser.parse_args()  
  
model = Generator(vocab=args.vocab, d_model=args.d_model, d_ff=args.d_ff, head=args.head, dropout=args.dropout,  
num_encoder_layer=args.num_encoder_layer, num_decoder_layer=args.num_decoder_layer,  
source_mask=args.source_mask, target_mask=args.target_mask).cuda()  
  
x = torch.LongTensor([[100, 2, 421, 4], [225, 4, 85, 6]]).cuda()  
print(x.shape)  
output = model(x)  
print(output.shape)

参考:

[1].((28条消息) 举例理解transformer中的位置编码_位置编码的作用_陈壮实的搬砖生活的博客-CSDN博客)

[2].(三分钟读懂Softmax函数 - 知乎 (zhihu.com))

[3].(Transformer各层网络结构详解!面试必备!(附代码实现) - 掘金 (juejin.cn))

[4].(拆 Transformer 系列二:Multi- Head Attention 机制详解 - 知乎 (zhihu.com))

[5].((28条消息) 图解Transformer_transformer adam训练曲线图_adam-liu的博客-CSDN博客) 6.