Skip to content
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

Add experimental metrics package following the array API standard #499

Merged
merged 42 commits into from
Nov 14, 2023
Merged
Show file tree
Hide file tree
Changes from 29 commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
ae4d5d2
update dependencies
fcogidi Oct 20, 2023
0a11daa
Add lightning dependency for testing distributed metrics
fcogidi Nov 3, 2023
f9f0ab2
Merge branch 'main' into fco/init_array_api_adoption
fcogidi Nov 3, 2023
7111eae
Update numpy version and add torchmetrics and cupy
fcogidi Nov 3, 2023
b2cb752
Merge branch 'main' into fco/init_array_api_adoption
fcogidi Nov 3, 2023
b58c2d0
Add optional module import utility and tests
fcogidi Nov 3, 2023
ebc4488
Add utilities for experimental metrics module
fcogidi Nov 3, 2023
fe6b1ca
Add distributed backends package
fcogidi Nov 6, 2023
5f93feb
Add remove_ignore_index utility function to ops.py
fcogidi Nov 7, 2023
d628dba
Add base class for metrics
fcogidi Nov 7, 2023
9b4a735
Update dependencies and fix tests involving cupy arrays
fcogidi Nov 7, 2023
f0367d1
Ignore import errors for cupy and torch modules in
fcogidi Nov 7, 2023
500cab3
Fix import statement in test_metric.py
fcogidi Nov 7, 2023
b59a98e
Add torchmetrics as dependency to tests and fix tests
fcogidi Nov 8, 2023
cfa72fc
Fix code ops and update test
fcogidi Nov 8, 2023
452e16c
Add confusion matrix
fcogidi Nov 8, 2023
81c4bad
Fix import statements
fcogidi Nov 8, 2023
091394e
Refactor flatten function to use copy parameter.
fcogidi Nov 8, 2023
4bd842f
Disable mypy 'no-any-return' error code in mpi4py backend module
fcogidi Nov 8, 2023
549248e
Add MPI and cupy support for integration tests
fcogidi Nov 8, 2023
8d958c1
Fix MPI installation in integration tests workflow
fcogidi Nov 8, 2023
0b84999
Check openmpi installation
fcogidi Nov 8, 2023
5094c21
[WIP] integration test workflow fix
fcogidi Nov 8, 2023
83c671e
[WIP] integration test workflow fix
fcogidi Nov 8, 2023
50a0e16
[WIP] integration test workflow fix
fcogidi Nov 8, 2023
660b3b5
Remove unnecessary dependency installation and
fcogidi Nov 8, 2023
0a94fba
Add cupy to test group in poetry
fcogidi Nov 8, 2023
17f7f5d
use pip instead of poetry to install cupy
fcogidi Nov 8, 2023
ef81f0c
Merge branch 'main' into fco/init_array_api_adoption
fcogidi Nov 8, 2023
93dba63
Add openmpi installation check and export paths
fcogidi Nov 9, 2023
cad9859
update integration tests workflow config
fcogidi Nov 9, 2023
7856c07
use conftest for `metrics` tests
fcogidi Nov 9, 2023
87ca102
Add function to get open port for PyTorch DDP setup
fcogidi Nov 9, 2023
7bef9eb
Add tests for torch distributed backend
fcogidi Nov 9, 2023
16c59de
Add __init__.py file for distributed backend tests
fcogidi Nov 9, 2023
7d307d0
Fix flatten_seq function to handle single character strings.
fcogidi Nov 9, 2023
67982d8
Add mpi4py dependency for integration tests
fcogidi Nov 9, 2023
fad6941
Add MPICC environment variable to mpi4py installation.
fcogidi Nov 9, 2023
8a1e8d3
Restructure package for distributed backends
fcogidi Nov 10, 2023
71a42ce
cleanup + improve tests
fcogidi Nov 14, 2023
54af88f
Mark integration test for sync on compute list state
fcogidi Nov 14, 2023
b80384a
mark as integration test
fcogidi Nov 14, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/integration_tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ jobs:
poetry env use '3.10'
source $(poetry env info --path)/bin/activate
poetry install --with test
pip install cupy-cuda12x
coverage run -m pytest -m integration_test && coverage xml && coverage report -m
- name: Upload coverage to Codecov
uses: Wandalen/wretry.action@v1.0.36
Expand Down
6 changes: 6 additions & 0 deletions cyclops/evaluate/metrics/experimental/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
"""Array-API-compatible metrics."""
from cyclops.evaluate.metrics.experimental.confusion_matrix import (
BinaryConfusionMatrix,
fcogidi marked this conversation as resolved.
Show resolved Hide resolved
MulticlassConfusionMatrix,
MultilabelConfusionMatrix,
)
383 changes: 383 additions & 0 deletions cyclops/evaluate/metrics/experimental/confusion_matrix.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,383 @@
"""Confusion matrix."""
from typing import Any, List, Optional, Tuple, Union

from cyclops.evaluate.metrics.experimental.functional.confusion_matrix import (
_binary_confusion_matrix_compute,
_binary_confusion_matrix_format_inputs,
_binary_confusion_matrix_update_state,
_binary_confusion_matrix_validate_args,
_binary_confusion_matrix_validate_arrays,
_multiclass_confusion_matrix_compute,
_multiclass_confusion_matrix_format_inputs,
_multiclass_confusion_matrix_update_state,
_multiclass_confusion_matrix_validate_args,
_multiclass_confusion_matrix_validate_arrays,
_multilabel_confusion_matrix_compute,
_multilabel_confusion_matrix_format_inputs,
_multilabel_confusion_matrix_update_state,
_multilabel_confusion_matrix_validate_args,
_multilabel_confusion_matrix_validate_arrays,
)
from cyclops.evaluate.metrics.experimental.metric import Metric
from cyclops.evaluate.metrics.experimental.utils.ops import dim_zero_cat
from cyclops.evaluate.metrics.experimental.utils.typing import Array


class _AbstractConfusionMatrix(Metric):
"""Base class defining the common interface for confusion matrix classes."""

tp: Union[Array, List[Array]]
fp: Union[Array, List[Array]]
tn: Union[Array, List[Array]]
fn: Union[Array, List[Array]]

def _create_state(
self,
size: int = 1,
) -> None:
"""Create the state variables.

Parameters
----------
size : int
The size of the default Array to create for the state variables.

Returns
-------
None

Raises
------
RuntimeError
If ``size`` is not greater than 0.

"""
if size <= 0:
raise RuntimeError(

Check warning on line 56 in cyclops/evaluate/metrics/experimental/confusion_matrix.py

View check run for this annotation

Codecov / codecov/patch

cyclops/evaluate/metrics/experimental/confusion_matrix.py#L56

Added line #L56 was not covered by tests
f"Expected `size` to be greater than 0, got {size}.",
)
dist_reduce_fn = "sum"

def default(xp: Any) -> Array:
return xp.zeros(shape=size, dtype=xp.int64)

self.add_state_factory("tp", default, dist_reduce_fn=dist_reduce_fn) # type: ignore
self.add_state_factory("fp", default, dist_reduce_fn=dist_reduce_fn) # type: ignore
self.add_state_factory("tn", default, dist_reduce_fn=dist_reduce_fn) # type: ignore
self.add_state_factory("fn", default, dist_reduce_fn=dist_reduce_fn) # type: ignore

def _update_state(self, tp: Array, fp: Array, tn: Array, fn: Array) -> None:
"""Update the state variables."""
self.tp += tp
self.fp += fp
self.tn += tn
self.fn += fn

def _final_state(self) -> Tuple[Array, Array, Array, Array]:
"""Return the final state variables."""
tp = dim_zero_cat(self.tp)
fp = dim_zero_cat(self.fp)
tn = dim_zero_cat(self.tn)
fn = dim_zero_cat(self.fn)
return tp, fp, tn, fn


class BinaryConfusionMatrix(
_AbstractConfusionMatrix,
registry_key="binary_confusion_matrix",
):
"""Confusion matrix for binary classification tasks.

Parameters
----------
threshold : float, default=0.5
The threshold value to use when binarizing the inputs.
normalize : {'true', 'pred', 'all', 'none' None}, optional, default=None
Normalizes confusion matrix over the true (rows), predicted (columns)
samples or all samples. If `None` or `'none'`, confusion matrix will
not be normalized.
ignore_index : int, optional, default=None
Specifies a target value that is ignored and does not contribute to
the confusion matrix. If `None`, all values are used.
**kwargs : Any
Additional keyword arguments common to all metrics.

Examples
--------
>>> import numpy.array_api as np
>>> from cyclops.evaluate.metrics.experimental import BinaryConfusionMatrix
>>> target = np.asarray([0, 1, 0, 1, 0, 1])
>>> preds = np.asarray([0, 0, 1, 1, 0, 1])
>>> metric = BinaryConfusionMatrix()
>>> metric(target, preds)
Array([[2, 1],
[1, 2]], dtype=int64)
>>> target = np.asarray([0, 1, 0, 1, 0, 1])
>>> preds = np.asarray([0.11, 0.22, 0.84, 0.73, 0.33, 0.92])
>>> metric = BinaryConfusionMatrix()
>>> metric(target, preds)
Array([[2, 1],
[1, 2]], dtype=int32)
>>> target = np.asarray([[[0, 1], [1, 0], [0, 1]], [[1, 1], [0, 0], [1, 0]]])
>>> preds = np.asarray([[[0.59, 0.91], [0.91, 0.99], [0.63, 0.04]],
... [[0.38, 0.04], [0.86, 0.780], [0.45, 0.37]]])

"""

def __init__(
self,
threshold: float = 0.5,
normalize: Optional[str] = None,
ignore_index: Optional[int] = None,
**kwargs: Any,
) -> None:
"""Initialize the class."""
super().__init__(**kwargs)

_binary_confusion_matrix_validate_args(
threshold=threshold,
normalize=normalize,
ignore_index=ignore_index,
)

self.threshold = threshold
self.normalize = normalize
self.ignore_index = ignore_index

self._create_state(size=1)

def update_state(self, target: Array, preds: Array) -> None:
"""Update the state variables."""
_binary_confusion_matrix_validate_arrays(
target,
preds,
ignore_index=self.ignore_index,
)
target, preds = _binary_confusion_matrix_format_inputs(
target,
preds,
threshold=self.threshold,
ignore_index=self.ignore_index,
)

tn, fp, fn, tp = _binary_confusion_matrix_update_state(target, preds)
self._update_state(tp, fp, tn, fn)

def compute(self) -> Array:
"""Compute the confusion matrix."""
tp, fp, tn, fn = self._final_state()
return _binary_confusion_matrix_compute(
tp=tp,
fp=fp,
tn=tn,
fn=fn,
normalize=self.normalize,
)


class MulticlassConfusionMatrix(Metric, registry_key="multiclass_confusion_matrix"):
"""Confusion matrix for multiclass classification tasks.

Parameters
----------
num_classes : int
The number of classes.
normalize : {'true', 'pred', 'all', 'none' None}, optional, default=None
Normalizes confusion matrix over the true (rows), predicted (columns)
samples or all samples. If `None` or `'none'`, confusion matrix will
not be normalized.
ignore_index : int, optional, default=None
Specifies a target value that is ignored and does not contribute to
the confusion matrix. If `None`, all values are used.
**kwargs : Any
Additional keyword arguments common to all metrics.

Examples
--------
>>> import numpy.array_api as np
>>> from cyclops.evaluate.metrics.experimental import MulticlassConfusionMatrix
>>> target = np.asarray([2, 1, 0, 0])
>>> preds = np.asarray([2, 1, 0, 1])
>>> metric = MulticlassConfusionMatrix(num_classes=3)
>>> metric(target, preds)
Array([[1, 1, 0],
[0, 1, 0],
[0, 0, 1]], dtype=int64)
>>> target = np.asarray([2, 1, 0, 0])
>>> preds = np.asarray([[0.16, 0.26, 0.58],
... [0.22, 0.61, 0.17],
... [0.71, 0.09, 0.20],
... [0.05, 0.82, 0.13]])
>>> metric = MulticlassConfusionMatrix(num_classes=3)
>>> metric(target, preds)
Array([[1, 1, 0],
[0, 1, 0],
[0, 0, 1]], dtype=int64)

"""

confmat: Array

def __init__(
self,
num_classes: int,
normalize: Optional[str] = None,
ignore_index: Optional[Union[int, Tuple[int]]] = None,
**kwargs: Any,
) -> None:
"""Initialize the class."""
super().__init__(**kwargs)

_multiclass_confusion_matrix_validate_args(
num_classes,
normalize=normalize,
ignore_index=ignore_index,
)

self.num_classes = num_classes
self.normalize = normalize
self.ignore_index = ignore_index

dist_reduce_fn = "sum"

def default(xp: Any) -> Array:
return xp.zeros((num_classes,) * 2, dtype=xp.int64)

self.add_state_factory("confmat", default, dist_reduce_fn=dist_reduce_fn) # type: ignore

def update_state(self, target: Array, preds: Array) -> None:
"""Update the state variable."""
_multiclass_confusion_matrix_validate_arrays(
target,
preds,
self.num_classes,
ignore_index=self.ignore_index,
)
target, preds = _multiclass_confusion_matrix_format_inputs(
target,
preds,
ignore_index=self.ignore_index,
)
confmat = _multiclass_confusion_matrix_update_state(
target,
preds,
self.num_classes,
)

self.confmat += confmat

def compute(self) -> Array:
"""Compute the confusion matrix."""
confmat = self.confmat
return _multiclass_confusion_matrix_compute(
confmat,
normalize=self.normalize,
)


class MultilabelConfusionMatrix(
_AbstractConfusionMatrix,
registry_key="multilabel_confusion_matrix",
):
"""Confusion matrix for multilabel classification tasks.

Parameters
----------
num_labels : int
The number of labels.
threshold : float, default=0.5
The threshold value to use when binarizing the inputs.
normalize : {'true', 'pred', 'all', 'none' None}, optional, default=None
Normalizes confusion matrix over the true (rows), predicted (columns)
samples or all samples. If `None` or `'none'`, confusion matrix will
not be normalized.
ignore_index : int, optional, default=None
Specifies a target value that is ignored and does not contribute to
the confusion matrix. If `None`, all values are used.
**kwargs : Any
Additional keyword arguments common to all metrics.

Examples
--------
>>> import numpy.array_api as np
>>> from cyclops.evaluate.metrics.experimental import MultilabelConfusionMatrix
>>> target = np.asarray([[0, 1, 0], [1, 0, 1]])
>>> preds = np.asarray([[0, 0, 1], [1, 0, 1]])
>>> metric = MultilabelConfusionMatrix(num_labels=3)
>>> metric(target, preds)
Array([[[1, 0],
[0, 1]],

[[1, 0],
[1, 0]],

[[0, 1],
[0, 1]]], dtype=int64)
>>> target = np.asarray([[0, 1, 0], [1, 0, 1]])
>>> preds = np.asarray([[0.11, 0.22, 0.84], [0.73, 0.33, 0.92]])
>>> metric = MultilabelConfusionMatrix(num_labels=3)
>>> metric(target, preds)
Array([[[1, 0],
[0, 1]],

[[1, 0],
[1, 0]],

[[0, 1],
[0, 1]]], dtype=int64)

"""

def __init__(
self,
num_labels: int,
threshold: float = 0.5,
normalize: Optional[str] = None,
ignore_index: Optional[int] = None,
**kwargs: Any,
) -> None:
"""Initialize the class."""
super().__init__(**kwargs)

_multilabel_confusion_matrix_validate_args(
num_labels,
threshold=threshold,
normalize=normalize,
ignore_index=ignore_index,
)

self.num_labels = num_labels
self.threshold = threshold
self.normalize = normalize
self.ignore_index = ignore_index

self._create_state(size=num_labels)

def update_state(self, target: Array, preds: Array) -> None:
"""Update the state variables."""
_multilabel_confusion_matrix_validate_arrays(
target,
preds,
self.num_labels,
ignore_index=self.ignore_index,
)
target, preds = _multilabel_confusion_matrix_format_inputs(
target,
preds,
threshold=self.threshold,
ignore_index=self.ignore_index,
)
tn, fp, fn, tp = _multilabel_confusion_matrix_update_state(target, preds)
self._update_state(tp, fp, tn, fn)

def compute(self) -> Array:
"""Compute the confusion matrix."""
tp, fp, tn, fn = self._final_state()
return _multilabel_confusion_matrix_compute(
tp=tp,
fp=fp,
tn=tn,
fn=fn,
num_labels=self.num_labels,
normalize=self.normalize,
)
Loading