最近着手在撕Seq2Seq模型的翻译代码,运用到 了Transformer的编码器和解码器,所以今天就打算好好对Transformer相关的知识梳理一遍。将从整体到局部细节说明。附上了代码,里面流程写的比较细致
目录
优势:
1:能够利用分布式GPU并行训练,提高效率
2:预测分析更长的文本时,捕捉间隔时长文本关联更好。
Transformer的四个组成部分
一:输入部分
1:原文本嵌入层以及位置编码
2:目标文本嵌入层以及位置编码
二:输出部分
1:线性层
2:softmax层
三:编码器
1:由N个编码器组成
2:每个编码器由两个字层连接组成
3:每第一个字层包括一个多头自注意力子层和规范化层以及一个残差连接
4:第二个字层包括一个前馈全连接层以及残差连接
四:解码器层
1:由N个解码器组成
2:每个解码器由三个字层连接
3:第一个字层:一个多头自注意力层、规范化层、残差连接
4:第二个字层:多头注意力层、规范化层、残差连接
5:第三层:前馈全连接层、残差连接
文本嵌入作用
无论是原文本还是目标文本都是为了吧数字转为向量,希望可以在高纬空间捕捉关系
class Embeddings(nn.Module):
def __init__(self,d_model,vocal):
'''
d_model:词嵌入的大小
cocab:此表的大小
'''
super(Embeddings,self).__init__()
#定义emadding曾
self.lut=nn.Embedding(vocal,d_model)
self.d_model=d_model
def forward(self,x):
#x:代表输入模型的文本呢通过此映射后的数字张量
return self.lut(x)*math.sqrt(self.d_model)
d_model=512
vocab=1000
x=Variable(torch.LongTensor([[100,2,421,508],[491,998,1,221]]))
emb=Embeddings(d_model,vocab)
位置编码
在Transformer中,并没有正对词汇位置的处理,因此在Enbedding中加入位置编码,将词汇不同位置会产生不同的语意信息,加入到词嵌入的向量中,弥补了位置信息 ,位置编码层输出的是一个矩阵,每一行代表序列中的编码对象和其位置的信息相加
例如:
i | 0 | P00 | P01 | ...... | P0N | |
am | 1 | 编码矩阵-> | P10 | P11 | ...... | P1N |
a | 2 | P20 | P21 | ...... | P2N | |
people | 3 | P30 | P31 | ...... | P3N |
三角函数
位置编码是由不同频率的正弦和余弦函数给出,不同波形的波长和频率如下
公式
假设长度为L的输入序列,要计算K个元素的位置编码,位置编码有不同的频率对应
正弦和余弦函数输出,如下所示:
k:对象(句子中的字符串)在输入序列中的位置0<=K<L/2
d:输出嵌入空间的维度
P(K,J):位置函数,用于映射输入序列中k位置矩阵
n:用户定义的标量,一般会用10000
i:用于映射的到列索引,0<=i<d/2,单个i映射到正弦和余弦函数
位置编码总结
位置编码一方面通过正弦和余弦函数来编码位置信息,能够使得不同位置的编码在空间上有良好的分布。另一方面分别使用正弦和余弦函数来编码偶数和奇数的位置,以确保位置编码可以捕捉到位置之间的关系
详情过程+代码
class PositionalEncoding(nn.Module):
def __init__(self,d_model,dropout,max_len):
super(PositionalEncoding,self).__init__()
'''实例化dioput'''
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的编码太长了,将第二个维度,也就是axlen对应的那个维度缩小成x的句-'''
# x=x+Variable(self.pe[:,:x.size(1)],required_grad=False)
x=x+Variable(self.pe[:,:x.size(1)],requires_grad=False)
return self.dropout(x)
# m=nn.Dropout(p=0.2)
# input1=torch.randn(4,5)
# output=m(input1)
#
# x=torch.tensor([1,2,3,4])
# y=torch.unsqueeze(x,0)
# z=torch.unsqueeze(x,1)
d_model=512
dropout=0.1
max_len=60
x=embr
pe=PositionalEncoding(d_model,dropout,max_len)
pe_result = pe(x)
print(pe_result.shape)
掩码张量
什么是掩码张量
代表遮掩,码是张量中的数值,尺寸不一定,一般里面只有1和0,代表位置是否被遮掩,0代表遮掩,1可以自定义,因此他的作用就是让另一个张量中的一些树值被遮掩
作用
主要作用在attention,有些生成attention张量的数值可能已经了解到未来的信息,未来的信息被看到是因为在训练的时候把整个输出结果一次性进行了Enbedding,但理论上解码器输出不是一次性就会产生最终的 结果,而是一次次由上一次结论综合得出,因此,未来的信息被提前利用,所以我们会进行遮掩
详情过程+代码
def subsequent_mask(size):
'''生成掩码张量,参数size的掩码张量最后两个维度的大小,他的最后两维形成一个方'''
#首先先定义形状
attn_shape=(1,size,size)
'''然后用np.ones方向这个形状种添加1元素,形成上三角矩阵,最后未来节约空间,在使用其中的额数据类型变为无符号8位整形的unit8'''
subsequent_mask=np.triu(np.ones(attn_shape),k=1).astype('uint8')
return torch.from_numpy(1-subsequent_mask)
注意力机制
注意力机制
什么是注意力
观察事物的时候,之所以可以快速判断一种事物,是因为我们大脑可以很快的把注意力把事物具有辨别度的部分区分开,而不是从头到尾的观察,基于这样的理论,产生了注意力机制;注意力机制是注意力规则能够应用的深度学习网络的载体,除了注意力机制规则意外,还包括一些必要的全连接层以及相关张量的处理,使其与应用网络融为一体,使用自注意力计算规则的注意力机制称为自注意力机制。
Self-Attention 计算规则
主要需要三个指定输入Q(query),K(key),V(value),通过公示得到注意力的计算结果,这个结果代表Q在K和V的作用下表示,这个具体的计算规则有很多种,其中先说明一种
计算规则:
Q:代表一个提问,给出一段文本对它进行描述
K:代表提前已经给出的关键信息(可看真实数据结果)
V:表示匹配的结果
:为了防止结果过大,导致偏导数趋于0,dk一般是key的向量
softmax:在算法中,多数用于分类,在Transformer中 ,softmax函数对这些相似度分数进行归一化,生成一个权重分布,表示了在计算当前位置Q的标示的时候,K多大关注度影响。这些权重都会用来计算加权和,生成当前位置的上下文向量,该向量将作为该位置在后续的输入
Self-Attention 详解
这是弄得稍微比较粗糙,但是该有的信息都有,以下针对这个具体来说明
图1-1
图1-2
这里x1是苹果,x2是蔬菜,当然也可以有x3,x4,.......
首先举个简单的例子理解Q,K,V:
当一个求职网站上面有很多的岗位,每个岗位都有不同要求,前端(需要会HTML,CSS....);PYTHON(Django,reuqests,pandas,numpy.....)这种发出需求就是Q,并且会附上(自己公司的待遇,岗位薪资,补贴....)自身条件是否符合求职者是K,这是一场双方相互选择的情况,最后匹配的成功与否都将会对各自有用的特征进行计算,得到V
第一步:转为向量
对于编码器的输入会转为向量的形式,并且会创建,Q,K,V向量,她们是通词向量分别和3个矩阵相乘得到的,这三个矩阵通过训练获得,需要注意的是,这些向量的为主需要小于词向量的为数。
图1-1中,x1乘以权重W^q得到q1,即与该单词关联的Q向量,最终会未输入句子中的每个词创建,Q,K,V
第二步:乘积
我们是如何得到计算分数的,根据上述的例子,可以发现,这是一场相互选择的情况,有时候可能会出现1对多或者多对多的情况,为了准确得到需要的结果,需要对每个词进行计算,主要对请求条件(Q)和自身条件(K)做点乘
在向量中,点乘的结果反应了他们之间的相似度, (q1在k1^t上的投影与B的模相乘得到的结果)
如果q1k1^t
垂直,那么他们点乘为0
, 也就是他们的相似度为0
因此q1k1^t
点乘的结果越大, 我们就可以认为两个向量的相似度越高
第三步:归一化
通过以上的点乘,得到了Q的每一行与K的每一行之间的相似度结果,最终通过softmax进行归一化,得到一个直观的0~1之间的相似度的结果
详情过程+代码
def attention(query,key,value,mask=None,dropout=None):
'''注意力机制的实现,输入分别是q,k,v,mask'''
##在函数中,首先取q的最后一位的大小,一般情况下就等同于我们的词嵌入的维度,名为d_k
d_k=query.size(-1)
# d_k = d_model // num_heads # 每个头的维度
#3按照公式,将q,k,的转置相乘,里面的k是将最后两个维度镜像转置,随访系数,得到主力得分向量
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
query =key=value =pe_result
mask=Variable(torch.zeros(2,4,4))
attn,p_attn = attention(query,key,value,mask=mask)
print('自注意力机制:',attn.shape)
print('自注意力V 机制:',p_attn.shape)
多头注意力机制
什么是多头注意力机制
图1-3
根据注意力机制的知识点上继续衍生,从多头注意力的结构图中,貌似这个所谓的多个头就是指多组线性变换层,其实并不是,我只有使用了一组线性变化层,即三个变换张量对Q,K,V分别进行线性变换,这些变换不会改变原有张量的尺寸,因此每个变换矩阵都是方阵,得到输出结果后,多头的作用才开始显现,每个头开始从词义层面分割输出的张量,也就是每个头都想获得一组Q,K,V进行注意力机制的计算,但是句子中的每个词的表示只获得一部分,也就是只分割了最后一维的词嵌入向量.这就是所谓的多头,将每个头的获得的输入送到注意力机制中,就形成多头注意力机制.
多头注意力机制的作用
这种结构设计能让每个注意力机制去优化每个词汇的不同特征部分,从而均衡同一种注意力机制可能产生的偏差,让词义拥有来自更多元的表达,实验表明可以从而提升模型效果.
详情过程+代码
class MultiHeadAttention(nn.Module):
def __init__(self,head,embedding_dim,dropout=0.1):
'''
head:代表几个投
embadding:代表词嵌入的维度
droupout:
'''
super(MultiHeadAttention,self).__init__()
#确认一个事实:多投数量的head需要整除词嵌入的维度embadding_dim
print(embedding_dim)
print(head)
assert embedding_dim%head==0
##得到每个投的向量
self.d_k=embedding_dim//head
self.head=head
self.embedding_dim=embedding_dim
##获得线形曾,要获得4哥,分别是q,k,v和输出
self.linears=clones(nn.Linear(embedding_dim,embedding_dim),4)
##初始化注意力机制张良
self.attn=None
##初始化dropiyt对象
self.dropout=nn.Dropout(p=dropout)
def forward(self,query,key,value,mask=None):
if mask is not None:
mask=mask.unsqueeze(1)##unsqueeze
#3得到batch_size
batch_size=query.size(0)
##首先使用zip将网络层和输入数据链接在一起,模型的输出利用view和transpose进行维护和形状
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))]
#3将每个投的输出传入到注意力层
# print(query.size())
# print(key.size())
# print(value.size())
# print(mask.size())
x,self.attn=attention(query,key,value,mask=mask,dropout=self.dropout)
'''
得到每个头的计算结果是4维的张量,需要进行形状的转换
前面以及将1,2两个维度进行转置,在这里需要转置回来
注意:经历了transpose()方法在之后,必须要使用contiguous方法,不然使用不了view()方法
'''
x=x.transpose(1,2).contiguous().view(batch_size,-1,self.head*self.d_k)
##最后的输出x放到输入列表种的最后一个的线性层种进行处理,得到最终的多投注意力的结构输出
return self.linears[-1](x)
head=8
embedding_dim=512
dropout=0.2
query=key=value=pe_result
mask=Variable(torch.zeros(2,4,4))
mha=MultiHeadAttention(head,embedding_dim,dropout)
print(mha)
mha_result=mha(query,key,value,mask)
print('----------------------------')
前馈全连接层
什么是前馈全连接层
在Transformer中前馈全连接层具有两层线性层的全连接网络,每个位置的但粗经过这个完全相同的前馈神经网络,第一个全连接层的激活函数为ReLU,可以表示为
前馈全连接层的作用
考虑注意力机制可能对复杂的过程拟合度不够,通过增加两层网络增强模型的能力,虽然在每个编码器和解码器中的结构相同,但是不共享数据。
详情过程+代码
'''前馈全连阶层'''
class PositionwiseFeeForward(nn.Module):
def __init__(self,d_model,d_ff,dropout=0.1):
'''
d_model:代表潜入的维度,同时也是两个输入和输出的维度
d_ff:第一个输出,第二个输入
'''
super(PositionwiseFeeForward,self).__init__()
self.w1=nn.Linear(d_model,d_ff)
self.w2 = nn.Linear(d_ff, d_model)
self.dropout=nn.Dropout(p=dropout)
def forward(self,x):
'''x:代表上一层的输出,传入第一个进过relu激活然后传入第二次'''
return self.w2(self.dropout(F.relu(self.w1(x))))
d_model=512
d_ff=64
x=mha_result
ff=PositionwiseFeeForward(d_model,d_ff,dropout)
ff_result=ff(x)
规范化层
作用
它是所有深层网络摸型都需要的标准网络层,因为随着网络层数的增加,通过多层的计算后参数可能开始出现过大或过小的情况,这样可能会导致学习过程出现异常,模型可能收敛非常的慢.因此都会在一定层数后接规范化层进行数值的规范化,使其特征数值在合理范围内.
子层连接结构
什么是子层连接结构
如图所示,输入到每个子层以及规范化层的过程中,还使用了残差链接(跳跃连接),因此我们把这一部分结构整体叫做子层连接(代表子层及其链接结构),在每个编码器层中,都有两个子层,这两个子层加上周围的链接结构就形成了两个子层连接结构.
详情过程+代码
'''实现规范化层'''
class LayerNorm(nn.Module):
def __init__(self,features,eps=1e-6):
'''初始化两个,一个是feature,表示词潜入的维度,另一个足够小的数,在规范化公式重的分母出现,防止分母是0'''
super(LayerNorm, self).__init__()
'''
根据feature的形状初始化2个参数的张亮a2,b2,第一个初始化为1的,也就是里面全是1,第二个初始化0,这就是规范化
因为直接对上一层得到的结果做规范化计算,将改变结果的正常表,因此需要有数作为调试
使得可以满足规范化的要求,又不能改变正对目标的特征,最后封装用nn.parameter
'''
self.a2=nn.Parameter(torch.ones(features))
self.b2 = nn.Parameter(torch.zeros(features))
self.eps=eps #eps转入类中
def forward(self,x):
'''
在函数中,对输入的x求均值保持输出和输入是一致的,接下来求标准差根据规范化公式,用x-均值/标准差得到规范
最后对结果*我们的缩放参数
'''
mean=x.mean(-1,keepdim=True)
std = x.std(-1, keepdim=True)
datas=self.a2*(x-mean)/(std+self.eps)+self.b2
return self.a2*(x-mean)/(std+self.eps)+self.b2
feature=d_model=512
eps=1e-6
x=ff_result
ln=LayerNorm(feature,eps)
ln_result=ln(x)
####子层
class SublayerConnection(nn.Module):
def __init__(self,size,dropout):
'''size 词潜入的大小'''
super(SublayerConnection, self).__init__()
'''实力化规范化操作'''
self.norm=LayerNorm(size)
self.dropout=nn.Dropout(p=dropout)
def forward(self,x,sublayer):
'''
向前逻辑中,接受上一个层或者子层的输入作为一个参数,将子层中的子层函数作为第二个参数
对输出进行规范化然后传给子层处理进行dropout处理
随机停止一些网络中的神经元的作用,来防止过你和最后一个add操作
因为是条约连接,所以x和dropout后的子层出书结果相加作为最后的子层连接
'''
print('规范化层')
print(self.dropout(sublayer(self.norm(x))))
return x+self.dropout(sublayer(self.norm(x)))
size=d_mode=512
head=8
droupout=0.2
x=pe_result
mask=Variable(torch.zeros((2,8,4)))
self_attn=MultiHeadAttention(head,d_mode)
sublayer=lambda x:self_attn(x,x,x,mask)
sc=SublayerConnection(size,dropout)
编码器
编码器用于对输入进行指定的特征提取过称,也称为编码,由N个编码器层堆叠而成.
编码器层
作用
作为编码器的组成单元,每个编码器层完成一次对输入的特征提取过程,即编码过程.
详情过程+代码
'''编码器层'''
class EncoderLayer(nn.Module):
def __init__(self,zise,self_attn,feed_forwad,dropout):
'''
size:词嵌入大小
self_attn:将之后的多头注意力机制子层实力话对象,并且是自注意力机制
feed_forwad:之后传入的前馈全连接层实力话对象,最后一个置0比率dropout
'''
super(EncoderLayer, self).__init__()
self.self_attn=self_attn
self.feed_forwad=feed_forwad
'''有两个子层连接结构,所以用clones'''
self.sublayer=clones(SublayerConnection(size,dropout),2)
self.size=size
def forward(self,x,mask):
print('编码层')
'''
表示上一层的输出和掩码张量
里面是按照结构图的左边的流程,通过一个子层的连接结构,暴汗多头自注意力子层
然后通过第二个子层连接结构,其中暴汗前馈全连接子层
'''
x=self.sublayer[0](x,lambda x:self.self_attn(x,x,x,mask))
print(x)
return self.sublayer[1](x,self.feed_forwad)
size=d_mode=512
head=8
d_ff=64
x=pe_result
self_attn=MultiHeadAttention(head,d_mode)
ff=PositionwiseFeeForward(d_model,d_ff,dropout)
mask=Variable(torch.zeros(2,4,4))
el=EncoderLayer(size,self_attn,ff,dropout)
el_result=el(x,mask)
'''编码器代码'''
class Encode(nn.Module):
def __init__(self,layer,N):
'''编码器和层数'''
super(Encode, self).__init__()
self.layer=layer
'''规范化层用在编码器的前面'''
self.norm=LayerNorm(layer.size)
def forward(self,x,mask):
'''
首先对克隆的编码器循环得到一个新的x(上一层的输出)
这个循环的过程就等于输出的x 进过量N个编码器层的处理
最后通过规范化的对象self.norm进行处理
'''
for layer in self.layer:
x=layer(x,mask)
return self.norm(x)
size=512
head=8
d_model=512
d_ff=64
c=copy.deepcopy
attn=MultiHeadAttention(head,d_mode)
droupout=0.2
ff=PositionwiseFeeForward(d_mode,d_ff,dropout)
layer=EncoderLayer(size,c(attn),c(ff),dropout)
N=8
mask=Variable(torch.zeros((8,4,4)))
解码器
根据编码器的结果以及上一次预测的结果,对下一次可能出现的"值"进行特征表示.
解码器层
作用
作为解码器的组成单元,每个解码器层根据给定的输入向目标方向进行特征提取操作,即解码过程.
解码器部分
1:由N个解码器组成
2:每个解码器由三个字层连接
3:第一个字层:一个多头自注意力层、规范化层、残差连接
4:第二个字层:多头注意力层、规范化层、残差连接
5:第三层:前馈全连接层、残差连接
说明
解码器层中的各个部分,如,多头注意力机制,规范化层,前馈全连接网络,子层连接结构都与编码器中的实现相同.因此这里可以直接拿来构建解码器层.
详情过程+代码
'''解码层代码实现'''
class DecoderLayer(nn.Module):
def __init__(self,size,self_attn,src_attn,feed_forward,dropout):
'''
初始化的参数有5个,分别是size,代表此先去的维度大小,同时表示编码器层的尺寸
第二个是多头自注意力对象,Q=K=V
第三个是多头注意力对象Q!=K=V
'''
super(DecoderLayer,self).__init__()
self.size=size
self.self_attn=self_attn
self.src_attn=src_attn
self.feed_forward=feed_forward
##根据姐沟通使用clones克隆三个子层
self.sublayer=clones(SublayerConnection(size,dropout),3)
def forward(self, x, memory,source_mask,target_mask):
'''
四个参数分别来自于上一层的输入x,来自于编码器的语义存储变量的mermory,以及元数据的掩码张量
和目标数据的掩码张量,将memory表示成m之后更方便使用
'''
m=memory
# 将x传入第一个子层结构,第一个子层结构的输入分别是x和self-attn函数,因为是自注意力机制,所以q,k,v都是x
# #最后一个参数是目标数据掩码张量,这时要对目标数据进行遮掩,因为此时模型可能还没有生成任何目标数据
# #比如在解码器准备生成第一个字符或词汇时,我们其实已经传入了第一个字符以便计算损失
# #但是我们不希望在生成第一个字符时模型能利用这个信息,因此我们会将其遮掩,同样生成第二个字#模型只能使用第一个字符或词汇信息,第二个字符以及之后的信息都不允许被模型使用
x=self.sublayer[0](x,lambda x:self.self_attn(x,x,x,target_mask))
# 接着进入第二个子层,这个子层中常规的注意力机制,q是输入x;k,v是编码层输出memory,
# 同样也传入source_mask,但是进行源数据遮掩的原因并非是抑制信息泄漏,而是遮蔽掉对结果没有意义的字符而产生的注意力数值
# #以此提升模型效果和训练速度,这样就完成了第二个子层的处理
return self.sublayer[2](x,self.feed_forward)
class Decoder(nn.Module):
def __init__(self,layer,N):
'''初始化函数的参数有两个,一个是解码器的layer,第二个是解码器的N'''
super(Decoder,self).__init__()
self.layers=clones(layer,N)
self.norm=LayerNorm(layer.size)
# 首先使用clones方法克隆了N个layer,然后实例化了一个规范化层.
# 因为数据走过了所有的解码器层后最后要做规范化处理,
def forward(self, x, memory,source_mask,target_mask):
'""forward函数中的参数有4个,x代表目标数据的嵌入表示,memory是编码器层的输出, source_mask,target_mask代表源数据和目标数据的码张量'' '
# 然后就是对每个层进行循环,当然这个循环就是变量x通过每一个层的处理,#得出最后的结果,再进行一次规范化返回即可.
for layer in self.layers:
x = layer(x, memory, source_mask, target_mask)
return self.norm(x)
size=d_model=512
head=8
d_ff=64
droupout=0.2
c=copy.deepcopy
attn=MultiHeadAttention(head,d_mode)
ff=PositionwiseFeeForward(d_model,d_ff,dropout)
layer = DecoderLayer(d_model,c(attn),c(attn),c(ff),dropout)
N=8
x= pe_result
memory =Encode(layer,N)
mask=Variable(torch.zeros(2,4,4))
source_mask=target_mask = mask
de = Decoder(layer,N)
de_result =de(x,memory,source_mask, target_mask)
print(de_result)
print(de_result.shape)
注意
编码器和解码器中,有很多都是两个子层连接的结构,所以需要用clones函数进行克隆
def clones(module,N):
'''
modele:代表要克隆的目标网络层
N将modeule克罗多少哥
'''
return nn.ModuleList([copy.deepcopy(module) for _ in range(N)])
这篇文章陆陆续续写了几天,可能也有补充的不够完善,具体实现过程我放到了代码里面
如果出现有不对的地方也请各位大佬指出