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初始化);全零/全一张量多用于偏置参数或掩码;arangelinspace 常用于生成坐标轴、时间步等序列数据;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) #

属性生命周期:dtypedevice 在张量创建后基本固定(可通过 to() 转换),shape 可以通过视图操作改变而不改变底层数据,requires_grad 在创建后仍可修改(通过 x.requires_grad_(True)),gradgrad_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_gradFalse,且 grad_fnNone。这在实现某些特殊训练技巧(如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)自动管理参数,无需手动维护 wb;(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.fxtorch.export 等计算图捕获与导出工具,它们是将PyTorch模型部署到生产环境(如移动端、服务端推理)的关键技术;探索分布式训练中的张量并行(Tensor Parallelism)和流水线并行(Pipeline Parallelism)策略,这些是大规模语言模型训练的必备知识。

在实际工程中,完整的深度学习项目还需要掌握数据加载器(DataLoader)的高效配置、模型保存与加载的最佳实践(仅保存state_dict而非完整模型)、TensorBoard或WandB等可视化工具的使用、以及超参数调优的系统方法。这些内容将在后续的学习笔记中逐步展开。

学习路径建议:第一周重点掌握张量创建、属性和基本运算;第二周深入理解自动求导机制和计算图概念;第三周学习模型定义(nn.Module)、优化器(optim)和数据加载(DataLoader),完成端到端的训练流程;第四周开始接触卷积神经网络和循环神经网络等经典架构,并尝试在GPU上加速训练。