当前位置:   article > 正文

循环神经网络(内含LSTM、GRU实战)_simplernn实例

simplernn实例

目录

1. Embedding

2. RNN介绍

       2.1. memory机制

3. SimpleRNN (实战)

4. 循环神经网络的问题

5. LSTM

6. LSTM层在情感分类(实战)

7. GRU (理论+实战) 

8. 深层循环神经网络


1. Embedding

        以前讲的都是常用与空间性的学习,而时序性的学习则需要使用循环神经网络

        那项声音,文字等序列是如何表示的呢?

        可以利用序列的相关性表示。

        这就可以讲到一个Embedding了

        Embeddin层:

  1. 通过计算相似度,在空间中查找最近邻。
  2. 实现高纬稀疏特征向量像低稠密特征向量的转换。
  3. 训练好的embedding可以当作输入深度学习模型的特征。

        常用Embedding方式:

        Word2vec(使用最为广泛的词嵌入方法):速度快,效果好,容易扩展,简单

        

         一般Word2vec分为两个模型:        

        CBOW

  1.      1. 输入层: 上下文单词的onehot。(要训练此的上下文)
  2. 所欲的onehot分别乘以共享的输入权重矩阵W
  3. 所有的向量相加求平均作为隐层向量
  4. 乘以输出权重矩阵W
  5. 得到向量经过激活函数处理得到概率分布
  6. 概率最大的index所指示的单词为预测的中间词(target word)与true label的onehot作比较,误差越小越好(更具误差更新权重矩阵)
  7. 假设我们此时的到的概率分布已经达到了设定的迭代次数,那么输入层的每个单词与矩阵W相乘得到的向量的就是我们想要的词向量。

       TF实战实例:

        注意:输入进去的不是真的字,而是是一个索引!!!

        

  1. import tensorflow as tf
  2. from tensorflow import keras
  3. x = tf.range(5)
  4. x
  5. x = tf.random.shuffle(x)
  6. x
  7. net = tf.keras.layers.Embedding(input_dim=5,output_dim=4)
  8. # input_dim 输入单词的个数 是可处理的容量,要大于真正的输入,这次实验是>5
  9. # output_dim 是一个向量有多少个数表示。我理解为特征。
  10. net(x)
  11. (x_train, y_train), (x_test, y_test) = tf.keras.datasets.imdb.load_data()
  12. (x_train.shape, y_train.shape), (x_test.shape, y_test.shape)
  13. tf.keras.datasets.imdb.get_word_index()
  14. #Embedding 输入必须是等长的。因此要对数据做一些处理。
  15. x = tf.keras.preprocessing.sequence.pad_sequences(x_train , maxlen=250)
  16. len(x[0]),len(x_train[0])
  17. net = tf.keras.layers.Embedding(input_dim=90000,output_dim=4)
  18. net(x) # 25000个句子,一个句子250个单词,一个单词由4个词向量组成

情感分析,具体演示循环神经网络过程:

        把序列通过数字输入,经过Embedding层转为词向量,词向量是[句子数,单词长度,词向量长度(特征)] 。


2. RNN介绍

        

        首先思考这样的二分类全连接神经网络可行吗?

        没一个单词对应的词向量经过一个网络层,最后再经过一个合并,然后最后再进行一个类别的区分。可这样的话,和以前的相同的,参数量也会过多。同事每个句子也没法保证每个句子的长度保一样。这样构建的全连接网络层是动态变化的。是不太好的。而且每个输入之间是没有关系的。比如x1只会和x1有关系,但是一个句子是一句话,他们之间是有关系的,我们不能捕捉这种关系。因此我们要逐一解决这种问题。

        解决思路:

        以前在卷积就提出过,权值共享的思路,既能减少网络的参数量,但是这一段话的句子还是将句子分开几个部分理解,还是不能理解句子的整体特征。

       2.1. memory机制

        那么怎么获取网络的整体语义信息呢?或者说怎么将特征逐渐积累,直至最后提取整个句子的信息呢?我们提出了memory机制。

        如果说,网络提供一个单独的网络特征词向量,我们每次提取网络的词向量特征的时候,同时刷新memory,直到序列的最后。

         将memory机制实现成一个状态张量h:

         除了每个词都有一个句子的共享权值w_xh,还有一个w_hh是一个额外增加的权重。

        最后一个h就能够很好的代替整个句子的语义信息。

        基于上述的过程,有人提出了这样一个网络层:

        在每一个时间戳上都进行一个输入输出的循环计算,在每个时间戳上,网路都会接受一个输入xt(词向量),输入网络的xt会与上一个网络的输出ht-1,计算得到该网络的输出,简要公式是:

h(t) = f_{\Theta }(h_{t-1},x_{t})

        其中f是一个网络逻辑,\Theta是一个参数集合。

        这样的网络模型,我们既可以保留之前句子的信息,也能有当前词的信息。

        一个基本的CNN是:激活函数里面的,就是CNN的网络逻辑。

        模型简化:

3. SimpleRNN (实战)

        先介绍梯度的计算:

        上图模型最后得到的O,可以与真实值进行比较,然后得到一个loss,然后对loss进行反向传播求得梯度,再对参数进行更新。不过循环神经网络一个很大的问题就是,我们求得的梯度过程中要进行一个Whh的连乘计算,计算起来比较慢。

        

       SimpleRNNCell:

         SimpleRNNCell是指只完成了一个时间戳上的计算(因为每个时间戳都有一个输出,这个输出与h有关),而SimpleRNN是一整个CNN过程。

        输入的形状是什么呢?总共的训练集是:[多少句话,句子单词数,每个词的Embedding后的词向量长度],训练是在句子每个单词序列上展开的,则输入的是:[句子单词数,embedding后的特征]。

        那么每次运算后我们希望能够降维,只要利用 [b,f] @ [f,n],设置n大小即可。

        CNN还可以纵向进行多层训练。因为out也是一个输出,把out作为新层的输入即可。

        具体计算方式既可以先纵向,再横向。当然也可以先横向再纵向。 

  1. import tensorflow as tf
  2. from tensorflow import keras
  3. from tensorflow.keras import layers
  4. cell = layers.SimpleRNNCell(3) # 输出的个数,即n
  5. cell.build(input_shape=(None,4)) #4是输入的,3是输出神经元的个数
  6. cell.trainable_variables
  7. # 【4,3】是输出有四句话,每句话被Embedding到3个词向量长度。
  8. # 【3,3】是当前的whh,是一个额外的向量。
  9. h_0 = [tf.zeros([4,64])] # 4个句子,做计算后姜维的到64个词向量大小
  10. x = tf.random.normal([4,80,100]) # 4句话,每句话80个单词,100个Embedding后的大小。
  11. # 沿着80展开,让【4,100】 @ 【100,64】
  12. x_1 = x[:,0,:] #第一个词
  13. x_1.shape
  14. cell = layers.SimpleRNNCell(64)
  15. #两个输出,一个out,一个h,h还要进入下一层进行计算。
  16. out_1,h_1 = cell(x_1,h_0)
  17. #这里两者完全相等
  18. out_1==h_1
  19. out_1.shape
  20. h = h_0
  21. for x_i in tf.unstack(x,axis=1):
  22. out , h = cell(x_i , h)
  23. out
  24. x_1 = x[:,0,:]
  25. cell_0 = layers.SimpleRNNCell(64)
  26. cell_1 = layers.SimpleRNNCell(32)
  27. h_0 = [tf.zeros([4,64])] #把他转换为一个列表
  28. h_1 = [tf.zeros([4,32])]
  29. for x_i in tf.unstack(x,axis = 1): # 再s上扩展
  30. out_0 ,h_0 = cell_0(x_i,h_0)
  31. out_1 ,h_1 = cell_1(out_0,h_1)
  32. out_1
  33. layer = layers.SimpleRNN(64)
  34. out = layer(x)
  35. out
  36. model = keras.Sequential([
  37. #return_sequences=True,除了最末层以外,每个时间戳伤都要返回一个状态的输出
  38. layers.SimpleRNN(64,return_sequences=True),
  39. layers.SimpleRNN(32)
  40. ])
  41. model(x)

4. 循环神经网络的问题

        当层数过多的时候,RNN的长期依赖问题:梯度爆炸和梯度弥散(消失)

        上述梯度中有一个连乘计算,当Whh的最大特征值大于1时,就会出现梯度爆炸(因为指数级的计算是很庞大的)。而Wt 与等于 Wt-1时就会出现梯度弥散现象

        使用权重衰减或梯度截断(裁剪)解决

        权重衰减是通过给参数增加l1范数或l2范数的正则来限制参数的取值范围,从而使得γ <= 1,

        梯度截断是另一种有效的启发式方法,当梯度的模大于一定阈值时,就将他截断成为一个较小的数。

         如下图这种情况,如果不进行截断,会一下子上去,因此要在之前就要进行截断。

        除了以上提到的梯度爆炸问题,梯度消失问题是循环神经网络的最主要的问题。RNN最大的问题就是,当层数增多时,之前的记忆会逐渐模糊!!!即短时记忆。除了改变学习率,减小层数,等方法外,最直接的方法就是改变学习的模型        

        解决方案:让过去得输出不仅作为输入,还要作为输出的一部分保留信息。

  • 改进模型1:乘法保持信息。但是梯度爆炸的速度就更加快了。一般都会有限制,使用也只用于控制。
  • 改进模型2:加法保持信息。会丢失非线性激活的性质,降低模型表达能力。所以我们可以把ht-1放进g()中,即改进3.
  • 改进模型3:增加非线性关系即存在线性关系,又是非线性关系。所以可以缓解改进2的丢失非线性激活的性质,但仍然存在梯度爆炸问题以及记忆容量问题。

        因为每次记忆都会储存,则会出现即已饱和现象。随着时间,ht会越来越大,但是机器能存储的记忆容量是有限的,则越来越多时,也会出现记忆丢失现象。

        为了解决这两个问题,我们可以引入控这一个概念。        


5. LSTM

         相比传统的CNN来说,LSTM更擅长于处理长时间的记忆。

        LSTM的原理:

        与RNN的区别:

         1. 除了输入的xt和ht-1之外,还加入了一个长时记忆单元c,而h控制的是一个短时的记忆。 

         2. 还有门控机制:输入门、遗忘门、输出门,通过三个门来控制信息的流转。


        门控机制:

         输出有多少取决于门控值!我们称门控程度为:门控值,其可以控制阈值。首先把门控值压缩到(0,1)之间,当sigmoid输出为0时是完全关闭,1时是完全打开。

        输入门: 刷新Memory

        控制LSTM对输入的接收程度,不全接收所有输入。

        ht-1是之前记住的20个单词,xt表示新的10个单词。经过tanh将这两个输入做了一个融合(或者说对输入做一个非线性变换,提取所有的输入的信息,最后输出)。tanh还可以把值都压缩至(-1,1)之间。有点像归一化。而\sigma值是阀门,可以用sigmoid产生(0,1)之间的值。

        遗忘门:

        Ht-1相当于短时记忆,Ct-1相当于长时记忆。遗忘门来决定长时记忆能记住多少。记住多少也是取决于ht-1和xt。而以前的记忆到底能记住记住多少个,就要看遗忘门。

        当前的输出,能全部输出吗?这要看输出门。

        输出门

        ct-1是直接输入,但是输入ht-1与xt则与输入门差不多,是部分输入。

输入门遗忘门LSTM行为
01只能记忆
11综合输入和记忆
00清除记忆
10输入覆盖记忆

        LSTM变形:

        1. 没有遗忘门

        2. 耦合输入门和遗忘门:输入门和遗忘门,有一定的相同功能,同时使用两个门会有些冗余,于是可以将两者耦合

        3.peephole连接: 把长时 的记忆也放进RNN中 。

6. LSTM层在情感分类(实战)

  1. import tensorflow as tf
  2. from tensorflow import keras
  3. from tensorflow.keras import layers
  4. (x_train, y_train), (x_test, y_test) = tf.keras.datasets.imdb.load_data()
  5. (x_train.shape, y_train.shape), (x_test.shape, y_test.shape)
  6. # 我们希望每个输入的长度都是相同的
  7. x_train = keras.preprocessing.sequence.pad_sequences(x_train,maxlen=250)
  8. x_test = keras.preprocessing.sequence.pad_sequences(x_test,maxlen=250)
  9. # 整合成一个train_db,和test_db
  10. train_db = tf.data.Dataset.from_tensor_slices((x_train,y_train))
  11. test_db = tf.data.Dataset.from_tensor_slices((x_test,y_test))
  12. train_db = train_db.shuffle(10000).batch(128 , drop_remainder=True) # drop_remainder多余的就不要了
  13. test_db = test_db.shuffle(10000).batch(128 , drop_remainder=True)
  14. class myLSTM(keras.Model):
  15. def __init__(self):
  16. super().__init__()
  17. #可以吧两个输入整个作为一个输入
  18. self.state_0 = [tf.zeros([128,64]) , tf.zeros([128,64])]
  19. self.state_1 = [tf.zeros([128,32]) , tf.zeros([128,32])]
  20. self.embedding = layers.Embedding(input_dim=100000,output_dim=100)
  21. self.lstm_cell_0 = layers.LSTMCell(64)
  22. self.lstm_cell_1 = layers.LSTMCell(32)
  23. self.fc = layers.Dense(1)
  24. def __call__(self,inputs, *args, **kwargs):
  25. x = self.embedding(inputs)
  26. state_0 = self.state_0
  27. state_1 = self.state_1
  28. for x in tf.unstack(x,axis = 1):
  29. out_0 , state_0 = self.lstm_cell_0(x,state_0)
  30. out_1 , state_1 = self.lstm_cell_1(out_0,state_1)
  31. x = self.fc(out_1)
  32. x = tf.sigmoid(x)
  33. return x
  34. model = myLSTM()
  35. model.compile(
  36. optimizer=keras.optimizers.Adam(0.001),
  37. loss=keras.losses.BinaryCrossentropy(from_logits=True),
  38. metrics=['accuracy']
  39. )
  40. history = model.fit(train_db,epochs=10)
  41. import matplotlib.pyplot as plt
  42. plt.plot(history.history['loss'])
  43. plt.title('model loss')
  44. plt.ylabel('loss')
  45. plt.xlabel('epoch')
  46. plt.show()
  47. model.evaluate(test_db)

        也可以直接写在一个LSTM中:

  1. class myLSTM(keras.Model):
  2. def __init__(self):
  3. super().__init__()
  4. self.embedding = layers.Embedding(input_dim=100000,output_dim=100)
  5. self.lstm_0= layers.LSTM(64 ,return_sequences=True) #与RNN同理除了最后一个前面所有都需要一个return
  6. self.lstm_1 = layers.LSTM(32)
  7. self.fc = layers.Dense(1)
  8. def call(self,inputs):
  9. x = self.embedding(inputs)
  10. x = self.lstm_0(x)
  11. x = self.lstm_0(x)
  12. x = self.fc(x)
  13. x = tf.sigmoid(x)
  14. return x

        LOSS收敛图:

         测试机上的正确率:


7. GRU (理论+实战) 

        前面说了,LSTM主要是为了解决梯度弥散的问题,但是其有个缺点就是计算复杂。

        三个门复杂,参数量大。如何进行优化呢?

        经过一些研究发现,遗忘门在LSTM中是最重要的一个门,这个已经在2018年的论文中得到证实,甚至发现只有遗忘门在多个数据集上能有良好的表现,甚至有些数据集上只有遗忘门的网络更优于3个门都有的网络。

        在很多简化版LSTM中GRU门控循环网络是引用最广泛的LSTM变种。

        GRU把状态向量,输出向量做出了一个合并。统一成为一个向量h,且把门控减少到2

        复位门(重置门):控制上一个时间戳的状态ht-1进入GRU网络的一个量 。他不再像LSTM中用2个输入c和h,这里只有一个输入h。

         至于上一时段的记忆通过什么来控住阈值呢?还是g作为门控。至于Wr和br只是对记忆h和输入的信息做一个特征提取。然后上一个记忆ht-1经过门控g的控制,再与当前信息x作为输入经过Wr和br的特征提取,最后经过一个激活函数tanh即可。

        更新门:控制上一个时间戳上的记忆ht-1和新的ht_hat。

       

        首先对上一个记忆ht-1和当前信息进行特征提取,并经过一个sigmoid,g这里也是阀门。ht-1经过的阀门与ht_hat加起来为1,即此消彼长,二者相互竞争的关系,最后流向ht。

        具体代码:

  1. import tensorflow as tf
  2. from tensorflow import keras
  3. from tensorflow.keras import layers
  4. (x_train, y_train), (x_test, y_test) = tf.keras.datasets.imdb.load_data()
  5. (x_train.shape, y_train.shape), (x_test.shape, y_test.shape)
  6. # 我们希望每个输入的长度都是相同的
  7. x_train = keras.preprocessing.sequence.pad_sequences(x_train,maxlen=250)
  8. x_test = keras.preprocessing.sequence.pad_sequences(x_test,maxlen=250)
  9. # 整合成一个train_db,和test_db
  10. train_db = tf.data.Dataset.from_tensor_slices((x_train,y_train))
  11. test_db = tf.data.Dataset.from_tensor_slices((x_test,y_test))
  12. train_db = train_db.shuffle(10000).batch(128 , drop_remainder=True) # drop_remainder多余的就不要了
  13. test_db = test_db.shuffle(10000).batch(128 , drop_remainder=True)
  14. class myGRU(keras.Model):
  15. def __init__(self):
  16. super().__init__()
  17. #可以吧两个输入整个作为一个输入
  18. self.state_0 = [tf.zeros([128,64])]
  19. self.state_1 = [tf.zeros([128,32])]
  20. self.embedding = layers.Embedding(input_dim=100000,output_dim=100)
  21. self.gru_cell_0 = layers.GRUCell(64)
  22. self.gru_cell_1 = layers.GRUCell(32)
  23. self.fc = layers.Dense(1)
  24. def call(self,inputs):
  25. x = self.embedding(inputs)
  26. state_0 = self.state_0
  27. state_1 = self.state_1
  28. for x in tf.unstack(x,axis = 1):
  29. out_0 , state_0 = self.gru_cell_0(x,state_0)
  30. out_1 , state_1 = self.gru_cell_1(out_0,state_1)
  31. x = self.fc(out_1)
  32. x = tf.sigmoid(x)
  33. return x
  34. model = myGRU()
  35. model.compile(
  36. optimizer=keras.optimizers.Adam(0.001),
  37. loss=keras.losses.BinaryCrossentropy(from_logits=True),
  38. metrics=['accuracy']
  39. )
  40. history = model.fit(train_db,epochs=10)
  41. import matplotlib.pyplot as plt
  42. plt.plot(history.history['loss'])
  43. plt.title('model loss')
  44. plt.ylabel('loss')
  45. plt.xlabel('epoch')
  46. plt.show()
  47. model.evaluate(test_db)
  48. class myGRU(keras.Model):
  49. def __init__(self):
  50. super().__init__()
  51. self.embedding = layers.Embedding(input_dim=100000,output_dim=100)
  52. self.gru_0= layers.GRU(64 ,return_sequences=True) #与RNN同理除了最后一个前面所有都需要一个return
  53. self.gru_1 = layers.GRU(32)
  54. self.fc = layers.Dense(1)
  55. def call(self,inputs):
  56. x = self.embedding(inputs)
  57. x = self.gru_0(x)
  58. x = self.gru_1(x)
  59. x = self.Drop(x)
  60. x = self.fc(x)
  61. x = tf.sigmoid(x)
  62. return x
  63. # 为了防止过拟合
  64. 可以加入Dropout
  65. # 为了防止梯度爆炸
  66. 可以加入正则化项
  67. class myGRU(keras.Model):
  68. def __init__(self):
  69. super().__init__()
  70. self.embedding = layers.Embedding(input_dim=100000,output_dim=100)
  71. self.gru_0= layers.GRU(64 ,return_sequences=True) #与RNN同理除了最后一个前面所有都需要一个return
  72. self.gru_1 = layers.GRU(32)
  73. self.bn = layers.BatchNormalization()
  74. self.Drop = layers.Dropout(0.5)
  75. self.fc = layers.Dense(1)
  76. def call(self,inputs):
  77. x = self.embedding(inputs)
  78. x = self.gru_0(x)
  79. x = self.gru_1(x)
  80. x = self.Drop(x)
  81. x = self.fc(x)
  82. x = tf.sigmoid(x)
  83. return x
  84. model = myGRU()
  85. model.compile(
  86. optimizer=keras.optimizers.Adam(0.001),
  87. loss=keras.losses.BinaryCrossentropy(from_logits=True),
  88. metrics=['accuracy']
  89. )
  90. history = model.fit(train_db,epochs=10)

        Loss1:

        训练集和测试集上的准确率:0.943,0.8345。

        改进后Loss2(没有加入正则项,因为发现加入正则项无法收敛):

         训练集和测试集上的准确率:0.9529,0.8516 (10个epoch)0.9732,0.8598(20个epoch)0.9605,0.8397(学习率=0.0004,再训练10次)

8. 深层循环神经网络

        之前说过CNN由于梯度爆炸和梯度弥散,很难进行深层训练。而LSTM主要就是用来解决梯度弥散,至于梯度爆炸可以用梯度截断的方式。

        因为GRU(LSTM的改版)解决了梯度弥散的问题,于是循环神经网络就可以变得更加深层。

        深层循环神经网络有以下几种形式:

  •         堆叠循环神经网络:就是之前的纵向计算,之前是堆叠了两层。当然还可以继续堆叠。
  •         双向循环神经网络:顺序方向和逆序方向分别训练,后进行纵向叠加。

        应用场景:

         RNN的特点:引入记忆,但是长程依赖问题,或是记忆容量问题,很难深层。

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/运维做开发/article/detail/1012036
推荐阅读
相关标签
  

闽ICP备14008679号