PCA 算法也叫主成分分析(principal components analysis),主要是用于数据降维的。
为什么要进行数据降维?因为实际情况中我们的训练数据会存在特征过多或者是特征累赘的问题,比如:
PCA算法就是用来解决这种问题的,其核心思想就是将 n 维特征映射到 k 维上(k < n),这 k 维是全新的正交特征。我们将这 k 维成为主元,是重新构造出来的 k 维特征,而不是简单地从 n 维特征中取出其余 n-k 维特征。
PCA原理




关于为什么协方差的特征向量就是 k 维理想特征,有3个理论,分别是:
- 最大方差理论
- 最小错误理论
- 坐标轴相关度理论
最大方差理论
信号处理中认为信号具有较大的方差,噪声有较小的方差,信噪比就是信号与噪声的方差比,越大越好。因此我们认为,最好的 k 为特征既是将 n 维样本点转换为 k 维后,每一维上的样本方差都很大
PCA 处理图解如下:

降维转换后:

上图中的直线就是我们选取的特征向量,上面实例中PCA的过程就是将空间的2维的点投影到直线上。
那么问题来了,两幅图都是PCA的结果,哪一幅图比较好呢?
根据最大方差理论,答案是左边的图,其实也就是样本投影后间隔较大,容易区分。
其实从另一个角度看,左边的图每个点直线上的距离绝对值之和比右边的每个点到直线距离绝对值之和小,是不是有点曲线回归的感觉?其实从这个角度看,这就是最小误差理论:选择投影后误差最小的直线。
再回到上面的左图,也就是我们要求的最佳的 u ,前面说了,最佳的 u 也就是最佳的曲线,它能够使投影后的样本方差最大或者是误差最小。
另外,由于我们前面PCA算法第一步的时候已经执行对样本数据的每一维求均值,并让每个数据减去均值的预处理了,所以每个特征现在的均值都为0,投影到特征向量上后,均值也为0.因此方差为:

最后的等式中中间的那部分其实就是样本方差的协方差矩阵(xi 的均值为 0)

由于 u 是单位向量,得到

上式两边痛乘以 u,得到:


于是我们得到

最佳投影直线就是特征值 λ 最大是对应的特征向量,其次是 λ 第二大对应的特征向量(求解的到的特征向量都是正交的)。其中 λ 就是我们的方差,也对应了我们前面的最大方差理论,也就是找到能够使投影后方差最大的直线。
PCA 的计算过程
第一步:分别求 x 和 y 的平均值,然后对所有的样例都减去对应的均值
注意,此时我们一般应该在对特征进行方差归一化,目的是让每个特征的权重都一样
第二步:求特征协方差矩阵
公式如下:
第三步:求解协方差矩阵的特征值和特征向量
第四步:将特征值从大到小进行排序,选择其中最大的 k 个,然后将其对应的 k 个特征向量分别作为列向量组成特征矩阵
第五步: 将样本点投影到选取的特征向量上
e.g.假设样本列数为 m ,特征数为 n ,减去均值后的样本矩阵为 DataAdjust(m*n),协方差矩阵为 n*n ,选取 k 个特征向量组成后的矩阵为 EigenVectors(n*k),则投影后的数据 FinalData 为:
FinalData (m*k) = DataAdjust(m*n) X EigenVectors(n*k)
Python代码实现

PCA算法源码
语言:Python
函数库:Numpy
根据上面提到的一般步骤来实现PCA算法
(1)零均值化
假如原始数据集为矩阵dataMat,dataMat中每一行代表一个样本,每一列代表同一个特征。零均值化就是求每一列的平均值,然后该列上的所有数都减去这个均值。也就是说,这里零均值化是对每一个特征而言的,零均值化都,每个特征的均值变成0。实现代码如下:
- def zeroMean(dataMat):
- meanVal=np.mean(dataMat,axis=0)
- newData=dataMat-meanVal
- return newData,meanVal
函数中用numpy中的mean方法来求均值,axis=0表示按列求均值。
该函数返回两个变量,newData是零均值化后的数据,meanVal是每个特征的均值,是给后面重构数据用的。
(2)求协方差矩阵
- newData,meanVal=zeroMean(dataMat)
- covMat=np.cov(newData,rowvar=0)
numpy中的cov函数用于求协方差矩阵,参数rowvar很重要!若rowvar=0,说明传入的数据一行代表一个样本,若非0,说明传入的数据一列代表一个样本。因为newData每一行代表一个样本,所以将rowvar设置为0。
covMat即所求的协方差矩阵。
(3)求特征值、特征矩阵
调用numpy中的线性代数模块linalg中的eig函数,可以直接由covMat求得特征值和特征向量:
- eigVals,eigVects=np.linalg.eig(np.mat(covMat))
eigVals存放特征值,行向量。
eigVects存放特征向量,每一列带别一个特征向量。
特征值和特征向量是一一对应的
(4)保留主要的成分[即保留值比较大的前n个特征]
第三步得到了特征值向量eigVals,假设里面有m个特征值,我们可以对其排序,排在前面的n个特征值所对应的特征向量就是我们要保留的,它们组成了新的特征空间的一组基n_eigVect。将零均值化后的数据乘以n_eigVect就可以得到降维后的数据。代码如下:
- eigValIndice=np.argsort(eigVals)
- n_eigValIndice=eigValIndice[-1:-(n+1):-1]
- n_eigVect=eigVects[:,n_eigValIndice]
- lowDDataMat=newData*n_eigVect
- reconMat=(lowDDataMat*n_eigVect.T)+meanVal
- return lowDDataMat,reconMat
代码中有几点要说明一下,首先argsort对特征值是从小到大排序的,那么最大的n个特征值就排在后面,所以eigValIndice[-1:-(n+1):-1]就取出这个n个特征值对应的下标。【python里面,list[a:b:c]代表从下标a开始到b,步长为c。】
reconMat是重构的数据,乘以n_eigVect的转置矩阵,再加上均值meanVal。
OK,这四步下来就可以从高维的数据dataMat得到低维的数据lowDDataMat,另外,程序也返回了重构数据reconMat,有些时候reconMat课便于数据分析。
贴一下总的代码:
-
- def zeroMean(dataMat):
- meanVal=np.mean(dataMat,axis=0)
- newData=dataMat-meanVal
- return newData,meanVal
-
- def pca(dataMat,n):
- newData,meanVal=zeroMean(dataMat)
- covMat=np.cov(newData,rowvar=0)
-
- eigVals,eigVects=np.linalg.eig(np.mat(covMat))
- eigValIndice=np.argsort(eigVals)
- n_eigValIndice=eigValIndice[-1:-(n+1):-1]
- n_eigVect=eigVects[:,n_eigValIndice]
- lowDDataMat=newData*n_eigVect
- reconMat=(lowDDataMat*n_eigVect.T)+meanVal
- return lowDDataMat,reconMat
- def percentage2n(eigVals,percentage):
- sortArray=np.sort(eigVals)
- sortArray=sortArray[-1::-1]
- arraySum=sum(sortArray)
- tmpSum=0
- num=0
- for i in sortArray:
- tmpSum+=i
- num+=1
- if tmpSum>=arraySum*percentage:
- return num
pca函数也可以重写成百分比版本,默认百分比99%。
- def pca(dataMat,percentage=0.99):
- newData,meanVal=zeroMean(dataMat)
- covMat=np.cov(newData,rowvar=0)
- eigVals,eigVects=np.linalg.eig(np.mat(covMat))
- n=percentage2n(eigVals,percentage)
- eigValIndice=np.argsort(eigVals)
- n_eigValIndice=eigValIndice[-1:-(n+1):-1]
- n_eigVect=eigVects[:,n_eigValIndice]
- lowDDataMat=newData*n_eigVect
- reconMat=(lowDDataMat*n_eigVect.T)+meanVal
- return lowDDataMat,reconMat