22from typing import Literal
33
44import torch
5+ import torch .nn .functional as F
56
7+ from ignite .exceptions import NotComputableError
8+ from ignite .metrics .confusion_matrix import ConfusionMatrix
69from ignite .metrics .epoch_metric import EpochMetric
10+ from ignite .metrics .metric import Metric , reinit__is_reduced
711
812
9- class CohenKappa (EpochMetric ):
10- """Compute different types of Cohen's Kappa: Non-Wieghted, Linear, Quadratic.
11- Accumulating predictions and the ground-truth during an epoch and applying
12- `sklearn.metrics.cohen_kappa_score <https://scikit-learn.org/stable/modules/
13- generated/sklearn.metrics.cohen_kappa_score.html>`_ .
13+ def _kappa_from_conf (conf : torch .Tensor , weights : Literal ["linear" , "quadratic" ] | None ) -> float :
14+ n = conf .sum ()
15+ if n == 0 :
16+ raise NotComputableError ("CohenKappa cannot be computed on an empty confusion matrix (n == 0)." )
17+
18+ n_classes = conf .shape [0 ]
19+
20+ if weights is None :
21+ p_o = conf .trace () / n
22+ row = conf .sum (dim = 1 )
23+ col = conf .sum (dim = 0 )
24+ p_e = (row * col ).sum () / (n * n )
25+ else :
26+ idx = torch .arange (n_classes , device = conf .device )
27+ if weights == "linear" :
28+ w = torch .abs (idx .unsqueeze (0 ) - idx .unsqueeze (1 )).to (dtype = conf .dtype )
29+ else :
30+ w = ((idx .unsqueeze (0 ) - idx .unsqueeze (1 )) ** 2 ).to (dtype = conf .dtype )
31+
32+ w = w / w .max ()
33+ p_o = 1 - (w * conf ).sum () / n
34+ row = conf .sum (dim = 1 )
35+ col = conf .sum (dim = 0 )
36+ expected = row .unsqueeze (1 ) * col .unsqueeze (0 ) / n
37+ p_e = 1 - (w * expected ).sum () / n
38+
39+ epsilon = 1e-9
40+ return ((p_o - p_e ) / (1 - p_e ).clamp (min = epsilon )).item ()
41+
42+
43+ def _cohen_kappa_score (
44+ y_pred : torch .Tensor ,
45+ y : torch .Tensor ,
46+ weights : Literal ["linear" , "quadratic" ] | None ,
47+ double_dtype : torch .dtype ,
48+ ) -> float :
49+ if y_pred .ndim > 1 or y .ndim > 1 :
50+ raise ValueError ("multilabel-indicator is not supported" )
51+
52+ num_classes = int (max (y_pred .max ().item (), y .max ().item ())) + 1
53+
54+ # Build the confusion matrix locally with plain tensor ops. We must NOT use the
55+ # ``ConfusionMatrix`` metric here: its ``compute()`` is wrapped with ``sync_all_reduce``
56+ # and this function runs on rank 0 only (see ``EpochMetric.compute``), so a collective
57+ # would deadlock the other ranks.
58+ y , y_pred = y .long (), y_pred .long ()
59+ indices = num_classes * y + y_pred
60+ conf = torch .bincount (indices , minlength = num_classes ** 2 )
61+ conf = conf .reshape (num_classes , num_classes ).to (dtype = double_dtype )
62+
63+ return _kappa_from_conf (conf , weights )
64+
65+
66+ class _CohenKappaEpochMetric (EpochMetric ):
67+ """CohenKappa backed by EpochMetric — infers num_classes dynamically from data."""
68+
69+ def __init__ (
70+ self ,
71+ weights : Literal ["linear" , "quadratic" ] | None ,
72+ output_transform : Callable ,
73+ device : str | torch .device ,
74+ skip_unrolling : bool ,
75+ check_compute_fn : bool ,
76+ ):
77+ super ().__init__ (
78+ # ``self._double_dtype`` (set in Metric.__init__, float32 on MPS / float64 otherwise)
79+ # is resolved lazily at compute time.
80+ compute_fn = lambda y_pred , y : _cohen_kappa_score (y_pred , y , weights , self ._double_dtype ),
81+ output_transform = output_transform ,
82+ check_compute_fn = check_compute_fn ,
83+ device = device ,
84+ skip_unrolling = skip_unrolling ,
85+ )
86+
87+ @reinit__is_reduced
88+ def update (self , output : tuple [torch .Tensor , torch .Tensor ]) -> None :
89+ y_pred , y = output [0 ].detach (), output [1 ].detach ()
90+
91+ if y_pred .ndim == 2 and y_pred .shape [1 ] == 1 :
92+ y_pred = y_pred .squeeze (dim = - 1 )
93+ if y .ndim == 2 and y .shape [1 ] == 1 :
94+ y = y .squeeze (dim = - 1 )
95+
96+ super ().update ((y_pred , y ))
97+
98+
99+ class _CohenKappaConfusionMatrix (Metric ):
100+ """CohenKappa backed by ConfusionMatrix — requires num_classes at construction time.
101+ Accumulates a running confusion matrix; no raw tensor buffering.
102+ """
103+
104+ _state_dict_all_req_keys = ("_cm" ,)
105+
106+ def __init__ (
107+ self ,
108+ num_classes : int ,
109+ weights : Literal ["linear" , "quadratic" ] | None ,
110+ output_transform : Callable ,
111+ device : str | torch .device ,
112+ skip_unrolling : bool ,
113+ ):
114+ self ._weights = weights
115+ self ._cm = ConfusionMatrix (
116+ num_classes = num_classes ,
117+ output_transform = output_transform ,
118+ device = device ,
119+ skip_unrolling = skip_unrolling ,
120+ )
121+ super ().__init__ (output_transform = output_transform , device = device , skip_unrolling = skip_unrolling )
122+
123+ @reinit__is_reduced
124+ def reset (self ) -> None :
125+ self ._cm .reset ()
126+
127+ @reinit__is_reduced
128+ def update (self , output : tuple [torch .Tensor , torch .Tensor ]) -> None :
129+ y_pred , y = output [0 ].detach (), output [1 ].detach ()
130+
131+ if y_pred .ndim == 2 and y_pred .shape [1 ] == 1 :
132+ y_pred = y_pred .squeeze (dim = - 1 )
133+ if y .ndim == 2 and y .shape [1 ] == 1 :
134+ y = y .squeeze (dim = - 1 )
135+
136+ if y_pred .ndim > 1 or y .ndim > 1 :
137+ raise ValueError ("multilabel-indicator is not supported" )
138+
139+ num_classes = self ._cm .num_classes
140+ y_pred_oh = F .one_hot (y_pred .long (), num_classes ).float ().to (self ._device )
141+ self ._cm .update ((y_pred_oh , y .long ().to (self ._device )))
142+
143+ def compute (self ) -> float :
144+ conf = self ._cm .compute ().to (dtype = self ._double_dtype )
145+ return _kappa_from_conf (conf , self ._weights )
146+
147+
148+ class CohenKappa (Metric ):
149+ """Compute different types of Cohen's Kappa: Non-Weighted, Linear, Quadratic.
150+
151+ When ``num_classes`` is provided, accumulates a running confusion matrix via
152+ :class:`~ignite.metrics.confusion_matrix.ConfusionMatrix` (memory-efficient, no raw tensor buffering).
153+ When ``num_classes`` is ``None`` (default), buffers predictions and targets via
154+ :class:`~ignite.metrics.EpochMetric` and infers the number of classes from data.
14155
15156 Args:
16157 output_transform: a callable that is used to transform the
@@ -19,19 +160,20 @@ class CohenKappa(EpochMetric):
19160 you want to compute the metric with respect to one of the outputs.
20161 weights: a string is used to define the type of Cohen's Kappa whether Non-Weighted or Linear
21162 or Quadratic. Default, None.
22- check_compute_fn: Default False. If True, `cohen_kappa_score
23- <https://scikit-learn.org/stable/modules/generated/sklearn.metrics.cohen_kappa_score.html>`_
24- is run on the first batch of data to ensure there are
25- no issues. User will be warned in case there are any issues computing the function.
163+ check_compute_fn: Default False. If True, the compute function is run on the first batch
164+ of data to ensure there are no issues. User will be warned in case there are any issues
165+ computing the function.
26166 device: optional device specification for internal storage.
27167 skip_unrolling: specifies whether output should be unrolled before being fed to update method. Should be
28168 true for multi-output model, for example, if ``y_pred`` contains multi-output as ``(y_pred_a, y_pred_b)``
29169 Alternatively, ``output_transform`` can be used to handle this.
170+ num_classes: number of classes. If provided, uses a running confusion matrix
171+ (memory-efficient). If ``None``, infers from data at compute time (backward-compatible default).
30172
31173 Examples:
32174 To use with ``Engine`` and ``process_function``, simply attach the metric instance to the engine.
33175 The output of the engine's ``process_function`` needs to be in the format of
34- ``(y_pred, y)`` or ``{'y_pred': y_pred, 'y': y, ...}``. If not, ``output_tranform `` can be added
176+ ``(y_pred, y)`` or ``{'y_pred': y_pred, 'y': y, ...}``. If not, ``output_transform `` can be added
35177 to the metric to transform the output into the form expected by the metric.
36178
37179 .. include:: defaults.rst
@@ -52,6 +194,10 @@ class CohenKappa(EpochMetric):
52194
53195 .. versionchanged:: 0.5.1
54196 ``skip_unrolling`` argument is added.
197+
198+ .. versionchanged:: 0.6.0
199+ Replaced scikit-learn dependency with a native PyTorch implementation.
200+ Added ``num_classes`` argument; routes to a running-confusion-matrix backend when provided.
55201 """
56202
57203 def __init__ (
@@ -61,28 +207,39 @@ def __init__(
61207 check_compute_fn : bool = False ,
62208 device : str | torch .device = torch .device ("cpu" ),
63209 skip_unrolling : bool = False ,
210+ num_classes : int | None = None ,
64211 ):
65- try :
66- from sklearn .metrics import cohen_kappa_score # noqa: F401
67- except ImportError :
68- raise ModuleNotFoundError ("This contrib module requires scikit-learn to be installed." )
69212 if weights not in (None , "linear" , "quadratic" ):
70213 raise ValueError ("Kappa Weighting type must be None or linear or quadratic." )
71214
72- # initialize weights
73215 self .weights : Literal ["linear" , "quadratic" ] | None = weights
74216
75- super ().__init__ (
76- self ._cohen_kappa_score ,
77- output_transform = output_transform ,
78- check_compute_fn = check_compute_fn ,
79- device = device ,
80- skip_unrolling = skip_unrolling ,
81- )
82-
83- def _cohen_kappa_score (self , y_targets : torch .Tensor , y_preds : torch .Tensor ) -> float :
84- from sklearn .metrics import cohen_kappa_score
85-
86- y_true = y_targets .cpu ().numpy ()
87- y_pred = y_preds .cpu ().numpy ()
88- return cohen_kappa_score (y_true , y_pred , weights = self .weights )
217+ if num_classes is not None :
218+ self ._impl : Metric = _CohenKappaConfusionMatrix (
219+ num_classes = num_classes ,
220+ weights = weights ,
221+ output_transform = output_transform ,
222+ device = device ,
223+ skip_unrolling = skip_unrolling ,
224+ )
225+ else :
226+ self ._impl = _CohenKappaEpochMetric (
227+ weights = weights ,
228+ output_transform = output_transform ,
229+ device = device ,
230+ skip_unrolling = skip_unrolling ,
231+ check_compute_fn = check_compute_fn ,
232+ )
233+
234+ super ().__init__ (output_transform = output_transform , device = device , skip_unrolling = skip_unrolling )
235+
236+ @reinit__is_reduced
237+ def reset (self ) -> None :
238+ self ._impl .reset ()
239+
240+ @reinit__is_reduced
241+ def update (self , output : tuple [torch .Tensor , torch .Tensor ]) -> None :
242+ self ._impl .update (output )
243+
244+ def compute (self ) -> float :
245+ return self ._impl .compute ()
0 commit comments