目录
paddle rnn系列文档: https://github.com/PaddlePaddle/Paddle/tree/develop/doc/howto/deep_model/rnn
参考https://en.wikipedia.org/wiki/Long_short-term_memory
最开始的lstm只有inputgate和outputgateLong short-term memory, 1997。
后来在Learning to Forget: Continual Prediction with LSTM, 2000引入了forgetgate。
公式如下,其中,\(\circ \)
表示element-wise的乘积:
\[
\\ f_t=\sigma _g(W_fx_t+U_fh_{t-1}+b_f)
\\ i_t=\sigma _g(W_ix_t+U_ih_{t-1}+b_i)
\\ o_t=\sigma _g(W_ox_t+W_oh_{t-1}+b_o)
\\ c_t=f_t \circ c_{t-1}+i_t \circ \sigma _c(W_cx_t+U_ch_{t-1}+b_c)
\\ h_t=o_t \circ \sigma _h(c_t)
\]
其中,初始值\(c_0=0\)
,\(h_0=0\)
,\(\circ \)
表示element-wise的乘积,也就是图中的\(\otimes \)
变量:
\(x_t \in R^d\)
:LSTM block的input vector\(f_t \in R^h\)
:forgetgate的activation vector\(i_t \in R^h\)
:inputgate的activation vector\(o_t \in R^h\)
:outputgate的activation vector\(h_t \in R^h\)
:LSTM block的output vector\(c_t \in R^h\)
:cell state vector\(W \in R^{h \times d}\)
,\(U \in R^{h \times h}\)
和\(b \in R^{h}\)
:需要学习的权重和bias激活函数:
\(\sigma _g\)
: sigmoid\(\sigma _c\)
: tanh\(\sigma _h\)
: tanh,但在peephole LSTM中,这个是\(\sigma _h(x)=x\)
在LSTM Recurrent Networks Learn Simple Context Free and Context Sensitive Languages, 2001以及Learning Precise Timing with LSTM Recurrent Networks引入peephole conneciton:
和lstm基本一样,区别在于不用\(h_{t-1}\)
了,大部分都换成了\(c_{t-1}\)
。。
\[
\\ f_t=\sigma _g(W_fx_t+U_fc_{t-1}+b_f)
\\ i_t=\sigma _g(W_ix_t+U_ic_{t-1}+b_i)
\\ o_t=\sigma _g(W_ox_t+U_oc_{t-1}+b_o)
\\ c_t=f_t \circ c_{t-1}+i_t \circ \sigma _c(W_cx_t+b_c)
\\ h_t=o_t\circ \sigma _h(c_t)
\]
直观对比如下:
参考知乎:
An Empirical Exploration of Recurrent Network Architectures的第2节
Empirical Evaluation of Gated Recurrent Neural Networks on Sequence Modeling的第3.3节
\(S_t=f(S_{t-1},x_t)\)
,f是仿射运算后再套sigmoid。所以根据求导的链式法则,梯度会变成连积形式。多个小于1的连乘就会很快接近0,导致梯度消失。\(S_t=\sum_{\tau=1}^{t}\Delta S_{\tau}\)
,这种累加形式导致导数也是累加形式,因此可以缓解梯度消失。也就是说,传统的rnn是对\(\sigma _c(xxxh_{t-1})\)
【其中\(\sigma _c\)
是tanh】乘以w,然后再算sigmoid。而lstm对这项只是乘了一个(0,1)的系数(\(i_t\)
),然后加上\(f_t \dot c_{t-1}\)
,再求tanh再乘一个(0,1)的系数(\(o_t\)
),也就是说,并没有乘w,只是和一堆(0,1)的系数进行了线性变换,然后求和。。。
GRU是Cho等人在LSTM上提出的简化版本,也是RNN的一种扩展,如下图所示。GRU单元只有两个门:
\(z_t\)
【更新门】和\(r_t\)
【重置门】都作用(sigmoid)于\([h_{t-1},x_t]\)
【即上一时刻的隐层状态\(h_{t-1}\)
和这一时刻的输入\(x_t\)
】。
而重置门作用于\(h_{t-1}\)
得到\(r_t*h_{t-1}\)
【注意,是element-wise乘积】再与\(x_t\)
一起经过tanh得到节点状态。
最终的输出是更新门与节点状态相乘,再加上\(1-z_t\)
与上一时刻的隐层状态\(h_{t-1}\)
的乘积。
总结一下:
我们已经在语义角色标注一章中介绍了一种双向循环神经网络,这里介绍Bengio团队在论文【Bahdanau等人Neural Machine Translation by Jointly Learning to Align and Translate, ICLR, 2015】中提出的另一种结构。该结构的目的是输入一个序列,得到其在每个时刻的特征表示,即输出的每个时刻都用定长向量表示到该时刻的上下文语义信息。
具体来说,该双向循环神经网络分别在时间维以顺序和逆序——即前向(forward)和后向(backward)——依次处理输入序列,并将每个时间步RNN的输出拼接成为最终的输出层。这样每个时间步的输出节点,都包含了输入序列中当前时刻完整的过去和未来的上下文信息。下图展示的是一个按时间步展开的双向循环神经网络。该网络包含一个前向和一个后向RNN,其中有六个权重矩阵:输入到前向隐层和后向隐层的权重矩阵(W1,W3),隐层到隐层自己的权重矩阵(W2,W5),前向隐层和后向隐层到输出层的权重矩阵(W4,W6)。注意,该网络的前向隐层和后向隐层之间没有连接。
编码器-解码器(Encoder-Decoder)(Learning phrase representations using RNN encoder-decoder for statistical machine translation)框架用于解决由一个任意长度的源序列到另一个任意长度的目标序列的变换问题。即编码阶段将整个源序列编码成一个向量,解码阶段通过最大化预测序列概率,从中解码出整个目标序列。编码和解码的过程通常都使用RNN实现。
总体流程如下:
重点在于: 最后对于词xi,通过拼接两个GRU的结果得到它的隐层状态。
关键在于,预测的\(z_{i+1}\)
由以下三部分经过非线性激活产生:
\(c\)
(\(c=qh\)
,如果不用注意力,可以直接取最后一个时间步的编码\(c=h_T\)
,也可以用时间维上的pooling的结果)。\(u_i\)
。\(u_0\)
是开始标志<s>
。\(z_i\)
, \(z_0\)
是全零向量。如果在编码阶段的输出是一个固定维度的向量,会有以下两个问题:
而固定维度的向量相当于,任何时刻都对源语言所有信息给予同等程度的关注,Bahdanau等人Neural Machine Translation by Jointly Learning to Align and Translate, ICLR, 2015引入注意力机制,对编码后的上下文片段进行解码,以此解决长句子的特征学习问题。
和简单的编码器不同之处在于,前面生成\(z_{i+1}\)
用的是\(c\)
,而这里用的是\(c_i\)
,也就是,对每个真实目标语言序列中的词\(u_i\)
【注意!!是目标语言的i,不是源语言的!!】,都有一个特定的\(c_i\)
与之对应。
\[
z_{i+1}=\phi _{\theta }(c_i, u_i, z_i)
\]
\[
c_i=\sum ^T_{j=1}a_{ij}h_j, a_i=[a_{i1}, a_{i2}, ..., a_{iT}]
\]
可见,注意力机制是通过对编码器中各时刻的RNN状态\(h_j\)
进行加权平均实现的。权重\(a_{ij}\)
计算方法如下:
\[
c_i=\sum ^T_{j=1}a_{ij}h_j, a_i=[a_{i1}, a_{i2}, ..., a_{iT}]
\]
\[
a_{ij}=\frac {exp(e_{ij})}{\sum _{k=1}^Texp(e_{ik})}
\]
\[\
e_{ij}=align(z_i,h_j)
\]
其中,align可以看作一个对齐模型,用于衡量目标语言第i个词(第i个隐层状态\(z_i\)
)和源语言第j个词(第j个词的上下文片段\(h_j\)
)的匹配程度。
传统的对齐模型中,目标语言的每个词明确对应源语言的一个词或多个词(hard alignment);而这里用的是soft alignment,即任何两个目标语言和源语言词间均存在一定的关联(模型计算出的实数值)。
柱搜索(beam search)算法是一种启发式图搜索算法,用于在图或树中搜索有限集合中的最优扩展节点。通常用在解空间非常大的系统(如机器翻译、语音识别)中,因为内存无法存下图或权中所有展开的解。
beam search使用广度优先策略建立搜索树,在树的每一层,按照启发代价(heuristic cost, 例如本例中的生成词的log概率之和)对节点进行排序,然后仅留下预先确定的个数(即beam width/beam size/柱宽度)的节点。只有这些节点会在下一层继续进行扩展,其他节点被裁剪掉了。可以减少搜索所占用的时间和空间,但无法保证一定获得最优解。
使用beam search的解码阶段,目标是最大化生成序列的概率,具体思路如下:
\(u_i\)
,和i时刻RNN的隐层状态\(z_i\)
,计算出下一个隐层状态\(z_{i+1}\)
\(z_{i+1}\)
通过softmax【只针对beamsearch得到的可能的解做处理】归一化,得到的目标语言序列的第i+1个单词的概率分布\(p_{i+1}\)
\[
p(u_{i+1}|u_{\lt i+1}, x)=softmax(W_sz_{i+1}+b_z)
\]
其中,\(W_sz_{i+1}+b_z\)
是对每个可能的输出单词进行打分\(p_{i+1}\)
采样出单词u_{i+1}
<e>
或者超过句子的最大生成长度为止。一些全局变量
dict_size = 30000 # 字典维度
source_dict_dim = dict_size # 源语言字典维度
target_dict_dim = dict_size # 目标语言字典维度
word_vector_dim = 512 # 词向量维度
encoder_size = 512 # 编码器中的GRU隐层大小
decoder_size = 512 # 解码器中的GRU隐层大小
beam_size = 3 # 柱宽度
max_length = 250 # 生成句子的最大长度
输入是一个文字序列,被表示成整型的序列。序列中每个元素是文字在字典中的索引。
src_word_id = paddle.layer.data(
name='source_language_word',
type=paddle.data_type.integer_value_sequence(source_dict_dim))
然后映射成词向量src_embedding
:
src_embedding = paddle.layer.embedding(
input=src_word_id, size=word_vector_dim)
然后使用双向gru进行编码,然后拼接两个gru的输出得到\(h\)
【解码器中的\(h_j\)
】即encoded_vector
:
src_forward = paddle.networks.simple_gru(
input=src_embedding, size=encoder_size)
src_backward = paddle.networks.simple_gru(
input=src_embedding, size=encoder_size, reverse=True)
encoded_vector = paddle.layer.concat(input=[src_forward, src_backward])
首先,对源语言序列编码后的结果【\(h_j\)
】过一个fc,得到其映射encoded_proj
【即,\(h_j\)
和\(z_i\)
的映射,也就是对齐模型\(e_{ij}\)
】。
encoded_proj = paddle.layer.fc(
act=paddle.activation.Linear(),
size=decoder_size,
bias_attr=False,
input=encoded_vector)
由于解码器需要预测时序目标序列,但在0时刻并没有初始值,所以我们希望对其进行初始化。
这里采用的是将源语言序列逆序编码后(src_backward
)的最后一个状态(backward_first
)进行非线性映射fc,作为源语言上下文向量\(c_i\)
的初始值(decoder_boot
),即\(c_0=h_T\)
backward_first = paddle.layer.first_seq(input=src_backward)
decoder_boot = paddle.layer.fc(
size=decoder_size,
act=paddle.activation.Tanh(),
bias_attr=False,
input=backward_first)
根据三个输入【当前时刻的源语言上下文向量\(c_i\)
、解码器隐层状态\(z_i\)
、真实目标语言中的第i个词\(u_i\)
】,来预测第i+1个词的概率\(p_{i+1}\)
。
主要过程(输入\(h_j\)
【即enc_vec
】、\(e_{ij}\)
【即enc_proj
】和\(u_i\)
【即current_word
】:
\(c_0\)
的初始化,得到解码器隐层状态\(z_i\)
【即decoder_mem
】\(e_{ij}\)
生成attention\(a_{ij}\)
\(a_{ij}\)
和\(h_j\)
一起得到\(c_i\)
【和上一步一起,封装在simple_attention函数中,得到context
】\(c_i\)
、\(u_i\)
和\(z_i\)
得到\(z_{i+1}\)
【先context
和current_word
一起经过一个fc,得到decoder_inputs
,然后和decoder_mem
一起给gru_step
做输入,得到gru_step
】full_matrix_projection
+softmax,得到out
【即公式:\(p(u_{i+1}|u_{\lt i+1})=softmax(W_sz_{i+1}+b_z)\)
】def gru_decoder_with_attention(enc_vec, enc_proj, current_word):
"""
enc_vec: h_j
enc_proj: e_{ij}
current_word: u_i
"""
decoder_mem = paddle.layer.memory(
name='gru_decoder', size=decoder_size, boot_layer=decoder_boot)
context = paddle.networks.simple_attention(
encoded_sequence=enc_vec,
encoded_proj=enc_proj,
decoder_state=decoder_mem)
decoder_inputs = paddle.layer.fc(
act=paddle.activation.Linear(),
size=decoder_size * 3,
bias_attr=False,
input=[context, current_word],
layer_attr=paddle.attr.ExtraLayerAttribute(
error_clipping_threshold=100.0))
gru_step = paddle.layer.gru_step(
name='gru_decoder',
input=decoder_inputs,
output_mem=decoder_mem,
size=decoder_size)
out = paddle.layer.mixed(
size=target_dict_dim,
bias_attr=True,
act=paddle.activation.Softmax(),
input=paddle.layer.full_matrix_projection(input=gru_step))
return out
也就是说,\(h_j\)
和\(e_{ij}\)
需要分别通过StaticInput
进行包装一下。
recurrent_group处理的输入序列主要分为以下三种类型:
decoder_group_name = "decoder_group"
group_input1 = paddle.layer.StaticInput(input=encoded_vector)
group_input2 = paddle.layer.StaticInput(input=encoded_proj)
group_inputs = [group_input1, group_input2]
注意,wmt14的reader(paddle/v2/dataset/wmt14.py中)如下:
def reader_creator(tar_file, file_name, dict_size):
def reader():
src_dict, trg_dict = __read_to_dict__(tar_file, dict_size)
with tarfile.open(tar_file, mode='r') as f:
names = [
each_item.name for each_item in f
if each_item.name.endswith(file_name)
]
for name in names:
for line in f.extractfile(name):
line_split = line.strip().split('\t')
if len(line_split) != 2:
continue
src_seq = line_split[0] # one source sequence
src_words = src_seq.split()
src_ids = [
src_dict.get(w, UNK_IDX)
for w in [START] + src_words + [END]
]
trg_seq = line_split[1] # one target sequence
trg_words = trg_seq.split()
trg_ids = [trg_dict.get(w, UNK_IDX) for w in trg_words]
# remove sequence whose length > 80 in training mode
if len(src_ids) > 80 or len(trg_ids) > 80:
continue
trg_ids_next = trg_ids + [trg_dict[END]]
trg_ids = [trg_dict[START]] + trg_ids
yield src_ids, trg_ids, trg_ids_next
return reader
trg_embedding
,作为current_word
,即\(u_i\)
,放到group_inputs
中recurrent_group
,step指定为gru_decoder_with_attention
,input为group_inputs
target_language_next_word
作为labelif not is_generating:
trg_embedding = paddle.layer.embedding(
input=paddle.layer.data(
name='target_language_word',
type=paddle.data_type.integer_value_sequence(target_dict_dim)),
size=word_vector_dim,
param_attr=paddle.attr.ParamAttr(name='_target_language_embedding'))
group_inputs.append(trg_embedding)
# For decoder equipped with attention mechanism, in training,
# target embeding (the groudtruth) is the data input,
# while encoded source sequence is accessed to as an unbounded memory.
# Here, the StaticInput defines a read-only memory
# for the recurrent_group.
decoder = paddle.layer.recurrent_group(
name=decoder_group_name,
step=gru_decoder_with_attention,
input=group_inputs)
lbl = paddle.layer.data(
name='target_language_next_word',
type=paddle.data_type.integer_value_sequence(target_dict_dim))
cost = paddle.layer.classification_cost(input=decoder, label=lbl)
beam_search
函数循环调用gru_decoder_with_attention
函数,生成出序列id。和训练的区别:
\(h_j\)
和\(e_{ij}\)
的StaticInput,第三个输入有区别:训练是目标语言序列的embedding,而生成使用的是GeneratedInputbeam_search
,而训练使用的是recurrent_group
。if is_generating:
# In generation, the decoder predicts a next target word based on
# the encoded source sequence and the previous generated target word.
# The encoded source sequence (encoder's output) must be specified by
# StaticInput, which is a read-only memory.
# Embedding of the previous generated word is automatically retrieved
# by GeneratedInputs initialized by a start mark <s>.
trg_embedding = paddle.layer.GeneratedInput(
size=target_dict_dim,
embedding_name='_target_language_embedding',
embedding_size=word_vector_dim)
group_inputs.append(trg_embedding)
beam_gen = paddle.layer.beam_search(
name=decoder_group_name,
step=gru_decoder_with_attention,
input=group_inputs,
bos_id=0,
eos_id=1,
beam_size=beam_size,
max_length=max_length)
https://github.com/PaddlePaddle/Paddle/issues/1269
在AI Challenger比赛的机器翻译赛题中,冠军的做法如下:
用pytorch(轻量,且实现Deliberation networks相对容易),8卡titan xp,一次训练将近一周。
基于encoder-decoder+attention,其中,rnn使用3层lstm,dim(h)=1024。
对于处理未登录词:
参考Neural Machine Translation of Rare Words with Subword Units【NMT中的OOV(集外词)和罕见词(Rare Words)问题通常用back-off 词典的方式来解决,本文尝试用一种更简单有效的方式(Subword Units)来表示开放词表。 本文从命名实体、同根词、外来语、组合词(罕见词有相当大比例是上述几种)的翻译策略中得到启发,认为把这些罕见词拆分为“子词单元”(subword units)的组合,可以有效的缓解NMT的OOV和罕见词翻译的问题。子词单元的拆分策略,则是借鉴了一种数据压缩算法Byte Pair Encoding(BPE),这里的压缩算法不是针对于词做变长编码,而是对于子词来操作。这样,即使是训练语料里未见过的新词,也可以通过子词的拼接来生成翻译。本文还探讨了BPE的两种编码方式:一种是源语言词汇和目标语言词汇分别编码,另一种是双语词汇联合编码。前者的优势是让词表和文本的表示更紧凑,后者则可以尽可能保证原文和译文的子词切分方式统一。从实验结果来看,在音译或简单复制较多的情形下(比如英德)翻译,联合编码的效果更佳。github:https://github.com/rsennrich/subword-nmt】 解决低频词问题,相比Char/Word混合编码更为高效。源和目标的vocabulary size大约都是3.5w。
目标:
\[
max_\theta E_{\tau \sim \pi _\theta }[R(\tau )]
\]
先用cross-entropy训出一个基本可用的,再用增强学习,以bleu为目标,使用梯度上升的方法去改进\(\theta \)
。
\[
g=E_{\tau \sim \pi _\theta }[\nabla _\theta log\pi _\theta (\tau)R(\tau ))]
\]
其中,
\(\pi _\theta (\cdot )\)
为现有的翻译模型\tau
为根据现有模型所生成的一个翻译结果R(\tau)
采用句子级别BLEU进行计算假设模型基本正确,生成k个可能正确的翻译结果,即,采用beamsearch,得到源语言x的多个翻译结果\(\{t_i\}^K_{i=1}\)
,然后根据bleu score计算平均值:
\[
\bar{b}=\frac{1}{K}\sum_iR(t_i)
\]
然后估算梯度:
\[
\hat{g}=\frac{1}{K}\sum_i\nabla _\theta log\pi _\theta (t_i)[R(t_i)-\bar{b})
\]
有0.3bleu的提升
\[
L^{clip}(\theta)=E_\tau [min(r(\theta )R(\tau),clip(r(\theta),1-\epsilon,1+\epsilon)R(\tau))]
\]
其中,\(r(\theta)=\frac{\pi_\theta(\tau)}{\pi_{\theta_{old}}(\tau)}\)
计算算数平均值
\[
\bar{\pi_\theta}(a;s)=\frac{1}{M}\sum_{j=1}^M\pi^j_\theta(a;s)
\]
然后采用上述结果\(\bar{\pi_\theta}(a;s)\)
进行beam search,得到最终结果
M=8个模型融合,相比单模型,+1.1 bleu
beamsearch时,除了max-likelihood外,还加入了对coverage的penalty和对句子长度的normalization计算。
注意: cnn的卷积操作参考https://daiwk.github.io/posts/image-classical-cnns.html
所以,推荐系统里面cnn的例子里,原矩阵是nxk,卷积核是hxk,所以得到的结果是(n-h+1)x(k-k+1)=(n-h+1)x1.所谓的时间维度上的maxpooling,就是对着这(n-h+1)个数字取max.
如果有多个(例如,xx个)卷积核,那就有多个(n-h+1)个序列,h和每个卷积核相关,那取pooling就是每个(n-h+1)里取max,最后得到xx个max值。
在sequence_conv_pool中,本质是Text input => Context Projection => FC Layer => Pooling => Output.
,
with mixed_layer(
name=context_proj_layer_name,
size=input.size * context_len,
act=LinearActivation(),
layer_attr=context_attr) as m:
m += context_projection(
input,
context_len=context_len,
context_start=context_start,
padding_attr=context_proj_param_attr)
### 打平成一个input.size*context_len的了?假设输入是一个序列,那么输出不是序列的序列了?而是一个单层序列?另外注意,这里的输入实际上是embedding,也就是序列的序列.
fc_layer_name = "%s_conv_fc" % name \
if fc_layer_name is None else fc_layer_name
fl = fc_layer(
name=fc_layer_name,
input=m,
size=hidden_size,
act=fc_act,
layer_attr=fc_attr,
param_attr=fc_param_attr,
bias_attr=fc_bias_attr)
return pooling_layer(
name=name,
input=fl,
pooling_type=pool_type,
bias_attr=pool_bias_attr,
layer_attr=pool_attr)
而其中的context_projection如下:
For example, origin sequence is [A B C D E F G], context len is 3, then
after context projection and not set padding_attr, sequence will
be [ 0AB ABC BCD CDE DEF EFG FG0 ].
在stacked_lstm的例子中:
fc_last = paddle.layer.pooling(
input=inputs[0], pooling_type=paddle.pooling.Max())
lstm_last = paddle.layer.pooling(
input=inputs[1], pooling_type=paddle.pooling.Max())
output = paddle.layer.fc(
input=[fc_last, lstm_last],
size=class_dim,
act=paddle.activation.Softmax(),
bias_attr=bias_attr,
param_attr=para_attr)
另外,
lstmemory的输出大小固定是输入大小的1/4
size=input.size / 4,
agg_level=AggregateLevel.TO_NO_SEQUENCE pooling 默认把一个单层序列变成一个0层序列,默认是maxpooling,也就是整个序列取一个最大值当做输出。