From a9939fa49c619075deb7d0682a80a234c5fd0065 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20Branchaud-Charron?= Date: Sun, 26 May 2024 14:49:20 -0400 Subject: [PATCH] Add Args to ModelWrapper to simplify common API (#294) * Add Args to ModelWrapper to simplify common API * Update experiment scripts --- README.md | 6 +- baal/active/dataset/base.py | 6 +- baal/active/heuristics/heuristics_gpu.py | 28 +-- baal/active/stopping_criteria.py | 13 +- baal/calibration/calibration.py | 48 ++--- baal/ensemble.py | 10 +- baal/modelwrapper.py | 165 ++++++----------- baal/utils/equality.py | 19 +- baal/utils/pytorch_lightning.py | 2 +- docs/api/modelwrapper.md | 15 +- docs/research/dirichlet_calibration.md | 12 +- docs/support/faq.md | 2 +- experiments/mlp_mcdropout.py | 21 ++- experiments/mlp_regression_mcdropout.py | 21 ++- .../active_image_classification.py | 1 - .../lightning_flash_example.py | 14 +- .../segmentation/unet_mcdropout_pascal.py | 28 +-- .../pimodel_mcdropout_cifar10.py | 6 +- experiments/vgg_mcdropout_cifar10.py | 17 +- notebooks/deep_ensemble.ipynb | 54 ++---- notebooks/fairness/ActiveFairness.ipynb | 65 ++----- notebooks/fundamentals/posteriors.ipynb | 95 ++-------- notebooks/mccaching_layer.ipynb | 27 +-- poetry.lock | 85 +++++---- pyproject.toml | 5 +- tests/active/criterion_test.py | 6 +- tests/active/heuristics_gpu_test.py | 17 +- tests/bayesian/test_caching.py | 5 +- tests/calibration/calibration_test.py | 27 ++- tests/ensemble_test.py | 7 +- tests/integration_test.py | 31 ++-- tests/metrics/test_mixin.py | 25 +-- tests/modelwrapper_test.py | 173 +++++++++--------- 33 files changed, 444 insertions(+), 612 deletions(-) diff --git a/README.md b/README.md index 2b23852e..2403e1c0 100644 --- a/README.md +++ b/README.md @@ -114,15 +114,15 @@ In conclusion, your script should be similar to this: dataset = ActiveLearningDataset(your_dataset) dataset.label_randomly(INITIAL_POOL) # label some data model = MCDropoutModule(your_model) -model = ModelWrapper(model, your_criterion) +model = ModelWrapper(model, args=TrainingArgs(...)) active_loop = ActiveLearningLoop(dataset, get_probabilities=model.predict_on_dataset, heuristic=heuristics.BALD(), iterations=20, # Number of MC sampling. query_size=QUERY_SIZE) # Number of item to label. for al_step in range(N_ALSTEP): - model.train_on_dataset(dataset, optimizer, BATCH_SIZE, use_cuda=use_cuda) - metrics = model.test_on_dataset(test_dataset, BATCH_SIZE) + model.train_on_dataset(dataset) + metrics = model.test_on_dataset(test_dataset) # Label the next most uncertain items. if not active_loop.step(): # We're done! diff --git a/baal/active/dataset/base.py b/baal/active/dataset/base.py index 7aabd4e0..c7c6518f 100644 --- a/baal/active/dataset/base.py +++ b/baal/active/dataset/base.py @@ -1,10 +1,12 @@ import warnings -from typing import Union, List, Optional, Any, TYPE_CHECKING, Protocol +from typing import Union, List, Optional, Any, TYPE_CHECKING, Protocol, Tuple import numpy as np from sklearn.utils import check_random_state from torch.utils import data as torchdata +from baal.utils.equality import assert_not_none + class SizeableDataset(torchdata.Dataset): def __len__(self): @@ -40,7 +42,7 @@ def __init__( if last_active_steps == 0 or last_active_steps < -1: raise ValueError("last_active_steps must be > 0 or -1 when disabled.") self.last_active_steps = last_active_steps - self._indices_cache = (-1, None) + self._indices_cache: Tuple[int, List[int]] = (-1, []) def get_indices_for_active_step(self) -> List[int]: """Returns the indices required for the active step. diff --git a/baal/active/heuristics/heuristics_gpu.py b/baal/active/heuristics/heuristics_gpu.py index 0749a8fc..618ef9d9 100644 --- a/baal/active/heuristics/heuristics_gpu.py +++ b/baal/active/heuristics/heuristics_gpu.py @@ -67,13 +67,12 @@ class AbstractGPUHeuristic(ModelWrapper): def __init__( self, model: ModelWrapper, - criterion, shuffle_prop=0.0, threshold=None, reverse=False, reduction="none", ): - super().__init__(model, criterion) + super().__init__(model, model.args) self.shuffle_prop = shuffle_prop self.threshold = threshold self.reversed = reverse @@ -102,32 +101,15 @@ def get_uncertainties(self, predictions): def predict_on_dataset( self, dataset: Dataset, - batch_size: int, iterations: int, - use_cuda: bool, - workers: int = 4, - collate_fn: Optional[Callable] = None, half=False, verbose=True, ): - return ( - super() - .predict_on_dataset( - dataset, - batch_size, - iterations, - use_cuda, - workers, - collate_fn, - half, - verbose, - ) - .reshape([-1]) - ) + return super().predict_on_dataset(dataset, iterations, half, verbose).reshape([-1]) - def predict_on_batch(self, data, iterations=1, use_cuda=False): + def predict_on_batch(self, data, iterations=1): """Rank the predictions according to their uncertainties.""" - return self.get_uncertainties(self.model.predict_on_batch(data, iterations, cuda=use_cuda)) + return self.get_uncertainties(self.model.predict_on_batch(data, iterations)) class BALDGPUWrapper(AbstractGPUHeuristic): @@ -139,14 +121,12 @@ class BALDGPUWrapper(AbstractGPUHeuristic): def __init__( self, model: ModelWrapper, - criterion, shuffle_prop=0.0, threshold=None, reduction="none", ): super().__init__( model, - criterion=criterion, shuffle_prop=shuffle_prop, threshold=threshold, reverse=True, diff --git a/baal/active/stopping_criteria.py b/baal/active/stopping_criteria.py index ac9e6695..66ffcf3a 100644 --- a/baal/active/stopping_criteria.py +++ b/baal/active/stopping_criteria.py @@ -1,4 +1,4 @@ -from typing import Iterable, Dict +from typing import Iterable, Dict, List import numpy as np @@ -21,7 +21,7 @@ def __init__(self, active_dataset: ActiveLearningDataset, labelling_budget: int) self._start_length = len(active_dataset) self.labelling_budget = labelling_budget - def should_stop(self, uncertainty: Iterable[float]) -> bool: + def should_stop(self, metrics: Dict[str, float], uncertainty: Iterable[float]) -> bool: return (len(self._active_ds) - self._start_length) >= self.labelling_budget @@ -33,7 +33,8 @@ def __init__(self, active_dataset: ActiveLearningDataset, avg_uncertainty_thresh self.avg_uncertainty_thresh = avg_uncertainty_thresh def should_stop(self, metrics: Dict[str, float], uncertainty: Iterable[float]) -> bool: - return np.mean(uncertainty) < self.avg_uncertainty_thresh + arr = np.array(uncertainty) + return bool(np.mean(arr) < self.avg_uncertainty_thresh) class EarlyStoppingCriterion(StoppingCriterion): @@ -55,9 +56,11 @@ def __init__( self.metric_name = metric_name self.patience = patience self.epsilon = epsilon - self._acc = [] + self._acc: List[float] = [] def should_stop(self, metrics: Dict[str, float], uncertainty: Iterable[float]) -> bool: self._acc.append(metrics[self.metric_name]) near_threshold = np.isclose(np.array(self._acc), self._acc[-1], atol=self.epsilon) - return len(near_threshold) >= self.patience and near_threshold[-(self.patience + 1) :].all() + return len(near_threshold) >= self.patience and bool( + near_threshold[-(self.patience + 1) :].all() + ) diff --git a/baal/calibration/calibration.py b/baal/calibration/calibration.py index 42e96645..493bd024 100644 --- a/baal/calibration/calibration.py +++ b/baal/calibration/calibration.py @@ -1,4 +1,5 @@ from copy import deepcopy +from typing import Optional import structlog import torch @@ -7,6 +8,7 @@ from torch.optim import Adam from baal import ModelWrapper +from baal.modelwrapper import TrainingArgs from baal.utils.metrics import ECE, ECE_PerCLs log = structlog.get_logger("Calibrating...") @@ -37,6 +39,7 @@ class DirichletCalibrator(object): reg_factor (float): Regularization factor for the linear layer weights. mu (float): Regularization factor for the linear layer biases. If not given, will be initialized by "l". + training_duration (int): How long to train calibration layer. """ @@ -46,7 +49,8 @@ def __init__( num_classes: int, lr: float, reg_factor: float, - mu: float = None, + mu: Optional[float] = None, + training_duration: int = 5, ): self.num_classes = num_classes self.criterion = nn.CrossEntropyLoss() @@ -55,7 +59,17 @@ def __init__( self.mu = mu or reg_factor self.dirichlet_linear = nn.Linear(self.num_classes, self.num_classes) self.model = nn.Sequential(wrapper.model, self.dirichlet_linear) - self.wrapper = ModelWrapper(self.model, self.criterion) + self.optimizer = Adam(self.dirichlet_linear.parameters(), lr=self.lr) + self.wrapper = ModelWrapper( + self.model, + TrainingArgs( + criterion=self.criterion, + optimizer=self.optimizer, + regularizer=self.l2_reg, + epoch=training_duration, + use_cuda=wrapper.args.use_cuda, + ), + ) self.wrapper.add_metric("ece", lambda: ECE()) self.wrapper.add_metric("ece", lambda: ECE_PerCLs(num_classes)) @@ -75,8 +89,6 @@ def calibrate( self, train_set: Dataset, test_set: Dataset, - batch_size: int, - epoch: int, use_cuda: bool, double_fit: bool = False, **kwargs @@ -88,8 +100,6 @@ def calibrate( Args: train_set (Dataset): The training set. test_set (Dataset): The validation set. - batch_size (int): Batch size used. - epoch (int): Number of epochs to train the linear layer for. use_cuda (bool): If "True", will use GPU. double_fit (bool): If "True" would fit twice on the train set. kwargs (dict): Rest of parameters for baal.ModelWrapper.train_and_test_on_dataset(). @@ -106,36 +116,16 @@ def calibrate( if use_cuda: self.dirichlet_linear.cuda() - optimizer = Adam(self.dirichlet_linear.parameters(), lr=self.lr) - loss_history, weights = self.wrapper.train_and_test_on_datasets( - train_set, - test_set, - optimizer, - batch_size, - epoch, - use_cuda, - regularizer=self.l2_reg, - return_best_weights=True, - patience=None, - **kwargs + train_set, test_set, return_best_weights=True, patience=None, **kwargs ) self.model.load_state_dict(weights) if double_fit: lr = self.lr / 10 - optimizer = Adam(self.dirichlet_linear.parameters(), lr=lr) + self.wrapper.args.optimizer = Adam(self.dirichlet_linear.parameters(), lr=lr) loss_history, weights = self.wrapper.train_and_test_on_datasets( - train_set, - test_set, - optimizer, - batch_size, - epoch, - use_cuda, - regularizer=self.l2_reg, - return_best_weights=True, - patience=None, - **kwargs + train_set, test_set, return_best_weights=True, patience=None, **kwargs ) self.model.load_state_dict(weights) diff --git a/baal/ensemble.py b/baal/ensemble.py index da86f8dd..830d3a84 100644 --- a/baal/ensemble.py +++ b/baal/ensemble.py @@ -5,7 +5,7 @@ from torch import nn, Tensor from baal import ModelWrapper -from baal.modelwrapper import _stack_preds +from baal.modelwrapper import _stack_preds, TrainingArgs from baal.utils.cuda_utils import to_cuda @@ -15,16 +15,16 @@ class EnsembleModelWrapper(ModelWrapper): Args: model (nn.Module): A Model. - criterion (Callable): Loss function + args (TrainingArgs): Argument for model Notes: If you're looking to use ensembles for non-deep models, see our sklearn tutorial: baal.readthedocs.io/en/latest/notebooks/sklearn_tutorial.html """ - def __init__(self, model, criterion): - super().__init__(model, criterion) - self._weights = [] + def __init__(self, model, args: TrainingArgs): + super().__init__(model, args) + self._weights: List[Dict] = [] def add_checkpoint(self): """ diff --git a/baal/modelwrapper.py b/baal/modelwrapper.py index d1b36845..718641b9 100644 --- a/baal/modelwrapper.py +++ b/baal/modelwrapper.py @@ -2,6 +2,7 @@ from collections import defaultdict from collections.abc import Sequence from copy import deepcopy +from dataclasses import dataclass from typing import Callable, Optional import numpy as np @@ -12,10 +13,11 @@ from torch.utils.data.dataloader import default_collate from tqdm import tqdm +from baal.active.dataset.base import Dataset from baal.metrics.mixin import MetricMixin from baal.utils.array_utils import stack_in_memory -from baal.active.dataset.base import Dataset from baal.utils.cuda_utils import to_cuda +from baal.utils.equality import assert_not_none from baal.utils.iterutils import map_on_tensor from baal.utils.metrics import Loss from baal.utils.warnings import raise_warnings_cache_replicated @@ -31,6 +33,19 @@ def _stack_preds(out): return out +@dataclass +class TrainingArgs: + optimizer: Optional[Optimizer] = None + batch_size: int = 32 + epoch: int = 0 + use_cuda: bool = torch.cuda.is_available() + workers: int = 4 + collate_fn: Callable = default_collate + regularizer: Optional[Callable] = None + criterion: Optional[Callable] = None + replicate_in_memory: bool = True + + class ModelWrapper(MetricMixin): """ Wrapper created to ease the training/testing/loading. @@ -41,40 +56,24 @@ class ModelWrapper(MetricMixin): replicate_in_memory (bool): Replicate in memory optional. """ - def __init__(self, model, criterion, replicate_in_memory=True): + def __init__(self, model, args: TrainingArgs): self.model = model - self.criterion = criterion + self.args = args self.metrics = dict() self.active_learning_metrics = defaultdict(dict) self.add_metric("loss", lambda: Loss()) - self.replicate_in_memory = replicate_in_memory self._active_dataset_size = -1 - raise_warnings_cache_replicated(self.model, replicate_in_memory=replicate_in_memory) + raise_warnings_cache_replicated( + self.model, replicate_in_memory=self.args.replicate_in_memory + ) - def train_on_dataset( - self, - dataset, - optimizer, - batch_size, - epoch, - use_cuda, - workers=4, - collate_fn: Optional[Callable] = None, - regularizer: Optional[Callable] = None, - ): + def train_on_dataset(self, dataset): """ Train for `epoch` epochs on a Dataset `dataset. Args: dataset (Dataset): Pytorch Dataset to be trained on. - optimizer (optim.Optimizer): Optimizer to use. - batch_size (int): The batch size used in the DataLoader. - epoch (int): Number of epoch to train for. - use_cuda (bool): Use cuda or not. - workers (int): Number of workers for the multiprocessing. - collate_fn (Optional[Callable]): The collate function to use. - regularizer (Optional[Callable]): The loss regularization for training. Returns: The training history. @@ -83,17 +82,20 @@ def train_on_dataset( self.train() self.set_dataset_size(dataset_size) history = [] - log.info("Starting training", epoch=epoch, dataset=dataset_size) - collate_fn = collate_fn or default_collate - for _ in range(epoch): + log.info("Starting training", epoch=self.args.epoch, dataset=dataset_size) + for _ in range(self.args.epoch): self._reset_metrics("train") for data, target, *_ in DataLoader( - dataset, batch_size, True, num_workers=workers, collate_fn=collate_fn + dataset, + self.args.batch_size, + True, + num_workers=self.args.workers, + collate_fn=self.args.collate_fn, ): - _ = self.train_on_batch(data, target, optimizer, use_cuda, regularizer) + _ = self.train_on_batch(data, target) history.append(self.get_metrics("train")["train_loss"]) - optimizer.zero_grad() # Assert that the gradient is flushed. + self.args.optimizer.zero_grad() # Assert that the gradient is flushed. log.info("Training complete", train_loss=self.get_metrics("train")["train_loss"]) self.active_step(dataset_size, self.get_metrics("train")) return history @@ -101,10 +103,6 @@ def train_on_dataset( def test_on_dataset( self, dataset: Dataset, - batch_size: int, - use_cuda: bool, - workers: int = 4, - collate_fn: Optional[Callable] = None, average_predictions: int = 1, ): """ @@ -112,10 +110,6 @@ def test_on_dataset( Args: dataset (Dataset): Dataset to evaluate on. - batch_size (int): Batch size used for evaluation. - use_cuda (bool): Use Cuda or not. - workers (int): Number of workers to use. - collate_fn (Optional[Callable]): The collate function to use. average_predictions (int): The number of predictions to average to compute the test loss. @@ -127,11 +121,13 @@ def test_on_dataset( self._reset_metrics("test") for data, target, *_ in DataLoader( - dataset, batch_size, False, num_workers=workers, collate_fn=collate_fn + dataset, + self.args.batch_size, + False, + num_workers=self.args.workers, + collate_fn=self.args.collate_fn, ): - _ = self.test_on_batch( - data, target, cuda=use_cuda, average_predictions=average_predictions - ) + _ = self.test_on_batch(data, target, average_predictions=average_predictions) log.info("Evaluation complete", test_loss=self.get_metrics("test")["test_loss"]) self.active_step(None, self.get_metrics("test")) @@ -141,13 +137,6 @@ def train_and_test_on_datasets( self, train_dataset: Dataset, test_dataset: Dataset, - optimizer: Optimizer, - batch_size: int, - epoch: int, - use_cuda: bool, - workers: int = 4, - collate_fn: Optional[Callable] = None, - regularizer: Optional[Callable] = None, return_best_weights=False, patience=None, min_epoch_for_es=0, @@ -160,12 +149,6 @@ def train_and_test_on_datasets( train_dataset (Dataset): Dataset to train on. test_dataset (Dataset): Dataset to evaluate on. optimizer (Optimizer): Optimizer to use during training. - batch_size (int): Batch size used. - epoch (int): Number of epoch to train on. - use_cuda (bool): Use Cuda or not. - workers (int): Number of workers to use. - collate_fn (Optional[Callable]): The collate function to use. - regularizer (Optional[Callable]): The loss regularization for training. return_best_weights (bool): If True, will keep the best weights and return them. patience (Optional[int]): If provided, will use early stopping to stop after `patience` epoch without improvement. @@ -179,14 +162,12 @@ def train_and_test_on_datasets( best_loss = 1e10 best_epoch = 0 hist = [] - for e in range(epoch): + for e in range(self.args.epoch): _ = self.train_on_dataset( - train_dataset, optimizer, batch_size, 1, use_cuda, workers, collate_fn, regularizer + train_dataset, ) if e % skip_epochs == 0: - te_loss = self.test_on_dataset( - test_dataset, batch_size, use_cuda, workers, collate_fn - ) + te_loss = self.test_on_dataset(test_dataset) hist.append(self.get_metrics()) if te_loss < best_loss: best_epoch = e @@ -208,11 +189,7 @@ def train_and_test_on_datasets( def predict_on_dataset_generator( self, dataset: Dataset, - batch_size: int, iterations: int, - use_cuda: bool, - workers: int = 4, - collate_fn: Optional[Callable] = None, half=False, verbose=True, ): @@ -221,11 +198,7 @@ def predict_on_dataset_generator( Args: dataset (Dataset): Dataset to predict on. - batch_size (int): Batch size to use during prediction. iterations (int): Number of iterations per sample. - use_cuda (bool): Use CUDA or not. - workers (int): Number of workers to use. - collate_fn (Optional[Callable]): The collate function to use. half (bool): If True use half precision. verbose (bool): If True use tqdm to display progress @@ -240,13 +213,18 @@ def predict_on_dataset_generator( return None log.info("Start Predict", dataset=len(dataset)) - collate_fn = collate_fn or default_collate - loader = DataLoader(dataset, batch_size, False, num_workers=workers, collate_fn=collate_fn) + loader = DataLoader( + dataset, + self.args.batch_size, + False, + num_workers=self.args.workers, + collate_fn=self.args.collate_fn, + ) if verbose: loader = tqdm(loader, total=len(loader), file=sys.stdout) for idx, (data, *_) in enumerate(loader): - pred = self.predict_on_batch(data, iterations, use_cuda) + pred = self.predict_on_batch(data, iterations) pred = map_on_tensor(lambda x: x.detach(), pred) if half: pred = map_on_tensor(lambda x: x.half(), pred) @@ -255,11 +233,7 @@ def predict_on_dataset_generator( def predict_on_dataset( self, dataset: Dataset, - batch_size: int, iterations: int, - use_cuda: bool, - workers: int = 4, - collate_fn: Optional[Callable] = None, half=False, verbose=True, ): @@ -268,11 +242,7 @@ def predict_on_dataset( Args: dataset (Dataset): Dataset to predict on. - batch_size (int): Batch size to use during prediction. iterations (int): Number of iterations per sample. - use_cuda (bool): Use CUDA or not. - workers (int): Number of workers to use. - collate_fn (Optional[Callable]): The collate function to use. half (bool): If True use half precision. verbose (bool): If True use tqdm to show progress. @@ -285,11 +255,7 @@ def predict_on_dataset( preds = list( self.predict_on_dataset_generator( dataset=dataset, - batch_size=batch_size, iterations=iterations, - use_cuda=use_cuda, - workers=workers, - collate_fn=collate_fn, half=half, verbose=verbose, ) @@ -300,37 +266,31 @@ def predict_on_dataset( return np.vstack(preds) return [np.vstack(pr) for pr in zip(*preds)] - def train_on_batch( - self, data, target, optimizer, cuda=False, regularizer: Optional[Callable] = None - ): + def train_on_batch(self, data, target): """ Train the current model on a batch using `optimizer`. Args: data (Tensor): The model input. target (Tensor): The ground truth. - optimizer (optim.Optimizer): An optimizer. - cuda (bool): Use CUDA or not. - regularizer (Optional[Callable]): The loss regularization for training. - Returns: Tensor, the loss computed from the criterion. """ - if cuda: + if self.args.use_cuda: data, target = to_cuda(data), to_cuda(target) - optimizer.zero_grad() + self.args.optimizer.zero_grad() output = self.model(data) - loss = self.criterion(output, target) + loss = self.args.criterion(output, target) - if regularizer: - regularized_loss = loss + regularizer() + if self.args.regularizer: + regularized_loss = loss + self.args.regularizer() regularized_loss.backward() else: loss.backward() - optimizer.step() + self.args.optimizer.step() self._update_metrics(output, target, loss, filter="train") return loss @@ -338,7 +298,6 @@ def test_on_batch( self, data: torch.Tensor, target: torch.Tensor, - cuda: bool = False, average_predictions: int = 1, ): """ @@ -347,7 +306,6 @@ def test_on_batch( Args: data (Tensor): The model input. target (Tensor): The ground truth. - cuda (bool): Use CUDA or not. average_predictions (int): The number of predictions to average to compute the test loss. @@ -355,25 +313,24 @@ def test_on_batch( Tensor, the loss computed from the criterion. """ with torch.no_grad(): - if cuda: + if self.args.use_cuda: data, target = to_cuda(data), to_cuda(target) preds = map_on_tensor( lambda p: p.mean(-1), - self.predict_on_batch(data, iterations=average_predictions, cuda=cuda), + self.predict_on_batch(data, iterations=average_predictions), ) - loss = self.criterion(preds, target) + loss = assert_not_none(self.args.criterion)(preds, target) self._update_metrics(preds, target, loss, "test") return loss - def predict_on_batch(self, data, iterations=1, cuda=False): + def predict_on_batch(self, data, iterations=1): """ Get the model's prediction on a batch. Args: data (Tensor): The model input. iterations (int): Number of prediction to perform. - cuda (bool): Use CUDA or not. Returns: Tensor, the loss computed from the criterion. @@ -383,9 +340,9 @@ def predict_on_batch(self, data, iterations=1, cuda=False): Raises RuntimeError if CUDA rans out of memory during data replication. """ with torch.no_grad(): - if cuda: + if self.args.use_cuda: data = to_cuda(data) - if self.replicate_in_memory: + if self.args.replicate_in_memory: data = map_on_tensor(lambda d: stack_in_memory(d, iterations), data) try: out = self.model(data) diff --git a/baal/utils/equality.py b/baal/utils/equality.py index 86b71c63..55717a78 100644 --- a/baal/utils/equality.py +++ b/baal/utils/equality.py @@ -1,9 +1,11 @@ -from typing import Sequence, Mapping +from typing import Sequence, Mapping, Optional, TypeVar import numpy as np import torch from torch import Tensor +T = TypeVar("T") + def deep_check(obj1, obj2) -> bool: if type(obj1) != type(obj2): @@ -20,3 +22,18 @@ def deep_check(obj1, obj2) -> bool: return bool((obj1 == obj2).all()) else: return bool(obj1 == obj2) + + +def assert_not_none(val: Optional[T]) -> T: + """ + This function makes sure that the variable is not None and has a fixed type for mypy purposes. + Args: + val: any value which is Optional. + Returns: + val [T]: The same value with a defined type. + Raises: + Assertion error if val is None. + """ + if val is None: + raise AssertionError(f"value of {val} is None, expected not None") + return val diff --git a/baal/utils/pytorch_lightning.py b/baal/utils/pytorch_lightning.py index 2faf4a91..790e5f95 100644 --- a/baal/utils/pytorch_lightning.py +++ b/baal/utils/pytorch_lightning.py @@ -78,7 +78,7 @@ def predict_step(self, batch, batch_idx, dataloader_idx: Optional[int] = None): # Perform Monte-Carlo Inference fro I iterations. out = mc_inference( self, x, self.hparams.iterations, self.hparams.replicate_in_memory # type: ignore - ) # type: ignore + ) return out diff --git a/docs/api/modelwrapper.md b/docs/api/modelwrapper.md index 72811d32..e3a3ae85 100644 --- a/docs/api/modelwrapper.md +++ b/docs/api/modelwrapper.md @@ -8,23 +8,28 @@ Another optimization that we do is that instead of using a for-loop to perform M ### Example ```python -from baal.modelwrapper import ModelWrapper +from baal.modelwrapper import ModelWrapper, TrainingArgs from baal.active.dataset import ActiveLearningDataset from torch.utils.data import Dataset # You define ModelWrapper with a Pytorch model and a criterion. -wrapper = ModelWrapper(model=your_model, criterion=your_criterion) +wrapper = ModelWrapper(model=your_model, + args=TrainingArgs(criterion=your_criterion, + optimizer=your_optimizer, + batch_size=32, + epoch=10, + use_cuda=True)) # Assuming you have your ActiveLearningDataset ready, al_dataset: ActiveLearningDataset = ... test_dataset: Dataset = ... -train_history = wrapper.train_on_dataset(al_dataset, optimizer=your_optimizer, batch_size=32, epoch=10, use_cuda=True) +train_history = wrapper.train_on_dataset(al_dataset) # We can also use BMA during test time using `average_predictions`. -test_values = wrapper.test_on_dataset(test_dataset, average_predictions=20, **kwargs) +test_values = wrapper.test_on_dataset(test_dataset, average_predictions=20) # We use Monte-Carlo sampling using the `iterations` arguments. -predictions = wrapper.predict_on_dataset(al_dataset.pool, iterations=20, **kwargs) +predictions = wrapper.predict_on_dataset(al_dataset.pool, iterations=20) predictions.shape # > [len(al_dataset.pool), num_outputs, 20] diff --git a/docs/research/dirichlet_calibration.md b/docs/research/dirichlet_calibration.md index ebafe12e..b8196437 100644 --- a/docs/research/dirichlet_calibration.md +++ b/docs/research/dirichlet_calibration.md @@ -93,26 +93,24 @@ By giving more nuanced predictions, the model is deemed more trustable by the hu With Baal 1.2, we add a new module based on this report. We propose new tools and methods to calibrate your model. Our first method will be a Pytorch implementation of the Dirichlet Calibration method. Here is an example: ```python -from baal import DirichletCalibrator -from baal import ModelWrapper +from baal.calibration import DirichletCalibrator +from baal.modelwrapper import ModelWrapper, TrainingArgs """ Get your train and validation set. In addition, you need a held-out set to calibrate your model. """ train_ds, calib_ds, valid_ds = get_datasets() -wrapper = ModelWrapper(MyModel(), criterion=YourCriterion()) +wrapper = ModelWrapper(MyModel(), TrainingArgs(...)) # Make a calibrator object. calibrator = DirichletCalibrator(wrapper, 2, lr=0.001, reg_factor=0.001) # Train your model as usual. -wrapper.train_on_dataset(train_ds, SGD(...), batch_size=32, epoch=30, use_cuda=True) +wrapper.train_on_dataset(train_ds) # Calibrate your model on a held-out set. -calibrator.calibrate(calib_ds, valid_ds, batch_size=10, epoch=5, - use_cuda=True, - double_fit=True, workers=4) +calibrator.calibrate(calib_ds, valid_ds, use_cuda=True, double_fit=True) calibrated_model = calibrator.calibrated_model ``` diff --git a/docs/support/faq.md b/docs/support/faq.md index 8fd9b1f3..6a17295f 100644 --- a/docs/support/faq.md +++ b/docs/support/faq.md @@ -20,7 +20,7 @@ model = YourModel() # If not done already, you can wrap your model with our MCDropoutModule model = MCDropoutModule(model) dataset = YourDataset() -wrapper = ModelWrapper(model, criterion=None) +wrapper = ModelWrapper(model, args=TrainingArgs(...)) heuristic = BALD() diff --git a/experiments/mlp_mcdropout.py b/experiments/mlp_mcdropout.py index b7690522..03d2135c 100644 --- a/experiments/mlp_mcdropout.py +++ b/experiments/mlp_mcdropout.py @@ -11,6 +11,7 @@ from baal.active.heuristics import BALD from baal.active.stopping_criteria import LabellingBudgetStoppingCriterion from baal.bayesian.dropout import patch_module +from baal.modelwrapper import TrainingArgs use_cuda = torch.cuda.is_available() @@ -35,8 +36,18 @@ model = patch_module(model) # Set dropout layers for MC-Dropout. if use_cuda: model = model.cuda() -wrapper = ModelWrapper(model=model, criterion=nn.CrossEntropyLoss()) + optimizer = optim.SGD(model.parameters(), lr=0.001, momentum=0.9, weight_decay=5e-4) +wrapper = ModelWrapper( + model=model, + args=TrainingArgs( + criterion=nn.CrossEntropyLoss(), + optimizer=optimizer, + batch_size=32, + epoch=10, + use_cuda=use_cuda, + ), +) # We will use BALD as our heuristic as it is a great tradeoff between performance and efficiency. bald = BALD() @@ -48,8 +59,6 @@ query_size=100, # We will label 100 examples per step. # KWARGS for predict_on_dataset iterations=20, # 20 sampling for MC-Dropout - batch_size=32, - use_cuda=use_cuda, verbose=False, ) @@ -62,11 +71,11 @@ while True: model.load_state_dict(initial_weights) train_loss = wrapper.train_on_dataset( - al_dataset, optimizer=optimizer, batch_size=32, epoch=10, use_cuda=use_cuda + al_dataset, ) - test_loss = wrapper.test_on_dataset(test_ds, batch_size=32, use_cuda=use_cuda) + test_loss = wrapper.test_on_dataset(test_ds) pprint(wrapper.get_metrics()) flag = al_loop.step() - if stopping_criterion.should_stop() or flag: + if stopping_criterion.should_stop(uncertainty=[]) or flag: break diff --git a/experiments/mlp_regression_mcdropout.py b/experiments/mlp_regression_mcdropout.py index 5c2fa452..a1fd716e 100644 --- a/experiments/mlp_regression_mcdropout.py +++ b/experiments/mlp_regression_mcdropout.py @@ -13,6 +13,7 @@ from baal.active import ActiveLearningLoop from baal.active.heuristics import Variance from baal.bayesian.dropout import patch_module +from baal.modelwrapper import TrainingArgs use_cuda = torch.cuda.is_available() @@ -73,8 +74,15 @@ def __getitem__(self, item): if use_cuda: model = model.cuda() -wrapper = ModelWrapper(model=model, criterion=nn.L1Loss()) + optimizer = optim.SGD(model.parameters(), lr=0.001, momentum=0.9, weight_decay=5e-4) +wrapper = ModelWrapper( + model=model, + args=TrainingArgs( + criterion=nn.L1Loss(), optimizer=optimizer, batch_size=32, epoch=10, use_cuda=use_cuda + ), +) + # We will use Variance as our heuristic for regression problems. variance = Variance() @@ -84,13 +92,10 @@ def __getitem__(self, item): dataset=al_dataset, get_probabilities=wrapper.predict_on_dataset, heuristic=variance, - query_size=250, # We will label 20 examples per step. + query_size=20, # We will label 20 examples per step. # KWARGS for predict_on_dataset iterations=20, # 20 sampling for MC-Dropout - batch_size=16, - use_cuda=use_cuda, verbose=False, - workers=0, ) # Following Gal 2016, we reset the weights at the beginning of each step. @@ -98,10 +103,8 @@ def __getitem__(self, item): for step in range(1000): model.load_state_dict(initial_weights) - train_loss = wrapper.train_on_dataset( - al_dataset, optimizer=optimizer, batch_size=16, epoch=1000, use_cuda=use_cuda, workers=0 - ) - test_loss = wrapper.test_on_dataset(test_ds, batch_size=16, use_cuda=use_cuda, workers=0) + train_loss = wrapper.train_on_dataset(al_dataset) + test_loss = wrapper.test_on_dataset(test_ds) pprint(wrapper.get_metrics()) flag = al_loop.step() diff --git a/experiments/pytorch_lightning/active_image_classification.py b/experiments/pytorch_lightning/active_image_classification.py index 4970a7cd..8ad85226 100644 --- a/experiments/pytorch_lightning/active_image_classification.py +++ b/experiments/pytorch_lightning/active_image_classification.py @@ -5,7 +5,6 @@ import pytorch_lightning as pl import structlog -from pytorch_lightning import LightningModule from pytorch_lightning.loggers import TensorBoardLogger from torch import optim from torch.nn import CrossEntropyLoss diff --git a/experiments/pytorch_lightning/lightning_flash_example.py b/experiments/pytorch_lightning/lightning_flash_example.py index 928e73e6..4c837da0 100644 --- a/experiments/pytorch_lightning/lightning_flash_example.py +++ b/experiments/pytorch_lightning/lightning_flash_example.py @@ -1,20 +1,16 @@ import argparse import os -from typing import Any, List +from functools import partial -import numpy as np import structlog import torch import torch.backends +from flash.core.classification import LogitsOutput from pytorch_lightning import seed_everything from pytorch_lightning.loggers import TensorBoardLogger -from pytorch_lightning.trainer.progress import Progress -from pytorch_lightning.trainer.states import TrainerFn, TrainerStatus, RunningStage -from pytorch_lightning.utilities.exceptions import MisconfigurationException from torch import nn from torchvision import datasets from torchvision.transforms import transforms -from functools import partial try: import flash @@ -26,7 +22,6 @@ "lightning-flash.git#egg=lightning-flash[image]'" ) from flash.image import ImageClassifier, ImageClassificationData -from flash.core.classification import Logits from flash.image.classification.integrations.baal import ( ActiveLearningDataModule, ActiveLearningLoop, @@ -106,8 +101,11 @@ def get_model(dm): loss_fn=loss_fn, optimizer=partial(torch.optim.SGD, momentum=0.9, weight_decay=5e-4), learning_rate=LR, - serializer=Logits(), # Note the serializer to Logits to be able to estimate uncertainty. ) + model.output = ( + LogitsOutput() + ) # Note the serializer to Logits to be able to estimate uncertainty. + return model diff --git a/experiments/segmentation/unet_mcdropout_pascal.py b/experiments/segmentation/unet_mcdropout_pascal.py index d9a3fbd1..28ac72ca 100644 --- a/experiments/segmentation/unet_mcdropout_pascal.py +++ b/experiments/segmentation/unet_mcdropout_pascal.py @@ -8,11 +8,12 @@ from torchvision.transforms import transforms from tqdm import tqdm -from baal import get_heuristic, ActiveLearningLoop -from baal.bayesian.dropout import MCDropoutModule from baal import ModelWrapper -from baal import ClassificationReport -from baal import PILToLongTensor +from baal.active import get_heuristic, ActiveLearningLoop +from baal.bayesian.dropout import MCDropoutModule +from baal.modelwrapper import TrainingArgs +from baal.utils.metrics import ClassificationReport +from baal.utils.transforms import PILToLongTensor from utils import pascal_voc_ids, active_pascal, add_dropout, FocalLoss try: @@ -122,7 +123,16 @@ def main(): initial_weights = deepcopy(model.state_dict()) # Add metrics - model = ModelWrapper(model, criterion) + model = ModelWrapper( + model, + args=TrainingArgs( + criterion=criterion, + optimizer=optimizer, + batch_size=batch_size, + epoch=hyperparams["learning_epoch"], + use_cuda=use_cuda, + ), + ) model.add_metric("cls_report", lambda: ClassificationReport(len(pascal_voc_ids))) # Which heuristic you want to use? @@ -137,21 +147,17 @@ def main(): query_size=hyperparams["query_size"], # Instead of predicting on the entire pool, only a subset is used max_sample=1000, - batch_size=batch_size, iterations=hyperparams["iterations"], - use_cuda=use_cuda, ) acc = [] for epoch in tqdm(range(args.al_step)): # Following Gal et al. 2016, we reset the weights. model.load_state_dict(initial_weights) # Train 50 epochs before sampling. - model.train_on_dataset( - active_set, optimizer, batch_size, hyperparams["learning_epoch"], use_cuda - ) + model.train_on_dataset(active_set) # Validation! - model.test_on_dataset(test_set, batch_size, use_cuda) + model.test_on_dataset(test_set) should_continue = loop.step() logs = model.get_metrics() diff --git a/experiments/ssl_experiments/pimodel_mcdropout_cifar10.py b/experiments/ssl_experiments/pimodel_mcdropout_cifar10.py index 7f3ae2ad..7f039189 100644 --- a/experiments/ssl_experiments/pimodel_mcdropout_cifar10.py +++ b/experiments/ssl_experiments/pimodel_mcdropout_cifar10.py @@ -3,6 +3,9 @@ from argparse import Namespace import torch + +from baal.active import get_heuristic +from baal.utils.pytorch_lightning import ActiveLightningModule, BaalTrainer, ResetCallback from experiments.ssl_experiments.pimodel_cifar10 import PIModel from torch import nn from torch.hub import load_state_dict_from_url @@ -10,9 +13,8 @@ from torchvision.datasets import CIFAR10 from torchvision.models import vgg16 -from baal import ActiveLearningDataset, get_heuristic +from baal import ActiveLearningDataset from baal.bayesian.dropout import patch_module -from baal import ActiveLightningModule, BaalTrainer, ResetCallback class PIActiveLearningModel(ActiveLightningModule, PIModel): diff --git a/experiments/vgg_mcdropout_cifar10.py b/experiments/vgg_mcdropout_cifar10.py index d4bdec8d..8af49146 100644 --- a/experiments/vgg_mcdropout_cifar10.py +++ b/experiments/vgg_mcdropout_cifar10.py @@ -17,6 +17,7 @@ from baal.active.active_loop import ActiveLearningLoop from baal.bayesian.dropout import patch_module from baal import ModelWrapper +from baal.modelwrapper import TrainingArgs """ Minimal example to use BaaL. @@ -97,7 +98,15 @@ def main(): optimizer = optim.SGD(model.parameters(), lr=hyperparams["lr"], momentum=0.9) # Wraps the model into a usable API. - model = ModelWrapper(model, criterion) + model = ModelWrapper( + model, + TrainingArgs( + optimizer=optimizer, + criterion=criterion, + epoch=hyperparams["learning_epoch"], + use_cuda=use_cuda, + ), + ) logs = {} logs["epoch"] = 0 @@ -121,14 +130,10 @@ def main(): model.load_state_dict(init_weights) model.train_on_dataset( active_set, - optimizer, - hyperparams["batch_size"], - hyperparams["learning_epoch"], - use_cuda, ) # Validation! - model.test_on_dataset(test_set, hyperparams["batch_size"], use_cuda) + model.test_on_dataset(test_set) should_continue = active_loop.step() if not should_continue: break diff --git a/notebooks/deep_ensemble.ipynb b/notebooks/deep_ensemble.ipynb index 57987380..a608b02f 100644 --- a/notebooks/deep_ensemble.ipynb +++ b/notebooks/deep_ensemble.ipynb @@ -33,7 +33,6 @@ "name": "#%%\n" } }, - "outputs": [], "source": [ "import random\n", "from copy import deepcopy\n", @@ -64,7 +63,8 @@ " if isinstance(m, nn.Linear):\n", " nn.init.normal_(m.weight, 0, 0.01)\n", " nn.init.constant_(m.bias, 0)" - ] + ], + "outputs": [] }, { "cell_type": "code", @@ -74,7 +74,6 @@ "name": "#%%\n" } }, - "outputs": [], "source": [ "@dataclass\n", "class ExperimentConfig:\n", @@ -115,7 +114,8 @@ " # We start labeling randomly.\n", " active_set.label_randomly(initial_pool)\n", " return active_set, test_set" - ] + ], + "outputs": [] }, { "cell_type": "code", @@ -125,16 +125,6 @@ "name": "#%%\n" } }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Files already downloaded and verified\n", - "Files already downloaded and verified\n" - ] - } - ], "source": [ "hyperparams = ExperimentConfig()\n", "use_cuda = torch.cuda.is_available()\n", @@ -171,7 +161,8 @@ "\n", "# We will reset the weights at each active learning step.\n", "init_weights = deepcopy(model.state_dict())" - ] + ], + "outputs": [] }, { "cell_type": "markdown", @@ -214,7 +205,6 @@ "is_executing": true } }, - "outputs": [], "source": [ "report = []\n", "for epoch in tqdm(range(hyperparams.epoch)):\n", @@ -249,7 +239,8 @@ " \"Next Training set size\": len(active_set)\n", " }\n", " report.append(logs)" - ] + ], + "outputs": [] }, { "cell_type": "code", @@ -259,37 +250,14 @@ "name": "#%%\n" } }, - "outputs": [ - { - "data": { - "text/plain": [ - "[]" - ] - }, - "execution_count": 5, - "metadata": {}, - "output_type": "execute_result" - }, - { - "data": { - "image/png": "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\n", - "text/plain": [ - "
" - ] - }, - "metadata": { - "needs_background": "light" - }, - "output_type": "display_data" - } - ], "source": [ "import matplotlib.pyplot as plt\n", "\n", "x = [v['test_nll'] for v in report]\n", "y = [v['Next Training set size'] for v in report]\n", "plt.plot(y, x)" - ] + ], + "outputs": [] } ], "metadata": { @@ -313,4 +281,4 @@ }, "nbformat": 4, "nbformat_minor": 4 -} \ No newline at end of file +} diff --git a/notebooks/fairness/ActiveFairness.ipynb b/notebooks/fairness/ActiveFairness.ipynb index efa721ae..5c087cd4 100644 --- a/notebooks/fairness/ActiveFairness.ipynb +++ b/notebooks/fairness/ActiveFairness.ipynb @@ -60,16 +60,6 @@ "name": "#%%\n" } }, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "100%|██████████| 10000/10000 [02:28<00:00, 67.28it/s]\n", - "100%|██████████| 5000/5000 [01:13<00:00, 68.07it/s]\n" - ] - } - ], "source": [ "import numpy as np\n", "from math import pi\n", @@ -119,7 +109,8 @@ "train_set = make_dataset(p=0.9, seed=1000, num=10000)\n", "test_set = make_dataset(p=0.5, seed=2000, num=5000)\n", "dataset = {'train': train_set, 'test': test_set}" - ] + ], + "outputs": [] }, { "cell_type": "markdown", @@ -141,7 +132,6 @@ "name": "#%%\n" } }, - "outputs": [], "source": [ "from torchvision.transforms import transforms\n", "from active_fairness.dataset import SynbolDataset\n", @@ -185,7 +175,8 @@ " active_set = ActiveLearningDataset(ds, pool_specifics={'transform': test_transform})\n", " active_set.label_randomly(initial_pool)\n", " return active_set, test_set" - ] + ], + "outputs": [] }, { "cell_type": "code", @@ -195,7 +186,6 @@ "name": "#%%\n" } }, - "outputs": [], "source": [ "from torchvision import models\n", "from torch.hub import load_state_dict_from_url\n", @@ -214,7 +204,8 @@ "\n", "if use_cuda:\n", " model.cuda()\n" - ] + ], + "outputs": [] }, { "cell_type": "markdown", @@ -235,7 +226,6 @@ "name": "#%%\n" } }, - "outputs": [], "source": [ "from torch import nn\n", "\n", @@ -246,7 +236,8 @@ "\n", " def forward(self, input, target):\n", " return self.crit(input, target['target'])" - ] + ], + "outputs": [] }, { "cell_type": "markdown", @@ -272,7 +263,6 @@ "name": "#%%\n" } }, - "outputs": [], "source": [ "from copy import deepcopy\n", "from tqdm import tqdm\n", @@ -354,7 +344,8 @@ "\n", " if len(active_set) > 2000:\n", " break" - ] + ], + "outputs": [] }, { "cell_type": "markdown", @@ -378,20 +369,6 @@ "name": "#%%\n" } }, - "outputs": [ - { - "data": { - "image/png": "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J0ywvzjy57fY3ro13HknFCsWlGFE+9ehiARBWprFovbtrfrhggusJrphwxjZPXvKFKuRTkuDyZOhatVwj0hE5IiSkmDAANv0at06qFnTkugcsYdUUhLMnGkzOqtWHUic0y/JyQc//7zz7MuqVi1bdRmCHcKUTIvIfgkJ1kapf39Ytsz+iP/gA3jiCciTJ9yjywZr18KLL8KwYTbNPnZsti5CERHJTlu2WGeOnj0tib7uOgtf1auHe2RBtHWr1TX/8otd/vjDEmqwxLh4cftyuuYau06/n/ES4gXkSqZFcjjvrbSsXz/46iv7I//66+HNN6FOnSjtznGoffts79w337RfsE0b2/brlFPCPTIRkX/5+2/rzjFokDUbuvlma3tfs2YMNhravRu++eZA8jx/vh0/+WQrAm/aFK69Fq66ylqTROAHoGRaJIfassXqn/v1s62/Cxa0RhaNGsGll4Z7dNnohx+geXNbZHjXXdCjh7b9EpGI4z389BN0726NJ3LnhgcfhJYtoXz5cI8uSGbNggcegCVLbHOCq66yErxrr7VC8CiZ8FAyLZLDLFsGHTrYqcKkJItdgwZZI4sQlJaFzqpV8NxzNt1eqpSt6L7zznCPSkTkIMnJ1re/e3crDY6Lg9des8mNc84J9+iCJC3NJjZat7Ztcr//3qbfo7Q9lJJpkRxi/XpbUNi/v814PP44NGkCFSqEe2TZyHvYsQN69YK33rJj7dvDCy+Ado4TkQiybx98/LGFqtWrbflG377w8MNRMyGbNevX257m48fDPffY6va4uHCP6oQomRaJcdu32xbfPXpY8H7qKXj9dShaNNwjO07ew/TpVq6RkHDwZePGA7cTE+35depAt27WikREJEIkJ8PQofZ3fnw8/Oc/ttD7jjtywEYr338Pjz5qkx59+tg+5xFYA328lEyLxKi9e62pf8eOVh/9wAO2/u6ii8I9suO0bRt88olN2fz114HjefLYYpSzz7bThJdeateFC8OVV9oqShGRCJGaaptdtWtn5XZVqlgSfdttMZFPHl1SErz8sq2qLF8eJk2yLXRjhJJpkRiTkmI10G3bWie422+33qSVKoV7ZMchfRa6Xz8r7t671755BgywnlBFikCBAjngG0hEol1aGnzxhcXkxYuhYkUYNcrWQ+eIELZwoc3mzJ1ri8E7d465sjsl0yIxIjnZ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\n", - "text/plain": [ - "
" - ] - }, - "metadata": { - "needs_background": "light" - }, - "output_type": "display_data" - } - ], "source": [ "import matplotlib.pyplot as plt\n", "%matplotlib inline\n", @@ -414,7 +391,8 @@ " ax.legend()\n", "\n", "fig.show()" - ] + ], + "outputs": [] }, { "cell_type": "markdown", @@ -435,20 +413,6 @@ "name": "#%%\n" } }, - "outputs": [ - { - "data": { - "image/png": "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\n", - "text/plain": [ - "
" - ] - }, - "metadata": { - "needs_background": "light" - }, - "output_type": "display_data" - } - ], "source": [ "x = logs['bald']['epoch']\n", "fig, (ax0, ax1) = plt.subplots(nrows=1, ncols=2, sharex=True,\n", @@ -462,7 +426,8 @@ "ax1.plot(x, logs['random']['test_loss'], color='b', label='Uniform')\n", "ax1.legend()\n", "fig.show()" - ] + ], + "outputs": [] } ], "metadata": { @@ -487,4 +452,4 @@ }, "nbformat": 4, "nbformat_minor": 4 -} \ No newline at end of file +} diff --git a/notebooks/fundamentals/posteriors.ipynb b/notebooks/fundamentals/posteriors.ipynb index d59fe1a1..82a45bb6 100644 --- a/notebooks/fundamentals/posteriors.ipynb +++ b/notebooks/fundamentals/posteriors.ipynb @@ -67,7 +67,6 @@ "name": "#%%\n" } }, - "outputs": [], "source": [ "import torch\n", "\n", @@ -92,7 +91,8 @@ " baal.bayesian.dropout.Dropout(p=0.5),\n", " torch.nn.Linear(4, 2),\n", ")" - ] + ], + "outputs": [] }, { "cell_type": "markdown", @@ -113,16 +113,6 @@ "name": "#%%\n" } }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "True\n", - "False\n" - ] - } - ], "source": [ "dummy_input = torch.randn(8, 10)\n", "\n", @@ -131,7 +121,8 @@ "\n", "mc_dropout_model.eval()\n", "print(bool((mc_dropout_model(dummy_input) == mc_dropout_model(dummy_input)).all()))\n" - ] + ], + "outputs": [] }, { "cell_type": "markdown", @@ -154,7 +145,6 @@ "name": "#%%\n" } }, - "outputs": [], "source": [ "from baal.modelwrapper import ModelWrapper\n", "\n", @@ -165,7 +155,8 @@ "\n", "with torch.no_grad():\n", " predictions = wrapped_model.predict_on_batch(dummy_input, iterations=10000)" - ] + ], + "outputs": [] }, { "cell_type": "markdown", @@ -186,21 +177,10 @@ "name": "#%%\n" } }, - "outputs": [ - { - "data": { - "text/plain": [ - "torch.Size([8, 2, 10000])" - ] - }, - "execution_count": 4, - "metadata": {}, - "output_type": "execute_result" - } - ], "source": [ "predictions.shape" - ] + ], + "outputs": [] }, { "cell_type": "markdown", @@ -223,20 +203,6 @@ "name": "#%%\n" } }, - "outputs": [ - { - "data": { - "image/png": "iVBORw0KGgoAAAANSUhEUgAAAX0AAAD4CAYAAAAAczaOAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAALEgAACxIB0t1+/AAAADh0RVh0U29mdHdhcmUAbWF0cGxvdGxpYiB2ZXJzaW9uMy4xLjEsIGh0dHA6Ly9tYXRwbG90bGliLm9yZy8QZhcZAAATWklEQVR4nO3df6zd9X3f8ecr5keipS2m3DHX9mLWepvI1JrsDojarllowBBpplKWgdbgRkxuVZBarZvmNH/QkiGRbSlT1BTNHV5M1ZYy2gwrcUsdBymLNH6Y1HEwlHJLyLDr4NuY0GRsdND3/rgfqyfkXp9zf5177c/zIR2d7/f9/Xy/5/PxPfd1vvdzvuc4VYUkqQ9vWukOSJLGx9CXpI4Y+pLUEUNfkjpi6EtSR85Z6Q6czkUXXVSbNm1a6W5I0hnliSee+POqmpht26oO/U2bNnHw4MGV7oYknVGSfHWubU7vSFJHDH1J6oihL0kdMfQlqSOGviR1ZGjoJ3lzkseSfCnJkSS/3OqfTPKVJIfabUurJ8nHk0wlOZzkHQPH2p7k2XbbvnzDkiTNZpRLNl8F3l1V30pyLvCFJL/ftv2bqnrgDe2vBTa32xXA3cAVSS4EbgMmgQKeSLK3ql5aioFIkoYbeqZfM77VVs9tt9N9H/M24N623yPABUnWAdcA+6vqZAv6/cDWxXVfkjQfI83pJ1mT5BBwgpngfrRtuqNN4dyV5PxWWw+8MLD70Vabq/7Gx9qR5GCSg9PT0/McjiTpdEb6RG5VvQ5sSXIB8Kkk/wD4EPA14DxgF/BvgdsX26Gq2tWOx+TkpP/Di6Sz2qadn5m1/vyd712Wx5vX1TtV9Q3gYWBrVR1vUzivAv8VuLw1OwZsHNhtQ6vNVZckjckoV+9MtDN8krwFeA/wx22eniQBrgeebLvsBW5qV/FcCbxcVceBh4Crk6xNsha4utUkSWMyyvTOOmBPkjXMvEjcX1WfTvK5JBNAgEPAz7T2+4DrgCngFeCDAFV1MslHgMdbu9ur6uTSDUWSNMzQ0K+qw8Bls9TfPUf7Am6ZY9tuYPc8+yhJWiJ+IleSOmLoS1JHDH1J6oihL0kdMfQlqSOGviR1xNCXpI4Y+pLUEUNfkjpi6EtSRwx9SeqIoS9JHTH0Jakjhr4kdcTQl6SOGPqS1BFDX5I6YuhLUkcMfUnqiKEvSR0x9CWpI0NDP8mbkzyW5EtJjiT55Va/JMmjSaaS/E6S81r9/LY+1bZvGjjWh1r9mSTXLNegJEmzG+VM/1Xg3VX1Q8AWYGuSK4GPAndV1Q8ALwE3t/Y3Ay+1+l2tHUkuBW4A3g5sBX4tyZqlHIwk6fSGhn7N+FZbPbfdCng38ECr7wGub8vb2jpt+1VJ0ur3VdWrVfUVYAq4fElGIUkayUhz+knWJDkEnAD2A38KfKOqXmtNjgLr2/J64AWAtv1l4HsH67PsM/hYO5IcTHJwenp6/iOSJM1ppNCvqteraguwgZmz87+/XB2qql1VNVlVkxMTE8v1MJLUpXldvVNV3wAeBt4JXJDknLZpA3CsLR8DNgK07d8DfH2wPss+kqQxGOXqnYkkF7TltwDvAZ5mJvzf15ptBx5sy3vbOm3756qqWv2GdnXPJcBm4LGlGogkabhzhjdhHbCnXWnzJuD+qvp0kqeA+5L8O+CPgHta+3uA30gyBZxk5oodqupIkvuBp4DXgFuq6vWlHY4k6XSGhn5VHQYum6X+HLNcfVNV/xf4Z3Mc6w7gjvl3U5K0FPxEriR1xNCXpI4Y+pLUEUNfkjpi6EtSRwx9SeqIoS9JHTH0Jakjhr4kdcTQl6SOGPqS1BFDX5I6YuhLUkcMfUnqiKEvSR0x9CWpI4a+JHXE0Jekjhj6ktQRQ1+SOjI09JNsTPJwkqeSHEnyc63+S0mOJTnUbtcN7POhJFNJnklyzUB9a6tNJdm5PEOSJM3lnBHavAb8QlV9Mcl3AU8k2d+23VVV/3GwcZJLgRuAtwPfB3w2yd9tmz8BvAc4CjyeZG9VPbUUA5EkDTc09KvqOHC8LX8zydPA+tPssg24r6peBb6SZAq4vG2bqqrnAJLc19oa+pI0JvOa00+yCbgMeLSVbk1yOMnuJGtbbT3wwsBuR1ttrrokaUxGDv0kbwV+F/j5qvoL4G7g+4EtzPwl8LGl6FCSHUkOJjk4PT29FIeUJDUjhX6Sc5kJ/N+sqt8DqKoXq+r1qvor4Nf56ymcY8DGgd03tNpc9W9TVbuqarKqJicmJuY7HknSaYxy9U6Ae4Cnq+pXBurrBpr9BPBkW94L3JDk/CSXAJuBx4DHgc1JLklyHjNv9u5dmmFIkkYxytU7Pwx8APhykkOt9ovAjUm2AAU8D/w0QFUdSXI/M2/QvgbcUlWvAyS5FXgIWAPsrqojSzgWSdIQo1y98wUgs2zad5p97gDumKW+73T7SZKWl5/IlaSOGPqS1BFDX5I6YuhLUkcMfUnqiKEvSR0x9CWpI4a+JHXE0Jekjhj6ktQRQ1+SOmLoS1JHDH1J6oihL0kdMfQlqSOGviR1xNCXpI4Y+pLUEUNfkjpi6EtSRwx9SerI0NBPsjHJw0meSnIkyc+1+oVJ9id5tt2vbfUk+XiSqSSHk7xj4FjbW/tnk2xfvmFJkmYzypn+a8AvVNWlwJXALUkuBXYCB6pqM3CgrQNcC2xutx3A3TDzIgHcBlwBXA7cduqFQpI0HkNDv6qOV9UX2/I3gaeB9cA2YE9rtge4vi1vA+6tGY8AFyRZB1wD7K+qk1X1ErAf2Lqko5Eknda85vSTbAIuAx4FLq6q423T14CL2/J64IWB3Y622lz1Nz7GjiQHkxycnp6eT/ckSUOMHPpJ3gr8LvDzVfUXg9uqqoBaig5V1a6qmqyqyYmJiaU4pCSpGSn0k5zLTOD/ZlX9Xiu/2KZtaPcnWv0YsHFg9w2tNlddkjQmo1y9E+Ae4Omq+pWBTXuBU1fgbAceHKjf1K7iuRJ4uU0DPQRcnWRtewP36laTJI3JOSO0+WHgA8CXkxxqtV8E7gTuT3Iz8FXg/W3bPuA6YAp4BfggQFWdTPIR4PHW7vaqOrkko5AkjWRo6FfVF4DMsfmqWdoXcMscx9oN7J5PByVJS8dP5EpSRwx9SeqIoS9JHTH0Jakjhr4kdcTQl6SOGPqS1BFDX5I6YuhLUkcMfUnqiKEvSR0x9CWpI4a+JHXE0Jekjhj6ktQRQ1+SOmLoS1JHDH1J6oihL0kdMfQlqSOGviR1ZGjoJ9md5ESSJwdqv5TkWJJD7XbdwLYPJZlK8kySawbqW1ttKsnOpR+KJGmYUc70PwlsnaV+V1Vtabd9AEkuBW4A3t72+bUka5KsAT4BXAtcCtzY2kqSxuicYQ2q6vNJNo14vG3AfVX1KvCVJFPA5W3bVFU9B5Dkvtb2qXn3WJK0YIuZ0781yeE2/bO21dYDLwy0Odpqc9W/Q5IdSQ4mOTg9Pb2I7kmS3mihoX838P3AFuA48LGl6lBV7aqqyaqanJiYWKrDSpIYYXpnNlX14qnlJL8OfLqtHgM2DjTd0Gqcpi5JGpMFneknWTew+hPAqSt79gI3JDk/ySXAZuAx4HFgc5JLkpzHzJu9exfebUnSQgw900/y28C7gIuSHAVuA96VZAtQwPPATwNU1ZEk9zPzBu1rwC1V9Xo7zq3AQ8AaYHdVHVny0UiSTmuUq3dunKV8z2na3wHcMUt9H7BvXr2TJC0pP5ErSR0x9CWpI4a+JHXE0Jekjhj6ktQRQ1+SOmLoS1JHDH1J6oihL0kdMfQlqSOGviR1xNCXpI4Y+pLUEUNfkjpi6EtSRwx9SeqIoS9JHTH0Jakjhr4kdcTQl6SODA39JLuTnEjy5EDtwiT7kzzb7te2epJ8PMlUksNJ3jGwz/bW/tkk25dnOJKk0xnlTP+TwNY31HYCB6pqM3CgrQNcC2xutx3A3TDzIgHcBlwBXA7cduqFQpI0PkNDv6o+D5x8Q3kbsKct7wGuH6jfWzMeAS5Isg64BthfVSer6iVgP9/5QiJJWmYLndO/uKqOt+WvARe35fXACwPtjrbaXPXvkGRHkoNJDk5PTy+we5Kk2Sz6jdyqKqCWoC+njrerqiaranJiYmKpDitJYuGh/2KbtqHdn2j1Y8DGgXYbWm2uuiRpjBYa+nuBU1fgbAceHKjf1K7iuRJ4uU0DPQRcnWRtewP36laTJI3ROcMaJPlt4F3ARUmOMnMVzp3A/UluBr4KvL813wdcB0wBrwAfBKiqk0k+Ajze2t1eVW98c1iStMyGhn5V3TjHpqtmaVvALXMcZzewe169kyQtKT+RK0kdMfQlqSOGviR1ZOicviSdLTbt/Myc256/871j7MnK8Uxfkjpi6EtSRwx9SeqIoS9JHTH0Jakjhr4kdcTQl6SOGPqS1BFDX5I6YuhLUkcMfUnqiKEvSR0x9CWpI4a+JHXE0Jekjhj6ktQRQ1+SOrKo0E/yfJIvJzmU5GCrXZhkf5Jn2/3aVk+SjyeZSnI4yTuWYgCSpNEtxZn+P6mqLVU12dZ3AgeqajNwoK0DXAtsbrcdwN1L8NiSpHlYjumdbcCetrwHuH6gfm/NeAS4IMm6ZXh8SdIcFhv6BfxhkieS7Gi1i6vqeFv+GnBxW14PvDCw79FW+zZJdiQ5mOTg9PT0IrsnSRp0ziL3/5GqOpbkbwL7k/zx4MaqqiQ1nwNW1S5gF8Dk5OS89pU0P5t2fmbW+vN3vnfMPdG4LCr0q+pYuz+R5FPA5cCLSdZV1fE2fXOiNT8GbBzYfUOrSVpGcwW7+rTg6Z0kfyPJd51aBq4GngT2Attbs+3Ag215L3BTu4rnSuDlgWkgSdIYLOZM/2LgU0lOHee3quoPkjwO3J/kZuCrwPtb+33AdcAU8ArwwUU8tiRpARYc+lX1HPBDs9S/Dlw1S72AWxb6eJKkxfMTuZLUkcVevSNJq45vXs/NM31J6oihL0kdMfQlqSPO6UvSaZxtn1o29HVGO90bdvP9pTzbfrml2Rj6OmsZ4tJ3ck5fkjpi6EtSR5zekXTG8kNY8+eZviR1xDN9SRqD1fJXiaEvrbCz+Sqjs3lsZ6qzOvR7fMIt95h7/DfVX/Pnf+Y7q0N/qZwNT/TVNob59me1/Gm8Gqy2n6W+3Wp/rhr6q5i/3H1byfBY7cGlhfPqHUnqiGf6krQAZ+pfQ4a+pEVbqgB0SnP5GfqSVr0z9ax6NRr7nH6SrUmeSTKVZOe4H1+SejbW0E+yBvgEcC1wKXBjkkvH2QdJ6tm4z/QvB6aq6rmq+kvgPmDbmPsgSd1KVY3vwZL3AVur6l+29Q8AV1TVrQNtdgA72urfA56Z43AXAX++jN0dJ8eyOjmW1cmxDPe2qpqYbcOqeyO3qnYBu4a1S3KwqibH0KVl51hWJ8eyOjmWxRn39M4xYOPA+oZWkySNwbhD/3Fgc5JLkpwH3ADsHXMfJKlbY53eqarXktwKPASsAXZX1ZEFHm7oFNAZxLGsTo5ldXIsizDWN3IlSSvLL1yTpI4Y+pLUkTMm9JNcmGR/kmfb/drTtP3uJEeT/Oo4+ziqUcaS5G1JvpjkUJIjSX5mJfo6zIhj2ZLkf7ZxHE7yz1eir8OM+hxL8gdJvpHk0+Pu4zDDvuYkyflJfqdtfzTJpvH3cjQjjOUft9+R19pngFatEcbyr5I81X4/DiR523L15YwJfWAncKCqNgMH2vpcPgJ8fiy9WphRxnIceGdVbQGuAHYm+b4x9nFUo4zlFeCmqno7sBX4T0kuGGMfRzXqc+w/AB8YW69GNOLXnNwMvFRVPwDcBXx0vL0czYhj+V/ATwG/Nd7ezc+IY/kjYLKqfhB4APj3y9WfMyn0twF72vIe4PrZGiX5h8DFwB+OqV8LMXQsVfWXVfVqWz2f1fuzGmUsf1JVz7blPwNOALN+WnCFjfQcq6oDwDfH1al5GOVrTgbH+ABwVZKMsY+jGjqWqnq+qg4Df7USHZyHUcbycFW90lYfYeYzTMtitQbJbC6uquNt+WvMBPu3SfIm4GPAvx5nxxZg6FgAkmxMchh4AfhoC8zVZqSxnJLkcuA84E+Xu2MLMK+xrELrmXmunHK01WZtU1WvAS8D3zuW3s3PKGM5U8x3LDcDv79cnVlVX8OQ5LPA35pl04cHV6qqksx2renPAvuq6uhKn7wswVioqheAH2zTOv89yQNV9eLS9/b0lmIs7TjrgN8AtlfVipydLdVYpOWQ5CeBSeDHlusxVlXoV9WPz7UtyYtJ1lXV8RYeJ2Zp9k7gR5P8LPBW4Lwk36qqsX9v/xKMZfBYf5bkSeBHmfmTfKyWYixJvhv4DPDhqnpkmbo61FL+XFahUb7m5FSbo0nOAb4H+Pp4ujcvZ9NXtow0liQ/zszJx48NTO0uuTNpemcvsL0tbwcefGODqvoXVfW3q2oTM1M8965E4I9g6FiSbEjylra8FvgR5v7G0ZU0yljOAz7FzM9j7C9a8zB0LKvcKF9zMjjG9wGfq9X5Cc2z6Stbho4lyWXAfwb+aVUt78lGVZ0RN2bmHQ8AzwKfBS5s9Ungv8zS/qeAX13pfi90LMB7gMPAl9r9jpXu9yLG8pPA/wMODdy2rHTfF/ocA/4HMA38H2bmZ69Z6b4P9O064E+Yec/kw612OzNhAvBm4L8BU8BjwN9Z6T4vYiz/qP37/29m/lo5stJ9XsRYPgu8OPD7sXe5+uLXMEhSR86k6R1J0iIZ+pLUEUNfkjpi6EtSRwx9SeqIoS9JHTH0Jakj/x/cPZt0LzuHhwAAAABJRU5ErkJggg==\n", - "text/plain": [ - "
" - ] - }, - "metadata": { - "needs_background": "light" - }, - "output_type": "display_data" - } - ], "source": [ "import matplotlib.pyplot as plt\n", "% matplotlib inline\n", @@ -244,7 +210,8 @@ "fig, ax = plt.subplots()\n", "ax.hist(predictions[0, 0, :].numpy(), bins=50);\n", "plt.show()" - ] + ], + "outputs": [] }, { "cell_type": "markdown", @@ -282,7 +249,6 @@ "name": "#%%\n" } }, - "outputs": [], "source": [ "import torch\n", "\n", @@ -302,7 +268,8 @@ " x = self.linear(x)\n", " x = self.sigmoid(x)\n", " return x" - ] + ], + "outputs": [] }, { "cell_type": "code", @@ -312,7 +279,6 @@ "name": "#%%\n" } }, - "outputs": [], "source": [ "import numpy as np\n", "from baal.bayesian import MCDropoutConnectModule\n", @@ -324,7 +290,8 @@ "wrapped_model = ModelWrapper(model, torch.nn.CrossEntropyLoss(), replicate_in_memory=False)\n", "with torch.no_grad():\n", " predictions = wrapped_model.predict_on_batch(dummy_input.unsqueeze(0), iterations=10000)" - ] + ], + "outputs": [] }, { "cell_type": "code", @@ -334,21 +301,10 @@ "name": "#%%\n" } }, - "outputs": [ - { - "data": { - "text/plain": [ - "torch.Size([1, 1, 10000])" - ] - }, - "execution_count": 8, - "metadata": {}, - "output_type": "execute_result" - } - ], "source": [ "predictions.shape" - ] + ], + "outputs": [] }, { "cell_type": "markdown", @@ -369,20 +325,6 @@ "name": "#%%\n" } }, - "outputs": [ - { - "data": { - "image/png": "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\n", - "text/plain": [ - "
" - ] - }, - "metadata": { - "needs_background": "light" - }, - "output_type": "display_data" - } - ], "source": [ "import matplotlib.pyplot as plt\n", "% matplotlib inline\n", @@ -390,7 +332,8 @@ "fig, ax = plt.subplots()\n", "ax.hist(predictions[0, 0, :].numpy(), bins=50);\n", "plt.show()" - ] + ], + "outputs": [] }, { "cell_type": "markdown", @@ -435,4 +378,4 @@ }, "nbformat": 4, "nbformat_minor": 2 -} \ No newline at end of file +} diff --git a/notebooks/mccaching_layer.ipynb b/notebooks/mccaching_layer.ipynb index ff9708c4..26adb04b 100644 --- a/notebooks/mccaching_layer.ipynb +++ b/notebooks/mccaching_layer.ipynb @@ -38,17 +38,6 @@ { "cell_type": "code", "execution_count": 8, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Files already downloaded and verified\n", - "[12777-MainThread] [baal.modelwrapper:predict_on_dataset_generator:239] \u001B[2m2023-07-13T21:09:33.828796Z\u001B[0m [\u001B[32m\u001B[1minfo \u001B[0m] \u001B[1mStart Predict \u001B[0m \u001B[36mdataset\u001B[0m=\u001B[35m10000\u001B[0m\n", - "100%|██████████| 313/313 [02:49<00:00, 1.85it/s]\n" - ] - } - ], "source": [ "from torchvision.datasets import CIFAR10\n", "from torchvision.models import vgg16\n", @@ -76,7 +65,8 @@ "end_time": "2023-07-13T21:12:23.378811603Z", "start_time": "2023-07-13T21:09:29.068365127Z" } - } + }, + "outputs": [] }, { "cell_type": "markdown", @@ -94,16 +84,6 @@ { "cell_type": "code", "execution_count": 9, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "[12777-MainThread] [baal.modelwrapper:predict_on_dataset_generator:239] \u001B[2m2023-07-13T21:12:23.384108Z\u001B[0m [\u001B[32m\u001B[1minfo \u001B[0m] \u001B[1mStart Predict \u001B[0m \u001B[36mdataset\u001B[0m=\u001B[35m10000\u001B[0m\n", - "100%|██████████| 313/313 [00:47<00:00, 6.60it/s]\n" - ] - } - ], "source": [ "# Takes ~50 seconds!.\n", "with MCCachingModule(vgg) as model:\n", @@ -117,7 +97,8 @@ "end_time": "2023-07-13T21:13:11.076629413Z", "start_time": "2023-07-13T21:12:23.387507076Z" } - } + }, + "outputs": [] }, { "cell_type": "markdown", diff --git a/poetry.lock b/poetry.lock index 1c69a8f7..4dd3ebfc 100644 --- a/poetry.lock +++ b/poetry.lock @@ -675,21 +675,21 @@ files = [ [[package]] name = "datasets" -version = "2.18.0" +version = "2.19.1" description = "HuggingFace community-driven open-source library of datasets" optional = true python-versions = ">=3.8.0" files = [ - {file = "datasets-2.18.0-py3-none-any.whl", hash = "sha256:f1bbf0e2896917a914de01cbd37075b14deea3837af87ad0d9f697388ccaeb50"}, - {file = "datasets-2.18.0.tar.gz", hash = "sha256:cdf8b8c6abf7316377ba4f49f9589a4c74556d6b481afd0abd2284f3d69185cb"}, + {file = "datasets-2.19.1-py3-none-any.whl", hash = "sha256:f7a78d15896f45004ccac1c298f3c7121f92f91f6f2bfbd4e4f210f827e6e411"}, + {file = "datasets-2.19.1.tar.gz", hash = "sha256:0df9ef6c5e9138cdb996a07385220109ff203c204245578b69cca905eb151d3a"}, ] [package.dependencies] aiohttp = "*" dill = ">=0.3.0,<0.3.9" filelock = "*" -fsspec = {version = ">=2023.1.0,<=2024.2.0", extras = ["http"]} -huggingface-hub = ">=0.19.4" +fsspec = {version = ">=2023.1.0,<=2024.3.1", extras = ["http"]} +huggingface-hub = ">=0.21.2" multiprocess = "*" numpy = ">=1.17" packaging = "*" @@ -705,15 +705,15 @@ xxhash = "*" apache-beam = ["apache-beam (>=2.26.0)"] audio = ["librosa", "soundfile (>=0.12.1)"] benchmarks = ["tensorflow (==2.12.0)", "torch (==2.0.1)", "transformers (==4.30.1)"] -dev = ["Pillow (>=6.2.1)", "absl-py", "apache-beam (>=2.26.0)", "elasticsearch (<8.0.0)", "faiss-cpu (>=1.6.4)", "jax (>=0.3.14)", "jaxlib (>=0.3.14)", "joblib (<1.3.0)", "joblibspark", "librosa", "lz4", "py7zr", "pyspark (>=3.4)", "pytest", "pytest-datadir", "pytest-xdist", "rarfile (>=4.0)", "ruff (>=0.3.0)", "s3fs", "s3fs (>=2021.11.1)", "soundfile (>=0.12.1)", "sqlalchemy", "tensorflow (>=2.2.0,!=2.6.0,!=2.6.1)", "tensorflow (>=2.3,!=2.6.0,!=2.6.1)", "tensorflow-macos", "tiktoken", "torch", "torch (>=2.0.0)", "transformers", "typing-extensions (>=4.6.1)", "zstandard"] -docs = ["s3fs", "tensorflow (>=2.2.0,!=2.6.0,!=2.6.1)", "tensorflow-macos", "torch", "transformers"] +dev = ["Pillow (>=6.2.1)", "absl-py", "apache-beam (>=2.26.0)", "elasticsearch (<8.0.0)", "faiss-cpu (>=1.6.4)", "jax (>=0.3.14)", "jaxlib (>=0.3.14)", "joblib (<1.3.0)", "joblibspark", "librosa", "lz4", "polars[timezone] (>=0.20.0)", "protobuf (<4.0.0)", "py7zr", "pyspark (>=3.4)", "pytest", "pytest-datadir", "pytest-xdist", "rarfile (>=4.0)", "ruff (>=0.3.0)", "s3fs", "s3fs (>=2021.11.1)", "soundfile (>=0.12.1)", "sqlalchemy", "tensorflow (>=2.6.0)", "tiktoken", "torch", "torch (>=2.0.0)", "transformers", "typing-extensions (>=4.6.1)", "zstandard"] +docs = ["s3fs", "tensorflow (>=2.6.0)", "torch", "transformers"] jax = ["jax (>=0.3.14)", "jaxlib (>=0.3.14)"] metrics-tests = ["Werkzeug (>=1.0.1)", "accelerate", "bert-score (>=0.3.6)", "jiwer", "langdetect", "mauve-text", "nltk", "requests-file (>=1.5.1)", "rouge-score", "sacrebleu", "sacremoses", "scikit-learn", "scipy", "sentencepiece", "seqeval", "six (>=1.15.0,<1.16.0)", "spacy (>=3.0.0)", "texttable (>=1.6.3)", "tldextract", "tldextract (>=3.1.0)", "toml (>=0.10.1)", "typer (<0.5.0)"] quality = ["ruff (>=0.3.0)"] s3 = ["s3fs"] -tensorflow = ["tensorflow (>=2.2.0,!=2.6.0,!=2.6.1)", "tensorflow-macos"] -tensorflow-gpu = ["tensorflow-gpu (>=2.2.0,!=2.6.0,!=2.6.1)"] -tests = ["Pillow (>=6.2.1)", "absl-py", "apache-beam (>=2.26.0)", "elasticsearch (<8.0.0)", "faiss-cpu (>=1.6.4)", "jax (>=0.3.14)", "jaxlib (>=0.3.14)", "joblib (<1.3.0)", "joblibspark", "librosa", "lz4", "py7zr", "pyspark (>=3.4)", "pytest", "pytest-datadir", "pytest-xdist", "rarfile (>=4.0)", "s3fs (>=2021.11.1)", "soundfile (>=0.12.1)", "sqlalchemy", "tensorflow (>=2.3,!=2.6.0,!=2.6.1)", "tensorflow-macos", "tiktoken", "torch (>=2.0.0)", "transformers", "typing-extensions (>=4.6.1)", "zstandard"] +tensorflow = ["tensorflow (>=2.6.0)"] +tensorflow-gpu = ["tensorflow (>=2.6.0)"] +tests = ["Pillow (>=6.2.1)", "absl-py", "apache-beam (>=2.26.0)", "elasticsearch (<8.0.0)", "faiss-cpu (>=1.6.4)", "jax (>=0.3.14)", "jaxlib (>=0.3.14)", "joblib (<1.3.0)", "joblibspark", "librosa", "lz4", "polars[timezone] (>=0.20.0)", "protobuf (<4.0.0)", "py7zr", "pyspark (>=3.4)", "pytest", "pytest-datadir", "pytest-xdist", "rarfile (>=4.0)", "s3fs (>=2021.11.1)", "soundfile (>=0.12.1)", "sqlalchemy", "tensorflow (>=2.6.0)", "tiktoken", "torch (>=2.0.0)", "transformers", "typing-extensions (>=4.6.1)", "zstandard"] torch = ["torch"] vision = ["Pillow (>=6.2.1)"] @@ -2103,44 +2103,49 @@ dill = ">=0.3.6" [[package]] name = "mypy" -version = "0.910" +version = "1.0.1" description = "Optional static typing for Python" optional = false -python-versions = ">=3.5" +python-versions = ">=3.7" files = [ - {file = "mypy-0.910-cp35-cp35m-macosx_10_9_x86_64.whl", hash = "sha256:a155d80ea6cee511a3694b108c4494a39f42de11ee4e61e72bc424c490e46457"}, - {file = "mypy-0.910-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:b94e4b785e304a04ea0828759172a15add27088520dc7e49ceade7834275bedb"}, - {file = "mypy-0.910-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:088cd9c7904b4ad80bec811053272986611b84221835e079be5bcad029e79dd9"}, - {file = "mypy-0.910-cp35-cp35m-win_amd64.whl", hash = "sha256:adaeee09bfde366d2c13fe6093a7df5df83c9a2ba98638c7d76b010694db760e"}, - {file = "mypy-0.910-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:ecd2c3fe726758037234c93df7e98deb257fd15c24c9180dacf1ef829da5f921"}, - {file = "mypy-0.910-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:d9dd839eb0dc1bbe866a288ba3c1afc33a202015d2ad83b31e875b5905a079b6"}, - {file = "mypy-0.910-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:3e382b29f8e0ccf19a2df2b29a167591245df90c0b5a2542249873b5c1d78212"}, - {file = "mypy-0.910-cp36-cp36m-win_amd64.whl", hash = "sha256:53fd2eb27a8ee2892614370896956af2ff61254c275aaee4c230ae771cadd885"}, - {file = "mypy-0.910-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b6fb13123aeef4a3abbcfd7e71773ff3ff1526a7d3dc538f3929a49b42be03f0"}, - {file = "mypy-0.910-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:e4dab234478e3bd3ce83bac4193b2ecd9cf94e720ddd95ce69840273bf44f6de"}, - {file = "mypy-0.910-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:7df1ead20c81371ccd6091fa3e2878559b5c4d4caadaf1a484cf88d93ca06703"}, - {file = "mypy-0.910-cp37-cp37m-win_amd64.whl", hash = "sha256:0aadfb2d3935988ec3815952e44058a3100499f5be5b28c34ac9d79f002a4a9a"}, - {file = "mypy-0.910-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:ec4e0cd079db280b6bdabdc807047ff3e199f334050db5cbb91ba3e959a67504"}, - {file = "mypy-0.910-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:119bed3832d961f3a880787bf621634ba042cb8dc850a7429f643508eeac97b9"}, - {file = "mypy-0.910-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:866c41f28cee548475f146aa4d39a51cf3b6a84246969f3759cb3e9c742fc072"}, - {file = "mypy-0.910-cp38-cp38-win_amd64.whl", hash = "sha256:ceb6e0a6e27fb364fb3853389607cf7eb3a126ad335790fa1e14ed02fba50811"}, - {file = "mypy-0.910-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:1a85e280d4d217150ce8cb1a6dddffd14e753a4e0c3cf90baabb32cefa41b59e"}, - {file = "mypy-0.910-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:42c266ced41b65ed40a282c575705325fa7991af370036d3f134518336636f5b"}, - {file = "mypy-0.910-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:3c4b8ca36877fc75339253721f69603a9c7fdb5d4d5a95a1a1b899d8b86a4de2"}, - {file = "mypy-0.910-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:c0df2d30ed496a08de5daed2a9ea807d07c21ae0ab23acf541ab88c24b26ab97"}, - {file = "mypy-0.910-cp39-cp39-win_amd64.whl", hash = "sha256:c6c2602dffb74867498f86e6129fd52a2770c48b7cd3ece77ada4fa38f94eba8"}, - {file = "mypy-0.910-py3-none-any.whl", hash = "sha256:ef565033fa5a958e62796867b1df10c40263ea9ded87164d67572834e57a174d"}, - {file = "mypy-0.910.tar.gz", hash = "sha256:704098302473cb31a218f1775a873b376b30b4c18229421e9e9dc8916fd16150"}, + {file = "mypy-1.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:71a808334d3f41ef011faa5a5cd8153606df5fc0b56de5b2e89566c8093a0c9a"}, + {file = "mypy-1.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:920169f0184215eef19294fa86ea49ffd4635dedfdea2b57e45cb4ee85d5ccaf"}, + {file = "mypy-1.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:27a0f74a298769d9fdc8498fcb4f2beb86f0564bcdb1a37b58cbbe78e55cf8c0"}, + {file = "mypy-1.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:65b122a993d9c81ea0bfde7689b3365318a88bde952e4dfa1b3a8b4ac05d168b"}, + {file = "mypy-1.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:5deb252fd42a77add936b463033a59b8e48eb2eaec2976d76b6878d031933fe4"}, + {file = "mypy-1.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2013226d17f20468f34feddd6aae4635a55f79626549099354ce641bc7d40262"}, + {file = "mypy-1.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:48525aec92b47baed9b3380371ab8ab6e63a5aab317347dfe9e55e02aaad22e8"}, + {file = "mypy-1.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c96b8a0c019fe29040d520d9257d8c8f122a7343a8307bf8d6d4a43f5c5bfcc8"}, + {file = "mypy-1.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:448de661536d270ce04f2d7dddaa49b2fdba6e3bd8a83212164d4174ff43aa65"}, + {file = "mypy-1.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:d42a98e76070a365a1d1c220fcac8aa4ada12ae0db679cb4d910fabefc88b994"}, + {file = "mypy-1.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:e64f48c6176e243ad015e995de05af7f22bbe370dbb5b32bd6988438ec873919"}, + {file = "mypy-1.0.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5fdd63e4f50e3538617887e9aee91855368d9fc1dea30da743837b0df7373bc4"}, + {file = "mypy-1.0.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:dbeb24514c4acbc78d205f85dd0e800f34062efcc1f4a4857c57e4b4b8712bff"}, + {file = "mypy-1.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:a2948c40a7dd46c1c33765718936669dc1f628f134013b02ff5ac6c7ef6942bf"}, + {file = "mypy-1.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:5bc8d6bd3b274dd3846597855d96d38d947aedba18776aa998a8d46fabdaed76"}, + {file = "mypy-1.0.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:17455cda53eeee0a4adb6371a21dd3dbf465897de82843751cf822605d152c8c"}, + {file = "mypy-1.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e831662208055b006eef68392a768ff83596035ffd6d846786578ba1714ba8f6"}, + {file = "mypy-1.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:e60d0b09f62ae97a94605c3f73fd952395286cf3e3b9e7b97f60b01ddfbbda88"}, + {file = "mypy-1.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:0af4f0e20706aadf4e6f8f8dc5ab739089146b83fd53cb4a7e0e850ef3de0bb6"}, + {file = "mypy-1.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:24189f23dc66f83b839bd1cce2dfc356020dfc9a8bae03978477b15be61b062e"}, + {file = "mypy-1.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:93a85495fb13dc484251b4c1fd7a5ac370cd0d812bbfc3b39c1bafefe95275d5"}, + {file = "mypy-1.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5f546ac34093c6ce33f6278f7c88f0f147a4849386d3bf3ae193702f4fe31407"}, + {file = "mypy-1.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:c6c2ccb7af7154673c591189c3687b013122c5a891bb5651eca3db8e6c6c55bd"}, + {file = "mypy-1.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:15b5a824b58c7c822c51bc66308e759243c32631896743f030daf449fe3677f3"}, + {file = "mypy-1.0.1-py3-none-any.whl", hash = "sha256:eda5c8b9949ed411ff752b9a01adda31afe7eae1e53e946dbdf9db23865e66c4"}, + {file = "mypy-1.0.1.tar.gz", hash = "sha256:28cea5a6392bb43d266782983b5a4216c25544cd7d80be681a155ddcdafd152d"}, ] [package.dependencies] -mypy-extensions = ">=0.4.3,<0.5.0" -toml = "*" -typing-extensions = ">=3.7.4" +mypy-extensions = ">=0.4.3" +tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} +typing-extensions = ">=3.10" [package.extras] dmypy = ["psutil (>=4.0)"] -python2 = ["typed-ast (>=1.4.0,<1.5.0)"] +install-types = ["pip"] +python2 = ["typed-ast (>=1.4.0,<2)"] +reports = ["lxml"] [[package]] name = "mypy-extensions" @@ -4425,4 +4430,4 @@ vision = ["lightning-flash", "torchvision"] [metadata] lock-version = "2.0" python-versions = ">=3.8,<4" -content-hash = "22e8d75d7f7a07aacf0e135dba388aff00046daf4b15d13fd61414d2fbadb845" +content-hash = "1dedbeecd254bab411bca613c0ee249bface5ab66f682c036873a8d3ee721b53" diff --git a/pyproject.toml b/pyproject.toml index 09c46392..92231a81 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -33,7 +33,7 @@ lightning-flash = { version = ">=0.7.5", optional=true } # NLP transformers = {version = ">=4.10.2", optional=true} accelerate = {version = "^0.28.0", optional=true} -datasets = {version = ">=1.11.0", optional=true} +datasets = {version = ">=2.14.6", optional=true} [tool.poetry.dev-dependencies] pytest = "^6.2.5" @@ -43,7 +43,8 @@ hypothesis = "4.24.0" flake8 = "^3.9.2" pytest-mock = "^3.6.1" black = "^22.3.0" -mypy = "^0.910" +# Issue with mypy https://github.com/pydantic/pydantic/issues/5192 +mypy = "<=1.0.1" bandit = "^1.7.1" # Documentation diff --git a/tests/active/criterion_test.py b/tests/active/criterion_test.py index a586d0ba..96020efa 100644 --- a/tests/active/criterion_test.py +++ b/tests/active/criterion_test.py @@ -11,13 +11,13 @@ def test_labelling_budget(): ds = ActiveNumpyArray((np.random.randn(100, 3), np.random.randint(0, 3, 100))) ds.label_randomly(10) criterion = LabellingBudgetStoppingCriterion(ds, labelling_budget=50) - assert not criterion.should_stop([]) + assert not criterion.should_stop({}, []) ds.label_randomly(10) - assert not criterion.should_stop([]) + assert not criterion.should_stop({}, []) ds.label_randomly(40) - assert criterion.should_stop([]) + assert criterion.should_stop({}, []) def test_early_stopping(): diff --git a/tests/active/heuristics_gpu_test.py b/tests/active/heuristics_gpu_test.py index a6e4547d..957da012 100644 --- a/tests/active/heuristics_gpu_test.py +++ b/tests/active/heuristics_gpu_test.py @@ -13,6 +13,7 @@ from baal.active.heuristics.heuristics_gpu import BALDGPUWrapper from baal.bayesian import Dropout from baal.bayesian.dropout import Dropout2d +from baal.modelwrapper import TrainingArgs class Flatten(nn.Module): @@ -43,7 +44,7 @@ def classification_task(tmpdir): Dropout(), nn.Linear(128, 10) ) - model = ModelWrapper(model, nn.CrossEntropyLoss()) + model = ModelWrapper(model, TrainingArgs(criterion=nn.CrossEntropyLoss(), use_cuda=False, batch_size=4)) test = SimpleDataset() return model, test @@ -51,13 +52,13 @@ def classification_task(tmpdir): def test_bald_gpu(classification_task): torch.manual_seed(1337) model, test_set = classification_task - wrap = BALDGPUWrapper(model, criterion=None) + wrap = BALDGPUWrapper(model) - out = wrap.predict_on_dataset(test_set, 4, 10, False, 4) + out = wrap.predict_on_dataset(test_set, 10) assert out.shape[0] == len(test_set) bald = BALD() torch.manual_seed(1337) - out_bald = bald.get_uncertainties(model.predict_on_dataset(test_set, 4, 10, False, 4)) + out_bald = bald.get_uncertainties(model.predict_on_dataset(test_set, 10)) assert np.allclose(out, out_bald, rtol=1e-5, atol=1e-5) @@ -71,7 +72,7 @@ def segmentation_task(tmpdir): Dropout2d(), nn.ConvTranspose2d(64, 10, 3, 1) ) - model = ModelWrapper(model, nn.CrossEntropyLoss()) + model = ModelWrapper(model, TrainingArgs(criterion=nn.CrossEntropyLoss(), use_cuda=False, batch_size=4)) test = SimpleDataset() return model, test @@ -79,12 +80,12 @@ def segmentation_task(tmpdir): def test_bald_gpu_seg(segmentation_task): torch.manual_seed(1337) model, test_set = segmentation_task - wrap = BALDGPUWrapper(model, criterion=None, reduction='sum') + wrap = BALDGPUWrapper(model, reduction='sum') - out = wrap.predict_on_dataset(test_set, 4, 10, False, 4) + out = wrap.predict_on_dataset(test_set, 10) assert out.shape[0] == len(test_set) bald = BALD(reduction='sum') torch.manual_seed(1337) out_bald = bald.get_uncertainties_generator( - model.predict_on_dataset_generator(test_set, 4, 10, False, 4)) + model.predict_on_dataset_generator(test_set, 10)) assert np.allclose(out, out_bald, rtol=1e-5, atol=1e-5) diff --git a/tests/bayesian/test_caching.py b/tests/bayesian/test_caching.py index 9c74f8cb..1e4fcfb0 100644 --- a/tests/bayesian/test_caching.py +++ b/tests/bayesian/test_caching.py @@ -6,6 +6,7 @@ from baal import ModelWrapper from baal.bayesian.caching_utils import MCCachingModule +from baal.modelwrapper import TrainingArgs class LinearMocked(Linear): @@ -56,10 +57,10 @@ def test_caching(my_model): def test_caching_warnings(my_model): my_model = MCCachingModule(my_model) with warnings.catch_warnings(record=True) as tape: - ModelWrapper(my_model, criterion=None, replicate_in_memory=True) + ModelWrapper(my_model, args=TrainingArgs(replicate_in_memory=True)) assert len(tape) == 1 and "MCCachingModule" in str(tape[0].message) with warnings.catch_warnings(record=True) as tape: - ModelWrapper(my_model, criterion=None, replicate_in_memory=False) + ModelWrapper(my_model, args=TrainingArgs(replicate_in_memory=False)) assert len(tape) == 0 diff --git a/tests/calibration/calibration_test.py b/tests/calibration/calibration_test.py index 68a0bf2d..c2be4e37 100644 --- a/tests/calibration/calibration_test.py +++ b/tests/calibration/calibration_test.py @@ -8,7 +8,7 @@ from torch.utils.data import Dataset from baal.calibration import DirichletCalibrator -from baal.modelwrapper import ModelWrapper +from baal.modelwrapper import ModelWrapper, TrainingArgs def _get_first_module(seq): @@ -46,11 +46,12 @@ class CalibrationTest(unittest.TestCase): def setUp(self): self.model = DummyModel() self.criterion = nn.CrossEntropyLoss() - self.wrapper = ModelWrapper(self.model, self.criterion) - self.optim = torch.optim.SGD(self.wrapper.get_params(), 0.01) + self.optim = torch.optim.SGD(self.model.parameters(), 0.01) self.dataset = DummyDataset() + self.wrapper = ModelWrapper(self.model, TrainingArgs(optimizer=self.optim, criterion=self.criterion, batch_size=4, epoch=5, use_cuda=False)) self.calibrator = DirichletCalibrator(self.wrapper, 2, lr=0.001, reg_factor=0.001) + def test_calibrated_model(self): # Check that a layer was added. assert len(list(self.wrapper.model.modules())) < len( @@ -62,10 +63,7 @@ def test_calibration(self): before_calib_param = list( map(lambda x: x.clone(), self.calibrator.calibrated_model.parameters())) - self.calibrator.calibrate(self.dataset, self.dataset, - batch_size=10, epoch=5, - use_cuda=False, - double_fit=False, workers=0) + self.calibrator.calibrate(self.dataset, self.dataset, use_cuda=False, double_fit=False) after_calib_param_init = list( map(lambda x: x.clone(), _get_first_module(self.calibrator.wrapper.model).parameters())) after_calib_param = list( @@ -78,19 +76,16 @@ def test_calibration(self): for i, j in zip(before_calib_param, after_calib_param)]) def test_reg_l2_called(self): - self.calibrator.l2_reg = Mock(return_value=torch.Tensor([0])) - self.calibrator.calibrate(self.dataset, self.dataset, - batch_size=10, epoch=5, - use_cuda=False, - double_fit=False, workers=0) - self.calibrator.l2_reg.assert_called() + self.calibrator.wrapper.args.regularizer = Mock(return_value=torch.Tensor([0])) + self.calibrator.calibrate(self.dataset, self.dataset, use_cuda=False, double_fit=False) + self.calibrator.wrapper.args.regularizer .assert_called() def test_weight_assignment(self): params = list(self.wrapper.model.parameters()) - self.wrapper.train_on_dataset(self.dataset, self.optim, 32, 1, False) + self.wrapper.train_on_dataset(self.dataset) assert all([k is v for k, v in zip(params, self.optim.param_groups[0]['params'])]) - self.calibrator.calibrate(self.dataset, self.dataset, 32, 1, False, True) + self.calibrator.calibrate(self.dataset, self.dataset, False, True) assert all( [k is v for k, v in zip(self.wrapper.model.parameters(), self.optim.param_groups[0]['params'])]) @@ -98,7 +93,7 @@ def test_weight_assignment(self): # Check that we can train the original model before_params = list( map(lambda x: x.clone(), self.wrapper.model.parameters())) - self.wrapper.train_on_dataset(self.dataset, self.optim, 10, 2, False) + self.wrapper.train_on_dataset(self.dataset) after_params = list( map(lambda x: x.clone(), self.wrapper.model.parameters())) assert not all([np.allclose(i.detach(), j.detach()) diff --git a/tests/ensemble_test.py b/tests/ensemble_test.py index edc7af5a..f865d26d 100644 --- a/tests/ensemble_test.py +++ b/tests/ensemble_test.py @@ -5,6 +5,7 @@ from torch.utils.data import Dataset from baal.ensemble import EnsembleModelWrapper, ensemble_prediction +from baal.modelwrapper import TrainingArgs N_CLASS = 3 @@ -52,15 +53,17 @@ def weight_init(m): ) def test_prediction(use_cuda, n_ensemble): model = AModel() - ensemble = EnsembleModelWrapper(model, nn.CrossEntropyLoss()) optimizer = optim.SGD(model.parameters(), lr=0.001) + args = TrainingArgs(criterion=nn.CrossEntropyLoss(), optimizer=optimizer, batch_size=10, use_cuda=use_cuda, epoch=0) + ensemble = EnsembleModelWrapper(model, args ) + dataset = DummyDataset() if use_cuda: model.cuda() for i in range(n_ensemble): model.apply(weight_init) - ensemble.train_on_dataset(dataset, optimizer, 1, 2, use_cuda) + ensemble.train_on_dataset(dataset) ensemble.add_checkpoint() assert len(ensemble._weights) == n_ensemble diff --git a/tests/integration_test.py b/tests/integration_test.py index f87fd86f..d112a868 100644 --- a/tests/integration_test.py +++ b/tests/integration_test.py @@ -12,7 +12,7 @@ from baal.active import ActiveLearningDataset from baal.active import ActiveLearningLoop from baal.active import heuristics -from baal.modelwrapper import ModelWrapper +from baal.modelwrapper import ModelWrapper, TrainingArgs from baal.calibration import DirichletCalibrator @@ -47,28 +47,23 @@ def test_integration(): optimizer = optim.SGD(model.parameters(), lr=0.001, momentum=0.9, weight_decay=0.0005) # We can now use BaaL to create the active learning loop. - - model = ModelWrapper(model, criterion) + args = TrainingArgs(criterion=criterion, optimizer=optimizer, batch_size=10, use_cuda=use_cuda, epoch=1) + model = ModelWrapper(model, args) # We create an ActiveLearningLoop that will automatically label the most uncertain samples. # In this case, we use the widely used BALD heuristic. active_loop = ActiveLearningLoop(al_dataset, model.predict_on_dataset, heuristic=heuristics.BALD(), - query_size=10, - batch_size=10, iterations=2, - use_cuda=use_cuda, - workers=4) + query_size=10) # We're all set! num_steps = 10 for step in range(num_steps): old_param = list(map(lambda x: x.clone(), model.model.parameters())) - model.train_on_dataset(al_dataset, optimizer=optimizer, batch_size=10, - epoch=1, use_cuda=use_cuda, workers=2) - model.test_on_dataset(cifar10_test, batch_size=10, use_cuda=use_cuda, - workers=2) + model.train_on_dataset(al_dataset) + model.test_on_dataset(cifar10_test) if not active_loop.step(): break @@ -95,25 +90,21 @@ def test_calibration_integration(): criterion = nn.CrossEntropyLoss() optimizer = optim.SGD(model.parameters(), lr=0.001, momentum=0.9, weight_decay=0.0005) - wrapper = ModelWrapper(model, criterion) + args = TrainingArgs(criterion=criterion, optimizer=optimizer, batch_size=10, use_cuda=use_cuda, epoch=1) + wrapper = ModelWrapper(model, args) calibrator = DirichletCalibrator(wrapper=wrapper, num_classes=10, lr=0.001, reg_factor=0.01) for step in range(2): - wrapper.train_on_dataset(al_dataset, optimizer=optimizer, - batch_size=10, epoch=1, - use_cuda=use_cuda, workers=0) + wrapper.train_on_dataset(al_dataset) - wrapper.test_on_dataset(cifar10_test, batch_size=10, - use_cuda=use_cuda, workers=0) + wrapper.test_on_dataset(cifar10_test) before_calib_param = list(map(lambda x: x.clone(), wrapper.model.parameters())) - calibrator.calibrate(al_dataset, cifar10_test, - batch_size=10, epoch=5, - use_cuda=use_cuda, double_fit=False, workers=0) + calibrator.calibrate(al_dataset, cifar10_test, use_cuda=use_cuda, double_fit=False) after_calib_param = list(map(lambda x: x.clone(), model.parameters())) diff --git a/tests/metrics/test_mixin.py b/tests/metrics/test_mixin.py index 889716d0..2d346e7b 100644 --- a/tests/metrics/test_mixin.py +++ b/tests/metrics/test_mixin.py @@ -1,12 +1,12 @@ import numpy as np -import torch -from baal.modelwrapper import ModelWrapper -from baal.utils.metrics import Accuracy, Precision + +from baal.modelwrapper import ModelWrapper, TrainingArgs +from baal.utils.metrics import Accuracy def test_active_step(): - wrapper = ModelWrapper(None, None) - precisions = np.linspace(0,1, 10, endpoint=False) + wrapper = ModelWrapper(None, TrainingArgs()) + precisions = np.linspace(0, 1, 10, endpoint=False) recalls = np.linspace(0.5, 1, 10, endpoint=False) dataset_size = list(range(100, 1100, 100)) for ds_size, precision, recall in zip(dataset_size, precisions, recalls): @@ -19,8 +19,8 @@ def test_active_step(): 'Precision': 0.0, 'Recall': 0.5 } - - wrapper = ModelWrapper(None, None) + + wrapper = ModelWrapper(None, TrainingArgs()) wrapper.set_dataset_size(1000) wrapper.active_step(dataset_size=None, metrics={ 'Precision': 0.1, @@ -29,13 +29,14 @@ def test_active_step(): assert wrapper._active_dataset_size == 1000 assert wrapper.active_learning_metrics == { 1000: { - 'Precision': 0.1, - 'Recall': 0.2 - } + 'Precision': 0.1, + 'Recall': 0.2 + } } + def test_get_metrics(): - wrapper = ModelWrapper(None, None) + wrapper = ModelWrapper(None, TrainingArgs()) wrapper.add_metric('accuracy', Accuracy) assert len(wrapper.get_metrics()) == 4 @@ -49,4 +50,4 @@ def test_get_metrics(): assert len(wrapper.get_metrics('test')) == 3 assert sum('test' in ki for ki in wrapper.get_metrics('test')) == 2 assert len(wrapper.get_metrics('train')) == 3 - assert sum('train' in ki for ki in wrapper.get_metrics('train')) == 2 \ No newline at end of file + assert sum('train' in ki for ki in wrapper.get_metrics('train')) == 2 diff --git a/tests/modelwrapper_test.py b/tests/modelwrapper_test.py index c29e3b1c..c24f2af4 100644 --- a/tests/modelwrapper_test.py +++ b/tests/modelwrapper_test.py @@ -1,3 +1,4 @@ +import dataclasses import math import unittest from unittest.mock import Mock @@ -9,7 +10,7 @@ from torch import nn from torch.utils.data import Dataset, DataLoader -from baal.modelwrapper import ModelWrapper, mc_inference +from baal.modelwrapper import ModelWrapper, mc_inference, TrainingArgs from baal.utils.metrics import ClassificationReport @@ -59,15 +60,18 @@ def forward(self, x): self._crit = nn.MSELoss() self.criterion = lambda x, y: self._crit(x[0], y) + self._crit(x[1], y) self.model = MultiOutModel() - self.wrapper = ModelWrapper(self.model, self.criterion) - self.optim = torch.optim.SGD(self.wrapper.get_params(), 0.01) + self.optim = torch.optim.SGD(self.model.parameters(), 0.01) self.dataset = DummyDataset() + self.args = TrainingArgs(criterion=self.criterion, + optimizer=self.optim, + batch_size=4, epoch=1, use_cuda=False, workers=0) + self.wrapper = ModelWrapper(self.model, args=self.args) def test_train_on_batch(self): self.wrapper.train() old_param = list(map(lambda x: x.clone(), self.model.parameters())) input, target = [torch.stack(v) for v in zip(*(self.dataset[0], self.dataset[1]))] - self.wrapper.train_on_batch(input, target, self.optim) + self.wrapper.train_on_batch(input, target) new_param = list(map(lambda x: x.clone(), self.model.parameters())) assert any([not torch.allclose(i, j) for i, j in zip(old_param, new_param)]) @@ -75,7 +79,7 @@ def test_test_on_batch(self): self.wrapper.eval() input, target = [torch.stack(v) for v in zip(*(self.dataset[0], self.dataset[1]))] preds = torch.stack( - [self.wrapper.test_on_batch(input, target, cuda=False) for _ in range(10)] + [self.wrapper.test_on_batch(input, target) for _ in range(10)] ).view(10, -1) # Same loss @@ -84,7 +88,7 @@ def test_test_on_batch(self): preds = torch.stack( [ self.wrapper.test_on_batch( - input, target, cuda=False, average_predictions=10 + input, target, average_predictions=10 ) for _ in range(10) ] @@ -96,27 +100,29 @@ def test_predict_on_batch(self): input = torch.stack((self.dataset[0][0], self.dataset[1][0])) # iteration == 1 - pred = self.wrapper.predict_on_batch(input, 1, False) + pred = self.wrapper.predict_on_batch(input, iterations=1) assert pred[0].size() == (2, 1, 1) # iterations > 1 - pred = self.wrapper.predict_on_batch(input, 10, False) + pred = self.wrapper.predict_on_batch(input, 10, ) assert pred[0].size() == (2, 1, 10) # iteration == 1 - self.wrapper = ModelWrapper(self.model, self.criterion, replicate_in_memory=False) - pred = self.wrapper.predict_on_batch(input, 1, False) + new_args = dataclasses.replace(self.args) + new_args.replicate_in_memory = False + self.wrapper = ModelWrapper(self.model, new_args) + pred = self.wrapper.predict_on_batch(input, 1) assert pred[0].size() == (2, 1, 1) # iterations > 1 - pred = self.wrapper.predict_on_batch(input, 10, False) + pred = self.wrapper.predict_on_batch(input, 10) assert pred[0].size() == (2, 1, 10) def test_out_of_mem_raises_error(self): self.wrapper.eval() input = torch.stack((self.dataset[0][0], self.dataset[1][0])) with pytest.raises(RuntimeError) as e_info: - self.wrapper.predict_on_batch(input, 0, False) + self.wrapper.predict_on_batch(input, 0) assert 'CUDA ran out of memory while BaaL tried to replicate data' in str(e_info.value) def test_raising_type_errors(self): @@ -124,31 +130,25 @@ def test_raising_type_errors(self): self.wrapper.eval() input = torch.stack((self.dataset[0][0], self.dataset[1][0])) with pytest.raises(TypeError): - self.wrapper.predict_on_batch(input, iterations, False) - - def test_using_cuda_raises_error_while_testing(self): - '''CUDA is not available on test environment''' - self.wrapper.eval() - input = torch.stack((self.dataset[0][0], self.dataset[1][0])) - with pytest.raises(Exception): - self.wrapper.predict_on_batch(input, 1, True) + self.wrapper.predict_on_batch(input, iterations) def test_train(self): - history = self.wrapper.train_on_dataset(self.dataset, self.optim, 10, 2, use_cuda=False, - workers=0) + new_args = dataclasses.replace(self.args) + new_args.epoch = 2 + wrapper = ModelWrapper(model=self.model, args=new_args) + history = wrapper.train_on_dataset(self.dataset) assert len(history) == 2 def test_test(self): - l = self.wrapper.test_on_dataset(self.dataset, 10, use_cuda=False, workers=0) + l = self.wrapper.test_on_dataset(self.dataset, 10) assert np.isfinite(l) l = self.wrapper.test_on_dataset( - self.dataset, 10, use_cuda=False, workers=0, average_predictions=10 + self.dataset, average_predictions=10 ) assert np.isfinite(l) def test_predict(self): - l = self.wrapper.predict_on_dataset(self.dataset, 10, 20, use_cuda=False, - workers=0) + l = self.wrapper.predict_on_dataset(self.dataset, 20,) self.wrapper.eval() assert np.allclose( self.wrapper.predict_on_batch(self.dataset[0][0].unsqueeze(0), 20)[0].detach().numpy(), @@ -160,24 +160,21 @@ def test_predict(self): assert l[0].shape == (len(self.dataset), 1, 20) # Test generators - l_gen = self.wrapper.predict_on_dataset_generator(self.dataset, 10, 20, use_cuda=False, - workers=0) + l_gen = self.wrapper.predict_on_dataset_generator(self.dataset, 20) assert np.allclose(next(l_gen)[0][0], l[0][0]) for last in l_gen: pass # Get last item assert np.allclose(last[0][-1], l[0][-1]) # Test Half - l_gen = self.wrapper.predict_on_dataset_generator(self.dataset, 10, 20, use_cuda=False, - workers=0, half=True) - l = self.wrapper.predict_on_dataset(self.dataset, 10, 20, use_cuda=False, workers=0, + l_gen = self.wrapper.predict_on_dataset_generator(self.dataset, 20, half=True) + l = self.wrapper.predict_on_dataset(self.dataset, 10, half=True) assert next(l_gen)[0].dtype == np.float16 assert l[0].dtype == np.float16 data_s = [] - l_gen = self.wrapper.predict_on_dataset_generator(data_s, 10, 20, use_cuda=False, - workers=0, half=True) + l_gen = self.wrapper.predict_on_dataset_generator(data_s, 20, half=True) assert len(list(l_gen)) == 0 @@ -189,15 +186,18 @@ def setUp(self): # ) self.model = SimpleModel() self.criterion = nn.BCEWithLogitsLoss() - self.wrapper = ModelWrapper(self.model, self.criterion) - self.optim = torch.optim.SGD(self.wrapper.get_params(), 0.01) + self.optim = torch.optim.SGD(self.model.parameters(), 0.01) self.dataset = DummyDataset() + self.args = TrainingArgs(criterion=self.criterion, + optimizer=self.optim, + batch_size=4, epoch=2, use_cuda=False, workers=0) + self.wrapper = ModelWrapper(self.model, args=self.args) def test_train_on_batch(self): self.wrapper.train() old_param = list(map(lambda x: x.clone(), self.model.parameters())) input, target = torch.randn([1, 3, 10, 10]), torch.randn(1, 1) - self.wrapper.train_on_batch(input, target, self.optim) + self.wrapper.train_on_batch(input, target) new_param = list(map(lambda x: x.clone(), self.model.parameters())) assert any([not torch.allclose(i, j) for i, j in zip(old_param, new_param)]) @@ -220,7 +220,7 @@ def test_test_on_batch(self): self.wrapper.eval() input, target = torch.randn([1, 3, 10, 10]), torch.randn(1, 1) preds = torch.stack( - [self.wrapper.test_on_batch(input, target, cuda=False) for _ in range(10)] + [self.wrapper.test_on_batch(input, target) for _ in range(10)] ).view(10, -1) # Same loss @@ -229,7 +229,7 @@ def test_test_on_batch(self): preds = torch.stack( [ self.wrapper.test_on_batch( - input, target, cuda=False, average_predictions=10 + input, target, average_predictions=10 ) for _ in range(10) ] @@ -241,38 +241,38 @@ def test_predict_on_batch(self): input = torch.randn([2, 3, 10, 10]) # iteration == 1 - pred = self.wrapper.predict_on_batch(input, 1, False) + pred = self.wrapper.predict_on_batch(input, 1,) assert pred.size() == (2, 1, 1) # iterations > 1 - pred = self.wrapper.predict_on_batch(input, 10, False) + pred = self.wrapper.predict_on_batch(input, 10,) assert pred.size() == (2, 1, 10) # iteration == 1 - self.wrapper = ModelWrapper(self.model, self.criterion, replicate_in_memory=False) - pred = self.wrapper.predict_on_batch(input, 1, False) + new_args = dataclasses.replace(self.args) + new_args.replicate_in_memory = False + wrapper = ModelWrapper(self.model, new_args) + pred = wrapper.predict_on_batch(input, 1) assert pred.size() == (2, 1, 1) # iterations > 1 - pred = self.wrapper.predict_on_batch(input, 10, False) + pred = wrapper.predict_on_batch(input, 10) assert pred.size() == (2, 1, 10) def test_train(self): - history = self.wrapper.train_on_dataset(self.dataset, self.optim, 10, 2, use_cuda=False, - workers=0) + history = self.wrapper.train_on_dataset(self.dataset) assert len(history) == 2 def test_test(self): - l = self.wrapper.test_on_dataset(self.dataset, 10, use_cuda=False, workers=0) + l = self.wrapper.test_on_dataset(self.dataset, 10) assert np.isfinite(l) l = self.wrapper.test_on_dataset( - self.dataset, 10, use_cuda=False, workers=0, average_predictions=10 + self.dataset, average_predictions=10 ) assert np.isfinite(l) def test_predict(self): - l = self.wrapper.predict_on_dataset(self.dataset, 10, 20, use_cuda=False, - workers=0) + l = self.wrapper.predict_on_dataset(self.dataset, 20, ) self.wrapper.eval() assert np.allclose( self.wrapper.predict_on_batch(self.dataset[0][0].unsqueeze(0), 20)[0].detach().numpy(), @@ -283,17 +283,15 @@ def test_predict(self): assert l.shape == (len(self.dataset), 1, 20) # Test generators - l_gen = self.wrapper.predict_on_dataset_generator(self.dataset, 10, 20, use_cuda=False, - workers=0) + l_gen = self.wrapper.predict_on_dataset_generator(self.dataset, 20, ) assert np.allclose(next(l_gen)[0], l[0]) for last in l_gen: pass # Get last item assert np.allclose(last[-1], l[-1]) # Test Half - l_gen = self.wrapper.predict_on_dataset_generator(self.dataset, 10, 20, use_cuda=False, - workers=0, half=True) - l = self.wrapper.predict_on_dataset(self.dataset, 10, 20, use_cuda=False, workers=0, + l_gen = self.wrapper.predict_on_dataset_generator(self.dataset, 20, half=True) + l = self.wrapper.predict_on_dataset(self.dataset, 20, half=True) assert next(l_gen).dtype == np.float16 assert l.dtype == np.float16 @@ -302,13 +300,14 @@ def test_states(self): input = torch.randn([1, 3, 10, 10]) def pred_with_dropout(replicate_in_memory): - self.wrapper = ModelWrapper(self.model, self.criterion, - replicate_in_memory=replicate_in_memory) - self.wrapper.train() + new_args = dataclasses.replace(self.args) + new_args.replicate_in_memory = replicate_in_memory + wrapper = ModelWrapper(self.model, new_args) + wrapper.train() # Dropout make the pred changes preds = torch.stack( [ - self.wrapper.predict_on_batch(input, iterations=1, cuda=False) + wrapper.predict_on_batch(input, iterations=1) for _ in range(10) ] ).view(10, -1) @@ -318,13 +317,14 @@ def pred_with_dropout(replicate_in_memory): pred_with_dropout(replicate_in_memory=False) def pred_without_dropout(replicate_in_memory): - self.wrapper = ModelWrapper(self.model, self.criterion, - replicate_in_memory=replicate_in_memory) + new_args = dataclasses.replace(self.args) + new_args.replicate_in_memory = replicate_in_memory + wrapper = ModelWrapper(self.model, new_args) # Dropout is not active in eval - self.wrapper.eval() + wrapper.eval() preds = torch.stack( [ - self.wrapper.predict_on_batch(input, iterations=1, cuda=False) + wrapper.predict_on_batch(input, iterations=1) for _ in range(10) ] ).view(10, -1) @@ -337,36 +337,39 @@ def test_add_metric(self): self.wrapper.add_metric('cls_report', lambda: ClassificationReport(2)) assert 'test_cls_report' in self.wrapper.metrics assert 'train_cls_report' in self.wrapper.metrics - self.wrapper.train_on_dataset(self.dataset, self.optim, 32, 2, False) - self.wrapper.test_on_dataset(self.dataset, 32, False) + self.wrapper.train_on_dataset(self.dataset) + self.wrapper.test_on_dataset(self.dataset, ) assert (self.wrapper.metrics['train_cls_report'].value['accuracy'] != 0).any() assert (self.wrapper.metrics['test_cls_report'].value['accuracy'] != 0).any() def test_train_and_test(self): - res = self.wrapper.train_and_test_on_datasets(self.dataset, self.dataset, self.optim, - 32, 5, False, return_best_weights=False) - assert len(res) == 5 - res = self.wrapper.train_and_test_on_datasets(self.dataset, self.dataset, self.optim, - 32, 5, False, return_best_weights=True) + res = self.wrapper.train_and_test_on_datasets(self.dataset, self.dataset, + return_best_weights=False) + assert len(res) == 2 + res = self.wrapper.train_and_test_on_datasets(self.dataset, self.dataset, return_best_weights=True) assert len(res) == 2 - assert len(res[0]) == 5 + assert len(res[0]) == 2 assert isinstance(res[1], dict) mock = Mock() mock.side_effect = (((np.linspace(0, 50) - 10) / 10) ** 2).tolist() - self.wrapper.test_on_dataset = mock - res = self.wrapper.train_and_test_on_datasets(self.dataset, self.dataset, - self.optim, 32, 50, - False, return_best_weights=True, patience=1) + new_args = dataclasses.replace(self.args) + new_args.epoch = 50 + wrapper = ModelWrapper(self.wrapper.model, new_args) + wrapper.test_on_dataset = mock + res = wrapper.train_and_test_on_datasets(self.dataset, self.dataset, return_best_weights=True, patience=1) assert len(res) == 2 assert len(res[0]) < 50 mock = Mock() mock.side_effect = (((np.linspace(0, 50) - 10) / 10) ** 2).tolist() - self.wrapper.test_on_dataset = mock - res = self.wrapper.train_and_test_on_datasets(self.dataset, self.dataset, - self.optim, 32, 50, - False, return_best_weights=True, patience=1, + + # iteration == 1 + new_args = dataclasses.replace(self.args) + new_args.epoch = 50 + wrapper = ModelWrapper(self.wrapper.model, new_args) + wrapper.test_on_dataset = mock + res = wrapper.train_and_test_on_datasets(self.dataset, self.dataset, return_best_weights=True, patience=1, min_epoch_for_es=20) assert len(res) == 2 assert len(res[0]) < 50 and len(res[0]) > 20 @@ -374,14 +377,14 @@ def test_train_and_test(self): def test_torchmetric(self): mse_fn = lambda: torchmetrics.MeanSquaredError() corr_fn = lambda: torchmetrics.SpearmanCorrCoef() - wrapper = ModelWrapper(self.model, self.criterion) + wrapper = ModelWrapper(self.model, self.args) wrapper.add_metric('mse', mse_fn) wrapper.add_metric('corr', corr_fn) - wrapper.train_on_dataset(self.dataset, self.optim, batch_size=32, epoch=1, use_cuda=False) - wrapper.test_on_dataset(self.dataset, batch_size=32, use_cuda=False) + wrapper.train_on_dataset(self.dataset) + wrapper.test_on_dataset(self.dataset) metrics = wrapper.get_metrics() - assert {'train_corr', 'test_corr', 'train_mse', 'test_mse'}.issubset(metrics.keys()) # Torchmetric metric - assert {'train_loss', 'test_loss'}.issubset(metrics.keys()) # Baal metric + assert {'train_corr', 'test_corr', 'train_mse', 'test_mse'}.issubset(metrics.keys()) # Torchmetric metric + assert {'train_loss', 'test_loss'}.issubset(metrics.keys()) # Baal metric def test_multi_input_model(): @@ -397,11 +400,11 @@ def forward(self, x): return self.model(x1) + self.model(x2) model = MultiInModel() - wrapper = ModelWrapper(model, None) + wrapper = ModelWrapper(model, TrainingArgs(batch_size=15, epoch=1, use_cuda=False, optimizer=None)) dataset = DummyDataset(n_in=2) assert len(dataset[0]) == 2 b = next(iter(DataLoader(dataset, 15, False)))[0] - l = wrapper.predict_on_batch(b, iterations=10, cuda=False) + l = wrapper.predict_on_batch(b, iterations=10) assert l.shape[0] == 15 and l.shape[-1] == 10