
如果你是刚接触机器学习的开发者第一节课很可能不是数学模型而是先见到一个高频词张量Tensor。很多人在这一步就开始犯迷糊——它和数组什么关系和矩阵又有什么区别为什么 PyTorch 里一切操作都叫 Tensor连框架本身都用 Tensor 命名更让人头疼的是刚学完 Python 的 list 和 NumPy 的 ndarray打开 PyTorch 文档又看到torch.tensor(...)很快就分不清这些概念到底谁是谁。结果写代码的时候今天报shape mismatch明天报device mismatch后天报dtype不一致每一步都在跟“维度”作斗争。我的判断是张量不是什么高深莫测的数学对象它是机器学习框架为了训练神经网络而重新设计的一种数据结构——把多维数组和现代硬件加速、自动求导绑定在一起。你只有把这一层看透后面看模型源码、写训练循环、调 GPU 显存才不会被各种形状问题卡住。这篇文章会围绕张量的本质展开讲清楚它到底解决了什么问题与数组、矩阵、数据容器的边界在哪里再用 PyTorch 从创建、运算、广播到自动求导做一遍完整演示。读完之后你能理解张量的核心概念也能独立排查开发中最常见的形状、类型、设备错误。1. 这篇文章真正要解决的问题本文不打算堆概念先回答三个读者普遍关心的问题。第一个问题张量到底和数组、矩阵有什么区别这个问题如果只用“张量是高维矩阵”来回答等于没回答。因为矩阵已经能表示二维数据而真实世界的数据往往是更高维的一张彩色图片是三维一个批量的小批量图片是四维一条带 batch 的文本序列是二维到三维之间的结构。更重要的是矩阵本身没有“设备”“梯度”“计算图”这些概念张量有。第二个问题为什么从机器学习入门开始就绕不开张量因为机器学习的整个数据流转过程——原始数据输入、特征变换、模型权重、前向传播、损失函数、反向传播——全部以张量运算的形式存在。你训练一个神经网络本质上就是在不断调整一组张量的数值让损失函数变小。第三个问题新手最容易在哪些地方踩坑从我见过的初学者代码来看集中在三处一是维度对不上二是数据类型不一致三是 CPU 和 GPU 上的张量不能混算。这三类问题在刚接触张量时几乎都会出现本文会在第 7 章统一梳理排查思路。什么样的人最适合读这篇文章包括但不限于刚学完 Python准备进入机器学习或深度学习开发的人已经写过一些 NumPy想搞懂 PyTorch 底层数据结构的开发者在做机器学习课程作业或实验时被 shape 报错反复折磨的学生想从原理层面弄懂自动求导、计算图为后续看 Transformer、CNN 源码打基础的人。一句话总结读懂了张量就拿到了阅读一切深度学习源码的入场券。读不懂张量后面的模型代码都会变成“现象级编程”——照着抄能跑改一行就崩。2. 张量的核心概念从标量到多维数据容器张量的英文是 Tensor词典里常翻译为“张量”听起来很学术。但在机器学习的语境下你可以先把它理解为带有统一数据类型、固定形状、并能参与自动求导的多维数组容器。2.1 标量、向量、矩阵与张量的关系我们从数据维度的角度来梳理这比直接背数学定义更直观。数学对象维度形状示例生活中的类比标量 Scalar0 维()一个数比如气温 26 度向量 Vector1 维(3,)一列数比如三天的气温观测值矩阵 Matrix2 维(3, 4)一张二维表格比如学生的成绩单张量 Tensor3 维及以上(2, 3, 4)一批二维表格叠在一起或一段视频的连续帧你会发现标量、向量、矩阵其实都是张量的特例。严格来说张量是“多维数组”的统称标量是 0 维张量向量是 1 维张量矩阵是 2 维张量。但在日常交流中我们用“张量”时通常默认指三维及以上的高维数据。这种表达上的细微差别也正是很多初学者困惑的来源文档说“Tensor is a multidimensional matrix”你打开代码却发现一个一维的torch.tensor([1,2,3])也被称为 Tensor。其实这不矛盾只是一维数据被当作“秩为 1 的张量”来看待而已。2.2 张量的四个关键属性要真正掌握张量只需要抓住四个属性。第一个是形状shape。它描述每个维度的大小是排错时最常看的属性。x.shape输出(3, 4)表示这个张量有三行四列。第二个是维度数ndim也叫秩rank。x.ndim输出 2说明它是二维数据结构。注意区分shape和ndim一个是具体每维的长度一个是总共有多少维。第三个是数据类型dtype。张量里的所有元素必须是同一种类型常见的有torch.float32、torch.float64即 double、torch.int64即 long等。这一点和 NumPy 的 ndarray 很像本质上都是“同质数组”。第四个是设备device。张量可以存在于 CPU 上也可以存在于 GPU 上。CPU 和 GPU 上的张量不能直接参与混合运算。这个概念是机器学习框架特有的NumPy 数组只有一个“内存里”的存在方式而张量还多了“显存里”的存在方式。2.3 一个容易忽略的事实张量比多维数组多了一层“计算引擎”如果只是看存储结构张量和 NumPy 数组几乎一样都是同质多维数组。但张量多了两个特殊能力。第一个是 GPU 加速。把数据转换成张量后可以搬运到显卡上利用 GPU 的并行计算能力做大规模矩阵乘法这是现代深度学习训练能够跑起来的物理基础。第二个是自动求导。PyTorch 的张量在构建运算关系时会记录一个计算图computational graph。当你对最终结果调用backward()时框架会自动沿计算图回传梯度。这意味着你可以定义任意复杂的前向运算然后让框架替你完成链式求导而不需要手动推导梯度公式。这才是“张量”和“数组”最本质的分界线数组只是数据的容器而张量是“数据容器 计算引擎”的组合体。3. 为什么机器学习离不开张量理解了张量的定义再看它在机器学习中的位置会清晰很多。简而言之整个深度学习生命周期都建立在张量之上。3.1 数据表示一切数据都可以编码成张量图像是典型的高维张量。一张 RGB 彩色图片理论上可以表示为(通道数, 高度, 宽度)比如(3, 32, 32)。如果一次处理 64 张图片数据就变成(64, 3, 32, 32)这就是一个四维张量。中间那个 64 就是批量大小batch size。文本数据经过分词和词向量映射后也会变成张量。一个 batch 的句子通常表示为(batch_size, sequence_length, hidden_size)。例如(32, 128, 768)意思是 32 个句子每个句子 128 个 token每个 token 用一个 768 维的向量表示。表格数据、音频波形、时间序列也都可以在预处理阶段转换成不同形状的张量。可以说机器学习开发的第一步就是把真实世界的数据“张量化”。3.2 模型参数与中间计算权重是张量激活值也是张量一个线性层y Wx b中权重矩阵 W 和偏置 b 都是张量。神经网络中间每一层的输出也叫激活值仍然是张量。模型训练的目标就是不断调整这些权重张量的数值。如果你去看 PyTorch 里预训练模型的state_dict()会发现它就是一个由大量张量组成的字典。每个 key 对应网络层的名称每个 value 就是那一层的权重张量或偏置张量。这一设计贯穿所有深度学习框架。3.3 批量计算现代硬件需要“成块”的数据CPU 和 GPU 都是高度并行的设备它们喜欢一次处理一批数据而不是一条一条地处理。为了充分利用硬件我们通常把多个样本堆叠成 batch让矩阵运算在一大块数据上完成。batch 维是额外加上的张量维度。这也是为什么你会反复看到“四维张量”出现。没有 batch 的概念GPU 的并行能力无法充分发挥训练速度会慢到不可接受。3.4 自动求导张量承载了计算图信息训练神经网络时损失函数对每个参数的梯度决定了权重如何更新。如果让你手动计算每一层梯度的链式求导深度模型基本无法落地因为层数一旦加深推导复杂度会指数级增长。PyTorch 的方案是让张量自带“梯度记录”能力。只要你设置requires_gradTrue框架就会在前向传播时记录运算轨迹在调用backward()时自动完成反向传播。这时候张量就不只是一个数据容器还附带了一个关于“数据从哪来、经过哪些运算”的图结构。4. 环境准备与前置条件动手操作之前先搭好环境。由于本文用 PyTorch 做演示你需要准备 Python、PyTorch 和 NumPy。以目前的主流实践来看使用 conda 或 venv 创建独立的虚拟环境是较好的做法可以避免不同项目之间依赖冲突。4.1 安装 Python 与创建虚拟环境建议使用 Python 3.10 或更高版本。如果你已经安装了 Anaconda可以执行下面的命令创建环境conda create -n tensor-basics python3.10 -y conda activate tensor-basics如果你更习惯用 venv也可以python3 -m venv tensor-basics source tensor-basics/bin/activate虚拟环境的意义在于不同的机器学习项目可能依赖不同版本的 PyTorch将环境隔离可以避免“升级了 PyTorch 导致老项目跑不起来”这类问题。4.2 安装 PyTorchPyTorch 的安装命令与操作系统、是否使用 GPU 有关最稳妥的方式是打开 PyTorch 官方网站在首页选择对应的系统、包管理工具和 CUDA 版本复制生成的命令执行。如果你只是入门学习CPU 版本完全够用因为本文所有示例在小规模张量上运行CPU 就能瞬间完成。一个最小安装示例是pip install torch numpy装完后运行下面的命令验证环境是否正常python -c import torch; print(torch.__version__); print(torch.cuda.is_available())如果看到类似于2.x.x的版本号并且torch.cuda.is_available()返回False也是正常的——说明当前安装的是 CPU 版本或者当前机器没有可用的 NVIDIA GPU。入门阶段不用强求 GPU。5. 张量基础操作完整示例代码实现这一节会从创建、查询、形状操作、自动求导四个维度把张量最常用操作串起来写一遍。建议你打开环境边读边运行。5.1 创建张量创建张量是使用 PyTorch 的第一步。可以直接从数据创建也可以用初始化函数生成指定形状的张量。# 文件路径tensor_basics.py import torch import numpy as np # 从 Python 列表直接创建 a torch.tensor([[1.0, 2.0], [3.0, 4.0]]) print(从列表创建) print(a) print(dtype:, a.dtype) # 创建全 0 张量形状为 (2, 3) zeros torch.zeros((2, 3)) print(\n全 0 张量) print(zeros) # 创建全 1 张量形状为 (4,) ones torch.ones(4) print(\n全 1 张量) print(ones) # 创建单位矩阵形状为 (3, 3) eye torch.eye(3) print(\n单位矩阵) print(eye) # 创建服从标准正态分布的随机张量形状为 (2, 2) randn torch.randn(2, 2) print(\n随机张量) print(randn) # 从 NumPy 数组转换 numpy_array np.arange(6).reshape(2, 3) from_numpy torch.from_numpy(numpy_array) print(\n从 NumPy 转换) print(from_numpy)这段代码的关键在最后一行。torch.from_numpy()创建的张量和原 NumPy 数组共享底层内存修改其中一个会影响到另一个。如果你希望复制出一份独立数据应该使用torch.tensor(numpy_array)或torch.as_tensor()再配合克隆。5.2 查看张量的形状、维度、设备拿到一个不熟悉的张量时第一件事应该是打印它的形状和类型而不是直接往模型里塞。# 文件路径inspect_tensor.py import torch x torch.randn(2, 3, 4) print(shape:, x.shape) print(ndim:, x.ndim) print(dtype:, x.dtype) print(device:, x.device) print(元素个数:, x.numel())输出示例shape: torch.Size([2, 3, 4]) ndim: 3 dtype: torch.float32 device: cpu 元素个数: 24torch.Size([2, 3, 4])是 PyTorch 专用的形状对象本质上是一个 tuple因此可以直接用索引访问比如x.shape[0]得到 2。numel()是 number of elements 的缩写返回张量中元素总数。5.3 形状操作reshape、view、transpose 与 contiguous形状操作是学习张量时最需要花时间的部分。很多开发者的 shape 报错本质上都是不清楚张量底层如何组织。先看三个最常用的方法reshape、view、transpose。# 文件路径shape_ops.py import torch x torch.arange(24) print(原始形状:, x.shape) # reshape改变逻辑形状 a x.reshape(2, 3, 4) print(reshape 后:, a.shape) # view要求张量在内存中是连续的在可以复用内存时使用 b x.view(4, 6) print(view 后:, b.shape) # transpose交换两个维度 c torch.randn(3, 4) d c.transpose(0, 1) print(transpose 后:, d.shape) # 输出 torch.Size([4, 3])这里最容易混淆的是reshape和view。简单理解如果原始张量在内存中是连续存储的两者都可以用如果原始张量已经经过了转置等操作变得不连续那么view可能直接报错而reshape会优先尝试复用内存不行时再复制一份数据。transpose的返回值只是“换了逻辑上的解读方式”并没有真的在内存里把数据搬动。所以经过transpose的张量通常是非连续的。如果你接下来要调用一些要求连续内存的底层算子可能会遇到报错解决办法是调用.contiguous()把数据重新排布成连续内存。d_contiguous d.contiguous()在实际项目中一个比较推荐的写法是先统一用reshape当你确认某个张量不会有非连续风险并且关心性能时再考虑view。这样能少踩很多坑。5.4 自动求导张量的核心优势接下来用一个小例子演示自动求导。设 y x² 3x在 x2 处dy/dx 2x 3 7。# 文件路径autograd_demo.py import torch # requires_gradTrue 表示需要计算梯度 x torch.tensor([2.0], requires_gradTrue) y x ** 2 3 * x # 反向传播 y.backward() # 查看梯度 print(梯度:, x.grad)输出梯度: tensor([7.])这个例子虽然简单但它包含了自动求导的全部核心机制构建计算图、前向传播、反向传播、梯度累积到叶子张量。在设计上PyTorch 对叶子张量即用户创建的、requires_gradTrue的张量会保存.grad属性中间计算得到的张量默认不会保留历史梯度数据和图信息除非你显式调用retain_grad()。需要提醒的是requires_grad是“传染”的。如果输入张量需要梯度那么由它计算出来的新张量也会默认需要梯度。这在模型训练中是合理行为但如果你只做推理inference应该用torch.no_grad()上下文管理器临时关闭梯度计算以节省显存和加速。with torch.no_grad(): y_pred model(x)6. 张量运算与广播机制维度对齐的艺术创建张量只是第一步真正进入机器学习后会面对大量张量运算。这一节讲两个核心点基础运算和广播机制。6.1 基础运算PyTorch 支持逐元素运算和矩阵乘法两大类。逐元素运算包括加减乘除、比较、幂运算等矩阵乘法用运算符或torch.matmul。# 文件路径tensor_math.py import torch a torch.tensor([[1.0, 2.0], [3.0, 4.0]]) b torch.tensor([[5.0, 6.0], [7.0, 8.0]]) # 逐元素加法 print(逐元素加法:\n, a b) # 矩阵乘法 print(矩阵乘法:\n, a b) # 逐元素乘法Hadamard 积 print(逐元素乘法:\n, a * b)注意区分a * b和a b。前者是逐元素相乘要求两个张量形状相同后者是矩阵乘法要求 a 的最后一维等于 b 的倒数第二维。这两者在机器学习代码里都极其常见写混了会出现维度错误或完全错误的结果。6.2 广播机制小张量如何自动扩展到大形状广播broadcasting是 PyTorch 自动把不同形状的张量对齐到同一形状再进行运算的机制极大减少了手动复制数据的代码量。理解广播是理解“内存与张量对齐”的关键。广播的规则一句话概括从最后一个维度开始向前比较每一维度要么长度相等要么其中一个长度是 1否则无法广播。看一个经典例子# 文件路径broadcast_demo.py import torch # 形状 (3, 1) a torch.ones(3, 1) # 形状 (1, 4) b torch.ones(1, 4) c a * b print(c.shape) # torch.Size([3, 4])为什么(3, 1)和(1, 4)能相乘得到(3, 4)因为广播机制会把 a 沿着第二个维度“复制”扩展 4 份把 b 沿着第一个维度“复制”扩展 3 份。需要注意这里的“复制”是逻辑上的PyTorch 在底层并不一定真的为广播分配新的内存它通过步长stride技巧让运算看起来像扩展了。这也是新手理解内存对齐时报错的核心原因你以为数据已经被复制成相同形状了但底层可能仍然是非连续存储。再看一个不能广播的例子a torch.ones(3, 4) b torch.ones(2, 5) # 最后一维 4 和 5 不相等且没有一个是 1广播失败 c a b # RuntimeError这里的错误信息极其常见The size of tensor a (4) must match the size of tensor b (5) at non-singleton dimension 3。6.3 从内存布局理解形状错位很多真正棘手的问题不是“维度不同”而是“维度相同但语义不同”。比如一张图片既可以表示成(batch, height, width, channel)也可以表示成(batch, channel, height, width)也就是 NHWC 与 NCHW 两种布局。同一个数据换一种布局内存顺序就完全变了。如果两个张量逻辑形状相同但内存布局不同直接运算可能不报错但结果却与预期不符。排查这类问题可以打印张量的stride()属性看看每个维度在内存中占用的步长。我在实际代码里见到的多数“看起来没毛病但结果不对”往往就是这种暗中的布局不一致导致的。建议在项目开始阶段用注释为数据张量明确标注布局约定例如# image_tensor: (batch, channel, height, width), dtypefloat32这样团队成员在后续开发中就不会随意换布局。7. 张量开发中的常见问题与排查思路从运行报错来看初学者遇到最多的问题集中在维度、类型、设备、原地修改这四个方面。问题现象可能原因排查方式解决方案运行时报 shape mismatch两个张量形状无法广播或矩阵乘法维度不匹配在运算前打印x.shape和y.shape肉眼对齐最后几个维度用reshape、unsqueeze、squeeze调整形状报 dtype 不一致或无法转换整型张量和浮点张量混算或模型权重是 float32 输入是 int64打印x.dtype、y.dtype观察模型输入要求统一使用.float()、.long()或.to(torch.float32)GPU 与 CPU 张量混合运算报错.to(cuda)只迁移了部分张量模型和数据不在同一设备打印model.device与每个输入张量的.device把所有输入、标签、模型都统一迁移到同一设备in-place 操作导致 autograd 报错对requires_gradTrue的张量使用、**或x[0] ...查看报错信息中指向的行改成x x 1或先clone()再修改backward 得到 None某个中间张量没有参与损失计算或requires_grad未打开检查计算图中的叶子张量是否都设置了requires_grad给需要求导的参数设置requires_gradTrue结果差异很大但不报错数据布局混乱或 float32 与 float64 精度差异被放大打印张量stride()和dtype小范围对比中间结果统一布局约定和全局 dtype7.1 遇到 shape mismatch 的第一步不要盯着完整错误栈逐行读。先找到报错所在的那一行然后立刻打印参与运算的所有张量的shape再用广播规则逐个维度对齐。大部分 shape mismatch 问题都能在几十秒内定位。a torch.randn(2, 3, 4) b torch.randn(3, 4) print(a.shape:, a.shape) print(b.shape:, b.shape) # RuntimeError 时先看这两个输出 c a b7.2 关于 dtype 的一条实用经验PyTorch 默认浮点类型是 float32默认整型是 int64。在模型训练中推荐统一切到 float32。如果你从 NumPy 读入了一个 float64 数组再用它构造张量参与模型计算时可能会因为类型不匹配报错或者触发额外的类型转换降低训练效率。x torch.tensor(numpy_array, dtypetorch.float32)8. 最佳实践与工程建议张量操作的工程化比想象中更重要。下面几条是我认为对新手最有价值的建议。8.1 为项目制定数据形状约定项目里最好有一份简单的数据形状文档明确输入格式。例如图像(batch, channel, height, width)简称 NCHW文本(batch, sequence_length)id 类型为 int表格(batch, feature_dim)浮点类型为 float32。团队协作时这些约定能避免大量无意义的 shape 错误。即使个人开发固定约定也能减少心智负担。8.2 梯度清零PyTorch 的梯度是累积的也就是说每次backward()会把新的梯度加到已有梯度上。训练循环里如果不手动清零梯度会越积越大导致权重更新异常。推荐在训练循环开头统一调用优化器的清零方法# 每个 batch 开始前 optimizer.zero_grad()如果你的模型有自定义的中间张量需要保留梯度也可以调用model.zero_grad()。效果类似但optimizer.zero_grad()更精细只影响优化器管理和更新的参数。8.3 用torch.no_grad()做推理模型推理阶段不需要梯度。显式关闭梯度可以明显减少显存占用并提升速度。更重要的是能避免不小心让验证数据也进入计算图造成内存泄漏式的增长。model.eval() with torch.no_grad(): predictions model(batch_data)注意model.eval()和torch.no_grad()是两个不同概念。前者是切换模型的 dropout 和 batch norm 行为后者是关闭梯度记录两者经常一起使用但关系并不等价。8.4 固定随机种子保证实验可复现机器学习实验的可复现性非常关键。如果不固定随机种子每次运行结果都可能不同排查问题时很难判断是代码 bug 还是随机波动。# 文件路径fix_seed.py import random import numpy as np import torch def set_seed(seed: int 42): random.seed(seed) np.random.seed(seed) torch.manual_seed(seed) if torch.cuda.is_available(): torch.cuda.manual_seed(seed) torch.cuda.manual_seed_all(seed)8.5 控制数据在 CPU 和 GPU 之间的搬运GPU 显存昂贵数据搬运也有成本。尽量避免在训练循环中频繁调用.cpu()或.cuda()。正确做法是在循环外部把数据加载器设置成目标设备或每次取 batch 后统一迁移一次。一个低效示例for x, y in dataloader: x x.cuda() y y.cuda() # ...一个更可控的做法是在数据加载后统一处理并尽量保持 batch 的传输粒度。如果你做的是超大批量分布式训练还会涉及更复杂的分片和张量并行但那已经属于分布式训练的高级话题了。8.6 优先使用torch.utils.data.Dataset和DataLoader从工程化角度不建议手动写死数据切片。PyTorch 提供了标准化接口Dataset和DataLoader它们天然处理了张量转换、batch 堆叠、shuffle 和并行加载。# 文件路径simple_dataset.py import torch from torch.utils.data import Dataset, DataLoader class SimpleDataset(Dataset): def __init__(self, x_tensor, y_tensor): self.x x_tensor self.y y_tensor def __len__(self): return len(self.x) def __getitem__(self, index): return self.x[index], self.y[index] x torch.randn(20, 10) y torch.randint(0, 2, (20,)) dataset SimpleDataset(x, y) loader DataLoader(dataset, batch_size4, shuffleTrue) for batch_x, batch_y in loader: print(batch_x.shape, batch_y.shape)这段代码展示了张量如何与数据加载流程配合。写数据流的时候多用框架自带的类后续维护和扩展都会轻松很多。9. 总结与后续学习方向张量是机器学习框架中最基础、也最容易被低估的概念。它不是数组的另一个名字而是把多维数组、统一数据类型、GPU 加速、自动求导和计算图融为一体的专门设计。理解张量意味着你不再把 PyTorch 当成一个“神奇的 API 集合”而是能看懂每一步运算背后的数据怎么流动、形状怎么对齐、梯度怎么回来。本文真正讲透的点包括张量与数组矩阵的关系、决定张量的四个核心属性、机器学习离不开张量的原因、用 PyTorch 创建和操作张量的完整示例、广播机制与内存布局对齐、以及六类常见报错的排查思路。你已经可以拿着这些内容去分析一个最简单的线性回归模型里每一步的输入和输出形状了。下一步建议你按这样的路径继续实践先用torch.randn创建几个不同形状的张量练习 reshape、transpose、squeeze、unsqueeze 操作把所有操作之后的 shape 变化手写一遍接着用自动求导实现一个最小二乘法的梯度下降然后阅读torch.nn.Linear的源码或文档观察它的权重张量形状是如何与输入输出对齐的。如果你正在刷机器学习课程或者准备机器学习期末复习建议把张量这一章真正当成“地基”来对待不要跳过。后面的 CNN 卷积核是四维张量Transformer 的注意力矩阵是二维到三维张量的组合分布式训练里的张量并行则完全建立在张量切分之上。地基如果没打牢越往后越容易坍塌。遇到第一个 shape mismatch 的时候不用沮丧打开终端把x.shape和y.shape打出来一步一步对齐这本身就是掌握张量最好的方式。
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。