尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

从零实现PCA主成分分析:数学推导与Python代码详解

从零实现PCA主成分分析:数学推导与Python代码详解 1. 项目概述与核心价值最近在整理一些高维数据集时我又一次用到了PCA主成分分析。这玩意儿在数据科学、机器学习和信号处理里简直就像瑞士军刀一样基础又实用。无论是处理图像、基因序列还是用户行为数据当特征维度高到让人头疼、数据间存在大量冗余时PCA总能站出来帮你把数据“压缩”到几个核心维度上同时最大程度地保留原始信息。这次我们不依赖sklearn这样的高级库而是用纯Python和NumPy从数学原理开始一步步推导并实现一个完整的PCA。这个过程不仅能让你彻底搞懂PCA里“特征值”、“特征向量”、“方差最大化”这些概念到底在干什么更能让你在面对任何降维或特征提取问题时心里有底知道怎么调参、怎么解释结果。无论你是刚入门数据分析的新手还是想巩固基础原理的老手跟着走一遍这个“造轮子”的过程收获绝对比单纯调用fit_transform大得多。2. PCA的数学原理深度拆解2.1 核心目标方差最大化与信息保留PCA的根本目的是在将数据投影到一个低维子空间时尽可能保留原始数据的“信息”。在PCA的语境下“信息”被直观地定义为数据的方差。方差越大说明数据在该方向上的分布越分散包含的信息可能就越多。反之方差小数据点挤在一起区分度就低。所以PCA的第一个核心步骤就是寻找数据方差最大的方向。想象一下你有一群散落在二维平面上的点。PCA要做的是找到一条新的直线第一个主成分使得所有数据点投影到这条直线上后投影点的分布最“散”即方差最大。找到了第一条轴我们再找与第一条轴正交垂直且方差次大的方向作为第二条主成分依此类推。这些新找到的轴就是主成分它们是原始特征空间的一组新的基向量。注意这里有一个关键点PCA寻找的主成分方向是彼此正交的。这意味着降维后的各个新特征主成分之间是线性无关的消除了原始特征可能存在的相关性这是PCA非常棒的一个特性。2.2 关键步骤的数学推导理解了目标我们来看看怎么用数学工具找到这些主成分。整个过程可以分解为以下几个关键步骤数据标准化中心化这是至关重要的一步。PCA对数据的尺度非常敏感。如果某个特征的量纲很大比如“年薪”单位是万它的方差天然就会很大会主导主成分的方向而量纲小的特征比如“年龄”可能就被忽略了。为了避免这种情况我们首先对每个特征进行中心化即减去该特征的均值使得每个特征的均值为0。公式为X_centered X - np.mean(X, axis0)。有时我们还会进行标准化除以标准差但中心化是PCA必需的标准化则视情况而定如果特征尺度差异巨大建议进行标准化。计算协方差矩阵中心化后的数据我们计算其协方差矩阵。协方差矩阵是一个对称矩阵其对角线上的元素是各个特征的方差非对角线上的元素Cov(i, j)表示第i个特征和第j个特征之间的协方差衡量它们的线性相关程度。计算公式为C (X_centered.T X_centered) / (n_samples - 1)。这里用(n-1)是为了得到样本协方差的无偏估计。这个矩阵封装了数据所有特征之间的方差和协方差信息。特征值分解这是PCA的“魔法”发生的地方。我们需要对协方差矩阵C进行特征值分解。即找到一组特征值λ和对应的特征向量v满足C * v λ * v。特征值λ其大小直接对应了其关联的特征向量所代表的主成分方向上方差的大小。特征值越大说明数据在该主成分方向上的方差越大包含的信息越多。特征向量v就是我们要找的主成分方向轴。每个特征向量都是一个单位向量指向数据分布的一个“主要”方向。特征向量之间是正交的。选择主成分我们将特征值从大到小排序同时将其对应的特征向量也按同样顺序排列。排序后的特征值列表就代表了各个主成分的重要性排名。通常我们只保留前k个最大的特征值对应的特征向量用这k个向量组成一个投影矩阵W形状为[n_features, k]。这个k就是我们要降维到的目标维度。数据投影降维最后将中心化后的原始数据X_centered投影到我们选出的主成分方向上得到降维后的新数据集Z。计算公式非常简单Z X_centered W。Z的每一列就是一个主成分得分行数与原数据相同列数变为k。2.3 方差解释率与K值选择那么k到底选多少合适呢这里就需要引入“方差解释率”这个概念。第i个主成分的方差解释率 λ_i / sum(所有λ)。所有主成分的方差解释率之和为1或100%。通常我们会绘制一个“碎石图”横轴是主成分序号纵轴是特征值大小或累计方差解释率。图形通常会有一个明显的“拐点”elbow拐点之前的主成分特征值下降很快之后变得平缓。选择拐点对应的k值意味着我们保留了大部分方差信息而舍弃了那些方差很小的、可能是噪声的成分。另一种常见做法是设定一个累计方差解释率的阈值比如95%。然后从第一个主成分开始累加方差解释率直到累计值超过95%此时的k就是我们要保留的主成分数量。3. 从零实现PCA的完整代码与解析理论说了一大堆现在我们来动手实现。我们将创建一个名为PCA的类模仿sklearn的API风格包含fit,transform,fit_transform等方法。3.1 类结构与初始化import numpy as np class MyPCA: 手动实现的PCA主成分分析类。 属性 n_components : int, 要保留的主成分数量 components_ : ndarray, 主成分轴特征向量形状为 (n_features, n_components) explained_variance_ : ndarray, 保留主成分对应的特征值方差形状为 (n_components,) explained_variance_ratio_ : ndarray, 保留主成分的方差解释率形状为 (n_components,) mean_ : ndarray, 训练时每个特征的均值用于数据中心化 def __init__(self, n_componentsNone): self.n_components n_components self.components_ None self.explained_variance_ None self.explained_variance_ratio_ None self.mean_ None def fit(self, X): 拟合模型计算PCA所需的所有参数。 参数 X : ndarray形状为 (n_samples, n_features)训练数据。 返回 self : 返回实例本身。 # 1. 数据中心化 self.mean_ np.mean(X, axis0) X_centered X - self.mean_ # 2. 计算协方差矩阵 # 使用 (n-1) 作为分母得到样本协方差的无偏估计 n_samples X.shape[0] cov_matrix (X_centered.T X_centered) / (n_samples - 1) # 3. 特征值分解 # np.linalg.eig 返回特征值和特征向量 eigenvalues, eigenvectors np.linalg.eig(cov_matrix) # 确保特征值和特征向量是实数协方差矩阵是实对称阵特征值为实数 eigenvalues np.real(eigenvalues) eigenvectors np.real(eigenvectors) # 4. 对特征值和特征向量进行排序降序 # argsort返回的是升序索引[::-1]将其反转得到降序 sorted_index np.argsort(eigenvalues)[::-1] sorted_eigenvalues eigenvalues[sorted_index] sorted_eigenvectors eigenvectors[:, sorted_index] # 注意索引方式每一列是一个特征向量 # 5. 选择主成分数量 k if self.n_components is None: # 如果未指定默认保留所有特征值非零的主成分 self.n_components np.sum(sorted_eigenvalues 1e-12) # 用一个很小的阈值判断 elif 0 self.n_components 1: # 如果n_components是小数则视为要保留的方差解释率 total_variance np.sum(sorted_eigenvalues) explained_variance_ratio sorted_eigenvalues / total_variance cumulative_ratio np.cumsum(explained_variance_ratio) # 找到第一个使累计解释率大于等于设定值的索引 self.n_components np.argmax(cumulative_ratio self.n_components) 1 # 确保k不超过特征总数 self.n_components min(self.n_components, X.shape[1]) # 6. 存储结果 self.components_ sorted_eigenvectors[:, :self.n_components] self.explained_variance_ sorted_eigenvalues[:self.n_components] total_variance np.sum(sorted_eigenvalues) self.explained_variance_ratio_ self.explained_variance_ / total_variance return self实操心得在计算协方差矩阵时我们用了(X_centered.T X_centered) / (n-1)。对于大型矩阵np.cov(X_centered, rowvarFalse)是更标准的写法但手动计算有助于理解其本质。另外np.linalg.eig返回的特征值和向量可能是复数但由于协方差矩阵是实对称矩阵其特征值必为实数特征向量也可取实向量所以用np.real()转换是安全的。3.2 数据转换与逆转换拟合好模型后我们就可以用transform方法将任何新数据或训练数据降维。def transform(self, X): 将数据投影到主成分空间降维。 参数 X : ndarray形状为 (n_samples, n_features)要转换的数据。 返回 X_transformed : ndarray形状为 (n_samples, n_components)降维后的数据。 if self.mean_ is None or self.components_ is None: raise ValueError(必须先调用 fit 方法来训练模型。) # 使用训练时存储的均值进行中心化 X_centered X - self.mean_ # 投影点乘主成分矩阵 X_transformed X_centered self.components_ return X_transformed def fit_transform(self, X): 拟合模型并立即转换数据。 self.fit(X) return self.transform(X)有时我们还想看看降维后的数据“还原”到原始空间是什么样子当然会有信息损失这需要实现逆变换。def inverse_transform(self, X_transformed): 将降维后的数据反向投影回原始特征空间。 参数 X_transformed : ndarray形状为 (n_samples, n_components)降维后的数据。 返回 X_approx : ndarray形状为 (n_samples, n_features)重构的原始数据。 if self.components_ is None or self.mean_ is None: raise ValueError(必须先调用 fit 方法来训练模型。) # 反向投影X_approx X_transformed components_.T mean_ X_approx X_transformed self.components_.T self.mean_ return X_approx3.3 使用示例鸢尾花数据集让我们用经典的鸢尾花Iris数据集来测试我们的MyPCA。import matplotlib.pyplot as plt from sklearn.datasets import load_iris # 1. 加载数据 iris load_iris() X iris.data # (150, 4) y iris.target # 类别标签用于着色 # 2. 使用我们的MyPCA降维到2维 pca MyPCA(n_components2) X_pca pca.fit_transform(X) print(保留的主成分形状, pca.components_.shape) # 应输出 (4, 2) print(解释方差比, pca.explained_variance_ratio_) print(累计解释方差比, np.sum(pca.explained_variance_ratio_)) # 3. 可视化 plt.figure(figsize(8, 6)) scatter plt.scatter(X_pca[:, 0], X_pca[:, 1], cy, cmapviridis, edgecolork, s50) plt.xlabel(fPrincipal Component 1 ({pca.explained_variance_ratio_[0]:.2%})) plt.ylabel(fPrincipal Component 2 ({pca.explained_variance_ratio_[1]:.2%})) plt.title(Iris Dataset PCA Projection (2D)) plt.colorbar(scatter, labelIris Species) plt.grid(True, linestyle--, alpha0.7) plt.show()运行这段代码你会得到一个二维散点图三个不同品种的鸢尾花被清晰地分离出来。从输出的解释方差比可以看到仅用两个主成分就保留了原始四维数据中超过95%的方差信息。这就是PCA的威力——用更少的维度抓住了数据的核心结构。4. 关键参数、细节与高级话题4.1 中心化与标准化的抉择前面强调了中心化是必须的那标准化呢这取决于你的数据。如果各个特征是在相同或可比的尺度上测量的比如图像的不同像素点其值范围都是0-255那么只做中心化通常就够了。但如果特征尺度差异巨大比如“房屋面积”和“房间数量”不做标准化PCA的结果会被大尺度特征所主导。sklearn的PCA通常与StandardScaler结合使用先标准化再PCA这是一个稳健的默认选择。在我们的实现中可以在调用fit之前手动对X进行标准化X_scaled (X - np.mean(X, axis0)) / np.std(X, axis0)。4.2 特征值分解的替代方案SVD在实际的大型数据集中尤其是当样本数n远小于特征数m时直接计算协方差矩阵(m x m)并进行特征值分解计算量很大。更高效且数值稳定的方法是使用奇异值分解SVD。对于中心化后的数据矩阵X_centered (n x m)其SVD分解为X_centered U * Σ * V^T。其中V的列就是协方差矩阵(X_centered^T X_centered)的特征向量即我们的主成分components_。Σ是奇异值矩阵其对角线元素的平方除以(n-1)就是特征值explained_variance_。sklearn的PCA默认就是使用SVD求解因为它更稳定且能处理稀疏矩阵。我们可以修改fit方法中的计算部分# 替代特征值分解的部分 from scipy import linalg # 对中心化后的数据直接进行SVD U, s, Vt linalg.svd(X_centered, full_matricesFalse) # Vt 是 V 的转置其行是主成分 self.components_ Vt[:self.n_components].T # 注意转置回来使每一列是一个主成分 # 计算特征值 self.explained_variance_ (s ** 2) / (n_samples - 1) self.explained_variance_ self.explained_variance_[:self.n_components]4.3 主成分的解释与载荷降维后我们得到了新的特征主成分但它们是由原始特征线性组合而成的缺乏直接的物理意义。为了理解每个主成分到底代表了什么我们需要查看载荷Loading。载荷就是主成分向量components_本身其每个元素的大小和符号代表了对应原始特征对该主成分的贡献程度。例如在鸢尾花数据集中如果第一个主成分在“花瓣长度”和“花瓣宽度”上有很大的正权重而在“萼片宽度”上有负权重那么我们可以将这个主成分解释为“花朵大小与形状”的综合指标。分析载荷矩阵是理解PCA结果、进行特征工程和业务解释的关键。5. 常见问题、陷阱与实战技巧5.1 特征值为负或复数理论上协方差矩阵是半正定对称矩阵其特征值应为非负实数。如果你在手动实现时特别是用np.linalg.eig处理接近奇异的矩阵时得到了极小的负值或复数这通常是数值计算误差。一个实用的处理方法是取特征值的实部并过滤掉那些小于某个极小阈值如1e-12的值将其视为0。这在前面的代码中已有体现。5.2 如何应用于新数据这是很多初学者困惑的地方。拟合 (fit) 阶段PCA从训练数据X_train中计算出了均值mean_和主成分方向components_。当有新的测试数据X_test需要降维时必须使用训练时计算出的mean_对其进行中心化然后再用训练好的components_进行投影 (transform)。绝对不能用X_test自己的均值去中心化否则投影空间就错乱了会导致毫无意义甚至错误的结果。我们的transform方法内部已经正确处理了这一点。5.3 PCA是万能药吗绝对不是。PCA有其明确的适用场景和局限性线性假设PCA只能捕捉线性关系。如果数据的内在结构是非线性的比如流形结构PCA效果会很差此时应考虑t-SNE、UMAP或核PCAKernel PCA。方差代表信息PCA保留方差最大的方向但方差大不一定等于信息重要。有时方差小的方向可能包含关键的判别信息特别是在分类任务中。对异常值敏感由于基于方差L2范数异常值会显著影响协方差矩阵的计算从而拉偏主成分的方向。在应用PCA前进行异常值检测和处理是很好的实践。丢失可解释性降维后的特征失去了原始特征的含义给业务解释带来挑战。5.4 实战技巧与心得可视化辅助决策在决定保留几个主成分时一定要画碎石图。肉眼观察“拐点”比死守一个百分比阈值如95%更直观有时保留拐点前的主成分就能抓住主要模式避免引入过多噪声。# 绘制碎石图 pca_full MyPCA() # 不指定n_components拟合所有 pca_full.fit(X) explained_variance pca_full.explained_variance_ plt.figure(figsize(10, 4)) plt.subplot(1, 2, 1) plt.plot(range(1, len(explained_variance)1), explained_variance, bo-) plt.xlabel(Principal Component) plt.ylabel(Eigenvalue (Variance)) plt.title(Scree Plot) plt.grid(True) plt.subplot(1, 2, 2) cumulative_ratio np.cumsum(pca_full.explained_variance_ratio_) plt.plot(range(1, len(cumulative_ratio)1), cumulative_ratio, ro-) plt.xlabel(Number of Principal Components) plt.ylabel(Cumulative Explained Variance Ratio) plt.axhline(y0.95, colorg, linestyle--, label95% threshold) plt.title(Cumulative Explained Variance) plt.legend() plt.grid(True) plt.tight_layout() plt.show()与模型结合PCA常用于监督学习前的特征预处理。流程是拆分训练集和测试集 - 只在训练集上拟合PCA - 用训练集拟合的PCA转换训练集和测试集 - 在新特征上训练模型。千万避免在合并训练集和测试集后做PCA这是严重的数据泄露。内存与效率对于海量数据样本数或特征数极大全量SVD可能内存不足。可以考虑使用增量PCAIncremental PCA或随机SVDRandomized SVD等算法sklearn.decomposition中有相应的实现。不要盲目降维降维不总是有益的。如果原始特征不多且它们对模型都有意义强行降维可能会损失信息。PCA更适用于特征数远大于样本数“维数灾难”或特征间存在高度多重共线性的情况。手动实现一遍PCA虽然比调用库函数麻烦但就像亲手拆解一个精密仪器再装回去你对它的每个螺丝、每个齿轮的作用都会了然于胸。下次当你面对高维数据犹豫该降维到几维、或者疑惑为什么降维后模型效果反而变差时这次深入原理和实现的经历会给你提供最直接的判断依据和排查思路。
返回列表