你给猫拍了张照片,在手机相册里搜”猫”,很多时候就能把它找出来。可电脑并看不见猫:它唯一会做的,就是飞快地加数和乘数。

这一课的傻问题是:电脑怎样把照片变成数字? 顺便再问一句:变成数字以后,它拿这些数做什么?答案就是三个听起来像考试题的概念:向量、点积和矩阵;再加上一道算术:乘一乘,再加起来。

选一张牌:♣ 从零开始,♦ 不怕简单的公式,♠ 想看符号。随时可以换。

讲解难度

照片就是一列数

讲解难度

新手

数码照片就像一幅马赛克:它由一个个彩色小方块拼成,这些小方块叫像素,小到肉眼看不出来。电脑打开照片时,会把每个小方块存成三个数:红色有多少、绿色有多少、蓝色有多少,每个数都在 0 到 255 之间。纯红是 (255, 0, 0);白色是 (255, 255, 255);黑色是 (0, 0, 0)。

一张宽 4000、高 3000 个小方块的照片有 1200 万个像素。每个像素三个数,加起来就是排成一行的 3600 万个数。

排成一行的一列数叫向量,一共有多少个数,就是它的维数。在电脑眼里,你的照片就是一个 3600 万维的向量。它对照片做的一切,都是拿这列数做算术。

进阶

向量是一列有顺序的数,其中每个数叫它的分量。只有两个分量时,可以画成从中心出发的一支箭头:v = (3, 2) 就是”向右 3,向上 2”。它的长度用勾股定理算:√(3² + 2²) = √13 ≈ 3.6(OpenStax)。

一张只有两个像素的灰度照片,就是平面上这样一支箭头;三个像素,就是空间里的一支箭头。再多就画不出来了,但算法一模一样:长度还是各分量平方和的平方根。维数就是向量里有多少个数。

彩色照片里,每个像素有三个 0 到 255 之间的数,每个通道(红、绿、蓝)一个,因为每个数占 8 位(Pillow,一个 Python 图像库)。所以打开的照片是一张 高 × 宽 × 3 的数表。JPEG 文件之所以小,是因为压缩过,但打开时会解压成这张表。要交给神经网络,就把它全部排成一行:4000 × 3000 × 3 = 3600 万个数。

技术

♦ → ♠ 从进阶到技术
♦ 里的说法♠ 里的写法
向量x ∈ ℝⁿ,由 n 个实数组成
分量x = (x₁, …, xₙ)
长度欧几里得范数 ‖x‖ = √(Σᵢ xᵢ²)
维数dim ℝⁿ = n:一组基 e₁, …, eₙ 中向量的个数
通道C;RGB 中 C = 3,取值为 {0, …, 255} 的 8 位整数
高 × 宽 × 3张量 H × W × C(HWC);torchvision 用 C × H × W
把它全部排成一行展平(flatten):x ∈ ℝⁿ,n = H·W·C

8 位 RGB 图像是一个取值于 {0, …, 255}^(H×W×C) 的整数张量,C = 3:Pillow 13.0 把这种模式描述为 «RGB (3x8-bit pixels, true color)»,并说明 «an 8-bit pixel has a range of 0-255»。torchvision 0.29 用 ToTensor 把它从 [0, 255] 区间的 (H × W × C) 转成 [0.0, 1.0] 区间、形状为 (C × H × W) 的浮点张量。展平后,它是 ℝⁿ 中的一个向量,n = H·W·C;对 4000 × 3000 的照片,n = 3.6×10⁷。

一个空间的维数,是它任意一组基中向量的个数(Austin,Understanding Linear Algebra,定义 3.5.6)。标准基 e₁, …, eₙ 有 n 个向量,所以 dim ℝⁿ = n,也就是分量的个数。范数 ‖x‖ = √(Σᵢ xᵢ²) 就是 n 维的勾股定理。

点积:乘一乘,再加起来

讲解难度

新手

一个水果摊一天卖了 30 个苹果、12 根香蕉和 18 个橙子,单价分别是 0.50 元、0.25 元和 1 元。一共卖了多少钱?把每样的数量乘以单价,再加起来:15 + 3 + 18 = 36 元。两列数这样”逐个相乘再相加”,就是点积,也是电脑处理照片时做得最多的一道算术。这个例子来自一本数学教材,OpenStax。

用箭头来看,点积说的是另一件事:两支箭头有多指向同一侧。斜着推一辆购物车,只有朝车子前进方向的那部分力才算数。两支箭头指向同一侧,点积为正;互成直角,为零;方向相反,为负。

进阶

a = (a₁, a₂) 和 b = (b₁, b₂) 的点积是 a·b = a₁b₁ + a₂b₂:把分量逐个相乘,再加起来(OpenStax)。分量更多也一样。a = (3, 1),b = (1, 2):3·1 + 1·2 = 5。两张各有 3600 万个数的照片,就是 3600 万次乘法。

用箭头来看还有另一种读法:a·b = a 的长度 × b 的长度 × 夹角的余弦(OpenStax)。同向时余弦为 1,成直角时为 0,相反时为 −1。所以两支垂直的箭头,点积为 0。

把点积除以两个长度,就只剩下余弦,它衡量两个方向有多像:这就是余弦相似度。拿照片来说有个妙处。把照片里的每个数都乘以 1.2,照片看起来更亮:箭头变长了,但没有转动,它和原图的余弦相似度仍然是 1(自行计算,前提是没有哪个数超过 255)。

技术

♦ → ♠ 从进阶到技术
♦ 里的说法♠ 里的写法
点积a·b = Σᵢ aᵢbᵢ(也写作 ⟨a, b⟩ 或 aᵀb)
3600 万次乘法代价 O(n):n 次乘法、n − 1 次加法
a 的长度 × b 的长度 × 夹角的余弦a·b = ‖a‖‖b‖cos θ,θ ∈ [0, π]
同向时余弦为 1,成直角时为 0,相反时为 −1−1 ≤ cos θ ≤ 1,即 |a·b| ≤ ‖a‖‖b‖(柯西–施瓦茨不等式)
垂直正交:a·b = 0(a ⊥ b)
余弦相似度cos θ = a·b / (‖a‖‖b‖)
箭头变长了,但没有转动对任意 λ > 0,cos(x, λx) = 1

在 ℝⁿ 中,a·b = Σᵢ aᵢbᵢ。OpenStax(Calculus Volume 3,2.3)用余弦定理证明了 a·b = ‖a‖‖b‖cos θ(定理 2.4),以及两个非零向量正交当且仅当 a·b = 0(定理 2.5)。超过三维时反过来做:用 cos θ = a·b / (‖a‖‖b‖) 来定义夹角,而柯西–施瓦茨不等式 |a·b| ≤ ‖a‖‖b‖ 保证这个比值落在 [−1, 1] 内。

PyTorch 2.14 用 torch.nn.functional.cosine_similarity 计算余弦相似度,分母取 max(‖x₁‖‖x₂‖, ε),以免除以零。因为 λ > 0 时 cos(x, λx) = 1,如果另一张照片只是把数值整体放大或缩小,变亮或变暗,余弦相似度就分辨不出来。代价是线性的:n 次乘法、n − 1 次加法。

乘一乘,加一加,就这么多。靠这个我能比价格、比箭头、比照片,还剩时间睡个午觉。

动手试试:两支箭头

拖动箭头的尖端,或者用滑块、键盘方向键来移动。找一个让点积等于 0 的位置:这时两支箭头互成直角。蓝色宽带是 b 在 a 上的影子:两支箭头越同向,影子越长。

两支箭头和它们的点积一张网格,两支箭头从中心出发:红色的 a 和蓝色的 b。一条虚线标出 b 在 a 上的影子。初始时 a = (4, 1),b = (1, 3),点积是 7。
4
1
1
3
点积 a·b
4·1 + 1·3 = 7
长度
‖a‖ = 4.12 · ‖b‖ = 3.16
夹角
57.5°
余弦(方向有多像)
0.54

a = (4, 1),b = (1, 3),a·b = 7。指向同一侧:点积为正。

矩阵会挪动平面

讲解难度

新手

现在水果摊老板要比较两家店,每家价格不同。他把价格写成一张表:每家店一行,每种水果一列。这张表就是矩阵。想知道在每家店要花多少钱,就把购物清单和每一行逐个相乘再相加:每家店一个点积,一次全算出来。

矩阵还能挪动图画。想象一张画在橡皮膜上的网格:矩阵决定每个点去哪里,就像把橡皮膜拉长或推斜,但有两条规矩:直线还是直线,中心不动。

旋转或拉伸照片的程序,就是做这类计算来决定每个像素该去哪里。有些矩阵会把整幅图压成一条线:压扁以后,就再也还原不回来了。

进阶

矩阵是一张长方形的数表;有 m 行 n 列的矩阵叫 m × n 矩阵(OpenStax)。矩阵乘向量,就是用矩阵的每一行和这个向量做点积。A = (2 1 / 0 1),x = (1, 2):第一行得 2·1 + 1·2 = 4,第二行得 0·1 + 1·2 = 2,所以 Ax = (4, 2)。

读矩阵有个窍门:第一列是 (1, 0) 去的地方,第二列是 (0, 1) 去的地方(Austin)。在这个例子里,(1, 0) 变成 (2, 0),(0, 1) 变成 (1, 1):矩阵把图形横向拉长,又推斜了。知道了这两点,就知道一切点去哪里,因为任何一个点都是这两者的组合。

边长为 1 的小方块变成一个平行四边形。矩阵为 (a b / c d) 时,它的面积是 ad − bc:在例子里是 2·1 − 1·0 = 2,变成两倍(Austin)。如果等于 0,矩阵就把整个平面压扁成一条直线:少了一个维度,而且回不去了。(1 −1 / 2 −2) 就是这样。如果是负数,图形会像照镜子一样翻过来。

Pillow 就是这样旋转或推斜照片的:对每个新像素,它用一个矩阵算出它来自原图的哪个点(再加上一个平移)。

技术

♦ → ♠ 从进阶到技术
♦ 里的说法♠ 里的写法
矩阵A ∈ ℝ^(m×n),元素 aᵢⱼ(第 i 行第 j 列)
每一行(Ax)ᵢ = Σⱼ aᵢⱼxⱼ = (A 的第 i 行)·x
第一列是 (1, 0) 去的地方,第二列是 (0, 1) 去的地方A = [Ae₁ Ae₂]:第 j 列是 T(eⱼ)
任何一个点都是这两者的组合线性:A(c₁e₁ + c₂e₂) = c₁Ae₁ + c₂Ae₂
推斜剪切(错切),例如 (1 1 / 0 1)
ad − bcdet A,列向量所张平行四边形的有向面积
压扁det A = 0:A 不可逆;秩下降,像是一条直线
像照镜子一样det A < 0:定向反转
旋转旋转矩阵 R(θ) = (cos θ −sin θ / sin θ cos θ)
它来自原图的哪个点用 A⁻¹:遍历输出,到输入里去找
(再加上一个平移)仿射变换 x ↦ Ax + t

一个 m × n 矩阵 A 定义了从 ℝⁿ 到 ℝᵐ 的映射 T(x) = Ax,其中 (Ax)ᵢ = Σⱼ aᵢⱼxⱼ。它的列是基向量的像:A = [T(e₁) ⋯ T(eₙ)](Austin,命题 2.5.8)。T 是线性的,所以 T(Σⱼ cⱼeⱼ) = Σⱼ cⱼT(eⱼ),知道了列,就知道了一切。

在 2×2 的情形,转角为 θ 的旋转是 (cos θ −sin θ / sin θ cos θ),(1 1 / 0 1) 是一个剪切(Austin,2.6)。det A = ad − bc 是列向量所张平行四边形的有向面积(3.4),A 可逆当且仅当 det A ≠ 0(命题 3.4.12)。det A = 0 时秩下降:像空间 Col(A) 的维数等于 rank A(命题 3.5.12),对 (1 −1 / 2 −2) 来说是一条直线。

Pillow 13.0(ImageTransform.AffineTransform)接收 «the first two rows from the inverse of an affine transform matrix»:对每个输出像素 (x, y),到输入的 (ax + by + c, dx + ey + f) 处取值。它用逆矩阵,是因为它遍历的是输出照片;平移项 (c, f) 让这个变换成为仿射变换,而不是线性变换。

动手试试:让网格变形

调一调矩阵的四个数,或者试试下面的按钮。红色箭头是第一列,即 (1, 0) 去的地方;蓝色箭头是第二列。找一个能把网格压成一条直线的矩阵:面积会变成 0。再看看镜像时字母 F 会怎样。

让网格变形的矩阵一张灰色网格,上面叠着同一张网格乘上矩阵之后的样子。红色箭头标出 (1, 0) 去了哪里,蓝色箭头标出 (0, 1) 去了哪里。阴影小方块显示面积变了多少,字母 F 显示图形有没有翻面。初始矩阵是 [[1, 1], [0, 1]]:它把网格推斜,但面积不变。
试一个
1
1
0
1
(1, 0) 变成
(1, 0)
(0, 1) 变成
(1, 1)
面积乘以 (ad − bc)
1
剩下的维数
2:平面还是平面

矩阵 (1 1 / 0 1):面积乘以 1,剩下 2 维。

矩阵乘法:顺序很重要

讲解难度

新手

穿衣服要先穿袜子再穿鞋,反过来就不是一回事了。矩阵也一样:先做一个变换再做另一个,相当于一个矩阵,这个矩阵是把两个矩阵相乘得到的,而且顺序很重要。先旋转照片再横向拉伸,和先拉伸再旋转,结果不一样。

开头那个问题的答案就在这里。一个认猫的神经网络,把照片当成一个巨大的向量接收进来,再一个接一个地乘上矩阵,中间还夹着几个别的步骤。这些矩阵的每一行都像一张价目表:它和照片的点积得出一个数。每张照片都要做上百万次这样的乘加运算。

所以显卡特别适合人工智能:它就是为了同时做大量乘法而设计的。

进阶

两个矩阵相乘时,结果里的每个数,都是第一个矩阵的某一行和第二个矩阵的某一列做点积(OpenStax)。只有当第一个矩阵的列数等于第二个矩阵的行数时才能相乘:m × k 矩阵乘 k × n 矩阵,得到 m × n 矩阵。

对一个点先做 B 再做 A,等于一次做矩阵 AB。注意:写法和做的顺序是反的(Austin)。而且 AB 一般不等于 BA(OpenStax)。设 R 为转 90°,S 为横向拉伸到两倍:先转再拉是 S·R = (0 −2 / 1 0),先拉再转是 R·S = (0 −1 / 2 0)。

神经网络的一层会把输入乘上一个权重矩阵(PyTorch)。一张 100 × 100 的灰度照片有 10000 个数;一个有 1000 个输出的层用的是 1000 × 10000 的矩阵,每张照片要做 1000 万次乘法(自行计算)。层与层之间还有一个不是乘法的步骤;《逻辑回归还是神经网络?》那一课讲了它是什么,以及网络靠它多得到了什么。

显卡会一次放进很多张照片,每张一列,于是一切就变成一次巨大的矩阵乘法(NVIDIA)。

技术

♦ → ♠ 从进阶到技术
♦ 里的说法♠ 里的写法
两个矩阵相乘(AB)ᵢⱼ = Σₖ aᵢₖbₖⱼ
第一个矩阵的某一行和第二个矩阵的某一列(A 的第 i 行)·(B 的第 j 列)
m × k 矩阵乘 k × n 矩阵,得到 m × n 矩阵(m×k)(k×n) → m×n:内侧维数相同
AB复合:A(Bx) = (AB)x;若 T ↦ A,S ↦ B,则 S∘T ↦ BA
AB 一般不等于 BA不满足交换律;满足结合律:(AB)C = A(BC)
层线性层(nn.Linear):y = xAᵀ + b
权重A ∈ ℝ^(out×in),通过学习得到;b 为偏置
1000 万次乘法out·in = 10³·10⁴ = 10⁷ 次乘加(FMA),每张照片
一个不是乘法的步骤非线性激活函数 σ;没有它,A₂(A₁x) = (A₂A₁)x 只相当于一层
一次放进很多张照片批量(batch)N:一次 (M×K)(K×N) 的 GEMM,2·M·N·K FLOP

(AB)ᵢⱼ = Σₖ aᵢₖbₖⱼ,要求内侧维数相同(OpenStax,College Algebra 2e,7.5)。矩阵乘法满足结合律和分配律,但不满足交换律。若 T(x) = Ax,S(x) = Bx,则 S∘T 的矩阵是 BA(Austin,命题 2.5.11):先做的那个写在右边。

m·n 个元素中的每一个,都是 k 项的点积:共 m·n·k 次乘加(FMA),即 2·m·n·k FLOP。NVIDIA 的 GEMM 指南也这样算,每个元素都是 «a dot-product of K-element vectors»。对 1000 × 10000 的层、一批 64 张照片:2 · 1000 · 64 · 10000 = 1.28×10⁹ FLOP(自行计算)。

PyTorch 2.14 把线性层写成 y = xAᵀ + b,每个样本占一行,权重 A 的形状是 (out_features, in_features)。因为有偏置 b,它是仿射的,不是线性的。NVIDIA 把权重放在 A 里、激活值放在 B 里,每个样本占一列:只是转置了一下,算的是同一件事。层与层之间如果没有非线性的 σ,由于结合律,连着的几层就会合成一层。

我看不见猫,只看见 3600 万个数。难的是知道该拿哪些矩阵去乘它们。

最后挑战

一张 100 × 100 像素的彩色照片。电脑打开它时要存多少个数?
两支箭头互成直角。它们的点积是多少?
把照片转 90°,再横向拉伸。这和先拉伸、再旋转一样吗?

三句话总结

  1. 在电脑眼里,照片是一列数(每个像素三个,取值 0 到 255):一个向量,它的维数就是数的个数。
  2. 点积把两列数逐项相乘再相加:既能算出总价,就像水果摊的账;也能说明两支箭头有多同向(垂直时为 0)。
  3. 矩阵是一张能挪动平面上每个点的数表,矩阵乘法把这些变换串起来,而且顺序要紧:这正是神经网络对每张照片做上百万次的运算。

参考资料

**这一课是怎么做的:**在 Claude 的帮助下,根据上面的资料调研和撰写,资料查阅于 2026 年 10 月 17 日。没有单独链接的数字,是根据定义算出来的。例子(4000 × 3000 的照片、两家店、有 1000 个输出的层)都是编的。插图都是原创的。