【Python機器學習實戰】决策樹與集成學習(七)——集成學習(5)XGBoost實例及調參

Uniqe 2021-09-18 04:03:34 阅读数:721

python 决策 集成 集成 xgboost

上一節對XGBoost算法的原理和過程進行了描述,XGBoost在算法優化方面主要在原損失函數中加入了正則項,同時將損失函數的二階泰勒展開近似展開代替殘差(事實上在GBDT中葉子結點的最優值求解也是使用的二階泰勒展開(詳細上面Tips有講解),但XGBoost在求解决策樹和最優值都用到了),同時在求解過程中將兩步優化(求解最優决策樹和葉子節點最優輸出值)合並成為一步。本節主要對XGBoot進行實現並調參。


XGBoost框架及參數

XGBoost原生框架與sklearn風格框架

XGBoost有兩個框架,一個是原生的XGBoost框架,另一個是sklearn所帶的XGBoost框架。二者實現基本一致,但在API的使用方法和參數名稱不同,在數據集的初始化方面也有不同。

XGBoost原生庫的使用過程如下:

 

其中主要是在DMatrix讀取數據和train訓練數據的類,其中DMatrix在原生XGBoost庫中的需要先把數據集按輸入特征部分、輸出特征部分分開,然後放到DMatrix中,即:

X_train, X_test, y_train, y_test = train_test_split(X, y, random_state=1)
dtrain = xgb.DMatrix(X_train, y_train)
dtest = xgb.DMatrix(X_test, y_test)

sklearn中參數都是寫在train中(不過也有使用原生XGBoost參數風格的sklearn用法),而原生庫中必須將參數寫入參數param的字典中,再輸入到train中,之所以這麼做是因為XGBoost所涉及的參數實在過多,都放在一起太長也容易出錯,比如(參數含義後面再說):

param = {'max_depth':5, 'eta':0.5, 'verbosity':1, 'objective':'binary:logistic'}

在模型訓練上,一種是使用原生XGBoost接口:

import xgboost as xgb
model = xgb.train(param, dtrain,num_boost_round=10,evals=(),obj=None,feval=None,maximize=False,early_stopping_rounds=None)

第二種是使用sklearn風格的接口,sklearn風格的xgb有兩種,一種是分類用的XGBClassifier,還有一個回歸用的XGBRegressor:

model = xgb.XGBClassifier('max_depth':5, 'eta':0.5, 'verbosity':1, 'objective':'binary:logistic')
model.fit(X_train, y_train, early_stopping_rounds=10, eval_metric="error",)

同時也可以sklearn風格下的原生參數param,只需傳入**param即可:

model = xgb.XGBClassifier(**param)
model.fit(X_train, y_train, early_stopping_rounds=10, eval_metric="error")

上面就是XGBoost的大致使用過程,下面主要對模型中的參數進行說明。

XGBoost框架下的參數

由於XGBoost原生庫與sklearn庫的參數在名字上有一定的差异,既然有sklearn風格的接口,為了與其他算法保持一致,這裏主要對sklearn風格的參數進行說明,並盡量與原生庫與GBDT中進行對應。

首先參數主要包括三個方面:XGBoost框架參數、弱學習器參數以及其他學習參數

XGBoost框架參數:

這方面參數主要包括3個:booster、n_estimators、objective。

  • 模型選擇參數booster:該參數决定了XGBoost學習時使用的弱學習器類型,有默認的gbtree,也就是CART决策樹,還有線性學習器gblinear或者DART,一般使用gbtree就可以,不需要調整。該參數在sklearn中命名一樣;

 

  • n_estimators:這是一個非常重要的參數,關系到模型的複雜度,錶示了弱學習器的個數,默認為100,與GBDT中的類似,當參數過小時,容易欠擬合,過大時會過擬合,在原生庫中對應的num_boost_bounds參數,默認為10;

 

  • objective:錶示所解决的問題是分類問題還是回歸問題,或者其它問題,以及對應的損失函數,具體取值有很多情况,這裏主要說明在分類或者回歸所使用的參數:silent:靜默參數,指訓練中每次是否打印訓練結果,在sklearn中默認為True,原生庫中默認為False
    • 在回歸問題中,這個參數一般使用reg:squarederror,即MSE均方誤差;
    • 在二分類問題中,這個參數一般使用binary:logistic;
    • 在多分類中,這個參數一般使用multi:softmax

弱學習器參數:

由於默認學習器為gbtree,其效果較好,這裏只介紹gbtree相關參數,其包含參數較多,大多與GBDT中的一樣,下面一一說明:

  • 學習率learning_rate:通過較少每一步的權重,提高模型的泛化能力,該參數與GBDT中的學習率相同,默認為0.1,在原生庫中該參數對應著eta,默認為0.3(GBDT中默認為1),一般取值0.01~0.2;

 

  • 最小葉子結點權重和min_child_weight:如果某樹節點的權重小於該閾值,則不再進行分裂,即這個樹節點就是葉子結點,樹節點的權重和也就是該節點所有樣本二階導數和:

該參數與GBDT中的min_samples_split類似,但又不完全一樣,XGBoost中是最小樣本權重和,GBDT中限定的是樣本的數量,在原生庫中該參數一致;

  • 樹的最大深度max_depth:限定樹的深度,與DGBT中一樣(默認為3),這裏默認為6,原生庫中該參數一致;

 

  • 損失所减小的閾值gamma:XGBoost樹分裂所帶來的的損失减小的閾值,當小於該值時不進行分裂,默認為0,在原生庫中一致,即XGBoost原理中γ,該值需要進行網格搜索調參:

  • 子采樣參數subsample:和GBDT中的參數一樣,控制樣本數量,默認為1,sklearn中一致;

 

  • 特征采樣參數colsample_bytree/colsample_bylevel:前者和GBDT中的max_features相似,用來控制采樣特征數量,這裏一般只能輸入浮點類型,錶示采樣特征比例,後者是每一層數再進行特征采樣,在原生庫中參數一致,默認都是1,即不做采樣;

 

  • 正則化參數reg_alpha/reg_lambda:原理篇中的正則化項的參數,alpha默認為0,lambda默認1,在原生庫中為alpha、lambda;

 

  • scale_pos_weight:用於類別不平衡的時候,負例和正例的比例,類似於GBDT中的class_weight參數,默認為1,原生庫中一致;

上面除了scale_pos_weight,其他基本都是需要進行調參的參數,一般先調learning_rate,n_eatimators,max_depth,min_child_weight和gamma,如果還是過擬合,繼續調節後面的參數

其他參數

其他參數主要用於控制XGBoost性能以及結果的相關參數,主要有以下這些:

  • n_jobs:控制算法的線程數,默認為最大線程;

 

  • early_stop_rounds:這是一種自動查找n_estimators的方法。通常是設置一個較大的n_estimators,然後通過該參數來找到最佳停止迭代的時間,由於隨機幾率有時候會導致單次驗證分數沒有提高,您需要指定一個數字,設置驗證分數連續惡化多少輪時停止。設置early_stopping_rounds=5是一個合理的選擇。此時,訓練過程中驗證分數連續5輪惡化就會停止;

 

  • eval_set:在指定early_stop_rounds時,需要指定驗證集來計算驗證分數,如eval_set=[(X_valid, y_valid)];

 

  • eval_metric:計算目標函數值的方式,默認取值為objective中的參數的取值,根據目標函數的形式,回歸問題默認為rmse,分類問題為error,還有以下幾種:

  • 還有importance_type:可以查詢各個特征的重要性程度,可以選擇“gain”、“weight”,“cover”,“total gain”或者“total cover”,然後通過booster中的get_score方法獲得對應的特征權重。“weight”通過特征被選中作為分裂特征的計數來計算重要性,“gain”和“total gain”則通過分別計算特征被選中最為分裂特征所帶來的增益和總增益來計算重要性,“cover”和“total cover”通過計算特征被選中作分裂時的平均樣本覆蓋度和總體樣本覆蓋度來計算重要性。

以上就是XGBoost的基本參數,通常先通過網格搜索找出比較合適的n_estimators和learning_rate的組合,然後調整max_depth和gamma,查看模型的處於什麼樣的狀態(過擬合還是欠擬合)然後再决定是否進行剪枝調整其他參數,通常來說是需要進行剪枝的,為了增强模型的泛化能力,因為XGB屬於天然過擬合模型。

 XGBoost的實例及調參

下面我們就來使用XGBoost進行分類,數據集采用Kaggle入門比賽中的Titanic數據集來根據乘客特征,預測是否生存,數據集的下載可以在官網網站中:https://www.kaggle.com/c/titanic,數據集包含train、test和gender submission,首先來導入數據並對數據進行有個初步的認知:

import pandas as pd
import numpy as np
import seaborn as sns
import missingno as msno_plot
import matplotlib.pyplot as plt
train_data = pd.read_csv('./train.csv')
test_data = pd.read_csv('./test.csv')
train_data.describe()
test_data.describe()

        

可以看到訓練數據有891條,測試集有418條數據。由於我們是要利用乘客特征預測是否生還“Survive”,測試集上沒有直接給出結果,因此我們就拿訓練集上的數據進行訓練和測試。(由於目前Kaggle需要爬梯子提交,暫時先不提交驗證了)。

然後是對數據進行一個初步的分析,首先查看是否存在缺失值:

plt.figure(figsize=(10, 8))
msno_plot.bar(train_data)

可以看到Age屬性和Cabin屬性還是存在很多缺失值的,同時由於生存與否與姓名、PassengerId、Ticket無關(主觀認知),這裏先對這幾個屬性特征進行删除,由於Cabin缺失值較多,且為非數值型數據,這裏也暫且删除,然後將Age屬性缺失值填補Age的均值(為簡單起見,還有其他很多方式),Embark屬性填補出現最多的值:

# 删除屬性
train_data.drop(['Name', 'PassengerId', 'Ticket', 'Cabin'], axis=1, inplace=True)
test_data.drop(['Name', 'PassengerId', 'Ticket', 'Cabin'], axis=1, inplace=True)
# 填補缺失值
train_data['Age'].fillna(30, inplace=True)
train_data['Embarked'].fillna('S', inplace=True)
test_data['Age'].fillna(30, inplace=True)
test_data['Embarked'].fillna('S', inplace=True)
for i in range(8):
plt.subplot(241+i)
sns.countplot(x=train_data.iloc[:, i])

填補後的數據分布如上圖中,然後就是對一些屬性進行數值化處理,主要有Sex和Embark:

train_data['Sex'].replace('male', 0, inplace=True)
train_data['Sex'].replace('female', 1, inplace=True)
train_data['Embarked'] = [0 if example == 'S' else 1 if example=='Q' else 2 for example in train_data['Embarked'].values.tolist()]

這裏數據就初步處理完成了,處理後的數據包含7個特征和1個類別“Survive”,然後就要開始利用XGBoost對數據進行分類了,首先導入所需要的包:

from sklearn.model_selection import train_test_split
import xgboost as xgb
from sklearn.model_selection import GridSearchCV
from sklearn import metrics

將數據進一步分為訓練集和測試集兩部分:

trainX, testX, trainy, testy = train_test_split(train_data.drop(['Survived'], axis=1), train_data['Survived'], test_size=0.2, random_state=10)

然後我們初始隨便給定一組參數建立一個模型:

model = xgb.XGBClassifier(learning_rate=0.1, n_estimators=100, max_depth=6, min_child_weight=1, gamma=0, subsample=1,
objective='binary:logistic')
model.fit(trainX, trainy)
print(model.score(trainX, trainy))

#0.9269662921348315

可以看到在訓練集上的分數已經很高了,然後利用GridSearchCV進行調參,首先調整n_estimators和max_depth兩個參數,調參方法跟GBDT中一樣:

gsearch = GridSearchCV(estimator=model, param_grid={'n_estimators': range(10, 301, 10), 'max_depth': range(2, 7, 1)})
gsearch.fit(trainX, trainy)
means = gsearch.cv_results_['mean_test_score']
params = gsearch.cv_results_['params']
for i in range(len(means)):
print(params[i], means[i])
print(gsearch.best_score_)
print(gsearch.best_params_)

# 0.8244262779474048
# {'max_depth': 5, 'n_estimators': 30}

可以看到分數降低了很多,說明原先模型確實存在過擬合,接下來繼續調整gamma和subsample參數:

model2 = xgb.XGBClassifier(learning_rate=0.1, n_estimators=30, max_depth=5, min_child_weight=1, gamma=0, subsample=1,
objective='binary:logistic', random_state=1)
gsearch = GridSearchCV(estimator=model2, param_grid={'gamma': np.linspace(0, 1, 11), 'subsample': np.linspace(0.1, 1, 10)})
gsearch.fit(trainX, trainy)
means = gsearch.cv_results_['mean_test_score']
params = gsearch.cv_results_['params']
for i in range(len(means)):
print(params[i], means[i])
print(gsearch.best_score_)
print(gsearch.best_params_)

# 0.825824879346006
# {'gamma': 0.7, 'subsample': 0.7}

分數略微提昇了一些,繼續調整min_child_weight參數:

model3 = xgb.XGBClassifier(learning_rate=0.1, n_estimators=30, max_depth=5, min_child_weight=1, gamma=0.7, subsample=0.7,
objective='binary:logistic', random_state=1, silent=False)
gsearch = GridSearchCV(estimator=model3, param_grid={'min_child_weight': range(1, 11)})
gsearch.fit(trainX, trainy)
# 0.825824879346006
# {'min_child_weight': 1}

分數已經不再提昇了,應該是已經達到極限了,再進行正則化也沒有意義了,這裏嘗試了一下:

model4 = xgb.XGBClassifier(learning_rate=0.1, n_estimators=30, max_depth=5, min_child_weight=1, gamma=0.7, subsample=0.7,
objective='binary:logistic', random_state=1)
gsearch = GridSearchCV(estimator=model4, param_grid={'reg_lambda': [1e-5, 1e-4, 1e-3, 1e-2, 1e-1, 1, 10, 100, 1000]})
gsearch.fit(trainX, trainy)
# 0.825824879346006
# {'reg_lambda': 1}

已經不能够再提昇了,然後再次嘗試改變learning_rate和n_estimators的值,成倍的放大和縮小相應的值,learning_rate縮小10倍,n_estimators放大10倍:

odel3 = xgb.XGBClassifier(learning_rate=0.01, n_estimators=300, max_depth=5, min_child_weight=1, gamma=0.7, subsample=0.7,
objective='binary:logistic', random_state=1, silent=False)
model3.fit(trainX, trainy)
model3.score(trainX, trainy)

# 0.848314606741573

再進一步提昇發現分數已經不再增長,然後確定最終模型即為上述的model3,然後最後進行訓練:

model3.fit(trainX, trainy, early_stopping_rounds=10, eval_metric='error', eval_set=[(testX, testy)])
print(model3.score(trainX, trainy))
print(model3.score(testX, testy))
# 0.84831460674157
# 0.875

利用前面GBDT算法,找到的一組參數,所帶來的錶現跟XGBoost差不多,略微低於XGBoost一點:

model3_2 = GradientBoostingClassifier(n_estimators=80, learning_rate=0.1, subsample=0.7, max_depth=5, max_features=7, min_samples_leaf=31, min_samples_split=17)
model3_2.fit(trainX, trainy)
print(model3_2.score(trainX, trainy))
print(model3_2.score(testX, testy))

# 0.875
# 0.8547486033519553

但總體來說准確率並不是很高,這個可能數據處理不當等問題吧,後面會找一下原因,該數據集只作為算法的訓練和熟悉用,在某乎上搜到一些原因:“一般來說姓名對於預測能否生還沒有太大的價值,但在這個賽題的設置下,適當的考慮姓名可以發揮意想不到的作用。如訓練集中頭等艙一個姓Abel(隨便起的,但是ms確實有這樣的實例)的男性生還了,那麼測試集中頭等艙同樣姓Abel的女子和小孩則很可能也能够生還,因為一家子基本上男的活下來了老婆孩子也問題不大”。所以這裏就不深究了,後面會找其他一些數據集再進行測試和訓練。


到這裏集成學習內容已基本完了,後面還是要利用一些有價值的數據集進行實戰,此外還有一個lightGBM算法,但lightGBM在算法本身的優化的內容不多。更多的還是運行速度提昇和內存占用降低。唯一值得討論的是它的决策樹深度分裂方式,相比之下XGBoost使用的决策樹廣度分裂方式。後面有時間會對這一算法進行了解,之後可能在進行算法學習的同時對機器學習的一些基礎知識進行整理和回顧。

 

版权声明:本文为[Uniqe]所创,转载请带上原文链接,感谢。 https://gsmany.com/2021/09/20210918040333624i.html