Skip to content

kirad.ordinal_thresholding

kirad.ordinal_thresholding

Tune ordinal thresholds that turn continuous scores into severity classes.

Stage: thresholds/metrics. After each validation epoch base_models tunes six threshold sets (accuracy, absolute_error, macro_recall, macro_precision, macro_f1 by DP; coral from the CORAL biases), stores them as buffers in the checkpoint and reuses them on test.

OrdinalThresholding

Bases: Module

Base module: K-1 sorted thresholds (buffer) that map scores to K ordinal classes.

Basic 1d thresholding logic.

Source code in kirad/ordinal_thresholding.py
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
class OrdinalThresholding(torch.nn.Module):
    """Base module: K-1 sorted thresholds (buffer) that map scores to K ordinal classes.

    Basic 1d thresholding logic.
    """

    def __init__(self, num_classes: int):
        """Create num_classes - 1 zero thresholds as a buffer (saved in the checkpoint).

        Init thresholding module with the specified number of classes (one more than the number of thresholds)."""
        super().__init__()
        self.num_classes = num_classes
        self.register_buffer("thresholds", torch.zeros(num_classes - 1))
        self.thresholds: torch.Tensor

    def is_valid(self) -> bool:
        """Check whether the thresholds are monotone non-decreasing."""
        return all(torch.greater_equal(self.thresholds[1:], self.thresholds[:-1]))

    def forward(self, scores) -> torch.Tensor:
        """Class index per score = number of thresholds strictly below it (searchsorted).

        Find which thresholds each score lies between.
        Note: a score equal to a threshold falls in the lower class.
        """
        return torch.searchsorted(self.thresholds, scores)

    def tune_thresholds(
        self,
        *,
        scores: torch.Tensor,
        labels: torch.Tensor,
        available_thresholds: Optional[torch.Tensor] = None,
    ) -> torch.Tensor:
        """Tune thresholds on validation scores/labels; no-op here, overridden by subclasses.

        Adapt the thresholds to the given data.

        This is essentially an abstract method, but for testing purposes it's helpful to be able to instantiate the
        class with a no-op version.

        Parameters
        ----------
        scores : a vector of `float` scores for each example in the validation set
        labels : a vector of `int` labels having the same shape as `scores` containing the corresponding labels
        available_thresholds : a vector of `float` score values over which to optimize choice of thresholds;
            `None`, then thresholds between every score in the validation set are allowed. +/- inf are always allowed.

        Returns
        -------
        scalar `float` mean cost on the validation set using optimal thresholds

        """

num_classes = num_classes instance-attribute

thresholds instance-attribute

__init__(num_classes)

Create num_classes - 1 zero thresholds as a buffer (saved in the checkpoint).

Init thresholding module with the specified number of classes (one more than the number of thresholds).

Source code in kirad/ordinal_thresholding.py
21
22
23
24
25
26
27
28
def __init__(self, num_classes: int):
    """Create num_classes - 1 zero thresholds as a buffer (saved in the checkpoint).

    Init thresholding module with the specified number of classes (one more than the number of thresholds)."""
    super().__init__()
    self.num_classes = num_classes
    self.register_buffer("thresholds", torch.zeros(num_classes - 1))
    self.thresholds: torch.Tensor

is_valid()

Check whether the thresholds are monotone non-decreasing.

Source code in kirad/ordinal_thresholding.py
30
31
32
def is_valid(self) -> bool:
    """Check whether the thresholds are monotone non-decreasing."""
    return all(torch.greater_equal(self.thresholds[1:], self.thresholds[:-1]))

forward(scores)

Class index per score = number of thresholds strictly below it (searchsorted).

Find which thresholds each score lies between. Note: a score equal to a threshold falls in the lower class.

Source code in kirad/ordinal_thresholding.py
34
35
36
37
38
39
40
def forward(self, scores) -> torch.Tensor:
    """Class index per score = number of thresholds strictly below it (searchsorted).

    Find which thresholds each score lies between.
    Note: a score equal to a threshold falls in the lower class.
    """
    return torch.searchsorted(self.thresholds, scores)

tune_thresholds(*, scores, labels, available_thresholds=None)

Tune thresholds on validation scores/labels; no-op here, overridden by subclasses.

Adapt the thresholds to the given data.

This is essentially an abstract method, but for testing purposes it's helpful to be able to instantiate the class with a no-op version.

Parameters:

Name Type Description Default
scores a vector of `float` scores for each example in the validation set
required
labels a vector of `int` labels having the same shape as `scores` containing the corresponding labels
required
available_thresholds a vector of `float` score values over which to optimize choice of thresholds;

None, then thresholds between every score in the validation set are allowed. +/- inf are always allowed.

None

Returns:

Type Description
scalar `float` mean cost on the validation set using optimal thresholds
Source code in kirad/ordinal_thresholding.py
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
def tune_thresholds(
    self,
    *,
    scores: torch.Tensor,
    labels: torch.Tensor,
    available_thresholds: Optional[torch.Tensor] = None,
) -> torch.Tensor:
    """Tune thresholds on validation scores/labels; no-op here, overridden by subclasses.

    Adapt the thresholds to the given data.

    This is essentially an abstract method, but for testing purposes it's helpful to be able to instantiate the
    class with a no-op version.

    Parameters
    ----------
    scores : a vector of `float` scores for each example in the validation set
    labels : a vector of `int` labels having the same shape as `scores` containing the corresponding labels
    available_thresholds : a vector of `float` score values over which to optimize choice of thresholds;
        `None`, then thresholds between every score in the validation set are allowed. +/- inf are always allowed.

    Returns
    -------
    scalar `float` mean cost on the validation set using optimal thresholds

    """

CORALThresholding

Bases: OrdinalThresholding

Thresholds = -CORAL bias at the chosen cutoff indices (learned on train, no tuning).

Thresholding using the biases learned on the TRAINING set from the CORAL loss.

Source code in kirad/ordinal_thresholding.py
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
class CORALThresholding(OrdinalThresholding):
    """Thresholds = -CORAL bias at the chosen cutoff indices (learned on train, no tuning).

    Thresholding using the biases learned on the TRAINING set from the CORAL loss.
    """

    def __init__(self, coral_loss: CORALLoss, threshold_indices: Sequence[int]):
        """Keep a reference to the CORAL biases and the cutoff indices to read.

        Init module taking thresholds at specified indices from a given CORALLoss instance
        """
        super().__init__(len(threshold_indices) + 1)
        self.biases = coral_loss.biases
        self.threshold_indices = torch.tensor(threshold_indices)

    def tune_thresholds(
        self,
        *,
        scores: torch.Tensor,
        labels: torch.Tensor,
        available_thresholds: Optional[torch.Tensor] = None,
    ):
        """Copy -biases[threshold_indices] from the CORAL loss; scores and labels are ignored.

        Ignore the data, pull the specified biases from the CORAL loss, and multiply by -1 to get thresholds.
        """
        self.thresholds[:] = -torch.gather(
            self.biases, 0, self.threshold_indices.to(self.biases.device)
        )

biases = coral_loss.biases instance-attribute

threshold_indices = torch.tensor(threshold_indices) instance-attribute

__init__(coral_loss, threshold_indices)

Keep a reference to the CORAL biases and the cutoff indices to read.

Init module taking thresholds at specified indices from a given CORALLoss instance

Source code in kirad/ordinal_thresholding.py
76
77
78
79
80
81
82
83
def __init__(self, coral_loss: CORALLoss, threshold_indices: Sequence[int]):
    """Keep a reference to the CORAL biases and the cutoff indices to read.

    Init module taking thresholds at specified indices from a given CORALLoss instance
    """
    super().__init__(len(threshold_indices) + 1)
    self.biases = coral_loss.biases
    self.threshold_indices = torch.tensor(threshold_indices)

tune_thresholds(*, scores, labels, available_thresholds=None)

Copy -biases[threshold_indices] from the CORAL loss; scores and labels are ignored.

Ignore the data, pull the specified biases from the CORAL loss, and multiply by -1 to get thresholds.

Source code in kirad/ordinal_thresholding.py
85
86
87
88
89
90
91
92
93
94
95
96
97
98
def tune_thresholds(
    self,
    *,
    scores: torch.Tensor,
    labels: torch.Tensor,
    available_thresholds: Optional[torch.Tensor] = None,
):
    """Copy -biases[threshold_indices] from the CORAL loss; scores and labels are ignored.

    Ignore the data, pull the specified biases from the CORAL loss, and multiply by -1 to get thresholds.
    """
    self.thresholds[:] = -torch.gather(
        self.biases, 0, self.threshold_indices.to(self.biases.device)
    )

OptimalOrdinalThresholdingViaDynamicProgramming

Bases: OrdinalThresholding, ABC

Base for DP threshold tuning: pick K-1 thresholds from candidates to optimize a metric.

Super-class for general dynamic programming implementations of ordinal threshold tuning.

Subclasses implement different ways of computing the mean cost and corresponding DP step.

Source code in kirad/ordinal_thresholding.py
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
class OptimalOrdinalThresholdingViaDynamicProgramming(OrdinalThresholding, ABC):
    """Base for DP threshold tuning: pick K-1 thresholds from candidates to optimize a metric.

    Super-class for general dynamic programming implementations of ordinal threshold tuning.

    Subclasses implement different ways of computing the mean cost and corresponding DP step.

    """

    direction: Literal["min", "max"]  # provided by subclasses

    def __init__(self, num_classes: int):
        """Check that the subclass set `direction` to "min" or "max"."""
        super().__init__(num_classes=num_classes)
        if self.direction not in ("min", "max"):
            raise ValueError(
                f"Got direction {self.direction!r}, expected 'min' or 'max'."
            )

    @abstractmethod
    def mean_cost(
        self, *, labels: torch.Tensor, preds: int | torch.Tensor
    ) -> torch.Tensor:
        """Compute the mean cost of assigning label(s) `preds` when the ground truth is `labels`."""

    def best_constant_output_classifier(self, labels: torch.Tensor):
        """Best metric of a constant-class predictor and that class (baseline_const_* in logs).

        Find the optimal mean cost of a constant-output classifier for given `labels` and the associated constant.
        """
        # Pick min or max: is a lower or higher metric better?
        if self.direction == "min":
            optimize = torch.min
        else:
            optimize = torch.max
        # Score every constant-class predictor; keep best value and its class.
        optimum = optimize(
            torch.tensor(
                [
                    self.mean_cost(labels=labels, preds=c)
                    for c in range(self.num_classes)
                ],
                device=labels.device,
            ),
            0,
        )
        return optimum.values, optimum.indices

    @abstractmethod
    def dp_step(
        self,
        c_idx: int,
        *,
        scores: torch.Tensor,
        labels: torch.Tensor,
        available_thresholds: torch.Tensor,
        prev_cost: Optional[torch.Tensor] = None,
    ) -> (torch.Tensor, Optional[torch.Tensor]):
        """One DP step: best cost of classes <= c_idx for each candidate upper threshold of c_idx.

        Given optimal cost `prev_cost` of classes < `c_idx`, optimize cost of `c_idx` as a function of threshold.

        Arguments
        ---------
            c_idx : current class index
            scores, labels, available_thresholds : see `tune_thresholds`
            prev_cost (optional float tensor) : optimal cost of classes < `c_idx` as a function of upper threshold
                for class `c_idx - 1`; ignored if `c_idx == 0`

        Returns
        -------
            cost: `cost[i]` is for choosing upper threshold of class `c_idx` equal to `available_thresholds[i]`
                when thresholds for lower classes are chosen optimally
            indices : to achieve `cost[i]`, optimal upper threshold for class `c_idx - 1` is
                `available_thresholds[indices[i]]`; `None` if `c_idx == 0`

        """

    def tune_thresholds(
        self,
        *,
        scores: torch.Tensor,
        labels: torch.Tensor,
        available_thresholds: Optional[torch.Tensor] = None,
    ) -> torch.Tensor:
        """Set thresholds by DP over midpoints of unique scores (+/-inf); return the best metric.

        Set `self.thresholds` to optimize mean cost of given `scores` and `labels`.

        Arguments
        ---------
            scores (1d float tensor) : scores of examples on tuning dataset
            labels (1d int tensor) : labels in {0, ..., self.num_classes - 1} of same shape as scores
            available_thresholds (optional 1d float tensor) : thresholds which will be considered when tuning.
                +/-inf will be added automatically to ensure all examples are classified. If omitted, will
                insert thresholds between each element of sorted(unique(scores)).

        Returns
        -------
            float tensor : optimal mean cost achieved on the provided dataset at the tuned `self.thresholds`

        """
        # Candidate thresholds: given ones, else midpoints of unique scores.
        inf = torch.tensor([torch.inf], device=scores.device)
        if available_thresholds is None:  # use all possible thresholds
            unique_scores = torch.unique(scores)
            available_thresholds = (unique_scores[:-1] + unique_scores[1:]) / 2.0
        # Always allow some classes to be omitted entirely by setting thresholds to +/- inf.
        # This simplifies the algorithm and also guarantees that the baseline constant-output
        # classifiers are feasible choices for tuning, which is needed to assure that the
        # optimum is at least as good as a constant-output classifier.
        available_thresholds = torch.concatenate(
            [
                -inf,
                available_thresholds,
                inf,
            ]
        )
        # Backpointers: best lower-threshold index per middle class/candidate.
        indices = torch.empty(
            (self.num_classes - 2, len(available_thresholds)),
            dtype=torch.int,
            device=scores.device,
        )

        # cost[j] = optimal total cost of items assigned pred <= c if the
        # threshold between class c and c+1 is available_thresholds[j] (by appropriate choice of lower thresholds).
        cost, _ = self.dp_step(
            c_idx=0,
            scores=scores,
            labels=labels,
            available_thresholds=available_thresholds,
        )
        # Middle classes: extend the DP, storing argmax backpointers.
        for c in range(1, self.num_classes - 1):
            cost, indices[c - 1, :] = self.dp_step(
                c_idx=c,
                scores=scores,
                labels=labels,
                available_thresholds=available_thresholds,
                prev_cost=cost,
            )
        # Last class: keep only the best final cost and its threshold index.
        cost, best_index = self.dp_step(
            c_idx=self.num_classes - 1,
            scores=scores,
            labels=labels,
            available_thresholds=available_thresholds,
            prev_cost=cost,
        )
        # The DP maximized the negated cost; flip back to the real cost.
        if self.direction == "min":
            cost *= -1

        # Follow DP path backwards to find thresholds which optimized cost
        self.thresholds[self.num_classes - 2] = available_thresholds[
            best_index
        ]  # final threshold
        for c in range(self.num_classes - 3, -1, -1):  # counting down to zero
            best_index = indices[c, best_index.long()]
            self.thresholds[c] = available_thresholds[best_index.long()]

        return cost

direction instance-attribute

__init__(num_classes)

Check that the subclass set direction to "min" or "max".

Source code in kirad/ordinal_thresholding.py
112
113
114
115
116
117
118
def __init__(self, num_classes: int):
    """Check that the subclass set `direction` to "min" or "max"."""
    super().__init__(num_classes=num_classes)
    if self.direction not in ("min", "max"):
        raise ValueError(
            f"Got direction {self.direction!r}, expected 'min' or 'max'."
        )

mean_cost(*, labels, preds) abstractmethod

Compute the mean cost of assigning label(s) preds when the ground truth is labels.

Source code in kirad/ordinal_thresholding.py
120
121
122
123
124
@abstractmethod
def mean_cost(
    self, *, labels: torch.Tensor, preds: int | torch.Tensor
) -> torch.Tensor:
    """Compute the mean cost of assigning label(s) `preds` when the ground truth is `labels`."""

best_constant_output_classifier(labels)

Best metric of a constant-class predictor and that class (baseline_const_* in logs).

Find the optimal mean cost of a constant-output classifier for given labels and the associated constant.

Source code in kirad/ordinal_thresholding.py
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
def best_constant_output_classifier(self, labels: torch.Tensor):
    """Best metric of a constant-class predictor and that class (baseline_const_* in logs).

    Find the optimal mean cost of a constant-output classifier for given `labels` and the associated constant.
    """
    # Pick min or max: is a lower or higher metric better?
    if self.direction == "min":
        optimize = torch.min
    else:
        optimize = torch.max
    # Score every constant-class predictor; keep best value and its class.
    optimum = optimize(
        torch.tensor(
            [
                self.mean_cost(labels=labels, preds=c)
                for c in range(self.num_classes)
            ],
            device=labels.device,
        ),
        0,
    )
    return optimum.values, optimum.indices

dp_step(c_idx, *, scores, labels, available_thresholds, prev_cost=None) abstractmethod

One DP step: best cost of classes <= c_idx for each candidate upper threshold of c_idx.

Given optimal cost prev_cost of classes < c_idx, optimize cost of c_idx as a function of threshold.

Arguments
c_idx : current class index
scores, labels, available_thresholds : see `tune_thresholds`
prev_cost (optional float tensor) : optimal cost of classes < `c_idx` as a function of upper threshold
    for class `c_idx - 1`; ignored if `c_idx == 0`

Returns:

Type Description
cost: `cost[i]` is for choosing upper threshold of class `c_idx` equal to `available_thresholds[i]`
when thresholds for lower classes are chosen optimally

indices : to achieve cost[i], optimal upper threshold for class c_idx - 1 is available_thresholds[indices[i]]; None if c_idx == 0

Source code in kirad/ordinal_thresholding.py
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
@abstractmethod
def dp_step(
    self,
    c_idx: int,
    *,
    scores: torch.Tensor,
    labels: torch.Tensor,
    available_thresholds: torch.Tensor,
    prev_cost: Optional[torch.Tensor] = None,
) -> (torch.Tensor, Optional[torch.Tensor]):
    """One DP step: best cost of classes <= c_idx for each candidate upper threshold of c_idx.

    Given optimal cost `prev_cost` of classes < `c_idx`, optimize cost of `c_idx` as a function of threshold.

    Arguments
    ---------
        c_idx : current class index
        scores, labels, available_thresholds : see `tune_thresholds`
        prev_cost (optional float tensor) : optimal cost of classes < `c_idx` as a function of upper threshold
            for class `c_idx - 1`; ignored if `c_idx == 0`

    Returns
    -------
        cost: `cost[i]` is for choosing upper threshold of class `c_idx` equal to `available_thresholds[i]`
            when thresholds for lower classes are chosen optimally
        indices : to achieve `cost[i]`, optimal upper threshold for class `c_idx - 1` is
            `available_thresholds[indices[i]]`; `None` if `c_idx == 0`

    """

tune_thresholds(*, scores, labels, available_thresholds=None)

Set thresholds by DP over midpoints of unique scores (+/-inf); return the best metric.

Set self.thresholds to optimize mean cost of given scores and labels.

Arguments
scores (1d float tensor) : scores of examples on tuning dataset
labels (1d int tensor) : labels in {0, ..., self.num_classes - 1} of same shape as scores
available_thresholds (optional 1d float tensor) : thresholds which will be considered when tuning.
    +/-inf will be added automatically to ensure all examples are classified. If omitted, will
    insert thresholds between each element of sorted(unique(scores)).

Returns:

Type Description
float tensor : optimal mean cost achieved on the provided dataset at the tuned `self.thresholds`
Source code in kirad/ordinal_thresholding.py
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
def tune_thresholds(
    self,
    *,
    scores: torch.Tensor,
    labels: torch.Tensor,
    available_thresholds: Optional[torch.Tensor] = None,
) -> torch.Tensor:
    """Set thresholds by DP over midpoints of unique scores (+/-inf); return the best metric.

    Set `self.thresholds` to optimize mean cost of given `scores` and `labels`.

    Arguments
    ---------
        scores (1d float tensor) : scores of examples on tuning dataset
        labels (1d int tensor) : labels in {0, ..., self.num_classes - 1} of same shape as scores
        available_thresholds (optional 1d float tensor) : thresholds which will be considered when tuning.
            +/-inf will be added automatically to ensure all examples are classified. If omitted, will
            insert thresholds between each element of sorted(unique(scores)).

    Returns
    -------
        float tensor : optimal mean cost achieved on the provided dataset at the tuned `self.thresholds`

    """
    # Candidate thresholds: given ones, else midpoints of unique scores.
    inf = torch.tensor([torch.inf], device=scores.device)
    if available_thresholds is None:  # use all possible thresholds
        unique_scores = torch.unique(scores)
        available_thresholds = (unique_scores[:-1] + unique_scores[1:]) / 2.0
    # Always allow some classes to be omitted entirely by setting thresholds to +/- inf.
    # This simplifies the algorithm and also guarantees that the baseline constant-output
    # classifiers are feasible choices for tuning, which is needed to assure that the
    # optimum is at least as good as a constant-output classifier.
    available_thresholds = torch.concatenate(
        [
            -inf,
            available_thresholds,
            inf,
        ]
    )
    # Backpointers: best lower-threshold index per middle class/candidate.
    indices = torch.empty(
        (self.num_classes - 2, len(available_thresholds)),
        dtype=torch.int,
        device=scores.device,
    )

    # cost[j] = optimal total cost of items assigned pred <= c if the
    # threshold between class c and c+1 is available_thresholds[j] (by appropriate choice of lower thresholds).
    cost, _ = self.dp_step(
        c_idx=0,
        scores=scores,
        labels=labels,
        available_thresholds=available_thresholds,
    )
    # Middle classes: extend the DP, storing argmax backpointers.
    for c in range(1, self.num_classes - 1):
        cost, indices[c - 1, :] = self.dp_step(
            c_idx=c,
            scores=scores,
            labels=labels,
            available_thresholds=available_thresholds,
            prev_cost=cost,
        )
    # Last class: keep only the best final cost and its threshold index.
    cost, best_index = self.dp_step(
        c_idx=self.num_classes - 1,
        scores=scores,
        labels=labels,
        available_thresholds=available_thresholds,
        prev_cost=cost,
    )
    # The DP maximized the negated cost; flip back to the real cost.
    if self.direction == "min":
        cost *= -1

    # Follow DP path backwards to find thresholds which optimized cost
    self.thresholds[self.num_classes - 2] = available_thresholds[
        best_index
    ]  # final threshold
    for c in range(self.num_classes - 3, -1, -1):  # counting down to zero
        best_index = indices[c, best_index.long()]
        self.thresholds[c] = available_thresholds[best_index.long()]

    return cost

OptimalCostPerSampleOrdinalThresholding

Bases: OptimalOrdinalThresholdingViaDynamicProgramming, ABC

DP tuning for metrics that are a mean of per-sample costs (accuracy, MAE, macro recall).

Optimal 1d thresholding based on tuning thresholds to optimize the mean of a sample-wise cost function.

Source code in kirad/ordinal_thresholding.py
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
class OptimalCostPerSampleOrdinalThresholding(
    OptimalOrdinalThresholdingViaDynamicProgramming, ABC
):
    """DP tuning for metrics that are a mean of per-sample costs (accuracy, MAE, macro recall).

    Optimal 1d thresholding based on tuning thresholds to optimize the mean of a sample-wise cost function.
    """

    @abstractmethod
    def cost(self, *, labels: torch.Tensor, preds: int | torch.Tensor):
        """Per-sample cost of predicting `preds` for `labels` (subclasses define the metric).

        Compute the sample-wise cost of assigning label(s) `preds` when the ground truth is `labels`."""

    def mean_cost(
        self, *, labels: torch.Tensor, preds: int | torch.Tensor
    ) -> torch.Tensor:
        """Compute the mean cost of assigning label(s) `preds` when the ground truth is `labels`."""
        return torch.mean(self.cost(labels=labels, preds=preds))

    def dp_step(
        self,
        c_idx: int,
        *,
        scores: torch.Tensor,
        labels: torch.Tensor,
        available_thresholds: torch.Tensor,
        prev_cost: Optional[torch.Tensor] = None,
    ) -> (torch.Tensor, Optional[torch.Tensor]):
        """O(n) DP step: histogram per-sample costs over candidates, cumsum, running max.

        O(len(scores)) implementation for per-sample cost.
        Note: torch.histogram runs on CPU (no CUDA kernel); tensors are moved back and forth.
        """
        # Compute running_cost[i] = sum of costs of elements with score less than available_thresholds[i] if assigned label c
        item_costs = self.cost(labels=labels, preds=c_idx) / len(scores)
        # The DP always maximizes, so negate costs when lower is better.
        if self.direction == "min":
            item_costs *= -1
        # move tensors to and from CPU because histogram has no CUDA implementation
        cost_new_class_by_thresh, _ = torch.histogram(
            scores.cpu().float(),
            weight=item_costs.cpu().float(),
            bins=available_thresholds.cpu().float(),
        )
        running_cost = cumsum_with_0(cost_new_class_by_thresh.to(labels.device))

        # Combine with running_cost with prev_cost
        if c_idx == 0:
            return running_cost, None
        # Running max over lower thresholds: best cost per upper threshold.
        diff = prev_cost - running_cost
        cummax = torch.cummax(diff, dim=0)
        cost = running_cost + cummax.values
        if c_idx == self.num_classes - 1:
            # -1 to always set the *upper* threshold for class `num_classes - 1` to include the rest of the data
            return cost[-1], cummax.indices[-1]
        return cost, cummax.indices

cost(*, labels, preds) abstractmethod

Per-sample cost of predicting preds for labels (subclasses define the metric).

Compute the sample-wise cost of assigning label(s) preds when the ground truth is labels.

Source code in kirad/ordinal_thresholding.py
279
280
281
282
283
@abstractmethod
def cost(self, *, labels: torch.Tensor, preds: int | torch.Tensor):
    """Per-sample cost of predicting `preds` for `labels` (subclasses define the metric).

    Compute the sample-wise cost of assigning label(s) `preds` when the ground truth is `labels`."""

mean_cost(*, labels, preds)

Compute the mean cost of assigning label(s) preds when the ground truth is labels.

Source code in kirad/ordinal_thresholding.py
285
286
287
288
289
def mean_cost(
    self, *, labels: torch.Tensor, preds: int | torch.Tensor
) -> torch.Tensor:
    """Compute the mean cost of assigning label(s) `preds` when the ground truth is `labels`."""
    return torch.mean(self.cost(labels=labels, preds=preds))

dp_step(c_idx, *, scores, labels, available_thresholds, prev_cost=None)

O(n) DP step: histogram per-sample costs over candidates, cumsum, running max.

O(len(scores)) implementation for per-sample cost. Note: torch.histogram runs on CPU (no CUDA kernel); tensors are moved back and forth.

Source code in kirad/ordinal_thresholding.py
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
def dp_step(
    self,
    c_idx: int,
    *,
    scores: torch.Tensor,
    labels: torch.Tensor,
    available_thresholds: torch.Tensor,
    prev_cost: Optional[torch.Tensor] = None,
) -> (torch.Tensor, Optional[torch.Tensor]):
    """O(n) DP step: histogram per-sample costs over candidates, cumsum, running max.

    O(len(scores)) implementation for per-sample cost.
    Note: torch.histogram runs on CPU (no CUDA kernel); tensors are moved back and forth.
    """
    # Compute running_cost[i] = sum of costs of elements with score less than available_thresholds[i] if assigned label c
    item_costs = self.cost(labels=labels, preds=c_idx) / len(scores)
    # The DP always maximizes, so negate costs when lower is better.
    if self.direction == "min":
        item_costs *= -1
    # move tensors to and from CPU because histogram has no CUDA implementation
    cost_new_class_by_thresh, _ = torch.histogram(
        scores.cpu().float(),
        weight=item_costs.cpu().float(),
        bins=available_thresholds.cpu().float(),
    )
    running_cost = cumsum_with_0(cost_new_class_by_thresh.to(labels.device))

    # Combine with running_cost with prev_cost
    if c_idx == 0:
        return running_cost, None
    # Running max over lower thresholds: best cost per upper threshold.
    diff = prev_cost - running_cost
    cummax = torch.cummax(diff, dim=0)
    cost = running_cost + cummax.values
    if c_idx == self.num_classes - 1:
        # -1 to always set the *upper* threshold for class `num_classes - 1` to include the rest of the data
        return cost[-1], cummax.indices[-1]
    return cost, cummax.indices

MaxAccuracyOrdinalThresholding

Bases: OptimalCostPerSampleOrdinalThresholding

Threshold to maximize accuracy.

Source code in kirad/ordinal_thresholding.py
331
332
333
334
335
336
337
338
class MaxAccuracyOrdinalThresholding(OptimalCostPerSampleOrdinalThresholding):
    """Threshold to maximize accuracy."""

    direction = "max"

    def cost(self, *, labels: torch.Tensor, preds: int | torch.Tensor):
        """1.0 where pred == label, else 0.0."""
        return torch.eq(labels, preds).float()

direction = 'max' class-attribute instance-attribute

cost(*, labels, preds)

1.0 where pred == label, else 0.0.

Source code in kirad/ordinal_thresholding.py
336
337
338
def cost(self, *, labels: torch.Tensor, preds: int | torch.Tensor):
    """1.0 where pred == label, else 0.0."""
    return torch.eq(labels, preds).float()

MaxMacroRecallOrdinalThresholding

Bases: OptimalCostPerSampleOrdinalThresholding

Threshold to maximize macro-averaged recall.

Source code in kirad/ordinal_thresholding.py
341
342
343
344
345
346
347
348
349
350
351
352
class MaxMacroRecallOrdinalThresholding(OptimalCostPerSampleOrdinalThresholding):
    """Threshold to maximize macro-averaged recall."""

    direction = "max"

    def cost(self, *, labels: torch.Tensor, preds: int | torch.Tensor):
        """Correct = N / (K * count[label]), so the mean equals macro-averaged recall."""
        counts = torch.bincount(labels, minlength=self.num_classes).float()
        ratios = counts.sum() / (self.num_classes * counts)
        return torch.eq(labels, preds).float() * torch.gather(
            ratios, 0, labels.type(torch.int64)
        )

direction = 'max' class-attribute instance-attribute

cost(*, labels, preds)

Correct = N / (K * count[label]), so the mean equals macro-averaged recall.

Source code in kirad/ordinal_thresholding.py
346
347
348
349
350
351
352
def cost(self, *, labels: torch.Tensor, preds: int | torch.Tensor):
    """Correct = N / (K * count[label]), so the mean equals macro-averaged recall."""
    counts = torch.bincount(labels, minlength=self.num_classes).float()
    ratios = counts.sum() / (self.num_classes * counts)
    return torch.eq(labels, preds).float() * torch.gather(
        ratios, 0, labels.type(torch.int64)
    )

MinAbsoluteErrorOrdinalThresholding

Bases: OptimalCostPerSampleOrdinalThresholding

Threshold to minimize mean absolute error.

Source code in kirad/ordinal_thresholding.py
355
356
357
358
359
360
361
362
class MinAbsoluteErrorOrdinalThresholding(OptimalCostPerSampleOrdinalThresholding):
    """Threshold to minimize mean absolute error."""

    direction = "min"

    def cost(self, *, labels: torch.Tensor, preds: int | torch.Tensor):
        """|pred - label| in class units."""
        return torch.abs(preds - labels).float()

direction = 'min' class-attribute instance-attribute

cost(*, labels, preds)

|pred - label| in class units.

Source code in kirad/ordinal_thresholding.py
360
361
362
def cost(self, *, labels: torch.Tensor, preds: int | torch.Tensor):
    """|pred - label| in class units."""
    return torch.abs(preds - labels).float()

ClassWeightedOptimalCostPerSampleOrdinalThresholding

Bases: OptimalCostPerSampleOrdinalThresholding

Wrap a per-sample cost so each class weighs equally; not used by base_models.

Compute cost weighted equally over classes instead of equally over samples.

This class takes another instance of OptimalCostPerSampleOrdinalThresholding which computes its cost independently for each sample and reweights the cost based on label frequencies.

Note: this class depends on an implementation detail of its superclass: namely calling self.cost with the full tuning or eval set of labels, rather than a single label. This is required to do the re-weighting properly.

Source code in kirad/ordinal_thresholding.py
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
class ClassWeightedOptimalCostPerSampleOrdinalThresholding(
    OptimalCostPerSampleOrdinalThresholding
):
    """Wrap a per-sample cost so each class weighs equally; not used by base_models.

    Compute cost weighted equally over classes instead of equally over samples.

    This class takes another instance of OptimalCostPerSampleOrdinalThresholding
    which computes its cost independently for each sample and reweights the cost
    based on label frequencies.

    Note: this class depends on an implementation detail of its superclass:
      namely calling `self.cost` with the full tuning or eval set of labels,
      rather than a single label. This is required to do the re-weighting properly.

    """

    def __init__(self, unweighted_instance: OptimalCostPerSampleOrdinalThresholding):
        """Copy direction and num_classes from the wrapped instance."""
        self.direction = unweighted_instance.direction
        super().__init__(unweighted_instance.num_classes)
        self.unweighted_instance = unweighted_instance

    def cost(self, *, labels: torch.Tensor, preds: int | torch.Tensor):
        """Wrapped cost times N / (K * count[label]); raises if any class is missing."""
        # Every class must appear in labels, else its weight is undefined.
        counts = torch.bincount(labels, minlength=self.num_classes)
        (indices,) = torch.where(counts == 0)
        if len(indices) > 0:
            raise ValueError(
                f"Cannot compute class-weighted cost because classes {set(indices.tolist())} are missing."
            )
        # Reweight the wrapped per-sample cost by inverse class frequency.
        unweighted_cost = self.unweighted_instance.cost(labels=labels, preds=preds)
        weights = len(labels) / (self.num_classes * counts[labels].float())
        return weights * unweighted_cost

direction = unweighted_instance.direction instance-attribute

unweighted_instance = unweighted_instance instance-attribute

__init__(unweighted_instance)

Copy direction and num_classes from the wrapped instance.

Source code in kirad/ordinal_thresholding.py
382
383
384
385
386
def __init__(self, unweighted_instance: OptimalCostPerSampleOrdinalThresholding):
    """Copy direction and num_classes from the wrapped instance."""
    self.direction = unweighted_instance.direction
    super().__init__(unweighted_instance.num_classes)
    self.unweighted_instance = unweighted_instance

cost(*, labels, preds)

Wrapped cost times N / (K * count[label]); raises if any class is missing.

Source code in kirad/ordinal_thresholding.py
388
389
390
391
392
393
394
395
396
397
398
399
400
def cost(self, *, labels: torch.Tensor, preds: int | torch.Tensor):
    """Wrapped cost times N / (K * count[label]); raises if any class is missing."""
    # Every class must appear in labels, else its weight is undefined.
    counts = torch.bincount(labels, minlength=self.num_classes)
    (indices,) = torch.where(counts == 0)
    if len(indices) > 0:
        raise ValueError(
            f"Cannot compute class-weighted cost because classes {set(indices.tolist())} are missing."
        )
    # Reweight the wrapped per-sample cost by inverse class frequency.
    unweighted_cost = self.unweighted_instance.cost(labels=labels, preds=preds)
    weights = len(labels) / (self.num_classes * counts[labels].float())
    return weights * unweighted_cost

OptimalCostPerClassOrdinalThresholding

Bases: OptimalOrdinalThresholdingViaDynamicProgramming, ABC

DP tuning for per-class metrics (macro precision, macro F1) via a T x T cost matrix.

General DP case for when the linear algorithm for per-sample costs is not applicable.

Complexity depends on the implementation of cost_matrix.

Source code in kirad/ordinal_thresholding.py
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
class OptimalCostPerClassOrdinalThresholding(
    OptimalOrdinalThresholdingViaDynamicProgramming, ABC
):
    """DP tuning for per-class metrics (macro precision, macro F1) via a T x T cost matrix.

    General DP case for when the linear algorithm for per-sample costs is not applicable.

    Complexity depends on the implementation of `cost_matrix`.

    """

    @abstractmethod
    def cost_matrix(
        self,
        c_idx: int,
        *,
        scores: torch.Tensor,
        labels: torch.Tensor,
        available_thresholds: torch.Tensor,
        start: bool,
        end: bool,
    ) -> torch.Tensor:
        """Cost matrix [T, T]: metric of class c_idx if scores in [thr[i], thr[j]) get c_idx.

        Each output[i, j] = cost for when scores in range `available_thresholds[i:j]` are assigned label `c_idx`."""

    def mean_cost(
        self, *, labels: torch.Tensor, preds: int | torch.Tensor
    ) -> torch.Tensor:
        """Macro metric of fixed `preds`, reusing cost_matrix with thresholds c +/- 0.5.

        Compute the mean cost of assigning label(s) `preds` when the ground truth is `labels`.
        """

        # A scalar prediction becomes a constant prediction per sample.
        if isinstance(preds, int) or preds.numel() == 1:
            preds = preds * torch.ones_like(labels, dtype=torch.int)

        # Sum per-class metric on the single range [c-0.5, c+0.5], then average.
        total_cost = torch.tensor(0.0, device=labels.device)
        for c_idx in range(self.num_classes):
            thresholds = torch.tensor([c_idx - 0.5, c_idx + 0.5], device=labels.device)
            total_cost += self.cost_matrix(
                c_idx, preds.float(), labels, thresholds, start=True, end=True
            )[0, 0]
        return total_cost / self.num_classes

    def dp_step(
        self,
        c_idx: int,
        *,
        scores: torch.Tensor,
        labels: torch.Tensor,
        available_thresholds: torch.Tensor,
        prev_cost: Optional[torch.Tensor] = None,
    ) -> (torch.Tensor, Optional[torch.Tensor]):
        """O(T^2) DP step: add prev_cost to each lower-threshold row and max over it.

        T = number of candidate thresholds. Cost is divided by K, giving a macro average.
        """
        # Per-class metric per (lower, upper) pair, divided by K (macro average).
        cost_matrix = (
            self.cost_matrix(
                c_idx,
                scores=scores,
                labels=labels,
                available_thresholds=available_thresholds,
                start=c_idx == 0,
                end=c_idx == self.num_classes - 1,
            )
            / self.num_classes
        )
        # The DP maximizes, so negate costs when lower is better.
        if self.direction == "min":
            cost_matrix *= -1
        # Add best cost of lower classes per lower threshold (rows); max over rows.
        if prev_cost is not None:
            cost_matrix += prev_cost[:, None]
        max_ = torch.max(cost_matrix, dim=0)
        return max_.values, max_.indices

cost_matrix(c_idx, *, scores, labels, available_thresholds, start, end) abstractmethod

Cost matrix [T, T]: metric of class c_idx if scores in [thr[i], thr[j]) get c_idx.

Each output[i, j] = cost for when scores in range available_thresholds[i:j] are assigned label c_idx.

Source code in kirad/ordinal_thresholding.py
414
415
416
417
418
419
420
421
422
423
424
425
426
427
@abstractmethod
def cost_matrix(
    self,
    c_idx: int,
    *,
    scores: torch.Tensor,
    labels: torch.Tensor,
    available_thresholds: torch.Tensor,
    start: bool,
    end: bool,
) -> torch.Tensor:
    """Cost matrix [T, T]: metric of class c_idx if scores in [thr[i], thr[j]) get c_idx.

    Each output[i, j] = cost for when scores in range `available_thresholds[i:j]` are assigned label `c_idx`."""

mean_cost(*, labels, preds)

Macro metric of fixed preds, reusing cost_matrix with thresholds c +/- 0.5.

Compute the mean cost of assigning label(s) preds when the ground truth is labels.

Source code in kirad/ordinal_thresholding.py
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
def mean_cost(
    self, *, labels: torch.Tensor, preds: int | torch.Tensor
) -> torch.Tensor:
    """Macro metric of fixed `preds`, reusing cost_matrix with thresholds c +/- 0.5.

    Compute the mean cost of assigning label(s) `preds` when the ground truth is `labels`.
    """

    # A scalar prediction becomes a constant prediction per sample.
    if isinstance(preds, int) or preds.numel() == 1:
        preds = preds * torch.ones_like(labels, dtype=torch.int)

    # Sum per-class metric on the single range [c-0.5, c+0.5], then average.
    total_cost = torch.tensor(0.0, device=labels.device)
    for c_idx in range(self.num_classes):
        thresholds = torch.tensor([c_idx - 0.5, c_idx + 0.5], device=labels.device)
        total_cost += self.cost_matrix(
            c_idx, preds.float(), labels, thresholds, start=True, end=True
        )[0, 0]
    return total_cost / self.num_classes

dp_step(c_idx, *, scores, labels, available_thresholds, prev_cost=None)

O(T^2) DP step: add prev_cost to each lower-threshold row and max over it.

T = number of candidate thresholds. Cost is divided by K, giving a macro average.

Source code in kirad/ordinal_thresholding.py
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
def dp_step(
    self,
    c_idx: int,
    *,
    scores: torch.Tensor,
    labels: torch.Tensor,
    available_thresholds: torch.Tensor,
    prev_cost: Optional[torch.Tensor] = None,
) -> (torch.Tensor, Optional[torch.Tensor]):
    """O(T^2) DP step: add prev_cost to each lower-threshold row and max over it.

    T = number of candidate thresholds. Cost is divided by K, giving a macro average.
    """
    # Per-class metric per (lower, upper) pair, divided by K (macro average).
    cost_matrix = (
        self.cost_matrix(
            c_idx,
            scores=scores,
            labels=labels,
            available_thresholds=available_thresholds,
            start=c_idx == 0,
            end=c_idx == self.num_classes - 1,
        )
        / self.num_classes
    )
    # The DP maximizes, so negate costs when lower is better.
    if self.direction == "min":
        cost_matrix *= -1
    # Add best cost of lower classes per lower threshold (rows); max over rows.
    if prev_cost is not None:
        cost_matrix += prev_cost[:, None]
    max_ = torch.max(cost_matrix, dim=0)
    return max_.values, max_.indices

MaxMacroPrecisionOrdinalThresholding

Bases: OptimalCostPerClassOrdinalThresholding

Threshold to maximize macro-averaged precision.

Source code in kirad/ordinal_thresholding.py
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
class MaxMacroPrecisionOrdinalThresholding(OptimalCostPerClassOrdinalThresholding):
    """Threshold to maximize macro-averaged precision."""

    direction = "max"

    def cost_matrix(
        self,
        c_idx: int,
        scores: torch.Tensor,
        labels: torch.Tensor,
        available_thresholds: torch.Tensor,
        start: bool,
        end: bool,
    ) -> torch.Tensor:
        """Precision of class c_idx for each (lower, upper) threshold pair; empty range = 0."""
        tp, tp_plus_fp = _compute_metrics_matrices(
            scores, torch.eq(labels, c_idx), available_thresholds, start=start, end=end
        )
        # Clamp counts to >= 1 so empty ranges give precision 0, not NaN.
        safe_tp_plus_fp = torch.maximum(
            tp_plus_fp, torch.ones(1, device=tp_plus_fp.device)
        )
        return torch.where(torch.ge(tp_plus_fp, 0.0), tp / safe_tp_plus_fp, -torch.inf)

direction = 'max' class-attribute instance-attribute

cost_matrix(c_idx, scores, labels, available_thresholds, start, end)

Precision of class c_idx for each (lower, upper) threshold pair; empty range = 0.

Source code in kirad/ordinal_thresholding.py
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
def cost_matrix(
    self,
    c_idx: int,
    scores: torch.Tensor,
    labels: torch.Tensor,
    available_thresholds: torch.Tensor,
    start: bool,
    end: bool,
) -> torch.Tensor:
    """Precision of class c_idx for each (lower, upper) threshold pair; empty range = 0."""
    tp, tp_plus_fp = _compute_metrics_matrices(
        scores, torch.eq(labels, c_idx), available_thresholds, start=start, end=end
    )
    # Clamp counts to >= 1 so empty ranges give precision 0, not NaN.
    safe_tp_plus_fp = torch.maximum(
        tp_plus_fp, torch.ones(1, device=tp_plus_fp.device)
    )
    return torch.where(torch.ge(tp_plus_fp, 0.0), tp / safe_tp_plus_fp, -torch.inf)

MaxMacroF1OrdinalThresholding

Bases: OptimalCostPerClassOrdinalThresholding

Threshold to maximize macro-averaged F1 score.

Source code in kirad/ordinal_thresholding.py
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
class MaxMacroF1OrdinalThresholding(OptimalCostPerClassOrdinalThresholding):
    """Threshold to maximize macro-averaged F1 score."""

    direction = "max"

    def cost_matrix(
        self,
        c_idx: int,
        scores: torch.Tensor,
        labels: torch.Tensor,
        available_thresholds: torch.Tensor,
        start: bool,
        end: bool,
    ) -> torch.Tensor:
        """F1 of class c_idx = 2TP / (TP+FP + TP+FN) for each (lower, upper) threshold pair."""
        tp, tp_plus_fp = _compute_metrics_matrices(
            scores, torch.eq(labels, c_idx), available_thresholds, start=start, end=end
        )
        # Total positives of class c_idx (TP+FN), constant across ranges.
        tp_plus_fn = torch.eq(labels, c_idx).float().sum()  # scalar
        # Clamp counts to >= 1 so empty ranges do not divide by zero.
        safe_tp_plus_fp = torch.maximum(
            tp_plus_fp, torch.ones(1, device=tp_plus_fp.device)
        )
        return torch.where(
            torch.ge(tp_plus_fp, 0.0),
            2 * tp / (safe_tp_plus_fp + tp_plus_fn),
            -torch.inf,
        )

direction = 'max' class-attribute instance-attribute

cost_matrix(c_idx, scores, labels, available_thresholds, start, end)

F1 of class c_idx = 2TP / (TP+FP + TP+FN) for each (lower, upper) threshold pair.

Source code in kirad/ordinal_thresholding.py
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
def cost_matrix(
    self,
    c_idx: int,
    scores: torch.Tensor,
    labels: torch.Tensor,
    available_thresholds: torch.Tensor,
    start: bool,
    end: bool,
) -> torch.Tensor:
    """F1 of class c_idx = 2TP / (TP+FP + TP+FN) for each (lower, upper) threshold pair."""
    tp, tp_plus_fp = _compute_metrics_matrices(
        scores, torch.eq(labels, c_idx), available_thresholds, start=start, end=end
    )
    # Total positives of class c_idx (TP+FN), constant across ranges.
    tp_plus_fn = torch.eq(labels, c_idx).float().sum()  # scalar
    # Clamp counts to >= 1 so empty ranges do not divide by zero.
    safe_tp_plus_fp = torch.maximum(
        tp_plus_fp, torch.ones(1, device=tp_plus_fp.device)
    )
    return torch.where(
        torch.ge(tp_plus_fp, 0.0),
        2 * tp / (safe_tp_plus_fp + tp_plus_fn),
        -torch.inf,
    )

cumsum_with_0(t)

Cumulative sum along dim 0 with a leading 0 (length n + 1).

Source code in kirad/ordinal_thresholding.py
266
267
268
def cumsum_with_0(t: torch.Tensor):
    """Cumulative sum along dim 0 with a leading 0 (length n + 1)."""
    return torch.nn.functional.pad(torch.cumsum(t, dim=0), (1, 0))