transfoermer简述
背景
Transformer是一种在自然语言处理领域中引起了革命性变革的模型架构。
它首次被提出于2017年的论文《Attention is All You Need》中,由Google的研究团队提出。
这篇论文开创了一种全新的模型架构,成为了许多自然语言处理任务的基础,如机器翻译、文本摘要、对话生成等。随后许多基于Transformer的变种模型也相继涌现,例如BERT、GPT等,进一步推动了自然语言处理领域的发展。
2018年10月,Google发出一篇论文《BERT: Pre-training of Deep Bidirectional Transformers for Language Understanding》, BERT模型横空出世, 并横扫NLP领域11项任务的最佳成绩!
BERT中发挥重要作用的结构就是Transformer!
相比之前占领市场的LSTM和GRU模型,Transformer有两个显著的优势:
#1.Transformer能够利用分布式GPU进行并行训练,提升模型训练效率 #2.在分析预测更长的文本时, 捕捉间隔较长的语义关联效果更好
RNN、LSTM、Transformer对长文本提取事物特征效果对比:

结论:
#1.rnn和lstm文本长度20~30之间效果下降显著 #2.transformer超过句子长度40以后也能保持较好效果
扩展阅读:
#1.SOTA模型 全称是state-of-the-art model,并不是特指某个具体的模型,而是指在该项研究任务中,目前最好最先进的模型。 #2.NLP常规任务划分 NLP是人工智能的子领域,它有两个核心任务: NLU:Natural Language Understanding,自然语言理解。 NLG:Natural Language Generating,自然语言生成。

架构
Transformer的架构如下图所示:

Transformer架构分为四部分:
-
输入部分
#1.源文本词嵌入层 + 位置编码器 #2.目标文本词嵌入层 + 位置编码器
-
编码器部分
#1.由N个编码器层堆叠而成 #2.每个编码器由2个子层连接结构组成 #3.第一个子层连接结构包括普通的多头注意力机制 + 规范化层 + 残差连接 #4.第二个子层连接结构包括前馈全连接层 + 规范化层 + 残差连接
-
解码器部分
#1.由N个解码器层堆叠而成 #2.每个解码器由3个子层连接结构组成 #3.第一个子层连接结构包括带掩码的多头注意力机制 + 规范化层 + 残差连接 #4.第二个子层连接结构包括普通的多头注意力机制 + 规范化层 + 残差连接 #5.第三个子层连接结构包括前馈全连接层 + 规范化层 + 残差连接
-
输出部分
#1.线性层 #2.softmax层
输入部分
输入部分由两部分组成,词嵌入层和位置编码器

词嵌入层
就是一个Embedding层
目的是把文本数值化+张量化
代码如下
class Embeddings(nn.Module):
def __init__(self, vocab, d_model):
super().__init__()
self.vocab = vocab
self.d_model = d_model
self.embedded = nn.Embedding(vocab, d_model)
def forward(self, x):
x = self.embedded(x)
return x * math.sqrt(self.d_model)
位置编码器
因为在Transformer的编码器结构中, 并没有针对词汇位置信息的处理,因此需要在Embedding层后加入位置编码器,将词汇位置不同可能会产生不同语义的信息加入到词嵌入张量中, 以弥补位置信息的缺失.
作用:就是给文本添加位置信息。
-
为什么要给文本添加位置信息?
词所在的位置不同,表示的含义可能不一样。
-
什么是给文本添加位置信息?
给文本添加位置信息,准确地说,就是给文本的特征添加位置信息。
-
如何添加位置信息?
引入Positional Encoding(PE,位置编码)矩阵,用特征矩阵加上PE矩阵,就相当于给文本添加了位置信息。
-
如何构建PE矩阵?
位置矩阵由2个元素组成:行(类似于词表大小)、列(类似于维度)。
代码如下
class PositionalEncoding(nn.Module):
def __init__(self, d_model, dropout, max_len=60):
super().__init__()
# dropout:随机失活的系数
self.dropout = nn.Dropout(p=dropout)
# 准备pe
pe = torch.zeros(max_len, d_model) # [60,512]
# 准备position
position = torch.arange(0, max_len).unsqueeze(dim=1)
# print("position-->", position) # [60,1]
# 基于公式,实现位置编码
# float():转换为小数
_2i = torch.arange(0, d_model, 2).float()
# print("_2i-->", _2i) # 一共256个
# 对奇数位置赋值为sin函数的值
pe[:, 0::2] = torch.sin(position / (10000 ** (_2i/d_model)))
# 对偶数位置赋值为cos函数的值
pe[:, 1::2] = torch.cos(position / (10000 ** (_2i/d_model)))
# 对pe升维
pe = pe.unsqueeze(dim=0) # [1,60,512]
# 把pe注册到缓冲区
self.register_buffer("pe", pe)
def forward(self, x):
# x就是Embedding后的数据,x的形状:[2,4,512]
# 所以,截取的时候,只需要截取前4个token的位置编码即可
x = x + self.pe[:, :x.shape[1]]
return self.dropout(x)
绘制词向量中特征的分布曲线
def draw_pe_graph():
# 初始化位置编码层对象
pe = PositionalEncoding(d_model=20, dropout=0, max_len=100)
# 准备数据x->【1,100,20】,1句话,100个 token,每个token使用20维度的向量表示
x = torch.zeros(1, 100, 20)
# 把x送给pe层
y = pe(x)
print("y-->", y.shape) # [1, 100, 20]
# 画图
plt.plot(np.arange(100), y[0, :, 4:8])
# fontsize:字体大小
plt.legend(["dim_%d" % p for p in [4,5,6,7]], fontsize='small')
plt.show()

效果如上
-
效果分析
-
每条颜色的曲线代表某一个词汇中的特征在不同位置的含义
-
保证同一词汇随着所在位置不同它对应位置嵌入向量会发生变化
-
正弦波和余弦波的值域范围都是1到-1这又很好的控制了嵌入数值的大小, 有助于梯度的快速计算
编码器部分实现
编码器部分由六层编码器层组成,而每个编码器层由2个子连接层结构组成
第一个子连接层结构:多头自注意力(QKV相同)+规范化层+残差连接
第二个子连接层结构:前馈全连接层层+规范化层+残差连接

N为6
掩码张量
上三角矩阵:0组成的一个三角形,如下图:

下三角矩阵:0组成的一个三角形,如下图:

下三角矩阵的作用:
以模型解码为例,生成字符时,一个时间步一个时间步的解码。
使用掩码mask,比如:(0表示能看的见, 1表示被这遮掩) 希望模型不要使用当前字符和后面的字符。
也就是防止模型看到未来信息,用1给他遮掩住。
代码如下:
def dm_test_nptriu():
# 测试产生上三角矩阵
print(np.triu([[1, 1, 1, 1, 1],
[2, 2, 2, 2, 2],
[3, 3, 3, 3, 3],
[4, 4, 4, 4, 4],
[5, 5, 5, 5, 5]], k=1))
print(np.triu([[1, 1, 1, 1, 1],
[2, 2, 2, 2, 2],
[3, 3, 3, 3, 3],
[4, 4, 4, 4, 4],
[5, 5, 5, 5, 5]], k=0))
print(np.triu([[1, 1, 1, 1, 1],
[2, 2, 2, 2, 2],
[3, 3, 3, 3, 3],
[4, 4, 4, 4, 4],
[5, 5, 5, 5, 5]], k=-1))
# 生成掩码张量
def subsuquent_mask(size):
# 生成上三角矩阵
masked = np.triu(m=np.ones((size, size), dtype='uint8'), k=1)
# print('masked-->\n', masked)
# 生成下三角矩阵
return torch.tensor(1 - masked)
def test_subsuquent_mask():
size = 5
masked = subsuquent_mask(5)
print("masked-->\n", masked)
# 掩码张量:作用在另外一个张量上。对另外一个张量进行掩码。
# 绘图
plt.imshow(subsuquent_mask(20))
plt.show()

1:代表被遮掩(黄色部分)
0的位置向上看,全部被遮掩。1的位置向上看,只能看到一个信息。
自注意力机制
编码器使用的是自注意力机制,也就是Q=K=V。

已知:Q=K=V,他们的数据机构都是[2, 4, 512]。
问题:
-
为什么Q要乘以K的转置?
因为不转置无法相乘。
-
为什么要除以根号下d~k~?
控制方差,把方差拉回到0-1之间
代码如下:
def attention(query, key, value, mask=None, dropout=None):
# 获取d_k,也就是维度,这里就是512
d_k = query.shape[-1]
# 基于自注意力计算公式,得到score
scores = torch.matmul(query, key.transpose(-1, -2)) / math.sqrt(d_k)
# print("scores-->", scores.shape)
# 判断是否有mask
if mask is not None:
scores = scores.masked_fill(mask==0, -1e9)
# 让score经过softmax,得到注意力权重
p_attn = torch.softmax(scores, dim=-1)
# 让p_attn经过dropout层
if dropout is not None:
p_attn = dropout(p_attn)
# 返回结果
return torch.matmul(p_attn, value), p_attn
def test_attention():
vocab = 1000
d_model = 512
x = torch.tensor([[100, 2, 421, 508], [491, 998, 1, 221]])
embed = Embeddings(vocab, d_model)
x = embed(x)
# 初始化PositionalEncoding对象
dropout = 0.1
pe = PositionalEncoding(d_model=d_model, dropout=dropout, max_len=60)
# 让数据加上pe
x = pe(x)
# 准备query、key、value
query = key = value = x
# 不带掩码
p_attn, attn_weights = attention(query, key, value)
print("p_attn-->", p_attn, p_attn.shape) # [2, 4, 512]
print("attn_weights-->", attn_weights, attn_weights.shape) # [2, 4, 4]
# 带掩码
# mask = torch.zeros(2, 4, 4)
# p_attn, attn_weights = attention(query, key, value, mask)
# print("p_attn-->", p_attn, p_attn.shape) # [2, 4, 512]
# print("attn_weights-->", attn_weights, attn_weights.shape) # [2, 4, 4]
多头注意力机制
从多头注意力的结构图中,貌似这个所谓的多个头就是指多组线性变换层,其实并不是,我只有使用了一组线性变化层,即三个变换张量对Q,K,V分别进行线性变换,这些变换不会改变原有张量的尺寸,因此每个变换矩阵都是方阵,得到输出结果后,多头的作用才开始显现,每个头开始从词义层面分割输出的张量,也就是每个头都想获得一组Q,K,V进行注意力机制的计算,但是句子中的每个词的表示只获得一部分,也就是只分割了最后一维的词嵌入向量. 这就是所谓的多头,将每个头的获得的输入送到注意力机制中, 就形成多头注意力机制.

多头注意力机制的作用:这种结构设计能让每个注意力机制去优化每个词汇不同特征部分,从而均衡同一种注意力机制可能产生过的偏差,让词义拥有来自更多元的表达,实验表明可以提升模型效果
解释:
#1.线性变换:QKV分别输入到线性层 #2.view切分:特征做多头切分, 比如:256个特征切分成8个头,每个头64个特征 #3.attention操作:通过attention函数进行多头特征提取 #4.Concat操作:合并多头特征提取结果 #5.线性层变换,最后的得到我们想要的数据形状
图解:

代码
def clones(model, N):
# copy.deepcopy 深拷贝
# Module
return nn.ModuleList([copy.deepcopy(model) for _ in range(N)])
class MultiHeadAttention(nn.Module):
def __init__(self, head, embedding_dim, dropout=0.1):
super(MultiHeadAttention, self).__init__()
# 断言,如果能整除,则代码继续,否则报错
assert embedding_dim % head == 0
# 属性
# 多头:8个
self.head = head
# 每个头注意的维度数,也就是64
self.d_k = embedding_dim // head
# 注意力权重
self.attn = None
self.dropout = nn.Dropout(p=dropout)
# 定义线性层
# 4 = Q,K,V都要经过线性层(共3个线性层) + 最后一个线性层拼接所有多头维度
self.linears = clones(nn.Linear(embedding_dim, embedding_dim), 4)
# 打印多个线性层
# print(self.linears[0])
# print(self.linears[1])
# print(self.linears[2])
# print(self.linears[3])
# print(self.linears[4]) # 报错
def forward(self, query, key, value, mask=None):
# query=key=value->[2,4,512]
# 得到batch_size
batch_size = query.shape[0]
# 对mask升维
if mask is not None:
mask = mask.unsqueeze(0)
# 让Q,K,V数据分别经过线性层
# 这里的意思是,拉链后,线性层1,2,3,分别和query, key,value对应,就像
"""
(self.linears[0], query)
(self.linears[1], key)
(self.linears[2], value)
"""
# 所以model对应linear1,2,3,x对应query,key, value,将x传入model(线性层)经过变换后,再进行reshaoe升维后交换位置得到2,8,4,512
query, key, value = [model(x).reshape(batch_size, -1, self.head, self.d_k).transpose(1, 2)
for model,x in zip(self.linears, (query, key, value))]
# print('query-->', query.shape) # [2,8,4,64]
# print('key-->', key.shape) # [2,8,4,64]
# print('value-->', value.shape) # [2,8,4,64]
# 把query、key,value送给注意力函数进行注意力计算
x, self.attn = attention(query, key, value, mask=mask, dropout=self.dropout)
# 把数据维度交换回来 # 2,4,8,512
x = x.transpose(1, 2).reshape(batch_size, -1, self.head * self.d_k)
# 让x经过最后一个线性层
return self.linears[-1](x)
前馈全连接层
-
在Transformer中前馈全连接层就是具有两层线性层的全连接网络.
-
前馈全连接层的作用:
-
考虑注意力机制可能对复杂过程的拟合程度不够, 通过增加两层网络来增强模型的能力.
-
代码实现
class FeedForward(nn.Module):
def __init__(self, d_model, d_ff, dropout=0.1):
super().__init__()
# 额外增加2个线性层,增强模型的表达能力
#
self.linear1 = nn.Linear(d_model, d_ff)
self.linear2 = nn.Linear(d_ff, d_model)
self.dropout = nn.Dropout(p=dropout)
def forward(self, x):
return self.linear2(self.dropout(torch.relu(self.linear1(x))))
规范化层
概念:它是所有深层网络模型都需要的标准网络层,因为随着网络层数的增加,通过多层的计算后参数可能开始出现过大或过小的情况,这样可能会导致学习过程出现异常,模型可能收敛非常的慢. 因此都会在一定层数后接规范化层进行数值的规范化,使其特征数值在合理范围内.。
规范化层实际上是一种归一化的操作,旨在使输入的均值保持接近0,标准差保持接近1。
作用:
-
防止梯度消失和梯度爆炸:在深度神经网络中,随着层数的增加,梯度很容易变得非常小(梯度消失)或非常大(梯度爆炸)。规范化层通过将输入数据归一化,可以缓解这一问题,使得梯度的传播更加稳定。
-
加速训练过程:规范化层可使模型在训练时更快地收敛,加速整个训练过程
计算方式:
计算每个位置的所有特征维度上的均值和方差,然后对该位置的所有特征维度进行线性变换,以保证均值为0,方差为1,最后再进行缩放和平移操作,引入了可学习的参数(缩放因子和平移因子)。
为什么引入缩放因子和平移因子:
-
为了学习不同批次之间的数据特征,能让模型有更好的泛化能力
-
举个例子:每个批次有8个数,第1个批次有均值和方差,第2个批次有均值和方差。第n个批次也有均值和方差,可以学习所有数据的均值和方差,模型有更好的泛化能力
代码:
class LayerNorm(nn.Module):
def __init__(self,features, eps=1e-6):
super().__init__()
# 封装2个参数,用于神经网络学习
# y = ax+b
self.a = nn.Parameter(torch.ones(features))
self.b = nn.Parameter(torch.zeros(features))
# eps记录属性
self.eps = eps
def forward(self, x):
# 均值
mean = x.mean(-1, keepdim=True)
# 标准差
std = x.std(-1, keepdim=True)
# 封装为y函数
return (self.a * (x - mean) / (std + self.eps) + self.b)
子层连接结构
-
如图所示,输入到每个子层以及规范化层的过程中,还使用了残差链接(跳跃连接),因此我们把这一部分结构整体叫做子层连接(代表子层及其链接结构),在每个编码器层中,都有两个子层,这两个子层加上周围的链接结构就形成了两个子层连接结构.
-
子层连接结构图:

编码器层的子层连接结构:多头注意力/前馈全连接层 + 规范化层 + 残差连接
为了方便使用,我们将解码器层或者编码器层都用到的规范化层+残差连接封装成一个类
代码实现
class SublayerConnection(nn.Module):
def __init__(self,size, dropout=0.1):
super().__init__()
# 规范化层
self.norm = LayerNorm(size)
# dropout层
self.dropout = nn.Dropout(p=dropout)
def forward(self, x, sublayer):
# x数据
# sublayer 子层连接结构对象,不同的子层连接结构,有可能是多头或者前馈全链接,它是一个函数入口
# 先经过规范化层,再经过子层链接结构(前馈全链接或者多头注意力,再经过残差
return x + self.dropout(sublayer(self.norm(x))) # x+提现了残差连接
编码器层
概念:是编码器的组成单元(编码部分的组成单元)。
作用:完成一次对输入数据特征的提取,即编码过程。
代码:
class Encoderlayer(nn.Module):
def __init__(self, size, self_attn, feed_forward, dropout):
super().__init__()
self.size = size
self.self_attn = self_attn
self.feed_forward = feed_forward
# 复制2个子层链接结构对象
self.sublayer = clones(SublayerConnection(size, dropout), 2)
def forward(self, x, mask):
# 调用第一个子层链接结构
x = self.sublayer[0](x, lambda x : self.self_attn(x, x, x, mask))
# 调用第二个子层连接结构
x = self.sublayer[1](x, lambda x: self.feed_forward(x))
return x
编码器
-
编码器用于对输入进行指定的特征提取过程, 也称为编码, 由N个编码器层堆叠而成.
-
编码器的结构图

代码实现
class Encoder(nn.Module):
def __init__(self, layer, N):
super().__init__()
self.layers = clones(layer, N)
# layer :编码器层对象
# layer.sizeL:从layer对象中拿到size属性
self.norm = LayerNorm(layer.size)
def forward(self, x, mask):
# 遍历每个编码器层,让每一层的 编码器都经过x和mask
for layer in self.layers:
x = layer(x, mask)
# 最后让x经过规范化层
return self.norm(x)
解码器部分实现
结构:

其中解码器层的第二个子层连接结构的输入来自于编码器的输出,而且在图中可知,QKV不完全相同,所以是多头注意力机制,不是多头自注意力机制(QKV相同)
所以前面设计的MultiHeadAttention模块可以直接使用,仅仅是输入不同而已,但是所用的公式是同一个
所以,综上所知,解码器层的构建与编码器层的构建有一定相似度
解码器层
概念:是解码器的组成单元。
作用:每个解码器层根据给定的输入向目标方向进行特征提取操作,即解码过程。
解码器层的组成:
第一个子层连接结构:多头自注意力机制+规范化层+残差连接
第二个子层连接结构:多头注意力机制+规范化层+残差连接
第三个子层连接结构:前馈全链接层+规范化层+残差连接
而编码器由6个编码器层组成
class DecoderLayer(nn.Module): # 编码器层
def __init__(self, size, self_attn, src_attn, feed_forward, dropout):
super(DecoderLayer, self).__init__()
# 属性
self.size = size
self.self_attn = self_attn
self.src_attn = src_attn
self.feed_forward = feed_forward
# 拷贝3个子层连接结构
self.sublayer = clones(SublayerConnection(size, dropout), 3)
def forward(self, x, memory, source_mask, target_mask):
# x:解码器端经过词嵌入+位置编码后的结果
# memory:编码器的输出
# source_mask:编码器的掩码
# target_mask:解码器的掩码
# 先让数据经过解码器的带掩码的多头自注意力
x = self.sublayer[0](x, lambda x : self.self_attn(x, x, x, target_mask))
# 其次让数据经过普通注意力层
x = self.sublayer[1](x, lambda x : self.src_attn(x, memory, memory, source_mask))
# 最后让数据经过前馈全连接层
return self.sublayer[2](x, lambda x : self.feed_forward(x))
class Decoder(nn.Module):
def __init__(self, layer, N):
super(Decoder, self).__init__()
# 克隆多个解码器层
self.layers = clones(layer, N)
# 创建一个规范化层,用于最后的规范化输出
self.norm = LayerNorm(features=layer.size)
def forward(self, x, memory, source_mask, target_mask):
for layer in self.layers:
x = layer(x, memory, source_mask, target_mask)
# 返回,加上规范化层
return self.norm(x)
输出部分实现
输出部分包括线性层和softmax层:
-
线性层:对上一步的结果进行指定维度变换,也就是转换维度的作用。
-
softmax层:转换概率分布,使最后一维的向量中的数字缩放到0-1的概率值域内, 并满足他们的和为1。
代码:
class Generator(nn.Module):
def __init__(self, d_model, vocab_size):
super(Generator, self).__init__()
# d_model:维度数,这里就是512
# vocab_size:目标文本词表大小
self.linear = nn.Linear(d_model, vocab_size)
def forward(self, x):
# log_softmax等价于 nn.LogSoftmax()
return torch.log_softmax(self.linear(x), dim=-1)
模型构建
我们已经完成了所有组成部分的实现,接下来实现完整的编码器-解码器结构。
完整代码如下
import copy
import torch
import torch.nn as nn
import math
import matplotlib.pyplot as plt
import numpy as np
# TODO 1.输入部分
# 词嵌入层
class Embeddings(nn.Module):
def __init__(self, vocab, d_model):
super().__init__()
# 属性
# vocab:词表大小
# d_model:词嵌入维度,默认就是512
# 在Transformer模型中,词嵌入维度,默认就是512
self.vocab = vocab
self.d_model = d_model
# embedding层
self.embedded = nn.Embedding(num_embeddings=vocab, embedding_dim=d_model)
def forward(self, x):
# 让x经过Embedding层,x就是输入的张量数据(数字)
x = self.embedded(x)
# 在Transformer中,Embedding的初始化默认使用的Xavier_uniform初始化,不是正态分布数据,
# 为了让x保持数据处于正态分布,所以额外乘以 sqrt(d_model)
return x * math.sqrt(self.d_model)
# 位置编码器
class PositionalEncoding(nn.Module):
def __init__(self, d_model, dropout, max_len=60):
super().__init__()
# dropout:随机失活的系数
self.dropout = nn.Dropout(p=dropout)
# 准备pe
pe = torch.zeros(max_len, d_model) # [60,512]
# 准备position
position = torch.arange(0, max_len).unsqueeze(dim=1)
# print("position-->", position) # [60,1]
# 基于公式,实现位置编码
# float():转换为小数
_2i = torch.arange(0, d_model, 2).float()
# print("_2i-->", _2i) # 一共256个
# 对奇数位置赋值为sin函数的值
pe[:, 0::2] = torch.sin(position / (10000 ** (_2i/d_model)))
# 对偶数位置赋值为cos函数的值
pe[:, 1::2] = torch.cos(position / (10000 ** (_2i/d_model)))
# 对pe升维
pe = pe.unsqueeze(dim=0) # [1,60,512]
# 把pe注册到缓冲区
self.register_buffer("pe", pe)
def forward(self, x):
# x就是Embedding后的数据,x的形状:[2,4,512]
# 所以,截取的时候,只需要截取前4个token的位置编码即可
x = x + self.pe[:, :x.shape[1]]
return self.dropout(x)
# 生成掩码张量
def subsuquent_mask(size):
# 生成上三角矩阵
masked = np.triu(m=np.ones((size, size), dtype='uint8'), k=1)
# print('masked-->\n', masked)
# 生成下三角矩阵
return torch.tensor(1 - masked)
def test_subsuquent_mask():
size = 5
masked = subsuquent_mask(5)
print("masked-->\n", masked)
# 掩码张量:作用在另外一个张量上。对另外一个张量进行掩码。
# 绘图
plt.imshow(subsuquent_mask(20))
plt.show()
# 注意力机制
def attention(query, key, value, mask=None, dropout=None):
# 获取d_k,也就是维度,这里就是512
d_k = query.shape[-1]
# 基于自注意力计算公式,得到score
scores = torch.matmul(query, key.transpose(-1, -2)) / math.sqrt(d_k)
# print("scores-->", scores.shape)
# 判断是否有mask
if mask is not None:
scores = scores.masked_fill(mask==0, -1e9)
# 让score经过softmax,得到注意力权重
p_attn = torch.softmax(scores, dim=-1)
# 让p_attn经过dropout层
if dropout is not None:
p_attn = dropout(p_attn)
# 返回结果
return torch.matmul(p_attn, value), p_attn
# 多头注意力机制
def clones(model, N):
# copy.deepcopy(x):深拷贝
# ModuleList:模型列表,可以装多个模型,我们可以把它当做普通的Python列表来看待
return nn.ModuleList([copy.deepcopy(model) for _ in range(N)])
class MultiHeadAttention(nn.Module):
def __init__(self, head, embedding_dim, dropout=0.1):
super(MultiHeadAttention, self).__init__()
# 断言,如果能整除,则代码继续,否则报错
assert embedding_dim % head == 0
# 属性
# 多头:8个
self.head = head
# 每个头注意的维度数,也就是64
self.d_k = embedding_dim // head
# 注意力权重
self.attn = None
self.dropout = nn.Dropout(p=dropout)
# 定义线性层
# 4 = Q,K,V都要经过线性层(共3个线性层) + 最后一个线性层拼接所有多头维度
self.linears = clones(nn.Linear(embedding_dim, embedding_dim), 4)
# 打印多个线性层
# print(self.linears[0])
# print(self.linears[1])
# print(self.linears[2])
# print(self.linears[3])
# print(self.linears[4]) # 报错
def forward(self, query, key, value, mask=None):
# query=key=value->[2,4,512]
# 得到batch_size
batch_size = query.shape[0]
# 对mask升维
if mask is not None:
mask = mask.unsqueeze(0)
# 让Q,K,V数据分别经过线性层
# 这里的意思是,拉链后,线性层1,2,3,分别和query, key,value对应,就像
"""
(self.linears[0], query)
(self.linears[1], key)
(self.linears[2], value)
"""
# 所以model对应linear1,2,3,x对应query,key, value,将x传入model(线性层)经过变换后,再进行reshaoe升维后交换位置得到2,8,4,512
query, key, value = [model(x).reshape(batch_size, -1, self.head, self.d_k).transpose(1, 2)
for model,x in zip(self.linears, (query, key, value))]
# print('query-->', query.shape) # [2,8,4,64]
# print('key-->', key.shape) # [2,8,4,64]
# print('value-->', value.shape) # [2,8,4,64]
# 把query、key,value送给注意力函数进行注意力计算
x, self.attn = attention(query, key, value, mask=mask, dropout=self.dropout)
# 把数据维度交换回来 # 2,4,8,512
x = x.transpose(1, 2).reshape(batch_size, -1, self.head * self.d_k)
# 让x经过最后一个线性层
return self.linears[-1](x)
# 前馈全连接层
class FeedForward(nn.Module):
def __init__(self, d_model, d_ff, dropout=0.1):
super(FeedForward, self).__init__()
# 额外增加2个线性层,增强模型的表达能力
# 第一个线性层
self.linear1 = nn.Linear(d_model, d_ff)
# 第二个线性层
self.linear2 = nn.Linear(d_ff, d_model)
# 随机失活
self.dropout = nn.Dropout(p=dropout)
def forward(self, x):
return self.linear2(self.dropout(torch.relu(self.linear1(x))))
# 规范化层,为了让数据保持同分布,可以减少训练时间,需要的数据也更少
class LayerNorm(nn.Module):
def __init__(self, features, eps=1e-6):
super(LayerNorm, self).__init__()
# 封装2个参数,用于用于神经网络学习,经过Parameter封装后的参数可以随着神经网络的训练而更新。
# 封装函数:y = ax + b
self.a = nn.Parameter(torch.ones(features))
self.b = nn.Parameter(torch.zeros(features))
# eps纪录属性
self.eps = eps
def forward(self, x):
# 均值
mean = x.mean(dim=-1, keepdim=True)
# 标准差
std = x.std(dim=-1, keepdim=True)
# 封装为y函数,# 这是标准化的公式 x-mean/方差
return self.a * (x - mean) / (std + self.eps) + self.b
# 解码器或者编码器都需要用到规范化+残差连接,将他封装到一个类中,子层连接结构
class SublayerConnection(nn.Module):
def __init__(self, size, dropout=0.1):
super(SublayerConnection, self).__init__()
# 规范化层
self.norm = LayerNorm(size)
# dropout层
self.dropout = nn.Dropout(p=dropout)
def forward(self, x, sublayer):
# x:数据
# sublayer:子层连接结构对象,不同的子层连接结构,有可能是多头或者前馈全连接,它是一个函数入口地址(对象)
# 先经过规范化层,再经过子层连接结构(前馈全连接或者多头注意力),再经过残差
return x + self.dropout(sublayer(self.norm(x)))
# 这里的 x + 就体现了残差连接
# 编码器层,每个编码器部分有6个编码器层组成
class EncoderLayer(nn.Module):
def __init__(self, size, self_attn, feed_forward, dropout):
super(EncoderLayer, self).__init__()
# 属性
self.size = size
self.self_attn = self_attn
self.feed_forward = feed_forward
# 复制2个子层连接结构对象
self.sublayer = clones(SublayerConnection(size, dropout), 2)
def forward(self, x, mask):
# 调用第1个子层连接结构
x = self.sublayer[0](x, lambda x : self.self_attn(x, x, x, mask))
# 调用第2个子层连接结构
# x = self.sublayer[1](x, self.feed_forward)
x = self.sublayer[1](x, lambda x : self.feed_forward(x))
return x
# 编码器
class Encoder(nn.Module):
def __init__(self, layer, N):
# layer:编码器层
super(Encoder, self).__init__()
self.layers = clones(layer, N)
# layer:编码器层对象
# layer.size:从layer对象中拿到size属性
self.norm = LayerNorm(features=layer.size)
def forward(self, x, mask):
# 遍历每个编码器层,让每一层的编码器都经过x和mask
for layer in self.layers:
x = layer(x, mask)
# 最后让x经过规范化层
return self.norm(x)
# TODO 3.解码器部分
# 解码器层
class DecoderLayer(nn.Module):
def __init__(self, size, self_attn, src_attn, feed_forward, dropout):
super(DecoderLayer, self).__init__()
# 属性
self.size = size
self.self_attn = self_attn
self.src_attn = src_attn
self.feed_forward = feed_forward
# 拷贝3个子层连接结构
self.sublayer = clones(SublayerConnection(size, dropout), 3)
def forward(self, x, memory, source_mask, target_mask):
# x:解码器端经过词嵌入+位置编码后的结果
# memory:编码器的输出
# source_mask:编码器的掩码
# target_mask:解码器的掩码
# 先让数据经过解码器的带掩码的多头自注意力
x = self.sublayer[0](x, lambda x : self.self_attn(x, x, x, target_mask))
# 其次让数据经过普通注意力层
x = self.sublayer[1](x, lambda x : self.src_attn(x, memory, memory, source_mask))
# 最后让数据经过前馈全连接层
return self.sublayer[2](x, lambda x : self.feed_forward(x))
# 解码器
class Decoder(nn.Module):
def __init__(self, layer, N):
super(Decoder, self).__init__()
# 克隆多个解码器层
self.layers = clones(layer, N)
# 创建一个规范化层,用于最后的规范化输出
self.norm = LayerNorm(features=layer.size)
def forward(self, x, memory, source_mask, target_mask):
for layer in self.layers:
x = layer(x, memory, source_mask, target_mask)
# 返回,加上规范化层
return self.norm(x)
# TODO 4.输出部分
class Generator(nn.Module):
def __init__(self, d_model, vocab_size):
super(Generator, self).__init__()
# d_model:维度数,这里就是512
# vocab_size:目标文本词表大小
self.linear = nn.Linear(d_model, vocab_size)
def forward(self, x):
# log_softmax等价于 nn.LogSoftmax()
return torch.log_softmax(self.linear(x), dim=-1)
def test_generator():
vocab = 1000 # 源文本词表大小
target_vocab = 2000 # 目标文本词表大小
d_model = 512
d_ff = 64
N = 6
c = copy.deepcopy
x = torch.tensor([[100, 2, 421, 508], [491, 998, 1, 221]])
embed = Embeddings(vocab, d_model)
x = embed(x)
# 初始化PositionalEncoding对象
dropout = 0.1
pe = PositionalEncoding(d_model=d_model, dropout=dropout, max_len=60)
# 让数据加上pe
x = pe(x)
mha = MultiHeadAttention(head=8, embedding_dim=d_model)
ff = FeedForward(d_model=d_model, d_ff=d_ff, dropout=dropout)
# mask没有业务逻辑,只是测试代码是否能正常运行。
mask = torch.zeros(8, 4, 4)
# 实例化编码器层对象
layer = EncoderLayer(size=d_model, self_attn=c(mha), feed_forward=c(ff), dropout=dropout)
# 准备编码器对象
encoder = Encoder(layer, N)
# 让数据经过编码器
memory = encoder(x, mask)
# print("memory-->", memory.shape) # 编码器的输出结果
# 准备解码器的x,这个x是目标文本的词表数值化后的结果
x = torch.tensor([[130, 234, 521, 598], [993, 938, 123, 261]])
target_embed = Embeddings(target_vocab, d_model)
target_x = target_embed(x)
target_pe = PositionalEncoding(d_model=d_model, dropout=dropout, max_len=60)
target_x = target_pe(target_x)
# print("target_x-->", target_x.shape) # 解码器的输入
# 构建解码器层
decoder_layer = DecoderLayer(size=d_model, self_attn=c(mha), src_attn=c(mha), feed_forward=c(ff), dropout=dropout)
# 构建解码器对象
decoder = Decoder(decoder_layer, N)
# 把数据送给解码器
decoder_result = decoder(target_x, memory, mask, mask)
# print("decoder_result-->", decoder_result.shape)
# 构建输出层对象
gen = Generator(d_model, target_vocab)
output = gen(decoder_result)
print("output-->", output.shape) # [2,4,2000]
if __name__ == '__main__':
test_generator()
877

被折叠的 条评论
为什么被折叠?



