XGBoost原理解析

828 阅读4分钟


本文作者】刘英涛

【作者简介】达观数据推荐算法工程师,负责达观数据个性化推荐系统的研发与优化。


XGBoost的全称是 eXtremeGradient Boosting,2014年2月诞生的专注于梯度提升算法的机器学习函数库,作者为华盛顿大学研究机器学习的大牛——陈天奇。他在研究中深深的体会到现有库的计算速度和精度问题,为此而着手搭建完成 xgboost 项目。xgboost问世后,因其优良的学习效果以及高效的训练速度而获得广泛的关注,并在各种算法大赛上大放光彩。


1. CART

CART(回归树, regressiontree)是xgboost最基本的组成部分。其根据训练特征及训练数据构建分类树,判定每条数据的预测结果。其中构建树使用gini指数计算增益,即进行构建树的特征选取,gini指数公式如式(1), gini指数计算增益公式如式(2):

Gini(D)=\sum_{k=1}^{K}{p_{k}(1-p_{k})} (1)

p_{k} 表示数据集 D 中类别 k 的概率, K 表示类别个数。

注:此处的 k 表示分类类别。

Gini(D,A)=\frac{\left| D_{1} \right|}{\left| D \right|}Gini(D_{1})+\frac{\left| D_{2} \right|}{\left| D \right|}Gini(D_{2}) (2)

D 表示整个数据集, D_{1}D_{2} 分别表示数据集中特征为 A 的数据集和特征非 A 的数据集, Gini(D_{1}) 表示特征为 A 的数据集的gini指数。


以是否打网球为例(只是举个栗子):

Gini(D,A_{1}="晴")=0.4

Gini(D,A_{2}="高")=0.305

Gini(D,A_{2}="适中")=0.3

Gini(D,A_{2}="低")=0.419

Gini(D,A_{1}="晴")=\frac{5}{10}\times(\frac{1}{5}\times\frac{4}{5}\times2)+\frac{5}{10}\times(\frac{3}{5}\times\frac{2}{5}\times2)=0.4

其中, Gini(D,A_{2}="适中") 最小,所以构造树首先使用温度适中。然后分别在左右子树中查找构造树的下一个条件。


本例中,使用温度适中拆分后,是子树刚好类别全为是,即温度适中时去打网球的概率为1。


2. Boosting tree

一个CART往往过于简单,并不能有效地做出预测,为此,采用更进一步的模型boosting tree,利用多棵树来进行组合预测。具体算法如下:


输入:训练集 T=\left\{ (x_1,y_1 ),(x_2,y_2 ),…(x_n,y_n ) \right\}

输出:提升树 f_{M}(x)

步骤:

(1) 初始化 f_{0}(x)=0

(2) 对 m=1,2,…,M

(a) 计算残差 r_{mi}=y_i-f_{(m-1)} (x_i ),i=1,2,…,n

(b) 拟合残差 r_{mi} 学习一个回归树,得到 T(x:θ_m )

(c) 更新 f_m (x)=f_{(m-1)} (x)+T(x:θ_m )

(3) 得到回归提升树: f_M (x)=\Sigma_{m=1}^{M}T(x:θ_m )

例子详见后面代码部分。


3. xgboost

首先,定义一个目标函数:

Obj(t)=\sum_{i=1}^{n}{L(y_i, \hat y^{t-1}+f_t (x_i))+Ω(f_t )+constant } (3)

constant 为一个常数,正则项 Ω(f_t ) 如下:

Ω(f_t )=γT+\frac{1}{2} \lambda\sum_{i=1}^{T}{w_{j}}^{2} (4)

其中, T 表示叶子节点数, w_j 表示第 j 个叶子节点的权重。

例如下图,叶子节点数为3,每个叶子节点的权重分别为2,0.1,-1,正则项计算见图:

利用泰勒展开式 f(x+Δx)≈f(x)+f'^{(x)}Δx+\frac{1}{2} f''(x)Δx^2 ,对式(3)进行展开:

Obj(t)≈∑_{i=1}^n[L(y_i,\hat y^{t-1})+g_i f_t (x_i )+\frac{1}{2} h_i f_t^2 (x_i )]+Ω(f_t )+constant (5)

其中, g_{i} 表示 L(y_i,\hat y^{t-1} )y ̂^{(t-1)} 的一阶导数, h_{i} 表示 L(y_i,\hat y^{t-1})\hat y^{t-1} 的二阶导数。


L(y_i,\hat y^{t-1}) 为真实值与前一个函数计算所得残差是已知的(我们都是在已知前一个树的情况下计算下一颗树的),同时,在同一个叶子节点上的数的函数值是相同的,可以做合并,于是:

通过对求导等于0,可以得到:

w_{j}=-\frac{G_{j}}{H_{j}+\lambda} (6)

w_{j} 带入得目标函数的简化公式如下:

Obj(t)=-\frac{1}{2} \sum_{j=1}^{T}{\frac{G_{j}}{H_{j}+\lambda}}+γT+C (7)

目标函数简化后,可以看到xgboost的目标函数是可以自定义的,计算时只是用到了它的一阶导和二阶导。得到简化公式后,下一步针对选择的特征计算其所带来的增益,从而选取合适的分裂特征。

gain(ϕ)=gain(before)-gain(after)

=\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}]-γ


提升树例子代码:

# !/usr/bin/env python
# -*- coding: utf-8 -*-

# 目标函数为真实值与预测值的差的平方和

import math

# 数据集,只包含两列
test_list = [[1,5.56], [2,5.7], [3,5.81], [4,6.4], [5,6.8],\
            [6,7.05], [7,7.9], [8,8.7], [9,9], [10,9.05]]

step = 1 #eta
# 起始拆分点
init = 1.5
# 最大拆分次数
max_times = 10
# 允许的最大误差
threshold = 1.0e-3

def train_loss(t_list):
    sum = 0
    for fea in t_list:
        sum += fea[1]
    avg = sum * 1.0 / len(t_list)
    sum_pow = 0
    for fea in t_list:
        sum_pow = math.pow((fea[1]-avg), 2)
    return sum_pow, avg

def boosting(data_list):
    ret_dict = {}
    split_num = init
    while split_num < data_list[-1][0]:
        pos = 0
        for idx, data in enumerate(data_list):
            if data[0] > split_num:
                pos = idx
                break
        if pos > 0:
            l_train_loss, l_avg = train_loss(data_list[:pos])
            r_train_loss, r_avg = train_loss(data_list[pos:])
            ret_dict[split_num] = [pos, l_train_loss+r_train_loss, l_avg, r_avg] 
        split_num += step
    return ret_dict

def main():
    ret_list = []
    data_list = sorted(test_list, key=lambda x:x[0])

    time_num = 0
    while True:
        time_num += 1
        print 'before split:',data_list
        ret_dict = boosting(data_list)
        t_list = sorted(ret_dict.items(), key=lambda x:x[1][1])
        print 'split node:',t_list[0]
        ret_list.append([t_list[0][0], t_list[0][1][1]])
        if ret_list[-1][1] < threshold or time_num > max_times:
            break
        for idx, data in enumerate(data_list):
            if idx < t_list[0][1][0]:
                data[1] -= t_list[0][1][2]
            else:
                data[1] -= t_list[0][1][3]
        print 'after split:',data_list
    print 'split node and loss:'
    print '\n'.join(["%s\t%s" %(str(data[0]), str(data[1])) for data in ret_list])


if __name__ == '__main__':
    main()


编者注

上文选自由达观研究院编写而成的《达观数据技术实践特刊》,该书集合了当下最热门的人工智能领域自然语言处理、个性化推荐、垂直搜索引擎三大方向的技术实践总结,融合了达观技术团队在服务华为、中兴、招行、平安、京东云等不同行业上百家企业后的技术感悟,是国内第一本系统介绍NLP、深度学习等AI技术实践应用的电子刊,欢迎各位技术爱好者前往下载