简介LFWLabeled Faces in the Wild人脸识别基准数据集资源包面向人脸验证与人脸识别方向的算法研究者、深度学习者及安全风控相关从业者可用于模型训练、效果评估与学术基准对比也是人脸识别课程设计与毕业设计的常用数据来源。压缩包内含经尺寸标准化与光照调整的 lfwfunneled.tgz 人脸图像归档以及 pairs.txt、pairsDevTrain.txt、pairsDevTest.txt 三份正负样本配对文件分别对应评估、训练与验证阶段另有 pkl、json 格式数据及 py 辅助脚本等配套内容。资源共 9 个文件以 txt、pkl、json 类型为主压缩包约 241.84MB已有 512 人学习下载。研究者拿到后可直接开展同类人脸比对实验省去自行爬取和清洗数据的环节借助标准配对划分亦能快速复现经典的 LFW 评估流程为人脸识别模型选择阈值、测试泛化能力提供可靠参考并从传统手工特征到深度学习方法的演进脉络中获得实验设计启发。1. 人脸识别数据集LFW到底能用来做什么做算法验证时最怕的不是模型写不出来而是手里没有一份拿得出手的人脸识别数据集。lfw_home.zip 解决的问题很直接它把 13000 多张真实人脸按人名归类并且配好了 pairs.txt 验证协议让没有条件自采数据的团队也能在统一的评测口径下跑训练、跑验证、跟别人对比结果。它适合三类人刚入门想把人脸识别流程走通的新手做算法预研时需要基准库的开发者以及想快速测试别人模型能力的评估人员。这里提前说清楚LFW 给的是“人名级”标注不是“人脸框级”标注所以用之前必须自己做检测和对齐。下面从解压开始把这套数据一层层拆开。2. 解压与数据解析把lfw_home读成训练能用的格式先把压缩包解压。常见的 lfw_home 发行版里核心结构大致是下面这样图片按人名建目录一个人名一个文件夹标注文件放在根目录。图片目录通常有两套一套是没有额外几何处理的原始图另一套是做过粗对齐的版本常见的目录名是 lfw_funneled。这两套不要混着用后面第四章会专门说这个问题。给你一份文件清单解压后对着找路径内容主要用途lfw_funneled/粗对齐后的人脸图按人名分目录训练和验证的默认图片来源lfw_raw/未对齐的原始图背景差异大需要自己写检测器时用pairs.txt验证协议一行一个/两个样本对测试模型时按这个文件打分people.txt人名和对应图片数量的清单检查数据完整性、按人划分数据集提示不同来源的压缩包目录名可能不同解压后先ls看一眼再写代码。2.1 解析 pairs.txt正负样本对与验证协议pairs.txt 是整份数据集最关键的文件它定义的不是分类标签而是“两张图是不是同一个人”的判断任务。常见格式有两种三列的行表示同一个人的两张图四列的行表示两个不同的人各取一张图。我一般先按行数做防御性解析不要一上来就假定固定列数def load_pairs(path): lines [ln.strip() for ln in open(path, r, encodingutf-8) if ln.strip()] if lines and lines[0].split()[0].isdigit(): # 有些发行版第一行是 fold 数和每折对数跳过 lines lines[1:] pairs [] for idx, line in enumerate(lines): t line.split() if len(t) 3: name, i1, i2 t[0], int(t[1]), int(t[2]) pairs.append((name, i1, name, i2, 1)) elif len(t) 4: n1, i1, n2, i2 t[0], int(t[1]), t[2], int(t[3]) pairs.append((n1, i1, n2, i2, 0)) else: print(fskip line {idx}: {line}) return pairs这段代码的重点在if len(t) 3和elif len(t) 4的分支三列分支构造的是正样本对pairs 里的 1 表示“同一人”四列分支构造负样本对0 表示“不同人”。五个元素里前四个是人物身份和图片编号最后一个是标签。跳过异常行时打印行号是防止某些发行版里混进空行或 tab 缩进的误导。解析完成后建议立刻print(len(pairs))和压缩包说明里的对数核对一下。2.2 构建图片索引按人名组织路径有了 pairs 后还要把图片路径和人物名对上。LFW 的目录是按人名分的所以读取起来很直接但也有细节要处理比如图片后缀可能是 .jpg 也可能是 .png文件夹名里可能混进无关文件。我常用的写法是这样from pathlib import Path def build_image_index(root, image_subdirlfw_funneled): idx {} for folder in sorted((Path(root) / image_subdir).iterdir()): if not folder.is_dir(): continue files [] for p in sorted(folder.iterdir()): if p.suffix.lower() in [.jpg, .jpeg, .png]: files.append(p) if files: idx[folder.name] files return idx返回值是一个字典人名 - 排序后的图片路径列表。排序是必须的因为 pairs.txt 里的编号比如1和2对应的是目录内排序后的第 1 张、第 2 张不排序就可能取错图。这个函数里没有做去重遇到文件夹名重复时后一个会覆盖前一个实际发行版一般不会出现但 debug 时可以留意。如果项目里遇到两个不同的人恰好同名比如数据集整理时把多个子目录合到一起那么只用folder.name当主键会串。常见做法是维护一个person_id自增编号把(name, person_id)当成唯一身份。2.3 划分训练与验证按人分而不是按图片分人脸识别里最容易出问题的不是模型而是数据划分。LFW 的标准做法是把图片按人分组再把人切到不同 fold保证同一个人的所有照片只出现在训练或验证中的一边。下面是按人名分配 fold 的稳定方法from hashlib import md5 def assign_fold(name, num_folds10): digest md5(name.encode(utf-8)).hexdigest()[:8] return int(digest, 16) % num_folds这里没有直接用 Python 内置hash()因为内置哈希带随机盐同一个名字在不同进程里分到的 fold 可能不同实验就没法复现。用 md5 截前 8 位十六进制转整数再取模结果稳定也和操作系统无关。分配好 fold 之后再配合 pairs.txt 按行区间取验证集标准协议的 10 折就是把 pairs 顺序切成 10 段每一段单独做验证。这一段逻辑看起来简单却是后面所有评估数字的基准值得在代码里单独写一个模块。3. 预处理而非直接训练人脸对齐、裁剪与归一化的四步实操LFW 里标好的是“谁是谁”不是“脸在哪”。funneled 目录虽然做了粗对齐但依然不是深度学习框架能直接吃的标准输入。有的图片头部偏上有的偏下有的带了一半背景眼睛不在固定位置。如果把这堆图直接塞进卷积网络模型会把背景纹理、肤色差异当成身份特征验证准确率会很飘。所以拿到数据后的第二件事是对齐和裁剪。3.1 为什么要对齐先说清楚目标对齐是把两张同一人的照片弄到“眼睛在差不多的位置、脸在差不多的尺度”的过程。现代做法是检测人脸关键点比如左眼、右眼、鼻尖或嘴角然后做仿射变换把这些点映射到目标坐标系。LFW 的原始图多数只有一张脸没有遮挡极端的照片所以关键点检测的成功率比其他场景高不少适合当对齐的练习数据。如果你只是跑一个基线实验funneled 版本可以直接用省掉检测步骤如果你想复现论文里的高精度结果那需要自己用关键点做二次对齐。常见做法是用现成的关键点检测库检测失败时退回中心裁剪不要为了几个失败样本中断整批流程。3.2 仿射对齐的参考实现与参数说明我一般会把对齐函数写成独立模块输入是 BGR 图片和关键点字典输出是固定尺寸的对齐图。参考实现如下import cv2 import numpy as np OUTPUT_SIZE (112, 112) # 目标位置左眼、右眼、鼻尖 DST_LANDMARKS np.float32([ [0.35 * 112, 0.35 * 112], [0.65 * 112, 0.35 * 112], [0.50 * 112, 0.55 * 112], ]) def align_face(img, landmarks): # landmarks 是 dict至少包含 left_eye / right_eye / nose src np.float32([ landmarks[left_eye], landmarks[right_eye], landmarks[nose], ]) warp_mat, _ cv2.estimateAffinePartial2D(src, DST_LANDMARKS) aligned cv2.warpAffine(img, warp_mat, OUTPUT_SIZE, flagscv2.INTER_LINEAR, borderModecv2.BORDER_CONSTANT, borderValue(0, 0, 0)) return aligned参数含义src是检测到的三个关键点DST_LANDMARKS是处理后的固定位置。眼睛间距设成 0.3 倍宽度是主流人脸识别实验比较常看到的配置鼻尖放低一点给额头留出空间。estimateAffinePartial2D只估计旋转、平移和缩放不做切变用在普通人脸上更稳定。borderValue设成黑色避免 warp 之后边缘冒出灰白噪声。如果你手里没有关键点检测结果可以退化为人脸框加中心裁剪def center_crop_fallback(img, box): x, y, w, h box cx, cy x w / 2, y h / 2 side max(w, h) x1 int(cx - side / 2) y1 int(cy - side / 2) crop img[y1:y1 side, x1:x1 side] return cv2.resize(crop, OUTPUT_SIZE)这个兜底逻辑的关键是裁剪框边长取max(w, h)保证脸不会被截掉一部分然后整体缩放破坏宽高比也无所谓因为后面网络输入是正方形。对齐失败的数据宁可退化成中心裁剪也不要跳过否则训练时样本数量会忽多忽少导致每个 epoch 的 loss 曲线对不上。3.3 归一化与数据增强送进网络前的最后一公里对齐之后再谈颜色。LFW 图片来自不同年代的采集色温、亮度差异明显。有的像素值在 0 到 255 之间有的因为压缩已经偏移。训练前统一转成 RGB并做 z-score 标准化是基本操作MEAN np.array([0.5, 0.5, 0.5]) * 255.0 STD np.array([0.5, 0.5, 0.5]) * 255.0 def normalize(img): return (img.astype(np.float32) - MEAN) / STD这里固定用 0.5/0.5/0.5不按数据集统计均值和方差是为了让验证时的预处理和训练保持一致避免在评估阶段踩“预处理不一致导致精度掉点”的坑。数据增强方面我一般只做随机水平翻转和 0.95~1.05 倍的轻度缩放不做随机擦除或强颜色扰动因为 LFW 的样本量不大增强太猛会让模型学不到稳定的身份特征。预处理部分的教训是宁可多写几个小函数也不要全堆在__getitem__里。把对齐、缩放、归一化拆开后面排查图片预处理问题时能直接定位到哪一步出了问题。4. 避坑排查四个容易查不出来的数据问题这章写的不是模型调参的玄学而是我实际拆 LFW 时踩过的真坑。每一条都按“现象 → 原因 → 解决”的顺序来排查时可以对着一条条看。4.1 坑一pairs.txt 解析错位正负样本对数量对不上现象解析完 pairs 后打印样本对数发现和说明里写的 3000 或 6000 对差了上百对用这些样本去评估模型准确率低得离谱。原因不同来源的 pairs.txt 格式不完全一致。有些版本首行是 “10 300” 这类头信息有些没有有些行以空格分隔有些混了 tab还有的发行版夹带了空行和注释行。解析时如果按固定行号取数必然错位。解决先head -n 20 pairs.txt查看真实格式。解析时跳过所有空行检测首行是否全是数字是的话跳过头。对长短不一的列做防御三列当正样本四列当负样本其他长度打印警告并跳过。解析完成后打印标签分布核对正负样本数量应该大致各占一半。4.2 坑二按图片随机切分造成身份泄露现象训练集和验证集是同一个人的不同照片模型在验证集上的准确率高达 98% 以上比正常水平高出一大截很明显不对劲。原因很多人把 PyTorch 的random_split当成默认划分方法。在一般图像分类任务里这没问题但在人脸数据上同一个人出现在两边模型只是记住了这个人的特征验证任务退化成了“查表”并不是真正意义上的识别。解决划分数据集前先按人名分组再对人名做 fold 分配而不是对图片做随机洗牌。可以加一个断言来防呆train_names set(train_items.keys()) val_names set(val_items.keys()) assert not (train_names val_names), 同一人出现在训练和验证两边这个断言在 LFW 这种小数据集上会立刻暴露出划分问题值得放进数据加载流程里。4.3 坑三funneled 和 raw 两个目录混用现象有一段时间模型精度只在 80% 上下波动上不去排查后发现训练集里一部分图片是粗对齐后的一部分是原始图而且两者的文件尺寸还不一致。原因代码里用了glob.glob(root /**/*.jpg)这种递归查找方式同时命中了lfw_raw和lfw_funneled。两个目录的图片处理程度不同网络输入规格也不同混在一起等于用两套分布训练。解决构建图片索引时固定传入image_subdirlfw_funneled不要把整个根目录都丢进去。读取完成后还可以抽查图片尺寸funneled 版本尺寸通常统一raw 版本则五花八门用尺寸分布就能判断是不是混了。4.4 坑四灰度图和三通道图混在一起训练 loss 突然变 NaN现象训练到一半 loss 变成 NaN或者精度上下剧烈震荡检查代码没发现明显问题。原因LFW 早期采集的图片有些是灰度图只有单通道。DataLoader 如果统一按三通道读某些图片在cv2.imread返回后仍然是二维数组后续的归一化、卷积一碰到这种张量就崩。解决在__getitem__里做通道兜底读图后判断ndimimg cv2.imread(str(path)) if img is None: raise FileNotFoundError(path) if img.ndim 2: img cv2.cvtColor(img, cv2.COLOR_GRAY2BGR)更稳妥的做法是在构建索引时就把灰度图筛出来单独处理而不是等到训练时才暴露。你可以在build_image_index里顺带读一遍所有图片把通道数异常的文件名记下来这样训练之前就知道哪些样本需要特殊对待。5. 训练一个简易验证模型从分类到开集验证的一整套流程很多人拿到 LFW 第一反应是把它当成 5000 多类的分类任务直接训练一个分类器。这个做法不算错但它只解决“闭集识别”不是 LFW 协议里规定的“验证”任务。验证任务给的是两张图模型要回答“同一人还是不同人”这个人永远不会出现在训练集里。所以正确的使用方式是先用分类任务训练出一个特征提取器再用 pairs.txt 里的样本对做相似度打分跑开集验证。5.1 数据集封装把索引和标签交给 DataLoader前面已经把图片索引和 pairs 都准备好了这里把它们拼成一个标准 PyTorch Datasetimport torch from torch.utils.data import Dataset, DataLoader import cv2 class LFWDataset(Dataset): def __init__(self, items, transformNone): # items: List[(path, person_id)] self.items items self.transform transform def __len__(self): return len(self.items) def __getitem__(self, idx): path, label self.items[idx] img cv2.imread(str(path)) img cv2.cvtColor(img, cv2.COLOR_BGR2RGB) img cv2.resize(img, (112, 112)) if self.transform: img self.transform(img) return torch.from_numpy(img).permute(2, 0, 1).float(), label这段代码有两个容易忽略的点cvtColor是必须的OpenCV 读出来是 BGR直接给模型会导致颜色分布和预训练假设对不上permute(2,0,1)把 HWC 转成 CHWPyTorch 默认的输入顺序是通道在前。getitem里没有做归一化是刻意留到训练脚本里统一做方便和数据增强组合。5.2 特征提取器与分类头训练循环里的关键参数分类训练用一个小型卷积网络就够不需要在 LFW 上追求大模型。结构是两层卷积加自适应池化后面接一个线性分类头import torch.nn as nn class SimpleFaceNet(nn.Module): def __init__(self, num_classes): super().__init__() self.backbone nn.Sequential( nn.Conv2d(3, 32, 3, padding1), nn.BatchNorm2d(32), nn.ReLU(inplaceTrue), nn.MaxPool2d(2), nn.Conv2d(32, 64, 3, padding1), nn.BatchNorm2d(64), nn.ReLU(inplaceTrue), nn.MaxPool2d(2), ) self.pool nn.AdaptiveAvgPool2d(1) self.head nn.Linear(64, num_classes) def forward(self, x): x self.backbone(x) x self.pool(x).flatten(1) return self.head(x)全连接层的输入是 64 维来自最后一层特征图的池化结果。用 AdaptiveAvgPool2d 而不是 Flatten是为了让网络不依赖输入尺寸验证阶段换分辨率也不容易崩。训练时 optimizer 我一般选 Adamlr1e-3weight_decay1e-4batch size 取 32。LFW 总共就一万三千多张图这个规模下更大的 batch 只会让收敛变慢不会有明显收益。5.3 从分类到验证用余弦相似度处理样本对分类头训练完成后把head丢掉只用backbone输出 64 维特征。对 pairs.txt 里的每一个样本对分别取两张图的特征然后算余弦相似度def cosine_similarity(feat1, feat2): return float((feat1 feat2) / (torch.norm(feat1) * torch.norm(feat2) 1e-6))相似度大于阈值判为“同一人”小于阈值判为“不同人”。这个阈值不是拍脑袋定的而是要在验证集上遍历找最优值。准确率、误识率、拒识率这些指标的含义可以在评估阶段统一算。下面给一张表是你在看结果时最常对它比对的几个数指标计算方式说明Accuracy(TPTN) / 总对数最容易看的总体准确率FARFP / (FPTN)不同人被错认成同一人的比例FRRFN / (FNTP)同一人被错认成不同人的比例EERFAR 和 FRR 最接近的错误率越低说明模型区分能力越好我用这个流程在模拟项目X上跑过一遍最大的体会是模型结构在 LFW 上的影响远不如数据对齐和评估协议来得大。同样的网络对齐做得干净可以比不处理高好几个点。所以训练之前多花时间在前两章的数据管线上性价比很高。6. 在标准协议上验证效果一个可复用的评估函数与阈值选择有了上面的余弦相似度下一步就是把分数聚合成一份可读的验证报告。很多人最后翻车就翻在“不同工具算出来的 FAR/FRR 对不上”原因基本都是阈值方向和正负对顺序没有统一。下面这个评估函数输入是标准列表输出是最好阈值和四个指标直接写进项目里就能复用。import numpy as np def evaluate_lfw(scores, labels, steps1000): scores np.asarray(scores, dtypenp.float64) labels np.asarray(labels, dtypenp.int64) best {acc: -1.0, threshold: 0.0, far: 1.0, frr: 1.0} lo, hi scores.min(), scores.max() for t in np.linspace(lo, hi, steps): pred np.where(scores t, 1, 0) tp np.sum((pred 1) (labels 1)) fp np.sum((pred 1) (labels 0)) tn np.sum((pred 0) (labels 0)) fn np.sum((pred 0) (labels 1)) acc (tp tn) / len(labels) far fp / max(fp tn, 1) frr fn / max(fn tp, 1) if acc best[acc]: best {acc: acc, threshold: float(t), far: far, frr: frr} return best代码里scores t的判定方向是针对相似度设计的分数越大越可能是同一个人。如果你模型输出的是欧氏距离负责评估的人必须把比较方向调转否则准确率会直接反着走。max(fp tn, 1)是防止分母为 0 的兜底样本对少了会保护你的调试过程不闪退。拿到 best 之后还可以用 sklearn 的 roc_curve 画出 ROC 曲线帮助确认阈值附近是否平滑。如果曲线在某个区间抖动比较厉害多半是验证对里混入了异常样本回到第三章的对齐脚本去查那一批图。最后讲一个我自己的教训。那次我为了尽快看结果随手用random_split切了数据跑出来的验证准确率比预期的好了快两个点当时还挺高兴直到后来按标准协议重新切分才意识到之前的数字是身份泄露带来的虚高白白折腾了三天。从那以后我每次在 LFW 上做实验都强制走一遍“按人分 fold → 断言训练验证无重叠人名 → 标准评估函数”这串流程再拿数字去跟别人对比。希望帮到你。本文还有配套的精品资源点击获取