在基于词语的语言模型中,我们使用了循环神经网络。它的输入是一段不定长的序列,输出却是定长的,例如输入:They are,输出可能是 watching 或者 sleeping。
英语:The are watching
法语:lls regardent
当输入输出序列都是不定长时,我们可以使用编码器 - 解码器(encoder-decoder)或者 seq2seq。它们分别是基于 2014 年的两个工作:
- Cho et al., Learning Phrase Representations using RNN Encoder-Decoder for Statistical Machine Translation
- Sutskever et al., Sequence to Sequence Leaerning with Neural Networks
以上两个工作本质上都用到了两个循环神经网络结构,分别叫做编码器和解码器。编码器对应输入序列,解码器对应输出序列
Seq2Seq模型指的是首先对一个序列(如一个自然语言句子)编码,然后再对齐进行解码,即生成一个新的序列。很多自然语言处理的问题都可以看作为Seq2Seq模型,如机器翻译。
机器翻译流程:
- 首先编码器使用RNN对源语言句子编码
- 然后以最后一个单词对应的隐含层作为decoder的输入
- 再调用decoder(另一个RNN)逐词生成目标语言句子
编码器和解码器分别对应输入序列和输出序列的两个循环神经网络。我们通常会在输入序列和输出序列后面分别附上一个特殊字符 ‘
’(end of sequence)表示序列的终止。在测试模型时,一旦输出 ‘’ 就终止当前的输出序列
基于RNN的Seq2Seq的基本假设:原始序列的最后一个隐含状态(一个向量)包含了该序列的全部信息。
Encoder的作用是把一个不定长的输入序列转化成一个定长的背景向量 C C C。该背景向量包含了输入序列的信息。常用的编码器是循环神经网络。
Encoder最终输出了一个背景向量 C C C,该背景向量整合了输入序列 x 1 , x 2 , . . . , x T x_1,x_2,...,x_T x1,x2,...,xT
假设训练数据中的输出序列是 y 1 , y 2 , . . . , y T ′ y_1,y_2,...,y_{T\prime} y1,y2,...,yT′,我们希望表示每个 t ′ t\prime t′时刻输出的向量,既取决于之前的输出,又取决于背景向量。
输出序列的联合概率 P ( y 1 , y 2 , . . . , y T ′ ) = ∏ t ′ = 1 T ′ P ( y t ′ ∣ y 1 , . . . , y t ′ − 1 , c ) P(y_1,y_2,...,y_{T \prime}) = \prod \limits_{t \prime=1}^{T\prime} P(y_{t\prime}|y_1,...,y_{t\prime-1},c) P(y1,y2,...,yT′)=t′=1∏T′P(yt′∣y1,...,yt′−1,c)
该输出序列的损失函数 − l o g ( y 1 , . . . , y T ′ ) -log(y_1,...,y_{T\prime}) −log(y1,...,yT′)
为此,我们使用另一个RNN来作为解码器。解码器使用函数p来表示单个输出 y t ′ y_{t\prime} yt′的概率 P ( y t ′ ∣ y 1 , . . . , y t ′ − 1 , c ) = p ( y t ′ − 1 , s t ′ , c ) P(y_{t\prime}|y_1,...,y_{t\prime-1},c) = p(y_{t\prime-1},s_{t\prime},c) P(yt′∣y1,...,yt′−1,c)=p(yt′−1,st′,c)
其中 s t ′ s_{t\prime} st′为 t ′ t\prime t′时刻的解码器的隐藏层变量
s t ′ = g ( y t ′ − 1 , c , s t ′ − 1 ) s_{t\prime} = g(y_{t\prime-1},c,s_{t\prime-1}) st′=g(yt′−1,c,st′−1)
其中函数g是循环神经网络单元。
需要注意的是:编码器和解码器通常会使用多层循环神经网络
任务:翻译(英文 to 英文)(man to woman)
目的只是为了实现模型
import torch
import numpy as np
import torch.nn as nn
import torch.utils.data as Data
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
对单词长度不够的 用?填充
将Encoder的输入数据 末尾 添加终止符号 E
将Decoder的输入数据 开头 添加开始符号 S
将Decoder的 输出 数据 末尾 添加终止符号 E
letter = [c for c in 'SE?abcdefghijklmnopqrstuvwxyz']
letter2idx = {n: i for i, n in enumerate(letter)}
seq_data = [['man', 'women'], ['black', 'white'], ['king', 'queen'], ['girl', 'boy'], ['up', 'down'], ['high', 'low']]
# Seq2Seq Parameter
n_step = max([max(len(i), len(j)) for i, j in seq_data]) # max_len(=5)
n_hidden = 128
n_class = len(letter2idx) # classfication problem
batch_size = 3
def make_data(seq_data):
enc_input_all, dec_input_all, dec_output_all = [], [], []
for seq in seq_data:
for i in range(2):
seq[i] = seq[i] + '?' * (n_step - len(seq[i])) # 'man??', 'women'
# 将所有单词补全到五个字母,用?占位
enc_input = [letter2idx[n] for n in (seq[0] + 'E')] # ['m', 'a', 'n', '?', '?', 'E']
dec_input = [letter2idx[n] for n in ('S' + seq[1])] # ['S', 'w', 'o', 'm', 'e', 'n']
dec_output = [letter2idx[n] for n in (seq[1] + 'E')] # ['w', 'o', 'm', 'e', 'n', 'E']
enc_input_all.append(np.eye(n_class)[enc_input])
dec_input_all.append(np.eye(n_class)[dec_input])
dec_output_all.append(dec_output) # not one-hot
# make tensor
return torch.Tensor(enc_input_all), torch.Tensor(dec_input_all), torch.LongTensor(dec_output_all)
'''
enc_input_all: [6, n_step+1 (because of 'E'), n_class]
dec_input_all: [6, n_step+1 (because of 'S'), n_class]
dec_output_all: [6, n_step+1 (because of 'E')]
'''
enc_input_all, dec_input_all, dec_output_all = make_data(seq_data)
# 两个输入一个输出
class TranslateDataSet(Data.Dataset):
def __init__(self, enc_input_all, dec_input_all, dec_output_all):
self.enc_input_all = enc_input_all
self.dec_input_all = dec_input_all
self.dec_output_all = dec_output_all
def __len__(self): # return dataset size
return len(self.enc_input_all)
def __getitem__(self, idx):
return self.enc_input_all[idx], self.dec_input_all[idx], self.dec_output_all[idx]
loader = Data.DataLoader(TranslateDataSet(enc_input_all, dec_input_all, dec_output_all), batch_size, shuffle = True)
h t h_t ht和 o u t out out(上下为rnn层数,左右为时间戳)
- h t : h_t: ht:最后一个时间戳上面所有的memory状态
- o u t : out: out:所有时间戳上的最后一个memory状态
# Model
class Seq2Seq(nn.Module):
def __init__(self):
super(Seq2Seq, self).__init__()
self.encoder = nn.RNN(input_size=n_class, hidden_size=n_hidden, dropout=0.5) # encoder
self.decoder = nn.RNN(input_size=n_class, hidden_size=n_hidden, dropout=0.5) # decoder
self.fc = nn.Linear(n_hidden, n_class)
def forward(self, enc_input, enc_hidden, dec_input):
# enc_input(=input_batch): [batch_size, n_step+1, n_class]
# dec_inpu(=output_batch): [batch_size, n_step+1, n_class]
enc_input = enc_input.transpose(0, 1) # enc_input: [n_step+1, batch_size, n_class]
dec_input = dec_input.transpose(0, 1) # dec_input: [n_step+1, batch_size, n_class]
# h_t : [num_layers(=1) * num_directions(=1), batch_size, n_hidden]
# encoder只要最后一个隐藏层状态
_, h_t = self.encoder(enc_input, enc_hidden)
# outputs : [n_step+1, batch_size, num_directions(=1) * n_hidden(=128)]
# decoder只要最后的outputs
outputs, _ = self.decoder(dec_input, h_t)
model = self.fc(outputs) # model : [n_step+1, batch_size, n_class]
return model
model = Seq2Seq().to(device)
criterion = nn.CrossEntropyLoss().to(device)
optimizer = torch.optim.Adam(model.parameters(), lr=0.001)
for epoch in range(5000):
for enc_input_batch, dec_input_batch, dec_output_batch in loader:
# make hidden shape [num_layers * num_directions, batch_size, n_hidden]
h_0 = torch.zeros(1, batch_size, n_hidden).to(device)
(enc_input_batch, dec_intput_batch, dec_output_batch) = (enc_input_batch.to(device), dec_input_batch.to(device), dec_output_batch.to(device))
# enc_input_batch : [batch_size, n_step+1, n_class]
# dec_intput_batch : [batch_size, n_step+1, n_class]
# dec_output_batch : [batch_size, n_step+1], not one-hot
pred = model(enc_input_batch, h_0, dec_intput_batch)
# pred : [n_step+1, batch_size, n_class]
pred = pred.transpose(0, 1) # [batch_size, n_step+1(=6), n_class]
loss = 0
for i in range(len(dec_output_batch)):
# pred[i] : [n_step+1, n_class]
# dec_output_batch[i] : [n_step+1]
loss += criterion(pred[i], dec_output_batch[i])
if (epoch + 1) % 1000 == 0:
print('Epoch:', '%04d' % (epoch + 1), 'cost =', '{:.6f}'.format(loss))
optimizer.zero_grad()
loss.backward()
optimizer.step()
# Test
def translate(word):
enc_input, dec_input, _ = make_data([[word, '?' * n_step]])
enc_input, dec_input = enc_input.to(device), dec_input.to(device)
# make hidden shape [num_layers * num_directions, batch_size, n_hidden]
hidden = torch.zeros(1, 1, n_hidden).to(device)
output = model(enc_input, hidden, dec_input)
# output : [n_step+1, batch_size, n_class]
predict = output.data.max(2, keepdim=True)[1] # select n_class dimension
decoded = [letter[i] for i in predict]
translated = ''.join(decoded[:decoded.index('E')])
return translated.replace('?', '')
print('test')
print('man ->', translate('man'))
print('mans ->', translate('mans'))
print('king ->', translate('king'))
print('black ->', translate('black'))
print('up ->', translate('up'))
Seq2Seq 的 PyTorch 实现
自然语言处理:基于预训练模型的方法