☰
CK+数据集详解:结构、标注与人脸表情识别实战
2026/10/17 6:56:36 网站建设 项目流程

第一次在论文里看到“CK+数据集”这个词时,我其实是有点懵的。满屏都是“Extended Cohn-Kanade”这类英文缩写,读起来像某种实验室内部代号,对一个刚接触人脸表情识别方向的人来说,很容易被绕晕。后来我花了一整个下午,把数据下载下来、解压、翻目录、读标注文件,才算真正搞清楚它是什么、能用来做什么。CK和CK+本质上是同一套人脸表情数据库的两代版本,现在绝大多数表情识别相关的算法实验,都会拿它当对照基准。这篇文章不打算复述论文里的官话,就从一个实际使用者的角度,聊聊这个数据集的结构、标注逻辑,以及我在跑实验过程中攒下来的经验。

1. 认识CK与CK+:从起源到结构

1.1 从CK到CK+:两代数据集的迭代逻辑

很多人第一次接触时,会误以为CK+只是CK的“加量版”,多塞了几百张图片而已。实际情况并非这么简单。

CK版本是由某高校研究团队在2000年左右发布的一套面部表情序列数据。它的核心设计思路,是录制一批受试者在实验室环境下从平静表情逐渐变化到表情峰值的连续画面。当时的初衷主要是服务于心理学家研究面部动作编码,后来才被计算机视觉领域大规模采用。

到了2010年前后,研究团队在原有基础上做了比较大的扩展,推出了CK+。这个“+”体现在几个方面:一是受试者数量增加了,从最早的一百人左右扩充到一百多人;二是序列数量大幅增加,最终接近六百段图像序列;三是标注信息更丰富,除了原有的FACS动作单元编码之外,还补充了七种基本表情的类别标签,以及部分关键点定位数据。简而言之,CK+才是真正适合做表情分类算法训练和验证的版本。

我在实际使用中的体会是,如果你只是想快速验证一个表情识别思路,直接用CK+就够了,没必要回溯到CK早期版本,因为两代数据的目录结构有差异,混用反而容易出错。后面章节我会详细解释为什么。

1.2 读懂FACS标注:表情背后的“动作单元”

CK和CK+真正让人抓狂的地方,在于它不直接告诉你“这张脸是开心”,而是给了一串看起来像密码的标注代码,比如“AU12 + AU25”。这套密码来自心理学领域著名的面部动作编码系统,也就是FACS。

FACS的思路很朴素:人的表情再复杂,归根结底是面部肌肉运动造成的。研究者把面部拆分为多个动作单元,每个动作单元对应一块或一组肌肉的运动状态。比如AU4代表眉毛内侧下压,通常出现在皱眉或生气表情里;AU12代表嘴角向斜上方拉伸,是微笑的核心动作;AU25代表嘴唇分开,经常伴随着惊讶或开心。CK+的道路在于,它把这种心理学的细分标注和计算机视觉的分类数据融合在了一起。

所以你在解析CK+的标注文件时,会看到两种不同粒度的信息:FACS标注文件里记录的是动作单元编号及其强度等级,Emotion标注文件里记录的才是六个或七个表情类别。前者适合做动作单元识别、辅助心理学分析,后者适合做分类模型的监督信号。

1.3 这个数据集为什么能成为行业基准

我后来带过几个新人,他们都有一个疑惑:网上能找到的表情数据集那么多,为什么大家偏偏喜欢用CK+做基准?

一个很重要的原因,是它足够“干净”。CK+的采集环境是受控的:受试者基本正对摄像头,光照条件相对统一,背景干扰少,每个序列还有清晰的起点(中性表情)和终点(表情峰值)。这意味着研究者可以非常明确地把“表情变化”从其他干扰因素中剥离出来。相比之下,从互联网上爬取的表情图像,姿态各异、光照复杂、标签噪声也比较大,拿来做算法验证时,很难说清楚模型性能下降到底是算法问题还是数据问题。

另一个原因,是它的序列化结构非常稀缺。很多静态表情数据集只有一张张孤立的图片,但CK+提供的是连续帧序列,从平静到峰值的完整过程。这给了研究者额外的自由度,比如可以基于时序信息做动态表情识别,或者用起始帧和峰值帧做差分分析。这种结构在十年前是很大的优势,即使是现在,能让研究人员做时序建模的公开表情数据集仍然不多。

2. 数据真实规模与适用场景

2.1 数据量没有想象中那么大

很多刚刚准备用CK+的人,拿到解压后的文件夹,看到里面有一万多张图片,会以为数据量很充足。但实际上,当你把它们按序列归纳之后,会发现总数并没有那么夸张:大约一百多位受试者,贡献了五百多段表情变化序列。每段序列平均几十帧,真正包含明显表情峰值的帧,每段可能只有最后两三张。

这个体量对深度学习训练来说,其实属于偏小的数据集。假如你直接拿一个大型卷积神经网络从头开始训练,不用多久就会遇到过拟合——训练集的准确率冲到99%,验证集却一直徘徊在七八十。所以使用CK+的正确姿势,要么是用预训练模型做微调,要么是把它作为验证集,而不是作为主训练集。

我自己的项目里,通常把CK+当作“测试模型是否学到了真实特征”的试金石。因为它数据干净、标注可靠,模型如果连CK+都做不好,基本不用指望它在真实场景里能有多大作为。

2.2 七种表情的序列结构与标注粒度

CK+的Emotion标签覆盖了七种基本表情:生气、蔑视、厌恶、恐惧、开心、难过、惊讶。这里有个细节值得注意:并不是所有序列都标注了表情类别。有一部分序列只有FACS动作单元编码,却没有对应的七分类标签。所以在使用的时候,必须先过滤出“有Emotion标签”的序列,否则训练时就会碰到标签缺失的意外。

每段标注了表情的序列,通常以中性表情起始,逐步过渡到目标表情的峰值。研究者普遍的做法是取序列的最后几帧,或者干脆只取峰值帧作为分类样本。但也有人反其道而行之,把表情变化过程当作一个整体输入,让模型学习动态特征。这两种思路都没有问题,关键是实验目的不同。

我做过一次统计,在完整CK+的Emotion标注中,不同表情类别的样本数量并不均衡。惊讶和开心往往比较多,蔑视和恐惧相对较少。这个现象在训练时会被放大,需要在数据采样策略上做处理。后面章节我会说具体怎么做。

2.3 它能做什么、不能做什么

基于我自己的项目经验,CK+最适合做四件事情。

第一,算法效果的对照基准。论文里报精度,大家都会带上CK+的结果作为标准参照。第二,小模型的快速验证。比如你想验证一个轻量网络结构是否适合表情分类,用CK+可以很快得到有意义的比较结果,因为它的样本量不大,训练一个epoch往往只需要几秒。第三,动作单元识别研究。因为CK+同时带了FACS标注,你可以训练模型预测AU编号,而不是简单分类表情。第四,表情生成类任务。序列帧结构很适合做“从平静到表情”的图像生成实验,比如用生成对抗网络产生中间帧。

反过来,它也有很明显的短板:不适用于大规模预训练,数据分布与真实世界差异较大,受试者身份背景相对单一,而且拍摄环境太可控,无法验证模型的抗干扰能力。所以只依赖CK+做评测,在如今的学术评价体系里已经不太够用,通常需要搭配其他场景的数据集一起看结果。

3. 实操手记:从下载到训练集构建

3.1 目录结构与命名规则解读

假设你已经拿到了CK+的压缩包,解压后会发现几个核心目录。不同来源的压缩包目录名可能略有差异,但基本都会包含:

  • 图像帧目录:存放所有受试者的灰度图像序列,常见命名是cohn-kanade-images。
  • 关键点目录:存放每帧图像的人脸关键点坐标,常见命名是landmarks。
  • FACS标注目录:存放动作单元标注文件,常见命名是FACS_labels。
  • 表情标注目录:存放七分类标签文件,常见命名是Emotion_labels。

图像文件的命名方式非常有规律,例如S010_001_00000001.png,含义是:编号为010的受试者,第001段序列,第1帧。理解了这套命名,你就能轻松地把同一段的帧归拢到一起。标注文件则是单独放在另一个目录下,文件名与序列对应,内容通常是几行数字或者键值对。

这里有一个非常容易踩的坑:部分下载渠道提供的CK+数据是拆分过的,比如只给了图像帧文件夹,没有Emotion标签;有的渠道则把两代版本混在同一个压缩包里。所以下载之后第一件事,不是急着写代码,而是先检查是否包含完整子目录。

3.2 用Python批量读取表情标签

我会先写一段代码,把Emotion目录下的所有标签读取进来,映射到对应的序列帧上。之前的实践中,标签文件通常是一个名为S010_001_00000001_00000001.txt的小文件,内容包含一个数字,代表表情类别。

这里给出一个实用的读取逻辑:

from pathlib import Path import numpy as np def load_emotion_labels(emotion_dir): label_map = {} for txt_path in Path(emotion_dir).glob("*.txt"): # 文件名形如 S010_001_00000001_00000001.txt seq_id = txt_path.stem[:11] # 提取 S010_001 value = int(txt_path.read_text().strip().split()[0]) label_map[seq_id] = value return label_map def load_sequence_frames(image_dir): seq_frames = {} for img_path in Path(image_dir).glob("*.png"): seq_id = img_path.stem[:11] seq_frames.setdefault(seq_id, []).append(str(img_path)) return seq_frames

注意,我这里是按S010_001这种前11个字符来提取序列ID。实际命名有细微差异时,最好先打印几个文件名确认一下格式,再决定切片策略。我见过有的渠道在序列号后多了一个下划线,切片前必须验证。

读取之后,还需要把标签和帧对应上。常见做法是遍历label_map的每个序列ID,只保留有标签的序列帧:

frames = load_sequence_frames("data/cohn-kanade-images") labels = load_emotion_labels("data/Emotion_labels") dataset = [] for seq_id, label in labels.items(): if seq_id in frames: for img_path in frames[seq_id]: dataset.append((img_path, label))

这一步做完,你就有了一个最基础的(图片路径, 类别ID)列表。

3.3 图像预处理与人脸区域裁剪

CK+的原始图像分辨率是统一的灰度图,但由于受试者头肩位置存在轻微偏移,直接整张图喂给模型,会引入大量背景区域。常见做法是先去人脸区域,裁剪后统一缩放,送入网络。

最简单的人脸区域提取方案是用OpenCV的预训练人脸检测器:

import cv2 face_cascade = cv2.CascadeClassifier( cv2.data.haarcascades + "haarcascade_frontalface_default.xml" ) def extract_face(img_path, target_size=224): img = cv2.imread(img_path, cv2.IMREAD_GRAYSCALE) faces = face_cascade.detectMultiScale(img, scaleFactor=1.1, minNeighbors=5) if len(faces) == 0: # 检测不到人脸时,退化为中心裁剪 h, w = img.shape cx, cy = w // 2, h // 2 crop = img[cy - 100:cy + 100, cx - 100:cx + 100] else: x, y, w, h = faces[0] crop = img[y:y + h, x:x + w] crop = cv2.resize(crop, (target_size, target_size)) return crop

这段代码对CK+的绝大多数帧都有效,因为受试者基本正对镜头。但如果你用关键点标注做更精细的对齐,效果会更好。关键点的思路是先取眼睛和嘴巴位置,做仿射变换,把面部统一摆正。这个步骤对表情识别挺重要,因为同一个表情在稍有不同的角度下,像素差异会被放大,导致过拟合到姿态信息上。

我实际训练模型时,还会给图像做归一化,把像素值缩放到0到1之间,并用ImageNet均值和标准差做标准化,因为我的基础模型是在ImageNet上预训练的。如果是自己从头训练,归一化策略可以更简单一些。

3.4 一个可直接套用的表情分类训练流程

当我们把数据集和预处理逻辑准备好之后,可以开始搭建训练流程。这里给出一个自认为比较稳妥的方案。

第一步,按“受试者”划分训练集和验证集,而不是按帧随机划分。原因我在后面章节会展开说,简单讲就是避免同一个人的不同帧同时出现在训练集和验证集,导致结果虚高。第二步,使用预训练网络做特征提取和微调,比如在ImageNet上预训练的轻量卷积网络。第三步,采用数据增强策略,包括随机水平翻转、小角度旋转、亮度扰动,这是因为CK+样本量小,增强能显著提升泛化能力。第四步,用加权采样处理类别不均衡。

下面是一个用PyTorch风格实现的数据加载器骨架:

from torch.utils.data import Dataset from torchvision import transforms class CKPlusDataset(Dataset): def __init__(self, data_pairs, train=True): self.data_pairs = data_pairs self.train = train if train: self.transform = transforms.Compose([ transforms.ToPILImage(), transforms.RandomHorizontalFlip(0.5), transforms.RandomRotation(10), transforms.ColorJitter(brightness=0.2, contrast=0.2), transforms.ToTensor(), transforms.Normalize([0.5], [0.5]) ]) else: self.transform = transforms.Compose([ transforms.ToPILImage(), transforms.ToTensor(), transforms.Normalize([0.5], [0.5]) ]) def __len__(self): return len(self.data_pairs) def __getitem__(self, idx): img_path, label = self.data_pairs[idx] img = extract_face(img_path) img = self.transform(img) return img, label

训练时建议把batch size设为32或64,学习率设在0.001数量级,并且只训练后面几层分类头,先观察验证准确率是否合理,再决定要不要解冻更多层。这个流程本身并不复杂,真正容易出问题的往往不是模型结构,而是数据划分和预处理细节。

4. 踩坑笔记:CK+使用中的常见问题

4.1 标签缺失与文件名对齐

先说说我最初遇到的一个非常典型的问题。当时我把所有帧和标签粗暴地拼接成列表,结果训练集里混入了大量没有标签的序列。这些序列在构建训练批次时,直接导致索引越界或者标签数组长度不匹配。

后来我才意识到,CK+文件夹里的每张图都对应一个序列,但并非每个序列都有情感标签。解决方法是先把带标签的序列ID存成集合,再以此集合筛选图像帧。这也提醒我,拿到任何数据集的第一件事,永远是“检查字段完整性”,而不是急着写模型。

还有一个坑是名字对齐。CK+的文件命名在不同渠道提供了不同版本,有的标签文件名是S010_001_00000001_00000001.txt,有的是S010_001_00000001.txt。你在做序列ID提取时,最好把图像和标注的命名规则打印出来对比一遍,再决定切片位置。

4.2 类别不均衡的处理策略

我从CK+统计出的标签分布,基本可以肯定“惊讶”和“开心”的数量明显高于“蔑视”和“恐惧”。如果训练时不做处理,模型会对多数类产生偏好,验证集里多数类的召回率很高,而少数类几乎全错。

这里的处理策略可以分为四个层次,从简单到复杂分别是:简单过采样,即对少数类样本做重复采样,让每个batch里各类别数量接近;加权损失函数,即根据类别频率调整交叉熵的权重,让少数类错误贡献更大的梯度;类别平衡采样器,在PyTorch里可以用WeightedRandomSampler实现;以及做额外的扩充,比如对少数类做更强的几何变换,人为增加样本多样性。

我用了加权采样加轻度过采样组合,效果比较理想,尤其对“蔑视”这个类别的提升很明显。不过要注意,过采样倍数不宜太大,否则模型容易在少数类上过拟合,导致验证集表现不错、真实场景里却崩掉。

4.3 数据划分方式决定实验结果可信度

这个问题我专门拿出来说,因为它直接影响实验结论的可靠性。很多初学的人会把所有帧随机打乱,按比例划分训练集和验证集。这样做其实有很严重的偏差:同一个人的同一段表情序列,前几帧被分到训练集,最后几帧被分到验证集,模型相当于“见过”了同一个人的同一表情的中间帧,验证准确率很容易虚高到接近满分。

我自己的做法是,按受试者做分组划分。比如把所有序列按受试者编号聚合,随机选取一部分受试者的全部帧做验证,剩下的做训练。这样模型面对的是完全没见过的“新人脸”,更能反映真实场景的泛化能力。这个经验后来也被我应用在其他数据集上,整体效果很稳定。

4.4 混合数据集训练的小技巧

用CK+做基准测试时,还有一个常见做法是与其他静态表情数据混合。比如用一个公开的、场景更复杂的人脸表情数据集作为主训练集,再把CK+的峰值帧作为额外样本补充进来,最后用CK+的验证集做测试。

这种方式的好处是,既解决了CK+规模小的问题,又能通过它的干净标注校准模型输出。但要注意,不同数据集的标签体系可能不完全一致。有些数据集把“蔑视”和“厌恶”合并为一个类,有些则以情感词汇为标签而不是表情类别。合并前必须确认类别语义是否对齐,否则模型会被相互矛盾的学习目标弄乱。

我在项目中的做法是,先用混合数据集训练一轮,再在CK+上做一次微调。微调用较小的学习率,比如0.0001,并且只更新最后两三个全连接层,这样能在保留复杂场景特征的前提下,让模型适应实验室干净分布。

写到这里,我忍不住想起最开始跑CK+实验时的状态,总觉得数据量太少,怎么跑都出不了让人满意的结果。后来慢慢想明白了:这个数据集的价值不在于“大”,而在于“干净”。它就像一块标尺,能帮你检验模型的真实能力,也能让你在纷繁复杂的视觉任务里找到一个相对稳定的起点。如果你正准备做人脸表情识别方向的工作,我的建议是先花半天时间把这个数据集彻底吃透,再动手搭建训练流程,这会比急着找大数据集要划算得多。希望我踩过的这些坑,能帮你走得更顺一些。

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

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

立即咨询