金融风控实战——模型融合
過采樣方法使用條件
(1)負樣本可以代表樣本空間
(2)數(shù)據(jù)是足夠干凈的(樣本、特征沒有噪聲)
過擬合
(1)增多數(shù)據(jù)
(2)特征篩選
(3)調參
(4)模型融合
模型融合
投票器模型融合
from sklearn import model_selection from sklearn.linear_model import LogisticRegression from sklearn.tree import DecisionTreeClassifier from sklearn.svm import SVC from sklearn.ensemble import VotingClassifier import pandas import warnings warnings.filterwarnings('ignore')data = "pima-indians-diabetes.data.csv" names = ['preg', 'plas', 'pres', 'skin', 'test', 'mass', 'pedi', 'age', 'class']#class是類別標簽 df = pandas.read_csv(data, names=names) df.head() df['class'].unique() #array([1, 0], dtype=int64)array = df.values X = array[:,0:8] Y = array[:,8] kfold = model_selection.KFold(n_splits=5,shuffle=True,random_state=2018)# 創(chuàng)建投票器的子模型 estimators = [] model_1 = LogisticRegression() estimators.append(('logistic', model_1))model_2 = DecisionTreeClassifier() estimators.append(('dt', model_2))model_3 = SVC() estimators.append(('svm', model_3))# 構建投票器融合 ensemble = VotingClassifier(estimators) result = model_selection.cross_val_score(ensemble, X, Y, cv=kfold) print(result.mean()) #0.7410067057125881Bagging
from sklearn.ensemble import BaggingClassifierdt = DecisionTreeClassifier() num = 100 kfold = model_selection.KFold(n_splits=5,shuffle=True,random_state=2018) model = BaggingClassifier(base_estimator=dt, n_estimators=num, random_state=2018) result = model_selection.cross_val_score(model, X, Y, cv=kfold) print(result.mean()) #0.764349376114082RandomForest
from sklearn.ensemble import RandomForestClassifier num_trees = 100 max_feature_num = 5 kfold = model_selection.KFold(n_splits=5, shuffle=True, random_state=2018) model = RandomForestClassifier(n_estimators=num_trees, max_features=max_feature_num) result = model_selection.cross_val_score(model, X, Y, cv=kfold) print(result.mean()) #0.7696120872591461Adaboost
from sklearn.ensemble import AdaBoostClassifier num_trees = 25 kfold = model_selection.KFold(n_splits=5, shuffle=True, random_state=2018) model = AdaBoostClassifier(n_estimators=num_trees, random_state=2018) result = model_selection.cross_val_score(model, X, Y, cv=kfold) print(result.mean()) #0.7513623631270689通常我們有兩種簡單加權平均的方法,一種是預測結果直接加權平均,一種是按秩加權平均。
-
預測結果直接加權平均時,需要注意的是如果不同模型預測的結果分布不太一致,如 A 模型預測結果區(qū)間一般是 [0, 0.4],而 B 模型的大多在 [0.6, 1],則需要先對 A、B 的預測結果做概率較正(probability calibration)。
-
按秩加權是指將預測結果在預測樣本集合中的序,加權平均。它的好處是不需要概率較正,但不能直接應用于線上實時預估場景中。
當然,模型融合(Stacking/Ensembling)有更 fancy 的用法,比如和交叉驗證結合,訓練多層的融合模型。
Blending&Stacking
Stacking
stacking是一種分層模型集成框架。以兩層為例,第一層由多個基學習器組成,其輸入為原始訓練集,第二層的模型則是以第一層基學習器的輸出作為特征加入訓練集進行再訓練,從而得到完整的stacking模型。
以5折劃分為例,我們將原始訓練集分為5折,分別記為fold1、fold2、fold3、fold4和fold5。此時我們使用fold2-fold5的數(shù)據(jù)來訓練基模型1,并對fold1進行預測,該預測值即作為基模型1對fold1生成的元特征;同樣地,使用fold1、fold3-fold5的數(shù)據(jù)來訓練基模型1,并對fold2進行預測,該預測值即作為基模型1對fold2生成的元特征;以此類推,得到基模型1對整個原始訓練集生成的元特征。同樣地,對其他基模型也采用相同的方法生成元特征,從而構成用于第二層模型(下記為元模型,meta model)訓練的完整元特征集。對于測試集,我們可以在每次基模型訓練好時預測,再將預測值做均值處理;也可以將基模型擬合全部的訓練集之后再對測試集進行預測。
需要注意的是,在生成第二層特征的時候,各個基模型要采用相同的Kfold,這樣得到的元特征的每一折(對應于之前的K折劃分)都將不會泄露進該折數(shù)據(jù)的目標值信息 ,從而盡可能的降低過擬合的風險。雖然如此,實際上我們得到的元特征還是存在一定程度上的信息泄露,比如我們在預測第二折的時候,是利用了第一折的目標值信息用于訓練基模型的,也就是說第一折的目標值信息雜糅在對第二折進行預測的基模型里。但是,實踐中,這種程度的信息泄露所造成的過擬合程度很小。
Blending
Blending與Stacking大致相同,只是Blending的主要區(qū)別在于訓練集不是通過K-Fold的CV策略來獲得預測值從而生成第二階段模型的特征,而是建立一個Holdout集,例如10%的訓練數(shù)據(jù),第二階段的stacker模型就基于第一階段模型對這10%訓練數(shù)據(jù)的預測值進行擬合。說白了,就是把Stacking流程中的K-Fold CV 改成 HoldOut CV。
1)比賽常用,基本上排名靠前的選手都會使用模型融合的方法,通常在千分位上有所提升;
2)實際工作中極少數(shù)場景可以使用,看似高級,實際業(yè)務中千分位的提升可有可無,部署30個模型費時費力,一旦出了問題查到死…
我們比賽時一般會使用一些特殊的方法進行多模型融合提高線上表現(xiàn)。在實際工作中可能用處不太廣泛。
感興趣的同學可以看一下這篇code Introduction to Ensembling/Stacking in Python
思考一下我們常用的邏輯回歸模型,在我們這個場景有什么優(yōu)勢?
LR+離散特征優(yōu)勢
在工業(yè)界,很少直接將連續(xù)值作為特征喂給邏輯回歸模型,而是將連續(xù)特征離散化為一系列0、1特征交給邏輯回歸模型,這樣做的優(yōu)勢有以下幾點:
1)稀疏向量內積乘法運算速度快,計算結果方便存儲,容易scalable(擴展)。
2)離散化后的特征對異常數(shù)據(jù)有很強的魯棒性:比如一個特征是年齡>30是1,否則0。如果特征沒有離散化,一個異常數(shù)據(jù)“年齡300歲”會給模型造成很大的干擾。
3)邏輯回歸屬于廣義線性模型,表達能力受限;單變量離散化為N個后,每個變量有單獨的權重,相當于為模型引入了非線性,能夠提升模型表達能力,加大擬合。
4)離散化后可以進行特征交叉,由M+N個變量變?yōu)?span id="ze8trgl8bvbq" class="katex--inline">M?NM*NM?N個變量,進一步引入非線性,提升表達能力。
5)特征離散化后,模型會更穩(wěn)定,比如如果對用戶年齡離散化,20-30作為一個區(qū)間,不會因為一個用戶年齡長了一歲就變成一個完全不同的人。當然處于區(qū)間相鄰處的樣本會剛好相反,所以怎么劃分區(qū)間是門學問。
6)離散后每一個特征的每一個取值對應著評分卡加減分數(shù),業(yè)務上更容易理解。
歸納一下就是:
1)計算簡單
2)簡化模型
3)增強模型的泛化能力,不易受噪聲的影響
4)業(yè)務性強
海量離散特征+LR是業(yè)內常見的一個做法。而少量連續(xù)特征+復雜模型是另外一種做法,例如GBDT。
模型是使用離散特征還是連續(xù)特征,其實是一個“海量離散特征+簡單模型” 同 “少量連續(xù)特征+復雜模型”的權衡。既可以離散化用線性模型,也可以用連續(xù)特征加深度學習。
人工特征 VS 機器特征
首先,海量離散特征+LR是業(yè)內常見的一個做法,但并不是Holy Grail,事實上這一般而言僅僅是因為LR的優(yōu)化算法更加成熟,而且可以在計算中利用稀疏特性進行更好的優(yōu)化—可謂不得已而為之。
事實證明GBDT和深度學習特征的加入對于CTR預測是有正面幫助的。如果這個問題思考地更深一點,其實當前深度學習網絡的最后一層,如果是binary classification,其實等同于LR。
所以說,通過人工/半人工的方式產生的features,跟深度神經網絡(無論之前用了怎樣的結構)最后學出來的representation,其實是異曲同工,區(qū)別在于深度學習一般而言會學出一個dense representation,而特征工程做出來的是一堆sparse representation。
某些時候,人工特征其實跟神經網絡經過幾層非線性之后的結果是高度相似的。
在暴力提取高階/非線性特征的本事上,機器肯定勝過人類。但是,就算最牛的機器智能,有時候都敵不過一些“人類常識”。尤其是業(yè)務的一些邏輯,可以認為是人腦在更大的一個數(shù)據(jù)集上pre-train出來的一些特征,其包含的信息量一定是大于你用于預測的dataset的。在這種情況下,往往厲害的人工features會outperform暴力的機器方法。
所以,特征離散化,從數(shù)學角度來說可以認為是增加robustness,但是更重要的,make sense of the data,將數(shù)據(jù)轉變成人類可以理解、可以validate的格式。人類的業(yè)務邏輯,當然也不是完美的。在當前機器智能還未征服“常識”這個領域之前,人類的business insights還是一個有力的補充(在很多case,甚至是最重要的部分)。在機器能夠完全掌握的范圍內,譬如圍棋,人類引以為傲的intuition已經無法抵抗機器的暴力計算了——所以在未來,我們一定會看到越來越多的機器智能開始侵入一些傳統(tǒng)上認為必須要依靠人類的“感覺”的一些領域。風控領域當然也不能躲過這個大的趨勢。
LR 適用于稀疏特征原因
假設有1w 個樣本, y類別0和1,100維特征,其中10個樣本都是類別1,而特征 f1的值為0,1,且剛好這10個樣本的 f1特征值都為1,其余9990樣本都為0(在高維稀疏的情況下這種情況很常見),我們都知道這種情況在樹模型的時候,很容易優(yōu)化出含一個使用 f1為分裂節(jié)點的樹直接將數(shù)據(jù)劃分的很好,但是當測試的時候,卻會發(fā)現(xiàn)效果很差,因為這個特征只是剛好偶然間跟 y擬合到了這個規(guī)律,這也是我們常說的過擬合。但是為什么線性模型就能對這種 case 處理的好?照理說線性模型在優(yōu)化之后不也會產生這樣一個式子嘛:
y=W1?f1+?+Wi?fi+?y=W_1 * f_1+?+W_i?f_i+? y=W1??f1?+?+Wi??fi?+?
其中 W1W_1W1?特別大以擬合這十個樣本。因為反正f1f_1f1?的值只有0和1,W1W_1W1?過大對其他9990樣本不會有任何影響。
原因是因為現(xiàn)在的模型普遍都會帶著正則項,而 lr 等線性模型的正則項是對權重的懲罰,也就是W1W_1W1?一旦過大,懲罰就會很大,進一步壓縮W1W_1W1?的值,使他不至于過大。
而樹模型則不一樣,樹模型的懲罰項通常為葉子節(jié)點數(shù)和深度等,而我們都知道,對于上面這種 case,樹只需要一個節(jié)點就可以完美分割9990和10個樣本,懲罰項極其之小。
LightGBM + LR 模型融合方案
大家都知道,在風控領域發(fā)展過程中,使用最多的方法就是邏輯回歸(LR),LR使用了Sigmoid變換將函數(shù)值映射到0~1區(qū)間,映射后的函數(shù)值就是一個人會違約的概率的預估值。
LR屬于線性模型,容易并行化,可以輕松處理上億條數(shù)據(jù),但是學習能力十分有限,需要大量的特征工程來增加模型的學習能力。將連續(xù)特征離散化,并對離散化的特征進行One-Hot編碼,最后對特征進行二階或者三階的特征組合,目的是為了得到非線性的特征。但又無法直接通過特征笛卡爾積解決,只能依靠人工經驗,耗時耗力同時并不一定會帶來效果提升。特征工程存在幾個難題:
- 連續(xù)變量切分點如何選取?
- 離散化為多少份合理?
- 選擇哪些特征交叉?
- 多少階交叉,二階,三階或更多?
一般都是按照經驗,不斷嘗試一些組合,然后根據(jù)線下評估選適當參數(shù)。
但是,使用GBDT編碼,一舉解決了上面的問題。確定切分點不在是憑主觀經驗,而是根據(jù)信息增益,客觀的選取切分點和份數(shù)。每棵決策樹從根節(jié)點到葉節(jié)點的路徑,會經過不同的特征,此路徑就是特征組合,而且包含了二階,三階甚至更多。
為什么不直接用GBDT?
而非要用GBDT+LR呢?因為GBDT在線預測比較困難,而且訓練時間復雜度高于LR。所以實際中,可以離線訓練GBDT,然后將該模型作為在線ETL的一部分。
在介紹這個模型之前,我們先來介紹兩個問題:
1)為什么建樹采用ensemble決策樹,而不是單棵的決策樹模型:
- 一棵樹的表達能力很弱,不足以表達多個有區(qū)分性的特征組合。
- 多棵樹的表達能力更強一些。可以更好的發(fā)現(xiàn)有效的特征和特征組合
2)為什么建樹采用GBDT而非RF:
- RF也是多棵樹,但從效果上有實踐證明不如GBDT。
- 且GBDT前面的樹,特征分裂主要體現(xiàn)對多數(shù)樣本有區(qū)分度的特征。
- 后面的樹,主要體現(xiàn)的是經過前N顆樹,殘差仍然較大的少數(shù)樣本。
- 優(yōu)先選用在整體上有區(qū)分度的特征,再選用針對少數(shù)樣本有區(qū)分度的特征,思路更加合理,這應該也是用GBDT的原因。
下面看一下二者是如何融合的。
GBDT用來對訓練集提取特征作為新的訓練輸入數(shù)據(jù),LR作為新訓練輸入數(shù)據(jù)的分類器。
具體步驟:
1)GBDT首先對原始訓練數(shù)據(jù)做訓練,得到一個二分類器,當然這里也需要利用網格搜索尋找最佳參數(shù)組合。
2)與通常做法不同的是,當GBDT訓練好做預測的時候,輸出的并不是最終的二分類概率值,而是要把模型中的每棵樹計算得到的預測概率值所屬的葉子結點位置記為1,這樣,就構造出了新的訓練數(shù)據(jù)。在用GBDT構造新的訓練數(shù)據(jù)時,采用的正是One-hot方法。并且由于每一弱分類器有且只有一個葉子節(jié)點輸出預測結果,所以在一個具有n個弱分類器、共計m個葉子結點的GBDT中,每一條訓練數(shù)據(jù)都會被轉換為 1 * m 維稀疏向量,且有n個元素為1,其余m-n 個元素全為0。
3)新的訓練數(shù)據(jù)構造完成后,下一步就要與原始的訓練數(shù)據(jù)中的label(輸出)數(shù)據(jù)一并輸入到Logistic Regression分類器中進行最終分類器的訓練。思考一下,在對原始數(shù)據(jù)進行GBDT提取為新的數(shù)據(jù)這一操作之后,數(shù)據(jù)不僅變得稀疏,而且由于弱分類器個數(shù),葉子結點個數(shù)的影響,可能會導致新的訓練數(shù)據(jù)特征維度過大的問題,因此,在Logistic Regression這一層中,可使用正則化來減少過擬合的風險,在Facebook的論文中采用的是L1正則化。
GBDT與LR的融合方式,Facebook的paper有個例子如下圖所示。
圖中Tree1、Tree2為通過GBDT模型學出來的兩顆樹,x為一條輸入樣本,遍歷兩棵樹后,x樣本分別落到兩顆樹的葉子節(jié)點上,每個葉子節(jié)點對應LR一維特征,那么通過遍歷樹,就得到了該樣本對應的所有LR特征。由于樹的每條路徑,是通過最小化均方差等方法最終分割出來的有區(qū)分性路徑,根據(jù)該路徑得到的特征、特征組合都相對有區(qū)分性,效果理論上不會亞于人工經驗的處理方式。
論文中GBDT的參數(shù),樹的數(shù)量最多500顆(500以上就沒有提升了),每棵樹的節(jié)點不多于12。
通過GBDT生成的特征,可直接作為LR的特征使用,省去人工處理分析特征的環(huán)節(jié)。同時,也可考慮將GBDT生成特征與LR原有特征結合起來使用。
GBDT模型的特點,非常適合用來挖掘有效的特征、特征組合。業(yè)界不僅GBDT+LR融合有實踐,GBDT+FM也有實踐,2014 Kaggle CTR競賽冠軍就是使用GBDT+FM(因子分解機)。主要是因為CTR領域有大量的稀疏特征,LR學習效果沒有FM好,但本節(jié)課只介紹XGBOOST+LR的方法,感興趣的同學可以自己去做嘗試。
import lightgbm as lgb import random import pandas as pd import numpy as np from sklearn.model_selection import train_test_split from sklearn.metrics import mean_squared_error from sklearn.linear_model import LogisticRegression from sklearn import metrics from sklearn.metrics import roc_curve from matplotlib import pyplot as plt import math 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() val = 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']val_x = val[feature_lst] val_y = val['bad_ind']lr_model = LogisticRegression(C=0.1,class_weight='balanced',solver='liblinear') 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(val_x)[:,1] fpr_lr,tpr_lr,_ = roc_curve(val_y,y_pred) val_ks = abs(fpr_lr - tpr_lr).max() print('val_ks : ',val_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 #val_ks : 0.4198642457760936 df_train = data[data.obs_mth != '2018-11-30'].reset_index().copy() df_test = data[data.obs_mth == '2018-11-30'].reset_index().copy() NUMERIC_COLS = ['person_info','finance_info','credit_info','act_info']通過集成模型進行離散后究竟是升維還是降維全由樹的個數(shù)來決定,num_boost_round 設置為50則變量轉換后新的變量有50個維度,分別為在那一顆樹上的第幾個葉子節(jié)點上。葉子數(shù)量決定了每一個變量有多少取值,當然一層決策樹即使設置100個葉子節(jié)點變量取值也只有{0,1}。
df_train.shape #(79831, 14) from sklearn.preprocessing import OneHotEncoder,LabelEncoderlgb_train = lgb.Dataset(df_train[NUMERIC_COLS], df_train['bad_ind'], free_raw_data=False) params = {'num_boost_round': 50,'boosting_type': 'gbdt','objective': 'binary','num_leaves': 2,'metric': 'auc','max_depth':1,'feature_fraction':1,'bagging_fraction':1, } model = lgb.train(params,lgb_train) leaf = model.predict(df_train[NUMERIC_COLS],pred_leaf=True) leaf.shape #(79831, 50) lgb_enc = OneHotEncoder() lgb_enc.fit(leaf) data_leaf = np.hstack((lgb_enc.transform(leaf).toarray(),df_train[NUMERIC_COLS])) #特征拼到一起 train, val, train_y, val_y = train_test_split(data_leaf,df_train['bad_ind'],test_size=0.2, random_state=random.choice(range(10000)))lgb_lm = LogisticRegression(penalty='l1',C = 0.3,solver='liblinear') lgb_lm.fit(train, train_y)y_pred_lgb_lm_train = lgb_lm.predict_proba(train)[:, 1] fpr_lgb_lm_train, tpr_lgb_lm_train, _ = roc_curve(train_y, y_pred_lgb_lm_train)y_pred_lgb_lm = lgb_lm.predict_proba(val)[:, 1] fpr_lgb_lm, tpr_lgb_lm, _ = roc_curve(val_y, y_pred_lgb_lm)plt.figure(1) plt.plot([0, 1], [0, 1], 'k--') plt.plot(fpr_lgb_lm_train, tpr_lgb_lm_train, label='LGB + LR train') plt.plot(fpr_lgb_lm, tpr_lgb_lm, label='LGB + LR test') plt.xlabel('False positive rate') plt.ylabel('True positive rate') plt.title('ROC curve') plt.legend(loc='best') plt.show() print('LGB+LR train ks:',abs(fpr_lgb_lm_train - tpr_lgb_lm_train).max(),'LGB+LR AUC:', metrics.auc(fpr_lgb_lm_train, tpr_lgb_lm_train)) print('LGB+LR test ks:',abs(fpr_lgb_lm - tpr_lgb_lm).max(),'LGB+LR AUC:', metrics.auc(fpr_lgb_lm, tpr_lgb_lm)) #LGB+LR train ks: 0.4768266998563515 LGB+LR AUC: 0.8089506271108136 #LGB+LR test ks: 0.482941737060417 LGB+LR AUC: 0.8150497181380825 leaf_test = model.predict(df_test[NUMERIC_COLS],pred_leaf=True) lgb_enc = OneHotEncoder() lgb_enc.fit(leaf_test) data_leaf_test = np.hstack((lgb_enc.transform(leaf_test).toarray(),df_test[NUMERIC_COLS]))train = data_leaf.copy() train_y = df_train['bad_ind'].copy() val = data_leaf_test.copy() val_y = df_test['bad_ind'].copy()lgb_lm = LogisticRegression(penalty='l2',C = 0.2,class_weight='balanced',solver='liblinear') lgb_lm.fit(train, train_y)y_pred_lgb_lm_train = lgb_lm.predict_proba(train)[:, 1] fpr_lgb_lm_train, tpr_lgb_lm_train, _ = roc_curve(train_y, y_pred_lgb_lm_train)y_pred_lgb_lm = lgb_lm.predict_proba(val)[:, 1] fpr_lgb_lm, tpr_lgb_lm, _ = roc_curve(val_y, y_pred_lgb_lm)plt.figure(1) plt.plot([0, 1], [0, 1], 'k--') plt.plot(fpr_lgb_lm_train, tpr_lgb_lm_train, label='LGB + LR train') plt.plot(fpr_lgb_lm, tpr_lgb_lm, label='LGB + LR test') plt.xlabel('False positive rate') plt.ylabel('True positive rate') plt.title('ROC curve') plt.legend(loc='best') plt.show() print('LGB+LR train ks:',abs(fpr_lgb_lm_train - tpr_lgb_lm_train).max(),'LGB+LR AUC:', metrics.auc(fpr_lgb_lm_train, tpr_lgb_lm_train)) print('LGB+LR test ks:',abs(fpr_lgb_lm - tpr_lgb_lm).max(),'LGB+LR AUC:', metrics.auc(fpr_lgb_lm, tpr_lgb_lm)) #LGB+LR train ks: 0.4812287054151174 LGB+LR AUC: 0.8116324829085549 #LGB+LR test ks: 0.4441149787927866 LGB+LR AUC: 0.777624032191942
變量維度應該是樹的個數(shù),50維變量可能稍顯多了一點,我們再做一次特征篩選
總結
以上是生活随笔為你收集整理的金融风控实战——模型融合的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 金融风控实战——不均衡学习
- 下一篇: Lesson 11.1-11.5 梯度下