本文介绍主成分分析(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()
接下来,我们将把葡萄酒数据分成独立的训练集和测试集,使用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个特征向量和特征值。
现在,进入我们的第三步,让我们获得协方差矩阵的特征值和特征向量。特征向量 满足以下条件:
这里,是标量,也就是协方差矩阵的特征值。由于手动计算特征向量和特征值是一项相当繁琐且复杂的任务,我们将使用NumPy
的linalg.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()
如上生成的图表显示,仅第一个主成分就占了大约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()
从上述图可以看到,数据在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()
通过执行前面的代码,我们看到了训练数据在两个主成分轴上的决策区域。
为了完整起见,让我们也绘制转换后的测试数据集上的逻辑回归决策区域,以查看它是否能够很好地分离类别:
# 绘制测试边界的决策边界
plot_decision_regions(X_test_pca, y_test, classifier=lr)
plt.xlabel('PC1')
plt.ylabel('PC2')
plt.legend(loc='lower left')
plt.show()
上述代码绘制了测试集的决策区域后,我们可以看到逻辑回归在这个小的二维特征子空间上表现得相当好,仅在测试数据集中错误分类了很少的样本。
如果我们对不同主成分的解释方差比感兴趣,可以简单地将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。
欢迎扫码关注: