RNN循环神经网络原理介绍详见RNN循环神经网络之原理详解。本篇主要通过Pytorch实现RNN,包括手动编写代码实现及基于Pytorch自带的RNN去实现。
一、手动代码实现RNN
1.1 通过简单代码熟悉RNN前向传播和反向传播
先导入所需的库
import torch
import torch.nn as nn
import torch.optim as optim
device="cuda" if torch.cuda.is_available() else "cpu"
手动实现RNNCELL
(为什么叫RNNCELL呢?因为RNN是按序列进行迭代计算的,RNNCELL只是负责处理某一个时刻数据的神经单元)
class SimpleRNN_1(nn.Module): #手动实现RNNCELL
def __init__(self,input_size,hidden_size,output_size):
super(SimpleRNN_1,self).__init__()
#分别初始化U、W、V及偏置bh和by
#U、W、V的具体含义详见“RNN循环神经网络之原理详解”
#此处采用了nn.Paramter去定义了各个需要优化的参数
self.U=nn.Parameter(torch.randn(input_size,hidden_size).to(device))
self.W=nn.Parameter(torch.randn(hidden_size,hidden_size).to(device))
self.V=nn.Parameter(torch.randn(hidden_size,output_size).to(device))
self.bh=nn.Parameter(torch.zeros(hidden_size).to(device))
self.by=nn.Parameter(torch.zeros(output_size).to(device))
def forward(self,x,h_prev):
#h_prev代表前一时刻的隐藏状态,进行RNN前向传播,并输出新的隐藏状态h和输出值y
h=torch.tanh(torch.mm(x,self.Wxh)+torch.mm(h_prev,self.Whh)+self.bh)
y=torch.mm(h,self.Why)+self.by
return y,h
由于只是通过代码让大家熟悉RNN的前向及反向传播过程,所以所用的训练数据都是随机进行初始化的,无真实含义。
input_size=10 #定义输入层神经元个数
hidden_size=20 #定义隐藏层神经元个数
output_size=5 #定义输出层神经元个数
rnn_1=SimpleRNN_1(input_size,hidden_size,output_size)
#定义3个训练数据,例如可将x1想象为“我”的词向量,“x2”为“爱”的词向量,“x3”为“北京”的词向量
x1=torch.randn(1,input_size).to(device)
x2=torch.randn(1,input_size).to(device)
x3=torch.randn(1,input_size).to(device)
#初始化隐藏状态h_prev的值
h_prev=torch.zeros(1,hidden_size).to(device)
criterion=nn.MSELoss()
#定义目标值target1、target2和targer3
target1=torch.randn(1,output_size).to(device)
target2=torch.randn(1,output_size).to(device)
target3=torch.randn(1,output_size).to(device)
optimizer=optim.SGD(rnn_1.parameters(),lr=0.01)
进行模型训练
for epoch in range(100):
optimizer.zero_grad()
y1,h1=rnn_1(x1,h_prev)
y2,h2=rnn_1(x2,h1)
y3,h3=rnn_1(x3,h2)
loss1=criterion(y1,target1)
loss2=criterion(y2,target2)
loss3=criterion(y3,target3)
total_loss=loss1+loss2+loss3 #损失值为三个时间状态损失值之和
total_loss.backward() #进行反向传播
optimizer.step()
if epoch % 10 ==0:
print(f"Epoch {epoch},loss:{total_loss.item()}")
输出结果为:
Epoch 0,loss:34.923057556152344
Epoch 10,loss:3.1045074462890625
Epoch 20,loss:0.7735685110092163
Epoch 30,loss:0.23181605339050293
Epoch 40,loss:0.07589025050401688
Epoch 50,loss:0.026361465454101562
Epoch 60,loss:0.009438274428248405
Epoch 70,loss:0.003402857342734933
Epoch 80,loss:0.0012187340762466192
Epoch 90,loss:0.00043166097020730376
1.2 将一个数据(词)一个数据(词)依次输入到模型,然后进行反向传播
以“我爱北京天安门”为例,将按照“我”、“爱”、“北京”、“天安门”这4个词按顺序依次输入到模型中进行前向传播,然后进行反向传播
定义RNNCELL,和1.1代码一样,仅仅是为了区分,将模型命名为SimpleRNN_2。
class SimpleRNN_2(nn.Module): #手动实现RNNCELL
def __init__(self,input_size,hidden_size,output_size):
super(SimpleRNN_1,self).__init__()
#分别初始化U、W、V及偏置bh和by
#U、W、V的具体含义详见“RNN循环神经网络之原理详解”
#此处采用了nn.Paramter去定义了各个需要优化的参数
self.U=nn.Parameter(torch.randn(input_size,hidden_size).to(device))
self.W=nn.Parameter(torch.randn(hidden_size,hidden_size).to(device))
self.V=nn.Parameter(torch.randn(hidden_size,output_size).to(device))
self.bh=nn.Parameter(torch.zeros(hidden_size).to(device))
self.by=nn.Parameter(torch.zeros(output_size).to(device))
def forward(self,x,h_prev):
#h_prev代表前一时刻的隐藏状态,进行RNN前向传播,并输出新的隐藏状态h和输出值y
h=torch.tanh(torch.mm(x,self.Wxh)+torch.mm(h_prev,self.Whh)+self.bh)
y=torch.mm(h,self.Why)+self.by
return y,h
定义训练数据
input_size=10
hidden_size=20
output_size=5
rnn_2=SimpleRNN_2(input_size,hidden_size,output_size)
x=torch.randn(100,1,input_size).to(device) #假设有100个词,每个词的向量为input_size维
y=torch.randn(100,1,output_size).to(device)
optimizer2=optim.Adam(rnn_2.parameters(),lr=0.01)
loss=nn.MSELoss() #定义损失函数为MSE
h_prev=torch.zeros(1,hidden_size).to(device) #初始化隐藏状态
进行模型训练
for epoch in range(100):
loss_all=0.0 #初始化损失函数值
for t in range(x.shape[0]): #一个词一个词的输入模型中
if t==0:
y_pre,h=rnn_2(x[t],h_prev) #t=0时刻,采用初始化隐藏状态
else:
y_pre,h=rnn_2(x[t],h)
loss_all=loss_all+loss(y_pre,y[t]) #更新损失函数值,是所有时刻的损失值之和
loss_all=loss_all.to(device)
optimizer2.zero_grad()
loss_all.backward(retain_graph=True) #反向传播
optimizer2.step()
if epoch % 10==0:
print("epoch is:",epoch,"loss is:",loss_all.item())
输出结果为:
epoch is: 0 loss is: 2135.439697265625
epoch is: 10 loss is: 1681.2464599609375
epoch is: 20 loss is: 1343.6063232421875
epoch is: 30 loss is: 1209.5137939453125
epoch is: 40 loss is: 989.8624267578125
epoch is: 50 loss is: 855.2536010742188
epoch is: 60 loss is: 732.1452026367188
epoch is: 70 loss is: 623.844970703125
epoch is: 80 loss is: 632.3255615234375
epoch is: 90 loss is: 533.6336669921875
1.3 将训练数据以序列的形式输入到模型,然后进行反向传播
所谓序列形式,就是区别于1.2中一个词一次词输入的形式:比如,每次输入的数据是5个词组成的词向量,用这前5个词去预测第6个词:1-5个词去预测第6个词,2-6个词去预测第7个词...
class SimpleRNN_3(nn.Module):
def __init__(self,input_size,hidden_size,output_size):
super(SimpleRNN_3,self).__init__()
self.U=nn.Parameter(torch.randn(input_size,hidden_size).to(device))
self.W=nn.Parameter(torch.randn(hidden_size,hidden_size).to(device))
self.V=nn.Parameter(torch.randn(hidden_size,output_size).to(device))
self.bh=nn.Parameter(torch.zeros(hidden_size).to(device))
self.by=nn.Parameter(torch.zeros(output_size).to(device))
def forward(self,x,h_prev):
seq=x.shape[0] #获取输入数据X的第0维:即输入序列包含的数据个数
for i in range(seq):
if i==0:
h=torch.tanh(torch.mm(x[i],self.U)+torch.mm(h_prev,self.W)+self.bh)
else:
h=torch.tanh(torch.mm(x[i],self.U)+torch.mm(h,self.W)+self.bh)
y=torch.mm(h,self.V)+self.by
return y,h
定义训练数据
input_size=10
hidden_size=20
output_size=5
rnn_3=SimpleRNN_3(input_size,hidden_size,output_size)
x=torch.randn(10,1,input_size).to(device) #由于再前向传播时需要进行矩阵相乘(在SimpleRNN_3的forward函数中要遍历x中的每个向量,向量不能相乘,所以需要加个1,变为1xinput_size维的矩阵)
y=torch.randn(10,1,output_size).to(device)
optimizer3=optim.Adam(rnn_3.parameters(),lr=0.01)
loss=nn.MSELoss()
h_prev=torch.zeros(1,hidden_size).to(device)
进行模型训练
for epoch in range(100):
optimizer3.zero_grad()
y_pre,h=rnn_3(x,h_prev)
loss_all=loss(y_pre,y[-1])
loss_all.backward()
optimizer3.step()
if epoch % 10==0:
print("epoch is:",epoch,"loss is:",loss_all.item())
输出结果为:
epoch is: 0 loss is: 4.718745231628418
epoch is: 10 loss is: 0.26759418845176697
epoch is: 20 loss is: 0.2974125146865845
epoch is: 30 loss is: 0.04763997346162796
epoch is: 40 loss is: 0.03225860744714737
epoch is: 50 loss is: 0.005860757082700729
epoch is: 60 loss is: 0.003772950265556574
epoch is: 70 loss is: 0.0016446646768599749
epoch is: 80 loss is: 0.00038170343032106757
epoch is: 90 loss is: 0.00014042628754395992
1.4 按batch输入到模型,batch中的元素为一个个序列,然后进行反向传播
此时,输入数据X的形状为[batch_size,seq,1,input_size],其中seq为序列的个数。
class SimpleRNN_4(nn.Module):
def __init__(self,input_size,hidden_size,output_size):
super(SimpleRNN_5,self).__init__()
self.Wxh=nn.Parameter(torch.randn(input_size,hidden_size).to(device))
self.Whh=nn.Parameter(torch.randn(hidden_size,hidden_size).to(device))
self.Why=nn.Parameter(torch.randn(hidden_size,output_size).to(device))
self.bh=nn.Parameter(torch.zeros(hidden_size).to(device))
self.by=nn.Parameter(torch.zeros(output_size).to(device))
def forward(self,x,h_prev):
seq=x.shape[1]
for i in range(seq):
if i==0:
h=torch.tanh(x[:,i,:,:]@self.Wxh+h_prev@self.Whh+self.bh) #一定要注意,这里不使用torch.mm,而使用@,原因是torch.mm为二维矩阵相乘,这里x[:,i,:,:]的维度早已超出了二维矩阵的维度
else:
h=torch.tanh(x[:,i,:,:]@self.Wxh+h@self.Whh+self.bh)
y=h@self.Why+self.by
return y,h
定义训练数据
input_size=10
hidden_size=20
output_size=5
rnn_4=SimpleRNN_4(input_size,hidden_size,output_size)
x=torch.randn(5,10,1,input_size).to(device)
y=torch.randn(5,10,1,output_size).to(device)
optimizer4=optim.Adam(rnn_4.parameters(),lr=0.01)
loss=nn.MSELoss()
h_prev=torch.zeros(1,hidden_size).to(device)
进行模型训练
for epoch in range(100):
optimizer4.zero_grad()
loss_all=0
for k in range(x.shape[0]):
y_pre,h=rnn_4(x[k],h_prev)
loss_all=loss_all+loss(y_pre,y[k][-1])
loss_all.backward()
optimizer4.step()
if epoch % 10==0:
print("epoch is:",epoch,"loss is:",loss_all.item())
输出结果为:
epoch is: 0 loss is: 13.898061752319336
epoch is: 10 loss is: 3.803760290145874
epoch is: 20 loss is: 2.6271839141845703
epoch is: 30 loss is: 0.9352027177810669
epoch is: 40 loss is: 0.3608148694038391
epoch is: 50 loss is: 0.11155679821968079
epoch is: 60 loss is: 0.03883976861834526
epoch is: 70 loss is: 0.01533182617276907
epoch is: 80 loss is: 0.004870606120675802
epoch is: 90 loss is: 0.0015913982642814517
二、调用pytorch中的RNN包
2.1 nn.RNNCell
主要参数:
- input_size:输入特征的维度。例如,如果某个词的embedding后的维度为100,那么input_size=100;或者一条数据有10个特征,那么input_size=10
- hidden_size:隐藏状态的维度。表示每个时间步隐藏状态的特征数量,也就是一个隐藏层的神经元个数
- bias:是否使用偏置项,默认为 True。
- nolinearity:非线性激活函数,可选 'tanh' 或 'relu',默认为 'tanh'
import torch
import torch.nn as nn
# 定义一个 RNNCell 单元
rnn_cell = nn.RNNCell(input_size=10, hidden_size=20)
# 假设输入数据是一个批次大小为 32,特征维度为 10 的张量
input_tensor = torch.randn(32, 10)
# 初始化隐藏状态,维度为 (批次大小, 隐藏状态维度)
h0 = torch.zeros(32, 20)
# 对单个时间步进行前向传播
h1 = rnn_cell(input_tensor, h0)
2.2 nn.RNN
主要参数:
- Input_size:输入特征的维度。例如,如果某个词的embedding后的维度为100,那么input_size=100;或者一条数据有10个特征,那么input_size=10
- hidden_size:隐藏状态的维度,也就是一个隐藏层的神经元个数,当然,如果是按batch_size输入时,那么hidden_size是一个多维向量
- num_layers:RNN的层数,可以支持多层RNN的实现
- nonlinearity:非线性激活函数,可以选“tanh”、“relu”,默认为“tanh”
- bias:是否使用偏置项,默认为True
- batch_first:如果设置为 True,输入和输出张量的第一个维度是批次大小(batch size),否则,第二个维度是批次大小。默认为False
- dropout:如果不为0,在除最后一层外的每个 RNN 层的输出上添加一个 Dropout 层,以防止过拟合。默认为0.
- bidirectional:如果设置为True,使用双向RNN,即同时处理正向和反向序列。
import torch
import torch.nn as nn
# 定义一个 RNN 层
rnn = nn.RNN(input_size=10, hidden_size=20, num_layers=2, batch_first=True)
# 假设输入数据是一个批次大小为 32,序列长度为 5,特征维度为 10 的张量
input_tensor = torch.randn(32, 5, 10)
# 初始化隐藏状态,维度为 (层数 * 方向数, 批次大小, 隐藏状态维度)
h0 = torch.zeros(2 * 1, 32, 20)
# 前向传播
output, hn = rnn(input_tensor, h0)
nn.RNN和nn.RNNCell区别详见RNN循环神经网络之RNN十问