CNN - - Transformer实践篇(一)

本文基于PyTorch框架,详细实践卷积神经网络(CNN)在手写数字识别任务中的完整流程:从环境配置、digits数据集加载与预处理,到CNN模型搭建(含卷积、ReLU、池化、全连接层)、损失函数与优化器定义、模型训练与测试,最终达到96.94%测试准确率。重点涵盖8×8灰度图像的通道处理、特征图尺寸变化、梯度更新机制及过拟合分析。

前言

古语有云:“光说不练假把式。”马克思主义也教导我们,要坚持理论与实践相结合,实事求是,在实践中检验和发展真理。因此,笔者也需要通过更多实践来真正掌握所学内容。本推送主要使用 PyTorch 实现 CNN 的手写数字识别功能。

本推送以 Windows 笔记本为运行环境,覆盖大多数读者常用的系统;对性能要求很低,步骤详细,非常适合练手。

目录

1 CNN是什么

2 环境准备 

3 导入工具

4 认识手写数字数据集

5 划分训练集和测试集

6 搭建CNN模型

7 训练模型

8 测试模型的识别效果

9 预测结果

10 练习

1 CNN是什么

CNN 的英文全称是 Convolutional Neural Network,即卷积神经网络。

简单来说,CNN 是一种特别适合处理图像的神经网络。它可以从图片中自动学习特征,再根据这些特征完成分类。

例如,当我们识别手写数字 8 时,人眼会观察它的线条、弯曲程度和形状。CNN 也会学习类似的信息,只不过这些特征是通过训练自动学出来的。

CNN 主要包含以下几个部分。

(1) 卷积(Convolution)

可以把卷积理解成一个小窗口在图片上滑动,寻找边缘、线条、转角等特征。

(2)激活函数(ReLU)

ReLU 会将负数变成 0,为网络提供学习复杂模式所需的非线性能力。

(3)池化(Pooling)

池化会缩小特征图的尺寸,同时保留较重要的信息,减少后续计算量。

(4)全连接层(Linear)

将前面提取到的特征汇总起来,最终判断图片属于哪个数字类别。

因此,一个简单的 CNN 可以按照下面的流程工作:

图片 → 卷积 → ReLU → 池化 → 卷积 → ReLU → 池化 → 全连接层 → 分类结果

接下来,我们就用代码实现这个过程。

2 环境准备

(1)python安装

python 3.11版本是目前兼容性比较好的版本,windows电脑可以通过以下链接下载:

https://www.python.org/ftp/python/3.13.11/python-3.13.11-amd64.exe

安装可以按照这样的步骤,对python比较熟悉的朋友可以按照自己的需求自定义安装:

如果出现 “ 你要允许此应用对你的设备进行更改吗? ” ,都点确认即可

(2)jupyter notebook安装

在搜索栏输入cmd,打开命令提示符,输入:

python -m pip install notebook

(3)安装运行CNN所需包

同样地,在命令提示符中输入:

python -m pip install torch torchvision scikit-learn matplotlib numpy

(4)启动jupyter notebook

在命令提示符中输入:

jupyter notebook

双击打开Desktop文件夹,即进入桌面,然后打开我们的ipynb文件:

下载链接:

通过网盘分享的文件:CNN_learning.ipynb

链接: https://pan.baidu.com/s/1ibVstR4qsGE0KJHVwP0PGg?pwd=p5ck 提取码: p5ck

--来自百度网盘超级会员v5的分享

3 导入工具

首先导入后面需要使用的 Python 库。

import numpy as np
import matplotlib
import matplotlib.pyplot as plt
import torch
from torch import nn
from torch.utils.data import TensorDataset, DataLoader
from sklearn.datasets import load_digits
from sklearn.model_selection import train_test_split
# Windows 常用中文字体:SimHei(黑体)、Microsoft YaHei(微软雅黑)、SimSun(宋体)
matplotlib.rcParams['font.sans-serif'] = ['SimHei', 'Microsoft YaHei', 'SimSun']
matplotlib.rcParams['axes.unicode_minus'] = False  # 正常显示负号
torch.manual_seed(42)
np.random.seed(42)

这段代码主要完成三件事:

1.导入数据处理、绘图、神经网络和数据集划分工具。

2.设置绘图字体,尽量避免中文标题显示成方框。

3.设置随机种子,让相同环境下的实验结果尽可能具有可重复性。

4 认识手写数字数据集

本教程使用 scikit-learn 自带的 digits 数据集这个数据已经包含在 scikit-learn 中,不需要另外下载图片文件,每张图片只有 8×8 个像素,标签为 0 到 9。

运行下面的代码:​​​

digits = load_digits()
X = (digits.images / 16.0).astype('float32')  # 像素缩放到 [0, 1]
y = digits.target.astype('int64')
print('图片形状:', X.shape, '标签形状:', y.shape)
fig, axes = plt.subplots(1, 5, figsize=(10, 2))
for i, ax in enumerate(axes):
    ax.imshow(X[i], cmap='gray_r')
    ax.set_title(f'标签:{y[i]}')
    ax.axis('off')
plt.tight_layout()
plt.show()

    (1)X 和 y 分别是什么?

    这里的 X 表示输入数据,也就是图片;y 表示每张图片对应的正确答案,运行后会看到类似这样的形状:

    图片形状: (1797, 8, 8)

    标签形状: (1797,)

    这意味着:一共有 1797 张图片,每张图片的大小为 8×8,每张图片对应一个数字标签。

    (2)为什么要除以 16?

    原始像素值的范围是 0 到 16。

    X = (digits.images / 16.0).astype('float32')

    就是将像素缩放到 0 到 1 之间。这样处理可以让神经网络更方便地学习数据中的规律。需要注意的是,本教程使用的是 8×8 的 digits 数据集,并不是常见的 28×28 的 MNIST 数据集。

    5 划分训练集和测试集

    在训练模型之前,需要把数据分成两部分:

    • 训练集:让模型学习图片和数字之间的关系。

    • 测试集:检查模型对没有参与训练的图片识别得怎么样。

    代码如下:

    X_train, X_test, y_train, y_test = train_test_split(
        X, y, test_size=0.2, random_state=42, stratify=y)
    train_ds = TensorDataset(torch.from_numpy(X_train[:, None, :, :]), torch.from_numpy(y_train))
    test_ds = TensorDataset(torch.from_numpy(X_test[:, None, :, :]), torch.from_numpy(y_test))
    train_loader = DataLoader(train_ds, batch_size=64, shuffle=True)
    test_loader = DataLoader(test_ds, batch_size=128)

    其中一些关键参数的意义如下:

    test_size=0.2

    表示 20% 的数据用于测试,剩余 80% 用于训练。

    stratify=y

    stratify是 “ 分层、分组 ” 的意思,stratify=y 表示尽量保证训练集和测试集中,各个数字类别的比例与原始数据接近。

    batch_size=64

    表示训练时,每次取 64 张图片进行计算。

    shuffle=True

    表示每轮训练时打乱训练数据的顺序。

    X_train[:, None, :, :]

    为什么中间是 None?

    因为 CNN 通常要求输入的形状为:

    [样本数, 通道数, 高, 宽]

    本例是灰度图,每张图片只有一个通道,所以需要把原来的图片数据转换成类似下面的形状:

    [样本数, 1, 8, 8]

    这个额外的维度,就是图片的通道维度。

    6 搭建CNN模型

    接下来进入整个教程的核心:构建神经网络。

    class SmallCNN(nn.Module):
        def __init__(self):
            super().__init__()
            self.net = nn.Sequential(
                nn.Conv2d(1, 8, kernel_size=3, padding=1), nn.ReLU(),
                nn.MaxPool2d(2),
                nn.Conv2d(8, 16, kernel_size=3, padding=1), nn.ReLU(),
                nn.MaxPool2d(2),
                nn.Flatten(),
                nn.Linear(16 * 2 * 2, 10)
            )
        def forward(self, x):
            return self.net(x)
    model = SmallCNN()
    print(model)

    被这些代码吓到,我们逐层理解:

    (1)第一层卷积

    nn.Conv2d(1, 8, kernel_size=3, padding=1)

    这行代码表示:

    • 输入通道数为 1,因为输入是灰度图。

    • 输出通道数为 8,表示学习 8 组特征。

    • 卷积窗口大小为 3×3。

    • padding=1 用于在边缘补充像素,使卷积后图片的高和宽保持不变。

    可以把它理解成:让网络从原始图片中寻找不同类型的局部特征。

    (2)ReLU 激活函数

    nn.ReLU()

    它会将负数变成 0,让网络能够学习更复杂的关系。

    (3)最大池化

    nn.MaxPool2d(2)

    这一步会将特征图的高和宽各缩小一半,即图片像素数从8×8 变为4×4,这样可以减少后续的计算量

    (4)第二层卷积

    nn.Conv2d(8, 16, kernel_size=3, padding=1)

    第二层接收前一层输出的 8 个特征通道,并进一步提取 16 个特征通道。在直观上,可以把它理解为:第一层先学习比较基础的图像特征,后续层再对这些特征进行组合和加工。

    (5)展平特征

    nn.Flatten()

    经过第二次池化后,特征图的尺寸为:16×2×2,展平后就变成一个长度为 64 的向量。

    (6)输出分类结果

    nn.Linear(16 * 2 * 2, 10)

    最后的全连接层输出 10 个分数,分别对应数字 0 到 9。模型会选择分数最高的数字作为预测结果。

    因此,本例的尺寸变化可以总结为:

    1×8×8          ↓ 第一层卷积8×8×8        ↓ 最大池化8×4×4          ↓ 第二层卷积16×4×4        ↓ 最大池化16×2×2    ↓ 展平64        ↓ 全连接层10 个数字类别的分数

    到这里,CNN 模型就搭建完成了。

    7 训练模型

    刚搭建好的模型还没有学会识别数字,需要通过训练不断调整参数。

    loss_fn = nn.CrossEntropyLoss()
    optimizer = torch.optim.Adam(model.parameters(), lr=0.01)
    for epoch in range(8):
        model.train()
        loss_sum = 0.0
        for images, labels in train_loader:
            optimizer.zero_grad()
            logits = model(images)
            loss = loss_fn(logits, labels)
            loss.backward()
            optimizer.step()
            loss_sum += loss.item() * len(labels)
        print(f'Epoch {epoch + 1:02d} | 平均损失: {loss_sum / len(train_ds):.4f}')

    这一段代码可以分成四个重要步骤:

    (1) 定义损失函数

    loss_fn = nn.CrossEntropyLoss()

    损失函数(loss_fn)用于衡量模型的预测与真实答案之间的差距,损失越小,通常表示模型在当前训练数据上的预测与真实标签越接近。

    这里使用的是交叉熵损失函数(CrossEntropyLoss)。

    交叉熵来自信息论,用于衡量两个概率分布p(真实)与q(预测)之间的差异,当预测分布与真实分布一致时,交叉熵最小。

    (2)定义优化器

    optimizer = torch.optim.Adam(model.parameters(), lr=0.01)

    优化器负责根据梯度调整模型参数。

    其中:

    • model.parameters():需要训练的模型参数。

    • lr=0.01:学习率,控制每次参数更新的步幅。

    (3)前向传播和反向传播

    logits = model(images)
    loss = loss_fn(logits, labels)
    loss.backward()
    optimizer.step()

    这是神经网络训练中最核心的过程:首先,模型接收图片,输出 10 个类别的分数,然后,损失函数计算预测与真实标签之间的差距,接着, loss.backward() 计算各个参数的梯度,最后, optimizer.step() 根据梯度更新参数,让模型逐步改进预测能力。 在此之前:

    在此之前:

    optimizer.zero_grad()

    会清空上一轮迭代留下的梯度。

    (4)什么是 Epoch?

    for epoch in range(8):

    这里的 8 表示训练 8 轮,一个 Epoch 指模型完整遍历一次训练集。每轮结束后,代码都会打印平均损失,方便观察模型的学习过程。

    在 Notebook 的一次运行中,训练损失从第一轮约 2.08 下降到了第八轮约 0.06,这说明模型在训练过程中逐步学到了数据中的规律。

    不过,训练损失下降并不代表模型一定具有良好的泛化能力,还需要通过测试集进一步验证。

    8 测试模型的识别效果

    训练结束后,我们要检查模型对未参与训练的数据识别得怎么样。

    model.eval()
    correct = total = 0
    with torch.no_grad():
        for images, labels in test_loader:
            preds = model(images).argmax(dim=1)
            correct += (preds == labels).sum().item()
            total += labels.size(0)
    print(f'测试准确率:{100 * correct / total:.2f}%')
    
    

    这段代码中有三个重要概念。

    model.eval()

    将模型切换到评估模式。

    torch.no_grad()

    告诉 PyTorch,当前不需要计算梯度,可以减少不必要的计算和内存开销。

    argmax(dim=1)

    在每张图片对应的 10 个类别分数中,选择分数最高的类别作为预测结果。

    最终,测试准确率的计算方式为:

    准确率 = 预测正确的图片数量 ÷ 测试图片总数 × 100%

    运行结果为:

    测试准确率:96.94%

    也就是说,在这次测试中,大约 97% 的测试图片被正确识别。

    需要说明的是,这是一个小型教学数据集上的结果,不能直接代表模型在真实世界手写图片上的识别效果。

    9 预测结果

    除了看准确率,我们还可以直接观察模型预测对不对。

    with torch.no_grad():
        images, labels = next(iter(test_loader))
        preds = model(images).argmax(dim=1)
    fig, axes = plt.subplots(2, 5, figsize=(10, 4))
    for i, ax in enumerate(axes.flat):
        ax.imshow(images[i, 0], cmap='gray_r')
        ax.set_title(f'预测 {preds[i].item()} / 真实 {labels[i].item()}')
        ax.axis('off')
    plt.tight_layout()
    plt.show()
    
    

    运行后会得到 10 张手写数字图片,每张图片上方都有两个数字:

    • 预测:模型认为这张图片是什么数字。

    • 真实:数据集给出的正确答案。

    如果两者相同,说明这张图片识别正确;如果不同,就说明模型在这个样本上判断错误。

    通过这种方式,我们不仅能看到一个总体准确率,还能直观地了解模型的预测结果。

    10 练习

    练习 1:增加训练轮数

    把:

    for epoch in range(15):

    改成:

    for epoch in range(8):

    观察训练损失和测试准确率是否发生变化。

    答:训练轮数增加后,训练损失进一步下降,测试准确率也略有提高。

    但要注意:增加训练轮数不一定会提高测试准确率。如果训练过度,模型可能会过拟合,也就是越来越熟悉训练数据,却不一定更擅长识别新图片。

    练习 2:修改卷积通道数

    把第一层卷积的输出通道数从 8 改成 16。思考一下:第二层卷积的输入通道数需要怎样调整?

    答:输入通道也要从8改为16

    练习 3:思考 CNN 为什么适合图像

    如果直接把所有像素输入全连接层,模型还能像 CNN 一样利用像素之间的局部空间关系吗?
    试着从卷积窗口、局部特征和参数共享三个角度思考这个问题。

    答:普通全连接神经网络也能学习图像特征,但它不会像 CNN 一样,天然地利用图像像素之间的局部空间关系。CNN 的优势主要体现在卷积窗口、局部特征和参数共享三个方面:

    普通全连接神经网络将图像像素展平后进行连接,虽然也能学习像素之间的关系,但没有利用局部空间结构。

    CNN 通过卷积窗口提取局部特征,通过多层卷积组合复杂特征,并利用参数共享机制在不同位置识别相同模式。

    因此,CNN 通常更适合图像识别任务,且在具有局部空间规律的图像任务中往往具有更高的参数利用效率。

    结语

    通过这个小项目,我们已经走完了一个典型深度学习任务的主要流程:

    准备数据 → 划分训练集和测试集 → 搭建模型 → 定义损失函数和优化器 → 训练 → 测试 → 查看预测结果。

    虽然我们只识别了 8×8 的手写数字,但这里的基本思想同样可以帮助你理解更复杂的图像分类模型。

    作者:bioinforiver(CSDN同名)

    2026年10月24日

    本系列内容来源于网络,仅用于分享学习,不得用于任何商业用途。

    参考文献:

    1.交叉熵损失CrossEntropyLoss,湾仔码农,2023年2月20日,https://www.cnblogs.com/Fish0403/p/17073047.html

    评论
    成就一亿技术人!
    拼手气红包6.0元
    还能输入1000个字符
     
     条评论被折叠 查看
    添加红包

    请填写红包祝福语或标题

    个

    红包个数最小为10个

    元

    红包金额最低5元

    当前余额3.43元 前往充值 >
    需支付:10.00元
    成就一亿技术人!
    领取后你会自动成为博主和红包主的粉丝 规则
    hope_wisdom
    发出的红包
    实付元
    使用余额支付
    点击重新获取
    扫码支付
    钱包余额 0

    抵扣说明:

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

    余额充值