机器学习中样本不平衡,怎么办?
在银行要判断一个"新客户是否会违约",通常不违约的人VS
违约的人会是99:1
的比例,真正违约的人 其实是非常少的。这种分类状况下,即便模型什么也不做,全把所有人都当成不会违约的人,正确率也能有99%, 这使得模型评估指标变得毫无意义,根本无法达到我们的"要识别出会违约的人"的建模目的。
像这样样本不均衡等例子在生活中随处可见。通常出现在异常检测、客户流失、罕见时间分析、发生低频率事件等场景,具体如垃圾邮件等识别,信用卡征信问题、欺诈交易检测、工厂中不良品检测等。在处理诸如此类的样本不均衡的任务中,使用常规方法并不能达到实际业务需求,正确且尽可能多捕获少数类样本。因为样本不均衡会使得分类模型存在很严重的偏向性。
本文中,介绍了在机器学习中样本不平衡处理策略及常用方法和工具。
样本不平衡分类
数据集中各个类别的样本数量极不均衡,从数据规模上可分为:
大数据分布不均衡。整体数据规模大,小样本类的占比较少,但小样本也覆盖了大部分或全部特征。 小数据分布不均衡。整体数据规模小,少数样本比例的分类数量也少,导致特征分布严重不均衡。
样本分布不均衡在于不同类别间的样本比例差异,导致很难从样本中提取规律。一般超过10倍就需要引起注意,20倍就一定要处理了。
样本不平衡处理策略
扩大数据集
样本不平衡时,可以增加包含一定比例小类样本数据以扩大数据集,更多的数据往往战胜更好的算法。因为机器学习是使用现有的数据多整个数据的分布进行估计,因此更多的数据往往能够得到更多的分布信息,以及更好分布估计。
但有时在增加小类样本数据的同时,也增加了大类数据,并不能显著解决样本不平衡问题。此时可以通过对大类样本数据进行欠采样,以放弃部分大类数据来解决。
重新选择评价指标
准确度在类别均衡的分类任务中并不能有效地评价分类器模型,造成模型失效,甚至会误导业务,造成较大损失。
最典型的评价指标即混淆矩阵Confusion Matrix
:使用一个表格对分类器所预测的类别与其真实的类别的样本统计,分别为:TP、FN、FP、TN
。包括精确度Precision
、召回率Recall
、F1得分F1-Score
等。
重采样数据集
使用采样sampling
策略该减轻数据的不平衡程度。主要有两种方法
对小类的数据样本进行采样来增加小类的数据样本个数,即过采样 over-sampling
对大类的数据样本进行采样来减少该类数据样本的个数,即欠采样 under-sampling
采样算法往往很容易实现,并且其运行速度快,并且效果也不错。在使用采样策略时,可以考虑
对大类下的样本 (超过1万, 十万甚至更多) 进行欠采样,即删除部分样本
对小类下的样本 (不足1为甚至更少) 进行过采样,即添加部分样本的副本
尝试随机采样与非随机采样两种采样方法
对各类别尝试不同的采样比例
同时使用过采样与欠采样
产生人工数据样本
一种简单的方法,对该类下的所有样本的每个属性特征的取值空间中随机选取一个值以组成新的样本,即属性值随机采样。可以使用基于经验对属性值进行随机采样而构造新的人工样本,或使用类似朴素贝叶斯方法假设各属性之间互相独立进行采样,这样便可得到更多的数据,但是无法保证属性之前的线性关系(如果本身是存在的)。
有一个系统的构造人工数据样本的方法SMOTE
(Synthetic Minority Over-sampling Technique)
。SMOTE
是一种过采样算法,它构造新的小类样本而不是产生小类中已有的样本的副本,即该算法构造的数据是新样本,原数据集中不存在的。该基于距离度量选择小类别下两个或者更多的相似样本,然后选择其中一个样本,并随机选择一定数量的邻居样本对选择的那个样本的一个属性增加噪声,每次处理一个属性。这样就构造了更多的新生数据。
尝试不同的分类算法
对待每一个机器学习任务都使用自己喜欢而熟悉的算法,相信很多人都会感同身受。但对待不同任务需要根据任务本身特点选用不同等算法,尤其对样本不均衡等分类任务。应该使用不同的算法对其进行比较,因为不同的算法使用于不同的任务与数据。
决策树往往在类别不均衡数据上表现不错。它使用基于类变量的划分规则去创建分类树,因此可以强制地将不同类别的样本分开。
对模型进行惩罚
你可以使用相同的分类算法,但是使用一个不同的角度,比如你的分类任务是识别那些小类,那么可以对分类器的小类样本数据增加权值,降低大类样本的权值,从而使得分类器将重点集中在小类样本身上。一个具体做法就是,在训练分类器时,若分类器将小类样本分错时额外增加分类器一个小类样本分错代价,这个额外的代价可以使得分类器更加"关心"小类样本。如penalized-SVM
和penalized-LDA
算法。
尝试一个新的角度理解问题
我们可以从不同于分类的角度去解决数据不均衡性问题,我们可以把那些小类的样本作为异常点outliers
,因此该问题便转化为异常点检测anomaly detection
与变化趋势检测问题change detection
。
异常点检测即是对那些罕见事件进行识别。如通过机器的部件的振动识别机器故障,又如通过系统调用序列识别恶意程序。这些事件相对于正常情况是很少见的。
变化趋势检测类似于异常点检测,不同在于其通过检测不寻常的变化趋势来识别。如通过观察用户模式或银行交易来检测用户行为的不寻常改变。
将小类样本作为异常点这种思维的转变,可以帮助考虑新的方法去分离或分类样本。这两种方法从不同的角度去思考,让你尝试新的方法去解决问题。
尝试创新
仔细对你的问题进行分析与挖掘,是否可以将你的问题划分成多个更小的问题,而这些小问题更容易解决。
以上策略参见Jason Brownlee的回答
https://machinelearningmastery.com/tactics-to-combat-imbalanced-classes-in-your-machine-learning-dataset/
处理样本不平衡方法
通过抽样
过采样
又称上采样(over-sampling)
通过增加分类中少数类样本的数量来实现样本不均衡。比较流行的算法有
The Synthetic Minority OverSampling Technique(SMOTE)
算法。
SMOTE
: 对于少数类样本a
, 随机选择一个最近邻的样本b
, 然后从a
与b
的连线上随机选取一个点c
作为新的少数类样本。
语法:
imblearn.over_sampling.SMOTE(sampling_strategy='auto', random_state=None, k_neighbors=5, n_jobs=1)
例:
>>> from collections import Counter
>>> from sklearn.datasets import make_classification
>>> from imblearn.over_sampling import SMOTE # doctest: +NORMALIZE_WHITESPACE
>>> X, y = make_classification(n_classes=2, class_sep=2,
... weights=[0.1, 0.9], n_informative=3, n_redundant=1, flip_y=0,
... n_features=20, n_clusters_per_class=1, n_samples=1000, random_state=10)
>>> print('Original dataset shape %s' % Counter(y))
Original dataset shape Counter({1: 900, 0: 100})
>>> smote = SMOTE(random_state=42)
>>> X_res, y_res = smote.fit_resample(X, y)
>>> print('Resampled dataset shape %s' % Counter(y_res))
Resampled dataset shape Counter({0: 900, 1: 900})
Step1: 对于少数类样本中的某个样本x,找到其K近邻的样本(当然这里的KNN是同类别的KNN)。
Step2: 随机挑选一个K近邻样本 y。
Step3: 生成新样本为 x, , λ 是[0,1]上随机数。
更多详情请参见
https://imbalanced-learn.readthedocs.io/en/stable/generated/imblearn.over_sampling.SMOTE.html
SMOTE
变体borderlineSMOTE
、SVMSMOTE
相对于基本的SMOTE
算法, 关注的是所有的少数类样本, 这些情况可能会导致产生次优的决策函数, 因此SMOTE
就产生了一些变体: 这些方法关注在最优化决策函数边界的一些少数类样本, 然后在最近邻类的相反方向生成样本。
这两种类型的SMOTE
使用的是危险样本来生成新的样本数据。
borderlineSMOTE(kind='borderline-1')最近邻中的随机样本b与该少数类样本a来自于不同的类。
borderlineSMOTE(kind='borderline-2')随机样本b可以是属于任何一个类的样本。
SVMSMOTE()使用支持向量机分类器产生支持向量然后再生成新的少数类样本。
少数类的样本分为三类:
噪音样本 noise
, 该少数类的所有最近邻样本都来自于不同于样本a
的其他类别。危险样本 in danger
, 至少一半的最近邻样本来自于同一类(不同于a
的类别)。安全样本 safe
, 所有的最近邻样本都来自于同一个类。
语法:
imblearn.over_sampling.BorderlineSMOTE(sampling_strategy='auto', random_state=None, k_neighbors=5, n_jobs=1, m_neighbors=10, kind='borderline-1')
imblearn.over_sampling.SVMSMOTE(sampling_strategy='auto', random_state=None, k_neighbors=5, n_jobs=1, m_neighbors=10, svm_estimator=None, out_step=0.5)
更多详情请参见
https://imbalanced-learn.readthedocs.io/en/stable/generated/imblearn.over_sampling.BorderlineSMOTE.html
https://imbalanced-learn.readthedocs.io/en/stable/generated/imblearn.over_sampling.SVMSMOTE.html
Adaptive Synthetic (ADASYN)
自适应合成上采样
ADASYN
: 关注的是在那些基于K
最近邻分类器被错误分类的原始样本附近生成新的少数类样本。
语法:
imblearn.over_sampling.ADASYN(sampling_strategy='auto', random_state=None, n_neighbors=5, n_jobs=1, ratio=None)
例:
>>> from collections import Counter
>>> from sklearn.datasets import make_classification
>>> from imblearn.over_sampling import ADASYN # doctest: +NORMALIZE_WHITESPACE
>>> X, y = make_classification(n_classes=2, class_sep=2,
... weights=[0.1, 0.9], n_informative=3, n_redundant=1, flip_y=0,
... n_features=20, n_clusters_per_class=1, n_samples=1000,
... random_state=10)
>>> print('Original dataset shape %s' % Counter(y))
Original dataset shape Counter({1: 900, 0: 100})
>>> ada = ADASYN(random_state=42)
>>> X_res, y_res = ada.fit_resample(X, y)
>>> print('Resampled dataset shape %s' % Counter(y_res))
Resampled dataset shape Counter({0: 904, 1: 900})
更多详情请参见
https://imbalanced-learn.readthedocs.io/en/stable/generated/imblearn.over_sampling.ADASYN.html
RandomOverSampler
随机采样增加新样本
例:跟之前类似,此处及后面均简化列出
>>> from imblearn.over_sampling import RandomOverSampler
>>> ros = RandomOverSampler(random_state=0)
>>> X_resampled, y_resampled = ros.fit_resample(X, y)
随机对欠表达样本进行采样,该算法允许对heterogeneous data(异构数据)进行采样(例如含有一些字符串)。通过对原少数样本的重复取样进行上采样。
更多详情请参见
https://imbalanced-learn.readthedocs.io/en/stable/generated/imblearn.over_sampling.RandomOverSampler.html
欠采样
又称下采样(under-sampling)
通过减少分类中多数类样本的数量来实现样本不均衡。
RandomUnderSampler
直接随机选取删除法
RandomUnderSampler
函数是一种快速并十分简单的方式来平衡各个类别的数据----随机选取数据的子集。
语法:
>>> from imblearn.under_sampling import RandomUnderSample
>>> model_undersample = RandomUnderSample()
>>> x_undersample_resampled, y_undersample_resampled = model_undersample.fit_sample(X,y)
注意1: 当设置
replacement=True
时, 采用bootstrap
注意2: 允许非结构化数据,例如数据中含字符串 注意3: 默认参数的时候, 采用的是不重复采样。
NearMiss
基于NN启发式算法
NearMiss
函数则添加了一些启发式heuristic
的规则来选择样本, 通过设定version
参数来实现三种启发式的规则。
>>> from imblearn.under_sampling import NearMiss
>>> nm1 = NearMiss(version=1)
>>> X_resampled_num1, y_resampled = nm1.fit_resample(X, y)
version=1
:选取正例样本中与N个最近邻负样本平均距离最小的样本。
version=2
:选取正例样本中与N个最远邻负样本平均距离最小的样本。
version=3
:2-steps 算法----首先对于每个负类样本, 保留它们的M近邻正样本。然后那些到N个近邻样本平均距离最大的正样本将被选择。
更多详情请参见
https://imbalanced-learn.readthedocs.io/en/stable/generated/imblearn.under_sampling.NearMiss.html
更多样本不平衡处理方法请参见ibmlearn官网
https://imbalanced-learn.readthedocs.io/en/stable/api.html
通过正负样本多惩罚权重
在算法实现过程中,对于分类中不同样本数量多类别分别负于不同的权重(少数类样本权重高)然后进行计算和建模。如模型中的参数class_weight:{dic,'balanced'}
分类模型评价指标
混淆矩阵
代码:
>>> from sklearn import metrics
>>> metrics.confusion_matrix(y_true, y_pred)
参数:
y_true 真实的label,一维数组格式,列名
y_pred 模型预测的label,一维数组,行名
labels 默认不指定,此时
y_true
、y_pred
取并集,升序,做labelsample_weight 样本权重
返回混淆矩阵,注意label
混淆矩阵:
Predicted as Positive | Predicted as Negative | |
---|---|---|
Labeled as Positive | True Positive(TP) | False Negative(FN) |
Labeled as Negative | False Positive(FP) | True Negative(TN) |
真正率
True Positive Rate, TPR
又名灵敏度Sensitivity
:被预测为正的正样本数 / 正样本实际数,即
假负率
False Negative Rate, FNR
被预测为负的正样本数/正样本实际数,即:
假正率
False Positive Rate, FPR
被预测为正的负样本数/负样本实际数,即:
真负率
True Negative Rate, TNR
特异度Specificity
:被预测为负的负样本数/负样本实际数,即:
准确率
Accuracy
分类正确的样本数/所有样本数量,即:
语法:
sklearn.metrics.accuracy_score(y_true, y_pred, normalize=True, sample_weight=None)
y_true 真实的
label
,一维数组格式y_pred 模型预测的
label
,一维数组normalize 默认
True
,返回正确预测的比例,False
返回预测正确的个数sample_weight 样本权重
score 返回正确的比例或者个数,由
normalize
指定
错误率
精确度和召回率
精确率
Precision
,又称查准率:
语法:
sklearn.metrics.precision_score(y_true, y_pred, *, labels=None, pos_label=1, sample_weight=None)
例:
>>> from sklearn.metrics import accuracy_score
>>> y_pred = [0, 2, 1, 3]
>>> y_true = [0, 1, 2, 3]
>>> accuracy_score(y_true, y_pred)
0.5
>>> accuracy_score(y_true, y_pred, normalize=False)
2
召回率
Recall
,又称查全率
语法:
sklearn.metrics.recall_score(y_true, y_pred, *, labels=None, pos_label=1, sample_weight=None)
例:
>>> from sklearn.metrics import recall_score
>>> y_true = [0, 1, 2, 0, 1, 2]
>>> y_pred = [0, 2, 1, 0, 0, 1]
>>> recall_score(y_true, y_pred, average='macro')
0.33...
>>> recall_score(y_true, y_pred, average='micro')
0.33...
>>> recall_score(y_true, y_pred, average='weighted')
0.33...
>>> recall_score(y_true, y_pred, average=None)
array([1., 0., 0.])
>>> y_true = [0, 0, 0, 0, 0, 0]
>>> recall_score(y_true, y_pred, average=None)
array([0.5, 0. , 0. ])
>>> recall_score(y_true, y_pred, average=None, zero_division=1)
array([0.5, 1. , 1. ])
F1-Score
精确度和召回率低调和平均值----总体各统计量的倒数的算术平均数的倒数。
语法:
sklearn.metrics.f1_score(y_true, y_pred, *, labels=None, pos_label=1, sample_weight=None)
classification_report
构建一个文本报告,显示主要的分类度量。得到每个类别的precision, recall, F1 score
语法:
sklearn.metrics.classification_report(y_true, y_pred, *, labels=None, target_names=None, sample_weight=None, digits=2, output_dict=False)
参数:
y_true 真实的
label
,一维数组格式,列名y_pred 模型预测的
label
,一维数组,行名labels 默认不指定,此时
y_true
、y_pred
取并集,升序,做label
sample_weight 样本权重
target_names 行标签,顺序和
label
的要一致digits
int
,小数的位数output_dict 输出格式,默认
False
,如果True
,返回字典report 返回计算结果,形式依赖于
output_dict
例:
>>> from sklearn.metrics import classification_report
>>> y_true = [0, 1, 2, 2, 2]
>>> y_pred = [0, 0, 2, 2, 1]
>>> target_names = ['class 0', 'class 1', 'class 2']
>>> print(classification_report(y_true, y_pred, target_names=target_names))
precision recall f1-score support
<BLANKLINE>
class 0 0.50 1.00 0.67 1
class 1 0.00 0.00 0.00 1
class 2 1.00 0.67 0.80 3
<BLANKLINE>
accuracy 0.60 5
macro avg 0.50 0.56 0.49 5
weighted avg 0.70 0.60 0.61 5
<BLANKLINE>
ROC 与 AUC
ROC的全名叫做Receiver Operating Characteristic
,其主要分析工具是一个画在二维平面上的曲线----ROC curve
。
平面的横坐标是假正率false positive rate(FPR) 纵坐标是真正率true positive rate(TPR)
面试问题:当阈值theta调高时,TP和FP都增加,精确度变高,召回率变小。
语法:
sklearn.metrics.roc_auc_score(y_true, y_score, *, average='macro', sample_weight=None, max_fpr=None, multi_class='raise', labels=None)
y_true 真实的
label
,一维数组格式,列名y_pred 模型预测的
label
,一维数组,行名average 有多个参数可选,一般默认即可
sample_weight 样本权重
max_fpr 取值范围
[0,1)
,如果不是None
,则会标准化,使得最大值等于max_fpr
report 返回计算结果,形式依赖于
output_dict
例:
>>> from sklearn.datasets import load_breast_cancer
>>> from sklearn.linear_model import LogisticRegression
>>> from sklearn.metrics import roc_auc_score
>>> X, y = load_breast_cancer(return_X_y=True)
>>> clf = LogisticRegression(solver="liblinear", random_state=0).fit(X, y)
>>> roc_auc_score(y, clf.predict_proba(X)[:, 1])
0.99...
>>> roc_auc_score(y, clf.decision_function(X))
0.99...
可视化
roc_curve
sklearn.metrics.plot_roc_curve
语法:
sklearn.metrics.plot_roc_curve(estimator, X, y, *, sample_weight=None, name=None, ax=None, pos_label=None, **kwargs)
例:
>>> import matplotlib.pyplot as plt
>>> from sklearn import datasets, metrics, model_selection, svm
>>> X, y = datasets.make_classification(random_state=0)
>>> X_train, X_test, y_train, y_test = model_selection.train_test_split(
... X, y, random_state=0)
>>> clf = svm.SVC(random_state=0)
>>> clf.fit(X_train, y_train)
SVC(random_state=0)
>>> metrics.plot_roc_curve(clf, X_test, y_test)
>>> plt.show()
自定义函数
>>> import numpy as np
>>> import pandas as pd
>>> import matplotlib.pyplot as plt
>>> from sklearn.ensemble import RandomForestClassifier as RFC
>>> from sklearn.metrics import auc, accuracy_score, recall_score, f1_score, roc_curve
>>> model = RFC()
# ROC
>>> y_score = model.fit(Xtrain, ytrain).predict_proba(Xtest) # 随机森林
>>> fpr, tpr, thresholds = roc_curve(ytest, y_score[:, 1])
>>> roc_auc = auc(fpr, tpr)
>>> def drawRoc(roc_auc, fpr, tpr):
... plt.subplots(figsize=(7, 5.5))
... plt.plot(fpr, tpr, color='darkorange', lw=2, label='ROC curve (area = %0.2f)' % roc_auc)
... plt.plot([0, 1], [0, 1], color='navy', lw=2, linestyle='--')
... plt.xlim([0.0, 1.0])
... plt.ylim([0.0, 1.05])
... plt.xlabel('False Positive Rate')
... plt.ylabel('True Positive Rate')
... plt.title('ROC Curve')
... plt.legend(loc="lower right")
... plt.show()
>>> drawRoc(roc_auc, fpr, tpr)
输出:
附
def plot_roc_curve(fpr,tpr, thresholds):
plt.figure()
plt.plot(fpr, tpr, color='darkorange', label='ROC curve (area = %0.2f)' % metrics.auc(fpr, tpr))
plt.plot([0, 1], [0, 1], color='navy', linestyle='--')
plt.xlim([0.0, 1.0])
plt.ylim([0.0, 1.05])
plt.xlabel('False Positive Rate')
plt.ylabel('True Positive Rate')
plt.legend(loc="lower right")
# create the axis of thresholds (scores)
ax2 = plt.gca().twinx()
ax2.plot(fpr, thresholds, markeredgecolor='r',linestyle='dashed', color='r')
ax2.set_ylabel('Threshold',color='r')
ax2.set_ylim([thresholds[-1],thresholds[0]])
ax2.set_xlim([fpr[0],fpr[-1]])
plt.savefig('roc_and_threshold.png')
plt.close()
# 寻找最佳阈值
def Find_Optimal_Cutoff(target, predicted):
fpr, tpr, threshold = roc_curve(target, predicted)
i = np.arange(len(tpr))
roc = pd.DataFrame({'tf' : pd.Series(tpr-(1-fpr), index=i), 'threshold' : pd.Series(threshold, index=i)})
roc_t = roc.ix[(roc.tf-0).abs().argsort()[:1]]
return list(roc_t['threshold'])
threshold = Find_Optimal_Cutoff(target_column,predicted_column)
Kappa系数
Kappa系数用于一致性检验,Kappa计算结果为[-1, 1],但通常kappa是落在0~1之间,可分为五组表示不同级别的一致性。
大小 | 一致性程度 |
---|---|
0.0 ~ 0.20 | 极低的一致性 |
0.21 ~ 0.40 | 较差的一致性 |
0.41 ~ 0.60 | 一般的一致性 |
0.61 ~ 0.80 | 较好的一致性 |
0.81 ~ 1 | 很好的一致性 |
公式:
其中,为样本整体分类准确度 假设每一类的真实样本个数为:;预测出来的每一类样本个数为:;样本总个数为
本文较为详细地介绍了在机器学习中处理不平衡样本的常用思考策略及常用的处理方法、工具等。
- EOF -
觉得本文对你有帮助?请分享给更多人
推荐关注「Python开发者」,提升Python技能
点赞和在看就是最大的支持❤️