目录
前言
RNN(循环神经网络)
为什么要用循环神经网络(RNN)?
循环神经网络(RNN)可以处理什么类型的任务?
多对一问题
一对多问题
多对多问题
循环神经网络结构
单层网络情况
正向传播
反向传播
存在问题
优化方案
多层网络情况
双向网络结构
LSTM(Long Short Term Memory)长短期记忆网络
LSTM与RNN的不同
选择性机制的实现
LSTM模型结构
记忆单元状态
遗忘门
输入门
输出门
GRU(Gated Recurrent Unit)门控循环单元
LSTM实现简单文本分类
预处理
代码
效果截图:
前言
Stay hungry. Stay Foolish.
RNN(循环神经网络)
上一篇介绍到深度学习最经典的CNN,这一篇来研究一下深度学习同样经典的RNN。首先先研究清楚为什么我们有了CNN这么好用的神经网络后还需要循环神经网络。
为什么要用循环神经网络(RNN)?
首先上图是一张普通的神经网络模型,卷积神经网络也是这样的,输入一张图片,中间经过一个神经网络,输出他的类别。
但是上面的输入是固定输入固定输出的,比如输入一张图片,输出一个类别,那么如果拿来做输入不固定,或者输出不固定,又或者二者都不固定的任务,是不是就没辙了,比如输入是一个句子,或者要求输出是一个句子,而且输出的句子长度也不固定。循环神经网络就是用于应对这样的序列式问题,所以为了处理这一类变长的数据和序列式的问题,我们需要使用循环神经网络。
循环神经网络(RNN)可以处理什么类型的任务?
它一般可以处理三种类型的任务:1. 输入不固定,输出固定单一(多对一问题)2. 输入固定单一,输出不固定(一对多问题) 3. 输入和输出都不固定(多对多问题)
多对一问题
这样的问题的模型结构一般是这样的:
输入可能是一句话,输出是一个固定的输出,这种情况下卷积神经网络可能是无法处理的,这就需要循环神经网络,这样的问题对应到任务上有:文本分类、情感分析(给一句评论输出这句话的情感态度)
一对多问题
一对多问题的模型结构一般是这样的:
这种问题可能输入是一张图片,输出是一句话对吧,常见任务很经典的就是:Image Caption(当然输入前也是要加一层CNN抽特征的)
多对多问题
对于这种多对多的问题我们也称为序列到序列的问题(Seq2Seq),最经典的毫无疑问就是机器翻译了,输入一个源语言的句子,输出一个目标语言的句子。
循环神经网络结构
单层网络情况
下图就是循环神经网络的一个结构,可以看到有一个自指向的一个箭头,它代表一个中间状态,从头到尾的输出通过维持一个中间状态来记录之前输出的情况(也就是上下文信息),该状态和下一步的输入一起作为输入。
正向传播
===>===>
代表当前输入,代表上一步输出的隐藏状态,他们通过F函数这样的非线性变换得到这一步输出的隐藏状态。W、U、V均为参数矩阵
损失函数就是每一步的输出计算一个损失,然后求和作为损失
最终通过softmax归一化算出最终概率,正向传播的过程也就是序列从头到尾的一个过程。
反向传播
其过程是序列的尾部到头部的过程,设E为Loss的值,梯度下降现在E要对每个参数进行求偏导,保证每个方向都成下降趋势。
可以看到参数矩阵都是共享的,每一步的W都是一样的,可以看成是一个变量。
我们计算对W的偏导计算梯度 :
通过一个变换:
但对W求偏导是比较困难的,通过上面的公式可以知道是的函数:
(正向传播中的公式)
那么,知道这一点我们的公式可以变换为:
当 j = 0的时候,对W求偏导是比较容易的,那么我们的反向传播的机制就会变成如下的一个流程:
它会从当前时间的损失,一直反向传播到最开始的状态,影响到最开始的节点。
存在问题
梯度消失,或者说较远的步骤对梯度下降贡献小。
来看看这个激活函数:tanh
可以看到激活函数的导数的范围是在 [0 , 1] 之间的,那么我们上面这条公式的中间求偏导假如步骤拉的很长的话,中间有十几步连乘,那么每一个的值都在[ 0, 1 ]之间,假设他们的值都是0.1也就是10的-1次方,那么全部相乘也就是相当于10的负十几次方,这样的数微乎其微,基本可以当做消失了。那么说明我们的步骤拉的越长,对梯度的影响就越小,导致梯度消失。
优化方案
对于这种问题我们可以通过只在黄色背景框的部分进行反向传播,缩短了序列长度。提高了程序执行效率并且解决了这样的问题。
多层网络情况
多层的网络结构和单层的相似,就是再叠多几层,每一层的梯度下降还是和单层一致,输出给到下一层
好处
每一层经过一个非线性的变换,它的好处就是可以增加网络的拟合能力,神经网络的层数每多一层,它的语义层次,表达能力就更上一层楼,语义信息更强,对于具体的实际应用问题能更好拟合。
双向网络结构
我们的序列按照正向的方向输入循环神经网络,那么逆向思维,我们能不能反向输入网络中,以未来状态作为初始状态呢,答案是ok的,这种做法就是双向的网络。
可以看到上图,输入的序列分别从两个方向输入网络中。
好处
如果单向的循环神经网络,我们当前的状态只能得到上文的信息,而得不到文末的信息,通过双向的网络结构,我们可以在当前时刻获得上下文的信息,将两个隐含状态做拼接给到输出。提高语言的表达能力。
不足
无法用于实时任务,比如输入的一句话是实时产生的,一开始的时候不知道最后的几个单词是什么,那么这种情况句子不完整,就没有句末的状态。
LSTM(Long Short Term Memory)长短期记忆网络
LSTM大家可能都是非常熟悉了,随便翻几篇论文基本都能看见,那么为什么要提出LSTM,首先先来看RNN的不足之处。
刚才说到了RNN的参数共享的问题,也就是每一步的参数矩阵W是共享的,但是思考一个问题,就是一个W的矩阵将承载过多的信息,比如一个训练集有29000条句子,那么这个W矩阵要记录着29000种主谓情况,还有什么定状语从句,那么会导致一个什么问题,W矩阵信息过载,举个具体的例子:比如说句子里面有一个信息,小红去上学这一特征,但是这个特征假设对于我们整体的应用可能是没有用的,可能这一特征是有训练集有,测试集没有,那么如果我们把这些无关紧要的特征全部都记下来了,那我们的模型就变成了只能拟合训练集,在其他的测试集上表现不好导致过拟合,而且还有过载的问题就是这一个W的矩阵参数有限可能记录不了那么多的状态,有些不重要的信息记下来了,重要的可能记不下来了。
那么我们RNN的局限性就比较大了,每一句话过来都要记住全部的信息,对于我们的参数而言也有些负载,于是引入了LSTM选择性机制。
LSTM与RNN的不同
LSTM是在RNN的单元上进行扩展,加入选择性机制:
选择性输入选择性遗忘选择性输出
选择性机制的实现
通过"门"机制,比如遗忘门等,其实际是通过Sigmoid的函数,Sigmoid的输出是0或者1,那么当一个值经过这个Sigmoid后若输出是0,选择将其遗忘,输出是1的话,选择将其记下来。
LSTM模型结构
上面是一张经典的LSTM的结构图,可以看到它的大体结构和RNN的模型结构式一样的,只是在每个单元里面做了一些工作。
为了理解模型图先来看看每个组件代表什么意思,黄色的框代表一个网络结构,粉色的圆形代表点积操作,一个箭头代表向量传递,第四个代表向量拼接,最后一个代表向量copy。
记忆单元状态
这里就代表一个隐含状态。隐含状态会经过一个点乘,也就是一个遗忘门,后面还会经过一个相加,是加入输入的信息,然后传给下一个状态。
遗忘门
是上一个单元的输出,是当前状态的输入,和同时作为输入输入到遗忘门中,经过遗忘门会得到0和1的向量,和上一个隐含状态做点积,物理意义可以理解为:当输入一些新的东西之后,模型会考虑遗忘之前的那些东西。
输入门
输入仍是上一个单元的输出和当前的输入,可以看到左边的门机制是传入门,控制传入的信息,其机制和遗忘门类似,而右侧的tanh看他的公式和RNN的隐含状态的公式差不多:
输入通过这样的激活函数得到一个新的隐含状态,然后在通过传入门输出的 i 进行选择,他从物理意义上也就是一句话的语义有一部分要输入有一部分不要输入,选择后和隐含状态进行相加(融入隐含状态中)
输出门
和上面两个门的机制一样,输出门是选择性输出,输入通过一个门机制选择那些东西要输出,计算出选择向量,然后和融合完的隐含状态做点积,得到输出
上面就是所谓的LSTM模型结构。
GRU(Gated Recurrent Unit)门控循环单元
考虑到LSTM网络输入们和遗忘门之间有一些互补的关系,因此同时使用两个门机制显得有些冗余。
于是思考将输入门和遗忘门进行合并,合并的思路如下:
通过一个门机制互补的形式合并输入门和遗忘门,并将其叫做更新门
GRU的结构图如下:
1.输入门与和遗忘门合并成一个门:更新门 zt:
左侧的红色矩形框相当于原本的遗忘门,通过门机制点积上一个隐含状态,Sigmoid的取值为0或1,可以决定哪些特征遗忘,哪些特征保留。右侧的红色椭圆框代表原本的输入门,通过门机制去决定哪些需要输入,哪些不需要输入。
2.引入重置门 ,用来控制输入的内容的计算是否依赖上一时刻的状态,决定上一个隐含状态,对当前的输入内容有多少影响。这么说可能有点抽象,大家可能就会有一个疑问:那么我不是有输入门(现融进更新门)控制哪些需要输入哪些不用输入吗?那这个重置门又来控制一遍,不是做了重复工?
解决疑问:OK,这个问题也是我最初的问题,后来思考明白了,首先我们来看输入内容的公式
可以看到这里是乘以,那么可以说明什么?说明重置门控制的是上一个隐含状态在输入内容里面的占比
而输入门的公式是怎么样的?
输入门是乘以整一个输入内容,那么两者控制的内容是不是不一样?
如果还是很迷可以这么来看,输入门控制的是这个整体的比例,而重置门控制的是中ht-1这一个变量的比例。
3.去除 LSTM 中的内部细胞记忆单元 , 直接在当前状态 和历史状态 之间引入线性依赖关系
LSTM实现简单文本分类
预处理
数据集:
格式
类别 \t 一段一百字左右的中文新闻\n
样本截图:
预处理分词后结果:
数据集及预处理完毕文件下载:
链接:/s/1H_xSJvfoS3r2bS97KtFsaQ
提取码:i2h6
代码
版本:
tensorflow 1.13.1
python 3.6
代码逐行注释,所以不做文字说明:
import tensorflow as tfimport osimport sysimport numpy as npimport math# 打印日志tf.logging.set_verbosity(tf.logging.INFO)def get_default_params():# 返回对象return tf.contrib.training.HParams(# 词向量大小num_embedding_size=16,# 步长num_timesteps=50,# lstm单元输出维度(又叫输出神经元数)num_lstm_nodes=[32, 32],# 层数num_lstm_layers=2,# 全连接输出维度,最后一维num_fc_nodes=32,batch_size=100,# 控制梯度,梯度上线clip_lstm_grads=1.0,# 学习率learning_rate=0.001,# 词频最低下限num_word_threshold=10)# 设置参数hps = get_default_params()train_file = 'data/new_train_seg.txt'val_file = 'data/new_val_seg.txt'test_file = 'data/new_test_seg.txt'vocab_file = 'data/new_vocab.txt'category_file = 'data/new_category.txt'class Vocab:def __init__(self, filename, num_word_threshold):# 词典self._word_to_id = {}# <UNK> 的id(初始值)self._unk = -1# 频率下限self._num_word_threshold = num_word_threshold# 将词典读出来存到dict里self._read_dict(filename)def _read_dict(self, filename):with open(filename, 'r', encoding="utf-8") as f:lines = f.readlines()for line in lines:word, frequency = line.strip('\r\n').split('\t')frequency = int(frequency)# 低于下限不要if frequency < self._num_word_threshold:continueidx = len(self._word_to_id)if word == '<UNK>':# 刷新UNK的idself._unk = idxself._word_to_id[word] = idxdef word_to_id(self, word):# 如果没有word返回UNKreturn self._word_to_id.get(word, self._unk)@propertydef unk(self):return self._unkdef size(self):# 返回大小return len(self._word_to_id)def sentence_to_id(self, sentence):# 分词进字典里吧id取出来变成listword_ids = [self.word_to_id(cur_word) for cur_word in sentence.split()]return word_idsclass CategoryDict:def __init__(self, filename):self._category_to_id = {}# 读取类别并存入字典,给每个一个idwith open(filename, 'r', encoding="utf-8") as f:lines = f.readlines()for line in lines:category = line.strip('\r\n')idx = len(self._category_to_id)self._category_to_id[category] = idxdef size(self):return len(self._category_to_id)def category_to_id(self, category):# 传入类别返回idif not category in self._category_to_id:print("%s is not in our category list" % category)return self._category_to_id[category]# 建立词典vocab = Vocab(vocab_file, hps.num_word_threshold)vocab_size = vocab.size()# 打日志tf.logging.info('vocab_size: %d' % vocab_size)# 建立类别词典category_vocab = CategoryDict(category_file)num_classes = category_vocab.size()tf.logging.info('num_classes: %d' % num_classes)class TextDataSet:def __init__(self, filename, vocab, category_vocab, num_timesteps):self._vocab = vocabself._category_vocab = category_vocabself._num_timesteps = num_timesteps# matrixself._inputs = []# vectorself._outputs = []self._indicator = 0self._parse_file(filename)def _parse_file(self, filename):tf.logging.info('Loading data from %s', filename)with open(filename, 'r', encoding="utf-8") as f:lines = f.readlines()for line in lines:label, content = line.strip('\r\n').split('\t')# 将传入的类别转化为对应的idid_label = self._category_vocab.category_to_id(label)# 将传入的句子转化为一个词一个词对应的idid_words = self._vocab.sentence_to_id(content)# 控制句长在50,超过截断id_words = id_words[0: self._num_timesteps]# paddingpadding_num = self._num_timesteps - len(id_words)id_words = id_words + [self._vocab.unk for i in range(padding_num)]# 将句子的向量放到输入listself._inputs.append(id_words)# 将类别的向量放到输出的listself._outputs.append(id_label)self._inputs = np.asarray(self._inputs, dtype=np.int32)self._outputs = np.asarray(self._outputs, dtype=np.int32)self._random_shuffle()self._num_examples = len(self._inputs)def _random_shuffle(self):# 整个input的顺序p = np.random.permutation(len(self._inputs))self._inputs = self._inputs[p]self._outputs = self._outputs[p]def num_examples(self):# 返回输入list的长度return self._num_examplesdef next_batch(self, batch_size):# 获取下一个batchend_indicator = self._indicator + batch_size# 当获取的指针超过数据集大小时,归0if end_indicator > len(self._inputs):self._random_shuffle()self._indicator = 0end_indicator = batch_size# 归0还超过就报错if end_indicator > len(self._inputs):print("batch_size: %d is too large" % batch_size)# 取值batch_inputs = self._inputs[self._indicator: end_indicator]batch_outputs = self._outputs[self._indicator: end_indicator]self._indicator = end_indicatorreturn batch_inputs, batch_outputstrain_dataset = TextDataSet(train_file, vocab, category_vocab, hps.num_timesteps)val_dataset = TextDataSet(val_file, vocab, category_vocab, hps.num_timesteps)test_dataset = TextDataSet(test_file, vocab, category_vocab, hps.num_timesteps)# 创建模型def create_model(hps, vocab_size, num_classes):num_timesteps = hps.num_timestepsbatch_size = hps.batch_size# 占位,当run的时候赋值inputs = tf.placeholder(tf.int32, (batch_size, num_timesteps))outputs = tf.placeholder(tf.int32, (batch_size,))# dropoutkeep_prob = tf.placeholder(tf.float32, name='keep_prob')# 保存模型运行进度global_step = tf.Variable(tf.zeros([], tf.int64), name='global_step', trainable=False)# embedding的值初始化到-1到1embedding_initializer = tf.random_uniform_initializer(-1.0, 1.0)# 上下文管理器with tf.variable_scope('embedding', initializer=embedding_initializer):embeddings = tf.get_variable('embedding',[vocab_size, hps.num_embedding_size],tf.float32)#在embedding的命名空间内创建名字为embedding的变量# [1, 10, 7] -> [embeddings[1], embeddings[10], embeddings[7]]embed_inputs = tf.nn.embedding_lookup(embeddings, inputs)# 初始化随机值scale = 1.0 / math.sqrt(hps.num_embedding_size + hps.num_lstm_nodes[-1]) / 3.0lstm_init = tf.random_uniform_initializer(-scale, scale)with tf.variable_scope('lstm_nn', initializer=lstm_init):# 双层所以写一个listcells = []for i in range(hps.num_lstm_layers):# 先设置每一层的cell = tf.contrib.rnn.BasicLSTMCell(# 输出神经元的数量,也就是输出一个32维的向量hps.num_lstm_nodes[i],state_is_tuple=True)# dropoutcell = tf.contrib.rnn.DropoutWrapper(cell,output_keep_prob=keep_prob)# 双层的,将每一层加入列表cells.append(cell)# 合并两层cell = tf.contrib.rnn.MultiRNNCell(cells)# 初始化隐含状态为0initial_state = cell.zero_state(batch_size, tf.float32)# rnn_outputs => [batch_size, num_timesteps, lstm_outputs[-1](32)]rnn_outputs, _ = tf.nn.dynamic_rnn(cell, embed_inputs, initial_state=initial_state)# 取最后一步# [batch_size, 1, lstm_outputs[-1](32)]last = rnn_outputs[:, -1, :]# 输出和全连接层拼接fc_init = tf.uniform_unit_scaling_initializer(factor=1.0)with tf.variable_scope('fc', initializer=fc_init):# 上面lstm的cell的输出拿过来# 激活函数用relu# 第二个参数是输出维度大小,就是最后一个参数,这里是没有变化的fc1 = tf.layers.dense(last,hps.num_fc_nodes,activation=tf.nn.relu,name='fc1')# dropoutfc1_dropout = tf.contrib.layers.dropout(fc1, keep_prob)# [batch_size, 1, lstm_outputs[-1](32)]# 在经过一层全连接映射到多少个类别上 [batch_size, 1, class_size]# [batch_size, 1 , 10]logits = tf.layers.dense(fc1_dropout,num_classes,name='fc2')with tf.name_scope('metrics'):# 做了softmax并且计算了差值,如果单单softmax => tf.nn.softmax(self.logits)softmax_loss = tf.nn.sparse_softmax_cross_entropy_with_logits(logits=logits, labels=outputs)# 计算均值loss = tf.reduce_mean(softmax_loss)# [0, 1, 5, 4, 2] -> argmax: 2(下标)# 这里第一个参数就是softmax,第二个参数意思是取第几维最大,比如是1的话就是在第一维里去最大y_pred = tf.argmax(tf.nn.softmax(logits),1,output_type=tf.int32)# 计算准确率,看看算对多少个correct_pred = tf.equal(outputs, y_pred)# tf.cast 将数据转换成 tf.float32 类型accuracy = tf.reduce_mean(tf.cast(correct_pred, tf.float32))with tf.name_scope('train_op'):# 获得所有可训练的变量(优化器优化列表中的变量)tvars = tf.trainable_variables()# 看看这些变量叫啥for var in tvars:tf.logging.info('variable name: %s' % (var.name))# tf.gradients求导(梯度下降,对每个训练变量求偏导,每个方向都往下走)# 不超过hps.clip_lstm_grads,防止梯度爆炸grads, _ = tf.clip_by_global_norm(tf.gradients(loss, tvars), hps.clip_lstm_grads)# Adam优化器optimizer = tf.train.AdamOptimizer(hps.learning_rate)# 这里是应用,将梯度grads应用到变量上,让函数吧global_step回调train_op = optimizer.apply_gradients(zip(grads, tvars), global_step=global_step)return ((inputs, outputs, keep_prob),(loss, accuracy),(train_op, global_step))placeholders, metrics, others = create_model(hps, vocab_size, num_classes)inputs, outputs, keep_prob = placeholdersloss, accuracy = metricstrain_op, global_step = othersdef eval_holdout(sess, accuracy, dataset_for_test, batch_size):# 数据集大小/batch_size 其实就是iter,校验集拿来校验一轮num_batches = dataset_for_test.num_examples() // batch_sizetf.logging.info("Eval holdout: num_examples = %d, batch_size = %d",dataset_for_test.num_examples(), batch_size)accuracy_vals = []for i in range(num_batches):batch_inputs, batch_labels = dataset_for_test.next_batch(batch_size)accuracy_val = sess.run(accuracy,feed_dict = {inputs: batch_inputs,outputs: batch_labels,keep_prob: 1.0,})accuracy_vals.append(accuracy_val)# 计算平均准确率return np.mean(accuracy_vals)init_op = tf.global_variables_initializer()# dropouttrain_keep_prob_value = 0.8# 跑一万个batchnum_train_steps = 10000with tf.Session() as sess:sess.run(init_op)for i in range(num_train_steps):batch_inputs, batch_labels = train_dataset.next_batch(hps.batch_size)# 那三个占位符输进去# 计算loss, accuracy, train_op, global_step的图outputs_val = sess.run([loss, accuracy, train_op, global_step],feed_dict={inputs: batch_inputs,outputs: batch_labels,keep_prob: train_keep_prob_value,})loss_val, accuracy_val, _, global_step_val = outputs_val# 两百个batch输出一次if global_step_val % 200 == 0:tf.logging.info("Step: %5d, loss: %3.3f, accuracy: %3.3f"% (global_step_val, loss_val, accuracy_val))# 一千个batch校验一次if global_step_val % 1000 == 0:accuracy_eval = eval_holdout(sess, accuracy, val_dataset, hps.batch_size)accuracy_test = eval_holdout(sess, accuracy, test_dataset, hps.batch_size)tf.logging.info("Step: %5d, val_accuracy: %3.3f, test_accuracy: %3.3f"% (global_step_val, accuracy_eval, accuracy_test))
效果截图:
以上是对循环神经网络的粗浅认识
本菜鸟学习不好,如有不妥望各位大佬指点
如要转载请说明原文:/qq_36652619/article/details/95952068
自然语言菜鸟学习笔记(七):RNN(循环神经网络)及变体(LSTM GRU)理解与实现(TensorFlow)