第二篇:深度学习基础(快速回顾)#

目标读者:有机器学习基础,需要快速掌握深度学习和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    ↓ Softmax

PyTorch实现:

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 x

3.2 反向传播算法详解#

3.2.1 核心思想#

反向传播(Backpropagation)是训练神经网络的核心算法,通过链式法则高效计算梯度。

前向传播:

输入 → 计算输出 → 计算损失
x → ŷ → L(ŷ, y)

反向传播:

损失 → 计算梯度 → 更新参数
L → ∂L/∂w → w = w - η·∂L/∂w

3.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·x

3.3.2 常用激活函数#

激活函数公式优点缺点使用场景
Sigmoidσ(x) = 1/(1+e^-x)输出范围(0,1)梯度消失、计算慢二分类输出层
Tanhtanh(x) = (e^x-e^-x)/(e^x+e^-x)输出范围(-1,1),零中心梯度消失RNN(历史原因)
ReLUmax(0, x)计算快、缓解梯度消失神经元死亡隐藏层首选
LeakyReLUmax(0.01x, x)解决神经元死亡需调节负斜率ReLU替代方案
GELUx·Φ(x)平滑、性能好计算稍慢Transformer标配
Softmaxe^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,性能不佳时尝试GELULeakyReLU
  • 输出层:二分类用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#

特性BatchNormDropout
主要作用加速收敛、缓解梯度消失防止过拟合
适用场景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%

常见问题排查:

  1. 损失不下降

    • 检查学习率(太大或太小)
    • 确认optimizer.zero_grad()已调用
    • 查看数据是否正确加载
  2. 准确率停滞

    • 尝试调整网络深度/宽度
    • 减小dropout rate
    • 增加训练epoch
  3. 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 本章小结#

核心知识点#

  1. 神经网络结构:输入层、隐藏层、输出层
  2. 反向传播:链式法则计算梯度
  3. 激活函数:ReLU(隐藏层)、Softmax(分类输出)
  4. 正则化: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 练习与思考#

基础练习#

  1. 修改网络结构:

    • 增加/减少隐藏层数量
    • 调整每层神经元数量
    • 观察训练速度和准确率变化
  2. 超参数调优:

    • 学习率:[0.0001, 0.001, 0.01]
    • Dropout率:[0.1, 0.3, 0.5, 0.7]
    • Batch size:[32, 64, 128, 256]
  3. 激活函数对比:

    • 分别使用ReLU、LeakyReLU、GELU
    • 记录训练曲线差异

进阶挑战#

  1. 实现学习率调度:

    scheduler = torch.optim.lr_scheduler.StepLR(optimizer, step_size=5, gamma=0.1)
  2. 数据增强:

    transform = transforms.Compose([
        transforms.RandomRotation(10),
        transforms.ToTensor(),
    ])
  3. 尝试其他数据集:

    • Fashion-MNIST(服装分类)
    • KMNIST(日文字符)

思考题#

  1. 为什么BatchNorm能加速收敛?
  2. 神经网络越深越好吗?什么时候会出现问题?
  3. 如何判断模型是过拟合还是欠拟合?

下一章预告:我们将学习卷积神经网络(CNN),它是计算机视觉的基石。相比全连接网络,CNN能更好地处理图像数据,并大幅减少参数量。

继续学习第4章:卷积神经网络(CNN)


第4章:卷积神经网络(CNN)#

本章目标:理解卷积操作的本质,掌握CNN的设计原则,能独立构建CNN进行图像分类


4.1 为什么需要CNN?#

4.1.1 全连接网络的局限#

回顾第3章的MNIST分类器,我们使用全连接网络:

28×28图像 → 展平成784维向量 → MLP → 10个类别

存在的问题:

  1. 参数量爆炸:对于32×32的RGB图像(3072维),第一层若有256个神经元,参数量=3072×256≈78万
  2. 丢失空间信息:展平操作破坏了像素的空间位置关系
  3. 无法处理不同尺寸输入:输入尺寸固定

4.1.2 CNN的核心思想#

**卷积神经网络(Convolutional Neural Network)**基于三个关键假设:

  1. 局部连接:相邻像素比远距离像素更相关
  2. 权值共享:同一个特征(如边缘)在图像不同位置都有用
  3. 平移不变性:特征检测器可在任意位置工作

结果:大幅减少参数量,同时保留空间结构!


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 n

4.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⌋ + 1

4.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 x

4.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-51998760K-首个成功CNN
AlexNet2012860M15.3%ReLU+Dropout+GPU
VGG16201416138M7.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 x

4.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 本章小结#

核心知识点#

  1. 卷积层:局部连接+权值共享,大幅减少参数
  2. 池化层:降低分辨率,增强不变性
  3. 经典架构:LeNet(开创) → AlexNet(引爆) → VGG(深化)
  4. 设计原则:
    • 通道数逐渐增加(64→128→256→512)
    • 特征图尺寸逐渐减小(卷积+池化)
    • 使用BatchNorm加速训练
    • 数据增强提升泛化

CNN vs MLP#

特性CNNMLP
参数量少(权值共享)
空间信息保留丢失
平移不变性
适用场景图像、视频表格数据

检查清单#

  • 理解卷积操作的数学定义
  • 掌握卷积核、步长、填充的作用
  • 能计算卷积层输出尺寸
  • 理解VGG的设计原则
  • 成功运行CIFAR-10代码(准确率>75%)
  • 会使用torchvision加载预训练模型

4.8 练习与思考#

基础练习#

  1. 修改CIFAR-10网络:

    • 增加/减少卷积块数量
    • 调整每层通道数
    • 观察参数量和性能变化
  2. 数据增强实验:

    • 对比有/无数据增强的效果
    • 尝试不同增强策略(旋转、缩放等)
  3. 使用预训练模型:

    from torchvision.models import resnet18
    model = resnet18(weights=None)  # 从头训练
    model.fc = nn.Linear(512, 10)   # 修改最后一层

进阶挑战#

  1. 实现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
  2. 可视化卷积核和特征图

  3. 尝试其他数据集:

    • CIFAR-100(100个类别)
    • Tiny ImageNet

思考题#

  1. 为什么VGG使用3×3卷积而不是更大的卷积核?
  2. 什么时候应该使用池化?什么时候使用步长卷积?
  3. 如何判断网络容量是否合适?(过拟合vs欠拟合)

恭喜!你已经掌握了CNN的核心知识。

第二篇(深度学习基础)到此结束。接下来的第三篇,我们将学习现代CNN架构,包括:

  • ResNet:残差连接解决深度网络退化问题
  • Inception:多尺度特征融合
  • EfficientNet:神经架构搜索(NAS)

这些现代架构是当前计算机视觉应用的基石,是从学术研究走向工业应用的桥梁。

继续学习第三篇:现代CNN架构


[统计组件仅在生产环境显示]