-
-
Notifications
You must be signed in to change notification settings - Fork 26.6k
FEA Add array API support for average_precision_score
#32909
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
base: main
Are you sure you want to change the base?
FEA Add array API support for average_precision_score
#32909
Conversation
| binary_metric : callable, returns shape [n_classes] | ||
| The binary metric function to use. | ||
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.
I changed the order of the params in the docs to match the order of reading these into the function, like in all the other places.
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.
_average_binary_score is also used in roc_auc_score. So it's probably best to first discuss how to properly add array API support to it, or wait until this PR is merged before creating a PR for roc_auc_score.
average_precision_score
| if metric.__name__ == "average_precision_score": | ||
| # we need y_pred_nd to be of shape (n_samples, n_classes | ||
| y_pred_np = np.array( | ||
| [ | ||
| [0.7, 0.2, 0.05, 0.05], | ||
| [0.1, 0.8, 0.05, 0.05], | ||
| [0.1, 0.1, 0.7, 0.1], | ||
| [0.05, 0.05, 0.1, 0.8], | ||
| ], | ||
| dtype=dtype_name, | ||
| ) | ||
|
|
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 case handling needs to be added, because average_precision_score handels multiclass inputs by binarising it. As a consequence, y_score needs to be of shape (n_samples, n_classes). I have made a PR with a fix here: #32912.
| if average_weight is not None: | ||
| # Scores with 0 weights are forced to be 0, preventing the average | ||
| # score from being affected by 0-weighted NaN elements. | ||
| average_weight = np.asarray(average_weight) |
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 is not needed anymore, since average_weight is converted to an array earlier.
| if not get_config().get("array_api_dispatch", False): | ||
| y_true = _convert_to_numpy(y_true, xp=xp) | ||
| y_score = _convert_to_numpy(y_score, xp=xp) | ||
| if sample_weight is not None: | ||
| sample_weight = _convert_to_numpy(sample_weight, xp=xp) |
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 (hopefully) fixes an error occurring on the CI (link to output):
E TypeError: sum() received an invalid combination of arguments - got (out=NoneType, axis=int, ), but expected one of:
E * (*, torch.dtype dtype = None)
E didn't match because some of the keywords were incorrect: out, axis
E * (tuple of ints dim, bool keepdim = False, *, torch.dtype dtype = None)
E * (tuple of names dim, bool keepdim = False, *, torch.dtype dtype = None)
@lesteve and I investigated this together. It seems that numpy.sum() when passed a torch tensor, internally tries to dispatch to torch, but that fails because torch.sum() doesn't have the out argument. The same happens with np.repeat() which has an axis argument, but torch.repeat() hasn't (which had raised in my setup locally).
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.
Yes, I ran into the same issue when I was working on roc_auc_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.
Instead of converting everything to NumPy ahead of time, we should inspect the namespace and device of y_score only. Indeed, we want to allow y_true to have an object dtype for string class labels (hence a NumPy array) even when the predictions are a non-NumPy namespace with a non-CPU device.
See: https://scikit-learn.org/dev/modules/array_api.html#scoring-functions
y_true should thereafter convert to binary indicators (using pos_label when available, and moved to the same device, dtype and namespace as y_score just before doing the arithmetic computation.
sample_weight should similarly follow the namespace, dtype and device of y_score using move_to.
See the discussion and implementation of the common test being worked on in #32755 for mixed input metrics.
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.
I will keep your comment in mind for later, but this here addresses a different problem. It fixes a test failure if all inputs (specifically y_true and y_score) are torch tensors on cpu.
In check_array_api_metric() we have defined a numpy_as_array_works boolean that is True if array api is not enabled, but we get input from a non-numpy namespace that can be converted to numpy (see here):
# When array API dispatch is disabled, and np.asarray works (for example PyTorch
# with CPU device), calling the metric function with such numpy compatible inputs
# should work (albeit by implicitly converting to numpy arrays instead of
# dispatching to the array library).
try:
np.asarray(a_xp)
np.asarray(b_xp)
numpy_as_array_works = TrueIn this case, we feed two torch arrays into the metric, but the test fails (see here):
if numpy_as_array_works:
metric_xp = metric(a_xp, b_xp, **metric_kwargs)If I have understood correctly, then we would always try to convert to numpy, if get_config().get("array_api_dispatch", False) but the input would be convertible to numpy, and making sure that we do is the purpose of this test (added in #30454).
(In this WIP PR I haven't taken care of mixed inputs at all yet, also #32755 is not merged yet.)
| binary_metric : callable, returns shape [n_classes] | ||
| The binary metric function to use. | ||
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.
_average_binary_score is also used in roc_auc_score. So it's probably best to first discuss how to properly add array API support to it, or wait until this PR is merged before creating a PR for roc_auc_score.
sklearn/metrics/_base.py
Outdated
|
|
||
| import sklearn.externals.array_api_extra as xpx | ||
| from sklearn.utils import check_array, check_consistent_length | ||
| from sklearn.utils._array_api import _ravel, get_namespace_and_device |
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.
| from sklearn.utils._array_api import _ravel, get_namespace_and_device | |
| from sklearn.utils._array_api import _ravel, get_namespace_and_device, xpx |
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.
I wasn't aware of that, and maybe that had been discussed somewhere I missed. So I did a bit of searching and I have found xpx imported via _array_api.py in sklearn/model_selection/_search.py and in sklearn/tree/tests/test_tree.py.
And I have found from sklearn.externals import array_api_extra as xpx imports in three other files.
I prefer the more explicit import from sklearn.externals, since it is easier to see what is going on the first glance even if you are unfamiliar with array api, and it will have to be obvious for people unfamilar with array api, as it will be in every function in the future and we also want to make it easy for future contributors.
| if not get_config().get("array_api_dispatch", False): | ||
| y_true = _convert_to_numpy(y_true, xp=xp) | ||
| y_score = _convert_to_numpy(y_score, xp=xp) | ||
| if sample_weight is not None: | ||
| sample_weight = _convert_to_numpy(sample_weight, xp=xp) |
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.
Yes, I ran into the same issue when I was working on roc_auc_score.
average_precision_scoreaverage_precision_score
Reference Issues/PRs
towards #26024
What does this implement/fix? Explain your changes.
Adds array API support to
average_precision_scoreAI usage disclosure
I used AI assistance for:
Any other comments?
WIP
TODO:
average_precision_scoreaverage_precision_scoreas well as from_check_set_wise_labelswhich allows to havepresent_labelsas an array instead of a listLabelBinarizerinstead oflabel_binarize# TODO: check if in fact necessary_average_binary_score