当前位置:网站首页>【Basic model】Transformer-实现中英翻译

【Basic model】Transformer-实现中英翻译

2022-08-09 11:15:00 临淮郡人

  • 本文是对于Pytorch项目成员:Ben Trevett的教程https://github.com/bentrevett/pytorch-seq2seq中,第6个项目的个人学习整理。原文是实现的“德语-英语”互译。在本博文内,期望在此基础上实现:
  1. 个人对于调参的理解(尚待调整)
  2. 中英互译;
  3. 低资源下的中英互译;(尚待完成)
  • 其它关于Transformer的架构,还可以参考链接:
    http://nlp.seas.harvard.edu/2018/04/03/attention.html、以及李宏毅的ML课程讲解。

  • 代码仓库:https://github.com/zuochao912/NMT_transformer_zh2en

其中,我这里的数据集使用的是IWSLT15Zh-En。这里由于实验设备限制,用若干年前的Titan-XP,在Batch为128的时候,即便EncoderLayer和DecoderLayer都只有3层,单卡在训练集上跑一个Epoch要4min左右。而可怜的是我多GPU训练的loss下降比较慢,单卡的学习率技巧又弄不好,这里就简单的设为0.0005。warmup,cool down或者指数衰减、余弦退火之类的技巧也一下子用不好,batch也设不太好,也希望各位大佬能支个招。

一、数据准备

这里使用Spacy构建两边语料的字典;embedding也是train from scratch的,没有使用预训练的词向量。其中具体内容,请详见另外的Spacy使用指南。

二、模型结构

本文并不介绍Self-attention和Layer_norm,请见CV与NLP中的注意力模块与激活函数模块

2.1 Encoder部分

Encoder由若干个Encoder_layer堆叠组成;

2.1.1 Encoder_layer

Encoder_layer
Multihead Attention
Layer_norm+Residual
FFN(Feed forward network)
Layer_norm+Residual

Transformer并不尝试将一句话中的各word_embedding压缩成一个sentence_embedding,因此对于一句话,若表示为 X = ( x 1 , . . . , x n ) X = (x_1, ... ,x_n) X=(x1,...,xn),其有n个词,就有n个Context_vector,如表示为 Z = ( z 1 , . . . , z n ) Z = (z_1, ... , z_n) Z=(z1,...,zn),这些向量均参考了句子中所有词语。事实上,用Self-attention机制产生的这些context vector的表达能力比RNN强,因为RNN在时间 t t t产生的向量 x t x_t xt,只能对 1 : t 1 − 1 1:t_1-1 1:t11时刻建模,但是注意力机制可以看到句子中的所有位置。
因此,在Transformer中,句子向量在通过Encoder Layer时,数目并不会减少。

  • 特别提醒,句子长度不同,因此输入时会有pad标签;在计算注意力的时候,我们不需要对这些地方算注意力!
  • 具体实现如下,mask的作用其实就是,把算出来 a t t e n t i o n attention attention分数,对应变为0;那么根据 Q K QK QK算出来的 e n e r g y energy energy在进行 s o f t m a x softmax softmax前,将 e n e r g y energy energy选用一个特别小的数字就行,如此处为 1 e − 10 1e-10 1e10,如下所示
	energy = torch.matmul(Q, K.permute(0, 1, 3, 2)) / self.scale
	#energy = [batch size, n heads, query len, key len]
	if mask is not None:
		energy = energy.masked_fill(mask == 0, -1e10)
	attention = torch.softmax(energy, dim = -1)     
	#attention = [batch size, n heads, query len, key len]
	x = torch.matmul(self.dropout(attention), V)

2.1.2 Position_Encoding

此外,由于注意力机制的注意力分数,是位置无关,内容相关地;而在句子中,相对位置是十分重要的,因此需要对word加入position_encoding
有的论文中,Position_Encoding是固定的,比如Transformer这里采用的是位置的三角函数;而其实也可以学习得到。

  • 个人觉得只需要体现位置信息就行,是否需要学习并不重要;这就像Condition GAN一样。

2.1.3 FFN

其实就是具有一层hidden_layer的全连接神经网络,先将输入维度,从 hid_dim t映射为 pf_dim, 再映射回hid_dim,在这里pf_dim 通常比hid_dim大,似乎也是常规操作了.
原本的l Transformer 使用 hid_dim 为512 ,使用 pf_dim 为2048.,使用激活函数为RELU,并且也使用了drop_out。
但是在BERT中,使用的是 GELU 激活函数, pytorch实现的时候,只需要把torch.relu替换为 F.gelu. 当然论文中没有解释,为什么这样

  • Encoder与Decoder的FFN都是这样设置的,因此以下不再重复介绍

2.2Decoder部分

本处目标,在时刻 t t t时,根据encoder提供的Context vector们, Z Z Z,以及时刻 t t t之前生成的句子,得到 Y t ^ \hat{Y_t} Yt^. 在评估时,用生成的 Y ^ \hat{Y} Y^ 与实际答案 Y Y Y计算损失函数,优化。
Decoder也是由若干个Decoder_layer堆叠组成;

2.2.1 Decoder_layer:

Decoder_layer的层次和Encoder_layer其实基本是一样的;唯一不同的就是注意力部分,使用了两个注意力模块:

  1. 因为Decoder需要知到Encoder的信息才能正确输出,因此采用了cross attention,即其中的K和V来源于Encoder,而Q来源于Decoder,这就像我们知道了之前生成结果后,我们需要查看源语言信息,来得到我们的结果一样。不过V是不是也可以来源于Decoder呢…
  2. 因为在翻译的时候,我们不能通过查看答案,来翻译,因此我们需要再翻译的时候,把之后的值给mask掉。因此采用了Masked attention。不过在具体实现中,和Encoder的Multihead attention并没显著差异,因为那里需要把<pad>给mask掉.

当然最后也是FFN,其中也都是使用了Layer_norm和Residual_connection

2.3 Seq2Seq包装

我们将Encoder和Decoder包装为一个Seq2Seq类,就是本模型的完整结构了,可以用于训练(train)和推理(inference)

source mask目标是对句子的 <pad> token进行掩码,因此是 <pad> 的地方设为0,其它地方设为1。注意,一个batch的一个句子,虽然用一个一维向量就可以达到mask目的,但是之后句子经过embedding后变为四维张量,因此还需要升维,以便广播。 energy张量的shape是 [batch size, n heads, seq len, seq len],因此sourch mask的形状是**[batch size, 1, 1, seq len]**

然后target mask 其实就是一个下三角阵,和上面对 <pad> token的mask阵的逻辑和,首先生成下三角阵,

1 0 0 0 0 1 1 0 0 0 1 1 1 0 0 1 1 1 1 0 1 1 1 1 1 \begin{matrix} 1 & 0 & 0 & 0 & 0\\ 1 & 1 & 0 & 0 & 0\\ 1 & 1 & 1 & 0 & 0\\ 1 & 1 & 1 & 1 & 0\\ 1 & 1 & 1 & 1 & 1\\ \end{matrix} 1111101111001110001100001

意思是target token可以看到的src token(其实也是自己所在的这句话),第一行,表示第一个 target token 的mask是 [1, 0, 0, 0, 0] ,只能看自己。 第二个target token的mask为 [1, 1, 0, 0, 0] ,即他能看包括自己的前两个。

可以想到,那么最终的结果,如下示意,其中句子长度为3.

1 0 0 0 0 1 1 0 0 0 1 1 1 0 0 1 1 1 0 0 1 1 1 0 0 \begin{matrix} 1 & 0 & 0 & 0 & 0\\ 1 & 1 & 0 & 0 & 0\\ 1 & 1 & 1 & 0 & 0\\ 1 & 1 & 1 & 0 & 0\\ 1 & 1 & 1 & 0 & 0\\ \end{matrix} 1111101111001110000000000

三、其它implement细节:

3.1 参数初始化:

本文并没有说到参数初始化细节,但是Transformer模型一般采用 Xavier uniform 初始化方法,如下使用

def initialize_weights(m):
    if hasattr(m, 'weight') and m.weight.dim() > 1:
        nn.init.xavier_uniform_(m.weight.data)

 model.apply(initialize_weights)
 #model就是Seq2Seq模型

3.2 优化器设置:

这里我的炼丹经验明显就很差了,参考原文:

  • Transformer原文中学习率使用了warm-upcool-down的训练技巧,采用Adam优化器
  • BERT和其它一些Transformer事实上就是用固定的学习率和Adam优化器.
  • 注意,使用一个比Adam优化器的默认学习率更小的参数,否则很容易学不好!(尚待实验)
LEARNING_RATE = 0.0005
#三层Encoder_layer的效果不凑,但是6层效果不太好,之后似乎会发生奇怪的loss爆炸现象,而并没有过拟合。
optimizer = torch.optim.Adam(model.parameters(), lr = LEARNING_RATE)

3.3.优化目标

由于BLEU score这种评价方法是不做为代价函数的,一般的,还是采用交叉熵;
但是这里也得特别注意,我们不能看<pad>标签来计算结果,因此要如下设置:

criterion = nn.CrossEntropyLoss(ignore_index = TRG_PAD_IDX)

3.4 注意输入输出的标签

我们在输入模型的时候,并不输入EOS标签,这是要求模型输出的
t r g = [ s o s , y 1 , y 2 , y 3 , e o s ] t r g [ : − 1 ] = [ s o s , x 1 , x 2 , x 3 ] trg = [sos,y_1, y_2, y_3, eos]\\ trg[:-1] = [sos,x_1, x_2, x_3] trg=[sos,y1,y2,y3,eos]trg[:1]=[sos,x1,x2,x3]

而在我们用交叉熵损失函数的时候,我们得到的output是没有sos标签的,我们需要注意一下:
o u t p u t = [ y 1 , y 2 , y 3 , e o s ] t r g [ 1 : ] = [ x 1 , x 2 , x 3 , e o s ] output = [y_1, y_2, y_3, eos]\\ trg[1:] = [x_1, x_2, x_3, eos] output=[y1,y2,y3,eos]trg[1:]=[x1,x2,x3,eos]

模型训练时的结构应该是如下的:

def train(model, iterator, optimizer, criterion, clip):
  
    model.train()
    epoch_loss = 0
    
    for i, batch in enumerate(iterator):     
        src = batch.src
        trg = batch.trg
        #trg = [batch size, trg len]
        optimizer.zero_grad()
        
        output, _ = model(src, trg[:,:-1]) #output = [batch size, trg len - 1, output dim]
        output_dim = output.shape[-1]
            
        output = output.contiguous().view(-1, output_dim)#output = [batch size * trg len - 1, output dim]
        trg = trg[:,1:].contiguous().view(-1)  #trg = [batch size * trg len - 1]

        loss = criterion(output, trg)
        loss.backward()
        torch.nn.utils.clip_grad_norm_(model.parameters(), clip)
        optimizer.step()
        epoch_loss += loss.item()
        
    return epoch_loss / len(iterator)

注意,在训练时使用了梯度剪裁语句,可以试验一下不同的CLIP值,以及不使用会造成什么后果?

nn.utils.clip_grad_norm

3.5 pipeline:

在训练集上训练以及dev集上测试的流程如下。其中,evaluate相比于train,只是不需要计算梯度,也不需要反向传播;这里注意

  • 最好记个时间,这样修改超参数的时候可以感受到对训练时间的改变;
  • 保存模型的时候,在dev集上loss最小的时候保存;其实最好把命名仔细一点,这样可以区分不同时间保存下来的模型;
  • 一定要关注一下train和dev集合上的loss变化,经常可以关注到train集合上损失变小,但dev集合上损失不断增大;这时候往往过拟合了!
best_valid_loss = float('inf')
#这一行是在干什么?

for epoch in range(N_EPOCHS):
    
    start_time = time.time()
    
    train_loss = train(model, train_iterator, optimizer, criterion, CLIP)
    valid_loss = evaluate(model, valid_iterator, criterion)
    
    end_time = time.time()
    
    epoch_mins, epoch_secs = epoch_time(start_time, end_time)
    
    if valid_loss < best_valid_loss:
        best_valid_loss = valid_loss
        torch.save(model.state_dict(), 'transformer_model.pt')

3.6 遗留问题:

1.在我们数据准备中,把出现次数很少的词语转化为了一个奇怪的token,那么我们是不是就永远不可能产生这个词语?
2. 在本指导中所有步骤中,矩阵的行列、维度,以及mask的方向、维度有没有分辨明白啊?好像是没有。。

四、模型推理(Inference)

模型推理似乎可以一句一句输入,也可以将一个矩阵整体输入。

4.1 整体步骤

如下所示:

  • tokenize源语言的句子,把句子从字符串转化为token标记,并且加上 <sos><eos> 的首尾标记;
  • 将token数值化,即embedding化;和训练时一样,先转化为index,再在embedding矩阵中对应位置查找就行
  • 对于单个句子,将矩阵格式变为张量,同时要把batch维度设为1;
  • 生成src_mask矩阵,然后将句子和mask矩阵输入encoder
  • 对于trg部分,我们初始化只有一个<sos> token,同时我们需要进行Auto_Regressive的自回归输入,具体如下,这里我们注意,输出是通过“选取概率最大”的采样而得,因此我们得到的首先都是index,而不是token
  • 当翻译输出没有达到最大限制时
    • 将当前输出转化为batch为1的矩阵
    • 每次都要生成新的trg_mask;
    • 对于decoder,需要输入encoder的context vector,以及“上一时刻”的输入,还有trg_mask矩阵
    • 我们可以得到“本时刻”的注意力分数,以及token(其实是index)
    • 进行自回归,把本时刻的输出,加到decoder输入的最后
    • 当我们看到 <eos> token(其实是index)的时候,停止生成
  • 将句子的index转化为词语,通过查表就行。
    -真正返回句子,这是我们人能看得懂的句子 ( 移除<sos> token) 然后我们可以查看decoder最后一层 attention

代码如下所示意:

def translate_sentence(sentence, src_field, trg_field, model, device, max_len = 50):
    #这里sentence的输入,是一个句子,不是一个Batch
    model.eval()
        
    if isinstance(sentence, str):
        nlp = spacy.load('de_core_news_sm')
        tokens = [token.text.lower() for token in nlp(sentence)]
    else:
        tokens = [token.lower() for token in sentence]

    tokens = [src_field.init_token] + tokens + [src_field.eos_token]
    #src句子前后补齐
    src_indexes = [src_field.vocab.stoi[token] for token in tokens]
    #将src句子转化为index
    src_tensor = torch.LongTensor(src_indexes).unsqueeze(0).to(device)
    #将src的batch维度补齐
    src_mask = model.make_src_mask(src_tensor)    
    #生成src_mask矩阵
    with torch.no_grad():
    #此时不需要计算梯度
        enc_src = model.encoder(src_tensor, src_mask)
    trg_indexes = [trg_field.vocab.stoi[trg_field.init_token]]
    
    for i in range(max_len):
	#自回归操作
        trg_tensor = torch.LongTensor(trg_indexes).unsqueeze(0).to(device)
		#将trg_index转化为embedding,并补齐batch
        trg_mask = model.make_trg_mask(trg_tensor)
        
        with torch.no_grad():
            output, attention = model.decoder(trg_tensor, enc_src, trg_mask, src_mask)
        #生成时刻t的logits概率分布
        pred_token = output.argmax(2)[:,-1].item()
        #找到最大词语index
        trg_indexes.append(pred_token)
		#补齐句子
        if pred_token == trg_field.vocab.stoi[trg_field.eos_token]:
            break
    
    trg_tokens = [trg_field.vocab.itos[i] for i in trg_indexes]
    #将trg_index通过查表,转化为词语
    #去掉sos首部标签,得到结果
    return trg_tokens[1:], attention

4.2 Attention可视化:

一般似乎都是用decoder最后一层的attention来可视化内容的。根据cross attention的含义,我们用decoder的q去查询encoder提供的k,v部分,因此,就是目标语言的一个词,对应于encoder的一句话的不同v的分数。

代码如下所示:

def display_attention(sentence, translation, attention, n_heads = 8, n_rows = 4, n_cols = 2):
    
    assert n_rows * n_cols == n_heads
    
    fig = plt.figure(figsize=(15,25))
    
    for i in range(n_heads):
        
        ax = fig.add_subplot(n_rows, n_cols, i+1)
        
        _attention = attention.squeeze(0)[i].cpu().detach().numpy()

        cax = ax.matshow(_attention, cmap='bone')

        ax.tick_params(labelsize=12)
        ax.set_xticklabels(['']+['<sos>']+[t.lower() for t in sentence]+['<eos>'], 
                           rotation=45)
        ax.set_yticklabels(['']+translation)

        ax.xaxis.set_major_locator(ticker.MultipleLocator(1))
        ax.yaxis.set_major_locator(ticker.MultipleLocator(1))

    plt.show()
    plt.close()
原网站

版权声明
本文为[临淮郡人]所创,转载请带上原文链接,感谢
https://blog.csdn.net/zeiyousao/article/details/121565787