
簡介面向深度學習算法訓練的人臉性別檢測與分類數據集涵蓋woman、man兩類共300張真實手機采集的高質量人臉圖片均已人工分類標注適合人臉檢測、性別特征提取與分類模型的訓練及評估。資源包共505個文件、約339.41MB包含Python訓練腳本、模型配置文件config/pbtxt、TensorFlow模型權重pb/checkpoint/meta、圖像樣本jpg/png、標注文件xml/txt/record以及說明文檔md/ipynb等可支撐從數據加載、模型訓練到推理部署的完整流程。數據集圖片源自真實拍攝環境光線、角度、表情多樣性較好有助于提升性別分類模型的泛化能力配套腳本與配置還能用于目標檢測、人流統計等擴展任務。目前已有58人學習下載適合從事計算機視覺研究的學生、算法工程師以及需要快速獲取規范訓練數據的開發者使用。1. 300張人臉圖做性別分類為什么夠用拿到一份標注好 woman/man 兩類的人臉性別檢測分類數據集第一反應通常是300 張圖夠深度學習算法用嗎我一開始也懷疑直到用預訓練模型做遷移學習試了一輪才確認這個體量下決定泛化能力的不是絕對數據量而是分布一致性。數據全部來自真實手機采集光線、角度、遮擋都帶著日常場景的噪聲這類“臟而真實”的樣本對微調的幫助比網上爬來的干凈人頭大得多。真正值得花時間的路徑是先驗證數據質量再基于 ResNet 系模型微調性別分類器然后把檢測和分類串成完整管線最后落到人流量統計這類應用上。下文從目錄結構講起到硬樣本挖掘收尾每一步都有可直接運行的代碼。2. 數據體檢目錄結構、標注完整性與類別平衡2.1 目錄結構與標簽組織這類人臉性別檢測分類數據集常見組織方式是分成 train/val 兩個根目錄內部再按 woman/man 分子目錄。目錄名就是標簽訓練時借助 torchvision 的 ImageFolder 直接讀取即可不需要額外解析標注文件。拿到資源后先列出完整目錄樹確認沒有多余的隱藏文件或嵌套文件夾避免 DataLoader 把非圖片文件也掃進來。find . -type f | sed s|[^/]*/| |g上面命令把路徑縮進打印一眼能看出目錄的嵌套層級。如果出現 .DS_Store、Thumbs.db 這類系統文件訓練前統一刪掉。2.2 標注體檢文件完整性、重復圖片與類別比例標題寫“已做分類劃分標注”這一點必須先驗證再信。手機采集的圖片經常出現導出不全、EXIF 旋轉信息丟失、同一張圖被重復拷貝等問題它們不會直接報錯但會悄悄拖低分類準確率。下面的腳本遍歷兩個子目錄統計數量、計算 MD5、并嘗試用 PIL 打開每張圖from pathlib import Path from PIL import Image from collections import Counter import hashlib data_root Path(gender_dataset/train) counts Counter() broken [] seen {} for label_dir in [woman, man]: for img_path in (data_root / label_dir).glob(*.jpg): counts[label_dir] 1 digest hashlib.md5(img_path.read_bytes()).hexdigest() if digest in seen: print(f重復文件: {img_path} 與 {seen[digest]} 相同) else: seen[digest] str(img_path) try: with Image.open(img_path) as im: im.load() except Exception as e: broken.append((str(img_path), e)) print(類別分布:, dict(counts)) print(損壞文件:, broken)邏輯說明MD5 比對只針對字節完全一致的副本能抓出重復采集PIL 的 load 會真正解碼像素數據能暴露擴展名偽裝成 jpg 的損壞文件。運行后注意兩個指標——兩類的圖片數量差最好別超過 15%這是類別不平衡的簡單判據損壞文件出現一條就要排查原始采集批次。結合性別分類任務我一般還會記錄每張圖的寬高分布用于決定 resize 策略。手機相機默認拍 3000×4000 左右的大圖原始分辨率差異不會影響最終訓練但 EXIF 旋轉標記會導致同一張人臉被轉成橫豎兩種方向建議在預處理階段按 orientation 字段修正后再使用。2.3 訓練集與驗證集的劃分方式300 張樣本不建議隨機切分完事。性別分類的難點經常集中在特定年齡段和特定姿態上隨機劃分可能讓老人、側臉樣本全部掉進訓練集或驗證集造成評估虛高或虛低。比較穩的做法是先按人分組——同一張照片里的多張臉不拆散再按 7:1.5:1.5 劃分成 train/val/test。分組后驗證集至少保留 40 張以上否則單次評估的置信區間太寬90% 和 95% 的準確率差異無法分辨。寫個按文件名前綴分組的腳本是值得的import random from pathlib import Path from collections import defaultdict groups defaultdict(list) for p in Path(gender_dataset/train).rglob(*.jpg): person_id p.name.split(_)[0] # 按命名前綴模擬人員分組 groups[person_id].append(p) all_persons list(groups.keys()) random.seed(42) random.shuffle(all_persons) n len(all_persons) train_ids set(all_persons[:int(n*0.7)]) val_ids set(all_persons[int(n*0.7):int(n*0.85)])按人員分組的意義在于防止同一個人臉的不同幀同時出現在訓練和驗證里模型一旦記住的是“這張臉”而不是“性別特征”分數再高也失去遷移意義。劃分完成后把路徑列表寫成 csv方便后續恢復到同一實驗環境。檢查項手段判定標準文件可解碼PIL open load無異常重復圖片MD5 哈希比對無重復類別平衡統計兩子目錄數量差值不超過 15%分組不泄漏按 person_id 劃分val/test 不出現訓練身份3. 用 ResNet18 微調性別分類模型3.1 預訓練權重加載與分類頭替換分類數據集只有 300 張從零訓練 CNN 必然過擬合。常見做法是加載在 ImageNet 上預訓練過的 ResNet18凍結前面幾層只微調最后幾個殘差塊和分類頭。性別識別依賴的眉眼、下頜線等紋理屬于中高層特征網絡前幾層學到的邊緣和顏色結構不需要大改。import torch import torch.nn as nn from torchvision import models model models.resnet18(pretrainedTrue) # 替換最后一層全連接原輸出1000類這里映射到2類 model.fc nn.Linear(model.fc.in_features, 2) # 凍結前三個殘差層只訓練 layer4 和 fc for name, param in model.named_parameters(): if not name.startswith(layer4) and not name.startswith(fc): param.requires_grad False參數說明pretrainedTrue拉取的是 ImageNet 權重之后的微調相當于把“貓狗分類”里學到的通用特征遷移到性別判斷上model.fc.in_features動態讀取原全連接輸入維度避免手工寫死 512換成 ResNet50 時這段代碼也不需改。凍結前三個殘差層的理由是 ImageNet 預訓練模型底層對紋理、邊緣的表達已經足夠通用在 300 張小數據集上繼續調整反而容易過擬合到訓練集的光線分布上。3.1.1 數據增強策略小數據集必須配合強增強。除了常規的隨機水平翻轉和隨機旋轉我建議加上 ColorJitter 和 RandomErasing。手機照片存在大量室內暖光、夜間閃光燈、逆光背光場景ColorJitter 能模擬不同白平衡下的膚色偏移RandomErasing 模擬發絲遮擋和眼鏡反光——這兩類噪聲在手機人臉里非常常見。from torchvision import transforms train_transform transforms.Compose([ transforms.Resize((256, 256)), transforms.RandomResizedCrop(224, scale(0.7, 1.0)), transforms.RandomHorizontalFlip(p0.5), transforms.RandomRotation(degrees10), transforms.ColorJitter(brightness0.3, contrast0.3, saturation0.2), transforms.RandomErasing(p0.3, scale(0.02, 0.15)), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]), ]) val_transform transforms.Compose([ transforms.Resize((224, 224)), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]), ])參數說明RandomResizedCrop 的 scale 下限 0.7 保證裁剪后仍然保留大部分人臉區域太小的裁剪會切掉眼睛或下巴放大到 224 后已經丟失判別信息RandomErasing 的 scale 控制在 0.02~0.15 之間遮擋面積超過 15% 就不像真實發絲或反光了。Normalize 用的是 ImageNet 統計量因為預訓練權重就是在這個分布下學出來的換用自定義 mean/std 會導致微調初期 loss 劇烈震蕩。3.2 訓練循環與超參配置300 張訓練集、batch size 取 16 比較合適再小的話 BatchNorm 的統計量不穩定再大則每個 epoch 參數更新次數太少。優化器我用 SGD 加 momentum原因在于遷移學習微調階段 SGD 對學習率的衰減更敏感配合 cosine 退火能在小數據上收斂到更平緩的極小值。import torch.optim as optim from torch.utils.data import DataLoader from torchvision.datasets import ImageFolder train_loader DataLoader( ImageFolder(gender_dataset/train, train_transform), batch_size16, shuffleTrue, num_workers4, drop_lastTrue) val_loader DataLoader( ImageFolder(gender_dataset/val, val_transform), batch_size32, shuffleFalse, num_workers4) optimizer optim.SGD(filter(lambda p: p.requires_grad, model.parameters()), lr1e-3, momentum0.9, weight_decay5e-4) scheduler optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max30) criterion nn.CrossEntropyLoss() best_acc 0.0 for epoch in range(30): model.train() running_loss 0.0 for images, labels in train_loader: optimizer.zero_grad() outputs model(images) loss criterion(outputs, labels) loss.backward() optimizer.step() running_loss loss.item() * images.size(0) scheduler.step() # 每個 epoch 結束做一次驗證 model.eval() correct 0 total 0 with torch.no_grad(): for images, labels in val_loader: preds model(images).argmax(dim1) correct (preds labels).sum().item() total labels.size(0) acc correct / total if acc best_acc: best_acc acc torch.save(model.state_dict(), gender_resnet18.pt) print(fepoch{epoch} loss{running_loss/len(train_loader.dataset):.4f} val_acc{acc:.4f})邏輯說明每個 epoch 清空梯度、前向、反向、更新、調度器退火驗證集不參與反向傳播只統計 argmax 后的類別是否與標簽一致。filter(lambda p: p.requires_grad)保證凍結層不會出現在 optimizer 里否則 weight_decay 依然會對凍結參數產生不必要的衰減。CosineAnnealing 在 30 個 epoch 內把學習率從 1e-3 平滑降到接近 0相比固定學習率省去了手動找下降時機的麻煩。訓練時盯著 val loss 而不只看 val acc。300 張圖上準確率只有 5% 的波動都很正常但 loss 的變化更早暴露擬合趨勢。如果 train loss 持續下降而 val acc 停滯優先考慮增強強度不夠而不是增加模型容量。3.3 評估混淆矩陣與類別獨立指標整體準確率在男女比例不平衡時沒有參考價值。驗證集如果 woman 占 60%模型全猜 woman 也有 60%跟蹤 Precision/Recall 才能看出哪一類出錯。性別分類的典型錯誤方向是“男性被錯判為女性尤其是長發年輕男性”因此重點看 man 類的 recall 和 woman 類的 precision。from sklearn.metrics import confusion_matrix, classification_report y_true, y_pred [], [] model.eval() with torch.no_grad(): for images, labels in val_loader: y_true.extend(labels.tolist()) y_pred.extend(model(images).argmax(dim1).tolist()) print(classification_report(y_true, y_pred, target_names[woman, man])) print(confusion_matrix(y_true, y_pred))運行后會輸出每類的 precision、recall、f1-score。當 man 類 recall 明顯低于 woman 類時說明分類器把不少男性認成了女性。接下來要做的不一定是加數據而是回到數據增強給 man 類的訓練樣本提高 RandomRotation 的度數范圍或者對眉骨、下頜區域做隨機裁剪放大。4. 把檢測和分類串成完整管線4.1 config 文件與檢測模型的對應關系項目包里出現的一串配置文件像 facessd_mobilenet_v2_quantized_320x320_open_image_v4.config、ssd_mobilenet_v2_quantized_300x300_coco.config是 TensorFlow Object Detection API 的 pipeline 配置文件名把骨干網絡、輸入分辨率和訓練數據集都寫清楚了。facessd 這組在 Open Images 上專門針對人臉做了微調適合直接作為性別分類前的人臉檢測器后面幾個是通用目標檢測基線用途是驗證檢測模塊能否被整體替換。配置文件輸入分辨率骨干網絡適用任務facessd_mobilenet_v2_quantized_320x320_open_image_v4.config320×320MobileNetV2人臉檢測ssd_mobilenet_v2_quantized_300x300_coco.config300×300MobileNetV2COCO 通用目標ssdlite_mobilenet_v2_coco.config320×320MobileNetV2輕量級部署ssd_mobilenet_v1_coco.config300×300MobileNetV1CPU 推理理解這些配置的意義在于推理階段的輸入尺寸必須與訓練一致。config 里寫 320×320推理時 blobFromImage 就 resize 到 320否則檢測框位置和置信度都會漂移。4.2 用 OpenCV DNN 加載檢測器并接上分類模型有配套 checkpoint 時最快的方式是用 TensorFlow Object Detection API 導出 frozen_inference_graph.pb再由 OpenCV 的 DNN 模塊加載。省去 TensorFlow 依賴的同時保留 SSD 的檢測能力。完整管線分四步讀圖、SSD 出框、裁臉、ResNet18 分類按幀循環即可處理一組手機照片。import cv2 import torch from torchvision import transforms from PIL import Image import numpy as np # 1. 人頭檢測SSD MobileNet從 pb 加載 det_net cv2.dnn.readNetFromTensorflow(frozen_inference_graph.pb) # 2. 性別分類上一章保存的 ResNet18 cls_model models.resnet18(num_classes2) cls_model.load_state_dict(torch.load(gender_resnet18.pt)) cls_model.eval() def inference(img_bgr, conf_thresh0.6): h, w img_bgr.shape[:2] blob cv2.dnn.blobFromImage(img_bgr, 1.0, (320, 320), (127.5, 127.5, 127.5), swapRBTrue) det_net.setInput(blob) detections det_net.forward() # shape: [1,1,N,7] results [] for i in range(detections.shape[2]): confidence detections[0, 0, i, 2] if confidence conf_thresh: continue x1 int(detections[0, 0, i, 3] * w) y1 int(detections[0, 0, i, 4] * h) x2 int(detections[0, 0, i, 5] * w) y2 int(detections[0, 0, i, 6] * h) x1, y1 max(0, x1), max(0, y1) x2, y2 min(w, x2), min(h, y2) face img_bgr[y1:y2, x1:x2] if face.size 0: continue face_rgb cv2.cvtColor(face, cv2.COLOR_BGR2RGB) face_pil Image.fromarray(face_rgb).resize((224, 224)) tensor val_transform(face_pil).unsqueeze(0) with torch.no_grad(): logits cls_model(tensor) gender woman if logits.argmax(1).item() 0 else man prob torch.softmax(logits, dim1).max().item() results.append((x1, y1, x2, y2, gender, prob)) return results參數說明blobFromImage 里的(127.5,127.5,127.5)對應 SSD MobileNet 的均值縮放換用 COCO 版檢測器時該項不變但換用別的網絡要查它的預處理定義detections 每個候選框的第 3 號元素是置信度4~7 號元素是歸一化后的左上角、右下角坐標乘回原圖寬高才得到像素坐標。分類前裁剪人臉區域是最容易出錯的一步——如果直接把整張圖丟給分類器背景中的長發、衣領顏色會主導預測結果性別分類就退化成“發型分類”了。這里建議框邊適當外擴 5% 左右把下頜和鬢角包含進來避免檢測框貼臉太緊丟掉判別區域。4.3 置信度閾值與多框去重SSD 在側臉、低頭場景中可能出現一個頭給出多個重疊框。簡單閾值的寫法在手機照片上往往誤報較多實際項目里大多會再加一步 NMS。OpenCV 的 NMSBoxes 接收的是像素框和置信度在把結果 append 進 results 前做過濾boxes [] confidences [] for i in range(detections.shape[2]): confidence detections[0, 0, i, 2] if confidence 0.5: # 先放寬閾值交給 NMS 篩 continue x1 int(detections[0,0,i,3]*w) y1 int(detections[0,0,i,4]*h) x2 int(detections[0,0,i,5]*w) y2 int(detections[0,0,i,6]*h) boxes.append([x1, y1, x2, y2]) confidences.append(float(confidence)) idx cv2.dnn.NMSBoxes(boxes, confidences, score_threshold0.5, nms_threshold0.4)NMS 參數里 score_threshold 用 0.5nms_threshold 用 0.4 是一個比較通用的起點。nms_threshold 越大保留的框越多適合多人密集場景越小越容易把同一張臉的重復框合并干凈代價是兩個人挨得太近時可能漏掉其中一個。先放寬閾值再交給 NMS比只靠一個高置信度閾值更穩因為 NMS 會保留最高置信度的框而不是簡單丟棄所有低分框。提示檢測器輸出的人臉框如果出現負坐標一定要做max(0, x1)這類裁剪否則后續 PIL 的 crop 直接拋異常中斷整個推理管線。5. 進階性別比例統計與硬樣本挖掘5.1 從逐幀檢測到人群計數有了第 4 章的管線可以進一步做性別比例統計這也正是“人流統計”的簡化落法。跟 UCF101 那種視頻動作分類關注時序語義不同這里只關心畫面中出現的人和他們的性別不需要建模具體動作。最簡單的做法是按幀統計檢測到的人臉數再用滑窗中位數消掉檢測器單幀跳變from collections import deque frame_history deque(maxlen15) gender_counter {woman: 0, man: 0} def process_frame(frame): dets inference(frame, conf_thresh0.6) counts {woman: 0, man: 0} for x1, y1, x2, y2, gender, prob in dets: counts[gender] 1 for k in counts: gender_counter[k] counts[k] frame_history.append(len(dets)) return int(np.median(frame_history)) # 穩定的人數估計中位數濾波對 15 幀窗口的離群值不敏感一個人突然檢測失敗一幀不會讓計數沖高回落。正式做人群計數時再把幀級統計升級成卡爾曼濾波跟蹤軌跡這里的數據集規模更適合先把檢測和分類準確率做實。5.2 硬樣本挖掘用錯題集反向提升準確率當整體準確率達到 90% 以上后光調學習率收益很小。可以把驗證集里預測錯誤的樣本單獨導出觀察共性——這類手機采集數據里比較常見的錯誤是光線很暗的室內側臉以及戴深色墨鏡的樣本。把這些難例像下面這樣找出來hard_samples [] model.eval() with torch.no_grad(): for img_path, label in val_samples: img load_image(img_path) pred model(img.unsqueeze(0)).argmax(1).item() if pred ! label: hard_samples.append(img_path) for i, p in enumerate(hard_samples): shutil.copy(p, fdebug/hard_{i}_{Path(p).stem}.jpg)人工看過難例后把其中 10~20 張加入訓練集并配上更強的遮擋增強比盲目加預訓練數據更有效。錯題集的分布往往能看出數據采集的盲區比如漏檢的全部是逆光場景那就回到第 4 章調檢測器的預處理而不是動分類模型。5.3 常見失敗模式與參數調整失敗現象可能原因調整方向長發男性判為 woman發型特征主導模型沒看眉骨下頜訓練時對 man 類做局部裁剪增強強化面部結構側臉漏檢檢測器對夸張角度召回不足置信度閾值從 0.6 降到 0.45并加入 NMS 去重膚色偏色導致誤判室內暖光/閃光燈白平衡漂移ColorJitter 的 brightness 上限提到 0.4重跑微調同一個人重復計數檢測框在相鄰幀抖動用 5.1 的中位濾波或升級為按檢測框 IoU 的軌跡匹配一次只改一個參數。最常見的問題是同時調低了置信度、又加了增強、又換了優化器出了 bug 也不知道是誰的問題。收斂不理想時先把增強降到最低驗證模型能擬合訓練集再逐項加回來。硬樣本挖掘后訓練時把 cosine 周期換成階梯下降、每 5 個 epoch 學習率減半在難例二次微調里比 cosine 更容易穩定因為難例集太小平滑退火的收益有限。盯著 val loss 找到學習率下降點通常在躍升前兩個 epoch 出現截斷訓練能省下不少調參時間。訓練時盯著 val loss 而不是 train loss收斂不明顯就把 lr 除以 5 再跑 15 輪這個策略在任何小數據集微調里都通用。本文還有配套的精品資源點擊獲取