← 返回深度学习目录
← 返回学习笔记首页
PyTorch张量与自动求导
深度学习专题 · PyTorch的核心计算引擎
专题: 深度学习系统学习
关键词: 深度学习, PyTorch, 张量, Autograd, 自动求导, 计算图, CUDA, backward, 梯度
一、张量基础
张量(Tensor)是PyTorch中最核心的数据结构,可以理解为多维数组的泛化。标量是零维张量,向量是一维张量,矩阵是二维张量,而图像、视频等数据则需要更高维的张量来表示。PyTorch的张量设计在接口和使用习惯上与NumPy高度一致,这使得从NumPy迁移到PyTorch的学习成本非常低。但PyTorch张量相比NumPy数组有两个关键优势:一是支持GPU加速计算,二是集成了自动求导机制,这两者共同构成了深度学习训练的基石。
1.1 张量创建方式
PyTorch提供了极为丰富的张量创建API,覆盖了从基础创建到专业初始化的大部分场景。理解这些创建方法的适用场景,能够帮助我们在实际编码中选择最合适的工具。
import torch
# 从Python列表直接创建
a = torch.tensor([[1, 2, 3], [4, 5, 6]])
# tensor([[1, 2, 3],
# [4, 5, 6]])
# 全零张量(常用于初始化偏置)
b = torch.zeros(3, 4)
# tensor([[0., 0., 0., 0.],
# [0., 0., 0., 0.],
# [0., 0., 0., 0.]])
# 全一张量(常用于缩放因子初始化)
c = torch.ones(2, 3)
# 标准正态分布随机张量(常用作权重初始化)
d = torch.randn(3, 3)
# tensor([[ 0.5432, -1.2367, 0.9843],
# [-0.2341, 1.5678, -0.8876],
# [ 0.3345, 0.1234, -0.4598]])
# 均匀分布随机张量 [0, 1)
e = torch.rand(2, 2)
# 等差序列(常用于生成连续值)
f = torch.arange(0, 10, step=2)
# tensor([0, 2, 4, 6, 8])
# 等间隔序列(指定点数)
g = torch.linspace(0, 1, steps=5)
# tensor([0.0000, 0.2500, 0.5000, 0.7500, 1.0000])
# 填充固定值
h = torch.full((2, 3), fill_value=7)
# tensor([[7, 7, 7],
# [7, 7, 7]])
# 单位矩阵
i = torch.eye(4)
# tensor([[1., 0., 0., 0.],
# [0., 1., 0., 0.],
# [0., 0., 1., 0.],
# [0., 0., 0., 1.]])
# 随机排列(常用于打乱数据索引)
j = torch.randperm(10)
# tensor([3, 7, 0, 9, 2, 1, 5, 8, 4, 6])
创建方式选择指南: 在定义模型权重时,通常使用 randn 配合特定初始化策略(如Kaiming初始化);全零/全一张量多用于偏置参数或掩码;arange 和 linspace 常用于生成坐标轴、时间步等序列数据;eye 在实现某些正则化项或注意力机制中有独特用途;randperm 是数据加载器打乱样本顺序时不可或缺的工具。
1.2 张量属性
每个PyTorch张量都携带一组重要的元信息属性,这些属性决定了张量的数据解释方式和计算行为。在调试网络或排查维度不匹配错误时,检查这些属性是第一步操作。
x = torch.randn(3, 224, 224)
# 数据类型:决定每个元素占用的内存大小和精度
print (x.dtype) # torch.float32(默认)
# 所在设备:CPU或GPU
print (x.device) # device(type='cpu')
# 形状元组:定义了每个维度的大小
print (x.shape) # torch.Size([3, 224, 224])
print (x.size()) # 同上,.shape是属性,.size()是方法
# 总元素个数
print (x.numel()) # 3 * 224 * 224 = 150528
# 维度数
print (x.ndim) # 3
# 是否需要梯度(默认False)
print (x.requires_grad) # False
# 创建包含梯度的张量
y = torch.randn(3, 3, requires_grad=True)
print (y.requires_grad) # True
# 梯度信息(反向传播后才有值)
z = y.sum()
z.backward()
print (y.grad) # tensor([[1., 1., 1.], ...])
print (z.grad_fn) #
属性生命周期: dtype 和 device 在张量创建后基本固定(可通过 to() 转换),shape 可以通过视图操作改变而不改变底层数据,requires_grad 在创建后仍可修改(通过 x.requires_grad_(True)),grad 和 grad_fn 则是在反向传播过程中动态生成和填充的。
二、张量运算
张量运算是构建神经网络的前向传播基础。PyTorch为张量提供了丰富的运算接口,从基本的算术运算到复杂的矩阵变换,覆盖了深度学习所需的全部计算原语。
2.1 基本算术运算
PyTorch支持运算符重载,使得张量计算可以像普通数值一样直观书写。同时提供了函数式接口,便于在更复杂的计算图中精确控制。所有算术运算都是逐元素执行的,符合广播规则。
a = torch.tensor([1, 2, 3])
b = torch.tensor([4, 5, 6])
# 运算符重载(简洁直观)
print (a + b) # tensor([5, 7, 9])
print (a - b) # tensor([-3, -3, -3])
print (a * b) # tensor([4, 10, 18])(逐元素乘法,不是点积)
print (a / b) # tensor([0.2500, 0.4000, 0.5000])
print (a ** 2) # tensor([1, 4, 9])
# 函数式接口(适合链式调用)
print (torch.add(a, b))
print (torch.sub(a, b))
print (torch.mul(a, b))
print (torch.div(a, b))
# 原地操作(以_结尾,节省内存但丢失计算图)
a.add_(b) # 等价于 a = a + b,但更高效
2.2 矩阵乘法
矩阵乘法是神经网络中最核心的运算,全连接层、卷积层的实现都离不开它。PyTorch提供了多种矩阵乘法方式,各自适用的场景有所不同。理解它们的差异是正确构建网络的基础。
# 方式一:@ 运算符(Python 3.5+,最推荐)
x = torch.randn(32, 128) # batch_size=32, 特征维度=128
w = torch.randn(128, 10) # 权重矩阵:128输入 -> 10输出
out = x @ w # shape: (32, 10) ✅ 清晰直观
# 方式二:torch.mm(仅限2D张量,不支持广播)
out = torch.mm(x, w) # 同上,但只能处理2D
# 方式三:torch.matmul(支持广播,推荐高维)
# 对于3D以上张量,matmul会进行批量矩阵乘法
batched_x = torch.randn(16, 32, 128) # 16个样本,每个32x128
batched_w = torch.randn(16, 128, 10) # 16个权重矩阵
batched_out = torch.matmul(batched_x, batched_w) # (16, 32, 10)
# 实际应用:线性层前向传播
def linear (x, weight, bias=None):
out = x @ weight.T # weight.T 转置使维度匹配
if bias is not None :
out += bias
return out
矩阵乘法对比: @ 运算符最简洁,是PyTorch官方推荐方式;torch.mm 严格限定2D输入,速度略快但有局限性;torch.matmul 支持高维张量的批量乘法,是通用性最强的选择。在编写自定义层时,优先使用 @ 以提升可读性。
2.3 形状变换
张量形状变换是数据处理和模型构建中不可或缺的操作。PyTorch提供了多种形状变换函数,它们的底层内存布局策略各不相同,理解这些差异有助于写出更高效的代码。
x = torch.randn(2, 3, 4)
# reshape:返回新视图(可能复制数据)
r1 = x.reshape(6, 4) # (2,3,4) -> (6,4)
r2 = x.reshape(2, -1) # -1自动推断: (2, 12)
# view:返回新视图(必须满足内存连续性)
v1 = x.view(6, 4) # 要求x是连续内存
v2 = x.contiguous().view(6, 4) # 先保证连续性再view
# squeeze:删除长度为1的维度
y = torch.randn(1, 3, 1, 4)
print (y.squeeze().shape) # (3, 4),所有长度为1的维度都被移除
print (y.squeeze(dim=2).shape) # (1, 3, 4),只移除指定维度
# unsqueeze:增加长度为1的维度
z = torch.randn(3, 4)
print (z.unsqueeze(0).shape) # (1, 3, 4),在dim=0处增加
print (z.unsqueeze(1).shape) # (3, 1, 4),在dim=1处增加
# permute:任意维度置换(不改变内存布局)
img = torch.randn(3, 224, 224) # (C, H, W)
img_permuted = img.permute(1, 2, 0) # (H, W, C)
# transpose:交换两个维度
m = torch.randn(3, 5)
mt = m.transpose(0, 1) # (5, 3),等价于 .T
# 实际应用:图像数据格式转换
def chw_to_hwc (img_tensor):
"""将 (C, H, W) 转换为 (H, W, C)"""
return img_tensor.permute(1, 2, 0)
def add_batch_dim (img_tensor):
"""为单张图片添加batch维度: (C,H,W) -> (1,C,H,W)"""
return img_tensor.unsqueeze(0)
reshape vs view 的关键区别: view() 要求张量在内存中是连续的(contiguous),如果不连续会抛出运行时错误。而 reshape() 在数据不连续时会自动调用 contiguous() 再 view(),因此更加健壮。在性能敏感的场景中,优先使用 view() 并在必要时显式调用 contiguous(),可以更好地控制内存行为。
2.4 索引与切片
PyTorch的索引语法与NumPy一脉相承,支持整数索引、切片索引、布尔掩码和花式索引等多种方式,灵活性极强。合理运用这些索引技巧,可以大幅简化数据预处理和特征提取的代码。
x = torch.arange(12).reshape(3, 4)
# tensor([[ 0, 1, 2, 3],
# [ 4, 5, 6, 7],
# [ 8, 9, 10, 11]])
# 基本索引
print (x[0]) # 第一行: [0, 1, 2, 3]
print (x[0, 1]) # 0行1列: 1
# 切片索引(含头不含尾)
print (x[:, 1:3]) # 所有行,第1-2列
print (x[1:, :2]) # 第1行以后,前2列
print (x[::2, ::2]) # 步长为2的行和列
# 布尔掩码(按条件筛选)
mask = x > 5
print (x[mask]) # tensor([ 6, 7, 8, 9, 10, 11])
print (x[x % 2 == 0]) # 所有偶数
# 花式索引(整数数组索引)
indices = torch.tensor([0, 2])
print (x[indices]) # 第0行和第2行
# 高级组合索引
rows = torch.tensor([0, 1])
cols = torch.tensor([1, 3])
print (x[rows, cols]) # x[0,1]和x[1,3]: tensor([1, 7])
# where条件选取
condition = x > 5
result = torch.where(condition, x, torch.zeros_like(x))
# 满足条件的保留原值,否则置0
2.5 广播机制
广播(Broadcasting)是PyTorch中一项极具实用价值的功能,它允许不同形状的张量之间进行逐元素运算,而无需显式地复制数据。理解广播规则是避免隐性bug的关键。
# 广播规则:从尾部维度对齐,每个维度要么相等要么为1要么缺失
a = torch.randn(3, 1, 5) # 形状 (3, 1, 5)
b = torch.randn( 4, 5) # 形状 ( 4, 5) — 尾部对齐
# 结果形状: (3, 4, 5) — a的维度1被广播为4,b前面补1被广播为3
c = a + b
print (c.shape) # torch.Size([3, 4, 5])
# 实际应用1:特征标准化
batch = torch.randn(64, 256) # (batch, features)
mean = batch.mean(dim=0) # (256,)
std = batch.std(dim=0) # (256,)
normalized = (batch - mean) / std # 广播自动完成
# 实际应用2:偏置项加法
x = torch.randn(32, 128) # 输入
w = torch.randn(128, 10) # 权重
bias = torch.randn(10) # 偏置 (10,)
out = x @ w + bias # (32, 10) + (10,) -> broadcast
# 错误示例:不兼容的形状
# a = torch.randn(3, 5, 4)
# b = torch.randn( 3, 5) # RuntimeError! 尾部维度4 != 3
广播最佳实践: 在编写涉及广播的代码时,建议保持最后一个维度(特征维度)对齐,这是最自然的对齐方式。当遇到形状不匹配错误时,优先检查尾部维度是否相等或其中之一是否为1。使用 unsqueeze 显式添加维度可以更好地控制广播行为,避免隐式广播带来的意外结果。
三、自动求导机制
自动求导(Autograd)是PyTorch最核心的特性之一,它让深度学习从业者无需手动推导和编写梯度计算代码。PyTorch通过动态构建计算图(Dynamic Computational Graph)来记录所有张量操作,并在调用 backward() 时自动执行反向传播计算梯度。这种"定义即运行"(Define-by-Run)的方式赋予了极大的灵活性,使得即使是包含控制流的复杂网络结构也能正确求导。
3.1 计算图的基本概念
计算图是一个有向无环图(DAG),其中节点代表张量或运算,边代表数据流向。在前向传播过程中,PyTorch自动构建计算图并记录每个运算;在反向传播时,利用链式法则从输出节点向输入节点逐层计算梯度。
# 构建简单的计算图
x = torch.tensor(2.0, requires_grad=True)
y = torch.tensor(3.0, requires_grad=True)
z = x ** 2 + y ** 3
# z = 4 + 27 = 31
# z.grad_fn 指向创建z的运算节点
print (z.grad_fn)
# — 加法运算节点
# 反向传播计算梯度
z.backward()
# dz/dx = 2*x = 4.0
print (x.grad)
# tensor(4.)
# dz/dy = 3*y^2 = 27.0
print (y.grad)
# tensor(27.)
# 计算图可视化(理解结构)
# x --(平方)--> x^2 --+
# +--(加法)--> z
# y --(立方)--> y^3 --+
3.2 链式法则详解
自动求导的核心数学基础是微积分中的链式法则。当函数由多个基本运算复合而成时,梯度可以沿着计算图中的路径逐层传播。PyTorch的Autograd引擎自动处理了这一过程,但理解其背后的数学原理对于调试和优化模型至关重要。
# 多步复合函数示例
x = torch.tensor(2.0, requires_grad=True)
# 复合函数: z = sin(x^2)
u = x ** 2 # u = 4
z = torch.sin(u) # z = sin(4) ≈ -0.7568
z.backward()
# 链式法则: dz/dx = dz/du * du/dx = cos(x^2) * 2x
# = cos(4) * 4 ≈ -0.6536 * 4 = -2.6146
print (x.grad) # tensor(-2.6146) ✅ 与理论值一致
# 分步检查中间梯度
# 注意: 默认情况下中间节点的梯度会被释放以节省内存
# 如需保留,需使用 retain_grad()
u = x ** 2
u.retain_grad() # 保留中间变量u的梯度
z = torch.sin(u)
z.backward()
print (u.grad) # dz/du = cos(4) ≈ -0.6536
print (x.grad) # dz/dx = -2.6146
# 验证: 手动计算
# x=2, u=4, z=sin(4)
# du/dx = 2x = 4
# dz/du = cos(4) ≈ -0.6536
# dz/dx = dz/du * du/dx = -0.6536 * 4 = -2.6146 ✅
3.3 梯度累积与清零
PyTorch的Autograd机制默认会将梯度累积(累加)到张量的 .grad 属性中,而不是每次反向传播时覆盖。这一设计在RNN等需要累积多个小批量梯度的场景中非常有用,但也意味着在标准的mini-batch训练中,我们需要在每次反向传播前手动清零梯度,否则梯度会持续累加导致训练失败。
# 梯度累积演示
x = torch.tensor(2.0, requires_grad=True)
# 第一次反向传播
y1 = x ** 2
y1.backward()
print (x.grad) # tensor(4.) — dy1/dx = 2x = 4
# 第二次反向传播(未清零!)
y2 = x ** 3
y2.backward()
print (x.grad) # tensor(31.) — 4 + 27 = 31(累积了!)
# 正确的做法:每次迭代前清零
x.grad.zero_() # 或 optimizer.zero_grad()
y3 = x ** 4
y3.backward()
print (x.grad) # tensor(32.) — dy3/dx = 4*x^3 = 32
# 训练循环中的标准模式
optimizer = torch.optim.SGD(model.parameters(), lr=0.01)
for epoch in range (100):
optimizer.zero_grad() # 清零所有参数梯度
loss = model(x_batch, y_batch)
loss.backward() # 计算梯度
optimizer.step() # 更新参数
梯度累积的陷阱: 在训练循环中忘记调用 optimizer.zero_grad() 或 model.zero_grad() 是最常见的PyTorch bug之一。如果不清零,梯度会随着迭代不断累加,导致参数更新量呈线性增长,模型永远无法收敛。但当显存不足时,梯度累积技术(Gradient Accumulation)正是利用这一特性:在多个mini-batch上累积梯度后一次性更新参数,从而模拟更大的batch size。
3.4 梯度函数追踪
每个张量的 grad_fn 属性指向创建该张量的运算节点,构成了一条完整的求导链。通过检查 grad_fn 可以深入理解计算图的结构,在复杂模型调试中非常有用。PyTorch的Autograd系统为几乎所有的张量运算都实现了对应的反向传播函数。
x = torch.tensor(2.0, requires_grad=True)
y = torch.tensor(3.0, requires_grad=True)
z = x * y + torch.sin(x)
print (z.grad_fn)
#
# 追踪计算图结构
def trace_grad_fn (tensor, depth=0):
if tensor.grad_fn
is None :
print (
" " * depth +
f"Leaf: {tensor.data.item()}" )
return
print (
" " * depth +
f"Op: {tensor.grad_fn}" )
for inp
in tensor.grad_fn.next_functions:
if inp[0]
is not None :
trace_grad_fn(inp[0], depth + 1)
# 打印计算图结构
trace_grad_fn(z)
# 输出:
# Op:
# Op:
# Leaf: 2.0
# Leaf: 3.0
# Op:
# Leaf: 2.0
四、梯度控制
在实际训练和推理过程中,并非所有张量操作都需要计算梯度。合理控制梯度的传播不仅可以节省内存和计算资源,还能实现诸如特征提取、模型微调、对抗样本生成等高级功能。PyTorch提供了多层级的梯度控制机制,从细粒度的 detach() 到上下文管理器 torch.no_grad(),再到全局开关 torch.set_grad_enabled(),覆盖了各种使用场景。
4.1 detach() — 切断计算图
detach() 返回一个新的张量,它与原始张量共享底层数据但脱离了计算图。这个新张量的 requires_grad 为 False,且 grad_fn 为 None。这在实现某些特殊训练技巧(如GAN的交替训练、强化学习的固定目标网络)时非常关键。
x = torch.tensor([1, 2, 3], dtype=torch.float32, requires_grad=True)
y = x ** 2
z = y.detach() # z 与 y 共享数据,但不追踪梯度
print (z.requires_grad) # False ❌ 不追踪梯度
print (z.grad_fn) # None
# 但 z 可以与 y 共享底层内存!
z[0] = 100 # ⚠️ 会同时修改 y!
print (y) # tensor([100., 4., 9.]) — y也被改了
# 安全做法:使用 .clone().detach()
x2 = torch.tensor([1, 2, 3], dtype=torch.float32, requires_grad=True)
y2 = x2 ** 2
z2 = y2.detach().clone() # 完全独立的副本
z2[0] = 100
print (y2) # tensor([1., 4., 9.]) — y2不受影响 ✅
# 实际应用:固定目标网络(如DQN)
with torch.no_grad(): # 或者在更新时用 detach()
target_q = target_network(next_state).max(dim=1).values.detach()
detach() 的典型应用场景: (1)迁移学习中冻结预训练特征提取器,只训练分类头;(2)GAN训练中固定生成器或判别器其中之一;(3)计算评估指标(如准确率)时不需要梯度;(4)实现梯度的截断或裁剪操作;(5)在不求导的分支中计算辅助损失时避免梯度反向传播到主网络。
4.2 torch.no_grad() — 上下文管理器
torch.no_grad() 是一个上下文管理器,在其作用域内的所有张量操作都不会构建计算图,也不会记录梯度信息。这在模型推理(inference)和评估(evaluation)阶段广泛使用,可以显著降低内存消耗并提升计算速度。在推理时,我们只需要前向传播的结果,不需要保存中间激活值用于反向传播。
# 训练模式:需要梯度
def train_epoch (model, loader, optimizer):
model.train()
total_loss = 0
for x, y in loader:
optimizer.zero_grad()
pred = model(x) # 构建计算图
loss = loss_fn(pred, y)
loss.backward() # 反向传播
optimizer.step()
total_loss += loss.item()
# 推理模式:无需梯度(大幅提升性能)
def evaluate (model, loader):
model.eval()
total_correct = 0
with torch.no_grad(): # ✅ 推荐方式
for x, y in loader:
pred = model(x) # 不构建计算图!
total_correct += (pred.argmax(dim=1) == y).sum().item()
return total_correct / len(loader.dataset)
# 性能对比:no_grad 可节省约30-50%的显存
4.3 requires_grad — 冻结参数
对于需要微调的预训练模型,我们通常希望冻结大部分层,只训练新增的分类头。通过设置 requires_grad=False,可以阻止特定参数的梯度计算,这些参数在前向传播中仍会参与计算,但在反向传播时不会接收到梯度,因此不会被优化器更新。
import torchvision.models as models
# 加载预训练ResNet
model = models.resnet50(pretrained=True)
# 冻结所有参数(迁移学习标准做法)
for param in model.parameters():
param.requires_grad = False # ❌ 冻结特征提取层
# 替换分类头(新层默认 requires_grad=True)
num_features = model.fc.in_features
model.fc = torch.nn.Linear(num_features, 10) # 10类新任务
# 只优化分类头参数
optimizer = torch.optim.SGD(model.fc.parameters(), lr=0.01)
# 训练时:分类头更新,主干网络保持不变
# 这被称为"微调"(fine-tuning)的最简形式
# 选择性解冻(渐进式微调)
for param in model.layer4.parameters():
param.requires_grad = True # ✅ 只解冻最后几个块
optimizer = torch.optim.Adam([
{'params' : model.layer4.parameters(), 'lr' : 1e-4},
{'params' : model.fc.parameters(), 'lr' : 1e-3},
])
4.4 全局梯度控制
torch.set_grad_enabled(bool) 是一个全局开关,可以影响整个代码块的梯度行为,适合在训练/评估切换时使用。相比之下,torch.inference_mode() 是PyTorch 1.9引入的新模式,它比 no_grad 更加高效,因为它不仅禁用了梯度计算,还禁用了其他与推理无关的内部机制,适合纯粹的模型部署场景。
# 三种梯度控制方式对比
# 方式1:torch.no_grad() — 最常用
with torch.no_grad():
out = model(x) # 推理模式
# 方式2:torch.inference_mode() — 速度更快(PyTorch 1.9+)
with torch.inference_mode():
out = model(x) # 更激进的优化
# 方式3:torch.set_grad_enabled(False) — 全局范围控制
torch.set_grad_enabled(False) # 关闭梯度,影响整个线程
out = model(x)
torch.set_grad_enabled(True) # 重新开启
# 训练/评估装饰器模式
from functools import wraps
def inference_mode (func):
"""装饰器:确保函数在推理模式下执行"""
@wraps (func)
def wrapper (*args, **kwargs):
with torch.inference_mode():
return func(*args, **kwargs)
return wrapper
@inference_mode
def predict (model, x):
return model(x)
五、设备管理
GPU加速是深度学习能够处理大规模数据的关键因素之一。PyTorch提供了简洁而强大的设备管理API,使得数据在CPU和GPU之间、以及不同GPU之间灵活迁移变得非常简单。正确管理设备是编写高效、可移植训练代码的必要技能。
5.1 CPU与GPU切换
PyTorch通过 .to() 方法实现统一设备管理,同时提供了 .cpu() 和 .cuda() 这两个便捷方法。推荐的实践是在代码中定义统一的设备变量,在需要时通过 .to(device) 进行转换,这样可以轻松在一行代码中切换CPU/GPU运行环境。
# 定义设备(最佳实践)
device = torch.device("cuda" if torch.cuda.is_available() else "cpu" )
print (f"Using device: {device}" )
# 创建张量并迁移到设备
x = torch.randn(3, 3)
x = x.to(device) # 统一设备转移方式
# 直接在目标设备创建(更高效)
y = torch.randn(3, 3, device=device)
# 模型移动到设备
model = MyModel()
model = model.to(device) # 模型所有参数迁移到device
# 数据也必须在同一设备
for batch, labels in dataloader:
batch = batch.to(device) # 别忘了移动数据!
labels = labels.to(device)
pred = model(batch)
# 检查GPU可用性
print (torch.cuda.is_available()) # True/False
print (torch.cuda.device_count()) # GPU数量
print (torch.cuda.get_device_name()) # 显卡型号
# 多GPU训练(DataParallel简单封装)
if torch.cuda.device_count() > 1:
model = torch.nn.DataParallel(model)
设备管理常见错误: (1)张量在CPU上创建后未移动到GPU就直接传入模型,会抛出"Expected all tensors to be on the same device"错误;(2)在多GPU场景中,DataParallel封装的模型访问参数时需要通过 model.module;(3)不同GPU之间的张量不能直接进行运算;(4)模型保存时推荐只保存 model.state_dict() 而不是整个模型对象,以保持设备无关性。
5.2 数据类型与半精度
PyTorch支持多种数值精度类型,其中 float32(单精度)是默认选择,在大多数情况下能在精度和性能之间取得良好平衡。float16(半精度)可以大幅减少显存占用并加速计算,但需要注意数值范围限制。现代GPU(如NVIDIA Volta/V100及之后的架构)支持自动混合精度(AMP),可以同时利用float16的计算速度和float32的数值稳定性。
# 常见数据类型
print (torch.tensor([1, 2, 3]).dtype) # torch.int64
print (torch.tensor([1., 2., 3.]).dtype) # torch.float32
print (torch.tensor([1, 2, 3], dtype=torch.float16).dtype) # torch.float16
# 数据类型转换
x = torch.randn(3, 3)
x_fp16 = x.half() # float32 -> float16
x_double = x.double() # float32 -> float64
x_int = x.int() # float32 -> int32(截断)
# 通过to()统一转换
x = x.to(dtype=torch.float16)
# 自动混合精度训练(AMP)
from torch.cuda.amp import autocast, GradScaler
scaler = GradScaler() # 梯度缩放,防止fp16下溢出
for x, y in dataloader:
optimizer.zero_grad()
with autocast(): # 自动选择精度
pred = model(x)
loss = loss_fn(pred, y)
scaler.scale(loss).backward() # 缩放loss避免梯度下溢出
scaler.step(optimizer) # 更新参数
scaler.update() # 更新缩放因子
精度选择策略: (1)模型训练默认使用 float32,保证数值稳定性;(2)显存不足时优先尝试AMP自动混合精度,通常可以节省约40%显存且几乎不损失精度;(3)模型推理部署时可以使用 float16 甚至 int8 量化以提升吞吐量;(4)需要超高精度计算(如某些科学计算)时才使用 float64(double)。在训练大型模型(如LLM、ViT)时,混合精度训练已经成为事实标准。
六、实战:线性回归完整实现
下面通过一个完整的线性回归示例,将前面所学的张量操作、自动求导、梯度控制和设备管理知识串联起来。这个例子虽然简单,但包含了深度学习训练流程的所有核心要素:数据生成、模型定义、损失函数、梯度计算和参数更新。
6.1 数据生成
import torch
import matplotlib.pyplot as plt
# 设置随机种子确保可复现
torch.manual_seed(42)
# 生成合成数据: y = 2 * x + 1 + 噪声
# 真实权重 w_true = 2, 真实偏置 b_true = 1
X = torch.linspace(-1, 1, 100).reshape(-1, 1) # (100, 1)
true_w, true_b = 2.0, 1.0
y = true_w * X + true_b + torch.randn_like(X) * 0.1 # 加入高斯噪声
print (f"数据形状: X {X.shape}, y {y.shape}" )
6.2 手动实现线性回归
# 初始化参数(需要梯度追踪!)
w = torch.randn(1, 1, requires_grad=True)
b = torch.zeros(1, requires_grad=True)
# 超参数
lr = 0.01
num_epochs = 500
# 训练循环(完全手动实现,不使用nn.Module)
losses = []
for epoch in range (num_epochs):
# 前向传播: y_pred = X @ w + b
y_pred = X @ w + b
# MSE损失: mean((y_pred - y)^2)
loss = ((y_pred - y) ** 2).mean()
# 反向传播计算梯度
loss.backward()
# 梯度下降更新参数(手动实现优化器)
with torch.no_grad(): # 梯度更新不在计算图中
w -= lr * w.grad
b -= lr * b.grad
# 清零梯度(关键!)
w.grad.zero_()
b.grad.zero_()
losses.append(loss.item())
if (epoch + 1) % 100 == 0:
print (f"Epoch {epoch+1}: loss = {loss.item():.6f}" )
print (f"训练完成! w = {w.item():.4f} (真实值: {true_w}), b = {b.item():.4f} (真实值: {true_b})" )
6.3 使用nn.Module实现
import torch.nn as nn
import torch.optim as optim
# 定义模型(nn.Module封装)
class LinearRegression (nn.Module):
def __init__ (self, in_dim, out_dim):
super ().__init__ ()
self.linear = nn.Linear(in_dim, out_dim) # 自动管理参数
def forward (self, x):
return self.linear(x)
# 实例化模型和优化器
device = torch.device("cuda" if torch.cuda.is_available() else "cpu" )
model = LinearRegression(1, 1).to(device)
criterion = nn.MSELoss()
optimizer = optim.SGD(model.parameters(), lr=0.01)
# 数据移至设备
X, y = X.to(device), y.to(device)
# 训练循环(标准PyTorch模式)
for epoch in range (num_epochs):
model.train()
# 前向传播
y_pred = model(X)
loss = criterion(y_pred, y)
# 反向传播与优化三步曲
optimizer.zero_grad() # 第一步:清零梯度
loss.backward() # 第二步:反向传播
optimizer.step() # 第三步:更新参数
if (epoch + 1) % 100 == 0:
print (f"Epoch {epoch+1}: loss = {loss.item():.6f}" )
# 查看训练结果
print (f"权重: {model.linear.weight.item():.4f}, 偏置: {model.linear.bias.item():.4f}" )
学习要点: 上述两种实现方式在数学上完全等价,但使用 nn.Module 有三大优势:(1)自动管理参数,无需手动维护 w 和 b;(2)优化器封装梯度更新逻辑,不再需要手动减法和清零;(3).to(device) 一键迁移所有参数。在实际项目中,始终使用 nn.Module 方式。
七、常见错误与调试
在使用PyTorch张量和自动求导时,有几类错误是初学者最容易遇到的。掌握这些错误的特征和解决方法,可以大幅提升调试效率。下面总结最常见的三类错误及其解决方案。
7.1 梯度相关错误
# 错误1: 对不需要梯度的张量调用backward
x = torch.tensor([1, 2, 3], dtype=torch.float32) # requires_grad默认为False
# x.sum().backward() # RuntimeError! ❌
# 修复:设置 requires_grad=True
x = torch.tensor([1, 2, 3], dtype=torch.float32, requires_grad=True)
x.sum().backward() # ✅
# 错误2: 对非标量张量调用backward未指定gradient
x = torch.randn(3, requires_grad=True)
y = x ** 2
# y.backward() # RuntimeError! ❌ y不是标量
# 修复:传入与y同形的梯度权重
y.backward(torch.ones_like(x)) # ✅ 等价于 y.sum().backward()
# 错误3: 在no_grad上下文中调用backward
with torch.no_grad():
x = torch.randn(3, requires_grad=True)
y = x ** 2
# y.backward() # RuntimeError! ❌ y没有grad_fn
7.2 设备不匹配错误
# 最常见的设备错误
cpu_tensor = torch.randn(3, 3)
if torch.cuda.is_available():
gpu_tensor = torch.randn(3, 3, device="cuda" )
# result = cpu_tensor + gpu_tensor # RuntimeError! ❌ 设备不匹配
# 修复:将两个张量移到同一设备
result = cpu_tensor.to("cuda" ) + gpu_tensor # ✅
# 调试技巧
def debug_tensor (tensor, name):
"""打印张量的关键调试信息"""
print (f"{name}: shape={tensor.shape}, dtype={tensor.dtype}, "
f"device={tensor.device}, requires_grad={tensor.requires_grad}" )
7.3 计算图泄漏
# 计算图泄漏:在不该追踪梯度的地方保留了计算图
def bad_inference (model, x):
"""错误:推理时仍追踪梯度"""
return model(x) # 会构建计算图,浪费显存
def good_inference (model, x):
"""正确:推理时禁用梯度追踪"""
with torch.no_grad():
return model(x) # 不构建计算图,节省显存
# 计算图泄漏的另一个常见来源:在tensor上修改用于后续计算
x = torch.tensor([1.0], requires_grad=True)
y = x * 2
# 如果不小心修改了用于y计算的x的值
x.data.fill_(10.0) # ⚠️ 改变了x的值,但y的计算图仍引用旧的x!
loss = y.sum()
loss.backward() # 梯度基于当前x的值计算,不正确!
调试黄金法则: 遇到PyTorch相关错误时,按以下顺序排查:(1)检查所有张量和模型的 device 是否一致;(2)检查所有需要梯度的张量是否已设置 requires_grad=True;(3)检查训练循环中是否调用了 optimizer.zero_grad();(4)检查推理模式下是否误用了 torch.no_grad() 或 torch.inference_mode();(5)检查 backward() 的调用对象是否为标量。
八、核心要点总结
张量创建: PyTorch提供丰富API(tensor/zeros/ones/randn/arange/linspace/full/eye/randperm),每种适用于不同初始化场景,权重初始化常用randn,掩码操作常用zeros/ones,序列生成用arange/linspace
张量属性: dtype决定精度(默认float32),device决定计算位置(CPU/GPU),shape决定维度结构,requires_grad决定是否追踪梯度,四者共同定义了张量的计算行为
矩阵乘法: @运算符最推荐,matmul支持广播和高维批量乘法,mm仅限2D。理解三者差异是构建正确网络的前提
形状变换: reshape更安全(自动处理不连续内存),view更高效(需连续内存),permute/transpose实现维度重排,squeeze/unsqueeze增删维度
广播机制: 从尾部维度对齐,每个维度要么相等要么为1。巧妙利用广播可简化代码,但不当使用会导致隐性维度错误
Autograd核心: 动态计算图(Define-by-Run)自动记录前向传播中的所有运算,backward()触发反向传播计算梯度,链式法则沿计算图逐层传播
梯度管理: 每次迭代前必须zero_grad()清零梯度,否则梯度累积导致训练失败。梯度累积技术也可用于模拟更大batch size
梯度控制: detach()切断计算图(共享数据但无梯度),no_grad()禁用梯度追踪(推理标准做法),set_grad_enabled()全局控制,inference_mode()极致推理优化
设备管理: 使用统一device变量("cuda"/"cpu"),通过.to(device)迁移张量和模型。混合精度AMP可节省约40%显存,已成为大模型训练标配
训练循环标准模式: optimizer.zero_grad() -> loss.backward() -> optimizer.step() 三步曲,外加推理时的with torch.no_grad()上下文
九、进一步思考
掌握了张量操作和自动求导机制后,可以进一步探索以下方向:深入理解PyTorch的 torch.compile 编译优化技术,它将Python代码编译为高性能内核,在大规模模型训练中可提升15-30%的训练速度;研究 torch.fx 和 torch.export 等计算图捕获与导出工具,它们是将PyTorch模型部署到生产环境(如移动端、服务端推理)的关键技术;探索分布式训练中的张量并行(Tensor Parallelism)和流水线并行(Pipeline Parallelism)策略,这些是大规模语言模型训练的必备知识。
在实际工程中,完整的深度学习项目还需要掌握数据加载器(DataLoader)的高效配置、模型保存与加载的最佳实践(仅保存state_dict而非完整模型)、TensorBoard或WandB等可视化工具的使用、以及超参数调优的系统方法。这些内容将在后续的学习笔记中逐步展开。
学习路径建议: 第一周重点掌握张量创建、属性和基本运算;第二周深入理解自动求导机制和计算图概念;第三周学习模型定义(nn.Module)、优化器(optim)和数据加载(DataLoader),完成端到端的训练流程;第四周开始接触卷积神经网络和循环神经网络等经典架构,并尝试在GPU上加速训练。