自然语言处理[1]是计算机科学领域与人工智能领域中的另一个重要方向,其中很重要的一点就是语音识别(speech recognition)、机器翻译、智能机器人。
与语言相关的技术可以应用在很多地方。例如,日本的富国生命保险公司花费170万美元安装人工智能系统,把客户的语言转换为文本,并分析这些词是正面的还是负面的。这些自动化工作将帮助人类更快地处理保险业务。除此之外,现在的人工智能公司也在把智能客服作为重点的研究方向。
与图像识别不同,在自然语言处理中输入的往往是一段语音或者一段文字,输入数据的长短是不确定的,并且它与上下文有很密切的关系,所以常用的是循环神经网络(recurrent neural network,RNN)模型。
在本节里,我们将分别介绍自然语言模型的选择、神经机器翻译的原理,最后,用200余行PaddlePaddle代码手把手带领大家做一个英法翻译机。
自然语言处理模型的选择
下面我们就来介绍使用不同输入和不同数据时,分别适用哪种模型以及如何应用。
在下图中,每一个矩形是一个向量,箭头则表示函数(如矩阵相乘)。最下面一行为输入向量,最上面一行为输出向量,中间一行是RNN的状态。
图中从左到右分别表示以下几种情况。
(1)一对一:没有使用RNN,如Vanilla模型,从固定大小的输入得到固定大小输出(应用在图像分类)。
(2)一对多:以序列输出(应用在图片描述,输入一张图片输出一段文字序列,这种往往需要CNN和RNN相结合,也就是图像和语言相结合,详见第12章)。
(3)多对一:以序列输入(应用在情感分析,输入一段文字,然后将它分类成积极或者消极情感,如淘宝下某件商品的评论分类),如使用LSTM。
(4)多对多:异步的序列输入和序列输出(应用在机器翻译,如一个RNN读取一条英文语句,然后将它以法语形式输出)。
(5)多对多:同步的序列输入和序列输出(应用在视频分类,对视频中每一帧打标记)。
我们注意到,在上述讲解中,因为中间RNN的状态的部分是固定的,可以多次使用,所以不需要对序列长度进行预先特定约束。更详细的讨论参见Andrej Karpathy的文章《The Unreasonable Effectiveness of Recurrent Neural Networks》[2]。
自然语言处理通常包括语音合成(将文字生成语音)、语音识别、声纹识别(声纹鉴权),以及它们的一些扩展应用,以及文本处理,如分词、情感分析、文本挖掘等。
神经机器翻译原理
机器翻译的作用就是将一个源语言的序列(如英文Economic growth has slowed down in recent years)转化成目标语言序列(如法文La croissance economique sest ralentie ces dernieres annees)。其中翻译机器是需要利用已有的语料库(Corpora)来进行训练。
所谓的神经网络机器翻译就是利用神经网络来实现上述的翻译机器。基于神经网络的很多技术都是从Bengio的那篇开创性论文[3]衍生出来的。这里我们介绍在机器翻译中最常用的重要技术及演进。
自然语言处理模型演进概览
我们知道正如卷积神经网络(convolutional neural network,CNN)的演进从LeNet到AlexNet,再到VggNet、GoogLeNet,最后到ResNet,演进的方式有一定规律,并且也在ImageNet LSVRC竞赛上用120万张图片、1000类标记上取得了很好的成绩。循环神经网络(recurrent neural networks,RNN)的演进从vanilla RNN到隐藏层结构精巧的GRU和LSTM,再到双向和多层的Deep Bidirectional RNN,都有一些结构和演化脉络,下面我们就首先来探讨。
Original LSTM
1997年Hochreiter和Schmidhuber首先提出了LSTM的网络结构,解决了传统RNN对于较长的序列数据,训练过程中容易出现梯度消失或爆炸的现象。Original LSTM的结构如下:
Standard LSTM
但是,传统的LSTM存在一个问题:随着时间序列的增多,LSTM网络没有重置机制(比如两句话合成一句话作为输入的话,希望是在第一句话结束的时候进行重置),从而导致cell state容易发生饱和;另一方面输出h趋近于1,导致cell的输出近似等于output gate的输出,意味着网络丧失了memory的功能。相比于简单的循环神经网络,LSTM增加了记忆单元、输入门、遗忘门及输出门。这些门及记忆单元组合起来大大提升了循环神经网络处理长序列数据的能力。
Standard LSTM的结构如下:
GRU[5]
相比于简单的RNN,LSTM增加了记忆单元(memory cell)、输入门(input gate)、遗忘门(forget gate)及输出门(output gate),这些门及记忆单元组合起来大大提升了RNN处理远距离依赖问题的能力。
GRU是Cho等人在LSTM上提出的简化版本,也是RNN的一种扩展,如下图所示。GRU单元只有两个门:
- 重置门(reset gate):如果重置门关闭,会忽略掉历史信息,即历史不相干的信息不会影响未来的输出。
- 更新门(update gate):将LSTM的输入门和遗忘门合并,用于控制历史信息对当前时刻隐层输出的影响。如果更新门接近1,会把历史信息传递下去。
GRU(门控循环单元)
一般来说,具有短距离依赖属性的序列,其重置门比较活跃;相反,具有长距离依赖属性的序列,其更新门比较活跃。GRU虽然参数更少,但是在多个任务上都和LSTM有相近的表现。
双向循环神经网络
双向循环神经网络结构的目的是输入一个序列,得到其在每个时刻的特征表示,即输出的每个时刻都用定长向量表示到该时刻的上下文语义信息。
具体来说,该双向循环神经网络分别在时间维以顺序和逆序——即前向(forward)和后向(backward)——依次处理输入序列,并将每个时间步RNN的输出拼接成为最终的输出层。这样每个时间步的输出节点,都包含了输入序列中当前时刻完整的过去和未来的上下文信息。
下图展示的是一个按时间步展开的双向循环神经网络。该网络包含一个前向和一个后向RNN,其中有六个权重矩阵:输入到前向隐层和后向隐层的权重矩阵(W1,W3W1,W3),隐层到隐层自己的权重矩阵(W2,W5W2,W5),前向隐层和后向隐层到输出层的权重矩阵(W4,W6W4,W6)。注意,该网络的前向隐层和后向隐层之间没有连接。
图 按时间步展开的双向循环神经网络
seq2seq+Attention
seq2seq模型是一个翻译模型,主要是把一个序列翻译成另一个序列。它的基本思想是用两个RNNLM,一个作为编码器,另一个作为解码器,组成RNN编码器-解码器。
在文本处理领域,我们常用编码器-解码器(encoder-decoder)框架,如图所示。
图
这是一种适合处理由一个上下文(context)生成一个目标(target)的通用处理模型。因此,对于一个句子对,当输入给定的句子X,通过编码器-解码器框架来生成目标句子Y。X和Y可以是不同语言,这就是机器翻译;X和Y可以是对话的问句和答句,这就是聊天机器人;X和Y可以是图片和这个图片的对应描述(看图说话)。
X由x1、x2等单词序列组成,Y也由y1、y2等单词序列组成。编码器对输入的X进行编码,生成中间语义编码C,然后解码器对中间语义编码C进行解码,在每个i时刻,结合已经生成的y1, y2,…, yi-1的历史信息生成Yi。但是,这个框架有一个缺点,就是生成的句子中每一个词采用的中间语义编码是相同的,都是C。因此,在句子比较短的时候,还能比较贴切,句子长时,就明显不合语义了。
在实际实现聊天系统的时候,一般编码器和解码器都采用RNN模型以及RNN模型的改进模型LSTM。当句子长度超过30以后,LSTM模型的效果会急剧下降,一般此时会引入Attention模型,对长句子来说能够明显提升系统效果。
Attention机制是认知心理学层面的一个概念,它是指当人在做一件事情的时候,会专注地做这件事而忽略周围的其他事。例如,人在专注地看这本书,会忽略旁边人说话的声音。这种机制应用在聊天机器人、机器翻译等领域,就把源句子中对生成句子重要的关键词的权重提高,产生出更准确的应答。
增加了Attention模型的编码器-解码器框架如下图所示。
现在的中间语义编码变成了不断变化的Ci,能够生产更准确的目标Yi。
目标结果展示[6]
以中英翻译(中文翻译到英文)的模型为例,当模型训练完毕时,如果输入如下已分词的中文句子:
这些 是 希望 的 曙光 和 解脱 的 迹象 .
如果设定显示翻译结果的条数为3,生成的英语句子如下:
0 -5.36816 These are signs of hope and relief .
1 -6.23177 These are the light of hope and relief .
2 -7.7914 These are the light of hope and the relief of hope .
左起第一列是生成句子的序号;左起第二列是该条句子的得分(从大到小),分值越高越好;左起第三列是生成的英语句子。 另外有两个特殊标志:表示句子的结尾,表示未登录词(unknown word),即未在训练字典中出现的词。
PaddlePaddle最佳实践[7]
下面我们就来用200余行代码构建一个英法文翻译机。
数据集及数据预处理
本次实践使用WMT-14[8]数据集中的bitexts(after selection)作为训练集,dev+test data作为测试集和生成集。
数据集格式如下:
bitexts.selected数据集,共12075604行,有大量的并行数据的英文/法文对,约850M法文单词。这个数据是相当嘈杂的,是神经网络训练的一大挑战。因此,官方已经执行了数据选择来提取最合适的数据。
“pc”之后的数字表示百分比。 一般我们基于短语的基准系统仅在这些数据上进行训练。
ep7_pc45 Europarl版本7 (27.8M)
nc9 新闻评论版本9 (5.5M)
2008年至2011年的dev08_11旧开发数据 (0.3M)
抓取常见抓取数据 (90M)
ccb2_pc30 10 ^ 9平行语料库 (81M)
un2000_pc34 联合国语料库 (143M)
下载后的数据文件如下:
我们打开Europarl版本7(ep7_pc45)数据一探究竟。
less ep7_pc45.en
可以看到,英文版本的第8行:Me ?
less ep7_pc45.fr
可以看到,对应法文版本的第8行Moi ?
因为完整的数据集数据量较大,为了验证训练流程,PaddlePaddle接口paddle.dataset.wmt14中默认提供了一个经过预处理的较小规模的数据集(wmt14)。该数据集有193319条训练数据,6003条测试数据,词典长度为30000。我们可以在这个数据集上对模型进行实验;但真正需要训练,还是建议采用原始数据集。
我们对这个较小规模的数据集进行预处理。预处理后的文件如下:
预处理流程包括3步:
1.将每个源语言到目标语言的平行语料库文件合并为一个文件;
2.合并每个XXX.src和XXX.trg文件为XXX。 - XXX中的第i行内容为XXX.src中的第i行和XXX.trg中的第i行连接,用'\t'分隔。 如train和test中处理后如下,下图每一行是一句法文和英文的平行语料,红框处代表两句之间用’\t’的分隔:
3.创建训练数据的“源字典”和“目标字典”。每个字典都有DICTSIZE个单词,包括:语料中词频最高的(DICTSIZE - 3)个单词,和3个特殊符号(序列的开始)、(序列的结束)和(未登录词)。得到的src.dict(法文词典)和trg.dict(英文词典)分别如下:
最佳实践[9]
数据处理好后,接下来我们就开始编写代码搭建神经网络及训练。[10]
paddle初始化
首先,进行paddle的初始化,直接导入Python版本的Paddle库,和TensorFlow很相似。
# 加载 paddle的python包
import sys
import paddle.v2 as paddle
# 配置只使用cpu,并且使用一个cpu进行训练
paddle.init(use_gpu=False, trainer_count=1)
# 训练模式False,生成模式True
is_generating = False
全局变量及超参数定义
这里,因为我们对数据预处理做了30000维的数据字典,所以在全局变量中也填写对应的值。
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 # 生成句子的最大长度
构建模型
首先,构建编码器框架:
输入是一个文字序列,被表示成整型的序列。序列中每个元素是文字在字典中的索引。所以,我们定义数据层的数据类型为integer_value_sequence(整型序列),序列中每个元素的范围是[0, source_dict_dim]。
src_word_id = paddle.layer.data(
name='source_language_word',
type=paddle.data_type.integer_value_sequence(source_dict_dim))
将上述编码映射到低维语言空间的词向量s。
src_embedding = paddle.layer.embedding(
input=src_word_id, size=word_vector_dim)
用双向GRU编码源语言序列,拼接两个GRU的编码结果得到h。
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])
接着,构建基于注意力机制的解码器框架:
对源语言序列编码后的结果(即上面的encoded_vector),过一个前馈神经网络(Feed Forward Neural Network),得到其映射。
encoded_proj = paddle.layer.fc(
act=paddle.activation.Linear(),
size=decoder_size,
bias_attr=False,
input=encoded_vector)
构造解码器RNN的初始状态。
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)
定义解码阶段每一个时间步的RNN行为。
def gru_decoder_with_attention(enc_vec, enc_proj, current_word):
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
那在训练模式下的解码器如何调用呢?
首先,将目标语言序列的词向量trg_embedding,直接作为训练模式下的current_word传给gru_decoder_with_attention函数。
其次,使用recurrent_group函数循环调用gru_decoder_with_attention函数。
接着,使用目标语言的下一个词序列作为标签层lbl,即预测目标词。
最后,用多类交叉熵损失函数classification_cost来计算损失值。
代码如下:
if 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)
那生成(预测)模式下的解码器如何调用呢?
首先,在序列生成任务中,由于解码阶段的RNN总是引用上一时刻生成出的词的词向量,作为当前时刻的输入,
其次,使用beam_search函数循环调用gru_decoder_with_attention函数,生成出序列id。
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 .
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)
训练模型
1.构造数据定义
我们获取wmt14的dataset reader。
if not is_generating:
wmt14_reader = paddle.batch(
paddle.reader.shuffle(
paddle.dataset.wmt14.train(dict_size=dict_size), buf_size=8192),
batch_size=5)
2.构造trainer
根据优化目标cost,网络拓扑结构和模型参数来构造出trainer用来训练,在构造时还需指定优化方法,这里使用最基本的SGD方法。
if not is_generating:
optimizer = paddle.optimizer.Adam(
learning_rate=5e-5,
regularization=paddle.optimizer.L2Regularization(rate=8e-4))
trainer = paddle.trainer.SGD(cost=cost,
parameters=parameters,
update_equation=optimizer)
3.构造event_handler
可以通过自定义回调函数来评估训练过程中的各种状态,比如错误率等。下面的代码通过event.batch_id % 2 == 0 指定每2个batch打印一次日志,包含cost等信息。
if not is_generating:
def event_handler(event):
if isinstance(event, paddle.event.EndIteration):
if event.batch_id % 2 == 0:
print "\nPass %d, Batch %d, Cost %f, %s" % (
event.pass_id, event.batch_id, event.cost, event.metrics)
4.启动训练
if not is_generating:
trainer.train(
reader=wmt14_reader, event_handler=event_handler, num_passes=2)
随后,就可以开始训练了。训练开始后,可以观察到event_handler输出的日志如下:
Pass 0, Batch 0, Cost 148.444983, {'classification_error_evaluator': 1.0}
.........
Pass 0, Batch 10, Cost 335.896802, {'classification_error_evaluator': 0.9325153231620789}
.........
预测模型
我们加载预训练的模型,然后从wmt14生成集中读取样本,试着生成结果。
1.加载预训练的模型
if is_generating:
parameters = paddle.dataset.wmt14.model()
2. 数据定义
从wmt14的生成集中读取前3个样本作为源语言句子。
if is_generating:
gen_creator = paddle.dataset.wmt14.gen(dict_size)
gen_data = []
gen_num = 3
for item in gen_creator():
gen_data.append((item[0], ))
if len(gen_data) == gen_num:
break
3. 构造infer
根据网络拓扑结构和模型参数构造出infer用来生成,在预测时还需要指定输出域field,这里使用生成句子的概率prob和句子中每个词的id。
if is_generating:
beam_result = paddle.infer(
output_layer=beam_gen,
parameters=parameters,
input=gen_data,
field=['prob', 'id'])
4.打印生成结果
根据源/目标语言字典,将源语言句子和beam_size个生成句子打印输出。
if is_generating:
# load the dictionary
src_dict, trg_dict = paddle.dataset.wmt14.get_dict(dict_size)
gen_sen_idx = np.where(beam_result[1] == -1)[0]
assert len(gen_sen_idx) == len(gen_data) * beam_size
# -1 is the delimiter of generated sequences.
# the first element of each generated sequence its length.
start_pos, end_pos = 1, 0
for i, sample in enumerate(gen_data):
print(" ".join([src_dict[w] for w in sample[0][1:-1]]))
for j in xrange(beam_size):
end_pos = gen_sen_idx[i * beam_size + j]
print("%.4f\t%s" % (beam_result[0][i][j], " ".join(
trg_dict[w] for w in beam_result[1][start_pos:end_pos])))
start_pos = end_pos + 2
print("\n")
生成开始后,可以观察到输出的日志如下:
日志的第一行为源语言的句子。下面的三行分别是分数由高到低排列的生成的英文翻译结果。
总结
这里我们着重讲解了自然语言处理当中神经机器翻译的原理,以及如何用200余行PaddlePaddle代码做一个英法翻译机。更多的,PaddlePaddle在
线性回归、识别数字、图像分类、词向量、个性化推荐、情感分析、语义角色标注等各个领域也有非常成熟的应用和简洁易上手示例,期待和大家一起探讨。
1.广义的自然语言处理包含语音处理及文本处理,狭义的单指理解和处理文本。这里指广义的概念。 ↑
2.http://karpathy.github.io/2015/05/21/rnn-effectiveness/ ↑
3.《A Neural Probabilistic Language Model》 ↑
4.https://github.com/PaddlePaddle/book/blob/develop/06.understand_sentiment/README.cn.md ↑
5.http://staging.paddlepaddle.org/docs/develop/book/08.machine_translation/index.cn.html#%E5%8F%82%E8%80%83%E6%96%87%E7%8C%AE ↑
6.http://staging.paddlepaddle.org/docs/develop/book/08.machine_translation/index.cn.html#%E5%8F%82%E8%80%83%E6%96%87%E7%8C%AE ↑
7.http://staging.paddlepaddle.org/docs/develop/book/08.machine_translation/index.cn.html#%E5%8F%82%E8%80%83%E6%96%87%E7%8C%AE ↑
8.http://www-lium.univ-lemans.fr/~schwenk/cslm_joint_paper/ ↑
9.http://staging.paddlepaddle.org/docs/develop/book/08.machine_translation/index.cn.html#%E5%8F%82%E8%80%83%E6%96%87%E7%8C%AE ↑
10.https://github.com/PaddlePaddle/book/blob/develop/08.machine_translation/train.py ↑
hello