CNN 卷积神经网络
2558 字约 9 分钟
2026-05-20
CNN 是处理网格状数据(图像、视频、音频频谱)的专用架构。它通过卷积操作自动学习局部特征,层层抽象——从低层的边缘、纹理,到高层的形状、语义。
2012年,AlexNet 在 ImageNet 竞赛上以 10% 的优势击败传统方法,正式开启了深度学习时代。
1. 卷积运算
1.1 二维卷积
用一个小的滤波器(kernel)在图像上滑动,做点积运算:
输入图像(5×5) 卷积核(3×3) 输出特征图(3×3)
┌─────────────┐ ┌─────────┐ ┌─────────┐
│ 1 2 3 0 1│ │1 0 1 │ │4 3 4 │
│ 0 0 1 2 3│ × │0 1 0 │ = │2 4 3 │
│ 1 2 3 4 1│ │1 0 1 │ │2 3 4 │
│ 0 1 2 3 0│ └─────────┘ └─────────┘
│ 1 0 1 2 3│
└─────────────┘输出尺寸(无 padding):
nout=⌊snin−k+2p⌋+1
- n_in:输入大小
- k:卷积核大小
- p:padding 大小
- s:步长(stride)
1.2 关键特性
局部连接(Local Connectivity):每个神经元只和输入的局部区域连接,而不是全连接。图像中的局部相关性更强(相邻像素关联大),局部连接是一种先验知识。
参数共享(Parameter Sharing):同一个卷积核在整张图上共享权重。好处:
- 大幅减少参数量(不用每个位置学一个独立滤波器)
- 平移不变性(猫在图片左边和右边都能被同一个"猫探测器"找到)
多通道多滤波器:
- 输入 RGB 图像:3个输入通道
- 64个不同的卷积核:提取64种不同类型的特征
- 输出:64个特征图(feature maps)
import torch.nn as nn
# Conv2d 参数详解
conv = nn.Conv2d(
in_channels=3, # 输入通道数(RGB=3)
out_channels=64, # 滤波器数量(输出特征图数)
kernel_size=3, # 3×3 卷积核
stride=1, # 步长
padding=1, # padding=1 保持尺寸不变(same padding)
bias=True # 偏置
)
# 输入形状:(batch, 3, H, W)
# 输出形状:(batch, 64, H, W) # padding=1 时保持 H, W 不变1.3 1×1 卷积
看起来没意义,实际上非常有用:
- 跨通道信息融合:对每个位置的所有通道做线性组合
- 降维:从 256 通道降到 64 通道,大幅减少计算量
- 增加非线性:通过 1×1 conv + 激活函数增加模型表达能力(Inception 中大量使用)
2. 池化(Pooling)
2.1 最大池化(MaxPool)
在局部区域内取最大值,保留最显著的特征:
输入(4×4) 2×2 MaxPool(步长2) 输出(2×2)
┌──────────┐ ┌──────┐
│ 1 3 2 4│ → max(1,3,0,2)=3 │ 3 4 │
│ 0 2 1 3│ → max(2,4,2,3)=4 │ 3 4 │
│ 2 1 3 2│ → max(2,1,3,1)=3 └──────┘
│ 1 0 1 4│ → max(3,2,1,4)=4
└──────────┘作用:
- 减小特征图大小,降低计算量
- 增大感受野(后续层能看到更大范围的输入)
- 一定程度的平移不变性
2.2 平均池化(AvgPool)
取区域平均值,保留更多信息。常用于网络最后的全局平均池化(Global Average Pooling, GAP)。
Global Average Pooling:将每个特征图压缩为一个值,代替全连接层,大幅减少参数:
# 传统方式:Flatten + FC
nn.Flatten()
nn.Linear(512 * 7 * 7, 1000) # 参数量:25,088,000
# Global Average Pooling:更少参数,效果不差
nn.AdaptiveAvgPool2d(1) # 每个特征图→标量
nn.Flatten()
nn.Linear(512, 1000) # 参数量:512,0003. 经典架构演进
3.1 LeNet-5(1998)— 奠基者
Yann LeCun 用于手写数字识别(MNIST),开创了卷积神经网络范式。
结构:Conv → AvgPool → Conv → AvgPool → FC → FC → 输出
参数量约 60K,当时已经是大模型了,现在看来非常小巧。
3.2 AlexNet(2012)— 深度学习元年
ImageNet 2012 Top-5 错误率:15.3%(第二名26.2%),一举奠定深度学习地位。
创新点:
- 首次使用 ReLU(解决 Sigmoid 梯度消失)
- Dropout(首次大规模应用)
- 数据增强(随机裁剪、翻转、颜色抖动)
- GPU 训练(两块 GTX 580,并行训练)
- 局部响应归一化(LRN)(后被 BN 取代)
# 简化版 AlexNet
class AlexNet(nn.Module):
def __init__(self, num_classes=1000):
super().__init__()
self.features = nn.Sequential(
nn.Conv2d(3, 64, 11, stride=4, padding=2), nn.ReLU(),
nn.MaxPool2d(3, stride=2),
nn.Conv2d(64, 192, 5, padding=2), nn.ReLU(),
nn.MaxPool2d(3, stride=2),
nn.Conv2d(192, 384, 3, padding=1), nn.ReLU(),
nn.Conv2d(384, 256, 3, padding=1), nn.ReLU(),
nn.Conv2d(256, 256, 3, padding=1), nn.ReLU(),
nn.MaxPool2d(3, stride=2),
)
self.classifier = nn.Sequential(
nn.Dropout(0.5),
nn.Linear(256 * 6 * 6, 4096), nn.ReLU(), nn.Dropout(0.5),
nn.Linear(4096, 4096), nn.ReLU(),
nn.Linear(4096, num_classes),
)
def forward(self, x):
x = self.features(x)
x = x.view(x.size(0), -1)
return self.classifier(x)3.3 VGGNet(2014)— 深度的力量
核心思想:只用 3×3 卷积核(最小的能捕捉方向信息的核),通过叠加来等效更大的感受野。
两个 3×3 conv 感受野 = 一个 5×5 conv,但参数量更少(2×3²=18 vs 5²=25),且中间有激活函数,非线性更强。
VGG-16(16层),ImageNet Top-1: 71.5%。结构简单,至今是迁移学习的常用骨干网络。
3.4 GoogLeNet/Inception(2014)— 宽度与并行
Inception 模块:在同一层并行使用不同大小的卷积核(1×1、3×3、5×5)和池化,让网络自己选择最合适的特征提取粒度:
class InceptionModule(nn.Module):
def __init__(self, in_channels, n1x1, n3x3_reduce, n3x3, n5x5_reduce, n5x5, pool_proj):
super().__init__()
# 1×1 分支
self.branch1 = nn.Sequential(nn.Conv2d(in_channels, n1x1, 1), nn.ReLU())
# 3×3 分支(先用1×1降维)
self.branch2 = nn.Sequential(
nn.Conv2d(in_channels, n3x3_reduce, 1), nn.ReLU(),
nn.Conv2d(n3x3_reduce, n3x3, 3, padding=1), nn.ReLU()
)
# 5×5 分支(先用1×1降维)
self.branch3 = nn.Sequential(
nn.Conv2d(in_channels, n5x5_reduce, 1), nn.ReLU(),
nn.Conv2d(n5x5_reduce, n5x5, 5, padding=2), nn.ReLU()
)
# 池化分支
self.branch4 = nn.Sequential(
nn.MaxPool2d(3, stride=1, padding=1),
nn.Conv2d(in_channels, pool_proj, 1), nn.ReLU()
)
def forward(self, x):
return torch.cat([
self.branch1(x), self.branch2(x),
self.branch3(x), self.branch4(x)
], dim=1) # 在通道维度拼接3.5 ResNet(2015)— 残差连接改变一切 ⭐
问题:深层网络(>20层)训练时性能反而下降(不是过拟合,是优化困难)。
解决方案:残差连接(Skip Connection / Shortcut)
F(x)+x
直觉:让网络学习"残差",而不是学习完整映射。如果恒等映射是最优的,只需让 F(x)→0 即可,比学习恒等映射容易得多。
class ResidualBlock(nn.Module):
def __init__(self, in_channels, out_channels, stride=1):
super().__init__()
self.conv1 = nn.Conv2d(in_channels, out_channels, 3, stride=stride, padding=1, bias=False)
self.bn1 = nn.BatchNorm2d(out_channels)
self.conv2 = nn.Conv2d(out_channels, out_channels, 3, padding=1, bias=False)
self.bn2 = nn.BatchNorm2d(out_channels)
self.relu = nn.ReLU(inplace=True)
# 当维度不匹配时需要投影
self.shortcut = nn.Sequential()
if stride != 1 or in_channels != out_channels:
self.shortcut = nn.Sequential(
nn.Conv2d(in_channels, out_channels, 1, stride=stride, bias=False),
nn.BatchNorm2d(out_channels)
)
def forward(self, x):
identity = self.shortcut(x)
out = self.relu(self.bn1(self.conv1(x)))
out = self.bn2(self.conv2(out))
out = out + identity # 残差连接!
out = self.relu(out)
return out残差连接使得训练超过100层的网络成为可能(ResNet-152),并带来了梯度高速公路——梯度可以直接通过 skip connection 流回浅层。
3.6 EfficientNet(2019)— 复合缩放
关键发现:同比例扩展网络的深度(层数)、宽度(通道数)、分辨率(输入图像大小),比单独扩展任一维度效果更好。
用 NAS(神经网络架构搜索)找到最优缩放系数,在相同参数量下达到 SOTA 效果。
4. 迁移学习
4.1 为什么迁移学习有效?
CNN 的低层特征(边缘、纹理、颜色)是通用的,对各种视觉任务都有用。在 ImageNet 上预训练的网络,其特征提取能力可以迁移到其他任务。
4.2 迁移学习策略
策略1:特征提取(冻结全部) 适合:目标数据集很小,且和预训练任务相似
import torchvision.models as models
model = models.resnet50(weights='IMAGENET1K_V2')
# 冻结所有参数
for param in model.parameters():
param.requires_grad = False
# 替换输出层
num_features = model.fc.in_features
model.fc = nn.Linear(num_features, num_classes) # 只有这层被训练策略2:Fine-tuning(解冻部分) 适合:目标数据集中等大小,或和预训练任务差异较大
model = models.resnet50(weights='IMAGENET1K_V2')
# 冻结前面的层,解冻后面的层
for name, param in model.named_parameters():
if 'layer4' in name or 'fc' in name:
param.requires_grad = True
else:
param.requires_grad = False
# 不同层使用不同学习率
optimizer = optim.Adam([
{'params': model.fc.parameters(), 'lr': 1e-3},
{'params': model.layer4.parameters(), 'lr': 1e-4},
], lr=1e-4)策略3:全量 Fine-tuning 适合:目标数据集较大,或差异很大
model = models.resnet50(weights='IMAGENET1K_V2')
# 所有参数都训练,但学习率要小(避免破坏预训练权重)
optimizer = optim.Adam(model.parameters(), lr=1e-5)5. 数据增强
图像数据增强是提升泛化能力的关键,相当于免费扩充训练集:
from torchvision import transforms
# 训练集增强
train_transform = transforms.Compose([
transforms.RandomResizedCrop(224, scale=(0.8, 1.0)), # 随机裁剪并缩放
transforms.RandomHorizontalFlip(p=0.5), # 水平翻转
transforms.RandomVerticalFlip(p=0.2),
transforms.RandomRotation(degrees=15),
transforms.ColorJitter(
brightness=0.2, contrast=0.2,
saturation=0.2, hue=0.1
),
transforms.RandomGrayscale(p=0.1),
transforms.ToTensor(),
transforms.Normalize(
mean=[0.485, 0.456, 0.406], # ImageNet 统计量
std=[0.229, 0.224, 0.225]
)
])
# 验证集/测试集不做随机增强
val_transform = transforms.Compose([
transforms.Resize(256),
transforms.CenterCrop(224),
transforms.ToTensor(),
transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])
])高级增强(Albumentations 库):
import albumentations as A
from albumentations.pytorch import ToTensorV2
transform = A.Compose([
A.RandomResizedCrop(224, 224),
A.HorizontalFlip(p=0.5),
A.ShiftScaleRotate(shift_limit=0.1, scale_limit=0.1, rotate_limit=30),
A.OneOf([
A.GaussianBlur(),
A.MedianBlur(),
A.MotionBlur(),
], p=0.3),
A.CoarseDropout(max_holes=8, max_height=32, max_width=32, p=0.3), # CutOut
A.Normalize(mean=(0.485, 0.456, 0.406), std=(0.229, 0.224, 0.225)),
ToTensorV2(),
])6. 目标检测
6.1 核心任务
在图像中找到目标的位置(bounding box)并识别类别。
6.2 YOLO 系列(实时检测)
思路:将图像分成 S×S 的网格,每个格子预测 B 个边界框和置信度,以及类别概率。
# 使用 YOLOv8(最新,最简单)
from ultralytics import YOLO
model = YOLO('yolov8n.pt') # n=nano(最小), s/m/l/x(越来越大)
# 推理
results = model('image.jpg')
for result in results:
boxes = result.boxes # bounding boxes
print(boxes.xyxy) # 坐标
print(boxes.conf) # 置信度
print(boxes.cls) # 类别
# 训练自定义数据集
model = YOLO('yolov8n.pt')
model.train(data='dataset.yaml', epochs=100, imgsz=640, batch=16)6.3 非极大值抑制(NMS)
模型会对同一目标产生多个候选框,NMS 用来选出最好的那个:
- 按置信度从高到低排序
- 取置信度最高的框
- 删除与它 IoU > 阈值的所有其他框
- 重复直到所有框处理完毕
IoU(交并比):
IoU=预测框∪真实框预测框∩真实框
7. 完整 CNN 分类器训练示例
import torch
import torch.nn as nn
import torchvision
from torchvision import datasets, transforms, models
from torch.utils.data import DataLoader
# 准备数据
train_dataset = datasets.ImageFolder('data/train', transform=train_transform)
val_dataset = datasets.ImageFolder('data/val', transform=val_transform)
train_loader = DataLoader(train_dataset, batch_size=64, shuffle=True,
num_workers=4, pin_memory=True)
val_loader = DataLoader(val_dataset, batch_size=64, shuffle=False,
num_workers=4, pin_memory=True)
# 加载预训练模型
model = models.efficientnet_b0(weights='IMAGENET1K_V1')
model.classifier[1] = nn.Linear(model.classifier[1].in_features, num_classes)
model = model.to(device)
# 使用标签平滑(防止过拟合)
criterion = nn.CrossEntropyLoss(label_smoothing=0.1)
optimizer = optim.AdamW(model.parameters(), lr=1e-3, weight_decay=1e-2)
scheduler = optim.lr_scheduler.OneCycleLR(
optimizer, max_lr=1e-3,
steps_per_epoch=len(train_loader), epochs=50
)
# 混合精度训练(加速)
from torch.cuda.amp import autocast, GradScaler
scaler = GradScaler()
for epoch in range(50):
model.train()
for images, labels in train_loader:
images, labels = images.to(device), labels.to(device)
optimizer.zero_grad()
with autocast(): # 自动使用 FP16
outputs = model(images)
loss = criterion(outputs, labels)
scaler.scale(loss).backward()
scaler.unscale_(optimizer)
nn.utils.clip_grad_norm_(model.parameters(), 1.0)
scaler.step(optimizer)
scaler.update()
scheduler.step()