Transformer
1、概述
(一)、诞生
自从2017年此文《Attention is All You Need》提出来Transformer后,便开启了大规模预训练的新时代,也在历史的长河中一举催生出了GPT、BERT这样的里程碑模型。
(二)、优势
相比之前占领市场的LSTM和GRU模型,Transformer有两个显著的优势:
- Transformer能够利用分布式GPU进行并行训练,提升模型训练效率.
- 在分析预测更长的文本时, 捕捉间隔较长的语义关联效果更好.
2、架构
(一)、模型作用
- 基于seq2seq架构的transformer模型可以完成NLP领域研究的典型任务, 如机器翻译, 文本生成等. 同时又可以构建预训练语言模型,用于不同任务的迁移学习.
- 在接下来的架构分析中, 我们将假设使用Transformer模型架构处理从一种语言文本到另一种语言文本的翻译工作, 因此很多命名方式遵循NLP中的规则. 比如: Embeddding层将称作文本嵌入层, Embedding层产生的张量称为词嵌入张量, 它的最后一维将称作词向量等.
(二)、总体架构图
transformer总体架构:输入部分,输出部分,编码器部分,解码器部分
3、输入部分
(一)、文本嵌入式作用
无论是源文本嵌入还是目标文本嵌入,都是为了将文本中词汇的数字表示转变为向量表示, 希望在这样的高维空间捕捉词汇间的关系
# 导入必备的工具包
import torch
# 预定义的网络层torch.nn, 工具开发者已经帮助我们开发好的一些常用层,
# 比如,卷积层, lstm层, embedding层等, 不需要我们再重新造轮子.
import torch.nn as nn
# 数学计算工具包
import math
# torch中变量封装函数Variable.
from torch.autograd import Variable
# Embeddings类 实现思路分析
# 1 init函数 (self, d_model, vocab)
# 设置类属性 定义词嵌入层 self.lut层
# 2 forward(x)函数
# self.lut(x) * math.sqrt(self.d_model)
# 词嵌入类
class Embedding(nn.Module):
def __init__(self, d_model, vocab_size):
'''
初始化embedding编码模型和词嵌入维度
:param d_model: 词嵌入维度
:param vocab_size: 词表大小
'''
super().__init__()
# 初始化词嵌入对象
self.lcut = nn.Embedding(vocab_size, d_model)
# 初始化词嵌入维度
self.d_model = d_model
def forward(self, x):
'''
前向传播,对词表进行编码和放大
:param x: 输入的原始信息
:return: 放大后的编码
'''
# 对token进行编码并放大
return self.lcut(x) * math.sqrt(self.d_model)
(二)、位置编码器的作用
因为在Transformer的编码器结构中, 所有输入的词语并行训练,并没有针对词汇位置信息的处理,因此需要在Embedding层后加入位置编码器,将词汇位置不同可能会产生不同语义的信息加入到词嵌入张量中, 以弥补位置信息的缺失。
为什么选择使用sin和cos来作为位置编码?
- 绝对位置编码(0-512):冲淡文本嵌入信息,模型外推性差,当超出设置长度,无法表示位置。(ROPE alibi)
- 对绝对位置编码进行归一化:512/512=1 511/512约=1,相邻token位置信息区别不大
- 为什么不单独用一个三角函数:三角函数的周期,周期大,相邻token位置信息区别不大,周期小,间隔的token可能位置信息相似甚至相等
# 位置编码器类PositionalEncoding 实现思路分析
# 1 init函数 (self, d_model, dropout, max_len=5000)
# super()函数 定义层self.dropout
# 定义位置编码矩阵pe 定义位置列-矩阵position 定义变化矩阵div_term
# 套公式div_term = torch.exp(torch.arange(0, d_model, 2) * -(math.log(10000.0)/d_model))
# 位置列-矩阵 * 变化矩阵 哈达码积my_matmulres
# 给pe矩阵偶数列奇数列赋值 pe[:, 0::2] pe[:, 1::2]
# pe矩阵注册到模型缓冲区 pe.unsqueeze(0)三维 self.register_buffer('pe', pe)
# 2 forward(self, x) 返回self.dropout(x)
# 给x数据添加位置特征信息 x = x + Variable( self.pe[:,:x.size()[1]], requires_grad=False)
class PositionalEncoding(nn.Module):
def __init__(self, d_model, dropout, max_len=5000):
# 参数d_model 词嵌入维度 eg: 512个特征
# 参数max_len 单词token个数 eg: 60个单词
super(PositionalEncoding, self).__init__()
# 定义dropout层
self.dropout = nn.Dropout(p=dropout)
# 思路:位置编码矩阵 + 特征矩阵 相当于给特征增加了位置信息
# 定义位置编码矩阵PE eg pe[60, 512], 位置编码矩阵和特征矩阵形状是一样的
pe = torch.zeros(max_len, d_model)
# 定义位置列-矩阵position 数据形状[max_len,1] eg: [0,1,2,3,4...60]^T
position = torch.arange(0, max_len).unsqueeze(1)# 保存绝对位置信息
# print('position--->', position.shape, position)
# 定义变化矩阵div_term [1,256]
# torch.arange(start=1, end=512, 2)结果并不包含end。在start和end之间做一个等差数组 [0, 2, 4, 6 ... 510]
div_term = torch.exp(torch.arange(0, d_model, 2) * -(math.log(10000.0) / d_model))
# 位置列-矩阵 @ 变化矩阵 做矩阵运算 [60*1]@ [1*256] ==> 60 *256
# 矩阵相乘也就是行列对应位置相乘再相加,其含义,给每一个列属性(列特征)增加位置编码信息
my_matmulres = position * div_term
# print('my_matmulres--->', my_matmulres.shape, my_matmulres)
# 给位置编码矩阵奇数列,赋值sin曲线特征
pe[:, 0::2] = torch.sin(my_matmulres)
# 给位置编码矩阵偶数列,赋值cos曲线特征
pe[:, 1::2] = torch.cos(my_matmulres)
# 形状变化 [60,512]-->[1,60,512]
pe = pe.unsqueeze(0)
# 把pe位置编码矩阵 注册成模型的持久缓冲区buffer; 模型保存再加载时,可以根模型参数一样,一同被加载
# 什么是buffer: 对模型效果有帮助的,但是却不是模型结构中超参数或者参数,不参与模型训练
self.register_buffer('pe', pe)
def forward(self, x):
# 注意:输入的x形状2*4*512 pe是1*60*512 形状 如何进行相加
# 只需按照x的单词个数 给特征增加位置信息
x = x + Variable( self.pe[:,:x.size()[1]], requires_grad=False)
return self.dropout(x)
4、编码器部分*
由N个编码器层堆叠而成
每个编码器层由两个子层连接结构组成
第一个子层连接结构包括一个多头自注意力子层和规范化层以及一个残差连接
第二个子层连接结构包括一个前馈全连接子层和规范化层以及一个残差连接
(一)、注意力机制
(1)、注意力计算
A t t e n t i o n ( Q , K , V ) = S o f t m a x ( Q ⋅ K T d k ⋅ V ) Attention(Q,K,V)=Softmax(\frac{Q·K^T}{\sqrt{d_k}}·V) Attention(Q,K,V)=Softmax(dkQ⋅KT⋅V)
为什么除以根号下dk?
假设:输入x (2,4,512),分别经过三个线性层,Q、K、V (2,4,512)—> Q@K^T (2,4,4)
词嵌入维度 dk=512
前提假设 Q、K 满足标准正态分布,(均值)期望0,方差1, E(QK)=0, Var(QK)=dk, 把方差dk拉回到1,除以标准差(根号下dk)
可以解决:
- 内部协变量偏移问题 输入输出最好是同分布的,好处是: 需要的data更少,训练时间更短
- f = softmax(xi),f‘ = f(1-f)(i=j);f’ = -fi*fj (j!=i),dk越大,方差越大,数据越分散,大的很大,小的很小,再经过softmax,大的变成1,小的变成了0,类似onehot编码形式,把这些值带入softmax导数公式,梯度接近为0,梯度消失问题,模型可训练性非常差
(2)、实现
# 实现单头attn
def attention(query, key, value, dropout=None, mask=None):
'''
该函数实现单头注意力机制计算
:param query:Q
:param key:K
:param value:V
:param dropout:随机失活函数对象
:param mask:掩码矩阵padding mask(遮蔽填充的0);subsquent mask(解码并行)
:return:处理后的数据,权重概率矩阵
'''
# 拿到d_k,是一个头词嵌入的维度
d_k = query.size(-1)
# 先将Q·K^T,再除以sqrt(d_k)
scores = torch.matmul(query, key.transpose(-2, -1)) / math.sqrt(d_k)
# 使用掩码矩阵对原始信息进行遮掩。masked_fill->将目标数字替换为第二个参数。
# 此处替换是因为如果是原始0,在softmax转换后分子为1,替换为一个负值后,分子e^i极小,几乎不对结果概率产生影响
if mask is not None:
scores = scores.masked_fill(mask == 0, -1e9)
# 将上述的结果经过softmax得到权重概率矩阵
p_attn = F.softmax(scores, dim=-1)
# 对权重概率矩阵做随机失活
if dropout is not None:
p_attn = dropout(p_attn)
# 返回处理后的数据和权重概率矩阵
return torch.matmul(p_attn, value), p_attn
(二)、多头注意力机制
(1)、作用
多头注意力机制的理解:1、代码;2、原理
a、注意力计算公式,(2,4,512)—》(2,4,8,64)—》(2,8,4,64)
b、512,划分成了8个细分子空间,每个子空间就是一个观察角度,特征抽取会更丰富更全面,盲人摸象
c、降低了计算复杂度,4*512*4*512,变成了 8 个 4*64*4*64
(2)、实现
# 克隆函数
def clones(module, N):
'''
复制N个目标对象module
:param module: 目标module
:param N: 复制N个
:return: 返回一个内部有N个module对象列表
'''
# 对目标对象进行深拷贝,拷贝N份,再将放到到一个列表中输出
return nn.ModuleList([copy.deepcopy(module) for _ in range(N)])
# 多头注意力机制类
class MutiHeadAttn(nn.Module):
def __init__(self, head, embedding_dim, dropout=0.1):
'''
初始化多头注意力机制参数
:param head: 多少头
:param embedding_dim: 词向量长度
:param dropout: 随机失活比例
'''
super().__init__()
# 因为多头必须均分词向量,所以不能整除就不继续往下走
assert embedding_dim % head == 0
# 计算每个头的d_k
self.d_k = embedding_dim // head
# 复制头数
self.head = head
# 初始化注意力
self.attn = None
# 初始化随机失活对象
self.dropout = nn.Dropout(dropout)
# 初始化4个线性层,计算QKV使用3个,最后的x需要再经过1个
self.linears = clones(nn.Linear(embedding_dim, embedding_dim), 4)
def forward(self, query, key, value, mask=None):
'''
实现多头注意力机制,通过输入的QKV控制是自注意力机制还是一般注意力机制
:param query: Q
:param key: K
:param value: V
:param mask: 掩码矩阵
:return: 返回多头处理后的x
'''
# 对掩码矩阵进行升维,为了和多头注意力矩阵处理
if mask is not None:
mask = mask.unsqueeze(0)
# 获得batch_size
batch_size = query.size(0)
'''
q,k,v 三者的维度都是 (batch_size, head, seq_len, d_k) d_k就是分为多个头之后的维度 64
transpose(1, 2) 是为了让 seq-len(代表的token)与 d_k 相邻 才有意义,同时方便计算
此处的-1是为了动态适应句子长度,因为句子长度都不一样,用-1自适应更加灵活
'''
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))]
# 利用单头注意力函数计算处理后的数据
x, self.attn = attention(query, key, value, mask=mask, dropout=self.dropout)
# 先(batch_size, head, seq_len, d_k)-》(batch_size, seq_len, head, d_k)
# 将处理后的内存合并,再将数据降维回三维
x = x.transpose(1, 2).contiguous().view(batch_size, -1, self.head * self.d_k)
# 使用第4个线性层处理原始数据
return self.linears[-1](x)
(3)、小结
- 多头注意力机制:
- 每个头开始从词义层面分割输出的张量,也就是每个头都想获得一组Q,K,V进行注意力机制的计算,但是句子中的每个词的表示只获得一部分,也就是只分割了最后一维的词嵌入向量. 这就是所谓的多头.将每个头的获得的输入送到注意力机制中, 就形成了多头注意力机制.
- 多头注意力机制的作用:
- 这种结构设计能让每个注意力机制去优化每个词汇的不同特征部分,从而均衡同一种注意力机制可能产生的偏差,让词义拥有来自更多元的表达,实验表明可以从而提升模型效果.
- 实现多头注意力机制的类: MultiHeadedAttention
- 因为多头注意力机制中需要使用多个相同的线性层, 首先实现了克隆函数clones.
- clones函数的输入是module,N,分别代表克隆的目标层,和克隆个数.
- clones函数的输出是装有N个克隆层的Module列表.
- 接着实现MultiHeadedAttention类, 它的初始化函数输入是h, d_model, dropout分别代表头数,词嵌入维度和置零比率.
- 它的实例化对象输入是Q, K, V以及掩码张量mask.
- 它的实例化对象输出是通过多头注意力机制处理的Q的注意力表示
(三)、前馈全连接层
(1)、作用
作用:考虑注意力机制可能对复杂过程的拟合程度不够, 通过增加两层网络来增强模型的能力
(2)、实现
# feed_forward前馈全连接层
class PositionwiseFeedForward(nn.Module):
def __init__(self, d_model, d_ff, dropout=0.1):
'''
增加两层线性层来增加模型的拟合能力
:param d_model: 词嵌入维度
:param d_ff: 中间过度维度
:param dropout: 随机失活率
'''
super().__init__()
# 第一层线性层,将词向量维度转为中间过渡维度
self.w1 = nn.Linear(d_model, d_ff)
# 第二层线性层,将中间过度维度转为词向量维度
self.w2 = nn.Linear(d_ff, d_model)
# 随机失活率
self.dropout = nn.Dropout(dropout)
def forward(self, x):
'''
将数据经过两层线性层、relu激活函数和随机失活
:param x: 上一层输入的数据
:return: 经过线性层处理的数据
'''
# 线性层-》relu激活-》随机失活-》线性层
return self.w2(self.dropout(F.relu(self.w1(x))))
(3)、小结
- 前馈全连接层:
- 在Transformer中前馈全连接层就是具有两层线性层的全连接网络.
- 前馈全连接层的作用:
- 考虑注意力机制可能对复杂过程的拟合程度不够, 通过增加两层网络来增强模型的能力.
- 实现前馈全连接层的类: PositionwiseFeedForward
- 它的实例化参数为d_model, d_ff, dropout, 分别代表词嵌入维度, 线性变换维度, 和置零比率.
- 它的输入参数x, 表示上层的输出.
- 它的输出是经过2层线性网络变换的特征表示
(四)、规划化层
(1)、作用
它是所有深层网络模型都需要的标准网络层,因为随着网络层数的增加,通过多层的计算后参数可能开始出现过大或过小的情况,这样可能会导致学习过程出现异常,模型可能收敛非常的慢. 因此都会在一定层数后接规范化层进行数值的规范化,使其特征数值在合理范围内.
norm的作用:
1、解决内部协变量偏移问题;
2、把数据分布拉回到激活函数的非饱和区
norm会不会丢失网络学到的参数:不会。
1、norm改变的是数据的空间分布,不改变数据内部的相对大小
2、增加了两个可学习参数:缩放系数、偏移系数,分别决定方差、期望,让模型自行决定缩放、偏移多少
(2)、实现
# 规范化层
class LayerNorm(nn.Module):
def __init__(self, features, esp=1e-6):
'''
初始化规划化层的数据
:param features: 词向量维度
:param esp: 很小的值,防止在计算时数据违法
'''
super().__init__()
# 词向量维度
self.size = features
# 将a2定义为一个随着模型更新的参数
self.a2 = nn.Parameter(torch.ones(features))
# 将b2定义为一个随着模型更新的参数
self.b2 = nn.Parameter(torch.zeros(features))
# 很小的值
self.esp = esp
def forward(self, x):
'''
规范化/标准化,相当于BN层
:param x:
:return:
'''
# 对数据的最后一维做均值计算
mean = x.mean(-1, keepdim=True)
# 对数据做标准差计算
std = x.std(-1, keepdim=True)
# 用均值和标准差对数据做标准化,再用反向传播的a2和b2做学习的比例和偏移
return self.a2 * (x - mean) / (std + self.esp) + self.b2
(3)、小结
- 规范化层的作用:
- 它是所有深层网络模型都需要的标准网络层,因为随着网络层数的增加,通过多层的计算后参数可能开始出现过大或过小的情况,这样可能会导致学习过程出现异常,模型可能收敛非常的慢. 因此都会在一定层数后接规范化层进行数值的规范化,使其特征数值在合理范围内.
- 实现规范化层的类: LayerNorm
- 它的实例化参数有两个, features和eps,分别表示词嵌入特征大小,和一个足够小的数.
- 它的输入参数x代表来自上一层的输出.
- 它的输出就是经过规范化的特征表示
(五)、子层连接结构
(1)、概述
输入到每个子层以及规范化层的过程中,还使用了残差链接(跳跃连接),因此我们把这一部分结构整体叫做子层连接(代表子层及其链接结构),在每个编码器层中,都有两个子层,这两个子层加上周围的链接结构就形成了两个子层连接结构
(2)、实现
# 子层连接结构 子层(前馈全连接层 或者 注意力机制层)+ norm层 + 残差连接
# SublayerConnection实现思路分析
# 1 init函数 (self, size, dropout=0.1):
# 定义self.norm层 self.dropout层, 其中LayerNorm(size)
# 2 forward(self, x, sublayer) 返回+以后的结果
# 数据self.norm() -> sublayer()->self.dropout() + x
# 子层连接结构
class SublayerConnection(nn.Module):
def __init__(self, size, dropout=0.1):
'''
初始化归一化对象,初始化随机失活比例
:param size: 词嵌入维度
:param dropout: 随机失活参数
'''
super().__init__()
# 初始化归一化对象
self.norm = LayerNorm(size)
# 初始化随机失活对象
self.dropout = nn.Dropout(dropout)
def forward(self, x, sublayer):
'''
将原始数据和
:param x: 原始数据(刚经过位置编码)
:param sublayer: 处理数据对象,如多头注意力层
:return: 处理后的数据
'''
# 残差连接,将原始数据与处理后的数据相加
res = x + self.dropout(sublayer(self.norm(x)))
# 方法二
# res = x + self.dropout(self.norm(x.subtype(x)))
# 返回处理后的数据
return res
(3)、小结
- 子层连接结构:
- 如图所示,输入到每个子层以及规范化层的过程中,还使用了残差链接(跳跃连接),因此我们把这一部分结构整体叫做子层连接(代表子层及其链接结构), 在每个编码器层中,都有两个子层,这两个子层加上周围的链接结构就形成了两个子层连接结构.
- 实现子层连接结构的类: SublayerConnection
- 类的初始化函数输入参数是size, dropout, 分别代表词嵌入大小和置零比率.
- 它的实例化对象输入参数是x, sublayer, 分别代表上一层输出以及子层的函数表示.
- 它的输出就是通过子层连接结构处理的输出.
(六)、编码器层
(1)、作用
编码器的组成单元,每个编码器层完成一次对输入特征的提取过程,即编码过程
(2)、实现
# 编码器层
class EncoderLayer(nn.Module):
def __init__(self, size, self_attn, ffn, dropout=0.1):
'''
初始化各类对象
:param size: 词嵌入维度
:param self_attn: 注意力层对象
:param ffn: 前馈全连接层对象
:param dropout: 随机失活参数
'''
super().__init__()
# 初始化词嵌入维度
self.size = size
# 初始化注意力层对象
self.self_attn = self_attn
# 初始化前馈全连接层对象
self.ffn = ffn
# 复制两个子层连接结构
self.sublayer = clones(SublayerConnection(size), 2)
def forward(self, x, mask=None):
'''
构建一个完整的编码器层
:param x: 经过位置编码后的数据
:param mask: 掩码矩阵
:return: 处理后的数据
'''
# 计算自注意力机制后(因为qkv原始数据相等),再经过一次残差连接后输出数据
x = self.sublayer[0](x, lambda x: self.self_attn(x, x, x, mask))
# 计算前馈全连接层后,经过一次残差连接后输出数据
x = self.sublayer[1](x, self.ffn)
return x
(3)、小结
- 编码器层的作用:
- 作为编码器的组成单元, 每个编码器层完成一次对输入的特征提取过程, 即编码过程.
- 实现编码器层的类: EncoderLayer
- 类的初始化函数共有4个, 别是size,其实就是我们词嵌入维度的大小. 第二个self_attn,之后我们将传入多头自注意力子层实例化对象, 并且是自注意力机制. 第三个是feed_froward, 之后我们将传入前馈全连接层实例化对象. 最后一个是置0比率dropout.
- 实例化对象的输入参数有2个,x代表来自上一层的输出, mask代表掩码张量.
- 它的输出代表经过整个编码层的特征表示
(七)、编码器
(1)、作用
对于输入进行指定的特征提取过程,也称为编码,由多个编码器层堆叠构成
(2)、实现
# 编码器
class Encoder(nn.Module):
def __init__(self, layer, N):
'''
复制N份编码器模型
:param layer: 编码器对象
:param N: 复制多少个
'''
super().__init__()
# 复制N个编码器对象,并放到一个列表中
self.layers = clones(layer, N)
# 定义规范化对象
self.norm = LayerNorm(layer.size)
def forward(self, x, mask):
'''
构建多个编码器层构成的编码器
:param x: 经过位置编码后的数据
:param mask: 掩码矩阵
:return: 经过多层编码器层处理,再经过一层规范化的数据
'''
# 将数据在N个编码器层中串行处理
for layer in self.layers:
x = layer(x, mask)
# 将上面处理的数据进行规范化
return self.norm(x)
(3)、小结
- 编码器的作用:
- 编码器用于对输入进行指定的特征提取过程, 也称为编码, 由N个编码器层堆叠而成.
- 实现编码器的类: Encoder
- 类的初始化函数参数有两个,分别是layer和N,代表编码器层和编码器层的个数.
- forward函数的输入参数也有两个, 和编码器层的forward相同, x代表上一层的输出, mask代码掩码张量.
- 编码器类的输出就是Transformer中编码器的特征提取表示, 它将成为解码器的输入的一部分.
5、解码器部分
- 由N个解码器层堆叠而成
- 每个解码器层由三个子层连接结构组成
- 第一个子层连接结构包括一个多头自注意力子层和规范化层以及一个残差连接
- 第二个子层连接结构包括一个多头注意力子层和规范化层以及一个残差连接
- 第三个子层连接结构包括一个前馈全连接子层和规范化层以及一个残差连接
(一)、掩码机制
(1)、概述
掩代表遮掩,码就是我们张量中的数值,它的尺寸不定,里面一般只有1和0的元素,代表位置被遮掩或者不被遮掩,至于是0位置被遮掩还是1位置被遮掩可以自定义,因此它的作用就是让另外一个张量中的一些数值被遮掩,也可以说被替换, 它的表现形式是一个张量
(2)、作用
在transformer中, 掩码张量的主要作用在应用attention(将在下一小节讲解)时,有一些生成的attention张量中的值计算有可能已知了未来信息而得到的,未来信息被看到是因为训练时会把整个输出结果都一次性进行Embedding,但是理论上解码器的的输出却不是一次就能产生最终结果的,而是一次次通过上一次结果综合得出的,因此,未来的信息可能被提前利用. 所以,我们会进行遮掩
(3)、实现
# 下三角矩阵作用: 生成字符时,希望模型不要使用当前字符和后面的字符。
# 使用遮掩mask,防止未来的信息可能被提前利用
# 实现方法: 1 - 上三角矩阵
# 函数 subsequent_mask 实现分析
# 产生上三角矩阵 np.triu(m=np.ones((1, size, size)), k=1).astype('uint8')
# 返回下三角矩阵 torch.from_numpy(1 - my_mask )
# 实现掩码张量
def subsquent_mask(seq_len):
'''
构造掩码矩阵,目的是在解码器预测时掩盖未来信息
:param seq_len: 句子长,因为掩码步骤在Q*K^T后,所以矩阵大小就是句子长
:return: 返回目标掩码矩阵
'''
# 定义掩码矩阵大小,1是因为可以广播,seq_len是Q*K^T后,矩阵大小就是句子长
attn_shape = (1, seq_len, seq_len)
# triu函数是保留右上半区
# subsquent_mask = np.triu(np.ones(attn_shape), k=1).astype('uint8')
# return torch.from_numpy(1 - subsquent_mask)
# tril函数是保留左下半区
subsquent_mask = np.tril(np.ones(attn_shape), k=0).astype('uint8')
return torch.from_numpy(subsquent_mask)
(4)、小结
- 掩码张量:
- 掩代表遮掩,码就是我们张量中的数值,它的尺寸不定,里面一般只有1和0的元素,代表位置被遮掩或者不被遮掩,至于是0位置被遮掩还是1位置被遮掩可以自定义,因此它的作用就是让另外一个张量中的一些数值被遮掩, 也可以说被替换, 它的表现形式是一个张量.
- 掩码张量的作用:
- 在transformer中, 掩码张量的主要作用在应用attention(将在下一小节讲解)时,有一些生成的attetion张量中的值计算有可能已知量未来信息而得到的,未来信息被看到是因为训练时会把整个输出结果都一次性进行Embedding,但是理论上解码器的的输出却不是一次就能产生最终结果的,而是一次次通过上一次结果综合得出的,因此,未来的信息可能被提前利用. 所以,我们会进行遮掩. 关于解码器的有关知识将在后面的章节中讲解.
- 实现生成向后遮掩的掩码张量函数: subsequent_mask
- 它的输入是size, 代表掩码张量的大小.
- 它的输出是一个最后两维形成1方阵的下三角阵.
- 最后对生成的掩码张量进行了可视化分析, 更深一步理解了它的用途.
(二)、解码器层
(1)、作用
解码器的组成单元,每个解码器层根据给定的输入向目标进行特征提取操作,即解码过程。
(2)、实现
# 解码器层
class DecoderLayer(nn.Module):
def __init__(self, size, self_attn, src_attn, ffn, dropout=0.1):
'''
构建出一个完整的解码器层所需的数据和类
:param size: 词向量维度
:param self_attn: 自注意力机制对象
:param src_attn: 一般注意力机制对象
:param ffn: 前馈全连接层对象
:param dropout: 随机失活率
'''
super().__init__()
# 初始化词向量维度
self.size = size
# 自注意力机制对象
self.self_attn = self_attn
# 一般注意力机制对象
self.src_attn = src_attn
# 前馈全连接层对象
self.ffn = ffn
# 克隆三份自连接结构层
self.sublayer = clones(SublayerConnection(size), 3)
def forward(self, x, memory, source_mask, target_mask):
'''
构建出一个完整的解码器层
:param x: 目标语言经过位置编码后的数据
:param memory: 中间语义张量c
:param source_mask: 自有语言的掩码矩阵
:param target_mask: 目标语言的掩码矩阵
:return: 解码数据
'''
# 中间语义张量,也就是编码器最后输出的值
m = memory
# 输入目标语言带位置编码的数据,经过自注意力层,规范化层,残差连接
x = self.sublayer[0](x, lambda x: self.self_attn(x, x, x, target_mask))
# 将上面输出的数据作为Q输入,KV为编码器输出,计算一般注意力权重,在过规范化层,残差连接
x = self.sublayer[1](x, lambda x: self.src_attn(x, m, m, source_mask))
# 计算前馈全连接层后,经过一次残差连接后输出数据
x = self.sublayer[2](x, self.ffn)
return x
(3)、小结
- 解码器层的作用:
- 作为解码器的组成单元, 每个解码器层根据给定的输入向目标方向进行特征提取操作,即解码过程.
- 实现解码器层的类: DecoderLayer
- 类的初始化函数的参数有5个, 分别是size,代表词嵌入的维度大小, 同时也代表解码器层的尺寸,第二个是self_attn,多头自注意力对象,也就是说这个注意力机制需要Q=K=V,第三个是src_attn,多头注意力对象,这里Q!=K=V, 第四个是前馈全连接层对象,最后就是droupout置0比率.
- forward函数的参数有4个,分别是来自上一层的输入x,来自编码器层的语义存储变量mermory, 以及源数据掩码张量和目标数据掩码张量.
- 最终输出了由编码器输入和目标数据一同作用的特征提取结果.
(三)、解码器
(1)、作用
编码器的结果以及上一次预测的结果, 对下一次可能出现的’值’进行特征表示
(2)、实现
# 解码器
class Decoder(nn.Module):
def __init__(self, layer, N):
'''
初始化出N个解码器层
:param layer: 解码器层对象
:param N: 需要几个解码器层
'''
super().__init__()
# 复制出N个解码器层对象
self.layers = clones(layer, N)
# 规范化层对象
self.norm = LayerNorm(layer.size)
def forward(self, x, memory, source_mask, target_mask):
'''
构建多个解码器层构成的解码器
:param x: 经过位置编码后的数据
:param memory: 中间语义张量c,也就是编码器的输出
:param source_mask: 源语言的掩码矩阵
:param target_mask: 目标语言的掩码矩阵
:return: 规划化后的预测矩阵
'''
# 经过N个解码器层,对数据进行解码
for layer in self.layers:
x = layer(x, memory, source_mask, target_mask)
# 对数据进行规范化
return self.norm(x)
(3)、小结
- 解码器的作用:
- 根据编码器的结果以及上一次预测的结果, 对下一次可能出现的’值’进行特征表示.
- 实现解码器的类: Decoder
- 类的初始化函数的参数有两个,第一个就是解码器层layer,第二个是解码器层的个数N.
- forward函数中的参数有4个,x代表目标数据的嵌入表示,memory是编码器层的输出,src_mask, tgt_mask代表源数据和目标数据的掩码张量.
- 输出解码过程的最终特征表示
6、输出部分
- 线性层
- softmax层
# 输出部分
class Generator(nn.Module):
def __init__(self, d_model, vocab_size):
'''
初始化线性层
:param d_model: 输入词嵌入维度
:param vocab_size: 输出词嵌入维度
'''
super().__init__()
# 初始化线性层
self.project = nn.Linear(d_model, vocab_size)
def forward(self, x):
'''
输出目标数据
:param x: 编码器的输出数据
:return: 最终输出数据
'''
# 将编码器输出的数据经过线性层后,对最后一维做softmax
return F.log_softmax(self.project(x), dim=-1)
实现线性层和softmax层的类: Generator
- 初始化函数的输入参数有两个, d_model代表词嵌入维度, vocab_size代表词表大小.
- forward函数接受上一层的输出.
- 最终获得经过线性层和softmax层处理的结果.
7、模型构建
(一)、实现编码器和解码器
# 编码器-解码器联合
class EncoderDecoder(nn.Module):
def __init__(self, encoder, decoder, source_embed, target_embed, generator):
'''
初始化解码器、编码器、源输入、目标输入、输出对象
:param encoder: 编码器对象
:param decoder: 解码器对象
:param source_embed: 源输入对象
:param target_embed: 目标输入对象
:param generator: 输出对象
'''
super().__init__()
# 初始化编码器对象
self.encoder = encoder
# 初始化解码器对象
self.decoder = decoder
# 初始化源输入对象(包含位置编码)
self.src_embed = source_embed
# 初始化目标输入对象(包含位置编码)
self.tgt_embed = target_embed
# 初始化输出对象
self.generator = generator
def forward(self, source, target, source_mask, target_mask):
'''
将源数据和目标数据输入后解码
:param source: 源数据
:param target: 目标数据
:param source_mask: 源数据掩码矩阵
:param target_mask: 目标数据掩码矩阵
:return: 解码后的数据
'''
# 编码器的输出作为解码器的QK,其他的和解码器一样,再将解码器输出给输出层,最终返回最终结果
return self.generator(self.decode(self.encode(source, source_mask), source_mask, target, target_mask))
def encode(self, source, source_mask):
'''
构造编码器
:param source: 源数据
:param source_mask: 源数据掩码矩阵
:return: 编码后的数据
'''
# 先将原数据进行编码(包括位置编码和掩码),再进行编码
return self.encoder(self.src_embed(source), source_mask)
def decode(self, memory, source_mask, target, target_mask):
'''
构造解码器
:param memory: 中间语义张量c,解码器的输出
:param source_mask: 源数据掩码矩阵
:param target: 目标数据
:param target_mask: 目标数据掩码矩阵
:return: 解码后的数据
'''
# 先将目标数据进行编码(包括位置编码和掩码),再进行解码
return self.decoder(self.tgt_embed(target), memory, source_mask, target_mask)
(二)、实现组件模型
# 组建模型
def make_model(source_vocab, target_vocab, d_model=512, d_ff=2048, head=8, dropout=0.1, N=6):
'''
组建完整的transformer
:param source_vocab: 原数据
:param target_vocab: 目标数据
:param d_model: 词嵌入维度
:param d_ff: 中间过度维度
:param head: 多头数量
:param dropout: 失活比率
:param N: 组成编码器和解码器的编码器层和解码器层个数
:return: 模型
'''
# 深拷贝对象
c = copy.deepcopy
# 定义位置编码对象
position = PositionalEncoding(d_model)
# 定义多头注意力对象
attn = MutiHeadAttn(head, d_model)
# 定义前馈层对象
ff = PositionwiseFeedForward(d_model, d_ff, dropout)
# 定义解码编码器对象
model = EncoderDecoder(
# 输入编码器对象,由N个编码器层对象构成
Encoder(EncoderLayer(d_model, c(attn), c(ff), dropout), N),
# 输入解码器对象,由N个解码器层对象构成
Decoder(DecoderLayer(d_model, c(attn), c(ff), dropout), N),
# 源数据enbedding对象和位置编码对象
nn.Sequential(Embedding(d_model, source_vocab), c(position)),
# 目标数据enbedding对象和位置编码对象
nn.Sequential(Embedding(d_model, target_vocab), c(position)),
# 输出对象
Generator(d_model, target_vocab)
)
# 遍历model中所有的迭代器,只要维度大于1的就进行xavier_uniform初始化,后面的_表示在原地址修改
for p in model.parameters():
if p.dim() > 1:
nn.init.xavier_uniform_(p)
# 返回模型
return model
8、执行
if __name__ == '__main__':
# 词嵌入维度
d_model = 512
# 中间过度维度
d_ff = 1024
# 源词表维度
source_vocab = 1000
# 目标词表维度
target_vocab = 1000
# 解码后词嵌入维度
vocab_size = 5000
# 多头注意力头数
head = 8
# 编码器/解码器内部编码器层/解码器层个数
N = 6
# 深拷贝对象
c = copy.deepcopy
# 多头一般注意力和多头自注意力对象
src_attn = self_attn = MutiHeadAttn(head, d_model)
# 前馈全连接层对象
ffn = PositionwiseFeedForward(d_model, d_ff)
# 编码器层对象
el = EncoderLayer(d_model, c(self_attn), c(ffn))
# 解码器层对象
dl = DecoderLayer(d_model, self_attn, src_attn, ffn)
# 编码器对象
encoder = Encoder(el, N)
# 解码器对象
decoder = Decoder(dl, N)
# 源数据词嵌入对象
source_embed = nn.Embedding(source_vocab, d_model)
# 目标数据词嵌入对象
target_embed = nn.Embedding(target_vocab, d_model)
# 输出对象
gen = Generator(d_model, vocab_size)
# 源数据和目标数据掩码矩阵
source_mask = target_mask = subsquent_mask(4)
# 源数据和目标数据
source = target = torch.LongTensor([[1, 2, 3, 4], [6, 7, 8, 9]]) # (2,4)
# 定义解码编码器对象
ed = EncoderDecoder(encoder, decoder, source_embed, target_embed, gen)
# 调用解码编码器对象
ed_res = ed(source, target, source_mask, target_mask)
print(ed_res)
print(ed_res.shape)
# 构建模型
# model = make_model(source_vocab, target_vocab)
# print(model)