当前位置:   article > 正文

PyTorch-09 循环神经网络RNN&LSTM (时间序列表示、RNN循环神经网络、RNN Layer使用、时间序列预测案例、RNN训练难题、解决梯度离散LSTM、LSTM使用、情感分类问题实战)_rnn多变量时间序列预测pytorch

rnn多变量时间序列预测pytorch

PyTorch-09 循环神经网络RNN&LSTM (时间序列表示、RNN循环神经网络、RNN Layer使用、时间序列预测案例(一层的预测点的案例)、RNN训练难题(梯度爆炸和梯度离散)和解决梯度爆炸问题grad clipping、解决梯度离散问题LSTM、LSTM使用、情感分类问题实战)

一、时间信号序列表示

空间域信号Spatial Signals

图片类型数据,视频类似数据这种2D的数据,可以使用卷积神经网络进行处理。
在这里插入图片描述

时间信号?Temporal Signals?

语音、文字这些数据都有时间先后顺序
在这里插入图片描述

序列Sequence

对于图片数据,我们使用像素的RGB值表示图片的色彩度。

那么对于语音、文字这种类型的信号如何表示呢?
对于语音的话,每个时间段会有一个波形,这个波形的波峰值,就可以表示此刻声音的强度。
对于文字,文字的字符,也可以表示;但是pytorch没有自带支持string字符的,pytorch处理的都是数值类型。因此,需要想一种办法,将string类型表示为另一种数值类型。这种表示方法叫做representation或者词向量word embedding。

Sequence representation序列表征

对于word embedding,我们说一句话,这句话有五个单词,每个单词用一个向量来表示。这样第一个维度seq_len,可能表示单词的数量5;第二个维度feature_len,可能表示每个单词的表示方法,取决于应用场景。
在这里插入图片描述
例子:
1、房价的变化趋势
[month/days, 当前节点的价值(用一个数值类型来表示即可)]
在这里插入图片描述
2、图片数据
图片数据也可用词向量word embedding来进行理解,每行可以视为一个embedding,一个需要扫描28行,通过这样理解也可以获得一张完整图片。
但是利用后面要学的RNN来处理这种图片数据,没有CNN的方式来的优秀。
在这里插入图片描述

文本信息的表达方式1 [words, word_vec]

假设单词有3500个,这里使用了5个单词。
那么文本信息的表达为[5, 3500]。其中3500是独热编码的形式。
在这里插入图片描述

文本信息的表达方式2 [words, word_vec]

one-hot编码的缺点:独热编码是非常稀疏的,比如一个3500长的vector,只有一个位置为1,其他位置为0,因此是非常非常稀疏的,而且占据了大量的空间,但其表示的信息非常非常小,同时维度还非常高,对于英文常用单词有二万多,其维度非常高了,因为one-hot这种特性的存在(非常稀疏、占据大量空间、维度非常高),所以在实际应用中可能基本上用不了。

因此我们使用另一种方式:这种方式更加主流,更加有意义。
我们让这个vector不能这么稀疏,让其更加dense稠密,维度小一点;
同时还有一个更重要的特性是,比如说,语言本身是存在semantic similarity
语义相关性的(猫、狗,同为动物;happy、unhappy,同为情绪,这些都存在语义相关性)。因此我们在转换为编码的时候,也需要利用这种特性,做这种等价的转换。

这里我们举个例子来表示如何利用语义相关性进行编码:(以权力游戏小说的人物关系进行语义相关性编码)
对于King这个单词,我做embedding的时候搜索发现,与King语义最接近的是Kings。
这里的king和kings的相似度是如何计算的呢?
一个vector表示king,另一个vector表示kings,这两个vector存在一个夹角θ,计算这两单词vector的cosθ的值,值越大,这个夹角越小,说明这两个单词的相似度是最高的。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
因此我们采用这种embedding模式,这种embedding一般是我们已经得到了现成的,我们如果不是做NLP(自然语言处理)的话,我们就不用了解过多NLP的相关知识。

因此就直接介绍两种编码的方式,一个是word2vec;另一个是GloVe。
在这里插入图片描述

Batch批处理

在CNN中为了提高train效率,增加了batch。
这里也是为了提供效率,增加了batch维度。
这里对于文本的内容的表达有两种形式:
▪ [word num, b, word vec] 这种是b方中间。
▪ [b, word num, word vec] 这种是b方开头。
在这里插入图片描述

word2vec vs GloVe

word2vec

在pytorch中可以很方便的使用nn.Embedding()进行查表操作。

word sequence representation的表达方式其实就是非常简单的查表,首先对单词编一个索引号,比如说有4万个单词,就编一个0~4万的索引,根据索引取查询一张表word vector lookup table!单词矢量查找表!。这张表示提前存好的,只要告诉pytorch,你有多少个单词数量,这里我们有4万个,那么我们的单词矢量查找表就有4万行,每个单词使用300个word vector feature来表达,单词矢量查找表的列数就是300。查表的时候,只需要给embedding layer传一个index就可以了,这样就会获得索引对应的word vector feature,这个word vector feature及时这个单词的word representation。

下图我们的embedding是没有初始化的,所以nn.Embedding layer的这张表是随机生成的,所以获得的word vector feature是非常random的。

如何初始化呢?一般我们使用现成的编码方式,比如word2vec或GloVe。我们将这些编码方式下载下来以后,数据集可能有几GB的样子,让后我们将数据的内容填充到单词矢量查找表中,这样这张表就可以直接使用了。这里需要注意,这张表是不能优化的。
在这里插入图片描述

GloVe

直接使用GloVe方式,实现一个查表操作。这里我们使用一个pytorch面向NLP处理的一个包。得到这个包后,直接使用GloVe()方式,这个是一种编码方式,会下载一个2.2GB的文件,下载好后,直接输入单词,就可以获得这个单词所对应的word vector feature。
在这里插入图片描述

二、RNN循环神经网络(对时间信号序列处理的网络类型)

情感分析Sentiment Analysis

比如这句话:
I hate this boring movie 这句话是一个负面评价。
那么对于机器来说如何才能解决这个情感理解呢?
我们已经知道了如何对单词来表示,这里使用GloVe方式,每个单词都会获得一个100维的word vector feature [100]。整句话有五个单词,那么就是[5,100]的tensor来表示这句话。
我们将每个单词的[100]的tensor与一个线性层Linear相连接,来提取一部分特征,即将[100]做为x,输入到x@w+b的线性层中,通过这个线性层来抽取一些高层的特征,比如说抽取出2维的特征[2],通过这种方式,我们有5个线性层与5个单词的[100]相对应,每个线性层都有两个参数w和b,最后我们再用一个线性层,将五个线性层抽取的五个[2]进行聚合输出,即[5,2]的一个输出,输出维度是[1],代表着表示积极的概率,这样就是一个二分类的问题。
在这里插入图片描述

上面这种方式的缺陷Flaws

▪ Long sentence对于长句子
▪ 100+ words
▪ too much parametes [w, b]

▪ no context information没有上下语境的信息(即缺少上一句和下一句语义关系信息)
▪ consistent tensor 需要协调一致性的单元,希望能存储一个语境信息,从而将这个语境信息利用起来,从而处理时间信号序列才能得心应手。

在这里插入图片描述

如何处理时间信号序列呢(包含有上下语句的情感)?Naïve version

最初始的版本:
在这里插入图片描述
Weight sharing:减少网络的参数量
首先,我们希望权值w能够得到共享,这样我们能够减少网络的参数量,这样能够很好的处理更长的句子。
在这里插入图片描述
consistent memory:用于保存语境信息
需要一个单元,这个单元能够贯穿整个语境,每次train不仅仅是支持当前的一个输入,应该还需要一个语境单元来帮助更好的分析和提取出特征。
下图中的h0表示上一个单元,或则网络初始化的一个输入,这个单元要贯穿整个网络用于保存语境信息。因此不仅考虑到了x的输入,还考虑到了上一个单元或则初始化时的一个输入,根据第一个单词和初始化输入会得到第一个单词的语境信息h1,到第二个单词时,会考虑第二单词x和上一个单词的语境信息h1,通过这种模式,从而实现了语境的贯穿。

这里的h就相当于memory,用于存储语境信息。
在这里插入图片描述
合并起来Folded model
我们将这个展开的信息合并在一起:
当前的memory初始化为h0 [0,0,0,…],feature是 [5,3,100] 分别表示,1句话有5单词, 3个batch, 每个单词以100维word vector feature向量表示,对于每个单词xt为[3,100]。我们将当前的xt和初始化的memory h0合在一起考虑,会得到一个新的memory,将这个新的memory称为ht。得到了新的memory会更新自我(这里就是RNN与CNN的区别,CNN会一直往前传,RNN会有一个核心变量ht,这个ht就类似于语境信息,语境信息会不停的自我更新,每次自我更新都取决于上一次的语境和当前的输入),通过不断的更新memory以及输入,从而获得最终的输出。
在这里插入图片描述
展开Unfolded model
每次都会获得一个语境信息(h1,h2,h3,…)。
输出到底是取决于那个h呢,这完全是由自己决定,可以采用最后时间的ht,也可以将每个时间的ht综合起来再做一个聚合,也可以取中间某个时间的语境信息。这个是非常自由的,完全取决于自己。
在这里插入图片描述

将上面的model规范化一下

这里的激活函数不在是sigmoid或则relu了,是tanh双曲正切函数,其范围是(1,-1)
在这里插入图片描述
在这里插入图片描述
hₜ = tanh ( xₜ @ wₓₕ + hₜ ₋ ₁ @ wₕₕ )
在这里插入图片描述
既然RNN,我们使用了权值共享和memory这种方式在建立模型,是否可以使用梯度下降这种方式呢?

How to train?
h0一般初始化都是[0,0,0,…]
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
这里需要注意的是,∂hₖ /∂h₁ 这个梯度会带来一些问题的,每一步的梯度信息都会有一个Wₕₕ(就是图上的Wʀ)矩阵相乘,最终会有Wₕₕ^(k-i),如果i=0,这有Wₕₕ⁽ᵏ⁾,这个才是问题关键所在,梯度里会有一个Wₕₕ⁽ᵏ⁾。
在这里插入图片描述

三、RNN Layer使用

回顾RNN的原理

h这个tensor:[10个单词, batch=3句话, feature len=100(每个单词用一个100维的向量表示)] = [10, 3, 100]

xt这个tensor:[ 3(这里的3表示:一次送batch三句话的各一个单词), 100(每个单词用一个100维的向量表示) ] = [3, 100]

需要更新的单元公式:xₜ @ wₓₕ + hₜ @ wₕₕ = [batch, feature len] @ [hidden len, feature len]ᵀ + [batch, hidden len] @ [hidden len, hidden len]ᵀ

首先是xₜ @ wₓₕ,这两个矩阵相乘 [batch, feature len] @ [hidden len, feature len]ᵀ,[batch, feature len] = [3, 100],[hidden len, feature len]ᵀ = [20, 100]ᵀ = [100, 20] (这里我们假设memory是一个20维的向量,因此hidden len是20) , xₜ @ wₓₕ = [3, 20] => 代表3句话,每句话的当前单词memory的表达方式。

接下来是上一个时间戳的memory,hₜ @ wₕₕ = [batch, hidden len] @ [hidden len, hidden len]ᵀ,[batch, hidden len] = [3, 20],[hidden len, hidden len]ᵀ = [20, 20]ᵀ,hₜ @ wₕₕ = [3, 20]

最后聚合一下 ( xₜ @ wₓₕ = [3,20] ) + ( hₜ @ wₕₕ =[3,20] ) = [3,20] = h ₜ₊₁

因此,最初始的memory不能初始化为[20],必须初始化为[batch, 20],这才能很好的进行矩阵相乘。当然,如果只初始化为[20],可以通过boardcasting机制变成一样的shape,这里需要让我们从原理层面理解,这里初始是一个[batch,20]的memory。
在这里插入图片描述
验证一下,上面分析的是否合适:

input dim:这个就是单词所对应的word vector feature;
hidden dim:就是memory的size,用一个多长的向量来表达memory。

from torch import nn

#参数1:word vector(word dim),100维的向量来表示一个单词。
#参数2:表示memory(即:h这个shape),10维表示memory
rnn = nn.RNN(100,10) #input dim, hidden dim

#rnn._parameters.keys()可以查看有多少个tensor
print(rnn._parameters.keys())
#打印出的结果:odict_keys(['weight_ih_l0', 'weight_hh_l0', 'bias_ih_l0', 'bias_hh_l0'])
#['weight_ih_l0', 'weight_hh_l0', 'bias_ih_l0', 'bias_hh_l0']
#其中,l0表示0层;第一个是wᵢₕ;第二个是wₕₕ;第三个是bᵢₕ;第四个是bₕₕ
#为什么要加一个l0呢? 循环神经网络不止有一层,也可以有多层。

#首先我们查看一下wₕₕ
print(rnn.weight_hh_l0.shape)

#再查看一下wᵢₕ
print(rnn.weight_ih_l0.shape)

#最后查看一下bias
print(rnn.bias_hh_l0.shape)
print(rnn.bias_ih_l0.shape)

print(rnn.bias_hh_l0)
print(rnn.bias_ih_l0)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25

在这里插入图片描述

在pytorch中如何使用nn.RNN

nn.RNN(参数1,参数2,参数3)
参数1:input dim:这个就是单词所对应的word vector feature;
参数2:hidden dim:就是memory的size,用一个多长的向量来表达memory。
参数3(选填,默认是1):num_layers:表示循环层数Number of recurrent layers。
在这里插入图片描述

nn.RNN所对应的forward函数是怎么定义的呢?

out, ht = forward(x, h0)

比如说[5,3,100],有五个单词,一次3句话,每个单词用100维的向量来表达。我们是将这个完整的全部数据直接传给参数x。即forward(x, h0)中x = [5,3,100]。因此只需提供一个h0即可。h0=[layer层数, batch, hidden dim=10],也可以不写,不写的话就是初始为0的状态。

out, ht = forward(x, h0)得到两个返回值:

其中ht表示每个t时刻的返回,hT表示最后ht最后时刻的返回,hT = [如果只有1层,有b个句子,每个句子在每层上面的状态是10 ],即ht = [layer = 1, batch = 3, hidden dim = 10],h是所有层上每层中最后一个时间戳的状态,如果有两层,ht = [2,3,10]。

返回值out表示每一个时间戳 [h0,h1,h2,…ht] 的最后一层所有输出ht的状态,即out = [word = 5,3,10],有五个单词就意味着要送五次,这样就会获得5个[3,10],就意味着是5个这样的ht=[1,3,10]在一起了,因此out是一个聚合过的信息[5,3,10]。

下图是一个例子:
返回的h是所有层上每层中最后一个时间戳的状态,因为这里只有1层,为[1,3,20]。
对于返回的out,是最后一层所有时间戳的状态,因为这里只有1层,因此有10个[3,20],即[10,3,20]。
在这里插入图片描述
在这里插入图片描述

单层RNN (Single layer RNN)

from    torch import nn
import torch

#参数1:word vector(word dim),100维的向量来表示一个单词。
#参数2:表示memory(即:h这个shape),20维表示memory
#参数3:表示层数,这里设置为1层
rnn = nn.RNN(input_size=100,hidden_size=20,num_layers=1)
print(rnn)

#输入
#10表示单词的数量,3表示batch句子的数量,100是word_vector(这里必须和RNN里的参数1匹配上)。
#h0也可以不写。
x = torch.randn(10,3,100)

#做forward
# out, ht = forward(x, h0)
# 这里的h0 = torch.zeros(1,3,20) 表示1层,3个句子,memory的shape是20,这里的20必须和RNN中参数2匹配上。
out, h = rnn(x,torch.zeros(1,3,20))
print(out.shape,h.shape)
#返回的h是所有层最后一个时间戳的状态,因为这里只有1层,为[1,3,20]
#对于返回的out,是最后一层所有时间戳的状态,因为这里只有1层,因此有10个[3,20],即[10,3,20]

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22

在这里插入图片描述

两层RNN (2 layer RNN)

这里是两层的RNN,一共有两个h0,分别是第一层的h₀¹和第二层的h₀²。

由于有两层,out, ht = forward(x = torch.randn(10,3,100), h0)这里的h0 = torch.zeros(2,b,20) 表示2层,b个句子,memory的shape是20,这里的20必须和RNN中参数2匹配上。

out, h = rnn(x,torch.zeros(2,b,20)),因此forward 返回的h是所有层最后一个时间戳的状态,因为有两层h为[2,b,20]。

对于返回的out,是最后一层所有时间戳的状态,因为有两层,所有是第二层的所有时间戳的状态,因为有10个单词,所以对应的有10个ht,所以out为[10,b,20]
在这里插入图片描述

from    torch import nn
import torch

#参数1:word vector(word dim),100维的向量来表示一个单词。
#参数2:表示memory(即:h这个shape),10维表示memory
#参数3:表示层数,这里设置为2层
rnn = nn.RNN(input_size=100,hidden_size=10,num_layers=2)
print(rnn)

print(rnn._parameters.keys())
# 更新的单元公式:xₜ @ wₓₕ + hₜ @ wₕₕ = [batch, feature len] @ [hidden len, feature len]ᵀ + [batch, hidden len] @ [hidden len, hidden len]ᵀ
print('l0层:')
print(rnn.weight_ih_l0.shape)# wih = torch.Size([10, 100]) 这里是需要将100维word vector feature转为memory shape的10。
print(rnn.weight_hh_l0.shape)# whh = torch.Size([10, 10])
print('l1层:')
print(rnn.weight_ih_l1.shape)# 而对于第二层来说,就不需要转换了,第二层的输入就是第一层的memory。因此是torch.Size([10, 10])
print(rnn.weight_hh_l1.shape)#whh = torch.Size([10, 10])

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

在这里插入图片描述

四层RNN (4 layer RNN) [T, b, h_dim], [layers, b, h_dim]

from    torch import nn
import torch

#参数1:word vector(word dim),100维的向量来表示一个单词。
#参数2:表示memory(即:h这个shape),20维表示memory
#参数3:表示层数,这里设置为4层
rnn = nn.RNN(input_size=100,hidden_size=20,num_layers=4)
print(rnn)

x = torch.randn(10,3,100) #10个单词,3句话,每个单词用100维的向量来表达。
out,h = rnn(x) #这里我们没有填入h0,如果要填h0=[4,3,20],4层,3句话,memory的shape是20,这里的20必须和RNN中参数2匹配上。
print(out.shape,h.shape)
#返回的h是所有层最后一个时间戳的状态,因为这里有4层,为[4,3,20]
#对于返回的out,是最后一层所有时间戳的状态,因为这里有4层,返回第四层的所有时间戳的状态,因此有10个[3,20],即[10,3,20]
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

在这里插入图片描述

RNN的第二种方式 nn.RNNCell

之前所涉及的RNN,都是一步到位的输入x,这个x是包含了整个要传入的训练的句子的,比如x = torch.randn(10,3,100) ,10个单词,3句话,每个单词用100维的向量来表达。
按照最开始的讲解,我们会在时间戳上传递10次[3,100],即仅一次的话,是传递xt = [3,100],我们这有10个单词,因此是[10,3,100]。对于这种一次一次手动传递一个单词,且不进行更新T次的方式,pytorch也提供了这种手动的形式,即nn.RNNCell。这个类的初始化参数与nn.RNN的参数是一模一样的。
在这里插入图片描述

nn.RNNCell所对应的forward函数

nn.RNNCell所对应的forward函数与nn.RNN的forward是不同的。
▪ ht = cell(xt, ht_1)
因此这里只送入一个单词[3,100],memory的状态也不一样了[4,3,10],表示4层,3句话,每一句话的memory shape是10。
▪ xt: [b, word vec]
▪ ht_1/ht: [num layers, b, h dim]
▪ out = torch.stack([h1, h2,…, ht]) 表示所有h状态的一个集合

使用nn.RNNCell编写1层

from    torch import nn
import torch

#参数1:word vector(word dim),100维的向量来表示一个单词。
#参数2:表示memory(即:h这个shape),20维表示memory
#参数3:表示层数,这里设置为1层
cell1 = nn.RNNCell(100,20)
h1 = torch.zeros(3,20) #3句话,20维表示memory

#下面这步是关键,人为手动循环
x = torch.randn(10,3,100) #10个单词,3句话,100维的向量来表示一个单词
for xt in x: #循环10次
    h1 = cell1(xt,h1) #这里xt为[3,100],传入的h1为[3,20]

print(h1.shape) #这里的是返回的h1

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

在这里插入图片描述

使用nn.RNNCell编写2层

from    torch import nn
import torch

#参数1:word vector(word dim),100维的向量来表示一个单词。
#参数2:表示memory(即:h这个shape),30维表示memory
#参数3:表示层数,这里设置为1层
cell1 = nn.RNNCell(100,30) #第一层是将100的输入,变成30的memory
cell2 = nn.RNNCell(30,20) #第二层是将30的memory,变成20的memory
h1 = torch.zeros(3,30)
h2 = torch.zeros(3,20)

x = torch.randn(10,3,100) #10个单词,3句话,100维的向量来表示一个单词

for xt in x:
    h1 = cell1(xt,h1)#这里我们填入h1,如果要填h1=[3,30],3句话,memory的shape是30,这里的30必须和第一层RNNCell中参数2匹配上。
    h2 = cell2(h1,h2)#这里我们填入h2,如果要填h2=[3,20],3句话,memory的shape是20,这里的20必须和第二层RNNCell中参数2匹配上。

print(h2.shape)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

在这里插入图片描述

四、时间序列预测案例(一层的预测点的案例)

这里我们用RNN来预测正弦曲线的下一时期的分布情况。
在这里插入图片描述

Sample data

import  numpy as np
import  torch
import  torch.nn as nn
import  torch.optim as optim
from    matplotlib import pyplot as plt

start = np.random.randint(3,size=1)
print(start)
#刚开始点的相位,即最开始的点的数据,我们称为start,以start开始的往后50个点的数据。
#start是一个随机值,这是为了防止如果每次都给相同的曲线,下一下再经过同一start时刻点的曲线是固定的,这样网络会记住,为了防止网络记住,所以start点要随机。
start = start[0]
print(start)

num_time_steps = 50 #50个点的数据
time_steps = np.linspace(start,start+10,num_time_steps)

data = np.sin(time_steps)
print(data.shape)
data = data.reshape(num_time_steps,1)
print(data.shape)


#这里x是[1句话,50个单词(0-49),1维表示单词的特征向量],这里单词指数值,数值就可以直接用对应的数值表示特征向量。
x = torch.tensor(data[:-1]).float().view(1,num_time_steps-1,1) #这里的data取的是0~48
y = torch.tensor(data[1:]).float().view(1,num_time_steps-1,1) #这里的data取的是1~49
#这样就是只预测一个新的点。
print(x.shape)
print(y.shape)
print((x.numpy().ravel() == x.data.numpy().ravel()).sum())
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29

在这里插入图片描述

Network

#network
#新建一个模块,继承自nn.Module类
input_size = 1
hidden_size = 16
output_size = 1
lr=0.01
class Net(nn.Module):
    def __init__(self):
        super(Net, self).__init__()
        self.rnn = nn.RNN(
            input_size=input_size, #这里不需要embedding,输入的是数值,所以直接填1维。
            hidden_size=hidden_size, #这里memory为10
            num_layers=1, #1层
            batch_first=True, #这里只有输入的是[batch, sequence word, vector word feature] batch在最开始,才需要batch_first = True
        )
        #输出层:将memory的大小,直接输出为output size=1
        self.linear = nn.Linear(hidden_size, output_size)

    def forward(self,x,hidden_prev): #这里hidden_prev就是h0=[1,1,16],1句话,1层,memory shape为16
        out, hidden_prev = self.rnn(x, hidden_prev) #forward函数
        # 返回的h是所有层最后一个时间戳的状态,因为这里有1层,为[1,1,16],1句,1层,16维的memory。
        # 对于返回的out,是最后一层所有时间戳的状态,即[1,50,16] 1句,50个单词sequence,16维的memory。

        #我们需要收集out,因此将out打平送到下一层
        out = out.view(-1,hidden_size)
        out = self.linear(out) #[word sequence, h] => [sequence, 1]
        out = out.unsqueeze(dim=0) # =>[1, seq ,1] 这里是在[seq,1]的seq前面插一个维度,这是为了让这个out能够和y进行均方差计算。
        return out, hidden_prev
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28

Train

#train
model = Net() #获得这个网络的实例
criterion = nn.MSELoss() #mse均方误差
optimizer = optim.Adam(model.parameters(),lr) #Adam优化器

hidden_prev = torch.zeros(1,1,hidden_size) #这里就是h0
#循环6000次
for iter in range(6000):
    #生成x和y数据的样本对。
    start = np.random.randint(10,size=1)[0]
    time_steps = np.linspace(start,start+10,num_time_steps)
    data = np.sin(time_steps)
    data = data.reshape(num_time_steps,1)
    # 这里x是[1句话,50个单词(0-49),1维表示单词的特征向量],这里单词指数值,数值就可以直接用对应的数值表示特征向量。
    x = torch.tensor(data[:-1]).float().view(1, num_time_steps - 1, 1)  # 这里的data取的是0~48
    y = torch.tensor(data[1:]).float().view(1, num_time_steps - 1, 1)  # 这里的data取的是1~49

    output, hidden_prev = model(x,hidden_prev)
    hidden_prev = hidden_prev.detach()
    #.detach()返回一个新的tensor,从当前计算图中分离下来的,但是仍指向原变量的存放位置,不同之处只是requires_grad为false,得到的这个tensor永远不需要计算其梯度,不具有grad。

    loss = criterion(output,y) #进行mse误差计算
    #这个loss用来更新模型参数whh-l0和wih-l0
    model.zero_grad()
    loss.backward()
    optimizer.step()

    if iter %100 ==0:
        print("Iteration: {} loss {}".format(iter, loss.item()))

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30

Predict

#Predict
predictions = []
#原来的x是[1,50,1],1句batch,50个单词seq,1维vector word feature
#这里我们随便给一个单词(这里单词其实指的是数值)
input = x[:,0,:] #x=[b,1]
for _ in range(x.shape[1]):
    input = input.view(1,1,1)
    (pred, hidden_prev) = model(input, hidden_prev)
    input = pred #用预测值作为下一个点的input
    predictions.append(pred.detach().numpy().ravel()[0])
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

在这里插入图片描述

将上面每部分的代码整合一下:

import  numpy as np
import  torch
import  torch.nn as nn
import  torch.optim as optim
from    matplotlib import pyplot as plt

start = np.random.randint(3,size=1)
print(start)
#刚开始点的相位,即最开始的点的数据,我们称为start,以start开始的往后50个点的数据。
#start是一个随机值,这是为了防止如果每次都给相同的曲线,下一下再经过同一start时刻点的曲线是固定的,这样网络会记住,为了防止网络记住,所以start点要随机。
start = start[0]
print(start)

num_time_steps = 50 #50个点的数据
time_steps = np.linspace(start,start+10,num_time_steps)

data = np.sin(time_steps)
print(data.shape)
data = data.reshape(num_time_steps,1)
print(data.shape)

#这里x是[1句话,50个单词(0-49),1维表示单词的特征向量],这里单词指数值,数值就可以直接用对应的数值表示特征向量。
x = torch.tensor(data[:-1]).float().view(1,num_time_steps-1,1) #这里的data取的是0~48
y = torch.tensor(data[1:]).float().view(1,num_time_steps-1,1) #这里的data取的是1~49
#这样就是只预测一个新的点。
print(x.shape)
print(y.shape)
print((x.numpy().ravel() == x.data.numpy().ravel()).sum())

#network
#新建一个模块,继承自nn.Module类
input_size = 1
hidden_size = 16
output_size = 1
lr=0.01
class Net(nn.Module):
    def __init__(self):
        super(Net, self).__init__()
        self.rnn = nn.RNN(
            input_size=input_size, #这里不需要embedding,输入的是数值,所以直接填1维。
            hidden_size=hidden_size, #这里memory为10
            num_layers=1, #1层
            batch_first=True, #这里只有输入的是[batch, sequence word, vector word feature] batch在最开始,才需要batch_first = True
        )
        #输出层:将memory的大小,直接输出为output size=1
        self.linear = nn.Linear(hidden_size, output_size)

    def forward(self,x,hidden_prev): #这里hidden_prev就是h0=[1,1,16],1句话,1层,memory shape为16
        out, hidden_prev = self.rnn(x, hidden_prev) #forward函数
        # 返回的h是所有层最后一个时间戳的状态,因为这里有1层,为[1,1,16],1句,1层,16维的memory。
        # 对于返回的out,是最后一层所有时间戳的状态,即[1,50,16] 1句,50个单词sequence,16维的memory。

        #我们需要收集out,因此将out打平送到下一层
        out = out.view(-1,hidden_size)
        out = self.linear(out) #[word sequence, h] => [sequence, 1]
        out = out.unsqueeze(dim=0) # =>[1, seq ,1] 这里是在[seq,1]的seq前面插一个维度,这是为了让这个out能够和y进行均方差计算。
        return out, hidden_prev

#train
model = Net() #获得这个网络的实例
criterion = nn.MSELoss() #mse均方误差
optimizer = optim.Adam(model.parameters(),lr) #Adam优化器

hidden_prev = torch.zeros(1,1,hidden_size) #这里就是h0
#循环6000次
for iter in range(6000):
    
    #生成x和y数据的样本对。
    start = np.random.randint(10,size=1)[0]
    time_steps = np.linspace(start,start+10,num_time_steps)
    data = np.sin(time_steps)
    data = data.reshape(num_time_steps,1)
    # 这里x是[1句话,50个单词(0-49),1维表示单词的特征向量],这里单词指数值,数值就可以直接用对应的数值表示特征向量。
    x = torch.tensor(data[:-1]).float().view(1, num_time_steps - 1, 1)  # 这里的data取的是0~48
    y = torch.tensor(data[1:]).float().view(1, num_time_steps - 1, 1)  # 这里的data取的是1~49

    output, hidden_prev = model(x,hidden_prev)
    hidden_prev = hidden_prev.detach()
    #.detach()返回一个新的tensor,从当前计算图中分离下来的,但是仍指向原变量的存放位置,不同之处只是requires_grad为false,得到的这个tensor永远不需要计算其梯度,不具有grad。

    loss = criterion(output,y) #进行mse误差计算
    #这个loss用来更新模型参数whh-l0和wih-l0
    model.zero_grad()
    loss.backward()
    optimizer.step()

    if iter %100 ==0:
        print("Iteration: {} loss {}".format(iter, loss.item()))

#Predict
predictions = []
#原来的x是[1,50,1],1句batch,50个单词seq,1维vector word feature
#这里我们随便给一个单词(这里单词其实指的是数值)
input = x[:,0,:] #x=[b,1]
for _ in range(x.shape[1]):
    input = input.view(1,1,1)
    (pred, hidden_prev) = model(input, hidden_prev)
    input = pred #用预测值作为下一个点的input
    predictions.append(pred.detach().numpy().ravel()[0])


x = x.data.numpy().ravel()
y = y.data.numpy()
plt.scatter(time_steps[:-1], x.ravel(), s=90)
plt.plot(time_steps[:-1], x.ravel())

plt.scatter(time_steps[1:], predictions)
plt.show()
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
这样就完成了一个非常简单的RNN案例

五、RNN训练难题(梯度爆炸和梯度离散)

梯度爆炸Gradient Exploding 和梯度离散Gradient Vanishing

RNN网络的梯度推导公式:
∂Eₜ / ∂Wᵣ 梯度信息 = Wₕₕᵏ 再去乘以其他东西所得到的
Wₕₕᵏ => grad梯度信息的一部分,因此会产生两个情况:
1、梯度爆炸:Wₕₕ > 1 大于1很多的话,Wₕₕ的k次方很大,Wₕₕᵏ 再去乘以其他东西,会造成梯度无穷大。
2、梯度离散:Wₕₕ < 1 小于1很多的话,Wₕₕ的k次方很小,Wₕₕᵏ 再去乘以其他东西,会造成梯度接近0。
在这里插入图片描述

梯度爆炸如何解决Gradient Exploding——grad clipping

梯度爆炸的情况是:当loss慢慢减小的时候,突然一下loss急剧的增大(即loss慢慢的从0.25=>0.24=>0.23,突然一下loss变为了1或者10)。

梯度爆炸解决方法:梯度裁切grad clipping。

当每一次计算的时候,都会检查一下w的梯度grad(每一个tensor的.grad成员变量是记录这个tensor的grad信息,.grad.norm()可以查看梯度的模,一般模在10左右比较合适),如果这个grad大于我们设置的阈值threshold,我们用当前梯度grad除以这个梯度模(一个向量除以该向量的模结果为1,长度变为了1,但是方向不变)后,再乘以我们的阈值threshold。这样操作后会将梯度约束在我们设置的阈值范围内,我们将向量的长度缩放到15,但方向保持不变,因为我们的方向代表了我们梯度下降的方向,梯度的模代表梯度前进的长度,这样操作只是将梯度前进的长度变小了,但方向不变。这里需要特别注意是对gradt梯度做处理,不是对w。
在这里插入图片描述
具体如何实现grad clipping:

for循环简单查阅一下梯度的模是否存在爆炸的情况,如果查看后发现存在,就可以将for循环中的print的部分注释掉了,在for循环内输入这行代码:torch.nn.utils.clip_grad_norm_(p, 10),这里需要注意clip_grad_norm_的norm后面要有一个小的下划线,这个小下划线表示in-place操作。(Pytorch对in-place操作使用尾随下划线约定。所以区别在于,一个带下划线的会修改原有的张量,另一个则会保留原有的张量,并返回一个新的张量。)

loss = criterion(output,y)
model.zero_grad()
loss.backward() #backward()回溯后就可以获得grad梯度信息了
#通过循环查看所有梯度的模
for p in model.parameters():
    print(p.grad.norm())
    #对每一个p的grad压缩到10内。
    torch.nn.utils.clip_grad_norm_(p, 10) #这个10就是阈值
optimizer.step() #用修改后的梯度信息,来更新w'
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

梯度离散问题:

∂Eₜ / ∂Wᵣ 梯度趋近于0,这样w会存在长时间没有更新的情况。

RNN VS LSTM(解决梯度离散出现的算法) 梯度可视化(RNN V.S. LSTM Gradient Visualization)

LSTM是解决梯度离散而出现的算法。

从最后时刻到最开始的梯度传播可视化情况。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

六、解决梯度离散问题LSTM

LSTM(long short-term memory)网络在一定程度上减少了梯度离散情况的出现。

长短期记忆(Long short-term memory, LSTM)是一种特殊的RNN,主要是为了解决长序列训练过程中的梯度消失和梯度爆炸问题。简单来说,就是相比普通的RNN,LSTM能够在更长的序列中有更好的表现。
在这里插入图片描述

原始RNN的表达方式:

这里的h就相当于memory,用于存储语境信息。
在这里插入图片描述
这里的激活函数不在是sigmoid或则relu了,是tanh双曲正切函数,其范围是(1,-1)
在这里插入图片描述
在这里插入图片描述
hₜ = tanh ( xₜ @ wₓₕ + hₜ ₋ ₁ @ wₕₕ )
在这里插入图片描述
在这里插入图片描述

LSTM的结构是如何引进来的呢?

LSTM:LSTM 具有三个门,用于保护和控制单元状态。
第一道门(遗忘门或记忆门) :对于原来的 hₜ ₋ ₁,并通过激活函数处理成为一道闸门,我们希望有一个闸门,能够让过去的的信息有目的性的过滤输出,过去的信息为Cₜ ₋ ₁ (这里符号变了);
第二道门(输入门) :对于新来的信息xₜ ,我们也有一道闸门,能够让C~(新的信息)有目的性的过滤输出;将这两个通过闸门过滤后的信息相结合,结合输出的结果是Cₜ ,也希望通过一道闸门进行控制新的信息的输出量,这里的Cₜ,也会做下一时间戳的Cₜ ₋ ₁ 。
**第三道门 ** : 将上一道门结合输出的结果Cₜ,通过激活函数处理,通过这个处理后的闸门进行控制新的信息输出,这个输出 ht 就有用于下一时间戳的输入hₜ ₋ ₁ 。

这里文字描述稍微有些乱,需要结合图片来理解:
在这里插入图片描述

因此LSTM设置了三道门(下面会详细解释三道门),这三道门其实就是经过激活函数处理后的结果,经过激活函数后输出范围被限制了,因此就可以用这个受到限制的结果来控制其他信息的输出量。(比如xₜ @ wₓₕ + hₜ ₋ ₁ @ wₕₕ通过sigmoid激活函数后,输出的结果范围是0~1,如果激活函数后的结果为0,hₜ 就不能获得以前的信息,过去的信息为Cₜ ₋ ₁ Ⓧ 0 = 0 ,过去的信息就不能获得了;如果激活函数后的结果为1,hₜ 就能够很有效的获得以前的信息,过去的信息为Cₜ ₋ ₁ Ⓧ 1 = Cₜ ₋ ₁,过去的信息就全部获得了。)

LSTM 背后的核心理念:Cell状态(The Core Idea Behind LSTMs : Cell State)

我们需要了解一下,下图中的两个符号:Ⓧ 和 ㊉ ,这两个符号都是闸门用于控制信息的状态。
Ⓧ 相乘,相乘是信息过滤的过程,是对应element-wise相乘,一个σ激活函数后的输出是0~1,Cₜ ₋ ₁ element-wise相乘后的输出为0到Cₜ ₋ ₁ 。
㊉相加,相加就是简单的信息融合,Cₜ ₋ ₁ ㊉ C~ => Cₜ 两个经过门处理后相融合,这里融合输出Cₜ 也是下一个时间戳的Cₜ ₋ ₁。
在这里插入图片描述

LSTM第一道门(遗忘门或记忆门)Forget gate

[ xₜ @ wₓₕ + hₜ ₋ ₁ @ wₕₕ ] = W [ hₜ ₋ ₁, xₜ ]
σ( xₜ @ wₓₕ + hₜ ₋ ₁ @ wₕₕ ) = σ( W [ hₜ ₋ ₁, xₜ ] + b ) = ft = Forget gate
ft(遗忘门)是经过sigmoid后的输出结果,其范围是0~1。

需要注意一下:三道门,其实都是由 hₜ ₋ ₁, 和 xₜ 这两个量来控制门开度的大小 。

过去的信息Cₜ ₋ ₁ Ⓧ ft ,这样可以获得过滤后的过去的信息,ft第一道门(sigmoid激活函数)的输出是0~1,过去的信息Cₜ ₋ ₁ 相乘(element-wise)ft后的输出为0到Cₜ ₋ ₁ 。
在这里插入图片描述

LSTM第二道门(输入门)Input gate and Cell State

这个门的开度是多少,完全由back propagation反向传播决定。
这里的 it = σ( W [ hₜ ₋ ₁, xₜ ] + b ) = 上面经过第一道门的 ft,it和ft是一样的东西,只是W和b不同。

此外第二道门是针对新来的信息xt,C~ = tanh(xₜ @ wₓₕ + hₜ ₋ ₁ @ wₕₕ ) = tanh( W [ hₜ ₋ ₁, xₜ ] + b ) ,这里的C~是对新来的信息处理后的结果。

将C~ Ⓧ it 这样可以获得过滤后的新输入的信息。
在这里插入图片描述
我们将这两个门处理后的结果进行相加,Ct = (Cₜ ₋ ₁ Ⓧ ft) ㊉ (C~ Ⓧ it) 这里的Ct也就是ht(这里符号变了)。这样第二道门处理完成了。
在这里插入图片描述

LSTM第三道门(输出门)Output

这里的 Ot = σ( W [ hₜ ₋ ₁, xₜ ] + b ) = 上面经过第一道门的 ft,以及第二道门的it,是一样的东西,只是W和b不同。

这里将Ot Ⓧ tanh(Ct) = ht,ht是最终的结果,是下一层的输入hₜ ₋ ₁ 。
在这里插入图片描述

直观Intuitive

新的输出是ht,memory是这个ht,而Ct一致维护在模型流程里面,Ct成为了存储原来信息状态的量。
在这里插入图片描述
在这里插入图片描述
LSTM会产生一下四种状态:(三道门产生的四种状态)
如果input gate 关闭(即为0),forget gate(remember gate)打开(即为1),表示全部用过去的信息(Cₜ = Cₜ ₋ ₁)。
如果input gate 打开(即为1),forget gate(remember gate)打开(即为1),表示将新的信息添加到过去的信息上面(Cₜ = Cₜ ₋ ₁ + C~ )。
如果input gate 关闭(即为0),forget gate(remember gate)关闭(即为0),表示会清除当前的memory(Cₜ = 0 + 0)。
如果input gate 打开(即为1),forget gate(remember gate)关闭(即为0),表示会覆盖过去的信息(Cₜ = C~)。
在这里插入图片描述

LSTM的三道门如何解决梯度离散的呢?How to solve Gradient Vanishing?

σCₜ / σCₜ ₋ ₁是四项的累加值,不在出现wₕₕᵏ,累加出现就不管w是大于1还是小于1,就不会在出现很大或者很小的情况了。因为累加的每部分都相互制约,不可能每项累加都是很小的或则都是很大的。

(RNN网络的梯度推导公式:
∂Eₜ / ∂Wᵣ 梯度信息 = Wₕₕᵏ 再去乘以其他东西所得到的
Wₕₕᵏ => grad梯度信息的一部分,因此会产生两个情况:
1、梯度爆炸:Wₕₕ > 1 大于1很多的话,Wₕₕ的k次方很大,Wₕₕᵏ 再去乘以其他东西,会造成梯度无穷大。
2、梯度离散:Wₕₕ < 1 小于1很多的话,Wₕₕ的k次方很小,Wₕₕᵏ 再去乘以其他东西,会造成梯度接近0。)

LSTM避免梯度离散,就是避免了求梯度的时候出现wₕₕᵏ这个情况。

RNN的hₜ ₋ ₁ 到 hₜ 没有直通的通道,都必须经过Wₕₕ,因此才出现Wₕₕᵏ 的情况。

LSTM有一个直通个通道,当input gate 关闭(即为0),forget gate(remember gate)打开(即为1),表示全部用过去的信息(Cₜ = Cₜ ₋ ₁),因此Cₜ 与 Cₜ ₋ ₁之间会很接近,在时间轴上展开,σCₜ / σCₜ ₋ ₁就会很接近1,就不会出现梯度离散和梯度爆炸的情况。
在这里插入图片描述

七、LSTM使用

第一种方式

nn.LSTM

在现实中,原始版本的RNN使用非常少。
回顾一下nn.RNN:
nn.RNN(100,20,2)
参数1:word vector(word dim),100维的向量来表示一个单词。
参数2:表示hidden的memory size(即:h这个shape),20维表示memory size。
参数3:表示层数,2层。

对于LSTM来说,与nn.RNN也是一样的,只不过增加了C这个内容。
在LSTM中,RNN原来的h的memory size变为了现在的C,现在h变成了output输出内容。

因此LSTM的初始化,nn.LSTM(100,20,2),现在参数2不止是C的memory,也是h的memory,C和h的memory size是一样的。
在这里插入图片描述

LSTM.foward()

forward函数对于RNN来说,out, ht = forward(x, h0)
比如说[5,3,100],有五个单词,一次3句话,每个单词用100维的向量来表达。我们是将这个完整的全部数据直接传给参数x。即forward(x, h0)中x = [5,3,100]。因此只需提供一个h0即可。h0=[layer层数, batch, hidden dim=10],也可以不写,不写的话就是初始为0的状态。

forward函数对于LSTM来说,out, (ht, ct) = lstm(x, [ht_1, ct_1])
▪ x: [seq, b, vec]
▪ h/c: [num_layer, b, h]
包含两个中间变量,原来RNN的h0也就是现在的ct0,和现在的ht0。也就是最开始的ht0和ct0。
返回值中包含最后一个时间戳的ht以及最后一个时间戳的ct。
▪ out: [seq, b, h]
还返回一个out,即所有时间戳的最后一层的ht的状态。因此有seq个时间戳(seq是单词数量),batch个句子,h是每一个时间戳上的输出,因为LSTM输出是h,不是c。

因此对比RNN的forward函数和LSTM的forward函数,存在两处不同,一个是RNN的输入一个h0,在LSTM是要输入h0和c0;另一个是RNN的返回值ht,在LSTM返回ht和ct。
在这里插入图片描述

nn.LSTM例子

import  numpy as np
import  torch
import  torch.nn as nn
import  torch.optim as optim
from    matplotlib import pyplot as plt

#参数1:embedding层是100的shape。
#参数2:hidden的memory shape是一个20维。
#参数3:表示层数,4层。
lstm = nn.LSTM(input_size=100, hidden_size=20, num_layers=4)
print(lstm)

#输入:
#3个句子(参数2),每个句子有10个单词(参数1),每个单词encoding成为100的vector(参数3)
x = torch.randn(10,3,100)
out,(h,c) = lstm(x) #这里还应有[h0,c0],这个是可以省略的
#对于h/c来说:[4层, batch, 20维memory], h和c每一层都有。
#out: [有多少个时间戳10(因为有10个单词),3句话,每一次保存的是最后一层的memory(所有时间戳的最后一层的ht的状态)为20]
print(out.shape)
print(h.shape)
print(c.shape)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21

在这里插入图片描述

第二种方式

nn.LSTMCell

nn.LSTMCell(input_size=100, hidden_size=20)
参数1: word vector(word dim),100维的向量来表示一个单词。
参数2:表示hidden的memory size(即:h这个shape),20维表示memory size。
在这里插入图片描述

LSTMCell.forward()

这里是有区别的,LSTM.forward()是输入x[10个单词, 3句话, 100维embedding];而在LSTMCell.forward()中是输入xt[3句话, 100维embedding],这个xt表示每3句话的每个单词。
因此这里的 [ht_1,ct_1] 为 [layer_num层数, 3句话batch, 每句话的hidden dim为20 ]。
对于返回值,没有了out的概念,out完全可以由ht推导出来,out是所有ht在一起,只取最后一层的ht,即有时间戳上最后一层所的ht。
在这里插入图片描述

1层LSTMCell的使用方法 Single layer

import  torch
import  torch.nn as nn

#===========================
print('==================')
print('one layer lstm')
cell = nn.LSTMCell(input_size=100, hidden_size=20)
h = torch.zeros(3,20)
c = torch.zeros(3,20)
#3个句子(参数2),每个句子有10个单词(参数1),每个单词encoding成为100的vector(参数3)
x = torch.randn(10,3,100)
for xt in x:
    print(xt.shape)
    h,c = cell(xt,[h,c])

print('h.shape:',h.shape)
print('c.shape:',c.shape)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

在这里插入图片描述

多层LSTMCell的使用方法 Two Layers

# ======================
print('==================')
print('two layer lstm')
cell1 = nn.LSTMCell(input_size=100, hidden_size=30)
cell2 = nn.LSTMCell(input_size=30, hidden_size=20)

#3个句子(参数2),每个句子有10个单词(参数1),每个单词encoding成为100的vector(参数3)
x = torch.randn(10,3,100)

h1 = torch.zeros(3, 30)
c1 = torch.zeros(3, 30)

h2 = torch.zeros(3, 20)
c2 = torch.zeros(3, 20)

i = 0
for xt in x:
    print(i)
    h1, c1 = cell1(xt, [h1, c1])
    h2, c2 = cell2(h1, [h2, c2])
    i+=1

print(h2.shape, c2.shape)

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24

在这里插入图片描述

八、情感分类(Sentiment Classification)问题实战

在这里插入图片描述
这里我们使用的编译环境是Google CoLab:
▪ Continuous 12 hours
▪ free K80 for GPU
▪ no need to cross GFW

下面的代码是在colab中运行的:

!pip install torch
!pip install torchtext==0.5.0
!python -m spacy download en


# K80 gpu for 12 hours
import torch
from torch import nn, optim
# 关于数据集的问题,我们应该输入抓取这个好评或者差评呢?我们将用torchtext的数据集中的IMDB数据
# torchtext中提供了IMDB的文件
from torchtext import data, datasets


print('GPU:', torch.cuda.is_available())

torch.manual_seed(123) #设置CPU生成随机数的种子
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

在这里插入图片描述

#1、首先加载数据集、glove单词word feature vector。

TEXT = data.Field(tokenize='spacy')
LABEL = data.LabelField(dtype=torch.float)

#我们将IMDB数据集中的相关信息提取出来:text和label
#text是一个list,包含了评论本身的内容;text信息就是x,这个是一个string类型,因此需要embedding一下变成vector类型。
#label是一个positive和negative的数值;label信息就是y。
train_data, test_data = datasets.IMDB.splits(TEXT, LABEL)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

在这里插入图片描述

print('len of train data:', len(train_data))
print('len of test data:', len(test_data))
  • 1
  • 2

在这里插入图片描述

print(train_data.examples[15].text)
print(train_data.examples[15].label)
  • 1
  • 2

在这里插入图片描述

# word2vec, glove
# glove下载的word feature vector
# 给TEXT建立vector
# 给LABEL建立vector
TEXT.build_vocab(train_data, max_size=10000, vectors='glove.6B.100d')
LABEL.build_vocab(train_data)


batchsz = 30
#设置运行设备为GPU
device = torch.device('cuda')
#train和test的划分:
train_iterator, test_iterator = data.BucketIterator.splits(
    (train_data, test_data),
    batch_size = batchsz,
    device=device
)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

在这里插入图片描述

#这里名字命名为RNN只是一个network的统称,网络内使用的是LSTM,因此这里就不修改了。
class RNN(nn.Module):
    
    def __init__(self, vocab_size, embedding_dim, hidden_dim):
        """
        """
        super(RNN, self).__init__()
        
        # [0-10001] => [100]
        #nn.Embedding()可以非常方便的完成对string类型需要embedding一下变成vector类型。
        #Embedding本身是一张表,这张表有多少行,就代表有多少个单词,Embedding有一个限制最多有10000行,
        #因此就可以有9998个单词 + 1个标记为不知道 + 1还有就是标记为特殊标记符号 = 10000行
        #也即是说,一共能记住9998个单词,其他都标记为不认识的单词
        #nn.Embedding()中,参数1:单词的数量vocab_size为10000(即只需编码10000个单词);参数2:embedding_dim为100,表示每一个单词编码为100维的vector。
        self.embedding = nn.Embedding(vocab_size, embedding_dim) #这样生成一个行数10000,列数为100。[10000,100]
        
        # [100] => [256]
        #nn.LSTM(): 
        #参数1:emneding_dim为100,表示每个单词用100维的vector编码。
        #参数2:memory size是256,因此c/h都是256的dimension
        #参数3:层数为2
        
        #参数4:bidirectional双向循环神经网络
        #双向循环神经网络的特点是,当前时刻的输出不仅和之前的状态有关,还可能和未来的状态有关系,也就是同一层节点之间的信息是双向流动的。
        #与一般的循环神经网络相比,在代码上改动不多,调用的函数仍是nn.LSTM,只是函数参数bidirectional设置为True,而隐层分成forward layer和backward layer,分别进行两个不同时间方向的信息传递,因此,隐层节点数翻倍。

        #参数5:防止鲁棒性overfitting,需要有50%的dropout
        self.rnn = nn.LSTM(embedding_dim, hidden_dim, num_layers=2, 
                           bidirectional=True, dropout=0.5)
        
        # [256*2] => [1]
        #有一个全连接层,是将h层的信息做一个综合,再转为1维的scalar标量
        self.fc = nn.Linear(hidden_dim*2, 1)
        #再加一个dropout层,这样就有两个dropout了。
        self.dropout = nn.Dropout(0.5)
        
        
    def forward(self, x):
        """
        x: [seq_len, b] vs [b, 3, 28, 28]
        """
        # [seq, b, 1] => [seq, b, 100]
        embedding = self.dropout(self.embedding(x)) #经过一个embedding和一个dropout
        
        #这里的hid_dim和num_layers都乘以了2,是因为bidirectional双向循环神经网络。
        #bidirectional会让隐层节点数翻倍,还会让num_layers翻倍,因为是双向的。
        # output: [seq, b, hid_dim*2] 
        # hidden/h: [num_layers*2, b, hid_dim]
        # cell/c: [num_layers*2, b, hid_di]
        output, (hidden, cell) = self.rnn(embedding) #经过LSTM,这里没有输入h0和c0,因为我们默认是0,所以就没有写了。
        
        #将双向输出的最后hidden,取[-2]和[-1],即取出最后的ht,因为是双向所以是ht1和ht2。
        #将双向输出的最后hidden的ht1和ht2进行concat,原本的ht1[b,hid_dim],ht2[b,hid_dim]
        #二者合并后,获得[b,hid_dim*2]
        # [num_layers*2, b, hid_dim] => 2 of [b, hid_dim] => [b, hid_dim*2] 
        # [b,256*2] = [b,512]
        hidden = torch.cat([hidden[-2], hidden[-1]], dim=1) #
        
        # [b, hid_dim*2] => [b, 1]
        hidden = self.dropout(hidden)
        #获得[b],这个tensor的元素值表示positive的概率。
        out = self.fc(hidden) #将hidden送入全连接层。
        
        #输出的是一个positive的概率。
        return out
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
#load word embedding
#用glove的单词矢量覆盖实例化RNN中的self.embedding的权值
rnn = RNN(len(TEXT.vocab), 100, 256)

pretrained_embedding = TEXT.vocab.vectors #这个是glove的单词矢量
print('pretrained_embedding:', pretrained_embedding.shape)

rnn.embedding.weight.data.copy_(pretrained_embedding) #用glove的单词矢量覆盖实例化RNN中的self.embedding的权值。
#这样,每次查询的word feature vector就来自glove的vector了。
print('embedding layer inited.')

#建立优化器
optimizer = optim.Adam(rnn.parameters(), lr=1e-3)
#loss函数
criteon = nn.BCEWithLogitsLoss().to(device)
rnn.to(device)

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

在这里插入图片描述

import numpy as np

#计算预测正确的准确率
def binary_acc(preds, y):
    """
    get accuracy
    """
    preds = torch.round(torch.sigmoid(preds))
    correct = torch.eq(preds, y).float()
    acc = correct.sum() / len(correct)
    return acc

#建立training
def train(rnn, iterator, optimizer, criteon):
    
    avg_acc = []
    rnn.train()
    
    for i, batch in enumerate(iterator):
        
        # [seq, b] => [b, 1] => [b]
        pred = rnn(batch.text).squeeze(1) #squeeze移除数组中维度为1的维度
        
        #预测值与真实值进行loss函数计算:label的维度也是[b]
        loss = criteon(pred, batch.label)
        
        #计算一个train的accuracy
        acc = binary_acc(pred, batch.label).item()
        avg_acc.append(acc)
        
        #清除之前的梯度
        optimizer.zero_grad()
        #在做一个loss的回溯,获得梯度
        loss.backward()
        #优化一下,这样pred的值越来越接近label真实值。
        optimizer.step()
        
        if i%10 == 0:
            print(i, acc)
        
    avg_acc = np.array(avg_acc).mean()
    print('avg acc:', avg_acc)
    

#进行test
def eval(rnn, iterator, criteon):
    
    avg_acc = []
    
    rnn.eval()
    
    #test不需要梯度信息
    with torch.no_grad():
        
        for batch in iterator:
            #每一次batch可以获得predict的值。
            # [b, 1] => [b]
            pred = rnn(batch.text).squeeze(1)

            loss = criteon(pred, batch.label)

            acc = binary_acc(pred, batch.label).item()
            avg_acc.append(acc)
        
    avg_acc = np.array(avg_acc).mean()
    
    print('>>test:', avg_acc)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
for epoch in range(10):
    
    eval(rnn, test_iterator, criteon)
    train(rnn, train_iterator, optimizer, criteon)
  • 1
  • 2
  • 3
  • 4

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

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

闽ICP备14008679号