向量和张量这两个词,做数据、搞算法、写引擎的人迟早都会正面撞上。我刚入门机器学习那阵最大的困惑就是:数学课上学的是“带箭头的线段”,到了代码里变成方括号里的一堆数字,到了论文里又变成一堆上下标。同一个词,三副面孔,每次都得靠上下文猜它到底在说啥。
这篇文章想做的事很单纯:用一线干活的人看得懂的方式,把向量和张量从头捋一遍。从几何直觉讲起,讲到数组表示,再讲到深度学习框架里天天打交道的“张量形状”,最后附上我这些年踩过的坑和排查思路。适合谁?准备入门机器学习但被数学劝退过的人、写代码但没系统学过线性代数的人,以及想搞清楚“张量不就是高维数组吗”这句话到底对在哪、错在哪的人。
先把结论放这儿:张量确实可以理解为高维数组,但只停留在这一层,后面看论文、调模型、自己写算子时会不断掉坑。真正需要抓住的是两件事——第一,每个维度到底代表什么含义;第二,维度之间的运算规则是什么。这两件事想明白了,向量和张量基本就跑不出你的掌控范围。
1. 向量和张量到底在说什么
1.1 一个词在不同领域的三副面孔
我干了这么多年项目,发现一个特别有意思的现象:向量这个词,在不同场景里表达的侧重点完全不一样。
在几何里,向量就是带箭头的线段,有起点、有方向、有长度,平移之后还是同一个向量。在物理里,力、速度、位移都是向量,有大小又有方向,合成的时候要遵守平行四边形法则。到了计算机这里,向量就是一个一维数组,比如[3, 4, 5],它既可以表示三维空间里的一个坐标点,也可以表示一个样本的特征向量,甚至可以表示一段时间的价格序列。三种说法都对,因为向量本身就是个多面手。
张量也一样。在数学物理的传统语境里,张量是用来描述“物理量在不同坐标系下怎么变换”的,牵扯到协变、逆变、分量规则等等,讲究得很。但在工程领域,大多数人第一次见到张量这个词,是在深度学习框架报错的时候——某个操作的输入张量形状不匹配,训练直接中断。所以你会发现,实战里的张量,几乎已经成了“多维数组”的同义词。
这两种理解的差距,说白了就是“数学家眼中的张量”和“工程师手中的张量”之间的差距。我写这篇,两边视角都会带到,但重心放在工程这一侧,毕竟真正让你在电脑前抓耳挠腮的,往往不是抽象定义,而是形状、求导、广播这些看得见摸得着的东西。
1.2 为什么叫“浅尝”而不是“精通”
把它定位成“浅尝”,不是凡尔赛,而是我对张量理论的完整体系心存敬畏。张量分析往后走,连着微分几何、连续介质力学、广义相对论的数学框架,那是一条深不见底的路,我也不敢说自己趟明白了。
但对绝大多数写代码、调模型、做数据分析的人来说,真正需要的其实就三样:理解每个维度的含义、掌握张量之间的基本运算规则、熟练处理维度变换。这三样抓牢了,日常工作中九成以上的场景都能覆盖。剩下那不到一成,真遇到再去查文献、补补课,完全来得及。
我在带新人的时候见过太多卡壳案例,比如把一个二维数组变成一维时,搞不清数据到底是按行取还是按列取;比如矩阵乘法之前忘了转置,结果两个形状对不上;再比如减法过程中触发了广播机制,代码不报错,但结果悄悄错了。这些问题完全不是理论深度不够,而是最基础的“维度感”没建立起来。所以这篇文章的核心任务,就是把这种“维度感”帮你立住。
2. 向量:从箭头到数组
2.1 先建立几何直觉,再谈抽象
很多教材一上来就扔给你定义:向量是有序数组,写在括号里。然后告诉你什么叫加法、数乘、点积。定义没错,但脱离了图像,记不牢。
我习惯先让学生看一支箭。你从原点画一个箭头指向坐标点 (3, 4),这就是向量。它有多长?用勾股定理一算,长度是 5。朝哪个方向?从原点看过去,它和 x 轴的夹角是有固定值的。长度和方向,这就是一个向量最本质的两个属性。至于它写成[3, 4]还是画成一支箭,都只是表现形式不同,背后的东西是同一个。
有了这个图,向量的加法就很好理解:两股力同时作用在一个点上,合力怎么算?把两个箭头的起点挪到一起,以它们为邻边画平行四边形,对角线的方向就是合力的方向。这就是平行四边形法则,物理课上肯定见过。
数乘更简单:给向量乘一个数 2,箭头沿着同一个方向拉长一倍;乘一个负数,箭头掉头并且长度按绝对值缩放。这些操作在图像上都是空间变换,脑子里有图,写代码时就不容易懵——比如你把一张图片的像素坐标整体平移、旋转、缩放,本质就是一串向量运算。
2.2 加法、数乘、点积与叉积
运算本身不复杂,但每一类运算都对应一个“为什么存在”的问题。
先看点积。两个向量的点积,几何意义是一个向量在另一个向量方向上的投影长度再相乘。这个运算太重要了,因为它直接给了你“两个向量有多像”的度量。两个向量方向一致,点积最大;互相垂直,点积是 0;方向相反,点积为负。机器学习里的余弦相似度,本质就是归一化之后的点积,应用极其广泛——推荐系统里比较用户和物品的相似度、文本检索里比较词向量之间的接近程度,全都在用这个东西。
叉积就需要一点空间想象力了。两个三维向量的叉积,结果是一个新的向量,它垂直于原来两个向量张成的平面,长度等于以两向量为边的平行四边形面积。这个性质在图形学里是算法向量、算面积、判断遮挡关系的看家本领。哪怕你现在用不到,知道有这回事也值了,因为你会发现很多几何问题本质上都是向量问题。
| 运算 | 输入 | 输出 | 几何含义 | 常见用途 |
|---|---|---|---|---|
| 加法 | 两个向量 | 一个向量 | 位移合成 | 坐标叠加、力合成 |
| 数乘 | 向量 + 标量 | 一个向量 | 缩放长度或反向 | 缩放、归一化 |
| 点积 | 两个向量 | 一个标量 | 投影乘积 | 余弦相似度、矩阵乘法 |
| 叉积 | 两个三维向量 | 一个向量 | 垂直向量与面积 | 法向量、旋转计算 |
2.3 范数:给向量一个“尺寸”
光有方向还不够,很多时候你需要回答“这个向量有多大”。这时候就要引入范数。最常用的是 L2 范数,就是向量的欧几里得长度,前面那个例子里[3, 4]的 L2 范数是 5。计算方式是把每个分量的平方加起来再开根号。
L1 范数则是所有分量绝对值之和。同一个向量[3, -4],L1 范数是 7,L2 范数是 5,区别很明显:L1 对离群分量更敏感,L2 对整体能量更敏感。这个区别在机器学习的正则化里被玩出了花。L2 正则化倾向于把参数压得很小很均匀,L1 正则化倾向于把不重要的参数直接压成 0,起到稀疏化的作用。你看到的很多特征选择手段,背后的数学根源就是这两种范数的性格差异。
还有 L∞ 范数,取的是分量绝对值里的最大值,相当于问“你这个向量最夸张的那个分量有多夸张”。在计算最大误差、衡量数值稳定性的时候会用到。给向量定义了范数,你就可以比较两个向量谁“更长”,也可以定义距离——两个向量的差的范数,就是它们之间的距离。聚类、近邻搜索、相似度计算,全建立在这套距离定义之上。
3. 张量:把“维度”这件事系统化
3.1 张量的“阶”:从标量到高维数组
理解张量最快的方式,是从“阶”这个概念入手。
标量是零阶张量,就是一个数。向量是一阶张量,一维数组。矩阵是二阶张量,二维数组。三阶张量呢,可以想象成一摞矩阵叠在一起,三维数组。四阶张量就是一摞三维数组叠在一起。数学里张量没有维度上限,但你实际在代码里见到的,绝大多数在四阶以下。
这套对应关系极其重要,因为深度学习里所有的数据都是这么装的。一张彩色图片是什么?如果按“高度、宽度、颜色通道”的排法,它就是一个三阶张量:第 0 维是像素行数,第 1 维是像素列数,第 2 维是 RGB 三个通道。一批图片呢?四个维度叠进去:批次大小、高度、宽度、通道数。你平时看框架报错信息里那些数字,每一个数字背后都对应着一个真实的物理含义。
| 张量阶数 | 通俗说法 | 数学记法 | 生活中的例子 |
|---|---|---|---|
| 0 阶 | 标量 | 一个数 | 温度、房价、单次预测值 |
| 1 阶 | 向量 | 一维数组 | 某个样本的特征 |
| 2 阶 | 矩阵 | 二维表格 | 特征矩阵、关系表 |
| 3 阶 | 三阶张量 | 三维数组 | 单张彩色图片 |
| 4 阶 | 四阶张量 | 四维数组 | 一批图片、视频帧序列 |
这个表值得你打印出来贴在显示器旁边。我见过很多人的问题不在于不会算,而在于一看四维数组就头皮发麻,根本想象不出这堆数字长什么样。其实你就把它当“装着很多张图片的柜子”,每一层柜子是一张图,每张图是一张表格,表格每个格子是一个像素,像素又有 RGB 三个值。拆到这层,四维也没那么吓人。
3.2 张量的严格定义:多重线性映射
工程视角够用了,但我也得稍微提一嘴严格的数学定义,不然你以后看更深的资料会被绕晕:张量是多重线性映射。
这句话听着绕,拆开就好。线性映射,你可以理解为一个“均匀放大”的函数,输入扩大多少倍,输出就跟着扩大多少倍,而且加法可以分开计算。多重线性映射,就是同时接收多个输入的线性映射——它对每一个输入单独都是线性的,但几个输入是一起作用的。
拿一个很常见的例子:双线性插值。你要在图片两个像素之间插出一个新像素的颜色值,这个值既依赖左邻居的颜色,又依赖右邻居的颜色,两边按权重各取一部分。权重的分配对每一个输入都是线性的,但最终结果是两个输入共同决定的。这就是多重线性映射的味道。矩阵乘法其实也是个多重线性映射:一个矩阵作用于一个向量,结果的每个分量都是矩阵行和向量元素的“乘积再求和”,对矩阵和向量双方都是线性的。
理解到这层就够用了。你不需要现在就去钻研协变变量、张量场那些东西,但至少知道:张量不只是一种“装数据的容器”,它还自带一套运算规则。这套规则是它和普通多维数组的根本区别——数组只是静态存放数据,张量的运算方式有严格的数学约束。实战中很多隐形 bug,就是因为把“数组思维”带进了“张量世界”。
3.3 注意:张量的“阶”和矩阵的“秩”不是一回事
这个坑我踩过,而且栽得挺深,必须单独拎出来讲。
线性代数里管矩阵叫“秩”多少多少,比如秩为 2 的矩阵,意思是矩阵的列空间是二维的。可到了张量这边,“秩”这个词经常被用来表示张量的“阶”:一阶张量是向量,二阶张量是矩阵。于是你经常会看到 “rank-3 tensor”这种说法,它意思是“三阶张量”,而不是“秩为 3 的矩阵”。
同一个英文单词,两套含义,特别容易引起混乱。我在看一篇讲高阶张量分解的文章时,就被 “rank”折腾了半天,看懂了才发现那边说的始终是“阶”。所以我给你的建议是:中文交流里尽量用“阶”指代张量维度数,用“秩”指代矩阵的秩;英文资料里看到 rank 一定要根据上下文判断。判断依据很简单——看它的数字是修饰“数组维度”还是修饰“列空间维数”,前者是阶,后者是秩。
4. 实操视角:张量形状才是真正的战场
4.1 学会读 shape:每一维都必须有名字
如果你去生产环境里排查问题,十次报错里有八次跟张量形状有关。所以读 shape 是基本功,而且不能只读形状,要把每一维的含义刻在脑子里。
拿图像数据举例。一张 32×32 的彩色图片,如果按“通道、高度、宽度”的排列方式存储,它的张量形状是[3, 32, 32];如果按“高度、宽度、通道”存,则是[32, 32, 3]。这两种排列在工程界都存在,分别有不同的框架和硬件偏好。代码本身不会告诉你按哪种顺序存,你得自己搞清楚模型的每个输入层期望什么排列,否则数据进去了,结果完全错乱。
怎么练?我的习惯是拿到一个张量,先给每一维起名字,写注释。比如一个形状为[64, 3, 32, 32]的张量,我会标注成“批次=64,通道=3,高=32,宽=32”。每次用 reshape、转置、切片之前,先问自己一句:我要操作的是哪个维度?它在这个张量里排第几位?这一步多花十秒钟,后面能省下几个小时的调试时间。
4.2 reshape、转置与广播:三个高频操作,三种脾气
reshape 是把数据重新排列成新的形状,但它有个隐藏陷阱:数据是按固定顺序线性排列的。[a, b, c, d, ...]这串元素,不打乱顺序重新码放,码放方式是由形状决定的。如果你没搞清楚原始数据的存储顺序就乱 reshape,结果就是一个巨大的数字迷宫,表面不报错,内在全乱套。我见过最典型的一个错误:把一个[4, 4]的矩阵 reshape 成[2, 8]和转置成[8, 2],然后拿着两个结果去做运算,自以为一样,其实完全不同。记住一句话:reshape 是在“不改变元素顺序”的前提下重新划分格子,转置是“按维度交换数据位置”,二者完全是两回事。
广播机制则是另一个温柔陷阱。它的设计初衷是好的:形状不完全一致的张量做运算时,自动把维度扩展到能匹配的形状,省得你手动复制数据。比如一个形状为[4, 1]的向量和一个形状为[4, 3]的矩阵相加,框架会自动把前者沿着第二维复制成[4, 3]。方便是真方便,危险也是真危险。因为广播一旦生效,代码不报错,结果却是错的。比如你做减法,本意是拿向量a逐行去减矩阵的每一行,结果广播把a沿另一个方向扩展了,减完的结果从“每行都处理”变成“只有一个值被处理”。
所以我的铁律是:涉及广播的操作,一定要先在注释里写明参与运算的张量各维名称,并检查哪些维度是显式指定的,哪些是隐式广播出来的。该显式的地方绝不依赖广播,该手动 reshape 的地方绝不用转置代替。细节做到位,这一类 bug 能消掉大半。
4.3 下标求和:把复杂运算压缩成一行
张量的运算种类很多,但你会发现,真正核心的运算——矩阵乘法、点积、转置后的乘法、按维度求和——背后都遵循同一个模式:指定某些维度两两配对相乘,再把乘积结果按剩下未配对的维度求和。
为了统一表达这套逻辑,数学里有一种下标求和约定:把参与运算的每个张量标注上下标,下标相同的维度说明它们要配对相乘,最后对配对维度的所有可能取值求和。它虽然没啥花哨的名字,却是让你一眼看穿复杂运算的神器。很多数值计算库都把它实现成了类似 einsum 风格的一行式算子,几个字母就能描述一长串运算。
实际操作中,我会在纸上把表达式拆两步:第一步,写下每个张量的形状和维度名;第二步,圈出要配对的维度,明确输出里保留哪些维度、去掉哪些维度。比如矩阵乘法就是ik, kj -> ij:i 保留、k 配对求和、j 保留。点积就是i, i ->,输出一个标量。整个过程比背公式直观得多,而且跟库函数的行为完全对得上。你把这个习惯练熟了,很多看起来复杂的网络层核心运算,几秒就能在脑子里拆清楚。
5. 常见误区与排查心得
5.1 我踩过的几个典型坑
这些年我踩过的跟张量相关的坑,挑四个典型的列出来,每一个都是血泪教训。
第一个,矩阵乘法算反了。矩阵乘法不满足交换律,A 乘以 B和B 乘以 A通常结果不同。神经网络的公式里,你经常要在右乘和左乘之间切换,忘了调整顺序,形状没报错,因为恰好整体维数一致,但数值全错了。现在我的习惯是,每次写矩阵乘法前,先确认公式里是谁乘谁,写上注释再写代码。
第二个,转置位置放错。很多时候你需要对某个张量转置后再做乘法,但转置必须放在正确的位置。一些库的矩阵乘法接口内部会自动处理转置,有些不会。结果就是,同样的数据在不同环境下跑,一个对、一个错,排查起来相当崩溃。解决方法是:少依赖接口的隐含行为,显式写出转置步骤。
第三个,reshape 当成转置用。这个前面提过,但值得再说一遍:reshape 是重排格子,不是换数据顺序。我把一个[2, 3]的矩阵 reshape 成[3, 2],以为等于转置,结果每行数据完全错位,训练指标莫名其妙地崩了。花了整整一天才用逐元素打印定位到问题。
第四个,广播方向反了。两个张量形状是[3, 1]和[1, 3],做加法时广播机制会把两个都扩展成[3, 3],看起来没问题。但如果你本意是让[3, 1]和[3]对齐,给你悄悄扩展成别的方向,结果就是灾难。我现在的自检办法是:每个张量用之前,先打印它的形状,确认每个维度数值和语义对应。
5.2 一个实用的自检查清单
为了避免反复犯同样的错误,我把排查流程固定成了清单,每次张量相关的代码出问题,按顺序过一遍。
- 打印所有参与运算的张量形状,和公式推导的预期形状逐一对比。多半问题在这一步就暴露了。
- 检查每个维度的语义:批次、通道、高度、宽度、特征长度,一一对应。
- 确认所有 reshape 和转置操作显式写清楚,不依赖隐含行为。
- 确认所有广播操作,都清楚哪些维度是显式的,哪些会隐式扩展。
- 用最小样例验证:拿几个数字,手工算一遍,再跟代码输出对比。
- 把每一步中间结果打印出来,别急着看最终值,中间过程错了最后一定错。
这个清单看起来很朴素,但真能救命。我有一次排查一个多小时没头绪的问题,最后就是靠“把中间结果一步一步打印出来”这个方法,发现是某个归一化操作把维度的均值算错了方向。中间结果一旦对了,错误范围就能从“整个网络”缩小到“某一层内部的几十行代码”,效率直接翻倍。
5.3 给新手的建议:学习路径怎么走
如果你刚入门,我建议按下面这个顺序来,每一步都可以配合手写计算。
第一步,把标量、向量、矩阵、张量之间的关系彻底理清。写一个表格,把每阶张量的例子、维度数、含义列清楚,直到能不看资料默写出来。第二步,手算大量的二维矩阵运算,包括乘法、转置、点积、求和,每个都至少用小规模数字验算一遍。第三步,上手写代码,用自己熟悉的数值计算库去复现这些手算结果。先 2×2,再 3×3,再带批次的高阶数据。第四步,看一个真实模型的结构,找出每个层输入输出的张量形状,并画出维度变化。这一步做完,你基本就出师了。
线代书要不要看?要,但不用一开始啃大部头。先把“矩阵乘以向量”、“矩阵分解的意义”、“特征值到底是什么”这几个核心概念搞明白,比通读一整本教材有用得多。剩下的内容,遇到具体场景再查,学习效率会高很多。我见过太多人买了一摞数学书,结果全被复杂度劝退,最后还是靠干活学到的——带着问题学,永远是最快的路径。
最后分享一个我自己用着特别顺的小技巧:永远准备一个“最小样例习惯”。任何张量运算,先用极小的数据跑通逻辑,再换大规模数据。不要图省事直接拿真实数据开跑,因为真实数据形状大、数值杂,出错了根本不知道错在哪个环节。先拿一个 2×2 的矩阵验证你的思路,再放大到 1000×1000,心态完全不一样。这个习惯我保持了几年,陪我扛过了数不清的 shape mismatch 夜战,今天一并送给你。