正样本几万、负样本个位数:极端类别不平衡下怎么训模型(全文可跑)

核心结论 :当正样本几万、负样本只有个位数时,直接训练等于白练 ------模型会「聪明地」全猜正类,accuracy 高达 99.99% 却一个负样本都抓不到。正确的姿势是四步走:① 先把评估指标从 accuracy 换成 PR-curve / F1 / recall;② 用 focal loss 或加权交叉熵重设损失函数;③ 对少数类做 SMOTE 过采样或数据增强;④ 最后调低预测阈值(threshold),在 precision 和 recall 之间找回平衡。极端到个位数时,别再想「把它当分类问题」,直接转成异常检测 / one-class 问题更现实。

目录

    1. 一句话结论
    1. 为什么会翻车:accuracy 的陷阱
    1. 第一步:先换评估指标
    1. 第二步:换损失函数(focal loss / 加权交叉熵)
    1. 第三步:数据层过采样
    1. 第四步:调预测阈值
    1. 完整可运行代码(PyTorch 端到端)
    1. 极端情形:个位数样本怎么处理
    1. 对照表:症状 → 原因 → 解法
    1. 判断收尾

1. 一句话结论

先建立一个直觉 🎭:极端类别不平衡就像一场只有 5 个坏人、几万个好人的面试 ------如果你只回答「全录好人」,正确率高达 99.99%,面试官还夸你「准确率真高」,可那 5 个坏人一个都没抓出来。我们要的不是「猜对大多数」,而是「把少数类揪出来」。

所以这套方法的核心不是「怎么训得更准」,而是「怎么让模型在意那几个少数样本」:

  • 损失函数:让少数类的错误代价更高(focal loss、加权 CE)。
  • 采样:把少数类的样本变多(SMOTE、过采样、增强)。
  • 阈值:模型默认在 0.5 处切一刀,对极端不平衡要往下调。
  • 评估 :别再用 accuracy,用 PR 曲线(Precision-Recall curve) 与 F1。

一句话:极端不平衡下,训练目标不是「整体准确率」,而是「少数类的召回率(recall)」。

2. 为什么会翻车:accuracy 的陷阱

绝大多数模型默认用交叉熵(cross-entropy) 损失 + accuracy 评估。在平衡数据集上这没问题,但一旦负样本只有个位数,就会立刻崩坏。

陷阱一:模型「摆烂」能拿高分。

假设训练集 30000 个正样本、10 个负样本。一个模型啥都不学,见到什么都预测「正类」,那么:

ext{accuracy} = rac{TP + TN}{TP + TN + FP + FN} = rac{30000}{30010} approx 99.97%

它一个负样本都没抓到(recall = 0),但 accuracy 接近满分。这就是为什么「训练 loss 一直在降、accuracy 99%,上线却是个废物」的根源。

陷阱二:梯度被正样本淹没。

交叉熵的梯度按样本加权,30000 个正样本贡献的梯度会把 10 个负样本的梯度彻底稀释。模型几乎没有动力去「纠正」对负样本的判断,因为纠正带来的 loss 下降微乎其微。

打个比方 🎭:**你在大海上喊一声「这里有个落水的」(负样本),周围几千个人都在喊「我没事」(正样本),你的声音根本传不到救生员耳朵里。**第一步:先换评估指标

训练之前,先想清楚「什么才算好」。在极端不平衡下,accuracy 是无效指标,必须换成下面这些:

指标 关注什么 极端不平衡下是否可用
Accuracy 全部预测对的比例 ❌ 严重失真,几乎无意义
Precision(精确率) 预测为正的里面,真正的比例 ✅ 可用,但单独看会骗人
Recall(召回率) 真正的正样本里,被抓住的比例 ✅ 最该盯的指标
F1-Score Precision 与 Recall 的调和平均 ✅ 推荐,均衡两个方向
PR 曲线 / PR-AUC Precision 随 Recall 变化的曲线下面积 ✅ 强烈推荐,最能反映少数类表现
ROC-AUC 对类别比例不敏感 ⚠️ 极端不平衡下会「虚高」,不如 PR-AUC 真实

为什么极端不平衡下 PR-AUC 比 ROC-AUC 更可信? ROC-AUC 的横轴是 FP(假正例)占比,在负样本极少的场景,FP 占比本身就很小,曲线容易「看起来很漂亮」;而 PR-AUC 直接以少数类的 precision / recall 为轴,更多表现在少数类上的真实能力。

实操建议 :训练时用 validation F1 或 macro-F1 选模型,不要看 loss 或 accuracy。很多研究(如 2025 年某随机森林 vs 深度模型研究)也表明,用 F1 优化的模型在少数类上的表现远优于只看 accuracy 的模型。

4. 第二步:换损失函数(focal loss / 加权交叉熵)

评估指标换了,损失函数也要换。这是最重要的一步。

4.1 加权交叉熵(Weighted Cross-Entropy)

最简单有效。给不同类别赋一个权重,少数类的权重设大、多数类的权重设小。公式:

mathcal Lwce =−wtcdotlog(pt)mathcal{L}_{wce} = -w_t cdot log(p_t) mathcalLwce=−wtcdotlog(pt)

其中 wt w_t wt 就是类别 tt t 的权重。常用权重设为类别的逆频率:

w_{neg} = rac{N_{total}}{2 cdot N_{neg}}, quad w_{pos} = rac{N_{total}}{2 cdot N_{pos}}

  • Npos =30000 N_{pos} = 30000 Npos=30000, Nneg =10 N_{neg} = 10 Nneg=10 时, wneg w_{neg} wneg 会是 wpos w_{pos} wpos 的几千倍,保证每个负样本的梯度都被放大。

4.2 Focal Loss(焦点损失)

Focal Loss 是加权 CE 的进阶版,由 Lin 等人为解决类别不平衡 + 难易样本不平衡而提出,公式:

mathcal Lfocal =−alphat(1−pt)gammalog(pt)mathcal{L}_{focal} = -alpha_t (1 - p_t)^gamma log(p_t) mathcalLfocal=−alphat(1−pt)gammalog(pt)

  • alphat alpha_t alphat:类别权重,作用同加权 CE;
  • gammagamma gamma(聚焦参数,通常取 2):降低「易分类样本」的贡献,让模型把注意力放到「难分类」的样本上。

打个比方 🎭:Focal Loss 像老师在考试时给「大家都做对的基础题」降权,把分数重心压到「只有少数人做对的难题」上------那些已经被正确分类的大量正样本(易样本)权重被削弱,模型被迫去学那些让它犯错的少数负样本。

何时用加权 CE、何时用 Focal Loss? 数据规模小、类别极少时,加权 CE 更稳 (Focal 的 gammagamma gamma 多一个超参,样本少时容易过拟合);数据量大、且希望模型聚焦「难样本」时,Focal Loss 更优。

5. 第三步:数据层过采样

如果损失函数还不够,就要在数据层面做文章,核心思路是「把少数类变多」。

5.1 朴素过采样(Random Oversampling)

直接把 10 个负样本复制几万次。简单,但有一个大坑 :模型会把少数样本「背下来」------因为重复的样本完全一样,模型记住这几个样本的特征就「过拟合」了,泛化能力反而变差。

5.2 SMOTE(合成少数类过采样)

SMOTE(Synthetic Minority Over-sampling Technique)在少数类样本之间做插值,生成「新的」样本而不是复制旧的:

  1. 对每个少数类样本,找它的 kk k 个最近邻;
  2. 在样本与某个邻居的连线上随机取一个点作为新样本。

mathbf xnew =mathbfxi+lambdacdot(mathbf xnn −mathbfxi),quadlambdasimU(0,1)mathbf{x}_{new} = mathbf{x}i + lambda cdot (mathbf{x}{nn} - mathbf{x}_i), quad lambda sim U(0, 1) mathbfxnew=mathbfxi+lambdacdot(mathbfxnn−mathbfxi),quadlambdasimU(0,1)

  • 生成的样本不是原样本的复制,而是「邻居间的中间点」,能有效缓解过拟合。

5.3 数据增强(Data Augmentation)

针对图像、文本等,用旋转、裁剪、加噪、同义词替换等方式,从一个少数样本「造出」多个变体。这比纯复制更鲁棒,因为它引入了「语义不变但形态不同」的多样性。

极端到个位数时的取舍 :当负样本只有 5--10 个,SMOTE 也容易在「极小的簇」里打转,生成的样本高度相似。此时更推荐「数据增强 + 加权损失」的组合,而不是疯狂过采样。

6. 第四步:调预测阈值

这一步最容易被忽略,却往往是救命的。

模型预测的是「正类的概率 pp p」,默认在 pge0.5p ge 0.5 pge0.5 时判为正。但在极端不平衡下,模型的概率分布整体都被正样本拉低------即使是最「坏」的样本,模型可能也只敢给 0.2 的概率。此时 0.5 的阈值等于「永远不报警」。

解法 :把阈值(threshold)从 0.5 往下调 ,比如降到 0.1 甚至 0.05,在 precision 和 recall 之间找一个平衡点。这个点通常看 PR 曲线上的拐点(precision≈recall 处)。

实测案例:某压力预测研究把预测阈值从默认调到 0.3 后,模型 recall 从不到 0.5 一举提升到 0.8857,代价是 precision 下降,但换来了「少数类更多被抓住」。

7. 完整可运行代码(PyTorch 端到端)

下面是一段复制粘贴就能跑的完整代码:构造一个 30000 正样本、10 负样本的极端不平衡二分类任务,用加权交叉熵 + SMOTE + 阈值调优,并输出 PR 曲线与 F1。

运行前:pip install torch scikit-learn imbalanced-learn matplotlib numpy。

python 复制代码
import numpy as np
import torch
import torch.nn as nn
from sklearn.model_selection import train_test_split
from sklearn.metrics import precision_recall_fscore_support, precision_recall_curve, auc
from imblearn.over_sampling import SMOTE
import matplotlib.pyplot as plt

# ① 构造极端不平衡数据:30000 正样本、10 负样本,8 维特征
np.random.seed(0)
n_pos, n_neg = 30000, 10
X_pos = np.random.randn(n_pos, 8) * 0.5 + 1.0      # 正样本:均值+1
X_neg = np.random.randn(n_neg, 8) * 0.5 - 1.0      # 负样本:均值-1
X = np.vstack([X_pos, X_neg])                      # 合并特征
y = np.concatenate([np.ones(n_pos), np.zeros(n_neg)])  # 1=正,0=负

# ② 划分训练/测试集(分层,保住那 10 个负样本)
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2,
                                                    stratify=y, random_state=42)

# ③ 用 SMOTE 过采样少数类(只在训练集做,防止数据泄露)
smote = SMOTE(random_state=42)
X_train_res, y_train_res = smote.fit_resample(X_train, y_train)
print(f"SMOTE 后训练集:正 {np.sum(y_train_res==1)} / 负 {np.sum(y_train_res==0)}")

# ④ 构建一个简单的二分类网络,用加权交叉熵
class Net(nn.Module):
    def __init__(self):
        super().__init__()
        self.fc = nn.Sequential(nn.Linear(8, 16), nn.ReLU(), nn.Linear(16, 1))
    def forward(self, x):
        return self.fc(x)

model = Net()
# 负样本权重放大:正/负样本数量比决定权重(负类权重更高)
w = torch.tensor([n_pos / n_neg])           # 负样本权重 ≈ 3000
criterion = nn.BCEWithLogitsLoss(pos_weight=w)  # 加权二分类交叉熵
optimizer = torch.optim.Adam(model.parameters(), lr=1e-3)

# ⑤ 训练
X_tr = torch.tensor(X_train_res, dtype=torch.float32)
y_tr = torch.tensor(y_train_res, dtype=torch.float32).unsqueeze(1)
for epoch in range(100):
    optimizer.zero_grad()
    logits = model(X_tr)
    loss = criterion(logits, y_tr)
    loss.backward()
    optimizer.step()
    if epoch % 20 == 0:
        print(f"epoch {epoch:3d}  loss={loss.item():.4f}")

# ⑥ 在测试集(原始 20% 数据)上评估,并扫描多个阈值
X_te = torch.tensor(X_test, dtype=torch.float32)
with torch.no_grad():
    probs = torch.sigmoid(model(X_te)).numpy()   # 正类的预测概率
thresholds = np.linspace(0.01, 0.99, 50)        # 从 0.01 到 0.99 扫阈值
f1s, precs, recs = [], [], []
for t in thresholds:
    y_pred = (probs >= t).astype(int)
    p, r, f1, _ = precision_recall_fscore_support(y_test, y_pred,
                                                 average="binary", zero_division=0)
    precs.append(p); recs.append(r); f1s.append(f1)
best_i = int(np.argmax(f1s))
print(f"
最优阈值 = {thresholds[best_i]:.2f}  "
      f"F1 = {f1s[best_i]:.3f}  "
      f"Precision = {precs[best_i]:.3f}  Recall = {recs[best_i]:.3f}")

# ⑦ 画 PR 曲线
precision, recall, _ = precision_recall_curve(y_test, probs)
pr_auc = auc(recall, precision)
plt.plot(recall, precision, label=f"PR-AUC = {pr_auc:.3f}")
plt.axvline(recs[best_i], ls="--", color="red", label=f"best threshold={thresholds[best_i]:.2f}")
plt.xlabel("Recall"); plt.ylabel("Precision"); plt.legend()
plt.title("Precision-Recall Curve"); plt.show()

怎么读这段代码 :第 ③ 步只在训练集做 SMOTE(测试集绝不能过采样 ,否则数据泄露、评估失真);第 ④ 步用 pos_weight 实现加权交叉熵,负类权重提高 3000 倍,让模型在意那几个负样本;第 ⑥ 步扫 50 个阈值,从中挑 F1 最高的那个,这正是第 6 节「调阈值」的落地。

8. 极端情形:个位数样本怎么处理

当负样本少到根本撑不起一个分类器 (比如只有 3--5 个),前面几招可能都失效。此时要转换问题定义:

8.1 转成异常检测(Anomaly Detection)

把「正样本(多数)当正常」,「负样本(少数)当异常」。异常检测只学「正常」的分布,任何偏离正常分布的都算异常。常用方法:One-Class SVM 、Isolation Forest 、Autoencoder 重构误差。

  • 优点:不再需要大量负样本,因为异常检测默认「只知道正常是什么」。
  • 适用:负样本 ≤ 10 个,且「负样本」本身没有统一模式(杂乱的异常)。

8.2 转成排序 / 检索问题

把「判断是不是异常」改成「给每个样本打一个异常分数,取分数最高的 top-N」。这是一个学习排序(learning to rank)问题,对不平衡没那么敏感。

8.3 数据能搞到就别省

除非实在拿不到,否则尽可能多收集几个负样本。哪怕是人工标注 20 个,也比 5 个强太多------因为 5 个样本既撑不起交叉验证,也容易让过采样「记忆」。

核心判断 :如果负样本数量少于所需验证集的最小粒度 (比如连 3--5 个验证样本都凑不出),就别硬当二分类做,转异常检测更靠谱。

9. 对照表:症状 → 原因 → 解法

症状 原因 解法
训练 loss 一直降,accuracy 99%,上线却啥都检不出 模型「摆烂」全猜正类 换评估指标:用 F1 / recall / PR-AUC,别再看 accuracy
负样本几乎学不到,权重不更新 正样本梯度淹没少数类 用加权交叉熵 / focal loss 放大少数类梯度
过采样后模型记住少数样本,泛化差 直接复制同一样本导致记忆 改用 SMOTE 插值生成新样本,或做数据增强
负样本永远判不出来,概率都低于 0.5 默认阈值 0.5 对不平衡过高 扫阈值下调(如 0.3 / 0.1),看 PR 曲线拐点
测试集效果虚高、上线崩 测试集也做了过采样(数据泄露) 只对训练集过采样,测试集用原始分布
负样本只有 3--5 个,啥方法都没用 样本量撑不起二分类 转异常检测(One-Class SVM / Isolation Forest)

10. 判断收尾

什么时候该用这套方法 :少数组虽少但有一定量(≥ 十几二十个) 、且你希望「尽量多抓少数类、宁肯误报多一些」时,用「加权损失 + SMOTE + 阈值下调 + PR 评估」这套组合拳。什么时候不该用:负样本只有 3--5 个且毫无规律,此时再怎么调损失也是空谈,直接转异常检测 / one-class 更现实。判断标准只有一条------少数样本的数量还撑不撑得起分类器。

本专栏《机器学习》收录从数据预处理、模型评估到不平衡分类、特征工程等实战方法与面试考点,每篇都配一段可直接运行的代码,持续更新。想在实战里少踩坑,可在专栏页订阅。

相关推荐
布吉岛的石头6 小时前
Java 程序员第 50 阶段13:对齐税 alignment tax 是什么
人工智能·深度学习·transformer
米饭是菜qy7 小时前
深度学习Transformer详细介绍
人工智能·深度学习·transformer
智码看视界8 小时前
深度学习:生成对抗网络 GAN 原理与图像生成应用
深度学习·生成对抗网络·gan·wgan·图像生成·dcgan·stylegan
eric-sjq8 小时前
小思框架 · 30M 级长上下文新架构对比技术报告
人工智能·python·深度学习·语言模型
无所畏俱的老尹8 小时前
OpenAI 等支持 robots.txt 共享 AI 训练偏好信号:policy token 一次声明多厂商,落地仍待实现
人工智能·深度学习·机器学习·aigc
服务器硬件项目经理9 小时前
《超节点修仙录》· 第一卷·炼气期【第04话】 超节点 vs 普通服务器——不就是多插了几张卡吗?
运维·服务器·人工智能·嵌入式硬件·深度学习·机器学习
tellmewhoisi9 小时前
深度学习28:PyTorch:参数更新方法(AdaGrad和RMSProp)
人工智能·pytorch·深度学习
一切皆是因缘际会10 小时前
当模型足够大
人工智能·深度学习·机器学习