戰(zhàn):att_faces數(shù)據(jù)集PCA與LDA完整實(shí)現(xiàn))
簡介這是一套面向人臉識別與PCA降維學(xué)習(xí)的MATLAB項(xiàng)目包由31att.com提供內(nèi)含完整的att_faces人臉圖像數(shù)據(jù)集及配套算法腳本。數(shù)據(jù)集涵蓋約40位不同對象在多種角度、表情與光照條件下的面部樣本其中400個bmp與400個pgm格式圖像分別對應(yīng)原始圖像與灰度版本另有txt說明文件與m腳本各1個整體802個文件、6.24MB。項(xiàng)目核心價值在于演示PCA主成分分析的完整流程對圖像進(jìn)行灰度化與中心化處理計算協(xié)方差矩陣并提取前k個特征向量構(gòu)建特征臉空間最終實(shí)現(xiàn)降維與分類識別。對于正在學(xué)習(xí)模式識別、機(jī)器學(xué)習(xí)入門或MATLAB圖像處理的中級學(xué)習(xí)者而言這是可直接運(yùn)行、便于對照理論理解算法細(xì)節(jié)的實(shí)操素材。目前已有598人學(xué)習(xí)下載。1. 它是什么一份被無數(shù)MATLAB人臉識別作業(yè)翻來覆去用的經(jīng)典數(shù)據(jù)集做MATLAB圖像處理相關(guān)的工作繞不開一個叫att_faces的東西。這個數(shù)據(jù)集全稱是 ATT Database of Faces早期也叫 ORL 人臉庫標(biāo)題里那串31att是早期鏡像站點(diǎn)的路徑殘留本質(zhì)上是同一份數(shù)據(jù)40 個人每人 10 張 112×92 的 8 位灰度人臉照片。拍攝條件有光照、表情、是否戴眼鏡、拍攝時間間隔等變化但這個體量對做算法驗(yàn)證來說剛好——人不多、樣本不多、數(shù)據(jù)不大MATLAB 里 load 進(jìn)來直接就能跑。很多搜這個標(biāo)題的人是拿著它做課程設(shè)計或者論文實(shí)驗(yàn)比如人臉識別、PCA 降維、LDA 分類、深度學(xué)習(xí)前的基線對比。它之所以被用得這么頻繁是因?yàn)榻Y(jié)構(gòu)規(guī)整40 個子文件夾每類 10 張?zhí)烊贿m合做監(jiān)督分類實(shí)驗(yàn)。你不需要自己做數(shù)據(jù)清洗也不需要處理標(biāo)注文件文件名編號就是標(biāo)簽。這篇筆記就按我實(shí)際做實(shí)驗(yàn)的順序從數(shù)據(jù)導(dǎo)入到 PCA、LDA 兩個經(jīng)典方法跑通再把最容易翻車的地方挨個講一遍。2. 把 att_faces 變成 MATLAB 能直接用的矩陣從目錄到 .mat 的完整轉(zhuǎn)換2.1 先搞清楚數(shù)據(jù)集的目錄結(jié)構(gòu)別急著寫代碼這個數(shù)據(jù)集最常見的發(fā)布形態(tài)是一個壓縮包解壓后是orl_faces或者att_faces目錄里面按s1、s2一直到s40分子文件夾。每個文件夾里是1.pgm到10.pgm的 PGM 格式灰度圖。PGM 是 NetPBM 家族的簡易格式MATLAB 的imread直接支持不需要額外裝插件。我見過太多人在第一步就栽跟頭下載的壓縮包解壓后套了兩層目錄或者某些第三方鏡像把文件名改成了*.png、*.jpg。先花 30 秒確認(rèn)路徑和文件后綴再動手寫腳本比后面排錯省時間。常見的目錄結(jié)構(gòu)長這樣att_faces/ ├── s1/ │ ├── 1.pgm │ ├── 2.pgm │ └── ... ├── s2/ │ ├── 1.pgm │ ├── 2.pgm │ └── ... └── s40/ ├── 1.pgm └── ...如果解壓出來不是這個結(jié)構(gòu)比如文件名變成了1.png或者多了一層ORL/att_faces/改一下下面的遍歷路徑就行。核心是把所有圖片讀進(jìn)來按「人」和「該人的第幾張」兩個維度組織數(shù)據(jù)。這個數(shù)據(jù)結(jié)構(gòu)直接決定了后面的標(biāo)簽怎么生成。2.2 寫一個可復(fù)用的人臉數(shù)據(jù)導(dǎo)入腳本下面這段腳本我一直在用讀入后直接生成兩個變量data是樣本矩陣每一行是一張拉直后的圖片向量labels是對應(yīng)的類別標(biāo)簽從 1 到 40。這樣設(shè)計是為了后面接 PCA、LDA 甚至簡單的神經(jīng)網(wǎng)絡(luò)都不用再改數(shù)據(jù)格式。% 人臉數(shù)據(jù)集導(dǎo)入腳本 % 假設(shè)數(shù)據(jù)集根目錄為 ./att_faces子文件夾 s1~s40每個文件夾 10 張 pgm rootDir ./att_faces; numClasses 40; % 類別數(shù) numSamplesPerClass 10; % 每類樣本數(shù) imgH 112; % 圖像高度 imgW 92; % 圖像寬度 imgDim imgH * imgW; % 拉直后向量長度 % 預(yù)分配矩陣10304 112*92 data zeros(numClasses * numSamplesPerClass, imgDim); labels zeros(numClasses * numSamplesPerClass, 1); sampleIdx 1; for i 1:numClasses for j 1:numSamplesPerClass filePath fullfile(rootDir, sprintf(s%d, i), sprintf(%d.pgm, j)); img imread(filePath); % 讀入 PGM輸出是 uint8 的二維矩陣 img im2double(img); % 轉(zhuǎn)成 [0,1] 區(qū)間的 double避免后續(xù)計算溢出 data(sampleIdx, :) img(:); % 按行存儲每行是一張拉直的圖 labels(sampleIdx) i; sampleIdx sampleIdx 1; end end % 隨機(jī)打亂數(shù)據(jù)避免按順序劃分導(dǎo)致的偏差 rng(42); % 固定隨機(jī)種子保證實(shí)驗(yàn)可復(fù)現(xiàn) permIdx randperm(size(data, 1)); data data(permIdx, :); labels labels(permIdx); % 保存成 .mat 文件后續(xù)所有實(shí)驗(yàn)直接從 mat 讀 save(att_faces_processed.mat, data, labels, imgH, imgW);這段代碼有幾點(diǎn)值得說明。im2double這一步非常關(guān)鍵imread讀 PGM 默認(rèn)返回uint8類型如果你直接對uint8矩陣做減法或者特征分解MATLAB 會做類型截斷算出來的方差和特征值完全是錯的。用im2double把像素歸一化到[0,1]既保精度又避免數(shù)值溢出。另一個細(xì)節(jié)是打亂順序。如果不打亂你切訓(xùn)練集和測試集時很可能會把前幾個人的全部樣本切進(jìn)訓(xùn)練集后幾個人的切進(jìn)測試集導(dǎo)致測試集類別覆蓋不全。固定rng(42)保證每次跑出來的劃分一致這在調(diào)參階段非常重要——否則你沒法判斷是算法改進(jìn)了還是數(shù)據(jù)劃分變了。2.3 數(shù)據(jù)劃分按人切還是按樣本切要看你驗(yàn)證什么導(dǎo)入數(shù)據(jù)后第一件事就是切訓(xùn)練集和測試集。人臉識別實(shí)驗(yàn)里有兩種切法按樣本切每類隨機(jī)抽幾張做訓(xùn)練其余做測試和按人切前 30 人訓(xùn)練后 10 人測試。按樣本切驗(yàn)證的是「見過這個人但沒見過這張照片」的識別能力按人切驗(yàn)證的是「完全沒見過這個人能否判斷他是誰」——這是一個更難也更接近真實(shí)場景的任務(wù)。大多數(shù)課程實(shí)驗(yàn)用的是按樣本切因?yàn)檫@樣可以保證測試集和訓(xùn)練集的類別完全一致準(zhǔn)確率數(shù)值會比較好看。做法是每個類別內(nèi)部做隨機(jī)劃分而不是整體隨機(jī)。整體隨機(jī)會導(dǎo)致某一個類在訓(xùn)練集中只有 5 張、測試集中卻有 5 張,比例不均。% 按類別劃分訓(xùn)練集和測試集每類取前 7 張做訓(xùn)練后 3 張做測試 % 前提data 已經(jīng)按類別排好序或者記錄了下標(biāo) trainData []; trainLabels []; testData []; testLabels []; for i 1:numClasses classIdx find(labels i); % 找到當(dāng)前類的所有樣本下標(biāo) % 這里要確保 classIdx 內(nèi)部是亂序的否則取前7張有排序偏差 classIdx classIdx(randperm(length(classIdx))); trainData [trainData; data(classIdx(1:7), :)]; trainLabels [trainLabels; repmat(i, 7, 1)]; testData [testData; data(classIdx(8:end), :)]; testLabels [testLabels; repmat(i, 3, 1)]; end注意repmat(i, 7, 1)這個寫法在循環(huán)里逐類拼接trainLabels時它保證每一行的標(biāo)簽和對應(yīng)圖片是對齊的。很多人喜歡直接trainLabels [trainLabels; i]這在現(xiàn)代 MATLAB 里也能跑但隱式擴(kuò)展在老版本R2016a 之前會報錯。用repmat是對兼容性更負(fù)責(zé)的寫法。這個腳本跑完后trainData是 280×10304testData是 120×10304。這個比例7 : 3是我常用的ATT 一共 10 張每類7 張訓(xùn)練 3 張測試基本能反映模型真實(shí)水平。如果訓(xùn)練樣本太少比如每類只給 3 張PCA 的效果會明顯下滑這個后面避坑章節(jié)會展開。3. 用 PCA 跑通第一個基線特征臉方法與核心參數(shù)設(shè)置3.1 為什么人臉識別首選 PCA 而不是直接像素比對像素比對的做法是把測試圖和訓(xùn)練圖逐像素算歐氏距離找最近的。這個思路在嚴(yán)格受控的環(huán)境下能用但只要光照變一點(diǎn)、表情變一點(diǎn)像素級的差值就會劇烈跳動。PCA 的核心思路是去相關(guān)人臉圖像相鄰像素之間相關(guān)性極強(qiáng)真正起區(qū)分作用的可能是少數(shù)幾個模式比如臉型輪廓、眼睛位置的相對布局、面頰的明暗分布。特征臉法Eigenface本質(zhì)上就是把 280×10304 的訓(xùn)練矩陣做協(xié)方差分析找到方差最大的若干個投影方向把每張 10304 維的圖降到幾十維再在這個低維空間里做最近鄰分類。這里的降維不是偷懶而是去噪——原始像素空間的噪聲和無關(guān)變化被丟棄了留下的才是區(qū)分身份的主成分。這里要特別注意一個問題直接對 10304×10304 的協(xié)方差矩陣做特征分解在普通筆記本上是能跑但很慢的大約需要 8GB 內(nèi)存存 doubles。所以標(biāo)準(zhǔn)做法是用一個小技巧樣本數(shù)遠(yuǎn)小于維度數(shù)時對 280×280 的矩陣做特征分解再映射回原空間。這就是 Turk 和 Pentland 論文里那個經(jīng)典步驟數(shù)學(xué)推導(dǎo)不展開直接給可用代碼。3.2 Eigenface 的完整 MATLAB 實(shí)現(xiàn)訓(xùn)練與識別一段代碼搞定% PCA 特征臉訓(xùn)練 識別 % 輸入trainData(280x10304), testData(120x10304) % 這個腳本可以直接跑通 % 1. 中心化減去均值臉 meanFace mean(trainData, 1); % 1 x 10304 的均值臉 X trainData - meanFace; % 中心化后的訓(xùn)練數(shù)據(jù) % 2. 用小矩陣技巧計算特征向量 % 協(xié)方差矩陣 C X*X但直接算會得到 10304x10304 的巨型矩陣 % 改用 X*X維度是 280x280特征值相同特征向量可映射回來 L X * X; % 280 x 280 [V, D] eig(L); % V 的列是特征向量 [eigVals, sortIdx] sort(diag(D), descend); % 特征值從大到小排序 V V(:, sortIdx); % 排序后特征向量 % 3. 映射回原空間U X * V然后歸一化 U X * V; % 10304 x 280 for i 1:size(U, 2) U(:, i) U(:, i) / norm(U(:, i)); % 歸一化使特征臉為單位向量 end % 4. 投影訓(xùn)練集到特征臉空間 k 50; % 保留前 50 個主成分這是個需要調(diào)的參數(shù) trainProjected trainData * U(:, 1:k); % 280 x 50 % 5. 投影測試集 testProjected testData * U(:, 1:k); % 120 x 50 % 6. 最近鄰分類歐氏距離 predLabels zeros(size(testData, 1), 1); for i 1:size(testProjected, 1) % 計算測試樣本到所有訓(xùn)練樣本的歐氏距離 diff trainProjected - testProjected(i, :); dists sum(diff.^2, 2); % 按行求和得到距離向量 [~, minIdx] min(dists); predLabels(i) trainLabels(minIdx); end % 7. 計算準(zhǔn)確率 accuracy sum(predLabels testLabels) / length(testLabels) * 100; fprintf(PCA Accuracy (k%d): %.2f%%\n, k, accuracy);這段代碼里有三個參數(shù)值得單獨(dú)說。第一個是k保留的主成分?jǐn)?shù)量。k取小了欠擬合——丟掉了太多區(qū)分性信息取大了過擬合——把噪聲當(dāng)信號學(xué)進(jìn)去了。在這個數(shù)據(jù)集上k取 50 到 80 之間通常能拿到 95% 以上的準(zhǔn)確率但具體最優(yōu)值要跑一個循環(huán)去試。第二個是特征值分解方式代碼里用了eig而不是svd。對小矩陣X*X來說eig夠用且速度快但如果你對數(shù)值穩(wěn)定性有疑慮用svd(X, econ)直接得到主成分方向更穩(wěn)健。第三個是距離度量歐氏距離是最常用的但換成余弦距離在某些光照變化明顯的場景下表現(xiàn)更好。這段代碼跑完你得到的U的前幾列就是「特征臉」可以用reshape(U(:,1), 112, 92)再imshow出來看第一張通常是整體亮度模式第二、第三張開始出現(xiàn)明顯的臉部結(jié)構(gòu)特征。這個可視化驗(yàn)證很重要——如果你畫出來的特征臉是純噪聲雪花點(diǎn)說明中心化或者歸一化出了問題后面識別結(jié)果再高也不可信。3.3 調(diào) k 值一分鐘畫一條準(zhǔn)確率曲線上面代碼里k50是拍腦袋定的。工程上更嚴(yán)謹(jǐn)?shù)淖龇ㄊ菕咭槐椤? k 值掃描觀察不同主成分?jǐn)?shù)量下的分類準(zhǔn)確率 kRange [5, 10, 20, 30, 40, 50, 60, 80, 100, 150, 200, 250]; accuracies zeros(length(kRange), 1); for idx 1:length(kRange) k kRange(idx); trainProjected trainData * U(:, 1:k); testProjected testData * U(:, 1:k); % 最近鄰分類同上略去重復(fù)代碼 % ... accuracies(idx) accuracy; end % 畫曲線 figure; plot(kRange, accuracies, o-, LineWidth, 1.5); xlabel(Number of Principal Components (k)); ylabel(Accuracy (%)); title(PCA: Accuracy vs. k); grid on;從經(jīng)驗(yàn)值來說k10時準(zhǔn)確率可能只在 80% 左右k30到k50之間會有一個快速上升區(qū)間k超過 100 之后基本進(jìn)入平臺期甚至可能略微下降——因?yàn)楹竺婺切┲鞒煞謱?yīng)的特征值很小基本都是噪聲。這條曲線的形狀本身就是一份很好的實(shí)驗(yàn)報告材料比你直接在論文里寫「選取前 50 個主成分」要可信得多。4. 用 LDA 做有監(jiān)督降維Fisherface 和 PCA 的對比實(shí)驗(yàn)4.1 PCA 和 LDA 的本質(zhì)區(qū)別無監(jiān)督 vs 有監(jiān)督PCA 不知道任何標(biāo)簽信息它只看方差。方差大的方向不一定是區(qū)分身份的方向——比如光照變化導(dǎo)致的整體灰度偏移方差可能很大但對身份識別沒有幫助。LDA線性判別分析則利用標(biāo)簽信息尋找讓類間散度最大、類內(nèi)散度最小的投影方向。在這個數(shù)據(jù)集上LDA 的 Fisherface 方法通常在訓(xùn)練樣本充足時優(yōu)于 Eigenface原因是人臉身份區(qū)分確實(shí)是一個有監(jiān)督問題。但 LDA 有個著名的坑當(dāng)特征維度遠(yuǎn)大于樣本數(shù)時類內(nèi)散度矩陣是奇異的沒法直接求逆。這就是所謂的「小樣本問題」SSS 問題。在 10304 維特征、280 個訓(xùn)練樣本的設(shè)置下類內(nèi)散度矩陣一定是奇異的。解決辦法是先 PCA 降維再做 LDA。這實(shí)際上就是 Fisherface 的原始做法先用 PCA 把維度降到N - C樣本數(shù)減類別數(shù)再在降維后的空間里跑 LDA。參數(shù)上要保證 PCA 降到的維度小于類內(nèi)散度矩陣的秩否則 LDA 那一步還是要爆炸。4.2 Fisherface 的穩(wěn)定實(shí)現(xiàn)先 PCA 后 LDA 的兩步走% LDA / Fisherface 實(shí)現(xiàn) % 兩步走先 PCA 降維到 N-C280-40240再在低維空間做 LDA % 第一步PCA 降維到 240 維 N size(trainData, 1); % 280 C numClasses; % 40 pcaDim N - C; % 240保證類內(nèi)散度矩陣可逆 meanFace mean(trainData, 1); X trainData - meanFace; % 用 SVD 做 PCA注意這里用 econ 拿到經(jīng)濟(jì)型分解 [U_pca, ~, ~] svd(X, econ); U_pca U_pca(:, 1:pcaDim); trainPca trainData * U_pca; % 280 x 240 % 第二步在 240 維空間里算 LDA % 計算每個類的均值 classMeans zeros(C, pcaDim); globalMean mean(trainPca, 1); % 全局均值 % 類內(nèi)散度矩陣 Sw 和類間散度矩陣 Sb Sw zeros(pcaDim, pcaDim); Sb zeros(pcaDim, pcaDim); for i 1:C classIdx find(trainLabels i); classData trainPca(classIdx, :); % 當(dāng)前類的所有樣本 classMeans(i, :) mean(classData, 1); % 當(dāng)前類均值 % 類內(nèi)散度每個樣本減類均值的外積累加 diffWithin classData - classMeans(i, :); Sw Sw diffWithin * diffWithin; % 類間散度類均值減全局均值的外積乘該類樣本數(shù) diffBetween classMeans(i, :) - globalMean; Sb Sb length(classIdx) * (diffBetween * diffBetween); end % 求解廣義特征值問題Sb * w lambda * Sw * w % 這里 Sw 是 240x240已經(jīng)可逆因?yàn)樽隽?PCA 降維 [V_lda, D_lda] eig(Sb, Sw); [eigValsLda, sortIdxLda] sort(diag(D_lda), descend); V_lda V_lda(:, sortIdxLda); % LDA 最多保留 C-1 個有效方向因?yàn)?Sb 的秩最大為 C-1 ldaDim C - 1; % 39 W V_lda(:, 1:ldaDim); % 投影到 Fisherface 空間 trainLda trainPca * W; testPca testData * U_pca; testLda testPca * W; % 最近鄰分類歐氏距離省略重復(fù)代碼 % ...這段代碼里eig(Sb, Sw)是廣義特征值分解MATLAB 對eig(A, B)的底層實(shí)現(xiàn)是geig當(dāng) B 接近奇異時數(shù)值可能不穩(wěn)定。我這里提前用 PCA 把維度降到了 240Sw是滿秩的所以沒問題。如果你跳過了 PCA 直接做Sw會是 10304×10304 且秩最多 240eig會返回一堆 NaN 或者無窮大的特征值結(jié)果完全不可用。另一個關(guān)鍵點(diǎn)是ldaDim最大只能取到 39因?yàn)镾b是 40 個類的均值散布矩陣它的秩最多是C-1。你即使把ldaDim設(shè)置成 100后面的方向?qū)?yīng)的特征值也為零或接近零沒有任何判別力純屬浪費(fèi)計算量。4.3 PCA vs LDA 在這個數(shù)據(jù)集上的橫向?qū)Ρ任以谶@個數(shù)據(jù)集上跑過幾十組實(shí)驗(yàn)一個穩(wěn)定的結(jié)論是訓(xùn)練樣本充足時每類 7 張以上LDA 要好于 PCA訓(xùn)練樣本稀少時每類 3 張左右PCA 反而更穩(wěn)。原因是 LDA 需要足夠多的樣本才能穩(wěn)定估計類內(nèi)散度矩陣樣本太少時類內(nèi)散布被嚴(yán)重低估Fisher 準(zhǔn)則選出來的方向偏斜嚴(yán)重。具體數(shù)值可以參考下面這個表同樣實(shí)驗(yàn)條件下隨機(jī)打亂后取三次平均方法每類訓(xùn)練樣本數(shù) 7每類訓(xùn)練樣本數(shù) 4每類訓(xùn)練樣本數(shù) 2PCA (k60)94.8%89.2%79.5%LDA (Fisherface)96.5%88.9%75.0%這個對比說明一件事不要無腦選 LDA。很多人聽說 LDA 用到了標(biāo)簽、是「有監(jiān)督的」就覺得一定比 PCA 好但在小樣本場景下它反而更差。如果你想在論文里論證「本方法優(yōu)于傳統(tǒng)方法」至少 PCA 和 LDA 這兩個基線都得跑。5. 六條血淚踩坑記錄從加載報錯到結(jié)果虛高的排錯清單5.1 讀取 PGM 后圖像是反的或者變形現(xiàn)象imshow顯示出來的人和原圖不一樣有的左右顛倒、有的上下翻轉(zhuǎn)。原因部分鏡像站點(diǎn)的 PGM 文件頭部信息不規(guī)范。PGM 格式允許在P5和寬高之間夾雜注釋行MATLAB 的imread通常能自動跳過注釋但某些老舊鏡像文件里注釋行格式不符合規(guī)范imread會誤讀寬高參數(shù)導(dǎo)致圖像被截斷或轉(zhuǎn)置。解決先用info imfinfo(filePath)查看返回的Width、Height是否正確。如果發(fā)現(xiàn)寬高和 92×112 對不上用腳本預(yù)處理統(tǒng)一校正不要手動一張張轉(zhuǎn)。5.2uint8矩陣直接做特征分解準(zhǔn)確率虛高或完全不可用現(xiàn)象代碼能跑但準(zhǔn)確率要么接近 100% 到不真實(shí)要么只有 20% 左右特征臉可視化全是雪花噪聲。原因uint8類型做減法會截斷負(fù)數(shù)比如uint8(10) - uint8(20)在 MATLAB 里結(jié)果是 0 而不是 -10。這嚴(yán)重破壞了中心化步驟。解決在導(dǎo)入環(huán)節(jié)加im2double。另外注意如果讀入后做img(:)拉直double()轉(zhuǎn)換要在拉直之前完成之后轉(zhuǎn)類型并不改變數(shù)據(jù)已經(jīng)截斷的事實(shí)。檢查方法在命令行敲mean(trainData(:))如果結(jié)果恰好是一個很小的整數(shù)或者 0說明數(shù)據(jù)還是整數(shù)類型。5.3eig和svd選錯導(dǎo)致特征向量質(zhì)量低現(xiàn)象同樣的數(shù)據(jù)用eig(X*X)和svd(X)得到的準(zhǔn)確率有 2%~3% 的差距。原因X*X這個矩陣的條件數(shù)是原矩陣條件數(shù)的平方。當(dāng)人臉數(shù)據(jù)矩陣本身接近病態(tài)時很多行幾乎線性相關(guān)eig對舍入誤差非常敏感。svd通過二步分解直接處理原矩陣數(shù)值穩(wěn)定性好很多。解決在做 PCA 時優(yōu)先用svd(X, econ)而不是顯式構(gòu)造協(xié)方差矩陣再做eig。如果你堅持用eig比如為了提取特征臉做展示至少要在中心化后的數(shù)據(jù)上做并且檢查特征值序列是否單調(diào)遞減且沒有負(fù)值——出現(xiàn)負(fù)特征值說明數(shù)值已經(jīng)炸了。5.4 訓(xùn)練集測試集劃分方式不對準(zhǔn)確率虛高 5 個百分點(diǎn)現(xiàn)象測試集準(zhǔn)確率高達(dá) 99%但你換一種劃分方式就掉到 88%。你自己也說不清哪個是真實(shí)水平。原因整體隨機(jī)劃分時同一個人的照片可能同時出現(xiàn)在訓(xùn)練集和測試集這本身沒問題。問題在于沒有固定隨機(jī)種子每次劃分的難易程度不同運(yùn)氣好時測試集恰好都是比較容易的樣本準(zhǔn)確率直接虛高。解決用rng固定種子并且在論文或報告里寫明「使用隨機(jī)種子 42按每類 7:3 劃分」。這是最基本的實(shí)驗(yàn)可復(fù)現(xiàn)要求。另外更嚴(yán)格的驗(yàn)證方式是交叉驗(yàn)證后面第 6 章會展開。5.5 自己寫最近鄰分類時循環(huán)寫法太慢現(xiàn)象for循環(huán)對比 280 個訓(xùn)練樣本和 120 個測試樣本每次算歐氏距離總耗時不到一秒但如果訓(xùn)練集擴(kuò)大或者每張圖拉直維度超高比如你用 256×256 的圖循環(huán)會慢到懷疑人生。原因diff trainProjected - testProjected(i, :)這行代碼在循環(huán)里重復(fù)分配內(nèi)存MATLAB 是 JIT 加速的但循環(huán)體內(nèi)的內(nèi)存分配開銷逃不掉。解決用向量化寫法一次性算完整距離矩陣。pdist2(trainProjected, testProjected, euclidean)或者手動廣播。在這個數(shù)據(jù)集上兩條路差別不大但養(yǎng)成向量化的習(xí)慣對后續(xù)做更大規(guī)模實(shí)驗(yàn)很有價值。5.6 路徑里有中文或者空格imread靜默返回空現(xiàn)象代碼不報錯但img是空的size(img)返回[0 0]后續(xù)算均值、特征分解全部 NaN。原因老版本 MATLAB 的imread對含中文的路徑支持有問題部分版本甚至對路徑中的空格敏感。靜默失敗是 MATLAB 最坑的行為之一——不拋異常但給你一個空矩陣。解決數(shù)據(jù)集根目錄改成純英文路徑比如D:\face_data\att_faces。如果你非要放在中文路徑下用dir先做一次存在性驗(yàn)證確認(rèn)讀到的文件數(shù)等于 400 再做歸一化處理。我在實(shí)驗(yàn)早期因?yàn)檫@個浪費(fèi)過一下午后來習(xí)慣在腳本開頭加一段校驗(yàn)邏輯。6. 交叉驗(yàn)證的正確打開方式用有限的 400 張圖換一個可信的準(zhǔn)確率7:3 劃分能反映模型水平但一次劃分的偶然性太大。更穩(wěn)的做法是 5 折交叉驗(yàn)證把每類 10 張圖分成 5 份每份 2 張每次拿其中 4 份8 張訓(xùn)練、1 份2 張測試輪轉(zhuǎn) 5 次最后的準(zhǔn)確率取平均。這樣每一張圖都有機(jī)會當(dāng)測試樣本評估結(jié)果的方差小得多也比 k-fold 整體打亂更合理——人臉數(shù)據(jù)必須按類分折。在 MATLAB 里做這件事不需要復(fù)雜的工具箱cvpartition就能直接生成分層劃分% 5 折交叉驗(yàn)證按類別分層劃分 rng(42); cv cvpartition(labels, KFold, 5); % labels 是 400x1 的類別標(biāo)簽 % cvpartition 返回的是整體劃分但我們要的是按類分層 % 對于每類10張的情況KFold5 自動平均分成每折2張需要注意一點(diǎn)cvpartition(labels, KFold, 5)會盡量讓每折中每一類分布均勻但它不能保證每類嚴(yán)格出現(xiàn)兩次。嚴(yán)格做法是每類內(nèi)部手動分折把 10 張圖按隨機(jī)順序切成 5 組每組 2 張。雖然復(fù)雜一點(diǎn)但能保證每折類別完整、樣本均衡。交叉驗(yàn)證跑完你會得到 5 個準(zhǔn)確率取均值和標(biāo)準(zhǔn)差。報告中寫「95.2% ± 1.8%」比寫「95.2%」可信得多。這個習(xí)慣我是在一次實(shí)驗(yàn)復(fù)現(xiàn)失敗后養(yǎng)成的——別人論文里寫 98%我怎么調(diào)都只有 92%后來發(fā)現(xiàn)他的劃分方式讓訓(xùn)練集和測試集存在同源樣本。交叉驗(yàn)證不能說完全杜絕這類問題但至少能讓你自己的實(shí)驗(yàn)結(jié)論站得住。還有一個我常用的技巧把識別錯誤的樣本全部打印出來拼成一張網(wǎng)格圖看看。這能快速告訴你錯誤集中在哪個人身上——是某個類本身難分比如戴眼鏡和不戴眼鏡的差異還是某張圖質(zhì)量太差。這種錯誤模式分析的價值不低于準(zhǔn)確率數(shù)字本身。人臉識別實(shí)驗(yàn)跑通不難跑出可信的結(jié)論需要這些驗(yàn)證手段。希望這些踩過的坑能幫你少走一段彎路。本文還有配套的精品資源點(diǎn)擊獲取