Python深度学习实战:IMDB-WIKI年龄性别预测全流程 📅 发布时间:2026/9/4 14:55:45 👁 浏览次数: 我接手这个项目时最深的感触是年龄和性别预测看起来是计算机视觉里的老面孔真要做起来却处处是坑。IMDB-WIKI 数据集本身质量参差不齐文件结构复杂网上大量教程停留在“研究背景 ResNet 网络结构”的阶段一落地就卡壳。这篇博文就把我的完整思路和实践过程拆开写清楚——从数据集下载与清洗、环境搭建、模型加载与训练到测试阶段遇到的各类问题围绕 Python 和 IMDB-WIKI 数据集这条主线逐步展开。无论你是刚接触人脸属性分析的新手还是想快速跑通一个可演示项目的开发者这篇内容都能提供直接可参考的实操路径。1. 项目准备与环境搭建IMDB-WIKI 数据集的获取和常见阻碍1.1 “无数据集”这三个字的真实含义项目标题里写明“无数据集”这其实意味着我们需要自己去获取原始数据。IMDB-WIKI 数据集由两个来源组成IMDB 电影演员照片和 WIKI 百科人物照片官方发布的压缩包包含图片、标签文件、Matlab 格式的标注文件等。实际动手之前你要搞清楚一个情况这个数据集并不提供统一的图片包下载而是提供图片链接列表官方脚本再根据链接批量抓取。很多链接随时间失效数据不完整几乎是必然的。我在准备阶段统计过原始数据中有相当比例的图片早已无法下载替代方案要么是找第三方镜像打包好的完整图片集要么接受缺失并用现有图片训练。整理之后我使用约 26 万张可用图片其中带有较清晰年龄标签的约占八成。年龄和性别预测模型对数据量的敏感度很高如果只拿到几万张图训练效果会明显下降尤其是年龄分布不均匀的情况下模型会偏向样本量多的年龄段。1.2 环境选用Python 版本、深度学习框架与硬件配置这个项目我使用的核心环境如下给各位一个直接参考组件推荐版本备注Python3.8 或 3.9兼容性最稳3.10 以上个别依赖容易出问题PyTorch1.10~2.0本项目的代码使用 PyTorch 生态torchvision0.11~0.15与 PyTorch 版本对应OpenCV4.5 以上主要用于图片读取和预处理NumPy1.21 以上数据处理基础库Matplotlib3.5 以上可视化训练曲线和预测结果GPU 驱动CUDA 11.3无 GPU 也能跑但训练会慢很多安装时最容易卡的坑是 torch 和 torchvision 版本不一致导致无法调用 CUDA建议用官方 pip 命令统一安装不要分开装最新版。1.3 数据集目录结构与标注文件解析拿到 IMDB-WIKI 数据集后目录内主要有以下内容imdb.mat和wiki.mat包含性别、年龄、图片路径、脸部矩形框坐标等标注信息按人名或 ID 组织的图片文件夹官方提供的下载脚本如果选择自己抓图。解析mat文件是第一步这里我直接给出可用的加载代码import scipy.io import numpy as np def load_mat(file_path): data scipy.io.loadmat(file_path) meta data[imdb][0, 0] # 若读取wiki.mat改成data[wiki][0, 0] dob meta[dob][0] # 出生日期 photo_taken meta[photo_taken][0] # 拍摄年份 full_path meta[full_path][0] # 图片路径 gender meta[gender][0] # 0为女1为男 face_score meta[face_score][0] # 人脸检测分数 face_location meta[face_location][0] # 人脸矩形框 records [] for i in range(len(dob)): if face_score[i] 1.0 and not np.isnan(dob[i]) and not np.isnan(gender[i]): age photo_taken[i] - dob[i] if 0 age 100: records.append({ path: str(full_path[i][0]), age: float(age), gender: int(gender[i]), face_location: face_location[i][0] if len(face_location[i]) 0 else None }) return records为什么设置face_score 1.0这个阈值因为数据集中很多图片人脸检测分数很低不是正脸或模糊不清硬塞进训练集只会增加噪声。也有不少dob或gender字段是 NaN这类记录需要过滤掉。年龄计算用拍摄年份减去出生年份默认得到的是周岁整数。2. 数据清洗与人脸预处理决定模型效果的上游环节2.1 过滤、裁剪与归一化流程拿到原始标注后不能直接交给模型。需要先根据face_location把人脸区域裁剪出来同时做适当的边缘扩展避免人脸刚好顶满边界导致特征缺失。扩展比例一般取 0.2 到 0.3两侧补齐之后再做缩放和归一化。数据集中有些图片是从电影剧照里裁剪出来的画质和光线差异非常大。年龄预测模型对输入图像的分辨率要求并不算极端但稳定缩放到 224×224 或 256×256 是比较常见的选择。如果为了速度和显存考虑也可以把输入尺寸降到 160×160实测精度损失在可接受范围内。人脸裁切这一步要注意图片是 BGR 还是 RGB。OpenCV 默认读入的顺序是 BGR而 PyTorch 预训练模型是基于 RGB 训练的读图后用cv2.cvtColor(img, cv2.COLOR_BGR2RGB)做个通道转换否则颜色信息是反的。这个细节不急的话不会立刻暴露但会稳定拉低模型精度。2.2 年龄标签的处理策略IMDB-WIKI 的年龄标签噪声非常大主要体现在三点出生日期精确到年和拍摄年份相减会带来最多 1 岁的误差部分演员角色年龄与实际年龄不同剧照数据天然有噪声老人和儿童的样本量相对较少类别分布不平衡。年龄建模有两种常见策略作为回归问题输出一个连续值和作为分类问题按年龄段分桶。直接回归年龄模型常会在中位数附近“躺平”预测结果偏保守按年龄分桶也有问题比如相邻年龄段边界处容易混淆。我在项目中把年龄分成 0-2、3-6、7-12、13-19、20-30、31-45、46-60、60 以上共 8 个区间每个区间内的样本重新加权让年龄段之间的数量差异不至于压扁网络的学习重心。2.3 性别标签重编码与样本均衡性别标记在源数据里本身是 Binary 类型直接转成int即可0 表示女性、1 表示男性。训练时常用CrossEntropyLoss或带权重的二分类损失函数。IMDB-WIKI 中男女比例大概是 55:45没有到严重失衡的程度所以不额外做重采样也能跑。但如果你的清洗逻辑把很多女性样本筛掉了就需要考虑对少数类别加权。我的处理方式是统计清洗后的性别分布不手动改数据只在 Loss 里给少数类乘上系数计算方式很简单weights torch.tensor([female_count / total, male_count / total]) criterion torch.nn.CrossEntropyLoss(weightweights)这样比直接复制少数类样本更平滑也不容易过拟合。2.4 数据增强的有效性与尺度问题人脸年龄预测对几何形变非常敏感。过度旋转或拉伸会让人脸比例失衡年龄特征被破坏。我实际验证下来比较可靠的数据增强手段包括水平翻转人脸对称不影响年龄判断轻微随机旋转±8 度以内亮度、对比度扰动随机裁切。不建议对年龄模型使用过强的颜色抖动因为肤色和发色虽然对性别判断有影响但对年龄判断属于边缘信息过度扰动会导致颜色通道成为干扰项。尺度方面IMDB-WIKI 人脸框的尺寸差异很大有的对得紧有的带大片额头或背景干脆统一在裁切时做一次中心对齐再裁剪为正方形区域缩放至输入尺寸。3. 模型构建与训练从预训练权重到多任务输出的完整实现3.1 为什么用预训练网络而非从头训练IMDB-WIKI 数据集虽然有几十万张图片但和 ImageNet 那种千万级通用数据集相比仍然不够大而且标签噪声高。年龄和性别预测任务需要的底层特征——眼睛、嘴巴、皮肤纹理、发际线——在 ImageNet 预训练模型中已经具备较强的表达能力。因此最佳方案是加载预训练的 ResNet 或 EfficientNet替换掉最后的全连接层同时输出性别分数和年龄预测。实际测试中ResNet-50在精度和推理速度之间相对平衡EfficientNet-B4精度更高但训练更慢。显存不够时优先用 ResNet-50可以在 backbone 之后接入一个全局平均池化再分两条分支一条做性别二分类一条做年龄回归或年龄段分类。这种多任务结构让两个任务共享人脸特征提取器比单独训练两个模型节省大量计算资源也能互相提供正则效果。3.2 网络结构实现代码这里给出一个可直接运行的 PyTorch 模型定义同时输出年龄和性别import torch import torch.nn as nn import torchvision.models as models class AgeGenderNet(nn.Module): def __init__(self, num_age_classes8, backboneresnet50): super(AgeGenderNet, self).__init__() if backbone resnet50: self.backbone models.resnet50(pretrainedTrue) in_features self.backbone.fc.in_features self.backbone.fc nn.Identity() else: raise ValueError(Unsupported backbone) self.age_classifier nn.Sequential( nn.Linear(in_features, 512), nn.BatchNorm1d(512), nn.ReLU(inplaceTrue), nn.Dropout(0.3), nn.Linear(512, num_age_classes) ) self.gender_classifier nn.Sequential( nn.Linear(in_features, 256), nn.BatchNorm1d(256), nn.ReLU(inplaceTrue), nn.Dropout(0.2), nn.Linear(256, 2) ) def forward(self, x): feat self.backbone(x) age_out self.age_classifier(feat) gender_out self.gender_classifier(feat) return age_out, gender_outBatchNorm1d放在全连接层之间非常关键尤其是两个分支共享同一个特征向量时它能避免内部协变量偏移让训练更稳。3.3 训练超参数设置与个人经验训练时我最常用的配置是这样的参数数值说明优化器Adam初始学习率 1e-4权重衰减 1e-4学习率调整StepLR每 5 个 epoch 乘以 0.5Batch Size128输入 224×2248G 显存合适Epoch25过大会过拟合过小精度不够年龄损失权重1.0与性别损失相加性别损失权重0.5性别任务简单降权重避免干扰年龄为什么不把两个 Loss 都设为 1.0性别分类任务的收敛速度远快于年龄预测若权重一致性别梯度会占主导导致共享 backbone 对年龄特征的表达被压制。把性别损失权重降到 0.5或者加入年龄损失的在线困难样本挖掘逻辑模型对年龄段的敏感度会有明显提升。训练过程中我还发现一个常规教程很少提及的现象直接用pretrainedTrue的 ResNet 加载后会有一个短暂的“退化期”前 1-2 个 epoch 验证集精度反而低于随机初始化这是因为网络内部 BatchNorm 统计量还在适应人脸数据分布。遇到这种情况不需要调参继续训练第三四个 epoch 后会自然回升。3.4 完整训练循环与模型保存下面是一个实用的训练循环框架包含早停逻辑和最优模型保存from torch.utils.data import DataLoader, Dataset import torch.optim as optim import os class FaceDataset(Dataset): def __init__(self, records, transformNone): self.records records self.transform transform def __len__(self): return len(self.records) def __getitem__(self, idx): item self.records[idx] image cv2.imread(item[path]) if image is None: return self.__getitem__((idx 1) % len(self.records)) image cv2.cvtColor(image, cv2.COLOR_BGR2RGB) if item[face_location] is not None: x, y, w, h item[face_location] margin int(max(w, h) * 0.2) x1 max(0, int(x) - margin) y1 max(0, int(y) - margin) x2 min(image.shape[1], int(x w) margin) y2 min(image.shape[0], int(y h) margin) image image[y1:y2, x1:x2] if self.transform: image self.transform(image) age_label torch.tensor(item[age_bucket], dtypetorch.long) gender_label torch.tensor(item[gender], dtypetorch.long) return image, age_label, gender_label训练主循环中除了常规的前向反向过程还要记录每个 epoch 的年龄分类准确率和性别分类准确率def train_one_epoch(model, dataloader, optimizer, criterion_age, criterion_gender, device): model.train() total_age_loss 0.0 total_gender_loss 0.0 correct_age 0 correct_gender 0 total 0 for images, age_labels, gender_labels in dataloader: images images.to(device) age_labels age_labels.to(device) gender_labels gender_labels.to(device) optimizer.zero_grad() age_out, gender_out model(images) loss_age criterion_age(age_out, age_labels) loss_gender criterion_gender(gender_out, gender_labels) loss loss_age 0.5 * loss_gender loss.backward() optimizer.step() total_age_loss loss_age.item() * images.size(0) total_gender_loss loss_gender.item() * images.size(0) _, age_pred torch.max(age_out, 1) _, gender_pred torch.max(gender_out, 1) correct_age (age_pred age_labels).sum().item() correct_gender (gender_pred gender_labels).sum().item() total images.size(0) avg_age_loss total_age_loss / total avg_gender_loss total_gender_loss / total age_acc correct_age / total gender_acc correct_gender / total return avg_age_loss, avg_gender_loss, age_acc, gender_acc模型保存时不要只存 state_dict建议把年龄类别映射、训练参数一并记录下来避免验证阶段要重新推导中间产物torch.save({ model_state_dict: model.state_dict(), age_bucket_list: age_bucket_list, input_size: 224, }, age_gender_model.pth)4. 训练过程中的常见问题与排查实录4.1 第一个大坑全量数据直接进内存导致 OOMIMDB-WIKI 解压缩后图片占几十 GB如果一口气把所有图片读入内存做原始数据矩阵16G 内存机器直接崩。正确做法是自定义Dataset每轮按路径读取配合DataLoader的num_workers利用多进程预读取。网络上很多二手代码把数据一次性读成numpy数组再开始训练这在人脸数据集上基本跑不通。如果显存不足除了调小batch_size还可以选择冻结 backbone 的前几层。前期我只训练最后的分类层等验证集精度不再上升时再解冻 backbone 的后两个 Stage 做微调。这种方式能大幅降低显存占用还能减少预训练特征被破坏的概率。4.2 第二个大坑验证集 Loss 与测试集表现严重不符训练时模型在验证集上的年龄分类准确率能到 58% 左右但一到真实照片上明显偏低。根源在于验证集和训练集都来自 IMDB-WIKI人脸框裁剪方式和背景分布高度相似而真实场景中的自拍、侧脸、遮挡等因素在训练集里占比有限。解决思路是在验证集上额外加一条“真实照片评估线”手动找 20-30 张网络照片或自拍用同一套预处理流程做评估实时观察模型在新域上的表现。做年龄预测项目尤其在公开数据集上跑模型时这一步不能省。否则模型只是记住了数据集的风格而不是真正学会了人脸年龄特征。4.3 第三个大坑年龄极值段的预测塌陷我早期训练出的模型对 20-40 岁区间的预测比较准超过 60 岁和小于 10 岁的样本输出几乎全部集中到中间年龄段。这种现象是回归目标和分类目标都会出现的“均值回归”效应噪声标签越多模型越倾向于保守预测。后来我把年龄分类从 8 类扩展到 16 类并且在损失函数中按类别样本量的倒数加权极端年龄段的预测能力得到改善但整体准确率略有下降。这个取舍没有标准答案取决于你更看重整体准确率还是结构化的年龄分布还原能力。如果是做商业项目建议按年龄段分别报告准确率而不是只看整体指标。4.4 第四个坑读图失败和路径不一致不同机器的路径分隔符不一致full_path中可能是/mnt/data/...或C:\data\...必须统一转换成平台友好的路径。另外有少量图片本身就是损坏文件cv2.imread返回None需要在Dataset中做容错处理否则训练过程会中断。为了保证训练稳定我在读取失败时递归取下一张图而不是报错退出image cv2.imread(image_path) if image is None: return self.__getitem__((idx random.randint(1, 10)) % len(self.records))5. 测试与效果评估给模型做一次“体检”5.1 测试流程与预测输出训练完成后加载保存的模型对一张新图片做预测完整流程如下import cv2 import torch from torchvision import transforms def predict_image(model, image_path, device): model.eval() img cv2.imread(image_path) img cv2.cvtColor(img, cv2.COLOR_BGR2RGB) img cv2.resize(img, (224, 224)) transform transforms.Compose([ transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ]) img_tensor transform(img).unsqueeze(0).to(device) with torch.no_grad(): age_out, gender_out model(img_tensor) gender_pred torch.argmax(gender_out, dim1).item() age_prob torch.softmax(age_out, dim1).cpu().numpy()[0] age_class int(np.argmax(age_prob)) age_center age_bucket_centers[age_class] gender_str female if gender_pred 0 else male print(fGender: {gender_str}, Age: {age_center:.1f})5.2 不同输出的波动性分析我在一批测试图上做过多次推理发现同一张图在同一模型上性别输出非常稳定几乎不会翻转年龄输出则存在一定波动相邻年龄段之间的概率差距有时不到 5%。这本质上是分类器置信度不高导致的不能说明模型有问题。更好的工程化做法是取年龄各类别概率的加权期望作为最终输出而不是直接取argmax这样预测出的年龄会更平滑也更适合作为推荐系统或营销场景的输入特征age_expectation float(np.sum(age_prob * age_bucket_centers))5.3 在 CPU 上面向真实照片做推理没有 GPU 时单张 224×224 图片在 ResNet-50 上的 CPU 推理时间约 200~500ms。如果想提升并发性能可以把输入降到 160×160并转换成 FP16 或 ONNX 格式CPU 推理速度能提升一倍左右。对需要部署到 Web 服务或移动端的项目这个优化是必要的。转换 ONNX 的步骤很简单先装onnx和onnxruntime再用dummy_input torch.randn(1, 3, 224, 224) torch.onnx.export(model, dummy_input, age_gender.onnx, input_names[input], output_names[age, gender], dynamic_axes{input: {0: batch}, age: {0: batch}, gender: {0: batch}})注意有些自定义层不支持 ONNX 导出如果报错替换成标准卷积层或全连接层即可。6. 模型改进方向与后续扩展6.1 从单张图片到视频流预测项目跑通后最自然的扩展方向是视频流预测。实际处理时建议每 5 到 10 帧抽取一帧做人脸检测并对连续帧的预测结果做时序平滑比如滑动窗口平均。单帧预测的抖动会比较大不加平滑直接输出的话画面里年龄数值会频繁跳动。时序平滑其实只是把预测结果做了简单滤波并不需要引入额外的时序模型。6.2 融入注意力机制与轻量化模型如果追求更高的精度可以考虑在 backbone 末端加入轻量级注意力模块比如 SE 模块或 CBAM。它们对年龄这类细粒度特征提取是有帮助的——眼睛周围的皱纹、嘴角下垂程度这些局部特征需要通道注意力和空间注意力共同增强。但需要注意的是IMDB-WIKI 的标签噪声太大注意力机制带来的精度提升有时会被噪声淹没需要配合标签清洗策略才能看到明显收益。如果追求实时性可以用 MobileNetV3 替换 ResNet-50在 GPU 上推理速度提升接近三倍精度下降约 4~5 个百分点。轻量化模型配合剪枝量化后完全可以在边缘设备上运行。6.3 多任务与跨数据集微调年龄和性别预测不一定要局限在 IMDB-WIKI 上。实际应用中尤其是面向真实客户时用 UTKFace、AFAD 或 Apparel 数据集做一次微调可以让模型适应更贴近业务场景的人脸分布。跨数据集微调时要特别注意标签体系的统一比如年龄边界定义不同、性别字段含义不同必须提前做映射。我的建议是先用 IMDB-WIKI 训练出具有泛化能力的基座模型再用业务场景的数据做小规模微调这样保留前者丰富的多样性又融入新场景的分布特征。6.4 在模型之上搭建完整服务模型本身只是能力组件工程化还差一层 API 封装。用 FastAPI 搭建一个推理服务并不复杂核心逻辑是先做人脸检测把检测结果输入年龄和性别模型最后返回结构化 JSONfrom fastapi import FastAPI, UploadFile, File app FastAPI() app.post(/predict) async def predict(file: UploadFile File(...)): image_bytes await file.read() result run_inference_on_bytes(image_bytes) return result如果并发量不高单机多进程部署完全够用。每增加一个 worker 进程显存占用也会成倍增加需要根据实际显存评估最大并发数。7. 总结一些值得记住的实操习惯这个项目从零到跑通模型我踩了不少坑其中几个习惯现在看来非常值得坚持。第一数据集清洗阶段宁可多筛选也别贪多。网络上很多教程直接拿全部数据训练结果噪声样本把模型的边缘特征搅乱最终效果反而不如清洗后的子集。把face_score阈值调到 1.0 以上过滤掉大半模糊样本虽然数据量减少但训练曲线更平滑。第二保存模型时把数据预处理的细节一起存下来。不同项目之间最容易出现的偏差就是归一化方式不一致。推理阶段用了不同的均值和方差整体精度会下降几个点这种问题排查起来非常隐蔽。第三评估模型不能只看分类准确率对年龄预测还要画一张“预测年龄 vs 真实年龄”的散点图观察是否存在系统性偏移。如果散点整体在某个区间上抬高或压低说明标签里有系统误差直接调模型没有用得回去清洗数据。第四用多任务网络同时预测年龄和性别并不只是省显存这么简单。两个任务共享骨干网络实际上会互相约束特征表达。性别分支对局部纹理更敏感年龄分支对整体脸型更敏感共同训练让 backbone 两边都照顾到泛化能力比单独训练更强。这个项目做完之后我再去看网上类似的开源代码时明显能分辨出哪些可以直接用、哪些只是搬运理论。数据清洗和训练细节才是决定模型效果的关键。如果不处理噪声不检查真实场景表现任何网络结构都无法发挥应有的潜力。后续如果读者对模型部署、ONNX 加速或视频流预测感兴趣我再把这部分实践单独整理成文。