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

  • 本文是对于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()
Transformer是一种用于机器翻译的神经网络模型,它在处理序列到序列的任务中表现出色。下面是一个使用Transformer进行中英翻译的示例: ```python import torch from torchtext.data.metrics import bleu_score from torchtext.datasets import Multi30k from torchtext.data import Field, BucketIterator # 定义源语言和目标语言的Field SRC = Field(tokenize='spacy', tokenizer_language='en', init_token='<sos>', eos_token='<eos>', lower=True) TRG = Field(tokenize='spacy', tokenizer_language='de', init_token='<sos>', eos_token='<eos>', lower=True) # 加载数据集 train_data, valid_data, test_data = Multi30k.splits(exts=('.en', '.de'), fields=(SRC, TRG)) # 构建词汇表 SRC.build_vocab(train_data, min_freq=2) TRG.build_vocab(train_data, min_freq=2) # 定义模型 class Transformer(nn.Module): def __init__(self, input_dim, output_dim, hid_dim, n_layers, n_heads, pf_dim, dropout, max_length=100): super().__init__() self.encoder = Encoder(input_dim, hid_dim, n_layers, n_heads, pf_dim, dropout, max_length) self.decoder = Decoder(output_dim, hid_dim, n_layers, n_heads, pf_dim, dropout, max_length) self.fc_out = nn.Linear(hid_dim, output_dim) self.dropout = nn.Dropout(dropout) def forward(self, src, trg, src_mask, trg_mask, src_padding_mask, trg_padding_mask): enc_src = self.encoder(src, src_mask, src_padding_mask) output, attention = self.decoder(trg, enc_src, trg_mask, src_mask, trg_padding_mask, src_padding_mask) output = self.fc_out(output) return output, attention # 定义训练和评估函数 def train(model, iterator, optimizer, criterion, clip): model.train() epoch_loss = 0 for i, batch in enumerate(iterator): src = batch.src trg = batch.trg optimizer.zero_grad() output, _ = model(src, trg[:,:-1], src_mask, trg_mask, src_padding_mask, trg_padding_mask) output_dim = output.shape[-1] output = output.contiguous().view(-1, output_dim) trg = trg[:,1:].contiguous().view(-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) def evaluate(model, iterator, criterion): model.eval() epoch_loss = 0 with torch.no_grad(): for i, batch in enumerate(iterator): src = batch.src trg = batch.trg output, _ = model(src, trg[:,:-1], src_mask, trg_mask, src_padding_mask, trg_padding_mask) output_dim = output.shape[-1] output = output.contiguous().view(-1, output_dim) trg = trg[:,1:].contiguous().view(-1) loss = criterion(output, trg) epoch_loss += loss.item() return epoch_loss / len(iterator) # 定义超参数 INPUT_DIM = len(SRC.vocab) OUTPUT_DIM = len(TRG.vocab) HID_DIM = 256 N_LAYERS = 6 N_HEADS = 8 PF_DIM = 512 DROPOUT = 0.1 CLIP = 1 # 初始化模型和优化器 model = Transformer(INPUT_DIM, OUTPUT_DIM, HID_DIM, N_LAYERS, N_HEADS, PF_DIM, DROPOUT) optimizer = torch.optim.Adam(model.parameters(), lr=0.0005) criterion = nn.CrossEntropyLoss(ignore_index=TRG.vocab.stoi[TRG.pad_token]) # 将数据加载到迭代器中 train_iterator, valid_iterator, test_iterator = BucketIterator.splits((train_data, valid_data, test_data), batch_size=128, device=device) # 训练模型 N_EPOCHS = 10 best_valid_loss = float('inf') for epoch in range(N_EPOCHS): train_loss = train(model, train_iterator, optimizer, criterion, CLIP) valid_loss = evaluate(model, valid_iterator, criterion) if valid_loss < best_valid_loss: best_valid_loss = valid_loss torch.save(model.state_dict(), 'tut6-model.pt') print(f'Epoch: {epoch+1:02} | Train Loss: {train_loss:.3f} | Val. Loss: {valid_loss:.3f}') # 加载最佳模型并在测试集上进行评估 model.load_state_dict(torch.load('tut6-model.pt')) test_loss = evaluate(model, test_iterator, criterion) print(f'Test Loss: {test_loss:.3f}') # 使用训练好的模型进行翻译 def translate_sentence(sentence, src_field, trg_field, model, device, max_len=50): model.eval() if isinstance(sentence, str): nlp = spacy.load('en') 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_indexes = [src_field.vocab.stoi[token] for token in tokens] src_tensor = torch.LongTensor(src_indexes).unsqueeze(0).to(device) src_mask = model.make_src_mask(src_tensor) 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_mask = model.make_trg_mask(trg_tensor) with torch.no_grad(): output, attention = model.decoder(trg_tensor, enc_src, trg_mask, src_mask) pred_token = output.argmax(2)[:,-1].item() 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] return trg_tokens[1:], attention # 示例翻译 example_sentence = "I love transformers" translated_sentence, attention = translate_sentence(example_sentence, SRC, TRG, model, device) print(f'原句: {example_sentence}') print(f'翻译: {" ".join(translated_sentence[:-1])}') # 计算BLEU得分 def calculate_bleu(data, src_field, trg_field, model, device, max_len=50): trgs = [] pred_trgs = [] for datum in data: src = vars(datum)['src'] trg = vars(datum)['trg'] pred_trg, _ = translate_sentence(src, src_field, trg_field, model, device, max_len) pred_trg = pred_trg[:-1] pred_trgs.append(pred_trg) trgs.append([trg]) return bleu_score(pred_trgs, trgs) bleu_score = calculate_bleu(test_data, SRC, TRG, model, device) print(f'BLEU score = {bleu_score*100:.2f}') ```
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值