我们已经学习了如何构建卷积神经网络,并且在 Fashion-MNIST 数据集上执行图像分类。在本节中,我们使用更复杂数据集训练卷积神经网络,对包含猫或狗的图像进行分类。同时,我们还将了解当用于训练的图像数量变化时,模型的准确率如何变化。
猫狗分类是一个常见的计算机视觉任务,将图片中的猫和狗进行分类。猫狗分类数据集是常用的计算机视觉任务数据集,用于训练和评估机器学习模型对猫和狗图像进行分类,该数据集包含大量的猫和狗的图片,每张图片都有相应的标签,表示其所属的类别(猫或狗)。
本节中,我们将使用 Kaggle
提供的猫狗分类数据集,Kaggle
猫狗分类数据集是公开的猫狗分类竞赛数据集,由 Kaggle
主办,该数据集包含了 25,000
张训练图像和 12,500
张测试图像,其中图像数量基本平衡,即一半是猫的图像,一半是狗的图像,在 Kaggle
官方网站下载猫狗数据集。在下一节中,将使用 PyTorch
基于卷积神经网络 (Convolutional Neural Networks
, CNN
) 综合利用所学习的优化技术实现猫狗分类。
(1) 导入所需使用的库:
import torch.nn as nn
import torch
import matplotlib.pyplot as plt
import matplotlib.ticker as mticker
import numpy as np
device = 'cuda' if torch.cuda.is_available() else 'cpu'
import cv2
from glob import glob
(2) 解压已经下载完成的猫狗数据集,定义训练和测试数据集文件夹:
train_data_dir = 'archive/training_set/training_set'
test_data_dir = 'archive/test_set/test_set'
(3) 定义数据集类,从指定文件夹中获取数据。然后,根据图像对应的目录,将“狗”图像标签设为 1
,将“猫”图像标签设为 0
。将图像缩放到 0
到 1 之间,并置换输入数据维度,将通道置于图像的高度和宽度之前。
定义 __init__
方法,将文件夹作为输入,并存储猫和狗文件夹中的图像对应的文件路径:
from torch.utils.data import DataLoader, Dataset
class cats_dogs(Dataset):
def __init__(self, folder):
cats = glob(folder+'/cats/*.jpg')
dogs = glob(folder+'/dogs/*.jpg')
self.fpaths = cats + dogs
打乱文件路径顺序并根据这些文件路径对应的文件夹创建目标变量(图像标签):
from random import shuffle
shuffle(self.fpaths)
self.targets = [fpath.split('/')[-1].startswith('dog') for fpath in self.fpaths] # dog=1 & cat=0
定义 __len__
方法:
def __len__(self):
return len(self.fpaths)
定义 __getitem__
方法,从文件路径列表中选择一个随机文件路径,读取图像,并调整图像的大小为 224 x 224
,同时置换输入图像维度顺序:
def __getitem__(self, ix):
f = self.fpaths[ix]
target = self.targets[ix]
im = (cv2.imread(f)[:,:,::-1])
im = cv2.resize(im, (224,224))
return torch.tensor(im/255).permute(2,0,1).to(device).float(), torch.tensor([target]).float().to(device)
(4) 采样并绘制随机图像。
我们需要置换采样的图像维度顺序,因为 matplotlib
期望图像的通道维度在图像的高度和宽度维度之后:
data = cats_dogs(train_data_dir)
im, label = data[200]
print(len(data))
# 8000
plt.imshow(im.permute(1,2,0).cpu())
print(label)
# 1
plt.show()
首先,定义 conv_layer()
函数,按顺序执行卷积、ReLU
激活、批归一化和最大池化,该函数将作为模型的构建块将会多次使用:
def conv_layer(ni,no,kernel_size,stride=1):
return nn.Sequential(
nn.Conv2d(ni, no, kernel_size, stride),
nn.ReLU(),
nn.BatchNorm2d(no),
nn.MaxPool2d(2)
)
在以上代码中,我们将输入通道的数量 (ni
)、输出通道的数量 (no
)、滤波器尺寸 kernel_size
和步幅 stride
作为 conv_layer()
函数的输入。
定义 get_model()
函数,该函数通过调用 conv_layer
方法执行多个卷积和池化操作,展平特征输出,并在连接到隐藏层后再连接到输出层:
def get_model():
model = nn.Sequential(
conv_layer(3, 64, 3),
conv_layer(64, 512, 3),
conv_layer(512, 512, 3),
conv_layer(512, 512, 3),
conv_layer(512, 512, 3),
conv_layer(512, 512, 3),
nn.Flatten(),
nn.Linear(512, 1),
nn.Sigmoid(),
).to(device)
loss_fn = nn.BCELoss()
optimizer = torch.optim.Adam(model.parameters(), lr= 1e-3)
return model, loss_fn, optimizer
可以根据需要将一个 nn.Sequential
对象链接到另一 nn.Sequential
对象,以构建深度神经网络。在以上代码中,可以向使用其它 nn.Module
层一样调用 conv_layer
。
调用 get_model()
函数来获取模型、损失函数 (loss_fn
) 和优化器,然后使用从 torchsummary
库中导入的 summary
方法打印模型摘要信息:
from torchsummary import summary
model, loss_fn, optimizer = get_model()
print(summary(model, input_size=(3, 224, 224)))
"""
----------------------------------------------------------------
Layer (type) Output Shape Param #
================================================================
Conv2d-1 [-1, 64, 222, 222] 1,792
ReLU-2 [-1, 64, 222, 222] 0
BatchNorm2d-3 [-1, 64, 222, 222] 128
MaxPool2d-4 [-1, 64, 111, 111] 0
Conv2d-5 [-1, 512, 109, 109] 295,424
ReLU-6 [-1, 512, 109, 109] 0
BatchNorm2d-7 [-1, 512, 109, 109] 1,024
MaxPool2d-8 [-1, 512, 54, 54] 0
Conv2d-9 [-1, 512, 52, 52] 2,359,808
ReLU-10 [-1, 512, 52, 52] 0
BatchNorm2d-11 [-1, 512, 52, 52] 1,024
MaxPool2d-12 [-1, 512, 26, 26] 0
Conv2d-13 [-1, 512, 24, 24] 2,359,808
ReLU-14 [-1, 512, 24, 24] 0
BatchNorm2d-15 [-1, 512, 24, 24] 1,024
MaxPool2d-16 [-1, 512, 12, 12] 0
Conv2d-17 [-1, 512, 10, 10] 2,359,808
ReLU-18 [-1, 512, 10, 10] 0
BatchNorm2d-19 [-1, 512, 10, 10] 1,024
MaxPool2d-20 [-1, 512, 5, 5] 0
Conv2d-21 [-1, 512, 3, 3] 2,359,808
ReLU-22 [-1, 512, 3, 3] 0
BatchNorm2d-23 [-1, 512, 3, 3] 1,024
MaxPool2d-24 [-1, 512, 1, 1] 0
Flatten-25 [-1, 512] 0
Linear-26 [-1, 1] 513
Sigmoid-27 [-1, 1] 0
================================================================
Total params: 9,742,209
Trainable params: 9,742,209
Non-trainable params: 0
----------------------------------------------------------------
Input size (MB): 0.57
Forward/backward pass size (MB): 271.85
Params size (MB): 37.16
Estimated Total Size (MB): 309.59
----------------------------------------------------------------
"""
(6) 创建 get_data()
函数,该函数实例化 cats_dogs
类,并使用训练和验证文件夹创建 DataLoader
:
def get_data():
train = cats_dogs(train_data_dir)
trn_dl = DataLoader(train, batch_size=16, shuffle=True, drop_last = True)
val = cats_dogs(test_data_dir)
val_dl = DataLoader(val, batch_size=16, shuffle=True, drop_last = True)
return trn_dl, val_dl
在以上代码中,通过使用 drop_last = True
忽略最后一批数据,这是是因为最后一批数据的数量可能与其他批数量不同。
(7) 定义模型训练函数:
def train_batch(x, y, model, optimizer, loss_fn):
prediction = model(x)
batch_loss = loss_fn(prediction, y)
batch_loss.backward()
optimizer.step()
optimizer.zero_grad()
return batch_loss.item()
(8) 定义计算准确率和验证损失的函数:
def accuracy(x, y, model):
with torch.no_grad():
prediction = model(x)
is_correct = (prediction > 0.5) == y
return is_correct.cpu().numpy().tolist()
@torch.no_grad()
def val_loss(x, y, model, loss_fn):
prediction = model(x)
val_loss = loss_fn(prediction, y)
return val_loss.item()
准确率计算的代码与 Fashion-MNIST
分类中的代码有所不同,因为当前模型(猫狗分类)是二分类模型,而 Fashion-MNIST
分类模型是多分类模型。
(9) 训练模型,并在每个 epoch
结束时检查测试数据的准确率:
trn_dl, val_dl = get_data()
model, loss_fn, optimizer = get_model()
train_losses, train_accuracies = [], []
val_losses, val_accuracies = [], []
for epoch in range(10):
# print(epoch)
train_epoch_losses, train_epoch_accuracies = [], []
val_epoch_accuracies = []
for ix, batch in enumerate(iter(trn_dl)):
#print(ix)
x, y = batch
batch_loss = train_batch(x, y, model, optimizer, loss_fn)
train_epoch_losses.append(batch_loss)
train_epoch_loss = np.array(train_epoch_losses).mean()
for ix, batch in enumerate(iter(trn_dl)):
x, y = batch
is_correct = accuracy(x, y, model)
train_epoch_accuracies.extend(is_correct)
train_epoch_accuracy = np.mean(train_epoch_accuracies)
for ix, batch in enumerate(iter(val_dl)):
x, y = batch
val_is_correct = accuracy(x, y, model)
val_epoch_accuracies.extend(val_is_correct)
#validation_loss = val_loss(x, y, model)
val_epoch_accuracy = np.mean(val_epoch_accuracies)
train_losses.append(train_epoch_loss)
train_accuracies.append(train_epoch_accuracy)
#val_losses.append(validation_loss)
val_accuracies.append(val_epoch_accuracy)
(10) 绘制训练和验证准确率随时间的变化情况:
epochs = np.arange(10)+1
plt.plot(epochs, train_accuracies, 'bo', label='Training accuracy')
plt.plot(epochs, val_accuracies, 'r', label='Validation accuracy')
plt.gca().xaxis.set_major_locator(mticker.MultipleLocator(1))
plt.title('Training and validation accuracy with 4K data points used for training')
plt.xlabel('Epochs')
plt.ylabel('Accuracy')
#plt.ylim(0.8,1)
plt.gca().set_yticklabels(['{:.0f}%'.format(x*100) for x in plt.gca().get_yticks()])
plt.legend()
plt.grid('off')
plt.show()
训练结束时的分类准确率约为 90%
,训练此模型使用了大约 8,000
个数据样本,其中 4,000
个样本属于 cat
,其余样本属于 dog
。在下一节中,我们将了解减少训练数据对模型性能影响。
一般来说,模型使用的训练样本越多,分类准确率就越高。在本节中,通过减少训练图像的数量,观察对测试数据集进行分类时的准确率,来了解使用不同数量的图像训练模型对准确率的影响。
接下来,我们在训练数据集中只使用每个类别的 500
个数据样本,通过在 __init__
方法中将文件数量限制为每个文件夹中的前 500
个图像路径实现:
def __init__(self, folder):
cats = glob(folder+'/cats/*.jpg')
dogs = glob(folder+'/dogs/*.jpg')
self.fpaths = cats[:500] + dogs[:500]
from random import shuffle
shuffle(self.fpaths)
self.targets = [fpath.split('/')[-1].startswith('dog') for fpath in self.fpaths] # dog=1 & cat=0
在以上代码中,与上一小节中初始化代码唯一的区别在于 self.paths
,仅考虑每个文件夹中前 500
个文件路径。
使用测试数据集测试基于 1,000
张图像(每个类别 500
张)构建的模型的准确率:
class cats_dogs(Dataset):
def __init__(self, folder):
cats = glob(folder+'/cats/*.jpg')
dogs = glob(folder+'/dogs/*.jpg')
self.fpaths = cats[:500] + dogs[:500]
from random import shuffle
shuffle(self.fpaths)
self.targets = [fpath.split('/')[-1].startswith('dog') for fpath in self.fpaths] # dog=1 & cat=0
def __len__(self):
return len(self.fpaths)
def __getitem__(self, ix):
f = self.fpaths[ix]
target = self.targets[ix]
im = (cv2.imread(f)[:,:,::-1])
im = cv2.resize(im, (224,224))
return torch.tensor(im/255).permute(2,0,1).to(device).float(), torch.tensor([target]).float().to(device)
可以看到,由于在训练集中的图像样本较少,因此模型在测试数据集的准确率大幅降低,低至约 70%
。
通过改变用于训练模型的训练样本的数量查看训练数据的数量对模型准确率的影响。改变用于训练模型的图像数量,并绘制使用不同数据样本的模型在训练过程中的模型准确率变化:
如上图所示,可用的训练数据越多,模型在测试数据上的准确率就越高。但是,在实现场景中,我们通常可能没有足够多的训练数据,此时便可以使用数据增强来增加训练样本数量,此外,在之后的学习中也将介绍迁移学习,来解决训练样本不足的问题。
PyTorch深度学习实战(1)——神经网络与模型训练过程详解
PyTorch深度学习实战(2)——PyTorch基础
PyTorch深度学习实战(3)——使用PyTorch构建神经网络
PyTorch深度学习实战(4)——常用激活函数和损失函数详解
PyTorch深度学习实战(5)——计算机视觉基础
PyTorch深度学习实战(6)——神经网络性能优化技术
PyTorch深度学习实战(7)——批大小对神经网络训练的影响
PyTorch深度学习实战(8)——批归一化
PyTorch深度学习实战(9)——学习率优化
PyTorch深度学习实战(10)——过拟合及其解决方法
PyTorch深度学习实战(11)——卷积神经网络
PyTorch深度学习实战(12)——数据增强
PyTorch深度学习实战(13)——可视化神经网络中间层输出