资讯详情 基于Python机器学习的年龄和性别预测:源码、数据集与实战避坑指南
📅 2026/10/12 22:02:37
简介这份资源面向计算机相关专业的在校学生、教师及企业员工提供一套基于Python机器学习的年龄与性别预测完整项目适合作为课程设计、大作业、毕业设计或入门进阶的学习案例。压缩包共36个文件约34KB以20个Python源码文件为核心辅以txt说明、cpickle与json序列化数据、log日志及md文档覆盖数据构建、模型训练、精度测试与部署预测等环节。项目代码完整且经过运行验证包含数据集与配置脚本便于读者理解从数据预处理到模型评估的完整流程并可直接用于二次开发或功能扩展。目前已有41人学习关注。使用前建议将解压目录重命名为英文路径避免中文路径引发运行异常遇到问题可先排查环境配置再结合日志与说明文档定位原因。1. 从一张人脸到两个标签年龄和性别预测到底在做什么你手里有一批带人脸的图片业务方想要的是「这张脸大概多少岁、是男是女」。听起来像两个分类任务实际落地时你会发现它比想象中拧巴性别是二分类年龄却既可以是回归预测一个连续数值也可以是分类按年龄段分桶。选错了模型要么抖动得没法看要么精度上不去。这个标题里的「基于 Python 机器学习的年龄和性别预测源码数据集」本质就是一套能跑通的端到端方案人脸检测 → 对齐裁剪 → 特征提取 → 双头输出。它适合两类人一是想拿现成源码快速验证效果的开发者二是想搞懂「为什么我的年龄预测总是不准」的调参党。下面我按自己踩过的顺序把这条链路拆开讲清楚。2. 数据集怎么选、怎么洗别急着喂给模型2.1 年龄性别数据集的三条硬指标拿到「数据集」三个字先别高兴年龄性别任务的数据集有几个坑是绕不开的。第一是年龄标注方式有的数据集给的是精确年龄如 25有的是年龄段如 25-32后者直接决定了你只能做分类。第二是性别平衡度如果男性样本占 80%模型会对女性脸系统性偏男。第三是人种与光照分布UTKFace、IMDB-Clean 这类常见数据集以欧美脸为主直接套到亚洲场景会掉点。我一般会先跑一段统计脚本把这三项摸清楚再决定任务形式。下面这段代码读入一个「文件名即标签」的典型数据集很多年龄性别数据集用age_gender_race_timestamp.jpg这种命名统计分布import os import re import matplotlib.pyplot as plt from collections import Counter DATA_DIR ./dataset/images ages, genders [], [] for fname in os.listdir(DATA_DIR): if not fname.lower().endswith((.jpg, .png)): continue # 文件名格式: 25_0_2_20170116174525125.jpg parts fname.split(_) if len(parts) 3: continue try: age int(parts[0]) gender int(parts[1]) # 0男 1女 except ValueError: continue if 0 age 116: ages.append(age) genders.append(gender) print(样本总数:, len(ages)) print(性别分布:, Counter(genders)) print(年龄范围:, min(ages), -, max(ages)) # 按 10 岁分桶看分布决定是回归还是分类 buckets Counter([a // 10 * 10 for a in ages]) print(年龄段分布:, sorted(buckets.items()))逻辑说明先按文件名解析出年龄和性别过滤掉越界年龄有些数据集有 0 或 116 的边界值。Counter输出性别和年龄段分布如果某个年龄段样本少于总数的 3%做分类时就要考虑合并桶或者加权采样。参数上age // 10 * 10是分桶粒度你可以改成 5 岁一桶粒度越细分类越难但业务表达越精确。2.2 人脸检测与对齐省掉这步精度直接崩很多人拿到数据集直接 resize 成 224×224 就丢进网络结果年龄预测误差大得离谱。原因是背景、头发、肩膀都进了感受野模型学到的是「这张图的整体风格」而不是「这张脸」。正确做法是先用人脸检测器框出人脸再按关键点做仿射对齐把眼睛和嘴巴摆到标准位置。常见做法是用 OpenCV 的 DNN 人脸检测器或者 MTCNN。下面用 OpenCV 自带的人脸检测做最小可用版本import cv2 import numpy as np # OpenCV 自带的人脸检测模型需提前下载 caffemodel 和 prototxt net cv2.dnn.readNetFromCaffe( deploy.prototxt, res10_300x300_ssd_iter_140000.caffemodel ) def detect_and_crop(img_path, margin0.2, size(224, 224)): img cv2.imread(img_path) h, w img.shape[:2] blob cv2.dnn.blobFromImage( cv2.resize(img, (300, 300)), 1.0, (300, 300), (104.0, 177.0, 123.0) ) net.setInput(blob) detections net.forward() best None for i in range(detections.shape[2]): conf detections[0, 0, i, 2] if conf 0.5: box detections[0, 0, i, 3:7] * np.array([w, h, w, h]) if best is None or conf best[0]: best (conf, box.astype(int)) if best is None: return None _, (x1, y1, x2, y2) best # 外扩 margin避免裁掉下巴和额头 bw, bh x2 - x1, y2 - y1 x1 max(0, int(x1 - bw * margin)) y1 max(0, int(y1 - bh * margin)) x2 min(w, int(x2 bw * margin)) y2 min(h, int(y2 bh * margin)) face img[y1:y2, x1:x2] return cv2.resize(face, size) face detect_and_crop(./dataset/images/25_0_2_20170116174525125.jpg) print(裁剪结果尺寸:, None if face is None else face.shape)逻辑说明blobFromImage里的均值(104.0, 177.0, 123.0)是 OpenCV 人脸模型的固定预处理参数别改。conf 0.5是置信度阈值漏检多就降到 0.3误检多就升到 0.7。margin0.2是外扩比例太小会切掉下巴导致年龄偏大太大又会引入背景。这一步做完把裁剪后的人脸存成新目录后续训练直接读能省掉每个 epoch 重复检测的开销。提示如果数据集本身已经是裁剪好的人脸比如很多年龄性别数据集可以跳过检测但建议仍做一次关键点对齐眼睛水平对齐对年龄预测的提升在 1 岁以上。3. 模型结构双头输出怎么搭才不互相拖累3.1 共享骨干 两个头的取舍年龄和性别是相关但不相同的任务。共享底层特征边缘、纹理、五官结构能省参数、防过拟合但两个任务的损失尺度差很多性别是 0/1 交叉熵年龄如果是回归MSE 动辄几十上百。直接相加会让年龄损失主导梯度性别学不动。我一般用「共享骨干 两个独立头 损失加权」的结构。骨干用轻量的 MobileNetV2 或 ResNet18 就够年龄头输出回归值性别头输出二分类 logits。关键是损失权重loss gender_loss 0.01 * age_loss这个 0.01 不是拍脑袋是让两个损失初始量级接近。下面用 PyTorch 搭一个最小版本import torch import torch.nn as nn import torchvision.models as models class AgeGenderNet(nn.Module): def __init__(self, pretrainedTrue): super().__init__() backbone models.mobilenet_v2(pretrainedpretrained) self.features backbone.features self.pool nn.AdaptiveAvgPool2d(1) feat_dim 1280 # 性别头二分类 self.gender_head nn.Sequential( nn.Dropout(0.3), nn.Linear(feat_dim, 2) ) # 年龄头回归输出 1 维 self.age_head nn.Sequential( nn.Dropout(0.3), nn.Linear(feat_dim, 1) ) def forward(self, x): feat self.pool(self.features(x)).flatten(1) gender_logits self.gender_head(feat) age_pred self.age_head(feat).squeeze(-1) return gender_logits, age_pred model AgeGenderNet() print(参数量:, sum(p.numel() for p in model.parameters()) / 1e6, M)逻辑说明AdaptiveAvgPool2d(1)把特征图压成 1280 维向量MobileNetV2 的最后一层通道数就是 1280。两个头都加了Dropout(0.3)因为年龄性别数据集通常只有几万张不加容易过拟合。年龄头输出 1 维训练时用 MSE 或 SmoothL1推理时直接就是岁数。如果你想把年龄也做成分类比如 8 个年龄段把Linear(feat_dim, 1)改成Linear(feat_dim, 8)损失换成交叉熵即可。3.2 训练循环里的三个关键参数训练脚本本身不复杂但有几个参数直接决定成败。下面是一个可复现的训练片段import torch from torch.utils.data import DataLoader from torchvision import transforms device torch.device(cuda if torch.cuda.is_available() else cpu) model AgeGenderNet().to(device) 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]) ]) optimizer torch.optim.AdamW(model.parameters(), lr1e-4, weight_decay1e-4) scheduler torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max30) ce_loss torch.nn.CrossEntropyLoss() mse_loss torch.nn.SmoothL1Loss() AGE_WEIGHT 0.01 # 年龄损失权重关键参数 for epoch in range(30): model.train() for imgs, ages, genders in train_loader: imgs imgs.to(device) ages ages.float().to(device) genders genders.long().to(device) g_logits, a_pred model(imgs) loss_g ce_loss(g_logits, genders) loss_a mse_loss(a_pred, ages) loss loss_g AGE_WEIGHT * loss_a optimizer.zero_grad() loss.backward() optimizer.step() scheduler.step() print(fepoch {epoch} loss_g{loss_g.item():.4f} loss_a{loss_a.item():.4f})逻辑说明AdamW的weight_decay1e-4比 SGD 的默认值更稳适合小数据集。CosineAnnealingLR让学习率从 1e-4 余弦下降到 0T_max30要和总 epoch 数一致。AGE_WEIGHT0.01是核心如果训练时发现性别准确率一直卡在 50%说明年龄损失压过了性别把它降到 0.005如果年龄 MAE 下不去升到 0.02 试试。SmoothL1Loss比纯 MSE 对异常年龄标注更鲁棒数据集里那些标错年龄的样本不会把梯度带飞。注意RandomHorizontalFlip对性别任务安全但如果你的数据集里性别和左右脸有强相关极少见可以关掉。ColorJitter的强度 0.2 是经验值再大肤色会失真年龄预测反而变差。4. 避坑与排查这五个坑我基本每次都遇到4.1 年龄 MAE 卡在 8 岁以上下不去现象训练 loss 正常下降但验证集年龄平均绝对误差MAE一直在 8-10 岁徘徊性别准确率却有 95%。原因年龄回归对特征的要求比性别高得多性别看发型和轮廓就够年龄要看皱纹、皮肤纹理、眼周细节。如果输入分辨率只有 96×96或者骨干太浅年龄头拿不到足够细节。解决把输入提到 224×224骨干换成 ResNet50 或 EfficientNet-B0或者在年龄头前接一层注意力模块让模型聚焦眼周和法令纹区域。我实测把分辨率从 112 提到 224MAE 能降 1.5 岁左右。4.2 性别预测对女性系统性偏男现象整体性别准确率 94%但单独看女性样本只有 85%混淆矩阵里大量女性被预测成男性。原因训练集性别不平衡或者数据增强里的ColorJitter让部分女性肤色偏暗模型学到了「暗男」的伪相关。解决先统计性别比例用WeightedRandomSampler做平衡采样把ColorJitter的亮度扰动调小到 0.1。如果还不行在损失里给女性样本加权重CrossEntropyLoss(weighttorch.tensor([1.0, 1.5]))。4.3 验证集 loss 比训练集还低现象训练 loss 0.8验证 loss 0.6看起来「泛化很好」但实际测试时效果一塌糊涂。原因验证集和训练集来自同一批人、同一场景分布太像。或者验证集的年龄标注比训练集更规范导致 loss 天然更低。解决按人划分数据集同一个人不能同时出现在训练和验证里。如果数据集没有身份 ID至少按拍摄时间或场景分组划分。别被验证 loss 骗了最终要看独立测试集。4.4 推理时单张图预测年龄每次都不一样现象同一张图连续预测三次年龄分别是 27、31、29。原因模型里还有Dropout没关或者 BatchNorm 在推理时用了当前 batch 的统计量。解决推理前必须model.eval()并用torch.no_grad()包住。如果用了Dropouteval()会自动关闭。BatchNorm 在eval()下用滑动平均统计量结果就稳定了。4.5 数据集里混进了非人脸图现象训练几个 epoch 后 loss 突然飙升检查发现有些图是风景或卡通。原因数据集下载或清洗时没过滤文件名解析虽然过了但内容不是人脸。解决在数据加载阶段加一道人脸检测过滤检测不到人脸的图直接跳过并记录到日志。宁可少几千张也别让噪声样本污染梯度。5. 把 MAE 再压 1 岁的两个技巧第一个技巧是年龄标签平滑。年龄标注本身有主观误差25 岁和 26 岁的人脸可能没区别。与其硬回归到精确值不如把标签做成软分布真实年龄 25标签在 23-27 之间给高斯权重。实现上把SmoothL1Loss换成 KL 散度年龄头输出改成多分类再求期望。我实测这个改动让 MAE 从 6.8 降到 5.9代价是推理多一次 softmax。第二个技巧是测试时增强TTA。推理时对同一张脸做原图、水平翻转、轻微缩放三个版本年龄取平均性别取投票。代码很短def predict_tta(model, img_tensor): model.eval() with torch.no_grad(): variants [ img_tensor, torch.flip(img_tensor, dims[3]), transforms.functional.resize(img_tensor, [240, 240]) ] ages, genders [], [] for v in variants: v transforms.functional.resize(v, [224, 224]) g_logits, a_pred model(v.unsqueeze(0)) ages.append(a_pred.item()) genders.append(g_logits.argmax(1).item()) final_age sum(ages) / len(ages) final_gender max(set(genders), keygenders.count) return final_age, final_gender逻辑说明torch.flip做水平翻转resize到 240 再回到 224 模拟轻微尺度变化。年龄取均值降方差性别取众数抗单次误判。TTA 会让推理耗时变成 3 倍如果业务对延迟敏感只保留翻转这一路收益已经有一半。最后说个我自己的习惯每次训完模型我都会挑 20 张预测误差最大的图单独看一遍。十次里有八次能发现共性问题——要么是侧脸太多要么是戴墨镜要么是年龄标注本身错了。这个动作比调任何超参都值。希望帮到你。本文还有配套的精品资源点击获取