Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[python] Bug fix for first_metric_only on earlystopping. #2209

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
72 commits
Select commit Hold shift + click to select a range
0c9c77c
Bug fix for first_metric_only if the first metric is train metric.
matsuken92 Jun 1, 2019
130fe38
Update bug fix for feval issue.
matsuken92 Jun 1, 2019
7ab1a59
Disable feval for first_metric_only.
matsuken92 Jun 1, 2019
ba8a5aa
Additional test items.
matsuken92 Jun 1, 2019
25850fa
Fix wrong assertEqual settings & formating.
matsuken92 Jun 1, 2019
fddf8da
Change dataset of test.
matsuken92 Jun 1, 2019
6b71ebc
Fix random seed for test.
matsuken92 Jun 1, 2019
979f4df
Modiry assumed test result due to different sklearn verion between CI…
matsuken92 Jun 1, 2019
5e68ae9
Remove f-string
matsuken92 Jun 1, 2019
0f196e2
Applying variable assumed test result for test.
matsuken92 Jun 1, 2019
c0d61fa
Fix flake8 error.
matsuken92 Jun 1, 2019
0e91956
Modifying in accordance with review comments.
matsuken92 Jun 1, 2019
6f30b81
Modifying for pylint.
matsuken92 Jun 1, 2019
3770cc1
simplified tests
StrikerRUS Jun 2, 2019
c4e0af5
Deleting error criteria `if eval_metric is None`.
matsuken92 Jun 3, 2019
2f4e2b0
Delete test items of classification.
matsuken92 Jun 3, 2019
9387197
Simplifying if condition.
matsuken92 Jun 7, 2019
5aeb2bd
Applying first_metric_only for sklearn wrapper.
matsuken92 Jun 10, 2019
79ba017
Merge branch 'master' into bugfix/first_metric_only_train_metric
matsuken92 Jun 10, 2019
c40408c
Modifying test_sklearn for comforming to python 2.x
matsuken92 Jun 10, 2019
6a70b0c
Merge branch 'bugfix/first_metric_only_train_metric' of https://githu…
matsuken92 Jun 10, 2019
fe7d586
Fix flake8 error.
matsuken92 Jun 10, 2019
71c1bc2
Additional fix for sklearn and add tests.
matsuken92 Jun 11, 2019
3e956ea
Bug fix and add test cases.
matsuken92 Jun 17, 2019
0338bc7
some refactor
StrikerRUS Jun 18, 2019
75d7c57
fixed lint
StrikerRUS Jun 18, 2019
4645126
fixed lint
StrikerRUS Jun 18, 2019
60233bb
Fix duplicated metrics scores to pass the test.
matsuken92 Jun 29, 2019
f3f1e83
Fix the case first_metric_only not in params.
matsuken92 Jun 29, 2019
e054f97
Converting metrics aliases.
matsuken92 Jul 2, 2019
4e62ef7
Add comment.
matsuken92 Jul 3, 2019
3b154b6
Modify comment for pylint.
matsuken92 Jul 3, 2019
6dc7e85
Modify comment for pydocstyle.
matsuken92 Jul 3, 2019
1dc5397
Using split test set for two eval_set.
matsuken92 Jul 6, 2019
ebc97b3
added test case for metric aliases and length checks
StrikerRUS Jul 6, 2019
f7f0dfe
minor style fixes
StrikerRUS Jul 6, 2019
4221b8a
fixed rmse name and alias position
StrikerRUS Jul 7, 2019
5470265
Fix the case metric=[]
matsuken92 Jul 10, 2019
e292b39
Fix using env.model._train_data_name
matsuken92 Jul 10, 2019
ffa95b8
Fix wrong test condition.
matsuken92 Jul 10, 2019
3403f7b
Move initial process to _init() func.
matsuken92 Jul 10, 2019
43ea2df
Merge remote-tracking branch 'upstream/master' into bugfix/first_metr…
matsuken92 Jul 27, 2019
b509afa
Modify test setting for test_sklearn & training data matching on call…
matsuken92 Jul 27, 2019
c4e4b33
Support composite name metrics.
matsuken92 Jul 27, 2019
2f1578c
Remove metric check process & reduce redundant test cases.
matsuken92 Jul 27, 2019
45fc5eb
Revised according to the matters pointed out on a review.
matsuken92 Aug 29, 2019
f270258
increased code readability
StrikerRUS Aug 30, 2019
df03f4c
Fix the issue of order of validation set.
matsuken92 Sep 1, 2019
37770b0
Merge branch 'bugfix/first_metric_only_train_metric' of https://githu…
matsuken92 Sep 1, 2019
0a73a67
Changing to OrderdDict from default dict for score result.
matsuken92 Sep 1, 2019
6209c4a
added missed check in cv function for first_metric_only and feval co-…
StrikerRUS Sep 1, 2019
ea0312a
keep order only for metrics but not for datasets in best_score
StrikerRUS Sep 1, 2019
c881171
move OrderedDict initialization to init phase
StrikerRUS Sep 1, 2019
386fe1c
fixed minor printing issues
StrikerRUS Sep 1, 2019
8fe0469
move first metric detection to init phase and split can be performed …
StrikerRUS Sep 1, 2019
13737ac
split only once during callback
StrikerRUS Sep 1, 2019
5128b34
removed excess code
StrikerRUS Sep 1, 2019
ca4fd0c
fixed typo in variable name and squashed ifs
StrikerRUS Sep 1, 2019
cb9e327
use setdefault
StrikerRUS Sep 1, 2019
97004e0
hotfix
StrikerRUS Sep 1, 2019
19319c3
fixed failing test
StrikerRUS Sep 1, 2019
58a800c
refined tests
StrikerRUS Sep 2, 2019
f5a2b74
refined sklearn test
StrikerRUS Sep 3, 2019
b7a03e7
Making "feval" effective on early stopping.
matsuken92 Sep 9, 2019
47b7a23
Merge branch 'master' into bugfix/first_metric_only_train_metric
matsuken92 Sep 9, 2019
5c99e7e
fixed conflicts
StrikerRUS Sep 10, 2019
15a5fc2
allow feval and first_metric_only for cv
StrikerRUS Sep 10, 2019
d20b338
removed unused code
StrikerRUS Sep 10, 2019
c3fbf6b
added tests for feval
StrikerRUS Sep 10, 2019
cbaadbe
fixed printing
StrikerRUS Sep 10, 2019
a2d6449
add note about whitespaces in feval name
StrikerRUS Sep 10, 2019
88050da
Modifying final iteration process in case valid set is training data.
matsuken92 Sep 11, 2019
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
12 changes: 6 additions & 6 deletions python-package/lightgbm/basic.py
Original file line number Diff line number Diff line change
Expand Up @@ -1628,7 +1628,7 @@ def __init__(self, params=None, train_set=None, model_file=None, model_str=None,
self.handle = None
self.network = False
self.__need_reload_eval_info = True
self.__train_data_name = "training"
self._train_data_name = "training"
self.__attr = {}
self.__set_objective_to_none = False
self.best_iteration = -1
Expand Down Expand Up @@ -1817,7 +1817,7 @@ def set_train_data_name(self, name):
self : Booster
Booster with set training Dataset name.
"""
self.__train_data_name = name
self._train_data_name = name
return self

def add_valid(self, data, name):
Expand Down Expand Up @@ -2044,7 +2044,7 @@ def eval(self, data, name, feval=None):
eval_data : Dataset
The evaluation dataset.
eval_name : string
The name of evaluation function.
The name of evaluation function (without whitespaces).
eval_result : float
The eval result.
is_higher_better : bool
Expand Down Expand Up @@ -2090,7 +2090,7 @@ def eval_train(self, feval=None):
train_data : Dataset
The training dataset.
eval_name : string
The name of evaluation function.
The name of evaluation function (without whitespaces).
eval_result : float
The eval result.
is_higher_better : bool
Expand All @@ -2104,7 +2104,7 @@ def eval_train(self, feval=None):
result : list
List with evaluation results.
"""
return self.__inner_eval(self.__train_data_name, 0, feval)
return self.__inner_eval(self._train_data_name, 0, feval)

def eval_valid(self, feval=None):
"""Evaluate for validation data.
Expand All @@ -2121,7 +2121,7 @@ def eval_valid(self, feval=None):
valid_data : Dataset
The validation dataset.
eval_name : string
The name of evaluation function.
The name of evaluation function (without whitespaces).
eval_result : float
The eval result.
is_higher_better : bool
Expand Down
41 changes: 29 additions & 12 deletions python-package/lightgbm/callback.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,12 +89,13 @@ def record_evaluation(eval_result):
The callback that records the evaluation history into the passed dictionary.
"""
if not isinstance(eval_result, dict):
raise TypeError('Eval_result should be a dictionary')
raise TypeError('eval_result should be a dictionary')
eval_result.clear()

def _init(env):
for data_name, _, _, _ in env.evaluation_result_list:
eval_result.setdefault(data_name, collections.defaultdict(list))
for data_name, eval_name, _, _ in env.evaluation_result_list:
eval_result.setdefault(data_name, collections.OrderedDict())
eval_result[data_name].setdefault(eval_name, [])

def _callback(env):
if not eval_result:
Expand Down Expand Up @@ -132,7 +133,7 @@ def _callback(env):
if key in ['num_class', 'num_classes',
'boosting', 'boost', 'boosting_type',
'metric', 'metrics', 'metric_types']:
raise RuntimeError("cannot reset {} during training".format(repr(key)))
raise RuntimeError("Cannot reset {} during training".format(repr(key)))
if isinstance(value, list):
if len(value) != env.end_iteration - env.begin_iteration:
raise ValueError("Length of list {} has to equal to 'num_boost_round'."
Expand Down Expand Up @@ -182,6 +183,7 @@ def early_stopping(stopping_rounds, first_metric_only=False, verbose=True):
best_score_list = []
cmp_op = []
enabled = [True]
first_metric = ['']

def _init(env):
enabled[0] = not any((boost_alias in env.params
Expand All @@ -196,9 +198,11 @@ def _init(env):
'at least one dataset and eval metric is required for evaluation')

if verbose:
msg = "Training until validation scores don't improve for {} rounds."
msg = "Training until validation scores don't improve for {} rounds"
print(msg.format(stopping_rounds))

# split is needed for "<dataset type> <metric>" case (e.g. "train l1")
first_metric[0] = env.evaluation_result_list[0][1].split(" ")[-1]
for eval_ret in env.evaluation_result_list:
best_iter.append(0)
best_score_list.append(None)
Expand All @@ -209,6 +213,15 @@ def _init(env):
best_score.append(float('inf'))
cmp_op.append(lt)

def _final_iteration_check(env, eval_name_splitted, i):
if env.iteration == env.end_iteration - 1:
if verbose:
print('Did not meet early stopping. Best iteration is:\n[%d]\t%s' % (
best_iter[i] + 1, '\t'.join([_format_eval_result(x) for x in best_score_list[i]])))
if first_metric_only:
print("Evaluated only: {}".format(eval_name_splitted[-1]))
raise EarlyStopException(best_iter[i], best_score_list[i])

def _callback(env):
if not cmp_op:
_init(env)
Expand All @@ -220,17 +233,21 @@ def _callback(env):
best_score[i] = score
best_iter[i] = env.iteration
best_score_list[i] = env.evaluation_result_list
# split is needed for "<dataset type> <metric>" case (e.g. "train l1")
eval_name_splitted = env.evaluation_result_list[i][1].split(" ")
if first_metric_only and first_metric[0] != eval_name_splitted[-1]:
continue # use only the first metric for early stopping
if ((env.evaluation_result_list[i][0] == "cv_agg" and eval_name_splitted[0] == "train"
or env.evaluation_result_list[i][0] == env.model._train_data_name)):
_final_iteration_check(env, eval_name_splitted, i)
continue # train data for lgb.cv or sklearn wrapper (underlying lgb.train)
elif env.iteration - best_iter[i] >= stopping_rounds:
if verbose:
print('Early stopping, best iteration is:\n[%d]\t%s' % (
best_iter[i] + 1, '\t'.join([_format_eval_result(x) for x in best_score_list[i]])))
if first_metric_only:
print("Evaluated only: {}".format(eval_name_splitted[-1]))
raise EarlyStopException(best_iter[i], best_score_list[i])
if env.iteration == env.end_iteration - 1:
if verbose:
print('Did not meet early stopping. Best iteration is:\n[%d]\t%s' % (
best_iter[i] + 1, '\t'.join([_format_eval_result(x) for x in best_score_list[i]])))
raise EarlyStopException(best_iter[i], best_score_list[i])
if first_metric_only: # the only first metric is used for early stopping
break
_final_iteration_check(env, eval_name_splitted, i)
_callback.order = 30
return _callback
9 changes: 5 additions & 4 deletions python-package/lightgbm/engine.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ def train(params, train_set, num_boost_round=100,
train_data : Dataset
The training dataset.
eval_name : string
The name of evaluation function.
The name of evaluation function (without whitespaces).
eval_result : float
The eval result.
is_higher_better : bool
Expand Down Expand Up @@ -266,7 +266,7 @@ def train(params, train_set, num_boost_round=100,
booster.best_iteration = earlyStopException.best_iteration + 1
evaluation_result_list = earlyStopException.best_score
break
booster.best_score = collections.defaultdict(dict)
booster.best_score = collections.defaultdict(collections.OrderedDict)
for dataset_name, eval_name, score, _ in evaluation_result_list:
booster.best_score[dataset_name][eval_name] = score
if not keep_training_booster:
Expand Down Expand Up @@ -356,7 +356,7 @@ def _make_n_folds(full_data, folds, nfold, params, seed, fpreproc=None, stratifi

def _agg_cv_result(raw_results, eval_train_metric=False):
"""Aggregate cross-validation results."""
cvmap = collections.defaultdict(list)
cvmap = collections.OrderedDict()
metric_type = {}
for one_result in raw_results:
for one_line in one_result:
Expand All @@ -365,6 +365,7 @@ def _agg_cv_result(raw_results, eval_train_metric=False):
else:
key = one_line[1]
metric_type[key] = one_line[3]
cvmap.setdefault(key, [])
cvmap[key].append(one_line[2])
return [('cv_agg', k, np.mean(v), metric_type[k], np.std(v)) for k, v in cvmap.items()]

Expand Down Expand Up @@ -429,7 +430,7 @@ def cv(params, train_set, num_boost_round=100,
train_data : Dataset
The training dataset.
eval_name : string
The name of evaluation function.
The name of evaluation function (without whitespaces).
eval_result : float
The eval result.
is_higher_better : bool
Expand Down
9 changes: 5 additions & 4 deletions python-package/lightgbm/sklearn.py
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ def __init__(self, func):
group : array-like
Group/query data, used for ranking task.
eval_name : string
The name of evaluation function.
The name of evaluation function (without whitespaces).
eval_result : float
The eval result.
is_higher_better : bool
Expand All @@ -147,7 +147,7 @@ def __call__(self, preds, dataset):
Returns
-------
eval_name : string
The name of evaluation function.
The name of evaluation function (without whitespaces).
eval_result : float
The eval result.
is_higher_better : bool
Expand Down Expand Up @@ -464,7 +464,7 @@ def fit(self, X, y,
group : array-like
Group/query data, used for ranking task.
eval_name : string
The name of evaluation function.
The name of evaluation function (without whitespaces).
eval_result : float
The eval result.
is_higher_better : bool
Expand Down Expand Up @@ -524,7 +524,8 @@ def fit(self, X, y,
# concatenate metric from params (or default if not provided in params) and eval_metric
original_metric = [original_metric] if isinstance(original_metric, (string_type, type(None))) else original_metric
eval_metric = [eval_metric] if isinstance(eval_metric, (string_type, type(None))) else eval_metric
params['metric'] = set(original_metric + eval_metric)
params['metric'] = [e for e in eval_metric if e not in original_metric] + original_metric
params['metric'] = [metric for metric in params['metric'] if metric is not None]

if not isinstance(X, (DataFrame, DataTable)):
_X, _y = _LGBMCheckXY(X, y, accept_sparse=True, force_all_finite=False, ensure_min_samples=2)
Expand Down