简介这是一份面向计算机相关专业学生与初学者的机器学习实战资源围绕基于Python的年龄与性别预测任务展开适合课程设计、大作业、毕业设计或入门项目练手。压缩包共36个文件约34KB以20个Python脚本为核心涵盖数据构建、模型训练、精度测试与预测部署等环节另含4个cpickle序列化模型文件、4个json均值配置、4个txt说明及2个md文档便于理解项目结构与运行流程。资源已有40人学习虽体量轻巧但代码完整解压后按英文路径运行即可上手。读者可借此掌握图像分类中年龄与性别双任务预测的完整链路包括数据集构建、标签编码、网络定义、训练日志分析与推理测试并参考Windows下GPU环境安装说明排查常见问题具备较高的学习借鉴与二次开发价值。1. 从一张人脸到两个标签年龄和性别预测到底在做什么你手里有一张人脸照片想同时知道这个人大概多少岁、是男是女。这件事在业务里出现的频率远比想象中高——门店客流分析要按性别和年龄段统计、内容平台要做分龄推荐、安防场景要快速筛出可疑人员画像。基于 Python 机器学习的年龄和性别预测本质上是把「人脸检测 → 特征提取 → 双任务分类/回归」串成一条流水线让模型从像素里同时吐出两个标签。这个方向适合两类人一类是刚学完机器学习基础、想找一个「有数据集、有源码、能跑通、能改参数」的完整项目练手的人另一类是在做计算机视觉落地、需要快速搭一个年龄性别识别基线再迭代的工程师。它不要求你从零推导反向传播但要求你能看懂数据长什么样、模型输出怎么解释、评估指标为什么不能只看准确率。下面按「数据怎么准备 → 模型怎么搭 → 怎么训 → 坑在哪 → 怎么调」的顺序把这条流水线拆开讲清楚。2. 数据集长什么样标签分布、人脸对齐与划分策略2.1 年龄性别数据集的典型结构与标签含义常见的年龄性别数据集如 Adience、UTKFace、IMDB-WIKI 的子集通常是一堆人脸图片加一个标注文件。标注文件里每行对应一张图字段大致是文件名、性别0/1、年龄整数或区间。UTKFace 风格的文件名直接编码了年龄、性别、种族例如25_0_2_20170116174525125.jpg解析文件名就能拿到标签Adience 风格则用 fold 文件管理交叉验证划分。先别急着写模型第一步是把数据摸清楚。用下面这段脚本统计标签分布你会立刻发现两个问题性别可能接近 1:1但年龄分布往往严重偏斜20-40 岁占了大半老人和小孩样本很少。import os import re import pandas as pd import matplotlib.pyplot as plt # 以 UTKFace 文件名编码标签为例age_gender_race_timestamp.jpg def parse_utkface(data_dir): records [] for fname in os.listdir(data_dir): if not fname.lower().endswith((.jpg, .png)): continue parts fname.split(_) if len(parts) 3: continue try: age int(parts[0]) gender int(parts[1]) # 0 男 1 女 except ValueError: continue records.append({path: os.path.join(data_dir, fname), age: age, gender: gender}) return pd.DataFrame(records) df parse_utkface(./data/utkface) print(df[gender].value_counts(normalizeTrue)) print(df[age].describe()) # 年龄分桶看分布决定要不要做重采样 bins [0, 10, 20, 30, 40, 50, 60, 120] df[age_bin] pd.cut(df[age], binsbins) print(df[age_bin].value_counts().sort_index())这段代码做了三件事遍历目录、从文件名解析标签、输出性别比例和年龄统计。parse_utkface里的parts[0]和parts[1]是 UTKFace 的固定约定换成 Adience 就要改成读 fold 文件。pd.cut的分桶边界按业务需求调整如果你关心未成年人识别就把 0-18 单独切出来。注意不同数据集的标签编码不统一有的性别用 m/f 字符串有的用 0/1 但含义相反。跑模型前一定打印几行原始标注确认否则训练半天发现性别全反了。2.2 人脸对齐与尺寸归一化别让背景干扰模型原始数据集里的人脸位置、大小、角度都不一致。如果直接把整张图缩到 224×224 丢给模型背景、头发、衣服都会成为噪声。常见做法是先用 OpenCV 的 Haar 级联或 DNN 人脸检测器把脸框出来再做对齐和裁剪。import cv2 import numpy as np # 加载 OpenCV 自带的人脸检测器 face_cascade cv2.CascadeClassifier( cv2.data.haarcascades haarcascade_frontalface_default.xml) def crop_face(img_path, margin0.2, size(224, 224)): img cv2.imread(img_path) if img is None: return None gray cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) faces face_cascade.detectMultiScale(gray, 1.1, 5, minSize(30, 30)) if len(faces) 0: return None # 取面积最大的脸 x, y, w, h max(faces, keylambda r: r[2] * r[3]) # 向外扩 margin保留一点上下文 mx, my int(w * margin), int(h * margin) x1, y1 max(0, x - mx), max(0, y - my) x2, y2 min(img.shape[1], x w mx), min(img.shape[0], y h my) face img[y1:y2, x1:x2] face cv2.resize(face, size) return face face crop_face(./data/utkface/25_0_2_20170116174525125.jpg) print(face.shape if face is not None else no face detected)detectMultiScale的三个关键参数scaleFactor1.1控制每次缩放的步长越小越慢但越不容易漏minNeighbors5控制误检越大越严格minSize过滤太小的框。margin0.2是经验值扩太多会把背景带回来扩太少下巴和额头会被切掉。检测不到脸就返回 None训练时直接跳过这些样本。2.3 训练集/验证集/测试集怎么切才不泄漏同一个人的多张照片如果被分到训练集和验证集验证指标会虚高。UTKFace 这类数据集没有明确的人物 ID只能按文件名或时间戳近似去重。更稳妥的做法是按年龄和性别做分层抽样保证每个集合的标签分布接近。from sklearn.model_selection import train_test_split # 先切出 15% 做测试集剩下的再按 8:2 切训练和验证 train_val, test train_test_split( df, test_size0.15, stratifydf[age_bin], random_state42) train, val train_test_split( train_val, test_size0.2, stratifytrain_val[age_bin], random_state42) print(len(train), len(val), len(test))stratify按年龄分桶做分层避免某个年龄段在验证集里完全缺失。random_state固定后结果可复现。如果你的数据集有明确的人物 ID一定要按 ID 切分而不是按图片切分。3. 模型怎么搭从 CNN 基线到双头输出3.1 为什么用多任务学习而不是训两个独立模型年龄预测是回归或有序分类性别预测是二分类。最直接的做法是训两个模型但人脸特征里年龄和性别高度相关——比如胡须、皱纹、发际线同时影响两个任务。多任务学习让两个任务共享底层卷积特征只在最后分叉出两个头参数量更少、泛化通常更好推理时一次前向就出两个结果。常见做法是用一个轻量骨干网络MobileNetV2、ResNet18 或自己搭的小 CNN接一个全局池化层然后分成两个分支性别头输出 2 维 logits年龄头输出 1 维回归值或 N 个年龄桶的 logits。import torch import torch.nn as nn import torchvision.models as models class AgeGenderNet(nn.Module): def __init__(self, backbonemobilenet_v2, age_bins8): super().__init__() if backbone mobilenet_v2: net models.mobilenet_v2(weightsIMAGENET1K_V1) self.features net.features feat_dim 1280 else: net models.resnet18(weightsIMAGENET1K_V1) self.features nn.Sequential(*list(net.children())[:-2]) feat_dim 512 self.pool nn.AdaptiveAvgPool2d(1) self.dropout nn.Dropout(0.3) self.gender_head nn.Linear(feat_dim, 2) self.age_head nn.Linear(feat_dim, age_bins) # 分类式年龄预测 def forward(self, x): f self.features(x) f self.pool(f).flatten(1) f self.dropout(f) return self.gender_head(f), self.age_head(f) model AgeGenderNet() g_logits, a_logits model(torch.randn(4, 3, 224, 224)) print(g_logits.shape, a_logits.shape) # [4,2] [4,8]weightsIMAGENET1K_V1加载预训练权重小数据集上这是提点最快的一步。AdaptiveAvgPool2d(1)把任意空间尺寸压成 1×1避免全连接层对输入尺寸敏感。age_bins8对应前面分桶的 8 个区间用分类代替回归的好处是训练更稳定坏处是丢失了桶内的连续信息推理时取桶中心作为年龄估计。3.2 损失函数怎么配分类交叉熵加回归损失的权重性别用交叉熵年龄如果用分类就用交叉熵如果用回归就用 MSE 或 Smooth L1。多任务的关键是两项损失的权重。年龄回归的 MSE 数值范围可能到几百性别交叉熵只有零点几不加权的话年龄损失会主导梯度。def multitask_loss(g_logits, a_logits, g_target, a_target, w_gender1.0, w_age0.002): loss_g nn.functional.cross_entropy(g_logits, g_target) loss_a nn.functional.cross_entropy(a_logits, a_target) return w_gender * loss_g w_age * loss_a, loss_g.item(), loss_a.item()w_age0.002是经验值让两项损失量级接近。实际调的时候先各跑一个单任务看收敛时的损失值再按比例设权重。如果年龄头用回归w_age通常设成 0.01 到 0.1 之间。训练时把两项损失分别打印出来如果某一项一直不降说明权重太小或标签有问题。3.3 数据增强与类别不平衡处理年龄分布偏斜时除了在损失里给少数类加权还可以在采样时做重采样。增强方面人脸任务常用的有随机水平翻转、小角度旋转、颜色抖动。注意水平翻转会改变性别标签吗不会但会改变一些左右不对称的特征通常无害。from torchvision import transforms from torch.utils.data import WeightedRandomSampler train_tf transforms.Compose([ transforms.ToPILImage(), transforms.RandomHorizontalFlip(0.5), transforms.RandomRotation(10), transforms.ColorJitter(0.2, 0.2, 0.2), transforms.ToTensor(), transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]), ]) # 按年龄桶的倒数做采样权重 bin_counts train[age_bin].value_counts() weights train[age_bin].map(lambda b: 1.0 / bin_counts[b]).values sampler WeightedRandomSampler(weights, num_sampleslen(weights), replacementTrue)Normalize的均值方差是 ImageNet 的统计值用预训练权重时保持一致。WeightedRandomSampler让少数年龄段被采到的概率更高replacementTrue表示有放回采样。增强不要太猛旋转超过 15 度、颜色抖动过大都会让模型学到无关特征。4. 训练与评估指标怎么看、模型怎么选4.1 训练循环的关键参数与早停训练循环本身不复杂关键是学习率、batch size、优化器和早停策略。预训练骨干用较小学习率1e-4 到 1e-3新加的头可以用大一点1e-3 到 1e-2。常见做法是分参数组设置学习率。from torch.optim import AdamW from torch.optim.lr_scheduler import CosineAnnealingLR head_params list(model.gender_head.parameters()) list(model.age_head.parameters()) backbone_params list(model.features.parameters()) optimizer AdamW([ {params: backbone_params, lr: 1e-4}, {params: head_params, lr: 1e-3}, ], weight_decay1e-4) scheduler CosineAnnealingLR(optimizer, T_max30) best_val float(inf) patience, wait 5, 0 for epoch in range(30): model.train() for imgs, g_labels, a_labels in train_loader: optimizer.zero_grad() g_logits, a_logits model(imgs) loss, _, _ multitask_loss(g_logits, a_logits, g_labels, a_labels) loss.backward() optimizer.step() scheduler.step() # 验证逻辑省略计算 val_loss # if val_loss best_val: 保存; else: wait 1 # if wait patience: breakAdamW的weight_decay1e-4是正则化防止过拟合。CosineAnnealingLR让学习率按余弦曲线下降T_max设成总 epoch 数。早停的patience5表示验证损失连续 5 轮不降就停避免浪费算力。4.2 年龄预测的评估MAE 比准确率更有意义年龄如果用分类做准确率只能说明落在同一个桶里的比例桶的粒度决定了这个指标的上限。更有意义的是 MAE平均绝对误差即预测年龄和真实年龄的平均差距。性别则看准确率、精确率、召回率和 F1。import numpy as np def evaluate(model, loader, age_bin_centers): model.eval() g_preds, g_trues, a_preds, a_trues [], [], [], [] with torch.no_grad(): for imgs, g_labels, a_labels in loader: g_logits, a_logits model(imgs) g_preds.extend(g_logits.argmax(1).cpu().numpy()) g_trues.extend(g_labels.numpy()) # 年龄取概率最大的桶映射回桶中心 a_bin a_logits.argmax(1).cpu().numpy() a_preds.extend([age_bin_centers[b] for b in a_bin]) a_trues.extend(a_labels.numpy()) g_acc np.mean(np.array(g_preds) np.array(g_trues)) age_mae np.mean(np.abs(np.array(a_preds) - np.array(a_trues))) return g_acc, age_maeage_bin_centers是每个年龄桶的中心值比如 [5, 15, 25, 35, 45, 55, 65, 75]。MAE 在 5 岁以内算不错10 岁以内说明模型只学到了粗粒度趋势。性别准确率在平衡数据集上通常能到 90% 以上但如果测试集里某个性别占绝大多数准确率会虚高一定要看混淆矩阵。4.3 推理阶段的批处理与阈值调整上线推理时性别输出可以设阈值调整精确率和召回率的平衡。比如业务更怕把男性误判成女性就把女性的判定阈值调高。年龄输出除了取最大桶也可以做桶间的期望值得到更平滑的估计。def predict(model, img_tensor, age_centers, gender_threshold0.5): model.eval() with torch.no_grad(): g_logits, a_logits model(img_tensor.unsqueeze(0)) g_prob torch.softmax(g_logits, dim1)[0] gender 1 if g_prob[1] gender_threshold else 0 a_prob torch.softmax(a_logits, dim1)[0] # 用期望值代替最大桶减少桶边界跳变 age float((a_prob * torch.tensor(age_centers)).sum()) return gender, age, float(g_prob[gender])gender_threshold按业务调默认 0.5。年龄期望值的好处是输出连续坏处是当概率分散在多个桶时期望值可能落在两个桶之间解释性稍弱。返回的置信度可以用于过滤低质量样本。5. 避坑与排查那些让指标崩掉的细节5.1 损失不下降先查标签编码现象训练几轮后损失几乎不动性别准确率停在 50% 左右。原因性别标签编码和数据加载时的映射不一致比如标注里 0 是女性代码里当成男性。解决打印一个 batch 的标签和对应图片人工确认几张再检查Dataset.__getitem__里的映射逻辑。5.2 验证集指标远高于测试集现象验证集性别准确率 95%测试集只有 80%。原因划分时同一个人或同一批采集的照片同时出现在两个集合造成数据泄漏。解决按人物 ID 或时间戳分组切分没有 ID 就用图片哈希去重确保同一来源只出现在一个集合。5.3 年龄 MAE 很大但训练损失正常现象训练损失稳步下降但年龄 MAE 一直在 15 岁以上。原因年龄桶的边界设得太粗或者桶中心映射错了。解决检查age_bin_centers是否和pd.cut的 bins 对应比如 bins 是 [0,10,20,...]中心应该是 5,15,25 而不是 0,10,20。另外确认验证时用的年龄标签是原始年龄而不是桶索引。5.4 推理时显存暴涨或速度慢现象单张推理正常批量推理时显存不够或耗时线性增长。原因没有用torch.no_grad()或者 batch size 设得太大。解决推理包在torch.no_grad()里batch size 从 32 开始试用torch.cuda.amp.autocast()做混合精度。如果部署在 CPU 上考虑把模型转成 ONNX 或 TorchScript。5.5 人脸检测漏检导致样本丢失现象训练时发现很多图片被跳过实际参与训练的样本远少于数据集大小。原因Haar 级联对侧脸、遮挡、低光照的漏检率高。解决换用 OpenCV DNN 人脸检测器或 MTCNN检测不到时退化为整图缩放而不是直接丢弃。统计漏检率如果超过 10%说明检测器需要换。6. 把 MAE 压到 4 岁以内几个我反复用到的调优习惯年龄预测想从「能跑」到「能用」MAE 从 8 岁压到 4 岁以内靠的不是换更大的模型而是几个细节的叠加。第一个习惯是年龄标签做有序回归而不是纯分类。把 8 个桶的 one-hot 换成累积概率即预测「年龄大于第 k 个边界」的概率推理时对这些概率求和得到连续年龄。这样既保留了分类的稳定性又恢复了桶间的顺序信息。def ordinal_targets(ages, bin_edges): # bin_edges 形如 [10,20,30,40,50,60] targets [] for a in ages: targets.append([1.0 if a e else 0.0 for e in bin_edges]) return torch.tensor(targets, dtypetorch.float32) def ordinal_loss(a_logits, a_targets): # a_logits: [B, K-1]每个位置是二分类 logit return nn.functional.binary_cross_entropy_with_logits(a_logits, a_targets) def ordinal_to_age(probs, bin_edges): # probs: [K-1] 的 sigmoid 输出累加得到年龄 return float(sum(probs)) bin_edges[0] # 简化写法按实际边界调整ordinal_targets把每个年龄转成 K-1 个二分类标签ordinal_loss用 BCE 逐位计算。推理时把 sigmoid 输出累加再加上最小边界就得到连续年龄。这个做法在多个数据集上比纯分类的 MAE 低 1 到 2 岁。第二个习惯是验证时按年龄段分别看 MAE。整体 MAE 会被 20-40 岁的大样本拉低掩盖老人和小孩的误差。我一般会打印每个桶的 MAE如果某个桶明显偏高就针对性地补样本或调采样权重。第三个习惯是固定随机种子跑三次取指标的中位数。年龄性别任务在小数据集上波动很大单次结果不可信。调优手段典型 MAE 变化代价预训练骨干-2 到 -3 岁需要下载权重有序回归替代分类-1 到 -2 岁改损失和推理逻辑年龄桶重采样-0.5 到 -1 岁训练轮数增加人脸对齐裁剪-1 到 -2 岁预处理耗时测试时增强-0.3 到 -0.5 岁推理耗时翻倍这些手段可以叠加但要注意边际递减。我的习惯是先把预训练和对齐做上再看有序回归最后才考虑测试时增强。每次只改一个变量记录指标不然出了问题不知道是哪个改动导致的。这套流程跑下来UTKFace 这类干净数据集上 MAE 到 4 岁以内、性别准确率 95% 左右是可以复现的。希望帮到你。本文还有配套的精品资源点击获取