循环神经网络

循环神经网络

Time:2021-07-16

Author:雾雨霜星

我的网站:雾雨霜星

前言

开始学习新的神经网络算法了,循环神经网络。越来越多的实践,我却从中越来越感到自己不懂编程,或许是我不够聪明,或许是我不适合从事这样的工作?居然会从中稍微感到失落。

本文主要记录了我使用Tensorflow的Keras接口训练循环神经网络的过程。

循环神经网络原理

RNN定义

循环神经网络(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(VSt)St=f(WXt+USt1)
其中O(t)代表了t时刻输入下的输出结果。

可以认为是一种环形的结构,即输出量(或可以反应输出的计算过程量)会反馈进入输入中。

最基本的RNN,本质就是一个具有units个神经元,且可以记录每次计算状态量的隐藏层。对于输入维度为n,时间序列长度为T的数据。总共计算T次,每次计算是对第t(t<T)时刻的n维向量进行全连接计算,同时引入了上一时刻t-1时刻计算的状态量,参与计算,得到t时刻的状态量,以及units维向量的t时刻的输出。

输出

Keras官方文档对RNN基类的输出描述如下:

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.

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为例子:

layer = tensorflow.keras.layers.LSTM(units=16)

对[timesteps, feature]样本,每个时间点数据为features维度向量进行全连接计算得到units维向量,计算timesteps次就得到了[timesteps, units]。

RNN的训练方法

采用基于时间步的反向传播算法(BPTT)进行训练(更新参数)。

BPTT:Back-Propagation Through Time。使用链式法则进行反向传播时,对计算St的式子中的参数矩阵进行更新,求导时会涉及到上一时刻状态量,因此在链式法则中需要不断顺着t进行求导直至t=0。

多元函数链式法则(乘法求导):

多元函数链式法则的乘法求导应该描述如下:
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)x1y=fyg1fx1g1+fyg2fx1g2

上述的描述方法的认识非常重要,对于后面BPTT的推导,由此可以得到最为普遍的形式。

而我之前一直没有推导出来,就是因为对这一步的认识不够清晰。

BPTT

假设总的误差函数为L,每个时刻计算所得输出Ot的误差函数Lt。

由上述的RNN前向传播的方程,如下:
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(VSt)St=f(WXt+USt1)Zt=WXt+USt1
设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=0TLtUL=t=0TULt

其中不同时刻的误差函数对参数U的求导,需要回溯到之前时刻的数据。这是因为U参数不仅仅表现与此次St的更新,对St-1也有影响,即St-1是U的函数。

考虑简单的状况,先考虑L3下的对U参数求导,使用链式法则可以得到:
∂ 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} UL3=O3L3UO3=O3L3S3O3US3+O3L3S3O3S2S3US2+O3L3S3O3S2S3S2S2US1
这里没有把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} US3=Z3S3UZ3S2S3US2=Z3S3S2Z3Z2S2UZ2
至于Zt对U的求导,那是详细的展开了。

需要注意的是,这里已经应用了求导法则,在具体的求导时无需再分步骤求导。即这里已经将Zt中的U和St-1分开了,所以计算方法如下:
∂ 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 UZ3=S2S2Z3=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} ULt=k=1tOtLtStOt(j=k+1tSj1Sj)USk
同理,参数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) UZt=UUSt1+UUSt1USt1=Zt1St1UZt1=Zt1St1(UUSt2+USt2U)
由于我在这里直接用具体的展开,没有使用链式法则的乘法求导,所有没有得到普遍的形式。

梯度消失与梯度爆炸

从上述参数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} ULt=k=1tOtLtStOt(j=k+1tSj1Sj)USk
其中有又一个连乘项,展开可以得到:
∏ 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+1tSj1Sj=j=k+1tZjSjSj1Zj
通常在RNN中使用的是tanh激活函数,即上述公式中,f=tanh,而tanh函数其中一个特点是导数小于1。大多数的激活函数导数都是小于1的。

由于普通RNN中连乘项不会被消去,因此随着深度的增加,会连乘激活函数的导数。

连乘小于1的数,可以看做是幂级别的计算,使得梯度变得很小,几乎接近于0。这就是梯度消失。

实际上,若参数U非常大,tanh或许会无限接近1,但是此时参数U已经非常大了,连乘小梯度会趋于无穷,从而引起梯度爆炸。

避免梯度消失和梯度爆炸较好的方法是使用LSTM或者GRU,本质就是引入门控制,使得连乘项接为0或者为1。

LSTM

LSTM:Long Short-Term Memory Network,长短时记忆网络。

相对于普通的RNN,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[ht1,Xt]+bf)zi=sigmoid(Wi[ht1,Xt]+bi)zo=sigmoid(Wo[ht1,Xt]+bo)z=sigmoid(W[ht1,Xt]+b)ct=zfct1+zizht=zotanh(ct)yt=σ(Wyht)
⊙是Hadamard Product,矩阵中对应的元素相乘。

其中的zf,zi,zo分别代表了遗忘门、输入门、输出门,他们的值都是0或者1。

执行流程如下:

  1. 使用遗忘门zf计算是否需要遗忘上一次的元状态ct-1,使用输入门zi计算是否需要记录本次学习z。
  2. 上述计算结果的两者结合得到新的元状态。
  3. 使用tanh是对当前元状态ct进行数据缩放。
  4. 使用输出门zo决定是否需要输出本次元状态ct的信息ht。
  5. 获取输出为yt。

可见,LSTM不仅沿着时间传递输出计算状态(以hidden_state为载体),还传递了一个cell_state,可以理解为:hidden_state代表了近期记忆,而cell_state则代表了远期记忆。

LSTM通过使用遗忘门和输出门,使得在参数更新过程中,连乘项ht对ht-1的偏导ct对ct-1的偏导,变为0或者1,从而避免梯度消失和梯度爆炸。

GRU

GRU:Gate Recurrent Unit。门控循环单元。

也是为了解决RNN反向传播中的梯度等问题而提出来的变体。

优势:GRU内部结构相对于LSTM而言较为简单,其所需要的计算代价也更小。

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[ht1,Xt]+br)Zupdata=sigmoid(Wu[ht1,Xt]+bu)ht1=ht1Zreseth=tanh(W[ht1,Xt]+b)ht=(1Zupdata)ht1+Zupdatahyt=σ(Wyht)
详细原理论证可以参考:

Empirical Evaluation of Gated Recurrent Neural Networks on Sequence Modeling(arxiv.org)

数据导入与处理

使用的是IMBD的电影评论数据,每条评论数据被记录在一个txt文本文件中。数据的标签类型分为积极评价和消极评价两种。

数据来源:https://mng.bz./0tIo

数据集格式:有train和text两个文件夹,每个文件夹里面有pos和neg两个文件夹,其中每个文件夹里面又有许多txt文本文件。

数据处理的目标:将读入的字符串数据通过分词器转化为整数索引数据序列。

数据的导入和处理基本步骤:

  1. 读取数据集,得到记录各条文本数据的列表。同时按照来源制作标签列表
  2. 制作字典,得到全部文本数据中每个词对应一个数字的字典
  3. 按照字典,把原本的string格式数据转化为数字序列数据
  4. 对每个序进行填充,得到全部长度一样的序列
  5. 转化为numpy矩阵,且进行数据打乱
  6. 划分训练集和验证集

首先使用Python的os模型进行文件的获取,使用open方法打开文件,读取文件内容:

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)

其中trainDataPath是数据集train文件夹的路径,trainData和trainLabel是一个初始化时为空的列表。
注意,此处在读取文件时加上了errors='ignore’的属性,否则会报错:

'gbk' codec can't decode byte 0x93 in position 596: illegal multibyte sequen

猜测可能是出现了超出gbk编码的字符,在网上看到的方法都是加忽略错误属性。

然后是制作文本数据的字典:

# 训练词向量字典
from tensorflow.keras.preprocessing.text import Tokenizer
tokenizer = Tokenizer(num_words=10000)
tokenizer.fit_on_texts(trainData)

其中的trainData就是上述读取数据得到的以string格式评论数据为元素的列表。

使用Keras的分词器(Tokenizer),需要确定字词数量,也就是制作的字典中最多包含多少个字。

Tokenizer使用fit_on_texts方法完成字典的制作,输入的是相应的文本列表(每个元素是字符串)。

获取Keras分词器Tokenizer的字典:

tokenizer.word_index

返回得到一个字典,此字典是分词器的训练结果,键是相应的单词,值是单词对应的数字(int)。

然后是进行序列转化,即使用训练后的分词器,把字符串转化为数字序列:

train_sequence_r = tokenizer.texts_to_sequences(trainData)
test_sequence_r = tokenizer.texts_to_sequences(testData)

然后是进行数字序列的填充,使得所有序列数据的长度一致。使用Keras内置的方法:

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)

再将标签列表转化为numpy矩阵,然后使用numpy的arange方法创建等差数组,让后使用numpy的random.shuffle方法将此等差数组打乱顺序,把此打乱顺序后的等差数组应用到numpy矩阵上:

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:]

嵌入层

词向量:在对文本数据进行分词后,可将每个单词映射为一个向量,这个向量即使词向量。

词嵌入:是密集的词向量,与one-hot编码不同,是低维的浮点数向量。

嵌入层(Embedding layer):神经网络中用于训练词嵌入空间,将输入序列数据转化为密集向量数据的层。

官方文档对嵌入层的解释:

Turns positive integers (indexes) into dense vectors of fixed size.
翻译:将正整数(索引)转换为固定大小的密集向量。

从此定义可知,我们要对文本数据进行分词后,把每个词转化为整数索引,而每个文本序列数据变为整数序列数据,再使用嵌入层,得到密集词向量数据。这也是为什么前面数据处理,需要使用分词器Tokenizer,以及对序列进行填充。

并且该层只能是在神经网络的第一层进行使用。

可以使用外部已经预训练好的词嵌入空间,从而减少训练的任务量。著名的词嵌入空间是:word2vec和GloVe。

Embedding Layer

创建时需要确定三个参数: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函数打开相应词嵌入空间文件,按照每行进行读取,使用分词器的字典对应起来每个词的整数索引和词向量:

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

这里需要制作的是词嵌入矩阵embedding_matrix,每一行对应一个词语,每一行的长度即为词嵌入空间中每个词向量维度。

对词嵌入矩阵初始化时,大小设置为(max_word, embedding_dim)。max_word即分词器最大分词数,embedding_dim即词向量维度。

嵌入层加载GloVe嵌入空间

按照上面的步骤得到了词嵌入矩阵embedding_matrix。

这个词嵌入矩阵,每一行代表一个分词器所得的词,而行序号就是分词器制作的字典的整数索引,此行的数据就是该词在GloVe词嵌入空间中的词向量。

使用Keras.layers的set_weights方法设置层参数,而嵌入层的参数正是词嵌入矩阵。

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

需要注意的是,set_weights必须在该层add进入model后才能正常使用。

如果按照如下的方法:

# 设置嵌入层
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'))

会出现报错:

ValueError: You called set_weights(weights) on on layer “embedding” with a weight list of length 1, but the layer was expecting 0 weights

大概是和set_weights(weights)方法自身传入参数的过程有关。

Keras RNN API使用

Keras的循环神经网络API

在Keras的Layers API中,提供的循环神经网络层主要API有三种:

  • SimpleRNN
  • GRU
  • LSTM

SimpleRNN是由最基本的RNN神经元构成的Layer。

此外Keras还提供了各种循环神经元,包括SimpleRNNCell、GRUCell、LSTMCell,可以直接使用神经元自己设计层。

使用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()

在getData函数中获取训练集、验证集和测试集,全部都是已经进行数据处理,从字符序列转化为了数字序列且进行了填充,同时还获取了Keras分词器得到的字典,用于输入getEmbeddingMat函数中获取指定词嵌入向量空间的词嵌入矩阵。

getModel函数获取训练的模型后,与以往使用全连接神经网络的做法一样,进行model的编译后训练即可。

以下为data.py的程序:

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

以下为model.py的程序:

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

其中提供给layers.SimpleRNN的输入参数是指多少个RNN神经元,即深度。该参数决定了输出张量的长度。

注意,如果是使用多层RNN的时候,需要设置参数return_sequences=True,否则会报错。这是因为SimpleRNN层默认输出的是最后一次(最后时刻)输入得到的计算结果。设置该参数后,会将之前每一时刻输入计算得到的值也放到层的输出中。

而参数dropout则用于设置防止过拟合。

另外需要说明的是,Keras提供的RNN Layer API有两个dropout参数:

  • dropout:对应用在输入上的线性变换矩阵的参数丢失率

    Fraction of the units to drop for the linear transformation of the inputs. Default: 0.
    
  • recurrent_dropout:对应用在循环状态上的线性变换矩阵的参数丢失率

    Fraction of the units to drop for the linear transformation of the recurrent state. Default: 0.
    
模型结构与各层输出

在模型的搭建中,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

LSTM和GRU本质只是和SimpleRNN的神经元在传输通道上存在差异,而它们同属于RecurrentLayer,使用方法是一样的。

因此,使用LSTM只需要在上述模型中修改如下:

# model.add(layers.SimpleRNN(32))
model.add(layers.LSTM(32))

同理,使用GRU也只需要修改add中调用的layersAPI:

# model.add(layers.SimpleRNN(32))
model.add(layers.GRU(32))

其余地方完全一致都行。

return_state参数的意义

对于layers.SimpleRNN、layers.LSTM、layers.GRU三个API,均有一个可选的参数return_state可调用,通常默认值为False。

调用:

# 函数式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.

如果设置为True,那么在计算后不仅会返回输出结果,还会返回RNN的状态,对于LSTM与GRU而言,会返回hidden_state与cell_state。

从tensorflow.keras.layers.LSTM源码可见:

    ......
        # 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

其中new_h即最终的hidden_state(ht),而new_c是最终的cell_state(ct)。对应了LSTM模型计算公式:
h t = z o ⊙ t a n h ( c t ) h_t=z_o\odot tanh(c_t) ht=zotanh(ct)
经过检验可以确定,keras API下hidden_state就是RNN的t时刻的输出,即yt=ht。

训练结果性能简单比较

使用IMDB电影评论数据,使用100维度的GloVe预训练词嵌入空间。

分布使用SimpleRNN、LSTM、GRU训练12轮,得到效果如下:

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

SimpleRNN的训练参数是最少的,但是训练的速度却是最慢的。LSTM和GRU的训练速度明显比SimpleRNN的好很多,训练也比较快。至于为什么我现在都还没有想明白。

尽管只训练了12轮,但是从绘制的曲线来看,他们的过拟合在第10/11轮左右就开始体现了。

其实这个训练效果并不是很好,书上说这是因为这个分类任务交给全连接神经网络更加合适,而RNN更加适合分析序列的长期性结构,对情感分类帮助不大。

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文件中读取。

代码如下:

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<end
        # 查看是否有索引指示列表
        if indices_base is not None and type(indices_base) is list:
            indices = indices_base
    else:
        # 不用打乱,则顺序读取(每次读取的数据区域不会重叠,若考虑重合则将back去掉)
        indices = list(range(start, end, back))  # 注意范围是start<=x<end
        print(len(indices))

    # 设置迭代器获取数据序号所到的index
    iter_index = 0

    # 迭代器循环
    while True:
        # 迭代器每次取数据获得数据样本
        samples = np.zeros((batch_size, math.ceil(back / step), data.shape[-1]))  # 还是需要向上取整,主要是对不能整除的状况的考虑
        targets = np.zeros((batch_size,))
        # 迭代器每次取数据所取数据的行序号列表
        indices_batch = indices[iter_index:iter_index + batch_size]
        # 更新每次取数据后的获取数据所到的index
        iter_index = iter_index + batch_size
        # 如果已经取完了一次数据,那么置零重新开始
        if iter_index + batch_size >= len(indices):
            iter_index = 0
        # 制作数据样本
        for i, indices_num in enumerate(indices_batch):
            # samples[i]对应的数据是csv文件中的excel序号:indices_num-back+2行,到indices_num+1行
            # 例如1:11:2,取值1,3,5,7,9;对应原csv文件的3,5,7,9,11行的数据
            data_index_list = list(range(indices_num - back, indices_num, step))
            samples[i] = data[data_index_list]  # 回溯使用的数据序列,注意实际取值是不会到右区间的
            targets[i] = data[data_index_list[-1] + delay][predict_data_index]  # 目标得到的预测值,即上述样本最后一条数据后的第delay条
        yield samples, targets

书上的代码示例
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()

注意:按照书上的示例代码,其进行训练时的数据集,每一行代表一个时间节点下全部特征的数据,而每一列是一个特征随时间变化的序列。

训练状况

训练速度非常慢,跑了一个下午才跑完这个demo…

奇怪的问题

在设置了recurrent_dropout后,会发现loss非常大,在我一开始设置dropout=0.2,recurrent_dropout=0.2,loss甚至达到了10亿(不止了,20位数以上)。。。。。。

设置recurrent_dropout=0.1下,一开始loss也有几千,但是一直在降低,最终去到1以下。

训练结果
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

也不知道到底是否有问题,总之还是令人非常不安。

报错记录

  • 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的控制台输出处有一大段信息,比较关键的信息摘录如下:

    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显存分配:

    # GPU内存配置
    os.environ["CUDA_VISIBLE_DEVICES"] = "0"
    config = ConfigProto()
    config.gpu_options.allow_growth = True
    session = InteractiveSession(config=config)
    

参考推荐

LSTM简介:人人都能看懂的LSTM - 知乎 (zhihu.com)

零基础入门深度学习(5) - 循环神经网络 - 作业部落 Cmd Markdown 编辑阅读器 (zybuluo.com)

转载请注明出处:https://www.shuangxing.top/#/

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值