torch_em.self_training.pseudo_labeling

  1from typing import Callable, Literal, Optional, Union
  2
  3import torch
  4import numpy as np
  5
  6
  7class DefaultPseudoLabeler:
  8    """Compute pseudo labels based on model predictions, typically from a teacher model.
  9
 10    Args:
 11        activation: Activation function applied to the teacher prediction.
 12        confidence_threshold: Threshold for computing a mask for filtering the pseudo labels.
 13            If None is given no mask will be computed.
 14        threshold_from_both_sides: Whether to include both values bigger than the threshold
 15            and smaller than 1 - the thrhesold, or only values bigger than the threshold, in the mask.
 16            The former should be used for binary labels, the latter for for multiclass labels.
 17        mask_channel: A specific channel to use for computing the confidence mask.
 18            By default the confidence mask is computed across all channels independently.
 19            This is useful, if only one of the channels encodes a probability.
 20    """
 21    def __init__(
 22        self,
 23        activation: Optional[torch.nn.Module] = None,
 24        confidence_threshold: Optional[float] = None,
 25        threshold_from_both_sides: bool = True,
 26        mask_channel: Optional[int] = None,
 27    ):
 28        self.activation = activation
 29        self.confidence_threshold = confidence_threshold
 30        self.threshold_from_both_sides = threshold_from_both_sides
 31        self.mask_channel = mask_channel
 32        # TODO serialize the class names and kwargs for activation instead
 33        self.init_kwargs = {
 34            "activation": None, "confidence_threshold": confidence_threshold,
 35            "threshold_from_both_sides": threshold_from_both_sides,
 36            "mask_channel": mask_channel,
 37        }
 38
 39    def _compute_label_mask_both_sides(self, pseudo_labels):
 40        upper_threshold = self.confidence_threshold
 41        lower_threshold = 1.0 - self.confidence_threshold
 42        mask = ((pseudo_labels >= upper_threshold) + (pseudo_labels <= lower_threshold)).to(dtype=torch.float32)
 43        return mask
 44
 45    def _compute_label_mask_one_side(self, pseudo_labels):
 46        mask = (pseudo_labels >= self.confidence_threshold)
 47        return mask
 48
 49    def __call__(self, teacher: torch.nn.Module, input_: torch.Tensor) -> torch.Tensor:
 50        """Compute pseudo-labels.
 51
 52        Args:
 53            teacher: The teacher model.
 54            input_: The input for this batch.
 55
 56        Returns:
 57            The pseudo-labels.
 58        """
 59        pseudo_labels = teacher(input_)
 60        if self.activation is not None:
 61            pseudo_labels = self.activation(pseudo_labels)
 62        if self.confidence_threshold is None:
 63            label_mask = None
 64        else:
 65            mask_input = pseudo_labels if self.mask_channel is None \
 66                else pseudo_labels[:, [self.mask_channel]]
 67            label_mask = self._compute_label_mask_both_sides(mask_input) if self.threshold_from_both_sides \
 68                else self._compute_label_mask_one_side(mask_input)
 69            if self.mask_channel is not None:
 70                label_mask = label_mask.expand_as(pseudo_labels)
 71        return pseudo_labels, label_mask
 72
 73    def step(self, metric, epoch):
 74        pass
 75
 76
 77class ProbabilisticPseudoLabeler:
 78    """Compute pseudo labels from a Probabilistic UNet.
 79
 80    Args:
 81        activation: Activation function applied to the teacher prediction.
 82        confidence_threshold: Threshold for computing a mask for filterign the pseudo labels.
 83            If none is given no mask will be computed.
 84        threshold_from_both_sides: Whether to include both values bigger than the threshold
 85            and smaller than 1 - the thrhesold, or only values bigger than the threshold, in the mask.
 86            The former should be used for binary labels, the latter for for multiclass labels.
 87        prior_samples: The number of times to sample from the model distribution per input.
 88        consensus_masking: Whether to activate consensus masking in the label filter.
 89            If False, the weighted consensus response (weighted per-pixel response) is returned.
 90            If True, the masked consensus response (complete aggrement of pixels) is returned.
 91    """
 92    def __init__(
 93        self,
 94        activation: Optional[torch.nn.Module] = None,
 95        confidence_threshold: Optional[float] = None,
 96        threshold_from_both_sides: bool = True,
 97        prior_samples: int = 16,
 98        consensus_masking: bool = False,
 99    ):
100        self.activation = activation
101        self.confidence_threshold = confidence_threshold
102        self.threshold_from_both_sides = threshold_from_both_sides
103        self.prior_samples = prior_samples
104        self.consensus_masking = consensus_masking
105        # TODO serialize the class names and kwargs for activation instead
106        self.init_kwargs = {
107            "activation": None, "confidence_threshold": confidence_threshold,
108            "threshold_from_both_sides": threshold_from_both_sides
109        }
110
111    def _compute_label_mask_both_sides(self, pseudo_labels):
112        upper_threshold = self.confidence_threshold
113        lower_threshold = 1.0 - self.confidence_threshold
114        mask = [
115            torch.where((sample >= upper_threshold) + (sample <= lower_threshold), torch.tensor(1.), torch.tensor(0.))
116            for sample in pseudo_labels
117        ]
118        return mask
119
120    def _compute_label_mask_one_side(self, pseudo_labels):
121        mask = [
122            torch.where((sample >= self.confidence_threshold), torch.tensor(1.), torch.tensor(0.))
123            for sample in pseudo_labels
124        ]
125        return mask
126
127    def __call__(self, teacher: torch.nn.Module, input_: torch.Tensor) -> torch.Tensor:
128        """Compute pseudo-labels.
129
130        Args:
131            teacher: The teacher model. Must be a `torch_em.model.probabilistic_unet.ProbabilisticUNet`.
132            input_: The input for this batch.
133
134        Returns:
135            The pseudo-labels.
136        """
137        teacher.forward(input_)
138        if self.activation is not None:
139            pseudo_labels = [self.activation(teacher.sample()) for _ in range(self.prior_samples)]
140        else:
141            pseudo_labels = [teacher.sample() for _ in range(self.prior_samples)]
142        pseudo_labels = torch.stack(pseudo_labels, dim=0).sum(dim=0)/self.prior_samples
143
144        if self.confidence_threshold is None:
145            label_mask = None
146        else:
147            label_mask = self._compute_label_mask_both_sides(pseudo_labels) if self.threshold_from_both_sides \
148                else self._compute_label_mask_one_side(pseudo_labels)
149            label_mask = torch.stack(label_mask, dim=0).sum(dim=0)/self.prior_samples
150            if self.consensus_masking:
151                label_mask = torch.where(label_mask == 1, 1, 0)
152
153        return pseudo_labels, label_mask
154
155    def step(self, metric, epoch):
156        pass
157
158
159class ScheduledPseudoLabeler:
160    """
161    Implement scheduled pseudo-labeling with dynamic confidence-threshold updates.
162
163    Pseudo labels are generated from a teacher model prediction and can be filtered
164    by a confidence mask. The confidence threshold can be adapted over time either
165    by decreasing it based on a monitored metric (plateau behavior) or by increasing
166    it with a fixed epoch schedule.
167
168    Args:
169        activation: Activation function applied to the teacher prediction.
170        confidence_threshold: Threshold for computing a mask for filtering the pseudo labels.
171            If None is given, no mask will be computed.
172        increase: If True, increase the confidence threshold over time according to
173            a fixed schedule. If False, decrease it based on plateau detection.
174        last_step_epoch: Last epoch at which threshold increase is allowed when
175            `increase=True`.
176        threshold_from_both_sides: Whether to include values larger than the
177            threshold and smaller than 1 - the threshold in the mask, or only values
178            larger than the threshold. The former should be used for binary labels,
179            the latter for multiclass labels.
180        mode: Determines whether the confidence threshold reduction is triggered by a "min" or "max" metric.
181            - 'min': A lower value of the monitored metric is considered better (e.g., loss).
182            - 'max': A higher value of the monitored metric is considered better (e.g., accuracy).
183        factor: Update size for confidence-threshold scheduling. Interpreted as a
184            multiplicative factor for `threshold_mode='rel'` and as an additive step
185            for `threshold_mode='abs'`.
186        patience: Number of epochs (with no improvement) after which the confidence threshold will be reduced.
187        threshold: Threshold value for determining a significant improvement in the performance metric
188            to reset the patience counter. This can be relative (percentage improvement)
189            or absolute depending on `threshold_mode`.
190        threshold_mode: Determines whether the `threshold` is interpreted as a relative improvement ('rel')
191            or an absolute improvement ('abs').
192        min_ct: Minimum allowed confidence threshold. The threshold will not be reduced below this value.
193        max_ct: Maximum allowed confidence threshold. The threshold will not be increased above this value.
194        eps: A small value to avoid floating-point precision errors during threshold comparison.
195        warm_up_epochs: Number of warm-up epochs. At the end of warm-up,
196            `confidence_threshold` is set to `max_ct`. This is intended for
197            decreasing-threshold scheduling (`increase=False`).
198        mask_channel: Specific channel to use for confidence masking. If set, the threshold
199            is applied only to that channel and the resulting mask is broadcast to all channels.
200        verbose: If True, prints messages when the confidence threshold is updated.
201    """
202
203    def __init__(
204        self,
205        activation: Optional[Union[torch.nn.Module, Callable]] = None,
206        confidence_threshold: Optional[float] = None,
207        increase: bool = False,
208        last_step_epoch: int = None,
209        threshold_from_both_sides: bool = True,
210        mode: Literal["min", "max"] = "min",
211        factor: float = 0.05,
212        patience: int = 10,
213        threshold: float = 1e-4,
214        threshold_mode: Literal["rel", "abs"] = "abs",
215        min_ct: float = 0.5,
216        max_ct: float = 0.95,
217        eps: float = 1e-8,
218        warm_up_epochs: int = 0,
219        mask_channel: Optional[int] = None,
220        verbose: bool = True,
221    ):
222        self.activation = activation
223        self.confidence_threshold = confidence_threshold
224        self.increase = increase
225        self.threshold_from_both_sides = threshold_from_both_sides
226        self.init_kwargs = {
227            "activation": None, "confidence_threshold": confidence_threshold,
228            "threshold_from_both_sides": threshold_from_both_sides,
229            "mask_channel": mask_channel,
230        }
231        # scheduler arguments
232        if mode not in {"min", "max"}:
233            raise ValueError(f"Invalid mode: {mode}. Mode should be 'min' or 'max'.")
234        self.mode = mode
235
236        assert factor < 1, f"Factor must be smaller than 1, got {factor}"
237        self.factor = factor
238
239        self.patience = patience
240        self.threshold = threshold
241
242        if threshold_mode not in {"rel", "abs"}:
243            raise ValueError(f"Invalid threshold mode: {mode}. Threshold mode should be 'rel' or 'abs'.")
244        self.threshold_mode = threshold_mode
245
246        self.min_ct = min_ct
247        self.max_ct = max_ct
248        self.eps = eps
249        self.warm_up_epochs = warm_up_epochs
250
251        if self.increase and self.warm_up_epochs > 0:
252            raise ValueError("warm_up_epochs > 0 is only supported when increase=False.")
253
254        self.mask_channel = mask_channel
255        self.verbose = verbose
256
257        if mode == "min":
258            self.best = float("inf")
259        else:  # mode == "max":
260            self.best = float("-inf")
261
262        # self.best = 0
263        self.num_bad_epochs: int = 0
264        self.last_epoch = 0
265
266        if self.increase:
267            self.last_step_epoch = last_step_epoch
268
269            n_ct = len(np.arange(self.min_ct, self.max_ct + self.factor / 2, self.factor))
270            n_increments = n_ct - 1
271
272            if n_increments <= 0:
273                # nothing to increase
274                self.step_epoch = 0
275            else:
276                # compute initial step size; enforce minimum step size of 1
277                self.step_epoch = max(1, int(np.floor(self.last_step_epoch / n_increments)))
278
279                # ensure max_ct is reachable
280                required_epochs = self.step_epoch * n_increments
281                if self.last_step_epoch < required_epochs:
282                    self.last_step_epoch = required_epochs
283
284            print(
285                f"ScheduledPseudoLabeler: Increasing confidence_threshold every {self.step_epoch} epochs;",
286                f"until epoch {self.last_step_epoch}"
287            )
288
289    def _compute_label_mask_both_sides(self, pseudo_labels):
290        upper_threshold = self.confidence_threshold
291        lower_threshold = 1.0 - self.confidence_threshold
292        mask = ((pseudo_labels >= upper_threshold) + (pseudo_labels <= lower_threshold)).to(dtype=torch.float32)
293        return mask
294
295    def _compute_label_mask_one_side(self, pseudo_labels):
296        mask = (pseudo_labels >= self.confidence_threshold)
297        return mask
298
299    def __call__(self, teacher, input_):
300        pseudo_labels = teacher(input_)
301        if self.activation is not None:
302            pseudo_labels = self.activation(pseudo_labels)
303        if self.confidence_threshold is None:
304            label_mask = None
305        else:
306            mask_input = pseudo_labels if self.mask_channel is None \
307                else pseudo_labels[:, [self.mask_channel]]
308            label_mask = self._compute_label_mask_both_sides(mask_input) if self.threshold_from_both_sides \
309                else self._compute_label_mask_one_side(mask_input)
310            if self.mask_channel is not None:
311                label_mask = label_mask.expand_as(pseudo_labels)
312        return pseudo_labels, label_mask
313
314    def end_warm_up(self):
315        self.confidence_threshold = self.max_ct
316        print(f"End of warm-up phase reached: setting confidence threshold to {self.confidence_threshold}")
317
318    def _is_better(self, a, best):
319        if self.mode == "min" and self.threshold_mode == "rel":
320            rel_epsilon = 1.0 - self.threshold
321            return a < best * rel_epsilon
322
323        elif self.mode == "min" and self.threshold_mode == "abs":
324            return a < best - self.threshold
325
326        elif self.mode == "max" and self.threshold_mode == "rel":
327            rel_epsilon = self.threshold + 1.0
328            return a > best * rel_epsilon
329
330        else:  # mode == 'max' and epsilon_mode == 'abs':
331            return a > best + self.threshold
332
333    def _reduce_ct(self, epoch):
334        old_ct = self.confidence_threshold
335        if self.threshold_mode == "rel":
336            new_ct = max(self.confidence_threshold * (1-self.factor), self.min_ct)
337        else:  # threshold_mode == 'abs':
338            new_ct = max(self.confidence_threshold - self.factor, self.min_ct)
339        if abs(old_ct - new_ct) > self.eps:
340            self.confidence_threshold = new_ct
341        if self.verbose:
342            print(f"Epoch {epoch}: reducing confidence threshold from {old_ct} to {self.confidence_threshold}")
343
344    def decrease_step(self, metric, epoch=None):
345        if epoch is None:
346            epoch = self.last_epoch + 1
347            self.last_epoch = epoch
348
349        # If the metric is None, reduce the confidence threshold every epoch
350        if metric is None:
351            if epoch == 0:
352                return
353            if epoch % self.patience == 0:
354                self._reduce_ct(epoch)
355            return
356
357        else:
358            current = float(metric)
359
360            if self._is_better(current, self.best):
361                self.best = current
362                self.num_bad_epochs = 0
363            else:
364                self.num_bad_epochs += 1
365
366            if self.num_bad_epochs > self.patience:
367                self._reduce_ct(epoch)
368                self.num_bad_epochs = 0
369
370    def _increase_ct(self, epoch):
371        old_ct = self.confidence_threshold
372        if self.threshold_mode == "rel":
373            new_ct = min(self.confidence_threshold * (1+self.factor), self.max_ct)
374        else:  # threshold_mode == 'abs':
375            new_ct = min(self.confidence_threshold + self.factor, self.max_ct)
376        if abs(old_ct - new_ct) > self.eps:
377            self.confidence_threshold = new_ct
378        if self.verbose:
379            print(f"Epoch {epoch}: increase confidence threshold from {old_ct} to {self.confidence_threshold}")
380
381    def increase_step(self, epoch):
382        if epoch > self.last_step_epoch:
383            return
384
385        if epoch % self.step_epoch == 0 and epoch != 0:
386            self._increase_ct(epoch)
387
388    def step(self, metric=None, epoch=None):
389        if epoch == self.warm_up_epochs and self.warm_up_epochs > 0:
390            self.end_warm_up()
391        elif epoch > self.warm_up_epochs:
392            if self.increase:
393                self.increase_step(epoch)
394            else:
395                self.decrease_step(metric, epoch)
396        else:
397            return
class DefaultPseudoLabeler:
 8class DefaultPseudoLabeler:
 9    """Compute pseudo labels based on model predictions, typically from a teacher model.
10
11    Args:
12        activation: Activation function applied to the teacher prediction.
13        confidence_threshold: Threshold for computing a mask for filtering the pseudo labels.
14            If None is given no mask will be computed.
15        threshold_from_both_sides: Whether to include both values bigger than the threshold
16            and smaller than 1 - the thrhesold, or only values bigger than the threshold, in the mask.
17            The former should be used for binary labels, the latter for for multiclass labels.
18        mask_channel: A specific channel to use for computing the confidence mask.
19            By default the confidence mask is computed across all channels independently.
20            This is useful, if only one of the channels encodes a probability.
21    """
22    def __init__(
23        self,
24        activation: Optional[torch.nn.Module] = None,
25        confidence_threshold: Optional[float] = None,
26        threshold_from_both_sides: bool = True,
27        mask_channel: Optional[int] = None,
28    ):
29        self.activation = activation
30        self.confidence_threshold = confidence_threshold
31        self.threshold_from_both_sides = threshold_from_both_sides
32        self.mask_channel = mask_channel
33        # TODO serialize the class names and kwargs for activation instead
34        self.init_kwargs = {
35            "activation": None, "confidence_threshold": confidence_threshold,
36            "threshold_from_both_sides": threshold_from_both_sides,
37            "mask_channel": mask_channel,
38        }
39
40    def _compute_label_mask_both_sides(self, pseudo_labels):
41        upper_threshold = self.confidence_threshold
42        lower_threshold = 1.0 - self.confidence_threshold
43        mask = ((pseudo_labels >= upper_threshold) + (pseudo_labels <= lower_threshold)).to(dtype=torch.float32)
44        return mask
45
46    def _compute_label_mask_one_side(self, pseudo_labels):
47        mask = (pseudo_labels >= self.confidence_threshold)
48        return mask
49
50    def __call__(self, teacher: torch.nn.Module, input_: torch.Tensor) -> torch.Tensor:
51        """Compute pseudo-labels.
52
53        Args:
54            teacher: The teacher model.
55            input_: The input for this batch.
56
57        Returns:
58            The pseudo-labels.
59        """
60        pseudo_labels = teacher(input_)
61        if self.activation is not None:
62            pseudo_labels = self.activation(pseudo_labels)
63        if self.confidence_threshold is None:
64            label_mask = None
65        else:
66            mask_input = pseudo_labels if self.mask_channel is None \
67                else pseudo_labels[:, [self.mask_channel]]
68            label_mask = self._compute_label_mask_both_sides(mask_input) if self.threshold_from_both_sides \
69                else self._compute_label_mask_one_side(mask_input)
70            if self.mask_channel is not None:
71                label_mask = label_mask.expand_as(pseudo_labels)
72        return pseudo_labels, label_mask
73
74    def step(self, metric, epoch):
75        pass

Compute pseudo labels based on model predictions, typically from a teacher model.

Arguments:
  • activation: Activation function applied to the teacher prediction.
  • confidence_threshold: Threshold for computing a mask for filtering the pseudo labels. If None is given no mask will be computed.
  • threshold_from_both_sides: Whether to include both values bigger than the threshold and smaller than 1 - the thrhesold, or only values bigger than the threshold, in the mask. The former should be used for binary labels, the latter for for multiclass labels.
  • mask_channel: A specific channel to use for computing the confidence mask. By default the confidence mask is computed across all channels independently. This is useful, if only one of the channels encodes a probability.
DefaultPseudoLabeler( activation: Optional[torch.nn.modules.module.Module] = None, confidence_threshold: Optional[float] = None, threshold_from_both_sides: bool = True, mask_channel: Optional[int] = None)
22    def __init__(
23        self,
24        activation: Optional[torch.nn.Module] = None,
25        confidence_threshold: Optional[float] = None,
26        threshold_from_both_sides: bool = True,
27        mask_channel: Optional[int] = None,
28    ):
29        self.activation = activation
30        self.confidence_threshold = confidence_threshold
31        self.threshold_from_both_sides = threshold_from_both_sides
32        self.mask_channel = mask_channel
33        # TODO serialize the class names and kwargs for activation instead
34        self.init_kwargs = {
35            "activation": None, "confidence_threshold": confidence_threshold,
36            "threshold_from_both_sides": threshold_from_both_sides,
37            "mask_channel": mask_channel,
38        }
activation
confidence_threshold
threshold_from_both_sides
mask_channel
init_kwargs
def step(self, metric, epoch):
74    def step(self, metric, epoch):
75        pass
class ProbabilisticPseudoLabeler:
 78class ProbabilisticPseudoLabeler:
 79    """Compute pseudo labels from a Probabilistic UNet.
 80
 81    Args:
 82        activation: Activation function applied to the teacher prediction.
 83        confidence_threshold: Threshold for computing a mask for filterign the pseudo labels.
 84            If none is given no mask will be computed.
 85        threshold_from_both_sides: Whether to include both values bigger than the threshold
 86            and smaller than 1 - the thrhesold, or only values bigger than the threshold, in the mask.
 87            The former should be used for binary labels, the latter for for multiclass labels.
 88        prior_samples: The number of times to sample from the model distribution per input.
 89        consensus_masking: Whether to activate consensus masking in the label filter.
 90            If False, the weighted consensus response (weighted per-pixel response) is returned.
 91            If True, the masked consensus response (complete aggrement of pixels) is returned.
 92    """
 93    def __init__(
 94        self,
 95        activation: Optional[torch.nn.Module] = None,
 96        confidence_threshold: Optional[float] = None,
 97        threshold_from_both_sides: bool = True,
 98        prior_samples: int = 16,
 99        consensus_masking: bool = False,
100    ):
101        self.activation = activation
102        self.confidence_threshold = confidence_threshold
103        self.threshold_from_both_sides = threshold_from_both_sides
104        self.prior_samples = prior_samples
105        self.consensus_masking = consensus_masking
106        # TODO serialize the class names and kwargs for activation instead
107        self.init_kwargs = {
108            "activation": None, "confidence_threshold": confidence_threshold,
109            "threshold_from_both_sides": threshold_from_both_sides
110        }
111
112    def _compute_label_mask_both_sides(self, pseudo_labels):
113        upper_threshold = self.confidence_threshold
114        lower_threshold = 1.0 - self.confidence_threshold
115        mask = [
116            torch.where((sample >= upper_threshold) + (sample <= lower_threshold), torch.tensor(1.), torch.tensor(0.))
117            for sample in pseudo_labels
118        ]
119        return mask
120
121    def _compute_label_mask_one_side(self, pseudo_labels):
122        mask = [
123            torch.where((sample >= self.confidence_threshold), torch.tensor(1.), torch.tensor(0.))
124            for sample in pseudo_labels
125        ]
126        return mask
127
128    def __call__(self, teacher: torch.nn.Module, input_: torch.Tensor) -> torch.Tensor:
129        """Compute pseudo-labels.
130
131        Args:
132            teacher: The teacher model. Must be a `torch_em.model.probabilistic_unet.ProbabilisticUNet`.
133            input_: The input for this batch.
134
135        Returns:
136            The pseudo-labels.
137        """
138        teacher.forward(input_)
139        if self.activation is not None:
140            pseudo_labels = [self.activation(teacher.sample()) for _ in range(self.prior_samples)]
141        else:
142            pseudo_labels = [teacher.sample() for _ in range(self.prior_samples)]
143        pseudo_labels = torch.stack(pseudo_labels, dim=0).sum(dim=0)/self.prior_samples
144
145        if self.confidence_threshold is None:
146            label_mask = None
147        else:
148            label_mask = self._compute_label_mask_both_sides(pseudo_labels) if self.threshold_from_both_sides \
149                else self._compute_label_mask_one_side(pseudo_labels)
150            label_mask = torch.stack(label_mask, dim=0).sum(dim=0)/self.prior_samples
151            if self.consensus_masking:
152                label_mask = torch.where(label_mask == 1, 1, 0)
153
154        return pseudo_labels, label_mask
155
156    def step(self, metric, epoch):
157        pass

Compute pseudo labels from a Probabilistic UNet.

Arguments:
  • activation: Activation function applied to the teacher prediction.
  • confidence_threshold: Threshold for computing a mask for filterign the pseudo labels. If none is given no mask will be computed.
  • threshold_from_both_sides: Whether to include both values bigger than the threshold and smaller than 1 - the thrhesold, or only values bigger than the threshold, in the mask. The former should be used for binary labels, the latter for for multiclass labels.
  • prior_samples: The number of times to sample from the model distribution per input.
  • consensus_masking: Whether to activate consensus masking in the label filter. If False, the weighted consensus response (weighted per-pixel response) is returned. If True, the masked consensus response (complete aggrement of pixels) is returned.
ProbabilisticPseudoLabeler( activation: Optional[torch.nn.modules.module.Module] = None, confidence_threshold: Optional[float] = None, threshold_from_both_sides: bool = True, prior_samples: int = 16, consensus_masking: bool = False)
 93    def __init__(
 94        self,
 95        activation: Optional[torch.nn.Module] = None,
 96        confidence_threshold: Optional[float] = None,
 97        threshold_from_both_sides: bool = True,
 98        prior_samples: int = 16,
 99        consensus_masking: bool = False,
100    ):
101        self.activation = activation
102        self.confidence_threshold = confidence_threshold
103        self.threshold_from_both_sides = threshold_from_both_sides
104        self.prior_samples = prior_samples
105        self.consensus_masking = consensus_masking
106        # TODO serialize the class names and kwargs for activation instead
107        self.init_kwargs = {
108            "activation": None, "confidence_threshold": confidence_threshold,
109            "threshold_from_both_sides": threshold_from_both_sides
110        }
activation
confidence_threshold
threshold_from_both_sides
prior_samples
consensus_masking
init_kwargs
def step(self, metric, epoch):
156    def step(self, metric, epoch):
157        pass
class ScheduledPseudoLabeler:
160class ScheduledPseudoLabeler:
161    """
162    Implement scheduled pseudo-labeling with dynamic confidence-threshold updates.
163
164    Pseudo labels are generated from a teacher model prediction and can be filtered
165    by a confidence mask. The confidence threshold can be adapted over time either
166    by decreasing it based on a monitored metric (plateau behavior) or by increasing
167    it with a fixed epoch schedule.
168
169    Args:
170        activation: Activation function applied to the teacher prediction.
171        confidence_threshold: Threshold for computing a mask for filtering the pseudo labels.
172            If None is given, no mask will be computed.
173        increase: If True, increase the confidence threshold over time according to
174            a fixed schedule. If False, decrease it based on plateau detection.
175        last_step_epoch: Last epoch at which threshold increase is allowed when
176            `increase=True`.
177        threshold_from_both_sides: Whether to include values larger than the
178            threshold and smaller than 1 - the threshold in the mask, or only values
179            larger than the threshold. The former should be used for binary labels,
180            the latter for multiclass labels.
181        mode: Determines whether the confidence threshold reduction is triggered by a "min" or "max" metric.
182            - 'min': A lower value of the monitored metric is considered better (e.g., loss).
183            - 'max': A higher value of the monitored metric is considered better (e.g., accuracy).
184        factor: Update size for confidence-threshold scheduling. Interpreted as a
185            multiplicative factor for `threshold_mode='rel'` and as an additive step
186            for `threshold_mode='abs'`.
187        patience: Number of epochs (with no improvement) after which the confidence threshold will be reduced.
188        threshold: Threshold value for determining a significant improvement in the performance metric
189            to reset the patience counter. This can be relative (percentage improvement)
190            or absolute depending on `threshold_mode`.
191        threshold_mode: Determines whether the `threshold` is interpreted as a relative improvement ('rel')
192            or an absolute improvement ('abs').
193        min_ct: Minimum allowed confidence threshold. The threshold will not be reduced below this value.
194        max_ct: Maximum allowed confidence threshold. The threshold will not be increased above this value.
195        eps: A small value to avoid floating-point precision errors during threshold comparison.
196        warm_up_epochs: Number of warm-up epochs. At the end of warm-up,
197            `confidence_threshold` is set to `max_ct`. This is intended for
198            decreasing-threshold scheduling (`increase=False`).
199        mask_channel: Specific channel to use for confidence masking. If set, the threshold
200            is applied only to that channel and the resulting mask is broadcast to all channels.
201        verbose: If True, prints messages when the confidence threshold is updated.
202    """
203
204    def __init__(
205        self,
206        activation: Optional[Union[torch.nn.Module, Callable]] = None,
207        confidence_threshold: Optional[float] = None,
208        increase: bool = False,
209        last_step_epoch: int = None,
210        threshold_from_both_sides: bool = True,
211        mode: Literal["min", "max"] = "min",
212        factor: float = 0.05,
213        patience: int = 10,
214        threshold: float = 1e-4,
215        threshold_mode: Literal["rel", "abs"] = "abs",
216        min_ct: float = 0.5,
217        max_ct: float = 0.95,
218        eps: float = 1e-8,
219        warm_up_epochs: int = 0,
220        mask_channel: Optional[int] = None,
221        verbose: bool = True,
222    ):
223        self.activation = activation
224        self.confidence_threshold = confidence_threshold
225        self.increase = increase
226        self.threshold_from_both_sides = threshold_from_both_sides
227        self.init_kwargs = {
228            "activation": None, "confidence_threshold": confidence_threshold,
229            "threshold_from_both_sides": threshold_from_both_sides,
230            "mask_channel": mask_channel,
231        }
232        # scheduler arguments
233        if mode not in {"min", "max"}:
234            raise ValueError(f"Invalid mode: {mode}. Mode should be 'min' or 'max'.")
235        self.mode = mode
236
237        assert factor < 1, f"Factor must be smaller than 1, got {factor}"
238        self.factor = factor
239
240        self.patience = patience
241        self.threshold = threshold
242
243        if threshold_mode not in {"rel", "abs"}:
244            raise ValueError(f"Invalid threshold mode: {mode}. Threshold mode should be 'rel' or 'abs'.")
245        self.threshold_mode = threshold_mode
246
247        self.min_ct = min_ct
248        self.max_ct = max_ct
249        self.eps = eps
250        self.warm_up_epochs = warm_up_epochs
251
252        if self.increase and self.warm_up_epochs > 0:
253            raise ValueError("warm_up_epochs > 0 is only supported when increase=False.")
254
255        self.mask_channel = mask_channel
256        self.verbose = verbose
257
258        if mode == "min":
259            self.best = float("inf")
260        else:  # mode == "max":
261            self.best = float("-inf")
262
263        # self.best = 0
264        self.num_bad_epochs: int = 0
265        self.last_epoch = 0
266
267        if self.increase:
268            self.last_step_epoch = last_step_epoch
269
270            n_ct = len(np.arange(self.min_ct, self.max_ct + self.factor / 2, self.factor))
271            n_increments = n_ct - 1
272
273            if n_increments <= 0:
274                # nothing to increase
275                self.step_epoch = 0
276            else:
277                # compute initial step size; enforce minimum step size of 1
278                self.step_epoch = max(1, int(np.floor(self.last_step_epoch / n_increments)))
279
280                # ensure max_ct is reachable
281                required_epochs = self.step_epoch * n_increments
282                if self.last_step_epoch < required_epochs:
283                    self.last_step_epoch = required_epochs
284
285            print(
286                f"ScheduledPseudoLabeler: Increasing confidence_threshold every {self.step_epoch} epochs;",
287                f"until epoch {self.last_step_epoch}"
288            )
289
290    def _compute_label_mask_both_sides(self, pseudo_labels):
291        upper_threshold = self.confidence_threshold
292        lower_threshold = 1.0 - self.confidence_threshold
293        mask = ((pseudo_labels >= upper_threshold) + (pseudo_labels <= lower_threshold)).to(dtype=torch.float32)
294        return mask
295
296    def _compute_label_mask_one_side(self, pseudo_labels):
297        mask = (pseudo_labels >= self.confidence_threshold)
298        return mask
299
300    def __call__(self, teacher, input_):
301        pseudo_labels = teacher(input_)
302        if self.activation is not None:
303            pseudo_labels = self.activation(pseudo_labels)
304        if self.confidence_threshold is None:
305            label_mask = None
306        else:
307            mask_input = pseudo_labels if self.mask_channel is None \
308                else pseudo_labels[:, [self.mask_channel]]
309            label_mask = self._compute_label_mask_both_sides(mask_input) if self.threshold_from_both_sides \
310                else self._compute_label_mask_one_side(mask_input)
311            if self.mask_channel is not None:
312                label_mask = label_mask.expand_as(pseudo_labels)
313        return pseudo_labels, label_mask
314
315    def end_warm_up(self):
316        self.confidence_threshold = self.max_ct
317        print(f"End of warm-up phase reached: setting confidence threshold to {self.confidence_threshold}")
318
319    def _is_better(self, a, best):
320        if self.mode == "min" and self.threshold_mode == "rel":
321            rel_epsilon = 1.0 - self.threshold
322            return a < best * rel_epsilon
323
324        elif self.mode == "min" and self.threshold_mode == "abs":
325            return a < best - self.threshold
326
327        elif self.mode == "max" and self.threshold_mode == "rel":
328            rel_epsilon = self.threshold + 1.0
329            return a > best * rel_epsilon
330
331        else:  # mode == 'max' and epsilon_mode == 'abs':
332            return a > best + self.threshold
333
334    def _reduce_ct(self, epoch):
335        old_ct = self.confidence_threshold
336        if self.threshold_mode == "rel":
337            new_ct = max(self.confidence_threshold * (1-self.factor), self.min_ct)
338        else:  # threshold_mode == 'abs':
339            new_ct = max(self.confidence_threshold - self.factor, self.min_ct)
340        if abs(old_ct - new_ct) > self.eps:
341            self.confidence_threshold = new_ct
342        if self.verbose:
343            print(f"Epoch {epoch}: reducing confidence threshold from {old_ct} to {self.confidence_threshold}")
344
345    def decrease_step(self, metric, epoch=None):
346        if epoch is None:
347            epoch = self.last_epoch + 1
348            self.last_epoch = epoch
349
350        # If the metric is None, reduce the confidence threshold every epoch
351        if metric is None:
352            if epoch == 0:
353                return
354            if epoch % self.patience == 0:
355                self._reduce_ct(epoch)
356            return
357
358        else:
359            current = float(metric)
360
361            if self._is_better(current, self.best):
362                self.best = current
363                self.num_bad_epochs = 0
364            else:
365                self.num_bad_epochs += 1
366
367            if self.num_bad_epochs > self.patience:
368                self._reduce_ct(epoch)
369                self.num_bad_epochs = 0
370
371    def _increase_ct(self, epoch):
372        old_ct = self.confidence_threshold
373        if self.threshold_mode == "rel":
374            new_ct = min(self.confidence_threshold * (1+self.factor), self.max_ct)
375        else:  # threshold_mode == 'abs':
376            new_ct = min(self.confidence_threshold + self.factor, self.max_ct)
377        if abs(old_ct - new_ct) > self.eps:
378            self.confidence_threshold = new_ct
379        if self.verbose:
380            print(f"Epoch {epoch}: increase confidence threshold from {old_ct} to {self.confidence_threshold}")
381
382    def increase_step(self, epoch):
383        if epoch > self.last_step_epoch:
384            return
385
386        if epoch % self.step_epoch == 0 and epoch != 0:
387            self._increase_ct(epoch)
388
389    def step(self, metric=None, epoch=None):
390        if epoch == self.warm_up_epochs and self.warm_up_epochs > 0:
391            self.end_warm_up()
392        elif epoch > self.warm_up_epochs:
393            if self.increase:
394                self.increase_step(epoch)
395            else:
396                self.decrease_step(metric, epoch)
397        else:
398            return

Implement scheduled pseudo-labeling with dynamic confidence-threshold updates.

Pseudo labels are generated from a teacher model prediction and can be filtered by a confidence mask. The confidence threshold can be adapted over time either by decreasing it based on a monitored metric (plateau behavior) or by increasing it with a fixed epoch schedule.

Arguments:
  • activation: Activation function applied to the teacher prediction.
  • confidence_threshold: Threshold for computing a mask for filtering the pseudo labels. If None is given, no mask will be computed.
  • increase: If True, increase the confidence threshold over time according to a fixed schedule. If False, decrease it based on plateau detection.
  • last_step_epoch: Last epoch at which threshold increase is allowed when increase=True.
  • threshold_from_both_sides: Whether to include values larger than the threshold and smaller than 1 - the threshold in the mask, or only values larger than the threshold. The former should be used for binary labels, the latter for multiclass labels.
  • mode: Determines whether the confidence threshold reduction is triggered by a "min" or "max" metric.
    • 'min': A lower value of the monitored metric is considered better (e.g., loss).
    • 'max': A higher value of the monitored metric is considered better (e.g., accuracy).
  • factor: Update size for confidence-threshold scheduling. Interpreted as a multiplicative factor for threshold_mode='rel' and as an additive step for threshold_mode='abs'.
  • patience: Number of epochs (with no improvement) after which the confidence threshold will be reduced.
  • threshold: Threshold value for determining a significant improvement in the performance metric to reset the patience counter. This can be relative (percentage improvement) or absolute depending on threshold_mode.
  • threshold_mode: Determines whether the threshold is interpreted as a relative improvement ('rel') or an absolute improvement ('abs').
  • min_ct: Minimum allowed confidence threshold. The threshold will not be reduced below this value.
  • max_ct: Maximum allowed confidence threshold. The threshold will not be increased above this value.
  • eps: A small value to avoid floating-point precision errors during threshold comparison.
  • warm_up_epochs: Number of warm-up epochs. At the end of warm-up, confidence_threshold is set to max_ct. This is intended for decreasing-threshold scheduling (increase=False).
  • mask_channel: Specific channel to use for confidence masking. If set, the threshold is applied only to that channel and the resulting mask is broadcast to all channels.
  • verbose: If True, prints messages when the confidence threshold is updated.
ScheduledPseudoLabeler( activation: Union[torch.nn.modules.module.Module, Callable, NoneType] = None, confidence_threshold: Optional[float] = None, increase: bool = False, last_step_epoch: int = None, threshold_from_both_sides: bool = True, mode: Literal['min', 'max'] = 'min', factor: float = 0.05, patience: int = 10, threshold: float = 0.0001, threshold_mode: Literal['rel', 'abs'] = 'abs', min_ct: float = 0.5, max_ct: float = 0.95, eps: float = 1e-08, warm_up_epochs: int = 0, mask_channel: Optional[int] = None, verbose: bool = True)
204    def __init__(
205        self,
206        activation: Optional[Union[torch.nn.Module, Callable]] = None,
207        confidence_threshold: Optional[float] = None,
208        increase: bool = False,
209        last_step_epoch: int = None,
210        threshold_from_both_sides: bool = True,
211        mode: Literal["min", "max"] = "min",
212        factor: float = 0.05,
213        patience: int = 10,
214        threshold: float = 1e-4,
215        threshold_mode: Literal["rel", "abs"] = "abs",
216        min_ct: float = 0.5,
217        max_ct: float = 0.95,
218        eps: float = 1e-8,
219        warm_up_epochs: int = 0,
220        mask_channel: Optional[int] = None,
221        verbose: bool = True,
222    ):
223        self.activation = activation
224        self.confidence_threshold = confidence_threshold
225        self.increase = increase
226        self.threshold_from_both_sides = threshold_from_both_sides
227        self.init_kwargs = {
228            "activation": None, "confidence_threshold": confidence_threshold,
229            "threshold_from_both_sides": threshold_from_both_sides,
230            "mask_channel": mask_channel,
231        }
232        # scheduler arguments
233        if mode not in {"min", "max"}:
234            raise ValueError(f"Invalid mode: {mode}. Mode should be 'min' or 'max'.")
235        self.mode = mode
236
237        assert factor < 1, f"Factor must be smaller than 1, got {factor}"
238        self.factor = factor
239
240        self.patience = patience
241        self.threshold = threshold
242
243        if threshold_mode not in {"rel", "abs"}:
244            raise ValueError(f"Invalid threshold mode: {mode}. Threshold mode should be 'rel' or 'abs'.")
245        self.threshold_mode = threshold_mode
246
247        self.min_ct = min_ct
248        self.max_ct = max_ct
249        self.eps = eps
250        self.warm_up_epochs = warm_up_epochs
251
252        if self.increase and self.warm_up_epochs > 0:
253            raise ValueError("warm_up_epochs > 0 is only supported when increase=False.")
254
255        self.mask_channel = mask_channel
256        self.verbose = verbose
257
258        if mode == "min":
259            self.best = float("inf")
260        else:  # mode == "max":
261            self.best = float("-inf")
262
263        # self.best = 0
264        self.num_bad_epochs: int = 0
265        self.last_epoch = 0
266
267        if self.increase:
268            self.last_step_epoch = last_step_epoch
269
270            n_ct = len(np.arange(self.min_ct, self.max_ct + self.factor / 2, self.factor))
271            n_increments = n_ct - 1
272
273            if n_increments <= 0:
274                # nothing to increase
275                self.step_epoch = 0
276            else:
277                # compute initial step size; enforce minimum step size of 1
278                self.step_epoch = max(1, int(np.floor(self.last_step_epoch / n_increments)))
279
280                # ensure max_ct is reachable
281                required_epochs = self.step_epoch * n_increments
282                if self.last_step_epoch < required_epochs:
283                    self.last_step_epoch = required_epochs
284
285            print(
286                f"ScheduledPseudoLabeler: Increasing confidence_threshold every {self.step_epoch} epochs;",
287                f"until epoch {self.last_step_epoch}"
288            )
activation
confidence_threshold
increase
threshold_from_both_sides
init_kwargs
mode
factor
patience
threshold
threshold_mode
min_ct
max_ct
eps
warm_up_epochs
mask_channel
verbose
num_bad_epochs: int
last_epoch
def end_warm_up(self):
315    def end_warm_up(self):
316        self.confidence_threshold = self.max_ct
317        print(f"End of warm-up phase reached: setting confidence threshold to {self.confidence_threshold}")
def decrease_step(self, metric, epoch=None):
345    def decrease_step(self, metric, epoch=None):
346        if epoch is None:
347            epoch = self.last_epoch + 1
348            self.last_epoch = epoch
349
350        # If the metric is None, reduce the confidence threshold every epoch
351        if metric is None:
352            if epoch == 0:
353                return
354            if epoch % self.patience == 0:
355                self._reduce_ct(epoch)
356            return
357
358        else:
359            current = float(metric)
360
361            if self._is_better(current, self.best):
362                self.best = current
363                self.num_bad_epochs = 0
364            else:
365                self.num_bad_epochs += 1
366
367            if self.num_bad_epochs > self.patience:
368                self._reduce_ct(epoch)
369                self.num_bad_epochs = 0
def increase_step(self, epoch):
382    def increase_step(self, epoch):
383        if epoch > self.last_step_epoch:
384            return
385
386        if epoch % self.step_epoch == 0 and epoch != 0:
387            self._increase_ct(epoch)
def step(self, metric=None, epoch=None):
389    def step(self, metric=None, epoch=None):
390        if epoch == self.warm_up_epochs and self.warm_up_epochs > 0:
391            self.end_warm_up()
392        elif epoch > self.warm_up_epochs:
393            if self.increase:
394                self.increase_step(epoch)
395            else:
396                self.decrease_step(metric, epoch)
397        else:
398            return