Define best_iteration only if early stopping is used. (#9403)
* Define `best_iteration` only if early stopping is used. This is the behavior specified by the document but not honored in the actual code. - Don't set the attributes if there's no early stopping. - Clean up the code for callbacks, and replace assertions with proper exceptions. - Assign the attributes when early stopping `save_best` is used. - Turn the attributes into Python properties. --------- Co-authored-by: Philip Hyunsu Cho <chohyu01@cs.washington.edu>
This commit is contained in:
@@ -37,6 +37,7 @@ class LintersPaths:
|
||||
"demo/rmm_plugin",
|
||||
"demo/json-model/json_parser.py",
|
||||
"demo/guide-python/cat_in_the_dat.py",
|
||||
"demo/guide-python/callbacks.py",
|
||||
"demo/guide-python/categorical.py",
|
||||
"demo/guide-python/feature_weights.py",
|
||||
"demo/guide-python/sklearn_parallel.py",
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import json
|
||||
import os
|
||||
import tempfile
|
||||
from contextlib import nullcontext
|
||||
from typing import Union
|
||||
|
||||
import pytest
|
||||
@@ -104,15 +103,6 @@ class TestCallbacks:
|
||||
dump = booster.get_dump(dump_format='json')
|
||||
assert len(dump) - booster.best_iteration == early_stopping_rounds + 1
|
||||
|
||||
# No early stopping, best_iteration should be set to last epoch
|
||||
booster = xgb.train({'objective': 'binary:logistic',
|
||||
'eval_metric': 'error'}, D_train,
|
||||
evals=[(D_train, 'Train'), (D_valid, 'Valid')],
|
||||
num_boost_round=10,
|
||||
evals_result=evals_result,
|
||||
verbose_eval=True)
|
||||
assert booster.num_boosted_rounds() - 1 == booster.best_iteration
|
||||
|
||||
def test_early_stopping_custom_eval(self):
|
||||
D_train = xgb.DMatrix(self.X_train, self.y_train)
|
||||
D_valid = xgb.DMatrix(self.X_valid, self.y_valid)
|
||||
@@ -204,8 +194,9 @@ class TestCallbacks:
|
||||
X, y = load_breast_cancer(return_X_y=True)
|
||||
n_estimators = 100
|
||||
early_stopping_rounds = 5
|
||||
early_stop = xgb.callback.EarlyStopping(rounds=early_stopping_rounds,
|
||||
save_best=True)
|
||||
early_stop = xgb.callback.EarlyStopping(
|
||||
rounds=early_stopping_rounds, save_best=True
|
||||
)
|
||||
cls = xgb.XGBClassifier(
|
||||
n_estimators=n_estimators,
|
||||
eval_metric=tm.eval_error_metric_skl,
|
||||
@@ -216,20 +207,27 @@ class TestCallbacks:
|
||||
dump = booster.get_dump(dump_format='json')
|
||||
assert len(dump) == booster.best_iteration + 1
|
||||
|
||||
early_stop = xgb.callback.EarlyStopping(rounds=early_stopping_rounds,
|
||||
save_best=True)
|
||||
early_stop = xgb.callback.EarlyStopping(
|
||||
rounds=early_stopping_rounds, save_best=True
|
||||
)
|
||||
cls = xgb.XGBClassifier(
|
||||
booster='gblinear', n_estimators=10, eval_metric=tm.eval_error_metric_skl
|
||||
booster="gblinear",
|
||||
n_estimators=10,
|
||||
eval_metric=tm.eval_error_metric_skl,
|
||||
callbacks=[early_stop],
|
||||
)
|
||||
with pytest.raises(ValueError):
|
||||
cls.fit(X, y, eval_set=[(X, y)], callbacks=[early_stop])
|
||||
cls.fit(X, y, eval_set=[(X, y)])
|
||||
|
||||
# No error
|
||||
early_stop = xgb.callback.EarlyStopping(rounds=early_stopping_rounds,
|
||||
save_best=False)
|
||||
xgb.XGBClassifier(
|
||||
booster='gblinear', n_estimators=10, eval_metric=tm.eval_error_metric_skl
|
||||
).fit(X, y, eval_set=[(X, y)], callbacks=[early_stop])
|
||||
booster="gblinear",
|
||||
n_estimators=10,
|
||||
eval_metric=tm.eval_error_metric_skl,
|
||||
callbacks=[early_stop],
|
||||
).fit(X, y, eval_set=[(X, y)])
|
||||
|
||||
def test_early_stopping_continuation(self):
|
||||
from sklearn.datasets import load_breast_cancer
|
||||
@@ -252,8 +250,11 @@ class TestCallbacks:
|
||||
cls.load_model(path)
|
||||
assert cls._Booster is not None
|
||||
early_stopping_rounds = 3
|
||||
cls.set_params(eval_metric=tm.eval_error_metric_skl)
|
||||
cls.fit(X, y, eval_set=[(X, y)], early_stopping_rounds=early_stopping_rounds)
|
||||
cls.set_params(
|
||||
eval_metric=tm.eval_error_metric_skl,
|
||||
early_stopping_rounds=early_stopping_rounds,
|
||||
)
|
||||
cls.fit(X, y, eval_set=[(X, y)])
|
||||
booster = cls.get_booster()
|
||||
assert booster.num_boosted_rounds() == \
|
||||
booster.best_iteration + early_stopping_rounds + 1
|
||||
@@ -280,20 +281,20 @@ class TestCallbacks:
|
||||
watchlist = [(dtest, 'eval'), (dtrain, 'train')]
|
||||
num_round = 4
|
||||
|
||||
warning_check = nullcontext()
|
||||
|
||||
# learning_rates as a list
|
||||
# init eta with 0 to check whether learning_rates work
|
||||
param = {'max_depth': 2, 'eta': 0, 'verbosity': 0,
|
||||
'objective': 'binary:logistic', 'eval_metric': 'error',
|
||||
'tree_method': tree_method}
|
||||
evals_result = {}
|
||||
with warning_check:
|
||||
bst = xgb.train(param, dtrain, num_round, watchlist,
|
||||
callbacks=[scheduler([
|
||||
0.8, 0.7, 0.6, 0.5
|
||||
])],
|
||||
evals_result=evals_result)
|
||||
bst = xgb.train(
|
||||
param,
|
||||
dtrain,
|
||||
num_round,
|
||||
evals=watchlist,
|
||||
callbacks=[scheduler([0.8, 0.7, 0.6, 0.5])],
|
||||
evals_result=evals_result,
|
||||
)
|
||||
eval_errors_0 = list(map(float, evals_result['eval']['error']))
|
||||
assert isinstance(bst, xgb.core.Booster)
|
||||
# validation error should decrease, if eta > 0
|
||||
@@ -304,11 +305,15 @@ class TestCallbacks:
|
||||
'objective': 'binary:logistic', 'eval_metric': 'error',
|
||||
'tree_method': tree_method}
|
||||
evals_result = {}
|
||||
with warning_check:
|
||||
bst = xgb.train(param, dtrain, num_round, watchlist,
|
||||
callbacks=[scheduler(
|
||||
[0.8, 0.7, 0.6, 0.5])],
|
||||
evals_result=evals_result)
|
||||
|
||||
bst = xgb.train(
|
||||
param,
|
||||
dtrain,
|
||||
num_round,
|
||||
evals=watchlist,
|
||||
callbacks=[scheduler([0.8, 0.7, 0.6, 0.5])],
|
||||
evals_result=evals_result,
|
||||
)
|
||||
eval_errors_1 = list(map(float, evals_result['eval']['error']))
|
||||
assert isinstance(bst, xgb.core.Booster)
|
||||
# validation error should decrease, if learning_rate > 0
|
||||
@@ -320,12 +325,14 @@ class TestCallbacks:
|
||||
'eval_metric': 'error', 'tree_method': tree_method
|
||||
}
|
||||
evals_result = {}
|
||||
with warning_check:
|
||||
bst = xgb.train(param, dtrain, num_round, watchlist,
|
||||
callbacks=[scheduler(
|
||||
[0, 0, 0, 0]
|
||||
)],
|
||||
evals_result=evals_result)
|
||||
bst = xgb.train(
|
||||
param,
|
||||
dtrain,
|
||||
num_round,
|
||||
evals=watchlist,
|
||||
callbacks=[scheduler([0, 0, 0, 0])],
|
||||
evals_result=evals_result,
|
||||
)
|
||||
eval_errors_2 = list(map(float, evals_result['eval']['error']))
|
||||
assert isinstance(bst, xgb.core.Booster)
|
||||
# validation error should not decrease, if eta/learning_rate = 0
|
||||
@@ -336,12 +343,14 @@ class TestCallbacks:
|
||||
return num_boost_round / (ithround + 1)
|
||||
|
||||
evals_result = {}
|
||||
with warning_check:
|
||||
bst = xgb.train(param, dtrain, num_round, watchlist,
|
||||
callbacks=[
|
||||
scheduler(eta_decay)
|
||||
],
|
||||
evals_result=evals_result)
|
||||
bst = xgb.train(
|
||||
param,
|
||||
dtrain,
|
||||
num_round,
|
||||
evals=watchlist,
|
||||
callbacks=[scheduler(eta_decay)],
|
||||
evals_result=evals_result,
|
||||
)
|
||||
eval_errors_3 = list(map(float, evals_result['eval']['error']))
|
||||
|
||||
assert isinstance(bst, xgb.core.Booster)
|
||||
@@ -351,8 +360,7 @@ class TestCallbacks:
|
||||
for i in range(1, len(eval_errors_0)):
|
||||
assert eval_errors_3[i] != eval_errors_2[i]
|
||||
|
||||
with warning_check:
|
||||
xgb.cv(param, dtrain, num_round, callbacks=[scheduler(eta_decay)])
|
||||
xgb.cv(param, dtrain, num_round, callbacks=[scheduler(eta_decay)])
|
||||
|
||||
def run_eta_decay_leaf_output(self, tree_method: str, objective: str) -> None:
|
||||
# check decay has effect on leaf output.
|
||||
@@ -378,7 +386,7 @@ class TestCallbacks:
|
||||
param,
|
||||
dtrain,
|
||||
num_round,
|
||||
watchlist,
|
||||
evals=watchlist,
|
||||
callbacks=[scheduler(eta_decay_0)],
|
||||
)
|
||||
|
||||
@@ -391,7 +399,7 @@ class TestCallbacks:
|
||||
param,
|
||||
dtrain,
|
||||
num_round,
|
||||
watchlist,
|
||||
evals=watchlist,
|
||||
callbacks=[scheduler(eta_decay_1)],
|
||||
)
|
||||
bst_json0 = bst0.save_raw(raw_format="json")
|
||||
@@ -474,3 +482,24 @@ class TestCallbacks:
|
||||
callbacks=callbacks,
|
||||
)
|
||||
assert len(callbacks) == 1
|
||||
|
||||
def test_attribute_error(self) -> None:
|
||||
from sklearn.datasets import load_breast_cancer
|
||||
|
||||
X, y = load_breast_cancer(return_X_y=True)
|
||||
|
||||
clf = xgb.XGBClassifier(n_estimators=8)
|
||||
clf.fit(X, y, eval_set=[(X, y)])
|
||||
|
||||
with pytest.raises(AttributeError, match="early stopping is used"):
|
||||
clf.best_iteration
|
||||
|
||||
with pytest.raises(AttributeError, match="early stopping is used"):
|
||||
clf.best_score
|
||||
|
||||
booster = clf.get_booster()
|
||||
with pytest.raises(AttributeError, match="early stopping is used"):
|
||||
booster.best_iteration
|
||||
|
||||
with pytest.raises(AttributeError, match="early stopping is used"):
|
||||
booster.best_score
|
||||
|
||||
@@ -173,7 +173,7 @@ class TestInplacePredict:
|
||||
np.testing.assert_allclose(predt_from_dmatrix, predt_from_array)
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
booster.predict(test, iteration_range=(0, booster.best_iteration + 2))
|
||||
booster.predict(test, iteration_range=(0, booster.num_boosted_rounds() + 2))
|
||||
|
||||
default = booster.predict(test)
|
||||
|
||||
@@ -181,7 +181,7 @@ class TestInplacePredict:
|
||||
np.testing.assert_allclose(range_full, default)
|
||||
|
||||
range_full = booster.predict(
|
||||
test, iteration_range=(0, booster.best_iteration + 1)
|
||||
test, iteration_range=(0, booster.num_boosted_rounds())
|
||||
)
|
||||
np.testing.assert_allclose(range_full, default)
|
||||
|
||||
|
||||
@@ -100,8 +100,8 @@ class TestTrainingContinuation:
|
||||
res2 = mean_squared_error(
|
||||
y_2class,
|
||||
gbdt_04.predict(
|
||||
dtrain_2class, iteration_range=(0, gbdt_04.best_iteration + 1)
|
||||
)
|
||||
dtrain_2class, iteration_range=(0, gbdt_04.num_boosted_rounds())
|
||||
),
|
||||
)
|
||||
assert res1 == res2
|
||||
|
||||
@@ -112,7 +112,7 @@ class TestTrainingContinuation:
|
||||
res2 = mean_squared_error(
|
||||
y_2class,
|
||||
gbdt_04.predict(
|
||||
dtrain_2class, iteration_range=(0, gbdt_04.best_iteration + 1)
|
||||
dtrain_2class, iteration_range=(0, gbdt_04.num_boosted_rounds())
|
||||
)
|
||||
)
|
||||
assert res1 == res2
|
||||
@@ -126,7 +126,7 @@ class TestTrainingContinuation:
|
||||
|
||||
res1 = gbdt_05.predict(dtrain_5class)
|
||||
res2 = gbdt_05.predict(
|
||||
dtrain_5class, iteration_range=(0, gbdt_05.best_iteration + 1)
|
||||
dtrain_5class, iteration_range=(0, gbdt_05.num_boosted_rounds())
|
||||
)
|
||||
np.testing.assert_almost_equal(res1, res2)
|
||||
|
||||
@@ -138,15 +138,16 @@ class TestTrainingContinuation:
|
||||
@pytest.mark.skipif(**tm.no_sklearn())
|
||||
def test_training_continuation_updaters_json(self):
|
||||
# Picked up from R tests.
|
||||
updaters = 'grow_colmaker,prune,refresh'
|
||||
updaters = "grow_colmaker,prune,refresh"
|
||||
params = self.generate_parameters()
|
||||
for p in params:
|
||||
p['updater'] = updaters
|
||||
p["updater"] = updaters
|
||||
self.run_training_continuation(params[0], params[1], params[2])
|
||||
|
||||
@pytest.mark.skipif(**tm.no_sklearn())
|
||||
def test_changed_parameter(self):
|
||||
from sklearn.datasets import load_breast_cancer
|
||||
|
||||
X, y = load_breast_cancer(return_X_y=True)
|
||||
clf = xgb.XGBClassifier(n_estimators=2)
|
||||
clf.fit(X, y, eval_set=[(X, y)], eval_metric="logloss")
|
||||
|
||||
Reference in New Issue
Block a user