小明第一次拆开乐高套装时,最着迷的不是成品模型,而是那些大大小小的积木块:有的像窄窄的尺子(1×6),有的像扁平的托盘(2×4),还有的是立体方块(2×2×2)。无论形状多么五花八门,每块积木都由一颗颗整齐排列的凸点组成。把“数字”装进这些凸点,你就得到了深度学习世界的绝对主角——张量(Tensor)。张量运算,就是这些“数字积木”之间的拼合游戏:两块同尺寸的积木可以逐颗凸点对碰(逐元素运算);一块积木的所有凸点可以统一涂色(标量乘法);用行槽卡住列销能搭建新结构(矩阵乘法);一张小贴纸还能自动复制到每个位置(广播机制)。这篇文章请乐高当全程向导,把张量的加减乘、缩并、广播和形状变换一网打尽。读完你会发现,PyTorch 和 TensorFlow 里那些高深的 tensor 操作,不过是积木拼搭的另一种说法。
张量运算是深度学习底层计算的“乐高说明书”。掌握它,你就能看懂框架里每个 shape 的来龙去脉,理解模型为什么这样算、算得对不对——这是从“调库选手”升级为“算法工程师”的必经之路。
可视化演示
展示张量运算三大核心概念:从标量到3D张量的阶数层次、广播机制自动扩展形状对齐、以及爱因斯坦求和约定的索引缩并运算过程。
本节配有交互动效(进入视口后自动加载,约 3KB)。
张量阶数:从标量到多维数组 0D标量(),单个数值 1D向量(n),一维数组 2D矩阵(m,n),行列结构 shape = (d1, d2, ..., dn) 张量阶数等于索引维度的数量。
张量初识
想象一盒刚开封的乐高:1×1 的方块上只有一颗凸点,1×6 的长条上六颗凸点排成一线,2×4 的平板上凸点铺成一个平面,而 2×2×2 的立方体则把凸点撑向了立体空间。无论多复杂的模型,本质都是凸点按特定形状排列。深度学习里的“张量”就是这些“数字积木”——只不过凸点换成了实实在在的数字。
张量是存储数字的多维数组,它的“阶数”就是积木有几个延伸方向:0 阶张量是单个数字(标量),宛如一颗孤零零的凸点;1 阶张量是数字列表(向量),好比一条长条积木;2 阶张量是数字表格(矩阵),恰似一块平面平板;3 阶及以上的张量则是立体甚至超立方的数字排列,每个数字的位置由多个下标共同确定。
在深度学习框架里,你接触到的几乎一切数据都是张量。一张灰度图是 (高,宽) 的矩阵;一张彩色图是 (高,宽,通道) 的三阶张量;一批图片则是 (批大小,高,宽,通道) 的四阶张量。张量的“形状”用 shape 记录每个维度上的长度,就像乐高包装盒侧面印的尺寸规格。
看懂形状这个“尺寸说明书”,后面所有运算规则才能对得上号。没有它,张量加法会乱套,矩阵乘法会撞车,广播机制更无从谈起。
- 张量记号 \\(\mathcal{X}\\)用大写花体字母表示整个多维数组,就像乐高积木�的“型号名”。
- 属于符号 \\(\in\\)表示“位于、属于”,说明这个张量不是凭空存在,而是某个数学空间中的一员。
- 实数空间 \\(\mathbb{R}\\)\\(\mathbb{R}\\) 是所有实数的集合,表示张量里装的每个元素都是普通实数(比如 1.5、-3、1024)。
- 形状列表 \\(d_1 \times d_2 \times \cdots \times d_n\\)\\(d_1, d_2, \dots, d_n\\) 是每一维的长度,它们相乘的“维数结构”决定了积木的尺寸说明书。
- 阶数 \\(n\\)右上角的 \\(n\\) 表示总共有多少个维度方向,也就是你这个积木“长”了几个方向。
逐元素运算
两块尺寸完全相同的乐高平板放在桌上,凸点天然一一对位。你要做的就是把对应位置的凸点按规则处理:上层 1 号凸点碰下层 1 号凸点,2 号碰 2 号,绝不跨位。这种“同位置对碰”就是逐元素运算(element-wise operation),它是张量世界里最朴素也最常用的一类操作。
逐元素运算要求两个张量形状完全一致,然后对同一位置的数字做相同操作。最常见的是逐元素加法和逐元素乘法(也叫哈达玛积,Hadamard product)。注意:这里的“乘法”是每个位置独立相乘,完全不同于后文要讲的矩阵乘法——就像乐高凸点是逐颗拼接,而不是整行整列咬合。
以两个 \\(2 \times 2\\) 矩阵为例,逐元素加法的结果矩阵中,第 \\(i\\) 行第 \\(j\\) 列的元素,等于两个输入矩阵同一位置元素之和。用公式写出来就是:
逐元素运算就像两块同尺寸乐高板逐颗凸点拼接,不会改变积木的外形——结果积木的尺寸永远等于输入尺寸。深度学习里的激活函数(ReLU、Sigmoid)本质上就是对张量做逐元素变换,所以“形状不变”这个特性在神经网络中无处不在。
- 下标 \\((i,j)\\)乐高平板上第 \\(i\\) 行、第 \\(j\\) 列的那颗凸点坐标,是整个运算的“定位系统”。
- 读取 \\(A_{i,j}\\)从积木 A 中取出坐标 \\((i,j)\\) 处的那颗“数字凸点”。
- 读取 \\(B_{i,j}\\)从积木 B 中取出同一坐标处的数字凸点——两块积木必须同尺寸,否则这里就取空了。
- 相加操作把两个取出来的数字直接相加,相当于两颗凸点“对碰合并”成一个数值。
- 写入结果 \\(C_{i,j}\\)把合并后的数值放到结果积木 C 的同一坐标位置,形状完全保留。
标量乘法
如果你想把一块乐高积木整体换成另一种颜色,最省事的办法是给每一颗凸点都涂上相同的颜料:一颗凸点涂一次,全部涂完,整块积木就焕然一新。这就是标量乘法(scalar multiplication)的直观画面——用一个数字去“涂”遍张量的每一个元素。
标量就是一个独立的数字,也就是 0 阶张量。标量乘法是用这个数字乘上张量的每个元素,相当于把整块积木“整体缩放”。注意它和逐元素乘法的区别:逐元素乘法是两个同形状张量逐颗对碰,而标量乘法是一个数字对全体元素统一操作。
公式写出来非常简洁:结果张量 B 的第 \\(i\\) 行第 \\(j\\) 列元素,等于标量 \\(c\\) 乘以原张量 A 的对应元素。
标量乘法不改变积木的形状,只改变每颗凸点的“颜色深浅”。在神经网络中,学习率(learning rate)本质上就是一个标量:它乘以梯度张量,控制每次参数更新的步长——一步迈多大,全看这个“颜料浓度旋钮”拧到了哪里。
- 标量 \\(c\\)那个统一颜色的“颜料桶”,它是一个普通实数,没有维度,所以能自由地“触碰”张量里每个元素。
- 原张量元素 \\(A_{i,j}\\)从积木 A 的 \\((i,j)\\) 位置取出待涂色的数字凸点。
- 乘法操作 \\(c \times A_{i,j}\\)标量与这个元素相乘,相当于给凸点涂上新颜色的同时按比例调整了它的“深浅”。
- 写回 \\(B_{i,j}\\)把缩放后的值放回结果积木 B 的同一位置,逐颗涂完,形状不变。
矩阵乘法
乐高能搭出复杂模型,靠的是榫卯卡扣:一块积木的行槽与另一块的列销精确咬合。不同于逐元素的“颗粒对颗粒”,矩阵乘法是“整行与整列”的配对——左矩阵的第 \\(i\\) 行要和右矩阵的第 \\(j\\) 列做一次“内积约会”,约会的产物就是结果矩阵的 \\(C_{i,j}\\)。
具体来说,把左矩阵第 \\(i\\) 行上的每个数字,与右矩阵第 \\(j\\) 列上对应位置的数字相乘,再把所有乘积加起来。用一个求和符号概括就是:
矩阵乘法有一条铁律:左矩阵 \\((m, k)\\) 与右矩阵 \\((k, n)\\) 相乘,内维 \\(k\\) 必须相等,结果形状是 \\((m, n)\\),由两个外维决定。形象地说,只有行槽的宽度和列销的宽度一致,榫卯才能咬合。这个“先乘后加、行配列”的模式正是深度学习全连接层的计算内核。
- 求和符号 \\(\sum_{k=1}^{n}\\)让变量 \\(k\\) 从 1 走到 \\(n\\),把沿途每一项都累加起来——这是“流水线汇总”的总开关。
- 左矩阵元素 \\(A_{i,k}\\)取左积木第 \\(i\\) 行第 \\(k\\) 列的那颗凸点,\\(k\\) 是行内游走的位置。
- 右矩阵元素 \\(B_{k,j}\\)取右积木第 \\(k\\) 行第 \\(j\\) 列的那颗凸点,与 \\(A_{i,k}\\) 形成“行槽对列销”的配对。
- 相乘并累加每一对 \\(A_{i,k} \times B_{k,j}\\) 先相乘,再交给求和符号累加,最终汇总成一个数。
- 写入结果 \\(C_{i,j}\\)这个汇总数就是结果积木 \\((i,j)\\) 位置上的凸点值,整行配整列的“婚礼”完成。
缩并运算
当你把两块乐高板用力压在一起时,中间的隔层消失了,原本各自独立的两个维度融成一个整体。张量里的“缩并”(contraction)就是这样一种“压合”操作:沿着某个轴把元素求和,从而消去这个维度。
最典型的缩并是向量点积:两个长度相同的向量,对应元素相乘后全部累加,最终得到一个标量。矩阵的迹也是缩并——只对主对角线元素求和。无论是哪种,共同点是“某些轴被求和吃掉,维度消失”。
向量点积的公式是:
矩阵的迹公式类似,只是它只对 \\(i=j\\) 的对角线元素求和:
缩并就是“压合成一块”——两个向量的 \\(n\\) 个分量像 \\(n\\) 对齿轮,咬合转动后只剩下总的力矩(一个数)。在爱因斯坦求和约定中,出现两次的下标就是“要消失的轴”的标记。深度学习里的注意力分数 \\(QK^{\top}\\) 和全连接层的加权求和,本质都是缩并运算。
- 对角线下标 \\(A_{i,i}\\)只取行号等于列号的对角线凸点。
- 求和 \\(\sum_{i}\\)把整条主对角线上的元素相加,一个正方形矩阵被压缩成一个数字。
广播机制
老师发给每个小组一张“规则贴纸”,上面写着“全体加 1”。贴纸只有一份,却会自动复制,贴满教室每个座位——这就是广播(Broadcasting):当两个张量的形状不完全一致时,小张量会自动“复制扩展”成大张量的形状,然后进行逐元素运算。
广播的规则可以概括为三条:第一,从右往左对齐两个张量的形状;第二,某维度长度为 1 时,自动扩展到另一方的长度;第三,若某维度两边都大于 1 且不相等,则直接报错。最常见的例子是标量与张量运算——标量的形状是空的(或视为 \\((1,)\\)),它可以广播到任意形状。
一个经典的广播例子是列向量与行向量相加:
广播机制让代码简洁得惊人:给一批数据统一加一个均值向量,不用写任何 for 循环,一行 X + mean 就完成。就像贴纸自动复制到每个座位,省去手工一张张贴的功夫——这正是 NumPy 和 PyTorch 中 bias 叠加、归一化等操作高效的秘密。
- 形状 \\((3,1)\\)一个 3 行 1 列的“竖条”积木,只有一列凸点。
- 形状 \\((1,4)\\)一个 1 行 4 列的“横条”积木,只有一行凸点。
- 从右对齐第 2 维是 1 和 4,取较大值 4;第 1 维是 3 和 1,取较大值 3。
- 维度扩展竖条自动复制成 4 列,横条自动复制成 3 行,二者都变成 \\((3,4)\\) 的大积木。
- 逐元素相加扩展后的两块同尺寸积木执行普通的逐元素加法。
形状变换
一块 2×4 的乐高平板拆下来,可以重新拼成 4×2 的长条——颗粒总数不变,只是排列方式变了。张量的形状变换正如拆拼重组:元素总数不变,“形状”被重新解释。
最常用的形状变换有三个:reshape 重排形状(要求新旧形状的元素总数一致);转置 交换两个轴(就像把积木旋转 90°);增删维度 用 None 或 unsqueeze 增加一个长度为 1 的轴(相当于给积木加一个“单层包装盒”)。注意 reshape 是按行优先顺序把元素重新排列,不是随意乱改数据。
公式上,reshape 和转置可以写为:
更进一步的“张量积”(外积)则完全不同:它保留所有维度并让它们互相搭配,结果是一个更高阶的张量:
形状变换不增不减元素,只是换了个视角看同一堆积木。无论是把图片展平成向量送进全连接层,还是在注意力机制里调整维度顺序方便计算,都离不开这些“拆了再拼”的操作。
- 张量积符号 \\(\otimes\\)外积,意思是“两块积木的每个凸点都要和另一块的每个凸点两两组合”。
- 原元素 \\(A_{i,j}\\) 与 \\(B_{k,l}\\)分别取自两块积木的任意位置。
- 两两相乘所有组合都相乘,生成一个 4 阶新张量,每个组合值占据新积木的一个独立位置。
AI 应用场景
神经网络的全连接层
阶段:模型前向传播
解决的问题:全连接层要对一批样本同时做线性变换 \\(Y = XW^{\top} + b\\)。传统写法要嵌套循环遍历每个样本、每个特征,代码冗长且慢;用张量矩阵乘法一次算完所有样本,bias 通过广播自动加到每一行。
具体案例:在 PyTorch 中,输入 X 形状为 (batch, 784),权重 W 形状为 (128, 784),一句 X @ W.T + b 就完成了 128 个神经元的线性变换。b 形状是 (128,),广播机制自动把它平铺到 batch 个样本上。这正是全连接层每秒执行数百万次的底层操作。
图像批处理与归一化
阶段:数据预处理
解决的问题:训练时一次要处理几十上百张图片,每张图片是 \\((通道,高,宽)\\) 的三阶张量,拼在一起是四阶张量 \\((批大小,通道,高,宽)\\)。传统逐张处理既慢又容易出错;张量的逐元素运算和广播让“整批归一化”变成一行代码。
具体案例:对一批 CIFAR-10 图像做标准化时,用 (images - mean) / std,其中 mean 和 std 形状是 (3, 1, 1),通过广播自动应用到每张图、每个像素位置。与 for 循环遍历 10000 张图相比,速度提升几十倍,代码量减少一个量级。
注意力机制中的缩并
阶段:模型前向传播
解决的问题:Transformer 的注意力分数需要计算 Query 与 Key 的相似度 \\(\text{score} = QK^{\top}\\),这本质是批量矩阵乘法加缩并。传统实现要求手动管理多维下标,极易出错;张量 API 让高维缩并表达得清晰简洁。
具体案例:在自注意力中,Q 形状为 (batch, heads, seq_len, d_k),K 转置后与 Q 做批量矩阵乘法,得到形状 (batch, heads, seq_len, seq_len) 的注意力分数矩阵。其中每个分数都是两个向量的点积——一次缩并运算,把 d_k 维压缩成一个相似度值。整个 GPT 系列模型的“灵魂”就建立在这个操作上。
Python 演示
import numpy as np # 张量初识: 构建一个3阶张量x = np.array([ [[1, 2], [3, 4]], [[5, 6], [7, 8]]])print("X shape:", x.shape) # 逐元素运算: 对应位置相加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) # 标量乘法: 每个元素乘3print("3 * a =\n", 3 * a) # 矩阵乘法: 行与列内积print("a @ b =\n", a @ b) # 缩并: 向量点积u = np.array([1, 2, 3])v = np.array([4, 5, 6])print("dot(u, v) =", np.dot(u, v)) # 广播: (3,1) 与 (1,4) 相加col = np.array([[1], [2], [3]])row = np.array([[10, 20, 30, 40]])print("col + row =\n", col + row) # 形状变换: reshape 与 转置c = np.arange(6).reshape(2, 3)print("c =\n", c)print("c.reshape(3, 2) =\n", c.reshape(3, 2))print("c.T =\n", c.T) # 增删维度print("c[None].shape =", c[None].shape)
x = np.array([[[1,2],[3,4]],[[5,6],[7,8]]])用嵌套列表构建 3 阶张量,等价于乐高立体块。print("X shape:", x.shape)shape属性返回每个维度的长度,就是积木的“尺寸说明书”。a + b逐元素加法,要求形状一致,对应位置凸点直接相加。a * b逐元素乘法(哈达玛积),不是矩阵乘法,每个位置独立相乘。3 * a标量乘法,3 这个“颜料”涂遍每个元素,形状不变。a @ b矩阵乘法,@运算符做行×列内积,要求内维匹配。np.dot(u, v)向量点积,属于缩并运算,两个 3 维向量压合成一个标量。col + row广播机制,(3,1) 和 (1,4) 自动扩展成 (3,4) 再相加。c.reshape(3, 2)重塑形状,6 个元素从 2×3 重排成 3×2,总数不变。c.T转置,交换两个轴,相当于积木旋转 90 度。c[None]在最前面增加一个长度为 1 的新轴,为广播做准备。
常见误区
误区我以为在 NumPy 里写 `A * B` 就是做矩阵乘法,结果算出来的数字全不对,还找不到原因。
正解A * B 是逐元素乘法(哈达玛积),只有对应位置的数字相乘,就像两块乐高积木逐颗凸点对碰。真正的矩阵乘法要用 A @ B 或 np.dot(A, B),它做的是“整行配整列、先乘后加”。两者一个是“颗粒对颗粒”,一个是“行槽对列销”,完全不是一回事。
为何常见:因为数学课上矩阵乘法通常直接写 \\(AB\\),大家先入为主地认为 * 就是矩阵乘,忽略了编程语言里运算符各有专门含义。
误区只要写 `A + B`,不管两个张量形状差多远,NumPy 都会自动帮我“对齐”成功。
正解广播有严格规则:从右往左对齐,某维度为 1 或缺失才可扩展,如果两个维度都大于 1 且不相等(比如 (3,2) 和 (2,3)),会直接抛 ValueError。广播不是万能胶,更像是“贴纸只能贴到形状兼容的位置,硬贴会撕坏图纸”。报错是保护你,不是系统不智能。
为何常见:网上教程都在强调广播“自动扩展”的便利,却很少强调它“会报错”的边界,导致初学者只记住了方便的一面。
误区一提张量我就想到立体的立方体,觉得至少得有长宽高三个维度才算张量。
正解张量是“多维数组”的统称,0 阶(标量)、1 阶(向量)、2 阶(矩阵)、3 阶及以上都叫张量。就像乐高积木既有 1×1 的方块也有立体件,它们都是积木家族成员。深度学习里连一个损失值(0 阶)、一组梯度(1 阶)、一层权重(2 阶)都叫张量。
为何常见:教科书插图总爱画立体张量,看多了自然以为张量必须“立体”,忽略了阶数从 0 开始的事实。
误区reshape 是万能的,我想把 (2,3) 变成 (4,2) 甚至 (5,5) 都行,数据会自动补齐。
正解reshape 的前提是元素总数完全一致——(2,3) 有 6 个元素,只能重排成各维度乘积为 6 的形状(如 (3,2)、(1,6)、(6,))。它只是“拆了重拼”,不会凭空多出或丢失凸点。想加元素得用拼接(concatenate),想减少得用切片(slicing)。
为何常见:reshape 用起来太顺手,一行代码就变形状,让人误以为它神通广大,忽略了它“不增不减”的本质约束。
面试题
Q1. 张量的“阶”指的是什么?
- A.张量中元素的个数
- B.张量维度的数量← 正确答案
- C.张量中数值的大小
- D.张量存储空间的字节数
答案:B。阶(rank)是张量的维度方向数,就像乐高积木有几个延伸方向。0 阶是标量,1 阶是向量,2 阶是矩阵。A 是元素总数,C 是数值大小,D 是存储量,都与“阶”无关。
Q2. 以下关于逐元素运算的说法正确的是?
- A.要求两张量形状一致← 正确答案
- B.对应位置独立运算← 正确答案
- C.结果形状与输入不同
- D.乘法用符号 @ 表示
答案:AB。逐元素运算像两块同尺寸乐高板逐颗凸点对碰,形状必须一致,结果形状也保持一致,所以 C 错。逐元素乘法用 *,矩阵乘法才用 @,D 错。只有 A 和 B 正确。
Q3. 在 NumPy 中,A * B 表示什么运算?
- A.矩阵乘法
- B.逐元素乘法← 正确答案
- C.向量叉积
- D.张量外积
答案:B。A * B 是哈达玛积,对应位置独立相乘,就像积木逐颗对碰。矩阵乘法要用 A @ B。叉积和外积都是完全不同的运算,分别针对三维向量和张量积场景。
Q4. 矩阵乘法 A(m,k) × B(k,n) 的结果形状是?
- A.(m, n)← 正确答案
- B.(k, k)
- C.(m, k)
- D.(k, n)
答案:A。矩阵乘法中内维 k 被“吃掉”(缩并),结果形状由两个外维决定,即 (m, n)。这就像行槽与列销咬合后,中间隔层消失,只剩外部轮廓。
Q5. 广播机制下,(3,1) 与 (1,4) 的两个张量相加,结果形状是?
- A.(3, 4)← 正确答案
- B.(3, 1)
- C.(1, 4)
- D.直接报错
答案:A。两个形状从右往左对齐:第 2 维 1 和 4 取 4,第 1 维 3 和 1 取 3,最终扩展成 (3,4)。竖条积木复制成 4 列,横条复制成 3 行,拼成完整平面。
Q6. 关于缩并运算,下列说法正确的是?
- A.会使部分维度消失← 正确答案
- B.向量点积是特例← 正确答案
- C.运算后形状完全不变
- D.只适用于一维向量
答案:AB。缩并沿某轴求和消去该维度,A 对。向量点积把一维压缩成标量,是缩并的最简特例,B 对。形状必然改变(维度减少),C 错。矩阵的迹、批量矩阵乘中的内维压缩都是缩并,不止一维向量,D 错。
Q7. 将形状 (2,3) 的张量 reshape 为 (3,2),前提条件是?
- A.元素总数相同← 正确答案
- B.行列数相同
- C.维度数相同
- D.无需任何条件
答案:A。reshape 只是“拆了重拼”,(2,3) 有 6 个元素,(3,2) 也有 6 个,总数一致才行。这就像乐高积木颗粒总数不变,只是排列方式变了。
Q8. 广播运算中,形状 (4,3) 与形状 (3,) 相加,结果形状是?
- A.(4, 3)← 正确答案
- B.(3, 4)
- C.(12, 3)
- D.无法直接广播
答案:A。从右对齐:(3,) 视为 (1,3),第 2 维 3 与 3 相等取 3,第 1 维 4 与缺失(视为 1)取 4,结果为 (4,3)。这相当于把一条“横条积木”复制 4 份叠在每行上。
Q9. 关于矩阵乘法性质,说法正确的有?
- A.满足乘法结合律← 正确答案
- B.满足乘法交换律
- C.内维度必须相等← 正确答案
- D.结果取外维形状← 正确答案
答案:ACD。矩阵乘法满足结合律 (AB)C=A(BC),A 对。但不满足交换律,AB 一般不等于 BA,B 错。左列数必须等于右行数(内维相等),C 对。结果形状由外维决定,D 对。就像榫卯咬合方向有讲究,正着插和反着插完全不同。
Q10. 广播 (3,1,1) 与 (1,4,2) 相加,结果的形状是?
- A.(3, 4, 2)← 正确答案
- B.(3, 4, 1)
- C.(3, 1, 2)
- D.无法广播运算
答案:A。从右往左对齐三对维度:1 和 2 取 2,1 和 4 取 4,3 和 1 取 3,各维度均兼容(其中一方为 1 即可扩展),结果为 (3,4,2)。这是三维广播的典型例子,类似把“竖条”“横条”“厚片”三块积木拼成完整立体块。