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] Allowing custom objective / metric function with "objective" and "metric" parameters #3244

Closed
daanklijn opened this issue Jul 21, 2020 · 17 comments
Assignees

Comments

@daanklijn
Copy link

Summary

It would be nice if one could register custom objective and loss functions, so that these can be passed into the LightGBM's train function via the param argument.

Motivation

I believe that this would be a nice feature as this allows for easier hyperparameter tuning. Currently one would need to write logic that retrieves the custom function based on the name in a config file and pass those into the train function's feval and fobj arguments. It would be much easier and cleaner if one could just register these custom functions s.t. they can be passed in like the standard objective functions and eval metrics.

Description

Currently one would do something like this to use custom objective functions and evaluation metrics:

import lightgbm as lgb
from custom import custom_objective, custom_metric

params = {}
gbm = lgb.train(params=params,
                train_set=train_set,
                valid_sets=eval_set,
                fobj=custom_objective,
                feval=custom_metric
                )

I would propose to also allow something similar to this:

import lightgbm as lgb
from custom import custom_objective, custom_metric

lgb.register_metric(name="custom_metric", function=custom_metric)
lgb.register_objective(name="custom_objective", function=custom_objective)

params = {
    'objective': 'custom_objective',
    'metric': ['custom_metric'],
}

gbm = lgb.train(params=params,
                train_set=train_set,
                valid_sets=eval_set
                )
@daanklijn daanklijn changed the title Register custom objective / loss function [Python] Register custom objective / loss function Jul 21, 2020
@guolinke
Copy link
Collaborator

guolinke commented Aug 5, 2020

thanks @daanklijn
I think this is quite easy to implement.
@shiyu1994 could you help with this when you have time?

@shiyu1994
Copy link
Collaborator

thanks @daanklijn
I think this is quite easy to implement.
@shiyu1994 could you help with this when you have time?

Sure.

@shiyu1994 shiyu1994 self-assigned this Aug 18, 2020
@StrikerRUS
Copy link
Collaborator

@daanklijn Will passing callable metric/loss in standard params will suit your needs?
We have something similar for sklearn API and I personally find it more convenient than registering callables.

eval_metric=[rmsle, rae],

# Separate built-in from callable evaluation metrics
eval_metrics_callable = [_EvalFunctionWrapper(f) for f in eval_metric_list if callable(f)]
eval_metrics_builtin = [m for m in eval_metric_list if isinstance(m, str)]

@StrikerRUS
Copy link
Collaborator

Closed in favor of being in #2302. We decided to keep all feature requests in one place.

Welcome to contribute this feature! Please re-open this issue (or post a comment if you are not a topic starter) if you are actively working on implementing this feature.

@TremaMiguel
Copy link
Contributor

TremaMiguel commented Jan 27, 2022

@StrikerRUS , @jameslamb I'm open to develop this

I will consider starting first with register_objective because it is only a callable or None.

I think a similar logic as with Loggers can be used, that is, first register the metric (example register_logger) and then making the callable available through a function (example _log_warning) _retrieve_objective, this function either return a callable or None.

Inside lgb.train() if fobj is not provided check for a previous register callable, reference

fobj = fobj or _retrieve_objective()  # either passed argument or registered callable

if fobj is not None:
    for obj_alias in _ConfigAliases.get("objective"):
        params.pop(obj_alias, None)
    params['objective'] = 'none'

what are your thoughts?

@StrikerRUS
Copy link
Collaborator

@TremaMiguel Sorry, I'm not 100% sold on registering custom functions. I believe that allowing to pass callables to objective/metric argument is more user friendly and is already implemented in the sklearn wrapper (#3244 (comment)). Adding one more way to pass custom functions will add a lot of maintenance burden and may confuse users by number of possibilities.

@jameslamb
Copy link
Collaborator

I agree completely with @StrikerRUS

@TremaMiguel
Copy link
Contributor

Agree I think registering custom callables could bring some unnecessary logic. I've found this issue available on #2302, should it be marked as completed or removed from the list?

@StrikerRUS
Copy link
Collaborator

I believe we can wait for some other opinions. Or this issue can be transformed into feature request for allowing custom callables via objective/metric parameter for all Python interfaces.

@jameslamb
Copy link
Collaborator

I agree with @StrikerRUS , I'd support passing callables to parameter objective or metric, and don't support adding something like a lgb.register_metric() function.

For more inspiration, the R package also supports passing a mix of strings and custom functions:

bst <- lgb.train(
params = list(
objective = "regression"
, metric = "None"
, verbose = VERBOSITY
)
, data = DTRAIN_RANDOM_REGRESSION
, nrounds = nrounds
, valids = list(
"valid1" = DVALID_RANDOM_REGRESSION
)
, eval = list(
.increasing_metric
, "rmse"
, .constant_metric
, "l2"
)
)

@shiyu1994
Copy link
Collaborator

I agree with @jameslamb @StrikerRUS. Allowing passing customized function with parameter "objective" or "metric" can also fulfill the need in the motivation of this feature request. I'll change the description in #2302.

@StrikerRUS StrikerRUS changed the title [Python] Register custom objective / loss function [Python] Allowing custom objective / metric function with "objective" and "metric" parameters Feb 18, 2022
@TremaMiguel
Copy link
Contributor

@jameslamb I'm out of context, as I understand fobj and feval already accept custom callables, what's different with @StrikerRUS proposal?

@jameslamb
Copy link
Collaborator

what's different with @StrikerRUS proposal

We are saying we'd support a pull request making it possible to do something like the following:

bst = lgb.train(
    ...,
    params = {
        "metric": ["auc", _some_function_i_wrote],
    }
)

And the same for objective.

Like the code linked in #3244 (comment).

@TremaMiguel
Copy link
Contributor

@jameslamb @StrikerRUS so basically to achieve

bst = lgb.train(
    ...,
    params = {
        "objective": "regression" # or custom function  "objective":custom_objective_function
        "metric": ["auc", _some_function_i_wrote],
    }
)

one needs to get the following done

  1. Check if objective is in params and assigned it to fobj like the R implementation. This will be passed to Booster.update()
  2. Check if metric is in params and pass it to Booster.eval_train() and Booster.eval_valid()
  3. Add support for multiple metric values like the R implementation, creating a list of eval_functions

is there anything missing?

@StrikerRUS
Copy link
Collaborator

I think we can remove fobj and feval dedicated arguments after implementing this feature.

@StrikerRUS
Copy link
Collaborator

Firstly I guess we can simply transfer this logic

# Separate built-in from callable evaluation metrics
eval_metrics_callable = [_EvalFunctionWrapper(f) for f in eval_metric_list if callable(f)]
eval_metrics_builtin = [m for m in eval_metric_list if isinstance(m, str)]
# concatenate metric from params (or default if not provided in params) and eval_metric
params['metric'] = [params['metric']] if isinstance(params['metric'], (str, type(None))) else params['metric']
params['metric'] = [e for e in eval_metrics_builtin if e not in params['metric']] + params['metric']
params['metric'] = [metric for metric in params['metric'] if metric is not None]

into basic.py

@TremaMiguel
Copy link
Contributor

TremaMiguel commented Mar 3, 2022

@StrikerRUS Ohh I see, I think this one is kind of easy, one just need to handle:

  • metrics with the code you provide, ignoring feval if params has a metric key.
  • handle fobj if callable or string with
# objective is callable
f_obj = params['objective']
if callable(f_obj):
    for obj_alias in _ConfigAliases.get("objective"):
        params.pop(obj_alias, None)
    fobj = params['objective']
    params['objective'] = None
elif isinstance(f_obj, str):
    for obj_alias in _ConfigAliases.get("objective"):
        params.pop(obj_alias, None)

and no need to add a list of eval_functions cause Booster.__inner_eval supports it.

P.D: I think there's an improvement with docs regarding Booster eval, eval_train and eval_valid methods #5046

jameslamb pushed a commit that referenced this issue Apr 22, 2022
…unction in params (fixes #3244) (#5052)

* feat: support custom metrics in params

* feat: support objective in params

* test: custom objective and metric

* fix: imports are incorrectly sorted

* feat: convert eval metrics str and set to list

* feat: convert single callable eval_metric to list

* test: single callable objective in params

Signed-off-by: Miguel Trejo <armando.trejo.marrufo@gmail.com>

* feat: callable fobj in basic cv function

Signed-off-by: Miguel Trejo <armando.trejo.marrufo@gmail.com>

* test: cv support objective callable

Signed-off-by: Miguel Trejo <armando.trejo.marrufo@gmail.com>

* fix: assert in cv_res

Signed-off-by: Miguel Trejo <armando.trejo.marrufo@gmail.com>

* docs: objective callable in params

Signed-off-by: Miguel Trejo <armando.trejo.marrufo@gmail.com>

* recover test_boost_from_average_with_single_leaf_trees

Signed-off-by: Miguel Trejo <armando.trejo.marrufo@gmail.com>

* linters fail

Signed-off-by: Miguel Trejo <armando.trejo.marrufo@gmail.com>

* remove metrics helper functions

Signed-off-by: Miguel Trejo <armando.trejo.marrufo@gmail.com>

* feat: choose objective through _choose_param_values

Signed-off-by: Miguel Trejo <armando.trejo.marrufo@gmail.com>

* test: test objective through _choose_param_values

Signed-off-by: Miguel Trejo <armando.trejo.marrufo@gmail.com>

* test: test objective is callabe in train

Signed-off-by: Miguel Trejo <armando.trejo.marrufo@gmail.com>

* test: parametrize choose_param_value with objective aliases

Signed-off-by: Miguel Trejo <armando.trejo.marrufo@gmail.com>

* test: cv booster metric is none

Signed-off-by: Miguel Trejo <armando.trejo.marrufo@gmail.com>

* fix: if string and callable choose callable

Signed-off-by: Miguel Trejo <armando.trejo.marrufo@gmail.com>

* test train uses custom objective metrics

Signed-off-by: Miguel Trejo <armando.trejo.marrufo@gmail.com>

* test: cv uses custom objective metrics

Signed-off-by: Miguel Trejo <armando.trejo.marrufo@gmail.com>

* refactor: remove fobj parameter in train and cv

Signed-off-by: Miguel Trejo <armando.trejo.marrufo@gmail.com>

* refactor: objective through params in sklearn API

Signed-off-by: Miguel Trejo <armando.trejo.marrufo@gmail.com>

* custom objective function in advanced_example

Signed-off-by: Miguel Trejo <armando.trejo.marrufo@gmail.com>

* fix whitespackes lint

* objective is none not a particular case for predict method

Signed-off-by: Miguel Trejo <armando.trejo.marrufo@gmail.com>

* replace scipy.expit with custom implementation

Signed-off-by: Miguel Trejo <armando.trejo.marrufo@gmail.com>

* test: set num_boost_round value to 20

Signed-off-by: Miguel Trejo <armando.trejo.marrufo@gmail.com>

* fix: custom objective default_value is none

Signed-off-by: Miguel Trejo <armando.trejo.marrufo@gmail.com>

* refactor: remove self._fobj

Signed-off-by: Miguel Trejo <armando.trejo.marrufo@gmail.com>

* custom_objective default value is None

Signed-off-by: Miguel Trejo <armando.trejo.marrufo@gmail.com>

* refactor: variables name reference dummy_obj

Signed-off-by: Miguel Trejo <armando.trejo.marrufo@gmail.com>

* linter errors

* fix: process objective parameter when calling predict

Signed-off-by: Miguel Trejo <armando.trejo.marrufo@gmail.com>

* linter errors

* fix: objective is None during predict call

Signed-off-by: Miguel Trejo <armando.trejo.marrufo@gmail.com>
@jameslamb jameslamb reopened this Apr 22, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

6 participants