赞
踩
图片类型数据,视频类似数据这种2D的数据,可以使用卷积神经网络进行处理。
语音、文字这些数据都有时间先后顺序
对于图片数据,我们使用像素的RGB值表示图片的色彩度。
那么对于语音、文字这种类型的信号如何表示呢?
对于语音的话,每个时间段会有一个波形,这个波形的波峰值,就可以表示此刻声音的强度。
对于文字,文字的字符,也可以表示;但是pytorch没有自带支持string字符的,pytorch处理的都是数值类型。因此,需要想一种办法,将string类型表示为另一种数值类型。这种表示方法叫做representation或者词向量word embedding。
对于word embedding,我们说一句话,这句话有五个单词,每个单词用一个向量来表示。这样第一个维度seq_len,可能表示单词的数量5;第二个维度feature_len,可能表示每个单词的表示方法,取决于应用场景。
例子:
1、房价的变化趋势
[month/days, 当前节点的价值(用一个数值类型来表示即可)]
2、图片数据
图片数据也可用词向量word embedding来进行理解,每行可以视为一个embedding,一个需要扫描28行,通过这样理解也可以获得一张完整图片。
但是利用后面要学的RNN来处理这种图片数据,没有CNN的方式来的优秀。
假设单词有3500个,这里使用了5个单词。
那么文本信息的表达为[5, 3500]。其中3500是独热编码的形式。
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。
在CNN中为了提高train效率,增加了batch。
这里也是为了提供效率,增加了batch维度。
这里对于文本的内容的表达有两种形式:
▪ [word num, b, word vec] 这种是b方中间。
▪ [b, word num, word vec] 这种是b方开头。
在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方式,实现一个查表操作。这里我们使用一个pytorch面向NLP处理的一个包。得到这个包后,直接使用GloVe()方式,这个是一种编码方式,会下载一个2.2GB的文件,下载好后,直接输入单词,就可以获得这个单词所对应的word vector feature。
比如这句话:
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],代表着表示积极的概率,这样就是一个二分类的问题。
▪ Long sentence对于长句子
▪ 100+ words
▪ too much parametes [w, b]
▪ no context information没有上下语境的信息(即缺少上一句和下一句语义关系信息)
▪ consistent tensor 需要协调一致性的单元,希望能存储一个语境信息,从而将这个语境信息利用起来,从而处理时间信号序列才能得心应手。
最初始的版本:
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综合起来再做一个聚合,也可以取中间某个时间的语境信息。这个是非常自由的,完全取决于自己。
这里的激活函数不在是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ₕₕ⁽ᵏ⁾。
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)
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。
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]。
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]
这里是两层的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])
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]
之前所涉及的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.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状态的一个集合
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
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)
这里我们用RNN来预测正弦曲线的下一时期的分布情况。
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])
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()
这样就完成了一个非常简单的RNN案例
RNN网络的梯度推导公式:
∂Eₜ / ∂Wᵣ 梯度信息 = Wₕₕᵏ 再去乘以其他东西所得到的
Wₕₕᵏ => grad梯度信息的一部分,因此会产生两个情况:
1、梯度爆炸:Wₕₕ > 1 大于1很多的话,Wₕₕ的k次方很大,Wₕₕᵏ 再去乘以其他东西,会造成梯度无穷大。
2、梯度离散:Wₕₕ < 1 小于1很多的话,Wₕₕ的k次方很小,Wₕₕᵏ 再去乘以其他东西,会造成梯度接近0。
梯度爆炸的情况是:当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'
∂Eₜ / ∂Wᵣ 梯度趋近于0,这样w会存在长时间没有更新的情况。
RNN VS LSTM(解决梯度离散出现的算法) 梯度可视化(RNN V.S. LSTM Gradient Visualization)
LSTM是解决梯度离散而出现的算法。
从最后时刻到最开始的梯度传播可视化情况。
LSTM(long short-term memory)网络在一定程度上减少了梯度离散情况的出现。
长短期记忆(Long short-term memory, LSTM)是一种特殊的RNN,主要是为了解决长序列训练过程中的梯度消失和梯度爆炸问题。简单来说,就是相比普通的RNN,LSTM能够在更长的序列中有更好的表现。
这里的h就相当于memory,用于存储语境信息。
这里的激活函数不在是sigmoid或则relu了,是tanh双曲正切函数,其范围是(1,-1)
hₜ = tanh ( xₜ @ wₓₕ + hₜ ₋ ₁ @ wₕₕ )
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ₜ ₋ ₁,过去的信息就全部获得了。)
我们需要了解一下,下图中的两个符号:Ⓧ 和 ㊉ ,这两个符号都是闸门用于控制信息的状态。
Ⓧ 相乘,相乘是信息过滤的过程,是对应element-wise相乘,一个σ激活函数后的输出是0~1,Cₜ ₋ ₁ element-wise相乘后的输出为0到Cₜ ₋ ₁ 。
㊉相加,相加就是简单的信息融合,Cₜ ₋ ₁ ㊉ C~ => Cₜ 两个经过门处理后相融合,这里融合输出Cₜ 也是下一个时间戳的Cₜ ₋ ₁。
[ 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ₜ ₋ ₁ 。
这个门的开度是多少,完全由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(这里符号变了)。这样第二道门处理完成了。
这里的 Ot = σ( W [ hₜ ₋ ₁, xₜ ] + b ) = 上面经过第一道门的 ft,以及第二道门的it,是一样的东西,只是W和b不同。
这里将Ot Ⓧ tanh(Ct) = ht,ht是最终的结果,是下一层的输入hₜ ₋ ₁ 。
新的输出是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~)。
σ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,就不会出现梯度离散和梯度爆炸的情况。
在现实中,原始版本的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是一样的。
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。
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)
nn.LSTMCell(input_size=100, hidden_size=20)
参数1: word vector(word dim),100维的向量来表示一个单词。
参数2:表示hidden的memory size(即:h这个shape),20维表示memory size。
这里是有区别的,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。
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)
# ====================== 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)
这里我们使用的编译环境是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、首先加载数据集、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)
print('len of train data:', len(train_data))
print('len of test data:', len(test_data))
print(train_data.examples[15].text)
print(train_data.examples[15].label)
# 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 )
#这里名字命名为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
#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)
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)
for epoch in range(10):
eval(rnn, test_iterator, criteon)
train(rnn, train_iterator, optimizer, criteon)
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。