PyTorch 的基础是张量,张量的魅力不仅在于可以像数组一样存取数据,更在于那一套简洁却极富表现力的运算规则。很多初学者在刚接触 PyTorch 时,会被torch.mm、torch.matmul、*和@这些运算符搞得一头雾水,也会在看代码时反复琢磨“这里两个张量的形状明明不一样,为什么还能相加?”其实,这些问题都指向同一个核心主题:张量运算规则。
本文作为 PyTorch 入门系列的第 2 课,围绕逐元素计算、矩阵乘法和广播机制展开。我们会从概念出发,结合大量可运行的代码示例,把三种运算的底层逻辑和使用场景讲透。学完本文,你将能看懂绝大多数 PyTorch 模型代码中的张量运算,也能在遇到形状不匹配报错时快速定位问题。
本文适合以下读者:
- 刚学会创建张量、想进一步掌握张量运算的初学者。
- 看过不少 PyTorch 代码,但对
*与@的区别、广播机制何时生效仍有疑惑的开发者。 - 准备开始阅读 Transformer、YOLO、TCN 等模型源码的读者。
文章内容围绕环境准备、张量核心概念、三种运算讲解、完整实战项目、常见报错排查、最佳实践六个部分展开,代码均在 PyTorch 2.x 环境下验证通过,但核心 API 在 1.x 同样适用。
1. 张量的核心概念:先理解数据长什么样
1.1 张量到底是什么
张量(Tensor)可以理解为多维数组的泛化。它和 NumPy 的ndarray非常相似,但额外具备两个关键能力:
- 自动求导:PyTorch 张量可以记录自身的运算历史,并在反向传播时自动计算梯度。
- GPU 加速:张量可以轻松在 CPU 与 GPU 之间迁移,大批量矩阵运算在 GPU 上的速度远高于 CPU。
从维度上看,张量可以这样理解:
- 0 维张量:标量,例如数字
3.14。 - 1 维张量:向量,例如
[1, 2, 3]。 - 2 维张量:矩阵,例如
[[1, 2], [3, 4]]。 - 3 维及以上张量:可以理解为多个矩阵的集合,例如一批图片数据
[批量大小, 通道数, 高度, 宽度]。
在深度学习项目中,最常见的张量形状(shape)有:
- 自然语言处理:
[batch_size, seq_len, hidden_size],即一批句子中每个位置的向量表示。 - 计算机视觉:
[batch_size, channels, height, width],即一批图片的像素数据。 - 表格数据:
[batch_size, num_features],即一批样本、每条样本有若干特征。
之所以要反复强调形状,是因为本文后面所有运算规则的根基都建立在“形状如何对齐”这个问题上。
1.2 张量的三个关键属性
在开始运算之前,建议先掌握张量的三个属性:shape、dtype、device。
shape决定了张量的维度结构,任何运算都必须满足形状约束。dtype表示张量中元素的类型,例如torch.float32、torch.int64。不同 dtype 之间的运算往往需要显式转换。device表示张量存放在 CPU 还是 GPU 上。CPU 张量与 GPU 张量不能直接运算。
这三个属性也是后续报错排查的主要方向。
import torch x = torch.tensor([[1.0, 2.0], [3.0, 4.0]]) print("shape:", x.shape) print("dtype:", x.dtype) print("device:", x.device)输出结果:
shape: torch.Size([2, 2]) dtype: torch.float32 device: cpu1.3 为什么张量运算规则如此重要
深度学习模型在训练和推理过程中,本质上就是不断对张量进行运算。卷积、池化、全连接层、注意力机制、损失函数,最终都可以拆解为以下三类操作的组合:
- 逐元素运算:对张量中每个位置的元素独立进行计算。
- 矩阵乘法 / 批量矩阵乘法:对数据维度进行线性变换。
- 广播机制:让不同形状的张量在满足一定规则时自动对齐参与运算。
换句话说,如果这三类操作掌握扎实,阅读模型源码时就不会再惧怕那些复杂的高维张量表达式。本文的核心目标,就是把这三种运算彻底讲清楚。
2. 环境准备与版本说明
2.1 运行环境检查
本文示例基于 PyTorch 2.x 编写。如果你的环境安装的是 PyTorch 1.x,绝大多数 API 仍然兼容,但部分打印输出可能有细微差异。
建议先运行下面的命令确认版本:
python -c "import torch; print(torch.__version__)"如果还没有安装 PyTorch,可以根据自己的系统环境选择安装方式。CPU 版本可以直接通过 pip 或 conda 安装:
pip install torchGPU 版本建议到 PyTorch 官网选择对应的 CUDA 版本,再执行安装命令。需要注意,安装的 CUDA 版本需要与本地显卡驱动兼容。无论使用哪种方式,安装完成后都建议运行以下代码验证是否可以调用 GPU:
import torch print(torch.cuda.is_available())返回True说明 GPU 可用,返回False则说明当前只能使用 CPU。本文后续示例在 CPU 上即可运行,不强制要求 GPU。
2.2 示例代码文件结构
为了便于读者在本地练习,建议按以下结构组织文件:
pytorch-tensor-basics/ ├── main.py # 综合实战:线性回归示例 ├── broadcast_demo.py # 广播机制示例 ├── matmul_demo.py # 矩阵乘法示例 └── elementwise_demo.py # 逐元素运算示例也可以直接在 Python 交互式环境(Jupyter Notebook、VS Code 交互窗口)中逐段运行本文代码。需要特别提醒的是,如果使用 Jupyter Notebook,运行顺序要保持一致,因为张量状态会在单元格之间共享。
3. 逐元素计算:最直观的运算方式
3.1 什么是逐元素计算
逐元素计算(element-wise operation)是指参与运算的两个张量形状完全相同时,对应位置上的元素进行独立计算,最终输出的形状与输入张量形状保持一致。
例如两个形状均为[2, 2]的张量相加,公式表达为:
output[0][0] = x[0][0] + y[0][0] output[0][1] = x[0][1] + y[0][1] output[1][0] = x[1][0] + y[1][0] output[1][1] = x[1][1] + y[1][1]逐元素计算是 PyTorch 中最简单、也最常用的运算方式。
3.2 基础运算:加减乘除幂
PyTorch 支持 Python 运算符与函数式调用两种写法。以下代码演示了加减乘除等常见逐元素运算:
import torch x = torch.tensor([[1.0, 2.0], [3.0, 4.0]]) y = torch.tensor([[5.0, 6.0], [7.0, 8.0]]) print("x + y =", x + y) print("x - y =", x - y) print("x * y =", x * y) print("x / y =", x / y) print("x ** 2 =", x ** 2)输出结果:
x + y = tensor([[ 6., 8.], [10., 12.]]) x - y = tensor([[-4., -4.], [-4., -4.]]) x * y = tensor([[ 5., 12.], [21., 32.]]) x / y = tensor([[0.2000, 0.3333], [0.4286, 0.5000]]) x ** 2 = tensor([[ 1., 4.], [ 9., 16.]])其中x * y是逐元素乘法,即哈达玛积(Hadamard product),这一点非常重要。很多初学者会把*误认为是矩阵乘法,导致计算结果完全不同。关于矩阵乘法,下一节会详细介绍。
3.3 函数式 API:torch.add、torch.mul
除了运算符,PyTorch 还提供了对应的函数式 API。它们在功能上与运算符等价,但在代码中可以更清楚地表达意图,也方便传入额外参数(如alpha系数):
import torch x = torch.tensor([[1.0, 2.0], [3.0, 4.0]]) y = torch.tensor([[5.0, 6.0], [7.0, 8.0]]) # torch.add 支持 alpha 缩放:result = x + alpha * y result = torch.add(x, y, alpha=2.0) print(result)输出结果:
tensor([[11., 14.], [17., 20.]])同理,torch.sub、torch.mul、torch.div分别对应减法、乘法和除法。在深度学习代码中,torch.mul常用来对梯度或特征图进行缩放。
3.4 比较运算与逻辑运算
逐元素计算不仅限于数值运算,也包括比较运算和逻辑运算。比较运算的结果是一个布尔类型张量,常用于掩码(mask)操作:
import torch x = torch.tensor([1.0, 5.0, 3.0, 8.0]) y = torch.tensor([2.0, 3.0, 3.0, 4.0]) print(x > y) # 大于 print(x == y) # 等于 print(x >= y) # 大于等于输出结果:
tensor([False, True, False, True]) tensor([False, False, True, False]) tensor([False, True, True, True])逻辑运算包括torch.logical_and、torch.logical_or、torch.logical_not:
mask1 = x > 2 mask2 = y < 4 print(torch.logical_and(mask1, mask2))这种掩码操作在模型评估、数据筛选、损失计算中经常出现。
3.5 数学函数:exp、log、sqrt、sin
深度学习中的激活函数、归一化操作本质上也是逐元素计算。PyTorch 内置了大量数学函数:
import torch x = torch.tensor([1.0, 2.0, 3.0]) print(torch.exp(x)) print(torch.log(x)) print(torch.sqrt(x)) print(torch.sin(x))输出结果:
tensor([ 2.7183, 7.3891, 20.0855]) tensor([0.0000, 0.6931, 1.0986]) tensor([1.0000, 1.4142, 1.7321]) tensor([0.8415, 0.9093, 0.1411])在神经网络的损失函数中,torch.log和torch.exp尤其常见,例如交叉熵损失的前置计算就涉及对数概率。
3.6 inplace 运算与共享内存问题
PyTorch 中的许多逐元素运算都提供了 inplace 版本,即在原张量上直接修改,而不返回新张量。常见的 inplace 方法有add_、mul_、div_、clamp_、zero_等,注意函数名以_结尾:
import torch x = torch.tensor([1.0, 2.0, 3.0]) x.add_(10) # 等价于 x = x + 10,但直接在原张量上修改 print(x)输出结果:
tensor([11., 12., 13.])inplace 操作虽然能够节省部分内存,但会带来一个隐藏风险:如果该张量是通过某个运算得到的,并且需要参与后续反向传播,PyTorch 的自动求导机制可能无法正确追踪这部分修改记录。
因此,在实际模型代码中,如果没有充分把握,建议优先使用非 inplace 写法,例如x = x + 10,而不使用x.add_(10)。这也是一种更安全的工程习惯。
4. 矩阵乘法:深度学习的核心运算
4.1 为什么要单独理解矩阵乘法
逐元素乘法虽然简单,但在深度学习中并不承担“特征变换”的任务。真正承担这一任务的是矩阵乘法。全连接层、卷积层中的权重叠加、注意力机制中的 QKV 变换,都离不开矩阵乘法。
矩阵乘法在 PyTorch 中有三种常见写法:
torch.mm:只适用于二维矩阵。torch.matmul:支持高维张量,自动处理批量维度,最常使用。@运算符:torch.matmul的简写形式,更推荐在代码中使用。
4.2 torch.mm:二维矩阵乘法
二维矩阵乘法的数学定义是:
C[i][j] = sum_k A[i][k] * B[k][j]其中 A 的形状为[m, k],B 的形状为[k, n],C 的形状为[m, n]。关键在于 A 的最后一维必须等于 B 的倒数第二维。
import torch A = torch.tensor([[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]]) # 形状 [2, 3] B = torch.tensor([[7.0, 8.0], [9.0, 10.0], [11.0, 12.0]]) # 形状 [3, 2] C = torch.mm(A, B) print(C.shape) print(C)输出结果:
torch.Size([2, 2]) tensor([[ 58., 64.], [139., 154.]])逐行验证一下第一行第一列的计算过程:
C[0][0] = 1*7 + 2*9 + 3*11 = 7 + 18 + 33 = 584.3 torch.matmul 与 @:支持高维批量运算
在深度学习中,我们通常不是对一个矩阵做变换,而是对一批矩阵同时做变换。例如一批样本的特征矩阵形状为[batch_size, seq_len, feature_dim],要乘以一个权重矩阵[feature_dim, hidden_dim]。这时候torch.mm无法直接处理三维输入,需要使用torch.matmul:
import torch # 模拟批量矩阵乘法 batch_A = torch.randn(2, 3, 4) # 2 个 [3, 4] 的矩阵 batch_B = torch.randn(2, 4, 5) # 2 个 [4, 5] 的矩阵 result = torch.matmul(batch_A, batch_B) print(result.shape)输出结果:
torch.Size([2, 3, 5])需要注意的是,torch.matmul在高维运算时会自动把“最后一个维度”和“倒数第二个维度”作为矩阵乘法的内维度,而前面的维度被称为批量维度。如果两个输入张量的批量维度不一致,在某些条件下也会触发广播机制,这一点在下一节会详细讲解。
在实际代码中,@运算符是torch.matmul的等价写法,且更加简洁:
result = batch_A @ batch_B两种写法结果完全一致。PyTorch 官方文档与开源项目源码中,@运算符使用频率非常高。
4.4 逐元素乘法与矩阵乘法的区别
这是初学者最容易混淆的概念。用一个非常简单的例子对比:
import torch x = torch.tensor([[1.0, 2.0], [3.0, 4.0]]) y = torch.tensor([[5.0, 6.0], [7.0, 8.0]]) # 逐元素乘法 elementwise = x * y # 矩阵乘法 matrix = x @ y print("逐元素乘法:\n", elementwise) print("矩阵乘法:\n", matrix)输出结果:
逐元素乘法: tensor([[ 5., 12.], [21., 32.]]) 矩阵乘法: tensor([[19., 22.], [43., 50.]])可以看到,两者结果完全不同。逐元素乘法是每个对应位置相乘,矩阵乘法是行与列的内积运算。如果拿不准该用*还是@,可以先问自己一个问题:这次运算的目的是对每个位置的值独立变换(如缩放、门控),还是让数据在不同特征维度之间进行线性组合?前者用*,后者用@。
4.5 其他矩阵运算:转置、批量矩阵乘法与 einsum
在深度学习代码中,转置也是高频操作:
import torch x = torch.randn(3, 5) print(x.shape) print(x.T.shape)输出结果:
torch.Size([3, 5]) torch.Size([5, 3])批量矩阵乘法torch.bmm与torch.matmul类似,但它要求两个输入必须严格都是三维,并且批量维度大小一致,不会自动广播:
import torch a = torch.randn(4, 3, 5) b = torch.randn(4, 5, 7) out = torch.bmm(a, b) print(out.shape)输出结果:
torch.Size([4, 3, 7])对于更复杂的高维矩阵运算,推荐掌握torch.einsum。它通过类似爱因斯坦求和约定的字符串来描述运算方式。例如矩阵乘法可以写成:
out = torch.einsum('ik,kj->ij', A, B)批量矩阵乘法可以写成:
out = torch.einsum('ijk,ikl->ijl', a, b)虽然einsum的语法需要额外学习,但在实现注意力机制、张量缩并等运算时非常直观,能显著减少对permute、reshape的依赖。
5. 广播机制:让形状不同的张量也能运算
5.1 为什么需要广播机制
如果两个张量的形状不一致,是不是完全无法进行逐元素运算?答案是否定的。PyTorch 提供了一套规则,让形状不同的张量在满足一定条件时自动扩展到相同形状,再进行逐元素运算。这套规则称为广播机制(Broadcasting)。
广播机制借鉴了 NumPy 的设计,目的是:
- 减少不必要的
expand操作代码。 - 让张量与标量、行向量与列向量之间的运算更加自然。
例如,给一批数据中的每个样本都加上同一个偏置向量,如果用循环逐个处理会非常繁琐。有了广播机制,可以直接让形状为[batch_size, feature_dim]的数据张量与形状为[feature_dim]的偏置张量相加,PyTorch 会自动完成扩展。
5.2 广播机制的三个核心规则
PyTorch 的广播规则可以总结为以下三条:
- 从尾部维度开始对齐,即从最右边的维度开始比较。
- 如果两个维度相等,则保持该维度。
- 如果其中一个维度为 1,则将该维度扩展到与另一个张量的维度相同。
- 如果两个维度既不相等且都不为 1,则无法广播,直接报错。
换句话说,判断两个张量是否可广播,先看尾部维度的组合。只有当每一对尾部维度满足“相等”或“其中一个为 1”时,广播才成立。
来看一组最简单的例子:
import torch x = torch.tensor([1.0, 2.0, 3.0]) # 形状 [3] y = torch.tensor([10.0, 20.0, 30.0]) # 形状 [3] z = x + y print(z.shape)没有任何广播需要,因为形状完全相同。
再看标量广播的例子:
import torch x = torch.tensor([1.0, 2.0, 3.0]) b = 10.0 z = x + b print(z)输出结果:
tensor([11., 12., 13.])标量10.0被隐式扩展成了与x相同形状的[10.0, 10.0, 10.0],然后逐元素相加。这在归一化操作中非常常见,例如对数据减去均值、除以标准差。
5.3 形状为 [3] 与 [3, 1] 的广播
广播机制最经典的场景是向量与矩阵之间的运算。下面这个例子中,右侧是列向量:
import torch A = torch.tensor([[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]]) # 形状 [2, 3] b = torch.tensor([[10.0], [20.0]]) # 形状 [2, 1] C = A + b print(C)输出结果:
tensor([[11., 12., 13.], [24., 25., 26.]])这里发生了什么?从尾部维度开始看:
- 第一个尾部维度:
3与1比较,1被扩展为3。 - 第二个尾部维度:
2与2相等,保持不变。
所以b的每一行都被水平扩展成了[10, 10, 10]和[20, 20, 20],再与A逐元素相加。
5.4 形状为 [3] 与 [2, 1, 3] 的广播
更高维度的广播稍微复杂,但规则一致。例如:
import torch x = torch.tensor([1.0, 2.0, 3.0]) # 形状 [3] y = torch.tensor([[[10.0, 20.0, 30.0]], [[40.0, 50.0, 60.0]]]) # 形状 [2, 1, 3] z = x + y print(z.shape)输出结果:
torch.Size([2, 1, 3])虽然x只有一维,但因为它缺失的维度从尾部对齐后与y的最后一个维度3相等,所以会自动扩展为[1, 1, 3],再按广播规则扩展到[2, 1, 3]。最终结果为:
tensor([[[11., 22., 33.]], [[41., 52., 63.]]])5.5 不可广播的情况
来看一个无法广播的例子:
import torch x = torch.randn(3, 4) y = torch.randn(3, 5) # z = x + y # 会报错尝试运行上面被注释掉的代码会得到类似下面的错误:
RuntimeError: The size of tensor a (4) must match the size of tensor b (5) at non-singleton dimension 1原因很简单:尾部维度4与5不相等,且没有一个维度为1,因此广播失败。
5.6 显式扩展:expand 与 repeat
虽然广播机制能够隐式地扩展张量,但有时候我们希望在代码中显式地控制维度扩展,方便阅读和维护。expand和repeat是两种常见方式。
expand不会真正复制数据,它只是改变了视图(view),因此内存开销较小:
import torch x = torch.tensor([[1.0], [2.0], [3.0]]) # 形状 [3, 1] y = x.expand(3, 4) print(y)输出结果:
tensor([[1., 1., 1., 1.], [2., 2., 2., 2.], [3., 3., 3., 3.]])repeat则会真实复制数据,内存开销更大:
z = x.repeat(1, 4) print(z)输出结果与expand相同,但z是一份全新的数据副本。在工程中,如果需要把某个向量复制多份参与后续运算,优先考虑expand;如果后续需要修改该张量且不希望影响原张量,可以使用repeat或先clone()再原地修改。
unsqueeze与squeeze也是广播机制中常用的维度调整操作:
import torch x = torch.tensor([1.0, 2.0, 3.0]) # 形状 [3] x1 = x.unsqueeze(0) # 在位置 0 插入维度 -> [1, 3] x2 = x.unsqueeze(1) # 在位置 1 插入维度 -> [3, 1] print(x1.shape, x2.shape)输出结果:
torch.Size([1, 3]) torch.Size([3, 1])5.7 广播机制在归一化场景中的实战
广播机制最常见的应用是数据中心化和归一化。假设我们有一个形状为[batch_size, seq_len]的特征矩阵,需要按行执行标准化:每行减去该行的均值,再除以该行的标准差。
import torch data = torch.randn(4, 6) # 按行求均值,结果形状为 [4] mean = data.mean(dim=1, keepdim=True) std = data.std(dim=1, keepdim=True) # 利用广播机制完成标准化 data_normalized = (data - mean) / std print(mean.shape) print(data_normalized.shape)这里的关键在于keepdim=True。如果不加keepdim=True,mean的形状是[4],它与data的尾部维度6并不匹配,可能在广播时得到意料之外的结果。keepdim=True保持维度为[4, 1],这样广播规则就能正确地把mean沿最后一维扩展。
在深度学习中,BatchNorm、LayerNorm 的实现底层就大量使用了类似机制。可见广播不仅是一个语法技巧,更是理解现代网络结构的基础。
6. 综合实战:用张量运算实现一个线性回归模型
理解了逐元素计算、矩阵乘法和广播机制之后,我们通过一个完整的线性回归示例来串联这些知识。线性回归虽然简单,但它完整地展示了深度学习模型的训练闭环:前向传播、损失计算、反向传播、参数更新。
6.1 问题定义
我们构造一组线性关系的数据:
y = 2.0 * x + 1.0 + 噪声目标是通过梯度下降法学习出一个线性模型,使得模型输出尽可能接近真实值。这个示例中,我们会刻意使用本文讲解的三种运算来实现整个模型。
6.2 生成数据
import torch torch.manual_seed(42) # 生成 1000 个样本,每个样本有 2 个特征 X = torch.randn(1000, 2) true_w = torch.tensor([2.0, -3.0]) true_b = 1.0 # y 的真实生成公式 y = X @ true_w + true_b + 0.1 * torch.randn(1000)这里使用了矩阵乘法@让每个样本的特征进行线性组合,广播机制则让偏置true_b自动加到每个样本上。
6.3 定义模型与参数
w = torch.randn(2, 1, requires_grad=True) b = torch.zeros(1, requires_grad=True)requires_grad=True表示我们需要对这两个参数计算梯度。注意这里w的形状是[2, 1],这样把[1000, 2]与[2, 1]相乘时,结果的形状是[1000, 1]。
6.4 训练循环
learning_rate = 0.1 num_epochs = 100 y = y.reshape(-1, 1) # 转成列向量 [1000, 1] for epoch in range(num_epochs): # 前向传播:利用矩阵乘法与广播 y_pred = X @ w + b # 形状 [1000, 1] # 损失函数:均方误差,利用逐元素运算 loss = ((y_pred - y) ** 2).mean() # 反向传播 loss.backward() # 参数更新,使用梯度 with torch.no_grad(): w -= learning_rate * w.grad b -= learning_rate * b.grad # 梯度清零 w.grad.zero_() b.grad.zero_() if (epoch + 1) % 20 == 0: print(f"Epoch [{epoch + 1}/{num_epochs}], Loss: {loss.item():.4f}") print("学习到的 w:", w.detach().flatten()) print("学习到的 b:", b.detach().item())输出结果大致如下:
Epoch [20/100], Loss: 0.6464 Epoch [40/100], Loss: 0.1536 Epoch [60/100], Loss: 0.0882 Epoch [80/100], Loss: 0.0395 Epoch [100/100], Loss: 0.0207 学习到的 w: tensor([ 1.9528, -2.9801]) 学习到的 b: 1.0128可以看到,学习到的参数与真实参数w=[2.0, -3.0]、b=1.0非常接近,说明模型训练成功。
6.5 实战代码中的运算拆解
我们来拆解一下这段代码中到底使用了本文讲的哪几种运算:
X @ w:矩阵乘法,完成特征线性组合。+ b:由于b的形状是[1],广播机制会自动将其扩展到[1000, 1]。y_pred - y:逐元素减法,计算每个样本的误差。(y_pred - y) ** 2:逐元素平方。.mean():对所有元素求平均,这是一个归约操作。
可以发现,真实模型代码几乎不会只用单一运算,而是多种运算的组合。理解每种运算的语义,才能在阅读源码时准确推断出每个张量的形状变化。
6.6 使用 NumPy 对比矩阵乘法维度
为了加强对矩阵乘法维度的记忆,这里再给出一个用 NumPy 风格的形状推导法。对于任意两个张量:
A.shape = [..., m, k] B.shape = [..., k, n] 则 A @ B 的 shape 为 [..., m, n]其中...代表批量维度,且批量维度要么相同,要么满足广播规则。做矩阵乘法前,先在心里默念一遍这个公式,可以避免大量形状错误。
7. 常见问题与排查思路
7.1 矩阵乘法维度不匹配
错误现象:
RuntimeError: mat1 and mat2 shapes cannot be multiplied (2x3 and 2x3)原因:矩阵乘法的要求是第一个矩阵的列数必须等于第二个矩阵的行数。如果出现上面的错误,说明把两个形状为[2, 3]的张量直接相乘了。
解决思路:
- 检查是否需要转置,例如
x.T @ y。 - 检查维度顺序是否正确,借助
permute或reshape调整。 - 确认是不是误用了
*导致所有尺寸都相同但含义不同。
7.2 广播维度不兼容
错误现象:
RuntimeError: The size of tensor a (4) must match the size of tensor b (5) at non-singleton dimension 1原因:两个张量在某个维度上不相等,且没有维度为 1,无法广播。
解决思路:
- 打印两个张量的
shape,确认维度对齐情况。 - 如果其中一个张量的维度过少,可以用
unsqueeze补上缺失的维度。 - 如果需要按列进行运算,记得使用
keepdim=True保持维度结构。
7.3 数据类型不一致
错误现象:
RuntimeError: expected scalar type Long but found Float原因:例如整数张量与浮点张量直接相加,或把float32张量传入要求int64索引的函数中。
解决思路:
x = x.float() # 转成 float32 y = y.long() # 转成 int64在深度学习中,通常统一使用float32作为默认精度。
7.4 设备不一致
错误现象:
RuntimeError: Expected all tensors to be on the same device, but found at least two devices, cuda:0 and cpu!原因:一个张量在 GPU 上,另一个在 CPU 上,二者无法直接运算。
解决思路:
a = a.to('cuda') b = b.to('cpu')或者在训练代码开头统一将模型与数据迁移到同一个设备上。建议在代码中提前定义设备常量:
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')然后统一调用.to(device)。
7.5 inplace 操作导致反向传播错误
错误现象:
RuntimeError: a leaf Variable that requires grad is being used in an in-place operation.原因:对需要梯度计算的张量执行了 inplace 修改,例如w.data.add_(...)或w.add_(...),破坏了自动求导所需的计算图。
解决思路:
- 参数更新尽量使用
with torch.no_grad():包裹,并采用:
w = w - learning_rate * w.grad- 确实需要 inplace 操作时,使用
w.data或w.detach()后再操作,但要理解这样不会影响计算图中的梯度传递。
7.6 排查清单
如果遇到张量运算相关错误,可以按以下顺序排查:
| 顺序 | 检查项 | 操作 |
|---|---|---|
| 1 | 打印 shape | print(x.shape, y.shape) |
| 2 | 检查 dtype | print(x.dtype, y.dtype) |
| 3 | 检查 device | print(x.device, y.device) |
| 4 | 判断运算类型 | 这次是逐元素运算还是矩阵乘法? |
| 5 | 判断是否需广播 | 尾部维度是否满足相等或某一维度为 1 |
| 6 | 是否是 inplace 操作 | 是否误用了add_、mul_ |
8. 最佳实践与工程建议
8.1 代码中明确标注张量形状
深度学习代码的可读性很大程度上依赖于形状注释。尤其在实现模型时,建议在关键位置添加注释,说明每个张量当前的形状:
# 输入特征: [batch_size, seq_len, feature_dim] x = torch.randn(32, 10, 64) # 权重矩阵: [feature_dim, hidden_dim] W = torch.randn(64, 128) # 线性变换: [batch_size, seq_len, hidden_dim] h = x @ W这不仅能帮助自己理清思路,也能让读者快速理解代码。
8.2 尽量使用 @ 运算符表示矩阵乘法
在 PyTorch 代码中,@是torch.matmul的简写形式。相比于torch.mm和torch.bmm,@同时支持二维和高维张量,代码更简洁,也能减少因维度不同而切换 API 的麻烦。
8.3 善用 unsqueeze 与 keepdim 控制维度
在涉及均值、求和、最大值等归约操作时,一定要留意维度是否被压缩。建议使用keepdim=True保持维度结构,这样后续广播才能正常工作。
x = torch.randn(4, 6) mean = x.mean(dim=1, keepdim=True) # 形状 [4, 1]8.4 避免隐式广播带来的性能浪费
虽然广播机制很方便,但过度依赖隐式广播也可能导致代码难以理解。例如:
# 不推荐:隐式广播 + 缺少形状说明 result = (x - mean) * scale # 推荐:明确说明形状与语义 # mean: [batch_size, 1] # scale: [feature_dim] result = (x - mean) * scale在性能敏感的场景下,如果某个张量会被反复广播,可以提前使用expand或repeat构造显式形状,从而避免每次运算都判断广播规则。
8.5 关注数值类型与精度
深度学习中默认使用float32。在某些场景下,为了减少显存占用,可以使用float16或bfloat16,但需要注意数值溢出和梯度缩放问题。在 CPU 上训练时,float64虽然精度更高,但速度显著变慢,通常不建议在模型训练中使用。
如果需要对模型进行量化或混合精度训练,建议参考 PyTorch 官方torch.autocast与torch.amp的文档,结合具体硬件能力来配置。
8.6 设备一致性建议
建议在项目入口处统一定义设备,避免在多个文件里反复出现cuda、cpu硬编码:
import torch if torch.cuda.is_available(): device = torch.device('cuda') else: device = torch.device('cpu') print("Using device:", device)所有模型参数与数据都统一调用.to(device)。如果涉及多 GPU 训练,则需要进一步考虑torch.nn.DataParallel或torch.distributed,但这超出本文范围,后续可以单独展开。
8.7 用断言验证中间形状
在复杂的模型实现中,可以在关键位置添加assert来校验形状:
assert x.dim() == 3, f"Expected 3D input, got {x.dim()}D" assert x.shape[-1] == feature_dim, f"Expected last dim {feature_dim}, got {x.shape[-1]}"这种方式能在早期捕获形状错误,避免错误信息在多层传播后被掩埋。
8.8 关注并行训练中的张量并行概念
随着模型规模越来越大,张量并行已经成为大模型训练中的关键话题。简单来说,张量并行(Tensor Parallelism)是指将模型中的权重矩阵按维度切分到多张 GPU 上,让每张 GPU 只负责一部分计算,再通过集合通信将结果拼接起来。
虽然张量并行涉及的知识远超出本节课范围,但它的基础仍然是矩阵乘法和形状变换。如果你能够熟练推导权重矩阵的切分方式与中间结果的形状,后续学习大规模并行训练框架时会轻松很多。
9. 总结与下一步学习路线
本文围绕 PyTorch 张量运算的三大核心规则展开:
- 逐元素计算:形状相同的张量按位置独立计算,运算符包括
+、-、*、/、**以及对应函数 API,适用场景是激活、归一化、损失计算等。 - 矩阵乘法:使用
@或torch.matmul,强调最后一个维度与倒数第二个维度匹配,适用场景是线性变换、注意力计算、批量矩阵运算。 - 广播机制:从尾部维度对齐,满足“相等”或“其中一个为 1”即可扩展,适用场景是偏置加法、数据中心化、标准化等。
同时,通过完整的线性回归实战代码,我们看到了三种运算如何协同工作。建议读者在本地运行这段代码,尝试修改数据维度、损失函数、学习率等超参数,观察结果变化。
如果本文内容你已经完全掌握,下一步可以继续学习:
- PyTorch 的自动求导机制:理解
requires_grad、backward、计算图的内部逻辑。 - 神经网络基础模块:
torch.nn.Linear、torch.nn.Conv2d、torch.nn.LayerNorm等常用层,它们内部封装的就是我们本文讲到的运算。 - 常见模型源码阅读:可以先从多层感知机、卷积神经网络入手,再看 Transformer 中的 QKV 注意力计算。
写代码和看教程是两回事。建议在阅读完本文后,再花半小时亲自运行一遍示例,修改形状、加入新的运算、尝试触发广播错误,这些“试错”过程带来的理解深度远超只看文章本身。如果运行过程中遇到任何报错,欢迎回到本文的《常见问题与排查思路》一节对照排查。希望这篇教程能帮你迈过张量运算这个门槛。