InterviewsVector

How to implement custom loss function in scikit-learn?

Quick answer

scikit-learn does not take arbitrary training loss functions the way Keras does — most estimators have a fixed built-in objective. What you CAN customise is the evaluation metric used for model selection: wrap your metric with make_scorer(your_metric, greater_is_better=...) and pass it to GridSearchCV or cross_val_score. That steers hyperparameter selection, but it does not change what the model minimises during fit. For a genuinely custom training objective, use a library that accepts one — XGBoost or LightGBM take a custom objective returning the gradient and hessian, and SGDRegressor/SGDClassifier expose a fixed menu of built-in losses via the loss= argument.

Short answer: You usually can't pass an arbitrary training loss to a scikit-learn estimator — most have a fixed built-in objective. What you customise is the evaluation metric for model selection: wrap it with make_scorer(fn, greater_is_better=...) and pass it to GridSearchCV/cross_val_score. That steers which model is chosen, not what fit minimises. For a true custom training objective, use XGBoost/LightGBM (which take a gradient+hessian objective).

There's a common misconception here, so it's worth being precise: a loss is what a model minimises during training; a metric/scorer is how you evaluate a fitted model. scikit-learn lets you customise the second freely, but the first is mostly fixed.

What you can't do

Most scikit-learn estimators hard-code their objective — LinearRegression minimises squared error, LogisticRegression minimises log loss — and there's no loss=my_function parameter to override it. The partial exception is SGDClassifier/SGDRegressor, which expose a menu of built-in losses (hinge, log_loss, squared_error, huber, …) via loss=, but you still can't plug in an arbitrary Python function.

What you can do: a custom scorer with make_scorer

If your goal is to select a model by a metric that matters to you (cost-weighted error, a business KPI), wrap it with make_scorer and use it as the scoring argument. Here's a real, runnable example — a cost matrix where false negatives cost 5× a false positive:

import numpy as np
from sklearn.metrics import make_scorer, confusion_matrix
from sklearn.model_selection import GridSearchCV
from sklearn.ensemble import RandomForestClassifier
 
def cost_metric(y_true, y_pred):
    tn, fp, fn, tp = confusion_matrix(y_true, y_pred).ravel()
    return fp * 1 + fn * 5          # total cost — lower is better
 
# greater_is_better=False flips the sign so GridSearchCV minimises it
cost_scorer = make_scorer(cost_metric, greater_is_better=False)
 
grid = GridSearchCV(
    RandomForestClassifier(random_state=0),
    param_grid={"max_depth": [3, 5, None]},
    scoring=cost_scorer,
)
grid.fit(X_train, y_train)
print(grid.best_params_)            # the model with the lowest cost

greater_is_better=False tells scikit-learn lower is better, so it negates the score internally and still "maximises". Crucially, this changes which hyperparameters win, not the objective each forest minimises while fitting.

For probability-based metrics, add response_method="predict_proba" (scikit-learn ≥ 1.4) so the scorer receives probabilities rather than hard labels.

What you can do: a true custom training objective (XGBoost/LightGBM)

When you genuinely need the model to train against your loss, use a gradient-boosting library that accepts one. You supply a function returning the gradient and hessian of your loss w.r.t. the prediction:

import numpy as np
from xgboost import XGBRegressor
 
def squared_log_error(y_pred, dtrain):
    y_true = dtrain.get_label()
    grad = (np.log1p(y_pred) - np.log1p(y_true)) / (y_pred + 1)
    hess = ((-np.log1p(y_pred) + np.log1p(y_true) + 1) / (y_pred + 1) ** 2)
    return grad, hess
 
model = XGBRegressor(objective=squared_log_error, n_estimators=200)
model.fit(X_train, y_train)

Choosing the right tool

GoalUse
Pick the best model by a custom metricmake_scorer + GridSearchCV/cross_val_score
A different built-in training lossSGDClassifier/SGDRegressor loss=
A genuinely custom training objectiveXGBoost / LightGBM custom objective (grad + hess)
Full control over the lossPyTorch / Keras, not scikit-learn

Sources

Key takeaways

  • •scikit-learn estimators mostly have fixed objectives - you cannot pass an arbitrary training loss the way Keras allows.
  • •To optimise a custom metric for model selection or cross-validation, wrap it with make_scorer(fn, greater_is_better=...).
  • •Set greater_is_better=False when lower is better (a loss) so the score is oriented correctly.
  • •For a true custom training objective, use a library that accepts one - XGBoost or LightGBM take a custom objective with gradient and hessian.

Frequently asked questions

Can I pass a custom loss function to a scikit-learn model?

Not as a training objective for most estimators, which have fixed objectives. Use make_scorer to score a custom metric during GridSearchCV or cross_val_score.

What does make_scorer do?

It turns a plain metric function into a scorer that GridSearchCV and cross_val_score can use, with greater_is_better controlling whether higher or lower is better.

How do I optimise a genuinely custom training objective?

Use a gradient-boosting library such as XGBoost or LightGBM, which accept a custom objective function returning the gradient and hessian.

By Mohammad Wasi

Software Engineering Leader & Technical Author · Updated September 9, 2026


Related Posts