简介:人脸识别算法开发与评估离不开标准化数据集的支持。这份合集整合了AR、ORL、Yale、YaleB、FERET、PIE六类经典人脸数据库的mat预处理版本,面向从事人脸识别、表情识别与光照鲁棒性研究的学生和算法工程师,可直接用于模型训练、跨库对比和性能验证。压缩包共1250个文件,以mat矩阵数据为主,另有2个m脚本用于标签随机排列和数据读取,统一处理为32×32、64×64等常用尺寸,整个压缩包约38.77MB,文件按库划分,便于按需取用和定向研究。目前已有2422人学习下载。与零散下载原始图像相比,这份数据省去了格式转换与尺寸归一化的重复工作,配合脚本能快速完成样本划分和标签处理,适合快速搭建实验基线、复现论文结果,也可作为课程设计与算法评测的标准化测试集。
1. 从 ORL 上 98% 准确率到现场失灵:人脸识别数据库不能乱选
有一次我在 ORL 库上训练一个人脸识别模型,测试集也从 ORL 里留出,最终准确率刷到了 98%,当时觉得这个项目稳了。结果换到真实办公室环境做第一次现场测试就翻车,识别率掉到六成左右。后来才反应过来:ORL 是几十年前拍摄的灰度照片库,背景干净、表情幅度小、没有明显光照变化,它只能验证“算法能不能把人脸特征学出来”,根本验证不了“现场环境里的鲁棒性”。那 98% 只是数据库给的错觉,不是模型真实水平。
人脸识别圈子里,公开数据库的使用有很强的“潜规则”。AR、ORL、Yale、YaleB、FERET、PIE 这六个库是很多论文的实验标配,但每个库都有自己的脾气:有的主攻光照,有的主攻遮挡,有的主攻姿态,有的自带时间跨度。混在一起用、或者选错了库,得出的结论可能完全不可信。这篇笔记面向准备跑实验的工程师和学生,讲清楚六个库到底能验证什么、怎么把六套格式不同的数据整理成一套、训练前有哪些坑。读完你至少不会在 ORL 上刷完 98% 就急着上线。
2. 六大数据库的“人设”与分工:AR、ORL、Yale、YaleB、FERET、PIE 到底能验证什么
做实验前先搞清楚一件事:公开数据库不是用来堆数据的,而是用来制造“可控的困难”。每个库在采集时只动了少数几个变量,其他变量尽量保持不变。这样你才能定位模型短板到底出在光照、遮挡、姿态还是表情上。下面这张表把六个库的核心差异列出来,方便对照选型。
| 数据库 | 核心变化量 | 样本特征 | 常被用来验证什么 |
|---|---|---|---|
| AR | 表情、遮挡(墨镜/围巾)、两次拍摄间隔 | 彩色图像,含大量带遮挡样本,部分带时间跨度 | 遮挡鲁棒性、局部特征、跨时间稳定性 |
| ORL | 简单表情与轻微姿态变化 | 灰度图,背景固定,结构简单,人数不多 | 算法流程冒烟测试、基础特征提取 |
| Yale | 表情、是否戴眼镜、少量光照 | 灰度图,小规模 | 小样本对比、表情变化、配饰干扰 |
| YaleB | 极端光照变化 | 灰度图,同一个人在多种光照下拍摄 | 光照归一化、低光鲁棒性 |
| FERET | 姿态、重复采集、时间跨度 | 灰度图,带多时段回访样本 | 跨姿态识别、跨时间老化测试 |
| PIE | 姿态、光照、表情多因素组合 | 灰度图,控制变量组合齐全 | 多因素交叉分析、子集对比 |
2.1 一眼看清六个库的差异:选错库等于白训
ORL 是典型的“入门库”,总量小、环境干净,跑一遍全流程几分钟就能结束。我习惯用它做冒烟测试,也就是确认代码没写错、loss 能降、模型不会是随机初始化状态。但 ORL 的问题也在这里:它太干净了,图像里的人基本正脸、背景单一、没有大尺度旋转和强光干扰。如果你只在这上面调参,模型学到的其实是“在固定背景下找脸”,而不是“识别人”。
Yale 和 YaleB 经常被弄混。Yale 是小规模表情/配饰库,适合做快速算法对比;YaleB 则是极端光照库,同一个人的图像从亮到暗覆盖很大范围。YaleB 的价值是逼你把光照归一化做好,如果直接拿原图去训练,网络很可能学到“亮度分布”而不是“人脸结构”。AR 库的核心卖点是遮挡,墨镜挡住眼睛、围巾挡住下半张脸,这是真实场景里最难处理的情况之一。
FERET 和 PIE 更像“工业级”库。FERET 里存在同一个人不同时间段的回访图像,适合做跨时间验证,比如门禁卡在几个月后还能不能刷过。PIE 则是把姿态、光照、表情三个变量组合起来,适合做因素拆解实验。选库不是看哪个“名气大”,而是看你真实落地场景里最怕什么变化。
2.2 为什么没有一个库是全能库:先定义场景再选库
所有模型都有自己的暗假设,这和算法无关,和数据有关。你在 ORL 上训出来的模型,暗假设是“光照不变、姿态轻微、无遮挡”;在 YaleB 上训出来的模型,暗假设是“人脸角度基本固定,但光可以很暗”。真实世界不会按照某个库的暗假设来运作,所以你必须先定义清楚:做出来的东西到底要扛哪种变化。
我做选型时一般分四步。第一步,列出目标场景里的主要干扰项,比如闸机场景主要考虑光照、遮挡、戴帽;第二步,找主攻这个干扰项的库,光照问题优先 YaleB,遮挡问题优先 AR,姿态问题优先 FERET 和 PIE;第三步,用另外一到两个库做交叉验证,比如用 AR 训练、用 PIE 测试,看模型能不能迁移;第四步,无论如何都要留一小部分自己拍摄的现场数据做最终验收。前两步解决“怎么训练”,第三步解决“是否过拟合”,第四步解决“是否真的能上线”。
有一种常见误用是“六个库全混在一起训”。表面看数据量变大了,实际因为六个库的人脸风格、图像质量、标签体系不一致,模型会花大量参数去适应数据源差异。更合理的做法是保留数据源的维度,训练时按源做加权或分组,测试时按源分别统计。否则你只得到一个平均的准确率,看不出来模型到底在哪个维度上崩。
2.3 这些库的“隐藏分支”:子集用对了是宝,用错了是坑
六个库都不是一个整块。YaleB 在官方整理时通常会按光照强度分成多个子集,有的子集图像暗到人眼都难识别,有的子集接近正常亮度。如果你不加区分地把所有子集混进训练集,模型会被极端暗样本带偏;反过来,单独拿最暗子集当测试集,才是检验光照鲁棒性的正确姿势。FERET 也存在多种采集批次,不同批次间隔了几个月,这正是做“跨时间识别”的天然测试集。
AR 库里的第二次拍摄批次是容易被忽略的宝藏。同一批人在不同时间回到采集环境重新拍摄,表情和遮挡不完全一样,这相当于一个迷你型的时间漂移测试。很多人只把 AR 当遮挡库用,丢了这批数据有点可惜。PIE 的结构更像实验矩阵,姿态、光照、表情分别有不同档位,你可以只取某些档位组合,构造出“正面+强光”“侧面+正常光”这类针对性测试集。
使用子集时我习惯先读随包附带的结构说明,再把子集目录名记录在实验笔记里。很多坑不是在训练时爆的,而是在论文复现或团队协作时爆的:对方说“用了 YaleB”,其实是用了 YaleB 的某一个子集,结果完全不同。给数据目录打上明确的子集标记,看起来麻烦,实际上是在给自己留后悔药。
3. 把六个数据包整理成统一目录:下载、验货与改名
拿到六个库之后,最先遇到的问题是格式不统一。有的包解压出来是一堆零散文件,有的是子目录套子目录,文件名规则也千奇百怪。如果直接在这些原始目录上写预处理脚本,脚本会被各种边界条件撑得越来越复杂。我的习惯是先花小半天把六个库整理成同一套结构,后面所有训练代码只面向一种目录规范。
3.1 下载之后先验货再解压:避免解压到一半发现坏包
公开数据库通常通过官方申请渠道下发,拿到的一般是压缩包。第一步不是急着解压,而是先校验文件完整性。网络传输偶尔会丢数据,压缩包表面上能解开但里面的图片可能已经损坏,这类问题在训练时很难定位,通常表现为某个样本读取失败或者像素值异常。校验用 md5 是最直接的做法。
mkdir -p D:/face_db/raw && cd D:/face_db/raw # 把六个压缩包放在同一个目录,生成校验值清单 md5sum *.zip > checksum.txt # 查看清单,和发布方提供的摘要逐一对比 cat checksum.txt # 解压前看一眼整体体积,避免解压途中磁盘写满 du -sh .参数说明:md5sum对每个文件生成 128 位哈希值,如果两个文件内容完全相同,哈希值一定相同;任何一位变化,哈希值都会变。生成checksum.txt后不要删,后续怀疑某个图像损坏时还能回来校验。du -sh是显示当前目录总占用空间,-s表示只汇总、-h以人可读的兆/吉字节显示。校验发现错误时,只重新下载对应的包,不要把已经解压的全删了重来。
提示:有些包的压缩格式是老式
.Z或.sit,Windows 自带解压可能不支持,建议统一用开源压缩工具处理。遇到未知格式先file 包名看真实类型,不要靠后缀名猜。
3.2 按“库/人员/图像”三层结构落盘:解除层层嵌套的目录地狱
六个包解压后的层级差异很大,有的第一层就是图片,有的中间还夹着好几层分类目录。直接面对这种结构写代码,路径规则根本没法收敛。我一般会先建立一层固定的中间目录,把每个库的解压结果单独放进去,再写个脚本扫描所有图片文件,把图片移动到目标目录。
# 建立统一工作目录,processed 是整理后的目标根目录 mkdir -p D:/face_db/processed/{AR,ORL,Yale,YaleB,FERET,PIE} # 把六个包分别解压到对应目录,这里用关键字匹配做分发 for z in D:/face_db/raw/*.zip; do case "$z" in *AR*) unzip -q "$z" -d D:/face_db/processed/AR ;; *ORL*) unzip -q "$z" -d D:/face_db/processed/ORL ;; *YaleB*) unzip -q "$z" -d D:/face_db/processed/YaleB ;; *Yale*) unzip -q "$z" -d D:/face_db/processed/Yale ;; *FERET*) unzip -q "$z" -d D:/face_db/processed/FERET ;; *PIE*) unzip -q "$z" -d D:/face_db/processed/PIE ;; esac done参数说明:unzip -q中的-q是 quiet 模式,减少解压过程中的大量文件名输出,否则六个库解压完终端会被刷屏。-d指定解压目标目录。注意case判断里*Yale*要放在*YaleB*后面,否则 YaleB 的包会被误配到 Yale 目录。这一步的要点是让六个库在物理目录上先分离,后面扫描处理时不需要再根据文件名猜来源。
3.3 命名映射与统一标签:把“人”的代号变成数字 ID
解压完成后,下一件事是替每个库里的人建立统一身份。不同库对文件名的编码方式差异很大:有的用目录名代表人,有的用文件名里的数字段,还有的用几位字母缩写。我一般先用一个脚本按图片文件名中的数字做初筛,生成“原始人员代号 → 数字 ID”的映射表,再和官方文档里的身份清单核对。
# 扫描所有图片文件,按“库/文件名中的数字”生成初步人员 ID find D:/face_db/processed -type f \ \( -name '*.pgm' -o -name '*.ppm' -o -name '*.bmp' -o -name '*.jpg' \) | while read img; do fname=$(basename "$img") # 提取文件名中的第一段连续数字作为初步人员 ID pid=$(echo "$fname" | sed -n 's/.*\([0-9][0-9]*\).*/\1/p' | head -1) echo "$img -> $pid" done > D:/face_db/raw/id_mapping_preview.txt命令说明:sed -n 's/.*\([0-9][0-9]*\).*/\1/p'的作用是从文件名里提取第一段连续数字,head -1只取第一个匹配结果,防止文件名里有多个数字段时选错。这个脚本只用于快速预览,不能当作最终标签。原因是不同库的数字含义不一致,有的数字是人员 ID,有的数字是拍摄序号,有的数字是姿态代码,必须对照每个库自带的身份说明逐条核对。标签错位的后果比图像损坏更隐蔽,模型照样能收敛,但准确率永远上不去,而且很难排查。
核对完成后,我会把人员 ID 统一成从 0 开始的连续整数,因为分类网络的输出维度就是人数。跨库训练时每个库的人数不同,还要在 ID 前加上库名前缀,比如AR_002、FERET_037,防止不同库的 002 撞车。这个统一标签表建议存成 CSV,字段包括original_filename、source_lib、raw_person_code、global_label,后续所有脚本都从这张表读标签,而不是再从文件名里猜。
4. 预处理实战:把六套图转成一套能直接喂模型的数据
整理好目录和标签之后,离训练还差一步:把图片变成张量。这一步看着简单,实际藏着不少选择。统一尺寸、灰度还是彩色、裁剪方式、归一化和增强的顺序,都会影响最终效果。我下面的做法不是唯一答案,但在六个库上都能跑稳,可以当作默认起点。
4.1 统一尺寸与裁剪策略:别让图像的长宽比坑了模型
六个库的图像原始尺寸差异较大,有的接近正方形,有的明显偏宽。如果直接resize到固定尺寸,人脸比例会被拉伸变形,模型学到的特征就被扭曲了。正确做法是先按人脸区域做中心裁剪,裁剪成正方形,再缩放。虽然有些库提供了人脸标注框,但大多数情况下中心裁剪已经够用,因为采集时人脸基本位于画面中央。
from PIL import Image import numpy as np IMG_SIZE = (112, 112) def load_face(path, resize=IMG_SIZE, normalize=False): """读取图片并统一为 112x112 灰度图""" img = Image.open(path).convert('L') w, h = img.size # 取短边作为边长,中心裁剪成正方形,避免人脸被拉伸 side = min(w, h) left = (w - side) // 2 top = (h - side) // 2 img = img.crop((left, top, left + side, top + side)) # LANCZOS 重采样对缩小图像保留更多细节 img = img.resize(resize, Image.Resampling.LANCZOS) arr = np.asarray(img, dtype=np.float32) / 255.0 return (arr - 0.5) / 0.5 if normalize else arr逻辑说明:convert('L')把彩色图转成灰度,减少单通道输入对网络参数的压力。中心裁剪用min(w, h)确定边长,保证裁剪区域在图像范围内。Image.Resampling.LANCZOS是 Pillow 的高质量重采样滤波器,图像缩小时比默认的BILINEAR更锐利。/ 255.0把像素值从 0 到 255 映射到 0 到 1;normalize=True时进一步把数值归一到 -1 到 1 之间,让网络输入接近零均值分布,收敛更快。
如果后续要做更精细的人脸对齐,可以用开源的人脸检测模型先检测关键点,再做仿射变换。但六个库里有些图像光照很暗,检测模型不一定都能命中,这时候中心裁剪反而是最稳定的兜底方案。我通常先跑中心裁剪,确认 baseline 能训练起来后,再考虑是否加关键点对齐。
4.2 建立索引文件:路径、标签、数据源三权分离
训练脚本不应该直接遍历目录树来获取样本,应该先扫一遍目录树,生成一个索引文件,训练时只读索引文件。这样做的好处是:数据预处理只跑一次,后续启动训练不需要重复扫描大量小文件;目录里新增或删除了图片,也不会悄悄改变训练集内容而不被发现。
import json from pathlib import Path root = Path('D:/face_db/processed') records = [] # 每个库单独扫描,保证 source 字段不会混 for lib in ['AR', 'ORL', 'Yale', 'YaleB', 'FERET', 'PIE']: lib_dir = root / lib # rglob 递归找出所有图片文件,sorted 确保顺序稳定 for img_path in sorted(lib_dir.rglob('*.*')): if img_path.suffix.lower() not in {'.pgm', '.ppm', '.bmp', '.jpg'}: continue # 人员目录名经第 3 章整理后就是人员代号 person_code = img_path.parent.name # 全局 ID 加上库前缀,避免跨库撞名 global_id = f"{lib}_{person_code}" records.append({ 'path': str(img_path), 'source': lib, 'person_code': person_code, 'global_id': global_id, 'filename': img_path.name, }) with open('D:/face_db/dataset.json', 'w', encoding='utf-8') as fp: json.dump(records, fp, ensure_ascii=False, indent=2) print(f"扫描完成,共 {len(records)} 条样本")逻辑说明:sorted(lib_dir.rglob('*.*'))对整个库的路径做字典序排序,保证每次运行生成的索引顺序一致,这个细节能避免后续调试时“上次和这次的顺序不一样”这类奇怪问题。global_id是训练时真正使用的分类标签,如YaleB_11和ORL_11不会混淆。source字段用于测试时按库统计准确率,这一步在后面做跨库体检时非常有用。
索引文件生成后,训练代码只需要json.load读取列表,再按比例切分训练集和测试集。切分时要按global_id分层,不能按样本随机切,否则同一个人的不同照片会同时出现在训练集和测试集里,模型相当于提前见过测试对象,准确率会虚高。
4.3 训练管线:归一化放在增强之前,顺序别搞反
数据增强和归一化是两个操作,很多初学代码会先做随机裁剪、翻转,再统一归一化,这在六个库组成的混合数据上会产生一个隐蔽问题:不同库的图像亮度分布差异很大,如果增强后再归一化,归一化的均值和方差会被增强后的分布污染。固定统一的口径是先归一化,再在此基础上做随机增强,保证增强操作不改变像素值的基本分布。
import json import numpy as np import torch from torch.utils.data import Dataset import torchvision.transforms.functional as F class FaceDataset(Dataset): def __init__(self, index_path, global_ids, train=True): with open(index_path, encoding='utf-8') as fp: all_samples = json.load(fp) # 只保留属于当前训练/测试集合的样本 self.samples = [s for s in all_samples if s['global_id'] in global_ids] self.train = train def __len__(self): return len(self.samples) def __getitem__(self, idx): info = self.samples[idx] # 先归一化,再增强 arr = load_face(info['path'], resize=(112, 112), normalize=True) if self.train: # 随机水平翻转,模拟左右侧脸变化 if np.random.rand() < 0.5: arr = np.fliplr(arr) # 1 到 2 像素随机平移,模拟检测框抖动 dx, dy = np.random.randint(-2, 3, size=2) arr = np.roll(arr, (dy, dx), axis=(0, 1)) # 归一化后 arr 是二维灰度图,补上 channel 维 tensor = torch.from_numpy(arr.copy()).unsqueeze(0).float() return tensor, info['global_id']逻辑说明:翻转用np.fliplr做左右镜像,注意这个操作只对水平方向有效,不能上下翻转,因为上下翻转的人脸会违背生物学常识。np.roll做平移时,从边缘滚过去的像素会绕回另一边,虽然不完美,但幅度只有 1 到 2 个像素,影响很小,换来的是对检测框偏移的鲁棒性。torch.from_numpy(arr.copy())里加copy()很重要,因为arr可能来自只读内存,后续torch操作会要求内存可写,不加会偶发莫名报错。
增强强度我习惯保守,太多会改变人脸身份信息。翻转概率设 0.5,平移范围 2 个像素,基本够用。六个库本身已经包含大量自然变化,增强只是补充,不要喧宾夺主。先跑一个最小配置验证数据管线没问题,再逐步增加增强策略,这样出了问题容易定位到底是在数据端还是在模型端。
5. 六大数据库使用避坑与排查:五个真实的翻车现场
这部分是整理六个库时最常遇见的五个坑,每条都来自实际操作里踩过的泥坑。写成“现象 → 原因 → 解决”三段,方便你对照排查。
5.1 ORL 上惊艳、AR 上翻车:模型只会认“干净脸”
现象:在 ORL 上测试准确率 98%,同一个模型放到 AR 库的测试集上,准确率直接掉到 75% 左右,而且错误集中在戴墨镜、围巾的样本上。 原因:ORL 数据太干净,所有照片都是正脸、无遮挡、光照稳定。模型学到的特征大多来自脸部中上区域和整体轮廓,一旦下半张脸被围巾遮住,或者眼睛区域被墨镜遮住,特征就断层。准确率高只是因为测试分布和训练分布高度一致,并不代表模型理解了人脸。 解决:如果目标场景存在遮挡,训练集里必须加入 AR 库对应的遮挡样本,至少要保证训练集和测试集来自同类变化。更稳妥的做法是把 ORL 当作流程验证,AR 当作泛化验证,两者分开测评。看到 ORL 的高分先别高兴,去 AR 上跑一遍再判断模型是否真的可用。
5.2 批量训练时 Shape 报错:同一个人脸图被裁剪出了不同尺寸
现象:训练循环跑到中途抛 ValueError,提示张量尺寸不一致,有时是(112, 112)和(96, 112)在 batch 拼接时爆掉。 原因:图片长宽比不同,直接 resize 到相同尺寸看似解决了,但如果代码里某一步用了自适应裁剪,比如“随机裁剪 80% 区域”,不同图片裁出来的像素尺寸就不一样。六个库中 PIE 和 FERET 的图像长宽比差异最大,最容易触发这类问题。 解决:把尺寸统一逻辑固定在一个函数里,所有路径都走这一个入口;裁剪区域必须从min(w, h)推出来,保证裁剪后是正方形;resize 的目标尺寸用常量定义,不要在不同函数里各写一遍。我习惯在load_face里打印校验尺寸,开发阶段开断言,确认所有样本输出都是(112, 112)再关掉。这个坑的本质是代码里存在多条路径,统一入口是最省心的解决方案。
5.3 标签错位:文件名排序与标签文件对不上
现象:训练 loss 能下降,但验证集的混淆矩阵对角线明显偏移,A 类人的照片经常被预测成相邻编号的人,整体准确率一直上不去。 原因:很多初学代码用os.listdir获取文件名列表,再用sorted生成标签列表,但这两个列表没有强制对应。os.listdir返回顺序在不同操作系统上不同,而标签文件可能按另一种顺序排列,两个列表叠加后标签与图片错位。 解决:不要依赖隐式顺序,建立显式索引。读图片路径时用sorted(Path.rglob(...)),同时用文件名解析出的 person_code 生成标签,标签跟着文件名走,而不是另起一个独立的排序工具。最保险的方式是第 4 章提到的 dataset.json,把 path 和 global_id 放在同一条记录里,训练时只读 index 文件,不再关心目录扫描顺序。
5.4 YaleB 图像太暗:模型学到的是“亮度阈值”而不是人脸结构
现象:在 YaleB 上训练后,把模型放到真实晚上场景,深色衣服的人经常被漏检,背景暗区反而被误报成人脸。 原因:YaleB 包含大量暗光照样本,灰度值集中在 10 到 30 之间。如果不做任何处理,网络很容易直接学到“像素值低 → 是这个人”的捷径,而不是人脸结构。真实场景的暗光背景也低像素值,自然触发误判。 解决:对 YaleB 这类极端光照库做好灰度拉伸,而不是只做简单的归一化。局部直方图均衡(CLAHE)是效果稳定且不引入太多噪声的手段。
import cv2 def clahe_stretch(img_path, clip_limit=2.0, tile_size=8): """对低照度人脸图做对比度受限直方图均衡""" img = cv2.imread(img_path, cv2.IMREAD_GRAYSCALE) clahe = cv2.createCLAHE( clipLimit=clip_limit, tileGridSize=(tile_size, tile_size) ) return clahe.apply(img)参数说明:clipLimit=2.0控制对比度增强的强度,值越大增强越明显,但过大会放大图像噪声;tileGridSize=(8, 8)把图像分成 8×8 的小块分别做直方图均衡,保留局部细节。这个操作对正常光照的图像影响很小,但能显著拉高低光样本的可辨识度。建议对 YaleB 单独走这步预处理,其他库保持原流程。
5.5 FERET 重复样本混进训练集:测试集虚假高分的元凶
现象:训练集包含 FERET 全部图像后,测试准确率 97%,但上线后被同一个人隔几个月重新拍照,识别直接失败。 原因:FERET 里有同一个人在不同时间点拍摄的重复样本,如果把时间跨度样本全部混进训练集,测试时抽到的是同一时间点的兄弟照片,模型当然认得出来。这等于提前看过考试答案,测试分数没有参考意义。 解决:按照拍摄批次和时间戳把 FERET 分开,用早期批次训练、后期批次测试。这也解释了为什么global_id不能简单地等于 person_code,需要再加上拍摄批次字段,比如FERET_037_batch1和FERET_037_batch2在训练时要视为不同样本,在测试时要单独计算“跨时间准确率”。训练代码里也要谨慎对待数据增强里的随机采样,防止同批次样本被不均匀地分配到训练和测试集合。
6. 从“能跑通”到“可信”:给模型做一次跨库体检
模型在一个库上训练完成,不代表它真的具备识别能力。我每次训练结束都会强制自己做一件事:用模型从未参与训练的其他库做一次体检,这个环节叫跨库验证。做法是先固定训练集来自 A 库,再用 B 库和 C 库的完整数据做测试。注意测试集里不要排除任何子集,特别是最难的子集,因为体检的目的就是看模型在极限情况下会不会崩。
6.1 跨库体检的设计与指标
我用三个指标衡量模型是否可信:同库准确率、跨库准确率、难例子集准确率。同库准确率反映模型有没有正常收敛;跨库准确率反映模型学到的是通用特征还是库内特有特征;难例子集准确率反映模型在极端光照、遮挡、姿态下的短板。三个指标一起看,能快速定位问题。如果同库准确率高但跨库准确率低,说明过拟合了;如果跨库也不低,但难例子集低,说明模型对常规变化鲁棒、对极端情况不足。
6.2 翻车日志:让错误样本说出模型短板
体检过程中我会开启一个错误日志记录,把每个样本的预测结果和真实标签写到 CSV。不要只看总量,按 source 字段分组统计准确率,再按文件名里的光照、姿态等信息细分,能看出模型具体在哪里翻车。这一步相当于给模型做“错题分析”。
import json import csv # 跨库体检时记录每个样本的预测结果 with open('cross_lib_eval.csv', 'w', newline='', encoding='utf-8') as fp: writer = csv.writer(fp) writer.writerow(['path', 'source', 'true_id', 'pred_id', 'correct']) # 假设 eval_loader 是待测库的数据加载器,model 是训练好的模型 # for img, true_id in eval_loader: # pred_id = predict(model, img) # writer.writerow([path, source, true_id, pred_id, pred_id == true_id])然后我会用 pandas 读 CSV,按source和true_id分组算准确率,把准确率最低的库标记为“当前最大短板”。这个表就是下一步投入方向的依据,比拍脑袋决定“再增强一下光照”要靠谱得多。
跨库体检不要求高分,但要求结果可解释。哪怕跨库准确率只有 60%,只要你能说出是光照问题还是遮挡问题,就已经比一个 95% 的黑匣子更接近落地。我以前也吃过这个亏,ORL 上 98% 的模型让我白高兴了一周,后来养成了每个模型上线前必须过跨库体检的习惯。多花两天,省一个月后悔药,值。希望帮到你。
本文还有配套的精品资源,点击获取