作为pytorch的入门篇,本文将介绍如何使用标准数据集CIFAR-10来搭建一个完整的VGG16网络,以达到简单测试环境和认识pytorch网络基本框架的目的。
参考了博客:CNN02:Pytorch实现VGG16的CIFAR10分类
import torch
import torch.nn as nn
from torch import optim
from torch.utils.data import DataLoader, Dataset
from torchvision import transforms, utils, datasets
pytorch中的自定义网络均需要继承nn.Module
类,在其__init__
中定义各种类型的网络层,并实现forward()
函数搭建完整的前向传播网络结构。反向传播机制已经隐藏在各个类型网络层的实现中了,无需手动实现。
class VGG16(nn.Module):
def __init__(self, num_classes=10):
super(VGG16, self).__init__()
# Sequential相当于一个网络层的容器,继承nn.Module类,实现__call__函数
self.features = nn.Sequential(
# 1, 224*224*3, conv=3*3*64
nn.Conv2d(3, 64, kernel_size=3, padding=1),
nn.BatchNorm2d(64),
nn.ReLU(True),
# 2, 224*224*64, conv=3*3*64
nn.Conv2d(64, 64, kernel_size=3, padding=1),
nn.BatchNorm2d(64),
nn.ReLU(True),
nn.MaxPool2d(kernel_size=2, stride=2),
# 3, 112*112*64, conv=3*3*128
nn.Conv2d(64, 128, kernel_size=3, padding=1),
nn.BatchNorm2d(128),
nn.ReLU(True),
# 4
nn.Conv2d(128, 128, kernel_size=3, padding=1),
nn.BatchNorm2d(128),
nn.ReLU(True),
nn.MaxPool2d(kernel_size=2, stride=2),
# 5, 56*56*128, conv=3*3*256
nn.Conv2d(128, 256, kernel_size=3, padding=1),
nn.BatchNorm2d(256),
nn.ReLU(True),
# 6
nn.Conv2d(256, 256, kernel_size=3, padding=1),
nn.BatchNorm2d(256),
nn.ReLU(True),
# 7
nn.Conv2d(256, 256, kernel_size=3, padding=1),
nn.BatchNorm2d(256),
nn.ReLU(True),
nn.MaxPool2d(kernel_size=2, stride=2),
# 8, 28*28*256, conv=3*3*512
nn.Conv2d(256, 512, kernel_size=3, padding=1),
nn.BatchNorm2d(512),
nn.ReLU(True),
# 9
nn.Conv2d(512, 512, kernel_size=3, padding=1),
nn.BatchNorm2d(512),
nn.ReLU(True),
# 10
nn.Conv2d(512, 512, kernel_size=3, padding=1),
nn.BatchNorm2d(512),
nn.ReLU(True),
nn.MaxPool2d(kernel_size=2, stride=2),
# 11, 14*14*512, conv=3*3*512
nn.Conv2d(512, 512, kernel_size=3, padding=1),
nn.BatchNorm2d(512),
nn.ReLU(True),
# 12
nn.Conv2d(512, 512, kernel_size=3, padding=1),
nn.BatchNorm2d(512),
nn.ReLU(True),
# 13
nn.Conv2d(512, 512, kernel_size=3, padding=1),
nn.BatchNorm2d(512),
nn.ReLU(True),
nn.MaxPool2d(kernel_size=2, stride=2),
)
self.classifier = nn.Sequential(
# 14, 512=>4096
nn.Linear(512, 4096),
nn.ReLU(True),
nn.Dropout(),
# 15, 4096=>4096
nn.Linear(4096, 4096),
nn.ReLU(True),
nn.Dropout(),
# 16, 4096=>output_sizes
nn.Linear(4096, num_classes),
)
def forward(self, x):
# x.size=batch_size, channels, width, height
out = self.features(x)
# view()作用是合并除batch_size的其它全部的维度成一维
# -1表示剩余的维度数会自动统计放入该维度
out = out.view(out.size(0), -1)
out = self.classifier(out)
return out
__init__
中定义网络层:
首先要调用super(VGG16, self).__init__()
,然后就可以定义各种层了。这里使用了nn.Sequential
,它相当于一个网络层或者局部网络的容器,可以串联起各个网络层或者局部网络结构。当然也可以在这里只声明VGG16所需要的各种类型卷积层、池化层、全连接层和BN层等,然后在forward()
中一层层搭建。
forward()
中搭建完整的网络结构:
在这里才是真正的神经网络搭建的过程,目的是使用标准的或者在__init__
中声明的网络层或者网络结构对象,一步步地将原始数据x
转换成网络的最终输出out
。
上述代码中在__init__
中封装了卷积部分的self.features
对象和全连接部分的self.classifier
对象,并在两者之间用view()
做了特征图向特征向量的维度转换。
输入的x
是四维Tensor
类型,对应每个维度为batch_size, channels, width, height
。VGG16网络输出为二维的Tensor
类型,对应每个维度为batch_size, classifier_vector_size
。
在datasets.CIFAR10
中封装了标准的CIFAR-10训练集,可以直接调用下载使用。另外,需要使用DataLoader
来生成batch的数据,供网络批量调用。
# 下载训练集 CIFAR-10训练集
train_dataset = datasets.CIFAR10('./data', train=True, transform=transforms.ToTensor(), download=True)
train_loader = DataLoader(train_dataset, batch_size=batch_size, shuffle=True)
test_dataset = datasets.CIFAR10('./data', train=False, transform=transforms.ToTensor(), download=True)
test_loader = DataLoader(test_dataset, batch_size=batch_size, shuffle=False)
train=
对应的是调用不同的数据文件,为True
时使用data_batch_1
到data_batch_5
文件,为False
时使用test_batch
文件。这也正是训练集train_dataset
和测试集test_dataset
之间的区别。
transform=
是将原始数据转换成Tensor
类型的操作,调用transforms
下的对象也是标准的数据转换操作对象,无需手动实现。
download=True
的时候将自动下载数据集压缩包并解压。如果在路径中已经有该压缩包或者其解压文件夹,则均不会重新下载。
它下载的压缩包文件为cifar-10-python.tar
,解压后的文件夹为cifar-10-batches-py
。如果下载速度很慢,也可以在浏览器中下载,然后放到python项目的路径中。下载路径为:http://www.cs.toronto.edu/~kriz/cifar.html。
batch_size
是将数据集划分成一个个batch进行训练或者测试的尺寸,值得注意的是,batch_size
的数值并不会影响网络使用的数据总量,神经网络仍然会使用数据集中的全部数据。网络使用时,是在CPU统一读取一个batch的数据,然后再传给GPU统一处理(如果是使用CUDA的话),故通常而言,太小的batch_size
会增加CPU和GPU之间I/O的次数,形成性能瓶颈。实际效果需要观察运行时CPU和GPU的工作负载情况。另外,test_loader
和train_loader
的batch_size
可以不同,test_loader
中的batch_size
大小只会影响测试时处理的性能而不会改变模型训练的效果。
batch_size = 64 #不易过小
learning_rate = 0.01 # 学习率不易过大,以避免震荡不收敛
num_epoches = 10 # 每一个epoch均完全使用一个数据集中的全部数据
代码如下:
# 创建model实例对象
model = VGG16()
use_gpu = torch.cuda.is_available() # 判断是否有GPU加速
if use_gpu:
model = model.cuda()
# 定义loss为交叉熵损失
criterion = nn.CrossEntropyLoss()
# SGD优化器,参数如下:
# params (iterable) – 网络的parameters()
# lr (float) – 学习率
# momentum (float, 可选) – 动量因子(默认:0),即v=v∗momemtum−dx∗lr,然后x=x+v,如果为0是原始GD
# weight_decay (float, 可选) – 权重衰减(L2惩罚)(默认:0)
# dampening (float, 可选) – 动量的抑制因子(默认:0)
# nesterov (bool, 可选) – 使用Nesterov动量(默认:False)
optimizer = optim.SGD(model.parameters(), lr=learning_rate)
for epoch in range(num_epoches):
model.train() # 训练开始
print('*' * 25, 'epoch {}'.format(epoch + 1), '*' * 25) # 打印信息
running_loss = 0.0
running_acc = 0.0
# enumerate函数用于将一个可遍历的数据对象组合成一个索引序列
# enumerate(sequence, [start=0])
# sequence -- 一个序列、迭代器或其他支持迭代对象。
# start -- 下标起始位置。
# 返回的是元组(index, data)
for i, data in enumerate(train_loader, 0):
img, label = data
# cuda
if use_gpu:
img = img.cuda()
label = label.cuda()
# 向前传播
out = model(img)
loss = criterion(out, label)
# loss是一个标量(即一个batch的交叉熵损失),label的size是64
# 这里应该是不用乘size的,因为交叉熵损失本来就包含求和过程
running_loss += loss.item() * label.size(0)
# 返回batch中每个图片预测最大值所在的位置标签,size为64
# torch.max函数返回张量的最大值,参数0是获取每列的最大值,1是获取每行的最大值
# 返回out的最大值及其索引,两个均为tensor类型
_, pred = torch.max(out, 1)
# sum()用于统计pred==label的个数
num_correct = (pred == label).sum()
# mean()用于统计pred==label的均值
accuracy = (pred == label).float().mean()
# acc统计一个batch中预测正确的个数
running_acc += num_correct.item()
# 向后传播
optimizer.zero_grad() # 清空优化器的梯度
loss.backward() # 反向传播梯度,记录在优化器中
optimizer.step()
print('Finish {} epoch, Loss: {:.6f}, Acc: {:.6f}'.format(
epoch + 1, running_loss/(len(train_dataset)),
running_acc/(len(train_dataset))))
model.eval() # 训练结束,测试开始
eval_loss = 0
eval_acc = 0
for data in test_loader:
img, label = data
if use_gpu:
img = img.cuda()
label = label.cuda()
out = model(img)
loss = criterion(out, label)
eval_loss += loss.item()*label.size(0)
_, pred = torch.max(out, 1)
num_correct = (pred == label).sum()
eval_acc += num_correct.item()
print('Test Loss: {:.6f}, Acc: {:.6f}'.format(
eval_loss/len(test_dataset), eval_acc/len(test_dataset)))
# 向后传播
optimizer.zero_grad() # 清空优化器的梯度
loss.backward() # 反向传播梯度,记录在优化器中
optimizer.step() # 利用优化器中的梯度更新参数
batch
更新参数之后就需要清空,以免影响了后续batch
的梯度计算。而batch
的作用就是让网络能够使用一个batch
的图片信息计算的梯度来更新网络的权重参数而不是只使用一张图片的信息,从而避免陷入局部最优解中。model.train()
和model.eval()
是控制网络的self.training=True/Flase
的函数,主要是影响dropout
等在训练和测试阶段有不同的前向传播策略的网络层。epoch
都需要计算一次在训练集和测试集上的平均误差loss
和准确率acc
。Loader
类型而不是原始的Dataset
类型。Loader
类型中获取到的image
和label
都是一个batch
的数据,拥有batch
的维度(总共是四个维度和两个维度),而且已经转换成Tensor
类型,image
和label
之间两两对应;而原始的Dataset
类型没有batch
的维度,获取到的image
就是[3,32,32]
的一张三通道图片(三个维度),而label
就是int
类型(一个维度)。x
变量是一个四维Tensor
,所以如果误用了Dataset
类型,会有以下报错: RuntimeError: Expected 4-dimensional input for 4-dimensional weight [64, 3, 3, 3], but got 3-dimensional input of size [3, 32, 32] instead