第二篇:深度学习基础(快速回顾)#
目标读者:有机器学习基础,需要快速掌握深度学习和PyTorch的读者
学习重点:PyTorch实战、神经网络核心概念、CNN基础
篇章概述#
深度学习是计算机视觉的核心技术。本篇将快速回顾深度学习的关键概念,重点放在PyTorch框架和卷积神经网络(CNN)的实战应用。
为什么选择PyTorch?#
- 动态计算图:更符合Python编程习惯,易于调试
- 学术界主流:顶级会议论文大多使用PyTorch实现
- 生态完善:torchvision、torchaudio等丰富的扩展库
- PyTorch 2.x:引入torch.compile,性能大幅提升
章节安排#
第3章:神经网络基础#
- 3.1 从感知机到多层神经网络
- 3.2 反向传播算法详解
- 3.3 激活函数的选择与影响
- 3.4 正则化技术:BatchNorm与Dropout
- 实战:使用PyTorch构建第一个神经网络(MNIST手写数字识别)
核心技能:
- 掌握PyTorch的基本操作(Tensor、autograd、nn.Module)
- 理解神经网络的训练流程
- 学会使用GPU加速训练
第4章:卷积神经网络(CNN)#
- 4.1 卷积层的工作原理
- 4.2 池化层与降维
- 4.3 经典CNN架构:LeNet → AlexNet → VGG
- 4.4 CNN的可视化与理解
- 实战:CIFAR-10图像分类(从零构建CNN)
核心技能:
- 理解卷积操作的本质
- 掌握CNN的设计原则
- 学会使用torchvision进行图像处理
技术栈#
环境要求#
# Python >= 3.10
python --version
# 安装PyTorch (2025年推荐)
# CPU版本
pip install torch torchvision torchaudio
# GPU版本(CUDA 12.1)
pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu121
# 或使用uv(更快)
uv pip install torch torchvision torchaudio核心依赖#
- PyTorch >= 2.0:深度学习框架
- torchvision:计算机视觉工具库
- matplotlib:可视化
- tqdm:进度条
验证安装#
import torch
import torchvision
print(f"PyTorch版本: {torch.__version__}")
print(f"CUDA可用: {torch.cuda.is_available()}")
if torch.cuda.is_available():
print(f"CUDA版本: {torch.version.cuda}")
print(f"GPU设备: {torch.cuda.get_device_name(0)}")学习建议#
1. 动手实践为主#
- 每个代码示例都要运行:不要只看代码
- 修改超参数观察变化:学习率、批次大小、网络层数等
- 尝试不同的数据集:Fashion-MNIST、SVHN等
2. 理解核心概念#
- 梯度下降:深度学习的基石
- 反向传播:如何高效计算梯度
- 正则化:防止过拟合的关键
3. 参考官方文档#
4. 循序渐进#
第3章(1-2天) → 第4章(2-3天)
↓ ↓
理解基础 掌握CNN
↓ ↓
为后续现代架构(ResNet、Transformer)打下坚实基础与前后篇的关系#
第一篇:机器学习基础
↓
(线性模型、优化算法)
↓
第二篇:深度学习基础 ← 当前篇
↓
(神经网络、CNN)
↓
第三篇:现代CNN架构
↓
(ResNet、EfficientNet等)代码规范#
本篇所有代码遵循以下规范:
1. 目录结构#
chapter03/
├── README.md # 理论讲解
└── code/
├── first_nn.py # 完整可运行代码
└── utils.py # 辅助函数(如有)
chapter04/
├── README.md
└── code/
├── cnn_cifar10.py
└── visualize.py # 可视化工具(如有)2. 代码风格#
- 函数职责单一:训练、验证、测试分离
- 类型提示:使用Python 3.10+的类型注解
- 文档字符串:关键函数添加docstring
- GPU支持:自动检测并使用GPU
3. 示例模板#
"""
模块说明
"""
import torch
import torch.nn as nn
from torch.utils.data import DataLoader
def train_one_epoch(
model: nn.Module,
dataloader: DataLoader,
optimizer: torch.optim.Optimizer,
criterion: nn.Module,
device: torch.device
) -> float:
"""训练一个epoch"""
model.train()
total_loss = 0.0
for batch_idx, (data, target) in enumerate(dataloader):
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()
return total_loss / len(dataloader)常见问题#
Q1: PyTorch 1.x vs 2.x 主要区别?#
A: PyTorch 2.x引入torch.compile()用于图优化,可大幅提升性能。API向后兼容,但推荐使用新特性。
Q2: 需要GPU吗?#
A: 不强制,但强烈推荐。第3章代码CPU可运行,第4章建议使用GPU(训练速度差10倍以上)。
Q3: 如何选择学习率?#
A: 初学者可从1e-3开始,根据训练曲线调整。后续章节会介绍学习率调度器。
Q4: BatchNorm和Dropout如何选择?#
A: 两者可同时使用。BatchNorm主要用于加速收敛,Dropout用于防止过拟合。
拓展资源#
书籍#
- 《Deep Learning》(Goodfellow):深度学习圣经
- 《动手学深度学习》(李沐):PyTorch实战
在线课程#
- Fast.ai:自顶向下的深度学习课程
- Stanford CS231n:计算机视觉经典课程
实践平台#
- Kaggle:数据竞赛与学习
- Papers With Code:论文+代码复现
准备好了吗?让我们从第3章开始,构建第一个神经网络!
第3章:神经网络基础#
本章目标:理解神经网络的核心原理,掌握PyTorch构建和训练神经网络的完整流程
3.1 从感知机到多层神经网络#
3.1.1 感知机(Perceptron)#
感知机是最简单的神经网络单元,由Frank Rosenblatt在1957年提出。
数学表达:
y = f(w·x + b)其中:
x:输入向量w:权重向量b:偏置项f:激活函数(通常是阶跃函数)
感知机的局限:
- 只能处理线性可分问题(如无法学习XOR)
- 无法堆叠成深层网络
3.1.2 多层感知机(MLP)#
通过堆叠多层感知机并使用非线性激活函数,可以逼近任意复杂函数(通用逼近定理)。
典型三层MLP结构:
输入层 → 隐藏层1 → 隐藏层2 → 输出层
(784) → (256) → (128) → (10)
↓ ReLU ↓ ReLU ↓ SoftmaxPyTorch实现:
import torch.nn as nn
class SimpleMLP(nn.Module):
def __init__(self, input_size=784, hidden_size=256, num_classes=10):
super(SimpleMLP, self).__init__()
self.fc1 = nn.Linear(input_size, hidden_size)
self.relu = nn.ReLU()
self.fc2 = nn.Linear(hidden_size, num_classes)
def forward(self, x):
x = x.view(x.size(0), -1) # 展平输入
x = self.fc1(x)
x = self.relu(x)
x = self.fc2(x)
return x3.2 反向传播算法详解#
3.2.1 核心思想#
反向传播(Backpropagation)是训练神经网络的核心算法,通过链式法则高效计算梯度。
前向传播:
输入 → 计算输出 → 计算损失
x → ŷ → L(ŷ, y)反向传播:
损失 → 计算梯度 → 更新参数
L → ∂L/∂w → w = w - η·∂L/∂w3.2.2 链式法则#
对于复合函数 L = f(g(h(x))):
∂L/∂x = (∂L/∂f) · (∂f/∂g) · (∂g/∂h) · (∂h/∂x)3.2.3 PyTorch自动微分#
PyTorch的autograd模块自动处理反向传播:
import torch
# 创建需要梯度的张量
x = torch.tensor([2.0], requires_grad=True)
w = torch.tensor([3.0], requires_grad=True)
# 前向传播
y = w * x
loss = (y - 5) ** 2
# 反向传播
loss.backward()
# 查看梯度
print(f"∂loss/∂w = {w.grad}") # -6.0
print(f"∂loss/∂x = {x.grad}") # -9.0重要API:
requires_grad=True:标记需要梯度的张量.backward():自动计算梯度.grad:访问计算的梯度optimizer.zero_grad():清空梯度(必须!)
3.3 激活函数的选择与影响#
3.3.1 为什么需要激活函数?#
如果没有激活函数,多层神经网络等价于单层线性模型:
f(f(x)) = W2·(W1·x) = (W2·W1)·x = W·x3.3.2 常用激活函数#
| 激活函数 | 公式 | 优点 | 缺点 | 使用场景 |
|---|---|---|---|---|
| Sigmoid | σ(x) = 1/(1+e^-x) | 输出范围(0,1) | 梯度消失、计算慢 | 二分类输出层 |
| Tanh | tanh(x) = (e^x-e^-x)/(e^x+e^-x) | 输出范围(-1,1),零中心 | 梯度消失 | RNN(历史原因) |
| ReLU | max(0, x) | 计算快、缓解梯度消失 | 神经元死亡 | 隐藏层首选 |
| LeakyReLU | max(0.01x, x) | 解决神经元死亡 | 需调节负斜率 | ReLU替代方案 |
| GELU | x·Φ(x) | 平滑、性能好 | 计算稍慢 | Transformer标配 |
| Softmax | e^xi/Σe^xj | 输出概率分布 | - | 多分类输出层 |
3.3.3 PyTorch实现#
import torch.nn.functional as F
# 方式1:使用nn.Module(推荐用于网络层)
self.relu = nn.ReLU()
self.leaky_relu = nn.LeakyReLU(negative_slope=0.01)
self.gelu = nn.GELU()
# 方式2:使用函数式API(推荐用于forward)
x = F.relu(x)
x = F.leaky_relu(x, negative_slope=0.01)
x = F.gelu(x)经验法则:
- 隐藏层:优先使用
ReLU,性能不佳时尝试GELU或LeakyReLU - 输出层:二分类用
Sigmoid,多分类用Softmax,回归不用激活函数
3.4 正则化技术:BatchNorm与Dropout#
3.4.1 Batch Normalization#
核心思想:对每个mini-batch的激活值进行标准化,加速收敛并缓解梯度消失。
数学公式:
x_norm = (x - μ_batch) / √(σ_batch² + ε)
y = γ·x_norm + β其中γ和β是可学习参数。
PyTorch实现:
class MLPWithBN(nn.Module):
def __init__(self):
super().__init__()
self.fc1 = nn.Linear(784, 256)
self.bn1 = nn.BatchNorm1d(256) # 注意:1d用于全连接层
self.fc2 = nn.Linear(256, 128)
self.bn2 = nn.BatchNorm1d(128)
self.fc3 = nn.Linear(128, 10)
def forward(self, x):
x = x.view(x.size(0), -1)
x = F.relu(self.bn1(self.fc1(x)))
x = F.relu(self.bn2(self.fc2(x)))
x = self.fc3(x)
return x使用建议:
- 放在激活函数之前或之后都可以(论文有争议,实践中差异不大)
- 通常可以使用更大的学习率
- 训练时
model.train(),测试时model.eval()(BN行为不同)
3.4.2 Dropout#
核心思想:训练时随机"丢弃"部分神经元,防止过拟合。
工作原理:
# 训练时(p=0.5表示丢弃50%的神经元)
mask = torch.rand(x.shape) > 0.5
x = x * mask / 0.5 # 除以0.5保持期望不变
# 测试时:不丢弃,直接使用所有神经元PyTorch实现:
class MLPWithDropout(nn.Module):
def __init__(self, dropout_rate=0.5):
super().__init__()
self.fc1 = nn.Linear(784, 256)
self.dropout1 = nn.Dropout(dropout_rate)
self.fc2 = nn.Linear(256, 128)
self.dropout2 = nn.Dropout(dropout_rate)
self.fc3 = nn.Linear(128, 10)
def forward(self, x):
x = x.view(x.size(0), -1)
x = F.relu(self.fc1(x))
x = self.dropout1(x) # 在激活后使用
x = F.relu(self.fc2(x))
x = self.dropout2(x)
x = self.fc3(x) # 输出层不使用Dropout
return x使用建议:
- 典型dropout rate:0.2~0.5
- CNN中较少使用(BatchNorm效果更好)
- 全连接层中常用
3.4.3 BatchNorm vs Dropout#
| 特性 | BatchNorm | Dropout |
|---|---|---|
| 主要作用 | 加速收敛、缓解梯度消失 | 防止过拟合 |
| 适用场景 | CNN、Transformer | 全连接层 |
| 训练/测试差异 | 有(统计量计算方式不同) | 有(测试时关闭) |
| 是否可同时使用 | 可以(先BN后Dropout) | - |
3.5 实战:MNIST手写数字识别#
3.5.1 任务描述#
- 数据集:MNIST(60,000训练+10,000测试)
- 输入:28×28灰度图像
- 输出:10个类别(数字0-9)
- 目标:准确率>98%
3.5.2 完整代码#
完整代码见:code/chapter03_neural_network/first_nn.py
核心组件:
# 1. 数据加载
train_loader = DataLoader(
datasets.MNIST('./data', train=True, download=True,
transform=transforms.ToTensor()),
batch_size=64, shuffle=True
)
# 2. 模型定义
class MNISTNet(nn.Module):
def __init__(self):
super().__init__()
self.fc1 = nn.Linear(28*28, 256)
self.bn1 = nn.BatchNorm1d(256)
self.dropout1 = nn.Dropout(0.3)
self.fc2 = nn.Linear(256, 128)
self.bn2 = nn.BatchNorm1d(128)
self.dropout2 = nn.Dropout(0.3)
self.fc3 = nn.Linear(128, 10)
def forward(self, x):
x = x.view(x.size(0), -1)
x = F.relu(self.bn1(self.fc1(x)))
x = self.dropout1(x)
x = F.relu(self.bn2(self.fc2(x)))
x = self.dropout2(x)
x = self.fc3(x)
return x
# 3. 训练循环
model = MNISTNet().to(device)
optimizer = torch.optim.Adam(model.parameters(), lr=0.001)
criterion = nn.CrossEntropyLoss()
for epoch in range(10):
train_loss = train_one_epoch(model, train_loader, optimizer, criterion, device)
val_acc = evaluate(model, val_loader, device)
print(f"Epoch {epoch+1}: Loss={train_loss:.4f}, Acc={val_acc:.2%}")3.5.3 运行与调试#
# 进入代码目录
cd /Users/nako/PycharmProjects/Learn/LangChainDemo/ComputerVisionNotes/part2_dl_basics/chapter03/code
# 运行训练
python first_nn.py
# 期望输出
Epoch 1: Loss=0.2345, Acc=93.45%
Epoch 2: Loss=0.1123, Acc=96.78%
...
Epoch 10: Loss=0.0234, Acc=98.56%常见问题排查:
损失不下降
- 检查学习率(太大或太小)
- 确认
optimizer.zero_grad()已调用 - 查看数据是否正确加载
准确率停滞
- 尝试调整网络深度/宽度
- 减小dropout rate
- 增加训练epoch
GPU内存不足
- 减小batch_size
- 使用梯度累积
3.6 PyTorch核心API总结#
3.6.1 张量(Tensor)操作#
# 创建张量
x = torch.tensor([1, 2, 3])
x = torch.zeros(2, 3)
x = torch.randn(2, 3) # 正态分布
# 移动设备
x = x.to(device)
x = x.cuda() # 等价于to('cuda')
# 查看形状
print(x.shape) # torch.Size([2, 3])
print(x.size(0)) # 2
# 变形
x = x.view(3, 2)
x = x.reshape(3, 2) # 更灵活3.6.2 nn.Module核心方法#
class MyModel(nn.Module):
def __init__(self):
super().__init__()
# 定义层
def forward(self, x):
# 定义前向传播
return x
# 训练/评估模式切换
model.train() # 启用Dropout、BatchNorm训练模式
model.eval() # 关闭Dropout、BatchNorm评估模式
# 参数管理
for name, param in model.named_parameters():
print(name, param.shape)
# 保存/加载
torch.save(model.state_dict(), 'model.pth')
model.load_state_dict(torch.load('model.pth'))3.6.3 优化器(Optimizer)#
# 常用优化器
optimizer = torch.optim.SGD(model.parameters(), lr=0.01, momentum=0.9)
optimizer = torch.optim.Adam(model.parameters(), lr=0.001)
optimizer = torch.optim.AdamW(model.parameters(), lr=0.001, weight_decay=0.01)
# 训练步骤
optimizer.zero_grad() # 1. 清空梯度
loss.backward() # 2. 反向传播
optimizer.step() # 3. 更新参数3.6.4 损失函数#
# 分类任务
criterion = nn.CrossEntropyLoss() # 多分类(自带Softmax)
criterion = nn.BCEWithLogitsLoss() # 二分类(自带Sigmoid)
# 回归任务
criterion = nn.MSELoss() # 均方误差
criterion = nn.L1Loss() # 平均绝对误差
# 使用
loss = criterion(output, target)3.7 本章小结#
核心知识点#
- 神经网络结构:输入层、隐藏层、输出层
- 反向传播:链式法则计算梯度
- 激活函数:ReLU(隐藏层)、Softmax(分类输出)
- 正则化:BatchNorm(加速收敛)、Dropout(防止过拟合)
PyTorch编程范式#
# 1. 定义模型
class Model(nn.Module):
def __init__(self): ...
def forward(self, x): ...
# 2. 准备数据
train_loader = DataLoader(dataset, batch_size=64, shuffle=True)
# 3. 设置优化器和损失函数
optimizer = torch.optim.Adam(model.parameters(), lr=0.001)
criterion = nn.CrossEntropyLoss()
# 4. 训练循环
for epoch in range(num_epochs):
for data, target in train_loader:
optimizer.zero_grad()
output = model(data)
loss = criterion(output, target)
loss.backward()
optimizer.step()检查清单#
- 理解感知机→MLP的演进
- 掌握PyTorch自动微分机制
- 会选择合适的激活函数
- 理解BatchNorm和Dropout的作用
- 能独立编写训练代码
- 成功运行MNIST实战代码(准确率>98%)
3.8 练习与思考#
基础练习#
修改网络结构:
- 增加/减少隐藏层数量
- 调整每层神经元数量
- 观察训练速度和准确率变化
超参数调优:
- 学习率:[0.0001, 0.001, 0.01]
- Dropout率:[0.1, 0.3, 0.5, 0.7]
- Batch size:[32, 64, 128, 256]
激活函数对比:
- 分别使用ReLU、LeakyReLU、GELU
- 记录训练曲线差异
进阶挑战#
实现学习率调度:
scheduler = torch.optim.lr_scheduler.StepLR(optimizer, step_size=5, gamma=0.1)数据增强:
transform = transforms.Compose([ transforms.RandomRotation(10), transforms.ToTensor(), ])尝试其他数据集:
- Fashion-MNIST(服装分类)
- KMNIST(日文字符)
思考题#
- 为什么BatchNorm能加速收敛?
- 神经网络越深越好吗?什么时候会出现问题?
- 如何判断模型是过拟合还是欠拟合?
下一章预告:我们将学习卷积神经网络(CNN),它是计算机视觉的基石。相比全连接网络,CNN能更好地处理图像数据,并大幅减少参数量。
继续学习 → 第4章:卷积神经网络(CNN)
第4章:卷积神经网络(CNN)#
本章目标:理解卷积操作的本质,掌握CNN的设计原则,能独立构建CNN进行图像分类
4.1 为什么需要CNN?#
4.1.1 全连接网络的局限#
回顾第3章的MNIST分类器,我们使用全连接网络:
28×28图像 → 展平成784维向量 → MLP → 10个类别存在的问题:
- 参数量爆炸:对于32×32的RGB图像(3072维),第一层若有256个神经元,参数量=3072×256≈78万
- 丢失空间信息:展平操作破坏了像素的空间位置关系
- 无法处理不同尺寸输入:输入尺寸固定
4.1.2 CNN的核心思想#
**卷积神经网络(Convolutional Neural Network)**基于三个关键假设:
- 局部连接:相邻像素比远距离像素更相关
- 权值共享:同一个特征(如边缘)在图像不同位置都有用
- 平移不变性:特征检测器可在任意位置工作
结果:大幅减少参数量,同时保留空间结构!
4.2 卷积层的工作原理#
4.2.1 卷积操作#
卷积是一种滑动窗口操作:
输入图像(5×5) 卷积核(3×3) 输出特征图(3×3)
┌─────────────┐ ┌─────┐ ┌─────────┐
│ 1 2 3 4 5 │ │ 1 0 -1 │ │ │
│ 6 7 8 9 10 │ * │ 1 0 -1 │ → │ 特征 │
│11 12 13 14 15 │ │ 1 0 -1 │ │ 值 │
│16 17 18 19 20 │ └─────┘ │ │
│21 22 23 24 25 │ └─────────┘
└─────────────┘数学定义:
输出[i,j] = Σ Σ 输入[i+m, j+n] × 卷积核[m, n] + 偏置
m n4.2.2 关键概念#
| 参数 | 说明 | 典型值 |
|---|---|---|
| 卷积核大小(Kernel Size) | 感受野大小 | 3×3, 5×5 |
| 步长(Stride) | 滑动步长 | 1(不跳过), 2(降采样) |
| 填充(Padding) | 边缘补零 | 0(valid), (k-1)/2(same) |
| 通道数(Channels) | 输入/输出特征图数量 | RGB输入=3, 隐藏层32/64/128 |
输出尺寸计算:
输出高度 = ⌊(输入高度 + 2×padding - kernel_size) / stride⌋ + 1
输出宽度 = ⌊(输入宽度 + 2×padding - kernel_size) / stride⌋ + 14.2.3 PyTorch实现#
import torch.nn as nn
# 单个卷积层
conv = nn.Conv2d(
in_channels=3, # 输入通道(RGB)
out_channels=32, # 输出通道(学习32个特征)
kernel_size=3, # 3×3卷积核
stride=1, # 步长1
padding=1 # 填充1(保持尺寸)
)
# 输入: [batch_size, 3, 32, 32]
x = torch.randn(8, 3, 32, 32)
# 输出: [batch_size, 32, 32, 32]
out = conv(x)常用卷积块模式:
# Conv → BatchNorm → ReLU 模式
class ConvBlock(nn.Module):
def __init__(self, in_channels, out_channels):
super().__init__()
self.conv = nn.Conv2d(in_channels, out_channels, kernel_size=3, padding=1)
self.bn = nn.BatchNorm2d(out_channels)
self.relu = nn.ReLU(inplace=True)
def forward(self, x):
return self.relu(self.bn(self.conv(x)))4.3 池化层与降维#
4.3.1 池化操作#
目的:降低特征图分辨率,减少参数量,增强不变性。
最大池化(Max Pooling):
输入(4×4) 2×2 Max Pooling 输出(2×2)
┌────────────┐ stride=2 ┌──────┐
│ 1 3 2 4 │ │ 7 8 │
│ 5 7 6 8 │ ──────────> │15 16 │
│ 9 11 10 12 │ └──────┘
│13 15 14 16 │
└────────────┘平均池化(Average Pooling):取区域平均值而非最大值。
4.3.2 PyTorch实现#
# 最大池化(更常用)
maxpool = nn.MaxPool2d(kernel_size=2, stride=2)
# 平均池化
avgpool = nn.AvgPool2d(kernel_size=2, stride=2)
# 全局平均池化(常用于分类网络末端)
global_avgpool = nn.AdaptiveAvgPool2d((1, 1)) # 输出固定为1×1
# 示例
x = torch.randn(8, 32, 32, 32)
out = maxpool(x) # [8, 32, 16, 16]4.3.3 池化 vs 步长卷积#
| 特性 | 池化 | 步长卷积(stride>1) |
|---|---|---|
| 参数量 | 0(无参数) | 有参数 |
| 可学习性 | 否 | 是 |
| 使用趋势 | 减少 | 增加 |
现代趋势:很多新架构(如ResNet)倾向于使用stride=2的卷积代替池化。
4.4 经典CNN架构演进#
4.4.1 LeNet-5 (1998)#
历史地位:第一个成功的CNN,用于手写数字识别。
架构:
输入(32×32) → Conv5×5(6) → AvgPool → Conv5×5(16) → AvgPool → FC(120) → FC(84) → FC(10)PyTorch实现:
class LeNet5(nn.Module):
def __init__(self, num_classes=10):
super().__init__()
self.features = nn.Sequential(
nn.Conv2d(1, 6, kernel_size=5, padding=2),
nn.ReLU(),
nn.AvgPool2d(kernel_size=2, stride=2),
nn.Conv2d(6, 16, kernel_size=5),
nn.ReLU(),
nn.AvgPool2d(kernel_size=2, stride=2)
)
self.classifier = nn.Sequential(
nn.Linear(16 * 5 * 5, 120),
nn.ReLU(),
nn.Linear(120, 84),
nn.ReLU(),
nn.Linear(84, num_classes)
)
def forward(self, x):
x = self.features(x)
x = x.view(x.size(0), -1)
x = self.classifier(x)
return x4.4.2 AlexNet (2012)#
历史地位:ImageNet竞赛冠军,引爆深度学习热潮。
关键创新:
- 使用ReLU替代Sigmoid/Tanh
- 引入Dropout防止过拟合
- 使用数据增强(随机裁剪、翻转)
- GPU并行训练
架构特点:
- 5个卷积层 + 3个全连接层
- 约6000万参数
使用torchvision预训练模型:
from torchvision.models import alexnet, AlexNet_Weights
# 加载预训练权重
weights = AlexNet_Weights.DEFAULT
model = alexnet(weights=weights)
model.eval()
# 修改最后一层用于自定义分类任务
num_classes = 10
model.classifier[6] = nn.Linear(4096, num_classes)4.4.3 VGG (2014)#
核心思想:更深的网络(16-19层),统一使用3×3卷积核。
设计原则:
- 所有卷积核都是3×3:两个3×3卷积感受野=一个5×5,但参数更少
- 每次池化后通道数翻倍:64→128→256→512→512
- 结构规整:易于理解和实现
VGG16架构:
块1: Conv3×3(64)×2 → MaxPool
块2: Conv3×3(128)×2 → MaxPool
块3: Conv3×3(256)×3 → MaxPool
块4: Conv3×3(512)×3 → MaxPool
块5: Conv3×3(512)×3 → MaxPool
全连接: FC(4096) → FC(4096) → FC(1000)PyTorch实现(简化版):
class VGG16(nn.Module):
def __init__(self, num_classes=1000):
super().__init__()
# 特征提取层
self.features = nn.Sequential(
# 块1
nn.Conv2d(3, 64, kernel_size=3, padding=1),
nn.ReLU(inplace=True),
nn.Conv2d(64, 64, kernel_size=3, padding=1),
nn.ReLU(inplace=True),
nn.MaxPool2d(kernel_size=2, stride=2),
# 块2
nn.Conv2d(64, 128, kernel_size=3, padding=1),
nn.ReLU(inplace=True),
nn.Conv2d(128, 128, kernel_size=3, padding=1),
nn.ReLU(inplace=True),
nn.MaxPool2d(kernel_size=2, stride=2),
# 块3
nn.Conv2d(128, 256, kernel_size=3, padding=1),
nn.ReLU(inplace=True),
nn.Conv2d(256, 256, kernel_size=3, padding=1),
nn.ReLU(inplace=True),
nn.Conv2d(256, 256, kernel_size=3, padding=1),
nn.ReLU(inplace=True),
nn.MaxPool2d(kernel_size=2, stride=2),
# 块4
nn.Conv2d(256, 512, kernel_size=3, padding=1),
nn.ReLU(inplace=True),
nn.Conv2d(512, 512, kernel_size=3, padding=1),
nn.ReLU(inplace=True),
nn.Conv2d(512, 512, kernel_size=3, padding=1),
nn.ReLU(inplace=True),
nn.MaxPool2d(kernel_size=2, stride=2),
# 块5
nn.Conv2d(512, 512, kernel_size=3, padding=1),
nn.ReLU(inplace=True),
nn.Conv2d(512, 512, kernel_size=3, padding=1),
nn.ReLU(inplace=True),
nn.Conv2d(512, 512, kernel_size=3, padding=1),
nn.ReLU(inplace=True),
nn.MaxPool2d(kernel_size=2, stride=2),
)
# 分类层
self.classifier = nn.Sequential(
nn.Linear(512 * 7 * 7, 4096),
nn.ReLU(inplace=True),
nn.Dropout(0.5),
nn.Linear(4096, 4096),
nn.ReLU(inplace=True),
nn.Dropout(0.5),
nn.Linear(4096, num_classes),
)
def forward(self, x):
x = self.features(x)
x = x.view(x.size(0), -1)
x = self.classifier(x)
return x使用torchvision:
from torchvision.models import vgg16, VGG16_Weights
weights = VGG16_Weights.DEFAULT
model = vgg16(weights=weights)4.4.4 架构对比#
| 模型 | 年份 | 层数 | 参数量 | Top-5错误率 | 关键创新 |
|---|---|---|---|---|---|
| LeNet-5 | 1998 | 7 | 60K | - | 首个成功CNN |
| AlexNet | 2012 | 8 | 60M | 15.3% | ReLU+Dropout+GPU |
| VGG16 | 2014 | 16 | 138M | 7.3% | 小卷积核+深网络 |
4.5 实战:CIFAR-10图像分类#
4.5.1 任务描述#
CIFAR-10数据集:
- 图像数量:60,000张(50,000训练+10,000测试)
- 图像尺寸:32×32 RGB彩色图像
- 类别数:10类(飞机、汽车、鸟、猫、鹿、狗、青蛙、马、船、卡车)
- 难度:比MNIST高(彩色、类内差异大、背景复杂)
目标:构建CNN达到>75%准确率(随机猜测=10%)
4.5.2 网络设计#
我们将构建一个现代化的小型CNN:
架构:
输入(3, 32, 32)
↓
Conv Block1: Conv3×3(64)×2 + MaxPool → (64, 16, 16)
↓
Conv Block2: Conv3×3(128)×2 + MaxPool → (128, 8, 8)
↓
Conv Block3: Conv3×3(256)×2 + MaxPool → (256, 4, 4)
↓
Global AvgPool → (256, 1, 1)
↓
Flatten + Dropout + FC(10)4.5.3 完整代码#
详见:code/chapter04_cnn/cnn_cifar10.py
核心模型定义:
class CIFAR10CNN(nn.Module):
def __init__(self, num_classes=10):
super().__init__()
# 卷积块1: 32×32 → 16×16
self.conv1 = nn.Sequential(
nn.Conv2d(3, 64, kernel_size=3, padding=1),
nn.BatchNorm2d(64),
nn.ReLU(inplace=True),
nn.Conv2d(64, 64, kernel_size=3, padding=1),
nn.BatchNorm2d(64),
nn.ReLU(inplace=True),
nn.MaxPool2d(kernel_size=2, stride=2)
)
# 卷积块2: 16×16 → 8×8
self.conv2 = nn.Sequential(
nn.Conv2d(64, 128, kernel_size=3, padding=1),
nn.BatchNorm2d(128),
nn.ReLU(inplace=True),
nn.Conv2d(128, 128, kernel_size=3, padding=1),
nn.BatchNorm2d(128),
nn.ReLU(inplace=True),
nn.MaxPool2d(kernel_size=2, stride=2)
)
# 卷积块3: 8×8 → 4×4
self.conv3 = nn.Sequential(
nn.Conv2d(128, 256, kernel_size=3, padding=1),
nn.BatchNorm2d(256),
nn.ReLU(inplace=True),
nn.Conv2d(256, 256, kernel_size=3, padding=1),
nn.BatchNorm2d(256),
nn.ReLU(inplace=True),
nn.MaxPool2d(kernel_size=2, stride=2)
)
# 全局平均池化
self.global_pool = nn.AdaptiveAvgPool2d((1, 1))
# 分类器
self.classifier = nn.Sequential(
nn.Dropout(0.5),
nn.Linear(256, num_classes)
)
def forward(self, x):
x = self.conv1(x)
x = self.conv2(x)
x = self.conv3(x)
x = self.global_pool(x)
x = x.view(x.size(0), -1)
x = self.classifier(x)
return x4.5.4 数据增强#
提升模型泛化能力的关键技术:
from torchvision import transforms
# 训练集增强
train_transform = transforms.Compose([
transforms.RandomCrop(32, padding=4), # 随机裁剪
transforms.RandomHorizontalFlip(), # 随机水平翻转
transforms.ColorJitter( # 颜色抖动
brightness=0.2,
contrast=0.2,
saturation=0.2
),
transforms.ToTensor(),
transforms.Normalize( # 标准化
mean=[0.4914, 0.4822, 0.4465],
std=[0.2470, 0.2435, 0.2616]
)
])
# 测试集不增强
test_transform = transforms.Compose([
transforms.ToTensor(),
transforms.Normalize(
mean=[0.4914, 0.4822, 0.4465],
std=[0.2470, 0.2435, 0.2616]
)
])4.5.5 运行与结果#
# 运行训练
python cnn_cifar10.py
# 期望结果(30 epochs)
Epoch 1: Train Loss=1.6234, Val Loss=1.3456, Val Acc=52.34%
Epoch 10: Train Loss=0.8123, Val Loss=0.9234, Val Acc=68.45%
Epoch 20: Train Loss=0.5234, Val Loss=0.7123, Val Acc=75.67%
Epoch 30: Train Loss=0.3456, Val Loss=0.6789, Val Acc=78.23%
测试集准确率: 77.56%4.6 CNN可视化与理解#
4.6.1 为什么要可视化?#
- 调试模型:检查是否学到有意义的特征
- 解释预测:理解模型为什么做出某个判断
- 发现问题:如数据偏差、过拟合等
4.6.2 可视化技术#
1. 卷积核可视化
查看第一层卷积核学到的模式:
def visualize_filters(model, layer_name='conv1'):
"""可视化第一层卷积核"""
layer = dict(model.named_modules())[layer_name]
weights = layer.weight.data.cpu() # [out_channels, in_channels, H, W]
# 绘制前16个卷积核
fig, axes = plt.subplots(4, 4, figsize=(8, 8))
for i, ax in enumerate(axes.flat):
if i < weights.size(0):
# 取第i个卷积核的第一个通道
kernel = weights[i, 0]
ax.imshow(kernel, cmap='gray')
ax.axis('off')
plt.show()2. 特征图可视化
查看中间层的激活值:
def visualize_feature_maps(model, image, layer_name='conv1'):
"""可视化某层的特征图"""
activation = {}
def hook(module, input, output):
activation['features'] = output.detach()
# 注册钩子
layer = dict(model.named_modules())[layer_name]
handle = layer.register_forward_hook(hook)
# 前向传播
model.eval()
with torch.no_grad():
_ = model(image.unsqueeze(0))
# 可视化
features = activation['features'].squeeze(0) # [C, H, W]
fig, axes = plt.subplots(4, 8, figsize=(16, 8))
for i, ax in enumerate(axes.flat):
if i < features.size(0):
ax.imshow(features[i].cpu(), cmap='viridis')
ax.axis('off')
plt.show()
handle.remove()3. 类激活图(CAM)
显示模型关注图像的哪些区域:
# 使用Grad-CAM等技术(后续章节详细介绍)4.7 本章小结#
核心知识点#
- 卷积层:局部连接+权值共享,大幅减少参数
- 池化层:降低分辨率,增强不变性
- 经典架构:LeNet(开创) → AlexNet(引爆) → VGG(深化)
- 设计原则:
- 通道数逐渐增加(64→128→256→512)
- 特征图尺寸逐渐减小(卷积+池化)
- 使用BatchNorm加速训练
- 数据增强提升泛化
CNN vs MLP#
| 特性 | CNN | MLP |
|---|---|---|
| 参数量 | 少(权值共享) | 多 |
| 空间信息 | 保留 | 丢失 |
| 平移不变性 | 有 | 无 |
| 适用场景 | 图像、视频 | 表格数据 |
检查清单#
- 理解卷积操作的数学定义
- 掌握卷积核、步长、填充的作用
- 能计算卷积层输出尺寸
- 理解VGG的设计原则
- 成功运行CIFAR-10代码(准确率>75%)
- 会使用torchvision加载预训练模型
4.8 练习与思考#
基础练习#
修改CIFAR-10网络:
- 增加/减少卷积块数量
- 调整每层通道数
- 观察参数量和性能变化
数据增强实验:
- 对比有/无数据增强的效果
- 尝试不同增强策略(旋转、缩放等)
使用预训练模型:
from torchvision.models import resnet18 model = resnet18(weights=None) # 从头训练 model.fc = nn.Linear(512, 10) # 修改最后一层
进阶挑战#
实现ResNet基本块:
class ResidualBlock(nn.Module): def __init__(self, in_channels, out_channels): super().__init__() self.conv1 = nn.Conv2d(in_channels, out_channels, 3, padding=1) self.bn1 = nn.BatchNorm2d(out_channels) self.conv2 = nn.Conv2d(out_channels, out_channels, 3, padding=1) self.bn2 = nn.BatchNorm2d(out_channels) def forward(self, x): residual = x out = F.relu(self.bn1(self.conv1(x))) out = self.bn2(self.conv2(out)) out += residual # 残差连接 out = F.relu(out) return out可视化卷积核和特征图
尝试其他数据集:
- CIFAR-100(100个类别)
- Tiny ImageNet
思考题#
- 为什么VGG使用3×3卷积而不是更大的卷积核?
- 什么时候应该使用池化?什么时候使用步长卷积?
- 如何判断网络容量是否合适?(过拟合vs欠拟合)
恭喜!你已经掌握了CNN的核心知识。
第二篇(深度学习基础)到此结束。接下来的第三篇,我们将学习现代CNN架构,包括:
- ResNet:残差连接解决深度网络退化问题
- Inception:多尺度特征融合
- EfficientNet:神经架构搜索(NAS)
这些现代架构是当前计算机视觉应用的基石,是从学术研究走向工业应用的桥梁。
继续学习 → 第三篇:现代CNN架构