本篇博文基本不涉及PCA的任何数学过程,仅讲解在sklearn中如何调用相关类实现PCA降维
对普通数组和Series来说,维度就是shape返回的结果,shape中返回了几个数字,就是几维
数组中的每一张表,都可以是一个特征矩阵,针对每一个特征矩阵,维度一般指的都是特征的数量,一个特征是一维,二个特征是两维,n个特征是n维
对于图像来说,维度就是图像中特征向量的数量。特征向量可以理解为是坐标轴,一个特征向量定义一条直线,是一维,两个相互垂直的特征向量定义一个平面,即一个直角坐标系,就是二维,三个相互垂直的特征向量定义一个空间,即一个立体直角坐标系,就是三维。三个以上的特征向量相互垂直,定义人眼无法看见,也无法想象的高维空间。
假设原始的数据集是3D的,它们有3个坐标轴x1x2x3,但是绝大部分的数据点都在同一个2D平面的周围,我们将此3D数据在2D平面上进行投影,这意味着我只需要2个数z1和z2就可以代表这些点在平面中的位置
这时候,我们将三维的样本点的数据压缩到的二维上,即实现了三维变二维,并且尽可能保留原始数据的信息,一个成功的降维,就实现了。
降维算法中的”降维“,指的是降低特征矩阵中特征的数量,降维的目的是为了让算法运算更快,效果更好,另外是为了数据可视化,三维及以下的特征矩阵,是可以被可视化的,这可以帮助我们很快地理解数据的分布,而三维以上特征矩阵的则不能被可视化,数据的性质也就比较难理解。
在降维过程中,我们会减少特征的数量,这意味着删除数据,数据量变少则表示模型可以获取的信息会变少,模型的表现可能会因此受影响。
高维数据中,必然有一些特征是不带有有效的信息的(比如噪音),或者有一些特征带有的信息和其他一些特征是重复的(比如一些特征可能会高度线性相关)。我们希望能够找出一种办法来帮助我们衡量特征上所带的信息量,让我们在降维的过程中,能够即减少特征的数量,又保留大部分有效信息,即将那些带有重复信息的特征合并,并删除那些带无效信息的特征,逐渐创造出能够代表原特征矩阵大部分信息的,特征更少的,新特征矩阵
将一个n维特征矩阵降为k维特征矩阵,大致有如下几个步骤:
在上述的步骤3中,我们用来找出n个新特征向量,让数据能够被压缩到少数特征上并且总信息量不损失太多的技术就是矩阵分解。PCA和SVD是两种不同的降维算法,但他们都遵从上面的过程来实现降维,只是两种算法中矩阵分解的方法不同,信息量的衡量指标不同罢了。
PCA使用方差作为信息量的衡量指标(方差很小或者为0的特征不带有有效信息了,因为它对样本没有区分度),并且特征值分解来找出空间V。样本方差,又称可解释性方差,方差越大,特征所带的信息量越多。降维完成之后,PCA找到的每个新特征向量就叫做“主成分”,而被丢弃的特征向量被认为信息量很少,这些信息很可能就是噪音。
PCA,是将已存在的特征进行压缩,降维完毕后的特征不是原本的特征矩阵中的任何一个特征,而是通过某些方式组合起来的新特征。通常来说,在新的特征矩阵生成之前,我们无法知晓PCA都建立了怎样的新特征向量,新特征矩阵生成之后也不具有可读性,我们无法判断新特征矩阵的特征是从原数据中的什么特征组合而来,新特征虽然带有原始数据的信息,却已经不是原数据上代表着的含义了。以PCA为代表的降维算法因此是特征创造的一种
SVD使用奇异值分解来找出空间V,奇异值是SVD中用来衡量特征上信息量的指标
class sklearn.decomposition.PCA(n_components=None, *, copy=True, whiten=False,
svd_solver='auto', tol=0.0, iterated_power='auto',
random_state=None):
n_components
是我们降维后需要的维度,即降维后需要保留的特征数量,是降维流程中第2步里需要确认的k值,一般输入[0, min(x.shape)]
范围中的整数。这是一个需要我们人为去确认的超参数,并且我们设定的数字会影响到模型的表现。如果留下的特征太多,就达不到降维的效果,如果留下的特征太少,那新特征向量可能无法容纳原始数据集中的大部分信息,因此,n_components
既不能太大也不能太小。
下面看一个鸢尾花数据集(特征是4维)的案例,将n_components
取为2,取2的原因是方便可视化。将特征向量为4维的鸢尾花数据集降至2维后,用散点图画出来,看看三种不同的鸢尾花在新的二维特征向量中的分布情况
import matplotlib.pyplot as plt
from sklearn.datasets import load_iris
from sklearn.decomposition import PCA
import pandas as pd
#1.数据准备
iris = load_iris()
x = iris.data
y = iris.target
#print(x.shape) #对于数组(150,4)来说是二维的
#print(pd.DataFrame(x)) #对于特征矩阵来说,这是4维的,注意和数组的维度相区分
#2.建立模型,调用PCA
pca = PCA(n_components=2) #实例化
pca = pca.fit(x) #拟合模型
x_new = pca.transform(x) #获取新矩阵。也可以一步到位,x_new = pca.fit_transform(x)。
#现在x_new.shape=(150,2),即数据集由原本的x->x_new,降至了二维可以可视化了,下面看看三种不同的鸢尾花在新的特征矩阵中的分布情况
#3.可视化。这段代码最好用循环实现,为了更清晰,这里重复写三次
plt.figure() #画布。画三个图在同一个画布上
plt.scatter(x_new[y==0 , 0] , x_new[y==0 , 1] , c="red" , label=iris.target_names[0])
plt.scatter(x_new[y==1 , 0] , x_new[y==1 , 1] , c="black" , label=iris.target_names[1])
plt.scatter(x_new[y==2 , 0] , x_new[y==2 , 1] , c="orange" , label=iris.target_names[2])
plt.legend()
plt.title("PCA of iris dataset")
plt.show()
鸢尾花的分布被展现在我们眼前了,明显这是一个分簇的分布,并且每个簇之间的分布相对比较明显,也许versicolor和virginia这两种花之间会有一些分类错误,但setosa肯定不会被分错。这样的数据很容易分类,可以预见KNN,随机森林,神经网络,朴素贝叶斯,Adaboost这些分类器在鸢尾花数据集上,未调整的时候都可以有95%上下的准确率。
直观上看,降维后的数据分布非常好,可以调用pca中的几个属性探索下降维后的数据
explained_variance_
:查看降维后每个新特征向量上所带的信息量大小(可解释性方差的大小)
因为新特征矩阵是两个特征,所以返回了两个。从结果可看出,大部分的信息都汇聚在第一个特征上,当然这也是必然的,后面的特征的信息量会逐步减少
pca.explained_variance_
array([4.22824171, 0.24267075])
explained_variance_ratio_
:查看降维后每个新特征向量所占的信息量占原始数据总信息量的百分比,又叫做可解释方差贡献率。
降维后信息量必然有所损失,因此我们想知道降维后的新特征向量组成的新特征空间里,信息量保留了多少呢。结果分析:第一个特征降维后,它带有了原特征矩阵中92.46%的信息,正如前面所言,大部分的有效信息都集中在了第一个特征上。第二个特征带有5.3%
pca.explained_variance_ratio_
array([0.92461872, 0.05306648])
#求和。降维后的新特征矩阵带有原特征矩阵的97.7%的信息。特征减少一半,但是信息损失不足3%
pca.explained_variance_ratio_.sum()
0.977685206318795
pca_line = PCA().fit(x) #默认特征个数为4个,所以返回了4个
pca_line.explained_variance_ratio_
array([0.92461872, 0.05306648, 0.01710261, 0.00521218])
pca_line.explained_variance_ratio_.sum()
1.0
np.cumsum(pca_line.explained_variance_ratio_) #用np.cumsum函数实现累加,画个图更清晰
array([0.92461872, 0.97768521, 0.99478782, 1. ])
当参数n_components中不填写任何值,则默认返回min(x.shape)
个特征,一般来说,样本量都会大于特征数目,所以什么都不填就相当于转换了新特征空间,但没有减少特征的个数。一般来说,不会使用这种输入方式。但我们却可以使用这种输入方式来画出累计可解释方差贡献率曲线,以此选择最好的n_components的整数取值
pca_line = PCA().fit(x)
plt.plot([1,2,3,4] , np.cumsum(pca_line.explained_variance_ratio_))
plt.xticks([1,2,3,4])
plt.xlabel("number of components after dimension reduction")
plt.ylabel("cumulative explained variance")
plt.show()
可见随着所选择的特征逐渐增大,可解释性方差的贡献率也逐渐增大,即获取到的原数据的信息也越来越多。通常来说,如果有几百个特征的时候,我们主要找这个突然往上增然后转折的位置,即转折点处,在那个位置取一个n_compoents的值比较合适
不止输入整数,n_components还有其他选择,可用最大似然估计自选超参数n_components,输入"mle"
作为n_components的参数输入
#用最大似然估计来选超参数,但是计算量很大
pca_mle = PCA(n_components="mle")
pca_mle = pca_mle.fit(x)
x_mle = pca_mle.transform(x)
x_mle.shape = (150,3) #即mle自己选了3个特征
pca_mle.explained_variance_ratio_.sum()
0.9947878161267247
输入[0,1]之间的浮点数,并且让参数svd_solver =='full'
,表示希望降维后的总解释性方差占比大于n_components
指定的百分比,即是说,希望保留百分之多少的信息量。比如说,如果我们希望保留97%的信息量,就可以输入n_components
= 0.97,PCA会自动选出能够让保留的信息量超过97%的特征数量
#按信息量占比选超参数
pca_f = PCA(n_components=0.99 , svd_solver="full")
pca_f = pca_f.fit(x)
x_f = pca_f.transform(x)
pca_f.explained_variance_ratio_
array([0.92461872, 0.05306648, 0.01710261])
pca_f = PCA(n_components=0.97 , svd_solver="full")
pca_f = pca_f.fit(x)
x_f = pca_f.transform(x)
pca_f.explained_variance_ratio_
array([0.92461872, 0.05306648])
PCA和SVD涉及了大量的矩阵计算,两者都是运算量很大的模型,但是SVD有一个很好的数学性质,它不需要计算协方差矩阵,可直接求出新特征空间和降维后的特征矩阵,大大的简化了数学运算。
简而言之,SVD在矩阵分解中的过程比PCA简单快速,虽然两个算法都走一样的分解流程,但SVD可以直接算出新特征空间V。但是遗憾的是,SVD的信息量衡量指标比较复杂,要理解”奇异值“远不如理解”方差“来得容易,因此,sklearn将降维流程拆成了两部分:一部分是计算新特征空间V,由奇异值分解完成,另一部分是映射数据和求解新特征矩阵,由主成分分析完成,实现了用SVD的性质减少计算量,却让信息量的评估指标是方差,即通过SVD和PCA的合作,sklearn实现了一种计算更快更简单,但效果却很好的“合作降维“,具体流程如下图:
所以,PCA里包含了SVD的参数。奇异值分解追求的仅仅是V,只要有了V,就可以计算出降维后的特征矩阵。在transform过程之后,fit中奇异值分解的结果除了V(k,n)以外,就会被舍弃,而V(k,n)会被保存在属性components_
当中,可以调用查看。
PCA(2).fit(x).components_ #返回的是降维后的新特征空间V(k,n) k=2是因为n_components为2,n=4是原特征空间维度为4
array([[ 0.36138659, -0.08452251, 0.85667061, 0.3582892 ],
[ 0.65658877, 0.73016143, -0.17337266, -0.07548102]])
参数svd_solver是在降维过程中,用来控制矩阵分解的一些细节的参数。有四种模式可选:"auto"
, "full"
, "arpack"
,"randomized"
,默认"auto"
。
"auto"
:基于x.shape和n_components的默认策略来选择分解器:如果输入数据的尺寸大于500x500且要提min(X.shape)
的80%,就自动启用效率更高的"randomized"
方法。否则,精确完整"full"
:从scipy.linalg.svd
中调用标准的LAPACK
分解器来生成精确完整的SVD,适合数据量比较适中,计算时"arpack"
:从scipy.sparse.linalg.svds
调用ARPACK
分解器来运行截断奇异值分解(SVD truncated),分解时就"randomized"
:在此方法中,分解器会先生成多个随机向量,然后一一去检测这些随机向量中是否有任何一个符合我们的分解需求,如果符合,就保留这个随机向量,并基于这个随机向量来构建后续的向量空间。这个方法已经被Halko等人证明,比"full"
模式下计算快很多,并且还能够保证模型运行效果。适合特征矩阵巨大,计算量庞大的情况而参数random_state
在参数svd_solver
的值为"arpack" or "randomized"
的时候生效,可以控制这两种SVD模式中的随机模式。
通常我们就选用”auto“
,不必对这个参数纠结太多
首先谈下PCA与特征选择的区别,特征选择后的特征矩阵是可解读的,而PCA降维后的特征矩阵式不可解
读的:PCA是将已存在的特征进行压缩,降维完毕后的特征不是原本的特征矩阵中的任何一个特征,而是通过某些方式组合起来的新特征。
通常来说,在新的特征矩阵生成之前,我们无法知晓PCA都建立了怎样的新特征向量,新特征矩阵生成之后也不具有可读性,我们无法判断新特征矩阵的特征是从原数据中的什么特征组合而来,新特征虽然带有原始数据的信息,却已经不是原数据上代表着的含义了。但是其实,在矩阵分解时,PCA是有目标的:在原有特征的基础上,找出能够让信息尽量聚集的新特征向量。在sklearn使用的PCA和SVD联合的降维方法中,这些新特征向量组成的新特征空间其实就是V(k,n)
。当V(k,n)
是数字时,我们无法判断V(k,n
)和原有的特征究竟有着怎样千丝万缕的数学联系。但是,如果原特征矩阵是图像,V(k,n)这个空间矩阵也可以被可视化的话,我们就可以通过两张图来比较,就可以看出新特征空间V(k,n)
究竟从原始数据里提取了什么重要的信息,而新特征空间V(k,n)就保存在components_
这个属性中,下面将用一个人脸识别的案例来看看新特征空间V(k,n)
究竟从原始的人脸数据里提取了哪些重要的信息
在特征工程中,inverse_transform
可以将归一化,标准化,甚至做过哑变量的特征矩阵还原回原始数据中的特征矩阵,那PCA降维以后,能否通过inverse_transform
接口将新特征矩阵还原为原特征矩阵呢?在下面的人脸识别案例中,若是两个矩阵画出的图像一摸一样,那么两个矩阵就携带了一样的信息
from sklearn.datasets import fetch_lfw_people #7个人的1000多张照片组成了一组人脸数据
from sklearn.decomposition import PCA
import matplotlib.pyplot as plt
import numpy as np
faces = fetch_lfw_people(min_faces_per_person=60) #实例化(在这个数据集中,每个人取出60张图)
"""
faces同样是一个字典,和前面的数据集不同的是,它除了有特征矩阵data外还有另外一个矩阵images
faces.data.shape
(1348, 2914) 行1348是样本,列2914是相关的所有特征
faces.images.shape
(1348, 62, 47) 三维数组,对图像数据而言,严格来说此矩阵才是特征矩阵
1348:矩阵中图像的个数。对于三维矩阵而言,第三个维度写在最前面
62:每个图像的特征矩阵的行
47:每个图像的特征矩阵的列
一组62*47=2914个像素点的值决定了一张图长啥样,一张图就是一张表
"""
x = faces.data #本质特征矩阵
#可视化。这里数据本身是图像,和数据本身只是数字的可视化方法不一样 subplots专门用来画子图和建立子图画布的
#fig生成了3*8=24张大小为(8,4)的画布,axes生成了3*8=24个子图对象,和fig一一对应,要对axes里的对象进行逐一处理
fig , axes = plt.subplots(3 , 8 #3行8列共24个子图
, figsize=(8,4) #每一个画布的大小
, subplot_kw={
"xticks" : [] , "yticks" : []} #不要显示坐标轴
)
#在画布里画图 faces.images:用来画图的矩阵
for i , ax in enumerate(axes.flat) : #.flat二维降到一维
ax.imshow(faces.images[i,:,:] , cmap="gray")#imshow要求的数据格式必须是一个(m,n)格式的矩阵,即每个数据都是一张单独的图
plt.show()
建模降维,可视化新特征空间,新特征空间就存储在属性components_
中
pca = PCA(150).fit(x) #注意这里输入的是x,即faces.data,不是faces.images.data,sklearn在降维算法中只接受二维
V = pca.components_ #V.shape = (150, 2914) 可分解成(150,62,47),所以可以可视化
#可视化新特征空间V,V决定着新特征有什么含义,是什么方向等
fig , axes = plt.subplots(3 , 8
, figsize=(8,4)
, subplot_kw={
"xticks" : [] , "yticks" : []}
)
for i , ax in enumerate(axes.flat) :
ax.imshow(V[i,:].reshape(62,47), cmap="gray")#注意#imshow要求的数据格式必须是一个(m,n)格式的矩阵,所以要reshape
plt.show()
可视化新特征空间的图,注意,这里虽然子图的数量和上面一致,但是和上面的人脸图不具有一一对应的关系
原特征矩阵:(1348,2914)
新特征空间:(150,2914)
新特征矩阵:(1348,150)
原特征矩阵 * 新特征空间的转置 = 新特征矩阵
这样来理解,1348表示样本个数,即有1348张人脸图,2914表示特征的个数,即在原数据集中,用2914个数字可以表示一张人脸图(即每个像素点的像素值),由于是图片数据,所以每一个样本的2914个数据可以被可视化出来。150表示降维后的维度,也是新特征矩阵中生成的新维度,即用150个特征去表示原来用2914个特征表示的人脸图像数据,自然而然,这150个特征肯定不是像素点的像素值,肯定是一些具有代表性的人脸的数据。从维度上来看,我们不能去可视化这个新特征矩阵,但是我们可以可视化这个新特征空间,直观的看下怎么用150个数据表示原来需要2914个数据表示的信息。
比起降维前的数据,新特征空间可视化后的人脸非常模糊,这是因为原始数据还没有被映射到特征空间中。但是可以大致看出,在映射数据之前,降维算法选取了哪些特征,整体比较亮的图片,获取的信息较多,整体比较暗的图片,却只能看见黑漆漆的一块。在比较亮的图片中,眼睛,鼻子,嘴巴,都相对清晰,脸的轮廓,头发之类的比较模糊
这说明,新特征空间里的特征向量们,大部分是**“五官”和“亮度”**相关的向量,所以新特征向量上的信息肯定大部分是由原数据中和"五官"和"亮度"相关的特征中提取出来的。到这里,我们通过可视化新特征空间V,解释了一部分降维后的特征:虽然显示出来的数字看着不知所云,但画出来的图表示,这些特征是和”五官“以及”亮度“有关的。这也再次证明了,PCA能够将原始数据集中重要的数据进行聚集
注意,这里不能通过新特征矩阵画图,从新特征矩阵的shape就可看出新特征矩阵不符合画图的要求,没有维度满足62*47=2914
x_new = pca.transform(x)
x_new.shape
(1348, 150)
但是我们可以通过接口inverse_transform
将新特征矩阵转到另外一个矩阵,此矩阵所携带的信息与原特征矩阵携带的信息大致相同,可以可视化这个矩阵
x_new = pca.transform(x)
x_inverse = pca.inverse_transform(x_new) #shape:(1348,2914),可以被可视化
fig , axes = plt.subplots(3 , 8
, figsize=(8,4)
, subplot_kw={
"xticks" : [] , "yticks" : []}
)
for i , ax in enumerate(axes.flat) :
ax.imshow(x_inverse[i,:].reshape(62,47), cmap="gray")
plt.show()
这个图就和原特征矩阵的图是一一对应的了,拿下来进行比较
inverse_transform
转回去的图
原图
虽然可以看出相应的两张图是同一个人,但是明显x_inverse
里面的数据比原数据要模糊,毕竟x_inverse
是通过150个数据转回重组到2914个数据的,这不代表在原本降维过程中删掉的信息被复原了,而那些删掉的信息是不能被复原的了,特征空间可以变回去,但是损失掉的信息找不回了,所以x_inverse
的图比原图要模糊,所以降维不是完全可逆的
pca.explained_variance_ratio_.sum()
0.9457099
可见有5.5%左右的信息被删掉了,不能复原了,同时也表示此150维的信息包含了原2914维代表的信息的94.5%左右,很不错了。
降维的目的之一就是希望抛弃掉对模型带来负面影响的特征,而我们相信,带有效信息的特征的方差应该是远大于噪音的,所以相比噪音,有效的特征所带的信息应该不会在PCA过程中被大量抛弃。
inverse_transform
能够在不恢复原始数据的情况下,将降维后的数据返回到原本的高维空间,即是说能够实现”保证维度,但去掉方差很小特征所带的信息“。利用inverse_transform
的这个性质,我们能够实现噪音过滤
from sklearn.datasets import load_digits
from sklearn.decomposition import PCA
import matplotlib.pyplot as plt
import numpy as np
#这是一个手写数字集,数据集本身没有噪声,需要人为的添加噪声
digits = load_digits() #digits.data.shape:(1797,64) digits.images.shape:(1797,8,8)
def plot_digits(data) :
"""
画手写数字的图
:param data: data.shape 必须为 (m,64),m表示手写数字图的个数
"""
fig , axes = plt.subplots(4,10
, figsize=(10,4)
, subplot_kw={
"xticks" : [] , "yticks" : []}
)
for i , ax in enumerate(axes.flat) :
ax.imshow(data[i].reshape(8,8) , cmap="binary")
plt.show()
#没有添加噪音的,原数据集的图
x = digits.data
plot_digits(x)
#人为添加噪音
"""
加噪音有很多方法,比如一条正弦曲线,我们可以在正弦曲线上,上下各加上一组随机数,让正弦曲线的某些点偏离这条曲线
对于图像数据,这里使用正态分布随机抽取,np.normal函数可实现从输入的数据集中随机抽取一个满足正态分布的另一个数据集
所以可用np.normal抽取一个来自原数据集的,符合正态分布的数据集
"""
rng = np.random.RandomState(42) #规定np中的随机模式
#从原数据集digits.data中随机抽取的,符合正态分布的数据集,返回的noisy即为加了噪声后的数据集,第二个参数为方差的大小
noisy = rng.normal(digits.data , 2)
plot_digits(noisy)
#降噪
#开始降维
pca = PCA(n_components=0.5 , svd_solver="full")
x_new = pca.fit_transform(noisy) #x_new.shape:(1797,6)原本64个特征,若只需要保留一半的信息量的话,只需要生成6个新特征就好了,就把维度从64降到了6
#转换回去
"""
#只是把x_new的信息映射到高维特征中,这只是个升维过程,并不会增加额外的信息
所以说在降维的过程中,被我们过滤掉的那些特征应该是噪音的特征,在inverse_transform中是不会回来的
"""
without_noisy = pca.inverse_transform(x_new)
plot_digits(without_noisy)