XGBoost本质上还是一个GBDT,但是速度和效率发挥到极致,所以叫X(Extreme)GBoosted,两者都是boosting方法。跟GBDT不同的是,XGBoost在目标函数上增加了正则项,以此来控制模型的复杂度。在对损失函数求解时进行了二阶泰勒展开,而且XGBoost块结构可以很好的支持并行计算。
一,原理理解
1.1,目标函数
相比GBDT,Xgboost考虑了树的复杂度来防止模型过拟合,因此在目标函数中增加惩罚项,也称正则项。目标函数如下
用g和h替换上一轮学习的目标函数的一阶和二阶导数
因此,目标函数可以转换为
1.2,树的复杂度
GBoost定义每棵树的复杂度包含叶子节点数和叶子节点的取值,对于XGB,每个叶子节点上有一个预测分数,也称为叶子权重,是在这个叶子节点上的样本的取值,用w表示。
在陈天奇自己的论文上只注明了L2正则项,但是在python的Xgboost包和sklearn中,都包含了L1正则的可调节参数,一般使L1正则的参数置为0
把树的复杂度加到目标函数中得到
用G和H表示每个叶子节点上的一阶导数之和和二阶导数之和
因此,目标函数可以转换为
上面的式子是一元二次函数的形式,求目标函数最小值,则每个叶子节点最终的拟合值为
其中G和H为这片叶子样本的一阶导数之和和二阶导数之和,因此,最终目标函数
2,树的形状确定--分裂准则
弱学习器的分裂过程使用了贪心算法,每次分裂生成左右两个叶子,需要使下面式子的值大于0,使以下式子的值最大时为最佳分裂点
Gain也可以作为feature_importance_的计算方法之一。树分裂也可表达为
即为参数中gamma,可用来调节模型复杂度
3,贪心算法和近似算法
XGBoost中采用预排序的方法,计算过程当中是按照value的排序,逐个数据样本来计算划分收益,这样的算法能够精确的找到最佳划分值。贪心算法遍历所有特征以及所有分割点,每次选最好的那个,但是这种计算代价太大,当数据量和特征维度都比较多的时候,计算起来比较复杂,所以陈天奇用了一种近似算法,分裂时选一些候选点,在候选点里面找到最佳分裂点进行分裂,找寻候选点的方式是让损失在左右两个叶子上分布得均匀一些,陈天奇用损失函数的二次导数h代表每个样本的权重,让分裂后每个箱子的二次导数之和都差不多,控制分裂的候选点个数是由min_child_weight来进行控制。
寻找候选点有两种方法:
全局近似:构建树开始时提出所有候选分裂点,树的每一层采用相同的候选,一般适用于层次较少的树
局部近似:每次分裂时都分裂提出各自的候选分裂点,适用于层次较深的树
Xgboost 在处理带缺失值的特征时,先对非缺失的样本进行排序,对该特征缺失的样本先不处理,然后在遍历每个分裂点时,将这些缺失样本分别划入左子树和右子树来计算损失然后求最优。如果训练样本中没有缺失值,而预测过程中出现了缺失值,那么样本会被默认分到右子树。
二,参数对比
Xgboost提供了原生接口和sklearn接口,参数大同小异,如果要使用Xgboost的原生接口,需要先安装xgboost包,安装命令如下:
pip install xgboost -i https://pypi.tuna.tsinghua.edu.cn/simple
两个接口的参数对比,字有点小,打开图片
三,常用调包命令
例子用到的数据是在kaggle上下载的信用卡欺诈的样本《creditcard.csv》
导入常用包
#分析import pandas as pdimport numpy as np#可视化import matplotlib.pyplot as pltimport seaborn as sns#建模import xgboost as xgb #原生接口from xgboost import XGBClassifier as XGBC #sklearn接口from sklearn.model_selection import GridSearchCV,train_test_split#评估from sklearn.metrics import accuracy_score,roc_auc_score as auc,recall_score as recall,confusion_matrix as cm #样本不均衡适用
原数据样本有284807条数据,正负样本分布极不平衡,正样本比例仅为0.17%,在建模之前先做了欠抽样,将样本减低到25092条,样本比例为1.96%,提高模型的运行速率。我也做了实验,可以把全部样本导进模型,模型效果跟抽样后效果差不多,只是运行速率较慢。
样本拆分
Xtrain,Xtest,Ytrain,Ytest=train_test_split(data,label,test_size=0.3,random_state=20200517)#如调用xgboost的原生接口,需要把数据装进DMatrix里面DTrain=xgb.DMatrix(Xtrain,label=Ytrain)DTest=xgb.DMatrix(Xtest)num_boost_round=200#xgboost的原生接口需要把模型参数单独生命,初始参数param={'eta':0.05 ,'objective':'binary:logistic' ,'gamma':0 ,'eval_metrics':'auc' }
创建一个函数,对比参数调节前后的评价指标变化
def cvplot(DTrain,param,num_boost_round,metrics,nfold,cvresult): ''' DTrain:训练数据集 param:调整的训练参数 num_boost_round:弱学习器个数 metrics:评价准则 nfold:交叉验证折数 cvresult:对比的调参结果 ''' cvresult_adjust=xgb.cv(param,DTrain,num_boost_round=num_boost_round,metrics=metrics,nfold=nfold) plt.figure(5*20) plt.plot(range(1,num_boost_round+1),cvresult.iloc[:,0],c='red',label='Train-Orginal') plt.plot(range(1,num_boost_round+1),cvresult.iloc[:,2],c='darkred',label='Test-Orginal') plt.plot(range(1,num_boost_round+1),cvresult_adjust.iloc[:,0],c='blue',label='Train-Adjust') plt.plot(range(1,num_boost_round+1),cvresult_adjust.iloc[:,2],c='skyblue',label='Test-Adjust') plt.legend() plt.show() return cvresult_adjust
原生接口的交叉验证--xgb.cv
#粗选学习率和学习器个数,确定范围cvresult_vv1=xgb.cv(param,DTrain,num_boost_round=num_boost_round,metrics='auc',nfold=5)#平衡不平衡样本-测试scale_pos_weight作用param['scale_pos_weight']=(Ytrain.count()-Ytrain.sum())/Ytrain.sum()cvresult_vv2=cvplot(DTrain,param,num_boost_round,metrics='auc',nfold=5,cvresult=cvresult_vv1)
加入scale_pos_weight之后可以看到模型的收敛速度确实比较快
网格搜索法--GridSearchCV
param_grid_tree={'max_depth':range(2,9,2) ,'min_child_weight':range(1,6,2)}grid_search=GridSearchCV(estimator=XGBC(learing_rate=0.05 ,n_estimators=200 ,scale_pos_weight=param['scale_pos_weight'] ) ,param_grid=param_grid_tree,cv=5,scoring='roc_auc')grid_search.fit(Xtrain,Ytrain)grid_search.best_params_grid_search.best_estimator_grid_search.best_score_
可以用这种方式来调整模型复杂度,包括max_depth,min_child_weight,gamma和正则项等
训练模型
原生接口
#训练xg_cl=xgb.train(params=param,dtrain=DTrain,num_boost_round=140)#预测测试集#原生接口只输出预测概率preds = xg_cl.predict(DTest)preditLabel=[round(values) for values in preds]#评估accuracy_score(Ytest,preditLabel) #准确率recall(Ytest,preditLabel) #召回率cm(Ytest,preditLabel,labels=[1,0]) #混淆矩阵auc(Ytest,preditLabel) #auc #特征重要性'''重要性指标:weight - 该特征在所有树中被用作分割样本的特征的次数。gain - 分裂时在所有树中的平均增益。cover - 每个特征在分裂时结点处的平均二阶导数'''xgb.plot_importance(xg_cl,importance_type='weight')feature_score=xg_cl.get_score(importance_type='weight')
sklearn接口
#训练xbg_model= XGBC(learing_rate=0.01 ,n_estimators=140 ,scale_pos_weight=param['scale_pos_weight'] ,max_depth=4 ,min_child_weight=1 ,gamma=0 ,objective='binary:logistic' ,random_state=2020)xbg_model.fit(Xtrain,Ytrain)#预测测试集y_pred_test = xbg_model.predict(Xtest)y_prob_test = xbg_model.predict_proba(Xtest)#评估accuracy_score(Ytest,y_pred_test)recall(Ytest,y_pred_test)cm(Ytest,y_pred_test,labels=[1,0])auc(Ytest,y_pred_test)#特征重要性'''重要性指标:weight - 该特征在所有树中被用作分割样本的特征的次数。gain - 分裂时在所有树中的平均增益。cover - 每个特征在分裂时结点处的平均二阶导数'''xgb.plot_importance(xbg_model,importance_type='gain')#model.feature_importances_的重要性排名默认使用gainfeature_score=xbg_model.feature_importances_ #等于原生接口的get_score
保存模型
""" 第一种, pickle"""import picklepickle.dump(xg_cl, open('D:/bike/机器学习/xgboost/output/xg_cl.pkl', 'wb'))model1 = pickle.load(open('D:/bike/机器学习/xgboost/output/xg_cl.pkl', 'rb'))model1.predict(DTrain)"""第二种, sklearn的joblib"""#joblib更适合大数据量的模型,且只能往硬盘存储,不能往字符串存储from sklearn.externals import joblibjoblib.dump(xbg_model, 'D:/bike/机器学习/xgboost/output/xbg_model.pkl')model2 = joblib.load('D:/bike/机器学习/xgboost/output/xbg_model.pkl')model2.predict(Xtrain)
自定义目标函数(这一段代码是直接网上copy的哈哈哈)
#定义目标函数,返回一阶和二阶导数def logregobj(pred, dtrain): labels = dtrain.get_label() pred = 1.0 / (1+np.exp(-pred)) # sigmoid函数 grad = pred - labels hess = pred * (1-pred) return grad, hess # 返回一阶导数和二阶导数def evalerror(pred, dtrain): labels = dtrain.get_label() return 'error', float(sum(labels!=(pred>0.0)))/len(labels) # 自定义目标函数训练model = xgb.train(param, dtrain, num_round, watch_list, logregobj, evalerror)# 交叉验证xgb.cv(param, dtrain, num_round, nfold=5, seed=3, obj=logregobj, feval=evalerror)
三,跟GBDT的对比