不均衡学习
一、簡(jiǎn)介
? ? ? ? ? ?在很多場(chǎng)景的數(shù)據(jù)集中,都會(huì)出現(xiàn)某一類數(shù)據(jù)的數(shù)量遠(yuǎn)遠(yuǎn)多于其它類的數(shù)據(jù),一般都是以二分類的類別不平衡問(wèn)題為主。一個(gè)簡(jiǎn)單的理解,假如某個(gè)數(shù)據(jù)集,10萬(wàn)個(gè)正樣本(正常用戶標(biāo)簽為0)與1000個(gè)負(fù)樣本(有問(wèn)題用戶標(biāo)簽為1),正負(fù)樣本比例為100:1,如果模型學(xué)習(xí)每一次梯度下降使用全量樣本,負(fù)樣本的權(quán)重不到1/100,即使完全不學(xué)習(xí)負(fù)樣本的信息,準(zhǔn)確率也有99%,所以實(shí)際應(yīng)用中不能完全用準(zhǔn)確率衡量模型的效果,還會(huì)使用AUC等指標(biāo)衡量模型的表現(xiàn),但是依然沒有辦法保證模型將負(fù)樣本很好的學(xué)習(xí),這個(gè)例子就是說(shuō)明數(shù)據(jù)集中正例和負(fù)例不平衡。為了使模型即對(duì)正例有很高的的準(zhǔn)確率也對(duì)負(fù)例有很好的準(zhǔn)確率,需要保持?jǐn)?shù)據(jù)集相對(duì)平衡。
二、解決數(shù)據(jù)不平衡方法
? ? ? ?通常解決數(shù)據(jù)不平衡的方法有下探、半監(jiān)督學(xué)習(xí)、標(biāo)簽分裂、代價(jià)敏感、采樣算法,下面為具體介紹:
(一) 下探
? ? ? 下探是最直接解決風(fēng)控場(chǎng)景樣本不均衡的方法。所謂下探,就是對(duì)評(píng)分較低被拒絕的人不進(jìn)行監(jiān)管,犧牲一部分收益,來(lái)積累壞樣本,供后續(xù)模型學(xué)習(xí)。此外,隨著業(yè)務(wù)開展,后續(xù)模型迭代的時(shí)候,使用的樣本是有偏的,下探同樣可以解決這個(gè)問(wèn)題。
(二)?半監(jiān)督學(xué)習(xí)
? ? ? ?將有問(wèn)題用戶的數(shù)據(jù)通過(guò)半監(jiān)督的方法逐漸生成標(biāo)簽,然后帶入模型中進(jìn)行訓(xùn)練。比較典型分方法有拒絕演繹、暴力半監(jiān)督等等。
? ? ? ?1)拒絕演繹
? ? ? ? ?拒絕演繹或者叫拒絕推斷,是一種根據(jù)經(jīng)驗(yàn)對(duì)低分客戶進(jìn)行百分比采樣的方法。比如最低分的客群百分之五十視為壞人,其次百分之四十等等。
? ? ? ?2)暴力半監(jiān)督
? ? ? ? 比較粗暴的做法是將樣本的每一種標(biāo)簽方式進(jìn)行窮舉,帶入模型看對(duì)模型是否有幫助,效率較低,容易過(guò)擬合。
? ? ? ?3)模型篩選
? ? ? ? 用訓(xùn)練過(guò)的其他模型對(duì)無(wú)標(biāo)簽樣本打標(biāo)簽,然后模型進(jìn)行訓(xùn)練。很多公司會(huì)用當(dāng)前模型在上面做預(yù)測(cè),然后帶入模型繼續(xù)訓(xùn)練。很不推薦這樣做,效果一般是很差的。可以考慮無(wú)監(jiān)督算法或者用很舊的樣本做訓(xùn)練然后做預(yù)測(cè)。
(三)?標(biāo)簽分裂
? ? ? ? ?我們有時(shí)候會(huì)不止使用傳統(tǒng)的一些定義來(lái)定義好壞。而是通過(guò)一些聚類手段對(duì)數(shù)據(jù)進(jìn)行切分,然后分別在自己的樣本空間內(nèi)單獨(dú)學(xué)習(xí)。基于模型的比如kmeans,分層聚類等等。基于經(jīng)驗(yàn)的比如將失聯(lián)客戶、欺詐客戶拆開,單獨(dú)建模。
簡(jiǎn)單的理解如下面這個(gè)例子:
? ? ? ?張三生了病,她的失散多年的哥哥找到有2家比較好的醫(yī)院,醫(yī)院A和醫(yī)院B供?張三選擇就醫(yī)。
? ? ? ?張三的哥哥多方打聽,搜集了這兩家醫(yī)院的統(tǒng)計(jì)數(shù)據(jù),它們是這樣的:
? ? ? ?醫(yī)院A最近接收的1000個(gè)病人里,有900個(gè)活著,100個(gè)死了。
? ? ? ?醫(yī)院B最近接收的1000個(gè)病人里,有800個(gè)活著,200個(gè)死了。
? ? ? 作為對(duì)統(tǒng)計(jì)學(xué)懵懵懂懂的普通人來(lái)說(shuō),看起來(lái)最明智的選擇應(yīng)該是醫(yī)院A對(duì)吧,病人存活率很高有90%啊!總不可能選醫(yī)院B吧,存活率只有80%啊。呵呵,如果?張三的選擇是醫(yī)院A,那么她就中計(jì)了。
? ? ? 就這么說(shuō)吧,如果醫(yī)院A最近接收的1000個(gè)病人里,有100個(gè)病人病情很嚴(yán)重,900個(gè)病人病情并不嚴(yán)重。
? ? ? 在這100個(gè)病情嚴(yán)重的病人里,有30個(gè)活下來(lái)了,其他70人死了。所以病重的病人在醫(yī)院A的存活率是30%。
? ? ? 而在病情不嚴(yán)重的900個(gè)病人里,870個(gè)活著,30個(gè)人死了。所以病情不嚴(yán)重的病人在醫(yī)院A的存活率是96.7%。
? ? ? 在醫(yī)院B最近接收的1000個(gè)病人里,有400個(gè)病情很嚴(yán)重,其中210個(gè)人存活,因此病重的病人在醫(yī)院B的存活率是52.5%。
? ? ? 有600個(gè)病人病情不嚴(yán)重,590個(gè)人存活,所以病情不嚴(yán)重的病人在醫(yī)院B的存活率是98.3%。
更直觀的如下面圖片所示:
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?? ?
? ? ? ?實(shí)際上,我們剛剛看到的例子,就是統(tǒng)計(jì)學(xué)中著名的黑魔法之一——辛普森悖論(Simpson's paradox)。辛普森悖論就是當(dāng)你把數(shù)據(jù)拆開細(xì)看的時(shí)候,細(xì)節(jié)和整體趨勢(shì)完全不同的現(xiàn)象。
(四)?代價(jià)敏感
? ? ? ?代價(jià)敏感學(xué)習(xí)則是利用不同類別的樣本被誤分類而產(chǎn)生不同的代價(jià),使用這種方法解決數(shù)據(jù)不平衡問(wèn)題。而且有很多研究表明,代價(jià)敏感學(xué)習(xí)和樣本不平衡問(wèn)題有很強(qiáng)的聯(lián)系,并且使用代價(jià)敏感學(xué)習(xí)的方法解決不平衡學(xué)習(xí)問(wèn)題要優(yōu)于使用隨機(jī)采樣的方法。
? ? ?1) 把誤分類代價(jià)作為數(shù)據(jù)集的權(quán)重,然后采用 Bootstrap 采樣方法選擇具有最好的數(shù)據(jù)分布的數(shù)據(jù)集;
? ? ?2) 以集成學(xué)習(xí)的模式來(lái)實(shí)現(xiàn)代價(jià)最小化的技術(shù),這種方法可以選擇很多標(biāo)準(zhǔn)的學(xué)習(xí)算法作為集成學(xué)習(xí)中的弱分類器;
? ? ?3) 把代價(jià)敏感函數(shù)或者特征直接合并到分類器的參數(shù)中,這樣可以更好的擬合代價(jià)敏感函數(shù)。由于這類技術(shù)往往都具有特定的參數(shù),因此這類方法沒有統(tǒng)一的框架;
(五)?采樣算法
- 樸素隨機(jī)過(guò)采樣
- SMOTE
? ? ? ? 對(duì)于少數(shù)類樣本a, 隨機(jī)選擇一個(gè)最近鄰的樣本b, 然后從a與b的連線上隨機(jī)選取一個(gè)點(diǎn)c作為新的少數(shù)類樣本;但是,SMOTE容易出現(xiàn)過(guò)泛化和高方差的問(wèn)題,而且,容易制造出重疊的數(shù)據(jù)。
為了克服SMOTE的缺點(diǎn),Adaptive Synthetic Sampling方法被提出,主要包括:Borderline-SMOTE和Adaptive Synthetic Sampling(ADA-SYN)算法。
Borderline-SMOTE:對(duì)靠近邊界的minority樣本創(chuàng)造新數(shù)據(jù)。其與SMOTE的不同是:SMOTE是對(duì)每一個(gè)minority樣本產(chǎn)生綜合新樣本,而Borderline-SMOTE僅對(duì)靠近邊界的minority樣本創(chuàng)造新數(shù)據(jù)。如下圖,只對(duì)A中的部分?jǐn)?shù)據(jù)進(jìn)行操作:
? ? ? ? ? ? ? ? ? ? ? ? ??
?
? ? ? ? 這個(gè)圖中展示了該方法的實(shí)現(xiàn)過(guò)程,我們可以發(fā)現(xiàn)和SMOTE方法的不同之處:SMOTE對(duì)于每一個(gè)少數(shù)類樣本都會(huì)產(chǎn)生合成樣本,但是Borderline-SMOTE只會(huì)對(duì)鄰近邊界的少數(shù)類樣本生成合成數(shù)據(jù)。ADA-SYN:根據(jù)majority和minority的密度分布,動(dòng)態(tài)改變權(quán)重,決定要generate多少minority的新數(shù)據(jù)。
? ? ? ? 相對(duì)于基本的SMOTE算法, 關(guān)注的是所有的少數(shù)類樣本, 這些情況可能會(huì)導(dǎo)致產(chǎn)生次優(yōu)的決策函數(shù)。
因此SMOTE就產(chǎn)生了一些變體,這些方法關(guān)注在最優(yōu)化決策函數(shù)邊界的一些少數(shù)類樣本, 然后在最近鄰類的相反方向生成樣本。、 SMOTE函數(shù)中的kind參數(shù)控制了選擇哪種變體
- regular
- borderline1
- borderline2
- svm
三、實(shí)際應(yīng)用
? ? ? ?目前應(yīng)用最多的是smote中變體為borderline1
1)構(gòu)建baseline - LR模型
import glob import numpy as np import pandas as pd import lightgbm as lgb from sklearn.metrics import roc_auc_score,roc_curve,auc from sklearn.model_selection import train_test_split from sklearn.linear_model import LogisticRegression from sklearn.model_selection import GridSearchCV as gscv from sklearn.neighbors import KNeighborsClassifier data = pd.read_csv('xxxxx.txt')train = data[data.obs_mth != '2018-11-30'].reset_index().copy() evl = data[data.obs_mth == '2018-11-30'].reset_index().copy()x = train[feature_lst] y = train['bad_ind']evl_x = evl[feature_lst] evl_y = evl['bad_ind']lr_model = LogisticRegression(C=0.1,class_weight='balanced') lr_model.fit(x,y)y_pred = lr_model.predict_proba(x)[:,1] fpr_lr_train,tpr_lr_train,_ = roc_curve(y,y_pred) train_ks = abs(fpr_lr_train - tpr_lr_train).max() print('train_ks : ',train_ks)y_pred = lr_model.predict_proba(evl_x)[:,1] fpr_lr,tpr_lr,_ = roc_curve(evl_y,y_pred) evl_ks = abs(fpr_lr - tpr_lr).max() print('evl_ks : ',evl_ks)from matplotlib import pyplot as plt plt.plot(fpr_lr_train,tpr_lr_train,label = 'train LR') plt.plot(fpr_lr,tpr_lr,label = 'evl LR') plt.plot([0,1],[0,1],'k--') plt.xlabel('False positive rate') plt.ylabel('True positive rate') plt.title('ROC Curve') plt.legend(loc = 'best') plt.show()? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?
2) 優(yōu)化
? ? ? 先用lgb做預(yù)測(cè),然后做前融合,相比于不修改損失函數(shù)的xgb,lgb的優(yōu)勢(shì)只是比較快,思想類似于對(duì)訓(xùn)練樣本做異常點(diǎn)檢測(cè)只不過(guò)不是根據(jù)數(shù)據(jù)內(nèi)部分布差異,而是使用精準(zhǔn)度更高的集成模型,將難以辨認(rèn)的樣本,視為噪音。
?首先做網(wǎng)格調(diào)參,給lgb找一組較好的參數(shù)
train_x,test_x,train_y,test_y = train_test_split(x,y,random_state=0,test_size=0.4)params = {'boosting_type':'gbdt','objective':'binary','metric':'auc','nthread':4,'learning_rate':0.1,'num_leaves':30,'max_depth':5,'subsample':0.8,'colsample_bytree':0.8,}data_train = lgb.Dataset(train_x,train_y)cv_results = lgb.cv(params,data_train,num_boost_round = 1000,nfold = 5,stratified = False,shuffle = True,metrics = 'auc',early_stopping_rounds = 100,seed = 0) print('best n_estimators:',len(cv_results['auc-mean'])) print('best cv score:',pd.Series(cv_results['auc-mean']).max())best n_estimators: 24 best cv score: 0.8097663177199287 def lgb_test(train_x,train_y,test_x,test_y):clf =lgb.LGBMClassifier(boosting_type = 'gbdt',objective = 'binary',metric = 'auc',learning_rate = 0.1,n_estimators = 24,max_depth = 4,num_leaves = 25,max_bin = 40,min_data_in_leaf = 5,bagging_fraction = 0.6,bagging_freq = 0,feature_fraction = 0.8,)clf.fit(train_x,train_y,eval_set = [(train_x,train_y),(test_x,test_y)],eval_metric = 'auc')return clf,clf.best_score_['valid_1']['auc'], lgb_model , lgb_auc = lgb_test(train_x,train_y,test_x,test_y) feature_importance = pd.DataFrame({'name':lgb_model.booster_.feature_name(),'importance':lgb_model.feature_importances_}).sort_values(by=['importance'],ascending=False)pred = lgb_model.predict_proba(train_x)[:,1] fpr_lgb,tpr_lgb,_ = roc_curve(train_y,pred) print(abs(fpr_lgb - tpr_lgb).max())pred = lgb_model.predict_proba(test_x)[:,1] fpr_lgb,tpr_lgb,_ = roc_curve(test_y,pred) print(abs(fpr_lgb - tpr_lgb).max())pred = lgb_model.predict_proba(evl_x)[:,1] fpr_lgb,tpr_lgb,_ = roc_curve(evl_y,pred) print(abs(fpr_lgb - tpr_lgb).max())0.5064991567297175 0.48909811193341235 0.41935471928695134粗略調(diào)參的lgb比lr無(wú)顯著提升,下面進(jìn)行權(quán)重調(diào)整。前后各取部分錯(cuò)分樣本,減小權(quán)重,其余樣本為1。雖然后面還會(huì)給予新的權(quán)重,但是這部分權(quán)重永遠(yuǎn)只有正常樣本的固定比例。
sample = x[feature_lst] sample['bad_ind'] = y sample['pred'] = lgb_model.predict_proba(x)[:,1] sample = sample.sort_values(by=['pred'],ascending=False).reset_index() sample['rank'] = np.array(sample.index)/75522def weight(x,y):if x == 0 and y < 0.1:return 0.1elif x == 1 and y > 0.7:return 0.1else:return 1sample['weight'] = sample.apply(lambda x: weight(x.bad_ind,x['rank']),axis = 1)def lr_wt_predict(train_x,train_y,evl_x,evl_y,weight):lr_model = LogisticRegression(C=0.1,class_weight='balanced')lr_model.fit(train_x,train_y,sample_weight = weight )y_pred = lr_model.predict_proba(train_x)[:,1]fpr_lr,tpr_lr,_ = roc_curve(train_y,y_pred)train_ks = abs(fpr_lr - tpr_lr).max()print('train_ks : ',train_ks)y_pred = lr_model.predict_proba(evl_x)[:,1]fpr_lr,tpr_lr,_ = roc_curve(evl_y,y_pred)evl_ks = abs(fpr_lr - tpr_lr).max()print('evl_ks : ',evl_ks)lr_wt_predict(sample[feature_lst],sample['bad_ind'],evl_x,evl_y,sample['weight'])train_ks : 0.4602564513254416 evl_ks : 0.4289610959476374? ? ? ?此時(shí)的lr,相比于最開始的lr,提升了1個(gè)百分點(diǎn)。這里省略了一些其他的探索,由于其他算法實(shí)驗(yàn)效果不理想,最終選取lgb作為篩選樣本的工具。接下來(lái)考慮基于差值思想的過(guò)采樣方法,增加一部分虛擬的負(fù)樣本。這里需要注意,之前權(quán)重減小的樣本是不應(yīng)該用來(lái)做過(guò)采樣的。所以將訓(xùn)練數(shù)據(jù)先拆分成兩部分。weight=1的做過(guò)采樣,其余的不變。
osvp_sample = sample[sample.weight == 1].drop(['pred','index','weight'],axis = 1) osnu_sample = sample[sample.weight < 1].drop(['pred','index',],axis = 1)train_x_osvp = osvp_sample[feature_lst] train_y_osvp = osvp_sample['bad_ind']#下面做基于borderline1的smote算法做過(guò)采樣def lr_predict(train_x,train_y,evl_x,evl_y):lr_model = LogisticRegression(C=0.1,class_weight='balanced')lr_model.fit(train_x,train_y)y_pred = lr_model.predict_proba(train_x)[:,1]fpr_lr,tpr_lr,_ = roc_curve(train_y,y_pred)train_ks = abs(fpr_lr - tpr_lr).max()print('train_ks : ',train_ks)y_pred = lr_model.predict_proba(evl_x)[:,1]fpr_lr,tpr_lr,_ = roc_curve(evl_y,y_pred)evl_ks = abs(fpr_lr - tpr_lr).max()print('evl_ks : ',evl_ks)return train_ks,evl_ksfrom imblearn.over_sampling import SMOTE,RandomOverSampler,ADASYN smote = SMOTE(k_neighbors=15, kind='borderline1', m_neighbors=4, n_jobs=1,out_step='deprecated', random_state=0, ratio=None,svm_estimator='deprecated') rex,rey = smote.fit_resample(train_x_osvp,train_y_osvp) print('badpctn:',rey.sum()/len(rey)) df_rex = pd.DataFrame(rex) df_rex.columns =feature_lst df_rex['weight'] = 1 df_rex['bad_ind'] = rey df_aff_ovsp = df_rex.append(osnu_sample) lr_predict(df_aff_ovsp[feature_lst],df_aff_ovsp['bad_ind'],evl_x,evl_y)badpctn: 0.5train_ks : 0.4859866821876423 evl_ks : 0.44085108654818894? ? ? 下面嘗試使用KNN做前融合, 主要思想是knn和邏輯回歸對(duì)樣本的分布先驗(yàn)是相同的, 雖然是弱分類器, 識(shí)別出的異常值應(yīng)該對(duì)模型影響更大。
? 首先尋找最優(yōu)k值
lr_model = LogisticRegression(C=0.1,class_weight='balanced') lr_model.fit(df_aff_ovsp[feature_lst],df_aff_ovsp['bad_ind'] )y_pred = lr_model.predict_proba(df_aff_ovsp[feature_lst])[:,1] fpr_lr_train,tpr_lr_train,_ = roc_curve(df_aff_ovsp['bad_ind'],y_pred) train_ks = abs(fpr_lr_train - tpr_lr_train).max() print('train_ks : ',train_ks)y_pred = lr_model.predict_proba(evl_x)[:,1] fpr_lr,tpr_lr,_ = roc_curve(evl_y,y_pred) evl_ks = abs(fpr_lr - tpr_lr).max() print('evl_ks : ',evl_ks)from matplotlib import pyplot as plt plt.plot(fpr_lr_train,tpr_lr_train,label = 'train LR') plt.plot(fpr_lr,tpr_lr,label = 'evl LR') plt.plot([0,1],[0,1],'k--') plt.xlabel('False positive rate') plt.ylabel('True positive rate') plt.title('ROC Curve') plt.legend(loc = 'best') plt.show()train_ks : 0.4859866821876423 evl_ks : 0.44085108654818894? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ??
可以看到,最終跨時(shí)間驗(yàn)證集上,是有3.5個(gè)百分點(diǎn)的提升的。而訓(xùn)練集上提升了5個(gè)百分點(diǎn),較為符合預(yù)期,過(guò)擬合的風(fēng)險(xiǎn)不是很大。
《新程序員》:云原生和全面數(shù)字化實(shí)踐50位技術(shù)專家共同創(chuàng)作,文字、視頻、音頻交互閱讀總結(jié)
- 上一篇: 无监督算法与异常检测
- 下一篇: Word2Vec(Efficient E