PCA降维算法的Python实现

摘要

PCA算法本质实际上是n维数据向量空间的基变换,再将数据向量投影在变化之后的基之上,从而达到降维的目的,为了使n维数据向量在降维之后仍能尽可能保持原有的信息量,这就类似于有损压缩的概念(有损压缩是指使用压缩后的数据进行重构,重构后的数据与原来的数据有所不同,但不影响人对原始资料表达的信息造成误解)。所以基的选择自然显得十分重要。

信息熵

PCA降维的目的首先就是要尽可能保持其原有的信息量。那么信息的度量或者量化标准则成了一个关键问题。

所谓信息量是指从N个相等可能事件中选出一个事件所需要的信息度量或含量,也就是在辩识N个事件中特定的一个事件的过程中所需要提问"是或否"的最少次数. —《百度百科》

信息量与事件的不确定性有关

​ 如果一个事件,不论在什么情况下,发生的结果都不会发生变化,那我们就认为它所包含的信息量为0。比如说一个程序,在代码不变的情况下,只要输入固定其结果必然一定,不论我们运行多少次都是同样的结果,我们从中所能获取的信息几乎为0,因为这件事对我们来说已经是非常确定了的。我们说科学就是探索未知,因为未知包含了无限的可能,包含了无限的信息。

香农给出了信息度量的定义:信息是用不确定性的量度定义的.一个消息的可能性愈小,其信息愈多;而消息的可能性愈大,则其信息愈少.事件出现的概率小,不确定性越多,信息量就大,反之则少。

换句话说,信息的度量是与概率相关的。

依据Boltzmann's H-theorem,香农把随机变量X的熵值 Η定义如下,其值域为{x1, ..., *x**n*}:

其中,P为X的概率质量函数(probability mass function),E为期望函数,而I(X)是X的信息量。I(X)本身是个随机变数。

当取自有限的样本时,熵的公式可以表示为:

在这里b是对数所使用的底,通常是2,自然常数e,或是10。当b = 2,熵的单位是bit;当b = e,熵的单位是nat;而当b = 10,熵的单位是Hart。

*p**i* = 0时,对于一些i值,对应的被加数0 logb 0的值将会是0,这与极限一致。

还可以定义事件 XY 分别取 xiyj 时的条件熵为

其中p(xi, yj)为 X = xiY = yj 时的概率。这个量应当理解为你知道Y的值前提下随机变量 X 的随机性的量。

熵与方差

方差

方差的定义

方差越大数据的波动也就越大,其不确定性自然越大,方差描述不确定度在某些情况下会失效,因为它要求数据均匀分布并且忽略极端事件的发生。熵本就是对信息的不确定度的度量,相比于方差必然更加精确。

基的选择

举个栗子:

假设我们的数据由五条记录组成,将它们表示成矩阵形式:

其中每一列为一条数据记录,而一行为一个字段。为了后续处理方便,我们首先将每个字段内所有值都减去字段均值,其结果是将每个字段都变为均值为0(这样做的道理和好处后面会看到)。

我们看上面的数据,第一个字段均值为2,第二个字段均值为3,所以变换后:

我们可以看下五条数据在平面直角坐标系内的样子:

如果我们必须使用一维来表示这些数据,又希望尽量保留原始的信息,你要如何选择?

直观的说这个问题实际上是要在二维平面中选择一个方向,将所有数据都投影到这个方向所在直线上,用投影值表示原始记录。这是一个实际的二维降到一维的问题。

所以数据样本投影到基后的投影值尽可能分散。也就是使得数据变化到基上的坐标后,方差最大。

优化目标

在多维的情况下,基的维数也是多维,因此投影方向也变成了多维问题,且数据样本的字段存在相关性显然,为了使得样本不同字段之间完全独立(相关性意味着两个字段不是完全独立,必然存在重复表示的信息。),选择的基两两之间必然是正交的。即:

将一组N维向量降为K维(K大于0,小于N),其目标是选择K个单位(模为1)正交基,使得原始数据变换到这组基上后,各字段两两间协方差为0,而字段的方差则尽可能大(在正交的约束下,取最大的K个方差)

协方差矩阵

我们知道最终优化目标与字段内的方差和字段之间的协方差有关(字段之间独立)

对于矩阵x:

X乘以X的转置,并乘上系数1/m:

我们发现,对角线上的两个元素分别是两个字段的方差,而其它元素是a和b的协方差。要达到优化目前,等价于将协方差矩阵对角化:即除对角线外的其它元素化为0,并且在对角线上将元素按大小从上到下排列,这样我们就达到了优化目的。

设原始数据矩阵X对应的协方差矩阵为C,而P是一组基按行组成的矩阵,设Y=PX,则Y为X对P做基变换后的数据。设Y的协方差矩阵为D,我们推导一下D与C的关系:

变化矩阵P就是原始协方差矩阵对角化的P。

我们知道:协方差矩阵C是一个是对称矩阵,在线性代数上,实对称矩阵有一系列非常好的性质:

1)实对称矩阵不同特征值对应的特征向量必然正交。

2)设特征向量λ重数为r,则必然存在r个线性无关的特征向量对应于λ,因此可以将这r个特征向量单位正交化。

则对协方差矩阵C有如下结论:

对角元素为各特征向量对应的特征值,P对应与E的转置

P是协方差矩阵的特征向量单位化后按行排列出的矩阵,其中每一行都是C的一个特征向量。如果设P按照ΛΛ中特征值的从大到小,将特征向量从上到下排列,则用P的前K行组成的矩阵乘以原始数据矩阵X,就得到了我们需要的降维后的数据矩阵Y。

PCA算法

设有m条n维数据。

1)将原始数据按列组成n行m列矩阵X

2)将X的每一行(代表一个属性字段)进行零均值化,即减去这一行的均值

3)求出协方差矩阵C

4)求出协方差矩阵的特征值及对应的特征向量

5)将特征向量按对应特征值大小从上到下按行排列成矩阵,取前k行组成矩阵P

6)Y=PX即为降维到k维后的数据

结果示例

样本为符合[[-5,5],[5,-5]]的协方差矩阵的数据

Python实现代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
import numpy as np
import matplotlib.pyplot as plt
from sklearn.datasets import load_iris
from mpl_toolkits.mplot3d import Axes3D


class PCA(object):
def __init__(self, matrix):
self.sign = False
self.matrix = matrix
self.m, self.n = matrix.shape[:2]

def normalization(self): # 数据中心化
matrix = self.matrix.T
for i in matrix:
i -= np.mean(i)
return matrix

def pca(self, dim):
matrix_T = self.normalization()
Cov = np.dot(matrix_T, self.matrix) / (self.m - 1)
x, y = np.linalg.eig(Cov)
eig_pairs = [[np.abs(x[i]), y[:, i]] for i in range(len(x))]
eig_pairs = sorted(eig_pairs, reverse=True)

Lambda = np.zeros_like(Cov)
for i in range(self.n):
Lambda[i][i] = eig_pairs[i][0]
# print("\nLambda\n", Lambda)
self.sign = True
if dim > self.n:
print("illegal dimensions")
return False
P = np.array([eig_pairs[i][1] for i in range(dim)]) # 正交变换矩阵P
# print("\n正交变换矩阵P:\n", P)
D = np.dot(np.dot(P, Cov).T, P)
D[D < 0.000001] = 0
# print("\n目标降维矩阵的协方差矩阵D\n", D)
new_data_reduced = np.dot(self.matrix, P.T)
print("\n", 20 * '-', "PCA降到{}维数据前10项".format(dim), 20 * '-', "\n", new_data_reduced[:10])
return new_data_reduced


def draw(ax, data, y):
X_lda = data
dim = X_lda.shape[1]
if X_lda.shape[1] == 1:
X_lda = np.array([[i[0], 1] for i in X_lda])
if dim == 3:
ax = plt.subplot(111, projection='3d')
label_dict = {0: 'Setosa', 1: 'Versicolor', 2: 'Virginica'}
for label, marker, color in zip(
range(0, 3), ('^', 's', 'o'), ('blue', 'red', 'green')):
plt.scatter(x=X_lda[:, 0][y == label],
y=X_lda[:, 1][y == label],
marker=marker,
color=color,
alpha=0.5,
label=label_dict[label]
)

leg = plt.legend(loc='upper right', fancybox=True)
leg.get_frame().set_alpha(0.5)
ax.set_title('PCA: Iris onto the first {} linear discriminants'.format(dim))
# hide axis ticks
plt.tick_params(axis="both", which="both", bottom="off", top="off",
labelbottom="on", left="off", right="off", labelleft="on")

# remove axis spines
ax.spines["top"].set_visible(False)
ax.spines["right"].set_visible(False)
ax.spines["bottom"].set_visible(False)
ax.spines["left"].set_visible(False)

plt.grid()
plt.tight_layout()



if __name__ == "__main__":
data = load_iris()['data']
label = load_iris()['target']
label_dict = {0: 'Setosa', 1: 'Versicolor', 2: 'Virginica'}
pca = PCA(data)
pca_2d = pca.pca(2)
pca_3d = pca.pca(3)
fig = plt.figure(figsize=(12, 6))
ax1 = fig.add_subplot(121)
draw(ax1,pca_2d, label)
ax2 = fig.add_subplot(122, projection='3d')
ax2.scatter(*zip(*pca_3d),c=label)
ax2.set_title("PCA: Iris onto the first 3 linear discriminants",size=20)
plt.show()