数据集包含n个样本m个特征,对于提升树模型来说,假设有K个叠加函数(additive functions,即决策树),模型可表示为:
我们要学习上述加性模型,需要最小化正则化后的损失函数:
假设 y ^ i ( t ) \hat{y}_i^{(t)} y^i(t)表示在第 t t t次迭代时对第 i i i个样本的预测值,那么目标函数可表示为:
其中 ( y ) ^ i t − 1 \hat{(y)}_i^{t-1} (y)^it−1表示第 t − 1 t-1 t−1次迭代时,集成模型的预测值, f t ( . ) f_t(.) ft(.)表示第 t t t个决策树,在经过前 t − 1 t-1 t−1轮迭代后, y ^ i ( t − 1 ) \hat{y}_i^{(t-1)} y^i(t−1)为常数,所以只需要学习 f t ( x i ) f_t(x_i) ft(xi),同时在前 t − 1 t-1 t−1轮迭代后,集成模型的复杂度也是常数,所以目标函数没有包括。
泰勒公式的二阶近似:
f ( x + Δ x ) = f ( x ) + f ′ ( x ) Δ x + 1 2 f ′ ′ ( x ) Δ x + o ( Δ x ) f(x+\Delta x)=f(x)+f'(x)\Delta x+\frac{1}{2}f''(x)\Delta x+o(\Delta x) f(x+Δx)=f(x)+f′(x)Δx+21f′′(x)Δx+o(Δx)
对于损失函数来说,将 y ^ i ( t − 1 ) \hat y_i^{(t-1)} y^i(t−1)对应于 x x x, f t ( x i ) f_t(x_i) ft(xi)对应于 Δ x \Delta x Δx,损失函数可以近似为:
L ( t ) = ∑ i = 1 l ( y i , y ^ i ( t − 1 ) + f t ( x i ) ) + Ω ( f t ) L^{(t)}=\sum_{i=1}l(y_i, \hat y_i^{(t-1)}+f_t(x_i))+\Omega(f_t) L(t)=i=1∑l(yi,y^i(t−1)+ft(xi))+Ω(ft)
L ( t ) ≅ ∑ i = 1 n [ l ( y i , y ^ ( t − 1 ) ) + ∂ y ^ ( t − 1 ) l ( y i , y ^ ( t − 1 ) ) f t ( x i ) + 1 2 ∂ y ^ ( t − 1 ) 2 l ( y i , y ^ ( t − 1 ) ) f t 2 ( x i ) ] + Ω ( f t ) L^{(t)}\cong\sum_{i=1}^n[l(y_i, \hat y^{(t-1)})+\partial_{\hat y(t-1)}l(y_i, \hat y^{(t-1)})f_t(x_i)+\frac{1}{2}\partial^2_{\hat y(t-1)}l(y_i, \hat y^{(t-1)})f^2_t(x_i)]+\Omega(f_t) L(t)≅i=1∑n[l(yi,y^(t−1))+∂y^(t−1)l(yi,y^(t−1))ft(xi)+21∂y^(t−1)2l(yi,y^(t−1))ft2(xi)]+Ω(ft)
我们令 g i = ∂ y ^ ( t − 1 ) l ( y i , y ^ ( t − 1 ) ) g_i=\partial_{\hat y(t-1)}l(y_i, \hat y^{(t-1)}) gi=∂y^(t−1)l(yi,y^(t−1)), h i = ∂ y ^ ( t − 1 ) 2 l ( y i , y ^ ( t − 1 ) ) h_i=\partial^2_{\hat y(t-1)}l(y_i, \hat y^{(t-1)}) hi=∂y^(t−1)2l(yi,y^(t−1))
因此目标函数可以近似为:
L ( t ) ≅ ∑ i = 1 n [ l ( y i , y ^ ( t − 1 ) ) + g i f t ( x i ) + 1 2 h i f t 2 ( x i ) ] + Ω ( f t ) L^{(t)}\cong\sum_{i=1}^n[l(y_i, \hat y^{(t-1)})+g_if_t(x_i)+\frac{1}{2}h_if^2_t(x_i)]+\Omega(f_t) L(t)≅i=1∑n[l(yi,y^(t−1))+gift(xi)+21hift2(xi)]+Ω(ft)
并且 l ( y i , y ^ ( t − 1 ) ) l(y_i, \hat y^{(t-1)}) l(yi,y^(t−1))为常数,可以去掉:
L ( t ) ≅ ∑ i = 1 n [ g i f t ( x i ) + 1 2 h i f t 2 ( x i ) ] + Ω ( f t ) L^{(t)}\cong\sum_{i=1}^n[g_if_t(x_i)+\frac{1}{2}h_if^2_t(x_i)]+\Omega(f_t) L(t)≅i=1∑n[gift(xi)+21hift2(xi)]+Ω(ft)
一阶导 g i g_i gi和二阶导 h i h_i hi是已知的,因此需要学习的参数在 f t ( x i ) f_t(x_i) ft(xi)中。
复杂度 Ω ( f t ) \Omega(f_t) Ω(ft)与叶子节点数量和叶子结点的值 w w w(学习参数)(某个样本的预测值对应于某个叶子结点的值,这样就能描述清楚决策树上对应每个样本的预测值)有关。一般表示为:
Ω ( f t ) = γ T + 1 2 λ ∥ w ∥ 2 \Omega(f_t)=\gamma T+\frac{1}{2} \lambda \lVert w \rVert ^2 Ω(ft)=γT+21λ∥w∥2
我们将目标函数表示为:
L ( t ) ≅ ∑ i = 1 n [ g i f t ( x i ) + 1 2 h i f t 2 ( x i ) ] + γ T + 1 2 λ ∑ j T w j 2 = ∑ j = 1 T [ ( ∑ i ∈ I j g i ) w j + 1 2 ( ∑ i ∈ I j h i + λ ) w j 2 ] + γ T L^{(t)}\cong\sum_{i=1}^n[g_i f_t(x_i)+\frac{1}{2}h_if^2_t(x_i)]+\gamma T+\frac{1}{2} \lambda \sum_{j}^T w_j^2 \\ =\sum_{j=1}^T[(\sum_{i \in I_j }g_i)w_j+\frac{1}{2}(\sum_{i\in I_j}h_i+\lambda)w_j^2]+\gamma T L(t)≅i=1∑n[gift(xi)+21hift2(xi)]+γT+21λj∑Twj2=j=1∑T[(i∈Ij∑gi)wj+21(i∈Ij∑hi+λ)wj2]+γT
I j = { i ∣ q ( x i ) = j } I_j=\{i|q(x_i)=j\} Ij={i∣q(xi)=j}
式 I j I_j Ij表示决策树中分配到第 j j j个叶子节点中的样本集合,其中 q ( x ) q(x) q(x)表示单个树的结果,即样本对应叶子节点的索引。
可以看到 L ( t ) L^(t) L(t)是关于 w j w_j wj的二次函数,最优解为 w j ∗ = − b 2 a w_j^*=-\frac{b}{2a} wj∗=−2ab,即
w j ∗ = − ∑ i ∈ I j g i ∑ i ∈ I j h i + λ w_j^*=-\frac{\sum_{i \in I_j }g_i}{\sum_{i\in I_j}h_i+\lambda} wj∗=−∑i∈Ijhi+λ∑i∈Ijgi
将 w j ∗ w_j^* wj∗带入 L ( t ) L^{(t)} L(t),可以得到对应的目标函数为:
L ∗ ( t ) = − 1 2 ∑ j = 1 T ( ∑ i ∈ I j g i ) 2 ∑ i ∈ I j h i + λ + γ T L^{(t)}_*=-\frac{1}{2}\sum_{j=1}^T\frac{(\sum_{i\in I_j }g_i)^2}{\sum_{i\in I_j}h_i+\lambda}+\gamma T L∗(t)=−21j=1∑T∑i∈Ijhi+λ(∑i∈Ijgi)2+γT
简单的方法是将所有决策树的结构全部罗列出来,然后优化,计算他们的目标函数值,进行比较,选择最小目标函数值的决策树。但是如果决策树深度过深,那么所有决策树的数量太多,很难完成上述步骤。
所以,文章采用了一种贪心算法(greedy algorithm)的策略,从单个叶子结点开始,迭代的增加分治进行比较。
当前有样本 1 , 2 , 3 , 4 , 5 , 6 {1, 2, 3, 4, 5, 6} 1,2,3,4,5,6,目前决策树为:
其中目标函数值可以表示为:
o b j o l d ∗ = − 1 2 [ ( g 1 + g 2 ) 2 h 1 + h 2 + λ + ( g 3 + g 4 + g 5 + g 6 ) 2 h 3 + h 4 + h 5 + h 6 + λ ] + 2 γ obj^*_{old}=-\frac{1}{2}[\frac{(g_1+g_2)^2}{h1+h2+\lambda}+\frac{(g_3+g_4+g_5+g_6)^2}{h3+h4+h5+h6+\lambda}]+2\gamma objold∗=−21[h1+h2+λ(g1+g2)2+h3+h4+h5+h6+λ(g3+g4+g5+g6)2]+2γ
我们对右子树进行划分:
此时目标函数为:
o b j n e w ∗ = − 1 2 [ ( g 1 + g 2 ) 2 h 1 + h 2 + λ + ( g 3 + g 4 ) 2 h 3 + h 4 + λ + ( g 5 + g 6 ) 2 h 5 + h 6 + λ ] + 3 γ obj^*_{new}=-\frac{1}{2}[\frac{(g_1+g_2)^2}{h1+h2+\lambda}+\frac{(g_3+g_4)^2}{h3+h4+\lambda}+\frac{(g_5+g_6)^2}{h5+h6+\lambda}]+3\gamma objnew∗=−21[h1+h2+λ(g1+g2)2+h3+h4+λ(g3+g4)2+h5+h6+λ(g5+g6)2]+3γ
将二者相减为:
o b j o l d ∗ − o b j n e w ∗ = 1 2 [ ( g 3 + g 4 ) 2 h 3 + h 4 + λ + ( g 5 + g 6 ) 2 h 5 + h 6 + λ − ( g 3 + g 4 + g 5 + g 6 ) 2 h 3 + h 4 + h 5 + h 6 + λ ] − γ obj^*_{old}-obj^*_{new}=\frac{1}{2}[\frac{(g_3+g_4)^2}{h3+h4+\lambda}+\frac{(g_5+g_6)^2}{h5+h6+\lambda}-\frac{(g_3+g_4+g_5+g_6)^2}{h3+h4+h5+h6+\lambda}]-\gamma objold∗−objnew∗=21[h3+h4+λ(g3+g4)2+h5+h6+λ(g5+g6)2−h3+h4+h5+h6+λ(g3+g4+g5+g6)2]−γ
如果 o b j o l d ∗ − o b j n e w ∗ > 0 obj^*_{old}-obj^*_{new}>0 objold∗−objnew∗>0,则说明可以进行分支。
通过以上分割方法,就可以分步的找到基于贪心的局部最优决策树。
以kaggle 2015年航班延误数据为例,进行建模。
数据来源:https://www.kaggle.com/usdot/flight-delays
该数据集完整数据量有500多万条航班记录数据,特征有31个,我们仅采用1%的数据和11个特征,经过预处理后重新构建训练数据集,目标是构建对航班是否延误的二分类模型。
#导入模块
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import xgboost as xgb
import warnings
warnings.filterwarnings('ignore')#忽略warning
from sklearn.model_selection import train_test_split
from sklearn.metrics import roc_auc_score, accuracy_score, confusion_matrix
#数据读取
flight=pd.read_csv(r'./flights/flights.csv')
print(flight.shape)
#结果
(5819079, 31)
#抽取选取的列
cols=["MONTH", "DAY", "DAY_OF_WEEK", "AIRLINE","FLIGHT_NUMBER",
"DESTINATION_AIRPORT", "ORIGIN_AIRPORT","AIR_TIME","DEPARTURE_TIME",
"DISTANCE", "ARRIVAL_DELAY"]
#月、日,每周的第几天,航空公司,航班号,目的地机场,始发机场,空气流动时间,起飞时间,距离,航班延误。
#数据抽样,获取指定特征
flight_part=flight.sample(frac=0.01, random_state=10)[cols]
print('flight_part.shape=', flight_part.shape)
#训练标签进行二值离散化,延误大于10分钟的转化为1(延误),延误小于10分钟的转化为0(不延误)
flight_part['ARRIVAL_DELAY']=(flight_part['ARRIVAL_DELAY']>10)*1
#类别特征编码,对“航线”、“航班号”、“目的地机场”、“出发地机场”等类别特征进行类别编码处理
cat_cols=["AIRLINE","FLIGHT_NUMBER","DESTINATION_AIRPORT", "ORIGIN_AIRPORT"]
for i in cat_cols:
flight_part[i]=flight_part[i].astype('category').cat.codes+1
#训练集和测试集划分
X_train,X_test,y_train,y_test=train_test_split(flight_part.drop('ARRIVAL_DELAY',axis=1),
flight_part['ARRIVAL_DELAY'],
random_state=10,
test_size=0.3)
#打印划分后的数据集大小
print('X_train.shape=',X_train.shape,
'\ny_train.shape=',y_train.shape,
'\nX_test.shape=',X_test.shape,
'\ny_test.shape=',y_test.shape)
X_train.head()
sklearn中xgboost模块的XGBClassifier函数参数:
常规参数:
模型参数之Tree Booster
:
模型参数之 Linear Booster
:
学习任务参数:
clf = xgb.XGBClassifier(learning_rate=0.1, # 学习率
n_estimators=100, # 树的个数--100棵树建立xgboost
max_depth=8, # 树的深度
gamma=0.1, # 惩罚项中叶子结点个数T前的参数
reg_lambda=2, # L2惩罚参数
random_state=1000, # 随机数
min_child_weight=3, # 最小叶子节点样本权重和
colsample=0.7, # 列采样的比例
eta=0.001, # 学习率
nthread=-1, # 使用全部CPU进行计算
eval_metric='logloss').fit(X_train, y_train) # 损失函数选择对数似然
y_test, y_pred = y_test, clf.predict(X_test) # y_pred输出预测样本的类别
print("Accuracy : %.4f" % accuracy_score(y_test, y_pred))
# predict_proba(X_train)输出预测样本的概率,输出2个值:[0.38, 0.62],其中0.38表示此样本预测为0类别的概率,0.62表示为1类别的概率,所以[:,1]将1类别的概率索引出来。
y_train_proba = clf.predict_proba(X_train)[:, 1]
print("AUC Score (Train): %.4f" % roc_auc_score(y_train, y_train_proba))
y_proba = clf.predict_proba(X_test)[:, 1]
print("AUC Score (Test): %.4f" % roc_auc_score(y_test, y_proba))
confusion_matrix(y_test, y_pred)
# 结果
#结果
Accuracy : 0.7983
AUC Score (Train): 0.8560
AUC Score (Test): 0.7206
array([[13470, 221],
[ 3300, 467]], dtype=int64)
机器学习中常用的参数调节方法有:网格搜索(grid search)、随机搜索(random search)和贝叶斯优化(bayesian optimization)。
网格搜索是一项常用的超参数调优方法,常用于优化三个或者更少数量的超参数,本质是一种穷举法。对于每个超参数,使用者选择一个较小的有限集去探索。然后,这些超参数笛卡尔乘积得到若干个超参数组合。网格搜索使用每组超参数训练模型,挑选验证集误差最小的超参数作为最好的超参数。
例如,我们有三个需要优化的超参数a,b,c,候选的取值分别是{1,2},{3,4},{5,6}。则所有可能的参数取值组合组成了一个8个点的3维空间网格如下:{(1,3,5),(1,3,6),(1,4,5),(1,4,6),(2,3,5),(2,3,6),(2,4,5),(2,4,6)},网格搜索就是通过遍历这8个可能的参数取值组合,进行训练和验证,最终得到最优超参数。
sklearn中通过model_selection模块下的GridSearchCV来实现网格搜索调参(详见:https://scikit-learn.org/stable/modules/generated/sklearn.model_selection.GridSearchCV.html),并且这个调参过程是加了交叉验证的。
from sklearn.model_selection import GridSearchCV
model=xgb.XGBClassifier()
param_dict={'max_depth':range(3,10,1),
'min_child_weight':[1,3,6],
'n_estimators':[100,150,200],
'learning_rate':[0.01,0.05,0.1]}
grid_search=GridSearchCV(estimator=model, param_grid=param_dict,cv=3,scoring='accuracy',verbose=10,n_jobs=-1)
grid_search.fit(X_train, y_train)
#结果
Fitting 3 folds for each of 189 candidates, totalling 567 fits
[Parallel(n_jobs=-1)]: Using backend LokyBackend with 8 concurrent workers.
[Parallel(n_jobs=-1)]: Done 2 tasks | elapsed: 1.3s
[Parallel(n_jobs=-1)]: Done 9 tasks | elapsed: 2.8s
[Parallel(n_jobs=-1)]: Done 16 tasks | elapsed: 4.9s
[Parallel(n_jobs=-1)]: Done 25 tasks | elapsed: 7.6s
[Parallel(n_jobs=-1)]: Done 34 tasks | elapsed: 10.3s
[Parallel(n_jobs=-1)]: Done 45 tasks | elapsed: 14.3s
[Parallel(n_jobs=-1)]: Done 56 tasks | elapsed: 18.5s
[Parallel(n_jobs=-1)]: Done 69 tasks | elapsed: 25.4s
[Parallel(n_jobs=-1)]: Done 82 tasks | elapsed: 33.2s
[Parallel(n_jobs=-1)]: Done 97 tasks | elapsed: 44.8s
[Parallel(n_jobs=-1)]: Done 112 tasks | elapsed: 56.5s
[Parallel(n_jobs=-1)]: Done 129 tasks | elapsed: 1.2min
[Parallel(n_jobs=-1)]: Done 146 tasks | elapsed: 1.5min
[Parallel(n_jobs=-1)]: Done 165 tasks | elapsed: 1.8min
[Parallel(n_jobs=-1)]: Done 184 tasks | elapsed: 2.3min
[Parallel(n_jobs=-1)]: Done 205 tasks | elapsed: 2.4min
[Parallel(n_jobs=-1)]: Done 226 tasks | elapsed: 2.6min
[Parallel(n_jobs=-1)]: Done 249 tasks | elapsed: 2.7min
[Parallel(n_jobs=-1)]: Done 272 tasks | elapsed: 2.9min
[Parallel(n_jobs=-1)]: Done 297 tasks | elapsed: 3.2min
[Parallel(n_jobs=-1)]: Done 322 tasks | elapsed: 3.6min
[Parallel(n_jobs=-1)]: Done 349 tasks | elapsed: 4.0min
[Parallel(n_jobs=-1)]: Done 376 tasks | elapsed: 4.5min
[Parallel(n_jobs=-1)]: Done 405 tasks | elapsed: 4.7min
[Parallel(n_jobs=-1)]: Done 434 tasks | elapsed: 4.9min
[Parallel(n_jobs=-1)]: Done 465 tasks | elapsed: 5.2min
[Parallel(n_jobs=-1)]: Done 496 tasks | elapsed: 5.5min
[Parallel(n_jobs=-1)]: Done 529 tasks | elapsed: 6.0min
[Parallel(n_jobs=-1)]: Done 567 out of 567 | elapsed: 6.6min finished
[11:41:37]
GridSearchCV(cv=3,
estimator=XGBClassifier(base_score=None, booster=None,
colsample_bylevel=None,
colsample_bynode=None,
colsample_bytree=None, gamma=None,
gpu_id=None, importance_type='gain',
interaction_constraints=None,
learning_rate=None, max_delta_step=None,
max_depth=None, min_child_weight=None,
missing=nan, monotone_constraints=None,
n_estimators=100, n_jobs=None,
num_parallel_tree=None, random_state=None,
reg_alpha=None, reg_lambda=None,
scale_pos_weight=None, subsample=None,
tree_method=None, validate_parameters=None,
verbosity=None),
n_jobs=-1,
param_grid={'learning_rate': [0.01, 0.05, 0.1],
'max_depth': range(3, 10),
'min_child_weight': [1, 3, 6],
'n_estimators': [100, 150, 200]},
scoring='accuracy', verbose=10)
scoring参数可参考网站:https://scikit-learn.org/stable/modules/model_evaluation.html#scoring-parameter
#参数
#最优的参数
print(grid_search.best_params_)
#最好的模型
print(grid_search.best_estimator_)
#输出最优的精度
print(grid_search.best_score_)
#结果
{'learning_rate': 0.05, 'max_depth': 9, 'min_child_weight': 6, 'n_estimators': 200}
XGBClassifier(base_score=0.5, booster='gbtree', colsample_bylevel=1,
colsample_bynode=1, colsample_bytree=1, gamma=0, gpu_id=-1,
importance_type='gain', interaction_constraints='',
learning_rate=0.05, max_delta_step=0, max_depth=9,
min_child_weight=6, missing=nan, monotone_constraints='()',
n_estimators=200, n_jobs=8, num_parallel_tree=1, random_state=0,
reg_alpha=0, reg_lambda=1, scale_pos_weight=1, subsample=1,
tree_method='exact', validate_parameters=1, verbosity=None)
0.8007266808733092
from sklearn.model_selection import RandomizedSearchCV
model=xgb.XGBClassifier()
param_dict={'max_depth':range(3,10,1),
'min_child_weight':[1,3,6],
'n_estimators':[100,150,200],
'learning_rate':[0.01,0.05,0.1]}
grid_search=RandomizedSearchCV(estimator=model, param_distributions=param_dict,cv=3,scoring='accuracy',verbose=10,n_jobs=-1)
grid_search.fit(X_train, y_train)
#结果
Fitting 3 folds for each of 10 candidates, totalling 30 fits
[Parallel(n_jobs=-1)]: Using backend LokyBackend with 8 concurrent workers.
[Parallel(n_jobs=-1)]: Done 2 tasks | elapsed: 6.3s
[Parallel(n_jobs=-1)]: Done 9 tasks | elapsed: 13.2s
[Parallel(n_jobs=-1)]: Done 19 out of 30 | elapsed: 19.8s remaining: 11.4s
[Parallel(n_jobs=-1)]: Done 23 out of 30 | elapsed: 20.7s remaining: 6.2s
[Parallel(n_jobs=-1)]: Done 27 out of 30 | elapsed: 21.9s remaining: 2.3s
[Parallel(n_jobs=-1)]: Done 30 out of 30 | elapsed: 22.9s finished
[11:54:03] WARNING: C:/Users/Administrator/workspace/xgboost-win64_release_1.4.0/src/learner.cc:1095: Starting in XGBoost 1.3.0, the default evaluation metric used with the objective 'binary:logistic' was changed from 'error' to 'logloss'. Explicitly set eval_metric if you'd like to restore the old behavior.
RandomizedSearchCV(cv=3,
estimator=XGBClassifier(base_score=None, booster=None,
colsample_bylevel=None,
colsample_bynode=None,
colsample_bytree=None, gamma=None,
gpu_id=None, importance_type='gain',
interaction_constraints=None,
learning_rate=None,
max_delta_step=None, max_depth=None,
min_child_weight=None, missing=nan,
monotone_constraints=None,
n_estimators=100,...obs=None,
num_parallel_tree=None,
random_state=None, reg_alpha=None,
reg_lambda=None,
scale_pos_weight=None,
subsample=None, tree_method=None,
validate_parameters=None,
verbosity=None),
n_jobs=-1,
param_distributions={'learning_rate': [0.01, 0.05, 0.1],
'max_depth': range(3, 10),
'min_child_weight': [1, 3, 6],
'n_estimators': [100, 150, 200]},
scoring='accuracy', verbose=10)
#最优的参数
print(grid_search.best_params_)
#最好的模型
print(grid_search.best_estimator_)
#输出最优的精度
print(grid_search.best_score_)
#结果
{'n_estimators': 150, 'min_child_weight': 6, 'max_depth': 7, 'learning_rate': 0.1}
XGBClassifier(base_score=0.5, booster='gbtree', colsample_bylevel=1,
colsample_bynode=1, colsample_bytree=1, gamma=0, gpu_id=-1,
importance_type='gain', interaction_constraints='',
learning_rate=0.1, max_delta_step=0, max_depth=7,
min_child_weight=6, missing=nan, monotone_constraints='()',
n_estimators=150, n_jobs=8, num_parallel_tree=1, random_state=0,
reg_alpha=0, reg_lambda=1, scale_pos_weight=1, subsample=1,
tree_method='exact', validate_parameters=1, verbosity=None)
0.7999656259758351
贝叶斯优化的例子:
比如我们有一个目标函数c(x),代表输入为x下的代价为c(x)。优化器是无法知道这个c(x)的真实曲线如何的,只能通过部分(有限)的样本x和对应的c(x)值。假设这个c(x)下图所示。
贝叶斯优化器为了得到c(x)的全局最优解,首先要采样一些点x来观察c(x)的形状,这个过程可以叫surrogate optimization(替代优化),由于无法窥见c(x)的全貌,只能通过采样点来找到一个模拟c(x)的替代曲线,如下图所示:
得到这个模拟的/替代的曲线之后,我们就能找到两个还算不错的最小值对应的点了(上图中标注的是promising minima),于是根据当前观察到的这两个最小点,再采样更多的点,用更多的点模拟出一个更逼真的c(x)再找最小点的位置,如下图所示
然后我们重复上面这个过程,每次重复的时候我们干以下几件事情:先找到可拟合当前点的一个替代函数,然后根据替代函数的最小值所在的位置去采样更多的 ,再更新替代函数,如此往复。
基本思想:
基于数据使用贝叶斯定理估计目标函数的后验分布,然后再根据分布选择下一个采样的超参数组合。它充分利用了前一个采样点的信息,其优化的工作方式是通过对目标函数形状的学习,并找到使结果向全局最大提升的参数。
高斯过程用于在贝叶斯优化中对目标函数建模,得到其后验分布。
通过高斯过程建模之后,我们尝试抽样进行样本计算,而贝叶斯优化很容易在局部最优解上不断采样,这就涉及到了开发和探索之间的权衡。
几种主流的Acquistion Function:
缺点和不足:
代码实现
我们先定义一个目标函数,里面放入我们希望优化的函数。比如此时,函数输入为XGBoost的所有参数,输出为模型交叉验证5次的AUC均值,作为我们的目标函数。因为bayes_opt库只支持最大值,所以最后的输出如果是越小越好,那么需要在前面加上负号,以转为最小值。由于bayes优化只能优化连续超参数,因此要加上int()转为离散超参数。(参考:https://www.cnblogs.com/yangruiGB2312/p/9374377.html)
(cross_val_score函数参考:https://scikit-learn.org/stable/modules/generated/sklearn.model_selection.cross_val_score.html)
from sklearn.model_selection import cross_val_score
import xgboost as xgb
def xgb_cv(n_estimators, max_depth,learning_rate,min_child_weight):
val = cross_val_score(
xgb.XGBClassifier(max_depth=int(max_depth),
learning_rate=min(learning_rate,0.3),
n_estimators=int(n_estimators),
nthread=-1,
min_child_weight=int(min_child_weight),
seed=0
),
X_train,y_train, scoring='roc_auc', cv=5
).mean()
return val
#然后我们就可以实例化一个bayes优化对象了
#里面的第一个参数是我们的优化目标函数,第二个参数是我们所需要输入的超参数名称,以及其范围。
#超参数名称必须和目标函数的输入名称一一对应。
xgb_bo = BayesianOptimization(
xgb_cv,
{
'max_depth': (5,10),
'learning_rate': (0.1,0.3),
'n_estimators': (50,100),
'min_child_weight': (5,10)
}
)
#开始优化
xgb_bo.maximize(n_iter=20)
#结果
| iter | target | learning_rate | max_depth | min_child_weight| n_estimators |
| 1 | 0.5614 | 0.257 | 8.252 | 6.389 | 90.26 |
| 2 | 0.5624 | 0.2817 | 9.346 | 7.396 | 66.48 |
| 3 | 0.5739 | 0.2782 | 6.772 | 5.166 | 53.22 |
.....
| 25 | 0.5789 | 0.1 | 5.0 | 10.0 | 55.02 |
=========================================================================
#查看最优参数
xgb_bo.max
#结果
{'target': 0.5789135834536424,
'params': {'learning_rate': 0.1,
'max_depth': 5.0,
'min_child_weight': 10.0,
'n_estimators': 55.020884298941965}}
XGBoost基于预排序方法的决策树算法。基本思想是:首先,对所有特征都按照特征取值进行预排序。其次,在遍历分割点的时,用O(#data)的代价找到一个特征上的最好分割点。最后,按照最好的分割点将数据分裂成左右子树。
预排序算法的优点是能精确地找到分割点。缺点是:
1)首先,空间消耗大。这样的算法需要保存数据的特征值,还保存了特征排序的结果(例如,为了后续快速的计算分割点,保存了排序后的索引),这就需要消耗训练数据两倍的内存。
2)其次,时间上也有较大的开销,在遍历每一个分割点的时候,都需要进行分裂增益的计算,消耗的代价大。
3)最后,对cache优化不友好。每轮迭代时,都需要遍历整个训练数据多次。如果把整个训练数据装进内存则会限制训练数据的大小;如果不装进内存,反复地读写训练数据又会消耗非常大的时间。
为了避免上述XGBoost的缺陷,并且能够在不损害准确率的条件下加快训练速度,lightGBM进行如下优化:
LightGBM另一个优化是Histogram(直方图)做差加速。一个叶子的直方图可以由它的父亲节点的直方图与它兄弟的直方图做差得到,在速度上可以提升一倍。
GOSS是一个样本的采样算法。在进行数据采样的时只保留了梯度较大的数据,如果样本梯度较小,则该样本的训练误差也比较小(gbdt拟合就是负梯度),但是如果直接将所有梯度较小的数据都丢弃掉势必会影响数据的总体分布。
GOSS首先将要进行分裂的特征的所有取值按照绝对值大小降序排序(XGBoost一样也进行了排序,但是LightGBM不用保存排序后的结果),选取绝对值最大的a个数据。然后在剩下的较小梯度数据中随机选择b个数据。接着将这b个数据乘以一个常数 1 − a b \frac{1-a}{b} b1−a ,这样算法就会更关注训练不足的样本,而不会过多改变原数据集的分布。最后使用这(a+b)个数据来计算信息增益。(a, b都是百分比)
对于每一步迭代(for循环里面的):
(1)先根据模型进行预测,得到样本预测值preds;
(2)根据preds计算loss,然后进一步计算得到样本梯度,样本权重w初始赋值都等于1;
(3)根据样本梯度的绝对值,降序排序得到sorted,它是样本的索引数组;
(4)大梯度样本数据选取topN=alen(I)个,得到topSet,也是索引数组;
(5)小梯度样本数据,从剩余的样本里随机挑选randN=blen(I)个,得到randSet;
(6)将topSet和randSet进行合并,得到usedSet,大小等于(a+b)*len(I) ;
(7)将小样本的样本权重乘上权重系数因子,得到新的样本权重w;
(8)根据usedSet索引上的样本I, 梯度g, 权重w,得到一个新的弱学习器newModel。
(9)将新弱学习器newModel,加入总模型(lgb是加法模型)。
总之,根据算法流程,每步迭代的样本数都减小为上一次样本数的(a+b)*100%,是指数变化的。实验表明,该做法并没有降低模型性能,反而还有一定提升。究其原因,应该是采样也会增加弱学习器的多样性,从而潜在地提升了模型的泛化能力,稍微有点像深度学习的dropout。
1)首先介绍如何判定哪些特征应该捆绑在一起?
2)如何将特征捆绑簇里面的所有特征捆绑(合并)为一个特征?
Histogram算法之上,LightGBM进行进一步的优化。首先它抛弃了大多数GBDT工具使用的按层生长 (level-wise) 的决策树生长策略,而使用了带有深度限制的按叶子生长 (leaf-wise) 算法。
XGBoost 采用 Level-wise 的增长策略,该策略遍历一次数据可以同时分裂同一层的叶子,容易进行多线程优化,也好控制模型复杂度,不容易过拟合。但实际上Level-wise是一种低效的算法,因为它不加区分的对待同一层的叶子,实际上很多叶子的分裂增益较低,没必要进行搜索和分裂,因此带来了很多没必要的计算开销。
LightGBM采用Leaf-wise的增长策略,该策略每次从当前所有叶子中,找到分裂增益最大的一个叶子,然后分裂,如此循环。因此同Level-wise相比,Leaf-wise的优点是:在分裂次数相同的情况下,Leaf-wise可以降低更多的误差,得到更好的精度;Leaf-wise的缺点是:可能会长出比较深的决策树,产生过拟合。因此LightGBM会在Leaf-wise之上增加了一个最大深度的限制,在保证高效率的同时防止过拟合。
(参考网址:https://mp.weixin.qq.com/s/UWgedc6pdz2Ydh13IAxiUQ)
from lightgbm import LGBMClassifier
from sklearn.metrics import accuracy_score
from sklearn.model_selection import GridSearchCV
from sklearn.datasets import load_iris
from sklearn.model_selection import train_test_split
from sklearn.externals import joblib
# 加载数据
iris = load_iris()
data = iris.data
target = iris.target
# 划分训练数据和测试数据
X_train, X_test, y_train, y_test = train_test_split(data, target, test_size=0.2)
# 模型训练
gbm = LGBMClassifier(num_leaves=31, learning_rate=0.05, n_estimators=20)
gbm.fit(X_train, y_train, eval_set=[(X_test, y_test)], early_stopping_rounds=5)
# 模型存储
joblib.dump(gbm, 'loan_model.pkl')
# 模型加载
gbm = joblib.load('loan_model.pkl')
# 模型预测
y_pred = gbm.predict(X_test, num_iteration=gbm.best_iteration_)
# 模型评估
print('The accuracy of prediction is:', accuracy_score(y_test, y_pred))
# 特征重要度
print('Feature importances:', list(gbm.feature_importances_))
# 网格搜索,参数优化
estimator = LGBMClassifier(num_leaves=31)
param_grid = {
'learning_rate': [0.01, 0.1, 1],
'n_estimators': [20, 40]
}
gbm = GridSearchCV(estimator, param_grid)
gbm.fit(X_train, y_train)
print('Best parameters found by grid search are:', gbm.best_params_)
import pandas as pd
from sklearn.model_selection import train_test_split
import lightgbm as lgb
from sklearn.metrics import mean_absolute_error
from sklearn.preprocessing import Imputer
# 1.读文件
data = pd.read_csv('./dataset/train.csv')
# 2.切分数据输入:特征 输出:预测目标变量
y = data.SalePrice
X = data.drop(['SalePrice'], axis=1).select_dtypes(exclude=['object'])
# 3.切分训练集、测试集,切分比例7.5 : 2.5
train_X, test_X, train_y, test_y = train_test_split(X.values, y.values, test_size=0.25)
# 4.空值处理,默认方法:使用特征列的平均值进行填充
my_imputer = Imputer()
train_X = my_imputer.fit_transform(train_X)
test_X = my_imputer.transform(test_X)
# 5.调用LightGBM模型,使用训练集数据进行训练(拟合)
# Add verbosity=2 to print messages while running boosting
my_model = lgb.LGBMRegressor(objective='regression', num_leaves=31, learning_rate=0.05, n_estimators=20,
verbosity=2)
my_model.fit(train_X, train_y, verbose=False)
# 6.使用模型对测试集数据进行预测
predictions = my_model.predict(test_X)
# 7.对模型的预测结果进行评判(平均绝对误差)
print("Mean Absolute Error : " + str(mean_absolute_error(predictions, test_y)))
参考网址:https://mp.weixin.qq.com/s/ABR1cDAvKA9pDVNe1a6qYg
数据集
这里我使用了 2015 年航班延误的 Kaggle 数据集,其中同时包含类别型变量和数值变量。这个数据集中一共有约 500 万条记录,我使用了 1% 的数据:5 万行记录。数据集官方地址:https://www.kaggle.com/usdot/flight-delays#flights.csv 。以下是建模使用的特征:
月、日、星期: 整型数据
航线或航班号: 整型数据
出发、到达机场: 数值数据
出发时间: 浮点数据
距离和飞行时间: 浮点数据
到达延误情况: 这个特征作为预测目标,并转为二值变量:航班是否延误超过 10 分钟
实验说明: 在对 CatBoost 调参时,很难对类别型特征赋予指标。因此,同时给出了不传递类别型特征时的调参结果,并评估了两个模型:一个包含类别型特征,另一个不包含。如果未在cat_features参数中传递任何内容,CatBoost会将所有列视为数值变量。注意,如果某一列数据中包含字符串值,CatBoost 算法就会抛出错误。另外,带有默认值的 int 型变量也会默认被当成数值数据处理。在 CatBoost 中,必须对变量进行声明,才可以让算法将其作为类别型变量处理。
不加Categorical features选项的代码-分类
import pandas as pd, numpy as np
from sklearn.model_selection import train_test_split, GridSearchCV
from sklearn import metrics
import catboost as cb
# 一共有约 500 万条记录,我使用了 1% 的数据:5 万行记录
# data = pd.read_csv("flight-delays/flights.csv")
# data = data.sample(frac=0.1, random_state=10) # 500->50
# data = data.sample(frac=0.1, random_state=10) # 50->5
# data.to_csv("flight-delays/min_flights.csv")
# 读取 5 万行记录
data = pd.read_csv("flight-delays/min_flights.csv")
print(data.shape) # (58191, 31)
data = data[["MONTH", "DAY", "DAY_OF_WEEK", "AIRLINE", "FLIGHT_NUMBER", "DESTINATION_AIRPORT",
"ORIGIN_AIRPORT", "AIR_TIME", "DEPARTURE_TIME", "DISTANCE", "ARRIVAL_DELAY"]]
data.dropna(inplace=True)
data["ARRIVAL_DELAY"] = (data["ARRIVAL_DELAY"] > 10) * 1
cols = ["AIRLINE", "FLIGHT_NUMBER", "DESTINATION_AIRPORT", "ORIGIN_AIRPORT"]
for item in cols:
data[item] = data[item].astype("category").cat.codes + 1
train, test, y_train, y_test = train_test_split(data.drop(["ARRIVAL_DELAY"], axis=1), data["ARRIVAL_DELAY"],
random_state=10, test_size=0.25)
cat_features_index = [0, 1, 2, 3, 4, 5, 6]
def auc(m, train, test):
return (metrics.roc_auc_score(y_train, m.predict_proba(train)[:, 1]),
metrics.roc_auc_score(y_test, m.predict_proba(test)[:, 1]))
# 调参,用网格搜索调出最优参数
params = {'depth': [4, 7, 10],
'learning_rate': [0.03, 0.1, 0.15],
'l2_leaf_reg': [1, 4, 9],
'iterations': [300, 500]}
cb = cb.CatBoostClassifier()
cb_model = GridSearchCV(cb, params, scoring="roc_auc", cv=3)
cb_model.fit(train, y_train)
# 查看最佳分数
print(cb_model.best_score_) # 0.7088001891107445
# 查看最佳参数
print(cb_model.best_params_) # {'depth': 4, 'iterations': 500, 'l2_leaf_reg': 9, 'learning_rate': 0.15}
# With Categorical features,用最优参数拟合数据
clf = cb.CatBoostClassifier(eval_metric="AUC", depth=4, iterations=500, l2_leaf_reg=9,
learning_rate=0.15)
clf.fit(train, y_train)
print(auc(clf, train, test)) # (0.7809684655761157, 0.7104617034553192)
from catboost import CatBoostClassifier
import pandas as pd
from sklearn.model_selection import train_test_split
import numpy as np
data = pd.read_csv("ctr_train.txt", delimiter="\t")
del data["user_tags"]
data = data.fillna(0)
X_train, X_validation, y_train, y_validation = train_test_split(data.iloc[:,:-1],data.iloc[:,-1],test_size=0.3 , random_state=1234)
categorical_features_indices = np.where(X_train.dtypes != np.float)[0] #找出类别变量
model = CatBoostClassifier(iterations=100, depth=5,cat_features=categorical_features_indices,learning_rate=0.5, loss_function='Logloss',
logging_level='Verbose')
model.fit(X_train,y_train,eval_set=(X_validation, y_validation),plot=True) #开始训练
#将plot = ture 打开后,catboot包还提供了非常炫酷的训练可视化功能,从下图可以看到我的Logloss正在不停的下降。
训练结束后,通过model.feature_importances_属性,我们可以拿到这些特征的重要程度数据,特征的重要性程度可以帮助我们分析出一些有用的信息。
import matplotlib.pyplot as plt
fea_ = model.feature_importances_
fea_name = model.feature_names_
plt.figure(figsize=(10, 10))
plt.barh(fea_name,fea_,height =0.5)
rom catboost import CatBoostRegressor
# Initialize data
train_data = [[1, 4, 5, 6],
[4, 5, 6, 7],
[30, 40, 50, 60]]
eval_data = [[2, 4, 6, 8],
[1, 4, 50, 60]]
train_labels = [10, 20, 30]
# Initialize CatBoostRegressor
model = CatBoostRegressor(iterations=2,
learning_rate=1,
depth=2)
# Fit model
model.fit(train_data, train_labels)
# Get predictions
preds = model.predict(eval_data)
print(preds)