2026.10.16 更新 · 修正“开心 → 轻蔑”误判
实验室现已使用两个经过验证的 v2 模型。网页判断阈值仍为 35%,八类输出全部保留。下面先记录这次修正;后面的十节保留 10 月 14 日原始训练实验,便于对照。
复核确认:博客上的权重与本地训练产物完全相同,类别顺序也一致。核心问题出在混合数据的训练损失:240,369 张训练图中有 203,110 张(84.50%)来自不标注轻蔑的来源,旧实现把轻蔑从这些图片的 softmax 分母中删除。对其中 48,343 张标注为开心的图片,即使模型原始轻蔑分数很高,轻蔑输出收到的纠错梯度仍是零。单元测试直接复现了这个现象。
此外,1,840 张轻蔑正样本中的 1,696 张(92.17%)来自第三方 AffectNet 镜像。抽查可见闭嘴微笑、嘴角轻抬等边界含糊的图片。目录与 CSV 一致只能检查内部一致性,不能证明人类标签完全正确;这里把它作为来源集中与标注歧义的风险,没有凭肉眼把测试标签改掉。
修正后,已知标签在完整八类分母中竞争,让开心等已标注样本也能纠正过高的轻蔑分数;标签平滑仍只分配给来源能标注的类别。七类数据的标签未必覆盖所有表情,因此这是一项需要验证的建模假设,不代表“没标注的类别永远不存在”。相同初始化、数据、随机种子和八轮训练的快速版对照中,旧损失在验证集仍有 326 次开心→轻蔑误判;新损失明显减少该错误。我们进一步比较了冻结特征的定向修正,以限制其他类别的变化。
最终快速版采用只训练最后一层轻蔑输出的权重与偏置,冻结特征和其他七类输出;精度版采用全网络小学习率微调。候选只能用 39,018 张验证图选择,要求开心误报轻蔑至少减少一半、开心召回率不下降、总体 Macro-F1 下降不超过 1 个百分点、轻蔑召回率至少保留原来一半;通过条件后按验证 Macro-F1 选择。然后才评估原有 78,689 张外部测试和固定 FER 测试。没有把测试图片加入训练,去重与分组划分完全保持原样。
| 模型 | 开心→轻蔑张数 | 开心误判率 | 开心召回率 | 轻蔑精确率 | 轻蔑召回率 | 总体准确率 |
|---|---|---|---|---|---|---|
| 快速版 | 592 → 59 | 3.44% → 0.34% | 70.93% → 72.86% | 11.00% → 46.46% | 45.25% → 29.79% | 51.35% → 52.49% |
| 精度版 | 540 → 75 | 3.14% → 0.44% | 72.18% → 73.45% | 15.06% → 48.65% | 53.90% → 35.89% | 56.95% → 58.21% |
上述开心误判率的分母都是 17,204 张标注为开心的外部测试图;表格比较不使用拒判阈值,以免把“更多拒绝回答”当成模型变准。轻蔑精确率和召回率同时列出,暴露减少误报的代价。新模型重新在原校准集上拟合温度,并经过 ONNX 数值一致性与 CPU 速度测试。模型结构、输入分辨率和浏览器 CPU 运行方式保持一致。
这仍不能保证每个人、每种手机光照都同样改善;用户本人的误判视频没有纳入本次评估。可见表情也不等于真实心理状态。下载完整诊断、对照和评估 JSON,或下载 快速版 v2 / 精度版 v2。原版权重与评估仍保留,方便复核和回退。
01 · 这次真正训练了什么
见微 JEV 是一个识别可见面部表情的小型视觉分类系统。输入一张人脸,输出中性、开心、惊讶、悲伤、愤怒、厌恶、恐惧、轻蔑八类分数;分数不足时返回“不确定”。它不生成文字,也不需要调用大语言模型。
名字中的 Jev 来自 TypeSafe 的 System One / Jev 介绍:这里借鉴的是快速、非生成式、结构化决策的产品方向。实际实现采用 MobileNetV3、监督学习和 ONNX,没有使用 TypeSafe Jev 权重,也不是其 RLCD 方法的复现。
本次提供两个已经完成训练的版本:expanded_fast(扩充快速版)和 expanded_accurate(扩充精度版)。二者使用相同的最终训练集合,区别是网络容量、内部输入尺寸与训练参数。打开实验室即可切换,并使用照片、实时摄像头、多脸选择、阈值调整、历史记录和 JSON 导出。
这篇文章描述的是 2026 年 10 月 14 日完成的灰度 MobileNetV3 版本。彩色高分辨率重训、人脸专用预训练、蒸馏和新一轮抗噪声训练仍是后续实验方向,没有把它们写成已完成的能力。
02 · 数据从哪里来,又有多少真正进入训练
下载任务共 14,531 个,保存约 66.15 GB。预处理后得到 555,376 个候选人脸,合并阶段因重复、标签冲突、评估集保护和分组规则等排除 183,007 个,最终留下 372,369 个样本,分配到五个互不混用的集合。下载文件数、候选图片数和训练图片数是三个不同的数字。
| 集合 | 数量 | 用途 |
|---|---|---|
| 训练集 | 240,369 | 更新模型参数 |
| 验证集 | 39,018 | 选最佳轮次、决定早停 |
| 校准集 | 10,933 | 拟合温度与选择拒判阈值 |
| 固定 FER 测试集 | 3,360 | 与原模型在同一批图片上比较 |
| 多来源外部测试集 | 78,689 | 检查跨来源表现 |
最终训练来源如下。表中按去重后保留的代表图片计数;一个合并包包含其他数据集的图片时,不会把同一张图再计算一次。
| 来源 | 实际训练样本 |
|---|---|
| ferplus | 25,000 |
| rafdb | 8,772 |
| affectnet | 12,259 |
| expw | 55,909 |
| yolo_emotions | 7,889 |
| mutfer2024 | 5,843 |
| ferv39k_16f | 46,313 |
| caer | 19,010 |
| caers | 38,126 |
| cremad | 16,446 |
| ravdess | 4,802 |
| AffectNet+ 官方验证包 | 0;3,979 张仅作外部测试 |
用户补充的 YOLO emotions 和 AffectNet 镜像都经过检查与去重后加入训练。AffectNet+ 官方 Kaggle 包实际只有验证数据:5,500 张中,4,000 张属于八类有效表情,另有 1,500 张无脸、不确定或非表情图片;有效图片去重后保留 3,979 张,仅进入外部测试。不能把这个验证包当作完整 AffectNet+ 训练集。
最终八类训练样本分别为:中性 66,962,开心 58,353,惊讶 22,091,悲伤 32,174,愤怒 27,764,厌恶 16,868,恐惧 14,317,轻蔑 1,840。中性约为轻蔑的 36 倍,样本量不平衡依然存在。本文与网页不分发原始训练图片;数据获取与使用应分别查看各来源的原始说明。
03 · 去重之前,先处理标签与数据泄漏
一个容易忽略的问题是:文件名、目录标签和标注文件不一定互相一致。AffectNet 镜像共有 31,002 张图片,其中 11,377 张的目录类别与 labels.csv 不一致。本次保守保留二者一致的 19,625 张候选,再进行跨来源去重与划分。最终进入训练的是 12,259 张。
另有 4,334 组重名文件,其中绝大多数内容不同。像 ffhq_1.png 这样的名字不能直接作为全局身份。YOLO 合并包还重新编号了 FER 图片,因此通过原图像素内容匹配 FER 来源,不能相信文件名中的数字。
- 先保护留出集合:原 FER 测试的 3,360 张像素和标签保持不变;已知原始来源、视频或演员分组遵守留出优先级。
- 完全重复:把人脸统一到 48×48 的规范像素表示,计算 SHA-256,并结合原图来源匹配。
- 严格近重复:要求 pHash 汉明距离 ≤ 4、dHash 距离 ≤ 6,并同时满足归一化相关系数 ≥ 0.985。只靠感知哈希相近不足以认定重复。
- 同原图不同裁剪:对可追溯的同一原始人脸只保留一个代表。训练重复项若主要标签冲突,连已有代表也移除。
- 保护整组:重复证据一旦连接视频、演员或来源分组,留出优先级传播到整个关联组,避免同一片段的相邻帧跨集合。
外部测试还排除了原模型训练、验证和校准时已见过的图片,使扩充前后的比较更可信。自动检查覆盖有限且归一化的标签、合法类别掩码、正样本权重、规范像素不重叠、已知来源/组不重叠,以及固定 FER 测试不变。
仍有边界:本流程没有用人脸身份模型给所有网络照片分配人物 ID,因此未知的同人跨来源重叠仍可能存在;大幅改变裁剪或姿态的近重复可能漏检。CAER-S 文件名不暴露父视频 ID,未通过内容关联的同源视频帧也可能跨来源出现。MUTFER 缺少可靠参与者 ID,所以只用于训练。这是一套尽量严格、可审计的去重方案,不是“完全无泄漏”的保证。
04 · 一张图片如何变成模型输入
自然图片先由 YuNet 检测人脸,再裁剪、转灰度、缩放为 96×96 的无符号 8 位像素。训练准备阶段也使用相应来源的人脸框或检测器,剔除过小、过于平坦或无法确认目标人脸的样本。视频来源每个片段抽取三个间隔帧,并降低这些样本的训练权重,因为视频级标签未必适用于每一帧。
原图 / 摄像头
→ YuNet 人脸检测
→ 人脸框四周约 3% 扩展并裁剪
→ 灰度 + 面积重采样到 96×96
→ uint8 [batch, 96, 96]
→ 网络内缩放到 112×112 或 144×144
→ 复制为三通道 + ImageNet 归一化
→ MobileNetV3 → 八类 logits
→ 温度校准 + softmax → 八类分数
这里的三通道是同一灰度图的三份复制,不会恢复已经丢失的颜色或细节。选用灰度和较低分辨率是当时的速度、显存与数据统一化取舍,也是后续提高精度时值得重新评估的环节。
当前版本按检测框裁剪,没有用眼睛关键点把歪头旋正,也没有做多帧概率平滑。训练时使用随机水平翻转、约 ±12° 仿射旋转、0.9–1.1 缩放、约 ±6% 平移和亮度/对比度扰动。训练增强只影响训练图片,验证、校准和推理走确定性的预处理。
05 · 两个模型的训练配方
基础骨干来自 torchvision 的 ImageNet 预训练 MobileNetV3,最后一层改为八类。先有原 FER 模型,再以对应原模型的最佳检查点初始化本次扩充训练;本轮不是从随机参数开始。
| 配置 | 扩充快速版 | 扩充精度版 |
|---|---|---|
| 骨干 | MobileNetV3-Small | MobileNetV3-Large |
| 参数量 | 1,526,056 | 4,212,280 |
| 数据输入 / 网络内部尺寸 | 96×96 / 112×112 | 96×96 / 144×144 |
| ONNX 文件 | 约 6.12 MB | 约 16.84 MB |
| 骨干初始学习率 | 0.00015 | 0.00010 |
| 分类头学习率 | 骨干的 4 倍 | 骨干的 4 倍 |
| 最大 / 实际轮次 | 20 / 16,触发早停 | 24 / 24 |
| 选中检查点 | 第 10 轮 | 第 21 轮 |
| 批量大小 | 192 | 192 |
优化器为 AdamW,权重衰减 0.01,余弦学习率调度降到初始值的 8%。第一轮只训练分类头并冻结骨干的 BatchNorm 统计,之后解冻骨干。损失使用 soft-target 交叉熵、2% 标签平滑和梯度范数裁剪(最大 5)。来源中若有多人投票分布,就保留软标签,避免强行抹掉标注歧义。
类别不平衡通过逆频率的 0.4 次方加权,最大截到 3 倍;来源权重使用逆频率的 0.35 次方、最大 2 倍,再乘以样本可靠性系数,并归一化到平均 1。这里是损失加权,不是简单复制少数类图片。只有七类的来源对缺失类别使用损失掩码;这避免把“没有标注轻蔑”直接视为“确定不是轻蔑”,但也会减少轻蔑类别的负样本约束,值得后续单独比较。
最佳轮次按验证集选取:对每个来源计算 0.6 × Macro-F1 + 0.4 × Accuracy,再对来源等权平均;其中验证来源内的 Macro-F1 使用该来源实际出现的标签。连续六轮没有改善就早停。校准集和测试集不用于选训练轮次。
训练设备为 NVIDIA GeForce RTX 5080,PyTorch 2.11.0 + CUDA 12.8,使用 BF16 混合精度,随机种子 20261014。在数据已经准备好的前提下,日志记录的扩充训练循环约为快速版 408 秒、精度版 867 秒;不含下载、清洗、导出与评估。这个数字只描述本次硬件和实现,不能当作其他电脑的耗时承诺。cuDNN 自动调优与 BF16 也不保证逐位复现。
06 · 分数、阈值与准确率是不同的东西
网络输出 logits 后,在独立的混合来源校准集上拟合一个温度参数:p = softmax(logits / T)。快速版的 T 为 1.24095,精度版为 2.20818。温度是正数,不改变最高分是哪一类;它调整分数的尖锐程度,使分数与经验正确率更相符,但不能保证所有来源都同样校准。
训练产物中的原始阈值按校准集确定。快速版在约 83.16% 阈值下,接受比例只有 14.67%,被接受样本准确率约 90.02%。精度版没有达到预设的 90% 目标,于是使用校准置信度中位数作为明确的回退规则:阈值约 48.18%,接受比例 50%,接受样本准确率约 70.39%。
互动网页根据本次使用偏好,将两个版本的默认判断阈值统一设为 35%,仍可自行调整。这会减少“不确定”,也可能增加明确输出中的错误。它没有重新训练网络,也没有改变每张图最高分的类别。页面上的 80% 分数不等于“这个模型准确率 80%”。
“只看模型愿意回答的样本”的准确率应同时报告覆盖率。例如本项目精度版在固定 FER 测试集上,35% 阈值接受约 80.30% 的样本,接受部分正确率约 87.62%;这些都是该测试集上的条件统计,不能移用于手机摄像头,也不能替代全部样本的准确率。
07 · 扩充数据后的真实收益与退步
下面四个版本在同一组 78,689 张自定义外部测试图片上比较。外部测试使用 PyTorch FP32,固定 FER 测试使用导出的 ONNX 模型;导出数值误差小于 0.000006。外部 Macro-F1 对八类等权,有助于发现少数类是否被总体准确率掩盖。
| 模型 | 外部准确率 | 外部 Macro-F1 | FER 准确率 | CPU 分类中位延迟 |
|---|---|---|---|---|
| 原快速版 | 39.06% | 20.75% | 81.04% | 0.47 ms |
| 扩充快速版 | 51.35% | 45.13% | 79.85% | 0.47 ms |
| 原精度版 | 40.73% | 22.07% | 83.54% | 1.17 ms |
| 扩充精度版 | 56.95% | 52.08% | 80.71% | 1.12 ms |
扩充精度版的外部准确率从 40.73% 提高到 56.95%,增加 16.22 个百分点;Macro-F1 从 22.07% 到 52.08%。但固定 FER 准确率从 83.54% 下降到 80.71%,ExpW 从 66.86% 下降到 65.81%,FERV 从 32.72% 下降到 29.17%。扩充数据改善了整体跨来源表现,并没有让每个来源都变好。
扩充精度版的八类外部 F1 分别是:中性 56.54%、开心 72.19%、惊讶 52.06%、悲伤 53.38%、愤怒 46.06%、厌恶 51.37%、恐惧 61.54%、轻蔑 23.54%。轻蔑仍是明显短板。常见混淆包括悲伤→中性、愤怒→中性,以及中性与开心互相混淆。
外部测试中的 AffectNet+ 子集准确率为 43.23%,RAF 子集为 78.52%,FERV 视频抽帧为 29.17%。这些差异反映数据分布、标签方式和任务难度不同。视频抽帧使用片段级标签,其中一帧可能没有显示整个片段的主要表情。
本项目经过清洗的混合集合不是论文标准协议。它的 56.95% 不能直接与别人标准 RAF-DB、AffectNet 或七分类实验的数字比较。这里也没有覆盖全部人群、设备、光照与姿态;网页显示的是可见表情分类,不是对人的真实内心感受作判断。
08 · 从 CUDA 训练到 CPU 和浏览器推理
导出模型时,把网络内部缩放、三通道归一化、分类网络、温度除法和 softmax 一起封装进 ONNX(opset 17)。输入仍是 uint8 [batch,96,96],输出为 [batch,8]。这样调用者不用分别实现温度校准或猜测类别顺序。
桌面 Python 版本使用 ONNX Runtime 的 CPUExecutionProvider。预热后,快速版分类中位延迟约 0.47 ms,精度版约 1.12 ms;在固定 640×480 单人脸测试图上,检测、裁剪和分类合计中位延迟约 9.30 / 10.43 ms。前一项不含人脸检测,后一项不含摄像头采集、解码和显示,均不能直接当作手机 FPS。
博客版使用 ONNX Runtime Web 1.30.0 的 WebAssembly CPU 后端,在独立 Web Worker 中运行。两个分类器的 ONNX 文件直接复制自训练产物,没有量化、蒸馏或更换权重。YuNet 只调整输入/输出的动态尺寸声明,以接受与 OpenCV 一样的 32 像素倍数补边;检测权重保持不变。
浏览器先下载模型,之后照片和相机像素只在本机内存与工作线程间传递。当前模型加载完后,同一页面的识别不需要向服务器发送图片。首次打开、刷新或首次切换未缓存模型仍可能需要联网;网站正常访问日志和模型下载记录不属于图片推理上传。
浏览器的图像解码、缩放和重采样与 OpenCV 可能有细微舍入差异,因此“使用同一权重”不等于“整条图像链逐位相同”。移植时应分别验证:固定灰度输入的概率一致性,以及真实照片从检测到分类的行为。网页端没有完成全量测试集重测,本文准确率仍明确指桌面评估协议。
摄像头预览独立播放,优先请求 60 FPS;识别目标默认 30 次/秒,可调整到最高 60 次。两项都是目标,实际数值由硬件和处理能力决定。页面分别统计画面帧率与识别频率,每次只处理一帧,避免排队;暂停时冻结最近完成识别的对应画面,关闭相机时停止设备轨道。浏览器 CPU 也能使用,不要求独立 GPU。
09 · 如何复核和复现
项目里的流程脚本按“下载 → 预处理 → 去重合并 → 验证划分 → 训练 → 导出校准 → 外部比较 → 速度测试”运行。下面展示关键命令;需要先准备相同来源的数据、原 FER 检查点和项目依赖,不能只凭两行命令从空目录复现全部结果。
python prepare_expanded_sources.py --source affectnet
python merge_expanded_data.py
python validate_expanded_data.py
python train.py --run expanded_accurate --arch large --size 144 \
--epochs 24 --batch-size 192 --lr 0.0001 --patience 6 \
--class-balance 0.4 --selection source_macro --missing-class-weight 0 \
--data-dir data/expanded --init-checkpoint runs/accurate/best.pt
python train.py --run expanded_fast --arch small --size 112 \
--epochs 20 --batch-size 192 --lr 0.00015 --patience 6 \
--class-balance 0.4 --selection source_macro --missing-class-weight 0 \
--data-dir data/expanded --init-checkpoint runs/fast/best.pt
python evaluate_export.py --run expanded_accurate
python evaluate_export.py --run expanded_fast
python compare_expanded_models.py
命令使用类 Unix 的反斜杠换行示意;Windows PowerShell 可将每条命令写在一行。项目保存了随机种子、硬件、参数、训练历史、划分内容指纹和最佳轮次。复现实验不仅要使用同名数据集,还要确认清洗规则、保留图片和划分内容相同。
- 下载数据划分与去重摘要 JSON:来源、数量、排除规则、限制与内容指纹。
- 下载四个版本的评估摘要 JSON:全部样本成绩、阈值条件统计与桌面 CPU 速度。
- 快速版模型元数据 / 精度版模型元数据:输入格式、类别顺序、温度与原始校准阈值。
- 下载快速版 ONNX / 下载精度版 ONNX。
类别顺序固定为 neutral, happy, surprise, sad, angry, disgust, fear, contempt。读取输出时应使用元数据顺序;网页为便于阅读调整了分数条的展示顺序,不应据此重新解释模型输出数组。
10 · 下一步如何验证精度提升
第一步值得试验的是从原始彩色图片重新制作更高分辨率的人脸,并让训练与推理使用一致的关键点对齐;单纯把现有 96×96 灰度图放大不会恢复细节。第二步是比较人脸专用预训练骨干,再针对轻蔑等薄弱类别审计难例、来源差异和不明确标签。第三步才是把更强教师的知识蒸馏给小模型,验证是否能保留 CPU 速度。
摄像头应用还需要单独的视频评估:短时信息融合可能减少抖动,也会增加响应延迟。对自己常用的手机,可以收集另一天、另一次拍摄的标注样本,按人物或拍摄场次隔离验证,不能把相邻帧随机切成训练与测试后就宣布提升。
这些改进应逐项做对照:用验证集选方案,再用未参与训练和调参的留出数据验收,同时记录全部样本准确率、Macro-F1、各类别召回、拒判覆盖率与实际端到端延迟。提高阈值、降低阈值或者让画面更流畅,都不能代替模型精度的独立证据。
实现参考:torchvision MobileNetV3、OpenCV YuNet、ONNX Runtime Web 部署文档。页面随附 ONNX Runtime 许可与 YuNet 许可;这些依赖的许可不替代各训练数据来源的条款。