本篇在上一篇笔记的基础上,使用公开数据集完整地训练出了一个基础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
训练一个图像分类器的有序步骤如下:
- 使用torchvision加载并归一化CIFAR10训练/测试数据
- 定义一个CNN网络
- 定义一个损失函数
- 使用训练数据训练这个网络
- 使用测试数据测试这个网络
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上跑了一把模型!算是成功入门啦!!!