(一)什么是pca
pca,也就是主成分分析法(principal component analysis),主要是用来对数据集进行降维处理。举个最简单的例子,我要根据姓名、年龄、头发的长度、身高、体重、皮肤的白皙程度(随便想的)等特征来预测一个人的性别,但这些特征中有一个是最没有用的,是什么的?显然是年龄,因为年龄的大小跟这个人的性别无关。还有姓名,这个特征显然起不到决定性作用,因为有的男孩的名字起的就像女孩(比如我本人),反之亦然,但是起码绝大多数情况还是能判断的。同理还有身高,一个180CM的很大概率是男孩,当然女孩也有180cm的,比如模特。像这样我从样本的特征中,挑选出最能代表样本、或者对样本预测起到决定性作用最大的n个特征,就叫做主成分分析。为什么会有pca呢?可以想象一个,显示生活中,样本的特征很多,成百上千个也是正常的,但是我们训练不可能用全部的特征进行训练,因为肯定有很多特征是没有用的,或者说起到的作用是很小的,我们的目的就是希望找到起到决定性最大的n个特征。
主成分分析的特征
- 一个非监督的机器学习算法
- 主要用于数据的降维
- 通过降维,可以发现更便于人类理解的特征
- 其他特征:可视化,去噪等等
我们举一个只有两个特征的例子
如果我们只考虑特征1,不考虑特征2的话,那么显然,蓝色的点要从二维映射到一维
那么同理,如果我们只考虑特征2,不考虑特征1的话,那么显然会是这样
现在我们将两种降维的结果对比一下,觉得哪个好呢?
可能会觉得映射到x轴上的效果好,因为点和点之间的距离比较大,映射到y轴上的点都挨着了,不好区分。但是这是最好的方法吗?有没有更好的方法呢?
那么有没有一根直线,这根直线是斜着的,我们将所有的点都映射到这根直线上。
这样的话,降维的效果是不是更好了呢?虽然这个轴是斜着的,但是总归还是一个轴,我们确实将样本从二维映射到了一维上。使用这种方式,这些点更加趋近于原来的那些点的分布情况。点与点之间的距离也比较大,更好区分。那么问题来了,如果找到使得样本间的距离最大的那个轴呢?
既然使得样本间的距离最大,那么距离的定义也是一个需要考虑的方面。如果定义呢?显然我们可以使用方差,因为方差就是描述样本之间分布的疏密或者说均匀情况,方差越大,说明样本之间约稀疏,方差越小,说明样本之间越密集。
那么我们问题就可以转换一下了,相当于是找到一个轴,使得样本空间所有点映射到这个轴之后,对应的方差最大。
那么在解决这个问题,使用主成分分析之前,我们需要做一下处理。
对样本的均值归零(demean),也就是所有样本都减去这批样本的均值,这样这批样本的均值就变为0了,为什么这么做后面会看到。其实样本的均值归0,就是把坐标轴平移了一下
可以看到,样本的分布式没有改变的,我们只是把样本坐标轴进行了平移。
由于均值为0,那么显然方差可以化简如下,为什么要将坐标轴平移,原因在此。因此我们的过程总结起来,如下。
此时的Xproject就是X在轴的方向w上的映射,那么此时的Xproject = ||x|| · ||w|| · cosθ = X · w
因此我们的目标函数可以变成如下
(二)使用梯度上升法求解pca问题
既然要找到一个函数的最大值,那么显然可以使用梯度上升法来解决。
如果把X1、X2、X3····Xn用向量X表示、w1、w2、w3····wn表示,那么式子可以转换如下。
其实到这里已经可以编程实现了,但是肯定还要使用for循环,我们看看可不可以再变换一下,使得其可以编程向量之间的运算
可以看到,左边的式子其实就是两个红色方框里面的式子做点乘运算,最终可以化成最右边的形式。
因此我们在求梯度,等于求最右边的式子
(三)求数据的主成分pca
生成测试数据集
import numpy as np import matplotlib.pyplot as plt X = np.zeros((100, 2)) X[:, 0] = np.random.uniform(0., 100., size=100) X[:, 1] = 0.75 * X[:, 0] + 3. + np.random.normal(0, 10., size=100) plt.scatter(X[:, 0], X[:, 1]) plt.show()
均值归零
# 均值归0 def demean(X): # 多个特征,每一个特征分别均值归0 return X - np.mean(X, axis=0) X_demean = demean(X) plt.scatter(X_demean[..., 0], X_demean[..., 1]) plt.show()
可以看到样本的分布没有什么变化,但是注意坐标轴变化了
梯度上升求解
# 目标函数 def f(w, X): return np.sum((X @ w) ** 2) / len(x) # 求梯度 def df(w, X): return (X.T @ (X @ w)) * 2. / len(x) # 梯度上升 def gradient_ascent(df, X, initial_w, eta, n_iters=1e4, epsilon=1e-8): # 当前的上升次数 cur_iter = 0 # 注意我们的initial_w实际上是一个单位向量,所以我们要转化一下 # 用w 除以w的模即可 w = initial_w / np.linalg.norm(initial_w) while cur_iter < n_iters: gradient = df(w, X) last_w = w w = w + eta * gradient w = w / np.linalg.norm(w) # 当函数增加的值小于epsilon,我们就说梯度上升,爬坡爬不动了 if abs(f(w, X) - f(last_w, X)) < epsilon: break cur_iter += 1 return w initial_w = np.random.random(X.shape[1]) # 不能从零向量开始 eta = 0.001 w = gradient_ascent(df, X_demean, initial_w, eta) print(w) # array([0.77718535, 0.62927175]) # 绘制图像 plt.scatter(X_demean[:, 0], X_demean[:, 1]) plt.plot([0, w[0] * 30], [0, w[1] * 30], color="red") plt.show()
此时红色的线便是我们找到的第一个主成分
(四)求数据的前n个主成分
我们之前使用梯度上升法求出了对于一组数据来说,相应的第一个主成分,是一个坐标轴对应的方向。
这是我们找到的第一个主成分,得到的结果就是,这些样本点所保持的方差是最大的。不过需要注意的是,这些样本点本身是二维的数据,我们映射到这个轴上,它还不是一维的数据,只不过是使得样本在这个轴上的方差是最大的,所以还会有另外一个轴。我们这里是二维的,同理n维也是一样,也是对应n个轴,只不过我们通过主成分分析重新对这n个轴进行了排列,使得第一个轴保持这些样本的方差是最大的,第二个轴次之,第三个轴再次之···,以此类推。因此主成分分析法实际上是从一组坐标系转移到了另外一组坐标系。
所以问题来了,当我们求出第一主成分之后,如何求出下一个主成分呢?
很简单,数据进行改变,将数据在第一个主成分上的分量去掉就行了。那么求出第二主成分,就相当于在去掉第一主成分的数据集上求第一主成分,那么同理,在去掉第一主成分、第二主成分的数据集上再求第一主成分就是原数据的第三主成分,然后循环往复
(五)sklearn中的pca
from sklearn.decomposition import PCA from sklearn.datasets import load_boston boston = load_boston() X = boston.data # 里面的n_components表示我们要选择多少个主成分 # 我们这里选择1个 pca = PCA(n_components=1) pca.fit(X) X_reduction = pca.transform(X) print(f"X shape: {X.shape}") # X shape: (506, 13) print(f"X_reduction shape: {X_pca.shape}") # X_reduction shape: (506, 1) # 我们还可以将降维后的数据转换回去 print(f"X_inverse: {pca.inverse_transform(X_reduction).shape}") # X_inverse: (506, 13)
我们来试试手写数据集
from sklearn.neighbors import KNeighborsClassifier from sklearn.datasets import load_digits from sklearn.model_selection import train_test_split data = load_digits() X = data.data y = data.target X_train, X_test, y_train, y_test = train_test_split(X, y, random_state=666) print(X_train.shape) # (1347, 64) knn_clf = KNeighborsClassifier() knn_clf.fit(X_train, y_train) print(knn_clf.score(X_test, y_test)) # 0.9866666666666667
可以看到在不进行降维,使用全部的数据集特征进行训练的时候,准确率是0.98667,下面我们进行降维处理。降到多少维呢?首先这个样本有64个特征,我们就从1到64全部看看
import matplotlib.pyplot as plt from sklearn.decomposition import PCA from sklearn.neighbors import KNeighborsClassifier from sklearn.datasets import load_digits from sklearn.model_selection import train_test_split data = load_digits() X = data.data y = data.target X_train, X_test, y_train, y_test = train_test_split(X, y, random_state=666) knn_clf = KNeighborsClassifier() # 存放维度和评分的列表 n_components = [] scores = [] for n in range(1, 64): pca = PCA(n_components=n) pca.fit(X_train) # 我们需要对训练集和测试集同时进行降维 X_train_reduction = pca.transform(X_train) X_test_reduction = pca.transform(X_test) # 训练模型 knn_clf.fit(X_train_reduction, y_train) # 将每一次的降维的个数和分数加到对应的列表里面 n_components.append(n) scores.append(knn_clf.score(X_test_reduction, y_test)) plt.plot(n_components, scores) plt.locator_params("y", nbins=20) plt.locator_params("y", nbins=30) plt.xlabel("n_components") plt.ylabel("score") plt.show()
从维度和分数之间的变化可以看出,当我们只取25个特征,准确率就已经达到百分之九十八了,说明剩下的特征对于样本的预测几乎起不到什么作用了,或许使用全部的样本训练会使准确率变得更高,但是提高的程度微乎其微,但是使用25个样本训练所需要的时间肯定远远小于使用全部样本训练所需要的时间,当然内存、cpu的消耗也会更少。因为使用25个样本所得到的准确率相比使用全部样本得到的准确率只下降了一点点,因此在实际的项目生产中我们是愿意牺牲这一点点的精确率来换取时间和空间上的效率的
我们这种搜索主成分个数的方式有点类似于网格搜索,实际上PCA为我们提供了一个特殊的指标,我们可以使用这个指标方便的找到合适的主成分个数
pca.explained_variance_ratio_
:可以解释的方差比率,如果我们只取两个主成分的话,会得到大概这样一个结果array([0.14566817, 0.13735469]),里面的每一个值代表了解释原来数据方差的比例。回忆一下pca主成分分析,就是找能使原来数据的方差维持到最大的轴。第一个轴能维持0.146,第二个轴能维持0.137,加一起大概百分之28左右,意思是只能维持原来方差的百分之28左右,剩下的百分之72就丢失掉了,所以只选两个主成分是不够的,需要多选几个,但是我们也能想到。总共64个特征,会对应64个轴,那么最后面的轴能解释的方差所占的比例基本上小到可以忽略不计了。
import numpy as np import matplotlib.pyplot as plt from sklearn.decomposition import PCA from sklearn.neighbors import KNeighborsClassifier from sklearn.datasets import load_digits from sklearn.model_selection import train_test_split data = load_digits() X = data.data y = data.target X_train, X_test, y_train, y_test = train_test_split(X, y, random_state=666) knn_clf = KNeighborsClassifier() pca = PCA(n_components=64) pca.fit(X_train) # 保留5位小数 print(np.array([round(i, 5) for i in pca.explained_variance_ratio_])) """ [1.4567e-01 1.3735e-01 1.1778e-01 8.5000e-02 5.8600e-02 5.1150e-02 4.2660e-02 3.6010e-02 3.4110e-02 3.0540e-02 2.4230e-02 2.2870e-02 1.8030e-02 1.7930e-02 1.4580e-02 1.4200e-02 1.3000e-02 1.2660e-02 1.0170e-02 9.0900e-03 8.8500e-03 7.7400e-03 7.6100e-03 7.1200e-03 6.8600e-03 5.7600e-03 5.7200e-03 5.0800e-03 4.8900e-03 4.3500e-03 3.7300e-03 3.5800e-03 3.2700e-03 3.1500e-03 3.0900e-03 2.8800e-03 2.5000e-03 2.2500e-03 2.2000e-03 1.9800e-03 1.8800e-03 1.5300e-03 1.4300e-03 1.3800e-03 1.1800e-03 1.0700e-03 9.6000e-04 9.0000e-04 5.8000e-04 3.8000e-04 2.4000e-04 8.0000e-05 6.0000e-05 5.0000e-05 1.0000e-05 0.0000e+00 0.0000e+00 0.0000e+00 0.0000e+00 0.0000e+00 0.0000e+00 0.0000e+00 0.0000e+00 0.0000e+00] """
可以看到,最后面的几个基本上为0了,可以丢掉不计了,我们以此来绘制图像。横轴很好理解,主成分的个数n,纵轴就是前n个主成分加起来所能解释的方差占总方差的比例
import numpy as np import matplotlib.pyplot as plt from sklearn.decomposition import PCA from sklearn.neighbors import KNeighborsClassifier from sklearn.datasets import load_digits from sklearn.model_selection import train_test_split data = load_digits() X = data.data y = data.target X_train, X_test, y_train, y_test = train_test_split(X, y, random_state=666) knn_clf = KNeighborsClassifier() pca = PCA(n_components=64) pca.fit(X_train) plt.plot([n for n in range(1, X.shape[1] + 1)], np.cumsum(pca.explained_variance_ratio_)) plt.locator_params("x", nbins=20) plt.locator_params("y", nbins=30) plt.show()
和我们之前通过预测样本得到的结果是类似的,但是有时候我们不希望每次都通过这种方式来判断到底应该选取多少个主成分,而是给一个比率,比如0.95,意思是我需要能解释原数据百分之95的方差,然后pca算法自动帮助我们判断该选取多少个主成分。我们可以通过循环来实现,但幸运的是,sklearn已经帮助我们实现好了
from sklearn.decomposition import PCA from sklearn.neighbors import KNeighborsClassifier from sklearn.datasets import load_digits from sklearn.model_selection import train_test_split data = load_digits() X = data.data y = data.target X_train, X_test, y_train, y_test = train_test_split(X, y, random_state=666) knn_clf = KNeighborsClassifier() pca = PCA(n_components=0.95) pca.fit(X_train) # 打印主成分的个数 print(pca.n_components_) # 28 X_train_reduction = pca.transform(X_train) X_test_reduction = pca.transform(X_test) knn_clf.fit(X_train_reduction, y_train) print(knn_clf.score(X_test_reduction, y_test)) # 0.98
(六)使用PCA对数据进行降噪
大家看这张图,我们发现曲线并不平滑,有些时候随着主成分的增加,精确率反而还降低了,这是因为数据含有噪声。因此我们对数据进行降维的时候,会发现速度变快的同时,精确率还提高了一点,这是因为在降维的时候还进行了降噪。