☰
CASIAwebFACE 十万级人脸数据清洗与训练实战指南
2026/10/11 17:31:32 网站建设 项目流程

简介:CASIA WebFace 完整数据集面向人脸识别方向的研究者、算法工程师与深度学习学习者,用于训练与评测人脸识别模型,解决大规模人脸数据获取困难、类别覆盖不足的问题。资源包共1个文件,为docx文档,压缩包约11KB,文档内提供百度网盘链接与提取密码,指向压缩后约4.1G的完整数据,包含1万个人物身份、约50万张人脸照片,是人脸识别领域最主流、使用最广泛的数据集之一。该数据集类别规模适中、身份标注清晰,适合用于训练卷积神经网络、验证损失函数与度量学习方案,也可作为课程实验、毕业设计及论文复现的基础数据。目前已有547人学习下载,读者可据此快速搭建训练与测试流程,省去自行采集与清洗数据的成本,将精力集中在模型结构、训练策略与识别精度优化上。

1. 从一张 10 万级人脸清单说起:CASIAwebFACE 到底解决什么问题

如果你正在做人脸识别相关的模型训练,大概率会遇到一个很尴尬的阶段:公开的小数据集(比如几千张、几百个身份)跑出来的模型,在测试集上指标好看得离谱,一换到真实场景就原形毕露。原因不复杂——身份数太少,模型学到的是「记住这几百个人」,而不是「区分不同的人」。CASIAwebFACE 这类十万级身份、百万级图像的人脸数据集,就是用来跨过这道坎的。

它最核心的价值有三个:身份规模大(十万量级 ID)、图像来自互联网自然场景(姿态、光照、遮挡、年龄跨度都比实验室摆拍数据集更野)、以及配套的清洗与对齐流程相对成熟。适合谁?适合已经跑通小数据集、想验证「数据规模对识别率影响」的算法工程师,也适合做人脸特征提取、度量学习(ArcFace、CosFace 这类损失函数)的研究者。但要注意,它不是拿来即用的「干净数据集」,脏数据、错标、重复人脸的比例不低,清洗这一步决定了你后面模型的上限。

2. 数据集结构拆解与清洗:为什么直接开训大概率翻车

拿到 CASIAwebFACE 之后,很多人的第一反应是直接写个 DataLoader 开跑。我见过不止一个团队这么干,结果训练 loss 震荡、验证集准确率卡在某个值上不去,排查半天发现是数据本身的问题。这一章先把「里面到底有什么」和「怎么把它变成能训的东西」讲清楚。

2.1 目录结构与命名规则:先看懂再动手

CASIAwebFACE 的典型组织方式是「按身份分目录」,每个身份一个文件夹,文件夹名就是该身份的 ID(通常是数字编号),里面放这个人的若干张照片。文件名一般是「ID_序号.jpg」这种格式。这种结构对分类任务很友好,因为可以直接用文件夹名当 label。

但有几个细节必须提前确认,否则后面会踩坑:

检查项常见情况影响
身份目录数量十万级决定分类头输出维度
每身份图像数分布极不均匀,有的几十张,有的只有 1-2 张长尾问题,影响采样策略
图像格式以 jpg 为主,偶有 png、bmp 混入DataLoader 报错
图像尺寸不统一,长边从几十到上千像素必须统一 resize
是否已对齐部分版本已做 5 点对齐,部分没有影响是否要自己跑对齐

我一般会先写个脚本统计一遍,别嫌麻烦,这一步能省掉后面几小时的 debug。

import os from collections import Counter from PIL import Image root = "/path/to/casia_webface" id_dirs = [d for d in os.listdir(root) if os.path.isdir(os.path.join(root, d))] print(f"身份总数: {len(id_dirs)}") counts = [] bad_files = [] for d in id_dirs: files = os.listdir(os.path.join(root, d)) counts.append(len(files)) for f in files: p = os.path.join(root, d, f) try: img = Image.open(p) img.verify() # 校验文件是否损坏 except Exception as e: bad_files.append((p, str(e))) print(f"每身份图像数: min={min(counts)}, max={max(counts)}, mean={sum(counts)/len(counts):.1f}") print(f"损坏文件数: {len(bad_files)}") # 统计长尾:图像数少于 5 的身份占比 few = sum(1 for c in counts if c < 5) print(f"图像数<5的身份占比: {few/len(counts)*100:.1f}%")

这段脚本做了三件事:统计身份总数、统计每身份图像数分布、校验文件完整性。img.verify()只能检测文件头是否损坏,不能检测图像内容是否正常,所以后面还需要人工抽检。参数上,root换成你自己的路径即可。跑完之后你会对「长尾有多严重」有个直观认识——通常图像数少于 5 的身份能占到 20% 以上,这些身份在训练时要么被过采样,要么直接过滤掉。

2.2 清洗三件套:去重、去脏、去错标

清洗是 CASIAwebFACE 落地里最耗人力的一步,但也是最不能省的一步。我一般按「去重 → 去脏 → 去错标」的顺序来。

去重:互联网来源的数据集,同一个人的同一张照片可能被爬了多次,甚至不同身份目录下出现同一张脸。用感知哈希(pHash)做近似去重比较稳。

import imagehash from PIL import Image import os def build_hash_index(root, hash_size=8): index = {} for d in os.listdir(root): dpath = os.path.join(root, d) if not os.path.isdir(dpath): continue for f in os.listdir(dpath): p = os.path.join(dpath, f) try: h = imagehash.phash(Image.open(p), hash_size=hash_size) index[p] = h except Exception: continue return index def find_duplicates(index, threshold=5): items = list(index.items()) dups = [] for i in range(len(items)): for j in range(i+1, len(items)): if items[i][1] - items[j][1] <= threshold: dups.append((items[i][0], items[j][0])) return dups

hash_size=8表示生成 64 位哈希,threshold=5是汉明距离阈值,越小越严格。注意这个双重循环在十万级数据上会非常慢,实际用的时候要先用 BK-tree 或者分桶优化,这里只是示意逻辑。去重之后,同一身份内的重复图删掉,跨身份的重复图要人工确认——有可能是错标,也有可能是双胞胎这类特殊情况。

去脏:包括损坏文件、纯色图、极端模糊图。损坏文件上面已经筛过;纯色图可以用像素方差判断;模糊图用拉普拉斯算子方差。

import cv2 import numpy as np def is_blurry(img_path, threshold=100): img = cv2.imread(img_path, cv2.IMREAD_GRAYSCALE) if img is None: return True return cv2.Laplacian(img, cv2.CV_64F).var() < threshold def is_solid_color(img_path, var_threshold=10): img = cv2.imread(img_path) if img is None: return True return img.reshape(-1, 3).var(axis=0).mean() < var_threshold

threshold=100是模糊判断的经验值,实际要根据你的图像分辨率调整——分辨率越高,拉普拉斯方差普遍越大。var_threshold=10是纯色判断,一般纯色图的像素方差会接近 0。

去错标:这一步最难自动化。常见做法是用一个预训练的人脸识别模型提取特征,对同一身份目录内的图像做聚类,如果某张图跟同目录其他图的余弦相似度明显偏低,就标记为疑似错标,人工复核。这一步没有捷径,但能显著提升数据质量。

2.3 对齐与统一尺寸:别让姿态成为模型的负担

CASIAwebFACE 里相当一部分图是自然场景,人脸角度、尺度差异很大。如果不做对齐直接 resize 到 112x112,模型要花大量容量去学「怎么把人脸摆正」,而不是「怎么区分不同的人」。常见做法是用 5 点关键点(双眼、鼻尖、双嘴角)做相似变换对齐。

import cv2 import numpy as np # 目标 112x112 的标准 5 点位置(ArcFace 常用) REF_POINTS = np.array([ [38.2946, 51.6963], # 左眼 [73.5318, 51.5014], # 右眼 [56.0252, 71.7366], # 鼻尖 [41.5493, 92.3655], # 左嘴角 [70.7299, 92.2041], # 右嘴角 ], dtype=np.float32) def align_face(img, landmarks): # landmarks: 5x2 的检测关键点 tform = cv2.estimateAffinePartial2D(landmarks, REF_POINTS)[0] aligned = cv2.warpAffine(img, tform, (112, 112), borderValue=0.0) return aligned

REF_POINTS是 ArcFace 论文里常用的标准位置,estimateAffinePartial2D估计的是相似变换(旋转+缩放+平移),不含剪切,能保持人脸比例。对齐之后统一到 112x112,这是目前主流人脸识别模型的输入尺寸。如果你的模型输入是 128 或 160,把REF_POINTS按比例缩放即可。

提示:对齐用的关键点检测器本身也有误差,如果检测失败(比如侧脸太极端),建议直接丢弃该图,不要强行对齐,否则会引入噪声。

3. 从清洗后数据到可训练集:划分、采样与增强的取舍

清洗完只是第一步,接下来要把它变成一个「能稳定训练」的数据集。这一章讲划分策略、长尾采样和增强参数,都是直接影响最终识别率的环节。

3.1 训练验证划分:按身份切,不能按图像切

这是最容易被忽视的坑。人脸识别任务里,验证集的作用是评估「模型对没见过的身份能不能区分」,所以验证集的身份必须和训练集完全不重叠。如果你按图像随机切,同一个人的照片同时出现在训练和验证里,验证准确率会虚高十几个点,上线就翻车。

import os import random def split_by_identity(root, val_ratio=0.02, seed=42): random.seed(seed) ids = [d for d in os.listdir(root) if os.path.isdir(os.path.join(root, d))] random.shuffle(ids) n_val = int(len(ids) * val_ratio) val_ids = set(ids[:n_val]) train_ids = set(ids[n_val:]) return train_ids, val_ids

val_ratio=0.02是常见取值,十万身份里拿两千个做验证,足够评估。seed固定住,保证每次划分一致,方便复现。注意验证集的身份数不能太少,否则指标波动大;也不能太多,否则训练身份不够。

3.2 长尾采样:让稀有身份也有话语权

前面统计过,图像数少于 5 的身份占比不低。如果按图像均匀采样,这些身份几乎不会被抽到,模型对它们完全没有区分能力。常见做法有两种:一是按身份做类别平衡采样(每个身份被抽到的概率相近),二是对稀有身份做过采样。

import numpy as np from torch.utils.data import WeightedRandomSampler def build_balanced_sampler(labels, num_samples=None): labels = np.array(labels) class_counts = np.bincount(labels) # 每个样本的权重 = 1 / 该类样本数 weights = 1.0 / class_counts[labels] if num_samples is None: num_samples = len(labels) sampler = WeightedRandomSampler(weights, num_samples, replacement=True) return sampler

weights是每个样本被抽到的相对概率,稀有类样本权重大,被抽到的机会就多。replacement=True表示有放回采样,这样才能实现过采样。num_samples一般设成训练集总样本数,保证一个 epoch 的步数不变。这个策略的代价是稀有身份会被反复看到,容易过拟合,所以通常配合较强的数据增强一起用。

3.3 增强参数:人脸识别里哪些增强有用,哪些是负优化

数据增强在人脸识别里不是越多越好。有些增强(比如水平翻转)几乎无脑可用,有些(比如大角度旋转、强色彩抖动)反而会破坏人脸结构,让模型学到错误的特征。

增强方式推荐参数说明
水平翻转p=0.5人脸近似对称,安全
随机裁剪scale=(0.9, 1.0)轻微裁剪,模拟框偏差
亮度/对比度0.8~1.2模拟光照变化,幅度别太大
高斯模糊p=0.1, kernel 3~5模拟低质量图,别过度
大角度旋转不推荐破坏关键点结构
强色彩抖动不推荐肤色是重要特征,别乱改
import torchvision.transforms as T train_tf = T.Compose([ T.RandomHorizontalFlip(p=0.5), T.RandomResizedCrop(112, scale=(0.9, 1.0), ratio=(0.95, 1.05)), T.ColorJitter(brightness=0.2, contrast=0.2), T.RandomApply([T.GaussianBlur(3, sigma=(0.1, 1.0))], p=0.1), T.ToTensor(), T.Normalize(mean=[0.5, 0.5, 0.5], std=[0.5, 0.5, 0.5]), ])

RandomResizedCrop的scale下限设 0.9,是为了避免裁掉太多人脸区域。ColorJitter只调亮度和对比度,不动色相和饱和度,因为肤色是区分身份的重要线索。GaussianBlur概率设 0.1,只是偶尔模拟低质量输入,不能常态化。

4. 训练配置与验证:CASIAwebFACE 上跑 ArcFace 的关键参数

数据准备好了,接下来是训练。这一章以 ArcFace 为例,讲清楚在 CASIAwebFACE 这种十万级身份数据集上,哪些参数是敏感的、哪些可以照抄。

4.1 分类头与损失函数:十万类怎么设

CASIAwebFACE 十万身份意味着分类头输出维度是十万级,这是一个巨大的全连接层。以 512 维特征为例,权重矩阵是 512x100000,参数量五千万左右,显存占用不小。常见做法是特征维度用 512,分类头用 ArcFace 加 margin。

import torch import torch.nn as nn import math class ArcFaceHead(nn.Module): def __init__(self, in_features, num_classes, s=64.0, m=0.5): super().__init__() self.s = s self.m = m self.weight = nn.Parameter(torch.FloatTensor(num_classes, in_features)) nn.init.xavier_uniform_(self.weight) def forward(self, x, labels): # 归一化特征和权重 x = nn.functional.normalize(x) w = nn.functional.normalize(self.weight) cos_theta = torch.mm(x, w.t()).clamp(-1+1e-7, 1-1e-7) theta = torch.acos(cos_theta) # 只在目标类上加 margin target_logit = torch.cos(theta - self.m) one_hot = torch.zeros_like(cos_theta) one_hot.scatter_(1, labels.view(-1, 1), 1) logits = one_hot * target_logit + (1 - one_hot) * cos_theta return logits * self.s

s=64.0是缩放因子,m=0.5是角度 margin,这两个是 ArcFace 的经典取值。s太小会导致 loss 太小、梯度不足;太大容易训练不稳定。m越大类间区分越强,但太大(比如 0.7 以上)会导致训练初期难以收敛。十万类场景下,m=0.5是比较稳的起点。

4.2 学习率与 batch size:大身份数下的调参经验

十万类分类任务,batch size 不能太小,否则每个 batch 覆盖的类别太少,梯度噪声大。常见配置是单卡 batch size 128 到 256,多卡线性放大。学习率用 warmup + cosine 衰减。

from torch.optim.lr_scheduler import CosineAnnealingLR, LambdaLR def build_scheduler(optimizer, warmup_epochs, total_epochs, base_lr): def lr_lambda(epoch): if epoch < warmup_epochs: return (epoch + 1) / warmup_epochs progress = (epoch - warmup_epochs) / (total_epochs - warmup_epochs) return 0.5 * (1 + math.cos(math.pi * progress)) return LambdaLR(optimizer, lr_lambda)

warmup_epochs一般设 1 到 2 个 epoch,让模型先「热身」,避免初期大梯度破坏权重。base_lr在单卡 batch 256 下常用 0.1,多卡按 batch 倍数放大。cosine 衰减让学习率平滑降到接近 0,训练后期更稳定。

4.3 验证指标:为什么 top-1 准确率不够用

人脸识别的验证不能只看分类准确率,因为分类头在验证集身份上根本没训练过。真正有意义的是「特征提取 + 比对」的指标:在验证集上两两配对,算同一人相似度和不同人相似度的分布,看 TAR@FAR(比如 FAR=1e-4 时的通过率)。

def evaluate_verification(model, val_loader, threshold=0.5): model.eval() feats, labels = [], [] with torch.no_grad(): for imgs, lbls in val_loader: out = model(imgs.cuda()) feats.append(nn.functional.normalize(out).cpu()) labels.append(lbls) feats = torch.cat(feats) labels = torch.cat(labels) # 计算两两余弦相似度 sim = feats @ feats.t() # 同人/不同人掩码 same = (labels[:, None] == labels[None, :]) pos = sim[same].numpy() neg = sim[~same].numpy() # 给定阈值算 TAR/FAR tar = (pos > threshold).mean() far = (neg > threshold).mean() return tar, far

threshold是判定「同一人」的相似度阈值,实际评估时会扫一遍阈值画 ROC,取 FAR=1e-4 或 1e-5 时的 TAR 作为核心指标。这个指标比分类准确率更能反映模型在真实比对场景下的能力。

5. 避坑与排查:CASIAwebFACE 落地时最容易翻车的 5 个点

这一章是我自己和身边同行踩过的坑,按「现象 → 原因 → 解决」整理,希望能帮你省点时间。

坑一:训练 loss 正常下降,但验证 TAR 一直不涨。现象是训练集分类准确率能到 99%,验证集比对指标却卡在很低的水平。原因通常是验证集身份和训练集身份有重叠,或者验证集里混入了训练集的图。解决方法是严格按身份划分,划分后写个脚本校验两个集合的身份交集为空。

坑二:显存爆了,但 batch size 已经调到很小。十万类分类头的参数量和梯度占显存很大。原因是分类头权重矩阵太大,加上优化器状态(比如 Adam 的动量)会翻倍。解决方法是分类头用 SGD 或者对分类头做梯度累积,或者用混合精度训练。混合精度能把显存占用降接近一半,是性价比最高的手段。

坑三:某些身份的图像全部被对齐模块丢弃,导致这些身份在训练集里消失。现象是训练时发现实际类别数比预期少。原因是关键点检测器对极端侧脸、遮挡脸检测失败,对齐脚本直接跳过。解决方法是统计丢弃率,如果超过 5%,要么换更鲁棒的关键点检测器,要么对这些图做兜底处理(比如中心裁剪后 resize)。

坑四:数据增强开了旋转,模型对正脸识别反而变差。现象是加了 RandomRotation 之后,验证指标不升反降。原因是大角度旋转破坏了人脸的关键结构,模型被迫学习旋转不变性,但这部分容量本可以用来学身份特征。解决方法是去掉大角度旋转,只保留水平翻转和轻微裁剪。

坑五:多卡训练时验证指标比单卡差。现象是同样的配置,多卡训练出来的模型验证 TAR 明显低于单卡。原因通常是多卡下 batch size 变大,学习率没同步调整,或者 BatchNorm 在多卡下的统计量不同步。解决方法是学习率按 batch 倍数线性放大,BatchNorm 换成 SyncBN,或者干脆用 GroupNorm。

注意:清洗和对齐是一次性投入,但收益贯穿整个训练周期。我见过太多团队为了赶进度跳过清洗,结果后面花几倍时间调参都补不回来。

6. 进阶技巧:用子集快速验证想法,再上全量

全量 CASIAwebFACE 训练一轮动辄几十小时,调参成本很高。我的习惯是先用一个「子集」快速验证想法,确认有效再上全量。具体做法是按身份采样,比如抽 5000 个身份、每个身份最多 20 张图,构成一个约十万张图的子集。这个规模在单卡上几小时能跑完一轮,足够看出趋势。

子集验证时要注意两点:一是子集的身份分布要尽量接近全量(长尾比例一致),否则结论不可迁移;二是子集的验证集要单独从全量验证集里抽,不能和子集训练身份重叠。我一般会固定一个「快速验证子集」和对应的验证集,所有新想法先在这个子集上跑,指标提升超过 1 个点才考虑上全量。

另一个技巧是「分阶段训练」:先用子集训练一个基础模型,再用全量数据 fine-tune。基础模型学到的特征提取能力可以迁移,fine-tune 阶段只需要调整分类头和部分高层特征,收敛更快。这个策略在身份数从几千扩到十万时特别有效,能省下不少时间。

def build_subset(root, out_root, num_ids=5000, max_per_id=20, seed=42): import shutil, random random.seed(seed) ids = [d for d in os.listdir(root) if os.path.isdir(os.path.join(root, d))] random.shuffle(ids) selected = ids[:num_ids] for d in selected: src = os.path.join(root, d) dst = os.path.join(out_root, d) os.makedirs(dst, exist_ok=True) files = os.listdir(src)[:max_per_id] for f in files: shutil.copy(os.path.join(src, f), os.path.join(dst, f))

num_ids=5000和max_per_id=20是我常用的快速验证配置,十万张图左右,单卡几小时一轮。seed固定保证子集可复现。这个子集不是用来出最终模型的,只是用来筛想法——指标涨了才值得上全量,不涨就果断放弃,省下的时间比什么都值钱。

最后说个我自己的教训:早期我总想一步到位,直接在全量上试各种损失函数和增强组合,结果一轮训练等一天,试错成本高到根本不敢多试。后来改成子集快速筛、全量精调,同样的时间能试的想法多了好几倍。数据集的规模是优势,但别让它变成你迭代速度的负担。希望帮到你。

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询