300张手机人脸图做性别分类:小样本迁移学习实战与避坑指南

发布时间:2026/10/5 9:27:29
300张手机人脸图做性别分类:小样本迁移学习实战与避坑指南 简介这份资源面向计算机视觉方向的科研人员、算法工程师与深度学习入门者提供一套可直接用于性别检测与分类训练的人脸数据集。数据源自真实手机采集共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), seed42): 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.3300 张图经不起太多破坏。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, transformNone): 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(p0.5), transforms.ColorJitter(brightness0.2, contrast0.2, saturation0.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.2saturation只给 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_classes2, freeze_backboneTrue): model models.mobilenet_v3_small(weightsmodels.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()), lr1e-3, weight_decay1e-4 ) scheduler torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max30)三个必调参数学习率、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 开始上升就是过拟合的明确信号这时候要么加正则要么提前停。我一般设 patience8验证 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(dim1).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], digits3))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加 dropoutMobileNetV3 分类头自带 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(weighttorch.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(p0.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_sampleslen(labels), replacementTrue) return sampler train_labels [0] * 120 [1] * 120 # 按实际标签替换 sampler make_sampler(train_labels) train_loader DataLoader(train_dataset, batch_size16, samplersampler)WeightedRandomSampler的replacementTrue允许重复采样小类别会被多抽几次。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), dim1) 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(fbest threshold: {best_thr:.3f})推理封装时把预处理、模型前向、阈值判断打包成一个函数输入是图片路径或 PIL Image输出是类别和置信度。预处理必须和验证时完全一致Resize(256)CenterCrop(224) 归一化一步都不能少。如果部署到端侧还要考虑把模型转成 ONNX 或 TorchScriptMobileNetV3-Small 转完后大概 10MB 以内手机 CPU 上单张推理 20ms 左右。最后说个我自己的习惯每次训完模型我会把验证集里分错的图单独存一个文件夹肉眼过一遍。300 张图里错的那几张往往能告诉你数据本身的问题——可能是标注错了可能是人脸太小可能是极端光照。模型指标只是黑匣子给你的信号真正的原因还得靠眼睛看。这个习惯帮我省过很多次盲目调参的时间。希望帮到你。本文还有配套的精品资源点击获取