Loading...
墨滴

blue吖

2021/11/02  阅读:42  主题:默认主题

集成学习——GBDT算法

1.前言

  在上一篇文章中,我们介绍了一种经典的Boosting算法,即AdaBoost算法。本文将介绍Boosting家族的另一个成员,GBDT(Gradient Boosting Decision Tree)算法。回顾一下Adaboost算法,AdaBoost算法是利用前一个弱学习器的误差率来更新训练集的权重,然后利用更新后的训练集来训练下一个弱学习器,这样一轮轮的迭代下去,最后将得到的弱学习器加权组合为强学习器。但是GBDT算法与Adaboost算法有很大的不同。首先从名字上也可以看出,GBDT算法规定了弱学习器为决策树模型,更精确的说,是规定了弱学习器为CART回归树模型(前面的文章中有讲过);除此之外,GBDT也不是像AdaBoost那样每次调整训练集样本的权重来训练下一个弱学习器,而是通过拟合前面模型的负梯度来训练下一个弱学习器,从而达到逐步减小偏差的目的。

  本篇文章后面内容的安排如下:为了推导和理解方便,我们有必要知道加法模型和前向分步算法是什么,在第2节中会介绍。第3节我们主要讨论梯度提升算法。第4节介绍GBDT算法。第5节给出GBDT的sklearn代码实现。

2.加法模型与前向分步算法

  前一篇文章中我们讲到,Boosting算法最后得到的强学习器是由若干个弱学习器加权求和得到的,可以表示为:

其中 为弱学习器, 为其在强学习器中的权重。

给出加法模型的定义:

其中 为基函数, 为基函数的假设空间, 为基函数的系数。

  可以发现,Boosting算法就是一种加法模型。在给定训练数据集和损失函数 的条件下,学习加法模型 就是损失函数最小化的问题,即:

但是,直接求解这个问题比较困难,因为我们并不确定基函数的个数,即 ,要取多少,所以通常采用前向分步算法来求解这个问题。

  前向分步算法的基本思想是:从前往后,每一步只学习一个基函数及其系数。比如在第 步,此时前 步已经得到了 个弱学习器,也就是已经得到了 ,第 步的目标是要得到 ,因为是加法模型,所以 ,因为 已经得到,所以第 步只需要学习基函数 和其系数 ,即第 步优化如下的损失函数:

前向分步算法的算法过程如下:

  • 输入:训练数据集 ,损失函数 ,基函数的假设空间
  • 输出:加法模型

(1)初始化 ;

(2)对 :

  (a)极小化损失函数:

得到 ;

  (b)更新:

(3)得到加法模型:

3.梯度提升(Gradient Boosting)算法

  Gradient Boosting算法是Boosting算法中的一大类,本文要讨论的GBDT算法以及后面文章中要讨论的XGBoost(eXtreme Gradient Boosting)、LightGBM (Light Gradient Boosting Machine)和CatBoost(Categorical Boosting)都属于Gradient Boosting算法。

  梯度提升算法由两部分组成,分别为梯度下降算法和Boosting算法。 这里的梯度下降算法与常规的梯度下降算法不太一样,常规的梯度下降是在参数空间内进行的,目的是得到最优的模型参数;而梯度提升算法中的梯度下降是在函数空间内进行的,目的是直接得到最优的模型。

下面对这段话进行一些解释说明:


常规的梯度下降算法:

  在机器学习任务中,要优化的目标往往是最小化损失函数 ,其中 为要求解的模型参数;比如在逻辑回归算法(前面的文章中讨论过)中,损失函数是交叉熵损失函数,我们的目标就是求解最优的模型参数 ,从而使损失函数达到最小。使用梯度下降算法求解最优模型参数 的过程如下:

(1)确定当前位置 处的损失函数的梯度 ;

(2)用步长 乘以损失函数的梯度,得到当前位置下降的距离,即: ;

(3)判断梯度下降的距离是否小于阈值 ,如果小于,则算法终止,当前的 即为求得的最优参数。否则进入步骤(4);

(4)更新 ,其更新表达式为 。更新后继续转入步骤(1)。

泰勒公式的角度来理解梯度下降:

首先给出迭代公式:

处进行泰勒展开:

移项可得:

在这个式子中 均为向量, 表示二者点乘,当二者方向相同,即夹角为0时,点乘的值最大。这个点乘值代表了从 函数值的上升量,而 处的梯度,这也就说明了当 与梯度方向相同时,函数值上升最快,反之,如果 与负梯度方向相同(与梯度方向相反)时,函数值下降的最快。所以 可以取 ,此时 与负梯度方向相同,函数值下降最快,这也正是我们所需要的,因为我们的目标就是最小化损失函数,当然损失函数值下降的越快越好。于是我们便有了参数的迭代公式:

Boosting算法中的梯度下降:

  由前向分步算法可知,第 步要求解的强学习器可以表示为: ,其中 表示第 步要求的弱学习器, 是已知的,所以求出 也就求得了 。此时要优化的损失函数为:

借鉴常规的梯度下降算法的思想,将 处进行一阶泰勒展开:

可以发现公式中的梯度变量为模型(函数),所以说是在函数空间中的梯度下降。在常规的梯度下降中,为了使损失函数下降最快,到这一步后是直接取负梯度乘上步长来更新参数,但是在Boosting算法中,我们要更新的是模型,而不是参数,所以只能让模型来拟合负梯度,而不是直接取为负梯度。 也就是说,在第 步中, 的求解公式为:

求出 就可以求出 了。


注意: 这里给出用泰勒公式来理解梯度下降的原因是为了下一篇文章讨论XGBoost做准备,因为在GBDT中只对损失函数进行了一阶泰勒展开,只用到了一阶导数信息,而XGBoost对损失函数进行二阶泰勒展开,同时用到了一阶导数信息和二阶导数信息。

下面给出在不同的损失函数下梯度提升算法求解过程。

(1)平方损失函数

的负梯度为:

所以有:

可以发现,当损失函数为平方损失时,负梯度就是残差,此时通过拟合 的残差来求解 ,即:

(2)绝对值损失函数

的负梯度为:

所以有:

的求解:

(3)指数损失函数

的负梯度为:

所以有:

的求解:

4.GBDT(Gradient Boosting Decision Tree)算法

  GBDT算法=梯度提升算法+CART回归树。在GBDT算法中,每一个弱学习器都是CART回归树。

4.1 GBDT回归算法流程

  在回归任务中,GBDT算法一般使用平方损失函数,此时拟合负梯度其实就是拟合残差。

  • 输入:训练数据集 ,损失函数
  • 输出:强学习器

(1)初始化弱学习器

(2)对 :

  (a)对 ,计算:

  (b)利用 拟合一颗CART回归树,得到第 个弱学习器,回归树对应的叶子节点区域为 。其中 为第 棵回归树的叶子节点的个数。

  (c)计算每个叶子节点区域 的最佳输出值:

  (d)更新:

(3)得到强学习器:

  还是以李航老师的《统计学习方法》中的例子来理解GBDT的算法流程,此处使用的是平方损失函数,拟合负梯度其实就是拟合残差。 例题_1_1 例题_1_2 例题_1_3 例题_1_4 例题_1_5

4.2 GBDT分类算法

  GBDT算法是可以做分类任务的,GBDT分类算法所使用的弱学习器仍然是CART回归树。但是由于分类任务的标签值是离散值,而GBDT使用的又是回归树,无法直接输出样本的类别,所以需要进行一些处理。GBDT用于分类任务的思想与逻辑回归算法(前面的文章中讲过)中相似,用的是类别的预测概率值和真实标签值之间的差来拟合损失,使用的是交叉熵损失函数。

  由于篇幅原因,GBDT分类算法的推导与算法流程这里不再详述,有兴趣学习的话推荐去B站https://www.bilibili.com/video/BV1z44y1r7ta?spm_id_from=333.999.0.0,我个人觉得这个up主讲的很好,推导也很全。

5.GBDT回归算法的sklearn实现

  本文以波士顿房价预测任务为例来给出GBDT回归算法的sklearn实现过程。波士顿房价数据集介绍可参考https://www.cnblogs.com/wjunneng/p/7323862.html

导入要用的包:

import numpy as np
import pandas as pd
import seaborn as sns
import matplotlib.pyplot as plt
from sklearn.ensemble import GradientBoostingRegressor
from sklearn.datasets import load_boston
from sklearn.model_selection import train_test_split
from sklearn.metrics import mean_squared_error
from sklearn.inspection import permutation_importance
import matplotlib
import matplotlib.pyplot as plt
%matplotlib inline

加载数据集并划分训练集与测试集:

# 加载数据集
boston = load_boston()
# 获取特征值和目标值
X,y = boston.data,boston.target
# 获取特征名称
feature_name = boston.feature_names
# 划分数据集
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=0)

设置GBDT回归模型的参数:

'''注意:我使用的scikit-learn的版本为0.23.1,版本不同损失函数的名称设置可能不一样,
    如果版本不一样,需要自己去看scikit-learn的使用文档。
'''

params = {'n_estimators'500# 弱分类器的个数
          'max_depth'3,       # 弱分类器(CART回归树)的最大深度
          'min_samples_split'5# 分裂内部节点所需的最小样本数
          'learning_rate'0.05,  # 学习率
          'loss''ls'}           # 损失函数:均方误差损失函数

模型实例化并训练:

GBDTreg = GradientBoostingRegressor(**params)
GBDTreg.fit(X_train, y_train)

预测输出并可视化:

y_predict = GBDTreg.predict(X_test)

# 为了正常显示中文
import matplotlib as mpl
mpl.rcParams['font.sans-serif'] = ['KaiTi''SimHei''FangSong']  # 汉字字体,优先使用楷体,如果找不到楷体,则使用黑体
mpl.rcParams['font.size'] = 12  # 字体大小

plt.plot(y_predict, label='预测房价')
plt.plot(y_test, label='真实房价')
plt.legend()
图1 波士顿房价预测数据可视化
图1 波士顿房价预测数据可视化

计算预测房价与真实房价之间的均方误差:

# 计算预测房价与真实房价之间的均方误差
mse = mean_squared_error(y_test, y_predict)
print("The mean squared error (MSE) on test set: {:.4f}".format(mse))

可视化随着弱学习器的增多时偏差的变化:

test_score = np.zeros((params['n_estimators'],), dtype=np.float64)
for i, y_pred in enumerate(GBDTreg.staged_predict(X_test)):
    test_score[i] = GBDTreg.loss_(y_test, y_pred)

fig = plt.figure(figsize=(66))
plt.subplot(111)
plt.title('偏差随弱学习器个数的变化')
plt.plot(np.arange(params['n_estimators']) + 1, GBDTreg.train_score_, 'b-',
         label='训练集偏差')
plt.plot(np.arange(params['n_estimators']) + 1, test_score, 'r-',
         label='测试集偏差')
plt.legend(loc='upper right')
plt.xlabel('弱学习器的个数')
plt.ylabel('偏差')
fig.tight_layout()
图2 偏差随弱学习器个数的变化
图2 偏差随弱学习器个数的变化

可视化特征的重要性:

feature_importance = GBDTreg.feature_importances_
sorted_idx = np.argsort(feature_importance)
pos = np.arange(sorted_idx.shape[0]) + .5
fig = plt.figure(figsize=(126))
plt.subplot(121)
plt.barh(pos, feature_importance[sorted_idx], align='center')
plt.yticks(pos, np.array(boston.feature_names)[sorted_idx])
plt.title('Feature Importance (MDI)')

result = permutation_importance(GBDTreg, X_test, y_test, n_repeats=10,
                                random_state=42, n_jobs=2)
sorted_idx = result.importances_mean.argsort()
plt.subplot(122)
plt.boxplot(result.importances[sorted_idx].T,
            vert=False, labels=np.array(boston.feature_names)[sorted_idx])
plt.title("Permutation Importance (test set)")
fig.tight_layout()
plt.show()
图3 特征重要性可视化
图3 特征重要性可视化

上图给出了两种特征重要性的计算方式,一种是GBDT自带的,在回归任务中,一个特征的重要性是通过利用此特征分裂结点后均方误差的减少量来衡量的,均方误差减少的越多,特征就越重要,在分类任务中,则是通过基尼指数来衡量。另一种是sklearn库中的,叫做排列重要性,它是通过打乱特征的取值后,根据模型预测的准确率衰减程度来衡量的,打乱特征取值后,如果模型准确率下降越多说明此特征越重要。

6.参考资料

1.李航《统计学习方法》
2.https://www.cnblogs.com/pinard/p/6140514.html
3.https://zhuanlan.zhihu.com/p/86354141
4.https://www.jianshu.com/p/005a4e6ac775
5.https://www.zhihu.com/question/54626685
6.https://davidrosenberg.github.io/mlcourse/Labs/9-GBM-Notes.pdf
7.https://scikit-learn.org/0.23/auto_examples/ensemble/plot_gradient_boosting_regression.html

blue吖

2021/11/02  阅读:42  主题:默认主题

作者介绍

blue吖