我們已經學習了 XGBoost淵源及優點、模型原理及優化推導、模型參數解析: 100天搞定機器學習|Day60 遇事不決,XGBoost ,文章有點太枯燥,大家可能對其中的公式推導和參數還不甚理解。
今天我們以西瓜數據集為例,配合手算,拆開揉碎,深入理解公式與代碼之間的內在聯系 ,然后用可視化的方式更形象地看透 XGBoost 的原理 !
本文又硬又干,歡迎同學們來個素質三連:在看、收藏、轉發 ,沒有關注的同學也來個關注下哈↓↓↓↓↓↓
#本文用到的庫 import numpy as np import pandas as pd from sklearn.model_selection import train_test_split from sklearn import preprocessing from xgboost.sklearn import XGBClassifier from xgboost import plot_tree import matplotlib.pyplot as plt from xgboost import plot_importance from sklearn.model_selection import GridSearchCV from sklearn.metrics import classification_report, precision_recall_curve
西瓜數據集及預處理 def getDataSet(): dataSet = [ ['青綠' , '蜷縮' , '濁響' , '清晰' , '凹陷' , '硬滑' , 0.697, 0.460, 1], ['烏黑' , '蜷縮' , '沉悶' , '清晰' , '凹陷' , '硬滑' , 0.774, 0.376, 1], ['烏黑' , '蜷縮' , '濁響' , '清晰' , '凹陷' , '硬滑' , 0.634, 0.264, 1], ['青綠' , '蜷縮' , '沉悶' , '清晰' , '凹陷' , '硬滑' , 0.608, 0.318, 1], ['淺白' , '蜷縮' , '濁響' , '清晰' , '凹陷' , '硬滑' , 0.556, 0.215, 1], ['青綠' , '稍蜷' , '濁響' , '清晰' , '稍凹' , '軟粘' , 0.403, 0.237, 1], ['烏黑' , '稍蜷' , '濁響' , '稍糊' , '稍凹' , '軟粘' , 0.481, 0.149, 1], ['烏黑' , '稍蜷' , '濁響' , '清晰' , '稍凹' , '硬滑' , 0.437, 0.211, 1], ['烏黑' , '稍蜷' , '沉悶' , '稍糊' , '稍凹' , '硬滑' , 0.666, 0.091, 0], ['青綠' , '硬挺' , '清脆' , '清晰' , '平坦' , '軟粘' , 0.243, 0.267, 0], ['淺白' , '硬挺' , '清脆' , '模糊' , '平坦' , '硬滑' , 0.245, 0.057, 0], ['淺白' , '蜷縮' , '濁響' , '模糊' , '平坦' , '軟粘' , 0.343, 0.099, 0], ['青綠' , '稍蜷' , '濁響' , '稍糊' , '凹陷' , '硬滑' , 0.639, 0.161, 0], ['淺白' , '稍蜷' , '沉悶' , '稍糊' , '凹陷' , '硬滑' , 0.657, 0.198, 0], ['烏黑' , '稍蜷' , '濁響' , '清晰' , '稍凹' , '軟粘' , 0.360, 0.370, 0], ['淺白' , '蜷縮' , '濁響' , '模糊' , '平坦' , '硬滑' , 0.593, 0.042, 0], ['青綠' , '蜷縮' , '沉悶' , '稍糊' , '稍凹' , '硬滑' , 0.719, 0.103, 0] ] features = ['color' , 'root' , 'knocks' , 'texture' , 'navel' , 'touch' , 'density' , 'sugar' ,'good' ] dataSet = np.array(dataSet) df = pd.DataFrame(dataSet,columns=features) for feature in features[0:6]: le = preprocessing.LabelEncoder() le = le.fit(df[feature]) df[feature] = le.transform(df[feature]) df.iloc[:,6:8]=df.iloc[:,6:8].astype(float ) df['good' ]=df['good' ].astype(int) return df
本文中,我們使用sklearn風格的接口,并使用sklearn風格的參數。xgboost.XGBClassifier實現了scikit-learn 的分類模型API:
xgboost.XGBClassifier(max_depth=3, learning_rate=0.1, n_estimators=100, silent=True, objective='binary:logistic' , booster='gbtree' , n_jobs=1, nthread=None, gamma=0, min_child_weight=1, max_delta_step=0, subsample=1, colsample_bytree=1, colsample_bylevel=1, reg_alpha=0, reg_lambda=1, scale_pos_weight=1, base_score=0.5, random_state=0, seed=None, missing=None, **kwargs)
為方便手算,我們設n_estimators=2,即XGBoost僅2棵樹,正則項 ,系數 =1,gamma=0。
#訓練模型 df = getDataSet() X, y = df[df.columns[:-1]],df['good' ] X_train, X_test, y_train, y_test = train_test_split(X,y, random_state=0) sklearn_model_new = XGBClassifier(n_estimators=2,max_depth=5,learning_rate= 0.1, verbosity=1, objective='binary:logistic' ,random_state=1) sklearn_model_new.fit(X_train, y_train) model.fit(X_train, y_train)
XGBoost可視化 現在看一下XGBoost的內部結構,看看樹的形狀。XGBoost可視化可使用xgboost.plot_tree方法:
xgboost.plot_tree(booster, fmap='' , num_trees=0, rankdir='UT' , ax=None, **kwargs)
參數:
booster:一個Booster對象, 一個 XGBModel 對象 fmap:一個字符串,給出了feature map 文件的文件名 num_trees:一個整數,制定了要繪制的子數的編號。默認為 0 rankdir:一個字符串,它傳遞給graphviz的graph_attr ax:一個matplotlib Axes 對象。特征重要性將繪制在它上面。如果為None,則新建一個Axes kwargs:關鍵字參數,用于傳遞給graphviz 的graph_attr
XGBoost很多函數會用的一個參數fmap (也就是feature map),但是文檔里面基本沒解釋這個fmap是怎么產生的,Kaggle上有好心人提供了解決方案:
https://www./mmueller/xgb-feature-importance-python
def ceate_feature_map(features): outfile = open('xgb.fmap' , 'w' ) i = 0 for feat in features: outfile.write('{0}\t{1}\tq\n' .format(i, feat)) i = i + 1 outfile.close() ceate_feature_map(df.columns)
這個函數就是根據給定的特征名字(直接使用數據的列名稱), 按照特定格式生成一個xgb.fmap文件, 這個文件就是XGBoost文檔里面多次提到的fmap, 注意使用的時候, 直接提供文件名, 比如fmap='xgb.fmap'.
有了fmap, 在調用plot_tree函數的時候, 直接指定fmap文件即可:
plot_tree(fmap='xgb.fmap' )
調整清晰度需要使用plt.gcf()方法
plot_tree(sklearn_model_new,fmap='xgb.fmap' ,num_trees=0) fig = plt.gcf() fig.set_size_inches(150, 100) plt.show()
第一棵樹 plot_tree(sklearn_model_new,fmap='xgb.fmap' ,num_trees=1) fig = plt.gcf() fig.set_size_inches(150, 100) plt.show()
第二棵樹 重頭戲分割線————手算
先看看X_train 第一棵樹僅以含糖率為分割點,對其排序,分割點為兩點間的均值
0.186為何可以成為根節點呢?這個咱們待會兒再算。
那我們就算一下,不過在此之前還要再復習一下損失函數 logloss:
由于后面需要用到logloss的一階導數以及二階導數,這里先簡單推導一下。
其中
在一階導的基礎上再求一次有(其實就是sigmod函數求導)
base_score初始值 0.5 ,我們計算每個樣本的一階導數值和二階導數值 樣本的一階導數值: 樣本的二階導數值:
=2.5 =-2.5 =1.25 =1.75
計算最優的權重 :
所以: =-0.111111111 =0.090909091
tips:上述結果直接計算與圖中不符,這里要記得乘以學習率:0.1
第一棵樹即為:
回答開頭的問題,第一棵樹為何以含糖率的0.1856為根節點?必然是所有特征中此處分割的 gain 最大!看一下這個公式
計算結果為5.0505,其他特征的gain小于它,原理類似這里就不挨個算了。
另一個問題,為何分裂到此怎么就停了呢? XGBClassifier參數min_child_weight默認值 1 ,是葉子節點包含樣本的所有二階偏導數之和,代表子節點的權重閾值。它刻畫的是:對于一個葉子節點,當對它采取劃分之后,它的所有子節點的權重之和的閾值。如果它的所有子節點的權重之和小于該閾值,則該葉子節點不值得繼續分裂。
本例中如再以0.344處或其他特征某處分裂則必有 >1,所以就不分裂了。min_child_weight的值較大時,可以避免模型學習到局部的特殊樣本。本例如將其值改為0.2,就會發現還會分裂(如下圖):
感興趣的同學可以試試,能算出-0.04和0.12嗎? 還以min_child_weight默認值為 1,然后,生成第二棵樹 ,此處僅需按第一棵樹的預測結果更新base_score的值,注意:預測結果要經過sigmod映射,即 當預測為0時,base_score更新為 當預測為0時,base_score更新為
進一步計算 , 和 ,大家可以試一下,結果如圖。
因為我們只有兩棵樹,特征選擇僅使用了含糖率,分裂了兩次,so,特征重要性為2
plot_importance(sklearn_model_new)
XGBoost網格搜索調參 參數調整是機器學習中的一門暗藝術,模型的最優參數可以依賴于很多場景。所以要創建一個全面的指導是不可能的。XGBoost使用sklearn風格的接口,并使用網格搜索類GridSeachCV來調參,非常方便。gsCv.best_params_獲取最優參數,添加新的參數進來,然后再次GridSearchCV。
也可以一把梭哈,把重要參數一次懟進去。
gsCv = GridSearchCV(sklearn_model_new, {'max_depth' : [4,5,6], 'n_estimators' : [5,10,20], 'learning_rate ' : [0.05,0.1,0.3,0.5,0.7], 'min_child_weight' :[0.1,0.2,0.5,1] }) gsCv.fit(X_train,y_train)print (gsCv.best_params_)
{'learning_rate ': 0.05, 'max_depth': 4, 'min_child_weight': 0.1, 'n_estimators': 5} 以此參數重新訓練即可。
老鐵,三連支持一下,好嗎? ↓↓↓