基于CNN的Mnist手写数字识别(Pytorch)

环境

系统环境:

环境配置
cuda11.8
python3.8
torch2.0.1+cu118

主要步骤

主要实现包括:

  1. 导入所需的PyTorch库。
  2. 定义数据预处理转换,将图像转换为张量并进行标准化。
  3. 加载MNIST训练集和测试集,并应用预处理转换。
  4. 创建训练集和测试集的数据加载器,用于批量加载数据。
  5. 定义CNN模型的类。在这个示例中,模型包含两个卷积层和两个全连接层。
  6. 实例化CNN模型。
  7. 定义损失函数和优化器。这里使用交叉熵损失和Adam优化器。
  8. 设置训练的总轮数和设备类型(GPU或CPU)。
  9. 将模型移动到指定的设备上。 在每个epoch中进行训练。
  10. 首先,将模型设置为训练模式,然后遍历训练数据加载器。
  11. 将输入数据和标签移动到指定的设备上。
  12. 清零优化器的梯度。
  13. 前向传播:通过模型获取输出。
  14. 计算损失。
  15. 反向传播:计算梯度并更新模型参数。
  16. 在每个epoch结束后,在测试集上评估模型的准确率。首先,将模型设置为评估模式,然后遍历测试数据加载器。
  17. 将输入数据和标签移动到指定的设备上。
  18. 前向传播:通过模型获取输出。
  19. 使用预测结果和真实标签计算准确率。
  20. 打印当前epoch的测试准确率。

CNN模型

import torch.nn as nn
# 定义用于进行训练的CNN模型,模型包含两个卷积层和两个全连接层。
class CNN(nn.Module):
    def __init__(self):
        super(CNN, self).__init__()
        # 因为输入图片为1*28*28,说明为单通道图片,大小为28*28。
        # 1: 输入通道数(input channels)表示输入数据的通道数,这里的值为1,说明输入是单通道的图像。
        # 32: 输出通道数(output channels)表示卷积层的输出通道数,这里的值为32,说明该卷积层会产生32个不同的特征图作为输出。
        # kernel_size = 3: 卷积核大小 表示卷积核的尺寸大小。在这种情况下,卷积核的大小为3x3,即3行3列。
        # stride = 1: 步幅(stride)表示卷积操作时滑动卷积核的步幅大小。在这里,步幅为1,意味着卷积核每次在水平和垂直方向上都以1个像素的距离滑动。
        # padding = 1: 填充(padding)表示在输入图像周围添加额外的零值像素来控制输出图像的尺寸。在这里,填充为1,意味着在输入图像的周围添加1个像素宽度的零值填充。
        # 所以输出为 28+2(padding1+1=2)-(3-1)(kernek_size-1)=28,即64,32,28,28
        self.conv1 = nn.Conv2d(1, 32, kernel_size=3, stride=1, padding=1)

        #表示在CNN模型中定义了一个ReLU激活函数层。
        # ReLU(Rectified Linear Unit)是一种常用的非线性激活函数,它的定义是 f(x) = max(0, x),即将小于零的输入值变为零,而大于等于零的输入值保持不变。
        # 在深度学习中,ReLU激活函数被广泛应用于神经网络的隐藏层,作为引入非线性性质的关键组件。ReLU的主要作用是引入非线性映射,使得神经网络能够学习复杂的非线性关系。
        # 输出大小与之前一样
        self.relu = nn.ReLU()

        # 最大池化是一种用于降低特征图维度的操作,常用于卷积神经网络中。它将输入的特征图划分为不重叠的矩形区域(通常是2x2的窗口),然后在每个区域中选择最大的值作为输出。这样可以减少特征图的空间维度,并保留最显著的特征。
        # kernel_size = 2:池化窗口大小:表示池化操作使用的窗口大小。在这里,池化窗口的大小为2x2,即2行2列的窗口。
        # stride = 2:步幅:表示在池化操作时滑动池化窗口的步幅大小。在这里,步幅为2,意味着池化窗口每次在水平和垂直方向上都以2个像素的距离滑动。
        # 最大池化层通常紧跟在卷积层之后,用于减小特征图的尺寸,同时保留主要的特征。它有助于减少模型的参数数量,提高计算效率,并具有一定的平移不变性。
        # 输出大小为原图片大小长宽除以2
        self.maxpool = nn.MaxPool2d(kernel_size=2, stride=2)

        # 32: 输入通道数(input channels)表示输入数据的通道数,为之前产生的32个不同的特征图
        # 64: 输出通道数(output channels)表示卷积层的输出通道数,这里的值为64,说明该卷积层会产生64个不同的特征图作为输出。
        # kernel_size = 3: 卷积核大小 表示卷积核的尺寸大小。在这种情况下,卷积核的大小为3x3,即3行3列。
        # stride = 1: 步幅(stride)表示卷积操作时滑动卷积核的步幅大小。在这里,步幅为1,意味着卷积核每次在水平和垂直方向上都以1个像素的距离滑动。
        # padding = 1: 填充(padding)表示在输入图像周围添加额外的零值像素来控制输出图像的尺寸。在这里,填充为1,意味着在输入图像的周围添加1个像素宽度的零值填充。
        # 所以输出为 14+2(padding1+1=2)-(3-1)(kernek_size-1)=28,即64,64,14,14
        self.conv2 = nn.Conv2d(32, 64, kernel_size=3, stride=1, padding=1)

        # 全连接层是深度学习模型中常用的一种层类型,也称为线性层或密集连接层。它的作用是将前一层的所有输入与当前层的每个神经元进行连接,通过权重和偏置进行线性变换,然后将结果传递给激活函数进行非线性映射。
        # 参数解释如下:
        # 7 * 7 * 64:输入特征的维度 表示前一层的输出特征的维度。在这里,这个维度的值是7 * 7 * 64,说明输入特征是一个7x7的图像,具有64个通道。
        # 10:输出特征的维度 表示当前层输出特征的维度。在这里,这个维度的值是10,说明全连接层将产生一个包含10个元素的输出。
        self.fc = nn.Linear(7 * 7 * 64, 10)

    def forward(self, x):
        # print(x.shape) torch.Size([64, 1, 28, 28])

        out = self.conv1(x)
        # print(out.shape) torch.Size([64, 32, 28, 28])

        out = self.relu(out)
        # print(out.shape) torch.Size([64, 32, 28, 28])

        out = self.maxpool(out)
        # print(out.shape) torch.Size([64, 32, 14, 14])

        out = self.conv2(out)
        # print(out.shape) torch.Size([64, 64, 14, 14])

        out = self.relu(out)
        # print(out.shape) torch.Size([64, 64, 14, 14])

        out = self.maxpool(out)
        # print(out.shape) torch.Size([64, 64, 7, 7])

        # out.view(out.size(0), -1)的含义是将张量 out 进行形状变换。其中,第一个维度的大小保持不变(即 out.size(0)),而剩余维度的大小会根据张量的元素数量自动计算得出。即变成64*7*7=3136
        out = out.view(out.size(0), -1)
        # print(out.shape) torch.Size([64, 3136])

        out = self.fc(out)
        # print(out.shape) torch.Size([64, 10])
        return out

数据预处理

if __name__ == '__main__':
	print_hi('PyTorch')
    train_dataset = datasets.MNIST(root='./data', train=True, download=True)
    mean, std = cal_mean_and_std(train_dataset)
    # 定义数据预处理转换,定义数据预处理转换,将图像转换为张量并进行标准化
    transform = transforms.Compose([
        transforms.ToTensor(),  # 将图像转换为张量
        transforms.Normalize(mean, std)  # 标准化图像数据 output = (input - mean) / std
    ])

    # 加载MNIST训练集和测试集,应用预处理转换
    train_dataset = datasets.MNIST(root='./data', train=True, download=True, transform=transform)
    test_dataset = datasets.MNIST(root='./data', train=False, download=True, transform=transform)

    # 创建数据加载器,用于批量加载数据
    batch_size = 64
    print("batch_size: ", batch_size)
    train_loader = DataLoader(train_dataset, batch_size=batch_size, shuffle=True)
    test_loader = DataLoader(test_dataset, batch_size=batch_size, shuffle=False)
    print("训练集数据大小", len(train_loader))
    print("验证集数据大小", len(test_loader))
    # 获取一个小批量数据并打印其形状
    dataiter = iter(train_loader)
    images, labels = next(dataiter)
    print("图片大小", images.shape)  # 打印图像数据的形状,如 (64, 1, 28, 28)
    print("标签数据大小", labels.shape)  # 打印标签数据的形状,如 (64)

其中计算数据集的平均数与标准差的函数为:

def cal_mean_and_std(dataset):
    data = [np.array(image) / 255 for image, _ in dataset]
    data = np.stack(data, axis=0)
    # 计算三维数组 data 沿着前三个维度的平均值,即计算所有元素的平均值
    mean = np.mean(data, axis=(0, 1, 2))
    # 计算三维数组 data 沿着前三个维度的平均值,即计算所有元素的平均值
    std = np.std(data, axis=(0, 1, 2))
    print("mean:", mean, "std:", std)
    return mean, std

模型训练与保存

每轮训练后在训练集和验证集上分别验证分类准确率,同时保存每轮的模型文件:

# 创建CNN模型实例
    model = CNN()

    # 定义交叉熵损失函数和Adam优化器
    criterion = nn.CrossEntropyLoss()
    print("损失函数", criterion)
    optimizer = optim.Adam(model.parameters(), lr=0.001)
    print("优化器", optimizer)

    # 训练模型
    num_epochs = 100
    device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
    print("使用设备", device)
    model.to(device)

    best_accuracy = 0
    best_epoch = 0

    # epoch大循环
    for epoch in range(num_epochs):
        model.train()
        # batch小循环: images[64, 1, 28, 28], labels[64]
        start_time = time.time()
        for images, labels in tqdm(train_loader, desc='训练ing...'):
            images = images.to(device)
            labels = labels.to(device)
            optimizer.zero_grad()
            outputs = model(images)
            loss = criterion(outputs, labels)
            loss.backward()
            optimizer.step()
        train_time = time.time() - start_time

        # 在每个epoch结束后计算模型在训练集上的准确率
        model.eval()
        total0 = 0
        correct0 = 0
        start_time = time.time()
        with torch.no_grad():
            for images, labels in tqdm(train_loader, desc='训练集准确性测试ing...'):
                images = images.to(device)
                labels = labels.to(device)
                # outputs_size: 64,10
                outputs = model(images)
                # predicted_size: 64, 每个image输出的10个元素的tensor中最大的元素的下标,只关注下标!!
                # outputs.data 是模型的输出张量,它的形状为 (batch_size, num_classes),其中 batch_size=64 是每个小批量数据的大小,num_classes 是分类问题的类别数。因此,torch.max(outputs.data, 1) 将返回 (max_values, max_indices) 二元组,其中 max_values 是每一行中的最大值,max_indices 是每一行中最大值所在的列的下标
                confidence_values, predicted = torch.max(outputs.data, 1)  # confidence_values是最大值的大小,即置信度值
                # 计算总数目
                total0 += labels.size(0)
                # 计算正确数目
                correct0 += (predicted == labels).sum().item()
        train_accuracy0 = 100 * correct0 / total0
        test_time0 = time.time() - start_time

        # 在每个epoch结束后计算模型在测试集上的准确率
        model.eval()
        total = 0
        correct = 0
        start_time = time.time()
        with torch.no_grad():
            for images, labels in tqdm(test_loader, desc='验证集准确性测试ing...'):
                images = images.to(device)
                labels = labels.to(device)
                outputs = model(images)
                _, predicted = torch.max(outputs.data, 1)
                total += labels.size(0)
                correct += (predicted == labels).sum().item()
        test_accuracy = 100 * correct / total
        test_time1 = time.time() - start_time
        tqdm.write(f"Time [{time.time()}], Epoch [{epoch + 1}/{num_epochs}], Train Accuracy: {train_accuracy0:.2f}%, "
                   f"Test Accuracy: {test_accuracy:.2f}%, Train Time: {train_time:.2f}s, "
                   f"Test_train_dateset Time: {test_time0:.2f}s, "
                   f"Test_test_dateset Time: {test_time1:.2f}s")
        torch.save(model.state_dict(), f'res/process/model_epoch_{epoch+1}.pkl')

保存验证集上效果最好的模型为pkl:

# 在每个epoch结束时,检查模型在验证集上的准确性是否最好
        if test_accuracy > best_accuracy:
            best_accuracy = test_accuracy
            if os.path.exists(f'res/best_model_epoch_{best_epoch}.pkl'):
                # 如果存在,则删除该文件
                os.remove(f'res/best_model_epoch_{best_epoch}.pkl')
            best_epoch = epoch + 1
            print(f"save new best_model_epoch: {best_epoch}")
            # 保存当前最好的模型
            torch.save(model.state_dict(), f'res/best_model_epoch_{best_epoch}.pkl')

训练过程保存

保存训练过程中的准确度变化为csv文件
训练前:

    file_name = 'res/acc.csv'  # csv 文件名
    file_path = os.path.abspath(os.path.join(os.getcwd(), file_name))  # csv 文件路径
    if os.path.exists(file_path):  # 判断文件是否存在
        os.remove(file_path)  # 如果文件存在则删除
    with open(file_path, 'a+', newline='') as csvfile:
        writer = csv.writer(csvfile)
        writer.writerow(['epoch','train_acc','test_acc'])

每个Epoch结束:

        # 保存loss
        with open(file_path, 'a+', newline='') as csvfile:
            writer = csv.writer(csvfile)
            writer.writerow([epoch + 1, train_accuracy0, test_accuracy])

训练结果

训练过程:
预处理阶段:
在这里插入图片描述
训练过程:
在这里插入图片描述
这里GPU负载还挺高的哈哈
在这里插入图片描述
保存的最好的模型文件是在第55轮
在这里插入图片描述
此时准确度测试集99.29%,训练集99.99%:
在这里插入图片描述保存的精确度文件acc.csv:

在这里插入图片描述
绘制训练集与验证集精确度随Epoch变化如下:
在这里插入图片描述
发现20轮左右就收敛了

  • 2
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

eduics

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值