简介:这份资源面向计算机相关专业的在校学生、教师及企业员工,提供一套基于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, margin=0.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。margin=0.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, pretrained=True): super().__init__() backbone = models.mobilenet_v2(pretrained=pretrained) 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(), lr=1e-4, weight_decay=1e-4) scheduler = torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=30) 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(f"epoch {epoch} loss_g={loss_g.item():.4f} loss_a={loss_a.item():.4f}")逻辑说明:AdamW的weight_decay=1e-4比 SGD 的默认值更稳,适合小数据集。CosineAnnealingLR让学习率从 1e-4 余弦下降到 0,T_max=30要和总 epoch 数一致。AGE_WEIGHT=0.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 提到 224,MAE 能降 1.5 岁左右。
4.2 性别预测对女性系统性偏男
现象:整体性别准确率 94%,但单独看女性样本只有 85%,混淆矩阵里大量女性被预测成男性。
原因:训练集性别不平衡,或者数据增强里的ColorJitter让部分女性肤色偏暗,模型学到了「暗=男」的伪相关。
解决:先统计性别比例,用WeightedRandomSampler做平衡采样;把ColorJitter的亮度扰动调小到 0.1。如果还不行,在损失里给女性样本加权重,CrossEntropyLoss(weight=torch.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()包住。如果用了Dropout,eval()会自动关闭。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), key=genders.count) return final_age, final_gender逻辑说明:torch.flip做水平翻转,resize到 240 再回到 224 模拟轻微尺度变化。年龄取均值降方差,性别取众数抗单次误判。TTA 会让推理耗时变成 3 倍,如果业务对延迟敏感,只保留翻转这一路,收益已经有一半。
最后说个我自己的习惯:每次训完模型,我都会挑 20 张预测误差最大的图单独看一遍。十次里有八次能发现共性问题——要么是侧脸太多,要么是戴墨镜,要么是年龄标注本身错了。这个动作比调任何超参都值。希望帮到你。
本文还有配套的精品资源,点击获取