在机器学习中,梯度提升树(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估计、测试损失和交叉验证损失之间的关系,以及如何使用这些信息来确定模型的最佳迭代次数。这种方法在实践中非常有用,因为它可以帮助平衡模型的复杂度和泛化能力。