08.循环神经网络------《Tensorflow实战Google深度学习框架》笔记

循环神经网络简介

循环神经网络的主要用途是处理和预测序列数据。在之前介绍的全连接神经网络或卷积神经网络模型中,网络结构都是从输入层到隐含层再到输出层,层与层之间是全连接或部分连接的,但每层之间的节点是无连接的。考虑这样一个问题,如果要预测句子的下一个单词是什么,一般需要用到当前单词以及前面的单词,因为句子中前后单词并不是独立的。比如,当前单词是“很”,前一个单词是“天空”,那么下一个单词很大概率是“蓝”。循环神经网络的来源就是为了刻画一个序列当前的输出与之前信息的关系。从网络结构上,循环神经网络会记忆之前的信息,并利用之前的信息影响后面结点的输出。也就是说,循环神经网络的隐藏层之间的结点是有连接的,隐藏层的输入不仅包括输入层的输出,还包括上一时刻隐藏层的输出。

图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的主要封装有4个:

第一个是TensorFlow-Slim;

第二个是tf.contrib.learn(之前也被称为skflow);

第三个是TFLearn;

最后一个是Keras。

本文将大致介绍这几种不同的高层封装的使用方法,并通过其中常用的三种方式在MNIST数据集上实现卷积神经网络。

TensorFlow-Slim

TensorFlow-Slim是一个相对轻量级的TensorFlow高层封装。通过TensorFlow-Slim,定义网络结构的代码可以得到很大程度的简化,使得整个代码更加可读。下面的代码对比了使用原生态TensorFlow实现卷积层和使用TensorFlow-Slim实现卷积层的代码:

# 直接使用TensorFlow原生态API实现卷积层。
with tf.variable_scope(scope_name):
    weights = tf.get_variable("weight", …)
    biases = tf.get_variable("bias", …) 
    conv = tf.nn.conv2d(…)
relu = tf.nn.relu(tf.nn.bias_add(conv, biases))

# 使用TensorFlow-Slim实现卷积层。通过TensorFlow-Slim可以在一行中实现一个卷积层的
# 前向传播算法。slim.conv2d函数的有3个参数是必填的。第一个参数为输入节点矩阵,第二参数
# 是当前卷积层过滤器的深度,第三个参数是过滤器的尺寸。可选的参数有过滤器移动的步长、
# 是否使用全0填充、激活函数的选择以及变量的命名空间等。
net = slim.conv2d(input, 32, [3, 3])

从上面的代码可以看出,使用TensorFlow-Slim可以大幅减少代码量。省去很多与网络结构无关的变量声明的代码。虽然TensorFlow-Slim可以起到简化代码的作用,但是在实际应用中,使用TensorFlow-Slim定义网络结构的情况相对较少,因为它既不如原生态TensorFlow的灵活,也不如下面将要介绍的其他高层封装简洁。但除了简化定义神经网络结构的代码量,使用TensorFlow-Slim的一个最大好处就是它直接实现了一些经典的卷积神经网络,并且Google提供了这些神经网络在ImageNet上训练好的模型。下表总结了通过TensorFlow-Slim可以直接实现的神经网络模型:

Google提供的训练好的模型可以在github上tensorflow/models/slim目录下找到。在该目录下也提供了迁移学习的案例和代码。

tf.contrib.learn

tf.contrib.learn是TensorFlow官方提供的另外一个对TensorFlow的高层封装,通过这个封装,用户可以和使用sklearn类似的方法使用TensorFlow。通过tf.contrib.learn训练模型时,需要使用一个Estimator对象。Estimator对象是tf.contrib.learn 进行模型训练(train/fit)和模型评估(evaluation)的入口。

tf.contrib.learn模型提供了一些预定义的 Estimator,例如线性回归(tf.contrib.learn.LinearRegressor)、逻辑回归(tf.contrib.learn.LogisticRegressor)、线性分类(tf.contrib.learn.LinearClassifier)以及一些完全由全连接层构成的深度神经网络回归或者分类模型(tf.contrib.learn.DNNClassifier、tf.contrib.learn.DNNRegressor)。

除了可以使用预先定义好的模型,tf.contrib.learn也支持自定义模型,下面的代码给出了使用tf.contrib.learn在MNIST数据集上实现卷积神经网络的过程。更多关于tf.contrib.learn的介绍可以参考Google官方文档

import tensorflow as tf
from sklearn import metrics
# 使用tf.contrib.layers中定义好的卷积神经网络结构可以更方便的实现卷积层。
layers = tf.contrib.layers
learn = tf.contrib.learn
# 自定义模型结构。这个函数有三个参数,第一个给出了输入的特征向量,第二个给出了
# 该输入对应的正确输出,最后一个给出了当前数据是训练还是测试。该函数的返回也有
# 三个指,第一个为定义的神经网络结构得到的最终输出节点取值,第二个为损失函数,第
# 三个为训练神经网络的操作。
def conv_model(input, target, mode):
     # 将正确答案转化成需要的格式。
    target = tf.one_hot(tf.cast(target, tf.int32), 10, 1, 0)
    # 定义神经网络结构,首先需要将输入转化为一个三维举证,其中第一维表示一个batch中的
    # 样例数量。
    network = tf.reshape(input, [-1, 28, 28, 1])
    # 通过tf.contrib.layers来定义过滤器大小为5*5的卷积层。
    network = layers.convolution2d(network, 32, kernel_size=[5, 5], activation_fn=tf.nn.relu)
    # 实现过滤器大小为2*2,长和宽上的步长都为2的最大池化层。
    network = tf.nn.max_pool(network, ksize=[1, 2, 2, 1], strides=[1, 2, 2, 1], padding='SAME')
    # 类似的定义其他的网络层结构。
    network = layers.convolution2d(network, 64, kernel_size=[5, 5], activation_fn=tf.nn.relu)
    network = tf.nn.max_pool(network, ksize=[1, 2, 2, 1], strides=[1, 2, 2, 1], padding='SAME')
    # 将卷积层得到的矩阵拉直成一个向量,方便后面全连接层的处理。
    network = tf.reshape(network, [-1, 7 * 7 * 64])
    # 加入dropout。注意dropout只在训练时使用。
    network = layers.dropout(
        layers.fully_connected(network, 500, activation_fn=tf.nn.relu),
        keep_prob=0.5,
        is_training=(mode == tf.contrib.learn.ModeKeys.TRAIN))
    # 定义最后的全连接层。
    logits = layers.fully_connected(network, 10, activation_fn=None)
    # 定义损失函数。
    loss = tf.losses.softmax_cross_entropy(target, logits)
    # 定义优化函数和训练步骤。
    train_op = layers.optimize_loss(
        loss,
              tf.contrib.framework.get_global_step(),
        optimizer='SGD',
        learning_rate=0.01)
    return tf.argmax(logits, 1), loss, train_op
# 加载数据。
mnist = learn.datasets.load_dataset('mnist')
# 定义神经网络结构,并在训练数据上训练神经网络。
classifier = learn.Estimator(model_fn=conv_model)
classifier.fit(mnist.train.images, mnist.train.labels, batch_size=100, steps=20000)
# 在测试数据上计算模型准确率。
score = metrics.accuracy_score(mnist.test.labels, list(classifier.predict(mnist.test.images)))
print('Accuracy: {0:f}'.format(score))
'''
运行上面的程序,可以得到类似如下的
Accuracy: 0.9901
'''

TFLearn

TensorFlow的另外一个高层封装TFLearn进一步简化了tf.contrib.learn中对模型定义的方法,并提供了一些更加简洁的方法来定义神经网络的结构。和上面两个高层封装不一样,使用TFLearn需要单独安装,安装的方法为:

pip install tflearn

下面的代码介绍了如何通过TFLearn来实现卷积神经网络。更多关于TFLearn的用法介绍可以参考TFLearn的官方网站(http://tflearn.org/

import tflearn
from tflearn.layers.core import input_data, dropout, fully_connected
from tflearn.layers.conv import conv_2d, max_pool_2d
from tflearn.layers.estimator import regression

import tflearn.datasets.mnist as mnist

# 读取MNIST数据。
trainX, trainY, testX, testY = mnist.load_data(one_hot=True)
# 将图像数据resize成卷积卷积神经网络输入的格式。
trainX = trainX.reshape([-1, 28, 28, 1])
testX = testX.reshape([-1, 28, 28, 1])

# 构建神经网络。input_data定义了一个placeholder来接入输入数据。
network = input_data(shape=[None, 28, 28, 1], name='input')
# 定义一个深度为5,过滤器为5*5的卷积层。从这个函数可以看出,它比tf.contrib.learn
# 中对卷积层的抽象要更加简洁。
network = conv_2d(network, 32, 5, activation='relu')
# 定义一个过滤器为2*2的最大池化层。
network = max_pool_2d(network, 2)
# 类似的定义其他的网络结构。
network = conv_2d(network, 64, 5, activation='relu')
network = max_pool_2d(network, 2)
network = fully_connected(network, 500, activation='relu', regularizer="L2")
network = dropout(network, 0.5)
network = fully_connected(network, 10, activation='softmax', regularizer="L2")
# 定义学习任务。指定优化器为sgd,学习率为0.01,损失函数为交叉熵。
network = regression(network, optimizer='sgd', learning_rate=0.01,
                                   loss='categorical_crossentropy', name='target')

# 通过定义的网络结构训练模型,并在指定的验证数据上验证模型的效果。
model = tflearn.DNN(network, tensorboard_verbose=0)
model.fit(trainX, trainY, n_epoch=20, validation_set=([testX, testY]), show_metric=True)

'''

运行上面的代码,可以得到类似如下的输出:

Run id: UY9GEP
Log directory: /tmp/tflearn_logs/
---------------------------------
Training samples: 55000
Validation samples: 10000
--
Training Step: 860  | total loss: 0.25554 | time: 493.917s
| SGD | epoch: 001 | loss: 0.25554 - acc: 0.9267 | 
val_loss: 0.24617 - val_acc: 0.9267 -- iter: 55000/55000
--
Training Step: 1054  | total loss: 0.28228 | time: 110.039s
| SGD | epoch: 002 | loss: 0.28228 - acc: 0.9207 -- iter: 12416/55000
'''

Keras

Keras是一个基于TensorFlow或者Theano的高层API,在安装好TensorFlow之后可以通过以下命令可以安装:

下面的代码介绍了如何通过Keras来实现卷积神经网络。更多关于Keras的用法介绍可以参考Keras的官方网站(http://tflearn.org/

import keras
from keras.datasets import mnist
from keras.models import Sequential
from keras.layers import Dense, Dropout, Flatten
from keras.layers import Conv2D, MaxPooling2D
from keras import backend as K

batch_size = 128
num_classes = 10
epochs = 20
img_rows, img_cols = 28, 28

# 加载MNIST数据。
(trainX, trainY), (testX, testY) = mnist.load_data()

# 根据系统要求设置输入层的格式。
if K.image_data_format() == 'channels_first':
    trainX = trainX.reshape(trainX.shape[0], 1, img_rows, img_cols)
    testX = testX.reshape(testX.shape[0], 1, img_rows, img_cols)
    input_shape = (1, img_rows, img_cols)
else:
    trainX = trainX.reshape(trainX.shape[0], img_rows, img_cols, 1)
    testX = testX.reshape(testX.shape[0], img_rows, img_cols, 1)
    input_shape = (img_rows, img_cols, 1)

# 将图像像素转化为0到1之间的实数。
trainX = trainX.astype('float32')
testX = testX.astype('float32')
trainX /= 255.0
testX /= 255.0

# 将标准答案转化为需要的格式。
trainY = keras.utils.to_categorical(trainY, num_classes)
testY = keras.utils.to_categorical(testY, num_classes)

# 定义模型。
model = Sequential()
# 一层深度为32,过滤器大小为5*5的卷积层。
model.add(Conv2D(32, kernel_size=(5, 5), activation='relu', input_shape=input_shape))
# 一层过滤器大小为2*2的最大池化层。
model.add(MaxPooling2D(pool_size=(2, 2)))
# 一层深度为64,过滤器大小为5*5的卷积层。
model.add(Conv2D(64, (5, 5), activation='relu'))
# 一层过滤器大小为2*2的最大池化层。
model.add(MaxPooling2D(pool_size=(2, 2)))
# 将上层最大池化层的输出在dropout之后提供给全连接层。
model.add(Dropout(0.5))
# 将卷积层的输出拉直后作为下面全连接层的输入。
model.add(Flatten())
# 全连接层,有500个节点。
model.add(Dense(500, activation='relu'))
# 全连接层,得到最后的输出。
model.add(Dense(num_classes, activation='softmax'))

# 定义损失函数、优化函数和评测方法。
model.compile(loss=keras.losses.categorical_crossentropy,
                         optimizer=keras.optimizers.SGD(),
                         metrics=['accuracy'])

# 类似TFLearn中的训练过程,给出训练数据、batch大小、训练轮数和验证数据,
# Keras可以自动完成模型训练过程。
model.fit(trainX, trainY,
          batch_size=batch_size,
          epochs=epochs,
          verbose=1,
          validation_data=(testX, testY))

# 在测试数据上计算准确率。
score = model.evaluate(testX, testY, verbose=0)
print('Test loss:', score[0])
print('Test accuracy:', score[1])

'''

运行上面的代码,可以得到类似如下的输出:

60000/60000 [==============================] 
- 255s - loss: 1.3127 - acc: 0.5943 - val_loss: 0.3409 - val_acc: 0.9046
Epoch 2/20
60000/60000 [==============================] 
- 219s - loss: 0.3827 - acc: 0.8840 - val_loss: 0.2006 - val_acc: 0.9433
Epoch 3/20
35072/60000 [================>.............] 
- ETA: 82s - loss: 0.2752 - acc: 0.9163 
'''

猜你喜欢

转载自blog.csdn.net/qq_25973779/article/details/80180886