2017 年 2 月 16 日,Google 正式对外发布 Google TensorFlow 1.0 版本,并保证本次的发布版本 API 接口完全满足生产环境稳定性要求。这是 TensorFlow 的一个重要里程碑,标志着它可以正式在生产环境放心使用。在国内,从 InfoQ 的判断来看,TensorFlow 仍处于创新传播曲线的创新者使用阶段,大部分人对于 TensorFlow 还缺乏了解,社区也缺少帮助落地和使用的中文资料。InfoQ 期望通过深入浅出 TensorFlow 系列文章能够推动 Tensorflow 在国内的发展。欢迎加入 QQ 群(群号:183248479)深入讨论和交流。下面为本系列的前四篇文章:
深入浅出Tensorflow(一):深度学习及TensorFlow 简介
深入浅出TensorFlow(二):TensorFlow 解决MNIST 问题入门
深入浅出Tensorflow(三):训练神经网络模型的常用方法
循环神经网络(recurrent neural network,RNN)源自于1982 年由Saratha Sathasivam 提出的霍普菲尔德网络。霍普菲尔德网络因为实现困难,在其提出的时候并且没有被合适地应用。该网络结构也于1986 年后被全连接神经网络以及一些传统的机器学习算法所取代。然而,传统的机器学习算法非常依赖于人工提取的特征,使得基于传统机器学习的图像识别、语音识别以及自然语言处理等问题存在特征提取的瓶颈。而基于全连接神经网络的方法也存在参数太多、无法利用数据中时间序列信息等问题。随着更加有效的循环神经网络结构被不断提出,循环神经网络挖掘数据中的时序信息以及语义信息的深度表达能力被充分利用,并在语音识别、语言模型、机器翻译以及时序分析等方面实现了突破。
循环神经网络的主要用途是处理和预测序列数据。在之前介绍的全连接神经网络或卷积神经网络模型中,网络结构都是从输入层到隐含层再到输出层,层与层之间是全连接或部分连接的,但每层之间的节点是无连接的。考虑这样一个问题,如果要预测句子的下一个单词是什么,一般需要用到当前单词以及前面的单词,因为句子中前后单词并不是独立的。比如,当前单词是“很”,前一个单词是“天空”,那么下一个单词很大概率是“蓝”。循环神经网络的来源就是为了刻画一个序列当前的输出与之前信息的关系。从网络结构上,循环神经网络会记忆之前的信息,并利用之前的信息影响后面结点的输出。也就是说,循环神经网络的隐藏层之间的结点是有连接的,隐藏层的输入不仅包括输入层的输出,还包括上一时刻隐藏层的输出。
图1 展示了一个典型的循环神经网络。对于循环神经网络,一个非常重要的概念就是时刻。循环神经网络会对于每一个时刻的输入结合当前模型的状态给出一个输出。从图1 中可以看到,循环神经网络的主体结构A 的输入除了来自输入层Xt,还有一个循环的边来提供当前时刻的状态。在每一个时刻,循环神经网络的模块A 会读取t 时刻的输入Xt,并输出一个值ht。同时A 的状态会从当前步传递到下一步。因此,循环神经网络理论上可以被看作是同一神经网络结构被无限复制的结果。但出于优化的考虑,目前循环神经网络无法做到真正的无限循环,所以,现实中一般会将循环体展开,于是可以得到图2 所展示的结构。
图1 循环神经网络经典结构示意图
在图2 中可以更加清楚的看到循环神经网络在每一个时刻会有一个输入Xt,然后根据循环神经网络当前的状态At 提供一个输出Ht。从而神经网络当前状态At 是根据上一时刻的状态At-1 和当前输入Xt 共同决定的。从循环神经网络的结构特征可以很容易地得出它最擅长解决的问题是与时间序列相关的。循环神经网络也是处理这类问题时最自然的神经网络结构。对于一个序列数据,可以将这个序列上不同时刻的数据依次传入循环神经网络的输入层,而输出可以是对序列中下一个时刻的预测。循环神经网络要求每一个时刻都有一个输入,但是不一定每个时刻都需要有输出。在过去几年中,循环神经网络已经被广泛地应用在语音识别、语言模型、机器翻译以及时序分析等问题上,并取得了巨大的成功。
(点击放大图像)
图2 循环神经网络按时间展开后的结构
以机器翻译为例来介绍循环神经网络是如何解决实际问题的。循环神经网络中每一个时刻的输入为需要翻译的句子中的单词。如图3 所示,需要翻译的句子为ABCD,那么循环神经网络第一段每一个时刻的输入就分别是A、B、C 和D,然后用“”作为待翻译句子的结束符。在第一段中,循环神经网络没有输出。从结束符“”开始,循环神经网络进入翻译阶段。该阶段中每一个时刻的输入是上一个时刻的输出,而最终得到的输出就是句子ABCD 翻译的结果。从图8-3 中可以看到句子ABCD 对应的翻译结果就是XYZ,而Q 是代表翻译结束的结束符。
(点击放大图像)
如之前所介绍,循环神经网络可以被看做是同一神经网络结构在时间序列上被复制多次的结果,这个被复制多次的结构被称之为循环体。如何设计循环体的网络结构是循环神经网络解决实际问题的关键。和卷积神经网络过滤器中参数是共享的类似,在循环神经网络中,循环体网络结构中的参数在不同时刻也是共享的。
图4 展示了一个使用最简单的循环体结构的循环神经网络,在这个循环体中只使用了一个类似全连接层的神经网络结构。下面将通过图4 中所展示的神经网络来介绍循环神经网络前向传播的完整流程。循环神经网络中的状态是通过一个向量来表示的,这个向量的维度也称为循环神经网络隐藏层的大小,假设其为h。从图4 中可以看出,循环体中的神经网络的输入有两部分,一部分为上一时刻的状态,另一部分为当前时刻的输入样本。对于时间序列数据来说(比如不同时刻商品的销量),每一时刻的输入样例可以是当前时刻的数值(比如销量值);对于语言模型来说,输入样例可以是当前单词对应的单词向量(word embedding)。
(点击放大图像)
图4 使用单层全连接神经网络作为循环体的循环神经网络结构图(图中中间标有tanh 的小方框表示一个使用了tanh 作为激活函数的全连接神经网络)
长短时记忆网络(LTSM)结构
循环神经网络工作的关键点就是使用历史的信息来帮组当前的决策。例如使用之前出现的单词来加强对当前文字的理解。循环神经网络可以更好地利用传统神经网络结构所不能建模的信息,但同时,这也带来了更大的技术挑战——长期依赖(long-term dependencies)问题。
在有些问题中,模型仅仅需要短期内的信息来执行当前的任务。比如预测短语“大海的颜色是蓝色”中的最后一个单词“蓝色”时,模型并不需要记忆这个短语之前更长的上下文信息——因为这一句话已经包含了足够的信息来预测最后一个词。在这样的场景中,相关的信息和待预测的词的位置之间的间隔很小,循环神经网络可以比较容易地利用先前信息。
但同样也会有一些上下文场景更加复杂的情况。比如当模型试着去预测段落“某地开设了大量工厂,空气污染十分严重… 这里的天空都是灰色的”的最后一个单词时,仅仅根据短期依赖就无法很好的解决这种问题。因为只根据最后一小段,最后一个词可以是“蓝色的”或者“灰色的”。但如果模型需要预测清楚具体是什么颜色,就需要考虑先前提到但离当前位置较远的上下文信息。因此,当前预测位置和相关信息之间的文本间隔就有可能变得很大。当这个间隔不断增大时,类似图4 中给出的简单循环神经网络有可能会丧失学习到距离如此远的信息的能力。或者在复杂语言场景中,有用信息的间隔有大有小、长短不一,循环神经网络的性能也会受到限制。
长短时记忆网络(long short term memory, LSTM)的设计就是为了解决这个问题,而循环神经网络被成功应用的关键就是LSTM。在很多的任务上,采用LSTM 结构的循环神经网络比标准的循环神经网络表现更好。在下文中将重点介绍LSTM 结构。LSTM 结构是由Sepp Hochreiter 和Jürgen Schmidhuber 于1997 年提出的,它是一种特殊的循环体结构。如图5 所示,与单一tanh 循环体结构不同,LSTM 是一种拥有三个“门”结构的特殊网络结构。
(点击放大图像)
图5 LSTM 单元结构示意图
LSTM 靠一些“门”的结构让信息有选择性地影响每个时刻循环神经网络中的状态。所谓“门”的结构就是一个使用 sigmoid 神经网络和一个按位做乘法的操作,这两个操作合在一起就是一个“门”的结构。之所以该结构叫做“门”是因为使用 sigmoid 作为激活函数的全连接神经网络层会输出一个 0 到 1 之间的数值,描述当前输入有多少信息量可以通过这个结构。于是这个结构的功能就类似于一扇门,当门打开时(sigmoid 神经网络层输出为 1 时),全部信息都可以通过;当门关上时(sigmoid 神经网络层输出为 0 时),任何信息都无法通过。本节下面的篇幅将介绍每一个“门”是如何工作的。
为了使循环神经网更有效的保存长期记忆,图 5 中“遗忘门”和“输入门”至关重要,它们是 LSTM 结构的核心。“遗忘门”的作用是让循环神经网络“忘记”之前没有用的信息。比如一段文章中先介绍了某地原来是绿水蓝天,但后来被污染了。于是在看到被污染了之后,循环神经网络应该“忘记”之前绿水蓝天的状态。这个工作是通过“遗忘门”来完成的。“遗忘门”会根据当前的输入 xt、上一时刻状态 ct-1 和上一时刻输出 ht-1 共同决定哪一部分记忆需要被遗忘。在循环神经网络“忘记”了部分之前的状态后,它还需要从当前的输入补充最新的记忆。这个过程就是“输入门”完成的。如图 5 所示,“输入门”会根据 xt、ct-1 和 ht-1 决定哪些部分将进入当前时刻的状态 ct。比如当看到文章中提到环境被污染之后,模型需要将这个信息写入新的状态。通过“遗忘门”和“输入门”,LSTM 结构可以更加有效的决定哪些信息应该被遗忘,哪些信息应该得到保留。
LSTM 结构在计算得到新的状态 ct 后需要产生当前时刻的输出,这个过程是通过“输出门”完成的。“输出们”会根据最新的状态 ct、上一时刻的输出 ht-1 和当前的输入 xt 来决定该时刻的输出 ht。比如当前的状态为被污染,那么“天空的颜色”后面的单词很可能就是“灰色的”。
相比图 4 中展示的循环神经网络,使用 LSTM 结构的循环神经网络的前向传播是一个相对比较复杂的过程。具体 LSTM 每个“门”中的公式可以参考论文 Long short-term memory。在 TensorFlow 中,LSTM 结构可以被很简单地实现。以下代码展示了在 TensorFlow 中实现使用 LSTM 结构的循环神经网络的前向传播过程。
#定义一个 LSTM 结构。在 TensorFlow 中通过一句简单的命令就可以实现一个完整 LSTM 结构。 # LSTM 中使用的变量也会在该函数中自动被声明。 lstm = rnn_cell.BasicLSTMCell(lstm_hidden_size) # 将 LSTM 中的状态初始化为全 0 数组。和其他神经网络类似,在优化循环神经网络时,每次也 # 会使用一个 batch 的训练样本。以下代码中,batch_size 给出了一个 batch 的大小。 # BasicLSTMCell 类提供了 zero_state 函数来生成全领的初始状态。 state = lstm.zero_state(batch_size, tf.float32) # 定义损失函数。 loss = 0.0 # 在 8.1 节中介绍过,虽然理论上循环神经网络可以处理任意长度的序列,但是在训练时为了 # 避免梯度消散的问题,会规定一个最大的序列长度。在以下代码中,用 num_steps # 来表示这个长度。 for i in range(num_steps): # 在第一个时刻声明 LSTM 结构中使用的变量,在之后的时刻都需要复用之前定义好的变量。 if i > 0: tf.get_variable_scope().reuse_variables() # 每一步处理时间序列中的一个时刻。将当前输入(current_input)和前一时刻状态 # (state)传入定义的 LSTM 结构可以得到当前 LSTM 结构的输出 lstm_output 和更新后 # 的状态 state。 lstm_output, state = lstm(current_input, state) # 将当前时刻 LSTM 结构的输出传入一个全连接层得到最后的输出。 final_output = fully_connected(lstm_output) # 计算当前时刻输出的损失。 loss += calc_loss(final_output, expected_output)
通过上面这段代码看到,通过 TensorFlow 可以非常方便地实现使用 LSTM 结构的循环神经网络,而且并不需要用户对 LSTM 内部结构有深入的了解。
自然语言建模
简单地说,语言模型的目的是为了计算一个句子的出现概率。在这里把句子看成是单词的序列,于是语言模型需要计算的就是 p(w1,w2,w3,…,wn)。利用语言模型,可以确定哪个单词序列的可能性更大,或者给定若干个单词,可以预测下一个最可能出现的词语。举个音字转换的例子,假设输入的拼音串为“xianzaiquna”,它的输出可以是“西安在去哪”,也可以是“现在去哪”。根据语言常识,我们知道转换成第二个的概率更高。语言模型就可以告诉我们后者的概率大于前者,因此在大多数情况下转换成后者比较合理。
语言模型效果好坏的常用评价指标是复杂度(perplexity)。简单来说,perplexity 值刻画的就是通过某一个语言模型估计的一句话出现的概率。比如当已经知道(w1,w2,w3···wm)这句话出现在语料库之中,那么通过语言模型计算得到的这句话的概率越高越好,也就是 perplexity 值越小越好。计算 perplexity 值的公式如下:
(点击放大图像)
复杂度perplexity 表示的概念其实是平均分支系数(average branch factor),即模型预测下一个词时的平均可选择数量。例如,考虑一个由0~9 这10 个数字随机组成的长度为m 的序列。由于这10 个数字出现的概率是随机的,所以每个数字出现的概率是1/10。因此,在任意时刻,模型都有10 个等概率的候选答案可以选择,于是perplexity 就是10(有10 个合理的答案)。perplexity 的计算过程如下:
(点击放大图像)
因此,如果一个语言模型的perplexity 是89,就表示,平均情况下,模型预测下一个词时,有89 个词等可能地可以作为下一个词的合理选择。
PTB (Penn Treebank Dataset)文本数据集是语言模型学习中目前最被广泛使用数据集。本小节将在 PTB 数据集上使用循环神经网络实现语言模型。在给出语言模型代码之前将先简单介绍 PTB 数据集的格式以及 TensorFlow 对于 PTB 数据集的支持。首先,需要下载来源于 Tomas Mikolov 网站上的 PTB 数据。数据的下载地址为:
http://www.fit.vutbr.cz/~imikolov/rnnlm/simple-examples.tgz
将下载下来的文件解压之后可以得到如下文件夹列表
1-train/ 2-nbest-rescore/ 3-combination/ 4-data-generation/ 5-one-iter/ 6-recovery-during-training/ 7-dynamic-evaluation/ 8-direct/ 9-char-based-lm/ data/ models/ rnnlm-0.2b/
在本文中只需要关心 data 文件夹下的数据,对于其他文件不再一一介绍,感兴趣的读者可以自行参考 README 文件。在 data 文件夹下总共有 7 个文件,但本文中将只会用到以下三个文件:
ptb.test.txt #测试集数据文件 ptb.train.txt #训练集数据文件 ptb.valid.txt #验证集数据文件
这三个数据文件中的数据已经经过了预处理,包含了 10000 个不同的词语和语句结束标记符(在文本中就是换行符)以及标记稀有词语的特殊符号。下面展示了训练数据中的一行:
mr. <unk> is chairman of <unk> n.v. the dutch publishing group
为了让使用 PTB 数据集更加方便,TensorFlow 提供了两个函数来帮助实现数据的预处理。首先,TensorFlow 提供了 ptb_raw_data 函数来读取 PTB 的原始数据,并将原始数据中的单词转化为单词 ID。以下代码展示了如何使用这个函数。
from tensorflow.models.rnn.ptb import reader # 存放原始数据的路径。 DATA_PATH = "/path/to/ptb/data" train_data, valid_data, test_data, _ = reader.ptb_raw_data(DATA_PATH) # 读取数据原始数据。 print len(train_data) print train_data[:100] '''
运行以上程序可以得到输出:
929589 [9970, 9971, 9972, 9974, 9975, 9976, 9980, 9981, 9982, 9983, 9984, 9986, 9987, 9988, 9989, 9991, 9992, 9993, 9994, 9995, 9996, 9997, 9998, 9999, 2, 9256, 1, 3, 72, 393, 33, 2133, 0, 146, 19, 6, 9207, 276, 407, 3, 2, 23, 1, 13, 141, 4, 1, 5465, 0, 3081, 1596, 96, 2, 7682, 1, 3, 72, 393, 8, 337, 141, 4, 2477, 657, 2170, 955, 24, 521, 6, 9207, 276, 4, 39, 303, 438, 3684, 2, 6, 942, 4, 3150, 496, 263, 5, 138, 6092, 4241, 6036, 30, 988, 6, 241, 760, 4, 1015, 2786, 211, 6, 96, 4] '''
从输出中可以看出训练数据中总共包含了 929589 个单词,而这些单词被组成了一个非常长的序列。这个序列通过特殊的标识符给出了每句话结束的位置。在这个数据集中,句子结束的标识符 ID 为 2。
虽然循环神经网络可以接受任意长度的序列,但是在训练时需要将序列按照某个固定的长度来截断。为了实现截断并将数据组织成 batch,TensorFlow 提供了 ptb_iterator 函数。以下代码展示了如何使用 ptb_iterator 函数。
from tensorflow.models.rnn.ptb import reader # 类似地读取数据原始数据。 DATA_PATH = "/path/to/ptb/data" train_data, valid_data, test_data, _ = reader.ptb_raw_data(DATA_PATH) # 将训练数据组织成 batch 大小为 4、截断长度为 5 的数据组。 result = reader.ptb_iterator(train_data, 4, 5) # 读取第一个 batch 中的数据,其中包括每个时刻的输入和对应的正确输出。 x, y = result.next() print "X:", x print "y:", y '''
运行以上程序可以得到输出:
X: [[9970 9971 9972 9974 9975] [ 332 7147 328 1452 8595] [1969 0 98 89 2254] [ 3 3 2 14 24]] y: [[9971 9972 9974 9975 9976] [7147 328 1452 8595 59] [ 0 98 89 2254 0] [ 3 2 14 24 198]] '''
图 6 展示了 ptb_iterator 函数实现的功能。ptb_iterator 函数会将一个长序列划分为 batch_size 段,其中 batch_size 为一个 batch 的大小。每次调用 ptb_iterator 时,该函数会从每一段中读取长度为 num_step 的子序列,其中 num_step 为截断的长度。从上面代码的输出可以看到,在第一个 batch 的第一行中,前面 5 个单词的 ID 和整个训练数据中前 5 个单词的 ID 是对应的。ptb_iterator 在生成 batch 时可以会自动生成每个 batch 对应的正确答案,这个对于每一个单词,它对应的正确答案就是该单词的后面一个单词。
(点击放大图像)
图 6 将一个长序列分成 batch 并截断的操作示意图
在介绍了语言模型的理论和使用到的数据集之后,下面给出了一个完成的 TensorFlow 样例程序来通过循环神经网络实现语言模型。
# -*- coding: utf-8 -*- import numpy as np import tensorflow as tf from tensorflow.models.rnn.ptb import reader DATA_PATH = "/path/to/ptb/data" # 数据存放的路径。 HIDDEN_SIZE = 200 # 隐藏层规模。 NUM_LAYERS = 2 # 深层循环神经网络中 LSTM 结构的层数。 VOCAB_SIZE = 10000 # 词典规模,加上语句结束标识符和稀有 # 单词标识符总共一万个单词。 LEARNING_RATE = 1.0 # 学习速率。 TRAIN_BATCH_SIZE = 20 # 训练数据 batch 的大小。 TRAIN_NUM_STEP = 35 # 训练数据截断长度。 # 在测试时不需要使用截断,所以可以将测试数据看成一个超长的序列。 EVAL_BATCH_SIZE = 1 # 测试数据 batch 的大小。 EVAL_NUM_STEP = 1 # 测试数据截断长度。 NUM_EPOCH = 2 # 使用训练数据的轮数。 KEEP_PROB = 0.5 # 节点不被 dropout 的概率。 MAX_GRAD_NORM = 5 # 用于控制梯度膨胀的参数。 # 通过一个 PTBModel 类来描述模型,这样方便维护循环神经网络中的状态。 class PTBModel(object): def __init__(self, is_training, batch_size, num_steps): # 记录使用的 batch 大小和截断长度。 self.batch_size = batch_size self.num_steps = num_steps # 定义输入层。可以看到输入层的维度为 batch_size × num_steps,这和 # ptb_iterator 函数输出的训练数据 batch 是一致的。 self.input_data = tf.placeholder(tf.int32, [batch_size, num_steps]) # 定义预期输出。它的维度和 ptb_iterator 函数输出的正确答案维度也是一样的。 self.targets = tf.placeholder(tf.int32, [batch_size, num_steps]) # 定义使用 LSTM 结构为循环体结构且使用 dropout 的深层循环神经网络。 lstm_cell = tf.nn.rnn_cell.BasicLSTMCell(HIDDEN_SIZE) if is_training : lstm_cell = tf.nn.rnn_cell.DropoutWrapper( lstm_cell, output_keep_prob=KEEP_PROB) cell = tf.nn.rnn_cell.MultiRNNCell([lstm_cell] * NUM_LAYERS) # 初始化最初的状态,也就是全零的向量。 self.initial_state = cell.zero_state(batch_size, tf.float32) # 将单词 ID 转换成为单词向量。因为总共有 VOCAB_SIZE 个单词,每个单词向量的维度 # 为 HIDDEN_SIZE,所以 embedding 参数的维度为 VOCAB_SIZE × HIDDEN_SIZE。 embedding = tf.get_variable("embedding", [VOCAB_SIZE, HIDDEN_SIZE]) # 将原本 batch_size × num_steps 个单词 ID 转化为单词向量,转化后的输入层维度 # 为 batch_size × num_steps × HIDDEN_SIZE。 inputs = tf.nn.embedding_lookup(embedding, self.input_data) # 只在训练时使用 dropout。 if is_training: inputs = tf.nn.dropout(inputs, KEEP_PROB) # 定义输出列表。在这里先将不同时刻 LSTM 结构的输出收集起来,再通过一个全连接 # 层得到最终的输出。 outputs = [] # state 存储不同 batch 中 LSTM 的状态,将其初始化为 0。 state = self.initial_state with tf.variable_scope("RNN"): for time_step in range(num_steps): if time_step > 0: tf.get_variable_scope().reuse_variables() # 从输入数据中获取当前时刻获的输入并传入 LSTM 结构。 cell_output, state = cell(inputs[:, time_step, :], state) # 将当前输出加入输出队列。 outputs.append(cell_output) # 把输出队列展开成 [batch, hidden_size*num_steps] 的形状,然后再 # reshape 成 [batch*numsteps, hidden_size] 的形状。 output = tf.reshape(tf.concat(1, outputs), [-1, HIDDEN_SIZE]) # 将从 LSTM 中得到的输出再经过一个全链接层得到最后的预测结果,最终的预测结果在 # 每一个时刻上都是一个长度为 VOCAB_SIZE 的数组,经过 softmax 层之后表示下一个 # 位置是不同单词的概率。 weight = tf.get_variable("weight", [HIDDEN_SIZE, VOCAB_SIZE]) bias = tf.get_variable("bias", [VOCAB_SIZE]) logits = tf.matmul(output, weight) + bias # 定义交叉熵损失函数。TensorFlow 提供了 sequence_loss_by_example 函数来计 # 算一个序列的交叉熵的和。 loss = tf.nn.seq2seq.sequence_loss_by_example( [logits], # 预测的结果。 [tf.reshape(self.targets, [-1])], # 期待的正确答案,这里将 # [batch_size, num_steps] # 二维数组压缩成一维数组。 # 损失的权重。在这里所有的权重都为 1,也就是说不同 batch 和不同时刻 # 的重要程度是一样的。 [tf.ones([batch_size * num_steps], dtype=tf.float32)]) # 计算得到每个 batch 的平均损失。 self.cost = tf.reduce_sum(loss) / batch_size self.final_state = state # 只在训练模型时定义反向传播操作。 if not is_training: return trainable_variables = tf.trainable_variables() # 通过 clip_by_global_norm 函数控制梯度的大小,避免梯度膨胀的问题。 grads, _ = tf.clip_by_global_norm( tf.gradients(self.cost, trainable_variables), MAX_GRAD_NORM) # 定义优化方法。 optimizer = tf.train.GradientDescentOptimizer(LEARNING_RATE) # 定义训练步骤。 self.train_op = optimizer.apply_gradients( zip(grads, trainable_variables)) # 使用给定的模型 model 在数据 data 上运行 train_op 并返回在全部数据上的 perplexity 值。 def run_epoch(session, model, data, train_op, output_log): # 计算 perplexity 的辅助变量。 total_costs = 0.0 iters = 0 state = session.run(model.initial_state) # 使用当前数据训练或者测试模型。 for step, (x, y) in enumerate( reader.ptb_iterator(data, model.batch_size, model.num_steps)): # 在当前 batch 上运行 train_op 并计算损失值。交叉熵损失函数计算的就是下一个单 # 词为给定单词的概率。 cost, state, _ = session.run( [model.cost, model.final_state, train_op], {model.input_data: x, model.targets: y, model.initial_state: state}) # 将不同时刻、不同 batch 的概率加起来就可以得到第二个 perplexity 公式等号右 # 边的部分,再将这个和做指数运算就可以得到 perplexity 值。 total_costs += cost iters += model.num_steps # 只有在训练时输出日志。 if output_log and step % 100 == 0: print("After %d steps, perplexity is %.3f" % ( step, np.exp(total_costs / iters))) # 返回给定模型在给定数据上的 perplexity 值。 return np.exp(total_costs / iters) def main(_): # 获取原始数据。 train_data, valid_data, test_data, _ = reader.ptb_raw_data(DATA_PATH) # 定义初始化函数。 initializer = tf.random_uniform_initializer(-0.05, 0.05) # 定义训练用的循环神经网络模型。 with tf.variable_scope("language_model", reuse=None, initializer=initializer): train_model = PTBModel(True, TRAIN_BATCH_SIZE, TRAIN_NUM_STEP) # 定义评测用的循环神经网络模型。 with tf.variable_scope("language_model", reuse=True, initializer=initializer): eval_model = PTBModel(False, EVAL_BATCH_SIZE, EVAL_NUM_STEP) with tf.Session() as session: tf.initialize_all_variables().run() # 使用训练数据训练模型。 for i in range(NUM_EPOCH): print("In iteration: %d" % (i + 1)) # 在所有训练数据上训练循环神经网络模型。 run_epoch(session, train_model, train_data, train_model.train_op, True) # 使用验证数据评测模型效果。 valid_perplexity = run_epoch( session, eval_model, valid_data, tf.no_op(), False) print("Epoch: %d Validation Perplexity: %.3f" % ( i + 1, valid_perplexity)) # 最后使用测试数据测试模型效果。 test_perplexity = run_epoch( session, eval_model, test_data, tf.no_op(), False) print("Test Perplexity: %.3f" % test_perplexity) if __name__ == "__main__": tf.app.run()
运行以上程序可以得到类似如下的输出:
In iteration: 1 After 0 steps, perplexity is 10003.783 After 100 steps, perplexity is 1404.742 After 200 steps, perplexity is 1061.458 After 300 steps, perplexity is 891.044 After 400 steps, perplexity is 782.037 … After 1100 steps, perplexity is 228.711 After 1200 steps, perplexity is 226.093 After 1300 steps, perplexity is 223.214 Epoch: 2 Validation Perplexity: 183.443 Test Perplexity: 179.420
从输出可以看出,在迭代开始时 perplexity 值为 10003.783,这基本相当于从一万个单词中随机选择下一个单词。而在训练结束后,在训练数据上的 perplexity 值降低到了 179.420。这表明通过训练过程,将选择下一个单词的范围从一万个减小到了大约 180 个。通过调整 LSTM 隐藏层的节点个数和大小以及训练迭代的轮数还可以将 perplexity 值降到更低。
本文内容来自作者图书作品《TensorFlow: 实战 Google 深度学习框架》,点击购买。
作者介绍
郑泽宇,才云首席大数据科学家,前谷歌高级工程师。从 2013 年加入谷歌至今,郑泽宇作为主要技术人员参与并领导了多个大数据项目,拥有丰富机器学习、数据挖掘工业界及科研项目经验。2014 年,他提出产品聚类项目用于衔接谷歌购物和谷歌知识图谱(Knowledge Graph)数据,使得知识卡片形式的广告逐步取代传统的产品列表广告,开启了谷歌购物广告在搜索页面投递的新纪元。他于 2013 年 5 月获得美国 Carnegie Mellon University(CMU)大学计算机硕士学位, 期间在顶级国际学术会议上发表数篇学术论文,并获得西贝尔奖学金。
评论