压缩填充序列,掩码,推断和BLEU

Packed Padded Sequences, Masking, Inference and BLEU

1 Introduction

这个项目将对之前项目的模型添加一些改进——填充序列和掩码。填充序列告诉模型跳过编码器中的填充token,掩码迫使模型忽略某些值。这两个技术常用于nlp。
此外还将研究如何使用模型进行推理,方法是给定一个句子看看它将其翻译为什么(终于),以及再翻译每个单词时注意到地方。
最后将使用BLEU来评价翻译质量。

关于pad_packed_sequqnece()和pack_padded_sequence()参考这篇博文

2 准备数据

和之前的没有太大区别,除了引入了matplotlib来画图。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
import torch
import torch.nn as nn
import torch.optim as optim
import torch.nn.functional as F

from torchtext.datasets import Multi30k
from torchtext.data import Field, BucketIterator

import matplotlib.pyplot as plt
import matplotlib.ticker as ticker

import spacy
import numpy as np

import random
import math
import time

SEED = 1234

random.seed(SEED)
np.random.seed(SEED)
torch.manual_seed(SEED)
torch.cuda.manual_seed(SEED)
torch.backends.cudnn.deterministic = True

spacy_de = spacy.load("en_core_web_sm")
spacy_en = spacy.load("de_core_news_sm")

def tokenize_de(text):
"""
Tokenizes German text from a string into a list of strings
"""
return [tok.text for tok in spacy_de.tokenizer(text)]

def tokenize_en(text):
"""
Tokenizes English text from a string into a list of strings
"""
return [tok.text for tok in spacy_en.tokenizer(text)]

当使用打包好的填充序列时,我们需要告诉pytorch非填充序列有多长,使用到了Torchtext中Filed对象的include_lengths参数。这使得batch.src成为元组,第一个元素和之前相同——序列化的句子张量,第二个元素时每个源句子的未填充长度。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
SRC = Field(tokenize=tokenize_de,
init_token='<sos>',
eos_token='<eos>',
lower=True,
include_lengths=True)

TRG = Field(tokenize=tokenize_en,
init_token='<sos>',
eos_token='<eos>',
lower=True)

train_data, valid_data, test_data = Multi30k.splits(exts=('.de', '.en'),fields=(SRC, TRG))

SRC.build_vocab(train_data, min_freq=2)
TRG.build_vocab(train_data, min_freq=2)

压缩填充序列有一个特点:批处理中的所有元素都按其未填充长度降序排序,即批处理的第一个句子最长。我们使用iterator的两个参数来处理这个问题:sort_within_batch告诉iterator需要对批处理内容进行排序,sort_key告诉iterator依照什么来进行排序,此处我们按src句子长度排序。

1
2
3
4
5
6
train_iterator, valid_iterator, test_iterator = BucketIterator.splits(
(train_data, valid_data, test_data),
batch_size=BATCH_SIZE,
sort_within_batch=True,
sort_key=lambda x: len(x.src),
device=device)

3 Model

3.1 Encoder

Encoder需要修改的时前向传播函数,它接受源句子的长度和句子本身。
嵌入源句子在自动填充后,我们可以在其上使用pack_padded_sequence和语句的长度。然后packed_embeded将其压缩未填充序列。再传送到RNN中,返回的时packed_outputs,压缩张量,包含序列中的所有隐藏状态。之前hidden是序列的最终隐藏状态,是个没有压缩的标准张量,唯一的区别是由于输入是压缩序列,因此该张量来自序列中最后一个未填充的元素。
然后使用pad_packed_sequence解压缩packed_outputs,返回不需要的输出和每个输出的长度。
输出的第一维是填充序列的长度,但是由于使用压缩的填充序列,因此当填充令牌为输入时张量的值将全部为零。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
class Encoder(nn.Module):
def __init__(self, input_dim, emb_dim, enc_hid_dim, dec_hid_dim, dropout):
super().__init__()

self.embedding = nn.Embedding(input_dim, emb_dim)

self.rnn = nn.GRU(emb_dim, enc_hid_dim, bidirectional = True)

self.fc = nn.Linear(enc_hid_dim * 2, dec_hid_dim)

self.dropout = nn.Dropout(dropout)

def forward(self, src, src_len):

#src = [src len, batch size]
#src_len = [batch size]

embedded = self.dropout(self.embedding(src))

#embedded = [src len, batch size, emb dim]

packed_embedded = nn.utils.rnn.pack_padded_sequence(embedded, src_len)

packed_outputs, hidden = self.rnn(packed_embedded)

#packed_outputs is a packed sequence containing all hidden states
#hidden is now from the final non-padded element in the batch

outputs, _ = nn.utils.rnn.pad_packed_sequence(packed_outputs)

#outputs is now a non-packed sequence, all hidden states obtained
# when the input is a pad token are all zeros

#outputs = [src len, batch size, hid dim * num directions]
#hidden = [n layers * num directions, batch size, hid dim]

#hidden is stacked [forward_1, backward_1, forward_2, backward_2, ...]
#outputs are always from the last layer

#hidden [-2, :, : ] is the last of the forwards RNN
#hidden [-1, :, : ] is the last of the backwards RNN

#initial decoder hidden is final hidden state of the forwards and backwards
# encoder RNNs fed through a linear layer
hidden = torch.tanh(self.fc(torch.cat((hidden[-2,:,:], hidden[-1,:,:]), dim = 1)))

#outputs = [src len, batch size, enc hid dim * 2]
#hidden = [batch size, dec hid dim]

return outputs, hidden

3.2 Attention

注意模块是我们在源句子上计算注意值的地方。

以前,我们允许该模块“注意”源句子中的填充标记。但是,使用遮罩,我们可以强制将注意力仅放在非填充元素上。

现在,forward方法将接受掩码输入。这是一个[batch size,src len]张量,当源句子标记不是填充标记时为1,而当它是填充标记时为0。例如,如果源句子为:[“ hello”,“ how”,“ are”,“ you”,“?”,],则掩码将为[1、1、1 ,1、1、0、0]。

我们在计算注意力之后但在通过softmax函数将其标准化之前应用遮罩。它是使用masked_fill来应用的。这将填充第一个参数(mask == 0)为true的每个元素的张量,并使用第二个参数(-1e10)给出的值。换句话说,它将采用未归一化的关注值,并将填充元素上的关注值更改为-1e10。由于这些数字与其他值相比微不足道,因此当它们通过softmax层时,它们将变为零,从而确保了对源句子中填充标记的关注。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
class Attention(nn.Module):
def __init__(self, enc_hid_dim, dec_hid_dim):
super().__init__()

self.attn = nn.Linear((enc_hid_dim * 2) + dec_hid_dim, dec_hid_dim)
self.v = nn.Linear(dec_hid_dim, 1, bias = False)

def forward(self, hidden, encoder_outputs, mask):

#hidden = [batch size, dec hid dim]
#encoder_outputs = [src len, batch size, enc hid dim * 2]

batch_size = encoder_outputs.shape[1]
src_len = encoder_outputs.shape[0]

#repeat decoder hidden state src_len times
hidden = hidden.unsqueeze(1).repeat(1, src_len, 1)

encoder_outputs = encoder_outputs.permute(1, 0, 2)

#hidden = [batch size, src len, dec hid dim]
#encoder_outputs = [batch size, src len, enc hid dim * 2]

energy = torch.tanh(self.attn(torch.cat((hidden, encoder_outputs), dim = 2)))

#energy = [batch size, src len, dec hid dim]

attention = self.v(energy).squeeze(2)

#attention = [batch size, src len]

attention = attention.masked_fill(mask == 0, -1e10)

return F.softmax(attention, dim = 1)

3.3 Decoder

Decoder修改了小部分。现在Decoder需要接受源句子的mask,并将其传递给注意力模块。此外,因为我们想在infer时查看注意力值,所以此处返回了注意力张量。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
class Decoder(nn.Module):
def __init__(self, output_dim, emb_dim, enc_hid_dim, dec_hid_dim, dropout, attention):
super().__init__()

self.output_dim = output_dim
self.attention = attention

self.embedding = nn.Embedding(output_dim, emb_dim)

self.rnn = nn.GRU((enc_hid_dim * 2) + emb_dim, dec_hid_dim)

self.fc_out = nn.Linear((enc_hid_dim * 2) + dec_hid_dim + emb_dim, output_dim)

self.dropout = nn.Dropout(dropout)

def forward(self, input, hidden, encoder_outputs, mask):

#input = [batch size]
#hidden = [batch size, dec hid dim]
#encoder_outputs = [src len, batch size, enc hid dim * 2]
#mask = [batch size, src len]

input = input.unsqueeze(0)

#input = [1, batch size]

embedded = self.dropout(self.embedding(input))

#embedded = [1, batch size, emb dim]

a = self.attention(hidden, encoder_outputs, mask)

#a = [batch size, src len]

a = a.unsqueeze(1)

#a = [batch size, 1, src len]

encoder_outputs = encoder_outputs.permute(1, 0, 2)

#encoder_outputs = [batch size, src len, enc hid dim * 2]

weighted = torch.bmm(a, encoder_outputs)

#weighted = [batch size, 1, enc hid dim * 2]

weighted = weighted.permute(1, 0, 2)

#weighted = [1, batch size, enc hid dim * 2]

rnn_input = torch.cat((embedded, weighted), dim = 2)

#rnn_input = [1, batch size, (enc hid dim * 2) + emb dim]

output, hidden = self.rnn(rnn_input, hidden.unsqueeze(0))

#output = [seq len, batch size, dec hid dim * n directions]
#hidden = [n layers * n directions, batch size, dec hid dim]

#seq len, n layers and n directions will always be 1 in this decoder, therefore:
#output = [1, batch size, dec hid dim]
#hidden = [1, batch size, dec hid dim]
#this also means that output == hidden
assert (output == hidden).all()

embedded = embedded.squeeze(0)
output = output.squeeze(0)
weighted = weighted.squeeze(0)

prediction = self.fc_out(torch.cat((output, weighted, embedded), dim = 1))

#prediction = [batch size, output dim]

return prediction, hidden.squeeze(0), a.squeeze(1)

3.4 Seq2seq

seq2seq模型需要对压缩填充序列,掩码,推断进行一些修改。

  • 我们需要告诉模型填充令牌的索引,并将源句子长度输入给forward方法
  • 我们使用填充令牌索引来创建掩码,方法是在源句子不等于填充令牌的地方创建一个掩码张量,为1。
  • 传递给编码器使用压缩填充序列所需的序列长度,
  • 每个时间步的注意力都储存在attentions中
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
class Seq2Seq(nn.Module):
def __init__(self, encoder, decoder, src_pad_idx, device):
super().__init__()

self.encoder = encoder
self.decoder = decoder
self.src_pad_idx = src_pad_idx
self.device = device

def create_mask(self, src):
mask = (src != self.src_pad_idx).permute(1, 0)
return mask

def forward(self, src, src_len, trg, teacher_forcing_ratio = 0.5):

#src = [src len, batch size]
#src_len = [batch size]
#trg = [trg len, batch size]
#teacher_forcing_ratio is probability to use teacher forcing
#e.g. if teacher_forcing_ratio is 0.75 we use teacher forcing 75% of the time

batch_size = src.shape[1]
trg_len = trg.shape[0]
trg_vocab_size = self.decoder.output_dim

#tensor to store decoder outputs
outputs = torch.zeros(trg_len, batch_size, trg_vocab_size).to(self.device)

#encoder_outputs is all hidden states of the input sequence, back and forwards
#hidden is the final forward and backward hidden states, passed through a linear layer
encoder_outputs, hidden = self.encoder(src, src_len)

#first input to the decoder is the <sos> tokens
input = trg[0,:]

mask = self.create_mask(src)

#mask = [batch size, src len]

for t in range(1, trg_len):

#insert input token embedding, previous hidden state, all encoder hidden states
# and mask
#receive output tensor (predictions) and new hidden state
output, hidden, _ = self.decoder(input, hidden, encoder_outputs, mask)

#place predictions in a tensor holding predictions for each token
outputs[t] = output

#decide if we are going to use teacher forcing or not
teacher_force = random.random() < teacher_forcing_ratio

#get the highest predicted token from our predictions
top1 = output.argmax(1)

#if teacher forcing, use actual next token as next input
#if not, use predicted token
input = trg[t] if teacher_force else top1

return outputs

4 训练

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
INPUT_DIM = len(SRC.vocab)
OUTPUT_DIM = len(TRG.vocab)
ENC_EMB_DIM = 256
DEC_EMB_DIM = 256
ENC_HID_DIM = 512
DEC_HID_DIM = 512
ENC_DROPOUT = 0.5
DEC_DROPOUT = 0.5
SRC_PAD_IDX = SRC.vocab.stoi[SRC.pad_token]

attn = Attention(ENC_HID_DIM, DEC_HID_DIM)
enc = Encoder(INPUT_DIM, ENC_EMB_DIM, ENC_HID_DIM, DEC_HID_DIM, ENC_DROPOUT)
dec = Decoder(OUTPUT_DIM, DEC_EMB_DIM, ENC_HID_DIM, DEC_HID_DIM, DEC_DROPOUT, attn)

model = Seq2Seq(enc, dec, SRC_PAD_IDX, device).to(device)



def init_weights(m):
for name, param in m.named_parameters():
if 'weight' in name:
nn.init.normal_(param.data, mean=0, std=0.01)
else:
nn.init.constant_(param.data, 0)

model.apply(init_weights)

def count_parameters(model):
return sum(p.numel() for p in model.parameters() if p.requires_grad)

print(f'The model has {count_parameters(model):,} trainable parameters')

optimizer = optim.Adam(model.parameters())

损失函数这里必须设置ignore_index 为目标语言的填充令牌索引。

1
2
3
TRG_PAD_IDX = TRG.vocab.stoi[TRG.pad_token]

criterion = nn.CrossEntropyLoss(ignore_index = TRG_PAD_IDX)

训练函数和评估函数将重新定义。因为我们在源字段中使用了inclede_lengths=True,所以现在的batch.src是一个元组,第一个元素是句子的序列化表示,第二个是批处理中每个句子的长度。
在每个解码时间步中,模型还会返回源句子的注意力向量,在推断中我们将使用到它。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
def train(model, iterator, optimizer, criterion, clip):

model.train()

epoch_loss = 0

for i, batch in enumerate(iterator):

src, src_len = batch.src
trg = batch.trg

optimizer.zero_grad()

output = model(src, src_len, trg)

#trg = [trg len, batch size]
#output = [trg len, batch size, output dim]

output_dim = output.shape[-1]

output = output[1:].view(-1, output_dim)
trg = trg[1:].view(-1)

#trg = [(trg len - 1) * batch size]
#output = [(trg len - 1) * batch size, output dim]

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)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
def evaluate(model, iterator, criterion):

model.eval()

epoch_loss = 0

with torch.no_grad():

for i, batch in enumerate(iterator):

src, src_len = batch.src
trg = batch.trg

output = model(src, src_len, trg, 0) #turn off teacher forcing

#trg = [trg len, batch size]
#output = [trg len, batch size, output dim]

output_dim = output.shape[-1]

output = output[1:].view(-1, output_dim)
trg = trg[1:].view(-1)

#trg = [(trg len - 1) * batch size]
#output = [(trg len - 1) * batch size, output dim]

loss = criterion(output, trg)

epoch_loss += loss.item()

return epoch_loss / len(iterator)

5 推断

终于可以推断了QAQ

translate_sentence有以下步骤:

  • model设置为eval模式
  • 分词源句子
  • 数字化句子
  • 将其转换为张量并添加batch-size
  • 获取源句子的长度
  • 将源句子输入编码器
  • 为源句子添加掩码
  • 创建一个列表来保存输出句子,并用初始化
  • 创建一个张量来保存注意力值
  • 当还没有达到最大长度时:
    • 获取输入张量,这个张量应该是或者这最后预测的标记。
    • 将输入、所有编码器输出、隐藏状态和掩码输入解码器。
    • 储存注意力值
    • 获得预测的下一个令牌
    • 将预测添加到当前输出句子的的预测中。
    • 如果预测是,中断
    • 将输出语句从索引转换为标记
    • 返回输出删除了的语句和整个序列的注意力值

还是直接看代码吧。。还是比较好理解的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
def translate_sentence(sentence, src_field, trg_field, model, device, max_len = 50):

model.eval()

if isinstance(sentence, str):
nlp = spacy.load('de')
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(1).to(device)

src_len = torch.LongTensor([len(src_indexes)]).to(device)

with torch.no_grad():
encoder_outputs, hidden = model.encoder(src_tensor, src_len)

mask = model.create_mask(src_tensor)

trg_indexes = [trg_field.vocab.stoi[trg_field.init_token]]

attentions = torch.zeros(max_len, 1, len(src_indexes)).to(device)

for i in range(max_len):

trg_tensor = torch.LongTensor([trg_indexes[-1]]).to(device)

with torch.no_grad():
output, hidden, attention = model.decoder(trg_tensor, hidden, encoder_outputs, mask)

attentions[i] = attention

pred_token = output.argmax(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:], attentions[:len(trg_tokens)-1]

注意力图展示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
def display_attention(sentence, translation, attention):

fig = plt.figure(figsize=(10,10))
ax = fig.add_subplot(111)

attention = attention.squeeze(1).cpu().detach().numpy()

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

ax.tick_params(labelsize=15)
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()
1

1
2
3
src=['ein', 'schwarzer', 'hund', 'und', 'ein', 'gefleckter', 'hund', 'kämpfen', '.']
src=['a', 'black', 'dog', 'and', 'a', 'spotted', 'dog', 'are', 'fighting']
predicted trg = ['a', 'black', 'dog', 'and', 'a', 'spotted', 'dog', 'fight', '.', '<eos>']

image-20200707000356276

计算bleu

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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)

# cut off <eos> token
pred_trg = pred_trg[:-1]

pred_trgs.append(pred_trg)
trgs.append([trg])

return bleu_score(pred_trgs, trgs)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
if __name__ == '__main__':
example_idx = 12
src = vars(train_data.examples[example_idx])['src']
trg = vars(train_data.examples[example_idx])['trg']
print(f'src={src}')
print(f'src={trg}')

translation, attention = translate_sentence(src, SRC, TRG, model, device)

print(f'predicted trg = {translation}')
display_attention(src, translation, attention)
bleu_score = calculate_bleu(test_data, SRC, TRG, model, device)

print(f'BLEU score = {bleu_score * 100:.2f}')