大家好,我是小白~
今儿是基础算法的第6天,我们来聊XGBoost~
XGBoost 是一种非常强大的机器学习算法,常常被用在比赛和实际问题中,特别是预测类问题。虽然名字听起来有点复杂,但其实可以通过简单的例子来理解。
XGBoost 是什么?
XGBoost 是「Extreme Gradient Boosting」的缩写,属于「提升树(Boosting Tree)」的一种。它的核心思想是通过逐步修正预测错误来提升整体模型的准确率。
可以简单理解为:多个弱小的模型(决策树)组合起来形成一个强大的模型。每个小模型在不同的地方犯错,但通过不断学习前面的错误,最终的模型就会越来越好。
举个例子
假设你是一个老师,给学生评分。但是你发现一次考试你评分不太准,总是有一些学生的分数给高了,另一些学生的分数给低了。那么怎么办呢?
XGBoost 的思路是这样的:
第一步:你先用一个简单的方法给所有学生打分(比如看他们答对了几个问题),这就是第一个弱模型。 第二步:你发现这个评分有一些偏差,比如某些聪明的学生得分太低了,某些学生虽然答题对了但思路不对却得分高了。 第三步:你再根据这些偏差构建第二个评分标准,专门纠正第一个模型的错误。这时我们就有了第二个弱模型。 第四步:你继续这么做,不断纠正前面的模型,直到你的评分结果非常准确。
XGBoost 就是通过不断构建新的弱模型(小决策树)来修正前面模型的错误,最后将所有的模型组合起来,形成一个强大的模型。
XGBoost 怎么修正错误?
XGBoost 的特别之处在于,它使用了一种叫梯度提升(Gradient Boosting)的方法来修正模型错误。
用简单话说,XGBoost 会看模型预测和实际结果之间的差距(也就是残差),然后它会想办法去「追着」这些差距,专门针对差距大的地方做出改进。
如果预测结果太低,XGBoost 就会提升这个地方的预测值。 如果预测结果太高,它就会降低这里的预测值。 这个过程类似于我们爬山时,沿着坡度(误差)不断调整方向,最终找到最好的路径。
XGBoost 的特点
XGBoost 之所以受欢迎,主要有以下几个特点:
速度快:XGBoost 经过了很多优化,计算速度非常快,能够在大数据集上高效运行。 准确率高:它的提升方法可以大大提高模型的预测准确度。 防止过拟合:它有一些技巧,能够防止模型在训练数据上表现很好,但在新数据上效果变差。
实际例子
假如你是某个公司的市场部主管,想预测每个客户购买某种产品的可能性。你有客户的历史行为、年龄、性别等数据。你可以用 XGBoost 来分析这些数据,并训练模型去预测哪个客户最有可能购买你的产品。通过不断优化,你的模型会越来越准,从而帮助你更好地制定市场策略。
总结来说,XGBoost 是一个通过不断修正错误,组合多个弱模型(决策树)来形成强大模型的机器学习算法。
原理部分
梯度提升公式
XGBoost 是一种基于梯度提升(Gradient Boosting)的决策树算法。在这里,我们详细推导 XGBoost 的核心公式,并解释其逻辑。
目标函数
XGBoost 的目标是最小化一个损失函数 ,并带有正则化项 来控制模型复杂度。总体目标函数定义为:
其中:
是损失函数,用来衡量真实值 和预测值 之间的差距。常见的选择是平方误差损失:。 是正则化项,用来控制决策树的复杂度,防止过拟合。常见形式是:,其中 是树的叶节点数, 是每个叶子节点的权重, 和 是超参数。
模型构建
XGBoost 的预测值 是通过一系列的弱学习器(决策树)累加得到的:
在每一步,我们通过拟合当前的残差来更新模型,即学习如何更好地预测误差。
二阶泰勒展开
为了优化目标函数,XGBoost 使用了二阶泰勒展开对损失函数进行近似:
其中:
是损失函数对预测值的梯度。 是损失函数的二阶导数(Hessian)。
叶子节点的分裂与权重优化
我们需要决定如何分裂节点,以及如何分配叶子节点的权重。对于每个叶子节点,我们可以通过最小化以下公式来得到最优的权重:
其中, 是叶子节点 包含的样本集合。
而分裂节点的增益(Gain)是:
增益决定了分裂是否值得,若增益较大则进行分裂。
代码实现
接下来,我们基于上述推导,手动实现一个简单的 XGBoost 算法~
import numpy as np
import matplotlib.pyplot as plt
from sklearn.datasets import make_regression
from sklearn.model_selection import train_test_split
# 生成虚拟数据集
X, y = make_regression(n_samples=300, n_features=1, noise=0.1, random_state=42)
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)
# 定义一个简单的回归树类
class SimpleTree:
def __init__(self, depth=3, min_samples_split=10, lambda_reg=1):
self.depth = depth
self.min_samples_split = min_samples_split
self.lambda_reg = lambda_reg
self.tree = None
# 拟合树
def fit(self, X, g, h):
self.tree = self._build_tree(X, g, h, depth=0)
def _build_tree(self, X, g, h, depth):
if depth == self.depth or len(X) < self.min_samples_split:
w = -np.sum(g) / (np.sum(h) + self.lambda_reg) # 最优叶子节点权重
return {"leaf": True, "weight": w}
best_gain = 0
best_split = None
best_left = None
best_right = None
for feature in range(X.shape[1]):
thresholds = np.unique(X[:, feature])
for threshold in thresholds:
left_idx = X[:, feature] <= threshold
right_idx = X[:, feature] > threshold
if sum(left_idx) == 0 or sum(right_idx) == 0:
continue
g_left, g_right = g[left_idx], g[right_idx]
h_left, h_right = h[left_idx], h[right_idx]
gain = self._calc_gain(g, h, g_left, g_right, h_left, h_right)
if gain > best_gain:
best_gain = gain
best_split = (feature, threshold)
best_left = left_idx
best_right = right_idx
if best_split is None:
w = -np.sum(g) / (np.sum(h) + self.lambda_reg)
return {"leaf": True, "weight": w}
left_tree = self._build_tree(X[best_left], g[best_left], h[best_left], depth + 1)
right_tree = self._build_tree(X[best_right], g[best_right], h[best_right], depth + 1)
return {"leaf": False, "feature": best_split[0], "threshold": best_split[1], "left": left_tree, "right": right_tree}
def _calc_gain(self, g, h, g_left, g_right, h_left, h_right):
gain = 0.5 * (
(np.sum(g_left) ** 2 / (np.sum(h_left) + self.lambda_reg)) +
(np.sum(g_right) ** 2 / (np.sum(h_right) + self.lambda_reg)) -
(np.sum(g) ** 2 / (np.sum(h) + self.lambda_reg))
)
return gain
# 预测
def predict(self, X):
return np.array([self._predict_row(x, self.tree) for x in X])
def _predict_row(self, x, tree):
if tree["leaf"]:
return tree["weight"]
feature, threshold = tree["feature"], tree["threshold"]
if x[feature] <= threshold:
return self._predict_row(x, tree["left"])
else:
return self._predict_row(x, tree["right"])
# 定义XGBoost模型
class XGBoost:
def __init__(self, n_estimators=50, learning_rate=0.1, max_depth=3, lambda_reg=1):
self.n_estimators = n_estimators
self.learning_rate = learning_rate
self.max_depth = max_depth
self.lambda_reg = lambda_reg
self.trees = []
# 拟合模型
def fit(self, X, y):
self.base_prediction = np.mean(y)
y_pred = np.full(y.shape, self.base_prediction)
for _ in range(self.n_estimators):
g = 2 * (y_pred - y) # 一阶导数(梯度)
h = 2 * np.ones_like(y) # 二阶导数(Hessian)
tree = SimpleTree(depth=self.max_depth, lambda_reg=self.lambda_reg)
tree.fit(X, g, h)
y_pred += self.learning_rate * tree.predict(X)
self.trees.append(tree)
# 预测
def predict(self, X):
y_pred = np.full(X.shape[0], self.base_prediction)
for tree in self.trees:
y_pred += self.learning_rate * tree.predict(X)
return y_pred
# 创建并训练XGBoost模型
model
= XGBoost(n_estimators=10, learning_rate=0.1, max_depth=3, lambda_reg=1)
model.fit(X_train, y_train)
# 预测结果
y_train_pred = model.predict(X_train)
y_test_pred = model.predict(X_test)
# 绘制数据分析图
plt.figure(figsize=(10, 8))
# 图1:训练数据和预测结果
plt.subplot(2, 2, 1)
plt.scatter(X_train, y_train, color='blue', label='True', alpha=0.6)
plt.scatter(X_train, y_train_pred, color='red', label='Predicted', alpha=0.6)
plt.title("Training Data vs Predictions")
plt.legend()
# 图2:测试数据和预测结果
plt.subplot(2, 2, 2)
plt.scatter(X_test, y_test, color='green', label='True', alpha=0.6)
plt.scatter(X_test, y_test_pred, color='orange', label='Predicted', alpha=0.6)
plt.title("Testing Data vs Predictions")
plt.legend()
# 图3:训练误差
plt.subplot(2, 2, 3)
plt.plot(np.arange(len(y_train)), y_train - y_train_pred, color='purple')
plt.title("Training Residuals")
# 图4:测试误差
plt.subplot(2, 2, 4)
plt.plot(np.arange(len(y_test)), y_test - y_test_pred, color='brown')
plt.title("Testing Residuals")
plt.tight_layout()
plt.show()
SimpleTree: 实现了一个简单的决策树,包括拟合和预测的功能。 XGBoost: 使用梯度信息逐步修正误差,手动实现多棵树组合的模型。
图1: 训练数据与预测值的对比。 图2: 测试数据与预测值的对比。 图3: 训练数据的残差图。 图4: 测试数据的残差图。
所有上述原理,以及这段代码手动实现了XGBoost算法,并使用了虚拟数据集进行训练和预测,同时展示了模型的训练与测试性能。大家可以根据原理和实际的编码,进一步理解内容~