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-package] Expose ObjectiveFunction class #6586

Open
wants to merge 18 commits into
base: master
Choose a base branch
from
Prev Previous commit
Next Next commit
Add comments and prepare for PR
  • Loading branch information
Atanas Dimitrov committed Aug 1, 2024
commit 60f95f83e4a0dc1e10a81460ef998a243803601a
23 changes: 21 additions & 2 deletions include/LightGBM/c_api.h
Original file line number Diff line number Diff line change
@@ -1565,25 +1565,44 @@ LIGHTGBM_C_EXPORT int LGBM_BoosterGetLowerBoundValue(BoosterHandle handle,
double* out_results);

/*!
* \brief Create an objective function.
* \param typ Type of the objective function
* \param parameter Parameters for the objective function
* \param[out] out Handle pointing to the created objective function
* \return 0 when succeed, -1 when failure happens
*/
LIGHTGBM_C_EXPORT int LGBM_ObjectiveFunctionCreate(const char *typ,
const char *parameter,
ObjectiveFunctionHandle *out);

/*!
* \brief Initialize an objective function with the dataset.
* \param handle Handle of the objective function
* \param dataset Handle of the dataset used for initialization
* \param[out] num_data Number of data points; this may be modified within the function
* \return 0 when succeed, -1 when failure happens
*/
LIGHTGBM_C_EXPORT int LGBM_ObjectiveFunctionInit(ObjectiveFunctionHandle handle,
int *num_data,
DatasetHandle dataset);
DatasetHandle dataset,
int *num_data);

/*!
* \brief Evaluate the objective function given model scores.
* \param handle Handle of the objective function
* \param score Array of scores predicted by the model
* \param[out] grad Gradient result array
* \param[out] hess Hessian result array
* \return 0 when succeed, -1 when failure happens
*/
LIGHTGBM_C_EXPORT int LGBM_ObjectiveFunctionEval(ObjectiveFunctionHandle handle,
const double* score,
float* grad,
float* hess);

/*!
* \brief Free the memory allocated for an objective function.
* \param handle Handle of the objective function
* \return 0 when succeed, -1 when failure happens
*/
LIGHTGBM_C_EXPORT int LGBM_ObjectiveFunctionFree(ObjectiveFunctionHandle handle);

95 changes: 72 additions & 23 deletions python-package/lightgbm/basic.py
Original file line number Diff line number Diff line change
@@ -5284,17 +5284,88 @@ def __get_eval_info(self) -> None:


class ObjectiveFunction:
"""
ObjectiveFunction in LightGBM.

This class exposes the builtin objective functions for evaluating gradients and hessians
on external datasets. LightGBM does not use this wrapper during its training as it is
using the underlying C++ class.
"""

def __init__(self, name: str, params: Dict[str, Any]):
"""
Initialize the ObjectiveFunction.

Parameters
----------
name : str
The name of the objective function.
params : dict
Dictionary of parameters for the objective function.
These are the parameters that would have been passed to ``booster.train``.
The ``name`` should be consistent with the ``params["objective"]`` field.
"""
self.name = name
self.params = params
self.num_data = None
self.num_class = params.get("num_class", 1)

if "objective" in params and params["objective"] != self.name:
raise ValueError("The name should be consistent with the params[\"objective\"] field.")

self.__create()

def init(self, dataset: Dataset) -> "ObjectiveFunction":
"""
Initialize the objective function using the provided dataset.

Parameters
----------
dataset : Dataset
The dataset object used for initialization.

Returns
-------
self : ObjectiveFunction
Initialized objective function object.
"""
return self.__init_from_dataset(dataset)

def __call__(self, y_pred: np.ndarray) -> Tuple[np.ndarray, np.ndarray]:
"""
Evaluate the objective function given model predictions.

Parameters
----------
y_pred : numpy.ndarray
Predicted scores from the model.

Returns
-------
(grad, hess) : Tuple[np.ndarray, np.ndarray]
A tuple containing gradients and Hessians.
"""
if self._handle is None:
raise ValueError("Objective function seems uninitialized")

if self.num_data is None or self.num_class is None:
raise ValueError("ObjectiveFunction was not created properly")

data_shape = self.num_data * self.num_class
grad = np.zeros(dtype=np.float32, shape=data_shape)
hess = np.zeros(dtype=np.float32, shape=data_shape)

_safe_call(
_LIB.LGBM_ObjectiveFunctionEval(
self._handle,
y_pred.ctypes.data_as(ctypes.POINTER(ctypes.c_double)),
grad.ctypes.data_as(ctypes.POINTER(ctypes.c_float)),
hess.ctypes.data_as(ctypes.POINTER(ctypes.c_float)),
)
)

return (grad, hess)

def __create(self):
self._handle = ctypes.c_void_p()
_safe_call(
@@ -5317,8 +5388,8 @@ def __init_from_dataset(self, dataset: Dataset) -> "ObjectiveFunction":
_safe_call(
_LIB.LGBM_ObjectiveFunctionInit(
self._handle,
ctypes.byref(tmp_num_data),
dataset._handle,
ctypes.byref(tmp_num_data),
)
)
self.num_data = tmp_num_data.value
@@ -5335,25 +5406,3 @@ def _free_handle(self) -> "ObjectiveFunction":
_safe_call(_LIB.LGBM_ObjectiveFunctionFree(self._handle))
self._handle = None
return self

def __call__(self, y_pred: np.ndarray) -> Tuple[np.ndarray, np.ndarray]:
if self._handle is None:
raise ValueError("Objective function seems uninitialized")

if self.num_data is None or self.num_class is None:
raise ValueError("ObjectiveFunction was not created properly")

data_shape = self.num_data * self.num_class
grad = np.zeros(dtype=np.float32, shape=data_shape)
hess = np.zeros(dtype=np.float32, shape=data_shape)

_safe_call(
_LIB.LGBM_ObjectiveFunctionEval(
self._handle,
y_pred.ctypes.data_as(ctypes.POINTER(ctypes.c_double)),
grad.ctypes.data_as(ctypes.POINTER(ctypes.c_float)),
hess.ctypes.data_as(ctypes.POINTER(ctypes.c_float)),
)
)

return (grad, hess)
4 changes: 2 additions & 2 deletions src/c_api.cpp
Original file line number Diff line number Diff line change
@@ -2763,8 +2763,8 @@ LIGHTGBM_C_EXPORT int LGBM_ObjectiveFunctionCreate(const char *typ,
}

LIGHTGBM_C_EXPORT int LGBM_ObjectiveFunctionInit(ObjectiveFunctionHandle handle,
int *num_data,
DatasetHandle dataset) {
DatasetHandle dataset,
int *num_data) {
API_BEGIN();
ObjectiveFunction* ref_fobj = reinterpret_cast<ObjectiveFunction*>(handle);
Dataset* ref_dataset = reinterpret_cast<Dataset*>(dataset);
2 changes: 1 addition & 1 deletion tests/python_package_test/test_engine.py
Original file line number Diff line number Diff line change
@@ -4409,7 +4409,7 @@ def test_quantized_training():
},
])
@pytest.mark.parametrize("num_boost_round", [5, 15])
def test_objective_function_multiclass(use_weight, test_data, num_boost_round):
def test_objective_function_class(use_weight, test_data, num_boost_round):
X, y = test_data["df"]
weight = np.random.choice([1, 2], y.shape) if use_weight else None
lgb_train = lgb.Dataset(X, y, weight=weight, init_score=np.zeros((len(y), test_data["num_class"])))