Classification¶
Binary classification metrics, applied per element.
Anywhere a model answers yes/no about each pixel — semantic segmentation, change detection, anomaly and defect detection, cloud or shadow masking, water and road extraction, any foreground/background split. Nothing here is specific to a task; it is specific to the shape of the question.
They live under "classification" rather than "segmentation" or "detection"
because that is what they are: F1, precision, recall and MCC do not know they
are looking at an image, and would behave identically on a flat array of
yes/no decisions. torchmetrics draws the same line — these are in its
classification package, while its segmentation package holds the
metrics that genuinely need region or boundary structure (generalized Dice,
Hausdorff distance), and its detection package is box-based and unrelated.
Everything is built from the four confusion counts, and the counts are the
honest primitive: they are additive, so they survive pooling over an epoch
and across ranks without any weighting, whatever size the images are. The
ratios (F1, precision, recall, IoU, MCC) are functions of those counts, which
is why none of them can be a per-pixel quantity — see :meth:pointwise on
the count classes for the one thing here that genuinely is.
Micro or macro. Every ratio takes aggregate (a :class:Metric
option, like on any other metric):
"micro"(default) pools every element seen in the epoch and computes the ratio once. This is what the literature usually reports."macro"scores each image and averages.
They are not close whenever the positive class is rare or absent from some images. An image containing no positives at all scores 0 under macro however well the model behaves, so macro is dominated by those images; measured gaps of tens of percent are ordinary on sparse masks. That case is the norm in change detection and in defect or anomaly detection, and common in segmentation of small structures. Pick deliberately; the name appears in your logs either way.
Input convention. x is a score or probability map, thresholded at
threshold; y is the target, treated as positive where >= 0.5.
Deliberately not logits: a model emitting multi-channel logits applies its
own softmax or argmax upstream, so this module holds no assumption about how
a particular network spells its output.
Counts
dataclass
¶
The four confusion cells, as attributes rather than dictionary keys.
Passed to :meth:ConfusionRatio.ratio. Per image under
aggregate="macro"; already totalled over the epoch under
"micro" — the same object either way, so a ratio is written once.
Attributes are used instead of string keys deliberately. c.tpp raises
immediately and names the attribute; c["tpp"] would raise a KeyError
somewhere downstream, or with a mistyped declaration would quietly
compute the wrong thing. All four are always populated, so there is no
second place to declare which ones a metric uses and no way for that
declaration to drift out of step with the formula.
ConfusionMetric
¶
Bases: Metric
Thresholding shared by every metric here.
Rarely subclassed directly — reach for :class:ConfusionCount or
:class:ConfusionRatio, which build on it. Exposed because
:meth:predicted and :meth:actual are what a subclass of either uses
to turn score maps into the boolean masks it reasons about.
ConfusionCount
¶
Bases: ConfusionMetric
A confusion cell, reported as a total over the epoch.
Implemented at Level 2 — :meth:pointwise returns a 0/1 indicator map —
for two reasons. Masking then comes from the framework rather than from
four hand-written copies of the same masking code. And a count is the one
quantity in this module that genuinely is per-pixel: every pixel falls
into exactly one of the four cells, independently of every other pixel.
Because a finalized Level-2 metric receives {"sum", "count"}, the sum
of the indicator map is exactly the number of pixels in this cell, and
summing that across images and ranks needs no weighting.
Writing your own means one method returning a boolean map::
@register_class
class PredictedPositives(ConfusionCount):
# Every pixel the model flagged positive, right or wrong.
@property
def best_min(self) -> bool:
return False
def indicator(self, x, y):
return self.predicted(x)
TruePositives
¶
FalsePositives
¶
FalseNegatives
¶
TrueNegatives
¶
Bases: ConfusionCount
Negative pixels correctly left alone.
Usually the overwhelming majority when the positive class is sparse, which is why plain accuracy is uninformative on such data and F1 or MCC are reported instead.
ConfusionRatio
¶
Bases: ConfusionMetric
A ratio of confusion counts, micro- or macro-averaged.
Level 1 rather than Level 2: a ratio of sums has no per-pixel value. The
mean of a per-pixel 'IoU' is the true-positive rate, not IoU — the same
reason :class:~srforge.metrics.regression.PSNR has no pointwise.
Writing your own is one method::
@register_class
class Specificity(ConfusionRatio):
# TN / (TN + FP): of the negative pixels, how many were
# correctly left alone.
def ratio(self, c: Counts):
return c.tn / (c.tn + c.fp + 1e-12)
:meth:ratio receives a :class:Counts — per image under
aggregate="macro", already totalled over the epoch under "micro"
— so the same method serves both and there is no second code path to
keep in step. Micro/macro, thresholding, masking and the epoch
arithmetic are all inherited.
All four cells are always available as attributes, so there is nothing to declare about which ones you use: a typo raises at the point of use instead of silently selecting the wrong counts, and a formula cannot drift out of step with a separate declaration of its inputs.
Training on it: soft=True. The yes/no decision stops any
gradient, so a hard ratio cannot be a loss. With soft=True the counts
use the probability itself (TP = Σ p·y, …; see :meth:_soft_counts)
and the same :meth:ratio becomes differentiable — every ratio,
including your own, has a trainable form. It keeps its direction: soft
F1 is higher-is-better, and the training total negates it like any such
metric. :class:DiceLoss, :class:JaccardLoss and :class:TverskyLoss
are the common ones by name.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
threshold
|
float
|
Score above which a pixel counts as predicted-positive.
Default 0.5; refused with |
None
|
smooth
|
float
|
Added to numerator and denominator. |
None
|
soft
|
bool
|
Count with probabilities instead of decisions, for training.
The default name becomes |
False
|
**kwargs
|
Any
|
Passed to :class: |
{}
|
ratio(c: Counts) -> torch.Tensor
¶
Combine the four cells into this metric's value.
Precision
¶
Recall
¶
F1Score
¶
Bases: ConfusionRatio
2TP / (2TP + FP + FN) — the harmonic mean of precision and recall.
Identical to the Dice coefficient for binary masks: 2|A∩B|/(|A|+|B|)
is the same expression. They are one metric under two names, so reporting
both is reporting the same number twice.
IoU
¶
Bases: ConfusionRatio
TP / (TP + FP + FN) — intersection over union, a.k.a. Jaccard.
A monotone function of :class:F1Score (IoU = F1 / (2 - F1)), so it
ranks models identically. Worth reporting because it is conventional, not
because it is independent evidence.
MCC
¶
Bases: ConfusionRatio
Matthews correlation coefficient, in [-1, 1].
Uses all four cells, which is why it is the metric of choice for heavily imbalanced masks — sparse defects, rare changes, thin structures. Unlike F1 it cannot be inflated by ignoring the negative class, and 0 means "no better than chance" rather than "no overlap".
TverskyIndex
¶
Bases: ConfusionRatio
TP / (TP + α·FP + β·FN) — F1 and IoU with the two errors priced apart.
alpha prices false alarms, beta missed positives. α = β = 0.5
is :class:F1Score and α = β = 1 is :class:IoU; raising beta
above alpha makes a missed change cost more than a false alarm, the
usual choice when positives are rare. To train on it see
:class:TverskyLoss.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
alpha
|
float
|
Weight of false positives. |
0.5
|
beta
|
float
|
Weight of false negatives. |
0.5
|
**kwargs
|
Any
|
|
{}
|
DiceLoss
¶
Bases: F1Score
Soft Dice for training — exactly F1Score(soft=True).
Dice is F1 on masks; this is its trainable form, counted with
probabilities (see :class:ConfusionRatio). Higher is better, like F1:
the training total maximises it. Report the hard :class:F1Score.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
**kwargs
|
Any
|
As :class: |
{}
|
JaccardLoss
¶
Bases: IoU
Soft IoU (Jaccard) for training — exactly IoU(soft=True).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
**kwargs
|
Any
|
As :class: |
{}
|
TverskyLoss
¶
Bases: TverskyIndex
Soft Tversky index for training — exactly TverskyIndex(soft=True).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
alpha
|
float
|
Weight of false alarms. |
0.5
|
beta
|
float
|
Weight of missed positives. |
0.5
|
**kwargs
|
Any
|
As :class: |
{}
|
BinaryCrossEntropy
¶
Bases: Metric
Per-pixel binary cross-entropy: −[y·log p + (1−y)·log(1−p)].
The per-pixel loss for a yes/no map — change, defect, cloud, water.
x is the probability of "yes" (a sigmoid output), y the target
in [0, 1] with the same shape; soft targets are fine.
Commonly summed with :class:DiceLoss: cross-entropy gives every pixel
a steady gradient, Dice keeps a rare positive class from being drowned
by the background.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
class_weights
|
Optional |
None
|
|
eps
|
float
|
Probabilities are clamped to |
1e-07
|
**kwargs
|
Any
|
:class: |
{}
|
CrossEntropy
¶
Bases: Metric
Per-pixel cross-entropy over classes: −Σ_c w_c·y_c·log p_c.
For a map with one channel per class: x holds the class
probabilities (a softmax output), shape [B, C, ...] with C ≥ 2.
y is either the same shape (one-hot or soft labels) or a map of
class indices — [B, ...] or [B, 1, ...], integer dtype. The sum
runs over the class axis, so each pixel contributes one value.
A one-channel map is refused: with one channel −y·log p has no
term for the negative class, and predicting "yes" everywhere scores a
perfect 0. Use :class:BinaryCrossEntropy for a yes/no map.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
class_weights
|
Optional per-class weights, one per channel in
channel order — e.g. inverse class frequency. A constant of your
data: compute it once and put it in the config. The loss is the
plain mean over pixels of the weighted sum, not torch's
|
None
|
|
eps
|
float
|
Probabilities are clamped from below at |
1e-12
|
**kwargs
|
Any
|
:class: |
{}
|