简介:本资源面向高校学生与深度学习入门者,提供一套基于PyTorch实现人脸性别识别的完整课程设计或毕设方案。数据集涵盖白种人、黄种人、黑种人等多种族样本,并包含姿态、光照、年龄等干扰因素,需按40%、10%、50%随机划分训练、验证与测试集,具有一定挑战性。压缩包共18个文件,以13个Python脚本为核心,涵盖数据加载、网络定义、多版本训练与GUI推理等模块,另附docx实验报告、md说明文档及txt姓名列表等辅助材料,整体约1.21MB。资源强调独立实现,不允许直接调用开源预训练模型或现成代码,报告中需说明模型方法、测试结果并给出文献引用,附加任务为设计GUI界面输入图像后显示性别。目前已有648人学习,适合需要完整实验流程、可运行代码与报告参考的读者借鉴。
1. 从一张毕业设计封面说起:人脸性别识别 GUI 到底在做什么
每年五六月,实验室里总有人抱着一台笔记本问同一个问题:有没有一个能跑起来、能演示、还能写进报告的人脸性别识别系统?标题里这套「基于 python pytorch 实现人脸性别识别 GUI 系统」,本质就是把三件事缝在一起——用 PyTorch 训练一个二分类卷积网络判断人脸是男是女,用 OpenCV 做检测和预处理,再套一层 Tkinter 或 PyQt 的图形界面,让答辩老师点一下按钮就能看到摄像头画面里跳出「Male / Female」的标签。它解决的不是工业级精度问题,而是「从数据集到可交互 Demo」的完整链路问题,适合毕设、课程设计、入门 PyTorch 实战的人。很多人卡在环境搭建和 GUI 与推理线程打架上,这篇就把这条链路拆开讲清楚,包括数据集怎么选、模型怎么改、界面怎么不卡死、报告里该放哪些图。
2. 数据集、模型与推理链路:先把技术选型定下来
2.1 为什么选 UTKFace 或 Adience,而不是自己爬
人脸性别识别第一个翻车点就是数据集。自己爬图听起来自由,但标注成本高、噪声大,训练出来的模型在答辩现场大概率把老师认成反的。常见做法是直接用公开数据集:UTKFace 约两万多张,文件名里带年龄、性别、种族标签,解析方便;Adience 更贴近真实场景,但标注格式乱,需要写解析脚本。我一般推荐 UTKFace 起步,因为它的标签直接写在文件名里,例如25_0_2_20170116174525125.jpg,第一位是年龄,第二位 0 表示男、1 表示女。这样写 Dataset 类时不用额外读 CSV,减少出错环节。
选 UTKFace 的另一个理由是图像已经对齐得比较好,人脸基本居中,省掉了大量预处理调试时间。如果你的题目要求「复杂场景」,那再考虑 Adience 或 WIDER FACE 配合性别标注,但工作量会翻倍。毕设场景下,UTKFace 加一个 OpenCV 的 Haar 或 DNN 检测器做推理前裁剪,已经足够撑起报告里的「数据预处理」章节。
2.2 用 ResNet18 做二分类:改哪里、为什么改
模型选型上,不要一上来就上 ResNet50 或 ViT。毕设的算力通常是单张消费级显卡甚至 CPU,ResNet18 在 224×224 输入下参数量约 1100 万,训练快、显存占用低,精度对性别二分类足够。PyTorch 里改分类头只需要替换最后的全连接层。下面是最小可运行的模型定义和训练循环骨架。
import torch import torch.nn as nn from torchvision import models, transforms from torch.utils.data import Dataset, DataLoader from PIL import Image import os class UTKFaceDataset(Dataset): def __init__(self, root_dir, transform=None): self.root_dir = root_dir self.transform = transform # 只保留文件名符合 年龄_性别_种族_时间.jpg 的样本 self.samples = [] for fname in os.listdir(root_dir): if not fname.lower().endswith('.jpg'): continue parts = fname.split('_') if len(parts) < 2: continue try: gender = int(parts[1]) except ValueError: continue if gender not in (0, 1): continue self.samples.append((os.path.join(root_dir, fname), gender)) 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 # 训练和验证的变换要分开:训练加增强,验证只做 Resize 和 Normalize train_tf = transforms.Compose([ transforms.Resize((224, 224)), transforms.RandomHorizontalFlip(), 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((224, 224)), transforms.ToTensor(), transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]) ]) def build_model(): model = models.resnet18(weights=models.ResNet18_Weights.IMAGENET1K_V1) # 替换最后一层,输出 2 类 model.fc = nn.Linear(model.fc.in_features, 2) return model def train_one_epoch(model, loader, optimizer, criterion, device): model.train() total_loss, correct, total = 0.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这段代码里几个参数值得说清楚。ResNet18_Weights.IMAGENET1K_V1是预训练权重,用迁移学习能显著加快收敛,如果环境下载不了,可以设成None从头训,但精度会掉几个点。RandomHorizontalFlip对性别识别是安全的增强,因为左右翻转不改变性别;但不要加垂直翻转或大角度旋转,人脸倒过来反而引入噪声。Normalize的均值和方差是 ImageNet 的统计值,用预训练权重时必须保持一致,否则输入分布对不上,收敛会变慢。训练循环里loss.item() * imgs.size(0)是为了按样本数加权平均,避免最后一个 batch 不满时 loss 统计偏差。
2.3 推理链路:检测、裁剪、分类三步不能少
训练完模型只是拿到权重,真正在 GUI 里跑的时候,输入是摄像头的一整帧,不是已经裁好的人脸。所以推理链路必须是「检测人脸 → 裁剪 → 送进分类网络」。常见做法是用 OpenCV 的 DNN 人脸检测器,比 Haar 级联稳,尤其在侧脸和光照不均时。下面是一个推理函数,把三步串起来。
import cv2 import numpy as np import torch from PIL import Image from torchvision import transforms class GenderPredictor: def __init__(self, weight_path, device='cpu'): self.device = torch.device(device) self.model = build_model() 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((224, 224)), transforms.ToTensor(), transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]) ]) # OpenCV DNN 人脸检测器,需要提前下载 caffemodel 和 prototxt self.detector = cv2.dnn.readNetFromCaffe( 'deploy.prototxt', 'res10_300x300_ssd_iter_140000.caffemodel' ) def detect_faces(self, frame, conf_threshold=0.6): h, w = frame.shape[:2] blob = cv2.dnn.blobFromImage( cv2.resize(frame, (300, 300)), 1.0, (300, 300), (104.0, 177.0, 123.0) ) self.detector.setInput(blob) detections = self.detector.forward() boxes = [] for i in range(detections.shape[2]): confidence = detections[0, 0, i, 2] if confidence < conf_threshold: continue box = detections[0, 0, i, 3:7] * np.array([w, h, w, h]) x1, y1, x2, y2 = box.astype('int') # 边界裁剪,防止越界 x1, y1 = max(0, x1), max(0, y1) x2, y2 = min(w, x2), min(h, y2) if x2 - x1 < 20 or y2 - y1 < 20: continue boxes.append((x1, y1, x2, y2)) return boxes @torch.no_grad() def predict(self, face_bgr): # OpenCV 是 BGR,PIL 需要 RGB face_rgb = cv2.cvtColor(face_bgr, cv2.COLOR_BGR2RGB) img = Image.fromarray(face_rgb) tensor = self.tf(img).unsqueeze(0).to(self.device) logits = self.model(tensor) prob = torch.softmax(logits, dim=1)[0] label = int(prob.argmax().item()) return label, float(prob[label].item())conf_threshold设 0.6 是经验值,太低会把人脸误检成背景,太高在暗光下漏检。blobFromImage里的(104.0, 177.0, 123.0)是 SSD 检测器的均值减除参数,不能随便改。裁剪时加边界保护是因为检测框可能超出画面,直接切片会报错。torch.no_grad()在推理时一定要加,否则显存会随着循环不断累积,跑几分钟就爆。分类输出用 softmax 转成概率,界面上显示置信度比只显示标签更有说服力,报告里也能画置信度分布图。
3. GUI 与推理线程:Tkinter 不卡死的写法
3.1 为什么你的界面一点按钮就「未响应」
Tkinter 是单线程事件循环,如果在按钮回调里直接跑摄像头循环和模型推理,主线程被占满,窗口就卡死,Windows 上还会弹「未响应」。血泪经验是:所有耗时操作必须放到独立线程,主线程只负责刷新界面。常见做法是用threading.Thread跑推理循环,用queue.Queue把结果传回主线程,主线程用after定时读取队列并更新 Label 和 Canvas。这样界面始终能响应关闭和切换摄像头的操作。
另一个坑是 OpenCV 的VideoCapture在不同后端下行为不一致。Windows 上默认后端有时打不开摄像头,需要显式指定cv2.CAP_DSHOW。Linux 上一般用cv2.CAP_V4L2。如果答辩现场用的是笔记本自带摄像头,提前在目标机器上测一遍,别等到演示时才发现读不到帧。
3.2 一个能跑的最小 GUI 骨架
下面这段代码把摄像头读取、推理、界面刷新拆成三个部分,主线程只做 UI,推理线程只做计算,队列做桥梁。你可以直接在这个骨架上加按钮、加置信度显示、加截图保存。
import tkinter as tk from tkinter import ttk from PIL import Image, ImageTk import cv2 import threading import queue import time class GenderGUI: def __init__(self, root, predictor): self.root = root self.predictor = predictor self.root.title('人脸性别识别系统') self.root.geometry('900x600') self.video_label = ttk.Label(root) self.video_label.pack(side=tk.LEFT, padx=10, pady=10) self.info_label = ttk.Label(root, text='等待开始', font=('Arial', 16)) self.info_label.pack(side=tk.RIGHT, padx=20) self.start_btn = ttk.Button(root, text='开始', command=self.start) self.start_btn.pack(side=tk.BOTTOM, pady=10) self.frame_queue = queue.Queue(maxsize=2) self.result_queue = queue.Queue(maxsize=2) self.running = False self.cap = None def start(self): if self.running: return self.running = True # Windows 下用 CAP_DSHOW 更稳,Linux 改成 CAP_V4L2 self.cap = cv2.VideoCapture(0, cv2.CAP_DSHOW) if not self.cap.isOpened(): self.info_label.config(text='摄像头打开失败') self.running = False return threading.Thread(target=self.capture_loop, daemon=True).start() threading.Thread(target=self.infer_loop, daemon=True).start() self.root.after(30, self.update_ui) def capture_loop(self): while self.running: ret, frame = self.cap.read() if not ret: time.sleep(0.01) continue if self.frame_queue.full(): try: self.frame_queue.get_nowait() except queue.Empty: pass self.frame_queue.put(frame) def infer_loop(self): while self.running: try: frame = self.frame_queue.get(timeout=0.5) except queue.Empty: continue boxes = self.predictor.detect_faces(frame) results = [] for (x1, y1, x2, y2) in boxes: face = frame[y1:y2, x1:x2] label, conf = self.predictor.predict(face) results.append((x1, y1, x2, y2, label, conf)) if self.result_queue.full(): try: self.result_queue.get_nowait() except queue.Empty: pass self.result_queue.put((frame, results)) def update_ui(self): if not self.running: return try: frame, results = self.result_queue.get_nowait() for (x1, y1, x2, y2, label, conf) in results: color = (0, 255, 0) if label == 0 else (255, 0, 0) text = f'Male {conf:.2f}' if label == 0 else f'Female {conf:.2f}' cv2.rectangle(frame, (x1, y1), (x2, y2), color, 2) cv2.putText(frame, text, (x1, y1 - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.7, color, 2) rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) img = Image.fromarray(rgb) imgtk = ImageTk.PhotoImage(image=img) self.video_label.imgtk = imgtk self.video_label.config(image=imgtk) if results: self.info_label.config(text=f'检测到 {len(results)} 张人脸') except queue.Empty: pass self.root.after(30, self.update_ui) def on_close(self): self.running = False if self.cap: self.cap.release() self.root.destroy()队列设maxsize=2是故意的:摄像头帧率通常高于推理速度,如果队列不设上限,内存会涨,延迟也会越来越大,最后界面显示的是几秒前的画面。丢旧帧保新帧,才能让画面「跟手」。daemon=True保证关闭窗口时线程自动退出,不然程序会挂在后台。update_ui里用after(30, ...)约等于 33 帧刷新,和摄像头帧率匹配,再高也没意义。self.video_label.imgtk = imgtk这行必须保留,否则 PhotoImage 被垃圾回收,界面显示空白,这是 Tkinter 最经典的坑之一。
4. 避坑与排查:从环境到精度的 5 个真实翻车点
4.1 现象:训练 loss 不降,准确率卡在 50%
原因通常是标签解析错了。UTKFace 文件名里性别位是第二位,但有些下载渠道的文件名被重命名过,或者混入了非 UTKFace 的图片。解决方法是先跑一遍统计脚本,打印每个类别的样本数和几张样本的标签,确认 0 和 1 的数量大致均衡。如果某一类只有几十张,那就是数据解析出了问题,不是模型问题。
4.2 现象:GPU 显存越跑越满,最后 OOM
原因是在验证或推理时没有加torch.no_grad(),PyTorch 默认会构建计算图,即使不调用 backward,中间变量也不会释放。解决方法是所有推理路径都包在with torch.no_grad():里,验证循环同理。另外 DataLoader 的num_workers在 Windows 上设大于 0 有时会卡死,设成 0 最稳,Linux 上可以设 4 或 8。
4.3 现象:GUI 显示的画面颜色发蓝或发红
原因是 OpenCV 读进来是 BGR,PIL 和 Tkinter 按 RGB 解释。解决方法是送进 PIL 之前做一次cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)。但要注意,送进模型之前如果用的是 PIL 流程,也要保证是 RGB,因为预训练权重是在 RGB 上训的。BGR 直接送模型,精度会掉,但不会报错,属于隐蔽 bug。
4.4 现象:答辩现场摄像头打不开
原因是后端选择或权限问题。Windows 上优先试cv2.CAP_DSHOW,如果不行再试默认。Linux 上检查/dev/video0是否存在,用户是否在video组。另外有些笔记本有物理摄像头开关,或者被其他程序占用(比如腾讯会议没退干净)。解决方法是提前写一个test_camera.py,只做打开和显示,现场先跑这个脚本确认硬件通路。
4.5 现象:模型在测试集上 95%,实际摄像头里频繁认错
原因是训练数据和推理数据分布不一致。UTKFace 的人脸对齐好、光照均匀,而摄像头画面有逆光、侧脸、遮挡。解决方法是训练时加更强的增强,比如随机裁剪、亮度对比度扰动,推理时用 DNN 检测器而不是 Haar,并且把检测框适当外扩 10% 到 20%,让模型看到更多上下文。如果还不行,就在报告里如实写「受限于数据集分布,复杂光照下精度下降」,这比硬吹 99% 更可信。
5. 把 Demo 变成能写进报告的实验:验证、对比与一个提分技巧
到这一步,系统能跑了,但毕设报告不能只有「我做了个界面」。评审看的是你有没有实验设计。我一般会做三组对比:第一组是 ResNet18 从头训练 vs 迁移学习,第二组是不同学习率(1e-3、1e-4、1e-5)对收敛的影响,第三组是加不加数据增强对验证集准确率的差异。每组跑 10 个 epoch,记录 loss 曲线和准确率,用 matplotlib 画出来放进报告。这些图比任何文字描述都有说服力。
验证方法上,不要只用训练集切出来的验证集,那样容易过拟合。从 UTKFace 里按 8:1:1 切训练、验证、测试,测试集只在最后跑一次。测试时输出混淆矩阵,看看是男性误判成女性多,还是反过来多。如果某一类偏多,说明数据不均衡,可以在损失函数里加类别权重,nn.CrossEntropyLoss(weight=torch.tensor([1.0, 1.5]))这种,具体权重按类别频率的倒数来设。
一个提分技巧是:在 GUI 里加一个「置信度阈值」滑块,低于阈值的检测框不显示标签,只画灰框。这样演示时不会出现「模型瞎猜」的尴尬,报告里也能写「引入置信度过滤提升用户体验」。实现上就是在update_ui里加一个判断,if conf < threshold: 只画框不写标签。这个改动很小,但答辩时老师会觉得你考虑了实际可用性。
最后说个我自己的习惯:每次改完模型或预处理,先在一个 200 张的小子集上跑一遍,确认 loss 能降、预测分布正常,再上全量数据。全量训练一次几十分钟,用小样本快速试错能省下大量时间。这套东西不难,难的是把每个环节的边界条件都想到,别让一个颜色通道或者一个队列上限毁掉整个演示。希望帮到你。
本文还有配套的精品资源,点击获取