☰
300张手机人脸图跑性别分类:迁移学习与两段式链路实战
2026/10/1 18:06:31 网站建设 项目流程

简介:这份资源面向计算机视觉方向的科研人员、算法工程师与深度学习入门者,提供一套可直接用于性别检测与分类训练的人脸数据集。数据源自真实手机采集环境,共300张高质量人脸图片,按woman与man两个子集完成分类划分与标注,覆盖不同光线、表情与姿态,有助于提升模型泛化能力与性别分类准确率。压缩包共505个文件,约339.41MB,除jpg、png图片外,还包含大量py脚本、config与proto配置文件、pbtxt标签映射、pb模型文件及checkpoint权重,并附带ipynb笔记、md说明与少量视频素材,可支撑从人脸检测到特征提取、分类训练的完整流程。资源同时涉及基于Faster R-CNN、Mean-shift与卡尔曼滤波的人流统计思路,便于拓展智能监控与商业分析场景。目前已有59人学习,适合希望快速搭建性别分类实验、复用检测配置与排错思路的读者参考。

1. 300 张手机人脸图,够不够跑一个性别分类模型?

先说结论:够,但前提是你得把它当成一个「小样本、强先验」的任务来做,而不是指望它像 ImageNet 那样喂出一个通用模型。我手上这类数据集——woman / man 两类、300 张真实手机采集、已经分好类标好注——最典型的用法不是从零训练,而是做迁移学习里的微调,或者验证一条轻量流水线能不能跑通。手机采集意味着光照、角度、肤色、遮挡都带着真实世界的脏,这恰恰是它的价值:实验室里拿公开大头照训出来的模型,一上真实场景就翻车,而这份数据天生带这种「脏」。

它适合谁?做深度学习入门、想跑通第一个图像分类实战的人;做毕设需要一个干净二分类起点的人;以及想验证人脸性别检测这条链路(检测框 → 裁剪 → 分类)能不能落地的人。300 张的规模决定了它不适合当唯一数据源,但非常适合当微调集和验证集。下面我按「先立住原理、再动手复现、最后讲坑」的顺序,把这条链路拆开讲清楚。

2. 人脸性别检测这条链路:从检测框到二分类的完整拆解

2.1 为什么性别分类不是「一个 CNN 就完事」

很多人一上来就想拿 ResNet 直接吃整张图输出男女,这在 300 张的规模下几乎必然过拟合。真实链路是两段式的:先用检测把人脸框出来,再对裁剪后的人脸做分类。原因很直接——整张手机照片里背景、衣服、发型占了大半像素,性别信号集中在人脸区域,让分类网络去学「哪里是人脸」是浪费它本就不多的容量。

检测这一环,常见做法是用现成的人脸检测器(MTCNN、RetinaFace、YOLO 的人脸变体都行),把每张图的人脸框和关键点拿到。分类这一环才是我们要训的部分。两段式还有个好处:检测器可以复用,你换分类头、换数据、换类别都不用重训检测。300 张数据下,这个解耦能显著降低你要训的参数量。

提示:如果你的数据里一张图只有一张脸,检测这步可以离线跑一次,把裁剪结果存成新图,训练时直接读裁剪图,省掉每轮重复检测的开销。

2.2 数据划分:别用随机切分,按人切

300 张里如果同一个人出现多张,随机切分会让同一个人的脸同时进训练集和验证集,验证准确率虚高,上线就露馅。正确做法是按身份切分——同一个人只出现在一个集合里。如果数据没标身份,退而求其次按拍摄场景或时间切,尽量让训练和验证的分布有差异,这样验证分数才有参考价值。

常见比例是 8:1:1 或 7:2:1。300 张的话,训练 240、验证 30、测试 30 是个合理起点。类别要均衡,woman 和 man 各占一半左右,如果原始数据偏了,训练时用加权采样或类别权重补偿。

import os, random, shutil SRC = "faces_dataset" # 原始数据,结构: SRC/woman/*.jpg, SRC/man/*.jpg DST = "split_dataset" # 输出: DST/train|val|test/{woman,man} RATIO = (0.8, 0.1, 0.1) # 训练/验证/测试 SEED = 42 random.seed(SEED) for cls in ["woman", "man"]: files = sorted(os.listdir(os.path.join(SRC, cls))) random.shuffle(files) n = len(files) n_train = int(n * RATIO[0]) n_val = int(n * RATIO[1]) splits = { "train": files[:n_train], "val": files[n_train:n_train + n_val], "test": files[n_train + n_val:], } for split, names in splits.items(): out_dir = os.path.join(DST, split, cls) os.makedirs(out_dir, exist_ok=True) for name in names: shutil.copy(os.path.join(SRC, cls, name), os.path.join(out_dir, name)) print(cls, "total", n, "->", {k: len(v) for k, v in splits.items()})

这段脚本做的是按类别分层切分,保证每个集合里两类都有。RATIO控制比例,SEED固定后结果可复现。注意它只做了随机切分,如果你的数据有身份信息,把files换成按身份分组后的列表再切,逻辑一样。跑完检查一下每个 split 下两类的数量,偏差超过 10% 就调SEED或手动干预。

2.3 用迁移学习把 300 张用出 3000 张的效果

300 张从零训一个 CNN,验证集准确率大概率在 60% 上下晃,因为参数比样本多太多。迁移学习是这里的正解:拿在 ImageNet 上预训练过的骨干(ResNet18、MobileNetV3、EfficientNet-B0 都行),换掉最后的全连接层,冻结前面的卷积层,只训分类头。等分类头收敛了,再解冻最后几个 block 做小学习率微调。

为什么有效?预训练骨干已经学会了边缘、纹理、五官结构这些通用特征,性别分类只需要在这些特征上再学一层线性可分边界。300 张足够学这个边界,不够学底层特征。MobileNetV3-Small 这类轻量骨干在 300 张上尤其稳,参数少、过拟合风险低,还能部署到边缘设备。

import torch, torch.nn as nn from torchvision import models, transforms, datasets from torch.utils.data import DataLoader device = "cuda" if torch.cuda.is_available() else "cpu" # 手机图分辨率不一,统一到 224,训练集加轻量增强 train_tf = transforms.Compose([ transforms.Resize((224, 224)), transforms.RandomHorizontalFlip(), transforms.ColorJitter(0.2, 0.2, 0.2), transforms.ToTensor(), transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]), ]) eval_tf = transforms.Compose([ transforms.Resize((224, 224)), transforms.ToTensor(), transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]), ]) train_ds = datasets.ImageFolder("split_dataset/train", train_tf) val_ds = datasets.ImageFolder("split_dataset/val", eval_tf) train_ld = DataLoader(train_ds, batch_size=16, shuffle=True, num_workers=2) val_ld = DataLoader(val_ds, batch_size=16, shuffle=False, num_workers=2) model = models.mobilenet_v3_small(weights=models.MobileNet_V3_Small_Weights.DEFAULT) for p in model.features.parameters(): p.requires_grad = False # 先冻结骨干 model.classifier[3] = nn.Linear(model.classifier[3].in_features, 2) model = model.to(device) criterion = nn.CrossEntropyLoss() optimizer = torch.optim.AdamW(model.classifier.parameters(), lr=1e-3, weight_decay=1e-4)

关键参数说明:batch_size=16是 300 张规模下的稳妥值,太小梯度噪声大,太大一轮迭代次数不够;lr=1e-3只作用于分类头,因为骨干冻结了;weight_decay=1e-4抑制过拟合。增强只用了水平翻转和轻微颜色抖动,手机图本身光照差异大,颜色抖动幅度别开太大,否则把性别相关的肤色信号也抖没了。训练循环里每轮记录验证准确率和损失,验证损失连续 5 轮不降就停,这是 300 张数据下防过拟合的基本操作。

3. 训练、评估与部署:把模型跑成能用的东西

3.1 训练循环与早停的写法

接着上面的代码,训练循环要同时盯训练损失和验证指标。300 张数据下,训练损失很快降到接近 0,这时候验证损失如果开始上升,就是过拟合的信号。早停不是可选项,是必需品。

best_acc, patience, wait = 0.0, 5, 0 for epoch in range(30): model.train() for x, y in train_ld: x, y = x.to(device), y.to(device) optimizer.zero_grad() loss = criterion(model(x), y) loss.backward() optimizer.step() model.eval() correct = total = 0 with torch.no_grad(): for x, y in val_ld: x, y = x.to(device), y.to(device) pred = model(x).argmax(1) correct += (pred == y).sum().item() total += y.size(0) acc = correct / total print(f"epoch {epoch} val_acc {acc:.3f}") if acc > best_acc: best_acc, wait = acc, 0 torch.save(model.state_dict(), "best_gender.pt") else: wait += 1 if wait >= patience: print("early stop") break

patience=5意味着验证准确率 5 轮没刷新就停。保存的是验证集上最好的权重,不是最后一轮的,这点很多人会搞错,导致部署时用的是过拟合的版本。best_gender.pt就是最终产物。

3.2 评估不能只看准确率

300 张的测试集只有 30 张,准确率一个小数点波动就是一张图的事,参考价值有限。必须看混淆矩阵和每类召回率。性别分类里,如果 woman 召回率明显低于 man,说明模型偏向多数类或某些特征(比如长发)被过度依赖。这时候要回去看误判样本,多半是侧脸、遮挡、光照极端的图。

from sklearn.metrics import confusion_matrix, classification_report model.load_state_dict(torch.load("best_gender.pt")) model.eval() ys, ps = [], [] with torch.no_grad(): for x, y in DataLoader(datasets.ImageFolder("split_dataset/test", eval_tf), batch_size=16): ys += y.tolist() ps += model(x.to(device)).argmax(1).cpu().tolist() print(confusion_matrix(ys, ps)) print(classification_report(ys, ps, target_names=["man", "woman"]))

classification_report会给出每类的 precision、recall、f1。测试集小的时候,f1 比准确率更能反映真实水平。如果某一类 f1 低于 0.8,先别急着调模型,去翻误判图,八成是数据问题而不是模型问题。

3.3 推理阶段的预处理必须和训练一致

部署时最常见的翻车是预处理不一致:训练用了 Normalize,推理忘了;训练 Resize 到 224,推理直接喂原图。结果就是线下 90%、线上 60%。把预处理封装成一个函数,训练和推理共用。

def predict(img_path, model, tf, device): from PIL import Image img = Image.open(img_path).convert("RGB") x = tf(img).unsqueeze(0).to(device) # 加 batch 维度 model.eval() with torch.no_grad(): prob = torch.softmax(model(x), dim=1)[0] idx = prob.argmax().item() return ["man", "woman"][idx], prob[idx].item()

tf必须传eval_tf,不能传带增强的train_tf。返回置信度是为了让你设阈值——低于 0.7 的样本可以打上「不确定」标签,交人工复核,这在真实业务里比强行二分类更靠谱。

4. 避坑与排查:300 张数据最容易踩的五个坑

4.1 验证准确率 95%,上线只有 60%

现象:验证集分数漂亮,实际用起来一塌糊涂。原因:随机切分导致同一人的多张图跨了训练和验证,模型记住了人脸而不是性别特征。解决:按身份切分,没有身份信息就按拍摄批次切,确保验证集的人训练时没见过。

4.2 模型把长发一律判成 woman

现象:混淆矩阵里 woman 召回率高得离谱,但短发女性全判错。原因:数据里 woman 类长发占比过高,模型学到了「长发=女」这个捷径。解决:检查数据分布,补充短发女性样本;训练时加随机裁剪和遮挡增强,逼模型看脸而不是看头发。

4.3 训练损失降到 0.01,验证损失开始涨

现象:训练集几乎完美,验证集停滞。原因:300 张数据下模型容量过剩,过拟合。解决:冻结更多层、加 weight_decay、加 dropout、减小学习率,或者换更小的骨干。早停一定要开。

4.4 推理速度慢到没法用

现象:单张推理几百毫秒。原因:没冻结梯度、没切 eval 模式、或者骨干选太大。解决:推理时torch.no_grad()+model.eval(),骨干换 MobileNetV3-Small,必要时导出 ONNX 或 TorchScript 加速。

4.5 换一批手机拍的图,效果断崖下跌

现象:同型号手机拍的没问题,换机型就崩。原因:不同手机的成像风格、白平衡、锐化差异被模型当成了特征。解决:训练时加颜色抖动和模糊增强模拟不同成像;如果目标机型明确,用该机型补拍几十张做微调,比调参管用。

5. 把 300 张用出花:半监督与主动学习的进阶玩法

300 张的天花板摆在那,想再往上走,靠的不是调参,是扩数据。但人工标 3000 张成本高,这时候半监督和主动学习就派上用场了。思路是:用当前模型去预测大量未标注的人脸图,挑出置信度高的伪标签加入训练集,置信度低的交人工标注。这样每一轮标注预算都花在模型最不确定的样本上,效率比随机标高好几倍。

具体操作上,先拿现有 300 张训一个基线模型,然后收集一批未标注人脸图(同场景、同机型最好),用基线模型推理,把 softmax 概率在 0.9 以上的直接当伪标签,0.5 到 0.7 之间的挑出来人工标。伪标签样本训练时给一个小于 1 的损失权重,比如 0.3,避免错误伪标签带偏模型。每轮迭代后重新评估,通常两三轮就能把有效训练集从 300 扩到 1000 以上,验证准确率能再提 5 到 10 个点。

验证这套流程有没有效,别只看准确率,看模型在「困难样本」上的表现——侧脸、遮挡、暗光这些。建一个 50 张的困难验证集,每轮迭代都在这上面测,这个分数涨了才说明模型真的变强了,而不是在简单样本上刷分。我自己做这类小数据集项目,习惯是先把困难验证集建好再开始训,不然训到最后都不知道模型到底行不行。这套流程跑通一次,你手上任何 300 张级别的人脸二分类数据都能复用,换类别、换场景只改数据路径和类别名。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询