简介:一份基于Python机器学习的年龄与性别预测项目资源,整合了完整源码与配套数据集,面向人工智能、数据科学等计算机相关专业的学生与开发者,适用于课程设计、毕业设计或入门进阶实践。项目围绕年龄和性别分类任务展开,覆盖数据构建、模型训练、测试评估与部署预测等环节,并附带有可用的预训练模型文件、配置参数,以及Windows环境安装MXNet GPU版本的常见问题说明,能有效降低环境配置门槛。全包共36个文件,以Python脚本为主(20个),另有模型缓存、配置文件、训练日志与说明文档,整体仅34KB,轻量易用,便于快速启动与二次开发。已有40人学习下载。目录结构清晰,包含数据预处理、标签映射、模型定义、测试与可视化等辅助脚本,有助于完整理解深度学习实现链路,也可作为毕设或课程设计的实用参考。
1. 年龄与性别预测:为什么一个模型要做两个任务
年龄预测的准确率会比性别准确率低一大截。同样是卷积网络,性别二分类在 Adience 上通常能到 93% 左右,而年龄八分类常见只到 55%~60%。这不是模型容量不够,而是年龄标签本身是回归问题,强行离散化成年龄段后,相邻类别的特征边界非常模糊。这个基于 Python 机器学习的年龄和性别预测项目,选择把两个任务塞进同一个多输出网络里,用 MXNet 的 gluon 接口实现,源码完整,含训练日志和标签映射文件,解压后改成英文路径就能跑。一个deploy.py调进去,人脸图进去,性别和年龄段同时出来。适合课程作业、毕设起步,也适合作为深度学习入门的第一个可复现项目。
2. 源码清单与 build_dataset.py 数据预处理:从 Adience 标注到 cpickle 标签映射
这套源码解压后是一堆.py和.cpickle文件,看起来杂乱,但功能边界很清晰。先搞懂每个文件在管道里的位置,后面改代码才不会迷路。
2.1 项目文件与职责
| 文件 | 实际职责 |
|---|---|
build_dataset.py | 解析 Adience 标注 CSV,生成标签编码和训练样本列表 |
mxagegendernet.py | 定义多输出卷积网络,年龄头 + 性别头 |
age_gender_config.py | 集中管理路径、批大小、学习率、训练轮数 |
train.py | 训练入口,加载数据、构建网络、执行前向和反向 |
mxmetrics.py | 自定义验证指标,比如每个年龄段各自的准确率 |
test_accuracy.py | 在验证集上计算整体准确率和单类准确率 |
test_prediction.py | 单张图片推理,输出性别和年龄段文本 |
age_gender_deploy.py | 部署用的封装模块,被外部调用 |
agegenderhelper.py | 公共工具函数,比如路径处理、模型加载、图像预处理 |
age_le.cpickle | 年龄段字符串到索引的映射 |
gender_le.cpickle | 性别字符串到索引的映射 |
age_adience_mean.json | 训练集年龄分布或均值数据,用于归一化或加权 |
可以看到,数据流是build_dataset.py产出.cpickle,train.py消费它们,test_prediction.py再反查它们把索引还原成文本。这个设计很朴素,但利于学,也利于二次开发。
2.2 标签解析:年龄区间不是连续值
Adience 数据集的 age 字段不是整数,而是字符串区间,比如25-32、38-43,还有(0, 2)或(8, 12)这种括号格式。性别字段只有m和f。直接把字符串喂给网络是不行的,常见做法是拆成闭区间后映射成类别索引。
import pandas as pd import pickle # 8 个年龄段与 Adience 官方口径一致 AGE_BINS = [(0, 2), (4, 6), (8, 12), (15, 20), (25, 32), (38, 43), (48, 53), (60, 100)] AGE_INDEX = {f"{lo}-{hi}": i for i, (lo, hi) in enumerate(AGE_BINS)} GENDER_INDEX = {"m": 0, "f": 1} def parse_adience_label(row): age_str = str(row["age"]).strip() # 部分标注写成 (0, 2) 或 (8, 12),统一成 "0-2" if age_str.startswith("("): lo, hi = age_str.strip("()").split(",") age_str = f"{int(lo)}-{int(hi)}" if age_str not in AGE_INDEX or row["gender"] not in GENDER_INDEX: return None age_label = AGE_INDEX[age_str] gender_label = GENDER_INDEX[row["gender"]] return age_label, gender_label def build_label_csv(csv_path): df = pd.read_csv(csv_path) # 只保留检测到人脸的样本 df = df[df["hasFace"] == True] records = [] for _, row in df.iterrows(): label = parse_adience_label(row) if label is None: continue records.append({ "image": f'{row["user_id"]}/landmark_aligned_face.{row["face_id"]}.jpg', "age_label": label[0], "gender_label": label[1], }) return recordsAGE_BINS的区间是 Adience 论文里的标准 8 分组,很多复现项目都沿用这组边界。AGE_INDEX生成的是"25-32": 4这种映射,训练时网络输出的 8 维 softmax 实际上对应年龄段索引,而不是回归一个具体的年龄值。这一步里最容易出错的是去重逻辑:Adience 里同一张脸可能有多条标注,同一个face_id只保留一条即可,否则训练集和验证集会交叉污染。
2.3 运行数据构建命令
这里不生成原始图片,只生成索引和标签映射文件。项目里的age_le.cpickle和gender_le.cpickle就是上一步的AGE_INDEX和GENDER_INDEX序列化结果。
python build_dataset.py \ --csv age_gender.csv \ --data_dir /data/Adience \ --output ./train.lst--data_dir是 Adience 原图所在的父目录,里面有很多用户 ID 文件夹;--output是生成的样本列表,每行是图片路径 \t age_label \t gender_label。跑完后用head -5 train.lst检查路径是否真实存在,这一条能拦掉大部分路径拼接问题。
我拿到的这套源码里,build_dataset.py还顺带把年龄均值和性别均值写成了 JSON,作用是部署时做减均值操作。MXNet 的 gluon 数据变换里没有自动做 Adience 均值的内置实现,所以项目在配置里直接用 JSON 文件里的值做transforms.Normalize,这个细节容易被忽略,但直接影响验证集准确率。
3. mxagegendernet.py 多输出卷积网络与 train.py 训练参数调优
网络部分是这个源码的核心,也是和二分类项目差异最大的地方。它不是两个独立网络,而是共享卷积特征层,最后分叉成两个全连接输出头。
3.1 共享骨干 + 双分类头的结构
这种设计的动机很直接:性别和年龄都依赖人脸纹理、轮廓、皮肤状态等底层特征,共享 frontend 可以减少一半参数量,也让梯度同时更新特征层,对性别相对有利,对年龄有正则化作用。代码里用的是 ResNet18 或类似的小型骨干。
from mxnet.gluon import nn from mxnet.gluon.model_zoo.vision import resnet18 class AgeGenderNet(nn.HybridBlock): def __init__(self, num_age=8, num_gender=2): super().__init__() # 共享特征提取器,去掉 ResNet 自带的全连接分类层 self.features = resnet18(pretrained=True) self.features.classifier = None # 性别头 self.gender_head = nn.HybridSequential() self.gender_head.add(nn.Dense(32, activation="relu")) self.gender_head.add(nn.Dense(num_gender)) # 年龄头 self.age_head = nn.HybridSequential() self.age_head.add(nn.Dense(128, activation="relu")) self.age_head.add(nn.Dense(num_age)) def hybrid_forward(self, F, x): feat = self.features(x) gender_out = self.gender_head(feat) age_out = self.age_head(feat) return gender_out, age_outhybrid_forward返回两个输出,顺序是性别在前、年龄在后。这个顺序由训练时的 loss 计算决定,部署时也要保持一致。age_head中间层维度比gender_head大,因为年龄类别区分度低,需要更高维的中间表示。ResNet18 的最后卷积输出是 512 维,接两个头都是够用的。
3.2 损失函数组合与训练循环
两个任务都是分类问题,所以都用 softmax cross entropy。年龄性能低于性别,可以在 loss 加权时给年龄更大的权重。不过这个项目默认是 1:1,想要调优可以按2 * age_loss + gender_loss试。
import mxnet as mx from mxnet import gluon, autograd net = AgeGenderNet() softmax_loss = gluon.loss.SoftmaxCrossEntropyLoss() trainer = gluon.Trainer(net.collect_params(), "adam", {"learning_rate": 0.001}) for epoch in range(60): for data, gender_label, age_label in train_loader: with autograd.record(): gender_out, age_out = net(data) loss_gender = softmax_loss(gender_out, gender_label) loss_age = softmax_loss(age_out, age_label) total_loss = loss_age + loss_gender total_loss.backward() trainer.step(data.shape[0])trainer.step传入的是批量大小,而不是 batch 内样本数手动除,MXNet 会自动按 batch size 归一化梯度。这里要注意autograd.record()必须包住两个输出的 loss,只 record 其中一个会导致另一个头的梯度为空,参数不更新。
3.3 训练超参数与运行命令
训练入口是train.py,超参集中在age_gender_config.py里。下表是这套源码里比较稳妥的一组:
| 超参数 | 推荐值 | 选型理由 |
|---|---|---|
| 输入尺寸 | 224x224 | ResNet 标准输入,过小损失细节,过大显存占用高 |
| 批大小 | 64 | 8GB 显存够用,显存小就降到 32 |
| 优化器 | Adam | 收敛快,避免 adam 调 lr 的坑 |
| 初始学习率 | 1e-3 | 适用迁移学习 |
| 学习率衰减 | 60 轮时降到 1e-4 | 后期微调分类头 |
| 训练轮数 | 60 | Adience 约 1.5w 样本,60 轮能收敛 |
| 数据增强 | 随机裁剪 + 水平翻转 | 人脸存在对称性,翻转是安全的 |
启动训练:
python train.py --config age_gender_config.py --gpu 0如果没有 GPU,就把配置里的ctx改成mx.cpu()。CPU 上训练 60 轮会很慢,预期 4~6 小时,GPU 只要 20 分钟左右。训练过程中日志会打印每个 epoch 的 loss 和验证集准确率,这个日志被保存为training_0.log,后面分析过拟合要看它。
有个容易犯错的地方:预训练模型的features层如果同时练,会发现前期 loss 降得很快,但验证集准确率反而偏低。我一般会先把前面几层 learning rate 设为全局的十分之一,或者先冻结features,只训练两个 head,等 head 收敛后再解冻全层。项目源码没有这个选项,但你可以在train.py里用net.features.collect_params().setattr('lr_mult', 0.1)做到。
4. test_accuracy.py 验证与 test_prediction.py 部署推理的完整链路
训练完的模型文件包括符号图、参数、标签映射三个部分。验证和部署的差异只在于是否遍历全部验证数据,以及最终输出是文本还是逗号分隔的准确率。
4.1 验证集准确率计算
test_accuracy.py的逻辑是加载训练好的mxagegendernet网络,把 checkpoint 参数灌进去,然后在验证集上跑一轮预测,计算整体准确率和每个年龄段、每个性别的准确率。注意这里不能直接复用训练时的 DataLoader,因为验证集不需要数据增强,且 batch 内的排序不需要 shuffle。
from mxnet import gluon, nd from mxagegendernet import AgeGenderNet def evaluate(net, val_loader, ctx): top1_gender = 0.0 top1_age = 0.0 total = 0 for data, gender_label, age_label in val_loader: data = data.as_in_context(ctx) gender_out, age_out = net(data) gender_pred = nd.argmax(gender_out, axis=1) age_pred = nd.argmax(age_out, axis=1) top1_gender += nd.sum(gender_pred == gender_label).asscalar() top1_age += nd.sum(age_pred == age_label).asscalar() total += len(gender_label) return top1_gender / total, top1_age / totalnd.argmax默认返回扁平索引,axis=1表示按每个样本的输出通道取最大。这里的gender_label和age_label是整型张量,在 Adience 验证集上这个代码能直接把两个预测结果对齐到样本维度,不会出现 batch 内错位。
4.2 部署推理:从图片到文本标签
部署时真正需要的文件只有四个:age_gender_deploy.py、age_gender_deploy.py生成的模型(实际上是通过 checkpoint 保存的符号和参数)、age_le.cpickle、gender_le.cpickle。在源码里,test_prediction.py是部署流程的骨架。
import pickle import numpy as np import mxnet as mx from mxnet import nd import cv2 def load_label_encoder(path): with open(path, "rb") as f: return pickle.load(f) def preprocess_image(image_path, input_size=224): img = cv2.imread(image_path) img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) img = cv2.resize(img, (input_size, input_size)) img = img.astype("float32") / 255.0 # 用 Adience 均值归一化 mean = np.array([0.4385, 0.4049, 0.3763]) img = img - mean img = img.transpose(2, 0, 1) img = np.expand_dims(img, axis=0) return nd.array(img) def predict_face(image_path, sym, arg_params, aux_params, age_le, gender_le): data = preprocess_image(image_path) executor = sym.simple_bind(ctx=mx.cpu(), data=data.shape) executor.copy_params_from(arg_params, aux_params) executor.forward(is_train=False, data=data) gender_out = executor.outputs[0].asnumpy() age_out = executor.outputs[1].asnumpy() gender_idx = int(np.argmax(gender_out[0])) age_idx = int(np.argmax(age_out[0])) gender_text = [k for k, v in gender_le.items() if v == gender_idx][0] age_text = [k for k, v in age_le.items() if v == age_idx][0] return gender_text, age_textpreprocess_image里三个关键点:颜色通道要先 BGR 转 RGB,因为模型是在 RGB 的图像上训练的;归一化用的是 Adience 数据集的像素均值,不是 ImageNet 均值;最后要加一个 batch 维度,simple_bind会严格校验数据 shape,漏掉np.expand_dims一定会报维度错误。
如果项目里模型是用 gluon 的save_parameters保存而不是save_checkpoint,还要改加载方式为net.load_parameters。从源码自带文件名推测,它用的是 MXNet 经典的save_checkpoint三件套:-symbol.json、-0000.params、-0000.nd。
4.3 单张图片预测的运行命令
python test_prediction.py --model_prefix age_gender_model --epoch 60 --image test_face.jpg输出常见是Predicted: male, 25-32这种格式。这一步最容易出问题的是测试图片里人脸不是正脸,或者检测框不准。源码没有内置人脸检测,所以我一般建议先用 OpenCV 的 DNN 人脸检测器跑一个裁剪框,再交给test_prediction.py。否则模型在非人脸图上输出没有意义,而且 softmax 概率往往接近均匀分布,从分布形状就能看出来输入不对。一个简单验证方式:打印age_out[0]的原始 logits,如果最大值和次大值差不大于 0.2,基本可以判定输入不是人脸。
5. 训练日志解读与年龄分类不平衡的排错技巧
training_0.log是训练过程中保存下来的标准输出,格式通常是Epoch[0] Train-accuracy=0.83 Valid-accuracy=0.58。很多人只看整体准确率,但这个项目必须分开看年龄和性别各自的表现,否则会掩盖问题。
5.1 从日志判断过拟合发生在哪个头
看前 10 个 epoch:性别准确率可能迅速爬到 0.9,年龄还在 0.4 附近,这是正常的,两个任务难度不同。如果第 30 轮后性别验证准确率不再变化,但年龄验证准确率小幅波动,就说明性别头已经收敛,年龄头还在学习困难样本。如果此时训练准确率已经接近 1.0 而验证只有 0.6,说明整体过拟合。常见的处理是给gender_head加 dropout,或者把features层冻结起来,只训两个分类头。
年龄类别不均衡在日志里的表现是:整体年龄准确率被 25-32 这类大样本主导,而 0-2 和 60-100 的准确率可能只有 20%。Adience 数据集中 25-32 年龄段样本量接近 0-2 的五六倍,网络会把所有不确定样本都推向高频类别。针对这一点,我会在 loss 里对年龄类别按样本量倒数加权:
from collections import Counter import numpy as np age_count = Counter(all_age_labels) total = sum(age_count.values()) age_weight = np.array([total / age_count[i] for i in range(8)], dtype="float32") age_weight = age_weight / age_weight.sum() # 训练时传给 MXNet Loss 的 sample_weight 参数 loss_age = softmax_loss(age_out, age_label, sample_weight=age_weight)权重只作用在年龄分支,性别分支不引入额外的类别权重。sample_weight的 shape 要跟 label 一致,或者能被广播成[batch_size, 1],否则 mxnet 会报错说维度不匹配。这个技巧能直接提升 0-2 和 60-100 的召回率,但会把 25-32 的准确率拉低 2~3 个百分点,业务上更关心整体准确率时谨慎使用。
5.2 验证集分布漂移的坑
项目里的age_adience_mean.json是训练集的年龄分布均值。如果下载的数据集和原版 Adience 不是同一个版本,或者训练前手动过滤了大量图片,这个均值就不再准确。最简单的自查方法是统计验证集 age 索引,和 json 里每个年龄段比例对比,相差超过 5% 就要重新生成训练列表。
另外,原始数据中同一张人脸出现多次,比如同一个人在不同照片里的脸,如果都进训练集,会导致模型记忆人物身份而非年龄特征。我在做数据拆分时一般按user_id分组,保证同一人的所有照片只出现在训练集或验证集中,否则测试准确率虚高 8% 以上。
5.3 Windows 下 MXNet GPU 安装的边界条件
项目附带了一个mxnet的gpu版本windows安装问题.txt,说明作者也踩过坑。Windows 下pip install mxnet-cu101这类版本包常见的失败原因是 CUDA 和 cuDNN 版本不匹配,或者 Visual Studio 编译环境缺失。建议先只用 CPU 版把流程跑通再换 GPU:pip install mxnet,然后在age_gender_config.py里设置ctx = mx.cpu()。CPU 推理一张 224x224 图片大约 200 毫秒,测试功能足够。等人脸数据规模大了再考虑 Docker 或 WSL 里的 Linux GPU 环境,这是性价比最高的路径。
要看年龄预测是否真的提升了,可以把几个难分的年龄区间合并,比如把 15-20 和 25-32 合并成“青年”,如果合并后准确率明显提高,说明网络学到的年龄排序特征是对的,只是区间边界定义太细了。这个结论可以直接指导你改写AGE_BINS,而不是盲目加深网络。
本文还有配套的精品资源,点击获取