在机器学习与深度学习的学习过程中,张量(Tensor)几乎是最早出现也最容易被低估的概念。很多人第一次接触张量,是在 PyTorch 或 TensorFlow 的官方快速入门文档里,文档会告诉你要用torch.tensor()创建数据,然后送入模型训练。但真正到了写模型、调 bug 的时候,报错信息里最常见的却是 shape mismatch、dtype 不一致、device 不统一这类与张量息息相关的问题。如果只把张量理解成“一个装数据的数组”,后面很难定位这类问题。
这篇整理会把张量这条主线走完。先建立从标量、向量、矩阵到张量的直觉,再拆解张量的形状、维度、数据精度和设备四个核心属性,然后用 PyTorch 代码把创建、变形、运算、与 NumPy 互转这些高频操作跑一遍,接着回到机器学习模型的真实场景里看张量是如何组织图像、batch 和特征数据的。最后集中列出常见的报错现象和排查路径,并给出一份可复用的张量操作检查清单。
无论你是刚开始学机器学习、跟随视频课程做练习,还是期末复习阶段想系统梳理张量概念,都可以按顺序读下去。掌握张量不只是在学一个数据结构,它直接决定你后面能否顺利理解维度匹配、反向传播和模型部署。
1. 先建立直觉:张量是标量、向量和矩阵的推广
1.1 从标量到矩阵:一步一步看数据维度的变化
张量在英文里就是 Tensor,中文翻译成“张量”之后少了一些直观感。如果暂时不看严谨的数学定义,最稳妥的理解方式是把它看成多维数组。这个说法不是完全准确,但在机器学习的实践场景里足够帮助你建立第一层认知。
先看最简单的数据形态:
- 一个数,例如
3.14,叫标量(Scalar)。它没有排列关系,在 PyTorch 中是一个 0 维张量。 - 一排数,例如
[1, 2, 3],叫向量(Vector)。它有一个方向,是 1 维张量。 - 一个表格,例如两行三列的数,叫矩阵(Matrix)。它有行和列两个方向,是 2 维张量。
- 多个矩阵叠在一起,就形成 3 维张量。例如一段视频由很多帧图片组成,每一帧又是一张二维图。
所以“张量是标量、向量、矩阵的推广”这句话的意思是:当你看一张彩色图片时,它有高、宽、颜色通道三个维度;当你看一批图片时,又增加了一个样本序号维度。这些数据都比矩阵多一个或多个方向,必须用更高维度的结构来装。
这里容易误解的地方是:维度这个词在机器学习里经常和“轴”(Axis)混用。口语里说“这是一个三维张量”,指的是它有 3 个轴,也就是rank=3,而不是指它的某一维大小是 3。比如一个形状为[2, 3, 4]的张量,有三个轴,第二维的长度是 3。你听到“3D 张量”时,应该马上想到它有三个方向,而不是某个方向的长度。
1.2 为什么机器学习会用张量表达所有数据
机器学习模型本质上做的是数学运算。线性回归要算权重和特征的加权求和,神经网络要算矩阵相乘、卷积、激活函数、损失函数,这些运算都需要输入数据具有稳定的结构。张量就是这个结构。
一张 64x64 的灰度图片可以表示成[64, 64]的矩阵,每个位置是 0 到 255 的像素值;一张 64x64 的彩色图片可以表示成[3, 64, 64]的张量,第一维是 RGB 三个通道;如果一次处理 32 张这样的图片,数据就变成[32, 3, 64, 64]。模型看到的不是一个一个孤立像素,而是一个具有明确形状的数据块。
除了图片,文本数据也一样。一段话通过分词和索引编码后,会变成一个长度为 N 的整数序列;一批句子为了对齐长度会补成等长,于是变成[batch_size, seq_len]的张量;再加入词向量映射后,变成[batch_size, seq_len, embedding_dim]。
可以这样说:张量为机器学习提供了一套统一的“数据容器”。无论前面是图像、文本、表格还是音频,最终都要转换成数值型张量才能参与矩阵运算和梯度下降。这也是为什么你在任何深度学习框架中看到的输入输出、模型参数、中间特征都是张量。
2. 张量的四个核心属性:形状、维度、数据类型和设备
要描述一个张量,不能只说“一个大数组”。在代码层面,必须同时关注四个核心属性:形状(shape)、维度数(rank)、数据类型(dtype)和所在设备(device)。四个属性缺一个,程序都可能出错或产生性能问题。
| 属性 | 作用 | 典型取值 | 相关问题 |
|---|---|---|---|
| rank | 轴的数量 | 0、1、2、3、4 | 少一个轴或多一个轴,运算结果会偏离预期 |
| shape | 每个轴的长度 | [32, 3, 64, 64] | 长度不匹配会导致 matmul、concat 报错 |
| dtype | 元素的数值类型 | float32、int64、bool | CPU 张量与 GPU 张量对 dtype 要求严格 |
| device | 数据存放在 CPU 还是 GPU | cpu、cuda:0 | 设备不一致会直接抛异常 |
2.1 rank 和 shape 决定张量的结构
rank 是轴的数量,shape 是每个轴的长度。一个张量的 shape 可以写成 tuple 形式,例如torch.empty(2, 3, 4)会创建一个形状为(2, 3, 4)的三维张量。
在调试模型时,最常用的手段就是打印tensor.shape。看 shape 能快速发现很多问题:
- 全连接层的输入应该是
[batch_size, input_features],如果你得到的是[input_features],说明少了 batch 维。 - 卷积层的输入应该是
[batch_size, channels, height, width],如果你得到[height, width, channels],说明通道维的位置不对。 - 两个张量拼接时,除了拼接维度,其他维度的长度必须一致,否则
torch.cat会报错。
这里有一个基本功要练:看到 shape 之后,能在脑子里还原这个张量的排列顺序。比如(2, 3, 4),意思是第一轴长度 2,第二轴长度 3,第三轴长度 4。修改轴顺序时,要用permute而不是reshape,因为reshape只是重新划分元素,并不会交换维度之间的语义。
2.2 dtype 决定数值精度和内存占用
dtype 是一个容易被忽视但在实际运行中经常出问题的属性。PyTorch 中常见的 dtype 包括 float32、float64、int64、int32、bool 等。深度学习训练默认使用 float32,因为它在精度和显存占用之间取得了一个较好的平衡。
| dtype | 说明 | 内存占用(每个元素) | 常见用途 |
|---|---|---|---|
| float32 | 单精度浮点 | 4 字节 | 模型默认参数与中间特征 |
| float64 | 双精度浮点 | 8 字节 | 数值分析、部分数据处理 |
| float16 / bfloat16 | 半精度浮点 | 2 字节 | 混合精度训练、推理加速 |
| int64 | 长整型 | 8 字节 | 标签、索引 |
| int32 | 整型 | 4 字节 | 部分数据处理 |
| bool | 布尔型 | 1 字节 | 掩码、判断结果 |
为什么训练时普遍用 float32 而不是 float64?核心原因是 GPU 对 float32 的矩阵计算经过大量优化,显存占用也更低。float64 精度更高,但显存翻倍,计算速度通常更慢,对大多数神经网络任务并不必要。float16 能明显提速,但数值范围小,容易出现溢出和梯度消失问题,所以使用混合精度训练时还需要额外的损失缩放策略。
注意:不要在没有明确必要的情况下,把 float32 的数据随手转成 float64。很多人只在报错后才检查 dtype,真正合理的做法是在创建数据集时统一约定数据精度。
2.3 device 决定计算发生的位置
device 属性决定了张量在 CPU 显存还是 GPU 显存中。CPU 张量和 GPU 张量不能直接参与同一运算。常见报错是Expected all tensors to be on the same device。解决办法是让参与运算的所有张量都走到同一个设备上。
在代码中通常这样管理设备:
device = torch.device("cuda" if torch.cuda.is_available() else "cpu") model = model.to(device) data = data.to(device) labels = labels.to(device)需要注意的是,张量在 CPU 和 GPU 之间拷贝是有代价的。训练循环里不应该反复进行.cpu()和.to(device),这会造成同步等待,拖慢训练。正确做法是在数据加载到设备后尽量保持设备一致,只在最终输出或可视化时转到 CPU。
3. 用 PyTorch 动手创建和操作张量
前面讲的是概念,这一节进入实际代码。下面示例均以 PyTorch 为例。如果你用的是 TensorFlow,核心概念相同,只是 API 名称不同,可以先跑通 PyTorch 再对照 TensorFlow 文档迁移。
3.1 创建张量的推荐方式
创建张量有很多入口,不同入口对应不同的语义:
import torch import numpy as np # 从 Python 列表创建,系统推断 dtype a = torch.tensor([1, 2, 3]) # int64 b = torch.tensor([1.0, 2.0, 3.0]) # float32 # 创建全 0、全 1、随机张量 zeros = torch.zeros(2, 3) ones = torch.ones(2, 3) randn = torch.randn(2, 3) # 标准正态分布 # 创建指定范围或类似结构的张量 arange_tensor = torch.arange(0, 12) # 0 到 11 like_tensor = torch.zeros_like(randn) # 保持 shape 和 dtype 一致这里的关键点是torch.tensor()和torch.Tensor()的区别。torch.tensor(data)会复制传入数据,并尝试推断 dtype;torch.Tensor()是类构造器,等价于torch.FloatTensor(),默认创建 float32 张量。实际项目中推荐显式使用torch.tensor或torch.zeros、torch.ones这类语义明确的 API,并在需要时指定dtype=torch.float32,不要依赖隐式推断。
创建张量后可以打印 shape、dtype、device 来确认:
print(randn.shape) # torch.Size([2, 3]) print(randn.dtype) # torch.float32 print(randn.device) # cpu3.2 形状转换和轴操作
形状转换是使用频率最高的操作。常见需求包括把一维数据变成二维、交换轴顺序、增加或删除维度。
x = torch.arange(12) # shape [12] x_2d = x.view(3, 4) # shape [3, 4] x_3d = x.reshape(2, 2, 3) # shape [2, 2, 3] # 增加维度 x_row = x.unsqueeze(0) # shape [1, 12] x_col = x.unsqueeze(1) # shape [12, 1] # 交换轴顺序 permuted = x_2d.permute(1, 0) # shape [4, 3] transposed = x_2d.T # 两者等价view和reshape的区别是新手容易踩的坑。view要求张量内存连续,如果对permute或transpose后的张量调用view,可能报错或产生非预期结果;reshape在内存不连续时会自动拷贝数据,更通用,但因为可能发生拷贝,它会返回新张量,不保证与原张量共享内存。
想修改某个轴的顺序,要使用permute。例如把[batch, height, width, channel]转成[batch, channel, height, width],这是一个语义层面的转换,必须用permute而不能用reshape。
3.3 张量运算与广播机制
张量之间支持逐元素运算和矩阵运算。逐元素运算要求两个张量形状兼容;矩阵运算要求维度满足矩阵乘法规则。
a = torch.tensor([1.0, 2.0, 3.0]) b = torch.tensor([4.0, 5.0, 6.0]) c = a + b # tensor([5., 7., 9.]) d = a * b # 逐元素乘法,不是矩阵乘法 dot = torch.dot(a, b) # 点积,结果为 32.0 A = torch.randn(3, 4) B = torch.randn(4, 5) C = A @ B # 矩阵乘法,结果 shape [3, 5]广播机制是张量运算里最重要的规则之一。它允许不同形状的张量在满足特定条件时直接运算,而不需要手动复制数据。规则是:从最后一个维度开始比较,如果两个维度相等,或者其中一个是 1,或者一方缺失,就可以广播。
x = torch.tensor([[1, 2, 3], [4, 5, 6]]) # shape [2, 3] y = torch.tensor([10, 20, 30]) # shape [3] z = x + y # y 广播成 [2, 3]广播机制让代码简洁,但也容易隐藏错误。比如形状[3, 1]和[1, 4]的矩阵相加,结果会变成[3, 4],如果你原本想实现的是两个矩阵逐元素相加,这种隐式扩展很可能得到错误结果而不报错。因此,对关键运算要主动检查结果的 shape。
3.4 与 NumPy 的互转和内存共享
NumPy 和 PyTorch 的数据转换非常高频。常见场景是先用 Pandas 或 NumPy 做数据处理,再转成 Tensor 送入模型。
arr = np.array([[1.0, 2.0], [3.0, 4.0]]) tensor_from_numpy = torch.from_numpy(arr) # 直接从 ndarray 转换 arr_back = tensor_from_numpy.numpy() # Tensor 转 ndarray # 显式复制转换 tensor_copy = torch.tensor(arr)这里有一个容易忽略的细节:torch.from_numpy()和tensor.numpy()默认与原始 NumPy 数组共享底层内存。也就是说,修改 Tensor 可能改变原数组的值,修改 NumPy 数组也可能改变 Tensor 的值。如果你的代码里后续还会操作原始数组,并且不希望互相影响,应使用torch.tensor(arr)这类复制方式,或调用.clone()。
注意:在使用
torch.from_numpy时,若后续修改了其中一个对象,另一个也可能同步变化。这不是框架 bug,而是内存共享设计。需要隔离时,务必显式 clone。
4. 张量在机器学习流程中的典型场景
4.1 图像数据为什么要带通道维
图像是最典型的非表格数据。一张灰度图像本质是二维矩阵,但彩色图像有 RGB 三个通道,不能用一个矩阵装下,于是引入第三个轴。PyTorch 中默认的图像张量布局是[C, H, W],C 是通道,H 是高度,W 是宽度。
到批处理时,图像张量再扩展到[B, C, H, W]。B 是 batch size,表示一批有多少张图像。卷积神经网络在计算时,会按通道做卷积,例如输入为[1, 3, 224, 224]时,表示一张 224x224 的彩色图片。
如果你用到 TensorFlow,它的默认布局在历史版本中是[B, H, W, C],也就是通道维在最后,这就是常说的 NHWC。PyTorch 的 NCHW 与 TensorFlow 的 NHWC 是两条不同的内存组织方式。两者在效率上会受框架和硬件影响,但更重要的是你要清楚自己的数据当前是哪种布局,否则模型预处理部分会错得无声无息。
4.2 批量训练靠 Batch 维组织数据
深度学习训练几乎不会一次只看一个样本。一次处理一批样本,能充分利用 GPU 并行能力,也能让梯度估计更稳定。于是几乎所有训练数据都会加上最前面的 batch 维。
假设单个样本是长度为 128 的特征向量,单个样本 tensor 的 shape 是[128]。一个 batch 包含 32 个样本时,shape 就变成[32, 128]。模型权重矩阵的第一维通常会设计成与特征维匹配,例如全连接层nn.Linear(128, 64),输入[32, 128]后输出[32, 64]。
很多人在处理表格数据时容易遗忘 batch 维。单个样本喂给模型是[128],模型在某些框架下会把它当作[1, 128]自动处理,但写成自定义损失函数或矩阵运算时,缺少 batch 维就可能产生维度不匹配。建议养成习惯:输入模型的张量,最前面一定是 batch 维。
4.3 模型参数、中间特征和梯度都是张量
张量不仅用于存储输入和输出。神经网络模型的每一层权重都是张量,例如nn.Linear(128, 64)的权重 shape 是[64, 128],偏置 shape 是[64]。卷积核是四维张量,shape 通常是[out_channels, in_channels, kernel_height, kernel_width]。
另外,反向传播过程中的梯度也是张量。PyTorch 中只有requires_grad=True的浮点张量才会记录梯度。训练时经常遇到“梯度为 None”的情况,很可能是因为该张量不参与计算图,或者它的requires_grad没有被正确设置。
x = torch.tensor([2.0], requires_grad=True) y = x ** 2 y.backward() print(x.grad) # tensor([4.0])从这个最小例子可以看到,张量的数值计算结果和梯度是分开存放的。理解这一点,有助于后续学习自定义训练循环时定位梯度更新不到参数的问题。
5. 常见报错与排查路径
在实际项目中,张量相关的报错往往集中在几个固定类型。下面按排查优先级整理成表格。
| 问题现象 | 常见原因 | 检查方式 | 处理建议 |
|---|---|---|---|
mat1 and mat2 shapes cannot be multiplied | 全连接层输入特征数不等于权重矩阵维度 | 打印x.shape和linear.weight.shape | 调整输入特征维度或修改模型结构 |
expected scalar type Float but found Long | 浮点张量和整型张量混算 | 打印参与运算的dtype | 对数据统一调用.float() |
Expected all tensors to be on the same device | CPU 张量和 GPU 张量混算 | 打印.device | 统一调用.to(device) |
view size is not compatible with input size | view参数乘积不等于总元素数 | 打印原始 shape 和参数乘积 | 使用正确的形状,或用reshape |
Trying to backward through the graph a second time | 重复 backward 而未保留计算图 | 检查训练循环位置 | 非必要不写retain_graph=True |
| 数值出现 NaN | 学习率过大、数据未归一化、精度溢出 | 检查梯度、loss、数据范围 | 降低学习率,检查浮点范围,使用稳定损失函数 |
5.1 维度不匹配:先看 shape 再改代码
维度不匹配是最高频的报错。遇到这类问题,第一条原则是不要凭记忆改代码,先打印每一步的 shape。报错信息中通常会告诉你两个参与运算的张量形状,但有时报错位置距离真正出错的位置隔了好几层前向传播。
排查顺序可以这样:
- 打印输入张量的 shape。
- 打印模型每一层输出 shape,常用方式是临时加打印语句,或使用
torchsummary打印模型结构。 - 找到第一个形状不匹配的层,确认是数据预处理问题还是模型结构问题。
- 如果数据预处理是
[C, H, W],但模型希望[H, W, C],需要用permute调整,而不是reshape。
注意:报错位置不一定是根因。前向传播里前面层的输出形状错误,往往到后面矩阵乘法时才暴露。最好建立逐层 shape 检查的习惯。
5.2 dtype 不一致:统一数据精度再运算
dtype 不一致的报错经常出现在数据预处理中。例如用 NumPy 读入整数像素值,转成 Tensor 后仍是 int64,而模型权重是 float32,两者相乘就会报 dtype 错误。
解决办法是在数据进入模型前统一 dtype。图像数据通常在归一化时转成 float32,标签则保持 int64 用于交叉熵损失。切忌在训练循环里每轮都转换 dtype,这会影响性能。更合理的做法是在Dataset的__getitem__中完成转换,或者加载数据后整体转换一次。
5.3 设备不一致:把张量放到同一个 device
设备不一致是 GPU 训练中常见的低级错误。模型放到了 GPU,但输入数据还在 CPU,或者数据在 GPU,某些临时变量在 CPU 创建。报错信息非常直接,但很多人容易忽略一个隐蔽场景:在验证阶段使用with torch.no_grad()时,没有把验证数据同步放到模型所在设备。
推荐做法是定义统一的 device 变量,模型、数据、标签都在同一步通过.to(device)转移。要多关注 loss 内部是否也有张量运算,某些自定义损失函数里手动创建的 mask 可能默认在 CPU 上。
5.4 view 与 reshape 的连续性陷阱
view的报错信息通常包含is not contiguous。这是因为view要求底层内存连续,而transpose、permute这类轴交换操作会打乱内存连续性。若你希望改变形状且不关心是否共享内存,直接用reshape更安全。
不过reshape并非万能。若你显式希望让修改结果影响原张量,例如某些自定义反向传播实现里需要原地操作,就要先调用.contiguous()确保内存连续,再使用view。理解连续性的关键在于:张量 shape 是逻辑结构,底层内存排列才是物理结构。两者不一致时,view无法完成,而reshape会借助一次拷贝完成转换。
6. 生产环境中的张量最佳实践
6.1 写代码之前先明确张量形状
在写模型前,先用注释或文档把输入张量的 shape 写清楚。例如一个图像分类任务的输入约定为[batch_size, 3, 224, 224],标签为[batch_size],dtype 为 int64。这个约定不只在代码里写,还应成为团队评审时的检查项。
许多模型 bug 的根源不是算法复杂,而是前后环节对张量 shape 的理解不一致。训练脚本认为数据是[B, H, W, C],模型实现认为输入是[B, C, H, W],双方都能跑通,但结果完全错误。
6.2 避免隐藏广播带来的数据错误
广播机制会让两个形状不完全相同的张量正常相加,但这并不代表结果符合你的业务逻辑。在关键运算后,建议显式检查结果的 shape,或者用注释标明期望的 shape。
例如对一个[batch_size, seq_len, hidden]的序列特征做聚合时,如果你误加了[hidden]的偏置,广播可能把偏置扩展到每个 token,导致每个位置都被同一个偏置影响。如果这不是你想要的,就必须先确认维度,或者用unsqueeze明确维度位置。
6.3 学习环境和生产环境的差异
学习环境里跑通一个张量运算,和生产环境稳定运行一套训练推理系统,差距很大。在学习阶段,你只需关注张量的创建、变形和运算结果;在开发和生产阶段,还需要额外考虑以下事项:
- 数据加载时统一 dtype 和 shape,避免上游数据变更导致维度错误。
- 显存占用监控:大 batch 会快速占满显存,需要根据显存调整 batch size 和输入分辨率。
- 精度策略:生产环境推理时,是否从 float32 转成 float16 或 int8,需要根据模型精度和硬件能力评估。
- 日志记录:在关键节点记录张量 shape、均值、标准差、梯度范数,便于训练异常时回溯。
- 版本兼容:不同版本的 PyTorch 或 TensorFlow 对某些算子行为可能有差异,落地前要固定依赖版本。
- 回滚方案:模型版本、数据版本、预处理代码要一起打版本,避免线上环境张量结构与离线训练不一致。
6.4 可复用的张量操作前检查清单
写代码或排查问题时,可以按下面这份清单逐项检查:
- [ ] 输入张量是否具备预期的 shape?
- [ ] 是否有 batch 维?batch 维是否位于正确位置?
- [ ] 所有参与运算的张量 dtype 是否一致?
- 若是神经网络,标签 dtype 是否为 int64,特征是否转为 float32?
- [ ] 所有张量是否在同一个 device 上?
- [ ] 轴交换是否使用了
permute而不是reshape? - [ ] 是否误用广播机制导致 shape 改变而不自知?
- [ ]
view是否作用在不连续张量上? - [ ] 与 NumPy 互转时,是否清楚内存是否共享?
- [ ] 自定义损失函数内的中间张量是否也满足 dtype 和 device 约束?
- [ ] 梯度张量是否为 None?
requires_grad是否正确开启?
这份清单适合在每次构建数据管道、写新模型、或排查训练异常时对照使用。
7. 从张量继续往前走:下一步学什么
张量只是机器学习的第一块基石。真正理解张量之后,下一步建议沿着三个方向继续深入。
第一个方向是自动微分和计算图。PyTorch 中的backward()如何根据张量之间的关系计算梯度,是所有神经网络原理的底层逻辑。如果你能把张量的前向运算和反向传播对应起来,理解 optimizer 的工作原理就会容易很多。
第二个方向是常见模型的数据流。建议定量分析一个 CNN 的输入张量如何经过卷积、池化、全连接,一步步从[B, 3, 224, 224]变成[B, 10]的分类输出。分析时可以逐层打印 shape,把张量结构变化画在纸上。
第三个方向是内存和性能优化。同样是张量运算,内存连续与不连续、float32 与 float16、CPU 与 GPU 之间的性能差异非常大。等你具备基础概念后,再学习混合精度训练、张量并行、批处理优化等知识点会更有抓手。
如果是在期末复习阶段,可以把张量的概念、创建方式、属性、广播规则、常见报错五部分作为重点。如果是在跟着课程做项目,就把注意力放在数据和模型接口的张量 shape 匹配上。总之,遇到任何报错,第一步先打印 shape 和 dtype,第二步看 device,第三步看运算是否符合数学规则。这三步能解决绝大多数张量相关的问题。
张量这个概念本身并不复杂,但它连接了数据预处理、模型结构、训练流程和部署优化各个环节。把基础打扎实,后续学任何模型都会顺畅很多。