RNN循环神经网络(Recurrent Neural Network, RNN)是一类以序列(sequence)数据为输入,在序列的演进方向进行递归(recursion)且所有节点(循环单元)按链式连接的递归神经网络。
一、网络架构
首先回顾一下传统的卷积神经网络的架构,如下图所示,是LeNet网络结构,输入一张28x28的图像,经过多层卷积处理,然后由三层的全连接层输出结果,从图像可以看出,传统的卷积神经网络是一个不断“向前”的神经网络,图片数据不断向前传播,然后误差反向优化网络。这是由于,卷积神经网络通常用于处理一些图像数据,通常可以理解为输入一张图片,输出一个结果,图片之间的关联不大。
![](https://i-blog.csdnimg.cn/blog_migrate/b79b567b28121ba525bc2c5831128ecc.png)
考虑一下生活中的其他场景,例如生活中与他人的对话,我们要想理解别人的意思,不仅要考虑他人当前的话语,还要考虑之前的几句话的意思,前后的连贯性较强,这种场景使用卷积神经网络就不太合适了,尤其是在NLP(自然语言处理)的领域,我们同通常使用RNN(循环神经网络)用于处理一些序列模型,即前后的关联性较大的场景。
下面是一个基础的RNN Cell,是一个基础的RNN单元。RNN的输入不仅由当前的输入还有隐藏层输入
,最终输出当前新的输出
,然后循环作用。
下面是RNN Cell内部具体的运算。
将RNN Cell展开的可以看到的如下图所示。
上图中几个RNN的权值是共享的,可以减小运算量,每一个RNN Cell(实际就是同一个RNN Cell的不断循环罢了)的输入不仅有当前时刻的输入还有当前时刻的隐藏层输入
,初始状态是
是自己预先定义的。上面就是简单RNN的基本单元的构造,下面使用PyTorch进行RNN网络的建立。
二、使用PyTorch建立RNN网络
1、使用RNNCell创建
此时解释一下其中几个参数的含义:batch_size是批次大小,seq_len是序列长度,input_size输入size,hidden_size是输出size,input_size和hidden_size好理解就是输入输出向量的长度,输入[1,2],size就是2,我们通常将输入的文字通过one-hot进行编码为一个向量,向量的长度就是输入的size,每一个Cell都会有一个hidden的结果向量,长度就是hidden_size。
下面是对于batch_size和seq_len进行解释,深度学习中采用mini-batch的方法进行迭代优化,在CNN中batch的思想较容易理解,一次输入batch个图片,进行迭代。但是RNN中引入了seq_len(time_step),理解较为困难。下面举一个例子:
sentences = ["i like dog", "i love coffee", "i hate milk", "i like music", "i hate you"]
上面这句话中有5个句子,即batch_size=5,每一个句子都可以看作一个sequence,seq_len可以由embedding,比如one-hot encoding,转换为一个向量。那么在RNN的训练中。
t=0时, 输入第一个batch[i, i, i, i, i]这里用字符表示,其实应该是对应的one-hot编码。
t=1时,输入第二个batch[like, love, hate, like, hate]
t=2时,输入第三个batch[dog, coffee, milk, music, you]
那么对应的时间t来说,RNN需要对先后输入的batch_size个字符进行前向计算迭代,得到输出。
或者另外一个例子,假如你有一个数据集10条句子,每个句子20个词,每个词可以用长度为64的向量表示,此时,seq_len=20,batch_size=10,input_size=64;seq_len的值决定了该RNN的结构,如果seq_len为20,那么这环要循环20次,每一步t都需要一个输入,同时也会有一个输出,共有20次输入和对应的20个输出。
import torch
batch_size=1
seq_len=3
input_size=4
hidden_size=2
cell=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)
for idx,input in enumerate(dataset):
print('='*20,idx,'='*20)
print('Input size:',input.shape)
hidden=cell(input,hidden)
print('ouputs size:',hidden.shape)
print(hidden)
'''
打印结果
==================== 0 ====================
Input size: torch.Size([1, 4])
ouputs size: torch.Size([1, 2])
tensor([[0.5464, 0.7889]], grad_fn=<TanhBackward>)
==================== 1 ====================
Input size: torch.Size([1, 4])
ouputs size: torch.Size([1, 2])
tensor([[ 0.8551, -0.3328]], grad_fn=<TanhBackward>)
==================== 2 ====================
Input size: torch.Size([1, 4])
ouputs size: torch.Size([1, 2])
tensor([[ 0.1827, -0.6966]], grad_fn=<TanhBackward>)
'''
2、使用RNN对象
此时多出一个参数num_layers,解释一下该参数,RNN Cell也可以有多层组成,不加的话,默认层数是一层,下面解释一下输入输出。
此时的输入input_size仍然为seq_len*batch_size*input_size,hidden为num_layers*batch_size*hidden_size,此时输出out的大小为seq_len*batch_size*hidden_size,相当于seq_len个hidden的叠加, 输出的hidden为num_layers*batch_size*hiddne_size。
cell=torch.nn.RNN(input_size=input_size,hidden_size=hidden_size,num_layers=num_layers)
inputs=torch.randn(seq_len,batch_size,input_size)
hidden=torch.zeros(num_layers,batch_size,hidden_size)
out,hidden=cell(inputs,hidden)
inputs=torch.randn(seq_len,batch_size,input_size)
hidden=torch.zeros(num_layers,batch_size,hidden_size)
out,hidden=cell(inputs,hidden)
'''
Output size: torch.Size([3, 1, 2])
Output: tensor([[[-0.8403, -0.4627]],
[[-0.0917, 0.9727]],
[[ 0.3013, 0.6235]]], grad_fn=<StackBackward>)
Hidden size: torch.Size([1, 1, 2])
Hidden: tensor([[[0.3013, 0.6235]]], grad_fn=<StackBackward>)
'''
三、实现完整的基础RNN网络
1、使用RNN Cell
将数据进行预处理
input_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]#此时的维度是seq*inputsize需要 进行转换为seq*batch*inputsize
inputs=torch.Tensor(x_one_hot).view(-1,batch_size,input_size)
labels=torch.LongTensor(y_data).view(-1,1)#标签seq*1
下面是网络架构:
class Model(torch.nn.Module):
def __init__(self,input_size,hidden_size,batch_size):
super(Model,self).__init__()
self.batch_size=batch_size
self.input_size=input_size
self.hidden_size=hidden_size
self.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
def init_hidden(self):
return torch.zeros(self.batch_size,self.hidden_size)
net=Model(input_size,hidden_size,batch_size)
训练过程,该过程中,训练迭代了15次,每一次迭代过程中,又有seq_len次的循环,我们都要手动的将每一个seq_len循环中的损失进行累加,然后误差反向传播,梯度下降。
criterion=torch.nn.CrossEntropyLoss()
optimizer=torch.optim.Adam(net.parameters(),lr=0.1)
for epoch in range(15):
loss=0
optimizer.zero_grad()
hidden = net.init_hidden()
print('Predicted string: ', end='')
# input:batch_size*input_size;inputs:seq*batch_size*input_size
for input,label in zip(inputs,labels):
hidden=net(input,hidden)
loss+=criterion(hidden,label)
_,idx=hidden.max(dim=1)
print(idx2char[idx.item()],end='')
loss.backward()
optimizer.step()
print(', Epoch [%d/15] loss=%.4f' % (epoch+1, loss.item()))
2、使用RNN对象
此时的网络模型:
input_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]#此时的维度是seq*inputsize需要 进行转换为seq*batch*inputsize
inputs=torch.Tensor(x_one_hot).view(-1,batch_size,input_size)
labels=torch.LongTensor(y_data)#标签seq
class Net(torch.nn.Module):
def __init__(self,input_size,hidden_size,batch_size,num_layers=1):
super(Net,self).__init__()
self.num_layers=num_layers
self.batch_size=batch_size
self.input_size=input_size
self.hidden_size=hidden_size
self.rnn=torch.nn.RNN(input_size=self.input_size,hidden_size=self.hidden_size,num_layers=self.num_layers)
def forward(self,input):
hidden=torch.zeros(self.num_layers,self.batch_size,self.hidden_size)
out,_=self.rnn(input,hidden)
return out.view(-1,self.hidden_size)
net=Net(input_size,hidden_size,batch_size,num_layers)
训练过程和CNN等传统神经网络的训练过程一致,将inputs和labels输入之后,PyTorch会自动帮我们完成seq_len的循环和误差累加,此时的hidden我们在forward函数中进行初始化为全0的tensor,没有手动传入。
criterion=torch.nn.CrossEntropyLoss()
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.backward()
optimizer.step()
_,idx = outputs.max(dim=1)
idx = idx.data.numpy()
print('Predicted: ', ''.join([idx2char[x] for x in idx]), end='')
print(', Epoch [%d/15] loss = %.3f' % (epoch + 1, loss.item()))