上一章《深度学习之pytorch实战计算机视觉》第9章 多模型融合(代码可跑通)介绍了多模型融合。循环神经网络(Recurrent Neural Network,简称RNN)是深度学习中重要的内容和我们之前使用的卷积神经网络有着同等重要的地位。循环神经网络主要被用于处理序列(Sequences)相关的问题,比如在自然语言领域(NLP)应用循环神经网络的情况就较多;当然,也可以用于解决分类问题,虽然在图片特征的提取上没有卷积神经网络那样强大,但是本章仍然会使用循环神经网络来解决图片分类的问题,并主要讲解循环神经网络的工作机制和原理。
【说明:本章使用的torch版本>=1.6.0】
卷积神经网络有几个特点:
循环神经网络与之不同,因为在循环神经网络中循环单元可以随意控制输入数据及输出数据的数量,具有非常大灵活性。如图10-1就是这两种模式之间的简单对比:
在图10-1共绘制了4种类型的网络结构,分别是一对一、一对多和两种多对一。可以将一对一网络结构看作一个简单的卷积神经网络模型,输入和输出都是固定维度。在一对多的网络结构中引入了循环单元,通过一个输入得到数量不等的输出。多对多的网络结构同样是循环模式,通过数量不等的输入得到数量不等的输出。
下面我们进一步对循环神经网络进行了解,如下图所示是循环神经网络的网络简化模型:
下图是展开形式:
H0是最初输入的隐藏层,一般情况下使用零初始化。 下图展示了RNN所代表的循环层内部的运算细节:
W表示权重参数,计算公式如下:
使用了偏置b的计算公式如下:
得到隐藏层H之后,进一步计算输出结果,公式如下:
虽然循环神经网络已经能够很好地对输入的序列数据进行处理,但它有一个弊端:不能进行长期记忆,影响就是如果近期输入的数据发生了变化,则会对当前输出结果产生重大影响。为了避免这种情况的出现,研究者开发了 LSTM ( Long Short Term Memory)类型的循环神网络模型。下面使用循环神经网络解决一个计算机视觉问题,这就是之前的手写数字识别问题。
先导包和载入数据,和第6章(《深度学习之pytorch实战计算机视觉》第6章 PyTorch基础)很多代码相似,代码如下:
import torch
import torchvision
from torchvision import datasets, transforms
from torch.autograd import Variable
import matplotlib.pyplot as plt
#和第6章很多代码相似
# 数据预处理
# transform = transforms.Compose([transforms.ToTensor(),
# transforms.Normalize(mean = [0.5,0.5,0.5],std =[0.5,0.5,0.5])])
#一定要做下面的修改,不然输入维度不对!
transform = transforms.Compose([transforms.ToTensor(),
transforms.Normalize(mean = [0.5],std = [0.5])])
# 读取数据,之前下载过,现在直接读取
dataset_train = datasets.MNIST(root = './data/',
transform = transform,
train = True,
download = False)
dataset_test = datasets.MNIST(root = './data/',
transform = transform,
train = False)
# 加载数据
train_loader = torch.utils.data.DataLoader(dataset = dataset_train,
batch_size = 64,
shuffle = True)
test_loader = torch.utils.data.DataLoader(dataset = dataset_test,
batch_size = 64,
shuffle = True)
images,labels = next(iter(train_loader)) #获取一个批次的图片和标签
img = torchvision.utils.make_grid(images) #将一个批次的图片构造成网格模式
img = img.numpy().transpose(1,2,0)
std = [0.5,0.5,0.5]
mean = [0.5,0.5,0.5]
img = img*std + mean
print([labels[i] for i in range(64)]) #打印这个批次数据的全部标签
plt.imshow(img) #显示图片
plt.show()
输出如下:
[tensor(1), tensor(9), tensor(1), tensor(5), tensor(8), tensor(6), tensor(8), tensor(3), tensor(3), tensor(9), tensor(5), tensor(7), tensor(7), tensor(8), tensor(6), tensor(4), tensor(2), tensor(5), tensor(9), tensor(3), tensor(8), tensor(2), tensor(0), tensor(1), tensor(5), tensor(8), tensor(4), tensor(9), tensor(7), tensor(8), tensor(0), tensor(0), tensor(9), tensor(1), tensor(5), tensor(5), tensor(4), tensor(1), tensor(7), tensor(9), tensor(8), tensor(3), tensor(6), tensor(2), tensor(7), tensor(9), tensor(7), tensor(4), tensor(7), tensor(0), tensor(4), tensor(2), tensor(0), tensor(8), tensor(1), tensor(6), tensor(1), tensor(5), tensor(6), tensor(1), tensor(6), tensor(7), tensor(1), tensor(8)]
下面看更重要的代码,首先是循环神经网络模型的搭建,代码如下:
#搭建RNN网络
class RNN(torch.nn.Module):
def __init__(self):
super(RNN,self).__init__()
self.rnn = torch.nn.RNN(
input_size = 28,
hidden_size = 128,
num_layers = 1, #指定循环层堆叠的数量,默认为1
batch_first = True)
self.output = torch.nn.Linear(128,10)
def forward(self,input):
output, _ = self.rnn(input, None)
output = self.output(output[:,-1,:])
return output
在代码中构建循环层使用的是 torch.nn.RNN 类,在这个类中使用的几个比较重要的参数如下
在上面代码里,我们定义的input_size=28,因为输入的手写数据的宽高为28×28,所以可以将每一张图片看作序列长度为28且每个序列中包含28个数据的组合。模型最后输出的结果是分类,所以仍然需要输出10个数据,在代码中的体现就是self.output=torch.nn.Linear(128,10)。
再来看前向传播函数forward中的两行代码,首先是output,_=self.rnn(input, None),其中包含两个输入参数,分别是input输出数据和H0的参数。在循环神经网络模型中,对隐层的初始化我们一般采用0初始化,所以这里传入的参数就是None。再看代码output=self.output(output[:,-1,:]),因为我们的模型需要处理的是分类问题,所以需要提取最后一个序列的输出结果作为当前循环神经网络模型的输出。
搭建好模型后,可以对打印模型:
model = RNN()
model = model.cuda()#使用GPU就加上这行
print(model)
打印结果如下:
RNN(
(rnn): RNN(28, 128, batch_first=True)
(output): Linear(in_features=128, out_features=10, bias=True)
)
然后我们对模型进行训练,训练10次,训练代码如下:
# 设置损失函数和优化器
optimizer = torch.optim.Adam(model.parameters())
loss_f = torch.nn.CrossEntropyLoss()
# 训练模型
epoch_n = 10
for epoch in range(epoch_n):
running_loss = 0.0
running_correct = 0.0
testing_correct = 0.0
print('Epoch{}/{}'.format(epoch,epoch_n))
print('-'*10)
for data in train_loader:
X_train,y_train = data
#注意对输入特征进行维度变换,对应维度(batch,seq,feature)
X_train = X_train.view(-1,28,28)
#这里-1表示一个不确定的数
X_train,y_train = Variable(X_train.cuda()),Variable(y_train.cuda())
y_pred = model(X_train)
loss = loss_f(y_pred,y_train)
_,pred = torch.max(y_pred.data,1)
optimizer.zero_grad()
loss.backward()
optimizer.step()
running_loss += loss.data.item()
running_correct += torch.sum(pred == y_train.data)
for data in test_loader:
X_test,y_test = data
X_test = X_test.view(-1,28,28)
X_test,y_test = Variable(X_test.cuda()),Variable(y_test.cuda())
output = model(X_test)
_,pred = torch.max(output.data,1)
testing_correct += torch.sum(pred == y_test.data)
print('Loss is:{:.4f},Train ACC is:{:.4f}%,Test ACC is:{:.4f}'.format(running_loss/len(dataset_train),
100*running_correct.cpu().numpy()/len(dataset_train),
100*testing_correct.cpu().numpy()/len(dataset_test)))
【注意】在这里的时候报错了(ValueError: Expected input batch_size (192) to match target batch_size (64).),我找了半天的bug,一定要注意一个细节(手写数据集的图像是单通道的,不是常见的RGB三通道的,所以在数据预处理部分需要将源码进行如下修改):
修改之后正常运行结果如下:
Epoch0/10
----------
Loss is:0.0019,Train ACC is:96.5850%,Test ACC is:95.9000
Epoch1/10
----------
Loss is:0.0018,Train ACC is:96.7400%,Test ACC is:95.9800
Epoch2/10
----------
Loss is:0.0017,Train ACC is:96.9800%,Test ACC is:96.4400
Epoch3/10
----------
Loss is:0.0018,Train ACC is:96.8067%,Test ACC is:95.9600
Epoch4/10
----------
Loss is:0.0016,Train ACC is:97.0950%,Test ACC is:96.7800
Epoch5/10
----------
Loss is:0.0017,Train ACC is:96.9133%,Test ACC is:96.0500
Epoch6/10
----------
Loss is:0.0016,Train ACC is:97.1833%,Test ACC is:97.0800
Epoch7/10
----------
Loss is:0.0016,Train ACC is:97.1483%,Test ACC is:97.2300
Epoch8/10
----------
Loss is:0.0016,Train ACC is:97.0400%,Test ACC is:94.5600
Epoch9/10
----------
Loss is:0.0016,Train ACC is:97.2250%,Test ACC is:97.4000
上述结果看出:输出的准确率较高而且有较低的损失值,这说明模型已经非常不错了。下面对结果进行测试,代码如下:
#对结果进行测试
data_loader_test = torch.utils.data.DataLoader(dataset = dataset_test,
batch_size = 64,
shuffle = True)
X_test,y_test= next(iter(data_loader_test))
X_pred = X_test.view(-1,28,28)
inputs = Variable(X_pred.cuda())
pred = model(inputs)
_,pred = torch.max(pred,1)
print("Predict Label is :",[i.cpu().numpy() for i in pred.data])
print ("Real Label is :", [i.cpu().numpy() for i in y_test])
img = torchvision.utils.make_grid(X_test)
img = img.numpy().transpose(1,2,0)
std = [0.5,0.5,0.5]
mean= [0.5,0.5,0.5]
img = img * std + mean
plt.imshow(img)
打印输出测试图片对应的标签,结果如下:
Predict Label is : [array(7), array(6), array(8), array(2), array(7), array(6), array(7), array(3), array(5), array(8), array(0), array(9), array(4), array(7), array(7), array(6), array(5), array(4), array(9), array(4), array(6), array(2), array(0), array(9), array(2), array(2), array(8), array(7), array(7), array(1), array(3), array(7), array(3), array(8), array(0), array(6), array(7), array(8), array(8), array(3), array(0), array(5), array(2), array(4), array(2), array(9), array(5), array(0), array(6), array(7), array(5), array(8), array(0), array(6), array(9), array(2), array(8), array(0), array(7), array(1), array(0), array(8), array(5), array(4)]
Real Label is : [array(7), array(6), array(5), array(2), array(7), array(6), array(7), array(3), array(5), array(8), array(0), array(9), array(4), array(7), array(7), array(6), array(5), array(4), array(9), array(4), array(6), array(2), array(0), array(9), array(2), array(2), array(8), array(7), array(7), array(1), array(3), array(7), array(3), array(8), array(0), array(9), array(7), array(8), array(8), array(3), array(0), array(5), array(2), array(4), array(2), array(9), array(5), array(0), array(6), array(7), array(5), array(8), array(0), array(6), array(9), array(2), array(8), array(0), array(7), array(1), array(0), array(8), array(5), array(4)]
通过Matplotlib对测试用到的图片进行绘制,效果如图:
从最后的输出结果和图片可以看出,错误率己经非常低了,这说明我们搭建的循环神经网络模型已经能够很好地解决图片分类的问题了。
根据自己的需要,可以选择是否保存训练好的模型,下次直接载入即可,保存代码如下:
torch.save(model.state_dict(),"MNIST_RNN.pth")
使用循环神经网络解决手写数字识别问题的完整代码如下(可跑通):
import torch
import torchvision
from torchvision import datasets, transforms
from torch.autograd import Variable
import matplotlib.pyplot as plt
#和第6章很多代码相似
# 数据预处理(注意这面这行代码要修改)
# transform = transforms.Compose([transforms.ToTensor(),
# transforms.Normalize(mean = [0.5,0.5,0.5],std = [0.5,0.5,0.5])])
transform = transforms.Compose([transforms.ToTensor(),
transforms.Normalize(mean = [0.5],std = [0.5])])
# 读取数据,之前下载过,现在直接读取
dataset_train = datasets.MNIST(root = './data/',
transform = transform,
train = True,
download = False)
dataset_test = datasets.MNIST(root = './data/',
transform = transform,
train = False)
# 加载数据
train_loader = torch.utils.data.DataLoader(dataset = dataset_train,
batch_size = 64,
shuffle = True)
test_loader = torch.utils.data.DataLoader(dataset = dataset_test,
batch_size = 64,
shuffle = True)
images,labels = next(iter(train_loader)) #获取一个批次的图片和标签
img = torchvision.utils.make_grid(images) #将一个批次的图片构造成网格模式
img = img.numpy().transpose(1,2,0)
std = [0.5,0.5,0.5]
mean = [0.5,0.5,0.5]
img = img*std + mean
print([labels[i] for i in range(64)]) #打印这个批次数据的全部标签
plt.imshow(img) #显示图片
plt.show()
#搭建RNN网络
class RNN(torch.nn.Module):
def __init__(self):
super(RNN,self).__init__()
self.rnn = torch.nn.RNN(
input_size = 28,
hidden_size = 128,
num_layers = 1, #指定循环层堆叠的数量,默认为1
batch_first = True)
self.output = torch.nn.Linear(128,10)
def forward(self,input):
# print("input.shape",input.shape)
output, _ = self.rnn(input, None)
output = self.output(output[:,-1,:])
return output
model = RNN()
model = model.cuda() #如果用gpu训练,则加上这行
# print(model)
# 设置损失函数和优化器
optimizer = torch.optim.Adam(model.parameters())
loss_f = torch.nn.CrossEntropyLoss()
# 训练模型
epoch_n = 10
for epoch in range(epoch_n):
running_loss = 0.0
running_correct = 0.0
testing_correct = 0.0
print('Epoch{}/{}'.format(epoch,epoch_n))
print('-'*10)
for data in train_loader:
X_train,y_train = data
#注意对输入特征进行维度变换,对应维度(batch,seq,feature)
X_train = X_train.view(-1,28,28)
#这里-1表示一个不确定的数
X_train,y_train = Variable(X_train.cuda()),Variable(y_train.cuda())
y_pred = model(X_train)
loss = loss_f(y_pred,y_train)
_,pred = torch.max(y_pred.data,1)
optimizer.zero_grad()
loss.backward()
optimizer.step()
running_loss += loss.data.item()
running_correct += torch.sum(pred == y_train.data)
for data in test_loader:
X_test,y_test = data
X_test = X_test.view(-1,28,28)
X_test,y_test = Variable(X_test.cuda()),Variable(y_test.cuda())
output = model(X_test)
_,pred = torch.max(output.data,1)
testing_correct += torch.sum(pred == y_test.data)
print('Loss is:{:.4f},Train ACC is:{:.4f}%,Test ACC is:{:.4f}'.format(running_loss/len(dataset_train),
100*running_correct.cpu().numpy()/len(dataset_train),
100*testing_correct.cpu().numpy()/len(dataset_test)))
#对结果进行测试
data_loader_test = torch.utils.data.DataLoader(dataset = dataset_test,
batch_size = 64,
shuffle = True)
X_test,y_test= next(iter(data_loader_test))
X_pred = X_test.view(-1,28,28)
inputs = Variable(X_pred.cuda())
pred = model(inputs)
_,pred = torch.max(pred,1)
print("Predict Label is :",[i.cpu().numpy() for i in pred.data])
print ("Real Label is :", [i.cpu().numpy() for i in y_test])
img = torchvision.utils.make_grid(X_test)
img = img.numpy().transpose(1,2,0)
std = [0.5,0.5,0.5]
mean= [0.5,0.5,0.5]
img = img * std + mean
plt.imshow(img)
#保存模型
torch.save(model.state_dict(),"MNIST_RNN.pth")
循环神经网络模型目前主要应用于自然语言处理领域,不过在计算机视觉的相关问题上也能够看到循环神经网络的身影。比如,我们在使用卷积神经网络识别出一张图片中的多个对象后,就可以通过循经网络依据识别的目标对象生成一个图片摘要。又比如,我们可以用循环神经网络处理连续 的视频数据,因为完整的视频画面是由它的最小单位帧构成的,每一帧画面都可以作为一个输入数据进行处理,这就变成了一个序列问题。这样的例子还有很多,我们可以不断地发现和发掘,让循环神经网络和卷积神经网络有效结合起来 ,这必然能够开拓计算机视觉领域的新思路。
说明:记录学习笔记,如果错误欢迎指正!写文章不易,转载请联系我。