简介:这份资源面向计算机视觉方向的科研人员、算法工程师与深度学习入门者,提供一套可直接用于性别检测与分类训练的人脸数据集。数据源自真实手机采集,共300张高质量人脸图片,按woman与man两个子集完成分类与标注,可用于人脸检测、特征提取及性别分类等算法实验,也适合作为课程设计与模型验证的素材。压缩包共505个文件,约339.41MB,包含317个Python脚本、48个配置文件、29个proto定义及若干模型权重与配置,覆盖SSD、MTCNN等检测框架的部署与训练流程,另有少量图片、视频与记录文件辅助验证。已有59人学习下载。借助该资源,读者可快速搭建性别分类流水线,结合Faster R-CNN、Mean-shift与卡尔曼滤波实现人流统计,并参考现成配置与脚本完成模型微调与效果对比,节省数据采集与标注成本。
1. 300 张手机人脸图做性别分类:这个数据集到底能不能打
拿到一个「300 张、woman/man 二分类、手机采集、已划分标注」的人脸性别数据集,第一反应往往是嫌小。我一开始也这么想,毕竟公开人脸集动辄几十万张。但真正在端侧和业务场景里跑过几轮之后,结论会反过来:小样本、真实手机采集、已做分类划分这三个属性凑在一起,恰恰是很多团队缺的那块拼图。公开大数据集大多是棚拍、单反、白平衡统一,模型在实验室刷到 99%,一上真实手机前置摄像头就翻车,玄学得很。
这个标题讲的就是这么一件事:用一份 300 张量级、已经分好 woman 和 man 两类、带标注划分的真实手机人脸图,去跑通一个深度学习性别分类的最小闭环。它解决的不是「刷 SOTA」,而是「我手上没有干净可用的真实分布数据,怎么快速验证一条 pipeline 到底通不通」。适合两类人:一类是刚入门深度学习、想找一个能完整走完训练到推理的小数据集练手;另一类是做端侧或业务落地,需要一份贴近真实手机成像的验证集来卡阈值、看边界。300 张不多,但足够让你把数据管线、增强策略、过拟合判断这些真正决定成败的环节全部踩一遍。
2. 先想清楚:300 张图为什么还能训,以及怎么划分才不骗自己
2.1 小样本性别分类的可行性边界在哪
性别分类本质是一个二分类任务,相比人脸识别、表情识别,它的类内差异小、类间差异大——成年男女在面部结构、发际线、胡须、妆容上的统计差异相当稳定。这意味着模型不需要学到特别细的判别特征,一个中等容量的 CNN 就能拿到不错的准确率。300 张图,按 8:1:1 划分大约是 240 训练 / 30 验证 / 30 测试,训练集每类 120 张左右。这个量级用迁移学习完全够用,从头训基本没戏。
关键判断是:你要的是泛化到真实手机场景,还是只在这个数据集上好看。如果是前者,300 张的价值在于它和你的目标分布接近;如果是后者,那随便一个大数据集切 300 张效果更好。我一般会先问自己这个问题,答案决定了后面所有参数怎么设。
迁移学习是这里唯一理性的选择。ImageNet 预训练的 ResNet18、MobileNetV3、EfficientNet-B0 都可以,参数量从 1.2M 到 5M 不等。300 张图微调,冻结主干只训分类头是最稳的起点,等验证集准确率上不去了再解冻最后几个 block。这个顺序别搞反,一上来就全量微调,30 张验证集根本兜不住,loss 曲线会像过山车。
2.2 划分比例与防泄漏:别让同一张脸跨集
小数据集最容易犯的错是随机划分导致数据泄漏。如果同一个人有多张照片,随机分到训练和验证两边,验证准确率会虚高十几个点,你以为模型学会了,其实它记住了这张脸。手机采集的数据集尤其要注意,同一个人可能连拍了好几张。
正确做法是按身份划分,同一身份的所有图片必须落在同一个集合里。如果数据集没有提供身份标签,退而求其次,用感知哈希或人脸 embedding 做去重聚类,把相似度高于阈值的图归为一组再划分。这一步多花半小时,能省掉后面几天的自我怀疑。
import os import random from collections import defaultdict # 假设目录结构: dataset/woman/*.jpg, dataset/man/*.jpg # 若文件名或附带 csv 含身份信息,按身份分组;否则用文件名前缀近似 def group_by_identity(root): groups = defaultdict(list) for cls in ["woman", "man"]: cls_dir = os.path.join(root, cls) for fname in os.listdir(cls_dir): # 这里用文件名前缀作为身份近似,实际应替换为真实身份字段 identity = fname.split("_")[0] groups[(cls, identity)].append(os.path.join(cls_dir, fname)) return groups def split_groups(groups, ratios=(0.8, 0.1, 0.1), seed=42): random.seed(seed) keys = list(groups.keys()) random.shuffle(keys) n = len(keys) n_train = int(n * ratios[0]) n_val = int(n * ratios[1]) train_keys = keys[:n_train] val_keys = keys[n_train:n_train + n_val] test_keys = keys[n_train + n_val:] def collect(ks): out = [] for k in ks: out.extend(groups[k]) return out return collect(train_keys), collect(val_keys), collect(test_keys) groups = group_by_identity("dataset") train, val, test = split_groups(groups) print(len(train), len(val), len(test))这段代码的核心逻辑是先按身份聚合,再对身份做划分,而不是对图片做划分。ratios控制训练/验证/测试比例,seed固定保证可复现。identity的提取方式要根据实际数据调整,如果数据集自带标注文件,直接读标注里的身份字段最可靠。跑完打印三个集合的图片数,如果验证集少于 20 张,说明身份粒度太细,需要放宽分组策略。
提示:划分完一定要检查三个集合的类别比例是否接近 1:1,小数据集随机划分很容易出现某一类在验证集里只有个位数的情况。
3. 从零跑通训练:PyTorch 数据管线、增强与迁移学习参数
3.1 数据增强:手机人脸图该加什么、不该加什么
手机采集的人脸图有几个特点:光照不均、轻微模糊、角度偏转、背景杂乱。增强策略要针对这些特点来,而不是照搬 ImageNet 那套。水平翻转可以用,性别分类里左右脸对称性不影响标签。随机裁剪和缩放可以用,模拟人脸在画面中占比变化。颜色抖动要克制,手机白平衡本身就不稳,再加剧烈色彩扰动会让模型学到无关特征。
不要用垂直翻转,人脸倒过来在真实场景里几乎不出现,加了只会增加噪声。不要用大角度旋转,超过 15 度的旋转在手机自拍里很少见,而且会引入大量黑边。RandomErasing 可以小概率用,模拟遮挡,但概率别超过 0.3,300 张图经不起太多破坏。
import torch from torch.utils.data import Dataset, DataLoader from torchvision import transforms from PIL import Image class FaceGenderDataset(Dataset): def __init__(self, file_list, labels, transform=None): self.file_list = file_list self.labels = labels # 0: woman, 1: man self.transform = transform def __len__(self): return len(self.file_list) def __getitem__(self, idx): img = Image.open(self.file_list[idx]).convert("RGB") label = self.labels[idx] if self.transform: img = self.transform(img) return img, label # 训练增强:翻转 + 轻裁剪 + 轻度色彩抖动 train_tf = transforms.Compose([ transforms.Resize((256, 256)), transforms.RandomResizedCrop(224, scale=(0.8, 1.0)), transforms.RandomHorizontalFlip(p=0.5), transforms.ColorJitter(brightness=0.2, contrast=0.2, saturation=0.1), transforms.ToTensor(), transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]), ]) # 验证/测试:只做确定性预处理 val_tf = transforms.Compose([ transforms.Resize((256, 256)), transforms.CenterCrop(224), transforms.ToTensor(), transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]), ])RandomResizedCrop的scale=(0.8, 1.0)是关键参数,控制裁剪后人脸占比的变化范围。手机自拍人脸通常占画面 30% 到 70%,这个范围能覆盖大部分情况。ColorJitter的四个参数都压得比较低,brightness和contrast各 0.2,saturation只给 0.1,避免颜色增强过猛。归一化用的是 ImageNet 统计量,因为主干是 ImageNet 预训练的,这一步必须对齐,否则预训练权重白搭。
3.2 迁移学习训练循环与三个必调参数
模型选 MobileNetV3-Small 起步,参数量 2.5M 左右,300 张图微调不容易过拟合,推理也快。加载预训练权重后,先把主干全部冻结,只训最后的分类层。等验证集准确率连续 3 个 epoch 不涨,再解冻最后一个 block 做小学习率微调。
import torch.nn as nn from torchvision import models def build_model(num_classes=2, freeze_backbone=True): model = models.mobilenet_v3_small(weights=models.MobileNet_V3_Small_Weights.IMAGENET1K_V1) if freeze_backbone: for param in model.features.parameters(): param.requires_grad = False in_features = model.classifier[3].in_features model.classifier[3] = nn.Linear(in_features, num_classes) return model model = build_model() criterion = nn.CrossEntropyLoss() # 只优化需要梯度的参数 optimizer = torch.optim.AdamW( filter(lambda p: p.requires_grad, model.parameters()), lr=1e-3, weight_decay=1e-4 ) scheduler = torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=30)三个必调参数:学习率、weight_decay、batch_size。冻结阶段学习率给 1e-3,解冻后降到 1e-4 甚至 1e-5。weight_decay设 1e-4 是二分类小数据集的常用起点,过拟合明显就加到 1e-3。batch_size受限于 240 张训练图,设 16 或 32 都行,设 16 时一个 epoch 有 15 个 step,梯度更新足够频繁。CosineAnnealingLR的T_max设成总 epoch 数,让学习率平滑衰减到底。
训练循环里每个 epoch 记录训练 loss、验证 loss 和验证准确率。小数据集上,训练 loss 持续下降但验证 loss 开始上升,就是过拟合的明确信号,这时候要么加正则,要么提前停。我一般设 patience=8,验证 loss 连续 8 个 epoch 不降就停。
3.3 验证集只有 30 张,指标怎么看才不虚
30 张验证集,一张图就是 3.3 个百分点。准确率从 86.7% 跳到 90% 可能只是多对了一张。这种情况下单看准确率会骗自己,必须同时看混淆矩阵和每类召回率。性别分类里,如果 woman 召回率明显低于 man,说明模型偏向预测 man,可能是训练集里 man 的图片特征更集中,或者 woman 的图片里侧脸、遮挡更多。
from sklearn.metrics import confusion_matrix, classification_report def evaluate(model, loader, device): model.eval() all_preds, all_labels = [], [] with torch.no_grad(): for imgs, labels in loader: imgs = imgs.to(device) outputs = model(imgs) preds = outputs.argmax(dim=1).cpu().numpy() all_preds.extend(preds) all_labels.extend(labels.numpy()) print(confusion_matrix(all_labels, all_preds)) print(classification_report(all_labels, all_preds, target_names=["woman", "man"], digits=3))classification_report会输出每类的 precision、recall、f1。小验证集上,recall 比 precision 更值得关注,因为漏判的代价通常比误判高。如果两类 recall 差距超过 10 个百分点,就要回头查数据分布,而不是调模型。混淆矩阵能直接看出错分方向,woman 被错判成 man 多,还是反过来多,对应不同的数据问题。
注意:验证集和测试集都要用
val_tf,不能带任何随机增强。测试集只在最后跑一次,跑之前不要看结果,否则调参就变成了对测试集的过拟合。
4. 避坑与排查:300 张图训练时最容易翻车的 5 个地方
4.1 现象:训练准确率 100%,验证准确率卡在 50%
原因:模型完全记住了训练集,泛化能力为零。300 张图里每类 120 张,参数量 2.5M 的模型容量远超数据量,不加约束必然过拟合。
解决:先确认主干是否真的冻结了,requires_grad检查一遍。然后加weight_decay到 1e-3,加 dropout(MobileNetV3 分类头自带 dropout,可以把model.classifier[2]的 p 调到 0.3)。增强强度可以适当加大,RandomResizedCrop的 scale 下限降到 0.7。如果还不行,换更小的模型,比如把 MobileNetV3-Small 换成自定义的 4 层 CNN。
4.2 现象:验证 loss 震荡剧烈,准确率忽高忽低
原因:验证集太小,30 张图的 loss 方差本身就大;加上 batch_size 小,每个 batch 的梯度噪声大。
解决:把验证集扩到 60 张,从训练集里再匀一部分过来,训练集降到 210 张仍然够用。训练时用梯度累积,累积 4 个 batch 再更新一次,等效增大 batch_size。学习率调低一个数量级,从 1e-3 降到 3e-4。评估时不要只看最后一个 epoch,用最近 5 个 epoch 的滑动平均。
4.3 现象:woman 类召回率只有 60%,man 类 95%
原因:数据不平衡或特征分布差异。可能 woman 图片里侧脸、戴口罩、遮挡的比例更高,模型学到的 woman 特征不够鲁棒。
解决:先统计两类的图片质量分布,模糊度、人脸占比、角度。如果 woman 类确实更难,用类别加权损失,CrossEntropyLoss(weight=torch.tensor([1.5, 1.0]))。或者对 woman 类做更强的增强,增加其样本多样性。还可以用 focal loss 替代交叉熵,让模型关注难分样本。
4.4 现象:推理时同一张图多次预测结果不一致
原因:推理时忘了切model.eval(),BatchNorm 和 Dropout 还在训练模式,每次前向传播结果都不同。
解决:推理前必须model.eval(),并且用torch.no_grad()包住。如果用了 TTA(测试时增强),要确保增强是确定性的,不能带随机。检查数据预处理是否和训练时一致,特别是归一化参数,用错统计量会导致特征分布偏移。
4.5 现象:换一批新手机拍的图,准确率掉 20 个点
原因:域偏移。训练集的手机型号、光照条件、拍摄角度和新数据不一致,模型学到的特征和特定域绑定。
解决:这是小数据集的固有局限,300 张图覆盖不了所有手机成像风格。缓解办法是在训练时加更强的颜色和光照增强,ColorJitter的 brightness 和 contrast 提到 0.4,加RandomGrayscale(p=0.1)降低对颜色的依赖。如果目标域明确,收集 50 张目标域的图做微调,效果比任何增强都直接。
5. 把 300 张图用到极致:分层采样、阈值校准与推理封装
300 张图的训练集,如果只是随机采样,每个 epoch 看到的组合有限。我一般会用分层采样,保证每个 batch 里 woman 和 man 的比例严格 1:1,同时让每个 epoch 的样本顺序不同。WeightedRandomSampler可以做到这一点,给每个样本按类别赋权重,采样时按权重抽。
from torch.utils.data import WeightedRandomSampler def make_sampler(labels): class_counts = [labels.count(0), labels.count(1)] weights = [1.0 / class_counts[l] for l in labels] sampler = WeightedRandomSampler(weights, num_samples=len(labels), replacement=True) return sampler train_labels = [0] * 120 + [1] * 120 # 按实际标签替换 sampler = make_sampler(train_labels) train_loader = DataLoader(train_dataset, batch_size=16, sampler=sampler)WeightedRandomSampler的replacement=True允许重复采样,小类别会被多抽几次。num_samples设成训练集大小,保证每个 epoch 的 step 数和普通采样一致。这个采样器在类别不平衡时特别有用,但即使类别平衡,它也能让每个 batch 的类别分布更稳定,减少梯度噪声。
阈值校准是另一个提分点。默认 argmax 相当于阈值 0.5,但业务场景里你可能更怕漏判某一类。用验证集画 ROC 曲线,找到使 Youden index 最大的阈值,或者直接按业务需求定。比如安防场景里,把 woman 的判定阈值降到 0.4,宁可误判也不漏判。校准后的阈值要写进推理代码,不能还用 0.5。
import numpy as np from sklearn.metrics import roc_curve def find_best_threshold(model, loader, device): model.eval() probs, labels = [], [] with torch.no_grad(): for imgs, lbls in loader: imgs = imgs.to(device) outputs = torch.softmax(model(imgs), dim=1) probs.extend(outputs[:, 1].cpu().numpy()) # man 类概率 labels.extend(lbls.numpy()) fpr, tpr, thresholds = roc_curve(labels, probs) youden = tpr - fpr best_idx = np.argmax(youden) return thresholds[best_idx] best_thr = find_best_threshold(model, val_loader, device) print(f"best threshold: {best_thr:.3f}")推理封装时,把预处理、模型前向、阈值判断打包成一个函数,输入是图片路径或 PIL Image,输出是类别和置信度。预处理必须和验证时完全一致,Resize(256)+CenterCrop(224)+ 归一化,一步都不能少。如果部署到端侧,还要考虑把模型转成 ONNX 或 TorchScript,MobileNetV3-Small 转完后大概 10MB 以内,手机 CPU 上单张推理 20ms 左右。
最后说个我自己的习惯:每次训完模型,我会把验证集里分错的图单独存一个文件夹,肉眼过一遍。300 张图里错的那几张,往往能告诉你数据本身的问题——可能是标注错了,可能是人脸太小,可能是极端光照。模型指标只是黑匣子给你的信号,真正的原因还得靠眼睛看。这个习惯帮我省过很多次盲目调参的时间。希望帮到你。
本文还有配套的精品资源,点击获取