PyTorch实现图像分类卷积神经网络(CNN)模型

本文中我们将探讨如何使用PyTorch构建简单的卷积神经网络(CNN)来对图像进行分类。

通过阅读完本文,大家将熟悉PyTorch、卷积神经网络、填充(padding)、步长(stride)、最大池化(max pooling)等概念,并能够自己构建CNN模型进行图像分类。

本文使用的数据集是Kaggle上的Intel图像分类数据集。

https://www.kaggle.com/datasets/puneet6060/intel-image-classification

本文目录:

图片.png

数据集准备

为了训练模型,我们需要一个包含图像及其标签的数据集,但一般来说,用于图像分类的数据集是由存储在相应文件夹中的图像组成的。

例如,我们的数据集包含6种类型的图像,它们分别存储在对应的文件夹中。

图片

PyTorch提供了ImageFolder类,可以轻松地根据这种结构准备数据集。

我们只需将数据的目录传递给它,它即可提供可用于训练模型的数据集。

import torch  # 导入PyTorch库import torchvision  # 导入PyTorch的vision库,用于处理图像数据from torchvision import transforms  # 从vision库中导入transforms模块,用于图像预处理from torchvision.datasets import ImageFolder  # 从vision库的datasets中导入ImageFolder类,用于加载文件夹中的图像数据 # 训练数据和测试数据的目录data_dir = "../input/intel-image-classification/seg_train/seg_train/"  # 训练数据集的路径test_data_dir = "../input/intel-image-classification/seg_test/seg_test"  # 测试数据集的路径 # 加载训练数据dataset = ImageFolder(data_dir, transform=transforms.Compose([  # 使用ImageFolder类加载训练数据,并应用一系列预处理操作    transforms.Resize((150,150)),  # 将图像大小调整为150x150    transforms.ToTensor()  # 将图像数据转换为PyTorch的张量格式])) # 加载测试数据test_dataset = ImageFolder(test_data_dir, transform=transforms.Compose([  # 使用ImageFolder类加载测试数据,并应用一系列预处理操作    transforms.Resize((150,150)),  # 将图像大小调整为150x150    transforms.ToTensor()  # 将图像数据转换为PyTorch的张量格式]))

torchvision.transforms模块提供了各种预处理图像的功能,首先,我们将图像调整为(150*150)的形状,然后将其转换为张量。

# 从数据集中取出第一个样本,其中img代表图像数据,label代表图像的标签img, label = dataset[0]
# 打印出图像数据的形状和标签# img.shape 的输出 torch.Size([3, 150, 150]) 表示图像是一个三维张量,# 其中3代表图像的通道数(RGB三个颜色通道),150x150代表图像的宽度和高度# label 的输出 0 表示这个图像对应的标签是0,通常用于分类任务中标识类别print(img.shape, label)

因此,数据集中的第一张图像的形状为(3,150,150),表示图像有3个通道(RGB)、高度为150、宽度为150,该图像的标签为0,代表“建筑物”类别。

图像的标签根据data.classes中的类别索引进行设置。

# 打印出数据集中存在的类别print("存在的类别有: \n", dataset.classes)
# 输出示例(基于提供的输出内容):# 存在的类别有: # ['buildings', 'forest', 'glacier', 'mountain', 'sea', 'street']
# dataset.classes 通常是在处理图像分类任务时,数据集对象(如torchvision中的ImageFolder)# 自动根据文件夹名称或其他方式设置的类别列表。这里的类别列表包含了六个类别:# 'buildings'(建筑物)、'forest'(森林)、'glacier'(冰川)、'mountain'(山)、'sea'(海)、'street'(街道)。

因此,我们的数据集中包含6种类型的图像。

图像探索

我们的数据集以张量的形式包含图像,可以使用matplotlib Python库的imshow()方法来可视化图像。

def display_img(img, label):
    # 打印出图像的标签,这里通过dataset.classes列表和label索引来获取具体的类别名称
    print(f"标签: {dataset.classes[label]}")
    
    # 使用matplotlib的imshow函数显示图像
    # 由于img是一个三维张量,形状为[通道数, 高度, 宽度],而imshow需要[高度, 宽度, 通道数]的形状
    # 因此,我们需要使用permute方法重新排列张量的维度
    # 注意:对于RGB图像,通道顺序应该是'RGB',即红色、绿色、蓝色
    plt.imshow(img.permute(1, 2, 0))
    


# 显示数据集中的第一张图像
# dataset[0]会返回一个元组,包含图像数据和标签
# 我们使用*操作符来解包这个元组,将图像数据和标签分别传递给display_img函数的参数
display_img(*dataset[0])
 

permute方法将图像的形状从(3,150,150)重塑为(150,150,3),可以在下方看到第一张训练数据图像是建筑物:

图片

划分数据与准备批次

由于内存大小固定,训练数据很可能超过CPU或GPU的内存容量,因此我们不能将整个数据集一次性传入模型进行训练。

因此,我们将数据集拆分为多个批次,而不是在单个阶段对整个数据集进行训练,批次大小可以根据内存容量决定,通常取2的幂次方。

例如,批次大小可以为16、32、64、128、256等。

我们取批次大小为128,从数据中取2000张图像用于验证,其余数据用于训练,为了将图像随机拆分为训练集和测试集,PyTorch提供了random_split()方法。

我们使用PyTorch的DataLoader类将数据拆分为批次,通过向DataLoader类提供训练数据和批次大小参数,分别为训练和验证数据创建train_dl和val_dl两个对象。

# 导入必要的库from torch.utils.data.dataloader import DataLoader  # 导入DataLoader类,用于加载数据集from torch.utils.data import random_split  # 导入random_split函数,用于随机划分数据集
# 设置批处理大小batch_size = 128  # 每个批次中的样本数val_size = 2000  # 验证集中的样本数# 计算训练集大小,即总样本数减去验证集样本数train_size = len(dataset) - val_size  
# 使用random_split函数随机划分数据集为训练集和验证集train_data, val_data = random_split(dataset, [train_size, val_size])# 打印训练集和验证集的长度(即样本数)print(f"训练集长度: {len(train_data)}")print(f"验证集长度: {len(val_data)}")
# 输出示例(基于提供的输出内容):# 训练集长度: 12034# 验证集长度: 2000
# 使用DataLoader加载训练集和验证集,并将它们分批处理# 对于训练集,我们设置了shuffle=True以在每个epoch开始时打乱数据,num_workers=4指定了加载数据时使用的进程数,# pin_memory=True在支持CUDA的设备上可以提高数据从CPU到GPU的传输速度。train_dl = DataLoader(train_data, batch_size=batch_size, shuffle=True, num_workers=4, pin_memory=True)# 对于验证集,我们没有设置shuffle=True,因为验证集通常不需要打乱,同时我们设置了更大的batch_size(训练集batch_size的两倍),# 这取决于具体的需求和资源情况。num_workers和pin_memory的设置与训练集相同。val_dl = DataLoader(val_data, batch_size=batch_size*2, num_workers=4, pin_memory=True)

 

图像可视化

为了可视化单个批次中的图像,可以使用torchvision工具包中的make_grid()方法。它以图像网格的形式为我们提供了批次中图像的整体视图。

# 导入必要的库from torchvision.utils import make_grid  # 导入make_grid函数,用于将多个图像拼接成一个网格import matplotlib.pyplot as plt  # 导入matplotlib的pyplot模块,用于绘图 # 定义一个函数,用于显示数据加载器中的一个批次图像def show_batch(dl):    """    显示单个批次中的图像网格        参数:    dl (DataLoader): 包含图像数据的数据加载器    """    # 遍历数据加载器(实际上这里只需要迭代一次,因为我们在循环内部使用了break语句)    for images, labels in dl:        # 创建一个图形和坐标轴对象,设置图形的大小        fig, ax = plt.subplots(figsize=(16, 12))                # 隐藏坐标轴的刻度标记        ax.set_xticks([])        ax.set_yticks([])                # 使用make_grid函数将图像拼接成网格,并调整张量的维度以匹配imshow函数的输入要求        # nrow=16指定了每行显示的图像数,permute(1,2,0)用于调整通道顺序(从CHW到HWC)        grid_images = make_grid(images, nrow=16).permute(1, 2, 0) if images.ndim == 4 else make_grid(images, nrow=16)        ax.imshow(grid_images)                # 由于我们只想显示一个批次的图像,所以在第一次迭代后就退出循环        break # 调用函数显示训练数据加载器中的一个批次图像show_batch(train_dl) 

图片

图像分类基础模型

我们先准备一个基类,该类扩展了torch.nn.Module(用于开发所有神经网络的基础类)的功能。

我们向基础类中添加各种功能来训练模型、验证模型,并在每个训练周期结束时获取结果。

该代码是可复用的,可用于任何图像分类模型,无需每次都重写。

import torch.nn as nn
import torch.nn.functional as F
 
class ImageClassificationBase(nn.Module):
    
    def training_step(self, batch):
        # 从批次数据中提取图像和标签
        images, labels = batch 
        # 通过模型生成预测结果
        out = self(images)                  
        # 计算交叉熵损失
        loss = F.cross_entropy(out, labels) 
        # 返回损失值
        return loss
    
    def validation_step(self, batch):
        # 从批次数据中提取图像和标签
        images, labels = batch 
        # 通过模型生成预测结果
        out = self(images)                    
        # 计算交叉熵损失
        loss = F.cross_entropy(out, labels)   
        # 计算准确率
        acc = accuracy(out, labels)           
        # 返回一个包含损失和准确率的字典
        return {'val_loss': loss.detach(), 'val_acc': acc}
        
    def validation_epoch_end(self, outputs):
        # 从每个批次的结果中提取损失值
        batch_losses = [x['val_loss'] for x in outputs]
        # 计算整个验证集的平均损失
        epoch_loss = torch.stack(batch_losses).mean()   
        # 从每个批次的结果中提取准确率
        batch_accs = [x['val_acc'] for x in outputs]
        # 计算整个验证集的平均准确率
        epoch_acc = torch.stack(batch_accs).mean()      
        # 返回一个包含平均损失和平均准确率的字典
        return {'val_loss': epoch_loss.item(), 'val_acc': epoch_acc.item()}
    
    def epoch_end(self, epoch, result):
        # 打印当前训练周期(epoch)的信息,包括训练损失、验证损失和验证准确率
        print("Epoch [{}], train_loss: {:.4f}, val_loss: {:.4f}, val_acc: {:.4f}".format(
            epoch, result['train_loss'], result['val_loss'], result['val_acc']))

 

卷积、填充、步长、最大池化

现在,让我们了解卷积、填充和最大池化的概念,这些概念有助于我们的神经网络从图像中学习特征。

卷积

根据维基百科,卷积是两个函数之间进行的数学运算,以生成显示一个函数的形状如何被另一个函数修改的第三个函数。

在这里,第一个函数是图像张量,第二个函数是与图像具有相同通道数的矩阵或张量(称为核)。

核被应用于图像以学习图像的特征,基本上,核会对图像的每个段执行点积,然后求和并输出张量。通过以下图像可以更容易地理解:

图片

填充

在卷积中对图像张量应用核时,会减小输出张量的尺寸,这会导致两个问题:

  • 输出会缩小
  • 图像角落的像素会失去重要性

为了解决这些问题,我们通过向图像张量的边界添加一些额外的像素来增加图像的形状。

这有助于增加图像的大小,并将图像边界的像素值移到张量内部,从它们中学习到的特征会传递到深度神经网络中的进一步层。

在下图中,向二维张量添加了零填充。

图片

步长

步长控制核的活动,即核如何在图像上移动,例如,如果步长设置为(1,1),则核每次在宽度和高度上移动1个像素。

第一个核在宽度上每次移动1个像素,在完成宽度上的操作后,在高度上移动1个像素,然后重复该过程。

如果步长设置为(2,2),则核在图像张量上每次移动两个像素。

池化

池化层有助于总结卷积层(也称为特征图)获得的结果,并将其降低维度。

有各种类型的池化,如最大池化、平均池化等,最大池化常用,下图对其进行了更精确的描述:

图片

用于分类的CNN模型

在了解这些概念之后,我们现在定义CNN模型,该模型包含这些概念以从图像中学习特征并训练模型。

该模型包含3个CNN块,每个块由2个卷积层和1个最大池化层组成。

使用ReLU激活函数从特征图中删除负值,因为像素值不能为负,步长(1,1)和填充也设置为1。

在对图像应用卷积并提取特征后,使用flatten层将具有3个维度的张量展平,flatten层将张量转换为一维,然后添加3个全连接层以减小张量的大小并学习特征。

CNN模型架构

import torch.nn as nn
class NaturalSceneClassification(nn.Module):    def __init__(self):        super().__init__()  # 调用父类的构造函数        # 定义一个顺序模型,包含多个卷积层、激活函数、池化层和全连接层        self.network = nn.Sequential(            # 第一个卷积层,输入通道数为3(RGB),输出通道数为32,卷积核大小为3x3,边缘填充为1            nn.Conv2d(3, 32, kernel_size=3, padding=1),            nn.ReLU(),  # 激活函数            # 第二个卷积层,输入通道数为32,输出通道数为64,其余参数同上            nn.Conv2d(32, 64, kernel_size=3, stride=1, padding=1),            nn.ReLU(),  # 激活函数            # 第一个池化层,使用最大池化,池化窗口大小为2x2            nn.MaxPool2d(2, 2),                        # 第三个和第四个卷积层,输入通道数为64,输出通道数为128,其余参数同上            nn.Conv2d(64, 128, kernel_size=3, stride=1, padding=1),            nn.ReLU(),  # 激活函数            nn.Conv2d(128, 128, kernel_size=3, stride=1, padding=1),            nn.ReLU(),  # 激活函数            # 第二个池化层,参数同上            nn.MaxPool2d(2, 2),                        # 第五个和第六个卷积层,输入通道数为128,输出通道数为256,其余参数同上            nn.Conv2d(128, 256, kernel_size=3, stride=1, padding=1),            nn.ReLU(),  # 激活函数            nn.Conv2d(256, 256, kernel_size=3, stride=1, padding=1),            nn.ReLU(),  # 激活函数            # 第三个池化层,参数同上            nn.MaxPool2d(2, 2),                        # 展平层,将多维的卷积特征图展平为一维向量            nn.Flatten(),            # 第一个全连接层,输入特征数为展平后的特征数量(这里假设为82944,但实际可能因输入图像大小而异),输出特征数为1024            nn.Linear(82944, 1024),  # 注意:82944这个数值需要根据实际输入图像大小和前面的网络层计算得出            nn.ReLU(),  # 激活函数            # 第二个全连接层,输入特征数为1024,输出特征数为512            nn.Linear(1024, 512),            nn.ReLU(),  # 激活函数            # 第三个全连接层(输出层),输入特征数为512,输出特征数为6(假设有6个分类)            nn.Linear(512, 6)        )        def forward(self, xb):        # 定义前向传播路径,输入数据xb通过定义好的网络进行前向传播        return self.network(xb)

超参数、模型训练与评估

现在,我们需要在训练数据集上训练自然场景分类模型,因此,我们首先定义拟合、评估和准确性方法。

import torch
# 计算准确率函数def accuracy(outputs, labels):    # 使用torch.max获取outputs中最大值所在的索引,即预测值preds    _, preds = torch.max(outputs, dim=1)    # 计算预测正确的数量,并除以总数量得到准确率,然后转换为tensor类型返回    return torch.tensor(torch.sum(preds == labels).item() / len(preds))
# 模型评估函数,使用@torch.no_grad()装饰器表示在评估过程中不计算梯度@torch.no_grad()def evaluate(model, val_loader):    # 将模型设置为评估模式    model.eval()    # 对验证集中的每个batch进行预测,并将结果存储在outputs列表中    outputs = [model.validation_step(batch) for batch in val_loader]    # 调用模型的validation_epoch_end方法计算并返回评估结果    return model.validation_epoch_end(outputs)
# 模型训练函数def fit(epochs, lr, model, train_loader, val_loader, opt_func=torch.optim.SGD):    # 存储训练历史的列表    history = []    # 创建优化器,默认使用SGD优化器    optimizer = opt_func(model.parameters(), lr)    # 进行多个epoch的训练    for epoch in range(epochs):        # 将模型设置为训练模式        model.train()        # 存储每个batch的训练损失的列表        train_losses = []        # 对训练集中的每个batch进行训练        for batch in train_loader:            # 调用模型的training_step方法进行前向传播、计算损失等            loss = model.training_step(batch)            # 将损失添加到train_losses列表中            train_losses.append(loss)            # 反向传播计算梯度            loss.backward()            # 更新模型参数            optimizer.step()            # 清零梯度,为下一个batch做准备            optimizer.zero_grad()        # 在一个epoch结束后,对模型进行评估        result = evaluate(model, val_loader)        # 计算并添加训练损失到结果字典中        result['train_loss'] = torch.stack(train_losses).mean().item()        # 调用模型的epoch_end方法,可能用于记录日志、保存模型等        model.epoch_end(epoch, result)        # 将当前epoch的结果添加到历史记录中        history.append(result)    # 训练结束后,返回历史记录    return history

现在,我们针对不同的超参数训练模型,以获得模型的最佳拟合,在这里训练了30个训练周期的模型,学习率为0.001,测试数据的准确率为80%。

num_epochs = 30opt_func = torch.optim.Adamlr = 0.001# 在训练数据上拟合模型,并记录每个epoch后的结果history = fit(num_epochs, lr, model, train_dl, val_dl, opt_func)

每个训练周期的结果:

Epoch [0], train_loss: 1.2263, val_loss: 1.1289, val_acc: 0.5676Epoch [1], train_loss: 0.8951, val_loss: 0.8003, val_acc: 0.6766Epoch [2], train_loss: 0.7444, val_loss: 0.6793, val_acc: 0.7462Epoch [3], train_loss: 0.5978, val_loss: 0.7027, val_acc: 0.7670Epoch [4], train_loss: 0.5132, val_loss: 0.5253, val_acc: 0.8123Epoch [5], train_loss: 0.4110, val_loss: 0.5356, val_acc: 0.8197Epoch [6], train_loss: 0.3137, val_loss: 0.6205, val_acc: 0.8035Epoch [7], train_loss: 0.2375, val_loss: 0.6337, val_acc: 0.8153Epoch [8], train_loss: 0.1705, val_loss: 0.7263, val_acc: 0.8119Epoch [9], train_loss: 0.1467, val_loss: 0.7694, val_acc: 0.8032Epoch [10], train_loss: 0.1044, val_loss: 0.9002, val_acc: 0.8026Epoch [11], train_loss: 0.1071, val_loss: 0.9872, val_acc: 0.8033Epoch [12], train_loss: 0.0509, val_loss: 1.0953, val_acc: 0.8236Epoch [13], train_loss: 0.0264, val_loss: 1.2699, val_acc: 0.8131Epoch [14], train_loss: 0.0281, val_loss: 1.1232, val_acc: 0.8079Epoch [15], train_loss: 0.0377, val_loss: 1.2976, val_acc: 0.8080Epoch [16], train_loss: 0.0347, val_loss: 1.2475, val_acc: 0.7905Epoch [17], train_loss: 0.0287, val_loss: 1.3546, val_acc: 0.8020Epoch [18], train_loss: 0.0385, val_loss: 1.2569, val_acc: 0.8256Epoch [19], train_loss: 0.0172, val_loss: 1.2729, val_acc: 0.8147Epoch [20], train_loss: 0.0447, val_loss: 8.4484, val_acc: 0.4511Epoch [21], train_loss: 0.7567, val_loss: 0.6766, val_acc: 0.7819Epoch [22], train_loss: 0.1603, val_loss: 0.9974, val_acc: 0.7862Epoch [23], train_loss: 0.0449, val_loss: 1.2573, val_acc: 0.7992Epoch [24], train_loss: 0.0174, val_loss: 1.3928, val_acc: 0.7953Epoch [25], train_loss: 0.0211, val_loss: 1.2213, val_acc: 0.8135Epoch [26], train_loss: 0.0057, val_loss: 1.5535, val_acc: 0.8034Epoch [27], train_loss: 0.0038, val_loss: 1.5412, val_acc: 0.7913Epoch [28], train_loss: 0.0057, val_loss: 1.5192, val_acc: 0.8018Epoch [29], train_loss: 0.0033, val_loss: 1.4707, val_acc: 0.8103
绘制准确率和损失值的图表,以便直观地展示模型在每个训练周期后如何提升其准确率:

import matplotlib.pyplot as plt  # 导入matplotlib的pyplot模块用于绘图
def plot_accuracies(history):    """    绘制准确率历史记录图    :param history: 包含每个epoch结果的列表,每个结果是一个字典,包含'val_acc'等键    """    # 从history列表中提取每个epoch的验证准确率    accuracies = [x['val_acc'] for x in history]    # 使用plt.plot绘制准确率随epoch变化的曲线,'-x'表示用线段连接数据点,并在数据点处标记'x'    plt.plot(accuracies, '-x')    # 设置x轴标签为'epoch'    plt.xlabel('epoch')    # 设置y轴标签为'accuracy'    plt.ylabel('accuracy')    # 设置图表标题    plt.title('Accuracy vs. No. of epochs')    # 显示图表    plt.show()  
# 调用plot_accuracies函数绘制准确率历史记录图plot_accuracies(history)
def plot_losses(history):    """    绘制每个epoch的损失图    :param history: 包含每个epoch结果的列表,每个结果是一个字典,包含'train_loss'和'val_loss'等键    """    # 从history列表中提取每个epoch的训练损失,使用get方法防止键不存在时出错,返回None(但这里我们期望它总是存在)    train_losses = [x.get('train_loss') for x in history]    # 从history列表中提取每个epoch的验证损失    val_losses = [x['val_loss'] for x in history]    # 使用plt.plot绘制训练损失随epoch变化的曲线,'-bx'表示用蓝色线段连接数据点,并在数据点处标记'x'    plt.plot(train_losses, '-bx')    # 使用plt.plot绘制验证损失随epoch变化的曲线,'-rx'表示用红色线段连接数据点,并在数据点处标记'x'    plt.plot(val_losses, '-rx')    # 设置x轴标签为'epoch'    plt.xlabel('epoch')    # 设置y轴标签为'loss'    plt.ylabel('loss')    # 设置图例,区分训练损失和验证损失    plt.legend(['Training', 'Validation'])    # 设置图表标题    plt.title('Loss vs. No. of epochs')    # 显示图表    plt.show()  # 调用plot_losses函数绘制损失历史记录图plot_losses(history)

图片

图片

来源:人工智能学习指南

THE END