Time:2021-07-16
Author:雾雨霜星
我的网站:雾雨霜星
开始学习新的神经网络算法了,循环神经网络。越来越多的实践,我却从中越来越感到自己不懂编程,或许是我不够聪明,或许是我不适合从事这样的工作?居然会从中稍微感到失落。
本文主要记录了我使用Tensorflow的Keras接口训练循环神经网络的过程。
循环神经网络(RNN,Recurrent Neutral Network):每次输入的计算结果都与之前的计算有关,这样的神经元构成的神经网络。
以上是我想到的暂时对RNN比较好的一个概括方法。
Keras官方文档给出的RNN定义:
Recurrent neural networks (RNN) are a class of neural networks that is powerful for modeling sequence data such as time series or natural language.
Schematically, a RNN layer uses a for loop to iterate over the timesteps of a sequence, while maintaining an internal state that encodes information about the timesteps it has seen so far.
翻译:
RNN是一类对时间序列或自然语言等序列数据建模功能强大的神经网络。
示意图上,RNN层使用for循环在序列的时间步上迭代,同时保持一个内部状态,该状态对迄今为止看到的时间步信息进行编码。
RNN应用对象:序列数据(2D张量),[timesteps, feature]。每条输入数据的第一个维度代表了时间步,第二维度代表特征。
文本数据的序列形式:对于一条字符串文本,进行数据处理后得到形式为序列的数据。即每个字符占一行,每行是该字符的特征形式,这种特征形式可以是以one-hot编码得到的二进制序列,也可以是通过使用词嵌入得到的密集向量,例如:
以一条文本数据为例子:
数据:"Hey,Hello world!"
1.分词为 "Hey"、"Hello"、"World"
2.对每个词进行编码:
2-1.one-hot编码:"Hey"=[1,0,0],"Hello"=[0,1,0],"World"=[0,0,1]
2-2.词嵌入编码:"Hey"=[12,2.2,0,3],"Hello"=[0.1,2,3.2,44],"World"=[0.9,32,13,0.44]
3.转化:"Hello world" = [[1,0,0],[0,1,0],[0,0,1]] / [[12,2.2,0,3],[0.1,2,3.2,44],[0.9,32,13,0.44]]
可见:文本数据转化为了序列数据,其中第一维度可以认为是“时间步”(人眼进行阅读看到单词的先后),第二维度是“特征”(描述字符的方法)
实现RNN的关键是每个神经元的实现,神经元需要具备以下两个特征:
简单的RNN神经元计算方法如下:
O t = g ( V ⋅ S t ) S t = f ( W ⋅ X t + U ⋅ S t − 1 ) O_t=g(V \cdot S_t)\\ S_t=f(W \cdot X_t+U \cdot S_{t-1}) Ot=g(V⋅St)St=f(W⋅Xt+U⋅St−1)
其中O(t)代表了t时刻输入下的输出结果。
可以认为是一种环形的结构,即输出量(或可以反应输出的计算过程量)会反馈进入输入中。
最基本的RNN,本质就是一个具有units个神经元,且可以记录每次计算状态量的隐藏层。对于输入维度为n,时间序列长度为T的数据。总共计算T次,每次计算是对第t(t Keras官方文档对RNN基类的输出描述如下: return_state、return_sequences两个参数一般在使用时都不会进行设置,默认为False。此时的返回是:[batch_sizem, units]。仅返回最后一个时间序列输入计算得到的结果。 如果设置了return_sequences=True,那么会得到[batch_sizem, timesteps,units],即每个时间序列的输出组合。 对于每个输入RNN的样本,基础的RNN模型,对此时间序列长度为timesteps的样本计算timesteps次,每次得到一个输出yt,组合起来就得到了timesteps个yt,而每个yt的维度,取决于参数units,此units为隐藏层神经元的个数。 在初始化时就需要指定units的大下,以keras的API为例子: 对[timesteps, feature]样本,每个时间点数据为features维度向量进行全连接计算得到units维向量,计算timesteps次就得到了[timesteps, units]。 采用基于时间步的反向传播算法(BPTT)进行训练(更新参数)。 BPTT:Back-Propagation Through Time。使用链式法则进行反向传播时,对计算St的式子中的参数矩阵进行更新,求导时会涉及到上一时刻状态量,因此在链式法则中需要不断顺着t进行求导直至t=0。 多元函数链式法则的乘法求导应该描述如下: 上述的描述方法的认识非常重要,对于后面BPTT的推导,由此可以得到最为普遍的形式。 而我之前一直没有推导出来,就是因为对这一步的认识不够清晰。 假设总的误差函数为L,每个时刻计算所得输出Ot的误差函数Lt。 由上述的RNN前向传播的方程,如下: 其中不同时刻的误差函数对参数U的求导,需要回溯到之前时刻的数据。这是因为U参数不仅仅表现与此次St的更新,对St-1也有影响,即St-1是U的函数。 考虑简单的状况,先考虑L3下的对U参数求导,使用链式法则可以得到: 需要注意的是,这里已经应用了求导法则,在具体的求导时无需再分步骤求导。即这里已经将Zt中的U和St-1分开了,所以计算方法如下: 由此,可以总结得到: 认识到上述公式这一步非常重要! 我曾经推导多次无果,是因为我没有正确使用链式法则,没有连接到最终的参数就直接展开了。主要是,应该说我不懂链式法则中的乘法求导吧,居然是分开各自相加。所有之前一直推导不到常见的形式。 曾经的推导如下: 根据导数的乘法法则: 从上述参数U更新的公式来看: 由于普通RNN中连乘项不会被消去,因此随着深度的增加,会连乘激活函数的导数。 连乘小于1的数,可以看做是幂级别的计算,使得梯度变得很小,几乎接近于0。这就是梯度消失。 实际上,若参数U非常大,tanh或许会无限接近1,但是此时参数U已经非常大了,连乘小梯度会趋于无穷,从而引起梯度爆炸。 避免梯度消失和梯度爆炸较好的方法是使用LSTM或者GRU,本质就是引入门控制,使得连乘项接为0或者为1。 LSTM:Long Short-Term Memory Network,长短时记忆网络。 相对于普通的RNN,LSTM增加了一个信息传送通道,这条信息通道模拟"遗忘"、“记忆”、"输出"三个阶段。 其中的zf,zi,zo分别代表了遗忘门、输入门、输出门,他们的值都是0或者1。 执行流程如下: 可见,LSTM不仅沿着时间传递输出计算状态(以hidden_state为载体),还传递了一个cell_state,可以理解为:hidden_state代表了近期记忆,而cell_state则代表了远期记忆。 LSTM通过使用遗忘门和输出门,使得在参数更新过程中,连乘项ht对ht-1的偏导ct对ct-1的偏导,变为0或者1,从而避免梯度消失和梯度爆炸。 GRU:Gate Recurrent Unit。门控循环单元。 也是为了解决RNN反向传播中的梯度等问题而提出来的变体。 优势:GRU内部结构相对于LSTM而言较为简单,其所需要的计算代价也更小。 GRU计算过程如下: Empirical Evaluation of Gated Recurrent Neural Networks on Sequence Modeling(arxiv.org) 使用的是IMBD的电影评论数据,每条评论数据被记录在一个txt文本文件中。数据的标签类型分为积极评价和消极评价两种。 数据来源:https://mng.bz./0tIo 数据集格式:有train和text两个文件夹,每个文件夹里面有pos和neg两个文件夹,其中每个文件夹里面又有许多txt文本文件。 数据处理的目标:将读入的字符串数据通过分词器转化为整数索引数据序列。 数据的导入和处理基本步骤: 首先使用Python的os模型进行文件的获取,使用open方法打开文件,读取文件内容: 其中trainDataPath是数据集train文件夹的路径,trainData和trainLabel是一个初始化时为空的列表。 猜测可能是出现了超出gbk编码的字符,在网上看到的方法都是加忽略错误属性。 然后是制作文本数据的字典: 其中的trainData就是上述读取数据得到的以string格式评论数据为元素的列表。 使用Keras的分词器(Tokenizer),需要确定字词数量,也就是制作的字典中最多包含多少个字。 Tokenizer使用fit_on_texts方法完成字典的制作,输入的是相应的文本列表(每个元素是字符串)。 获取Keras分词器Tokenizer的字典: 返回得到一个字典,此字典是分词器的训练结果,键是相应的单词,值是单词对应的数字(int)。 然后是进行序列转化,即使用训练后的分词器,把字符串转化为数字序列: 然后是进行数字序列的填充,使得所有序列数据的长度一致。使用Keras内置的方法: 再将标签列表转化为numpy矩阵,然后使用numpy的arange方法创建等差数组,让后使用numpy的random.shuffle方法将此等差数组打乱顺序,把此打乱顺序后的等差数组应用到numpy矩阵上: 最后划分验证集: 词向量:在对文本数据进行分词后,可将每个单词映射为一个向量,这个向量即使词向量。 词嵌入:是密集的词向量,与one-hot编码不同,是低维的浮点数向量。 嵌入层(Embedding layer):神经网络中用于训练词嵌入空间,将输入序列数据转化为密集向量数据的层。 官方文档对嵌入层的解释: 从此定义可知,我们要对文本数据进行分词后,把每个词转化为整数索引,而每个文本序列数据变为整数序列数据,再使用嵌入层,得到密集词向量数据。这也是为什么前面数据处理,需要使用分词器Tokenizer,以及对序列进行填充。 并且该层只能是在神经网络的第一层进行使用。 可以使用外部已经预训练好的词嵌入空间,从而减少训练的任务量。著名的词嵌入空间是:word2vec和GloVe。 创建时需要确定三个参数:input_dim, output_dim, input_length input_dim:输入序列的维度,即总共有多少个正整数索引值。 output_dim:输出序列的维度,即每个正整数索引值转化为密集向量的向量维度。 input_length:输入序列的长度。输入的序列是一个长度为input_length的一维numpy矩阵。 下载词嵌入数据GloVe:glove.6B.zip GloVe官网:GloVe: Global Vectors for Word Representation (stanford.edu) 下载后解压文件得到glove.6B.50d、glove.6B.100d、glove.6B.200d、glove.6B.300d四个文件,分别代表:50维度词嵌入空间、100维度词嵌入空间、200维度词嵌入空间、300维度词嵌入空间。 词嵌入空间的文本文件格式:每行记录一个单词的词向量,即:单词 + 词嵌入向量,使用空格隔开单词和每个维度的词向量值。 使用Python的open函数打开相应词嵌入空间文件,按照每行进行读取,使用分词器的字典对应起来每个词的整数索引和词向量: 这里需要制作的是词嵌入矩阵embedding_matrix,每一行对应一个词语,每一行的长度即为词嵌入空间中每个词向量维度。 对词嵌入矩阵初始化时,大小设置为(max_word, embedding_dim)。max_word即分词器最大分词数,embedding_dim即词向量维度。 按照上面的步骤得到了词嵌入矩阵embedding_matrix。 这个词嵌入矩阵,每一行代表一个分词器所得的词,而行序号就是分词器制作的字典的整数索引,此行的数据就是该词在GloVe词嵌入空间中的词向量。 使用Keras.layers的set_weights方法设置层参数,而嵌入层的参数正是词嵌入矩阵。 需要注意的是,set_weights必须在该层add进入model后才能正常使用。 如果按照如下的方法: 会出现报错: ValueError: You called 大概是和set_weights(weights)方法自身传入参数的过程有关。 在Keras的Layers API中,提供的循环神经网络层主要API有三种: SimpleRNN是由最基本的RNN神经元构成的Layer。 此外Keras还提供了各种循环神经元,包括SimpleRNNCell、GRUCell、LSTMCell,可以直接使用神经元自己设计层。 主程序如下: 在getData函数中获取训练集、验证集和测试集,全部都是已经进行数据处理,从字符序列转化为了数字序列且进行了填充,同时还获取了Keras分词器得到的字典,用于输入getEmbeddingMat函数中获取指定词嵌入向量空间的词嵌入矩阵。 getModel函数获取训练的模型后,与以往使用全连接神经网络的做法一样,进行model的编译后训练即可。 以下为data.py的程序: 以下为model.py的程序: 其中提供给layers.SimpleRNN的输入参数是指多少个RNN神经元,即深度。该参数决定了输出张量的长度。 注意,如果是使用多层RNN的时候,需要设置参数return_sequences=True,否则会报错。这是因为SimpleRNN层默认输出的是最后一次(最后时刻)输入得到的计算结果。设置该参数后,会将之前每一时刻输入计算得到的值也放到层的输出中。 而参数dropout则用于设置防止过拟合。 另外需要说明的是,Keras提供的RNN Layer API有两个dropout参数: dropout:对应用在输入上的线性变换矩阵的参数丢失率 recurrent_dropout:对应用在循环状态上的线性变换矩阵的参数丢失率 在模型的搭建中,Dense层直接放在了SimpleRNN层后面。通过model.summary可以看到: Model: “sequential” Layer (type) Output Shape Param # embedding (Embedding) (None, 200, 100) 1000000 simple_rnn (SimpleRNN) (None, 32) 4256 dense (Dense) (None, 1) 33 可见simple_rnn 输出是一个2D张量(包括batch_size),考虑到每时刻输入数据是一个高维行向量(每个时刻输入某个字符而该字符数字序列维度即前一层词嵌入空间的维度),可以理解为在上述描述RNN的基本公式中,V矩阵最终是一个行向量,从而输出得到1D张量,因此整个batch是一个2D张量。 而通过使用词嵌入,每条被转化为数字序列的评论数据,都变为一个2D张量(200×100矩阵)。 LSTM和GRU本质只是和SimpleRNN的神经元在传输通道上存在差异,而它们同属于RecurrentLayer,使用方法是一样的。 因此,使用LSTM只需要在上述模型中修改如下: 同理,使用GRU也只需要修改add中调用的layersAPI: 其余地方完全一致都行。 对于layers.SimpleRNN、layers.LSTM、layers.GRU三个API,均有一个可选的参数return_state可调用,通常默认值为False。 调用: 文档的解释: 如果设置为True,那么在计算后不仅会返回输出结果,还会返回RNN的状态,对于LSTM与GRU而言,会返回hidden_state与cell_state。 从tensorflow.keras.layers.LSTM源码可见: 其中new_h即最终的hidden_state(ht),而new_c是最终的cell_state(ct)。对应了LSTM模型计算公式: 使用IMDB电影评论数据,使用100维度的GloVe预训练词嵌入空间。 分布使用SimpleRNN、LSTM、GRU训练12轮,得到效果如下: SimpleRNN的训练参数是最少的,但是训练的速度却是最慢的。LSTM和GRU的训练速度明显比SimpleRNN的好很多,训练也比较快。至于为什么我现在都还没有想明白。 尽管只训练了12轮,但是从绘制的曲线来看,他们的过拟合在第10/11轮左右就开始体现了。 其实这个训练效果并不是很好,书上说这是因为这个分类任务交给全连接神经网络更加合适,而RNN更加适合分析序列的长期性结构,对情感分类帮助不大。 数据集来源:https://s3.amazonaws.com/keras-datasets/jena_climate_2009_2016.csv.zip 通过一段时间的气象数据,来预测指定延后时间的气象温度。 数据集文件在Windows系统中可以使用Excel打开。 共有420452行15列,每一行是一个时刻记录的各种气象指标数据,第一列是时间,其余列是各种气象指标(14种)。 从时间列的变化来看可知,气象数据每10min记录一次,是2009-2016年间记录的全部数据。 类似于使用Keras的preprocessing.image_dataset_from_directory,使用一个迭代器每次获取一个batch的数据,而不是将数据全部载入(这次使用的数据相当于420452*15的浮点数矩阵) 每次读取数据都要考虑需要回溯之前时间的数据,而目标值是最后读到的样本数据的延后指定时间的数据。 我自己写的迭代器,是通过外面指定相应的索引值,生成随机数序列,用于从指定索引值范围内打乱数据,从csv文件中读取。 代码如下: 注意:按照书上的示例代码,其进行训练时的数据集,每一行代表一个时间节点下全部特征的数据,而每一列是一个特征随时间变化的序列。 训练速度非常慢,跑了一个下午才跑完这个demo… 在设置了recurrent_dropout后,会发现loss非常大,在我一开始设置dropout=0.2,recurrent_dropout=0.2,loss甚至达到了10亿(不止了,20位数以上)。。。。。。 设置recurrent_dropout=0.1下,一开始loss也有几千,但是一直在降低,最终去到1以下。 也不知道到底是否有问题,总之还是令人非常不安。 NotImplementedError: Cannot convert a symbolic Tensor (2nd_target:0) to a numpy array 出现状况:在使用Keras的SimpleRNN进行训练时出现,指示我的嵌入层后的第一个RNN层有问题。 无法将符号张量(简单/跨步切片:0)转换为numpy数组。 还以为是自己的编程有问题,其实是numpy版本和tensorflow版本的对应问题,原本使用的是1.21.2,在降低版本到1.19.5后就正常。 详细参考: python-NotImplementedError: Cannot convert a symbolic Tensor (2nd_target:0) to a numpy array-Stack Overflow ValueError: Input 0 of layer simple_rnn_1 is incompatible with the layer: expected ndim=3, found ndim=2. Full shape received: [None, 32] 出现状况:使用Keras的SimpleRNN设置多层RNN Layer时进行训练出现。 原因在于我没有设置return_sequences参数,将该参数设置为True即可。 UnknownError: Fail to find the dnn implementation. [Op:CudnnRNN] 出现状况:使用德国耶拿研究所气象站数据跑demo时 本质是没有配置GPU的显存分配。由于序列数据本质也是二维矩阵数据(time,feature),因此模型会使用GPU来计算也很正常。 这个错误出现时,在PyCharm的控制台输出处有一大段信息,比较关键的信息摘录如下: 解决方法就是配置GPU显存分配: LSTM简介:人人都能看懂的LSTM - 知乎 (zhihu.com) 零基础入门深度学习(5) - 循环神经网络 - 作业部落 Cmd Markdown 编辑阅读器 (zybuluo.com) 转载请注明出处:https://www.shuangxing.top/#/输出
Output shape:
* If return_state:
a list of tensors. The first tensor is the output. The remaining tensors are the last states, each with shape [batch_size, state_size], where state_size could be a high dimension tensor shape.
* If return_sequences:
N-D tensor with shape [batch_size, timesteps, output_size], where output_size could be a high dimension tensor shape, or [timesteps, batch_size, output_size] when time_major is True.
Else, N-D tensor with shape [batch_size, output_size], where output_size could be a high dimension tensor shape.
layer = tensorflow.keras.layers.LSTM(units=16)
RNN的训练方法
多元函数链式法则(乘法求导):
y = Y [ f ( x 1 , x 2 ) ] f ( x 1 , x 2 ) = g 1 ( x 1 , x 2 ) ⋅ g 2 ( x 1 , x 2 ) ∂ y ∂ x 1 = ∂ y ∂ f ∂ f ∂ g 1 ∂ g 1 ∂ x 1 + ∂ y ∂ f ∂ f ∂ g 2 ∂ g 2 ∂ x 1 y=Y[f(x_1, x_2)]\quad f(x_1, x_2)=g_1(x_1, x_2)\cdot g_2(x_1, x_2)\\ \frac{\partial y}{\partial x_1}=\frac{\partial y}{\partial f}\frac{\partial f}{\partial g_1}\frac{\partial g_1}{\partial x_1}+\frac{\partial y}{\partial f}\frac{\partial f}{\partial g_2}\frac{\partial g_2}{\partial x_1} y=Y[f(x1,x2)]f(x1,x2)=g1(x1,x2)⋅g2(x1,x2)∂x1∂y=∂f∂y∂g1∂f∂x1∂g1+∂f∂y∂g2∂f∂x1∂g2BPTT
O t = g ( V ⋅ S t ) S t = f ( W ⋅ X t + U ⋅ S t − 1 ) Z t = W ⋅ X t + U ⋅ S t − 1 O_t=g(V \cdot S_t)\\ S_t=f(W \cdot X_t+U \cdot S_{t-1})\\ Z_t=W \cdot X_t+U \cdot S_{t-1} Ot=g(V⋅St)St=f(W⋅Xt+U⋅St−1)Zt=W⋅Xt+U⋅St−1
设L为误差函数,则可根据不同时刻的输出进行分解:
L = ∑ t = 0 T L t ∂ L ∂ U = ∑ t = 0 T ∂ L t ∂ U L=\sum_{t=0}^{T} L_t\\ \frac{\partial L}{\partial U}=\sum_{t=0}^{T}\frac{\partial L_t}{\partial U}\\ L=t=0∑TLt∂U∂L=t=0∑T∂U∂Lt
∂ L 3 ∂ U = ∂ L 3 ∂ O 3 ∂ O 3 ∂ U = ∂ L 3 ∂ O 3 ∂ O 3 ∂ S 3 ∂ S 3 ∂ U + ∂ L 3 ∂ O 3 ∂ O 3 ∂ S 3 ∂ S 3 ∂ S 2 ∂ S 2 ∂ U + ∂ L 3 ∂ O 3 ∂ O 3 ∂ S 3 ∂ S 3 ∂ S 2 ∂ S 2 ∂ S 2 ∂ S 1 ∂ U \frac{\partial L_3}{\partial U}=\frac{\partial L_3}{\partial O_3}\frac{\partial O_3}{\partial U}=\frac{\partial L_3}{\partial O_3}\frac{\partial O_3}{\partial S_3}\frac{\partial S_3}{\partial U} + \frac{\partial L_3}{\partial O_3}\frac{\partial O_3}{\partial S_3}\frac{\partial S_3}{\partial S_2}\frac{\partial S_2}{\partial U} + \frac{\partial L_3}{\partial O_3}\frac{\partial O_3}{\partial S_3}\frac{\partial S_3}{\partial S_2}\frac{\partial S_2}{\partial S_2}\frac{\partial S_1}{\partial U} ∂U∂L3=∂O3∂L3∂U∂O3=∂O3∂L3∂S3∂O3∂U∂S3+∂O3∂L3∂S3∂O3∂S2∂S3∂U∂S2+∂O3∂L3∂S3∂O3∂S2∂S3∂S2∂S2∂U∂S1
这里没有把Zt展开,代入也是一样的:
∂ S 3 ∂ U = ∂ S 3 ∂ Z 3 ∂ Z 3 ∂ U ∂ S 3 ∂ S 2 ∂ S 2 ∂ U = ∂ S 3 ∂ Z 3 ∂ Z 3 ∂ S 2 ∂ S 2 ∂ Z 2 ∂ Z 2 ∂ U \frac{\partial S_3}{\partial U}=\frac{\partial S_3}{\partial Z_3}\frac{\partial Z_3}{\partial U}\\ \frac{\partial S_3}{\partial S_2}\frac{\partial S_2}{\partial U}=\frac{\partial S_3}{\partial Z_3}\frac{\partial Z_3}{\partial S_2}\frac{\partial S_2}{\partial Z_2}\frac{\partial Z_2}{\partial U} ∂U∂S3=∂Z3∂S3∂U∂Z3∂S2∂S3∂U∂S2=∂Z3∂S3∂S2∂Z3∂Z2∂S2∂U∂Z2
至于Zt对U的求导,那是详细的展开了。
∂ Z 3 ∂ U = S 2 ∂ Z 3 ∂ S 2 = U \frac{\partial Z_3}{\partial U}=S_2\quad \frac{\partial Z_3}{\partial S_2}=U ∂U∂Z3=S2∂S2∂Z3=U
而我以前就因为没有区分好这个步骤而一直没有推导到最后。
∂ L t ∂ U = ∑ k = 1 t ∂ L t ∂ O t ∂ O t ∂ S t ( ∏ j = k + 1 t ∂ S j ∂ S j − 1 ) ∂ S k ∂ U \frac{\partial L_t}{\partial U}=\sum_{k=1}^{t}\frac{\partial L_t}{\partial O_t}\frac{\partial O_t}{\partial S_t}(\prod_{j=k+1}^{t}\frac{\partial S_j}{\partial S_{j-1}})\frac{\partial S_k}{\partial U} ∂U∂Lt=k=1∑t∂Ot∂Lt∂St∂Ot(j=k+1∏t∂Sj−1∂Sj)∂U∂Sk
同理,参数W的更新公式在形式上与该公式是一样的,不同的是具体的链式法则最后项求导展开细节。
( u v ) ′ = ( u ′ ) v + u ( v ′ ) (uv)^{'}=(u^{'})v+u(v^{'}) (uv)′=(u′)v+u(v′)
计算中,Zt内包含U和St-1,St-1是一个关于参数U的函数,因此:
∂ Z t ∂ U = ∂ U ∂ U S t − 1 + U ∂ S t − 1 ∂ U ∂ S t − 1 ∂ U = ∂ S t − 1 ∂ Z t − 1 ∂ Z t − 1 ∂ U = ∂ S t − 1 ∂ Z t − 1 ( ∂ U ∂ U S t − 2 + ∂ S t − 2 ∂ U U ) \frac{\partial Z_t}{\partial U}=\frac{\partial U}{\partial U}S_{t-1}+U\frac{\partial S_{t-1}}{\partial U}\\ \frac{\partial S_{t-1}}{\partial U}=\frac{\partial S_{t-1}}{\partial Z_{t-1}}\frac{\partial Z_{t-1}}{\partial U}=\frac{\partial S_{t-1}}{\partial Z_{t-1}}(\frac{\partial U}{\partial U}S_{t-2}+\frac{\partial S_{t-2}}{\partial U}U) ∂U∂Zt=∂U∂USt−1+U∂U∂St−1∂U∂St−1=∂Zt−1∂St−1∂U∂Zt−1=∂Zt−1∂St−1(∂U∂USt−2+∂U∂St−2U)
由于我在这里直接用具体的展开,没有使用链式法则的乘法求导,所有没有得到普遍的形式。梯度消失与梯度爆炸
∂ L t ∂ U = ∑ k = 1 t ∂ L t ∂ O t ∂ O t ∂ S t ( ∏ j = k + 1 t ∂ S j ∂ S j − 1 ) ∂ S k ∂ U \frac{\partial L_t}{\partial U}=\sum_{k=1}^{t}\frac{\partial L_t}{\partial O_t}\frac{\partial O_t}{\partial S_t}(\prod_{j=k+1}^{t}\frac{\partial S_j}{\partial S_{j-1}})\frac{\partial S_k}{\partial U} ∂U∂Lt=k=1∑t∂Ot∂Lt∂St∂Ot(j=k+1∏t∂Sj−1∂Sj)∂U∂Sk
其中有又一个连乘项,展开可以得到:
∏ j = k + 1 t ∂ S j ∂ S j − 1 = ∏ j = k + 1 t ∂ S j ∂ Z j ∂ Z j ∂ S j − 1 \prod_{j=k+1}^{t}\frac{\partial S_j}{\partial S_{j-1}}=\prod_{j=k+1}^{t}\frac{\partial S_j}{\partial Z_j}\frac{\partial Z_j}{\partial S_{j-1}} j=k+1∏t∂Sj−1∂Sj=j=k+1∏t∂Zj∂Sj∂Sj−1∂Zj
通常在RNN中使用的是tanh激活函数,即上述公式中,f=tanh,而tanh函数其中一个特点是导数小于1。大多数的激活函数导数都是小于1的。LSTM
z f = s i g m o i d ( W f ⋅ [ h t − 1 , X t ] + b f ) z i = s i g m o i d ( W i ⋅ [ h t − 1 , X t ] + b i ) z o = s i g m o i d ( W o ⋅ [ h t − 1 , X t ] + b o ) z = s i g m o i d ( W ⋅ [ h t − 1 , X t ] + b ) c t = z f ⊙ c t − 1 + z i ⊙ z h t = z o ⊙ t a n h ( c t ) y t = σ ( W y ⋅ h t ) z_f=sigmoid(W_f\cdot [h_{t-1},X_t]+b_f)\\ z_i=sigmoid(W_i\cdot [h_{t-1},X_t]+b_i)\\ z_o=sigmoid(W_o\cdot [h_{t-1},X_t]+b_o)\\ z=sigmoid(W\cdot [h_{t-1},X_t]+b)\\ c_t=z_f\odot c_{t-1}+z_i\odot z\\ h_t=z_o\odot tanh(c_t)\\ y_t=\sigma(W_y\cdot h_t) zf=sigmoid(Wf⋅[ht−1,Xt]+bf)zi=sigmoid(Wi⋅[ht−1,Xt]+bi)zo=sigmoid(Wo⋅[ht−1,Xt]+bo)z=sigmoid(W⋅[ht−1,Xt]+b)ct=zf⊙ct−1+zi⊙zht=zo⊙tanh(ct)yt=σ(Wy⋅ht)
⊙是Hadamard Product,矩阵中对应的元素相乘。
GRU
Z r e s e t = s i g m o i d ( W r ⋅ [ h t − 1 , X t ] + b r ) Z u p d a t a = s i g m o i d ( W u ⋅ [ h t − 1 , X t ] + b u ) h t − 1 ′ = h t − 1 ⊙ Z r e s e t h ′ = t a n h ( W ⋅ [ h t − 1 ′ , X t ] + b ) h t = ( 1 − Z u p d a t a ) ⊙ h t − 1 + Z u p d a t a ⊙ h ′ y t = σ ( W y ⋅ h t ) Z_{reset}=sigmoid(W_r\cdot [h_{t-1},X_t]+b_r)\\ Z_{updata}=sigmoid(W_u\cdot [h_{t-1},X_t]+b_u)\\ h_{t-1}^{'}=h_{t-1}\odot Z_{reset}\\ h^{'}=tanh(W\cdot [h_{t-1}^{'},X_t]+b)\\ h_t=(1-Z_{updata})\odot h_{t-1}+Z_{updata}\odot h^{'}\\ y_t=\sigma(W_y\cdot h_t) Zreset=sigmoid(Wr⋅[ht−1,Xt]+br)Zupdata=sigmoid(Wu⋅[ht−1,Xt]+bu)ht−1′=ht−1⊙Zreseth′=tanh(W⋅[ht−1′,Xt]+b)ht=(1−Zupdata)⊙ht−1+Zupdata⊙h′yt=σ(Wy⋅ht)
详细原理论证可以参考:数据导入与处理
for type in ['pos', 'neg']:
path = os.path.join(trainDataPath, type)
for name in os.listdir(path):
file = os.path.join(path, name)
with open(file, errors='ignore') as f:
trainData.append(f.read())
if type == "pos":
trainLabel.append(1)
else:
trainLabel.append(0)
注意,此处在读取文件时加上了errors='ignore’的属性,否则会报错:'gbk' codec can't decode byte 0x93 in position 596: illegal multibyte sequen
# 训练词向量字典
from tensorflow.keras.preprocessing.text import Tokenizer
tokenizer = Tokenizer(num_words=10000)
tokenizer.fit_on_texts(trainData)
tokenizer.word_index
train_sequence_r = tokenizer.texts_to_sequences(trainData)
test_sequence_r = tokenizer.texts_to_sequences(testData)
from tensorflow.keras.preprocessing.sequence import pad_sequences
train_sequences = pad_sequences(tokenizer.texts_to_sequences(trainData), maxlen)
test_sequences = pad_sequences(tokenizer.texts_to_sequences(testData), maxlen)
import numpy
# 标签列表转化为numpy矩阵
trainLabel = numpy.asarray(trainLabel)
testLabel = numpy.asarray(testLabel)
# 训练集打乱
indices = numpy.arange(train_sequences.shape[0])
numpy.random.shuffle(indices)
train_sequences = train_sequences[indices]
trainLabel = trainLabel[indices]
# 测试集打乱
indices = numpy.arange(test_sequences.shape[0])
numpy.random.shuffle(indices)
test_sequences = test_sequences[indices]
testLabel = testLabel[indices]
import math
# 划分验证集
trainSamplesCount = int(math.floor(train_sequences.shape[0] * 0.7))
train_data = train_sequences[:trainSamplesCount]
train_label = trainLabel[:trainSamplesCount]
val_data = train_sequences[trainSamplesCount:]
val_label = trainLabel[trainSamplesCount:]
嵌入层
Turns positive integers (indexes) into dense vectors of fixed size.
翻译:将正整数(索引)转换为固定大小的密集向量。
Embedding Layer
词嵌入空间的导入
def getEmbeddingMat(word_index):
# 词嵌入字典,键为单词,值为对应词向量
embedding_index = {}
# 使用with形式的open方法打开文件
with open(glove, errors='ignore') as f:
for line in f: # 按行读取
values = line.split() # 每一行使用split方法分开各个元素得到列表
word = values[0] # 词嵌入空间的文本文件,每行第一个是单词
embedding_index[word] = numpy.asarray(values[1:], dtype='float32')
# 初始化词嵌入矩阵,用于后面给嵌入层设置参数
max_word = 10000 # 对应上面的程序中Tokenizer(num_words=10000)
embedding_matrix = numpy.zeros((max_word, embedding_dim))
# 对分词器(Tokenizer)训练后所得的字典进行迭代。整数索引就是词嵌入矩阵的行,此行数据为该词对应的词向量。
for word, i in word_index.items():
if i < max_word:
embedding_vector = embedding_index.get(word)
if embedding_vector is not None: # 对于词嵌入空间中找不到的词,设置词向量全为0
embedding_matrix[i] = embedding_vector
return embedding_matrix
嵌入层加载GloVe嵌入空间
from tensorflow.keras import Sequential
from tensorflow.keras import layers
model = Sequential()
# 添加嵌入层
model.add(layers.Embedding(input_dim=max_word, output_dim=embedding_dim, input_length=maxlen))
# 添加其它层
model.add(layers.Flatten())
model.add(layers.Dense(32, activation='relu'))
model.add(layers.Dropout(0.5))
model.add(layers.Dense(16, activation='relu'))
model.add(layers.Dense(1, activation='sigmoid'))
# 设置嵌入层的参数为词嵌入矩阵
model.layers[0].set_weights([embedding_matrix])
# 因为是外部导入的词向量空间,所以不用训练此嵌入层
model.layers[0].trainable = False
# 设置嵌入层
layer = layers.Embedding(input_dim=max_word, output_dim=embedding_dim, input_length=maxlen)
layer.set_weights([matrix])
layer.trainable = False
# 搭建模型
model = Sequential()
model.add(layer)
model.add(layers.Flatten())
model.add(layers.Dense(32, activation='relu'))
model.add(layers.Dropout(0.5))
model.add(layers.Dense(16, activation='relu'))
model.add(layers.Dense(1, activation='sigmoid'))
set_weights(weights)
on on layer “embedding” with a weight list of length 1, but the layer was expecting 0 weightsKeras RNN API使用
Keras的循环神经网络API
使用SimpleRNN
from RNN import data
from RNN import model
from matplotlib import pyplot as plt
if __name__ == '__main__':
train_data, train_label, val_data, val_label, test_data, test_label, word_index = data.getData()
embedding_matrix = data.getEmbeddingMat(word_index=word_index)
model = model.getModel(data.max_word, data.embedding_dim, data.maxlen, embedding_matrix)
model.compile(optimizer='rmsprop',
loss='binary_crossentropy',
metrics=['acc'])
history = model.fit(train_data, train_label,
epochs=12,
batch_size=32,
validation_data=(val_data, val_label))
acc = history.history['acc']
val_acc = history.history['val_acc']
loss = history.history['loss']
val_loss = history.history['val_loss']
epochs = range(1, len(acc) + 1)
plt.figure(figsize=(12, 4))
plt.subplot(1, 2, 1)
plt.plot(epochs, acc, 'bo', label='Training acc')
plt.plot(epochs, val_acc, 'b', label='Validation acc')
plt.title("Training and Validation accuracy")
plt.xlabel('Epoch')
plt.ylabel('Value')
plt.legend()
plt.subplot(1, 2, 2)
plt.plot(epochs, loss, 'bo', label='Training loss')
plt.plot(epochs, val_loss, 'b', label='Validation loss')
plt.title("Training and Validation loss")
plt.xlabel('Epoch')
plt.ylabel('Value')
plt.legend()
plt.show()
from tensorflow.keras.preprocessing.text import Tokenizer
from tensorflow.keras.preprocessing.sequence import pad_sequences
import numpy
import math
import os
# 训练集样本位置
trainDataPath = 'E:/MLTrainingData/IMDB-FilmReviewTextData/maindata/train'
# 测试集样本位置
testDataPath = 'E:/MLTrainingData/IMDB-FilmReviewTextData/maindata/test'
# 词嵌入文件的位置
glove = 'E:/MLTrainingData/词向量模型/glove.6B.100d.txt'
# 数据集的分类
dataType = ['pos', 'neg']
# 序列的最长长度
maxlen = 200
# 字典最大词汇量
max_word = 10000
# 训练集数据所占总数据量比例
TrainSamplesScale = 0.7
# 词向量的维度
embedding_dim = 100
# 读取数据集
# Return:train_data, train_label, val_data, val_label, test_sequences, testLabel, tokenizer.word_index
# train_data:训练集数据(Numpy矩阵);train_label:训练集标签(Numpy矩阵)
# tokenizer.word_index:分词器字典
def getData():
trainData = []
trainLabel = []
testData = []
testLabel = []
for t in dataType:
path = os.path.join(trainDataPath, t)
for name in os.listdir(path):
file = os.path.join(path, name)
with open(file, errors='ignore') as f:
trainData.append(f.read())
if t == "pos":
trainLabel.append(1)
else:
trainLabel.append(0)
for t in dataType:
path = os.path.join(testDataPath, t)
for name in os.listdir(path):
file = os.path.join(path, name)
with open(file, errors='ignore') as f:
testData.append(f.read())
if t == "pos":
testLabel.append(1)
else:
testLabel.append(0)
# 训练词向量字典
tokenizer = Tokenizer(num_words=max_word)
tokenizer.fit_on_texts(trainData)
# 序列数据填充使得全部序列数据长度一样
train_sequences = pad_sequences(tokenizer.texts_to_sequences(trainData), maxlen)
test_sequences = pad_sequences(tokenizer.texts_to_sequences(testData), maxlen)
# 标签列表转化为numpy矩阵
trainLabel = numpy.asarray(trainLabel)
testLabel = numpy.asarray(testLabel)
# 训练集打乱
indices = numpy.arange(train_sequences.shape[0])
numpy.random.shuffle(indices)
train_sequences = train_sequences[indices]
trainLabel = trainLabel[indices]
# 测试集打乱
indices = numpy.arange(test_sequences.shape[0])
numpy.random.shuffle(indices)
test_sequences = test_sequences[indices]
testLabel = testLabel[indices]
# 划分验证集
trainSamplesCount = int(math.floor(train_sequences.shape[0] * TrainSamplesScale))
train_data = train_sequences[:trainSamplesCount]
train_label = trainLabel[:trainSamplesCount]
val_data = train_sequences[trainSamplesCount:]
val_label = trainLabel[trainSamplesCount:]
# 返回得到 训练集、验证集、测试集
return train_data, train_label, val_data, val_label, test_sequences, testLabel, tokenizer.word_index
# 读取词嵌入文件
# Input:word_index
# word_index:分词器字典。键是相应的单词,值是单词对应的数字(int)。
# Return:embedding_matrix
# embedding_matrix:词嵌入矩阵。每一行对应一个词语,行序号即为word_index中相应键(单词)的值(序号)。
def getEmbeddingMat(word_index):
embedding_index = {}
with open(glove, errors='ignore') as f:
for line in f:
values = line.split()
word = values[0]
embedding_index[word] = numpy.asarray(values[1:], dtype='float32')
embedding_matrix = numpy.zeros((max_word, embedding_dim))
for word, i in word_index.items():
if i < max_word:
embedding_vector = embedding_index.get(word)
if embedding_vector is not None:
embedding_matrix[i] = embedding_vector
return embedding_matrix
from tensorflow.keras import Sequential
from tensorflow.keras import layers
def getModel(max_word, embedding_dim, maxlen, matrix):
# 搭建模型
model = Sequential()
model.add(layers.Embedding(input_dim=max_word, output_dim=embedding_dim, input_length=maxlen))
# model.add(layers.SimpleRNN(32, return_sequences=True, dropout=0.5))
# model.add(layers.SimpleRNN(32, return_sequences=True, dropout=0.5))
model.add(layers.SimpleRNN(32))
model.add(layers.Dense(1, activation='sigmoid'))
model.layers[0].set_weights([matrix])
model.layers[0].trainable = False
return model
Fraction of the units to drop for the linear transformation of the inputs. Default: 0.
Fraction of the units to drop for the linear transformation of the recurrent state. Default: 0.
模型结构与各层输出
使用LSTM与GRU
# model.add(layers.SimpleRNN(32))
model.add(layers.LSTM(32))
# model.add(layers.SimpleRNN(32))
model.add(layers.GRU(32))
return_state参数的意义
# 函数式API,调用相应神经网络层
layer_input = layers.InputLayer(3, 3)
layer = layers.LSTM(16, return_state=True)
return_state Boolean. Whether to return the last state in addition to the output. Default: False.
......
# Under eager context, check the device placement and prefer the
# GPU implementation when GPU is available.
if can_use_gpu:
last_output, outputs, new_h, new_c, runtime = cudnn_lstm(
**cudnn_lstm_kwargs)
else:
last_output, outputs, new_h, new_c, runtime = standard_lstm(
**normal_lstm_kwargs)
else:
(last_output, outputs, new_h, new_c,
runtime) = lstm_with_backend_selection(**normal_lstm_kwargs)
states = [new_h, new_c]
......
if self.return_state:
return [output] + list(states)
elif self.return_runtime:
return output, runtime
else:
return output
h t = z o ⊙ t a n h ( c t ) h_t=z_o\odot tanh(c_t) ht=zo⊙tanh(ct)
经过检验可以确定,keras API下hidden_state就是RNN的t时刻的输出,即yt=ht。训练结果性能简单比较
SimpleRNN
loss: 0.5342 - acc: 0.7394 - val_loss: 0.5275 - val_acc: 0.7545
LSTM
loss: 0.2311 - acc: 0.9081 - val_loss: 0.3369 - val_acc: 0.8609
GRU
loss: 0.2051 - acc: 0.9193 - val_loss: 0.3631 - val_acc: 0.8632
RNN实践:德国耶拿研究所气象数据预测
回归任务
数据集分析
数据生成迭代器
def generator(data, back, indices_base=None, step=1, min_index=0, max_index=0, delay=0, batch_size=64, shuffle=True,
predict_data_index=1):
"""
德国耶拿气象站序列数据生成迭代器\n
:param data: 德国耶拿气象站数据numpy矩阵形式(行:每10min记录的数据,列:被记录的气象特征)
:param back: 回溯的数据,即每条样本的数据应该最大回溯到最初的数据数
:param indices_base: 外部提供使用样本数据顺序索引列表
:param step: 观测步长,即在每多少条数据中取一次数据到样本
:param min_index: 取data中数据的起始index
:param max_index: 取data中数据的终止index
:param delay: 目标延后,即需要预测的目标在当前数据延迟几条数据(多少个10min)后
:param batch_size: 每次迭代输出数据批量大小
:param shuffle: 是否打乱数据顺序
:param predict_data_index: 目标的气象数据类型索引(默认为温度数据)
:return:迭代器
"""
# 参数判断
if min_index < 0:
start = back
else:
start = min_index + back
if max_index <= 0:
end = data.shape[0] - delay + 1 # 考虑到range(start, end)不会取到end,故取值加1
else:
end = max_index - delay + 1
if end >= data.shape[0]:
end = data.shape[0] - delay + 1
if start <= 0:
start = back
# 设置打乱或者不打乱数据下的数据获取顺序
if shuffle:
# 获取数据范围内的随机数序列
indices = random.sample(list(range(start, end)), end - start) # 注意范围是start<=x
书上的代码示例
import os
import numpy as np
import random
import math
from tensorflow.keras import Sequential
from tensorflow.keras import layers
from matplotlib import pyplot as plt
from tensorflow.compat.v1 import ConfigProto
from tensorflow.compat.v1 import InteractiveSession
# GPU内存配置
os.environ["CUDA_VISIBLE_DEVICES"] = "0"
config = ConfigProto()
config.gpu_options.allow_growth = True
session = InteractiveSession(config=config)
# 存放数据的文件位置
data_dir = 'E:/MLTrainingData/德国耶拿MP研究所气象站记录/jena_climate_2009_2016.csv'
# 打开文件读取数据
f = open(data_dir)
data = f.read()
f.close()
lines = data.split('\n')
header = lines[0].split(',')
lines = lines[1:]
float_data = np.zeros((len(lines), len(header) - 1))
for i, line in enumerate(lines):
values = [float(x) for x in line.split(',')[1:]]
if len(values) == 0: # 不知道为什么会出现完全空的最后一行,总之判断如果读到空那就说明读完了
break
float_data[i, :] = values
# 数据标准化
mean = float_data[:20000].mean(axis=0)
float_data -= mean
std = float_data[:20000].std(axis=0)
float_data /= std
# 数据生成迭代器
def generator(data, lookback, delay, min_index, max_index, shuffle=False, batch_size=128, step=6):
if max_index is None:
max_index = len(data) - delay - 1
i = min_index + lookback
while True:
if shuffle:
rows = np.random.randint(min_index + lookback, max_index, size=batch_size)
else:
if i + batch_size >= max_index:
i = min_index + lookback
rows = np.arange(i, min(i + batch_size, max_index))
i += len(rows)
samples = np.zeros((len(rows), lookback // step, data.shape[-1]))
targets = np.zeros((len(rows),))
for j, row in enumerate(rows):
indices = range(rows[j] - lookback, rows[j], step)
# indices = list(range(rows[j] - lookback, rows[j], step))
samples[j] = data[indices]
targets[j] = data[rows[j] + delay][1]
yield samples, targets
lookback = 1440
step = 6
delay = 144
batch_size = 128
train_gen = generator(float_data, lookback=lookback, delay=delay, step=step, shuffle=True, batch_size=batch_size,
min_index=0, max_index=200000)
val_gen = generator(float_data, lookback=lookback, delay=delay, step=step, batch_size=batch_size,
min_index=200001, max_index=300000)
test_gen = generator(float_data, lookback=lookback, delay=delay, step=step, batch_size=batch_size,
min_index=300001, max_index=None)
val_step = (300000 - 200001 - lookback) // batch_size
test_step = (len(float_data) - 300001 - lookback) // batch_size
model = Sequential()
# model.add(layers.GRU(32,
# input_shape=(None, float_data.shape[-1]),
# dropout=0.2,
# recurrent_dropout=0.2))
model.add(layers.GRU(32,
recurrent_dropout=0.1,
input_shape=(None, float_data.shape[-1])))
model.add(layers.Dense(1))
model.compile(optimizer='rmsprop',
loss='mae',
metrics=['cosine_similarity'])
history = model.fit_generator(train_gen,
steps_per_epoch=500,
epochs=20,
validation_data=val_gen,
validation_steps=val_step)
cosine_similarity = history.history['cosine_similarity']
val_cosine_similarity = history.history['val_cosine_similarity']
loss = history.history['loss']
val_loss = history.history['val_loss']
epochs = range(1, len(cosine_similarity) + 1)
plt.figure(figsize=(12, 4))
plt.subplot(1, 2, 1)
plt.plot(epochs, cosine_similarity, 'bo', label='Training cosine similarity')
plt.plot(epochs, val_cosine_similarity, 'b', label='Validation cosine similarity')
plt.title("Training and Validation Cosine Similarity")
plt.xlabel('Epoch')
plt.ylabel('Value')
plt.legend()
plt.subplot(1, 2, 2)
plt.plot(epochs, loss, 'bo', label='Training loss')
plt.plot(epochs, val_loss, 'b', label='Validation loss')
plt.title("Training and Validation Loss")
plt.xlabel('Epoch')
plt.ylabel('Value')
plt.legend()
plt.show()
训练状况
奇怪的问题
训练结果
Epoch 1/20
loss: 34325184.0000 - cosine_similarity: 0.2656
......
Epoch 10/20
loss: 0.2981 - cosine_similarity: 0.8906
......
Epoch 20/20
loss: 2647.4542 - cosine_similarity: 0.8217 - val_loss: 0.3085 - val_cosine_similarity: 0.8120
报错记录
2021-09-20 22:12:49.674778: I tensorflow/stream_executor/platform/default/dso_loader.cc:44] Successfully opened dynamic library cudnn64_7.dll
2021-09-20 22:12:50.788377: E tensorflow/stream_executor/cuda/cuda_dnn.cc:329] Could not create cudnn handle: CUDNN_STATUS_ALLOC_FAILED
2021-09-20 22:12:50.788565: W tensorflow/core/framework/op_kernel.cc:1622] OP_REQUIRES failed at cudnn_rnn_ops.cc:1491 : Unknown: Fail to find the dnn implementation.
tensorflow.python.framework.errors_impl.UnknownError: Fail to find the dnn implementation. [Op:CudnnRNN]
# GPU内存配置
os.environ["CUDA_VISIBLE_DEVICES"] = "0"
config = ConfigProto()
config.gpu_options.allow_growth = True
session = InteractiveSession(config=config)
参考推荐