情感识别中恐惧、悲伤样本太少怎么办?一套少数类数据优化实践
在做情感识别的时候,有一个问题非常容易被忽略:
不是模型不够大,而是数据根本不够。
尤其是把情感进一步细分之后,像“喜悦”“中性”这类情感通常比较容易收集,而“恐惧”“悲伤”“失望”“焦虑”等类别的数据量明显偏少。
例如,一个实际项目的数据统计可能是这样:
中性 52000 喜悦 18600 愤怒 9200 惊讶 6800 悲伤 2300 恐惧 1100 失望 760如果直接拿这样的数据训练模型,最终很容易出现一种情况:
整体 Accuracy 看起来不错,但是少数情感几乎识别不出来。
这类问题在语音转写后的文本情感识别、会议内容分析等场景中尤其明显。类似熙瑾会悟这样的智能会议应用,如果进一步做情绪分析,也会遇到同样的数据分布问题。
下面记录一下比较完整的解决思路。
一、先别急着换模型,先确认到底是不是数据问题
遇到少数类识别效果差,我一般不会第一时间去换 BERT、RoBERTa 或者其他更大的模型。
先统计训练集:
from collections import Counter labels = [ "neutral", "neutral", "sad", "fear", "happy", "sad" ] counter = Counter(labels) for label, count in counter.most_common(): print(label, count)如果发现:
neutral 52000 happy 18600 angry 9200 sad 2300 fear 1100那么基本可以确定存在比较严重的类别不均衡。
这里有一个容易犯的错误:
不要只看 Accuracy。
假设测试集有 10000 条数据,其中 7000 条都是中性,那么模型全部预测成中性,也能获得 70% Accuracy。
所以情感识别更应该关注:
Precision
Recall
F1-score
Macro-F1
每个类别的 Confusion Matrix
尤其是 Macro-F1。
二、第一步:重新清洗少数类数据
数据少并不可怕,真正麻烦的是:
少,而且还不干净。
例如“悲伤”类别里可能混入:
我真的太开心了 这个结果让我有点难过 哈哈,这次终于成功了 我不知道该怎么办这些样本如果标签本身就不准确,后面再怎么增强都没有意义。
因此建议先做一轮人工抽检。
可以建立这样的数据检查表:
| 检查项 | 处理方式 |
|---|---|
| 标签明显错误 | 删除或重新标注 |
| 重复文本 | 去重 |
| 极短文本 | 根据任务决定是否保留 |
| 无情绪表达 | 重新确认是否属于中性 |
| 多情绪表达 | 允许多标签或制定主情绪规则 |
| 上下文缺失 | 补充上下文 |
| ASR错误 | 回查原始音频 |
这里特别需要注意语音场景。
例如:
“我真的太难过了”如果 ASR 转成:
“我真的太难过啦”通常问题不大。
但如果变成:
“我真的太难过了吧”甚至出现关键词漏识别,就可能影响情绪分类。
所以语音情感数据最好保留:
audio_id text emotion speaker confidence source后续才能追溯错误。
三、第二步:不要简单复制少数类
最直接的办法是:
悲伤 1000 ↓ 复制 ↓ 悲伤 10000虽然数量上平衡了,但模型实际上看到了大量完全相同的句子。
这样很容易造成过拟合。
所以更推荐:
少数类过采样 + 数据增强
例如原始数据:
恐惧:1000 悲伤:2000 中性:50000可以调整成:
恐惧:5000 悲伤:6000 中性:20000注意,这里的目标并不是强行做到 1:1。
很多时候只需要把极端的数据分布拉回来即可。
imbalanced-learn本身就提供了 RandomOverSampler 等重采样工具,可以针对少数类进行有放回采样。
四、第三步:文本增强比盲目复制更重要
对于“恐惧”和“悲伤”这类少数情感,可以做一些受约束的数据增强。
例如:
原始: 这个结果让我很难过。 增强: 这个结果真的让我有些难过。 这个结果让我感觉很失落。 看到这个结果,心里挺不是滋味的。常见方法包括:
同义表达替换
句式改写
局部词语替换
回译
大模型辅助生成
基于模板生成
但是这里有一个原则:
增强之后,情感标签必须保持稳定。
例如:
我有点害怕。不能增强成:
我一点都不害怕。否则就是“数据增强把标签改没了”。
因此建议给增强程序增加一个简单的质量过滤流程:
原始样本 ↓ 文本增强 ↓ 语义相似度检查 ↓ 情感一致性检查 ↓ 人工抽检 ↓ 进入训练集五、第四步:训练集可以重采样,但验证集不要动
这是实践中特别重要的一点。
假设:
原始数据 ↓ Train / Validation / Test ↓ 只处理 Train不要对 Validation 和 Test 做过采样。
正确流程应该是:
原始数据 ↓ 数据清洗 ↓ Train / Validation / Test ↓ ├── Train → 增强 + 重采样 │ ├── Validation → 保持原始分布 │ └── Test → 保持原始分布这样最终测出来的指标才更接近真实线上效果。
否则验证集也被人为“平衡”,指标很容易虚高。
六、第五步:加入 Class Weight
数据层面处理完之后,还可以从 Loss 层面解决。
对于类别数量差异比较大的情况,可以给少数类别更高的权重。
一个常见的 balanced 权重形式是:
weight_i = N / (C × n_i)其中:
N:训练样本总数
C:类别数量
n_i:第 i 个类别的样本数量
scikit-learn 的class_weight="balanced"就采用了与类别频率成反比的方式计算权重。
PyTorch 中可以直接这样实现:
import torch import torch.nn as nn class_counts = [52000, 18600, 9200, 2300, 1100] total = sum(class_counts) num_classes = len(class_counts) weights = [ total / (num_classes * count) for count in class_counts ] weights = torch.tensor(weights, dtype=torch.float) criterion = nn.CrossEntropyLoss( weight=weights )这样模型计算 Loss 时,错误预测“恐惧”的代价就会高于错误预测“中性”。
七、第六步:少数类特别难时,可以尝试 Focal Loss
Class Weight 解决的是:
类别之间的重要程度不同。
Focal Loss 更关注:
那些模型很难判断的样本。
它的基本形式为:
FL(pt) = -α(1 - pt)^γ log(pt)其中:
pt:模型对真实类别的预测概率α:类别权重γ:聚焦参数
当模型已经非常确定一个样本时:
pt → 1那么:
(1 - pt)^γ → 0这个样本对 Loss 的贡献就会降低。
而那些模型一直判断错误的困难样本,会获得更高的训练关注度。
一个简单实现如下:
class FocalLoss(nn.Module): def __init__(self, alpha=None, gamma=2.0): super().__init__() self.alpha = alpha self.gamma = gamma def forward(self, logits, targets): ce = nn.functional.cross_entropy( logits, targets, weight=self.alpha, reduction="none" ) pt = torch.exp(-ce) loss = (1 - pt) ** self.gamma * ce return loss.mean()实际项目里建议先使用:
gamma = 2作为初始值,再根据验证集表现调整。
八、不要一上来同时使用所有方法
这是我比较建议注意的一点。
如果一开始同时使用:
最后效果变好了,你反而不知道到底是哪一步起了作用。
而且权重过大还有可能造成新的问题:
模型过度关注少数类,开始误报。
因此更合理的实验顺序是:
每一步都记录:
Macro-F1 Fear Recall Sad Recall Fear Precision Sad Precision Overall Accuracy最终形成实验表。
例如:
| 方案 | Macro-F1 | Fear Recall | Sad Recall |
|---|---|---|---|
| Baseline | 0.71 | 0.38 | 0.45 |
| 数据增强 | 0.75 | 0.49 | 0.53 |
| +Oversampling | 0.77 | 0.56 | 0.59 |
| +Class Weight | 0.79 | 0.61 | 0.64 |
| +Focal Loss | 0.80 | 0.66 | 0.68 |
具体数值只是示例,实际项目应该使用自己的验证结果,不能直接套用。
九、最后还要做一次“少数类专项测试”
普通测试集跑完以后,我建议额外建立一个:
Rare Emotion Test Set专门收集:
恐惧 悲伤 失望 焦虑 困惑这些难分类样本。
然后单独统计:
from sklearn.metrics import classification_report print( classification_report( y_true, y_pred, target_names=[ "neutral", "happy", "angry", "sad", "fear" ] ) )重点观察:
sad recall fear recall而不是只看整体 Accuracy。
十、最终形成一套完整的数据处理链
整个方案可以总结成:
真正做情感识别时,我认为最值得投入时间的其实不是“换一个更大的模型”,而是把这条数据链路跑通。
尤其是恐惧、悲伤这种天然比较稀缺的情绪,数据质量往往比模型参数量更加重要。
如果后续发现模型仍然存在:
悲伤 → 中性 恐惧 → 中性 恐惧 → 悲伤这样的集中误判,再进一步检查上下文、ASR错误、声学特征以及标签边界,往往比继续堆训练轮数更有效。
总结
情感数据稀缺不是单纯的“数据量少”问题,而是一个完整的数据分布问题。
比较稳妥的工程方案是:
先清洗,再增强;先调整数据分布,再调整 Loss;最后通过独立测试集验证少数类到底有没有真正提升。
对于实际项目,建议把每一次数据处理都保留版本,例如:
dataset_v1 dataset_v2_clean dataset_v3_aug dataset_v4_resample同时保存每次实验的:
模型版本 数据版本 类别分布 训练参数 Macro-F1 各类别 Recall 混淆矩阵这样当线上再次出现情绪误判时,就能快速定位到底是数据问题、模型问题,还是阈值和场景分布问题,而不是重新从头排查。