Pytorch01:使用标准数据集CIFAR-10搭建VGG16网络

写在前面

作为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

VGG网络搭建

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
  1. __init__中定义网络层:
    首先要调用super(VGG16, self).__init__(),然后就可以定义各种层了。这里使用了nn.Sequential,它相当于一个网络层或者局部网络的容器,可以串联起各个网络层或者局部网络结构。当然也可以在这里只声明VGG16所需要的各种类型卷积层、池化层、全连接层和BN层等,然后在forward()中一层层搭建。

  2. forward()中搭建完整的网络结构:
    在这里才是真正的神经网络搭建的过程,目的是使用标准的或者在__init__中声明的网络层或者网络结构对象,一步步地将原始数据x转换成网络的最终输出out
    上述代码中在__init__中封装了卷积部分的self.features对象和全连接部分的self.classifier对象,并在两者之间用view()做了特征图向特征向量的维度转换。

  3. 输入的x是四维Tensor类型,对应每个维度为batch_size, channels, width, height。VGG16网络输出为二维的Tensor类型,对应每个维度为batch_size, classifier_vector_size

使用标准CIFAR-10训练集

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)
  1. train=对应的是调用不同的数据文件,为True时使用data_batch_1data_batch_5文件,为False时使用test_batch文件。这也正是训练集train_dataset和测试集test_dataset之间的区别。

  2. transform=是将原始数据转换成Tensor类型的操作,调用transforms下的对象也是标准的数据转换操作对象,无需手动实现。

  3. download=True的时候将自动下载数据集压缩包并解压。如果在路径中已经有该压缩包或者其解压文件夹,则均不会重新下载。
    它下载的压缩包文件为cifar-10-python.tar,解压后的文件夹为cifar-10-batches-py。如果下载速度很慢,也可以在浏览器中下载,然后放到python项目的路径中。下载路径为:http://www.cs.toronto.edu/~kriz/cifar.html。

  4. batch_size是将数据集划分成一个个batch进行训练或者测试的尺寸,值得注意的是,batch_size的数值并不会影响网络使用的数据总量,神经网络仍然会使用数据集中的全部数据。网络使用时,是在CPU统一读取一个batch的数据,然后再传给GPU统一处理(如果是使用CUDA的话),故通常而言,太小的batch_size会增加CPU和GPU之间I/O的次数,形成性能瓶颈。实际效果需要观察运行时CPU和GPU的工作负载情况。另外,test_loadertrain_loaderbatch_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)))
  1. 训练过程和测试过程的区别只在于测试过程没有反向传播的过程,即三行代码:
# 向后传播
optimizer.zero_grad()  # 清空优化器的梯度
loss.backward()  # 反向传播梯度,记录在优化器中
optimizer.step()  # 利用优化器中的梯度更新参数
  1. 梯度下降的过程主要是利用输入的特征通过迭代计算梯度,然后用梯度更新网络的权重参数。故梯度在一个batch更新参数之后就需要清空,以免影响了后续batch的梯度计算。而batch的作用就是让网络能够使用一个batch的图片信息计算的梯度来更新网络的权重参数而不是只使用一张图片的信息,从而避免陷入局部最优解中。
  2. model.train()model.eval()是控制网络的self.training=True/Flase的函数,主要是影响dropout等在训练和测试阶段有不同的前向传播策略的网络层。
  3. 每一个epoch都需要计算一次在训练集和测试集上的平均误差loss和准确率acc

注意

  1. 在提取数据的时候应当使用封装后的Loader类型而不是原始的Dataset类型。
    它们的区别在于从Loader类型中获取到的imagelabel都是一个batch的数据,拥有batch的维度(总共是四个维度和两个维度),而且已经转换成Tensor类型,imagelabel之间两两对应;而原始的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

你可能感兴趣的:(深度学习,python,深度学习,pytorch,神经网络)