在这里插入图片描述


一、PyTorch 到底是什么(真正讲清楚)

小白版:

想象你要搭乐高。你可以自己烧塑料、开模具、做积木——这相当于用 C++ 从零写神经网络。你也可以买现成的乐高套装,里面有标准积木、说明书,你只需要按图纸拼——PyTorch 就是这套"深度学习乐高套装"。它把"矩阵运算"“自动算梯度”"模型组件"这些基础零件都给你准备好了,你只要像写普通 Python 代码一样把它们拼起来就行。你不用操心积木是怎么造出来的,只管搭你的城堡。

专业版:

PyTorch 是一个深度学习框架。但这句话等于没说,因为 TensorFlow、JAX、飞桨都是深度学习框架。

更准确的定义是:PyTorch 是一个由 Meta(Facebook)开源的、面向 Python 的张量计算库,它的核心设计原则是"让写深度学习代码就像写普通 Python 代码一样自然"。

拆开来看,PyTorch 做了三件事:

能力 说明 类比
张量计算 替代 NumPy,且能跑在 GPU 上 NumPy 的 GPU 版本
自动求导 你只写前向计算,反向梯度自动算 计算器按等号
神经网络模块 提供现成的层、损失函数、优化器 搭积木的零件包

举个直白的例子:你用 NumPy 写了一个线性回归,想让它跑在 GPU 上、自动算梯度、用 Adam 优化——这需要你自己手写几百行反向传播代码。而用 PyTorch,核心代码就几行:

import torch
import torch.nn as nn
import torch.optim as optim

# 定义一个最简单的模型
model = nn.Linear(10, 1)

# 损失函数 + 优化器
loss_fn = nn.MSELoss()
optimizer = optim.Adam(model.parameters(), lr=0.001)

# 一个训练步
x = torch.randn(32, 10)        # 32 个样本,每个 10 维
y = torch.randn(32, 1)         # 标签

pred = model(x)                 # 前向传播
loss = loss_fn(pred, y)        # 计算损失
loss.backward()                 # 反向传播 —— 就这一行!
optimizer.step()                # 更新参数
optimizer.zero_grad()           # 清空梯度

这就是 PyTorch 的核心价值:把深度学习中重复的、底层的、易错的部分抽象掉,让你专注于模型设计本身。


二、为什么 AI 圈几乎都在用 PyTorch

小白版:

这就像当年智能手机大战:诺基亚(TensorFlow 1.x)功能强大但操作反人类,iPhone(PyTorch)一出来大家发现"原来写代码可以这么爽"。早期 TensorFlow 要求你先画一张"全局施工图"再施工,中途你想看看盖到哪了都不行;而 PyTorch 是你一边写代码一边就能看到结果,出错了马上能定位。这种"所见即所得"的体验让学术圈迅速倒戈,而工业界招聘时发现候选人都会 PyTorch,公司也就跟着转了。等 Google 回过神把 TensorFlow 改成类似 PyTorch 的风格(2.0),地盘已经丢了大半。

专业版:

根据 Papers with Code 的统计,2024 年 超过 80% 的顶会论文使用 PyTorch 实现。这不是偶然的。

历史原因:TensorFlow 1.x 的"作死"

TensorFlow 1.x 用的是静态图(后面会细讲)。你需要先定义整个计算图,然后在一个 Session 里运行。调试时你甚至不能直接 print 中间变量的值——你必须用 tf.Print

这是 TensorFlow 1.x 写一个简单的全连接网络:

# TensorFlow 1.x 风格
import tensorflow as tf

x = tf.placeholder(tf.float32, [None, 784])
W = tf.Variable(tf.zeros([784, 10]))
b = tf.Variable(tf.zeros([10]))
y = tf.nn.softmax(tf.matmul(x, W) + b)

with tf.Session() as sess:
    sess.run(tf.global_variables_initializer())
    result = sess.run(y, feed_dict={x: data})

而 PyTorch 的做法:

# PyTorch 风格 —— 和普通 Python 一模一样
x = torch.randn(100, 784)
W = torch.randn(784, 10)
b = torch.randn(10)
y = torch.softmax(x @ W + b, dim=1)

学术界的选择

2017 年 PyTorch 发布后,研究人员发现用它做实验比 TensorFlow 快太多了。调试能直接用 print、能用 pdb 断点、能写任意 Python 控制流——这些在 TensorFlow 1.x 里都是奢望。于是学术界迅速倒向 PyTorch。

而工业界跟着学术界走:招聘时候选人大多熟悉 PyTorch,公司也就顺势切换了。

Google 的反应

Google 在 TensorFlow 2.0 中完全转向了 Eager Execution(动态图),API 设计也大量参考 PyTorch,但为时已晚,心智已经流失。

关键时间线

时间 事件
2015.11 TensorFlow 1.0 发布
2016.10 PyTorch 0.1 发布
2018.12 PyTorch 1.0 发布(JIT 支持)
2019.10 TensorFlow 2.0 发布(全面转向动态图)
2020-2023 PyTorch 市占率超过 80%
2023 PyTorch 2.0 发布(torch.compile)

三、Tensor(张量)彻底讲透

小白版:

"张量"这个名字听起来吓人,其实它就是多维数组——Excel 表格的加强版。一个数(标量)是 0 维张量,就是一格;一行数字(向量)是 1 维张量,就是 Excel 的一行;一个表格(矩阵)是 2 维张量;如果再叠多层表格,就是 3 维、4 维的张量。比如一张彩色图片,可以看成 3 张表格叠在一起(红、绿、蓝),那就是 3 维张量。PyTorch 的 Tensor 本质和 NumPy 的多维数组几乎一样,唯一的区别是:Tensor 能自动计算梯度(回想一下高中数学的求导),而且能搬到显卡(GPU)上跑,速度快几十倍。

专业版:

3.1 张量就是多维数组

不要被"张量"这个词吓到。它就是个多维数组,和多维的 NumPy ndarray 本质一样:

标量(0 维): torch.tensor(3.14)          # 形状 []
向量(1 维): torch.tensor([1, 2, 3])     # 形状 [3]
矩阵(2 维): torch.randn(3, 4)            # 形状 [3, 4]
三维张量:    torch.randn(2, 3, 4)         # 形状 [2, 3, 4]  (比如 2 张 3×4 的灰度图)
四维张量:    torch.randn(16, 3, 224, 224)  # [batch, channel, height, width]
维度 实际含义 常见场景
0 标量 单个数值,如 loss 值
1 向量 一个样本的特征向量
2 矩阵 一批特征、一张灰度图
3 3D 张量 一张 RGB 图像(C×H×W)、NLP 中一个 batch 的序列
4 4D 张量 一个 batch 的 RGB 图像(N×C×H×W)
5 5D 张量 视频数据(N×C×T×H×W)

3.2 PyTorch Tensor vs NumPy ndarray

相同点:

  • 切片、索引、广播机制几乎一样
  • 大量同名函数:reshapesummeanargmax

关键区别:

PyTorch Tensor NumPy ndarray
GPU 支持 原生支持 .cuda() 不支持
自动求导 requires_grad=True 即可 不支持
内存布局 行优先(C-contiguous) 行优先
底层存储 共享存储视图 共享存储视图

从 NumPy 迁移的最快方式:

import numpy as np
import torch

# NumPy → Tensor
np_arr = np.array([1, 2, 3])
tensor = torch.from_numpy(np_arr)

# Tensor → NumPy(注意:必须在 CPU 上)
tensor_cpu = tensor.cpu()
np_arr_back = tensor_cpu.numpy()

# 直接创建 Tensor(推荐写法)
x = torch.tensor([1, 2, 3])          # 从数据创建
x = torch.zeros(3, 4)                 # 全零
x = torch.ones(3, 4)                  # 全一
x = torch.randn(3, 4)                 # 正态分布随机
x = torch.arange(0, 10, 2)           # 类似 range

3.3 三个最容易被忽视的关键属性

x = torch.randn(2, 3, 4)

# 1. shape / size —— 形状
print(x.shape)       # torch.Size([2, 3, 4])
print(x.size(0))     # 2

# 2. dtype —— 数据类型
print(x.dtype)       # torch.float32
x_int = x.long()     # 转为 int64
x_half = x.half()    # 转为 float16(省显存但精度低)

# 3. device —— 在哪个设备上
print(x.device)      # cpu
x_gpu = x.cuda()     # 移到 GPU
x_cpu = x_gpu.cpu()  # 移回 CPU

3.4 常见误区:view vs reshape vs permute

误区:以为它们可以互换。

x = torch.randn(2, 3, 4)   # shape: [2, 3, 4]

# view:要求内存连续,否则报错;返回视图(共享数据)
x_view = x.view(2, 12)              # [2, 12],OK
# x.transpose(0,1).view(2,12)       # RuntimeError! 不连续内存

# reshape:不要求连续,会自动拷贝;更安全但可能额外开销
x_reshaped = x.reshape(2, 12)       # 总是 OK

# permute:交换维度顺序,不改变数据
x_permuted = x.permute(2, 0, 1)     # [4, 2, 3]

建议: 开发阶段用 reshape,性能优化阶段再考虑 viewcontiguous

3.5 广播机制(Broadcasting)

和 NumPy 规则一致:从最后一维开始比对,维度为 1 或缺失的维度会自动扩展。

a = torch.randn(3, 1)    # [3, 1]
b = torch.randn(1, 4)    # [1, 4]
c = a + b                 # [3, 4]  自动广播

实际工作中的典型场景:

# 给一个 batch 的图片做归一化
images = torch.randn(16, 3, 224, 224)        # [B, C, H, W]
mean = torch.tensor([0.485, 0.456, 0.406])   # [3]
std  = torch.tensor([0.229, 0.224, 0.225])   # [3]

# mean 和 std 会自动广播: [3] → [1, 3, 1, 1] → 匹配 [16, 3, 224, 224]
images_normalized = (images - mean[:, None, None]) / std[:, None, None]

四、自动求导(Autograd)真正原理

小白版:

训练神经网络就是不断微调参数让模型更准,而"往哪个方向调、调多少"需要靠求导(梯度)来指导。就像你在山上蒙着眼找最低点,每一步都用脚试探地面是往上还是往下倾斜,然后往下的方向走。PyTorch 的 Autograd 就是自动帮你做这件事:你只写"向前算"的代码(比如输入×参数=输出),它会在后台悄悄记录每一步运算,等你算完说一句 backward(),它就自动倒着走一遍,算出每个参数该怎么调。不用你手写任何求导公式。

专业版:

4.1 为什么需要自动求导

深度学习训练的本质是:不断调整模型参数,使得损失函数值变小。

调整参数 = 损失函数对每个参数求偏导数,然后用梯度下降更新。

一个简单的全连接网络可能有几百万个参数,手算偏导数完全不可能。Autograd 就是解决这个问题的。

4.2 计算图(Computation Graph)

PyTorch 在你执行运算时,会在后台悄悄构建一张有向无环图(DAG),每个节点记录了三样东西:

  1. 执行了什么操作(加法、乘法、ReLU 等)
  2. 输入是什么
  3. 输出是什么

当你调用 .backward() 时,PyTorch 从最终输出反向遍历这张图,用链式法则自动计算每个参数的梯度。

import torch

# 开启梯度追踪
x = torch.tensor(2.0, requires_grad=True)
y = x ** 2 + 3 * x + 1

# 此时 y = 4 + 6 + 1 = 11
# dy/dx = 2x + 3 = 7

y.backward()            # 反向传播
print(x.grad)           # tensor(7.0)  ← 自动算出来了

这张图长什么样?

x  ──→  pow(2)  ──→  add  ──→  add  ──→  y
                         ↑
x  ──→  mul(3)  ────────┘
                         ↑
         1  ─────────────┘

4.3 requires_grad 的三个真相

# 1. 叶子节点默认不追踪
x = torch.tensor([1.0, 2.0])
print(x.requires_grad)   # False

# 需要显式开启
x = torch.tensor([1.0, 2.0], requires_grad=True)

# 2. 权重的 requires_grad 是自动开的
linear = nn.Linear(10, 5)
print(linear.weight.requires_grad)   # True
print(linear.bias.requires_grad)     # True

# 3. 推理时关掉,省显存、加速
with torch.no_grad():
    output = model(input)  # 不构建计算图,不追踪梯度

4.4 backward() 干了什么

x = torch.tensor([1.0, 2.0, 3.0], requires_grad=True)
y = (x ** 2).sum()    # y = 1 + 4 + 9 = 14
y.backward()

print(x.grad)          # tensor([2., 4., 6.])
# dy/dx_i = 2 * x_i

关键点:

  • .backward() 只能对标量调用(或者传一个与输出同形状的 gradient 参数)
  • 梯度是累加的——多次调用 .backward() 会在 .grad 上累加
  • 这就是为什么每个训练步必须调用 optimizer.zero_grad()
# 如果不 zero_grad 会怎样?
x = torch.tensor([1.0], requires_grad=True)
for _ in range(3):
    y = x ** 2
    y.backward()
    print(x.grad)    # 2, 4, 6 —— 梯度累加了!

4.5 detach() 的应用场景

当你需要拿一个张量的值做其他计算,但不想让梯度回传时:

# GAN 训练中:更新 Generator 时冻结 Discriminator
fake_output = generator(noise)
score = discriminator(fake_output.detach())  # detach: 梯度在此截断

detach() 返回一个共享数据但不共享计算图的新张量。


五、动态图 vs 静态图(为什么 PyTorch 赢了)

小白版:

“动态图"和"静态图"的区别,很像"即兴演讲"和"提前写稿”——或者更接地气地说,像"边打游戏边看血条"和"打完一局才能看回放"。

静态图(TensorFlow 1.x) 要求你先把整个计算流程画成一张大图纸,交给"执行引擎"去跑。跑的过程中你不能中途打断、不能看中间结果、不能用普通 if/for。就像你给包工头一份完整的施工蓝图,他在封闭的工地里施工,你想看看厕所的水管走位对不对,对不起,进不去。

动态图(PyTorch) 是你写一行,它就执行一行,你想 print 就 print,想 if 就 if,想 break 就 break。就像你一边写代码一边运行 Jupyter Notebook,写错了马上看到报错,立刻改。这在做 NLP(文本长度不固定)时优势巨大,因为长句子走长路径、短句子走短路径,不用为了对齐浪费算力。

这就是为什么研究人员爱死 PyTorch——做实验、调模型时,你需要随时看到中间结果,而不是等图跑完了才发现写错了。

专业版:

这是理解 PyTorch 最核心优势的一章。

5.1 静态图(Define-and-Run)

代表: TensorFlow 1.x

先定义整个计算图,然后编译,最后运行。就像:

  • 你画好整栋楼的图纸
  • 交给包工头
  • 包工头一次性盖完
# TensorFlow 1.x
graph = tf.Graph()
with graph.as_default():
    x = tf.placeholder(tf.float32, shape=[None, 784])
    W = tf.Variable(tf.zeros([784, 10]))
    y = tf.matmul(x, W)

with tf.Session(graph=graph) as sess:
    sess.run(tf.global_variables_initializer())
    result = sess.run(y, feed_dict={x: my_data})

静态图的痛苦:

  1. 不能在定义时打印中间值
  2. 不能用 Python 原生 if/for(要用 tf.while_looptf.cond
  3. 调试极其困难——出错信息指向的是图里面的某个 op,不是你写的代码行

5.2 动态图(Define-by-Run)

代表: PyTorch

代码执行到哪,图就构建到哪。就像:

  • 你一边写代码
  • 图一边生成
  • 随时可以 printiffor
# PyTorch:动态图
x = torch.randn(32, 784)

# 可以直接 print
print(f"输入形状: {x.shape}")

# 可以用任意 Python 控制流
if x.shape[0] > 16:
    x = x[:16]

# 可以用 for 循环
for i, sample in enumerate(x):
    if i == 5:
        break

5.3 动态图在 NLP 中的碾压优势

NLP 任务中序列长度是不固定的。静态图里你需要 pad 到最大长度,浪费大量计算。动态图里:

# PyTorch:每句话按实际长度走计算图
sentences = ["Hello", "This is a longer sentence", "Hi"]

for sent in sentences:
    tokens = tokenizer(sent)
    output = model(torch.tensor(tokens))   # 每条独立走图
    # 短句走的图短,长句走的图长,不浪费计算

在静态图中,一个 batch 里所有句子必须 pad 成同样长度,然后在计算时用 mask 把 padding 位置 mask 掉——凭空多了大量无效计算。

5.4 静态图的唯一优势(现在也被追上了)

静态图唯一的优势是:图在运行前已知全部结构,可以做图优化(算子融合、内存复用)。

但 PyTorch 2.0 引入的 torch.compile() 已经用 JIT 技术在运行时做了这些优化,动态图的性能差距几乎消失。

# PyTorch 2.0+
model = MyModel()
model = torch.compile(model)   # 一行代码获得静态图级别的优化

六、一个完整模型训练流程(重点)

小白版:

训练一个 AI 模型的流程,和教你家的狗学"坐下"几乎一模一样:

  1. 准备数据:拿出狗粮(训练图片+标签)
  2. 给指令:说"坐下"(输入图片给模型)
  3. 看结果:狗做了某个动作(模型输出预测)
  4. 给反馈:它做对了奖励、做错了纠正(计算损失函数——“差了多少”)
  5. 反向调整:狗下次会更倾向做对的动作(反向传播 + 优化器更新参数)
  6. 重复 1-5:一遍又一遍,直到狗(模型)学会

下面这套代码就是你"训狗"的完整自动化流程。

专业版:

这是你工作中 90% 时间在写的东西。我们用 MNIST 分类来演示。

6.1 完整代码

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

# ========== 1. 数据准备 ==========
transform = transforms.Compose([
    transforms.ToTensor(),
    transforms.Normalize((0.1307,), (0.3081,))  # MNIST 的经验值
])

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

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

# ========== 2. 定义模型 ==========
class MNISTModel(nn.Module):
    def __init__(self):
        super().__init__()
        self.flatten = nn.Flatten()
        self.fc1 = nn.Linear(28 * 28, 512)
        self.fc2 = nn.Linear(512, 256)
        self.fc3 = nn.Linear(256, 10)
        self.relu = nn.ReLU()
        self.dropout = nn.Dropout(0.2)

    def forward(self, x):
        x = self.flatten(x)
        x = self.relu(self.fc1(x))
        x = self.dropout(x)
        x = self.relu(self.fc2(x))
        x = self.dropout(x)
        x = self.fc3(x)
        return x

model = MNISTModel()
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
model = model.to(device)

# ========== 3. 损失函数和优化器 ==========
criterion = nn.CrossEntropyLoss()
optimizer = optim.Adam(model.parameters(), lr=0.001)

# ========== 4. 训练循环 ==========
epochs = 10

for epoch in range(epochs):
    # ---- 训练阶段 ----
    model.train()
    train_loss = 0.0

    for batch_idx, (data, target) in enumerate(train_loader):
        data, target = data.to(device), target.to(device)

        # 前向
        output = model(data)
        loss = criterion(output, target)

        # 反向
        optimizer.zero_grad()
        loss.backward()
        optimizer.step()

        train_loss += loss.item()

    avg_train_loss = train_loss / len(train_loader)

    # ---- 验证阶段 ----
    model.eval()
    correct = 0
    total = 0

    with torch.no_grad():
        for data, target in test_loader:
            data, target = data.to(device), target.to(device)
            output = model(data)
            _, predicted = torch.max(output, 1)
            total += target.size(0)
            correct += (predicted == target).sum().item()

    accuracy = 100.0 * correct / total
    print(f"Epoch {epoch+1:2d} | "
          f"Train Loss: {avg_train_loss:.4f} | "
          f"Test Accuracy: {accuracy:.2f}%")

print("训练完成!")

6.2 每个步骤在做什么

步骤 代码关键行 目的
数据准备 DataLoader(train_dataset, batch_size=64) 批量加载数据
定义模型 class MNISTModel(nn.Module) 定义网络结构
损失函数 nn.CrossEntropyLoss() 衡量预测和真实的差距
优化器 optim.Adam(model.parameters()) 决定如何更新参数
前向传播 output = model(data) 计算预测值
计算损失 loss = criterion(output, target) 算出差多少
清空梯度 optimizer.zero_grad() 防止梯度累加
反向传播 loss.backward() 计算每个参数的梯度
更新参数 optimizer.step() 沿梯度方向微调参数
验证 model.eval() + torch.no_grad() 评估模型,不更新参数

6.3 model.train() vs model.eval()

这可能是最容易被忽视但影响非常大的一行代码。

model.train()   # 训练模式
model.eval()    # 评估模式

它们影响哪些层:

train() 行为 eval() 行为
Dropout 随机丢弃神经元 关闭丢弃,所有神经元都工作
BatchNorm 用当前 batch 的均值和方差 用训练期间积累的全局均值和方差

如果忘了切换模式:在测试时 Dropout 还在随机丢弃,导致每次推理结果不一样,准确率偏低;BatchNorm 用测试 batch 的统计量而非全局统计量,对结果稳定性也有影响。

6.4 常见误区:Loss 不下降

看到 Loss 不下降时,按这个顺序排查:

# 1. 确认数据进去了
print(data.shape, target.shape)

# 2. 确认模型输出了
output = model(data)
print(output.shape)

# 3. 检查 loss 是否在合理范围
# CrossEntropyLoss 初始 loss 应该在 -ln(1/num_classes) 附近
# 对 10 分类,约 2.3;如果看到天文数字或 NaN,说明有问题
import math
expected_init_loss = -math.log(1 / 10)
print(f"期望初始 loss: {expected_init_loss:.2f}")

# 4. 梯度是否在流动
for name, param in model.named_parameters():
    if param.grad is not None:
        print(f"{name}: grad_mean={param.grad.mean():.6f}")

七、Dataset 和 DataLoader(工作中必会)

小白版:

训练 AI 需要的数据量往往是十万甚至百万级别,你绝不可能一次性把所有数据塞进内存(电脑会直接卡死)。Dataset 和 DataLoader 解决的就是"怎么高效喂数据"的问题。

打个比方:你在厨房做菜,冰箱(硬盘)里有 1000 个鸡蛋。你不可能一次全拿出来打在碗里(内存爆炸)。你需要的是:每次拿 8 个鸡蛋(batch),打散,下锅,然后再拿下一批。Dataset 就是你的冰箱管理员——它知道每个鸡蛋在哪、什么品质(标签)。DataLoader 就是你的帮手——它帮你按批次取蛋、打蛋(预处理)、送到灶台(GPU)。

专业版:

7.1 为什么需要 Dataset / DataLoader

你不可能一次性把所有数据加载到内存(ImageNet 有 140 万张图)。需要:

  1. 按需加载:只加载当前 batch 的数据
  2. 并行加载:用多进程加速
  3. 随机打乱 + 批处理 + 预处理

这就是 Dataset 和 DataLoader 的职责。

7.2 自定义 Dataset

工作中你面对的不是 MNIST,而是一堆散落文件。自定义 Dataset 是你最常用的技能:

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

class MyImageDataset(Dataset):
    def __init__(self, image_dir, label_file, transform=None):
        self.image_dir = image_dir
        self.transform = transform

        # 读取标签文件:每行 "image_name.jpg,label"
        self.samples = []
        with open(label_file, 'r') as f:
            for line in f:
                img_name, label = line.strip().split(',')
                self.samples.append((img_name, int(label)))

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

    def __getitem__(self, idx):
        img_name, label = self.samples[idx]
        img_path = os.path.join(self.image_dir, img_name)

        # 用 PIL 读取图片
        image = Image.open(img_path).convert('RGB')

        if self.transform:
            image = self.transform(image)

        return image, label

三个必须实现的方法:

方法 作用
__init__ 初始化文件列表、路径、transform
__len__ 返回数据集大小
__getitem__ 根据索引返回一个 (data, label) 元组

7.3 DataLoader 的关键参数

loader = DataLoader(
    dataset,
    batch_size=32,          # 每个 batch 多大
    shuffle=True,           # 是否打乱
    num_workers=4,          # 用几个子进程加载数据 ★ 重要
    pin_memory=True,        # 锁页内存,加速 CPU→GPU 传输
    drop_last=False,        # 最后一个不完整 batch 是否丢弃
    prefetch_factor=2,      # 每个 worker 提前加载的 batch 数
    persistent_workers=True # 不 kill worker,避免反复创建进程开销
)

num_workers 怎么调:

# 经验公式(不是绝对的)
# num_workers = min(CPU 核心数, batch_size, 8)

# 太小:数据加载成为瓶颈,GPU 空转
# 太大:CPU 吃不消,多进程创建开销反而拖慢

# 建议:从 4 开始试,逐步增大,找到 GPU 利用率最高且 CPU 不过载的值

pin_memory=True 为什么能加速:

正常流程:数据在可分页内存 → GPU 显存(需要先拷到一个锁页内存缓冲区)

开启后:数据直接加载到锁页内存 → 一步拷到 GPU 显存

速度提升通常在 1.5x ~ 2x

7.4 collate_fn 的真正用法

当你每个样本不是固定形状(NLP 变长序列、目标检测不同数量框)时,需要自定义 collate_fn

def my_collate_fn(batch):
    """batch 是一个 list,每个元素是 Dataset.__getitem__ 返回的元组"""
    images = [item[0] for item in batch]
    labels = [item[1] for item in batch]

    # pad 到相同大小
    max_h = max(img.shape[1] for img in images)
    max_w = max(img.shape[2] for img in images)

    padded = []
    for img in images:
        pad_h = max_h - img.shape[1]
        pad_w = max_w - img.shape[2]
        padded.append(torch.nn.functional.pad(img, (0, pad_w, 0, pad_h)))

    return torch.stack(padded), torch.tensor(labels)

loader = DataLoader(dataset, batch_size=16, collate_fn=my_collate_fn)

八、GPU 与 CUDA 加速

小白版:

一句话解释 CPU 和 GPU 的区别:CPU 是一个博士后,GPU 是一千个小学生。

博士后(CPU)智商极高,能处理复杂的逻辑判断、分支跳转,但人数少(8-16个核心)。一千个小学生(GPU)单个人不聪明,只能做简单的加减乘除,但胜在人多、手快。

训练神经网络的本质就是做海量的矩阵乘法——每个元素都是先乘再加,计算方式一模一样。这正好是"一千个小学生"最擅长的事:分一人一列,同时算。而 PyTorch 做的事情就是帮你把矩阵乘法自动分配给 GPU 上的上千个核心并行计算,所以你写 .cuda() 一行代码就能获得几十倍加速。

专业版:

8.1 GPU 为什么比 CPU 快

CPU 是"全能运动员":核心少(8-16 个),每个核心能力强,擅长串行、复杂逻辑。

GPU 是"万名小学生":上千个核心,每个核心简单,擅长做同一件事做很多遍

矩阵乘法就是"同一件事做很多遍"的典型——A @ B 中每个元素的计算方式完全一样。这正是 GPU 最擅长的。

CUDA 是 NVIDIA 提供的编程接口,让你能直接控制 GPU 做并行计算。PyTorch 在底层调用 CUDA,你只需要 .cuda()

8.2 多 GPU 的基础认知

误区:“我有两块 GPU,训练速度就翻倍。”

不是。多 GPU 有通信开销,实际加速比:

GPU 数量 理想加速比 实际加速比(典型)
1 1x 1x
2 2x 1.7x ~ 1.9x
4 4x 2.8x ~ 3.5x
8 8x 4.5x ~ 6.5x

GPU 越多,通信占比越大,效率越低。

8.3 单卡训练模板

# 最佳实践:自动检测 GPU 可用性
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
print(f"使用设备: {device}")

# 模型和数据都要移到同一设备
model = MyModel().to(device)

for data, target in loader:
    data, target = data.to(device), target.to(device)
    # 训练...

常见坑:

# 错误示范 1:模型在 GPU,数据在 CPU
model = MyModel().cuda()
for data, _ in loader:
    output = model(data)   # RuntimeError! data 还在 CPU 上

# 错误示范 2:张量在不同 GPU 上
x = torch.randn(10).cuda(0)
y = torch.randn(10).cuda(1)
z = x + y   # RuntimeError! 不在同一设备

# 正确做法
x = torch.randn(10).cuda(0)
y = torch.randn(10).cuda(0)  # 同一设备
z = x + y

九、多 GPU 分布式训练(DataParallel / DDP)

小白版:

如果你有一块 GPU,相当于一个人搬砖。有两块 GPU,你想让两个人一起搬。怎么分活?

DataParallel 方案(不推荐): 指定一个人当工头(GPU 0),他把一堆砖分成两份,自己拿一份,另一份丢给工友。工友搬完把砖交给工头,工头再统一码好。问题是:工头既要搬砖又要管分配,累死了,而且工友搬完得等工头。

DDP 方案(工业标准): 两个人各搬各的,搬完之后用对讲机同步一下"我已经搬了 50 块,你呢?""我也 50 块,那我俩平均一下,按 50 块来汇报。"没有工头瓶颈,效率高得多。

这就是为什么生产环境都用 DDP——每个 GPU 平等干活,梯度同步高效,还能跨多台机器。

专业版:

9.1 DataParallel(DP)—— 不推荐但要知道

# 最简单的方式:一行代码
model = nn.DataParallel(model)

# 完整示例
model = MyModel()
if torch.cuda.device_count() > 1:
    print(f"使用 {torch.cuda.device_count()} 张 GPU")
    model = nn.DataParallel(model)
model = model.cuda()

DP 的工作原理:

  1. 主卡(GPU 0)拿到一个 batch
  2. 把 batch 分成 N 份,发给 N 张卡
  3. 每张卡各自前向 + 反向
  4. 梯度汇总到主卡
  5. 主卡更新参数,然后广播给其他卡

DP 的问题:

问题 影响
主卡负载不均衡 GPU 0 显存占用明显高于其他卡
Python GIL 限制 多线程受 GIL 影响,效率低
只支持单机 不能跨机器
无法用混合精度 不支持 fp16

结论:DP 只适合快速验证,生产环境不要用。

9.2 DistributedDataParallel(DDP)—— 工业标配

DDP 解决了 DP 的所有问题。核心区别:

DataParallel DistributedDataParallel
通信方式 多线程 + 主卡汇总 多进程 + All-Reduce
负载均衡 主卡过载 每张卡均等
GIL 限制 受限制 多进程,不受限
跨机器 不支持 支持
混合精度 不支持 支持
速度 基准 比 DP 快 10%-30%

DDP 的标准写法:

import torch
import torch.distributed as dist
import torch.multiprocessing as mp
from torch.nn.parallel import DistributedDataParallel as DDP
from torch.utils.data.distributed import DistributedSampler

def setup(rank, world_size):
    """初始化进程组"""
    dist.init_process_group(
        backend='nccl',              # GPU 通信用 NCCL
        init_method='env://',        # 从环境变量读地址
        rank=rank,
        world_size=world_size
    )
    torch.cuda.set_device(rank)

def cleanup():
    dist.destroy_process_group()

def train(rank, world_size):
    setup(rank, world_size)

    # 模型
    model = MyModel().to(rank)
    model = DDP(model, device_ids=[rank])

    # 数据集:每个进程只拿自己的那部分
    dataset = MyDataset()
    sampler = DistributedSampler(
        dataset, 
        num_replicas=world_size, 
        rank=rank,
        shuffle=True
    )
    loader = DataLoader(
        dataset, 
        batch_size=32, 
        sampler=sampler,       # ★ 关键:用 sampler 而不是 shuffle
        num_workers=4,
        pin_memory=True
    )

    criterion = nn.CrossEntropyLoss()
    optimizer = optim.Adam(model.parameters(), lr=0.001)

    for epoch in range(10):
        sampler.set_epoch(epoch)  # ★ 每个 epoch 重新打乱

        for data, target in loader:
            data, target = data.to(rank), target.to(rank)

            output = model(data)
            loss = criterion(output, target)

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

    cleanup()

# 启动训练
if __name__ == "__main__":
    world_size = torch.cuda.device_count()
    mp.spawn(train, args=(world_size,), nprocs=world_size)

DDP 的三个关键点:

  1. 每个 GPU 一个独立进程mp.spawn 启动多个进程,每个进程管一张卡
  2. 数据分片DistributedSampler 确保每张卡拿不同的数据
  3. 梯度同步loss.backward() 时自动做 All-Reduce,梯度在所有卡之间平均

9.3 DDP 训练的启动方式

# 单机多卡
torchrun --nproc_per_node=4 train_ddp.py

# 或旧写法(也可以用,但 torchrun 更推荐)
python -m torch.distributed.launch --nproc_per_node=4 train_ddp.py

9.4 单卡训练 vs DP vs DDP 速度对比(典型 ResNet-50)

配置 训练时间(ImageNet 1 epoch) 加速比
1×V100 45 分钟 1.0x
4×V100 DP 18 分钟 2.5x
4×V100 DDP 14 分钟 3.2x

十、PyTorch Lightning 工程化训练

小白版:

原生 PyTorch 写一个完整的训练流程,100 行代码里 80 行是重复的模板——每次都要手写 zero_grad()backward()step()model.train() 这一套。PyTorch Lightning 就是把这些"每次都要写一遍的破事"全帮你做了,你只需要告诉它三件事:① 数据怎么来 ② 模型长啥样 ③ 训练一步该干嘛。然后它自动处理 GPU 分配、进度条、日志、断点续训、混合精度这些工程细节。

可以理解为:原生 PyTorch 是手动挡汽车,你享受驾驶乐趣但操作繁琐。Lightning 是自动挡,你踩油门(写核心逻辑)就行,不用操心离合和换挡(工程细节)。

专业版:

10.1 原生 PyTorch 的痛点

上面的训练代码写了 100 行,但其中 80 行是样板代码

  • optimizer.zero_grad()
  • loss.backward()
  • optimizer.step()
  • model.train() / model.eval()
  • to(device)
  • 学习率调度
  • checkpoint 保存/加载
  • 日志记录
  • 16 位精度训练

PyTorch Lightning 把这些全部抽象掉,让你只写模型逻辑

10.2 用 Lightning 重写 MNIST

import pytorch_lightning as pl
import torch
import torch.nn as nn
import torch.nn.functional as F
from torch.utils.data import DataLoader
from torchvision import datasets, transforms

class LitMNIST(pl.LightningModule):
    def __init__(self):
        super().__init__()
        self.fc1 = nn.Linear(28 * 28, 512)
        self.fc2 = nn.Linear(512, 256)
        self.fc3 = nn.Linear(256, 10)
        self.dropout = nn.Dropout(0.2)

    def forward(self, x):
        x = x.view(x.size(0), -1)
        x = F.relu(self.fc1(x))
        x = self.dropout(x)
        x = F.relu(self.fc2(x))
        x = self.dropout(x)
        x = self.fc3(x)
        return x

    # ↓ 你只需要写这一个方法
    def training_step(self, batch, batch_idx):
        x, y = batch
        logits = self(x)
        loss = F.cross_entropy(logits, y)
        self.log('train_loss', loss)
        return loss

    def validation_step(self, batch, batch_idx):
        x, y = batch
        logits = self(x)
        loss = F.cross_entropy(logits, y)
        preds = torch.argmax(logits, dim=1)
        acc = (preds == y).float().mean()
        self.log('val_loss', loss, prog_bar=True)
        self.log('val_acc', acc, prog_bar=True)

    def configure_optimizers(self):
        return torch.optim.Adam(self.parameters(), lr=0.001)

# 数据模块
class MNISTDataModule(pl.LightningDataModule):
    def __init__(self, batch_size=64):
        super().__init__()
        self.batch_size = batch_size
        self.transform = transforms.Compose([
            transforms.ToTensor(),
            transforms.Normalize((0.1307,), (0.3081,))
        ])

    def setup(self, stage=None):
        self.mnist_train = datasets.MNIST(
            './data', train=True, download=True, transform=self.transform
        )
        self.mnist_val = datasets.MNIST(
            './data', train=False, download=True, transform=self.transform
        )

    def train_dataloader(self):
        return DataLoader(self.mnist_train, batch_size=self.batch_size, shuffle=True)

    def val_dataloader(self):
        return DataLoader(self.mnist_val, batch_size=self.batch_size)

# 训练,就 3 行
model = LitMNIST()
dm = MNISTDataModule()
trainer = pl.Trainer(max_epochs=10, accelerator='auto', devices='auto')
trainer.fit(model, dm)

对比: 原生 PyTorch 的 100 多行变成了约 60 行,而且:

  • 自动处理 model.train() / model.eval()
  • 自动处理 GPU
  • 自动打印进度条和日志
  • 自动保存最优 checkpoint
  • 支持多 GPU / TPU / 混合精度——改一行配置即可

10.3 Lightning 的进阶配置

trainer = pl.Trainer(
    max_epochs=100,

    # 硬件
    accelerator='gpu',          # 'cpu', 'gpu', 'tpu', 'auto'
    devices=4,                  # GPU 数量
    strategy='ddp',             # 'dp', 'ddp', 'fsdp', 'deepspeed'

    # 精度
    precision='16-mixed',       # 混合精度训练,省显存、加速

    # 日志
    logger=pl.loggers.TensorBoardLogger('logs/'),
    log_every_n_steps=10,

    # checkpoint
    callbacks=[
        pl.callbacks.ModelCheckpoint(
            monitor='val_acc',
            mode='max',
            save_top_k=3,
            filename='mnist-{epoch:02d}-{val_acc:.2f}'
        ),
        pl.callbacks.EarlyStopping(
            monitor='val_loss',
            patience=5,
            mode='min'
        ),
        pl.callbacks.LearningRateMonitor(logging_interval='epoch')
    ]
)

十一、真实 AI 项目训练流程

小白版:

拿到一个真实 AI 任务,不要上来就写代码。遵循四步走:

  1. 先跑通:用最简单的模型、最小量的数据,先把训练→验证→保存整个流程跑起来,确认没 bug。
  2. 再调对:把小数据上的 loss 压到接近 0——证明模型有能力学会。
  3. 再调好:在全量数据上,逐个调整学习率、batch size、数据增强等参数,让验证指标达标。
  4. 最后工程化:加日志、加断点续训、加模型导出、加推理接口。

这和做菜一样:先确认锅和灶都能用(跑通),再确认调料比例没错(调对),再反复微调火候(调好),最后装盘上菜(工程化)。

专业版:

阶段一:项目启动(1-2 天)

1. 明确任务:分类 / 检测 / 分割 / 生成?
2. 确定评价指标:准确率 / mAP / IoU / BLEU?
3. 找基线模型(Baseline):github / Papers with Code
4. 搭建最小可运行环境
# 标准项目结构
project/
├── configs/          # 配置文件(推荐用 yaml)
│   └── baseline.yaml
├── data/             # 原始数据
│   ├── train/
│   └── val/
├── dataset/          # Dataset 类
│   └── my_dataset.py
├── models/           # 模型定义
│   └── my_model.py
├── utils/            # 工具函数
│   ├── metrics.py
│   └── visualize.py
├── train.py          # 训练入口
├── inference.py      # 推理入口
└── requirements.txt

阶段二:快速验证(1 天)

目标:用最简单的方法跑通全流程,确认代码能收敛。

# 先在小数据上跑通
tiny_dataset = torch.utils.data.Subset(full_dataset, range(1000))

# 过拟合小数据——确认模型能学习
model = MyModel()
optimizer = optim.Adam(model.parameters(), lr=0.01)

for epoch in range(100):
    # ... 训练,不调参,纯粹看 loss 能不能降到接近 0
    pass

# loss 降到接近 0 → 模型没问题
# loss 降不下去 → 代码有 bug 或模型容量不够

阶段三:调参优化(3-7 天)

关键超参数和调参顺序(按重要性排序):

优先级 超参数 典型范围 对效果影响
1 学习率 1e-5 ~ 1e-2 最大
2 batch size 16 / 32 / 64 / 128
3 优化器 Adam / AdamW / SGD + Momentum
4 正则化 Dropout 0.1~0.5, Weight Decay 0.0001~0.01
5 学习率调度 Cosine / Step / ReduceLROnPlateau
6 数据增强 Rotate / Flip / Cutout / MixUp
# 学习率调度示例
from torch.optim.lr_scheduler import CosineAnnealingLR, ReduceLROnPlateau

# 余弦退火:从初始 lr 平滑降到 0
scheduler = CosineAnnealingLR(optimizer, T_max=epochs)

# 或:监控 val_loss,不降了就减半
scheduler = ReduceLROnPlateau(optimizer, mode='min', factor=0.5, patience=3)

for epoch in range(epochs):
    train()
    val_loss = validate()
    scheduler.step(val_loss)   # ReduceLROnPlateau 需要传 val_loss
    # scheduler.step()          # CosineAnnealing 不需要参数

阶段四:完整训练与产出

# 保存完整训练状态(不只是权重)
checkpoint = {
    'epoch': epoch,
    'model_state_dict': model.state_dict(),
    'optimizer_state_dict': optimizer.state_dict(),
    'loss': loss,
    'config': config_dict,
}
torch.save(checkpoint, f'checkpoint_epoch_{epoch}.pt')

# 推理时只加载权重
model = MyModel()
model.load_state_dict(torch.load('best_model.pt')['model_state_dict'])
model.eval()

十二、常见坑与避坑指南

小白版:

PyTorch 初学者踩坑最多的地方可以总结为"三板斧":

  1. 忘清零:每次训练前要 optimizer.zero_grad(),否则梯度会像滚雪球一样越滚越大,参数更新越来越离谱。
  2. 模式忘切换:训练时用 model.train(),测试时用 model.eval()。忘了切换的话,测试时 Dropout 还在随机丢神经元、BatchNorm 还在用错误统计量,准确率直接掉坑里。
  3. 数据不在同一设备:模型搬到了 GPU 上,但输入数据还在 CPU 上——程序直接报错。记住一句话:模型和数据必须待在同一张卡(或 CPU)上

除此之外还有一些更隐蔽的坑,下面详细拆解。

专业版:

坑 1:忘了 zero_grad()

# ❌ 错误
for data, target in loader:
    loss = criterion(model(data), target)
    loss.backward()
    optimizer.step()            # 梯度一直累加,参数更新量越来越大

# ✅ 正确
for data, target in loader:
    loss = criterion(model(data), target)
    optimizer.zero_grad()        # 每次迭代前清空梯度
    loss.backward()
    optimizer.step()

坑 2:在 with torch.no_grad() 外面做验证

# ❌ 错误:验证时也在构建计算图,浪费显存
model.eval()
for data, target in val_loader:
    output = model(data)
    loss = criterion(output, target)   # 计算图被构建了!

# ✅ 正确
model.eval()
with torch.no_grad():
    for data, target in val_loader:
        output = model(data)
        loss = criterion(output, target)   # 不构建计算图

坑 3:在 eval() 模式下忘了关掉,又去训练

# ❌ 灾难性错误
model.eval()
# ... 做了验证
# 忘了 model.train()
for data, target in train_loader:
    output = model(data)     # Dropout 关闭了,BatchNorm 用全局统计量
    # 模型训练效果大打折扣

坑 4:nn.Module 中用了 Python list 存子模块

# ❌ 错误:list 里的模块不会被注册
class BadModel(nn.Module):
    def __init__(self):
        super().__init__()
        self.layers = [nn.Linear(10, 10) for _ in range(5)]

    def forward(self, x):
        for layer in self.layers:
            x = layer(x)
        return x

model = BadModel()
print(list(model.parameters()))   # 空的!

# ✅ 正确:用 nn.ModuleList
class GoodModel(nn.Module):
    def __init__(self):
        super().__init__()
        self.layers = nn.ModuleList([nn.Linear(10, 10) for _ in range(5)])

    def forward(self, x):
        for layer in self.layers:
            x = layer(x)
        return x

model = GoodModel()
print(len(list(model.parameters())))   # 10(5个weight + 5个bias)

坑 5:torch.load 的安全风险

# ❌ 危险:从不可信来源加载模型
model = torch.load('downloaded_from_internet.pt')
# 这个 .pt 文件可能包含恶意 pickle 代码,直接执行!

# ✅ 安全做法
model = MyModel()
checkpoint = torch.load('model.pt', weights_only=True)
model.load_state_dict(checkpoint['model_state_dict'])

坑 6:混合 tensor 类型

# ❌ 常见错误
x = torch.randn(10)                       # float32
y = torch.tensor([1, 2, 3, 4])            # int64
z = x + y                                 # RuntimeError!

# ✅ 统一类型
y = torch.tensor([1, 2, 3, 4], dtype=torch.float32)
z = x + y                                 # OK

坑 7:显存泄漏

# ❌ 每个 batch 都在创建新张量但不释放
losses = []
for data, target in loader:
    output = model(data)
    loss = criterion(output, target)
    losses.append(loss.item())   # .item() 没问题,但如果不加 .item()

    # loss 是个标量张量,如果放进 list,引用了计算图
    # 计算图越来越大,显存爆炸

# ✅ 正确
losses = []
for data, target in loader:
    output = model(data)
    loss = criterion(output, target)
    losses.append(loss.item())     # 转为 Python 数字,切断引用
    loss.backward()
    optimizer.step()
    optimizer.zero_grad()

十三、一句话总结 + 学习路线

小白版:

PyTorch 就是"深度学习的 Python"——它让你用写普通 Python 的方式写 AI。你不需要理解显卡底层怎么工作、不需要手算梯度、不需要自己实现矩阵乘法。你只需要学会三件事:定义模型(搭积木)、加载数据(喂数据)、写训练循环(调参数),就能做出能用的 AI。

推荐从小练习开始:先用 PyTorch 实现一个线性回归(10 行代码),再做 MNIST 手写数字识别(50 行代码),然后逐步进阶。不要只看不写——每个阶段写一个小项目,代码量是学会的唯一标准。

专业版:

一句话总结

PyTorch 就是把 NumPy 搬上 GPU,加上自动求导,然后用 Python 原生风格写深度学习代码的框架——它的成功来自于"让写深度学习像写普通 Python 一样自然"。

推荐学习路线

第一阶段(1 周)
├── Tensor 基本操作(创建、索引、运算、广播)
├── Autograd(requires_grad, backward, no_grad)
└── 小练习:用纯 Tensor + Autograd 实现线性回归

第二阶段(1 周)
├── nn.Module(定义模型、forward 方法)
├── nn.Linear, nn.Conv2d, nn.ReLU 等常用层
├── Loss + Optimizer + 训练循环
└── 小练习:在 MNIST 上训练一个分类器

第三阶段(2 周)
├── Dataset + DataLoader + transforms
├── GPU 训练
├── 保存 / 加载模型
└── 小练习:在自己的数据集上训练 ResNet

第四阶段(1 周)
├── 学习率调度
├── TensorBoard 可视化
├── 多 GPU(DDP)
└── 小练习:用 DDP 训练,打 Kaggle 一个比赛

第五阶段(按需)
├── PyTorch Lightning(工程化)
├── 混合精度训练
├── ONNX / TorchScript 模型部署
├── torch.compile(PyTorch 2.0)
└── 读论文 + 复现

核心资料

资源 链接 适合阶段
PyTorch 官方 60 min 教程 pytorch.org/tutorials 第一阶段
d2l.ai(动手学深度学习) d2l.ai 第二、三阶段
PyTorch Lightning 文档 lightning.ai/docs 第四阶段
Papers with Code paperswithcode.com 全阶段

这篇文章覆盖了从零基础到工业级多 GPU 训练的全部核心知识。前半段用通俗类比建立直觉,后半段用代码和表格给出可实操的方案。如果你正在入门 AI,建议按学习路线一步步来,每学完一个阶段就动手做一个小项目——代码写出来才能真正理解。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐