VGGNet16实现Cifar10分类-源码解析

# 1 载入数据
import torch
import torchvision
import torchvision.transforms as transforms
import os
import numpy as np

# 使用torchvision可以很方便的额下载cifar10数据集,而torchvision下载的数据集为[0,1]的PILImage格式,需要将张量Tensor归一化到[-1,1]
transform = transforms.Compose(
    [transforms.ToTensor(), transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))])  # 将[0,1]归一化到[-1, 1]
'''transform函数分析:
1、transforms.Compose([trans1Func,transFunc2])表示多个转换的组合,依次转换
2、transforms.ToTensor()表示转换一个PIL库的图片或者numpy的数组为tensor张量类型;
(1)从[0,255]转换至[0,1],
(2)注意,此处还调整了image数据的顺序,由(0,1,2)转为(2,0,1)
3、transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5)):通过平均值和标准差来标准化一个tensor图像,公式为:
output[channel] = (input[channel] - mean[channel]) / std[channel];    
(1)第一个(0.5,0.5,0.5) 即三个通道的平均值
(2)第二个(0.5,0.5,0.5) 即三个通道的标准差值;
    注:ToTensor()已经将图像变为[0,1],我们使其变为[-1,1],以第一个通道为例,将最大与最小值代入公式
    min:(0-0.5)/0.5=-1
    max:(1-0.5)/0.5=1
    其他数值同理操作,即映射到[-1,1]
'''
trainset = torchvision.datasets.CIFAR10(root='../../pycifar', train=True, download=False, transform=transform)
    #root='../../pycifar'表示下载的数据集所在目录,'../'代表当前文件的上一级目录,我的数据集存在当前项目的前两级文件夹下的pycifar文件夹中,所以是’../../数据集文件名’,此处需要根据自己项目文件路径修改
trainloader = torch.utils.data.DataLoader(trainset, batch_size=4, shuffle=True, num_workers=2)
testset = torchvision.datasets.CIFAR10(root='../../pycifar', train=False, download=False, transform=transform)
testloader = torch.utils.data.DataLoader(testset, batch_size=4, shuffle=False, num_workers=2)
cifar10_classes = ('plane', 'car', 'bird', 'cat', 'deer', 'dog', 'frog', 'horse', 'ship', 'truck')

# 查看训练数据
dataiter = iter(trainloader)    # 随机从训练数据中取一些数据。
images, labels = dataiter.next()
'''iter()函数分析:
(1)for imgs, labels in iter(train_dl) 与 for labels, imgs in enumerate(trainloader) 作用类似
(2)iter(trainloader).next() 与 for labels, imgs in enumerate(trainloader) 作用类似,其中enumerate枚举的结果一定是lable在前,是标签形式;imgs在后;——与iter的输出顺序相反
(3)iter(dataloader)访问时,imgs在前,labels在后,分别表示:图像转换0~1之间的值,labels为标签值。并且imgs和labels是按批次进行输入的。
'''
images.shape                    # (4, 3, 32, 32) batch_size=4
'''images.shape函数解析
print(img.shape)  #参数-1为按原通道读入,不写的话默认读入三通道图片,例如(112,112,3)
print(img.shape[0])#读入的时图片的高度height
print(img.shape[1])#读入的时图片的宽度weight
'''
torchvision.utils.save_image(images[1], 'test.jpg')
for j in range(4):              #batch_size=4,所以一次读入四张图和对应标签,遍历这四个输入样本
    cifar10_classes[labels[j]]  #根据输入样本标签序号,还原回对应的分类标签,labels[j]=0-9;cifar10_classes[i]= ('plane', 'car',...)


# 2 构建卷积神经网络
import math
import torch
import torch.nn as nn

cfg = {'VGG16': [64, 64, 'M', 128, 128, 'M', 256, 256, 256, 'M', 512, 512, 512, 'M', 512, 512, 512, 'M']}
class VGG(nn.Module):
    def __init__(self, net_name):
        super(VGG, self).__init__()
        # 构建网络的卷积层和池化层,最终输出命名features,因为通常认为经过这些操作的输出为含图像空间信息的特征层。
        self.features = self._make_layers(cfg[net_name])
        '''
        #make layer是对每一层的层自定义,有点类似于nn.Sequential();但是layer是将每一层所要的网络或者激活函数归一化存在一个数组,目前看来他的优点,可以用一个for循环去定义整个网络,不用对对每一层都写同样的函数。从而只改变他的参数就行;
        示例:
        def make_layers(self, cfg, batch_norm=False):
            layers = []
            in_channels = 3
            for v in cfg:
                if v == 'M':
                    layers += [nn.MaxPool2d(kernel_size=2, stride=2)]
                else:
                    conv2d = nn.Conv2d(in_channels, v, kernel_size=3, padding=1)
                    if batch_norm:
                        layers += [conv2d, nn.BatchNorm2d(v), nn.ReLU(inplace=True)]
                    else:
                        layers += [conv2d, nn.ReLU(inplace=True)]
                    in_channels = v
            return nn.Sequential(*layers)
        '''
        # 构建卷积层之后的全连接层以及分类器
        self.classifier = nn.Sequential(    #有序的容器,神经网络模块将按照在传入构造器的顺序依次被添加到计算图中执行,同时以神经网络模块为元素的有序字典也可以作为传入参数。
            nn.Dropout(p=0.5),              #剪枝,p为将元素置为0的概率,
            nn.Linear(512, 512),
            nn.ReLU(inplace=True),          #inplace设置为True,计算结果不会有影响。利用in-place计算可以节省内(显)存,同时还可以省去反复申请和释放内存的时间。但是会对原变量覆盖,只要不带来错误就用。
            nn.Dropout(),
            nn.Linear(512, 512),
            nn.ReLU(True),
            nn.Linear(512, 10))
        # 初始化权重
        for m in self.modules():
            if isinstance(m, nn.Conv2d):
                '''isinstance(object, classinfo)函数解析:
                    #isinstance() 函数来判断一个对象是否是一个已知的类型,是Python的一个内置函数,类似于type(),但考虑继承关系
                    参数:(1)object – 实例对象。
                         (2)classinfo – 可以是直接或间接类名、基本类型或者由它们组成的元组。一般为:int,float,bool,complex,str(字符串),list,dict(字典),set,tuple
                    输出:布尔值,即True/False
                '''
                n = m.kernel_size[0] * m.kernel_size[1] * m.out_channels
                m.weight.data.normal_(0, math.sqrt(2. / n))#weight.data.normal_(mean,var)表示对权重进行初始化,且以mean为均值,以var为方差
                m.bias.data.zero_()         #对偏置bias初始化为0
    #前向传播
    def forward(self, x):
        x = self.features(x)                # 前向传播的时候先经过卷积层和池化层
        x = x.view(x.size(0), -1)
        x = self.classifier(x)              # 再将features(得到网络输出的特征层)的结果拼接到分类器上
        return x

    #构建卷积层-根据cfg中卷积各层参数构建
    def _make_layers(self, cfg):            #对每一层的层自定义,有点类似于nn.Sequential(),layer将每一层所要的网络或者激活函数归一化存在一个数组,可以用一个for循环去定义整个网络,不用对每一层都写同样的函数。从而只改变他的参数就行了。
        layers = []
        in_channels = 3
        for v in cfg:                       #遍历cfg中每一层的参数,,M为最大池化,数字都为3×3,pad=1卷积
            if v == 'M':
                layers += [nn.MaxPool2d(kernel_size=2, stride=2)]
            else:
                layers += [nn.Conv2d(in_channels, v, kernel_size=3, padding=1), nn.BatchNorm2d(v),
                           nn.ReLU(inplace=True)]
                '''nn.Conv2d(self, in_channels, out_channels, kernel_size, stride=1, padding=0, dilation=1, groups=1, bias=True))
                参数:
                in_channel: 输入数据的通道数,例RGB图片通道数为3;
                out_channel: 输出数据的通道数,这个根据模型调整;
                kennel_size: 卷积核大小,可以是int,或tuple;kennel_size=2,意味着卷积大小(2,2), kennel_size=(2,3),意味着卷积大小(2,3)即非正方形卷积
                stride:步长,默认为1,与kennel_size类似,stride=2,意味着步长上下左右扫描皆为2, stride=(2,3),左右扫描步长为2,上下为3;
                padding: 默认为零填充
                h/w = (h/w - kennel_size + 2*padding) / stride + 1
                '''
                in_channels = v
        return nn.Sequential(*layers)       #*作用在形参上,代表这个位置接收任意多个非关键字参数,转化成元组方式;*作用在实参上,代表的是将输入迭代器拆成一个个元素。
        #nn.Sequential为一个有序的容器,神经网络模块将按照在传入构造器的顺序依次被添加到计算图中执行,同时以神经网络模块为元素的有序字典也可以作为传入参数。

net = VGG('VGG16')
'''
#网络结构实例化:
1-初始化:
(1)搭建特征层-features:根据cfg参数搭建卷积层
(2)搭建分类层-classifier:序列化多次剪枝模块+线性层+激活,
(3)初始化各层权重和偏置
'''


# 3 定义损失函数和优化方法
import torch.optim as optim #导入优化方法optimizer基类
criterion = nn.CrossEntropyLoss()
optimizer = optim.SGD(net.parameters(), lr=0.001, momentum=0.9)
'''class torch.optim.SGD(params, lr=, momentum=0, dampening=0, weight_decay=0, nesterov=False)函数参数解析:
    (1)params (iterable) – 待优化参数的iterable或者是定义了参数组的dict
    (2)lr (float) – 学习率
    (3)momentum (float, 可选) – 动量因子(默认:0),该参数可以加快梯度下降速度,避免陷入局部最优,需要特别关注
    (4)weight_decay (float, 可选) – 权重衰减(L2惩罚)(默认:0)
    (5)dampening (float, 可选) – 动量的抑制因子(默认:0)
    (6)nesterov (bool, 可选) – 使用Nesterov动量(默认:False)
'''

# 4 卷积神经网络训练
for epoch in range(5):
    train_loss = 0.0
    for batch_idx, data in enumerate(trainloader, 0):
        # 初始化
        inputs, labels = data               # 获取数据
        optimizer.zero_grad()               # 先将梯度置为0
        # 优化过程
        outputs = net(inputs)               # 将数据输入到网络,得到第一轮网络前向传播的预测结果outputs
        loss = criterion(outputs, labels)   # 预测结构outputs和labels通过之前定义的交叉熵计算损失。
        loss.backward()                     # 误差反向传播
        optimizer.step()                    # 随机梯度下降法优化权重
        # 查看网络训练状态
        train_loss += loss.item()
        if batch_idx % 2000 == 1999:        # 每迭代2000个batch打印看一次当前网络收敛情况
            print('[%d, %5d] loss: %.3f' % (epoch + 1, batch_idx + 1, train_loss / 2000))
            trian_loss = 0.0
    print('Saving epoch %d model ...' % (epoch + 1))
    state = {'net': net.state_dict(),
             'epoch': epoch + 1, }
    if not os.path.isdir('checkpoint'):
        os.mkdir('checkpoint')
    torch.save(state, './checkpoint/cifar10_epoch_%d.ckpt' % (epoch + 1))
print('Finished Trainig')


# 5 批量计算整个测试集预测效果
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()   #当标记的label种类和预测的种类一致时认为正确,并计数。
print('Accuracy of the network on the 10000 test images: %d %%' % (100 * correct / total))
#分别查看每个类的预测效果
class_correct = list(0. for i in range(10))
class_total = list(0. for i in range(10))
with torch.no_grad():
    for data in testloader:
        images, labels = data
        outputs = net(images)
        _, predicted = torch.max(outputs, 1) 
        #注:torch.max()这个函数返回的是两个值,第一个值是具体的value(我们用下划线_表示),第二个值是value所在的index(也就是predicted),用“_”命名返回的具体value,表示第一个返回值是我们不关心的输入,
        c = (predicted == labels).squeeze()
        for i in range(4):
            label = labels[i]
            class_correct[label] += c[i].item()
            class_total[label] += 1
for i in range(10):
    print('Accuracy of %5s : %2d %%' %(cifar10_classes[i], 100 * class_correct[i] / class_total[i]))

备注: 本文为自己学习《深度学习与图像识别:原理与实践》过程中,对第八章-卷积神经网络中VGGNet16实现Cifar10分类的源码不懂的地方的一些注释,用以记录自己学习过程中遇到的一些问题,供自己回顾学习,供大家参考

你可能感兴趣的:(深度学习,深度学习,计算机视觉,pytorch)