一、简要介绍
RNN适用了处理序列性的数据,并利用权重共享机制(即一个RNN网络使用多次)。序列性数据用一个例子来理解,比如我们想要预知某一天是否会下雨,我们采集了大量天数的气象信息,气象信息包括温度、气压和降雨,我们分四天为一组,前三天(用x1,x2,x3表示)就是一个序列,第四天作为已知结果,通过第四天的已知结果与模型的估计值进行比较求损失、求梯度来优化模型参数。使用序列模型的意义在于数据前后之间有一定的联系或影响,即x2的数据依赖于x1,x3的数据依赖于x2,x4的数据依赖于x3,即前三天气象信息与是否降雨对第四天有一定的影响(天气的变化是比较缓和的,很少出现断崖式的变化,所以可以根据前些天的天气预知后一天的天气)。注意,这里的x1,x2,x3是一个序列,而x1,x2,x3三个张量又各自包含三个特征值,分别表示温度、气压和天气。
除了天气、股市等信息外,自然语言也是具有序列关系的信息。
二、RNN神经网络
RNN cell本质上是一个也是线性层(Linear Layer,进行w*x+b的线性变换)。区别在于RNN cell的网络是共享的,即使用多次。
上图中的左边部分是RNN模型的一般书写模式,右边部分是其展开模式。,这里的X1,X2,X3,X4就是之前提到过的序列。h0可以是初始化全为0的张量。先将X1和h0(h表示hidden,即隐层)输入RNN cell(RNN 神经网络),输出结果为h1,再将x2和h1输入相同的RNN cell输入结果h2,依次类推,最终输出结果h4。可以看出h2的结果融合了x1与x2的信息,h3的结果融合了x1、x2与x3的信息,h4的结果融合了x1、x2、x3与x4的信息。
RNN网络的每一个隐藏层的具体过程:
input_size表示输入的维度,输入Xt先做一个线性变换,h(t-1)是上一层隐层的输出,也对其做一个线性变换,再将 两个线性变换的结果相加,最后使用激活函数tanh()使结果位于(-1,1)的区间内,ht为该隐层的输出。
实际上,两个线性运算可以写成一个线性运算:
RNN cell的公式:
三、代码实现
自定义一个RNN cell,只需输入参数input_size(即输入值的维度,即Xi有几个分量,是几维张量)和hidden_size(隐藏层的维度,即hi有几个分量)。
# 声明一个RNN cell
cell = torch.nn.RNNCell(input_size = input_size, hidden_size = hidden_size)
# 调用RNN cell,并把下列语句写下一个for循环里(实现序列多此调用RNN cell)
hidden = cell(input, hidden)
注意:input.shape为[batch, input_size],batch为批量的大小,数据集的大小,即input.shape=batch.size*input_size的二维张量,hidden.shape为[batch, hidden_size]。输出的hidden.shape为[batch, hidden_size]。
实例:
(1)使用RNN cell
RNN的输入张量的shape为[batchSize, inputSize],输出张量的维度为[batchSize, hiddenSize]。数据集(dataset)的shape定义为[seqLen,batchSize,inputSize],这里的seqLen即为序列长度。
import torchbatch_size = 1 # 批量大小
seq_len = 3
input_size = 4
hidden_size = 2cell = torch.nn.RNNCell(input_size = input_size, hidden_size = hidden_size)dataset = torch.randn(seq_len,batch_size,input_size)
hidden = torch.zeros(batch_size,hidden_size) # h0# 对dataset进行遍历,每次取出序列中的一个元素,即先取x1,再去x2,最后取x3
for idx,input in enumerate(dataset):print('=' * 20,idx,'='*20)print('Input size:',input.shape)hidden = cell(input, hidden) # cell中的hidden为上一次输出的隐层,等号左边的hidden为这一次的隐层。print('outputs size',hidden.shape)print(hidden)
(2)使用RNN
# num_layers表示隐层的层数(后面图解)
cell = torch.nn.RNN(input_size = input_size, hidden_size = hidden_size, num_layers= num_layers)
# 调用
out, hidden = cell(inputs,hidden) #input为输入的序列[x1,x2,x3,..,xN],input.shape为[seqLen, batch, input_size]
# cell中的hidden表示h0,out表示[h1,h1,...,hN),等号左边的hidden为hN。hidden.shape为[numLayers, batch, hidden_size]
# output.shape为[seqLen, batch, hiddden_size]
上图的numLayers即为3,表示三层隐藏层,同一种颜色的RNN cell为同一层(其实同一种颜色是同一个RNN cell)。
import torchbatch_size = 1 # 批量大小
seq_len = 3
input_size = 4
hidden_size = 2
num_layers = 1cell = torch.nn.RNN(input_size = input_size, hidden_size = hidden_size,num_layers=num_layers)inputs = torch.zeros(seq_len,batch_size,input_size)
hidden = torch.zeros(num_layers,batch_size,hidden_size)out, hidden = cell(inputs,hidden)print('Output size:', output.size)
print('Output:', output)
print('Hidden size:', hidden.size)
print('Hidden:', hidden)
四、实例
训练目标,将输入的“hello"经过RNN网络根据每一个隐藏层的结果输出为“ohlol”。
1.将单词转化为张量
使用独热(one-hot)编码:
对于hello的每一个字符都对应一个索引值,将hello转化为索引值得数组,再根据独热编码得方法转化为等长得向量,可以用一个一维含四个特征值得张量表示。注意每一次RNNcell输出的hi是一个含有四个元素的张量,通过softmax函数决定是输出结果是e,h,l,o中的哪一类:
import torchinput_size = 4
hidden_size = 4
batch_size = 1#准备数据
idx2char = ['e','h','l','o']
x_data = [1,0,2,2,3]
y_data = [3,1,2,3,2]# 便于查询
one_hot_lookup = [[1,0,0,0],[0,1,0,0],[0,0,1,0],[0,0,0,1]]
x_one_hot[one_hot_lookup[x] for x in x_data]inputs = torch.Tensor(x_one_hot).view(-1,batch_size,input_size)
labels = torch.LongTensor(y_data).view(-1,1)# 定义模型
class Model(torch.nn.Module):def __init__(self,input_size,hidden_size,batch_size):super(Model,self).__init__()self.batch_size = batch_sizeself.input_size = input_sizeself.hidden_size = hidden_sizeself.rnncell = torch.nn.RNNCell(input_size = self.input_size,hidden_size=self.hidden_size)def forward(self,input,hidden):hidden = self.rnncell(input,hidden)return hidden# 生成初始的h0def init_hidden(self):return torch.zeros(self.batch_size,self.hidden_size)net = Model(input_size,hidden_size,batch_size)# 损失函数
criterion = torch.nn.CrossEntryLoss()
# 优化器
optimizer = torch.optim.Adam(net.parameters(),lr=0.1)# 训练模型
for epoch in range(15):loss = 0optimizer.zero_grad()hidden = net.init_hidden()print("Predicted stringL:",end='')for input,label in zip(inputs,labels):hidden = net(input,hidden)loss += criterion(hidden,label)_,idx = hiddden.max(dim=1)print(idx2char[idx.item()],end='')loss.backward()optimizer.step()print(',Epoch[%d/15] loss=%.4f' % (epoch+1,loss.item()))
使用RNN的书写形式:
# 定义模型
class Model(torch.nn.Module):def __init__(self,input_size,hidden_size,batch_size,numlayers=1):super(Model,self).__init__()self.numlayers = num_layersself.batch_size = batch_sizeself.input_size = input_sizeself.hidden_size = hidden_sizeself.rnn = torch.nn.RNN(input_size = self.input_size,hidden_size=self.hidden_size,num_layers = num_layers)def forward(self,input):hidden = torch.zeros(self.num_layers,self.batch_size,slef.hidden_size)out,_=self.rnn(input,hidden)return out.view(-1,self.hidden_size) # out.shape为[seqLen*batchSize,hiddenSize]# 生成初始的h0def init_hidden(self):return torch.zeros(self.batch_size,self.hidden_size)net = Model(input_size,hidden_size,batch_size)
# 损失函数
criterion = torch.nn.CrossEntryLoss()
# 优化器
optimizer = torch.optim.Adam(net.parameters(),lr=0.1)# 训练模型
for epoch in range(15):optimizer.zero_grad()outputs = net(inputs)loss = criterion(outputs,labels)loss.backwrd()optimizer.step()_,idx= outputs.max(dim=1)idx = idx.data.numpy()
2.使用嵌入层代替独热编码(embedding)
独热编码有许多缺点,维度大、张量稀疏、是硬编码(不是通过提取特征得到的)。因此想到使用低维的(lower-dimension),稠密的(dense),学习到的张量(learn from data)。
embedding即把一个高位的、稀疏的样本映射到一个低维的、稠密的空间(即数据降维):
加入嵌入层后:
加入嵌入层的网络结构:
# 参数
num_class = 4
input_size = 4
hidden_size = 8
embedding_size = 10
num_layers = 2
batch_size = 1
seq_len = 5#准备数据
idx2char = ['e','h','l','o']
x_data = [1,0,2,2,3]
y_data = [3,1,2,3,2]inputs = torch.LongTensor(x_data)
labels = torch.LongTensor(y_data).view(-1,1)class Model(torch.nn.Module):def __init__(self):super(Model,self).__init__()self.emb = torch.nn.Embedding(input_size,embedding_size) # 加入嵌入层,self.emb.shape为[inputSize,embeddingSize]self.rnn = torch.nn.RNN(input_size =embedding_size,hidden_size=self.hidden_size,num_layers = num_layers,batch_first=True)# batch_first=True时input.shape为[batchSize,seqLen,embeddingSize],# output.shape为[batchSize,seqLen,hiddenSize] self.fc = torch.nn.Linear(hidden_size,num_class) # 完成输出类别的变换def forward(self,x):hidden = torch.zeros(num_layers,x.size(0),slef.hidden_size)x = self.emb(x)x,_=self.rnn(x,hidden)x =self.fc(x)return x.view(-1,num_class) # out.shape为[seqLen*batchSize,hiddenSize]# 生成初始的h0def init_hidden(self):return torch.zeros(self.batch_size,self.hidden_size)