通过代码认识 CNN:用 PyTorch 实现卷积神经网络识别手写数字

目录

一、从代码看 CNN 的核心组件

二、准备工作:库导入与数据加载

三、核心:用代码实现 CNN 并理解各层作用

1.网络层结构

2.重点理解:卷积层参数与输出尺寸计算

四、训练 CNN

五、结果分析


卷积神经网络(CNN)是计算机视觉领域的核心模型,相比全连接网络,它能更高效地提取图像特征。本文不空谈理论,而是通过 PyTorch 代码实现一个完整的 CNN 模型,带你在实战中理解卷积、池化等核心概念,掌握 CNN 的工作原理。


一、从代码看 CNN 的核心组件

在实现模型前,先明确 CNN 的三个核心层 —— 这些是区别于全连接网络的关键,后续代码会逐一对应:

  1. 卷积层(Conv2d):通过滑动窗口提取局部特征(如边缘、纹理);
  2. 激活层(ReLU):引入非线性,让模型学习复杂模式;
  3. 池化层(MaxPool2d):降低特征图尺寸,减少计算量,增强鲁棒性。

我们将用这些组件构建一个识别 MNIST 手写数字的 CNN 模型,边写代码边解释原理。


二、准备工作:库导入与数据加载

首先导入必要的库,加载 MNIST 数据集(28×28 的手写数字图片):

import torch
from torch import nn
from torch.utils.data import DataLoader
from torchvision import datasets
from torchvision.transforms import ToTensor

# 加载MNIST数据集
training_data = datasets.MNIST(
    root="data",
    train=True,
    download=True,
    transform=ToTensor()  # 转为张量,形状为[1,28,28]
)
test_data = datasets.MNIST(
    root="data",
    train=False,
    download=True,
    transform=ToTensor()
)

# 按批次加载数据(每批64张图)
batch_size = 64
train_dataloader = DataLoader(training_data, batch_size=batch_size)
test_dataloader = DataLoader(test_data, batch_size=batch_size)

# 查看数据形状([批次, 通道, 高度, 宽度])
for X, y in test_dataloader:
    print(f"数据形状: {X.shape}")  # 输出:torch.Size([64, 1, 28, 28])
    break

关键说明:MNIST 图片是单通道(灰度图),所以输入形状为[N,1,28,28](N 为批次大小),这会影响后续卷积层的参数设置。


三、核心:用代码实现 CNN 并理解各层作用

1.网络层结构

我们构建一个包含 4 个卷积块的 CNN 模型,每个卷积块由 “卷积层 + 激活层” 组成,部分块后添加池化层。通过代码注释详细说明每层的作用和参数含义。

# 自动选择设备(优先GPU)
device = "cuda" if torch.cuda.is_available() else "cpu"
print(f"使用设备: {device}")

class CNN(nn.Module):
    def __init__(self):
        super(CNN, self).__init__()
        # 第一个卷积块:卷积层+激活层+池化层
        self.conv1 = nn.Sequential(
            # 卷积层:输入1通道,输出16通道,卷积核5×5,步长1,填充2
            nn.Conv2d(
                in_channels=1,    # 输入通道数(灰度图为1)
                out_channels=16,  # 输出通道数(16个不同的卷积核)
                kernel_size=5,    # 卷积核大小5×5
                stride=1,         # 步长1(每次滑动1个像素)
                padding=2         # 填充2(保持输出尺寸与输入一致:28→28)
            ),
            nn.ReLU(),  # 激活层:引入非线性,过滤负值
            # 池化层:2×2窗口,步长2,输出尺寸变为14×14(28/2)
            nn.MaxPool2d(kernel_size=2, stride=2)
        )
        
        # 第二个卷积块:卷积层+激活层(无池化)
        self.conv2 = nn.Sequential(
            # 输入16通道(上一层输出),输出32通道,卷积核3×3
            nn.Conv2d(16, 32, kernel_size=3, stride=1, padding=1),
            nn.ReLU()  # 输出尺寸保持14×14
        )
        
        # 第三个卷积块:卷积层+激活层+池化层
        self.conv3 = nn.Sequential(
            nn.Conv2d(32, 64, kernel_size=3, stride=1, padding=1),
            nn.ReLU(),
            nn.MaxPool2d(2, 2)  # 输出尺寸变为7×7(14/2)
        )
        
        # 第四个卷积块:卷积层+激活层(无池化)
        self.conv4 = nn.Sequential(
            nn.Conv2d(64, 128, kernel_size=3, stride=1, padding=1),
            nn.ReLU()  # 输出尺寸保持7×7
        )
        
        # 全连接层:将特征图转为10个类别(0-9)
        self.fc = nn.Linear(128 * 7 * 7, 10)  # 128通道×7×7尺寸

    def forward(self, x):
        # 前向传播:数据依次经过各层
        x = self.conv1(x)  # 输出形状:[N,16,14,14]
        x = self.conv2(x)  # 输出形状:[N,32,14,14]
        x = self.conv3(x)  # 输出形状:[N,64,7,7]
        x = self.conv4(x)  # 输出形状:[N,128,7,7]
        x = x.view(x.size(0), -1)  # 展平:[N,128×7×7]
        x = self.fc(x)     # 输出形状:[N,10](10个类别分数)
        return x

# 创建模型并移动到设备
model = CNN().to(device)
print("CNN模型结构:")
print(model)

2.重点理解:卷积层参数与输出尺寸计算

以第一个卷积层为例,输入是[64,1,28,28](64 张图,1 通道,28×28),经过kernel_size=5, padding=2, stride=1的卷积后,输出尺寸计算公式:

输出尺寸 = (输入尺寸 - 卷积核大小 + 2×填充) / 步长 + 1
即:(28 - 5 + 2×2)/1 + 1 = 28

所以输出仍为 28×28,再经 2×2 池化后变为 14×14—— 这就是卷积层如何在保留特征的同时控制尺寸的核心逻辑。


四、训练 CNN

CNN 的训练流程和全连接网络类似,我们将训练轮次调整为 10 轮,既能保证模型收敛,又能节省训练时间。定义训练和测试函数如下:

# 损失函数(多分类用交叉熵)
loss_fn = nn.CrossEntropyLoss()
# 优化器(Adam,学习率0.0001)
optimizer = torch.optim.Adam(model.parameters(), lr=0.0001)

# 训练函数
def train(dataloader, model, loss_fn, optimizer):
    model.train()  # 开启训练模式
    batch_num = 1
    for X, y in dataloader:
        X, y = X.to(device), y.to(device)
        
        # 前向传播:计算预测
        pred = model(X)
        loss = loss_fn(pred, y)
        
        # 反向传播:更新参数
        optimizer.zero_grad()  # 梯度清零
        loss.backward()        # 计算梯度
        optimizer.step()       # 更新参数
        
        # 每100批次打印一次损失
        if batch_num % 100 == 1:
            print(f"批次 {batch_num} | 损失: {loss.item():.4f}")
        batch_num += 1

# 测试函数
def test(dataloader, model, loss_fn):
    model.eval()  # 开启测试模式
    size = len(dataloader.dataset)
    num_batches = len(dataloader)
    correct = 0
    test_loss = 0
    
    with torch.no_grad():  # 禁用梯度计算
        for X, y in dataloader:
            X, y = X.to(device), y.to(device)
            pred = model(X)
            test_loss += loss_fn(pred, y).item()
            correct += (pred.argmax(1) == y).type(torch.float).sum().item()
    
    # 计算准确率和平均损失
    test_loss /= num_batches
    correct /= size
    print(f"\n测试集:准确率 {100*correct:.2f}% | 平均损失 {test_loss:.4f}\n")

# 开始训练(10轮)
print("="*50)
print("开始训练CNN模型(10轮)")
print("="*50)
for epoch in range(10):
    print(f"轮次 {epoch+1}/10")
    print("-"*30)
    train(train_dataloader, model, loss_fn, optimizer)
    # 每2轮测试一次
    if (epoch+1) % 2 == 0:
        test(test_dataloader, model, loss_fn)
print("="*50)
print("训练结束")

五、结果分析

轮次 10/10
------------------------------
批次 1 | 损失: 0.0002
批次 101 | 损失: 0.0000
批次 201 | 损失: 0.0015
批次 301 | 损失: 0.0190
批次 401 | 损失: 0.0003
批次 501 | 损失: 0.0008
批次 601 | 损失: 0.0001
批次 701 | 损失: 0.0065
批次 801 | 损失: 0.0019
批次 901 | 损失: 0.0310

测试集:准确率 99.17% | 平均损失 0.0355

==================================================
训练结束

即使只训练 10 轮,CNN 在测试集上的准确率通常也能达到99% 以上,明显高于同轮次的全连接网络。这体现了 CNN 的高效性,原因在于:

  1. 局部感受野:卷积层通过滑动窗口只关注局部像素,更符合图像的局部相关性;
  2. 权值共享:同一通道的卷积核参数共享,大幅减少参数数量(全连接层 784→128 需要近 10 万个参数,而 5×5 的卷积层 1→16 仅需 400 个参数);
  3. 池化层:通过下采样保留关键特征,增强模型对图像位移、缩放的鲁棒性。

这些特性让 CNN 在较少的训练轮次下就能达到较好的性能。

首先需要安装PyTorch和torchvision库。可以使用以下命令进行安装: ```python !pip install torch torchvision ``` 接下来是代码实现: ```python import torch import torch.nn as nn import torch.optim as optim import torchvision.datasets as datasets import torchvision.transforms as transforms # 定义卷积神经网络 class CNN(nn.Module): def __init__(self): super(CNN, self).__init__() self.conv1 = nn.Conv2d(1, 32, kernel_size=3, padding=1) self.conv2 = nn.Conv2d(32, 64, kernel_size=3, padding=1) self.pool = nn.MaxPool2d(kernel_size=2, stride=2) self.fc1 = nn.Linear(64 * 7 * 7, 128) self.fc2 = nn.Linear(128, 10) def forward(self, x): x = self.conv1(x) x = nn.functional.relu(x) x = self.pool(x) x = self.conv2(x) x = nn.functional.relu(x) x = self.pool(x) x = x.view(-1, 64 * 7 * 7) x = self.fc1(x) x = nn.functional.relu(x) x = self.fc2(x) return x # 加载数据集 train_dataset = datasets.MNIST(root='./data', train=True, transform=transforms.ToTensor(), download=True) test_dataset = datasets.MNIST(root='./data', train=False, transform=transforms.ToTensor()) # 定义训练参数 batch_size = 64 learning_rate = 0.01 num_epochs = 10 # 加载数据集并定义数据加载器 train_loader = torch.utils.data.DataLoader(dataset=train_dataset, batch_size=batch_size, shuffle=True) test_loader = torch.utils.data.DataLoader(dataset=test_dataset, batch_size=batch_size, shuffle=False) # 定义模型、损失函数和优化器 model = CNN() criterion = nn.CrossEntropyLoss() optimizer = optim.Adam(model.parameters(), lr=learning_rate) # 训练模型 for epoch in range(num_epochs): for i, (images, labels) in enumerate(train_loader): # 前向传播 outputs = model(images) loss = criterion(outputs, labels) # 后向传播和优化 optimizer.zero_grad() loss.backward() optimizer.step() # 打印训练信息 if (i + 1) % 100 == 0: print('Epoch [{}/{}], Step [{}/{}], Loss: {:.4f}'.format(epoch + 1, num_epochs, i + 1, len(train_loader), loss.item())) # 测试模型 with torch.no_grad(): correct = 0 total = 0 for images, labels in test_loader: outputs = model(images) _, predicted = torch.max(outputs.data, 1) total += labels.size(0) correct += (predicted == labels).sum().item() print('Accuracy of the network on the 10000 test images: {} %'.format(100 * correct / total)) ``` 此代码实现了一个简单的卷积神经网络,用于识别手写数字。使用MNIST数据集进行训练和测试。可以通过调整超参数来提高模型的准确性。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值