一、学习单步的RNN:RNNCell

如果要学习TensorFlow中的RNN,第一站应该就是去了解“RNNCell”,它是TensorFlow中实现RNN的基本单元,每个RNNCell都有一个call方法,使用方式是:(output, next_state) = call(input, state)。

借助图片来说可能更容易理解。假设我们有一个初始状态h0,还有输入x1,调用call(x1, h0)后就可以得到(output1, h1):


TensorFlow中RNN实现的正确打开方式

再调用一次call(x2, h1)就可以得到(output2, h2):

TensorFlow中RNN实现的正确打开方式

也就是说,每调用一次RNNCell的call方法,就相当于在时间上“推进了一步”,这就是RNNCell的基本功能。

在代码实现上,RNNCell只是一个抽象类,我们用的时候都是用的它的两个子类BasicRNNCell和BasicLSTMCell。顾名思义,前者是RNN的基础类,后者是LSTM的基础类。这里推荐大家阅读其源码实现(地址:http://t.cn/RNJrfMl),一开始并不需要全部看一遍,只需要看下RNNCell、BasicRNNCell、BasicLSTMCell这三个类的注释部分,应该就可以理解它们的功能了。

除了call方法外,对于RNNCell,还有两个类属性比较重要:

  • state_size

  • output_size

前者是隐层的大小,后者是输出的大小。比如我们通常是将一个batch送入模型计算,设输入数据的形状为(batch_size, input_size),那么计算时得到的隐层状态就是(batch_size, state_size),输出就是(batch_size, output_size)。

可以用下面的代码验证一下(注意,以下代码都基于TensorFlow最新的1.2版本):

import tensorflow as tf

import numpy as np

cell = tf.nn.rnn_cell.BasicRNNCell(num_units=128) # state_size = 128

print(cell.state_size) # 128

inputs = tf.placeholder(np.float32, shape=(32, 100)) # 32 是 batch_size

h0 = cell.zero_state(32, np.float32) # 通过zero_state得到一个全0的初始状态,形状为(batch_size, state_size)

output, h1 = cell.call(inputs, h0) #调用call函数

print(h1.shape) # (32, 128)

对于BasicLSTMCell,情况有些许不同,因为LSTM可以看做有两个隐状态h和c,对应的隐层就是一个Tuple,每个都是(batch_size, state_size)的形状:

import tensorflow as tf

import numpy as np

lstm_cell = tf.nn.rnn_cell.BasicLSTMCell(num_units=128)

inputs = tf.placeholder(np.float32, shape=(32, 100)) # 32 是 batch_size

h0 = lstm_cell.zero_state(32, np.float32) # 通过zero_state得到一个全0的初始状态

output, h1 = lstm_cell.call(inputs, h0)

print(h1.h)  # shape=(32, 128)

print(h1.c)  # shape=(32, 128)

二、学习如何一次执行多步:tf.nn.dynamic_rnn

基础的RNNCell有一个很明显的问题:对于单个的RNNCell,我们使用它的call函数进行运算时,只是在序列时间上前进了一步。比如使用x1、h0得到h1,通过x2、h1得到h2等。这样的h话,如果我们的序列长度为10,就要调用10次call函数,比较麻烦。对此,TensorFlow提供了一个tf.nn.dynamic_rnn函数,使用该函数就相当于调用了n次call函数。即通过{h0,x1, x2, …., xn}直接得{h1,h2…,hn}。

具体来说,设我们输入数据的格式为(batch_size, time_steps, input_size),其中time_steps表示序列本身的长度,如在Char RNN中,长度为10的句子对应的time_steps就等于10。最后的input_size就表示输入数据单个序列单个时间维度上固有的长度。另外我们已经定义好了一个RNNCell,调用该RNNCell的call函数time_steps次,对应的代码就是:

# inputs: shape = (batch_size, time_steps, input_size)

# cell: RNNCell

# initial_state: shape = (batch_size, cell.state_size)。初始状态。一般可以取零矩阵

outputs, state = tf.nn.dynamic_rnn(cell, inputs, initial_state=initial_state)

此时,得到的outputs就是time_steps步里所有的输出。它的形状为(batch_size, time_steps, cell.output_size)。state是最后一步的隐状态,它的形状为(batch_size, cell.state_size)。

此处建议大家阅读tf.nn.dynamic_rnn的文档(地址:https://www.tensorflow.org/api_docs/python/tf/nn/dynamic_rnn)做进一步了解。

三、学习如何堆叠RNNCell:MultiRNNCell

很多时候,单层RNN的能力有限,我们需要多层的RNN。将x输入第一层RNN的后得到隐层状态h,这个隐层状态就相当于第二层RNN的输入,第二层RNN的隐层状态又相当于第三层RNN的输入,以此类推。在TensorFlow中,可以使用tf.nn.rnn_cell.MultiRNNCell函数对RNNCell进行堆叠,相应的示例程序如下:

import tensorflow as tf

import numpy as np

# 每调用一次这个函数就返回一个BasicRNNCell

def get_a_cell():
   return tf.nn.rnn_cell.BasicRNNCell(num_units=128)

# 用tf.nn.rnn_cell MultiRNNCell创建3层RNN

cell = tf.nn.rnn_cell.MultiRNNCell([get_a_cell() for _ in range(3)]) # 3层RNN

# 得到的cell实际也是RNNCell的子类

# 它的state_size是(128, 128, 128)

# (128, 128, 128)并不是128x128x128的意思

# 而是表示共有3个隐层状态,每个隐层状态的大小为128

print(cell.state_size) # (128, 128, 128)

# 使用对应的call函数

inputs = tf.placeholder(np.float32, shape=(32, 100)) # 32 是 batch_size

h0 = cell.zero_state(32, np.float32) # 通过zero_state得到一个全0的初始状态

output, h1 = cell.call(inputs, h0)

print(h1) # tuple中含有3个32x128的向量

通过MultiRNNCell得到的cell并不是什么新鲜事物,它实际也是RNNCell的子类,因此也有call方法、state_size和output_size属性。同样可以通过tf.nn.dynamic_rnn来一次运行多步。

此处建议阅读MutiRNNCell源码(地址:http://t.cn/RNJrfMl)中的注释进一步了解其功能。

四、可能遇到的坑1:Output说明

在经典RNN结构中有这样的图:

TensorFlow中RNN实现的正确打开方式

在上面的代码中,我们好像有意忽略了调用call或dynamic_rnn函数后得到的output的介绍。将上图与TensorFlow的BasicRNNCell对照来看。h就对应了BasicRNNCell的state_size。那么,y是不是就对应了BasicRNNCell的output_size呢?答案是否定的。

找到源码中BasicRNNCell的call函数实现:

def call(self, inputs, state):
   """Most basic RNN: output = new_state = act(W * input + U * state + B)."""
   output = self._activation(_linear([inputs, state], self._num_units, True))
   return output, output

这句“return output, output”说明在BasicRNNCell中,output其实和隐状态的值是一样的。因此,我们还需要额外对输出定义新的变换,才能得到图中真正的输出y。由于output和隐状态是一回事,所以在BasicRNNCell中,state_size永远等于output_size。TensorFlow是出于尽量精简的目的来定义BasicRNNCell的,所以省略了输出参数,我们这里一定要弄清楚它和图中原始RNN定义的联系与区别。

再来看一下BasicLSTMCell的call函数定义(函数的最后几行):

new_c = (
   c * sigmoid(f + self._forget_bias) + sigmoid(i) * self._activation(j))

new_h = self._activation(new_c) * sigmoid(o)

if self._state_is_tuple:
 new_state = LSTMStateTuple(new_c, new_h)

else:
 new_state = array_ops.concat([new_c, new_h], 1)

return new_h, new_state

我们只需要关注self._state_is_tuple == True的情况,因为self._state_is_tuple == False的情况将在未来被弃用。返回的隐状态是new_c和new_h的组合,而output就是单独的new_h。如果我们处理的是分类问题,那么我们还需要对new_h添加单独的Softmax层才能得到最后的分类概率输出。

还是建议大家亲自看一下源码实现(地址:http://t.cn/RNJsJoH)来搞明白其中的细节。

五、可能遇到的坑2:因版本原因引起的错误

在前面我们讲到堆叠RNN时,使用的代码是:

# 每调用一次这个函数就返回一个BasicRNNCell

def get_a_cell():
   return tf.nn.rnn_cell.BasicRNNCell(num_units=128)

# 用tf.nn.rnn_cell MultiRNNCell创建3层RNN

cell = tf.nn.rnn_cell.MultiRNNCell([get_a_cell() for _ in range(3)]) # 3层RNN

这个代码在TensorFlow 1.2中是可以正确使用的。但在之前的版本中(以及网上很多相关教程),实现方式是这样的:

one_cell =  tf.nn.rnn_cell.BasicRNNCell(num_units=128)

cell = tf.nn.rnn_cell.MultiRNNCell([one_cell] * 3) # 3层RNN

如果在TensorFlow 1.2中还按照原来的方式定义,就会引起错误!

六、一个练手项目:Char RNN

上面的内容实际上就是TensorFlow中实现RNN的基本知识了。这个时候,建议大家用一个项目来练习巩固一下。此处特别推荐Char RNN项目,这个项目对应的是经典的RNN结构,实现它使用的TensorFlow函数就是上面说到的几个,项目本身又比较有趣,可以用来做文本生成,平常大家看到的用深度学习来写诗写歌词的基本用的就是它了。

Char RNN的实现已经有很多了,可以自己去Github上面找,我这里也做了一个实现,供大家参考。项目地址为:hzy46/Char-RNN-TensorFlow(地址:https://github.com/hzy46/Char-RNN-TensorFlow)。代码的部分实现来自于《安娜卡列尼娜文本生成——利用TensorFlow构建LSTM模型》

这篇专栏,在此感谢 @天雨粟 。

我主要向代码中添加了embedding层,以支持中文,另外重新整理了代码结构,将API改成了最新的TensorFlow 1.2版本。

可以用这个项目来写诗(以下诗句都是自动生成的):

何人无不见,此地自何如。
一夜山边去,江山一夜归。
山风春草色,秋水夜声深。
何事同相见,应知旧子人。
何当不相见,何处见江边。
一叶生云里,春风出竹堂。
何时有相访,不得在君心。

还可以生成代码:

static int page_cpus(struct flags *str)
{
       int rc;
       struct rq *do_init;
};

/*
* Core_trace_periods the time in is is that supsed,
*/
#endif

/*
* Intendifint to state anded.
*/
int print_init(struct priority *rt)
{       /* Comment sighind if see task so and the sections */
       console(string, &can);
}

此外生成英文更不是问题(使用莎士比亚的文本训练):

LAUNCE:
The formity so mistalied on his, thou hast she was
to her hears, what we shall be that say a soun man
Would the lord and all a fouls and too, the say,
That we destent and here with my peace.

PALINA:
Why, are the must thou art breath or thy saming,
I have sate it him with too to have me of
I the camples.

最后,如果你脑洞够大,还可以来做一些更有意思的事情,比如我用了著名的网络小说《斗破苍穹》训练了一个RNN模型,可以生成下面的文本:

闻言,萧炎一怔,旋即目光转向一旁的那名灰袍青年,然后目光在那位老者身上扫过,那里,一个巨大的石台上,有着一个巨大的巨坑,一些黑色光柱,正在从中,一道巨大的黑色巨蟒,一股极度恐怖的气息,从天空上暴射而出 ,然后在其中一些一道道目光中,闪电般的出现在了那些人影,在那种灵魂之中,却是有着许些强者的感觉,在他们面前,那一道道身影,却是如同一道黑影一般,在那一道道目光中,在这片天地间,在那巨大的空间中,弥漫而开……

“这是一位斗尊阶别,不过不管你,也不可能会出手,那些家伙,可以为了这里,这里也是能够有着一些异常,而且他,也是不能将其他人给你的灵魂,所以,这些事,我也是不可能将这一个人的强者给吞天蟒,这般一次,我们的实力,便是能够将之击杀……”

“这里的人,也是能够与魂殿强者抗衡。”

萧炎眼眸中也是掠过一抹惊骇,旋即一笑,旋即一声冷喝,身后那些魂殿殿主便是对于萧炎,一道冷喝的身体,在天空之上暴射而出,一股恐怖的劲气,便是从天空倾洒而下。

“嗤!”

还是挺好玩的吧,另外还尝试了生成日文等等。

七、学习完整版的LSTMCell

上面只说了基础版的BasicRNNCell和BasicLSTMCell。TensorFlow中还有一个“完全体”的LSTM:LSTMCell。这个完整版的LSTM可以定义peephole,添加输出的投影层,以及给LSTM的遗忘单元设置bias等,可以参考其源码(地址:https://github.com/tensorflow/tensorflow/blob/master/tensorflow/python/ops/rnn_cell_impl.py#L417)了解使用方法。

八、学习最新的Seq2Seq API

Google在TensorFlow的1.2版本(1.3.0的rc版已经出了,貌似正式版也要出了,更新真是快)中更新了Seq2Seq API,使用这个API我们可以不用手动地去定义Seq2Seq模型中的Encoder和Decoder。此外它还和1.2版本中的新数据读入方式Datasets兼容。可以阅读此处的文档(地址:http://www.tensorflow.org/api_docs/python/tf/contrib/seq2seq)学习它的使用方法。

九、代码例子

使用循环神经网络(RNN)实现影评情感分类

作为对循环神经网络的实践,我用循环神经网络做了个影评情感的分类,即判断影评的感情色彩是正面的,还是负面的。

选择使用RNN来做情感分类,主要是因为影评是一段文字,是序列的,而RNN对序列的支持比较好,能够“记忆”前文。虽然可以提取特征词向量,然后交给传统机器学习模型或全连接神经网络去做,也能取得很好的效果,但只从端对端的角度来看的话,RNN无疑是最合适的。

以下介绍实现过程。

一、数据预处理

本文中使用的训练数据集为https://www.cs.cornell.edu/people/pabo/movie-review-data/上的sentence polarity dataset v1.0,包含正负面评论各5331条。可以点击进行下载。

数据下载下来之后需要进行解压,得到rt-polarity.negrt-polarity.pos文件,这两个文件是Windows-1252编码的,先将它转成unicode处理起来会更方便。

补充一下小知识,当我们打开一个文件,发现乱码,却又不知道该文件的编码是什么的时候,可以使用python 
chardet类库进行判断,这里的Windows-1252就是使用该类库检测出来的。

在数据预处理部分,我们要完成如下处理过程:

1.转码

即将文件转为unicode编码,方便我们后续操作。读取文件,转换编码,重新写入到新文件即可。不存在技术难点。

2.生成词汇表

读取训练文件,提取出所有的单词,并统计各个单词出现的次数。为了避免低频词的干扰,同时减少模型参数,我们只保留部分高频词,比如这里我只保存出现次数前9999个,同时将低频词标识符<unkown>加入到词汇表中。

3.借助词汇表将影评转化为词向量

单词是没法直接输入给模型的,所以我们需要将词汇表中的每个单词对应于一个编号,将影评数据转化成词向量。方便后面生成词嵌入矩阵。

4.填充词向量并转化为np数组

因为不同评论的长度是不同的,我们要组成batch进行训练,就需要先将其长度统一。这里我选择以最长的影评为标准,对其他较短的影评的空白部分进行填充。然后将其转化成numpy的数组。

5.按比例划分数据集

按照机器学习的惯例,数据集应被划分为三份,即训练集、开发集和测试集。当然,有时也会只划分两份,即只包括训练集和开发集。

这里我划分成三份,训练集、开发集和测试集的占比为[0.8,0.1,0.1]。划分的方式为轮盘赌法,在numpy中可以使用cumsumsearchsorted来简洁地实现轮盘赌法。

6.打乱数据集,写入文件

为了取得更好的训练效果,将数据集随机打乱。为了保证在训练和模型调整的过程中训练集、开发集、测试集不发生改变,将三个数据集写入到文件中,使用的时候从文件中读取。

下面贴上数据预处理的代码,注释写的很细,就不多说了。

# -*- coding: utf-8 -*-
# @Time    : 18-3-14 下午2:28
# @Author  : AaronJny
# @Email   : Aaron__7@163.com
import sysreload(sys)
sys.setdefaultencoding('utf8')
import collections
import settings
import utils
import numpy as npdef create_vocab():"""创建词汇表,写入文件中:return:"""# 存放出现的所有单词word_list = []# 从文件中读取数据,拆分单词with open(settings.NEG_TXT, 'r') as f:f_lines = f.readlines()for line in f_lines:words = line.strip().split()word_list.extend(words)with open(settings.POS_TXT, 'r') as f:f_lines = f.readlines()for line in f_lines:words = line.strip().split()word_list.extend(words)# 统计单词出现的次数counter = collections.Counter(word_list)sorted_words = sorted(counter.items(), key=lambda x: x[1], reverse=True)# 选取高频词word_list = [word[0] for word in sorted_words]word_list = ['<unkown>'] + word_list[:settings.VOCAB_SIZE - 1]# 将词汇表写入文件中with open(settings.VOCAB_PATH, 'w') as f:for word in word_list:f.write(word + '\n')def create_vec(txt_path, vec_path):"""根据词汇表生成词向量:param txt_path: 影评文件路径:param vec_path: 输出词向量路径:return:"""# 获取单词到编号的映射word2id = utils.read_word_to_id_dict()# 将语句转化成向量vec = []with open(txt_path, 'r') as f:f_lines = f.readlines()for line in f_lines:tmp_vec = [str(utils.get_id_by_word(word, word2id)) for word in line.strip().split()]vec.append(tmp_vec)# 写入文件中with open(vec_path, 'w') as f:for tmp_vec in vec:f.write(' '.join(tmp_vec) + '\n')def cut_train_dev_test():"""使用轮盘赌法,划分训练集、开发集和测试集打乱,并写入不同文件中:return:"""# 三个位置分别存放训练、开发、测试data = [[], [], []]labels = [[], [], []]# 累加概率 rate [0.8,0.1,0.1]  cumsum_rate [0.8,0.9,1.0]rate = np.array([settings.TRAIN_RATE, settings.DEV_RATE, settings.TEST_RATE])cumsum_rate = np.cumsum(rate)# 使用轮盘赌法划分数据集with open(settings.POS_VEC, 'r') as f:f_lines = f.readlines()for line in f_lines:tmp_data = [int(word) for word in line.strip().split()]tmp_label = [1, ]index = int(np.searchsorted(cumsum_rate, np.random.rand(1) * 1.0))data[index].append(tmp_data)labels[index].append(tmp_label)with open(settings.NEG_VEC, 'r') as f:f_lines = f.readlines()for line in f_lines:tmp_data = [int(word) for word in line.strip().split()]tmp_label = [0, ]index = int(np.searchsorted(cumsum_rate, np.random.rand(1) * 1.0))data[index].append(tmp_data)labels[index].append(tmp_label)# 计算一下实际上分割出来的比例print '最终分割比例', np.array([map(len, data)], dtype=np.float32) / sum(map(len, data))# 打乱数据,写入到文件中shuffle_data(data[0], labels[0], settings.TRAIN_DATA)shuffle_data(data[1], labels[1], settings.DEV_DATA)shuffle_data(data[2], labels[2], settings.TEST_DATA)def shuffle_data(x, y, path):"""填充数据,生成np数组打乱数据,写入文件中:param x: 数据:param y: 标签:param path: 保存路径:return:"""# 计算影评的最大长度maxlen = max(map(len, x))# 填充数据data = np.zeros([len(x), maxlen], dtype=np.int32)for row in range(len(x)):data[row, :len(x[row])] = x[row]label = np.array(y)# 打乱数据state = np.random.get_state()np.random.shuffle(data)np.random.set_state(state)np.random.shuffle(label)# 保存数据np.save(path + '_data', data)np.save(path + '_labels', label)def decode_file(infile, outfile):"""将文件的编码从'Windows-1252'转为Unicode:param infile: 输入文件路径:param outfile: 输出文件路径:return:"""with open(infile, 'r') as f:txt = f.read().decode('Windows-1252')with open(outfile, 'w') as f:f.write(txt)if __name__ == '__main__':# 解码文件decode_file(settings.ORIGIN_POS, settings.POS_TXT)decode_file(settings.ORIGIN_NEG, settings.NEG_TXT)# 创建词汇表create_vocab()# 生成词向量create_vec(settings.NEG_TXT, settings.NEG_VEC)create_vec(settings.POS_TXT, settings.POS_VEC)# 划分数据集cut_train_dev_test()

二、模型编写

数据处理好之后,开始模型的编写。这里选用循环神经网络,建模过程大致如下:

1.使用embedding构建词嵌入矩阵

在数据预处理中,我们将影评处理成了一个个单词编号构成的向量,也就是说,一条影评,对应于一个由单词编号构成的向量。

将这样的向量进行embedding,即可构建出词嵌入矩阵。在词嵌入矩阵中,每个词由一个向量表示,矩阵中不同向量之间的差异对应于它们表示的词之间的差异。

2.使用LSTM作为循环神经网络的基本单元

长短时记忆网络(LSTM)能够自动完成前文信息的“记忆”和“遗忘”,在循环神经网络中表现良好,已经成为在循环神经网络中大部分人的首选。这里我选择使用LSTM作为循环神经网络的基本单元。

3.对embedding和LSTM进行随机失活(dropout)

为了提高模型的泛化能力,并减少参数,我对embedding层和LSTM单元进行dropout。

4.建立深度为2的深度循环神经网络

为了提高模型的拟合能力,使用深度循环神经网络,我选择的深度为2。

5.给出二分类概率

对深度循环神经网络的最后节点的输出做逻辑回归,通过sigmoid使结果落到0-1之间,代表结果是正类的概率。

损失函数使用交叉熵,优化器选择Adam。

此部分代码如下(注:代码中装饰器的作用为划分命名空间以及保证张量运算只被定义一次):

# -*- coding: utf-8 -*-
# @Time    : 18-3-14 下午2:57
# @Author  : AaronJny
# @Email   : Aaron__7@163.com
import tensorflow as tf
import functools
import settingsHIDDEN_SIZE = 128
NUM_LAYERS = 2def doublewrap(function):@functools.wraps(function)def decorator(*args, **kwargs):if len(args) == 1 and len(kwargs) == 0 and callable(args[0]):return function(args[0])else:return lambda wrapee: function(wrapee, *args, **kwargs)return decorator@doublewrap
def define_scope(function, scope=None, *args, **kwargs):attribute = '_cache_' + function.__name__name = scope or function.__name__@property@functools.wraps(function)def decorator(self):if not hasattr(self, attribute):with tf.variable_scope(name, *args, **kwargs):setattr(self, attribute, function(self))return getattr(self, attribute)return decoratorclass Model(object):def __init__(self, data, lables, emb_keep, rnn_keep):"""神经网络模型:param data:数据:param lables: 标签:param emb_keep: emb层保留率:param rnn_keep: rnn层保留率"""self.data = dataself.label = lablesself.emb_keep = emb_keepself.rnn_keep = rnn_keepself.predictself.lossself.global_stepself.emaself.optimizeself.acc@define_scopedef predict(self):"""定义前向传播过程:return:"""# 词嵌入矩阵权重embedding = tf.get_variable('embedding', [settings.VOCAB_SIZE, HIDDEN_SIZE])# 使用dropout的LSTMlstm_cell = [tf.nn.rnn_cell.DropoutWrapper(tf.nn.rnn_cell.BasicLSTMCell(HIDDEN_SIZE), self.rnn_keep) for _ inrange(NUM_LAYERS)]# 构建循环神经网络cell = tf.nn.rnn_cell.MultiRNNCell(lstm_cell)# 生成词嵌入矩阵,并进行dropoutinput = tf.nn.embedding_lookup(embedding, self.data)dropout_input = tf.nn.dropout(input, self.emb_keep)# 计算rnn的输出outputs, last_state = tf.nn.dynamic_rnn(cell, dropout_input, dtype=tf.float32)# 做二分类问题,这里只需要最后一个节点的输出last_output = outputs[:, -1, :]# 求最后节点输出的线性加权和weights = tf.Variable(tf.truncated_normal([HIDDEN_SIZE, 1]), dtype=tf.float32, name='weights')bias = tf.Variable(0, dtype=tf.float32, name='bias')logits = tf.matmul(last_output, weights) + biasreturn logits@define_scopedef ema(self):"""定义移动平均:return:"""ema = tf.train.ExponentialMovingAverage(settings.EMA_RATE, self.global_step)return ema@define_scopedef loss(self):"""定义损失函数,这里使用交叉熵:return:"""loss = tf.nn.sigmoid_cross_entropy_with_logits(labels=self.label, logits=self.predict)loss = tf.reduce_mean(loss)return loss@define_scopedef global_step(self):"""step,没什么好说的,注意指定trainable=False:return:"""global_step = tf.Variable(0, trainable=False)return global_step@define_scopedef optimize(self):"""定义反向传播过程:return:"""# 学习率衰减learn_rate = tf.train.exponential_decay(settings.LEARN_RATE, self.global_step, settings.LR_DECAY_STEP,settings.LR_DECAY)# 反向传播优化器optimizer = tf.train.AdamOptimizer(learn_rate).minimize(self.loss, global_step=self.global_step)# 移动平均操作ave_op = self.ema.apply(tf.trainable_variables())# 组合构成训练opwith tf.control_dependencies([optimizer, ave_op]):train_op = tf.no_op('train')return train_op@define_scopedef acc(self):"""定义模型acc计算过程:return:"""# 对前向传播的结果求sigmoidoutput = tf.nn.sigmoid(self.predict)# 真负类ok0 = tf.logical_and(tf.less_equal(output, 0.5), tf.equal(self.label, 0))# 真正类ok1 = tf.logical_and(tf.greater(output, 0.5), tf.equal(self.label, 1))# 一个数组,所有预测正确的都为True,否则Falseok = tf.logical_or(ok0, ok1)# 先转化成浮点型,再通过求平均来计算accacc = tf.reduce_mean(tf.cast(ok, dtype=tf.float32))return acc

三、组织数据集

我编写了一个类用于组织数据,方便训练和验证使用。代码很简单,就不多说了,直接贴代码:

# -*- coding: utf-8 -*-
# @Time    : 18-3-14 下午3:33
# @Author  : AaronJny
# @Email   : Aaron__7@163.com
import numpy as np
import settingsclass Dataset(object):def __init__(self, data_kind=0):"""生成一个数据集对象:param data_kind: 决定了使用哪种数据集 0-训练集 1-开发集 2-测试集"""self.data, self.labels = self.read_data(data_kind)self.start = 0  # 记录当前batch位置self.data_size = len(self.data)  # 样例数def read_data(self, data_kind):"""从文件中加载数据:param data_kind:数据集种类 0-训练集 1-开发集 2-测试集:return:"""# 获取数据集路径data_path = [settings.TRAIN_DATA, settings.DEV_DATA, settings.TEST_DATA][data_kind]# 加载data = np.load(data_path + '_data.npy')labels = np.load(data_path + '_labels.npy')return data, labelsdef next_batch(self, batch_size):"""获取一个大小为batch_size的batch:param batch_size: batch大小:return:"""start = self.startend = min(start + batch_size, self.data_size)self.start = end# 当遍历完成后回到起点if self.start >= self.data_size:self.start = 0# 返回一个batch的数据和标签return self.data[start:end], self.labels[start:end]

四、模型训练

训练过程中,额外操作主要有两个:

1.使用移动平均

我使用移动平均的主要目的是使loss曲线尽量平滑,以及提升模型的泛化能力。

2.使用学习率指数衰减

目的是保证前期学习率足够大,能够快速降低loss,后期学习率变小,能更好地逼近最优解。

当然,就是说说而已,这次的训练数据比较简单,学习率衰减发挥的作用不大。

训练过程中,定期保存模型,以及checkpoint。这样可以在训练的同时,在验证脚本中读取最新模型进行验证。

此部分具体代码如下:

# -*- coding: utf-8 -*-
# @Time    : 18-3-14 下午4:41
# @Author  : AaronJny
# @Email   : Aaron__7@163.com
import settings
import tensorflow as tf
import models
import dataset
import osBATCH_SIZE = settings.BATCH_SIZE# 数据
x = tf.placeholder(tf.int32, [None, None])
# 标签
y = tf.placeholder(tf.float32, [None, 1])
# emb层的dropout保留率
emb_keep = tf.placeholder(tf.float32)
# rnn层的dropout保留率
rnn_keep = tf.placeholder(tf.float32)# 创建一个模型
model = models.Model(x, y, emb_keep, rnn_keep)# 创建数据集对象
data = dataset.Dataset(0)saver = tf.train.Saver()with tf.Session() as sess:# 全局初始化sess.run(tf.global_variables_initializer())# 迭代训练for step in range(settings.TRAIN_TIMES):# 获取一个batch进行训练x, y = data.next_batch(BATCH_SIZE)loss, _ = sess.run([model.loss, model.optimize],{model.data: x, model.label: y, model.emb_keep: settings.EMB_KEEP_PROB,model.rnn_keep: settings.RNN_KEEP_PROB})# 输出lossif step % settings.SHOW_STEP == 0:print 'step {},loss is {}'.format(step, loss)# 保存模型if step % settings.SAVE_STEP == 0:saver.save(sess, os.path.join(settings.CKPT_PATH, settings.MODEL_NAME), model.global_step)

五、验证模型

加载最新模型进行验证,通过修改数据集对象的参数可以制定训练/开发/测试集进行验证。

加载模型的时候,使用移动平均的影子变量覆盖对应变量。

代码如下:

# -*- coding: utf-8 -*-
# @Time    : 18-3-14 下午5:09
# @Author  : AaronJny
# @Email   : Aaron__7@163.com
import settings
import tensorflow as tf
import models
import dataset
import os
import time# 为了在使用GPU训练的同时,使用CPU进行验证
os.environ['CUDA_VISIBLE_DEVICES'] = ''BATCH_SIZE = settings.BATCH_SIZE# 数据
x = tf.placeholder(tf.int32, [None, None])
# 标签
y = tf.placeholder(tf.float32, [None, 1])
# emb层的dropout保留率
emb_keep = tf.placeholder(tf.float32)
# rnn层的dropout保留率
rnn_keep = tf.placeholder(tf.float32)# 创建一个模型
model = models.Model(x, y, emb_keep, rnn_keep)# 创建一个数据集对象
data = dataset.Dataset(1)  # 0-训练集 1-开发集 2-测试集# 移动平均变量
restore_variables = model.ema.variables_to_restore()
# 使用移动平均变量进行覆盖
saver = tf.train.Saver(restore_variables)with tf.Session() as sess:while True:# 加载最新的模型ckpt = tf.train.get_checkpoint_state(settings.CKPT_PATH)saver.restore(sess, ckpt.model_checkpoint_path)# 计算并输出accacc = sess.run([model.acc],{model.data: data.data, model.label: data.labels, model.emb_keep: 1.0, model.rnn_keep: 1.0})print 'acc is ', acctime.sleep(1)

六、对词汇表进行操作的几个方法

把对词汇表进行操作的几个方法提取出来了,放到了utils.py文件中。

# -*- coding: utf-8 -*-
# @Time    : 18-3-14 下午2:44
# @Author  : AaronJny
# @Email   : Aaron__7@163.com
import settingsdef read_vocab_list():"""读取词汇表:return:由词汇表中所有单词组成的列表"""with open(settings.VOCAB_PATH, 'r') as f:vocab_list = f.read().strip().split('\n')return vocab_listdef read_word_to_id_dict():"""生成一个单词到编号的映射:return:单词到编号的字典"""vocab_list = read_vocab_list()word2id = dict(zip(vocab_list, range(len(vocab_list))))return word2iddef read_id_to_word_dict():"""生成一个编号到单词的映射:return:编号到单词的字典"""vocab_list = read_vocab_list()id2word = dict(zip(range(len(vocab_list)), vocab_list))return id2worddef get_id_by_word(word, word2id):"""给定一个单词和字典,获得单词在字典中的编号:param word: 给定单词:param word2id: 单词到编号的映射:return: 若单词在字典中,返回对应的编号 否则,返回word2id['<unkown>']"""if word in word2id:return word2id[word]else:return word2id['<unkown>']

七、对模型进行配置

模型的配置参数大多数都被提取出来,单独放到了settings.py文件中,可以在这里对模型进行配置。

# -*- coding: utf-8 -*-
# @Time    : 18-3-14 下午2:44
# @Author  : AaronJny
# @Email   : Aaron__7@163.com# 源数据路径
ORIGIN_NEG = 'data/rt-polarity.neg'ORIGIN_POS = 'data/rt-polarity.pos'
# 转码后的数据路径
NEG_TXT = 'data/neg.txt'POS_TXT = 'data/pos.txt'
# 词汇表路径
VOCAB_PATH = 'data/vocab.txt'
# 词向量路径
NEG_VEC = 'data/neg.vec'POS_VEC = 'data/pos.vec'
# 训练集路径
TRAIN_DATA = 'data/train'
# 开发集路径
DEV_DATA = 'data/dev'
# 测试集路径
TEST_DATA = 'data/test'
# 模型保存路径
CKPT_PATH = 'ckpt'
# 模型名称
MODEL_NAME = 'model'
# 词汇表大小
VOCAB_SIZE = 10000
# 初始学习率
LEARN_RATE = 0.0001
# 学习率衰减
LR_DECAY = 0.99
# 衰减频率
LR_DECAY_STEP = 1000
# 总训练次数
TRAIN_TIMES = 2000
# 显示训练loss的频率
SHOW_STEP = 10
# 保存训练模型的频率
SAVE_STEP = 100
# 训练集占比
TRAIN_RATE = 0.8
# 开发集占比
DEV_RATE = 0.1
# 测试集占比
TEST_RATE = 0.1
# BATCH大小
BATCH_SIZE = 64
# emb层dropout保留率
EMB_KEEP_PROB = 0.5
# rnn层dropout保留率
RNN_KEEP_PROB = 0.5
# 移动平均衰减率
EMA_RATE = 0.99

八、运行模型

至此,模型构建完成。模型的运行步骤大致如下:

1.确保数据文件放在了对应路径中,运行python process_data对数据进行预处理。

2.运行python train.py对模型进行训练,训练好的模型会自动保存到对应的路径中。

3.运行python eval.py读取保存的最新模型,对训练/开发/测试集进行验证。

我简单跑了一下,由于数据集较小,模型的泛化能力不是很好。

当训练集、开发集、测试集的分布为[0.8,0.1,0.1],训练2000个batch_size=64的mini_batch时,模型在各数据集上的acc表现大致如下:

  • 训练集 0.95

  • 开发集 0.79

  • 测试集 0.80

更多

转行做机器学习,要学的还很多,文中如有错误纰漏之处,恳请诸位大佬拍砖指教…

项目GitHub地址:https://github.com/AaronJny/emotional_classification_with_rnn

补充

有朋友说希望我发一下网络结构图,所以就抽个时间随便画了一下,比较简陋,凑合着看吧=。=

补充

tensorflow教程:tf.contrib.rnn.DropoutWrapper

tf.contrib.rnn.DropoutWrapper 
Defined in tensorflow/python/ops/rnn_cell_impl.py.

__init__(cell,input_keep_prob=1.0,output_keep_prob=1.0,state_keep_prob=1.0,variational_recurrent=False,input_size=None,dtype=None,seed=None,dropout_state_filter_visitor=None
)
Args:cell:  an RNNCell, a projection to output_size is added to it.
input_keep_prob:  unit Tensor or float between 0 and 1, input keep probability; if it is constant and 1, no input dropout will be added.
output_keep_prob:  unit Tensor or float between 0 and 1, output keep probability; if it is constant and 1, no output dropout will be added.
state_keep_prob:  unit Tensor or float between 0 and 1, output keep probability; if it is constant and 1, no output dropout will be added. State dropout is performed on the outgoing states of the cell. Note the state components to which dropout is applied when state_keep_prob is in (0, 1) are also determined by the argument dropout_state_filter_visitor (e.g. by default dropout is never applied to the c component of an LSTMStateTuple).
variational_recurrent:  Python bool. If True, then the same dropout pattern is applied across all time steps per run call. If this parameter is set, input_size must be provided.
input_size:  (optional) (possibly nested tuple of) TensorShape objects containing the depth(s) of the input tensors expected to be passed in to the DropoutWrapper. Required and used iff variational_recurrent = True and input_keep_prob < 1.
dtype:  (optional) The dtype of the input, state, and output tensors. Required and used iff variational_recurrent = True.
seed:  (optional) integer, the randomness seed.
dropout_state_filter_visitor:  (optional), default: (see below). Function that takes any hierarchical level of the state and returns a scalar or depth=1 structure of Python booleans describing which terms in the state should be dropped out. In addition, if the function returns True, dropout is applied across this sublevel. If the function returns False, dropout is not applied across this entire sublevel. Default behavior: perform dropout on all terms except the memory (c) state of LSTMCellState objects, and don't try to apply dropout to TensorArray objects: def dropout_state_filter_visitor(s):  if isinstance(s, LSTMCellState): # Never perform dropout on the c state. return LSTMCellState(c=False, h=True) elif isinstance(s, TensorArray): return False return True

所谓dropout,就是指网络中每个单元在每次有数据流入时以一定的概率(keep prob)正常工作,否则输出0值。这是是一种有效的正则化方法,可以有效防止过拟合。在rnn中使用dropout的方法和cnn不同,推荐大家去把recurrent neural network regularization看一遍。 
在rnn中进行dropout时,对于rnn的部分不进行dropout,也就是说从t-1时候的状态传递到t时刻进行计算时,这个中间不进行memory的dropout;仅在同一个t时刻中,多层cell之间传递信息的时候进行dropout,如下图所示 
 
上图中,t-2时刻的输入xt−2首先传入第一层cell,这个过程有dropout,但是从t−2时刻的第一层cell传到t−1,t,t+1的第一层cell这个中间都不进行dropout。再从t+1时候的第一层cell向同一时刻内后续的cell传递时,这之间又有dropout了。

在使用tf.nn.rnn_cell.DropoutWrapper时,同样有一些参数,例如input_keep_prob,output_keep_prob等,分别控制输入和输出的dropout概率,很好理解。 
可以从官方文档中看到,它有input_keep_prob和output_keep_prob,也就是说裹上这个DropoutWrapper之后,如果我希望是input传入这个cell时dropout掉一部分input信息的话,就设置input_keep_prob,那么传入到cell的就是部分input;如果我希望这个cell的output只部分作为下一层cell的input的话,就定义output_keep_prob。 
备注:Dropout只能是层与层之间(输入层与LSTM1层、LSTM1层与LSTM2层)的Dropout;同一个层里面,T时刻与T+1时刻是不会Dropout的。

转载连接:https://blog.csdn.net/starzhou/article/details/77848156

https://blog.csdn.net/aaronjny/article/details/79561115

https://blog.csdn.net/abclhq2005/article/details/78683656

tensorflow rnn代码相关推荐

  1. TensorFlow (RNN)深度学习 双向LSTM(BiLSTM)+CRF 实现 sequence labeling 序列标注问题 源码下载...

    http://blog.csdn.net/scotfield_msn/article/details/60339415 在TensorFlow (RNN)深度学习下 双向LSTM(BiLSTM)+CR ...

  2. 【前沿】何恺明大神ICCV2017最佳论文Mask R-CNN的Keras/TensorFlow/Pytorch 代码实现

    我们提出了一个概念上简单.灵活和通用的用于目标实例分割(object instance segmentation)的框架.我们的方法能够有效地检测图像中的目标,同时还能为每个实例生成一个高质量的分割掩 ...

  3. Python API 撰写的 TensorFlow 示例代码

    Python API 撰写的 TensorFlow 示例代码 import tensorflow as tf import numpy as np # 使用 NumPy 生成假数据(phony dat ...

  4. 机器学习算法 09-02 TensorFlow核心概念 TensorFlow基础代码、TensorFlow线性回归解析解和BGD求法

    目录 1 核心概念 2 代码流程 3 基础代码: 3.1 tf的版本 定义常量 理解tensor 了解session 3. 2   指定设备.  Variable 初始化 .  with块创建sess ...

  5. 【神经网络】(12) MobileNetV2 代码复现,网络解析,附Tensorflow完整代码

    各位同学好,今天和大家分享一下如何使用 Tensorflow 复现谷歌轻量化神经网络 MobileNetV2. 在上一篇中我介绍了MobileNetV1,探讨了深度可分离卷积,感兴趣的可以看一下:ht ...

  6. TensorFlow RNN tutorial解读

    github链接 和其他代码比起来,这个代码的结构很不科学,只有一个主文件,model和train没有分开-- 参考链接: tensorflow笔记:多层LSTM代码分析 代码分析: num_step ...

  7. tensorflow RNN循环神经网络 (分类例子)-【老鱼学tensorflow】

    之前我们学习过用CNN(卷积神经网络)来识别手写字,在CNN中是把图片看成了二维矩阵,然后在二维矩阵中堆叠高度值来进行识别. 而在RNN中增添了时间的维度,因为我们会发现有些图片或者语言或语音等会在时 ...

  8. 基于tensorflow+RNN的MNIST数据集手写数字分类

    2018年9月25日笔记 tensorflow是谷歌google的深度学习框架,tensor中文叫做张量,flow叫做流. RNN是recurrent neural network的简称,中文叫做循环 ...

  9. TensorFlow RNN MNIST字符识别Demo快速了解TF RNN核心框架

    (2016-09-03 08:35:36) 转载▼     MNIST 字符数据库每个字符(0-9) 对应一张28x28的一通道图片,可以将图片的每一行(或者每一列)当作特征,共28行.则可以通过输入 ...

最新文章

  1. 深度学习中的优化算法之MBGD
  2. Android开发出现Warning:Gradle version 2.10 is required. Current version is 2.8
  3. 使用Spring Boot构建RESTFul服务
  4. docker0: iptables: No chain/target/match by that name.
  5. GDAL读取S-57海图数据中文属性值乱码问题解决(续)
  6. 电费管理系统php,25175水电费管理系统
  7. 短除法求最小公倍数c语言,短除法求最小公倍数
  8. linux上java设置内存,linux 设置java内存
  9. HTTP协议相关的网络经典五层模型
  10. 自建Kubernetes集群如何使用阿里云CSI存储组件
  11. matlab RGB到HSI的彩色转换及实现
  12. 纽约大学研究生 计算机科学 申请,纽约大学计算机科学理学硕士理学硕士研究生申请要求及申请材料要求清单...
  13. 组建无线网络的六条思路
  14. HTB Arctic[ATTCK模型]writeup系列7
  15. La Nina 年和El Nino 年 区分
  16. javascript ==等于与===恒等于
  17. 默纳克调试说明书_默纳克_NICE3000调试说明书(修改版)
  18. ERP生产管理软件(针对五金机械行业)
  19. FFmpeg环境安装及使用命令实现音视频转码
  20. Python degrees() 函数

热门文章

  1. 项目经理和team leader
  2. 懒癌患者的学习记录之小知识点
  3. Modbus功能码/异常功能码/错误码
  4. Unity 3D 中动态字体的创建
  5. Vue3入门笔记----后端框架解释
  6. H5解决IOS拍照横屏问题,含后端上传
  7. 如何通过IP地址进行精准定位
  8. AAAI2024 The Thirty-Eighth Conference on Artificial Intelligence
  9. 图书管理系统之图书管理模块(六)
  10. 基于PHP和MySQL实现的高校成绩管理系统