开始前,会这些就够了
- 知道坐标点和平方平均值
- 先读 矩阵与线性变换
- Python 3.10+;只使用标准库
一组数据降到一维后,报告说保留了 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 检查环境和所在目录。文中标出的阅读时间只估算文章篇幅,安装环境和动手练习可按自己的节奏进行。
保留下来的变化量大,标签还能分开吗?
正在准备实验控件…
固定四个等权点 (±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、训练分类器或评估外部数据。
展开收起代码 · 小实验
长行可在代码区域内左右滚动,按 Tab 可把焦点移入代码区。
"""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% 任务信息或准确率。
- 只报告重建误差,不检查任务标签是否发生碰撞。
- 利用测试标签选方向,再把同一测试结果当独立验证。
自己试一小步
换一条标签规则,原来的主方向还会丢掉任务信息吗?
- 保留原四点,把标签规则改为横坐标为正,再比较保留横轴与纵轴。
- 让两轴幅度相同,分别选择横轴、纵轴,解释为什么都是最大方差方向。
- 将样本权重改为不相等,重新推导相同投影坐标组内的最优正确比例;不要直接沿用 2
/4。
做到这些,就算完成
- 每次写出投影坐标与真实标签的配对。
- 区分有限集合可达上限、实际拟合结果和未来数据表现。
- 交代协方差分母、特征尺度与并列方向处理方式。
查原始资料
想确认一个细节,先看这份资料对应的部分,不必从头读完所有文档。
- scikit-learn:PCA API (新标签页)↗
PCA 中心化、不自动按列缩放、主方向与
explained_variance_ratio_的定义。 - scikit-learn:特征缩放的重要性 (新标签页)↗
特征尺度对 PCA 方向以及后续模型的影响;本篇四点与有限分类上限为自行构造。