PyTorch张量运算详解:逐元素、矩阵乘法与广播机制
2026/8/29 3:49:33 网站建设 项目流程

PyTorch 的基础是张量,张量的魅力不仅在于可以像数组一样存取数据,更在于那一套简洁却极富表现力的运算规则。很多初学者在刚接触 PyTorch 时,会被torch.mmtorch.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 张量的三个关键属性

在开始运算之前,建议先掌握张量的三个属性:shapedtypedevice

  • shape决定了张量的维度结构,任何运算都必须满足形状约束。
  • dtype表示张量中元素的类型,例如torch.float32torch.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: cpu

1.3 为什么张量运算规则如此重要

深度学习模型在训练和推理过程中,本质上就是不断对张量进行运算。卷积、池化、全连接层、注意力机制、损失函数,最终都可以拆解为以下三类操作的组合:

  • 逐元素运算:对张量中每个位置的元素独立进行计算。
  • 矩阵乘法 / 批量矩阵乘法:对数据维度进行线性变换。
  • 广播机制:让不同形状的张量在满足一定规则时自动对齐参与运算。

换句话说,如果这三类操作掌握扎实,阅读模型源码时就不会再惧怕那些复杂的高维张量表达式。本文的核心目标,就是把这三种运算彻底讲清楚。

2. 环境准备与版本说明

2.1 运行环境检查

本文示例基于 PyTorch 2.x 编写。如果你的环境安装的是 PyTorch 1.x,绝大多数 API 仍然兼容,但部分打印输出可能有细微差异。

建议先运行下面的命令确认版本:

python -c "import torch; print(torch.__version__)"

如果还没有安装 PyTorch,可以根据自己的系统环境选择安装方式。CPU 版本可以直接通过 pip 或 conda 安装:

pip install torch

GPU 版本建议到 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.subtorch.multorch.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_andtorch.logical_ortorch.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.logtorch.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 = 58

4.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.bmmtorch.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的语法需要额外学习,但在实现注意力机制、张量缩并等运算时非常直观,能显著减少对permutereshape的依赖。

5. 广播机制:让形状不同的张量也能运算

5.1 为什么需要广播机制

如果两个张量的形状不一致,是不是完全无法进行逐元素运算?答案是否定的。PyTorch 提供了一套规则,让形状不同的张量在满足一定条件时自动扩展到相同形状,再进行逐元素运算。这套规则称为广播机制(Broadcasting)。

广播机制借鉴了 NumPy 的设计,目的是:

  • 减少不必要的expand操作代码。
  • 让张量与标量、行向量与列向量之间的运算更加自然。

例如,给一批数据中的每个样本都加上同一个偏置向量,如果用循环逐个处理会非常繁琐。有了广播机制,可以直接让形状为[batch_size, feature_dim]的数据张量与形状为[feature_dim]的偏置张量相加,PyTorch 会自动完成扩展。

5.2 广播机制的三个核心规则

PyTorch 的广播规则可以总结为以下三条:

  1. 从尾部维度开始对齐,即从最右边的维度开始比较。
  2. 如果两个维度相等,则保持该维度。
  3. 如果其中一个维度为 1,则将该维度扩展到与另一个张量的维度相同。
  4. 如果两个维度既不相等且都不为 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.]])

这里发生了什么?从尾部维度开始看:

  • 第一个尾部维度:31比较,1被扩展为3
  • 第二个尾部维度:22相等,保持不变。

所以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

原因很简单:尾部维度45不相等,且没有一个维度为1,因此广播失败。

5.6 显式扩展:expand 与 repeat

虽然广播机制能够隐式地扩展张量,但有时候我们希望在代码中显式地控制维度扩展,方便阅读和维护。expandrepeat是两种常见方式。

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()再原地修改。

unsqueezesqueeze也是广播机制中常用的维度调整操作:

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=Truemean的形状是[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
  • 检查维度顺序是否正确,借助permutereshape调整。
  • 确认是不是误用了*导致所有尺寸都相同但含义不同。

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.dataw.detach()后再操作,但要理解这样不会影响计算图中的梯度传递。

7.6 排查清单

如果遇到张量运算相关错误,可以按以下顺序排查:

顺序检查项操作
1打印 shapeprint(x.shape, y.shape)
2检查 dtypeprint(x.dtype, y.dtype)
3检查 deviceprint(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.mmtorch.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

在性能敏感的场景下,如果某个张量会被反复广播,可以提前使用expandrepeat构造显式形状,从而避免每次运算都判断广播规则。

8.5 关注数值类型与精度

深度学习中默认使用float32。在某些场景下,为了减少显存占用,可以使用float16bfloat16,但需要注意数值溢出和梯度缩放问题。在 CPU 上训练时,float64虽然精度更高,但速度显著变慢,通常不建议在模型训练中使用。

如果需要对模型进行量化或混合精度训练,建议参考 PyTorch 官方torch.autocasttorch.amp的文档,结合具体硬件能力来配置。

8.6 设备一致性建议

建议在项目入口处统一定义设备,避免在多个文件里反复出现cudacpu硬编码:

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.DataParalleltorch.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_gradbackward、计算图的内部逻辑。
  • 神经网络基础模块:torch.nn.Lineartorch.nn.Conv2dtorch.nn.LayerNorm等常用层,它们内部封装的就是我们本文讲到的运算。
  • 常见模型源码阅读:可以先从多层感知机、卷积神经网络入手,再看 Transformer 中的 QKV 注意力计算。

写代码和看教程是两回事。建议在阅读完本文后,再花半小时亲自运行一遍示例,修改形状、加入新的运算、尝试触发广播错误,这些“试错”过程带来的理解深度远超只看文章本身。如果运行过程中遇到任何报错,欢迎回到本文的《常见问题与排查思路》一节对照排查。希望这篇教程能帮你迈过张量运算这个门槛。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询