自制机器学习模型系列
伴随代码库在此!
维度灾难是机器学习中的一个主要问题。随着特征数量的增加,模型的复杂度也会增加。此外,如果没有足够的训练数据,会导致过拟合。
本文将介绍主成分分析(PCA)。首先,我将解释为什么太多的特征是个问题。然后,介绍PCA背后的数学原理以及它的工作原理。此外,PCA将被分解为步骤,并附有可视化的例子和代码片段。此外,将解释PCA的优缺点。最后,这些步骤将封装在一个Python类中以供以后使用。
读者须知:如果您对数学解释不感兴趣,只想看实际例子以及PCA的工作原理,请跳转到章节“实践中的PCA“。如果您只对Python类感兴趣,请跳转到“自制PCA实现“。
太多特征的问题?
看一下图1中的特征空间。有很少的例子来填满整个空间,因此该数据的模型可能无法很好地推广到新的未见过的例子。
如果我们添加另一个特征会发生什么?让我们看看图2中的新功能空间。您可以看到,空白空间比之前的例子更多。随着特征数量的增加,模型将过拟合当前数据。这就是为什么有降低数据维度的技术以缓解这个问题的原因。[1]
PCA的目标是什么?
简单来说,PCA的目的是提取新的、不相关的低维特征,最大化从原始数据中保留的信息量。在这个背景下,信息量的度量是方差。让我们看看为什么:
这个技术是基于一个假设的,即我们的d维数据点x可以用一组正交基向量的线性组合来表示[1]:
不用担心,我稍后会解释我们如何得到这些基向量的向量。此外,我们可以使用组合中的m个向量(m < d)提取一个表示x̂:
当然,由于特征减少了,我们并没有得到精确的表示,但至少我们尽力最小化信息损失。让我们定义原始示例 x 和近似值 x̂ 之间的均方误差 (MSE):
由于求和使用相同变量和不同截止点,因此差异仅是偏移量:
我们知道,根据我们的起始假设,x 是正交向量的和。因此,这些向量的点积为零,它们的欧几里得范数各为一。因此:
求解重要性值 yi:
将该结果插入其期望值:
我们可以看到,如果 xi 居中(平均值为零),则期望值的结果将是整个数据的协方差矩阵,而这个结果不过是原始空间中的方差。通过选择最大化方差的正确向量 vi,我们将有效地最小化表示误差。
这个正交基是从哪里来的?
如前所述,我们要得到最大化方差的m个向量:
如果我们取整个数据矩阵,则可以看出 vi 是一个投影方向。数据将被投影到较低维度的空间中。
如果我们使用谱分解对协方差矩阵Σ进行对角化,我们会得到:
其中 U 是包含Σ的归一化特征向量的矩阵,Λ是一个对角矩阵,按降序包含Σ的特征值。这是可能的,因为Σ是一个实对称矩阵。
此外,由于Λ仅包含对角线上的非零值,因此我们可以将上述方程重写为:
其中:
注意,U中的向量和向量v都已经被归一化。因此,当每个v与a的平方点积时,我们得到一个值在[0,1]之间,因此w也必须是一个归一化向量:
从这里,有趣的属性出现了。
第一个主成分
回想一下优化问题。由于特征值是有序的,而w必须是一个归一化向量,我们最好的选择是用 w = (1,0,0,…) 得到第一个特征向量。因此,当:
最大化方差的投影方向是与最大特征值相关联的特征向量!
其余的成分
一旦第一个主成分被设置,就会添加一个新的优化问题限制:
这意味着新的v2分量必须与前一个分量u1的特征向量正交,以免信息冗余。可以证明,所有d个成分都对应于Σ的d个规范化特征向量,这些向量与特征值按降序排列。请查看这些笔记以获取此主张的正式证明[2]。
实践中的PCA
从上述理论描述中,可以描述出获取数据集的主要成分所需的步骤。让初始数据集成为以下2D正态分布的随机样本:
from scipy import statsmean = [3,3]var = [[6, 3], [3, 3.5]]n = 100data_raw = np.random.multivariate_normal(mean, var, 100)
1. 将数据居中
第一步是将数据云移动到坐标系的原点,以使数据具有零均值。通过从数据集中的每个点中减去样本均值来执行此步骤。
import numpy as npdata_centered = data_raw - np.mean(data_raw, axis=0)
2. 计算协方差矩阵
上述定义的方差是总体协方差矩阵Σ。在实践中,我们无法获得该信息,因为我们只有一个样本。因此,我们可以使用样本协方差S来近似该参数。
请记住,数据已经居中。因此:
我们可以使用矩阵乘法来简洁地表示这一点。这也可以帮助我们向量化计算:
cov_mat = np.matmul(data_centered.T, data_centered)/(len(data_centered) - 1)# > array([[5.62390186, 2.47275007],# > [2.47275007, 3.19395349]])
在代码中将转置矩阵作为第一个参数传递的原因是,在数据矩阵的数学公式中,特征在行中而主体在列中。在实现中,几乎所有系统中,事件、主体、日志等都是按行存储的,因此相反的情况会发生。
3. 对协方差矩阵执行特征值分解
使用scipy中的eig()
计算特征值和特征向量a:
from scipy.linalg import eigheigvals, eigvecs = eigh(cov_mat)# 对特征值和特征向量排序indices = eigvals.argsort()[::-1]eigvals, eigvecs = eigvals[indices], eigvecs[:,indices]eigvecs# > array([[-0.82348021, 0.56734499],# > [-0.56734499, -0.82348021]])
正如之前所解释的,特征值代表主成分的方差,而特征向量是投影方向:
可以看出,使用主成分的方向创建了一个新的坐标系。此外,必须存储特征值和特征向量以便以后转换新数据。
4. 强制确定性
特征向量的系数始终相同,只是它们的符号不同。PCA可以有多个有效的方向。因此,我们需要通过取特征向量矩阵和其每个列中绝对值最大的符号来强制确定性结果。
max_abs_cols = np.argmax(np.abs(eigvecs), axis=0)signs = np.sign(eigvecs[max_abs_cols, range(eigvecs.shape[1])])eigvecs = eigvecs*signseigvecs# > array([[ 0.82348021, -0.56734499],# > [ 0.56734499, 0.82348021]])
5. 提取新特征
通过在原始特征空间中的每个点和特征向量之间执行点积来提取每个新特征(主成分):
new_features = np.dot(data_centered, eigvecs)
对于这个特定的例子,在计算完组件之后,空间中的新点如下所示:
请注意,这个结果基本上是原始点云的旋转,使属性不相关。
6. 降低维度
到目前为止,为了以视觉方式理解主成分,完全计算了主成分。剩下的是选择需要多少个组件。我们求助于特征值来完成此任务,因为它们代表每个主成分的方差。
组件 i 所持有的方差的比率为:
选择m个主成分所保留的方差比例如下:
如果我们将示例中每个组件的方差可视化,我们会得到以下结果:
#每个组件的方差条形图plt.bar( [f"PC_{i}" for i in range(1,len(eigvals)+1)], eigvals/sum(eigvals))#由m个组件持有的百分比为线性图plt.plot( [f"PC_{i}" for i in range(1,len(eigvals)+1)], np.cumsum(eigvals)/sum(eigvals), color='red')plt.scatter( [f"PC_{i}" for i in range(1,len(eigvals)+1)], np.cumsum(eigvals)/sum(eigvals), color='red')
在这种情况下,PC1代表原始数据80%的方差,剩余20%属于PC2。此外,我们可以选择仅使用第一个主成分,此时数据将如下所示:
这是数据在第一个特征向量方向上的投影。现在看起来并不是很有用。如果我们选择属于三个类别的数据,PCA会怎样呢?
多类数据上的PCA
让我们创建一个可以线性可分的三类数据集:
from sklearn.datasets import make_blobsX, y = make_blobs()plt.scatter(X[:,0], X[:,1],c=y)plt.legend()plt.show()
如果我们对上述数据应用PCA,则会得到主成分的绘图:
这是第一个主成分的绘图(数据在对应于最大特征值的特征向量方向上的投影):
它奏效了!数据仍然可以轻松地被线性模型分离。
优点和缺点
像科学中的所有事物一样,没有万能的方法。在使用PCA处理实际数据之前,请考虑以下优点和缺点列表。
PCA的优点
- 降维:PCA可将高维数据降到较低维度空间,同时保留大部分重要信息。这对数据可视化、计算效率和处理维度灾难都很有用。
- 去相关性:PCA将原始变量转换为一组新的不相关变量,称为主成分。这种去相关化简化了分析,可以提高后续机器学习算法的性能,这些算法假定特征之间独立。
- 降噪:通过PCA获得的低维表示倾向于过滤噪声并关注数据中最显著的变化。这可以增强信噪比并改善后续分析的鲁棒性。
PCA的缺点
- 线性假设:PCA假设底层数据关系是线性的。如果数据具有复杂的非线性关系,PCA可能无法捕捉到最有意义的变化,可能提供次优结果。
- 可解释性:从PCA获得的主成分是原始特征的线性组合。将主成分与原始变量相关联并理解它们的确切含义可能很困难。
- 对比例尺敏感:PCA对输入变量的比例尺敏感。如果变量具有不同的比例尺,具有更大方差的变量可能会支配分析,从而可能导致偏倚的结果。正确的特征缩放对于获得可靠的PCA结果至关重要。
- 异常值:PCA对异常值敏感,因为它专注于捕捉数据中的方差。异常值可以显着影响主成分并扭曲结果。
自制PCA实现
现在,我们已经介绍了主成分分析的细节,剩下的就是创建一个封装前述行为并可在将来的问题中重用的类。
对于此实现,将使用scikit-learn接口,其具有以下方法:
fit()
transform()
fit_transform()
构造函数
不需要复杂的逻辑。构造函数只需定义转换数据将具有的组件(特征)数量。
import numpy as npfrom scipy.linalg import eighclass PCA: """主成分分析。 """ def __init__(self, n_components): """PCA类的构造函数。 参数: =========== n_components:int 转换数据的维数。 必须小于或等于n_features。 """ self.n_components = n_components self._fit_instance = False
拟合方法
拟合方法将应用上一节中的步骤1-4。
- 居中数据
- 计算协方差矩阵
- 计算特征值、特征向量并对它们进行排序
- 通过翻转特征向量强制确定性
它还将存储特征值和向量以及样本均值作为对象属性,以便稍后转换新数据。
def fit(self, X): """计算特征向量以便稍后转换数据 参数: =========== X:[n_examples,n_features]形状的np.array 数据矩阵 返回: =========== None """ # 拟合数据的均值并使其居中 self.mean = np.mean(X, axis=0) X_centered = X - self.mean # 计算协方差矩阵 cov_mat = np.matmul(X_centered.T, X_centered)/(len(X_centered) - 1) # 计算特征值、特征向量并排序 eigenvalues, eigenvectors = eigh(cov_mat) self.eigenvalues, self.eigenvectors = self._sort_eigen(eigenvalues, eigenvectors) # 获取解释的方差比 self.explained_variance_ratio = self.eigenvalues/np.sum(self.eigenvalues) # 通过翻转特征向量强制确定性 self.eigenvectors = self._flip_eigenvectors(self.eigenvectors)[:, :self.n_components] self._fit_instance = True
变换方法
它将应用步骤1、5和6:
- 使用存储的样本均值对新数据进行居中处理
- 提取新的主成分特征
- 通过选择
n_components
维度来减少维数。
def transform(self, X): """在特征向量的方向上投影数据。 参数: =========== X: np.array of shape [n_examples, n_features] 数据矩阵 返回: =========== pcs: np.array[n_examples, n_components] 来自PCA的新的不相关特征。 """ if not self._fit_instance: raise Exception("PCA必须先对数据进行拟合!调用fit()") X_centered = X - self.mean return np.dot(X_centered, self.eigenvectors)
拟合变换方法
为了实现简单,此方法将首先应用fit()
函数,然后应用transform()
函数。我相信你可以想出一个更聪明的定义。
def fit_transform(self, X): """拟合PCA并转换数据。 """ self.fit(X) return self.transform(X)
辅助函数
这些方法被定义为单独的组件,而不是在fit()
函数中应用所有步骤,以使代码更易于阅读和维护。
def _flip_eigenvectors(self, eigenvectors): """通过改变特征向量的符号来强制确定性。 """ max_abs_cols = np.argmax(np.abs(eigenvectors), axis=0) signs = np.sign(eigenvectors[max_abs_cols, range(eigenvectors.shape[1])]) return eigenvectors*signs def _sort_eigen(self, eigenvalues, eigenvectors): """按降序排序特征值及其相应的特征向量。 """ indices = eigenvalues.argsort()[::-1] return eigenvalues[indices], eigenvectors[:, indices]
测试类
让我们使用我们的PCA
类来进行先前的示例:
from pca import PCA# 使用我们的PCA实现pca = PCA(n_components=1)X_transformed = pca.fit_transform(X)# 绘制第一个PCplt.scatter(X_transformed[:,0], [0]*len(X_transformed),c=y)plt.legend()plt.show()
结论
对于具有少量数据的许多特征可能是有害的,并且很可能会导致过拟合。主成分分析是一种可以帮助缓解这个问题的工具。它是一种减少维度的技术,通过以尽可能保留原始可变性的方式找到数据的投影方向,并且所得到的特征是不相关的。此外,可以测量每个新特征或主成分解释的方差。然后,用户可以选择多少个主成分以及多少方差足够完成任务。最后,请确保首先了解您的数据,因为PCA使用可以进行线性分离并且可能对异常值敏感的样本。
参考文献
[1] Fernández, A. Dimensionality Reduction. Universidad Autónoma de Madrid. Madrid, Spain. 2022.
[2] Berrendero, J. R. Regresión lineal con datos de alta dimensión. Universidad Autónoma de Madrid. Madrid, Spain. 2022.
在LinkedIn上与我联系!