梯度提升树的OOB估计

机器学习中,梯度提升树(Gradient Boosting Trees)是一种强大的集成学习方法,它通过迭代地训练决策树来最小化损失函数。在训练过程中,一个关键的步骤是确定最佳的迭代次数,即树的数量。OOB(Out-of-Bag)估计提供了一种无需重复模型拟合即可实时计算的方法,它基于未被包含在自助样本中的样本(即OOB样本)的损失改进来计算。

OOB估计与交叉验证估计非常相似,但后者需要更多的计算资源。OOB估计仅适用于随机梯度提升(即subsample小于1.0的情况),它通过观察未参与训练的样本来评估模型的性能。虽然OOB估计对真实测试损失的估计偏悲观,但对于少量树来说,它仍然是一个相当好的近似。

在实际应用中,可以通过绘制OOB改进的累积和与迭代次数的关系图来观察模型性能的变化。图中显示了随着迭代次数的增加,OOB损失如何变化,以及它与测试损失的对比。通常,OOB损失在最初的几百次迭代中与测试损失紧密跟踪,但随后会以悲观的方式发散。

为了更准确地估计测试损失,可以使用3折交叉验证,尽管这在计算上更为复杂。在下面的代码示例中,首先生成数据,然后使用随机梯度提升分类器进行训练,并计算OOB估计和交叉验证估计。最后,绘制了OOB损失、测试损失和交叉验证损失的曲线,并标出了最佳迭代次数。

import matplotlib.pyplot as plt import numpy as np from scipy.special import expit from sklearn import ensemble from sklearn.metrics import log_loss from sklearn.model_selection import KFold, train_test_split # 生成数据 n_samples = 1000 random_state = np.random.RandomState(13) x1 = random_state.uniform(size=n_samples) x2 = random_state.uniform(size=n_samples) x3 = random_state.randint(0, 4, size=n_samples) p = expit(np.sin(3 * x1) - 4 * x2 + x3) y = random_state.binomial(1, p, size=n_samples) X = np.c_[x1, x2, x3] X = X.astype(np.float32) X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.5, random_state=9) # 训练模型 params = { "n_estimators": 1200, "max_depth": 3, "subsample": 0.5, "learning_rate": 0.01, "min_samples_leaf": 1, "random_state": 3, } clf = ensemble.GradientBoostingClassifier(**params) clf.fit(X_train, y_train) acc = clf.score(X_test, y_test) print("Accuracy: {:.4f}".format(acc)) # 计算OOB估计 n_estimators = params["n_estimators"] x = np.arange(n_estimators) + 1 def heldout_score(clf, X_test, y_test): ""“计算X_test和y_test上的偏差分数。”"" score = np.zeros((n_estimators,), dtype=np.float64) for i, y_proba in enumerate(clf.staged_predict_proba(X_test)): score[i] = 2 * log_loss(y_test, y_proba[:, 1]) return score def cv_estimate(n_splits=None): cv = KFold(n_splits=n_splits) cv_clf = ensemble.GradientBoostingClassifier(**params) val_scores = np.zeros((n_estimators,), dtype=np.float64) for train, test in cv.split(X_train, y_train): cv_clf.fit(X_train[train], y_train[train]) val_scores += heldout_score(cv_clf, X_train[test], y_train[test]) val_scores /= n_splits return val_scores # 使用交叉验证估计最佳n_estimators cv_score = cv_estimate(3) # 计算测试数据的最佳n_estimators test_score = heldout_score(clf, X_test, y_test) # 负累积OOB改进 cumsum = -np.cumsum(clf.oob_improvement_) # 根据OOB的最小损失 oob_best_iter = x[np.argmin(cumsum)] # 根据测试的最小损失(标准化,使得第一次损失为0) test_score -= test_score[0] test_best_iter = x[np.argmin(test_score)] # 根据cv的最小损失(标准化,使得第一次损失为0) cv_score -= cv_score[0] cv_best_iter = x[np.argmin(cv_score)] # 为三条曲线设置颜色 oob_color = list(map(lambda x: x / 256.0, (190, 174, 212))) test_color = list(map(lambda x: x / 256.0, (127, 201, 127))) cv_color = list(map(lambda x: x / 256.0, (253, 192, 134))) # 为三条曲线设置线型 oob_line = "dashed" test_line = "solid" cv_line = "dashdot" # 绘制曲线和最佳迭代次数的垂直线 plt.figure(figsize=(8, 4.8)) plt.plot(x, cumsum, label="OOB损失", color=oob_color, linestyle=oob_line) plt.plot(x, test_score, label="测试损失", color=test_color, linestyle=test_line) plt.plot(x, cv_score, label="CV损失", color=cv_color, linestyle=cv_line) plt.axvline(x=oob_best_iter, color=oob_color, linestyle=oob_line) plt.axvline(x=test_best_iter, color=test_color, linestyle=test_line) plt.axvline(x=cv_best_iter, color=cv_color, linestyle=cv_line) # 添加三条垂直线到xticks xticks = plt.xticks() xticks_pos = np.array(xticks[0].tolist() + [oob_best_iter, cv_best_iter, test_best_iter]) xticks_label = np.array(list(map(lambda t: int(t), xticks[0])) + ["OOB", "CV", "Test"]) ind = np.argsort(xticks_pos) xticks_pos = xticks_pos[ind] xticks_label = xticks_label[ind] plt.xticks(xticks_pos, xticks_label, rotation=90) plt.legend(loc="upper center") plt.ylabel("标准化损失") plt.xlabel("迭代次数") plt.show()

通过上述代码,可以看到OOB估计、测试损失和交叉验证损失之间的关系,以及如何使用这些信息来确定模型的最佳迭代次数。这种方法在实践中非常有用,因为它可以帮助平衡模型的复杂度和泛化能力。

沪ICP备2024098111号-1
上海秋旦网络科技中心:上海市奉贤区金大公路8218号1幢 联系电话:17898875485