人脸表情识别入门:CK+数据集使用、预处理与训练避坑指南

人脸表情识别入门:CK+数据集使用、预处理与训练避坑指南 简介CK人脸表情数据集是计算机视觉领域经典的人脸表情识别资源源自美国宾夕法尼亚州立大学Cohn-Kanade数据集扩展面向表情识别算法研究、深度学习训练和心理学实验等场景涵盖中性、快乐、悲伤、惊讶、愤怒、厌恶、恐惧七类基本表情的动态图像序列。压缩包共928个文件、约139.77MB主体为920张PNG格式人脸表情帧并包含rearrangeCK.c等预处理脚本、CVPR2010_CK.pdf文献、Consent for Publication.doc许可文档兼顾数据使用与学术引用。目前已有2523人学习下载适合初学者快速上手也便于科研人员复现实验。包内图像多已灰度化、归一化并附带标注信息可直接用于模型训练同时借助脚本和说明文档可梳理数据目录、理解表情连续变化过程为人脸表情识别项目提供扎实数据基础。 如果你刚开始做人脸表情识别去搜索“人脸表情数据集”最先跳出来的几乎肯定是CK。我第一次跑表情分类实验也是从CK下手的这个数据集自带时序帧、表情标签和FACS动作单元标注到现在依然是表情识别论文里出现频率最高的benchmark之一。这篇内容我从实际使用角度出发讲清楚CK到底怎么用、预处理时有哪些坑、以及怎么搭一个靠谱的训练流程。1. 为什么表情识别入门绕不开CK1.1 先从CK本身说起CK全称是Cohn-Kanade Extended Dataset是在早期Cohn-Kanade数据集基础上扩展出来的版本。原始CK数据集收集了97个人的486段人脸视频序列CK扩展后变成123个人的593段序列每段序列都从neutral表情开始逐步过渡到峰值表情。它发布于2010年前后距今十几年却依然是很多表情识别论文必跑的数据集之一。它的核心特点有三个第一它是一个带时序的数据集每个样本是一段连续帧不是单张图片第二它同时提供了离散表情标签和FACSFacial Action Coding System动作单元标注第三图像是在实验室受控条件下拍摄的光照、角度、遮挡都相对规整。这意味着你拿到的是一个“干净”的研究用数据集非常适合验证算法思路、跑baseline、对比消融实验。我自己的体会是CK最适合两类人一类是刚入门表情识别、想快速跑通完整pipeline的初学者另一类是论文里需要做公开数据集对比验证的研究者。它的样本量对于训练工业级模型来说不大但作为算法验证和学术对比价值非常高。1.2 和其他数据集放在一起看差距在哪里很多新手会问既然CK数据量这么小为什么不用更大更新的数据集这里需要区分一下数据集的定位。CK走的是“小而精”路线强调标注质量和多维标注信息而FER2013、AffectNet这些走的是“大而广”路线强调覆盖真实场景的多样性。数据集规模环境标注类型是否含时序适合场景CK593段序列、123人实验室受控离散表情 FACS AU是算法验证、AU分析、消融实验FER2013约3.5万张互联网野生7类离散表情否深度学习入门baselineAffectNet约45万张互联网野生8类离散 效价/唤醒否大模型预训练、真实场景RAF-DB约3万张互联网野生7类基本表情 复合表情否更细粒度的表情分类从这个表能看出来CK的不可替代性不在于数量而在于它同时具备时序、行为单元标注和表情标签。如果你做基于时序的模型、或者想分析表情的肌肉运动规律CK几乎是唯一的选择。FER2013这类静态数据只有一张最终表情图你没法研究“表情是怎么从neutral演变到peak的”这是本质区别。提示正因为CK是实验室受控数据模型在上面跑出高准确率并不代表真实场景泛化能力强。我在实际项目中通常把CK当作“算法正确性验证集”真正评估性能还需要结合其他真实场景数据。2. 拿到原始包后目录和标注怎么读2.1 从官方渠道下载后的目录长什么样CK需要向官方申请下载申请通过后会得到一个压缩包解压后的目录结构大致如下CK/ ├── cohn-kanade-images/ │ ├── S001/ │ │ ├── 001/ │ │ │ ├── 00000001.png │ │ │ ├── 00000002.png │ │ │ └── ... │ │ ├── 002/ │ │ └── ... │ ├── S002/ │ └── ... ├── Emotion/ ├── FACS/ └── Landmarks/先说命名规则。Sxxx是被试编号S001就是第一个被试者被试编号下面还有一级目录比如001、002表示这个人的第几段视频序列。cohn-kanade-images里就是真实的图像帧每段序列一个文件夹帧文件名是纯数字按时间顺序递增。Emotion和FACS目录下的文件命名和图像目录是对应的但只包含有标注的文件。比如Emotion/S001/001.txt就代表S001这个人的第001号序列txt内容是一个数字标签。Landmarks目录保存的是每一帧的68点人脸关键点坐标文件格式通常是每两行对应一个点的x、y坐标。这里尤其要注意Emotion目录下的标签文件不是每个序列都有。官方只给一部分有明确目标表情的序列提供了语义标签所以我每次拿到数据第一步就是统计一下有多少序列带表情标签。网上有说大约327个序列带标签具体数字你下载后数一下Emotion目录下的txt文件数就知道以你自己拿到的那份为准。2.2 FACS和AUCK的隐藏价值FACS是一套心理学领域发展出来的面部动作编码系统它把人的面部表情拆解成一个个独立的动作单元Action Unit简称AU。简单理解AU就是“面部肌肉运动的最小可标注单位”。比如AU1眉毛内侧上抬AU4皱眉眉毛下压AU12嘴角上扬也就是微笑的核心动作AU25嘴唇微张AU26下颌放松张开CK对序列提供了FACS编码标注一个序列的峰值帧通常会被标注多个AU编号比如“525”表示AU5和AU25同时发生。这个标注的价值非常大因为离散表情标签是粗粒度的而AU能揭示表情到底是靠哪些肌肉运动组合出来的。你可以基于AU做表情识别的中间监督也可以做“表情AU”的多任务学习让模型学到的特征更具可解释性。在官方目录的FACS文件夹里每个有标注的序列都有一个txt文件内容是类似“52025”这样的AU组合。如果要把它用在模型训练里常见做法是把AU组合转成多标签向量设定一个AU编号列表出现在组合里就标1否则标0。注意FACS目录的覆盖范围比Emotion目录更广很多没有语义表情标签的序列也有AU标注。如果你想尽可能多利用CK的数据量可以用AU标注做多任务学习如果只想做表情分类就以Emotion目录为准。2.3 把标签文件映射成分类任务时要注意什么Emotion目录下的txt标签文件是一个数字对应关系如下数字表情实际使用中的建议0neutral一般不做单独类别或作为第8类1anger直接参与分类2contempt样本最少多数人直接剔除3disgust直接参与分类4fear直接参与分类5happy直接参与分类6sadness直接参与分类7surprise直接参与分类这里最需要注意的就是contempt这个类别。CK在原始版本上增加了contempt表情但它的样本量比另外7类少很多如果直接做8分类模型容易在contempt上表现很差。我个人的做法是默认使用7分类也就是剔除contempt只有论文需要报告8分类结果时才保留并且用加权损失或者过采样来缓解样本不平衡。另外Emotion目录里没有neutral的标注标签。很多教程会把每个序列的第一帧当成neutral样本补充进去这样能多出一批数据但也要注意这些第一帧是否真的全部是neutral表情我在实际操作中发现大部分是但有个别序列起始帧已经带了轻微表情。如果你追求严谨可以先跑一遍人脸表情识别模型过滤掉非neutral帧或者自己人工抽检一批。3. 从原始帧到训练样本的全流程预处理3.1 提取峰值帧别把整条序列都塞给模型CK的每段序列都是从neutral逐渐过渡到目标表情目标表情在峰值帧上最明显。训练静态分类模型时最常用的做法是直接取每个序列的最后一帧作为峰值帧。但需要小心个别序列的最后一帧并不是表情最强烈的帧更稳妥的方案是读取该序列的FACS标注文件对应的帧号或者对整段序列逐帧做一次置信度统计取置信度最高的帧。下面是一个简化版的峰值帧提取脚本适合绝大多数情况import cv2 from pathlib import Path def get_peak_frame(seq_dir: Path) - Path: frames list(seq_dir.glob(*.png)) frames.sort(keylambda p: int(p.stem)) # 按帧编号排序 return frames[-1] # 遍历所有序列收集峰值帧 image_root Path(cohn-kanade-images) peak_frames [] for subject_dir in sorted(image_root.iterdir()): for seq_dir in sorted(subject_dir.iterdir()): peak_frames.append(get_peak_frame(seq_dir))这段代码的“最后一帧”策略在绝大多数情况下是可行的因为CK的序列设计就是到峰值帧结束。如果之后读取Emotion标签时发现某个峰值帧没有对应标签文件就直接跳过这个样本。3.2 人脸对齐和裁剪的确定性规则CK的图像本身是正脸为主但不同被试者的人脸位置、大小、左右偏移仍然有差异。如果直接把整张图resize成固定尺寸喂给模型背景比例不一致会引入噪声。我通常先做人脸对齐再统一裁剪到112x112或224x224。常规做法是用dlib检测68个关键点然后根据两只眼睛的位置做仿射变换把眼睛摆到同一水平线import cv2 import dlib import numpy as np detector dlib.get_frontal_face_detector() predictor dlib.shape_predictor(shape_predictor_68_face_landmarks.dat) def align_face(img): rect detector(img, 1)[0] shape predictor(img, rect) coords np.array([(shape.part(i).x, shape.part(i).y) for i in range(68)]) left_eye coords[36:42].mean(axis0) right_eye coords[42:48].mean(axis0) angle np.degrees(np.arctan2( right_eye[1] - left_eye[1], right_eye[0] - left_eye[0] )) center (left_eye right_eye) / 2 matrix cv2.getRotationMatrix2D(tuple(center), angle, 1.0) aligned cv2.warpAffine(img, matrix, (img.shape[1], img.shape[0])) return aligned对齐之后可以按检测框裁剪也可以固定以人脸中心裁一块正方形区域再resize到目标尺寸。这里有一个容易被忽略的要求训练集和测试集必须使用完全相同的对齐和裁剪流程不能训练时用一个版本、测试时用另一个版本否则模型性能会崩塌。提示Landmarks目录里官方也提供了68点坐标可以直接读取省去dlib检测这一步。但官方landmark可能和某些帧不完全对齐如果发现对齐失败回退到dlib检测会更有保障。3.3 数据划分必须按人分不能按样本分这是CK使用中最容易出问题的一步。很多人下载数据后直接把所有峰值帧放进一个列表然后train_test_split随机划分测试准确率高达98%以上但换了新的人脸就明显下降原因就是身份信息泄漏。同一个被试者的多段序列虽然表情不同但面部结构、肤色、纹理特征高度相似。如果同一个人的一部分序列在训练集、另一部分在测试集模型实际上记住了“这个人的脸”而不是“这个人的表情”。正确做法是按被试者划分即同一subject的所有序列只能出现在同一个集合里。sklearn提供了现成的GroupKFoldfrom sklearn.model_selection import GroupKFold # subject_ids 是每个样本对应的被试编号例如 S001、S002... # X 是所有样本的输入y 是对应的表情标签 gkf GroupKFold(n_splits5) for train_idx, val_idx in gkf.split(X, y, groupssubject_ids): x_train, x_val X[train_idx], X[val_idx] y_train, y_val y[train_idx], y[val_idx]按人划分后测试准确率通常会比随机划分低几个百分点但这才是泛化能力的真实反映。我在自己实验里踩过这个坑后来养成了习惯任何数据集拿到手先检查是否存在同一个人出现在多个集合里的情况。4. 两种典型训练路线和调参经验4.1 走静态帧分类路线baseline怎么搭最省事的方案是只用峰值帧做静态图像分类。输入是112x112的灰度图或转成RGB的三通道图模型用ResNet18、MobileNetV2这类轻量网络就够了。相比ImageNet动辄上百万张的训练规模CK只有几百个样本模型太大反而容易过拟合。我自己常用的配置是ResNet18预训练权重起步把最后一层全连接改成7维输出batch size设为32优化器用Adam初始学习率1e-4训练30到50个epoch配合早停。数据增强只做水平翻转、小角度旋转、轻微亮度对比度调整。增强强度不能太大因为CK本身是受控数据过度增强会把表情的细节破坏掉。在按subject划分、剔除contempt的7分类设置下ResNet18的准确率通常能到95%到98%这个数字看起来很高但不要因此兴奋。CK的数据量小、类别区分度大几乎所有方法都能刷到接近满分。真正有意义的是对比不同方法在相同划分下的相对差异以及查看每个类别的recall特别是fear、sadness这种容易混淆的类别。4.2 用上时序信息C3D与两帧方案的取舍CK带有时序信息这是它区别于FER2013的最大优势。想充分利用时序常见路线是用C3D、I3D这类3D卷积网络吃整段序列或者用RNN/LSTM聚合帧特征。但CK序列长度差异极大有的序列只有五六帧有的几十帧直接塞给3D卷积很麻烦需要先做长度归一化到固定帧数比如统一采样8帧或16帧。实际体验是这个方案很看运气受限于样本量3D卷积很容易过拟合而且训练速度远低于静态模型。如果你的目标是快速验证想法我不会推荐一上来就上C3D。另一个更轻量但也有效果的做法是“两帧方案”取序列的第一帧作为neutral状态取峰值帧作为表情状态把两帧拼接成双通道输入让模型自己学习“从neutral到目标的动态变化”。这个方案既保留了部分时序信息又避开了长序列处理问题在CK上性价比很高。论文里不少工作也是这么做的用作baseline很合适。4.3 小而偏的数据集增强怎么做CK的类别分布并不均匀。happy和surprise样本相对多fear、sadness、disgust相对少contempt最少。处理方式有两种一种是在采样阶段对样本少的类别做加权采样另一种是在损失函数里给样本少的类别更高的权重。在实践中我更倾向于先用加权采样把每个batch的类别分布拉平再配合一个普通的交叉熵损失如果模型在少数类上还是不够好再加权重。数据增强方面除了常用的翻转和裁剪还可以尝试Mixup即把两张训练图按比例混合生成新样本。Mixup在小数据集上的效果往往比单纯做仿射变换更明显因为它本质上是做了样本空间的插值让决策边界更平滑。但要注意Mixup适合单帧静态模型不适合带序列结构的输入。5. 常见问题与排查技巧实录5.1 标签文件对不上序列最常见的问题是Emotion目录下的标签文件比图像序列少很多。比如你在cohn-kanade-images下找到593个序列但Emotion下只有300多个txt文件。这不是数据损坏而是官方只为有明确目标表情的序列提供了语义标签。解决办法是先扫描Emotion目录拿到所有标签文件再去cohn-kanade-images里找对应的序列只保留两者匹配的样本。不要反过来先遍历所有序列再找标签否则你会收到一堆“缺失文件”报错。5.2 不同序列的长度差异极大有的序列只有5帧有的有60帧如果直接按固定步长采样短序列会被重复采样很多次导致特定样本过度出现。处理这类问题要分场景静态分类只取峰值帧长度差异无所谓时序模型需要规定一个目标长度N短序列循环补齐到N帧长序列均匀抽样到N帧。还有一个细节不要假设“峰值帧一定是最后一帧”。虽然大部分序列如此但极少数序列在达到峰值后还有回落帧取最后一帧可能导致标签和画面不匹配。我的排查方法是随机抽100个序列把最后一帧和标签对应起来人工看一眼发现问题再改用FACS标注的峰值帧。5.3 网上的预处理包可能坑过你很多GitHub仓库和网盘里流传着“CK预处理完毕版”直接把图像裁好、对齐好、按文件夹分好类用起来确实方便。但这类版本不知道经过了谁的处理划分逻辑不一定规范。比如有些包是按个人理解随机划分的数据集你拿它跑出来的98%准确率没法写进论文又比如有些包把水平翻转后的样本混入原始数据导致训练集和测试集之间存在关联样本。如果你要复现论文结果或者拿数据做正式实验我建议还是从官方渠道下载原始版本自己跑一遍预处理。纯学习验证那无所谓省时间更重要。5.4 有关准确率高低的认知问题即使按subject划分正确CK上能达到95%以上准确率也是正常的。一个常见认知误区是“准确率这么高说明模型已经很强了”。实际上CK是在受控环境下采集的背景统一、正脸摆拍、表情夸张清晰模型容易学会“区分表情”而不是“识别表情”。所以我在做CK实验时除了报告准确率还会做两件事一是画混淆矩阵找出fear和surprise、disgust和anger这类容易混淆的类别二是做一次跨数据集测试比如在CK上训练在FER2013的部分样本上直接测看看泛化能力剩多少。这才是更接近实际应用的评估方式。最后分享一个我一直在用的习惯任何表情识别模型我都会把训练好的模型跑在一个自拍视频上看实时预测的帧间抖动情况。CK是静态或半静态的峰值帧而真实视频里表情是连续变化的帧间抖动大说明模型对中间状态的鲁棒性不够。这个测试能帮你发现很多准确率指标暴露不出来的问题。如果你也打算用CK做实验建议从一开始就按照subject划分来做数据处理不要图省事随机切分否则后面调参过程中看到的所有高分都可能是假象。本文还有配套的精品资源点击获取