如何通俗理解PCA(主成分分析)算法的数学原理和代码实现

文摘   2024-08-01 08:33   广东  

本文介绍主成分分析(PCA,Principal Component Analysis)算法背后的数学原理,并用Python和scikit-learn实现和可视化PCA算法。

在现代科技时代,数据的产生和收集量不断增加。然而,在机器学习中,过多的数据可能会带来负面影响。在某些情况下,更多的特征或维度会降低模型的准确性,因为需要对更多的数据进行泛化——这被称为维度灾难。

降维是一种减少模型复杂性并避免过拟合的方法。降维主要有两种类别:特征选择和特征提取。通过特征选择,我们选择原始特征的一个子集,而在特征提取中,我们从特征集中提取信息来构建新的特征子空间。

在本教程中,我们将探讨特征提取。在实践中,特征提取不仅用于提高存储空间或学习算法的计算效率,还可以通过减少维度灾难来提高预测性能——特别是当我们使用非正则化模型时。

具体来说,我们将讨论用于把数据集压缩到低维特征子空间的PCA算法,目的是保持大部分相关信息。我们将探索以下内容:

  • PCA算法的概念和数学原理
  • 如何从头开始一步一步地用Python执行PCA
  • 如何使用scikit-learn包执行PCA

让我们开始吧!

PCA算法概念

主成分分析是一种无监督的线性变换技术,广泛应用于各个领域,最主要用于特征提取和降维,其他常见应用包括探索性数据分析、股票市场交易中的信号去噪,以及在生物信息学领域中对基因组数据和基因表达水平的分析。

PCA旨在找到高维数据中最大方差的方向,并将其投影到一个维度等于或小于原始维度的新子空间上。

如下动图是寻找数据点映射的最大方差方向,静止的红色粗线就是我们要找的最大方差方向:

寻找最大方差方向

子空间的正交轴(主成分)可以解释为高维数据的最大方差方向,因为每个主成分是彼此正交的,确定了它们之间没有冗余信息。

上图中,x1 和 x2 是原始特征轴,而 是主成分。

如果我们使用PCA进行降维,我们构建一个维的变换矩阵,使我们能够将样本向量 映射到一个新的 k 维特征子空间,该子空间的维度比原始的 d 维特征空间要少。

d维特征空间的样本向量

经过维度变换矩阵后得到新的样本向量

其中向量是k维特征空间:

将原始的 d 维数据转换到这个新的 k 维子空间(通常 k ≪ d)后,第一个主成分将具有最大的方差,所有后续的主成分将在与其他主成分不相关(正交)的约束下具有最大的方差——即使输入特征是相关的,所得的主成分也将是彼此正交(不相关)的。

需要注意的是,PCA 的方向对数据的缩放非常敏感,如果特征的度量尺度不同,我们希望赋予所有特征相同的重要性,因此需要在进行 PCA 之前对特征进行标准化。

在更详细地了解PCA算法进行降维之前,让我们用几个简单的步骤总结一下这种方法:

1)标准化d维数据集。

2)构建协方差矩阵。

3)将协方差矩阵分解为其特征向量和特征值。

4)按照特征值的递减顺序对相应的特征向量进行排序。

5)选择与 k 个最大特征值对应的 k 个特征向量,其中 k 是新特征子空间的维度(k ≤ d)。

6)从“前” k 个特征向量中构建投影矩阵

7)使用投影矩阵 转换 d 维输入数据集 以获得新的 k 维特征子空间。

让我们一步步地用Python编码实现PCA,以学习算法背后的数学原理。然后我们将看到如何使用scikit-learn更方便地进行PCA。

PCA算法蕴含的数学原理

我们将在示例中使用来自UCI机器学习库的葡萄酒数据集。该数据集包含178个葡萄酒样本,每个样本有13个特征,描述其不同的化学属性。

加载数据集:

import pandas as pd

df_wine = pd.read_csv('https://archive.ics.uci.edu/ml/'
                      'machine-learning-databases/wine/wine.data',
                      header=None)
df_wine.head()

image-20240730123958281

接下来,我们将把葡萄酒数据分成独立的训练集和测试集,使用7:3的比例,并将其标准化为单位方差:

from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler

# split into training and testing sets
X, y = df_wine.iloc[:, 1:].values, df_wine.iloc[:, 0].values
X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.3,
    stratify=y, random_state=0
)
# standardize the features
sc = StandardScaler()
X_train_std = sc.fit_transform(X_train)
X_test_std = sc.transform(X_test)

完成必要的预处理后,让我们进入第二步:构建协方差矩阵。这个对称的 维协方差矩阵,其中 是数据集中的维度数,存储了不同特征之间的成对协方差。例如,两个特征 在总体水平上的协方差可以通过以下公式计算:

其中n表示样本总数,是特征j和特征k所在样本的平均值。

请注意,如果我们对数据集进行了标准化,样本均值为零。两个特征之间的正协方差表明这些特征一起增加或减少,即正相关的关系;负协方差表明这些特征朝相反方向变化,即负相关的关系。

例如,三个特征的协方差矩阵可以写成如下形式(注意 Σ 代表希腊字母的大写 Sigma,不要与求和符号混淆)

协方差矩阵的特征向量代表主成分(最大方差的方向),而相应的特征值将定义它们的大小。在葡萄酒数据集的情况下,我们将从13 x 13维的协方差矩阵中获得13个特征向量和特征值。

现在,进入我们的第三步,让我们获得协方差矩阵的特征值和特征向量。特征向量 满足以下条件:

这里,是标量,也就是协方差矩阵的特征值。由于手动计算特征向量和特征值是一项相当繁琐且复杂的任务,我们将使用NumPylinalg.eig函数获取葡萄酒数据集协方差的特征值和特征向量:

import numpy as np

cov_mat = np.cov(X_train_std.T)
eigen_vals, eigen_vecs = np.linalg.eig(cov_mat)

如上代码,我们使用numpy.cov函数计算了标准化训练数据集的协方差矩阵。使用numpy.linalg.eig函数对数据集进行特征分解,得到了一个包含13个特征值的向量(eigen_vals)和对应矩阵列的对应特征向量(eigen_vecs)。

总方差和解释方差

我们希望通过将数据集压缩到一个新的特征子空间来减少其维度,因此只选择包含大部分信息(方差)的特征向量(主成分)子集。特征值定义了特征向量的大小,因此我们必须按特征值的大小递减排序,我们感兴趣的是基于其对应特征值的值的前k个特征向量。

但是在收集这k个信息量最多的特征向量之前,让我们绘制特征值的解释方差比。

特征值的解释方差比是特征值与特征值总和的比值:

使用NumPy的cumsum函数计算解释方差的特征累计和,然后通过matplotlib的step函数进行绘制:

import matplotlib.pyplot as plt

# 累加解释方差(特征值)之和
tot = sum(eigen_vals)
var_exp = [(i / tot) for i in sorted(eigen_vals, reverse=True)]
cum_var_exp = np.cumsum(var_exp)

# 绘制解释方差
plt.bar(range(1,14), var_exp, alpha=0.5,
        align='center', label='individual explained variance')
plt.step(range(1,14), cum_var_exp, where='mid',
         label='cumulative explained variance')
plt.ylabel('Explained variance ratio')
plt.xlabel('Principal component index')
plt.legend(loc='best')
plt.show()

image-20240730211510815

如上生成的图表显示,仅第一个主成分就占了大约40%的方差。此外,我们可以看到,前两个主成分合起来解释了数据集中将近60%的方差。

特征转换

将协方差矩阵分解为特征值和特征向量之后,让我们继续进行PCA的最后三个步骤,将葡萄酒数据集转换到新的主成分轴上。

我们将按特征值和特征向量按照特征值的降序排列,从选定的特征向量中构建投影矩阵,并使用投影矩阵将数据转换到低维子空间。

我们从按特征值的降序排列特征值和特征向量开始。

# 生成(特征值, 特征向量)的特征对,tuple类型
eigen_pairs = [(np.abs(eigen_vals[i]), eigen_vecs[:, i]) for i in range(len(eigen_vals))]

# 特征对按特征值降序排序
eigen_pairs.sort(key=lambda k: k[0], reverse=True)

接下来,我们收集对应于两个最大特征值的两个特征向量,以捕捉该数据集中约60%的方差。

请注意,我们只选择了两个特征向量是为了说明目的,因为我们将在本小节稍后通过二维散点图绘制数据。在实践中,主成分的数量需要在计算效率和分类器性能之间进行权衡来确定。

w = np.hstack((eigen_pairs[0][1][:, np.newaxis], eigen_pairs[1][1][:, np.newaxis]))
print('Matrix W:\n', w)

上述代码获取了包含了前两个特征值的特征向量,行拼接为投影矩阵。

投影矩阵:

[Out:]
Matrix W:
 [[-0.13724218  0.50303478]
 [ 0.24724326  0.16487119]
 [-0.02545159  0.24456476]
 [ 0.20694508 -0.11352904]
 [-0.15436582  0.28974518]
 [-0.39376952  0.05080104]
 [-0.41735106 -0.02287338]
 [ 0.30572896  0.09048885]
 [-0.30668347  0.00835233]
 [ 0.07554066  0.54977581]
 [-0.32613263 -0.20716433]
 [-0.36861022 -0.24902536]
 [-0.29669651  0.38022942]]

通过执行前面的代码,我们从前两个特征向量创建了一个13 x 2维的投影矩阵

使用这个投影矩阵,我们现在可以将样本(表示为1 x 13维的行向量)转换到PCA子空间(主成分一和主成分二),得到由两个新特征组成的二维样本向量

投影公式:

代码表示投影公式:

X_train_std[0].dot(w)

同样地,我们可以通过计算矩阵点积将整个124 x 13维的训练数据集转换到两个主成分上:

X_train_pca = X_train_std.dot(w)

最后,让我们在二维散点图中可视化转换后的葡萄酒训练集,该数据集现在是124 x 2维的矩阵:

colors = ['r''b''g']
markers = ['s''x''o']
for l, c, m in zip(np.unique(y_train), colors, markers):
    plt.scatter(X_train_pca[y_train==l, 0], 
                X_train_pca[y_train==l, 1], 
                c=c, label=l, marker=m) 
plt.xlabel('PC 1')
plt.ylabel('PC 2')
plt.legend(loc='lower left')
plt.show()

image-20240730213228605

从上述图可以看到,数据在x轴(即第一个主成分)上的分布比在y轴(第二个主成分)上更为广泛,这与我们之前创建的解释方差比的图一致。从上个图我们也可以直观地看到,一个线性分类器能够很好地分离这些类别。

尽管我们在前面的散点图中为了说明目的编码了类别标签信息,但我们必须记住,PCA是一种无监督技术,它不使用任何类别标签信息。

scikit-learn实现PCA

尽管上一节中的详细方法帮助我们了解了PCA的内部工作原理,但现在将讨论如何使用scikit-learn中PCA类。

PCA类是scikit-learn的变换器类之一,我们首先使用训练数据拟合模型,然后使用相同的模型参数对训练数据和测试数据集进行变换。

我们在葡萄酒训练数据集上使用PCA类,并通过逻辑回归对转换后的样本进行分类。

from sklearn.linear_model import LogisticRegression
from sklearn.decomposition import PCA

# 初始化pca和逻辑回归模型
pca = PCA(n_components=2)
lr = LogisticRegression(multi_class='auto', solver='liblinear')

# 13维数据投影为二维数据
X_train_pca = pca.fit_transform(X_train_std)
X_test_pca = pca.transform(X_test_std)
lr.fit(X_train_pca, y_train)

现在,使用自定义的 plot_decision_regions 函数可视化决策区域:

from matplotlib.colors import ListedColormap

def plot_decision_regions(X, y, classifier, resolution=0.02):
    # 设置标签和对应的颜色
    markers = ('s''x''o''^''v')
    colors = ('red''blue''lightgreen''gray''cyan')
    cmap = ListedColormap(colors[:len(np.unique(y))])

    # plot the decision surface
    x1_min, x1_max = X[:, 0].min() - 1, X[:, 0].max() + 1
    x2_min, x2_max = X[:, 1].min() - 1, X[:, 1].max() + 1
    xx1, xx2 = np.meshgrid(np.arange(x1_min, x1_max, resolution),
                           np.arange(x2_min, x2_max, resolution))
    Z = classifier.predict(np.array([xx1.ravel(), xx2.ravel()]).T)
    Z = Z.reshape(xx1.shape)
    plt.contourf(xx1, xx2, Z, alpha=0.4, cmap=cmap)
    plt.xlim(xx1.min(), xx1.max())
    plt.ylim(xx2.min(), xx2.max())

    # plot class samples
    for idx, cl in enumerate(np.unique(y)):
        plt.scatter(x=X[y == cl, 0], 
                    y=X[y == cl, 1],
                    alpha=0.6
                    c=[cmap(idx)],
                    edgecolor='black',
                    marker=markers[idx], 
                    label=cl)# plot decision regions for training set


plot_decision_regions(X_train_pca, y_train, classifier=lr)
plt.xlabel('PC 1')
plt.ylabel('PC 2')
plt.legend(loc='lower left')
plt.show()

image-20240730220144998

通过执行前面的代码,我们看到了训练数据在两个主成分轴上的决策区域。

为了完整起见,让我们也绘制转换后的测试数据集上的逻辑回归决策区域,以查看它是否能够很好地分离类别:

# 绘制测试边界的决策边界
plot_decision_regions(X_test_pca, y_test, classifier=lr)
plt.xlabel('PC1')
plt.ylabel('PC2')
plt.legend(loc='lower left')
plt.show()

image-20240730220331606

上述代码绘制了测试集的决策区域后,我们可以看到逻辑回归在这个小的二维特征子空间上表现得相当好,仅在测试数据集中错误分类了很少的样本。

如果我们对不同主成分的解释方差比感兴趣,可以简单地将PCA类初始化时的 n_components 参数设置为 None,这样就会保留所有主成分,然后可以通过 explained_variance_ratio_ 属性访问解释方差比:

pca = PCA(n_components=None)
X_train_pca = pca.fit_transform(X_train_std)
# 打印所有特征的解释方差比
pca.explained_variance_ratio_

请注意,我们在初始化PCA类时将 n_components 设置为 None,这样它将返回所有主成分,并按顺序排列,而不是执行降维。

结束语

希望你喜欢这篇主成分分析(PCA)降维的教程,本文涵盖了PCA算法背后的数学原理、如何用Python一步步执行PCA,以及如何使用scikit-learn实现PCA。

欢迎扫码关注:

机器学习实战
多名大厂算法工程师共同运营,主要专注机器学习算法、深度学习算法、计算机视觉等领域技术干货分享,一天进步一点点
 最新文章