PCA(Principal Component Analysis,主成分分析)是一种很常用的根据变量协方差对数据进行降维、压缩的方法。它的精髓在于尽量用最少数量的维度,尽可能精确地描述数据。
PCA对数据进行降维的过程可以用下面这个动图来解释(图片摘自http://stats.stackexchange.com/a/140579/93946):
在上图中,一组位于直角坐标系的二维样本集,沿着斜线的方向有很强的相关性。所以如果我们将直角坐标系转换到斜向,也就是让横轴沿斜线方向,纵轴垂直于斜线方向。于是,在这个新的坐标系下,数据点在横轴上分布很分散,但是在纵轴方向比较集中。如果在误差允许范围内,我们完全可以将数据点在新纵轴上的坐标全部置为0,只用新横轴上的坐标来表示点的位置。这样,就完成了对数据降维的过程(即将原始直角坐标系的2个维度减小到新坐标系的1个维度)。对更高维的情况,处理过程与之类似。
PCA人脸识别
将PCA用于人脸识别的过程如下:
1. 假设有400幅尺寸为100*100的图像,构成10000*400的矩阵 ;
2. 计算均值 ,令 ;
3. 根据定义,计算协方差矩阵 ;
4. 计算 的特征值与特征向量,取前h个最大特征值所对应的特征向量,构成矩阵 ;
5. 矩阵 可对数据降维: ,Y是h行400列的矩阵,也就是将数据从10000维降为h维。
这种做法一个明显的缺陷在于, 的维度为10000×10000,直接进行奇异值分解计算量非常大。利用QR分解,作间接的奇异值分解,可以减小计算量。
利用QR分解减小计算量
基于QR分解的PCA算法步骤如下:
1. 已知 ,其中 为d*d,H为d*n,d代表原始数据的维数,n代表样本数,d远大于n;
2. 对H作QR分解, ,其中Q为d*t,R为t*n, ;
3. 则 ,对 作奇异值分解 ,其中U为n*t,V为t*t, ;
4. 于是 ,其中 ;
5. 由于 ,所以QV可将 对角化,QV为 的特征向量矩阵, 为 的特征值矩阵;
6. 选取D前h个最大对角元所对应于V中的h个列,构成t*h的矩阵 ,则降维矩阵 ;
该过程涉及对一个很大的矩阵的QR分解,和对一个较小矩阵的奇异值分解。计算量与传统方法相比较的结果如下(图片摘自[1]):
进一步,进行人脸识别的过程如下:
1. 假设有c个类别,每类包含s个样本,则n=c∗s;
2. 对X计算 ,将Y(也称特征脸)按类别计算均值,得到c个长度为h的列向量 ;
3. 对于未知类别的新样本x,计算 ,y的长度为h;
4. 计算距离 ,取距离最小的i作为x的类标号。
距离度量d
1. 欧式距离
2. 曼哈顿距离
3. 马氏距离
在马氏距离中,x与y分布相同,且协方差矩阵为S。加入协方差矩阵的逆矩阵的作用是,将如下图(图片部分取自http://stats.stackexchange.com/a/62147/93946)中呈椭圆分布的数据归一化到圆形分布中,再来比较距离,可以抵消不同样本集在特征空间中的分布差异。
C++实现
环境要求:OpenCV(样本图像的读取),Armadillo(高性能线性代数C++函数库),Intel MKL(替代LAPACK为Armadillo提供矩阵分解计算)。
项目使用Visual Studio Ultimate 2012建立,不过核心代码只有一个cpp文件。
全部代码托管在 github.com/johnhany/QR-PCA-FaceRec 。
/************************************************************************/ /* QR-PCA-FaceRec by John Hany */ /* */ /* A face recognition algorithm using QR based PCA. */ /* */ /* Released under MIT license. */ /* */ /* Contact me at johnhany@163.com */ /* */ /* Welcome to my blog http://johnhany.net/, if you can read Chinese:) */ /* */ /************************************************************************/ #include <opencv2/opencv.hpp> #include <armadillo> #include <iostream> usingnamespace std; int component_num = 7; string orl_path = "G://Datasets//orl_faces"; enum distance_type {ECULIDEAN = 0, MANHATTAN, MAHALANOBIS}; //double distance_criterion[3] = { 10.0, 30.0, 3.0}; double distance_criterion[3] = { 1000.0, 1000.0, 1000.0}; bool compDistance(pair<int, double> a, pair<int, double> b); double calcuDistance(const arma::vecvec1, const arma::vecvec2, distance_typedis_type); double calcuDistance(const arma::vecvec1, const arma::vecvec2, const arma::matcov2, distance_typedis_type); int main(int argc, const char *argv[]) { int class_num = 40; int sample_num = 10; int img_cols = 92; int img_rows = 112; cv::Sizesample_size(img_cols, img_rows); arma::matmat_sample(img_rows*img_cols, sample_num*class_num); //Load samples in one matrix `mat_sample`. for(int class_idx = 0; class_idx < class_num; class_idx++) { for(int sample_idx = 0; sample_idx < sample_num; sample_idx++) { string filename = orl_path + "//s" + to_string(class_idx+1) + "//" + to_string(sample_idx+1) + ".pgm"; cv::Matimg_frame = cv::imread(filename, CV_LOAD_IMAGE_GRAYSCALE); cv::Matimg_sample; cv::resize(img_frame, img_sample, sample_size); for(int i = 0; i < img_rows; i++) { uchar* pframe = img_sample.ptr<uchar>(i); for(int j = 0; j < img_cols; j++) { mat_sample(i*img_cols+j, class_idx*sample_num+sample_idx) = (double)pframe[j]/255.0; } } } } // cout << mat_sample.n_rows << endl << mat_sample.n_cols << endl << mat_sample(img_rows*img_cols/2, 0) << endl; //Calculate PCA transform matrix `mat_pca`. arma::mat H = mat_sample; arma::matmean_x = arma::mean(mat_sample, 1); for(int j = 0; j < class_num * sample_num; j++) { H.col(j) -= mean_x.col(0); } H /= 1.0/sqrt(sample_num-1); arma::mat Q, R; arma::qr_econ(Q, R, H); arma::mat U, V; arma::vec d; arma::svd_econ(U, d, V, R.t()); // cout << "d" << endl << d << endl; // arma::rowvec vec_eigen = d.head(component_num).t(); // cout << "vec_eigen" << endl << vec_eigen << endl; arma::matV_h(V.n_rows, component_num); if(component_num == 1) { V_h = V.col(0); }else { V_h = V.cols(0, component_num-1); } arma::matmat_pca = Q * V_h; //Calculate eigenfaces `mat_eigen_vec`. arma::matmat_eigen = mat_pca.t() * mat_sample; // cout << "mat_eigen" << endl << mat_eigen << endl; arma::matmat_eigen_vec(component_num, class_num, arma::fill::zeros); vector<arma::mat> mat_cov_list; for(int class_idx = 0; class_idx < class_num; class_idx++) { arma::veceigen_sum(component_num, arma::fill::zeros); for(int sample_idx = 0; sample_idx < sample_num; sample_idx++) { eigen_sum += mat_eigen.col(class_idx*sample_num+sample_idx); } eigen_sum /= (double)sample_num; mat_eigen_vec.col(class_idx) = eigen_sum; mat_cov_list.push_back(arma::cov((mat_eigen.cols(class_idx*sample_num, class_idx*sample_num+sample_num-1)).t())); // cout << mat_cov_list[class_idx] << endl; } // cout << "mat_eigen_vec" << endl << mat_eigen_vec << endl; /* cout << "dis within class" << endl; for(int class_idx = 0; class_idx < class_num; class_idx++) { for(int sample_idx = 0; sample_idx < sample_num; sample_idx++) { double dis = calcuDistance(mat_eigen.col(class_idx*sample_num+sample_idx), mat_eigen_vec.col(class_idx), mat_cov_list[class_idx], distance_type::MAHALANOBIS); cout << dis << " "; } cout << endl; } cout << "dis between classes" << endl; for(int class_idx = 0; class_idx < class_num; class_idx++) { for(int sample_idx = 0; sample_idx < class_num; sample_idx++) { double dis = calcuDistance(mat_eigen.col(sample_idx*sample_num), mat_eigen_vec.col(class_idx), mat_cov_list[class_idx], distance_type::MAHALANOBIS); cout << dis << " "; } cout << endl; } */ //Classify new sample. int correct_count = 0; double max_dis = 0.0; for(int class_idx = 0; class_idx < class_num; class_idx++){ for(int sample_idx = 0; sample_idx < sample_num; sample_idx++) { arma::matmat_new_sample(img_rows*img_cols, 1); string filename = orl_path + "//s" + to_string(class_idx+1) + "//" + to_string(sample_idx+1) + ".pgm"; cv::Matimg_new_frame = cv::imread(filename, CV_LOAD_IMAGE_GRAYSCALE); cv::Matimg_new_sample; cv::resize(img_new_frame, img_new_sample, sample_size); for(int i = 0; i < img_rows; i++) { uchar* pframe = img_new_sample.ptr<uchar>(i); for(int j = 0; j < img_cols; j++) { mat_new_sample(i*img_cols+j, 0) = (double)pframe[j]/255.0; } } arma::matmat_new_eigen = mat_pca.t() * mat_new_sample; vector<pair<int, double>> dis_list; for(int new_class_idx = 0; new_class_idx < class_num; new_class_idx++) { double dis = calcuDistance(mat_new_eigen.col(0), mat_eigen_vec.col(new_class_idx), mat_cov_list[new_class_idx], distance_type::MAHALANOBIS); dis_list.push_back(make_pair(new_class_idx, dis)); } sort(dis_list.begin(), dis_list.end(), compDistance); if(dis_list[0].first == class_idx && dis_list[0].second <= distance_criterion[distance_type::MAHALANOBIS]) { correct_count++; } if(dis_list.back().second > max_dis) { max_dis = dis_list.back().second; } } } cout << "Maximum distance: " << max_dis << endl; double correct_ratio = (double)correct_count / (class_num * sample_num); cout << "Correctness ratio: " << correct_ratio * 100.0 << "%" << endl; cin.get(); return 0; } bool compDistance(pair<int, double> a, pair<int, double> b) { return (a.second < b.second); } double calcuDistance(const arma::vecvec1, const arma::vecvec2, distance_typedis_type) { if(dis_type == ECULIDEAN) { return arma::norm(vec1-vec2, 2); }else if(dis_type == MANHATTAN) { return arma::norm(vec1-vec2, 1); }else if(dis_type == MAHALANOBIS) { arma::mattmp = (vec1-vec2).t() * (vec1 - vec2); return sqrt(tmp(0,0)); } return -1.0; } double calcuDistance(const arma::vecvec1, const arma::vecvec2, const arma::matcov2, distance_typedis_type) { if(dis_type == ECULIDEAN) { return arma::norm(vec1-vec2, 2); }else if(dis_type == MANHATTAN) { return arma::norm(vec1-vec2, 1); }else if(dis_type == MAHALANOBIS) { arma::mattmp = (vec1-vec2).t() * cov2.i() * (vec1 - vec2); return sqrt(tmp(0,0)); } return -1.0; }
分类测试
测试样本采用The AT&T Database of Faces,原称The ORL Database of Faces,包含取自40个人的样本,每人10幅,共400幅图像,图像尺寸92*112。
样本库的链接为 http://www.cl.cam.ac.uk/research/dtg/attarchive/facedatabase.html 。
1. 欧式距离降维及分类效果:
即使将h设为最大的400(样本数),其分类正确率也只能达到99%。
2. 曼哈顿距离降维及分类效果:
在h为128时,分类正确率可以达到100%,降维能力略好于欧式距离。
3. 马氏距离降维及分类效果:
在h仅仅为7的时候,其分类正确率就已经达到100%了。采用马氏距离的PCA方法可以将人脸数据的维度从10000左右降到7,降维效果非常优秀。在h超过9时,分类过程中计算的最大马氏距离超出了双精度浮点数double的上限。
参考文献
[1] Sharma A, Paliwal K K, Imoto S, et al. Principal component analysis using QR decomposition[J]. International Journal of Machine Learning and Cybernetics, 2013, 4(6): 679-683.
[2] Raj D. A Realtime Face Recognition system using PCA and various Distance Classifiers[J]. 2011.
[3] Turk M, Pentland A. Eigenfaces for recognition[J]. Journal of Cognitive Neuroscience, 1991, 3(1): 71-86.