主题
字号
CHAPTER 03 ≈ 90 MIN READ

CNN实战篇:PyTorch图像处理与卷积神经网络

作者:USTC学生 | 适用人群:零基础 / 深度学习初学者 特别说明:本笔记面向中科大大一大二同学,即使你之前没有接触过图像处理或神经网络,也能完全理解本笔记的所有内容。 更新时间:2026年3月17日


学习提示:本章是CNN的核心基础,建议读者先完整阅读一遍建立起直观理解,然后根据需要回来查阅公式和代码。CNN的核心思想其实非常简单——它就像一个"带着放大镜的显微镜",在图像上滑动并提取特征。

前置知识说明

在开始本章学习之前,你需要了解:

前言:为什么CNN如此重要

卷积神经网络(Convolutional Neural Network,CNN)是深度学习在计算机视觉领域取得突破的核心技术。从2012年AlexNet在ImageNet竞赛中一举夺冠开始,CNN就成了图像分类、目标检测、语义分割等视觉任务的标配。

与全连接层相比,CNN具有以下优势:

参数效率:全连接层将每个像素作为独立输入,参数数量巨大;而卷积层通过权重共享,大幅减少参数数量。

空间不变性:卷积操作具有平移不变性,无论物体出现在图像的哪个位置,都能被有效识别。

层次化特征提取:CNN能够自动从浅层的边缘、纹理,到深层的物体部件,最终形成完整的物体表示。

本章将系统介绍PyTorch中CNN相关的核心函数,从卷积层、池化层,到数据增强、模型构建,再到训练技巧,帮助你全面掌握CNN的PyTorch实现。


第一章:从零开始理解卷积——图像、像素与滑动窗口

本章学习目标

  1. 理解图像在计算机中是如何表示的(像素、通道)
  2. 理解卷积操作的核心思想——"滑动窗口"做点积
  3. 掌握卷积的数学公式
  4. 理解常见卷积核的作用
  5. 理解通道、滤波器、特征图三个核心概念

本章是整个CNN的基础。如果你是零基础学习者,请一定要仔细阅读这一章,因为它会帮助你建立起对卷积神经网络的直观理解。卷积的核心思想非常简单:它就像一个拿着"放大镜"的人,在图像上从左到右、从上到下滑动,每滑到一个位置,就提取出该区域的特征。


1.1 图像在计算机中是如何表示的?

在深入卷积之前,我们首先需要理解图像在计算机中是如何存储的。

1.1.1 灰度图像:一张二维的"数字表格"

想象一张普通的黑白照片(比如手写的数字"3")。如果你用一个放大镜靠近看,你会发现这张照片实际上是由很多很多小格子组成的。每个小格子有一个数值,表示该点的亮度。

举例说明

假设我们有一张非常小的灰度图像,只有5×5个像素(5行5列):

图像矩阵(5×5):
[[255, 255, 255, 255, 255],   # 第1行(白色)
 [255, 255,   0, 255, 255],   # 第2行(中间是黑色)
 [255, 255,   0, 255, 255],   # 第3行(中间是黑色)
 [255, 255,   0, 255, 255],   # 第4行(中间是黑色)
 [255, 255, 255, 255, 255]]   # 第5行(白色)

在这个矩阵中:

这就是灰度图像的本质:一个二维数组(矩阵),每个元素是一个0-255之间的整数。

在PyTorch中,这样一张灰度图像表示为形状为 (1, H, W) 的张量:

import torch

# 创建上面那个"数字1"的5×5灰度图像
# 注意:PyTorch中图像的形状是 (通道数, 高度, 宽度)
gray_image = torch.tensor([[[255, 255, 255, 255, 255],
                            [255, 255,   0, 255, 255],
                            [255, 255,   0, 255, 255],
                            [255, 255,   0, 255, 255],
                            [255, 255, 255, 255, 255]]], dtype=torch.float32)

print("灰度图像形状:", gray_image.shape)  # torch.Size([1, 5, 5])

小提示:实际使用中,我们通常会将像素值归一化到0-1之间(除以255),这样计算更方便。

1.1.2 彩色图像:三个"图层"的叠加

现实中的彩色照片比灰度图复杂得多。计算机中表示彩色图像最常用的方法是RGB模型——每 个像素位置实际上存储了三个数值:红色(Red)、绿色(Green)、蓝色(Blue)的强度。

你可以把RGB图像想象成三张透明的玻璃纸叠在一起

举例说明

假设还是那张5×5的图片,但现在是一张彩色图片:

红色通道 (R):
[[255, 255, 255, 255, 255],
 [255, 200, 100, 100, 255],
 [255, 100, 100, 100, 255],
 [255, 100, 100, 100, 255],
 [255, 255, 255, 255, 255]]

绿色通道 (G):
[[255, 255, 255, 255, 255],
 [255, 150,  50,  50, 255],
 [255,  50,  50,  50, 255],
 [255,  50,  50,  50, 255],
 [255, 255, 255, 255, 255]]

蓝色通道 (B):
[[255, 255, 255, 255, 255],
 [255, 100,  50,  50, 255],
 [255,  50,  50,  50, 255],
 [255,  50,  50,  50, 255],
 [255, 255, 255, 255, 255]]

在PyTorch中,彩色图像表示为形状为 (3, H, W) 的张量:

# 创建5×5的RGB彩色图像
# 形状: (通道数=3, 高度=5, 宽度=5)
rgb_image = torch.zeros(3, 5, 5)

# 红色通道 - 设置一些红色像素
rgb_image[0, 2, 2] = 255  # 在中心位置设置红色

# 绿色通道
rgb_image[1, 2, 2] = 100

# 蓝色通道
rgb_image[2, 2, 2] = 50

print("RGB图像形状:", rgb_image.shape)  # torch.Size([3, 5, 5])
print("红色通道:\n", rgb_image[0])
print("绿色通道:\n", rgb_image[1])
print("蓝色通道:\n", rgb_image[2])

1.1.3 批量图像:张量的批量处理

在实际训练中,我们通常一次处理多张图像。为了提高效率,PyTorch使用一个4维张量来存储批量图像:

张量形状: (批量大小, 通道数, 高度, 宽度)
       = (batch_size, channels, height, width)
       = (B, C, H, W)

举例说明

# 创建批量为4的RGB图像数据集
# 4张图片,每张图片3通道(RGB),每张图片224×224像素
batch_images = torch.randn(4, 3, 224, 224)

print("批量图像形状:", batch_images.shape)
# torch.Size([4, 3, 224, 224])
# 解释:4张图片,每张3通道,224高224宽

1.2 什么是卷积?用一个具体的例子来解释

现在你理解了图像的本质——它就是一个数字矩阵。那么卷积到底在做什么呢?

1.2.1 核心思想:滑动窗口 + 点积

卷积的核心思想只有两步

  1. 取出一个局部区域:在输入图像上取一个“小窗口”(比如3×3的区域)
  2. 做点积运算:把这个小窗口里的每个数值,与卷积核(也是一个小矩阵)对应位置相乘,然后求和

用一个具体例子来演示

假设我们有一个5×5的输入图像:

输入图像 (5×5):
[[1, 1, 1, 0, 0],
 [0, 1, 1, 1, 0],
 [0, 0, 1, 1, 1],
 [0, 0, 1, 1, 0],
 [0, 1, 1, 0, 0]]

还有一个3×3的卷积核(也叫做"滤波器",英文叫 Kernel):

卷积核/滤波器 (3×3):
[[1, 0, 1],
 [0, 1, 0],
 [1, 0, 1]]

现在我们来做卷积操作

第一步:取窗口

我们从图像的左上角开始,取一个3×3的区域:

输入图像 (标记了第一个窗口):
[1, 1, 1]    ← 第一个3×3窗口
[0, 1, 1]
[0, 0, 1]

第二步:点积计算

将窗口中的每个元素与卷积核对应位置的元素相乘,然后求和:

窗口 (3×3):           卷积核 (3×3):       乘积:
[[1, 1, 1],         [[1, 0, 1],        [[1, 0, 1],
 [0, 1, 1],    *     [0, 1, 0],    =    [0, 1, 0],
 [0, 0, 1]]          [1, 0, 1]]         [0, 0, 1]]

点积结果 = 1×1 + 1×0 + 1×1 + 0×0 + 1×1 + 1×0 + 0×1 + 0×0 + 1×1
         = 1 + 0 + 1 + 0 + 1 + 0 + 0 + 0 + 1
         = 4

所以,卷积操作输出的第一个值就是 4

第三步:滑动

然后,我们把这个"小窗口"向右滑动一格(步长为1),继续做同样的操作:

下一个窗口位置:
[1, 1, 0]    ← 第二个3×3窗口
[1, 1, 1]
[0, 1, 1]

点积 = 1×1 + 1×0 + 0×1 + 1×0 + 1×1 + 1×0 + 0×1 + 1×0 + 1×1
     = 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1
     = 3

第四步:遍历全图

重复这个过程,直到遍历完整个图像。最终我们会得到一个3×3的输出矩阵(注意一下,卷积扫到的面积是3×3,最后得到的是一个值):

卷积输出 (3×3):
[[4, 3, 4],
 [3, 4, 3],
 [4, 3, 4]]

这就是卷积操作的全部过程!简单来说,卷积就是:用一个小窗口(卷积核)在图像上滑动,每到一个位置就做一次"对应元素相乘后求和"的运算。

1.2.2 用PyTorch代码来验证

让我们用PyTorch来实际验证上面的计算过程:

import torch
import torch.nn as nn

# 创建输入图像 (1, 1, 5, 5) - 1张灰度图,5×5
input_image = torch.tensor([[[[1, 1, 1, 0, 0],
                              [0, 1, 1, 1, 0],
                              [0, 0, 1, 1, 1],
                              [0, 0, 1, 1, 0],
                              [0, 1, 1, 0, 0]]]], dtype=torch.float32)

# 创建卷积核 (1, 1, 3, 3) - 1个卷积核,1通道,3×3
kernel = torch.tensor([[[[1, 0, 1],
                         [0, 1, 0],
                         [1, 0, 1]]]], dtype=torch.float32)

# 创建卷积层
conv = nn.Conv2d(in_channels=1, out_channels=1, kernel_size=3, bias=False)

# 手动设置卷积核权重
conv.weight.data = kernel

# 执行卷积
output = conv(input_image)

print("输入图像:\n", input_image[0, 0])
print("\n卷积核:\n", kernel[0, 0])
print("\n卷积输出:\n", output[0, 0])

输出结果

输入图像:
 tensor([[1., 1., 1., 0., 0.],
        [0., 1., 1., 1., 0.],
        [0., 0., 1., 1., 1.],
        [0., 0., 1., 1., 0.],
        [0., 1., 1., 0., 0.]])

卷积核:
 tensor([[1., 0., 1.],
        [0., 1., 0.],
        [1., 0., 1.]])

卷积输出:
 tensor([[4., 3., 4.],
        [2., 4., 3.],
        [2., 3., 4.]])

完美匹配我们的手动计算!

Mav's Tip:这里的nn.Conv2d是一个类,会创建一个卷积层对象(如代码中的 conv),之后可以把这个对象当函数来调用。 还有一点,创建了 conv 只是明确了基本“交通规则”,还需要像下方的conv.weight.data 一样来引入你自定义的kernel才有用。


1.3 卷积的数学公式与物理意义

1.3.1 数学公式

卷积操作的数学表达式如下:

$(I * K){i,j} = \sum{m}\sum_{n} I_{i+m, j+n} \cdot K_{m,n}$

其中:

更直观的理解

如果我们使用0索引(从0开始),并且卷积核大小为 $k \times k$,公式可以写成:

$(I * K){i,j} = \sum{a=0}^{k-1}\sum_{b=0}^{k-1} I_{i+a, j+b} \cdot K_{a,b}$

举例说明

对于位置 $(i=0, j=0)$ 的输出:

$(I * K){0,0} = I{0,0}K_{0,0} + I_{0,1}K_{0,1} + I_{0,2}K_{0,2} + \cdots + I_{2,2}K_{2,2}$

这正是我们前面手动计算的例子!

1.3.2 物理意义:特征提取

为什么卷积能够提取特征?

卷积核就像一个"探测器"或"过滤器"。不同的卷积核能够"检测"图像中不同的模式:

关键洞察:卷积核本质上是在做**"模式匹配"**。如果输入图像的某一部分与卷积核的"形状"相似,那么该位置的输出值就会很大(表示"匹配上了")。


1.4 常见卷积核类型及其作用

不同的卷积核能够提取不同的特征。让我们来看看几种经典的卷积核及其作用。

1.4.1 边缘检测卷积核

边缘是图像最基本的特征之一。边缘检测卷积核能够检测出图像中颜色发生突变的位置。

水平边缘检测

# 水平边缘检测卷积核
# 这种卷积核能够检测水平方向的颜色变化
kernel_h = torch.tensor([[[-1., -1., -1.],
                         [ 0.,  0.,  0.],
                         [ 1.,  1.,  1.]]])

垂直边缘检测

# 垂直边缘检测卷积核
# 这种卷积核能够检测垂直方向的颜色变化
kernel_v = torch.tensor([[[-1., 0., 1.],
                         [-1., 0., 1.],
                         [-1., 0., 1.]]])

为什么能检测边缘?让我们用具体数值来理解

假设输入图像的某一区域:
[[10, 10, 10],     ← 上方全是暗色(10)
 [10, 10, 10],
 [90, 90, 90]]     ← 下方全是亮色(90)
                    ↑  颜色突变的地方就是边缘

使用水平边缘检测卷积核:
[[-1, -1, -1],
 [ 0,  0,  0],
 [ 1,  1,  1]]

点积计算:
= 10×(-1) + 10×(-1) + 10×(-1)   ← 第一行
+ 10×0  + 10×0  + 10×0          ← 第二行
+ 90×1  + 90×1  + 90×1          ← 第三行
= -30 + 0 + 270
= 240  ← 一个很大的正数!

关键发现:当图像上方颜色暗、下方颜色亮时,输出是一个大正数。这意味着检测到了"从暗到亮"的水平边缘!

如果我们把图像反过来(上方亮、下方暗):

[[90, 90, 90],     ← 上方全是亮色(90)
 [10, 10, 10],
 [10, 10, 10]]     ← 下方全是暗色(10)

点积 = 90×(-1) + 90×(-1) + 90×(-1) + 10×0 + 10×0 + 10×0 + 10×1 + 10×1 + 10×1
     = -270 + 0 + 30
     = -240  ← 一个很大的负数!

结论

1.4.2 锐化卷积核

锐化卷积核能够增强图像的对比度,让边缘更加明显。

# 锐化卷积核
kernel_sharpen = torch.tensor([[[[ 0., -1.,  0.],
                                 [-1.,  5., -1.],
                                 [ 0., -1.,  0.]]]])

工作原理

原始像素:
[[10, 20, 10],
 [20, 20, 20],
 [10, 20, 10]]

中心像素是20,周围也是20,没有变化

锐化卷积:
[[ 0, -1,  0],
 [-1,  5, -1],
 [ 0, -1,  0]]

点积 = 20×0 + 20×(-1) + 20×0      ← 上下左右各一个20
     + 20×(-1) + 20×5 + 20×(-1)  ← 中心×5
     + 20×0 + 20×(-1) + 20×0
     = 0 -20 + 0 -20 +100 -20 +0 -20 +0
     = 20

原来中心是20,输出是20,保持不变

但是!如果中心像素与周围不同:

原始像素:
[[10, 20, 10],
 [20, 80, 20],   ← 中心80特别亮
 [10, 20, 10]]

点积 = 10×0 + 20×(-1) + 10×0    ← 上下左右
     + 20×(-1) + 80×5 + 20×(-1)
     + 10×0 + 20×(-1) + 10×0
     = 0 -20 +0 -20 +400 -20 +0 -20 +0
     = 320  ← 增强了很多!

输出 = 320  远大于原始的80

结论:锐化卷积核会放大中心像素与周围像素的差异,从而增强边缘。

1.4.3 模糊/平滑卷积核

模糊卷积核能够减少图像噪声,让图像变得平滑。

# 模糊卷积核(均值滤波器)
# 取周围8个像素和中心像素的平均值
kernel_blur = torch.tensor([[[[1., 1., 1.],
                              [1., 1., 1.],
                              [1., 1., 1.]]]]) / 9  # 除以9是为了归一化

高斯模糊(更常用)

# 高斯模糊卷积核
# 离中心越近权重越大,符合高斯分布
kernel_gaussian = torch.tensor([[[[1., 2., 1.],
                                  [2., 4., 2.],
                                  [1., 2., 1.]]]]) / 16  # 归一化

1.4.4 用代码验证各种卷积核的效果

import torch
import torch.nn as nn
import matplotlib.pyplot as plt

# 创建测试图像(简单的梯度图案)
test_image = torch.zeros(1, 1, 10, 10)
# 左边是暗的,右边是亮的 - 用于测试边缘检测
test_image[0, 0, :, :5] = 0      # 左边黑色
test_image[0, 0, :, 5:] = 255    # 右边白色

# 定义各种卷积核
kernels = {
    '水平边缘': torch.tensor([[[[-1., -1., -1.],
                                [ 0.,  0.,  0.],
                                [ 1.,  1.,  1.]]]]),
    '垂直边缘': torch.tensor([[[[-1., 0., 1.],
                                [-1., 0., 1.],
                                [-1., 0., 1.]]]]),
    '锐化': torch.tensor([[[[ 0., -1.,  0.],
                            [-1.,  5., -1.],
                            [ 0., -1.,  0.]]]]),
    '模糊': torch.tensor([[[[1., 1., 1.],
                            [1., 1., 1.],
                            [1., 1., 1.]]]]) / 9,
}

# 创建卷积层并应用每个卷积核
fig, axes = plt.subplots(1, 5, figsize=(15, 3))

# 显示原图
axes[idx+1].imshow(output[0, 0].detach().numpy(), cmap='gray')
axes[0].set_title('原图')
axes[0].axis('off')

# 应用各个卷积核
for idx, (name, kernel) in enumerate(kernels.items()):
    conv = nn.Conv2d(1, 1, kernel_size=3, bias=False)
    conv.weight.data = kernel
    output = conv(test_image)

    axes[idx+1].imshow(output[0, 0].numpy(), cmap='gray')
    axes[idx+1].set_title(name)
    axes[idx+1].axis('off')

plt.tight_layout()
plt.show()

1.5 通道、滤波器和特征图:三个核心概念

理解"通道"、"滤波器"和"特征图"这三个概念,对于掌握CNN至关重要。

1.5.1 通道(Channel)

什么是通道?

通道可以理解为图像的"层面"或"视角":

举例说明

import torch

# 模拟卷积网络中间层的特征图
# 假设是第5层的输出,有64个通道,每个通道是28×28
features = torch.randn(1, 64, 28, 28)

print("特征图形状:", features.shape)
# torch.Size([1, 64, 28, 28])
# 解释:1张图片,64个通道,每个通道28×28

# 查看单个通道
print("第0个通道的形状:", features[0, 0].shape)  # torch.Size([28, 28])

通道的物理意义

在卷积网络中,每个通道通常代表一种"特征"。比如:

这些通道不是我们设计的,而是网络自动学习到的!

**Mav's Tips:**这里通道不包括输入层。

1.5.2 滤波器/卷积核(Filter/Kernel)

什么是滤波器?

滤波器就是卷积操作中的那个"小窗口",也叫做"卷积核"或"权重矩阵"。

在PyTorch中,一个滤波器的形状是:

import torch.nn as nn

# 定义一个卷积层
conv = nn.Conv2d(in_channels=3, out_channels=64, kernel_size=3, padding=1)

# 查看滤波器的形状
print("滤波器权重形状:", conv.weight.shape)
# torch.Size([64, 3, 3, 3])
# 解释:有64个滤波器,每个滤波器有3个通道(对应RGB),每个滤波器是3×3

print("偏置形状:", conv.bias.shape)
# torch.Size([64]) - 每个滤波器有一个偏置

滤波器 vs 通道

1.5.3 特征图(Feature Map)

什么是特征图?

特征图是卷积操作输出的结果。每个滤波器会产生一个特征图。

import torch
import torch.nn as nn

# 输入:1张RGB图片(3通道),224×224
x = torch.randn(1, 3, 224, 224)

# 卷积层:3通道输入,64通道输出
conv = nn.Conv2d(3, 64, kernel_size=3, padding=1)

# 输出:1张图片,64通道,224×224
output = conv(x)

print("输入形状:", x.shape)       # torch.Size([1, 3, 224, 224])
print("输出形状:", output.shape)  # torch.Size([1, 64, 224, 224])

特征图的含义

输入图像 (RGB):
[红色通道]   [绿色通道]   [蓝色通道]
  ↓            ↓            ↓
         ↓    卷积层 (64个滤波器)    ↓
         ↓            ↓            ↓
[特征图0]  [特征图1]  [特征图2] ... [特征图63]

每个输出通道(特征图)都是输入图像经过某个滤波器处理后的结果。

1.5.4 三者关系总结

概念 英文 含义 形状示例
通道 Channel 图像的"层面",或卷积输出的"特征维度" (64, H, W) = 64个通道
滤波器 Filter/Kernel 卷积核,卷积操作中滑动的小窗口,是需要学习的参数 (3, 3, 3) = 3通道的3×3卷积核
特征图 Feature Map 卷积操作的输出结果 (1, 64, 224, 224) = 1张图,64个特征图

理解要点

  1. 输入图像有通道(如RGB的3个通道)
  2. 卷积层有滤波器(每个滤波器也是一个"小图像",但它的数值是学出来的)
  3. 卷积输出是特征图(每个滤波器产生一个特征图)
  4. 特征图的通道数 = 滤波器的数量 = 卷积层的out_channels

1.6 完整的卷积过程:一个滤波器的视角

让我们用一个完整的例子来理解整个卷积过程:

import torch
import torch.nn as nn

# 假设输入是一张RGB图像
# 形状: (batch=1, channels=3, height=5, width=5)
input_image = torch.randn(1, 3, 5, 5)

print("=" * 50)
print("输入图像信息")
print("=" * 50)
print(f"形状: {input_image.shape}")
print(f"通道数: {input_image.shape[1]} (RGB三个通道)")
print(f"每个通道的尺寸: {input_image.shape[2]}×{input_image.shape[3]}")

# 创建卷积层:3通道输入 -> 1通道输出,3×3卷积核
conv = nn.Conv2d(in_channels=3, out_channels=1, kernel_size=3, bias=False)

print("\n" + "=" * 50)
print("卷积层信息")
print("=" * 50)
print(f"输入通道数: {conv.in_channels}")
print(f"输出通道数: {conv.out_channels}")
print(f"卷积核大小: {conv.kernel_size}")
print(f"权重形状: {conv.weight.shape}")
print(f"偏置形状: {conv.bias}")

# 执行卷积
output = conv(input_image)

print("\n" + "=" * 50)
print("卷积输出信息")
print("=" * 50)
print(f"输出形状: {output.shape}")
print(f"输出通道数: {output.shape[1]} (每个滤波器产生一个通道)")
print(f"输出尺寸: {output.shape[2]}×{output.shape[3]}")

运行结果(每次运行会不同,因为是随机初始化):

==================================================
输入图像信息
==================================================
形状: torch.Size([1, 3, 5, 5])
通道数: 3 (RGB三个通道)
每个通道的尺寸: 5×5

==================================================
卷积层信息
==================================================
输入通道数: 3
输出通道数: 1
卷积核大小: (3, 3)
权重形状: torch.Size([1, 3, 3, 3])
偏置形状: None

==================================================
卷积输出信息
==================================================
输出形状: torch.Size([1, 1, 3, 3])
输出通道数: 1 (每个滤波器产生一个通道)
输出尺寸: 3×3

计算过程可视化

输入图像 (3通道, 5×5):
[通道0: 5×5]  [通道1: 5×5]  [通道2: 5×5]
   ↓              ↓             ↓
   ├──────────────┼──────────────┤
   │         卷积层: 1个滤波器       │
   │    权重形状: (3, 3, 3)         │
   │    (3通道输入 × 3×3卷积核)     │
   ├──────────────┼──────────────┤
   ↓              ↓             ↓
   [输出特征图: 1通道, 3×3]

第二章:PyTorch中的卷积操作

本章学习目标

  1. 掌握 nn.Conv2d 的用法
  2. 理解 kernel_sizestridepaddinggroupsdilation 等参数
  3. 掌握卷积输出尺寸的计算公式

在第一章中,我们理解了卷积的原理。现在我们学习如何在PyTorch中实现卷积操作。

2.1 nn.Conv2d:二维卷积层

nn.Conv2d 是PyTorch中实现二维卷积的核心类。

import torch.nn as nn

# 基础用法
conv = nn.Conv2d(in_channels=3, out_channels=64, kernel_size=3, padding=1)

# 参数说明:
# in_channels: 输入通道数(RGB图像为3,灰度图为1)
# out_channels: 输出通道数(卷积核的数量,也等于输出特征图的数量)
# kernel_size: 卷积核大小(3表示3x3,也可以是5, 7等奇数)
# padding: 填充层数(用于保持图像尺寸)

**Mav's Tips:**Padding是指在最外层填充一些像素,通常是空白的。

前向传播

# 输入形状:(batch_size, channels, height, width)
x = torch.randn(8, 3, 224, 224)  # 8张RGB图像,224x224

# 通过卷积层
output = conv(x)
print(output.shape)  # torch.Size([8, 64, 224, 224])
# 输出:batch_size=8, 通道数=64, 高度=224, 宽度=224

卷积层的内部结构

# 查看卷积层的参数
conv = nn.Conv2d(3, 64, kernel_size=3, padding=1)
print("权重形状:", conv.weight.shape)   # torch.Size([64, 3, 3, 3])
print("偏置形状:", conv.bias.shape)     # torch.Size([64])

# 权重形状解释:
# 64个卷积核,每个卷积核3个通道(对应RGB),每个卷积核3x3大小

2.2 卷积层的参数详解

nn.Conv2d 有多个重要参数,理解它们对于设计网络结构非常重要。

2.2.1 kernel_size:卷积核大小

卷积核决定了卷积操作提取特征的"视野范围"。常见的卷积核大小有1×1、3×3、5×5、7×7等。

# 1x1卷积:用于改变通道数,类似全连接
conv_1x1 = nn.Conv2d(256, 64, kernel_size=1)

# 3x3卷积:最常用的卷积核大小
conv_3x3 = nn.Conv2d(64, 128, kernel_size=3, padding=1)

# 5x5卷积:更大的感受野
conv_5x5 = nn.Conv2d(64, 128, kernel_size=5, padding=2)

# 可以使用元组指定不同的高宽核
conv_rect = nn.Conv2d(64, 128, kernel_size=(3, 5), padding=(1, 2))

感受野(Receptive Field):指的是卷积神经网络中,一个输出像素点"看"到的输入区域的大小。3×3的卷积核,感受野就是3×3;5×5的卷积核,感受野就是5×5。

2.2.2 stride:步长

stride 控制卷积核在图像上滑动的步长。stride=1时逐像素移动,stride=2时每次移动2个像素(输出尺寸减半)。

# stride=1(默认):保持尺寸
conv_s1 = nn.Conv2d(3, 64, kernel_size=3, stride=1, padding=1)
x = torch.randn(1, 3, 224, 224)
print(conv_s1(x).shape)  # torch.Size([1, 64, 224, 224])

# stride=2:尺寸减半
conv_s2 = nn.Conv2d(3, 64, kernel_size=3, stride=2, padding=1)
print(conv_s2(x).shape)  # torch.Size([1, 64, 112, 112])

# stride=3:尺寸变为原来的1/3
conv_s3 = nn.Conv2d(3, 64, kernel_size=3, stride=3, padding=1)
print(conv_s3(x).shape)  # torch.Size([1, 64, 75, 75])  # ceil(224/3)

2.2.3 padding:填充

padding 在输入图像边缘添加像素,以控制输出尺寸。

# padding=0(无填充):尺寸减小
conv_no_pad = nn.Conv2d(3, 64, kernel_size=3)
x = torch.randn(1, 3, 224, 224)
print(conv_no_pad(x).shape)  # torch.Size([1, 64, 222, 222])

# padding=1:保持尺寸(最常用)
conv_pad1 = nn.Conv2d(3, 64, kernel_size=3, padding=1)
print(conv_pad1(x).shape)  # torch.Size([1, 64, 224, 224])

# padding=2:尺寸增加
conv_pad2 = nn.Conv2d(3, 64, kernel_size=3, padding=2)
print(conv_pad2(x).shape)  # torch.Size([1, 64, 226, 226])

# padding=kernel_size//2:保持尺寸的通用公式
kernel_size = 5
padding = kernel_size // 2  # 2
conv_pad = nn.Conv2d(3, 64, kernel_size=kernel_size, padding=padding)
print(conv_pad(x).shape)  # torch.Size([1, 64, 224, 224])

2.2.4 groups:分组卷积

groups 参数允许将输入和输出分成多个组,实现分组卷积。这减少了计算量,也是Depthwise Separable Convolution的基础。

# groups=1(默认):普通卷积
conv_normal = nn.Conv2d(12, 12, kernel_size=3, padding=1, groups=1)

# groups=in_channels=out_channels:Depthwise卷积
# 输入12通道,输出12通道,分12组
conv_depthwise = nn.Conv2d(12, 12, kernel_size=3, padding=1, groups=12)
# 权重形状:torch.Size([12, 1, 3, 3]) - 每组独立

# groups=in_channels:逐通道卷积,每个通道独立处理
# groups=out_channels:逐点卷积,1x1卷积

**Mav's Tips:**group减少了通道之间的关联,牺牲了表达能力换取效率的提升,在某些情况有用,但有些情况会降低精度。

2.2.5 dilation:空洞卷积

dilation 参数控制卷积核中间的空隙大小,用于增大感受野而不增加参数量。

# dilation=1(默认):普通卷积
conv_d1 = nn.Conv2d(3, 64, kernel_size=3, padding=1, dilation=1)

# dilation=2:空洞卷积,感受野扩大
conv_d2 = nn.Conv2d(3, 64, kernel_size=3, padding=2, dilation=2)
# 实际感受野:1 + (3-1)*2 = 5x5

# dilation=4:更大的感受野
conv_d4 = nn.Conv2d(3, 64, kernel_size=3, padding=4, dilation=4)
# 实际感受野:1 + (3-1)*4 = 9x9

Mav's Tips: 感受野是指把当前像素反推会原始图片占的面积,比如在kernel_size=3的情况,第一层感受野是3×3,第二层就是5×5了,以此类推。dilation则可以有效扩大感受野,让网络学习更多的细节。 但同时也要注意避免“Gridding Artifact(网格效应)”,即空隙太大,忽略了中间的内容。

2.3 卷积输出尺寸计算

理解卷积输出尺寸的计算公式对于设计网络结构至关重要。

通用公式

$H_{out} = \left\lfloor\frac{H_{in} + 2 \times padding - dilation \times (kernel_size - 1) - 1}{stride} + 1\right\rfloor$

简化版(当dilation=1时):

$H_{out} = \left\lfloor\frac{H_{in} + 2 \times padding - kernel_size}{stride} + 1\right\rfloor$

常用快速计算

def calc_conv_output_size(H, W, kernel_size=3, stride=1, padding=0, dilation=1):
    """计算卷积输出尺寸"""
    H_out = ((H + 2*padding - dilation*(kernel_size-1) - 1) // stride) + 1
    W_out = ((W + 2*padding - dilation*(kernel_size-1) - 1) // stride) + 1
    return H_out, W_out

# 示例:224x224输入,3x3卷积,stride=2,padding=1
H, W = 224, 224
H_out, W_out = calc_conv_output_size(H, W, kernel_size=3, stride=2, padding=1)
print(f"输出尺寸: {H_out}x{W_out}")  # 112x112

实际应用示例

import torch
import torch.nn as nn

# 构建一个典型的CNN特征提取器
class FeatureExtractor(nn.Module):
    def __init__(self):
        super().__init__()
        # Block 1: 224 -> 112 -> 56
        self.conv1 = nn.Conv2d(3, 64, kernel_size=7, stride=2, padding=3)
        self.pool1 = nn.MaxPool2d(kernel_size=3, stride=2, padding=1)

        # Block 2: 56 -> 28
        self.conv2 = nn.Conv2d(64, 128, kernel_size=3, stride=2, padding=1)

        # Block 3: 28 -> 14
        self.conv3 = nn.Conv2d(128, 256, kernel_size=3, stride=2, padding=1)

        # Block 4: 14 -> 7
        self.conv4 = nn.Conv2d(256, 512, kernel_size=3, stride=2, padding=1)

    def forward(self, x):
        x = self.pool1(torch.relu(self.conv1(x))) 
        x = torch.relu(self.conv2(x))               
        x = torch.relu(self.conv3(x))             
        x = torch.relu(self.conv4(x))               
        return x

# 测试
extractor = FeatureExtractor()
x = torch.randn(1, 3, 224, 224)
output = extractor(x)
print("输出形状:", output.shape)  # torch.Size([1, 512, 7, 7])

**Mav's Tips:**这里有一个小细节,我们直接调用了extractor(x),为什么不是extractor.forward(x)?这是因为这个类继承了nn.Modulenn.Module里面有一个__call__方法,是专门调用forward()函数的,也就是说,extractor(x)其实是extractor.forward(x)的缩写,更加便捷。


第三章:池化层与下采样

池化层(Pooling Layer)用于降低特征图的空间尺寸,同时保留重要信息。池化操作可以增强网络的平移不变性,减少计算量,并帮助控制过拟合。

3.1 nn.MaxPool2d:最大池化

最大池化选取每个池化窗口中的最大值,是最常用的池化方式。

import torch.nn as nn

# 基础用法
maxpool = nn.MaxPool2d(kernel_size=2, stride=2)

# 参数说明:
# kernel_size: 池化窗口大小(2表示2x2)
# stride: 步长(默认等于kernel_size,即不重叠)
# padding: 填充
# dilation: 空洞率
# return_indices: 是否返回最大值的索引(用于MaxUnpool)
# 输入
x = torch.randn(1, 1, 4, 4)
print("输入:\n", x)
# tensor([[[[ 0.2341,  0.2341, -0.0116, -1.0414],
#          [-0.7376, -0.5123,  0.2134,  0.3894],
#          [-1.5151,  0.0160,  0.4503, -0.0131],
#          [-0.4530,  0.0339, -0.3623,  0.1413]]]])

# 最大池化
output = maxpool(x)
print("输出:\n", output)
# tensor([[[[ 0.2341,  0.3894],
#          [ 0.0160,  0.4503]]]])

池化后尺寸计算

# 池化尺寸计算公式
def calc_pool_output_size(H, W, kernel_size=2, stride=2, padding=0, dilation=1):
    H_out = ((H + 2*padding - dilation*(kernel_size-1) - 1) // stride) + 1
    W_out = ((W + 2*padding - dilation*(kernel_size-1) - 1) // stride) + 1
    return H_out, W_out

# 常见配置
H, W = 224, 224

# 2x2池化,stride=2
H_out, W_out = calc_pool_output_size(H, W, kernel_size=2, stride=2)
print(f"2x2池化,stride=2: {H_out}x{W_out}")  # 112x112

# 3x3池化,stride=2
H_out, W_out = calc_pool_output_size(H, W, kernel_size=3, stride=2, padding=1)
print(f"3x3池化,stride=2,padding=1: {H_out}x{W_out}")  # 112x112

实际应用

class CNNWithPooling(nn.Module):
    def __init__(self):
        super().__init__()
        # 卷积层
        self.conv1 = nn.Conv2d(3, 32, kernel_size=3, padding=1)
        self.conv2 = nn.Conv2d(32, 64, kernel_size=3, padding=1)

        # 池化层
        self.pool = nn.MaxPool2d(kernel_size=2, stride=2)

        # 全连接层
        self.fc1 = nn.Linear(64 * 28 * 28, 256)
        self.fc2 = nn.Linear(256, 10)

    def forward(self, x):
        # 224x224 -> 112x112
        x = self.pool(torch.relu(self.conv1(x)))

        # 112x112 -> 56x56
        x = self.pool(torch.relu(self.conv2(x)))

        # 展平
        x = x.view(x.size(0), -1)

        # 全连接
        x = torch.relu(self.fc1(x))
        x = self.fc2(x)
        return x

3.2 nn.AvgPool2d:平均池化

平均池化计算池化窗口内所有值的平均值。在某些任务中比最大池化效果更好。

# 平均池化
avgpool = nn.AvgPool2d(kernel_size=2, stride=2)

# 输入
x = torch.randn(1, 1, 4, 4)
print("输入:\n", x)
# tensor([[[[ 0.2341,  0.2341, -0.0116, -1.0414],
#          [-0.7376, -0.5123,  0.2134,  0.3894],
#          [-1.5151,  0.0160,  0.4503, -0.0131],
#          [-0.4530,  0.0339, -0.3623,  0.1413]]]])

# 平均池化
output = avgpool(x)
print("输出:\n", output)
# tensor([[[[-0.1944, -0.1123],
#          [-0.4781,  0.0587]]]])

平均池化 vs 最大池化

特性 MaxPool2d AvgPool2d
计算方式 取最大值 取平均值
特点 保留显著特征 平滑特征
常用场景 分类任务 特征平滑、注意力机制
感受野 更关注最强响应 关注整体统计

Global Average Pooling(GAP)

GAP是一种特殊的平均池化,将每个通道的整个特征图压缩为一个值。

# 全局平均池化
gap = nn.AdaptiveAvgPool2d(1)  # 输出1x1

x = torch.randn(1, 512, 7, 7)
gap_output = gap(x)
print(gap_output.shape)  # torch.Size([1, 512, 1, 1])

# 展平后用于分类
gap_output = gap_output.view(gap_output.size(0), -1)
print(gap_output.shape)  # torch.Size([1, 512])

3.3 nn.AdaptiveAvgPool2d:自适应池化

自适应池化可以指定输出的目标尺寸,PyTorch会自动计算所需的padding和stride。

# 输出固定尺寸
adaptive_pool = nn.AdaptiveAvgPool2d(output_size=(7, 7))

x = torch.randn(1, 512, 28, 28)
output = adaptive_pool(x)
print(output.shape)  # torch.Size([1, 512, 7, 7])

# 输出1x1(GAP)
adaptive_pool_gap = nn.AdaptiveAvgPool2d(output_size=1)
x = torch.randn(1, 512, 28, 28)
output = adaptive_pool_gap(x)
print(output.shape)  # torch.Size([1, 512, 1, 1])

# 输出特定高度,宽度自适应
adaptive_pool_h = nn.AdaptiveAvgPool2d(output_size=(1, None))
x = torch.randn(1, 512, 28, 28)
output = adaptive_pool_h(x)
print(output.shape)  # torch.Size([1, 512, 1, 28])

自适应最大池化

adaptive_maxpool = nn.AdaptiveMaxPool2d(output_size=(3, 3))
x = torch.randn(1, 64, 10, 10)
output = adaptive_maxpool(x)
print(output.shape)  # torch.Size([1, 64, 3, 3])

自适应池化的应用场景

# 典型的分类网络骨架
class ClassifierBackbone(nn.Module):
    def __init__(self, in_channels=3, num_classes=10):
        super().__init__()
        self.features = nn.Sequential(
            # Conv Block 1
            nn.Conv2d(in_channels, 64, kernel_size=3, padding=1),
            nn.BatchNorm2d(64),
            nn.ReLU(inplace=True),
            nn.MaxPool2d(kernel_size=2, stride=2),  # 112x112

            # Conv Block 2
            nn.Conv2d(64, 128, kernel_size=3, padding=1),
            nn.BatchNorm2d(128),
            nn.ReLU(inplace=True),
            nn.MaxPool2d(kernel_size=2, stride=2),  # 56x56

            # Conv Block 3
            nn.Conv2d(128, 256, kernel_size=3, padding=1),
            nn.BatchNorm2d(256),
            nn.ReLU(inplace=True),
            nn.MaxPool2d(kernel_size=2, stride=2),  # 28x28

            # 全局平均池化(无论输入尺寸如何,输出都是1x1)
            nn.AdaptiveAvgPool2d(output_size=1)
        )

        self.classifier = nn.Linear(256, num_classes)

    def forward(self, x):
        x = self.features(x)
        x = x.view(x.size(0), -1)  # 展平
        x = self.classifier(x)
        return x

# 测试:不同输入尺寸
model = ClassifierBackbone()

x1 = torch.randn(1, 3, 224, 224)
print(model(x1).shape)  # torch.Size([1, 10])

x2 = torch.randn(1, 3, 112, 112)
print(model(x2).shape)  # torch.Size([1, 10])

x3 = torch.randn(1, 3, 96, 96)
print(model(x3).shape)  # torch.Size([1, 10])

第四章:归一化与正则化

4.1 nn.BatchNorm2d:批归一化

BatchNorm是深度学习中最重要的技巧之一,它通过规范化层的输入来加速训练、提高稳定性。

数学原理

y = gamma * (x - mean) / sqrt(var + epsilon) + beta

其中gamma(缩放)和beta(偏移)是可学习参数,mean和var是当前batch的统计量。

import torch.nn as nn

# BatchNorm2d用于2D特征图(图像)
bn = nn.BatchNorm2d(num_features=64, eps=1e-5, momentum=0.1)

# 参数说明:
# num_features: 通道数(C)
# eps: 防止除零的小常数(默认1e-5)
# momentum: 移动平均的动量(用于训练时累积均值和方差)
# affine: 是否学习gamma和beta(默认True)
# 前向传播
x = torch.randn(8, 64, 32, 32)  # batch=8, channels=64, 32x32
output = bn(x)

print("输入形状:", x.shape)
print("输出形状:", output.shape)
print("训练模式均值:", bn.running_mean[:5])  # 累积的均值
print("训练模式方差:", bn.running_var[:5])   # 累积的方差

BatchNorm的关键特性

# 训练模式 vs 评估模式
bn_train = nn.BatchNorm2d(64)

# 训练模式:使用batch统计量,并更新running统计量
bn_train.train()
output = bn_train(x)
print("训练中:", bn_train.training)  # True

# 评估模式:使用running统计量
bn_train.eval()
output = bn_train(x)
print("评估中:", bn_train.training)  # False

在CNN中使用BatchNorm

# 典型的Conv-BatchNorm-ReLU组合
class ConvBlock(nn.Module):
    def __init__(self, in_channels, out_channels):
        super().__init__()
        self.block = nn.Sequential(
            nn.Conv2d(in_channels, out_channels, kernel_size=3, padding=1),
            nn.BatchNorm2d(out_channels),
            nn.ReLU(inplace=True)
        )

    def forward(self, x):
        return self.block(x)

# 使用
conv_block = ConvBlock(3, 64)
x = torch.randn(4, 3, 32, 32)
output = conv_block(x)
print(output.shape)  # torch.Size([4, 64, 32, 32])

BatchNorm的优势

  1. 加速收敛:减少内部协变量偏移
  2. 允许更高学习率:梯度更稳定
  3. 正则化效果:每个batch的均值方差有噪声,提供正则化
  4. 减少对初始化的依赖

4.2 nn.Dropout:正则化

Dropout通过随机"关闭"部分神经元来防止过拟合,是深度学习中最常用的正则化技术之一。

import torch.nn as nn

# Dropout
dropout = nn.Dropout(p=0.5)  # p: 丢弃概率

x = torch.randn(4, 10)
output = dropout(x)

print("原始:\n", x)
print("Dropout后:\n", output)
# 大约50%的元素变为0

在CNN中使用Dropout

# nn.Dropout2d:随机丢弃整个通道
dropout_2d = nn.Dropout2d(p=0.5)

x = torch.randn(4, 64, 32, 32)
output = dropout_2d(x)
# 随机将整个通道置零
print("输出:", output.shape)

Dropout vs Dropout2d

类型 作用 适用场景
nn.Dropout 随机丢弃单个元素 全连接层、特征图较小时
nn.Dropout2d 随机丢弃整个通道 CNN的特征图
# 典型的带Dropout的分类网络
class CNNWithDropout(nn.Module):
    def __init__(self, num_classes=10):
        super().__init__()
        self.features = nn.Sequential(
            nn.Conv2d(3, 64, kernel_size=3, padding=1),
            nn.ReLU(inplace=True),
            nn.MaxPool2d(2),

            nn.Conv2d(64, 128, kernel_size=3, padding=1),
            nn.ReLU(inplace=True),
            nn.MaxPool2d(2),

            nn.Conv2d(128, 256, kernel_size=3, padding=1),
            nn.ReLU(inplace=True),
            nn.MaxPool2d(2),
        )

        # 全连接层之间使用Dropout
        self.classifier = nn.Sequential(
            nn.Dropout(0.5),
            nn.Linear(256 * 4 * 4, 128),
            nn.ReLU(inplace=True),
            nn.Dropout(0.5),
            nn.Linear(128, num_classes)
        )

    def forward(self, x):
        x = self.features(x)
        x = x.view(x.size(0), -1)
        x = self.classifier(x)
        return x

Dropout2d的实际应用

# Spatial Dropout(Dropout2d)
# 丢弃整个通道而不是单个元素,更适合CNN
class CNNWithSpatialDropout(nn.Module):
    def __init__(self):
        super().__init__()
        self.conv1 = nn.Conv2d(3, 64, 3, padding=1)
        # Spatial Dropout:丢弃整个通道
        self.dropout = nn.Dropout2d(p=0.2)
        self.conv2 = nn.Conv2d(64, 64, 3, padding=1)

    def forward(self, x):
        x = torch.relu(self.conv1(x))
        x = self.dropout(x)  # 随机丢弃通道
        x = torch.relu(self.conv2(x))
        return x

4.3 nn.LayerNorm:层归一化

LayerNorm与BatchNorm类似,但归一化的维度不同。LayerNorm在单个样本的特征维度上进行归一化,不依赖于batch大小。

import torch.nn as nn

# LayerNorm
ln = nn.LayerNorm(normalized_shape=[64, 32, 32])

# 参数:
# normalized_shape: 要归一化的形状
# eps: 防止除零
# elementwise_affine: 是否学习gamma和beta
# 输入
x = torch.randn(8, 64, 32, 32)  # batch=8

# LayerNorm:在通道和空间维度上归一化
output = ln(x)
print("输入形状:", x.shape)
print("输出形状:", output.shape)

BatchNorm vs LayerNorm vs InstanceNorm vs GroupNorm

# 各种归一化方法的对比
import torch

# 输入形状
x = torch.randn(8, 64, 32, 32)

# BatchNorm2d:在batch和空间维度上归一化 (N, C, H, W) -> (N, C, 1, 1)
bn = nn.BatchNorm2d(64)
print("BatchNorm:", bn(x).shape)

# LayerNorm:在通道和空间维度上归一化 (N, C, H, W) -> (N, 1, 1, 1)
ln = nn.LayerNorm([64, 32, 32])
print("LayerNorm:", ln(x).shape)

# InstanceNorm2d:在空间维度上归一化 (N, C, H, W) -> (N, C, 1, 1)
inn = nn.InstanceNorm2d(64)
print("InstanceNorm:", inn(x).shape)

# GroupNorm:将通道分组后在组内归一化 (N, C, H, W) -> (N, C, 1, 1)
gn = nn.GroupNorm(num_groups=8, num_channels=64)
print("GroupNorm:", gn(x).shape)

各种归一化的可视化

输入形状: (N, C, H, W) = (8, 64, 32, 32)

BatchNorm:    对每个特征,在(N, H, W)上求均值/方差
              依赖batch大小,训练快,但batch小时不稳定

LayerNorm:    对每个样本,在(C, H, W)上求均值/方差
              不依赖batch,常用于RNN、Transformer

InstanceNorm: 对每个样本、每个通道,在(H, W)上求均值/方差
              风格迁移效果好

GroupNorm:    对每个样本,将通道分成G组,在每组的(C/G, H, W)上求均值/方差
              .batch大小无关,效果稳定,推荐使用

实际应用建议

# Transformer中常用LayerNorm
class TransformerBlock(nn.Module):
    def __init__(self, d_model, n_head):
        super().__init__()
        self.attention = nn.MultiheadAttention(d_model, n_head)
        self.norm1 = nn.LayerNorm(d_model)
        self.norm2 = nn.LayerNorm(d_model)
        self.ff = nn.Linear(d_model, d_model * 4)
        self.ff_out = nn.Linear(d_model * 4, d_model)

    def forward(self, x):
        # Self-attention with residual
        attn_out, _ = self.attention(x, x, x)
        x = self.norm1(x + attn_out)

        # FFN with residual
        ff_out = torch.relu(self.ff(x))
        ff_out = self.ff_out(ff_out)
        x = self.norm2(x + ff_out)
        return x

# CNN中常用GroupNorm(替代BatchNorm)
class CNNWithGN(nn.Module):
    def __init__(self, in_channels, out_channels):
        super().__init__()
        self.conv = nn.Conv2d(in_channels, out_channels, 3, padding=1)
        # GroupNorm: 8 groups
        self.norm = nn.GroupNorm(num_groups=8, num_channels=out_channels)
        self.relu = nn.ReLU(inplace=True)

    def forward(self, x):
        x = self.conv(x)
        x = self.norm(x)
        x = self.relu(x)
        return x

第五章:图像变换与数据增强

数据增强是提升模型泛化能力的关键技术。torchvision.transforms提供了丰富的图像变换函数。

5.1 transforms.Compose:组合多个变换

Compose用于将多个变换组合成管道。

from torchvision import transforms

# 创建变换管道
transform = transforms.Compose([
    transforms.ToTensor(),           # 1. 转为张量
    transforms.Normalize([0.5], [0.5]),  # 2. 标准化
    transforms.RandomHorizontalFlip(),   # 3. 随机翻转
])

# 应用变换
from PIL import Image
img = Image.open('cat.jpg')
img_tensor = transform(img)
print(img_tensor.shape)  # torch.Size([3, H, W])

5.2 transforms.ToTensor:图像转张量

ToTensor将PIL Image或NumPy数组转换为PyTorch张量,并自动归一化到[0, 1]。

from torchvision import transforms
from PIL import Image
import numpy as np

# 从PIL Image转换
img_pil = Image.open('cat.jpg')
print("PIL Image:", img_pil.mode, img_pil.size)

to_tensor = transforms.ToTensor()
tensor = to_tensor(img_pil)
print("Tensor:", tensor.shape, tensor.min(), tensor.max())
# 自动转换为 (C, H, W),值在 [0, 1]

# 从NumPy数组转换
img_np = np.array(img_pil)
print("NumPy:", img_np.shape)
tensor_np = to_tensor(img_np)
print("Tensor from NumPy:", tensor_np.shape)

# 转换前后对比
print("\n=== 转换说明 ===")
# PIL Image (H, W, C) [0-255]
# NumPy   (H, W, C) [0-255]
# Tensor  (C, H, W) [0, 1]

5.3 transforms.Normalize:标准化

Normalize使用均值和标准差对图像进行标准化。

数学公式

normalized = (input - mean) / std
# ImageNet常用均值和标准差
normalize = transforms.Normalize(
    mean=[0.485, 0.456, 0.406],  # RGB通道的均值
    std=[0.229, 0.224, 0.225]     # RGB通道的标准差
)

# 标准化流程
x = torch.rand(3, 224, 224)  # [0, 1]
x_normalized = normalize(x)

print("标准化前: min={:.3f}, max={:.3f}".format(x.min(), x.max()))
print("标准化后: min={:.3f}, max={:.3f}".format(x_normalized.min(), x_normalized.max()))
# 标准化后值通常在 [-2, 2] 范围内

训练和测试使用相同的标准化

# 定义标准化参数(ImageNet)
imagenet_mean = [0.485, 0.456, 0.406]
imagenet_std = [0.229, 0.224, 0.225]

# 训练变换
train_transform = transforms.Compose([
    transforms.RandomResizedCrop(224),
    transforms.RandomHorizontalFlip(),
    transforms.ToTensor(),
    transforms.Normalize(mean=imagenet_mean, std=imagenet_std),
])

# 测试变换(验证集、测试集)
test_transform = transforms.Compose([
    transforms.Resize(256),
    transforms.CenterCrop(224),
    transforms.ToTensor(),
    transforms.Normalize(mean=imagenet_mean, std=imagenet_std),
])

# 使用变换
train_dataset = datasets.FashionMNIST(
    root='./data',
    train=True,
    transform=train_transform,
    download=True
)

test_dataset = datasets.FashionMNIST(
    root='./data',
    train=False,
    transform=test_transform,
    download=True
)

5.4 transforms.Resize与transforms.CenterCrop

调整图像尺寸并裁剪。

# Resize:调整图像大小
resize = transforms.Resize(size=(224, 224))  # 调整为指定尺寸
# 或者
resize = transforms.Resize(size=224)  # 短边调整为224,保持比例

# CenterCrop:中心裁剪
center_crop = transforms.CenterCrop(size=(224, 224))

# FiveCrop:四个角和中心裁剪(返回5个图像)
five_crop = transforms.FiveCrop(size=(224, 224))

# TenCrop:水平翻转后裁剪(返回10个图像)
ten_crop = transforms.TenCrop(size=(224, 224), vertical_flip=False)

# 组合使用
transform = transforms.Compose([
    transforms.Resize(256),
    transforms.CenterCrop(224),
    transforms.ToTensor(),
])

5.5 transforms.RandomHorizontalFlip:随机水平翻转

随机水平翻转是数据增强中最常用、最有效的技术之一。

# 随机水平翻转
hflip = transforms.RandomHorizontalFlip(p=0.5)  # p: 翻转概率

# 随机垂直翻转
vflip = transforms.RandomVerticalFlip(p=0.5)

# 实际应用
img = Image.open('cat.jpg')
flipped = hflip(img)

5.6 transforms.RandomRotation:随机旋转

# 随机旋转
rotation = transforms.RandomRotation(degrees=15)  # -15到15度之间随机旋转
rotation10 = transforms.RandomRotation(degrees=(10, 30))  # 10到30度之间随机旋转

# 随机旋转(包含填充)
rotation_fill = transforms.RandomRotation(
    degrees=30,
    fill=(255, 255, 255)  # 填充颜色
)

5.7 transforms.ColorJitter:颜色抖动

# 颜色抖动
color_jitter = transforms.ColorJitter(
    brightness=0.2,    # 亮度调整范围
    contrast=0.2,      # 对比度调整范围
    saturation=0.2,    # 饱和度调整范围
    hue=0.1           # 色调调整范围
)

# 单独使用
brightness = transforms.ColorJitter(brightness=0.3)
contrast = transforms.ColorJitter(contrast=0.3)
saturation = transforms.ColorJitter(saturation=0.3)
hue = transforms.ColorJitter(hue=0.1)

5.8 transforms.RandomAffine:随机仿射变换

# 随机仿射变换
affine = transforms.RandomAffine(
    degrees=15,              # 旋转角度
    translate=(0.1, 0.1),   # 平移范围(相对于尺寸的比例)
    scale=(0.9, 1.1),        # 缩放范围
    shear=15                # 剪切角度
)

5.9 transforms.RandomErasing:随机擦除

RandomErasing(随机擦除)是一种有效的正则化技术,模拟遮挡。

# 随机擦除
random_erase = transforms.RandomErasing(
    p=0.5,                  # 擦除概率
    scale=(0.02, 0.33),     # 擦除区域相对于图像的比例范围
    ratio=(0.3, 3.3),       # 擦除区域宽高比范围
    value=0                 # 擦除区域的值
)

# 在张量上应用
to_tensor = transforms.ToTensor()
img_tensor = to_tensor(img)
erased = random_erase(img_tensor)

综合数据增强示例

# 完整的数据增强策略
train_transform = transforms.Compose([
    # 1. 随机大小裁剪并调整到224x224
    transforms.RandomResizedCrop(
        224,
        scale=(0.8, 1.0),    # 裁剪区域为原图的80%-100%
        ratio=(0.9, 1.1)     # 宽高比范围
    ),

    # 2. 随机水平翻转
    transforms.RandomHorizontalFlip(p=0.5),

    # 3. 随机旋转(-15到15度)
    transforms.RandomRotation(15),

    # 4. 颜色抖动
    transforms.ColorJitter(
        brightness=0.2,
        contrast=0.2,
        saturation=0.2,
        hue=0.1
    ),

    # 5. 转换为张量
    transforms.ToTensor(),

    # 6. 标准化
    transforms.Normalize(
        mean=[0.485, 0.456, 0.406],
        std=[0.229, 0.224, 0.225]
    ),

    # 7. 随机擦除
    transforms.RandomErasing(p=0.3),
])

# 测试变换
test_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]
    ),
])

第六章:图像数据集加载

6.1 torchvision.datasets:内置数据集

PyTorch提供了常用的图像数据集,可以直接下载使用。

from torchvision import datasets, transforms

# Fashion-MNIST
fashion_train = datasets.FashionMNIST(
    root='./data',
    train=True,
    transform=transforms.ToTensor(),
    download=True
)

fashion_test = datasets.FashionMNIST(
    root='./data',
    train=False,
    transform=transforms.ToTensor(),
    download=True
)

# CIFAR-10
cifar10_train = datasets.CIFAR10(
    root='./data',
    train=True,
    transform=transforms.ToTensor(),
    download=True
)

cifar10_test = datasets.CIFAR10(
    root='./data',
    train=False,
    transform=transforms.ToTensor(),
    download=True
)

# ImageNet(需要手动下载)
# imagenet_train = datasets.ImageNet(
#     root='./data/imagenet',
#     split='train',
#     transform=transforms.ToTensor()
# )

# 查看数据集信息
print("Fashion-MNIST训练集:", len(fashion_train))
print("Fashion-MNIST测试集:", len(fashion_test))
print("CIFAR-10类别:", datasets.CIFAR10.classes)

常用数据集速查

数据集 类别数 训练集大小 图像大小 说明
MNIST 10 60,000 28x28 手写数字
Fashion-MNIST 10 60,000 28x28 服装分类
CIFAR-10 10 50,000 32x32 通用物体
CIFAR-100 100 50,000 32x32 100类物体
ImageNet 1000 1.2M 可变 大规模图像

6.2 Dataset类:自定义数据集

当需要加载自己的数据时,需要自定义Dataset类。

from torch.utils.data import Dataset
from PIL import Image
import os

class CustomDataset(Dataset):
    def __init__(self, root_dir, transform=None):
        self.root_dir = root_dir
        self.transform = transform
        self.image_paths = []
        self.labels = []

        # 读取数据目录
        for label_name in os.listdir(root_dir):
            label_path = os.path.join(root_dir, label_name)
            if os.path.isdir(label_path):
                for img_name in os.listdir(label_path):
                    if img_name.endswith(('.jpg', '.png', '.jpeg')):
                        self.image_paths.append(os.path.join(label_path, img_name))
                        self.labels.append(label_name)

        # 创建标签到索引的映射
        self.classes = sorted(list(set(self.labels)))
        self.class_to_idx = {cls: idx for idx, cls in enumerate(self.classes)}

    def __len__(self):
        return len(self.image_paths)

    def __getitem__(self, idx):
        # 加载图像
        img_path = self.image_paths[idx]
        image = Image.open(img_path).convert('RGB')

        # 应用变换
        if self.transform:
            image = self.transform(image)

        # 获取标签
        label = self.labels[idx]
        label_idx = self.class_to_idx[label]

        return image, label_idx

# 使用自定义数据集
custom_dataset = CustomDataset(
    root_dir='./my_data/train',
    transform=train_transform
)

print("数据集大小:", len(custom_dataset))
print("类别:", custom_dataset.classes)

处理文件夹结构

my_data/
├── train/
│   ├── cat/
│   │   ├── cat001.jpg
│   │   ├── cat002.jpg
│   │   └── ...
│   ├── dog/
│   │   ├── dog001.jpg
│   │   └── ...
│   └── ...
└── val/
    ├── cat/
    └── dog/

6.3 DataLoader:批量数据加载

DataLoader是PyTorch中最重要的数据加载工具。

from torch.utils.data import DataLoader

# 基础用法
train_loader = DataLoader(
    dataset=train_dataset,
    batch_size=32,
    shuffle=True,
    num_workers=4,
    pin_memory=True,
    drop_last=True
)

# 迭代数据
for batch_idx, (images, labels) in enumerate(train_loader):
    print("批次数:", batch_idx)
    print("图像形状:", images.shape)    # [32, 3, 224, 224]
    print("标签形状:", labels.shape)    # [32]
    break

DataLoader参数详解

参数 说明 常用值
dataset 数据集 -
batch_size 批大小 32, 64, 128
shuffle 是否打乱 True(训练), False(测试)
num_workers 数据加载进程数 4, 8
pin_memory 锁页内存,加快GPU传输 True
drop_last 丢弃最后一个不完整batch True(训练), False(测试)
collate_fn 自定义批处理函数 自定义

多GPU数据加载

# 多GPU时使用DistributedSampler
from torch.utils.data.distributed import DistributedSampler

train_sampler = DistributedSampler(
    dataset,
    num_replicas=num_gpus,
    rank=rank,
    shuffle=True
)

train_loader = DataLoader(
    dataset,
    batch_size=batch_size,
    sampler=train_sampler,
    num_workers=4,
    pin_memory=True
)

第七章:CNN模型构建实战

7.1 经典LeNet模型实现

LeNet是1998年Yann LeCun提出的第一个卷积神经网络,是现代CNN的开山之作。

import torch.nn as nn

class LeNet(nn.Module):
    def __init__(self, num_classes=10):
        super().__init__()

        # 特征提取部分
        self.features = nn.Sequential(
            # C1: 第一卷积层
            nn.Conv2d(1, 6, kernel_size=5, padding=2),  # 28x28 -> 28x28
            nn.ReLU(inplace=True),
            nn.AvgPool2d(kernel_size=2, stride=2),      # 28x28 -> 14x14

            # C3: 第二卷积层
            nn.Conv2d(6, 16, kernel_size=5),            # 14x14 -> 10x10
            nn.ReLU(inplace=True),
            nn.AvgPool2d(kernel_size=2, stride=2),      # 10x10 -> 5x5
        )

        # 分类器部分
        self.classifier = nn.Sequential(
            nn.Flatten(),
            nn.Linear(16 * 5 * 5, 120),
            nn.ReLU(inplace=True),
            nn.Linear(120, 84),
            nn.ReLU(inplace=True),
            nn.Linear(84, num_classes)
        )

    def forward(self, x):
        x = self.features(x)
        x = self.classifier(x)
        return x

# 测试
model = LeNet(num_classes=10)
x = torch.randn(1, 1, 28, 28)
output = model(x)
print("输出形状:", output.shape)  # torch.Size([1, 10])

# 打印模型结构
print(model)

LeNet架构图

输入 (1, 28, 28)
    ↓
Conv2d(1, 6, 5x5) + ReLU + AvgPool  →  (6, 14, 14)
    ↓
Conv2d(6, 16, 5x5) + ReLU + AvgPool  →  (16, 5, 5)
    ↓
Flatten  →  (400)
    ↓
Linear(400, 120) + ReLU  →  (120)
    ↓
Linear(120, 84) + ReLU  →  (84)
    ↓
Linear(84, 10)  →  (10)

7.2 AlexNet模型实现

AlexNet在2012年ImageNet竞赛中取得突破性成绩,标志着深度学习的复兴。

class AlexNet(nn.Module):
    def __init__(self, num_classes=1000):
        super().__init__()

        self.features = nn.Sequential(
            # Conv1
            nn.Conv2d(3, 96, kernel_size=11, stride=4, padding=2),
            nn.ReLU(inplace=True),
            nn.MaxPool2d(kernel_size=3, stride=2),  # 224 -> 55

            # Conv2
            nn.Conv2d(96, 256, kernel_size=5, padding=2),
            nn.ReLU(inplace=True),
            nn.MaxPool2d(kernel_size=3, stride=2),  # 55 -> 27

            # Conv3
            nn.Conv2d(256, 384, kernel_size=3, padding=1),
            nn.ReLU(inplace=True),

            # Conv4
            nn.Conv2d(384, 384, kernel_size=3, padding=1),
            nn.ReLU(inplace=True),

            # Conv5
            nn.Conv2d(384, 256, kernel_size=3, padding=1),
            nn.ReLU(inplace=True),
            nn.MaxPool2d(kernel_size=3, stride=2),  # 27 -> 13
        )

        self.avgpool = nn.AdaptiveAvgPool2d((6, 6))

        self.classifier = nn.Sequential(
            nn.Dropout(0.5),
            nn.Linear(256 * 6 * 6, 4096),
            nn.ReLU(inplace=True),
            nn.Dropout(0.5),
            nn.Linear(4096, 4096),
            nn.ReLU(inplace=True),
            nn.Linear(4096, num_classes),
        )

    def forward(self, x):
        x = self.features(x)
        x = self.avgpool(x)
        x = torch.flatten(x, 1)
        x = self.classifier(x)
        return x

# 测试
model = AlexNet(num_classes=1000)
x = torch.randn(1, 3, 224, 224)
output = model(x)
print("输出形状:", output.shape)  # torch.Size([1, 1000])

# 参数统计
total_params = sum(p.numel() for p in model.parameters())
trainable_params = sum(p.numel() for p in model.parameters() if p.requires_grad)
print(f"总参数: {total_params:,}")
print(f"可训练参数: {trainable_params:,}")

7.3 VGG模型实现

VGG通过使用更小的3x3卷积核堆叠来增加网络深度,是当时最常用的特征提取网络。

class VGG(nn.Module):
    def __init__(self, num_classes=1000):
        super().__init__()

        # VGG16配置
        # [64, 64, 'M', 128, 128, 'M', 256, 256, 256, 'M',
        #  512, 512, 512, 'M', 512, 512, 512, 'M']
        # 'M' = MaxPool

        self.features = self._make_layers([
            64, 64, 'M',           # Block 1: 224 -> 112
            128, 128, 'M',         # Block 2: 112 -> 56
            256, 256, 256, 'M',    # Block 3: 56 -> 28
            512, 512, 512, 'M',    # Block 4: 28 -> 14
            512, 512, 512, 'M',    # Block 5: 14 -> 7
        ])

        self.avgpool = nn.AdaptiveAvgPool2d((7, 7))

        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(4097, num_classes),
        )

    def _make_layers(self, config):
        layers = []
        in_channels = 3

        for v in config:
            if v == 'M':
                layers.append(nn.MaxPool2d(kernel_size=2, stride=2))
            else:
                layers.append(nn.Conv2d(in_channels, v, kernel_size=3, padding=1))
                layers.append(nn.BatchNorm2d(v))
                layers.append(nn.ReLU(inplace=True))
                in_channels = v

        return nn.Sequential(*layers)

    def forward(self, x):
        x = self.features(x)
        x = self.avgpool(x)
        x = torch.flatten(x, 1)
        x = self.classifier(x)
        return x

# 测试
model = VGG(num_classes=1000)
x = torch.randn(1, 3, 224, 224)
output = model(x)
print("输出形状:", output.shape)

# 参数统计
total_params = sum(p.numel() for p in model.parameters())
print(f"VGG16参数: {total_params:,}")

VGG变体

# VGG11, VGG13, VGG16, VGG19
# 数字表示卷积层+全连接层的总层数
# 常用VGG16(在准确率和参数量之间平衡较好)

7.4 ResNet残差网络

ResNet通过残差连接解决了深层网络梯度消失问题,是现代CNN的里程碑。

class ResidualBlock(nn.Module):
    """残差块"""
    def __init__(self, in_channels, out_channels, stride=1, downsample=None):
        super().__init__()
        self.conv1 = nn.Conv2d(
            in_channels, out_channels,
            kernel_size=3, stride=stride, padding=1, bias=False
        )
        self.bn1 = nn.BatchNorm2d(out_channels)
        self.relu = nn.ReLU(inplace=True)
        self.conv2 = nn.Conv2d(
            out_channels, out_channels,
            kernel_size=3, stride=1, padding=1, bias=False
        )
        self.bn2 = nn.BatchNorm2d(out_channels)
        self.downsample = downsample

    def forward(self, x):
        identity = x

        out = self.conv1(x)
        out = self.bn1(out)
        out = self.relu(out)

        out = self.conv2(out)
        out = self.bn2(out)

        if self.downsample is not None:
            identity = self.downsample(x)

        out += identity  # 残差连接
        out = self.relu(out)

        return out


class ResNet(nn.Module):
    def __init__(self, num_classes=1000):
        super().__init__()

        # 初始卷积层
        self.conv1 = nn.Conv2d(3, 64, kernel_size=7, stride=2, padding=3, bias=False)
        self.bn1 = nn.BatchNorm2d(64)
        self.relu = nn.ReLU(inplace=True)
        self.maxpool = nn.MaxPool2d(kernel_size=3, stride=2, padding=1)

        # 残差层
        self.layer1 = self._make_layer(64, 64, blocks=2, stride=1)
        self.layer2 = self._make_layer(64, 128, blocks=2, stride=2)
        self.layer3 = self._make_layer(128, 256, blocks=2, stride=2)
        self.layer4 = self._make_layer(256, 512, blocks=2, stride=2)

        # 全局平均池化和分类器
        self.avgpool = nn.AdaptiveAvgPool2d((1, 1))
        self.fc = nn.Linear(512, num_classes)

    def _make_layer(self, in_channels, out_channels, blocks, stride):
        downsample = None

        if stride != 1 or in_channels != out_channels:
            downsample = nn.Sequential(
                nn.Conv2d(in_channels, out_channels, kernel_size=1, stride=stride, bias=False),
                nn.BatchNorm2d(out_channels),
            )

        layers = []
        layers.append(ResidualBlock(in_channels, out_channels, stride, downsample))

        for _ in range(1, blocks):
            layers.append(ResidualBlock(out_channels, out_channels))

        return nn.Sequential(*layers)

    def forward(self, x):
        x = self.conv1(x)
        x = self.bn1(x)
        x = self.relu(x)
        x = self.maxpool(x)

        x = self.layer1(x)
        x = self.layer2(x)
        x = self.layer3(x)
        x = self.layer4(x)

        x = self.avgpool(x)
        x = torch.flatten(x, 1)
        x = self.fc(x)

        return x


# 测试
model = ResNet(num_classes=1000)
x = torch.randn(1, 3, 224, 224)
output = model(x)
print("输出形状:", output.shape)

# ResNet18参数统计
total_params = sum(p.numel() for p in model.parameters())
print(f"ResNet参数: {total_params:,}")

ResNet的优势

普通网络: 输出 = F(x)
残差网络: 输出 = F(x) + x

残差连接使得梯度可以直接反向传播到浅层
解决了深层网络梯度消失的问题

第八章:预训练模型与迁移学习

8.1 torchvision.models:预训练模型

PyTorch提供了在ImageNet上预训练的模型,可以直接下载使用。

import torchvision.models as models

# 加载预训练模型
resnet18 = models.resnet18(pretrained=True)
resnet50 = models.resnet50(pretrained=True)
vgg16 = models.vgg16(pretrained=True)
alexnet = models.alexnet(pretrained=True)

# 加载未训练的模型(用于微调)
resnet18_no_pretrain = models.resnet18(pretrained=False)

# 查看模型结构
print(resnet18)

常用预训练模型

# ImageNet预训练模型
models.resnet18(pretrained=True)    # 11.7M参数
models.resnet34(pretrained=True)   # 21.8M参数
models.resnet50(pretrained=True)   # 25.6M参数
models.resnet101(pretrained=True)  # 44.5M参数

models.vgg11(pretrained=True)       # 132.9M参数
models.vgg13(pretrained=True)
models.vgg16(pretrained=True)      # 138.4M参数

models.alexnet(pretrained=True)    # 61.1M参数

# EfficientNet系列(最新最强)
models.efficientnet_b0(pretrained=True)
models.efficientnet_b1(pretrained=True)

# MobileNet系列(移动端优化)
models.mobilenet_v2(pretrained=True)
models.mobilenet_v3_small(pretrained=True)

# ViT(Vision Transformer)
models.vit_b_16(pretrained=True)    # 需要较大计算资源

8.2 特征提取:冻结主干网络

特征提取是最常用的迁移学习方法,将预训练模型作为固定特征提取器。

import torchvision.models as models

# 加载预训练模型
model = models.resnet18(pretrained=True)

# 冻结所有层(不更新参数)
for param in model.parameters():
    param.requires_grad = False

# 修改最后的全连接层
num_features = model.fc.in_features
model.fc = nn.Linear(num_features, 10)  # 10类分类

# 训练时只有fc层的参数会更新
# 其他层参数固定不变

特征提取的完整示例

import torchvision.models as models
import torch.nn as nn

# 1. 加载预训练模型
feature_extractor = models.resnet18(pretrained=True)

# 2. 冻结所有参数
for param in feature_extractor.parameters():
    param.requires_grad = False

# 3. 替换分类头
feature_extractor.fc = nn.Sequential(
    nn.Dropout(0.3),
    nn.Linear(512, 256),
    nn.ReLU(),
    nn.Dropout(0.3),
    nn.Linear(256, 10)
)

# 4. 冻结BatchNorm(可选,防止更新running stats)
for module in feature_extractor.modules():
    if isinstance(module, nn.BatchNorm2d):
        module.eval()

# 5. 训练
optimizer = torch.optim.Adam(feature_extractor.fc.parameters(), lr=0.001)

# 训练循环
for images, labels in train_loader:
    outputs = feature_extractor(images)
    loss = criterion(outputs, labels)

    optimizer.zero_grad()
    loss.backward()
    optimizer.step()

8.3 微调:解冻部分层

微调(Fine-tuning)是在预训练模型基础上解冻部分层进行训练。

# 方法1:解冻所有层
model = models.resnet18(pretrained=True)
optimizer = torch.optim.Adam(model.parameters(), lr=0.001)

# 方法2:只解冻最后几层
model = models.resnet18(pretrained=True)

# 冻结前面的层
for name, param in model.named_parameters():
    if 'layer4' not in name and 'fc' not in name:
        param.requires_grad = False

# 只优化可训练参数
optimizer = torch.optim.Adam(
    filter(lambda p: p.requires_grad, model.parameters()),
    lr=0.001
)

# 方法3:使用不同的学习率
model = models.resnet18(pretrained=True)

# 基础层(浅层)使用较小学习率
base_params = []
base_layers = ['conv1', 'bn1', 'layer1', 'layer2']
for name, param in model.named_parameters():
    if any(layer in name for layer in base_layers):
        base_params.append(param)
        param.requires_grad = True
    else:
        param.requires_grad = False

optimizer = torch.optim.Adam([
    {'params': base_params, 'lr': 1e-4},      # 基础层:低学习率
    {'params': model.fc.parameters(), 'lr': 1e-3}  # 分类头:高学习率
])

8.4 模型保存与加载

import torch

# 方法1:只保存参数(推荐)
torch.save(model.state_dict(), 'model.pth')

# 加载
model = MyModel()
model.load_state_dict(torch.load('model.pth'))

# 方法2:保存完整模型
torch.save(model, 'model_full.pth')

# 加载
model = torch.load('model_full.pth')

# 方法3:保存检查点(训练中断恢复)
checkpoint = {
    'epoch': 10,
    'model_state_dict': model.state_dict(),
    'optimizer_state_dict': optimizer.state_dict(),
    'loss': 0.5,
}
torch.save(checkpoint, 'checkpoint.pth')

# 加载检查点
checkpoint = torch.load('checkpoint.pth')
model.load_state_dict(checkpoint['model_state_dict'])
optimizer.load_state_dict(checkpoint['optimizer_state_dict'])
start_epoch = checkpoint['epoch'] + 1

# 方法4:GPU到CPU的模型加载
device = torch.device('cpu')
model = MyModel()
model.load_state_dict(torch.load('model.pth', map_location=device))

# 方法5:跨设备加载(GPU/CPU兼容)
model = MyModel()
model.load_state_dict(torch.load('model.pth', map_location='cuda:0' if torch.cuda.is_available() else 'cpu'))

第九章:训练技巧与实战

8.1 学习率调度器

学习率调度器可以根据训练进程动态调整学习率。

import torch.optim as optim

# 1. StepLR:固定步长衰减
optimizer = optim.SGD(model.parameters(), lr=0.1)
scheduler = optim.lr_scheduler.StepLR(optimizer, step_size=10, gamma=0.1)

for epoch in range(100):
    train(...)
    scheduler.step()
    print(f"Epoch {epoch}: LR = {scheduler.get_last_lr()[0]}")

# 2. MultiStepLR:多个里程碑衰减
scheduler = optim.lr_scheduler.MultiStepLR(
    optimizer, milestones=[30, 60, 90], gamma=0.1
)

# 3. CosineAnnealingLR:余弦退火
scheduler = optim.lr_scheduler.CosineAnnealingLR(
    optimizer, T_max=50, eta_min=1e-6
)

# 4. ReduceLROnPlateau:监控指标下降时调整
scheduler = optim.lr_scheduler.ReduceLROnPlateau(
    optimizer, mode='min', factor=0.1, patience=5
)

# 训练循环
for epoch in range(100):
    train_loss = train(...)
    val_loss = validate(...)

    scheduler.step(val_loss)  # 根据验证损失调整

# 5. Warmup策略:学习率预热
class WarmupScheduler:
    def __init__(self, optimizer, warmup_epochs, base_lr):
        self.optimizer = optimizer
        self.warmup_epochs = warmup_epochs
        self.base_lr = base_lr

    def step(self, epoch):
        if epoch < self.warmup_epochs:
            lr = self.base_lr * (epoch + 1) / self.warmup_epochs
            for param_group in self.optimizer.param_groups:
                param_group['lr'] = lr

8.2 早停策略

早停(Early Stopping)防止过拟合。

class EarlyStopping:
    def __init__(self, patience=7, min_delta=0, mode='min'):
        self.patience = patience
        self.min_delta = min_delta
        self.mode = mode
        self.counter = 0
        self.best_score = None
        self.early_stop = False

    def __call__(self, score):
        if self.best_score is None:
            self.best_score = score
        elif self._is_improvement(score):
            self.best_score = score
            self.counter = 0
        else:
            self.counter += 1
            if self.counter >= self.patience:
                self.early_stop = True

        return self.early_stop

    def _is_improvement(self, score):
        if self.mode == 'min':
            return score < self.best_score - self.min_delta
        else:
            return score > self.best_score + self.min_delta

# 使用
early_stopping = EarlyStopping(patience=10, mode='min')

for epoch in range(100):
    train_loss = train(...)
    val_loss = validate(...)

    early_stopping(val_loss)
    if early_stopping.early_stop:
        print(f"早停!Epoch {epoch}")
        break

8.3 梯度裁剪

梯度裁剪防止梯度爆炸。

# 方法1:按值裁剪
torch.nn.utils.clip_grad_value_(model.parameters(), clip_value=1.0)

# 方法2:按范数裁剪(推荐)
torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)

# 训练循环中的使用
for images, labels in train_loader:
    outputs = model(images)
    loss = criterion(outputs, labels)

    optimizer.zero_grad()
    loss.backward()

    # 梯度裁剪
    torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)

    optimizer.step()

8.4 混合精度训练

混合精度训练可以显著加速训练并减少显存使用。

from torch.cuda.amp import autocast, GradScaler

# 检查GPU是否支持混合精度
print(torch.cuda.is_available())
print(torch.cuda.get_device_capability())

# 创建scaler
scaler = GradScaler()

# 训练循环
model = model.cuda()
criterion = nn.CrossEntropyLoss()
optimizer = torch.optim.Adam(model.parameters(), lr=0.001)

for epoch in range(10):
    for images, labels in train_loader:
        images = images.cuda()
        labels = labels.cuda()

        optimizer.zero_grad()

        # 前向传播使用自动混合精度
        with autocast():
            outputs = model(images)
            loss = criterion(outputs, labels)

        # 反向传播
        scaler.scale(loss).backward()

        # 梯度裁剪
        scaler.unscale_(optimizer)
        torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0)

        # 更新参数
        scaler.step(optimizer)
        scaler.update()

8.5 完整的训练流程

import torch
import torch.nn as nn
from torch.utils.data import DataLoader
from torchvision import datasets, transforms
import torchvision.models as models
from tqdm import tqdm

def train_one_epoch(model, train_loader, criterion, optimizer, device):
    model.train()
    running_loss = 0.0
    correct = 0
    total = 0

    pbar = tqdm(train_loader, desc='Training')
    for images, labels in pbar:
        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()

        pbar.set_postfix({
            'loss': running_loss / (pbar.n + 1),
            'acc': 100. * correct / total
        })

    return running_loss / len(train_loader), 100. * correct / total


def validate(model, val_loader, criterion, device):
    model.eval()
    running_loss = 0.0
    correct = 0
    total = 0

    with torch.no_grad():
        for images, labels in val_loader:
            images, labels = images.to(device), labels.to(device)

            outputs = model(images)
            loss = criterion(outputs, labels)

            running_loss += loss.item()
            _, predicted = outputs.max(1)
            total += labels.size(0)
            correct += predicted.eq(labels).sum().item()

    return running_loss / len(val_loader), 100. * correct / total


# 主训练流程
def main():
    # 设置设备
    device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
    print(f"使用设备: {device}")

    # 数据增强
    train_transform = transforms.Compose([
        transforms.RandomResizedCrop(224),
        transforms.RandomHorizontalFlip(),
        transforms.ToTensor(),
        transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])
    ])

    val_transform = transforms.Compose([
        transforms.Resize(256),
        transforms.CenterCrop(224),
        transforms.ToTensor(),
        transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])
    ])

    # 加载数据
    train_dataset = datasets.FakeData(
        size=1000, image_size=(3, 224, 224), transform=train_transform
    )
    val_dataset = datasets.FakeData(
        size=200, image_size=(3, 224, 224), transform=val_transform
    )

    train_loader = DataLoader(train_dataset, batch_size=32, shuffle=True)
    val_loader = DataLoader(val_dataset, batch_size=32)

    # 创建模型
    model = models.resnet18(pretrained=True)
    model.fc = nn.Linear(model.fc.in_features, 10)
    model = model.to(device)

    # 损失函数和优化器
    criterion = nn.CrossEntropyLoss()
    optimizer = torch.optim.Adam(model.parameters(), lr=0.001)
    scheduler = torch.optim.lr_scheduler.StepLR(optimizer, step_size=3, gamma=0.1)

    # 训练
    best_acc = 0.0
    num_epochs = 10

    for epoch in range(num_epochs):
        print(f"\nEpoch {epoch + 1}/{num_epochs}")

        train_loss, train_acc = train_one_epoch(
            model, train_loader, criterion, optimizer, device
        )

        val_loss, val_acc = validate(model, val_loader, criterion, device)

        scheduler.step()

        print(f"Train Loss: {train_loss:.4f}, Train Acc: {train_acc:.2f}%")
        print(f"Val Loss: {val_loss:.4f}, Val Acc: {val_acc:.2f}%")

        # 保存最佳模型
        if val_acc > best_acc:
            best_acc = val_acc
            torch.save(model.state_dict(), 'best_model.pth')
            print("保存最佳模型!")

    print(f"\n最佳验证准确率: {best_acc:.2f}%")

if __name__ == '__main__':
    main()

第十章:模型评估与可视化

9.1 混淆矩阵

import numpy as np
from sklearn.metrics import confusion_matrix
import matplotlib.pyplot as plt
import seaborn as sns

def plot_confusion_matrix(y_true, y_pred, classes):
    cm = confusion_matrix(y_true, y_pred)

    plt.figure(figsize=(10, 8))
    sns.heatmap(cm, annot=True, fmt='d', cmap='Blues',
                xticklabels=classes, yticklabels=classes)
    plt.ylabel('真实标签')
    plt.xlabel('预测标签')
    plt.title('混淆矩阵')
    plt.show()

# 使用
all_preds = []
all_labels = []

model.eval()
with torch.no_grad():
    for images, labels in test_loader:
        images = images.to(device)
        outputs = model(images)
        _, predicted = outputs.max(1)

        all_preds.extend(predicted.cpu().numpy())
        all_labels.extend(labels.numpy())

plot_confusion_matrix(all_labels, all_preds, classes)

9.2 分类报告

from sklearn.metrics import classification_report

# 详细分类报告
print(classification_report(
    all_labels,
    all_preds,
    target_names=['T恤', '裤子', '套头衫', '连衣裙', '外套',
                  '凉鞋', '衬衫', '运动鞋', '包', '短靴']
))

9.3 学习曲线可视化

import matplotlib.pyplot as plt

def plot_learning_curves(train_losses, val_losses, train_accs, val_accs):
    fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 4))

    # 损失曲线
    ax1.plot(train_losses, label='训练损失')
    ax1.plot(val_losses, label='验证损失')
    ax1.set_xlabel('Epoch')
    ax1.set_ylabel('损失')
    ax1.set_title('损失曲线')
    ax1.legend()
    ax1.grid(True)

    # 准确率曲线
    ax2.plot(train_accs, label='训练准确率')
    ax2.plot(val_accs, label='验证准确率')
    ax2.set_xlabel('Epoch')
    ax2.set_ylabel('准确率 (%)')
    ax2.set_title('准确率曲线')
    ax2.legend()
    ax2.grid(True)

    plt.tight_layout()
    plt.show()

# 使用
history = {
    'train_loss': [],
    'val_loss': [],
    'train_acc': [],
    'val_acc': []
}

for epoch in range(num_epochs):
    # 训练...
    history['train_loss'].append(train_loss)
    history['val_loss'].append(val_loss)
    history['train_acc'].append(train_acc)
    history['val_acc'].append(val_acc)

plot_learning_curves(
    history['train_loss'],
    history['val_loss'],
    history['train_acc'],
    history['val_acc']
)

9.4 中间层特征可视化

import matplotlib.pyplot as plt

def visualize_feature_maps(model, image, layer_idx=0):
    """可视化指定层的特征图"""
    model.eval()

    # 提取中间层
    layers = list(model.features.children())
    layer = layers[layer_idx]

    # 创建特征提取器
    feature_extractor = nn.Sequential(*layers[:layer_idx+1])

    with torch.no_grad():
        features = feature_extractor(image)

    # 可视化前16个通道
    fig, axes = plt.subplots(4, 4, figsize=(12, 12))
    for i, ax in enumerate(axes.flat):
        if i < features.shape[1]:
            feature_map = features[0, i].cpu()
            ax.imshow(feature_map, cmap='viridis')
            ax.set_title(f'Channel {i}')
        ax.axis('off')

    plt.tight_layout()
    plt.show()

# 使用
model = models.resnet18(pretrained=True)
image = torch.randn(1, 3, 224, 224).cuda()
visualize_feature_maps(model, image, layer_idx=4)

9.5 Grad-CAM:梯度加权类激活映射

Grad-CAM可以可视化模型关注图像的哪些区域进行分类。

import torch
import torch.nn.functional as F
import numpy as np
import cv2

class GradCAM:
    def __init__(self, model, target_layer):
        self.model = model
        self.target_layer = target_layer
        self.gradients = None
        self.activations = None

        # 注册hook
        target_layer.register_forward_hook(self.save_activation)
        target_layer.register_full_backward_hook(self.save_gradient)

    def save_activation(self, module, input, output):
        self.activations = output.detach()

    def save_gradient(self, module, grad_input, grad_output):
        self.gradients = grad_output[0].detach()

    def generate_cam(self, input_tensor, target_class):
        # 前向传播
        output = self.model(input_tensor)

        # 反向传播
        self.model.zero_grad()
        one_hot = torch.zeros_like(output)
        one_hot[0][target_class] = 1
        output.backward(gradient=one_hot, retain_graph=True)

        # 计算CAM
        gradients = self.gradients[0]  # (C, H, W)
        activations = self.activations[0]  # (C, H, W)

        # 全局平均池化梯度作为权重
        weights = torch.mean(gradients, dim=(1, 2))  # (C,)

        # 加权求和
        cam = torch.zeros(activations.shape[1:], dtype=torch.float32)
        for i, w in enumerate(weights):
            cam += w * activations[i]

        # ReLU
        cam = F.relu(cam)

        # 归一化
        cam = cam.cpu().numpy()
        cam = cam - cam.min()
        cam = cam / (cam.max() + 1e-8)

        # 调整大小到输入图像
        cam = cv2.resize(cam, (224, 224))

        return cam


def show_gradcam(image, cam):
    """显示Grad-CAM结果"""
    img = image.cpu().squeeze().permute(1, 2, 0).numpy()
    img = (img - img.min()) / (img.max() - img.min())

    # 热力图
    heatmap = cv2.applyColorMap(np.uint8(255 * cam), cv2.COLORMAP_JET)
    heatmap = cv2.cvtColor(heatmap, cv2.COLOR_BGR2RGB)
    heatmap = np.float32(heatmap) / 255

    # 叠加
    result = heatmap * 0.4 + np.float32(img) * 0.6

    plt.figure(figsize=(10, 4))
    plt.subplot(1, 3, 1)
    plt.imshow(img)
    plt.title('原图')
    plt.axis('off')

    plt.subplot(1, 3, 2)
    plt.imshow(heatmap)
    plt.title('热力图')
    plt.axis('off')

    plt.subplot(1, 3, 3)
    plt.imshow(result)
    plt.title('叠加')
    plt.axis('off')

    plt.tight_layout()
    plt.show()


# 使用
model = models.resnet18(pretrained=True)
target_layer = model.layer4[-1]
gradcam = GradCAM(model, target_layer)

image = torch.randn(1, 3, 224, 224)
image.requires_grad = True
cam = gradcam.generate_cam(image, target_class=0)
show_gradcam(image, cam)

常见错误与解决方案

1. 图像维度错误

# 错误:PIL Image直接传入模型
img = Image.open('cat.jpg')
output = model(img)  # 错误!

# 解决:转换为张量
transform = transforms.Compose([
    transforms.ToTensor(),
])
img_tensor = transform(img).unsqueeze(0)  # 添加batch维度
output = model(img_tensor)

2. 标准化不一致

# 错误:训练和测试使用不同的标准化
train_transform = transforms.Compose([
    transforms.Normalize(mean=[0.5, 0.5, 0.5], std=[0.5, 0.5, 0.5])
])

test_transform = transforms.Compose([
    transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])
])

# 解决:使用相同的标准化
IMAGENET_MEAN = [0.485, 0.456, 0.406]
IMAGENET_STD = [0.229, 0.224, 0.225]

normalize = transforms.Normalize(mean=IMAGENET_MEAN, std=IMAGENET_STD)
# 训练和测试都使用这个normalize

3. GPU内存不足

# 错误:batch_size太大
train_loader = DataLoader(dataset, batch_size=256)  # 可能OOM

# 解决1:减小batch_size
train_loader = DataLoader(dataset, batch_size=32)

# 解决2:使用梯度累积
accumulation_steps = 8
optimizer.zero_grad()
for i, (images, labels) in enumerate(train_loader):
    outputs = model(images)
    loss = criterion(outputs, labels)
    loss = loss / accumulation_steps
    loss.backward()

    if (i + 1) % accumulation_steps == 0:
        optimizer.step()
        optimizer.zero_grad()

# 解决3:使用混合精度
scaler = GradScaler()
with autocast():
    outputs = model(images)

4. 模型eval模式忘记切换

# 错误:训练后忘记切换到eval模式
model.train()
# 训练完成后直接测试
test_acc = evaluate(model, test_loader)  # 错误!

# 解决:在评估前切换到eval模式
model.eval()
with torch.no_grad():
    test_acc = evaluate(model, test_loader)

CNN训练快速查阅表

# ============ 导入 ============
import torch
import torch.nn as nn
import torch.optim as optim
from torch.utils.data import DataLoader
from torchvision import transforms, datasets, models

# ============ 数据变换 ============
train_transform = transforms.Compose([
    transforms.RandomResizedCrop(224),
    transforms.RandomHorizontalFlip(),
    transforms.ToTensor(),
    transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])
])

test_transform = transforms.Compose([
    transforms.Resize(256),
    transforms.CenterCrop(224),
    transforms.ToTensor(),
    transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])
])

# ============ 数据加载 ============
train_dataset = datasets.CIFAR10(root='./data', train=True, transform=train_transform)
test_dataset = datasets.CIFAR10(root='./data', train=False, transform=test_transform)

train_loader = DataLoader(train_dataset, batch_size=64, shuffle=True, num_workers=4)
test_loader = DataLoader(test_dataset, batch_size=64, shuffle=False, num_workers=4)

# ============ 模型构建 ============
model = models.resnet18(pretrained=True)
model.fc = nn.Linear(model.fc.in_features, 10)  # 10类分类
model = model.cuda()  # 或 .to(device)

# ============ 损失函数和优化器 ============
criterion = nn.CrossEntropyLoss()
optimizer = optim.Adam(model.parameters(), lr=0.001)
scheduler = optim.lr_scheduler.StepLR(optimizer, step_size=10, gamma=0.1)

# ============ 训练循环 ============
for epoch in range(20):
    model.train()
    for images, labels in train_loader:
        images, labels = images.cuda(), labels.cuda()

        optimizer.zero_grad()
        outputs = model(images)
        loss = criterion(outputs, labels)
        loss.backward()
        optimizer.step()

    scheduler.step()

    # 验证
    model.eval()
    correct = 0
    total = 0
    with torch.no_grad():
        for images, labels in test_loader:
            images, labels = images.cuda(), labels.cuda()
            outputs = model(images)
            _, predicted = torch.max(outputs.data, 1)
            total += labels.size(0)
            correct += (predicted == labels).sum().item()

    print(f'Epoch {epoch}: Accuracy {100 * correct / total}%')

# ============ 保存模型 ============
torch.save(model.state_dict(), 'model.pth')

# 加载模型
model.load_state_dict(torch.load('model.pth'))

总结与下一步

本篇笔记核心要点

  1. 卷积操作:nn.Conv2d的参数(kernel_size, stride, padding, groups, dilation)
  2. 池化层:MaxPool2d, AvgPool2d, AdaptiveAvgPool2d
  3. 归一化:BatchNorm2d, LayerNorm, GroupNorm, Dropout
  4. 数据增强:transforms的各种变换(翻转、旋转、裁剪、颜色抖动等)
  5. 模型构建:LeNet, AlexNet, VGG, ResNet
  6. 迁移学习:特征提取、微调、模型保存加载
  7. 训练技巧:学习率调度、早停、梯度裁剪、混合精度

下一步学习建议

推荐资源


本笔记是USTC学生深度学习笔记系列第二篇