transformer代码学习
理论介绍
transformer有两个输入,编码端输入和解码端输入。编码端输入经过词向量层以及位置编码层得到一个最终输入,然后流经自注意力层,然后经过前馈神经网络层,得到一个编码端的输出;同样,解码端的输入经过词向量层和位置编码层,得到最终输入,经过掩码自注意力层(把单词全部mask掉),然后流经交互注意力层(编码端的输出和解码端的信息进行一个交互,q来自解码端,k,v来自编码端输出),然后经过前馈神经网络层,最后得到一个输出。
问题一:一条数据中三个句子分别代表什么?
编码端的输入;解码端的输入;解码端的真实标签
问题二:
P代表什么?PAD填充字符
S代表什么?开始标志
E代表什么?结束标志
代码部分
主函数
if __name__ =='__main__':#句子的输入部分
sentences =['ich mochte ein bier P','S i want a beer','i want a beer E']#transformer parameters#padding should be zero#构建词表#编码器词表
src_vocab ={'P':0,'ich':1,'mochte':2,'ein':3,'bier':4}
src_vocab_size =len(src_vocab)#解码器词表
tgt_vocab ={'P':0,'i':1,'want':2,'a':3,'beer':4}
tgt_vocab_size =len(tgt_vocab)
src_vocab =5#length of source
tgt_vocab =5#length of target#模型参数
d_model =512#每个词转成embedding时的大小
d_ff =2048#前馈神经网络中映射到多少维度
d_k = d_v =64#
n_layers =6# number of encoder of decoder layer
n_heads =8#多头注意力机制的头的数量
model = Transformer()
代码1:Transformer整体架构层代码
编码层
解码层
输出层
classTransformer(nn.Module):def__init__(self):super(Transformer, self).__init__()
self.encoder = Encoder()
self.decoder = Decoder()
self.projection = nn.Linear(d_model, tgt_vocab_size, bias=False)#实现函数 defforward(self, enc_inputs, dec_inputs):#这里有两个数据进行输入,解码端输入形状为[batch_size, src_len],解码端输入形状为[batch_size, tgt_len]#enc_inputs作为输入,输出由自己的函数内部指定,想要什么指定输出什么,可以是全部tokens的输出,可以是特定每一层的输出;也可以是中间某些参数的输出;#enc_outputs是decoder主要的输出,enc_self_attns是QK转置相乘之后softmax之后的矩阵值(相似度),代表的是每个单词和其他单词的相关性。
enc_outputs, enc_self_attns = self.encoder(enc_inputs)#dec_outputs是decoder主要输出,用于后续的Linear映射;dec_self_attns类比于enc_self_attns是查看每个单词对decoder中输入的其余单词的相关性;dec_enc_attns是decoder中每个单词对encoder中每个单词的相关性
dec_outputs, dec_self_attns, dec_enc_attns = self.decoder(dec_inputs, enc_inputs, enc_outputs)#dec_outputs做映射到词表大小
dec_logits = self.projection(dec_outputs)#dec_logits : [batch_size × src_vocab_size × tgt_vocab_size]return dec_logits.view(-1, dec_logits.size(-1)),enc_self_attns, dec_self_attns, dec_enc_attns
代码2:Encoder部分
词向量embedding
位置编码部分
注意力层及后续的前馈神经网络
classEncoder(nn.Module):def__init__(self):super(Encoder, self).__init__()
self.src_emb = nn.Embedding(src_vocab_size, d_model)# 定义生成一个矩阵,大小是src_vocab_size × d_model
self.pos_emb = PositionalEncoding(d_model)# 位置编码情况,这里是固定的正余弦函数,也可以使用类似的nn.Embedding获得一个可以更新学习的位置编码
self.layers = nn.ModuleList([EncoderLayer()for _ inrange(n_layers)])# 使用ModuleList对多个encoder进行堆叠,因为后续的encoder并没有使用词向量和位置编码,所以抽离出来#实现函数defforward(self, enc_inputs):# 这里的enc_inputs形状是:[batch_size × source_len](source_len是编码端输入句子的长度)# 下面这个代码是src_emb进行索引定位,enc_outputs输出形状是[batch_size,src_len,d_model]
enc_outputs = self.src_emb(enc_inputs)# 这里就是位置编码,把两者相加放入到了这个函数里面,从这里可以去看一下位置编码函数的实现:3
enc_outputs = self.pos_emb(enc_outputs.transpose(0,1)).transpose(0,1)# get_attn_pad_mask是为了得到句子中pad的位置信息,给到模型后面,在计算自注意力和交互注意力的时候去掉pad符号的影响,这个函数的实现:4
enc_self_attn_mask = get_attn_pad_mask(enc_inputs, enc_inputs)
enc_self_attns =[]for layer in self.layers:# 看EncoderLayer函数:5
enc_outputs, enc_self_attn = layer(enc_outputs, enc_self_attn_mask)
enc_self_attns.append(enc_self_attn)return enc_outputs, enc_self_attns
代码3:位置编码
classPositionEncoding(nn.Module):def__init__(self, d_model, dropout=0.1, max_len=5000):super(PositionEncoding, self).__init__()#位置编码的实现很简单,直接对照公式敲代码即可,下面的代码只是其中实现的一种方式;#需要注意的是偶数和奇数在公式上有一个共同的部分,我们使用log函数把次方拿下来,方便计算;#假设dmodel是512,那么公式里的0,1,...,511代表每一个位置,2i符号中i从0取到了255,那么2i对应取值是0,2,4,...,510
self.dropout = nn.Dropout(p=dropout)
pe = torch.zeros(max_len,d_model)
position = torch.arange(0, max_len, dtype=torch.float).unsqueeze(1)
div_term = torch.exp(torch.arange(0, d_model,2).float()*(-math.log(10000.0)/ d_model))
pe[:,0::2]= torch.sin(position * div_term)# pe[:,0::2],从0开始到最后,步长为2,代表偶数位置
pe[:,1::2]= torch.cos(position * div_term)# 代表奇数位置# 上面代码获取之后得到的pe:[max_len * d_model]#加一个维度的变换,pe形状变成[max_len * 1 * d_model]
pe = pe.unsqueeze(0).transpose(0,1)
self.register_buffer('pe', pe)# 定义一个缓冲区,简单理解为这个参数不更新defforward(self, x)"""
x: [seq_len, batch_size, d_model]
"""
x = x + self.pe[:x.size(0),:]return self.dropout(x)
代码4:get_attn_pad_mask
# 比如说,现在句子长度是5,在后面注意力机制的部分,在计算出来QK转置除以根号之后,softmax之前,我们得到的形状# len_ipuut × len_input 代表每个单词对其余包含自己的单词的影响力# 所以这里需要有一个同等大小形状的矩阵,告诉哪些位置是PAD,之后在计算softmax之前会把这里设置为无穷大;# 注意这里得到的矩阵形状[batch_size × len_q × len_k], 对k中的符号进行标识,并没有对q中的做标识,因为没必要# seq_q和seq_k不一定一致,在交互注意力,q来自解码端,k来自编码端,所以告诉模型编码这边pad符号信息就可以,解码端的pad信息在交互注意力层是没有用到的;defget_attn_pad_mask(seq_q, seq_k):
batch_size, len_q = seq_q.size()
batch_size, len_k = seq_k.size()# eq(zero) is PAD token
pad_attn_mask = sen_k.data.eq(0).unsqueeze(1)return pad_attn_mask.expand(batch_size, len_q, len_k)# [batch_size × len_q × len_k]
代码5:EncoderLayer:多头注意力机制和前馈神经网络
classEncoderLayer(nn.Module):def__init__(self):super(EncoderLayer, self).__init__()
self.enc_self_attn = MultiHeadAttention()
self.pos_ffn = PoswiseFeedForwardNet()defforward(self, enc_inputs, enc_self_attn_mask):# 这个是做自注意力层,输入是enc_inouts,形状是[batch_size × seq_len_q × d_model] 注意最初始得QKV矩阵是等同于这个输入得,看一下enc_self_attn函数:6
enc_outputs, attn = self.enc_self_attn(enc_inputs, enc_inputs, enc_inputs, enc_self_attn_mask)# enc_inputs to same Q,K,V
enc_outputs = self.pos_ffn(enc_outputs)return enc_outputs, attn
代码6:MultiHeadAttention
classMultiHeadAttention(nn.Module):def__init__(self):super(MultiHeadAttention, self).__init__()#输入进来的QKV是相等的,会使用映射Linear做一个映射得到参数矩阵Wq,Wk,Wv
self.W_Q = nn.Linear(d_model, d_k * n_heads)
self.W_K = nn.Linear(d_model, d_k * n_heads)
self.W_V = nn.Linear(d_model, d_v * n_heads)
总结:
写模型时应遵循的两个规则:
- 从整体到局部:先搭大的框架,再去搭细节的部分;
- 搞清楚数据的流动形状:经过这个小部分模型的输入是什么形状,输出是什么形状(下一部分的输入)
版权归原作者 陌上骑驴Yiping_Chen 所有, 如有侵权,请联系我们删除。