之前我们聊过了 Transformer 架构和 PyTorch 框架的基础,今天来聊聊深度学习里的另一个经典架构 —— CNN (Convolutional Neural Network,卷积神经网络)。如果说 Transformer 是 NLP 领域的王者,那 CNN 就是计算机视觉 (Computer Vision) 的开山鼻祖。虽然现在 Vision Transformer (ViT) 也很火,但 CNN 的思想依然是理解深度学习视觉模型的基础,很多概念在后续的架构中都能看到影子。
这篇文章会从直觉出发,一步步拆解 CNN 的核心组件,最后用 PyTorch 写一个完整的图像分类模型。走起!
为什么需要 CNN?
先想一个问题:如果我们用最朴素的全连接层 (Fully Connected Layer / Dense Layer) 来处理一张 224×224 的 RGB 图片,输入层就有 224 × 224 × 3 = 150,528 个神经元。假设第一个隐藏层有 1000 个神经元,光这一层就有 1.5 亿个参数!这不仅计算量爆炸,而且很容易 overfitting。
更关键的是,全连接层完全忽略了图像的空间结构。图片里一只猫在左上角还是右下角,对于全连接层来说完全是不同的输入,它学不到「这都是猫」这种平移不变性 (translation invariance)。
CNN 的核心思想其实很简单:用小的卷积核 (kernel/filter) 在图像上滑动,只看局部区域,然后层层叠加提取越来越抽象的特征。 这样做的好处是:
- 参数共享 (Parameter Sharing):同一个卷积核在整个图像上滑动,大大减少参数量
- 局部连接 (Local Connectivity):每个神经元只关注局部区域,符合图像的局部相关性
- 平移等变性 (Translation Equivariance):猫在图片任何位置,卷积核都能检测到同样的特征
卷积操作 (Convolution)
卷积操作是 CNN 的灵魂。想象你有一个 3×3 的小窗口(卷积核/kernel/filter),它在图像上从左到右、从上到下滑动,每到一个位置就做一次元素对应相乘再求和的操作。
具体来说,假设输入是一个 5×5 的灰度图,卷积核是 3×3:
# 卷积操作的直觉理解
# 输入: 5x5 灰度图
# 卷积核: 3x3
# 输出: 3x3 的 feature map (当 stride=1, no padding 时)
# 输出尺寸公式:
# output_size = (input_size - kernel_size + 2 * padding) / stride + 1
# 例: (5 - 3 + 2*0) / 1 + 1 = 3
几个重要的超参数:
- Kernel Size:卷积核大小,常用 3×3、5×5、1×1。VGGNet 证明了用两个 3×3 卷积核堆叠等效于一个 5×5 的感受野 (receptive field),但参数更少
- Stride:步长,即卷积核每次移动的距离。stride=2 时输出尺寸减半,可以代替 pooling 做下采样
- Padding:在输入边缘补零。「same」padding 可以保持输出尺寸不变
- Channels:卷积核的通道数必须和输入通道数一致,一个卷积核输出一个 feature map。想要 N 个 feature map 就用 N 个卷积核
池化层 (Pooling Layer)
Pooling 的作用是降低空间维度,减少计算量,同时增加一些平移不变性。最常见的两种:
- Max Pooling:取窗口内的最大值。比如 2×2 的 max pooling 会让 feature map 的长宽各减半。这是最常用的
- Average Pooling:取窗口内的平均值。在一些轻量级网络(如 MobileNet)或 Global Average Pooling (GAP) 中比较常见
值得注意的是,最近的趋势是用 stride=2 的卷积代替 pooling(比如 ResNet 的一些变体),因为 pooling 会丢弃信息,而 stride convolution 可以学习如何更好地下采样。
经典 CNN 架构演进
LeNet-5 (1998)
Yann LeCun 的开山之作,用于手写数字识别。结构很简单:两层卷积 + 两层池化 + 三层全连接。虽然现在看起来很原始,但它奠定了 CNN 的基本范式:卷积 → 激活 → 池化的堆叠。
AlexNet (2012)
AlexNet 在 ImageNet 上把 top-5 error 从 26% 干到了 15%,直接引爆了深度学习革命。关键创新:ReLU 激活函数、Dropout、Data Augmentation、GPU 训练。
VGGNet (2014)
VGG 的哲学是「更深更小」:全部用 3×3 卷积核,通过堆叠层数来增加感受野。VGG-16 有 16 层,VGG-19 有 19 层。虽然参数量很大,但结构非常优雅,到现在还经常被用作 feature extractor。
ResNet (2015)
ResNet 引入了 残差连接 (skip connection / residual connection),解决了深层网络的退化问题。核心公式:y = F(x) + x,让网络学习残差映射而不是直接映射。这个思想后来影响了几乎所有深度学习架构,包括 Transformer。
ResNet 证明了:网络不是越深越好,但有了残差连接,可以做到又深又好。ResNet-152 有 152 层,至今仍是很多任务的 baseline。
PyTorch 实现 CNN
说了这么多理论,终于到写代码的时候了。我们用 PyTorch 在 CIFAR-10 数据集上实现一个简单的 CNN 分类器。
Step 1: 数据准备
import torch
import torch.nn as nn
import torch.optim as optim
import torchvision
import torchvision.transforms as transforms
# CIFAR-10 是 32x32 的彩色图片,10 个类别
# 数据预处理:ToTensor + Normalize
transform = transforms.Compose([
transforms.RandomHorizontalFlip(), # 随机水平翻转,数据增强
transforms.RandomCrop(32, padding=4), # 随机裁剪
transforms.ToTensor(),
transforms.Normalize((0.4914, 0.4822, 0.4465), # CIFAR-10 的均值
(0.2023, 0.1994, 0.2010)), # CIFAR-10 的标准差
])
# 下载并加载训练集和测试集
trainset = torchvision.datasets.CIFAR10(root='./data', train=True,
download=True, transform=transform)
trainloader = torch.utils.data.DataLoader(trainset, batch_size=128,
shuffle=True, num_workers=2)
testset = torchvision.datasets.CIFAR10(root='./data', train=False,
download=True, transform=transform)
testloader = torch.utils.data.DataLoader(testset, batch_size=128,
shuffle=False, num_workers=2)
classes = ('plane', 'car', 'bird', 'cat', 'deer',
'dog', 'frog', 'horse', 'ship', 'truck')
Step 2: 定义模型
class SimpleCNN(nn.Module):
"""
一个简单的 CNN 模型,结构:
Conv(3->32) -> BN -> ReLU -> MaxPool
Conv(32->64) -> BN -> ReLU -> MaxPool
Conv(64->128) -> BN -> ReLU -> MaxPool
FC(128*4*4 -> 256) -> Dropout -> ReLU
FC(256 -> 10)
"""
def __init__(self, num_classes=10):
super(SimpleCNN, self).__init__()
# 特征提取部分 (Feature Extractor)
self.features = nn.Sequential(
# Block 1: 32x32x3 -> 16x16x32
nn.Conv2d(in_channels=3, out_channels=32, kernel_size=3, padding=1),
nn.BatchNorm2d(32),
nn.ReLU(inplace=True),
nn.MaxPool2d(kernel_size=2, stride=2),
# Block 2: 16x16x32 -> 8x8x64
nn.Conv2d(32, 64, kernel_size=3, padding=1),
nn.BatchNorm2d(64),
nn.ReLU(inplace=True),
nn.MaxPool2d(2, 2),
# Block 3: 8x8x64 -> 4x4x128
nn.Conv2d(64, 128, kernel_size=3, padding=1),
nn.BatchNorm2d(128),
nn.ReLU(inplace=True),
nn.MaxPool2d(2, 2),
)
# 分类器部分 (Classifier)
self.classifier = nn.Sequential(
nn.Flatten(), # 展平: 128*4*4 = 2048
nn.Linear(128 * 4 * 4, 256),
nn.Dropout(0.5), # 防止过拟合
nn.ReLU(inplace=True),
nn.Linear(256, num_classes),
)
def forward(self, x):
x = self.features(x)
x = self.classifier(x)
return x
# 创建模型,放到 GPU (如果有的话)
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
model = SimpleCNN(num_classes=10).to(device)
print(f'模型参数量: {sum(p.numel() for p in model.parameters()):,}')
print(f'使用设备: {device}')
Step 3: 训练
# 定义损失函数和优化器
criterion = nn.CrossEntropyLoss()
optimizer = optim.Adam(model.parameters(), lr=1e-3, weight_decay=1e-4)
scheduler = optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=50)
# 训练循环
num_epochs = 50
for epoch in range(num_epochs):
model.train()
running_loss = 0.0
correct = 0
total = 0
for images, labels in trainloader:
images, labels = images.to(device), labels.to(device)
# 前向传播
outputs = model(images)
loss = criterion(outputs, labels)
# 反向传播和优化
optimizer.zero_grad() # 清零梯度
loss.backward() # 计算梯度
optimizer.step() # 更新参数
# 统计
running_loss += loss.item()
_, predicted = outputs.max(1)
total += labels.size(0)
correct += predicted.eq(labels).sum().item()
scheduler.step()
train_acc = 100. * correct / total
# 每 10 个 epoch 打印一次
if (epoch + 1) % 10 == 0:
print(f'Epoch [{epoch+1}/{num_epochs}], '
f'Loss: {running_loss/len(trainloader):.4f}, '
f'Train Acc: {train_acc:.2f}%')
Step 4: 测试评估
# 测试
model.eval()
correct = 0
total = 0
with torch.no_grad(): # 测试时不需要计算梯度
for images, labels in testloader:
images, labels = images.to(device), labels.to(device)
outputs = model(images)
_, predicted = outputs.max(1)
total += labels.size(0)
correct += predicted.eq(labels).sum().item()
print(f'测试集准确率: {100. * correct / total:.2f}%')
# 这个简单的模型在 CIFAR-10 上通常能达到 85-90% 的准确率
CNN 的感受野计算
理解感受野 (Receptive Field) 对设计网络很重要。感受野指的是输出 feature map 上某个位置对应的输入图像区域大小。简单理解:第 N 层的一个神经元能「看到」原始图像中多大的区域。
# 感受野计算公式 (简化版,假设 stride=1)
# RF_l = RF_{l-1} + (kernel_size - 1) * stride_product_{l-1}
# 以我们的 SimpleCNN 为例:
# Layer kernel stride Receptive Field
# Conv1 (3x3) 3 1 3
# MaxPool1 (2x2) 2 2 5
# Conv2 (3x3) 3 1 9
# MaxPool2 (2x2) 2 2 13
# Conv3 (3x3) 3 1 21
# MaxPool3 (2x2) 2 2 25
# 也就是说,输出层的一个神经元可以看到输入图像中 25x25 的区域
# 对于 32x32 的 CIFAR-10 图片,这已经覆盖了大部分区域
使用预训练模型 (Transfer Learning)
在实际项目中,我们很少从头训练 CNN。更常见的做法是用迁移学习 (Transfer Learning):加载在 ImageNet 上预训练好的模型,然后 fine-tune 到自己的任务上。PyTorch 的 torchvision.models 提供了大量预训练模型。
import torchvision.models as models
# 加载预训练的 ResNet-18
model = models.resnet18(weights=models.ResNet18_Weights.DEFAULT)
# 冻结所有层的参数
for param in model.parameters():
param.requires_grad = False
# 只替换最后的全连接层(适配你的任务)
num_classes = 10 # 比如 CIFAR-10 有 10 个类别
model.fc = nn.Linear(model.fc.in_features, num_classes)
# 只有新的 fc 层会被训练
print(f'可训练参数: {sum(p.numel() for p in model.parameters() if p.requires_grad):,}')
# 用更小的学习率 fine-tune
optimizer = optim.Adam(model.fc.parameters(), lr=1e-3)
# 如果想 fine-tune 整个网络(推荐在训练几轮后再解冻):
# for param in model.parameters():
# param.requires_grad = True
# optimizer = optim.Adam(model.parameters(), lr=1e-5) # 更小的学习率
一些实用技巧
- Data Augmentation 很重要:RandomCrop、RandomHorizontalFlip、ColorJitter、Cutout、Mixup 等,能显著提升泛化能力
- Batch Normalization 是标配:加速收敛、允许更大学习率、有轻微正则化效果
- 用 AdamW 替代 Adam:weight decay 的实现更正确,配合 cosine annealing 效果更好
- Label Smoothing:把 hard label 变成 soft label,
CrossEntropyLoss(label_smoothing=0.1),防止模型过度自信 - 混合精度训练 (AMP):用
torch.cuda.amp可以加速训练且几乎不影响精度
CNN vs Transformer
最后简单对比一下 CNN 和 Vision Transformer:
- 归纳偏置 (Inductive Bias):CNN 有局部性和平移等变性的先验,ViT 几乎没有,需要大量数据才能学到
- 计算效率:在小数据集上 CNN 通常更快更好;大数据集上 ViT 有优势
- 可解释性:CNN 的卷积核可以可视化,直觉上更容易理解
- 趋势:现在很多工作在融合两者优势,比如 ConvNeXt(用 CNN 的架构模仿 ViT 的设计),或者在 ViT 中加入卷积操作
总结
这篇文章从直觉出发,聊了 CNN 的核心组件:卷积层、池化层、全连接层,回顾了经典架构的演进(LeNet → AlexNet → VGG → ResNet),然后用 PyTorch 实现了一个完整的 CNN 图像分类器。
几个关键 takeaways:
- CNN 通过局部连接 + 参数共享大幅减少参数量,同时保留空间信息
- 现代 CNN 的标配是 Conv + BN + ReLU 的组合
- 实际项目中优先考虑迁移学习,用预训练模型 fine-tune
- CNN 的思想(局部特征提取、层次化表示)在深度学习中无处不在
下一篇可能会写一下目标检测 (Object Detection) 或者语义分割 (Semantic Segmentation) 的内容,毕竟 CNN 在 CV 领域的应用实在太广了。Stay tuned 🚀
参考资料: