之前是論文中看到PCA,有了一個簡單瞭解,還寫了一篇入門筆記:PCA主成分分析
直到前一陣幫做實驗用到PCA,才發現之前的理解實在是too young too naive…
1 基於opencv的幾種PCA實現
故事是這樣的,有一天,我拿到一個5000個樣本的數據集,每個樣本用一個30w維的特徵向量描述,被要求對特徵降維到3w維。簡單複習了下之前的入門筆記,便哼哧哼哧開始coding了。
1.1 使用cvCalcPCA
我查到opencv中已經有函數cvCalcPCA和cvProjectPCA,大喜,於是很快寫出瞭如下的代碼:
cv::Mat data;
LoadData(inputFile, data , nSamples, 0, 0);
int nDim = data.cols;
CvMat tmpD = data;
CvMat* pData;
pData = &tmpD;
CvMat* pMean = cvCreateMat(1,nDim,CV_32FC1);
CvMat* pEigVectors = cvCreateMat(nComponents,nDim,CV_32FC1);
CvMat* pEigValues = cvCreateMat(1,nComponents,CV_32FC1);
cvCalcPCA(pData,pMean,pEigValues,pEigVectors,CV_PCA_DATA_AS_ROW);
cvProjectPCA(pData,pMean,pEigVectors,pResult);
cv::Mat tmpResult(pResult->rows, pResult->cols, CV_32FC1, pResult->data.fl);
cvReleaseMat(&pResult);
WriteData(pcaResultFile, tmpResult);
運行程序,發現,在opencv的matmul.cpp中函數cvCalcPCA的實現中,這一句:
CV_Assert( (evals0.cols == 1 || evals0.rows == 1) &&
ecount0 <= ecount &&
evects0.cols == evects.cols &&
evects0.rows == ecount0 );
第二句報錯,意思是已創建的eignvalue數組和程序計算得到的eignvalue數組大小不一致。而當設nComponents小於5000時,程序不報錯。
1.2 使用cv::PCA
看到在cvCalcPCA中封裝了cv::PCA,於是把cv::PCA單獨拎出來,想擺脫那句Assert,就有了如下的代碼:
cv::PCA pca(data,noArray(),CV_PCA_DATA_AS_ROW,nComponents);
FileStorage outputPca(pcaMatrixFile,FileStorage::WRITE);
pca.write(outputPca);
cv::Mat evects = pca.eigenvectors;
cv::Mat result = pca.project(data);
FileStorage outputResult(pcaResultFile,FileStorage::WRITE);
outputResult << "result" << result;
發現這種寫法舒服太多,首先,直接用Mat,省去了向CvMat*轉換的麻煩,其次,這個pca.write()函數可直接將特徵向量特徵值等信息寫入同個xml文件。
然而不幸的是,只要nComponents超過樣本個數(5000),程序輸出的結果都是5000維的,我們本來期望是nComponents維,也就是3w維。
在pca.cpp中查看其實現,發現,當樣本維度D大於樣本個數N時,使用“scrambled”方法計算pca,設A爲輸入樣本按行存儲,A大小爲N×D:
因爲C只有N個特徵值,所以B也只有N個特徵值,也就是隻有N個components。爲什麼這樣呢?不懂。
1.3 使用eigen
只好捨棄opencv現成的函數,用eigen函數算特徵向量和特徵值,其他運算直接DIY,好在opencv的eigen函數返回的特徵向量是按特徵值降序排好了的,於是又有了如下的代碼:
void MyPCACalcEvects( const Mat &_data, Mat &eigenvalues, Mat &eigenvectors )
{
Mat data = cv::Mat_<float>(_data);
int N = data.rows;
int D = data.cols;
// mean
Mat m = Mat::zeros( 1, D, data.type() );
for ( int j=0; j<D; j++ )
{
for ( int i=0; i<N; i++ )
{
m.at<float>(0,j) += data.at<float>(i,j);
}
m.at<float>(0,j) /= N;
}
WriteData("mu.txt",m);
Mat S = Mat::zeros( N, D, data.type() );
for ( int i=0; i<N; i++ )
{
for ( int j=0; j<D; j++ )
{
S.at<float>(i,j) = data.at<float>(i,j) - m.at<float>(0,j);
}
}
Mat C = S.t() * S /(N-1);
eigen( C, eigenvalues, eigenvectors );
}
但是問題又來了,對於我的task,這裏計算出的特徵向量在內存中放不下,如果要在計算特徵向量的過程中寫外存,就要自己DIY一個寫外存的SVD分解。。並且用小的數據測試有一個key observation:輸出的nComponents個特徵值和特徵向量,發現第N個之後的特徵值都爲0了。這麼說來之前那種”scrambled”的方法好像是有道理的,可這又是爲什麼呢?
1.4 Online PCA
就在一籌莫展的時候,發現了一種online PCA,就是將樣本逐個輸入,降維結果逐個輸出,pca映射矩陣是每次都在更新的,也就是說,每個樣本降維時,用的映射矩陣都不一樣。
2 PCA的病態問題
在讀online PCA算法的論證時,忽然想到以前看到的,PCA降維的過程,實際上就是每一次向着能最大程度保持樣本variance(差異性)的一個主軸方向投影,我們假設有N個D維樣本,根據之前的實驗結果,在第N次投影后,特徵值爲0了,也即投影后樣本方差爲0了,那麼投影后樣本沒了差異性,就不能再投影了。
當然這只是自己的一個初步的想法,去stack exchange翻了一些大牛們比較嚴謹的解釋,才弄明白這個困擾了一個星期的問題到底是怎麼回事。
假設在3維空間中有N=2個點,N個點來自維度爲N-1的manifold,因爲2個點必然在一條直線上,即這2個點得到1維子空間,它們的方差是“spread”在這個1維子空間上的,和樣本的維度並沒有什麼關係。也就是說,N個樣本的方差是spread在N-1維子空間上,即只有N-1個components。
舉個例子來說,有兩個點(1,1,1),(2,2,2),根據PCA的投影步驟:
(1)找到新的座標系統的原點(1.5,1.5,1.5)
(2)兩個點到新座標軸的投影距離儘可能小,此處爲0
(3)第一個component將是從(0,0,0)到(3,3,3)的直線,這個直線是使樣本點投影到它後方差最大的一條線
(4)第二個component方向必須和第一個component正交,這條線可能是從(0,0,3)到(3,3,0),或者(0,3,0)到(3,0,3),但無論向誰投影,這兩個點投影后它們的方差爲0.
當樣本數小於樣本維度時,出現的這種現象就是“the curse of dimensionality”
另外,有大牛推薦了PLSR(Partial Least Squares Regression),也就是偏最小二乘迴歸做成分分析,號稱結合了PCA和CCA的優點,且components不用正交,細節還不清楚,先開個坑以後用到再細看吧。
Reference
[1] A tutorial on Principal Components Analysis
[2] Making sense of principal component analysis, eigenvectors & eigenvalues
[3] PCA when the dimensionality is greater than the number of samples
[4] Why are there only n−1 principal components for n data points if the number of dimensions is larger than n?
[5] Partial Least Squares Regression