很多刚接触机器学习的同学,在看教程或者读框架源码时,经常会遇到“张量”这个词,比如“把输入数据转换成张量”“模型的权重是一个四维张量”。刚开始听到张量,总觉得它是什么高深的数学概念,好像需要很强的线性代数功底才能理解。实际上,在学习机器学习的过程中,张量并没有想象中那么可怕,它本质上就是一种“装数据”的容器,只是比我们熟悉的数组、矩阵更通用、更灵活。
这篇文章会从最基础的数据形态讲起,逐步拆解张量的含义、形状、维度、运算规则,并且结合 NumPy 和 PyTorch 给出大量可运行的代码示例。无论你是刚入门机器学习的新手,还是想系统梳理张量概念的开发者,这篇文章都能帮你建立一套清晰的理解框架。学完之后,你至少能看懂 PyTorch 模型里的 shape 变化,能自己创建、变形、运算张量,也知道了常见的广播机制和形状报错该怎么排查。
1. 什么是张量:从数据形态说起
1.1 从标量、向量、矩阵到张量
在理解张量之前,我们先回顾一下数学里常见的数据组织形式。
- 标量(Scalar):一个单独的数,比如 5、3.14。
- 向量(Vector):一组有序排列的数,比如 [1, 2, 3],它是一维的。
- 矩阵(Matrix):一个二维的数表,比如一个 2 行 3 列的二维数组。
- 张量(Tensor):维度大于等于 0 的数组统称,也就是标量、向量、矩阵都是张量的特例。
从这个角度来看,张量是一个更大的概念。你可以把标量理解为“0 维张量”,向量是“1 维张量”,矩阵是“2 维张量”。当数据的维度超过 2 时,比如一个形状为 (2, 3, 4) 的数据,它就很难再被叫作矩阵了,这时候用“张量”来描述是最合适的。
换句话说,张量就是“广义化的数组”,它可以拥有任意多个维度,每一个维度都代表数据的一种排列方式。
1.2 张量的正式定义与记号
在数学中,张量可以理解为向量和矩阵向更高维度的推广。不过在机器学习的工程语境里,我们通常不讨论太多微分几何里的张量定义,而是把它当作“多维数组”来使用。
一个张量有几个关键属性:
- 阶数(Rank):也叫维度数量。标量的阶数是 0,向量的阶数是 1,矩阵的阶数是 2。
- 形状(Shape):每个维度的大小。比如形状为 (2, 3) 的张量,表示有 2 行 3 列。
- 数据类型(Dtype):元素的数据类型,比如整数、浮点数、布尔值。
在 PyTorch 中,我们可以通过tensor.ndim查看张量的维度数量,通过tensor.shape查看形状。NumPy 中也类似,用array.ndim和array.shape。
import numpy as np a = np.array([1, 2, 3]) print("维度数量:", a.ndim) print("形状:", a.shape) print("数据类型:", a.dtype)输出:
维度数量: 1 形状: (3,) 数据类型: int64可以看到,这个一维数组的维度数量是 1,形状是 (3,),表示有 3 个元素。
1.3 为什么机器学习离不开张量
机器学习模型的本质,就是“输入数据,经过一系列数学运算,输出预测结果”。而计算机处理数学运算时,最有效率的数据组织形式就是数组和矩阵。神经网络里大量的矩阵乘法、逐元素运算、梯度计算,都需要统一的数据结构来承载。
张量之所以成为机器学习的核心数据结构,原因可以概括为三点。
第一,统一性。图像、文本、表格、音频,最终都可以转换成张量。一张彩色图片可以表示成 (高度, 宽度, 通道数) 的三维张量,一段文本可以表示成 (句子长度, 词向量维度) 的二维张量。
第二,并行计算。GPU 擅长对大规模张量做并行运算,深度学习框架把计算组织成张量运算之后,可以高效利用 GPU 加速。
第三,自动求导。现代深度学习框架基于张量构建计算图,张量在参与运算时会记录操作历史,框架可以自动计算梯度,这是反向传播算法的基础。
因此,想学好机器学习,尤其是深度学习,理解张量是绕不开的一步。
2. 环境准备与工具选择
2.1 Python 环境
本文的示例代码主要使用 Python 和常见的科学计算库。建议使用 Python 3.8 或更高版本,具体版本可以根据你的项目需求调整。如果你还没有配置环境,推荐使用 Anaconda 或 Miniconda 创建独立的虚拟环境,避免不同项目之间依赖冲突。
conda create -n tensor-tutorial python=3.10 conda activate tensor-tutorial2.2 NumPy 与 PyTorch 的作用
本文会用到两个库:
- NumPy:Python 科学计算的基础库,提供了高性能的多维数组对象
ndarray,也是很多数据分析、机器学习库的底层依赖。 - PyTorch:主流的深度学习框架,其核心数据结构就是
torch.Tensor,和 NumPy 的数组非常相似,但额外支持自动求导和设备迁移(CPU/GPU)。
安装命令如下:
pip install numpy pip install torch如果你的电脑有 NVIDIA 显卡并安装了 CUDA,建议按照 PyTorch 官网的提示安装对应 CUDA 版本的 PyTorch;如果只是为了学习张量概念,CPU 版本的 PyTorch 完全够用。
2.3 示例项目结构
为了方便实验,建议新建一个目录,里面放一到两个 Python 脚本,或者直接用 Jupyter Notebook 逐段运行。
tensor-tutorial/ ├── tensor_basics.py └── tensor_operations.py本文的代码如果标注了tensor_basics.py,就表示这部分代码可以集中放在该文件里运行。接下来的内容,我会先介绍张量的核心属性,再逐步展开创建、变形、运算和应用。
3. 张量的核心属性:形状、维度与轴
3.1 轴的编号规则
在张量中,“轴”(Axis)和“维度”(Dimension)经常混用,它们指的都是数据排列的“方向”。轴是从 0 开始编号的。
看一个简单的三维张量:
import numpy as np x = np.array([ [[1, 2], [3, 4]], [[5, 6], [7, 8]] ]) print(x.shape) # 输出 (2, 2, 2)这个张量的形状是 (2, 2, 2),含义是:
- 第 0 轴长度为 2,表示外层有 2 个“块”。
- 第 1 轴长度为 2,表示每个块里有 2 行。
- 第 2 轴长度为 2,表示每行有 2 个元素。
用坐标来理解:x[0, 1, 0]表示第 0 个块、第 1 行、第 0 列的元素,也就是数字 3。轴编号的顺序,决定了你在索引时怎么定位数据。
3.2 理解形状 shape
形状是张量最直观的描述,它用一个元组表示每个轴的长度。比如:
(3,)表示一维张量,共 3 个元素。(2, 3)表示二维张量,2 行 3 列。(4, 2, 3)表示三维张量,可以理解为 4 个 2 行 3 列的矩阵堆叠在一起。
在 PyTorch 中,形状同样非常重要。像nn.Linear、nn.Conv2d这类网络层,输入输出形状必须严格对应,形状不匹配会直接报错。
3.3 张量的数据类型
张量里的元素有不同类型。常见的包括:
- 整数型:
int32、int64 - 浮点型:
float32、float64 - 布尔型:
bool
深度学习模型默认多使用float32,因为它在精度和计算速度之间取得了平衡。某些场景下,比如数据处理阶段用整数索引,模型计算阶段则转成浮点数。
import torch a = torch.tensor([1, 2, 3]) b = torch.tensor([1.0, 2.0, 3.0]) print(a.dtype) # torch.int64 print(b.dtype) # torch.float32这里需要注意,PyTorch 创建整数列表后默认是int64,创建带小数的列表后默认是float32,这和 NumPy 的默认规则略有差异。
4. 张量的创建与基本操作
4.1 使用 NumPy 创建张量
NumPy 提供了非常丰富的数组创建函数。
import numpy as np # 从 Python 列表创建 a = np.array([[1, 2, 3], [4, 5, 6]]) # 全零数组 zeros = np.zeros((2, 3)) # 全一数组 ones = np.ones((2, 3)) # 指定数值填充 full = np.full((2, 3), 7) # 随机数组 random_arr = np.random.rand(2, 3) # 等差数列 arange_arr = np.arange(12).reshape(3, 4) print("a:", a.shape) print("zeros:", zeros.shape) print("full:\n", full)这些函数在数据预处理中非常常用,比如初始化权重、创建掩码、生成测试数据等。
4.2 使用 PyTorch 创建张量
PyTorch 的创建方式和 NumPy 很像,常见的有:
import torch # 从列表创建 a = torch.tensor([[1, 2], [3, 4]]) # 全零 zeros = torch.zeros(2, 3) # 全一 ones = torch.ones(2, 3) # 随机均匀分布 rand = torch.rand(2, 3) # 随机正态分布 randn = torch.randn(2, 3) # 和 NumPy 互相转换 np_arr = a.numpy() back_to_torch = torch.from_numpy(np_arr) print("zeros:", zeros.shape) print("rand:", rand)在 PyTorch 中,还有一个非常常用的函数torch.arange,它和 Python 的range类似,但返回的是张量。
x = torch.arange(24).reshape(2, 3, 4) print(x.shape) # torch.Size([2, 3, 4])4.3 索引与切片
张量的索引和 Python 列表的切片语法非常相似,但因为轴变多了,需要理解每个位置对应哪个轴。
import numpy as np x = np.arange(12).reshape(3, 4) print(x) # 取第 1 行 print(x[1]) # 取第 1 行第 2 列 print(x[1, 2]) # 取所有行的第 2 列 print(x[:, 2]) # 取前 2 行的后 2 列 print(x[:2, 2:])输出:
[[ 0 1 2 3] [ 4 5 6 7] [ 8 9 10 11]] [ 4 5 6 7] 6 [ 2 6 10] [[2 3] [6 7]]多维切片的关键就是记住:每一维都独立写一个切片范围,用逗号隔开。:表示这一维全部保留,:两边的数字表示起止位置,和 Python 列表规则一致。
5. 张量的变形与维度变换
5.1 reshape 与 view
reshape是改变张量形状最常用的方法,它把张量按某种顺序重新排列成新的形状,前提是元素总数必须一致。
import numpy as np import torch # NumPy x = np.arange(12) y = x.reshape(3, 4) print(y) # PyTorch xt = torch.arange(12) yt = xt.view(3, 4) print(yt)PyTorch 中view和reshape都用于变形,但实现机制略有不同。view要求原张量在内存中是连续的,而reshape在必要时会自动复制数据。如果你不确定,优先使用reshape更稳妥。
还有一种常见情况是通过-1让框架自动推断某一维的大小。
x = np.arange(12) y = x.reshape(-1, 4) # 自动算出行数是 3 print(y.shape) # (3, 4)这种方式在写数据加载、批处理时非常实用,不需要手动计算形状。
5.2 增删维度:squeeze 与 unsqueeze
squeeze用于删除长度为 1 的维度,unsqueeze用于在指定位置增加一个长度为 1 的维度。
import torch x = torch.randn(1, 3, 1, 4) # 删除所有长度为 1 的维度 y = x.squeeze() print(y.shape) # torch.Size([3, 4]) # 在指定位置加维度 z = x.unsqueeze(0) print(z.shape) # torch.Size([1, 1, 3, 1, 4])为什么需要增加长度为 1 的维度?因为某些模型层要求输入是四维张量,比如卷积层期望形状为 (批次大小, 通道数, 高度, 宽度)。当你只有一张灰度图时,数据形状可能是 (高度, 宽度),就需要先unsqueeze(0)增加通道维度,再unsqueeze(0)增加批次维度,才能送入模型。
5.3 转置 transpose 与 permute
transpose交换两个指定的维度,permute可以按照任意顺序重排所有维度。
import torch x = torch.randn(2, 3, 4) # 交换第 0 和第 2 维 y = x.transpose(0, 2) print(y.shape) # torch.Size([4, 3, 2]) # 重排所有维度 z = x.permute(2, 0, 1) print(z.shape) # torch.Size([4, 2, 3])这在实际项目中很常见。比如一张图片在读取时形状可能是 (高度, 宽度, 通道数),而 PyTorch 的卷积层期望的是 (通道数, 高度, 宽度),这时候就需要用transpose或permute调整维度顺序。
# 假设读入的图片张量形状为 (H, W, C) image = torch.randn(224, 224, 3) # 转换为 (C, H, W) image_t = image.permute(2, 0, 1) print(image_t.shape) # torch.Size([3, 224, 224])5.4 拼接与分割
拼接可以用torch.cat在某个轴上把多个张量连接起来,也可以用torch.stack增加一个新维度后堆叠。
import torch a = torch.randn(2, 3) b = torch.randn(2, 3) # 沿第 0 轴拼接 cat0 = torch.cat([a, b], dim=0) print(cat0.shape) # torch.Size([4, 3]) # 沿第 1 轴拼接 cat1 = torch.cat([a, b], dim=1) print(cat1.shape) # torch.Size([2, 6]) # stack 会增加一个新维度 stacked = torch.stack([a, b], dim=0) print(stacked.shape) # torch.Size([2, 2, 3])这两个操作在构建批数据、合并特征时经常出现。需要强调的是,cat要求除拼接轴外其他维度完全一致,否则会报错。
6. 张量运算与广播机制
6.1 逐元素运算
张量可以直接参与加减乘除,运算符会对每个位置的元素独立计算。
import torch a = torch.tensor([1, 2, 3]) b = torch.tensor([4, 5, 6]) print(a + b) # tensor([5, 7, 9]) print(a * b) # tensor([4, 10, 18]) print(a ** 2) # tensor([1, 4, 9]) print(torch.exp(a)) # 指数运算这里要注意,*是逐元素相乘,不是矩阵乘法。矩阵乘法用@或者torch.matmul。
6.2 矩阵乘法
矩阵乘法要求第一个矩阵的列数等于第二个矩阵的行数。
import torch A = torch.randn(2, 3) B = torch.randn(3, 4) C = A @ B # 等价于 torch.matmul(A, B) print(C.shape) # torch.Size([2, 4])神经网络中的全连接层,本质上就是输入张量和权重矩阵做矩阵乘法,再加上偏置。理解矩阵乘法的维度变化,是看懂模型结构的基础。
6.3 广播机制详解
广播(Broadcasting)是 NumPy 和 PyTorch 中非常重要且容易困惑的机制。简单来说,当两个张量形状不完全一致时,框架会尝试自动扩张较小的张量,使它们能够进行运算。
import torch a = torch.tensor([1, 2, 3]) b = torch.tensor(10) print(a + b) # tensor([11, 12, 13]),10 被广播到每个元素再看一个二维的例子:
m = torch.arange(6).reshape(2, 3) row = torch.tensor([10, 20, 30]) print(m + row)输出:
tensor([[10, 21, 32], [13, 24, 35]])这里row形状是 (3,),m形状是 (2, 3),框架把row沿第 0 轴广播,相当于复制了一份,和每一行相加。
广播规则可以概括为:从最后一个维度开始比较,如果两个维度相等,或者其中一个是 1,就可以继续匹配;如果既不等长,也不是 1,就无法广播,会报错。
# 例子:形状 (3, 1) 与 (1, 4) 可以广播成 (3, 4) x = torch.randn(3, 1) y = torch.randn(1, 4) z = x + y print(z.shape) # torch.Size([3, 4])广播机制能减少很多显式的复制操作,让代码更简洁、内存占用更低,但如果不理解它,很容易写出形状不匹配的代码,或者得到意想不到的结果。
7. 机器学习中的张量实战场景
7.1 批处理数据:一次喂入多个样本
在训练模型时,我们通常不会一次只输入一个样本,而是把一个批次(Batch)的样本同时送入模型。假设每个样本是一个 64 维的特征向量,批次大小为 32,那么输入张量的形状就是 (32, 64)。
import torch batch_size = 32 feature_dim = 64 inputs = torch.randn(batch_size, feature_dim) print(inputs.shape) # torch.Size([32, 64])模型权重通常也是一个矩阵,形状为 (输出特征数, 输入特征数)。矩阵乘法得到的结果形状为 (32, 输出特征数),每一个样本对应一行输出。
output_dim = 10 weight = torch.randn(output_dim, feature_dim) outputs = inputs @ weight.T print(outputs.shape) # torch.Size([32, 10])这里的weight.T是权重矩阵的转置,这样矩阵乘法才能正确匹配维度。
7.2 图像数据:通道、高度、宽度
图像数据是理解高维张量最经典的例子。一张 RGB 彩色图片,如果高度为 224、宽度为 224,它有 3 个颜色通道,那么单个图片可以表示为形状 (3, 224, 224) 的张量。一个批次 16 张图片,形状就是 (16, 3, 224, 224)。
import torch batch_size = 16 channels = 3 height = 224 width = 224 images = torch.randn(batch_size, channels, height, width) print(images.shape) # torch.Size([16, 3, 224, 224])卷积神经网络处理的就是这种四维张量。卷积核在高度和宽度方向上滑动,同时跨通道聚合信息,最终输出的特征图仍然是张量。
7.3 序列文本:批次、步长、特征
自然语言处理中,一段文本经过分词和词向量映射后,会转换成一个形状为 (序列长度, 词向量维度) 的二维张量。一个批次的多条文本,形状就是 (批次大小, 序列长度, 词向量维度)。
batch_size = 8 seq_len = 50 embed_dim = 128 texts = torch.randn(batch_size, seq_len, embed_dim) print(texts.shape) # torch.Size([8, 50, 128])这里的第 0 轴是批次方向,第 1 轴是序列的时间步方向,第 2 轴是每个词的特征方向。RNN、LSTM、Transformer 等模型都会操作这种三维张量。
7.4 神经网络参数:权重与偏置张量
神经网络的每一层参数同样以张量形式存储。比如二维卷积层的卷积核,形状通常为 (输出通道数, 输入通道数, 卷积核高, 卷积核宽);全连接层的权重是二维矩阵,偏置是一维向量。
import torch.nn as nn linear = nn.Linear(in_features=64, out_features=10) print(linear.weight.shape) # torch.Size([10, 64]) print(linear.bias.shape) # torch.Size([10])训练过程中,PyTorch 会为这些参数张量自动计算梯度,并更新它们的值。可以说,模型训练本质上就是不断调整这些张量参数的过程。
8. 常见问题与排查思路
在实际操作张量时,会遇到各种各样的报错。为了帮你快速定位问题,这里整理了一张排查表。
| 问题现象 | 常见原因 | 解决思路 |
|---|---|---|
shape mismatch报错 | 两个张量在某个维度上大小不一致,无法直接运算或拼接 | 打印两个张量的shape,确认需要对齐的维度,必要时使用reshape、transpose或unsqueeze调整 |
| 广播时意外变成大矩阵 | 不理解广播规则,维度被自动扩展,结果与预期不符 | 打印中间结果的形状,逐步确认每一步的形状变化 |
view报错is not contiguous | 张量在transpose或permute后内存不连续,无法直接view | 改用reshape,或先调用contiguous()再view |
| 张量在 GPU 上,运算时报设备不一致 | 部分张量在 CPU,部分在 GPU,无法直接混合运算 | 用.to(device)统一设备,确保参与运算的张量在同一个设备上 |
| NumPy 张量和 PyTorch 张量混用报错 | 两个库的数据结构不能直接做运算 | 用torch.from_numpy()转成 PyTorch 张量,或用.numpy()转回 NumPy 数组 |
| 矩阵乘法维度不对 | 左侧张量的最后一维不等于右侧张量的倒数第二维 | 检查矩阵乘法的维度匹配条件,必要时对权重做转置或重排 |
| 索引结果维度减少 | 用单个整数索引某一维时,该维度会被去除 | 如果需要保留维度,使用切片写法,比如x[0:1]代替x[0] |
排查形状类问题时,最有效的办法就是在报错位置前后打印print(x.shape)。把每个关键步骤的形状都看清楚,问题往往很快就能定位。
9. 最佳实践与工程建议
9.1 养成注释形状的习惯
在写模型或数据处理代码时,建议在关键张量操作附近用注释标明形状变化。这样做不仅能帮助自己理清逻辑,也能让团队其他人快速理解代码。
# 输入: (batch_size, 3, 224, 224) x = x.permute(0, 2, 3, 1) # -> (batch_size, 224, 224, 3) x = x.reshape(batch_size, -1) # -> (batch_size, 3*224*224)这种方式在排错时能节省大量时间。
9.2 注意数据类型一致性
模型输入通常要求float32,而标签可能是整数类型。送入损失函数前,要确认数据类型的匹配。用到 NumPy 和 PyTorch 混编时,要显式转换,避免运行时才暴露问题。
9.3 内存与性能优化
处理大张量时,需要注意内存占用。批量创建的中间变量会占用大量显存,因此:
- 不需要的中间结果要及时释放,或者用
del删除后调用torch.cuda.empty_cache()。 - 能用
inplace操作时谨慎使用,因为它虽然省内存,但会影响自动求导。 - 尽量使用批量运算而不是循环,循环处理张量在 Python 中性能很低。
9.4 使用断言约束形状
在数据加载或模型前向传播中,可以用assert主动检查形状,提前暴露错误,而不是等到运算失败才排查。
def preprocess_batch(images): assert images.ndim == 4, f"期望 4 维张量,实际是 {images.ndim} 维" assert images.shape[1] == 3, "通道数必须为 3" return images.float()9.5 安全操作与生产环境注意事项
在真实项目中,涉及数据变换时,尽量在测试环境验证形状变化是否符合预期,尤其是使用reshape时,它不保证保持语义顺序。需要保持语义的业务数据,建议优先使用transpose、permute等明确的维度变换操作。对大文件或大数据集做预处理时,先在小规模数据上验证流程,再扩展到全量数据,避免算力浪费。
9.6 善用官方文档与调试工具
NumPy 和 PyTorch 的官方文档都非常完善。遇到不熟悉的函数,可以先查看函数签名和参数说明。调试时,可以用print、ipdb或者 PyTorch 的torch.set_printoptions控制输出格式,让大张量更容易阅读。
10. 总结与下一步学习建议
本文从标量、向量、矩阵开始,逐步引出了张量的定义,讲解了形状、维度、轴、数据类型等核心属性,并通过大量代码示例演示了张量的创建、索引切片、变形、维度重排、拼接、矩阵乘法和广播机制。最后结合图像、文本、批处理、网络参数等真实场景,说明了张量在机器学习中的应用方式,也整理了一份常见报错排查表。
如果你能独立完成文中大部分示例,并且能说出(32, 3, 224, 224)每个数字代表什么含义,说明你已经掌握了张量的基本使用方法。
下一步可以继续学习:
- PyTorch 的
autograd机制,理解张量如何实现自动求导。 Dataset和DataLoader的使用,掌握真实数据如何转换成批张量。- 卷积层、循环神经网络等常见网络结构中的张量维度变化。
- 使用 TensorBoard 或张量可视化工具观察模型中间层的输出。
张量是机器学习中绕不开的基础概念,也是连接数据与模型之间的桥梁。建议不要只看不练,打开 Python 环境,把文中的代码逐段运行一遍,多打印几次shape,很快就能建立起对张量的直觉。如果本文对你有帮助,可以收藏备用,也欢迎分享给正在学习机器学习的朋友。