習(xí)人臉識(shí)別系統(tǒng)畢業(yè)設(shè)計(jì):從CNN選型到答辯演示全鏈路)
簡(jiǎn)介這份資源面向高校學(xué)生與深度學(xué)習(xí)入門者提供一套基于Python的人臉識(shí)別系統(tǒng)完整畢業(yè)設(shè)計(jì)實(shí)現(xiàn)涵蓋代碼、模型與文檔說(shuō)明可用于畢業(yè)設(shè)計(jì)、課程設(shè)計(jì)或期末大作業(yè)。項(xiàng)目采用深度學(xué)習(xí)方案涉及FER2013、CK、JAFFE等公開數(shù)據(jù)集的表情識(shí)別訓(xùn)練流程并包含數(shù)據(jù)標(biāo)注與旋轉(zhuǎn)、鏡像翻轉(zhuǎn)、亮度調(diào)整等數(shù)據(jù)增強(qiáng)手段代碼注釋詳盡新手也能理解部署。壓縮包共103個(gè)文件約20.98MB以19個(gè)py源碼、35個(gè)png與14個(gè)jpg圖像、8個(gè)xml配置、3個(gè)hdf5模型權(quán)重及docx說(shuō)明文檔為主另含少量mp4演示與gif素材結(jié)構(gòu)清晰便于按模塊查閱。目前已有655人學(xué)習(xí)下載適合需要完整賽題方案、可運(yùn)行代碼與訓(xùn)練思路參考的讀者能幫助快速搭建環(huán)境、理解模型訓(xùn)練與識(shí)別流程并作為答辯與報(bào)告撰寫的實(shí)用素材。1. 從零搭一套能跑通答辯的人臉識(shí)別系統(tǒng)Python 深度學(xué)習(xí)畢業(yè)設(shè)計(jì)到底要交什么很多同學(xué)拿到“基于 Python 深度學(xué)習(xí)人臉識(shí)別系統(tǒng)設(shè)計(jì)與實(shí)現(xiàn)”這個(gè)題目時(shí)第一反應(yīng)是去搜免費(fèi) Python 源碼大全找到一個(gè)能跑的 demo 就以為萬(wàn)事大吉。真正到了答辯現(xiàn)場(chǎng)老師問(wèn)一句“你的模型輸入尺寸是多少、為什么用這個(gè)損失函數(shù)、誤識(shí)別率怎么測(cè)的”當(dāng)場(chǎng)就卡住了。這套系統(tǒng)的核心不是“能識(shí)別”而是“你能說(shuō)清楚它為什么能識(shí)別、邊界在哪、怎么驗(yàn)證”。它解決的是從數(shù)據(jù)集準(zhǔn)備、模型選型、訓(xùn)練調(diào)參到推理部署的完整鏈路適合計(jì)算機(jī)、電子信息、物聯(lián)網(wǎng)等專業(yè)的畢業(yè)設(shè)計(jì)場(chǎng)景也適合想用一個(gè)人臉識(shí)別門禁系統(tǒng)設(shè)計(jì)作為切入深度學(xué)習(xí)入門的人。下面按我實(shí)際帶過(guò)幾屆畢設(shè)的經(jīng)驗(yàn)把這條鏈路拆開講透。2. 人臉識(shí)別系統(tǒng)的技術(shù)選型為什么是 CNN 而不是傳統(tǒng)特征2.1 從 Haar 到 CNN兩條路線的本質(zhì)差別傳統(tǒng)人臉識(shí)別走的是“人工特征 分類器”路線。Haar 級(jí)聯(lián)做檢測(cè)LBPH 做識(shí)別OpenCV 幾行代碼就能跑。它的優(yōu)點(diǎn)是 CPU 上就能實(shí)時(shí)缺點(diǎn)是光照一變、角度一偏準(zhǔn)確率斷崖式下跌。我見過(guò)太多畢設(shè)用 LBPH 跑出 95% 的準(zhǔn)確率一問(wèn)測(cè)試集才發(fā)現(xiàn)是同一個(gè)人的不同照片換個(gè)人直接崩。深度學(xué)習(xí)路線用 CNN 自動(dòng)提取特征本質(zhì)是把“長(zhǎng)什么樣算同一個(gè)人”這件事交給網(wǎng)絡(luò)去學(xué)。它需要 GPU 訓(xùn)練但推理階段可以在 CPU 上跑精度和魯棒性完全不是一個(gè)量級(jí)。對(duì)于畢業(yè)設(shè)計(jì)來(lái)說(shuō)選 CNN 路線還有一個(gè)隱性好處你能講的東西多——卷積核、池化、損失函數(shù)、數(shù)據(jù)增強(qiáng)每一個(gè)都是答辯時(shí)的得分點(diǎn)。常見做法是檢測(cè)用 MTCNN 或 RetinaFace識(shí)別用 FaceNet 或 ArcFace 架構(gòu)。如果算力有限MobileNet 做 backbone 也能跑出可用的結(jié)果。2.2 環(huán)境搭建Python 安裝到 vscode python 環(huán)境配置先把環(huán)境理順。Python 版本建議 3.8 到 3.10太新的版本有些深度學(xué)習(xí)庫(kù)還沒跟上。安裝時(shí)勾選“Add Python to PATH”不然后面 pip 命令找不到。vscode 配置 Python 環(huán)境時(shí)按 CtrlShiftP 輸入“Python: Select Interpreter”選中你安裝的那個(gè)解釋器。# 創(chuàng)建虛擬環(huán)境避免污染全局包 python -m venv face_env # Windows 激活 face_env\Scripts\activate # Linux/Mac 激活 source face_env/bin/activate # 安裝核心依賴 pip install torch torchvision opencv-python numpy matplotlib scikit-learn pip install facenet-pytorch # 如果走 FaceNet 路線這里解釋一下為什么用虛擬環(huán)境深度學(xué)習(xí)庫(kù)版本沖突是家常便飯torch 和 torchvision 版本不匹配直接報(bào)錯(cuò)。虛擬環(huán)境讓你可以隨時(shí)刪掉重來(lái)不用重裝整個(gè) Python。facenet-pytorch這個(gè)包封裝了 MTCNN 和 InceptionResnetV1省去自己搭網(wǎng)絡(luò)的麻煩適合畢設(shè)快速出結(jié)果。注意如果你用的是 GPU 訓(xùn)練torch 安裝命令要去官網(wǎng)查對(duì)應(yīng) CUDA 版本的直接 pip install torch 裝的是 CPU 版。3. 數(shù)據(jù)集準(zhǔn)備與預(yù)處理LFW 和自建數(shù)據(jù)集怎么選3.1 公開數(shù)據(jù)集 vs 自建數(shù)據(jù)集LFW 是人臉識(shí)別最常用的公開測(cè)試集13000 多張人臉每張標(biāo)注了姓名。它的好處是標(biāo)準(zhǔn)、可對(duì)比壞處是圖片小、場(chǎng)景單一用它訓(xùn)練出來(lái)的模型在真實(shí)場(chǎng)景下表現(xiàn)一般。我一般建議用 LFW 做預(yù)訓(xùn)練或驗(yàn)證用自建數(shù)據(jù)集做微調(diào)。自建數(shù)據(jù)集就是拿手機(jī)拍每個(gè)人拍 20 到 50 張不同角度、不同光照、不同表情。拍的時(shí)候注意幾點(diǎn)背景不要太雜亂人臉占畫面比例不低于三分之一避免逆光。拍完用 OpenCV 做檢測(cè)和對(duì)齊把人臉區(qū)域裁出來(lái)統(tǒng)一到 160x160 或 112x112。import cv2 import os # 人臉檢測(cè)器這里用 OpenCV 自帶的 DNN 檢測(cè)器 net cv2.dnn.readNetFromCaffe( deploy.prototxt, res10_300x300_ssd_iter_140000.caffemodel ) def extract_faces(img_path, output_dir, size(160, 160)): 檢測(cè)并裁剪人臉統(tǒng)一尺寸后保存 img cv2.imread(img_path) if img is None: return 0 h, w img.shape[:2] blob cv2.dnn.blobFromImage( cv2.resize(img, (300, 300)), 1.0, (300, 300), (104.0, 177.0, 123.0) ) net.setInput(blob) detections net.forward() count 0 for i in range(detections.shape[2]): confidence detections[0, 0, i, 2] if confidence 0.5: # 置信度閾值 box detections[0, 0, i, 3:7] * [w, h, w, h] x1, y1, x2, y2 box.astype(int) # 邊界保護(hù)防止越界 x1, y1 max(0, x1), max(0, y1) x2, y2 min(w, x2), min(h, y2) face img[y1:y2, x1:x2] if face.size 0: continue face cv2.resize(face, size) name os.path.basename(img_path).split(.)[0] cv2.imwrite(f{output_dir}/{name}_{count}.jpg, face) count 1 return count這段代碼的邏輯是先用 DNN 檢測(cè)器找到人臉位置置信度低于 0.5 的直接丟棄然后裁剪、縮放、保存。置信度閾值這個(gè)參數(shù)很關(guān)鍵調(diào)高會(huì)漏掉側(cè)臉調(diào)低會(huì)引入誤檢。我一般從 0.5 開始試根據(jù)實(shí)際圖片質(zhì)量上下浮動(dòng)。size參數(shù)要和后面模型的輸入尺寸對(duì)齊FaceNet 用 160x160ArcFace 常用 112x112。3.2 數(shù)據(jù)增強(qiáng)讓模型見過(guò)更多“意外”自建數(shù)據(jù)集最大的問(wèn)題是樣本少幾十張照片訓(xùn)練出來(lái)的模型泛化能力差。數(shù)據(jù)增強(qiáng)就是人工制造變化隨機(jī)翻轉(zhuǎn)、隨機(jī)裁剪、亮度調(diào)整、加噪聲。torchvision 的 transforms 模塊可以一行搞定。from torchvision import transforms train_transform transforms.Compose([ transforms.RandomHorizontalFlip(p0.5), # 50% 概率水平翻轉(zhuǎn) transforms.RandomRotation(degrees15), # 隨機(jī)旋轉(zhuǎn) ±15 度 transforms.ColorJitter( brightness0.3, contrast0.3, saturation0.3 ), # 顏色抖動(dòng) transforms.RandomResizedCrop(160, scale(0.8, 1.0)), # 隨機(jī)裁剪 transforms.ToTensor(), transforms.Normalize(mean[0.5, 0.5, 0.5], std[0.5, 0.5, 0.5]) ])翻轉(zhuǎn)概率設(shè) 0.5 是經(jīng)驗(yàn)值太高會(huì)讓模型學(xué)到不自然的對(duì)稱性。旋轉(zhuǎn)角度不要超過(guò) 20 度人臉識(shí)別里大角度旋轉(zhuǎn)反而引入噪聲。RandomResizedCrop的 scale 參數(shù)控制裁剪區(qū)域占原圖的比例0.8 到 1.0 表示至少保留 80% 的人臉區(qū)域。歸一化用 0.5 均值和標(biāo)準(zhǔn)差是 FaceNet 的慣例如果你換別的預(yù)訓(xùn)練模型要查對(duì)應(yīng)的歸一化參數(shù)。4. 模型訓(xùn)練與調(diào)參從損失函數(shù)到學(xué)習(xí)率策略4.1 損失函數(shù)選型Softmax、Triplet 還是 ArcFace這是答辯時(shí)最容易被問(wèn)到的點(diǎn)。Softmax 是最基礎(chǔ)的分類損失把每個(gè)人當(dāng)一類訓(xùn)練簡(jiǎn)單但類間距離不夠大。Triplet Loss 是 FaceNet 的核心每次取三張圖——錨點(diǎn)、正樣本、負(fù)樣本讓正樣本靠近錨點(diǎn)、負(fù)樣本遠(yuǎn)離。它的缺點(diǎn)是訓(xùn)練不穩(wěn)定需要精心構(gòu)造三元組。ArcFace 在 Softmax 基礎(chǔ)上加了角度間隔讓同類更緊湊、異類更分散目前是人臉識(shí)別的主流選擇。對(duì)于畢設(shè)我的建議是如果只是做幾個(gè)人的人臉分類Softmax 夠用如果要做“判斷是不是同一個(gè)人”的驗(yàn)證任務(wù)用 ArcFace 或 Triplet。下面給一個(gè)基于預(yù)訓(xùn)練 FaceNet 做微調(diào)的例子。import torch import torch.nn as nn from facenet_pytorch import InceptionResnetV1 # 加載預(yù)訓(xùn)練模型classifyFalse 表示不加載最后的分類層 model InceptionResnetV1(pretrainedvggface2, classifyFalse) # 凍結(jié)前面的卷積層只訓(xùn)練最后的全連接層 for param in model.parameters(): param.requires_grad False # 替換最后的分類層num_classes 是你的類別數(shù) num_classes 10 model.logits nn.Linear(512, num_classes) # 只讓新加的分類層參與訓(xùn)練 for param in model.logits.parameters(): param.requires_grad True # 損失函數(shù)和優(yōu)化器 criterion nn.CrossEntropyLoss() optimizer torch.optim.Adam( filter(lambda p: p.requires_grad, model.parameters()), lr1e-3 )凍結(jié)卷積層的邏輯是預(yù)訓(xùn)練模型在 VGGFace2 上已經(jīng)學(xué)到了通用的人臉特征你只需要教它認(rèn)識(shí)你的幾個(gè)人。這樣訓(xùn)練快、不容易過(guò)擬合。lr1e-3是 Adam 的常用初始學(xué)習(xí)率如果 loss 震蕩就降到 1e-4。filter(lambda p: p.requires_grad, ...)這行確保優(yōu)化器只更新需要訓(xùn)練的參數(shù)不加這個(gè)會(huì)把凍結(jié)的層也傳進(jìn)去浪費(fèi)顯存。4.2 訓(xùn)練循環(huán)與驗(yàn)證什么時(shí)候該停訓(xùn)練循環(huán)本身不復(fù)雜關(guān)鍵是加驗(yàn)證集和早停。每訓(xùn)練完一個(gè) epoch在驗(yàn)證集上算準(zhǔn)確率如果連續(xù) 5 個(gè) epoch 沒提升就停。def train_one_epoch(model, loader, criterion, optimizer, device): model.train() total_loss, correct, total 0, 0, 0 for imgs, labels in loader: imgs, labels imgs.to(device), labels.to(device) optimizer.zero_grad() outputs model(imgs) loss criterion(outputs, labels) loss.backward() optimizer.step() total_loss loss.item() _, predicted outputs.max(1) correct predicted.eq(labels).sum().item() total labels.size(0) return total_loss / len(loader), correct / total # 早停邏輯 best_acc 0 patience 5 counter 0 for epoch in range(50): train_loss, train_acc train_one_epoch( model, train_loader, criterion, optimizer, device ) val_loss, val_acc evaluate(model, val_loader, criterion, device) print(fEpoch {epoch}: train_acc{train_acc:.4f}, val_acc{val_acc:.4f}) if val_acc best_acc: best_acc val_acc torch.save(model.state_dict(), best_model.pth) counter 0 else: counter 1 if counter patience: print(fEarly stop at epoch {epoch}) breakpatience5是耐心值表示驗(yàn)證集準(zhǔn)確率連續(xù) 5 輪不提升就停。這個(gè)值設(shè)太小容易停早了設(shè)太大浪費(fèi)時(shí)間。torch.save只保存效果最好的那組權(quán)重答辯演示時(shí)加載這個(gè)文件就行。注意model.train()和model.eval()的切換Dropout 和 BatchNorm 在兩種模式下行為不同忘了切會(huì)導(dǎo)致驗(yàn)證結(jié)果不穩(wěn)定。4.3 學(xué)習(xí)率調(diào)度一個(gè)容易被忽略的提分點(diǎn)固定學(xué)習(xí)率往往不是最優(yōu)的。前期用大學(xué)習(xí)率快速下降后期用小學(xué)習(xí)率精細(xì)調(diào)整。PyTorch 提供了現(xiàn)成的調(diào)度器。from torch.optim.lr_scheduler import CosineAnnealingLR scheduler CosineAnnealingLR(optimizer, T_max50, eta_min1e-6) # 在每個(gè) epoch 結(jié)束后調(diào)用 for epoch in range(50): train_loss, train_acc train_one_epoch(...) val_loss, val_acc evaluate(...) scheduler.step() # 更新學(xué)習(xí)率CosineAnnealingLR讓學(xué)習(xí)率按余弦曲線從初始值降到eta_min。T_max是總 epoch 數(shù)要和你的訓(xùn)練輪數(shù)匹配。這個(gè)策略在訓(xùn)練后期能讓模型更穩(wěn)定地收斂我實(shí)測(cè)比固定學(xué)習(xí)率能提 1 到 2 個(gè)百分點(diǎn)。5. 避坑與排查那些讓系統(tǒng)跑不起來(lái)的常見問(wèn)題5.1 現(xiàn)象訓(xùn)練 loss 不下降一直卡在 2.3 左右原因通常是數(shù)據(jù)標(biāo)簽沒對(duì)齊或者輸入沒歸一化。檢查你的 Dataset 類里圖片和標(biāo)簽是不是一一對(duì)應(yīng)打印幾張看看。另一個(gè)可能是學(xué)習(xí)率太大梯度爆炸了。解決方法是先把學(xué)習(xí)率降到 1e-4 試一輪如果 loss 開始動(dòng)說(shuō)明是學(xué)習(xí)率問(wèn)題。同時(shí)確認(rèn)transforms.Normalize的均值和標(biāo)準(zhǔn)差和預(yù)訓(xùn)練模型匹配。5.2 現(xiàn)象驗(yàn)證集準(zhǔn)確率很高但實(shí)際用攝像頭測(cè)試全錯(cuò)這是典型的過(guò)擬合加數(shù)據(jù)分布不一致。訓(xùn)練集和驗(yàn)證集來(lái)自同一批照片模型只是記住了這些人沒學(xué)到泛化特征。解決辦法是增加數(shù)據(jù)增強(qiáng)的強(qiáng)度或者引入 LFW 等外部數(shù)據(jù)做驗(yàn)證。攝像頭測(cè)試時(shí)注意光照和角度如果訓(xùn)練集全是正面光照均勻的照片實(shí)際場(chǎng)景稍微暗一點(diǎn)就崩。我一般會(huì)在自建數(shù)據(jù)集里刻意加入不同光照條件的照片。5.3 現(xiàn)象推理速度太慢一幀要好幾秒先確認(rèn)是不是在用 CPU 推理。如果有 GPU檢查model.to(device)和輸入張量的.to(device)有沒有漏。如果只有 CPU考慮換 MobileNet 做 backbone或者用 ONNX Runtime 加速。另一個(gè)常見原因是沒加torch.no_grad()推理時(shí)還在算梯度白白浪費(fèi)算力。model.eval() with torch.no_grad(): # 關(guān)鍵關(guān)閉梯度計(jì)算 embedding model(img_tensor)5.4 現(xiàn)象OpenCV 讀中文路徑的圖片返回 NoneOpenCV 的imread不支持中文路徑這是血淚經(jīng)驗(yàn)。解決辦法是用cv2.imdecode配合np.fromfile。import numpy as np def cv_imread(file_path): 支持中文路徑的圖片讀取 img cv2.imdecode( np.fromfile(file_path, dtypenp.uint8), cv2.IMREAD_COLOR ) return img5.5 現(xiàn)象換一臺(tái)電腦跑報(bào) CUDA out of memory顯存不夠。降低 batch_size 是最直接的辦法從 32 降到 16 甚至 8。如果還不行檢查是不是忘了torch.cuda.empty_cache()或者在驗(yàn)證階段沒加torch.no_grad()導(dǎo)致顯存累積。另外如果用了預(yù)訓(xùn)練模型確認(rèn)加載方式不會(huì)重復(fù)占用顯存。6. 從能跑到能答辯驗(yàn)證指標(biāo)與演示技巧6.1 用 ROC 曲線和閾值選型說(shuō)清楚系統(tǒng)邊界答辯時(shí)老師最想看到的是你知道系統(tǒng)的邊界在哪。準(zhǔn)確率這個(gè)指標(biāo)太粗糙人臉識(shí)別驗(yàn)證任務(wù)應(yīng)該用 ROC 曲線和 AUC 值。ROC 曲線的橫軸是誤識(shí)率縱軸是通過(guò)率曲線越靠近左上角越好。AUC 是曲線下面積0.5 是隨機(jī)猜1.0 是完美。from sklearn.metrics import roc_curve, auc import matplotlib.pyplot as plt # scores 是模型輸出的相似度分?jǐn)?shù)labels 是真實(shí)標(biāo)簽1 同一人0 不同人 fpr, tpr, thresholds roc_curve(labels, scores) roc_auc auc(fpr, tpr) plt.figure() plt.plot(fpr, tpr, labelfROC curve (AUC {roc_auc:.3f})) plt.plot([0, 1], [0, 1], k--) # 對(duì)角線 plt.xlabel(False Positive Rate) plt.ylabel(True Positive Rate) plt.legend() plt.savefig(roc_curve.png, dpi300)有了 ROC 曲線你可以選一個(gè)合適的閾值。比如要求誤識(shí)率低于 1%就在曲線上找到對(duì)應(yīng)點(diǎn)讀出閾值。這個(gè)閾值就是你系統(tǒng)實(shí)際部署時(shí)用的判斷標(biāo)準(zhǔn)。答辯時(shí)把這張圖放出來(lái)再解釋“我選擇閾值 0.6此時(shí)誤識(shí)率 0.8%通過(guò)率 96%”比單純說(shuō)“準(zhǔn)確率 98%”有說(shuō)服力得多。6.2 演示系統(tǒng)的三個(gè)實(shí)用技巧第一個(gè)技巧準(zhǔn)備一個(gè)“兜底”視頻。答辯現(xiàn)場(chǎng)攝像頭可能出問(wèn)題提前錄一段演示視頻包含識(shí)別成功和識(shí)別失敗的案例。失敗的案例反而加分說(shuō)明你知道系統(tǒng)不是萬(wàn)能的。第二個(gè)技巧把訓(xùn)練過(guò)程的 loss 曲線和準(zhǔn)確率曲線截圖放進(jìn) PPT。老師看到你調(diào)過(guò)參、試過(guò)不同配置印象分直接拉滿。第三個(gè)技巧準(zhǔn)備一張對(duì)比表列出不同 backbone、不同損失函數(shù)、不同學(xué)習(xí)率下的實(shí)驗(yàn)結(jié)果。哪怕只做了三組對(duì)比也能體現(xiàn)你有實(shí)驗(yàn)設(shè)計(jì)的意識(shí)。配置Backbone損失函數(shù)學(xué)習(xí)率驗(yàn)證集準(zhǔn)確率AMobileNetSoftmax1e-392.3%BResNet50Softmax1e-395.1%CResNet50ArcFace1e-497.8%這張表不用很復(fù)雜關(guān)鍵是讓老師看到變量控制。我一般會(huì)建議學(xué)生至少跑三組每組只改一個(gè)參數(shù)這樣結(jié)論才站得住。6.3 文檔說(shuō)明怎么寫才不被挑刺畢業(yè)設(shè)計(jì)的文檔說(shuō)明不是代碼注釋的堆砌。老師看的是你的設(shè)計(jì)思路和決策過(guò)程。結(jié)構(gòu)上按“需求分析 → 方案選型 → 詳細(xì)設(shè)計(jì) → 實(shí)驗(yàn)驗(yàn)證 → 結(jié)論”走。每個(gè)技術(shù)選擇都要寫清楚“為什么選 A 不選 B”比如“選擇 MTCNN 而非 Haar 是因?yàn)榍罢邔?duì)側(cè)臉和遮擋的魯棒性更好實(shí)測(cè)在自建數(shù)據(jù)集上檢測(cè)率高 12 個(gè)百分點(diǎn)”。代碼說(shuō)明部分不要貼大段源碼用流程圖加關(guān)鍵代碼片段的方式。每段代碼配一句“這段代碼解決了什么問(wèn)題”。文檔里的圖表要編號(hào)正文里引用“如圖 3-2 所示”。這些細(xì)節(jié)看起來(lái)瑣碎但答辯老師翻文檔時(shí)就是看這些。最后說(shuō)一個(gè)我自己的習(xí)慣每次訓(xùn)練完把超參數(shù)、數(shù)據(jù)集版本、代碼 commit 號(hào)記在一個(gè)文本文件里。過(guò)兩周回頭調(diào)參時(shí)你絕對(duì)記不住當(dāng)時(shí)用的什么配置。這個(gè)習(xí)慣幫我省了無(wú)數(shù)次后悔藥。希望幫到你。本文還有配套的精品資源點(diǎn)擊獲取