評分卡系列(四):泛化誤差估計和模型調參


作者:JSong,時間:2017.10.21

本文大量引用了 jasonfreak 的系列文章,在此進行注明和感謝.

廣義的偏差(bias)描述的是預測值和真實值之間的差異,方差(variance)描述的是不同樣本下模型效果的離散程度。在《Understanding the Bias-Variance Tradeoff》當中有一副圖形象地向我們展示了偏差和方差的關系:

一、Bias-variance 分解

算法在不同訓練集上學到的結果很可能不同,即便這些訓練集來自於同一分布。對測試樣本 x ,令 y_D 為 x 在數據集中的標記,y 為 x 的真實標記, f(x;D) 為訓練集 D 上學的模型 f 在 x 上的預測輸出。

在回歸任務中,學習算法的期望輸出為:

\[\bar{f}(x)=\mathbb{E}_{D}[f(x;D)] \]

使用樣本數相同的不同訓練集產生的方差為:

\[var(x)=\mathbb{E}_{D}[(f(x;D)-\bar{f}(x))^2] \]

噪聲為

\[\varepsilon^2=\mathbb{E}_{D}[(y_{D}-y)^2] \]

我們將期望輸出與真實標記之間的差別稱為偏差(bias):

\[bias^2(x)=(\bar{f}(x)-y)^2 \]

為便於討論,假定噪聲期望為零,即

\[\mathbb{E}_D[y_D-y]=0 \]

通過簡單的多項式展開合並,對算法的期望泛化誤差進行分解:

\[\begin{align*} E(f;D)=&\mathbb{E}_{D}\left[(f(x;D)-y_D)^2\right]\\ =&\mathbb{E}_{D}\left[(f(x;D)-\bar{f}(x)+\bar{f}(x)-y_D)^2\right]\\ =&\mathbb{E}_{D}\left[(f(x;D)-\bar{f}(x))^2\right]+(\bar{f}(x)-y)^2+\mathbb{E}_{D}[(y_D-y)^2] \end{align*}\]

於是有

\[E(f;D)=bias^2(x)+var(x)+\varepsilon^2 \]

也就是說,泛化誤差可分解為偏差、方差與噪聲之和。

偏差和方差是有沖突的,下面是一個示意圖。在訓練不足(模型復雜度低)時,偏差主導了泛化誤差率;隨着訓練程度的加深,方差逐漸主導了泛化誤差率。

二、集成學習框架下的泛化誤差分解

在 bagging 和 boosting 框架中,通過計算基模型的期望和方差,我們可以得到模型整體的期望和方差。為了簡化模型,我們假設基模型的權重、方差及兩兩間的相關系數相等。由於bagging和boosting的基模型都是線性組成的,那么有:

\[E(f)=E(\sum_{i=1}^{m}\gamma_{i}\cdot_{}f_{i})=\sum_{i=1}^{m}\gamma_{i}\cdot_{}E(f_i)=\gamma\cdot\sum_{i}^{m}E(f_i) \]

\[\begin{align} Var(f)=&Var(\sum_{i}^{m}\gamma_{i}\cdot_{}f_{i})=Cov(\sum_{i}^{m}\gamma_{i}\cdot_{}f_{i},\sum_{i}^{m}\gamma_{i}\cdot_{}f_{i})\\ =&\sum_{i}^{m}\gamma_{i}^2\cdot_{}Var(f_i)+\sum_{i}^{m}\sum_{j\neq_{}i}^{m}2\rho\gamma_{i}\gamma_{j}\sqrt{Var(f_i)}\sqrt{Var(f_j)}\\ =&m^2\gamma^2\sigma^2\rho+m\gamma^2\sigma^2(1-\rho) \end{align}\]

2.1 bagging 的偏差和方差

對於bagging來說,每個基模型的權重等於 1/m 且期望近似相等(子訓練集都是從原訓練集中進行子抽樣),故我們可以進一步化簡得到:

\[E(f)=\gamma\cdot\sum_{i}^{m}E(f_i)=\frac{1}{m}m\mu=\mu \]

\[\begin{align} Var(F)=&m^2\gamma^2\sigma^2\rho+m\gamma^2\sigma^2(1-\rho)\\ =&m^2\frac{1}{m^2}\sigma^2\rho+m\frac{1}{m^2}\sigma^2(1-\rho)\\ =&\sigma^2\rho+\frac{\sigma^2(1-\rho)}{m} \end{align}\]

根據上式我們可以看到,整體模型的期望近似於基模型的期望,這也就意味着整體模型的偏差和基模型的偏差近似。同時,整體模型的方差小於等於基模型的方差(當相關性為1時取等號),隨着基模型數(m)的增多,整體模型的方差減少,從而防止過擬合的能力增強,模型的准確度得到提高。但是,模型的准確度一定會無限逼近於1嗎?並不一定,當基模型數增加到一定程度時,方差公式第二項的改變對整體方差的作用很小,防止過擬合的能力達到極限,這便是准確度的極限了。另外,在此我們還知道了為什么bagging中的基模型一定要為強模型,否則就會導致整體模型的偏差度低,即准確度低。

  Random Forest是典型的基於bagging框架的模型,其在bagging的基礎上,進一步降低了模型的方差。Random Fores中基模型是樹模型,在樹的內部節點分裂過程中,不再是將所有特征,而是隨機抽樣一部分特征納入分裂的候選項。這樣一來,基模型之間的相關性降低,從而在方差公式中,第一項顯著減少,第二項稍微增加,整體方差仍是減少。

2.2 boosting 的偏差和方差

對於boosting來說,基模型的訓練集抽樣是強相關的,那么模型的相關系數近似等於1,故我們也可以針對boosting化簡公式為:

\[E(f)=\gamma\sum_{i}^{m}E(f_i) \]

\[Var(F)=m^2\gamma^2\sigma^2\rho+m\gamma^2\sigma^2(1-\rho)=m^2\gamma^2\sigma^2 \]

通過觀察整體方差的表達式,我們容易發現,若基模型不是弱模型,其方差相對較大,這將導致整體模型的方差很大,即無法達到防止過擬合的效果。因此,boosting框架中的基模型必須為弱模型。

因為基模型為弱模型,導致了每個基模型的准確度都不是很高(因為其在訓練集上的准確度不高)。隨着基模型數的增多,整體模型的期望值增加,更接近真實值,因此,整體模型的准確度提高。但是准確度一定會無限逼近於1嗎?仍然並不一定,因為訓練過程中准確度的提高的主要功臣是整體模型在訓練集上的准確度提高,而隨着訓練的進行,整體模型的方差變大,導致防止過擬合的能力變弱,最終導致了准確度反而有所下降。

基於boosting框架的 Gradient Tree Boosting 模型中基模型也為樹模型,同 Random Forrest,我們也可以對特征進行隨機抽樣來使基模型間的相關性降低,從而達到減少方差的效果。

2.3 總結一下:bagging & boosting

調參的最終目的在於:模型在訓練集上的准確度和防止過擬合能力的大和諧!

  1. 使用模型的偏差和方差來描述其在訓練集上的准確度和防止過擬合的能力

  2. 對於bagging來說,整體模型的偏差和基模型近似,隨着訓練的進行,整體模型的方差降低

  3. 對於boosting來說,整體模型的初始偏差較高,方差較低,隨着訓練的進行,整體模型的偏差降低(雖然也不幸地伴隨着方差增高),當訓練過度時,因方差增高,整體模型的准確度反而降低.

  4. 整體模型的偏差和方差與基模型的偏差和方差息息相關

三、常見分類模型及其參數介紹

本文介紹的都是 scikit-learn 中的模型。

3.1 邏輯回歸模型

在scikit-learn中,與邏輯回歸有關的主要是這3個類。

  • logisticRegression
  • LogisticRegressionCV
  • logistic_regression_path

其中LogisticRegression和LogisticRegressionCV的主要區別是LogisticRegressionCV使用了交叉驗證來選擇正則化系數C。而LogisticRegression需要自己每次指定一個正則化系數。另外logistic_regression_path類則比較特殊,它擬合數據后,不能直接來做預測,只能為擬合數據選擇合適邏輯回歸的系數和正則化系數,主要是用在模型選擇的時候,一般情況用不到這個類,所以后面不再講述logistic_regression_path類。

LogisticRegression 的參數使用如下:

class sklearn.linear_model.LogisticRegression(penalty='l2', dual=False, tol=0.0001, C=1.0, fit_intercept=True, intercept_scaling=1, class_weight=None, random_state=None, solver='liblinear', max_iter=100, multi_class='ovr', verbose=0, warm_start=False, n_jobs=1)

具體含義為:

3.2 集成學習模型

在scikit-learn中,與隨機森林有關的有兩個。

  • RandomForestClassifier(): A random forest classifier.
  • RandomForestRegressor(): A random forest regressor.

這里主要講第一個,它的參數如下:

class sklearn.ensemble.RandomForestClassifier(n_estimators=10, criterion=’gini’, max_depth=None, min_samples_split=2, min_samples_leaf=1, min_weight_fraction_leaf=0.0, max_features=’auto’, max_leaf_nodes=None, min_impurity_decrease=0.0, min_impurity_split=None, bootstrap=True, oob_score=False, n_jobs=1, random_state=None, verbose=0, warm_start=False, class_weight=None)

與GBDT有關的也是兩個。

  • GradientBoostingClassifier([loss, …]): Gradient Boosting for classification.
  • GradientBoostingRegressor([loss, …]): Gradient Boosting for regression.

其中分類模型的參數如下:

class sklearn.ensemble.GradientBoostingClassifier(loss=’deviance’, learning_rate=0.1, n_estimators=100, subsample=1.0, criterion=’friedman_mse’, min_samples_split=2, min_samples_leaf=1, min_weight_fraction_leaf=0.0, max_depth=3, min_impurity_decrease=0.0, min_impurity_split=None, init=None, random_state=None, max_features=None, verbose=0, max_leaf_nodes=None, warm_start=False, presort=’auto’)

這兩個模型參數的詳細介紹如下:

在上一節我們對bagging和boosting兩種集成學習技術的泛化誤差估計有了初步的了解。Random Forest的子模型都擁有較低的偏差,整體模型的訓練過程旨在降低方差,故其需要較少的子模型(n_estimators默認值為10)且子模型不為弱模型(max_depth的默認值為None),同時,降低子模型間的相關度可以起到減少整體模型的方差的效果(max_features的默認值為auto)。

另一方面,Gradient Tree Boosting的子模型都擁有較低的方差,整體模型的訓練過程旨在降低偏差,故其需要較多的子模型(n_estimators默認值為100)且子模型為弱模型(max_depth的默認值為3),但是降低子模型間的相關度不能顯著減少整體模型的方差(max_features的默認值為None)。

四、分類模型的調參方法

調參是一個最優化問題。如果樣本量不是非常大,計算資源也足夠,那我們可以直接用網格搜索進行窮舉。例如在隨機森林算法中,可以用sklearn提供的GridSearchCV函數來調參。

param_test1 ={'n_estimators':range(10,71,10),'max_features':range(20,50,1)}  
gsearch1= GridSearchCV(estimator =RandomForestClassifier(min_samples_split=100,  
                                 min_samples_leaf=20,max_depth=8,random_state=10),   
                       param_grid =param_test1,scoring='roc_auc',cv=5)  
gsearch1.fit(X,y)  
gsearch1.grid_scores_, gsearch1.best_params_, gsearch1.best_score_

對於小數據集,我們還能這么任性,但是參數組合爆炸,在大數據集上,如果用網格搜索,那真的要下一個猴年馬月才能看到結果了。而且實際上網格搜索也不一定能得到全局最優解,而另一些研究者從解優化問題的角度嘗試解決調參問題。

坐標下降法是一類非梯度優化算法。算法在每次迭代中,在當前點處沿一個坐標方向進行一維搜索以求得一個函數的局部極小值。在整個過程中循環使用不同的坐標方向。對於不可拆分的函數而言,算法可能無法在較小的迭代步數中求得最優解。為了加速收斂,可以采用一個適當的坐標系,例如通過主成分分析獲得一個坐標間盡可能不相互關聯的新坐標系(如自適應坐標下降法)。

我們最容易想到一種特別朴實的類似於坐標下降法的方法,與坐標下降法不同的是,其不是循環使用各個參數進行調整,而是貪心地選取了對整體模型性能影響最大的參數。參數對整體模型性能的影響力是動態變化的,故每一輪坐標選取的過程中,這種方法在對每個坐標的下降方向進行一次直線搜索(line search)。首先,找到那些能夠提升整體模型性能的參數,其次確保提升是單調或近似單調的。這意味着,我們篩選出來的參數是對整體模型性能有正影響的,且這種影響不是偶然性的,要知道,訓練過程的隨機性也會導致整體模型性能的細微區別,而這種區別是不具有單調性的。最后,在這些篩選出來的參數中,選取影響最大的參數進行調整即可。

另外無法對整體模型性能進行量化,也就談不上去比較參數影響整體模型性能的程度。我們還沒有一個准確的方法來量化整體模型性能,只能通過交叉驗證來近似計算整體模型性能。然而交叉驗證也存在隨機性,假設我們以驗證集上的平均准確度作為整體模型的准確度,我們還得關心在各個驗證集上准確度的變異系數,如果變異系數過大,則平均值作為整體模型的准確度也是不合適的。

五、評分卡實踐

這次我們與時俱進,用 Kaggle上的 Lending Club 數據來建模。大家可以到 Kaggle/Lending Club/ 上下載,在主頁上還可以看到世界各地的人用各種姿勢在玩這份數據。另外也可以關注我的公眾號:JSong老師,后台回復 “數據集” 下載

同樣第一步先導入數據,其包含了 Lending Club 從2007-2015年共887379個用戶的借款數據。這份數據一共有75個字段,這些字段又分為五種類型:ID類、開卡前的數據、描述信用卡的數據、注銷后的數據、欠款狀態等。由於我們要在開卡前預測用戶時候會欠款,所以這里只能使用 var_submission 中的數據。

data=pd.read_csv('.\\lending-club-loan-data\\loan.csv')
#data=data.sample(frac=0.1)

# Identifiers, etc. (we won't use them as predictors)
var_identifiers = ['id', 'member_id', 'url']
# Features available at a moment of credit application submission
var_submission = ['loan_amnt', 'term', 'int_rate', 'installment', 'grade', 'sub_grade',
         'emp_title', 'emp_length', 'home_ownership', 'annual_inc',
         'verification_status', 'desc', 'purpose', 'title', 'zip_code',
         'addr_state', 'dti', 'delinq_2yrs', 'earliest_cr_line',
         'inq_last_6mths', 'mths_since_last_delinq', 'mths_since_last_record',
         'open_acc', 'pub_rec', 'total_acc', 'initial_list_status',
         'collections_12_mths_ex_med', 'mths_since_last_major_derog',
         'policy_code', 'application_type', 'annual_inc_joint', 'dti_joint',
         'verification_status_joint', 'acc_now_delinq', 'tot_coll_amt',
         'tot_cur_bal', 'open_acc_6m', 'open_il_6m', 'open_il_12m',
         'open_il_24m', 'mths_since_rcnt_il',  'total_bal_il', 'il_util',
         'open_rv_12m', 'open_rv_24m', 'max_bal_bc', 'all_util',
         'total_rev_hi_lim', 'inq_fi', 'total_cu_tl', 'inq_last_12m']

# Features describing an open credit
var_open = ['funded_amnt', 'funded_amnt_inv', 'issue_d', 'pymnt_plan',
         'revol_bal', 'revol_util', # revol_bal maybe available during the application
         'out_prncp', 'out_prncp_inv', 'total_pymnt', 'total_pymnt_inv',
         'total_rec_prncp', 'total_rec_int', 'total_rec_late_fee',
         'last_pymnt_d', 'last_pymnt_amnt', 'next_pymnt_d',
         'last_credit_pull_d']

# Features available after closing a credit
var_close = ['recoveries', 'collection_recovery_fee']

# Response variable
var_y = ['loan_status']

本系列最后一篇文章的目的在於講解調參的方法,所以這次不再詳細介紹數據清洗和特征變換的過程。這次我們主要采用集成學習相關算法來建模,所以特征工程中就直接用啞變量,有興趣的童鞋可以自己用woe編碼試試。

# 借款狀態處理
statuses = data.loan_status.unique()
bad = ['Charged Off', 'Default',
                   'Does not meet the credit policy. Status:Charged Off',
                   'Late (31-120 days)']
good = ['Fully Paid','Does not meet the credit policy. Status:Fully Paid']
# current 的樣本都不能用
current = ['Current', 'In Grace Period', 'Late (16-30 days)','Issued']
# Selecting relevant features and observations
data = data[var_submission+var_y]
data = data[data.loan_status.isin(bad+good)]
# Features per type of preprocessing required
data['loan_status']=data['loan_status'].map(lambda x: 1 if x in bad else 0)

# 去除缺失率大於60%的字段
data=data.dropna(thresh=len(data) * 0.6,axis=1)
nunique=data.apply(pd.Series.nunique)
data=data.loc[:,nunique!=1]

###### 特征變換
data['term'] = data['term'].str.split(' ').str[1].astype(float)
# extract numbers from emp_length and fill missing values with the median
data['emp_length'] = data['emp_length'].str.extract('(\d+)').astype(float)
data['emp_length'] = data['emp_length'].fillna(data.emp_length.median())

# 將日期轉換成月份數,以最早的日期數為基准
tmp=pd.to_datetime('01-'+data['earliest_cr_line'])
data['earliest_cr_line']=(tmp-tmp.min()).map(lambda x:round(x.days/30) if pd.notnull(x) else np.nan)
data['earliest_cr_line']=data['earliest_cr_line'].fillna(data['earliest_cr_line'].median())

# drop 無意義ID和難以分析的文本變量
var_drop = ['emp_title','title','zip_code']
data=data.drop(var_drop,axis=1)
X=data.drop('loan_status',axis=1)
y=data['loan_status']

categorical_var=list(set(X.columns[X.apply(pd.Series.nunique)<30])|set(X.select_dtypes(include=['O']).columns))

continuous_var=list(set(X.columns)-set(categorical_var))

# 特征工程
tmp=pd.get_dummies(X[categorical_var].applymap(lambda x:'%s'%x),drop_first=True)
# 補缺
imputer=preprocessing.Imputer(strategy='median')
X[continuous_var]=imputer.fit_transform(X[continuous_var])
X[continuous_var]=preprocessing.MinMaxScaler().fit_transform(X[continuous_var])
X=pd.concat([tmp,X[continuous_var]],axis=1)

處理后一共還有 268530 個樣本,212個變量。接下來我們開始建模和調參。首先看一下 bad 和 good 的占比:

0 ( good ) 1 ( bad )
209711 (78.0959%) 58819 (21.9041%)

這是一個典型的樣本不平衡問題,如果不做任何處理,建模得到的結果雖然auc和精確率都挺高,但召回率(壞瓜有多少被挑出)和F1分數都會相對偏低(f1只有0.11左右)。這里我們可以采用下采樣和Smote算法,為了簡單和充分利用到每個樣本,我將直接用第二種方法。

# Balanced
from imblearn.over_sampling import SMOTE
index_split = int(len(X)*0.6)
X_train, y_train = SMOTE().fit_sample(X[0:index_split], y[0:index_split])
X_test, y_test = X[index_split:], y[index_split:]

此時訓練集有26萬,測試集有10萬。接下來我們用邏輯回歸、隨機森林和GBDT三種算法來建模,而且都用默認的參數。

clfs={'LogisticRegression':LogisticRegressionCV(),
'RandomForest':RandomForestClassifier(),
'GradientBoosting':GradientBoostingClassifier()}
y_preds,y_probas={},{}
for clf in clfs:
    clfs[clf].fit(X_train, y_train)
    y_preds[clf] =clfs[clf].predict(X_test)
    y_probas[clf] = clfs[clf].predict_proba(X_test)[:,1]
models_report,conf_matrix=Classifier_Report(y_test,y_preds,y_probas)
print(models_report)

模型結果如下:

可以看到三個模型中隨機森林的效果最差。另外邏輯回歸的f1分數最大,但它的精確率有點低,密度曲線也不是很分散。

目前的樣本量對於個人計算機而言,已經很大了,夢想用暴力網格搜索的話,還是算了。。。現在這兩種算法中,GBDT的調參相對更復雜一些,所以我們以它為例。 GBDT算法的核心思路是每次迭代學習上一步迭代之后的模型殘差,並且通過梯度下降的方法來求解參數。其過程影響類參數有“子模型數”(n_estimators)和“學習率”(learning_rate),這兩個參數不能單獨來討論,需要一起優化。我們可以使用GridSearchCV找到關於這兩個參數的最優解。

param_test ={'n_estimators':range(80,200,10),'learning_rate':[i/100 for i in range(1,25,2)]}
gb=GradientBoostingClassifier(random_state=10)
gsearch1= GridSearchCV(estimator = gb,param_grid =param_test,scoring='f1',n_jobs=4,cv=3)  
gsearch1.fit(X_train, y_train)
gsearch1.grid_scores_, gsearch1.best_params_, gsearch1.best_score_

將調參的結果用圖像展示如下,顏色越深,代表預測准確率越高:

這里有一個很大的陷阱:“子模型數”和“學習率”帶來的性能提升是不均衡的,在前期會比較高,在后期會比較低,如果一開始我們將這兩個參數調成最優,這樣很容易陷入一個“局部最優解”。在目標函數都不確定的情況下(如是否凸?),談局部最優解就是耍流氓,本文中“局部最優解”指的是調整各參數都無明顯性能提升的一種狀態,所以打了引號。

這應該算是最優化理論中的 過猶不及經驗 ,在學校時候老師曾告訴我,每個維度優化一點點,這樣反而更容易達到最優。所以我在這里選擇 n_estimators = 140, learning_rate = 0.17,不大不小。

另外細心的話會發現我在 GridSearchCV 中用的是 f1 分數,這是因為在樣本不平衡問題中,p-r 曲線比 roc 曲線更好用。roc曲線利用的是正類和負類的概率分布函數,對樣本不平衡不敏感。

接下來我們依次優化剩下的參數,代碼如下:

param={
'min_samples_leaf':range(1,10,2),
'max_depth':range(3,8),
'subsample':[0.6+i*0.04 for i in range(11)],
'max_features':np.floor(np.sqrt(np.linspace(0.04,1,15))*X_train.shape[1]).astype(np.int)
}

gb=GradientBoostingClassifier(n_estimators=140,learning_rate=0.17,random_state=10)
trained_param={}
param_test={}
for k in param:
    param_test.update({k:param[k]})
    param_test.update(trained_param)
    gsearch1= GridSearchCV(estimator = gb,param_grid =param_test,scoring='f1',n_jobs=4,cv=3)
    gsearch1.fit(X_train, y_train)
    best_params=gsearch1.best_params_
    trained_param={k:[best_params[k]] for k in best_params}
    print('優化后的參數:', gsearch1.best_params_)

得到的參數為:

max_depth max_features min_samples_leaf subsample
4 180 5 0.76

事實上調參並沒有結束,我們還需要殺幾個回馬槍,但這里我就不繼續了(訓練太慢了。。)。將優化后的模型用於測試集額預測,結果如下:

可以看到模型精確率從0.703972 提高到了0.741109,但f1分數下降了(暫時沒想明白為什么) 。理論上肯定還有提升空間,這個樣本量對於我的 surface 而言壓力還是挺大的,每一個參數的調休都花了我一兩個個多小時去訓練模型,甚至在子模型數和學習率的調優過程中不得不對訓練集進行采樣。

整個調參過程其實能看得出並不嚴謹,但不管怎樣,我還是放出來供大家一起參考和探討(一些細節的討論可以看 jasonfreak 的博客)。一個好的結果取決於對業務、模型理論等的理解程度,取決於好的數據、好的特征工程以及好的調參技巧。相關的代碼都可以關注我的公眾號,后台回復:“評分卡” 下載。

參考文獻

[1]. Understanding the Bias-Variance Tradeoff

[2]. 使用sklearn進行集成學習——理論

[3] sklearn:model_evaluation

[4]. GridSearch使用方法

[5] 使用sklearn進行集成學習——理論

[6]. 使用sklearn進行集成學習——實踐


評分卡系列文章

1、評分卡系列(一):講講評分系統的構建

2、評分卡系列(二):特征工程

3、評分卡系列(三):分類學習器的評估

4、評分卡系列(四):分類學習器的評估


歡迎關注我的公眾號,評分卡系列相關代碼和數據都可在公眾號中下載。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM