☰
基于PyTorch的人脸性别识别GUI系统:从模型训练到毕设交付全流程
2026/10/1 3:22:59 网站建设 项目流程

简介:这份资源面向计算机、人工智能相关专业的本科生与自学者,提供一套基于 Python 与 PyTorch 实现的人脸性别识别完整方案,可直接作为课程大作业或毕业设计参考。数据集涵盖白种人、黄种人、黑种人等多种族样本,并包含姿态、光照、年龄等干扰因素,训练、验证、测试集按 40%、10%、50% 随机划分,具备一定挑战性。压缩包共 18 个文件,以 13 个 py 脚本为核心,辅以 txt 名单、docx 实验报告、md 说明与 license 文件,整体约 1.21MB,结构紧凑便于阅读与复现。资源强调自主实现,不允许直接调用开源预训练模型或现成代码,报告中需说明所用模型方法、测试结果并附文献引用,同时提供 GUI 界面,可输入图像后显示性别识别结果。目前已有 648 人学习,适合希望掌握从数据处理、模型搭建到界面展示全流程的读者参考。

1. 人脸性别识别 GUI 系统:从 PyTorch 训练到可交付毕设的完整路径

很多同学做毕设时卡在一个尴尬的位置:模型在 notebook 里跑通了,准确率也还行,但导师一句「做个界面演示一下」就懵了。人脸性别识别这个题目本身不复杂,真正拉开差距的是能不能把它做成一个能双击运行、能选图片、能实时看摄像头、还能写进报告里的完整系统。这篇内容就是围绕「基于 python pytorch 实现人脸性别识别 GUI 系统」这条线,把数据准备、模型选型、训练调参、GUI 封装、报告撰写这几个环节串起来讲清楚。适合正在做毕设的本科生、想快速搭一个可演示 demo 的开发者,以及需要把深度学习模型落地成桌面工具的人。读完你应该能自己复现一套完整流程,而不是只拿到一个跑不起来的压缩包。

2. 数据与模型选型:为什么不用自己从零训一个 backbone

2.1 人脸性别识别的数据长什么样、从哪来

性别识别本质是一个二分类任务,但它和人脸识别、人脸检测是三个不同的东西。人脸检测负责框出人脸位置,人脸识别负责判断「这是谁」,性别识别只判断「这是男是女」。实际做的时候,输入通常是一张已经裁剪好的人脸图,输出是 male / female 两个类别之一。

公开数据集里,常用的是 UTKFace、CelebA 这两个。UTKFace 大概两万多张,文件名里直接带了年龄、性别、种族标签,解析起来非常省事。CelebA 量大但标签需要额外处理。如果只是做毕设,UTKFace 足够用,而且它的图片尺寸不统一,正好能练一下预处理流程。

我一般会按 8:1:1 划分训练集、验证集、测试集,并且按性别做分层抽样,避免某一类样本过少导致模型偏向。下面这段代码是解析 UTKFace 文件名并生成划分清单的常见写法:

import os import random from sklearn.model_selection import train_test_split def parse_utkface(data_dir): samples = [] for fname in os.listdir(data_dir): if not fname.lower().endswith(('.jpg', '.png')): continue # UTKFace 文件名格式: age_gender_race_date.jpg parts = fname.split('_') if len(parts) < 3: continue try: age = int(parts[0]) gender = int(parts[1]) # 0 男 1 女 except ValueError: continue samples.append((os.path.join(data_dir, fname), gender)) return samples samples = parse_utkface('./UTKFace') labels = [s[1] for s in samples] train_val, test = train_test_split(samples, test_size=0.1, stratify=labels, random_state=42) train, val = train_test_split(train_val, test_size=0.111, stratify=[s[1] for s in train_val], random_state=42) print(len(train), len(val), len(test))

这段逻辑的关键点是stratify=labels,它保证划分后每一类的比例和原始数据一致。random_state固定住是为了让结果可复现,写报告时这一点很重要,否则导师问你为什么两次跑出来不一样,你没法解释。参数上test_size=0.1表示测试集占 10%,0.111是在剩余 90% 里再切出约 10% 作为验证集,最终比例接近 8:1:1。

2.2 用预训练 ResNet 还是自己搭 CNN

这是选型上最容易纠结的地方。我的建议很直接:毕设场景下,用 torchvision 里的预训练 ResNet18 或 MobileNetV3,把最后一层全连接改成二分类输出,冻结前面几层做微调。原因有三个。

第一,UTKFace 只有两万多张,从零训一个 CNN 很容易过拟合,验证集准确率上不去。第二,预训练模型在 ImageNet 上学到的边缘、纹理特征对人脸任务有迁移价值,收敛快。第三,ResNet18 参数量小,CPU 也能推理,GUI 里实时跑摄像头不会卡成幻灯片。

如果你非要从零搭,也不是不行,但至少要有 BatchNorm 和 Dropout,否则训练曲线会很难看。下面是用预训练模型改头的标准写法:

import torch import torch.nn as nn from torchvision import models def build_model(pretrained=True): # 使用 ResNet18 作为 backbone model = models.resnet18(weights=models.ResNet18_Weights.DEFAULT if pretrained else None) # 替换最后的全连接层,输出 2 类 in_features = model.fc.in_features model.fc = nn.Sequential( nn.Dropout(0.3), nn.Linear(in_features, 2) ) return model model = build_model() print(sum(p.numel() for p in model.parameters()))

ResNet18_Weights.DEFAULT会自动下载官方预训练权重,第一次运行需要联网。Dropout(0.3)是防止全连接层过拟合,这个值在 0.2 到 0.5 之间调,太大欠拟合,太小没效果。替换model.fc之后,只有这一层和后面的层参数是随机初始化的,训练时可以只优化这部分,也可以全部微调,取决于你的数据量和显存。

提示:如果显存紧张,可以先把 backbone 冻结,只训 fc 层几个 epoch,再解冻全部微调。这样比一上来就全量微调更稳。

3. 训练流程与参数设置:让验证集准确率稳定在 90% 以上

3.1 数据增强和 DataLoader 的配置细节

数据增强在人脸任务里要克制。水平翻转可以用,因为人脸左右对称,性别不会因为翻转改变。但垂直翻转、大角度旋转要慎用,否则会出现「倒着的脸」这种训练集里不存在的样本,反而干扰学习。颜色抖动可以轻微加一点,模拟不同光照。

下面是一个完整的 Dataset 和 DataLoader 配置:

from torch.utils.data import Dataset, DataLoader from torchvision import transforms from PIL import Image class FaceGenderDataset(Dataset): def __init__(self, samples, transform=None): self.samples = samples self.transform = transform def __len__(self): return len(self.samples) def __getitem__(self, idx): path, label = self.samples[idx] img = Image.open(path).convert('RGB') if self.transform: img = self.transform(img) return img, label train_tf = transforms.Compose([ transforms.Resize((128, 128)), transforms.RandomHorizontalFlip(p=0.5), transforms.ColorJitter(brightness=0.2, contrast=0.2), transforms.ToTensor(), transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]) ]) val_tf = transforms.Compose([ transforms.Resize((128, 128)), transforms.ToTensor(), transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]) ]) train_ds = FaceGenderDataset(train, train_tf) val_ds = FaceGenderDataset(val, 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 会更准但更慢,GUI 实时场景下 128 够用。Normalize的均值和方差是 ImageNet 的统计值,用预训练模型就必须保持一致,否则输入分布对不上,效果会掉。num_workers=4在 Windows 上如果报错,改成 0 就行,这是多进程加载的常见坑。

3.2 损失函数、优化器和学习率调度

二分类用 CrossEntropyLoss 就够了,不需要自己写。优化器选 AdamW,学习率从 1e-3 开始,配合 CosineAnnealingLR 或者 StepLR。如果做全量微调,学习率要调小到 1e-4 级别,否则预训练权重会被冲掉。

import torch.optim as optim from torch.optim.lr_scheduler import CosineAnnealingLR device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') model = build_model().to(device) criterion = nn.CrossEntropyLoss() optimizer = optim.AdamW(model.parameters(), lr=1e-3, weight_decay=1e-4) scheduler = CosineAnnealingLR(optimizer, T_max=20) def train_one_epoch(model, loader, optimizer, criterion, device): model.train() total_loss, correct, total = 0, 0, 0 for imgs, labels in loader: imgs, labels = imgs.to(device), labels.to(device) optimizer.zero_grad() outputs = model(imgs) loss = criterion(outputs, labels) loss.backward() optimizer.step() total_loss += loss.item() * imgs.size(0) preds = outputs.argmax(dim=1) correct += (preds == labels).sum().item() total += imgs.size(0) return total_loss / total, correct / total

weight_decay=1e-4是 L2 正则,配合 Dropout 一起抑制过拟合。CosineAnnealingLR让学习率按余弦曲线下降,前期大步走,后期小步收敛,比固定学习率稳。T_max=20表示 20 个 epoch 走完一个周期,你可以根据总 epoch 数调整。

训练循环里每个 epoch 结束后跑一次验证,记录 val loss 和 val acc,保存验证集准确率最高的那个权重,而不是最后一个。这是血泪经验,最后一个 epoch 往往已经过拟合了。

3.3 训练曲线怎么看、什么时候该停

正常情况下的曲线是:train loss 持续下降,val loss 先降后升,val acc 先升后平。当 val loss 连续 3 到 5 个 epoch 不再下降,就可以早停了。如果 train acc 到 99% 而 val acc 卡在 80%,说明过拟合严重,要么加数据增强,要么减小模型,要么加 Dropout。

如果 val acc 一直上不去,先检查数据标签有没有错,再看学习率是不是太大导致震荡。我见过有人把 gender 标签解析反了,训练半天准确率一直在 50% 附近晃,这就是典型的标签错误。

4. GUI 封装:用 PyQt5 把模型包成能双击运行的程序

4.1 界面布局和功能模块划分

GUI 部分用 PyQt5 是最常见的选择,资料多、组件全。界面我一般分三块:左边是图片显示区,右边是按钮区(选择图片、打开摄像头、开始识别),底部是结果显示区(性别、置信度、耗时)。

核心逻辑是:选择图片后,先用 OpenCV 的 Haar 级联或者 DNN 人脸检测器框出人脸,裁剪出来送进模型,得到性别和置信度,再画回原图显示。摄像头模式就是循环读帧、检测、识别、显示。

import sys import cv2 import torch from PyQt5.QtWidgets import (QApplication, QMainWindow, QLabel, QPushButton, QVBoxLayout, QHBoxLayout, QWidget, QFileDialog) from PyQt5.QtGui import QImage, QPixmap from PyQt5.QtCore import Qt, QTimer class GenderGUI(QMainWindow): def __init__(self, model, device): super().__init__() self.model = model self.device = device self.cap = None self.timer = QTimer() self.timer.timeout.connect(self.update_frame) self.init_ui() def init_ui(self): self.setWindowTitle('人脸性别识别系统') self.img_label = QLabel('请选择图片或打开摄像头') self.img_label.setAlignment(Qt.AlignCenter) self.img_label.setMinimumSize(640, 480) self.result_label = QLabel('结果:--') self.btn_open = QPushButton('选择图片') self.btn_cam = QPushButton('打开摄像头') self.btn_open.clicked.connect(self.open_image) self.btn_cam.clicked.connect(self.toggle_camera) right = QVBoxLayout() right.addWidget(self.btn_open) right.addWidget(self.btn_cam) right.addStretch() layout = QHBoxLayout() layout.addWidget(self.img_label, stretch=3) layout.addLayout(right, stretch=1) container = QVBoxLayout() container.addLayout(layout) container.addWidget(self.result_label) central = QWidget() central.setLayout(container) self.setCentralWidget(central)

QTimer是用来定时刷新摄像头帧的,不能直接在按钮回调里写 while 循环,否则界面会卡死。img_label设置最小尺寸是为了布局稳定,不然窗口一缩放图片区域就塌了。

4.2 人脸检测加性别推理的完整链路

推理部分要封装成一个函数,输入是 BGR 图像,输出是带框和标签的图像。人脸检测用 OpenCV 自带的haarcascade_frontalface_default.xml就够,虽然精度一般但零依赖。追求效果可以换 DNN 检测器,但模型文件要额外下载。

import numpy as np from torchvision import transforms infer_tf = transforms.Compose([ transforms.Resize((128, 128)), transforms.ToTensor(), transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]) ]) face_cascade = cv2.CascadeClassifier( cv2.data.haarcascades + 'haarcascade_frontalface_default.xml') def predict_frame(frame, model, device): gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) faces = face_cascade.detectMultiScale(gray, 1.1, 5, minSize=(60, 60)) results = [] for (x, y, w, h) in faces: roi = frame[y:y+h, x:x+w] roi_rgb = cv2.cvtColor(roi, cv2.COLOR_BGR2RGB) pil_img = Image.fromarray(roi_rgb) tensor = infer_tf(pil_img).unsqueeze(0).to(device) with torch.no_grad(): out = model(tensor) prob = torch.softmax(out, dim=1)[0] pred = prob.argmax().item() conf = prob[pred].item() label = 'Female' if pred == 1 else 'Male' color = (0, 255, 0) if pred == 0 else (255, 0, 0) cv2.rectangle(frame, (x, y), (x+w, y+h), color, 2) cv2.putText(frame, f'{label} {conf:.2f}', (x, y-10), cv2.FONT_HERSHEY_SIMPLEX, 0.8, color, 2) results.append((label, conf)) return frame, results

detectMultiScale的1.1是每次缩放比例,5是邻域阈值,值越大检测越少但误检也少。minSize=(60,60)过滤掉太小的框,避免把噪点当人脸。推理时torch.no_grad()必须加,否则会累积计算图,显存越跑越满。

注意:模型推理前一定要model.eval(),否则 BatchNorm 和 Dropout 还是训练模式,结果会不稳定。

4.3 把训练好的权重接进 GUI

训练脚本保存权重时用torch.save(model.state_dict(), 'gender_resnet18.pth'),GUI 启动时加载。加载时要先构建同样的模型结构,再load_state_dict,最后eval()和to(device)。

def load_model(weight_path, device): model = build_model(pretrained=False) state = torch.load(weight_path, map_location=device) model.load_state_dict(state) model.to(device) model.eval() return model if __name__ == '__main__': dev = torch.device('cuda' if torch.cuda.is_available() else 'cpu') net = load_model('gender_resnet18.pth', dev) app = QApplication(sys.argv) win = GenderGUI(net, dev) win.show() sys.exit(app.exec_())

map_location=device是为了在没有 GPU 的机器上也能加载 GPU 训出来的权重,不加会报错。pretrained=False是因为我们要加载自己的权重,不需要再下载 ImageNet 的。

5. 避坑与排查:那些让毕设卡住三天的具体问题

5.1 现象:训练准确率一直卡在 50% 左右

原因通常是标签解析错了,或者标签和类别索引对不上。UTKFace 文件名里 gender 是 0 男 1 女,但有些教程会写成 0 女 1 男,如果你混用了两份代码,模型学到的就是反的。解决方法是打印前 20 个样本的路径和标签,人工核对几张图,确认标签和图片内容一致。

5.2 现象:GUI 打开摄像头后界面卡死

原因是在主线程里写了while True读帧循环,Qt 的事件循环被阻塞了。解决方法是把读帧逻辑放到QTimer的回调里,每 30 毫秒触发一次,每次只处理一帧。这样界面还能响应按钮点击。另外摄像头释放要在关闭窗口时做,否则下次打开会提示设备被占用。

5.3 现象:PyQt5 报错「could not find or load the Qt platform plugin windows」

这是环境变量冲突,通常是 conda 环境和系统里其他 Qt 库打架。解决方法是在代码最开头加import os; os.environ['QT_QPA_PLATFORM_PLUGIN_PATH'] = '',或者干脆用 pip 重装 PyQt5,不要混用 conda 和 pip 安装的版本。这个坑在 Windows 上特别常见,翻车率很高。

5.4 现象:模型在测试集上准确率 92%,但实际用摄像头只有 70%

原因是训练数据都是裁剪好的人脸,而摄像头画面背景复杂、光照多变,分布不一致。解决方法是在训练时加入随机裁剪和背景干扰,或者用 OpenCV 先做一次人脸检测再送模型,保证输入和训练时一致。另外 Haar 检测器本身会漏检侧脸,侧脸识别率低是正常的,报告里可以如实写。

5.5 现象:打包成 exe 后运行报错找不到模型文件

PyInstaller 打包后,相对路径的基准目录变了。解决方法是用sys._MEIPASS获取临时解压目录,把模型文件路径拼成绝对路径。或者简单点,把模型文件放在 exe 同级目录,用os.path.dirname(sys.executable)定位。

6. 进阶技巧:把报告和演示做得让导师挑不出毛病

毕设不只是代码,报告和演示同样重要。我一般会在报告里放三样东西:训练曲线图、混淆矩阵、以及一组对比实验。训练曲线用 matplotlib 画 train/val 的 loss 和 acc,混淆矩阵用 sklearn 的confusion_matrix加 seaborn 热力图,对比实验可以比较 ResNet18 和 MobileNetV3 在同样数据上的准确率和推理耗时。

import matplotlib.pyplot as plt from sklearn.metrics import confusion_matrix import seaborn as sns def plot_curves(train_loss, val_loss, train_acc, val_acc): epochs = range(1, len(train_loss) + 1) fig, axes = plt.subplots(1, 2, figsize=(12, 4)) axes[0].plot(epochs, train_loss, label='train') axes[0].plot(epochs, val_loss, label='val') axes[0].set_title('Loss') axes[0].legend() axes[1].plot(epochs, train_acc, label='train') axes[1].plot(epochs, val_acc, label='val') axes[1].set_title('Accuracy') axes[1].legend() plt.savefig('training_curve.png', dpi=150) def plot_cm(y_true, y_pred): cm = confusion_matrix(y_true, y_pred) sns.heatmap(cm, annot=True, fmt='d', cmap='Blues', xticklabels=['Male', 'Female'], yticklabels=['Male', 'Female']) plt.xlabel('Predicted') plt.ylabel('True') plt.savefig('confusion_matrix.png', dpi=150)

dpi=150是为了报告里插图清晰,默认 72 打印出来会糊。混淆矩阵能直观看出模型是不是偏向某一类,如果 Male 的召回率明显低于 Female,说明男性样本可能偏少或者特征更难学。

演示环节我建议准备一个「后悔药」:提前录一段摄像头识别的视频,万一现场摄像头抽风或者光线太差,直接放视频兜底。另外把测试集里识别正确的和错误的各挑几张,做成对比图,导师问起来你能说清楚模型的边界在哪,这比只报一个准确率数字有说服力得多。

还有一个容易被忽略的点:requirements.txt 一定要写清楚版本。torch==2.0.1、torchvision==0.15.2、PyQt5==5.15.9这种,不要只写包名。我吃过亏,换台机器装最新版 torch,API 变了,pretrained=True直接报错,因为新版本改成了weights参数。报告里附上环境配置,既是规范,也是给自己留后路。

最后说个习惯:每改一版代码就 commit 一次,训练权重按日期命名,比如resnet18_20240501.pth。毕设周期长,中间隔两周回来你根本记不清哪个权重是哪个配置训的。这个习惯帮我省过很多次重新训练的时间。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询