-
Notifications
You must be signed in to change notification settings - Fork 148
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
fix: SGDClassifier post-processing with multi-class and improve linear models' predict method #585
Conversation
@@ -1605,31 +1605,11 @@ def test_predict_correctness( | |||
|
|||
|
|||
@pytest.mark.parametrize("model_class, parameters", MODELS_AND_DATASETS) | |||
@pytest.mark.parametrize( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
there's no point of simulating here (we are testing encrypt
+ run
+ decrypt
)
# make sure we only compile below that bit-width. | ||
# Additionally, prevent computations in FHE with too many bits | ||
@pytest.mark.parametrize( | ||
"n_bits", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
there no real point of testing multiple n_bits values, as we are only testing the api + comparing fhe vs simulation
5ffaf9a
to
8baa5e9
Compare
@@ -1735,38 +1735,6 @@ class SklearnLinearRegressorMixin(SklearnLinearModelMixin, sklearn.base.Regresso | |||
""" | |||
|
|||
|
|||
class SklearnSGDRegressorMixin(SklearnLinearRegressorMixin): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
just moved it for better readability
@@ -1815,6 +1783,48 @@ def predict_proba(self, X: Data, fhe: Union[FheMode, str] = FheMode.DISABLE) -> | |||
y_proba = self.post_processing(y_logits) | |||
return y_proba | |||
|
|||
# In scikit-learn, the argmax is done on the scores directly, not the probabilities | |||
def predict(self, X: Data, fhe: Union[FheMode, str] = FheMode.DISABLE) -> numpy.ndarray: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this fixes https://github.com/zama-ai/concrete-ml-internal/issues/4344 (reason is explained in main comment, but basically this is how sklearn does)
@@ -253,27 +252,12 @@ def __init__( | |||
"Setting 'parameter_range' is mandatory if FHE training is enabled " | |||
f"({fit_encrypted=}). Got {parameters_range=}" | |||
) | |||
|
|||
def post_processing(self, y_preds: numpy.ndarray) -> numpy.ndarray: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this post_processing was wrong + I moved it below
@@ -835,61 +819,24 @@ def partial_fit( | |||
# FIXME: https://github.com/zama-ai/concrete-ml-internal/issues/4184 | |||
raise NotImplementedError("Partial fit is not currently supported for clear training.") | |||
|
|||
# This method is taken directly from scikit-learn | |||
def _predict_proba_lr(self, X: Data, fhe: Union[FheMode, str]) -> numpy.ndarray: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this part should be included in our post_processing
method
|
||
def predict_proba(self, X: Data, fhe: Union[FheMode, str] = FheMode.DISABLE) -> numpy.ndarray: | ||
"""Probability estimates. | ||
def post_processing(self, y_preds: numpy.ndarray) -> numpy.ndarray: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
basically we don't need to define the predict_proba
method as it is in sklearn, we only need to re-define post_processing
with sklearn's implem
@@ -133,27 +133,23 @@ def test_fit_error_if_non_binary_targets(n_bits, max_iter, parameter_min_max): | |||
model.partial_fit(x, y, fhe="disable") | |||
|
|||
|
|||
@pytest.mark.parametrize("loss", ["log_loss", "modified_huber"]) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
here we are testing error raises for specific arguments, we don't need all these inputs
@@ -651,7 +648,11 @@ def check_separated_inference(model, fhe_circuit, x, check_float_array_equal): | |||
is_classifier_or_partial_classifier(model) | |||
and get_model_name(model) != "KNeighborsClassifier" | |||
): | |||
y_pred = numpy.argmax(y_pred, axis=-1) | |||
# For linear classifiers, the argmax is done on the scores directly, not the probabilities | |||
if is_model_class_in_a_list(model, _get_sklearn_linear_models()): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
as mentioned in the main comment, let's do it like sklearn
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Awesome! Thanks a lot for the fixes + cleaning. Great explanations as well.
9e8b111
to
a7b606c
Compare
@@ -420,7 +421,7 @@ def check_accuracy(): | |||
"""Fixture to check the accuracy.""" | |||
|
|||
def check_accuracy_impl(expected, actual, threshold=0.9): | |||
accuracy = numpy.mean(expected == actual) | |||
accuracy = accuracy_score(expected, actual) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
better imo
y_preds = self.output_quantizers[0].dequant(q_y_preds) | ||
|
||
# If the preds have shape (n, 1), squeeze it to shape (n,) like in scikit-learn | ||
if y_preds.ndim == 2 and y_preds.shape[1] == 1: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
like in sklearn (same for the followings one)
# "Method 'decision_function' outputs different shapes between scikit-learn and " | ||
# f"Concrete ML in FHE (fhe={fhe})" | ||
# ) | ||
assert y_scores_sklearn.shape == y_scores_fhe.shape, ( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
put back the assert here and below
@@ -1912,7 +1892,7 @@ def test_rounding_consistency_for_regular_models( | |||
else: | |||
# Check `predict` for regressors | |||
predict_method = model.predict | |||
metric = check_accuracy | |||
metric = check_r2_score |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
regressors should be tested with r2 score
93aaec4
to
a7dc084
Compare
Coverage passed ✅Coverage details
|
(for info, the last commits are about fixing https://github.com/zama-ai/concrete-ml-internal/issues/4029) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks!
first step towards a green weekly CI
So there are a few things to note in this PR :
post_processing
method was not properly integrated : instead, everything was done inpredict_proba
, which interferes with the way we build and test our API (notably thetest_separated_inference
)predict
method: while trees / qnns usually dopredict_proba
+argmax
, for linear models sklearn doesdecision_function
+argmax
. In theory this should not change anything as thepredict_proba
basically does a sigmoid/softmax or normalization, but in practice it made the argmax behave differently because of slight floating points errors (basically the same as in https://github.com/zama-ai/concrete-ml-internal/issues/3369)Why are we discovering this only recently ? A few reasons :
predict
method because of the float issues mentioned above : we consider that the most important fact is making sure that quantized == quantized. The only paces where this seem to happen is the hyper parameter test, which is where @jfrery detected the issuetest_separated_inference
. Still, our custom post_processing function was good for 1D array (tested in regular CIs) with "log_loss" loss (default). Only the weekly tests 2D arrays, which is where we found the errorI also took the liberty to clean a bit some parts/tests (no breaking changes)
refs https://github.com/zama-ai/concrete-ml-internal/issues/4030
closes https://github.com/zama-ai/concrete-ml-internal/issues/4344
closes https://github.com/zama-ai/concrete-ml-internal/issues/4252
closes https://github.com/zama-ai/concrete-ml-internal/issues/4029