大可的PyTorch学习笔记(二) 神经网络进一步

本篇在上一篇笔记的基础上,使用公开数据集完整地训练出了一个基础CNN模型,并使用测试集进行测试,同时也尝试了GPU进行训练。对应官方教程请点击

前情提要

在上一篇中,我们已经学会了如何去搭建一个简单的CNN框架,并实现反向传播更新权重参数。然而,我们并没有使用真实数据集进行训练和测试,同时也没有实现大量的迭代训练过程。除此之外,深度学习中十分重要的GPU使用我们也未曾涉及。在本篇中,我们将根据官网的教程继续深入下去,完成一套基于CIFAR10数据集的CNN网络训练和测试。

What about data?

对于现实问题中的输入数据,我们往往会有图像、文本、音频或视频等形式,对于这些数据,我们可以使用标准的Python数据包进行加载并存储为numpy array格式,再通过torch提供的方法转换为torch.Tensor类型。这些常见的Python工具包有:

  • For images, packages such as Pillow, OpenCV are useful
  • For audio, packages such as scipy and librosa
  • For text, either raw Python or Cython based loading, or NLTK and SpaCy are useful

Pytroch专门为视觉类公共数据集创建了名为torchvision的模块,囊括了诸如Imagenet, CIFAR10, MNIST等常见数据集。工具包内最常使用的两个函数是torchvision.datasets及torch.utils.data.DataLoader,至于怎么使用,我们后文会提到。

本次教程使用的是CIFAR10数据集,一共提供了10种标签:‘airplane’, ‘automobile’, ‘bird’, ‘cat’, ‘deer’, ‘dog’, ‘frog’, ‘horse’, ‘ship’, ‘truck’,每张图尺寸为3x32x32,即3通道(red-green-blue)的32x32像素图片。

Training an image classifier

训练一个图像分类器的有序步骤如下:

  1. 使用torchvision加载并归一化CIFAR10训练/测试数据
  2. 定义一个CNN网络
  3. 定义一个损失函数
  4. 使用训练数据训练这个网络
  5. 使用测试数据测试这个网络

1. Loading and normalizing CIFAR10

首先要import相关模块

import torch
import torchvision
import torchvision.transforms as transforms

然后加载CIFAR10数据集并将值归一化到[-1, 1]区间便于计算:

transform = transforms.Compose(
    [transforms.ToTensor(),
     transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))])

trainset = torchvision.datasets.CIFAR10(root='./data', train=True,
                                        download=True, transform=transform) #由于是第一次使用该数据集,所以设置download=True来下载它
trainloader = torch.utils.data.DataLoader(trainset, batch_size=4, #一个batch为4张图
                                          shuffle=True, num_workers=2) #num_workers表示线程数

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')

上述代码中的第一步Compose函数起到的作用是将多个transform操作衔接到一起。这里是将转化为Tensor操作和归一化操作合并到了一起,为后续函数的传参做准备。由于我们是第一次使用这个数据集,因此在相关函数中,我们把download设置为True,如果之后再次运行这段代码,可以修改为False防止重复下载。num_workers代表加载数据集的线程数,官方提示道,若在Windows系统上运行得到了'BrokenPipeError'错误,可以尝试把该参数设为0解决,在这里我没有遇到。留意一个细节,加载函数中的batch_size设置成了4,我们后续会用到这个参数。

 

随后,我们可以试着打印出数据集中的一些图片看看是否加载成功:

import matplotlib.pyplot as plt
import numpy as np

# functions to show an image


def imshow(img):
    img = img / 2 + 0.5     # unnormalize
    npimg = img.numpy()
    plt.imshow(np.transpose(npimg, (1, 2, 0)))
    plt.show()


# get some random training images
dataiter = iter(trainloader) #创建一个迭代器
images, labels = dataiter.next()

# show images
imshow(torchvision.utils.make_grid(images))
# print labels
print(' '.join('%5s' % classes[labels[j]] for j in range(4)))

得到图片如下(超低清): 

 2. Define a Convolutional Neural Network

这次定义的CNN模型和上一章中一样,只是代码略有不同:

import torch.nn as nn
import torch.nn.functional as F


class Net(nn.Module):
    def __init__(self):
        super(Net, self).__init__()
        self.conv1 = nn.Conv2d(3, 6, 5) #输入为3个channel,用6个5x5卷积核
        self.pool = nn.MaxPool2d(2, 2) #这次的池化层不在forward中
        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):
        x = self.pool(F.relu(self.conv1(x)))
        x = self.pool(F.relu(self.conv2(x)))
        x = x.view(-1, 16 * 5 * 5)
        x = F.relu(self.fc1(x))
        x = F.relu(self.fc2(x))
        x = self.fc3(x)
        return x


net = Net()

可以看到的是,这一次池化层没有在forward函数中使用,而是直接定义在了__init__中。

按理来说,普通的CNN中池化层是没有需要更新的参数的,那为何可以将其定义在__init__中,又如何能保证它参与到前向传播的运算呢?

在查询相关资料后,发现Python支持一种类特性,即在类中实现了__call__方法就可以将某个类的实例化能像函数一样接收参数并执行__call__方法内定义的操作,而在这里我们所使用的Net、nn.Conv2d、nn.MaxPool2d等类都实现了__call__方法。我们在使用net实例时,通过__call__方法间接调用了forward方法;而forward中用到的卷积层实例、池化层实例也通过各自的__call__方法调用了自身的forward的方法实现前向传播。

因此,我们可以通过像前一篇的方法中那样,直接在Net类的forward方法中显示定义池化层的操作,也可以像本篇中一样,通过Net的forward间接调用nn.MaxPool2d类的forward来实现传播。这两种方法带来的核心思想是:Pytorch正是利用了Python的__init__、__call__和forward之间巧妙的关系来构建神经网络的

刚刚提到的另一个疑惑则是,明明池化层没有需要更新的参数,为何能定义在__init__中?换句话说,其他放在__init__方法中的实例为何就能够被更新?

我试着去搜寻了“Pytorch自定义训练参数”的方法,发现,如果要实现一个自定义的参数,必须要在__init__内将该变量的requires_grad属性显式地设置为True,否则即使写在__init__中,该参数也不会被更新。那么可以见得,Pytorch封装好的这些卷积层、全连接层的参数都已实现这一点,而池化层则没有,因此不会被更新。

我们可以打印模型参数来印证这一点:

params = list(net.parameters())
print(len(params))
for p in params:
    print(p.size())

输出结果为:

10
torch.Size([6, 3, 5, 5])
torch.Size([6])
torch.Size([16, 6, 5, 5])
torch.Size([16])
torch.Size([120, 400])
torch.Size([120])
torch.Size([84, 120])
torch.Size([84])
torch.Size([10, 84])
torch.Size([10])

可见,只有卷积层和全连接层的参数会被更新。

 3. Define a Loss function and optimizer

这里使用交叉熵作为损失函数,并用momentum进行优化

import torch.optim as optim

criterion = nn.CrossEntropyLoss()
optimizer = optim.SGD(net.parameters(), lr=0.001, momentum=0.9)

4. Train the network

有了数据和模型,我们就可以进行训练啦!

for epoch in range(2):  # loop over the dataset multiple times

    running_loss = 0.0
    for i, data in enumerate(trainloader, 0):
        # get the inputs; data is a list of [inputs, labels]
        inputs, labels = data

        # zero the parameter gradients
        optimizer.zero_grad()

        # forward + backward + optimize
        outputs = net(inputs)
        loss = criterion(outputs, labels)
        loss.backward()
        optimizer.step()

        # print statistics
        running_loss += loss.item()
        if i % 2000 == 1999:    # print every 2000 mini-batches
            print('[%d, %5d] loss: %.3f' %
                  (epoch + 1, i + 1, running_loss / 2000))
            running_loss = 0.0

print('Finished Training')

打印出的训练过程如下:

[1,  2000] loss: 1.568
[1,  4000] loss: 1.518
[1,  6000] loss: 1.483
[1,  8000] loss: 1.433
[1, 10000] loss: 1.389
[1, 12000] loss: 1.341
[2,  2000] loss: 1.257
[2,  4000] loss: 1.286
[2,  6000] loss: 1.260
[2,  8000] loss: 1.227
[2, 10000] loss: 1.245
[2, 12000] loss: 1.221
Finished Training

 如果想要保存模型以便下次继续使用,可以进行如下操作:

PATH = './cifar_net.pth'
torch.save(net.state_dict(), PATH)

加载模型的代码如下:

net = Net()
net.load_state_dict(torch.load(PATH))

5. Test the network on the test data

接下来,我们利用测试集来测试模型的准确率:

correct = 0
total = 0
with torch.no_grad(): #这一行使下列操作默认不进行梯度相关的中间计算
    for data in testloader:
        images, labels = data
        outputs = net(images)
        #torch.max()返回Tensor每一行的最大值以及索引,这里我们关心的是索引值
        _, predicted = torch.max(outputs.data, 1) 
        total += labels.size(0)
        correct += (predicted == labels).sum().item() #item方法能取出Tensor中的值

print('Accuracy of the network on the 10000 test images: %d %%' % (
    100 * correct / total))

模型的准确率为:

Accuracy of the network on the 10000 test images: 58 %

 我们可以试着打印出每个标签的分类准确率:

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)
        c = (predicted == labels) #squeeze函数压缩掉维数为1的维度
        #c = (predicted == labels).squeeze()实验证明不加squeeze也没问题
        for i in range(4):
            label = labels[i]
            class_correct[label] += c[i].item() #item()将值从Tensor中取出来
            class_total[label] += 1


for i in range(10):
    print('Accuracy of %5s : %2d %%' % (
        classes[i], 100 * class_correct[i] / class_total[i]))

我的输出结果:

Accuracy of plane : 63 %
Accuracy of   car : 88 %
Accuracy of  bird : 34 %
Accuracy of   cat : 27 %
Accuracy of  deer : 57 %
Accuracy of   dog : 56 %
Accuracy of  frog : 72 %
Accuracy of horse : 62 %
Accuracy of  ship : 62 %
Accuracy of truck : 57 %

至此,我们的模型已经训练完毕并且可以使用了,鼓掌!!!

Training on GPU

然而!!!!最炫酷的一部分还没介绍!!!那就是在GPU上跑模型!!实验室那么多块GPU不用多浪费!!!!当然Pytorch提供的方法非常简单!

首先,你要先检查你有么有GPU可以用,如果有的话就用,并且给他送给代号方便随时召唤,没有的话就乖乖用你的CPU吧!代码如下!

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)

我直接用了我的台式机的GPU,输出如下:

cuda:0

 于是乎!我们拿到了一块可以使用的GPU,接下来就是把模型和数据搬上去了!官方教程这里给的很简单,只是告诉你该调用什么方法,没告诉你在哪儿调用!于是我就试试试试试试试试试试,然后试明白了!直接上代码!

把模型搬上去请用:

net.to(device)

把数据搬上去请用:

inputs, labels = data[0].to(device), data[1].to(device)

原来模型需要修改的地方是:首先在使用数据前把模型给搬上去!光搬数据不搬模型训练时会报错!(当然只搬模型也会报错,这个代码我就不放了)然后在训练和测试的时候搬数据!

训练时:

for epoch in range(2):  # loop over the dataset multiple times

    running_loss = 0.0
    for i, data in enumerate(trainloader, 0):
        # get the inputs; data is a list of [inputs, labels]
        #inputs, labels = data
        inputs, labels = data[0].to(device), data[1].to(device) #一定要把模型转移到GPU上,否则会报错

        # zero the parameter gradients
        optimizer.zero_grad()

        # forward + backward + optimize
        outputs = net(inputs)
        loss = criterion(outputs, labels)
        loss.backward()
        optimizer.step()

        # print statistics
        running_loss += loss.item()
        if i % 2000 == 1999:    # print every 2000 mini-batches
            print('[%d, %5d] loss: %.3f' %
                  (epoch + 1, i + 1, running_loss / 2000))
            running_loss = 0.0

测试时:

correct = 0
total = 0
with torch.no_grad(): #这一行使下列操作默认不进行梯度相关的中间计算
    for data in testloader:
        images, labels = data[0].to(device), data[1].to(device)
        outputs = net(images)
        _, predicted = torch.max(outputs.data, 1)
        total += labels.size(0)
        correct += (predicted == labels).sum().item()

本来我想偷个懒直接把trainloader给搬上GPU,就写了句

trainloader.to(device)
print(trainloader[0][0])

结果报错

AttributeError                            Traceback (most recent call last)
<ipython-input-17-37772259abd4> in <module>()
----> 1 trainloader.to(device) #.to只能对Module和Tensor起作用
      2 print(trainloader[0][0])

AttributeError: 'DataLoader' object has no attribute 'to'

呕!只有模型和Tensor能搬!好的好的我知道了!呕! 

以上就是所有内容!

总结

终于搞清楚nn.Module使用的奥义了!__init__和forward到底是什么神仙也算心里有个底了!也成功在GPU上跑了一把模型!算是成功入门啦!!!

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值