前面对GBDT的算法原理进行了描述,通过前文了解到GBDT是以回归树为基分类器的集成学习模型,既可以做分类,也可以做回归,由于GBDT设计很多CART决策树相关内容,就暂不对其算法流程进行实现,本节就根据具体数据,直接利用Python自带的Sklearn工具包对GBDT进行实现。
数据集采用之前决策树中的红酒数据集,之前的数据集我们做了类别的处理(将连续的数据删除了,且小批量数据进行了合并),这里做同样的处理,将其看为一个多分类问题。
首先依旧是读取数据,并对数据进行检查和预处理,这里就不再赘述,所得数据情况如下:
wine_df = pd.read_csv(‘./winequality-red.csv‘, delimiter=‘;‘, encoding=‘utf-8‘) columns_name = list(wine_df.columns) for name in columns_name: q1, q2, q3 = wine_df[name].quantile([0.25, 0.5, 0.75]) IQR = q3 - q1 lower_cap = q1 - 1.5 * IQR upper_cap = q3 + 1.5 * IQR wine_df[name] = wine_df[name].apply(lambda x: upper_cap if x > upper_cap else (lower_cap if (x < lower_cap) else x))
sns.countplot(wine_df[‘quality‘]) wine_df.describe()
接下来就是先导入使用GBDT所需要用到的工具包:
# 这里采用的是回归,因此是GradientBoostingRegressor,如果是分类则使用GradientBoostingClassifier from sklearn.ensemble import GradientBoostingClassifier from sklearn.metrics import mean_squared_error from sklearn import metrics from sklearn.model_selection import train_test_split import matplotlib.pyplot as plt
然后依旧是对数据进行切分,将数据分为训练集和测试集:
trainX, testX, trainY, testY = train_test_split(wine_df.drop([‘quality‘]), wine_df[‘quality‘], test_size=0.3, random_state=22)
然后就是建立模型:
model = GradientBoostingClssifier()
这里模型就有很多可选参数,用于调整模型,下面进行具体介绍:
首先是Boosting的框架的参数,这里在使用GradientBoostingRegressor和GradientBoostingClassifier是一样的,具体包括:
- n_estimators:弱分类器的最大迭代次数,也就是多少个弱分类器组成,默认值为100。当该值越小时容易欠拟合,太大又会过拟合;
- learning_rate:学习率,这在前面原理部分有进行介绍,默认值为1,较小的learning_rate意味着步长较小,需要更多的分类器才能够达到效果,通常需要与上面n_eatimators结合共同调参;
- subsample:这也是一种正则化的方法,在正则化中有提到过,取值为(0,1],默认值为1,即不进行子采样;
- init:初始化的弱分类器,即f0(x),默认为采用训练样本初始化的分类回归预测,若赋予值需要根据一定的先验知识或者预拟合;
-
loss:即损失函数,在原理篇介绍过相关损失函数,对于分类和回归中损失函数是不相同的:
- 在分类模型中,有对数似然损失函数“deviance”和指数损失函数“exponential”,默认为对数似然损失函数,一般选择默认,因为选择指数损失函数“exponential”又退回到AdaBoost了;
- 在回归模型中,有方差损失“ls”、绝对损失“lad”、Huber损失“huber”和分位数损失“quantile”,默认为均方差损失“ls”,一般来说,数据的噪音不多,采用均方差损失即可,噪音点较多,则推荐使用抗噪能力较强的Huber损失,如果需要对训练集进行分段预测时则采用分位数损失“quantile”;
- alpha:这个参数只存在于GradientBoostingRegressor中,当使用Huber损失和分位数损失时,需要指定分位数的值,默认为0.9,如果噪音数据较多,可以适当降低这个值。
然后就是弱分类器有关的参数值,弱分类器采用的CART回归树,决策树中的相关参数在决策树实现部分已经进行介绍,这里主要对其中一些重要的参数再进行解释:
- max_features:划分树时所用到的最大特征数,默认为None,即使用全部的特征,当取值为“log2”时,划分时使用log2N个特征,如果是“sqrt”和“auto”则分别对应着最多考虑√Nˉ个特征,如果是整数,则代表考虑特征的绝对数,如果是浮点数,则代表考虑总特征数的百分比。一般来说样本总特征数小于50,直接采用50即可,当样本特征数量较大时,再考虑其他特征数;
- max_depth:每个弱分类器的最大深度,默认为不输入,树的深度为3,一般对于数据较少或者特征较少,该值不需要输入,当样本数量和特征数量过于庞大,推荐使用最大深度限制,一般选择10~100;
- min_samples_split:内部节点再划分所需最小的样本数,它限制了子树进一步划分的条件,如果节点的样本数小于min_samples_split则不再进行分裂。默认值为2,若样本数量较大,则推荐增大该值;
- min_samples_leaf:叶子节点最小样本数,该值限定了叶子节点的最小样本数,默认值为1,如果叶子节点样本数量小于该值,则会和兄弟节点一起被剪枝,如果样本量巨大,则推荐增加该值;
- min_weight_fraction_leaf:叶子节点最小样本权重和,该值限制了叶子节点所有样本权重和的最小值,若小于该值,则会和兄弟节点被剪枝,默认值为0,即不考虑权重。当样本分类问题中类别分布偏差较大,则会引入样本权重,需要调整该值;
- max_leaf_nodes:最大叶子节点数,通过限制最大叶子结点数防止过拟合,默认为None。如果加入了限制,则算法会建立在最大叶子节点数内最优的决策树,当样本特征数量过多的话,可以限制该值;
- min_impurity_split:节点划分最小不纯度,这个值限定了决策树的生长,若节点的不纯度(即基尼系数)小于这个值,则该节点不再生长,即为叶子结点,默认值为1e-7,一般不推荐修改。
上述即为模型的主要参数,这里首先全部使用默认值,对样本进行训练:
model.fit(trainX, trainY)
print("模型在训练集上分数为%s"%model.score(trainX, trainY))
pred_prob = model.predict_proba(trainX)
print(‘AUC:‘, metrics.roc_auc_score(np.array(trainY), pred_prob, multi_class=‘ovo‘))
模型在训练集上分数为0.8817106460418562
AUC: 0.9757763363472337
可以看到在训练集上AUC表现还不错,模型的分数但并不高,尝试调整训练参数,首先对于迭代次数和学习率共同进行调整:
param_test1 = {‘n_estimators‘: range(10, 501, 10), ‘learning_rate‘: np.linspace(0.1, 1, 10)} gsearch = GridSearchCV(estimator=GradientBoostingClassifier(learning_rate=1, min_samples_split=2, min_samples_leaf=1, max_depth=3, max_features=None, subsample=0.8, ), param_grid=param_test1, cv=5) gsearch.fit(trainX, trainY) means = gsearch.cv_results_[‘mean_test_score‘] params = gsearch.cv_results_[‘params‘] for i in range(len(means)): print(params[i], means[i]) print(gsearch.best_params_) print(gsearch.best_score_) # {‘learning_rate‘: 0.2, ‘n_estimators‘: 100}
找出最好的n_estimators=100和learning_rate=0.2,将其定下来,带回模型,再次验证:
model = GradientBoostingClassifier(n_estimators=100, learning_rate=0.2,subsample=0.8) model.fit(trainX, trainY) print("模型在训练集上分数为%s"%model.score(trainX, trainY)) pred_prob = model.predict_proba(trainX) print(‘AUC:‘, metrics.roc_auc_score(np.array(trainY), pred_prob, multi_class=‘ovo‘))
模型在训练集上分数为0.9663330300272975
AUC: 0.9977791940084874
可以看到拟合效果已经很好了,再次调整参数,接下来调整弱分类器中的参数,max_depth和min_samples_split:
param_test1 = {‘max_depth‘: range(1, 6, 1), ‘min_samples_split‘: range(1, 101, 10)} gsearch2 = GridSearchCV(estimator=GradientBoostingClassifier(n_estimators=100, learning_rate=0.2, max_features=None, min_samples_leaf=1, subsample=0.8, ), param_grid=param_test1, cv=5) gsearch2.fit(trainX, trainY) means = gsearch2.cv_results_[‘mean_test_score‘] params = gsearch2.cv_results_[‘params‘] for i in range(len(means)): print(params[i], means[i]) print(gsearch2.best_params_) print(gsearch2.best_score_)
找出了树的最大深度为5,由于最小样本划分数量同叶子节点最小样本数量有一定关系,暂时不能定下min_samples_split,将其同min_samples_leaf共同调整:
param_test1 = {‘min_samples_leaf‘: range(1, 101, 10), ‘min_samples_split‘: range(1, 101, 10)} gsearch3 = GridSearchCV(estimator=GradientBoostingClassifier(n_estimators=100, learning_rate=0.2, max_features=None, max_depth=5, subsample=0.8, ), param_grid=param_test1, cv=5) gsearch3.fit(trainX, trainY) means = gsearch3.cv_results_[‘mean_test_score‘] params = gsearch3.cv_results_[‘params‘] for i in range(len(means)): print(params[i], means[i]) print(gsearch3.best_params_) print(gsearch3.best_score_) # {‘min_samples_leaf‘: 21, ‘min_samples_split‘: 41}
可以找出最小样本划分数量21和叶子节点最小数量,我们将这些参数再带回模型:
model = GradientBoostingClassifier(n_estimators=100, learning_rate=0.2, max_depth=5, min_samples_leaf=21, min_samples_split=41, subsample=0.8) model.fit(trainX, trainY) print("模型在训练集上分数为%s"%model.score(trainX, trainY)) pred_prob = model.predict_proba(trainX) print(‘AUC:‘, metrics.roc_auc_score(np.array(trainY), pred_prob, multi_class=‘ovo‘))
模型在训练集上分数为1.0
AUC: 1.0
可以看到在训练集上已经完美拟合了,但为了验证模型,我们需要再分离出一部分用于验证模型的数据集:
validX, tX, validY, tY = train_test_split(testX, testY, test_size=0.2)
然后使用验证集,验证模型:
print("模型在测试集上分数为%s"%metrics.accuracy_score(validY, model.predict(validX))) pred_prob = model.predict_proba(validX) print(‘AUC test:‘, metrics.roc_auc_score(np.array(validY), pred_prob, multi_class=‘ovo‘))
模型在测试集上分数为0.726790450928382
AUC test: 0.8413890948027345
可以看到模型在验证集上表现并不是很好,上面模型存在一定的过拟合问题,继续调整参数,通过调整max_features来提高模型的泛华能力:
param_test1 = {‘max_features‘: range(3, 12, 1)} gsearch4 = GridSearchCV(estimator=GradientBoostingClassifier(n_estimators=100, learning_rate=0.2, min_samples_leaf=21, min_samples_split=41, max_depth=5, subsample=0.8, ), param_grid=param_test1, cv=5) gsearch4.fit(trainX, trainY) means = gsearch4.cv_results_[‘mean_test_score‘] params = gsearch4.cv_results_[‘params‘] for i in range(len(means)): print(params[i], means[i]) print(gsearch4.best_params_) print(gsearch4.best_score_) # {‘max_features‘: 5}
进一步调整subsamples:
param_test1 = {‘subsample‘: np.linspace(0.1, 1, 10)} gsearch5 = GridSearchCV(estimator=GradientBoostingClassifier(n_estimators=100, learning_rate=0.2, min_samples_leaf=21, min_samples_split=41, max_depth=5, max_features=5 ), param_grid=param_test1, cv=5) gsearch5.fit(trainX, trainY) means = gsearch5.cv_results_[‘mean_test_score‘] params = gsearch5.cv_results_[‘params‘] for i in range(len(means)): print(params[i], means[i]) print(gsearch5.best_params_) print(gsearch5.best_score_) # {‘subsample‘: 0.7}
到这里基本主要参数都进行了调整,带回到模型中:
model = GradientBoostingClassifier(n_estimators=100, learning_rate=0.2, max_depth=5, min_samples_leaf=21, min_samples_split=41, max_features=5, subsample=0.7) model.fit(trainX, trainY) print("模型在训练集上分数为%s"%model.score(trainX, trainY)) pred_prob = model.predict_proba(trainX) print(‘AUC:‘, metrics.roc_auc_score(np.array(trainY), pred_prob, multi_class=‘ovo‘))
模型在训练集上分数为0.9990900818926297
AUC: 0.9999992641648271
有略微下降,因为通过提高模型的泛华能力,会增大模型的偏差,然后利用验证集验证模型:
print("模型在测试集上分数为%s"%metrics.accuracy_score(validY, model.predict(validX))) pred_prob = model.predict_proba(validX) print(‘AUC test:‘, metrics.roc_auc_score(np.array(validY), pred_prob, multi_class=‘ovo‘)) 模型在测试集上分数为0.7161803713527851 AUC test: 0.8429467644071055
进一步将模型的迭代次数增加一倍,学习率减半:
model = GradientBoostingClassifier(n_estimators=200, learning_rate=0.1, max_depth=5, min_samples_leaf=21, min_samples_split=41, max_features=5, subsample=0.7) model.fit(trainX, trainY) print("模型在训练集上分数为%s"%model.score(trainX, trainY)) pred_prob = model.predict_proba(trainX) print(‘AUC:‘, metrics.roc_auc_score(np.array(trainY), pred_prob, multi_class=‘ovo‘)) # validX, tX, validY, tY = train_test_split(testX, testY, test_size=0.2) print("模型在测试集上分数为%s"%metrics.accuracy_score(validY, model.predict(validX))) pred_prob = model.predict_proba(validX) print(‘AUC test:‘, metrics.roc_auc_score(np.array(validY), pred_prob, multi_class=‘ovo‘))
模型在训练集上分数为0.9990900818926297
AUC: 1.0
模型在测试集上分数为0.7427055702917772
AUC test: 0.851199242237048
可以看到模型泛化能力有略微增强,可以尝试进一步上述步骤,当迭代次数增加到一定程度,学习率减小到一定程度,模型泛化能力下降,可能是由于步长过小导致拟合和泛化能力下降。
以上就是GBDT的一个实例和参数调整过程,这里使用的是CvGrid网格遍历搜索调参的方法,从结果来看并不理想,可能是由于样本分布的问题,还有就是在进行数据处理的时候采用了replace直接更改了样本的标签,在测试集中这部分数据可能会预测错误。 后面会继续查找原因,调整数据集再次进行训练。
本例仅作为一个调参的学习过程,主要对GBDT中的参数有一个初步的了解,也是刚开始学习调参的方法,参数设置的也比较粗糙,后续会找一些新的数据集进一步对调参进行学习。