https://blog.csdn.net/thriving_fcl/article/details/72565455

前言

学习RNN的时候很多人应该都有看过Andrej Karpathy写的The Unreasonable Effectiveness of Recurrent Neural Networks,使用基于字符粒度的RNN让机器学会自己生成文本,比如令自己训练的RNN学会写歌词、写代码、写小说、写诗,听着就很新奇。

github上虽然已经有实现好的Char RNN,比如 
1. https://github.com/karpathy/char-rnn 
2. https://github.com/crazydonkey200/tensorflow-char-rnn

但是想要学习,最好的方式就是自己动手实现一遍。自己写一遍好处还是很多的,比如加深对RNN(LSTM)的理解,可以熟悉深度学习的框架。因为我主要用tensorflow,所以就基于tensorflow实现了一遍Char-RNN。

注:本文使用的tensorflow版本为1.0.0

个人经验,在实现的过程中最好是抛开别人代码的影响,只根据基本理论以及所用的框架的API文档一步步把代码写出来跑通,这样自己的收益才是最大的。

模型选择

要让机器生成文本,本质上是需要一个语言模型。语言模型可以用来评估一句话是自然语言的概率,即根据一句话中已观测到的词,预测下一个词出现的概率。也就是要能够处理序列数据,根据已有的序列数据,推断接下来可能的数据。如一句话“已经到了午餐时间,我正准备去吃{?}”,根据前面的描述,可以推断“吃”字背后是要接上可食用的东西,并且是可以作为午餐的,可能是“饭”、“面”等等,通常不可能是“汽车”、“树木”之类…因此我们需要一个能够处理序列数据,并且能够抽象出过去序列与任务相关方面的信息,再根据这些信息预测未来的模型。

神经网络中,RNN天然适合用于处理序列数据,它可以提取任意长度序列(x(t),x(t−1),...,x(1))(x(t),x(t−1),...,x(1))的摘要,选择性地精确保留过去序列的某些方面。而保留这些信息的方式则是通过RNN内部的隐藏状态。

但是RNN又有很多变体,因为基本RNN只有一个隐藏状态,对长距离的记忆效果不好,在模型参数迭代优化的时候存在梯度弥散的问题,因此又有了采用LSTM单元的RNN以及其他的变体,如GRU等等。

因此,在Char RNN的实践当中,就选用LSTM作为基本的模型。

因为tensorflow中已经实现了LSTM的单元,如果不是为了学习LSTM的原理,可以不需要自己去实现它。相应的API为

tf.contrib.rnn.LSTMCell()
  • 1

模型定义

我们需要定义一个class用来定义网络的结构,以及实现inference的接口。如果初次接触RNN,刚开始动手写的时候可能会一头雾水,我们已经有了LSTM的API,怎么把它拓展成可以接受文本的训练数据进行训练,最后再根据输入的一些文字,输出接下来文字的模型呢?

我的做法是先明确输入与输出,以及我所知道的必备要素,然后再把它们衔接拼凑起来。

基本LSTM单元

首先我们要用到LSTMCell,它的必填参数是num_units,也就是每个LSTM Cell中的单元数,与输入向量的维度是一致的。我们的输入是词向量,维度是我们自己定义的,这里用一个参数rnn_size来表示。定义基本LSTM Cell的代码如下

# 定义基本lstm单元
lstm_cell_list = [tf.contrib.rnn.LSTMCell(rnn_size) for _ in xrange(layer_size)]
# 使用MultiRNNCell 接口连接多层lstm, 并加上dropout
self.cell = tf.contrib.rnn.DropoutWrapper(tf.contrib.rnn.MultiRNNCell(lstm_cell_list), output_keep_prob=output_keep_prob)
  • 1
  • 2
  • 3
  • 4

明确输入

在训练的过程中,每次都feed进一个batch的数据,batch的大小也是我们定义的,用batch_size表示,因此LSTM模型所接受输入的shape为(batch_size, rnn_size)

如果我们使用预训练好的词向量作为输入,那么这里就可以写成

tf.input_data = tf.placeholder(tf.float32, shape=[batch_size, rnn_size], name='input_data')
  • 1

但我们希望词向量可以在train的过程中被改变,更适应我们的训练数据。那就要用Variable来表示词向量矩阵。因此我们要定义一个变量来表示,词向量矩阵的维度应该是 vocab_size * rnn_size。 即每一行代表一个词,列数就是我们需要自己定义的词向量维度。定义了词向量矩阵的变量,每次输入的时候,还需要为输入的词找到对应的词向量,这些tensorflow都为我们封装好了,代码如下

embedding = tf.Variable(tf.truncated_normal([vocab_size, rnn_size], stddev=0.1), name='embedding')
inputs = tf.nn.embedding_lookup(embedding, self.input_data)
  • 1
  • 2

tf.nn.embedding_lookup这个函数就是用于返回所查找的词向量Tensor的。

embedding_lookup(params, ids, partition_strategy=’mod’, name=None, validate_indices=True, max_norm=None)

其中params是词向量矩阵,ids是需要需要查找的词的id。举个简单的例子如下

# 假设有词向量空间x
x = [[1.0,2.0,3.0],[4.0,5.0,6.0],[7.0,8.0,9.0]]
vx = tf.Variable(x, name='vx') ids = tf.placeholder(tf.int32, name='ids')
inputs = tf.nn.embedding_lookup(vx, ids) # 假如每个batch有3个句子,每个句子有两个词,词的id如下
input_data = [[0,1],[1,2],[0,2]] with tf.Session() as sess:
sess.run(tf.global_variables_initializer())
sess.run(inputs, feed_dict={ids:input_data}) # 输出结果如下
>>> array([[[ 1., 2., 3.],
[ 4., 5., 6.]], [[ 4., 5., 6.],
[ 7., 8., 9.]], [[ 1., 2., 3.],
[ 7., 8., 9.]]], dtype=float32) 输出结果的shape为(3,2,3)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25

用上述方式就可以查出来一个batch中每个句子的每个词对应的词向量。所以我们原始输入的batch中,每个元素是一个sequence,sequence中的元素又是每个词对应的id。

这部分的完整代码如下

self.input_data = tf.placeholder(tf.int32, shape=[batch_size, sequence_length], name='input_data')

# 指定这部分使用CPU进行计算
with tf.device('/cpu:0'):
embedding = tf.Variable(tf.truncated_normal([vocab_size, rnn_size], stddev=0.1), name='embedding')
inputs = tf.nn.embedding_lookup(embedding, self.input_data)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

明确输出

因为在Char RNN中,每一时刻的输出都是下一时刻的输入,因此LSTM的输出otot与输入xtxt维度是一样的。但otot并不是Char RNN模型的输出,otot之后还需要跟全连接层以及softmax层来判断每个词出现的概率。每一时刻都有一个输出,在训练的阶段,需要收集每一时刻的输出,以便与targets进行比较来计算loss。因此需要有一个循环来展开整个lstm。展开的这部分tensorflow也有API可以调用,但是为了更好的理解,还是自己实现一遍比较好。代码如下

# 定义初始状态
self.initial_state = self.cell.zero_state(batch_size, tf.float32) with tf.variable_scope('RNN'):
for time_step in xrange(sequence_length):
# 因为LSTM Cell调用__call__()方法时,会使用到get_variable()获取内部变量
# 如果reuse的flag是False,调用get_variable()后会查找该variable_scope中有没有重名的变量,如果有就报错
# 如果reuse的flag是True,调用get_variable()后则是在当前的variable_scope找不到变量时报错
# 因此在这部分需要reuse的时候要定义一个variable_scope,否则之后想用get_variable()定义新变量都会报错
if time_step > 0:
tf.get_variable_scope().reuse_variables()
if time_step == 0:
output, state = self.cell(inputs[:, time_step, :], self.initial_state)
else:
output, state = self.cell(inputs[:, time_step, :], state)
outputs.append(output) self.final_state = state
softmax_w = tf.Variable(tf.truncated_normal([rnn_size, vocab_size], stddev=0.1), name='softmax_w')
softmax_b = tf.Variable(tf.zeros([vocab_size]), name='softmax_b') # 执行完循环以后,outputs的shape=(sequence_length, batch_size, rnn_size)
# 而matmul接受的矩阵的rank必须是2,因此还需要做一下转换
# tf.concat()转换后的outputs的shape为(batch_size * sequence_size, rnn_size)
outputs = tf.concat(outputs, 0)
self.logits = tf.matmul(outputs, softmax_w) + softmax_b
self.prob = tf.nn.softmax(self.logits)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27

定义loss与train_op

要定义loss函数首先要有正确的输入,因此先定义targets。在实际feed的时候,要注意targets中的顺序必须与outputs中预测结果是对应的。这个之后写一个辅助函数来对输入的targets进行转换。

loss函数的定义使用cross_entropy,tensorflow中有相应的API tf.losses.softmax_cross_entropy, 这个API封装了softmax步骤,因此应该传入logits而不是把softmax之后的prob传进去。

定义完loss之后就需要定义optimizer与train_op。

通常可以直接train_op = tf.train.AdamOptimizer(self.lr).minimize(self.cost)。但是RNN的训练中很有可能因为梯度过大导致训练过程不稳定而不收敛,因此需要对计算出的梯度做一步裁剪,再手动更新梯度。

这部分的代码如下

self.targets = tf.placeholder(tf.int32, shape=[None, vocab_size], name='targets')

self.cost = tf.losses.softmax_cross_entropy(self.targets, self.logits)
self.lr = tf.Variable(0.0, trainable=False)
tvars = tf.trainable_variables()
grads, _ = tf.clip_by_global_norm(tf.gradients(self.cost, tvars), grad_clip) optimizer = tf.train.AdamOptimizer(self.lr)
self.train_op = optimizer.apply_gradients(zip(grads, tvars))
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

到这里为止Char RNN的主要部分,即模型的结构及其训练所需的op都定义完成了,train与inference部分的代码都大同小异,就不特别说明了。

 

使用TensorFlow动手实现一个Char-RNN的更多相关文章

  1. github上虽然已经有实现好的Char RNN

    前言 学习RNN的时候很多人应该都有看过Andrej Karpathy写的The Unreasonable Effectiveness of Recurrent Neural Networks,使用基 ...

  2. 这可能是国内最全面的char RNN注释

    char RNN代码来源于https://github.com/hzy46/Char-RNN-TensorFlow 前言 本人在学习char RNN的过程中,遇到了很多的问题,但是依然选择一行代码一行 ...

  3. 自己动手实现一个简单的JSON解析器

    1. 背景 JSON(JavaScript Object Notation) 是一种轻量级的数据交换格式.相对于另一种数据交换格式 XML,JSON 有着诸多优点.比如易读性更好,占用空间更少等.在 ...

  4. 自己动手实现一个WEB服务器

    自己动手实现一个 Web Server 项目背景 最近在重温WEB服务器的相关机制和原理,为了方便记忆和理解,就尝试自己用Java写一个简化的WEB SERVER的实现,功能简单,简化了常规服务器的大 ...

  5. 《动手实现一个网页加载进度loading》

    loading随处可见,比如一个app经常会有下拉刷新,上拉加载的功能,在刷新和加载的过程中为了让用户感知到 load 的过程,我们会使用一些过渡动画来表达.最常见的比如"转圈圈" ...

  6. C#中自己动手创建一个Web Server(非Socket实现)

    目录 介绍 Web Server在Web架构系统中的作用 Web Server与Web网站程序的交互 HTTPListener与Socket两种方式的差异 附带Demo源码概述 Demo效果截图 总结 ...

  7. 2.1确定一个char包含何种字符

    知识点: 1.char.IsControl 2.char.IsPunctuation 3.char.IsSurrogate 4.char.IsWhitespace 5.char.IsDigit 6.c ...

  8. 动手实现一个vue中的模态对话框组件

    写在前面 对话框是很常用的组件 , 在很多地方都会用到,一般我们可以使用自带的alert来弹出对话框,但是假如是设计 出的图该怎么办呢 ,所以我们需要自己写一个对话框,并且如果有很多地方都用到,那我们 ...

  9. 超详细动手搭建一个Vuepress站点及开启PWA与自动部署

    超详细动手搭建一个Vuepress站点及开启PWA与自动部署 五一之前就想写一篇关于Vuepress的文章,结果朋友结婚就不了了之了. 记得最后一定要看注意事项! Vuepress介绍 官网:http ...

随机推荐

  1. 一个页面从输入 URL 到页面加载显示完成,这个过程中都发生了什么?

    分为4个步骤: 1)  当发送一个 URL 请求时,不管这个 URL 是 Web 页面的 URL 还是 Web 页面上每个资源的 URL,浏览器都会开启一个线程来处理这个请求,同时在远程 DNS 服务 ...

  2. Cygwin、MinGw、mingw-w64,MSys msys2区别与联系

    https://www.biaodianfu.com/cygwin-ming-msys.html http://www.mingw-w64.org/doku.php http://blog.csdn. ...

  3. Revit API过滤元素类别(FamilySymbol与FamilyInstance)

    仅OfCategory()过滤的元素包含系统FamilySymbolOfClass(typeof(FamilyInstance))过滤出来文档中族实例. ;         ;         ;   ...

  4. Dropdown.js基于jQuery开发的轻量级下拉框插件

    Dropdown.js 前言 在SPA(Single Page Application)盛行的时代,jQuery插件的轮子正在减少,由于我厂有需求而开发了这个插件.如果觉得本文对您有帮助,请给个赞,以 ...

  5. Entity Framework 6 (7) vs NHibernate 4: DDD perspective(纯净DDD很难很难...)

    There is quite a bit of Entity Framework vs NHibernate comparisons on the web already, but all of th ...

  6. Delphi 局域网点对点文件传输(IdTcpClient控件)

    unit Unit1; interface uses Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms ...

  7. The Win32 Rundll and Rundll32 Interface Related Topics

    The Win32 Rundll and Rundll32 Interface Related Topics Microsoft Knowledge Base Article Q164787 Appl ...

  8. 实现iOS长时间后台的两种方法:Audiosession和VOIP

    http://www.cocoachina.com/applenews/devnews/2012/1212/5313.html 我们知道iOS开启后台任务后可以获得最多600秒的执行时间,而一些需要在 ...

  9. MEF在运行时替换插件

    利用AppDomain的ShadowCopy特性. var setup = new AppDomainSetup { CachePath = cachePath, ShadowCopyFiles = ...

  10. SharePoint Online 创建文档库

    前言 本文介绍如何在Office 365中创建文档库,以及文档库的一些基本设置. 正文 通过登录地址登录到Office 365的SharePoint Online站点中,我们可以在右上角的设置菜单中, ...