torch_em.self_training.fix_match

  1import time
  2from typing import Callable, List, Optional
  3
  4import torch
  5import torch_em
  6from torch_em.util import get_constructor_arguments
  7
  8from .logger import SelfTrainingTensorboardLogger
  9from .mean_teacher import Dummy
 10from ..transform.invertible_augmentations import InvertibleAugmenter
 11
 12
 13class FixMatchTrainer(torch_em.trainer.DefaultTrainer):
 14    """Trainer for semi-supervised learning and domain adaptation following the FixMatch approach.
 15
 16    FixMatch was introduced by Sohn et al. in https://arxiv.org/abs/2001.07685).
 17    It uses a teacher model derived from the student model via weight sharing to predict pseudo-labels
 18    on unlabeled data. We support two training strategies:
 19    - Joint training on labeled and unlabeled data (with a supervised and unsupervised loss function).
 20    - Taining only on the unsupervised data.
 21
 22    This class expects the following data loaders:
 23    - unsupervised_train_loader: Returns two augmentations (weak and strong) of the same input.
 24    - supervised_train_loader (optional): Returns input and labels.
 25    - unsupervised_val_loader (optional): Same as unsupervised_train_loader
 26    - supervised_val_loader (optional): Same as supervised_train_loader
 27    At least one of unsupervised_val_loader and supervised_val_loader must be given.
 28
 29    The following arguments can be used to customize the pseudo labeling:
 30    - pseudo_labeler: to compute the psuedo-labels
 31        - Parameters: model, teacher_input
 32        - Returns: pseudo_labels, label_filter (<- label filter can for example be mask, weight or None)
 33    - unsupervised_loss: the loss between model predictions and pseudo labels
 34        - Parameters: model, model_input, pseudo_labels, label_filter
 35        - Returns: loss
 36    - supervised_loss (optional): the supervised loss function
 37        - Parameters: model, input, labels
 38        - Returns: loss
 39    - unsupervised_loss_and_metric (optional): the unsupervised loss function and metric
 40        - Parameters: model, model_input, pseudo_labels, label_filter
 41        - Returns: loss, metric
 42    - supervised_loss_and_metric (optional): the supervised loss function and metric
 43        - Parameters: model, input, labels
 44        - Returns: loss, metric
 45    At least one of unsupervised_loss_and_metric and supervised_loss_and_metric must be given.
 46
 47    Note: adjust the batch size ratio between the 'unsupervised_train_loader' and 'supervised_train_loader'
 48    for setting the ratio between supervised and unsupervised training samples.
 49
 50    Args:
 51        model: The model to be trained.
 52        unsupervised_train_loader: The loader for unsupervised training.
 53        unsupervised_loss: The loss for unsupervised training.
 54        pseudo_labeler: The pseudo labeler that predicts labels in unsupervised training.
 55        supervised_train_loader: The loader for supervised training.
 56        supervised_loss: The loss for supervised training.
 57        unsupervised_loss_and_metric: The loss and metric for unsupervised training.
 58        supervised_loss_and_metric: The loss and metrhic for supervised training.
 59        logger: The logger.
 60        source_distribution: The ratio of labels in the source label distribution.
 61            If given, the predicted distribution of the trained model will be regularized to
 62            match this source label distribution.
 63        separate_backward: Whether to backpropagate each loss term separately to reduce peak memory.
 64            The default uses one backward pass on the combined loss.
 65        kwargs: Additional keyword arguments for `torch_em.trainer.DefaultTrainer`.
 66    """
 67
 68    def __init__(
 69        self,
 70        model: torch.nn.Module,
 71        unsupervised_train_loader: torch.utils.data.DataLoader,
 72        unsupervised_loss: torch.utils.data.DataLoader,
 73        pseudo_labeler: Callable,
 74        supervised_train_loader: Optional[torch.utils.data.DataLoader] = None,
 75        unsupervised_val_loader: Optional[torch.utils.data.DataLoader] = None,
 76        supervised_val_loader: Optional[torch.utils.data.DataLoader] = None,
 77        supervised_loss: Optional[Callable] = None,
 78        unsupervised_loss_and_metric: Optional[Callable] = None,
 79        supervised_loss_and_metric: Optional[Callable] = None,
 80        logger=SelfTrainingTensorboardLogger,
 81        source_distribution: List[float] = None,
 82        separate_backward: bool = False,
 83        **kwargs,
 84    ):
 85        self.separate_backward = separate_backward
 86        # Do we have supervised data or not?
 87        if supervised_train_loader is None:
 88            # No. -> We use the unsupervised training logic.
 89            train_loader = unsupervised_train_loader
 90            self._train_epoch_impl = self._train_epoch_unsupervised
 91        else:
 92            # Yes. -> We use the semi-supervised training logic.
 93            assert supervised_loss is not None
 94            train_loader = supervised_train_loader if len(supervised_train_loader) < len(unsupervised_train_loader)\
 95                else unsupervised_train_loader
 96            self._train_epoch_impl = self._train_epoch_semisupervised
 97
 98        self.unsupervised_train_loader = unsupervised_train_loader
 99        self.supervised_train_loader = supervised_train_loader
100
101        # Check that we have at least one of supvervised / unsupervised val loader.
102        assert sum((
103            supervised_val_loader is not None,
104            unsupervised_val_loader is not None,
105        )) > 0
106        self.supervised_val_loader = supervised_val_loader
107        self.unsupervised_val_loader = unsupervised_val_loader
108
109        if self.unsupervised_val_loader is None:
110            val_loader = self.supervised_val_loader
111        else:
112            val_loader = self.unsupervised_val_loader
113
114        # Check that we have at least one of supvervised / unsupervised loss and metric.
115        assert sum((
116            supervised_loss_and_metric is not None,
117            unsupervised_loss_and_metric is not None,
118        )) > 0
119        self.supervised_loss_and_metric = supervised_loss_and_metric
120        self.unsupervised_loss_and_metric = unsupervised_loss_and_metric
121
122        # train_loader, val_loader, loss and metric may be unnecessarily deserialized
123        kwargs.pop("train_loader", None)
124        kwargs.pop("val_loader", None)
125        kwargs.pop("metric", None)
126        kwargs.pop("loss", None)
127        super().__init__(
128            model=model, train_loader=train_loader, val_loader=val_loader,
129            loss=Dummy(), metric=Dummy(), logger=logger, **kwargs
130        )
131
132        self.unsupervised_loss = unsupervised_loss
133        self.supervised_loss = supervised_loss
134
135        self.pseudo_labeler = pseudo_labeler
136
137        if source_distribution is None:
138            self.source_distribution = None
139        else:
140            self.source_distribution = torch.FloatTensor(source_distribution).to(self.device)
141
142        self._kwargs = kwargs
143
144    #
145    # functionality for saving checkpoints and initialization
146    #
147
148    def save_checkpoint(self, name, current_metric, best_metric, **extra_save_dict):
149        """@private
150        """
151        train_loader_kwargs = get_constructor_arguments(self.train_loader)
152        val_loader_kwargs = get_constructor_arguments(self.val_loader)
153        extra_state = {
154            "init": {
155                "train_loader_kwargs": train_loader_kwargs,
156                "train_dataset": self.train_loader.dataset,
157                "val_loader_kwargs": val_loader_kwargs,
158                "val_dataset": self.val_loader.dataset,
159                "loss_class": "torch_em.self_training.mean_teacher.Dummy",
160                "loss_kwargs": {},
161                "metric_class": "torch_em.self_training.mean_teacher.Dummy",
162                "metric_kwargs": {},
163            },
164        }
165        extra_state.update(**extra_save_dict)
166        super().save_checkpoint(name, current_metric, best_metric, **extra_state)
167
168    # Distribution alignment:
169    # Encourages the distribution of the model's generated pseudo labels to match the marginal
170    # distribution of pseudo labels from the source transfer (key idea: to maximize the mutual information).
171    def get_distribution_alignment(self, pseudo_labels, label_threshold=0.5):
172        """@private
173        """
174        if self.source_distribution is not None:
175            pseudo_labels_binary = torch.where(pseudo_labels >= label_threshold, 1, 0)
176            _, target_distribution = torch.unique(pseudo_labels_binary, return_counts=True)
177            target_distribution = target_distribution / target_distribution.sum()
178            distribution_ratio = self.source_distribution / target_distribution
179            pseudo_labels = torch.where(
180                pseudo_labels < label_threshold,
181                pseudo_labels * distribution_ratio[0],
182                pseudo_labels * distribution_ratio[1]
183            ).clip(0, 1)
184
185        return pseudo_labels
186
187    #
188    # training and validation functionality
189    #
190
191    def _train_epoch_unsupervised(self, progress, forward_context, backprop):
192        self.model.train()
193
194        n_iter = 0
195        t_per_iter = time.time()
196
197        # Sample from both the supervised and unsupervised loader.
198        for xu1, xu2 in self.unsupervised_train_loader:
199            xu1, xu2 = xu1.to(self.device, non_blocking=True), xu2.to(self.device, non_blocking=True)
200
201            teacher_input, model_input = xu1, xu2
202
203            with forward_context(), torch.no_grad():
204                # Compute the pseudo labels.
205                pseudo_labels, label_filter = self.pseudo_labeler(self.model, teacher_input)
206
207            pseudo_labels = pseudo_labels.detach()
208            if label_filter is not None:
209                label_filter = label_filter.detach()
210
211            # Perform distribution alignment for pseudo labels
212            pseudo_labels = self.get_distribution_alignment(pseudo_labels)
213
214            self.optimizer.zero_grad()
215            # Perform unsupervised training
216            with forward_context():
217                loss = self.unsupervised_loss(self.model, model_input, pseudo_labels, label_filter)
218
219            backprop(loss)
220
221            if self.logger is not None:
222                with torch.no_grad(), forward_context():
223                    pred = self.model(model_input) if self._iteration % self.log_image_interval == 0 else None
224                self.logger.log_train_unsupervised(
225                    self._iteration, loss, xu1, xu2, pred, pseudo_labels, label_filter
226                )
227                lr = [pm["lr"] for pm in self.optimizer.param_groups][0]
228                self.logger.log_lr(self._iteration, lr)
229                if self.pseudo_labeler.confidence_threshold is not None:
230                    self.logger.log_ct(self._iteration, self.pseudo_labeler.confidence_threshold)
231
232            self._iteration += 1
233            n_iter += 1
234            if self._iteration >= self.max_iteration:
235                break
236            progress.update(1)
237
238        t_per_iter = (time.time() - t_per_iter) / n_iter
239        return t_per_iter
240
241    def _train_epoch_semisupervised(self, progress, forward_context, backprop):
242        self.model.train()
243
244        n_iter = 0
245        t_per_iter = time.time()
246
247        # Sample from both the supervised and unsupervised loader.
248        for (xs, ys), (xu1, xu2) in zip(self.supervised_train_loader, self.unsupervised_train_loader):
249            xs, ys = xs.to(self.device, non_blocking=True), ys.to(self.device, non_blocking=True)
250            xu1, xu2 = xu1.to(self.device, non_blocking=True), xu2.to(self.device, non_blocking=True)
251
252            # Perform supervised training.
253            self.optimizer.zero_grad()
254            with forward_context():
255                # We pass the model, the input and the labels to the supervised loss function,
256                # so that how the loss is calculated stays flexible, e.g. to enable ELBO for PUNet.
257                supervised_loss = self.supervised_loss(self.model, xs, ys)
258
259            if self.separate_backward:
260                # Free supervised activations before the unsupervised forward pass.
261                self._accumulate_gradients(supervised_loss / 2)
262
263            teacher_input, model_input = xu1, xu2
264
265            with forward_context(), torch.no_grad():
266                # Compute the pseudo labels.
267                pseudo_labels, label_filter = self.pseudo_labeler(self.model, teacher_input)
268
269            pseudo_labels = pseudo_labels.detach()
270            if label_filter is not None:
271                label_filter = label_filter.detach()
272
273            # Perform distribution alignment for pseudo labels
274            pseudo_labels = self.get_distribution_alignment(pseudo_labels)
275
276            # Perform unsupervised training
277            with forward_context():
278                unsupervised_loss = self.unsupervised_loss(self.model, model_input, pseudo_labels, label_filter)
279
280            loss = (supervised_loss + unsupervised_loss) / 2
281            backprop(unsupervised_loss / 2 if self.separate_backward else loss)
282
283            if self.logger is not None:
284                with torch.no_grad(), forward_context():
285                    unsup_pred = self.model(model_input) if self._iteration % self.log_image_interval == 0 else None
286                    supervised_pred = self.model(xs) if self._iteration % self.log_image_interval == 0 else None
287
288                self.logger.log_train_supervised(self._iteration, supervised_loss, xs, ys, supervised_pred)
289                self.logger.log_train_unsupervised(
290                    self._iteration, unsupervised_loss, xu1, xu2, unsup_pred, pseudo_labels, label_filter
291                )
292
293                self.logger.log_combined_loss(self._iteration, loss)
294                lr = [pm["lr"] for pm in self.optimizer.param_groups][0]
295                self.logger.log_lr(self._iteration, lr)
296                if self.pseudo_labeler.confidence_threshold is not None:
297                    self.logger.log_ct(self._iteration, self.pseudo_labeler.confidence_threshold)
298
299            self._iteration += 1
300            n_iter += 1
301            if self._iteration >= self.max_iteration:
302                break
303            progress.update(1)
304
305        t_per_iter = (time.time() - t_per_iter) / n_iter
306        return t_per_iter
307
308    def _validate_supervised(self, forward_context):
309        metric_val = 0.0
310        loss_val = 0.0
311
312        for x, y in self.supervised_val_loader:
313            x, y = x.to(self.device, non_blocking=True), y.to(self.device, non_blocking=True)
314            with forward_context():
315                loss, metric = self.supervised_loss_and_metric(self.model, x, y)
316            loss_val += loss.item()
317            metric_val += metric.item()
318
319        metric_val /= len(self.supervised_val_loader)
320        loss_val /= len(self.supervised_val_loader)
321
322        if self.logger is not None:
323            with forward_context():
324                pred = self.model(x)
325            self.logger.log_validation_supervised(self._iteration, metric_val, loss_val, x, y, pred)
326
327        return metric_val
328
329    def _validate_unsupervised(self, forward_context):
330        metric_val = 0.0
331        loss_val = 0.0
332
333        for x1, x2 in self.unsupervised_val_loader:
334            x1, x2 = x1.to(self.device, non_blocking=True), x2.to(self.device, non_blocking=True)
335            teacher_input, model_input = x1, x2
336            with forward_context():
337                pseudo_labels, label_filter = self.pseudo_labeler(self.model, teacher_input)
338                loss, metric = self.unsupervised_loss_and_metric(self.model, model_input, pseudo_labels, label_filter)
339            loss_val += loss.item()
340            metric_val += metric.item()
341
342        metric_val /= len(self.unsupervised_val_loader)
343        loss_val /= len(self.unsupervised_val_loader)
344
345        if self.logger is not None:
346            with forward_context():
347                pred = self.model(model_input)
348            self.logger.log_validation_unsupervised(
349                self._iteration, metric_val, loss_val, x1, x2, pred, pseudo_labels, label_filter
350            )
351
352        return metric_val
353
354    def _validate_impl(self, forward_context):
355        self.model.eval()
356
357        with torch.no_grad():
358
359            if self.supervised_val_loader is None:
360                supervised_metric = None
361            else:
362                supervised_metric = self._validate_supervised(forward_context)
363
364            if self.unsupervised_val_loader is None:
365                unsupervised_metric = None
366            else:
367                unsupervised_metric = self._validate_unsupervised(forward_context)
368
369        if unsupervised_metric is None:
370            metric = supervised_metric
371        elif supervised_metric is None:
372            metric = unsupervised_metric
373        else:
374            metric = (supervised_metric + unsupervised_metric) / 2
375
376        return metric
377
378
379class FixMatchTrainerWithInvertibleAugmentations(FixMatchTrainer):
380    """Trainer for semi-supervised learning and domain adaptation following the FixMatch approach.
381
382    FixMatch was introduced by Sohn et al. in https://arxiv.org/abs/2001.07685).
383    It uses a teacher model derived from the student model via weight sharing to predict pseudo-labels
384    on unlabeled data. We support two training strategies:
385    - Joint training on labeled and unlabeled data (with a supervised and unsupervised loss function).
386    - Taining only on the unsupervised data.
387
388    This class expects the following data loaders:
389    - unsupervised_train_loader: Returns two augmentations (weak and strong) of the same input.
390    - supervised_train_loader (optional): Returns input and labels.
391    - unsupervised_val_loader (optional): Same as unsupervised_train_loader
392    - supervised_val_loader (optional): Same as supervised_train_loader
393    At least one of unsupervised_val_loader and supervised_val_loader must be given.
394
395    The augmenter defines separate invertible transforms for teacher and student inputs.
396    Teacher and student views are generated independently, and the corresponding inverse
397    transforms map predictions and pseudo-labels back into a shared reference frame.
398
399    The following arguments can be used to customize the pseudo labeling:
400    - pseudo_labeler: to compute the psuedo-labels
401        - Parameters: model, teacher_input
402        - Returns: pseudo_labels, label_filter (<- label filter can for example be mask, weight or None)
403    - unsupervised_loss: the loss between model predictions and pseudo labels
404        - Parameters: model, model_input, pseudo_labels, label_filter
405        - Returns: loss
406    - supervised_loss (optional): the supervised loss function
407        - Parameters: model, input, labels
408        - Returns: loss
409    - unsupervised_loss_and_metric (optional): the unsupervised loss function and metric
410        - Parameters: model, model_input, pseudo_labels, label_filter
411        - Returns: loss, metric
412    - supervised_loss_and_metric (optional): the supervised loss function and metric
413        - Parameters: model, input, labels
414        - Returns: loss, metric
415    At least one of unsupervised_loss_and_metric and supervised_loss_and_metric must be given.
416
417    Note: adjust the batch size ratio between the 'unsupervised_train_loader' and 'supervised_train_loader'
418    for setting the ratio between supervised and unsupervised training samples.
419
420    Args:
421        model: The model to be trained.
422        unsupervised_train_loader: The loader for unsupervised training.
423        unsupervised_loss: The loss for unsupervised training.
424        pseudo_labeler: The pseudo labeler that predicts labels in unsupervised training.
425        augmenter: Invertible augmenter providing separate teacher and student transforms,
426            including corresponding inverse transforms to align predictions in a common frame.
427        supervised_train_loader: The loader for supervised training.
428        supervised_loss: The loss for supervised training.
429        unsupervised_loss_and_metric: The loss and metric for unsupervised training.
430        supervised_loss_and_metric: The loss and metrhic for supervised training.
431        logger: The logger.
432        source_distribution: The ratio of labels in the source label distribution.
433            If given, the predicted distribution of the trained model will be regularized to
434            match this source label distribution.
435        separate_backward: Whether to backpropagate each loss term separately to reduce peak memory.
436            The default uses one backward pass on the combined loss.
437        kwargs: Additional keyword arguments for `torch_em.trainer.DefaultTrainer`.
438    """
439
440    def __init__(
441        self,
442        model: torch.nn.Module,
443        unsupervised_train_loader: torch.utils.data.DataLoader,
444        unsupervised_loss: torch.utils.data.DataLoader,
445        pseudo_labeler: Callable,
446        augmenter: InvertibleAugmenter,
447        supervised_train_loader: Optional[torch.utils.data.DataLoader] = None,
448        unsupervised_val_loader: Optional[torch.utils.data.DataLoader] = None,
449        supervised_val_loader: Optional[torch.utils.data.DataLoader] = None,
450        supervised_loss: Optional[Callable] = None,
451        unsupervised_loss_and_metric: Optional[Callable] = None,
452        supervised_loss_and_metric: Optional[Callable] = None,
453        logger=SelfTrainingTensorboardLogger,
454        source_distribution: List[float] = None,
455        separate_backward: bool = False,
456        **kwargs,
457    ):
458        super().__init__(
459            model=model,
460            unsupervised_train_loader=unsupervised_train_loader,
461            unsupervised_loss=unsupervised_loss,
462            pseudo_labeler=pseudo_labeler,
463            supervised_train_loader=supervised_train_loader,
464            unsupervised_val_loader=unsupervised_val_loader,
465            supervised_val_loader=supervised_val_loader,
466            supervised_loss=supervised_loss,
467            unsupervised_loss_and_metric=unsupervised_loss_and_metric,
468            supervised_loss_and_metric=supervised_loss_and_metric,
469            logger=logger,
470            source_distribution=source_distribution,
471            separate_backward=separate_backward,
472            **kwargs,
473        )
474
475        self.augmenter = augmenter
476
477    #
478    # training and validation functionality
479    #
480
481    def _train_epoch_unsupervised(self, progress, forward_context, backprop):
482        self.model.train()
483
484        n_iter = 0
485        t_per_iter = time.time()
486
487        # Sample from both the supervised and unsupervised loader.
488        for xu in self.unsupervised_train_loader:
489            self.augmenter.reset_all()
490            xu = xu.to(self.device, non_blocking=True)
491
492            xu1, xu2 = self.augmenter.teacher.transform(xu), self.augmenter.student.transform(xu)
493            teacher_input, model_input = xu1, xu2
494
495            with forward_context(), torch.no_grad():
496                # Compute the pseudo labels.
497                pseudo_labels, label_filter = self.pseudo_labeler(self.model, teacher_input)
498
499            pseudo_labels = pseudo_labels.detach()
500            if label_filter is not None:
501                label_filter = label_filter.detach()
502
503            # Perform distribution alignment for pseudo labels, then invert into the reference frame.
504            pseudo_labels = self.get_distribution_alignment(pseudo_labels)
505            pseudo_labels_inv = self.augmenter.teacher.reverse_transform(pseudo_labels)
506            label_filter_inv = (
507                self.augmenter.teacher.reverse_transform(label_filter)
508                if label_filter is not None else None
509            )
510
511            self.optimizer.zero_grad()
512            # Perform unsupervised training
513            with forward_context():
514                pred = self.model(model_input)
515                pred_inv = self.augmenter.student.reverse_transform(pred)
516                loss = self.unsupervised_loss(pred_inv, pseudo_labels_inv, label_filter_inv)
517
518            backprop(loss)
519
520            if self.logger is not None:
521                with torch.no_grad(), forward_context():
522                    pred = pred if self._iteration % self.log_image_interval == 0 else None
523                self.logger.log_train_unsupervised(
524                    self._iteration, loss, xu, xu, pred_inv, pseudo_labels_inv, label_filter_inv
525                )
526                self.logger.log_train_augmentations(
527                    self._iteration, xu1, xu2, pseudo_labels, pred,
528                )
529                lr = [pm["lr"] for pm in self.optimizer.param_groups][0]
530                self.logger.log_lr(self._iteration, lr)
531                if self.pseudo_labeler.confidence_threshold is not None:
532                    self.logger.log_ct(self._iteration, self.pseudo_labeler.confidence_threshold)
533
534            self._iteration += 1
535            n_iter += 1
536            if self._iteration >= self.max_iteration:
537                break
538            progress.update(1)
539
540        t_per_iter = (time.time() - t_per_iter) / n_iter
541        return t_per_iter
542
543    def _train_epoch_semisupervised(self, progress, forward_context, backprop):
544        self.model.train()
545
546        n_iter = 0
547        t_per_iter = time.time()
548
549        # Sample from both the supervised and unsupervised loader.
550        for (xs, ys), xu in zip(self.supervised_train_loader, self.unsupervised_train_loader):
551            self.augmenter.reset_all()
552            xs, ys = xs.to(self.device, non_blocking=True), ys.to(self.device, non_blocking=True)
553            xu = xu.to(self.device, non_blocking=True)
554
555            xu1, xu2 = self.augmenter.teacher.transform(xu), self.augmenter.student.transform(xu)
556            teacher_input, model_input = xu1, xu2
557
558            # Perform supervised training.
559            self.optimizer.zero_grad()
560            with forward_context():
561                # We pass the model, the input and the labels to the supervised loss function,
562                # so that how the loss is calculated stays flexible, e.g. to enable ELBO for PUNet.
563                supervised_pred = self.model(xs)
564                supervised_loss = self.supervised_loss(supervised_pred, ys)
565
566            if self.separate_backward:
567                # Free supervised activations before the unsupervised forward pass.
568                self._accumulate_gradients(supervised_loss / 2)
569
570            with forward_context(), torch.no_grad():
571                # Compute the pseudo labels.
572                pseudo_labels, label_filter = self.pseudo_labeler(self.model, teacher_input)
573
574            pseudo_labels = pseudo_labels.detach()
575            if label_filter is not None:
576                label_filter = label_filter.detach()
577
578            # Perform distribution alignment for pseudo labels, then invert into the reference frame.
579            pseudo_labels = self.get_distribution_alignment(pseudo_labels)
580            pseudo_labels_inv = self.augmenter.teacher.reverse_transform(pseudo_labels)
581            label_filter_inv = (
582                self.augmenter.teacher.reverse_transform(label_filter)
583                if label_filter is not None else None
584            )
585
586            # Perform unsupervised training
587            with forward_context():
588                unsup_pred = self.model(model_input)
589                unsup_pred_inv = self.augmenter.student.reverse_transform(unsup_pred)
590                unsupervised_loss = self.unsupervised_loss(unsup_pred_inv, pseudo_labels_inv, label_filter_inv)
591
592            loss = (supervised_loss + unsupervised_loss) / 2
593            backprop(unsupervised_loss / 2 if self.separate_backward else loss)
594
595            if self.logger is not None:
596                with torch.no_grad(), forward_context():
597                    unsup_pred = unsup_pred if self._iteration % self.log_image_interval == 0 else None
598                    supervised_pred = supervised_pred if self._iteration % self.log_image_interval == 0 else None
599
600                self.logger.log_train_supervised(self._iteration, supervised_loss, xs, ys, supervised_pred)
601                self.logger.log_train_unsupervised(
602                    self._iteration, unsupervised_loss, xu, xu, unsup_pred_inv, pseudo_labels_inv, label_filter_inv
603                )
604                self.logger.log_train_augmentations(
605                    self._iteration, xu1, xu2, pseudo_labels, unsup_pred,
606                )
607
608                self.logger.log_combined_loss(self._iteration, loss)
609                lr = [pm["lr"] for pm in self.optimizer.param_groups][0]
610                self.logger.log_lr(self._iteration, lr)
611                if self.pseudo_labeler.confidence_threshold is not None:
612                    self.logger.log_ct(self._iteration, self.pseudo_labeler.confidence_threshold)
613
614            self._iteration += 1
615            n_iter += 1
616            if self._iteration >= self.max_iteration:
617                break
618            progress.update(1)
619
620        t_per_iter = (time.time() - t_per_iter) / n_iter
621        return t_per_iter
622
623    def _validate_supervised(self, forward_context):
624        metric_val = 0.0
625        loss_val = 0.0
626
627        for x, y in self.supervised_val_loader:
628            x, y = x.to(self.device, non_blocking=True), y.to(self.device, non_blocking=True)
629            with forward_context():
630                pred = self.model(x)
631                loss, metric = self.supervised_loss_and_metric(pred, y)
632            loss_val += loss.item()
633            metric_val += metric.item()
634
635        metric_val /= len(self.supervised_val_loader)
636        loss_val /= len(self.supervised_val_loader)
637
638        if self.logger is not None:
639            self.logger.log_validation_supervised(self._iteration, metric_val, loss_val, x, y, pred)
640
641        return metric_val
642
643    def _validate_unsupervised(self, forward_context):
644        metric_val = 0.0
645        loss_val = 0.0
646
647        for x in self.unsupervised_val_loader:
648            self.augmenter.reset_all()
649            x = x.to(self.device, non_blocking=True)
650
651            x1, x2 = self.augmenter.teacher.transform(x), self.augmenter.student.transform(x)
652            teacher_input, model_input = x1, x2
653
654            with forward_context():
655                pseudo_labels, label_filter = self.pseudo_labeler(self.model, teacher_input)
656                pseudo_labels_inv = self.augmenter.teacher.reverse_transform(pseudo_labels)
657                label_filter_inv = (
658                    self.augmenter.teacher.reverse_transform(label_filter)
659                    if label_filter is not None else None
660                )
661
662                pred = self.model(model_input)
663                pred_inv = self.augmenter.student.reverse_transform(pred)
664                loss, metric = self.unsupervised_loss_and_metric(pred_inv, pseudo_labels_inv, label_filter_inv)
665            loss_val += loss.item()
666            metric_val += metric.item()
667
668        metric_val /= len(self.unsupervised_val_loader)
669        loss_val /= len(self.unsupervised_val_loader)
670
671        if self.logger is not None:
672            self.logger.log_validation_unsupervised(
673                self._iteration, metric_val, loss_val, x, x, pred_inv, pseudo_labels_inv, label_filter_inv
674            )
675            self.logger.log_validation_augmentations(
676                self._iteration, x1, x2, pseudo_labels, pred,
677            )
678
679        self.pseudo_labeler.step(metric_val, self._epoch)
680
681        return metric_val
682
683    def _validate_impl(self, forward_context):
684        self.model.eval()
685
686        with torch.no_grad():
687
688            if self.supervised_val_loader is None:
689                supervised_metric = None
690            else:
691                supervised_metric = self._validate_supervised(forward_context)
692
693            if self.unsupervised_val_loader is None:
694                unsupervised_metric = None
695            else:
696                unsupervised_metric = self._validate_unsupervised(forward_context)
697
698        if unsupervised_metric is None:
699            metric = supervised_metric
700        elif supervised_metric is None:
701            metric = unsupervised_metric
702        else:
703            metric = (supervised_metric + unsupervised_metric) / 2
704
705        return metric
class FixMatchTrainer(torch_em.trainer.default_trainer.DefaultTrainer):
 14class FixMatchTrainer(torch_em.trainer.DefaultTrainer):
 15    """Trainer for semi-supervised learning and domain adaptation following the FixMatch approach.
 16
 17    FixMatch was introduced by Sohn et al. in https://arxiv.org/abs/2001.07685).
 18    It uses a teacher model derived from the student model via weight sharing to predict pseudo-labels
 19    on unlabeled data. We support two training strategies:
 20    - Joint training on labeled and unlabeled data (with a supervised and unsupervised loss function).
 21    - Taining only on the unsupervised data.
 22
 23    This class expects the following data loaders:
 24    - unsupervised_train_loader: Returns two augmentations (weak and strong) of the same input.
 25    - supervised_train_loader (optional): Returns input and labels.
 26    - unsupervised_val_loader (optional): Same as unsupervised_train_loader
 27    - supervised_val_loader (optional): Same as supervised_train_loader
 28    At least one of unsupervised_val_loader and supervised_val_loader must be given.
 29
 30    The following arguments can be used to customize the pseudo labeling:
 31    - pseudo_labeler: to compute the psuedo-labels
 32        - Parameters: model, teacher_input
 33        - Returns: pseudo_labels, label_filter (<- label filter can for example be mask, weight or None)
 34    - unsupervised_loss: the loss between model predictions and pseudo labels
 35        - Parameters: model, model_input, pseudo_labels, label_filter
 36        - Returns: loss
 37    - supervised_loss (optional): the supervised loss function
 38        - Parameters: model, input, labels
 39        - Returns: loss
 40    - unsupervised_loss_and_metric (optional): the unsupervised loss function and metric
 41        - Parameters: model, model_input, pseudo_labels, label_filter
 42        - Returns: loss, metric
 43    - supervised_loss_and_metric (optional): the supervised loss function and metric
 44        - Parameters: model, input, labels
 45        - Returns: loss, metric
 46    At least one of unsupervised_loss_and_metric and supervised_loss_and_metric must be given.
 47
 48    Note: adjust the batch size ratio between the 'unsupervised_train_loader' and 'supervised_train_loader'
 49    for setting the ratio between supervised and unsupervised training samples.
 50
 51    Args:
 52        model: The model to be trained.
 53        unsupervised_train_loader: The loader for unsupervised training.
 54        unsupervised_loss: The loss for unsupervised training.
 55        pseudo_labeler: The pseudo labeler that predicts labels in unsupervised training.
 56        supervised_train_loader: The loader for supervised training.
 57        supervised_loss: The loss for supervised training.
 58        unsupervised_loss_and_metric: The loss and metric for unsupervised training.
 59        supervised_loss_and_metric: The loss and metrhic for supervised training.
 60        logger: The logger.
 61        source_distribution: The ratio of labels in the source label distribution.
 62            If given, the predicted distribution of the trained model will be regularized to
 63            match this source label distribution.
 64        separate_backward: Whether to backpropagate each loss term separately to reduce peak memory.
 65            The default uses one backward pass on the combined loss.
 66        kwargs: Additional keyword arguments for `torch_em.trainer.DefaultTrainer`.
 67    """
 68
 69    def __init__(
 70        self,
 71        model: torch.nn.Module,
 72        unsupervised_train_loader: torch.utils.data.DataLoader,
 73        unsupervised_loss: torch.utils.data.DataLoader,
 74        pseudo_labeler: Callable,
 75        supervised_train_loader: Optional[torch.utils.data.DataLoader] = None,
 76        unsupervised_val_loader: Optional[torch.utils.data.DataLoader] = None,
 77        supervised_val_loader: Optional[torch.utils.data.DataLoader] = None,
 78        supervised_loss: Optional[Callable] = None,
 79        unsupervised_loss_and_metric: Optional[Callable] = None,
 80        supervised_loss_and_metric: Optional[Callable] = None,
 81        logger=SelfTrainingTensorboardLogger,
 82        source_distribution: List[float] = None,
 83        separate_backward: bool = False,
 84        **kwargs,
 85    ):
 86        self.separate_backward = separate_backward
 87        # Do we have supervised data or not?
 88        if supervised_train_loader is None:
 89            # No. -> We use the unsupervised training logic.
 90            train_loader = unsupervised_train_loader
 91            self._train_epoch_impl = self._train_epoch_unsupervised
 92        else:
 93            # Yes. -> We use the semi-supervised training logic.
 94            assert supervised_loss is not None
 95            train_loader = supervised_train_loader if len(supervised_train_loader) < len(unsupervised_train_loader)\
 96                else unsupervised_train_loader
 97            self._train_epoch_impl = self._train_epoch_semisupervised
 98
 99        self.unsupervised_train_loader = unsupervised_train_loader
100        self.supervised_train_loader = supervised_train_loader
101
102        # Check that we have at least one of supvervised / unsupervised val loader.
103        assert sum((
104            supervised_val_loader is not None,
105            unsupervised_val_loader is not None,
106        )) > 0
107        self.supervised_val_loader = supervised_val_loader
108        self.unsupervised_val_loader = unsupervised_val_loader
109
110        if self.unsupervised_val_loader is None:
111            val_loader = self.supervised_val_loader
112        else:
113            val_loader = self.unsupervised_val_loader
114
115        # Check that we have at least one of supvervised / unsupervised loss and metric.
116        assert sum((
117            supervised_loss_and_metric is not None,
118            unsupervised_loss_and_metric is not None,
119        )) > 0
120        self.supervised_loss_and_metric = supervised_loss_and_metric
121        self.unsupervised_loss_and_metric = unsupervised_loss_and_metric
122
123        # train_loader, val_loader, loss and metric may be unnecessarily deserialized
124        kwargs.pop("train_loader", None)
125        kwargs.pop("val_loader", None)
126        kwargs.pop("metric", None)
127        kwargs.pop("loss", None)
128        super().__init__(
129            model=model, train_loader=train_loader, val_loader=val_loader,
130            loss=Dummy(), metric=Dummy(), logger=logger, **kwargs
131        )
132
133        self.unsupervised_loss = unsupervised_loss
134        self.supervised_loss = supervised_loss
135
136        self.pseudo_labeler = pseudo_labeler
137
138        if source_distribution is None:
139            self.source_distribution = None
140        else:
141            self.source_distribution = torch.FloatTensor(source_distribution).to(self.device)
142
143        self._kwargs = kwargs
144
145    #
146    # functionality for saving checkpoints and initialization
147    #
148
149    def save_checkpoint(self, name, current_metric, best_metric, **extra_save_dict):
150        """@private
151        """
152        train_loader_kwargs = get_constructor_arguments(self.train_loader)
153        val_loader_kwargs = get_constructor_arguments(self.val_loader)
154        extra_state = {
155            "init": {
156                "train_loader_kwargs": train_loader_kwargs,
157                "train_dataset": self.train_loader.dataset,
158                "val_loader_kwargs": val_loader_kwargs,
159                "val_dataset": self.val_loader.dataset,
160                "loss_class": "torch_em.self_training.mean_teacher.Dummy",
161                "loss_kwargs": {},
162                "metric_class": "torch_em.self_training.mean_teacher.Dummy",
163                "metric_kwargs": {},
164            },
165        }
166        extra_state.update(**extra_save_dict)
167        super().save_checkpoint(name, current_metric, best_metric, **extra_state)
168
169    # Distribution alignment:
170    # Encourages the distribution of the model's generated pseudo labels to match the marginal
171    # distribution of pseudo labels from the source transfer (key idea: to maximize the mutual information).
172    def get_distribution_alignment(self, pseudo_labels, label_threshold=0.5):
173        """@private
174        """
175        if self.source_distribution is not None:
176            pseudo_labels_binary = torch.where(pseudo_labels >= label_threshold, 1, 0)
177            _, target_distribution = torch.unique(pseudo_labels_binary, return_counts=True)
178            target_distribution = target_distribution / target_distribution.sum()
179            distribution_ratio = self.source_distribution / target_distribution
180            pseudo_labels = torch.where(
181                pseudo_labels < label_threshold,
182                pseudo_labels * distribution_ratio[0],
183                pseudo_labels * distribution_ratio[1]
184            ).clip(0, 1)
185
186        return pseudo_labels
187
188    #
189    # training and validation functionality
190    #
191
192    def _train_epoch_unsupervised(self, progress, forward_context, backprop):
193        self.model.train()
194
195        n_iter = 0
196        t_per_iter = time.time()
197
198        # Sample from both the supervised and unsupervised loader.
199        for xu1, xu2 in self.unsupervised_train_loader:
200            xu1, xu2 = xu1.to(self.device, non_blocking=True), xu2.to(self.device, non_blocking=True)
201
202            teacher_input, model_input = xu1, xu2
203
204            with forward_context(), torch.no_grad():
205                # Compute the pseudo labels.
206                pseudo_labels, label_filter = self.pseudo_labeler(self.model, teacher_input)
207
208            pseudo_labels = pseudo_labels.detach()
209            if label_filter is not None:
210                label_filter = label_filter.detach()
211
212            # Perform distribution alignment for pseudo labels
213            pseudo_labels = self.get_distribution_alignment(pseudo_labels)
214
215            self.optimizer.zero_grad()
216            # Perform unsupervised training
217            with forward_context():
218                loss = self.unsupervised_loss(self.model, model_input, pseudo_labels, label_filter)
219
220            backprop(loss)
221
222            if self.logger is not None:
223                with torch.no_grad(), forward_context():
224                    pred = self.model(model_input) if self._iteration % self.log_image_interval == 0 else None
225                self.logger.log_train_unsupervised(
226                    self._iteration, loss, xu1, xu2, pred, pseudo_labels, label_filter
227                )
228                lr = [pm["lr"] for pm in self.optimizer.param_groups][0]
229                self.logger.log_lr(self._iteration, lr)
230                if self.pseudo_labeler.confidence_threshold is not None:
231                    self.logger.log_ct(self._iteration, self.pseudo_labeler.confidence_threshold)
232
233            self._iteration += 1
234            n_iter += 1
235            if self._iteration >= self.max_iteration:
236                break
237            progress.update(1)
238
239        t_per_iter = (time.time() - t_per_iter) / n_iter
240        return t_per_iter
241
242    def _train_epoch_semisupervised(self, progress, forward_context, backprop):
243        self.model.train()
244
245        n_iter = 0
246        t_per_iter = time.time()
247
248        # Sample from both the supervised and unsupervised loader.
249        for (xs, ys), (xu1, xu2) in zip(self.supervised_train_loader, self.unsupervised_train_loader):
250            xs, ys = xs.to(self.device, non_blocking=True), ys.to(self.device, non_blocking=True)
251            xu1, xu2 = xu1.to(self.device, non_blocking=True), xu2.to(self.device, non_blocking=True)
252
253            # Perform supervised training.
254            self.optimizer.zero_grad()
255            with forward_context():
256                # We pass the model, the input and the labels to the supervised loss function,
257                # so that how the loss is calculated stays flexible, e.g. to enable ELBO for PUNet.
258                supervised_loss = self.supervised_loss(self.model, xs, ys)
259
260            if self.separate_backward:
261                # Free supervised activations before the unsupervised forward pass.
262                self._accumulate_gradients(supervised_loss / 2)
263
264            teacher_input, model_input = xu1, xu2
265
266            with forward_context(), torch.no_grad():
267                # Compute the pseudo labels.
268                pseudo_labels, label_filter = self.pseudo_labeler(self.model, teacher_input)
269
270            pseudo_labels = pseudo_labels.detach()
271            if label_filter is not None:
272                label_filter = label_filter.detach()
273
274            # Perform distribution alignment for pseudo labels
275            pseudo_labels = self.get_distribution_alignment(pseudo_labels)
276
277            # Perform unsupervised training
278            with forward_context():
279                unsupervised_loss = self.unsupervised_loss(self.model, model_input, pseudo_labels, label_filter)
280
281            loss = (supervised_loss + unsupervised_loss) / 2
282            backprop(unsupervised_loss / 2 if self.separate_backward else loss)
283
284            if self.logger is not None:
285                with torch.no_grad(), forward_context():
286                    unsup_pred = self.model(model_input) if self._iteration % self.log_image_interval == 0 else None
287                    supervised_pred = self.model(xs) if self._iteration % self.log_image_interval == 0 else None
288
289                self.logger.log_train_supervised(self._iteration, supervised_loss, xs, ys, supervised_pred)
290                self.logger.log_train_unsupervised(
291                    self._iteration, unsupervised_loss, xu1, xu2, unsup_pred, pseudo_labels, label_filter
292                )
293
294                self.logger.log_combined_loss(self._iteration, loss)
295                lr = [pm["lr"] for pm in self.optimizer.param_groups][0]
296                self.logger.log_lr(self._iteration, lr)
297                if self.pseudo_labeler.confidence_threshold is not None:
298                    self.logger.log_ct(self._iteration, self.pseudo_labeler.confidence_threshold)
299
300            self._iteration += 1
301            n_iter += 1
302            if self._iteration >= self.max_iteration:
303                break
304            progress.update(1)
305
306        t_per_iter = (time.time() - t_per_iter) / n_iter
307        return t_per_iter
308
309    def _validate_supervised(self, forward_context):
310        metric_val = 0.0
311        loss_val = 0.0
312
313        for x, y in self.supervised_val_loader:
314            x, y = x.to(self.device, non_blocking=True), y.to(self.device, non_blocking=True)
315            with forward_context():
316                loss, metric = self.supervised_loss_and_metric(self.model, x, y)
317            loss_val += loss.item()
318            metric_val += metric.item()
319
320        metric_val /= len(self.supervised_val_loader)
321        loss_val /= len(self.supervised_val_loader)
322
323        if self.logger is not None:
324            with forward_context():
325                pred = self.model(x)
326            self.logger.log_validation_supervised(self._iteration, metric_val, loss_val, x, y, pred)
327
328        return metric_val
329
330    def _validate_unsupervised(self, forward_context):
331        metric_val = 0.0
332        loss_val = 0.0
333
334        for x1, x2 in self.unsupervised_val_loader:
335            x1, x2 = x1.to(self.device, non_blocking=True), x2.to(self.device, non_blocking=True)
336            teacher_input, model_input = x1, x2
337            with forward_context():
338                pseudo_labels, label_filter = self.pseudo_labeler(self.model, teacher_input)
339                loss, metric = self.unsupervised_loss_and_metric(self.model, model_input, pseudo_labels, label_filter)
340            loss_val += loss.item()
341            metric_val += metric.item()
342
343        metric_val /= len(self.unsupervised_val_loader)
344        loss_val /= len(self.unsupervised_val_loader)
345
346        if self.logger is not None:
347            with forward_context():
348                pred = self.model(model_input)
349            self.logger.log_validation_unsupervised(
350                self._iteration, metric_val, loss_val, x1, x2, pred, pseudo_labels, label_filter
351            )
352
353        return metric_val
354
355    def _validate_impl(self, forward_context):
356        self.model.eval()
357
358        with torch.no_grad():
359
360            if self.supervised_val_loader is None:
361                supervised_metric = None
362            else:
363                supervised_metric = self._validate_supervised(forward_context)
364
365            if self.unsupervised_val_loader is None:
366                unsupervised_metric = None
367            else:
368                unsupervised_metric = self._validate_unsupervised(forward_context)
369
370        if unsupervised_metric is None:
371            metric = supervised_metric
372        elif supervised_metric is None:
373            metric = unsupervised_metric
374        else:
375            metric = (supervised_metric + unsupervised_metric) / 2
376
377        return metric

Trainer for semi-supervised learning and domain adaptation following the FixMatch approach.

FixMatch was introduced by Sohn et al. in https://arxiv.org/abs/2001.07685). It uses a teacher model derived from the student model via weight sharing to predict pseudo-labels on unlabeled data. We support two training strategies:

  • Joint training on labeled and unlabeled data (with a supervised and unsupervised loss function).
  • Taining only on the unsupervised data.

This class expects the following data loaders:

  • unsupervised_train_loader: Returns two augmentations (weak and strong) of the same input.
  • supervised_train_loader (optional): Returns input and labels.
  • unsupervised_val_loader (optional): Same as unsupervised_train_loader
  • supervised_val_loader (optional): Same as supervised_train_loader At least one of unsupervised_val_loader and supervised_val_loader must be given.

The following arguments can be used to customize the pseudo labeling:

  • pseudo_labeler: to compute the psuedo-labels
    • Parameters: model, teacher_input
    • Returns: pseudo_labels, label_filter (<- label filter can for example be mask, weight or None)
  • unsupervised_loss: the loss between model predictions and pseudo labels
    • Parameters: model, model_input, pseudo_labels, label_filter
    • Returns: loss
  • supervised_loss (optional): the supervised loss function
    • Parameters: model, input, labels
    • Returns: loss
  • unsupervised_loss_and_metric (optional): the unsupervised loss function and metric
    • Parameters: model, model_input, pseudo_labels, label_filter
    • Returns: loss, metric
  • supervised_loss_and_metric (optional): the supervised loss function and metric
    • Parameters: model, input, labels
    • Returns: loss, metric At least one of unsupervised_loss_and_metric and supervised_loss_and_metric must be given.

Note: adjust the batch size ratio between the 'unsupervised_train_loader' and 'supervised_train_loader' for setting the ratio between supervised and unsupervised training samples.

Arguments:
  • model: The model to be trained.
  • unsupervised_train_loader: The loader for unsupervised training.
  • unsupervised_loss: The loss for unsupervised training.
  • pseudo_labeler: The pseudo labeler that predicts labels in unsupervised training.
  • supervised_train_loader: The loader for supervised training.
  • supervised_loss: The loss for supervised training.
  • unsupervised_loss_and_metric: The loss and metric for unsupervised training.
  • supervised_loss_and_metric: The loss and metrhic for supervised training.
  • logger: The logger.
  • source_distribution: The ratio of labels in the source label distribution. If given, the predicted distribution of the trained model will be regularized to match this source label distribution.
  • separate_backward: Whether to backpropagate each loss term separately to reduce peak memory. The default uses one backward pass on the combined loss.
  • kwargs: Additional keyword arguments for torch_em.trainer.DefaultTrainer.
FixMatchTrainer( model: torch.nn.modules.module.Module, unsupervised_train_loader: torch.utils.data.dataloader.DataLoader, unsupervised_loss: torch.utils.data.dataloader.DataLoader, pseudo_labeler: Callable, supervised_train_loader: Optional[torch.utils.data.dataloader.DataLoader] = None, unsupervised_val_loader: Optional[torch.utils.data.dataloader.DataLoader] = None, supervised_val_loader: Optional[torch.utils.data.dataloader.DataLoader] = None, supervised_loss: Optional[Callable] = None, unsupervised_loss_and_metric: Optional[Callable] = None, supervised_loss_and_metric: Optional[Callable] = None, logger=<class 'torch_em.self_training.logger.SelfTrainingTensorboardLogger'>, source_distribution: List[float] = None, separate_backward: bool = False, **kwargs)
 69    def __init__(
 70        self,
 71        model: torch.nn.Module,
 72        unsupervised_train_loader: torch.utils.data.DataLoader,
 73        unsupervised_loss: torch.utils.data.DataLoader,
 74        pseudo_labeler: Callable,
 75        supervised_train_loader: Optional[torch.utils.data.DataLoader] = None,
 76        unsupervised_val_loader: Optional[torch.utils.data.DataLoader] = None,
 77        supervised_val_loader: Optional[torch.utils.data.DataLoader] = None,
 78        supervised_loss: Optional[Callable] = None,
 79        unsupervised_loss_and_metric: Optional[Callable] = None,
 80        supervised_loss_and_metric: Optional[Callable] = None,
 81        logger=SelfTrainingTensorboardLogger,
 82        source_distribution: List[float] = None,
 83        separate_backward: bool = False,
 84        **kwargs,
 85    ):
 86        self.separate_backward = separate_backward
 87        # Do we have supervised data or not?
 88        if supervised_train_loader is None:
 89            # No. -> We use the unsupervised training logic.
 90            train_loader = unsupervised_train_loader
 91            self._train_epoch_impl = self._train_epoch_unsupervised
 92        else:
 93            # Yes. -> We use the semi-supervised training logic.
 94            assert supervised_loss is not None
 95            train_loader = supervised_train_loader if len(supervised_train_loader) < len(unsupervised_train_loader)\
 96                else unsupervised_train_loader
 97            self._train_epoch_impl = self._train_epoch_semisupervised
 98
 99        self.unsupervised_train_loader = unsupervised_train_loader
100        self.supervised_train_loader = supervised_train_loader
101
102        # Check that we have at least one of supvervised / unsupervised val loader.
103        assert sum((
104            supervised_val_loader is not None,
105            unsupervised_val_loader is not None,
106        )) > 0
107        self.supervised_val_loader = supervised_val_loader
108        self.unsupervised_val_loader = unsupervised_val_loader
109
110        if self.unsupervised_val_loader is None:
111            val_loader = self.supervised_val_loader
112        else:
113            val_loader = self.unsupervised_val_loader
114
115        # Check that we have at least one of supvervised / unsupervised loss and metric.
116        assert sum((
117            supervised_loss_and_metric is not None,
118            unsupervised_loss_and_metric is not None,
119        )) > 0
120        self.supervised_loss_and_metric = supervised_loss_and_metric
121        self.unsupervised_loss_and_metric = unsupervised_loss_and_metric
122
123        # train_loader, val_loader, loss and metric may be unnecessarily deserialized
124        kwargs.pop("train_loader", None)
125        kwargs.pop("val_loader", None)
126        kwargs.pop("metric", None)
127        kwargs.pop("loss", None)
128        super().__init__(
129            model=model, train_loader=train_loader, val_loader=val_loader,
130            loss=Dummy(), metric=Dummy(), logger=logger, **kwargs
131        )
132
133        self.unsupervised_loss = unsupervised_loss
134        self.supervised_loss = supervised_loss
135
136        self.pseudo_labeler = pseudo_labeler
137
138        if source_distribution is None:
139            self.source_distribution = None
140        else:
141            self.source_distribution = torch.FloatTensor(source_distribution).to(self.device)
142
143        self._kwargs = kwargs
separate_backward
unsupervised_train_loader
supervised_train_loader
supervised_val_loader
unsupervised_val_loader
supervised_loss_and_metric
unsupervised_loss_and_metric
unsupervised_loss
supervised_loss
pseudo_labeler
class FixMatchTrainerWithInvertibleAugmentations(FixMatchTrainer):
380class FixMatchTrainerWithInvertibleAugmentations(FixMatchTrainer):
381    """Trainer for semi-supervised learning and domain adaptation following the FixMatch approach.
382
383    FixMatch was introduced by Sohn et al. in https://arxiv.org/abs/2001.07685).
384    It uses a teacher model derived from the student model via weight sharing to predict pseudo-labels
385    on unlabeled data. We support two training strategies:
386    - Joint training on labeled and unlabeled data (with a supervised and unsupervised loss function).
387    - Taining only on the unsupervised data.
388
389    This class expects the following data loaders:
390    - unsupervised_train_loader: Returns two augmentations (weak and strong) of the same input.
391    - supervised_train_loader (optional): Returns input and labels.
392    - unsupervised_val_loader (optional): Same as unsupervised_train_loader
393    - supervised_val_loader (optional): Same as supervised_train_loader
394    At least one of unsupervised_val_loader and supervised_val_loader must be given.
395
396    The augmenter defines separate invertible transforms for teacher and student inputs.
397    Teacher and student views are generated independently, and the corresponding inverse
398    transforms map predictions and pseudo-labels back into a shared reference frame.
399
400    The following arguments can be used to customize the pseudo labeling:
401    - pseudo_labeler: to compute the psuedo-labels
402        - Parameters: model, teacher_input
403        - Returns: pseudo_labels, label_filter (<- label filter can for example be mask, weight or None)
404    - unsupervised_loss: the loss between model predictions and pseudo labels
405        - Parameters: model, model_input, pseudo_labels, label_filter
406        - Returns: loss
407    - supervised_loss (optional): the supervised loss function
408        - Parameters: model, input, labels
409        - Returns: loss
410    - unsupervised_loss_and_metric (optional): the unsupervised loss function and metric
411        - Parameters: model, model_input, pseudo_labels, label_filter
412        - Returns: loss, metric
413    - supervised_loss_and_metric (optional): the supervised loss function and metric
414        - Parameters: model, input, labels
415        - Returns: loss, metric
416    At least one of unsupervised_loss_and_metric and supervised_loss_and_metric must be given.
417
418    Note: adjust the batch size ratio between the 'unsupervised_train_loader' and 'supervised_train_loader'
419    for setting the ratio between supervised and unsupervised training samples.
420
421    Args:
422        model: The model to be trained.
423        unsupervised_train_loader: The loader for unsupervised training.
424        unsupervised_loss: The loss for unsupervised training.
425        pseudo_labeler: The pseudo labeler that predicts labels in unsupervised training.
426        augmenter: Invertible augmenter providing separate teacher and student transforms,
427            including corresponding inverse transforms to align predictions in a common frame.
428        supervised_train_loader: The loader for supervised training.
429        supervised_loss: The loss for supervised training.
430        unsupervised_loss_and_metric: The loss and metric for unsupervised training.
431        supervised_loss_and_metric: The loss and metrhic for supervised training.
432        logger: The logger.
433        source_distribution: The ratio of labels in the source label distribution.
434            If given, the predicted distribution of the trained model will be regularized to
435            match this source label distribution.
436        separate_backward: Whether to backpropagate each loss term separately to reduce peak memory.
437            The default uses one backward pass on the combined loss.
438        kwargs: Additional keyword arguments for `torch_em.trainer.DefaultTrainer`.
439    """
440
441    def __init__(
442        self,
443        model: torch.nn.Module,
444        unsupervised_train_loader: torch.utils.data.DataLoader,
445        unsupervised_loss: torch.utils.data.DataLoader,
446        pseudo_labeler: Callable,
447        augmenter: InvertibleAugmenter,
448        supervised_train_loader: Optional[torch.utils.data.DataLoader] = None,
449        unsupervised_val_loader: Optional[torch.utils.data.DataLoader] = None,
450        supervised_val_loader: Optional[torch.utils.data.DataLoader] = None,
451        supervised_loss: Optional[Callable] = None,
452        unsupervised_loss_and_metric: Optional[Callable] = None,
453        supervised_loss_and_metric: Optional[Callable] = None,
454        logger=SelfTrainingTensorboardLogger,
455        source_distribution: List[float] = None,
456        separate_backward: bool = False,
457        **kwargs,
458    ):
459        super().__init__(
460            model=model,
461            unsupervised_train_loader=unsupervised_train_loader,
462            unsupervised_loss=unsupervised_loss,
463            pseudo_labeler=pseudo_labeler,
464            supervised_train_loader=supervised_train_loader,
465            unsupervised_val_loader=unsupervised_val_loader,
466            supervised_val_loader=supervised_val_loader,
467            supervised_loss=supervised_loss,
468            unsupervised_loss_and_metric=unsupervised_loss_and_metric,
469            supervised_loss_and_metric=supervised_loss_and_metric,
470            logger=logger,
471            source_distribution=source_distribution,
472            separate_backward=separate_backward,
473            **kwargs,
474        )
475
476        self.augmenter = augmenter
477
478    #
479    # training and validation functionality
480    #
481
482    def _train_epoch_unsupervised(self, progress, forward_context, backprop):
483        self.model.train()
484
485        n_iter = 0
486        t_per_iter = time.time()
487
488        # Sample from both the supervised and unsupervised loader.
489        for xu in self.unsupervised_train_loader:
490            self.augmenter.reset_all()
491            xu = xu.to(self.device, non_blocking=True)
492
493            xu1, xu2 = self.augmenter.teacher.transform(xu), self.augmenter.student.transform(xu)
494            teacher_input, model_input = xu1, xu2
495
496            with forward_context(), torch.no_grad():
497                # Compute the pseudo labels.
498                pseudo_labels, label_filter = self.pseudo_labeler(self.model, teacher_input)
499
500            pseudo_labels = pseudo_labels.detach()
501            if label_filter is not None:
502                label_filter = label_filter.detach()
503
504            # Perform distribution alignment for pseudo labels, then invert into the reference frame.
505            pseudo_labels = self.get_distribution_alignment(pseudo_labels)
506            pseudo_labels_inv = self.augmenter.teacher.reverse_transform(pseudo_labels)
507            label_filter_inv = (
508                self.augmenter.teacher.reverse_transform(label_filter)
509                if label_filter is not None else None
510            )
511
512            self.optimizer.zero_grad()
513            # Perform unsupervised training
514            with forward_context():
515                pred = self.model(model_input)
516                pred_inv = self.augmenter.student.reverse_transform(pred)
517                loss = self.unsupervised_loss(pred_inv, pseudo_labels_inv, label_filter_inv)
518
519            backprop(loss)
520
521            if self.logger is not None:
522                with torch.no_grad(), forward_context():
523                    pred = pred if self._iteration % self.log_image_interval == 0 else None
524                self.logger.log_train_unsupervised(
525                    self._iteration, loss, xu, xu, pred_inv, pseudo_labels_inv, label_filter_inv
526                )
527                self.logger.log_train_augmentations(
528                    self._iteration, xu1, xu2, pseudo_labels, pred,
529                )
530                lr = [pm["lr"] for pm in self.optimizer.param_groups][0]
531                self.logger.log_lr(self._iteration, lr)
532                if self.pseudo_labeler.confidence_threshold is not None:
533                    self.logger.log_ct(self._iteration, self.pseudo_labeler.confidence_threshold)
534
535            self._iteration += 1
536            n_iter += 1
537            if self._iteration >= self.max_iteration:
538                break
539            progress.update(1)
540
541        t_per_iter = (time.time() - t_per_iter) / n_iter
542        return t_per_iter
543
544    def _train_epoch_semisupervised(self, progress, forward_context, backprop):
545        self.model.train()
546
547        n_iter = 0
548        t_per_iter = time.time()
549
550        # Sample from both the supervised and unsupervised loader.
551        for (xs, ys), xu in zip(self.supervised_train_loader, self.unsupervised_train_loader):
552            self.augmenter.reset_all()
553            xs, ys = xs.to(self.device, non_blocking=True), ys.to(self.device, non_blocking=True)
554            xu = xu.to(self.device, non_blocking=True)
555
556            xu1, xu2 = self.augmenter.teacher.transform(xu), self.augmenter.student.transform(xu)
557            teacher_input, model_input = xu1, xu2
558
559            # Perform supervised training.
560            self.optimizer.zero_grad()
561            with forward_context():
562                # We pass the model, the input and the labels to the supervised loss function,
563                # so that how the loss is calculated stays flexible, e.g. to enable ELBO for PUNet.
564                supervised_pred = self.model(xs)
565                supervised_loss = self.supervised_loss(supervised_pred, ys)
566
567            if self.separate_backward:
568                # Free supervised activations before the unsupervised forward pass.
569                self._accumulate_gradients(supervised_loss / 2)
570
571            with forward_context(), torch.no_grad():
572                # Compute the pseudo labels.
573                pseudo_labels, label_filter = self.pseudo_labeler(self.model, teacher_input)
574
575            pseudo_labels = pseudo_labels.detach()
576            if label_filter is not None:
577                label_filter = label_filter.detach()
578
579            # Perform distribution alignment for pseudo labels, then invert into the reference frame.
580            pseudo_labels = self.get_distribution_alignment(pseudo_labels)
581            pseudo_labels_inv = self.augmenter.teacher.reverse_transform(pseudo_labels)
582            label_filter_inv = (
583                self.augmenter.teacher.reverse_transform(label_filter)
584                if label_filter is not None else None
585            )
586
587            # Perform unsupervised training
588            with forward_context():
589                unsup_pred = self.model(model_input)
590                unsup_pred_inv = self.augmenter.student.reverse_transform(unsup_pred)
591                unsupervised_loss = self.unsupervised_loss(unsup_pred_inv, pseudo_labels_inv, label_filter_inv)
592
593            loss = (supervised_loss + unsupervised_loss) / 2
594            backprop(unsupervised_loss / 2 if self.separate_backward else loss)
595
596            if self.logger is not None:
597                with torch.no_grad(), forward_context():
598                    unsup_pred = unsup_pred if self._iteration % self.log_image_interval == 0 else None
599                    supervised_pred = supervised_pred if self._iteration % self.log_image_interval == 0 else None
600
601                self.logger.log_train_supervised(self._iteration, supervised_loss, xs, ys, supervised_pred)
602                self.logger.log_train_unsupervised(
603                    self._iteration, unsupervised_loss, xu, xu, unsup_pred_inv, pseudo_labels_inv, label_filter_inv
604                )
605                self.logger.log_train_augmentations(
606                    self._iteration, xu1, xu2, pseudo_labels, unsup_pred,
607                )
608
609                self.logger.log_combined_loss(self._iteration, loss)
610                lr = [pm["lr"] for pm in self.optimizer.param_groups][0]
611                self.logger.log_lr(self._iteration, lr)
612                if self.pseudo_labeler.confidence_threshold is not None:
613                    self.logger.log_ct(self._iteration, self.pseudo_labeler.confidence_threshold)
614
615            self._iteration += 1
616            n_iter += 1
617            if self._iteration >= self.max_iteration:
618                break
619            progress.update(1)
620
621        t_per_iter = (time.time() - t_per_iter) / n_iter
622        return t_per_iter
623
624    def _validate_supervised(self, forward_context):
625        metric_val = 0.0
626        loss_val = 0.0
627
628        for x, y in self.supervised_val_loader:
629            x, y = x.to(self.device, non_blocking=True), y.to(self.device, non_blocking=True)
630            with forward_context():
631                pred = self.model(x)
632                loss, metric = self.supervised_loss_and_metric(pred, y)
633            loss_val += loss.item()
634            metric_val += metric.item()
635
636        metric_val /= len(self.supervised_val_loader)
637        loss_val /= len(self.supervised_val_loader)
638
639        if self.logger is not None:
640            self.logger.log_validation_supervised(self._iteration, metric_val, loss_val, x, y, pred)
641
642        return metric_val
643
644    def _validate_unsupervised(self, forward_context):
645        metric_val = 0.0
646        loss_val = 0.0
647
648        for x in self.unsupervised_val_loader:
649            self.augmenter.reset_all()
650            x = x.to(self.device, non_blocking=True)
651
652            x1, x2 = self.augmenter.teacher.transform(x), self.augmenter.student.transform(x)
653            teacher_input, model_input = x1, x2
654
655            with forward_context():
656                pseudo_labels, label_filter = self.pseudo_labeler(self.model, teacher_input)
657                pseudo_labels_inv = self.augmenter.teacher.reverse_transform(pseudo_labels)
658                label_filter_inv = (
659                    self.augmenter.teacher.reverse_transform(label_filter)
660                    if label_filter is not None else None
661                )
662
663                pred = self.model(model_input)
664                pred_inv = self.augmenter.student.reverse_transform(pred)
665                loss, metric = self.unsupervised_loss_and_metric(pred_inv, pseudo_labels_inv, label_filter_inv)
666            loss_val += loss.item()
667            metric_val += metric.item()
668
669        metric_val /= len(self.unsupervised_val_loader)
670        loss_val /= len(self.unsupervised_val_loader)
671
672        if self.logger is not None:
673            self.logger.log_validation_unsupervised(
674                self._iteration, metric_val, loss_val, x, x, pred_inv, pseudo_labels_inv, label_filter_inv
675            )
676            self.logger.log_validation_augmentations(
677                self._iteration, x1, x2, pseudo_labels, pred,
678            )
679
680        self.pseudo_labeler.step(metric_val, self._epoch)
681
682        return metric_val
683
684    def _validate_impl(self, forward_context):
685        self.model.eval()
686
687        with torch.no_grad():
688
689            if self.supervised_val_loader is None:
690                supervised_metric = None
691            else:
692                supervised_metric = self._validate_supervised(forward_context)
693
694            if self.unsupervised_val_loader is None:
695                unsupervised_metric = None
696            else:
697                unsupervised_metric = self._validate_unsupervised(forward_context)
698
699        if unsupervised_metric is None:
700            metric = supervised_metric
701        elif supervised_metric is None:
702            metric = unsupervised_metric
703        else:
704            metric = (supervised_metric + unsupervised_metric) / 2
705
706        return metric

Trainer for semi-supervised learning and domain adaptation following the FixMatch approach.

FixMatch was introduced by Sohn et al. in https://arxiv.org/abs/2001.07685). It uses a teacher model derived from the student model via weight sharing to predict pseudo-labels on unlabeled data. We support two training strategies:

  • Joint training on labeled and unlabeled data (with a supervised and unsupervised loss function).
  • Taining only on the unsupervised data.

This class expects the following data loaders:

  • unsupervised_train_loader: Returns two augmentations (weak and strong) of the same input.
  • supervised_train_loader (optional): Returns input and labels.
  • unsupervised_val_loader (optional): Same as unsupervised_train_loader
  • supervised_val_loader (optional): Same as supervised_train_loader At least one of unsupervised_val_loader and supervised_val_loader must be given.

The augmenter defines separate invertible transforms for teacher and student inputs. Teacher and student views are generated independently, and the corresponding inverse transforms map predictions and pseudo-labels back into a shared reference frame.

The following arguments can be used to customize the pseudo labeling:

  • pseudo_labeler: to compute the psuedo-labels
    • Parameters: model, teacher_input
    • Returns: pseudo_labels, label_filter (<- label filter can for example be mask, weight or None)
  • unsupervised_loss: the loss between model predictions and pseudo labels
    • Parameters: model, model_input, pseudo_labels, label_filter
    • Returns: loss
  • supervised_loss (optional): the supervised loss function
    • Parameters: model, input, labels
    • Returns: loss
  • unsupervised_loss_and_metric (optional): the unsupervised loss function and metric
    • Parameters: model, model_input, pseudo_labels, label_filter
    • Returns: loss, metric
  • supervised_loss_and_metric (optional): the supervised loss function and metric
    • Parameters: model, input, labels
    • Returns: loss, metric At least one of unsupervised_loss_and_metric and supervised_loss_and_metric must be given.

Note: adjust the batch size ratio between the 'unsupervised_train_loader' and 'supervised_train_loader' for setting the ratio between supervised and unsupervised training samples.

Arguments:
  • model: The model to be trained.
  • unsupervised_train_loader: The loader for unsupervised training.
  • unsupervised_loss: The loss for unsupervised training.
  • pseudo_labeler: The pseudo labeler that predicts labels in unsupervised training.
  • augmenter: Invertible augmenter providing separate teacher and student transforms, including corresponding inverse transforms to align predictions in a common frame.
  • supervised_train_loader: The loader for supervised training.
  • supervised_loss: The loss for supervised training.
  • unsupervised_loss_and_metric: The loss and metric for unsupervised training.
  • supervised_loss_and_metric: The loss and metrhic for supervised training.
  • logger: The logger.
  • source_distribution: The ratio of labels in the source label distribution. If given, the predicted distribution of the trained model will be regularized to match this source label distribution.
  • separate_backward: Whether to backpropagate each loss term separately to reduce peak memory. The default uses one backward pass on the combined loss.
  • kwargs: Additional keyword arguments for torch_em.trainer.DefaultTrainer.
FixMatchTrainerWithInvertibleAugmentations( model: torch.nn.modules.module.Module, unsupervised_train_loader: torch.utils.data.dataloader.DataLoader, unsupervised_loss: torch.utils.data.dataloader.DataLoader, pseudo_labeler: Callable, augmenter: torch_em.transform.invertible_augmentations.InvertibleAugmenter, supervised_train_loader: Optional[torch.utils.data.dataloader.DataLoader] = None, unsupervised_val_loader: Optional[torch.utils.data.dataloader.DataLoader] = None, supervised_val_loader: Optional[torch.utils.data.dataloader.DataLoader] = None, supervised_loss: Optional[Callable] = None, unsupervised_loss_and_metric: Optional[Callable] = None, supervised_loss_and_metric: Optional[Callable] = None, logger=<class 'torch_em.self_training.logger.SelfTrainingTensorboardLogger'>, source_distribution: List[float] = None, separate_backward: bool = False, **kwargs)
441    def __init__(
442        self,
443        model: torch.nn.Module,
444        unsupervised_train_loader: torch.utils.data.DataLoader,
445        unsupervised_loss: torch.utils.data.DataLoader,
446        pseudo_labeler: Callable,
447        augmenter: InvertibleAugmenter,
448        supervised_train_loader: Optional[torch.utils.data.DataLoader] = None,
449        unsupervised_val_loader: Optional[torch.utils.data.DataLoader] = None,
450        supervised_val_loader: Optional[torch.utils.data.DataLoader] = None,
451        supervised_loss: Optional[Callable] = None,
452        unsupervised_loss_and_metric: Optional[Callable] = None,
453        supervised_loss_and_metric: Optional[Callable] = None,
454        logger=SelfTrainingTensorboardLogger,
455        source_distribution: List[float] = None,
456        separate_backward: bool = False,
457        **kwargs,
458    ):
459        super().__init__(
460            model=model,
461            unsupervised_train_loader=unsupervised_train_loader,
462            unsupervised_loss=unsupervised_loss,
463            pseudo_labeler=pseudo_labeler,
464            supervised_train_loader=supervised_train_loader,
465            unsupervised_val_loader=unsupervised_val_loader,
466            supervised_val_loader=supervised_val_loader,
467            supervised_loss=supervised_loss,
468            unsupervised_loss_and_metric=unsupervised_loss_and_metric,
469            supervised_loss_and_metric=supervised_loss_and_metric,
470            logger=logger,
471            source_distribution=source_distribution,
472            separate_backward=separate_backward,
473            **kwargs,
474        )
475
476        self.augmenter = augmenter
augmenter