FIX always use logits in CalibratedClassifierCV - #34313
Conversation
|
I agree, we should definitively use the logits for sigmoid recalibration / Platt scaling. @ogrisel Could you grant me rights to push to your PR ? I'll try to merge main and update this draft PR. |
|
@antoinebaker you were already registered as a collaborator on my scikit-learn fork :) Please go ahead with the merge and update of this PR. |
|
Hi @ogrisel and @virchan, the PR is currently passing the CI. However I would like to refactor the code, in line with your code comments @ogrisel and your comment @virchan. I would greatly appreciate your feedback (see Comments section) to discuss future implementations ! With the help of cursor, here a summary of the current logic of
|
| Calibration method | Target logit space | Transformation |
|---|---|---|
| sigmoid / isotonic | OvR (“Bernoulli”) logits | z_k = logit(p_k) |
| temperature | Multinomial logits | z_k = log(p_k) - log(p_mean) |
decision_function
Inputs are scores s whose meaning varies by estimator.
| Calibration method | Target logit space | Current transformation |
|---|---|---|
| sigmoid / isotonic | OvR (“Bernoulli”) logits | Binary (1D): pass-through s (assumed positive-class log-odds). Multiclass (K>2): softmax(s) → p_k, then z_k = logit(p_k). |
| temperature | Multinomial logits | Pass-through s (assumed joint logits with p = softmax(s)). |
Comments
For me, the handling of predict_proba is correct and solves #34312. The handling of decision_function is more debatable. It seems to always assume "Multinomial logits"-like scores, and in particular converts them to Bernoulli logits for the sigmoid/isotonic calibration method.
Here are some implementation options I'd like to discuss:
- get rid of the
logit_preprocessingparameter. Instead we always decide based on the calibrationmethod(sigmoid vs softmax) andresponse_method_name(predict_proba vs decision_function) - only convert
predict_probafor now, letdecision_functionpassthrough. This would make the code much easier to understand. This would already fixes # 34312. - try to determine (for an estimator) if the
decision_functionscores are "morally" more Bernoulli vs Multinomial logits and convert based on that. But this seems difficult to do in general. - always convert the
decision_functionscores to the target logit space, even if they are in the correct logit space to start with. Currently done for thesigmoidmethod, but could be done for thetemperaturemethod as well.
I think I originally introduced the
I agree. |
|
@antoinebaker Let's first add temperature scaling to Let's also check that "sigmoid" logits preprocessing is always better or similar to "softmax" logits preprocessing for the two OvR methods. If all goes according to our plan, we can hard code the logits preprocessing logic and remove the empirical evaluation of the logits_processing parameter from that example. |
| implements :term:`predict_proba`, :math:`f_i = \text{logit}(\hat{p}_i)` is the | ||
| logit of the predicted probability; otherwise, :math:`f_i` is the score from | ||
| :term:`decision_function`. :math:`A` and :math:`B` are real numbers to be | ||
| determined when fitting the regressor via maximum likelihood. |
There was a problem hiding this comment.
"fitting the regressor" is a bit weird as we speak of classifiers
| _CLASSIFIER_RESPONSE_METHODS = ("predict_proba", "decision_function") | ||
|
|
||
|
|
||
| def _ensure_logits(predictions, response_method_name, method): |
There was a problem hiding this comment.
We already have _convert_to_logits in this file. Why a new function.
There was a problem hiding this comment.
_ensure_logits and _convert_to_logits both do some sort of "logit preprocessing", but in a different way, I tried to merge them at some point but the resulting code was too messy.
_ensure_logits is only used inside CalibratedClassifierCV while _convert_to_logits is only used inside _TemperatureScaling.
Co-authored-by: Christian Lorentzen <[email protected]>
virchan
left a comment
There was a problem hiding this comment.
Both calibration.py and test_calibration.py LGTM. I will review the examples later.
|
Merging main to re-trigger an HTML rendering of the doc changed in this PR. |
There was a problem hiding this comment.
I did another pass of review and tried to mark as resolved all previous comments by @lorentzenchr on outdated sections of the diff that seem to have been actually addressed by subsequent commits. This was quite painful because I think the comment navigation of the diff view of github seems to be badly broken on this PR...
From my point of view this seems good to go.
Co-authored-by: Olivier Grisel <[email protected]>
| # indicated by the diagonal calibration curve. This is expected since we | ||
| # adjusted the regularization parameter to minimize the :ref:`log_loss` via | ||
| # internal cross-validation. |
There was a problem hiding this comment.
This is expected
Is it? I would put it less strongly.
| # by default because of the redundant features which violate the assumption of | ||
| # feature-independence and result in an **overly confident** classifier, which | ||
| # is indicated by the typical transposed-sigmoid calibration curve. |
There was a problem hiding this comment.
Can you split the sentence? It is too long and complex.
| # | ||
| # Post-hoc calibration of the predicted probabilities of | ||
| # :class:`~sklearn.naive_bayes.GaussianNB` with :ref:`isotonic` or | ||
| # :ref:`Sigmoid regression <sigmoid_regressor>` can fix this issue as can be |
There was a problem hiding this comment.
| # :ref:`Sigmoid regression <sigmoid_regressor>` can fix this issue as can be | |
| # :ref:`Sigmoid regression <sigmoid_regressor> fix this issue to a good extent as can be |
| # because of the limited amount of training and calibration data (1,000 samples | ||
| # for both). | ||
| # | ||
| # Below we will make a quantitative analysis considering several classification |
There was a problem hiding this comment.
| # Below we will make a quantitative analysis considering several classification | |
| # Below we make a quantitative analysis considering several classification |
Same as section dataset.
| # Post-hoc calibration improves the :ref:`brier_score_loss` (a metric composed | ||
| # of calibration term and refinement term) and :ref:`log_loss` as expected |
There was a problem hiding this comment.
Both metrics, Brier score and log loss, are composed of a calibration (reliability) and refinement (resolution) term.
| # According to the Brier score, the calibrated classifier is also slightly | ||
| # better than the original model. | ||
| # | ||
| # Be aware that an improvement in log-loss or Brier score on a test set is not |
There was a problem hiding this comment.
| # Be aware that an improvement in log-loss or Brier score on a test set is not | |
| # Be aware that an improvement in log loss or Brier score on a test set is not |
| # always guaranteed since the calibration set is finite and sometimes the base | ||
| # classifier is already well calibrated. |
There was a problem hiding this comment.
We could just state the fact and remove the "because".
Or we add that calibration might decrease the resolution/discrimination term of the loss.
|
|
||
| # %% | ||
| # | ||
| # Let's now do the same for various classifiers with different mis-calibration |
There was a problem hiding this comment.
| # Let's now do the same for various classifiers with different mis-calibration | |
| # Let's now repeat the above for various classifiers with different mis-calibration |
| # - Some estimators such as highly regularized polynomial classifiers and | ||
| # shallow tree-based models tend to be under-confident by default and each |
There was a problem hiding this comment.
| # - Some estimators such as highly regularized polynomial classifiers and | |
| # shallow tree-based models tend to be under-confident by default and each | |
| # - Some estimators such as the highly regularized polynomial classifier and | |
| # the shallow tree-based model tend to be under-confident by default and each |
Similar below.
| # - The temperature scaling method applies a single global rescaling of the | ||
| # multinomial logits. This induces a smooth mapping that preserves the | ||
| # ranking of the predicted classes while adjusting their confidence. |
There was a problem hiding this comment.
It's not so much the multinomial logits, it is that it minimized the multinomial log loss (instead of ovr binary log loss + normalization).
lorentzenchr
left a comment
There was a problem hiding this comment.
Remaining part of the review.
| # TODO: once we have a calibration loss, use it instead of the | ||
| # brier score to check recalibration. |
There was a problem hiding this comment.
This is absolute not related to the fix of this PR. But ok, let's keep it.
| # Check probabilities sum to 1 | ||
| assert_allclose(np.sum(probas, axis=1), np.ones(len(X_test))) | ||
| y_pred_cal = cal_clf.predict_proba(X_test) | ||
| assert_allclose(np.sum(y_pred_cal, axis=1), np.ones(len(X_test))) |
There was a problem hiding this comment.
| assert_allclose(np.sum(y_pred_cal, axis=1), np.ones(len(X_test))) | |
| assert_allclose(np.sum(y_pred_cal, axis=1), 1) |
| labels = np.arange(n_classes) | ||
| bs_uncal = brier_score_loss(y_test, y_pred_uncal, labels=labels) | ||
| bs_cal = brier_score_loss(y_test, y_pred_cal, labels=labels) |
There was a problem hiding this comment.
| labels = np.arange(n_classes) | |
| bs_uncal = brier_score_loss(y_test, y_pred_uncal, labels=labels) | |
| bs_cal = brier_score_loss(y_test, y_pred_cal, labels=labels) | |
| bs_uncal = brier_score_loss(y_test, y_pred_uncal, labels=cal_clf.classes_) | |
| bs_cal = brier_score_loss(y_test, y_pred_cal, labels=cal_clf.classes_) |
This is cleaner and more secure.
| All calibration methods are expected to improve the log-loss of the base | ||
| classifiers given enough calibration data because the log-loss is a | ||
| strictly proper scoring rule. |
There was a problem hiding this comment.
I am not 100% convinced of the premise. A proper scoring rule is decomposed as:
loss = mis-calibration - discrimination + entropy. Entropy is constant (given by data only, not the model). If we change the predictions, we change the mis-calibration term, but also the discrimination term.
| def test_ensure_logits_decision_function(method, predictions): | ||
| # Apart from reshaping, this is a passthrough. | ||
| logits = _ensure_logits(predictions, "decision_function", method) | ||
| assert_allclose(logits.ravel(), predictions.ravel()) |
|
|
||
| When the response method is ``predict_proba``: | ||
|
|
||
| - For ``method='sigmoid'``, Bernoulli logits are computed per class |
There was a problem hiding this comment.
| - For ``method='sigmoid'``, Bernoulli logits are computed per class | |
| - For ``method='sigmoid'``, logits are computed per class |
| eps = xp.finfo(predictions.dtype).eps | ||
| eps_ = xp.asarray(eps, dtype=predictions.dtype, device=device_) |
There was a problem hiding this comment.
| eps = xp.finfo(predictions.dtype).eps | |
| eps_ = xp.asarray(eps, dtype=predictions.dtype, device=device_) | |
| eps = xp.asarray(xp.finfo(predictions.dtype).eps, dtype=predictions.dtype, device=device_) |
Or just renaming eps_ to eps.
| return xp.reshape(predictions, (-1, 1)) | ||
| return predictions | ||
|
|
||
| if method == "isotonic": |
There was a problem hiding this comment.
| if method == "isotonic": | |
| elif method == "isotonic": |
and so on. Not really needed, but a bit cleaner, imo.
| raise ValueError( | ||
| f"Unknown calibration method: {method}. " | ||
| "Expected 'sigmoid', 'isotonic', or 'temperature'." | ||
| ) |
There was a problem hiding this comment.
| raise ValueError( | |
| f"Unknown calibration method: {method}. " | |
| "Expected 'sigmoid', 'isotonic', or 'temperature'." | |
| ) | |
| else: | |
| raise ValueError( | |
| f"Unknown calibration method: {method}. " | |
| "Expected 'sigmoid', 'isotonic', or 'temperature'." | |
| ) |
| ) | ||
|
|
||
|
|
||
| def _to_calibration_logits(predictions, *, response_method_name, method, classes=None): |
There was a problem hiding this comment.
This function is only used once: Do we needed it or better put the code in place?
|
For a bugfix this is quite a large PR. I understand that examples are improved, too. |
Fixes: #34312
Note: this draft PR was started a long time ago. Work is needed to realign it with the current
mainbranch.