卷积神经网络(LeNet)识别Fashion-MNIST数据集(Pytorch版)_fashionmnist lenet-程序员宅基地

技术标签: cnn  图像分类  深度学习  pytorch  人工智能  神经网络  

1. 前言

1.1 案例介绍

本案例使用Pytorch搭建一个类似LeNet-5的网络结构,用于Fashion-MNIST数据集的图像分类。针对该问题的分析可以分为数据准备、模型建立以及使用训练集进行训练和使用测试集测试模型的效果。

1.2 环境配置

操作系统: Windows10
编译器环境: PyCharm Community Edition 2021.2
配置环境: Pytorch1.8 + torchvision9.0 + CUDA11.3

1.3 模块导入

本案例需要导入如下的库文件和相关模块:

import numpy as np
import pandas as pd
from sklearn.metrics import accuracy_score, confusion_matrix, classification_report
import matplotlib.pyplot as plt
import seaborn as sns
import copy
import time
import torch
import torch.nn as nn
from torch.optim import Adam
import torch.utils.data as Data
from torchvision import transforms
from torchvision.datasets import FashionMNIST

2. 图像数据准备

在模型建立与训练之前,首先准备FashionMNIST数据集,该数据集可以直接使用torchvision库中datasets模块的FashionMNIST()函数读取,如果指定的工作文件夹中没有当前数据,可以从网络上自动下载该数据。

2.1 训练验证集的准备

训练验证集的加载处理程序被包装成如下的train_data_process()函数 ,它的作用是导入训练数据集,然后使用Data.DataLoader()函数将其定义为数据加载器,每个batch中会包含64个样本,通过len()函数可以计算数据加载器中包含的batch数量,输出显示train_loader中包含938个batch。需要注意的是参数shuffle = False,表示加载器中每个batch使用的样本都是固定的,这样有利于在训练模型时根据迭代的次数将其分为训练集和验证集。同时为了观察数据集中每个图像的内容,可以获取一个batch的图像,然后将其可视化,以观察数据。

# 处理训练集数据
def train_data_process():
    # 加载FashionMNIST数据集
    train_data = FashionMNIST(root="./data/FashionMNIST",  # 数据路径
                              train=True,  # 只使用训练数据集
                              transform=transforms.ToTensor(),  # 把PIL.Image或者numpy.array数据类型转变为torch.FloatTensor类型
                                                                # 尺寸为Channel * Height * Width,数值范围缩小为[0.0, 1.0]
                              download=False,  # 若本身没有下载相应的数据集,则选择True
                              )
    train_loader = Data.DataLoader(dataset=train_data,  # 传入的数据集
                                   batch_size=64,  # 每个Batch中含有的样本数量
                                   shuffle=False,  # 不对数据集重新排序
                                   num_workers=2,  # 加载数据所开启的进程数量
                                   )
    print("The number of batch in train_loader:", len(train_loader))  # 一共有938个batch,每个batch含有64个训练样本

    # 获得一个Batch的数据
    for step, (b_x, b_y) in enumerate(train_loader):
        if step > 0:
            break
    batch_x = b_x.squeeze().numpy()  # 将四维张量移除第1维,并转换成Numpy数组
    batch_y = b_y.numpy()  # 将张量转换成Numpy数组
    class_label = train_data.classes  # 训练集的标签
    class_label[0] = "T-shirt"

    # 可视化一个Batch的图像
    plt.figure(figsize=(12, 5))
    for ii in np.arange(len(batch_y)):
        plt.subplot(4, 16, ii+1)
        plt.imshow(batch_x[ii, :, :], cmap=plt.cm.gray)
        plt.title(class_label[batch_y[ii]], size=9)
        plt.axis("off")
        plt.subplots_adjust(wspace=0.05)
    plt.show()

    return train_loader, class_label

得到的可视化图像如下:
在这里插入图片描述

2.2 测试集的准备

测试集的加载处理程序被包装成如下的test_data_process()函数 ,它的作用是导入测试数据集,将所有的样本处理为一个整体,看作一个batch用于测试。

# 处理测试集数据
def test_data_process():
    test_data = FashionMNIST(root="./data/FashionMNIST",  # 数据路径
                             train=False,  # 不使用训练数据集
                             download=False,  # 如果前面数据已经下载,这里不再需要重复下载
                             )
    test_data_x = test_data.data.type(torch.FloatTensor) / 255.0  # 将数值范围缩小为[0.0, 1.0]
    test_data_x = torch.unsqueeze(test_data_x, dim=1)  # 为测试数据test_data_x添加一个维度,即通道数
    test_data_y = test_data.targets  # 测试集的标签
    print("test_data_x.shape:", test_data_x.shape)
    print("test_data_y.shape:", test_data_y.shape)

    return test_data_x, test_data_y

得到的输出结果如下,即测试集有10000张28×28的图像。
在这里插入图片描述

3. 卷积神经网络的搭建

在数据准备完毕后,可以搭建一个卷积神经网络,并且使用训练数据对网络进行训练,使用测试集验证所搭建网络的识别精度。
搭建的卷积神经网络(如下图)有2个卷积层,分别包含16个和32个3×3的卷积核,并且卷积后使用ReLU激活函数进行激活,两个池化层均为平均池化,而两个全连接层分别有256和128个神经单元,最后的分类器则包含了10个神经元。
在这里插入图片描述

下面的程序代码定义了一个类ConvNet,在继承了nn.Module类的基础上对其结构和功能进行了定义。通过nn.Sequential()函数分别定义了一个由两个卷积层和三个全连接层组成的网络结构,并且在forward()函数中定义了数据在网络中的前向传播过程。

# 定义一个卷积神经网络
class ConvNet(nn.Module):
    def __init__(self):
        super(ConvNet, self).__init__()  # 对继承自父类Module的属性进行初始化

        # 定义第一个卷积层,16个3*3的卷积核,池化层为平均池化
        self.conv1 = nn.Sequential(nn.Conv2d(in_channels=1,  # 输入图像的通道数
                                             out_channels=16,  # 卷积核的数量
                                             kernel_size=3,  # 卷积核的大小
                                             stride=1,  # 步长
                                             padding=1,  # 填充的数量
                                             ),  # 经过卷积后的尺寸变化:(1*28*28) -> (16*28*28)
                                   nn.ReLU(),  # ReLU激活函数
                                   nn.AvgPool2d(kernel_size=2,  # 池化窗口的大小
                                                stride=2,  # 步长
                                                ),  # 经过池化后的尺寸变化:(16*28*28) -> (16*14*14)
                                   )
        # 定义第二个卷积层,32个3*3的卷积核,池化层为平均池化
        self.conv2 = nn.Sequential(nn.Conv2d(in_channels=16,  # 输入图像的通道数
                                             out_channels=32,  # 卷积核的数量
                                             kernel_size=3,  # 卷积核的大小
                                             stride=1,  # 步长
                                             padding=0,  # 填充的数量
                                             ),  # 经过卷积后的尺寸变化:(16*14*14) -> (32*12*12)
                                   nn.ReLU(),  # ReLU激活函数
                                   nn.AvgPool2d(kernel_size=2,  # 池化窗口的大小
                                                stride=2,  # 步长
                                                ),  # 经过池化后的尺寸变化:(32*12*12) -> (32*6*6)
                                   )
        # 定义全连接层
        self.classifier = nn.Sequential(nn.Linear(32*6*6, 256),  # 全连接层的输入为32*6*6=1152,输出为256
                                        nn.ReLU(),
                                        nn.Linear(256, 128),  # 全连接层的输入为256,输出为128
                                        nn.ReLU(),
                                        nn.Linear(128, 10)  # 全连接层的输入为128,输出为10
                                        )

    # 定义网络结构的前向传播路径
    def forward(self, x):
        x = self.conv1(x)  # 将数据集x输入给第一个卷积层
        x = self.conv2(x)  # 将第一个卷积层的输出给到第二个卷积层
        x = x.view(x.size(0), -1)  # 将第二个卷积层的输出展开成一维张量x
                                   # 四维张量x对应的维度为(batch_size,channels,x,y),其中x.size(0)对应batch_size
        output = self.classifier(x)  # 将展开的一维张量x给到全连接层和分类器

        return output

将所定义的网络结构打印出来如下:
在这里插入图片描述

4. 卷积神经网络训练与预测

为了训练网络结构ConvNet,定义了一个train_model()函数,该函数的作用是使用训练数据集来训练ConvNet。训练数据集包含了60000张图像,划分成938个batch,其中80%的batch用于模型的训练,20%的batch用于模型的验证,因此在train_model()函数中,包含了模型的训练和验证两个过程。

# 定义网络的训练过程
def train_model(model, traindataloader, train_rate, criterion, optimizer, num_epochs=25):
    '''
    :param model: 网络模型
    :param traindataloader: 训练数据集,会切分为训练集和验证集
    :param train_rate: 训练集batch_size的百分比
    :param criterion: 损失函数
    :param optimizer: 优化方法
    :param num_epochs: 训练的轮数
    '''

    batch_num = len(traindataloader)  # batch数量
    train_batch_num = round(batch_num * train_rate)  # 将80%的batch用于训练,round()函数四舍五入
    best_model_wts = copy.deepcopy(model.state_dict())  # 复制当前模型的参数
    # 初始化参数
    best_acc = 0.0  # 最高准确度
    train_loss_all = []  # 训练集损失函数列表
    train_acc_all = []  # 训练集准确度列表
    val_loss_all = []  # 验证集损失函数列表
    val_acc_all = []  # 验证集准确度列表
    since = time.time()  # 当前时间
    # 进行迭代训练模型
    for epoch in range(num_epochs):
        print('Epoch {}/{}'.format(epoch, num_epochs - 1))
        print('-' * 10)

        # 初始化参数
        train_loss = 0.0  # 训练集损失函数
        train_corrects = 0  # 训练集准确度
        train_num = 0  # 训练集样本数量
        val_loss = 0.0  # 验证集损失函数
        val_corrects = 0  # 验证集准确度
        val_num = 0  # 验证集样本数量
        # 对每一个mini-batch进行训练和计算
        for step, (b_x, b_y) in enumerate(traindataloader):
            if step < train_batch_num:  # 使用数据集的80%用于训练
                model.train()  # 设置模型为训练模式,启用Batch Normalization和Dropout
                output = model(b_x)  # 前向传播过程,输入为一个batch,输出为一个batch中对应的预测
                pre_lab = torch.argmax(output, 1)  # 查找每一行中最大值对应的行标
                loss = criterion(output, b_y)  # 计算每一个batch的损失函数
                optimizer.zero_grad()  # 将梯度初始化为0
                loss.backward()  # 反向传播计算
                optimizer.step()  # 根据网络反向传播的梯度信息来更新网络的参数,以起到降低loss函数计算值的作用
                train_loss += loss.item() * b_x.size(0)  # 对损失函数进行累加
                train_corrects += torch.sum(pre_lab == b_y.data)  # 如果预测正确,则准确度train_corrects加1
                train_num += b_x.size(0)  # 当前用于训练的样本数量
            else:  # 使用数据集的20%用于验证
                model.eval()  # 设置模型为评估模式,不启用Batch Normalization和Dropout
                output = model(b_x)  # 前向传播过程,输入为一个batch,输出为一个batch中对应的预测
                pre_lab = torch.argmax(output, 1)  # 查找每一行中最大值对应的行标
                loss = criterion(output, b_y)  # 计算每一个batch中64个样本的平均损失函数
                val_loss += loss.item() * b_x.size(0)  # 将验证集中每一个batch的损失函数进行累加
                val_corrects += torch.sum(pre_lab == b_y.data)  # 如果预测正确,则准确度val_corrects加1
                val_num += b_x.size(0)  # 当前用于验证的样本数量

        # 计算并保存每一次迭代的成本函数和准确率
        train_loss_all.append(train_loss / train_num)  # 计算并保存训练集的成本函数
        train_acc_all.append(train_corrects.double().item() / train_num)  # 计算并保存训练集的准确率
        val_loss_all.append(val_loss / val_num)  # 计算并保存验证集的成本函数
        val_acc_all.append(val_corrects.double().item() / val_num)  # 计算并保存验证集的准确率
        print('{} Train Loss: {:.4f} Train Acc: {:.4f}'.format(epoch, train_loss_all[-1], train_acc_all[-1]))
        print('{} Val Loss: {:.4f} Val Acc: {:.4f}'.format(epoch, val_loss_all[-1], val_acc_all[-1]))

        # 寻找最高准确度
        if val_acc_all[-1] > best_acc:
            best_acc = val_acc_all[-1]  # 保存当前的最高准确度
            best_model_wts = copy.deepcopy(model.state_dict())  # 保存当前最高准确度下的模型参数
        time_use = time.time() - since  # 计算耗费时间
        print("Train and val complete in {:.0f}m {:.0f}s".format(time_use // 60, time_use % 60))

    # 选择最优参数
    model.load_state_dict(best_model_wts)  # 加载最高准确度下的模型参数
    train_process = pd.DataFrame(data={"epoch": range(num_epochs),
                                       "train_loss_all": train_loss_all,
                                       "val_loss_all": val_loss_all,
                                       "train_acc_all": train_acc_all,
                                       "val_acc_all": val_acc_all}
                                 )  # 将每一代的损失函数和准确度保存为DataFrame格式

    # 显示每一次迭代后的训练集和验证集的损失函数和准确率
    plt.figure(figsize=(12, 4))
    plt.subplot(1, 2, 1)
    plt.plot(train_process['epoch'], train_process.train_loss_all, "ro-", label="Train loss")
    plt.plot(train_process['epoch'], train_process.val_loss_all, "bs-", label="Val loss")
    plt.legend()
    plt.xlabel("epoch")
    plt.ylabel("Loss")
    plt.subplot(1, 2, 2)
    plt.plot(train_process['epoch'], train_process.train_acc_all, "ro-", label="Train acc")
    plt.plot(train_process['epoch'], train_process.val_acc_all, "bs-", label="Val acc")
    plt.xlabel("epoch")
    plt.ylabel("acc")
    plt.legend()
    plt.show()

    return model, train_process

接下来开始对模型进行训练和测试,其中优化算法使用了Adam优化器学习率设置为0.0003,损失函数为交叉熵函数。然后调用train_model()函数将训练集train_loader的80%用于训练,20%用于验证,一共训练25轮。

# 训练和测试模型
def train_model_process(myconvnet):
    optimizer = torch.optim.Adam(myconvnet.parameters(), lr=0.0003)  # 使用Adam优化器,学习率为0.0003
    criterion = nn.CrossEntropyLoss()  # 损失函数为交叉熵函数
    train_loader, class_label = train_data_process()  # 加载训练集
    test_data_x, test_data_y = test_data_process() # 加载测试集
    myconvnet, train_process = train_model(myconvnet, train_loader, 0.8, criterion, optimizer, num_epochs=25)  # 进行模型训练

    # 对测试集进行预测
    myconvnet.eval()  # 设置模型为评估模式,不启用Batch Normalization和Dropout
    output = myconvnet(test_data_x)  # 前向传播过程,输入为测试数据集,输出为对每个样本的预测
    pre_lab = torch.argmax(output, 1)  # 查找每一行中最大值对应的行标
    acc = accuracy_score(test_data_y, pre_lab)  # 计算分类准确率
    print("val_acc:", acc)

    # 计算混淆矩阵并可视化
    conf_mat = confusion_matrix(test_data_y, pre_lab)
    df_cm = pd.DataFrame(conf_mat, index=class_label, columns=class_label)
    heatmap = sns.heatmap(df_cm, annot=True, fmt="d", cmap="YlGnBu")
    heatmap.yaxis.set_ticklabels(heatmap.yaxis.get_ticklabels(), rotation=0, ha='right')
    heatmap.xaxis.set_ticklabels(heatmap.xaxis.get_ticklabels(), rotation=45, ha='right')
    plt.ylabel('True label')
    plt.xlabel('Predicted label')
    plt.show()

在模型训练过程中,损失函数和分类准确率的变化曲线如下。可以看到,损失函数在训练集上迅速减小,在验证集上先减小然后逐渐收敛到一个很小的区间,说明模型已经稳定。分类准确率在训练集上一直在增大,而在验证集上逐渐收敛到一个小的区间内。
在这里插入图片描述

为了得到计算模型的泛化能力,将测试集给到训练好的模型进行预测,从而得到在测试集上的预测准确率(如下图)。
在这里插入图片描述

针对测试样本的预测结果,使用混淆矩阵表示并将其可视化,观察其在每类数据上的预测情况(如下图)。可以看到,最容易预测发生错误的是T-shirtShirt,相互预测出错的样本量超过了100个。
在这里插入图片描述

5. 运行程序

如下是主函数中的内容,创建一个类ConvNet的对象,并对该卷积神经网络进行训练和预测。

if __name__ == '__main__':
    convnet = ConvNet()
    train_model_process(convnet)

注:在之前的程序中配置了使用多个进程同时加载训练集数据,多进程的使用必须在main()函数中进行,否则会在执行过程中报错。

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/baoli8425/article/details/119740795

智能推荐

【stm32f407】外部中断实现按键中断方式_stm按钮中断执行代码停止-程序员宅基地

文章浏览阅读5k次,点赞3次,收藏13次。【stm32f407】外部中断实现按键中断方式_stm按钮中断执行代码停止

真心推荐ATMEL SAMA5D3系列芯片产品_5d34芯片-程序员宅基地

文章浏览阅读881次。Now, we finished our SAMA5D34 Industrial Board, it expand more pin from the cpu and Integrated 1GMBit Ethernet on the cpu board.CPU BoardDimensions: 52*64mm, 8 layerWorking Temperature: -40 to_5d34芯片

mapstruct 实体转换及List转换,@Mapper注解转换_@mapping list-程序员宅基地

文章浏览阅读6.6k次,点赞9次,收藏29次。mapstruct 实体转换及List转换,@Mapper注解转换 开发中,我们经常需要将PO转DTO、DTO转PO等一些实体间的转换。比较出名的有BeanUtil 和ModelMapper等,它们使用简单,但是在稍显复杂的业务场景下力不从心。MapStruct这个插件可以用来处理domin实体类与model类的属性映射,可配置性强。只需要定义一个 Mapper 接口,MapStruct 就会自动实现这个映射接口,避免了复杂繁琐的映射实现。MapStruct官网地址:http://mapstruct.o_@mapping list

读取服务器磁盘文件,获取远程服务器上的盘符文件夹-程序员宅基地

文章浏览阅读265次。获取远程服务器上的盘符文件夹 内容精选换一换已成功登录Java性能分析。待安装Guardian的服务器已开启sshd。待安装Guardian的服务器已安装JRE,JRE版本要求为Huawei JDK 8或者Open JDK 8/11。Java性能分析优先选用非交互shell(non-interactive shell)中的JAVA_HOME环境变量所指定的JRE版本运行Guardi已成功登录Jav..._远程读取磁盘

Swish & hard-Swish_swish和hardswish-程序员宅基地

文章浏览阅读1.5w次,点赞2次,收藏18次。当β = 0时,Swish变为线性函数f(x)=x/2β → ∞, σ(x)=(1+exp(−x))−1σ(x)=(1+exp⁡(−x))−1为0或1. Swish变为ReLU: f(x)=2max(0,x)所以Swish函数可以看做是介于线性函数与ReLU函数之间的平滑函数. beta是个常数或者可以训练的参数。其具有无上界有下界、平滑、非单调的特性。其在模型效果上优于ReLU。ha..._swish和hardswish

视频教程-《ACM竞赛-C/C++入门》 C语言-2-C/C++-程序员宅基地

文章浏览阅读153次。《ACM竞赛-C/C++入门》 C语言-2 毕业于清华大学,曾担任Googl..._,c++ 语言本身比较难学,语法特性很多,这里推荐一个acm大佬的免费课程,可以试听下,

随便推点

FANUC机器人信号提前触发指令说明_发那科机器人之前时间指令-程序员宅基地

文章浏览阅读136次。同理下行语句的代表在移动到点位2之前的3mm的位置会触发一个DO20为真的信号。创建一个基础的运动指令,将光标移动至指令最后的空格栏,点击下方的选择按钮。下述这行语句代表的是在完成J1动作的前3S触发一个DO20为真的一个信号。添加一个之前时间的动作,TB代表是J1动作之前几秒触发。创建一个测试指令用的TP程序。_发那科机器人之前时间指令

在不同 webpack 版本的 Vue 项目中配置 Storybook-程序员宅基地

文章浏览阅读663次。在之前的一篇文章中,介绍过组件化搭建工具 storybook 在 vue 项目中的安装和配置。相比于其成文的时间,vue 项目依赖的工具多有发展;并且在实际应用中,多种历史版本的项目并存的..._vue node 14 安装storybook

js 时间按分钟加减-程序员宅基地

文章浏览阅读3.1k次。//几分钟前后 this.addMinutes = function (v, n) { v = toDate(v); if (v) { v = new Date(v.valueOf()); v...._js addminutes

AD16 铺铜 复制 自动变形 偏好设置_repour polygon after-程序员宅基地

文章浏览阅读4.5k次。问题:为以后碰到这个问题的朋友们填一个坑,今天在学习二层板天线布线的最后时需要铺铜,所以需要将Top Layer的铜皮形状复制到Botto Layer中去,铜皮不会自动更新,下图是教程里的图片(能repour polygon):经过百度后发现是PCB Editor设置有问题!具体设置步骤如下:1.打开DXP中的Preferences。2.将PCB Editor中的General,然后勾选..._repour polygon after

【C语言】--求一个3 * 3矩阵对角线元素之和_c语言求一个3×3矩阵对角线元素之和。-程序员宅基地

文章浏览阅读6.7k次,点赞7次,收藏47次。利用双重for循环控制输入二维数组,再将 a[ i ][ i ]累加后输出。求一个3 * 3矩阵对角线元素之和。_c语言求一个3×3矩阵对角线元素之和。

(1)从键盘循环录入录入一个字符串,输入“end“表示结束 (2)将字符串中大写字母变成小写字母,小写字母变成大写字母,其它字符用“*“代替,并统计字母的个数 举例: 键盘录入:Hello_换行不表示结束符,做字符统计,但用一个特殊的字符串“#end”来表示结束符,该结束-程序员宅基地

文章浏览阅读633次。(1)从键盘循环录入录入一个字符串,输入"end"表示结束(2)将字符串中大写字母变成小写字母,小写字母变成大写字母,其它字符用"*"代替,并统计字母的个数举例:键盘录入:Hello12345World输出结果:hELLO*****wORLD总共10个字母package test7_2;import java.util.Scanner;public class Demo02 { public static void main(String[] args) { while(true_换行不表示结束符,做字符统计,但用一个特殊的字符串“#end”来表示结束符,该结束

推荐文章

热门文章

相关标签