4 XGBoost应用中的其他问题
4.1 过拟合:剪枝参数与回归模型调参
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
α,控制迭代速度的参数
η
\eta
η以及管理每次迭代前进行的随机有放回抽样的参数subsample。所有的这些参数都可以用来减轻过拟合。但除此之外,我们还有几个影响重大的,专用于剪枝的参数:
这些参数中,树的最大深度是决策树中的剪枝法宝,算是最常用的剪枝参数,不过在XGBoost中,最大深度的功能与参数
γ
\gamma
γ相似,因此如果先调节了
γ
\gamma
γ ,则最大深度可能无法展示出巨大的效果。当然,如果先调整了最大深度,则
γ
\gamma
γ也有可能无法显示明显的效果。通常来说,这两个参数中我们只使用一个,不过两个都试试也没有坏处。
三个随机抽样特征的参数中,前两个比较常用。在建立树时对特征进行抽样其实是决策树和随机森林中比较常见的一种方法,但是在XGBoost之前,这种方法并没有被使用到boosting算法当中过。Boosting算法一直以抽取样本(横向抽样)来调整模型过拟合的程度,而实践证明其实纵向抽样(抽取特征)更能够防止过拟合。
参数min_child_weight不太常用,它是一篇叶子上的二阶导数 h i h_{i} hi之和,当样本所对应的二阶导数很小时,比如说为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()
#00:00:513584
从曲线上可以看出,模型现在处于过拟合的状态。我们决定要进行剪枝。我们的目标是:训练集和测试集的结果尽量接近,如果测试集上的结果不能上升,那训练集上的结果降下来也是不错的选择(让模型不那么具体到训练数据,增加泛化能力)。在这里,我们要使用三组曲线。一组用于展示原始数据上的结果,一组用于展示上一个参数调节完毕后的结果,最后一组用于展示现在我们在调节的参数的结果。具体怎样使用,我们来看:
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'
,"nfold":5}
param3 = {'silent':True
,'obj':'reg:linear'
,"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()
在这里,为大家提供我调出来的结果,供大家参考:
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()
#00:00:532621
#00:00:223373
#00:00:259346
调参顺序:
(1)eta+num_round
(2)gamma/max_depth
(3)抽样进行调整
(4)正则化参数
在这个调整过程中,大家可能会有几个问题:
- 一个个参数调整太麻烦,可不可以使用网格搜索呢?
当然可以!只要电脑有足够的计算资源,并且你信任网格搜索,那任何时候我们都可以使用网格搜索。只是使用的时候要注意,首先XGB的参数非常多,参数可取的范围也很广,究竟是使用np.linspace或者np.arange作为参数的备选值也会影响结果,而且网格搜索的运行速度往往不容乐观,因此建议至少先使用xgboost.cv来确认参数的范围,否则很可能花很长的时间做了无用功。
并且,在使用网格搜索的时候,最好不要一次性将所有的参数都放入进行搜索,最多一次两三个。有一些互相影响的参数需要放在一起使用,比如学习率eta和树的数量n_estimators。
另外,如果网格搜索的结果与你的理解相违背,与你手动调参的结果相违背,选择模型效果较好的一个。如果两者效果差不多,那选择相信手动调参的结果。网格毕竟是枚举出结果,很多时候得出的结果可能会是具体到数据的巧合,我们无法去一一解释网格搜索得出的结论为何是这样。如果你感觉都无法解释,那就不要去在意,直接选择结果较好的一个。
- 调参的时候参数的顺序会影响调参结果吗?
会影响,因此在现实中,我们会优先调整那些对模型影响巨大的参数。在这里,我建议的剪枝上的调参顺序是:n_estimators与eta共同调节,gamma或者max_depth,采样和抽样参数(纵向抽样影响更大),最后才是正则化的两个参数。当然,可以根据自己的需求来进行调整。
- 调参之后测试集上的效果还没有原始设定上的效果好怎么办?
如果调参之后,交叉验证曲线确实显示测试集和训练集上的模型评估效果是更加接近的,推荐使用调参之后的效果。我们希望增强模型的泛化能力,然而泛化能力的增强并不代表着在新数据集上模型的结果一定优秀,因为未知数据集并非一定符合全数据的分布,在一组未知数据上表现十分优秀,也不一定就能够在其他的未知数据集上表现优秀。因此不必过于纠结在现有的测试集上是否表现优秀。当然了,在现有数据上如果能够实现训练集和测试集都非常优秀,
那模型的泛化能力自然也会是很强的。自己找一个数据集,剪枝试试看吧。
4.2 XGBoost模型的保存和调用
在使用Python进行编程时,我们可能会需要编写较为复杂的程序或者建立复杂的模型。比如XGBoost模型,这个模型的参数复杂繁多,并且调参过程不是太容易,一旦训练完毕,我们往往希望将训练完毕后的模型保存下来,以便日后用于新的数据集。在Python中,保存模型的方法有许多种。我们以XGBoost为例,来讲解两种主要的模型保存和
调用方法。
4.2.1 使用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进行保存的这个文件中是一个可以进行读取或者调用的模型
#看看模型被保存到了哪里?
import sys
sys.path
#['C:\\Pythonwork\\micro-class\\11 xgboost',
# 'C:\\Python\\python37.zip',
# 'C:\\Python\\DLLs',
# 'C:\\Python\\lib',
# 'C:\\Python',
# '',
# 'C:\\Python\\lib\\site-packages',
# 'C:\\Python\\lib\\site-packages\\win32',
# 'C:\\Python\\lib\\site-packages\\win32\\lib',
# 'C:\\Python\\lib\\site-packages\\Pythonwin',
# 'C:\\Python\\lib\\site-packages\\IPython\\extensions',
# 'C:\\Users\\Shuyu\\.ipython']
#重新打开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")
#Loaded model from: xgboostonboston.dat
#做预测,直接调用接口predict
ypreds = loaded_model.predict(dtest)
ypreds
#array([ 9.244746, 22.536953, 28.47614 , 13.126131, 9.944413, 21.356094,
# 15.187935, 15.559099, 15.629611, 15.555439, 21.427156, 35.502792,
# 20.827318, 29.397932, 21.669186, 11.906522, 21.464252, 26.143337,
# 。。。
# 8.979549, 28.35794 , 29.80491 , 21.987814, 19.893597, 19.730898,
# 10.501988, 17.405378, 40.51527 , 17.420282, 24.272373, 19.771631,
# 32.620422, 19.19032 , 12.364113, 38.63305 , 24.189354, 23.38174 ,
# 16.924698, 22.633028], dtype=float32)
from sklearn.metrics import mean_squared_error as MSE, r2_score
MSE(Ytest,ypreds)
#9.107608696116197
r2_score(Ytest,ypreds)
#0.9021254331073938
4.2.2 使用Joblib保存和调用模型
Joblib是SciPy生态系统中的一部分,它为Python提供保存和调用管道和对象的功能,处理NumPy结构的数据尤其高效,对于很大的数据集和巨大的模型非常有用。Joblib与pickle API非常相似,来看看代码:
bst = xgb.train(param, dtrain, num_round)
import joblib
#同样可以看看模型被保存到了哪里
joblib.dump(bst,"xgboost-boston.dat")
#['xgboost-boston.dat']
loaded_model = joblib.load("xgboost-boston.dat")
dtest = xgb.DMatrix(Xtest,Ytest)
ypreds = loaded_model.predict(dtest)
ypreds
#array([ 9.244746, 22.536953, 28.47614 , 13.126131, 9.944413, 21.356094,
# 15.187935, 15.559099, 15.629611, 15.555439, 21.427156, 35.502792,
# 20.827318, 29.397932, 21.669186, 11.906522, 21.464252, 26.143337,
# 26.300356, 23.474188, 18.186035, 15.851086, 22.928507, 22.919674,
# 。。。
# 8.979549, 28.35794 , 29.80491 , 21.987814, 19.893597, 19.730898,
# 10.501988, 17.405378, 40.51527 , 17.420282, 24.272373, 19.771631,
# 32.620422, 19.19032 , 12.364113, 38.63305 , 24.189354, 23.38174 ,
# 16.924698, 22.633028], dtype=float32)
MSE(Ytest, ypreds)
#9.107608696116197
r2_score(Ytest,ypreds)
#0.9021254331073938
在这两种保存方法下,我们都可以找到保存下来的dat文件,将这些文件移动到任意计算机上的python下的环境变量路径中(使用sys.path进行查看),则可以使用import来对模型进行调用。注意,模型的保存调用与自写函数的保存调用是两回事,大家要注意区分。
4.3 分类案例:XGB中的样本不均衡问题
在之前的学习中,我们一直以回归作为演示的例子,这是由于回归是XGB的常用领域的缘故。然而作为机器学习中的大头,分类算法也是不可忽视的,XGB作为分类的例子自然也是非常多。存在分类,就会存在样本不平衡问题带来的影响,XGB中存在着调节样本不平衡的参数scale_pos_weight,这个参数非常类似于之前随机森林和支持向量机中我们都使用到过的class_weight参数,通常我们在参数中输入的是负样本量与正样本量之比
sum
(
negative instances
)
sum(positive instances)
\frac{\text { sum }(\text { negative instances })}{\text { sum(positive instances) }}
sum(positive instances) sum ( negative instances )
来看看如何使用这个参数吧。
import numpy as np
import xgboost as xgb
import matplotlib.pyplot as plt
from xgboost import XGBClassifier as XGBC
from sklearn.datasets import make_blobs #自创数据集
from sklearn.model_selection import train_test_split as TTS
from sklearn.metrics import confusion_matrix as cm, recall_score as recall, roc_auc_score as auc
class_1 = 500 #类别1有500个样本
class_2 = 50 #类别2只有50个
centers = [[0.0, 0.0], [2.0, 2.0]] #设定两个类别的中心
clusters_std = [1.5, 0.5] #设定两个类别的方差,通常来说,样本量比较大的类别会更加松散
X, y = make_blobs(n_samples=[class_1, class_2],
centers=centers,
cluster_std=clusters_std,
random_state=0, shuffle=False)
X.shape
#(550, 2)
y
#array([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
# 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
# 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
# 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
# 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
# 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
# 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
# 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
# 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
# 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
# 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
# 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
# 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
# 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
# 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
# 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
# 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
# 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
# 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
# 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
# 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
# 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
# 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1,
# 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
# 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1])
(y == 1).sum() / y.shape[0] #9%
#0.09090909090909091
Xtrain, Xtest, Ytrain, Ytest = TTS(X,y,test_size=0.3,random_state=420)
#在sklearn下建模#
clf = XGBC().fit(Xtrain,Ytrain)
ypred = clf.predict(Xtest)
ypred
#array([0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
# 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
# 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0,
# 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0,
# 1, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0,
# 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
# 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0,
# 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1])
clf.score(Xtest,Ytest) #默认模型评估指标 - 准确率
#0.9272727272727272
cm(Ytest,ypred,labels=[1,0]) #少数类写在前面
#array([[ 9, 4],
# [ 8, 144]], dtype=int64)
recall(Ytest,ypred)
#0.6923076923076923
auc(Ytest,clf.predict_proba(Xtest)[:,1])
#0.9671052631578947
#负/正样本比例
clf_ = XGBC(scale_pos_weight=10).fit(Xtrain,Ytrain)
ypred_ = clf_.predict(Xtest)
clf_.score(Xtest,Ytest)
#0.95151515151515
cm(Ytest,ypred_,labels=[1,0])
#array([[ 13, 0],
# [ 8, 144]])
recall(Ytest,ypred_)
#1.0
auc(Ytest,clf_.predict_proba(Xtest)[:,1])
#0.9696356275303644
#随着样本权重逐渐增加,模型的recall,auc和准确率如何变化?
for i in [1,5,10,20,30]:
clf_ = XGBC(scale_pos_weight=i).fit(Xtrain,Ytrain)
ypred_ = clf_.predict(Xtest)
print(i)
print("\tAccuracy:{}".format(clf_.score(Xtest,Ytest)))
print("\tRecall:{}".format(recall(Ytest,ypred_)))
print("\tAUC:{}".format(auc(Ytest,clf_.predict_proba(Xtest)[:,1])))
#1
# Accuracy:0.9272727272727272
# Recall:0.6923076923076923
# AUC:0.9671052631578947
#5
# Accuracy:0.9454545454545454
# Recall:0.9230769230769231
# AUC:0.9665991902834008
#10
# Accuracy:0.9515151515151515
# Recall:1.0
# AUC:0.9696356275303644
#20
# Accuracy:0.9515151515151515
# Recall:1.0
# AUC:0.9706477732793523
#30
# Accuracy:0.9515151515151515
# Recall:1.0
# AUC:0.9701417004048584
#负/正样本比例
clf_ = XGBC(scale_pos_weight=20).fit(Xtrain,Ytrain)
ypred_ = clf_.predict(Xtest)
clf_.score(Xtest,Ytest)
#0.9515151515151515
cm(Ytest,ypred_,labels=[1,0])
#array([[ 13, 0],
# [ 8, 144]], dtype=int64)
recall(Ytest,ypred_)
#1.0
auc(Ytest,clf_.predict_proba(Xtest)[:,1])
#0.9706477732793523
dtrain = xgb.DMatrix(Xtrain,Ytrain)
dtest = xgb.DMatrix(Xtest,Ytest)
#看看xgboost库自带的predict接口
param = {'silent':True,'objective':'binary:logistic',"eta":0.1,"scale_pos_weight":1}
num_round = 100
bst = xgb.train(param, dtrain, num_round)
preds = bst.predict(dtest)
#看看preds返回了什么?
preds
#array([0.00110357, 0.00761518, 0.00110357, 0.00110357, 0.93531454,
# 0.00466839, 0.00110357, 0.00110357, 0.00110357, 0.00110357,
# 0.00110357, 0.00410493, 0.00454478, 0.00571528, 0.00751026,
# . . .
# 0.9830577 , 0.00110357, 0.00644181, 0.00110357, 0.00571528,
# 0.00110357, 0.00110357, 0.00110357, 0.00110357, 0.00466839,
# 0.00110357, 0.00110357, 0.92388713, 0.90231985, 0.80084217],
# dtype=float32)
#xgb库分类返回的是概率
#自己设定阈值
ypred = preds.copy()
ypred[preds > 0.5] = 1
ypred[ypred != 1] = 0
ypred
#array([0., 0., 0., 0., 1., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,
# 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,
# 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,
# 0., 0., 0., 0., 0., 0., 1., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,
# 0., 0., 0., 0., 0., 0., 1., 0., 0., 1., 0., 0., 0., 0., 0., 0., 1.,
# 0., 0., 0., 1., 0., 0., 0., 0., 0., 1., 0., 1., 0., 0., 0., 0., 1.,
# 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 1., 0., 1., 0., 0., 0.,
# 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,
# 1., 0., 0., 0., 0., 1., 0., 0., 0., 0., 0., 0., 0., 0., 1., 0., 0.,
# 0., 0., 0., 0., 0., 0., 0., 0., 0., 1., 1., 1.], dtype=float32)
#写明参数
scale_pos_weight = [1,5,10]
names = ["negative vs positive: 1"
,"negative vs positive: 5"
,"negative vs positive: 10"]
[*zip(names,scale_pos_weight)]
#[('negative vs positive: 1', 1),
# ('negative vs positive: 5', 5),
# ('negative vs positive: 10', 10)]
#导入模型评估指标
from sklearn.metrics import accuracy_score as accuracy, recall_score as recall, roc_auc_score as auc
for name,i in zip(names,scale_pos_weight):
param = {'silent':True,'objective':'binary:logistic'
,"eta":0.1,"scale_pos_weight":i}
num_round = 100
clf = xgb.train(param, dtrain, num_round)
preds = clf.predict(dtest)
ypred = preds.copy()
ypred[preds > 0.5] = 1
ypred[ypred != 1] = 0
print(name)
print("\tAccuracy:{}".format(accuracy(Ytest,ypred)))
print("\tRecall:{}".format(recall(Ytest,ypred)))
print("\tAUC:{}".format(auc(Ytest,preds)))
#negative vs positive: 1
# Accuracy:0.9272727272727272
# Recall:0.6923076923076923
# AUC:0.9741902834008097
#negative vs positive: 5
# Accuracy:0.9393939393939394
# Recall:0.8461538461538461
# AUC:0.9635627530364372
#negative vs positive: 10
# Accuracy:0.9515151515151515
# Recall:1.0
# AUC:0.9665991902834008
#当然我们也可以尝试不同的阈值
for name,i in zip(names,scale_pos_weight):
for thres in [0.3,0.5,0.7,0.9]:
param= {'silent':True,'objective':'binary:logistic'
,"eta":0.1,"scale_pos_weight":i}
clf = xgb.train(param, dtrain, num_round)
preds = clf.predict(dtest)
ypred = preds.copy()
ypred[preds > thres] = 1
ypred[ypred != 1] = 0
print("{},thresholds:{}".format(name,thres))
print("\tAccuracy:{}".format(accuracy(Ytest,ypred)))
print("\tRecall:{}".format(recall(Ytest,ypred)))
print("\tAUC:{}".format(auc(Ytest,preds)))
#negative vs positive: 1,thresholds:0.3
# Accuracy:0.9393939393939394
# Recall:0.8461538461538461
# AUC:0.9741902834008097
#negative vs positive: 1,thresholds:0.5
# Accuracy:0.9272727272727272
# Recall:0.6923076923076923
# AUC:0.9741902834008097
#negative vs positive: 1,thresholds:0.7
# Accuracy:0.9212121212121213
# Recall:0.6153846153846154
# AUC:0.9741902834008097
#negative vs positive: 1,thresholds:0.9
# Accuracy:0.9515151515151515
# Recall:0.5384615384615384
# AUC:0.9741902834008097
#negative vs positive: 5,thresholds:0.3
# Accuracy:0.9515151515151515
# Recall:1.0
# AUC:0.9635627530364372
#negative vs positive: 5,thresholds:0.5
# Accuracy:0.9393939393939394
# Recall:0.8461538461538461
# AUC:0.9635627530364372
#negative vs positive: 5,thresholds:0.7
# Accuracy:0.9272727272727272
# Recall:0.6923076923076923
# AUC:0.9635627530364372
#negative vs positive: 5,thresholds:0.9
# Accuracy:0.9212121212121213
# Recall:0.6153846153846154
# AUC:0.9635627530364372
#negative vs positive: 10,thresholds:0.3
# Accuracy:0.9515151515151515
# Recall:1.0
# AUC:0.9665991902834008
#negative vs positive: 10,thresholds:0.5
# Accuracy:0.9515151515151515
# Recall:1.0
# AUC:0.9665991902834008
#negative vs positive: 10,thresholds:0.7
# Accuracy:0.9393939393939394
# Recall:0.8461538461538461
# AUC:0.9665991902834008
#negative vs positive: 10,thresholds:0.9
# Accuracy:0.9212121212121213
# Recall:0.6153846153846154
# AUC:0.9665991902834008
可以看出,在xgboost库和sklearnAPI中,参数scale_pos_weight都非常有效。本质上来说,scale_pos_weight参数是通过调节预测的概率值来调节,大家可以通过查看bst.predict(Xtest)返回的结果来观察概率受到了怎样的影响。因此,当我们只关心预测出的结果是否准确,AUC面积或者召回率是否足够好,我们就可以使用scale_pos_weight参数来帮助我们。然而xgboost除了可以做分类和回归,还有其他的多种功能,在一些需要使用精确概率的领域(比如排序ranking),我们希望能够保持概率原有的模样,而提升模型的效果。这种时候,我们就无法使用scale_pos_weight来帮助我们。来看看xgboost官网是怎么说明这个问题的:
官网上说,如果我们只在意模型的整表现,则使用AUC作为模型评估指标,使用scale_pos_weight来处理样本不平衡问题,如果我们在意预测出正确的概率,那我们就无法通过调节scale_pos_weight来减轻样本不平衡问题带来的影响。
这种时候,我们需要考虑另一个参数:max_delta_step。
这个参数非常难以理解,它被称之为是“树的权重估计中允许的单次最大增量”,既可以考虑成是影响的估计的参数。xgboost官网上认为,如果我们在处理样本不均衡问题,并且十分在意得到正确的预测概率,则可以设置max_delta_step参数为一个有限的数(比如1)来帮助收敛。max_delta_step参数通常不进行使用,二分类下的样本不均衡问题时这个参数唯一的用途。
4.4 XGBoost类中的其他参数和功能
到目前为止,我们已经讲解了XGBoost类中的大部分参数和功能。这些参数和功能主要覆盖了XGBoost中的梯度提升树的原理以及XGBoost自身所带的一些特性。还有一些其他的参数和用法,是算法实际应用时需要考虑的问题。接下来,我们就来看看这些参数。
-
更多计算资源:n_jobs
nthread和n_jobs都是算法运行所使用的线程,与sklearn中规则一样,输入整数表示使用的线程,输入-1表示使用计算机全部的计算资源。如果我们的数据量很大,则我们可能需要这个参数来为我们调用更多线程。 -
降低学习难度:base_score
base_score是一个比较容易被混淆的参数,它被叫做全局偏差,在分类问题中,它是我们希望关注的分类的先验概率。比如说,如果我们有1000个样本,其中300个正样本,700个负样本,则base_score就是0.3。对于回归来说,这个分数默认0.5,但其实这个分数在这种情况下并不有效。许多使用XGBoost的人已经提出,当使用回归的时候base_score的默认应该是标签的均值,不过现在xgboost库尚未对此做出改进。使用这个参数,我们便是在告诉模型一些我们了解但模型不一定能够从数据中学习到的信息。通常我们不会使用这个参数,但对于严重的样本不均衡问题,设置一个正确的base_score取值是很有必要的。 -
生成树的随机模式:random_state
在xgb库和sklearn中,都存在空值生成树的随机模式的参数random_state。在之前的剪枝中,我们提到可以通过随机抽样样本,随机抽样特征来减轻过拟合的影响,我们可以通过其他参数来影响随机抽样的比例,却无法对随机抽样干涉更多,因此,真正的随机性还是由模型自己生成的。如果希望控制这种随机性,可以在random_state参数中输入固定整数。需要注意的是,xgb库和sklearn库中,在random_state参数中输入同一个整数未必表示同一个随机模式,不一定会得到相同的结果,因此导致模型的feature_importances也会不一致。 -
自动处理缺失值:missing
XGBoost被设计成是能够自动处理缺失值的模型,这个设计的初衷其实是为了让XGBoost能够处理稀疏矩阵。我们可以在参数missing中输入一个对象,比如np.nan,或数据的任意取值,表示将所有含有这个对象的数据作为空值处理。XGBoost会将所有的空值当作稀疏矩阵中的0来进行处理,因此在使用XGBoost的时候,我们也可以不处理缺失值。当然,通常来说,如果我们了解业务并且了解缺失值的来源,我们还是希望手动填补缺失值。
XGBoost结语
作为工程能力强,效果优秀的算法,XGBoost应用广泛并且原理复杂。不过在我们为大家解读完毕XGBoost之后,相信大家已经意识到,XGBoost的难点在于它是一个集大成的模型。它所涉及到的知识点和模型流程,多半在其他常用的机器学习模型中出现过:比如树的迭代过程,其实可以和逻辑回归中的梯度下降过程进行类比;比如剪枝的过程,许多都可以与随机森林和决策树中的知识进行对比。当然,XGBoost还有很多可以进行探索,能够使用XGB算法的库和模块也不止sklearn和xgboost,许多库对xgboost的底层原理进行了更多优化,让它变得更快更优秀(比如lightGBM,比如使用分布式进行计算等等)。本周我们学习了许多内容,大家已经对XGBoost算法有了基本的认识,了解了如何使用它来建立模型,如何使用它进行基本的调整。本周的课程只不过是梯度提升算法和XGB上的一个向导,希望大家继续探索XGBoost这个可爱的算法,再接再厉。