主成分分析
与特征选择类似,我们可以使用特征提取 (Feature Extraction) 来减少数据集中特征的数量。其区别在于,特征选择保留原始特征,而特征提取则将数据转换或投影到新的特征空间。本文介绍主成分分析 (Principal Component Analysis,PCA),它是一种无监督的线性变换技术,主要特点是特 征提取和降维。
算法思想
PCA 广泛应用于不同领域,帮助我们根据特征之间的相关性来识别数据中的模式。其原理是找到高维数据中最大方差的方向,并将其投影到与原始维数相同或更少维数的新的正交空间上。
新的子空间的正交轴 (即主成分) 可以被解释为在新的特征轴彼此正交的约束下的最大方差的方向,如下图所示:
在上图中, 和 是原始特征轴,PC1 和 PC2 是数据集的第一主成分和第二主成分。
PCA 构造了一个维度变换矩阵 ,将高维空间向量 映射到低维空间向量 :
维度变换后,第一主成分将具有最 大的方差,第二主成分将具有第二大的方差,以此类推。这些主成分必须均不相关 (即使输入特征之间有相关性),数学表示为主成分向量之间两两正交。
需要注意的是,PCA 方向对数据缩放非常敏感。举例来说,假设一个特征在 [-100,100] 之间平均分布,另外一个特征在 [-1,1] 之间平均分布,PCA 会认为前一个特征重要得多。因此在运用 PCA 时,需要使用标准化将输入特征缩放到相同的尺度。
PCA 的算法流程的简单介绍如下:
- 对输入的 维数据集 进行标准化。
- 构造协方差矩阵。
- 计算协方差矩阵的特征向量和特征值。
- 对特征值进行降序排序,对相应的特征向量进行排序。
- 提取 个最大特征值对应的特征向量,其中 () 是新特征子空间的维数。
- 使用提取的 个特征向量构造投影矩阵 。
- 使用投影矩阵 对 进行变换,得到新的 维特征子空间。
数据导入和标准化
导入 Wine 数据集,第 1 列为其分类标签,第 2~14 列为特征。分割数据集为 70% 的训练集和 30% 的测试集:
import pandas as pd
from sklearn.model_selection import train_test_split
df_wine = pd.read_csv(
"https://archive.ics.uci.edu/ml/machine-learning-databases/wine/wine.data",
header=None,
)
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, random_state=0, stratify=y
)
对训练集和测试集进行标准化:
from sklearn.preprocessing import StandardScaler
sc = StandardScaler()
sc.fit(X_train)
X_train_std = sc.transform(X_train)
X_test_std = sc.transform(X_test)
协方差矩阵的构造
接下来引入协方差矩阵。对于 的两个特征向量 (机器学习中通常用列向量表示) 和 ,其协方差如下:
其中 和 分别代表 和 平均值。对于 PCA 的样本,在标准化后这两个平均值都为 0。
协方差 可以表示 和 的相关程度:
- 若 大于 0,表明这两个特征正相关,即一个特征随另外一个特征的变大而变大。
- 若 大于 0,表明这两个特征负相关,即一个特征随另外一个特征的变大而变小。
- 若 等于 0,表明这两个特征不相关,即一个特征变化不受另外一个特征的变化影响。此时这两个特征向量两两正交。
接下来构建协方差矩阵 (这是大写的希腊字母 Sigma,不要与求和符号 混淆)。以 3 个特征为例,其协方差矩阵如下:
协方差矩阵 是实对称矩阵,矩阵对角线为特征的方差,其余为特征之间的协方差。 的特征向量表示主成分 (即具有最大方差的方向),对应的特征值为主成分的大小。
Wine 数据集有 13 个特征,我们将从 13×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
和 13 个特征向量 (列向量) 组成的矩阵 eigen_vecs
。
解释方差占比
把 维空间压缩到 维空间,需要从中挑选包含信息最多 (方差最大) 的 个特征向量 (主成分)。特征值反映了特征向量的大小,需要降序排序特征值,然后提取前 个特征值对应的特征向量。
在提取特征之前,为了观察特征的方差之间的差异,这里先对特征值的解释方差占比 (explained variance ratio) 进行绘图:
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%,第二主成分大约占了总方差的 20%,这两个特征值解释了接近 60% 的数据集方差。
需要注意的是,整个过程中没有使用 y_train
。PCA 属于无监督学习,在分析和提取主成分的过程中从来不使用类的标签。
特征提取
本节以 Wine 数据集进行演示特征提取的最后几个步骤:
- 提取 个最大特征值对应的特征向量,其中 () 是新特征子空间的维数。
- 使用提取的 个特征向量构造投影矩阵 ,
- 使用投影矩阵 对 进行变换,得到新 的 维特征子空间。
简单来说,就是按照特征值降序将特征向量进行排序,从特征向量中构造投影矩阵,并使用投影矩阵将数据转换到低维子空间。
特征向量排序:
# Make a list of (eigenvalue, eigenvector) tuples
eigen_pairs = [
(np.abs(eigen_vals[i]), eigen_vecs[:, i]) for i in range(len(eigen_vals))
]
# Sort the (eigenvalue, eigenvector) tuples from high to low
eigen_pairs.sort(key=lambda k: k[0], reverse=True)
以两个主成分为例,构造投影矩阵:
注意,为了后面的绘制二维散点图,因此这里只挑选了两个主成分。实际中应该根据效率性能的平衡来确定主成分的个数。
w = np.hstack((eigen_pairs[0][1][:, np.newaxis], eigen_pairs[1][1][:, np.newaxis]))
使用投影矩阵转换原始数据集:
其中 为原始数据集, 为其中的单个样本; 为变换后的数据集, 为其中的单个样本。
X_train_pca = X_train_std.dot(w)
原始的 Wine 数据集有 13 维,通过特征矩阵 后降为 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()
从下图可以看到,数据沿着 轴 (第一主成分) 比沿着 轴 (第二主成分) 更加分散,与上节中第一主成分的方差较大相吻合。另外,降维后的分类数据仍保留其特征,可以用线性分类器进行分类。
需要再次强调,PCA 是一种无监督的技术,在整个 PCA 过程中不使用任何类标签信息。
Scikit-learn 实现
边界绘图函数
plot_decision_regions
见之前的文章。
通过 sklearn.decomposition.PCA 将测试集和训练集降至二维,然后使用逻辑回归对降维后的测试集进行线性分类:
from sklearn.linear_model import LogisticRegression
from sklearn.decomposition import PCA
pca = PCA(n_components=2)
lr = LogisticRegression()
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(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)
print(pca.explained_variance_ratio_)