
简介这份资源是面向计算机相关专业学生与初学者的机器学习实战项目围绕年龄与性别预测任务提供完整的Python源码与配套数据集适合课程设计、大作业、毕业设计或入门练手。项目基于MXNet深度学习框架搭建涵盖数据构建、模型训练、精度测试与预测部署等环节代码结构清晰便于理解从数据处理到模型推理的完整流程。压缩包共36个文件以20个py脚本为核心辅以txt说明、cpickle序列化模型文件、json配置与日志、md文档整体约34KB体积轻量解压后即可按说明运行。目前已有40人学习关注。对于希望掌握图像分类与多任务预测思路的读者可借此熟悉数据预处理、标签编码、网络定义与评估指标等关键环节并在此基础上进行二次开发或功能扩展具备较高的学习借鉴价值。1. 从一张人脸到两个标签年龄和性别预测到底在做什么你手里有一张用户上传的自拍业务侧想要两个标签大概多少岁、是男是女。听起来像两个独立任务但真正落地时它们往往被塞进同一个模型里一起出结果。这就是「基于 Python 机器学习的年龄和性别预测」最朴素的形态——输入一张人脸图输出一个年龄数值和一个性别类别。它不神秘也不依赖什么黑匣子核心就是卷积神经网络加两个输出头。适合谁做想入门计算机视觉、手里有数据集、需要快速跑通一个多任务学习 demo 的 Python 开发者。你不需要从零推导反向传播但得知道数据怎么洗、模型怎么搭、两个 loss 怎么平衡。很多人卡住的地方不是网络结构而是数据集里那些模糊的、侧脸的、戴墨镜的样本它们会让模型在验证集上看起来还行一上真实场景就翻车。这一篇就按「数据准备 → 模型搭建 → 训练调参 → 避坑排查 → 进阶技巧」的顺序把这条链路拆开讲清楚让你能照着复现也能判断这个方向值不值得投入。2. 数据集处理与输入管线别急着喂给模型2.1 年龄和性别标签的常见组织方式你拿到的数据集压缩包里通常是一个文件夹放图片另一个 CSV 或 txt 记录文件名、年龄、性别。常见字段名有file、age、gender性别一般用 0/1 或 male/female 表示。第一步不是写模型而是把这份索引读进 pandas检查三件事年龄有没有负数或超过 120 的异常值、性别字段有没有第三种取值、文件名和实际图片是否一一对应。我一般会先跑一段统计脚本把年龄分布画出来。如果发现 0 到 5 岁和 60 到 80 岁的样本极少那后面训练时就要考虑加权采样否则模型会偏向预测 20 到 40 岁这个区间。import pandas as pd import os # 读取标签文件假设是 CSV分隔符为逗号 df pd.read_csv(labels.csv) # 统一列名避免大小写和空格干扰 df.columns [c.strip().lower() for c in df.columns] # 检查年龄范围 print(df[age].describe()) # 过滤掉明显异常年龄小于 1 或大于 100 df df[(df[age] 1) (df[age] 100)] # 检查性别取值 print(df[gender].value_counts()) # 确认图片路径存在 df[exists] df[file].apply(lambda x: os.path.exists(os.path.join(images, x))) print(df[exists].value_counts())这段代码的逻辑是先做描述性统计再用硬阈值剔除异常样本最后验证文件路径。参数上年龄上下限可以根据你的业务场景调整比如只做成年人就设 18 到 80。性别字段如果出现unknown要么归入某一类要么直接丢弃别让模型学一个模棱两可的第三类。2.2 用 OpenCV 做人脸对齐和裁剪原始图片往往包含背景、肩膀、头发直接缩放到 224x224 会把大量无关像素喂给网络。常见做法是用 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, target_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, scaleFactor1.1, minNeighbors5, minSize(30, 30)) if len(faces) 0: # 没检测到人脸时取图像中心区域 h, w img.shape[:2] side min(h, w) y1 (h - side) // 2 x1 (w - side) // 2 crop img[y1:y1side, x1:x1side] else: # 取面积最大的那个人脸框 x, y, w, h max(faces, keylambda f: f[2] * f[3]) crop img[y:yh, x:xw] crop cv2.resize(crop, target_size) return cropscaleFactor控制每次图像缩小的比例设 1.1 表示每次缩小 10%值越小检测越慢但更细。minNeighbors越大误检越少但漏检越多一般 5 到 6 比较稳。minSize过滤掉太小的框避免把噪点当人脸。裁剪后统一到 224x224是因为后面用的骨干网络通常按这个尺寸预训练。2.3 训练集、验证集划分与数据增强划分不能随机打乱就完事。如果同一个人有多张照片必须按人划分否则同一个人既出现在训练集又出现在验证集验证分数会虚高。我一般用GroupShuffleSplit把人员 ID 作为分组依据。数据增强方面年龄和性别预测对水平翻转、轻微旋转、亮度调整是安全的但不要用大角度旋转或裁剪因为那会改变面部结构年龄特征会失真。from sklearn.model_selection import GroupShuffleSplit from tensorflow.keras.preprocessing.image import ImageDataGenerator # 假设 df 里有一列 person_id 标识同一个人 gss GroupShuffleSplit(n_splits1, test_size0.2, random_state42) train_idx, val_idx next(gss.split(df, groupsdf[person_id])) train_df df.iloc[train_idx] val_df df.iloc[val_idx] # 训练集增强翻转、小角度旋转、亮度微调 train_datagen ImageDataGenerator( horizontal_flipTrue, rotation_range10, brightness_range(0.8, 1.2), fill_modenearest ) # 验证集只做归一化不做增强 val_datagen ImageDataGenerator()rotation_range10表示随机旋转正负 10 度再大就可能把下巴或额头切掉。brightness_range模拟不同光照但别设得太宽否则肤色会变得不自然。验证集不做增强是为了让评估结果稳定可复现。3. 多任务模型搭建一个骨干两个头3.1 为什么用多任务而不是两个独立模型年龄和性别共享大量底层特征比如脸型、五官比例、皮肤纹理。如果分别训练两个模型等于把同样的卷积计算做了两遍推理时也要跑两次。多任务学习让一个骨干网络同时输出两个结果不仅省算力还能让性别分类的梯度帮助年龄回归学到更鲁棒的特征。常见做法是拿一个预训练的图像分类网络当骨干比如 MobileNetV2 或 ResNet50去掉顶层接一个全局池化然后分两个分支一个输出年龄的回归值一个输出性别的二分类概率。3.2 用 Keras 搭一个双输出网络下面这段代码用 TensorFlow/Keras 实现一个基于 MobileNetV2 的双输出模型。骨干网络加载 ImageNet 预训练权重前几层冻结后面再解冻微调。import tensorflow as tf from tensorflow.keras import layers, models, applications def build_model(input_shape(224, 224, 3)): # 加载预训练骨干不包含顶层分类器 base applications.MobileNetV2(input_shapeinput_shape, include_topFalse, weightsimagenet) # 先冻结骨干只训练后面的头 base.trainable False inputs layers.Input(shapeinput_shape) x base(inputs, trainingFalse) x layers.GlobalAveragePooling2D()(x) x layers.Dropout(0.3)(x) # 年龄分支回归输出一个数值 age_branch layers.Dense(128, activationrelu)(x) age_branch layers.Dropout(0.2)(age_branch) age_output layers.Dense(1, activationlinear, nameage)(age_branch) # 性别分支二分类输出一个概率 gender_branch layers.Dense(128, activationrelu)(x) gender_branch layers.Dropout(0.2)(gender_branch) gender_output layers.Dense(1, activationsigmoid, namegender)(gender_branch) model models.Model(inputsinputs, outputs[age_output, gender_output]) return model model build_model() model.summary()年龄输出用linear激活因为年龄是连续值。性别输出用sigmoid输出 0 到 1 之间的概率大于 0.5 判为男性或女性取决于你的标签编码。两个分支各自带一个 Dropout防止过拟合。base.trainable False表示第一阶段只训练新加的全连接层等 loss 稳定后再解冻部分骨干层做微调。3.3 损失函数加权与编译参数多任务训练的关键是平衡两个 loss。年龄回归用均方误差 MSE性别分类用二元交叉熵 BCE。如果直接相加MSE 的数值通常在几十到几百BCE 在 0.1 到 1 之间年龄 loss 会主导梯度。常见做法是给年龄 loss 一个较小的权重比如 0.01 到 0.05或者对年龄做归一化把它缩放到 0 到 1 再算 MSE。# 年龄归一化假设年龄范围 1 到 100 age_min, age_max 1.0, 100.0 def age_loss(y_true, y_pred): # 把真实年龄缩放到 0-1 y_true_norm (y_true - age_min) / (age_max - age_min) return tf.keras.losses.mse(y_true_norm, y_pred) model.compile( optimizertf.keras.optimizers.Adam(learning_rate1e-3), loss{age: age_loss, gender: binary_crossentropy}, loss_weights{age: 1.0, gender: 1.0}, metrics{age: mae, gender: accuracy} )这里把年龄 loss 内部做了归一化所以loss_weights可以都设 1.0。如果你不想改 loss 函数也可以直接把loss_weights设成{age: 0.02, gender: 1.0}效果类似。学习率先用 1e-3等验证 loss 不降了再降到 1e-4。metrics里年龄看 MAE性别看准确率方便你判断两个任务是否都在正常收敛。4. 训练、评估与调参让两个头都收敛4.1 用 fit 跑通第一轮训练数据管线用ImageDataGenerator的flow_from_dataframe把图片路径和标签对应起来。注意年龄和性别要作为两个输出传给模型所以生成器需要返回一个字典或列表。Keras 的flow_from_dataframe默认只返回一个标签这里我一般自己写一个tf.data管线更灵活。import tensorflow as tf def load_and_preprocess(path, age, gender): img tf.io.read_file(path) img tf.image.decode_jpeg(img, channels3) img tf.image.resize(img, (224, 224)) img img / 255.0 # 年龄归一化到 0-1 age (age - age_min) / (age_max - age_min) return img, {age: age, gender: gender} def make_dataset(df, batch_size32, shuffleTrue): paths df[file].apply(lambda x: os.path.join(images, x)).values ages df[age].values.astype(float32) genders df[gender].values.astype(float32) ds tf.data.Dataset.from_tensor_slices((paths, ages, genders)) if shuffle: ds ds.shuffle(buffer_sizelen(df)) ds ds.map(load_and_preprocess, num_parallel_callstf.data.AUTOTUNE) ds ds.batch(batch_size).prefetch(tf.data.AUTOTUNE) return ds train_ds make_dataset(train_df, batch_size32, shuffleTrue) val_ds make_dataset(val_df, batch_size32, shuffleFalse) history model.fit( train_ds, validation_dataval_ds, epochs20, callbacks[ tf.keras.callbacks.EarlyStopping(patience5, restore_best_weightsTrue), tf.keras.callbacks.ReduceLROnPlateau(factor0.5, patience3) ] )num_parallel_calls和prefetch让数据加载和 GPU 计算重叠避免 GPU 空等。EarlyStopping在验证 loss 连续 5 轮不降时停止并恢复最佳权重。ReduceLROnPlateau在 3 轮不降后把学习率减半帮助跳出局部震荡。4.2 解冻骨干做微调第一阶段训练完后把骨干网络的后半部分解冻用更小的学习率再跑几轮。MobileNetV2 有 150 多层一般解冻最后 30 到 50 层就够了前面的层保留通用特征。# 解冻骨干的最后 40 层 base_model model.layers[1] # 假设骨干是第二层 for layer in base_model.layers[-40:]: layer.trainable True # 重新编译学习率降到 1e-4 model.compile( optimizertf.keras.optimizers.Adam(learning_rate1e-4), loss{age: age_loss, gender: binary_crossentropy}, loss_weights{age: 1.0, gender: 1.0}, metrics{age: mae, gender: accuracy} ) history_fine model.fit( train_ds, validation_dataval_ds, epochs15, callbacks[ tf.keras.callbacks.EarlyStopping(patience5, restore_best_weightsTrue), tf.keras.callbacks.ReduceLROnPlateau(factor0.5, patience3) ] )微调时学习率一定要小否则预训练权重会被破坏。如果验证 loss 反而上升说明解冻层数太多或学习率太大减少解冻层数或再降学习率。4.3 评估指标年龄看 MAE性别看准确率训练完后在验证集上分别算年龄的平均绝对误差和性别的准确率。年龄 MAE 在 5 到 8 岁之间通常算可用低于 5 岁说明数据集比较干净。性别准确率在 90% 以上算正常低于 85% 要检查是不是侧脸或遮挡样本太多。# 在验证集上预测 val_images [] val_ages [] val_genders [] for img, labels in val_ds: val_images.append(img) val_ages.append(labels[age]) val_genders.append(labels[gender]) val_images tf.concat(val_images, axis0) val_ages tf.concat(val_ages, axis0) val_genders tf.concat(val_genders, axis0) pred_ages, pred_genders model.predict(val_images) # 反归一化年龄 pred_ages pred_ages.flatten() * (age_max - age_min) age_min true_ages val_ages.numpy() * (age_max - age_min) age_min mae np.mean(np.abs(pred_ages - true_ages)) # 性别准确率 pred_gender_labels (pred_genders.flatten() 0.5).astype(int) true_gender_labels val_genders.numpy().astype(int) acc np.mean(pred_gender_labels true_gender_labels) print(fAge MAE: {mae:.2f}, Gender Acc: {acc:.4f})如果年龄 MAE 偏大先看是不是某些年龄段样本太少可以分桶统计误差。性别准确率低优先检查标签编码是否一致比如训练时 0 代表男性验证时有没有搞反。5. 避坑与排查那些让模型看起来还行但实际翻车的细节5.1 现象验证 loss 正常但推理时年龄全预测成同一个值原因通常是年龄回归的 loss 被性别分类的梯度淹没或者年龄标签没有归一化导致模型直接输出均值就能让 MSE 很小。解决方法是把年龄归一化到 0 到 1或者给年龄 loss 加权重。另外检查最后一层年龄输出有没有被sigmoid误激活回归任务必须用linear。5.2 现象性别准确率很高但年龄 MAE 很大这往往是因为两个任务共享了太多底层特征而性别分类更容易学梯度更新时把骨干带偏了。可以尝试给两个分支各自加一个独立的卷积层或者用梯度裁剪把年龄分支的梯度范数限制住。我一般会在年龄分支前加一个BatchNormalization让数值更稳定。5.3 现象训练集准确率 99%验证集只有 70%典型的过拟合。先检查是不是同一个人同时出现在训练集和验证集用GroupShuffleSplit按人划分。然后增加 Dropout 比例或者加 L2 正则。数据增强也可以再强一点但别用大角度旋转。如果数据集本身只有几千张考虑用预训练模型冻结更多层只训练最后的分类头。5.4 现象OpenCV 检测不到人脸裁剪后全是背景Haar 级联对侧脸和暗光很敏感。换成 OpenCV 的 DNN 人脸检测器或者用 MTCNN。如果还是检测不到就退化为中心裁剪但要在训练时告诉模型这不是标准人脸可以加一个标志位。另外检查图片是不是灰度图或四通道 PNGcv2.imread默认读成 BGR 三通道遇到四通道会出错需要加cv2.IMREAD_COLOR参数。5.5 现象模型推理速度太慢达不到实时MobileNetV2 在 CPU 上单张推理大约 50 到 100 毫秒如果还嫌慢可以换成 MobileNetV3 Small 或把输入尺寸降到 128x128。另外检查是不是每次推理都重新加载了模型应该把模型加载放在服务启动时而不是每次请求都加载。用 TensorFlow Lite 转换后还能再快一倍但精度会掉一点需要权衡。6. 进阶技巧用年龄分桶辅助回归和模型量化部署年龄回归有一个天然优势你可以把它变成分类问题来辅助回归。具体做法是把年龄分成若干个桶比如 0-10、11-20、…、91-100先训练一个分类头预测桶再用桶的中心值作为年龄的粗估计最后用一个小的回归头在桶内做微调。这样模型不会因为个别极端年龄样本而震荡MAE 通常能降 0.5 到 1 岁。我一般会在骨干后面同时接三个头年龄回归、年龄分桶分类、性别分类三个 loss 加权求和。分桶分类的 loss 用交叉熵权重设 0.3 左右。# 年龄分桶每 10 岁一个桶共 10 个桶 def age_to_bucket(age): bucket tf.floor(age / 10.0) bucket tf.clip_by_value(bucket, 0, 9) return tf.cast(bucket, tf.int32) # 在模型里增加一个分桶分类头 bucket_branch layers.Dense(128, activationrelu)(x) bucket_output layers.Dense(10, activationsoftmax, nameage_bucket)(bucket_branch) model models.Model(inputsinputs, outputs[age_output, gender_output, bucket_output]) model.compile( optimizertf.keras.optimizers.Adam(learning_rate1e-3), loss{age: age_loss, gender: binary_crossentropy, age_bucket: sparse_categorical_crossentropy}, loss_weights{age: 1.0, gender: 1.0, age_bucket: 0.3}, metrics{age: mae, gender: accuracy, age_bucket: accuracy} )分桶分类的标签需要是整数所以用sparse_categorical_crossentropy。训练完后最终年龄可以取回归输出和分桶中心值的加权平均比如0.7 * 回归值 0.3 * 桶中心。这个技巧在数据量偏少或年龄分布不均时特别管用。另一个进阶方向是模型量化。用 TensorFlow Lite 把模型转成 int8 格式体积缩小到原来的四分之一推理速度提升明显。转换时需要一个代表数据集做校准通常从训练集里抽 100 到 200 张图就够了。量化后年龄 MAE 可能会涨 0.3 到 0.5性别准确率掉 1% 左右如果业务能接受部署成本会低很多。# 转换为 TFLite int8 量化模型 converter tf.lite.TFLiteConverter.from_keras_model(model) converter.optimizations [tf.lite.Optimize.DEFAULT] # 代表数据集生成器 def representative_dataset(): for img, _ in train_ds.take(100): yield [img] converter.representative_dataset representative_dataset converter.target_spec.supported_ops [tf.lite.OpsSet.TFLITE_BUILTINS_INT8] converter.inference_input_type tf.uint8 converter.inference_output_type tf.uint8 tflite_model converter.convert() with open(age_gender_quant.tflite, wb) as f: f.write(tflite_model)量化后的模型输入输出都变成 uint8推理时要把图片归一化到 0 到 255 再传入。我自己的习惯是每次做完一个模型先不急着上线而是拿 50 张真实场景的图跑一遍看看年龄误差和性别误判的案例长什么样。很多时候模型在验证集上的分数和真实体验之间的差距就藏在那几张侧脸、逆光、戴帽子的图里。希望帮到你。本文还有配套的精品资源点击获取