-
Notifications
You must be signed in to change notification settings - Fork 3.8k
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-package] stop relying on string concatenation / splitting for cv() eval results #6761
base: master
Are you sure you want to change the base?
Conversation
metric_type[key] = one_line[3] | ||
cvmap.setdefault(key, []) | ||
cvmap[key].append(one_line[2]) | ||
return [("cv_agg", k, float(np.mean(v)), metric_type[k], float(np.std(v))) for k, v in cvmap.items()] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This, removing this "cv_agg"
string literal, is the key change... everything else flows from that.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM! Thanks a lot for clear refactoring!
Just some minor suggestions.
python-package/lightgbm/callback.py
Outdated
@@ -71,6 +71,14 @@ class CallbackEnv: | |||
evaluation_result_list: Optional[_ListOfEvalResultTuples] | |||
|
|||
|
|||
def _using_cv(env: CallbackEnv) -> bool: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
def _using_cv(env: CallbackEnv) -> bool: | |
def _is_using_cv(env: CallbackEnv) -> bool: |
For the consistency with other functions like
LightGBM/python-package/lightgbm/callback.py
Line 307 in 480600b
def _is_train_set(self, ds_name: str, eval_name: str, env: CallbackEnv) -> bool: |
LightGBM/python-package/lightgbm/basic.py
Line 175 in 480600b
def _is_zero(x: float) -> bool: |
LightGBM/python-package/lightgbm/basic.py
Line 315 in 480600b
def _is_numeric(obj: Any) -> bool: |
LightGBM/python-package/lightgbm/basic.py
Line 326 in 480600b
def _is_numpy_1d_array(data: Any) -> bool: |
etc.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sure, good point. Just pushed 5b25aed updating this to _is_using_cv()
.
python-package/lightgbm/callback.py
Outdated
@@ -71,6 +71,14 @@ class CallbackEnv: | |||
evaluation_result_list: Optional[_ListOfEvalResultTuples] | |||
|
|||
|
|||
def _using_cv(env: CallbackEnv) -> bool: | |||
"""Check if model in callback env is a CVBooster.""" | |||
# this string-matching is used instead of isinstance() to avoid a circular import |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Instead of string-matching I think it's possible to use lazy import, i.e. import CVBooster
inside this function.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was thinking that it'd be expensive to do that import (since this could end up running on every iteration), but I guess it shouldn't be much overhead because Python caches imports: https://docs.python.org/3/reference/import.html#the-module-cache
I'll try that, thanks for the suggestion!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just pushed 5b25aed using a lazy import + isinstance()
check.
python-package/lightgbm/engine.py
Outdated
# } | ||
# | ||
metric_types: Dict[Tuple[str, str], bool] = OrderedDict() | ||
metric_values: Dict[Tuple[str, str], List[float]] = OrderedDict() | ||
for one_result in raw_results: | ||
for one_line in one_result: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just removes unnecessary intermediate variable one_line
.
for one_line in one_result: | |
for dataset_name, metric_name, metric_value, is_higher_better in one_result: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I suspect that this double loop has been there in lightgbm
for a long time to account for the fact that raw_results
could either be a list of tuples or a list of list of tuples?
LightGBM/python-package/lightgbm/engine.py
Lines 638 to 641 in 4feee28
feval : callable, list of callable, or None, optional (default=None) | |
Customized evaluation function. | |
Each evaluation function should accept two parameters: preds, eval_data, | |
and return (eval_name, eval_result, is_higher_better) or list of such tuples. |
I'll double-check that, and add some tests on that pattern if we don't yet have any.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I realize now that I read your comment too quickly... you're not recommending removing the double-nested loop, just moving the tuple unpacking up into the second for
line.
I agree! I just pushed 5b25aed doing that.
In that commit, I also added 2 tests confirming that if a custom metric function returns a list of evaluation tuples, train()
and cv()
will work as expected. I only added 1 test case for each, in the interest of not expanding the already very thorough test_metrics
test, but please let me know if you'd like to see more test cases added there.
Co-authored-by: Nikita Titov <[email protected]>
…into python/remove-cv-agg
Contributes to #6748
There are a few activities where
lightgbm
(the Python package) needs to inspect the output of one or more evaluation metrics on one or more datasets.For example:
For
train()
and other APIs that end up using it, it tracks those using a list of tuples like this (pseudocode):cv()
does something similar. However, its "metric value" is actually a mean of such values taken over all cross-validation folds. Because multiple values are being aggregated, it appends a 5th item with the standard deviation.Some code in
callbacks.py
needs to know, given a list of such tuples, whether they were produced by cross-validation or regulartrain()
.To facilitate that while still somewhat preserving the schema for the tuples, the
cv()
code:"cv_agg"
to the beginning of the tupleSo e.g.
("valid1", "auc", ...)
becomes("cv_agg", "valid1 auc", ...)
. That happens here:LightGBM/python-package/lightgbm/engine.py
Lines 580 to 592 in 480600b
Every place dealing with such tuples then needs to deal with that, including splitting and re-combining that second element. Like this:
LightGBM/python-package/lightgbm/callback.py
Lines 416 to 418 in 480600b
This proposes changes to remove that, so that the
cv()
andtrain()
tuples follow a similar schema and all the complexity of splitting and re-combining names can be removed.It also standardizes on the names from #6749 (comment)
Notes for Reviewers
This change should be completely backwards-compatible, including with user-provided custom metric function. The code paths here are well-covered by tests (as I found out from many failed tests while developing this 😅 ).