目录
1 概述
1.1 从什么叫“维度”说开来
2 PCA与SVD
2.2 重要参数n_components
2.2.1 迷你案例:高维数据的可视化
2.2.2 最大似然估计自选超参数
2.2.3 按信息量占比选超参数
2.3 PCA中的SVD
2.3.1 PCA中的SVD哪里来?
2.3.2 重要参数svd_solver 与 random_state
2.3.3 重要属性components_
2.4 重要接口inverse_transform
2.4.1 迷你案例:用人脸识别看PCA降维后的信息保存量
2.4.2 迷你案例:用PCA做噪音过滤
3 案例:PCA对手写数字数据集的降维
4 附录
4.1 PCA参数列表
4.2 PCA属性列表
4.3 PCA接口列表
SVD和主成分分析PCA都属于矩阵分解算法中的入门算法,都是通过分解特征矩阵来进行降维.
我们现在有一组简单的数据,有特征x1和x2,三个样本数据的坐标点分别为(1,1),(2,2),(3,3)。我们可以让x1和x2分别作为两个特征向量,很轻松地用一个二维平面来描述这组数据。这组数据现在每个特征的均值都为2,方差则等于:
每个特征的数据一模一样,因此方差也都为1,数据的方差总和是2。
现在我们的目标是:只用一个特征向量来描述这组数据,即将二维数据降为一维数据,并且尽可能地保留信息量, 即让数据的总方差尽量靠近2。于是,我们将原本的直角坐标系逆时针旋转45°,形成了新的特征向量x1*和x2*组 成的新平面,在这个新平面中,三个样本数据的坐标点可以表示为
可以注意到, x2*上的数值此时都变成了0,因此x2*明显不带有任何有效信息了(此时x2*的方差也为0了)。此时,x1*特征上的数据均值是 ,而方差则可表示成:
x1*上的数据均值为0,方差也为0。
此时,我们根据信息含量的排序,取信息含量最大的一个特征,因为我们想要的是一维数据。所以我们可以将x2* 删除,同时也删除图中的x2*特征向量,剩下的x1*就代表了曾经需要两个特征来代表的三个样本点。通过旋转原有特征向量组成的坐标轴来找到新特征向量和新坐标平面,我们将三个样本点的信息压缩到了一条直线上,实现了二维变一维,并且尽量保留原始数据的信息。一个成功的降维,就实现了。
不难注意到,在这个降维过程中,有几个重要的步骤:
过程 |
二维特征矩阵 |
n维特征矩阵 |
1 |
输入原数据,结构为 (3,2) 找出原本的2个特征对应的直角坐标系,本质是找出这2个特征构成的2维平面 |
输入原数据,结构为 (m,n) 找出原本的n个特征向量构成的n维空间V |
2 |
决定降维后的特征数量:1 |
决定降维后的特征数量:k |
3 |
旋转,找出一个新坐标系 本质是找出2个新的特征向量,以及它们构成的新2维平面 新特征向量让数据能够被压缩到少数特征上, 并且总信息量不损失太多 |
通过某种变化,找出n个新的特征向量,以及它们构成的新n维空间V |
4 |
找出数据点在新坐标系上,2个新坐标轴上的坐标 |
找出原始数据在新特征空间V中的n个新特征向量上对应的值,即“将数据映射到新空间中” |
5 |
选取第1个方差最大的特征向量,删掉没有被选中的特征,成功将2维平面降为1维 |
选取前k个信息量最大的特征,删掉没有被选中的特征,成功将n维空间V降为k维 |
在步骤3当中,我们用来找出n个新特征向量,让数据能够被压缩到少数特征上并且总信息量不损失太多的技术就是 矩阵分解。PCA和SVD是两种不同的降维算法,但他们都遵从上面的过程来实现降维,只是两种算法中矩阵分解的 方法不同,信息量的衡量指标不同罢了。PCA使用方差作为信息量的衡量指标,并且特征值分解来找出空间V。降 维时,它会通过一系列数学的神秘操作(比如说,产生协方差矩阵 )将特征矩阵X分解为以下三个矩阵,其中
和
是辅助的矩阵,Σ是一个对角矩阵(即除了对角线上有值,其他位置都是0的矩阵),其对角线上的元素 就是方差。降维完成之后,PCA找到的每个新特征向量就叫做“主成分”,而被丢弃的特征向量被认为信息量很少,这些信息很可能就是噪音。
而SVD使用奇异值分解来找出空间V,其中Σ也是一个对角矩阵,不过它对角线上的元素是奇异值,这也是SVD中用来衡量特征上的信息量的指标。U和V^{T}分别是左奇异矩阵和右奇异矩阵,也都是辅助矩阵。
在数学原理中,无论是PCA和SVD都需要遍历所有的特征和样本来计算信息量指标。并且在矩阵分解的过程之中, 会产生比原来的特征矩阵更大的矩阵,比如原数据的结构是(m,n),在矩阵分解中为了找出最佳新特征空间V,可能 需要产生(n,n),(m,m)大小的矩阵,还需要产生协方差矩阵去计算更多的信息。而现在无论是Python还是R,或者 其他的任何语言,在大型矩阵运算上都不是特别擅长,无论代码如何简化,我们不可避免地要等待计算机去完成这 个非常庞大的数学计算过程。因此,降维算法的计算量很大,运行比较缓慢,但无论如何,它们的功能无可替代, 它们依然是机器学习领域的宠儿。
思考:PCA和特征选择技术都是特征工程的一部分,它们有什么不同?
特征工程中有三种方式:特征提取,特征创造和特征选择。仔细观察上面的降维例子和上周我们讲解过的特征 选择,你发现有什么不同了吗?
特征选择是从已存在的特征中选取携带信息最多的,选完之后的特征依然具有可解释性,我们依然知道这个特 征在原数据的哪个位置,代表着原数据上的什么含义。
而PCA,是将已存在的特征进行压缩,降维完毕后的特征不是原本的特征矩阵中的任何一个特征,而是通过某 些方式组合起来的新特征。通常来说,在新的特征矩阵生成之前,我们无法知晓PCA都建立了怎样的新特征向 量,新特征矩阵生成之后也不具有可读性,我们无法判断新特征矩阵的特征是从原数据中的什么特征组合而 来,新特征虽然带有原始数据的信息,却已经不是原数据上代表着的含义了。以PCA为代表的降维算法因此是 特征创造(feature creation,或feature construction)的一种。
可以想见,PCA一般不适用于探索特征和标签之间的关系的模型(如线性回归),因为无法解释的新特征和标 签之间的关系不具有意义。在线性回归模型中,我们使用特征选择。
n_components是我们降维后需要的维度,即降维后需要保留的特征数量,降维流程中第二步里需要确认的k值,一般输入[0, min(X.shape)]范围中的整数。一说到K,大家可能都会想到,类似于KNN中的K和随机森林中的n_estimators,这是一个需要我们人为去确认的超参数,并且我们设定的数字会影响到模型的表现。如果留下的特 征太多,就达不到降维的效果,如果留下的特征太少,那新特征向量可能无法容纳原始数据集中的大部分信息,因 此,n_components既不能太大也不能太小。那怎么办呢?
可以先从我们的降维目标说起:如果我们希望可视化一组数据来观察数据分布,我们往往将数据降到三维以下,很 多时候是二维,即n_components的取值为2。
1.调用库和模块
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
from sklearn.datasets import load_iris
from sklearn.decomposition import PCA
2.提取数据
iris=load_iris()
y=iris.target
x=iris.data
x.shape #(150, 4)
y.shape #(150,)
pd.DataFrame(x).head()
3. 建模
#调用PCA
pca = PCA(n_components=2) #实例化.降维到2
pca = pca.fit(x) #拟合模型
X_dr = pca.transform(x) #获取新矩阵
X_dr
#也可以fit_transform一步到位
#X_dr = PCA(2).fit_transform(X)
4.可视化
"""
plt.figure()
plt.scatter(X_dr[y==0, 0], X_dr[y==0, 1], c="red", label=iris.target_names[0])
plt.scatter(X_dr[y==1, 0], X_dr[y==1, 1], c="black", label=iris.target_names[1])
plt.scatter(X_dr[y==2, 0], X_dr[y==2, 1], c="orange", label=iris.target_names[2])
plt.legend()
plt.title('PCA of IRIS dataset')
plt.show()
"""
colors = ['red', 'black', 'orange']
iris.target_names
plt.figure()
for i in [0, 1, 2]:
plt.scatter(X_dr[y == i, 0]
,X_dr[y == i, 1]
,alpha=.7
,c=colors[i]
,label=iris.target_names[i]
)
plt.legend()
plt.title('PCA of IRIS dataset')
plt.show()
5. 探索降维后的数据
#属性explained_variance_,查看降维后每个新特征向量上所带的信息量大小(可解释性方差的大小)
pca.explained_variance_
#属性explained_variance_ratio,查看降维后每个新特征向量所占的信息量占原始数据总信息量的百分比
#又叫做可解释方差贡献率
pca.explained_variance_ratio_
#大部分信息都被有效地集中在了第一个特征上
pca.explained_variance_ratio_.sum()
6. 选择最好的n_components:累积可解释方差贡献率曲线
除了输入整数,n_components还有哪些选择呢?之前我们提到过,矩阵分解的理论发展在业界独树一帜,勤奋智慧的数学大神Minka, T.P.在麻省理工学院媒体实验室做研究时找出了让PCA用最大似然估计(maximum likelihood estimation)自选超参数的方法,输入“mle”作为n_components的参数输入,就可以调用这种方法。
pca_mle = PCA(n_components="mle")
pca_mle = pca_mle.fit(x)
X_mle = pca_mle.transform(x)
X_mle
#可以发现,mle为我们自动选择了3个特征
pca_mle.explained_variance_ratio_.sum()
#得到了比设定2个特征时更高的信息含量,对于鸢尾花这个很小的数据集来说,3个特征对应这么高的信息含量,并不
#需要去纠结于只保留2个特征,毕竟三个特征也可以可视化
'''
0.9947878161267247
'''
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])
'''
细心的小伙伴可能注意到了,svd_solver是奇异值分解器的意思,为什么PCA算法下面会有有关奇异值分解的参数?不是两种算法么?我们之前曾经提到过,PCA和SVD涉及了大量的矩阵计算,两者都是运算量很大的模型,但其实,SVD有一种惊人的数学性质,即是它可以跳过数学神秘的宇宙,不计算协方差矩阵,直接找出一个新特征向量组成的n维空间,而这个n维空间就是奇异值分解后的右矩阵(所以一开始在讲解降维过程时,我们说”生成新特征向量组成的空间V",并非巧合,而是特指奇异值分解中的矩阵
)。
PCA(2).fit(X).components_
PCA(2).fit(X).components_.shape
from sklearn.datasets import fetch_lfw_people
from sklearn.decomposition import PCA
import matplotlib.pyplot as plt
import numpy as np
faces = fetch_lfw_people(min_faces_per_person=60)
faces.images.shape
#怎样理解这个数据的维度?
'''
(1348, 62, 47)
#1348 是矩阵中图像的个数
#62是每个图像的特征矩阵的行
#47是每个图像的特征矩阵的列
'''
faces.data.shape
#换成特征矩阵之后,这个矩阵是什么样?
X = faces.data
fig, axes = plt.subplots(4,5
,figsize=(8,9)
,subplot_kw = {"xticks":[],"yticks":[]} #不要显示坐标轴
)
fig
axes
axes.flat
enumerate(axes.flat) #填充图像
for i, ax in enumerate(axes.flat):
ax.imshow(faces.images[i,:,:]
,cmap="gray" #选择色彩的模式
)
https://matplotlib.org/tutorials/colors/colormaps.html
4. 建模降维,提取新特征空间矩阵
#原本有2900维,我们现在来降到150维
pca = PCA(150).fit(X)
V = pca.components_
V.shape
'''
(150, 2914)
'''
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")
from sklearn.datasets import fetch_lfw_people
from sklearn.decomposition import PCA
import matplotlib.pyplot as plt
import numpy as np
faces = fetch_lfw_people(min_faces_per_person=60)
faces.images.shape
#怎样理解这个数据的维度?
faces.data.shape
#换成特征矩阵之后,这个矩阵是什么样?
X = faces.data
pca = PCA(150)
X_dr = pca.fit_transform(X)
X_dr.shape
X_inverse = pca.inverse_transform(X_dr)
X_inverse.shape
fig, ax = plt.subplots(2,10,figsize=(10,2.5)
,subplot_kw={"xticks":[],"yticks":[]}
)
#和2.3.3节中的案例一样,我们需要对子图对象进行遍历的循环,来将图像填入子图中
#那在这里,我们使用怎样的循环?
#现在我们的ax中是2行10列,第一行是原数据,第二行是inverse_transform后返回的数据
#所以我们需要同时循环两份数据,即一次循环画一列上的两张图,而不是把ax拉平
for i in range(10):
ax[0,i].imshow(faces.images[i,:,:],cmap="binary_r")
ax[1,i].imshow(X_inverse[i].reshape(62,47),cmap="binary_r")
可以明显看出,这两组数据可视化后,由降维后再通过inverse_transform转换回原维度的数据画出的图像和原数据画的图像大致相似,但原数据的图像明显更加清晰。这说明,inverse_transform并没有实现数据的完全逆转。这是因为,在降维的时候,部分信息已经被舍弃了,X_dr中往往不会包含原数据100%的信息,所以在逆转的时候,即便维度升高,原数据中已经被舍弃的信息也不可能再回来了。所以,降维不是完全可逆的。
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)
'''
3. 定义画图函数
def plot_digits(data):
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")
plot_digits(digits.data)
4. 为数据加上噪音
np.random.RandomState(42)
#在指定的数据集中,随机抽取服从正态分布的数据
#两个参数,分别是指定的数据集,和抽取出来的正太分布的方差
noisy = np.random.normal(digits.data,2)
plot_digits(noisy)
5. 降维
pca = PCA(0.5).fit(noisy)
X_dr = pca.transform(noisy)
X_dr.shape
'''
(1797, 6)
'''
without_noise = pca.inverse_transform(X_dr)
plot_digits(without_noise)
2.5 重要接口,参数和属性总结
from sklearn.decomposition import PCA
from sklearn.ensemble import RandomForestClassifier as RFC
from sklearn.model_selection import cross_val_score
import matplotlib.pyplot as plt
import pandas as pd
import numpy as np
2. 导入数据,探索数据
data = pd.read_csv(r"E:\数据分析师学习\data\digit recognizor.csv",engine = 'python')
X = data.iloc[:,1:]
y = data.iloc[:,0]
X.shape
'''
(42000, 784)
'''
3. 画画累计方差贡献率曲线,找最佳降维后维度的范围
pca_line = PCA().fit(X)
plt.figure(figsize=[20,5])
plt.plot(np.cumsum(pca_line.explained_variance_ratio_))
plt.xlabel("number of components after dimension reduction")
plt.ylabel("cumulative explained variance ratio")
plt.show()
4. 降维后维度的学习曲线,继续缩小最佳维度的范围
score = []
for i in range(1,101,10):
X_dr = PCA(i).fit_transform(X)
once = cross_val_score(RFC(n_estimators=10,random_state=0)
,X_dr,y,cv=5).mean()
score.append(once)
plt.figure(figsize=[20,5])
plt.plot(range(1,101,10),score)
plt.show()
5. 细化学习曲线,找出降维后的最佳维度
score = []
for i in range(10,25):
X_dr = PCA(i).fit_transform(X)
once = cross_val_score(RFC(n_estimators=10,random_state=0),X_dr,y,cv=5).mean()
score.append(once)
plt.figure(figsize=[20,5])
plt.plot(range(10,25),score)
plt.show()
6. 导入找出的最佳维度进行降维,查看模型效果
X_dr = PCA(23).fit_transform(X)
cross_val_score(RFC(n_estimators=100,random_state=0),X_dr,y,cv=5).mean()
'''
0.945595238095238
'''
模型效果还好,跑出了94.56%的水平,但还是没有我们使用嵌入法特征选择过后的96%高,有没有什么办法能够提高模型的表现呢?
7. 突发奇想,特征数量已经不足原来的3%,换模型怎么样?
from sklearn.neighbors import KNeighborsClassifier as KNN
cross_val_score(KNN(),X_dr,y,cv=5).mean()
'''
0.9699047619047618
'''
score = []
for i in range(10):
X_dr = PCA(23).fit_transform(X)
once = cross_val_score(KNN(i+1),X_dr,y,cv=5).mean()
score.append(once)
plt.figure(figsize=[20,5])
plt.plot(range(10),score)
plt.show()
可以发现,原本785列的特征被我们缩减到23列之后,用KNN跑出了目前位置这个数据集上最好的结果。再进行更细致的调整,我们也许可以将KNN的效果调整到98%以上。PCA为我们提供了无限的可能,终于不用再因为数据量太庞大而被迫选择更加复杂的模型了!
cross_val_score(KNN(4),X_dr,y,cv=5).mean()
'''
0.9687619047619048
'''
%%timeit
cross_val_score(KNN(4),X_dr,y,cv=5).mean()
'''
28.1 s ± 1.41 s per loop (mean ± std. dev. of 7 runs, 1 loop each)
'''