Skip to content

ENH: Add Array API support to hamming_loss #30838

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

Merged
merged 12 commits into from
Mar 18, 2025

Conversation

lithomas1
Copy link
Contributor

Reference Issues/PRs

xref #26024

What does this implement/fix? Explain your changes.

This makes hamming_loss array API compatible.

Any other comments?

Copy link

github-actions bot commented Feb 15, 2025

✔️ Linting Passed

All linting checks passed. Your pull request is in excellent shape! ☀️

Generated for commit: 595b2bd. Link to the linter CI: here

@lithomas1 lithomas1 marked this pull request as ready for review February 15, 2025 18:24
Copy link
Member

@virchan virchan left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the PR @lithomas1!

Appreciate your comment about about the weight_average!

@lithomas1 lithomas1 requested a review from virchan February 22, 2025 13:57
@github-actions github-actions bot removed the CUDA CI label Feb 25, 2025
Copy link
Member

@virchan virchan left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CUDA CI passes on GitHub Actions, but I kept encountering the following DeprecationWarning when testing on my local machine:

DeprecationWarning: __array__ implementation doesn't accept a copy keyword, so passing copy=False failed. __array__ must implement 'dtype' and 'copy' keyword arguments. To learn more, see the migration guide https://numpy.org/devdocs/numpy_2_0_migration_guide.html#adapting-to-changes-in-the-copy-keyword

The warning seems to originate from the count_zero function while testing PyTorch on CPU:

sklearn/metrics/tests/test_common.py::test_array_api_compliance[hamming_loss-check_array_api_multilabel_classification_metric-torch-cpu-float64] FAILED                                           
sklearn/metrics/tests/test_common.py::test_array_api_compliance[hamming_loss-check_array_api_multilabel_classification_metric-torch-cpu-float32] FAILED

It could be a false positive on my end, but I also noticed that casting sample_weight back to the xp array type:

if sample_weight is not None:
   sample_weight = xp.asarray(sample_weight, device=device)
   weight average = _average(sample_weight, xp=xp)

would suppress the warning message.

This "fix" makes sense to me because moving sample_weight to the same namespace as y_true and y_pred ensures consistency across backends and prevents NumPy array methods from being called on a PyTorch tensor.

However, I’m not entirely sure if this is the correct fix or if it’s simply avoiding the warning. Therefore, I’d like to ping @OmarManzoor for help to take a closer look. Also, I don’t have the hardware to test for MPS, so @OmarManzoor, SOS! 😅

@OmarManzoor
Copy link
Contributor

It could be a false positive on my end, but I also noticed that casting sample_weight back to the xp array type:

if sample_weight is not None:
   sample_weight = xp.asarray(sample_weight, device=device)
   weight average = _average(sample_weight, xp=xp)

would suppress the warning message.

This "fix" makes sense to me because moving sample_weight to the same namespace as y_true and y_pred ensures consistency across backends and prevents NumPy array methods from being called on a PyTorch tensor.

I think this makes sense as sample_weight should be on the same namespace and device, in the case where it isn't already. So this additional code to ensure that seems correct. @virchan feel free to add that as a suggestion.

@ogrisel Would it be possible for you to run this on a Mac with mps?

Copy link
Member

@virchan virchan left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To be safe, let's ensure that sample_weight is in the same namespace and on the same device as y_true and y_pred.

Co-authored-by: Virgil Chan <virchan.math@gmail.com>
Copy link
Contributor

@OmarManzoor OmarManzoor left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the PR @lithomas1

Co-authored-by: Omar Salman <omar.salman2007@gmail.com>
@lithomas1
Copy link
Contributor Author

Thanks for the review.

r.e. the count_nonzero change

I think I copied the original code from here (from accuracy_score)

if _is_numpy_namespace(xp):
differing_labels = count_nonzero(y_true - y_pred, axis=1)
else:
differing_labels = _count_nonzero(
y_true - y_pred, xp=xp, device=device, axis=1
)
score = xp.asarray(differing_labels == 0, device=device)
.

Do you think it's fine for me to update that section here, or should I open a followup?

@OmarManzoor
Copy link
Contributor

Do you think it's fine for me to update that section here, or should I open a followup?

Yes sure, I think it would be nice to refactor that as well considering this applies there. We can run the CUDA CI after you push your change to ensure everything is working as expected.

Copy link
Contributor

@OmarManzoor OmarManzoor left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM. Thank you @lithomas1

Copy link
Member

@virchan virchan left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM! Thanks @lithomas1!

@OmarManzoor
Copy link
Contributor

@ogrisel @adrinjalali Would it be possible for you run this on a Mac with mps so that we can merge this PR?

@adrinjalali
Copy link
Member

Unfortunately I don't have a mac machine. Maybe @adam2392 does?

@adam2392
Copy link
Member

adam2392 commented Mar 7, 2025

Sure I can try running this tonight. I have a M1

@lithomas1
Copy link
Contributor Author

@adam2392
Were you able to run this on MPS?

@adam2392
Copy link
Member

Sorry trying to get this to work on my Mac M1, but for some reason when I run:

    xp, _, device = get_namespace_and_device(y_true, y_pred, sample_weight)
print(device)
> None

which is weird to me. I've installed scikit-learn on miniforge too

@adam2392
Copy link
Member

adam2392 commented Mar 16, 2025

So I think this function works for MPS, but I am not that familiar with the testing, and I cannot get the MPS device to get picked up. So I can't say forsure… I'm running the following to test and then adding a breakpoint to check if the device is using mps:0, which it is not.

PYTORCH_ENABLE_MPS_FALLBACK=1 pytest ./sklearn/metrics/tests/test_common.py::test_array_api_compliance

However, if I run this file manually, the output seems correct. Mind educating me @OmarManzoor @adrinjalali ?

import torch
import numpy as np
from sklearn import config_context
from sklearn.metrics import hamming_loss
from sklearn.datasets import make_multilabel_classification

# Check if MPS is available
if torch.backends.mps.is_available():
    device = torch.device("mps")
    print("MPS backend is available.")
else:
    device = torch.device("cpu")
    print("MPS backend is not available. Running on CPU.")

# Generate sample multi-label classification data
X, Y = make_multilabel_classification(n_samples=100, n_classes=5, random_state=42)

# Convert to PyTorch tensors and move to MPS device
Y_true = torch.tensor(Y, dtype=torch.float32).to(device)
Y_pred = (torch.rand_like(Y_true) > 0.5).float().to(device)  # Random binary predictions

try:
    # Compute Hamming loss
    with config_context(array_api_dispatch=True):
        loss = hamming_loss(Y_true, Y_pred)
    print(f"Hamming loss: {loss:.4f}")
except Exception as e:
    print(f"Error while computing Hamming loss: {e}")

> MPS backend is available.
> Hamming loss: 0.5000

@lithomas1
Copy link
Contributor Author

Thanks for giving this a run.
I tried checking on my Macbook Pro (note: I have a pre-M1 Mac), and the test seems to pass

sklearn/metrics/tests/test_common.py::test_array_api_compliance[hamming_loss-check_array_api_binary_classification_metric-numpy-None-None] PASSED                                                 [ 34%]
sklearn/metrics/tests/test_common.py::test_array_api_compliance[hamming_loss-check_array_api_binary_classification_metric-array_api_strict-None-None] PASSED                                      [ 34%]
sklearn/metrics/tests/test_common.py::test_array_api_compliance[hamming_loss-check_array_api_binary_classification_metric-cupy-None-None] SKIPPED (cupy is not installed: not checking array_...) [ 34%]
sklearn/metrics/tests/test_common.py::test_array_api_compliance[hamming_loss-check_array_api_binary_classification_metric-torch-cpu-float64] PASSED                                               [ 34%]
sklearn/metrics/tests/test_common.py::test_array_api_compliance[hamming_loss-check_array_api_binary_classification_metric-torch-cpu-float32] PASSED                                               [ 34%]
sklearn/metrics/tests/test_common.py::test_array_api_compliance[hamming_loss-check_array_api_binary_classification_metric-torch-cuda-float64] SKIPPED (PyTorch test requires cuda, which is n...) [ 35%]
sklearn/metrics/tests/test_common.py::test_array_api_compliance[hamming_loss-check_array_api_binary_classification_metric-torch-cuda-float32] SKIPPED (PyTorch test requires cuda, which is n...) [ 35%]
sklearn/metrics/tests/test_common.py::test_array_api_compliance[hamming_loss-check_array_api_binary_classification_metric-torch-mps-float32] PASSED                                               [ 35%]
sklearn/metrics/tests/test_common.py::test_array_api_compliance[hamming_loss-check_array_api_multiclass_classification_metric-numpy-None-None] PASSED                                             [ 35%]
sklearn/metrics/tests/test_common.py::test_array_api_compliance[hamming_loss-check_array_api_multiclass_classification_metric-array_api_strict-None-None] PASSED                                  [ 36%]
sklearn/metrics/tests/test_common.py::test_array_api_compliance[hamming_loss-check_array_api_multiclass_classification_metric-cupy-None-None] SKIPPED (cupy is not installed: not checking ar...) [ 36%]
sklearn/metrics/tests/test_common.py::test_array_api_compliance[hamming_loss-check_array_api_multiclass_classification_metric-torch-cpu-float64] PASSED                                           [ 36%]
sklearn/metrics/tests/test_common.py::test_array_api_compliance[hamming_loss-check_array_api_multiclass_classification_metric-torch-cpu-float32] PASSED                                           [ 36%]
sklearn/metrics/tests/test_common.py::test_array_api_compliance[hamming_loss-check_array_api_multiclass_classification_metric-torch-cuda-float64] SKIPPED (PyTorch test requires cuda, which ...) [ 36%]
sklearn/metrics/tests/test_common.py::test_array_api_compliance[hamming_loss-check_array_api_multiclass_classification_metric-torch-cuda-float32] SKIPPED (PyTorch test requires cuda, which ...) [ 37%]
sklearn/metrics/tests/test_common.py::test_array_api_compliance[hamming_loss-check_array_api_multiclass_classification_metric-torch-mps-float32] PASSED                                           [ 37%]
sklearn/metrics/tests/test_common.py::test_array_api_compliance[hamming_loss-check_array_api_multilabel_classification_metric-numpy-None-None] PASSED                                             [ 37%]
sklearn/metrics/tests/test_common.py::test_array_api_compliance[hamming_loss-check_array_api_multilabel_classification_metric-array_api_strict-None-None] PASSED                                  [ 37%]
sklearn/metrics/tests/test_common.py::test_array_api_compliance[hamming_loss-check_array_api_multilabel_classification_metric-cupy-None-None] SKIPPED (cupy is not installed: not checking ar...) [ 37%]
sklearn/metrics/tests/test_common.py::test_array_api_compliance[hamming_loss-check_array_api_multilabel_classification_metric-torch-cpu-float64] PASSED                                           [ 38%]
sklearn/metrics/tests/test_common.py::test_array_api_compliance[hamming_loss-check_array_api_multilabel_classification_metric-torch-cpu-float32] PASSED                                           [ 38%]
sklearn/metrics/tests/test_common.py::test_array_api_compliance[hamming_loss-check_array_api_multilabel_classification_metric-torch-cuda-float64] SKIPPED (PyTorch test requires cuda, which ...) [ 38%]
sklearn/metrics/tests/test_common.py::test_array_api_compliance[hamming_loss-check_array_api_multilabel_classification_metric-torch-cuda-float32] SKIPPED (PyTorch test requires cuda, which ...) [ 38%]
sklearn/metrics/tests/test_common.py::test_array_api_compliance[hamming_loss-check_array_api_multilabel_classification_metric-torch-mps-float32] PASSED

The command I ran was

PYTORCH_ENABLE_MPS_FALLBACK=1 SCIPY_ARRAY_API=1 pytest sklearn/metrics/tests/test_common.py -k "array_api" -v

I'm also on an older torch version (2.2), which might also make a difference.

@adam2392
Copy link
Member

adam2392 commented Mar 17, 2025

I'm curious: Will the tests pass even if MPS is not used (and CPU is used), or does passing imply MPS was used correctly? Since you're not on a M1+ Mac, it seems the tests are passing even tho it's not testing MPS which seems like a bug to me.

I think the code works based on my snippet showing the hamming loss is computed using a MPS tensor from PyTorch. I'm just not sure if the tests are working as intended tho.

@lithomas1
Copy link
Contributor Author

I think MPS was used correctly (the Intel Mac I have has a discrete AMD GPU that has some MPS capabilities - I don't know if it is falling back for the hamming loss stuff though).

While running the metrics tests, I also did see GPU usage and some Metal processes (MTLCompilerService) when running on MPS, so at least something in there is using my GPU.

The device thing is very strange, though.

@virchan
Copy link
Member

virchan commented Mar 17, 2025

I'm curious: Will the tests pass even if MPS is not used (and CPU is used), or does passing imply MPS was used correctly? Since you're not on a M1+ Mac, it seems the tests are passing even tho it's not testing MPS which seems like a bug to me.

I think the code works based on my snippet showing the hamming loss is computed using a MPS tensor from PyTorch. I'm just not sure if the tests are working as intended tho.

@virchan virchan closed this Mar 17, 2025
@virchan virchan reopened this Mar 17, 2025
@virchan
Copy link
Member

virchan commented Mar 17, 2025

I'm curious: Will the tests pass even if MPS is not used (and CPU is used), or does passing imply MPS was used correctly?

If MPS is not used, pytest will skip the associated tests with one of the following messages:

Skipping MPS device test because PYTORCH_ENABLE_MPS_FALLBACK is not set.

or

MPS is not available because the current PyTorch install was not built with MPS enabled.

This is typically what I got when running the Array API tests on my local Windows machine.

@virchan
Copy link
Member

virchan commented Mar 17, 2025

xp, _, device = get_namespace_and_device(y_true, y_pred, sample_weight)
print(device)
None

This might be related to #30454, as we changed the _single_array_device function to return device as None instead of cpu when array_api_dispatch=False.

@lithomas1
Copy link
Contributor Author

xp, _, device = get_namespace_and_device(y_true, y_pred, sample_weight)
print(device)
None

What does xp show as here?
(I wonder if this is maybe because this wasn't ran in a config_context(array_api_dispatch=True) block?)

I initially got back None after forgetting to wrap stuff in config_context, but after wrapping with config_context I get

>>> from sklearn import config_context
>>> with config_context(array_api_dispatch=True):
...      get_namespace_and_device(torch.tensor([1,2,3]).to("mps:0"))
... 
(<module 'array_api_compat.torch' from '/Users/thomasli/opt/miniconda3/envs/scikit-learn/lib/python3.12/site-packages/array_api_compat/torch/__init__.py'>, True, device(type='mps', index=0))

@adam2392
Copy link
Member

Ah okay it was my fault. The metric call in the unit-test is always called once w/ numpy array and once with the relevant pytorch tensor

The second time indeed catches the mps:0 as expected, so this LGTM. Sorry for the confusion. But TIL :)

Copy link
Member

@adam2392 adam2392 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the PR @lithomas1!

@adam2392 adam2392 merged commit 8f167d2 into scikit-learn:main Mar 18, 2025
45 checks passed
@lithomas1 lithomas1 deleted the hamming-loss-array-api branch March 18, 2025 02:02
@lithomas1
Copy link
Contributor Author

Thanks for the reviews!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants