深度学习笔记2——CNN识别手写数字

news2025/1/18 3:28:19

深度学习笔记2——CNN识别手写数字

本文将介绍LeNet-5和MNIST手写数字识别的PyTorch实现案例。

  • 参考文献:《Gradient-Based Learning Applied to Document Recognition》
  • 数据集(MNIST):THE MNIST DATABASE
  • 完整代码(Github):MNIST_LeNet-5_PyTorch.py

手写数字数据集(MNIST)

在深度学习中,常用的实验手写数据集为28x28的MNIST黑白手写数字数据集,由60000张训练+10000张测试图片组成,示例图片如下:
在这里插入图片描述
原始数据集文件下载解压后是由4个ubyte文件组成,如下:

文件名文件大小注释
t10k-images-idx3-ubyte7,657KB测试10000张图片矩阵数据
t10k-labels-idx1-ubyte10KB测试10000张图片对应标签数据
train-images-idx3-ubyte45,938KB训练60000张图片矩阵数据
train-labels-idx1-ubyte59KB训练60000张图片对应标签数据

可以引入struct.unpack()读取这些ubyte文件,需要注意的是头部的魔法数字,读取函数和案例如下:

import struct
import numpy as np

def load_byte(file, cache='>IIII', dtp=np.uint8):
    """ 
    读取 ubyte 格式数据
    Args:
        file (str): 文件路径的字符串
        cache (str): 缓存字符
        dtp (type): 矩阵类型

    Returns:
        np.array
    """
    iter_num = cache.count('I') * 4
    with open(file, 'rb') as f:
        magic = struct.unpack(cache, f.read(iter_num))
        data = np.fromfile(f, dtype=dtp)
    return data


# 读取出来的均是Numpy矩阵,可以通过dtype指定矩阵类型
train_data = load_byte("train-images-idx3-ubyte")  # shape(47040000,)
test_data = load_byte("t10k-images-idx3-ubyte")  # shape(60000,)
train_label = load_byte("train-labels-idx1-ubyte", ">II")  # shape(7840000,)
test_label = load_byte("t10k-labels-idx1-ubyte", ">II")  # shape(10000,)

卷积神经网络(Convolution Neural Network)

卷积神经网络,是Yann Lecun等人于1998年投稿的《Gradient-Based Learning Applied to Document Recognition》中首次提出使用卷积神经网络识别32x32手写数字黑白图像,其网络结构名称为LeNet-5。

网络中采用了Conv2D卷积+Subsampliing下采样的组合提取图像特征,最后采用MLP(Multi-Layer Perceptrons)多层感知机的形式将输入特征映射到输出的10个类中,整体结构如下图:
在这里插入图片描述
在PyTorch框架中,可以采用MaxPool2d代替Subsampling实现下采样操作,即Conv2d+MaxPool2d的组合。由于文中提出的模型结构输入图片是32x32,而MNIST数据集图片是28x28,因此需要对第一个Conv2d卷积层进行调整,输入通道为1,添加一个padding,使得后续的输出能够适应LeNet-5结构输出,搭建PyTorch代码如下:

import torch
import torch.nn as nn
import torchsummary


class Net(nn.Module):
    """ CNN 卷积网络在 MNIST 28x28 手写数字灰色图像上应用版本 """

    def __init__(self):
        super(Net, self).__init__()
        # 卷积层 #
        self.conv1 = nn.Conv2d(in_channels=1, out_channels=6,  # 输入通道由 3 调整为 1
                               kernel_size=5, stride=1, padding=2)  # padding 使得模型与原文提供的 32x32 结构保持不变
        self.conv2 = nn.Conv2d(in_channels=6, out_channels=16,
                               kernel_size=5, stride=1)
        # 池化层 #
        self.pool1 = nn.MaxPool2d(kernel_size=2, stride=2)
        self.pool2 = nn.MaxPool2d(kernel_size=2, stride=2)
        # 全连接层 #
        self.fc1 = nn.Linear(16 * 5 * 5, 120)
        self.fc2 = nn.Linear(120, 84)
        self.fc3 = nn.Linear(84, 10)
        # 激活函数 #
        self.relu = nn.ReLU()

    def forward(self, x):
        # 卷积层 1 #
        out = self.conv1(x)
        out = self.relu(out)
        out = self.pool1(out)
        # 卷积层 2 #
        out = self.conv2(out)
        out = self.relu(out)
        out = self.pool2(out)
        # 全连接层 #
        out = out.view(out.size(0), -1)
        out = self.fc1(out)
        out = self.relu(out)
        out = self.fc2(out)
        out = self.relu(out)
        out = self.fc3(out)

        return out


model = Net()
torchsummary.summary(net, input_size=(1, 28, 28), device="cpu")  # 采用 keras 的方式顺序打印模型结构

可以调用torchsummary输出keras风格的模型结构表:

----------------------------------------------------------------
        Layer (type)               Output Shape         Param #
================================================================
            Conv2d-1            [-1, 6, 28, 28]             156
              ReLU-2            [-1, 6, 28, 28]               0
         MaxPool2d-3            [-1, 6, 14, 14]               0
            Conv2d-4           [-1, 16, 10, 10]           2,416
              ReLU-5           [-1, 16, 10, 10]               0
         MaxPool2d-6             [-1, 16, 5, 5]               0
            Linear-7                  [-1, 120]          48,120
              ReLU-8                  [-1, 120]               0
            Linear-9                   [-1, 84]          10,164
             ReLU-10                   [-1, 84]               0
           Linear-11                   [-1, 10]             850
================================================================
Total params: 61,706
Trainable params: 61,706
Non-trainable params: 0
----------------------------------------------------------------
Input size (MB): 0.00
Forward/backward pass size (MB): 0.11
Params size (MB): 0.24
Estimated Total Size (MB): 0.35
----------------------------------------------------------------

构造数据集

在PyTorch中,训练过程需要使用到torch.utils.data.DataLoader构造的数据迭代器进行模型训练,DataLoader传入的是torch.utils.data.Dataset对象,因此采用torchvision.datasetMNIST构建数据集对象,代码如下:

from torchvision import transforms
from torchvision.datasets import MNIST

bs = 64  # batch_size
transform = transforms.Compose([
        transforms.ToTensor(),  # 转换为张量
    ])
# 构建数据集对象
train_data = MNIST(root=".", train=True, transform=transform)
test_data = MNIST(root=".", train=False, transform=transform)
# 构建数据迭代器
train_db = DataLoader(train_data, batch_size=bs)
test_db = DataLoader(test_data, batch_size=bs)
# 查看单个案例
for (x, y) in train_db:
	print(x.shape, y.shape)
	break
torch.Size([64, 1, 28, 28]) torch.Size([64])

传入的路径中,需要包含MNIST\raw目录,该目录下包含上述的四个解压后ubyte文件。

训练、验证、测试模型

采用keras进度条的风格的tqdm进度条,在每次epoch训练结束后进行模型验证,部分代码如下:

net = Net()  # 初始化模型并加载到GPU
net.to(device)
torchsummary.summary(net, input_size=(num_channels, image_size, image_size))  # 采用 keras 的方式顺序打印模型结构

'''+++++++++++++++++++++++
@@@ 模型训练和验证
+++++++++++++++++++++++'''

criterion = nn.CrossEntropyLoss()  # 设置损失函数
optimizer = torch.optim.Adam(net.parameters())  # 配置优化器

# 初始化损失和准确率
train_history = {"loss": {"train": [[] for _ in range(epochs)], "val": [[] for _ in range(epochs)]},
                 "acc": {"train": [[] for _ in range(epochs)], "val": [[] for _ in range(epochs)]}}

st = time.time()
for epoch in range(epochs):
    with tqdm(total=len(train_db), desc=f'Epoch {epoch + 1}/{epochs}') as pbar:
        for step, (x, y) in enumerate(train_db):
            net.train()  # 标记模型开始训练,此时权重可变
            x, y = x.to(device), y.to(device)  # 转移张量至 GPU
            output = net(x)  # 将 x 送进模型进行推导

            # 计算损失
            loss = criterion(output, y)  # 计算交叉熵损失
            optimizer.zero_grad()  # 清空梯度
            loss.backward()  # 反向传播
            optimizer.step()  # 一步随机梯度下降算法

            # 计算准确率
            prediction = torch.softmax(output, dim=1).argmax(dim=1)  # 将预测值转换成标签

            # 记录损失和准确率
            train_history["loss"]["train"][epoch].append(loss.item())
            train_history["acc"]["train"][epoch].append(((prediction == y).sum() / y.shape[0]).item())

            # 进度条状态更新
            pbar.update(1)
            pbar.set_postfix({"loss": "%.4f" % np.mean(train_history["loss"]["train"][epoch]),
                              "acc": "%.2f%%" % (np.mean(train_history["acc"]["train"][epoch]) * 100)})

        # 每一个 epoch 训练结束后进行验证
        net.eval()  # 标记模型开始验证,此时权重不可变
        for x, y in val_db:
            x, y = x.to(device), y.to(device)
            output = net(x)
            loss_val = criterion(output, y).item() / len(val_db)

            prediction = torch.softmax(output, dim=1).argmax(dim=-1)
            # 记录验证损失和准确率
            train_history["loss"]["val"][epoch].append(loss_val)
            train_history["acc"]["val"][epoch].append(((prediction == y).sum() / y.shape[0]).item())

            # 更新进度条
            pbar.set_postfix({"loss": "%.4f" % np.mean(train_history["loss"]["train"][epoch]),
                              "acc": "%.2f%%" % (np.mean(train_history["acc"]["train"][epoch]) * 100),
                              'val_loss': "%.4f" % np.mean(train_history["loss"]["val"][epoch]),
                              "val_acc": "%.2f%%" % (np.mean(train_history["acc"]["val"][epoch]) * 100)})

et = time.time()
time.sleep(0.1)
print('Time Taken: %d seconds' % (et - st))  # 69

'''+++++++++++++++++++
@@@ 模型测试
+++++++++++++++++++'''

print('Test data in model...')
correct, total, loss = 0, 0, 0
per_time = []  # 计算每个
net.eval()
with tqdm(total=len(test_db)) as pbar:
    for step, (x, y) in enumerate(test_db):
        x, y = x.to(device), y.to(device)
        st = time.perf_counter()
        output = net(x)
        torch.cuda.synchronize()
        et = time.perf_counter()
        per_time.append(et - st)
        loss += float(criterion(output, y)) / len(test_db)
        prediction = torch.softmax(output, dim=1).argmax(dim=1)
        correct += int((prediction == y).sum())
        total += y.shape[0]

        pbar.update(1)
        pbar.set_postfix({'loss': '%.4f' % loss,
                          'accuracy': '%.2f%%' % (correct / total * 100),
                          'per_time': '%.4fs' % (et - st)})

print('Time Per-Image Taken: %f seconds' % np.mean(per_time))  # 0.000984
print('FPS: %f' % (1.0 / (np.sum(per_time) / len(per_time))))  # 1015.759509

训练过程中命令行窗口将输出以下进度条记录形式:

Epoch 1/20: 100%|██████████| 94/94 [00:03<00:00, 26.21it/s, loss=1.0145, acc=68.89%, val_loss=0.0165, val_acc=88.14%]
Epoch 2/20: 100%|██████████| 94/94 [00:03<00:00, 27.80it/s, loss=0.2832, acc=91.37%, val_loss=0.0095, val_acc=93.23%]
Epoch 3/20: 100%|██████████| 94/94 [00:03<00:00, 28.02it/s, loss=0.1792, acc=94.60%, val_loss=0.0064, val_acc=95.41%]
Epoch 4/20: 100%|██████████| 94/94 [00:03<00:00, 27.47it/s, loss=0.1261, acc=96.10%, val_loss=0.0051, val_acc=96.21%]
Epoch 5/20: 100%|██████████| 94/94 [00:03<00:00, 26.54it/s, loss=0.0972, acc=96.99%, val_loss=0.0042, val_acc=96.94%]
Epoch 6/20: 100%|██████████| 94/94 [00:03<00:00, 27.78it/s, loss=0.0797, acc=97.56%, val_loss=0.0037, val_acc=97.18%]
Epoch 7/20: 100%|██████████| 94/94 [00:03<00:00, 27.48it/s, loss=0.0693, acc=97.90%, val_loss=0.0034, val_acc=97.52%]
Epoch 8/20: 100%|██████████| 94/94 [00:03<00:00, 27.15it/s, loss=0.0620, acc=98.12%, val_loss=0.0030, val_acc=97.81%]
Epoch 9/20: 100%|██████████| 94/94 [00:03<00:00, 26.14it/s, loss=0.0559, acc=98.30%, val_loss=0.0028, val_acc=98.09%]
Epoch 10/20: 100%|██████████| 94/94 [00:03<00:00, 27.63it/s, loss=0.0510, acc=98.48%, val_loss=0.0026, val_acc=98.14%]
Epoch 11/20: 100%|██████████| 94/94 [00:03<00:00, 24.55it/s, loss=0.0466, acc=98.60%, val_loss=0.0025, val_acc=98.23%]
Epoch 12/20: 100%|██████████| 94/94 [00:03<00:00, 26.40it/s, loss=0.0431, acc=98.70%, val_loss=0.0025, val_acc=98.25%]
Epoch 13/20: 100%|██████████| 94/94 [00:03<00:00, 26.93it/s, loss=0.0396, acc=98.79%, val_loss=0.0024, val_acc=98.33%]
Epoch 14/20: 100%|██████████| 94/94 [00:03<00:00, 26.35it/s, loss=0.0376, acc=98.84%, val_loss=0.0024, val_acc=98.36%]
Epoch 15/20: 100%|██████████| 94/94 [00:03<00:00, 27.65it/s, loss=0.0354, acc=98.91%, val_loss=0.0024, val_acc=98.39%]
Epoch 16/20: 100%|██████████| 94/94 [00:03<00:00, 27.16it/s, loss=0.0332, acc=98.99%, val_loss=0.0024, val_acc=98.41%]
Epoch 17/20: 100%|██████████| 94/94 [00:03<00:00, 25.94it/s, loss=0.0305, acc=99.08%, val_loss=0.0025, val_acc=98.36%]
Epoch 18/20: 100%|██████████| 94/94 [00:03<00:00, 27.69it/s, loss=0.0281, acc=99.13%, val_loss=0.0025, val_acc=98.31%]
Epoch 19/20: 100%|██████████| 94/94 [00:03<00:00, 28.22it/s, loss=0.0256, acc=99.21%, val_loss=0.0025, val_acc=98.31%]
Epoch 20/20: 100%|██████████| 94/94 [00:03<00:00, 28.84it/s, loss=0.0234, acc=99.31%, val_loss=0.0024, val_acc=98.32%]

训练结果可视化

每次训练结束后,对训练过程的损失、准确率进行一定可视化,将更直观分析训练过程模型是否收敛。可视化模块采用matplotlib.pyplot,绘制图片如下:

def plot_train_history(history, num_epoch=epochs):
    """
    对训练结果的可视化
    Args:
        history (dict): 训练结果字典(包含 loss 和 accuracy 键)
        num_epoch (int): 展示周期数量(默认为 epochs)

    Returns:

    """
    keys = ['loss', 'acc']
    for k in keys:
        plt.plot(range(1, num_epoch + 1), np.mean(history[k]["train"][:num_epoch + 1], -1))
        plt.plot(range(1, num_epoch + 1), np.mean(history[k]["val"][:num_epoch + 1], -1))
        plt.legend(labels=['Train', 'Val'])
        plt.title(f'MNIST LeNet-5 Train & Valid {k.title()}')
        plt.xlabel('Epoch')
        plt.ylabel(k.title())
        plt.grid(True)
        plt.show()


plot_train_history(train_history, epochs)

在这里插入图片描述
在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/532729.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

ChatGPT接入微信公众号(手把手教学)

前言 本篇文章参考国内服务器 3 分钟将 ChatGPT 接入微信公众号&#xff08;超详细&#xff09;配置&#xff0c;纠正了一些过时的信息。 准备 一个微信公众号 一个能访问外网的梯子 一个ChatGPT账号 有了这些就可以配置了 注册免费服务器&#xff0c;并部署代码 前往Laf…

CloudCompare二次开发之如何通过PCL进行点云配准?

文章目录 0.引言1.CloudCompare界面设计配准(registrate)按钮2.ICP配准&#xff08;ICP_Reg&#xff09;3.多幅点云逐步配准&#xff08;Many_Reg&#xff09; 0.引言 因笔者课题涉及点云处理&#xff0c;需要通过PCL进行点云数据一系列处理分析&#xff0c;查阅现有网络资料&a…

ChatgGPT生成Excel统计公式

需求背景 编写excel公式&#xff0c;提取下图中符号之前的内容并填充到“修改后的内容”这一列 流程思路 借助ChatGPT完成Excel公式的大致流程如下&#xff1a; 确定要解决的问题&#xff1a;明确你需要在Excel中实现的具体任务或计算需求。例如&#xff0c;求和、平均值、…

优思学院|用ChatGPT人工智能制作FMEA可以吗?

问题和缺陷是昂贵的&#xff0c;它们是质量成本的主要构成部分。同时&#xff0c;顾客可以对制造商和服务提供商抱有很高的期望&#xff0c;希望他们提供高质量和高可靠性。 通常&#xff0c;很多企业只会在产品和服务的开发后期&#xff0c;通过广泛的测试和檢查来发现问题。…

提高 Maya 渲染质量和速度的4个小技巧

Autodesk Maya&#xff0c;通常简称为Maya&#xff0c;是一种3D计算机图形应用程序&#xff0c;可在Windows、macOS和Linux上运行&#xff0c;最初由Alias开发&#xff0c;目前由Autodesk拥有和开发。它用于为交互式3D应用程序、动画电影、电视剧和视觉效果创建资产。 您可以通…

以 29K 成功入职字节跳动,这份《 软件测试面试笔记》让我受益匪浅

朋友入职已经两周了&#xff0c;整体工作环境还是非常满意的&#xff01;所以这次特意抽空给我写出了这份面试题&#xff0c;而我把它分享给小伙伴们&#xff0c;面试&入职的经验&#xff01; 大概是在3月中的时候他告诉我投递了简历&#xff0c;4月的时候经过了3轮面试收…

【C++】4. 类和对象终章

专栏导读 &#x1f341;作者简介&#xff1a;余悸&#xff0c;在读本科生一枚&#xff0c;致力于 C方向学习。 &#x1f341;收录于 C专栏&#xff0c;本专栏主要内容为 C初阶、 C 进阶、STL 详解等&#xff0c;持续更新中&#xff01; &#x1f341;相关专栏推荐&#xff1a; …

做F牌独立站要做好功课,拒绝被割韭菜!

做过爆品独立站的朋友们都知道&#xff0c;遇到爆品不容易&#xff0c;很多都具有滞后性&#xff0c;都是当你发现了之后&#xff0c;这个帖子/视频/产品已经被人跑烂了&#xff0c;你再去跑&#xff0c;这样只会浪费大量的广告费。既然爆品独立站的广告费烧不过大卖&#xff0…

知识图谱学习笔记——(五)知识图谱推理

一、知识学习 声明&#xff1a;知识学习中本文主体按照浙江大学陈华钧教授的《知识图谱》公开课讲义进行介绍&#xff0c;并个别地方加入了自己的注释和思考&#xff0c;希望大家尊重陈华钧教授的知识产权&#xff0c;在使用时加上出处。感谢陈华钧教授。 &#xff08;一&…

Java配置方式使用Spring MVC:实战练习

文章目录 续写任务1、创建登录页面、登录成功与登录失败页面1、创建登录页面2、创建登录成功页面3、创建登录失败页面 任务2、首页添加登录链接&#xff0c;单击可跳转到登录页面1、 修改首页&#xff0c;添加超链接2、修改Spring MVC配置类&#xff0c;定义视图控制器3、创建登…

Spark - 创建 _SUCCESS 文件与获取最新可用文件

目录 一.引言 二.增加 _SUCCESS 标识 1.SparkContext 生成 2.FileSystem 生成 3.Hadoop 生成 三.获取最新文件 1.获取 SparkContext 2.按照时间排序 3.遍历生成 Input 四.总结 一.引言 有任务需要每小时生成多个 split 文件分片&#xff0c;为了保证线上任务读取最新…

Linux实操篇---常用的基本命令5(进程管理类和crontab系统定时任务)

一、进程管理类 进程是正在执行的一个程序或命令&#xff0c;每一个进程都是一个运行的实体&#xff0c;都有自己的地址空间&#xff0c;并占用一定的系统资源。 守护进程和系统服务就是一一对应的关系。 有系统级别的进程和用户级别的进程。 进程管理&#xff1a;所有的进…

如何使用自定义知识库构建自定义ChatGPT机器人

目录 隐藏 使用自定义数据源为您的 ChatGPT 机器人提供数据 1. 通过Prompt提示工程提供数据 2. 使用 LlamaIndex&#xff08;GPT 索引&#xff09;扩展 ChatGPT 如何添加自定义数据源 先决条件 怎么运行的 最后的总结 使用自定义数据源为您的 ChatGPT 机器人提供数据…

rt-thread启动流程

资料下载 RT-Thread Simulator 例程 操作流程 将上面的仿真例程下载并解压&#xff0c;通过MDK打开&#xff0c;编译&#xff0c;调试&#xff0c;并打开串口点击运行&#xff0c;就可以看到如下输出了&#xff1a; 添加自己的 thread&#xff1a;在main()函数中添加即可&am…

[Java基础]基本概念(上)(标识符,关键字,基本数据类型)

hello 大家好&#xff0c;计算机语言各有不同&#xff0c;但本质上都是操作内存和计算。这章的内容是介绍Java中的基本概念展开&#xff0c;包括&#xff1a;标识符&#xff0c;关键字&#xff0c;Java基本数据类型&#xff0c;运算符&#xff0c;表达式和语句&#xff0c;分支…

前端架构师-week6-require源码解析

require 源码解析——彻底搞懂 npm 模块加载原理 require 的使用场景 加载模块类型 加载内置模块&#xff1a;require(fs)加载 node_modules 模块&#xff1a;require(ejs)加载本地模块&#xff1a;require(./utils)支持文件类型 加载 .js 文件加载 .mjs 文件加载 .json 文件…

AI女友同时和1000人谈恋爱,狂赚500万

AI女友&#xff0c;预计暴赚4亿 要说当下什么最火&#xff0c;AI首当其冲无可置疑。00后网络红人红卡琳玛乔丽&#xff08;Caryn Marjorie&#xff09;最近与Forever Voices公司合作&#xff0c;通过视频训练等方式打造出个人形象、声音和性格的AI虚拟女友&#xff0c;就像在和…

Redis高可用--持久化

在Web服务器中&#xff0c;高可用是指服务器可以正常访问的时间&#xff0c;衡量的标准实在多长时间内可以提供正常服务&#xff08;99.9%、99.99%、99.999%等等&#xff09;。 但是在Redis语境中&#xff0c;高可用的含义似乎要宽泛一些&#xff0c;除了保证提提供正常服务&a…

大疆无人机 MobileSDK(遥控器/手机端)开发 v4版<3>

导读 第三篇文章准备单独拿出来写,因为在大疆为人机的所有功能中,航线规划的功能最为复杂,也相当的繁琐,这里需要说仔细一点,可能会将代码进行多步分解。 航线规划 1)航线打点 点击 按钮进行打点,在地图中手动选择点位选择完成后点击**[完成]**按钮,即可完成航线打点…

新展预告 | YT U LOVE——许峰个展即将亮相!

深圳东方美术馆荣幸地宣布&#xff0c;将于5月20日呈现艺术家许峰在鹏城的首次个展“YT U LOVE”&#xff0c;展出艺术家从2020年至2023年创作的油画、纸本及雕塑40余件作品。此次展览以“YT U LOVE”为题&#xff0c;恰逢兔年&#xff0c;yutu在中国意指玉兔&#xff0c;前后两…