DL Practice:Cifar 10分类

Step 1:数据加载和处理

一般使用深度学习框架会经过下面几个流程:

模型定义(包括损失函数的选择)——>数据处理和加载——>训练(可能包括训练过程可视化)——>测试

所以自己写代码的时候基本上按照这四大模块四步走就ok了。

本例步骤:

A、Load and normalizing the CIFAR10 training and test datasets using torchvision
B、Define a Convolution Neural Network
C、Define a loss function
D、Train the network on the training data
E、Test the network on the test data

首先使用torchvision加载和归一化训练数据与测试数据。

DL Practice:Cifar 10分类

 

torchvision实现了常用的一些深度学习的相关图像数据的加载功能,比如cifar10、Imagenet、Mnist等。保存在torchvision.datasets模块中;

torchvision也封装了一些处理数据的方法。保存在torchvision.transforms模块中。

torchvision还封装了一些模型和工具封装在相应模型中。

#  首先当然肯定要导入torch和torchvision,至于第三个是用于进行数据预处理的模块
import torch
import torchvision
import torchvision.transforms as transforms
 
   #  由于torchvision的datasets的输出是[0,1]的PILImage,所以我们先先归一化为[-1,1]的Tensor
    #  首先定义了一个变换transform,利用的是上面提到的transforms模块中的Compose( )
    #  把多个变换组合在一起,可以看到这里面组合了ToTensor和Normalize这两个变换
transform = transforms.Compose(
    [transforms.ToTensor(),
     transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))]) 
 
    # 定义了我们的训练集,名字就叫trainset,至于后面这一堆,其实就是一个类:
    # torchvision.datasets.CIFAR10( )也是封装好了的,就在我前面提到的torchvision.datasets
    # 模块中,不必深究,如果想深究就看我这段代码后面贴的图1,其实就是在下载数据
    #(不FQ可能会慢一点吧)然后进行变换,可以看到transform就是我们上面定义的transform
trainset = torchvision.datasets.CIFAR10(root='./data', train=True,
                                        download=True, transform=transform)
    # trainloader其实是一个比较重要的东西,我们后面就是通过trainloader把数据传入网
    # 络,当然这里的trainloader其实是个变量名,可以随便取,重点是它是由后面的
    # torch.utils.data.DataLoader()定义的,这个东西来源于torch.utils.data模块,
    #  网页链接http://pytorch.org/docs/0.3.0/data.html,这个类可见我后面图2
trainloader = torch.utils.data.DataLoader(trainset, batch_size=4,
                                          shuffle=True, num_workers=2)
    # 对于测试集的操作和训练集一样
testset = torchvision.datasets.CIFAR10(root='./data', train=False,
                                       download=True, transform=transform)
testloader = torch.utils.data.DataLoader(testset, batch_size=4,
                                         shuffle=False, num_workers=2)
    # 类别信息也是需要我们给定的
classes = ('plane', 'car', 'bird', 'cat',
'deer', 'dog', 'frog', 'horse', 'ship', 'truck')

  

Step 2:定义卷积神经网络

这一步虽然代码量很少,但是却包含很多难点和重点,执行这一步的代码需要包含神经网络工具箱torch.nn、神经网络函数torch.nn.functional。

注意:虽然官网给的程序有这么一句 from torch.autograd import Variable,但是此步中确实没有显式地用到variable,只能说网络里运行的数据确实要以variable的形式存在,在后面我们会讲解这个内容。

# 首先是调用Variable、 torch.nn、torch.nn.functional
from torch.autograd import Variable   # 这一步还没有显式用到variable,但是现在写在这里也没问题,后面会用到
import torch.nn as nn
import torch.nn.functional as F
 
 
class Net(nn.Module):                 # 我们定义网络时一般是继承的torch.nn.Module创建新的子类
    def __init__(self):    
        super(Net, self).__init__()   # 第二、三行都是python类继承的基本操作,此写法应该是python2.7的继承格式,但python3里写这个好像也可以
        self.conv1 = nn.Conv2d(3, 6, 5)       # 添加第一个卷积层,调用了nn里面的Conv2d()
        self.pool = nn.MaxPool2d(2, 2)        # 最大池化层
        self.conv2 = nn.Conv2d(6, 16, 5)      # 同样是卷积层
        self.fc1 = nn.Linear(16 * 5 * 5, 120) # 接着三个全连接层
        self.fc2 = nn.Linear(120, 84)
        self.fc3 = nn.Linear(84, 10)
 
    def forward(self, x):                  # 这里定义前向传播的方法,为什么没有定义反向传播的方法呢?这其实就涉及到torch.autograd模块了,
                                           # 但说实话这部分网络定义的部分还没有用到autograd的知识,所以后面遇到了再讲
        x = self.pool(F.relu(self.conv1(x)))  # F是torch.nn.functional的别名,这里调用了relu函数 F.relu()
        x = self.pool(F.relu(self.conv2(x)))
        x = x.view(-1, 16 * 5 * 5)  # .view( )是一个tensor的方法,使得tensor改变size但是元素的总数是不变的。
                                    #  第一个参数-1是说这个参数由另一个参数确定, 比如矩阵在元素总数一定的情况下,确定列数就能确定行数。
                                    #  那么为什么这里只关心列数不关心行数呢,因为马上就要进入全连接层了,而全连接层说白了就是矩阵乘法,
                                    #  你会发现第一个全连接层的首参数是16*5*5,所以要保证能够相乘,在矩阵乘法之前就要把x调到正确的size
                                    # 更多的Tensor方法参考Tensor: http://pytorch.org/docs/0.3.0/tensors.html
        x = F.relu(self.fc1(x))
        x = F.relu(self.fc2(x))
        x = self.fc3(x)
        return x
 
 # 和python中一样,类定义完之后实例化就很简单了,我们这里就实例化了一个net
net = Net()

知识点

1、神经网络工具箱torch.nn

:这是一个专为深度学习设计的模块,我们来看一下官方文档中它的目录。

DL Practice:Cifar 10分类

a.  Container中的Module,也即nn.Module

DL Practice:Cifar 10分类

 

 

 看一下nn.Module的详细介绍:

DL Practice:Cifar 10分类

 

 

  可知,nn.Module是所有神经网络的基类,我们自己定义任何神经网络,都要继承nn.Module!class Net(nn.Module)。

b.  convolution layers

DL Practice:Cifar 10分类

 

 

 我们在上面的代码块中用到了Conv2d: self.conv1 = nn.Conv2d(3, 6, 5)    self.conv2 = nn.Conv2d(6, 16, 5)

DL Practice:Cifar 10分类

 

 

 例如Conv2d(1,20,5)的意思就是说,输入是1通道的图像,输出是20通道,也就是20个卷积核,卷积核是5*5,其余参数都是用的默认值。

c.  pooling layers

DL Practice:Cifar 10分类

 

 

 可以看到有很多的池化方式,我们上面的代码采用的是Maxpool2d: self.pool = nn.MaxPool2d(2, 2)

DL Practice:Cifar 10分类

d.  Linear layer

DL Practice:Cifar 10分类

我们代码中用的是线性层Linear: self.fc1 = nn.Linear(16 * 5 * 5, 120)      self.fc2 = nn.Linear(120, 84)        self.fc3 = nn.Linear(84, 10)

DL Practice:Cifar 10分类

e.   Non-linear Activations

要注意,其实这个例子中的非线性激活函数用的并不是torch.nn模块中的这个部分,但是torch.nn模块中有这个部分,所以还是提一下。
此例中的激活函数用的其实是torch.nn.functional 模块中的函数。它们是有区别的,区别下文继续讲。现在先浏览一下这个部分的内容即可:
DL Practice:Cifar 10分类

可以看出,torch.nn 模块中其实也有很多激活函数的,只不过我们此例用的不是这里的激活函数!!!  

2、torch.nn.functional

torch.nn中大多数layer在torch.nn.funtional中都有一个与之对应的函数。二者的区别在于:
torch.nn.Module中实现layer的都是一个特殊的类,可以去查阅,他们都是以class xxxx来定义的,会自动提取可学习的参数
而nn.functional中的函数,更像是纯函数,由def function( )定义,只是进行简单的数学运算而已。
说到这里你可能就明白二者的区别了,functional中的函数是一个确定的不变的运算公式,输入数据产生输出就ok,
而深度学习中会有很多权重是在不断更新的,不可能每进行一次forward就用新的权重重新来定义一遍函数来进行计算,所以说就会采用类的方式,以确保能在参数发生变化时仍能使用我们之前定好的运算步骤。
所以从这个分析就可以看出什么时候改用nn.Module中的layer了:
如果模型有可学习的参数,最好使用nn.Module对应的相关layer,否则二者都可以使用,没有什么区别。
比如此例中的Relu其实没有可学习的参数,只是进行一个运算而已,所以使用的就是functional中的relu函数,
而卷积层和全连接层都有可学习的参数,所以用的是nn.Module中的类。
不具备可学习参数的层,将它们用函数代替,这样可以不用放在构造函数中进行初始化。

定义网络模型,主要会用到的就是torch.nn 和torch.nn.funtional这两个模块,这两个模块值得去细细品味一番,希望大家可以去读一下官方文档!

Step 3:定义损失函数和优化器

import torch.optim as optim          #导入torch.potim模块
 
criterion = nn.CrossEntropyLoss()    #同样是用到了神经网络工具箱 nn 中的交叉熵损失函数
optimizer = optim.SGD(net.parameters(), lr=0.001, momentum=0.9)   #optim模块中的SGD梯度优化方式---随机梯度下降

涉及知识点

1、优化器

pytorch将深度学习中常用的优化方法全部封装在torch.optim之中,所有的优化方法都是继承基类optim.Optimizier

DL Practice:Cifar 10分类

 

图中提到了如果想要把模型搬到GPU上跑,就要在定义优化器之前就完成.cuda( )这一步。

2、损失函数

损失函数是封装在神经网络工具箱nn中的,包含很多损失函数,如图所示:

DL Practice:Cifar 10分类

 

此例中用到的是交叉熵损失,criterion = nn.CrossEntropyLoss() 详情如下:

DL Practice:Cifar 10分类

 

 DL Practice:Cifar 10分类

 

DL Practice:Cifar 10分类

 

 基本上这一步没什么好说的,就是在众多的优化器方法和损失函数中选择就ok,具体选什么就由自己情况定。

 Step 4:训练

经过前面的数据加载和网络定义后,就可以开始训练了,这里会看到前面遇到的一些东西究竟在后面会有什么用,所以这一步应该仔细研究一下。

for epoch in range(2):  # loop over the dataset multiple times 指定训练一共要循环几个epoch
 
    running_loss = 0.0  #定义一个变量方便我们对loss进行输出
    for i, data in enumerate(trainloader, 0): # 这里我们遇到了第一步中出现的trailoader,代码传入数据
                                              # enumerate是python的内置函数,既获得索引也获得数据,详见下文
        # get the inputs
        inputs, labels = data   # data是从enumerate返回的data,包含数据和标签信息,分别赋值给inputs和labels
 
        # wrap them in Variable
        inputs, labels = Variable(inputs), Variable(labels) # 将数据转换成Variable,第二步里面我们已经引入这个模块
                                                            # 所以这段程序里面就直接使用了,下文会分析
        # zero the parameter gradients
        optimizer.zero_grad()                # 要把梯度重新归零,因为反向传播过程中梯度会累加上一次循环的梯度
 
        # forward + backward + optimize      
        outputs = net(inputs)                # 把数据输进网络net,这个net()在第二步的代码最后一行我们已经定义了
        loss = criterion(outputs, labels)    # 计算损失值,criterion我们在第三步里面定义了
        loss.backward()                      # loss进行反向传播,下文详解
        optimizer.step()                     # 当执行反向传播之后,把优化器的参数进行更新,以便进行下一轮
 
        # print statistics                   # 这几行代码不是必须的,为了打印出loss方便我们看而已,不影响训练过程
        running_loss += loss.data[0]         # 从下面一行代码可以看出它是每循环0-1999共两千次才打印一次
        if i % 2000 == 1999:    # print every 2000 mini-batches   所以每个2000次之类先用running_loss进行累加
            print('[%d, %5d] loss: %.3f' %
                  (epoch + 1, i + 1, running_loss / 2000))  # 然后再除以2000,就得到这两千次的平均损失值
            running_loss = 0.0               # 这一个2000次结束后,就把running_loss归零,下一个2000次继续使用
 
print('Finished Training')

分析:

1、autograd

DL Practice:Cifar 10分类

 

在第二步中我们定义网络时定义了前向传播函数,但是并没有定义反向传播函数,可是深度学习是需要反向传播求导的,
Pytorch其实利用的是Autograd模块来进行自动求导,反向传播。
Autograd中最核心的类就是Variable了,它封装了Tensor,并几乎支持所有Tensor的操作,这里可以参考官方给的详细解释:
http://pytorch.org/tutorials/beginner/blitz/autograd_tutorial.html#sphx-glr-beginner-blitz-autograd-tutorial-py
以上链接详细讲述了variable究竟是怎么能够实现自动求导的,怎么用它来实现反向传播的。

这里涉及到计算图的相关概念。想要计算各个variable的梯度,只需调用根节点的backward方法,Autograd就会自动沿着整个计算图进行反向计算
而在此例子中,根节点就是我们的loss,所以:

程序中的loss.backward()代码就是在实现反向传播,自动计算所有的梯度。

所以训练部分的代码其实比较简单:
running_loss和后面负责打印损失值的那部分并不是必须的,所以关键行不多,总得来说分成三小节
第一节:把最开始放在trainloader里面的数据给转换成variable,然后指定为网络的输入;
第二节:每次循环新开始的时候,要确保梯度归零
第三节:forward+backward,就是调用我们在第三步里面实例化的net()实现前传,loss.backward()实现后传
每结束一次循环,要确保梯度更新。

 Step 5:测试

dataiter = iter(testloader)      # 创建一个python迭代器,读入的是我们第一步里面就已经加载好的testloader
images, labels = dataiter.next() # 返回一个batch_size的图片,根据第一步的设置,应该是4张
 
# print images
imshow(torchvision.utils.make_grid(images))  # 展示这四张图片
print('GroundTruth: ', ' '.join('%5s' % classes[labels[j]] for j in range(4))) # python字符串格式化 ' '.join表示用空格来连接后面的字符串,参考python的join()方法

这一部分代码就是先随机读取4张图片,让我们看看这四张图片是什么并打印出相应的label信息,

因为第一步里面设置了是shuffle了数据的,也就是顺序是打乱的,所以各自出现的图像不一定相同。

outputs = net(Variable(images))      # 注意这里的images是我们从上面获得的那四张图片,所以首先要转化成variable
_, predicted = torch.max(outputs.data, 1)  
                # 这个 _ , predicted是python的一种常用的写法,表示后面的函数其实会返回两个值
                # 但是我们对第一个值不感兴趣,就写个_在那里,把它赋值给_就好,我们只关心第二个值predicted
                # 比如 _ ,a = 1,2 这中赋值语句在python中是可以通过的,你只关心后面的等式中的第二个位置的值是多少
 
print('Predicted: ', ' '.join('%5s' % classes[predicted[j]] for j in range(4)))  # python的字符串格式化

 

这里用到了torch.max(  ), 它是属于Tensor的一个方法:

DL Practice:Cifar 10分类

 

 注意到注释中第一句话,是说返回输入Tensor中每行的最大值,并转换成指定的dim(维度).

DL Practice:Cifar 10分类

 

 

所以我们程序中的 torch.max(outputs.data, 1) ,返回一个tuple (元组)

而这里很明显,这个返回的元组的第一个元素是image data,即是最大的 值,第二个元素是label, 即是最大的值 的 索引!

我们只需要label(最大值的索引),所以就会有 _ , predicted这样的赋值语句,表示忽略第一个返回值,把它赋值给 _, 就是舍弃它的意思;

我在注释中也说明了这是什么意思

这里说一下,这第二个参数1,看清楚上面的说明是 the dimension to reduce! 而不是去这个dimension上面找最大

所以这里dim=1,基于我们的a是 4行 x 4列 这么一个维度,所以指的是 消除列这个维度,这是个什么意思呢?

如果我们把上面的示例代码中,的参数 keepdim=True写上,torch.max(a,1,keepdim=True), 会发现,返回的结果的第一个元素,即表示最大的值的那部分,其实是一个 size为 【4,1】的Tensor,也就是其实它是在 按照每行 来找最大,所以结果是4行,然后因为只找一个最大值,所以是1列,整个size就是 4行 1 列, 然后参数dim=1,相当于调用了 squeeze(1),这个操作,上面的说明也是这么写的,所以最后就得到结果是一个size为4的vector。

你可以自己下去在ipython里面做实验,发现如果dim=0,它其实是在返回每列的最大值,

所以一定不要搞混!这里的dim是指的 the dimension to reduce!并不是在the dimension上去返回最大值。

所以其实我自己写的时候一般更喜欢用 torch.argmax()这个函数更直观更好理解一些

总之在这里你只需要理解这行操作的功能是:返回了最大的索引,即预测出来的类别。 想深入研究可以自己去ipython里面试一下

correct = 0   # 定义预测正确的图片数,初始化为0
total = 0     # 总共参与测试的图片数,也初始化为0
for data in testloader:  # 循环每一个batch
    images, labels = data
    outputs = net(Variable(images))  # 输入网络进行测试
    _, predicted = torch.max(outputs.data, 1)
    total += labels.size(0)          # 更新测试图片的数量
    correct += (predicted == labels).sum() # 更新正确分类的图片的数量
 
print('Accuracy of the network on the 10000 test images: %d %%' % (
    100 * correct / total))          # 最后打印结果

tutorial给的结果是53%.

来测试一下每一类的分类正确率:

class_correct = list(0. for i in range(10)) # 定义一个存储每类中测试正确的个数的 列表,初始化为0
class_total = list(0. for i in range(10))   # 定义一个存储每类中测试总数的个数的 列表,初始化为0
for data in testloader:     # 以一个batch为单位进行循环
    images, labels = data
    outputs = net(Variable(images))
    _, predicted = torch.max(outputs.data, 1)
    c = (predicted == labels).squeeze()
    for i in range(4):      # 因为每个batch都有4张图片,所以还需要一个4的小循环
        label = labels[i]   # 对各个类的进行各自累加
        class_correct[label] += c[i]
        class_total[label] += 1
 
 
for i in range(10):
    print('Accuracy of %5s : %2d %%' % (
        classes[i], 100 * class_correct[i] / class_total[i]))

 

上一篇:PAT (Basic Level) Practice || 1011 A+B 和 C (15 分)


下一篇:一个好用的小工具 thefuck