如何向数学小白解释PCA(主成分分析)算法

主成分分析(PCA,Principal Componet Analysis)是数据科学中用于可视化和降维的必不可少的工具,但它通常被复杂的数学所掩盖。至少可以说,要理解其原理是非常困难的,导致很难完全欣赏到它的美妙之处。

虽然公式对于证明一个概念的有效性很重要,但我认为同样重要的是通过一个故事来分享公式背后的叙述。

什么是PCA

主成分分析(PCA)是一种将高维数据转换为低维数据的技术,同时尽可能保留更多的信息。如下图的三维转二维效果图:

image-20240722230006745

image-20240722230020043

PCA在处理具有大量特征的数据集时非常有用。常见的应用,如图像处理和基因组研究,往往需要处理成千上万甚至数万个列。

虽然拥有更多数据通常是好的,但有时数据中包含的信息过多,会导致模型训练时间极长,并且维度灾难开始成为问题。在这种情况下,减少维度可能会更有效。

我喜欢将PCA与写书总结进行比较。

找到时间阅读一本1000页的书是一种少有人能享受的奢侈。如果我们能在2到3页内总结出最重要的要点,让即使是最忙碌的人也能轻松消化这些信息,那不是很好吗?在这个过程中我们可能会丢失一些信息,但至少我们能抓住整体概貌。

PCA工作原理

这是一个两步过程。如果我们没有阅读或理解书的内容,就无法写出有效的书总结。

PCA的工作原理也一样——先理解,然后总结。

以PCA的方式理解数据

人类通过富有表现力的语言来理解故事书的意义。不幸的是,PCA不会开口讲话。它必须通过它偏好的语言——数学,来从我们的数据中寻找意义。

这里的关键问题是……

PCA能理解我们数据的哪个部分是重要的吗?我们能否用数学方法量化数据中嵌入的信息量?嗯,方差可以。

方差越大,信息量越多;反之亦然。

对大多数人来说,方差并不是一个陌生的术语。我们在高中时学过,方差衡量的是每个点与均值的平均差异程度。

方差公式:

但是方差从未与信息联系在一起。那么这种关联是从哪里来的?为什么这种关联有意义呢?

假设我们和朋友们玩一个猜谜游戏。游戏很简单。朋友们会遮住脸,我们需要仅根据他们的身高来猜出他们是谁。作为好朋友的我们,记得每个人的身高。

image-20240722230049783

我先猜吧!

image-20240722230106388

毫无疑问,我会说A是Chris,B是Alex,C是Ben。

现在,让我们试着猜一组不同的朋友。

image-20240722230117674

轮到你来猜了!

image-20240722230132082

你能猜出谁是谁吗?当他们的身高非常相似时,这就变得很难了。

之前,我们毫不费力地区分出了一个185厘米的人和160厘米、145厘米的人,因为他们的身高差异很大。

同样地,当我们的数据具有更高的方差时,它包含的信息就更多。这就是为什么我们总是把PCA和最大方差放在同一个句子中。我想通过引用维基百科的一段话来正式说明这一点。

主成分分析(PCA)被定义为一种正交线性变换,它将数据转换到一个新的坐标系统中,使得数据的最大方差通过某种标量投影落在第一个坐标上(称为第一主成分),第二大方差落在第二个坐标上,依此类推。

在PCA看来,方差是一种量化我们数据中信息量的客观数学方法。

方差就是信息。

为了进一步说明这一点,我建议重新进行猜谜游戏,不过这次我们将根据身高和体重来猜出谁是谁。

第2回合!

image-20240722230148255

一开始,我们只有身高数据。现在,我们基本上增加了一倍的朋友数据。这会改变你的猜测策略吗?

这是一个很好的引入,进入下一部分——PCA如何总结我们的数据,或更准确地说,如何降低数据的维度。

以PCA的方式总结数据

就我个人而言,体重差异太小(即小方差),对区分我们的朋友没有任何帮助。我仍然主要依靠身高来做出猜测。

直观地说,我们刚刚将数据从二维减少到一维。这个想法是我们可以选择性地保留方差较大的变量,然后忘记方差较小的变量。

但是,如果身高和体重具有相同的方差呢?这是否意味着我们不能再降低这个数据集的维度?我想用一个示例数据集来说明这一点。

image-20240722230239242

我们计算上图每个维度的方差:

image-20240722230310637

在这种情况下,很难选择我们想要删除的变量。如果我丢掉其中一个变量,就等于丢掉了一半的信息。

我们能保留两个变量吗?

也许,从不同的角度来看。最好的故事书总是有隐藏的主题,这些主题并不是直接写出来的,而是暗示的。单独阅读每一章可能没什么意义,但如果我们读完整本书,就能拼凑出足够的背景——潜在的情节就会浮现出来。

到目前为止,我们只是单独看了身高和体重的方差。与其限制自己只能选择一个,为什么不把它们结合起来呢?

当我们仔细观察我们的数据时,最大方差不在 x 轴,也不在 y 轴,而是在一条对角线上。第二大方差将是一条与第一条对角线垂直的线。

如下图的虚线表示最大方差的方向:

image-20240722230631969

为了表示这两条线,PCA 将身高和体重结合起来,创建两个全新的变量。它可以是 30% 的身高和 70% 的体重,或 87.2% 的身高和 13.8% 的体重,或根据我们拥有的数据的任何其他组合。

这两个新变量分别称为第一个主成分(PC1,first Principal Component)和第二个主成分(PC2,second Principal Component)。我们可以使用 PC1 和 PC2 分别作为两个坐标轴,而不是使用身高和体重。

如下图,左图是以高和宽为轴的数据分布,右图是以PC1和PC2为轴的数据分布。

image-20240722231302299

经过这一系列操作后,让我们再看看两者的方差对比。

如下图,左图的宽高方差相同,右图是经过PCA转换后以PC1和PC2为轴的数据分布,PC1轴的方差较大,PC2轴的方差为0.

image-20240722231608837

方差表格对比:

image-20240722231626911

如上图,PC1 单独就能捕捉到身高和体重的总方差。由于 PC1 包含了所有信息,你已经知道了这一点——我们可以非常放心地删除 PC2,并且知道我们的新数据仍然能代表原始数据。

当涉及到真实数据时,通常不会有一个主成分能捕捉到 100% 的方差。执行 PCA 会给我们 N 个主成分,其中 N 等于我们原始数据的维度。从这些主成分中,我们通常会选择最少数量的主成分来解释最多的原始数据。

我们使用视觉工具Scree Plot可视化主成分和方差的关系:

image-20240722231858255

条形图告诉我们每个主成分解释的方差比例。另一方面,叠加的折线图给出了截至第 N 个主成分的累积解释方差之和。理想情况下,我们希望用 2 到 3 个主成分就能获得至少 90% 的方差,这样可以保留足够的信息,同时我们仍然可以在图表上可视化数据,如上图,我觉得使用 2 个主成分是很合适的。

主成分分析丢失了哪些信息

因为我们没有选择所有的主成分,所以不可避免地会丢失一些信息。但我们还没有确切地描述我们丢失了什么。让我们通过一个新的示例更深入地探讨一下。

image-20240722232219009

上图的数据点尽管是分散的,但我们仍然可以看到它们在对角线上存在一定的正相关。

如果我们将数据输入 PCA 模型,它会首先绘制第一主成分,然后绘制第二主成分。当我们将原始数据从二维变换到二维时,除了方向以外,其他一切都保持不变。我们只是旋转了数据,使得最大方差位于 PC1 中,这并没有什么新奇之处。

image-20240722232409466

如上图,左图的虚线表示第一主成分和第二主成分的方向,右图是将左图的轴进行旋转,使的最大方差落在PC1(X轴)和PC2(Y轴)方向。

然而,假设我们决定只保留第一主成分,我们将不得不将所有数据点投影到第一主成分上,因为我们不再有 y 轴。

image-20240722232745815

如上图,左图的虚线表示第一主成分和第二主成分的方向,由于我们移除了第二主成分,右图所有的点都位于虚线上,因此我们将会失去的是第二主成分中的距离,如下方用红色线条突出显示的部分。

image-20240722233040001

这对每个数据点的距离有影响,如果我们查看两个特定点之间的欧几里得距离(即成对距离),你会发现一些点在原始数据中比在变换后的数据中要远得多。左图的红色线条是PCA转换前的距离,右图的红色线条是PCA转换后的距离,我们可以看到距离发生了改变。

image-20240722233204198

PCA 是一种线性变换,因此本身不会改变距离,但当我们开始去除维度时,距离会被扭曲。

但并非所有的成对距离都会受到相同的影响,如果我们选择两个最远的点,你会发现它们几乎是平行于主轴的。尽管它们的欧几里得距离仍然被扭曲,但扭曲的程度要小得多。如下图的红色线条,转换前后的距离只发生了细微的变化。

image-20240722233449399

主成分轴是在方差最大的方向上绘制的。根据定义,当数据点之间的距离更远时,方差会增加。因此,自然地,距离最远的点会更好地与主轴对齐。

总而言之,使用 PCA 降维会改变数据的距离。它以最大方差的原理进行PCA转换,使得大数据对对之间的距离比小数据对之间的距离保留得更好。

这是使用 PCA 降维的一些缺点之一,有时使用原始数据运行算法可能更有利。在这种情况下,数据科学家需要根据数据和使用场景做出决策。

PCA算法的Python实现

真正欣赏 PCA 的美丽,只有亲身体验才能做到,我们编码实现之前讲述的内容。首先,我们来处理一些导入操作,并生成我们将要使用的数据。

import pandas as pd
import numpy as np

# 生成数据包
from sklearn.datasets import make_blobs

# PCA
from sklearn.preprocessing import StandardScaler
from sklearn.decomposition import PCA

# 数据可视化
import matplotlib.pyplot as plt
import seaborn as sns
import plotly.express as px

产生数据集:

# 产生包含三个特征的三个簇的数据
X, y = make_blobs(n_samples=1000, centers=3, n_features=3, random_state=0, cluster_std=[1,2,3], center_box=(10,65))

# 数据归一化
X = StandardScaler().fit_transform(X)

# Prepare the array in a DataFrame
col_name = ['x' + str(idx) for idx in range(0, X.shape[1])]
df = pd.DataFrame(X, columns=col_name)
df['cluster_label'] = y

df.head()

我们的示例数据集包含 3 个变量 — x0、x1 和 x2,它们的分布方式使得数据点聚集在 3 个不同的簇中。cluster_label 指示了数据点所属的簇。

image-20240722234326463

数据可视化:

# 数据可视化
colors = px.colors.sequential.Plasma
colors[0], colors[1], colors[2] = ['red''green''blue']
fig = px.scatter_3d(df, x='x0', y='x1', z='x2', color=df['cluster_label'].astype(str), color_discrete_sequence=colors, height=500, width=1000)
fig.update_layout(showlegend=False,
                  scene_camera=dict(up=dict(x=0, y=0, z=1), 
                                    center=dict(x=0, y=0, z=-0.1),
                                    eye=dict(x=1.5, y=-1.4, z=0.5)),
                  margin=dict(l=0, r=0, b=0, t=0),
                  scene=dict(xaxis=dict(backgroundcolor='white',
                                        color='black',
                                        gridcolor='#f0f0f0',
                                        title_font=dict(size=10),
                                        tickfont=dict(size=10)),
                             yaxis=dict(backgroundcolor='white',
                                        color='black',
                                        gridcolor='#f0f0f0',
                                        title_font=dict(size=10),
                                        tickfont=dict(size=10)),
                             zaxis=dict(backgroundcolor='lightgrey',
                                        color='black'
                                        gridcolor='#f0f0f0',
                                        title_font=dict(size=10),
                                        tickfont=dict(size=10))))
fig.update_traces(marker=dict(size=3, line=dict(color='black', width=0.1)))
fig.show()

image-20240722234428393

我们尝试降低维度,幸运的是,Sklearn 使得执行 PCA 非常简单。虽然我们用超过 2000 字解释了 PCA,但运行它只需要 3 行代码。

# 执行PCA
pca = PCA()
_ = pca.fit_transform(df[col_name])
PC_components = np.arange(pca.n_components_) + 1

这里有几个要点。当我们将数据拟合到 Sklearn 的 PCA 函数时,它会完成所有繁重的工作,返回一个 PCA 模型和转换后的数据。

模型提供了许多属性,例如特征值、特征向量、原始数据的均值、解释的方差,等等。如果我们想了解 PCA 对数据的处理,这些都是非常有用的。

我想特别强调一个属性,即 pca.explained_variance_ratio_,它告诉我们每个主成分解释的方差比例。我们可以通过 Scree Plot图来可视化这个比例。

# Scree Plot
_ = sns.set(style='whitegrid', font_scale=1.2)
fig, ax = plt.subplots(figsize=(107))
_ = sns.barplot(x=PC_components, y=pca.explained_variance_ratio_, color='b')
_ = sns.lineplot(x=PC_components-1, y=np.cumsum(pca.explained_variance_ratio_), color='black', linestyle='-', linewidth=2, marker='o', markersize=8)

plt.title('Scree Plot')
plt.xlabel('N-th Principal Component')
plt.ylabel('Variance Explained')
plt.ylim(01)
plt.show()

image-20240722234910594

上面图表告诉我们,使用 2 个主成分而不是 3 个是可以的,因为这两个主成分可以捕获 90% 以上的方差。

此外,我们还可以查看每个主成分所创建的变量组合,使用 pca.components_。我们可以用热图来展示这些组合。

# Feature Weight
_ = sns.heatmap(pca.components_**2,
                 yticklabels=["PCA"+str(x) for x in range(1,pca.n_components_+1)],
                 xticklabels=list(col_name),
                 annot=True,
                 fmt='.2f',
                 square=True,
                 linewidths=0.05,
                 cbar_kws={"orientation""horizontal"})

image-20240722235031176

在我们的示例中,我们可以看到 PCA1 由 34% 的 x0、30% 的 x1 和 36% 的 x2 组成。PCA2 主要由 x1 主导。

Sklearn 提供了许多其他有用的属性。如果你感兴趣,我建议查看 Sklearn 文档中 PCA 的属性部分。

现在我们对主成分有了更好的理解,我们可以最终决定要保留多少个主成分。在这种情况下,我觉得 2 个主成分已经足够了。

所以,我们可以重新运行 PCA 模型,这次使用 n_components=2 参数,这告诉 PCA 只保留前 2 个主成分。

# 保留两个主成分
pca = PCA(n_components=2)
pca_array = pca.fit_transform(df)

# 使用dataframe保存数据
df_pca = pd.DataFrame(data=pca_array)
df_pca.columns = ['PC' + str(col+1for col in df_pca.columns.values]
df_pca['label'] = y

df_pca.head()

返回一个包含前两个主成分的 DataFrame。最后,我们可以绘制散点图来可视化我们的数据。

# 主成分可视化
_ = sns.set(style='ticks', font_scale=1.2)
fig, ax = plt.subplots(figsize=(107))
_ = sns.scatterplot(data=df_pca, x='PC1', y='PC2', hue=df_pca['label'], palette=['red''green''blue'])

左图是原始数据集的散点图,右图是经过PCA转换后,只保留前两个主成分的数据散点图。

image-20240722235423958

最后发言

PCA是一个数学上很漂亮的概念,我希望我能够用一种随意的语气来传达它,这样就不会让人感到不知所措。

升华一下,下面这段视频应该很好的解释了PCA算法:

对于那些渴望了解细节的人,我后面会写相关的博文。感谢大家的关注!

相关推荐

  • 原来支持OPC UA的PLC真么牛!!!
  • 2.2K Star精美监控!!!运维用了,在公司横着走
  • Spring Boot集成screw实现数据库文档生成
  • 启动资金5000块,2个人如何在抖音带货100w/月?
  • 解锁转转门店业务灵活性:如何利用MVEL引擎优化结算流程
  • 【云原生|K8S系列】不再迷茫!跟随这份攻略,10分钟了解K8S持久化存储!
  • 最高贴息1000万!杭州14条重磅AI新政发布,每年发2.5亿元算力券
  • 扎克伯格深度专访:中美AI竞争完全错误,美国别想长期领先中国
  • 云巨头大暴走,自研CPU落地200万张!新一轮芯片洗牌开始了
  • 基于大模型 + 知识库的 Code Review 实践
  • AI编程哪家强?一次对比4大编程助手
  • “开源模型是智商税” v.s. “开源AI是前进的道路”
  • 苹果iPhone 15 Pro跑起精简版Win11
  • 成都最硬核的公司,陆奇投了
  • 美团后端日常实习面试,轻松拿捏了!
  • 深入分析 C++ 错误处理:哪种策略的性能最强?
  • “Llama 会成为 AI 界的 Linux”,扎克伯格最新访谈出炉!最强模型 Llama 3.1 来了
  • 在 iPad 上「复活」WinXP!耗时 2.5 小时后,用户直呼:“这就是我一直想要的”
  • Java新闻汇总:JDK 24更新、Spring Framework、Piranha Cloud、Gradle 8.9
  • Rich Harris 承诺:使用 Svelte 5.0 你将编写更少的代码