Pytorch核心基础入门

PyTorch 核心基础:从张量到数据流水线

文章目录

  • [PyTorch 核心基础:从张量到数据流水线](#PyTorch 核心基础:从张量到数据流水线)
    • [一、初识 PyTorch:现代深度学习的核心工具](#一、初识 PyTorch:现代深度学习的核心工具)
      • [1.1 PyTorch 为何成为研究与 LLM 开发的首选?](#1.1 PyTorch 为何成为研究与 LLM 开发的首选?)
      • [1.2 动态计算图 vs TensorFlow 静态图:为什么 PyTorch 更适合实验创新](#1.2 动态计算图 vs TensorFlow 静态图:为什么 PyTorch 更适合实验创新)
      • [1.3 PyTorch 生态系统概览:这几个库你更该关注](#1.3 PyTorch 生态系统概览:这几个库你更该关注)
    • [二、张量(Tensor)与自动微分 ---核心基石](#二、张量(Tensor)与自动微分 ---核心基石)
      • [2.1:张量操作------PyTorch 的通用数据载体](#2.1:张量操作——PyTorch 的通用数据载体)
        • [2.1.1 创建张量:从列表、NumPy 到 GPU 加速](#2.1.1 创建张量:从列表、NumPy 到 GPU 加速)
        • [2.1.2 张量属性解析:shape、dtype、device 及其重要性](#2.1.2 张量属性解析:shape、dtype、device 及其重要性)
        • [2.1.2.1 形状 Shape](#2.1.2.1 形状 Shape)
        • [2.1.2.2 dtype 数据类型](#2.1.2.2 dtype 数据类型)
        • [2.1.2.3 device 设备](#2.1.2.3 device 设备)
        • [2.1.3 基本运算:加减乘除、矩阵乘法](#2.1.3 基本运算:加减乘除、矩阵乘法)
        • [2.1.4 索引、切片与重塑](#2.1.4 索引、切片与重塑)
        • [2.1.5 CPU 与 GPU 之间自由迁移:`.to(device)`](#2.1.5 CPU 与 GPU 之间自由迁移:.to(device))
      • [2. 2 :自动微分机制------深度学习的"求导引擎"](#2. 2 :自动微分机制——深度学习的"求导引擎")
        • [2.2.1 requires_grad:何时开启梯度追踪?](#2.2.1 requires_grad:何时开启梯度追踪?)
        • [2.2.2 backward() 与 grad:反向传播背后的原理](#2.2.2 backward() 与 grad:反向传播背后的原理)
        • [2.2.3 动态计算图的魅力:每一次前向都是独立的图构建](#2.2.3 动态计算图的魅力:每一次前向都是独立的图构建)
        • [2.2.4 梯度清零的重要性:optimizer.zero_grad()](#2.2.4 梯度清零的重要性:optimizer.zero_grad())
        • [2.2.5 with torch.no_grad() 推理加速](#2.2.5 with torch.no_grad() 推理加速)
    • 三、高效数据流水线
      • 3.1:数据加载与预处理
        • [3.1.1 自定义 Dataset 类:如何封装自己的数据?](#3.1.1 自定义 Dataset 类:如何封装自己的数据?)
        • [3.1.2 DataLoader 使用:batch_size、shuffle、num_workers 调优](#3.1.2 DataLoader 使用:batch_size、shuffle、num_workers 调优)
        • [3.1.3 数据增强策略简介(LLM 中已被 [Tokenizer](https://www.google.com/search?q=Tokenizer) 替代)](#3.1.3 数据增强策略简介(LLM 中已被 Tokenizer 替代))
        • [3.1.4 多进程加载陷阱与解决方案(Windows/macOS 注意事项)](#3.1.4 多进程加载陷阱与解决方案(Windows/macOS 注意事项))
        • [3.1.5 【拓展】使用 TorchData 构建可复用的数据管道](#3.1.5 【拓展】使用 TorchData 构建可复用的数据管道)

一、初识 PyTorch:现代深度学习的核心工具

1.1 PyTorch 为何成为研究与 LLM 开发的首选?

如果你关注过最近几年的深度学习论文,会发现一个有趣的现象:90% 以上的 LLM 研究都基于 PyTorch。无论是 GPT、LLaMA、Claude 还是国产的 GLM、Qwen,背后都是 PyTorch 的身影。

为什么会这样? 三个关键原因:

  1. 动态计算图:代码即模型,所见即所得
  2. Pythonic 设计:符合 Python 开发者直觉,学习曲线平滑
  3. 开放生态:与 HuggingFace、NVIDIA、Meta 等深度集成

把深度学习框架比作编程语言,TensorFlow 就像 Java(严谨但繁琐),而 PyTorch 更像 Python(灵活且优雅)。研究人员需要快速实验新想法,PyTorch 的动态特性让他们可以像写普通 Python 一样写模型。


1.2 动态计算图 vs TensorFlow 静态图:为什么 PyTorch 更适合实验创新

这是理解 PyTorch 核心优势的关键。我们用一个简单的例子说明:

python 复制代码
import torch

# 动态计算图示例(PyTorch)
def dynamic_forward(x, threshold=0.5):
    """根据输入动态选择计算路径"""
    if x.mean() > threshold:
        # 路径A:复杂处理
        return x ** 2 + torch.sin(x)
    else:
        # 路径B:简单处理
        return x * 2

# 每次调用都可以有不同的计算图!
x1 = torch.randn(10)
output1 = dynamic_forward(x1)  # 可能走路径A

x2 = torch.randn(10) - 1.0
output2 = dynamic_forward(x2)  # 可能走路径B

对比:

特性 PyTorch(动态图) TensorFlow 1.x(静态图)
定义方式 边运行边构建 先定义图,再运行
调试体验 可用普通 Python 调试器 需要特殊工具(Session)
控制流 原生 Python(if/for/while) 需要 tf.cond/tf.while_loop
适用场景 研究、快速迭代 生产部署(现在 TF 2.x 也动态了)

在LLM训练等场景中,PyTorch的灵活性能够做到:

  • 可依序列长度动态调整模型结构
  • 能在训练过程中灵活切换损失函数或训练策略
  • 实时查看并可视化中间层激活值、梯度等信息

1.3 PyTorch 生态系统概览:这几个库你更该关注

很多教程会列举 TorchVision、TorchText、TorchAudio 等官方库,但对于 LLM 开发者来说,以下库才是真正的"生产力工具":

核心生态
库名 用途 在 LLM 中的应用
HuggingFace Transformers 预训练模型库 加载 GPT/BERT/LLaMA 等模型
HuggingFace Accelerate 分布式训练抽象层 简化多 GPU/多机训练代码
FSDP (Fully Sharded Data Parallel) 大模型并行策略 训练 70B+ 参数模型
bitsandbytes 量化与优化器 8bit 优化器节省显存
python 复制代码
# 示例:3行代码启用混合精度 + 分布式训练(Accelerate)
from accelerate import Accelerator

accelerator = Accelerator(mixed_precision='fp16')
model, optimizer, dataloader = accelerator.prepare(model, optimizer, dataloader)

# 训练循环无需改动!
for batch in dataloader:
    outputs = model(batch)
    loss = outputs.loss
    accelerator.backward(loss)  # 自动处理梯度缩放
    optimizer.step()

二、张量(Tensor)与自动微分 ---核心基石

2.1:张量操作------PyTorch 的通用数据载体

2.1.1 创建张量:从列表、NumPy 到 GPU 加速

张量(Tensor)是 PyTorch 的最基本的核心数据结构,可以理解为"支持 GPU 加速的多维数组 "。就像 NumPy 中的 ndarray,但支持 GPU 加速和自动求导。

python 复制代码
import torch
import numpy as np

# ===== 方法1:从Python列表创建 =====
tensor_from_list = torch.tensor([1, 2, 3, 4])
print(tensor_from_list)  # tensor([1, 2, 3, 4])

# ===== 方法2:从NumPy数组转换 =====
numpy_array = np.array([[1, 2], [3, 4]])
tensor_from_numpy = torch.from_numpy(numpy_array)
print(tensor_from_numpy)
# tensor([[1, 2],
#         [3, 4]])

# ===== 方法3:使用工厂函数 =====
# 创建全0张量
zeros = torch.zeros(2, 3)  # 2行3列

# 创建全1张量
ones = torch.ones(2, 3)

# 创建随机张量(均匀分布 [0, 1))
rand_uniform = torch.rand(2, 3)

# 创建随机张量(标准正态分布)
rand_normal = torch.randn(2, 3)

# 创建等差序列
arange = torch.arange(0, 10, 2)  # [0, 2, 4, 6, 8]

# 创建线性间隔
linspace = torch.linspace(0, 1, steps=5)  # [0.00, 0.25, 0.50, 0.75, 1.00]

# ===== 方法4:创建GPU张量 =====
if torch.cuda.is_available():
    gpu_tensor = torch.tensor([1, 2, 3], device='cuda')
    print(f"张量在GPU上: {gpu_tensor.device}")

torch.tensor()复制数据 ,而 torch.from_numpy() 与原 NumPy 数组共享内存(修改一个会影响另一个)。


2.1.2 张量属性解析:shape、dtype、device 及其重要性

张量的三大核心属性决定了它的"身份信息":

python 复制代码
x = torch.randn(2, 3, 4)  # 创建一个3维张量

# ===== 形状 (Shape) =====
print(f"形状: {x.shape}")          # torch.Size([2, 3, 4])
print(f"维度数: {x.ndim}")         # 3
print(f"总元素数: {x.numel()}")    # 24 (2*3*4)

# ===== 数据类型 (dtype) =====
print(f"数据类型: {x.dtype}")      # torch.float32(默认)

# 创建不同类型的张量
int_tensor = torch.tensor([1, 2, 3], dtype=torch.int64)
float_tensor = torch.tensor([1.0, 2.0], dtype=torch.float16)

# ===== 设备 (device) =====
print(f"设备: {x.device}")         # cpu

# 检查CUDA是否可用
print(f"CUDA可用: {torch.cuda.is_available()}")
print(f"CUDA设备数: {torch.cuda.device_count()}")

为什么这三个属性如此重要?

属性 重要性 LLM 中的应用场景
shape 决定能否参与运算 注意力机制中 Q、K、V 的形状必须匹配
dtype 影响精度和性能 混合精度训练(FP16 节省显存)
device 决定计算位置 模型和数据必须在同一设备上
2.1.2.1 形状 Shape

shape 表示张量在每个维度上的大小,即每个轴上有多少个元素。

python 复制代码
print(x.shape)   # 输出: torch.Size([2, 3, 4])
print(type(x.shape))  # <class 'torch.Size'> → 实际上是一个类似元组的对象

我们可以把它看成是一个 Python 元组 (2, 3, 4),其中:

  • 第0维大小为 2 → 通常代表 batch size(批次)
  • 第1维度大小为 3 → 例如序列长度或通道数
  • 第2维度大小为 4 → 特征维度或嵌入维度

"这是一本书,有 2 页,每页是 3 行 × 4 列 的表格。"

第一个数 2:有几"页"(可以理解为有多少个样本)

第二个数 3:每页有几"行"

第三个数 4:每行有几"个数"

张量运算时,shape 不同不能互相运算,就像一本书不能和一页纸做相加,一箱苹果和一个橘子不能做平均。

操作

python 复制代码
import torch

# 创建一个3维张量作为例子
x = torch.randn(2, 3, 4)  # shape: [2, 3, 4]

print(x.shape)        # 输出: torch.Size([2, 3, 4]) → 形状
print(x.size())       # 输出: torch.Size([2, 3, 4]) → 和 shape 一样
print(x.ndim)         # 输出: 3 → 维度数(几维?)
print(x.dim())        # 输出: 3 → 和 ndim 一样
print(x.numel())      # 输出: 24 → 总元素个数(2*3*4)

# 改变形状(必须总元素数不变)
y = x.view(6, 4)              # → [6, 4],要求内存连续
y = x.reshape(6, 4)           # → [6, 4],更安全,自动处理不连续

# 增加一个维度(升维)
y = x.unsqueeze(0)            # → [1, 2, 3, 4],在第0维加1
y = x.unsqueeze(-1)           # → [2, 3, 4, 1],在最后一维加1

# 删除长度为1的维度(降维)
x_squeezed = x.unsqueeze(0)   # [1, 2, 3, 4]
y = x_squeezed.squeeze()      # → [2, 3, 4],去掉所有 size=1 的维
y = x_squeezed.squeeze(0)     # → [2, 3, 4],只去掉第0维(如果size=1)

# 调换维度顺序
y = x.permute(0, 2, 1)        # → [2, 4, 3],把第1维和第2维交换
y = x.transpose(0, 1)         # → [3, 2, 4],只交换两个维度(针对2D常用)

# 展平(flatten):把多个维度压成一个
y = x.flatten()               # → [24],全部展成一维
y = x.flatten(1)              # → [2, 12],从第1维开始展平
2.1.2.2 dtype 数据类型

常见类型

python 复制代码
import torch

# 创建不同数据类型的张量
x_float32 = torch.tensor([1.0, 2.0])                    # 默认 float32
x_float64 = torch.tensor([1.0, 2.0], dtype=torch.float64)  # 双精度
x_float16 = torch.tensor([1.0, 2.0], dtype=torch.float16)  # 半精度(节省显存)
x_int64   = torch.tensor([1, 2, 3], dtype=torch.int64)     # 长整型(标签常用)
x_int32   = torch.tensor([1, 2, 3], dtype=torch.int32)     # 32位整型
x_bool    = torch.tensor([True, False], dtype=torch.bool)  # 布尔型(掩码用)

查看和转换

python 复制代码
print(x_float32.dtype)           # 输出: torch.float32

# 转换数据类型
x_float16 = x_float32.half()     # → float16
x_float32 = x_float16.float()    # → float32
x_long    = x_float32.long()     # → int64 (常用于索引/标签)
x_bool    = x_float32.bool()     # → bool (非0为True)

# 推荐统一写法:使用 .to()
x = x.to(torch.float16)
x = x.to(dtype=torch.long)
  • 训练大模型常用 混合精度 (AMP),部分用 float16
  • 推理时模型可用 .half() 转成 float16 加速
2.1.2.3 device 设备
  • 查看设备信息
python 复制代码
x = torch.randn(2, 3)

print(x.device)                    # 输出: cpu

# 检查 CUDA 是否可用
print(torch.cuda.is_available())   # True/False
print(torch.cuda.device_count())   # 有几个 GPU
print(torch.cuda.current_device()) # 当前 GPU 编号
print(torch.cuda.get_device_name(0)) # GPU 型号(如 RTX 3090)
  • 移动张量到不同设备
python 复制代码
# 方法1:使用 .to()
x_cpu = x.to('cpu')
x_gpu = x.to('cuda')         # 移动到 GPU(要求 CUDA 可用)
x_gpu = x.to(torch.device('cuda'))

# 方法2:创建时直接指定
x = torch.randn(2, 3, device='cuda')

# 方法3:如果 CUDA 可用就用 GPU,否则用 CPU(推荐写法)
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
x = x.to(device)
  • 大模型(如 LLM)必须加载到 GPU 才能运行
  • 多卡训练要用 device='cuda:0', 'cuda:1' 区分
    一般这样写:
python 复制代码
import torch

# 1. 设置设备
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
print(f"使用设备: {device}")

# 2. 准备数据
x = torch.randn(32, 10, 512)           # 输入数据 [batch, seq_len, feature]
y = torch.randint(0, 100, (32,))       # 标签(类别)

# 3. 移动到设备并确保类型正确
x = x.to(device).float()               # float32,放到 device
y = y.to(device).long()                # 标签必须是 long (int64)

# 4. 定义模型并移到设备
model = torch.nn.Linear(512, 10).to(device)

# 5. 前向计算
with torch.no_grad():
    output = model(x.mean(dim=1))      # 简单测试

2.1.3 基本运算:加减乘除、矩阵乘法
python 复制代码
import torch

a = torch.tensor([[1, 2], [3, 4]], dtype=torch.float32)
b = torch.tensor([[5, 6], [7, 8]], dtype=torch.float32)

# ===== 逐元素运算 (Element-wise) =====
print("加法:", a + b)
# tensor([[ 6.,  8.],
#         [10., 12.]])

print("乘法(逐元素):", a * b)
# tensor([[ 5., 12.],
#         [21., 32.]])

# ===== 矩阵乘法 (Matrix Multiplication) =====
# 方法1:使用 @ 运算符(推荐)
matmul_1 = a @ b
print("矩阵乘法 (@):\n", matmul_1)

# 方法2:使用 torch.matmul
matmul_2 = torch.matmul(a, b)

# 方法3:使用 torch.mm(仅支持2D)
matmul_3 = torch.mm(a, b)

# ===== 批量矩阵乘法 (Batch Matrix Multiplication) =====
# 形状: [batch_size, n, m] @ [batch_size, m, p] -> [batch_size, n, p]
batch_a = torch.randn(32, 10, 20)  # 32个样本,每个是10x20矩阵
batch_b = torch.randn(32, 20, 30)  # 32个样本,每个是20x30矩阵
batch_result = torch.bmm(batch_a, batch_b)  # 结果: [32, 10, 30]

# ===== 广播机制 (Broadcasting) =====
x = torch.tensor([[1, 2, 3]])      # 形状: [1, 3]
y = torch.tensor([[1], [2], [3]])  # 形状: [3, 1]
result = x + y  # 广播后: [3, 3]
print("广播结果:\n", result)
# tensor([[2, 3, 4],
#         [3, 4, 5],
#         [4, 5, 6]])

易误点:

python 复制代码
#  错误:形状不兼容
a = torch.randn(3, 4)
b = torch.randn(3, 5)
# c = a @ b  # RuntimeError: size mismatch

# 正确:确保内部维度匹配
b = torch.randn(4, 5)
c = a @ b  # [3, 4] @ [4, 5] -> [3, 5]

2.1.4 索引、切片与重塑
python 复制代码
import torch

x = torch.arange(24).reshape(2, 3, 4)  # 形状: [2, 3, 4]
print("原始张量形状:", x.shape)

# ===== 索引 (Indexing) =====
print(x[0])        # 第一个"页",形状: [3, 4]
print(x[0, 1])     # 第一个"页"的第二行,形状: [4]
print(x[0, 1, 2])  # 单个元素: tensor(6)

# ===== 切片 (Slicing) =====
print(x[:, 0, :])  # 所有"页"的第一行,形状: [2, 4]
print(x[..., -1])  # 所有位置的最后一列,形状: [2, 3]

# ===== 高级索引 =====
# 布尔索引
mask = x > 10
print(x[mask])  # 所有大于10的元素(一维张量)

# 整数数组索引
indices = torch.tensor([0, 2])
print(x[:, indices, :])  # 选择第0和第2行

# ===== 重塑 (Reshaping) =====
# reshape:可能复制数据
reshaped = x.reshape(6, 4)  # [2, 3, 4] -> [6, 4]

# view:要求内存连续,不复制数据(更高效)
viewed = x.view(6, 4)

# 自动推断维度(用-1)
auto_shape = x.view(2, -1)  # [2, 3, 4] -> [2, 12]

# ===== 转置 (Transpose) =====
# 交换两个维度
transposed = x.transpose(0, 2)  # [2, 3, 4] -> [4, 3, 2]

# 转置最后两个维度(常用于注意力机制)
last_transpose = x.transpose(-1, -2)  # [2, 3, 4] -> [2, 4, 3]

# permute:重新排列所有维度
permuted = x.permute(2, 0, 1)  # [2, 3, 4] -> [4, 2, 3]

# ===== 维度操作 =====
# 增加维度
unsqueezed = x.unsqueeze(0)  # [2, 3, 4] -> [1, 2, 3, 4]

# 删除维度(仅限size为1的维度)
squeezed = unsqueezed.squeeze(0)  # [1, 2, 3, 4] -> [2, 3, 4]

# 展平为一维
flattened = x.flatten()  # [2, 3, 4] -> [24]

💡 reshape vs view 的区别

  • view() 要求内存连续,速度更快,但在某些操作(如 transpose)后会失败
  • reshape() 总是有效,必要时会复制数据
  • 推荐:优先用 view(),失败时再用 reshape()

2.1.5 CPU 与 GPU 之间自由迁移:.to(device)
python 复制代码
import torch

# ===== 检查设备可用性 =====
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
print(f"使用设备: {device}")

# ===== 创建张量并迁移 =====
# 方法1:创建后迁移
x = torch.randn(1000, 1000)
x = x.to(device)

# 方法2:创建时指定
x = torch.randn(1000, 1000, device=device)

# ===== 模型迁移示例 =====
import torch.nn as nn

class SimpleModel(nn.Module):
    def __init__(self):
        super().__init__()
        self.linear = nn.Linear(10, 1)
    
    def forward(self, x):
        return self.linear(x)

model = SimpleModel().to(device)

# ===== 训练循环中的设备管理 =====
for batch_idx, (data, target) in enumerate(dataloader):
    # 关键:确保数据和模型在同一设备上
    data = data.to(device)
    target = target.to(device)
    
    output = model(data)
    # ... 后续训练代码

# ===== 最佳实践:统一设备管理 =====
class DeviceManager:
    """统一管理设备的工具类"""
    
    @staticmethod
    def get_device(device_id=None):
        if device_id is not None:
            return torch.device(f'cuda:{device_id}')
        return torch.device('cuda' if torch.cuda.is_available() else 'cpu')
    
    @staticmethod
    def to_device(obj, device):
        """将模型/张量/列表迁移到设备"""
        if isinstance(obj, (list, tuple)):
            return [DeviceManager.to_device(item, device) for item in obj]
        return obj.to(device)

# 使用示例
device = DeviceManager.get_device()
model = DeviceManager.to_device(SimpleModel(), device)

注意:

python 复制代码
# ❌ 错误:模型在GPU,数据在CPU
model = model.to('cuda')
for data, target in dataloader:
    output = model(data)  # RuntimeError: Expected all tensors on same device

# 正确:确保一致
model = model.to('cuda')
for data, target in dataloader:
    data = data.to('cuda')
    output = model(data)

2. 2 :自动微分机制------深度学习的"求导引擎"

自动微分(Automatic Differentiation,简称 Autograd)

是一种能够自动计算函数导数 的技术。在深度学习中,它用于自动求出损失函数对模型参数的梯度

求导方式对比

方法 说明 缺点
手动求导 自己推公式,写代码实现 容易出错、改模型就得重推
数值微分 (f(x+h) - f(x))/h 近似 精度低、速度慢
符号微分 用代数软件(如 Mathematica)推导表达式 表达式爆炸、不实用
自动微分 精确、高效、运行时动态计算 深度学习的基石

在程序执行过程中精确地应用链式法则

  • 自动微分如何工作的?
    2 个核心思想:
  1. 分解复杂函数为基本操作
    任何复杂计算都可以拆成:加、乘、指数、激活函数等基本运算。
  2. 构建计算图 + 链式法则
    • PyTorch 在你运行代码时,动态记录每一步操作,形成一个"计算图"
    • 反向传播时,从输出开始,按图反向应用链式法则,逐层求导
2.2.1 requires_grad:何时开启梯度追踪?

梯度追踪就是告诉 PyTorch:'请记住我对这个张量做了哪些操作,将来我要算它的梯度。'

本质是构造有向无环图(DAG),每个节点是一个操作(如乘法),箭头表示数据流动。反向传播时,PyTorch 沿着这个图从 y 往回走,应用链式法则求梯度。

自动微分是 PyTorch 的"灵魂",但梯度追踪也有开销。理解 requires_grad 的使用场景至关重要。

python 复制代码
import torch

# ===== 默认行为 =====
x = torch.tensor([1.0, 2.0, 3.0])
print(x.requires_grad)  # False(默认不追踪)

# ===== 开启梯度追踪 =====
# 方法1:创建时指定
x = torch.tensor([1.0, 2.0, 3.0], requires_grad=True)

# 方法2:事后设置
x = torch.tensor([1.0, 2.0, 3.0])
x.requires_grad = True

# 方法3:使用 requires_grad_()
x = torch.tensor([1.0, 2.0, 3.0])
x.requires_grad_()

# ===== 梯度传播示例 =====
x = torch.tensor([1.0, 2.0, 3.0], requires_grad=True)
w = torch.tensor([0.5, 1.0, 1.5], requires_grad=True)

# 计算:y = x * w 的和
y = (x * w).sum()  # y 自动 requires_grad=True

print(f"y.requires_grad: {y.requires_grad}")  # True
print(f"y.grad_fn: {y.grad_fn}")              # <SumBackward0>

何时开启 requires_grad

场景 是否开启 原因
模型参数 需要通过反向传播更新
输入数据 数据是固定的,不需要计算梯度
中间变量 自动继承 如果任何输入 requires_grad=True,输出也会是
推理阶段 使用 torch.no_grad() 节省内存

2.2.2 backward() 与 grad:反向传播背后的原理

反向传播利用链式法则 (Chain Rule)自动计算损失函数对每一个模型参数的梯度。PyTorch 通过 loss.backward() 实现这一过程。

python 复制代码
import torch

# ===== 基础示例:标量反向传播 =====
x = torch.tensor([2.0], requires_grad=True)
y = x ** 2  # y = x²

# 反向传播
y.backward()

# 查看梯度
print(f"x.grad: {x.grad}")  # tensor([4.])(dy/dx = 2x = 2*2 = 4)

# ===== 多变量示例 =====
x = torch.tensor([1.0, 2.0, 3.0], requires_grad=True)
w = torch.tensor([0.1, 0.2, 0.3], requires_grad=True)
b = torch.tensor([0.5], requires_grad=True)

# 前向传播:y = (w·x + b)²
y = torch.sum((w * x + b) ** 2)

# 反向传播
y.backward()

print(f"w的梯度: {w.grad}")
print(f"b的梯度: {b.grad}")

# ===== 非标量需要指定 gradient 参数 =====
x = torch.randn(3, requires_grad=True)
y = x * 2  # y 是向量

# ❌ 错误:backward() 仅支持标量
# y.backward()  # RuntimeError

# ✅ 正确:指定 gradient 向量
y.backward(torch.ones_like(y))
print(f"x.grad: {x.grad}")  # tensor([2., 2., 2.])

为什么标量才能直接 backward?

反向传播计算的是 标量对各参数的导数 。如果输出是向量/矩阵,需要先聚合成标量(如 .sum().mean())。


2.2.3 动态计算图的魅力:每一次前向都是独立的图构建

这是 PyTorch 区别于 TensorFlow 1.x 的核心特性。

python 复制代码
import torch

def dynamic_network(x, use_complex_path):
    """根据条件动态选择计算路径"""
    if use_complex_path:
        # 复杂路径
        x = torch.sin(x)
        x = x ** 2
        x = torch.log(x + 1)
    else:
        # 简单路径
        x = x * 2
    return x.sum()

x = torch.tensor([1.0, 2.0, 3.0], requires_grad=True)

# 第一次:复杂路径
y1 = dynamic_network(x, use_complex_path=True)
y1.backward()
print(f"复杂路径梯度: {x.grad}")

# 第二次:简单路径(注意:需要清零梯度!)
x.grad.zero_()  # 清零
y2 = dynamic_network(x, use_complex_path=False)
y2.backward()
print(f"简单路径梯度: {x.grad}")

动态图的优势实例:

python 复制代码
# 示例:变长序列处理(RNN/Transformer)
def process_variable_length(sequences):
    """处理不同长度的序列"""
    outputs = []
    for seq in sequences:
        # 每个序列可以有不同长度!
        for token in seq:
            output = model(token)  # 动态构建计算图
            outputs.append(output)
    return torch.stack(outputs)

# 在 TensorFlow 1.x 中,这需要 tf.while_loop 和复杂的图定义

2.2.4 梯度清零的重要性:optimizer.zero_grad()

这里容易踩坑

python 复制代码
import torch
import torch.nn as nn

model = nn.Linear(10, 1)
optimizer = torch.optim.SGD(model.parameters(), lr=0.01)

x = torch.randn(5, 10)
target = torch.randn(5, 1)

# ===== 错误示例:不清零梯度 =====
for epoch in range(3):
    output = model(x)
    loss = ((output - target) ** 2).mean()
    
    # ❌ 忘记清零!
    loss.backward()
    
    print(f"Epoch {epoch}, 梯度范数: {model.weight.grad.norm().item():.4f}")
    # 输出会发现梯度越来越大(因为累积了)

# ===== 正确示例:每次迭代前清零 =====
for epoch in range(3):
    optimizer.zero_grad()  # ✅ 关键步骤
    
    output = model(x)
    loss = ((output - target) ** 2).mean()
    loss.backward()
    optimizer.step()

为什么 PyTorch 默认累积梯度?

梯度累积在某些场景下非常有用(如梯度累积训练大模型)。PyTorch 选择了"灵活性优先",把清零的责任交给用户。

python 复制代码
# 梯度累积示例(小batch模拟大batch)
accumulation_steps = 4

for i, (data, target) in enumerate(dataloader):
    output = model(data)
    loss = criterion(output, target)
    
    # 梯度累积
    loss = loss / accumulation_steps
    loss.backward()
    
    if (i + 1) % accumulation_steps == 0:
        optimizer.step()
        optimizer.zero_grad()  # 累积4步后才清零

2.2.5 with torch.no_grad() 推理加速
python 复制代码
import torch
import torch.nn as nn

model = nn.Linear(1000, 1000)
x = torch.randn(100, 1000)

# ===== 方法1:torch.no_grad() 上下文管理器 =====
with torch.no_grad():
    output = model(x)
    # 此处所有操作都不追踪梯度
    loss = (output ** 2).mean()

print(f"loss.requires_grad: {loss.requires_grad}")  # False

# ===== 方法2:@torch.no_grad() 装饰器 =====
@torch.no_grad()
def evaluate(model, dataloader):
    """评估函数"""
    total_loss = 0
    for data, target in dataloader:
        output = model(data)
        loss = criterion(output, target)
        total_loss += loss.item()
    return total_loss / len(dataloader)

# ===== 方法3:model.eval() + torch.no_grad() =====
model.eval()  # 切换到评估模式(影响 Dropout/BatchNorm)
with torch.no_grad():
    for data, target in test_loader:
        output = model(data)
        # 评估代码...

# ===== 性能对比 =====
import time

# 不使用 no_grad
start = time.time()
for _ in range(100):
    output = model(x)
    loss = (output ** 2).mean()
time_with_grad = time.time() - start

# 使用 no_grad
start = time.time()
with torch.no_grad():
    for _ in range(100):
        output = model(x)
        loss = (output ** 2).mean()
time_no_grad = time.time() - start

print(f"带梯度: {time_with_grad:.4f}s")
print(f"无梯度: {time_no_grad:.4f}s")
print(f"加速比: {time_with_grad / time_no_grad:.2f}x")

使用场景总结:

场景 使用方法 说明
模型评估 model.eval() + no_grad() 两者配合使用
推理部署 @torch.no_grad() 装饰器更简洁
数据预处理 with torch.no_grad(): 避免无意义的梯度计算
梯度调试 不使用 需要检查梯度时

三、高效数据流水线

3.1:数据加载与预处理

组件 作用
数据集 torch.utils.data.Dataset 定义数据的"源头" ------ 如何读取单个样本
数据加载器 torch.utils.data.DataLoader 把 Dataset 包装成可迭代的批量数据
预处理工具 torchvision.transforms(常用) 对图像等数据进行变换
3.1.1 自定义 Dataset 类:如何封装自己的数据?

Dataset 是 PyTorch 数据加载的基石,只需实现三个方法:

python 复制代码
import torch
from torch.utils.data import Dataset
import numpy as np

# 简单数值数据集 
class SimpleDataset(Dataset):
    """最简单的数据集实现"""
    
    def __init__(self, data, labels):
        self.data = data
        self.labels = labels
    
    def __len__(self):
        """返回数据集大小"""
        return len(self.data)
    
    def __getitem__(self, idx):
        """获取单个样本"""
        return self.data[idx], self.labels[idx]

# 使用示例
data = torch.randn(100, 10)  # 100个样本,每个10维
labels = torch.randint(0, 2, (100,))  # 二分类标签

dataset = SimpleDataset(data, labels)
print(f"数据集大小: {len(dataset)}")
sample, label = dataset[0]
print(f"第一个样本形状: {sample.shape}, 标签: {label}")

文件系统数据集

python 复制代码
import os
from PIL import Image
from torch.utils.data import Dataset

class ImageFolderDataset(Dataset):
    """从文件夹加载图像数据集"""
    
    def __init__(self, root_dir, transform=None):
        """
        Args:
            root_dir: 根目录路径(包含子文件夹,每个子文件夹代表一个类别)
            transform: 数据增强/预处理函数
        """
        self.root_dir = root_dir
        self.transform = transform
        self.samples = []
        self.class_to_idx = {}
        
        # 扫描文件夹
        for idx, class_name in enumerate(sorted(os.listdir(root_dir))):
            class_dir = os.path.join(root_dir, class_name)
            if not os.path.isdir(class_dir):
                continue
            
            self.class_to_idx[class_name] = idx
            
            # 收集图像路径
            for img_name in os.listdir(class_dir):
                if img_name.endswith(('.jpg', '.png', '.jpeg')):
                    self.samples.append((
                        os.path.join(class_dir, img_name),
                        idx
                    ))
    
    def __len__(self):
        return len(self.samples)
    
    def __getitem__(self, idx):
        img_path, label = self.samples[idx]
        
        # 加载图像
        image = Image.open(img_path).convert('RGB')
        
        # 应用变换
        if self.transform:
            image = self.transform(image)
        
        return image, label

LLM 专用:文本数据集

python 复制代码
class TextDataset(Dataset):
    """文本数据集(适用于语言模型预训练)"""
    
    def __init__(self, texts, tokenizer, max_length=512):
        """
        Args:
            texts: 文本列表
            tokenizer: 分词器(如 HuggingFace Tokenizer)
            max_length: 最大序列长度
        """
        self.texts = texts
        self.tokenizer = tokenizer
        self.max_length = max_length
    
    def __len__(self):
        return len(self.texts)
    
    def __getitem__(self, idx):
        text = self.texts[idx]
        
        # 分词
        encoding = self.tokenizer(
            text,
            max_length=self.max_length,
            padding='max_length',
            truncation=True,
            return_tensors='pt'
        )
        
        # 返回输入和标签(语言模型自回归任务)
        input_ids = encoding['input_ids'].squeeze(0)
        attention_mask = encoding['attention_mask'].squeeze(0)
        
        # 标签是输入向右移动一位
        labels = input_ids.clone()
        
        return {
            'input_ids': input_ids,
            'attention_mask': attention_mask,
            'labels': labels
        }

建议:

  • __init__:加载元数据(文件路径、索引),不要加载所有数据到内存
  • __getitem__:实时加载单个样本(支持多进程并行)
  • 将耗时操作(如图像解码)放在 __getitem__ 中,利用多进程加速

3.1.2 DataLoader 使用:batch_size、shuffle、num_workers 调优

DataLoader 是 PyTorch 的"数据泵",负责批处理、打乱、多进程加载。

python 复制代码
from torch.utils.data import DataLoader

# ===== 基础用法 =====
dataset = SimpleDataset(data, labels)

dataloader = DataLoader(
    dataset,
    batch_size=32,        # 每批32个样本
    shuffle=True,         # 训练时打乱
    num_workers=4,        # 4个子进程加载数据
    pin_memory=True,      # 固定内存(加速GPU传输)
    drop_last=False       # 最后一批不足batch_size时是否丢弃
)

# 迭代数据
for batch_idx, (data, labels) in enumerate(dataloader):
    print(f"Batch {batch_idx}: data shape {data.shape}, labels shape {labels.shape}")

参数调优指南:

参数 推荐值 说明 常见错误
batch_size 32/64/128 显存允许尽量大 太大导致 OOM,太小训练慢
shuffle 训练=True 验证=False 打乱避免过拟合 忘记验证集设为 False
num_workers CPU 核心数的一半 多进程并行加载 Windows 下设为 0
pin_memory GPU训练=True 加速 CPU→GPU 传输 CPU 训练时无用
drop_last 视情况 BatchNorm 等对 batch 敏感 忘记设置导致最后一批问题

性能调优:

python 复制代码
import time
from torch.utils.data import Dataset, DataLoader
import torch

class SlowDataset(Dataset):
    """模拟耗时的数据加载"""
    def __init__(self, size=1000):
        self.size = size
    
    def __len__(self):
        return self.size
    
    def __getitem__(self, idx):
        # 模拟耗时操作(如图像解码)
        time.sleep(0.01)
        return torch.randn(10), torch.tensor(idx % 10)

dataset = SlowDataset(1000)

# 测试不同 num_workers
for num_workers in [0, 2, 4, 8]:
    dataloader = DataLoader(dataset, batch_size=32, num_workers=num_workers)
    
    start = time.time()
    for batch in dataloader:
        pass  # 模拟训练
    elapsed = time.time() - start
    
    print(f"num_workers={num_workers}: {elapsed:.2f}s")

# 典型输出:
# num_workers=0: 10.50s  ← 单进程,慢
# num_workers=2: 5.80s   ← 2个进程,快2倍
# num_workers=4: 3.20s   ← 4个进程,接近4倍
# num_workers=8: 3.10s   ← 8个进程,提升不明显(瓶颈在磁盘IO)

3.1.3 数据增强策略简介(LLM 中已被 Tokenizer 替代)

说明 :在计算机视觉中,数据增强(随机裁剪、翻转、颜色变换)是提升模型泛化能力的关键。但在 LLM/NLP 领域 ,传统的数据增强(如同义词替换、回译)已逐渐被 Tokenizer 的标准化处理更大规模的数据替代。

CV 数据增强示例(供参考):

python 复制代码
from torchvision import transforms

# 定义变换流程
transform = transforms.Compose([
    transforms.RandomResizedCrop(224),       # 随机裁剪到224x224
    transforms.RandomHorizontalFlip(),       # 50%概率水平翻转
    transforms.ColorJitter(0.4, 0.4, 0.4),   # 颜色抖动
    transforms.ToTensor(),                   # 转为张量 [0, 1]
    transforms.Normalize(                     # 标准化
        mean=[0.485, 0.456, 0.406],
        std=[0.229, 0.224, 0.225]
    )
])

# 在 Dataset 中使用
class ImageDataset(Dataset):
    def __init__(self, image_paths, labels, transform=None):
        self.image_paths = image_paths
        self.labels = labels
        self.transform = transform
    
    def __getitem__(self, idx):
        image = Image.open(self.image_paths[idx])
        if self.transform:
            image = self.transform(image)
        return image, self.labels[idx]

NLP/LLM 的"数据增强"方式:

  1. 更多数据:预训练语料从 GB 到 TB 级别
  2. Tokenizer 标准化:小写化、标点处理、子词分割
  3. 上下文窗口:随机截取不同位置的文本片段
  4. 混合数据集:代码 + 书籍 + 网页 + 对话

3.1.4 多进程加载陷阱与解决方案(Windows/macOS 注意事项)

常见错误 1:Windows 下多进程死锁

python 复制代码
# 错误代码(Windows 下会卡死)
if __name__ == '__main__':
    dataset = MyDataset()
    dataloader = DataLoader(dataset, num_workers=4)  # Windows 下会fork失败
    for batch in dataloader:
        pass

# 正确代码
if __name__ == '__main__':  # 必须加这个保护!
    dataset = MyDataset()
    dataloader = DataLoader(dataset, num_workers=4)
    for batch in dataloader:
        pass

💡Windows 不支持 fork(),使用 spawn() 启动子进程。子进程需要重新导入主模块,如果没有 if __name__ == '__main__': 保护,会无限递归创建子进程。

常见错误 2:自定义 Dataset 中的全局变量

python 复制代码
# 全局变量无法跨进程共享
GLOBAL_CACHE = {}

class CachedDataset(Dataset):
    def __getitem__(self, idx):
        if idx in GLOBAL_CACHE:  # 子进程看不到主进程的缓存!
            return GLOBAL_CACHE[idx]
        data = expensive_load(idx)
        GLOBAL_CACHE[idx] = data
        return data

#  使用实例变量 + 每个worker独立缓存
class CachedDataset(Dataset):
    def __init__(self):
        self._cache = {}  # 每个worker都有独立的缓存
    
    def __getitem__(self, idx):
        if idx not in self._cache:
            self._cache[idx] = expensive_load(idx)
        return self._cache[idx]

常见错误 3:CUDA 初始化在主进程

python 复制代码
# 在主进程初始化 CUDA,子进程会报错
device = torch.device('cuda')

class MyDataset(Dataset):
    def __getitem__(self, idx):
        return torch.randn(10).to(device)  # RuntimeError!

# 正确写法:数据加载在 CPU,迁移 GPU 在训练循环
class MyDataset(Dataset):
    def __getitem__(self, idx):
        return torch.randn(10)  # CPU 张量

# 训练循环中迁移
for batch in dataloader:
    batch = batch.to('cuda')  # 主进程负责迁移

3.1.5 【拓展】使用 TorchData 构建可复用的数据管道

TorchData 是 PyTorch 官方推出的新数据加载库,引入"数据管道"概念,更灵活、可组合,官方也在主推

特性 Dataset + DataLoader(原生) TorchData(高级)
所属 PyTorch 核心包 torch.utils.data 独立库torchdata,PyTorch 官方项目)
定位 基础构建块("零件级") 高级数据流水线("发动机级")
编程范式 面向对象(继承 Dataset) 函数式 + 流式 pipeline.map, .shuffle
性能优化 基础多进程(num_workers 更智能的预取、分片、缓存
易用性 简单任务够用,复杂时代码冗长 更清晰、可组合、适合大规模数据
兼容性 所有 PyTorch 项目都支持 需额外安装 torchdata
成熟度 极成熟,广泛使用 较新(2022 年起推出),逐步推广中
python 复制代码
# 安装:pip install torchdata

from torchdata.datapipes.iter import IterableWrapper, Mapper, Shuffler

# ===== 基础示例:链式操作 =====
# 1. 创建数据源
dp = IterableWrapper([1, 2, 3, 4, 5])

# 2. 映射操作
dp = dp.map(lambda x: x ** 2)  # [1, 4, 9, 16, 25]

# 3. 过滤操作
dp = dp.filter(lambda x: x > 10)  # [16, 25]

# 4. 打乱
dp = dp.shuffle()

# 使用
for item in dp:
    print(item)

# ===== 实战:文件加载管道 =====
from torchdata.datapipes.iter import FileLister, FileOpener

# 1. 列出所有图像文件
dp = FileLister('data/images', masks='*.jpg')

# 2. 打开文件
dp = FileOpener(dp, mode='rb')

# 3. 解码图像
def decode_image(path_and_stream):
    path, stream = path_and_stream
    from PIL import Image
    return Image.open(stream).convert('RGB')

dp = dp.map(decode_image)

# 4. 预处理
from torchvision import transforms
transform = transforms.Compose([
    transforms.Resize(224),
    transforms.ToTensor()
])
dp = dp.map(transform)

# 5. 批处理
dp = dp.batch(32)

# 转换为 DataLoader
dataloader = DataLoader(dp, batch_size=None)  # batch_size=None(已经批处理过)

TorchData 的优势:

  1. 流式处理:像流水线一样逐步处理数据,内存更省、延迟更低。
  2. 函数式编程 :用 .map().filter().shuffle() 等链式操作,代码更清晰、易读易测。
  3. 高性能:支持智能预取、缓存、分片,提升 GPU 利用率。
  4. 分布式友好 :自动处理多卡/多机训练的数据分片,无需手动写 DistributedSampler
  5. 与 WebDataset 集成好 :原生支持从 .tar 压缩包中高效读取大规模数据(如图文对)。
  6. 模块化强:每步可复用、可测试,适合复杂数据流程。

建议先掌握 Dataset + DataLoader,进阶后再探索 TorchData。

相关推荐
跨境卫士—小依2 小时前
TikTok Shop 进化全解析,从内容驱动到品牌共建,抢占跨境新赛道
大数据·人工智能·跨境电商·亚马逊·防关联
一瞬祈望2 小时前
ResNet50 图像分类完整实战(Notebook Demo + 训练代码)
人工智能·python·神经网络·数据挖掘
其美杰布-富贵-李2 小时前
PyTorch Lightning Callback 指南
人工智能·pytorch·python·回调函数·callback
_codemonster2 小时前
python易混淆知识点(十六)lambda表达式
开发语言·python
Mintopia2 小时前
🤖 2025 年的人类还需要 “Prompt 工程师” 吗?
人工智能·llm·aigc
agicall.com2 小时前
实时语音转文字设备在固话座机中的重要价值
人工智能·语音识别
aitoolhub2 小时前
AI生成圣诞视觉图:从节日元素到创意落地的路径
人工智能·深度学习·自然语言处理·节日
神州问学2 小时前
除了 DeepSeek-OCR,还有谁在“把字当图看”?
人工智能
Mintopia2 小时前
意图驱动编程(Intent-Driven Programming)
人工智能·llm·aigc