基与对偶 基(basis)定义了向量空间的坐标系,对偶(duality)揭示了线性函数如何作用于向量。本节介绍线性无关、生成集、基变换、对偶空间和余向量——这些概念是 ML 中 PCA、特征变换以及注意力查询背后的原理。 我们已经看到向量生活在一组有若干维数的空间里。可是什么定义了这些维数呢?这正是基向量登场的地方。 基是这样一组向量:通过缩放和相加(线性组合),它们可以构建出空间中的所有其他向量,且没有任何冗余。它们是空间的构建积木。 一个基必须满足两个条件: 线性无关:没有任何一个基向量能由其他基向量构建出来。每一个都贡献了一个真正的新方向。 生成(张成):空间中的每一个向量都能表示为基向量的组合。没有任何遗漏。 一个基中向量的个数等于该空间的维数。
基(basis)定义了向量空间的坐标系,对偶(duality)揭示了线性函数如何作用于向量。本节介绍线性无关、生成集、基变换、对偶空间和余向量——这些概念是 ML 中 PCA、特征变换以及注意力查询背后的原理。
我们已经看到向量生活在一组有若干维数的空间里。可是什么定义了这些维数呢?这正是基向量登场的地方。
基是这样一组向量:通过缩放和相加(线性组合),它们可以构建出空间中的所有其他向量,且没有任何冗余。它们是空间的构建积木。
一个基必须满足两个条件:
线性无关:没有任何一个基向量能由其他基向量构建出来。每一个都贡献了一个真正的新方向。
生成(张成):空间中的每一个向量都能表示为基向量的组合。没有任何遗漏。
一个基中向量的个数等于该空间的维数。在 \mathbb{R}^2 中需要 2 个,在 \mathbb{R}^3 中需要 3 个,依此类推。
最自然的基是标准基,即沿各坐标轴的单位向量:
任何向量都只是这些基向量的加权和。向量 (3, 2) 其实就是 3\hat{\mathbf{i}} + 2\hat{\mathbf{j}}。权重(3 和 2)就是该向量在这个基下的坐标。
但标准基并不是唯一合法的基。在 \mathbb{R}^2 中,向量 (1, 1) 和 (-1, 1) 也构成一个基。它们线性无关且能到达平面上的任何一点。同一个向量在这个新基下只是坐标不同。
基变换(change of basis)就是用不同的基重新表示同一个向量。向量并没有移动,我们只是换了一个视角来描述它。
做法是乘上一个基变换矩阵 P,它的列就是用旧坐标写下的新基向量。要换回去,就乘 P^{-1}。
把它想成两个朋友给同一家咖啡馆指路。你按街道导航:(3, 2) 表示「向东 3 个街区,向北 2 个街区」。你的朋友按对角线导航:他的方向是 \mathbf{b}_1 = (1, 1)(东北)和 \mathbf{b}_2 = (-1, 1)(西北)——也就是前面的新基。同一家咖啡馆,两种描述如何到达的语言。
你的朋友说咖啡馆在 (2.5, -0.5):「沿我的东北对角线走 2.5 步,再沿西北对角线倒退半步」。想知道这在你的街道语言里意味着什么,你只需照着他的配方走:
这就是你叫做 (3, 2) 的那家咖啡馆!
「照配方走」这一步正是乘以 P 所做的事。把朋友的基向量作为列排好,然后乘以他们的坐标向量,就取了第一列的 2.5 倍和第二列的 -0.5 倍——还是那个配方,只是写得紧凑些:
什么都没动。(3, 2) 和 (2.5, -0.5) 是同一个点的两种描述,而 P 是两者之间翻译用的字典。
在 ML 中,基变换经常出现。例如 PCA 就是寻找一个新基(主成分),使得数据更容易理解——坐标轴对齐到方差最大的方向。
接下来的一组概念会比较抽象、目前也不容易掌握,要等到后续章节把它们用起来时才会豁然开朗,所以请做好思想准备,抱歉。
这里其实藏着一个更深的想法。当我们写 \mathbf{v} = (3, 2) 时,坐标 3 和 2 其实是沿着每个基方向「测量」\mathbf{v} 得到的结果。第一个坐标在问「\mathbf{v} 里有多少 \hat{\mathbf{i}}?」,第二个在问「有多少 \hat{\mathbf{j}}?」
在咖啡馆的故事里,这些测量就是朋友的提问:「你沿我的东北对角线走了多远?」「沿西北那条呢?」。每个基都自带一套提问,每个方向一个问题。
每个提问都是一个线性泛函(linear functional):一个接受向量、返回单个数字(读数)的函数。
「线性」一词意味着这个泛函尊重两种向量空间运算。测一组向量之和,得到的是读数之和;把向量翻倍,读数也翻倍:
换句话说,线性泛函是「诚实的尺子」。向量是被测量的对象,线性泛函是测量它们的尺子,而所有可能的尺子的集合构成了对偶空间 V^\ast。
对于每个基 \{\mathbf{e}_1, \mathbf{e}_2, \ldots, \mathbf{e}_n\},都有一组与之匹配的尺子,即对偶基 \{\mathbf{e}_1^\ast, \mathbf{e}_2^\ast, \ldots, \mathbf{e}_n^\ast\},其中 \mathbf{e}_i^\ast 只回答一个问题:「有多少步 \mathbf{e}_i?」。一把校准得当的尺子在自己的基向量上读数为 1,对其余基向量完全无视,这可以用克罗内克 δ(Kronecker delta)\delta_{ij} 紧凑地写成:
\mathbf{e}_i^\ast(\mathbf{e}_j) = \delta_{ij} = \begin{cases} 1 & \text{若 } i = j \\ 0 & \text{若 } i \neq j \end{cases}
这把尺子忽略自己以外的所有方向,只报告坐标。坐标就是对偶基的读数。
那么一把尺子具体长什么样?在 \mathbb{R}^n 中,每个线性泛函就是一行数字,通过点积来作用。而对于朋友的基,我们其实早已不知不觉地造好了这些尺子:把 (3, 2) 翻译成朋友的语言就是乘以 P^{-1},而 P^{-1} 的每一行恰好给出一个坐标。P^{-1} 的各行就是对偶基:
拿咖啡馆一程来验证:\mathbf{b}_1^\ast \cdot (3, 2) = 0.5 \cdot 3 + 0.5 \cdot 2 = 2.5,正好是朋友的第一个坐标。本节的两半其实是同一个想法:基变换换掉了构建积木,而对偶基是与新积木配套、用来读出新坐标的那套尺子。
那条校准规则也藏得并不深:P^{-1}P = I 说的正是尺子 i 在基向量 i 上读 1、在其余上读 0。单位矩阵就是把克罗内克 δ 写成表格的样子。
一个提醒。人们很容易猜测 \mathbf{b}_1 的尺子就是 \mathbf{b}_1 本身,也就是直接和 (1, 1) 做点积。但 (1, 1) \cdot (3, 2) = 5,是真实坐标 2.5 的两倍。用基向量做点积只有在基标准正交(互相垂直且各自长度为 1,如标准基)时才读出自己的坐标。这也正是 \mathbf{e}_1^\ast 看起来与 \mathbf{e}_1 一模一样的唯一原因。对任何其他基,尺子都活在 P^{-1} 的各行里。
这也解释了点积的「双重身份」。每个向量 \mathbf{u} 都暗中定义了一把尺子(通过计算 \mathbf{u} \cdot \mathbf{v} 来测量 \mathbf{v}),而每把尺子也都是与某个向量的点积。在有限维中,对偶空间本质上是原空间的一面镜像。
对偶现在看来可能很抽象,但它支撑着许多实用的想法:坐标就是对偶基的求值,点积是一种对偶配对,而神经网络中像注意力这样的变换之所以能运作,正是因为一组向量去「查询」另一组向量——这就是对偶在发挥作用。
import jax.numpy as jnp v = jnp.array([3.0, 2.0]) # 标准基:坐标就是各分量 print(f"Standard basis coords: {v}") # 新基:(1,1) 和 (-1,1) P = jnp.array([[1.0, -1.0], [1.0, 1.0]]) new_coords = jnp.linalg.solve(P, v) print(f"New basis coords: {new_coords}") # 验证:从新坐标重建 reconstructed = new_coords[0] * P[:, 0] + new_coords[1] * P[:, 1] print(f"Reconstructed: {reconstructed}")
import jax.numpy as jnp v = jnp.array([3.0, 2.0]) # 朋友的基 (1,1) 和 (-1,1),按列排列 P = jnp.array([[1.0, -1.0], [1.0, 1.0]]) # 对偶基:P^{-1} 的各行 P_inv = jnp.linalg.inv(P) b1_star, b2_star = P_inv[0], P_inv[1] print(f"b1* = {b1_star}, b2* = {b2_star}") # 每把尺子在朋友的基下读出一个坐标(与练习 1 吻合!) print(f"b1*(v) = {jnp.dot(b1_star, v)}") # 2.5 print(f"b2*(v) = {jnp.dot(b2_star, v)}") # -0.5 # 校准:在自己的基向量上读 1,在另一个上读 0 print(f"b1*(b1) = {jnp.dot(b1_star, P[:, 0])}, b1*(b2) = {jnp.dot(b1_star, P[:, 1])}") # 陷阱:用 b1 本身做点积并不给出坐标 print(f"b1 . v = {jnp.dot(P[:, 0], v)} (5.0,不是 2.5——b1 并非标准正交)") # 标准基是标准正交的,所以那里捷径有效 e1 = jnp.array([1.0, 0.0]) print(f"e1 . v = {jnp.dot(e1, v)} (3.0,第一个标准坐标)")