不均衡学习
一、簡介
? ? ? ? ? ?在很多場景的數據集中,都會出現某一類數據的數量遠遠多于其它類的數據,一般都是以二分類的類別不平衡問題為主。一個簡單的理解,假如某個數據集,10萬個正樣本(正常用戶標簽為0)與1000個負樣本(有問題用戶標簽為1),正負樣本比例為100:1,如果模型學習每一次梯度下降使用全量樣本,負樣本的權重不到1/100,即使完全不學習負樣本的信息,準確率也有99%,所以實際應用中不能完全用準確率衡量模型的效果,還會使用AUC等指標衡量模型的表現,但是依然沒有辦法保證模型將負樣本很好的學習,這個例子就是說明數據集中正例和負例不平衡。為了使模型即對正例有很高的的準確率也對負例有很好的準確率,需要保持數據集相對平衡。
二、解決數據不平衡方法
? ? ? ?通常解決數據不平衡的方法有下探、半監督學習、標簽分裂、代價敏感、采樣算法,下面為具體介紹:
(一) 下探
? ? ? 下探是最直接解決風控場景樣本不均衡的方法。所謂下探,就是對評分較低被拒絕的人不進行監管,犧牲一部分收益,來積累壞樣本,供后續模型學習。此外,隨著業務開展,后續模型迭代的時候,使用的樣本是有偏的,下探同樣可以解決這個問題。
(二)?半監督學習
? ? ? ?將有問題用戶的數據通過半監督的方法逐漸生成標簽,然后帶入模型中進行訓練。比較典型分方法有拒絕演繹、暴力半監督等等。
? ? ? ?1)拒絕演繹
? ? ? ? ?拒絕演繹或者叫拒絕推斷,是一種根據經驗對低分客戶進行百分比采樣的方法。比如最低分的客群百分之五十視為壞人,其次百分之四十等等。
? ? ? ?2)暴力半監督
? ? ? ? 比較粗暴的做法是將樣本的每一種標簽方式進行窮舉,帶入模型看對模型是否有幫助,效率較低,容易過擬合。
? ? ? ?3)模型篩選
? ? ? ? 用訓練過的其他模型對無標簽樣本打標簽,然后模型進行訓練。很多公司會用當前模型在上面做預測,然后帶入模型繼續訓練。很不推薦這樣做,效果一般是很差的。可以考慮無監督算法或者用很舊的樣本做訓練然后做預測。
(三)?標簽分裂
? ? ? ? ?我們有時候會不止使用傳統的一些定義來定義好壞。而是通過一些聚類手段對數據進行切分,然后分別在自己的樣本空間內單獨學習。基于模型的比如kmeans,分層聚類等等。基于經驗的比如將失聯客戶、欺詐客戶拆開,單獨建模。
簡單的理解如下面這個例子:
? ? ? ?張三生了病,她的失散多年的哥哥找到有2家比較好的醫院,醫院A和醫院B供?張三選擇就醫。
? ? ? ?張三的哥哥多方打聽,搜集了這兩家醫院的統計數據,它們是這樣的:
? ? ? ?醫院A最近接收的1000個病人里,有900個活著,100個死了。
? ? ? ?醫院B最近接收的1000個病人里,有800個活著,200個死了。
? ? ? 作為對統計學懵懵懂懂的普通人來說,看起來最明智的選擇應該是醫院A對吧,病人存活率很高有90%啊!總不可能選醫院B吧,存活率只有80%啊。呵呵,如果?張三的選擇是醫院A,那么她就中計了。
? ? ? 就這么說吧,如果醫院A最近接收的1000個病人里,有100個病人病情很嚴重,900個病人病情并不嚴重。
? ? ? 在這100個病情嚴重的病人里,有30個活下來了,其他70人死了。所以病重的病人在醫院A的存活率是30%。
? ? ? 而在病情不嚴重的900個病人里,870個活著,30個人死了。所以病情不嚴重的病人在醫院A的存活率是96.7%。
? ? ? 在醫院B最近接收的1000個病人里,有400個病情很嚴重,其中210個人存活,因此病重的病人在醫院B的存活率是52.5%。
? ? ? 有600個病人病情不嚴重,590個人存活,所以病情不嚴重的病人在醫院B的存活率是98.3%。
更直觀的如下面圖片所示:
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?? ?
? ? ? ?實際上,我們剛剛看到的例子,就是統計學中著名的黑魔法之一——辛普森悖論(Simpson's paradox)。辛普森悖論就是當你把數據拆開細看的時候,細節和整體趨勢完全不同的現象。
(四)?代價敏感
? ? ? ?代價敏感學習則是利用不同類別的樣本被誤分類而產生不同的代價,使用這種方法解決數據不平衡問題。而且有很多研究表明,代價敏感學習和樣本不平衡問題有很強的聯系,并且使用代價敏感學習的方法解決不平衡學習問題要優于使用隨機采樣的方法。
? ? ?1) 把誤分類代價作為數據集的權重,然后采用 Bootstrap 采樣方法選擇具有最好的數據分布的數據集;
? ? ?2) 以集成學習的模式來實現代價最小化的技術,這種方法可以選擇很多標準的學習算法作為集成學習中的弱分類器;
? ? ?3) 把代價敏感函數或者特征直接合并到分類器的參數中,這樣可以更好的擬合代價敏感函數。由于這類技術往往都具有特定的參數,因此這類方法沒有統一的框架;
(五)?采樣算法
- 樸素隨機過采樣
- SMOTE
? ? ? ? 對于少數類樣本a, 隨機選擇一個最近鄰的樣本b, 然后從a與b的連線上隨機選取一個點c作為新的少數類樣本;但是,SMOTE容易出現過泛化和高方差的問題,而且,容易制造出重疊的數據。
為了克服SMOTE的缺點,Adaptive Synthetic Sampling方法被提出,主要包括:Borderline-SMOTE和Adaptive Synthetic Sampling(ADA-SYN)算法。
Borderline-SMOTE:對靠近邊界的minority樣本創造新數據。其與SMOTE的不同是:SMOTE是對每一個minority樣本產生綜合新樣本,而Borderline-SMOTE僅對靠近邊界的minority樣本創造新數據。如下圖,只對A中的部分數據進行操作:
? ? ? ? ? ? ? ? ? ? ? ? ??
?
? ? ? ? 這個圖中展示了該方法的實現過程,我們可以發現和SMOTE方法的不同之處:SMOTE對于每一個少數類樣本都會產生合成樣本,但是Borderline-SMOTE只會對鄰近邊界的少數類樣本生成合成數據。ADA-SYN:根據majority和minority的密度分布,動態改變權重,決定要generate多少minority的新數據。
? ? ? ? 相對于基本的SMOTE算法, 關注的是所有的少數類樣本, 這些情況可能會導致產生次優的決策函數。
因此SMOTE就產生了一些變體,這些方法關注在最優化決策函數邊界的一些少數類樣本, 然后在最近鄰類的相反方向生成樣本。、 SMOTE函數中的kind參數控制了選擇哪種變體
- regular
- borderline1
- borderline2
- svm
三、實際應用
? ? ? ?目前應用最多的是smote中變體為borderline1
1)構建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) 優化
? ? ? 先用lgb做預測,然后做前融合,相比于不修改損失函數的xgb,lgb的優勢只是比較快,思想類似于對訓練樣本做異常點檢測只不過不是根據數據內部分布差異,而是使用精準度更高的集成模型,將難以辨認的樣本,視為噪音。
?首先做網格調參,給lgb找一組較好的參數
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粗略調參的lgb比lr無顯著提升,下面進行權重調整。前后各取部分錯分樣本,減小權重,其余樣本為1。雖然后面還會給予新的權重,但是這部分權重永遠只有正常樣本的固定比例。
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? ? ? ?此時的lr,相比于最開始的lr,提升了1個百分點。這里省略了一些其他的探索,由于其他算法實驗效果不理想,最終選取lgb作為篩選樣本的工具。接下來考慮基于差值思想的過采樣方法,增加一部分虛擬的負樣本。這里需要注意,之前權重減小的樣本是不應該用來做過采樣的。所以將訓練數據先拆分成兩部分。weight=1的做過采樣,其余的不變。
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算法做過采樣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和邏輯回歸對樣本的分布先驗是相同的, 雖然是弱分類器, 識別出的異常值應該對模型影響更大。
? 首先尋找最優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? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ??
可以看到,最終跨時間驗證集上,是有3.5個百分點的提升的。而訓練集上提升了5個百分點,較為符合預期,過擬合的風險不是很大。
《新程序員》:云原生和全面數字化實踐50位技術專家共同創作,文字、視頻、音頻交互閱讀總結
- 上一篇: 无监督算法与异常检测
- 下一篇: Word2Vec(Efficient E