当前位置: 首页 > ds >正文

搭建卷积神经网络

卷积神经网络(CNN)讲解:从原理到 MNIST 实战

在深度学习领域,卷积神经网络(Convolutional Neural Network, CNN)是专门为图像任务设计的神经网络架构。它通过模拟人类视觉系统的 “局部感知” 特性,解决了传统全连接网络处理图像时 “参数爆炸” 和 “忽略空间信息” 的痛点。本文将结合之前的 MNIST 手写数字识别代码,从核心原理、组件拆解、数据流动到训练逻辑,全面讲解 CNN 的工作机制。

一、为什么需要 CNN?—— 图像任务的特殊性
以 MNIST 数据集为例,每张图像是28×28 的灰度图(1 个通道)。若用全连接网络,输入层需要 28×28=784 个神经元;若第一层隐藏层设为 1000 个神经元,仅这一层的参数就有 784×1000=78.4万 个。而 CNN 通过两个核心机制大幅减少参数,同时保留图像的空间特征:

  1. 局部感知:人类看图像时先关注局部(如边缘、纹理),再组合成整体。CNN 的卷积层仅用 “卷积核”(小窗口)提取局部特征,而非关注整个图像。
  2. 参数共享:一个卷积核在整个图像上滑动时,使用同一组参数(权重),避免为每个像素位置单独设置参数。

二、CNN 的核心组件:拆解代码中的网络结构

# 导入PyTorch核心库
import torch
# 从torchvision导入数据集模块,用于加载MNIST等标准数据集
from torchvision import datasets
# 从torch导入神经网络模块(nn)、优化器模块(optim)
from torch import nn, optim
# 导入数据加载器,用于批量加载数据
from torch.utils.data import DataLoader
# 导入ToTensor转换工具,用于将图像转换为PyTorch张量
from torchvision.transforms import ToTensor# 加载MNIST手写数字数据集(训练集)
training_data = datasets.MNIST(root='data',          # 数据存储路径train=True,           # 加载训练集download=True,        # 如果本地没有数据则自动下载transform=ToTensor()  # 数据转换:将PIL图像转为张量,并归一化到[0,1]范围
)# 加载MNIST手写数字数据集(测试集)
test_data = datasets.MNIST(root='data',          # 数据存储路径train=False,          # 加载测试集download=True,        # 如果本地没有数据则自动下载transform=ToTensor()  # 同样进行张量转换
)# 打印数据集大小,确认数据加载成功
print(f"训练集大小: {len(training_data)}, 测试集大小: {len(test_data)}")# 创建训练集数据加载器
train_dataloader = DataLoader(training_data, batch_size=64,  # 每次迭代加载64个样本shuffle=True    # 训练时打乱数据顺序,增强模型泛化能力
)# 创建测试集数据加载器
test_dataloader = DataLoader(test_data, batch_size=64,  # 测试时同样使用64的批次大小shuffle=True    # 测试时打乱数据(非必须,这里仅为演示)
)# 查看数据集中样本的形状,确认数据格式正确
for x, y in test_dataloader:# x: 输入图像张量,形状为[batch_size, 通道数, 高度, 宽度]# y: 标签张量,形状为[batch_size]print(f"输入形状: {x.shape}, 标签形状: {y.shape}")break  # 只查看第一个批次# 自动判断并选择可用的计算设备,优先使用GPU加速
device = torch.device('cuda' if torch.cuda.is_available() else  # 检查NVIDIA GPU'mps' if torch.backends.mps.is_available() else  # 检查Apple M系列芯片'cpu'  # 都不支持则使用CPU
)
print(f"使用设备: {device}")# 定义卷积神经网络(CNN)模型,继承自nn.Module
class CNN(nn.Module):def __init__(self):super(CNN, self).__init__()  # 调用父类构造函数# 第一个卷积块:卷积层 + ReLU激活 + 最大池化self.conv1 = nn.Sequential( #Sequential创建一个容器,将多个层组合在一起nn.Conv2d(              #2d用于处理图像in_channels=1,      # 输入通道数:MNIST是灰度图,所以为1out_channels=32,    # 输出通道数:32个卷积核,提取32种特征kernel_size=5,      # 卷积核大小:5x5stride=1,           # 步长:1,每次移动1个像素padding=2           # 填充:2,保持卷积后特征图大小不变),nn.ReLU(),             # 激活函数:将数据进行非线性映射nn.MaxPool2d(2)        # 最大池化:2x2窗口,将特征图尺寸缩小一半)# 第二个卷积块:两个卷积层 + ReLU激活 + 最大池化self.conv2 = nn.Sequential(# 第一个卷积:32->32通道nn.Conv2d(in_channels=32, out_channels=32, kernel_size=5, stride=1, padding=2),nn.ReLU(),# 第二个卷积:32->64通道nn.Conv2d(in_channels=32, out_channels=64, kernel_size=5, stride=1, padding=2),nn.ReLU(),nn.MaxPool2d(2)        # 再次池化,特征图尺寸再缩小一半)# 第三个卷积块:单个卷积层 + ReLU激活(无池化,保留特征图尺寸)self.conv3 = nn.Sequential(nn.Conv2d(in_channels=64, out_channels=128, kernel_size=5, stride=1, padding=2),nn.ReLU(),)# 全连接层:将卷积提取的特征映射到10个类别(0-9)self.out = nn.Linear(in_features=128 * 7 * 7, out_features=10)# 输入特征数计算:128通道,每个特征图7x7(原始28x28经两次池化后变为7x7)# 前向传播函数:定义数据在网络中的流动路径def forward(self, x):x = self.conv1(x)   # 经过第一个卷积块x = self.conv2(x)   # 经过第二个卷积块x = self.conv3(x)   # 经过第三个卷积块x = x.view(x.size(0), -1)  # 展平操作:将四维张量[batch, 128, 7, 7]转为二维[batch, 128*7*7]output = self.out(x)       # 经过全连接层得到最终输出return output# 初始化模型并将其移动到之前选择的计算设备上
model = CNN().to(device)# 训练函数:负责模型的训练过程
def train(dataloader, model, loss_fn, optimizer, epoch, device):model.train()  # 设置模型为训练模式(启用 dropout、批量归一化等训练特有的层)total_loss = 0.0  # 记录总损失batch_size_num = 1  # 批次计数器# 遍历数据加载器中的每个批次for x, y in dataloader:# 将输入和标签移动到计算设备x = x.to(device)y = y.to(device)# 前向传播:计算模型预测值pred = model(x)# 计算损失:预测值与真实标签的差距loss = loss_fn(pred, y)# 反向传播与参数优化optimizer.zero_grad()  # 清零梯度,避免梯度累积loss.backward()        # 反向传播计算梯度optimizer.step()       # 更新模型参数# 累加损失total_loss += loss.item()# 每100个批次打印一次损失,监控训练过程if batch_size_num % 100 == 0:print(f"Epoch {epoch}, Batch {batch_size_num}, Loss: {loss.item():.7f}")batch_size_num += 1# 计算并打印该轮的平均损失avg_loss = total_loss / len(dataloader)print(f"Epoch {epoch} 训练平均损失: {avg_loss:.7f}")return avg_loss  # 返回平均损失用于后续分析# 测试函数:评估模型在测试集上的性能
def test(dataloader, model, loss_fn, epoch, device):size = len(dataloader.dataset)  # 测试集总样本数num_batches = len(dataloader)   # 测试集批次数model.eval()                    # 设置模型为评估模式(关闭 dropout 等)total_loss = 0.0                # 总测试损失correct = 0                     # 正确预测的样本数# 关闭梯度计算(测试时不需要更新参数,节省内存和计算资源)with torch.no_grad():# 遍历测试集中的每个批次for x, y in dataloader:x = x.to(device)y = y.to(device)pred = model(x)  # 前向传播获取预测值# 累加测试损失total_loss += loss_fn(pred, y).item()# 计算正确预测数:取预测概率最大的类别与真实标签比较correct += (pred.argmax(1) == y).type(torch.float).sum().item()# 计算平均损失和准确率avg_loss = total_loss / num_batchesaccuracy = correct / sizeprint(f"Epoch {epoch} 测试平均损失: {avg_loss:.7f}, 准确率: {accuracy * 100:.2f}%\n")return avg_loss, accuracy  # 返回测试损失和准确率用于后续分析# 定义损失函数:交叉熵损失,适用于分类任务
loss_fn = nn.CrossEntropyLoss()
# 定义优化器:Adam优化器,学习率为0.001
optimizer = torch.optim.Adam(model.parameters(), lr=0.001)# 训练参数设置
epochs = 10  # 训练轮次
# 用于记录训练过程中的指标,便于后续可视化
train_losses = []      # 训练损失
test_losses = []       # 测试损失
test_accuracies = []   # 测试准确率# 开始训练循环
for epoch in range(1, epochs + 1):print(f"\nEpoch {epoch}/{epochs}")print("-" * 50)  # 分隔线,美化输出# 训练模型并记录训练损失train_loss = train(train_dataloader, model, loss_fn, optimizer, epoch, device)train_losses.append(train_loss)# 测试模型并记录测试损失和准确率test_loss, test_acc = test(test_dataloader, model, loss_fn, epoch, device)test_losses.append(test_loss)test_accuracies.append(test_acc)print("训练完成!")
# 打印模型结构,确认网络配置
print(model)

以上的代码定义了一个 3 个卷积块 + 1 个全连接层的 CNN 模型(class CNN(nn.Module)),我们逐一拆解每个核心组件的作用,以及代码中的具体实现。

1. 卷积层(Conv2d):提取图像局部特征

卷积层是 CNN 的 “眼睛”,负责从图像中提取低级特征(如边缘、线条)和高级特征(如轮廓、形状)。

代码中的卷积层示例(以conv1为例):

python

nn.Conv2d(in_channels=1,      # 输入通道数:MNIST是灰度图,仅1个通道(彩色图为3)out_channels=32,    # 输出通道数:32个卷积核,提取32种不同特征kernel_size=5,      # 卷积核大小:5×5的正方形窗口(局部感知范围)stride=1,           # 步长:卷积核每次滑动1个像素padding=2           # 填充:在图像边缘补2个像素,确保卷积后尺寸不变
)
关键计算:卷积后特征图的尺寸

卷积层输出的 “特征图” 尺寸是核心指标,公式为:
输出尺寸 = (输入尺寸 - 卷积核尺寸 + 2×padding) / stride + 1
conv1为例:
输入尺寸 = 28,卷积核 = 5,padding=2,stride=1
输出尺寸 = (28 - 5 + 2×2)/1 + 1 = 28 → 特征图仍为 28×28,保证空间信息不丢失。

2. 激活函数(ReLU):引入非线性特征

卷积层的输出是 “线性组合”(类似y=wx+b),无法捕捉图像中的复杂非线性关系(如曲线、不规则边缘)。激活函数的作用是给特征图加入非线性,让 CNN 能学习更复杂的特征。

代码中的实现:

python

nn.ReLU()  # 最常用的激活函数,公式:ReLU(x) = max(0, x)

ReLU 的优势:计算简单(避免梯度消失)、能快速收敛,是图像任务的首选激活函数。

3. 池化层(MaxPool2d):降维与特征浓缩

池化层(又称下采样层)的核心作用是减少特征图尺寸,从而降低计算量和过拟合风险,同时保留关键特征(如 “最大值” 对应最显著的局部特征)。

代码中的实现(以conv1后的池化为例):

python

nn.MaxPool2d(2)  # 2×2的最大池化窗口,步长默认等于窗口大小
关键计算:池化后尺寸

最大池化会将2×2的窗口压缩为 1 个像素(取窗口内最大值),因此尺寸直接减半:
conv1输出的 28×28 特征图,经过MaxPool2d(2)后,尺寸变为 14×14

4. 卷积块:层的 “组合拳”

代码中没有单独使用卷积层,而是将 “卷积层 + 激活函数 + 池化层”(或多卷积层 + 激活函数)组合成卷积块(如conv1conv2conv3),这是 CNN 的经典设计范式:

  • conv1:1 个卷积层 + ReLU + 最大池化 → 提取最基础的边缘特征
  • conv2:2 个卷积层 + ReLU + 最大池化 → 组合基础特征,提取轮廓特征
  • conv3:1 个卷积层 + ReLU → 进一步细化高级特征(无池化,避免过度降维)

5. 全连接层(Linear):从特征到分类

经过多轮卷积和池化后,特征图已经浓缩了图像的关键信息,但仍需通过全连接层将 “空间特征” 转化为 “类别概率”。

代码中的实现:

python

self.out = nn.Linear(in_features=128 * 7 * 7, out_features=10)

  • in_features=128×7×7:输入特征数。conv3输出的特征图是128通道×7×7尺寸(怎么来的?下文数据流动会讲),需要通过x.view(x.size(0), -1)展平为 1 维向量(batch_size × 128×7×7)。
  • out_features=10:输出特征数 = MNIST 的类别数(0-9),最终输出每个类别的 “logits 分数”(通过 Softmax 可转化为概率)。

三、CNN 的数据流动:跟着代码走一遍

理解 CNN 的关键是跟踪数据在网络中的尺寸变化。我们以代码中的forward函数为线索,从输入(28×28 的 MNIST 图像)到输出(10 个类别分数),完整梳理数据流动过程:

输入:MNIST 图像张量

输入数据x的形状为 [batch_size, 1, 28, 28](批量大小 × 通道数 × 高度 × 宽度),对应代码中test_dataloader打印的 “输入形状: torch.Size ([64, 1, 28, 28])”(batch_size=64)。

1. 经过conv1:基础特征提取

python

x = self.conv1(x)  # conv1 = 卷积层(1→32) + ReLU + MaxPool2d(2)

  • 卷积层后:[64, 32, 28, 28](通道从 1→32,尺寸保持 28×28)
  • ReLU 后:形状不变,仅值做非线性变换 → [64, 32, 28, 28]
  • 最大池化后:尺寸减半 → [64, 32, 14, 14]

2. 经过conv2:轮廓特征提取

python

x = self.conv2(x)  # conv2 = 卷积层(32→32) + ReLU + 卷积层(32→64) + ReLU + MaxPool2d(2)

  • 第一个卷积层(32→32):尺寸不变 → [64, 32, 14, 14]
  • ReLU 后:形状不变 → [64, 32, 14, 14]
  • 第二个卷积层(32→64):尺寸不变 → [64, 64, 14, 14]
  • ReLU 后:形状不变 → [64, 64, 14, 14]
  • 最大池化后:尺寸减半 → [64, 64, 7, 7]

3. 经过conv3:高级特征细化

python

x = self.conv3(x)  # conv3 = 卷积层(64→128) + ReLU

  • 卷积层(64→128):尺寸不变(padding=2) → [64, 128, 7, 7]
  • ReLU 后:形状不变 → [64, 128, 7, 7]

4. 展平(view):适配全连接层

python

x = x.view(x.size(0), -1)  # 将4维张量展平为2维

  • x.size(0):批量大小(64)
  • -1:自动计算剩余维度 → 128×7×7=6272
  • 展平后形状:[64, 6272]

5. 经过全连接层:输出类别分数

python

output = self.out(x)  # 全连接层(6272→10)

  • 输出形状:[64, 10] → 每个样本对应 10 个类别(0-9)的分数。

四、CNN 的训练与评估:代码中的核心逻辑

CNN 的训练流程与其他神经网络一致,但需注意 “训练模式” 和 “评估模式” 的区别,以及梯度计算的开关。我们结合代码中的traintest函数讲解:

1. 训练函数(train):更新模型参数

训练的核心是 “梯度下降”—— 通过反向传播计算参数梯度,再用优化器更新参数,最小化损失。

关键步骤:

python

model.train()  # 设为训练模式:启用Dropout、BatchNorm的训练逻辑
for x, y in dataloader:x, y = x.to(device), y.to(device)  # 数据移到GPU/CPUpred = model(x)  # 前向传播:计算预测值loss = loss_fn(pred, y)  # 计算损失(CrossEntropyLoss适配分类任务)# 反向传播与优化optimizer.zero_grad()  # 清零梯度(避免累积)loss.backward()        # 反向传播:计算各层梯度optimizer.step()       # 优化器更新参数(如Adam)

  • 损失函数:nn.CrossEntropyLoss() → 同时包含 “Softmax” 和 “交叉熵”,直接输入 logits 即可。
  • 优化器:torch.optim.Adam() → 自适应学习率,收敛快,适合 CNN 训练。

2. 评估函数(test):验证模型性能

评估时不需要更新参数,因此需关闭梯度计算,避免内存浪费;同时需切换到 “评估模式”。

关键步骤:

python

model.eval()  # 设为评估模式:关闭Dropout、固定BatchNorm参数
with torch.no_grad():  # 关闭梯度计算for x, y in dataloader:pred = model(x)total_loss += loss_fn(pred, y).item()  # 累加测试损失# 计算准确率:取预测概率最大的类别(pred.argmax(1))与真实标签比较correct += (pred.argmax(1) == y).type(torch.float).sum().item()

  • 准确率计算:correct / len(dataloader.dataset) → 正确预测数 / 总样本数。

3. 训练效果:预期结果

在 MNIST 数据集上,该 CNN 模型训练 10 轮后:

  • 训练损失会从初始的~2.3(随机猜测水平)下降到~0.0001
  • 测试准确率会达到 99% 以上 → 充分说明 CNN 对图像任务的有效性。

五、总结:CNN 为什么能搞定图像任务?

从原理到代码,我们可以总结出 CNN 的核心优势:

  1. 局部感知 + 参数共享:大幅减少参数数量,避免过拟合,降低计算成本。
  2. 特征层级提取:从低级特征(边缘)到高级特征(形状),逐步抽象,符合人类视觉认知。
  3. 空间信息保留:池化层在降维的同时保留关键空间特征,而全连接网络会丢失空间关系。
http://www.xdnf.cn/news/19522.html

相关文章:

  • 软考 系统架构设计师系列知识点之杂项集萃(139)
  • C++11语言(三)
  • Nginx实现P2P视频通话
  • codecombat(Ubuntu环境详细docker部署教程)
  • 项目-云备份
  • 面试 八股文 经典题目 - HTTPS部分(一)
  • Flink NettyBufferPool
  • 大模型时代:用Redis构建百亿级向量数据库方
  • EtherCAT主站IGH-- 41 -- IGH之sdo_request.h/c文件解析
  • Library cache lock常见案例分析(一)
  • Encoder编码器
  • 图像描述编辑器 (Image Caption Editor)
  • 极客时间AI 全栈开发实战营毕业总结(2025年8月31日)
  • 【Linux基础】深入理解计算机存储:GPT分区表详解
  • 前端组件拆分与管理实战:如何避免 props 地狱,写出高可维护的项目
  • 《Unity Shader入门精要》学习笔记四(高级纹理)
  • ing Data JPA 派生方法 数据操作速查表
  • 【WEB】[BUUCTF] <GXYCTF2019禁止套娃>《php函数的运用》
  • ADC platfrom day65
  • MVC架构模式
  • Blender建模:对于模型布线的一些思考
  • 介绍GSPO:一种革命性的语言模型强化学习算法
  • 现代C++性能陷阱:std::function的成本、异常处理的真实开销
  • Luma 视频生成 API 对接说明
  • AI 智能体汇总,自动执行任务的“真 Agent”
  • 查看所有装在c盘软件的方法
  • Trae接入自有Deepseek模型,不再排队等待
  • OpenStack 03:创建实例
  • 并发编程——11 并发容器(Map、List、Set)实战及其原理分析
  • Opencv的数据结构