PCA 给数据降维并可视化
亲手实现 PCA(主成分分析),不调 sklearn。直观看到特征值和特征向量在「压缩信息、保留主要差异」中的作用。
学完本案例你将理解:PCA 就是找到数据分布最「散」的方向,用更少的维度保留尽可能多的信息。
生活引入
给朋友描述一条鱼的形状
你不需要精确描述鱼身上每一个点的三维坐标。你只需要说:「大概这么长、这么宽、这个形状」。你用最少的几个数据抓住了最关键的差异。
PCA 做的正是这件事:从高维数据中找出最重要的几个方向,扔掉不重要的细节,用更少的数字描述同一份数据。
直观理解
假设我们有 300 个三维数据点。它们并不是均匀分布的——大多数点沿着某条斜线排列,只有很少的点偏离这条斜线。
这意味着什么?意味着我们其实不需要三个维度的坐标。两个维度就够了——一个沿斜线方向,一个垂直于斜线方向。垂直方向的第三个维度数据几乎没什么变化,可以丢掉。
中心化
协方差矩阵
特征分解
投影降维
特征值越大,说明数据在这个方向上「散得越开」——这个方向越重要。
数学定义
PCA 算法步骤
\[ \begin{aligned} \text{1. 中心化:}&\quad X_c = X - \bar{X} \\ \text{2. 协方差矩阵:}&\quad \Sigma = \frac{1}{n-1}X_c^T X_c \\ \text{3. 特征分解:}&\quad \Sigma = V \Lambda V^T \\ \text{4. 选取前 k 个特征向量:}&\quad V_k = [v_1, v_2, ..., v_k] \\ \text{5. 投影:}&\quad X_{\text{reduced}} = X_c \cdot V_k \end{aligned} \]解释方差比例
\[ \text{解释比例}_i = \frac{\lambda_i}{\sum_{j=1}^{d} \lambda_j} \]其中 \(\lambda_i\) 是第 \(i\) 大的特征值。这个比例告诉你:第 \(i\) 个主成分「贡献」了多少总方差。
Python 动手实践
构造一个 3 维但实际主要沿某方向分布的数据集,手写 PCA 将其降到 2 维。
实例
import numpy as np
np.random.seed(42)
# 1. 构造 3 维数据,数据主要沿某方向分布
n = 300
base = np.random.randn(n, 1)
X = np.hstack([
base * 3 + np.random.randn(n, 1) * 0.3, # x1 列
base * 1.5 + np.random.randn(n, 1) * 0.3, # x2 列
np.random.randn(n, 1) * 0.2, # x3 列(噪声)
])
print("RUNOOB 原始数据形状:", X.shape,
"(300 个样本,每个样本 3 维)")
# 2. 手写 PCA(四步走)
def pca(X, n_components=2):
# 第一步:中心化——把数据的均值移到 0
X_centered = X - X.mean(axis=0)
# 第二步:协方差矩阵——找出所有两两变量间的关系
cov = np.cov(X_centered, rowvar=False)
# 第三步:特征分解——eigh 用于对称矩阵更稳定
eigvals, eigvecs = np.linalg.eigh(cov)
# 按特征值从大到小排序
order = np.argsort(eigvals)[::-1]
eigvals = eigvals[order]
eigvecs = eigvecs[:, order]
# 第四步:取前 k 个特征向量,投影
components = eigvecs[:, :n_components]
X_reduced = X_centered @ components
# 计算每个主成分解释的方差比例
explained_ratio = eigvals[:n_components] / eigvals.sum()
return X_reduced, eigvals, explained_ratio
X_reduced, eigvals, explained_ratio = pca(X, n_components=2)
print("\nRUNOOB 三个特征值 (方差大小):",
np.round(eigvals, 3))
print("前 2 个主成分解释的方差比例:",
np.round(explained_ratio, 3))
print("累计保留信息:",
round(explained_ratio.sum() * 100, 1), "%")
print("降维后数据形状:", X_reduced.shape,
"(从 3 维降到了 2 维)")
# 3. 关键观察:第三个特征值非常小
print(f"\nRUNOOB 第三个特征值只有 {eigvals[2]:.4f},"
f"远小于前两个 ({eigvals[0]:.2f}, {eigvals[1]:.2f})")
print("说明第三维几乎没有信息量——可以安全丢掉!")
np.random.seed(42)
# 1. 构造 3 维数据,数据主要沿某方向分布
n = 300
base = np.random.randn(n, 1)
X = np.hstack([
base * 3 + np.random.randn(n, 1) * 0.3, # x1 列
base * 1.5 + np.random.randn(n, 1) * 0.3, # x2 列
np.random.randn(n, 1) * 0.2, # x3 列(噪声)
])
print("RUNOOB 原始数据形状:", X.shape,
"(300 个样本,每个样本 3 维)")
# 2. 手写 PCA(四步走)
def pca(X, n_components=2):
# 第一步:中心化——把数据的均值移到 0
X_centered = X - X.mean(axis=0)
# 第二步:协方差矩阵——找出所有两两变量间的关系
cov = np.cov(X_centered, rowvar=False)
# 第三步:特征分解——eigh 用于对称矩阵更稳定
eigvals, eigvecs = np.linalg.eigh(cov)
# 按特征值从大到小排序
order = np.argsort(eigvals)[::-1]
eigvals = eigvals[order]
eigvecs = eigvecs[:, order]
# 第四步:取前 k 个特征向量,投影
components = eigvecs[:, :n_components]
X_reduced = X_centered @ components
# 计算每个主成分解释的方差比例
explained_ratio = eigvals[:n_components] / eigvals.sum()
return X_reduced, eigvals, explained_ratio
X_reduced, eigvals, explained_ratio = pca(X, n_components=2)
print("\nRUNOOB 三个特征值 (方差大小):",
np.round(eigvals, 3))
print("前 2 个主成分解释的方差比例:",
np.round(explained_ratio, 3))
print("累计保留信息:",
round(explained_ratio.sum() * 100, 1), "%")
print("降维后数据形状:", X_reduced.shape,
"(从 3 维降到了 2 维)")
# 3. 关键观察:第三个特征值非常小
print(f"\nRUNOOB 第三个特征值只有 {eigvals[2]:.4f},"
f"远小于前两个 ({eigvals[0]:.2f}, {eigvals[1]:.2f})")
print("说明第三维几乎没有信息量——可以安全丢掉!")
RUNOOB 原始数据形状: (300, 3) (300 个样本,每个样本 3 维) RUNOOB 三个特征值 (方差大小): [9.569 2.267 0.04 ] 前 2 个主成分解释的方差比例: [0.806 0.191] 累计保留信息: 99.7 % 降维后数据形状: (300, 2) (从 3 维降到了 2 维) RUNOOB 第三个特征值只有 0.0404,远小于前两个 (9.57, 2.27) 说明第三维几乎没有信息量——可以安全丢掉!
AI 中的应用场景
| AI 场景 | 如何使用 PCA |
|---|---|
| 数据可视化 | 将高维数据(如词向量 768 维)降到 2 维,用散点图直观观察数据分布 |
| 特征降维 | 训练模型前,去除冗余特征,减少计算量同时降低过拟合风险 |
| 噪声过滤 | 只保留方差大的主成分,丢弃方差小的(往往是噪声) |
| 图像压缩 | 将人脸图片用少数几个「特征脸」(Eigenface)的组合来近似表示 |
| 推荐系统 | SVD(PCA 的矩阵版)分解用户-物品评分矩阵,发现隐藏的用户偏好模式 |
