上一節對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使用的決策樹廣度分裂方式。後面有時間會對這一演算法進行了解,之後可能在進行演算法學習的同時對機器學習的一些基礎知識進行整理和回顧。