71·深度学习入门进阶

卷积神经网络(CNN)基础

deep-learningcnn

第 71 课 - 卷积神经网络(CNN)基础

所属模块: 深度学习入门
难度: Intermediate
标签: deep-learning, cnn


1. 学习目标

通过本节课的学习,你将能够:

  • 理解 为什么需要卷积神经网络(CNN)来处理图像等网格状数据。
  • 掌握 卷积层、池化层和全连接层等核心组件的工作原理。
  • 使用 PyTorch 构建一个基础的 CNN 模型结构。
  • 识别 CNN 模型中各层输出的张量维度变化。

2. 核心概念

为什么需要 CNN?

全连接网络(像上一节课学到的)处理图像时,每个神经元都需要与上一层的所有神经元相连。对于一张 28x28 的灰度图,输入层就有 784 个神经元。如果第一个隐藏层有 1000 个神经元,那么仅这一层的权重参数就有 784,000 个。参数过多不仅计算量大,而且容易过拟合,更无法有效捕捉图像中的局部特征(如边缘、纹理)和空间结构

CNN 的出现解决了这个问题,它通过 “卷积” 操作,模仿了人类视觉皮层的感受野机制,能够局部连接参数共享,极大地减少了参数数量,并能自动学习从低级特征(边缘)到高级特征(部件、物体)的层次化表示。

核心组件

  1. 卷积层

    • 作用: 特征提取。使用一个小的滤波器(卷积核,Kernel)在输入图像上滑动,在每个位置计算卷积核与图像局部区域的点积,生成一个特征图。
    • 关键概念:
      • 卷积核: 一个小的权重矩阵(如 3x3),是网络需要学习的参数。不同的卷积核可以检测不同的特征(如垂直边缘、水平边缘)。
      • 步长: 卷积核每次滑动的像素数。步长为1时,输出特征图尺寸较大;步长大于1时,起到降维作用。
      • 填充: 在输入图像周围补零。可以保持输出特征图的空间尺寸(当 padding=(kernel_size-1)/2stride=1 时)。
    • 输出尺寸计算: 输出尺寸 = (输入尺寸 - 卷积核尺寸 + 2 * 填充) / 步长 + 1
  2. 激活函数

    • 作用: 引入非线性。最常用的是 ReLU,因为它计算简单,能有效缓解梯度消失问题。通常紧接在卷积层之后。
  3. 池化层

    • 作用: 下采样,降低特征图的空间尺寸,减少计算量,并赋予模型一定程度的平移不变性(轻微移动物体,特征仍能被识别)。
    • 常见类型: 最大池化(取局部区域的最大值)、平均池化(取局部区域的平均值)。最大池化更常用。
  4. 展平与全连接层

    • 作用: 将经过多次卷积和池化得到的高维、局部的特征图“展平”成一维向量,然后连接到一个或多个全连接层,进行最终的分类或回归。
  5. CNN 流水线(前向传播) 输入图像 -> [卷积层 + ReLU] -> [池化层] -> ... -> [展平] -> [全连接层] -> 输出

3. 代码示例

我们将使用 PyTorch 构建一个用于 MNIST 手写数字识别的简单 CNN。

import torch
import torch.nn as nn
import torch.optim as optim
from torch.utils.data import DataLoader
from torchvision import datasets, transforms

# 1. 准备数据集
# 定义图像预处理:转为Tensor,并进行标准化(使用MNIST数据集的均值和标准差)
transform = transforms.Compose([
    transforms.ToTensor(),
    transforms.Normalize((0.1307,), (0.3081,))
])

# 下载并加载训练集和测试集
train_dataset = datasets.MNIST(root='./data', train=True, download=True, transform=transform)
test_dataset = datasets.MNIST(root='./data', train=False, download=True, transform=transform)

train_loader = DataLoader(train_dataset, batch_size=64, shuffle=True)
test_loader = DataLoader(test_dataset, batch_size=1000, shuffle=False)

# 2. 定义CNN模型
class SimpleCNN(nn.Module):
    def __init__(self):
        super(SimpleCNN, self).__init__()
        # 第一个卷积层:1个输入通道(灰度图),16个输出通道(16个卷积核),卷积核大小3x3,填充1(保持尺寸)
        self.conv1 = nn.Conv2d(1, 16, kernel_size=3, padding=1)
        # 第二个卷积层:16个输入通道(来自上一层),32个输出通道,卷积核大小3x3,填充1
        self.conv2 = nn.Conv2d(16, 32, kernel_size=3, padding=1)
        # 最大池化层:窗口大小2x2,步长2(尺寸减半)
        self.pool = nn.MaxPool2d(2, 2)
        # 第一个全连接层:输入特征数 = 32个通道 * 7x7特征图(经过两次池化后尺寸变化)
        self.fc1 = nn.Linear(32 * 7 * 7, 128)
        # 第二个全连接层(输出层):输出10个类别(数字0-9)
        self.fc2 = nn.Linear(128, 10)
        # ReLU激活函数
        self.relu = nn.ReLU()

    def forward(self, x):
        # 输入x形状:(batch_size, 1, 28, 28)
        x = self.pool(self.relu(self.conv1(x)))  # 输出形状:(batch_size, 16, 14, 14)
        x = self.pool(self.relu(self.conv2(x)))  # 输出形状:(batch_size, 32, 7, 7)
        # 将多维特征图展平为一维向量
        x = x.view(-1, 32 * 7 * 7)  # 输出形状:(batch_size, 32*7*7)
        x = self.relu(self.fc1(x))  # 输出形状:(batch_size, 128)
        x = self.fc2(x)  # 输出形状:(batch_size, 10)
        return x

# 3. 初始化模型、损失函数和优化器
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
model = SimpleCNN().to(device)
criterion = nn.CrossEntropyLoss()
optimizer = optim.Adam(model.parameters(), lr=0.001)

# 4. 训练模型 (简化版,仅展示一个epoch)
def train_one_epoch(model, loader, criterion, optimizer, device):
    model.train()
    total_loss = 0
    for batch_idx, (data, target) in enumerate(loader):
        data, target = data.to(device), target.to(device)
        optimizer.zero_grad()
        output = model(data)
        loss = criterion(output, target)
        loss.backward()
        optimizer.step()
        total_loss += loss.item()
        if batch_idx % 200 == 0:
            print(f'  Batch {batch_idx}, Loss: {loss.item():.4f}')
    return total_loss / len(loader)

# 5. 测试模型
def test_model(model, loader, device):
    model.eval()
    correct = 0
    total = 0
    with torch.no_grad():
        for data, target in 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()
    accuracy = 100. * correct / total
    return accuracy

# 主程序
if __name__ == "__main__":
    print("开始训练...")
    for epoch in range(1):  # 训练1个epoch
        train_loss = train_one_epoch(model, train_loader, criterion, optimizer, device)
        print(f'Epoch {epoch+1}, Average Loss: {train_loss:.4f}')
    
    print("\n开始测试...")
    accuracy = test_model(model, test_loader, device)
    print(f'测试集准确率: {accuracy:.2f}%')

    # 查看模型结构(可选)
    # print(model)
    # 查看第一层卷积核(可选)
    # print(“卷积核形状:“, model.conv1.weight.shape) # 输出: torch.Size([16, 1, 3, 3])

代码解读:

  1. SimpleCNN 类定义了网络结构。__init__ 方法定义层,forward 方法定义数据如何流经这些层。
  2. nn.Conv2d(1, 16, 3, padding=1): 第一层卷积,1个输入通道(灰度),16个输出通道,3x3卷积核,填充1。
  3. nn.MaxPool2d(2, 2): 使用2x2窗口,步长为2进行最大池化,特征图长宽各减半。
  4. x.view(-1, 32 * 7 * 7): 将四维张量 (batch, channels, height, width) 展平为二维张量 (batch, features),以便输入全连接层。
  5. 训练和测试流程与上一课的全连接网络基本一致。

4. 实践练习

练习1:观察卷积核

  • 任务: 修改上述代码,在模型初始化后,打印第一个卷积层 conv1 的权重形状(model.conv1.weight),并解释 [16, 1, 3, 3] 这四个数字分别代表什么。
  • 预期输出: 一个形状为 [16, 1, 3, 3] 的张量。解释:16个卷积核,每个卷积核的输入通道数为1(因为输入是灰度图),每个卷积核的空间尺寸是3x3。

练习2:理解特征图尺寸变化

  • 任务: 假设输入是一张 1x32x32 的彩色图片(3通道)。经过一个 nn.Conv2d(3, 8, kernel_size=5, padding=2) 和一个 nn.MaxPool2d(2, 2) 后,输出特征图的形状是什么?
  • 要求: 写出计算过程。
  • 预期输出: (8, 16, 16)。计算过程:卷积后尺寸 (32 - 5 + 2*2)/1 + 1 = 32,通道数变为8,得到 8x32x32。池化后尺寸 (32 - 2)/2 + 1 = 16,得到 8x16x16

练习3:设计一个更小的CNN

  • 任务: 设计一个仅包含一个卷积层一个全连接层的CNN,用于MNIST分类。要求卷积层输出通道数为8,卷积核大小5x5,使用ReLU激活和一次2x2最大池化。
  • 预期输出: 代码能成功运行并输出一个合理的测试准确率(>90%)。

5. 常见错误

  1. 忘记激活函数: 在卷积层后没有接 ReLU 等激活函数,导致网络缺乏非线性表达能力,效果接近于线性模型。
  2. 维度匹配错误: 在连接卷积层和全连接层时,viewflatten 操作的特征数量计算错误。务必仔细推导或打印中间张量的形状。
  3. 混淆通道和空间维度: PyTorch 中图像张量的默认格式是 (N, C, H, W)(批次,通道,高度,宽度),而不是 (N, H, W, C)。在自定义操作时需要注意。
  4. 池化层尺寸设置不当: 使用了过大的池化窗口(如 MaxPool2d(4, 4))或在小特征图上重复池化,导致特征图过早地丧失所有空间信息,模型性能下降。
  5. 在卷积层上使用不恰当的激活函数: 最后一层全连接输出层通常不使用激活函数(分类任务接CrossEntropyLoss,回归任务可能接线性激活)。中间层避免使用 Sigmoid/Tanh,优先使用 ReLU

6. 小结

本节课我们学习了卷积神经网络(CNN)的基础知识,它是处理图像、视频等网格数据的强大工具。

  • 核心思想: 通过卷积操作实现局部连接参数共享,高效地提取图像的层次化特征。
  • 核心组件:
    • 卷积层:用于特征提取。
    • 激活函数(如ReLU):引入非线性。
    • 池化层:用于下采样,降低空间维度,增强平移不变性。
    • 全连接层:用于综合特征并进行最终预测。
  • 构建流程: 通常遵循 卷积 -> 激活 -> 池化 的模式重复数次,然后展平并连接全连接层
  • 关键实践: 使用 PyTorch 时,要特别注意 nn.Conv2dnn.Linear 之间维度的正确转换。

下一课预览: 我们将学习循环神经网络(RNN)与 LSTM,了解如何处理序列数据(如文本、时间序列),这与CNN处理空间数据的思想既有联系又有显著区别。

练习编辑器

rust
Loading...

继续学习

完成本课后,建议继续学习下一课「循环神经网络(RNN)与 LSTM」 以巩固所学知识。