AI 数学课程 · 第 24 课,共 180

行阶梯形揭示冗余 AI 特征

使用主元、自由变量和行阶梯形,诊断小型设计矩阵中的冗余特征与非唯一模型参数。

分享这篇文章

当模型有三列特征时,人们很容易假设它有三个可以学习的独立方向。但一个特征虽然存在于数据中,却可能没有增加任何新信息。如果某一列是其他列的组合,模型就可能用许多不同的参数向量拟合同样的预测。

行阶梯形能让这种隐藏结构显现出来。学完本课后,你将能够定位主元、识别自由变量、描述通解,并使用一个小型 NumPy 实现诊断冗余特征,而不会把数值警告与数学结论混为一谈。

**学习成果:**找出主元、自由变量和通解。

第 23 课:高斯消元保持解集不变介绍了保持方程组解不变的合法行运算。第 18 课:矩阵组织线性规则介绍了系数矩阵。官方 Mathematics for Machine Learning 配套网站 将本课放在当前书籍 PDF的第 2.3 节。本课中的数字、特征场景、代码、可视化和评估题均为原创。

阶梯记录方程组能够确定什么

从一个矩阵 AinmathbbRmimesnAinmathbb{R}^{m imes n} 开始。当一个矩阵满足以下三个结构规则时,它处于行阶梯形:

  1. 每个非零行都位于所有全零行的上方。
  2. 每个非零行的第一个非零项称为主元,它比上一行的主元更靠右。
  3. 每个主元下方的所有项都为零。

主元形成一条从左向右的阶梯。主元列是行所约束的方向。没有主元的列是一个自由方向:它的值可以变化,而方程组会据此确定其他变量。

行阶梯形不等同于简化行阶梯形。简化形式还要求每个主元等于 11,并且在其所在列中是唯一的非零项。两种形式都能揭示秩和自由变量;简化形式通常更便于读取参数化答案。

下面的可视化保留了中间状态。最终矩阵有两个主元,位于列 f1f_1 和 f2f_2;列 f3f_3 没有主元。

静态行化简记录

主元形成阶梯

每个状态都保留完整矩阵。最后一个非零行的主元比上一行的主元更靠右,而零行已经移到了底部。

  1. 步骤 0原始特征矩阵开始
    原始特征矩阵
    行f₁f₂f₃
    R1123
    R2246
    R3112
  2. 步骤 1清除主元下方的第一列R₂ ← R₂ − 2R₁;R₃ ← R₃ − R₁
    清除主元下方的第一列
    行f₁f₂f₃
    R1123
    R2000
    R30-1-1
  3. 步骤 2将非零行上移R₂ ↔ R₃
    将非零行上移
    行f₁f₂f₃
    R1123
    R20-1-1
    R3000
  4. 步骤 3规范化第二个主元R₂ ← −R₂
    规范化第二个主元
    行f₁f₂f₃
    R1123
    R2011
    R3000

高亮条目是最终结构中的主元。列 f₃ 没有主元,因此它是揭示冗余特征的自由方向。

行运算揭示了两个独立的特征方向和一个冗余列。带下划线的条目标记了最终的主元位置。

例题:一个冗余的特征列

设想一个很小的设计矩阵,它总结了三个样本中提取的三种信号:

  • f1f_1:检索匹配强度;
  • f2f_2:验证强度;
  • f3f_3:一个意外被记录为独立特征的组合信号。

矩阵为

A=[123246112].A= \begin{bmatrix} 1&2&3\\ 2&4&6\\ 1&1&2 \end{bmatrix}.

在开始化简之前,先注意第三列是前两列之和:f3=f1+f2f_3=f_1+f_2。这是一个有用的直觉,但即使某种关系无法通过观察直接看出,行化简仍然提供了有效的步骤。

首先使用左上角的项作为主元。消去它下方的项:

R2←R2−2R1,R3←R3−R1,R_2\leftarrow R_2-2R_1, \qquad R_3\leftarrow R_3-R_1,

得到

[1230000−1−1].\begin{bmatrix} 1&2&3\\ 0&0&0\\ 0&-1&-1 \end{bmatrix}.

第二行暂时变成了零行,但系统并没有丢失信息:下面的非零行仍然需要上移。交换两行,然后将新的第二行乘以 −1-1:

[1230−1−1000]⟶[123011000].\begin{bmatrix} 1&2&3\\ 0&-1&-1\\ 0&0&0 \end{bmatrix} \quad\longrightarrow\quad \boxed{ \begin{bmatrix} 1&2&3\\ 0&1&1\\ 0&0&0 \end{bmatrix}}.

主元位于第 1 列和第 2 列。第 3 列没有主元,因此它是自由列。矩阵的秩是 2 而不是 3:数据包含三个命名特征,但只有两个独立方向。

自由变量描述的是一个族,而不是失败

为了理解缺失的主元意味着什么,为方程组附加一个与相同行关系相容的右端:

[123∣4246∣8112∣3].\begin{bmatrix} 1&2&3&\mid&4\\ 2&4&6&\mid&8\\ 1&1&2&\mid&3 \end{bmatrix}.

相同的运算得到

[101∣2011∣1000∣0].\begin{bmatrix} 1&0&1&\mid&2\\ 0&1&1&\mid&1\\ 0&0&0&\mid&0 \end{bmatrix}.

现在方程为

θ1+θ3=2,θ2+θ3=1.\theta_1+\theta_3=2, \qquad \theta_2+\theta_3=1.

令自由变量为 θ3=t\theta_3=t。那么

θ1=2−t,θ2=1−t,θ3=t.\theta_1=2-t, \qquad \theta_2=1-t, \qquad \theta_3=t.

tt 的每个取值都会产生一个给出相同右端的参数向量。例如,t=0t=0 得到 (2,1,0)(2,1,0),而 t=1t=1 得到 (1,0,1)(1,0,1)。两者都满足原来的三个方程。模型的预测是固定的,但仅凭这些观测无法识别各个权重。

零行不同于矛盾

例子中的最后一行变为

0θ1+0θ2+0θ3=0.0\theta_1+0\theta_2+0\theta_3=0.

这一行是冗余的,因此没有贡献新的约束。不要把它与

0θ1+0θ2+0θ3=50\theta_1+0\theta_2+0\theta_3=5

混淆。在增广矩阵中,左侧为零而右端非零的行表示不相容,因此没有解。同一个左侧矩阵,根据变换后的右端不同,可能支持无穷多个解、一个解或没有解。

这个区别在数据管道中很重要。重复行或相关行可能减少独立证据的数量,却不会使方程互相矛盾。相反,矛盾行说明观测无法被所提出的线性模型精确解释。

在 NumPy 中实现主元搜索

下面的紧凑实现使用部分主元选取。在每一列中,它会从当前主元行下方选择绝对值最大的候选项,将其交换到位,规范化主元,然后清除其下方的项。容差可以防止仅仅由舍入噪声造成的数值被当作真实主元。

import numpy as np

def row_echelon(matrix, tolerance=1e-12):
    a = np.array(matrix, dtype=float, copy=True)
    rows, columns = a.shape
    pivot_row = 0
    pivot_columns = []

    for column in range(columns):
        if pivot_row == rows:
            break

        candidate = pivot_row + np.argmax(np.abs(a[pivot_row:, column]))
        if abs(a[candidate, column]) <= tolerance:
            continue

        a[[pivot_row, candidate]] = a[[candidate, pivot_row]]
        a[pivot_row] /= a[pivot_row, column]

        for row in range(pivot_row + 1, rows):
            a[row] -= a[row, column] * a[pivot_row]

        a[np.abs(a) <= tolerance] = 0
        pivot_columns.append(column)
        pivot_row += 1

    return a, pivot_columns


features = np.array([
    [1, 2, 3],
    [2, 4, 6],
    [1, 1, 2],
])

echelon, pivots = row_echelon(features)
print(echelon)
print(pivots)
print(np.linalg.matrix_rank(features))

有意义的输出大致为

[[1. 2. 3.]
 [0. 1. 1.]
 [0. 0. 0.]]
[0, 1]
2

确切的小数取决于数据类型和容差,但这里的结构性结论是稳定的:两个主元列和一个自由列。NumPy 的 matrix_rank 同样会使用围绕奇异值的容差,而不是询问浮点数值是否严格等于零。因此,秩的判断应当连同尺度和容差一起报告,而不应被表述为关于含噪测量的永恒真理。

这对 AI 模型意味着什么

假设线性模型计算

y^=θ1f1+θ2f2+θ3f3.\hat{y}=\theta_1f_1+\theta_2f_2+\theta_3f_3.

当 f3=f1+f2f_3=f_1+f_2 时,预测变为

y^=(θ1+θ3)f1+(θ2+θ3)f2.\hat{y}=(\theta_1+\theta_3)f_1+(\theta_2+\theta_3)f_2.

在这个设计中,只有 θ1+θ3\theta_1+\theta_3 和 θ2+θ3\theta_2+\theta_3 的和是可观测的。因此,不同的参数向量可以在矩阵的每一行上产生完全相同的预测。训练过程仍然可能返回一组权重,因为优化器、初始化、正则化项或数值路径会从这个参数族中选出一个点。这个被选中的点并不能证明数据识别出了每个特征的独立贡献。

把行阶梯形用作诊断工具,而不是验证的替代品。小样本上的秩计算可能会随着预处理、缩放、缺失值处理或新数据的变化而改变。对于更大或含噪的矩阵,还要检查奇异值的尺度和条件数。一个满秩矩阵仍然可能接近相关,从而使其解对微小的测量变化很敏感。

condition = np.linalg.cond(features)
residual = np.linalg.norm(features[:, 2] - features[:, 0] - features[:, 1])
print(condition, residual)

这里,存储值中的残差恰好为零,而浮点运算中的条件数极大(本次运行约为 3.8×10163.8\times10^{16})。秩严格不足的矩阵在数学上的条件数是无穷大,但浮点 SVD 可能报告一个很大的有限值。这种对比本身就是警告。在生产数据中,应使用与尺度相关的容差:很小的残差可能表示真正的近似依赖,也可能只是低于应用所关心精度的测量。

检查你的理解

问题

显示分步解答

行阶梯形要求每个非零行都位于所有全零行的上方,每个后续主元都向右移动,并且每个主元下方的项都为零。即使主元项不等于 11、主元上方的项不为零,矩阵仍然可以满足这些规则。那些额外条件属于简化行阶梯形。

问题

显示分步解答

使用第一行作为主元,并将第二行替换为 R2−2R1R_2-2R_1:

[1224]⟶[1200].\begin{bmatrix}1&2\\2&4\end{bmatrix} \longrightarrow \begin{bmatrix}1&2\\0&0\end{bmatrix}.

这里只有一个主元,因此秩为 11。第二行没有提供独立约束;它只是第一行放大两倍后的重复。

问题

显示分步解答

第三个变量是自由变量,因为最终的阶梯矩阵在第 1 列和第 2 列有主元,但第 3 列没有。令 θ3=t\theta_3=t 后,前两个方程就能用 tt 表示 θ1\theta_1 和 θ2\theta_2。

问题

显示分步解答

这一行表示 0=50=5 或另一个不可能的陈述。未知数无论取什么值,都无法让左侧从零变成非零,因此方程组不相容。0=00=0 则不同:它是冗余的,并且不会改变已有的解集。

问题

显示分步解答

当 t=1t=1 时,向量为 (1,0,1)(1,0,1)。三行分别得到 1+2(0)+3(1)=41+2(0)+3(1)=4、2(1)+4(0)+6(1)=82(1)+4(0)+6(1)=8 和 1+0+2(1)=31+0+2(1)=3。这些正好是所选的右端值,因此参数化解确实描述了有效解,而不只是化简后方程的解。

问题

显示分步解答

数据允许存在一族预测效果相同的参数向量。初始化、优化器的路径、正则化或隐含的数值偏好都可能从这个族中选出一个成员。返回的向量是完整训练过程作出的选择;它并不表示观测唯一识别了每个特征权重。

问题

显示分步解答

检查奇异值尺度、条件数、数据单位、测量不确定性以及秩判断使用的容差。即使数学秩为满秩,一个很小的奇异值也会使解对微小变化非常敏感。与其相信许多不稳定的有效数字,不如考虑重新缩放、收集更多独立观测、删除冗余特征或使用正则化形式。

下一课将使用行结构解释为什么数值软件通常直接求解方程组,而不是形成显式逆矩阵。主元不只是纸上的标记:它告诉你数据约束了哪些方向,以及哪些方向仍然含糊不清。

资料来源

  1. Mathematics for Machine Learning companion website
  2. Mathematics for Machine Learning book PDF
  3. NumPy documentation: numpy.linalg.matrix_rank
  4. NumPy documentation: numpy.linalg.solve
  5. NumPy documentation: numpy.linalg.cond
  6. NumPy documentation: numpy.linalg.norm