PyTorch记录四:训练分类器

根据前面的三次教程,我们了解了如何定义网络,计算损失和更新网络权重。前面我们的输入数据都是随机的,那么真实的数据呢?这一次我们将用真实数据训练分类器。

一. 关于数据:

通常,我们会处理图像,文本,音频或视频数据。你可以使用下面列出的python标准包将数据加载到numpy数组中,之后像我们记录一中提到的,使用torch.as_tensor()方法,将它转化成torch.Tensor。

  • 对于图像数据,可以使用像Pillow, OpenCV等包

  • 对于音频数据,使用scipy和librosa等

  • 对于文本数据,使用原生的Python语句和Cython,或者NLTK和SpaCy

具体的,我们使用torchvision来处理计算机视觉的任务,它包含一下内容:

  • 数据集:torchvision.datasets提供了一些常见的图像数据集,如 ImageNet、CIFAR10 和 MNIST。
  • 数据转换器:torchvision.transforms提供了一些图像转换操作,对图像进行预处理,例如将图像调整大小、裁剪、归一化等。
  • 数据加载器:torch.utils.data.DataLoader可以按批次(patches)的加载数据,提供了批量加载(以便有效利用GPU)、是否打乱数据顺序(是否在每一个轮次epoch开始时打乱数据,避免模型对数据的顺序敏感)、并行加载(多线程)等功能。
from torch.utils.data import DataLoader
from torchvision import datasets, transforms

在本教程中,我们将使用 CIFAR10 数据集。它包含很多类别:飞机,汽车,鸟,猫,鹿,狗,青蛙,马,船,卡车。图像的大小为 3x32x32,也就是 32x32 像素大小的 3 通道彩色图像。

下面我们将按顺序执行以下步骤:

  1. 使用torchvision加载并归一化 CIFAR10 训练和测试数据集

  2. 定义卷积神经网络

  3. 定义损失函数

  4. 在训练数据上训练网络

  5. 在测试数据上测试网络

二. 训练分类器:

1. 加载数据并归一化

为了加速模型的收敛过程,我们首先将数据的范围归一化到[-1, 1]。原因是,torchvision中数据集的格式是范围在[0, 1]的PILImage,不利于训练。然后我们按照批量大小依次加载图像到torch.utils.data.DataLoader的实例trainloader和testloader中。

import torch
import torchvision
import torchvision.transforms as transforms

# 转换成张量,再使用transforms.Normalize()方法进行归一化
# PILImage在[0,1]之间,因此每个通道的均值和标准差都是0.5
transform = transforms.Compose(
    [transforms.ToTensor(),
     transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))])

# 批量大小
batch_size = 4

# 训练集
trainset = torchvision.datasets.CIFAR10(root='./data', train=True,
                                        download=True, transform=transform)
trainloader = torch.utils.data.DataLoader(trainset, batch_size=batch_size,
                                          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=batch_size,
                                         shuffle=False, num_workers=2)

# 类标签
classes = ('plane', 'car', 'bird', 'cat',
           'deer', 'dog', 'frog', 'horse', 'ship', 'truck')

一个有趣的例子,刚刚我们将图像归一化处理了,现在我们进行反向操作,复原它们并且画出它们。由于torch.utils.data.DataLoader是可迭代的,我们可以使用迭代的方法依次调用这些批次的图像。那么可以像这样打印一个批次的图像的代码:

import matplotlib.pyplot as plt
import numpy as np

# 打印一张图


def imshow(img):
    img = img / 2 + 0.5     # 刚刚将图片数据归一化了,现在反归一化,以便显示
    npimg = img.numpy()
    # transforms.ToTensor()会自动将通道数放第一位,这里要改回来
    # 使用np.transpose()更改多维数组的轴顺序
    # CHW->HWC
    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(f'{classes[labels[j]]:5s}' for j in range(batch_size)))

这里我们就完成了数据的加载。如果你运行下面的代码,查看images的大小:

images.shape

 Out:

torch.Size([4, 3, 32, 32])

这可以验证,每一次迭代输入的数据images,是一个四个三通道32*32的图像,这与我们定义的批次大小(batch_size)相符。

上一个教程中,我们定义了一个神经网络,并使用一张单通道图像对它进行训练。下面我们根据上述加载真实数据的过程对它进行修改,以实现分批次的训练。

2. 定义卷积神经网络
import torch.nn as nn
import torch.nn.functional as F


class Net(nn.Module):
    def __init__(self):
        super().__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 = torch.flatten(x, 1) # flatten all dimensions except batch
        x = F.relu(self.fc1(x))
        x = F.relu(self.fc2(x))
        x = self.fc3(x)
        return x


net = Net()
3. 定义损失函数和优化器

上次我们提到优化器模块optim提供不同的优化器,这里我们选择交叉熵损失计算损失,使用带动量的SGD来更新参数。

import torch.optim as optim

criterion = nn.CrossEntropyLoss()
optimizer = optim.SGD(net.parameters(), lr=0.001, momentum=0.9)
4. 训练网络:

这里我们就很简单了,循环遍历数据迭代器,将输入提供给网络并进行优化。

for epoch in range(2):  # 多次遍历整个数据集

    running_loss = 0.0
    for i, data in enumerate(trainloader, 0):  # 遍历trainloader中的所有批次
        # 得到输入; data是[inputs, labels]的list
        inputs, labels = data

        # 梯度清零
        optimizer.zero_grad()

        # 前向 + 后向 + 优化
        outputs = net(inputs)
        loss = criterion(outputs, labels)
        loss.backward()
        optimizer.step()

        # 打印统计信息
        running_loss += loss.item()
        if i % 2000 == 1999:    # 每2000个批次,打印一次,每个批次四个图像
            # 计算他们损失的平均值
            print(f'[{epoch + 1}, {i + 1:5d}] loss: {running_loss / 2000:.3f}')
            running_loss = 0.0

print('Finished Training')

Out:

[1,  2000] loss: 2.156
[1,  4000] loss: 1.803
[1,  6000] loss: 1.630
[1,  8000] loss: 1.563
[1, 10000] loss: 1.502
[1, 12000] loss: 1.472
[2,  2000] loss: 1.409
[2,  4000] loss: 1.361
[2,  6000] loss: 1.359
[2,  8000] loss: 1.346
[2, 10000] loss: 1.322
[2, 12000] loss: 1.286
Finished Training

现在我们可以保存我们训练好的模型了。这里还提供了其他关于保存和加载的方法。训练结束之后,只对模型进行验证的话,就可以直接加载这个训练好的模型。而不用重新训练。

PATH = './trained_net.pth'
torch.save(net.state_dict(), PATH)
5. 使用测试集测试:

我们的模型已经在训练集上训练了两次,下面我们将使用测试集来测试,模型是否从训练集中学到了什么。如果模型正确预测了测试集的标签,那么该样本将被存储在正确预测列表中。

好的,首先我们先用一组测试图片来练习一下,输出图片和它对应的标签。

dataiter = iter(testloader)
images, labels = next(dataiter)

# print images
imshow(torchvision.utils.make_grid(images))
print('GroundTruth: ', ' '.join(f'{classes[labels[j]]:5s}' for j in range(4)))

Out:

GroundTruth:  cat   ship  ship  plane

现在,让我们重新加载刚才保存的模型(由于你执行了上述代码训练了模型,其实不需要重新加载它,我们这里只是为了展示如何重新加载它)。

net = Net()
net.load_state_dict(torch.load(PATH, weights_only=True)) # PATH就是刚才我们保存的路径

那么,下面我们就可以查看,模型认为这几张图是什么。

outputs = net(images)

如果你查看或者打印outputs,将会得到图片十个类别的能量。能量越高,代表图片属于该类别的可能性越大,我们输出预测的类别。

# predicted是能量最大的类别的索引组成的一维张量
_, predicted = torch.max(outputs, 1)
print('Predicted: ', ' '.join(f'{classes[predicted[j]]:5s}'
                              for j in range(4)))

Out:

Predicted:  cat   car   ship  ship 

结果看起来还不错,下面我们可以尝试整个测试集。

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(f'网络在10000个测试数据上的准确率是: {100 * correct // total} %')

准确率是56%,看起来还不错。但如果我们只是随机从十个种类中选择一个,那么正确的概率只有10%。下面,我们需要查看哪些类别被训练的很好,哪些不好,以便我们调整训练的方向。

correct_pred = {classname: 0 for classname in classes}
total_pred = {classname: 0 for classname in classes}


with torch.no_grad():
    for data in testloader:
        images, labels = data
        outputs = net(images)
        _, predictions = torch.max(outputs, 1)
        # 找到每个类别被正确预测的数据
        # zip()可以将相应未知的元素对应成为元组
        for label, prediction in zip(labels, predictions):
            if label == prediction:
                correct_pred[classes[label]] += 1
            total_pred[classes[label]] += 1


# 打印每个类别的正确率
for classname, correct_count in correct_pred.items():
    accuracy = 100 * float(correct_count) / total_pred[classname]
    print(f'类别正确率: {classname:5s} 是 {accuracy:.1f} %')

Out:

Accuracy for class: plane is 59.1 %
Accuracy for class: car   is 67.9 %
Accuracy for class: bird  is 23.2 %
Accuracy for class: cat   is 39.4 %
Accuracy for class: deer  is 54.4 %
Accuracy for class: dog   is 45.5 %
Accuracy for class: frog  is 54.0 %
Accuracy for class: horse is 68.9 %
Accuracy for class: ship  is 79.0 %
Accuracy for class: truck is 57.0 %

后面,我准备用一些示例作为练习,熟悉torch的使用。感谢大家的关注!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值