踐詳解)
簡介一份以Python實(shí)現(xiàn)PCA人臉識別算法的完整學(xué)習(xí)與實(shí)戰(zhàn)資源主要面向計(jì)算機(jī)專業(yè)學(xué)生、算法初學(xué)者及需要完成課程設(shè)計(jì)或畢業(yè)設(shè)計(jì)的開發(fā)者幫助其系統(tǒng)理解主成分分析在人臉識別中的降維原理與代碼落地方式。壓縮包共22個(gè)文件包含4個(gè)Python腳本核心算法實(shí)現(xiàn)、人臉識別入口及輔助數(shù)組操作、1份Markdown文檔原理推導(dǎo)與使用說明、16張過程截圖展示不同階段的效果以及ORL人臉數(shù)據(jù)集整體約3.76MB結(jié)構(gòu)緊湊、便于快速上手。已有473人學(xué)習(xí)下載。通過該資源讀者能掌握從數(shù)據(jù)預(yù)處理、協(xié)方差矩陣計(jì)算、特征值分解到主成分選取與投影的完整流程并可直接運(yùn)行代碼觀察識別效果對撰寫實(shí)驗(yàn)報(bào)告或搭建人臉識別原型具有較強(qiáng)的參考價(jià)值。1. PCA人臉識別這個(gè)標(biāo)題真正能解決的是什么當(dāng)你手里只有幾千張像素照片卻想判斷“攝像頭前這個(gè)人是不是庫里的某個(gè)人”直接比較像素會非常脆弱光照一變、角度稍偏整張圖的灰度分布就變了拿原始像素算歐氏距離幾乎不可用。PCA主成分分析把人臉從高維像素空間投影到低維特征空間保留的是“臉的大致結(jié)構(gòu)”而不是像素細(xì)節(jié)所以對光照和表情有一定抗性。基于Python的PCA人臉識別算法是絕大多數(shù)人臉識別課程和入門項(xiàng)目的第一站代碼量小、依賴少、可解釋性強(qiáng)適合在校學(xué)生、轉(zhuǎn)崗工程師快速跑通完整鏈路。下面用一套最小可運(yùn)行的代碼和配套文檔把原理、實(shí)現(xiàn)細(xì)節(jié)和常見翻車點(diǎn)一次講清楚。2. PCA人臉識別原理先搞懂?dāng)?shù)學(xué)再寫代碼2.1 為什么人臉識別要先做主成分分析人臉圖像一旦被拉平成向量維度就是像素個(gè)數(shù)。一張64×64灰度圖是4096維100個(gè)人每人10張圖訓(xùn)練矩陣也只有1000行。在幾千維空間里做最近鄰分類距離被大量噪聲維度稀釋這就是課堂里常說的“高維災(zāi)難”。PCA主成分分析要做的事是找到數(shù)據(jù)方差最大的若干個(gè)正交方向把每張臉投影到這個(gè)低維子空間里用幾十個(gè)特征系數(shù)代替幾千個(gè)像素值。人臉成像時(shí)整體結(jié)構(gòu)高度相關(guān)眼睛、鼻子、嘴的相對位置基本一致真正能區(qū)分不同人的差異集中在少數(shù)方向和少數(shù)維度上所以主成分方向抓取的是“人臉的公共結(jié)構(gòu)”而不是某一張照片的隨機(jī)噪聲。這里有一個(gè)容易被誤解的點(diǎn)PCA不做識別它是無監(jiān)督降維。它只需要訓(xùn)練矩陣不關(guān)心標(biāo)簽就能得到一組主成分方向。這些方向在圖像空間里還原出來是一批模糊的人臉模板業(yè)內(nèi)叫特征臉。特征臉不指代任何一個(gè)人而是這一批人臉數(shù)據(jù)的“公共坐標(biāo)系”。說得更直白一點(diǎn)把人臉識別看成“把每張臉投射到特征臉坐標(biāo)系再做最近鄰分類”特征臉就是給所有臉準(zhǔn)備的一套基函數(shù)。從落地角度看為什么選PCA而不是直接拿卷積神經(jīng)網(wǎng)絡(luò)來做因?yàn)镻CA能在十分鐘內(nèi)跑通且每個(gè)環(huán)節(jié)都能用圖表解釋特征臉長什么樣、主成分占比多少、最近鄰距離分布如何。這些對排查問題極有價(jià)值。CNN方案雖然上限更高但對數(shù)據(jù)集規(guī)模、算力和排錯(cuò)手段的要求也更高。入門項(xiàng)目選PCA不是因?yàn)樗顝?qiáng)而是因?yàn)樗馨选疤卣魈崛》诸悺边@條主線看得清清楚楚。2.2 核心數(shù)學(xué)推導(dǎo)與SVD計(jì)算路徑理論上要算主成分就要對中心化后的協(xié)方差矩陣做特征值分解協(xié)方差矩陣C (1/N) * X_centered^T * X_centered其中X_centered是中心化后的訓(xùn)練矩陣N是樣本數(shù)。C的形狀是n_features×n_features。一張64×64人臉圖的特征維度是4096圖像再大一點(diǎn)C的內(nèi)存就是維度數(shù)的平方幾十萬像素的圖像做特征分解基本不現(xiàn)實(shí)。所以實(shí)際項(xiàng)目里一般走SVD分解對X_centered直接做奇異值分解X_centered U S V^TV的行就是協(xié)方差矩陣的特征向量S是奇異值主成分方向就是V^T的行。數(shù)學(xué)上兩者等價(jià)工程上SVD不需要構(gòu)造大矩陣數(shù)值穩(wěn)定性也好得多這是PCA實(shí)現(xiàn)里最值得記的一個(gè)技巧。import numpy as np class ManualPCA: def __init__(self, n_comp50): self.n_comp n_comp self.mean_ None self.components_ None self.explained_variance_ratio_ None def fit(self, X): self.mean_ X.mean(axis0) Xc X - self.mean_ U, S, Vt np.linalg.svd(Xc, full_matricesFalse) # Vt 的第 i 行就是第 i 主成分方向按奇異值降序排列 self.components_ Vt[:self.n_comp] total_var np.sum(S ** 2) self.explained_variance_ratio_ (S[:self.n_comp] ** 2) / total_var def transform(self, X): Xc X - self.mean_ return Xc self.components_.T邏輯說明fit 中先對原始矩陣做中心化也就是減去均值臉讓所有樣本圍繞原點(diǎn)分布然后做 SVD。np.linalg.svd 返回的 Vt 是右奇異向量矩陣按奇異值降序排列Vt[0] 對應(yīng)方差最大的方向。components_ 按“行”存特征向量后面做投影或可視化時(shí)不要轉(zhuǎn)置繞暈。transform 里 Xc self.components_.T 就是投影到主成分方向得到新的低維特征系數(shù)。補(bǔ)充一個(gè)參數(shù)細(xì)節(jié)用 SVD 算 PCA 時(shí)U、S、Vt 的對齊關(guān)系由 SVD 直接保證不需要再排序如果自己寫特征值分解eigvals 的順序是亂序的必須用 np.argsort 處理一遍。很多人在封裝 PCA 類時(shí)死磕不出來問題往往出在特征值排序上而不是算法本身。2.3 PCA的人臉分布假設(shè)與算法邊界PCA 基于線性假設(shè)它認(rèn)為人臉可以表達(dá)為少量特征臉的線性組合?,F(xiàn)實(shí)中的光照變化大、姿態(tài)變化大、存在遮擋時(shí)線性假設(shè)會被破壞純 PCA 識別率明顯下降。這個(gè)邊界是客觀存在的不是代碼寫錯(cuò)。很多項(xiàng)目里出現(xiàn)“PCA 識別率從 95% 掉到 70%”的怪現(xiàn)象通常不是算法崩潰而是數(shù)據(jù)集里的人臉姿態(tài)和光照后來變了或者測試時(shí)加入了陌生人的臉。了解這個(gè)邊界對后續(xù)調(diào)參很重要當(dāng)識別率不達(dá)標(biāo)時(shí)不要盲目去掃主成分?jǐn)?shù)量先看圖是不是已經(jīng)超出了線性降維能描述的范圍。如果訓(xùn)練集里全是正面照測試集拿偏側(cè)面來打那 k 再怎么掃都沒用。這條經(jīng)驗(yàn)值得寫進(jìn)項(xiàng)目文檔避免將來排查時(shí)走彎路。3. 環(huán)境搭建與數(shù)據(jù)準(zhǔn)備讓每張臉都變成一行向量3.1 Python環(huán)境與依賴庫安裝先把環(huán)境準(zhǔn)備到位。與平臺無關(guān)Windows、macOS、Linux 都行Python 3.8 以上版本即可。如果你還在用編輯器直接跑 Python建議先按 python 安裝教程把解釋器和環(huán)境變量配好再創(chuàng)建一個(gè)虛擬環(huán)境避免和系統(tǒng)里其他項(xiàng)目互相污染。python -m venv faceenv source faceenv/bin/activate pip install numpy opencv-python scikit-learn matplotlib參數(shù)說明numpy 負(fù)責(zé)矩陣運(yùn)算opencv-python 負(fù)責(zé)圖片讀取、灰度化、resize 和直方圖均衡化scikit-learn 只用到 model_selection 和 metricsmatplotlib 用于畫特征臉和混淆矩陣。不裝第三方人臉識別庫因?yàn)楹诵乃惴ㄎ覀冏约河?SVD 手寫。如果之后想替換成 sklearn.decomposition.PCA也只需要改模型封裝那一層預(yù)處理代碼完全不用動。安裝完可以順手驗(yàn)證三個(gè)關(guān)鍵 importpython -c import numpy, cv2, sklearn, matplotlib; print(ok)如果某一項(xiàng)報(bào)錯(cuò)先處理對應(yīng)庫的安裝問題再繼續(xù)不要帶著殘破環(huán)境跑后面的腳本否則報(bào)錯(cuò)來源很容易混淆。3.2 數(shù)據(jù)集目錄約定與批量導(dǎo)入人臉數(shù)據(jù)怎么組織決定了代碼能多快跑通。最簡單也最不容易出錯(cuò)的布局是“每個(gè)身份一個(gè)子目錄”子目錄名就是身份編號或姓名。比如dataset/ s1/ a.jpg b.jpg c.jpg s2/ ...用 OpenCV 批量讀進(jìn)來統(tǒng)一灰度、統(tǒng)一尺寸做直方圖均衡化再拉平成一行放到訓(xùn)練矩陣?yán)?。這里直接給一段可復(fù)制到項(xiàng)目里的函數(shù)import os import cv2 import numpy as np def load_dataset(root_dir, target_size(64, 64), use_equalizeTrue): X, y [], [] names sorted(os.listdir(root_dir)) label_of {name: i for i, name in enumerate(names)} for name in names: person_dir os.path.join(root_dir, name) if not os.path.isdir(person_dir): continue for fname in sorted(os.listdir(person_dir)): if not fname.lower().endswith((.jpg, .jpeg, .png, .pgm)): continue path os.path.join(person_dir, fname) img cv2.imread(path, cv2.IMREAD_GRAYSCALE) if img is None: continue img cv2.resize(img, target_size) if use_equalize: img cv2.equalizeHist(img) X.append((img.astype(np.float32) / 255.0).flatten()) y.append(label_of[name]) return np.asarray(X), np.asarray(y)邏輯說明cv2.imread 的第二個(gè)參數(shù)填 cv2.IMREAD_GRAYSCALE直接讀成單通道灰度圖避免彩色圖三通道帶來的冗余計(jì)算。resize 強(qiáng)制統(tǒng)一尺寸這是后面訓(xùn)練矩陣能否拼接的前提。flatten 把二維圖像拉成一維歸一化除以 255 把像素值壓在 0 到 1 區(qū)間避免不同位深的圖片混在一起產(chǎn)生數(shù)值偏差。use_equalize 這個(gè)開關(guān)要保留因?yàn)楹竺姹芸诱鹿?jié)會講到均衡化不是對所有數(shù)據(jù)集都有效。參數(shù)說明target_size 選 64×64 時(shí)訓(xùn)練快占內(nèi)存小想提升細(xì)節(jié)信息可以換 112×112 或 128×128但 PCA 的特征維度隨之上升訓(xùn)練時(shí)間變長。建議先用小尺寸跑通全流程再逐步放大做對比。3.3 按人劃分訓(xùn)練集和測試集而不是按圖片隨機(jī)切PCA 本質(zhì)上是分類模型它只能對“見過的人”做區(qū)分。如果隨機(jī)打散照片再劃分訓(xùn)練測試同一個(gè)人的照片同時(shí)出現(xiàn)在兩邊模型等于作弊識別率虛高。網(wǎng)上很多入門 demo 就是這么干的識別率動不動 99%一上真實(shí)場景就翻車。from sklearn.model_selection import train_test_split def split_by_person(X, y, test_size0.4, random_state42): train_idx, test_idx [], [] for label in np.unique(y): idx np.where(y label)[0] a, b train_test_split(idx, test_sizetest_size, random_staterandom_state, stratifyy[idx]) train_idx.extend(a) test_idx.extend(b) train_idx np.array(train_idx) test_idx np.array(test_idx) return X[train_idx], y[train_idx], X[test_idx], y[test_idx]邏輯說明這里每次循環(huán)處理一個(gè)身份的下標(biāo)train_test_split 在單個(gè)身份內(nèi)部隨機(jī)切保證“身份隔離”。random_state 固定下來讓每次運(yùn)行結(jié)果可復(fù)現(xiàn)否則兩次跑出來的準(zhǔn)確率不同調(diào)參時(shí)很難判斷是參數(shù)變了還是隨機(jī)劃分變了。這一步做得越嚴(yán)謹(jǐn)后面排查問題越省力。3.4 數(shù)據(jù)量不夠時(shí)的快速驗(yàn)證路徑如果沒有自己的數(shù)據(jù)集可以先用 sklearn 自帶的 Olivetti 人臉庫做算法驗(yàn)證。這個(gè)數(shù)據(jù)集包含 40 個(gè)人每人 10 張灰度照片都是正面且光照相對均勻非常適合先跑通 PCA 流程。加載方式通常是 fetch_olivetti_faces把返回的 images 和 target 組織成樣本矩陣后代碼路徑與本地?cái)?shù)據(jù)集完全一致。不過要注意內(nèi)置數(shù)據(jù)集的大小只有 64×64驗(yàn)證用沒問題真上線還是要換成自己的業(yè)務(wù)數(shù)據(jù)。PCA 對訓(xùn)練集人員構(gòu)成非常敏感內(nèi)置數(shù)據(jù)集上跑出的 95% 準(zhǔn)確率不能直接當(dāng)作真實(shí)場景的預(yù)期值。4. PCA特征臉實(shí)現(xiàn)訓(xùn)練、投影與最近鄰識別完整代碼4.1 用SVD實(shí)現(xiàn)PCA模型類不把 PCA 流程寫成一個(gè)類后面維護(hù)會很痛苦。我一般把特征臉識別封裝成類似 sklearn 風(fēng)格的類fit 負(fù)責(zé)訓(xùn)練project 負(fù)責(zé)提取特征predict 負(fù)責(zé)最近鄰分類。這樣測試代碼、調(diào)參腳本、文檔里的復(fù)現(xiàn)命令都能共用同一個(gè)接口。import numpy as np class EigenFaceRecognizer: def __init__(self, n_components0.95, distance_metriceuclidean): self.n_components n_components self.distance_metric distance_metric self.mean_face None self.components None self.W_train None self.y_train None self.k None def fit(self, X, y): n_samples X.shape[0] self.mean_face X.mean(axis0) Xc X - self.mean_face U, S, Vt np.linalg.svd(Xc, full_matricesFalse) explained_ratio np.cumsum(S ** 2) / np.sum(S ** 2) if isinstance(self.n_components, float): self.k int(np.searchsorted(explained_ratio, self.n_components) 1) else: self.k int(self.n_components) self.k min(self.k, n_samples - 1, X.shape[1]) self.components Vt[:self.k] self.W_train Xc self.components.T self.y_train y return self def project(self, X): Xc X - self.mean_face return Xc self.components.T def predict(self, X, thresholdNone): W self.project(X) if self.distance_metric euclidean: diff W[:, None, :] - self.W_train[None, :, :] dist np.sqrt((diff ** 2).sum(axis2)) elif self.distance_metric cosine: norm_w np.linalg.norm(W, axis1, keepdimsTrue) norm_t np.linalg.norm(self.W_train, axis1, keepdimsTrue) cos (W self.W_train.T) / (norm_w norm_t.T) dist 1 - cos else: raise ValueError(distance_metric 只支持 euclidean/cosine) best np.argmin(dist, axis1) min_dist dist[np.arange(len(X)), best] if threshold is not None: unknown min_dist threshold return self.y_train[best], unknown return self.y_train[best]邏輯說明fit 里components 是主成分方向W_train 是訓(xùn)練樣本投影后的系數(shù)矩陣一行對應(yīng)一張圖在特征臉坐標(biāo)系里的坐標(biāo)。predict 里沒有額外訓(xùn)練分類器因?yàn)?PCA 人臉識別最常見的就是最近鄰把測試圖投到特征臉空間再計(jì)算它和所有訓(xùn)練系數(shù)的距離距離最小的類作為預(yù)測結(jié)果。參數(shù)說明n_components 傳 0.95 表示自動選擇能解釋 95% 方差的最小 k傳整數(shù)則強(qiáng)制保留固定數(shù)量的主成分。最大值不能超過 min(n_samples-1, n_features)因?yàn)榫€性子空間的自由度受樣本數(shù)約束。k 選擇太小會丟失差異太大會把噪聲學(xué)進(jìn)去實(shí)際項(xiàng)目中一般通過交叉驗(yàn)證找平衡點(diǎn)。distance_metric 支持歐氏距離和余弦相似度后面進(jìn)階章會做對比。threshold 參數(shù)是為“陌生人拒識”準(zhǔn)備的。攝像頭前的人不在庫里時(shí)算法不應(yīng)該強(qiáng)行認(rèn)領(lǐng)成庫中最像的人。設(shè)置一個(gè)距離閾值當(dāng)最小距離超過它時(shí)返回 unknown。閾值怎么定放到避坑章節(jié)詳細(xì)說。4.2 最小跑通訓(xùn)練與識別一條命令把前面的數(shù)據(jù)加載和切分函數(shù)接進(jìn)來就能做第一次完整訓(xùn)練和預(yù)測。X, y load_dataset(dataset, target_size(64, 64), use_equalizeTrue) X_train, y_train, X_test, y_test split_by_person(X, y, test_size0.4) model EigenFaceRecognizer(n_components0.95) model.fit(X_train, y_train) pred model.predict(X_test) acc (pred y_test).mean() print(test acc:, round(acc, 3))邏輯說明流程就是“加載 → 按人切分 → 訓(xùn)練 → 預(yù)測 → 算準(zhǔn)確率”。如果輸出準(zhǔn)確率在 80% 以上說明鏈路正常。如果只有六七十甚至更低先不要懷疑 PCA 實(shí)現(xiàn)回去查數(shù)據(jù)集是否統(tǒng)一了尺寸、是否做了按人切分、圖片是否全是有效人臉。數(shù)據(jù)問題在 PCA 項(xiàng)目里遠(yuǎn)比算法問題常見。從這套最小流程開始后續(xù)所有調(diào)參都在這條鏈路上做替換不需要改數(shù)據(jù)加載部分。4.3 特征臉可視化確認(rèn)模型學(xué)到了什么訓(xùn)練結(jié)束后把 components 還原成圖像尺寸能直觀看到主成分的實(shí)際面貌。import matplotlib.pyplot as plt def visualize_eigenfaces(model, image_size(64, 64), top_n10): n min(top_n, model.k) fig, axes plt.subplots(2, (n 1) // 2, figsize(10, 5)) axes axes.ravel() for i in range(n): face model.components[i].reshape(image_size) face face - face.min() if face.max() 0: face face / face.max() axes[i].imshow(face, cmapgray) axes[i].set_title(eigenface {}.format(i)) axes[i].axis(off) plt.tight_layout() plt.show()邏輯說明特征臉向量的元素有正有負(fù)直接交給 imshow 會被截?cái)嗟?[0,1] 區(qū)間負(fù)值全部變成黑塊看起來像噪點(diǎn)。所以要先做 min-max 歸一化再顯示。正常結(jié)果里前幾個(gè)特征臉通常是整體明暗方向后面的逐漸出現(xiàn)五官輪廓。如果顯示結(jié)果完全沒有臉的結(jié)構(gòu)基本都是中心化、尺寸歸一化或數(shù)據(jù)組織出了問題。這一步能提前暴露很多隱患比盯著準(zhǔn)確率猜原因高效得多。4.4 代碼文檔組織一個(gè)能交付的項(xiàng)目長什么樣標(biāo)題里的“文檔詳解”對應(yīng)的其實(shí)是交付物。一個(gè)能提交的人臉識別小項(xiàng)目至少要包含四塊環(huán)境依賴說明、數(shù)據(jù)集結(jié)構(gòu)說明、運(yùn)行命令、核心參數(shù)與調(diào)參記錄。我一般習(xí)慣在 README 里固定這幾段內(nèi)容并且把復(fù)現(xiàn)命令寫成一行能讓后來者直接執(zhí)行的形式。比如運(yùn)行命令固定為python run_eigenface.py --dataset ./dataset --n-components 0.95 --size 64這樣別人拿到壓縮包后先讀 README再敲命令五分鐘內(nèi)就能看到結(jié)果。文檔不需要長篇大論但關(guān)鍵參數(shù)和復(fù)現(xiàn)步驟必須寫死。如果下載下來的壓縮包里只有代碼沒有文檔按這個(gè)框架自己補(bǔ)一份也行梳理過程比寫代碼更容易查漏補(bǔ)缺。5. PCA人臉識別避坑與排查5個(gè)反復(fù)出現(xiàn)的翻車場景5.1 現(xiàn)象圖片尺寸不一致訓(xùn)練矩陣拼接報(bào)錯(cuò)現(xiàn)象運(yùn)行 load_dataset 時(shí) numpy 報(bào)錯(cuò)提示 setting an array element with a sequence或者模型訓(xùn)練時(shí)維度對不上。原因數(shù)據(jù)集里圖片分辨率不統(tǒng)一漏了 resize 或者只對部分圖片做了 resize。PCA 要求所有樣本在特征維度上嚴(yán)格對齊一旦某個(gè)樣本比其他樣本多幾個(gè)像素numpy 就無法構(gòu)建二維數(shù)組。解決在 load_dataset 中始終先 cv2.resize 到固定尺寸再 flatten。同時(shí)處理 cv2.imread 返回 None 的情況因?yàn)榭瘴募驌p壞文件會讓 OpenCV 靜默返回 None跳過并打印日志比直接報(bào)錯(cuò)退出更好用。5.2 現(xiàn)象直方圖均衡化反而降低識別率現(xiàn)象加上 equalizeHist 后識別率從 85% 掉到 75%某幾個(gè)人的照片反復(fù)認(rèn)錯(cuò)。原因當(dāng)照片本身光照均勻、對比度足夠時(shí)均衡化會把灰度分布強(qiáng)行拉寬讓不同照片的灰度分布變得更接近反而抹掉了身份差異。在過度曝光或欠曝的照片上均衡化還會放大噪聲。解決把 use_equalize 做成可配置開關(guān)分別在開和關(guān)兩種狀態(tài)下跑同一份訓(xùn)練測試劃分用結(jié)果決定是否啟用。預(yù)處理不是越多越好每個(gè)環(huán)節(jié)都要用實(shí)驗(yàn)數(shù)據(jù)來驗(yàn)證。5.3 現(xiàn)象測試集準(zhǔn)確率很高換一批新照片就翻車現(xiàn)象訓(xùn)練測試劃分后準(zhǔn)確率超過 98%換一批同一批人的新照片立刻誤判。原因大概率是隨機(jī)切分照片同一個(gè)人的照片同時(shí)進(jìn)了訓(xùn)練集和測試集。模型記住了個(gè)體級別的高頻特征本質(zhì)上是在背答案不是泛化。解決嚴(yán)格按身份劃分測試集。寧可測試集小一點(diǎn)也要保證每個(gè)身份參與測試的照片完全不參與訓(xùn)練。對真實(shí)場景來說最好再收集一些同一個(gè)人在不同時(shí)間、不同環(huán)境下的新照片放到測試?yán)矧?yàn)證才算有效。5.4 現(xiàn)象陌生人被強(qiáng)行認(rèn)成庫里的某個(gè)人現(xiàn)象輸入一張訓(xùn)練集中不存在的人臉模型仍然輸出了某個(gè)人的名字。原因predict 只做了最近鄰查找沒有設(shè)置拒絕閾值。最近鄰分類天然會產(chǎn)生一個(gè)最近類不管當(dāng)前人臉離所有已知類都遠(yuǎn)。解決在 predict 中增加 threshold 參數(shù)當(dāng)最小距離 min_dist 超過閾值時(shí)返回 unknown。閾值的選擇要在驗(yàn)證集上統(tǒng)計(jì)兩類距離分布同一個(gè)人的距離通常小不同人的距離通常大取兩個(gè)分布的交點(diǎn)作為初始閾值再根據(jù)錯(cuò)認(rèn)率和拒識率做調(diào)整。如果項(xiàng)目里能收集到“非庫內(nèi)人員”的照片可以作為負(fù)樣本來輔助標(biāo)定。5.5 現(xiàn)象內(nèi)存占用過高特征臉全是噪點(diǎn)現(xiàn)象圖像分辨率較大時(shí)訓(xùn)練過程內(nèi)存直接吃掉幾個(gè) G特征臉顯示成一片雪花點(diǎn)。原因如果 PCA 實(shí)現(xiàn)里用了 np.cov(X.T) 構(gòu)造協(xié)方差矩陣200×200 的圖像對應(yīng) 40000 維特征協(xié)方差矩陣就是 40000×40000算下來約 12.8 GB 內(nèi)存普通電腦直接卡死。特征臉全是噪點(diǎn)則通常是沒做中心化、主成分?jǐn)?shù)量選太大或者樣本本身沒對齊。解決改用 SVD 路徑對中心化矩陣直接做奇異值分解不顯式構(gòu)造協(xié)方差矩陣。特征臉噪點(diǎn)多時(shí)先檢查中心化和歸一化再把 k 調(diào)小一些。也可以先用 32×32 跑通流程再逐步放大尺寸對比效果觀察準(zhǔn)確率和資源開銷的變化。6. 從驗(yàn)證到改進(jìn)讓特征臉模型在真實(shí)場景更可靠6.1 用分層K折交叉驗(yàn)證和混淆矩陣評估模型只跑一次隨機(jī)劃分不能說明模型可靠。我習(xí)慣用分層 K 折交叉驗(yàn)證配合混淆矩陣看清楚哪些身份最容易互相認(rèn)錯(cuò)。from sklearn.model_selection import StratifiedKFold from sklearn.metrics import confusion_matrix, accuracy_score skf StratifiedKFold(n_splits5, shuffleTrue, random_state1) accs [] cm_all np.zeros((len(np.unique(y)), len(np.unique(y))), dtypeint) for train_idx, test_idx in skf.split(X, y): model EigenFaceRecognizer(n_components0.95) model.fit(X[train_idx], y[train_idx]) pred model.predict(X[test_idx]) accs.append(accuracy_score(y[test_idx], pred)) cm_all confusion_matrix(y[test_idx], pred, labelsnp.unique(y)) print(5-fold acc:, np.mean(accs), /-, np.std(accs))邏輯說明StratifiedKFold 能保證每一折里每個(gè)身份的樣本比例基本一致。打印平均準(zhǔn)確率和標(biāo)準(zhǔn)差標(biāo)準(zhǔn)差大說明模型對數(shù)據(jù)劃分很敏感可能是個(gè)別身份樣本太少或者某幾個(gè)身份特征太接近?;煜仃噷蔷€外的格子就是最容易認(rèn)錯(cuò)的那幾對人臉值得翻出原圖看看是什么原因。6.2 歐氏距離與余弦相似度距離度量怎么選4.1 的代碼里已經(jīng)預(yù)留了 distance_metric 參數(shù)。歐氏距離對投影系數(shù)的整體幅值敏感當(dāng)測試圖偏亮或偏暗時(shí)系數(shù)幅值會整體偏移容易誤判余弦相似度只看方向?qū)Ψ底兓€(wěn)定。for metric in [euclidean, cosine]: model EigenFaceRecognizer(n_components0.95, distance_metricmetric) model.fit(X_train, y_train) pred model.predict(X_test) acc (pred y_test).mean() print(metric, acc:, round(acc, 3))邏輯說明兩種距離的差別在光照變化大的數(shù)據(jù)集上體現(xiàn)得更明顯。如果兩者準(zhǔn)確率差不多默認(rèn)用歐氏距離就行因?yàn)殚撝禈?biāo)定更直觀。如果差異大優(yōu)先選擇更穩(wěn)的那個(gè)并同步調(diào)整 threshold 參數(shù)。實(shí)際項(xiàng)目中這兩種距離都跑一遍再結(jié)合“陌生人拒識”場景看誤報(bào)率是我常用的一套驗(yàn)證組合。6.3 從PCA到LBPH再到深度特征下一步怎么走PCA 的線性假設(shè)決定了它的天花板。當(dāng)人數(shù)超過幾十、光照復(fù)雜、帶遮擋時(shí)可以換用 LBPH它對局部紋理做編碼對光照變化更魯棒如果還不行就換成深度特征提取用預(yù)訓(xùn)練模型把人臉圖片映射成 embedding 向量再做同樣的最近鄰分類。你會發(fā)現(xiàn)PCA 階段建立的“提特征 最近鄰分類”框架在后續(xù)方案里依然成立只是特征來源從像素投影換成了更復(fù)雜的學(xué)習(xí)器。做 PCA 人臉識別這一年最深的感受是代碼十分鐘寫完數(shù)據(jù)卻要折騰兩天。維度、預(yù)處理、劃分方式任何一個(gè)環(huán)節(jié)出錯(cuò)最后都以準(zhǔn)確率下降的形式暴露出來。后來我養(yǎng)成一個(gè)習(xí)慣每次只改一個(gè)變量固定隨機(jī)種子和同一份劃分把實(shí)驗(yàn)結(jié)果記在同一張表里再好的“感覺”也不如一張能對比的記錄表。項(xiàng)目文檔也順著這個(gè)習(xí)慣寫每個(gè)參數(shù)和對應(yīng)準(zhǔn)確率都留痕。這是把算法從玩具跑到可信結(jié)果最踏實(shí)的一步。希望這篇筆記能幫你在復(fù)現(xiàn)的路上少繞彎子。本文還有配套的精品資源點(diǎn)擊獲取