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 的身影。
为什么会这样? 三个关键原因:
- 动态计算图:代码即模型,所见即所得
- Pythonic 设计:符合 Python 开发者直觉,学习曲线平滑
- 开放生态:与 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 个核心思想:
- 分解复杂函数为基本操作
任何复杂计算都可以拆成:加、乘、指数、激活函数等基本运算。 - 构建计算图 + 链式法则
- 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 的"数据增强"方式:
- 更多数据:预训练语料从 GB 到 TB 级别
- Tokenizer 标准化:小写化、标点处理、子词分割
- 上下文窗口:随机截取不同位置的文本片段
- 混合数据集:代码 + 书籍 + 网页 + 对话
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 的优势:
- 流式处理:像流水线一样逐步处理数据,内存更省、延迟更低。
- 函数式编程 :用
.map()、.filter()、.shuffle()等链式操作,代码更清晰、易读易测。 - 高性能:支持智能预取、缓存、分片,提升 GPU 利用率。
- 分布式友好 :自动处理多卡/多机训练的数据分片,无需手动写
DistributedSampler。 - 与 WebDataset 集成好 :原生支持从
.tar压缩包中高效读取大规模数据(如图文对)。 - 模块化强:每步可复用、可测试,适合复杂数据流程。
建议先掌握 Dataset + DataLoader,进阶后再探索 TorchData。