主成分分析原理与实现

  主成分分析是一种矩阵的压缩算法,在减少矩阵维数的同时尽可能的保留原矩阵的信息,简单来说就是将 \(n×m\)的矩阵转换成\(n×k\)的矩阵,仅保留矩阵中所存在的主要特性,从而可以大大节省空间和数据量。最近课上学到这个知识,感觉很有意思,就在网上找一些博客进行学习,发现网上关于这方面的介绍很多,但是感觉都不太全面,单靠某一个介绍还是无法理解,当然这可能也跟个人基础有关。所以我在这里根据自己的理解写一个总结性的帖子,与大家分享同时也方便自己复习。对于主成分分析,可以参照以下几篇博客:

  1. PCA的数学原理该博客介绍了主成分中的数学原理,给出了比较清晰的数学解释。简单易懂,但是有一些细节并没有涉及到,所以还是不能完全理解。

  2. PCA 原理:为什么用协方差矩阵介绍了为什么在降维的时候采用协方差矩阵,但是对于协方差矩阵的解释不详细。

  3. 关于协方差矩阵的理解对协方差矩阵的进行了详细的推导,解释了为什么可以通过\(A A^T\)来计算协方差矩阵。

  4. 矩阵求导、几种重要的矩阵及常用的矩阵求导公式对矩阵求导进行了介绍。提到了可能会用到的一些求导公式。

  5. UFLDL 教程学习笔记(四)主成分分析对主成分的原理和使用进行了介绍。

1. 数学原理

  数学原理的介绍部分可以参考文献1,该博客对主成分分析的数学原理进行了很直观的介绍。这里我根据自己的理解进行简单介绍。

*图一(图片来源于文献 1)*

  对于一个坐标点\((3,2)\),我们知道其代表的意思是在二维坐标里其横坐标为3,纵坐标为2。其实这隐含了一个假设,即其横纵坐标的基为\((1,0)和(0,1)\)。对于一般的二维向量,这似乎是大家的默认情况,就像随便给出一个数字\(10\),大家会认为这是\(10\)进制表示,除非特殊标明,不会把它当作其他进制来理解。对于任意一个坐标点\((x,y)\),我们可以将其表示为:

\[\begin{pmatrix}
1 & 0 \\
0 & 1 \\
\end{pmatrix} \cdot
\begin{pmatrix}
x \\
y \\
\end{pmatrix} =
\begin{pmatrix}
x \\
y \\
\end{pmatrix}
\]

其中\(\begin{pmatrix}
1 & 0 \\
0 & 1 \\
\end{pmatrix}\)的每一个行向量代表一个基向量。

如果我想更换基向量怎么办呢,如上图所示,如果我想知道\((3,2)\)在\((\sqrt{2}/2,\sqrt{2}/2)与(-\sqrt{2}/2,\sqrt{2}/2)\)基下的坐标值,该如何计算呢?回顾基本的数学知识,我们发现对于一个向量在一个基上的值其实就是该向量在该基向量上的投影。所以,已知基向量,我们可以很容易求得,对于一个向量,如\((3,2)\),其在基\((\sqrt{2}/2,\sqrt{2}/2)与(-\sqrt{2}/2,\sqrt{2}/2)\)上的投影为:

\[\begin{pmatrix}
\sqrt{2}/2 & \sqrt{2}/2 \\
-\sqrt{2}/2 & \sqrt{2}/2 \\
\end{pmatrix} \cdot
\begin{pmatrix}
3 \\
2 \\
\end{pmatrix} =
\begin{pmatrix}
5\sqrt{2}/2 \\
-\sqrt{2}/2 \\
\end{pmatrix}
\]

直观的图表示如上图所示。

  再回到主成分分析上来,如果我们想对一个矩阵\(A\)进行降维,其中\(A\)为:

\[\begin{pmatrix}
a_{11} & a_{12} & \cdots & a_{1n} \\
a_{21} & a_{22} & \cdots & a_{2n} \\
\vdots & \vdots & \ddots & \vdots \\
a_{m1} & a_{m2} & \cdots & a_{mn} \\
\end{pmatrix}
\]

行向量代表样本,列向量代表特征,所以其矩阵含义为m个具有n个特征的样本值。对于每一个样本具有的n个特征值,其特征值之间可能会存在很大的耦合,就如文献1中所列举的那样,特征M代表是否为男性,特征F代表是否为女性,因为一个人的性别只能为其中的一个(不考虑特殊情况)。所以这两个特征只留一个就行了,所以就可以省下一半的空间。这个例子有些极端,但是并不影响理解。

*图二(图片来源于网络)*

  同样对于一个具有n个特征的集合来说,很难说这n个特征都是完全有必要的,所以我们就想办法来精简一些特征。选取少于n个的基向量组,将数据投影在这个向量组上,减少空间的同时又能保证信息量。首先需要明确的一点是什么才算好的基向量?首先举一个将二维空间的数据投影到一维空间的情况。如上图所示,对于空间中的这些点,我们应该怎么投影才能够尽可能的保持数据的信息量呢?通过上图中可以看出,如果将数据投影到PC1上,那么所有的数据点较为分散,与之相反,如果投影到PC2上,则数据较为集中。考虑一个极端的情况,假如所有的点在投影之后全部集中在一个点上,这样好吗?当然不!如果所有的点都集中到一个点上,那就说明所有的点都没有差别,信息全部丢失了。所以我们希望当数据点投影到某个坐标轴之上以后,数据越分散越好,而衡量一组数据是否发散恰好有一个统计名词“方差”,也就是说投影过后的点值方差越大越好。同时,如果数据被投影到多个基向量上,那么我们希望这些基向量之间的耦合程度越小越好,也就说基向量之间应该是正交的,如图三所示(建议点击链接去相应网站查看3D演示)。因为如果不考虑基向量之间的正交性,只考虑方差最大的话,那么所求得的值其实都是一样的。关于在不同的基向量上的投影的线性相关度也有一个度量标准--协方差。那么我们的目标明确了,使得相同特征之间方差越大越好,不同特征之间协方差越小越好

[*图三(参考文献【6】)*](http://setosa.io/ev/principal-component-analysis/)

  那么这些方差,协方差什么的怎么计算呢?这里可以先给出一个结论,将\(A\)向量的每一列减去该列的平均值得到一个新的\(A\)矩阵。然后计算\(Cov=1/m \cdot A^T\cdot A\),得到一个\(n\times n\)的矩阵\(Cov\),那么\(Cov\)的对角线上的元素\(c_{ii}\)即为第i个特征的方差,对于其他元素\(c_{ij}\)表示第i个和第j个特征的协方差,很明显该矩阵是对称矩阵。关于该矩阵的求解方式可以参考文献3,其介绍的很详细,这里就不再重复。需要注意的一点是这里\(Cov=1/m \cdot A^T\cdot A\)是因为A矩阵的列向量为特征,所以才这样计算。如果A矩阵的行列向量所表达的含义相反则\(Cov=1/m \cdot A\cdot A^T\)。

  已经知道了计算协方差矩阵的方法,下面看一下怎么跟我们要做的结合在一起。再次总结一下我们要做的是什么,对于一个已有的矩阵\(A\),我们希望将它投影在一组新的基空间上,使之矩阵大小得到压缩。即:

\[D_{m,N} = A_{mn} \cdot P_{nN}, \,\,\,\,given (N<n)
\]

我们要做的就是将n个特征压缩为N个特征。对于压缩过的数据投影,根据上面的叙述可知,我们希望对于相同特征之间方差越大越好,不同特征之间协方差越小越好,并且我们已经知道该如何计算方差和协方差了。

\[Cov(D)_{NN} = D^T \cdot D = P^T A^T A P.
\]

所以现在的目标很明确,我们要做的就是求得\(P\),使得\(Cov(D)\)的对角线元素尽可能大,非对角线元素尽可能小。学过线性代数的应该都知道,对于\(A^T A\)矩阵来说,其特征向量就满足这一条件。因为已知\(A^T A\)矩阵为对称矩阵,所以可知:

\[P^T (A^T A) P = P^{-1} (A^T A) P = \Lambda
\]

其中\(\Lambda\)为\(A^T A\)的特征值组成的对角阵,\(P\)为相应的的特征向量组。

  至此,我们就找到了进行主成分分析的方法:

  1. 首先对矩阵A进行处理,使得其每一列(或者行)减去其相应列的平均值,使得每一列的平均值都为0,然后计算\(B = A^TA\)。
  2. 求B矩阵的特征值和特征向量,将特征值进行排序,并选取前N大的特征值,选取其对应的特征向量组成特征向量组\(P_{nN}\)。
  3. \(D_{m,N} = A_{mn} \cdot P_{nN}\)即为最终想要得到的值。

2.实验验证

  下面我们对该算法进行实际的实现,为了更好的了解PCA的工作原理,同时又保证程序的计算速度,我才用了C语言进行实现,并借助OpenBLAS库进行高效的矩阵运算。OpenBLAS是BLAS标准的一个开源实现,据说也是目前性能和维护的最好的一个。BLAS是Basic Linear Algebra Subprograms的简称,是一个矩阵运算的接口标准。既然是接口标准,那么所有根据该标准的实现都具有相同的使用方式和功能。相似的实现还有BLAS、MKL、ACML等,我使用OpenBLAS进行实现,因为其实现不依赖于任何平台,具有良好的性能,而且亲测易于安装。下面将附上我的实现代码:

//矩阵运算部分 Matrix.cpp
#include<iostream>
#include<stdio.h>
#include<stdlib.h>
//#include "mkl.h"
#include"OpenBLAS/cblas.h"
class Matrix
{
public:
//Print matrix;
bool printMatrix() const;
//get r.
int getr() {return r;}
//get l.
int getc() {return c;}
//get a.
float *geta() {return a;}
//normalization.
void nmlt();
//Compute Coevariance of a, aTxa
void coev(Matrix &c);
//Default constructor.
Matrix():a(NULL), r(0), c(0) {}
//Constructor with matrix pointer and dimension.
Matrix(float *aa, int rr, int cc): a(aa), r(rr), c(cc) {}
//Constructor with only dimension, should allocate space.
Matrix(int rr, int cc): r(rr), c(cc)
{
a = new float[rr*cc];
}
//Destructor.
~Matrix() {delete []a; a=NULL;} protected:
//Matrix pointer.
float *a;
//Dimension n, order lda
int r,c;
}; extern bool printArray(float *p, int n); class SquareMatrix:public Matrix
{
public:
//Default constructor.
SquareMatrix(float *aa, int nn):Matrix(aa, nn, nn), n(nn) {}
SquareMatrix(int nn): Matrix(nn, nn), n(nn){}
//Destructor.
~SquareMatrix() {}
//Get eigenvalue and eigenvector;
int ssyevd(float *w); private:
int n;
};
bool Matrix::printMatrix() const
{
int i=0, j=0;
float temp(0);
for(i=0; i<r; i++)
{
for(j=0; j<c; j++)
{
temp = *(a+c*i+j);
printf("%7.3f\t", temp);
}
std::cout<<std::endl;
}
} int SquareMatrix::ssyevd(float *w)
{
lapack_int res = 0;
res = LAPACKE_ssyevd(LAPACK_ROW_MAJOR, 'V', 'U', n, a, n, w);
if(res == 0)
{
return res;
}
else
{
std::cout<<"ERROR:"<<res<<std::endl;
exit(-1);
}
} void Matrix::coev(Matrix &cc)
{
nmlt();
cblas_sgemm(CblasRowMajor, CblasTrans, CblasNoTrans, c, c, r, 1.0/r, a, c, a, c, 0.0, cc.geta(), c);
} void Matrix::nmlt()
{
int i=0,j=0;
float av = 0.0;
for(i=0;i<c;i++)
{
av = 0.0;
for(j=0;j<r;j++)
{
av+=*(a+i+j*c);
}
av = av/r;
for(j=0;j<r;j++)
{
*(a+i+j*c) -= av;
}
}
} bool printArray(float *p, int n)
{
for(int i=0; i<n; i++)
{
printf("%7.3f\t", p[i]);
}
std::cout<<std::endl;
return true;
}
//PCA部分 PCA.cpp
#include<iostream>
#include<stdio.h>
#include<stdlib.h>
//#include "mkl.h"
#include"OpenBLAS/cblas.h"
#include"Matrix.h"
#include"PCA.h" #define N 5
#define T 0.8f
const char SEP = ','; static unsigned int R = 5;
static unsigned int C = 5; int main(int argc, char *argv[])
{
// float *A = new float [N*N]
// {
// 1.96f, -6.49f, -0.47f, -7.20f, -0.65f,
// -6.49f, 3.80f, -6.39f, 1.50f, -6.34f,
// -0.47f, -6.39f, 4.17f, -1.51f, 2.67f,
// -7.20f, 1.50f, -1.51f, 5.70f, 1.80f,
// -0.65f, -6.34f, 2.67f, 1.80f, -7.10f
// };
if(argc <= 1)
{
printf("Usage: PCA [INPUT FILE] [OUTPUT FILE] [ROW] [COLUM]\n");
printf("INPUT FILE: input file path.\n");
printf("OUTPUT FILE: output file path.\n");
printf("ROW: Row of matrix.\n");
printf("COLUM: Colum of matrix.\n");
exit(0);
}
FILE *input = fopen(argv[1], "r");
FILE *output = fopen(argv[2], "w+");
R = atof(argv[3]);
C = atof(argv[4]);
printf("Input:%s\nOutput:%s\nR:%d\nC:%d\n",argv[1], argv[2], R, C);
float *I = new float[R*C]();
//float *O = new float[R*C]();
char *label = new char[R];
//read matrix.
readMtx(input, I, label); SquareMatrix cov = SquareMatrix(C);
float *eValue = new float[C]();
Matrix m = Matrix(I, R, C);
Matrix n = Matrix(R, C);
// m.printMatrix();
//compute coveriance matrix.
m.coev(cov);
//compute eigenvalue and eigenvector of coveriance matrix.
cov.ssyevd(eValue);
//Compute compressed matrix.
eMtx(m, cov, n);
//n.printMatrix();
saveMtx(output, n.geta(), label); fclose(input);
fclose(output);
delete []label;
delete []eValue;
return 0;
} //eigen matrix
void eMtx(Matrix&a, Matrix&b, Matrix&r)
{
cblas_sgemm(CblasRowMajor, CblasNoTrans, CblasNoTrans, a.getr(), b.getc(), a.getc(), 1.0, a.geta(), a.getc(), b.geta(), b.getc(), 0.0, r.geta(), b.getc());
} bool readUtl(FILE *f, char sep)
{
char c;
if((c=fgetc(f))!=EOF && c==sep)
{
return true;
}
return false;
} void readMtx(FILE *f, float *m, char *la)
{
float ft(0.0);
char ch;
int i(0),j(0),index(0);
while(i<R)
{
while(!readUtl(f, SEP));
la[i++] = fgetc(f);
readUtl(f, SEP);
for(j=0;j<C-1;j++)
{
fscanf(f, "%f,", &m[index++]);
}
fscanf(f, "%f", &m[index++]);
while(!readUtl(f, '\n') && i<R);
}
} void saveMtx(FILE *f, float *m, char *la)
{
int i(0),j(0);
for(i=0;i<R;i++)
{
fprintf(f, "%c,", la[i]);
for(j=0;j<C-1;j++)
{
fprintf(f, "%.4f,", m[i*C+j]);
}
fprintf(f, "%.4f", m[i*C+j]);
fprintf(f, "\n");
}
}

编译运行:

./PCA wdbc.data wdbc.out 569 30

本文所采用的实验数据为开源数据集,该数据集是有关于乳腺癌诊断的相关数据,共有569条记录,每一个记录有30个特征,并且每一条记录都有一个标签,标签为'B'意味着良性,'M'意味着恶性。上述代码对该数据集继续主成分分析,最后将输出矩阵保存在wdbc.out中。

下面我通过散点图的方式直观的展示分析的效果:

PCA一维映射
其中绿色代表良性,红色代表恶性。从图中可以看出,即使仅映射到一维,不同类别的数据似乎就已经很容易分离开了,这是因为我们选取的这个一维空间正是最大的那个特征值对应的空间,所以包含最多的信息。接下来我们将数据映射到二维和三维空间:

PCA二维映射
\\

PCA三维映射

参考文献

[1]http://blog.codinglabs.org/articles/pca-tutorial.html

[2]https://blog.csdn.net/a10767891/article/details/80288463

[3]https://blog.csdn.net/itplus/article/details/11452743#commentsedit

[4]https://blog.csdn.net/daaikuaichuan/article/details/80620518

[5]https://blog.csdn.net/itplus/article/details/11451327

[6]http://setosa.io/ev/principal-component-analysis/

主成分分析(PCA)原理与实现的更多相关文章

  1. 机器学习之主成分分析PCA原理笔记

    1.    相关背景 在许多领域的研究与应用中,通常需要对含有多个变量的数据进行观测,收集大量数据后进行分析寻找规律.多变量大数据集无疑会为研究和应用提供丰富的信息,但是也在一定程度上增加了数据采集的 ...

  2. 主成分分析(PCA)原理及推导

    原文:http://blog.csdn.net/zhongkejingwang/article/details/42264479 什么是PCA? 在数据挖掘或者图像处理等领域经常会用到主成分分析,这样 ...

  3. 主成分分析(PCA)原理详解_转载

    一.PCA简介 1. 相关背景 在许多领域的研究与应用中,往往需要对反映事物的多个变量进行大量的观测,收集大量数据以便进行分析寻找规律.多变量大样本无疑会为研究和应用提供了丰富的信息,但也在一定程度上 ...

  4. 主成分分析PCA数据降维原理及python应用(葡萄酒案例分析)

    目录 主成分分析(PCA)——以葡萄酒数据集分类为例 1.认识PCA (1)简介 (2)方法步骤 2.提取主成分 3.主成分方差可视化 4.特征变换 5.数据分类结果 6.完整代码 总结: 1.认识P ...

  5. 线性判别分析(LDA), 主成分分析(PCA)及其推导【转】

    前言: 如果学习分类算法,最好从线性的入手,线性分类器最简单的就是LDA,它可以看做是简化版的SVM,如果想理解SVM这种分类器,那理解LDA就是很有必要的了. 谈到LDA,就不得不谈谈PCA,PCA ...

  6. 一步步教你轻松学主成分分析PCA降维算法

    一步步教你轻松学主成分分析PCA降维算法 (白宁超 2018年10月22日10:14:18) 摘要:主成分分析(英语:Principal components analysis,PCA)是一种分析.简 ...

  7. PCA原理分析

    动机 在机器学习领域中,我们常常会遇到维数很高的数据,有些数据的特征维度高达上百万维,很显然这样的数据是无法直接计算的,而且维度这么高,其中包含的信息一定有冗余,这时就需要进行降维,总的来说,我们降维 ...

  8. 主成分分析(PCA)模型概述

    数据降维 降维是对数据高维度特征的一种预处理方法.降维是将高维度的数据保留下最重要的一些特征,去除噪声和不重要的特征,从而实现提升数据处理速度的目的.在实际的生产和应用中,降维在一定信息损失范围内,可 ...

  9. 运用sklearn进行主成分分析(PCA)代码实现

    基于sklearn的主成分分析代码实现 一.前言及回顾 二.sklearn的PCA类介绍 三.分类结果区域可视化函数 四.10行代码完成葡萄酒数据集分类 五.完整代码 六.总结 基于sklearn的主 ...

随机推荐

  1. KMP算法计算next值和nextVal值

    KMP算法: 给定一个主串S及一个模式串P,判断模式串是否为主串的子串:若是,返回匹配的第一个元素的位置(序号从1开始),否则返回0: 这里先不写算法,仅仅计算next和nextVal值 那么计算时只 ...

  2. Java面试题-基础篇一(干货篇)

    1.一个".java"源文件中是否可以包括多个类(不是内部类)?有什么限制? 可以有多个类,但只能有一个public的类,并且public的类名必须与文件名相一致. 2.Java有 ...

  3. html里js的execCommand的一点用法

    editorDoc.execCommand ('italic', false, null); 添加斜体 参考 http://help.dottoro.com/ljcvtcaw.php

  4. Vue生命周期钩子---2

    vue生命周期简介 咱们从上图可以很明显的看出现在vue2.0都包括了哪些生命周期的函数了. 生命周期探究 对于执行顺序和什么时候执行,看上面两个图基本有个了解了.下面我们将结合代码去看看钩子函数的执 ...

  5. C# 深入分析 GC 处理机制

    引用地址:https://www.cnblogs.com/nele/p/5673215.html GC的前世与今生 虽然本文是以.NET作为目标来讲述GC,但是GC的概念并非才诞生不久.早在1958年 ...

  6. python3.5.3rc1学习十一:字典与模块

    #os模块import oscurDir = os.getcwd()print(curDir) os.mkdir("新建") import timetime.sleep(2)os. ...

  7. 浅谈this指向问题

    链接地址:https://www.jianshu.com/p/34572435b5d0

  8. 突然看到原来除了jar包还有war包啊?????

    先来说说区别: 首先,jar包呢,是一个压缩文件,可以由很多文件压缩而成,,简单来说就是,jar包是别人写好的一些类,然后对这些类 进行打包,这就是jar包,你可以直接用这些  jar包,使用里面的类 ...

  9. 【2019年08月26日】A股最便宜的股票

    查看更多A股最便宜的股票:androidinvest.com/CNValueTop/ 便宜指数 = PE + PB + 股息 + ROE,四因子等权,数值越大代表越低估. 本策略只是根据最新的数据来选 ...

  10. 原生js实现append()方法

    一.在使用jquery时,append() 方法在被选元素的结尾(仍然在内部)插入指定内容 使用方法:$(selector).append(content),content为必需的.规定要插入的内容( ...