XGBoost - XGBoost应用中的其他问题(六)

1,262 阅读10分钟

根据菜菜的课程进行整理,方便记忆理解

代码位置如下:

XGBoost应用中的其他问题

过拟合:剪枝参数与回归模型调参

class xgboost.XGBRegressor (max_depth=3, learning_rate=0.1, n_estimators=100, silent=True, objective='reg:linear', 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, importance_type='gain', kwargs)

作为天生过拟合的模型,XGBoost应用的核心之一就是减轻过拟合带来的影响。作为树模型,减轻过拟合的方式主要是靠对决策树剪枝来降低模型的复杂度,以求降低方差。在之前的讲解中,我们已经学习了好几个可以用来防止过拟合的参数,包括上一节提到的复杂度控制γ\gamma,正则化的两个参数λ\lambdaα\alpha,控制迭代速度的参数以及管理每次迭代前进行的随机有放回抽样的参数subsample。所有的这些参数都可以用来减轻过拟合。但除此之外,我们还有几个影响重大的,专用于剪枝的参数:

参数含义xgb.train()xgb.XGBRegressor()
树的最大深度max_depth,默认6max_depth,默认6
每次生成树时随机抽样特征的比例colsample_bytree,默认1colsample_bytree,默认1
每次生成树的一层时
随机抽样特征的比例
colsample_bylevel,默认1colsample_bylevel,默认1
每次生成一个叶子节点时
随机抽样特征的比例
colsample_bynode,默认1N.A.
一个叶子节点上所需要的最小hih_i
即叶子节点上的二阶导数之和
类似于样本权重
min_child_weight,默认1min_child_weight,默认1

这些参数中,树的最大深度是决策树中的剪枝法宝,算是最常用的剪枝参数,不过在XGBoost中,最大深度的功能与参数γ\gamma相似,因此如果先调节了γ\gamma,则最大深度可能无法展示出巨大的效果。当然,如果先调整了最大深度,则γ\gamma也有可能无法显示明显的效果。通常来说,这两个参数中我们只使用一个,不过两个都试试也没有坏处。

三个随机抽样特征的参数中,前两个比较常用。在建立树时对特征进行抽样其实是决策树和随机森林中比较常见的一种方法,但是在XGBoost之前,这种方法并没有被使用到boosting算法当中过。Boosting算法一直以抽取样本(横向抽样)来调整模型过拟合的程度,而实践证明其实纵向抽样(抽取特征)更能够防止过拟合。

参数min_child_weight不太常用,它是一篇叶子上的二阶导数hih_i之和,当样本所对应的二阶导数很小时,比如说为0.01,min_child_weight若设定为1,则说明一片叶子上至少需要100个样本。本质上来说,这个参数其实是在控制叶子上所需的最小样本量,因此对于样本量很大的数据会比较有效。如果样本量很小(比如我们现在使用的波士顿房价数据集,则这个参数效用不大)。就剪枝的效果来说,这个参数的功能也被γ\gamma替代了一部分,通常来说我们会试试看这个参数,但这个参数不是我的优先选择。

通常当我们获得了一个数据集后,我们先使用网格搜索找出比较合适的n_estimators和eta组合,然后使用gamma或者max_depth观察模型处于什么样的状态(过拟合还是欠拟合,处于方差-偏差图像的左边还是右边?),最后再决定是否要进行剪枝。通常来说,对于XGB模型,大多数时候都是需要剪枝的。接下来我们就来看看使用xgb.cv这个类来进行剪枝调参,以调整出一组泛化能力很强的参数。

让我们先从最原始的,设定默认参数开始,先观察一下默认参数下,我们的交叉验证曲线长什么样:

  • 参数准备
dfull = xgb.DMatrix(X,y)

param1 = {'silent':True
          ,'obj':'reg:linear'
          ,"subsample":1
          ,"max_depth":6
          ,"eta":0.3
          ,"gamma":0
          ,"lambda":1
          ,"alpha":0
          ,"colsample_bytree":1
          ,"colsample_bylevel":1
          ,"colsample_bynode":1
          ,"nfold":5}
num_round = 200
  • 绘制图像
time0 = time()
cvresult1 = xgb.cv(param1, dfull, num_round)
print(datetime.datetime.fromtimestamp(time()-time0).strftime("%M:%S:%f"))

fig,ax = plt.subplots(1,figsize=(15,8))
ax.set_ylim(top=5)
ax.grid()
ax.plot(range(1,201),cvresult1.iloc[:,0],c="red",label="train,original")
ax.plot(range(1,201),cvresult1.iloc[:,2],c="orange",label="test,original")
ax.legend(fontsize="xx-large")
plt.show()

image.png

从曲线上可以看出,模型现在处于过拟合的状态。我们决定要进行剪枝。我们的目标是:训练集和测试集的结果尽量接近,如果测试集上的结果不能上升,那训练集上的结果降下来也是不错的选择(让模型不那么具体到训练数据,增加泛化能力)。在这里,我们要使用三组曲线。一组用于展示原始数据上的结果,一组用于展示上一个参数调节完毕后的结果,最后一组用于展示现在我们在调节的参数的结果。具体怎样使用,我们来看:

param1 = {'silent':True
          ,'obj':'reg:linear'
          ,"subsample":1
          ,"max_depth":6
          ,"eta":0.3
          ,"gamma":0
          ,"lambda":1
          ,"alpha":0
          ,"colsample_bytree":1
          ,"colsample_bylevel":1
          ,"colsample_bynode":1
          ,"nfold":5}
num_round = 200

time0 = time()
cvresult1 = xgb.cv(param1, dfull, num_round)
print(datetime.datetime.fromtimestamp(time()-time0).strftime("%M:%S:%f"))

fig,ax = plt.subplots(1,figsize=(15,8))
ax.set_ylim(top=5)
ax.grid()
ax.plot(range(1,201),cvresult1.iloc[:,0],c="red",label="train,original")
ax.plot(range(1,201),cvresult1.iloc[:,2],c="orange",label="test,original")

param2 = {'silent':True
          ,'obj':'reg:linear'
          ,"max_depth":2
          ,"eta":0.05
          ,"gamma":0
          ,"lambda":1
          ,"alpha":0
          ,"colsample_bytree":1
          ,"colsample_bylevel":0.4
          ,"colsample_bynode":1
          ,"nfold":5}

param3 = {'silent':True
          ,'obj':'reg:linear'
          ,"subsample":1
          ,"eta":0.05
          ,"gamma":20
          ,"lambda":3.5
          ,"alpha":0.2
          ,"max_depth":4
          ,"colsample_bytree":0.4
          ,"colsample_bylevel":0.6
          ,"colsample_bynode":1
          ,"nfold":5}

time0 = time()
cvresult2 = xgb.cv(param2, dfull, num_round)
print(datetime.datetime.fromtimestamp(time()-time0).strftime("%M:%S:%f"))

time0 = time()
cvresult3 = xgb.cv(param3, dfull, num_round)
print(datetime.datetime.fromtimestamp(time()-time0).strftime("%M:%S:%f"))

ax.plot(range(1,201),cvresult2.iloc[:,0],c="green",label="train,last")
ax.plot(range(1,201),cvresult2.iloc[:,2],c="blue",label="test,last")
ax.plot(range(1,201),cvresult3.iloc[:,0],c="gray",label="train,this")
ax.plot(range(1,201),cvresult3.iloc[:,2],c="pink",label="test,this")
ax.legend(fontsize="xx-large")
plt.show()

image.png

在这个调整过程中,大家可能会有几个问题:

  • 一个个参数调整太麻烦,可不可以使用网格搜索呢?

    • 当然可以!只要电脑有足够的计算资源,并且你信任网格搜索,那任何时候我们都可以使用网格搜索。只是使用的时候要注意,首先XGB的参数非常多,参数可取的范围也很广,究竟是使用np.linspace或者np.arange作为参数的备选值也会影响结果,而且网格搜索的运行速度往往不容乐观,因此建议至少先使用xgboost.cv来确认参数的范围,否则很可能花很长的时间做了无用功。
    • 并且,在使用网格搜索的时候,最好不要一次性将所有的参数都放入进行搜索,最多一次两三个。有一些互相影响的参数需要放在一起使用,比如学习率eta和树的数量n_estimators。
    • 另外,如果网格搜索的结果与你的理解相违背,与你手动调参的结果相违背,选择模型效果较好的一个。如果两者效果差不多,那选择相信手动调参的结果。网格毕竟是枚举出结果,很多时候得出的结果可能会是具体到数据的巧合,我们无法去一一解释网格搜索得出的结论为何是这样。如果你感觉都无法解释,那就不要去在意,直接选择结果较好的一个。
  • 调参的时候参数的顺序会影响调参结果吗?

    • 会影响,因此在现实中,我们会优先调整那些对模型影响巨大的参数。在这里,我建议的剪枝上的调参顺序是:n_estimators与eta共同调节,gamma或者max_depth,采样和抽样参数(纵向抽样影响更大),最后才是正则化的两个参数。当然,可以根据自己的需求来进行调整。
  • 调参之后测试集上的效果还没有原始设定上的效果好怎么办?

    • 如果调参之后,交叉验证曲线确实显示测试集和训练集上的模型评估效果是更加接近的,推荐使用调参之后的效果。我们希望增强模型的泛化能力,然而泛化能力的增强并不代表着在新数据集上模型的结果一定优秀,因为未知数据集并非一定符合全数据的分布,在一组未知数据上表现十分优秀,也不一定就能够在其他的未知数据集上表现优秀。因此不必过于纠结在现有的测试集上是否表现优秀。当然了,在现有数据上如果能够实现训练集和测试集都非常优秀,那模型的泛化能力自然也会是很强的。

XGBoost模型的保存和调用

在使用Python进行编程时,我们可能会需要编写较为复杂的程序或者建立复杂的模型。比如XGBoost模型,这个模型的参数复杂繁多,并且调参过程不是太容易,一旦训练完毕,我们往往希望将训练完毕后的模型保存下来,以便日后用于新的数据集。在Python中,保存模型的方法有许多种。我们以XGBoost为例,来讲解两种主要的模型保存和调用方法。

使用Pickle保存和调用模型

pickle是python编程中比较标准的一个保存和调用模型的库,我们可以使用pickle和open函数的连用,来将我们的模型保存到本地。以刚才我们已经调整好的参数和训练好的模型为例,我们可以这样来使用pickle:

import pickle

dtrain = xgb.DMatrix(Xtrain,Ytrain)

#设定参数,对模型进行训练
param = {'silent':True
          ,'obj':'reg:linear'
          ,"subsample":1
          ,"eta":0.05
          ,"gamma":20
          ,"lambda":3.5
          ,"alpha":0.2
          ,"max_depth":4
          ,"colsample_bytree":0.4
          ,"colsample_bylevel":0.6
          ,"colsample_bynode":1}
num_round = 180

bst = xgb.train(param, dtrain, num_round)

#保存模型
pickle.dump(bst, open("xgboostonboston.dat","wb"))

#注意,open中我们往往使用w或者r作为读取的模式,但其实w与r只能用于文本文件 - txt
#当我们希望导入的不是文本文件,而是模型本身的时候,我们使用"wb"和"rb"作为读取的模式
#其中wb表示以二进制写入,rb表示以二进制读入,使用open进行保存的这个文件中是一个可以进行读取或者调用的模型
  • 关闭jupyter,使用pickle在次载入模型
#重新打开jupyter lab

from sklearn.datasets import load_boston
from sklearn.model_selection import train_test_split as TTS
from sklearn.metrics import mean_squared_error as MSE
import pickle
import xgboost as xgb

data = load_boston()

X = data.data
y = data.target

Xtrain,Xtest,Ytrain,Ytest = TTS(X,y,test_size=0.3,random_state=420)

#注意,如果我们保存的模型是xgboost库中建立的模型,则导入的数据类型也必须是xgboost库中的数据类型
dtest = xgb.DMatrix(Xtest,Ytest)

#导入模型
loaded_model = pickle.load(open("xgboostonboston.dat", "rb"))
print("Loaded model from: xgboostonboston.dat")

#做预测,直接调用接口predict
ypreds = loaded_model.predict(dtest)

ypreds
"""
array([ 9.278189 , 22.734112 , 29.49379  , 12.983151 ,  9.501983 ,
       20.643223 , 15.942372 , 15.831039 , 15.698411 , 15.967683 ,
       21.101307 , 35.83475  , 20.486403 , 29.231373 , 20.785269 ,
       12.0639305, 17.634281 , 26.05238  , 25.247683 , 23.5034   ,
       18.00751  , 16.483337 , 25.402018 , 22.421213 , 20.117733 ,
       16.29775  , 21.58729  , 25.936457 , 23.091265 , 16.585093 ,
       35.39749  , 20.128454 , 20.370457 , 23.711693 , 23.2132   ,
       24.522055 , 16.185257 , 23.857044 , 18.04799  , 34.886368 ,
       17.500023 , 21.3877   , 33.37537  , 18.835125 , 15.2021055,
       28.557238 , 42.054836 , 16.839176 , 10.032375 , 37.126007 ,
       26.214668 , 21.136717 , 20.56424  , 47.07938  , 27.928053 ,
       25.919254 , 18.91586  , 20.73725  , 17.170164 , 18.296001 ,
       15.074967 , 23.753801 , 19.82896  , 31.379152 , 29.385721 ,
       20.15055  , 20.949522 , 17.336159 , 22.490997 , 16.978098 ,
       28.754507 , 40.5415   , 30.079725 , 22.954508 , 20.131071 ,
       23.611767 , 39.112865 , 27.09449  , 21.863207 , 20.840895 ,
       18.106676 , 45.172653 , 23.532963 ,  9.185723 , 26.472696 ,
       23.175745 , 17.478828 , 20.660913 , 15.487839 , 13.609945 ,
       21.267662 , 19.99994  , 39.685055 , 32.446346 , 23.493828 ,
       11.488627 , 15.72672  , 21.053246 ,  9.769615 , 11.224182 ,
       32.408775 , 16.89148  , 24.925585 , 24.327538 , 33.56309  ,
       41.950325 , 20.534348 ,  9.128101 , 22.954508 , 14.764961 ,
       44.470955 , 20.587046 , 22.605795 , 24.460056 , 19.11823  ,
       28.227682 , 23.851608 , 19.594564 , 42.40337  , 18.06053  ,
       24.152496 , 25.261152 , 16.51031  , 18.09877  , 15.671002 ,
       22.91     , 32.17411  , 10.821065 , 21.38708  , 19.205914 ,
       15.028279 , 19.736324 ,  9.437382 , 28.889278 , 29.728348 ,
       20.992556 , 18.890804 , 22.11941  , 10.96947  , 17.206701 ,
       41.021526 , 17.42233  , 23.244827 , 20.014555 , 32.103203 ,
       19.674358 , 11.808359 , 38.164032 , 24.953072 , 23.238205 ,
       16.352705 , 24.270111 ], dtype=float32)
"""

from sklearn.metrics import mean_squared_error as MSE, r2_score
MSE(Ytest,ypreds)

r2_score(Ytest,ypreds)
# 0.9013649408758324

使用Joblib保存和调用模型

Joblib是SciPy生态系统中的一部分,它为Python提供保存和调用管道和对象的功能,处理NumPy结构的数据尤其高效,对于很大的数据集和巨大的模型非常有用。Joblib与pickle API非常相似,来看看代码:

bst = xgb.train(param, dtrain, num_round)

import joblib

#同样可以看看模型被保存到了哪里
joblib.dump(bst,"xgboost-boston.dat")

loaded_model = joblib.load("xgboost-boston.dat")

dtest = xgb.DMatrix(Xtest,Ytest)
ypreds = loaded_model.predict(dtest)

ypreds
"""
array([ 9.278189 , 22.734112 , 29.49379  , 12.983151 ,  9.501983 ,
       20.643223 , 15.942372 , 15.831039 , 15.698411 , 15.967683 ,
       21.101307 , 35.83475  , 20.486403 , 29.231373 , 20.785269 ,
       12.0639305, 17.634281 , 26.05238  , 25.247683 , 23.5034   ,
       18.00751  , 16.483337 , 25.402018 , 22.421213 , 20.117733 ,
       16.29775  , 21.58729  , 25.936457 , 23.091265 , 16.585093 ,
       35.39749  , 20.128454 , 20.370457 , 23.711693 , 23.2132   ,
       24.522055 , 16.185257 , 23.857044 , 18.04799  , 34.886368 ,
       17.500023 , 21.3877   , 33.37537  , 18.835125 , 15.2021055,
       28.557238 , 42.054836 , 16.839176 , 10.032375 , 37.126007 ,
       26.214668 , 21.136717 , 20.56424  , 47.07938  , 27.928053 ,
       25.919254 , 18.91586  , 20.73725  , 17.170164 , 18.296001 ,
       15.074967 , 23.753801 , 19.82896  , 31.379152 , 29.385721 ,
       20.15055  , 20.949522 , 17.336159 , 22.490997 , 16.978098 ,
       28.754507 , 40.5415   , 30.079725 , 22.954508 , 20.131071 ,
       23.611767 , 39.112865 , 27.09449  , 21.863207 , 20.840895 ,
       18.106676 , 45.172653 , 23.532963 ,  9.185723 , 26.472696 ,
       23.175745 , 17.478828 , 20.660913 , 15.487839 , 13.609945 ,
       21.267662 , 19.99994  , 39.685055 , 32.446346 , 23.493828 ,
       11.488627 , 15.72672  , 21.053246 ,  9.769615 , 11.224182 ,
       32.408775 , 16.89148  , 24.925585 , 24.327538 , 33.56309  ,
       41.950325 , 20.534348 ,  9.128101 , 22.954508 , 14.764961 ,
       44.470955 , 20.587046 , 22.605795 , 24.460056 , 19.11823  ,
       28.227682 , 23.851608 , 19.594564 , 42.40337  , 18.06053  ,
       24.152496 , 25.261152 , 16.51031  , 18.09877  , 15.671002 ,
       22.91     , 32.17411  , 10.821065 , 21.38708  , 19.205914 ,
       15.028279 , 19.736324 ,  9.437382 , 28.889278 , 29.728348 ,
       20.992556 , 18.890804 , 22.11941  , 10.96947  , 17.206701 ,
       41.021526 , 17.42233  , 23.244827 , 20.014555 , 32.103203 ,
       19.674358 , 11.808359 , 38.164032 , 24.953072 , 23.238205 ,
       16.352705 , 24.270111 ], dtype=float32)
"""

MSE(Ytest, ypreds)
# 9.178375452806907

r2_score(Ytest,ypreds)
# 0.9013649408758324
  • 使用xgboost
#使用sklearn中的模型
from xgboost import XGBRegressor as XGBR

bst = XGBR(n_estimators=200
           ,eta=0.05,gamma=20
           ,reg_lambda=3.5
           ,reg_alpha=0.2
           ,max_depth=4
           ,colsample_bytree=0.4
           ,colsample_bylevel=0.6).fit(Xtrain,Ytrain) #训练完毕

joblib.dump(bst,"xgboost-boston-sklearn.dat")

loaded_model = joblib.load("xgboost-boston-sklearn.dat")
#则这里可以直接导入Xtest,直接是我们的numpy
ypreds = loaded_model.predict(Xtest)

Xtest
"""
array([[4.15292e+01, 0.00000e+00, 1.81000e+01, ..., 2.02000e+01,
        3.29460e+02, 2.73800e+01],
       [2.73100e-02, 0.00000e+00, 7.07000e+00, ..., 1.78000e+01,
        3.96900e+02, 9.14000e+00],
       [3.15000e-02, 9.50000e+01, 1.47000e+00, ..., 1.70000e+01,
        3.96900e+02, 4.56000e+00],
       ...,
       [5.08300e-02, 0.00000e+00, 5.19000e+00, ..., 2.02000e+01,
        3.89710e+02, 5.68000e+00],
       [3.77498e+00, 0.00000e+00, 1.81000e+01, ..., 2.02000e+01,
        2.20100e+01, 1.71500e+01],
       [1.96091e+01, 0.00000e+00, 1.81000e+01, ..., 2.02000e+01,
        3.96900e+02, 1.34400e+01]])
"""

dtest
"""
<xgboost.core.DMatrix at 0x19b45ceda58>
"""

ypreds
"""
array([ 9.209096 , 22.70573  , 29.492292 , 12.896596 ,  9.380931 ,
       20.598948 , 15.930355 , 15.803482 , 15.670854 , 15.964941 ,
       21.098568 , 35.83325  , 20.443174 , 29.229876 , 20.717224 ,
       12.092506 , 17.78799  , 26.038475 , 25.238808 , 23.554081 ,
       17.9599   , 16.415293 , 25.361229 , 22.430357 , 20.073458 ,
       16.253475 , 21.560781 , 26.077642 , 23.089767 , 16.53853  ,
       35.38861  , 20.125715 , 20.407557 , 23.721558 , 23.204325 ,
       24.524542 , 16.116165 , 23.840025 , 18.020435 , 34.884865 ,
       17.455748 , 21.375666 , 33.334576 , 18.818806 , 15.237378 ,
       28.55574  , 42.143997 , 17.188314 , 10.274554 , 37.110027 ,
       26.213171 , 21.206903 , 20.553146 , 47.0634   , 27.926361 ,
       25.937069 , 18.871584 , 20.754017 , 17.125889 , 18.293262 ,
       15.072226 , 23.72542  , 19.820786 , 31.370277 , 29.376846 ,
       20.147812 , 20.909628 , 17.267067 , 22.507765 , 16.909006 ,
       28.764372 , 40.577892 , 30.116125 , 22.956995 , 20.086796 ,
       23.621632 , 39.299065 , 27.158384 , 21.881021 , 20.877995 ,
       18.0624   , 45.261814 , 23.537197 ,  9.192714 , 26.463821 ,
       23.158726 , 17.434553 , 20.71752  , 15.730018 , 13.616936 ,
       21.251343 , 19.997202 , 39.68355  , 32.430367 , 23.49233  ,
       11.485886 , 15.682443 , 21.004986 ,  9.803841 , 11.179905 ,
       32.477985 , 16.87516  , 24.961985 , 24.318663 , 33.54711  ,
       42.246696 , 20.518028 ,  9.111778 , 22.956995 , 14.737404 ,
       44.657154 , 20.672129 , 22.568897 , 24.431675 , 19.073956 ,
       28.22599  , 23.854095 , 19.591825 , 42.49253  , 18.05779  ,
       24.21578  , 25.244133 , 16.466034 , 18.054495 , 15.60191  ,
       22.908503 , 32.15813  , 10.776788 , 21.375986 , 19.203175 ,
       15.0255375, 19.692049 ,  9.367598 , 28.887781 , 29.719473 ,
       20.944296 , 18.835167 , 22.56017  , 10.848418 , 17.162426 ,
       41.103146 , 17.406012 , 23.32718  , 20.011816 , 32.094322 ,
       19.67162  , 11.764082 , 38.330032 , 24.96984  , 23.236708 ,
       16.266153 , 24.512293 ], dtype=float32)
"""

MSE(Ytest, ypreds)
# 9.18818551539731

r2_score(Ytest,ypreds)
# 0.9012595174151566