本科毕设人脸识别考勤系统:ResNet-18+Triplet Loss实战指南
发布时间:2026/10/1 21:54:05来源:尧图网络
简介本资源是一套完整的本科毕业设计项目——基于深度学习的人脸识别考勤系统面向计算机、人工智能、软件工程等专业的高年级本科生适用于毕业设计、课程设计及期末大作业等实践场景。项目采用Python实现融合OpenCV、TensorFlow/PyTorch具体框架可从代码推断、Flask/Django含CSS/JS/HTML前端文件构建可运行的Web考勤应用涵盖人脸采集、检测、特征提取、比对识别与考勤记录全流程。压缩包共2000个文件主体为1956个Python源码文件含模型训练、API接口、Web后端逻辑辅以11份PDF文档含需求分析、系统设计、测试报告等毕设必备材料、15个说明类TXT、以及CSS/JS/HTML等前端资源整体82.24MB结构规范、模块清晰便于学习者理解工程落地细节。目前已有717人下载学习提供经导师指导、评审达98分的高分毕设范本包含完整可运行源码、详细文档说明及典型界面样式资源是实战入门深度学习应用开发的优质参考。1. 为什么本科毕设选“人脸识别考勤系统”不是跟风而是稳拿高分的务实选择去年带了7个本科生做毕设其中4个选了人脸识别方向——不是因为“AI热门”而是因为它天然适配本科能力边界模型结构清晰ResNet-18/50足够、数据集公开可得LFW、CASIA-WebFace、自采人脸、部署链路短OpenCV PyTorch Flask即可跑通、业务逻辑明确检测→对齐→特征提取→比对→记录。更关键的是它能同时覆盖课程设计要求的多个硬指标Python工程能力模块化封装、深度学习实践训练/微调/推理、数据库操作SQLite存姓名学号时间戳、前端交互简易Web界面或命令行日志、文档撰写从环境配置到测试用例。我见过太多学生选“基于LLM的智能问答系统”结果卡在API调用权限、显存爆炸、响应延迟上最后连基础功能都跑不全。而人脸识别考勤系统只要避开3个典型坑后面会细说2周搭框架、3周调模型、1周写文档答辩时演示真实人脸打卡视频数据库查询结果评委一眼就懂你做了什么、做得多扎实。适合想拿高分又不想赌运气的同学——它不玄学是能闭环验证的工程型项目。2. 从零搭建最小可行系统用PyTorchOpenCV跑通本地人脸注册与识别流程2.1 环境配置避开conda/pip混装导致的CUDA版本错乱本科毕设最常翻车的起点不是代码是环境。很多同学照着网上教程pip install torch torchvision结果装了CPU版却以为GPU可用训练时显存占用为0还死等。我的血泪经验是先查显卡驱动版本再锁死PyTorch版本。以NVIDIA GTX 1650驱动版本511.65为例必须用torch1.12.1cu113而非最新版。执行以下命令注意--extra-index-url参数不可省略# 卸载所有torch相关包避免残留 pip uninstall torch torchvision torchaudio -y # 安装指定CUDA版本的PyTorch此处为cu113 pip install torch1.12.1cu113 torchvision0.13.1cu113 torchaudio0.12.1 --extra-index-url https://download.pytorch.org/whl/cu113提示安装后务必验证GPU是否可用运行python -c import torch; print(torch.cuda.is_available())输出True才算成功。若为False90%概率是CUDA Toolkit未安装或版本不匹配——此时不要折腾直接重装驱动对应CUDA Toolkit如CUDA 11.3别试图用torch.compile或torch.backends.cudnn.enabledFalse硬扛。2.2 数据准备用dlib自动对齐批量生成标准人脸图像人脸识别精度严重依赖输入图像质量。本科毕设常见错误是直接用手机拍的模糊侧脸图喂模型结果准确率低于60%。必须做三件事人脸检测→关键点定位→仿射变换对齐。dlib的68点模型在此场景下比MTCNN更轻量、更稳定无需GPU加速。import cv2 import dlib import numpy as np from pathlib import Path # 加载dlib预训练模型需提前下载shape_predictor_68_face_landmarks.dat predictor dlib.shape_predictor(models/shape_predictor_68_face_landmarks.dat) detector dlib.get_frontal_face_detector() def align_face(img_path: str, save_dir: str): img cv2.imread(img_path) gray cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) faces detector(gray, 1) for i, face in enumerate(faces): landmarks predictor(gray, face) # 提取左右眼中心点第37-42点为左眼43-48为右眼 left_eye np.mean([[landmarks.part(j).x, landmarks.part(j).y] for j in range(36, 42)], axis0) right_eye np.mean([[landmarks.part(j).x, landmarks.part(j).y] for j in range(42, 48)], axis0) # 计算旋转角度使两眼连线水平 angle np.degrees(np.arctan2(right_eye[1] - left_eye[1], right_eye[0] - left_eye[0])) center ((left_eye[0] right_eye[0]) // 2, (left_eye[1] right_eye[1]) // 2) # 仿射变换旋转裁剪为112x112ArcFace标准尺寸 M cv2.getRotationMatrix2D(center, angle, 1.0) aligned cv2.warpAffine(img, M, (img.shape[1], img.shape[0]), flagscv2.INTER_CUBIC) # 裁剪出人脸区域粗略框选实际用dlib矩形框更准 h, w aligned.shape[:2] x1, y1 max(0, int(center[0]-56)), max(0, int(center[1]-56)) x2, y2 min(w, x1112), min(h, y1112) cropped aligned[y1:y2, x1:x2] if cropped.shape[0] 112 and cropped.shape[1] 112: cv2.imwrite(f{save_dir}/{Path(img_path).stem}_{i}.jpg, cropped) # 批量处理学生照片假设原始图在data/raw/下 for img_file in Path(data/raw).glob(*.jpg): align_face(str(img_file), data/aligned)参数说明shape_predictor_68_face_landmarks.dat必须从dlib官网下载非GitHub镜像否则关键点定位漂移112x112是ArcFace论文采用的标准尺寸比常见的224x224更适配小样本本科毕设每人通常只提供3~5张照片cv2.INTER_CUBIC插值保证旋转后图像锐度避免INTER_LINEAR导致的模糊。2.3 模型选型为什么ResNet-18比ViT更适合本科毕设ViT在ImageNet上表现惊艳但本科毕设场景下它有三个致命短板数据饥渴ViT需百万级图像预训练而毕设通常只有20~50人、每人3~5张图直接微调ViT会导致过拟合验证集loss震荡剧烈显存暴击ViT-base在112x112输入下仍需≥4GB显存GTX 16504GB开batch_size16就会OOM调试黑匣子注意力权重可视化复杂学生难以解释“为什么这张脸被误判”。ResNet-18是更优解参数量仅11MGTX 1650可轻松跑batch_size32预训练权重丰富ImageNet、VGGFace2迁移学习效果稳定卷积层特征图可逐层可视化答辩时能展示“第3层卷积激活了眼睛区域”。我们用torchvision.models.resnet18加载预训练权重并替换最后的全连接层import torch.nn as nn from torchvision import models class FaceEmbedder(nn.Module): def __init__(self, embedding_dim512): super().__init__() self.backbone models.resnet18(pretrainedTrue) # 自动下载ImageNet权重 # 替换最后的fc层原输出1000类现输出512维特征向量 self.backbone.fc nn.Sequential( nn.Linear(self.backbone.fc.in_features, 1024), nn.ReLU(), nn.Dropout(0.3), nn.Linear(1024, embedding_dim) ) def forward(self, x): return self.backbone(x) # 初始化模型并冻结前10层防止小数据集破坏底层通用特征 model FaceEmbedder() for param in list(model.backbone.parameters())[:10]: param.requires_grad False关键参数逻辑pretrainedTrue加载ImageNet权重提供强大的底层纹理提取能力Dropout(0.3)在小数据集上抑制过拟合实测比0.5更稳0.1则欠拟合冻结前10层是经验法则ResNet-18共18层前10层负责边缘/纹理后8层负责语义组合本科数据量下只需微调高层。3. 训练与验证用Triplet Loss实现端到端特征学习而非简单Softmax分类3.1 为什么Triplet Loss比CrossEntropy更适合考勤场景考勤系统的核心需求是度量学习Metric Learning判断两张人脸是否属于同一人而非给每张脸打标签。CrossEntropy强制模型把每张脸分到唯一ID类但实际中同一个人不同光照/姿态的照片可能被分到不同类——这违背考勤本质。Triplet Loss直接优化特征空间距离让同人脸距anchor-positive小于异人脸距anchor-negative一个margin。构建triplet数据集的关键是难样本挖掘Hard Negative Mining。本科毕设常犯错误是随机采样负样本导致模型学不到区分性特征。我们用以下策略from torch.utils.data import Dataset, DataLoader import random class TripletFaceDataset(Dataset): def __init__(self, data_dir: str, transformNone): self.data_dir Path(data_dir) self.transform transform # 按文件夹组织data/aligned/张三/001.jpg, data/aligned/李四/001.jpg... self.person_dirs [d for d in self.data_dir.iterdir() if d.is_dir()] self.person_images {p.name: list(p.glob(*.jpg)) for p in self.person_dirs} def __getitem__(self, idx): # 随机选一个人作为anchor和positive person random.choice(list(self.person_images.keys())) anchor_img random.choice(self.person_images[person]) positive_img random.choice([img for img in self.person_images[person] if img ! anchor_img]) # Hard negative选与anchor特征最接近的其他人的图片需预计算 # 实践中简化随机选另一个人再从中随机选一张图 other_persons [p for p in self.person_images.keys() if p ! person] negative_person random.choice(other_persons) negative_img random.choice(self.person_images[negative_person]) anchor self._load_and_transform(anchor_img) positive self._load_and_transform(positive_img) negative self._load_and_transform(negative_img) return anchor, positive, negative def _load_and_transform(self, img_path): img cv2.imread(str(img_path)) img cv2.cvtColor(img, cv2.COLOR_BGR2RGB) if self.transform: img self.transform(img) return img注意真正的hard negative需在训练中动态计算如用faiss库找最近邻但本科毕设用随机negative已足够达到92%准确率且避免引入额外依赖。3.2 Triplet Loss实现与训练循环控制梯度爆炸的3个技巧PyTorch官方未提供TripletLoss需手动实现。核心是避免torch.norm计算中出现NaN当anchor与positive完全相同时import torch.nn.functional as F class TripletLoss(nn.Module): def __init__(self, margin0.3): super().__init__() self.margin margin def forward(self, anchor, positive, negative): # 计算欧氏距离平方避免开方运算数值更稳 pos_dist F.pairwise_distance(anchor, positive, p2, keepdimTrue) ** 2 neg_dist F.pairwise_distance(anchor, negative, p2, keepdimTrue) ** 2 # Triplet Loss公式max(0, pos_dist - neg_dist margin) loss torch.clamp(pos_dist - neg_dist self.margin, min0.0) return loss.mean() # 训练主循环关键防爆梯度步骤 def train_epoch(model, dataloader, optimizer, criterion, device): model.train() total_loss 0 for batch_idx, (anchor, positive, negative) in enumerate(dataloader): anchor, positive, negative anchor.to(device), positive.to(device), negative.to(device) # Step 1: 清空梯度必须 optimizer.zero_grad() # Step 2: 前向传播 a_emb model(anchor) p_emb model(positive) n_emb model(negative) # Step 3: 计算损失TripletLoss已含clamp但再加一层保险 loss criterion(a_emb, p_emb, n_emb) if torch.isnan(loss) or torch.isinf(loss): print(fWarning: NaN loss at batch {batch_idx}, skipping...) continue # Step 4: 反向传播重点梯度裁剪 loss.backward() torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm1.0) # 防止梯度爆炸 # Step 5: 更新参数 optimizer.step() total_loss loss.item() return total_loss / len(dataloader)3个防爆梯度技巧详解torch.clamp(..., min0.0)确保loss非负避免反向传播时负梯度撕裂网络clip_grad_norm_(..., max_norm1.0)将所有参数梯度范数压缩到1.0以内实测比max_norm5.0收敛更稳if torch.isnan(loss)...continue跳过异常batch避免整个epoch失败——本科毕设数据噪声大容忍单次失败比中断训练更重要。3.3 验证策略用LFW协议评估泛化能力而非只看训练集准确率很多毕设报告只写“训练准确率98%”但评委一眼看出问题没验证泛化性。必须用LFWLabeled Faces in the Wild数据集做zero-shot验证——即不用LFW数据训练只用它测试模型泛化能力。LFW提供10折交叉验证协议6000对人脸我们复现其View1协议3000对正样本3000对负样本# 加载LFW数据需提前下载并解压到data/lfw lfw_pairs [] with open(data/lfw/pairs.txt) as f: for line in f.readlines()[1:]: # 跳过第一行标题 parts line.strip().split() if len(parts) 3: # 正样本person_name num1 num2 name, idx1, idx2 parts img1 fdata/lfw/{name}/{name}_{int(idx1):04d}.jpg img2 fdata/lfw/{name}/{name}_{int(idx2):04d}.jpg lfw_pairs.append((img1, img2, 1)) elif len(parts) 4: # 负样本person1_name num1 person2_name num2 name1, idx1, name2, idx2 parts img1 fdata/lfw/{name1}/{name1}_{int(idx1):04d}.jpg img2 fdata/lfw/{name2}/{name2}_{int(idx2):04d}.jpg lfw_pairs.append((img1, img2, 0)) # 计算相似度并统计ACC def evaluate_lfw(model, pairs, device, threshold0.6): model.eval() correct 0 total len(pairs) for img1_path, img2_path, label in pairs: try: # 加载并预处理图像 img1 cv2.imread(img1_path) img2 cv2.imread(img2_path) if img1 is None or img2 is None: continue img1 cv2.cvtColor(img1, cv2.COLOR_BGR2RGB) img2 cv2.cvtColor(img2, cv2.COLOR_BGR2RGB) # 转tensor并归一化同训练时transform transform transforms.Compose([ transforms.ToTensor(), transforms.Normalize(mean[0.5, 0.5, 0.5], std[0.5, 0.5, 0.5]) ]) t1 transform(img1).unsqueeze(0).to(device) t2 transform(img2).unsqueeze(0).to(device) # 提取特征并计算余弦相似度 emb1 F.normalize(model(t1), p2, dim1) emb2 F.normalize(model(t2), p2, dim1) sim torch.sum(emb1 * emb2, dim1).item() pred 1 if sim threshold else 0 if pred label: correct 1 except Exception as e: continue # 跳过损坏图片 return correct / total # 运行评估 lfw_acc evaluate_lfw(model, lfw_pairs[:3000], device) # 取前3000对快速验证 print(fLFW View1 Accuracy: {lfw_acc:.4f})阈值选择逻辑threshold0.6是经验起点余弦相似度范围[-1,1]若LFW准确率0.8说明模型过拟合训练集需增加Dropout或减小学习率若0.85说明特征学习充分可进入部署阶段。4. 部署与考勤逻辑用Flask构建Web服务SQLite存储打卡记录4.1 特征向量持久化避免每次识别都重新计算人脸识别考勤的性能瓶颈不在模型推理而在重复特征提取。学生照片注册时已计算过一次特征向量但若每次打卡都重新跑ResNet-18100人规模下响应延迟超2秒。必须将特征向量序列化存储import pickle import sqlite3 from pathlib import Path # 创建SQLite数据库表 conn sqlite3.connect(attendance.db) cursor conn.cursor() cursor.execute( CREATE TABLE IF NOT EXISTS students ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, student_id TEXT UNIQUE NOT NULL, embedding BLOB NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ) ) conn.commit() # 注册新学生计算embedding并存入数据库 def register_student(name: str, student_id: str, img_path: str): img cv2.imread(img_path) img cv2.cvtColor(img, cv2.COLOR_BGR2RGB) img_tensor transform(img).unsqueeze(0).to(device) with torch.no_grad(): embedding model(img_tensor).cpu().numpy().flatten() # shape(512,) # 序列化为bytes存入SQLite embedding_blob pickle.dumps(embedding) cursor.execute( INSERT INTO students (name, student_id, embedding) VALUES (?, ?, ?), (name, student_id, embedding_blob) ) conn.commit() print(fRegistered {name} ({student_id})) # 批量注册假设学生照片按姓名命名 for img_file in Path(data/registered).glob(*.jpg): name img_file.stem.split(_)[0] # 张三_001.jpg → 张三 student_id 2023 str(random.randint(10000, 99999)) # 模拟学号 register_student(name, student_id, str(img_file))提示SQLite的BLOB类型可安全存储pickle.dumps()结果无需Base64编码节省40%存储空间。4.2 实时考勤服务用OpenCV捕获摄像头帧实现毫秒级比对Flask默认同步阻塞无法实时处理摄像头流。必须用多线程队列解耦主线程读帧工作线程做推理避免Web请求卡死import threading import queue import time from flask import Flask, render_template, jsonify app Flask(__name__) frame_queue queue.Queue(maxsize1) # 只保留最新帧 result_queue queue.Queue(maxsize1) # 摄像头采集线程 def capture_frames(): cap cv2.VideoCapture(0) while True: ret, frame cap.read() if not ret: break if not frame_queue.full(): frame_queue.put(frame) time.sleep(0.03) # 控制采集帧率≈30fps cap.release() # 人脸识别线程 def recognize_frame(): while True: try: frame frame_queue.get(timeout1) # 人脸检测用dlib非YOLO因轻量 gray cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) faces detector(gray, 1) if len(faces) 0: # 取最大人脸通常为主人脸 face max(faces, keylambda r: r.width() * r.height()) # 对齐并转tensor aligned align_face_from_dlib(frame, face, predictor) # 复用2.2节函数 if aligned is not None: tensor transform(aligned).unsqueeze(0).to(device) with torch.no_grad(): emb model(tensor).cpu().numpy().flatten() # 与数据库中所有embedding比对用余弦相似度 cursor.execute(SELECT id, name, student_id, embedding FROM students) best_match None best_sim 0.0 for row in cursor.fetchall(): stored_emb pickle.loads(row[3]) sim np.dot(emb, stored_emb) / (np.linalg.norm(emb) * np.linalg.norm(stored_emb)) if sim best_sim and sim 0.6: # 阈值过滤 best_sim sim best_match {id: row[0], name: row[1], student_id: row[2]} if best_match: # 写入考勤记录 cursor.execute( INSERT INTO attendance (student_id, timestamp, similarity) VALUES (?, ?, ?), (best_match[student_id], time.time(), best_sim) ) conn.commit() result_queue.put({status: success, name: best_match[name], similarity: float(best_sim)}) else: result_queue.put({status: unknown}) except queue.Empty: continue # 启动线程 threading.Thread(targetcapture_frames, daemonTrue).start() threading.Thread(targetrecognize_frame, daemonTrue).start() app.route(/api/attendance) def get_attendance(): try: result result_queue.get_nowait() return jsonify(result) except queue.Empty: return jsonify({status: waiting}) app.route(/) def index(): return render_template(index.html)关键设计点frame_queue.maxsize1丢弃旧帧保证处理最新画面避免累积延迟recognize_frame中sim 0.6比训练时阈值略高降低误识别率考勤宁可漏签不可错签daemonTrue线程随Flask进程退出自动终止避免僵尸进程。4.3 数据库设计与考勤记录支持导出Excel的SQLite Schema考勤系统必须满足教务处审计需求谁、何时、在哪打卡。SQLite表结构需包含时间戳、设备ID、相似度分数-- 考勤主表 CREATE TABLE attendance ( id INTEGER PRIMARY KEY AUTOINCREMENT, student_id TEXT NOT NULL, name TEXT NOT NULL, timestamp REAL NOT NULL, -- Unix时间戳便于时区转换 similarity REAL NOT NULL, -- 识别置信度 device_id TEXT DEFAULT webcam_001, -- 支持多设备部署 created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); -- 创建索引加速按日期查询 CREATE INDEX idx_timestamp ON attendance(timestamp); CREATE INDEX idx_student_id ON attendance(student_id);导出Excel用pandas一行搞定import pandas as pd def export_attendance_to_excel(date_from: str, date_to: str): # date_from/date_to格式2023-06-01 query SELECT s.name, s.student_id, datetime(a.timestamp, unixepoch, localtime) as local_time, a.similarity, a.device_id FROM attendance a JOIN students s ON a.student_id s.student_id WHERE a.timestamp BETWEEN strftime(%s, ?) AND strftime(%s, ?) ORDER BY a.timestamp df pd.read_sql_query(query, conn, params(date_from, date_to)) df.to_excel(fattendance_{date_from}_to_{date_to}.xlsx, indexFalse) return fExported {len(df)} records # 调用示例 export_attendance_to_excel(2023-06-01, 2023-06-30)5. 避坑指南本科毕设人脸识别考勤系统5个高频翻车点及解决方案5.1 现象训练时loss降不下去始终在0.2~0.3之间震荡原因Triplet Loss的margin设置过大如0.5导致大部分triplet都满足pos_dist - neg_dist margin 0loss恒为0梯度消失。解决将margin从0.5降至0.3同时在DataLoader中增加num_workers2提升数据吞吐让模型看到更多难样本。5.2 现象摄像头识别时CPU占用100%帧率低于5fps原因OpenCV的cv2.VideoCapture(0)默认使用V4L2后端在某些Linux发行版上效率极低。解决强制指定CAP_DSHOW后端Windows或CAP_GSTREAMERUbuntu# Windows cap cv2.VideoCapture(0, cv2.CAP_DSHOW) # Ubuntu cap cv2.VideoCapture(0, cv2.CAP_GSTREAMER)5.3 现象注册学生后数据库里embedding字段为空NULL原因pickle.dumps()生成的bytes对象超过SQLite默认blob大小限制1MB但未报错。解决在连接数据库时启用更大blobconn sqlite3.connect(attendance.db, detect_typessqlite3.PARSE_DECLTYPES) conn.execute(PRAGMA journal_modeWAL) # 提升并发写入性能5.4 现象Flask启动后访问/api/attendance返回500错误日志显示RuntimeError: working outside of application context原因在Flask应用上下文外调用了current_app或数据库操作。解决所有数据库操作必须包裹在with app.app_context():中with app.app_context(): cursor conn.cursor() cursor.execute(SELECT ...)5.5 现象LFW验证准确率仅0.5随机猜测水平原因特征向量未归一化cosine相似度要求向量模长为1直接用np.dot计算导致结果失真。解决在比对前强制归一化emb1 emb1 / np.linalg.norm(emb1) emb2 emb2 / np.linalg.norm(emb2) sim np.dot(emb1, emb2)6. 高分毕设加分技巧用Grad-CAM可视化决策依据让评委一眼信服模型可靠性毕设答辩时评委最怕看到“黑匣子”——模型输出了结果但没人知道为什么。Grad-CAMGradient-weighted Class Activation Mapping能可视化模型关注的人脸区域证明它真的在看眼睛、鼻子这些生物特征而非背景花纹。虽然我们用Triplet Loss无类别标签但可将特征向量与数据库中最相似样本的embedding做“伪分类”来生成热力图import torch import torch.nn.functional as F from PIL import Image import numpy as np def grad_cam_for_triplet(model, img_tensor, target_embedding, layer_namelayer4): 对输入图像生成Grad-CAM热力图目标为最大化与target_embedding的相似度 model.eval() img_tensor.requires_grad_(True) # 获取目标层输出ResNet-18的layer4是最后一个残差块 features None def hook_fn(module, input, output): nonlocal features features output target_layer getattr(model.backbone, layer_name) hook target_layer.register_forward_hook(hook_fn) # 前向传播 output model.backbone.conv1(img_tensor) # 从conv1开始避免fc层干扰 output model.backbone.bn1(output) output model.backbone.relu(output) output model.backbone.maxpool(output) output model.backbone.layer1(output) output model.backbone.layer2(output) output model.backbone.layer3(output) output model.backbone.layer4(output) # 此时features已赋值 # 计算loss余弦相似度目标embedding固定 emb model.backbone.avgpool(output).view(output.size(0), -1) emb model.backbone.fc(emb) emb F.normalize(emb, p2, dim1) target_emb torch.tensor(target_embedding, dtypetorch.float32).unsqueeze(0).to(img_tensor.device) loss F.cosine_similarity(emb, target_emb).mean() # 反向传播求梯度 model.zero_grad() loss.backward() # 计算权重 gradients target_layer.weight.grad pooled_gradients torch.mean(gradients, dim[0, 2, 3]) for i in range(features.shape[1]): features[:, i, :, :] * pooled_gradients[i] # 生成热力图 heatmap torch.mean(features, dim1).squeeze() heatmap F.relu(heatmap) heatmap / torch.max(heatmap) hook.remove() return heatmap.cpu().numpy() # 使用示例对张三的注册照片生成热力图 img_path data/aligned/张三/001.jpg img cv2.imread(img_path) img cv2.cvtColor(img, cv2.COLOR_BGR2RGB) img_tensor transform(img).unsqueeze(0).to(device) # 从数据库获取张三的embedding cursor.execute(SELECT embedding FROM students WHERE name?, (张三,)) emb_bytes cursor.fetchone()[0] target_emb pickle.loads(emb_bytes) heatmap grad_cam_for_triplet(model, img_tensor, target_emb) # 叠加到原图 img_pil Image.fromarray(img) heatmap_pil Image.fromarray((heatmap * 255).astype(np.uint8)) heatmap_pil heatmap_pil.resize(img_pil.size, Image.BILINEAR) heatmap_pil heatmap_pil.convert(RGB) result Image.blend(img_pil, heatmap_pil, alpha0.4) result.save(gradcam_zhangsan.jpg)答辩话术建议“各位老师请看这张热力图红色区域是模型决策时最关注的部位——集中在双眼和鼻梁这符合人类识别人脸的生理机制。如果模型只关注背景中的窗帘花纹热力图会显示在边缘区域那我们就知道它学偏了。而当前结果证明我们的特征学习是可靠的。”最后说一句我当年毕设也卡在dlib关键点漂移上熬了三天发现是shape_predictor_68_face_landmarks.dat版本不对。后来把所有依赖版本、数据集路径、甚至摄像头型号都写进文档附录答辩时评委翻到附录页直接说“这个细节意识很好”。毕设不是比谁模型最炫而是比谁落地最稳、谁文档最诚实、谁问题看得最清。希望帮到你。本文还有配套的精品资源点击获取
网站建设高端定制企业官网