Seg2Seg及其pytorch实现

Seq2Seq由来

在基于词语的语言模型中,我们使用了循环神经网络。它的输入是一段不定长的序列,输出却是定长的,例如输入:They are,输出可能是 watching 或者 sleeping。

  • many to many
    然而,很多问题的输出是不定长的序列。以机器翻译为例,输入是一段英文,输出是一段法语,输入和输出皆不定长,例如:

英语: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模型指的是首先对一个序列(如一个自然语言句子)编码,然后再对齐进行解码,即生成一个新的序列。很多自然语言处理的问题都可以看作为Seq2Seq模型,如机器翻译。

机器翻译流程:

  1. 首先编码器使用RNN对源语言句子编码
  2. 然后以最后一个单词对应的隐含层作为decoder的输入
  3. 再调用decoder(另一个RNN)逐词生成目标语言句子

Encoder-Decoder

编码器和解码器分别对应输入序列和输出序列的两个循环神经网络。我们通常会在输入序列和输出序列后面分别附上一个特殊字符 ‘’(end of sequence)表示序列的终止。在测试模型时,一旦输出 ‘’ 就终止当前的输出序列

基于RNN的Seq2Seq的基本假设:原始序列的最后一个隐含状态(一个向量)包含了该序列的全部信息。

Encoder

Encoder的作用是把一个不定长的输入序列转化成一个定长的背景向量 C C C该背景向量包含了输入序列的信息。常用的编码器是循环神经网络。

  • 循环神经网络的隐藏层变量 h t = f ( x t , h t − 1 ) h_t = f(x_t,h_{t-1}) ht=f(xt,ht1)
  • 编码器的背景向量 C = q ( h 1 , . . . , h T ) C = q(h_1,...,h_T) C=q(h1,...,hT)
    一个简单的背景向量可以认为是该网络最终时刻的隐藏层变量 h T h_T hT/。我们将这里的循环神经网络叫做编码器。

Decoder

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=1TP(yty1,...,yt1,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(yty1,...,yt1,c)=p(yt1,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(yt1,c,st1)

其中函数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 实现
自然语言处理:基于预训练模型的方法

你可能感兴趣的:(pytorch,机器翻译,自然语言处理)