CASIA WebFace 数据集实战:从预处理到人脸识别模型训练

📅 发布时间:2026/10/11 17:31:22
CASIA WebFace 数据集实战:从预处理到人脸识别模型训练
简介CASIA WebFace 是人脸识别领域最主流的大规模数据集之一面向从事人脸识别算法研究、模型训练与工程落地的开发者及高校师生尤其适合需要海量身份标注数据来训练或微调深度人脸识别模型的场景。该资源以百度网盘链接形式提供压缩后约 4.1G内含 1 万个人物身份、约 50 万张人脸照片覆盖丰富的姿态、光照与表情变化是复现和对比主流人脸识别方法的理想数据基础。资源包本身为单个 docx 文档大小约 11KB文档内给出网盘地址与提取密码便于快速获取完整数据集。目前已有 547 人浏览学习说明其在人脸识别学习群体中具有一定关注度。对于希望搭建训练流水线、验证损失函数与网络结构效果或开展身份分类与特征提取实验的读者这份数据能直接支撑从数据预处理到模型评估的完整流程省去自行采集与清洗标注的高昂成本。1. 从一次翻车现场说起CASIA WebFace 到底能干什么去年帮一个做门禁系统的朋友调模型他抱怨说自研的人脸识别在测试集上准确率 98%一到真实场景就崩——同一个人换了副眼镜就认不出双胞胎更是直接抓瞎。我问他训练集用了多少张脸他说两万。问题就出在这两万张脸训练出来的特征提取器泛化能力约等于闭眼猜。后来换成 CASIA WebFace 重训同样的网络结构跨眼镜场景的误识率直接降了一个数量级。这就是 CASIA WebFace 的价值所在。它不是那种精挑细选的“干净”数据集而是从互联网上爬取的、带着真实世界噪声的大规模人脸集合——1 万个身份、约 50 万张图像压缩包 4.1G。这个量级意味着什么意味着你的模型能见到足够多的类内变化不同角度、不同光照、不同表情、不同遮挡。对于做人脸识别、人脸验证、特征嵌入学习的从业者来说它是绕不开的基线数据集。如果你正在训练 ArcFace、CosFace 这类度量学习模型或者想验证自己设计的损失函数在万级身份规模下是否还收敛这份资源就是你的起跑线。2. 数据集结构拆解1 万个人、50 万张图是怎么组织的拿到压缩包后别急着解压完就往 DataLoader 里塞。先搞清楚它的目录逻辑能省掉后面很多返工。2.1 目录层级与命名规则CASIA WebFace 解压后的典型结构是两层根目录下是 1 万个以数字编号命名的子文件夹每个子文件夹对应一个身份 ID里面存放该身份的所有图像。文件名通常是编号_序号.jpg的格式比如0000045_001.jpg表示第 45 号身份的第 1 张图。这种“文件夹即类别”的组织方式天然适配 PyTorch 的ImageFolder和 TensorFlow 的image_dataset_from_directory不需要额外写标签映射文件。但要注意身份编号不连续中间有跳号这是爬取过程中过滤掉低质量数据留下的空洞。写代码时不要假设 ID 从 0 到 9999 连续用os.listdir动态获取实际类别列表。2.2 图像规格与质量分布图像尺寸不统一长边从 100 多像素到 300 多像素都有短边更参差。大部分是彩色 JPEG少数是灰度图。人脸在画面中的占比也不一致——有些是近景大头照有些是半身像人脸只占画面一小块。这意味着两件事第一你必须在数据加载阶段做统一 resize 和人脸对齐否则网络输入的分布差异太大会拖慢收敛第二不要指望这个数据集能直接拿来训练高精度关键点检测它的标注只有身份标签没有人脸框和五点关键点。常见做法是先用 MTCNN 或 RetinaFace 做一遍人脸检测和对齐把对齐后的 112×112 或 96×96 图像存成新的训练集。2.3 类别不平衡的真实情况虽然平均下来每个身份约 50 张图但实际分布并不均匀。有些身份有上百张有些只有十几张。这种长尾分布是真实场景的常态也是 CASIA WebFace 比那些精平衡数据集更有价值的地方——用它训练出来的模型对少样本类别的泛化能力更接近实际部署环境。处理不平衡的常见手段有三种按类别采样权重、对少样本类做数据增强、或者直接用 ArcFace 这类对类别不平衡鲁棒性较好的损失函数。我一般会先跑一遍统计脚本看看最少和最多的类别各有多少张再决定要不要做重采样。import os from collections import Counter root /path/to/CASIA-WebFace counts {} for identity in os.listdir(root): id_dir os.path.join(root, identity) if os.path.isdir(id_dir): counts[identity] len(os.listdir(id_dir)) values list(counts.values()) print(f总身份数: {len(counts)}) print(f总图片数: {sum(values)}) print(f每身份最少: {min(values)}, 最多: {max(values)}, 中位数: {sorted(values)[len(values)//2]})这段脚本遍历根目录下所有子文件夹统计每个身份的图像数量。os.listdir返回的文件列表长度就是该身份的样本数。输出结果能帮你判断是否需要做类别平衡——如果中位数和均值差距超过 30%建议在采样器里加权重。3. 从原始图像到训练张量预处理流水线怎么搭数据集本身是“生”的直接喂给网络效果不会好。这一章把预处理拆成三步人脸对齐、尺寸归一化、数据增强。每一步都有参数要调调错了就是白跑。3.1 人脸检测与五点对齐CASIA WebFace 的图像来自互联网人脸位置和姿态千奇百怪。不做对齐直接 resize等于让网络去学“如何容忍人脸不在画面中央”浪费模型容量。标准做法是用 RetinaFace 或 MTCNN 检测人脸框和五个关键点左右眼、鼻尖、左右嘴角然后通过相似变换把关键点映射到标准位置。以 112×112 输入为例标准五点坐标通常设为左眼 (38.29, 51.69)、右眼 (73.53, 51.50)、鼻尖 (56.02, 71.73)、左嘴角 (41.55, 92.37)、右嘴角 (70.73, 92.20)。这个坐标来自 ArcFace 论文的设定被广泛沿用。import cv2 import numpy as np from retinaface import RetinaFace # 标准五点坐标112x112 REF_POINTS np.array([ [38.2946, 51.6963], [73.5318, 51.5014], [56.0252, 71.7366], [41.5493, 92.3655], [70.7299, 92.2041] ], dtypenp.float32) def align_face(img_path, output_size112): img cv2.imread(img_path) if img is None: return None faces RetinaFace.detect_faces(img) if not isinstance(faces, dict) or len(faces) 0: return None # 取置信度最高的人脸 best max(faces.values(), keylambda x: x[score]) landmarks np.array(best[landmarks], dtypenp.float32) # 五点顺序需与REF一致 # 计算相似变换矩阵 tform, _ cv2.estimateAffinePartial2D(landmarks, REF_POINTS) if tform is None: return None aligned cv2.warpAffine(img, tform, (output_size, output_size)) return alignedRetinaFace.detect_faces返回字典每个键对应一张检测到的人脸score是置信度。estimateAffinePartial2D计算从检测关键点到标准关键点的相似变换矩阵只包含旋转、平移和缩放不引入剪切变形。warpAffine按这个矩阵把原图映射到 112×112 画布。如果检测不到人脸或变换矩阵计算失败返回None后续统一丢弃这些样本。3.2 归一化参数与通道顺序对齐后的图像是 BGR 三通道像素值 0-255。送入网络前要做两件事转 RGB、归一化到 [-1, 1] 或 [0, 1]。用 ImageNet 预训练权重做 backbone 时通常按 ImageNet 的均值和标准差做标准化mean[0.485, 0.456, 0.406]std[0.229, 0.224, 0.225]。如果从零训练用简单的 (x/255 - 0.5)/0.5 也够用。注意 OpenCV 读进来是 BGRPyTorch 的ToTensor不会帮你转通道得手动cv2.cvtColor(img, cv2.COLOR_BGR2RGB)。这个坑我踩过不止一次——训练 loss 死活不降排查半天发现是通道反了。3.3 数据增强的边界人脸识别的增强和通用图像分类不太一样。水平翻转可以用但垂直翻转不行——没人倒着长脸。随机裁剪要小心裁太狠可能把眼睛或嘴巴裁掉破坏身份信息。颜色抖动幅度别太大否则可能改变肤色特征让模型学到错误的身份线索。我一般用这套组合随机水平翻转p0.5、随机亮度对比度调整幅度 0.2、随机擦除p0.3擦除面积不超过 15%。随机擦除模拟遮挡对提升真实场景鲁棒性很有帮助但擦除区域别盖住双眼和嘴巴。import torchvision.transforms as T train_transform T.Compose([ T.ToPILImage(), T.RandomHorizontalFlip(p0.5), T.ColorJitter(brightness0.2, contrast0.2), T.RandomErasing(p0.3, scale(0.02, 0.15), ratio(0.3, 3.3)), T.ToTensor(), T.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ])RandomErasing的scale参数控制擦除面积占整图的比例(0.02, 0.15)表示最小 2%、最大 15%。ratio控制擦除区域的宽高比范围。这两个参数别设太大否则一张脸被擦掉一半标签就成噪声了。4. 训练集划分与评估协议别在测试集上翻车数据集准备好了怎么切分训练集和测试集直接决定你论文里的数字能不能复现、工程里的模型能不能上线。4.1 身份级划分不是图片级人脸识别的评估逻辑是训练时见过的身份测试时不能再出现。所以划分必须按身份走——比如 1 万个人里取 9000 个做训练1000 个做测试。如果按图片随机划分同一个人既有训练图又有测试图准确率能刷到 99.9%但上线就废。常见做法是留出 10% 到 20% 的身份做验证集和测试集。具体选哪些身份随机选就行但建议固定随机种子保证实验可复现。如果要做跨数据集评估可以把 CASIA WebFace 全部用于训练在 LFW、AgeDB 等标准测试集上验证——但那些测试集不在本次资源范围内需要另行获取。4.2 验证集上的评估指标人脸验证的标准评估方式是 ROC 曲线和 TARFAR。TARFAR1e-3 表示误报率控制在千分之一时正确接受率是多少。这个指标比单纯看准确率更有意义因为真实门禁场景对误报极其敏感。评估时从测试身份中随机抽取正负样本对正样本对来自同一身份的两张图负样本对来自不同身份。通常抽 6000 对正样本、6000 对负样本计算特征余弦相似度再扫阈值画 ROC。import numpy as np from sklearn.metrics import roc_curve def evaluate(model, test_loader, device): model.eval() feats, labels [], [] with torch.no_grad(): for imgs, lbls in test_loader: imgs imgs.to(device) emb model(imgs) feats.append(emb.cpu().numpy()) labels.extend(lbls.numpy()) feats np.concatenate(feats, axis0) labels np.array(labels) # 归一化特征 feats feats / np.linalg.norm(feats, axis1, keepdimsTrue) # 构造正负样本对 pos_sims, neg_sims [], [] for i in range(len(labels)): for j in range(i1, len(labels)): sim np.dot(feats[i], feats[j]) if labels[i] labels[j]: pos_sims.append(sim) else: neg_sims.append(sim) scores np.concatenate([pos_sims, neg_sims]) y_true np.concatenate([np.ones(len(pos_sims)), np.zeros(len(neg_sims))]) fpr, tpr, _ roc_curve(y_true, scores) # 找 FAR1e-3 时的 TAR idx np.searchsorted(fpr, 1e-3) tar tpr[min(idx, len(tpr)-1)] return tar这段代码先提取所有测试样本的特征并做 L2 归一化然后穷举所有样本对计算余弦相似度。正样本对标签相同负样本对不同。roc_curve返回不同阈值下的 FPR 和 TPRsearchsorted找到 FPR 最接近 1e-3 的位置取对应的 TPR 作为 TARFAR1e-3。注意穷举所有样本对在测试身份多的时候会很慢实际可以用随机采样代替。4.3 训练时的监控指标训练过程中不能只看 loss。建议每跑完一个 epoch在验证集上算一次 TARFAR1e-3。如果 loss 在降但 TAR 不涨说明模型在过拟合训练身份泛化能力没提升。这时候要么加正则要么检查数据增强是不是太弱。另一个要盯的是特征范数和类中心距离。用 ArcFace 时如果类中心范数异常增大说明 margin 参数设得太激进模型在强行拉开类间距离可能导致训练不稳定。我一般把 margin 从 0.2 开始试稳定后再往上加。5. 避坑指南五个让我重跑训练的血泪教训这一章记录的是我在用 CASIA WebFace 过程中真实踩过的坑。每个坑都对应一个具体的现象、原因和解决方式希望能帮你省下几天的 GPU 时间。5.1 解压后文件名乱码现象解压后部分文件夹名或文件名显示为乱码程序读取时报FileNotFoundError。原因压缩包在打包时用了非 UTF-8 编码的文件名在中文 Windows 系统上解压时编码解析错误。解决用 7-Zip 或 Bandizip 解压在解压设置里指定编码为 GBK 或 UTF-8。如果已经解压乱了用 Python 的os.rename批量重命名把乱码字符替换成合法 ASCII 字符。更稳妥的做法是在 Linux 环境下解压默认 UTF-8 编码不会出问题。5.2 图像损坏导致训练中途崩溃现象训练跑了几千步突然报PIL.UnidentifiedImageError或cv2.imread返回None整个进程挂掉。原因爬取的数据集里混入了少量损坏的 JPEG 文件可能是下载中断或存储损坏。解决在 Dataset 的__getitem__里加异常捕获遇到读不出来的图就返回一个全零张量并跳过。更彻底的做法是预处理阶段跑一遍完整性检查把所有损坏文件列出来删掉。from PIL import Image import os def check_images(root): bad [] for dirpath, _, filenames in os.walk(root): for fname in filenames: fpath os.path.join(dirpath, fname) try: with Image.open(fpath) as im: im.verify() except Exception: bad.append(fpath) return badImage.verify()不实际解码图像只检查文件头速度快。但注意 verify 之后图像对象不能再用需要重新打开。跑完这个脚本把bad列表里的文件删掉或移走。5.3 类别编号不连续导致标签错位现象用ImageFolder加载后类别数和实际身份数对不上或者标签映射混乱。原因CASIA WebFace 的身份编号有跳号ImageFolder按文件夹名排序后重新编号跳号被忽略。如果你自己写标签映射时假设了连续编号就会错位。解决始终用ImageFolder.classes或dataset.class_to_idx获取实际映射关系不要手动构造标签数组。保存模型时把class_to_idx一起存下来推理时才能正确对应。5.4 数据增强过猛导致身份信息丢失现象训练 loss 震荡不收敛验证集 TAR 始终在低位徘徊。原因随机裁剪或随机擦除的幅度太大把眼睛、鼻子等关键身份特征裁掉了网络学到的是噪声。解决把RandomErasing的scale上限从 0.4 降到 0.15随机裁剪的比例控制在 0.8 到 1.0 之间即最多裁掉 20% 边缘。如果用了RandomRotation角度限制在 ±10 度以内。人脸识别对几何变换比通用分类敏感得多。5.5 验证集身份混入训练集现象验证集 TAR 高得离谱99%但实际部署时效果很差。原因划分时按图片随机切分同一个人的图既在训练集又在验证集模型只是记住了这张脸不是学会了区分。解决严格按身份划分。写一个划分脚本先os.listdir拿到所有身份列表random.shuffle后按比例切分再把对应文件夹整体移动到train和val目录。移动而不是复制避免后续误用。6. 进阶技巧用子集快速验证与特征可视化完整跑一遍 CASIA WebFace 训练即使用 8 卡 V100 也要大半天。如果你只是想验证一个新损失函数或新网络结构是否有效没必要一上来就全量跑。这一章分享两个提速技巧和一个排查手段。6.1 构建分层子集2000 身份、10 万张图从 1 万身份里随机抽 2000 个每个身份最多取 50 张图构成约 10 万张的子集。这个规模在单卡 3090 上跑 ArcFace 大约 2 小时能收敛到可比较的水平。如果子集上 TAR 有明显提升再上全量验证如果子集上都看不出差异全量大概率也白跑。import os import random import shutil def build_subset(src_root, dst_root, num_ids2000, max_per_id50, seed42): random.seed(seed) identities [d for d in os.listdir(src_root) if os.path.isdir(os.path.join(src_root, d))] random.shuffle(identities) selected identities[:num_ids] for idx, identity in enumerate(selected): src_dir os.path.join(src_root, identity) dst_dir os.path.join(dst_root, f{idx:05d}) os.makedirs(dst_dir, exist_okTrue) imgs os.listdir(src_dir) random.shuffle(imgs) for img in imgs[:max_per_id]: shutil.copy(os.path.join(src_dir, img), os.path.join(dst_dir, img)) print(f子集构建完成: {len(selected)} 个身份)num_ids控制身份数max_per_id控制每个身份最多取多少张。seed固定随机种子保证可复现。复制而不是移动保留原始数据集完整。子集目录用连续编号重命名避免原编号跳号带来的困扰。6.2 用 t-SNE 看特征聚类效果训练完之后怎么直观判断模型学到的特征好不好t-SNE 是个趁手的工具。从验证集里抽 20 个身份、每个身份 30 张图提取特征后用 t-SNE 降到二维画散点图。好的模型应该呈现“类内紧凑、类间分离”的簇状分布如果所有点混在一起说明模型根本没学到区分性特征。import matplotlib.pyplot as plt from sklearn.manifold import TSNE def visualize_features(model, loader, device, num_classes20): model.eval() feats, labels [], [] with torch.no_grad(): for imgs, lbls in loader: imgs imgs.to(device) emb model(imgs).cpu().numpy() feats.append(emb) labels.extend(lbls.numpy()) feats np.concatenate(feats, axis0) labels np.array(labels) # 只取前 num_classes 个类别 mask labels num_classes feats, labels feats[mask], labels[mask] tsne TSNE(n_components2, perplexity30, random_state42) embedded tsne.fit_transform(feats) plt.figure(figsize(10, 8)) scatter plt.scatter(embedded[:, 0], embedded[:, 1], clabels, cmaptab20, s5) plt.colorbar(scatter) plt.title(t-SNE of Face Features) plt.savefig(tsne_features.png, dpi150)perplexity控制 t-SNE 的邻域大小30 左右适合几百到几千个样本。n_components2降到二维方便画图。颜色按标签区分如果同一颜色的点聚成一团、不同颜色之间有明显间隙说明特征质量不错。这个图我每次训练完都会看一眼比盯着 loss 曲线直观得多。6.3 一个容易忽略的细节特征归一化最后说一个我踩过的坑。用余弦相似度评估时特征必须先做 L2 归一化。如果忘了这一步相似度会被特征范数主导——范数大的样本跟谁算相似度都高评估结果完全不可信。ArcFace 训练时通常已经在损失函数里做了归一化但推理时从网络输出的原始 embedding 不一定归一化了得手动补上。从那以后我每次写评估代码第一行就是feats feats / np.linalg.norm(feats, axis1, keepdimsTrue)不管前面有没有归一化多除一次不会错。这个习惯帮我省掉了至少两次“模型明明训练得很好但评估指标诡异”的排查时间。希望帮到你。本文还有配套的精品资源点击获取