问题来源
Encoder-Decoder模型可以根据Encoder产生的信息
c
c
c来作为Decoder的input来进行机器翻译,
c
c
c是通过Encoder计算出来的,包含了被翻译内容的所有信息。但是通常某个词的翻译只和被翻译内容的一部分信息有关,比如“我爱做饭。”,翻译成"I love cooking.",cooking的翻译只和"做饭"有关。还有其他的例子,比如某个词的翻译依赖于前面某些信息,让机器做一个阅读理解题之类的。对于短句来讲,Attention的有点不明显,但如果句子比较长,语义编码完全依赖于一个
c
c
c向量会丢失很多信息,这也是为什么会有Attention机制的原因。
Attention机制是2016年谷歌一篇机器翻译的论文带火的,虽然在199几年就有这个思想。
Attention原理
之前Decoder的预测模型是这样
y
1
=
f
(
C
)
y_1=f(C)
y1=f(C)
y
2
=
f
(
C
,
H
y
−
1
)
y_2=f(C,H_{y-1})
y2=f(C,Hy−1)
y
3
=
f
(
C
,
H
y
−
1
)
y_3=f(C,H_{y-1})
y3=f(C,Hy−1)
…
…
……
……
这意味着每个
y
y
y都依赖相同的信息
C
C
C,现在要想每个
y
i
y_i
yi依赖的信息不同,将上式做一个变化
y
1
=
f
(
C
1
)
y_1=f(C_1)
y1=f(C1)
y
2
=
f
(
C
2
,
H
y
−
1
)
y_2=f(C_2,H_{y-1})
y2=f(C2,Hy−1)
y
3
=
f
(
C
3
,
H
y
−
1
)
y_3=f(C_3,H_{y-1})
y3=f(C3,Hy−1)
…
…
……
……
比如我现在要翻译“我爱做饭。”,对于这句话来讲,对“cooking”的翻译贡献大小如下:
(我,0.3)(爱,0.2) (做饭,0.5)
可见“做饭”对“cooking”的翻译贡献最大。这意味着翻译出来的结果对原来信息的每个词的关注量是不一样的,计算
y
i
y_i
yi对每个单词的关注量会对翻译结果产生帮助。增加注意力机制的Encoder-Decoder模型就变成:
那么现在的问题变成了如何求
C
i
C_i
Ci。
这张图的
h
1
h_1
h1、
h
2
h_2
h2、
h
3
h_3
h3、
h
4
h_4
h4是Encoder的输入序列,
z
0
z_0
z0是Decoder初始化的语义编码向量(代码里在Decoder做的初始化),看箭头
z
0
z_0
z0和
h
1
h_1
h1经过一个match function,变成了
α
0
1
\alpha_0^1
α01,这个
α
0
1
\alpha_0^1
α01首先是个标量,这个值包含了
z
0
z_0
z0和
h
1
h_1
h1的信息,称作匹配程度。
match function可以自己来定义,常用的有如下几种:
- 余弦相似度
- 全连接神经网络
- 点积
我们以点积为例,计算公式如下:
α
=
h
T
W
z
\alpha=h^TWz
α=hTWz
W
W
W是我们需要训练的权重矩阵。
那么
α
j
i
=
h
i
T
W
z
j
\alpha_j^i=h_i^TWz_j
αji=hiTWzj
j
j
j是Decoder里的序列号
这样计算我们可以得到
z
0
z_0
z0对应的
α
0
1
\alpha_0^1
α01、
α
0
2
\alpha_0^2
α02、
α
0
3
\alpha_0^3
α03、
α
0
4
\alpha_0^4
α04
然后对
α
0
1
\alpha_0^1
α01、
α
0
2
\alpha_0^2
α02、
α
0
3
\alpha_0^3
α03、
α
0
4
\alpha_0^4
α04做了一步可能不是那么必要的softmax(李宏毅说的),把
α
0
1
ˊ
\acute{\alpha_0^1}
α01ˊ、
α
0
2
ˊ
\acute{\alpha_0^2}
α02ˊ、
α
0
3
ˊ
\acute{\alpha_0^3}
α03ˊ、
α
0
4
ˊ
\acute{\alpha_0^4}
α04ˊ的和变成1(看起来是标准化一下)
接着把这四个词通过下面公式计算出
c
0
c_0
c0
c
0
=
∑
a
0
i
ˊ
h
i
c_0=\sum\acute{a^i_0}h^i
c0=∑a0iˊhi
这个
c
0
c_0
c0就是要得到的注意力信息了,是个和
h
h
h尺寸一样的向量,代表着
z
0
z_0
z0和输入向量序列的关系度。然后把
c
0
c_0
c0作为Decoder下一次序列的输入进行预测,把hidden state
z
1
z_1
z1继续安装上面的方式计算。关于
z
1
z_1
z1是如何计算得出的,现在没有公认做好的做法,怎么做都可以。
用
z
1
z_1
z1继续Encoder的
h
i
h^i
hi计算,得到
a
1
i
a^i_1
a1i
这样就计算出了
c
i
c_i
ci(由于和图片用的字母不一样,图里的
z
i
z_i
zi就是这里的
c
i
c_i
ci)
Seq2Seq
Seq2Seq模型1是说序列对序列,是Encoder-Decoder思想的一种应用实现,我觉得没啥大的区别,提到Seq2Seq就想是NLP领域的Encoder-Decoder就好,毕竟Encoder-Decoder还可以做图像。这里介绍的是加入Attention的Seq2Seq模型。
训练阶段,保存了Encoder侧所有的hidden state,Key是时间步,Value是state向量值。在Decoder侧,每次按照上面Attention里介绍的,去计算相关性。
解码器部分
class Seq2SeqAttentionDecoder(d2l.Decoder):
def __init__(self, vocab_size, embed_size, num_hiddens, num_layers,
dropout=0, **kwargs):
super(Seq2SeqAttentionDecoder, self).__init__(**kwargs)
self.attention_cell = MLPAttention(num_hiddens,num_hiddens, dropout)
self.embedding = nn.Embedding(vocab_size, embed_size)
self.rnn = nn.LSTM(embed_size+ num_hiddens,num_hiddens, num_layers, dropout=dropout)
self.dense = nn.Linear(num_hiddens,vocab_size)
def init_state(self, enc_outputs, enc_valid_len, *args):
outputs, hidden_state = enc_outputs
# print("first:",outputs.size(),hidden_state[0].size(),hidden_state[1].size())
# Transpose outputs to (batch_size, seq_len, hidden_size)
return (outputs.permute(1,0,-1), hidden_state, enc_valid_len)
#outputs.swapaxes(0, 1)
def forward(self, X, state):
enc_outputs, hidden_state, enc_valid_len = state
#("X.size",X.size())
X = self.embedding(X).transpose(0,1)
# print("Xembeding.size2",X.size())
outputs = []
for l, x in enumerate(X):
# print(f"\n{l}-th token")
# print("x.first.size()",x.size())
# query shape: (batch_size, 1, hidden_size)
# select hidden state of the last rnn layer as query
query = hidden_state[0][-1].unsqueeze(1) # np.expand_dims(hidden_state[0][-1], axis=1)
# context has same shape as query
# print("query enc_outputs, enc_outputs:\n",query.size(), enc_outputs.size(), enc_outputs.size())
context = self.attention_cell(query, enc_outputs, enc_outputs, enc_valid_len)
# Concatenate on the feature dimension
# print("context.size:",context.size())
x = torch.cat((context, x.unsqueeze(1)), dim=-1)
# Reshape x to (1, batch_size, embed_size+hidden_size)
# print("rnn",x.size(), len(hidden_state))
out, hidden_state = self.rnn(x.transpose(0,1), hidden_state)
outputs.append(out)
outputs = self.dense(torch.cat(outputs, dim=0))
return outputs.transpose(0, 1), [enc_outputs, hidden_state,
enc_valid_len]
训练
import zipfile
import torch
import requests
from io import BytesIO
from torch.utils import data
import sys
import collections
class Vocab(object): # This class is saved in d2l.
def __init__(self, tokens, min_freq=0, use_special_tokens=False):
# sort by frequency and token
counter = collections.Counter(tokens)
token_freqs = sorted(counter.items(), key=lambda x: x[0])
token_freqs.sort(key=lambda x: x[1], reverse=True)
if use_special_tokens:
# padding, begin of sentence, end of sentence, unknown
self.pad, self.bos, self.eos, self.unk = (0, 1, 2, 3)
tokens = ['', '', '', '']
else:
self.unk = 0
tokens = ['']
tokens += [token for token, freq in token_freqs if freq >= min_freq]
self.idx_to_token = []
self.token_to_idx = dict()
for token in tokens:
self.idx_to_token.append(token)
self.token_to_idx[token] = len(self.idx_to_token) - 1
def __len__(self):
return len(self.idx_to_token)
def __getitem__(self, tokens):
if not isinstance(tokens, (list, tuple)):
return self.token_to_idx.get(tokens, self.unk)
else:
return [self.__getitem__(token) for token in tokens]
def to_tokens(self, indices):
if not isinstance(indices, (list, tuple)):
return self.idx_to_token[indices]
else:
return [self.idx_to_token[index] for index in indices]
def load_data_nmt(batch_size, max_len, num_examples=1000):
"""Download an NMT dataset, return its vocabulary and data iterator."""
# Download and preprocess
def preprocess_raw(text):
text = text.replace('\u202f', ' ').replace('\xa0', ' ')
out = ''
for i, char in enumerate(text.lower()):
if char in (',', '!', '.') and text[i-1] != ' ':
out += ' '
out += char
return out
with open('/home/kesci/input/fraeng6506/fra.txt', 'r') as f:
raw_text = f.read()
text = preprocess_raw(raw_text)
# Tokenize
source, target = [], []
for i, line in enumerate(text.split('\n')):
if i >= num_examples:
break
parts = line.split('\t')
if len(parts) >= 2:
source.append(parts[0].split(' '))
target.append(parts[1].split(' '))
# Build vocab
def build_vocab(tokens):
tokens = [token for line in tokens for token in line]
return Vocab(tokens, min_freq=3, use_special_tokens=True)
src_vocab, tgt_vocab = build_vocab(source), build_vocab(target)
# Convert to index arrays
def pad(line, max_len, padding_token):
if len(line) > max_len:
return line[:max_len]
return line + [padding_token] * (max_len - len(line))
def build_array(lines, vocab, max_len, is_source):
lines = [vocab[line] for line in lines]
if not is_source:
lines = [[vocab.bos] + line + [vocab.eos] for line in lines]
array = torch.tensor([pad(line, max_len, vocab.pad) for line in lines])
valid_len = (array != vocab.pad).sum(1)
return array, valid_len
src_vocab, tgt_vocab = build_vocab(source), build_vocab(target)
src_array, src_valid_len = build_array(source, src_vocab, max_len, True)
tgt_array, tgt_valid_len = build_array(target, tgt_vocab, max_len, False)
train_data = data.TensorDataset(src_array, src_valid_len, tgt_array, tgt_valid_len)
train_iter = data.DataLoader(train_data, batch_size, shuffle=True)
return src_vocab, tgt_vocab, train_iter
训练调用
embed_size, num_hiddens, num_layers, dropout = 32, 32, 2, 0.0
batch_size, num_steps = 64, 10
lr, num_epochs, ctx = 0.005, 500, d2l.try_gpu()
src_vocab, tgt_vocab, train_iter = load_data_nmt(batch_size, num_steps)
encoder = d2l.Seq2SeqEncoder(
len(src_vocab), embed_size, num_hiddens, num_layers, dropout)
decoder = Seq2SeqAttentionDecoder(
len(tgt_vocab), embed_size, num_hiddens, num_layers, dropout)
model = d2l.EncoderDecoder(encoder, decoder)
参考资料
深度学习中的注意力机制(2017版)
台湾李宏毅老师-机器学习
Sutskever, I., Vinyals, O., & Le, Q. V. (2014). Sequence to sequence learning with neural networks. In Advances in neural information processing systems (pp. 3104-3112). ↩︎