一、为什么需要 CNN?从图像识别的 “麻烦” 说起
假设你想让电脑识别一张图片里有没有猫。
如果用传统神经网络:
- 一张 100×100 的彩色图片,有 100×100×3=30000 个像素点,每个像素点都是一个输入神经元。
- 传统网络需要每个输入神经元和隐藏层神经元全连接,参数数量会爆炸(比如隐藏层有 1000 个神经元,就有 30000×1000=3000 万个参数),不仅计算慢,还容易 “学傻”(过拟合)。
CNN 的聪明之处:
- 模仿人类视觉:人眼看图片时,先关注局部(比如猫的耳朵、胡须),再组合成整体。
- 减少参数:通过 “局部感知” 和 “权值共享”,让电脑高效提取特征,避免重复计算。
二、CNN 的核心组件:像搭积木一样理解
CNN 由几个关键层组成,每一层都有明确的 “分工”,我们用 “识别猫的图片” 来举例:
1. 卷积层(Convolution Layer):找 “局部特征” 的 “过滤器”
- 作用:提取图片中的局部特征(比如边缘、颜色块、简单形状)。
- 类比:
就像用不同的 “放大镜” 观察图片:- 有的放大镜专门找 “横线”(比如猫的胡须),
- 有的专门找 “圆形”(比如猫的眼睛),
- 有的专门找 “黄色块”(比如橘猫的毛)。
- 核心概念:
- 过滤器(Filter):也叫 “卷积核”,是一个小矩阵,负责检测特定特征。比如 3×3 的过滤器在图片上滑动,每次只看 3×3 的局部区域(局部感知)。
- 权值共享:同一个过滤器在图片上滑动时,参数(权重)不变,就像用同一个 “放大镜” 扫描全图,减少参数数量。
- 输出特征图(Feature Map):过滤器扫描后,会生成一张新图,记录 “哪里有这个特征”。比如检测 “横线” 的过滤器,会在胡须位置输出高值。
举个例子:
输入一张猫的图片,经过一个 “边缘检测” 过滤器,输出的特征图会突出显示猫的轮廓边缘,忽略颜色和纹理细节。
2. 池化层(Pooling Layer):“压缩” 信息的 “简化器”
- 作用:缩小特征图尺寸,保留关键信息,减少计算量,同时让特征更 “抗干扰”(比如猫的位置稍微移动,特征仍能识别)。
- 类比:
就像看地图时,从 “街道级” 缩放成 “区域级”,忽略细节,只保留主要地标。 - 常见类型:
- 最大池化(Max Pooling):取每个小区域的最大值(比如 2×2 区域),相当于保留最明显的特征。
- 平均池化(Average Pooling):取平均值,保留整体趋势。
- 效果:
比如一个 100×100 的特征图,经过 2×2 最大池化后变成 50×50,尺寸减半,但关键特征(如猫的眼睛位置)依然存在。
3. 全连接层(Fully Connected Layer):“综合判断” 的 “大脑”
- 作用:把前面提取的所有特征整合起来,判断图片属于哪个类别(比如 “猫”“狗”“汽车”)。
- 类比:
前面的卷积和池化层找到了 “胡须”“耳朵”“黄色毛” 等特征,全连接层就像大脑,根据这些特征组合判断:“有胡须 + 尖耳朵 + 黄色毛 = 猫!” - 工作方式:
把所有特征图 “拍扁” 成一维向量,然后通过多层神经网络计算概率,输出分类结果(比如 “猫” 的概率 90%,“狗” 10%)。
三、CNN 的 “思考” 过程:用猫图举个完整例子
- 输入图片:一张彩色猫的照片。
- 卷积层处理:
- 用多个过滤器(比如边缘、颜色、形状过滤器)扫描图片,生成多张特征图,分别记录 “哪里有边缘”“哪里有黄色”“哪里有圆形” 等。
- 池化层压缩:
- 缩小特征图尺寸,比如从 100×100→50×50,保留关键特征的位置和强度。
- 重复卷积 + 池化(深层网络):
- 浅层网络提取简单特征(边缘、颜色),深层网络组合简单特征成复杂特征(比如 “边缘 + 圆形 = 眼睛”“眼睛 + 胡须 = 猫脸”)。
- 全连接层分类:
- 把所有特征整合,计算属于 “猫” 的概率,输出结果。
代码实战
一、环境准备与设备配置
import torch
import torch.nn as nn
import torch.optim as optim
from torchvision import datasets, transforms
from torch.utils.data import DataLoader
import matplotlib.pyplot as pltdevice = torch.device("cuda" if torch.cuda.is_available() else "cpu")
print(f"使用设备: {device}")if torch.cuda.is_available():print(f"GPU名称: {torch.cuda.get_device_name(0)}")print(f"GPU内存: {torch.cuda.get_device_properties(0).total_memory / 1024 / 1024} MB")
核心功能解析:
导入库说明:
torch
:PyTorch 核心库,提供张量计算和自动微分功能torch.nn
:包含神经网络层定义(如卷积层、全连接层)torch.optim
:优化器库(如 Adam、SGD)torchvision
:提供 MNIST 数据集和图像预处理工具matplotlib
:用于可视化图像和结果
设备配置原理:
torch.device
会自动检测 GPU 是否可用(CUDA 是 NVIDIA 的 GPU 计算平台)- 将模型和数据放在 GPU 上可加速计算(矩阵运算并行化)
- 若没有 GPU,默认使用 CPU(计算速度较慢但可运行)
二、数据预处理与加载
transform = transforms.Compose([transforms.Resize((32, 32)), # LeNet-5输入尺寸为32x32transforms.ToTensor(),transforms.Normalize((0.1307,), (0.3081,))
])train_dataset = datasets.MNIST('data', train=True, download=True, transform=transform)
test_dataset = datasets.MNIST('data', train=False, transform=transform)
train_loader = DataLoader(train_dataset, batch_size=64, shuffle=True)
test_loader = DataLoader(test_dataset, batch_size=1000)
数据处理全解析:
预处理流程拆解:
Resize((32, 32))
:将 MNIST 原始 28x28 图像放大到 32x32,因为 LeNet-5 设计输入尺寸为 32x32,更大尺寸便于提取边缘特征ToTensor()
:将图像转换为 PyTorch 张量,并将像素值从 [0,255] 缩放到 [0,1]Normalize
:标准化处理,公式为(x-mean)/std
,MNIST 数据集的全局均值 0.1307 和标准差 0.3081,可让数据分布更稳定,加速训练
数据集与数据加载器:
datasets.MNIST
:自动下载 MNIST 数据集(6 万训练图 + 1 万测试图),transform
参数应用预处理流程DataLoader
:批量加载数据的工具:batch_size=64
:每次训练使用 64 张图像组成一个批次(Batch)shuffle=True
:每个训练周期打乱数据顺序,避免模型按固定模式学习- 测试集
batch_size=1000
:批量更大,减少测试次数
三、LeNet-5 模型定义(核心架构)
class LeNet5(nn.Module):def __init__(self):super().__init__()self.conv1 = nn.Conv2d(1, 6, kernel_size=5) # C1层:6个5x5卷积核self.pool1 = nn.MaxPool2d(kernel_size=2, stride=2) # S2层:2x2池化self.conv2 = nn.Conv2d(6, 16, kernel_size=5) # C3层:16个5x5卷积核self.pool2 = nn.MaxPool2d(kernel_size=2, stride=2) # S4层:2x2池化self.fc1 = nn.Linear(16 * 5 * 5, 120) # C5层:全连接self.fc2 = nn.Linear(120, 84) # F6层:全连接self.fc3 = nn.Linear(84, 10) # 输出层self.relu = nn.ReLU()def forward(self, x):x = self.pool1(self.relu(self.conv1(x)))x = self.pool2(self.relu(self.conv2(x)))x = x.view(-1, 16 * 5 * 5) # 展平x = self.relu(self.fc1(x))x = self.relu(self.fc2(x))x = self.fc3(x)return x
LeNet-5 完整参数流动(关键!)
层名 | 操作 / 代码 | 输入尺寸 | 输出尺寸 | 参数数量 | 计算过程(关键!) |
---|---|---|---|---|---|
输入 | - | [1, 32, 32] | - | - | 32×32 像素的灰度图像(单通道) |
Conv1 | nn.Conv2d(1, 6, kernel_size=5) | [1, 32, 32] | [6, 28, 28] | 156 | 输出尺寸:(32-5+1)×(32-5+1)=28×28 参数:(5×5×1+1)×6=156 |
ReLU1 | self.relu(conv1(x)) | [6, 28, 28] | [6, 28, 28] | 0 | 对每个元素应用 max (0,x) |
Pool1 | nn.MaxPool2d(kernel_size=2) | [6, 28, 28] | [6, 14, 14] | 0 | 输出尺寸:28÷2=14×14 |
Conv2 | nn.Conv2d(6, 16, kernel_size=5) | [6, 14, 14] | [16, 10, 10] | 2,416 | 输出尺寸:(14-5+1)×(14-5+1)=10×10 参数:(5×5×6+1)×16=2416 |
ReLU2 | self.relu(conv2(x)) | [16, 10, 10] | [16, 10, 10] | 0 | 对每个元素应用 max (0,x) |
Pool2 | nn.MaxPool2d(kernel_size=2) | [16, 10, 10] | [16, 5, 5] | 0 | 输出尺寸:10÷2=5×5 |
Flatten | x.view(-1, 16*5*5) | [16, 5, 5] | [400] | 0 | 将张量展平为一维向量:16×5×5=400 |
FC1 | nn.Linear(400, 120) | [400] | [120] | 48,120 | 参数:400×120+120=48120 |
ReLU3 | self.relu(fc1(x)) | [120] | [120] | 0 | 对每个元素应用 max (0,x) |
FC2 | nn.Linear(120, 84) | [120] | [84] | 10,164 | 参数:120×84+84=10164 |
ReLU4 | self.relu(fc2(x)) | [84] | [84] | 0 | 对每个元素应用 max (0,x) |
FC3 | nn.Linear(84, 10) | [84] | [10] | 850 | 参数:84×10+10=850 |
输出 | - | [10] | - | - | 10 个类别得分(对应 0-9 数字) |
模型架构逐行解析:
继承 nn.Module:所有 PyTorch 模型需继承
nn.Module
基类,重写__init__
和forward
方法卷积层(特征提取):
nn.Conv2d(1, 6, kernel_size=5)
:- 输入通道 1(MNIST 是灰度图),输出通道 6(生成 6 个特征图)
- 卷积核大小 5x5(局部感知野,每次看 5x5 的图像区域)
- 作用:提取 6 种不同的边缘特征(如横线、竖线、斜线)
池化层(特征压缩):
nn.MaxPool2d(kernel_size=2, stride=2)
:- 2x2 窗口,步长 2(窗口不重叠)
- 取窗口内最大值(保留最强特征,忽略位置偏移)
- 作用:将特征图尺寸减半,减少计算量,增强抗变形能力
全连接层(特征分类):
nn.Linear(16*5*5, 120)
:- 输入维度 1655=400(来自第二层池化后的特征图尺寸:16 个 5x5 特征图)
- 输出维度 120(将 400 维特征映射到 120 维)
- 作用:前两层全连接层用于组合特征,最后一层
fc3
输出 10 维(对应 0-9 数字分类)
ReLU 激活函数:
nn.ReLU()
:公式f(x)=max(0, x)
- 作用:引入非线性,让模型能学习复杂特征(如数字的曲线形状),解决线性模型表达能力不足的问题
forward 前向传播:
- 数据流向:输入→卷积 1→ReLU→池化 1→卷积 2→ReLU→池化 2→展平→全连接 1→ReLU→全连接 2→ReLU→全连接 3→输出
x.view(-1, 16*5*5)
:将多维特征图展平为一维向量(-1 表示自动计算批量大小)
四、模型训练流程
model = LeNet5().to(device)
loss_function = nn.CrossEntropyLoss()
optimizer = optim.Adam(model.parameters(), lr=0.001)model.train()
epochs = 5
for epoch in range(epochs):running_loss = 0.0for batch_idx, (data, target) in enumerate(train_loader):optimizer.zero_grad()data, target = data.to(device), target.to(device)output = model(data)loss = loss_function(output, target)loss.backward()optimizer.step()running_loss += loss.item()if batch_idx % 100 == 0:print(f"Epoch {epoch + 1}, Batch {batch_idx}, Loss {loss.item():.4f}")print(f"Epoch {epoch + 1}, Average Loss {running_loss / len(train_loader):.4f}")
训练过程深度解析:
初始化三要素:
model.to(device)
:将模型参数移至 GPU/CPUnn.CrossEntropyLoss()
:交叉熵损失函数,适用于多分类问题(自动包含 Softmax)optim.Adam(model.parameters(), lr=0.001)
:Adam 优化器,自动调整学习率,参数lr=0.001
是初始学习率
训练循环逻辑:
model.train()
:设置模型为训练模式(启用 Dropout、BatchNorm 等,此处 LeNet-5 未使用)epochs=5
:完整遍历训练集 5 次batch_idx, (data, target)
:从数据加载器中获取一个批次的数据(data 是图像,target 是真实标签)
单批次训练步骤:
optimizer.zero_grad()
:清零梯度(PyTorch 默认累积梯度,每次迭代前需清零)data.to(device)
:将数据移至计算设备(GPU/CPU)output = model(data)
:前向传播,计算模型预测结果loss = loss_function(output, target)
:计算预测与真实标签的差异(损失)loss.backward()
:反向传播,计算损失对所有参数的梯度optimizer.step()
:根据梯度更新模型参数(如卷积核权重、全连接层权重)
损失监控:
running_loss += loss.item()
:累加每个批次的损失loss.item():.4f
:打印当前批次损失(保留 4 位小数)Average Loss
:每个周期的平均损失,用于判断模型是否在学习(理想情况是逐渐下降)
五、模型评估与可视化
# 评估模型
model.eval()
correct = 0
total = 0
with torch.no_grad():for data, target in test_loader:data, target = data.to(device), target.to(device)output = model(data)_, predicted = torch.max(output.data, 1)total += target.size(0)correct += (predicted == target).sum().item()print(f"Test Accuracy: {100 * correct / total}%")# 可视化预测结果
model.eval()
num_samples = 5
fig, axes = plt.subplots(1, num_samples, figsize=(15, 3))with torch.no_grad():for i, (data, target) in enumerate(test_loader):data, target = data.to(device), target.to(device)output = model(data)_, predicted = torch.max(output, 1)for j in range(num_samples):img = data[j].view(32, 32).cpu().numpy()axes[j].imshow(img, cmap='gray')axes[j].set_title(f'Pred: {predicted[j]}, True: {target[j]}')axes[j].axis('off')break
plt.tight_layout()
plt.show()
评估与可视化解析:
模型评估流程:
model.eval()
:设置模型为评估模式(关闭训练特有的操作,如 Dropout)with torch.no_grad()
:禁用梯度计算(节省内存,加速计算)torch.max(output, 1)
:获取每个样本预测概率最大的类别(索引 0-9)- 准确率计算:正确预测数 ÷ 总样本数 ×100%
可视化原理:
fig, axes = plt.subplots(1, 5)
:创建 1 行 5 列的子图data[j].view(32, 32)
:将张量从 [1,32,32] 重塑为 [32,32](去掉通道维度)cpu().numpy()
:将 GPU 数据移至 CPU 并转换为 numpy 数组(matplotlib 只能显示 numpy 数组)set_title
:显示预测类别和真实类别,验证模型预测是否正确
六、LeNet-5 核心参数流动图解
输入尺寸变化:
- 输入:[batch_size, 1, 32, 32](批量大小,通道数,高度,宽度)
conv1
:6 个 5x5 卷积核 → 输出 [batch_size, 6, 28, 28](尺寸计算:32-5+1=28)pool1
:2x2 最大池化 → 输出 [batch_size, 6, 14, 14](尺寸减半)conv2
:16 个 5x5 卷积核 → 输出 [batch_size, 16, 10, 10](14-5+1=10)pool2
:2x2 最大池化 → 输出 [batch_size, 16, 5, 5]view
展平:[batch_size, 1655=400]fc1
:400→120 → 输出 [batch_size, 120]fc2
:120→84 → 输出 [batch_size, 84]fc3
:84→10 → 输出 [batch_size, 10](10 个类别概率)
参数数量计算:
- 卷积层 1:(1×5×5+1)×6=156 参数(每个卷积核 5×5×1 权重 + 1 偏置,共 6 个)
- 卷积层 2:(6×5×5+1)×16=2416 参数
- 全连接层 1:400×120+120=48120 参数
- 全连接层 2:120×84+84=10164 参数
- 全连接层 3:84×10+10=850 参数
- 总参数:156+2416+48120+10164+850=61706(约 6 万参数,远少于全连接网络)
七、代码中隐藏的 CNN 核心概念映射
代码片段 | 对应 CNN 概念 | 通俗解释 |
---|---|---|
nn.Conv2d | 卷积层 | 用滤镜提取图像局部特征(如边缘、形状) |
nn.MaxPool2d | 池化层 | 缩小特征图,保留关键特征,忽略位置偏移 |
nn.ReLU() | 激活函数 | 只保留强特征,过滤弱特征,让模型能学习复杂模式 |
CrossEntropyLoss | 损失函数 | 衡量模型预测与真实标签的差异,指导模型优化 |
optimizer.step() | 反向传播与参数更新 | 根据损失调整模型参数(滤镜权重、全连接层权重),让下次预测更准 |
DataLoader(batch_size=64) | 批量训练(Batch Training) | 一次训练多个样本,加速收敛,减少噪声影响 |