fer2013数据集提取与训练:从CSV到图片的完整流程

发布时间:2026/9/26 3:17:03
fer2013数据集提取与训练:从CSV到图片的完整流程
简介这份资源面向情绪识别与面部表情识别方向的学习者和开发者提供fer2013经典数据集的完整素材与配套处理代码帮助解决数据获取、格式转换与模型训练前的预处理问题。压缩包共2000个文件以1998张jpg图片为主另含1个txt说明文档和1个py脚本整体约130.43MB图片可直接用于查看样本分布与表情类别脚本则承担数据读取、灰度归一化、标签映射及训练/验证/测试集划分等任务。数据集覆盖愤怒、厌恶、恐惧、快乐、中立、悲伤、惊讶七类情绪图像统一为48×48灰度图适合CNN或迁移学习模型的训练与评估。目前已有536人学习下载读者可借此快速搭建表情识别实验流程理解数据标注编码、类别不均衡评估及常见预处理思路为课程设计、毕业项目或算法复现提供可直接上手的基础素材。1. 拿到 fer2013 数据集压缩包先搞清楚它到底装了什么fer2013 数据集是 2013 年 Kaggle 人脸表情识别挑战赛放出的经典资源七类表情、近三万五千张灰度人脸至今仍是表情识别入门和模型对比的基准。但很多人第一次拿到手的不是干净的图片文件夹而是一个名为「fer2013数据集和提取出的数据集图片以及python提取代码.zip」的压缩包——里面既有原始 CSV又有已经切好的图片目录还塞了一段 Python 提取脚本。这就有意思了它同时解决了「数据从哪来」和「怎么变成模型能吃的格式」两个问题省掉了新手最头疼的格式转换环节。如果你正在做人脸表情分类、想跑通一个完整的训练闭环或者只是需要一批规整的 48×48 灰度图做实验这个包能让你跳过数据清洗直接进入建模。但前提是你得先弄明白里面每一层目录到底对应什么否则很容易在路径上翻车。2. fer2013 的原始格式与提取逻辑为什么不能直接喂给模型2.1 原始 CSV 长什么样像素是怎么存的fer2013 原始文件通常叫fer2013.csv结构非常扁平三列——emotion、pixels、Usage。emotion是 0 到 6 的整数标签对应愤怒、厌恶、恐惧、开心、悲伤、惊讶、中性pixels是一串用空格分隔的灰度值长度 2304正好是 48×48Usage标记这条数据属于训练集、验证集还是测试集。这个设计在 2013 年很省空间但对现代训练流程极不友好——你没法直接用ImageFolder或tf.keras.preprocessing去读必须先解析字符串再 reshape 成图像。我见过太多人卡在这一步用pandas.read_csv读进来发现pixels列是 object 类型然后试图直接np.array(df[pixels])结果得到一个形状诡异的二维数组。正确做法是逐行 split 再转uint8。下面这段代码是我常用的解析函数直接抄就能跑import numpy as np import pandas as pd def parse_fer2013(csv_path): df pd.read_csv(csv_path) # pixels 列是空格分隔的字符串先 split 再转 uint8 pixels df[pixels].apply(lambda x: np.fromstring(x, dtypenp.uint8, sep )) # 堆叠成 (N, 2304) 再 reshape 成 (N, 48, 48) images np.stack(pixels.values).reshape(-1, 48, 48) labels df[emotion].values.astype(np.int64) usage df[Usage].values return images, labels, usage逻辑说明np.fromstring虽然在新版 NumPy 里被标记弃用但处理这种空格分隔的定长字符串依然是最快的方式如果你用的是 NumPy 1.24 以上换成np.frombuffer或np.array(x.split(), dtypenp.uint8)也行只是慢一点。reshape(-1, 48, 48)里的 -1 让 NumPy 自动推断样本数避免硬编码。labels转成 int64 是为了后续交叉熵损失不报类型错误。参数说明csv_path指向解压后的fer2013.csv如果你的 CSV 里列名不是emotion/pixels/Usage先print(df.columns)确认一下有些二次分发的版本会改列名。2.2 提取脚本一般怎么写输出目录怎么组织压缩包里附带的 Python 提取代码核心逻辑无非是「读 CSV → 按标签和 Usage 分目录 → 保存为 PNG」。但不同人写的脚本细节差异很大有的用cv2.imwrite有的用PIL.Image.save有的还会做直方图均衡化。我建议你拿到脚本后先别急着跑打开看一眼输出路径的拼接方式——这是最容易出问题的地方。一个健壮的提取脚本应该长这样import os import cv2 import numpy as np import pandas as pd def extract_fer2013(csv_path, output_root): df pd.read_csv(csv_path) # 七类表情的文件夹名顺序必须和标签 0-6 对应 emotion_names [angry, disgust, fear, happy, sad, surprise, neutral] for usage in [Training, PublicTest, PrivateTest]: for idx, name in enumerate(emotion_names): os.makedirs(os.path.join(output_root, usage, name), exist_okTrue) for _, row in df.iterrows(): emotion int(row[emotion]) usage row[Usage] # 解析像素并 reshape img np.fromstring(row[pixels], dtypenp.uint8, sep ).reshape(48, 48) save_dir os.path.join(output_root, usage, emotion_names[emotion]) # 用行号做文件名避免重名 filename f{_}.png cv2.imwrite(os.path.join(save_dir, filename), img)逻辑说明先建好三层目录Usage / 表情名再逐行写图。用df.iterrows()虽然慢但 fer2013 只有三万多行几十秒就能跑完没必要上多进程。文件名用行索引_保证唯一因为原始 CSV 没有提供图片 ID。参数说明output_root是你想输出的根目录建议放在 SSD 上机械盘写三万张小文件会明显拖慢速度。cv2.imwrite保存的是单通道灰度 PNG如果你后续要用预训练模型比如 ResNet需要在 DataLoader 里再转三通道或改第一层卷积。注意有些提取脚本会把PublicTest和PrivateTest合并成test这取决于你的评估协议。如果你要和 Kaggle 榜单对比必须保持原始划分如果只是自己跑实验合并也无妨但要在论文或报告中说明。3. 用提取出的图片目录跑通第一个训练流程3.1 目录结构确认与 Dataset 类编写提取完成后你的目录应该长这样fer2013_images/ ├── Training/ │ ├── angry/ │ ├── disgust/ │ ├── ... ├── PublicTest/ │ ├── angry/ │ ├── ... └── PrivateTest/ ├── angry/ └── ...这个结构可以直接被torchvision.datasets.ImageFolder或tf.keras.utils.image_dataset_from_directory读取。但 fer2013 是灰度图而 ImageFolder 默认按 RGB 三通道加载会导致通道数不匹配。解决办法有两种一是在 Dataset 的__getitem__里转灰度二是改模型第一层卷积的in_channels1。我一般选后者因为改数据比改模型更耗时。下面是一个自定义 Dataset 的写法兼容灰度且带基础增强import torch from torch.utils.data import Dataset from torchvision import transforms from PIL import Image import os class FER2013Dataset(Dataset): def __init__(self, root_dir, transformNone): self.samples [] self.transform transform # 遍历七类文件夹收集路径和标签 for label, emotion in enumerate(sorted(os.listdir(root_dir))): emotion_dir os.path.join(root_dir, emotion) if not os.path.isdir(emotion_dir): continue for fname in os.listdir(emotion_dir): self.samples.append((os.path.join(emotion_dir, fname), label)) def __len__(self): return len(self.samples) def __getitem__(self, idx): path, label self.samples[idx] # 强制转灰度避免 PNG 带 alpha 通道 img Image.open(path).convert(L) if self.transform: img self.transform(img) return img, label # 训练集增强随机水平翻转 轻微旋转 train_tf transforms.Compose([ transforms.RandomHorizontalFlip(p0.5), transforms.RandomRotation(10), transforms.ToTensor(), transforms.Normalize(mean[0.5], std[0.5]) ])逻辑说明sorted(os.listdir(root_dir))保证标签顺序稳定否则不同机器上跑出来的 label 映射可能不一致。convert(L)是关键fer2013 原图就是单通道但有些提取脚本保存时可能带了 alpha不转灰度会在ToTensor后变成四通道。Normalize用 0.5/0.5 是灰度图的常规操作把像素从 [0,1] 拉到 [-1,1]。参数说明RandomRotation(10)的 10 度是经验值表情识别对旋转敏感度不高再大可能引入不自然的样本。RandomHorizontalFlip对表情识别要谨慎——左右翻转会改变某些表情的语义比如厌恶的嘴角方向但 fer2013 里这种影响很小可以开。3.2 训练参数怎么设学习率、batch size 和优化器选择fer2013 只有三万多张 48×48 的小图模型容量不用太大。我试过 ResNet18、VGG11 和一个五层自定义 CNN在验证集上的准确率差距不超过 2%。如果你只是想跑通流程自定义 CNN 足够如果要刷准确率ResNet18 加预训练权重在 ImageNet 上预训练后改第一层能到 65% 左右。关键参数我列个表参数推荐值说明batch size64 或 12848×48 图很小128 也不会爆显存初始学习率1e-3Adam 优化器的默认值配合余弦退火优化器AdamW比 SGD 收敛快weight_decay 设 1e-4epoch5030 轮后基本饱和早停 patience10损失函数CrossEntropyLoss标签是单标签七分类不需要 focal loss学习率调度用CosineAnnealingLR比StepLR更平滑尤其在小数据集上。下面是一段训练循环的骨架import torch.nn as nn import torch.optim as optim from torch.optim.lr_scheduler import CosineAnnealingLR model YourCNN(in_channels1, num_classes7).cuda() criterion nn.CrossEntropyLoss() optimizer optim.AdamW(model.parameters(), lr1e-3, weight_decay1e-4) scheduler CosineAnnealingLR(optimizer, T_max50) for epoch in range(50): model.train() for imgs, labels in train_loader: imgs, labels imgs.cuda(), labels.cuda() optimizer.zero_grad() outputs model(imgs) loss criterion(outputs, labels) loss.backward() optimizer.step() scheduler.step() # 每个 epoch 后在验证集上评估 model.eval() correct 0 with torch.no_grad(): for imgs, labels in val_loader: imgs, labels imgs.cuda(), labels.cuda() preds model(imgs).argmax(dim1) correct (preds labels).sum().item() print(fEpoch {epoch}, Val Acc: {correct / len(val_dataset):.4f})逻辑说明CosineAnnealingLR的T_max设成总 epoch 数让学习率从 1e-3 平滑降到接近 0。验证时用model.eval()关掉 dropout 和 batchnorm 的统计更新否则结果会偏低。参数说明weight_decay1e-4是 AdamW 的常用值fer2013 容易过拟合正则化不能少。如果你发现训练准确率远高于验证准确率比如 90% vs 60%先把 weight_decay 调到 1e-3 试试。4. 避坑与排查提取和使用 fer2013 时最容易翻车的五件事4.1 现象提取出的图片全是全黑或全白原因np.fromstring在某些 NumPy 版本里对空字符串或异常分隔符返回空数组reshape 后变成全零。另外如果 CSV 里pixels列有缺失值fromstring会直接报错或返回空。解决在解析前加一行df df.dropna(subset[pixels])并且用np.array(row[pixels].split(), dtypenp.uint8)替代fromstring虽然慢一点但更稳。提取完随机抽几张图用cv2.imshow看一眼别等训练时才发现。4.2 现象训练时 loss 不下降准确率卡在 14% 左右原因标签映射错了。fer2013 的 emotion 标签 0-6 对应七类但有些提取脚本按文件夹字母顺序重新编号导致angry变成了 0、disgust变成了 1……如果你用 ImageFolder 读取它会按文件夹名排序顺序是 angry, disgust, fear, happy, neutral, sad, surprise——注意 neutral 和 sad 的顺序和原始标签不一致。解决要么在 Dataset 里手动指定类别顺序要么在训练前打印train_dataset.class_to_idx确认映射。最稳妥的做法是直接用原始 CSV 的标签不依赖文件夹名。4.3 现象验证集准确率比训练集高原因fer2013 的 PublicTest 和 PrivateTest 分布和 Training 有差异而且 Training 里有一些标注噪声。如果你把 PublicTest 当验证集可能会看到验证准确率偶尔高于训练准确率这不是 bug是数据集本身的特点。解决别慌这是正常现象。真正要关注的是 PrivateTest 上的表现或者自己从 Training 里切 10% 做验证。如果训练准确率持续上升但验证准确率下降那才是过拟合。4.4 现象提取脚本跑一半报 MemoryError原因一次性把整个 CSV 读进内存再逐行处理三万多行虽然不大但如果用df[pixels].apply(lambda x: np.fromstring(...))会生成一个巨大的中间 Series每个元素是 2304 长度的数组内存占用可能超过 2GB。解决用chunksize分块读取或者直接for row in df.itertuples()逐行处理不要用apply。如果已经报了 MemoryError把pd.read_csv换成pd.read_csv(csv_path, chunksize5000)循环处理每个 chunk。4.5 现象保存的 PNG 文件大小异常有的几 KB 有的几十字节原因cv2.imwrite对全零或全 255 的图会压缩得极小几十字节的文件大概率是空图或损坏图。这通常是因为pixels解析失败reshape 后全是 0。解决在保存前加一个校验if img.sum() 0: continue跳过全黑图。同时统计一下跳过了多少张如果超过 100 张说明 CSV 本身有问题需要重新下载。5. 进阶技巧用 fer2013 做迁移学习和类别不平衡处理fer2013 的七类分布极不均匀——happy有 8989 张disgust只有 547 张差了 16 倍。直接训练会让模型偏向多数类disgust的召回率低得可怜。我一般用两种手段一是加权采样二是损失函数加 class weight。加权采样用torch.utils.data.WeightedRandomSampler每个样本的权重取该类数量的倒数from torch.utils.data import WeightedRandomSampler import numpy as np # 假设 train_dataset 的 labels 已经提取出来 labels [s[1] for s in train_dataset.samples] class_counts np.bincount(labels) class_weights 1.0 / class_counts sample_weights [class_weights[label] for label in labels] sampler WeightedRandomSampler(sample_weights, num_sampleslen(sample_weights), replacementTrue) train_loader DataLoader(train_dataset, batch_size128, samplersampler)逻辑说明WeightedRandomSampler会让少数类被抽到的概率和多数类接近replacementTrue表示有放回采样保证每个 epoch 的样本数不变。num_samples设成训练集大小这样每个 epoch 看到的样本总数和原来一致。参数说明class_weights用倒数而不是平方根倒数是为了让少数类获得足够的关注。如果你发现disgust的召回率上来了但整体准确率掉了 2-3 个点这是正常的权衡。另一个技巧是用预训练模型。虽然 fer2013 是灰度图但你可以把单通道复制成三通道加载 ImageNet 预训练的 ResNet18然后只微调最后几层。我试过冻结前三个 block只训练 layer4 和 fc20 个 epoch 就能到 66% 的验证准确率比从头训练快一倍。注意改第一层卷积为nn.Conv2d(3, 64, kernel_size7, stride2, padding3, biasFalse)然后把你的灰度图在 ToTensor 后repeat(3, 1, 1)变成三通道。最后说个我自己的习惯每次拿到一个新的 fer2013 提取包先别急着跑训练花五分钟写个脚本统计每个类别的图片数量、随机抽 20 张拼成网格图看一眼、检查一下文件大小分布。这三步能帮你提前发现 80% 的数据问题。表情识别这个方向数据质量比模型结构重要得多而 fer2013 的标注噪声和类别不平衡是出了名的别等到训练了三小时才发现disgust类全是糊的。希望帮到你。本文还有配套的精品资源点击获取