主成分分析(PCA,Principal Componet Analysis)是数据科学中用于可视化和降维的必不可少的工具,但它通常被复杂的数学所掩盖。至少可以说,要理解其原理是非常困难的,导致很难完全欣赏到它的美妙之处。
虽然公式对于证明一个概念的有效性很重要,但我认为同样重要的是通过一个故事来分享公式背后的叙述。
什么是PCA
主成分分析(PCA)是一种将高维数据转换为低维数据的技术,同时尽可能保留更多的信息。如下图的三维转二维效果图:
PCA在处理具有大量特征的数据集时非常有用。常见的应用,如图像处理和基因组研究,往往需要处理成千上万甚至数万个列。
虽然拥有更多数据通常是好的,但有时数据中包含的信息过多,会导致模型训练时间极长,并且维度灾难开始成为问题。在这种情况下,减少维度可能会更有效。
我喜欢将PCA与写书总结进行比较。
找到时间阅读一本1000页的书是一种少有人能享受的奢侈。如果我们能在2到3页内总结出最重要的要点,让即使是最忙碌的人也能轻松消化这些信息,那不是很好吗?在这个过程中我们可能会丢失一些信息,但至少我们能抓住整体概貌。
PCA工作原理
这是一个两步过程。如果我们没有阅读或理解书的内容,就无法写出有效的书总结。
PCA的工作原理也一样——先理解,然后总结。
以PCA的方式理解数据
人类通过富有表现力的语言来理解故事书的意义。不幸的是,PCA不会开口讲话。它必须通过它偏好的语言——数学,来从我们的数据中寻找意义。
这里的关键问题是……
PCA能理解我们数据的哪个部分是重要的吗?我们能否用数学方法量化数据中嵌入的信息量?嗯,方差可以。
方差越大,信息量越多;反之亦然。
对大多数人来说,方差并不是一个陌生的术语。我们在高中时学过,方差衡量的是每个点与均值的平均差异程度。
方差公式:
但是方差从未与信息联系在一起。那么这种关联是从哪里来的?为什么这种关联有意义呢?
假设我们和朋友们玩一个猜谜游戏。游戏很简单。朋友们会遮住脸,我们需要仅根据他们的身高来猜出他们是谁。作为好朋友的我们,记得每个人的身高。
我先猜吧!
毫无疑问,我会说A是Chris,B是Alex,C是Ben。
现在,让我们试着猜一组不同的朋友。
轮到你来猜了!
你能猜出谁是谁吗?当他们的身高非常相似时,这就变得很难了。
之前,我们毫不费力地区分出了一个185厘米的人和160厘米、145厘米的人,因为他们的身高差异很大。
同样地,当我们的数据具有更高的方差时,它包含的信息就更多。这就是为什么我们总是把PCA和最大方差放在同一个句子中。我想通过引用维基百科的一段话来正式说明这一点。
主成分分析(PCA)被定义为一种正交线性变换,它将数据转换到一个新的坐标系统中,使得数据的最大方差通过某种标量投影落在第一个坐标上(称为第一主成分),第二大方差落在第二个坐标上,依此类推。
在PCA看来,方差是一种量化我们数据中信息量的客观数学方法。
方差就是信息。
为了进一步说明这一点,我建议重新进行猜谜游戏,不过这次我们将根据身高和体重来猜出谁是谁。
第2回合!
一开始,我们只有身高数据。现在,我们基本上增加了一倍的朋友数据。这会改变你的猜测策略吗?
这是一个很好的引入,进入下一部分——PCA如何总结我们的数据,或更准确地说,如何降低数据的维度。
以PCA的方式总结数据
就我个人而言,体重差异太小(即小方差),对区分我们的朋友没有任何帮助。我仍然主要依靠身高来做出猜测。
直观地说,我们刚刚将数据从二维减少到一维。这个想法是我们可以选择性地保留方差较大的变量,然后忘记方差较小的变量。
但是,如果身高和体重具有相同的方差呢?这是否意味着我们不能再降低这个数据集的维度?我想用一个示例数据集来说明这一点。
我们计算上图每个维度的方差:
在这种情况下,很难选择我们想要删除的变量。如果我丢掉其中一个变量,就等于丢掉了一半的信息。
我们能保留两个变量吗?
也许,从不同的角度来看。最好的故事书总是有隐藏的主题,这些主题并不是直接写出来的,而是暗示的。单独阅读每一章可能没什么意义,但如果我们读完整本书,就能拼凑出足够的背景——潜在的情节就会浮现出来。
到目前为止,我们只是单独看了身高和体重的方差。与其限制自己只能选择一个,为什么不把它们结合起来呢?
当我们仔细观察我们的数据时,最大方差不在 x 轴,也不在 y 轴,而是在一条对角线上。第二大方差将是一条与第一条对角线垂直的线。
如下图的虚线表示最大方差的方向:
为了表示这两条线,PCA 将身高和体重结合起来,创建两个全新的变量。它可以是 30% 的身高和 70% 的体重,或 87.2% 的身高和 13.8% 的体重,或根据我们拥有的数据的任何其他组合。
这两个新变量分别称为第一个主成分(PC1,first Principal Component)和第二个主成分(PC2,second Principal Component)。我们可以使用 PC1 和 PC2 分别作为两个坐标轴,而不是使用身高和体重。
如下图,左图是以高和宽为轴的数据分布,右图是以PC1和PC2为轴的数据分布。
经过这一系列操作后,让我们再看看两者的方差对比。
如下图,左图的宽高方差相同,右图是经过PCA转换后以PC1和PC2为轴的数据分布,PC1轴的方差较大,PC2轴的方差为0.
方差表格对比:
如上图,PC1 单独就能捕捉到身高和体重的总方差。由于 PC1 包含了所有信息,你已经知道了这一点——我们可以非常放心地删除 PC2,并且知道我们的新数据仍然能代表原始数据。
当涉及到真实数据时,通常不会有一个主成分能捕捉到 100% 的方差。执行 PCA 会给我们 N 个主成分,其中 N 等于我们原始数据的维度。从这些主成分中,我们通常会选择最少数量的主成分来解释最多的原始数据。
我们使用视觉工具Scree Plot可视化主成分和方差的关系:
条形图告诉我们每个主成分解释的方差比例。另一方面,叠加的折线图给出了截至第 N 个主成分的累积解释方差之和。理想情况下,我们希望用 2 到 3 个主成分就能获得至少 90% 的方差,这样可以保留足够的信息,同时我们仍然可以在图表上可视化数据,如上图,我觉得使用 2 个主成分是很合适的。
主成分分析丢失了哪些信息
因为我们没有选择所有的主成分,所以不可避免地会丢失一些信息。但我们还没有确切地描述我们丢失了什么。让我们通过一个新的示例更深入地探讨一下。
上图的数据点尽管是分散的,但我们仍然可以看到它们在对角线上存在一定的正相关。
如果我们将数据输入 PCA 模型,它会首先绘制第一主成分,然后绘制第二主成分。当我们将原始数据从二维变换到二维时,除了方向以外,其他一切都保持不变。我们只是旋转了数据,使得最大方差位于 PC1 中,这并没有什么新奇之处。
如上图,左图的虚线表示第一主成分和第二主成分的方向,右图是将左图的轴进行旋转,使的最大方差落在PC1(X轴)和PC2(Y轴)方向。
然而,假设我们决定只保留第一主成分,我们将不得不将所有数据点投影到第一主成分上,因为我们不再有 y 轴。
如上图,左图的虚线表示第一主成分和第二主成分的方向,由于我们移除了第二主成分,右图所有的点都位于虚线上,因此我们将会失去的是第二主成分中的距离,如下方用红色线条突出显示的部分。
这对每个数据点的距离有影响,如果我们查看两个特定点之间的欧几里得距离(即成对距离),你会发现一些点在原始数据中比在变换后的数据中要远得多。左图的红色线条是PCA转换前的距离,右图的红色线条是PCA转换后的距离,我们可以看到距离发生了改变。
PCA 是一种线性变换,因此本身不会改变距离,但当我们开始去除维度时,距离会被扭曲。
但并非所有的成对距离都会受到相同的影响,如果我们选择两个最远的点,你会发现它们几乎是平行于主轴的。尽管它们的欧几里得距离仍然被扭曲,但扭曲的程度要小得多。如下图的红色线条,转换前后的距离只发生了细微的变化。
主成分轴是在方差最大的方向上绘制的。根据定义,当数据点之间的距离更远时,方差会增加。因此,自然地,距离最远的点会更好地与主轴对齐。
总而言之,使用 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
指示了数据点所属的簇。
数据可视化:
# 数据可视化
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()
我们尝试降低维度,幸运的是,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=(10, 7))
_ = 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(0, 1)
plt.show()
上面图表告诉我们,使用 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"})
在我们的示例中,我们可以看到 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+1) for 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=(10, 7))
_ = sns.scatterplot(data=df_pca, x='PC1', y='PC2', hue=df_pca['label'], palette=['red', 'green', 'blue'])
左图是原始数据集的散点图,右图是经过PCA转换后,只保留前两个主成分的数据散点图。
最后发言
PCA是一个数学上很漂亮的概念,我希望我能够用一种随意的语气来传达它,这样就不会让人感到不知所措。
升华一下,下面这段视频应该很好的解释了PCA算法:
对于那些渴望了解细节的人,我后面会写相关的博文。感谢大家的关注!