一、引言:当图像遇到卷积

在上一篇博客中,我们用全连接神经网络实现了手写数字识别,达到了不错的准确率。但如果你仔细观察那个网络结构,会发现一个"隐患":我们将28×28的图像直接展平成784维向量,然后连接到全连接层

这种做法存在两个严重问题:

问题一:参数爆炸。 假设第一层有1000个神经元,那么仅这一层就有784×1000 = 784,000个参数。如果输入是224×224的彩色图像(3通道),参数量会达到150,528×1000 = 1.5亿!这显然不可接受。

问题二:丢失空间信息。 图像展平后,相邻像素之间的空间关系被彻底打乱。对于图像识别来说,"某个像素的上下左右是什么"恰恰是最重要的信息。

卷积神经网络(Convolutional Neural Network, CNN) 正是为了解决这些问题而生的。它通过卷积操作提取图像的局部特征,通过权值共享大幅减少参数量,通过池化操作降低特征维度,是计算机视觉领域最重要的深度学习模型。

本篇博客将基于PyTorch框架,带领大家从零构建一个CNN来识别MNIST手写数字,深入理解卷积、池化、通道等核心概念。

二、CNN的核心概念

2.1 什么是卷积

卷积是CNN的核心操作。简单来说,它用一个小的窗口(称为卷积核滤波器)在图像上滑动,每次滑动时计算窗口内像素与卷积核的加权和,得到一个新的值。

输入图像(5×5)         卷积核(3×3)         输出特征图(3×3)
1 0 1 0 1            1 0 1               ? ? ?
0 1 0 1 0            0 1 0               ? ? ?
1 0 1 0 1            1 0 1               ? ? ?
0 1 0 1 0
1 0 1 0 1

卷积的意义

  • 卷积核可以看作一个"特征检测器"——不同的卷积核检测不同的特征(边缘、纹理、角点等)

  • 通过训练,网络会自动学习到最有用的卷积核

  • 权值共享:同一个卷积核在整个图像上滑动,参数只有9个,而不是784×1000个

2.2 关键参数

在PyTorch中,卷积层使用nn.Conv2d()创建,核心参数包括:

参数含义说明
in_channels输入通道数灰度图为1,RGB彩色图为3
out_channels输出通道数等于卷积核的个数
kernel_size卷积核大小常用3、5、7
stride步长卷积核每次滑动的距离,默认1
padding填充在图像边缘补零的圈数

2.3 输出尺寸计算公式

卷积后的特征图尺寸可以通过以下公式计算:

H_{out}=\frac{H_{in}+2\times padding- kernelsize}{stride}+1

示例:输入28×28,kernel_size=5,stride=1,padding=2

H_{out} = \frac{28+2\times 2-5}{1}+1 =28

这说明:当stride=1,padding=(kernel_size-1)/2时,输出尺寸与输入尺寸相同。这是一种常用的设计策略,使得网络可以加深而不改变空间维度。

2.4 池化操作

池化(Pooling) 用于降低特征图的空间维度,减少参数量和计算量,同时增强特征的平移不变性。

最常用的池化是最大池化(Max Pooling) :在窗口内取最大值。

特征图(4×4)          最大池化(2×2)        输出(2×2)
1 3 2 4              3 4
5 6 7 8      →       7 8
9 2 1 3
4 5 6 7

池化不改变通道数,只改变空间尺寸。

2.5 激活函数——ReLU

在CNN中,ReLU(Rectified Linear Unit) 是最常用的激活函数:

ReLU(x)=max(0,x)ReLU(x)=max(0,x)

ReLU的优势

  • 计算简单(只需比较大小)

  • 缓解梯度消失问题

  • 稀疏激活(负值被置零),提高计算效率

相比上一篇博客中使用的Sigmoid函数,ReLU在深层网络中表现更好。

三、CNN网络架构设计

3.1 整体结构

本篇示例代码设计的CNN由3个卷积块和一个全连接层组成:

输入(1×28×28)
    ↓
Conv1: Conv2d(1→16) + ReLU + MaxPool2d(2) → (16×14×14)
    ↓
Conv2: Conv2d(16→32) + ReLU + Conv2d(32→32) + ReLU + MaxPool2d(2) → (32×7×7)
    ↓
Conv3: Conv2d(32→64) + ReLU → (64×7×7)
    ↓
Flatten: (64×7×7 = 3136)
    ↓
全连接: Linear(3136→10)
    ↓
输出(10个类别得分)

尺寸变化分析

  • 输入:1×28×28

  • Conv2d(1→16, k=5, s=1, p=2):16×28×28(尺寸不变)

  • ReLU:16×28×28(尺寸不变)

  • MaxPool2d(2):16×14×14(尺寸减半)

第二个卷积块包含两个卷积层:

self.conv2 = nn.Sequential(
    nn.Conv2d(16, 32, 5, 1, 2),   # 32×14×14
    nn.ReLU(),                     # 32×14×14
    nn.Conv2d(32, 32, 5, 1, 2),   # 32×14×14
    nn.ReLU(),                     # 32×14×14
    nn.MaxPool2d(2),               # 32×7×7
)

第三个卷积块只有一个卷积层:

self.conv3 = nn.Sequential(
    nn.Conv2d(32, 64, 5, 1, 2),   # 64×7×7
    nn.ReLU(),                     # 64×7×7
)

3.3 全连接层与展平操作

self.out = nn.Linear(64 * 7 * 7, 10)

在进入全连接层之前,需要将三维特征图展平为一维向量:

x = x.view(x.size(0), -1)   # (batch_size, 64*7*7)
  • x.size(0):batch_size(本例中为64)

  • -1:自动推断剩余维度的大小(64×7×7 = 3136)

  • 展平后形状:(64, 3136)

3.4 参数数量统计

让我们计算每一层的参数量:

计算参数量
conv116×(5×5×1+1)416
conv2-132×(5×5×16+1)12,832
conv2-232×(5×5×32+1)25,632
conv364×(5×5×32+1)51,264
out3136×10+1031,370
总计约12万

相比全连接网络的约16万参数,CNN参数量更少,且随着图像尺寸增大,CNN的优势会更加明显。

四、前向传播

def forward(self, x):
    x = self.conv1(x)       # (64,1,28,28) → (64,16,14,14)
    x = self.conv2(x)       # (64,16,14,14) → (64,32,7,7)
    x = self.conv3(x)       # (64,32,7,7) → (64,64,7,7)
    x = x.view(x.size(0), -1)  # 展平 → (64, 3136)
    output = self.out(x)    # 全连接 → (64, 10)
    return output

数据流动过程

步骤操作输出形状
输入-(64, 1, 28, 28)
conv1卷积+ReLU+池化(64, 16, 14, 14)
conv2双层卷积+池化(64, 32, 7, 7)
conv3单层卷积(64, 64, 7, 7)
view展平(64, 3136)
out全连接(64, 10)

五、训练与测试

5.1 训练函数

训练函数与上一篇博客基本相同,核心五步:

def train(dataloader, model, loss_fn, optimizer):
    model.train()
    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()

5.2 测试函数

def test(dataloader, model, loss_fn):
    model.eval()
    size = len(dataloader.dataset)
    correct = 0
    with torch.no_grad():
        for x, y in dataloader:
            x, y = x.to(device), y.to(device)
            pred = model(x)
            correct += (pred.argmax(1) == y).type(torch.float).sum().item()
    print(f"Accuracy: {100*correct/size}%")

5.3 训练配置

loss_fn = nn.CrossEntropyLoss()                    # 交叉熵损失
optimizer = torch.optim.Adam(model.parameters(), lr=0.001)  # Adam优化器
epochs = 10

六、全连接网络 vs 卷积神经网络

对比维度全连接网络(NN)卷积神经网络(CNN)
输入处理直接展平保持二维结构
空间信息完全丢失保留局部关系
参数量较多较少(权值共享)
特征提取自动提取局部特征
平移不变性池化提供
适合任务结构化数据图像、视频

为什么CNN更适合图像?

  1. 局部连接:每个神经元只连接输入的局部区域,符合图像的局部相关性

  2. 权值共享:同一个卷积核在整张图上滑动,大大减少参数

  3. 平移不变性:无论物体出现在图像哪个位置,都能被同一卷积核检测到

  4. 层次化特征:浅层学习边缘、纹理,深层学习语义特征

七、CNN的核心设计思想

7.1 感受野

感受野是指输出特征图上一个像素对应输入图像上的区域大小。

  • 第一层卷积(5×5)的感受野为5×5

  • 经过两层卷积后,感受野会叠加扩大

  • 更深的网络,感受野更大,能捕捉更全局的特征

7.2 通道的变化规律

阶段通道数空间尺寸说明
输入128×28原始灰度图
conv1后1614×14提取16种低级特征
conv2后327×7提取32种中级特征
conv3后647×7提取64种高级特征

规律:随着网络加深,通道数增加,空间尺寸减小。这是一种常见的设计模式——用更多的通道来编码更丰富的语义信息。

7.3 为什么用多个卷积层堆叠?

在conv2中,我们使用了两个连续的3×3卷积层,而不是一个5×5卷积层。这样做有两个好处:

  1. 参数更少:两个3×3卷积的参数量为 2×3×3 = 18,而一个5×5卷积为 25

  2. 非线性更强:两个ReLU比一个ReLU引入更多非线性

这个思想来自VGG网络,是深度学习中的重要设计原则。

八、完整代码总结

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

# 1. 加载数据
training_data = datasets.MNIST(root="data", train=True, download=True, transform=ToTensor())
test_data = datasets.MNIST(root="data", train=False, download=True, transform=ToTensor())

# 2. 创建DataLoader
train_dataloader = DataLoader(training_data, batch_size=64)
test_dataloader = DataLoader(test_data, batch_size=64)

# 3. 选择设备
device = "cuda" if torch.cuda.is_available() else "mps" if torch.backends.mps.is_available() else "cpu"

# 4. 构建CNN模型
class CNN(nn.Module):
    def __init__(self):
        super(CNN, self).__init__()
        self.conv1 = nn.Sequential(
            nn.Conv2d(1, 16, 5, 1, 2),
            nn.ReLU(),
            nn.MaxPool2d(kernel_size=2),
        )
        self.conv2 = nn.Sequential(
            nn.Conv2d(16, 32, 5, 1, 2),
            nn.ReLU(),
            nn.Conv2d(32, 32, 5, 1, 2),
            nn.ReLU(),
            nn.MaxPool2d(2),
        )
        self.conv3 = nn.Sequential(
            nn.Conv2d(32, 64, 5, 1, 2),
            nn.ReLU(),
        )
        self.out = nn.Linear(64 * 7 * 7, 10)

    def forward(self, x):
        x = self.conv1(x)    # (64,1,28,28) → (64,16,14,14)
        x = self.conv2(x)    # (64,16,14,14) → (64,32,7,7)
        x = self.conv3(x)    # (64,32,7,7) → (64,64,7,7)
        x = x.view(x.size(0), -1)  # 展平
        output = self.out(x)
        return output

model = CNN().to(device)

# 5. 定义损失函数和优化器
loss_fn = nn.CrossEntropyLoss()
optimizer = torch.optim.Adam(model.parameters(), lr=0.001)

# 6. 训练函数
def train(dataloader, model, loss_fn, optimizer):
    model.train()
    for batch, (x, y) in enumerate(dataloader):
        x, y = x.to(device), y.to(device)
        pred = model(x)
        loss = loss_fn(pred, y)
        optimizer.zero_grad()
        loss.backward()
        optimizer.step()

# 7. 测试函数
def test(dataloader, model, loss_fn):
    model.eval()
    size = len(dataloader.dataset)
    correct = 0
    with torch.no_grad():
        for x, y in dataloader:
            x, y = x.to(device), y.to(device)
            pred = model(x)
            correct += (pred.argmax(1) == y).type(torch.float).sum().item()
    print(f"Accuracy: {100*correct/size}%")

# 8. 执行训练和测试
epochs = 10
for t in range(epochs):
    print(f"Epoch {t+1}")
    train(train_dataloader, model, loss_fn, optimizer)
test(test_dataloader, model, loss_fn)

九、总结

本篇博客通过构建一个完整的CNN,系统学习了卷积神经网络的核心知识:

知识点核心内容
卷积层nn.Conv2d(),提取局部特征,权值共享
池化层nn.MaxPool2d(),降低空间维度
激活函数ReLU,缓解梯度消失
Sequential组合多个层为容器
展平操作view(),将三维特征图转为一维
全连接层nn.Linear(),输出分类结果
通道变化通道递增,空间递减

CNN的核心设计原则

  1. 局部连接:每个神经元只关注局部区域

  2. 权值共享:同一卷积核在整张图上复用

  3. 层次化特征:浅层低级→深层高级

  4. 空间下采样:通过池化逐步减小空间尺寸

更多推荐