ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

LFW人脸识别数据集实战:从lfw_home.zip到验证评估全流程

LFW人脸识别数据集实战:从lfw_home.zip到验证评估全流程

简介:这份资源是经典人脸识别数据集 LFW 的本地化整理包,面向人脸验证与识别方向的研究者、算法工程师及深度学习入门学习者,用于训练和评估模型判断两张人脸是否属于同一人的能力。压缩包共 9 个文件,约 241.84MB,以 txt 配对清单、pkl 序列化数据、json 配置、tgz 图像归档和 py 脚本为主,其中图像归档承载经预处理的人脸样本,配对清单划分正负样本并区分训练与测试用途,序列化文件便于保存和加载中间结果。已有 510 人学习下载,说明其在入门与复现实验中具备一定参考价值。借助该资源,读者可快速搭建标准评测流程,理解正负样本配对、训练验证划分与泛化评估思路,并在此基础上复现卷积神经网络等经典人脸识别实验,为后续迁移到更大规模数据集打下基础。

1. 从 lfw_home.zip 说起:一份能直接跑通的人脸识别数据集

如果你正在找一个能直接上手、不用折腾申请流程的人脸识别数据集,lfw_home.zip大概率就是你要的东西。它对应的是 LFW(Labeled Faces in the Wild)那套经典数据,只不过被打包成了「解压即用」的目录结构,省掉了原始站点上按人名逐个下载、再手动拼目录的麻烦。LFW 在做人脸验证、人脸识别入门实验时几乎是绕不开的基准集,很多论文里的准确率数字都拿它当参照,所以手里有一份结构规整的本地副本,比每次现去拉数据要省心得多。

这份资源适合三类人:刚接触人脸识别、想跑通第一个验证实验的新手;需要快速搭一个 baseline 做对比的算法工程师;以及要给学生布置实验、又不想在数据准备上耗时间的教学场景。它解决的核心问题不是「数据从哪来」,而是「怎么让数据以最小成本进入你的训练/评估流程」。接下来我会按「这份资源长什么样 → 怎么读进来 → 怎么切训练测试 → 坑在哪 → 怎么验证结果」的顺序拆一遍,中间给到能直接抄的代码和参数。

2. 拆开 lfw_home.zip:目录结构、命名规则与读取方式

2.1 目录布局与文件命名逻辑

LFW 的原始组织方式是按人名建文件夹,每个人名下放若干张 250x250 的 JPEG 人脸图。lfw_home.zip解压后通常保留这套结构,形如:

lfw_home/ lfw_funneled/ Aaron_Peirsol/ Aaron_Peirsol_0001.jpg Aaron_Peirsol_0002.jpg Aaron_Sorkin/ Aaron_Sorkin_0001.jpg pairs.txt pairs_DevTrain.txt pairs_DevTest.txt people.txt ...

这里有几个关键点。第一,文件名里的编号不是全局序号,而是「该人名下的第几张」,所以你不能靠文件名排序去推断拍摄时间或身份顺序。第二,pairs.txt是官方给出的验证对列表,格式是「同名对」和「异名对」混排,每行要么是name n1 n2(同一人的第 n1、n2 张),要么是name1 n1 name2 n2(不同人的两张)。第三,people.txt列出所有人名,做闭集分类时会用到。很多人第一次拿到这份数据,直接os.listdir去遍历,结果把pairs.txt也当成一个人名目录读进去,后面报错找半天——这是最常见的翻车点之一。

2.2 用 Python 把数据读成可用结构

我一般先用一段脚本把目录扫一遍,确认人数、图片总数和分布,再决定后面怎么切。下面这段可以直接跑:

import os from collections import Counter root = "lfw_home/lfw_funneled" # 只保留目录,过滤掉 pairs.txt 之类的文件 people = [d for d in os.listdir(root) if os.path.isdir(os.path.join(root, d))] print("人数:", len(people)) counts = {} for p in people: imgs = [f for f in os.listdir(os.path.join(root, p)) if f.lower().endswith(".jpg")] counts[p] = len(imgs) total = sum(counts.values()) print("图片总数:", total) # 看看每人图片数的分布,LFW 里很多人只有 1 张 dist = Counter(counts.values()) for k in sorted(dist): print(f"有 {k} 张图的人: {dist[k]} 个")

逻辑说明:os.path.isdir那一步是必须的,它把非目录项挡在外面,避免把pairs.txt当人名。counts统计每人图片数,是因为 LFW 的长尾非常严重——大部分人只有一张图,只有少数名人有多张。这个分布直接决定你后面能不能做「每人至少 N 张」的筛选。参数上,root指向解压后的lfw_funneled,如果你解压出来是别的层级,改这一行即可。

跑完你大概率会看到:总人数五千多,图片总数一万三千左右,其中「只有 1 张图」的人占了一多半。这个数字很关键,它意味着如果你要做闭集分类(每人当一类),必须先把只有一张图的人剔掉,否则没法划分训练/测试。

2.3 按 pairs.txt 构造验证对

验证任务不关心「这是谁」,只关心「这两张是不是同一人」。官方pairs.txt已经给好了对,读它的代码如下:

def load_pairs(pairs_path): pairs = [] with open(pairs_path, "r") as f: # 第一行是 10 折的折数,跳过 n_folds = int(f.readline().strip()) for line in f: parts = line.strip().split() if len(parts) == 3: name, i, j = parts pairs.append((f"{name}/{name}_{int(i):04d}.jpg", f"{name}/{name}_{int(j):04d}.jpg", 1)) elif len(parts) == 4: n1, i, n2, j = parts pairs.append((f"{n1}/{n1}_{int(i):04d}.jpg", f"{n2}/{n2}_{int(j):04d}.jpg", 0)) return pairs pairs = load_pairs("lfw_home/pairs.txt") print("验证对总数:", len(pairs)) print("正例对:", sum(1 for p in pairs if p[2] == 1)) print("负例对:", sum(1 for p in pairs if p[2] == 0))

逻辑说明:pairs.txt第一行是折数(通常是 10),必须读掉,否则第一行会被当成数据解析出错。三列是正例(同一人),四列是负例(不同人),标签分别记 1 和 0。文件名拼接时用{int(i):04d}补零到四位,是因为 LFW 的编号就是四位格式,少补零会拼出不存在的路径。参数上,pairs_path换成你实际的路径;如果你用的是pairs_DevTrain.txt或pairs_DevTest.txt,格式一致,函数不用改。

这一步做完,你就有了一个标准的验证对列表,可以直接喂给任意人脸模型算相似度,再按阈值判正负。正负例大致各占一半,这是 LFW 验证协议的设计,别自己再随机抽,否则和论文数字没法比。

3. 从原始图到模型输入:预处理、对齐与切分策略

3.1 为什么不能直接把 250x250 塞进网络

LFW 的图是 250x250,但人脸在画面里的位置、尺度、角度都不统一,有的偏左有的偏右,背景也杂。直接把原图 resize 到 112x112 丢进网络,准确率会明显低于论文数字,因为模型学到的是「背景+姿态」而不是「身份特征」。常见做法是先做人脸检测和对齐,把眼睛、鼻尖、嘴角这些关键点摆到固定位置,再裁剪成统一尺寸。这一步在 LFW 上尤其重要,因为它的图本身就是「in the wild」,姿态变化大。

我一般用现成的检测器(比如基于 MTCNN 或 RetinaFace 的实现)跑一遍,把每张图对齐到 112x112 或 160x160。如果你只是想快速验证流程,也可以先用简单的中心裁剪加 resize 跑通,再逐步加对齐,这样能分清「是模型不行」还是「预处理没做」。

3.2 训练/测试切分的两种思路

LFW 的官方评估协议是「10 折交叉验证」,用pairs.txt里的对来算准确率。但如果你要训练一个模型,就不能用全部数据训练再在同一批对上测,那样数字虚高。两种常见切法:

第一种,按pairs_DevTrain.txt训练、pairs_DevTest.txt测试,这是官方给的开发集划分,适合快速迭代。第二种,自己按人名切:把一部分人划进训练集,另一部分人划进测试集,做「开集」验证,更接近真实场景。第二种更严格,但实现麻烦一点,需要保证训练和测试的人名不重叠。

下面是一个按人名切分的示例:

import random random.seed(42) people = [d for d in os.listdir(root) if os.path.isdir(os.path.join(root, d))] random.shuffle(people) split = int(len(people) * 0.8) train_people = set(people[:split]) test_people = set(people[split:]) # 只保留在对应集合里、且至少有 2 张图的人,方便构造对 def filter_pairs(pairs, allowed): out = [] for a, b, label in pairs: pa = a.split("/")[0] pb = b.split("/")[0] if pa in allowed and pb in allowed: out.append((a, b, label)) return out train_pairs = filter_pairs(pairs, train_people) test_pairs = filter_pairs(pairs, test_people) print("训练对:", len(train_pairs), "测试对:", len(test_pairs))

逻辑说明:random.seed(42)固定随机种子,保证每次切分一致,方便复现。split取 0.8 是经验值,人名级别的 8:2 通常够用。filter_pairs保证一对里的两张图都来自允许的人名集合,避免测试集里混进训练过的人。参数上,seed和split都可以调,但调完要重新跑一遍所有对比实验,否则数字不可比。

提示:如果你要做闭集分类,记得先把只有 1 张图的人过滤掉,否则划分时会出现某一类在训练集里一张图都没有的情况。

3.3 数据增强的边界

LFW 本身图不多,训练时加增强是常规操作:随机水平翻转、小角度旋转、亮度微调。但要注意,人脸识别对「水平翻转」是敏感的——翻转后左右脸互换,某些模型会把它当成不同身份。我的习惯是翻转可以用,但幅度控制在 ±10 度以内,且不要做垂直翻转。颜色抖动也别太狠,LFW 的图质量参差,过度增强反而让模型学偏。

4. 避坑与排查:LFW 实操里最容易翻车的五件事

4.1 把 pairs.txt 当成人名目录读进去

现象:遍历目录时多出一个叫pairs.txt的「人」,后面拼路径报NotADirectoryError。 原因:os.listdir返回目录下所有条目,文件和目录混在一起。 解决:遍历时加os.path.isdir判断,或者用glob只匹配目录。这个坑几乎每个新手都会踩一次。

4.2 文件名补零位数不对导致路径拼错

现象:明明图就在那,代码却报文件不存在。 原因:LFW 编号是四位,1要写成0001,直接str(i)拼出来是name_1.jpg,对不上。 解决:统一用f"{int(i):04d}"格式化。读pairs.txt时也要注意,文件里给的编号可能不带前导零,必须自己补。

4.3 用全部数据训练又在同一批对上评估

现象:准确率 99% 以上,换一批对就掉到 80%。 原因:训练集和测试集重叠,模型见过测试样本。 解决:严格按人名或按官方 DevTrain/DevTest 切分,训练用的人名绝不出现在测试对里。评估前先打印两边的人名交集,确认为空再跑。

4.4 忽略长尾分布,闭集分类类别数虚高

现象:分类模型类别数设成五千多,训练时大量类别只有一张图,loss 震荡不收敛。 原因:LFW 大部分人只有一张图,没法做「每类多样本」的监督训练。 解决:要么过滤掉少于 N 张图的人(N 取 2 或 5),要么改用度量学习(如 triplet loss),不依赖每类多样本。参数上,N 越大类别越少但每类越干净,我一般从 2 开始试。

4.5 对齐前后混用,导致对比实验不可比

现象:两次实验只改了模型,准确率却差很多,找不到原因。 原因:一次用了对齐后的图,一次用了原图,预处理不一致。 解决:把预处理固定成一条流水线,对齐参数(目标尺寸、关键点定义)写进配置,每次实验前确认配置没变。我习惯把对齐后的图单独存一份,训练只读这份,避免每次现算引入随机性。

5. 验证与进阶:用一对阈值把准确率算清楚

跑通流程之后,最后一步是验证你的结果到底靠不靠谱。人脸验证的标准做法是:对每一对图算一个相似度分数,然后扫一遍阈值,看哪个阈值下准确率最高。这里有个容易忽略的点——阈值必须在验证集上选,不能拿测试集选完再报测试集准确率,那是自欺欺人。

下面这段把「算分 → 扫阈值 → 报准确率」串起来:

import numpy as np def evaluate(scores, labels): # scores: 每对的相似度,labels: 1 同人 / 0 异人 scores = np.asarray(scores) labels = np.asarray(labels) best_acc, best_thr = 0.0, 0.0 for thr in np.linspace(scores.min(), scores.max(), 200): pred = (scores >= thr).astype(int) acc = (pred == labels).mean() if acc > best_acc: best_acc, best_thr = acc, thr return best_acc, best_thr # 假设你已经算好了 scores 和 labels # acc, thr = evaluate(scores, labels) # print(f"最佳准确率 {acc:.4f} @ 阈值 {thr:.4f}")

逻辑说明:np.linspace在分数范围内均匀取 200 个阈值,逐个算准确率,取最高的那个。pred = (scores >= thr)是「大于等于阈值判为同一人」的约定,如果你的相似度是距离(越小越像),要改成<=。参数上,200 是精度和速度的折中,想更细可以加到 1000,但收益有限。返回的best_thr要记下来,部署时直接用这个阈值,别每次重扫。

进阶一点的做法是画 ROC 曲线算 AUC,或者按 10 折分别选阈值再平均,这样报出来的数字更稳。我自己的习惯是:任何一次实验,先把预处理配置、切分种子、阈值选择方式三样固定死,再动模型。这三样只要有一个飘了,后面的对比全是玄学。从那以后我每次跑 LFW 实验,都强制先打印一遍人数、图片数、正负例对数和人名交集,确认无误再开训。希望帮到你。

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

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进