之前在做深度学习训练时,我经常被tensor相关的问题缠住:有时候是形状对不上,有时候是数据类型不一致,有时候是广播之后结果完全不符合预期。网上关于张量的资料很多,但大多只讲一层概念,一旦遇到实际代码就容易卡壳。如果你也在学习 PyTorch,或者想系统梳理张量这个核心数据结构,那这篇文章应该能帮你少走一些弯路。
本文将围绕tensor的底层逻辑、创建方式、常用操作、广播机制和实战案例逐层展开,最后还会整理一份高频报错排查清单和工程实践建议。适合刚入门深度学习的读者,也适合需要快速回顾张量知识的开发者。
1. 张量是什么:先建立整体认知
1.1 从“tensor_of_ice”看张量的本质
tensor_of_ice这个标题看起来像是一个项目名,其实可以把它拆成两个关键词:tensor和ice。
ice(冰)有一种特征:它在微观结构上是高度规则的晶格排列,宏观上却可以呈现出各种形态。如果我们把“冰”的规则性映射到数据科学领域,tensor恰好也是这样一种结构——它是一块多维数据网格,每个位置都有确定的值、确定的索引、确定的数据类型。换句话说,张量就是“规则化”的多维数组。
在 PyTorch、TensorFlow、JAX 这些深度学习框架中,张量是数据流通的基本单位。一张图片、一句话、一段音频,最终都要被转成张量才能输入模型。掌握张量的创建、变换、归约和内存排布,是后续学习自动求导、模型训练、分布式部署的基础。
1.2 张量与标量、向量、矩阵的关系
很多初学者会把张量理解成一个很玄的概念,其实它并不复杂。从维度上看:
| 维度 | 名称 | 示例 |
|---|---|---|
| 0 维 | 标量(Scalar) | 3.14 |
| 1 维 | 向量(Vector) | [1, 2, 3] |
| 2 维 | 矩阵(Matrix) | [[1, 2], [3, 4]] |
| 3 维及以上的数组 | 张量(Tensor) | 形状为 (2, 3, 4) 的三维数组 |
所以,张量其实是标量、向量、矩阵的自然推广。我们可以说:张量是一个任意维度的数组,并且框架为其提供了 GPU 加速、自动求导等额外能力。
这里需要区分一个容易混淆的点:NumPy 中的ndarray与 PyTorch 中的torch.Tensor。二者底层都表示多维数组,但 PyTorch 的张量有两个关键差异:
- 张量可以通过
requires_grad=True接入自动求导计算图。 - 张量可以直接在 GPU 上运行,而 NumPy 数组默认只支持 CPU。
因此在深度学习项目中,我们通常使用torch.Tensor而不是numpy.ndarray来承载数据和梯度。
1.3 为什么要系统掌握张量操作
从工程角度看,张量操作是模型代码中最常见、也最容易出错的部分。
比如在图像分类任务中,输入数据从文件读取后是形状为(H, W, C)的数组,但模型往往需要(B, C, H, W)格式,这个过程涉及维度变换;在自然语言处理任务中,批次内句子长度不同,需要padding到统一长度,这涉及维度填充和掩码操作;在训练过程中,损失函数返回的是标量张量或形状为(B,)的向量,如何正确归约也要依赖对dim参数的理解。
可以说,如果张量操作不过关,写模型代码时百分之百会磕磕绊绊。反过来,一旦你习惯了从形状、数据布局、设备位置三个角度看张量,那么阅读开源项目、调试训练代码都会顺很多。
2. 环境准备与版本说明
本文示例基于 PyTorch 编写,所有代码都可以在 CPU 环境下运行,不需要独立显卡。如果你的电脑上还没有配置好环境,可以按下面的步骤操作。
2.1 安装 Python 与虚拟环境
建议使用 Python 3.8 或更高版本,推荐通过conda或venv创建独立虚拟环境,避免不同项目依赖互相污染。这里以 conda 为例:
conda create -n tensor-demo python=3.10 -y conda activate tensor-demo2.2 安装 PyTorch
PyTorch 的安装命令会根据操作系统和 CUDA 版本不同而变化。由于本文内容全部可以在 CPU 环境运行,最简单的方式是直接使用 pip 安装 CPU 版本:
pip install torch torchvision torchaudio如果你需要使用 GPU,请前往 PyTorch 官网根据 CUDA 版本选择对应的安装命令。不同版本的安装命令差异较大,以官方安装文档为准。
2.3 验证安装
安装完成后,在命令行输入以下 Python 代码,确认 PyTorch 可以正常导入,并检查是否有可用的 GPU:
import torch print("PyTorch 版本:", torch.__version__) print("是否有可用 GPU:", torch.cuda.is_available())如果输出结果中可以看到版本号,并且torch.cuda.is_available()返回False,说明当前是 CPU 环境,不影响本文后续示例。
3. 张量核心概念拆解
在开始写代码之前,先把张量的三个核心属性讲清楚:shape(形状)、dtype(数据类型)和device(设备位置)。很多报错其实都可以归因到这三个属性上。
3.1 形状(Shape):张量的骨架
shape描述的是张量每个维度上有多少个元素。比如torch.randn(2, 3)创建了一个形状为(2, 3)的浮点张量,它有 2 行 3 列。
理解shape时,有一个关键点:维度顺序是有含义的,不能随便交换。以图像数据为例,PyTorch 中常见的张量格式是(B, C, H, W),分别表示:
B:batch size,批次大小,即一次处理几张图片。C:channel,通道数,RGB 图像是 3,灰度图是 1。H:height,图片高度。W:width,图片宽度。
如果一张图片的形状是(3, 224, 224),意思是 3 个通道,每个通道是224 × 224的矩阵,而不是 224 张 3×224 的图片。位置不同,含义完全不同。
查看张量形状的代码如下:
import torch x = torch.randn(4, 3, 224, 224) print(x.shape) # torch.Size([4, 3, 224, 224]) print(x.size()) # torch.Size([4, 3, 224, 224]) print(x.dim()) # 4在模型代码中,我们经常使用x.shape[0]获取 batch size,用x.size(1)获取通道数。这是一种非常常见的操作,可以避免硬编码维度大小,提高代码的通用性。
3.2 数据类型(dtype):张量的默认精度
PyTorch 张量的数据类型决定了每个元素占用的内存大小以及数值精度。常用的数据类型包括:
| dtype | 说明 | 默认场景 |
|---|---|---|
torch.float32 | 单精度浮点,PyTorch 默认浮点类型 | 模型权重、大部分输入输出 |
torch.float64 | 双精度浮点,内存占用更大 | 科学计算、需要高精度时 |
torch.int64 | 长整型,常用于索引和标签 | 分类任务的target |
torch.bool | 布尔型 | 掩码(mask)操作 |
常见的问题是数据类型不匹配。例如模型参数是float32,输入数据却是float64,这时 PyTorch 会直接报错。所以在定义输入张量时,通常需要显式指定 dtype,或者使用.float()、.long()等方法进行转换。
import torch a = torch.tensor([1, 2, 3], dtype=torch.float32) b = torch.tensor([1, 2, 3], dtype=torch.float64) print(a.dtype) # torch.float32 print(b.dtype) # torch.float64 # 将 b 转成 float32 b = b.float() print(b.dtype) # torch.float323.3 设备(Device):张量身处何方
device表示张量被存储在 CPU 还是 GPU 上。CPU 上的张量与 GPU 上的张量不能直接进行运算,否则会抛出类似Expected all tensors to be on the same device的报错。
推荐的写法是先在代码中定义当前设备,再把所有模型和张量移动到同一个设备上:
device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu") x = torch.randn(3, 3).to(device) model = MyModel().to(device)需要注意的是,.to(device)不是原地操作。它返回一个新的张量,如果直接丢掉返回值,原张量并不会改变,所以使用x = x.to(device)这样的写法才生效。
3.4 从“冰块视角”看张量
回到前面tensor_of_ice的意象,我们会发现张量和冰块有很像的地方:
- 冰块有规则的晶格结构,张量有规则的形状(shape),每个维度上的长度都明确。
- 冰块导热快,张量支持 GPU 并行计算,运算效率高。
- 冰块温度低,触碰容易产生不适;张量对类型、形状、设备要求严格,混用时会立即报错,而这种“冷冰冰”的约束恰好能让程序暴露问题,而不是默默给出错误结果。
所以学习张量时,不要害怕报错。报错是框架在告诉我们:这里的数据结构不符合预期,需要调整。只要学会从shape、dtype、device三个角度分析,大多数报错都能快速定位。
4. 张量操作实战:从创建到线性回归
这一部分会写很多可运行代码。建议打开 Python 交互环境,边读边敲,这样记忆更深刻。
4.1 创建张量的常用方式
PyTorch 提供了非常丰富的张量创建方法,覆盖了日常开发的大部分需求。
import torch # 1. 从 Python 列表或 NumPy 数组创建 t1 = torch.tensor([1, 2, 3]) t2 = torch.tensor([[1, 2], [3, 4]]) # 2. 全零张量与全一张量 t3 = torch.zeros(2, 3) # 2x3 的全零张量 t4 = torch.ones(2, 3) # 2x3 的全一张量 # 3. 等差序列张量 t5 = torch.arange(0, 10, step=2) # tensor([0, 2, 4, 6, 8]) # 4. 等间隔生成张量 t6 = torch.linspace(0, 1, steps=5) # tensor([0.0000, 0.2500, 0.5000, 0.7500, 1.0000]) # 5. 随机张量 t7 = torch.randn(3, 3) # 标准正态分布 t8 = torch.rand(3, 3) # [0, 1) 均匀分布 t9 = torch.randint(0, 10, size=(3, 3)) # 整数均匀分布 # 6. 单位矩阵 t10 = torch.eye(3) # 7. 相同值填充 t11 = torch.full((2, 3), fill_value=7.0)创建张量时最容易犯的错误是混淆torch.tensor([1, 2, 3])与torch.Tensor([1, 2, 3])。前者是 Python 的list转换,后者在部分场景下等价于torch.FloatTensor,但在旧版本中行为略有不同。日常开发建议统一使用torch.tensor。
4.2 索引与切片
张量的索引与 NumPy 非常相似,支持多维度同时索引,也支持步长切片。
import torch x = torch.arange(16).reshape(4, 4) print(x) # tensor([[ 0, 1, 2, 3], # [ 4, 5, 6, 7], # [ 8, 9, 10, 11], # [12, 13, 14, 15]]) # 取第 0 行 print(x[0]) # tensor([0, 1, 2, 3]) # 取第 2 行第 3 列的元素 print(x[2, 3]) # tensor(11) # 取前两行、前两列 print(x[:2, :2]) # tensor([[0, 1], # [4, 5]]) # 隔行取 print(x[::2, ::2]) # tensor([[ 0, 2], # [ 8, 10]])索引操作在训练数据加载时非常常见,比如从一批图片中取前 8 张、从特征向量中取某些特定位置等。这里的关键是不要一次性写太长的索引链,而是把拆分步骤写清楚,降低出错概率。
4.3 张量的变形操作
变形是项目中最高频的操作之一,也是新手最容易踩坑的地方。
reshape是最常用的方法,它会把张量数据按行优先顺序重新排列成目标形状。需要注意的是,reshape在底层数据不连续时,可能会自动拷贝数据,而view则要求张量在内存中连续,否则会报错。在 99% 的业务代码中,直接使用reshape更安全。
import torch x = torch.arange(6) y = x.reshape(2, 3) print(y) # tensor([[0, 1, 2], # [3, 4, 5]]) # 展平成 1 维 z = y.reshape(-1) print(z) # tensor([0, 1, 2, 3, 4, 5])permute用于交换多个维度,比如把形状为(B, H, W, C)的图片数据转成(B, C, H, W):
import torch # 模拟一张 2 张图片,宽高均为 4,通道数为 3 x = torch.randn(2, 4, 4, 3) y = x.permute(0, 3, 1, 2) print(y.shape) # torch.Size([2, 3, 4, 4])squeeze和unsqueeze用来增加或删除维度。unsqueeze(0)会在最前面增加一个维度,常用于给单个样本补上 batch 维度。
import torch x = torch.randn(3, 224, 224) y = x.unsqueeze(0) # 形状变为 (1, 3, 224, 224) z = y.squeeze(0) # 形状变回 (3, 224, 224) print(y.shape) print(z.shape)4.4 广播机制:让形状不同也能运算
广播(Broadcasting)是张量运算中的一个重要机制。它允许形状不完全相同的张量进行逐元素运算,前提是它们的形状满足一定规则。
规则可以简单总结为:从尾部维度开始比较,两个维度要么相等,要么其中一个为 1,要么其中一个不存在。
import torch # 形状 (3, 1) 与 (1, 4) a = torch.tensor([[1], [2], [3]]) # shape (3, 1) b = torch.tensor([[10, 20, 30, 40]]) # shape (1, 4) c = a + b print(c.shape) # torch.Size([3, 4]) print(c) # tensor([[11, 21, 31, 41], # [12, 22, 32, 42], # [13, 23, 33, 43]])a 的形状是(3, 1),b 的形状是(1, 4),在广播时:
- 第 1 维:
1与4,其中一个为 1,可以广播。 - 第 0 维:
3与1,其中一个为 1,可以广播。
所以最终结果形状是(3, 4)。
广播虽然方便,但也会掩盖设计问题。如果两个张量形状完全无法广播,会直接报错。因此在实际模型中,我习惯在关键运算前用注释标出形状,例如:
# x: (B, C, H, W) # weight: (C, 1, 1) out = x * weight这样别人在看代码时,可以快速理解每一步在做什么。
4.5 归约与聚合操作
sum、mean、max、argmax等归约操作在计算损失和评估指标时经常会用到。关键是理解dim参数。
import torch x = torch.tensor([[1, 2, 3], [4, 5, 6]], dtype=torch.float32) # 对整个张量求和 print(x.sum()) # tensor(21.) # 沿着 dim=0 求和,即按列求和 print(x.sum(dim=0)) # tensor([5., 7., 9.]) # 沿着 dim=1 求和,即按行求和 print(x.sum(dim=1)) # tensor([ 6., 15.]) # 求最大值所在索引 print(x.argmax(dim=1)) # tensor([2, 2])关于dim的理解,可以记住一个技巧:dim=k表示消除第 k 个维度。比如x.sum(dim=0)会消掉第 0 维,剩余形状为(3,)。
4.6 实战:使用张量实现一个线性回归
掌握了上述操作后,我们用一个简单的线性回归模型串起整个流程。虽然 PyTorch 有高层 API,但手动实现反向传播前的梯度计算反而更能体现张量操作的价值。
先准备数据:
import torch # 固定随机种子,保证结果可复现 torch.manual_seed(42) # 生成 100 个样本,特征数为 3 X = torch.randn(100, 3) true_w = torch.tensor([2.0, -3.5, 0.8]) true_b = 1.0 # y = X * true_w + true_b + 噪声 y = X @ true_w + true_b + 0.1 * torch.randn(100)这里使用了@运算符,它等价于torch.matmul,用于矩阵乘法。X的形状是(100, 3),true_w的形状是(3,),结果为形状(100,)的向量,加上偏置和噪声后,得到目标值。
接下来初始化模型参数,并设置requires_grad=True:
w = torch.randn(3, requires_grad=True) b = torch.zeros(1, requires_grad=True)定义前向传播和损失函数:
def predict(X): return X @ w + b def mse_loss(y_pred, y_true): return ((y_pred - y_true) ** 2).mean()训练 200 轮,每轮执行前向传播、计算损失、反向传播,然后手动更新参数:
learning_rate = 0.01 num_epochs = 200 for epoch in range(num_epochs): # 前向传播 y_pred = predict(X) loss = mse_loss(y_pred, y) # 反向传播 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}, Loss: {loss.item():.6f}")执行结果类似:
Epoch 20, Loss: 0.890412 Epoch 40, Loss: 0.387601 Epoch 60, Loss: 0.170233 Epoch 80, Loss: 0.076341 Epoch 100, Loss: 0.035277 Epoch 120, Loss: 0.017013 Epoch 140, Loss: 0.008734 Epoch 160, Loss: 0.004899 Epoch 180, Loss: 0.003091 Epoch 200, Loss: 0.002323训练结束后,可以查看学习到的参数:
print(w) # 接近 tensor([ 2.0052, -3.4891, 0.8096], requires_grad=True) print(b) # 接近 tensor([0.9981], requires_grad=True)从结果可以看到,模型学习到的参数与真实参数非常接近。这里的关键步骤是loss.backward()之后的参数更新必须放在torch.no_grad()环境下,否则计算图会被持续记录,导致内存不断增长并且梯度更新逻辑混乱。
5. 常见问题与排查思路
张量相关的报错非常多,但绝大多数都有规律可循。下面整理了一份高频问题对照表,并给出排查思路。
| 问题现象 | 常见原因 | 解决思路 |
|---|---|---|
The size of tensor a must match the size of tensor b | 两个张量形状不匹配,无法进行逐元素运算 | 打印a.shape和b.shape,检查是否需要reshape或permute |
Expected all tensors to be on the same device | 部分张量在 CPU,部分在 GPU | 统一通过.to(device)移动 |
Expected object of scalar type Float but got scalar type Double | dtype 不一致,例如float64与float32混用 | 统一转换为torch.float32 |
view size is not compatible with input tensor | view目标形状与原张量元素总数不匹配 | 检查元素总数,或者改用reshape |
RuntimeError: a leaf Variable that requires grad is being used in an in-place operation | 对需要梯度的叶子张量进行了原地操作 | 使用w = w - lr * w.grad,而不是w -= ... |
| CUDA out of memory | 显存不足 | 减小 batch size,清理不需要的变量,使用梯度累积 |
5.1 形状不匹配的问题
这是新手阶段出现频率最高的报错。有一种很典型的场景发生在全连接层或者卷积层输入输出对接时:
import torch x = torch.randn(8, 3, 224, 224) # 想解析成 (8, 3 * 224 * 224) flatten = x.reshape(x.size(0), -1) print(flatten.shape) # torch.Size([8, 150528])如果reshape的目标形状写错,比如(150528, 8),元素总数虽然一致,但语义完全错误。因此我在写代码时,会习惯用注释把形状变化标注出来:
# (B, C, H, W) -> (B, C*H*W) flatten = x.reshape(x.size(0), -1)5.2 数据类型转换问题
PyTorch 的自动类型提升能力有限,尤其是在涉及到loss.backward()的时候,float64与float32混用会直接报错。一个稳妥的做法是在数据加载阶段统一用.float()转换为torch.float32,标签则使用.long()。
import torch # 模拟原始数据是 float64 类型 data = torch.tensor([1.2, 2.3], dtype=torch.float64) data = data.float() print(data.dtype) # torch.float325.3 设备不一致的坑
在本地 CPU 调试好代码,部署到 GPU 服务器后,经常会遇到设备不一致的报错。最常见的原因是创建了一些未经.to(device)处理的常量或掩码张量。排查思路比较简单:在训练流程的入口处打印所有关键张量的设备,确认它们一致。
device = torch.device("cuda" if torch.cuda.is_available() else "cpu") x = torch.randn(4, 4, device=device) y = torch.randn(4, 4).to(device) print(x.device) # cuda:0 或 cpu print(y.device) # cuda:0 或 cpu5.4 原地操作导致梯度计算错误
初学者在手工更新参数时,容易写出这样的代码:
w -= learning_rate * w.grad这在w设置了requires_grad=True时会报错,因为叶子节点不允许原地操作。正确的做法是:
w = w - learning_rate * w.grad虽然两者语义上很像,但后者会生成新的张量,符合 PyTorch 对自动求导的限制。
6. 最佳实践与工程建议
6.1 固定随机种子
在深度学习中,随机性无处不在。为了确保实验结果可复现,建议在代码入口处固定所有相关随机种子:
import random import numpy as np import torch def set_seed(seed=42): random.seed(seed) np.random.seed(seed) torch.manual_seed(seed) torch.cuda.manual_seed_all(seed)这样可以避免在调试时因为随机数不同而出现“明明没改代码,结果变了好几个数”的困惑。
6.2 使用torch.no_grad()隔离推理逻辑
在验证集上计算指标、或者模型推理时,不需要更新梯度,此时可以用torch.no_grad()包裹推理代码。这样做能减少内存消耗,同时也能防止不小心在验证阶段修改模型参数:
with torch.no_grad(): y_pred = model(x_val) val_loss = loss_fn(y_pred, y_val)6.3 给张量形状写注释
模型代码中的张量操作非常多,代码可读性往往决定了下一次维护时的效率。推荐在关键操作前写上形状注释:
# x: (B, 3, H, W) x = x.permute(0, 2, 3, 1) # x: (B, H, W, 3) x = x.reshape(B, H * W, 3)这样的注释成本很低,但价值很高。尤其是在团队协作中,别人读到代码时,不需要手动推理每一步的形状变化。
6.4 避免频繁将张量转为 NumPy
tensor.cpu().numpy()在 CPU 上运行还好,但如果频繁在 GPU 张量和 NumPy 数组之间切换,会造成额外的数据传输开销,拖慢整体速度。更推荐的做法是尽可能让数据保持在张量形式,只在最终可视化或保存模型检查点时转换为 NumPy。
6.5 注意内存和显存的释放
在长时间训练或多次推理时,多余的变量会占用显存。下面几种做法值得形成习惯:
- 使用完的中间张量可以手动赋值
None,触发垃圾回收。 - 在长循环中尽量复用预先分配好的张量。
- 检查是否无意中创建了很大的中间张量,比如
(B, N, N)的矩阵,必要时使用分块计算。
6.6 推荐的单文件代码组织方式
对于学习阶段的小规模实验,建议把代码拆分得清晰一些,例如:
demo/ ├── data.py # 数据生成与预处理 ├── model.py # 模型定义 ├── train.py # 训练流程 └── config.py # 超参数与随机种子设置这种简单分层已经能应对大多数学习项目和中小型实验。等代码量继续增长时,再考虑引入配置管理、日志系统、模型注册等更完整的工程框架。
7. 总结
从最核心的tensor概念出发,我们完整梳理了张量的形状、数据类型、设备属性和内存语义,并通过大量代码示例演示了创建、索引、变形、广播、归约等常用操作。最后用一个纯张量实现的手动线性回归,把前向传播、损失计算、反向传播和参数更新串联在一起。
下一阶段可以继续深入学习几个关键方向:
- 自动求导:深入理解计算图、
backward()的内部机制。 - 数据加载:掌握
Dataset与DataLoader的使用,包括collate_fn的自定义。 - 模型封装:掌握
nn.Module、nn.Linear、nn.Conv2d等高层组件。 - 训练进阶:学习优化器、学习率调度、分布式训练的基础概念。
在实际项目中,请优先关注形状混乱、类型不一致和显存超限这三个风险点。遇到报错时,不要急着盲目搜索,先打印张量的shape、dtype、device,分析前后操作是否满足语义约束。张量虽然“冷冰冰”,但只要你理解了它的规则,它就会成为最可靠的数据伙伴。如果本文对你有帮助,可以收藏备用,方便后续查阅。