XGBOOST numpy实现代码解读

133 阅读3分钟

一、原理

​ XGBOOST推导请详见《机器学习公式推导与代码实现》(鲁伟,2022年)第12章P162~P168,书中有该算法的Numpy实现,本文对照公式推导部分和算法代码实现部分做注解,以方便理解。算法实现部分一个重要实现是把每条数据的标签部分分成了真实标签和预测标签,并且对二者进行one-hot编码,如下面代码中 " y = cat_label_convert(y)",如果分类有3类,就有3列等等,其他有关标签的一阶导数和二阶导数部分均是在one-hot编码上进行。

### xgboost类定义中的拟合方法
    def fit(self, X, y):
        y = cat_label_convert(y)  #标签部分one-hot编码;
        y_pred = np.zeros(np.shape(y))
        # 拟合每一棵树后进行结果累加;
        for i in range(self.n_estimators):
            tree = self.trees[i]
            y_true_pred = np.concatenate((y, y_pred), axis=1)  #将真实标签和预测标签组合以便于一阶二阶导数计算;
            tree.fit(X, y_true_pred)     # boosting 过程 对每棵树拟合;
            iter_pred = tree.predict(X)  #iter_pred是中间值;
            y_pred -= np.multiply(self.learning_rate, iter_pred)   # 设置缩减率,防止过拟合;

二、代码解读

import numpy as np
from cart import TreeNode, BinaryDecisionTree
from sklearn.model_selection import train_test_split
from sklearn.metrics import accuracy_score
from utils import cat_label_convert

### XGBoost单棵树类
class XGBoost_Single_Tree(BinaryDecisionTree):
    # 标签部分分离方法,分离真实值和预测值,以用于gain计算结点损失值;
    def node_split(self, y):
        # 中间特征所在列
        feature = int(np.shape(y)[1]/2)
        # 左子树为真实值,右子树为预测值
        y_true, y_pred = y[:, :feature], y[:, feature:]
        return y_true, y_pred

    # 结点的损失值计算方法,作为树分裂增益计算的基础;根据式(12-20)
    def gain(self, y, y_pred):
        # 梯度计算
        Gradient = np.power((y * self.loss.gradient(y, y_pred)).sum(), 2)  
        # 由于采用cat_label_convert,对于y_pred, 预测结果有多少类,y_pred 就有多少列,gradient结果相应有多少类,因此要求和,y * self.loss.gradient(y, y_pred) ,y起过滤作用,对没有标签值也有梯度的赋值为0; 
        #二级导数矩阵计算
        Hessian = self.loss.hess(y, y_pred).sum()
        return 0.5 * (Gradient / Hessian)   #作为一个简单的逻辑实现,并没有加入参数λ和γ,此外本式是为了计算增益的变形式;

    # 树分裂增益计算
    # 式(12-22)
    def gain_xgb(self, y, y1, y2):
        # 结点分裂的增益计算
        y_true, y_pred = self.node_split(y)
        y1, y1_pred = self.node_split(y1)
        y2, y2_pred = self.node_split(y2)
        true_gain = self.gain(y1, y1_pred)
        false_gain = self.gain(y2, y2_pred)
        gain = self.gain(y_true, y_pred)
        return true_gain + false_gain - gain

    # 计算叶子结点最优权重,根据式12-18
    def leaf_weight(self, y):
        y_true, y_pred = self.node_split(y)
        # 梯度计算
        gradient = np.sum(y_true * self.loss.gradient(y_true, y_pred), axis=0)  #按列求和
        # y_true 过滤作用
        # 二级导数矩阵计算
        hessian = np.sum(self.loss.hess(y_true, y_pred), axis=0) #按列求和
        # 叶子结点得分
        leaf_weight =  gradient / hessian
        return leaf_weight   # 得到一个np.array数组,是每个类上面的leaf_weight得分;

    # 树拟合方法
    def fit(self, X, y):
        self.impurity_calculation = self.gain_xgb   #设置基尼不纯度计算方法;
        self._leaf_value_calculation = self.leaf_weight   # 设置叶子权重计算方法;
        super(XGBoost_Single_Tree, self).fit(X, y)  # BinaryDecisionTree 派生;
        
        
        
### 分类损失函数定义
# 定义Sigmoid类
class Sigmoid:
    def __call__(self, x):
        return 1 / (1 + np.exp(-x))

    def gradient(self, x):
        return self.__call__(x) * (1 - self.__call__(x))

# 定义Logit损失,这里应用了logit损失函数的形式,通过sigmoid函数将y_pred映射到0-1区间,然后再计算损失;
class LogisticLoss:
    def __init__(self):
        sigmoid = Sigmoid()
        self._func = sigmoid
        self._grad = sigmoid.gradient
    
    # 定义损失函数形式
    def loss(self, y, y_pred):
        y_pred = np.clip(y_pred, 1e-15, 1 - 1e-15)   # 限定y_pred;
        p = self._func(y_pred)
        return y * np.log(p) + (1 - y) * np.log(1 - p)  #logit损失函数形式;

    # 定义一阶梯度 logit损失函数对y_pred的一阶梯度;p = self._func(y_pred)
    def gradient(self, y, y_pred):
        p = self._func(y_pred)
        return -(y - p)

    # 定义二阶梯度  logit损失函数对y_pred的二阶梯度,p = self._func(y_pred)
    def hess(self, y, y_pred):
        p = self._func(y_pred)
        return p * (1 - p)