你有没有遇到过这种情况:训练一个“欺诈检测模型”,99%的样本是正常交易,1%是欺诈交易。模型看似准确率99%,但实际上对所有样本都预测“正常”——因为只要讨好多数类,就能轻松拿高分,却完全漏掉了致命的欺诈样本。
这就是数据不平衡在搞鬼。当数据集中某些类别的样本数量远远少于其他类别时,模型会下意识“偏袒”多数类,对少数类视而不见。今天就来讲透数据平衡的核心方法,从过采样到加权损失,附公式推导和代码实战,帮你的模型“一碗水端平”!
一、数据不平衡:模型的“偏见之源”
先看一个扎心的例子:
- 二分类任务:预测“癌症患者”(少数类)和“健康人”(多数类);
- 数据分布:1000个样本中,只有50个癌症患者(5%),950个健康人(95%);
- 模型“偷懒”:直接预测所有样本为“健康人”,准确率也能达到95%,但对癌症患者的识别率为0——这样的模型毫无价值。
数据不平衡的危害:
- 模型偏向多数类,少数类识别率极低(如欺诈检测漏检、疾病诊断误诊);
- 评估指标“失真”:高准确率掩盖了少数类的预测失败;
- 模型学到的特征有偏差:只记住多数类的规律,忽略少数类的特点。
判断数据是否不平衡的简单标准:少数类样本占比低于20%,就需要做平衡处理。
二、数据平衡3大核心方法:从“偏袒”到“公平”
1. 过采样(Oversampling):给少数类“扩军”
原理:增加少数类样本数量,使其与多数类接近。就像给少数派“增加话语权”,让模型不得不关注它们。
经典算法:SMOTE( Synthetic Minority Oversampling Technique)
SMOTE不是简单复制少数类样本(会导致过拟合),而是生成“虚拟少数类样本”:
- 对每个少数类样本,找它最近的k个少数类邻居;
- 在样本与邻居之间随机选一点,生成新样本(如样本A(2,3),邻居B(4,5),新样本可能是(3,4))。
这样既增加了少数类数量,又保留了特征分布,避免过拟合。
2. 欠采样(Undersampling):给多数类“裁员”
原理:减少多数类样本数量,使其与少数类接近。就像给多数派“限权”,降低其在训练中的主导地位。
常用策略:
- 随机欠采样:随机删除多数类样本(简单但可能删错关键样本);
- 聚类欠采样:对多数类聚类,保留每个簇的中心样本(保留重要信息)。
缺点:可能丢失多数类的重要特征(比如删除了包含关键模式的样本),因此更适合多数类样本极多的场景(如10万+样本)。
3. 加权损失(Weighted Loss):让模型“更关注”少数类
原理:不改变样本数量,而是在计算损失时给少数类样本更高的权重——少数类样本的预测错误会“惩罚”模型更重,迫使模型重视它们。
核心公式:加权交叉熵损失
对于二分类任务,加权交叉熵损失为:
L=−1N∑i=1Nwyi(yilogy^i+(1−yi)log(1−y^i))L = -\frac{1}{N} \sum_{i=1}^N w_{y_i} \left( y_i \log \hat{y}_i + (1-y_i) \log (1-\hat{y}_i) \right)L=−N1i=1∑Nwyi(yilogy^i+(1−yi)log(1−y^i))
其中:
- wyiw_{y_i}wyi 是类别yiy_iyi的权重(yi=1y_i=1yi=1为少数类,yi=0y_i=0yi=0为多数类);
- 权重通常按“样本总数/类别样本数”计算:w1=NN1w_1 = \frac{N}{N_1}w1=N1N,w0=NN0w_0 = \frac{N}{N_0}w0=N0N(N1N_1N1是少数类样本数,N0N_0N0是多数类样本数)。
举例:若少数类占比10%(N1=100N_1=100N1=100,N0=900N_0=900N0=900,N=1000N=1000N=1000),则w1=1000/100=10w_1=1000/100=10w1=1000/100=10,w0=1000/900≈1.1w_0=1000/900≈1.1w0=1000/900≈1.1。少数类的损失权重是多数类的9倍,模型会更努力学习少数类的特征。
三、方法对比:该选过采样、欠采样还是加权损失?
方法 | 原理 | 优点 | 缺点 | 适用场景 |
---|---|---|---|---|
过采样(SMOTE) | 增加少数类样本 | 保留多数类信息,适合样本少的场景 | 可能生成不合理样本,导致过拟合 | 少数类样本极少(<10%) |
欠采样 | 减少多数类样本 | 训练快,适合大数据集 | 可能丢失多数类关键信息 | 多数类样本极多(如10万+) |
加权损失 | 调整损失权重 | 不改变数据分布,灵活 | 需调权重参数,对极端不平衡效果有限 | 中等不平衡(10%-20%) |
一句话总结:
- 少数类样本极少(如5%以下)→ 优先用SMOTE过采样;
- 多数类样本爆炸(如100万+)→ 试试欠采样;
- 怕改变数据分布或需在线学习 → 用加权损失。
四、代码实战:用SMOTE拯救“少数类”(附可视化)
我们用一个极度不平衡的二分类数据集(10%少数类),演示SMOTE处理过程,对比平衡前后的模型效果:
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from sklearn.datasets import make_classification
from sklearn.model_selection import train_test_split
from sklearn.ensemble import RandomForestClassifier
from sklearn.metrics import classification_report
from imblearn.over_sampling import SMOTE # 需安装:pip install imbalanced-learn
# ----------------------
# 1. 生成不平衡数据(修正参数冲突)
# ----------------------
np.random.seed(42)
X, y = make_classification(
n_classes=2, # 2个类别
class_sep=2, # 类别分离度
weights=[0.9, 0.1], # 多数类90%,少数类10%
n_features=2, # 总特征数=2
n_informative=2, # 信息特征数=2(关键修正:2^2=4)
n_redundant=0, # 冗余特征数=0
n_repeated=0, # 重复特征数=0
n_clusters_per_class=2, # 每个类别2个簇(默认值)
n_samples=1000, # 1000个样本
random_state=42
)
# 此时:2(类别数)* 2(每个类别簇数)=4 ≤ 2^2(信息特征数的2次方)=4,满足条件
# ----------------------
# 2. 可视化原始数据分布(解决中文显示)
# ----------------------
plt.figure(figsize=(12, 6))
plt.rcParams["font.family"] = ["SimHei"] # 中文显示
plt.rcParams["axes.unicode_minus"] = False # 负号显示
# 子图1:原始数据分布
plt.subplot(1, 2, 1)
plt.title("原始数据分布(少数类仅10%)", fontsize=14)
plt.scatter(X[y==0, 0], X[y==0, 1], c='blue', label='多数类(y=0)', alpha=0.6, edgecolor='k')
plt.scatter(X[y==1, 0], X[y==1, 1], c='yellow', label='少数类(y=1)', alpha=0.6, edgecolor='k')
plt.legend()
plt.xlabel("特征1")
plt.ylabel("特征2")
# ----------------------
# 3. SMOTE过采样平衡数据
# ----------------------
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.3, stratify=y, random_state=42
)
smote = SMOTE(random_state=42)
X_resampled, y_resampled = smote.fit_resample(X_train, y_train)
# ----------------------
# 4. 可视化平衡后的数据
# ----------------------
plt.subplot(1, 2, 2)
plt.title("SMOTE平衡后的数据分布(1:1)", fontsize=14)
plt.scatter(X_resampled[y_resampled==0, 0], X_resampled[y_resampled==0, 1],
c='blue', label='多数类(y=0)', alpha=0.6, edgecolor='k')
plt.scatter(X_resampled[y_resampled==1, 0], X_resampled[y_resampled==1, 1],
c='yellow', label='少数类(y=1)', alpha=0.6, edgecolor='k')
plt.legend()
plt.xlabel("特征1")
plt.ylabel("特征2")
plt.tight_layout()
plt.show()
# ----------------------
# 5. 模型评估
# ----------------------
clf = RandomForestClassifier(random_state=42)
clf.fit(X_resampled, y_resampled)
y_pred = clf.predict(X_test)
print("=== 平衡后模型评估报告 ===")
print(classification_report(y_test, y_pred))
# ----------------------
# 6. 可视化决策边界
# ----------------------
xx, yy = np.meshgrid(
np.linspace(X[:, 0].min()-1, X[:, 0].max()+1, 200),
np.linspace(X[:, 1].min()-1, X[:, 1].max()+1, 200)
)
Z = clf.predict(np.c_[xx.ravel(), yy.ravel()])
Z = Z.reshape(xx.shape)
plt.figure(figsize=(10, 6))
plt.contourf(xx, yy, Z, alpha=0.3, cmap='coolwarm')
plt.scatter(X_test[y_test==0, 0], X_test[y_test==0, 1], c='blue', label='多数类(y=0)', edgecolor='k')
plt.scatter(X_test[y_test==1, 0], X_test[y_test==1, 1], c='yellow', label='少数类(y=1)', edgecolor='k')
plt.title("平衡后模型的决策边界", fontsize=14)
plt.xlabel("特征1")
plt.ylabel("特征2")
plt.legend()
plt.show()
== 平衡后模型评估报告 ==
precision recall f1-score support
0 0.99 0.99 0.99 268
1 0.93 0.88 0.90 32
accuracy 0.98 300
macro avg 0.96 0.93 0.95 300
weighted avg 0.98 0.98 0.98 300
五、结果解读:SMOTE如何让模型“公平”?
-
原始数据分布(左图):
黄色点(少数类)稀疏,蓝色点(多数类)密集,模型很容易“只看到蓝色”,忽略黄色。 -
SMOTE平衡后(右图):
黄色点数量翻倍(生成了合理的虚拟样本),与蓝色点数量接近,模型不得不“认真学习”黄色点的特征。 -
评估报告:
重点看“少数类(y=1)”的recall(召回率)——平衡后通常能从30%以下提升到80%以上,意味着更多少数类样本被正确识别(如欺诈交易、癌症患者)。 -
决策边界(右图):
平衡后的模型会给少数类样本“划分出合理的决策区域”(冷色区域),而不是把所有区域都归为多数类。
六、避坑指南:数据平衡的3个“雷区”
-
只平衡训练集,别动测试集:
测试集必须保持真实分布(比如现实中欺诈就是1%),否则评估结果失真。SMOTE等操作只用于训练集! -
过采样别“过度复制”:
简单复制少数类样本(而不是SMOTE生成新样本)会导致模型“记住”这些样本,泛化差。优先用SMOTE等智能过采样。 -
别盲目追求“绝对平衡”:
某些场景下,完全1:1可能引入噪声(如少数类本身就稀有)。可根据业务需求调整比例(如2:1或3:1)。
总结:数据平衡的“终极目标”
数据平衡不是为了“让样本数量绝对相等”,而是让模型对每个类别都给予足够的关注。无论是过采样、欠采样还是加权损失,核心都是纠正模型的“偏见”,让少数类的重要特征不被忽略。
记住:在欺诈检测、疾病诊断等场景中,“漏检一个少数类样本”的代价可能远高于“错检一个多数类样本”。数据平衡,本质是在为这些“关键少数”争取应有的重视。
你在处理不平衡数据时踩过哪些坑?评论区聊聊你的解决方案~