我们知道, S e q 2 S e q Seq2Seq Seq2Seq 现在已经成为了机器翻译、对话聊天、文本摘要等工作的重要模型,真正提出 S e q 2 S e q Seq2Seq Seq2Seq 的文章是 《 S e q u e n c e 《Sequence 《Sequence t o to to S e q u e n c e Sequence Sequence L e a r n i n g Learning Learning w i t h with with N e u r a l Neural Neural N e t w o r k s 》 Networks》 Networks》,但本篇 《 L e a r n i n g 《Learning 《Learning P h r a s e Phrase Phrase R e p r e s e n t a t i o n s Representations Representations u s i n g using using R N N RNN RNN E n c o d e r – D e c o d e r Encoder–Decoder Encoder–Decoder f o r for for S t a t i s t i c a l Statistical Statistical M a c h i n e Machine Machine T r a n s l a t i o n 》 Translation》 Translation》比前者更早使用了 S e q 2 S e q Seq2Seq Seq2Seq 模型来解决机器翻译的问题,本文是该篇论文的概述。
论文地址:Learning Phrase Representations using RNN Encoder–Decoder for Statistical Machine Translation
2014 2014 2014 年 6 6 6 月发布于 A r x i v Arxiv Arxiv 上,一作是 K y u n g h y u n Kyunghyun Kyunghyun C h o Cho Cho,当时来自蒙特利尔大学,现在在纽约大学任教。
这篇论文中提出了一种新的模型,叫做 R N N RNN RNN E n c o d e r − D e c o d e r , Encoder-Decoder, Encoder−Decoder, 并将它用来进行机器翻译和比较不同语言的短语/词组之间的语义近似程度。这个模型由两个 R N N RNN RNN 组成,其中 E n c o d e r Encoder Encoder 用来将输入的序列表示成一个固定长度的向量, D e c o d e r Decoder Decoder 则使用这个向量重建出目标序列,这两个 R N N RNN RNN共同被训练,以最大化输出目标序列的概率。另外该论文提出了 G R U GRU GRU 的基本结构,为后来的研究奠定了基础。
因此本文的主要贡献是:
首先看一个简单的循环神经网络,它由输入层、一个隐藏层和一个输出层组成,如下图所示:
如果把上面有 W W W的那个带箭头的圈去掉,它就变成了最普通的全连接神经网络。 x x x是一个向量,它表示输入层的值(这里面没有画出来表示神经元节点的圆圈); s s s是一个向量,它表示隐藏层的值(这里隐藏层面画了一个节点,你也可以想象这一层其实是多个节点,节点数与向量 s s s的维度相同);
U U U是输入层到隐藏层的权重矩阵, o o o也是一个向量,它表示输出层的值; V V V是隐藏层到输出层的权重矩阵。
那么,现在我们来看看 W W W是什么。循环神经网络的隐藏层的值 s s s不仅仅取决于当前这次的输入 x x x,还取决于上一次隐藏层的值 s s s。权重矩阵 W W W就是隐藏层上一次的值作为这一次的输入的权重。
我们给出这个抽象图对应的具体图:
现在看上去就比较清楚了,这个网络在t时刻接收到输入 x t x_{t} xt之后,隐藏层的值是 s t s_{t} st ,输出值是o_{t}。关键一点是, o t o_{t} ot的值不仅仅取决于 s t s_{t} st,还取决于 s t − 1 s_{t-1} st−1 。我们可以用下面的公式来表示循环神经网络的计算方法:
起初乍一看这个公式都会比较懵,但是很多论文中都会以这种形式来表示 S o f t M a x SoftMax SoftMax,这种写法实际上等价于 p = S o f t m a x ( W h t ) p=Softmax(Wh_{t}) p=Softmax(Wht)
解释一下, W j W_{j} Wj是参数矩阵W的第 j j j行,想一想,采用 S o f t M a x SoftMax SoftMax激活函数也就是输出 ( W h t ) (Wh_{t}) (Wht)必须是一个向量所以 W j W_{j} Wj与 h t h_{t} ht作用产生了输出向量的第 j j j个维度,所以上面公式就可以理解了。
E n c o d e r Encoder Encoder是一个 R N N , RNN, RNN,它顺序读入输入序列 X X X,并逐步更新隐状态(和普通的 R N N RNN RNN是一样的):
h < t > = f ( h < t − 1 > , x t ) h_{<t>}=f(h_{<t-1>},x_{t}) h<t>=f(h<t−1>,xt)
读到序列结尾 ( E O S ) (EOS) (EOS)之后, R N N RNN RNN的隐状态就是整个输入序列对应的表示 c c c。
D e c o d e r Decoder Decoder也是一个 R N N , RNN, RNN,这个 R N N RNN RNN的输入是 c c c,我们定义这个 R N N RNN RNN的隐藏层向量是 h = ( h 1 , . . . , h T ) , h=(h_{1},...,h_{T}), h=(h1,...,hT),和之前的 R N N RNN RNN不同之处在于这个神经网络隐藏层向量h_{}的更新不仅取决于输入 c c c和前一个状态 h < t − 1 > h_{<t-1>} h<t−1>,还取决于上一个输出 y t − 1 y_{t-1} yt−1,这个设计的精妙之处就是重视了翻译中上下文之间语境的联系,公式如下:
h < t > = f ( h < t − 1 > , c , y t − 1 ) h_{<t>}=f(h_{<t-1>},c,y_{t-1}) h<t>=f(h<t−1>,c,yt−1)
得到输出向量 y y y的思路大体上没有改变,得到条件分布仍然是采用 S o f t M a x SoftMax SoftMax
我们刚才所描述的步骤如上图所示, E n c o d e r Encoder Encoder和 D e c o d e r Decoder Decoder共同进行训练,以最大化一个交叉熵损失函数:
训练完 R N N RNN RNN E n c o d e r − D e c o d e r Encoder-Decoder Encoder−Decoder之后,模型可以通过两种方式使用。一种是根据输入序列来生成输出序列。另一种是对给定的输入输出序列进行打分,分数就是概率 p θ ( y n ∣ x n ) p_{\theta}(y_{n}|x_{n}) pθ(yn∣xn)
实验中, R N N E n c o d e r − D e c o d e r RNN Encoder-Decoder RNNEncoder−Decoder的 e n c o d e r encoder encoder和 d e c o d e r decoder decoder各有 1000 1000 1000个隐藏单元。每个输入符号 x < t > x_{<t>} x<t>和隐藏单元之间的输入矩阵用两个低秩 ( 100 ) (100) (100)矩阵来模拟,相当于学习了每个词的 100 100 100维 e m b e d d i n g embedding embedding。隐藏单元中的 h ~ \widetilde{h} h 使用的是双曲余弦函数 ( h y p e r b o l i c (hyperbolic (hyperbolic t a n g e n t tangent tangent f u n c t i o n ) function) function)。 d e c o d e r decoder decoder中隐状态到输出的计算使用的是一个深度神经网络,含有一个包含了 500 500 500个 m a x o u t maxout maxout单元的中间层。
R N N RNN RNN E n c o d e r − D e c o d e r Encoder-Decoder Encoder−Decoder的权重初值都是通过对一个各向同性的均值为零的高斯分布采样得到的,其标准差为 0.01 0.01 0.01。
通过 A d a d e l t a Adadelta Adadelta和随机梯度下降法进行训练,其中超参数为 ϵ = 1 0 − 6 \epsilon=10^{-6} ϵ=10−6, ρ = 0.95 \rho =0.95 ρ=0.95.每次更新时,从短语表中随机选出 64 64 64个短语对。模型训练了大约 3 3 3天。
因为 C S L M CSLM CSLM和 R N N RNN RNN E n c o d e r − D e c o d e r Encoder-Decoder Encoder−Decoder共同使用能进一步提高表现,说明这两种方法对结果的贡献并不相同。
除此之外,它学习到的 w o r d word word e m b e d d i n g embedding embedding矩阵也是有意义的。
import numpy as np
import torch
import torch.nn as nn
from torch.autograd import Variable
dtype = torch.FloatTensor
# S: Symbol that shows starting of decoding input
# E: Symbol that shows starting of decoding output
# P: Symbol that will fill in blank sequence if current batch data size is short than time steps
char_arr = [c for c in 'SEPabcdefghijklmnopqrstuvwxyz']
num_dic = {n: i for i, n in enumerate(char_arr)}
seq_data = [['man', 'women'], ['black', 'white'], ['king', 'queen'], ['girl', 'boy'], ['up', 'down'], ['high', 'low']]
# Seq2Seq Parameter
n_step = 5
n_hidden = 128
n_class = len(num_dic)
batch_size = len(seq_data)
def make_batch(seq_data):
input_batch, output_batch, target_batch = [], [], []
for seq in seq_data:
for i in range(2):
seq[i] = seq[i] + 'P' * (n_step - len(seq[i]))
input = [num_dic[n] for n in seq[0]]
output = [num_dic[n] for n in ('S' + seq[1])]
target = [num_dic[n] for n in (seq[1] + 'E')]
input_batch.append(np.eye(n_class)[input])
output_batch.append(np.eye(n_class)[output])
target_batch.append(target) # not one-hot
# make tensor
return Variable(torch.Tensor(input_batch)), Variable(torch.Tensor(output_batch)), Variable(torch.LongTensor(target_batch))
# Model
class Seq2Seq(nn.Module):
def __init__(self):
super(Seq2Seq, self).__init__()
self.enc_cell = nn.RNN(input_size=n_class, hidden_size=n_hidden, dropout=0.5)
self.dec_cell = nn.RNN(input_size=n_class, hidden_size=n_hidden, dropout=0.5)
self.fc = nn.Linear(n_hidden, n_class)
def forward(self, enc_input, enc_hidden, dec_input):
enc_input = enc_input.transpose(0, 1) # enc_input: [max_len(=n_step, time step), batch_size, n_class]
dec_input = dec_input.transpose(0, 1) # dec_input: [max_len(=n_step, time step), batch_size, n_class]
# enc_states : [num_layers(=1) * num_directions(=1), batch_size, n_hidden]
_, enc_states = self.enc_cell(enc_input, enc_hidden)
# outputs : [max_len+1(=6), batch_size, num_directions(=1) * n_hidden(=128)]
outputs, _ = self.dec_cell(dec_input, enc_states)
model = self.fc(outputs) # model : [max_len+1(=6), batch_size, n_class]
return model
input_batch, output_batch, target_batch = make_batch(seq_data)
model = Seq2Seq()
criterion = nn.CrossEntropyLoss()
optimizer = torch.optim.Adam(model.parameters(), lr=0.001)
for epoch in range(5000):
# make hidden shape [num_layers * num_directions, batch_size, n_hidden]
hidden = Variable(torch.zeros(1, batch_size, n_hidden))
optimizer.zero_grad()
# input_batch : [batch_size, max_len(=n_step, time step), n_class]
# output_batch : [batch_size, max_len+1(=n_step, time step) (becase of 'S' or 'E'), n_class]
# target_batch : [batch_size, max_len+1(=n_step, time step)], not one-hot
output = model(input_batch, hidden, output_batch)
# output : [max_len+1, batch_size, num_directions(=1) * n_hidden]
output = output.transpose(0, 1) # [batch_size, max_len+1(=6), num_directions(=1) * n_hidden]
loss = 0
for i in range(0, len(target_batch)):
# output[i] : [max_len+1, num_directions(=1) * n_hidden, target_batch[i] : max_len+1]
loss += criterion(output[i], target_batch[i])
if (epoch + 1) % 1000 == 0:
print('Epoch:', '%04d' % (epoch + 1), 'cost =', '{:.6f}'.format(loss))
loss.backward()
optimizer.step()
# Test
def translate(word):
input_batch, output_batch, _ = make_batch([[word, 'P' * len(word)]])
# make hidden shape [num_layers * num_directions, batch_size, n_hidden]
hidden = Variable(torch.zeros(1, 1, n_hidden))
output = model(input_batch, hidden, output_batch)
# output : [max_len+1(=6), batch_size(=1), n_class]
predict = output.data.max(2, keepdim=True)[1] # select n_class dimension
decoded = [char_arr[i] for i in predict]
end = decoded.index('E')
translated = ''.join(decoded[:end])
return translated.replace('P', '')
print('test')
print('man ->', translate('man'))
print('mans ->', translate('mans'))
print('king ->', translate('king'))
print('black ->', translate('black'))
print('upp ->', translate('upp'))