引入
还在担心由于维数过多而导致的维数灾难么?
还在为数据的噪音而烦恼么?
那么使用PCA,是你的不二之选
原理
假设要把一些二维的点降到一维,要如何去做?
很自然想到可以直接投射到坐标轴上
对于映射到左边轴上,有两种选择,X轴与Y轴。
这两种映射哪一种更能表达原来的信息呢?
可以发现对于向X轴映射这种方式,点之间的距离更松散,而对于向Y轴映射则更密集,点之间越密集他们之间的区别就越不明显,所以可以得出,向X轴映射可以更好的保存原来的信息。
那么向X轴映射是否就是最优的降维方式呢?
很明显,并不一定。
如果映射在这么一条斜着的轴上可能就能得保留更多的信息。
而寻找这样的一个个轴,就是PCA需要做的事情
理论推导
接下来设样本矩阵 X X X为 m × n m ×n m×n的矩阵
首先我们需要一个量来描述样本之间的疏密程度,在统计学中有一个量正好合适,那就是方差。
为了方便进行计算,首先需要进行一个demean操作,这个操作有点类似于均值归一化,但又有所不同。
def demean(data: np.array):
return data - np.mean(data, axis=0)
这个操作就是把样本的均值放在0的位置,而方差不进行归一处理。
此时进行demean处理后要映射到的这个轴就会过原点,此时就可去设这条轴了。
设要映射到的轴是 w = ( x , y ) T w = (x,y)^T w=(x,y)T(记为w轴)这里用一个单位向量来表示轴的方向。
然后把每一个样本看作一个向量,就可以很容易的得到样本点在w轴上的映射。
假设一个样本为 a = ( x 1 , y 1 ) T a=(x_1, y_1)^T a=(x1,y1)T。
由于 w ⋅ a = ∣ w ∣ ⋅ ∣ a ∣ ⋅ c o s < a , b > w \cdot a=|w|\cdot|a|\cdot cos<a, b> w⋅a=∣w∣⋅∣a∣⋅cos<a,b>由于 ∣ w ∣ = 1 |w|=1 ∣w∣=1所以,此时 w ⋅ a = ∣ a ∣ ⋅ c o s < a , b > w \cdot a=|a|\cdot cos<a, b> w⋅a=∣a∣⋅cos<a,b>
所以得到样本点与方向向量w的点乘即为该样本点在w方向上的映射。
那么对于每一个映射他们的方差就是
v a r = 1 m ∑ i = 1 m ( X ( i ) ⋅ w ) 2 var=\frac{1}{m}\sum\limits_{i=1}^{m}(X^{(i)}\cdot w)^2 var=m1i=1∑m(X(i)⋅w)2
这个算式中只有s是未知的,所以我们要进行的操作就是求出一个s使 v a r var var函数最大化,很明显,对于求这种的最大或者最小值,可以用梯度(下降)上升法来进行求解,至此主成分分析的基本理论就出来了。
梯度上升法求单个主成分
求var值
梯度上升法需要求var的值,根据上述的表达式很容易可以求出var的值
def FunValue(self, data: np.array, w: np.array):
return np.sum(data.dot(w) ** 2) / len(data)
求var的梯度
这里需要明确,这里的未知量是w里的几个量,所以求梯度时就是对其进行求偏导。
这里为了得到一般性,所以假设w有n个维度记 w = ( x 1 , x 2 , . . . , x n ) w=(x_1, x_2, ...,x_n) w=(x1,x2,...,xn)
先把var函数进行一下改写方便求导。
v a r = 1 m ∑ i = 1 m ( X 1 ( i ) ∗ w 1 + X 2 ( i ) ∗ w 2 . . . + X n ( i ) ∗ w n ) 2 var=\frac{1}{m}\sum\limits_{i=1}^{m}(X^{(i)}_1 * w_1 + X^{(i)}_2 * w_2... + X^{(i)}_n * w_n)^2 var=m1i=1∑m(X1(i)∗w1+X2(i)∗w2...+Xn(i)∗wn)2
然后写出梯度的形式
L = ( ∂ v a r ∂ x 1 ∂ v a r ∂ x 2 ∂ v a r ∂ x 3 . . . ∂ v a r ∂ x n ) L = \begin{gathered} \begin{pmatrix} \frac{\partial var}{\partial x_1}\\ \frac{\partial var}{\partial x_2}\\ \frac{\partial var}{\partial x_3}\\ ...\\ \frac{\partial var}{\partial x_n}\\ \end{pmatrix} \end{gathered} L=⎝⎜⎜⎜⎜⎜⎛∂x1∂var∂x2∂var∂x3∂var...∂xn∂var⎠⎟⎟⎟⎟⎟⎞
得出 L = 2 m ( ∑ i = 1 m ( X 1 ( i ) ∗ w 1 + X 2 ( i ) ∗ w 2 . . . + X n ( i ) ∗ w n ) X 1 ( i ) ∑ i = 1 m ( X 1 ( i ) ∗ w 1 + X 2 ( i ) ∗ w 2 . . . + X n ( i ) ∗ w n ) X 2 ( i ) . . . . ∑ i = 1 m ( X 1 ( i ) ∗ w 1 + X 2 ( i ) ∗ w 2 . . . + X n ( i ) ∗ w n ) X n ( i ) ) L = \frac{2}{m}\begin{gathered} \begin{pmatrix} \sum\limits_{i=1}^{m}(X^{(i)}_1 * w_1 + X^{(i)}_2 * w_2... + X^{(i)}_n * w_n)X_1^{(i)}\\ \sum\limits_{i=1}^{m}(X^{(i)}_1 * w_1 + X^{(i)}_2 * w_2... + X^{(i)}_n * w_n)X_2^{(i)}\\ ....\\ \sum\limits_{i=1}^{m}(X^{(i)}_1 * w_1 + X^{(i)}_2 * w_2... + X^{(i)}_n * w_n)X_n^{(i)} \end{pmatrix} \end{gathered} L=m2⎝⎜⎜⎜⎜⎜⎜⎜⎛i=1∑m(X1(i)∗w1+X2(i)∗w2...+Xn(i)∗wn)X1(i)i=1∑m(X1(i)∗w1+X2(i)∗w2...+Xn(i)∗wn)X2(i)....i=1∑m(X1(i)∗w1+X2(i)∗w2...+Xn(i)∗wn)Xn(i)⎠⎟⎟⎟⎟⎟⎟⎟⎞
为了方便操作,可以把上述式子转化成矩阵的形式。
L = 2 m X T ∗ X ∗ w L=\frac{2}{m}X^T * X * w L=m2XT∗X∗w
上述的乘法均为矩阵的乘法。
那么就可以轻松地写出梯度的计算了。
def DFun(self, data: np.array, w: np.array):
return data.T.dot(data.dot(w)) * 2 / len(data)
到了这里就可以套用梯度上身法的模板了,这里我的上一篇博客写过了,所以就先不写了。
求前k个主成分
理论
当求出第一个主成分后,就可以把想样本映射到这个轴上了。
但是如果想把一个n维样本映射到k维空间中,显然只有一个坐标轴是不够的,那么此时就需要接连求出剩下k-1个主成分。
首先由于已经映射到了一个轴上,所以所有样本在这个轴上的分量都应该去掉,因为这样去掉之后接下来再求的主成分就会与这个轴无关。
由于 w ⋅ a = ∣ a ∣ ⋅ c o s < a , b > w \cdot a=|a|\cdot cos<a, b> w⋅a=∣a∣⋅cos<a,b>
记
所以两边同时乘以w得到
w ( w ⋅ a ) = w ( ∣ a ∣ ⋅ c o s < a , b > ) w(w \cdot a)=w(|a|\cdot cos<a, b>) w(w⋅a)=w(∣a∣⋅cos<a,b>)
注意 ( w ⋅ a ) (w \cdot a) (w⋅a)是一个常数,而不是向量。
据此就到该样本在w轴上的分向量了,那么减去这个分向量,就去除了该样本在这个轴上的成分。
X = demean(X - X.dot(w.reshape(-1, 1)) * w)
画图也可以看出
所有样本去除第一主成分后是和第一主成分的方向垂直的。
去除掉这个成分之后剩下的X就又是一个全新的样本矩阵,就可以再拿它求出第二个主成分,然后周而复始,就可以求出前k个主成分了。
由于前k个主成分相互垂直,所以前k个主成分的方向就可以当做k维的坐标轴
代码实现
定义一个PCA类,初始化一些变量, n_component用来表示要降到的维数,用component来存前k维矩阵。
class PCA:
def __init__(self, n_component=2):
self.n_component = n_component
self.components_ = None
加入求导与求值。
class PCA:
def __init__(self, n_component=2):
self.n_component = n_component
self.components_ = None
def FunValue(self, data: np.array, w: np.array):
return np.sum(data.dot(w) ** 2) / len(data)
def DFun(self, data: np.array, w: np.array):
return data.T.dot(data.dot(w)) * 2 / len(data)
然后写出梯度上升法。
这里需要添加一个新的函数standard函数,用来把向量变成一个单位向量,同时也把demean函数加入进来。
def demean(data: np.array):
return data - np.mean(data, axis=0)
def standard(w: np.array):
return w / np.sum(w ** 2) ** 0.5
class PCA:
def __init__(self, n_component=2, ratios=0.95):
self.n_component = n_component
self.components_ = None
self.ratios = ratios
self.vars = None
def FunValue(self, data: np.array, w: np.array):
return np.sum(data.dot(w) ** 2) / len(data)
def DFun(self, data: np.array, w: np.array):
return data.T.dot(data.dot(w)) * 2 / len(data)
def GradientAscent(self, init_w: np.array, X: np.array, eps: float = 1e-8, maxloop: int = 60000,
sep: float = 100):
X_pca = demean(X)# 用来存当前的样本矩阵
w_d = np.empty(shape=(self.n_component, X.shape[1])) # 用来临时存前k维主成分向量
for i in range(self.n_component):# 循环n_component次把前n_component个主成分都给求出来
w = standard(init_w)
count = 0
while True:
next = standard(w + sep * self.DFun(w=w, data=X_pca))
if abs(self.FunValue(X_pca, w) - self.FunValue(X_pca, next)) < eps:
w_d[i] = next.T# 求出一个存一个
break
w = next
count += 1
if count == maxloop:
return None
X_pca = demean(X_pca - X_pca.dot(w_d[i].reshape(-1, 1)) * w_d[i])# 去除当前主成分方向的分量。
self.components_ = w_d # 把前k个主成分它存起来
降维
原理
观察前K个主成分的方向构成的矩阵,设前k个主成分构成的矩阵为:
W = ( W 1 ( 1 ) W 2 ( 1 ) . . . W n ( 1 ) W 1 ( 2 ) W 2 ( 2 ) . . . W n ( 2 ) . . . W 1 ( k ) W 2 ( k ) . . . W n ( k ) ) W =\begin{gathered} \begin{pmatrix} W^{(1)}_1 W^{(1)}_2...W^{(1)}_n\\ W^{(2)}_1 W^{(2)}_2...W^{(2)}_n\\ ...\\ W^{(k)}_1 W^{(k)}_2...W^{(k)}_n\\ \end{pmatrix} \end{gathered} W=⎝⎜⎜⎜⎛W1(1)W2(1)...Wn(1)W1(2)W2(2)...Wn(2)...W1(k)W2(k)...Wn(k)⎠⎟⎟⎟⎞
我们可以知道第一行是第一个坐标轴,第二行是第二个,所我们希望每个样本分别点乘他们,这样就可以转化一个k维的样本了。所以得出降维的方法就是 X ′ = X ∗ W T X'=X*W^T X′=X∗WT。
从矩阵乘法的角度分析,X为 m × n m×n m×n的矩阵,W为 k × n k×n k×n的矩阵,所以 X ∗ W T X*W^T X∗WT为 m × k m×k m×k的矩阵。
至此就成功的把高维的数据进行了降维。
def __transform(self, X: np.array, k: int):
return X.dot(self.components_[: k].T)
使用降维可视化数据
以sklearn的手写数字为例。
头文件导入
import numpy as np
import matplotlib.pyplot as plt
from sklearn import datasets
导入手写数字
digits = datasets.load_digits()
X = digits.data
y = digits.target
使用上面写好的PCA,然后用matplotlib显示数字
pca = PCA()
pca.GradientAscent(init_w=standard(np.ones(shape=(X.shape[1], 1))), X=X)
X = pca.TransformByK(X) # 降维
for i in range(10):
plt.scatter(X[y == i][:, 0], X[y == i][:, 1], label=str(i))
plt.legend()
plt.show()
可以比较清晰地看到各个数字之间的区别
使用降维加速
我们知道,KNN在面对维度过高的数据时分类所需的时间会大大加长,而使用pca进行降维后可以比较好的缓解这种情况。
但是又一个问题出现了,随着维度的降低,数据会难免损失一些信息,降得太低会导致预测的准确率下降,而太高又无法提高预测的效率,所以这里需要一个办法来确定究竟要降到多少维度才能使准确率降低不高,而速度又可以得到比较大的提升。
回到刚开始的梯度上升,梯度上升是让方差达到最大值,而从第1主成分到第n主成分,他们的方差是越来越小的,而我们可以认为方差的大小就是原数据信息保留的大小,所以我们可以根据这些主成分上的方差所占总体方差的比例来取这个k的值,我们把主成分的方差所占的比例进行累加,如果这个值大于一定比例就可以确定此时取的前k个主成分最佳。
使用降维去噪
由于在降维过程中不可避免的会损失信息,从而也会去除掉一些噪音,在降维到 k k k维后,仍可以将数据恢复到 n n n维,但是损失的部分不会回来了。
我们把降到k维的 m ∗ k m*k m∗k大小的样本矩阵 X X X,乘以原本的 k ∗ n k*n k∗n大小的 W W W矩阵,就可以把还原出原来的矩阵,不过此矩阵几乎不肯能和原矩阵一致。
在这个过程中我们可以认为我们去除掉了一些噪音。
写出一个reTransform函数,用于还原数据
def reTransForm(self, X: np.array):
return X.dot(self.components_[: self.n_component])
为了方便可视化,这里使用一个线性的例子,下面这是去噪以前。
把它降到一维然后还原去噪
X = np.hstack([X, y])
pca = PCA(n_component=1)
pca.gradientAscent(X)
X = pca.reTransForm(pca.transFormByK(X))
plt.scatter(X[:, 0], X[:, 1])
plt.show()
黄色就是去噪之后的数据,可以发现,它更加的有规律,不再那么杂乱。