资讯详情

LFW人脸数据集实战:从lfw_home.zip到Siamese网络与AUC评估

📅 2026/10/12 21:26:51 | 华诺云谱 👁 阅读
LFW人脸数据集实战:从lfw_home.zip到Siamese网络与AUC评估
简介这份资源是经典人脸识别数据集 LFW 的本地化整理包面向从事人脸验证、人脸识别研究的学生与算法工程师尤其适合需要复现经典实验、验证模型泛化能力的入门与进阶读者。压缩包共 9 个文件约 241.84MB以 txt 配对列表、pkl 序列化数据、json 配置、tgz 图像归档和 py 脚本为主分别承担样本配对、数据持久化、参数配置与预处理流程等职责。其中图像归档经过标准化处理可减少光照与尺寸差异带来的干扰配对文件则区分正负样本并划分训练与验证用途便于直接构建评测流程。目前已有 510 人学习下载。借助这套数据读者可快速搭建人脸比对实验理解特征向量相似度判定思路并在此基础上训练卷积神经网络等模型为安全系统、社交媒体与监控场景中的识别任务打下基础。1. 从 lfw_home.zip 说起一个人脸数据集压缩包能撑起多少条技术路线第一次拿到lfw_home.zip的人大概率会愣一下解压出来是一堆以人名命名的文件夹每个文件夹里躺着若干张 250×250 的 JPEG文件名还带着_0001.jpg这样的编号。它不是什么框架也不是什么模型权重就是一个把人脸按身份归档的目录树。但正是这个结构让它成了人脸验证、特征嵌入、度量学习这几条线上最常被拿来当“第一块试金石”的数据集。你如果正在做门禁比对、相册聚类、或者只是想验证自己写的 Siamese 网络到底有没有学到东西这个压缩包就是那个能让你在半天内跑出第一条 ROC 曲线的起点。它适合两类人一类是刚接触人脸任务、需要一套干净目录结构来练手的新手另一类是做工程落地、想快速验证某个损失函数或预处理策略是否值得上生产的老手。接下来的内容我按“解压后怎么组织、怎么切对、怎么训、怎么避坑”这条线走一遍。2. 解压之后先别急着训目录结构、配对协议与切分逻辑2.1 目录树长什么样为什么不能直接丢给 DataLoader解压lfw_home.zip之后你会看到类似这样的层级lfw_home/ lfw_funneled/ Aaron_Peirsol/ Aaron_Peirsol_0001.jpg Aaron_Peirsol_0002.jpg ... Aaron_Sorkin/ Aaron_Sorkin_0001.jpg ...每个身份一个文件夹文件夹里是该身份的多张图。这个结构对人眼很友好但对torchvision.datasets.ImageFolder并不直接友好——ImageFolder会把每个子目录当成一个类别可人脸验证任务要的不是“分类”而是“同一人/不同人”的配对判断。如果你直接拿它做闭集分类类别数会飙到几千每类样本又只有几张训出来的分类头基本没法用。常见做法是先写一个索引脚本把(图片路径, 身份标签)全部读进一个列表再基于这个列表去构造正负对。这一步不做后面所有配对逻辑都是空中楼阁。2.2 配对协议为什么 6000 对是绕不开的基准LFW 的标准评测协议里最常被引用的是 6000 对验证任务分成 10 折每折 600 对其中 300 对同人、300 对异人。这个划分不是随便定的它保证了每折内正负样本均衡且同一身份不会跨折泄漏。你自己切分时如果随手train_test_split很容易出现同一个人既在训练对里又在测试对里指标虚高到 99% 然后上线翻车。我一般会先把身份列表按字典序排好再用固定随机种子做 10 折确保每次实验的折分一致。下面这段索引与折分代码可以直接抄import os import random from collections import defaultdict def build_index(root): 遍历 lfw_funneled返回 {identity: [img_path, ...]} index defaultdict(list) for identity in sorted(os.listdir(root)): id_dir os.path.join(root, identity) if not os.path.isdir(id_dir): continue for fname in sorted(os.listdir(id_dir)): if fname.lower().endswith((.jpg, .jpeg, .png)): index[identity].append(os.path.join(id_dir, fname)) return index def make_folds(index, n_folds10, pairs_per_fold600, seed42): 按身份切折每折内生成正负对各半 rng random.Random(seed) identities list(index.keys()) rng.shuffle(identities) # 按身份均分到各折避免同人跨折 fold_ids [[] for _ in range(n_folds)] for i, ident in enumerate(identities): fold_ids[i % n_folds].append(ident) folds [] for f in range(n_folds): pos, neg [], [] ids_in_fold fold_ids[f] # 正对同一身份内随机取两张 for ident in ids_in_fold: imgs index[ident] if len(imgs) 2: continue a, b rng.sample(imgs, 2) pos.append((a, b, 1)) # 负对跨身份随机取两张 while len(neg) pairs_per_fold // 2: id1, id2 rng.sample(ids_in_fold, 2) a rng.choice(index[id1]) b rng.choice(index[id2]) neg.append((a, b, 0)) pairs pos[:pairs_per_fold // 2] neg rng.shuffle(pairs) folds.append(pairs) return folds这段代码里build_index负责把目录树拍平成身份到路径的映射make_folds先按身份分折再在折内配对seed固定后每次跑出来的折分完全一致。参数上pairs_per_fold保持 600 是为了对齐常见基准方便你和别人的结果做横向比较如果你只是内部验证可以降到 200 加快迭代但别低于 100否则单折方差太大指标忽高忽低排查起来就是玄学。2.3 图像预处理250×250 不是让你直接塞进网络原始图是 250×250但大多数骨干网吃的是 112×112 或 160×160。直接Resize到 112 会丢细节尤其是侧脸和低分辨率样本。我一般会先做中心裁剪到 224×224 再缩到 112或者用RandomResizedCrop做增强。注意 LFW 里有一部分是灰度图读进来是单通道如果你按三通道处理会报错。下面这个Dataset类把配对、灰度转三通道、归一化都包了import torch from torch.utils.data import Dataset from PIL import Image from torchvision import transforms class PairDataset(Dataset): def __init__(self, pairs, trainTrue): self.pairs pairs if train: self.tf transforms.Compose([ transforms.RandomResizedCrop(112, scale(0.8, 1.0)), transforms.RandomHorizontalFlip(), transforms.Grayscale(num_output_channels3), transforms.ToTensor(), transforms.Normalize([0.5]*3, [0.5]*3), ]) else: self.tf transforms.Compose([ transforms.CenterCrop(224), transforms.Resize(112), transforms.Grayscale(num_output_channels3), transforms.ToTensor(), transforms.Normalize([0.5]*3, [0.5]*3), ]) def __len__(self): return len(self.pairs) def __getitem__(self, idx): a_path, b_path, label self.pairs[idx] a self.tf(Image.open(a_path).convert(RGB)) b self.tf(Image.open(b_path).convert(RGB)) return a, b, torch.tensor(label, dtypetorch.float32)Grayscale(num_output_channels3)是关键它把单通道图复制成三通道避免骨干网第一层卷积报维度错误。训练时用RandomResizedCrop加RandomHorizontalFlip验证时用CenterCrop加Resize这个组合在 LFW 上比较稳。归一化用 0.5 均值方差是常见做法换成 ImageNet 的统计量也可以但别不归一化否则损失震荡得厉害。3. 从零搭一个 Siamese 基线骨干选择、损失函数与训练循环3.1 骨干网选型为什么 MobileNetV3 是性价比首选在 LFW 这种小规模数据上上 ResNet50 不是不行但参数量大、收敛慢单卡跑一下午可能才刚过 95%。我一般先用 MobileNetV3-Small 或 EfficientNet-B0 做基线参数量小、预训练权重好找、推理快。如果你要做的是端侧比对MobileNetV3 几乎是默认选项。骨干网后面接一个Embedding层把特征压到 128 或 256 维再算余弦相似度。下面这段定义了一个最小 Siamese 结构import torch.nn as nn import torch.nn.functional as F from torchvision.models import mobilenet_v3_small class SiameseNet(nn.Module): def __init__(self, emb_dim128, pretrainedTrue): super().__init__() backbone mobilenet_v3_small(pretrainedpretrained) # 去掉原分类头保留特征提取部分 self.features backbone.features self.pool nn.AdaptiveAvgPool2d(1) self.emb nn.Linear(576, emb_dim) # MobileNetV3-Small 最后一层通道数 576 def forward_once(self, x): f self.features(x) f self.pool(f).flatten(1) f self.emb(f) return F.normalize(f, p2, dim1) # L2 归一化方便算余弦 def forward(self, a, b): ea self.forward_once(a) eb self.forward_once(b) return ea, ebF.normalize把嵌入向量压到单位球面上这样余弦相似度就等价于点积后面算对比损失时不用再手动除范数。emb_dim设 128 是经验值设 64 会欠拟合设 512 在 LFW 上提升不明显还拖慢训练。预训练权重建议开从头训在小数据上很容易卡在 80% 出头。3.2 损失函数对比损失和三元组损失怎么选对比损失Contrastive Loss对配对数据友好直接吃(a, b, label)实现简单。三元组损失Triplet Loss需要挖矿在 LFW 上如果随机采样三元组收敛会很慢。我一般先用对比损失跑通基线再考虑换三元组或 ArcFace 做提升。对比损失的实现如下class ContrastiveLoss(nn.Module): def __init__(self, margin1.0): super().__init__() self.margin margin def forward(self, ea, eb, label): # label: 1 表示同人0 表示异人 cos F.cosine_similarity(ea, eb) pos_loss (1 - cos) ** 2 neg_loss torch.clamp(cos - self.margin, min0) ** 2 loss label * pos_loss (1 - label) * neg_loss return loss.mean()margin控制异人对的惩罚边界设 1.0 意味着余弦相似度超过 1.0 才罚但余弦最大就是 1所以实际等价于只要异人相似度为正就罚。实践中margin设 0.5 到 0.8 更稳设 1.0 容易导致负对损失一直很大、梯度爆炸。你可以先设 0.6 跑一轮看损失曲线如果负对损失不降再往下调。3.3 训练循环与验证指标别只看准确率训练循环里我习惯每轮在验证折上算一次 ROC 曲线和 AUC而不是只看阈值 0.5 下的准确率。因为人脸验证的阈值是要根据业务调的AUC 更能反映嵌入质量。下面是一个最小训练循环import numpy as np from sklearn.metrics import roc_auc_score from torch.utils.data import DataLoader def train_one_epoch(model, loader, optimizer, criterion, device): model.train() total_loss 0 for a, b, label in loader: a, b, label a.to(device), b.to(device), label.to(device) ea, eb model(a, b) loss criterion(ea, eb, label) optimizer.zero_grad() loss.backward() optimizer.step() total_loss loss.item() return total_loss / len(loader) def evaluate(model, loader, device): model.eval() scores, labels [], [] with torch.no_grad(): for a, b, label in loader: a, b a.to(device), b.to(device) ea, eb model(a, b) cos F.cosine_similarity(ea, eb) scores.extend(cos.cpu().numpy()) labels.extend(label.numpy()) auc roc_auc_score(labels, scores) return aucroc_auc_score直接吃余弦相似度和 0/1 标签算出来的 AUC 在 LFW 上基线大概 0.95 左右调好后能到 0.98 以上。如果你算出来只有 0.8 几先检查折分有没有泄漏再检查归一化有没有做。4. 避坑与排查lfw_home.zip 上最容易翻车的五件事4.1 现象验证 AUC 高得离谱接近 1.0原因折分时同一个人同时出现在训练对和验证对里模型记住了身份而不是学到了通用特征。解决严格按身份切折确保验证折里的身份在训练折里没出现过。上面make_folds里按身份分折就是干这个的别偷懒改成按图片随机分。4.2 现象训练损失不降或者降到某个值就卡住原因学习率太大导致震荡或者margin设得太激进导致负对梯度爆炸。解决先把学习率降到 1e-4 试一轮margin从 0.5 起步。如果还不行检查输入图像是不是没归一化或者灰度图没转三通道导致第一层卷积输出全零。4.3 现象验证时准确率波动很大每折差好几个点原因单折样本量太小600 对里正负各 300随机性大。解决跑完 10 折取平均和标准差别只看单折。如果标准差超过 1.5 个点说明模型不稳定优先检查数据增强是不是太猛比如RandomResizedCrop的scale下限设到了 0.5把脸裁没了。4.4 现象推理时两张图相似度算出来是负数原因嵌入向量没做 L2 归一化或者归一化之后又过了带偏置的线性层。解决确认forward_once最后一步是F.normalize且后面没有再接Linear。如果你在嵌入层后加了BatchNorm注意推理时track_running_stats要设对否则 batch 统计量会引入偏移。4.5 现象解压后文件名乱码或读图报错原因压缩包在不同系统间传输时编码不一致或者部分图片本身损坏。解决用PIL.Image.open时加try/except跳过损坏图并在索引阶段就过滤掉打不开的文件。别等到训练循环里才报错那时候已经跑了一半。5. 进阶技巧把 LFW 当验证集而不是训练集来用如果你已经有一个在更大规模数据上预训练好的模型LFW 最好的用法不是拿来训而是拿来当验证集。具体做法是冻结骨干网只训一个小的投影头或者干脆不训直接抽特征算余弦相似度看 AUC 能到多少。我一般会先用预训练模型抽一遍 LFW 所有图的嵌入存成.npy然后在这个嵌入矩阵上快速试不同的距离度量和阈值不用反复过网络迭代速度能快十倍。下面这段抽特征的代码可以复用import numpy as np import torch from torch.utils.data import DataLoader from torchvision import transforms from PIL import Image def extract_embeddings(model, index, device, batch_size64): model.eval() tf transforms.Compose([ transforms.CenterCrop(224), transforms.Resize(112), transforms.Grayscale(num_output_channels3), transforms.ToTensor(), transforms.Normalize([0.5]*3, [0.5]*3), ]) all_paths, all_ids [], [] for ident, paths in index.items(): for p in paths: all_paths.append(p) all_ids.append(ident) embs [] with torch.no_grad(): for i in range(0, len(all_paths), batch_size): batch_paths all_paths[i:ibatch_size] imgs torch.stack([tf(Image.open(p).convert(RGB)) for p in batch_paths]) imgs imgs.to(device) e model.forward_once(imgs) embs.append(e.cpu().numpy()) return np.concatenate(embs, axis0), all_ids, all_paths拿到嵌入矩阵后你可以直接算所有正负对的余弦相似度画 ROC调阈值。这个流程的好处是换损失函数、换骨干、换预处理只需要重抽一次特征不用重训。我自己的习惯是任何新想法先在 LFW 上抽特征验证AUC 有提升再上大规模训练避免浪费卡时。最后说一句血泪经验LFW 上的指标好看不代表线上好用它太干净了真实场景的侧脸、遮挡、光照变化它覆盖得很少。把它当 sanity check别当终点。希望帮到你。本文还有配套的精品资源点击获取
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑