PyTorch核心概念详解:从基石到实践
PyTorch 作为当今最受欢迎的深度学习框架之一,凭借其灵活性、易用性以及强大的社区支持,在学术界和工业界都得到了广泛应用。无论是初学者想要入门深度学习,还是经验丰富的研究人员探索前沿算法,理解 PyTorch 的核心概念都是至关重要的第一步。本文将深入探讨 PyTorch 的基石组件和核心思想,帮助读者构建一个坚实的基础,从而更自信地驾驭这个强大的工具。
一、 引言:为什么选择 PyTorch?
在深入技术细节之前,我们先简单了解一下 PyTorch 脱颖而出的原因:
- Pythonic 与易用性: PyTorch 的 API 设计紧密贴合 Python 的编程习惯,使得熟悉 Python 的开发者能够快速上手。其直观的语法和清晰的结构降低了学习曲线。
- 动态计算图 (Dynamic Computational Graph): 这是 PyTorch 最显著的特点之一。与 TensorFlow 1.x 等框架使用的静态计算图不同,PyTorch 的计算图是在运行时动态构建的。这意味着你可以使用标准的 Python 控制流(如循环和条件判断)来构建和修改模型结构,极大地增强了灵活性,特别是在处理变长输入(如 NLP 任务)或进行复杂的模型调试时。
- 强大的 GPU 加速: PyTorch 提供了无缝的 CUDA 支持,可以轻松地将计算密集型任务转移到 NVIDIA GPU 上执行,显著加速模型训练和推理过程。
- 丰富的生态系统: 围绕 PyTorch 已经形成了一个庞大且活跃的生态系统,包括用于计算机视觉的
TorchVision
、处理文本的TorchText
、处理音频的TorchAudio
,以及大量的预训练模型、工具库和研究项目。 - 学术界与研究友好: 由于其灵活性,PyTorch 在学术界备受青睐,许多最新的研究成果都是使用 PyTorch 实现的。
理解了 PyTorch 的优势后,让我们正式开始探索其核心概念。
二、 Tensor (张量):PyTorch 的数据基石
在 PyTorch 中,所有的数据都通过 Tensor 来表示。可以将其理解为一个多维数组,类似于 NumPy 的 ndarray
,但具备在 GPU 上高效运算的能力,并且是构建计算图和自动求导的基础。
-
创建 Tensor:
-
可以直接从 Python 列表或 NumPy 数组创建:
“`python
import torch
import numpy as np从列表创建
data = [[1, 2], [3, 4]]
x_data = torch.tensor(data)从 NumPy 数组创建 (共享内存)
np_array = np.array(data)
x_np = torch.from_numpy(np_array)创建指定形状和类型的 Tensor
x_zeros = torch.zeros(2, 3) # 全零
x_ones = torch.ones(2, 3, dtype=torch.float16) # 全一,指定数据类型
x_rand = torch.randn(2, 3) # 标准正态分布随机数
* **Tensor 的属性:**
python
* `shape` 或 `size()`: 返回 Tensor 的维度(形状)。
* `dtype`: 返回 Tensor 中元素的数据类型 (e.g., `torch.float32`, `torch.int64`)。
* `device`: 返回 Tensor 所在的设备 (e.g., `cpu`, `cuda:0`)。
* **Tensor 操作:** PyTorch 提供了极其丰富的 Tensor 操作函数,涵盖了数学运算、线性代数、索引、切片、变形等。
tensor = torch.ones(4, 4)
# 索引和切片 (类似 NumPy)
print(f”First row: {tensor[0]}”)
print(f”First column: {tensor[:, 0]}”)
print(f”Last column: {tensor[…, -1]}”)
tensor[:,1] = 0 # 就地修改
拼接
t1 = torch.cat([tensor, tensor, tensor], dim=1) # 按列拼接
矩阵乘法
y1 = tensor @ tensor.T # .T 是转置
y2 = torch.matmul(tensor, tensor.T)逐元素运算
z1 = tensor * tensor
z2 = torch.mul(tensor, tensor)
z3 = torch.add(tensor, 1) # 加法
* **NumPy 桥:** PyTorch Tensor 可以轻松地与 NumPy 数组相互转换。
python
* `tensor.numpy()`: 将 CPU 上的 Tensor 转换为 NumPy 数组(共享内存)。
* `torch.from_numpy(ndarray)`: 将 NumPy 数组转换为 PyTorch Tensor(共享内存)。
* **注意:** 共享内存意味着修改其中一个会影响另一个。如果 Tensor 在 GPU 上,需要先用 `.cpu()` 方法将其移到 CPU。
* **GPU 加速:** 将 Tensor 移动到 GPU 是实现加速的关键。
if torch.cuda.is_available():
device = torch.device(“cuda”) # 获取 CUDA 设备
tensor = tensor.to(device) # 将 Tensor 移动到 GPU
print(f”Tensor is on device: {tensor.device}”)
else:
device = torch.device(“cpu”)
print(“CUDA not available, using CPU.”)
“`
在 GPU 上的 Tensor 进行的运算将自动在 GPU 上执行。 -
三、 Autograd:自动微分引擎
Autograd 是 PyTorch 的核心功能之一,它为 Tensor 上的所有操作提供了自动微分能力。这是训练神经网络(本质上是利用梯度下降优化参数)的关键。
- 计算图 (Computational Graph): PyTorch 使用动态计算图。当你对设置了
requires_grad=True
的 Tensor 执行操作时,PyTorch 会自动记录这些操作,构建一个由Function
对象组成的有向无环图 (DAG)。叶子节点是输入 Tensor,根节点是输出 Tensor。每个非叶子节点代表一个操作(Function
),并保存了其对应的梯度计算方法。 requires_grad
属性:- 一个 Tensor 的
requires_grad
属性默认为False
。 - 如果希望 PyTorch 跟踪对某个 Tensor 的操作并计算梯度,需要将其
requires_grad
设置为True
。通常,模型的参数(权重和偏置)会自动设置requires_grad=True
。
python
x = torch.ones(2, 2, requires_grad=True)
print(x.requires_grad) # Output: True
y = x + 2
print(y.requires_grad) # Output: True (由 requires_grad=True 的 Tensor 计算得到)
z = y * y * 3
out = z.mean()
print(out.requires_grad) # Output: True
- 一个 Tensor 的
grad_fn
属性:- 如果一个 Tensor 是由用户直接创建的(叶子节点),它的
grad_fn
为None
。 - 如果一个 Tensor 是某个操作的结果,且该操作的任何输入 Tensor 的
requires_grad=True
,那么这个 Tensor 会有一个grad_fn
属性。该属性指向创建这个 Tensor 的Function
对象(例如AddBackward0
,MulBackward0
,MeanBackward0
),用于反向传播时计算梯度。
python
print(x.grad_fn) # Output: None (用户创建)
print(y.grad_fn) # Output: <AddBackward0 object at 0x...>
print(out.grad_fn)# Output: <MeanBackward0 object at 0x...>
- 如果一个 Tensor 是由用户直接创建的(叶子节点),它的
-
.backward()
方法:- 当你在一个标量 Tensor(例如损失函数
loss
)上调用.backward()
时,Autograd 会从这个标量开始,沿着计算图反向传播,计算图中所有requires_grad=True
的叶子节点相对于该标量的梯度。 - 计算得到的梯度会累加到对应 Tensor 的
.grad
属性中。
“`python
out.backward() # 计算梯度
查看 x 的梯度 d(out)/dx
print(x.grad)
Output:
tensor([[4.5000, 4.5000],
[4.5000, 4.5000]])
这里的梯度计算过程大致是:`out = (1/4) * Σ z_i`,`z_i = 3 * y_i^2`,`y_i = x_i + 2`。 `d(out)/dz_i = 1/4`。`d(z_i)/dy_i = 6 * y_i`。`d(y_i)/dx_i = 1`。 根据链式法则,`d(out)/dx_i = d(out)/dz_i * d(z_i)/dy_i * d(y_i)/dx_i = (1/4) * (6 * y_i) * 1 = (3/2) * y_i`。 由于 `x` 初始是全1矩阵,`y = x + 2` 是全3矩阵,所以 `d(out)/dx_i = (3/2) * 3 = 4.5`。
python
* **梯度累加与 `.zero_grad()`:**
* PyTorch 的梯度是**累加 (accumulate)** 的。这意味着如果你连续多次调用 `.backward()` 而不清除梯度,新的梯度会加到 `.grad` 属性上。
* 在训练循环中,每个 batch 开始优化之前,必须手动清除上一步计算的梯度,否则梯度会错误地累积。这通常通过优化器(Optimizer)的 `.zero_grad()` 方法完成。
* **禁用梯度跟踪:**
* 有时我们不需要计算梯度,例如在模型评估(Inference)或更新模型参数之后。禁用梯度跟踪可以减少内存消耗并加速计算。
* 使用 `torch.no_grad()` 上下文管理器:
with torch.no_grad():
# 在这个块内的所有计算都不会被跟踪
y = x + 2
print(y.requires_grad) # Output: False
* 使用 Tensor 的 `.detach()` 方法:创建一个新的 Tensor,它与原始 Tensor 共享数据,但不参与梯度计算。
python
x_detached = x.detach()
print(x_detached.requires_grad) # Output: False
“` - 当你在一个标量 Tensor(例如损失函数
四、 torch.nn
:构建神经网络的核心模块
torch.nn
模块是 PyTorch 构建神经网络的核心。它提供了定义网络层、损失函数以及容器的类。
nn.Module
:- 所有神经网络模块的基类。自定义的网络层或整个模型都应该继承自
nn.Module
。 - 核心方法:
__init__()
: 定义模型的层(如卷积层、线性层)和其他组件。在这里实例化的nn.Module
子模块(如nn.Linear
,nn.Conv2d
)会被自动注册。forward()
: 定义数据通过网络的前向传播逻辑。输入数据,返回输出。当你调用model(input_data)
时,实际上是调用了model.forward(input_data)
。
nn.Module
会自动管理其包含的参数 (nn.Parameter
)。nn.Parameter
是一种特殊的 Tensor,当它被赋值给nn.Module
的属性时,会自动设置requires_grad=True
并被添加到模块的参数列表(可通过module.parameters()
或module.named_parameters()
访问)。
- 所有神经网络模块的基类。自定义的网络层或整个模型都应该继承自
- 常用的层 (
nn.Layer
):nn.Linear(in_features, out_features)
: 全连接层(密集层)。nn.Conv2d(in_channels, out_channels, kernel_size, stride=1, padding=0)
: 二维卷积层。类似地还有nn.Conv1d
,nn.Conv3d
。nn.MaxPool2d(kernel_size, stride=None, padding=0)
: 二维最大池化层。类似地还有nn.AvgPool2d
。nn.ReLU()
,nn.Sigmoid()
,nn.Tanh()
,nn.Softmax(dim=None)
: 常用的激活函数。nn.BatchNorm2d(num_features)
,nn.Dropout(p=0.5)
: 正则化层。nn.Embedding(num_embeddings, embedding_dim)
: 词嵌入层,常用于 NLP。nn.LSTM()
,nn.GRU()
: 循环神经网络层。
- 容器 (
nn.Container
):nn.Sequential
: 一个顺序容器。模块将按照它们在构造函数中传递的顺序添加到容器中。输入数据会依次通过每个模块。
python
model = nn.Sequential(
nn.Linear(784, 128),
nn.ReLU(),
nn.Linear(128, 10)
)
-
自定义
nn.Module
:
“`python
import torch.nn as nn
import torch.nn.functional as F # 函数式接口,通常无参数class SimpleNet(nn.Module):
def init(self):
super(SimpleNet, self).init() # 必须调用父类构造函数
# 定义层
self.conv1 = nn.Conv2d(1, 32, kernel_size=3, stride=1, padding=1)
self.relu1 = nn.ReLU()
self.pool1 = nn.MaxPool2d(kernel_size=2, stride=2)
self.conv2 = nn.Conv2d(32, 64, kernel_size=3, stride=1, padding=1)
self.relu2 = nn.ReLU()
self.pool2 = nn.MaxPool2d(kernel_size=2, stride=2)
# 假设输入是 28×28 图像,经过两次 2×2 池化后变为 7×7
self.fc1 = nn.Linear(64 * 7 * 7, 128)
self.relu3 = nn.ReLU()
self.fc2 = nn.Linear(128, 10) # 输出 10 个类别def forward(self, x): # 定义前向传播逻辑 x = self.pool1(self.relu1(self.conv1(x))) x = self.pool2(self.relu2(self.conv2(x))) x = x.view(-1, 64 * 7 * 7) # 展平特征图 x = self.relu3(self.fc1(x)) x = self.fc2(x) # 通常最后一层不加激活,留给损失函数处理(如 CrossEntropyLoss) return x
实例化模型
net = SimpleNet()
print(net) # 打印模型结构打印模型参数
for name, param in net.named_parameters():
if param.requires_grad:
print(name, param.data.shape)
“`
五、 torch.optim
:优化算法库
在计算出损失(Loss)和梯度之后,我们需要一种机制来根据梯度更新模型的参数(权重和偏置),以最小化损失。这就是优化器(Optimizer)的作用。torch.optim
模块提供了各种常用的优化算法。
- 常用的优化器:
optim.SGD
: 随机梯度下降(Stochastic Gradient Descent),可以附加动量(momentum)、权重衰减(weight decay)等。optim.Adam
: Adam 优化器,一种自适应学习率的优化算法,通常收敛速度较快,是目前广泛使用的优化器之一。optim.RMSprop
: RMSprop 优化器,也是一种自适应学习率算法。optim.Adagrad
,optim.AdamW
等。
-
使用方法:
- 创建优化器实例,传入需要优化的模型参数 (
model.parameters()
) 和学习率 (lr
) 等超参数。 - 在训练循环中:
optimizer.zero_grad()
: 清除上一步遗留的梯度。必须在计算损失和调用.backward()
之前执行。loss.backward()
: 计算损失相对于模型参数的梯度。optimizer.step()
: 根据计算出的梯度和优化算法的规则,更新模型参数。
“`python
import torch.optim as optim假设 net 是我们定义的 SimpleNet 实例
定义损失函数和优化器
criterion = nn.CrossEntropyLoss() # 交叉熵损失,常用于多分类
optimizer = optim.Adam(net.parameters(), lr=0.001)— 模拟训练循环中的一步 —
假设 inputs 和 labels 是一个 batch 的数据
inputs = torch.randn(64, 1, 28, 28) # 假设 batch size = 64
labels = torch.randint(0, 10, (64,))1. 清零梯度
optimizer.zero_grad()
2. 前向传播
outputs = net(inputs)
3. 计算损失
loss = criterion(outputs, labels)
4. 反向传播计算梯度
loss.backward()
5. 更新参数
optimizer.step()
print(f”Loss: {loss.item()}”) # .item() 获取标量 Tensor 的 Python 数值
— 训练循环结束 —
“`
- 创建优化器实例,传入需要优化的模型参数 (
六、 损失函数 (Loss Functions):衡量模型表现
损失函数用于衡量模型预测值与真实标签之间的差异。训练的目标就是最小化损失函数的值。损失函数通常也定义在 torch.nn
模块中。
-
常用的损失函数:
nn.MSELoss
: 均方误差损失(Mean Squared Error Loss),常用于回归任务。loss = mean((input - target)^2)
。nn.CrossEntropyLoss
: 交叉熵损失。它内部结合了nn.LogSoftmax
和nn.NLLLoss
(负对数似然损失),非常适合用于多分类任务。输入通常是未经 Softmax 的原始 logits,目标是类别索引。nn.BCELoss
: 二元交叉熵损失(Binary Cross Entropy Loss),用于二分类任务。输入需要先经过 Sigmoid 激活。nn.BCEWithLogitsLoss
: 结合了 Sigmoid 和 BCELoss,比分开使用更数值稳定,用于二分类任务,输入是原始 logits。nn.L1Loss
: L1 损失(Mean Absolute Error Loss),也用于回归任务。
-
使用: 损失函数通常是一个可调用的对象。将模型的输出和目标标签传入即可计算损失值(一个标量 Tensor)。
“`python
# 回归任务示例
output_reg = torch.randn(10, 1, requires_grad=True)
target_reg = torch.randn(10, 1)
loss_fn_mse = nn.MSELoss()
loss_mse = loss_fn_mse(output_reg, target_reg)
print(f”MSE Loss: {loss_mse.item()}”)分类任务示例 (已在优化器部分展示)
output_cls = torch.randn(64, 10, requires_grad=True) # 64 个样本,10 个类别 logits
target_cls = torch.randint(0, 10, (64,)) # 目标类别索引
loss_fn_ce = nn.CrossEntropyLoss()
loss_ce = loss_fn_ce(output_cls, target_cls)
print(f”Cross Entropy Loss: {loss_ce.item()}”)
“`
七、 数据加载与处理 (torch.utils.data
)
高效地加载和预处理数据对于训练深度学习模型至关重要,尤其是当数据集非常庞大无法一次性载入内存时。torch.utils.data
模块提供了两个关键类:Dataset
和 DataLoader
。
-
Dataset
:- 一个抽象类,代表一个数据集。你需要继承它并实现两个核心方法:
__len__()
: 返回数据集的大小(样本数量)。__getitem__(idx)
: 根据索引idx
获取并返回一个样本(通常是数据和标签组成的元组或字典)。这里可以包含数据的读取、预处理(如图像变换、文本分词)等逻辑。
“`python
from torch.utils.data import Dataset, DataLoader
from torchvision import transforms # 常用图像变换
class CustomImageDataset(Dataset):
def init(self, annotations_file, img_dir, transform=None, target_transform=None):
# self.img_labels = pd.read_csv(annotations_file) # 加载标签
self.img_dir = img_dir
self.transform = transform # 数据预处理
self.target_transform = target_transform # 标签预处理
# … 初始化,加载文件列表等 …
self.image_files = […] # 假设这是图像文件路径列表
self.labels = […] # 假设这是对应的标签列表def __len__(self): return len(self.image_files) # 返回数据集大小 def __getitem__(self, idx): img_path = os.path.join(self.img_dir, self.image_files[idx]) # image = read_image(img_path) # 读取图像 (e.g., using PIL) image = ... # 加载图像数据 label = self.labels[idx] if self.transform: image = self.transform(image) # 应用图像变换 if self.target_transform: label = self.target_transform(label) # 应用标签变换 return image, label # 返回一个样本
``
DataLoader
* **:**
Dataset
* 接收一个对象,并提供一个可迭代对象,用于按批次 (batch) 加载数据。
shuffle=True
* **核心功能:**
* **批处理 (Batching):** 将多个样本组合成一个 batch。
* **打乱 (Shuffling):** 在每个 epoch 开始时打乱数据顺序,有助于模型泛化 ()。
num_workers > 0
* **并行加载 (Multiprocessing):** 使用多个子进程并行加载数据,避免数据加载成为训练瓶颈 ()。
collate_fn`)。
* **自定义 Collate Function:** 定义如何将单个样本组合成一个 batch (“`python
假设我们有一个 CustomImageDataset 实例
train_dataset = CustomImageDataset(…)
创建 DataLoader
train_dataloader = DataLoader(
dataset=train_dataset,
batch_size=64, # 每个 batch 的样本数
shuffle=True, # 每个 epoch 打乱数据
num_workers=4 # 使用 4 个子进程加载数据 (根据 CPU 核心数调整)
)在训练循环中使用 DataLoader
num_epochs = 10
for epoch in range(num_epochs):
print(f”Epoch {epoch+1}\n——————————-“)
for batch, (X, y) in enumerate(train_dataloader):
# 将数据移动到正确的设备 (CPU or GPU)
X, y = X.to(device), y.to(device)# ... 模型训练步骤 (前向传播, 计算损失, 反向传播, 更新参数) ... if batch % 100 == 0: loss, current = loss.item(), batch * len(X) print(f"loss: {loss:>7f} [{current:>5d}/{len(train_dataset):>5d}]")
“`
- 一个抽象类,代表一个数据集。你需要继承它并实现两个核心方法:
八、 模型保存与加载
训练好的模型需要被保存下来,以便后续进行评估、部署或继续训练。PyTorch 提供了方便的接口来保存和加载模型状态。
-
保存/加载整个模型:
“`python
# 保存
torch.save(model, ‘model.pth’)加载
model = torch.load(‘model.pth’)
model.eval() # 设置为评估模式
这种方法简单直接,但不够灵活,可能在代码重构或不同版本的 PyTorch 之间存在兼容性问题。
python
* **保存/加载模型的状态字典 (State Dictionary) - 推荐方式:**
* 状态字典 (`state_dict`) 是一个 Python 字典,它将模型的每个层映射到其可学习的参数(权重和偏置)张量。只保存模型的参数,不保存模型结构。保存 state_dict
torch.save(model.state_dict(), ‘model_state_dict.pth’)
加载 state_dict
1. 先创建模型实例 (需要有模型的类定义)
model = SimpleNet() # 或者你的模型类
2. 加载 state_dict
model.load_state_dict(torch.load(‘model_state_dict.pth’))
3. 设置模式 (训练或评估)
model.eval() # 如果用于推理
model.train() # 如果要继续训练
这种方式更推荐,因为它更轻量、更灵活,也更容易迁移。
python
* **保存包含优化器状态等的检查点 (Checkpoint):**
在训练过程中,除了模型参数,可能还需要保存优化器的状态(如学习率、Adam 的动量信息等)、当前的 epoch、损失等,以便中断后能完全恢复训练。保存 Checkpoint
checkpoint = {
‘epoch’: epoch,
‘model_state_dict’: model.state_dict(),
‘optimizer_state_dict’: optimizer.state_dict(),
‘loss’: loss,
# … 其他需要保存的信息
}
torch.save(checkpoint, ‘checkpoint.pth’)加载 Checkpoint
model = SimpleNet()
optimizer = optim.Adam(model.parameters()) # 先创建实例checkpoint = torch.load(‘checkpoint.pth’)
model.load_state_dict(checkpoint[‘model_state_dict’])
optimizer.load_state_dict(checkpoint[‘optimizer_state_dict’])
epoch = checkpoint[‘epoch’]
loss = checkpoint[‘loss’]model.train() # 或者 model.eval()
“`
九、 总结与展望
本文详细介绍了 PyTorch 的几个核心概念:
- Tensor: 多维数组,PyTorch 的基本数据单元,支持 GPU 加速。
- Autograd: 自动微分引擎,通过动态计算图实现梯度的自动计算。
torch.nn
: 构建神经网络的核心模块,提供了nn.Module
基类、各种网络层和损失函数。torch.optim
: 优化算法库,用于根据梯度更新模型参数。- 损失函数: 衡量模型预测与真实值差异的标准。
torch.utils.data
: 高效加载和处理数据集的工具 (Dataset
和DataLoader
)。- 模型保存与加载: 持久化模型状态的方法。
这些概念是相互关联、协同工作的。Tensor 是数据流的基础;Autograd 在 Tensor 操作之上构建计算图并计算梯度;nn.Module
定义了包含可学习参数(也是 Tensor)的网络结构;损失函数计算出一个标量 Tensor;torch.optim
利用 Autograd 计算出的梯度来更新 nn.Module
中的参数;DataLoader
则负责高效地将数据(Tensor)喂给模型。
掌握了这些核心概念,你就拥有了使用 PyTorch 构建、训练和评估深度学习模型的坚实基础。当然,PyTorch 的世界远不止于此,还有分布式训练、模型部署(TorchServe, ONNX)、JIT 编译(TorchScript)、更高级的 Autograd 功能以及庞大的生态系统等待探索。但理解这些核心构件,无疑是开启 PyTorch 深度学习之旅最重要的一步。希望本文能为你提供清晰的指引,祝你在 PyTorch 的学习和实践中取得成功!