简介:这份资源面向计算机、人工智能相关专业的本科生与自学者,提供一套基于PyTorch实现人脸性别识别的完整课程设计或毕业设计参考方案。数据集涵盖白种人、黄种人、黑种人等多种族样本,并包含姿态、光照、年龄等干扰因素,需按40%、10%、50%比例随机划分训练、验证与测试集,具有一定挑战性。压缩包共18个文件,约1.21MB,以13个Python脚本为核心,涵盖自定义数据集加载、网络模型定义、多版本训练与推理流程,另附实验报告docx、说明文档md、依赖清单txt及许可证文件,便于理解整体工程结构。资源已有648人学习下载,报告部分说明了所用模型方法、测试结果与文献引用,并额外提供GUI界面脚本,可在界面中载入图像直接显示性别识别结果,适合作为毕设参考或动手实践项目。
1. 人脸性别识别 GUI 系统:从 PyTorch 模型到可交付毕设的完整路径
很多同学做毕设时卡在同一个地方:模型在 notebook 里跑通了,准确率也还行,但一到答辩演示就露怯——老师问「你这个东西怎么用」,你只能打开 Jupyter 一行行跑单元格。基于 Python + PyTorch 实现人脸性别识别 GUI 系统,要解决的正是这个断层:把训练好的卷积网络封装成一个能双击运行、能选图片、能实时显示结果的桌面程序。它适合计算机视觉方向的本科毕设,也适合想补一个完整「训练 + 推理 + 界面」闭环的入门者。核心链路只有三段:数据准备与模型训练、推理接口封装、GUI 事件绑定。下面按我实际做过的顺序拆开讲,包括环境怎么配、模型怎么选、界面怎么接、哪里最容易翻车。
2. 环境搭建与数据准备:把 PyTorch 和数据集先跑通
2.1 用 Anaconda 隔离环境,避开版本地狱
人脸性别识别这类毕设最怕的就是环境冲突。我一般不会在系统 Python 里直接装 PyTorch,而是用 conda 建一个独立环境。原因是 PyTorch、torchvision、OpenCV、GUI 库(PyQt 或 Tkinter)对 Python 版本和 CUDA 版本都有要求,混装很容易出现「import torch 成功但 torchvision 报错」这种玄学问题。
# 创建独立环境,Python 版本选 3.9 或 3.10,兼容性最好 conda create -n gender_gui python=3.10 -y conda activate gender_gui # 安装 PyTorch,CPU 版本先跑通逻辑,有 GPU 再换 cu 版本 # 注意:具体 CUDA 版本要和你显卡驱动匹配,不要照抄 pip install torch torchvision --index-url https://download.pytorch.org/whl/cpu # GUI 和图像处理依赖 pip install PyQt5 opencv-python pillow numpy这里的关键参数是 Python 版本和 PyTorch 安装源。Python 3.10 是目前生态兼容性最稳的区间,太新的版本有些 GUI 库还没跟上。--index-url指向官方 wheel 源,能避免 pip 从源码编译导致的长时间卡顿。如果你有 NVIDIA 显卡,把 cpu 换成对应的 cu 版本,但一定要先确认驱动支持的 CUDA 上限,否则装完torch.cuda.is_available()返回 False,白折腾。
提示:装完立刻验证,别等写完代码才发现环境有问题。
import torch import torchvision import cv2 from PyQt5 import QtWidgets print("torch:", torch.__version__) print("cuda available:", torch.cuda.is_available()) print("torchvision:", torchvision.__version__) print("opencv:", cv2.__version__)这段验证代码的作用是逐项确认核心依赖能正常导入。torch.cuda.is_available()是判断 GPU 是否可用的唯一标准,不要靠nvidia-smi能显示就认为 PyTorch 能用 GPU。如果返回 False 而你有显卡,大概率是 PyTorch 版本和 CUDA 不匹配,回退重装比硬调快得多。
2.2 数据集选择与目录结构设计
人脸性别识别常用的公开数据集是 UTKFace 和 CelebA。UTKFace 大约两万张图,文件名里直接带了年龄、性别、种族标签,解析起来最省事,适合毕设快速起步。CelebA 数量更大但标签需要额外处理。我一般选 UTKFace,因为它的标注格式对新手友好。
目录结构建议按下面这样组织,训练脚本和 GUI 推理脚本共用同一套路径常量,避免后期改路径改到崩溃:
gender_project/ ├── data/ │ ├── train/ │ │ ├── male/ │ │ └── female/ │ └── val/ │ ├── male/ │ └── female/ ├── models/ │ └── best_model.pth ├── train.py ├── inference.py └── gui_main.py按类别分文件夹的好处是可以直接用torchvision.datasets.ImageFolder,它会自动根据文件夹名生成标签映射。UTKFace 原始文件名形如25_0_2_20170104.jpg,其中第二段是性别(0 男 1 女),写个脚本按这个规则把图片复制到对应文件夹即可。划分比例我一般用 8:2,验证集不要太小,否则评估指标波动大,答辩时说不清楚。
import os import shutil import random SRC = "UTKFace" DST = "data" random.seed(42) # 固定随机种子,保证每次划分一致 for split in ["train", "val"]: for cls in ["male", "female"]: os.makedirs(os.path.join(DST, split, cls), exist_ok=True) files = [f for f in os.listdir(SRC) if f.endswith(".jpg")] random.shuffle(files) split_idx = int(len(files) * 0.8) for i, fname in enumerate(files): gender = int(fname.split("_")[1]) # 文件名第二段是性别标签 cls = "male" if gender == 0 else "female" split = "train" if i < split_idx else "val" shutil.copy(os.path.join(SRC, fname), os.path.join(DST, split, cls, fname))这段脚本做了三件事:建目录、打乱数据、按 8:2 复制到对应文件夹。random.seed(42)是血泪经验,不固定种子的话每次划分结果不同,你调参时根本分不清是模型变好了还是数据变了。fname.split("_")[1]依赖 UTKFace 的命名规则,如果你换数据集,这一行必须改。
3. 模型训练:用迁移学习把准确率拉到可用水平
3.1 为什么选 ResNet18 而不是自己搭 CNN
毕设场景下,从零搭一个五六层 CNN 也能跑,但准确率通常卡在 85% 左右上不去,而且训练慢。我一般直接用 torchvision 里的 ResNet18 做迁移学习,把最后的全连接层改成二分类输出。理由是 ResNet18 参数量适中(约 1100 万),在 CPU 上也能推理,预训练权重已经学到了通用的人脸特征,微调几轮就能到 95% 以上。
import torch import torch.nn as nn from torchvision import models def build_model(num_classes=2, freeze_backbone=True): model = models.resnet18(weights=models.ResNet18_Weights.DEFAULT) if freeze_backbone: # 冻结主干,只训练最后的分类头,适合数据量小的场景 for param in model.parameters(): param.requires_grad = False # 替换全连接层,输入维度是 ResNet18 的 512 model.fc = nn.Linear(512, num_classes) return modelfreeze_backbone=True时只训练最后的fc层,训练快、不容易过拟合,适合 UTKFace 这种两万张量级的数据。如果你的数据超过十万张,可以把freeze_backbone设为 False 做全量微调,但学习率要调小到 1e-4 量级,否则预训练权重会被冲掉。model.fc = nn.Linear(512, num_classes)这一行是必须的,不改的话输出还是 ImageNet 的 1000 类。
3.2 训练循环与关键参数设置
训练脚本的核心是数据增强、损失函数和优化器三块。数据增强我一般只做随机水平翻转和轻微颜色抖动,不做旋转,因为人脸旋转后性别特征会变形,反而降低效果。
from torchvision import transforms from torch.utils.data import DataLoader from torchvision.datasets import ImageFolder train_tf = transforms.Compose([ transforms.Resize((128, 128)), # 统一尺寸,ResNet 最小输入要求 transforms.RandomHorizontalFlip(), # 水平翻转,人脸对称性安全 transforms.ColorJitter(brightness=0.2, contrast=0.2), transforms.ToTensor(), transforms.Normalize([0.5, 0.5, 0.5], [0.5, 0.5, 0.5]) ]) val_tf = transforms.Compose([ transforms.Resize((128, 128)), transforms.ToTensor(), transforms.Normalize([0.5, 0.5, 0.5], [0.5, 0.5, 0.5]) ]) train_ds = ImageFolder("data/train", transform=train_tf) val_ds = ImageFolder("data/val", transform=val_tf) train_loader = DataLoader(train_ds, batch_size=64, shuffle=True, num_workers=4) val_loader = DataLoader(val_ds, batch_size=64, shuffle=False, num_workers=4)Resize((128, 128))是速度和精度的折中,用 224 精度会略高但训练时间翻倍,毕设演示 128 足够。Normalize的均值和方差设成 0.5 是因为人脸数据分布比较集中,用 ImageNet 的统计值也可以,但 0.5 更简单。num_workers=4在 Windows 上如果报错就改成 0,这是 DataLoader 多进程在 Windows 下的经典坑。
device = torch.device("cuda" if torch.cuda.is_available() else "cpu") model = build_model().to(device) criterion = nn.CrossEntropyLoss() optimizer = torch.optim.Adam(filter(lambda p: p.requires_grad, model.parameters()), lr=1e-3) EPOCHS = 15 best_acc = 0.0 for epoch in range(EPOCHS): model.train() for imgs, labels in train_loader: imgs, labels = imgs.to(device), labels.to(device) optimizer.zero_grad() loss = criterion(model(imgs), labels) loss.backward() optimizer.step() # 验证阶段 model.eval() correct, total = 0, 0 with torch.no_grad(): for imgs, labels in val_loader: imgs, labels = imgs.to(device), labels.to(device) preds = model(imgs).argmax(dim=1) correct += (preds == labels).sum().item() total += labels.size(0) acc = correct / total print(f"epoch {epoch+1}, val_acc {acc:.4f}") if acc > best_acc: best_acc = acc torch.save(model.state_dict(), "models/best_model.pth")filter(lambda p: p.requires_grad, ...)只把需要更新的参数交给优化器,冻结主干时这一步能避免无谓计算。lr=1e-3是 Adam 的常用起点,如果 loss 震荡就降到 5e-4。保存best_model.pth而不是最后一个 epoch 的权重,是因为训练后期可能过拟合,验证准确率反而下降。15 个 epoch 在单卡上大约十几分钟,CPU 上可能要一两个小时,建议先用小批量数据跑通流程再全量训练。
4. 推理接口与 GUI 集成:让模型真正能被点开用
4.1 把模型封装成单张图片推理函数
GUI 需要的是一个「输入图片路径,输出性别和置信度」的函数,而不是训练脚本里那套 batch 逻辑。我一般单独写一个inference.py,把模型加载和预处理封装成类,GUI 只调用这一个接口。
import torch from PIL import Image from torchvision import transforms from train import build_model class GenderPredictor: def __init__(self, weight_path="models/best_model.pth"): self.device = torch.device("cuda" if torch.cuda.is_available() else "cpu") self.model = build_model(freeze_backbone=False) self.model.load_state_dict(torch.load(weight_path, map_location=self.device)) self.model.to(self.device).eval() self.tf = transforms.Compose([ transforms.Resize((128, 128)), transforms.ToTensor(), transforms.Normalize([0.5, 0.5, 0.5], [0.5, 0.5, 0.5]) ]) self.classes = ["male", "female"] def predict(self, img_path): img = Image.open(img_path).convert("RGB") tensor = self.tf(img).unsqueeze(0).to(self.device) with torch.no_grad(): probs = torch.softmax(self.model(tensor), dim=1)[0] idx = probs.argmax().item() return self.classes[idx], probs[idx].item()load_state_dict时加map_location是为了兼容「GPU 训练、CPU 推理」的场景,不加的话在没显卡的机器上会直接报错。unsqueeze(0)是把单张图变成 batch 维度为 1 的张量,模型 forward 要求四维输入。torch.softmax把 logits 转成概率,返回的置信度可以显示在界面上,答辩时比只给一个标签更有说服力。
4.2 用 PyQt5 搭一个能演示的界面
GUI 框架我选 PyQt5 而不是 Tkinter,原因是 PyQt5 的布局和图片显示更省心,做出来的界面也更像「正经软件」。核心控件只有三个:一个显示图片的 QLabel、一个选择图片的按钮、一个显示结果的文本框。
import sys from PyQt5.QtWidgets import (QApplication, QWidget, QLabel, QPushButton, QVBoxLayout, QFileDialog, QMessageBox) from PyQt5.QtGui import QPixmap from inference import GenderPredictor class MainWindow(QWidget): def __init__(self): super().__init__() self.setWindowTitle("人脸性别识别系统") self.resize(500, 600) self.predictor = GenderPredictor() self.img_label = QLabel("请选择一张人脸图片") self.img_label.setFixedSize(400, 400) self.img_label.setStyleSheet("border: 1px solid gray;") self.btn = QPushButton("选择图片") self.btn.clicked.connect(self.on_select) self.result_label = QLabel("结果:") layout = QVBoxLayout() layout.addWidget(self.img_label) layout.addWidget(self.btn) layout.addWidget(self.result_label) self.setLayout(layout) def on_select(self): path, _ = QFileDialog.getOpenFileName(self, "选择图片", "", "Images (*.jpg *.png)") if not path: return pixmap = QPixmap(path).scaled(400, 400) self.img_label.setPixmap(pixmap) try: gender, conf = self.predictor.predict(path) self.result_label.setText(f"结果:{gender} 置信度:{conf:.2%}") except Exception as e: QMessageBox.warning(self, "推理失败", str(e)) if __name__ == "__main__": app = QApplication(sys.argv) win = MainWindow() win.show() sys.exit(app.exec_())on_select里先判断path是否为空,用户点取消时直接返回,不判断的话会拿空路径去推理然后报错。QPixmap(path).scaled(400, 400)把图片缩放到控件大小,不缩放的话大图会把界面撑变形。try/except包住推理调用,遇到非人脸图片或损坏文件时弹窗提示而不是让程序崩溃,这是演示时保命的写法。
注意:
GenderPredictor()在窗口初始化时就加载模型,如果模型文件不存在会直接抛异常导致窗口打不开。建议在__init__里加一层文件存在性检查,或者把加载放到按钮点击时懒执行。
5. 避坑与排查:那些让毕设演示翻车的细节
5.1 现象:训练准确率很高,但 GUI 里预测全是同一类
原因通常是预处理不一致。训练时用了Normalize([0.5,0.5,0.5],[0.5,0.5,0.5]),推理时如果忘了这一步,或者 Resize 尺寸对不上,模型看到的输入分布就变了,输出会退化成常数。解决方法是把训练和推理的 transform 抽成同一个函数或同一个模块里的常量,两边引用同一份定义,不要各写各的。
5.2 现象:PyQt5 界面能打开但点按钮没反应
多数是信号连接写错,比如self.btn.clicked.connect(self.on_select)写成了self.btn.clicked.connect(self.on_select()),加了括号等于立即执行函数并把返回值当槽函数。检查所有connect后面跟的是函数名而不是调用。另一个可能是槽函数里抛了异常被 Qt 吞掉,可以在on_select开头加print确认是否进入。
5.3 现象:DataLoader 在 Windows 上报 BrokenPipeError 或卡死
这是num_workers > 0时 Windows 多进程的经典问题。解决方法有两个:把num_workers设为 0,或者把训练代码包在if __name__ == "__main__":里。我一般直接设 0,虽然慢一点但稳定,毕设数据量不大,不值得为这点速度折腾。
5.4 现象:模型文件几百 MB,打包发给老师时超限
ResNet18 的权重约 45MB,如果保存时把优化器状态也存进去会更大。保存时只存model.state_dict()而不是整个model,能省一半以上空间。如果还嫌大,可以换 MobileNetV2,权重只有十几 MB,精度略降但演示够用。
5.5 现象:换一台电脑运行报 CUDA 相关错误
训练用了 GPU,推理机器没有显卡,torch.load默认会尝试恢复到原设备。解决方法是加载时加map_location="cpu",并且把模型.to(device)里的 device 动态判断。这个坑在答辩换机器时特别常见,提前在无显卡环境测一遍能省很多事。
6. 进阶技巧:用 OpenCV 做实时摄像头推理与置信度过滤
把静态图片推理跑通后,很多人会想加摄像头实时识别,这一步能让毕设的演示效果提升一个档次。核心思路是用 OpenCV 读摄像头帧,用 Haar 或 DNN 人脸检测器裁出人脸区域,再送进GenderPredictor。但直接每帧都推理会卡,我一般每隔 5 帧推理一次,中间帧复用上次结果。
import cv2 from inference import GenderPredictor predictor = GenderPredictor() face_cascade = cv2.CascadeClassifier( cv2.data.haarcascades + "haarcascade_frontalface_default.xml" ) cap = cv2.VideoCapture(0) frame_count = 0 last_result = ("unknown", 0.0) while True: ret, frame = cap.read() if not ret: break frame_count += 1 gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) faces = face_cascade.detectMultiScale(gray, 1.3, 5) for (x, y, w, h) in faces: # 每隔 5 帧推理一次,降低计算压力 if frame_count % 5 == 0: face_img = frame[y:y+h, x:x+w] cv2.imwrite("tmp_face.jpg", face_img) last_result = predictor.predict("tmp_face.jpg") label = f"{last_result[0]} {last_result[1]:.2f}" cv2.rectangle(frame, (x, y), (x+w, y+h), (0, 255, 0), 2) cv2.putText(frame, label, (x, y-10), cv2.FONT_HERSHEY_SIMPLEX, 0.8, (0, 255, 0), 2) cv2.imshow("Gender Detection", frame) if cv2.waitKey(1) & 0xFF == ord("q"): break cap.release() cv2.destroyAllWindows()detectMultiScale(gray, 1.3, 5)里 1.3 是缩放步长,5 是邻域阈值,调大 5 能减少误检但可能漏检小脸,实时场景我一般用这组默认值。frame_count % 5是性能关键,每帧推理在 CPU 上只能跑到几帧,隔帧推理能到 20 帧以上。cv2.imwrite写临时文件再读是为了复用predict接口,追求性能的话可以把 PIL 读取改成直接传 numpy 数组,但毕设阶段没必要过度优化。
置信度过滤是另一个实用技巧。当last_result[1] < 0.7时,我一般显示「不确定」而不是硬给一个性别,因为侧脸、遮挡、光照差的情况下模型输出不可靠,强行显示错误结果反而扣分。这个阈值可以根据你的验证集表现调整,如果模型整体置信度偏低就降到 0.6。
最后说个我自己的习惯:每次改完推理代码,一定先用同一张测试图跑一遍,确认输出和改之前一致,再去看摄像头效果。因为实时画面变量太多,一旦结果不对,你分不清是模型问题、检测问题还是摄像头问题。固定一张图做基准,能帮你快速定位改动引入的 bug。希望帮到你。
本文还有配套的精品资源,点击获取