注:
此博文仅为了解transformer架构,如果使用,建议直接调用库就行了
Transformer的优势
相比之前占领市场的LSTM和GRU模型,Transformer有两个显著的优势:
1. Transformer能够利用分布式GPU进行并行训练,提升模型训练效率。
2. 在分析预测更长的文本时,捕捉间隔较长的语义关联效果更好.
认识Transformer架构
Transformer模型的作用:
基于seq2seq架构的transformer模型可以完成NLP领域研究的典型任务,如机器翻译,文本生成等.同时又可以构建预训练语言模型,用于不同任务的迁移学习.
声明:
在接下来的架构分析中,我们将假设使用Transformer模型架构处理从一种语言文本到另一种语言文本的翻译工作,因此很多命名方式遵循NLP中的规则.比如:Embeddding层将称作文本嵌入层,Embedding层产生的张量称为词嵌入张量,它的最后一维将称作词向量等
Transformer总体架构可分为四个部分:
输入部分
源文本嵌入层及其位置编码器
文本嵌入层的作用:无论是源文本嵌入还是目标文本嵌入,都是为了将文本中词汇的数字表示转变为向量表示,希望在这样的高维空间捕捉词汇间的关系.
位置编码器的作用:因为在Transformer的编码器结构中,并没有针对词汇位置信息的处理,因此需要在Embedding层后加入位置编码器,将词汇位置不同可能会产生不同语义的信息加入到词嵌入张量中,以弥补位置信息的缺失.
# 构建Embedding类实现文本嵌入层代码
import torch
import torch.nn as nn
import math
from torch.autograd import Variableclass Embeddings(nn.Module):def __init__(self, d_model, vocab):"""类的初始化Args:d_model (_type_): 词嵌入的维度vocab (_type_): 词表的大小"""super(Embeddings, self).__init__()self.lut = nn.Embedding(vocab, d_model)self.d_model = d_modeldef forward(self, x):"""前向传播函数Args:x (_type_): 输入的索引张量,形状为 (L, N)Returns:_type_: 词嵌入张量,形状为 (L, N, d_model)"""return self.lut(x) * math.sqrt(self.d_model)
文本嵌入层使用
d_model=512
vocab=1000
x=Variable(torch.LongTensor([[1,2,3,4,5],[6,7,8,9,10]]))
embedding_layer=Embeddings(d_model,vocab)
output=embedding_layer(x)
print(output)
位置编码器
# 位置编码器
class PositionalEncoding(nn.Module):def __init__(self, d_model, dropout, max_len=5000):"""位置编码器Args:d_model (_type_): 词嵌入的维度dropout (_type_): 置零的比例,让一定的神经元失效max_len (int, optional): 每个句子的最大长度. Defaults to 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)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)self.register_buffer('pe', pe)def forward(self, x):"""_summary_Args:x (_type_): 文本序列的的词嵌入表示"""x=x+Variable(self.pe[:, :x.size(1)], requires_grad=False)return self.dropout(x)
使用
# 绘制词汇向量中特征的分布曲线
import matplotlib.pyplot as plt
import numpy as npplt.figure(figsize=(15, 5))
pe=PositionalEncoding(20,0)
y=pe(Variable(torch.zeros(1, 100,20)))
plt.plot(np.arange(100), y[0, :, 4:8].data.numpy())
plt.legend(['dim %d'%p for p in [4, 5, 6, 7]])
# 输出效果分析:
# 每条颜色的曲线代表某一个词汇中的特征在不同位置的含义.
# 保证同一词汇随着所在位置不同它对应位置嵌入向量会发生变化.
# 正弦波和余弦波的值域范围都是1到﹣1这又很好的控制了嵌入数值的大小,有助于梯度的快速计算.
目标文本嵌入层及其位置编码器
输出部分
线性层的作用:通过对上一步的线性变化得到指定维度的输出,也就是转换维度的作用.
softInax层的作用:使最后一维的向量中的数字缩放到0-1的概率值域内,并满足他们的和为1.
import torch.nn.functional as F
class Generator(nn.Module):def __init__(self, d_model, vocab):super(Generator, self).__init__()self.proj = nn.Linear(d_model, vocab)def forward(self, x):return F.log_softmax(self.proj(x), dim=-1)
编码器部分
由N个编码器层堆叠而成
每个编码器层由两个子层连接结构组成
第一个子层连接结构包括一个多头自注意力子层和规范化层以及一个残差连接
第二个子层连接结构包括一个前馈全连接子层和规范化层以及一个残差连接
class Encoder(nn.Module):def __init__(self, layer, N):"""_summary_Args:layer (_type_): 编码器层N (_type_): 编码器层个数"""super(Encoder, self).__init__()self.layers=clones(layer, N)self.norm=LayerNorm(layer.size)def forward(self, x, mask):for layer in self.layers:x=layer(x, mask)return self.norm(x)
解码器部分
由N个解码器层堆叠而成
每个解码器层由三个子层连接结构组成
第一个子层连接结构包括一个多头自注意力子层和规范化层以及一个残差连接
第二个子层连接结构包括一个多头注意力子层和规范化层以及一个残差连接
第三个子层连接结构包括一个前馈全连接子层和规范化层以及一个残差连接
class Decoder(nn.Module):def __init__(self, layer, N):super(Decoder, self).__init__()self.layers=clones(layer, N)self.norm=LayerNorm(layer.size)def forward(self, x, memory, src_mask, tgt_mask):for layer in self.layers:x=layer(x, memory, src_mask, tgt_mask)return self.norm(x)
组装构建各部件
class EncoderDecoder(nn.Module):def __init__(self, encoder, decoder, src_embed, tgt_embed, generator):"""编码器-解码器结构Args:encoder (_type_): 编码器对象decoder (_type_): 解码器对象src_embed (_type_): 源数据嵌入函数tgt_embed (_type_): 目标数据嵌入函数generator (_type_): 类别生成器对象"""super(EncoderDecoder, self).__init__()self.encoder = encoderself.decoder = decoderself.src_embed = src_embedself.tgt_embed = tgt_embedself.generator = generatordef forward(self, src, tgt, src_mask, tgt_mask):"""_summary_Args:src (_type_): 源数据tgt (_type_): 目标数据src_mask (_type_): 源数据掩码张量tgt_mask (_type_): 目标数据掩码张量"""return self.decode(self.encode(src, src_mask), src_mask,tgt, tgt_mask)def encode(self, src, src_mask):"""编码函数Args:src (_type_): _description_src_mask (_type_): _description_"""return self.encoder(self.src_embed(src), src_mask)def decode(self, memory, src_mask, tgt, tgt_mask):"""解码函数Args:memory (_type_): _description_src_mask (_type_): _description_tgt (_type_): _description_tgt_mask (_type_): _description_"""return self.decoder(self.tgt_embed(tgt), memory, src_mask, tgt_mask)
def make_model(src_vocab, tgt_vocab, N=6, d_model=512, d_ff=2048, head=8, dropout=0.1):"""_summary_Args:src_vocab (_type_): 源数据词汇总数tgt_vocab (_type_): 目标数据词汇总数N (int, optional): 子层堆叠数量. Defaults to 6.d_model (int, optional): 词向量维度. Defaults to 512.d_ff (int, optional): 前馈全连接网络的变换矩阵维度. Defaults to 2048.head (int, optional): 多头注意力的多头数. Defaults to 8.dropout (float, optional): 置零比率. Defaults to 0.1."""c=copy.deepcopyattn=MultiHeadedAttention(head,d_model)ff=PositionwiseFeedForward(d_model,d_ff,dropout)position=PositionalEncoding(d_model,dropout)model=EncoderDecoder(Encoder(EncoderLayer(d_model,c(attn),c(ff),dropout),N),Decoder(DecoderLayer(d_model,c(attn),c(attn),c(ff),dropout),N),nn.Sequential(Embeddings(d_model,src_vocab),c(position)),nn.Sequential(Embeddings(d_model,tgt_vocab),c(position)),Generator(d_model,tgt_vocab))for p in model.parameters():if p.dim()>1:nn.init.xavier_uniform_(p)return model
copy任务
任务描述:针对数字序列进行学习,学习的最终目标是使输出与输入的序列相同
任务意义: copy任务在模型基础测试中具有重要意义,因为copy操作对于模型来讲是一条明显规律,因此模型能否在短时间内,小数据集中学会它,可以帮助我们断定模型所有过程是否正常,是否已具备基本学习能力
from pyitcast.transformer_utils import Batchdef data_generator(V,batch,nbatches):"""数据集生成器Args:V (_type_): 随机生成数字的最大值+1batch (_type_): 放入多少数据集后一次更新nbatches (_type_): 一共要输入多少次batch"""for i in range(nbatches):data=torch.from_numpy(np.random.randint(1,V,size=(batch,10)))data[:,0]=1source=Variable(data,requires_grad=False)target=Variable(data,requires_grad=False)yield Batch(source,target,10)
专用名词
掩码张量
什么是掩码张量?
掩代表遮掩,码就是我们张量中的数值,它的尺寸不定,里面一般只有1和0的元素,代表位置被遮掩或者不被遮掩,至于是0位置被遮掩还是1位置被遮掩可以自定义,因此它的作用就是让另外一个张量中的一些数值被遮掩,也可以说被替换,它的表现形式是一个张量。
掩码张量的作用:
在transformer中,掩码张量的主要作用在应用attention(将在下一小节讲解)时,有一些生成的attention张量中的值计算有可能已知了未来信息而得到的,未来信息被看到是因为训练时会把整个输出结果都一次性进行Embedding,但是理论上解码器的的输出却不是一次就能产生最终结果的,而是一次次通过上一次结果综合得出的,因此,未来的信息可能被提前利用.所以,我们会进行遮掩。
def subsquest_mask(size):"""生成掩码张量代码Args:size (_type_): 掩码张量最后两个维度的大小,它的最后两维形成一个方阵"""attn_shape=(1, size, size)subsquest_mask=np.triu(np.ones(attn_shape), k=1).astype('uint8')return torch.from_numpy(1-subsquest_mask)
注意力机制
什么是注意力?
我们观察事物时,之所以能够快速判断一种事物(当然允许判断是错误的), 是因为我们大脑能够很快把注意力放在事物最具有辨识度的部分从而作出判断,而并非是从头到尾的观察一遍事物后,才能有判断结果.正是基于这样的理论,就产生了注意力机制.
注意力计算规则:
它需要三个指定的输入Q(query), K(key), V(value), 然后通过公式得到注意力的计算结果,这个结果代表query在key和value作用下的表示.而这个具体的计算规则有很多种,我这里只介绍我们用到的这一种.
什么是注意力机制:
注意力机制是注意力计算规则能够应用的深度学习网络的载体,除了注意力计算规则外,还包括一些必要的全连接层以及相关张量处理,使其与应用网络融为一体.使用自注意力计算规则的注意力机制称为自注意力机制.
注意力机制的计算规则
import torch.nn.functional as Fdef attention(query, key, value, mask=None, dropout=None):"""注意力机制的实现代码Args:query (_type_): 查询向量,是一段准备被概括的文本key (_type_): 键向量,是给出的提示value (_type_): 值向量,是大脑中的对提示K的延伸mask (_type_, optional): 掩码张量. Defaults to None.dropout (_type_, optional): 丢弃率. Defaults to None.Returns:_type_: 注意力机制的输出"""d_k=query.size(-1)scores=torch.matmul(query, key.transpose(-2, -1))/math.sqrt(d_k)if mask is not None:scores=scores.masked_fill(mask==0, -1e9)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
多头注意力机制
只有使用了一组线性变化层,即三个变换张量对Q,K,V分别进行线性变换,这些变换不会改变原有张量的尺寸,因此每个变换矩阵都是方阵。
得到输出结果后,多头的作用才开始显现,每个头开始从词义层面分割输出的张量,也就是每个头都想获得一组Q,K,V 进行注意力机制的计算,但是句子中的每个词的表示只获得一部分,也就是只分割了最后一维的词嵌入向量,这就是所谓的多头。
将每个头的获得的输入送到注意力机制中,就形成多头注意力机制.
多头注意力机制的作用:这种结构设计能让每个注意力机制去优化每个词汇的不同特征部分,从而均衡同一种注意力机制可能产生的偏差,让词义拥有来自更多元的表达,实验表明可以从而提升模型效果。
import copydef clones(module, N):"""用于生成相同的网络层Args:module (_type_): 要克隆的目标网络层N (_type_): 需要克隆的数量"""return nn.ModuleList([copy.deepcopy(module) for _ in range(N)])class MultiHeadedAttention(nn.Module):def __init__(self, head, embedding_dim, dropout=0.1):"""多头注意力机制的实现代码Args:head (_type_): 头数embedding_dim (_type_): 词嵌入维度dropout (_type_, optional): 丢弃率. Defaults to 0.1."""super(MultiHeadedAttention, self).__init__()# 保证d_model是头数的整数倍assert embedding_dim%head==0self.d_k=embedding_dim // headself.head=headself.embedding_dim=embedding_dimself.linears=clones(nn.Linear(embedding_dim, embedding_dim), 4)self.attn=Noneself.dropout=nn.Dropout(p=dropout)def forward(self, query, key, value, mask=None):"""前向逻辑结构Args:query (_type_): _description_key (_type_): _description_value (_type_): _description_mask (_type_, optional): _description_. Defaults to None."""if mask is not None:mask=maskbatch_size=query.size(0)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, self.dropout)x = x.transpose(1, 2).contiguous().view(batch_size, -1, self.head * self.d_k)return self.linears[-1](x)
前馈全连接层
在Transformer中前馈全连接层就是具有两层线性层的全连接网络.
前馈全连接层的作用:
考虑注意力机制可能对复杂过程的拟合程度不够,通过增加两层网络来增强模型的能力.
class PositionwiseFeedForward(nn.Module):def __init__(self, d_model, d_ff, dropout=0.1):"""_summary_Args:d_model (_type_): _description_d_ff (_type_): 词嵌入的维度dropout (float, optional): _description_. Defaults to 0.1."""super(PositionwiseFeedForward, self).__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):return self.w2(self.dropout(torch.relu(self.w1(x))))
规范化层
规范化层的作用:
它是所有深层网络模型都需要的标准网络层,因为随着网络层数的增加,通过多层的计算后参数可能开始出现过大或过小的情况,这样可能会导致学习过程出现异常,模型可能收敛非常的慢.因此都会在一定层数后接规范化层进行数值的规范化,使其特征数值在合理范围内.
class LayerNorm(nn.Module):def __init__(self, features, eps=1e-6):"""_summary_Args:features (_type_): 词嵌入维度eps (_type_, optional): 防止分母为0. Defaults to 1e-6."""super(LayerNorm, self).__init__()self.a2=nn.Parameter(torch.ones(features))self.b2=nn.Parameter(torch.zeros(features))self.eps=epsdef forward(self, x):"""_summary_Args:x (_type_): 上一层的输出"""mean=x.mean(-1, keepdim=True)std=x.std(-1, keepdim=True)return self.a2*(x-mean)/(std+self.eps)+self.b2
子层连接结构
输入到每个子层以及规范化层的过程中,还使用了残差链接(跳跃连接),因此我们把这一部结构整体叫做子层连接(代表子层及其链接结构),在每个编码器层中,都有两个子层,这两个子层加上周围的链接结构就形成了两个子层连接结构.
class SublayerConnection(nn.Module):def __init__(self, size, dropout):"""_summary_Args:size (_type_): 词嵌入维度dropout (_type_): 随机抑制比率"""super(SublayerConnection, self).__init__()self.norm=LayerNorm(size)self.dropout=nn.Dropout(dropout)def forward(self, x, sublayer):return x+self.dropout(sublayer(self.norm(x)))
编码器层
编码器层的作用:作为编码器的组成单元,每个编码器层完成一次对输入的特征提取过程,即编码过程
class EncoderLayer(nn.Module):def __init__(self, size, self_attn, feed_forward, dropout):"""_summary_Args:size (_type_): 词维度大小self_attn (_type_): 多头注意力层feed_forward (_type_): 实例化的前馈全连接层dropout (_type_): _description_"""super(EncoderLayer, self).__init__()self.self_attn=self_attnself.feed_forward=feed_forwardself.sublayer=clones(SublayerConnection(size, dropout),2)self.size=sizedef forward(self, x, mask):x=self.sublayer[0](x, lambda x:self.self_attn(x, x, x, mask))return self.sublayer[1](x, self.feed_forward)
解码器层
class DecoderLayer(nn.Module):def __init__(self, size, self_attn, src_attn, feed_forward, dropout):"""_summary_Args:size (_type_): 词维度大小self_attn (_type_): 注意力机制,自注意力src_attn (_type_): 非自注意力机制feed_forward (_type_): 前馈全连接层dropout (_type_): _description_"""super(DecoderLayer, self).__init__()self.size=sizeself.self_attn=self_attnself.src_attn=src_attnself.feed_forward=feed_forwardself.sublayer=clones(SublayerConnection(size, dropout), 3)def forward(self, x, memory, src_mask, tgt_mask):"""_summary_Args:x (_type_): 上一层输入memory (_type_): 来自编码器层语义储存变量src_mask (_type_): 源数据掩码张量tgt_mask (_type_): 目标数据掩码张量"""m=memoryx=self.sublayer[0](x, lambda x: self.self_attn(x, x, x, tgt_mask))x=self.sublayer[1](x, lambda x: self.src_attn(x, m, m, src_mask))return self.sublayer[2](x, self.feed_forward)