现在很多刚接触机器学习的同学,在翻开教材或者教程时,第一个被卡住的概念往往不是“梯度下降”,也不是“神经网络”,而是一个听起来有点物理味的词——张量(Tensor)。尤其是当你看到“TensorFlow”这个框架名字时,心里难免会想:张量到底是什么?为什么机器学习到处都在用它?它和我们熟悉的数组、矩阵、向量又有什么关系?
本文将会围绕“机器学习中的张量”做一个深入但易懂的解析。我会先帮你把张量的概念拆开,从标量、向量、矩阵一路推到张量;再讲讲为什么深度学习框架都偏爱张量;然后用 Python 代码带你实操创建和操作张量;最后总结一些高频踩坑点和学习建议,希望能帮你把这块基础补扎实。
如果你是机器学习入门阶段的学习者,或者学了一段时间但对张量仍然比较模糊,那这篇文章应该能帮到你。
1. 张量到底是什么:从标量到张量的递进
1.1 为什么一上来就要理解张量
很多人学习机器学习,最先接触的往往是“机器学习模型”“机器学习算法”这些宏观概念,真正开始写代码后却发现:输入数据、中间特征、模型参数,几乎全都在用张量表示。
比如一张彩色图片,在计算机里不是“一张图”,而是一个形状为(高, 宽, 通道数)的张量;一段文本经过编码后,会变成一个形状为(句子长度, 向量维度)的张量;一个批量输入给神经网络的数据,则往往是一个形状为(批量大小, 特征维度)的张量。
如果不理解张量的维度、形状和运算规则,后面看任何深度学习代码都会觉得吃力。所以,把张量的概念吃透,是机器学习学习路线里非常靠前的一步。
1.2 对标量、向量、矩阵的回顾
要理解张量,可以先从我们熟悉的数学对象看起:
- 标量(Scalar):一个单独的数,比如
5、3.14。它没有方向,也没有维度。在张量语境里,标量可以看作“0 维张量”。 - 向量(Vector):一组有序排列的数,比如
[1, 2, 3]。向量有方向和长度,在几何上可以理解为空间中的一个点或箭头。在张量语境里,向量是“1 维张量”。 - 矩阵(Matrix):一个二维数组,比如一个 2 行 3 列的表格。它可以看作多个向量按行或按列堆叠而成。在张量语境里,矩阵是“2 维张量”。
- 张量(Tensor):当维度继续增加,变成 3 维、4 维甚至更高维时,我们统称为张量。一个 3 维张量可以看作多个矩阵的堆叠,一个 4 维张量可以看作多个 3 维张量的堆叠。
用一个简单的对应表格来总结:
| 数学对象 | 常见叫法 | 张量维度 | 示例形状 |
|---|---|---|---|
| 标量 | 数值 | 0 维 | () |
| 向量 | 一维数组 | 1 维 | (3,) |
| 矩阵 | 二维数组 | 2 维 | (2, 3) |
| 3 维张量 | 三维数组 | 3 维 | (2, 3, 4) |
| 4 维张量 | 四维数组 | 4 维 | (2, 3, 4, 5) |
这里要注意一个容易混淆的地方:在机器学习领域,我们经常把“所有维度的数组”都叫张量,包括 0 维的标量。但在某些数学教材里,张量有更严格的变换定义。对于机器学习入门来说,你完全可以先把张量理解为“多维数组的通用叫法”。
1.3 张量的“维度”和“形状”
在学习张量时,有两个词经常混着用,但含义略有不同:
- 维度(Dimension / Rank):指张量有多少个“轴”(axis)。
- 形状(Shape):指每个轴上元素的数量。
举个例子:一个形状为(2, 3, 4)的张量,它的维度是 3,第一个轴上有 2 个元素,第二个轴上有 3 个元素,第三个轴上有 4 个元素。
在 Python 生态里,你通常可以通过ndim或dim()查看维度,通过shape查看具体形状。这两个属性是排查问题时的第一检查点。
2. 机器学习中为什么会用张量
2.1 数据天然是结构化的
机器学习处理的数据往往不是孤立的数值,而是具有结构的高维数据。以图像为例:一张 256×256 的彩色图片,实际上是一个形状为(256, 256, 3)的 3 维数组。其中3表示红、绿、蓝三个颜色通道。如果不使用张量,很难统一高效地表达这种多通道数据。
再比如自然语言处理中的一个句子:"我喜欢机器学习",分词后变成["我", "喜欢", "机器学习"],再经过词向量映射,会变成形状为(3, 向量维度)的 2 维张量。多个句子一起送入模型时,还会再加一个批量维度,变成 3 维张量。
可以看到,无论是图像、文本还是表格数据,张量都能提供一种统一的结构化表达方式。这也是为什么几乎所有的深度学习框架都以张量为基本数据单位。
2.2 张量运算非常适合并行计算
机器学习模型的训练本质上是在做大量的矩阵乘法和逐元素运算。张量作为一个整体,可以通过底层优化实现高效的向量化计算,避免 Python 级别的for循环带来的性能开销。
更关键的是,张量运算可以很方便地映射到 GPU(图形处理器)上。GPU 拥有大量计算核心,擅长同时处理成千上万个简单运算。把数据组织成张量后,我们可以一次性把整个张量“搬”到显存中,并用 GPU 并行完成计算,训练速度可以比 CPU 快几个数量级。
这也是为什么深度学习框架都围绕张量设计 API:TensorFlow里的Tensor、PyTorch里的Tensor、JAX里的Array,本质上都是张量在不同框架中的实现。
2.3 张量与自动求导深度绑定
现代的深度学习框架不仅把张量当作数据容器,还会在张量背后记录运算历史,从而支持自动求导(Automatic Differentiation)。
以 PyTorch 为例,当你把一个张量的requires_grad设置为True后,框架会追踪该张量的所有运算,并自动计算梯度。这对于反向传播来说至关重要:模型的前向传播就是张量之间的层层运算,反向传播则是沿着运算图逐层计算梯度。
换句话说,张量既是前向传播的载体,也是反向传播的“计算图节点”。理解了张量,你就同时理解了深度学习框架最核心的数据流机制。
3. 用代码理解张量:从 Python 列表到 PyTorch Tensor
前面我们从概念上认识了张量,接下来进入实操环节。我会分别用 Python 原生的嵌套列表、NumPy 数组以及 PyTorch 张量来展示张量的创建和基本操作。
为了避免版本不一致带来的困惑,本文示例以常见环境为例:
- Python 3.10 及以上
- NumPy 1.x
- PyTorch 2.x
如果你的版本略有不同,一般不影响示例逻辑。安装命令如下:
pip install numpy torch3.1 用 Python 列表表示多维数据
在没有任何第三方库时,我们可以用嵌套列表近似表示多维数组:
# 标量:一个数 scalar = 5 # 向量:一维列表 vector = [1, 2, 3] # 矩阵:二维嵌套列表 matrix = [ [1, 2, 3], [4, 5, 6] ] # 3 维张量:多个矩阵堆叠 tensor_3d = [ [ [1, 2, 3], [4, 5, 6] ], [ [7, 8, 9], [10, 11, 12] ] ]这种写法虽然直观,但问题是:原生 Python 列表不支持批量运算、不支持 GPU 加速,也没有统一的“形状”概念。你无法直接对两个嵌套列表做逐元素加法,也无法高效获取某个轴的长度。
因此,在实际机器学习项目中,我们会使用专门的库来创建和操作张量。
3.2 用 NumPy 创建数组并查看形状
NumPy 是 Python 数据科学生态中最基础的多维数组库。虽然严格来说 NumPy 数组和深度学习里的 Tensor 有一些区别,但在概念上是完全类似的。
import numpy as np # 创建一维数组 arr1 = np.array([1, 2, 3]) print("arr1:", arr1) print("shape:", arr1.shape) print("ndim:", arr1.ndim) print("dtype:", arr1.dtype) print() # 创建二维数组 arr2 = np.array([[1, 2, 3], [4, 5, 6]]) print("arr2:", arr2) print("shape:", arr2.shape) print("ndim:", arr2.ndim)运行结果:
arr1: [1 2 3] shape: (3,) ndim: 1 dtype: int64 arr2: [[1 2 3] [4 5 6]] shape: (2, 3) ndim: 2 dtype: int64从输出可以看到,shape返回的是元组,表示每个轴的大小;ndim表示总共有几个轴。这个信息在后面排查维度匹配问题时非常重要。
NumPy 也支持批量运算:
a = np.array([[1, 2], [3, 4]]) b = np.array([[5, 6], [7, 8]]) print("a + b:\n", a + b) print("a * b:\n", a * b) print("a @ b:\n", a @ b)其中*是逐元素乘法,@是矩阵乘法。初学者经常把这两者搞混,后面我们会专门讲解。
3.3 用 PyTorch 创建张量并理解基本属性
PyTorch 是当前深度学习和机器学习实战中最流行的框架之一。它的核心数据单位就是torch.Tensor,我们可以把它理解为“支持自动求导和 GPU 加速的 NumPy 数组”。
3.3.1 从列表或 NumPy 数组创建张量
import torch # 从 Python 列表创建 t1 = torch.tensor([[1, 2, 3], [4, 5, 6]]) print("t1:\n", t1) print("shape:", t1.shape) print("ndim:", t1.ndim) print("dtype:", t1.dtype)输出:
t1: tensor([[1, 2, 3], [4, 5, 6]]) shape: torch.Size([2, 3]) ndim: 2 dtype: torch.int643.3.2 使用内置函数创建特殊张量
# 全零张量 zeros = torch.zeros(2, 3) print("zeros:\n", zeros) # 全一张量 ones = torch.ones(2, 3) print("ones:\n", ones) # 随机张量,数值在 [0, 1) 内 rand = torch.rand(2, 3) print("rand:\n", rand) # 与现有张量相同形状的全零张量 like_zeros = torch.zeros_like(t1) print("zeros_like:\n", like_zeros)这里要注意,torch.rand生成的是均匀分布的随机数,取值在[0, 1)之间。如果你希望生成标准正态分布的随机数,可以使用torch.randn。
3.3.3 张量的基本运算
PyTorch 张量支持逐元素运算和矩阵运算:
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:\n", x + y) # 逐元素乘法 print("x * y:\n", x * y) # 矩阵乘法 print("x @ y:\n", x @ y) # 求和、均值 print("x.sum():", x.sum()) print("x.mean():", x.mean()) print("x.max():", x.max())在机器学习中,x @ y也就是矩阵乘法,出现频率最高,因为神经网络的每一层本质上就是一次矩阵乘法加上偏置和非线性激活。
3.3.4 形状重排与转置
实际处理数据时,经常需要调整张量的形状。PyTorch 提供了一些非常常用的方法:
x = torch.arange(12) # 从 0 到 11 的一维张量 print("x:", x) # 重新塑形为 3 行 4 列 x_reshaped = x.reshape(3, 4) print("x_reshaped:\n", x_reshaped) # 转置 x_t = x_reshaped.T print("x_reshaped.T:\n", x_t)运行结果:
x: tensor([ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]) x_reshaped: tensor([[ 0, 1, 2, 3], [ 4, 5, 6, 7], [ 8, 9, 10, 11]]) x_reshaped.T: tensor([[ 0, 4, 8], [ 1, 5, 9], [ 2, 6, 10], [ 3, 7, 11]])这里需要提醒:reshape的前提是元素总数不变。如果把一个包含 12 个元素的一维张量强行reshape(2, 5),就会因为元素个数不匹配而报错。
4. 完整实战:用张量实现一个小型线性层
为了把前面的概念串起来,我们来做一个小型实战:手动实现一个最简单的线性层,并用随机数据验证前向传播。
这个例子虽然简单,但它包含了机器学习和深度学习中最核心的张量操作:
- 创建输入张量。
- 创建权重矩阵和偏置向量。
- 执行矩阵乘法。
- 添加偏置。
- 使用激活函数。
4.1 场景设定
假设我们有一个批量大小为4的输入,每个样本有3个特征。我们希望经过一个线性层,将 3 维特征映射到 2 维输出。
用公式表示就是:
output = input @ weight.T + bias其中:
input的形状是(4, 3)weight的形状是(2, 3),表示 2 个输出神经元,每个神经元接收 3 个输入特征bias的形状是(2,)output的形状是(4, 2)
这里之所以对weight做转置,是因为我们希望矩阵乘法的结果是(4, 3) @ (3, 2) = (4, 2)。
4.2 完整代码
import torch import torch.nn.functional as F # 1. 创建输入张量:4 个样本,每个样本 3 个特征 x = torch.randn(4, 3) print("输入 x 的形状:", x.shape) # 2. 创建线性层的权重:2 个输出神经元,每个神经元连接 3 个输入特征 weight = torch.randn(2, 3) bias = torch.randn(2) # 3. 计算线性输出 linear_output = x @ weight.T + bias print("线性输出形状:", linear_output.shape) # 4. 应用 ReLU 激活函数 output = F.relu(linear_output) print("经过 ReLU 后的输出:") print(output)代码解释:
torch.randn(4, 3)生成服从标准正态分布的随机数,形状为(4, 3)。x @ weight.T是矩阵乘法。weight.T的形状是(3, 2),所以乘法的结果形状是(4, 2)。- 广播机制(Broadcasting)会自动把
bias这个形状为(2,)的向量加到(4, 2)矩阵的每一行上。 F.relu是 ReLU 激活函数,它会把所有负数变为 0,保留正数。这是神经网络中最常用的激活函数之一。
4.3 预期输出说明
运行这段代码,你会看到类似下面的输出:
输入 x 的形状: torch.Size([4, 3]) 线性输出形状: torch.Size([4, 2]) 经过 ReLU 后的输出: tensor([[0.0000, 0.5134], [0.0000, 0.3121], [1.2345, 0.0000], [0.0000, 0.8765]])由于是随机初始化,你的具体数值会不同,但形状一定保持不变:输入是(4, 3),输出是(4, 2)。
4.4 扩展到真正的神经网络层
实际编码中,你通常不会手写线性层,而是使用封装好的模块:
import torch.nn as nn linear_layer = nn.Linear(in_features=3, out_features=2) output = linear_layer(x)nn.Linear内部会自动创建weight和bias,并按照正确的形状进行矩阵运算。也就是说,我们在 4.2 中手写的代码,正是nn.Linear底层逻辑的简化版。
如果你想深入理解神经网络的本质,强烈建议手动实现一次线性层,再去看框架源码,理解会完全不一样。
5. 张量操作的常见错误与排查思路
在实际编码中,和张量相关的报错几乎都出现在“形状不匹配”和“数据类型不匹配”上。下面整理几个高频问题和排查方式。
5.1 常见报错与解决方向
| 问题现象 | 常见原因 | 解决思路 |
|---|---|---|
RuntimeError: The size of tensor a (3) must match the size of tensor b (4) | 逐元素运算时两个张量形状不一致 | 检查两个张量的shape,调整形状或使用广播 |
RuntimeError: mat1 and mat2 shapes cannot be multiplied | 矩阵乘法中,前一个矩阵的列数不等于后一个矩阵的行数 | 确认x @ y中x.shape[-1]是否等于y.shape[0] |
IndexError: dimension out of range | 访问了不存在的维度 | 检查ndim,确认dim参数是否越界 |
RuntimeError: expected scalar type Float but found Long | PyTorch 默认张量是int64,但要求浮点运算 | 使用.float()转换张量类型 |
| 结果全为 0 或全为 NaN | 输入数据未归一化、学习率过大、梯度爆炸 | 检查数据范围,调整学习率,检查是否出现除零 |
设备不匹配:Expected all tensors to be on the same device | 模型和数据分别位于 CPU 和 GPU | 使用model.to(device)和x.to(device) |
5.2 如何快速确认问题
当代码报错时,建议按以下顺序排查:
- 打印各张量的
shape,不要靠猜。 - 打印
dtype和device,很多错误是类型或设备不一致导致的。 - 从第一个报错往上找,找到真正发生运算的那一行。
- 对比公式:如果是矩阵乘法,确认乘法的前一个张量的最后一维和后一个张量的第一维是否一致。
- 尝试缩小数据量:用
x[:2]之类的方式截取小批量数据,便于观察问题。
5.3 复现一个真实错误
来看一个最常见的错误:
import torch a = torch.randn(3, 4) b = torch.randn(3, 4) # 错误示范:试图做矩阵乘法,但形状不支持 # c = a @ b # 报错:3x4 @ 3x4 无法相乘 # 正确方式:将 b 转置为 4x3 c = a @ b.T print(c.shape) # torch.Size([3, 3])这个例子告诉我们:做矩阵乘法前,一定要确认形状是否满足“前一个列数 = 后一个行数”。
6. 张量学习的关键概念:广播机制、设备与自动求导
6.1 广播机制(Broadcasting)
广播是张量运算中非常实用的规则,它允许形状不同的张量参与逐元素运算。PyTorch 和 NumPy 的广播规则类似:
- 从最后一个维度开始比较。
- 满足以下任一条件即可广播:两个维度相等,或其中一个维度为 1,或其中一个张量没有该维度。
举例:
a = torch.ones(4, 3) # 形状 (4, 3) b = torch.randn(3) # 形状 (3,) c = a + b # 广播,b 会先变成 (4, 3) 再相加 print(c.shape) # torch.Size([4, 3])理解广播可以让你写出更简洁的代码。但也要注意:过度依赖广播有时会让代码可读性下降,如果团队协作,建议在关键位置加上形状注释。
6.2 设备(Device):CPU 与 GPU
在机器学习中,“内存与张量对齐”是一个经常被忽略的话题。张量可以存储在 CPU 内存中,也可以存储在 GPU 显存中。如果你的数据在 CPU 上,但模型参数在 GPU 上,就会报设备不匹配错误。
常见的做法是:
device = torch.device("cuda" if torch.cuda.is_available() else "cpu") x = x.to(device) model = model.to(device)在训练大模型时,还要注意显存占用。如果你的输入批量过大,即使形状正确,也可能出现CUDA out of memory错误。此时可以减小batch_size,或者使用梯度累积等技巧。
6.3 自动求导:张量的进阶能力
PyTorch 张量最强大的特性之一就是自动求导。看一个简单例子:
x = torch.tensor([2.0], requires_grad=True) y = x ** 2 + 3 * x + 1 # 反向传播,计算梯度 y.backward() # 查看梯度:dy/dx = 2*x + 3,当 x=2 时,梯度为 7 print(x.grad) # tensor([7.])这里的核心是:requires_grad=True告诉 PyTorch 追踪所有与x相关的运算。backward()会从y开始反向计算梯度,并存入x.grad。
由此可见,深度学习中所有基于梯度的优化算法,底层都离不开张量。如果你想深入机器学习原理,张量是你绕不开的基础设施。
7. 最佳实践与工程建议
7.1 从第 0 维开始思考批量维
在深度学习中,几乎所有训练数据都会包含一个“批量大小(batch size)”维度,并且通常位于第 0 维。例如:
- 图像数据:
(batch_size, height, width, channels) - 文本数据:
(batch_size, sequence_length, embedding_dim) - 表格数据:
(batch_size, num_features)
这种统一习惯可以让代码更容易维护和复用。当你在框架中看到一个张量的第一个维度总是在变化时,不用紧张,那通常就是批量维。
7.2 命名规范与形状注释
张量代码最容易出问题的地方是“看不到形状”。建议在关键位置加上注释,例如:
# x: (batch_size, seq_len, hidden_dim) # weight: (hidden_dim, num_classes) output = x @ weight + bias在工程实践中,这种注释的成本极低,但对于代码审阅和排错有很大帮助。
7.3 控制随机种子
机器学习实验需要可复现性。每次运行都生成不同随机数会降低调试效率。建议固定随机种子:
import random import numpy as np import torch def set_seed(seed=42): random.seed(seed) np.random.seed(seed) torch.manual_seed(seed) if torch.cuda.is_available(): torch.cuda.manual_seed_all(seed) set_seed(42)7.4 注意数据类型
PyTorch 中默认的整数张量是torch.int64,默认浮点张量是torch.float32。当你从 NumPy 读取数据时,经常需要显式转换:
import numpy as np arr = np.array([[1, 2], [3, 4]], dtype=np.float32) tensor = torch.from_numpy(arr)使用.float()或.double()可以快速转换类型,但要注意混合精度训练时框架对类型有专门要求。
7.5 合理利用 GPU 但不必一开始就追求 GPU
对于刚入门机器学习的同学,并不建议一开始就纠结 GPU 环境。在 CPU 上用一个小数据集把张量的形状、维度、运算规则弄清楚,比盲目上 GPU 更有效。只有当你开始训练稍大的模型、发现训练速度无法接受时,再考虑使用 GPU 云服务或本地显卡。
7.6 多参考官方文档中的张量操作
前面介绍的reshape、transpose、matmul、sum等只是张量操作的冰山一角。学习张量时,最可靠的资料其实是 PyTorch 官方文档中的Tensor章节,以及 NumPy 官方文档中的Array章节。尤其是以下方法,在机器学习项目中会反复用到:
torch.cat/torch.stack:拼接和堆叠张量。torch.squeeze/torch.unsqueeze:压缩和增加维度。torch.permute/torch.transpose:维度换位。torch.mean/torch.max/torch.argmax:聚合和取值。torch.clamp:裁剪数值范围。
把这些方法练熟,再去看任何模型的源码,都会轻松很多。
8. 总结与后续学习路线
到这里,我们已经把“机器学习中的张量”做了一次比较全面的梳理:从标量到向量再到矩阵,然后引出张量的定义;解释了为什么深度学习框架都以张量为核心;用 NumPy 和 PyTorch 演示了创建张量、查看形状、执行运算的常见操作;通过一个小型线性层实战,把矩阵乘法、广播、激活函数串在了一起;最后总结了遇到形状错误时的排查思路以及工程中的若干建议。
下一步,你可以继续沿着以下方向深入:
- 学习“自动求导”的底层原理,理解计算结果和梯度计算的关系。
- 学习“数据加载器”中张量的常见预处理方式,例如归一化、标准化、one-hot 编码。
- 学习卷积神经网络时,重点关注图像张量形状的变化:
(batch_size, channels, height, width)。 - 学习循环神经网络或 Transformer 时,重点关注序列张量的维度。
- 实践一个小型项目,比如手写数字识别,从数据读取到模型训练完整走一遍,把张量的知识落实到实际代码中。
希望这篇文章能成为你机器学习学习路线中一块稳固的垫脚石。如果你在实践过程中遇到和张量相关的报错,不妨回到第 5 节,按表格中的排查思路逐一检查。动手练习,是掌握张量最好的方法。