Pytorch

一、 环境安装与配置

💡 核心逻辑

推荐使用 Conda 进行环境管理。通过创建“虚拟环境”,可以在同一台机器上隔离不同版本的 Python 和 PyTorch,避免不同项目间的依赖冲突。

1.1 第一步:安装 Conda

  • 版本选择

    • Miniconda:轻量级,仅包含 Conda 和基础包,适合开发者。

    • Anaconda:全家桶版,包含大量科学计算包,占用空间较大。

  • 安装步骤:前往 Miniconda 官网或 Anaconda 官网。

    • Mac (Apple Silicon): 必须选择 macOS Apple M1/M2/M3 (M-series) 64-bit 版。

    • Windows: 选择 Windows 64-bit 版。

1.2 第二步:(仅限 Windows NVIDIA 用户) 安装 CUDA

Windows 若要使用 GPU 加速,必须满足硬件是 NVIDIA 显卡。

  • 检查显卡:在任务管理器中确认是否有 NVIDIA GPU。

  • 安装驱动:前往 NVIDIA 官网更新最新的显卡驱动。

  • CUDA 版本:通常 PyTorch 会自带简版 CUDA,但建议手动安装 CUDA Toolkit(如 11.8 或 12.1)以获得最佳兼容性。

1.3 第三步:安装 Python 及 PyTorch

打开终端(Mac 使用 Terminal,Windows 使用 Anaconda Prompt 或 PowerShell)。

1. 创建并激活虚拟环境

1
2
3
4
5
# 创建环境:python 3.10 具有最佳的库兼容性
conda create -n torch_env python=3.10 -y

# 激活环境
conda activate torch_env
  • torch_env:为所创建的虚拟环境名称。

  • python=3.10:为该虚拟环境中安装的 python 版本。

  • -y:表示安装过程中的一切询问都默认 yes。

2. 安装 PyTorch 核心库

根据硬件平台选择安装命令。请确保已提前激活虚拟环境。

运行平台安装命令硬件说明
Mac (M系列)pip install torch torchvision torchaudio原生支持 MPS 硬件加速
Windows (NVIDIA)pip install torch torchvision torchaudio --index-url [https://download.pytorch.org/whl/cu121](https://download.pytorch.org/whl/cu121)需要 CUDA 驱动支持 (示例为 12.1 版)
通用 (仅 CPU)pip install torch torchvision torchaudio无显卡加速,适合基础逻辑测试

💡 核心参数详解:--index-url

  • 为什么要手动指定源地址? pip 默认从 PyPI 下载,但 PyPI 仓库通常只存放通用版。PyTorch 官方源提供的包是针对不同 CUDA 版本专门编译优化的。

  • 作用:显式指定下载服务器的地址,引导 pip 去特定的仓库“提货”。

  • 平台差异

    • Windows/Linux (GPU):必须添加。不加此参数会导致 pip 默认安装 CPU 版本,无法调用 NVIDIA 显卡算力。

    • Mac (M系列):通常不加。苹果的 MPS 加速代码已直接集成在 PyTorch 的标准版本中。

1.4 第四步:跨平台验证脚本

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
import torch

print(f"PyTorch 版本: {torch.__version__}")

if torch.cuda.is_available():
    print(f"✅ 检测到 NVIDIA GPU,正在使用 CUDA 加速!设备:{torch.cuda.get_device_name(0)}")
elif torch.backends.mps.is_available():
    print("✅ 检测到 Apple Silicon,正在使用 MPS 加速!")
else:
    print("ℹ️ 当前使用 CPU 模式。")

二、 PyTorch 张量基础 (Tensor)

2.1 什么是张量?

张量是神经网络中最基础的数据结构,可以看作是标量、向量、矩阵向更高维度的推广。

  • 0 维张量:标量 (Scalar),即单个数字。

  • 1 维张量:向量 (Vector),类似于列表。

  • 2 维张量:矩阵 (Matrix),二维数组。

  • 3 维张量及以上:多维数组。

2.2 创建 Tensor

1. 常用初始化方法

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# 随机初始化 (0-1 均匀分布)
x = torch.rand(5, 3)

# 全 0 矩阵初始化
x = torch.zeros(5, 3, dtype=torch.long)

# 全 1 矩阵初始化
x = torch.ones(5, 3, dtype=torch.long)

# 从现有数据直接创建
x = torch.tensor([5.5, 3], dtype=torch.float)

2. 基于现有 Tensor 创建

方法说明继承属性
x.new_ones(shape)需要手动指定新形状类型 (dtype), 设备 (device), 自动微分 (requires_grad)
torch.randn_like(x)无需指定形状形状, 类型, 设备

3. 查看属性

  • 数据类型x.dtype (同质数据类型)。

  • 形状/大小x.shapex.size() (互为别名)。

  • 所在设备x.device (CPU/CUDA/MPS)。

2.3 张量的运算操作

1. 三种加法实现方式

  • 运算符重载print(x + y)

  • 函数调用torch.add(x, y)

  • 原地操作 (In-place)y.add_(x)

    (注意:PyTorch 中以 _ 结尾的方法会直接修改调用者。)

2. 索引与内存共享 (重要)

PyTorch 索引遵循 视图 (View) 机制。

1
2
3
x = torch.rand(5, 3)
y = x[:, 0]  # 切片第一列
y += 1       # 修改 y 会同步改变 x 的第一列
  • 核心原理:索引结果与原数据共享内存。

  • 优点:节省显存,避免拷贝。

  • 风险:修改子集会意外改变原张量。需独立副本请使用 y = x[:, 0].clone()


三、 Autograd 自动求导系统

核心定义:Autograd 是 PyTorch 的自动求导引擎,通过构建 动态计算图 (Dynamic Computational Graph) 记录操作历史,计算反向传播梯度。

核心组件grad_fn (算子记录) 与 grad (梯度存储)。

3.1 计算图的基石

1.1 开启梯度追踪

  • requires_grad 默认为 False。设为 True 后开启追踪。

  • 传递性:参与运算的张量只要有一个为 True,输出也为 True。

1.2 理解 grad_fn (计算记录器)

  • 叶子节点 (Leaf):通过 torch.tensor() 等原始创建的张量,grad_fn 为 None。

  • 非叶子节点:运算生成的张量,grad_fn 指向具体的 Function 对象(如 AddBackward)。

1
2
3
4
5
6
7
8
x = torch.randn(3, 5, requires_grad=True)
y = torch.randn(3, 5) # 默认为 False

z = x + y  # z.grad_fn 为 <AddBackward0>
t = z ** 2 # t.grad_fn 为 <PowBackward0>

print(x.is_leaf) # True
print(z.is_leaf) # False

3.2 反向传播:backward()

2.1 梯度计算逻辑

调用 loss.backward(),Autograd 逆流计算导数并存入叶子节点的 .grad

2.2 约束与内存分配

  • 标量触发:通常对单一数值(如 Loss)调用 backward()

  • 非标量触发:非标量需传入形状相同的权重参数。

  • 内存分配:默认只保留叶子节点的 .grad。中间变量欲保留需调用 z.retain_grad()

1
2
3
4
5
6
7
8
x = torch.ones(2, 2, requires_grad=True)
y = x + 2
z = y * y * 3
out = z.mean() # 这是一个标量

out.backward() # 反向传播
print(x.grad)  # 叶子节点有梯度值
print(y.grad)  # 中间变量默认为 None

3.3 梯度脱离:阻止追踪

  • with torch.no_grad(): 全局屏蔽梯度计算,常用于模型预测/验证。

  • .detach() 方法:分离张量,返回 requires_grad=False 的新视图,共享内存。

1
2
3
4
5
6
7
8
# 方式 1: 上下文管理器
with torch.no_grad():
    y = x * 2
    print(y.requires_grad) # False

# 方式 2: detach
x_detach = x.detach()
print(x_detach.requires_grad) # False

3.4 💡 进阶补充:陷阱与技巧

  • 梯度累加.grad 不会自动清零,反向传播时会累加。训练迭代前需手动调用 optimizer.zero_grad()

  • In-place 操作:可能破坏反向传播所需数据。建议使用 z = x + y 替代 x.add_(y) 以保安全。

1
2
3
4
5
# 梯度累加演示
out.backward()
print(x.grad)
out.backward() 
print(x.grad) # 梯度值会变大,因为发生了累加

四、 使用 CUDA 加速与并行计算

💡 核心逻辑

PyTorch 提供了简单的方法将模型和数据从 CPU 移动到 GPU。当单块显卡无法满足需求时,可以使用并行计算技术:DP(简单但不均衡)或 DDP(官方推荐,高性能)。

4.1 CUDA 核心概念介绍

CUDA(Compute Unified Device Architecture,统一计算设备架构)是 NVIDIA 推出的并行计算平台和编程模型。它允许开发者利用 NVIDIA GPU(图形处理器)强大的并行计算能力来处理复杂的计算任务,而不仅仅局限于图形渲染。

1. 为什么需要 CUDA?(CPU vs GPU)

在深度学习和科学计算中,GPU 比 CPU 更具优势,其核心差异在于设计理念:

  • CPU(中央处理器):设计目标是处理复杂的逻辑控制和串行任务。它拥有强大的算术逻辑单元(ALU),但数量较少,擅长“一个老师处理一个复杂的数学难题”。

  • GPU(图形处理器):设计目标是处理大规模并行任务。它拥有数以千计的简单核心,擅长“一千个小学生同时做简单的加法题”。

CUDA 正是连接这种强大硬件能力的桥梁。

2. CUDA 的核心架构:软件与硬件的映射

CUDA 的编程模型将任务拆解为多层级结构,以便于在硬件上高效调度:

逻辑层级 (Software)硬件实现 (Hardware)描述
Thread (线程)CUDA Core计算的最小单位。
Warp (线程束)-CUDA 调度的基本单位,通常包含 32 个线程,执行相同的指令(SIMT 架构)。
Block (线程块)Streaming Multiprocessor (SM)一组线程的集合,共享同一块“共享内存”(Shared Memory)。
Grid (网格)Entire GPU一个核函数(Kernel)启动时产生的所有线程集合。

3. CUDA 的工作流程:Heterogeneous Computing

CUDA 采用“异构计算”模式,即 Host (CPU) 负责逻辑控制,Device (GPU) 负责大规模数值计算。典型的执行步骤如下:

  1. 分配内存:在 GPU 显存上分配空间。

  2. 数据拷贝 (H2D):将数据从 CPU 内存拷贝到 GPU 显存。

  3. 启动核函数 (Kernel):CPU 指挥 GPU 启动成千上万个线程进行并行运算。

  4. 数据回传 (D2H):将计算结果从 GPU 显存拷贝回 CPU 内存。

  5. 释放资源:清理显存。

4. CUDA 在 PyTorch 中的角色

在 PyTorch 中,CUDA 是实现张量加速的底层后端。当你执行 tensor.to("cuda") 时,实际上发生了以下动作:

  1. PyTorch 调用 CUDA API 在显存中开辟空间。

  2. 数据通过 PCIe 总线传输至显卡。

  3. 随后的算子(如矩阵乘法)会调用 NVIDIA 提供的 cuBLAS 或 cuDNN 等高度优化的加速库。

4.2 常用的并行方式

在深度学习中,常用的并行方法主要分为 数据并行模型并行混合并行。理解它们的关键在于:任务(数据或参数)是如何被拆分并分发到不同设备上的。

1. 数据并行 (Data Parallelism, DP)

这是最常用、最基础的并行方式。它的核心思想是 “模型不动,数据拆分”

  • 拆分方式

    • 模型副本:每个 GPU 上都完整地拷贝一份相同的模型参数。

    • 数据切分:将一个大的 Batch(批次)数据平均拆分成 N 份(N 为 GPU 数量)。例如,Batch Size 为 64,有 4 张显卡,则每张显卡分到 16 个样本。

  • 工作流程

    • 分发:主设备将不同的数据子集发给各 GPU。

    • 并行计算:每个 GPU 独立进行前向传播和反向传播,计算出各自的梯度。

    • 梯度同步:各 GPU 将计算出的梯度进行汇总(All-Reduce),取平均值。

    • 参数更新:所有 GPU 使用相同的平均梯度同步更新自己的模型参数,确保下一轮迭代时模型依然一致。

2. 模型并行 (Model Parallelism, MP)

当模型极其巨大(如千亿级参数量),单张显卡的显存连模型本身都装不下时,就需要模型并行。它的核心思想是 “数据不动,模型拆分”

模型并行又细分为 张量并行流水线并行

  • 2.1 张量并行 (Tensor Parallelism, TP)

    • 拆分方式:将模型中的某个层(如大型矩阵乘法层)拆开。

    • 并行逻辑:将一个巨大的权重矩阵 W 拆分为 W_1 和 W_2,分别放在 GPU 0 和 GPU 1 上。计算时,数据会同时进入两张显卡进行局部计算,最后通过通信合并结果。

    • 特点:通信非常频繁,通常要求显卡之间有极高的带宽(如 NVLink)。

  • 2.2 流水线并行 (Pipeline Parallelism, PP)

    • 拆分方式:按模型的层(Layer)进行纵向切割。

    • 并行逻辑:将模型的前一半层放在 GPU 0,后一半层放在 GPU 1。

    • 工作流程:数据像流水线一样,先在 GPU 0 处理,结果传给 GPU 1 继续处理。

    • 优化:为了避免 GPU 空闲(泡泡/Bubble),通常会将数据进一步切分为 Micro-batches(微批次),让不同 GPU 同时处理不同微批次的任务。

3. 混合并行 (Mixed Parallelism)

在训练超大规模模型(如 GPT-4, Llama 3)时,通常会将上述方法结合使用,构建成一个 3D 并行 体系:

  • 数据并行:扩大训练规模,增加吞吐量。

  • 张量并行:解决单个超大层(如 Attention 层)的显存瓶颈。

  • 流水线并行:跨节点连接不同的模型切片。

4. 总结与对比

方法拆分对象解决的问题通信开销
数据并行 (DP/DDP)数据 (Samples)提高训练速度,增加 Batch Size中等(同步梯度)
张量并行 (TP)层内参数 (Tensors)单层参数量过大,显存溢出极高(同步神经元输出)
流水线并行 (PP)网络层数 (Layers)模型层数过多,单卡装不下整个模型较低(同步层间激活值)

在实际操作中,PyTorch 提供的 DDP (Distributed Data Parallel) 是最推荐的通用数据并行工具;而对于大模型训练,则通常使用 DeepSpeed 或 Megatron-LM 这种高度集成的混合并行框架。

4.3 单卡训练 (Single GPU)

要让 GPU 运行,必须确保模型和数据都在同一块显存中。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
if torch.cuda.is_available():
    device = torch.device("cuda") # NVIDIA GPU
elif torch.backends.mps.is_available():
    device = torch.device("mps")  # Apple Silicon GPU
else:
    device = torch.device("cpu")  # 默认 CPU

# 1. 搬运模型(一次性)
model = Net().to(device) 

# 2. 搬运数据(在训练循环中)
for image, label in dataloader:
    image = image.to(device)
    label = label.to(device)

4.4 DP vs DDP

1. DataParallel (DP)

1
2
3
4
5
6
7
# --- Linux/Windows 版本 ---
if torch.cuda.device_count() > 1:
    model = torch.nn.DataParallel(model)
model.to(device)

# --- macOS 版本 ---
# Mac M 系列暂不支持显卡间的 DataParallel,通常直接使用 .to(device)
  • 运转方式:单机多卡

    DP 的核心是“单进程多线程”模式。

  • 内存空间的限制:在单机上,所有的线程都运行在同一个进程的内存空间内,主线程可以非常方便地通过 Python 内部指令去指挥不同的显卡线程。

  • 通信协议的缺失:DP 并没有设计一套能够跨越物理机器、通过网络(如以太网或 InfiniBand)进行数据交换的协议。它只能利用单机内部的 PCIe 总线进行极速通信。

在 DataParallel (DP) 模式下,存在一个“主卡”(通常是 device_id 序列中的第一块卡,默认是 cuda:0),它扮演着“调度中枢”的角色。

  • 深度理解:单进程多线程

    • 单进程:当你运行 Python 脚本时,操作系统只启动了一个 Python 进程。这意味着所有的显卡控制逻辑都挤在这个进程里。

    • 多线程:为了同时操作多张显卡,该进程会启动多个线程,每个线程负责控制一块显卡。

    • 瓶颈:由于 Python 存在 GIL(全局解释器锁),在同一时间只能有一个线程执行 Python 字节码。这导致 DP 在控制多块显卡时,线程切换的开销很大,无法发挥显卡的最高效率。

  • 主卡(Master Node)的具体职责

    在 DP 模式下,主卡不仅要参与计算,还要承担繁重的“管理工作”:

    • 分发 (Scatter):主卡负责将从磁盘读取的一个 Mini-Batch 数据,切分成更小的块(Sub-batches)。主卡通过 PCIe 总线将这些数据块分发给其他的从卡(Slave Nodes)。

    • 模型同步 (Replicate):主卡将自己的模型参数(Weights)完整地拷贝到每一块从卡上,确保大家算的是同一个模型。

    • 计算 (Parallel Apply):所有卡(包括主卡)并行进行前向传播(Forward Pass)。

    • 汇总 (Gather & Reduce)关键点:所有从卡计算出的输出(Outputs)必须传回主卡。主卡负责收集所有的输出,计算总的 Loss,并分发给各卡进行反向传播计算梯度。最后,主卡再次收集所有卡算出的梯度,进行平均(Reduce),并在主卡上更新参数。

  • 为什么会“负载不均衡”?

    由于上述的分发和汇总全部由主卡完成,你会观察到以下现象:

    • 显存占用不均:主卡因为要暂存所有卡的输出和梯度,其显存占用(Memory Usage)通常比其他卡高出很多。

    • 计算浪费:当显卡数量增多时,主卡忙于通信和汇总,会导致其他显卡进入短暂的“空转”等待状态。

  • 笔记补充:Linux 与 Mac 的差异

维度Linux/Windows (DP)macOS (M 系列)
进程模型单进程多线程单进程单线程
主从结构存在明确的主卡(Master)不存在,因为通常只有一块集成 GPU
内存逻辑显存与内存独立,存在 H2D/D2H 拷贝统一内存架构 (UMA),无需跨设备拷贝数据

总结

DataParallel 就像是一个中央集权的组织:主卡是决策者,负责分发任务、收集结果并总结。而 DistributedDataParallel (DDP) 则像是分布式联邦:每个 GPU 都是独立的进程,通过彼此交换信息(Ring-AllReduce)达成共识,因此效率更高、负载更稳。这种“单进程”与“多进程”的区别,正是 DP 逐渐被 DDP 取代的核心原因。

2. DistributedDataParallel (DDP)

DistributedDataParallel (DDP) 是 PyTorch 官方强烈推荐的并行训练方案。与传统的 DataParallel (DP) 相比,它在底层设计上有着本质的飞跃,是目前工业界处理大规模模型训练的标准工具。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
# --- Linux/Windows (NVIDIA NCCL) ---
import torch.distributed as dist
from torch.nn.parallel import DistributedDataParallel as DDP

dist.init_process_group(backend='nccl') # NVIDIA 专用高速后端
local_rank = dist.get_rank()
torch.cuda.set_device(local_rank)
device = torch.device("cuda", local_rank)

model = model.to(device)
model = DDP(model, device_ids=[local_rank])

# --- macOS (Gloo 后端) ---
# Mac 不支持 NCCL,若需跑通 DDP 流程进行调试,需使用 gloo 后端
if torch.backends.mps.is_available():
    dist.init_process_group(backend='gloo') # Mac 仅支持 gloo
    # 注:DDP 在 Mac MPS 上的加速效果有限,通常仅用于代码调试
  • 运转方式:多机多卡

  • DDP 的核心设计:多进程模式

    DDP 的精髓在于 “多进程”。

    • 进程分配:DDP 会为每一块显卡启动一个独立的 Python 进程。例如,如果你有 8 张显卡,系统就会运行 8 个独立的程序副本。

    • 消除 GIL 锁瓶颈:由于每个进程都有自己独立的 Python 解释器,因此彻底避开了 Python 全局解释器锁(GIL)的限制,让每块显卡都能满载运行。

    • 去中心化:不同于 DP 需要一个“主卡”来汇总所有数据,DDP 中的每个进程地位平等,它们通过相互通信来同步梯度。

  • 核心技术:Ring-AllReduce 算法

    DDP 高效的关键在于梯度同步的方式。它不采用“汇总到中心再分发”的模式,而是采用 Ring-AllReduce 算法:

    • 环形通信:所有显卡连接成一个逻辑上的环。

    • 切片交换:每个显卡将自己的梯度切成小块,只传递给环中的下一个邻居。

    • 带宽利用率高:无论显卡数量如何增加,每块显卡承担的通信量基本恒定。这使得 DDP 在显卡数量极多时(如百卡、千卡集群)依然能保持极高的效率。

  • DDP 的工作流程

    1. 初始化:启动 N 个进程,通过网络建立连接(使用 nccl 或 gloo 后端)。

    2. 数据分发:使用 DistributedSampler 确保每个进程读取的数据子集是不重叠的。

    3. 前向传播:每个进程在自己的显卡上运行模型,计算输出。

    4. 梯度计算与同步:在反向传播期间,各进程通过 AllReduce 异步交换梯度并取平均值。

    5. 更新参数:所有进程使用完全相同的平均梯度更新本地模型参数,确保下一轮训练开始前,所有显卡上的模型依然是镜像一致的。

  • 跨平台适配建议 (Linux vs Mac)

特性Linux/Windows (NVIDIA)macOS (Apple Silicon)
通信后端NCCL (NVIDIA 专用,性能最强)Gloo (Apple M系列目前仅支持此后端)
启动方式推荐使用 torchrun 工具推荐使用 torchrun 或多进程脚本
优势极高的吞吐量,支持多机多卡主要用于分布式代码本地调试
  • 进阶知识点:DistributedSampler

    在 DDP 模式下,必须配合使用 DistributedSampler

    • 原理:如果直接使用普通 DataLoader,每个进程都会从第一条数据开始读,导致所有 GPU 都在跑同样的数据,白白浪费算力。DistributedSampler 会根据当前的 rank(进程编号)和 world_size(总进程数)对数据集进行切分,确保“各跑各的”。
  • 保存模型的注意事项

    在使用 DDP 后,模型会被包装在 DistributedDataParallel 对象中。

    • 保存:必须使用 torch.save(model.module.state_dict(), PATH),加上 .module 才能剥离分布式外壳,保存纯净的模型参数。

    • 加载:加载时也要注意,如果是在单卡环境加载 DDP 保存的模型,可能需要处理键名中的 module. 前缀。

2.3.1 DDP 核心概念:进程组(Process Group)

在分布式计算中,DDP 通过“进程组”来管理多个计算单元。

  • GROUP: 进程组,默认为全局组(World)。可以通过 new_group 创建子集进行精细通信。

  • WORLD_SIZE: 全局总进程数。

    • 单机多卡:GPU 总数。

    • 多机多卡:机器数 × 每台机器的 GPU 数。

  • RANK: 全局进程序号(0 到 WORLD_SIZE-1)。Rank 0 通常作为 Master 节点。

  • LOCAL_RANK: 单个节点(机器)内的 GPU 编号。由启动工具自动分配。

2.3.2 DDP 代码编写流程

在使用 DDP 时,代码的逻辑结构需要进行“分布式改造”:

1. 自动获取 Local Rank

不再手动指定 GPU ID,而是通过环境变量或命令行参数接收启动工具传入的编号。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
import argparse
import os

# 推荐:从环境变量自动获取(torchrun 模式)
local_rank = int(os.environ.get("LOCAL_RANK", 0))

# 必须设置当前进程使用的设备
if torch.cuda.is_available():
    torch.cuda.set_device(local_rank)
    device = torch.device("cuda", local_rank)
elif torch.backends.mps.is_available():
    # Mac 虽不支持 NCCL 并行,但可用于代码逻辑调试
    device = torch.device("mps")

2. 初始化进程组(后端选择)

选择合适的通信后端(Backend)是性能的关键:

后端硬件环境建议场景
NCCLNVIDIA GPU最佳选择。支持 InfiniBand 和 Ethernet,性能最高。
GLOOCPU / MacCPU 分布式首选。在 Mac 上调试 DDP 时使用。
MPI高性能计算集群仅在有特殊 MPI 需求的环境下使用。
1
2
3
# 初始化
# Mac 用户请将 backend 改为 'gloo'
torch.distributed.init_process_group(backend='nccl' if torch.cuda.is_available() else 'gloo')

3. 数据集划分:DistributedSampler

为了确保 N 个进程不跑重复的数据,必须对数据进行“切片”。

1
2
3
4
5
6
7
8
# 仅训练集需要 Sampler,测试集通常不需要
train_sampler = torch.utils.data.distributed.DistributedSampler(train_dataset)

train_loader = torch.utils.data.DataLoader(
    train_dataset, 
    batch_size=16, 
    sampler=train_sampler # 传入采样器
)

4. 包装模型

1
2
3
# 使用 DDP 包装
model = model.to(device)
model = torch.nn.parallel.DistributedDataParallel(model, device_ids=[local_rank] if torch.cuda.is_available() else None)

2.3.3 如何启动 DDP 训练

DDP 不能直接用 python main.py 启动,需要使用专门的启动器来同步开启多个进程。

A. Linux/Windows (单机多卡)

使用 torchrun(PyTorch 官方目前推荐,取代了旧版的 launch):

1
2
# 使用 0,1,2,3 号显卡,每台机器 4 个进程
CUDA_VISIBLE_DEVICES=0,1,2,3 torchrun --nproc_per_node=4 main.py

B. macOS (本地逻辑调试)

由于 Mac 只有一块集成 GPU,多进程并行的意义在于调试分布式代码:

1
2
# 在 Mac 上模拟 2 个进程(使用 GLOO 后端)
torchrun --nproc_per_node=2 main.py

💡 经验之谈 (Obsidian Tips)

  • 网络接口设置

    如果服务器有多块网卡(如同时有以太网和 InfiniBand),NCCL 可能会找错接口导致挂起。

    可以在代码中手动指定:

    1
    2
    
    # 找到 ifconfig 中对应的网卡名,如 eth0
    os.environ['NCCL_SOCKET_IFNAME'] = 'eth0' 
    
  • DDP vs DP 的性能

    即使在单机多卡环境下,也优先选择 DDP。

    1. DDP 没有单进程的 GIL 锁限制。

    2. DDP 的梯度同步(Ring-AllReduce)比 DP 的主卡汇聚快得多。

    3. DDP 支持跨机器扩展,而 DP 只能死守单机。

4.5 💡 并行计算对比表

方案易用性性能场景建议
DP⭐⭐⭐⭐⭐⭐⭐⭐快速实验、双卡轻量训练
DDP⭐⭐⭐⭐⭐⭐⭐⭐工业界标准、大型模型训练

五、 AI 硬件加速设备

💡 核心逻辑

深度学习对算力的渴求推动了硬件从 通用(CPU)并行(GPU) 再向 专用(ASIC/TPU/NPU) 的演进。

  • CPU:全能管家,逻辑控制强,计算效率低。

  • GPU:并行专家,浮点运算强,功耗高,依赖 CPU。

  • ASIC:定制工匠,为特定算法设计,性能功耗比极高。

5.1 通用处理器:CPU vs GPU

特性CPU (中央处理器)GPU (图形处理器)
设计目标处理复杂指令、分支跳转、逻辑控制处理海量、简单的数据并行运算
架构特点控制单元大,晶体管主要用于缓存和逻辑大量晶体管组成专用电路和流水线
计算瓶颈冯·诺依曼结构导致频繁读取指令/存储功耗高,无法独立工作,需 CPU 调用

5.2 TPU (Tensor Processing Unit, 张量处理器)

TPU 是谷歌为优化 TensorFlow 框架而定制的 ASIC 芯片,专为神经网络计算设计。

1. 核心架构:脉动阵列 (Systolic Array)

  • 矩阵乘法单元 (MMU):TPU 的心脏。包含 $256 \times 256$ 个 MAC 部件,每个周期可执行 65,536 次 8 位乘加操作。

  • 计算逻辑:数据像血液脉动一样流过芯片。多个运算逻辑单元(ALU)串联,复用一个寄存器的读取结果,极大降低了内存带宽压力。

  • 性能指标:以 700MHz 运行,每秒可执行约 92 万亿次矩阵运算。

2. 技术特点

  • AI 加速专用:特定领域架构 (DSA),指令集精简,深度学习效率极高。

  • 确定性功能:舍弃了缓存、分支预测等复杂逻辑。运行时间可精准预测,使芯片能以接近峰值的吞吐量运行。

  • 大规模片上内存:一代 TPU 拥有占芯片面积 35% 的内存(24MB 局部内存 + 4MB 累加器内存),节约了访存能耗。

5.3 NPU (Neural-network Processing Unit, 神经网络处理器)

NPU 采用“数据驱动并行计算”架构,旨在解决冯·诺依曼结构中存储与处理分离导致的效率瓶颈。

1. 寒武纪 DianNao (电脑) 系列演进

型号定位与特点核心组件 (NFU)
DianNao基础加速器,模拟神经元工作NFU-1: 256个乘法器



NFU-2: 加法树



NFU-3: 激活函数单元
DaDianNao多核升级版,专攻训练任务$16 \times 16$ 尺寸 NFU,数据通路更灵活
ShiDianNao机器视觉专用,2D 格点结构唯一考虑运算单元级数据重用的 2D 阵列
PuDianNao异构加速器,支持多种机器学习算法MLU (机器学习单元) + ALU (处理通用计算)

2. NPU 的运算逻辑 (以 DianNao 为例)

  • 向量/卷积运算:NFU-1 完成元素相乘,NFU-2 完成结果累加,NFU-3 完成激活映射。

  • 池化运算:利用 NFU-2 完成最大值或平均值提取。

  • PuDianNao 的多算法支持:支持神经网络、SVM、朴素贝叶斯、K-Means 等 7 种算法,是该系列中功能最灵活的单元。

💡 核心知识点总结表

硬件类型核心优势主要缺点适用场景
CPU极高的灵活性、逻辑处理算力密度低、访存延迟高逻辑控制、前处理
GPU通用并行计算、成熟生态功耗巨大、数据吞吐效率上限通用深度学习训练
TPU极速矩阵运算、低延迟仅限特定框架、灵活性差云端大规模推理/训练
NPU存储处理一体化、低功耗硬件逻辑固定、开发门槛高手机/监控等移动端 AI

六、 PyTorch 各模块的使用

6.1 从机器学习到深度学习

1.1 什么是机器学习 (ML)?

机器学习是人工智能的一个子集,其核心在于“不通过显式编程来赋予计算机学习能力”。它通过算法从数据中寻找模式,并利用这些模式对未知数据进行预测。

  • 特征工程:传统机器学习(如 SVM、随机森林)极其依赖人工设计的特征。如果特征选得不好,模型性能上限会很低。

1.2 什么是深度学习 (DL)?

深度学习是机器学习的一种特殊形式,利用多层人工神经网络来学习数据的高阶抽象表示。

  • 端到端学习:它最大的优势是自动特征提取。你只需输入原始数据(如像素),神经网络会自动学习从简单线条到复杂轮廓的特征。

1.3 核心对比总结

维度机器学习深度学习
数据量少量数据即可起步极度依赖海量数据(大数据)
硬件普通 CPU 即可依赖高性能 GPU/TPU
特征工程人工干预多,需专家知识自动化程度高,端到端学习
可解释性较强(如决策树)较弱(“黑盒”模型)

6.2 深度学习项目标准流程 (PyTorch 视角)

在安装 PyTorch 时,官方通常建议通过一行命令同时安装 torchtorchvisiontorchaudio。这三者构成了 PyTorch 开发的“三剑客”,分别负责底层核心、视觉任务和音频任务。

以下是它们的详细解析及用途:

torch (核心引擎)

这是 PyTorch 的主体库,也是所有操作的基础。

  • 核心功能

    • 张量计算 (Tensors):提供类似于 NumPy 但支持 GPU 加速的多维数组运算。

    • 自动求导 (Autograd):深度学习的灵魂,自动计算梯度以实现反向传播。

    • 神经网络模块 (nn.Module):定义层、损失函数和各种架构。

  • 用途:它是所有 AI 模型运行的底座,负责处理数学运算和硬件调度(如前文提到的 CPU、GPU、TPU 资源分配)。

torchvision (计算机视觉专家)

这是一个专门为图像和视频处理设计的扩展库,极大地简化了视觉任务的开发。

  • 核心功能

    • 标准数据集 (datasets):内置了如 MNIST、CIFAR10 等常用数据集,并提供了 ImageFolder 等工具来读取按目录存放的图片数据。

    • 图像变换 (transforms):提供缩放、裁剪、归一化等预处理功能。

    • 底层 IO:如 read_image 函数,可直接将图片文件读取为 PyTorch 张量。

    • 预训练模型:内置了 ResNet、VGG、YOLO 等经典的成熟模型,可以实现“拎包入住”式的迁移学习。

  • 用途:只要你的任务涉及图片识别、目标检测或视频分析,就必然会用到它。

torchaudio (音频处理专家)

类似于 torchvision,它是专门针对音频和信号处理的扩展库。

  • 核心功能

    • 音频加载:支持 wav、mp3 等格式的加载,并自动转换为张量。

    • 信号变换:可以轻松将声波转换为梅尔频谱 (Mel-spectrogram) 等模型更易理解的形式。

    • 专业算法:内置了重采样、滤波、波形增强等音频专用算法。

  • 用途:适用于语音识别、音乐生成、声纹识别等任务。

总结对比

模块名称专注领域核心作用
torch通用深度学习提供张量运算、自动求导和硬件加速。
torchvision图像/视频处理图片数据、执行数据增强、调用预训练视觉模型。
torchaudio音频/语音加载音频文件、转换频谱、处理音频信号。

💡 关于 Dataset 与 DataLoader 的“户口”归属

这是一个非常关键的逻辑点,决定了你代码的组织方式:

  • 核心规则在 torchDataset 基类与 DataLoader 工具的源代码其实都在 torch 核心库中(位于 torch.utils.data)。它们定义了 PyTorch 处理数据的通用游戏规则:无论你处理的是图片、文字还是音频,都必须遵循“定义仓库 (Dataset)”和“架设传送带 (DataLoader)”的流程。

  • 现成素材在 torchvision / torchaudio:这两个模块是根据上述规则,针对特定领域提供的样板间。例如,torchvision.datasets.ImageFolder 就是一个已经按照 torch 的规范写好的 Dataset 子类,专门用来读图片。

深度总结:数据如何流转?

我们可以用下面这个流程图来理解它们是如何协同工作的:

  1. 现实世界:原始的 .jpg 图片或 .wav 音频文件。

  2. 翻译阶段 (torchvision / torchaudio):利用这些模块中的工具(如 read_image 或自定义的 MyDataset)将原始文件读取、裁剪、缩放。

  3. 标准化阶段 (Dataset):数据被包装成统一的格式,并明确“一共有多少”和“怎么拿一个”。

  4. 分发阶段 (DataLoader):利用 torch 自带的传送带,将数据打包成 Batch,并决定是否打乱顺序。

  5. 计算阶段 (torch):数据最终以 Tensor(张量) 的形式进入显卡,交给 torch 进行疯狂的矩阵运算。

核心结论torch 负责计算,不挑食;torchvision/torchaudio 负责洗菜切菜,把原始食材加工成 torch 爱的张量。 虽然我们在做视觉任务时经常引用 torchvision,但请记住,DataLoader 永远是你从 torch 主库里请来的“搬运工”。

2.1 基本配置 (Basic Configuration)

这是项目的“地基”。

  • 环境设置:包括导入 torch 库,设置随机种子以确保结果可复现。

  • 设备分配:检测并指定运行设备(cpu、cuda 或 mps)。

  • 超参数定义:统一管理学习率 (LR)、Batch Size、训练轮数 (Epochs) 等关键参数。

2.2 数据读入 (Data Loading)

PyTorch 的数据读入是通过 DatasetDataLoader 协作完成的。

  • Dataset:定义了数据的“仓库规范”。它规定了数据在哪、有多少、每一份长什么样。

  • DataLoader:定义了数据的“分发规则”。它负责按批次(Batch)抓取数据、打乱顺序(Shuffle)以及多进程加速。

1. 构建自己的数据读取流程

要实现灵活的数据读取,我们需要定义一个继承自 torch.utils.data.Dataset 的类。这个自定义类必须包含以下三个核心函数:

  • __init__:用于向类中传入外部参数(如文件路径、变换操作),并定义样本集。

  • __len__:返回数据集中的样本总数。

  • __getitem__:用于逐个读取样本集合中的元素。在这里可以进行数据变换,并返回训练或验证所需的 (data, label) 对。

2. 常见的数据读取方式

根据数据的复杂程度,通常有以下三种处理方案:

方案一:简单情况 —— 使用 PyTorch 内置数据集

如果你处于学习阶段,正在练习经典模型,torchvision.datasets 已经为你封装好了现成的类。

  • 适用场景:MNIST、CIFAR-10、COCO 等标准学术数据集。

  • 优点:一行代码即可实现自动下载、解析和加载。

1
2
3
4
5
6
7
8
9
from torchvision import datasets, transforms

# 以 MNIST 为例(如果你想用 CIFAR10,只需把 MNIST 改掉即可)
train_data = datasets.MNIST(
    root='./data',           # 1. 存储路径
    train=True,              # 2. 训练集 vs 测试集
    download=True,           # 3. 是否自动下载
    transform=transforms.ToTensor() # 4. 数据变换(最关键的一步)
)

参数详解

参数名作用深度解析
root存储路径指定数据集下载后存放在本地的哪个文件夹。如果文件夹不存在,它会自动创建。建议统一放在 ./data 下方便管理。
train模式选择True:加载训练集(通常 60,000 张);False:加载测试集(通常 10,000 张)。你需要分别创建这两个对象。
download自动下载True:如果 root 路径下没找到数据,就去官网下载。如果已经有了,它会跳过下载直接加载,非常智能。
transform数据变换必考点。原始下载的数据通常是 PIL 图片格式,模型看不懂。ToTensor() 将其转为 Tensor 并将像素值归一化到 [0, 1] 之间。

为什么 transform 这一步必不可少?

你可能会问:“我不设置 transform 行吗?” 答案是:不行。

  1. 格式不兼容:如果不加 transform,Dataset 返回的是 PIL 图片,当你把它喂给 DataLoader 时,程序会因为无法把图片打包成“张量批次”而报错。

  2. 维度重排(Dimension Reordering):原始图片(PIL 或 NumPy)通常是 HWC 格式(高度、宽度、通道)。PyTorch Tensor 要求必须是 CHW 格式(通道、高度、宽度)。ToTensor() 会自动把通道维度(如 RGB 的 3 层)挪到最前面。

  3. 数据类型转换(Type Conversion):将原本是整数类型(uint8,范围 0-255)的像素点,转化为浮点数张量(FloatTensor)。

  4. 归一化缩放(Scaling/Normalization):它会将像素值从原本的 [0, 255] 自动缩放到 [0.0, 1.0] 之间。这对于防止神经网络在训练初期发生梯度爆炸至关重要。

“全自动”与“纯手工”的对比

  • 使用 datasets.MNIST:你不需要关心数据集是从哪个 URL 下载的,不需要关心图片是怎么存在二进制文件里的,甚至不需要关心标签是怎么对齐的。PyTorch 全帮你做了。

  • 自定义 MyDataset:当你面对公司内部的私有数据(比如 CSV 里的医疗影像)时,你就需要手动实现 __init____getitem__ 来告诉 PyTorch 如何读取。

方案二:标准情况 —— 使用 ImageFolder

如果你的图片数据已经按照“一个类别一个文件夹”的格式整齐排好了,PyTorch 提供了“懒人神器”。

  • 逻辑:它会自动扫描目录,将子文件夹名直接映射为标签(Label)。

  • 代码实现

1
2
3
4
5
6
7
8
from torchvision import datasets

# 目录结构:data/train/cat/*.jpg, data/train/dog/*.jpg
train_data = datasets.ImageFolder(
    root='data/train', 
    transform=data_transform
)
# 此时 train_data.classes 会自动识别为 ['cat', 'dog']

在 PyTorch 中,原始图像(通常是不同尺寸、不同光照的图片文件)在喂给神经网络之前,必须经过统一的清洗和格式转换。这个“清洗和转换”的过程,就被统称为 Transform(变换)

data_transform 是如何定义的?

通常我们会使用 torchvision.transforms.Compose 将一系列操作像“串糖葫芦”一样串起来。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
from torchvision import transforms

data_transform = transforms.Compose([
    transforms.Resize((224, 224)),      # 1. 统一尺寸:管你原图多大,全变成 224x224
    transforms.RandomHorizontalFlip(),  # 2. 数据增强:随机左右翻转,增加模型鲁棒性
    transforms.ToTensor(),              # 3. 核心转换:把图片转为 Tensor,并归一化到 [0, 1]
    transforms.Normalize(               # 4. 标准化:让数据符合正态分布,加速收敛
        mean=[0.485, 0.456, 0.406], 
        std=[0.229, 0.224, 0.225]
    )
])

它在整个流程中起什么作用?

在你的代码 datasets.ImageFolder(root=..., transform=data_transform) 中,一旦你绑定了这个变量,就会触发以下连锁反应:

  • 按需加工:每当 DataLoader 准备去抓取一张图时,这张图会先经过 data_transform 里的所有步骤。

  • 数据增强(Data Augmentation):通过随机翻转、裁剪、旋转等操作,同一张图在每一轮训练中看起来可能略有不同,这能有效防止模型“死记硬背”,提升泛化能力。

  • 格式对齐:神经网络对输入非常挑剔。data_transform 确保了进入模型的所有数据都是同样的大小、同样的数值范围、同样的 Tensor 格式。

方案三:真实/复杂情况 —— 必须自定义 Dataset

在实际项目或科研中,数据往往是碎片化的(如标签在 CSV 里、输入是图文混排、或者是音频/点云等)。此时必须通过继承 Dataset 类来自定义逻辑。

核心“函数”:

你需要在一个类中实现三个关键动作:

  • __init__:初始化仓库(读取路径、读取 CSV 标签)。

  • __len__:告诉程序一共有多少条数据。

  • __getitem__:根据索引 idx 拿出一条数据(并在这里做数据清洗/变换)。

代码模板:

假设你的标签存储在一个 CSV 文件中:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
import os
import pandas as pd
from torch.utils.data import Dataset # 导入 Dataset 基类
from torchvision.io import read_image # 专门用于将图片读取为张量的工具

# 自定义类必须继承 torch.utils.data.Dataset
class MyDataset(Dataset):
    def __init__(self, annotations_file, img_dir, transform=None):
        """
        __init__: 初始化阶段,主要负责加载元数据(如 CSV 标签)
        annotations_file: 包含图片文件名和标签的 CSV 路径
        img_dir: 图片文件夹的实际存放路径
        transform: 预定义的数据变换流水线(可选项)
        """
        self.img_labels = pd.read_csv(annotations_file) # 使用 pandas 读取 CSV
        self.img_dir = img_dir                          # 记录图片目录
        self.transform = transform                      # 记录变换操作

    def __len__(self):
        """
        __len__: 告诉 PyTorch 这个数据集里总共有多少个样本
        """
        return len(self.img_labels) # 返回 CSV 文件的行数

    def __getitem__(self, idx):
        """
        __getitem__: 核心函数,根据索引 idx 抓取并返回一个样本
        idx: DataLoader 传过来的索引
        """
        # 1. 拼接图片的完整磁盘路径
        # self.img_labels.iloc[idx, 0] 取的是 CSV 中第 idx 行、第 0 列的内容(文件名)
        img_path = os.path.join(self.img_dir, self.img_labels.iloc[idx, 0])
        
        # 2. 读取图片并直接转化为 Tensor (张量)
        # torchvision.io.read_image 会自动处理底层 IO
        image = read_image(img_path) 
        
        # 3. 获取对应的标签
        # self.img_labels.iloc[idx, 1] 取的是 CSV 中第 idx 行、第 1 列的内容(标签数字)
        label = self.img_labels.iloc[idx, 1]
        
        # 4. 如果定义了 transform,则在返回前对图片进行加工
        # 这一步非常灵活,你可以同时处理图片、文字或音频
        if self.transform:
            image = self.transform(image)
            
        # 最后返回一个 (数据, 标签) 的元组
        return image, label

3. 最终投喂:DataLoader

不论你用哪种方案生成的 train_data,最后都要交给 DataLoader 这个“传送带”:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
from torch.utils.data import DataLoader

# 实例化 DataLoader:架设传送带
train_loader = DataLoader(
    dataset=train_data,      # 1. 告诉传送带从哪个仓库(Dataset 实例)取货
    batch_size=64,           # 2. 批次数:一次打包多少个样本。64 是常用的经验值
    shuffle=True,            # 3. 洗牌:每一轮(Epoch)开始前是否打乱顺序。防止模型死记硬背
    num_workers=4,           # 4. 搬运工数量:开启多少个子进程并行读取数据
    drop_last=True           # 5. 舍弃尾数:如果总数不能被 64 整除,是否丢弃最后那个“不满员”的包
)

# --- 验证读取流程 ---

# iter(train_loader) 将传送带转换为一个可迭代的“生成器”
# next(...) 则是从这个生成器中手动“抓取”第一个包(第一个 Batch)
images, labels = next(iter(train_loader))

# 打印查看这个包的“规格”
print(f"当前 Batch 的形状: {images.shape}") 
# 预期输出类似: torch.Size([64, 3, 224, 224])

一句话总结

torch 制定规则(Dataset/DataLoader),torchvision/audio 提供素材处理。 掌握了自定义 Dataset,你就拥有了处理任何异构数据的能力。

4. 验证数据加载情况

可以通过 iternext 手动查看加载的数据形状及内容。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
import matplotlib.pyplot as plt

# 获取第一个批次
images, labels = next(iter(val_loader))
print(f"图像张量形状: {images.shape}") # [batch_size, channels, height, width]

# 可视化第一张图片
# 注意:PyTorch 张量是 (C, H, W),绘图需转为 (H, W, C)
plt.imshow(images[0].transpose(1, 2, 0))
plt.show()

经验之谈:在使用 matplotlib 绘图时,一定要记得使用 .transpose(1, 2, 0)。这是因为 PyTorch 默认的通道顺序是 (C, H, W),而常用的绘图库要求 (H, W, C)。

6.3 模型构建 (Model Construction)

神经网络的兴起受益于卷积神经网络(CNN)和反向传播算法(BP)的实现。在 PyTorch 中,所有的模型构造几乎都是基于 nn.Module 类完成的,它提供了极高的灵活性。

1. 神经网络的构造基础

Module 类是 torch.nn 模块提供的模型构造基类。创建一个模型通常需要重载两个关键函数:

  • __init__:创建模型参数(如定义各种层)。

  • forward:定义前向计算逻辑(数据怎么流转)。

示例:多层感知机 (MLP)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import torch
from torch import nn

class MLP(nn.Module):
    def __init__(self, **kwargs):
        # 现代写法:直接使用 super(),Python 3 会自动处理继承链
        super().__init__(**kwargs) 
        
        # 定义全连接层:784 是输入维度(例如 28x28 像素展平后),256 是隐藏层神经元数
        self.hidden = nn.Linear(784, 256)    
        self.act = nn.ReLU()                # 激活函数层
        self.output = nn.Linear(256, 10)     # 输出层:对应 10 个类别(如 0-9 数字)
    
    def forward(self, x):
        """
        定义前向传播逻辑:数据 x 如何穿过网络
        """
        x = self.hidden(x)
        x = self.act(x)
        return self.output(x)

# 实例化与测试
net = MLP()
X = torch.rand(2, 784) # 模拟 2 个样本,每个样本 784 个特征
print(net(X).shape)    # 输出预期:torch.Size([2, 10])

关键点:你不需要定义 backward 函数。由于 PyTorch 的 Autograd 机制,系统会自动生成反向传播逻辑。

2. 自定义参数

在自定义层时,如果你只是定义一个普通的 torch.Tensor,模型在训练时是看不见它的。你必须告诉 PyTorch:“这是一个需要优化的权重。”

  • 什么是 nn.Parameter

    • 本质:它是 Tensor 的子类。

    • 自动注册:只要一个 Tensor 被定义为 Parameter,它就会被自动添加到模型的参数列表里。

    • 训练标记:这意味着当你调用 net.parameters() 时,PyTorch 能找到它;当你运行优化器时,它会被更新。

  • 为什么需要 ParameterListParameterDict

    如果你有很多参数,想用列表或字典存起来,不能直接用 Python 原生的 list 或 dict。

    • 普通 List/Dict:存放在里面的 Parameter 不会被模型识别,训练时梯度不会更新。

    • 专用容器:必须使用 nn.ParameterListnn.ParameterDict。它们像“带登记功能的抽屉”,确保放在里面的每一个参数都完成了“入职登记”。

代码示例:自定义含参层

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
import torch
from torch import nn

class MyCustomLayer(nn.Module):
    def __init__(self):
        super().__init__()
        
        # 1. 直接定义 Parameter:最常见的方式
        self.weight = nn.Parameter(torch.randn(4, 4))
        
        # 2. 使用 ParameterList:像列表一样存储多个参数
        # 适合层数不固定的场景
        self.list_params = nn.ParameterList([
            nn.Parameter(torch.randn(4, 4)) for _ in range(2)
        ])
        
        # 3. 使用 ParameterDict:通过键值对管理参数
        # 适合需要通过名称灵活调用参数的场景
        self.dict_params = nn.ParameterDict({
            'linear_weight': nn.Parameter(torch.randn(4, 4)),
            'bias': nn.Parameter(torch.randn(4))
        })

    def forward(self, x, choice='linear_weight'):
        # 使用 ParameterList 中的参数进行计算
        for p in self.list_params:
            x = torch.mm(x, p)
            
        # 根据键名灵活选择参数
        x = torch.mm(x, self.dict_params[choice])
        return x

# 验证:打印模型参数
net = MyCustomLayer()
for name, param in net.named_parameters():
    print(f"已识别参数: {name}") 

3. 神经网络中常见的层

深度学习的魅力在于各式各样的层(全连接、卷积、池化等)。在深度学习中,神经网络的每一层都各司其职,像一条精密工业流水线。在写代码之前,理解这些层的逻辑分工至关重要。

  • 宏观架构:输入、隐藏与输出

    无论神经网络多复杂,都可以划分为这三大主要区域:

    • 输入层 (Input Layer)

      • 定义:数据的入口。在代码中,它通常不是一个具体的类(如 nn.Linear),而是你传入 forward(self, x) 中的张量 x 的形状 (Shape)。

      • 作用:确定模型能接收多大的图片或多少个特征。

    • 隐藏层 (Hidden Layer)

      • 定义:模型中用于“学习”和“提取特征”的所有中间层。

      • 成员:卷积层、池化层以及中间的全连接层都属于隐藏层。

      • 作用:负责把原始像素转化为高级语义特征(例如从像素点识别出“猫耳朵”)。

    • 输出层 (Output Layer)

      • 定义:模型的最后一层。

      • 成员:通常是一个全连接层 (Linear),其输出神经元个数等于你的分类数量。

      • 作用:给出最终的预测结果(例如:这张图 90% 的概率是猫)。

  • 卷积层 (Convolutional Layer) —— “特征提取专家”

    • 逻辑归属:隐藏层。

    • 卷积层是视觉任务的核心。它通过一组可学习的“过滤网”(卷积核)在图像上滑动,提取局部特征。

    • 作用:提取局部特征(如边缘、纹理、形状)。

    • 核心参数

      • 卷积核 (Kernel):进行二维互相关运算的矩阵。

      • 填充 (Padding):在输入边缘补 0,常用于保持输出与输入形状一致。

      • 步幅 (Stride):滑动的快慢。步幅越大,输出维度减小越快。

  • 池化层 (Pooling Layer) —— “数据压缩专家”

    • 逻辑归属:隐藏层。

    • 池化层不包含任何可学习的参数,它的计算是固定的。

    • 作用:压缩特征图尺寸,减少计算量,并提高模型对平移的鲁棒性(即物体挪动一点也能识别出来)。

    • 常见类型

      • 最大池化 (MaxPool):取窗口内的最大值,保留最明显的特征。

      • 平均池化 (AvgPool):取窗口内的平均值。

  • 全连接层 (Fully Connected / Linear Layer) —— “分类决策专家”

    • 逻辑归属:隐藏层 或 输出层。

    • 全连接层中的每一个神经元都与前一层的所有神经元相连,进行全局的信息汇总。

    • 作用:将之前通过卷积和池化提取到的特征进行组合,映射到最终的分类空间。

    • 数学本质:一个仿射变换(Affine operation),公式为 $y = xW^T + b$。

    • 输出层应用:在模型的最后,全连接层的输出神经元个数通常等于类别数。

4. nn.Sequential

在 PyTorch 的神经网络构建中,如果说 nn.Module 是你的大地基,那么 nn.Sequential 就是一个预装好的“自动化流水线”或“逻辑胶囊”。

它是一个有序的容器,神经网络模块将按照在构造函数中传递的顺序依次被添加到计算图中,并按顺序执行。

  • 为什么需要 nn.Sequential

    在不使用它的情况下,你需要在 __init__ 里定义每一个层,然后在 forward 里手动写出数据流转的过程(如 x = self.layer1(x)x = self.layer2(x) 等)。使用 nn.Sequential 的核心优势在于:

    • 代码简洁:将功能相关的层打包在一起,大大缩短了 forward 函数的长度。

    • 逻辑清晰:可以将模型划分为明显的块(例如:特征提取块、分类块),让结构一目了然。

    • 自动前向传播:你只需要把输入丢给这个容器,它会自动按照你定义的顺序把数据传给内部的每一个层。

  • 现代代码示例:AlexNet 中的应用

    参考你提供的 AlexNet 实现,我们可以看到它使用了两个 nn.Sequential 容器,分别管理“卷积特征提取”和“全连接分类”:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
import torch
from torch import nn

class ModernAlexNet(nn.Module):
    def __init__(self):
        super().__init__() # 现代 Python 3 写法
        
        # 1. 卷积特征提取层(Hidden Layers - Features)
        self.conv_pipeline = nn.Sequential(
            # 输入 1 通道,输出 96 通道,卷积核 11x11,步幅 4
            nn.Conv2d(1, 96, 11, 4),
            nn.ReLU(),              
            nn.MaxPool2d(3, 2),     
            
            # 更多的卷积层串联
            nn.Conv2d(96, 256, 5, 1, 2),
            nn.ReLU(),                  
            nn.MaxPool2d(3, 2),         
            
            nn.Conv2d(256, 384, 3, 1, 1),
            nn.ReLU(),                   
            nn.Conv2d(384, 256, 3, 1, 1),
            nn.ReLU(),                   
            nn.MaxPool2d(3, 2)           
        )
        
        # 2. 全连接分类层(Hidden & Output Layers - Classifier)
        self.fc_pipeline = nn.Sequential(
            nn.Linear(256 * 5 * 5, 4096),
            nn.ReLU(),                   
            nn.Dropout(0.5),              # 缓解过拟合
            nn.Linear(4096, 4096),       
            nn.ReLU(),                   
            nn.Dropout(0.5),             
            nn.Linear(4096, 10)           # 最终输出层
        )

    def forward(self, x):
        # 数据直接流过第一条流水线
        x = self.conv_pipeline(x)
        
        # 展平:将多维卷积输出转为一维向量进入全连接层
        x = x.view(x.size(0), -1)
        
        # 数据直接流过第二条流水线
        return self.fc_pipeline(x)
  • 注意事项与限制

    虽然 nn.Sequential 非常好用,但它也不是万能的:

    • 单一输入输出:它只支持数据按照直线运行(单一输入 $\rightarrow$ 单一输出)。 如果你的模型需要复杂的“跳跃连接”(如 ResNet 的残差结构)或多输入多输出,则不能单纯依赖 Sequential,必须在 forward 中手动控制流转。

    • 调试粒度:因为层被打包了,如果你想在中间某一层停下来查看张量的形状,Sequential 会显得不够灵活(虽然可以通过 register_forward_hook 解决,但操作较复杂)。

5. 经典模型示例

  • 核心模型 A:LeNet

    LeNet 是一个简单的前馈神经网络,经典的“卷积+池化+全连接”结构。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
class Net(nn.Module):
    def __init__(self):
        super(Net, self).__init__()
        self.conv1 = nn.Conv2d(1, 6, 5)  # 1个输入通道,6个输出通道,5x5卷积核
        self.conv2 = nn.Conv2d(6, 16, 5)
        self.fc1 = nn.Linear(16 * 5 * 5, 120)
        self.fc2 = nn.Linear(120, 84)
        self.fc3 = nn.Linear(84, 10)

    def forward(self, x):
        x = F.max_pool2d(F.relu(self.conv1(x)), (2, 2))
        x = F.max_pool2d(F.relu(self.conv2(x)), 2)
        x = x.view(-1, 400) # 展平操作
        x = F.relu(self.fc1(x))
        x = F.relu(self.fc2(x))
        return self.fc3(x)
  • 核心模型 B:AlexNet

    相比 LeNet,AlexNet 使用了更深的网络、更小的卷积核、以及 nn.Sequential 容器,并引入了 Dropout 来缓解过拟合。

1
2
3
4
5
6
self.conv = nn.Sequential(
    nn.Conv2d(1, 96, 11, 4), # 步幅为4
    nn.ReLU(),
    nn.MaxPool2d(3, 2),
    # ... 更多卷积层
)

💡 避坑与进阶 Tips

  • 批量处理限制torch.nn 只支持小批量 (Mini-batches) 输入。如果只有一个样本,请使用 input.unsqueeze(0) 增加一个“假”的批大小维度。

  • 梯度清零:在进行反向传播前,务必调用 net.zero_grad() 清空之前的梯度缓存。

  • 权重更新公式:$weight = weight - learning_rate \times gradient$

  • Module vs Layer:在 PyTorch 中,Module 既可以是一个单独的层,也可以是一个庞大的模型,甚至可以是模型的一个子部件。

6.4 模型初始化 (Model Initialization)

在神经网络训练中,权重的初始值直接决定了模型的收敛速度和最终精度。

1. 为什么不能全初始化为 0?

虽然 nn.Linear 等层默认会有随机初始化,但我们通常会手动干预。

  • 规避对称性:如果权重全为 0,网络中每个神经元的表现将完全一致,导致梯度消失或模型无法学习复杂的特征。

  • 加速收敛:合理的初始化能让损失函数从一个更靠近“底谷”的地方开始下降。

2. PyTorch 的初始化工具箱:torch.nn.init

PyTorch 在 torch.nn.init 模块中提供了丰富的原地操作(In-place)函数。

注意:函数名后缀带有下划线 _ 的,表示会直接修改传入的张量。

常见的初始化方法:

  • 基础型uniform_ (均匀分布)、normal_ (正态分布)、constant_ (常数)。

  • 极简型ones_ (全1)、zeros_ (全0)。

  • 进阶型(最常用)

    • Xavier 初始化:适用于 Sigmoid 或 Tanh 激活函数,保持输入输出的方差一致。

    • Kaiming (He) 初始化:专为 ReLU 及其变体设计,是目前视觉模型的主流选择。

增益值 (Gain) 参考表:

不同的激活函数对梯度的影响不同,我们需要通过增益值进行修正:

激活函数增益值 (Gain)
Linear / Identity$1$
Sigmoid$1$
Tanh$5/3$
ReLU$\sqrt{2}$
Leaky ReLU$\sqrt{\frac{2}{1 + \text{negative_slope}^2}}$

3. 实战:如何优雅地初始化模型

在实际开发中,我们不会给每一个层手动写初始化代码,而是通过 isinstance() 判断层的类型,并使用 model.apply() 进行一键处理。

初始化函数封装:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
import torch
from torch import nn

def initialize_weights(m):
    """
    m: 传入的模块(层)
    """
    # 1. 如果是卷积层:使用 Kaiming 初始化
    if isinstance(m, nn.Conv2d):
        nn.init.kaiming_normal_(m.weight.data, nonlinearity='relu')
        if m.bias is not None:
            nn.init.constant_(m.bias.data, 0)
            
    # 2. 如果是全连接层:使用正态分布初始化
    elif isinstance(m, nn.Linear):
        nn.init.normal_(m.weight.data, mean=0, std=0.01)
        if m.bias is not None:
            nn.init.zeros_(m.bias.data)
            
    # 3. 如果是批归一化层:权重设为 1,偏置设为 0
    elif isinstance(m, nn.BatchNorm2d):
        m.weight.data.fill_(1)
        m.bias.data.zero_()

# --- 使用方法 ---
class MyMLP(nn.Module):
    def __init__(self):
        super().__init__()
        self.conv = nn.Conv2d(1, 16, 3)
        self.fc = nn.Linear(16*26*26, 10)
        
    def forward(self, x):
        return self.fc(self.conv(x).view(x.size(0), -1))

model = MyMLP()

# 核心操作:调用 apply 遍历模型所有子模块并应用初始化函数
model.apply(initialize_weights) 

在 PyTorch 中,apply 函数是 nn.Module 提供的一个非常强大的递归工具。如果把你的神经网络比作一棵大树,apply 就像是一个高效的“巡检员”,它会顺着树干(主模型)一直走到每一片叶子(子模块/层)上执行你指定的动作。

  • 核心定义apply(fn) 函数的作用是将函数 fn 递归地应用到模型的所有子模块(Submodules)上。这里的子模块包括模型本身、定义的各个层、以及 nn.Sequential 里的每一个部件。

  • 为什么要用 apply

    在处理复杂模型(如 AlexNet 或 ResNet)时,模型内部嵌套了很多层。如果你想给每一层做初始化,手动去写 self.conv1, self.fc2 会非常繁琐且容易遗漏。

    • 一键操作:通过 model.apply(fn),你只需要写一个针对单层的处理函数,它会自动帮你跑遍全场。

    • 解耦逻辑:你可以将“构建模型”的代码和“初始化模型”的代码分开,让程序结构更清晰。

💡 关键 Tips

  • 原地修改m.weight.data 配合带下划线的函数(如 normal_)是修改参数的正确姿势。

  • 不要“贪零”:除非是偏置(Bias),否则权重尽量不要初始化为 0。使用一个小正数(如 0.01)或 Kaiming 初始化效果会好得多。

  • 现代框架的默认值:PyTorch 的内置层其实自带了不错的默认初始化(通常是 LeCun 或 Xavier 的变体),但在复现特定论文或遇到训练不收敛时,手动初始化是你的第一件武器。

深度思考:初始化其实是在帮模型“打破僵局”。既然你已经给模型设置好了完美的起跑姿势,接下来我们要不要看看损失函数,学习如何衡量模型在跑道上到底有没有跑偏?

6.5 损失函数 (Loss Function)

损失函数是模型的“负反馈”来源,它衡量预测值与真实标签之间的差距。

1. 分类任务 (Classification)

这类函数用于衡量概率分布之间的差异。

  • BCELoss (二分类交叉熵)

    计算二分类任务的交叉熵,要求输入必须是概率形式(通常经过 Sigmoid)。

    • 关键参数weight (类别权重)、reduction (计算模式)。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
import torch
from torch import nn

# 模拟:3个样本的预测概率和真实标签
m = nn.Sigmoid()
loss_fn = nn.BCELoss(reduction='mean') # 默认取平均

input = torch.randn(3, requires_grad=True)
target = torch.empty(3).random_(2) # 随机生成 0 或 1

output = loss_fn(m(input), target)
output.backward()

print(f'BCELoss 计算结果: {output.item():.4f}')
  • CrossEntropyLoss (多分类交叉熵)

    最常用的分类损失,它在内部整合了 LogSoftmax 和 NLLLoss。

    • 关键参数ignore_index (忽略某个类别的计算)、weight
1
2
3
4
5
6
7
# 模拟:3个样本,5个类别的预测得分
loss_fn = nn.CrossEntropyLoss()
input = torch.randn(3, 5, requires_grad=True)
target = torch.empty(3, dtype=torch.long).random_(5) # 类别索引

output = loss_fn(input, target)
print(f'CrossEntropyLoss 计算结果: {output.item():.4f}')

2. 回归任务 (Regression)

用于预测具体的连续数值。

  • L1 & MSE & SmoothL1 (距离度量)

    • L1Loss:计算绝对值差。

    • MSELoss:计算平方差。

    • SmoothL1Loss:误差小时用平方,大时用绝对值,减轻离群点影响。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
input = torch.randn(3, 5, requires_grad=True)
target = torch.randn(3, 5)

# L1 损失
l1_fn = nn.L1Loss()
# MSE 损失
mse_fn = nn.MSELoss()
# SmoothL1 损失,beta 控制平滑阈值
smooth_fn = nn.SmoothL1Loss(beta=1.0) 

print(f'L1: {l1_fn(input, target).item():.4f}')
print(f'MSE: {mse_fn(input, target).item():.4f}')
print(f'SmoothL1: {smooth_fn(input, target).item():.4f}')

3. 相似度与特殊任务

  • KLDivLoss (KL 散度)

    计算相对熵,用于衡量两个概率分布的接近程度。

    • 关键参数reduction='batchmean' (在 Batch 维度求平均)。
1
2
3
4
5
6
7
loss_fn = nn.KLDivLoss(reduction='batchmean')
# 输入通常需要 log_softmax 形式
inputs = torch.tensor([[0.5, 0.3, 0.2], [0.2, 0.3, 0.5]]).log()
target = torch.tensor([[0.9, 0.05, 0.05], [0.1, 0.7, 0.2]])

output = loss_fn(inputs, target)
print(f'KLDivLoss 计算结果: {output.item():.4f}')
  • TripletMarginLoss (三元组损失)

    用于拉近正样本距离,推开负样本距离。

    • 关键参数margin (边界距离值)、p (范数阶数)。
1
2
3
4
5
6
7
8
triplet_loss = nn.TripletMarginLoss(margin=1.0, p=2)
# 三元组:锚点、正例、负例
anchor = torch.randn(100, 128, requires_grad=True)
positive = torch.randn(100, 128, requires_grad=True)
negative = torch.randn(100, 128, requires_grad=True)

output = triplet_loss(anchor, positive, negative)
print(f'TripletLoss 计算结果: {output.item():.4f}')
  • CTCLoss (连接时序分类)

    用于解决如语音识别、OCR 等时序数据对齐问题。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
ctc_loss = nn.CTCLoss(blank=0) 
# T:序列长度, N:批大小, C:类别数
T, N, C = 50, 16, 20
input = torch.randn(T, N, C).log_softmax(2).requires_grad_()
target = torch.randint(1, C, (N, 30), dtype=torch.long)

input_lengths = torch.full((N,), T, dtype=torch.long)
target_lengths = torch.randint(10, 30, (N,), dtype=torch.long)

loss = ctc_loss(input, target, input_lengths, target_lengths)
print(f'CTCLoss 计算结果: {loss.item():.4f}')

💡 核心参数 reduction 总结

所有的损失函数几乎都有这个参数,它决定了返回值的形式:

  • 'mean' (默认):计算 Batch 的平均损失,返回标量。

  • 'sum':计算 Batch 的总损失,返回标量。

  • 'none':不合并,返回每个样本独立的 Loss,形状与输入一致。

6.6 训练和评估 (Training and Evaluation)

在 PyTorch 中,训练和评估的逻辑框架非常相似,核心区别在于是否更新参数以及是否记录梯度。

1. 核心开关:模型状态切换

模型中有一些特殊的层(如 Dropout 和 BatchNorm)在训练和测试时的表现是完全不同的。因此,在开始逻辑前必须先设置状态:

  • model.train():训练模式。开启 Dropout 随机失活,开启 BatchNorm 的均值方差统计更新。

  • model.eval():评估/测试模式。关闭 Dropout,固定 BatchNorm。

2. 标准流程对比

训练和评估就像是“开卷考试”和“闭卷考试”的区别:

步骤训练阶段 (Training)验证/测试阶段 (Validation)
模式设置model.train()model.eval()
梯度计算必须计算关闭 (with torch.no_grad():)
梯度清零optimizer.zero_grad()不需要
前向传播output = model(data)output = model(data)
计算损失loss = criterion(output, label)loss = criterion(output, label)
反向传播loss.backward()跳过
更新权重optimizer.step()跳过

3. 训练与验证代码

A. 训练逻辑封装

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
def train_one_epoch(model, loader, optimizer, criterion, device):
    model.train() # 切换到训练模式
    running_loss = 0.0
    
    for data, label in loader:
        # 1. 移动数据到指定设备(GPU或CPU)
        data, label = data.to(device), label.to(device)
        
        # 2. 梯度清零:防止旧梯度累加影响当前步
        optimizer.zero_grad()
        
        # 3. 前向传播
        output = model(data)
        
        # 4. 计算损失
        loss = criterion(output, label)
        
        # 5. 反向传播:计算每个参数的梯度
        loss.backward()
        
        # 6. 更新权重:根据梯度调整螺丝钉
        optimizer.step()
        
        # 统计损失 (loss.item() 提取标量值,防止显存爆炸)
        running_loss += loss.item() * data.size(0)
        
    return running_loss / len(loader.dataset)

B. 验证逻辑封装

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
def validate(model, loader, criterion, device):
    model.eval() # 切换到评估模式
    val_loss = 0.0
    correct = 0
    
    # 关键:强制关闭梯度计算,节省大量内存和计算资源
    with torch.no_grad():
        for data, label in loader:
            data, label = data.to(device), label.to(device)
            
            output = model(data)
            loss = criterion(output, label)
            
            val_loss += loss.item() * data.size(0)
            
            # 计算准确率:获取概率最大的类别索引
            preds = output.argmax(dim=1)
            correct += (preds == label).sum().item()
            
    avg_loss = val_loss / len(loader.dataset)
    accuracy = correct / len(loader.dataset)
    return avg_loss, accuracy

4. 模型效果的“体检报告”:Metrics

除了简单的 Loss 和 Accuracy,我们通常使用 sklearn 来生成更详细的报告(精确率、召回率、F1值)。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
from sklearn.metrics import classification_report

def get_full_report(model, loader, device, class_names):
    model.eval()
    all_preds = []
    all_labels = []
    
    with torch.no_grad():
        for data, label in loader:
            data = data.to(device)
            output = model(data)
            preds = output.argmax(dim=1)
            
            # 收集结果,注意要转回 CPU 并转为 numpy
            all_preds.extend(preds.cpu().numpy())
            all_labels.extend(label.numpy())
            
    # 生成分类报告
    print(classification_report(all_labels, all_preds, target_names=class_names))

💡 避坑与进阶 Tips

  • loss.item() 的妙用:在累加 Loss 时,一定要用 .item() 提取数值。如果直接累加 loss 张量,由于它带着计算图,会导致显存逐渐堆积直至 OOM(内存溢出)。

  • .to(device) vs .cuda():推荐使用 .to(device)。你可以在代码开头写一句 device = torch.device("cuda" if torch.cuda.is_available() else "cpu"),这样代码在有无 GPU 的机器上都能跑。

  • torch.no_grad():在验证环节千万别漏了它!它不仅能提速,还能防止验证集的数据“污染”了模型的梯度,保证评估的客观性。

6.7 可视化 (Visualization)

在 PyTorch 的世界里,可视化工具主要分为基础绘图、实时监控和云端实验管理三大类。

6.7.1 常用可视化工具介绍

工具名称类型适用场景核心特点
Matplotlib基础绘图库静态图表、实验总结Python 绘图的鼻祖,最通用,适合把训练好的结果画成论文插图。
TensorBoard实时监控监控训练过程原本是 TensorFlow 的工具,现在已成为 PyTorch 的官方标配。适合看实时 Loss 曲线。
Weights & Biases (W&B)云端管理团队协作、大规模实验现代化的“炼丹记录仪”。自动同步实验数据到云端,支持多人协作。
Visdom实时监控轻量级监控Facebook 团队开发的工具,支持多种数据窗口展示,适合较老或轻量级的项目。

6.7.2 深度解析:必备双剑

1. Matplotlib:你的“多功能画板”

它是你笔记里最常见的工具。无论是在 Jupyter Notebook 里看一张图片,还是对比两个损失函数的数学曲线,它都是首选。

  • 优点:不需要额外开启服务器,随写随画。

  • 局限性:难以处理实时动态数据,如果训练跑了好几天,你很难用它一直盯着曲线。Matplotlib 最适合在训练结束后,对收集到的数据进行统一汇总和对比。

代码示例:对比训练与验证 Loss

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import matplotlib.pyplot as plt

# 1. 模拟训练过程中记录的数据
epochs = range(1, 6)
train_losses = [0.9, 0.7, 0.5, 0.3, 0.2]
val_losses = [0.95, 0.8, 0.65, 0.5, 0.45]

# 2. 创建画布
plt.figure(figsize=(8, 5))

# 3. 绘制曲线
plt.plot(epochs, train_losses, label='Train Loss', marker='o') # marker 添加点标记
plt.plot(epochs, val_losses, label='Val Loss', marker='s')

# 4. 装饰图表
plt.title('Training and Validation Loss Over Epochs')
plt.xlabel('Epoch')
plt.ylabel('Loss')
plt.legend()  # 显示右上角的图例
plt.grid(True) # 显示网格线,方便对齐数值

# 5. 显示图像
plt.show()
  • 关键说明

    • 适用性:适合在本地环境或 Jupyter Notebook 中快速查看结果。

    • 操作模式:它是“非交互式”的。如果你想更新图像,必须重新运行代码生成新的图表。

2. TensorBoard:你的“实时监视器”

在 PyTorch 中,通过 from torch.utils.tensorboard import SummaryWriter 即可调用。

  • 核心逻辑

    • 在代码中创建一个 Writer。

    • 每训练一步,用 add_scalar 把 Loss 或 Accuracy 写进日志文件。

    • 在终端输入 tensorboard --logdir=logs,打开浏览器就能看到跳动的曲线。

  • 优点:可以一边训一边看,还能查看网络结构图、直方图。TensorBoard 的精髓在于它能一边训练一边写日志,你只需要刷新浏览器就能看到最新的曲线。

代码示例:在训练循环中集成 TensorBoard

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
import torch
from torch.utils.tensorboard import SummaryWriter

# 1. 创建写入器,指定日志保存的目录
writer = SummaryWriter('logs/fit_experiment_1')

# 模拟一个简单的训练循环
for epoch in range(100):
    # 模拟计算出的 Loss 和 Accuracy
    loss = 1.0 / (epoch + 1)
    acc = 1 - (1.0 / (epoch + 1))
    
    # 2. 将数据写入日志文件
    # 参数含义:(标签名, 数值, 当前步数/Epoch)
    writer.add_scalar('Loss/train', loss, epoch)
    writer.add_scalar('Accuracy/train', acc, epoch)
    
    # 甚至可以把模型结构图存进去(假设 model 已定义)
    # writer.add_graph(model, input_to_model)

# 3. 训练结束,关闭写入器
writer.close()
  • 如何查看

    1. 在你的项目目录下打开终端(Terminal)。

    2. 输入命令:tensorboard --logdir=logs

    3. 点击终端给出的链接(通常是 http://localhost:6006/),在浏览器中查看动态曲线。

  • 关键说明

    • 层级管理:在标签名中使用斜杠(如 Loss/train),TensorBoard 会自动帮你把同类指标归纳到同一个面板下。

    • 实时性:即便训练还在跑,你也可以随时打开网页看曲线的变化趋势。

6.8 PyTorch 优化器 (PyTorch Optimizer)

深度学习模型的训练本质上是一个寻找最优解的过程。面对拥有数千万参数的复杂模型(如 ResNet-50),我们无法暴力穷举,而是依赖反向传播(BP)与优化器(Optimizer)来逐步逼近最优参数。

优化器根据网络反向传播得到的梯度信息来更新参数,其核心目标是降低损失函数(Loss)的值,使模型输出不断接近真实标签。

6.8.1 优化器的基石:torch.optim

PyTorch 的 torch.optim 库提供了多种现成的优化算法(如 SGD, Adam, RMSprop, AdamW 等),它们全都继承自基类 Optimizer

优化器的三大核心属性

  • defaults:存储优化器的默认超参数(如 lr 学习率、momentum 动量等)。

  • state:存储参数的缓存信息(如动量缓冲区 momentum_buffer),用于记录训练过程中的状态。

  • param_groups:管理参数组的列表。每一组都是一个字典,可以为不同的层设置不同的学习率或超参数。

6.8.2 优化器的五大核心方法

方法作用备注
zero_grad()清空所管理参数的梯度PyTorch 梯度会累加,每次更新前必须手动清零。
step()执行一步梯度更新根据当前梯度和算法逻辑修改参数数值。
add_param_group()动态添加参数组可用于微调模型或给新层设置特定优化策略。
state_dict()获取当前状态字典包含参数和超参数,用于模型保存。
load_state_dict()加载状态字典用于断点续训,恢复上次训练的完整状态。

6.8.3 现代实战:标准训练逻辑与技巧

1. 基础训练循环

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
import torch
from torch import optim

# 1. 实例化模型与优化器
model = MyNet()
optimizer = optim.Adam(model.parameters(), lr=1e-3)

for epoch in range(EPOCHS):
    for data, label in train_loader:
        # 梯度清零
        optimizer.zero_grad()
        
        # 前向传播与计算 Loss
        output = model(data)
        loss = criterion(output, label)
        
        # 反向传播计算梯度
        loss.backward()
        
        # 梯度更新
        optimizer.step()

2. 进阶技巧:差异化学习率

你可以为不同的网络层配置不同的优化强度,这在迁移学习中非常有用:

1
2
3
4
5
# 给 fc 层设置默认学习率,给 layer4 的卷积层设置更高的学习率
optimizer = optim.SGD([
    {'params': net.fc.parameters()}, 
    {'params': net.layer4[0].conv1.parameters(), 'lr': 1e-2}
], lr=1e-5)

6.8.4 优化器参数说明

1. 通用参数(几乎所有优化器都有)

在深入具体的优化器之前,这两个参数是你的“必修课”:

  • lr (Learning Rate, 学习率):最重要的参数。决定了每一步走的距离。

    • 太大:模型会在最优解附近反复横跳,甚至直接“起飞”(梯度爆炸)。

    • 太小:模型走得太慢,可能还没走到终点训练就结束了。

  • weight_decay (权重衰减):正则化技术(通常对应 L2 正则)。

    • 作用:像给模型戴上“紧箍咒”,防止权重数值变得过大,从而有效缓解过拟合。

2. SGD (随机梯度下降)

SGD 是最经典的优化器,它的参数非常像物理世界的运动。

  • momentum (动量):模拟物体的惯性。原理:它会积累之前的运动方向。如果之前一直在下坡,它会越滚越快,帮助模型冲出“平坦区域”或“局部最小值”。通常设为 0.9。

  • nesterov (牛顿动量):布尔值。原理:开启后,它会先根据惯性往前“看一眼”,然后再计算梯度。这让它在弯道处更加灵敏,不容易冲出赛道。

3. Adam (自适应矩估计)

Adam 是目前的“万金油”,它的参数主要用于控制如何自动调整步长。

  • betas (平滑常数):通常是一个元组 (0.9, 0.999)。

    • $\beta_1$ (0.9):控制一阶矩(类似动量),决定了梯度的平滑程度。

    • $\beta_2$ (0.999):控制二阶矩(梯度的平方),决定了它是如何针对每个参数调整步长的。通常不需要改动。

  • eps (Epsilon):一个极小的数(默认 1e-8)。作用:防止在数学运算中除以 0。除非你遇到了数值不稳定的问题,否则永远别动它。

4. AdamW (Adam 的改进版)

  • 核心区别:它是目前在 Transformer 和 现代视觉模型 中最推荐的优化器。它修改了 weight_decay 的作用方式,让权重衰减真正独立于梯度更新,能获得更好的泛化效果。

5. RMSprop (均方根传播)

常用于循环神经网络(RNN)。

  • alpha:平滑常数(默认 0.99)。类似于 Adam 中的 $\beta_2$,决定了对近期梯度的“记忆深度”。

核心对比表

优化器建议学习率 (lr)必调参数形象比喻
SGD1e-1 或 1e-2momentum像一个滚下山的铁球,靠惯性冲过小坑。
Adam1e-3 或 3e-4lr像一个带导航的赛车,自动在弯道减速、直道加速。
AdamW1e-3weight_decayAdam 的进化版,不仅跑得快,还不容犯错。

💡 怎么调最有效?

  • 先用 Adam:如果你是新手,或者在测试一个新模型,先用 optim.Adam(lr=1e-3)。它通常能给你一个不错的结果。

  • 后期切 SGD:如果你在参加竞赛或追求极致的精度,可以在训练后期换成带 momentum=0.9 的 SGD,配合细致的学习率调整,往往能磨出更高的分数。

  • 大模型必选 AdamW:如果你在玩 ViT、ResNet 变体或 Transformer,请直接使用 AdamW

6.8.5 如何选择优化器?

优化器的选择会显著影响模型的收敛速度和最终效果。

  • SGD (随机梯度下降):虽然收敛可能较慢,但通常能找到更优的泛化解,适合调优精细的模型。

  • Adam:结合了动量和自适应学习率,收敛极快,是绝大多数任务的默认首选。

  • AdamW:修复了 Adam 在权重衰减(Weight Decay)上的问题,在计算机视觉任务中表现优异。

核心心得

优化器的性能高度依赖于模型和数据。没有绝对最好的优化器,只有最合适的组合。通常建议从 Adam 开始快速验证,后期再考虑使用带动量的 SGD 进行极限精度的打磨。

Licensed under CC BY-NC-SA 4.0