简介
本文的目的是为了能够让读者对PCA有一个清晰的理解,并且能够用代码自己实现其算法。
PCA是一种较常用的统计分析、简化数据集的方法,在人脸识别和图像压缩等领域都有应用,同时也是在高维数据中寻找模式的常用技术。它利用正交变换来对一系列可能相关的变量的观测值进行线性变换,从而投影为一系列线性不相关变量的值,这些不相关变量称为主成分(Principal Components)。具体地,主成分可以看做一个线性方程,其包含一系列线性系数来指示投影方向。
基本数学概念
在介绍PCA之前,我首先会引入在PCA中会用到的一些基础数学概念,方便那些没有对应基础知识的初学者学习,包括:
- 标准差
- 方差
- 协方差
- 特征值和特征向量
这些背景知识对PCA的理解很重要,如果你对这些知识已经比较熟悉了,可以选择直接跳过本节。
标准差
要理解标准差,我们首先需要一个数据集。以美国大选民意调查为例,人口是指全美所有的人,而样本则是全美所有人口的一个子集。统计学的精妙之处在于,通过只抽样测量(通过电话调查或问卷等方式)样本数据,而不需要对全体人口都做调查,就可以计算出比较精确的结果。
下面是一个假设的样本数据集:
我们使用代表整个数据集,使用户代表数据集中的第个元素,我相信大家都知道这个样本数据集的平均值如何计算,计算公式如下:
平均值就是把所有的数字加起来再除以数字的总数。但是它除了告诉我们数据集的中间值以外,并没有提供更多的信息,比如下面两个集合:
简单计算可以知道这两个集合的平均值都是10,但是显然我们可以看出来它们不太一样,比如左边的集合中的数字明显分的比较开,而右边集合中的数字则相对紧凑,即两个数据集合的离散程度不太一样。
下面引入标准差的定义:
由此可知,标准差主要反映一个数据集的离散程度。平均数相同的两组数据,标准差未必相同。它的计算公式如下:标准差(Standard Deviation) ,是离均差平方的算术平均数(即:方差)的算术平方根,用σ表示。标准差也被称为标准偏差,或者实验标准差,在概率统计中最常使用作为统计分布程度上的测量依据。
至于上式的分母为什么是而不是,这个有点点复杂,感兴趣的同学可以参考链接。
总的来说,如果是估算总体方差,根号内除以,如果是估算样本方差,根号内除以。因为我们计算的是样本方差,所以除以。
接着针对上面提供的两个样本集合,我们可以使用python里面的numpy库快速计算,代码如下:
import numpy as np
X = [0,8,12,20]
Y = [8,9,11,12]
print("集合X的标准差: ", np.std(X, ddof=1))
print("集合Y的标砖茶: ", np.std(Y, ddof=1))
结果如下:
正如意料的那样,第一个集合的标准差会比第二个集合的标准差大很多,原因就是因为它的数据离集合的平均值距离较远,即离散程度较高。
而对于下面的集合:
我们甚至都不用计算就知道它的标准差为0,因为它的全部数据和平均值都是一样的,即数据离散程度为0。
方差
方差是另外一种测量数据离散程度的方式。实际上,它就是标准差的平方,其公式如下:也许你产生一个疑问,既然有了标准差,又来个方差干嘛呢?我以前也有这样的疑问,但是这不是本文的重点,有兴趣的同学可以参考这里。
协方差
上面所讲的两种测量公式都是针对一维数据的,比如全班同学的身高,每个人的数学考试成绩等。但是现实中很多数据集往往不止一维,对于这些高维数据的统计分析的目的是希望发现这些维度之间的关系。
比如,我们有这样一个样本数据,里面包含了班上每一位同学的身高,以及他们的数学成绩。我们可以通过统计分析来发现是否一个学生的身高与它的数学成绩是否包含某种关系。
上述只是个示例,实际上这两者之间可能压根没任何关系。
协方差(Covariance)总是用于衡量两个随机变量的联合变化程度。而方差是协方差的一种特殊情况,即变量与自身的协方差。即如果你衡量一个变量与其自身的联合变化程度,你得到的结果就是方差。
如果你有一个三维数据集,那你可以计算和,和,和之间的协方差。协方差的计算公式其实与方差的计算公式非常类似,为了对比它们之间的差异,我们换一种形式来描述方差计算公式,如下:
你看出来差别了吗?它仅仅是把其中一个换成了,它的含义是,对每一个数据项,将与的差值乘以与的差值,然后全部累加之后除以。
但是它是如何工作的呢?假如我们有以下数据集,它记录了班上每一个同学在数学上花费的时间,以及他们最终的数学成绩,如下图所示:
No. | Hours(H) | Mark(M) |
---|---|---|
0 | 9 | 39 |
1 | 15 | 56 |
2 | 25 | 93 |
3 | 14 | 61 |
4 | 10 | 50 |
5 | 18 | 75 |
6 | 0 | 32 |
7 | 16 | 85 |
8 | 5 | 42 |
9 | 19 | 70 |
10 | 16 | 66 |
11 | 20 | 80 |
你可能会疑惑最后这个计算结果的含义是什么?其实最终的具体值并不重要,重要的是它的符号。
如果这个值是正的,这表明随着其中一维数据的增大,另外一维数据也会相应增大。
如果这个数字是负的,表明随着其中一维数据的增加,另外一维数据会减少。
如果这个数字是0,则表明这两维数据之间是相互独立的,即其中一维数据的变化并不会影响另外一维。
观察我们的计算结果,它是正的,这表明随着学习时间的增加,对应的成绩也会相应提高。
当然你也许会问和是相等的么?回头看一下协方差的计算公式,你会很快得到答案,它们是相等的。因为它们之间的唯一区别在于被代替了而已,而顺序并不重要。
协方差矩阵
协方差总是计算衡量两个随机变量之间的联合变化程度,那如果我们的数据集超过2维了怎么办?比如外面有一个3维数据集,那么我们得对两两维度之间进行协方差计算,即要计算和和。实际上,对于一个n维的数据集,我们需要计算个不同的协方差值。
为了表述方便,比较好的方式是利用一个矩阵来保存每两个随机变量之间的协方差,定义如下:
其中是一个行列的矩阵,代表的是第维。矩阵中的每一个元素代表了两个不同维度之间的协方差值。举个例子,对于第2行,第3列来说,其中保存了第二维和第三维之间的协方差值。
对于上面距离的3维数据集来说,它对应的协方差矩阵大小为3x3,具体定义如下:
观察上述矩阵的主对角线,它计算的某一维与它自身的协方差,故其实就是那一维度的方差,再由于与相等,故上述矩阵是一个对称矩阵。
看起来很复杂对吧,但是实际使用中我们不需要自己计算,使用numpy库可以帮我们快速搞定。下面是针对上文提及的全班学生数学学习时长和成绩的数据计算得到的协方差矩阵,因为数据是2维的,即{Hours, Mark},故最后的协方差矩阵是2x2的。具体代码和结果如下:
import numpy as np
x = np.array([9,15,25,14,10,18,0,16,5,19,16,20])
y = np.array([39,56,93,61,50,75,32,85,42,70,66,80])
# 计算协方差矩阵
covxy = np.cov(x, y)
print(covxy)
结果:
呼~ 人生苦短,我用python。
线性代数
这一节会介绍一些关于PCA使用到的线性代数相关的基础知识。我会尤其介绍对于一个给定矩阵,如何去找到它的特征值和特征向量。当然这个前提是我假设你对矩阵已经有了基本的认识,尤其是矩阵乘法。
先给一下维基百科上对于特征值和特征向量的定义:
对于一个给定矩阵,它的特征向量经过一个线性变化之后,得到的新向量与原来的保持在同一条直线上,但是其长度或者方向也许会改变,即:
其中为标量,即特征向量的长度在该线性变化下缩放的比例,称为其特征值。
概念貌似有点枯涩,我们用一个例子来演示一下,假设我们定义一个矩阵如下:
还有一个向量(先别问我这个向量哪里来的),那么的计算过程如下:
此时我们可以认为就是的特征向量,是特征向量对应的特征值。
这里要注意特征值和特征向量是成对出现的。
这里的矩阵其实可以看做是一种线性变换,它对向量进行了一种变化,而这种变换作用到上的结果相当于将进行了一定比例的伸缩而保持方向不变。
特征值和特征向量有什么性质呢?主要包含以下几个方面:
- 只有方阵才有特征向量
- 不同特征值对应的特征向量是线性无关的
- 同一个特征值可以有多个特征向量,而一个特征向量不能属于不同的特征值
上述第二条性质对后续的PCA是十分有用的,即一个矩阵的特征向量是两两正交的,这一点很重要的原因是它意味着我们可以根据这些正交特征向量来重新表达数据,而不是原始的向量空间,比如轴和轴。
当我们找到特征向量之后,通常会对其单位化。因为我们知道,向量是有方向和大小的量,而我们一般更关注其方向而不是其大小。因此为了标准特征向量,我们通常对其单位化,这样所有的特征向量都有同样的大小1。下面以特征向量来演示下如何单位化:
首先计算出向量的长度,如下:
其次,我们让原向量除以这个长度,得到:
但是对于一个方阵,我们如何找到对应的特征值和特征向量呢?如果是比较小的矩阵,比如3x3大小的,我们可以通过一定的方法将其手算出来。但是对于比较大的矩阵,一般采用复杂的迭代方法来计算,但是这不在本文的范围之内,如果你想了解更多关于特征值和特征向量的内容,可以参考B站的线性代数视频,非常生动形象,强烈推荐。
同样,我们依旧使用numpy来演示如何计算上述矩阵的特征值和特征向量:
import numpy as np
A = np.matrix([[2,3],
[2,1]])
eigvalue, eigvector = np.linalg.eig(A)
print("特征值: ", eigvalue)
print("特征向量: ", eigvector)
结果如下:
PCA
呼~终于来到正题了。
PCA思路
PCA顾名思义,就是要找出数据的主成分,用数据的最主要的方面来代替原始数据。具体的,假如我们的数据集是n维的,共有m个数据,故我们可以用一个的矩阵来表达这个原始数据集。如果我们希望将其降低到维,希望这个个维的数据能够尽可能的代表原始数据集。我们知道从维降低到维肯定是会有损失的,那如何让损失尽可能的小呢?
先看下最简单的情况,当时,也就是我们希望将数据从2维降低到1维。如下图,我们希望找到一个维度方向,它可以代表这两个方向,图中列了两个向量方向和,那么哪一个更优呢?
直观上来看,会比较好,为什么呢?有两种解释:
- 样本点到方向的直线的距离足够近
- 样本点在方向的直线上的投影分的足够开
因此我们可以得到降维的两个标准为:样本点到这个超平面的距离足够近,或者说样本点在这个超平面上的投影能尽可能的分开。
基于最大投影方差的PCA推导
下面基于最大投影方差来推导一下PCA,如果觉得有困难,也可以直接跳过,直接看下一节的PCA算法流程。
假如有个维数据已经进行了中心化,即。
中心化是为了消除不同评价指标之间的量纲影响,处理方法就是用变量减去它的平均值,具体可参考这里。
经过投影变换之后得到的新坐标系为,其中是标准正交基,即。
如果我们把数据从维降到维,即丢弃新数据集中的部分坐标,新的坐标系为,样本点在维坐标系中的投影为。其中是在低维坐标系里第维的坐标。
对于任意的一个样本,在新的坐标系中的投影为,在新的坐标系中的投影方差为,要使得所有样本的投影方差最大,也就是需要最大化的迹,即:
利用拉格朗日函数可以得到:
对求导得,整理下得:
仔细观察上式你发现了什么?也许你啥都没发现,没关系,我们换一个形式,约定,那么上式可以写成:
这不就是特征值和特征向量的定义么?其中为矩阵的个特征向量组成的矩阵,为的若干特征值组成的矩阵,特征值在主对角线上,其余位置为0。
当需要将数据集从维降低到维时,我们只需要计算特征值矩阵,并且找到最大的前个特征值对应的特征向量。这个特征向量组成的矩阵即为我们需要的矩阵。
对于原始数据集,我们只需要用,就可以把原始数据集从维降到维。
PCA算法流程
根据上述内容可以看出,求样本的维的主成分其实就是求协方差矩阵的前个特征值对应的特征向量,然后对于每个样本,做以下变换,即达到降维的目的。
下面是具体的算法流程:
- 对所有的样本进行中心化操作
- 计算样本的协方差矩阵
- 求出矩阵的特征值和特征向量
- 取出最大的个特征值对应的特征向量然后分别对其单位化,然后组成特征向量矩阵
- 对样本集中的每一个样本,通过变换转换到新的向量空间中。
- 得到新的样本集
PCA实战
老规矩,举一个例子来说明PCA的过程,为了方便可视化,我们选择2维的数据来演示。
1.样本中心化
假如我们有这样一个数据集,它包含了10个数据,每个数据有2个特征,数据如下:
x | y |
---|---|
2.5 | 2.4 |
0.5 | 0.7 |
2.2 | 2.9 |
1.9 | 2.2 |
3.1 | 3.0 |
2.3 | 2.7 |
2 | 1.6 |
1 | 1.1 |
1.5 | 1.6 |
1.1 | 0.9 |
这么看不太明显,我们借助python的matplotlib将其绘制出来如下,代码如下:
import matplotlib.pyplot as plt
x = [2.5, 0.5, 2.2, 1.9, 3.1, 2.3, 2.0, 1.0, 1.5, 1.1]
y = [2.4, 0.7, 2.9, 2.2, 3.0, 2.7, 1.6, 1.1, 1.6, 0.9]
plt.figure('Draw')
plt.scatter(x,y)#绘制散点图
plt.xlabel('x')
plt.ylabel('y')
plt.xlim(-3,3)
plt.ylim(-3,3)
plt.grid(True, linestyle='-.')
plt.plot(x, y, 'ro')
plt.show()
结果:
接下来我们进行样本中心化:
import numpy as np
x = [2.5, 0.5, 2.2, 1.9, 3.1, 2.3, 2.0, 1.0, 1.5, 1.1]
y = [2.4, 0.7, 2.9, 2.2, 3.0, 2.7, 1.6, 1.1, 1.6, 0.9]
XXT = np.zeros([10,2]) #创建一个10x2的零矩阵
XXT[:,0] = x
XXT[:,1] = y
#计算每一列的均值
meanVals = np.mean(XXT, axis=0)
#中心化
XXT = XXT - meanVals
print(XXT)
结果如下:
x | y |
---|---|
0.69 | 0.49 |
-1.31 | -1.21 |
0.39 | 0.99 |
0.09 | 0.29 |
1.29 | 1.09 |
0.49 | 0.79 |
0.19 | -0.31 |
-0.81 | -0.81 |
-0.31 | -0.31 |
-0.71 | -1.01 |
同样,我们将其绘制出来:
plt.figure('Draw')
plt.scatter(XXT[:,0],XXT[:,1])#绘制散点图
plt.xlim(-3,3)
plt.ylim(-3,3)
plt.xlabel('x')
plt.ylabel('y')
plt.grid(True, linestyle='-.')
plt.plot(XXT[:,0],XXT[:,1], 'ro', color='red')
结果如下:
可以看到分子部分包含,在我们进行中心化操作之后,,分子不就变成了,这明显使得计算变简单了。
另外我不知道你有没有注意到,上一节PCA算法流程的第二步:
计算样本的协方差矩阵,
为什么就可以计算出协方差矩阵了?看上面公式不是很复杂么?其实正是因为我们对数据做了中心化之后才可以这么计算的。还记得上面列举的学生在数学上花的时间与最终考试成绩的数据集么,我们当时使用了numpy去计算协方差矩阵,一行代码就搞定了。现在我先对其去中心化,再使用来计算它的协方差矩阵,你看看两种方式的计算结果一不一样,代码如下:
import numpy as np
x = np.array([9,15,25,14,10,18,0,16,5,19,16,20])
y = np.array([39,56,93,61,50,75,32,85,42,70,66,80])
# 计算协方差矩阵
covxy = np.cov(x, y)
print(covxy)
X = np.zeros([12,2]) #创建一个12x2的零矩阵
X[:,0] = x
X[:,1] = y
meanVals = np.mean(X, axis=0)
X = X - meanVals #中心化
XT = X.T #得到矩阵的转置
# 计算协方差矩阵
XTX = np.matrix(XT)*np.matrix(X)/(len(x)-1)
print(XTX)
结果:
很明显,结果是一样的,那么说明中心化确实可以给计算带来很大的方便。
那肯定一样啊,结果不一样,我也不敢放上来呀。
2.计算协方差矩阵的特征值和特征向量
直接在原始矩阵XXT上计算协方差计算,使用以下代码:
covMat = np.cov(XXT, rowvar=0)
eigVals, eigVects = np.linalg.eig(np.mat(covMat))
print("特征值: ", eigVals)
print("特征向量: ", eigVects)
结果如下:
可以看到我们得到了两个特征值和对应的特征向量,注意这里得到的特征向量已经是单位化之后的特征向量。
也许你会问这代表着什么呢?那我画出来给你看看好了, 看图:
我画了两条直线,,分别用绿色和黑色表示,这两条直线的方程如下:
细心的你估计发现了,这两条直线的斜率不就是分别根据上面两个特征向量计算得到的么?
注意黑色的直线,你可以看到它直接从数据点中间穿了过去,怎么感觉有点线性回归拟合直线那味儿呢?
总之,从上图我们可以看出这个特征向量向我们展示了数据点与黑色直线的关系,直觉告诉我们,如果我们把数据点映射到黑色直线指向的方向,那么效果会是比较好的。
对应的特征值和特征向量分别是和,可以看到代表这条直线方向的特征向量对应的特征值是比较大的那一个。
实际上,具有最大特征值的特征向量代表了数据集中的主要部分(principal component),这也是PCA的来源,即只选择最主要的部分。因此我们需要对特征值进行一个排序,然后忽略掉不怎么重要的成分,这当然会造成信息损失,但是如果被丢弃的特征值比较小,那其实也没有丢掉太多,因为它们表示的也不是最重要的部分。确切地说,如果你的数据集有维,那么你可以计算出个特征值和特征向量,我们可以选择最大的前个作为主要部分,而丢弃掉剩余的部分,那么最后的数据就是维的。
3.得到输出集
因为我们的数据集是2维的,我们要降低到1维,故我们选择特征值较大的特征向量来进行投影。
这里给大家梳理一下转换流程,令代表中心化后的样本数据,它是的大小,在上面的例子中,。我们的特征向量矩阵原本是的大小,我们只选取了其中具有最大特征值的1个特征向量构成投影矩阵,即大小为,那么最终降维后的数据大小为:
即降维之后的样本数据大小为10x1。
代码如下:
covMat = np.cov(XXT, rowvar=0)
eigVals, eigVects = np.linalg.eig(np.mat(covMat))
print("特征值: ", eigVals)
print("特征向量: ", eigVects)
print(eigVects[:,1]) # 这是具有最大特征值的特征向量
print(XXT.shape)
print(eigVects[:,1].shape)
print(XXT * eigVects[:,1])
结果如下:
故我们成功地实现了PCA降维。
4. 利用Sklean实现PCA降维
肯定有人会说这个过程太麻烦了,我还是喜欢调包,这不,包来了。我们可以用python的机器学习库Sklearn来处理PCA,仅需短短几行代码,我把上面的例子也基于Sklearn实现了一下,代码如下:
from sklearn.decomposition import PCA
x = [2.5, 0.5, 2.2, 1.9, 3.1, 2.3, 2.0, 1.0, 1.5, 1.1]
y = [2.4, 0.7, 2.9, 2.2, 3.0, 2.7, 1.6, 1.1, 1.6, 0.9]
X = np.zeros([10,2]) #创建一个10x2的零矩阵
X[:,0] = x
X[:,1] = y
pca = PCA(n_components=1)
pca.fit(X)
X_new = pca.transform(X)
print(X_new)
结果:
可以看到Sklean输出的结果跟我们上面自己写的是一模一样的哦,相信你看了上面的教程也知道如何自己实现一个PCA算法了,我就不在这里贴出完整代码了,感兴趣的同学可以按照我上面的代码自己试试。
总结
PCA作为一个非监督学习的降维方法,它只需要特征值分解,就可以对数据进行压缩,去噪。因此在实际场景应用很广泛。
PCA算法优点:
- 仅仅需要以方差衡量信息量,不受数据集以外的因素影响。
- 各主成分之间正交,可消除原始数据成分间的相互影响的因素
- 计算方法简单,主要运算是特征值分解,易于实现。
PCA算法缺点:
- 主成分各个特征维度的含义具有一定的模糊性,不如原始样本特征的解释性强
- 方差小的非主成分也可能含有对样本差异的重要信息,因降维丢弃可能对后续数据处理有影响。
参考
- https://baike.baidu.com/item/%E6%A0%87%E5%87%86%E5%B7%AE
- https://www.zhihu.com/question/20099757
- https://zh.wikipedia.org/wiki/%E5%8D%8F%E6%96%B9%E5%B7%AE
- https://zh.wikipedia.org/wiki/%E7%89%B9%E5%BE%81%E5%80%BC%E5%92%8C%E7%89%B9%E5%BE%81%E5%90%91%E9%87%8F
- https://zh.wikipedia.org/wiki/%E4%B8%BB%E6%88%90%E5%88%86%E5%88%86%E6%9E%90
- http://www.cs.otago.ac.nz/cosc453/student_tutorials/principal_components.pdf
- https://www.cnblogs.com/pinard/p/6239403.html
- 《机器学习实战》-- Peter Harrington