使用TensorFlow动手实现一个Char-RNN

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部分的代码都大同小异,就不特别说明了。

原文地址:https://www.cnblogs.com/DjangoBlog/p/9504807.html

时间: 2024-11-13 10:21:08

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

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

1. 背景 JSON(JavaScript Object Notation) 是一种轻量级的数据交换格式.相对于另一种数据交换格式 XML,JSON 有着诸多优点.比如易读性更好,占用空间更少等.在 web 应用开发领域内,得益于 JavaScript 对 JSON 提供的良好支持,JSON 要比 XML 更受开发人员青睐.所以作为开发人员,如果有兴趣的话,还是应该深入了解一下 JSON 相关的知识.本着探究 JSON 原理的目的,我将会在这篇文章中详细向大家介绍一个简单的JSON解析器的解析流

OneExerciseOne创建一个类,它包含一个int域,一个char域,他们都没有初始化,验证Java执行了默认初始化

在学习<Thinking In java>时,遇到Chapter 2的Exercise 1,问题是这么说的"/**创建一个类,它包含一个int域,一个char域,他们都没有初始化,将他们的值打印出来,验证Java执行了默认初始化".刚拿到这道题目我是这么写的. 1. package Two; public class ExerciseOne {public static void main(String [] args){int a;char c;System.out.pr

java 中一个char包含几个字节

背景 ??char包含几个字节可能记得在上学的时候书上写的是2个字节,一直没有深究,今天我们来探究一下到底一个char多少个字节? Char ??char在设计之初的时候被用来存储字符,可是世界上有那么多字符,如果有1个字节,那么就意味着只能存储256中,显然不合适,而如果有两个字节,那么就可以存储65536种.这个数量符合大多数国家的语言字符的个数.于是Java团队默认使用unicode作为编码,一个char作为2个字节来存储. ??这里就有两个问题了? ????1. java的char一定会

自己动手实现一个队列LGQueue

自己动手实现一个队列LGQueue iOS系统没有提供queue容器,如果想用,需要手动自己来写一个: .h // // LGQueue.h // AntsSportApp // // Created by ligang on 15/3/24. // Copyright (c) 2015年 ligang. All rights reserved. // #import <Foundation/Foundation.h> @interface LGQueue : NSObject @proper

将一个字符串看作一个char类型的数组

有时候我们会遇到将用户输入的一句话中的某一个字符拿出来用的问题. 我们可以将用户输入的字符串看作一个char类型的数组 比如有一个字符串string a = "hello"; 要将'e'输出怎么办? 我们将a看作一个char类型的数组,然后使用for循环来遍历这个数组,然后在控制台中输出即可 for(int i = 0;i<a.Length;i++) { Console.writeLine(a[1]); } 关键是要有将字符串看作char类型数组的思维. 小菜鸟今天的一点小心得,

自己动手写一个FTP客户端

自己用socket写一个FTP客户端,模拟主动被动模式.(先支持LIST命令) # -*- coding: utf-8 -*- import socket, sys, thread, threading def main_sock(daddr, actions, saddr=()):     if saddr:         try:             sc=socket.create_connection(daddr, 3, saddr)             #print "Now

动手写一个Remoting测试工具

基于.NET开发分布式系统,经常用到Remoting技术.在测试驱动开发流行的今天,如果针对分布式系统中的每个Remoting接口的每个方法都要写详细的测试脚本,无疑非常浪费时间.所以,我想写一个能自动测试remoting接口的小工具InterfaceTester.而且,当分布式系统中的某个remoting接口出现bug时,该小工具可以提交需要模拟的数据,以便在调试remoting服务的环境中,快速定位和解决bug. InterfaceTester运行起来后的效果如下图: 1.如何使用 (1)首

自己动手做一个小型“资源管理器”吧

自己动手做一个小型“资源管理器”吧 注:tvDirectory是treeView控件,lvDirectory是listView控件 首先搭建一下界面: 左边是treeView控件,右边是listView控件.(listView的网格线只需把GridLins设置成True就可以了.) 由于要用到IO流,所以别忘了导入命名空间:using System.IO; 我们只要创建一个文件类就可以了: 1 public class MyFile 2 { 3 //文件长度 4 public float Fil

模拟spring - 动手写一个spring AOP

一.前言 AOP (Aspect Oriented Programing) - 面向切面编程,它主要用于日志记录.性能分析.安全控制.事务处理.异常处理等方面. AOP主要使用JDK的反射和动态代理,AOP代理其实是由AOP框架动态生成的一个对象,该对象可作为目标对象使用,AOP代理包含了目标对象的全部方法,但AOP代理的方法与目标对象的方法存在差异:AOP方法在特定切入点添加了增强处理,并回调了目标对象的方法. 动态代理的文章请参考:http://blog.csdn.net/zdp072/ar