PyTorch神经网络基础以CIFAR10图像分类为例
Posted ACMfans Club
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了PyTorch神经网络基础以CIFAR10图像分类为例相关的知识,希望对你有一定的参考价值。
本节详细介绍如何利用当下火热的PyTorch深度学习框架,搭建神经网络,并对CIFAR10图像数据集进行分类。如有错误,欢迎指出!阅读本文之前,读者应该到百科上面查找相关教程安装好PyTorch学习环境,如有则跳过。
编者按
01
—
机器学习基础知识
一般来讲,机器学习大体上分四种类型:监督学习、半监督学习、无监督学习、强化学习。在有监督学习中训练模型时,事先知道正确的答案;在强化学习过程中,定义了代理对特定动作的奖励。半监督学习,同时给出带类别标记的训练集和无标记的训练集然而,无监督学习处理的是无标签或结构未知的数据。
有关机器学习的认识,这里有个很好的学习网址《零基础入门深度学习》,建议新手还是通读完再继续往下看。戳原文连接。
02
—
开始搭建神经网络
一个神经网络的训练过程,一般遵循下面步骤:
定义一个包含可训练参数的神经网络
迭代整个输入
通过神经网络处理输入
计算损失
反向传播梯度到神经网络的参数
更新神经网络参数
一般来说,使用PyTorch都会引入下面包:
import torch # torch核心
import torch.nn as nn # torch神经网络包
import torch.nn.functional as F # 定义了神经网络中常用的一些函数
神经网络可以通过torch.nn来构建,一般来说,我们都会把自己自定义网络写成一个class,使其继承torch.nn.Module,它是所有神经网络单元的基类。
class Net(nn.Module):
我们要在Net里面实现__init__和forward函数模块,在__init__模块内,我们要定义神经网络的框架(例如定义多少层,每层多少节点等),在forward模块内,我们要定义数据如何在神经网络内传播(前向传播)。以上两点请务必记住其“使命”。
看一下官方给的__init__的示例:
class Net(nn.Module):
def __init__(self):
super(Net, self).__init__()
self.conv1 = nn.Conv2d(1, 6, 5)
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)
上面代码可能乍一看有些懵,我们一句一句来看懂它:
nn.Conv2d()是二维卷积方法,一般用于处理二维图像。第一和第二个参数分别指的是输入通道数和输出通道数。什么叫通道数?先简单考虑这样的一个例子:一张RGB图片,对应的通道数就是3.
那么第一句代码就指明了二维卷积方法的输入通道为1,输出通道为6。后面第三个参数呢?第三个参数表示的是卷积核的大小。类似的,第三句代码又定义了另外一个二维卷积方法,它的输入通道为6,输出通道为16,卷积核仍为5.
第三句呢?焦点集中在nn.Linear方法上面。这其实就是对输入数据做线性变换。第一个参数指的是输入样本的大小,第二个参数是输出样本的大小。
接下来要实现forward模块,还是来看官方的例子:
def forward(self, x):
x = F.max_pool2d(F.relu(self.conv1(x)), (2, 2))
x = F.max_pool2d(F.relu(self.conv2(x)), 2)
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
F.max_pool2d(F.relu(self.conv1(x)), (2, 2))表示二维最大池化,使用一个(2, 2)的滑动窗口。ReLU是一个激励函数,其图形是:
在torch里面,view相当于numpy的reshape函数,第一个参数为-1表示根据第二个参数值自动求出第一个参数值。
以上我们就完成了PyTorch搭建第一个神经网络的尝试。可视化这个网络,其实就是大名鼎鼎的LeNet:
注:其实,对RGB图像进行分类,更好的网络应该是AlexNet,但这里我们只是为了演示一下搭建神经网络的过程。相较于AlexNet,LeNet的搭建更佳简洁,适合入门。
附网络结构源码:
class Net(nn.Module):
def __init__(self):
super(Net, self).__init__()
self.conv1 = nn.Conv2d(3, 6, 5)
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):
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
03
—
认识CIFAR10数据集
The CIFAR-10 dataset consists of 60000 32x32 colour images in 10 classes, with 6000 images per class. There are 50000 training images and 10000 test images.
The dataset is divided into five training batches and one test batch, each with 10000 images. The test batch contains exactly 1000 randomly-selected images from each class. The training batches contain the remaining images in random order, but some training batches may contain more images from one class than another. Between them, the training batches contain exactly 5000 images from each class.
Here are the classes in the dataset, as well as 10 random images from each:
与 MNIST 数据集比, CIFAR-10 具有以下不同点:
• CIFAR-10 是 3 通道的彩色 RGB 图像,而 MNIST 是灰度图像。
• CIFAR-10 的图片尺寸为 32×32, 而 MNIST 的图片尺寸为 28×28,比 MNIST 稍大。
• 相比于手写字符, CIFAR-10 含有的是现实世界中真实的物体,不仅噪声很大,而且物体的比例、 特征都不尽相同,这为识别带来很大困难。直接的线性模型如 Softmax 在 CIFAR-10 上表现得很差。
04
—
加载CIFAR10数据集
PyTorch加载CIFAR10数据集异常简单,只需要:
(1)在原有头文件基础上再引入:
import torchvision # 一个加载著名视觉数据集的模块
import torchvision.transforms as transforms # 将图像数据转换为DataLoader模块
(2)加载模块:
trainset = torchvision.datasets.CIFAR10(root='./data', train=True, download=True, transform=transform)
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)
这里的transform为:
transform = transforms.Compose(
[
transforms.ToTensor(),
transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))
]
)
transform主要作用就是串联多个图片的变换操作,当执行compose方法的时候,会按照transform列表里面的操作进行遍历。transforms.ToTensor将numpy.ndarray或PIL.image读的照片转换成形状为(C, H, W)的Tensor格式,且归一化到[0, 1.0]之间。transforms.Normalize则把0-1变换到(-1, 1)
05
—
损失函数和梯度下降
回顾一下,Part 2我们已经构建起一个CNN网络结构,Part 4我们加载好了数据,现在就直接将数据送进神经网络吗?还差一步!那就是要定义神经网络的损失函数和梯度下降,这样神经网络才能在反向传播中更新参数。这里我们定义:
criterion = nn.CrossEntropyLoss()
optimizer = optim.SGD(net.parameters(), lr=0.001, momentum=0.9)
nn.CrossEntropyLoss损失函数结合了nn.LogSoftMax和nn.NLLLoss两个函数,它在做分类训练的时候是非常有用的,具体原因在这里就不多讲了(篇幅很长)。实际上,nn.CrossEntropyLoss并不完全和交叉熵完全一样,但是通过交叉熵的含义,我们还是可以浅显的理解一下nn.CrossEntropyLoss。
交叉熵:刻画实际输入与期望输出的距离,交叉熵的值越小,两个概率分布就越接近。
优化函数(梯度下降方法)就是要利用上面损失函数计算出来的损失值进行优化,这里我们采用了简单的随机梯度下降方法(SGD),学习率设置为0.001。
06
—
训练网络
先实例化网络:
net = Net()
接下来就要开始训练网络了,训练网络的逻辑理解起来就非常简单了,无非就是“喂数据-计算损失函数-反向传播更新参数”,代码(含注释):
for epoch in range(4): # 反复训练4轮
running_loss = 0.0 # 每一轮刚开始累计的损失值清零
for i, data in enumerate(trainloader, 0): # 取出当前迭代的序号i和数据data
inputs, labels = data # 图像数据inputs和标签数据labels
optimizer.zero_grad() # 清零梯度
outputs = net(inputs) # 喂数据
loss = criterion(outputs, labels) # 将计算结果和实际labels对比,计算出nn.CrossEntropyLoss
loss.backward() # 反向传播计算得到每个参数的梯度值
optimizer.step() # 执行梯度下降对参数进行更新
running_loss += loss.item()
if i % 2000 == 1999:
print('[%d, %5d] loss: %.3f' % (epoch + 1, i + 1, running_loss / 2000))
running_loss = 0.0
torch.save(net.state_dict(), "model.pth") # 阶段保存模型
torch.save(net.state_dict(), "model.pth")
print('Finished Training')
训练过程输出:
Files already downloaded and verified
Files already downloaded and verified
[1, 2000] loss: 2.232
[1, 4000] loss: 1.912
[1, 6000] loss: 1.712
[1, 8000] loss: 1.587
[1, 10000] loss: 1.524
[1, 12000] loss: 1.461
[2, 2000] loss: 1.411
[2, 4000] loss: 1.377
[2, 6000] loss: 1.353
[2, 8000] loss: 1.324
[2, 10000] loss: 1.306
[2, 12000] loss: 1.298
[3, 2000] loss: 1.230
[3, 4000] loss: 1.256
[3, 6000] loss: 1.213
[3, 8000] loss: 1.209
[3, 10000] loss: 1.213
[3, 12000] loss: 1.181
[4, 2000] loss: 1.129
[4, 4000] loss: 1.159
[4, 6000] loss: 1.149
[4, 8000] loss: 1.115
[4, 10000] loss: 1.156
[4, 12000] loss: 1.129
Finished Training
Process finished with exit code 0
可以看到,损失函数计算值基本呈现递减状态,到第4轮,损失函数计算值基本没什么有意义的变化了,这时达到收敛。
07
—
利用测试集直观感知模型训练效果
if os.path.exists('./model.pth'):
net.load_state_dict(torch.load("./model.pth"))
correct = 0
total = 0
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)
with torch.no_grad():
for data in testloader:
images, labels = data
outputs = net(images)
# 将net计算出来的tensor送入max,1表示取每行的最大值
# max返回两个tensor,第一个tensor是每一行的最大值(一般舍弃,我们不感兴趣),第二个tensor是每行最大值的索引
_, predicted = torch.max(outputs.data, 1)
total += labels.size(0) # labels.size(0) = 4
correct += (predicted == labels).sum().item()
# 等号右边的意义就是:predicted中4个值和labels中的4个值相同的个数
print('Accuracy of the network on the 10000 test images: %d %%' % (100 * correct / total))
输出:
Files already downloaded and verified
Accuracy of the network on the 10000 test images: 57 %
Process finished with exit code 0
准确率57%?没事,这是预料之中的,因为LeNet本来就不太适合对RGB图进行分类,对于灰度图像MNIST可以获得不错的效果。
值得注意的是:在CIFAR10给我们的训练集和测试集中,所谓“一张”图片,实际上是4张小图组成,对应的“一个”标签,由对应的4个类别组成。
08
—
模型应用
从网上下载若干张32 x 32的RGB图,用于模型的人工测试。
编写测试代码:
print('做个预测吧~,正在输入待预测的图片')
for i in range(10):
img_path = "./test_data/p" + str(i) + ".jpg" # 构造路径(图像集放在项目根目录下)
img = cv2.imread(img_path)
img = transform(img)
img = img.unsqueeze(0) # 最外围增加一个维度
outputs = net(img)
_, predicted = torch.max(outputs, 1)
print('预测类别:', classes[predicted[0]])
运行结果:
做个预测吧~,正在输入待预测的图片
预测类别:truck 实际类型:truck
预测类别:plane 实际类型:plane
预测类别:plane 实际类型:frog
预测类别:car 实际类型:car
预测类别:deer 实际类型:bird
预测类别:cat 实际类型:cat
预测类别:dog 实际类型:dog
预测类别:car 实际类型:deer
预测类别:truck 实际类型:house
预测类别:truck 实际类型:ship
Process finished with exit code 0
09
—
demo全部代码
import torch
import torchvision
import torchvision.transforms as transforms
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim
import matplotlib.pyplot as plt
import numpy as np
import os
import cv2
classes = ('plane', 'car', 'bird', 'cat', 'deer', 'dog', 'frog', 'horse', 'ship', 'truck')
class Net(nn.Module):
def __init__(self):
super(Net, self).__init__()
self.conv1 = nn.Conv2d(3, 6, 5)
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):
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
def imshow(img):
img = img / 2 + 0.5
npimg = img.numpy()
plt.imshow(np.transpose(npimg, (1, 2, 0)))
plt.show()
if __name__ == '__main__':
transform = transforms.Compose(
[
transforms.ToTensor(),
transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))
]
)
net = Net()
if os.path.exists('./model.pth'):
net.load_state_dict(torch.load("./model.pth"))
correct = 0
total = 0
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)
with torch.no_grad():
for data in testloader:
images, labels = data # 这里取出“一张”图像,实际上是包含了4张小图和对应的4个标签
# print(' '.join('%5s' % classes[labels[j]] for j in range(4)))
# imshow(torchvision.utils.make_grid(images)) # 以上这两句代码展示一下test集的图片及其对应的标签,可以注释掉
outputs = net(images)
# 将net计算出来的tensor送入max,1表示取每行的最大值
# max返回两个tensor,第一个tensor是每一行的最大值(一般舍弃),第二个tensor是每行最大值的索引
_, predicted = torch.max(outputs.data, 1)
total += labels.size(0) # labels.size(0) = 4
correct += (predicted == labels).sum().item()
# 等号右边的意义就是:predicted中4个值和labels中的4个值相同的个数
print('Accuracy of the network on the 10000 test images: %d %%' % (100 * correct / total))
print('做个预测吧~,正在输入待预测的图片')
for i in range(10):
img_path = "./test_data/p" + str(i) + ".jpg" # 构造路径(图像集放在项目根目录下)
img = cv2.imread(img_path)
img = transform(img)
img = img.unsqueeze(0) # 最外围增加一个维度
outputs = net(img) # 一维张量(含10个数值)
_, predicted = torch.max(outputs, 1)
# predicted是一个存储outputs最大值下标的tensor(也说明它只包含一个数值),那么predicted[0]就是outputs最大值的下标
print('预测类别:', classes[predicted[0]])
else:
trainset = torchvision.datasets.CIFAR10(root='./data', train=True, download=True, transform=transform)
trainloader = torch.utils.data.DataLoader(trainset, batch_size=4, shuffle=True, num_workers=2)
criterion = nn.CrossEntropyLoss()
optimizer = optim.SGD(net.parameters(), lr=0.001, momentum=0.9)
for epoch in range(4): # 反复训练4轮
running_loss = 0.0 # 每一轮刚开始累计的损失值清零
for i, data in enumerate(trainloader, 0): # 取出当前迭代的序号i和数据data
inputs, labels = data # 图像数据inputs和标签数据labels
optimizer.zero_grad() # 清零梯度
outputs = net(inputs) # 喂数据
loss = criterion(outputs, labels) # 将计算结果和实际labels对比,计算出nn.CrossEntropyLoss
loss.backward() # 反向传播计算得到每个参数的梯度值
optimizer.step() # 执行梯度下降对参数进行更新
running_loss += loss.item()
if i % 2000 == 1999:
print('[%d, %5d] loss: %.3f' % (epoch + 1, i + 1, running_loss / 2000))
running_loss = 0.0
torch.save(net.state_dict(), "model.pth") # 阶段保存模型
torch.save(net.state_dict(), "model.pth")
print('Finished Training')
10
—
What To Do Next ?....
以上是关于PyTorch神经网络基础以CIFAR10图像分类为例的主要内容,如果未能解决你的问题,请参考以下文章