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
Expose convert_outputs as well
  • Loading branch information
Atanas Dimitrov committed Aug 8, 2024
commit 698850da79b58a5b7d811dd61c9db3e71bf1a6ee
20 changes: 16 additions & 4 deletions include/LightGBM/c_api.h
Original file line number Diff line number Diff line change
@@ -1594,10 +1594,10 @@ LIGHTGBM_C_EXPORT int LGBM_ObjectiveFunctionInit(ObjectiveFunctionHandle handle,
* \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);
LIGHTGBM_C_EXPORT int LGBM_ObjectiveFunctionGetGradients(ObjectiveFunctionHandle handle,
const double* score,
float* grad,
float* hess);

/*!
* \brief Free the memory allocated for an objective function.
@@ -1606,6 +1606,18 @@ LIGHTGBM_C_EXPORT int LGBM_ObjectiveFunctionEval(ObjectiveFunctionHandle handle,
*/
LIGHTGBM_C_EXPORT int LGBM_ObjectiveFunctionFree(ObjectiveFunctionHandle handle);

/*!
* \brief Convert raw scores to outputs.
* \param handle Handle of the objective function
* \param num_data Number of data points
* \param inputs Array of raw scores
* \param[out] outputs Array of outputs
*/
LIGHTGBM_C_EXPORT int LGBM_ObjectiveFunctionConvertOutputs(ObjectiveFunctionHandle handle,
const int num_data,
const double* inputs,
double* outputs);

/*!
* \brief Initialize the network.
* \param machines List of machines in format 'ip1:port1,ip2:port2'
6 changes: 6 additions & 0 deletions include/LightGBM/objective_function.h
Original file line number Diff line number Diff line change
@@ -67,6 +67,12 @@ class ObjectiveFunction {
/*! \brief Return the number of positive samples. Return 0 if no binary classification tasks.*/
virtual data_size_t NumPositiveData() const { return 0; }

virtual void ConvertOutputs(const int num_data, const double* inputs, double* outputs) const {
for (int i = 0; i < num_data; i ++) {
ConvertOutput(inputs + i, outputs + i);
}
}

virtual void ConvertOutput(const double* input, double* output) const {
output[0] = input[0];
}
39 changes: 37 additions & 2 deletions python-package/lightgbm/basic.py
Original file line number Diff line number Diff line change
@@ -5331,7 +5331,42 @@ def init(self, dataset: Dataset) -> "ObjectiveFunction":
"""
return self.__init_from_dataset(dataset)

def __call__(self, y_pred: np.ndarray) -> Tuple[np.ndarray, np.ndarray]:
def convert_outputs(self, scores: np.ndarray) -> np.ndarray:
"""
Convert the raw scores to the final predictions.

Parameters
----------
scores : numpy.ndarray
Raw scores from the model.

Returns
-------
result : numpy.ndarray
"""
if self._handle is None:
raise ValueError("Objective function seems uninitialized")

if self.num_class == 1:
scores = _list_to_1d_numpy(scores, dtype=np.float64, name="scores")
else:
scores = _data_to_2d_numpy(scores, dtype=np.float64, name="scores")

num_data = scores.size
out_preds = np.zeros_like(scores, dtype=np.float64)

_safe_call(
_LIB.LGBM_ObjectiveFunctionConvertOutputs(
self._handle,
ctypes.c_int(num_data),
scores.ctypes.data_as(ctypes.POINTER(ctypes.c_double)),
out_preds.ctypes.data_as(ctypes.POINTER(ctypes.c_double)),
)
)

return out_preds

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

@@ -5356,7 +5391,7 @@ def __call__(self, y_pred: np.ndarray) -> Tuple[np.ndarray, np.ndarray]:
hess = np.zeros(dtype=np.float32, shape=data_shape)

_safe_call(
_LIB.LGBM_ObjectiveFunctionEval(
_LIB.LGBM_ObjectiveFunctionGetGradients(
self._handle,
y_pred.ctypes.data_as(ctypes.POINTER(ctypes.c_double)),
grad.ctypes.data_as(ctypes.POINTER(ctypes.c_float)),
26 changes: 22 additions & 4 deletions src/c_api.cpp
Original file line number Diff line number Diff line change
@@ -2773,10 +2773,10 @@ LIGHTGBM_C_EXPORT int LGBM_ObjectiveFunctionInit(ObjectiveFunctionHandle handle,
API_END();
}

LIGHTGBM_C_EXPORT int LGBM_ObjectiveFunctionEval(ObjectiveFunctionHandle handle,
const double* score,
float* grad,
float* hess) {
LIGHTGBM_C_EXPORT int LGBM_ObjectiveFunctionGetGradients(ObjectiveFunctionHandle handle,
const double* score,
float* grad,
float* hess) {
API_BEGIN();
#ifdef SCORE_T_USE_DOUBLE
(void) handle; // UNUSED VARIABLE
@@ -2790,6 +2790,24 @@ LIGHTGBM_C_EXPORT int LGBM_ObjectiveFunctionEval(ObjectiveFunctionHandle handle,
API_END();
}

LIGHTGBM_C_EXPORT int LGBM_ObjectiveFunctionConvertOutputs(ObjectiveFunctionHandle handle,
const int num_data,
const double* inputs,
double* outputs) {
API_BEGIN();
#ifdef SCORE_T_USE_DOUBLE
(void) handle; // UNUSED VARIABLE
(void) num_data; // UNUSED VARIABLE
(void) inputs; // UNUSED VARIABLE
(void) outputs; // UNUSED VARIABLE
Log::Fatal("Don't support evaluating objective function when SCORE_T_USE_DOUBLE is enabled");
#else
ObjectiveFunction* ref_fobj = reinterpret_cast<ObjectiveFunction*>(handle);
ref_fobj->ConvertOutputs(num_data, inputs, outputs);
#endif
API_END();
}

/*!
*/
LIGHTGBM_C_EXPORT int LGBM_ObjectiveFunctionFree(ObjectiveFunctionHandle handle) {
12 changes: 12 additions & 0 deletions src/objective/multiclass_objective.hpp
Original file line number Diff line number Diff line change
@@ -129,6 +129,12 @@ class MulticlassSoftmax: public ObjectiveFunction {
}
}

void ConvertOutputs(const int num_data, const double* inputs, double* outputs) const override {
for (int i = 0; i < num_data; i += num_class_) {
ConvertOutput(inputs + i, outputs + i);
}
}

void ConvertOutput(const double* input, double* output) const override {
Common::Softmax(input, output, num_class_);
}
@@ -236,6 +242,12 @@ class MulticlassOVA: public ObjectiveFunction {
return "multiclassova";
}

void ConvertOutputs(const int num_data, const double* inputs, double* outputs) const override {
for (int i = 0; i < num_data; i += num_class_) {
ConvertOutput(inputs + i, outputs + i);
}
}

void ConvertOutput(const double* input, double* output) const override {
for (int i = 0; i < num_class_; ++i) {
output[i] = 1.0f / (1.0f + std::exp(-sigmoid_ * input[i]));
6 changes: 6 additions & 0 deletions tests/python_package_test/test_engine.py
Original file line number Diff line number Diff line change
@@ -4427,6 +4427,7 @@ def test_objective_function_class(use_weight, test_data, num_boost_round):
"device": "cpu",
}
builtin_loss = builtin_objective(test_data["objective_name"], copy.deepcopy(params))
builtin_convert_outputs = lgb.ObjectiveFunction(test_data["objective_name"], copy.deepcopy(params)).convert_outputs

params["objective"] = builtin_loss
booster_exposed = lgb.train(params, lgb_train, num_boost_round=num_boost_round)
@@ -4442,3 +4443,8 @@ def test_objective_function_class(use_weight, test_data, num_boost_round):

y_pred = np.zeros_like(booster.predict(X, raw_score=True))
np.testing.assert_allclose(builtin_loss(y_pred, lgb_train), test_data["custom_objective"](y_pred, lgb_train))

np.testing.assert_allclose(
builtin_convert_outputs(booster_exposed.predict(X)),
booster.predict(X)
)
2 changes: 1 addition & 1 deletion tests/python_package_test/utils.py
Original file line number Diff line number Diff line change
@@ -174,7 +174,7 @@ def builtin_objective(name, params):
def wrapper(y_pred, dtrain):
fobj = lgb.ObjectiveFunction(name, params)
fobj.init(dtrain)
(grad, hess) = fobj(y_pred)
(grad, hess) = fobj.get_gradients(y_pred)
if fobj.num_class != 1:
grad = grad.reshape((fobj.num_class, -1)).transpose()
hess = hess.reshape((fobj.num_class, -1)).transpose()
Loading
Oops, something went wrong.