如何對 PCA 進行交叉驗證以確定主成分的數量?
我正在嘗試編寫我自己的主成分分析函數,PCA(當然已經寫了很多,但我只是對自己實現一些東西感興趣)。我遇到的主要問題是交叉驗證步驟和計算預測平方和(PRESS)。我使用哪種交叉驗證並不重要,這主要是關於背後理論的問題,但請考慮留一法交叉驗證(LOOCV)。從理論上我發現,為了執行 LOOCV,您需要:
- 刪除一個對象
- 縮放其餘部分
- 使用一些組件執行 PCA
- 根據(2)中獲得的參數縮放刪除的對象
- 根據 PCA 模型預測對象
- 計算此對象的 PRESS
- 對其他對象重新執行相同的算法
- 總結所有 PRESS 值
- 利潤
因為我是這個領域的新手,為了確保我是對的,我將結果與我擁有的一些軟件的輸出進行了比較(也為了編寫一些代碼,我按照軟件中的說明進行操作)。我得到完全相同的結果計算殘差平方和,但計算 PRESS 是個問題。
您能否告訴我我在交叉驗證步驟中實施的內容是否正確:
case 'loocv' % # n - number of objects % # p - number of variables % # vComponents - the number of components used in CV dataSets = divideData(n,n); % # it is just a variable responsible for creating datasets for CV % # (for LOOCV datasets will be equal to [1, 2, 3, ... , n]);' tempPRESS = zeros(n,vComponents); for j = 1:n Xmodel1 = X; % # X - n x p original matrix Xmodel1(dataSets{j},:) = []; % # delete the object to be predicted [Xmodel1,Xmodel1shift,Xmodel1div] = skScale(Xmodel1, 'Center', vCenter, 'Scaling', vScaling); % # scale the data and extract the shift and scaling factor Xmodel2 = X(dataSets{j},:); % # the object to be predicted Xmodel2 = bsxfun(@minus,Xmodel2,Xmodel1shift); % # shift and scale the object Xmodel2 = bsxfun(@rdivide,Xmodel2,Xmodel1div); [Xscores2,Xloadings2] = myNipals(Xmodel1,0.00000001,vComponents); % # the way to calculate the scores and loadings % # Xscores2 - n x vComponents matrix % # Xloadings2 - vComponents x p matrix for i = 1:vComponents tempPRESS(j,i) = sum(sum((Xmodel2* ... (eye(p) - transpose(Xloadings2(1:i,:))*Xloadings2(1:i,:))).^2)); end end PRESS = sum(tempPRESS,1);
在軟件(PLS_Toolbox)中,它的工作方式如下:
for i = 1:vComponents tempPCA = eye(p) - transpose(Xloadings2(1:i,:))*Xloadings2(1:i,:); for kk = 1:p tempRepmat(:,kk) = -(1/tempPCA(kk,kk))*tempPCA(:,kk); % # this I do not understand tempRepmat(kk,kk) = -1; % # here is some normalization that I do not get end tempPRESS(j,i) = sum(sum((Xmodel2*tempRepmat).^2)); end
因此,他們使用此
tempRepmat
變量進行了一些額外的標準化:我發現的唯一原因是他們將 LOOCV 應用於穩健的 PCA。不幸的是,支持團隊不想回答我的問題,因為我只有他們軟件的演示版本。
您所做的是錯誤的:像這樣為 PCA 計算 PRESS 是沒有意義的!具體來說,問題出在您的第 5 步。
PRESS for PCA 的樸素方法
讓數據集由點在維空間:. 計算單個測試數據點的重建誤差,你在訓練集上執行 PCA排除這一點,取一定數量主軸作為列,並找到重構誤差為. 然後 PRESS 等於所有測試樣本的總和,所以合理的等式似乎是:
為簡單起見,我在這裡忽略了居中和縮放的問題。
天真的方法是錯誤的
上面的問題是我們使用計算預測,這是一件非常糟糕的事情。
請注意與回歸案例的關鍵區別,其中重建誤差的公式基本相同, 但預測 使用預測變量而不是使用. 這在 PCA 中是不可能的,因為在 PCA 中沒有因變量和自變量:所有變量都被一起處理。
在實踐中,這意味著上面計算的 PRESS 會隨著組件數量的增加而減少並且永遠不會達到最低限度。這會導致人們認為所有組件很重要。或者在某些情況下它確實達到了最小值,但仍然傾向於過度擬合和高估最佳維度。
正確的做法
有幾種可能的方法,參見Bro 等人。(2008) 組件模型的交叉驗證:對當前方法的批判性研究,以進行概述和比較。一種方法是一次省略一個數據點的一維(即代替),使訓練數據成為一個有一個缺失值的矩陣,然後用 PCA 預測(“估算”)這個缺失值。(當然可以隨機保留一些較大比例的矩陣元素,例如 10%)。問題是用缺失值計算 PCA 的計算速度可能非常慢(它依賴於 EM 算法),但這裡需要多次迭代。更新:請參閱http://alexhwilliams.info/itsneuronalblog/2018/02/26/crossval/以獲得很好的討論和 Python 實現(具有缺失值的 PCA 是通過交替最小二乘法實現的)。
我發現一種更實用的方法是省略一個數據點一次,在訓練數據上計算 PCA(完全如上),然後循環遍歷,一次只留下一個,然後使用其餘部分計算重建誤差。這在開始時可能會很混亂,並且公式往往會變得很混亂,但實現起來相當簡單。先給出(有點嚇人的)公式,然後簡單解釋一下:
考慮這裡的內循環。我們遺漏了一點併計算訓練數據的主要成分,. 現在我們保留每個值作為測試並使用剩餘的尺寸來執行預測。預測是個-“投影”的坐標(在最小二乘意義上)到跨越的子空間. 要計算它,找到一個點在 PC 空間最接近通過計算在哪裡是和-第 行被踢出,並且代表偽逆。現在地圖回到原來的空間:並採取它-th 坐標.
正確方法的近似值
我不太了解 PLS_Toolbox 中使用的額外規範化,但這是一種朝著相同方向發展的方法。
還有另一種映射方式在主成分空間上:,即簡單地採用轉置而不是偽逆。換句話說,測試時遺漏的維度根本不計算在內,相應的權重也被簡單地踢掉了。我認為這應該不太準確,但通常可以接受。好消息是生成的公式現在可以按如下方式向量化(我省略了計算):
我寫的地方作為為了緊湊,和意味著將所有非對角元素設置為零。請注意,這個公式看起來與第一個公式(樸素的 PRESS)完全一樣,只是稍加修正!另請注意,此校正僅取決於對角線,就像在 PLS_Toolbox 代碼中一樣。但是,該公式與 PLS_Toolbox 中似乎實現的公式仍然不同,我無法解釋這種差異。
**更新(2018 年 2 月):**上面我稱一個程序“正確”和另一個“近似”,但我不太確定這是否有意義。這兩個程序都有意義,我認為兩者都不正確。我真的很喜歡“近似”程序有一個更簡單的公式。另外,我記得我有一些數據集,其中“近似”過程產生的結果看起來更有意義。不幸的是,我已經不記得細節了。
例子
以下是這些方法對兩個著名數據集的比較:Iris 數據集和 wine 數據集。請注意,樸素方法產生單調遞減曲線,而其他兩種方法產生具有最小值的曲線。進一步注意,在 Iris 案例中,近似方法建議 1 個 PC 作為最佳數量,但偽逆方法建議 2 個 PC。(查看 Iris 數據集的任何 PCA 散點圖,似乎兩個第一台 PC 都帶有一些信號。)在葡萄酒的情況下,偽逆方法清楚地指向 3 台 PC,而近似方法無法確定 3 和 5 之間。
用於執行交叉驗證並繪製結果的 Matlab 代碼
function pca_loocv(X) %// loop over data points for n=1:size(X,1) Xtrain = X([1:n-1 n+1:end],:); mu = mean(Xtrain); Xtrain = bsxfun(@minus, Xtrain, mu); [~,~,V] = svd(Xtrain, 'econ'); Xtest = X(n,:); Xtest = bsxfun(@minus, Xtest, mu); %// loop over the number of PCs for j=1:min(size(V,2),25) P = V(:,1:j)*V(:,1:j)'; %//' err1 = Xtest * (eye(size(P)) - P); err2 = Xtest * (eye(size(P)) - P + diag(diag(P))); for k=1:size(Xtest,2) proj = Xtest(:,[1:k-1 k+1:end])*pinv(V([1:k-1 k+1:end],1:j))'*V(:,1:j)'; err3(k) = Xtest(k) - proj(k); end error1(n,j) = sum(err1(:).^2); error2(n,j) = sum(err2(:).^2); error3(n,j) = sum(err3(:).^2); end end error1 = sum(error1); error2 = sum(error2); error3 = sum(error3); %// plotting code figure hold on plot(error1, 'k.--') plot(error2, 'r.-') plot(error3, 'b.-') legend({'Naive method', 'Approximate method', 'Pseudoinverse method'}, ... 'Location', 'NorthWest') legend boxoff set(gca, 'XTick', 1:length(error1)) set(gca, 'YTick', []) xlabel('Number of PCs') ylabel('Cross-validation error')