使用PaddleFluid和TensorFlow训练RNN语言模型

CAIYUNFREEDOM 2018-07-11

在图像领域,最流行的 building block 大多以卷积网络为主。上一篇我们介绍了如何在 PaddleFluid 和 TensorFlow 上训练图像分类任务。卷积网络本质上依然是一个前馈网络,在神经网络基本单元中循环神经网络是建模序列问题最有力的工具, 有着非常重要的价值。自然语言天生是一个序列,在自然语言处理领域(Nature Language Processing,NLP)中,许多经典模型都基于循环神经网络单元。可以说自然语言处理领域是 RNN 的天下。

这一篇以 NLP 领域的 RNN 语言模型(RNN Language Model,RNN LM)为实验任务,对比如何使用 PaddleFluid 和 TensorFlow 两个平台实现序列模型。 这一篇中我们会看到 PaddleFluid 和 TensorFlow 在处理序列输入时有着较大的差异:PaddleFluid 默认支持非填充的 RNN 单元,在如何组织 mini-batch 数据提供序列输入上也简化很多。

如何使用代码

本篇文章配套有完整可运行的代码, 请从随时从 github [1] 上获取最新代码。代码包括以下几个文件:

使用PaddleFluid和TensorFlow训练RNN语言模型

注意:在运行模型训练之前,请首先进入 data 文件夹,在终端运行 sh download.sh 下载训练数据。

在终端运行以下命令便可以使用默认结构和默认参数运行 PaddleFluid 训练 RNN LM。

python rnnlm_fluid.py

在终端运行以下命令便可以使用默认结构和默认参数运行 TensorFlow 训练 RNN LM。

python rnnlm_tensorflow.py

背景介绍

one-hot和词向量表示法

计算机如何表示语言是处理 NLP 任务的首要问题。这里介绍将会使用到的 one-hot 和词向量表示法。

one-hot 表示方法:一个编码单元表示一个个体,也就是一个词。于是,一个词被表示成一个长度为字典大小的实数向量,每个维度对应字典里的一个词,除了该词对应维度上的值是 1,其余维度都是 0。

词向量表示法:与 one-hot 表示相对的是 distributed representation ,也就是常说的词向量:用一个更低维度的实向量表示词语,向量的每个维度在实数域 RR 取值。

在自然语言处理任务中,一套好的词向量能够提供丰富的领域知识,可以通过预训练获取,或者与最终任务端到端学习而来。

循环神经网络

循环神经网络(Recurrent Neural Network)是一种对序列数据建模的重要单元,模拟了离散时间(这里我们只考虑离散时间)动态系统的状态演化。“循环” 两字刻画了模型的核心:上一时刻的输出作为下一个时刻的输入,始终留在系统中如下面的图 1 所示,这种循环反馈能够形成复杂的历史。自然语言是一个天生的序列输入,RNN 恰好有能力去刻画词汇与词汇之间的前后关联关系,因此,在自然语言处理任务中占有重要的地位。

使用PaddleFluid和TensorFlow训练RNN语言模型

▲ 图1. 最简单的RNN单元

RNN 形成“循环反馈” 的过程是一个函数不断复合的过程,可以等价为一个层数等于输入序列长度的前馈神经网络,如果输入序列有 100 个时间步,相当于一个 100 层的前馈网络,梯度消失和梯度爆炸的问题对 RNN 尤为严峻。

直觉上大于 1 的数连乘越乘越大,极端时会引起梯度爆炸;小于 1 的数连乘越乘越小,极端时会引起梯度消失。梯度消失也会令在循环神经网络中,后面时间步的信息总是会”压过”前面时间步。如果 t 时刻隐层状态依赖于 t 之前所有时刻,梯度需要通过所有的中间隐层逐时间步回传,这会形成如图 2 所示的一个很深的求导链。

使用PaddleFluid和TensorFlow训练RNN语言模型

▲ 图2. t时刻依赖t时刻之前所有时刻

在许多实际问题中时间步之间相互依赖的链条并没有那么长,t 时刻也许仅仅依赖于它之前有限的若干时刻。很自然会联想到:如果模型能够自适应地学习出一些如图 3 所示的信息传播捷径来缩短梯度的传播路径,是不是可以一定程度减梯度消失和梯度爆炸呢?答案是肯定的,这也就是 LSTM 和 GRU 这类带有 “门控”思想的神经网络单元。

使用PaddleFluid和TensorFlow训练RNN语言模型

▲ 图3. 自适应地形成一些信息传播的“捷径”

关于 LSTM 更详细的介绍请参考文献 [2],这里不再赘述,只需了解 LSTM/GUR 这些门控循环神经网络单元提出的动机即可。

RNN LM

语言模型是 NLP 领域的基础任务之一。语言模型是计算一个序列的概率,判断一个序列是否属于一个语言的模型,描述了这样一个条件概率

使用PaddleFluid和TensorFlow训练RNN语言模型

,其中

使用PaddleFluid和TensorFlow训练RNN语言模型

是输入序列中的 T 个词语,用 one-hot 表示法表示。

言模型顾名思义是建模一种语言的模型,这一过程如图 4 所示:

使用PaddleFluid和TensorFlow训练RNN语言模型

▲ 图4. RNN语言模型

RNN LM的工作流程如下:

1. 给定一段 one-hot 表示的输入序列 {x1,x2,...,xT},将它们嵌入到实向量空间,得到词向量表示 :{ω1,ω2,...,ωt}。

2. 以词向量序列为输入,使用 RNN 模型(可以选择LSTM或者GRU),计算输入序列到 t 时刻的编码 ht。

3. softmax 层以 ht 为输入,预测下一个最可能的词

使用PaddleFluid和TensorFlow训练RNN语言模型

的概率

使用PaddleFluid和TensorFlow训练RNN语言模型

4.

使用PaddleFluid和TensorFlow训练RNN语言模型

,根据

使用PaddleFluid和TensorFlow训练RNN语言模型

使用PaddleFluid和TensorFlow训练RNN语言模型

计算误差信号。

PTB数据集介绍

至此,介绍完 RNN LM 模型的原理和基本结构,下面准备开始分别使用 PaddleFluid 和 TensorFlow 来构建我们的 训练任务。这里首先介绍这一篇我们使用 Mikolov 与处理过的 PTB 数据,这是语言模型任务中使用最为广泛的公开数据之一。 PTB 数据集包含 10000 个不同的词语(包含句子结束符 <eos> ,以及表示 低频词的特殊符号 <unk> )。

通过运行 data 目录下的 download.sh 下载数据,我们将使用其中的 ptb.train.txt 文件进行训练,文件中一行是一句话,文本中的低频词已经全部被替换为 <unk> 预处理时我们会在 每一行的末尾附加上句子结束符 <e> 。

程序结构

这一节我们首先整体总结一下使用 PaddleFluid 平台和 TensorFlow 运行自己的神经网络模型都有哪些事情需要完成。

PaddleFluid

1. 调用 PaddleFluid API 描述神经网络模型。PaddleFluid 中一个神经网络训练任务被称之为一段 Fluid Program 。

2. 定义 Fluid Program 执行设备: place 。常见的有 fluid.CUDAPlace(0) 和 fluid.CPUPlace()

place = fluid.CUDAPlace(0) if conf.use_gpu else fluid.CPUPlace()

注:PaddleFluid 支持混合设备运行,一些运算(operator)没有特定设备实现,或者为了提高全局资源利用率,可以为他们指定不同的计算设备。

3. 创建 PaddleFluid 执行器(Executor),需要为执行器指定运行设备。

exe = fluid.Executor(place)

让执行器执行 fluid.default_startup_program() ,初始化神经网络中的可学习参数,完成必要的初始化工作。

5. 定义 DataFeeder,编写 data reader,只需要关注如何返回一条训练/测试数据

6. 进入训练的双层循环(外层在 epoch 上循环,内层在 mini-batch 上循环),直到训练结束。

TensorFlow

1. 调用 TensorFlow API 描述神经网络模型。 TensorFlow 中一个神经网络模型是一个 Computation Graph。

2. 创建TensorFlow Session用来执行计算图。

sess = tf.Session()

3. 调用 sess.run(tf.global_variables_initializer()) 初始化神经网络中的可学习参数。

4. 编写返回每个 mini-batch 数据的数据读取脚本。

5. 进入训练的双层循环(外层在 epoch 上循环,内层在 mini-batch 上循环),直到训练结束。

如果不显示地指定使用何种设备进行训练,TensorFlow 会对机器硬件进行检测(是否有 GPU), 选择能够尽可能利用机器硬件资源的方式运行。

从以上的总结中可以看到,PaddleFluid 程序和 TensorFlow 程序的整体结构非常相似,使用经验可以非常容易的迁移。

构建网络结构及运行训练

加载训练数据

PaddleFluid

定义 输入data layers

PaddleFluid 模型通过 fluid.layers.data 来接收输入数据。图像分类网络以图片以及图片对应的类别标签作为网络的输入:

word = fluid.layers.data(

name="current_word", shape=[1], dtype="int64", lod_level=1)

lbl = fluid.layers.data(

name="next_word", shape=[1], dtype="int64", lod_level=1)

1. 定义 data layer 的核心是指定输入 Tensor 的形状( shape )和类型。

2. RNN LM 使用 one-hot 作为输入,一个词用一个和字典大小相同的向量表示,每一个位置对应了字典中的 一个词语。one-hot 向量仅有一个维度为 1, 其余全部为 0。因此为了节约存储空间,通常都直接用一个整型数表示给出词语在字典中的 id,而不是真的创建一个和词典同样大小的向量 ,因此在上面定义的 data layer 中 word 和 lbl 的形状都是 1,类型是 int64。

3. 需要特别说明的是,实际上 word 和 lbl 是两个 [batch_size x 1] 的向量,这里的 batch size 是指一个 mini-batch 中序列中的总词数。对序列学习任务, mini-batch 中每个序列长度 总是在发生变化,因此实际的 batch_size 只有在运行时才可以确定。 batch size 总是一个输入 Tensor 的第 0 维,在 PaddleFluid 中指定 data layer 的 shape 时,不需要指定 batch size的大小,也不需要考虑占位。框架会自动补充占位符,并且在运行时 设置正确的维度信息。因此,上面的两个 data layer 的 shape 都只需要设置第二个维度,也就是 1。

LoD Tensor和Non-Padding的序列输入

与前两篇文章中的任务相比,在上面的代码片段中定义 data layer 时,出现了一个新的 lod_level 字段,并设置为 1。这里就要介绍在 Fluid 系统中表示序列输入的一个重要概念 LoDTensor。

那么,什么是 LoD(Level-of-Detail) Tensor 呢?

1. Tensor 是 nn-dimensional arry 的推广,LoDTensor 是在 Tensor 基础上附加了序列信息。

2. Fluid 中输入、输出,网络中的可学习参数全部统一使用 LoDTensor(n-dimension array)表示,对非序列数据,LoD 信息为空。一个 mini-batch 输入数据是一个 LoDTensor。

3. 在 Fluid 中,RNN 处理变长序列无需 padding,得益于 LoDTensor表示

4. 可以简单将 LoD 理解为:std::vector<std::vector>。

下图是 LoDTensor 示意图(图片来自 Paddle 官方文档):

使用PaddleFluid和TensorFlow训练RNN语言模型

▲ 图5. LoD Tensor示意图

LoD 信息是附着在一个 Tensor 的第 0 维(也就是 batch size 对应的维度),来对一个 batch 中的数据进一步进行划分,表示了一个序列在整个 batch 中的起始位置。

LoD 信息可以嵌套,形成嵌套序列。例如,NLP 领域中的段落是一种天然的嵌套序列,段落是句子的序列,句子是词语的序列。

LoD 中的 level 就表示了序列信息的嵌套:

  • 图 (a) 的 LoD 信息 [0, 5, 8, 10, 14] :这个 batch 中共含有 4 条序列。
  • 图 (b) 的 LoD 信息 [[0, 5, 8, 10, 14] /*level=1*/, [0, 2, 3, 5, 7, 8, 10, 13, 14] /*level=2*/] :这个 batch 中含有嵌套的双层序列。

有了 LoDTensor 这样的数据表示方式,用户不需要对输入序列进行填充,框架会自动完成 RNN 的并行计算处理。

如何构造序列输入信息

明白了 LoD Tensor 的概念之后,另一个重要的问题是应该如何构造序列输入。在 PaddleFluid 中,通过 DataFeeder 模块来为网络中的 data layer 提供数据,调用方式如下面的代码所示:

train_reader = paddle.batch(

paddle.reader.shuffle(train_data, buf_size=51200),

batch_size=conf.batch_size)

place = fluid.CUDAPlace(0) if conf.use_gpu else fluid.CPUPlace()

feeder = fluid.DataFeeder(feed_list=[word, lbl], place=place)

观察以上代码,需要用户完成的仅有:编写一个实现读取一条数据的 python 函数: train_data 。 train_data 的代码非常简单,我们再来看一下它的具体实现 [3]:

def train_data(data_dir="data"):

data_path = os.path.join(data_dir, "ptb.train.txt")

_, word_to_id = build_vocab(data_path)

with open(data_path, "r") as ftrain:

for line in ftrain:

words = line.strip().split()

word_ids = [word_to_id[w] for w in words]

yield word_ids[0:-1], word_ids[1:]

在上面的代码中:

1. train_data 是一个 python generator ,函数名字可以任意指定,无需固定。

2. train_data 打开原始数据数据文件,读取一行(一行既是一条数据),返回一个 python list,这个 python list 既是序列中所有时间步。具体的数据组织方式如下表所示(其中,f 代表一个浮点数,i 代表一个整数):

使用PaddleFluid和TensorFlow训练RNN语言模型

3. paddle.batch() 接口用来构造 mini-batch 输入,会调用 train_data 将数据读入一个 pool 中,对 pool 中的数据进行 shuffle,然后依次返回每个 mini-batch 的数据。

TensorFlow

TensorFlow 中使用占位符 placeholder 接收 训练数据,可以认为其概念等价于 PaddleFluid 中的 data layer。同样的,我们定义了如下两个 placeholder 用于接收当前词与下一个词语:

def placeholders(self):

self._inputs = tf.placeholder(tf.int32,

[None, self.max_sequence_length])

self._targets = tf.placeholder(tf.int32, [None, self.vocab_size])

1. placeholder 只存储一个 mini-batch 的输入数据。与 PaddleFluid 中相同, _inputs 这里接收的是 one-hot 输入,也就是该词语在词典中的 index,one-hot 表示 会进一步通过此词向量层的作用转化为实值的词向量表示。

2. 需要注意的是,TensorFlow 模型中网络输入数据需要进行填充,保证一个 mini-batch 中序列长度 相等。也就是一个 mini-batch 中的数据长度都是 max_seq_length ,这一点与 PaddleFluid 非常不同。

通常做法 是对不等长序列进行填充,在这一篇示例中我们使用一种简化的做法,每条训练样本都按照 max_sequence_length 来切割,保证一个 mini-batch 中的序列是等长的。

于是, _input 的 shape=[batch_size, max_sequence_length] 。 max_sequence_length 即为 RNN 可以展开长度。

构建网络结构

PaddleFluid RNN LM

这里主要关注最核心的 LSTM 单元如何定义:

def __rnn(self, input):

for i in range(self.num_layers):

hidden = fluid.layers.fc(

size=self.hidden_dim * 4,

bias_attr=fluid.ParamAttr(

initializer=NormalInitializer(loc=0.0, scale=1.0)),

input=hidden if i else input)

lstm = fluid.layers.dynamic_lstm(

input=hidden,

size=self.hidden_dim * 4,

candidate_activation="tanh",

gate_activation="sigmoid",

cell_activation="sigmoid",

bias_attr=fluid.ParamAttr(

initializer=NormalInitializer(loc=0.0, scale=1.0)),

is_reverse=False)

return lstm

PaddleFluid 中的所有 RNN 单元(RNN/LSTM/GRU)都支持非填充序列作为输入,框架会自动完成不等长序列的并行处理。当需要堆叠多个 LSTM 作为输入时,只需利用 Python 的 for 循环语句,让一个 LSTM 的输出成为下一个 LSTM 的输入即可。在上面的代码片段中有一点需要特别注意:PaddleFluid 中的 LSTM 单元是由 fluid.layers.fc+ fluid.layers.dynamic_lstm 共同构成的。

使用PaddleFluid和TensorFlow训练RNN语言模型

▲ 图6. LSTM计算公式

图 6 是 LSTM 计算公式,图中用红色圈起来的计算是每一时刻输入矩阵流入三个门和 memory cell 的之前的映射。PaddleFluid 将这个四个矩阵运算合并为一个大矩阵一次性计算完毕, fluid.layers.dynamic_lstm 不包含这部分运算。因此:

1. PaddleFluid 中的 LSTM 单元是由 fluid.layers.fc + fluid.layers.dynamic_lstm 。

2. 假设 LSTM 单元的隐层大小是 128 维, fluid.layers.fc 和 fluid.layers.dynamic_lstm 的 size 都应该设置为 128 * 4,而不是 128。

TensorFlow RNN LM

这里主要关注最核心的 LSTM 单元如何定义:

def rnn(self):

def lstm_cell():

return tf.contrib.rnn.BasicLSTMCell(

self.hidden_dim, state_is_tuple=True)

cells = [lstm_cell() for _ in range(self.num_layers)]

cell = tf.contrib.rnn.MultiRNNCell(cells, state_is_tuple=True)

_inputs = self.input_embedding()

_outputs, _ = tf.nn.dynamic_rnn(

cell=cell, inputs=_inputs, dtype=tf.float32)

last = _outputs[:, -1, :]

logits = tf.layers.dense(inputs=last, units=self.vocab_size)

prediction = tf.nn.softmax(logits)

tf.nn.rnn_cell.BasicLSTMCell(n_hidden, state_is_tuple=True) : 是最基本的 LSTM 单元。 n_hidden 表示 LSTM 单元隐层大小。 state_is_tuple=True 表示返回的状态用一个元祖表示。

tf.contrib.rnn.MultiRNNCell : 用来 wrap 一组序列调用的 RNN 单元的 wrapper。

tf.nn.dynamic_rnn : 通过指定 mini-batch 中序列的长度,可以跳过 padding 部分的计算,减少计算量。这一篇的例子中由于我们对输入数据进行了处理,将它们都按照 max_sequence_length 切割。

但是, dynamic_rnn 可以让不同 mini-batch 的 batch size 长度不同,但同一次迭代一个 batch 内部的所有数据长度仍然是固定的。

运行训练

运行训练任务对两个平台都是常规流程,可以参考上文在程序结构一节介绍的流程,以及代码部分:PaddleFluid vs. TensorFlow,这里不再赘述。

总结

这一篇我们第一次接触 PaddleFluid 和 TensorFlow 平台的序列模型。了解 PaddleFluid 和 TensorFlow 在接受序列输入,序列处理策略上的不同。序列模型是神经网络模型中较为复杂的一类模型结构,可以衍生出非常复杂的模型结构。

不论是 PaddleFluid 以及 TensorFlow 都实现了多种不同的序列建模单元,如何选择使用这些不同的序列建模单元有很大的学问。到目前为止平台使用的一些其它重要主题:例如多线程多卡,如何利用混合设备计算等我们还尚未涉及。接下来的篇章将会继续深入 PaddleFluid 和 TensorFlow 平台的序列模型处理机制,以及更多重要功能如何在两个平台之间实现。

相关推荐