一、为什么需要 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 的 “思考” 过程:用猫图举个完整例子

  1. 输入图片:一张彩色猫的照片。
  2. 卷积层处理
    • 用多个过滤器(比如边缘、颜色、形状过滤器)扫描图片,生成多张特征图,分别记录 “哪里有边缘”“哪里有黄色”“哪里有圆形” 等。
  3. 池化层压缩
    • 缩小特征图尺寸,比如从 100×100→50×50,保留关键特征的位置和强度。
  4. 重复卷积 + 池化(深层网络)
    • 浅层网络提取简单特征(边缘、颜色),深层网络组合简单特征成复杂特征(比如 “边缘 + 圆形 = 眼睛”“眼睛 + 胡须 = 猫脸”)。
  5. 全连接层分类
    • 把所有特征整合,计算属于 “猫” 的概率,输出结果。

代码实战

一、环境准备与设备配置
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 plt

device = 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")

核心功能解析

  1. 导入库说明

    • torch:PyTorch 核心库,提供张量计算和自动微分功能
    • torch.nn:包含神经网络层定义(如卷积层、全连接层)
    • torch.optim:优化器库(如 Adam、SGD)
    • torchvision:提供 MNIST 数据集和图像预处理工具
    • matplotlib:用于可视化图像和结果
  2. 设备配置原理

    • torch.device会自动检测 GPU 是否可用(CUDA 是 NVIDIA 的 GPU 计算平台)
    • 将模型和数据放在 GPU 上可加速计算(矩阵运算并行化)
    • 若没有 GPU,默认使用 CPU(计算速度较慢但可运行)
二、数据预处理与加载
transform = transforms.Compose([
    transforms.Resize((32, 32)),  # LeNet-5输入尺寸为32x32
    transforms.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)

数据处理全解析

  1. 预处理流程拆解

    • Resize((32, 32)):将 MNIST 原始 28x28 图像放大到 32x32,因为 LeNet-5 设计输入尺寸为 32x32,更大尺寸便于提取边缘特征
    • ToTensor():将图像转换为 PyTorch 张量,并将像素值从 [0,255] 缩放到 [0,1]
    • Normalize:标准化处理,公式为(x-mean)/std,MNIST 数据集的全局均值 0.1307 和标准差 0.3081,可让数据分布更稳定,加速训练
  2. 数据集与数据加载器

    • 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 数字)

模型架构逐行解析

  1. 继承 nn.Module:所有 PyTorch 模型需继承nn.Module基类,重写__init__forward方法

  2. 卷积层(特征提取)

    • nn.Conv2d(1, 6, kernel_size=5)
      • 输入通道 1(MNIST 是灰度图),输出通道 6(生成 6 个特征图)
      • 卷积核大小 5x5(局部感知野,每次看 5x5 的图像区域)
      • 作用:提取 6 种不同的边缘特征(如横线、竖线、斜线)
  3. 池化层(特征压缩)

    • nn.MaxPool2d(kernel_size=2, stride=2)
      • 2x2 窗口,步长 2(窗口不重叠)
      • 取窗口内最大值(保留最强特征,忽略位置偏移)
      • 作用:将特征图尺寸减半,减少计算量,增强抗变形能力
  4. 全连接层(特征分类)

    • nn.Linear(16*5*5, 120)
      • 输入维度 1655=400(来自第二层池化后的特征图尺寸:16 个 5x5 特征图)
      • 输出维度 120(将 400 维特征映射到 120 维)
      • 作用:前两层全连接层用于组合特征,最后一层fc3输出 10 维(对应 0-9 数字分类)
  5. ReLU 激活函数

    • nn.ReLU():公式f(x)=max(0, x)
    • 作用:引入非线性,让模型能学习复杂特征(如数字的曲线形状),解决线性模型表达能力不足的问题
  6. 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.0
    for 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}")

训练过程深度解析

  1. 初始化三要素

    • model.to(device):将模型参数移至 GPU/CPU
    • nn.CrossEntropyLoss():交叉熵损失函数,适用于多分类问题(自动包含 Softmax)
    • optim.Adam(model.parameters(), lr=0.001):Adam 优化器,自动调整学习率,参数lr=0.001是初始学习率
  2. 训练循环逻辑

    • model.train():设置模型为训练模式(启用 Dropout、BatchNorm 等,此处 LeNet-5 未使用)
    • epochs=5:完整遍历训练集 5 次
    • batch_idx, (data, target):从数据加载器中获取一个批次的数据(data 是图像,target 是真实标签)
  3. 单批次训练步骤

    • optimizer.zero_grad():清零梯度(PyTorch 默认累积梯度,每次迭代前需清零)
    • data.to(device):将数据移至计算设备(GPU/CPU)
    • output = model(data):前向传播,计算模型预测结果
    • loss = loss_function(output, target):计算预测与真实标签的差异(损失)
    • loss.backward():反向传播,计算损失对所有参数的梯度
    • optimizer.step():根据梯度更新模型参数(如卷积核权重、全连接层权重)
  4. 损失监控

    • 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()

评估与可视化解析

  1. 模型评估流程

    • model.eval():设置模型为评估模式(关闭训练特有的操作,如 Dropout)
    • with torch.no_grad():禁用梯度计算(节省内存,加速计算)
    • torch.max(output, 1):获取每个样本预测概率最大的类别(索引 0-9)
    • 准确率计算:正确预测数 ÷ 总样本数 ×100%
  2. 可视化原理

    • 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 核心参数流动图解
  1. 输入尺寸变化

    • 输入:[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 个类别概率)
  2. 参数数量计算

    • 卷积层 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) 一次训练多个样本,加速收敛,减少噪声影响

更多推荐