TensorFlow构建循环神经网络

原创 2017年08月29日 16:06:04

前言

前面在《循环神经网络》文章中已经介绍了深度学习的循环神经网络模型及其原理,接下去这篇文章将尝试使用TensorFlow来实现一个循环神经网络,该例子能通过训练给定的语料生成模型并实现对字符的预测。这里选择使用最原始的循环神经网络RNN模型。

语料库的准备

这里就简单用纪伯伦的《On Friendship》作为语料吧。

RNN简要说明

用下面两张图简要说明下,RNN模型有多个时刻的输入,从第一个图中看到输入x、隐层s和输出o都与时刻t有关,可以看到上一时刻的隐含层会影响到当前时刻的输出,这也就使循环神经网络具有记忆功能。

为了更加清晰理解,看第二个图,从神经元来看RNN,输入层有3个神经元,隐含层有4个神经元,输出层有2个神经元,保留当前时刻状态参与下一时刻计算。

创建词汇

def create_vocab(text):
    unique_chars = list(set(text))
    print(unique_chars)
    vocab_size = len(unique_chars)
    vocab_index_dict = {}
    index_vocab_dict = {}
    for i, char in enumerate(unique_chars):
        vocab_index_dict[char] = i
        index_vocab_dict[i] = char
    return vocab_index_dict, index_vocab_dict, vocab_size

处理字符首先就是需要创建包含语料中所有的词的词汇,需要一个从字符到词汇位置索引的词典,也需要一个从位置索引到字符的词典。

词汇保存及读取

def save_vocab(vocab_index_dict, vocab_file):
    with codecs.open(vocab_file, 'w', encoding='utf-8') as f:
        json.dump(vocab_index_dict, f, indent=2, sort_keys=True)

def load_vocab(vocab_file):
    with codecs.open(vocab_file, 'r', encoding='utf-8') as f:
        vocab_index_dict = json.load(f)
    index_vocab_dict = {}
    vocab_size = 0
    for char, index in iteritems(vocab_index_dict):
        index_vocab_dict[index] = char
        vocab_size += 1
    return vocab_index_dict, index_vocab_dict, vocab_size

第一次创建词汇后我们需要将它保存下来,后面在使用模型预测时需要读取该词汇,如果不保存而每次都创建的话则可能导致词汇顺序不同。

批量生成器

class BatchGenerator(object):
    def __init__(self, text, batch_size, seq_length, vocab_size, vocab_index_dict):
        self._text = text
        self._text_size = len(text)
        self._batch_size = batch_size
        self.vocab_size = vocab_size
        self.seq_length = seq_length
        self.vocab_index_dict = vocab_index_dict

        segment = self._text_size // batch_size
        self._cursor = [offset * segment for offset in range(batch_size)]
        self._last_batch = self._next_batch()

    def _next_batch(self):
        batch = np.zeros(shape=(self._batch_size), dtype=np.float)
        for b in range(self._batch_size):
            batch[b] = self.vocab_index_dict[self._text[self._cursor[b]]]
            self._cursor[b] = (self._cursor[b] + 1) % self._text_size
        return batch

    def next(self):
        batches = [self._last_batch]
        for step in range(self.seq_length):
            batches.append(self._next_batch())
        self._last_batch = batches[-1]
        return batches

创建一个批量生成器用于将文本生成批量的训练样本,其中text为整个语料,batch_size为批大小,vocab_size为词汇大小,seq_length为序列长度,vocab_index_dict为词汇索引词典。生成器的生成结构大致如下图,按文本顺序竖着填进矩阵,而矩阵的列大小为batch_size。

这里写图片描述

构建图

cell_fn = tf.contrib.rnn.BasicRNNCell
cell = cell_fn(hidden_size)
cells = [cell]
for i in range(rnn_layers - 1):
    higher_layer_cell = cell_fn(hidden_size)
    cells.append(higher_layer_cell)
multi_cell = tf.contrib.rnn.MultiRNNCell(cells)
self.zero_state = multi_cell.zero_state(self.batch_size, tf.float32)

这里我们仅仅使用基础的RNN,所以直接实例化一个BasicRNNCell对象,需要指定隐含层的神经元数hidden_size,另外因为单层的RNN学习能力有限,这里可以设置网络的层数rnn_layers来增强神经网络的学习能力,最终用MultiRNNCell封装起来。

接着需要给我们的multi cell进行初始化状态,初始状态全设为0,即用multi_cell.zero_state来实现。这里需要传入一个batch_size参数,它会生成rnn_layers层的(batch_size ,hidden_size)个初始状态。

self.initial_state = create_tuple_placeholders_with_default(multi_cell.zero_state(self.batch_size, tf.float32), shape=multi_cell.state_size)
self.input_data = tf.placeholder(tf.int64, [self.batch_size, self.seq_length], name='inputs')
self.targets = tf.placeholder(tf.int64, [self.batch_size, self.seq_length], name='targets')

def create_tuple_placeholders_with_default(inputs, shape):
    if isinstance(shape, int):
        result = tf.placeholder_with_default(
            inputs, list((None,)) + [shape])
    else:
        subplaceholders = [create_tuple_placeholders_with_default(
            subinputs, subshape)
            for subinputs, subshape in zip(inputs, shape)]
        t = type(shape)
        if t == tuple:
            result = t(subplaceholders)
        else:
            result = t(*subplaceholders)
    return result

接着我们开始创建占位符,有三个占位符需要创建,分别为初始状态占位符、输入占位符和target占位符。首先看初始状态占位符,这个主要是根据multi_cell.zero_state(self.batch_size, tf.float32)的结构使用tf.placeholder_with_default创建。其次看输入占位符,与批大小和序列长度相关的结构[batch_size, seq_length]。最后是target占位符,结构与输入占位符是一样的。为更好理解这里给输入和target画个图,如下:

这里写图片描述

这里写图片描述

self.embedding = tf.get_variable('embedding', [vocab_size, embedding_size])
inputs = tf.nn.embedding_lookup(self.embedding, self.input_data)

一般我们会需要一个嵌入层将词汇嵌入到指定的维度空间上,维度由embedding_size指定。同时vocab_size为词汇大小,这样就可以将所有单词都映射到指定的维数空间上。嵌入层结构如下图,通过tf.nn.embedding_lookup就能找到输入对应的词空间向量了,这里解释下embedding_lookup操作,它会从词汇中取到inputs每个元素对应的词向量,inputs为2维的话,通过该操作后变为3维,因为已经将词用embedding_size维向量表示了。

这里写图片描述

sliced_inputs = [tf.squeeze(input_, [1]) for input_ in
                         tf.split(axis=1, num_or_size_splits=self.seq_length, value=inputs)]
outputs, final_state = tf.contrib.rnn.static_rnn(multi_cell, sliced_inputs, initial_state=self.initial_state)

上面得到的3维的嵌入层空间向量,我们无法直接传入循环神经网络,需要一些处理。需要根据序列长度切割,通过split后再经过squeeze操作后得到一个list,这个list就是最终要进入到循环神经网络的输入,list的长度为seq_length,这个很好理解,就是由这么多个时刻的输入。每个输入的结构为(batch_size,embedding_size),也即是(20,128)。注意这里的embedding_size,刚好也是128,与循环神经网络的隐含层神经元数量一样,这里不是巧合,而是他们必须要相同,这样嵌入层出来的矩阵输入到神经网络才能刚好与神经网络的各个权重完美相乘。最终得到循环神经网络的输出和最终状态。

flat_outputs = tf.reshape(tf.concat(axis=1, values=outputs), [-1, hidden_size])
flat_targets = tf.reshape(tf.concat(axis=1, values=self.targets), [-1])

softmax_w = tf.get_variable("softmax_w", [hidden_size, vocab_size])
softmax_b = tf.get_variable("softmax_b", [vocab_size])
self.logits = tf.matmul(flat_outputs, softmax_w) + softmax_b

loss = tf.nn.sparse_softmax_cross_entropy_with_logits(logits=self.logits, labels=flat_targets)
mean_loss = tf.reduce_mean(loss)

经过2层循环神经网络得到了输出outputs,但该输出是一个list结构,我们要通过tf.reshape转成tf张量形式,该张量结构为(200,128)。同样target占位符也要连接起来,结构为(200,)。接着构建softmax层,权重结构为[hidden_size, vocab_size],偏置项结构为[vocab_size],输出矩阵与权重矩阵相乘并加上偏置项得到logits,然后使用sparse_softmax_cross_entropy_with_logits计算交叉熵损失,最后求损失平均值。

count = tf.Variable(1.0, name='count')
sum_mean_loss = tf.Variable(1.0, name='sum_mean_loss')
update_loss_monitor = tf.group(sum_mean_loss.assign(sum_mean_loss + mean_loss), count.assign(count + 1),
                                       name='update_loss_monitor')
with tf.control_dependencies([update_loss_monitor]):
    self.average_loss = sum_mean_loss / count
self.global_step = tf.get_variable('global_step', [], initializer=tf.constant_initializer(0.0))

这里逻辑比较清晰了,用于计算平均损失,另外global_step变量用于记录训练的全局步数。

tvars = tf.trainable_variables()
grads, _ = tf.clip_by_global_norm(tf.gradients(mean_loss, tvars), max_grad_norm)
optimizer = tf.train.AdamOptimizer(self.learning_rate)
self.train_op = optimizer.apply_gradients(zip(grads, tvars), global_step=self.global_step)

最后使用优化器对损失函数进行优化。为了防止梯度爆炸或梯度消失需要用clip_by_global_norm对梯度进行修正。

所以最后构建的图可以用下面的图大致描述。
这里写图片描述

创建会话

with tf.Session(graph=graph) as session:
tf.global_variables_initializer().run()
for i in range(num_epochs):
    model.train(session, train_size, train_batches)

def train(self, session, train_size, train_batches):
    epoch_size = train_size // (self.batch_size * self.seq_length)
    if train_size % (self.batch_size * self.seq_length) != 0:
        epoch_size += 1
    state = session.run(self.zero_state)
    start_time = time.time()
    for step in range(epoch_size):
        data = train_batches.next()
        inputs = np.array(data[:-1]).transpose()
        targets = np.array(data[1:]).transpose()
        ops = [self.average_loss, self.final_state, self.train_op, self.global_step, self.learning_rate]
        feed_dict = {self.input_data: inputs, self.targets: targets,
                     self.initial_state: state}
        average_loss, state, __, global_step, lr = session.run(ops, feed_dict)

创建会话开始训练,设置需要训练多少轮,由num_epochs指定。epoch_size为完整训练一遍语料库需要的轮数。运行self.zero_state得到初始状态,通过批量生成器获取一批样本数据,因为当前时刻的输入对应的正确输出为下一时刻的值,所以用data[:-1]和data[1:]得到输入和target。组织ops并将输入、target和状态对应输入到占位符上,执行。

预测

module_file = tf.train.latest_checkpoint(restore_path)
model_saver.restore(session, module_file)
start_text = 'your'
length = 20
print(model.predict(session, start_text, length, vocab_index_dict, index_vocab_dict))

将前面训练的模型保存下来后就可以加载该模型并且进行RNN预测了。

github

https://github.com/sea-boat/DeepLearning-Lab

========广告时间========

鄙人的新书《Tomcat内核设计剖析》已经在京东销售了,有需要的朋友可以到 https://item.jd.com/12185360.html 进行预定。感谢各位朋友。

为什么写《Tomcat内核设计剖析》

=========================

欢迎关注:

这里写图片描述

版权声明:本文为博主原创文章,未经博主允许不得转载。

相关文章推荐

Android设计模式学习之观察者模式

观察者模式在实际项目中使用的也是非常频繁的,它最常用的地方是GUI系统、订阅——发布系统等。因为这个模式的一个重要作用就是解耦,使得它们之间的依赖性更小,甚至做到毫无依赖。以GUI系统来说,应用的UI...

关注CSDN程序人生公众号,轻松获得下载积分

关注公众号 在公众号里回复“”秘密“”两个字 返回 http://task.csdn.net/m/task/home?task_id=398 领取奖励 提示:根据公众号里的自动回复,完成...

阿里云服务器Linux下配置web服务环境

下载XShell工具,链接到阿里云Linux服务器上。关于XShell使用,这里有一个入门教程:http://www.cnblogs.com/perseverancevictory/p/4910145...

属性动画----把图片渐渐变小不见(主函数MainActivity 页面)(XML布局)(本布局和渐变布局一样)

LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schema...

JavaEE 6及以上版本的web.xml问题?

JavaEE 6及以上版本的web.xml问题?MyEclipse JavaEE 6版本开始web.xml突然消失不见?没这回事,只是不太必须而已,有需要的项目可以自行进行添加或在创建项目的时候点击n...

面试题:8个试剂,其中一个有毒,最少多少只小白鼠能检测出有毒试剂

面试题:8个试剂,其中一个有毒,最少多少只小白鼠能检测出有毒试剂方法1:用3只小鼠,能组合成8种状态。 第一只喂食【1、3、5、7】四只试剂 第二只喂食【2、3、6、7】四只试剂 第三只喂食【4、5、...
内容举报
返回顶部
收藏助手
不良信息举报
您举报文章:深度学习:神经网络中的前向传播和反向传播算法推导
举报原因:
原因补充:

(最多只允许输入30个字)