今天我们将动手训练一个图像分类器,并用它去判断某些图片分别属于哪个类别
步骤
- 使用torchvision加载并且归一化CIFAR-10的训练集和测试集
- 定义一个卷积神经网络
- 定义一个损失函数
- 训练网络
- 测试网络
CIFAR-10数据集简介
CIFAR-10数据集由10个类别的60000张32x32彩色图像组成,每个类别有6000张图像。有50000个训练图像和10000个测试图像。
一共包含10 个类别的RGB 彩色图片:飞机( airplane )、汽车( automobile )、鸟类( bird )、猫( cat )、鹿( deer )、狗( dog )、蛙类( frog )、马( horse )、船( ship )和卡车( truck )。
官网链接:CIFAR-10 and CIFAR-100 datasets
torchvision介绍
它不仅可以加载数据,也可以进行数据预处理,其次,他还包含了大量预训练的模型结构,如AlexNet、VGG、ResNet、Inception系列、DenseNet以及SqueezeNet等,我们可以直接加载这些模型进行迁移学习
训练图像分类器的全过程
1.加载数据集
import torch
import torchvision
import torchvision.transforms as transforms
transform = transforms.Compose(
[transforms.ToTensor(),
transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))])
transforms.Compose:将多个图像变换操作组合成一个序列,以便按照特定的顺序应用到图像上,上述代码块将先对图像类型进行转化,再标准化图像
transforms.ToTensor():这个变换将 PIL 图像或者 NumPy 的 ndarray 转换为 PyTorch 的 Tensor,它会自动把图像的像素值从 0-255 的整数范围缩放到 [0.0,1.0] 的浮点数范围,并且改变图像的维度顺序,从 (h,w,c)
变为 (c,h,w)
h:高度
w:宽度
c:通道数(对于彩色图像,通常是 3,代表 RGB)
Q:为什么要这样调整维度顺序?
A:主要是为了适应深度学习框架的设计、优化内存访问、提高计算效率以及确保模型各层之间的顺利连接
transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5)):这个变换用于标准化图像。它有两个参数,都是长度为 3 的元组,分别代表 RGB 三个通道的均值和标准差。
而标准化公式是:
(image - mean) / std
因此,这个标准化操作实际上是将每个通道的像素值从 [0, 1]
范围移动到 [-1, 1]
范围
这是深度学习模型训练中常见的预处理步骤,有助于模型更好地收敛。
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)
若是第一次下载数据集,会出现进度条,如果下载很慢,可以在其他地方下载好后,放到data目录下
download=True
: 如果数据集不在指定的根目录下,则自动从互联网上下载
transform=transform
: 将之前定义的transform
应用到数据集的每一张图像上
batch_size=4
: 每个批次包含4张图像。
shuffle=True
: 在每个训练周期(epoch)开始时,打乱数据集中的样本顺序。这有助于模型更好地泛化,防止过拟合
num_workers=2
: 使用2个子进程来加载数据,可以加速数据的加载速度
至此数据集加载完毕,trainloader
现在是一个可以迭代的数据加载器,每次迭代返回一个包含4张图像及其对应标签的批次数据。这些数据已经被转换为了PyTorch的Tensor格式,并经过了归一化处理,可以直接用于模型的训练。
现在我们可以展示一些训练图片看看
classes = ('plane', 'car', 'bird', 'cat',
'deer', 'dog', 'frog', 'horse', 'ship', 'truck')
import matplotlib.pyplot as plt
import numpy as np
def imshow(img):
img = img / 2 + 0.5
npimg = img.numpy()
plt.imshow(np.transpose(npimg, (1, 2, 0)))
plt.show()
dataiter = iter(trainloader)
images, labels = next(dataiter)
imshow(torchvision.utils.make_grid(images))
print(' '.join('%5s' % classes[labels[j]] for j in range(4)))
iter( ):将trainloader
转换为一个迭代器对象,迭代器允许你逐个访问数据加载器中的批次数据,而不需要一次性加载所有数据到内存中,这对于处理大规模数据集特别有用,因为它可以有效地管理内存使用
torchvision.utils.make_grid(images):将多个图像组合成一个网格图像
2.定义卷积神经网络
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)
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 __init__(self):
nn.Module:它是所有神经网络模块的基类,自定义的神经网络也应该继承这个类
self.conv1 = nn.Conv2d(3, 6, 5):定义了一个二维卷积层,输入通道数为3(例如RGB图像),输出通道数为6,卷积核大小为5x5,卷积层默认步长为1
self.pool = nn.MaxPool2d(2, 2):定义了一个最大池化层,池化窗口大小为2x2,池化层默认步长为2
self.conv2 = nn.Conv2d(6, 16, 5):第二个二维卷积层,输入通道数为6(与conv1
的输出通道数匹配),输出通道数为16,卷积核大小为5x5
全连接层输入卷积核大小(卷积后的特征图大小)计算:
原特征图大小 - 卷积核大小 + 1
- +1:是为了补偿卷积核在边界上的第一次放置。当卷积核放在特征图的左上角时,它仍然会覆盖特征图的某些像素,即使这些像素可能位于特征图的边界上。如果不加1,那么计算出的新特征图大小会偏小,因为它没有考虑到卷积核在边界上的第一次放置。
self.fc1 = nn.Linear(16 * 5 * 5, 120):全连接层,输入节点数为16*5*5
,输出节点数为120
- 16:这是第二个卷积层
self.conv2
的输出通道数 - 5 * 5:
由于CIFAR-10数据集的图像大小是32x32,
那么在forward(前向传播)函数里:
对第传入的 x 进行conv1
(卷积核5x5
)卷积,得到特征图大小是 32 - 5 + 1 = 28,然后对其激活池化
,池化层之后,特征图大小减半,变为28 / 2 = 14,(如果特征大小为奇数,则
向上取整,例如:29
⮕ 15)。
接着conv2
(卷积核5x5
),特征图大小是14 - 5 + 1 = 10,继续激活池化10 / 2 = 5
所以32x32的图像最终通过卷积池化后的特征图大小,即输入到全连接层的大小为5*5
self.fc2 = nn.Linear(120, 84):第二个全连接层,输入节点数为120,输出节点数为84
self.fc3 = nn.Linear(84, 10):第三个全连接层,输入节点数为84,输出节点数为10(用于分类10个类别的问题)
def forward(self, x):
x = self.pool(F.relu(self.conv1(x))):首先对输入x
进行卷积操作,然后应用ReLU激活函数,最后进行最大池化
x = self.pool(F.relu(self.conv2(x))):对经过第一个卷积和池化层的输出进行第二个卷积操作,再次应用ReLU激活函数和最大池化
x = x.view(-1, 16 * 5 * 5):将卷积层的输出展平,以便可以输入到全连接层。这里的-1
表示自动计算该维度的大小,以确保总元素数不变
x = F.relu(self.fc1(x)) 和 x = F.relu(self.fc2(x)):数据通过两个全连接层,并在每层之后应用ReLU激活函数
x = self.fc3(x):最后,数据通过第三个全连接层,但这次没有激活函数(通常用于分类任务的输出层)
3.定义损失和优化器
net = Net()
import torch.optim as optim
criterion = nn.CrossEntropyLoss()
optimizer = optim.SGD(net.parameters(), lr=0.001, momentum=0.9)
nn.CrossEntropyLoss():一个在PyTorch中用于多分类问题的损失函数,在多分类问题中,每个输入样本都被赋予一个类别标签,而模型的输出是一个概率分布,表示每个类别的预测概率。nn.CrossEntropyLoss() 会比较这些预测概率和真实标签,并计算损失
net.parameters():返回一个包含模型中所有可训练参数的迭代器
optim.SGD(net.parameters(), ):创建了一个SGD优化器,它将对net
(一个神经网络模型)中的所有参数进行更新
momentum=0.9:
动量,一个用于加速SGD在相关方向上的收敛并抑制震荡的超参数。动量可以看作是之前梯度的累积,它有助于穿越狭长的山谷,以及逃离鞍点或局部最小值
4.训练网络
for epoch in range(2):
running_loss = 0.0
for i, data in enumerate(trainloader, 0):
# get the inputs
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')
epoch:叫法:周期、轮次
batch:叫法:批次
running_loss = 0.0:初始化一个变量来记录当前周期内的累积损失
optimizer.zero_grad():在每次迭代开始时,将模型的参数梯度清零。这是必要的,因为PyTorch会累积梯度,不清零会导致梯度累加
outputs = net(inputs):将输入数据inputs传递给模型net,并得到模型的输出outputs
loss = criterion(outputs, labels):使用损失函数计算模型输出outputs和真实标签labels之间的损失
loss.backward():对损失进行反向传播,计算模型参数的梯度
optimizer.step():使用优化器optimizer根据计算出的梯度更新模型的参数
running_loss += loss.item():将当前批次的损失添加到running_loss中
if i % 2000 == 1999::每处理完2000个批次的数据,执行一次里面的代码块
模余操作:简言之,当你用一个小于除数的被除数对除数取模时,结果就是那个数本身,因为这个数小于2000,所以不能被2000整除,余数就是它本身
running_loss = 0.0:重置running_loss
,为下一个2000批次的数据做准备
5.使用测试集评估
由于前面没有定义验证集,也没有在训练循环中每个epoch结束后使用验证集来评估模型的性能
写这篇文章的主要目的是想加深一下我对使用pytorch框架进行模型训练和卷积神经网络的理解
所以这里直接跳过验证集用测试集评估了
现在我们来检查一下,这个网络模型是否已经学到了东西
我们将用神经网络的输出作为预测的类标来检查网络的预测性能,用样本的真实类标来校对
如果预测是正确的,我们将样本添加到正确预测的列表里
# 提取测试集图片和标签,并做部分展示
dataiter = iter(testloader)
images, labels = dataiter.next()
imshow(torchvision.utils.make_grid(images))
print('GroundTruth: ', ' '.join('%5s' % classes[labels[j]] for j in range(4)))
网络的最后一层是一个全连接层,输出的是预测的与十个类的近似程度,值越大则表示与某一个类的近似程度越高,网络就越认为图像是属于这一类别。所以让我们打印其中最相似类别类标:
outputs = net(images)
_, predicted = torch.max(outputs, 1)
print('Predicted: ', ' '.join('%5s' % classes[predicted[j]]
for j in range(4)))
接下来我们对测试集中的每一张图片都进行预测,并且计算整体的准确率
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()
print('Accuracy of the network on the 10000 test images: %d %%' % (
100 * correct / total))
torch.no_grad():使用torch.no_grad上下文管理器可以确保在计算过程中不会保存任何中间变量的计算图。(也可以说是设置了不计算梯度)这对于推理或评估模型非常有用,因为它可以节省内存并加速计算
outputs = net(images):将图像传递给神经网络net
进行前向传播,得到输出
labels.size(0):得到当前批次中的图像数量
(predicted == labels).sum().item():predicted == labels会产生一个布尔张量,其中每个元素表示对应位置的预测是否正确,sum()方法计算这个布尔张量中True的数量(在 PyTorch 中,True
被视为 1
,False
被视为 0
),从而得到正确预测的总数。如果不使用 .item()
,.sum()
将返回一个包含单个元素的张量。如果 correct 是一个 Python 整数,那么直接将一个张量加到整数上会导致类型错误,所以现在知道item()的作用了吧:直接提取张量中的 Python 数值,并可以将其与 Python 变量(如整数)一起使用
如果模型是随机预测的话,那么准确率应该是10%(10分类任务),而我们的训练两轮得到的模型,准确率为53%。这说明网络还是学到了一些东西。
为了进行精细化分析,下面我们看看模型在每一个类别上的准确率
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
print('labels:', labels) # labels: tensor([3, 8, 8, 0]) 值为0-9,代表不同的类别
outputs = net(images)
_, predicted = torch.max(outputs, 1)
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 %%' % (
classes[i], 100 * class_correct[i] / class_total[i]))
class_correct和class_total:两个长度为10的列表,分别用于存储每个类别的正确预测数和总预测数。初始值都是0.0
.squeeze():这里我查了资料后还是感觉没理解,然后分别运行对比了一下,有无这个操作都不会报错,而且结果也是一样的🤔
for i in range(4)::
Q:为什么这里是4呢?
A:因为前面定义testloader的时候batch_size设置的值为4,所以现在这个data里面存放的就是4张图片的图像及标签,现在每个如果不想手动设置range()里面的参数可以这样:
for label ,correct in zip(labels, c)
这样就会遍历该批次中的所有图像
6.使用GPU加速
device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
print(device)
接着使用下面命令将神经网络模块移到CUDA设备上:
net.to(device)
需要注意的时,当网络移动到CUDA设备后,输入到网络中的张量也需要先移动到CUDA设备上。
因为PyTorch只能在同一个设备上做矩阵操作
inputs, labels = inputs.to(device), labels.to(device)