最近看了Bilinear CNN Models for Fine-grained Visual Recognition文章,想要复现论文中提到的双线性模型,由于刚接触pyTorch,所以在github上找了一份代码,代码来自于Hao Zhang,骨干网选择的是vgg16-pool5,应用于CUB200-2011数据集,在此基础上进行调试。以下记录我的复现和学习过程。下面的内容将从pytorch基础知识开始记录,不喜勿喷!
1.环境准备
2.pytorch基础知识
参考资料:PyTorch 中文教程_w3cschool,(将我学习时的笔记进行粗略整理,下面写到的知识是一会儿双线性模型里面都会用到的东西)
PyTorch
提供两个主要特性:
- 一个类似于
numpy
的n
维张量,但可以在 gpu
上运行;
- 自动区分建立和训练神经网络
2.1autograd包
其意义:PyTorch中,所有神经网络的核心是autograd包,提供了自动求导机制,在反向传播中很有用。
torch.Tensor
是这个包的核心类。如果设置它的属性 .requires_grad
为True
,那么它将会追踪对于该张量的所有操作。当完成计算后可以通过调用.backward()
,来自动计算所有的梯度。
为了防止跟踪历史记录(和使用内存),可以将代码块包装在with torch.no_grad():
中。在评估模型时特别有用,因为模型可能具有requires_grad = True
的可训练的参数,但是我们不需要在此过程中对他们进行梯度计算
2.2nn
包
意义:使用torch.nn
包来构建神经网络.
一个nn.Module
包含各个层和一个forward(input)
方法,该方法返回output
。
以下总结一下用pytorch搭建一个神经网络的流程,为复现双线性网络打下基础:
第一步:
首先定义一个Class,继承nn.Module。(先import torch.nn as nn)
Class里面有两个函数,分别为_init_函数和forward函数(先import torch.nn.functional as F)
def __init__(self): #定义卷积层
super().__init__() #父类nn.Module初始化
self.conv1=nn.Conv2d(1,6,5) #卷积层第一层conv1,输入为1通道,输出为6通道,卷积核为5*5
self.conv2=nn.Conv2d(6,16,5) #卷积层第二层conv2,输入为6通道,输出为16通道,卷积核为5*5
def forward(self, x):
x=F.max_pool2d(F.relu(self.conv1(x)),2)
x=F.max_pool2d(F.relu(self.conv2(x)),2)
return x
F.relu()是官方提供的函数。深度学习主要是学习卷积核里的参数。其他可以不用放进去学习,当然如果激活函数relu放进去学习了,那在forward里的F.relu()就修改为自己的my.relu()。
神经网络中的节点类型:
- 输入单元:把外界的资讯输入网络,统称为「输入层」。这些节点不执行任何计算,它们只是将信息传递给隐藏节点;
- 隐藏单元:这些节点与外界没有任何直接联系。它们执行计算并将信息从 Input 节点传输到
Output
节点。一个隐藏节点的集合形成了一个“隐藏层”。虽然前馈网络只有一个输入层和一个输出层,但是它可以有零个或多个隐藏层;
- 输出单元:输出节点统称为「输出层」 ,负责计算及将网络上的资讯传送到外界。
每一层包含一个或多个节点。
第二步:
Net是一个类,不能直接传入参数,需实例化:
传入参数:
调用output=net(input), 可以理解为等同于调用output=net.forward(input),这个过程是将各层的输入x都计算出来了。相当于一次正向传播。
第三步:计算误差
损失函数loss达到最小值,神经网络输出和期望越接近。希望损失函数loss能够按照梯度进行下降。这个过程能帮助我们理解神经网络学习和决定的是每一层卷积层的权重,输入是我们决定的。
要让loss达到最小,可以取极值。求极值涉及到求偏导数,让损失函数loss对于x的偏导数接近于0。这个过程通过更新卷积层的参数w实现。但是手动实现太费劲了,pytorch框架和torch.nn给我们提供了自动求导。
定义损失函数并实例化:(nn.MSELoss()是一个类,不能直接传入输入数据)
compute_loss=nn.MSELoss()
loss=compute_loss(target,output)
第四步:反向传播(得到对参数W一步的更新量,算是一次反向传播)
用nn.Module
创建的PyTorch
网络必须定义了一个forward
方法,它接受一个张量x
,并将其传递给在__init__
方法中定义的操作
第五步:设置优化器(使用优化器来自动实现对网络权重W的更新)
在反向传播的时候,我们得到了一次loss对输入x的偏导,但要完成对W的更新,需要对参数W进行修改,得到一次loss对于输入x的偏导值后,应该乘以一个步长,得到一个对W的修改量。然后用之前的W减掉这个修改量,从而完成一次对参数W的修改过程。这两个步骤就是通过优化器来实现的。
定义一个优化器并实例化(优化器也是一个类):
from torch import optim
optimizer=optim.SGD(net.parameters(),lr=0.001,momentum=0.9)
注意在optimizer定义的时候,需要给SGD传入了net的参数parameters,这样之后优化器就掌握了对网络参数的控制权,就能够对它进行修改了。传入的时候把学习率lr也传入了。
在每次迭代之前,先把optimizer里存的梯度清零一下(因为W已经更新过的“更新量”下一次就不需要用了)
在loss.backward()反向传播以后,更新参数:
其完整过程:
import torch.nn as nn
import torch.nn.functional as F
Class Net (nn.Module):
def __init__(self): #定义卷积层
super().__init__() #父类nn.Module初始化
self.conv1=nn.Conv2d(1,6,5) #卷积层第一层conv1,输入为1通道,输出为6通道,卷积核为5*5
self.conv2=nn.Conv2d(6,16,5) #卷积层第二层conv2,输入为6通道,输出为16通道,卷积核为5*5
def forward(self, x):
x=F.max_pool2d(F.relu(self.conv1(x)),2)
x=F.max_pool2d(F.relu(self.conv2(x)),2)
return x
net=Net() #先定义一个Net的实例
output=net(input)
compute_loss=nn.MSELoss()
loss=compute_loss(target,output)
loss.backward()
from torch import optim
optimizer=optim.SGD(net.parameters(),lr=0.001,momentum=0.9)
optimizer.zero_grad()
optimizer.step()
python日记:用pytorch搭建一个简单的神经网络 - 几维wk - 博客园 (cnblogs.com)这篇文章还写了创建数据集的过程,挺详细的。
2.3pytorch训练分类器
意义:我们已经看到了如何定义网络,计算损失,并更新网络的权重。当必须处理图像、文本、音频或视频数据时,数据怎么办呢?
PyTorch 训练分类器这个小节写的很清楚,可以试着跑一下。训练一个图片分类器进行分类。将使用CIFAR10数据集,它有如下的分类:“飞机”,“汽车”,“鸟”,“猫”,“鹿”,“狗”,“青蛙”,“马”,“船”,“卡车”等。在CIFAR-10里面的图片数据大小是3x32x32,即:三通道彩色图像,图像大小是32x32像素。
训练一个图片分类器
我们将按顺序做以下步骤:
- 通过torchvision加载CIFAR10里面的训练和测试数据集,并对数据进行标准化
- 定义卷积神经网络
- 定义损失函数
- 利用训练数据训练网络
- 利用测试数据测试网络
上面的神经网络没有提到利用测试数据测试网络的过程,这里将这个过程具体说一下。
当一个网络训练好了,需要检查网络是否学到了一些东西。可以通过预测神经网络输出的标签来检查这个问题,并和正确样本进行 ( ground-truth)对比。如果预测是正确的,我们将样本添加到正确预测的列表中。
dataiter = iter(testloader)
images, labels = dataiter.next()
# 输出图片
imshow(torchvision.utils.make_grid(images))
print('GroundTruth: ', ' '.join('%5s' % classes[labels[j]] for j in range(4)))
net = Net()#加载保存的模型
net.load_state_dict(torch.load(PATH))
outputs = net(images)#用训练好的网络去预测
_, predicted = torch.max(outputs, 1)
print('Predicted: ', ' '.join('%5s' % classes[predicted[j]] for j in range(4)))
输出:
GroundTruth: cat ship ship plane #这个是测试集图像
Predicted: dog ship ship plane#这个是预测出的图像
整个数据集上表现的怎么样可以计算预测的正确性来表示
correct = 0
total = 0
with torch.no_grad():
for data in testloader:
images, labels = data
outputs = net(images)
_, predicted = torch.max(outputs.data, 1)
total += labels.size(0)
correct += (predicted == labels).sum().item()
print('Accuracy of the network on the 10000 test images: %d %%' % (
100 * correct / total))
2.4pytorch数据并行处理
如果有GPU可用的话,可以将神经网络转移到GPU上。
定义第一个设备为可见cuda设备:
device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
# Assuming that we are on a CUDA machine, this should print a CUDA device:
print(device)
输出:
然后这些方法将递归遍历所有模块,并将它们的参数和缓冲区转换为CUDA张量:
net.to(device)
inputs, labels = inputs.to(device), labels.to(device) #将输入和目标在每一步都送入GPU
数据并行DataParallel
device = torch.device("cuda: 0") #把一个模型放到GPU上
model.to(device)
mytensor = my_tensor.to(device) #复制所有的张量到GPU上
PyTorch 默认将只是用一个GPU。可以使用数据并行DataParallel让模型并行运行来轻易的在多个GPU上运行操作
model = nn.DataParallel(model)
2.5pytorch编写自定义数据集
解决任何机器学习问题都需要花费大量精力来准备数据。 PyTorch 提供了许多工具来简化数据加载过程,并有望使代码更具可读性
数据集类
PyTorch 支持两种不同类型的数据集:
地图样式数据集
映射样式数据集是一种实现__getitem__()和__len__()协议的数据集,它表示从(可能是非整数)索引/关键字到数据样本的映射。
例如,当使用dataset[idx]访问时,此类数据集可以从磁盘上的文件夹中读取第idx张图像及其对应的标签。
迭代式数据集
可迭代样式的数据集是 IterableDataset 子类的实例,该子类实现了__iter__()协议,并表示数据样本上的可迭代。 这种类型的数据集特别适用于随机读取价格昂贵甚至不大可能,并且批处理大小取决于所获取数据的情况。
例如,这种数据集称为iter(dataset)时,可以返回从数据库,远程服务器甚至实时生成的日志中读取的数据流。
torch.utils.data.Dataset
是代表数据集的抽象类。 您的自定义数据集应继承Dataset
并覆盖以下方法:
__len__
,以便 len(dataset)
返回数据集的大小。
__getitem__
支持索引,以便可以使用dataset[i]
获取第 i个样本
Transfroms变换
大多数神经网络期望图像的大小固定。 因此,我们将需要编写一些预处理代码。 让我们创建三个转换:
Rescale
:缩放图像
RandomCrop
:从图像中随机裁剪。 这是数据增强。
ToTensor
:将 numpy 图像转换为 torch 图像(我们需要交换轴)。
我们会将它们编写为可调用的类,而不是简单的函数,这样就不必每次调用转换时都传递其参数。 为此,我们只需要实现__call__
方法,如果需要,还可以实现__init__
方法。 然后我们可以使用这样的变换:
tsfm = Transform(params)
transformed_sample = tsfm(sample)
要组成Rescale
和RandomCrop
转换
torchvision.transforms.Compose
是一个简单的可调用类,它使我们可以执行此操作。
scale = Rescale(256)
crop = RandomCrop(128)
composed = transforms.Compose([Rescale(256),
RandomCrop(224)])
双线性代码中的例子
train_transforms = torchvision.transforms.Compose([
torchvision.transforms.Resize(size=448), # 调整大小
torchvision.transforms.RandomHorizontalFlip(), # 依概率p垂直翻
torchvision.transforms.RandomCrop(size=448), # 随机裁剪
torchvision.transforms.ToTensor(),
torchvision.transforms.Normalize(mean=(0.485, 0.456, 0.406),
std=(0.229, 0.224, 0.225))
])
遍历数据集
使用简单的for
循环迭代数据,没有以下功能
- 批量处理数据
- 打乱数据
- 使用
multiprocessing
工作程序并行加载数据。
torch.utils.data.DataLoader
是提供所有这些功能的迭代器。
2.6导入模型
骨干网选择的是vgg16-pool5
针对VGG16进行具体分析发现,VGG16
共包含:
- 13个卷积层(Convolutional Layer),分别用conv3-XXX表示
- 3个全连接层(Fully connected Layer),分别用FC-XXXX表示
- 5个池化层(Pool layer),分别用maxpool表示
其中,卷积层和全连接层具有权重系数,因此也被称为权重层
,总数目为13+3=16,这即是
VGG16中16的来源。(池化层不涉及权重,因此不属于权重层,不被计数)。
PyTorch 的 VGG 实现是一个模块,分为两个子Sequential
模块:features
(包含卷积和池化层)和classifier
(包含完全连接的层)。某些层在训练期间的行为与评估不同,因此我们必须使用.eval()
将网络设置为评估模式。