自然语言处理[1]是计算机科学领域与人工智能领域中的另一个重要方向,其中很重要的一点就是语音识别(speech recognition)、机器翻译、智能机器人。
与语言相关的技术可以应用在很多地方。例如,日本的富国生命保险公司花费 170 万美元安装人工智能系统,把客户的语言转换为文本,并分析这些词是正面的还是负面的。
这些自动化工作将帮助人类更快地处理保险业务。除此之外,现在的人工智能公司也在把智能客服作为重点的研究方向。
与图像识别不同,在自然语言处理中输入的往往是一段语音或者一段文字,输入数据的长短是不确定的,并且它与上下文有很密切的关系,所以常用的是循环神经网络(recurrent neural network,RNN)模型。
在本节里,我们将分别介绍自然语言模型的选择、神经机器翻译的原理,最后,用 200 余行 PaddlePaddle 代码手把手带领大家做一个英法翻译机。
自然语言处理模型的选择
下面我们就来介绍使用不同输入和不同数据时,分别适用哪种模型以及如何应用。
在下图中,每一个矩形是一个向量,箭头则表示函数(如矩阵相乘)。最下面一行为输入向量,最上面一行为输出向量,中间一行是 RNN 的状态。
上图中,从左到右分别表示以下几种情况:
一对一:没有使用 RNN,如 Vanilla 模型,从固定大小的输入得到固定大小输出(应用在图像分类)。
一对多:以序列输出(应用在图片描述,输入一张图片输出一段文字序列,这种往往需要 CNN 和 RNN 相结合,也就是图像和语言相结合,详见第 12 章)。
多对一:以序列输入(应用在情感分析,输入一段文字,然后将它分类成积极或者消极情感,如淘宝下某件商品的评论分类),如使用 LSTM。
多对多:异步的序列输入和序列输出(应用在机器翻译,如一个 RNN 读取一条英文语句,然后将它以法语形式输出)。
多对多:同步的序列输入和序列输出(应用在视频分类,对视频中每一帧打标记)。
1.广义的自然语言处理包含语音处理及文本处理,狭义的单指理解和处理文本。这里指广义的概念。
我们注意到,在上述讲解中,因为中间 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,都有一些结构和演化脉络,下面我们就首先来探讨。
2.http://karpathy.github.io/2015/05/21/rnn-effectiveness/
3.《A Neural Probabilistic Language Model》
Original LSTM
1997 年 Hochreiter 和 Schmidhuber 首先提出了 LSTM 的网络结构,解决了传统 RNN 对于较长的序列数据,训练过程中容易出现梯度消失或爆炸的现象。
Original LSTM 的结构如下图:
Standard LSTM
但是,传统的 LSTM 存在一个问题:随着时间序列的增多,LSTM 网络没有重置机制(比如两句话合成一句话作为输入的话,希望是在第一句话结束的时候进行重置),从而导致 cell state 容易发生饱和。
另一方面输出 h 趋近于 1,导致 cell 的输出近似等于 output gate 的输出,意味着网络丧失了 memory 的功能。相比于简单的循环神经网络,LSTM 增加了记忆单元、输入门、遗忘门及输出门。
这些门及记忆单元组合起来大大提升了循环神经网络处理长序列数据的能力。
Standard LSTM 的结构如下:
图4
GRU[5]
相比于简单的 RNN,LSTM 增加了记忆单元(memory cell)、输入门(input gate)、遗忘门(forget gate)及输出门(output gate),这些门及记忆单元组合起来大大提升了 RNN 处理远距离依赖问题的能力。
GRU 是 Cho 等人在 LSTM 上提出的简化版本,也是 RNN 的一种扩展,如下图所示。
GRU 单元只有两个门:
重置门(reset gate):如果重置门关闭,会忽略掉历史信息,即历史不相干的信息不会影响未来的输出。
更新门(update gate):将 LSTM 的输入门和遗忘门合并,用于控制历史信息对当前时刻隐层输出的影响。如果更新门接近 1,会把历史信息传递下去。
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
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 由 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]
以中英翻译(中文翻译到英文)的模型为例,当模型训练完毕时,如果输入如下已分词的中文句子:
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
这些是希望的曙光和解脱的迹象 .
如果设定显示翻译结果的条数为 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 .
左起第一列是生成句子的序号;左起第二列是该条句子的得分(从大到小),分值越高越好;左起第三列是生成的英语句子。
另外有两个特殊标志:
PaddlePaddle 最佳实践[7]
下面我们就来用 200 余行代码构建一个英法文翻译机。
本次实践使用 WMT-14[8]数据集中的 bitexts(after selection)作为训练集,dev+test data 作为测试集和生成集。
数据集格式如下:
bitexts.selected 数据集,共 12075604 行,有大量的并行数据的英文/法文对,约 850M 法文单词。这个数据是相当嘈杂的,是神经网络训练的一大挑战。因此,官方已经执行了数据选择来提取最合适的数据。
“pc”之后的数字表示百分比。 一般我们基于短语的基准系统仅在这些数据上进行训练。
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/
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 步:
将每个源语言到目标语言的平行语料库文件合并为一个文件;
合并每个XXX.src和XXX.trg文件为XXX。 - XXX中的第i行内容为XXX.src中的第i行和XXX.trg中的第i行连接,用'\t'分隔。 如 train 和 test 中处理后如下,下图每一行是一句法文和英文的平行语料,红框处代表两句之间用’\t’的分隔:
创建训练数据的“源字典”和“目标字典”。每个字典都有 DICTSIZE 个单词,包括:语料中词频最高的(DICTSIZE - 3)个单词,和 3 个特殊符号(序列的开始)、
最佳实践[9]
数据处理好后,接下来我们就开始编写代码搭建神经网络及训练。[10]
paddle 初始化
首先,进行 paddle 的初始化,直接导入 Python 版本的 Paddle 库,和 TensorFlow 很相似。
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
# 加载 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 ↑