拓冰建站拓冰建站
首页 / 资讯中心 / 正文

用ResNet18微调300张人脸图实现性别分类与检测

简介面向深度学习算法训练的人脸性别检测与分类数据集涵盖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 轮这个策略在任何小数据集微调里都通用。本文还有配套的精品资源点击获取
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门