qinghuan
← 回到知识探索

AI 直觉 / 动手实验

PCA 保留了 99% 的方差,为什么分类线索还是全丢了?

四个点就能区分“保留变化量”与“保留任务信息”;核对投影碰撞、重建误差,以及输入尺度改变后的主方向。

展开本文目录
  1. 下载完整练习包
  2. 先动手看看
  3. 四个点:左右差得很远,上下决定类别
  4. PCA 找的是变化最大的方向
  5. 两个相反标签挤到一起,之后再复杂也分不开
  6. 只留小方差方向,反而保住了这次任务
  7. 改计量尺度,主方向也可能换;并列时更要说清楚
  8. 完整程序:用枚举核对分类上限
  9. 把百分比读回它真正衡量的对象
  10. 记住这三点
  11. 别把例子用过头
  12. 自己试一小步
  13. 查原始资料
开始前,会这些就够了
遇到不熟悉的词,先看这 2 个解释
解释方差比例
保留方向上的变化量,占全部方向变化量的比例。
投影碰撞
原本不同的点,降到低维后得到完全相同的坐标。
打开完整术语小抄 ↗

一组数据降到一维后,报告说保留了 99% 以上的方差。数字很高,却不能直接读成“保留了 99% 的分类能力”。PCA 选择方向时,默认并不知道你希望区分哪种标签。

本篇只用一个长方形的四个角点。把每次投影前后的坐标写全,就能看见哪些不同标签被挤到同一个位置,也能算出任何只看投影坐标的分类规则在这四点上的上限。

把例子带到自己的电脑上

下载完整练习包

需要 Python 3.10 或更新版本,无第三方依赖;实测 Windows / Python 3.14.0,Python 3.10 仅做语法兼容检查。使用标准库,不需要安装第三方包。下载 ZIP 后选择“全部提取”,进入能看到 README.md 的文件夹。

下载 ZIP · 5 个文件
包里有什么
  • demo.py:完整可运行源码
  • README.md:运行步骤与练习
  • observed.stdout.txt:本次实际输出
  • verification.json:源码和运行验证记录
  • SHA256SUMS.txt:文件摘要

在这个文件夹打开 PowerShell,逐行运行

python --version
python -B demo.py

解压后,在含 demo.py 的目录打开终端。命令分别回车执行。对称四角点的精确轴向 PCA、分类碰撞枚举与角度网格;不是通用 PCA 实现,没有训练分类器或外部评测。

找不到命令或文件时,先按 README 检查环境和所在目录。文中标出的阅读时间只估算文章篇幅,安装环境和动手练习可按自己的节奏进行。

保留下来的变化量大,标签还能分开吗?

正在准备实验控件…

先使用默认 PCA,再手工切到“只留纵坐标”。比较解释方差比例与四点分类上限,它们衡量不同的目标。

xy(-10, -1) · 类 0(-10, 1) · 类 1(10, -1) · 类 0(10, 1) · 类 1
实际保留方向
只留横坐标
解释方差比例
99.0099%
平均平方重建误差
1.00
这四点的分类上限
50%

横向方差 100.00,纵向方差 1.00,交叉项为 0。每个投影位置都含两个相反标签,只看投影坐标最多答对一半。

对照相同投影坐标是否含有不同标签
原始坐标标签投影坐标
(-10, -1)0(-10)
(-10, 1)1(-10)
(10, -1)0(10)
(10, 1)1(10)

固定四个等权点 (±a,±1),上方类别 1、下方类别 0;已中心化,没有自动标准化。协方差除以 N=4;此对角矩阵可直接选坐标轴,并列固定选横轴。这里的分类上限来自有限坐标碰撞,不是训练准确率或未来表现。

四个点:左右差得很远,上下决定类别

数据依次为 (−10,−1)、(−10,1)、(10,−1)、(10,1)。上方两点标成类别 1,下方两点标成类别 0,四点等权。完整坐标中,只检查纵坐标是否为正就能区分类别。

两列均值都为零。本文采用除以 N 的总体方差约定,横向方差为 100,纵向方差为 1,交叉项平均为零,协方差矩阵是 diag(100,1)。常见样本协方差使用 N−1,会把两个特征值一起放大,不改变本例方向和解释方差比例。

PCA 找的是变化最大的方向

对单位方向 u=(cosθ,sinθ),投影方差为 100cos²θ+sin²θ。因此沿横轴时达到最大值 100。只保留第一主成分,就只留下横向坐标。这个选择不需要查看类别标签。

解释方差比例是 100/(100+1),约 0.990099。它量的是沿所选方向保留的方差占总方差多少。这里没有一个“总分类信息量=101”的定义,也没有把这个比例与准确率相乘的依据。

对于本篇正交线性投影,回到二维时把丢弃的纵坐标填为零,每个点的平方重建误差为 1。平均重建误差很小,正好符合 PCA 所优化的几何目标;被丢掉的细小纵向变化却恰好决定标签。

两个相反标签挤到一起,之后再复杂也分不开

投影后,左边的两个点都变成 −10,右边的两个点都变成 10。每一个相同坐标上都有一个类别 0 和一个类别 1。任何只输入这一维坐标的确定性分类器,在每组最多判断对一个,因此这四个等权点上最多正确 2/4。

这个 50% 是对指定四点的有限枚举上限,不是训练出的准确率,也不是对未来数据的统计保证。若允许输入原始行号、隐藏纵坐标或其他旁路信息,已经改变了输入条件,不能拿来否定这个上限。随机化决策在每个等权冲突组上的期望正确率也不能超过一半。

只留小方差方向,反而保住了这次任务

作为明确对照,如果手工只保留纵坐标,解释方差比例只有 1/101,约 0.009901,但上下标签不再发生碰撞,有限四点上可以全部区分。若保留两个坐标,解释方差比例为 1,重建误差为零,同样可以区分。

手工挑纵轴使用了我们对任务的了解;它不是声称 PCA 本应自动选到纵轴。实际是否降维、留多少维,应在训练数据上拟合变换,再结合独立验证任务评估,不能用测试标签反复选择最漂亮的方向。

改计量尺度,主方向也可能换;并列时更要说清楚

把横坐标的数值缩小为原来的 0.05 倍,四点变成 (±0.5,±1)。类别不变,但方差变成 0.25 与 1,此时第一主成分转向纵轴。本例显示 PCA 依赖所采用的尺度;它本身进行中心化,并不自动把每列缩放为单位方差。

如果两轴都变成 ±1,两个方差一样大,第一主方向并不唯一。程序为了可复核,规定并列时选横轴,并明确打印 tie=True。这个教学选择会留下标签碰撞;选择纵轴也同样满足最大方差目标。不能把某次库给出的并列方向当成普遍唯一答案。

标准化可以改变哪些方向占主导,但不是分类质量的通用保证。它也会把这组长方形变成方差并列的正方形。是否缩放还要结合量纲、测量意义和验证结果。

完整程序:用枚举核对分类上限

下面代码不是通用 PCA 库。它先计算四角点的均值和协方差,再利用这个对称长方形的交叉项恰好为零,精确选择坐标轴;另用角度网格复核最大方差,并穷举投影后每个不同坐标能分配的二分类标签,核对最佳正确比例。程序把两个幅度都限制在 [10⁻⁶,10⁶],避免极小幅度平方下溢后除以零。

重建误差采用“每条样本的两个坐标平方误差之和,再对四条样本平均”,不是额外除以特征数。运行记录来自 Python 3.14.0;3.10 仅做 AST 语法兼容检查,没有执行 sklearn、训练分类器或评估外部数据。

展开收起代码 · 小实验python · 84 行

长行可在代码区域内左右滚动,按 Tab 可把焦点移入代码区。

小实验 · python
"""Exact rectangle PCA and finite-label collision bound; no fitted classifier."""
from collections import Counter, defaultdict
from itertools import product
import math

CHECKS = []

def positive(value):
    if isinstance(value, bool) or not isinstance(value, (int, float)) or not 1e-6 <= value <= 1e6:
        raise ValueError('scale must be finite and in [1e-6, 1e6]')
    return float(value)

def rectangle(horizontal, vertical):
    a, b = positive(horizontal), positive(vertical)
    return [(sx*a, sy*b, int(sy > 0)) for sx, sy in product([-1, 1], repeat=2)]

def summarize(horizontal, vertical, mode='pca'):
    rows = rectangle(horizontal, vertical)
    if mode not in ('pca', 'x', 'y', 'both'):
        raise ValueError('projection mode')
    n = len(rows)
    means = [sum(row[j] for row in rows)/n for j in range(2)]
    centered = [(x-means[0], y-means[1], label) for x, y, label in rows]
    vx = sum(x*x for x, y, label in centered)/n
    vy = sum(y*y for x, y, label in centered)/n
    covariance = sum(x*y for x, y, label in centered)/n
    axis = ('x' if vx >= vy else 'y') if mode == 'pca' else mode
    # This exact axis shortcut is valid ONLY for this symmetric rectangle.
    projected = [(x,) if axis == 'x' else (y,) if axis == 'y' else (x, y) for x, y, label in centered]
    groups = defaultdict(Counter)
    for point, row in zip(projected, centered):
        groups[point][row[2]] += 1
    ceiling = sum(max(counts.values()) for counts in groups.values())/n
    kept = vx if axis == 'x' else vy if axis == 'y' else vx+vy
    error = vy if axis == 'x' else vx if axis == 'y' else 0.
    return dict(axis=axis, vx=vx, vy=vy, covariance=covariance, ratio=kept/(vx+vy),
                mse=error, ceiling=ceiling, tie=vx == vy, projected=projected, groups=groups)

def check(name, ok):
    if not ok:
        raise AssertionError(name)
    CHECKS.append(name)

def main():
    cases = [('pca_original', 10., 1., 'pca'), ('keep_small_axis', 10., 1., 'y'),
             ('keep_both', 10., 1., 'both'), ('scale_horizontal_down', 0.5, 1., 'pca'),
             ('equal_variances', 1., 1., 'pca')]
    for name, a, b, mode in cases:
        result = summarize(a, b, mode)
        check(name + ' diagonal covariance', result['covariance'] == 0.)
        check(name + ' variance decomposition', math.isclose(result['mse']+(result['vx']+result['vy'])*result['ratio'], result['vx']+result['vy']))
        # Exhaust all deterministic label assignments to distinct projected points.
        groups = list(result['groups'].values())
        best = max(sum(counts[label] for counts, label in zip(groups, labels))/4 for labels in product([0,1], repeat=len(groups)))
        check(name + ' exhaustive classification ceiling', best == result['ceiling'])
        print(f"{name}: axis={result['axis']}; retained={result['ratio']:.6f}; reconstruction_mse={result['mse']:.2f}; finite_label_ceiling={result['ceiling']:.2f}; tie={result['tie']}")
    original = summarize(10, 1)
    check('99 percent variance is not label information', original['ratio'] > .99 and original['ceiling'] == .5)
    check('small axis keeps labels', summarize(10, 1, 'y')['ceiling'] == 1.)
    check('PCA ignores labels by construction', original['axis'] == 'x')
    for a, b in [(2., 1.), (1., 3.), (.5, 4.)]:
        result = summarize(a, b)
        # Grid directions independently verify the diagonal quadratic maximum.
        for degree in range(181):
            angle = math.radians(degree)
            variance = a*a*math.cos(angle)**2 + b*b*math.sin(angle)**2
            check('PCA direction dominates angle grid', variance <= max(a*a, b*b)+1e-10)
    for args in [(0,1), (-1,1), (True,1), (float('nan'),1), (1,float('inf')), (1,1e7), (1e-300,1e-300)]:
        try:
            summarize(*args)
        except ValueError:
            check('reject invalid scales', True)
        else:
            raise AssertionError('invalid input accepted')
    try:
        summarize(1, 1, 'unknown')
    except ValueError:
        check('reject invalid mode', True)
    else:
        raise AssertionError('invalid mode accepted')
    print(f'PASS checks={len(CHECKS)}; covariance_divisor=N; reference=label_assignment_enumeration+angle_grid')

if __name__ == '__main__':
    main()

怎样运行:在独立练习文件夹中,依次执行下载区的命令。2026-10-24 使用 Python 3.10 或更新版本,无第三方依赖;实测 Windows / Python 3.14.0,Python 3.10 仅做语法兼容检查 复跑这份源码并核对完整输出。对称四角点的精确轴向 PCA、分类碰撞枚举与角度网格;不是通用 PCA 实现,没有训练分类器或外部评测。

运行后对照结果
pca_original: axis=x; retained=0.990099; reconstruction_mse=1.00; finite_label_ceiling=0.50; tie=False
keep_small_axis: axis=y; retained=0.009901; reconstruction_mse=100.00; finite_label_ceiling=1.00; tie=False
keep_both: axis=both; retained=1.000000; reconstruction_mse=0.00; finite_label_ceiling=1.00; tie=False
scale_horizontal_down: axis=y; retained=0.800000; reconstruction_mse=0.25; finite_label_ceiling=1.00; tie=False
equal_variances: axis=x; retained=0.500000; reconstruction_mse=1.00; finite_label_ceiling=0.50; tie=True
PASS checks=569; covariance_divisor=N; reference=label_assignment_enumeration+angle_grid

把百分比读回它真正衡量的对象

scikit-learn 的 PCA 文档支持“中心化、正交主方向、解释方差比例”的定义;官方特征缩放示例展示缩放如何影响 PCA 和后续任务。本文的四点、碰撞证明与枚举结果是独立构造,没有把官方示例的实验结果移植为本地结果。

实际数据中高方差方向可能很有用,也可能主要表示背景或计量尺度;低方差方向也可能是噪声。这个反例只否定“解释方差高就保证任务信息足够”的推论,没有证明 PCA 普遍有害。先写明降维的目的,再分别验收重建、压缩或预测目标。

记住这三点

  • 解释方差比例衡量几何变化量,不是分类准确率或任务信息保留率。
  • 不同标签投影到相同坐标后,只使用该坐标的分类规则无法再区分。
  • 输入尺度和并列特征值会影响主方向;本例的轴选择不是通用 PCA 实现。

别把例子用过头

  • 把 99% 解释方差写成 99% 任务信息或准确率。
  • 只报告重建误差,不检查任务标签是否发生碰撞。
  • 利用测试标签选方向,再把同一测试结果当独立验证。

自己试一小步

换一条标签规则,原来的主方向还会丢掉任务信息吗?

  1. 保留原四点,把标签规则改为横坐标为正,再比较保留横轴与纵轴。
  2. 让两轴幅度相同,分别选择横轴、纵轴,解释为什么都是最大方差方向。
  3. 将样本权重改为不相等,重新推导相同投影坐标组内的最优正确比例;不要直接沿用 2/4。

做到这些,就算完成

  • 每次写出投影坐标与真实标签的配对。
  • 区分有限集合可达上限、实际拟合结果和未来数据表现。
  • 交代协方差分母、特征尺度与并列方向处理方式。

查原始资料

想确认一个细节,先看这份资料对应的部分,不必从头读完所有文档。