ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

基于PyTorch的人脸图像性别分类:CNN模型设计与训练优化

基于PyTorch的人脸图像性别分类:CNN模型设计与训练优化 简介面向计算机类专业硕士《机器学习》课程大作业要求这份资料提供一套人脸图像性别分类的Python实现方案覆盖数据预处理、模型搭建、训练评估到结果提交的完整链路也适合本科高年级课程设计或入门计算机视觉实践。压缩包共17个文件大小约50.76MB主要包含IPython Notebook交互式分析代码、Python脚本、H5模型权重文件、CSV数据文件以及TXT/MD/PDF说明文档可对应从数据读取到预测提交的各个模块。当前已有152人学习下载资源中不仅提供了可直接运行的训练与预测脚本还保留了多次训练的权重文件方便对比不同轮次的效果差异附带的Excel汇总表和测试集便于进一步分析并撰写大作业技术报告能帮助学习者减少重复踩坑快速搭建起自己的性别分类实验。1. 人脸图像性别分类大作业里的第一个“真”模型“性别分类”这四个字在深度学习课程里经常被当成入门练习但真把它当作大作业做完一遍你会发现它的信息量比想象的厚得多。它不是一个简单的二分类问题——从数据清洗、人脸区域提取、模型选型到训练策略、评估指标甚至最后的报告展示每一个环节都藏着一堆“差一点就翻车”的细节。很多人上手就套一个预训练模型跑通后准确率看着还行但一换测试集就崩原因往往在于数据分布的偏差而不是模型本身。这篇博文会顺着一条完整可复现的路径展开从环境搭建和数据集组织开始讲清楚为什么在“人脸图像”这个场景下定睛于卷积神经网络再手写一个能跑的基准 CNN逐步优化到接近实用水平最后把训练曲线、混淆矩阵和测试集验证整理成大作业该有的交付形态。无论你是第一次接触机器学习的大作业新手还是想快速搭一套基准方案的老手这里面的参数、坑和评估思路都能直接抄作业。2. 从任务定义到数据准备把人脸图像性别分类当成一个工程问题2.1 性别分类在机器学习里的任务定位性别分类本质上是一个二分类监督学习问题输入是一张包含人脸的图像输出是“男”或“女”两个类别之一。但在大作业场景下这个任务会被拆得更细你需要把原始图像处理成模型能吃的张量把标签从中文或文件名里解析成数值再划分训练集和验证集最后才能进入模型训练环节。常见做法是使用公开数据集比如 UTKFace、IMDB-WIKI 的裁剪版本或者自己爬取一批头部姿态度均匀的图像。需要注意大作业不同于工业项目数据量不需要特别大但分布一定要说明白。我一般会先在数据集里随机挑 50 张图看一遍确认标注是否准确、人脸是否足够居中这一步能避免后续训练时出现“模型学到了背景而不是人脸”的尴尬。2.2 数据集的目录组织与标签解析不管你在哪里下的数据第一步永远是把它整理成一个统一的文件夹结构。我习惯于用下面的方式组织face_gender/ ├── train/ │ ├── male/ │ │ ├── 000001.jpg │ │ └── ... │ └── female/ │ ├── 000101.jpg │ └── ... ├── val/ │ ├── male/ │ └── female/ └── test/ ├── male/ └── female/这样组织的好处是PyTorch 的ImageFolder可以直接按目录名生成标签不需要手写标签映射。如果你拿到的是 CSV 格式的标注文件则需要先写一个脚本把图片和标签对应起来。下面是一个通用的标签解析脚本import pandas as pd from sklearn.model_selection import train_test_split # 假设 CSV 有两列image_path, gender df pd.read_csv(annotation.csv) df[gender_label] df[gender].map({male: 0, female: 1}) # 按分层抽样划分训练集和验证集避免分布倾斜 train_df, val_df train_test_split( df, test_size0.2, stratifydf[gender_label], random_state42 ) print(f训练集: {len(train_df)} 张, 验证集: {len(val_df)} 张)这里的关键参数是stratifydf[gender_label]它保证训练集和验证集里的男女比例与原始数据集一致。如果不加这个参数万一你的数据里某个类别样本偏少划分后验证集里可能压根没有那个类别训练过程就会出现极其隐蔽的准确率虚高。2.3 人脸检测与对齐为什么不能把整张图直接丢进模型很多大作业失败的核心原因不是模型太弱而是输入图像太“脏”。一张照片里可能有半身像、多人、遮挡物直接缩放后喂给模型模型学到的特征会被背景干扰得七零八落。常见做法是基于 OpenCV 的 Haar Cascade 或 DNN 检测器做人脸区域提取。我推荐先用 OpenCV 的CascadeClassifier做一次检测它虽然不够新但胜在稳、无额外依赖、参数好调。下面是一个最小可用的提取代码import cv2 import os face_cascade cv2.CascadeClassifier( cv2.data.haarcascades haarcascade_frontalface_default.xml ) def detect_and_crop(image_path, output_path, target_size(128, 128)): img cv2.imread(image_path) gray cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) faces face_cascade.detectMultiScale( gray, scaleFactor1.1, minNeighbors5, minSize(48, 48) ) if len(faces) 0: # 检测不到人脸时直接缩放整张图保证数据不丢 resized cv2.resize(img, target_size) cv2.imwrite(output_path, resized) return False # 取最大的人脸区域 x, y, w, h max(faces, keylambda rect: rect[2] * rect[3]) margin int(0.2 * w) # 四周留一点边减少裁剪过紧的问题 x1, y1 max(0, x - margin), max(0, y - margin) x2, y2 min(img.shape[1], x w margin), min(img.shape[0], y h margin) face img[y1:y2, x1:x2] resized cv2.resize(face, target_size) cv2.imwrite(output_path, resized) return True这段代码里有两个参数值得记一下scaleFactor1.1表示每次缩放步长是 10%值越小检测越精细但速度越慢minNeighbors5控制误检率越大越保守调大可以显著减少把墙角当成人脸的情况。minSize(48, 48)过滤掉太小的检测框避免把远处的人脸放大后糊成一团。2.4 图像增强策略别让模型记住像素而不是人脸在数据量有限的情况下图像增强是提升模型泛化能力的重要手段。但人脸图像有一个特性你不能像做物体分类那样随意翻转和旋转因为左右翻转在语义上问题不大但 90 度旋转会破坏人脸的朝向结构。我在处理人脸性别分类时通用的增强管线只包含四种操作from torchvision import transforms train_transform transforms.Compose([ transforms.RandomHorizontalFlip(p0.5), # 水平翻转模拟左右脸对称 transforms.ColorJitter(brightness0.2, contrast0.2), transforms.RandomRotation(degrees10), # 小角度旋转不能太大 transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ]) val_transform transforms.Compose([ transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ])RandomRotation的degrees10是一个比较安全的阈值。头稍微歪一点是自然状态但超过 15 度就会引入无效的空白区域。ColorJitter的取值也不能太大因为肤色、亮度在性别分类中是有区分度的特征过度扰动反而会抹掉有效信息。3. 搭建基准 CNN用 PyTorch 实现一个人脸性别分类模型3.1 为什么选 CNN 而不是传统机器学习方法人脸图像性别分类的理论基础是图像中的性别特征主要体现在局部纹理和全局结构上比如下巴形状、眉骨高度、皮肤纹理密度。传统的机器学习方法比如 HOG 特征配 SVM在小型数据集上表现不错但有两个痛点一是特征工程的主观性太强你需要手动设计和筛选特征二是特征提取与分类是分离的无法针对“性别分类”这个任务端到端地联合优化。CNN 的卷积层天然具备层次化特征提取能力浅层卷积核学习边缘和颜色块中层学习眼睛、鼻子、嘴等局部部件深层将部件组合成更高层的语义特征。对于大作业而言这不仅能避免手工特征设计的繁琐还能让最终的准确率有明显提升空间。如果你的课程要求对比实验拿 HOG SVM 作为 baselineCNN 作为改进方案是个非常完整的叙事结构。3.2 最小 CNN 模型结构设计下面这个模型参照了 VGG 的风格但做了大幅简化适合在普通笔记本电脑的 CPU 上完成训练import torch.nn as nn class GenderCNN(nn.Module): def __init__(self, num_classes2): super(GenderCNN, self).__init__() self.features nn.Sequential( nn.Conv2d(3, 32, kernel_size3, padding1), nn.BatchNorm2d(32), nn.ReLU(inplaceTrue), nn.MaxPool2d(kernel_size2, stride2), nn.Conv2d(32, 64, kernel_size3, padding1), nn.BatchNorm2d(64), nn.ReLU(inplaceTrue), nn.MaxPool2d(kernel_size2, stride2), nn.Conv2d(64, 128, kernel_size3, padding1), nn.BatchNorm2d(128), nn.ReLU(inplaceTrue), nn.MaxPool2d(kernel_size2, stride2), ) self.classifier nn.Sequential( nn.AdaptiveAvgPool2d((1, 1)), nn.Flatten(), nn.Linear(128, 64), nn.ReLU(inplaceTrue), nn.Dropout(0.5), nn.Linear(64, num_classes) ) def forward(self, x): x self.features(x) x self.classifier(x) return x设计上有三个值得注意的点。第一每一层卷积后面都跟了BatchNorm2d它不仅加速收敛还能容忍稍大的学习率对大作业这种“试错成本高”的场景非常友好。第二AdaptiveAvgPool2d((1,1))替代了固定大小的全连接层输入这意味着模型可以接受任意尺寸的输入图虽然我们统一用 128x128 训练但这个设计让模型的通用性更强。第三Dropout(0.5)放在全连接层之前有效地降低了过拟合风险。3.3 训练循环的完整代码与参数解释训练循环是整篇大作业的骨架。下面这段代码经过了简化但保留了所有必要的逻辑import torch import torch.optim as optim from torch.utils.data import DataLoader from torchvision.datasets import ImageFolder device torch.device(cuda if torch.cuda.is_available() else cpu) model GenderCNN(num_classes2).to(device) train_dataset ImageFolder(face_gender/train, transformtrain_transform) val_dataset ImageFolder(face_gender/val, transformval_transform) train_loader DataLoader(train_dataset, batch_size32, shuffleTrue, num_workers2, pin_memoryTrue) val_loader DataLoader(val_dataset, batch_size32, shuffleFalse, num_workers2, pin_memoryTrue) criterion nn.CrossEntropyLoss() optimizer optim.Adam(model.parameters(), lr1e-3, weight_decay1e-4) scheduler optim.lr_scheduler.ReduceLROnPlateau( optimizer, modemin, factor0.5, patience3, verboseTrue ) num_epochs 30 best_val_acc 0.0 for epoch in range(num_epochs): model.train() running_loss 0.0 for images, labels in train_loader: images, labels images.to(device), labels.to(device) optimizer.zero_grad() outputs model(images) loss criterion(outputs, labels) loss.backward() optimizer.step() running_loss loss.item() * images.size(0) # 验证 model.eval() correct 0 total 0 val_loss 0.0 with torch.no_grad(): for images, labels in val_loader: images, labels images.to(device), labels.to(device) outputs model(images) loss criterion(outputs, labels) val_loss loss.item() * images.size(0) _, predicted torch.max(outputs, 1) total labels.size(0) correct (predicted labels).sum().item() val_acc correct / total avg_val_loss val_loss / len(val_dataset) scheduler.step(avg_val_loss) if val_acc best_val_acc: best_val_acc val_acc torch.save(model.state_dict(), best_gender_model.pth) print(fEpoch {epoch1}/{num_epochs} | fTrain Loss: {running_loss/len(train_dataset):.4f} | fVal Loss: {avg_val_loss:.4f} | Val Acc: {val_acc:.4f})这里的几个参数直接影响训练质量值得展开说明。batch_size32是显存和梯度稳定性之间的平衡点太小会导致梯度震荡剧烈太大在 CPU 上训练会很慢。Adam优化器的初始学习率1e-3经过验证是通用安全值配合weight_decay1e-4做 L2 正则化。ReduceLROnPlateau是一个极其好用的调度器验证集损失连续 3 个 epoch 不下降时学习率减半这比固定步长衰减更适应不同数据集的收敛节奏。3.4 训练过程中的指标监控与常见异常训练时不要只盯着准确率看损失值才是更敏感的指标。如果训练损失不断下降但验证准确率不升反降说明模型开始过拟合了——此时应该检查 Dropout 是否生效或者考虑增加数据增强的强度。另一个常见的异常是训练一开始准确率就卡在 50% 附近这往往意味着数据读取或标签映射出了问题。我会在训练前先跑一个简单的 sanity check取一个 batch 的数据打印标签和图片尺寸确认性别标注是否和图像内容一致。这个小步骤能帮你节省一天的排查时间。4. 优化训练策略让性别分类模型的准确率再上一个台阶4.1 利用预训练模型做迁移学习如果你的数据集只有几千张甚至几百张图从头训练一个 CNN 很容易过拟合。这时候迁移学习是默认的高效方案。PyTorch 内置了 torchvision 里的各种预训练模型常见做法是使用 ResNet18 或 MobileNetV3冻结前面的特征提取层只训练最后一层分类器。代码如下import torchvision.models as models import torch.nn as nn def create_restnet18_transfer(num_classes2): model models.resnet18(pretrainedTrue) # 冻结所有层 for param in model.parameters(): param.requires_grad False # 替换最后一层全连接 num_features model.fc.in_features model.fc nn.Sequential( nn.Linear(num_features, 128), nn.ReLU(inplaceTrue), nn.Dropout(0.3), nn.Linear(128, num_classes) ) return model第一次只训练最后的分类器学习率可以设大一点比如5e-3用 Adam。等验证集准确率稳定后可以解冻最后几层卷积块用一个较小的学习率1e-5微调。这个“冻结-解冻”两阶段策略是业界标准做法能兼顾训练速度和最终精度。需要注意pretrainedTrue会自动下载 ImageNet 预训练权重如果你的实验环境不能外网下载需要提前在别的机器上把权重下载好用torch.load手动加载。4.2 类别不平衡的应对方法性别分类数据集在理想状态下男女比例接近 1:1但真实场景经常会拿到偏斜的数据。比如某个人脸数据集里女性占 70%。这种情况下模型会倾向于预测多数类准确率虚高但实际泛化能力很差。我一般会先算一下类别分布from collections import Counter train_labels [path.split(/)[-2] for path in train_df[image_path]] print(Counter(train_labels))如果两个类别比例差距超过 1.5 倍我会优先用WeightedRandomSampler做采样而不是直接改损失函数因为前者更直观且不易影响收敛稳定性from torch.utils.data import WeightedRandomSampler # 统计每个类的样本数 class_counts [sum(1 for p in train_df[image_path] if /male/ in p), sum(1 for p in train_df[image_path] if /female/ in p)] sample_weights [1.0 / class_counts[0] if /male/ in p else 1.0 / class_counts[1] for p in train_df[image_path]] sampler WeightedRandomSampler(sample_weights, num_sampleslen(sample_weights), replacementTrue) train_loader DataLoader(train_dataset, batch_size32, samplersampler)这段代码的核心逻辑是样本少的类别每个样本被抽中的概率更高从而在每一批数据里让模型看到均衡的类别分布。replacementTrue表示允许同一个样本在一个 epoch 里被重复采样这是处理极度不平衡数据的常用做法。4.3 训练轮数与早停策略大作业的常见误区是跑满固定 epoch 数就算完事但这既浪费时间又容易带回过拟合的模型。我建议在训练循环中加入早停机制连续 7 个 epoch 验证集准确率没有提升就终止训练并回滚到历史最优权重。early_stop_patience 7 no_improve_epochs 0 for epoch in range(num_epochs): # ... 训练和验证代码 ... if val_acc best_val_acc: best_val_acc val_acc no_improve_epochs 0 torch.save(model.state_dict(), best_gender_model.pth) else: no_improve_epochs 1 if no_improve_epochs early_stop_patience: print(fEarly stopping at epoch {epoch1}) break这个策略在一次大作业里能节约至少 20% 的训练时间。配合ReduceLROnPlateau你实际上获得了两层防线学习率先减半给模型“缓一缓”的机会连续多个 epoch 无效才彻底停掉。4.4 多模型融合与预测结果分析当单个模型准确率卡在 93% 上下难以突破时一个不增加太多代码复杂度的技巧是做多模型投票。你可以分别训练一个从头训练的 CNN 和一个迁移学习的 ResNet18或者在同一个模型上改变随机种子跑三次然后在预测阶段取平均概率# 假设 model1, model2, model3 是三个已训练好的模型 def ensemble_predict(images): probs [] for model in [model1, model2, model3]: model.eval() with torch.no_grad(): output torch.softmax(model(images), dim1) probs.append(output) avg_prob torch.mean(torch.stack(probs), dim0) _, predicted torch.max(avg_prob, 1) return predicted集成策略本质上是在降低模型的方差不同初始化产生的模型会在不同样本上犯不同的错平均之后犯错的概率就降低了。5. 最终验证与提交技巧大作业的高分交付姿势5.1 用混淆矩阵和分类报告替代单纯的准确率准确率在二分类问题里存在严重的误导性。比如测试集 1200 张图里 900 男 300 女模型把所有人都预测成“男”准确率也有 75%。在大作业报告中你应该给出混淆矩阵和每个类别的精确率、召回率、F1-score从多个维度展示模型的能力。下面这段代码可以直接用在 Jupyter Notebook 或者提交的脚本里import matplotlib.pyplot as plt import numpy as np from sklearn.metrics import confusion_matrix, classification_report # 在测试集上收集所有预测 model.eval() all_preds [] all_labels [] with torch.no_grad(): for images, labels in test_loader: images images.to(device) outputs model(images) _, preds torch.max(outputs, 1) all_preds.extend(preds.cpu().numpy()) all_labels.extend(labels.numpy()) cm confusion_matrix(all_labels, all_preds) print(classification_report(all_labels, all_preds, target_names[male, female])) # 绘制混淆矩阵 plt.figure(figsize(5, 5)) plt.imshow(cm, interpolationnearest, cmapBlues) plt.colorbar() tick_marks np.arange(2) plt.xticks(tick_marks, [male, female], rotation45) plt.yticks(tick_marks, [male, female]) plt.xlabel(Predicted) plt.ylabel(True) plt.tight_layout() plt.savefig(confusion_matrix.png, dpi150)classification_report输出里最值得关注的是recall而不是precision因为在大作业答辩场景里“女性被识别成男性”和“男性被识别成女性”的代价往往不对称你需要根据测试集的实际用途来判断哪个指标更重要。5.2 可视化 Grad-CAM让模型决策过程可解释大作业加分项里Grad-CAM 热力图可以说是性价比最高的技巧。它可以帮助你观察模型在判断性别时到底在看图像中的哪些区域。用pytorch_grad_cam库几行代码就能出结果from pytorch_grad_cam import GradCAM from pytorch_grad_cam.utils.model_targets import ClassifierOutputTarget from pytorch_grad_cam.utils.image import show_cam_on_image # target_layers 通常是最后一个卷积层 target_layers [model.features[-1]] cam GradCAM(modelmodel, target_layerstarget_layers) input_tensor images[0].unsqueeze(0).to(device) targets [ClassifierOutputTarget(all_labels[0])] grayscale_cam cam(input_tensorinput_tensor, targetstargets) grayscale_cam grayscale_cam[0, :] # 将热力图叠加到原图上 img_rgb images[0].permute(1, 2, 0).cpu().numpy() img_rgb (img_rgb * [0.229, 0.224, 0.225]) [0.485, 0.456, 0.406] img_rgb np.clip(img_rgb, 0, 1) visualization show_cam_on_image(img_rgb, grayscale_cam, use_rgbTrue)如果热力图的高亮区域集中在眼睛、下巴轮廓、颧骨位置说明模型确实在学习人脸的结构特征如果高亮区域分散在背景、头发甚至图像角落说明训练数据里存在严重的背景偏差需要回到数据准备阶段重新做对齐和裁剪。5.3 一个完整的“端到端”验证脚本最后的大作业验收通常要求一个能直接吃一张图、输出性别预测结果的脚本。我把它放在项目的根目录下命名为predict.pyimport argparse import cv2 import torch from torchvision import transforms from model import GenderCNN def predict_image(model, image_path, device): transform transforms.Compose([ transforms.ToPILImage(), transforms.Resize((128, 128)), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ]) img cv2.imread(image_path) img_rgb cv2.cvtColor(img, cv2.COLOR_BGR2RGB) input_tensor transform(img_rgb).unsqueeze(0).to(device) model.eval() with torch.no_grad(): output model(input_tensor) prob torch.softmax(output, dim1) pred torch.argmax(prob, dim1).item() return male if pred 0 else female, prob[0][pred].item() if __name__ __main__: parser argparse.ArgumentParser() parser.add_argument(--image, typestr, requiredTrue) parser.add_argument(--checkpoint, typestr, defaultbest_gender_model.pth) args parser.parse_args() device torch.device(cuda if torch.cuda.is_available() else cpu) model GenderCNN(num_classes2).to(device) model.load_state_dict(torch.load(args.checkpoint, map_locationdevice)) gender, confidence predict_image(model, args.image, device) print(fPredicted gender: {gender} | Confidence: {confidence:.4f})这个脚本值得额外强调一个细节load_state_dict时用map_locationdevice这样即使在只有 CPU 的环境下也能加载 GPU 上训练得到的权重文件。另外注意在预测时也要做和训练时一致的归一化像素均值[0.485, 0.456, 0.406]和方差是 ImageNet 的标准值和训练时候统一性能才能对齐。测试的时候我会刻意挑几张“难样本”来验证模型鲁棒性戴眼镜的、有胡须的、头发极短的女性和长发男性。这类样本最容易暴露模型的弱点也是答辩时最能体现你对模型理解深度的素材。本文还有配套的精品资源点击获取
返回列表