机器学习算法原理实现——xgboost,核心是加入了正则化和损失函数二阶泰勒展开

 

先看总的图:

本质上就是在传统gbdt的决策树基础上加入了正则化防止过拟合,以及为了让损失函数求解更方便,加入了泰勒展开,这样计算损失函数更方便了(除了决策树代码有差别,其他都是gbdt一样,本文仅实现xgboost的决策树)。如下:

 

再解释各个步骤:

 

 

 

 

 

。。。

补充下:

 

让gpt来汇总下:

 

 

好了,我们直接写下实现代码:

 

import numpy as np

class XGBoostDecisionTree:
    def __init__(self, max_depth=3):
        self.max_depth = max_depth
        
    def fit(self, X, y):
        # X是特征数据,y的第一列是伪残差,第二列是hessians
        self.tree = self._grow_tree(X, y, depth=0)
        
    def _gain(self, gradients, hessians):
        return np.square(gradients.sum()) / (hessians.sum() + 1e-9)
    
    def _split(self, X, y):
        best_gain = 0
        best_split = None
        best_left_y, best_right_y = None, None
        gradients, hessians = y[:, 0], y[:, 1]
        total_gain = self._gain(gradients, hessians)
        for i in range(X.shape[1]):
            thresholds = np.unique(X[:, i])
            for thresh in thresholds:
                left_mask = X[:, i] < thresh
                right_mask = ~left_mask
                left_gradients = gradients[left_mask]
                right_gradients = gradients[right_mask]
                left_hessians = hessians[left_mask]
                right_hessians = hessians[right_mask]
                left_gain = self._gain(left_gradients, left_hessians)
                right_gain = self._gain(right_gradients, right_hessians)
                gain = left_gain + right_gain - total_gain
                if gain > best_gain:
                    best_gain = gain
                    best_split = (i, thresh)
                    best_left_y = y[left_mask]
                    best_right_y = y[right_mask]
        return best_gain, best_split, best_left_y, best_right_y
    
    def _grow_tree(self, X, y, depth):
        gradients, hessians = y[:, 0], y[:, 1]
        predicted_value = -gradients.sum() / (hessians.sum() + 1e-9)
        node = {"value": predicted_value}
        if depth < self.max_depth:
            gain, split, left_y, right_y = self._split(X, y)
            if gain > 0:  # 只有当增益大于0时我们才真正地进行分裂
                feature_idx, threshold = split
                left_mask = X[:, feature_idx] < threshold
                node["feature_idx"] = feature_idx
                node["threshold"] = threshold
                node["left"] = self._grow_tree(X[left_mask], left_y, depth+1)
                node["right"] = self._grow_tree(X[~left_mask], right_y, depth+1)
        return node
    
    def predict(self, X):
        return np.array([self._predict_single(x, self.tree) for x in X])
    
    def _predict_single(self, x, node):
        if "feature_idx" in node:
            if x[node["feature_idx"]] < node["threshold"]:
                return self._predict_single(x, node["left"])
            else:
                return self._predict_single(x, node["right"])
        return node["value"]

import matplotlib.pyplot as plt

X = np.linspace(0, 10, 100)[:, np.newaxis]
y_true = np.sin(X).ravel() + np.random.normal(0, 0.1, X.shape[0])
base_pred = np.ones_like(y_true) * y_true.mean()
pseudo_residuals = -2 * (y_true - base_pred)
y = np.c_[pseudo_residuals, np.ones_like(y_true)]

model = XGBoostDecisionTree(max_depth=8)
model.fit(X, y)
predictions = model.predict(X) + base_pred  # 加上基学习器的预测

plt.scatter(X, y_true, label='True values', color='b')
plt.plot(X, predictions, label='XGBoost Decision Tree', color='r')
plt.legend()
plt.title('XGBoost Decision Tree Regression')
plt.xlabel('X')
plt.ylabel('y')
plt.show()

  

效果图:

 代码中有几个细节值得注意:

hession矩阵就是二阶导数,就是1,因为损失函数二阶求导(泰勒展开后的)就是1!

 

 

预测值为什么是这个呢?predicted_value = -gradients.sum() / (hessians.sum() + 1e-9)
 
 

 

此外,代码里对于增益的变化计算(从后面回答看,严格说还有除2):

 

 

 

 

 

 所以说,实际上这里是Loss函数变化的值来模拟的gain! 变化更大了,loss就更小了!说明分裂效果越好!

 

好了,至于其他代码,和决策树算法步骤一样,可以参考之前的文章,就不再说了。

 

 
为什么损失函数是二阶泰勒展开而不是三阶?  看看gpt的回答:

使用二阶泰勒展开(而不是三阶或更高阶)来近似损失函数在机器学习,特别是在GBDT和XGBoost中,有几个原因:

  1. 计算效率:二阶泰勒展开为我们提供了一个关于损失的准确和有用的近似,同时保持了计算的简洁性。随着展开的阶数的增加,需要的计算量也会大幅增加。

  2. 准确性 vs. 复杂性的权衡:虽然更高阶的泰勒展开可以提供更准确的近似,但二阶展开通常足够用于损失的优化,尤其是在树的分裂点。超过二阶的项可能只会为优化过程带来微小的改进,但计算的复杂性会大幅增加。

  3. 二阶导数的物理含义:在优化的上下文中,二阶导数(即Hessian)提供了关于函数曲率的信息,这有助于我们了解步长应该是多大。这与牛顿方法的思想相一致,其中二阶导数被用来找到函数的最小值。对于大多数损失函数,二阶信息就足够了。

  4. 稳定性:引入更高阶的导数可能会导致算法在某些情况下的不稳定性,尤其是当高阶导数的值变化很大或接近零时。

  5. 经验上的成功:二阶泰勒展开在实践中被证明是非常成功的,特别是在XGBoost这样的算法中。这也给了我们一个实用的理由坚持使用它,而不是更高阶的展开。

总的来说,二阶泰勒展开为我们提供了一个既简洁又足够准确的损失近似,使得其在实际应用中成为了一个非常好的选择。

 

最后的疑点:

xgboost绕了这么大的圈,最后不就和adaboost是一样的原理吗,无非就是使用了残差,而adaboost直接是函数值。再就是xgboost再加入了所谓树的正则化防止过拟合,从上面各种推导可以看出来。

所以我觉得xgboost的核心优势还是加入了正则化,像adaboost的话,可能因为一个异常值而导致整个预测效果有偏差,而加入正则化以后可以使得这种偏差那边小。

其他工程化方法还包括:由于它可在特征粒度上并行计算, 结构风险和工程实现都做了很多优化, 泛化, 性能和扩展性都比GBDT要好。

 

posted @ 2023-09-16 22:40  bonelee  阅读(372)  评论(0编辑  收藏  举报