
咱直接切入正题。今天想聊的这个项目是用 Python 基于 IMDB-WIKI 人脸数据集做年龄与性别预测。这东西在计算机视觉领域里算是入门级但又不失分量的话题你能拿它练手深度学习、熟悉图像分类与回归的整套流程也能直接改吧改吧扔到安防、零售分析、人机交互这些实际场景里跑。不过要提醒一句资料里说的是“无数据集”版本意味着整个流程里不附带现成的图片和标注文件数据得你自己想办法从公开渠道获取或者按格式准备这一点我会在后面实操部分专门讲怎么处理。这个项目适合谁刚学完 Python 基础、想碰一碰深度学习的同学或者已经在做 CV 但还没完整跑过一个“图像 - 表格输出”任务的朋友。它不像目标检测那么复杂也不像图像分类那样只有一个输出而是在一张图上同时给出“性别”和“年龄”两个维度涉及分类和回归两类任务这个混合架构设计本身就是个很好的训练点。我会从整体设计、环境搭建、数据预理解、模型选型、训练推理、常见坑位这几个角度展开尽量把每一步为什么这么做讲透而不是只给一段能跑的代码。1. 内容整体设计与思路拆解1.1 任务本质这不是一个单纯分类问题很多人第一次看到“年龄与性别预测”下意识就觉得这是两个分类任务性别男/女年龄分几个区间。但实际操作下来你会发现年龄部分如果直接分成几个档位比如少年、青年、中年、老年模型表现极不稳定原因很简单——人脸的年龄特征在相邻年龄段之间是平滑过渡的硬切成几类会导致边界样本大量误判。所以业界更常见的做法是性别用二分类年龄用回归或者把回归结果再映射到年龄段。IMDB-WIKI 数据集的特殊之处在于它是由两张公开数据组合而成的IMDB 电影演员照片和 Wikipedia 上的人脸图片总共超过 50 万张每张图都带着出生日期和拍照时间两者相减就能算出真实年龄。这个标注方式是它的核心优势因为不需要人工标注年龄天然就带“监督信号”。但也正因为如此它的下界质量不高——名人照片的拍摄时间、照片里的实际人物是否真的处于那个年龄都存在不少噪声这点在训练时要心里有数。1.2 技术选型的核心考量先说框架。PyTorch 和 TensorFlow 都能做但我个人更推荐 PyTorch原因不是它比 TF 强多少而是这个任务里你绕不开自定义数据加载和混合 Loss 这几件事PyTorch 的 eager mode 写起来更顺手调试时能直接 print 中间张量省很多时间。Keras 也可以但如果你想把年龄回归和性别分类用同一个网络头输出Keras 的函数式 API 写起来略绕。模型结构方面轻量级网络如 MobileNetV3、ShuffleNet 足够胜任在 CPU 上也能推理如果你手头有 GPU 并且想追求更好的精度ResNet50 是稳妥选择。我实测下来如果用 ResNet50 在 IMDB-WIKI 上微调性别准确率能到 95% 左右年龄平均绝对误差MAE大概在 6-8 岁区间。MobileNet 则会在性别上掉 1-2 个百分点但推理速度快一倍不止看你自己的算力权衡。2. 核心细节解析与实操要点2.1 数据预理解没有数据集时该怎么准备“无数据集”这三个字其实是个关键提醒。很多初学者的惯性思维是“网上找个 zip 解压就开始训练”但 IMDB-WIKI 原始数据的组织方式并不友好它提供的是两个大目录下的 .mat 文件Matlab 格式和一堆裁剪好的人脸图片图片文件名里带着出生日期和拍摄日期但解析规则对新人并不友好。我的建议是别直接用原始 .mat就算你读出来了标注信息和你图片文件名的对应关系也容易搞错。正确做法是先解析 .mat 里的元数据生成一个干净的 CSV 文件里面包含图片路径、性别、年龄三列。性别在 .mat 里是 0/1 标识0 代表女性1 代表男性注意这里不是常见的 male/female 字符串。年龄用拍摄年份 - 出生年份计算如果差值小于 0 或者大于 100说明元数据有误直接丢掉。如果没有下载完整 IMDB-WIKI 数据也可以自己构造一个简化版数据集从开源人脸数据集比如 UTKFace、CACD里凑一批带年龄标签的图片统一格式后也可以用。这个方案精度上限会低一些但能跑通整个流程对学习来说完全够用。2.2 数据清洗和预处理的关键细节人脸识别领域有句话叫“垃圾进垃圾出”IMDB-WIKI 尤其如此。这个数据集很多照片并不是标准的人脸正脸有些是合影、剧照、艺术照光照条件也很极端。直接丢给网络训练模型会学到一堆无关的噪声。所以预处理阶段有几步必须做第一步是人脸检测与对齐。最简单的方式是用 OpenCV 的 Haar Cascade 或者 DNN 人脸检测器将人脸区域裁出来统一缩放到 224x224 或 96x96。更进一步可以用 MTCNN 来检测关键点双眼、鼻尖、嘴角然后做相似变换把人脸摆正。这一步最能提升模型精度——同样是 ResNet50有过对齐的模型比没对齐的能提升 2-3 个百分点的准确率。第二步是过滤异常样本。原始数据里的标注噪声很大比如同一张照片里有多张人脸时标注的出生日期可能对应的是某个不相关的人。实操中我会按年龄做一遍分布统计把年龄小于 0、大于 100、性别缺失的样本全部剔除。还有一个容易被忽略的点很多明星照片的拍摄时间并不是当年的会在年龄标注上造成 3-5 年的偏差这个噪声是模型误差的上限来源不用强求但可以用 Loss 的 Huber 形式来降低异常值影响。第三步是数据增强。这一点能有效缓解数据量不足的问题随机水平翻转、随机亮度对比度调整、轻微旋转角度±10 度、随机裁剪再缩放回原尺寸。注意不要加上下翻转人脸倒过来没有物理意义。增强参数不要设太猛否则把眉毛鼻子都扭曲了模型会学疯。2.3 混合任务网络结构的设计思路这里我直接给出一个经过验证的结构不是唯一答案但至少能跑出不错的效果。主干网络可以是 ResNet18轻量或 ResNet50精度优先去掉最后全连接层保留全局平均池化后输出的特征向量假设维度是 512 或 2048。在这个特征向量基础上分出两个头性别分类头线性层特征维度 - 2接 CrossEntropyLoss年龄回归头线性层特征维度 - 1接 SmoothL1Loss也就是 Huber Loss为什么要共用主干两张脸的特征提取过程本来就是统一的底层特征都是边缘、纹理、形状只有到高层才分化出“性别相关特征”和“年龄相关特征”所以共用 backbone 几乎不会损失精度还能显著减少参数量训练更快。关于年龄为何不用 CrossEntropy 直接分 101 类这也是一个常见争论点。将年龄视为 101 个类别的分类可以避免回归平均化问题但代价是类别不均衡严重——IMDB-WIKI 里 20-40 岁样本占了大多数老人和孩子样本极少。我的经验是先用回归把模型训稳定再在推理阶段将连续年龄值映射到年龄段比如 0-2、3-9、10-19、20-29...这样比直接分类更稳。如果你偏要试分类也建议把原始年龄值以 5 岁为粒度聚成 20 类这样样本分布会更均匀。3. 实操过程与核心环节实现3.1 环境搭建与依赖安装这部分是很多 Python 初学者的第一道坎我把自己的标准环境列出来。操作系统 Windows 或 Linux 皆可但不推荐在 Mac 上跑完整训练除非你有 M 系列芯片 PyTorch MPS 后端否则训练慢到怀疑人生。Python 版本建议 3.8 到 3.10太新或太旧都可能遇到某些库没有预编译 wheel 的问题。核心依赖如下# 基础数值与数据处理 pip install numpy pandas # 深度学习框架 pip install torch torchvision # 图像处理与人脸检测 pip install opencv-python opencv-contrib-python pip install mtcnn # 实验记录与可视化 pip install matplotlib tensorboard # 进度展示方便观察训练 pip install tqdm scikit-learn这里我要专门说一个小坑mtcnn这个包是基于 TensorFlow 1.x 的如果你装的是 TensorFlow 2.x 或者压根没装 TF直接pip install mtcnn有时候会给你装一堆不兼容的依赖。我现在的做法是人脸检测直接用 OpenCV 的 DNN 模块加载一个res10_300x300_ssd_iter_140000.caffemodel不需要额外框架速度比 MTCNN 快精度足够。代码后续用这个方案演示。PyTorch 的安装建议去官网选对应的 CUDA 版本如果你是纯 CPU 环境直接执行pip install torch torchvision --index-url https://download.pytorch.org/whl/cpu别小看这一步很多人后面import torch报错就是因为装错了 CUDA variantGPU 检测不到就突然 fallback 到 CPU。安装完之后一定要手动验证一下。python -c import torch; print(torch.__version__); print(torch.cuda.is_available())如果你有 N 卡且驱动正确第二行应该输出True。输出False也别慌90% 的情况是 torch 版本和 CUDA 驱动不匹配去官网换一个匹配版本重装即可。3.2 数据解析从 .mat 到 CSV 的完整代码这一段是整个项目最容易被卡住的地方我把关键代码贴出来并标注每一步在干什么。import scipy.io as sio import pandas as pd import os import datetime # 假设你已经下载并解压了 IMDB-WIKI 数据集 # 其中 imdb.mat / wiki.mat 是元数据文件路径按需修改 mat sio.loadmat(data/wiki.mat) meta mat[wiki][0][0] # 关键字段解读这些都是 .mat 里常见的列名 dob meta[dob][0] # date of birth (datetime 格式的序列号) photo_taken meta[photo_taken][0] # 拍摄年份 full_path meta[full_path][0] # 图片相对路径 gender meta[gender][0] # 性别0女/1男有的样本为 nan records [] for i in range(len(full_path)): # 计算年龄 try: birth_date datetime.datetime.fromordinal(int(dob[i][0])) if not isinstance(dob[i][0], datetime.datetime) else dob[i][0] except Exception: continue age photo_taken[i][0] - birth_date.year if age 0 or age 100: continue if gender[i].size 0: continue # 缺失性别就丢掉 # 拼接图片完整路径 img_path os.path.join(data, full_path[i][0]) records.append([img_path, int(gender[i][0]), float(age)]) df pd.DataFrame(records, columns[path, gender, age]) df.to_csv(meta.csv, indexFalse) print(df.shape) print(df.head())这个脚本跑完后你会在同目录得到一个meta.csv。注意full_path字段在 .mat 里其实是相对路径的字符串数组你需要full_path[i][0]才能取到字符串我第一次写的时候在这个上面耗了半小时这种嵌套数组的结构是 Matlab 导出文件的经典特征。如果运行时报keyError: dob说明你的 .mat 字段名可能不同先在 Python 里print(mat[wiki][0][0].dtype)看一下字段列表按实际字段名改脚本。另外datetime.fromordinal的精度问题会导致个别样本年龄偏差极大所以后面那个age 0 or age 100的过滤一定要有。3.3 自定义 Dataset 和 DataLoader 的写法有了 CSV 文件接下来要写一个 PyTorch Dataset 类。这一步看似简单但处理不好的话训练到一半会莫名其妙报错“图片通道数不对”或者“标签维度不匹配”。我的写法如下import torch from torch.utils.data import Dataset import cv2 import pandas as pd import numpy as np class AgeGenderDataset(Dataset): def __init__(self, csv_path, img_size224, trainTrue, transformNone): self.df pd.read_csv(csv_path) self.img_size img_size self.train train self.transform transform def __len__(self): return len(self.df) def __getitem__(self, idx): row self.df.iloc[idx] img cv2.imread(row[path]) if img is None: # 图片读取失败时随便返回一张纯灰图避免训练中断 img np.zeros((self.img_size, self.img_size, 3), dtypenp.uint8) img cv2.cvtColor(img, cv2.COLOR_BGR2RGB) img cv2.resize(img, (self.img_size, self.img_size)) # 人脸检测 裁剪用 OpenCV DNN 或 Haar # 这里做最简处理直接缩放后训练后续可替换为检测对齐流程 if self.transform: img self.transform(img) # 格式转换HWC - CHW归一化 img torch.from_numpy(img.transpose(2, 0, 1)).float() / 255.0 # 或者你在 transform 里用 torchvision.transforms.Compose 统一做 gender_label torch.tensor(int(row[gender]), dtypetorch.long) age_label torch.tensor(float(row[age]), dtypetorch.float32) return img, gender_label, age_label需要注意cv2.imread读进来是 BGR 顺序如果不转 RGB模型可能学到一个颜色颠倒的“偏置特征”在测试集上翻车。另外torchvision.transforms支持的图像类型是PIL.Image或torch.Tensor对 numpy 数组支持有限所以代码里干脆手动完成归一化和通道变换避免混用。3.4 模型构建与训练脚本核心逻辑模型构建我是参照 PyTorch 官方预训练 ResNet 的写法用torchvision.models加载 backbone然后替换最后全连接层。import torch.nn as nn import torchvision.models as models class AgeGenderModel(nn.Module): def __init__(self, backboneresnet18, num_genders2): super().__init__() if backbone resnet18: self.backbone models.resnet18(pretrainedTrue) elif backbone resnet50: self.backbone models.resnet50(pretrainedTrue) in_features self.backbone.fc.in_features self.backbone.fc nn.Identity() # 去掉最后的分类层 self.gender_head nn.Linear(in_features, num_genders) self.age_head nn.Linear(in_features, 1) def forward(self, x): features self.backbone(x) gender_out self.gender_head(features) age_out self.age_head(features).squeeze(1) # 变成 [B] return gender_out, age_out这里pretrainedTrue能省很多训练时间因为 ImageNet 预训练模型已经学到了通用的图像特征你只需要微调高层即可。如果完全从零训练ResNet50 在这个数据集上可能要跑 50 个 epoch 才能收敛但预训练模型通常 10-20 个 epoch 就能达到一个不错的 PSNR。训练逻辑里有个容易忽略的点两个 Loss 的量纲不同。性别 Loss 是分类 CrossEntropy通常在 0.5-0.7 量级年龄 Loss 是回归误差可能动辄几十甚至上百。直接相加的话年龄 Loss 会主导梯度性别分类头基本学不到东西。这里有两个选择一是对两个 Loss 做加权total_loss gender_loss 0.1 * age_loss二是对年龄标签做标准化减均值除标准差让回归 Loss 降到和分类 Loss 一个量级。我推荐第二种因为权重参数需要反复调标准化更省心。具体训练循环大致如下import torch.optim as optim model AgeGenderModel(backboneresnet50) device torch.device(cuda if torch.cuda.is_available() else cpu) model.to(device) criterion_gender nn.CrossEntropyLoss() criterion_age nn.SmoothL1Loss() # Huber Loss对异常值更鲁棒 optimizer optim.Adam(model.parameters(), lr1e-4) scheduler optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max10) for epoch in range(epochs): model.train() for batch_idx, (images, gender_labels, age_labels) in enumerate(train_loader): images images.to(device) gender_labels gender_labels.to(device) age_labels age_labels.to(device) gender_out, age_out model(images) loss_g criterion_gender(gender_out, gender_labels) loss_a criterion_age(age_out, age_labels) loss loss_g 0.1 * loss_a # 或者用标准化后的 age label权重可设为 1.0 optimizer.zero_grad() loss.backward() optimizer.step() if batch_idx % 50 0: print(fEpoch {epoch} Batch {batch_idx} LossG {loss_g.item():.4f} LossA {loss_a.item():.4f})这里Adam的初始学习率我设为1e-4比 ImageNet 全量训练的1e-3要低因为预训练模型已经具备较好的特征表达学习率太大会把已有参数冲乱。如果你用的是从头训练的模型可以调回1e-3但收敛会明显变慢。CosineAnnealing 学习率调度的好处是前几个 epoch 保持较高学习率快速下降后面逐渐减缓在务实的训练里表现比 StepLR 稳定我建议直接用。3.5 评估指标与推理代码训练完模型后评估指标建议同时看性别Accuracy 足够年龄Mean Absolute Error (MAE) 和 /-5 岁准确率业界常用 soft accuracy 概念即预测值与真实值误差在 5 岁以内算正确推理阶段的完整代码import cv2 import torch import numpy as np from PIL import Image model AgeGenderModel(backboneresnet50) model.load_state_dict(torch.load(best_model.pth, map_locationcpu)) model.eval() # 假设输入一张图片路径 img_path test_face.jpg img cv2.imread(img_path) img cv2.cvtColor(img, cv2.COLOR_BGR2RGB) img cv2.resize(img, (224, 224)) # 如果做严格人脸检测可以先检测人脸框再裁剪这里简化为整图 img_tensor torch.from_numpy(img.transpose(2, 0, 1)).unsqueeze(0).float() / 255.0 # 通常在训练时也做了 ImageNet 均值和方差标准化 # 如果没有做这里直接 raw [0,1] 推理也是可以的但保持一致更佳 with torch.no_grad(): gender_out, age_out model(img_tensor) gender_pred torch.argmax(gender_out, dim1).item() age_pred age_out.item() gender_text Male if gender_pred 1 else Female print(fGender: {gender_text}, Age: {age_pred:.1f})注意这里如果训练时用了 torchvision 的 Normalizemean0.485, std0.229 等推理时也要用同样的参数对输入做标准化否则测试精度暴跌。很多人训练时头头是道一到推理就忘了这茬然后感叹“模型怎么这么差”。3.6 快速验证从一个小的子集开始训练我强烈建议你第一次跑通时不要直接上 50 万张图那是灾难。先从 CSV 里随机抽样 2000 张图训练 2-3 个 epoch确认代码能跑通、Loss 能下降、推理能输出结果再放完整数据集去训练。这一步能省你至少一天的时间。实操中我会写一个快速脚本sample_df df.sample(n2000, random_state42) sample_df.to_csv(sample_meta.csv, indexFalse)然后把 Dataset 路径改为sample_meta.csv把 epochs 设为 2看有没有报错、Loss 是否下降。能下降说明整个 pipeline 是通的接下来再换大数据集调参。4. 常见问题与排查技巧实录4.1 训练时 Loss 不降反而上升这个现象最常见的原因就是学习率太大。预训练模型微调时 lr1e-3 可能就已经太大了尤其用 Adam 时初始学习率超过 3e-4 就容易出现震荡。建议直接降到 1e-4甚至 5e-5。另一个原因是数据预处理不一致比如图像没有归一化到 [0,1] 而是 [0,255]输入分布剧烈变化会让网络难以收敛。检查一下你的数据 pipeline确保 train 和 eval 的预处理完全一致。4.2 GPU 利用率只有 20%训练慢得离谱这种情况多半是 DataLoader 的num_workers设得太低或者数据读取遇到瓶颈。建议num_workers4或 8Windows 下注意要写在if __name__ __main__保护下否则多进程会反复递归执行同时把pin_memoryTrue加上能减少 CPU 到 GPU 的传输时间。如果你已经开了多 worker 还是跑不满可以试试用 DALI 或提前把所有图片一次性读入内存缓存小数据集时非常有效。4.3 年龄预测平均偏差在 20 岁以上极大概率是你的数据清洗没做干净。IMDB-WIKI 里有大量出生日期对应不上的样本比如把电影里演员扮演的角色年龄算进去了。你需要多做一步用一个人脸检测器跑一遍所有原始图片把检测不到人脸或检测到的人脸面积占比太小的样本全部剔除。这一步能显著提升数据质量。另外一个隐蔽的坑是如果 CSV 中的年龄是浮点数直接用 float 做回归标签但你其实应该先 round 成整数因为真实年龄本来就是整数浮点误差会让模型徒增学习难度。4.4 推理结果性别总是偏向某一类数据不平衡是原因之一。IMDB-WIKI 数据集中男性样本数量明显多于女性所以你训练的模型天然会有偏向于预测男性的倾向。解决方法是计算样本权重在CrossEntropyLoss里传入weight参数给少数类更高的惩罚权重。具体计算可以用sklearn.utils.class_weight.compute_class_weight得到。另一个可能是你的模型结构里gender_head后面没有加softmax但这不会影响 argmax 结果所以重点是权重问题。5. 实际经验分享这个项目还能往哪里延伸如果你把上面这套完整跑下来实际上你已经具备了一个非常通用的“人脸属性分析”框架。性别和年龄只是两个最简单的维度你可以用完全一样的架构换成“情绪识别”表情分类、“是否戴眼镜”、“肤色类型”等任务。我后来就在这个代码基础上把gender_head换成了 7 分类的emotion_head很快就训出了一个能用的表情识别模型整个迁移成本不到半天。另外一个值得尝试的改进方向是“年龄分布感知的标签平滑”。因为年龄标注本身有噪声直接回归到唯一值会让模型过度拟合错误样本。你可以把年龄标签做成一个高斯分布让模型去预测一个“年龄分布概率”而非单一数值这样对噪声的鲁棒性会明显增强。这一思路在很多学术论文里都有验证如 “Deep EXpectation” 方法实操上也不算复杂——损失函数从 SmoothL1 改成 KL Divergence 即可。当然这种方式训练和推理都略复杂适合你已经把基线跑通之后再去挑战。就我个人的操作习惯而言做完一个项目之后我会把整个训练好的模型、CSV 生成脚本、训练日志、评估曲线都归档到一个文件夹里标注好数据集版本、预处理方式、最终指标。原因很现实——这个项目三个月后你自己回来看代码很可能想不起来当时用了哪种对齐方式、哪个学习率。把这些关键参数写进 README不是为了给别人看是为了给未来的自己省时间。如果你的学习路线是“Python 基础 - OpenCV 图像处理 - PyTorch 深度学习”那这个 IMDB-WIKI 项目基本属于第一道综合应用题能帮你把前面学的所有知识串起来更像是搭积木过程中真正拧上第一颗螺丝。迈过这一步后面再碰检测、分割这类更复杂的任务你的心理阈值会低很多因为套路基本上都是一样处理好数据、搭好模型、设计好 Loss、跑起来调试。