1. 不平衡数据处理的必要性
在机器学习实践中,我们经常会遇到类别分布严重不均衡的数据集。比如在信用卡欺诈检测中,正常交易可能占99.9%,而欺诈交易只有0.1%。这种数据不平衡会导致模型严重偏向多数类,在测试集上看似准确率很高,但对少数类的识别率却惨不忍睹。
我最近在一个医疗诊断项目中就遇到了这个问题。原始数据中健康样本占比85%,患病样本只有15%。直接训练的逻辑回归模型对健康样本的预测准确率达到92%,但对患病样本的识别率只有可怜的43%——这在实际应用中是完全不可接受的。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 三种核心处理方法对比
2.1 k折交叉验证的巧妙应用
传统k折交叉验证在不平衡数据场景下需要特殊处理。我推荐使用分层k折(StratifiedKFold),它能确保每个折中类别比例与原始数据集一致。在Python中实现非常简单:
python复制from sklearn.model_selection import StratifiedKFold
skf = StratifiedKFold(n_splits=5)
for train_idx, test_idx in skf.split(X, y):
X_train, X_test = X[train_idx], X[test_idx]
y_train, y_test = y[train_idx], y[test_idx]
重要提示:千万不要在数据重采样后再做交叉验证!这会导致数据泄露,应该先在原始数据上划分训练测试集,再对训练集单独进行采样处理。
2.2 下采样(Undersampling)实战技巧
下采样通过减少多数类样本来平衡数据分布。最简单的随机下采样虽然实现容易,但会丢失大量信息。我更喜欢使用NearMiss算法,它基于KNN保留有代表性的多数类样本:
python复制from imblearn.under_sampling import NearMiss
nm = NearMiss(version=2)
X_resampled, y_resampled = nm.fit_resample(X_train, y_train)
实测发现NearMiss-2版本效果最好,它会选择那些与少数类样本平均距离最近的多数类样本。在下采样后,建议检查特征分布是否发生了明显偏移,这会影响模型泛化能力。
2.3 过采样(Oversampling)的进阶用法
与下采样相反,过采样通过增加少数类样本来平衡数据。SMOTE算法是当前最流行的选择,它通过在特征空间内插值生成新样本:
python复制from imblearn.over_sampling import SMOTE
sm = SMOTE(k_neighbors=5)
X_resampled, y_resampled = sm.fit_resample(X_train, y_train)
但SMOTE有个常见陷阱——当少数类样本本身很少时(比如少于5个),k_neighbors参数需要相应调小。我开发了一个自适应函数来自动确定最佳k值:
python复制def auto_smote(X, y):
min_samples = sum(y == 1) if sum(y == 1) < sum(y == 0) else sum(y == 0)
k = min(5, min_samples - 1)
return SMOTE(k_neighbors=k).fit_resample(X, y)
3. 方法组合与评估策略
3.1 混合采样(Hybrid Approach)
在实际项目中,我经常将SMOTE与ENN(Edited Nearest Neighbours)结合使用。这种组合先过采样少数类,再清理重叠区域的噪声样本:
python复制from imblearn.combine import SMOTEENN
smote_enn = SMOTEENN()
X_resampled, y_resampled = smote_enn.fit_resample(X_train, y_train)
3.2 评估指标选择
准确率在不平衡数据场景下毫无意义。我建议使用以下指标组合:
- 精确率-召回率曲线(PR曲线)
- F1分数(特别是F2分数,更看重召回率)
- 混淆矩阵的可视化分析
- AUC-ROC(但要注意在极端不平衡时可能过于乐观)
python复制from sklearn.metrics import classification_report
print(classification_report(y_test, y_pred, target_names=['多数类', '少数类']))
4. 实战案例:信用卡欺诈检测
4.1 数据准备
使用Kaggle信用卡欺诈数据集,原始数据中欺诈交易仅占0.17%。我们先进行基本处理:
python复制import pandas as pd
from sklearn.preprocessing import RobustScaler
data = pd.read_csv('creditcard.csv')
X = data.drop('Class', axis=1)
y = data['Class']
# 由于金额差异大,使用RobustScaler
scaler = RobustScaler()
X['Amount'] = scaler.fit_transform(X['Amount'].values.reshape(-1,1))
4.2 处理流程对比
我们对比三种处理方式的效果:
| 方法 | 精确率 | 召回率 | F1分数 |
|---|---|---|---|
| 原始数据 | 0.92 | 0.43 | 0.58 |
| SMOTE | 0.87 | 0.79 | 0.83 |
| NearMiss-2 | 0.34 | 0.91 | 0.49 |
| SMOTEENN | 0.91 | 0.85 | 0.88 |
4.3 模型训练技巧
即使经过采样处理,在训练时仍建议:
- 使用class_weight参数
- 对决策树类模型设置max_depth限制
- 对神经网络添加dropout层
python复制from sklearn.ensemble import RandomForestClassifier
model = RandomForestClassifier(
class_weight='balanced',
max_depth=10,
n_estimators=100
)
5. 常见陷阱与解决方案
5.1 数据泄露问题
我见过很多人在全局数据上先做SMOTE再划分训练测试集,这会导致评估结果虚高。正确的顺序应该是:
- 原始数据划分训练测试集
- 仅在训练集上应用采样方法
- 用未采样的测试集评估
5.2 类别完全分离
当下采样过度时,可能导致两类完全分离。这时模型会学习到过于简单的决策边界。解决方法包括:
- 调整采样比例(不要强制1:1)
- 添加正则化项
- 使用更复杂的模型
5.3 高维数据问题
在特征维度很高时(如>50),SMOTE生成的样本可能没有意义。这时可以:
- 先做PCA降维
- 使用专门的高维SMOTE变种
- 改用基于聚类的采样方法
6. 工程实践建议
在实际业务系统中,我总结出以下经验:
- 对于实时检测系统,下采样通常更合适,因为过采样会增加计算负担
- 定期重新评估采样策略,数据分布可能随时间变化
- 将采样参数与模型参数一起进行网格搜索
- 考虑业务代价:误报和漏报的成本可能不同
python复制from sklearn.model_selection import GridSearchCV
param_grid = {
'smote__k_neighbors': [3,5,7],
'model__C': [0.1, 1, 10]
}
pipeline = Pipeline([
('smote', SMOTE()),
('model', LogisticRegression())
])
grid = GridSearchCV(pipeline, param_grid, scoring='f1')
不平衡数据处理是机器学习工程中的重要环节,需要根据具体业务场景选择合适的方法组合。在我的实践中,SMOTEENN通常能取得不错的效果,但一定要通过严谨的实验来确定最佳方案。记住,没有放之四海而皆准的银弹,持续监控和迭代才是关键。
