写在前边
机器学习其实和人类的学习很相似,我们平时会有做对的题,常错的易错题,或是比较难得题,但是一般的学校布置肯定一套的题目给每个人,那么其实我们往往复习时候大部分碰到会的,而易错的其实就比较少,同时老师也没法对每个人都做到针对性讲解。这就有了像艾宾浩斯曲线或是个性化教学的软件,智能的加大错题和难题的出次数,达到针对性学习的效果。而很多时候有的人非常做了很多题,但是只是题海战术,并不能很好训练提升自己的成绩,老师就会让大家做错题本了,多从错题中抽来练习,避免做无用功。
那么机器学习里呢,有的场景一些类别的训练数据非常多,而某些类别的训练数据很少,这样就会使得训练出来的模型,对于多数类别的样本非常好,但是对少数样本的识别率就很低,但我们往往需要的就是要少数样本类尽量准确。比如像特别多医学检测,有的病检测出阳性的比例是非常低,还有诈骗电话,金融信用卡欺诈检测,网络入侵检测,以及仪器设备或是IT故障等,真实的异常数据不可能经常有,当然可以重点保存这些数据并把时间段加长,但这样就太耗时间了,我们就需要通过一些技术增加这些异常数据或是平衡训练数据了。
我们接下来具体看机器学习里的不平衡数据的问题
传统学习方法在不平衡数据中的局限性
首先我们要从问题本质出发,数据不平衡为何会影响传统机器学习算法,机器学习算法往往是基于一个评价函数或指标,得到的分类准确率是评价一个模型好坏的基础,但是准确率是分类正确和所有预测的比值,所以如果少数类是很小的时候,即使分类都错误,但是多数类错误就还是会得到较高的得分。而算法的搜索往往是基于一个梯度方向或是最优划分,那势必会向分类越多正确的倾斜,就像我们考试当然先把会的都做了尽量先得分一样,这样即使少数难题不会做也会得到相对高的分了。那么就有了一个F1-measure 评价指标更多被使用了,也相对公平。
具体考虑最常见的二分类不平衡情况,比如信用卡盗刷欺诈,数据样本可以用(x,y)表示,y=0 表示多数负样本,即不是欺诈的,y=1表示少数正样本,即是欺诈或盗刷的样本,
P=Precision ,R=Recall
F1-measure=P * R / (P + R)
样本不均衡的分类
样本分类不均衡的种类可以分为两种,一种是大数据分布不均;一种是小数据分布不均;
大数据分布不均衡:整体数据量是非常大的,只是小样本的比例比较小 ,但是从每个特征的分布来看,小样本也覆盖了大部分特征,也就是和其它的样本的分布比较一致,比如信用卡逾期,设备故障异常,占比是有的只是少了罢了,或者用客户流失或是营销获客的例子更贴近些,总有些客户是会流失或新转换的。
小数据分布不均衡:这种是本身数据量就很少,并且占少量样本比例的分类数量也少,这会导致特征分布的严重不均衡。例如有几千条样本的数据集中还有10条特别的类,其特征无论如何拟合也无法实现完整特征值得覆盖,此时属于严重的数据样本分布不均衡,也就是几个老鼠屎,如信用卡欺诈,窃电等。
所以我们可以认识到样本不均衡本质是少数分类特征不均衡导致的,假设少数类样本的特征和总体分布比较一致,或者说刚好选了几个典型代表,那么其实影响是比较小的,只要多训练就好。
从这两个大分类我们可以看那些场景可能会遇到样本不均衡:
容易出现样本不均衡的场景
异常检测场景:
比如一些少数个案的场景,恶意刷单,羊毛党刷漏洞,黄牛订单,信用卡欺诈,电力窃电,设备故障,系统异常等。比如信用卡欺诈比例一般是0.1%。
客户流失场景:
客户流失也是比较少数 ,但是对于大型的企业,每年都是会有部分客户流失,比如电信、石油、互联网运营商,有些客户其实都是到处游走。
罕见事件分析:
和异常事件的不同的是罕见事件不是现有规则和逻辑可以判定的,而是偶然发生,没有可预测性的事件,比如某网络大V无意中转发了一个趣味广告导致引来了大量客户流量。
发生频率低的事件:
这是指预期或计划性事件、但是发生频率非常低。例如一个新媒体平台每年有很多活动,社群和公众号等,但是双十一那天可能是某个触点或是产品会使这几天销售额提升很大,但是从一年总体来说是1%不到的。更别说不怎么做运营和活动的公司来说。
样本不均衡问题解决方法
从上面所说的原理来看我们知道关键是特征和训练是否拟合,所以解决的思路也是从两个方面来看,解决不均衡问题可以从两个方面考虑。 一个是改变模型,让其对较少的类别较敏感,增大样本数较少类别的样本的权重,当这样的样本被误分时,其损失值要乘上相应的权重,从而让分类器更加关注这一类数目较少的样本。比如逻辑回归,可以调整阈值,让其对少的类更敏感。二是改变训练集合,用过采样(over sampling,如SMOTE等)和欠采样(under sampling)的方式调整训练集的平衡。
总体我们可以分为通过过采样和欠采样、改变正负样本的惩罚权重、组合和集成方法解决一级特征选择的几种方法。
具体分析和代码实操
这里用kaggle上最著名的比赛数据,以Titanic的数据为例,这时候存活为1可以是正样本,为0的为负样本,正负比342/549 ,当然相对还是差别不大的。只是做个测试,后面用creditfraud比赛做个实例。当类别不平衡的时候,例如正反比为 9:1,学习器将所有样本判别为正例的正确率都能达到 0.9。这时候,我们就需要使用 “再缩放”、“欠采样”、“过采样”、“阈值移动” 等方法,主要学习这几个常用的。
比较常用的开源包是imblearn,后面其实也很多集成到sklearn里,不过一般是使用imblearn这个扩展包,比较专业。一般不平衡处理是在建模之前,也就是在做好预处理时候,其实主要是将缺失值填补和将数据数值化,以便一些计算和产生数据,不然imblearn的算法可能会报错。
初步分析和处理
#看特征数
features = list(set(train.columns) - set(['PassengerId','Survived']))
target = 'Survived'
Id = 'PassengerId'
#查看样本数和特征数
print(train[features].shape)
# 查看数据的前5行
#这个只能大概看下数据长什么样
print(train[features].head())
print(train[features].describe())
print(train[target].value_counts())
#可视化
import seaborn as sns
sns.countplot(x='Survived',data=train) #对不同值的 'Survived' 进行计数并绘图
from collections import Counter #统计包
# 查看样本类别分布,
print(Counter(train[target]))
口号喊完了,开始清洗数据,这里是初步简单处理达到处理要求即可
#清洗数据
train.drop(["Cabin","Name","Ticket"],axis=1,inplace=True)
train["Age"] = train["Age"].fillna(train["Age"].mean())
train["Sex"] = (train["Sex"]=="male").astype("int")
labels = train["Embarked"].unique().tolist()
train["Embarked"] = train["Embarked"].apply(lambda x: labels.index(x))
train.fillna(0,inplace=True)
类别不平衡处理
再缩放
先从一般的回归模型来看,样本X通过建立的模型(1),而得出估计值y,其实是个概率,一般我们假设正例和负例是对等的,比如逻辑回归得出是1的概率大于0.5,那么就预测是1,即判断为正例。
(1)
那么这个y实际是正例的可能性,那么y/(1-y)就是正例可能性和负例可能性的比值,阈值设为0.5就是分类器认为正例可能性和负例可能性比为1,即可能性是一样的:
则预测为正例(2)
然而,当训练集中正、反例的数目不同时,令表示正例数目,表示反例数目,则观测几率是,由于我们通常假设训练集是真实样本总体的无偏估计,因此观测几率就代表了真实几率。于是只要分类器的预测几率高于观测几率就应判定为正例,即
则预测为正例(3)
但是,我们的分类器是基于式(2)进行比较决策,因此,需对其预测值进行调整,使其基于式(2)决策时,实际上是在执行式(3),可以在不等式(3)两边同乘,
只需要令
那么最后就是使用
(4)
这个就是类不平衡的一个决策:再缩放(rescaling)。
这个可以形象的看为是对易错题或难题放宽要求,改的松点。就像高考一样,让新疆或是少数民族等地区的本科线设低点,从而加大那个地区人的录取概率。
再缩放的思想虽简单,但实际操作却不平凡,主要是因为“训练集是真实样本总体的无偏采样”这个假设往往并不成立,也就是说,我们未必能有效地基于训练集观测几率来推断出真实几率,而且如果是多分类问题就不好设置了。
欠采样(undersampling)
欠采样比较简单,就是减少进入训练的多数类的样本,可以随机下采样,但是容易丢失一些特征信息,也可以聚类后再按抽取各类的特征的代表来下采样,会比较合理点,但下采样都会丢失少数类样本的信息,开销小,也容易实现。
原型生成(prototype generation)
给定数据集S, 原型生成算法将生成一个子集S’, 其中|S’| < |S|, 但是子集并非来自于原始数据集. 意思就是说: 原型生成方法将减少数据集的样本数量, 剩下的样本是由原始数据集生成的, 而不是直接来源于原始数据集.
ClusterCentroids函数实现了上述功能: 每一个类别的样本都会用K-Means算法的中心点来进行合成, 而不是随机从原始样本进行抽取.
from imblearn.under_sampling import ClusterCentroids
cc = ClusterCentroids(random_state=0)
X = train[list(set(features)-set(["Cabin","Name","Ticket"]))]
y = train[target]
X_resampled, y_resampled = cc.fit_sample(X, y)
print(sorted(Counter(y_resampled).items()))
原型选择(prototype selection)
与原型生成不同的是, 原型选择算法是直接从原始数据集中进行抽取. 抽取的方法大概可以分为两类: (i) 可控的下采样技术(the controlled under-sampling techniques) ; (ii) the cleaning under-sampling techniques( 清洗的下采样技术). 第一类的方法可以由用户指定下采样抽取的子集中样本的数量; 第二类方法则不接受这种用户的干预.
#Controlled under-sampling techniques
#RandomUnderSampler函数是一种快速并十分简单的方式来平衡各个类别的数据: 随机选取数据的子集.
from imblearn.under_sampling import RandomUnderSampler
rus = RandomUnderSampler(random_state=0)
X_resampled, y_resampled = rus.fit_sample(X, y)
print(sorted(Counter(y_resampled).items()))
from imblearn.under_sampling import RandomUnderSampler
rus = RandomUnderSampler(random_state=0, replacement=True)
X_resampled, y_resampled = rus.fit_sample(X, y)
print(sorted(Counter(y_resampled).items()))
#NearMiss函数则添加了一些启发式(heuristic)的规则来选择样本, 通过设定version参数来实现三种启发式的规则.
#NearMiss-1: 选择离N个近邻的负样本的平均距离最小的正样本;
#NearMiss-2: 选择离N个负样本最远的平均距离最小的正样本;
#NearMiss-3: 是一个两段式的算法. 首先, 对于每一个负样本, 保留它们的M个近邻样本; 接着, 那些到N个近邻样本平均距离最大的正样本将被选择.
from imblearn.under_sampling import NearMiss
nm1 = NearMiss( version=1)
X_resampled_nm1, y_resampled = nm1.fit_sample(X, y)
print(sorted(Counter(y_resampled).items()))
过采样(Over-sampling)
朴素随机过采样
针对不平衡数据, 最简单的一种方法就是生成少数类的样本, 这其中最基本的一种方法就是: 从少数类的样本中进行随机采样来增加新的样本, RandomOverSampler 函数就能实现上述的功能.
from imblearn.over_sampling import RandomOverSampler
ros = RandomOverSampler(random_state=0)
X_resampled, y_resampled = ros.fit_sample(X, y)
print(sorted(Counter(y_resampled).items()))
SMOTE和ADASYN
Synthetic Minority Oversampling Technique (SMOTE)
Adaptive Synthetic (ADASYN) .
随机过采样可能导致信息偏差,就是样本会偏向某些形态的部分,而有的形态的样本变少了 ,所以有了SMOTE和ADASYN
SMOTE: 对于少数类样本a, 随机选择一个最近邻的样本b, 然后从a与b的连线上随机选取一个点c作为新的少数类样本;
ADASYN: 关注的是在那些基于K最近邻分类器被错误分类的原始样本附近生成新的少数类样本
这两个本质都是找相似的样本,生成长得像的。
from imblearn.over_sampling import SMOTE, ADASYN
X_resampled_smote, y_resampled_smote = SMOTE().fit_sample(X, y)
sorted(Counter(y_resampled_smote).items())
X_resampled_adasyn, y_resampled_adasyn = ADASYN().fit_sample(X, y)
print(sorted(Counter(y_resampled).items()))
SMOTE的变体
相对于基本的SMOTE算法, 关注的是所有的少数类样本, 这些情况可能会导致产生次优的决策函数, 因此SMOTE就产生了一些变体: 这些方法关注在最优化决策函数边界的一些少数类样本, 然后在最近邻类的相反方向生成样本.
SMOTE函数中的kind参数控制了选择哪种变体, (i) borderline1, (ii) borderline2, (iii) svm:
from imblearn.over_sampling import BorderlineSMOTE ,SVMSMOTE ,KMeansSMOTE
X_resampled, y_resampled = BorderlineSMOTE(kind="borderline-1").fit_sample(X, y)
print(sorted(Counter(y_resampled).items()))
SMOTE算法与ADASYN都是基于同样的算法来合成新的少数类样本: 对于少数类样本a, 从它的最近邻中选择一个样本b, 然后在两点的连线上随机生成一个新的少数类样本, 不同的是对于少数类样本的选择.
原始的SMOTE: kind='regular' , 随机选取少数类的样本.
The borderline SMOTE: kind='borderline1' or kind='borderline2'
此时, 少数类的样本分为三类: (i) 噪音样本(noise), 该少数类的所有最近邻样本都来自于不同于样本a的其他类别; (ii) 危险样本(in danger), 至少一半的最近邻样本来自于同一类(不同于a的类别); (iii) 安全样本(safe), 所有的最近邻样本都来自于同一个类.
这两种类型的SMOTE使用的是危险样本来生成新的样本数据, 对于 Borderline-1 SMOTE, 最近邻中的随机样本b与该少数类样本a来自于不同的类; 不同的是, 对于 Borderline-2 SMOTE , 随机样本b可以是属于任何一个类的样本;
SVM SMOTE: kind='svm', 使用支持向量机分类器产生支持向量然后再生成新的少数类样本.
过采样与下采样的结合
在之前的SMOTE方法中, 当由边界的样本与其他样本进行过采样差值时, 很容易生成一些噪音数据. 因此, 在过采样之后需要对样本进行清洗. 这样, 第三节中涉及到的TomekLink 与 EditedNearestNeighbours方法就能实现上述的要求. 所以就有了两种结合过采样与下采样的方法: (i) SMOTETomek and (ii) SMOTEENN.
from imblearn.combine import SMOTEENN
smote_enn = SMOTEENN(random_state=0)
X_resampled, y_resampled = smote_enn.fit_sample(X, y)
print(sorted(Counter(y_resampled).items()))
from imblearn.combine import SMOTETomek
smote_tomek = SMOTETomek(random_state=0)
X_resampled, y_resampled = smote_tomek.fit_sample(X, y)
print(sorted(Counter(y_resampled).items()))
Ensemble集成组合方法
一个不均衡的数据集能够通过多个均衡的子集来实现均衡, 比如抽取子集分别训练,再投票加权组合等等,imblearn.ensemble模块能实现上述功能.
EasyEnsemble 通过对原始的数据集进行随机下采样实现对数据集进行集成.
#from imblearn.ensemble import Ensemble
#新版改为EasyEnsembleClassifier
from imblearn.ensemble import EasyEnsembleClassifier
ee = EasyEnsembleClassifier(random_state=0, n_estimators=10)
print(sorted(Counter(y_resampled).items()))
BalanceCascade
与上述方法不同的是, BalanceCascade(级联平衡)的方法通过使用分类器(estimator参数)来确保那些被错分类的样本在下一次进行子集选取的时候也能被采样到. 同样, n_max_subset 参数控制子集的个数, 以及可以通过设置bootstrap=True来使用bootstraping(自助法).
#新版使用BalancedBaggingClassifier
from imblearn.ensemble import BalancedBaggingClassifier
#from sklearn.linear_model import LogisticRegression
bbc = BalancedBaggingClassifier(random_state=0)
X_resampled, y_resampled = bbc.fit(X, y)
print(sorted(Counter(y_resampled).items()))
阈值移动
基于原始训练集进行学习,但在用训练好的分类器进行预测时,将再缩放的公式嵌入到决策过程中,称为“阈值移动”。
在二分类任务中,我们将样本属于正类的概率记为p,因此样本属于负类的概率就是1-p。当p/(1-p)>1时,我们把样本分为正类。但这是在样本均衡的情况下,也就是说正负样本的比例接近于1,此时分类阈值为0.5。如果样本不均衡,那么我们需要在预测时修改分类阈值。
假设在数据集中有m个正样本,n个负样本,那么正负样本的观测几率为m/n(样本均衡的情况下观测几率为1)。在进行分类时,如果此时的几率p'/(1-p')大于实际的观测几率m/n,我们才把样本分为正类。此时m/(m+n) 取代0.5成为新的分类阈值。
值得一提的是,“再缩放”也是“代价敏感学习”(cost-sensitive learning)的基础,在代价敏感学习中将式(3)中的用代替即可,其中是将正例误分为反例的代价,是将反例误分为正例的代价。
参考:
SUN, Y., WONG, A. K. C., & KAMEL, M. S. (2009). CLASSIFICATION OF IMBALANCED DATA: A REVIEW. International Journal of Pattern Recognition and Artificial Intelligence, 23(04), 687–719. doi:10.1142/s0218001409007326
A review of boosting methods for imbalanced data classification
Learning from Class-Imbalanced Data in Wireless Sensor Networks
Learning from class-imbalanced data
python数据分析与数据化运营