机器篇——集成学习(七) 细说 XGBoost 算法

返回主目录

返回集成学习目录

上一章:机器篇——集成学习(六) 细说 GBDT 算法

下一章:机器篇——集成学习(八) 细说 ball49_pred 项目(彩票预测)

本小节,细说 XGBoost 算法,下一小节细说 ball49_pred 项目

论文参考以下链接: XGBoost: A Scalable Tree Boosting System

二. 具体算法

7. XGBoost 算法(Extreme Gradient Boostin, XGBoost)

   XGBoost 是 GB 算法的一种高效实现。XGBoost 中的基学习器除了可以是 CART(gbtree),也可以是线性分类器(gblinear)。

    (1). XGBoost 是一种监督学习算法,它实现了一个称为增强的过程,以产生准确的模型。

          监督学习的常用目标函数,通常包含两个部分:训练误差 + 正则化

                     \LARGE obj(\theta) = L(\theta) + \Omega (\theta)

                \large L(\theta):为损失函数,度量模型预测值和真实值的误差。

                               \LARGE L(\theta) = \sum_{i = 1}^{m} l(y_{i}, \hat{y_{i}})

                               平方损失函数:

                                      \LARGE l(y_{i}, \hat{y_{i}}) = (y_{i} - \hat{y}_{i})^2

                                交叉熵损失函数:

                                      \LARGE l(y_{i}, \hat{y_{i}}) = - \sum_{k}y_{i,k} \log_{2} \hat{y}_{i,k}

                                      \large k 为分类数,特别地,当 \large k = 2 时,为 二分类的交叉熵损失函数:

                                            \LARGE l(y_{i}, \hat{y}_{i}) = - y_{i} \log_{2} \hat{y}_{i} - (1 - \hat{y}_{i})\log_{2} (1 - \hat{y}_{i})

                                 logistic 损失函数:

                                       \LARGE l(y_{i}, \hat{y}_{i}) = y_{i}ln(1 + e^{-\hat{y}_{i}}) + (1 - y_{i})ln(1 + e^{\hat{y}_{i}})

                \large \Omega (\theta):为正则化项,度量模型的复杂度,避免过拟合。常用的正则化有 \large L_{1},L_{2} 正则化。

                               \large L_{1} 正则:\large \Omega (\theta) = \lambda |\theta|

                               \large L_{2} 正则:\large \Omega(\theta) = \lambda |\theta|^{2}

    (2). 模型融合

     一棵 CART 往往过于简单,而无法有效地进行预测,因此更加高效的是使用多个 CART 进行融合,使用集成的方法提升预测效果。

     ①. 假设有两棵回归树,进行融合预测结果,公式为:

                \LARGE \hat{y}_{i} = \sum_{k = 1}^{K} f_{k}(x_{i}) \;\;\;\; f_{k} \in F

               \large K:为树的棵数

              \large f_{k}(x_{i}):为第 \large k 棵树对于输入 \large x_{i} 输出的得分

              \large f_{k}:为相应函数

              \large F:为相应函数空间

     ②. 目标函数:

               \LARGE obj(\theta) = \sum_{i = 1}^{m} l(y_{i}, \hat{y}_{i}) + \sum_{k = 1}^{K} \Omega(f_{k})

                \large l:为损失函数。

    (3). 模型训练

     ①. 假设每次迭代生成一棵树,则训练目标函数可以写成:

                 \LARGE obj(\theta)_{t} = \sum_{i = 1}^{m} l(y_{i,t}, \hat{y}_{i,t}) + \sum_{k = 1}^{t} \Omega(f_{k})

                 \large \hat{y}_{it}:为第 \large t 步迭代的预测值。

     ②. 迭代关系:

              \LARGE \hat{y}_{i,0} = 0

              \LARGE \hat{y}_{i, 1} = f_{1}(x_{i}) = \hat{y}_{i, 0} + f_{1}(x_{i})

              \LARGE ......

              \LARGE \hat{y}_{i,t} = \sum_{i = 1}^{t} f_{k}(x_{i}) = \hat{y}_{i, t - 1} + f_{t}(x_{i})

     ③. 训练目标函数可以写成:

              \LARGE obj_{t} = \sum_{i = 1}^{m} l(y_{i}, \hat{y}_{i, t}) + \sum_{i = 1}^{t} \Omega(f_{i})

                             \LARGE = \sum_{i = 1}^{m} (l(y_{i}, \hat{y}_{i, t - 1} + f_{t}(x_{i})) + \Omega(f_{i}) )+ \sum_{i = 1}^{t - 1} \Omega(f_{i})

            对于第 \large t 步来说,\large \sum_{i = 1}^{t - 1} \Omega(f_{i})  是已知的,则:

              \LARGE obj_{t} = \sum_{i = 1}^{m} l(y_{i}, \hat{y}_{i, t - 1} + f_{t}(x_{i})) + \Omega(f_{t}) + const 

     ④. 如果 \large l 使用平方损失函数,则

              \large obj_{t} = \sum_{i = 1}^{m} (y_{i} - (\hat{y}_{i, t - 1} + f_{t}(x_{i})))^2 + \Omega(f_{t}) + const

                        \large = \sum_{i = 1}^{m} (y_{i} - 2y_{i}(\hat{y}_{i, t - 1} + f_{t}(x_{i})) + (\hat{y}_{i, t -1} + f_{t}(x_{i}))^2) + \Omega(f_{t}) + const

                 \large = \sum_{i = 1}^{m} [2 (\hat{y}_{i, t - 1} - y_{i}) f_{t}(x_{i}) + f_{t}^{2}(x_{i})] + \sum_{i = 1}^{m}(y_{i}^{2} - 2 y_{i} \hat{y}_{i, t - 1} + \hat{y}_{i, t - 1}^{2}) + \Omega(f_{t}) + const

            其中,对于第 \large t 步来说,\large \sum_{i = 1}^{m} (y_{i}^{2} - 2 y_{i} \hat{y}_{i, t - 1} + \hat{y}_{i, t - 1}^{2}) 也是常数。

            则目标函数可化为:

                \large obj_{t} = \sum_{i = 1}^{m} [2 (\hat{y}_{i, t - 1} - y_{i}) f_{t}(x_{i}) + f_{t}^{2}(x_{i})] + \Omega(f_{t}) + const

                \large \hat{y}_{i, t - 1} - y_{i}:为残差。

     ⑤. 泰勒展开

      a. 泰勒展开公式:

           \large f(x) 在 \large x = x_{0} 处有 \large n 阶导数

                   \large f(x) = \frac{f(x_{0})}{0!} + \frac{f^{'}(x_{0})}{1!} (x - x_{0}) + \frac{f^{''}(x_{0})}{2!} (x - x_{0})^{2} + ...... + \frac{f^{(n)}(x_{0})}{n!} (x - x_{0})^{n} + R_{n}(x)

                         \LARGE f(x + \Delta x) \approx f(x) + f^{'}(x) \Delta x + \frac{1}{2} f^{''}(x) \Delta x^2

      b.  目标函数:

                        \LARGE obj_t = \sum_{i = 1}^{m} l(y_{i}, \hat{y}_{i, t - 1} + f_{t}(x_{i})) + \Omega(f_{t}) + const

            令 \large g_{i} = \partial_{\hat{y}_{t - 1}} l(y_{i}, \hat{y}_{t - 1}), \large h_{i} = \partial_{\hat{y}_{t - 1}}^{2} l(y_{i}, \hat{y}_{t - 1}) 则 

                      \large obj_{t} \approx \sum_{i = 1}^{m} (l(y_{i}, \hat{y}_{i, t - 1}) + g_{i}f_{t}(x_{i}) + \frac{1}{2} h_{i} f_{t}^{2}(x_{i})) + \Omega(f_{t}) + const

      c. 其中 \large l(y_{i}, \hat{y}_{i, t - 1}) 和 \large const 为常数项。

          由于目标是让这个目标函数最小化,所以常数项并没有什么用,去掉常数项后,得到的目标函数:

                \LARGE obj_{t} = \sum_{i = 1}^{m} (g_{i} f_{t}(x_{i}) + \frac{1}{2} h_{i} f_{t}^{2}(x_{i})) + \Omega(f_{t})

     ⑥. 对于平方损失函数

                 \LARGE g_{i} = \partial_{\hat{y}_{t - 1}} (\hat{y}_{t - 1} - y_{i})^{2} = 2(\hat{y}_{t - 1} - y_{i})

                 \LARGE h_{i} = \partial_{\hat{y}_{t - 1}}^{2} (\hat{y}_{t - 1} - y_{i})^{2} = \partial_{\hat{y}_{t - 1}} 2(\hat{y}_{t - 1} - y_{i}) = 2

    (4). 模型正则化项

     ①. 如何衡量一棵树的正则化项,为此,首先对 CART 树作另一番定义:

               \LARGE f_{t}(x) = w_{q(x)} \;\;\;w \in R_{T},\;q: R_{d}\rightarrow \{1, 2, ......, T\}

     ②. 定义的解释

           一棵树有 \large T 个叶子节点,这 \large T 个叶子节点的值组成了一个 \large T 维向量 \large w\large q(x) 是一个映射,用来将样本映射成 1 到 \large T 的某个值。也就是把它分到某个叶子节点,\large q(x) 其实就代表了 CART 树的结构。\large w_{q(x)} 自然就是这棵树对样本 \large x 的预测值了。

     ③. XGBoost 的正则化项

               \LARGE \Omega(f_{t}) = rT + \frac{1}{2} \lambda \sum_{j = 1}^{T} w_{j}^{2}

      a. \large r 和 \large \lambda 为 XGBoost 中的两个超参

      b. \large r 越大,表示越希望获得结构简单的树,因为此时对较多叶子节点的树惩罚越大。

      c. \large \lambda 越大,也是希望获得结构越简单的树,因为此时对 \large L_{2} 惩罚越大。

     

    (5). 简化目标函数

     ①. 令 \large G_{j} = \sum_{i \in I_{j}} g_{i} , \;\; H_{j} = \sum_{i \in I{j}} h_{j}

                 \LARGE obj_{t} = \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}] + rT

                                \LARGE = \sum_{j = 1}^{T} [G_{j}w_{j} + \frac{1}{2}(H_{j} + \lambda) w_{j}^{2}] + rT

     ②. 对于第 \large t 棵 CART 树的某一个确定的结构(可用 \large q(x) 表示),所有的 \large G_{j} 和 \large H_{j} 都是正确的。而且 \large obj_{t} 中各叶子节点的值 \large w_{j} 之间是相互独立的。因此,可求出各个叶子节点的最佳值以及此时目标函数的值:

               \LARGE w_{j}^{*} = - \frac{G_{j}}{H_{j} + \lambda}

               \LARGE obj^{*} = - \frac{1}{2} \sum_{j = 1}^{T} \frac{G_{j}^{2}}{H_{j} + \lambda} + rT

               \large w_{j}^{*}:最佳树结构的最佳叶子权重

                         \LARGE \frac{\partial (G_{j} w_{j} + \frac{1}{2} (H_{j} + \lambda) w_{j}^{2})}{\partial w_{j}} = 0

                         \LARGE G_{j} + (H_{j} + \lambda)w_{j} = 0

                         \large w_{j} = - \frac{G_{j}}{H_{j} + \lambda}

                         \large w_{j}^{*} = \frac{1}{h_{j} + \lambda} (- g_{j})

                         \large \frac{1}{h_{j} + \lambda}:为学习率

                         \large -g_{j}:为反向梯度

                         \large w_{j}^{*} 的最佳值就是负的梯度乘以一个权重系数,该系数类似于随机梯度下降中的学习率。当 \large h_{j} 越大时,这个系数越小,也就是学习率越小;\large h_{j} 越大,代表在该点附近梯度变化非常剧烈。

               \large obj^{*}:表示这棵树的结构有多好。值越小,代表的结构越好。也就是说,它是衡量第 \large t 棵 CART 树的结构好坏的标准。该值仅仅是用来衡量结构的好坏,与叶子节点的值可是无关的。\large obj^{*} 和 \large G_{j} 和 \large H_{j} 和 \large T 有关,而它们又和树的结构 \large q(x) 有关,与叶子节点的值没有任何关系。

     

    (6). 最优目标函数

     在实践中,贪婪地种植这棵树

     ①. 从树深度为 0

     ②. 对每棵树的叶子节点尝试添加分裂。目标是添加拆分后的变化。

     ③. 对于每个节点,枚举所有特性。

     ④. 时间复杂度增加树的深度

               \large Gain = -\frac{1}{2} [\frac{G_{L}^{2}}{H_{L} + \lambda} + \frac{G_{R}^{2}}{H_{L} + \lambda} - \frac{(G_{L} + G_{R})^{2}}{H_{L} + H_{R} + \lambda}] + r

               \large \frac{G_{L}^{2}}{H_{L} + \lambda}:为左子树节点的分数

               \large \frac{G_{R}^{2}}{H_{R} + \lambda}:为右子树节点的分数

               \large \frac{(G_{L} + G_{R})^2}{H_{L} + H_{R} + \lambda}:为不分割时树节点的分数

               \large r:为加入新的叶子节点引入的复杂度代价。

               

               Gain = [(-\frac{1}{2} \frac{G_{L}^{2}}{H_{L} + \lambda} + r) + (-\frac{1}{2} \frac{G_{R}^{2}}{H_{R} + \lambda} + r) ]- (-\frac{1}{2} \frac{(G_{L} + G_{R})^{2}}{H_{L} + H_{R} + \lambda} + r)

                          = - \frac{1}{2} [\frac{G_{L}^{2}}{H_{L} + \lambda} + \frac{G_{R}^{2}}{H_{R} + \lambda} - \frac{(G_{L} + G_{R})^{2}}{H_{L} + H_{R} + \lambda}] + r

               Gain 值为分裂后的值减去分裂前的值。

    (7). XGBoost 的小结

     ①. XGBoost 是对 GBDT 进行的优化。GBDT 在优化中只用到了一阶导数信息,而 XGBoost 则对代价函数进行了二阶泰勒展开,并同时用到了一阶和二阶导数,所以 XGBoost 的效率相较于 GBDT 大大的提高,并且 XGBoost 由于可以并行计算一阶和二阶导数,所以 XGBoost 可以并行建树。

     ②. XGBoost 的代价函数里加入了正则项,用于控制模型的复杂度。

     ③. 对于缺失值的处理。对于特征的值有缺失的样本,XGBoost 可以自动学习出它的分裂方向。XGBoost 对于缺失值能预先学习一个默认的分裂方向。

     

    (8). 代码演示

   代码的操作和之前的 随机森林,GBDT 一样的,利用网格搜索,寻找出较优的参数,来进行模型训练,预测。

#!/usr/bin/env python
# _*_ coding:utf-8 _*_
# ============================================
# @Time     : 2020/01/14 21:50
# @Author   : WanDaoYi
# @FileName : xgboost_demo.py
# ============================================

import os
import numpy as np
import pandas as pd
from sklearn import metrics
from sklearn.model_selection import GridSearchCV, train_test_split
from xgboost.sklearn import XGBClassifier

import matplotlib.pyplot as plt
import matplotlib
# 用于解决画图中文乱码
font = {"family": "SimHei"}
matplotlib.rc("font", **font)


class XGBoostDemo(object):

    def __init__(self):
        self.base_path = os.getcwd()
        self.data_path_last = "data_info/car.csv"
        self.data_path = os.path.join(self.base_path, self.data_path_last)
        self.x_train, self.x_val, self.y_train, self.y_val = self.read_data()
        pass

    # 获取数据集
    def read_data(self):
        # 读取数据文件
        data_info = pd.read_csv(self.data_path, encoding="gbk")
        print(data_info.head())
        print("data_info_shape: {}".format(data_info.shape))

        # 划分数据为训练集和验证集
        x = data_info.values[:, : -1]
        y = data_info.values[:, -1]
        x_train, x_val, y_train, y_val = train_test_split(x, y, test_size=0.3)
        print("x_train_shape: {}".format(x_train.shape))
        return x_train, x_val, y_train, y_val
        pass

    def best_estimators_depth(self):
        # np.arange 可以生成 float 类型,range 只能生成 int 类型
        best_param = {'n_estimators': range(10, 201, 5),
                      'max_depth': range(1, 20, 1)
                      }
        best_gsearch = GridSearchCV(estimator=XGBClassifier(learning_rate=0.1,
                                                            gamma=0,
                                                            subsample=0.8,
                                                            colsample_bytree=0.8,
                                                            objective='binary:logistic',
                                                            nthread=4,
                                                            min_child_weight=5,
                                                            seed=27
                                                            ),
                                    param_grid=best_param, scoring='roc_auc', iid=False, cv=10)

        best_gsearch.fit(self.x_train, self.y_train)
        print("best_param:{0}".format(best_gsearch.best_params_))
        print("best_score:{0}".format(best_gsearch.best_score_))
        # best_param: {'max_depth': 7, 'n_estimators': 45}
        # best_score: 0.9627551020408163
        return best_gsearch.best_params_
        pass

    def best_lr_gamma(self):
        # np.arange 可以生成 float 类型,range 只能生成 int 类型
        best_param = {'learning_rate': np.arange(0.1, 1.1, 0.1),
                      'gamma': np.arange(0.1, 5.1, 0.2)
                      }
        best_gsearch = GridSearchCV(estimator=XGBClassifier(n_estimators=45,
                                                            max_depth=7,
                                                            # learning_rate=0.1,
                                                            # gamma=0,
                                                            subsample=0.8,
                                                            colsample_bytree=0.8,
                                                            objective='binary:logistic',
                                                            nthread=4,
                                                            min_child_weight=5,
                                                            seed=27
                                                            ),
                                    param_grid=best_param, scoring='roc_auc', iid=False, cv=10)

        best_gsearch.fit(self.x_train, self.y_train)
        print("best_param:{0}".format(best_gsearch.best_params_))
        print("best_score:{0}".format(best_gsearch.best_score_))
        # best_param: {'gamma': 4.500000000000001, 'learning_rate': 0.6}
        # best_score: 0.9671996144784749
        return best_gsearch.best_params_
        pass

    def best_subsmaple_bytree(self):
        # np.arange 可以生成 float 类型,range 只能生成 int 类型
        # 调整subsample(行),colsample_bytree(列)
        best_param = {'subsample': np.arange(0.1, 1.1, 0.1),
                      'colsample_bytree': np.arange(0.1, 1.1, 0.1)
                      }
        best_gsearch = GridSearchCV(estimator=XGBClassifier(n_estimators=45,
                                                            max_depth=7,
                                                            learning_rate=0.6,
                                                            gamma=4.5,
                                                            # subsample=1.0,
                                                            # colsample_bytree=0.8,
                                                            objective='binary:logistic',
                                                            nthread=4,
                                                            min_child_weight=5,
                                                            seed=27
                                                            ),
                                    param_grid=best_param, scoring='roc_auc', iid=False, cv=10)

        best_gsearch.fit(self.x_train, self.y_train)
        print("best_param:{0}".format(best_gsearch.best_params_))
        print("best_score:{0}".format(best_gsearch.best_score_))
        # best_param: {'colsample_bytree': 0.8, 'subsample': 0.9}
        # best_score: 0.9680867346938775
        return best_gsearch.best_params_
        pass

    def best_nthread_weight(self):
        # np.arange 可以生成 float 类型,range 只能生成 int 类型
        best_param = {'nthread': range(1, 20, 1),
                      'min_child_weight': range(1, 20, 1)
                      }
        best_gsearch = GridSearchCV(estimator=XGBClassifier(n_estimators=45,
                                                            max_depth=7,
                                                            learning_rate=0.1,
                                                            gamma=4.3,
                                                            subsample=0.9,
                                                            colsample_bytree=0.8,
                                                            objective='binary:logistic',
                                                            # nthread=4,
                                                            # min_child_weight=5,
                                                            seed=27
                                                            ),
                                    param_grid=best_param, scoring='roc_auc', iid=False, cv=10)

        best_gsearch.fit(self.x_train, self.y_train)
        print("best_param:{0}".format(best_gsearch.best_params_))
        print("best_score:{0}".format(best_gsearch.best_score_))
        # best_param: {'min_child_weight': 2, 'nthread': 1}
        # best_score: 0.9648386521025202
        return best_gsearch.best_params_
        pass

    def best_seek(self):
        # np.arange 可以生成 float 类型,range 只能生成 int 类型
        best_param = {'seed': range(1, 500, 1)}
        best_gsearch = GridSearchCV(estimator=XGBClassifier(n_estimators=45,
                                                            max_depth=7,
                                                            learning_rate=0.1,
                                                            gamma=4.3,
                                                            subsample=0.9,
                                                            colsample_bytree=0.8,
                                                            nthread=1,
                                                            min_child_weight=2,
                                                            # seed=27,
                                                            objective='binary:logistic'
                                                            ),
                                    param_grid=best_param, scoring='roc_auc', iid=False, cv=10)

        best_gsearch.fit(self.x_train, self.y_train)
        print("best_param:{0}".format(best_gsearch.best_params_))
        print("best_score:{0}".format(best_gsearch.best_score_))
        # best_param: {'seed': 134}
        # best_score: 0.9773406154065825
        return best_gsearch.best_params_
        pass

    # 较好的 模型参数 进行训练
    def best_param_xgboost(self):
        best_model = XGBClassifier(n_estimators=45,
                                   max_depth=7,
                                   learning_rate=0.1,
                                   gamma=4.3,
                                   subsample=0.9,
                                   colsample_bytree=0.8,
                                   nthread=1,
                                   min_child_weight=2,
                                   seed=134,
                                   objective='binary:logistic'
                                   )

        best_model.fit(self.x_train, self.y_train)
        y_pred = best_model.predict(self.x_val)
        acc_score = metrics.accuracy_score(self.y_val, y_pred)
        print("acc_score: {}".format(acc_score))
        print("score: {}".format(best_model.score(self.x_val, self.y_val)))

        print("AUC Score: {}".format(metrics.roc_auc_score(self.y_val, y_pred)))

        y_proba = best_model.predict_proba(self.x_val)
        # 预测为 0 的概率
        y_zero = y_proba[:, 0]
        # 预测为 1 的概率
        y_one = y_proba[:, 1]
        print("AUC Score2: {}".format(metrics.roc_auc_score(self.y_val, y_one)))
        # 得到误判率、命中率、门限
        fpr, tpr, thresholds = metrics.roc_curve(self.y_val, y_one)
        # 计算auc
        roc_auc = metrics.auc(fpr, tpr)
        # 对ROC曲线图正常显示做的参数设定
        # 用来正常显示中文标签, 上面设置过
        # plt.rcParams['font.sans-serif'] = ['SimHei']
        # 用来正常显示负号
        plt.rcParams['axes.unicode_minus'] = False

        plt.plot(fpr, tpr, label='{0}_AUC = {1:.5f}'.format("xgboost", roc_auc))

        plt.title('ROC曲线')
        plt.xlim([-0.05, 1.05])
        plt.ylim([-0.05, 1.05])

        plt.legend(loc='lower right')
        plt.plot([0, 1], [0, 1], 'r--')
        plt.ylabel('命中率: TPR')
        plt.xlabel('误判率: FPR')
        plt.show()

        pass


if __name__ == "__main__":
    demo = XGBoostDemo()

    # demo.best_estimators_depth()
    # demo.best_lr_gamma()
    # demo.best_subsmaple_bytree()
    # demo.best_nthread_weight()
    # demo.best_seek()
    demo.best_param_xgboost()
    pass

在代码中,我们不难发现,xgboost 利用 二阶求导,迭代次数明显下降了很多,而且,精度也提高了。xgboost,在目前的这集成学习里面,算是比较顶级的方法。所以,在生产当中,如果需要用到机器学习中的集成学习方法,我们一般优先选择 xgboost 算法来对业务进行实现。

在接下来下一章,我将用实例,讲解现实生产中,用xgboost 进行 训练和预测。

                 

返回主目录

返回集成学习目录

上一章:机器篇——集成学习(六) 细说 GBDT 算法

下一章:机器篇——集成学习(八) 细说 ball49_pred 项目(彩票预测)

发布了42 篇原创文章 · 获赞 15 · 访问量 2776

猜你喜欢

转载自blog.csdn.net/qq_38299170/article/details/103842265
今日推荐