金融风控实战——不均衡学习
上采樣/下采樣
下采樣,對于一個不均衡的數據,讓目標值(如0和1分類)中的樣本數據量相同,且以數據量少的一方的樣本數量為準。上采樣就是以數據量多的一方的樣本數量為標準,把樣本數量較少的類的樣本數量生成和樣本數量多的一方相同,稱為上采樣。
下采樣
獲取數據時一般是從分類樣本多的數據從隨機抽取等數量的樣本。
數據不平衡
在很多真實場景下,數據集往往是不平衡的。也就是說,在數據集中,有一類含有的數據要遠遠多于其他類的數據(類別分布不平衡)。在貸款場景下,我們主要介紹二分類中的類別不平衡問題。
常識告訴我們一家信用正常客戶的數據要遠遠多于欺詐客戶的。
考慮一個簡單的例子,10萬正樣本(正??蛻魳撕灋?)與1000個負樣本(欺詐客戶標簽為1),正負樣本比列為100:1,如果直接帶入模型中去學習,每一次梯度下降如果使用全量樣本,負樣本的權重只有不到1/100,即使完全不學習負樣本的信息,準確率也有超過99%,所以顯然我們絕不能以準確率來衡量模型的效果。但是實踐下面,我們其實也知道,即使用KS或者AUC來度量模型的表現,依然沒法保證模型能將負樣本很好的學習。而我們實際上需要得到一個分類器,既能對于正例有很高的準確率,同時又不會影響到負例的準確率。
類似于上面例子中的數據集,由于整個數據空間中,正例和負例的數據就是不平衡的。因此,這樣的不平衡數據集的產生往往是內在的。同時,也有很多其他的因素會造成數據的不平衡,例如,時間,存儲等。由于這些原因產生不平衡的數據集往往被稱為外在的。除了數據集的內在和外在,我們可能還要注意到數據集的相對不平衡以及絕對不平衡。假設上述例子中的數據集有100000條數據,負例和正例的比例為100:1,只包含1000個正例。明顯的,我們不能說1000個數據就是絕對小的,只不過相對于負例來說,它的數量相對較少。因此,這樣的數據集被認為是相對不平衡的。
解決方法
- 下探
- 半監督學習
- 標簽分裂
- 代價敏感
- 采樣算法
下探
最直接解決風控場景樣本不均衡的方法。
所謂下探,就是對評分較低被拒絕的人進行放款,犧牲一部分收益,來積累壞樣本,供后續模型學習。
這也是所有方法中最直接有效的。但是不是每一家公司都愿意承擔這部分壞賬。
此外我們之前提到過,隨著業務開展,后續模型迭代的時候,使用的樣本是有偏的,下探同樣可以解決這個問題。
半監督學習
將被模型拒絕客戶的數據通過半監督的方法逐漸生成標簽,然后帶入模型中進行訓練。比較典型分方法有拒絕演繹、暴力半監督等等。
1)拒絕演繹
拒絕演繹或者叫拒絕推斷,是一種根據經驗對低分客戶進行百分比采樣的方法。
比如最低分的客群百分之五十視為壞人,其次百分之四十等等。
效果沒有下探好。但不用額外有任何開銷。
參考資料:群內預習資料中的《信用風險評分卡研究》第十三章。
2)暴力半監督
比較粗暴的做法是將樣本的每一種標簽方式進行窮舉,帶入模型看對模型是否有幫助,效率較低,容易過擬合。
3)模型篩選
用訓練過的其他模型對無標簽樣本打標簽,然后模型進行訓練。很多公司會用當前模型在上面做預測,然后帶入模型繼續訓練。很不推薦這樣做,效果一般是很差的??梢钥紤]無監督算法或者用很舊的樣本做訓練然后做預測。
等等…
標簽分裂
我們有時候會不止使用傳統的逾期或者rollrate來定義好壞。而是通過一些聚類手段對數據進行切分,然后分別在自己的樣本空間內單獨學習。基于模型的比如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%。
畫成表格,就是這樣的——
你可以看到,在區分了病情嚴重和不嚴重的病人后,不管怎么看,最好的選擇都是醫院B。但是只看整體的存活率,醫院A反而是更好的選擇了。所謂遠看是汪峰,近看白巖松,就是這個道理。
實際上,我們剛剛看到的例子,就是統計學中著名的黑魔法之一——辛普森悖論(Simpson’s paradox)。辛普森悖論就是當你把數據拆開細看的時候,細節和整體趨勢完全不同的現象。
代價敏感學習
類似class_weight
代價敏感學習則是利用不同類別的樣本被誤分類而產生不同的代價,使用這種方法解決數據不平衡問題。而且有很多研究表明,代價敏感學習和樣本不平衡問題有很強的聯系,并且使用代價敏感學習的方法解決不平衡學習問題要優于使用隨機采樣的方法。
采樣算法
今天我們涉及的主要是過采樣方法
- 樸素隨機過采樣
- SMOTE
- ADASYN
樸素隨機過采樣
復制少數樣本
from sklearn.datasets import make_classification from collections import Counter X, y = make_classification(n_samples=5000, n_features=2, n_informative=2,n_redundant=0, n_repeated=0, n_classes=2,n_clusters_per_class=1,weights=[0.01, 0.99],class_sep=0.8, random_state=0) Counter(y) #Counter({1: 4923, 0: 77})from imblearn.over_sampling import RandomOverSampler ros = RandomOverSampler(random_state=0) X_resampled, y_resampled = ros.fit_resample(X, y) sorted(Counter(y_resampled).items()) #[(0, 4923), (1, 4923)]SMOTE
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只會對鄰近邊界的少數類樣本生成合成數據。
Borderline SMOTE-2和Borderline SMOTE-1是很相似的,區別是在獲得DANGER集合以后,對于DANGER中的每個樣本點xi:
- Borderline SMOTE-1:從少數類樣本集合P中獲得k個最近鄰樣本,再隨機選擇樣本點和xi做隨機的線性插值產生新的少數類樣本。(和普通SMOTE算法流程相同)
- Borderline SMOTE-2:從少數類樣本集合P和多數類樣本集合N中分別獲得k個最近鄰樣本Pk和Nk。設定一個比例α,在Pk中選出α比例的樣本點和xi做隨機的線性插值產生新的少數類樣本,方法同Borderline SMOTE-1;在Nk中選出1?α比例的樣本點和xi做隨機的線性插值產生新的少數類樣本,此處的隨機數范圍選擇的是(0,0.5),即便得產生的新的樣本點更靠近少數類樣本。
ADA-SYN:根據majority和minority的密度分布,動態改變權重,決定要generate多少minority的新數據。
k近鄰中,多數類樣本多,該少數類樣本合成數據多
基于聚類的隨機采樣(CBO)
基于聚類的隨機采樣方法可以用來解決類內不平衡問題,主要利用的聚類的方法。具體的過程如下:
隨機選擇K個樣本作為K個簇,并且計算K類樣本在特征空間的平均值,作為聚類中心;
對于剩下的每一個樣本,計算它和K個聚類中心的歐氏距離,根據歐式聚類將其分配到最近的類簇中;
更新每個簇的聚類中心,直到所有的樣本都用完;
采樣方法和集成方法的集成
目前已經有很多的方法把隨機采樣和集成學習的方法集成在一起,下面介紹兩種這樣的方法:
- SMOTEBoost
- DataBoost-IM
SMOTEBoost
SMOTEBoost主要是把SMOTE和AdaBoost.M2集成在一起,SMOTEBoost方法在每次Boost迭代過程中使用合成數據的方法。因此,每一次迭代過程中的分類器都會集中到更多的少數類樣本。
DataBoost-IM
DataBoost-IM主要是把數據生成技術和AdaBoost.M1方法結合在一起,主要根據不同類之間樣本的很難被學習到的比例。具體過程主要是如下:
相對于基本的SMOTE算法, 關注的是所有的少數類樣本, 這些情況可能會導致產生次優的決策函數。
因此SMOTE就產生了一些變體,這些方法關注在最優化決策函數邊界的一些少數類樣本, 然后在最近鄰類的相反方向生成樣本。
SMOTE函數中的kind參數控制了選擇哪種變體
- regular
- borderline1
- borderline2
- svm
接下來我們啟用上一節課的例子
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('Acard.txt') data.head() data.obs_mth.unique() #array(['2018-10-31', '2018-07-31', '2018-09-30', '2018-06-30', # '2018-11-30'], dtype=object) train = data[data.obs_mth != '2018-11-30'].reset_index().copy() evl = data[data.obs_mth == '2018-11-30'].reset_index().copy()feature_lst=['person_info','finance_info','credit_info','act_info']x = train[feature_lst] y = train['bad_ind']evl_x = evl[feature_lst] evl_y = evl['bad_ind']lr_model = LogisticRegression(C=0.1) 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() #train_ks : 0.41573985983413414 #evl_ks : 0.3928959732014397 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() #train_ks : 0.4482453222991063 #evl_ks : 0.4198642457760936
接下來先用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,"verbose":-1}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()) #[1] training's auc: 0.764327 valid_1's auc: 0.74748 #[2] training's auc: 0.81104 valid_1's auc: 0.795364 #[3] training's auc: 0.815393 valid_1's auc: 0.801769 #[4] training's auc: 0.819752 valid_1's auc: 0.804643 #[5] training's auc: 0.819358 valid_1's auc: 0.805129 #[6] training's auc: 0.821207 valid_1's auc: 0.805297 #[7] training's auc: 0.821572 valid_1's auc: 0.804743 #[8] training's auc: 0.822117 valid_1's auc: 0.80607 #[9] training's auc: 0.822494 valid_1's auc: 0.806053 #[10] training's auc: 0.821979 valid_1's auc: 0.805704 #[11] training's auc: 0.821362 valid_1's auc: 0.805741 #[12] training's auc: 0.822991 valid_1's auc: 0.806829 #[13] training's auc: 0.824437 valid_1's auc: 0.807311 #[14] training's auc: 0.82526 valid_1's auc: 0.807217 #[15] training's auc: 0.826336 valid_1's auc: 0.807852 #[16] training's auc: 0.826902 valid_1's auc: 0.807857 #[17] training's auc: 0.827597 valid_1's auc: 0.80819 #[18] training's auc: 0.827992 valid_1's auc: 0.808283 #[19] training's auc: 0.828076 valid_1's auc: 0.80852 #[20] training's auc: 0.828594 valid_1's auc: 0.808893 #[21] training's auc: 0.82915 valid_1's auc: 0.808473 #[22] training's auc: 0.829211 valid_1's auc: 0.808736 #[23] training's auc: 0.829284 valid_1's auc: 0.808657 #[24] training's auc: 0.829692 valid_1's auc: 0.808827 #0.5064991567297175 #0.48909811193341235 #0.41935471928695134粗略調參的lgb比lr無顯著提升,下面進行權重調整。
前后各取部分錯分樣本,減小權重,其余樣本為1。
雖然后面還會給予新的權重,但是這部分權重永遠只有正常樣本的固定比例。
此時的lr,相比于最開始的lr,提升了1個百分點。
這里省略了一些其他的探索,由于其他算法實驗效果不理想,最終選取lgb作為篩選樣本的工具。
接下來考慮基于差值思想的過采樣方法,增加一部分虛擬的負樣本。
這里需要注意,之前權重減小的樣本是不應該用來做過采樣的。 (權重小可能是噪聲)
所以將訓練數據先拆分成兩部分。weight=1的做過采樣,其余的不變。
下面做基于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 BorderlineSMOTE,RandomOverSampler,ADASYN smote = BorderlineSMOTE(k_neighbors=15, kind='borderline-1', m_neighbors=4, n_jobs=1,random_state=0) 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.5 #train_ks : 0.4828705350911966 #evl_ks : 0.439243398952811 #(0.4828705350911966, 0.439243398952811) 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個百分點,較為符合預期,過擬合的風險不是很大。
請同學們自行嘗試其他算法進行樣本篩選和其他采樣方法。
總結
以上是生活随笔為你收集整理的金融风控实战——不均衡学习的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 金融风控实战——集成学习
- 下一篇: 金融风控实战——模型融合