diff --git a/Deep-SAD-PyTorch/src/DeepSAD.py b/Deep-SAD-PyTorch/src/DeepSAD.py index 1f00204..5433012 100644 --- a/Deep-SAD-PyTorch/src/DeepSAD.py +++ b/Deep-SAD-PyTorch/src/DeepSAD.py @@ -41,53 +41,80 @@ class DeepSAD(object): self.ae_optimizer_name = None self.results = { - 'train_time': None, - 'test_auc': None, - 'test_time': None, - 'test_scores': None, + "train_time": None, + "test_auc": None, + "test_time": None, + "test_scores": None, } - self.ae_results = { - 'train_time': None, - 'test_auc': None, - 'test_time': None - } + self.ae_results = {"train_time": None, "test_auc": None, "test_time": None} def set_network(self, net_name): """Builds the neural network phi.""" self.net_name = net_name self.net = build_network(net_name) - def train(self, dataset: BaseADDataset, optimizer_name: str = 'adam', lr: float = 0.001, n_epochs: int = 50, - lr_milestones: tuple = (), batch_size: int = 128, weight_decay: float = 1e-6, device: str = 'cuda', - n_jobs_dataloader: int = 0): + def train( + self, + dataset: BaseADDataset, + optimizer_name: str = "adam", + lr: float = 0.001, + n_epochs: int = 50, + lr_milestones: tuple = (), + batch_size: int = 128, + weight_decay: float = 1e-6, + device: str = "cuda", + n_jobs_dataloader: int = 0, + ): """Trains the Deep SAD model on the training data.""" self.optimizer_name = optimizer_name - self.trainer = DeepSADTrainer(self.c, self.eta, optimizer_name=optimizer_name, lr=lr, n_epochs=n_epochs, - lr_milestones=lr_milestones, batch_size=batch_size, weight_decay=weight_decay, - device=device, n_jobs_dataloader=n_jobs_dataloader) + self.trainer = DeepSADTrainer( + self.c, + self.eta, + optimizer_name=optimizer_name, + lr=lr, + n_epochs=n_epochs, + lr_milestones=lr_milestones, + batch_size=batch_size, + weight_decay=weight_decay, + device=device, + n_jobs_dataloader=n_jobs_dataloader, + ) # Get the model self.net = self.trainer.train(dataset, self.net) - self.results['train_time'] = self.trainer.train_time + self.results["train_time"] = self.trainer.train_time self.c = self.trainer.c.cpu().data.numpy().tolist() # get as list - def test(self, dataset: BaseADDataset, device: str = 'cuda', n_jobs_dataloader: int = 0): + def test( + self, dataset: BaseADDataset, device: str = "cuda", n_jobs_dataloader: int = 0 + ): """Tests the Deep SAD model on the test data.""" if self.trainer is None: - self.trainer = DeepSADTrainer(self.c, self.eta, device=device, n_jobs_dataloader=n_jobs_dataloader) + self.trainer = DeepSADTrainer( + self.c, self.eta, device=device, n_jobs_dataloader=n_jobs_dataloader + ) self.trainer.test(dataset, self.net) # Get results - self.results['test_auc'] = self.trainer.test_auc - self.results['test_time'] = self.trainer.test_time - self.results['test_scores'] = self.trainer.test_scores + self.results["test_auc"] = self.trainer.test_auc + self.results["test_time"] = self.trainer.test_time + self.results["test_scores"] = self.trainer.test_scores - def pretrain(self, dataset: BaseADDataset, optimizer_name: str = 'adam', lr: float = 0.001, n_epochs: int = 100, - lr_milestones: tuple = (), batch_size: int = 128, weight_decay: float = 1e-6, device: str = 'cuda', - n_jobs_dataloader: int = 0): + def pretrain( + self, + dataset: BaseADDataset, + optimizer_name: str = "adam", + lr: float = 0.001, + n_epochs: int = 100, + lr_milestones: tuple = (), + batch_size: int = 128, + weight_decay: float = 1e-6, + device: str = "cuda", + n_jobs_dataloader: int = 0, + ): """Pretrains the weights for the Deep SAD network phi via autoencoder.""" # Set autoencoder network @@ -95,20 +122,27 @@ class DeepSAD(object): # Train self.ae_optimizer_name = optimizer_name - self.ae_trainer = AETrainer(optimizer_name, lr=lr, n_epochs=n_epochs, lr_milestones=lr_milestones, - batch_size=batch_size, weight_decay=weight_decay, device=device, - n_jobs_dataloader=n_jobs_dataloader) + self.ae_trainer = AETrainer( + optimizer_name, + lr=lr, + n_epochs=n_epochs, + lr_milestones=lr_milestones, + batch_size=batch_size, + weight_decay=weight_decay, + device=device, + n_jobs_dataloader=n_jobs_dataloader, + ) self.ae_net = self.ae_trainer.train(dataset, self.ae_net) # Get train results - self.ae_results['train_time'] = self.ae_trainer.train_time + self.ae_results["train_time"] = self.ae_trainer.train_time # Test self.ae_trainer.test(dataset, self.ae_net) # Get test results - self.ae_results['test_auc'] = self.ae_trainer.test_auc - self.ae_results['test_time'] = self.ae_trainer.test_time + self.ae_results["test_auc"] = self.ae_trainer.test_auc + self.ae_results["test_time"] = self.ae_trainer.test_time # Initialize Deep SAD network weights from pre-trained encoder self.init_network_weights_from_pretraining() @@ -132,30 +166,31 @@ class DeepSAD(object): net_dict = self.net.state_dict() ae_net_dict = self.ae_net.state_dict() if save_ae else None - torch.save({'c': self.c, - 'net_dict': net_dict, - 'ae_net_dict': ae_net_dict}, export_model) + torch.save( + {"c": self.c, "net_dict": net_dict, "ae_net_dict": ae_net_dict}, + export_model, + ) - def load_model(self, model_path, load_ae=False, map_location='cpu'): + def load_model(self, model_path, load_ae=False, map_location="cpu"): """Load Deep SAD model from model_path.""" model_dict = torch.load(model_path, map_location=map_location) - self.c = model_dict['c'] - self.net.load_state_dict(model_dict['net_dict']) + self.c = model_dict["c"] + self.net.load_state_dict(model_dict["net_dict"]) # load autoencoder parameters if specified if load_ae: if self.ae_net is None: self.ae_net = build_autoencoder(self.net_name) - self.ae_net.load_state_dict(model_dict['ae_net_dict']) + self.ae_net.load_state_dict(model_dict["ae_net_dict"]) def save_results(self, export_json): """Save results dict to a JSON-file.""" - with open(export_json, 'w') as fp: + with open(export_json, "w") as fp: json.dump(self.results, fp) def save_ae_results(self, export_json): """Save autoencoder results dict to a JSON-file.""" - with open(export_json, 'w') as fp: + with open(export_json, "w") as fp: json.dump(self.ae_results, fp) diff --git a/Deep-SAD-PyTorch/src/base/base_dataset.py b/Deep-SAD-PyTorch/src/base/base_dataset.py index 5989d72..6b1869c 100644 --- a/Deep-SAD-PyTorch/src/base/base_dataset.py +++ b/Deep-SAD-PyTorch/src/base/base_dataset.py @@ -10,15 +10,24 @@ class BaseADDataset(ABC): self.root = root # root path to data self.n_classes = 2 # 0: normal, 1: outlier - self.normal_classes = None # tuple with original class labels that define the normal class - self.outlier_classes = None # tuple with original class labels that define the outlier class + self.normal_classes = ( + None # tuple with original class labels that define the normal class + ) + self.outlier_classes = ( + None # tuple with original class labels that define the outlier class + ) self.train_set = None # must be of type torch.utils.data.Dataset self.test_set = None # must be of type torch.utils.data.Dataset @abstractmethod - def loaders(self, batch_size: int, shuffle_train=True, shuffle_test=False, num_workers: int = 0) -> ( - DataLoader, DataLoader): + def loaders( + self, + batch_size: int, + shuffle_train=True, + shuffle_test=False, + num_workers: int = 0, + ) -> (DataLoader, DataLoader): """Implement data loaders of type torch.utils.data.DataLoader for train_set and test_set.""" pass diff --git a/Deep-SAD-PyTorch/src/base/base_net.py b/Deep-SAD-PyTorch/src/base/base_net.py index af90a9a..08c1b2c 100644 --- a/Deep-SAD-PyTorch/src/base/base_net.py +++ b/Deep-SAD-PyTorch/src/base/base_net.py @@ -22,5 +22,5 @@ class BaseNet(nn.Module): """Network summary.""" net_parameters = filter(lambda p: p.requires_grad, self.parameters()) params = sum([np.prod(p.size()) for p in net_parameters]) - self.logger.info('Trainable parameters: {}'.format(params)) + self.logger.info("Trainable parameters: {}".format(params)) self.logger.info(self) diff --git a/Deep-SAD-PyTorch/src/base/base_trainer.py b/Deep-SAD-PyTorch/src/base/base_trainer.py index 530eee5..a76f9b7 100644 --- a/Deep-SAD-PyTorch/src/base/base_trainer.py +++ b/Deep-SAD-PyTorch/src/base/base_trainer.py @@ -6,8 +6,17 @@ from .base_net import BaseNet class BaseTrainer(ABC): """Trainer base class.""" - def __init__(self, optimizer_name: str, lr: float, n_epochs: int, lr_milestones: tuple, batch_size: int, - weight_decay: float, device: str, n_jobs_dataloader: int): + def __init__( + self, + optimizer_name: str, + lr: float, + n_epochs: int, + lr_milestones: tuple, + batch_size: int, + weight_decay: float, + device: str, + n_jobs_dataloader: int, + ): super().__init__() self.optimizer_name = optimizer_name self.lr = lr diff --git a/Deep-SAD-PyTorch/src/base/odds_dataset.py b/Deep-SAD-PyTorch/src/base/odds_dataset.py index a8c8340..005d2e7 100644 --- a/Deep-SAD-PyTorch/src/base/odds_dataset.py +++ b/Deep-SAD-PyTorch/src/base/odds_dataset.py @@ -19,15 +19,22 @@ class ODDSDataset(Dataset): """ urls = { - 'arrhythmia': 'https://www.dropbox.com/s/lmlwuspn1sey48r/arrhythmia.mat?dl=1', - 'cardio': 'https://www.dropbox.com/s/galg3ihvxklf0qi/cardio.mat?dl=1', - 'satellite': 'https://www.dropbox.com/s/dpzxp8jyr9h93k5/satellite.mat?dl=1', - 'satimage-2': 'https://www.dropbox.com/s/hckgvu9m6fs441p/satimage-2.mat?dl=1', - 'shuttle': 'https://www.dropbox.com/s/mk8ozgisimfn3dw/shuttle.mat?dl=1', - 'thyroid': 'https://www.dropbox.com/s/bih0e15a0fukftb/thyroid.mat?dl=1' + "arrhythmia": "https://www.dropbox.com/s/lmlwuspn1sey48r/arrhythmia.mat?dl=1", + "cardio": "https://www.dropbox.com/s/galg3ihvxklf0qi/cardio.mat?dl=1", + "satellite": "https://www.dropbox.com/s/dpzxp8jyr9h93k5/satellite.mat?dl=1", + "satimage-2": "https://www.dropbox.com/s/hckgvu9m6fs441p/satimage-2.mat?dl=1", + "shuttle": "https://www.dropbox.com/s/mk8ozgisimfn3dw/shuttle.mat?dl=1", + "thyroid": "https://www.dropbox.com/s/bih0e15a0fukftb/thyroid.mat?dl=1", } - def __init__(self, root: str, dataset_name: str, train=True, random_state=None, download=False): + def __init__( + self, + root: str, + dataset_name: str, + train=True, + random_state=None, + download=False, + ): super(Dataset, self).__init__() self.classes = [0, 1] @@ -37,25 +44,25 @@ class ODDSDataset(Dataset): self.root = Path(root) self.dataset_name = dataset_name self.train = train # training set or test set - self.file_name = self.dataset_name + '.mat' + self.file_name = self.dataset_name + ".mat" self.data_file = self.root / self.file_name if download: self.download() mat = loadmat(self.data_file) - X = mat['X'] - y = mat['y'].ravel() + X = mat["X"] + y = mat["y"].ravel() idx_norm = y == 0 idx_out = y == 1 # 60% data for training and 40% for testing; keep outlier ratio - X_train_norm, X_test_norm, y_train_norm, y_test_norm = train_test_split(X[idx_norm], y[idx_norm], - test_size=0.4, - random_state=random_state) - X_train_out, X_test_out, y_train_out, y_test_out = train_test_split(X[idx_out], y[idx_out], - test_size=0.4, - random_state=random_state) + X_train_norm, X_test_norm, y_train_norm, y_test_norm = train_test_split( + X[idx_norm], y[idx_norm], test_size=0.4, random_state=random_state + ) + X_train_out, X_test_out, y_train_out, y_test_out = train_test_split( + X[idx_out], y[idx_out], test_size=0.4, random_state=random_state + ) X_train = np.concatenate((X_train_norm, X_train_out)) X_test = np.concatenate((X_test_norm, X_test_out)) y_train = np.concatenate((y_train_norm, y_train_out)) @@ -88,7 +95,11 @@ class ODDSDataset(Dataset): Returns: tuple: (sample, target, semi_target, index) """ - sample, target, semi_target = self.data[index], int(self.targets[index]), int(self.semi_targets[index]) + sample, target, semi_target = ( + self.data[index], + int(self.targets[index]), + int(self.semi_targets[index]), + ) return sample, target, semi_target, index @@ -107,4 +118,4 @@ class ODDSDataset(Dataset): # download file download_url(self.urls[self.dataset_name], self.root, self.file_name) - print('Done!') + print("Done!") diff --git a/Deep-SAD-PyTorch/src/base/torchvision_dataset.py b/Deep-SAD-PyTorch/src/base/torchvision_dataset.py index 82d468b..9412615 100644 --- a/Deep-SAD-PyTorch/src/base/torchvision_dataset.py +++ b/Deep-SAD-PyTorch/src/base/torchvision_dataset.py @@ -8,10 +8,25 @@ class TorchvisionDataset(BaseADDataset): def __init__(self, root: str): super().__init__(root) - def loaders(self, batch_size: int, shuffle_train=True, shuffle_test=False, num_workers: int = 0) -> ( - DataLoader, DataLoader): - train_loader = DataLoader(dataset=self.train_set, batch_size=batch_size, shuffle=shuffle_train, - num_workers=num_workers, drop_last=True) - test_loader = DataLoader(dataset=self.test_set, batch_size=batch_size, shuffle=shuffle_test, - num_workers=num_workers, drop_last=False) + def loaders( + self, + batch_size: int, + shuffle_train=True, + shuffle_test=False, + num_workers: int = 0, + ) -> (DataLoader, DataLoader): + train_loader = DataLoader( + dataset=self.train_set, + batch_size=batch_size, + shuffle=shuffle_train, + num_workers=num_workers, + drop_last=True, + ) + test_loader = DataLoader( + dataset=self.test_set, + batch_size=batch_size, + shuffle=shuffle_test, + num_workers=num_workers, + drop_last=False, + ) return train_loader, test_loader diff --git a/Deep-SAD-PyTorch/src/baseline_SemiDGM.py b/Deep-SAD-PyTorch/src/baseline_SemiDGM.py index 554fbff..444d2e9 100644 --- a/Deep-SAD-PyTorch/src/baseline_SemiDGM.py +++ b/Deep-SAD-PyTorch/src/baseline_SemiDGM.py @@ -14,64 +14,215 @@ from datasets.main import load_dataset # Settings ################################################################################ @click.command() -@click.argument('dataset_name', type=click.Choice(['mnist', 'fmnist', 'cifar10', 'arrhythmia', 'cardio', 'satellite', - 'satimage-2', 'shuttle', 'thyroid'])) -@click.argument('net_name', type=click.Choice(['mnist_DGM_M2', 'mnist_DGM_M1M2', 'fmnist_DGM_M2', 'fmnist_DGM_M1M2', - 'cifar10_DGM_M2', 'cifar10_DGM_M1M2', - 'arrhythmia_DGM_M2', 'cardio_DGM_M2', 'satellite_DGM_M2', - 'satimage-2_DGM_M2', 'shuttle_DGM_M2', 'thyroid_DGM_M2'])) -@click.argument('xp_path', type=click.Path(exists=True)) -@click.argument('data_path', type=click.Path(exists=True)) -@click.option('--load_config', type=click.Path(exists=True), default=None, - help='Config JSON-file path (default: None).') -@click.option('--load_model', type=click.Path(exists=True), default=None, - help='Model file path (default: None).') -@click.option('--ratio_known_normal', type=float, default=0.0, - help='Ratio of known (labeled) normal training examples.') -@click.option('--ratio_known_outlier', type=float, default=0.0, - help='Ratio of known (labeled) anomalous training examples.') -@click.option('--ratio_pollution', type=float, default=0.0, - help='Pollution ratio of unlabeled training data with unknown (unlabeled) anomalies.') -@click.option('--device', type=str, default='cuda', help='Computation device to use ("cpu", "cuda", "cuda:2", etc.).') -@click.option('--seed', type=int, default=-1, help='Set seed. If -1, use randomization.') -@click.option('--optimizer_name', type=click.Choice(['adam']), default='adam', - help='Name of the optimizer to use for training the Semi-Supervised Deep Generative model.') -@click.option('--lr', type=float, default=0.001, - help='Initial learning rate for training. Default=0.001') -@click.option('--n_epochs', type=int, default=50, help='Number of epochs to train.') -@click.option('--lr_milestone', type=int, default=0, multiple=True, - help='Lr scheduler milestones at which lr is multiplied by 0.1. Can be multiple and must be increasing.') -@click.option('--batch_size', type=int, default=128, help='Batch size for mini-batch training.') -@click.option('--weight_decay', type=float, default=1e-6, - help='Weight decay (L2 penalty) hyperparameter.') -@click.option('--pretrain', type=bool, default=False, help='Pretrain a variational autoencoder.') -@click.option('--vae_optimizer_name', type=click.Choice(['adam']), default='adam', - help='Name of the optimizer to use for variational autoencoder pretraining.') -@click.option('--vae_lr', type=float, default=0.001, - help='Initial learning rate for pretraining. Default=0.001') -@click.option('--vae_n_epochs', type=int, default=100, help='Number of epochs to train the variational autoencoder.') -@click.option('--vae_lr_milestone', type=int, default=0, multiple=True, - help='Lr scheduler milestones at which lr is multiplied by 0.1. Can be multiple and must be increasing.') -@click.option('--vae_batch_size', type=int, default=128, help='Batch size for variational autoencoder training.') -@click.option('--vae_weight_decay', type=float, default=1e-6, - help='Weight decay (L2 penalty) hyperparameter for variational autoencoder.') -@click.option('--num_threads', type=int, default=0, - help='Number of threads used for parallelizing CPU operations. 0 means that all resources are used.') -@click.option('--n_jobs_dataloader', type=int, default=0, - help='Number of workers for data loading. 0 means that the data will be loaded in the main process.') -@click.option('--normal_class', type=int, default=0, - help='Specify the normal class of the dataset (all other classes are considered anomalous).') -@click.option('--known_outlier_class', type=int, default=1, - help='Specify the known outlier class of the dataset for semi-supervised anomaly detection.') -@click.option('--n_known_outlier_classes', type=int, default=0, - help='Number of known outlier classes.' - 'If 0, no anomalies are known.' - 'If 1, outlier class as specified in --known_outlier_class option.' - 'If > 1, the specified number of outlier classes will be sampled at random.') -def main(dataset_name, net_name, xp_path, data_path, load_config, load_model, ratio_known_normal, ratio_known_outlier, - ratio_pollution, device, seed, optimizer_name, lr, n_epochs, lr_milestone, batch_size, weight_decay, pretrain, - vae_optimizer_name, vae_lr, vae_n_epochs, vae_lr_milestone, vae_batch_size, vae_weight_decay, - num_threads, n_jobs_dataloader, normal_class, known_outlier_class, n_known_outlier_classes): +@click.argument( + "dataset_name", + type=click.Choice( + [ + "mnist", + "fmnist", + "cifar10", + "arrhythmia", + "cardio", + "satellite", + "satimage-2", + "shuttle", + "thyroid", + ] + ), +) +@click.argument( + "net_name", + type=click.Choice( + [ + "mnist_DGM_M2", + "mnist_DGM_M1M2", + "fmnist_DGM_M2", + "fmnist_DGM_M1M2", + "cifar10_DGM_M2", + "cifar10_DGM_M1M2", + "arrhythmia_DGM_M2", + "cardio_DGM_M2", + "satellite_DGM_M2", + "satimage-2_DGM_M2", + "shuttle_DGM_M2", + "thyroid_DGM_M2", + ] + ), +) +@click.argument("xp_path", type=click.Path(exists=True)) +@click.argument("data_path", type=click.Path(exists=True)) +@click.option( + "--load_config", + type=click.Path(exists=True), + default=None, + help="Config JSON-file path (default: None).", +) +@click.option( + "--load_model", + type=click.Path(exists=True), + default=None, + help="Model file path (default: None).", +) +@click.option( + "--ratio_known_normal", + type=float, + default=0.0, + help="Ratio of known (labeled) normal training examples.", +) +@click.option( + "--ratio_known_outlier", + type=float, + default=0.0, + help="Ratio of known (labeled) anomalous training examples.", +) +@click.option( + "--ratio_pollution", + type=float, + default=0.0, + help="Pollution ratio of unlabeled training data with unknown (unlabeled) anomalies.", +) +@click.option( + "--device", + type=str, + default="cuda", + help='Computation device to use ("cpu", "cuda", "cuda:2", etc.).', +) +@click.option( + "--seed", type=int, default=-1, help="Set seed. If -1, use randomization." +) +@click.option( + "--optimizer_name", + type=click.Choice(["adam"]), + default="adam", + help="Name of the optimizer to use for training the Semi-Supervised Deep Generative model.", +) +@click.option( + "--lr", + type=float, + default=0.001, + help="Initial learning rate for training. Default=0.001", +) +@click.option("--n_epochs", type=int, default=50, help="Number of epochs to train.") +@click.option( + "--lr_milestone", + type=int, + default=0, + multiple=True, + help="Lr scheduler milestones at which lr is multiplied by 0.1. Can be multiple and must be increasing.", +) +@click.option( + "--batch_size", type=int, default=128, help="Batch size for mini-batch training." +) +@click.option( + "--weight_decay", + type=float, + default=1e-6, + help="Weight decay (L2 penalty) hyperparameter.", +) +@click.option( + "--pretrain", type=bool, default=False, help="Pretrain a variational autoencoder." +) +@click.option( + "--vae_optimizer_name", + type=click.Choice(["adam"]), + default="adam", + help="Name of the optimizer to use for variational autoencoder pretraining.", +) +@click.option( + "--vae_lr", + type=float, + default=0.001, + help="Initial learning rate for pretraining. Default=0.001", +) +@click.option( + "--vae_n_epochs", + type=int, + default=100, + help="Number of epochs to train the variational autoencoder.", +) +@click.option( + "--vae_lr_milestone", + type=int, + default=0, + multiple=True, + help="Lr scheduler milestones at which lr is multiplied by 0.1. Can be multiple and must be increasing.", +) +@click.option( + "--vae_batch_size", + type=int, + default=128, + help="Batch size for variational autoencoder training.", +) +@click.option( + "--vae_weight_decay", + type=float, + default=1e-6, + help="Weight decay (L2 penalty) hyperparameter for variational autoencoder.", +) +@click.option( + "--num_threads", + type=int, + default=0, + help="Number of threads used for parallelizing CPU operations. 0 means that all resources are used.", +) +@click.option( + "--n_jobs_dataloader", + type=int, + default=0, + help="Number of workers for data loading. 0 means that the data will be loaded in the main process.", +) +@click.option( + "--normal_class", + type=int, + default=0, + help="Specify the normal class of the dataset (all other classes are considered anomalous).", +) +@click.option( + "--known_outlier_class", + type=int, + default=1, + help="Specify the known outlier class of the dataset for semi-supervised anomaly detection.", +) +@click.option( + "--n_known_outlier_classes", + type=int, + default=0, + help="Number of known outlier classes." + "If 0, no anomalies are known." + "If 1, outlier class as specified in --known_outlier_class option." + "If > 1, the specified number of outlier classes will be sampled at random.", +) +def main( + dataset_name, + net_name, + xp_path, + data_path, + load_config, + load_model, + ratio_known_normal, + ratio_known_outlier, + ratio_pollution, + device, + seed, + optimizer_name, + lr, + n_epochs, + lr_milestone, + batch_size, + weight_decay, + pretrain, + vae_optimizer_name, + vae_lr, + vae_n_epochs, + vae_lr_milestone, + vae_batch_size, + vae_weight_decay, + num_threads, + n_jobs_dataloader, + normal_class, + known_outlier_class, + n_known_outlier_classes, +): """ Semi-Supervised Deep Generative model (M1+M2 model) from Kingma et al. (2014) @@ -88,64 +239,78 @@ def main(dataset_name, net_name, xp_path, data_path, load_config, load_model, ra logging.basicConfig(level=logging.INFO) logger = logging.getLogger() logger.setLevel(logging.INFO) - formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') - log_file = xp_path + '/log.txt' + formatter = logging.Formatter( + "%(asctime)s - %(name)s - %(levelname)s - %(message)s" + ) + log_file = xp_path + "/log.txt" file_handler = logging.FileHandler(log_file) file_handler.setLevel(logging.INFO) file_handler.setFormatter(formatter) logger.addHandler(file_handler) # Print paths - logger.info('Log file is %s' % log_file) - logger.info('Data path is %s' % data_path) - logger.info('Export path is %s' % xp_path) + logger.info("Log file is %s" % log_file) + logger.info("Data path is %s" % data_path) + logger.info("Export path is %s" % xp_path) # Print experimental setup - logger.info('Dataset: %s' % dataset_name) - logger.info('Normal class: %d' % normal_class) - logger.info('Ratio of labeled normal train samples: %.2f' % ratio_known_normal) - logger.info('Ratio of labeled anomalous samples: %.2f' % ratio_known_outlier) - logger.info('Pollution ratio of unlabeled train data: %.2f' % ratio_pollution) + logger.info("Dataset: %s" % dataset_name) + logger.info("Normal class: %d" % normal_class) + logger.info("Ratio of labeled normal train samples: %.2f" % ratio_known_normal) + logger.info("Ratio of labeled anomalous samples: %.2f" % ratio_known_outlier) + logger.info("Pollution ratio of unlabeled train data: %.2f" % ratio_pollution) if n_known_outlier_classes == 1: - logger.info('Known anomaly class: %d' % known_outlier_class) + logger.info("Known anomaly class: %d" % known_outlier_class) else: - logger.info('Number of known anomaly classes: %d' % n_known_outlier_classes) - logger.info('Network: %s' % net_name) + logger.info("Number of known anomaly classes: %d" % n_known_outlier_classes) + logger.info("Network: %s" % net_name) # If specified, load experiment config from JSON-file if load_config: cfg.load_config(import_json=load_config) - logger.info('Loaded configuration from %s.' % load_config) + logger.info("Loaded configuration from %s." % load_config) # Set seed - if cfg.settings['seed'] != -1: - random.seed(cfg.settings['seed']) - np.random.seed(cfg.settings['seed']) - torch.manual_seed(cfg.settings['seed']) - torch.cuda.manual_seed(cfg.settings['seed']) + if cfg.settings["seed"] != -1: + random.seed(cfg.settings["seed"]) + np.random.seed(cfg.settings["seed"]) + torch.manual_seed(cfg.settings["seed"]) + torch.cuda.manual_seed(cfg.settings["seed"]) torch.backends.cudnn.deterministic = True - logger.info('Set seed to %d.' % cfg.settings['seed']) + logger.info("Set seed to %d." % cfg.settings["seed"]) # Default device to 'cpu' if cuda is not available if not torch.cuda.is_available(): - device = 'cpu' + device = "cpu" # Set the number of threads used for parallelizing CPU operations if num_threads > 0: torch.set_num_threads(num_threads) - logger.info('Computation device: %s' % device) - logger.info('Number of threads: %d' % num_threads) - logger.info('Number of dataloader workers: %d' % n_jobs_dataloader) + logger.info("Computation device: %s" % device) + logger.info("Number of threads: %d" % num_threads) + logger.info("Number of dataloader workers: %d" % n_jobs_dataloader) # Load data - dataset = load_dataset(dataset_name, data_path, normal_class, known_outlier_class, n_known_outlier_classes, - ratio_known_normal, ratio_known_outlier, ratio_pollution, - random_state=np.random.RandomState(cfg.settings['seed'])) + dataset = load_dataset( + dataset_name, + data_path, + normal_class, + known_outlier_class, + n_known_outlier_classes, + ratio_known_normal, + ratio_known_outlier, + ratio_pollution, + random_state=np.random.RandomState(cfg.settings["seed"]), + ) # Log random sample of known anomaly classes if more than 1 class if n_known_outlier_classes > 1: - logger.info('Known anomaly classes: %s' % (dataset.known_outlier_classes,)) + logger.info("Known anomaly classes: %s" % (dataset.known_outlier_classes,)) # Initialize semiDGM model and set neural network phi - alpha = 0.1 * (1 - ratio_known_normal - ratio_known_outlier) / (ratio_known_normal + ratio_known_outlier) + alpha = ( + 0.1 + * (1 - ratio_known_normal - ratio_known_outlier) + / (ratio_known_normal + ratio_known_outlier) + ) semiDGM = SemiDeepGenerativeModel(alpha=alpha) # If specified, load model @@ -155,86 +320,118 @@ def main(dataset_name, net_name, xp_path, data_path, load_config, load_model, ra semiDGM.set_network(net_name) # Load model semiDGM.load_model(model_path=load_model) - logger.info('Loading model from %s.' % load_model) + logger.info("Loading model from %s." % load_model) - logger.info('Pretraining: %s' % pretrain) + logger.info("Pretraining: %s" % pretrain) if pretrain: # Log pretraining details - logger.info('Pretraining optimizer: %s' % cfg.settings['vae_optimizer_name']) - logger.info('Pretraining learning rate: %g' % cfg.settings['vae_lr']) - logger.info('Pretraining epochs: %d' % cfg.settings['vae_n_epochs']) - logger.info('Pretraining learning rate scheduler milestones: %s' % (cfg.settings['vae_lr_milestone'],)) - logger.info('Pretraining batch size: %d' % cfg.settings['vae_batch_size']) - logger.info('Pretraining weight decay: %g' % cfg.settings['vae_weight_decay']) + logger.info("Pretraining optimizer: %s" % cfg.settings["vae_optimizer_name"]) + logger.info("Pretraining learning rate: %g" % cfg.settings["vae_lr"]) + logger.info("Pretraining epochs: %d" % cfg.settings["vae_n_epochs"]) + logger.info( + "Pretraining learning rate scheduler milestones: %s" + % (cfg.settings["vae_lr_milestone"],) + ) + logger.info("Pretraining batch size: %d" % cfg.settings["vae_batch_size"]) + logger.info("Pretraining weight decay: %g" % cfg.settings["vae_weight_decay"]) # Pretrain model on dataset (via variational autoencoder) semiDGM.set_vae(net_name) - semiDGM.pretrain(dataset, - optimizer_name=cfg.settings['vae_optimizer_name'], - lr=cfg.settings['vae_lr'], - n_epochs=cfg.settings['vae_n_epochs'], - lr_milestones=cfg.settings['vae_lr_milestone'], - batch_size=cfg.settings['vae_batch_size'], - weight_decay=cfg.settings['vae_weight_decay'], - device=device, - n_jobs_dataloader=n_jobs_dataloader) + semiDGM.pretrain( + dataset, + optimizer_name=cfg.settings["vae_optimizer_name"], + lr=cfg.settings["vae_lr"], + n_epochs=cfg.settings["vae_n_epochs"], + lr_milestones=cfg.settings["vae_lr_milestone"], + batch_size=cfg.settings["vae_batch_size"], + weight_decay=cfg.settings["vae_weight_decay"], + device=device, + n_jobs_dataloader=n_jobs_dataloader, + ) # Save pretraining results - semiDGM.save_vae_results(export_json=xp_path + '/vae_results.json') + semiDGM.save_vae_results(export_json=xp_path + "/vae_results.json") # Log training details - logger.info('Training optimizer: %s' % cfg.settings['optimizer_name']) - logger.info('Training learning rate: %g' % cfg.settings['lr']) - logger.info('Training epochs: %d' % cfg.settings['n_epochs']) - logger.info('Training learning rate scheduler milestones: %s' % (cfg.settings['lr_milestone'],)) - logger.info('Training batch size: %d' % cfg.settings['batch_size']) - logger.info('Training weight decay: %g' % cfg.settings['weight_decay']) + logger.info("Training optimizer: %s" % cfg.settings["optimizer_name"]) + logger.info("Training learning rate: %g" % cfg.settings["lr"]) + logger.info("Training epochs: %d" % cfg.settings["n_epochs"]) + logger.info( + "Training learning rate scheduler milestones: %s" + % (cfg.settings["lr_milestone"],) + ) + logger.info("Training batch size: %d" % cfg.settings["batch_size"]) + logger.info("Training weight decay: %g" % cfg.settings["weight_decay"]) # Train model on dataset semiDGM.set_network(net_name) - semiDGM.train(dataset, - optimizer_name=cfg.settings['optimizer_name'], - lr=cfg.settings['lr'], - n_epochs=cfg.settings['n_epochs'], - lr_milestones=cfg.settings['lr_milestone'], - batch_size=cfg.settings['batch_size'], - weight_decay=cfg.settings['weight_decay'], - device=device, - n_jobs_dataloader=n_jobs_dataloader) + semiDGM.train( + dataset, + optimizer_name=cfg.settings["optimizer_name"], + lr=cfg.settings["lr"], + n_epochs=cfg.settings["n_epochs"], + lr_milestones=cfg.settings["lr_milestone"], + batch_size=cfg.settings["batch_size"], + weight_decay=cfg.settings["weight_decay"], + device=device, + n_jobs_dataloader=n_jobs_dataloader, + ) # Test model semiDGM.test(dataset, device=device, n_jobs_dataloader=n_jobs_dataloader) # Save results, model, and configuration - semiDGM.save_results(export_json=xp_path + '/results.json') - semiDGM.save_model(export_model=xp_path + '/model.tar') - cfg.save_config(export_json=xp_path + '/config.json') + semiDGM.save_results(export_json=xp_path + "/results.json") + semiDGM.save_model(export_model=xp_path + "/model.tar") + cfg.save_config(export_json=xp_path + "/config.json") # Plot most anomalous and most normal test samples - indices, labels, scores = zip(*semiDGM.results['test_scores']) + indices, labels, scores = zip(*semiDGM.results["test_scores"]) indices, labels, scores = np.array(indices), np.array(labels), np.array(scores) idx_all_sorted = indices[np.argsort(scores)] # from lowest to highest score - idx_normal_sorted = indices[labels == 0][np.argsort(scores[labels == 0])] # from lowest to highest score + idx_normal_sorted = indices[labels == 0][ + np.argsort(scores[labels == 0]) + ] # from lowest to highest score - if dataset_name in ('mnist', 'fmnist', 'cifar10'): + if dataset_name in ("mnist", "fmnist", "cifar10"): - if dataset_name in ('mnist', 'fmnist'): + if dataset_name in ("mnist", "fmnist"): X_all_low = dataset.test_set.data[idx_all_sorted[:32], ...].unsqueeze(1) X_all_high = dataset.test_set.data[idx_all_sorted[-32:], ...].unsqueeze(1) - X_normal_low = dataset.test_set.data[idx_normal_sorted[:32], ...].unsqueeze(1) - X_normal_high = dataset.test_set.data[idx_normal_sorted[-32:], ...].unsqueeze(1) + X_normal_low = dataset.test_set.data[idx_normal_sorted[:32], ...].unsqueeze( + 1 + ) + X_normal_high = dataset.test_set.data[ + idx_normal_sorted[-32:], ... + ].unsqueeze(1) - if dataset_name == 'cifar10': - X_all_low = torch.tensor(np.transpose(dataset.test_set.data[idx_all_sorted[:32], ...], (0,3,1,2))) - X_all_high = torch.tensor(np.transpose(dataset.test_set.data[idx_all_sorted[-32:], ...], (0,3,1,2))) - X_normal_low = torch.tensor(np.transpose(dataset.test_set.data[idx_normal_sorted[:32], ...], (0,3,1,2))) - X_normal_high = torch.tensor(np.transpose(dataset.test_set.data[idx_normal_sorted[-32:], ...], (0,3,1,2))) + if dataset_name == "cifar10": + X_all_low = torch.tensor( + np.transpose( + dataset.test_set.data[idx_all_sorted[:32], ...], (0, 3, 1, 2) + ) + ) + X_all_high = torch.tensor( + np.transpose( + dataset.test_set.data[idx_all_sorted[-32:], ...], (0, 3, 1, 2) + ) + ) + X_normal_low = torch.tensor( + np.transpose( + dataset.test_set.data[idx_normal_sorted[:32], ...], (0, 3, 1, 2) + ) + ) + X_normal_high = torch.tensor( + np.transpose( + dataset.test_set.data[idx_normal_sorted[-32:], ...], (0, 3, 1, 2) + ) + ) - plot_images_grid(X_all_low, export_img=xp_path + '/all_low', padding=2) - plot_images_grid(X_all_high, export_img=xp_path + '/all_high', padding=2) - plot_images_grid(X_normal_low, export_img=xp_path + '/normals_low', padding=2) - plot_images_grid(X_normal_high, export_img=xp_path + '/normals_high', padding=2) + plot_images_grid(X_all_low, export_img=xp_path + "/all_low", padding=2) + plot_images_grid(X_all_high, export_img=xp_path + "/all_high", padding=2) + plot_images_grid(X_normal_low, export_img=xp_path + "/normals_low", padding=2) + plot_images_grid(X_normal_high, export_img=xp_path + "/normals_high", padding=2) -if __name__ == '__main__': +if __name__ == "__main__": main() diff --git a/Deep-SAD-PyTorch/src/baseline_isoforest.py b/Deep-SAD-PyTorch/src/baseline_isoforest.py index 1b25d6e..b870f01 100644 --- a/Deep-SAD-PyTorch/src/baseline_isoforest.py +++ b/Deep-SAD-PyTorch/src/baseline_isoforest.py @@ -14,46 +14,138 @@ from datasets.main import load_dataset # Settings ################################################################################ @click.command() -@click.argument('dataset_name', type=click.Choice(['mnist', 'fmnist', 'cifar10', 'arrhythmia', 'cardio', 'satellite', - 'satimage-2', 'shuttle', 'thyroid'])) -@click.argument('xp_path', type=click.Path(exists=True)) -@click.argument('data_path', type=click.Path(exists=True)) -@click.option('--load_config', type=click.Path(exists=True), default=None, - help='Config JSON-file path (default: None).') -@click.option('--load_model', type=click.Path(exists=True), default=None, - help='Model file path (default: None).') -@click.option('--ratio_known_normal', type=float, default=0.0, - help='Ratio of known (labeled) normal training examples.') -@click.option('--ratio_known_outlier', type=float, default=0.0, - help='Ratio of known (labeled) anomalous training examples.') -@click.option('--ratio_pollution', type=float, default=0.0, - help='Pollution ratio of unlabeled training data with unknown (unlabeled) anomalies.') -@click.option('--seed', type=int, default=-1, help='Set seed. If -1, use randomization.') -@click.option('--n_estimators', type=int, default=100, - help='Set the number of base estimators in the ensemble (default: 100).') -@click.option('--max_samples', type=int, default=256, - help='Set the number of samples drawn to train each base estimator (default: 256).') -@click.option('--contamination', type=float, default=0.1, - help='Expected fraction of anomalies in the training set. (default: 0.1).') -@click.option('--n_jobs_model', type=int, default=-1, help='Number of jobs for model training.') -@click.option('--hybrid', type=bool, default=False, - help='Train model on features extracted from an autoencoder. If True, load_ae must be specified.') -@click.option('--load_ae', type=click.Path(exists=True), default=None, - help='Model file path to load autoencoder weights (default: None).') -@click.option('--n_jobs_dataloader', type=int, default=0, - help='Number of workers for data loading. 0 means that the data will be loaded in the main process.') -@click.option('--normal_class', type=int, default=0, - help='Specify the normal class of the dataset (all other classes are considered anomalous).') -@click.option('--known_outlier_class', type=int, default=1, - help='Specify the known outlier class of the dataset for semi-supervised anomaly detection.') -@click.option('--n_known_outlier_classes', type=int, default=0, - help='Number of known outlier classes.' - 'If 0, no anomalies are known.' - 'If 1, outlier class as specified in --known_outlier_class option.' - 'If > 1, the specified number of outlier classes will be sampled at random.') -def main(dataset_name, xp_path, data_path, load_config, load_model, ratio_known_normal, ratio_known_outlier, - ratio_pollution, seed, n_estimators, max_samples, contamination, n_jobs_model, hybrid, load_ae, - n_jobs_dataloader, normal_class, known_outlier_class, n_known_outlier_classes): +@click.argument( + "dataset_name", + type=click.Choice( + [ + "mnist", + "fmnist", + "cifar10", + "arrhythmia", + "cardio", + "satellite", + "satimage-2", + "shuttle", + "thyroid", + ] + ), +) +@click.argument("xp_path", type=click.Path(exists=True)) +@click.argument("data_path", type=click.Path(exists=True)) +@click.option( + "--load_config", + type=click.Path(exists=True), + default=None, + help="Config JSON-file path (default: None).", +) +@click.option( + "--load_model", + type=click.Path(exists=True), + default=None, + help="Model file path (default: None).", +) +@click.option( + "--ratio_known_normal", + type=float, + default=0.0, + help="Ratio of known (labeled) normal training examples.", +) +@click.option( + "--ratio_known_outlier", + type=float, + default=0.0, + help="Ratio of known (labeled) anomalous training examples.", +) +@click.option( + "--ratio_pollution", + type=float, + default=0.0, + help="Pollution ratio of unlabeled training data with unknown (unlabeled) anomalies.", +) +@click.option( + "--seed", type=int, default=-1, help="Set seed. If -1, use randomization." +) +@click.option( + "--n_estimators", + type=int, + default=100, + help="Set the number of base estimators in the ensemble (default: 100).", +) +@click.option( + "--max_samples", + type=int, + default=256, + help="Set the number of samples drawn to train each base estimator (default: 256).", +) +@click.option( + "--contamination", + type=float, + default=0.1, + help="Expected fraction of anomalies in the training set. (default: 0.1).", +) +@click.option( + "--n_jobs_model", type=int, default=-1, help="Number of jobs for model training." +) +@click.option( + "--hybrid", + type=bool, + default=False, + help="Train model on features extracted from an autoencoder. If True, load_ae must be specified.", +) +@click.option( + "--load_ae", + type=click.Path(exists=True), + default=None, + help="Model file path to load autoencoder weights (default: None).", +) +@click.option( + "--n_jobs_dataloader", + type=int, + default=0, + help="Number of workers for data loading. 0 means that the data will be loaded in the main process.", +) +@click.option( + "--normal_class", + type=int, + default=0, + help="Specify the normal class of the dataset (all other classes are considered anomalous).", +) +@click.option( + "--known_outlier_class", + type=int, + default=1, + help="Specify the known outlier class of the dataset for semi-supervised anomaly detection.", +) +@click.option( + "--n_known_outlier_classes", + type=int, + default=0, + help="Number of known outlier classes." + "If 0, no anomalies are known." + "If 1, outlier class as specified in --known_outlier_class option." + "If > 1, the specified number of outlier classes will be sampled at random.", +) +def main( + dataset_name, + xp_path, + data_path, + load_config, + load_model, + ratio_known_normal, + ratio_known_outlier, + ratio_pollution, + seed, + n_estimators, + max_samples, + contamination, + n_jobs_model, + hybrid, + load_ae, + n_jobs_dataloader, + normal_class, + known_outlier_class, + n_known_outlier_classes, +): """ (Hybrid) Isolation Forest model for anomaly detection. @@ -69,78 +161,100 @@ def main(dataset_name, xp_path, data_path, load_config, load_model, ratio_known_ logging.basicConfig(level=logging.INFO) logger = logging.getLogger() logger.setLevel(logging.INFO) - formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') - log_file = xp_path + '/log.txt' + formatter = logging.Formatter( + "%(asctime)s - %(name)s - %(levelname)s - %(message)s" + ) + log_file = xp_path + "/log.txt" file_handler = logging.FileHandler(log_file) file_handler.setLevel(logging.INFO) file_handler.setFormatter(formatter) logger.addHandler(file_handler) # Print paths - logger.info('Log file is %s.' % log_file) - logger.info('Data path is %s.' % data_path) - logger.info('Export path is %s.' % xp_path) + logger.info("Log file is %s." % log_file) + logger.info("Data path is %s." % data_path) + logger.info("Export path is %s." % xp_path) # Print experimental setup - logger.info('Dataset: %s' % dataset_name) - logger.info('Normal class: %d' % normal_class) - logger.info('Ratio of labeled normal train samples: %.2f' % ratio_known_normal) - logger.info('Ratio of labeled anomalous samples: %.2f' % ratio_known_outlier) - logger.info('Pollution ratio of unlabeled train data: %.2f' % ratio_pollution) + logger.info("Dataset: %s" % dataset_name) + logger.info("Normal class: %d" % normal_class) + logger.info("Ratio of labeled normal train samples: %.2f" % ratio_known_normal) + logger.info("Ratio of labeled anomalous samples: %.2f" % ratio_known_outlier) + logger.info("Pollution ratio of unlabeled train data: %.2f" % ratio_pollution) if n_known_outlier_classes == 1: - logger.info('Known anomaly class: %d' % known_outlier_class) + logger.info("Known anomaly class: %d" % known_outlier_class) else: - logger.info('Number of known anomaly classes: %d' % n_known_outlier_classes) + logger.info("Number of known anomaly classes: %d" % n_known_outlier_classes) # If specified, load experiment config from JSON-file if load_config: cfg.load_config(import_json=load_config) - logger.info('Loaded configuration from %s.' % load_config) + logger.info("Loaded configuration from %s." % load_config) # Print Isolation Forest configuration - logger.info('Number of base estimators in the ensemble: %d' % cfg.settings['n_estimators']) - logger.info('Number of samples for training each base estimator: %d' % cfg.settings['max_samples']) - logger.info('Contamination parameter: %.2f' % cfg.settings['contamination']) - logger.info('Number of jobs for model training: %d' % n_jobs_model) - logger.info('Hybrid model: %s' % cfg.settings['hybrid']) + logger.info( + "Number of base estimators in the ensemble: %d" % cfg.settings["n_estimators"] + ) + logger.info( + "Number of samples for training each base estimator: %d" + % cfg.settings["max_samples"] + ) + logger.info("Contamination parameter: %.2f" % cfg.settings["contamination"]) + logger.info("Number of jobs for model training: %d" % n_jobs_model) + logger.info("Hybrid model: %s" % cfg.settings["hybrid"]) # Set seed - if cfg.settings['seed'] != -1: - random.seed(cfg.settings['seed']) - np.random.seed(cfg.settings['seed']) - torch.manual_seed(cfg.settings['seed']) - torch.cuda.manual_seed(cfg.settings['seed']) + if cfg.settings["seed"] != -1: + random.seed(cfg.settings["seed"]) + np.random.seed(cfg.settings["seed"]) + torch.manual_seed(cfg.settings["seed"]) + torch.cuda.manual_seed(cfg.settings["seed"]) torch.backends.cudnn.deterministic = True - logger.info('Set seed to %d.' % cfg.settings['seed']) + logger.info("Set seed to %d." % cfg.settings["seed"]) # Use 'cpu' as device for Isolation Forest - device = 'cpu' - torch.multiprocessing.set_sharing_strategy('file_system') # fix multiprocessing issue for ubuntu - logger.info('Computation device: %s' % device) - logger.info('Number of dataloader workers: %d' % n_jobs_dataloader) + device = "cpu" + torch.multiprocessing.set_sharing_strategy( + "file_system" + ) # fix multiprocessing issue for ubuntu + logger.info("Computation device: %s" % device) + logger.info("Number of dataloader workers: %d" % n_jobs_dataloader) # Load data - dataset = load_dataset(dataset_name, data_path, normal_class, known_outlier_class, n_known_outlier_classes, - ratio_known_normal, ratio_known_outlier, ratio_pollution, - random_state=np.random.RandomState(cfg.settings['seed'])) + dataset = load_dataset( + dataset_name, + data_path, + normal_class, + known_outlier_class, + n_known_outlier_classes, + ratio_known_normal, + ratio_known_outlier, + ratio_pollution, + random_state=np.random.RandomState(cfg.settings["seed"]), + ) # Log random sample of known anomaly classes if more than 1 class if n_known_outlier_classes > 1: - logger.info('Known anomaly classes: %s' % (dataset.known_outlier_classes,)) + logger.info("Known anomaly classes: %s" % (dataset.known_outlier_classes,)) # Initialize Isolation Forest model - Isoforest = IsoForest(hybrid=cfg.settings['hybrid'], n_estimators=cfg.settings['n_estimators'], - max_samples=cfg.settings['max_samples'], contamination=cfg.settings['contamination'], - n_jobs=n_jobs_model, seed=cfg.settings['seed']) + Isoforest = IsoForest( + hybrid=cfg.settings["hybrid"], + n_estimators=cfg.settings["n_estimators"], + max_samples=cfg.settings["max_samples"], + contamination=cfg.settings["contamination"], + n_jobs=n_jobs_model, + seed=cfg.settings["seed"], + ) # If specified, load model parameters from already trained model if load_model: Isoforest.load_model(import_path=load_model, device=device) - logger.info('Loading model from %s.' % load_model) + logger.info("Loading model from %s." % load_model) # If specified, load model autoencoder weights for a hybrid approach if hybrid and load_ae is not None: Isoforest.load_ae(dataset_name, model_path=load_ae) - logger.info('Loaded pretrained autoencoder for features from %s.' % load_ae) + logger.info("Loaded pretrained autoencoder for features from %s." % load_ae) # Train model on dataset Isoforest.train(dataset, device=device, n_jobs_dataloader=n_jobs_dataloader) @@ -149,35 +263,56 @@ def main(dataset_name, xp_path, data_path, load_config, load_model, ratio_known_ Isoforest.test(dataset, device=device, n_jobs_dataloader=n_jobs_dataloader) # Save results and configuration - Isoforest.save_results(export_json=xp_path + '/results.json') - cfg.save_config(export_json=xp_path + '/config.json') + Isoforest.save_results(export_json=xp_path + "/results.json") + cfg.save_config(export_json=xp_path + "/config.json") # Plot most anomalous and most normal test samples - indices, labels, scores = zip(*Isoforest.results['test_scores']) + indices, labels, scores = zip(*Isoforest.results["test_scores"]) indices, labels, scores = np.array(indices), np.array(labels), np.array(scores) idx_all_sorted = indices[np.argsort(scores)] # from lowest to highest score - idx_normal_sorted = indices[labels == 0][np.argsort(scores[labels == 0])] # from lowest to highest score + idx_normal_sorted = indices[labels == 0][ + np.argsort(scores[labels == 0]) + ] # from lowest to highest score - if dataset_name in ('mnist', 'fmnist', 'cifar10'): + if dataset_name in ("mnist", "fmnist", "cifar10"): - if dataset_name in ('mnist', 'fmnist'): + if dataset_name in ("mnist", "fmnist"): X_all_low = dataset.test_set.data[idx_all_sorted[:32], ...].unsqueeze(1) X_all_high = dataset.test_set.data[idx_all_sorted[-32:], ...].unsqueeze(1) - X_normal_low = dataset.test_set.data[idx_normal_sorted[:32], ...].unsqueeze(1) - X_normal_high = dataset.test_set.data[idx_normal_sorted[-32:], ...].unsqueeze(1) + X_normal_low = dataset.test_set.data[idx_normal_sorted[:32], ...].unsqueeze( + 1 + ) + X_normal_high = dataset.test_set.data[ + idx_normal_sorted[-32:], ... + ].unsqueeze(1) - if dataset_name == 'cifar10': - X_all_low = torch.tensor(np.transpose(dataset.test_set.data[idx_all_sorted[:32], ...], (0, 3, 1, 2))) - X_all_high = torch.tensor(np.transpose(dataset.test_set.data[idx_all_sorted[-32:], ...], (0, 3, 1, 2))) - X_normal_low = torch.tensor(np.transpose(dataset.test_set.data[idx_normal_sorted[:32], ...], (0, 3, 1, 2))) + if dataset_name == "cifar10": + X_all_low = torch.tensor( + np.transpose( + dataset.test_set.data[idx_all_sorted[:32], ...], (0, 3, 1, 2) + ) + ) + X_all_high = torch.tensor( + np.transpose( + dataset.test_set.data[idx_all_sorted[-32:], ...], (0, 3, 1, 2) + ) + ) + X_normal_low = torch.tensor( + np.transpose( + dataset.test_set.data[idx_normal_sorted[:32], ...], (0, 3, 1, 2) + ) + ) X_normal_high = torch.tensor( - np.transpose(dataset.test_set.data[idx_normal_sorted[-32:], ...], (0, 3, 1, 2))) + np.transpose( + dataset.test_set.data[idx_normal_sorted[-32:], ...], (0, 3, 1, 2) + ) + ) - plot_images_grid(X_all_low, export_img=xp_path + '/all_low', padding=2) - plot_images_grid(X_all_high, export_img=xp_path + '/all_high', padding=2) - plot_images_grid(X_normal_low, export_img=xp_path + '/normals_low', padding=2) - plot_images_grid(X_normal_high, export_img=xp_path + '/normals_high', padding=2) + plot_images_grid(X_all_low, export_img=xp_path + "/all_low", padding=2) + plot_images_grid(X_all_high, export_img=xp_path + "/all_high", padding=2) + plot_images_grid(X_normal_low, export_img=xp_path + "/normals_low", padding=2) + plot_images_grid(X_normal_high, export_img=xp_path + "/normals_high", padding=2) -if __name__ == '__main__': +if __name__ == "__main__": main() diff --git a/Deep-SAD-PyTorch/src/baseline_kde.py b/Deep-SAD-PyTorch/src/baseline_kde.py index f89e633..3a5474c 100644 --- a/Deep-SAD-PyTorch/src/baseline_kde.py +++ b/Deep-SAD-PyTorch/src/baseline_kde.py @@ -14,44 +14,133 @@ from datasets.main import load_dataset # Settings ################################################################################ @click.command() -@click.argument('dataset_name', type=click.Choice(['mnist', 'fmnist', 'cifar10', 'arrhythmia', 'cardio', 'satellite', - 'satimage-2', 'shuttle', 'thyroid'])) -@click.argument('xp_path', type=click.Path(exists=True)) -@click.argument('data_path', type=click.Path(exists=True)) -@click.option('--load_config', type=click.Path(exists=True), default=None, - help='Config JSON-file path (default: None).') -@click.option('--load_model', type=click.Path(exists=True), default=None, - help='Model file path (default: None).') -@click.option('--ratio_known_normal', type=float, default=0.0, - help='Ratio of known (labeled) normal training examples.') -@click.option('--ratio_known_outlier', type=float, default=0.0, - help='Ratio of known (labeled) anomalous training examples.') -@click.option('--ratio_pollution', type=float, default=0.0, - help='Pollution ratio of unlabeled training data with unknown (unlabeled) anomalies.') -@click.option('--seed', type=int, default=-1, help='Set seed. If -1, use randomization.') -@click.option('--kernel', type=click.Choice(['gaussian', 'tophat', 'epanechnikov', 'exponential', 'linear', 'cosine']), - default='gaussian', help='Kernel for the KDE') -@click.option('--grid_search_cv', type=bool, default=True, - help='Use sklearn GridSearchCV to determine optimal bandwidth') -@click.option('--n_jobs_model', type=int, default=-1, help='Number of jobs for model training.') -@click.option('--hybrid', type=bool, default=False, - help='Train KDE on features extracted from an autoencoder. If True, load_ae must be specified.') -@click.option('--load_ae', type=click.Path(exists=True), default=None, - help='Model file path to load autoencoder weights (default: None).') -@click.option('--n_jobs_dataloader', type=int, default=0, - help='Number of workers for data loading. 0 means that the data will be loaded in the main process.') -@click.option('--normal_class', type=int, default=0, - help='Specify the normal class of the dataset (all other classes are considered anomalous).') -@click.option('--known_outlier_class', type=int, default=1, - help='Specify the known outlier class of the dataset for semi-supervised anomaly detection.') -@click.option('--n_known_outlier_classes', type=int, default=0, - help='Number of known outlier classes.' - 'If 0, no anomalies are known.' - 'If 1, outlier class as specified in --known_outlier_class option.' - 'If > 1, the specified number of outlier classes will be sampled at random.') -def main(dataset_name, xp_path, data_path, load_config, load_model, ratio_known_normal, ratio_known_outlier, - ratio_pollution, seed, kernel, grid_search_cv, n_jobs_model, hybrid, load_ae, n_jobs_dataloader, normal_class, - known_outlier_class, n_known_outlier_classes): +@click.argument( + "dataset_name", + type=click.Choice( + [ + "mnist", + "fmnist", + "cifar10", + "arrhythmia", + "cardio", + "satellite", + "satimage-2", + "shuttle", + "thyroid", + ] + ), +) +@click.argument("xp_path", type=click.Path(exists=True)) +@click.argument("data_path", type=click.Path(exists=True)) +@click.option( + "--load_config", + type=click.Path(exists=True), + default=None, + help="Config JSON-file path (default: None).", +) +@click.option( + "--load_model", + type=click.Path(exists=True), + default=None, + help="Model file path (default: None).", +) +@click.option( + "--ratio_known_normal", + type=float, + default=0.0, + help="Ratio of known (labeled) normal training examples.", +) +@click.option( + "--ratio_known_outlier", + type=float, + default=0.0, + help="Ratio of known (labeled) anomalous training examples.", +) +@click.option( + "--ratio_pollution", + type=float, + default=0.0, + help="Pollution ratio of unlabeled training data with unknown (unlabeled) anomalies.", +) +@click.option( + "--seed", type=int, default=-1, help="Set seed. If -1, use randomization." +) +@click.option( + "--kernel", + type=click.Choice( + ["gaussian", "tophat", "epanechnikov", "exponential", "linear", "cosine"] + ), + default="gaussian", + help="Kernel for the KDE", +) +@click.option( + "--grid_search_cv", + type=bool, + default=True, + help="Use sklearn GridSearchCV to determine optimal bandwidth", +) +@click.option( + "--n_jobs_model", type=int, default=-1, help="Number of jobs for model training." +) +@click.option( + "--hybrid", + type=bool, + default=False, + help="Train KDE on features extracted from an autoencoder. If True, load_ae must be specified.", +) +@click.option( + "--load_ae", + type=click.Path(exists=True), + default=None, + help="Model file path to load autoencoder weights (default: None).", +) +@click.option( + "--n_jobs_dataloader", + type=int, + default=0, + help="Number of workers for data loading. 0 means that the data will be loaded in the main process.", +) +@click.option( + "--normal_class", + type=int, + default=0, + help="Specify the normal class of the dataset (all other classes are considered anomalous).", +) +@click.option( + "--known_outlier_class", + type=int, + default=1, + help="Specify the known outlier class of the dataset for semi-supervised anomaly detection.", +) +@click.option( + "--n_known_outlier_classes", + type=int, + default=0, + help="Number of known outlier classes." + "If 0, no anomalies are known." + "If 1, outlier class as specified in --known_outlier_class option." + "If > 1, the specified number of outlier classes will be sampled at random.", +) +def main( + dataset_name, + xp_path, + data_path, + load_config, + load_model, + ratio_known_normal, + ratio_known_outlier, + ratio_pollution, + seed, + kernel, + grid_search_cv, + n_jobs_model, + hybrid, + load_ae, + n_jobs_dataloader, + normal_class, + known_outlier_class, + n_known_outlier_classes, +): """ (Hybrid) KDE for anomaly detection. @@ -67,114 +156,157 @@ def main(dataset_name, xp_path, data_path, load_config, load_model, ratio_known_ logging.basicConfig(level=logging.INFO) logger = logging.getLogger() logger.setLevel(logging.INFO) - formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') - log_file = xp_path + '/log.txt' + formatter = logging.Formatter( + "%(asctime)s - %(name)s - %(levelname)s - %(message)s" + ) + log_file = xp_path + "/log.txt" file_handler = logging.FileHandler(log_file) file_handler.setLevel(logging.INFO) file_handler.setFormatter(formatter) logger.addHandler(file_handler) # Print paths - logger.info('Log file is %s.' % log_file) - logger.info('Data path is %s.' % data_path) - logger.info('Export path is %s.' % xp_path) + logger.info("Log file is %s." % log_file) + logger.info("Data path is %s." % data_path) + logger.info("Export path is %s." % xp_path) # Print experimental setup - logger.info('Dataset: %s' % dataset_name) - logger.info('Normal class: %d' % normal_class) - logger.info('Ratio of labeled normal train samples: %.2f' % ratio_known_normal) - logger.info('Ratio of labeled anomalous samples: %.2f' % ratio_known_outlier) - logger.info('Pollution ratio of unlabeled train data: %.2f' % ratio_pollution) + logger.info("Dataset: %s" % dataset_name) + logger.info("Normal class: %d" % normal_class) + logger.info("Ratio of labeled normal train samples: %.2f" % ratio_known_normal) + logger.info("Ratio of labeled anomalous samples: %.2f" % ratio_known_outlier) + logger.info("Pollution ratio of unlabeled train data: %.2f" % ratio_pollution) if n_known_outlier_classes == 1: - logger.info('Known anomaly class: %d' % known_outlier_class) + logger.info("Known anomaly class: %d" % known_outlier_class) else: - logger.info('Number of known anomaly classes: %d' % n_known_outlier_classes) + logger.info("Number of known anomaly classes: %d" % n_known_outlier_classes) # If specified, load experiment config from JSON-file if load_config: cfg.load_config(import_json=load_config) - logger.info('Loaded configuration from %s.' % load_config) + logger.info("Loaded configuration from %s." % load_config) # Print KDE configuration - logger.info('KDE kernel: %s' % cfg.settings['kernel']) - logger.info('Use GridSearchCV for bandwidth selection: %s' % cfg.settings['grid_search_cv']) - logger.info('Number of jobs for model training: %d' % n_jobs_model) - logger.info('Hybrid model: %s' % cfg.settings['hybrid']) + logger.info("KDE kernel: %s" % cfg.settings["kernel"]) + logger.info( + "Use GridSearchCV for bandwidth selection: %s" % cfg.settings["grid_search_cv"] + ) + logger.info("Number of jobs for model training: %d" % n_jobs_model) + logger.info("Hybrid model: %s" % cfg.settings["hybrid"]) # Set seed - if cfg.settings['seed'] != -1: - random.seed(cfg.settings['seed']) - np.random.seed(cfg.settings['seed']) - torch.manual_seed(cfg.settings['seed']) - torch.cuda.manual_seed(cfg.settings['seed']) + if cfg.settings["seed"] != -1: + random.seed(cfg.settings["seed"]) + np.random.seed(cfg.settings["seed"]) + torch.manual_seed(cfg.settings["seed"]) + torch.cuda.manual_seed(cfg.settings["seed"]) torch.backends.cudnn.deterministic = True - logger.info('Set seed to %d.' % cfg.settings['seed']) + logger.info("Set seed to %d." % cfg.settings["seed"]) # Use 'cpu' as device for KDE - device = 'cpu' - torch.multiprocessing.set_sharing_strategy('file_system') # fix multiprocessing issue for ubuntu - logger.info('Computation device: %s' % device) - logger.info('Number of dataloader workers: %d' % n_jobs_dataloader) + device = "cpu" + torch.multiprocessing.set_sharing_strategy( + "file_system" + ) # fix multiprocessing issue for ubuntu + logger.info("Computation device: %s" % device) + logger.info("Number of dataloader workers: %d" % n_jobs_dataloader) # Load data - dataset = load_dataset(dataset_name, data_path, normal_class, known_outlier_class, n_known_outlier_classes, - ratio_known_normal, ratio_known_outlier, ratio_pollution, - random_state=np.random.RandomState(cfg.settings['seed'])) + dataset = load_dataset( + dataset_name, + data_path, + normal_class, + known_outlier_class, + n_known_outlier_classes, + ratio_known_normal, + ratio_known_outlier, + ratio_pollution, + random_state=np.random.RandomState(cfg.settings["seed"]), + ) # Log random sample of known anomaly classes if more than 1 class if n_known_outlier_classes > 1: - logger.info('Known anomaly classes: %s' % (dataset.known_outlier_classes,)) + logger.info("Known anomaly classes: %s" % (dataset.known_outlier_classes,)) # Initialize KDE model - kde = KDE(hybrid=cfg.settings['hybrid'], kernel=cfg.settings['kernel'], n_jobs=n_jobs_model, - seed=cfg.settings['seed']) + kde = KDE( + hybrid=cfg.settings["hybrid"], + kernel=cfg.settings["kernel"], + n_jobs=n_jobs_model, + seed=cfg.settings["seed"], + ) # If specified, load model parameters from already trained model if load_model: kde.load_model(import_path=load_model, device=device) - logger.info('Loading model from %s.' % load_model) + logger.info("Loading model from %s." % load_model) # If specified, load model autoencoder weights for a hybrid approach if hybrid and load_ae is not None: kde.load_ae(dataset_name, model_path=load_ae) - logger.info('Loaded pretrained autoencoder for features from %s.' % load_ae) + logger.info("Loaded pretrained autoencoder for features from %s." % load_ae) # Train model on dataset - kde.train(dataset, device=device, n_jobs_dataloader=n_jobs_dataloader, - bandwidth_GridSearchCV=cfg.settings['grid_search_cv']) + kde.train( + dataset, + device=device, + n_jobs_dataloader=n_jobs_dataloader, + bandwidth_GridSearchCV=cfg.settings["grid_search_cv"], + ) # Test model kde.test(dataset, device=device, n_jobs_dataloader=n_jobs_dataloader) # Save results and configuration - kde.save_results(export_json=xp_path + '/results.json') - cfg.save_config(export_json=xp_path + '/config.json') + kde.save_results(export_json=xp_path + "/results.json") + cfg.save_config(export_json=xp_path + "/config.json") # Plot most anomalous and most normal test samples - indices, labels, scores = zip(*kde.results['test_scores']) + indices, labels, scores = zip(*kde.results["test_scores"]) indices, labels, scores = np.array(indices), np.array(labels), np.array(scores) idx_all_sorted = indices[np.argsort(scores)] # from lowest to highest score - idx_normal_sorted = indices[labels == 0][np.argsort(scores[labels == 0])] # from lowest to highest score + idx_normal_sorted = indices[labels == 0][ + np.argsort(scores[labels == 0]) + ] # from lowest to highest score - if dataset_name in ('mnist', 'fmnist', 'cifar10'): + if dataset_name in ("mnist", "fmnist", "cifar10"): - if dataset_name in ('mnist', 'fmnist'): + if dataset_name in ("mnist", "fmnist"): X_all_low = dataset.test_set.data[idx_all_sorted[:32], ...].unsqueeze(1) X_all_high = dataset.test_set.data[idx_all_sorted[-32:], ...].unsqueeze(1) - X_normal_low = dataset.test_set.data[idx_normal_sorted[:32], ...].unsqueeze(1) - X_normal_high = dataset.test_set.data[idx_normal_sorted[-32:], ...].unsqueeze(1) + X_normal_low = dataset.test_set.data[idx_normal_sorted[:32], ...].unsqueeze( + 1 + ) + X_normal_high = dataset.test_set.data[ + idx_normal_sorted[-32:], ... + ].unsqueeze(1) - if dataset_name == 'cifar10': - X_all_low = torch.tensor(np.transpose(dataset.test_set.data[idx_all_sorted[:32], ...], (0, 3, 1, 2))) - X_all_high = torch.tensor(np.transpose(dataset.test_set.data[idx_all_sorted[-32:], ...], (0, 3, 1, 2))) - X_normal_low = torch.tensor(np.transpose(dataset.test_set.data[idx_normal_sorted[:32], ...], (0, 3, 1, 2))) + if dataset_name == "cifar10": + X_all_low = torch.tensor( + np.transpose( + dataset.test_set.data[idx_all_sorted[:32], ...], (0, 3, 1, 2) + ) + ) + X_all_high = torch.tensor( + np.transpose( + dataset.test_set.data[idx_all_sorted[-32:], ...], (0, 3, 1, 2) + ) + ) + X_normal_low = torch.tensor( + np.transpose( + dataset.test_set.data[idx_normal_sorted[:32], ...], (0, 3, 1, 2) + ) + ) X_normal_high = torch.tensor( - np.transpose(dataset.test_set.data[idx_normal_sorted[-32:], ...], (0, 3, 1, 2))) + np.transpose( + dataset.test_set.data[idx_normal_sorted[-32:], ...], (0, 3, 1, 2) + ) + ) - plot_images_grid(X_all_low, export_img=xp_path + '/all_low', padding=2) - plot_images_grid(X_all_high, export_img=xp_path + '/all_high', padding=2) - plot_images_grid(X_normal_low, export_img=xp_path + '/normals_low', padding=2) - plot_images_grid(X_normal_high, export_img=xp_path + '/normals_high', padding=2) + plot_images_grid(X_all_low, export_img=xp_path + "/all_low", padding=2) + plot_images_grid(X_all_high, export_img=xp_path + "/all_high", padding=2) + plot_images_grid(X_normal_low, export_img=xp_path + "/normals_low", padding=2) + plot_images_grid(X_normal_high, export_img=xp_path + "/normals_high", padding=2) -if __name__ == '__main__': +if __name__ == "__main__": main() diff --git a/Deep-SAD-PyTorch/src/baseline_ocsvm.py b/Deep-SAD-PyTorch/src/baseline_ocsvm.py index 4110366..6969fa2 100644 --- a/Deep-SAD-PyTorch/src/baseline_ocsvm.py +++ b/Deep-SAD-PyTorch/src/baseline_ocsvm.py @@ -14,41 +14,127 @@ from datasets.main import load_dataset # Settings ################################################################################ @click.command() -@click.argument('dataset_name', type=click.Choice(['mnist', 'fmnist', 'cifar10', 'arrhythmia', 'cardio', 'satellite', - 'satimage-2', 'shuttle', 'thyroid'])) -@click.argument('xp_path', type=click.Path(exists=True)) -@click.argument('data_path', type=click.Path(exists=True)) -@click.option('--load_config', type=click.Path(exists=True), default=None, - help='Config JSON-file path (default: None).') -@click.option('--load_model', type=click.Path(exists=True), default=None, - help='Model file path (default: None).') -@click.option('--ratio_known_normal', type=float, default=0.0, - help='Ratio of known (labeled) normal training examples.') -@click.option('--ratio_known_outlier', type=float, default=0.0, - help='Ratio of known (labeled) anomalous training examples.') -@click.option('--ratio_pollution', type=float, default=0.0, - help='Pollution ratio of unlabeled training data with unknown (unlabeled) anomalies.') -@click.option('--seed', type=int, default=-1, help='Set seed. If -1, use randomization.') -@click.option('--kernel', type=click.Choice(['rbf', 'linear', 'poly']), default='rbf', help='Kernel for the OC-SVM') -@click.option('--nu', type=float, default=0.1, help='OC-SVM hyperparameter nu (must be 0 < nu <= 1).') -@click.option('--hybrid', type=bool, default=False, - help='Train OC-SVM on features extracted from an autoencoder. If True, load_ae must be specified.') -@click.option('--load_ae', type=click.Path(exists=True), default=None, - help='Model file path to load autoencoder weights (default: None).') -@click.option('--n_jobs_dataloader', type=int, default=0, - help='Number of workers for data loading. 0 means that the data will be loaded in the main process.') -@click.option('--normal_class', type=int, default=0, - help='Specify the normal class of the dataset (all other classes are considered anomalous).') -@click.option('--known_outlier_class', type=int, default=1, - help='Specify the known outlier class of the dataset for semi-supervised anomaly detection.') -@click.option('--n_known_outlier_classes', type=int, default=0, - help='Number of known outlier classes.' - 'If 0, no anomalies are known.' - 'If 1, outlier class as specified in --known_outlier_class option.' - 'If > 1, the specified number of outlier classes will be sampled at random.') -def main(dataset_name, xp_path, data_path, load_config, load_model, ratio_known_normal, ratio_known_outlier, - ratio_pollution, seed, kernel, nu, hybrid, load_ae, n_jobs_dataloader, normal_class, known_outlier_class, - n_known_outlier_classes): +@click.argument( + "dataset_name", + type=click.Choice( + [ + "mnist", + "fmnist", + "cifar10", + "arrhythmia", + "cardio", + "satellite", + "satimage-2", + "shuttle", + "thyroid", + ] + ), +) +@click.argument("xp_path", type=click.Path(exists=True)) +@click.argument("data_path", type=click.Path(exists=True)) +@click.option( + "--load_config", + type=click.Path(exists=True), + default=None, + help="Config JSON-file path (default: None).", +) +@click.option( + "--load_model", + type=click.Path(exists=True), + default=None, + help="Model file path (default: None).", +) +@click.option( + "--ratio_known_normal", + type=float, + default=0.0, + help="Ratio of known (labeled) normal training examples.", +) +@click.option( + "--ratio_known_outlier", + type=float, + default=0.0, + help="Ratio of known (labeled) anomalous training examples.", +) +@click.option( + "--ratio_pollution", + type=float, + default=0.0, + help="Pollution ratio of unlabeled training data with unknown (unlabeled) anomalies.", +) +@click.option( + "--seed", type=int, default=-1, help="Set seed. If -1, use randomization." +) +@click.option( + "--kernel", + type=click.Choice(["rbf", "linear", "poly"]), + default="rbf", + help="Kernel for the OC-SVM", +) +@click.option( + "--nu", + type=float, + default=0.1, + help="OC-SVM hyperparameter nu (must be 0 < nu <= 1).", +) +@click.option( + "--hybrid", + type=bool, + default=False, + help="Train OC-SVM on features extracted from an autoencoder. If True, load_ae must be specified.", +) +@click.option( + "--load_ae", + type=click.Path(exists=True), + default=None, + help="Model file path to load autoencoder weights (default: None).", +) +@click.option( + "--n_jobs_dataloader", + type=int, + default=0, + help="Number of workers for data loading. 0 means that the data will be loaded in the main process.", +) +@click.option( + "--normal_class", + type=int, + default=0, + help="Specify the normal class of the dataset (all other classes are considered anomalous).", +) +@click.option( + "--known_outlier_class", + type=int, + default=1, + help="Specify the known outlier class of the dataset for semi-supervised anomaly detection.", +) +@click.option( + "--n_known_outlier_classes", + type=int, + default=0, + help="Number of known outlier classes." + "If 0, no anomalies are known." + "If 1, outlier class as specified in --known_outlier_class option." + "If > 1, the specified number of outlier classes will be sampled at random.", +) +def main( + dataset_name, + xp_path, + data_path, + load_config, + load_model, + ratio_known_normal, + ratio_known_outlier, + ratio_pollution, + seed, + kernel, + nu, + hybrid, + load_ae, + n_jobs_dataloader, + normal_class, + known_outlier_class, + n_known_outlier_classes, +): """ (Hybrid) One-Class SVM for anomaly detection. @@ -64,74 +150,86 @@ def main(dataset_name, xp_path, data_path, load_config, load_model, ratio_known_ logging.basicConfig(level=logging.INFO) logger = logging.getLogger() logger.setLevel(logging.INFO) - formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') - log_file = xp_path + '/log.txt' + formatter = logging.Formatter( + "%(asctime)s - %(name)s - %(levelname)s - %(message)s" + ) + log_file = xp_path + "/log.txt" file_handler = logging.FileHandler(log_file) file_handler.setLevel(logging.INFO) file_handler.setFormatter(formatter) logger.addHandler(file_handler) # Print paths - logger.info('Log file is %s.' % log_file) - logger.info('Data path is %s.' % data_path) - logger.info('Export path is %s.' % xp_path) + logger.info("Log file is %s." % log_file) + logger.info("Data path is %s." % data_path) + logger.info("Export path is %s." % xp_path) # Print experimental setup - logger.info('Dataset: %s' % dataset_name) - logger.info('Normal class: %d' % normal_class) - logger.info('Ratio of labeled normal train samples: %.2f' % ratio_known_normal) - logger.info('Ratio of labeled anomalous samples: %.2f' % ratio_known_outlier) - logger.info('Pollution ratio of unlabeled train data: %.2f' % ratio_pollution) + logger.info("Dataset: %s" % dataset_name) + logger.info("Normal class: %d" % normal_class) + logger.info("Ratio of labeled normal train samples: %.2f" % ratio_known_normal) + logger.info("Ratio of labeled anomalous samples: %.2f" % ratio_known_outlier) + logger.info("Pollution ratio of unlabeled train data: %.2f" % ratio_pollution) if n_known_outlier_classes == 1: - logger.info('Known anomaly class: %d' % known_outlier_class) + logger.info("Known anomaly class: %d" % known_outlier_class) else: - logger.info('Number of known anomaly classes: %d' % n_known_outlier_classes) + logger.info("Number of known anomaly classes: %d" % n_known_outlier_classes) # If specified, load experiment config from JSON-file if load_config: cfg.load_config(import_json=load_config) - logger.info('Loaded configuration from %s.' % load_config) + logger.info("Loaded configuration from %s." % load_config) # Print OC-SVM configuration - logger.info('OC-SVM kernel: %s' % cfg.settings['kernel']) - logger.info('Nu-paramerter: %.2f' % cfg.settings['nu']) - logger.info('Hybrid model: %s' % cfg.settings['hybrid']) + logger.info("OC-SVM kernel: %s" % cfg.settings["kernel"]) + logger.info("Nu-paramerter: %.2f" % cfg.settings["nu"]) + logger.info("Hybrid model: %s" % cfg.settings["hybrid"]) # Set seed - if cfg.settings['seed'] != -1: - random.seed(cfg.settings['seed']) - np.random.seed(cfg.settings['seed']) - torch.manual_seed(cfg.settings['seed']) - torch.cuda.manual_seed(cfg.settings['seed']) + if cfg.settings["seed"] != -1: + random.seed(cfg.settings["seed"]) + np.random.seed(cfg.settings["seed"]) + torch.manual_seed(cfg.settings["seed"]) + torch.cuda.manual_seed(cfg.settings["seed"]) torch.backends.cudnn.deterministic = True - logger.info('Set seed to %d.' % cfg.settings['seed']) + logger.info("Set seed to %d." % cfg.settings["seed"]) # Use 'cpu' as device for OC-SVM - device = 'cpu' - torch.multiprocessing.set_sharing_strategy('file_system') # fix multiprocessing issue for ubuntu - logger.info('Computation device: %s' % device) - logger.info('Number of dataloader workers: %d' % n_jobs_dataloader) + device = "cpu" + torch.multiprocessing.set_sharing_strategy( + "file_system" + ) # fix multiprocessing issue for ubuntu + logger.info("Computation device: %s" % device) + logger.info("Number of dataloader workers: %d" % n_jobs_dataloader) # Load data - dataset = load_dataset(dataset_name, data_path, normal_class, known_outlier_class, n_known_outlier_classes, - ratio_known_normal, ratio_known_outlier, ratio_pollution, - random_state=np.random.RandomState(cfg.settings['seed'])) + dataset = load_dataset( + dataset_name, + data_path, + normal_class, + known_outlier_class, + n_known_outlier_classes, + ratio_known_normal, + ratio_known_outlier, + ratio_pollution, + random_state=np.random.RandomState(cfg.settings["seed"]), + ) # Log random sample of known anomaly classes if more than 1 class if n_known_outlier_classes > 1: - logger.info('Known anomaly classes: %s' % (dataset.known_outlier_classes,)) + logger.info("Known anomaly classes: %s" % (dataset.known_outlier_classes,)) # Initialize OC-SVM model - ocsvm = OCSVM(cfg.settings['kernel'], cfg.settings['nu'], cfg.settings['hybrid']) + ocsvm = OCSVM(cfg.settings["kernel"], cfg.settings["nu"], cfg.settings["hybrid"]) # If specified, load model parameters from already trained model if load_model: ocsvm.load_model(import_path=load_model, device=device) - logger.info('Loading model from %s.' % load_model) + logger.info("Loading model from %s." % load_model) # If specified, load model autoencoder weights for a hybrid approach if hybrid and load_ae is not None: ocsvm.load_ae(dataset_name, model_path=load_ae) - logger.info('Loaded pretrained autoencoder for features from %s.' % load_ae) + logger.info("Loaded pretrained autoencoder for features from %s." % load_ae) # Train model on dataset ocsvm.train(dataset, device=device, n_jobs_dataloader=n_jobs_dataloader) @@ -140,35 +238,56 @@ def main(dataset_name, xp_path, data_path, load_config, load_model, ratio_known_ ocsvm.test(dataset, device=device, n_jobs_dataloader=n_jobs_dataloader) # Save results and configuration - ocsvm.save_results(export_json=xp_path + '/results.json') - cfg.save_config(export_json=xp_path + '/config.json') + ocsvm.save_results(export_json=xp_path + "/results.json") + cfg.save_config(export_json=xp_path + "/config.json") # Plot most anomalous and most normal test samples - indices, labels, scores = zip(*ocsvm.results['test_scores']) + indices, labels, scores = zip(*ocsvm.results["test_scores"]) indices, labels, scores = np.array(indices), np.array(labels), np.array(scores) idx_all_sorted = indices[np.argsort(scores)] # from lowest to highest score - idx_normal_sorted = indices[labels == 0][np.argsort(scores[labels == 0])] # from lowest to highest score + idx_normal_sorted = indices[labels == 0][ + np.argsort(scores[labels == 0]) + ] # from lowest to highest score - if dataset_name in ('mnist', 'fmnist', 'cifar10'): + if dataset_name in ("mnist", "fmnist", "cifar10"): - if dataset_name in ('mnist', 'fmnist'): + if dataset_name in ("mnist", "fmnist"): X_all_low = dataset.test_set.data[idx_all_sorted[:32], ...].unsqueeze(1) X_all_high = dataset.test_set.data[idx_all_sorted[-32:], ...].unsqueeze(1) - X_normal_low = dataset.test_set.data[idx_normal_sorted[:32], ...].unsqueeze(1) - X_normal_high = dataset.test_set.data[idx_normal_sorted[-32:], ...].unsqueeze(1) + X_normal_low = dataset.test_set.data[idx_normal_sorted[:32], ...].unsqueeze( + 1 + ) + X_normal_high = dataset.test_set.data[ + idx_normal_sorted[-32:], ... + ].unsqueeze(1) - if dataset_name == 'cifar10': - X_all_low = torch.tensor(np.transpose(dataset.test_set.data[idx_all_sorted[:32], ...], (0, 3, 1, 2))) - X_all_high = torch.tensor(np.transpose(dataset.test_set.data[idx_all_sorted[-32:], ...], (0, 3, 1, 2))) - X_normal_low = torch.tensor(np.transpose(dataset.test_set.data[idx_normal_sorted[:32], ...], (0, 3, 1, 2))) + if dataset_name == "cifar10": + X_all_low = torch.tensor( + np.transpose( + dataset.test_set.data[idx_all_sorted[:32], ...], (0, 3, 1, 2) + ) + ) + X_all_high = torch.tensor( + np.transpose( + dataset.test_set.data[idx_all_sorted[-32:], ...], (0, 3, 1, 2) + ) + ) + X_normal_low = torch.tensor( + np.transpose( + dataset.test_set.data[idx_normal_sorted[:32], ...], (0, 3, 1, 2) + ) + ) X_normal_high = torch.tensor( - np.transpose(dataset.test_set.data[idx_normal_sorted[-32:], ...], (0, 3, 1, 2))) + np.transpose( + dataset.test_set.data[idx_normal_sorted[-32:], ...], (0, 3, 1, 2) + ) + ) - plot_images_grid(X_all_low, export_img=xp_path + '/all_low', padding=2) - plot_images_grid(X_all_high, export_img=xp_path + '/all_high', padding=2) - plot_images_grid(X_normal_low, export_img=xp_path + '/normals_low', padding=2) - plot_images_grid(X_normal_high, export_img=xp_path + '/normals_high', padding=2) + plot_images_grid(X_all_low, export_img=xp_path + "/all_low", padding=2) + plot_images_grid(X_all_high, export_img=xp_path + "/all_high", padding=2) + plot_images_grid(X_normal_low, export_img=xp_path + "/normals_low", padding=2) + plot_images_grid(X_normal_high, export_img=xp_path + "/normals_high", padding=2) -if __name__ == '__main__': +if __name__ == "__main__": main() diff --git a/Deep-SAD-PyTorch/src/baseline_ssad.py b/Deep-SAD-PyTorch/src/baseline_ssad.py index 3c16eb3..134f415 100644 --- a/Deep-SAD-PyTorch/src/baseline_ssad.py +++ b/Deep-SAD-PyTorch/src/baseline_ssad.py @@ -15,41 +15,119 @@ from datasets.main import load_dataset # Settings ################################################################################ @click.command() -@click.argument('dataset_name', type=click.Choice(['mnist', 'fmnist', 'cifar10', 'arrhythmia', 'cardio', 'satellite', - 'satimage-2', 'shuttle', 'thyroid'])) -@click.argument('xp_path', type=click.Path(exists=True)) -@click.argument('data_path', type=click.Path(exists=True)) -@click.option('--load_config', type=click.Path(exists=True), default=None, - help='Config JSON-file path (default: None).') -@click.option('--load_model', type=click.Path(exists=True), default=None, - help='Model file path (default: None).') -@click.option('--ratio_known_normal', type=float, default=0.0, - help='Ratio of known (labeled) normal training examples.') -@click.option('--ratio_known_outlier', type=float, default=0.0, - help='Ratio of known (labeled) anomalous training examples.') -@click.option('--ratio_pollution', type=float, default=0.0, - help='Pollution ratio of unlabeled training data with unknown (unlabeled) anomalies.') -@click.option('--seed', type=int, default=-1, help='Set seed. If -1, use randomization.') -@click.option('--kernel', type=click.Choice(['rbf']), default='rbf', help='Kernel for SSAD') -@click.option('--kappa', type=float, default=1.0, help='SSAD hyperparameter kappa.') -@click.option('--hybrid', type=bool, default=False, - help='Train SSAD on features extracted from an autoencoder. If True, load_ae must be specified') -@click.option('--load_ae', type=click.Path(exists=True), default=None, - help='Model file path to load autoencoder weights (default: None).') -@click.option('--n_jobs_dataloader', type=int, default=0, - help='Number of workers for data loading. 0 means that the data will be loaded in the main process.') -@click.option('--normal_class', type=int, default=0, - help='Specify the normal class of the dataset (all other classes are considered anomalous).') -@click.option('--known_outlier_class', type=int, default=1, - help='Specify the known outlier class of the dataset for semi-supervised anomaly detection.') -@click.option('--n_known_outlier_classes', type=int, default=0, - help='Number of known outlier classes.' - 'If 0, no anomalies are known.' - 'If 1, outlier class as specified in --known_outlier_class option.' - 'If > 1, the specified number of outlier classes will be sampled at random.') -def main(dataset_name, xp_path, data_path, load_config, load_model, ratio_known_normal, ratio_known_outlier, - ratio_pollution, seed, kernel, kappa, hybrid, load_ae, n_jobs_dataloader, normal_class, known_outlier_class, - n_known_outlier_classes): +@click.argument( + "dataset_name", + type=click.Choice( + [ + "mnist", + "fmnist", + "cifar10", + "arrhythmia", + "cardio", + "satellite", + "satimage-2", + "shuttle", + "thyroid", + ] + ), +) +@click.argument("xp_path", type=click.Path(exists=True)) +@click.argument("data_path", type=click.Path(exists=True)) +@click.option( + "--load_config", + type=click.Path(exists=True), + default=None, + help="Config JSON-file path (default: None).", +) +@click.option( + "--load_model", + type=click.Path(exists=True), + default=None, + help="Model file path (default: None).", +) +@click.option( + "--ratio_known_normal", + type=float, + default=0.0, + help="Ratio of known (labeled) normal training examples.", +) +@click.option( + "--ratio_known_outlier", + type=float, + default=0.0, + help="Ratio of known (labeled) anomalous training examples.", +) +@click.option( + "--ratio_pollution", + type=float, + default=0.0, + help="Pollution ratio of unlabeled training data with unknown (unlabeled) anomalies.", +) +@click.option( + "--seed", type=int, default=-1, help="Set seed. If -1, use randomization." +) +@click.option( + "--kernel", type=click.Choice(["rbf"]), default="rbf", help="Kernel for SSAD" +) +@click.option("--kappa", type=float, default=1.0, help="SSAD hyperparameter kappa.") +@click.option( + "--hybrid", + type=bool, + default=False, + help="Train SSAD on features extracted from an autoencoder. If True, load_ae must be specified", +) +@click.option( + "--load_ae", + type=click.Path(exists=True), + default=None, + help="Model file path to load autoencoder weights (default: None).", +) +@click.option( + "--n_jobs_dataloader", + type=int, + default=0, + help="Number of workers for data loading. 0 means that the data will be loaded in the main process.", +) +@click.option( + "--normal_class", + type=int, + default=0, + help="Specify the normal class of the dataset (all other classes are considered anomalous).", +) +@click.option( + "--known_outlier_class", + type=int, + default=1, + help="Specify the known outlier class of the dataset for semi-supervised anomaly detection.", +) +@click.option( + "--n_known_outlier_classes", + type=int, + default=0, + help="Number of known outlier classes." + "If 0, no anomalies are known." + "If 1, outlier class as specified in --known_outlier_class option." + "If > 1, the specified number of outlier classes will be sampled at random.", +) +def main( + dataset_name, + xp_path, + data_path, + load_config, + load_model, + ratio_known_normal, + ratio_known_outlier, + ratio_pollution, + seed, + kernel, + kappa, + hybrid, + load_ae, + n_jobs_dataloader, + normal_class, + known_outlier_class, + n_known_outlier_classes, +): """ (Hybrid) SSAD for anomaly detection as in Goernitz et al., Towards Supervised Anomaly Detection, JAIR, 2013. @@ -65,75 +143,91 @@ def main(dataset_name, xp_path, data_path, load_config, load_model, ratio_known_ logging.basicConfig(level=logging.INFO) logger = logging.getLogger() logger.setLevel(logging.INFO) - formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') - log_file = xp_path + '/log.txt' + formatter = logging.Formatter( + "%(asctime)s - %(name)s - %(levelname)s - %(message)s" + ) + log_file = xp_path + "/log.txt" file_handler = logging.FileHandler(log_file) file_handler.setLevel(logging.INFO) file_handler.setFormatter(formatter) logger.addHandler(file_handler) # Print paths - logger.info('Log file is %s.' % log_file) - logger.info('Data path is %s.' % data_path) - logger.info('Export path is %s.' % xp_path) + logger.info("Log file is %s." % log_file) + logger.info("Data path is %s." % data_path) + logger.info("Export path is %s." % xp_path) # Print experimental setup - logger.info('Dataset: %s' % dataset_name) - logger.info('Normal class: %d' % normal_class) - logger.info('Ratio of labeled normal train samples: %.2f' % ratio_known_normal) - logger.info('Ratio of labeled anomalous samples: %.2f' % ratio_known_outlier) - logger.info('Pollution ratio of unlabeled train data: %.2f' % ratio_pollution) + logger.info("Dataset: %s" % dataset_name) + logger.info("Normal class: %d" % normal_class) + logger.info("Ratio of labeled normal train samples: %.2f" % ratio_known_normal) + logger.info("Ratio of labeled anomalous samples: %.2f" % ratio_known_outlier) + logger.info("Pollution ratio of unlabeled train data: %.2f" % ratio_pollution) if n_known_outlier_classes == 1: - logger.info('Known anomaly class: %d' % known_outlier_class) + logger.info("Known anomaly class: %d" % known_outlier_class) else: - logger.info('Number of known anomaly classes: %d' % n_known_outlier_classes) + logger.info("Number of known anomaly classes: %d" % n_known_outlier_classes) # If specified, load experiment config from JSON-file if load_config: cfg.load_config(import_json=load_config) - logger.info('Loaded configuration from %s.' % load_config) + logger.info("Loaded configuration from %s." % load_config) # Print SSAD configuration - logger.info('SSAD kernel: %s' % cfg.settings['kernel']) - logger.info('Kappa-paramerter: %.2f' % cfg.settings['kappa']) - logger.info('Hybrid model: %s' % cfg.settings['hybrid']) + logger.info("SSAD kernel: %s" % cfg.settings["kernel"]) + logger.info("Kappa-paramerter: %.2f" % cfg.settings["kappa"]) + logger.info("Hybrid model: %s" % cfg.settings["hybrid"]) # Set seed - if cfg.settings['seed'] != -1: - random.seed(cfg.settings['seed']) - np.random.seed(cfg.settings['seed']) - co.setseed(cfg.settings['seed']) - torch.manual_seed(cfg.settings['seed']) - torch.cuda.manual_seed(cfg.settings['seed']) + if cfg.settings["seed"] != -1: + random.seed(cfg.settings["seed"]) + np.random.seed(cfg.settings["seed"]) + co.setseed(cfg.settings["seed"]) + torch.manual_seed(cfg.settings["seed"]) + torch.cuda.manual_seed(cfg.settings["seed"]) torch.backends.cudnn.deterministic = True - logger.info('Set seed to %d.' % cfg.settings['seed']) + logger.info("Set seed to %d." % cfg.settings["seed"]) # Use 'cpu' as device for SSAD - device = 'cpu' - torch.multiprocessing.set_sharing_strategy('file_system') # fix multiprocessing issue for ubuntu - logger.info('Computation device: %s' % device) - logger.info('Number of dataloader workers: %d' % n_jobs_dataloader) + device = "cpu" + torch.multiprocessing.set_sharing_strategy( + "file_system" + ) # fix multiprocessing issue for ubuntu + logger.info("Computation device: %s" % device) + logger.info("Number of dataloader workers: %d" % n_jobs_dataloader) # Load data - dataset = load_dataset(dataset_name, data_path, normal_class, known_outlier_class, n_known_outlier_classes, - ratio_known_normal, ratio_known_outlier, ratio_pollution, - random_state=np.random.RandomState(cfg.settings['seed'])) + dataset = load_dataset( + dataset_name, + data_path, + normal_class, + known_outlier_class, + n_known_outlier_classes, + ratio_known_normal, + ratio_known_outlier, + ratio_pollution, + random_state=np.random.RandomState(cfg.settings["seed"]), + ) # Log random sample of known anomaly classes if more than 1 class if n_known_outlier_classes > 1: - logger.info('Known anomaly classes: %s' % (dataset.known_outlier_classes,)) + logger.info("Known anomaly classes: %s" % (dataset.known_outlier_classes,)) # Initialize SSAD model - ssad = SSAD(kernel=cfg.settings['kernel'], kappa=cfg.settings['kappa'], hybrid=cfg.settings['hybrid']) + ssad = SSAD( + kernel=cfg.settings["kernel"], + kappa=cfg.settings["kappa"], + hybrid=cfg.settings["hybrid"], + ) # If specified, load model parameters from already trained model if load_model: ssad.load_model(import_path=load_model, device=device) - logger.info('Loading model from %s.' % load_model) + logger.info("Loading model from %s." % load_model) # If specified, load model autoencoder weights for a hybrid approach if hybrid and load_ae is not None: ssad.load_ae(dataset_name, model_path=load_ae) - logger.info('Loaded pretrained autoencoder for features from %s.' % load_ae) + logger.info("Loaded pretrained autoencoder for features from %s." % load_ae) # Train model on dataset ssad.train(dataset, device=device, n_jobs_dataloader=n_jobs_dataloader) @@ -142,35 +236,56 @@ def main(dataset_name, xp_path, data_path, load_config, load_model, ratio_known_ ssad.test(dataset, device=device, n_jobs_dataloader=n_jobs_dataloader) # Save results and configuration - ssad.save_results(export_json=xp_path + '/results.json') - cfg.save_config(export_json=xp_path + '/config.json') + ssad.save_results(export_json=xp_path + "/results.json") + cfg.save_config(export_json=xp_path + "/config.json") # Plot most anomalous and most normal test samples - indices, labels, scores = zip(*ssad.results['test_scores']) + indices, labels, scores = zip(*ssad.results["test_scores"]) indices, labels, scores = np.array(indices), np.array(labels), np.array(scores) idx_all_sorted = indices[np.argsort(scores)] # from lowest to highest score - idx_normal_sorted = indices[labels == 0][np.argsort(scores[labels == 0])] # from lowest to highest score + idx_normal_sorted = indices[labels == 0][ + np.argsort(scores[labels == 0]) + ] # from lowest to highest score - if dataset_name in ('mnist', 'fmnist', 'cifar10'): + if dataset_name in ("mnist", "fmnist", "cifar10"): - if dataset_name in ('mnist', 'fmnist'): + if dataset_name in ("mnist", "fmnist"): X_all_low = dataset.test_set.data[idx_all_sorted[:32], ...].unsqueeze(1) X_all_high = dataset.test_set.data[idx_all_sorted[-32:], ...].unsqueeze(1) - X_normal_low = dataset.test_set.data[idx_normal_sorted[:32], ...].unsqueeze(1) - X_normal_high = dataset.test_set.data[idx_normal_sorted[-32:], ...].unsqueeze(1) + X_normal_low = dataset.test_set.data[idx_normal_sorted[:32], ...].unsqueeze( + 1 + ) + X_normal_high = dataset.test_set.data[ + idx_normal_sorted[-32:], ... + ].unsqueeze(1) - if dataset_name == 'cifar10': - X_all_low = torch.tensor(np.transpose(dataset.test_set.data[idx_all_sorted[:32], ...], (0, 3, 1, 2))) - X_all_high = torch.tensor(np.transpose(dataset.test_set.data[idx_all_sorted[-32:], ...], (0, 3, 1, 2))) - X_normal_low = torch.tensor(np.transpose(dataset.test_set.data[idx_normal_sorted[:32], ...], (0, 3, 1, 2))) + if dataset_name == "cifar10": + X_all_low = torch.tensor( + np.transpose( + dataset.test_set.data[idx_all_sorted[:32], ...], (0, 3, 1, 2) + ) + ) + X_all_high = torch.tensor( + np.transpose( + dataset.test_set.data[idx_all_sorted[-32:], ...], (0, 3, 1, 2) + ) + ) + X_normal_low = torch.tensor( + np.transpose( + dataset.test_set.data[idx_normal_sorted[:32], ...], (0, 3, 1, 2) + ) + ) X_normal_high = torch.tensor( - np.transpose(dataset.test_set.data[idx_normal_sorted[-32:], ...], (0, 3, 1, 2))) + np.transpose( + dataset.test_set.data[idx_normal_sorted[-32:], ...], (0, 3, 1, 2) + ) + ) - plot_images_grid(X_all_low, export_img=xp_path + '/all_low', padding=2) - plot_images_grid(X_all_high, export_img=xp_path + '/all_high', padding=2) - plot_images_grid(X_normal_low, export_img=xp_path + '/normals_low', padding=2) - plot_images_grid(X_normal_high, export_img=xp_path + '/normals_high', padding=2) + plot_images_grid(X_all_low, export_img=xp_path + "/all_low", padding=2) + plot_images_grid(X_all_high, export_img=xp_path + "/all_high", padding=2) + plot_images_grid(X_normal_low, export_img=xp_path + "/normals_low", padding=2) + plot_images_grid(X_normal_high, export_img=xp_path + "/normals_high", padding=2) -if __name__ == '__main__': +if __name__ == "__main__": main() diff --git a/Deep-SAD-PyTorch/src/baselines/SemiDGM.py b/Deep-SAD-PyTorch/src/baselines/SemiDGM.py index a05a4ef..7da3a4e 100644 --- a/Deep-SAD-PyTorch/src/baselines/SemiDGM.py +++ b/Deep-SAD-PyTorch/src/baselines/SemiDGM.py @@ -36,17 +36,13 @@ class SemiDeepGenerativeModel(object): self.vae_optimizer_name = None self.results = { - 'train_time': None, - 'test_auc': None, - 'test_time': None, - 'test_scores': None, + "train_time": None, + "test_auc": None, + "test_time": None, + "test_scores": None, } - self.vae_results = { - 'train_time': None, - 'test_auc': None, - 'test_time': None - } + self.vae_results = {"train_time": None, "test_auc": None, "test_time": None} def set_vae(self, net_name): """Builds the variational autoencoder network for pretraining.""" @@ -58,71 +54,106 @@ class SemiDeepGenerativeModel(object): self.net_name = net_name self.net = build_network(net_name, ae_net=self.vae_net) # full M1+M2 model - def train(self, dataset: BaseADDataset, optimizer_name: str = 'adam', lr: float = 0.001, n_epochs: int = 50, - lr_milestones: tuple = (), batch_size: int = 128, weight_decay: float = 1e-6, device: str = 'cuda', - n_jobs_dataloader: int = 0): + def train( + self, + dataset: BaseADDataset, + optimizer_name: str = "adam", + lr: float = 0.001, + n_epochs: int = 50, + lr_milestones: tuple = (), + batch_size: int = 128, + weight_decay: float = 1e-6, + device: str = "cuda", + n_jobs_dataloader: int = 0, + ): """Trains the Semi-Supervised Deep Generative model on the training data.""" self.optimizer_name = optimizer_name - self.trainer = SemiDeepGenerativeTrainer(alpha=self.alpha, optimizer_name=optimizer_name, lr=lr, - n_epochs=n_epochs, lr_milestones=lr_milestones, batch_size=batch_size, - weight_decay=weight_decay, device=device, - n_jobs_dataloader=n_jobs_dataloader) + self.trainer = SemiDeepGenerativeTrainer( + alpha=self.alpha, + optimizer_name=optimizer_name, + lr=lr, + n_epochs=n_epochs, + lr_milestones=lr_milestones, + batch_size=batch_size, + weight_decay=weight_decay, + device=device, + n_jobs_dataloader=n_jobs_dataloader, + ) self.net = self.trainer.train(dataset, self.net) - self.results['train_time'] = self.trainer.train_time + self.results["train_time"] = self.trainer.train_time - def test(self, dataset: BaseADDataset, device: str = 'cuda', n_jobs_dataloader: int = 0): + def test( + self, dataset: BaseADDataset, device: str = "cuda", n_jobs_dataloader: int = 0 + ): """Tests the Semi-Supervised Deep Generative model on the test data.""" if self.trainer is None: - self.trainer = SemiDeepGenerativeTrainer(alpha=self.alpha, device=device, - n_jobs_dataloader=n_jobs_dataloader) + self.trainer = SemiDeepGenerativeTrainer( + alpha=self.alpha, device=device, n_jobs_dataloader=n_jobs_dataloader + ) self.trainer.test(dataset, self.net) # Get results - self.results['test_auc'] = self.trainer.test_auc - self.results['test_time'] = self.trainer.test_time - self.results['test_scores'] = self.trainer.test_scores + self.results["test_auc"] = self.trainer.test_auc + self.results["test_time"] = self.trainer.test_time + self.results["test_scores"] = self.trainer.test_scores - def pretrain(self, dataset: BaseADDataset, optimizer_name: str = 'adam', lr: float = 0.001, n_epochs: int = 100, - lr_milestones: tuple = (), batch_size: int = 128, weight_decay: float = 1e-6, device: str = 'cuda', - n_jobs_dataloader: int = 0): + def pretrain( + self, + dataset: BaseADDataset, + optimizer_name: str = "adam", + lr: float = 0.001, + n_epochs: int = 100, + lr_milestones: tuple = (), + batch_size: int = 128, + weight_decay: float = 1e-6, + device: str = "cuda", + n_jobs_dataloader: int = 0, + ): """Pretrains a variational autoencoder (M1) for the Semi-Supervised Deep Generative model.""" # Train self.vae_optimizer_name = optimizer_name - self.vae_trainer = VAETrainer(optimizer_name=optimizer_name, lr=lr, n_epochs=n_epochs, - lr_milestones=lr_milestones, batch_size=batch_size, weight_decay=weight_decay, - device=device, n_jobs_dataloader=n_jobs_dataloader) + self.vae_trainer = VAETrainer( + optimizer_name=optimizer_name, + lr=lr, + n_epochs=n_epochs, + lr_milestones=lr_milestones, + batch_size=batch_size, + weight_decay=weight_decay, + device=device, + n_jobs_dataloader=n_jobs_dataloader, + ) self.vae_net = self.vae_trainer.train(dataset, self.vae_net) # Get train results - self.vae_results['train_time'] = self.vae_trainer.train_time + self.vae_results["train_time"] = self.vae_trainer.train_time # Test self.vae_trainer.test(dataset, self.vae_net) # Get test results - self.vae_results['test_auc'] = self.vae_trainer.test_auc - self.vae_results['test_time'] = self.vae_trainer.test_time + self.vae_results["test_auc"] = self.vae_trainer.test_auc + self.vae_results["test_time"] = self.vae_trainer.test_time def save_model(self, export_model): """Save a Semi-Supervised Deep Generative model to export_model.""" net_dict = self.net.state_dict() - torch.save({'net_dict': net_dict}, export_model) + torch.save({"net_dict": net_dict}, export_model) def load_model(self, model_path): """Load a Semi-Supervised Deep Generative model from model_path.""" model_dict = torch.load(model_path) - self.net.load_state_dict(model_dict['net_dict']) + self.net.load_state_dict(model_dict["net_dict"]) def save_results(self, export_json): """Save results dict to a JSON-file.""" - with open(export_json, 'w') as fp: + with open(export_json, "w") as fp: json.dump(self.results, fp) def save_vae_results(self, export_json): """Save variational autoencoder results dict to a JSON-file.""" - with open(export_json, 'w') as fp: + with open(export_json, "w") as fp: json.dump(self.vae_results, fp) diff --git a/Deep-SAD-PyTorch/src/baselines/isoforest.py b/Deep-SAD-PyTorch/src/baselines/isoforest.py index cca71e5..d736fcc 100644 --- a/Deep-SAD-PyTorch/src/baselines/isoforest.py +++ b/Deep-SAD-PyTorch/src/baselines/isoforest.py @@ -14,8 +14,16 @@ from networks.main import build_autoencoder class IsoForest(object): """A class for Isolation Forest models.""" - def __init__(self, hybrid=False, n_estimators=100, max_samples='auto', contamination=0.1, n_jobs=-1, seed=None, - **kwargs): + def __init__( + self, + hybrid=False, + n_estimators=100, + max_samples="auto", + contamination=0.1, + n_jobs=-1, + seed=None, + **kwargs + ): """Init Isolation Forest instance.""" self.n_estimators = n_estimators self.max_samples = max_samples @@ -23,26 +31,39 @@ class IsoForest(object): self.n_jobs = n_jobs self.seed = seed - self.model = IsolationForest(n_estimators=n_estimators, max_samples=max_samples, contamination=contamination, - n_jobs=n_jobs, random_state=seed, **kwargs) + self.model = IsolationForest( + n_estimators=n_estimators, + max_samples=max_samples, + contamination=contamination, + n_jobs=n_jobs, + random_state=seed, + **kwargs + ) self.hybrid = hybrid self.ae_net = None # autoencoder network for the case of a hybrid model self.results = { - 'train_time': None, - 'test_time': None, - 'test_auc': None, - 'test_scores': None + "train_time": None, + "test_time": None, + "test_auc": None, + "test_scores": None, } - def train(self, dataset: BaseADDataset, device: str = 'cpu', n_jobs_dataloader: int = 0): + def train( + self, dataset: BaseADDataset, device: str = "cpu", n_jobs_dataloader: int = 0 + ): """Trains the Isolation Forest model on the training data.""" logger = logging.getLogger() # do not drop last batch for non-SGD optimization shallow_ssad - train_loader = DataLoader(dataset=dataset.train_set, batch_size=128, shuffle=True, - num_workers=n_jobs_dataloader, drop_last=False) + train_loader = DataLoader( + dataset=dataset.train_set, + batch_size=128, + shuffle=True, + num_workers=n_jobs_dataloader, + drop_last=False, + ) # Get data from loader X = () @@ -50,22 +71,28 @@ class IsoForest(object): inputs, _, _, _ = data inputs = inputs.to(device) if self.hybrid: - inputs = self.ae_net.encoder(inputs) # in hybrid approach, take code representation of AE as features - X_batch = inputs.view(inputs.size(0), -1) # X_batch.shape = (batch_size, n_channels * height * width) + inputs = self.ae_net.encoder( + inputs + ) # in hybrid approach, take code representation of AE as features + X_batch = inputs.view( + inputs.size(0), -1 + ) # X_batch.shape = (batch_size, n_channels * height * width) X += (X_batch.cpu().data.numpy(),) X = np.concatenate(X) # Training - logger.info('Starting training...') + logger.info("Starting training...") start_time = time.time() self.model.fit(X) train_time = time.time() - start_time - self.results['train_time'] = train_time + self.results["train_time"] = train_time - logger.info('Training Time: {:.3f}s'.format(self.results['train_time'])) - logger.info('Finished training.') + logger.info("Training Time: {:.3f}s".format(self.results["train_time"])) + logger.info("Finished training.") - def test(self, dataset: BaseADDataset, device: str = 'cpu', n_jobs_dataloader: int = 0): + def test( + self, dataset: BaseADDataset, device: str = "cpu", n_jobs_dataloader: int = 0 + ): """Tests the Isolation Forest model on the test data.""" logger = logging.getLogger() @@ -78,46 +105,54 @@ class IsoForest(object): labels = [] for data in test_loader: inputs, label_batch, _, idx = data - inputs, label_batch, idx = inputs.to(device), label_batch.to(device), idx.to(device) + inputs, label_batch, idx = ( + inputs.to(device), + label_batch.to(device), + idx.to(device), + ) if self.hybrid: - inputs = self.ae_net.encoder(inputs) # in hybrid approach, take code representation of AE as features - X_batch = inputs.view(inputs.size(0), -1) # X_batch.shape = (batch_size, n_channels * height * width) + inputs = self.ae_net.encoder( + inputs + ) # in hybrid approach, take code representation of AE as features + X_batch = inputs.view( + inputs.size(0), -1 + ) # X_batch.shape = (batch_size, n_channels * height * width) X += (X_batch.cpu().data.numpy(),) idxs += idx.cpu().data.numpy().astype(np.int64).tolist() labels += label_batch.cpu().data.numpy().astype(np.int64).tolist() X = np.concatenate(X) # Testing - logger.info('Starting testing...') + logger.info("Starting testing...") start_time = time.time() scores = (-1.0) * self.model.decision_function(X) - self.results['test_time'] = time.time() - start_time + self.results["test_time"] = time.time() - start_time scores = scores.flatten() # Save triples of (idx, label, score) in a list idx_label_score += list(zip(idxs, labels, scores.tolist())) - self.results['test_scores'] = idx_label_score + self.results["test_scores"] = idx_label_score # Compute AUC _, labels, scores = zip(*idx_label_score) labels = np.array(labels) scores = np.array(scores) - self.results['test_auc'] = roc_auc_score(labels, scores) + self.results["test_auc"] = roc_auc_score(labels, scores) # Log results - logger.info('Test AUC: {:.2f}%'.format(100. * self.results['test_auc'])) - logger.info('Test Time: {:.3f}s'.format(self.results['test_time'])) - logger.info('Finished testing.') + logger.info("Test AUC: {:.2f}%".format(100.0 * self.results["test_auc"])) + logger.info("Test Time: {:.3f}s".format(self.results["test_time"])) + logger.info("Finished testing.") def load_ae(self, dataset_name, model_path): """Load pretrained autoencoder from model_path for feature extraction in a hybrid Isolation Forest model.""" - model_dict = torch.load(model_path, map_location='cpu') - ae_net_dict = model_dict['ae_net_dict'] - if dataset_name in ['mnist', 'fmnist', 'cifar10']: - net_name = dataset_name + '_LeNet' + model_dict = torch.load(model_path, map_location="cpu") + ae_net_dict = model_dict["ae_net_dict"] + if dataset_name in ["mnist", "fmnist", "cifar10"]: + net_name = dataset_name + "_LeNet" else: - net_name = dataset_name + '_mlp' + net_name = dataset_name + "_mlp" if self.ae_net is None: self.ae_net = build_autoencoder(net_name) @@ -137,11 +172,11 @@ class IsoForest(object): """Save Isolation Forest model to export_path.""" pass - def load_model(self, import_path, device: str = 'cpu'): + def load_model(self, import_path, device: str = "cpu"): """Load Isolation Forest model from import_path.""" pass def save_results(self, export_json): """Save results dict to a JSON-file.""" - with open(export_json, 'w') as fp: + with open(export_json, "w") as fp: json.dump(self.results, fp) diff --git a/Deep-SAD-PyTorch/src/baselines/kde.py b/Deep-SAD-PyTorch/src/baselines/kde.py index 05a895a..61ce0df 100644 --- a/Deep-SAD-PyTorch/src/baselines/kde.py +++ b/Deep-SAD-PyTorch/src/baselines/kde.py @@ -16,7 +16,7 @@ from networks.main import build_autoencoder class KDE(object): """A class for Kernel Density Estimation models.""" - def __init__(self, hybrid=False, kernel='gaussian', n_jobs=-1, seed=None, **kwargs): + def __init__(self, hybrid=False, kernel="gaussian", n_jobs=-1, seed=None, **kwargs): """Init Kernel Density Estimation instance.""" self.kernel = kernel self.n_jobs = n_jobs @@ -29,20 +29,30 @@ class KDE(object): self.ae_net = None # autoencoder network for the case of a hybrid model self.results = { - 'train_time': None, - 'test_time': None, - 'test_auc': None, - 'test_scores': None + "train_time": None, + "test_time": None, + "test_auc": None, + "test_scores": None, } - def train(self, dataset: BaseADDataset, device: str = 'cpu', n_jobs_dataloader: int = 0, - bandwidth_GridSearchCV: bool = True): + def train( + self, + dataset: BaseADDataset, + device: str = "cpu", + n_jobs_dataloader: int = 0, + bandwidth_GridSearchCV: bool = True, + ): """Trains the Kernel Density Estimation model on the training data.""" logger = logging.getLogger() # do not drop last batch for non-SGD optimization shallow_ssad - train_loader = DataLoader(dataset=dataset.train_set, batch_size=128, shuffle=True, - num_workers=n_jobs_dataloader, drop_last=False) + train_loader = DataLoader( + dataset=dataset.train_set, + batch_size=128, + shuffle=True, + num_workers=n_jobs_dataloader, + drop_last=False, + ) # Get data from loader X = () @@ -50,39 +60,51 @@ class KDE(object): inputs, _, _, _ = data inputs = inputs.to(device) if self.hybrid: - inputs = self.ae_net.encoder(inputs) # in hybrid approach, take code representation of AE as features - X_batch = inputs.view(inputs.size(0), -1) # X_batch.shape = (batch_size, n_channels * height * width) + inputs = self.ae_net.encoder( + inputs + ) # in hybrid approach, take code representation of AE as features + X_batch = inputs.view( + inputs.size(0), -1 + ) # X_batch.shape = (batch_size, n_channels * height * width) X += (X_batch.cpu().data.numpy(),) X = np.concatenate(X) # Training - logger.info('Starting training...') + logger.info("Starting training...") start_time = time.time() if bandwidth_GridSearchCV: # use grid search cross-validation to select bandwidth - logger.info('Using GridSearchCV for bandwidth selection...') - params = {'bandwidth': np.logspace(0.5, 5, num=10, base=2)} - hyper_kde = GridSearchCV(KernelDensity(kernel=self.kernel), params, n_jobs=self.n_jobs, cv=5, verbose=0) + logger.info("Using GridSearchCV for bandwidth selection...") + params = {"bandwidth": np.logspace(0.5, 5, num=10, base=2)} + hyper_kde = GridSearchCV( + KernelDensity(kernel=self.kernel), + params, + n_jobs=self.n_jobs, + cv=5, + verbose=0, + ) hyper_kde.fit(X) self.bandwidth = hyper_kde.best_estimator_.bandwidth - logger.info('Best bandwidth: {:.8f}'.format(self.bandwidth)) + logger.info("Best bandwidth: {:.8f}".format(self.bandwidth)) self.model = hyper_kde.best_estimator_ else: # if exponential kernel, re-initialize kde with bandwidth minimizing the numerical error - if self.kernel == 'exponential': + if self.kernel == "exponential": self.bandwidth = np.max(pairwise_distances(X)) ** 2 self.model = KernelDensity(kernel=self.kernel, bandwidth=self.bandwidth) self.model.fit(X) train_time = time.time() - start_time - self.results['train_time'] = train_time + self.results["train_time"] = train_time - logger.info('Training Time: {:.3f}s'.format(self.results['train_time'])) - logger.info('Finished training.') + logger.info("Training Time: {:.3f}s".format(self.results["train_time"])) + logger.info("Finished training.") - def test(self, dataset: BaseADDataset, device: str = 'cpu', n_jobs_dataloader: int = 0): + def test( + self, dataset: BaseADDataset, device: str = "cpu", n_jobs_dataloader: int = 0 + ): """Tests the Kernel Density Estimation model on the test data.""" logger = logging.getLogger() @@ -95,46 +117,54 @@ class KDE(object): labels = [] for data in test_loader: inputs, label_batch, _, idx = data - inputs, label_batch, idx = inputs.to(device), label_batch.to(device), idx.to(device) + inputs, label_batch, idx = ( + inputs.to(device), + label_batch.to(device), + idx.to(device), + ) if self.hybrid: - inputs = self.ae_net.encoder(inputs) # in hybrid approach, take code representation of AE as features - X_batch = inputs.view(inputs.size(0), -1) # X_batch.shape = (batch_size, n_channels * height * width) + inputs = self.ae_net.encoder( + inputs + ) # in hybrid approach, take code representation of AE as features + X_batch = inputs.view( + inputs.size(0), -1 + ) # X_batch.shape = (batch_size, n_channels * height * width) X += (X_batch.cpu().data.numpy(),) idxs += idx.cpu().data.numpy().astype(np.int64).tolist() labels += label_batch.cpu().data.numpy().astype(np.int64).tolist() X = np.concatenate(X) # Testing - logger.info('Starting testing...') + logger.info("Starting testing...") start_time = time.time() scores = (-1.0) * self.model.score_samples(X) - self.results['test_time'] = time.time() - start_time + self.results["test_time"] = time.time() - start_time scores = scores.flatten() # Save triples of (idx, label, score) in a list idx_label_score += list(zip(idxs, labels, scores.tolist())) - self.results['test_scores'] = idx_label_score + self.results["test_scores"] = idx_label_score # Compute AUC _, labels, scores = zip(*idx_label_score) labels = np.array(labels) scores = np.array(scores) - self.results['test_auc'] = roc_auc_score(labels, scores) + self.results["test_auc"] = roc_auc_score(labels, scores) # Log results - logger.info('Test AUC: {:.2f}%'.format(100. * self.results['test_auc'])) - logger.info('Test Time: {:.3f}s'.format(self.results['test_time'])) - logger.info('Finished testing.') + logger.info("Test AUC: {:.2f}%".format(100.0 * self.results["test_auc"])) + logger.info("Test Time: {:.3f}s".format(self.results["test_time"])) + logger.info("Finished testing.") def load_ae(self, dataset_name, model_path): """Load pretrained autoencoder from model_path for feature extraction in a hybrid KDE model.""" - model_dict = torch.load(model_path, map_location='cpu') - ae_net_dict = model_dict['ae_net_dict'] - if dataset_name in ['mnist', 'fmnist', 'cifar10']: - net_name = dataset_name + '_LeNet' + model_dict = torch.load(model_path, map_location="cpu") + ae_net_dict = model_dict["ae_net_dict"] + if dataset_name in ["mnist", "fmnist", "cifar10"]: + net_name = dataset_name + "_LeNet" else: - net_name = dataset_name + '_mlp' + net_name = dataset_name + "_mlp" if self.ae_net is None: self.ae_net = build_autoencoder(net_name) @@ -154,11 +184,11 @@ class KDE(object): """Save KDE model to export_path.""" pass - def load_model(self, import_path, device: str = 'cpu'): + def load_model(self, import_path, device: str = "cpu"): """Load KDE model from import_path.""" pass def save_results(self, export_json): """Save results dict to a JSON-file.""" - with open(export_json, 'w') as fp: + with open(export_json, "w") as fp: json.dump(self.results, fp) diff --git a/Deep-SAD-PyTorch/src/baselines/ocsvm.py b/Deep-SAD-PyTorch/src/baselines/ocsvm.py index c8e5e2e..1e88a88 100644 --- a/Deep-SAD-PyTorch/src/baselines/ocsvm.py +++ b/Deep-SAD-PyTorch/src/baselines/ocsvm.py @@ -14,7 +14,7 @@ from networks.main import build_autoencoder class OCSVM(object): """A class for One-Class SVM models.""" - def __init__(self, kernel='rbf', nu=0.1, hybrid=False): + def __init__(self, kernel="rbf", nu=0.1, hybrid=False): """Init OCSVM instance.""" self.kernel = kernel self.nu = nu @@ -25,25 +25,34 @@ class OCSVM(object): self.hybrid = hybrid self.ae_net = None # autoencoder network for the case of a hybrid model - self.linear_model = None # also init a model with linear kernel if hybrid approach + self.linear_model = ( + None # also init a model with linear kernel if hybrid approach + ) self.results = { - 'train_time': None, - 'test_time': None, - 'test_auc': None, - 'test_scores': None, - 'train_time_linear': None, - 'test_time_linear': None, - 'test_auc_linear': None + "train_time": None, + "test_time": None, + "test_auc": None, + "test_scores": None, + "train_time_linear": None, + "test_time_linear": None, + "test_auc_linear": None, } - def train(self, dataset: BaseADDataset, device: str = 'cpu', n_jobs_dataloader: int = 0): + def train( + self, dataset: BaseADDataset, device: str = "cpu", n_jobs_dataloader: int = 0 + ): """Trains the OC-SVM model on the training data.""" logger = logging.getLogger() # do not drop last batch for non-SGD optimization shallow_ssad - train_loader = DataLoader(dataset=dataset.train_set, batch_size=128, shuffle=True, - num_workers=n_jobs_dataloader, drop_last=False) + train_loader = DataLoader( + dataset=dataset.train_set, + batch_size=128, + shuffle=True, + num_workers=n_jobs_dataloader, + drop_last=False, + ) # Get data from loader X = () @@ -51,13 +60,17 @@ class OCSVM(object): inputs, _, _, _ = data inputs = inputs.to(device) if self.hybrid: - inputs = self.ae_net.encoder(inputs) # in hybrid approach, take code representation of AE as features - X_batch = inputs.view(inputs.size(0), -1) # X_batch.shape = (batch_size, n_channels * height * width) + inputs = self.ae_net.encoder( + inputs + ) # in hybrid approach, take code representation of AE as features + X_batch = inputs.view( + inputs.size(0), -1 + ) # X_batch.shape = (batch_size, n_channels * height * width) X += (X_batch.cpu().data.numpy(),) X = np.concatenate(X) # Training - logger.info('Starting training...') + logger.info("Starting training...") # Select model via hold-out test set of 1000 samples gammas = np.logspace(-7, 2, num=10, base=2) @@ -72,17 +85,31 @@ class OCSVM(object): inputs, label_batch, _, _ = data inputs, label_batch = inputs.to(device), label_batch.to(device) if self.hybrid: - inputs = self.ae_net.encoder(inputs) # in hybrid approach, take code representation of AE as features - X_batch = inputs.view(inputs.size(0), -1) # X_batch.shape = (batch_size, n_channels * height * width) + inputs = self.ae_net.encoder( + inputs + ) # in hybrid approach, take code representation of AE as features + X_batch = inputs.view( + inputs.size(0), -1 + ) # X_batch.shape = (batch_size, n_channels * height * width) X_test += (X_batch.cpu().data.numpy(),) labels += label_batch.cpu().data.numpy().astype(np.int64).tolist() X_test, labels = np.concatenate(X_test), np.array(labels) - n_test, n_normal, n_outlier = len(X_test), np.sum(labels == 0), np.sum(labels == 1) + n_test, n_normal, n_outlier = ( + len(X_test), + np.sum(labels == 0), + np.sum(labels == 1), + ) n_val = int(0.1 * n_test) - n_val_normal, n_val_outlier = int(n_val * (n_normal/n_test)), int(n_val * (n_outlier/n_test)) + n_val_normal, n_val_outlier = int(n_val * (n_normal / n_test)), int( + n_val * (n_outlier / n_test) + ) perm = np.random.permutation(n_test) - X_val = np.concatenate((X_test[perm][labels[perm] == 0][:n_val_normal], - X_test[perm][labels[perm] == 1][:n_val_outlier])) + X_val = np.concatenate( + ( + X_test[perm][labels[perm] == 0][:n_val_normal], + X_test[perm][labels[perm] == 1][:n_val_outlier], + ) + ) labels = np.array([0] * n_val_normal + [1] * n_val_outlier) i = 1 @@ -103,30 +130,36 @@ class OCSVM(object): # Compute AUC auc = roc_auc_score(labels, scores) - logger.info(f' | Model {i:02}/{len(gammas):02} | Gamma: {gamma:.8f} | Train Time: {train_time:.3f}s ' - f'| Val AUC: {100. * auc:.2f} |') + logger.info( + f" | Model {i:02}/{len(gammas):02} | Gamma: {gamma:.8f} | Train Time: {train_time:.3f}s " + f"| Val AUC: {100. * auc:.2f} |" + ) if auc > best_auc: best_auc = auc self.model = model self.gamma = gamma - self.results['train_time'] = train_time + self.results["train_time"] = train_time i += 1 # If hybrid, also train a model with linear kernel if self.hybrid: - self.linear_model = OneClassSVM(kernel='linear', nu=self.nu) + self.linear_model = OneClassSVM(kernel="linear", nu=self.nu) start_time = time.time() self.linear_model.fit(X) train_time = time.time() - start_time - self.results['train_time_linear'] = train_time + self.results["train_time_linear"] = train_time - logger.info(f'Best Model: | Gamma: {self.gamma:.8f} | AUC: {100. * best_auc:.2f}') - logger.info('Training Time: {:.3f}s'.format(self.results['train_time'])) - logger.info('Finished training.') + logger.info( + f"Best Model: | Gamma: {self.gamma:.8f} | AUC: {100. * best_auc:.2f}" + ) + logger.info("Training Time: {:.3f}s".format(self.results["train_time"])) + logger.info("Finished training.") - def test(self, dataset: BaseADDataset, device: str = 'cpu', n_jobs_dataloader: int = 0): + def test( + self, dataset: BaseADDataset, device: str = "cpu", n_jobs_dataloader: int = 0 + ): """Tests the OC-SVM model on the test data.""" logger = logging.getLogger() @@ -139,59 +172,75 @@ class OCSVM(object): labels = [] for data in test_loader: inputs, label_batch, _, idx = data - inputs, label_batch, idx = inputs.to(device), label_batch.to(device), idx.to(device) + inputs, label_batch, idx = ( + inputs.to(device), + label_batch.to(device), + idx.to(device), + ) if self.hybrid: - inputs = self.ae_net.encoder(inputs) # in hybrid approach, take code representation of AE as features - X_batch = inputs.view(inputs.size(0), -1) # X_batch.shape = (batch_size, n_channels * height * width) + inputs = self.ae_net.encoder( + inputs + ) # in hybrid approach, take code representation of AE as features + X_batch = inputs.view( + inputs.size(0), -1 + ) # X_batch.shape = (batch_size, n_channels * height * width) X += (X_batch.cpu().data.numpy(),) idxs += idx.cpu().data.numpy().astype(np.int64).tolist() labels += label_batch.cpu().data.numpy().astype(np.int64).tolist() X = np.concatenate(X) # Testing - logger.info('Starting testing...') + logger.info("Starting testing...") start_time = time.time() scores = (-1.0) * self.model.decision_function(X) - self.results['test_time'] = time.time() - start_time + self.results["test_time"] = time.time() - start_time scores = scores.flatten() self.rho = -self.model.intercept_[0] # Save triples of (idx, label, score) in a list idx_label_score += list(zip(idxs, labels, scores.tolist())) - self.results['test_scores'] = idx_label_score + self.results["test_scores"] = idx_label_score # Compute AUC _, labels, scores = zip(*idx_label_score) labels = np.array(labels) scores = np.array(scores) - self.results['test_auc'] = roc_auc_score(labels, scores) + self.results["test_auc"] = roc_auc_score(labels, scores) # If hybrid, also test model with linear kernel if self.hybrid: start_time = time.time() scores_linear = (-1.0) * self.linear_model.decision_function(X) - self.results['test_time_linear'] = time.time() - start_time + self.results["test_time_linear"] = time.time() - start_time scores_linear = scores_linear.flatten() - self.results['test_auc_linear'] = roc_auc_score(labels, scores_linear) - logger.info('Test AUC linear model: {:.2f}%'.format(100. * self.results['test_auc_linear'])) - logger.info('Test Time linear model: {:.3f}s'.format(self.results['test_time_linear'])) + self.results["test_auc_linear"] = roc_auc_score(labels, scores_linear) + logger.info( + "Test AUC linear model: {:.2f}%".format( + 100.0 * self.results["test_auc_linear"] + ) + ) + logger.info( + "Test Time linear model: {:.3f}s".format( + self.results["test_time_linear"] + ) + ) # Log results - logger.info('Test AUC: {:.2f}%'.format(100. * self.results['test_auc'])) - logger.info('Test Time: {:.3f}s'.format(self.results['test_time'])) - logger.info('Finished testing.') + logger.info("Test AUC: {:.2f}%".format(100.0 * self.results["test_auc"])) + logger.info("Test Time: {:.3f}s".format(self.results["test_time"])) + logger.info("Finished testing.") def load_ae(self, dataset_name, model_path): """Load pretrained autoencoder from model_path for feature extraction in a hybrid OC-SVM model.""" - model_dict = torch.load(model_path, map_location='cpu') - ae_net_dict = model_dict['ae_net_dict'] - if dataset_name in ['mnist', 'fmnist', 'cifar10']: - net_name = dataset_name + '_LeNet' + model_dict = torch.load(model_path, map_location="cpu") + ae_net_dict = model_dict["ae_net_dict"] + if dataset_name in ["mnist", "fmnist", "cifar10"]: + net_name = dataset_name + "_LeNet" else: - net_name = dataset_name + '_mlp' + net_name = dataset_name + "_mlp" if self.ae_net is None: self.ae_net = build_autoencoder(net_name) @@ -211,11 +260,11 @@ class OCSVM(object): """Save OC-SVM model to export_path.""" pass - def load_model(self, import_path, device: str = 'cpu'): + def load_model(self, import_path, device: str = "cpu"): """Load OC-SVM model from import_path.""" pass def save_results(self, export_json): """Save results dict to a JSON-file.""" - with open(export_json, 'w') as fp: + with open(export_json, "w") as fp: json.dump(self.results, fp) diff --git a/Deep-SAD-PyTorch/src/baselines/shallow_ssad/ssad_convex.py b/Deep-SAD-PyTorch/src/baselines/shallow_ssad/ssad_convex.py index bc6aa31..ef9af26 100644 --- a/Deep-SAD-PyTorch/src/baselines/shallow_ssad/ssad_convex.py +++ b/Deep-SAD-PyTorch/src/baselines/shallow_ssad/ssad_convex.py @@ -8,31 +8,32 @@ from cvxopt.solvers import qp class ConvexSSAD: - """ Convex semi-supervised anomaly detection with hinge-loss and L2 regularizer - as described in Goernitz et al., Towards Supervised Anomaly Detection, JAIR, 2013 + """Convex semi-supervised anomaly detection with hinge-loss and L2 regularizer + as described in Goernitz et al., Towards Supervised Anomaly Detection, JAIR, 2013 - minimize 0.5 ||w||^2_2 - rho - kappa*gamma + eta_u sum_i xi_i + eta_l sum_j xi_j - {w,rho,gamma>=0,xi>=0} - subject to >= rho - xi_i - y_j >= y_j*rho + gamma - xi_j + minimize 0.5 ||w||^2_2 - rho - kappa*gamma + eta_u sum_i xi_i + eta_l sum_j xi_j + {w,rho,gamma>=0,xi>=0} + subject to >= rho - xi_i + y_j >= y_j*rho + gamma - xi_j - And the corresponding dual optimization problem: + And the corresponding dual optimization problem: - maximize -0.5 sum_(i,j) alpha_i alpha_j y_i y_j k(x_i,x_j) - {0<=alpha_i<=eta_i} - subject to kappa <= sum_j alpha_j (for all labeled examples) - 1 = sum_j y_i alpha_j (for all examples) + maximize -0.5 sum_(i,j) alpha_i alpha_j y_i y_j k(x_i,x_j) + {0<=alpha_i<=eta_i} + subject to kappa <= sum_j alpha_j (for all labeled examples) + 1 = sum_j y_i alpha_j (for all examples) - We introduce labels y_i = +1 for all unlabeled examples which enables us to combine sums. + We introduce labels y_i = +1 for all unlabeled examples which enables us to combine sums. - Note: Only dual solution is supported. + Note: Only dual solution is supported. - Written by: Nico Goernitz, TU Berlin, 2013/14 + Written by: Nico Goernitz, TU Berlin, 2013/14 """ + PRECISION = 1e-9 # important: effects the threshold, support vectors and speed! def __init__(self, kernel, y, kappa=1.0, Cp=1.0, Cu=1.0, Cn=1.0): - assert(len(y.shape) == 1) + assert len(y.shape) == 1 self.kernel = kernel self.y = y # (vector) corresponding labels (+1,-1 and 0 for unlabeled) self.kappa = kappa # (scalar) regularizer for importance of the margin @@ -53,7 +54,7 @@ class ConvexSSAD: self.cC = np.zeros(y.size) # cC=Cu (unlabeled) cC=Cp (pos) cC=Cn (neg) self.cC[y == 0] = Cu self.cC[y == 1] = Cp - self.cC[y ==-1] = Cn + self.cC[y == -1] = Cn self.alphas = None self.svs = None # (vector) list of support vector (contains indices) @@ -63,14 +64,18 @@ class ConvexSSAD: # the dual constraint kappa <= sum_{i \in labeled} alpha_i = 0.0 will # prohibit a solution if self.labeled == 0: - print('There are no labeled examples hence, setting kappa=0.0') + print("There are no labeled examples hence, setting kappa=0.0") self.kappa = 0.0 - print('Convex semi-supervised anomaly detection with {0} samples ({1} labeled).'.format(self.samples, self.labeled)) + print( + "Convex semi-supervised anomaly detection with {0} samples ({1} labeled).".format( + self.samples, self.labeled + ) + ) def set_train_kernel(self, kernel): dim1, dim2 = kernel.shape print([dim1, dim2]) - assert(dim1 == dim2 and dim1 == self.samples) + assert dim1 == dim2 and dim1 == self.samples self.kernel = kernel def fit(self, check_psd_eigs=False): @@ -81,20 +86,20 @@ class ConvexSSAD: Y = self.cy.dot(self.cy.T) # generate the final PDS kernel - P = matrix(self.kernel*Y) + P = matrix(self.kernel * Y) # check for PSD if check_psd_eigs: eigs = np.linalg.eigvalsh(np.array(P)) if eigs[0] < 0.0: - print('Smallest eigenvalue is {0}'.format(eigs[0])) + print("Smallest eigenvalue is {0}".format(eigs[0])) P += spdiag([-eigs[0] for i in range(N)]) # there is no linear part of the objective q = matrix(0.0, (N, 1)) # sum_i y_i alpha_i = A alpha = b = 1.0 - A = matrix(self.cy, (1, self.samples), 'd') + A = matrix(self.cy, (1, self.samples), "d") b = matrix(1.0, (1, 1)) # inequality constraints: G alpha <= h @@ -107,8 +112,8 @@ class ConvexSSAD: h = matrix([h1, h2]) if self.labeled > 0: # 3) kappa <= \sum_i labeled_i alpha_i -> -cl' alpha <= -kappa - print('Labeled data found.') - G3 = -matrix(self.cl, (1, self.cl.size), 'd') + print("Labeled data found.") + G3 = -matrix(self.cl, (1, self.cl.size), "d") h3 = -matrix(self.kappa, (1, 1)) G = sparse([G12, -G12, G3]) h = matrix([h1, h2, h3]) @@ -117,27 +122,49 @@ class ConvexSSAD: sol = qp(P, -q, G, h, A, b) # store solution - self.alphas = np.array(sol['x']) + self.alphas = np.array(sol["x"]) # 1. find all support vectors, i.e. 0 < alpha_i <= C # 2. store all support vector with alpha_i < C in 'margins' self.svs = np.where(self.alphas >= ConvexSSAD.PRECISION)[0] # these should sum to one - print('Validate solution:') - print('- found {0} support vectors'.format(len(self.svs))) - print('0 <= alpha_i : {0} of {1}'.format(np.sum(0. <= self.alphas), N)) - print('- sum_(i) alpha_i cy_i = {0} = 1.0'.format(np.sum(self.alphas*self.cy))) - print('- sum_(i in sv) alpha_i cy_i = {0} ~ 1.0 (approx error)'.format(np.sum(self.alphas[self.svs]*self.cy[self.svs]))) - print('- sum_(i in labeled) alpha_i = {0} >= {1} = kappa'.format(np.sum(self.alphas[self.cl == 1]), self.kappa)) - print('- sum_(i in unlabeled) alpha_i = {0}'.format(np.sum(self.alphas[self.y == 0]))) - print('- sum_(i in positives) alpha_i = {0}'.format(np.sum(self.alphas[self.y == 1]))) - print('- sum_(i in negatives) alpha_i = {0}'.format(np.sum(self.alphas[self.y ==-1]))) + print("Validate solution:") + print("- found {0} support vectors".format(len(self.svs))) + print("0 <= alpha_i : {0} of {1}".format(np.sum(0.0 <= self.alphas), N)) + print( + "- sum_(i) alpha_i cy_i = {0} = 1.0".format(np.sum(self.alphas * self.cy)) + ) + print( + "- sum_(i in sv) alpha_i cy_i = {0} ~ 1.0 (approx error)".format( + np.sum(self.alphas[self.svs] * self.cy[self.svs]) + ) + ) + print( + "- sum_(i in labeled) alpha_i = {0} >= {1} = kappa".format( + np.sum(self.alphas[self.cl == 1]), self.kappa + ) + ) + print( + "- sum_(i in unlabeled) alpha_i = {0}".format( + np.sum(self.alphas[self.y == 0]) + ) + ) + print( + "- sum_(i in positives) alpha_i = {0}".format( + np.sum(self.alphas[self.y == 1]) + ) + ) + print( + "- sum_(i in negatives) alpha_i = {0}".format( + np.sum(self.alphas[self.y == -1]) + ) + ) # infer threshold (rho) psvs = np.where(self.y[self.svs] == 0)[0] # case 1: unlabeled support vectors available - self.threshold = 0. + self.threshold = 0.0 unl_threshold = -1e12 lbl_threshold = -1e12 if psvs.size > 0: @@ -146,7 +173,7 @@ class ConvexSSAD: unl_threshold = np.max(self.apply(k)) if np.sum(self.cl) > 1e-12: - # case 2: only labeled examples available + # case 2: only labeled examples available k = self.kernel[:, self.svs] k = k[self.svs, :] thres = self.apply(k) @@ -154,7 +181,7 @@ class ConvexSSAD: ninds = np.where(self.y[self.svs] == -1)[0] # only negatives is not possible if ninds.size > 0 and pinds.size == 0: - print('ERROR: Check pre-defined PRECISION.') + print("ERROR: Check pre-defined PRECISION.") lbl_threshold = np.max(thres[ninds]) elif ninds.size == 0: lbl_threshold = np.max(thres[pinds]) @@ -162,7 +189,7 @@ class ConvexSSAD: # smallest negative + largest positive p = np.max(thres[pinds]) n = np.min(thres[ninds]) - lbl_threshold = (n+p)/2. + lbl_threshold = (n + p) / 2.0 self.threshold = np.max((unl_threshold, lbl_threshold)) def get_threshold(self): @@ -175,8 +202,8 @@ class ConvexSSAD: return self.alphas def apply(self, kernel): - """ Application of dual trained ssad. - kernel = get_kernel(Y, X[:, cssad.svs], kernel_type, kernel_param) + """Application of dual trained ssad. + kernel = get_kernel(Y, X[:, cssad.svs], kernel_type, kernel_param) """ if kernel.shape[1] == self.samples: # if kernel is not restricted to support vectors diff --git a/Deep-SAD-PyTorch/src/baselines/ssad.py b/Deep-SAD-PyTorch/src/baselines/ssad.py index 156ccfa..cab3187 100644 --- a/Deep-SAD-PyTorch/src/baselines/ssad.py +++ b/Deep-SAD-PyTorch/src/baselines/ssad.py @@ -17,7 +17,7 @@ class SSAD(object): A class for kernel SSAD models as described in Goernitz et al., Towards Supervised Anomaly Detection, JAIR, 2013. """ - def __init__(self, kernel='rbf', kappa=1.0, Cp=1.0, Cu=1.0, Cn=1.0, hybrid=False): + def __init__(self, kernel="rbf", kappa=1.0, Cp=1.0, Cu=1.0, Cn=1.0, hybrid=False): """Init SSAD instance.""" self.kernel = kernel self.kappa = kappa @@ -32,42 +32,59 @@ class SSAD(object): self.hybrid = hybrid self.ae_net = None # autoencoder network for the case of a hybrid model - self.linear_model = None # also init a model with linear kernel if hybrid approach + self.linear_model = ( + None # also init a model with linear kernel if hybrid approach + ) self.linear_X_svs = None self.results = { - 'train_time': None, - 'test_time': None, - 'test_auc': None, - 'test_scores': None, - 'train_time_linear': None, - 'test_time_linear': None, - 'test_auc_linear': None + "train_time": None, + "test_time": None, + "test_auc": None, + "test_scores": None, + "train_time_linear": None, + "test_time_linear": None, + "test_auc_linear": None, } - def train(self, dataset: BaseADDataset, device: str = 'cpu', n_jobs_dataloader: int = 0): + def train( + self, dataset: BaseADDataset, device: str = "cpu", n_jobs_dataloader: int = 0 + ): """Trains the SSAD model on the training data.""" logger = logging.getLogger() # do not drop last batch for non-SGD optimization shallow_ssad - train_loader = DataLoader(dataset=dataset.train_set, batch_size=128, shuffle=True, - num_workers=n_jobs_dataloader, drop_last=False) + train_loader = DataLoader( + dataset=dataset.train_set, + batch_size=128, + shuffle=True, + num_workers=n_jobs_dataloader, + drop_last=False, + ) # Get data from loader X = () semi_targets = [] for data in train_loader: inputs, _, semi_targets_batch, _ = data - inputs, semi_targets_batch = inputs.to(device), semi_targets_batch.to(device) + inputs, semi_targets_batch = inputs.to(device), semi_targets_batch.to( + device + ) if self.hybrid: - inputs = self.ae_net.encoder(inputs) # in hybrid approach, take code representation of AE as features - X_batch = inputs.view(inputs.size(0), -1) # X_batch.shape = (batch_size, n_channels * height * width) + inputs = self.ae_net.encoder( + inputs + ) # in hybrid approach, take code representation of AE as features + X_batch = inputs.view( + inputs.size(0), -1 + ) # X_batch.shape = (batch_size, n_channels * height * width) X += (X_batch.cpu().data.numpy(),) - semi_targets += semi_targets_batch.cpu().data.numpy().astype(np.int).tolist() + semi_targets += ( + semi_targets_batch.cpu().data.numpy().astype(np.int).tolist() + ) X, semi_targets = np.concatenate(X), np.array(semi_targets) # Training - logger.info('Starting training...') + logger.info("Starting training...") # Select model via hold-out test set of 1000 samples gammas = np.logspace(-7, 2, num=10, base=2) @@ -82,17 +99,31 @@ class SSAD(object): inputs, label_batch, _, _ = data inputs, label_batch = inputs.to(device), label_batch.to(device) if self.hybrid: - inputs = self.ae_net.encoder(inputs) # in hybrid approach, take code representation of AE as features - X_batch = inputs.view(inputs.size(0), -1) # X_batch.shape = (batch_size, n_channels * height * width) + inputs = self.ae_net.encoder( + inputs + ) # in hybrid approach, take code representation of AE as features + X_batch = inputs.view( + inputs.size(0), -1 + ) # X_batch.shape = (batch_size, n_channels * height * width) X_test += (X_batch.cpu().data.numpy(),) labels += label_batch.cpu().data.numpy().astype(np.int64).tolist() X_test, labels = np.concatenate(X_test), np.array(labels) - n_test, n_normal, n_outlier = len(X_test), np.sum(labels == 0), np.sum(labels == 1) + n_test, n_normal, n_outlier = ( + len(X_test), + np.sum(labels == 0), + np.sum(labels == 1), + ) n_val = int(0.1 * n_test) - n_val_normal, n_val_outlier = int(n_val * (n_normal/n_test)), int(n_val * (n_outlier/n_test)) + n_val_normal, n_val_outlier = int(n_val * (n_normal / n_test)), int( + n_val * (n_outlier / n_test) + ) perm = np.random.permutation(n_test) - X_val = np.concatenate((X_test[perm][labels[perm] == 0][:n_val_normal], - X_test[perm][labels[perm] == 1][:n_val_outlier])) + X_val = np.concatenate( + ( + X_test[perm][labels[perm] == 0][:n_val_normal], + X_test[perm][labels[perm] == 1][:n_val_outlier], + ) + ) labels = np.array([0] * n_val_normal + [1] * n_val_outlier) i = 1 @@ -110,21 +141,25 @@ class SSAD(object): train_time = time.time() - start_time # Test on small hold-out set from test set - kernel_val = pairwise_kernels(X_val, X[model.svs, :], metric=self.kernel, gamma=gamma) + kernel_val = pairwise_kernels( + X_val, X[model.svs, :], metric=self.kernel, gamma=gamma + ) scores = (-1.0) * model.apply(kernel_val) scores = scores.flatten() # Compute AUC auc = roc_auc_score(labels, scores) - logger.info(f' | Model {i:02}/{len(gammas):02} | Gamma: {gamma:.8f} | Train Time: {train_time:.3f}s ' - f'| Val AUC: {100. * auc:.2f} |') + logger.info( + f" | Model {i:02}/{len(gammas):02} | Gamma: {gamma:.8f} | Train Time: {train_time:.3f}s " + f"| Val AUC: {100. * auc:.2f} |" + ) if auc > best_auc: best_auc = auc self.model = model self.gamma = gamma - self.results['train_time'] = train_time + self.results["train_time"] = train_time i += 1 @@ -133,19 +168,25 @@ class SSAD(object): # If hybrid, also train a model with linear kernel if self.hybrid: - linear_kernel = pairwise_kernels(X, X, metric='linear') - self.linear_model = ConvexSSAD(linear_kernel, semi_targets, Cp=self.Cp, Cu=self.Cu, Cn=self.Cn) + linear_kernel = pairwise_kernels(X, X, metric="linear") + self.linear_model = ConvexSSAD( + linear_kernel, semi_targets, Cp=self.Cp, Cu=self.Cu, Cn=self.Cn + ) start_time = time.time() self.linear_model.fit() train_time = time.time() - start_time - self.results['train_time_linear'] = train_time + self.results["train_time_linear"] = train_time self.linear_X_svs = X[self.linear_model.svs, :] - logger.info(f'Best Model: | Gamma: {self.gamma:.8f} | AUC: {100. * best_auc:.2f}') - logger.info('Training Time: {:.3f}s'.format(self.results['train_time'])) - logger.info('Finished training.') + logger.info( + f"Best Model: | Gamma: {self.gamma:.8f} | AUC: {100. * best_auc:.2f}" + ) + logger.info("Training Time: {:.3f}s".format(self.results["train_time"])) + logger.info("Finished training.") - def test(self, dataset: BaseADDataset, device: str = 'cpu', n_jobs_dataloader: int = 0): + def test( + self, dataset: BaseADDataset, device: str = "cpu", n_jobs_dataloader: int = 0 + ): """Tests the SSAD model on the test data.""" logger = logging.getLogger() @@ -158,17 +199,25 @@ class SSAD(object): labels = [] for data in test_loader: inputs, label_batch, _, idx = data - inputs, label_batch, idx = inputs.to(device), label_batch.to(device), idx.to(device) + inputs, label_batch, idx = ( + inputs.to(device), + label_batch.to(device), + idx.to(device), + ) if self.hybrid: - inputs = self.ae_net.encoder(inputs) # in hybrid approach, take code representation of AE as features - X_batch = inputs.view(inputs.size(0), -1) # X_batch.shape = (batch_size, n_channels * height * width) + inputs = self.ae_net.encoder( + inputs + ) # in hybrid approach, take code representation of AE as features + X_batch = inputs.view( + inputs.size(0), -1 + ) # X_batch.shape = (batch_size, n_channels * height * width) X += (X_batch.cpu().data.numpy(),) idxs += idx.cpu().data.numpy().astype(np.int64).tolist() labels += label_batch.cpu().data.numpy().astype(np.int64).tolist() X = np.concatenate(X) # Testing - logger.info('Starting testing...') + logger.info("Starting testing...") start_time = time.time() # Build kernel @@ -176,45 +225,53 @@ class SSAD(object): scores = (-1.0) * self.model.apply(kernel) - self.results['test_time'] = time.time() - start_time + self.results["test_time"] = time.time() - start_time scores = scores.flatten() self.rho = -self.model.threshold # Save triples of (idx, label, score) in a list idx_label_score += list(zip(idxs, labels, scores.tolist())) - self.results['test_scores'] = idx_label_score + self.results["test_scores"] = idx_label_score # Compute AUC _, labels, scores = zip(*idx_label_score) labels = np.array(labels) scores = np.array(scores) - self.results['test_auc'] = roc_auc_score(labels, scores) + self.results["test_auc"] = roc_auc_score(labels, scores) # If hybrid, also test model with linear kernel if self.hybrid: start_time = time.time() - linear_kernel = pairwise_kernels(X, self.linear_X_svs, metric='linear') + linear_kernel = pairwise_kernels(X, self.linear_X_svs, metric="linear") scores_linear = (-1.0) * self.linear_model.apply(linear_kernel) - self.results['test_time_linear'] = time.time() - start_time + self.results["test_time_linear"] = time.time() - start_time scores_linear = scores_linear.flatten() - self.results['test_auc_linear'] = roc_auc_score(labels, scores_linear) - logger.info('Test AUC linear model: {:.2f}%'.format(100. * self.results['test_auc_linear'])) - logger.info('Test Time linear model: {:.3f}s'.format(self.results['test_time_linear'])) + self.results["test_auc_linear"] = roc_auc_score(labels, scores_linear) + logger.info( + "Test AUC linear model: {:.2f}%".format( + 100.0 * self.results["test_auc_linear"] + ) + ) + logger.info( + "Test Time linear model: {:.3f}s".format( + self.results["test_time_linear"] + ) + ) # Log results - logger.info('Test AUC: {:.2f}%'.format(100. * self.results['test_auc'])) - logger.info('Test Time: {:.3f}s'.format(self.results['test_time'])) - logger.info('Finished testing.') + logger.info("Test AUC: {:.2f}%".format(100.0 * self.results["test_auc"])) + logger.info("Test Time: {:.3f}s".format(self.results["test_time"])) + logger.info("Finished testing.") def load_ae(self, dataset_name, model_path): """Load pretrained autoencoder from model_path for feature extraction in a hybrid SSAD model.""" - model_dict = torch.load(model_path, map_location='cpu') - ae_net_dict = model_dict['ae_net_dict'] - if dataset_name in ['mnist', 'fmnist', 'cifar10']: - net_name = dataset_name + '_LeNet' + model_dict = torch.load(model_path, map_location="cpu") + ae_net_dict = model_dict["ae_net_dict"] + if dataset_name in ["mnist", "fmnist", "cifar10"]: + net_name = dataset_name + "_LeNet" else: - net_name = dataset_name + '_mlp' + net_name = dataset_name + "_mlp" if self.ae_net is None: self.ae_net = build_autoencoder(net_name) @@ -234,11 +291,11 @@ class SSAD(object): """Save SSAD model to export_path.""" pass - def load_model(self, import_path, device: str = 'cpu'): + def load_model(self, import_path, device: str = "cpu"): """Load SSAD model from import_path.""" pass def save_results(self, export_json): """Save results dict to a JSON-file.""" - with open(export_json, 'w') as fp: + with open(export_json, "w") as fp: json.dump(self.results, fp) diff --git a/Deep-SAD-PyTorch/src/datasets/cifar10.py b/Deep-SAD-PyTorch/src/datasets/cifar10.py index f3bfa1c..9d4d8c9 100644 --- a/Deep-SAD-PyTorch/src/datasets/cifar10.py +++ b/Deep-SAD-PyTorch/src/datasets/cifar10.py @@ -12,8 +12,16 @@ import numpy as np class CIFAR10_Dataset(TorchvisionDataset): - def __init__(self, root: str, normal_class: int = 5, known_outlier_class: int = 3, n_known_outlier_classes: int = 0, - ratio_known_normal: float = 0.0, ratio_known_outlier: float = 0.0, ratio_pollution: float = 0.0): + def __init__( + self, + root: str, + normal_class: int = 5, + known_outlier_class: int = 3, + n_known_outlier_classes: int = 0, + ratio_known_normal: float = 0.0, + ratio_known_outlier: float = 0.0, + ratio_pollution: float = 0.0, + ): super().__init__(root) # Define normal and outlier classes @@ -28,28 +36,48 @@ class CIFAR10_Dataset(TorchvisionDataset): elif n_known_outlier_classes == 1: self.known_outlier_classes = tuple([known_outlier_class]) else: - self.known_outlier_classes = tuple(random.sample(self.outlier_classes, n_known_outlier_classes)) + self.known_outlier_classes = tuple( + random.sample(self.outlier_classes, n_known_outlier_classes) + ) # CIFAR-10 preprocessing: feature scaling to [0, 1] transform = transforms.ToTensor() target_transform = transforms.Lambda(lambda x: int(x in self.outlier_classes)) # Get train set - train_set = MyCIFAR10(root=self.root, train=True, transform=transform, target_transform=target_transform, - download=True) + train_set = MyCIFAR10( + root=self.root, + train=True, + transform=transform, + target_transform=target_transform, + download=True, + ) # Create semi-supervised setting - idx, _, semi_targets = create_semisupervised_setting(np.array(train_set.targets), self.normal_classes, - self.outlier_classes, self.known_outlier_classes, - ratio_known_normal, ratio_known_outlier, ratio_pollution) - train_set.semi_targets[idx] = torch.tensor(semi_targets) # set respective semi-supervised labels + idx, _, semi_targets = create_semisupervised_setting( + np.array(train_set.targets), + self.normal_classes, + self.outlier_classes, + self.known_outlier_classes, + ratio_known_normal, + ratio_known_outlier, + ratio_pollution, + ) + train_set.semi_targets[idx] = torch.tensor( + semi_targets + ) # set respective semi-supervised labels # Subset train_set to semi-supervised setup self.train_set = Subset(train_set, idx) # Get test set - self.test_set = MyCIFAR10(root=self.root, train=False, transform=transform, target_transform=target_transform, - download=True) + self.test_set = MyCIFAR10( + root=self.root, + train=False, + transform=transform, + target_transform=target_transform, + download=True, + ) class MyCIFAR10(CIFAR10): @@ -71,7 +99,11 @@ class MyCIFAR10(CIFAR10): Returns: tuple: (image, target, semi_target, index) """ - img, target, semi_target = self.data[index], self.targets[index], int(self.semi_targets[index]) + img, target, semi_target = ( + self.data[index], + self.targets[index], + int(self.semi_targets[index]), + ) # doing this so that it is consistent with all other datasets # to return a PIL Image diff --git a/Deep-SAD-PyTorch/src/datasets/fmnist.py b/Deep-SAD-PyTorch/src/datasets/fmnist.py index 2a6df2f..2b8dd66 100644 --- a/Deep-SAD-PyTorch/src/datasets/fmnist.py +++ b/Deep-SAD-PyTorch/src/datasets/fmnist.py @@ -11,8 +11,16 @@ import random class FashionMNIST_Dataset(TorchvisionDataset): - def __init__(self, root: str, normal_class: int = 0, known_outlier_class: int = 1, n_known_outlier_classes: int = 0, - ratio_known_normal: float = 0.0, ratio_known_outlier: float = 0.0, ratio_pollution: float = 0.0): + def __init__( + self, + root: str, + normal_class: int = 0, + known_outlier_class: int = 1, + n_known_outlier_classes: int = 0, + ratio_known_normal: float = 0.0, + ratio_known_outlier: float = 0.0, + ratio_pollution: float = 0.0, + ): super().__init__(root) # Define normal and outlier classes @@ -27,28 +35,48 @@ class FashionMNIST_Dataset(TorchvisionDataset): elif n_known_outlier_classes == 1: self.known_outlier_classes = tuple([known_outlier_class]) else: - self.known_outlier_classes = tuple(random.sample(self.outlier_classes, n_known_outlier_classes)) + self.known_outlier_classes = tuple( + random.sample(self.outlier_classes, n_known_outlier_classes) + ) # FashionMNIST preprocessing: feature scaling to [0, 1] transform = transforms.ToTensor() target_transform = transforms.Lambda(lambda x: int(x in self.outlier_classes)) # Get train set - train_set = MyFashionMNIST(root=self.root, train=True, transform=transform, target_transform=target_transform, - download=True) + train_set = MyFashionMNIST( + root=self.root, + train=True, + transform=transform, + target_transform=target_transform, + download=True, + ) # Create semi-supervised setting - idx, _, semi_targets = create_semisupervised_setting(train_set.targets.cpu().data.numpy(), self.normal_classes, - self.outlier_classes, self.known_outlier_classes, - ratio_known_normal, ratio_known_outlier, ratio_pollution) - train_set.semi_targets[idx] = torch.tensor(semi_targets) # set respective semi-supervised labels + idx, _, semi_targets = create_semisupervised_setting( + train_set.targets.cpu().data.numpy(), + self.normal_classes, + self.outlier_classes, + self.known_outlier_classes, + ratio_known_normal, + ratio_known_outlier, + ratio_pollution, + ) + train_set.semi_targets[idx] = torch.tensor( + semi_targets + ) # set respective semi-supervised labels # Subset train_set to semi-supervised setup self.train_set = Subset(train_set, idx) # Get test set - self.test_set = MyFashionMNIST(root=self.root, train=False, transform=transform, - target_transform=target_transform, download=True) + self.test_set = MyFashionMNIST( + root=self.root, + train=False, + transform=transform, + target_transform=target_transform, + download=True, + ) class MyFashionMNIST(FashionMNIST): @@ -70,11 +98,15 @@ class MyFashionMNIST(FashionMNIST): Returns: tuple: (image, target, semi_target, index) """ - img, target, semi_target = self.data[index], int(self.targets[index]), int(self.semi_targets[index]) + img, target, semi_target = ( + self.data[index], + int(self.targets[index]), + int(self.semi_targets[index]), + ) # doing this so that it is consistent with all other datasets # to return a PIL Image - img = Image.fromarray(img.numpy(), mode='L') + img = Image.fromarray(img.numpy(), mode="L") if self.transform is not None: img = self.transform(img) diff --git a/Deep-SAD-PyTorch/src/datasets/main.py b/Deep-SAD-PyTorch/src/datasets/main.py index 845b36a..2ce2c2a 100644 --- a/Deep-SAD-PyTorch/src/datasets/main.py +++ b/Deep-SAD-PyTorch/src/datasets/main.py @@ -4,51 +4,83 @@ from .cifar10 import CIFAR10_Dataset from .odds import ODDSADDataset -def load_dataset(dataset_name, data_path, normal_class, known_outlier_class, n_known_outlier_classes: int = 0, - ratio_known_normal: float = 0.0, ratio_known_outlier: float = 0.0, ratio_pollution: float = 0.0, - random_state=None): +def load_dataset( + dataset_name, + data_path, + normal_class, + known_outlier_class, + n_known_outlier_classes: int = 0, + ratio_known_normal: float = 0.0, + ratio_known_outlier: float = 0.0, + ratio_pollution: float = 0.0, + random_state=None, +): """Loads the dataset.""" - implemented_datasets = ('mnist', 'fmnist', 'cifar10', - 'arrhythmia', 'cardio', 'satellite', 'satimage-2', 'shuttle', 'thyroid') + implemented_datasets = ( + "mnist", + "fmnist", + "cifar10", + "arrhythmia", + "cardio", + "satellite", + "satimage-2", + "shuttle", + "thyroid", + ) assert dataset_name in implemented_datasets dataset = None - if dataset_name == 'mnist': - dataset = MNIST_Dataset(root=data_path, - normal_class=normal_class, - known_outlier_class=known_outlier_class, - n_known_outlier_classes=n_known_outlier_classes, - ratio_known_normal=ratio_known_normal, - ratio_known_outlier=ratio_known_outlier, - ratio_pollution=ratio_pollution) + if dataset_name == "mnist": + dataset = MNIST_Dataset( + root=data_path, + normal_class=normal_class, + known_outlier_class=known_outlier_class, + n_known_outlier_classes=n_known_outlier_classes, + ratio_known_normal=ratio_known_normal, + ratio_known_outlier=ratio_known_outlier, + ratio_pollution=ratio_pollution, + ) - if dataset_name == 'fmnist': - dataset = FashionMNIST_Dataset(root=data_path, - normal_class=normal_class, - known_outlier_class=known_outlier_class, - n_known_outlier_classes=n_known_outlier_classes, - ratio_known_normal=ratio_known_normal, - ratio_known_outlier=ratio_known_outlier, - ratio_pollution=ratio_pollution) + if dataset_name == "fmnist": + dataset = FashionMNIST_Dataset( + root=data_path, + normal_class=normal_class, + known_outlier_class=known_outlier_class, + n_known_outlier_classes=n_known_outlier_classes, + ratio_known_normal=ratio_known_normal, + ratio_known_outlier=ratio_known_outlier, + ratio_pollution=ratio_pollution, + ) - if dataset_name == 'cifar10': - dataset = CIFAR10_Dataset(root=data_path, - normal_class=normal_class, - known_outlier_class=known_outlier_class, - n_known_outlier_classes=n_known_outlier_classes, - ratio_known_normal=ratio_known_normal, - ratio_known_outlier=ratio_known_outlier, - ratio_pollution=ratio_pollution) + if dataset_name == "cifar10": + dataset = CIFAR10_Dataset( + root=data_path, + normal_class=normal_class, + known_outlier_class=known_outlier_class, + n_known_outlier_classes=n_known_outlier_classes, + ratio_known_normal=ratio_known_normal, + ratio_known_outlier=ratio_known_outlier, + ratio_pollution=ratio_pollution, + ) - if dataset_name in ('arrhythmia', 'cardio', 'satellite', 'satimage-2', 'shuttle', 'thyroid'): - dataset = ODDSADDataset(root=data_path, - dataset_name=dataset_name, - n_known_outlier_classes=n_known_outlier_classes, - ratio_known_normal=ratio_known_normal, - ratio_known_outlier=ratio_known_outlier, - ratio_pollution=ratio_pollution, - random_state=random_state) + if dataset_name in ( + "arrhythmia", + "cardio", + "satellite", + "satimage-2", + "shuttle", + "thyroid", + ): + dataset = ODDSADDataset( + root=data_path, + dataset_name=dataset_name, + n_known_outlier_classes=n_known_outlier_classes, + ratio_known_normal=ratio_known_normal, + ratio_known_outlier=ratio_known_outlier, + ratio_pollution=ratio_pollution, + random_state=random_state, + ) return dataset diff --git a/Deep-SAD-PyTorch/src/datasets/mnist.py b/Deep-SAD-PyTorch/src/datasets/mnist.py index 1999264..37e1d1a 100644 --- a/Deep-SAD-PyTorch/src/datasets/mnist.py +++ b/Deep-SAD-PyTorch/src/datasets/mnist.py @@ -11,8 +11,16 @@ import random class MNIST_Dataset(TorchvisionDataset): - def __init__(self, root: str, normal_class: int = 0, known_outlier_class: int = 1, n_known_outlier_classes: int = 0, - ratio_known_normal: float = 0.0, ratio_known_outlier: float = 0.0, ratio_pollution: float = 0.0): + def __init__( + self, + root: str, + normal_class: int = 0, + known_outlier_class: int = 1, + n_known_outlier_classes: int = 0, + ratio_known_normal: float = 0.0, + ratio_known_outlier: float = 0.0, + ratio_pollution: float = 0.0, + ): super().__init__(root) # Define normal and outlier classes @@ -27,28 +35,48 @@ class MNIST_Dataset(TorchvisionDataset): elif n_known_outlier_classes == 1: self.known_outlier_classes = tuple([known_outlier_class]) else: - self.known_outlier_classes = tuple(random.sample(self.outlier_classes, n_known_outlier_classes)) + self.known_outlier_classes = tuple( + random.sample(self.outlier_classes, n_known_outlier_classes) + ) # MNIST preprocessing: feature scaling to [0, 1] transform = transforms.ToTensor() target_transform = transforms.Lambda(lambda x: int(x in self.outlier_classes)) # Get train set - train_set = MyMNIST(root=self.root, train=True, transform=transform, target_transform=target_transform, - download=True) + train_set = MyMNIST( + root=self.root, + train=True, + transform=transform, + target_transform=target_transform, + download=True, + ) # Create semi-supervised setting - idx, _, semi_targets = create_semisupervised_setting(train_set.targets.cpu().data.numpy(), self.normal_classes, - self.outlier_classes, self.known_outlier_classes, - ratio_known_normal, ratio_known_outlier, ratio_pollution) - train_set.semi_targets[idx] = torch.tensor(semi_targets) # set respective semi-supervised labels + idx, _, semi_targets = create_semisupervised_setting( + train_set.targets.cpu().data.numpy(), + self.normal_classes, + self.outlier_classes, + self.known_outlier_classes, + ratio_known_normal, + ratio_known_outlier, + ratio_pollution, + ) + train_set.semi_targets[idx] = torch.tensor( + semi_targets + ) # set respective semi-supervised labels # Subset train_set to semi-supervised setup self.train_set = Subset(train_set, idx) # Get test set - self.test_set = MyMNIST(root=self.root, train=False, transform=transform, target_transform=target_transform, - download=True) + self.test_set = MyMNIST( + root=self.root, + train=False, + transform=transform, + target_transform=target_transform, + download=True, + ) class MyMNIST(MNIST): @@ -70,11 +98,15 @@ class MyMNIST(MNIST): Returns: tuple: (image, target, semi_target, index) """ - img, target, semi_target = self.data[index], int(self.targets[index]), int(self.semi_targets[index]) + img, target, semi_target = ( + self.data[index], + int(self.targets[index]), + int(self.semi_targets[index]), + ) # doing this so that it is consistent with all other datasets # to return a PIL Image - img = Image.fromarray(img.numpy(), mode='L') + img = Image.fromarray(img.numpy(), mode="L") if self.transform is not None: img = self.transform(img) diff --git a/Deep-SAD-PyTorch/src/datasets/odds.py b/Deep-SAD-PyTorch/src/datasets/odds.py index aec907b..883586e 100644 --- a/Deep-SAD-PyTorch/src/datasets/odds.py +++ b/Deep-SAD-PyTorch/src/datasets/odds.py @@ -8,8 +8,16 @@ import torch class ODDSADDataset(BaseADDataset): - def __init__(self, root: str, dataset_name: str, n_known_outlier_classes: int = 0, ratio_known_normal: float = 0.0, - ratio_known_outlier: float = 0.0, ratio_pollution: float = 0.0, random_state=None): + def __init__( + self, + root: str, + dataset_name: str, + n_known_outlier_classes: int = 0, + ratio_known_normal: float = 0.0, + ratio_known_outlier: float = 0.0, + ratio_pollution: float = 0.0, + random_state=None, + ): super().__init__(root) # Define normal and outlier classes @@ -23,25 +31,58 @@ class ODDSADDataset(BaseADDataset): self.known_outlier_classes = (1,) # Get train set - train_set = ODDSDataset(root=self.root, dataset_name=dataset_name, train=True, random_state=random_state, - download=True) + train_set = ODDSDataset( + root=self.root, + dataset_name=dataset_name, + train=True, + random_state=random_state, + download=True, + ) # Create semi-supervised setting - idx, _, semi_targets = create_semisupervised_setting(train_set.targets.cpu().data.numpy(), self.normal_classes, - self.outlier_classes, self.known_outlier_classes, - ratio_known_normal, ratio_known_outlier, ratio_pollution) - train_set.semi_targets[idx] = torch.tensor(semi_targets) # set respective semi-supervised labels + idx, _, semi_targets = create_semisupervised_setting( + train_set.targets.cpu().data.numpy(), + self.normal_classes, + self.outlier_classes, + self.known_outlier_classes, + ratio_known_normal, + ratio_known_outlier, + ratio_pollution, + ) + train_set.semi_targets[idx] = torch.tensor( + semi_targets + ) # set respective semi-supervised labels # Subset train_set to semi-supervised setup self.train_set = Subset(train_set, idx) # Get test set - self.test_set = ODDSDataset(root=self.root, dataset_name=dataset_name, train=False, random_state=random_state) + self.test_set = ODDSDataset( + root=self.root, + dataset_name=dataset_name, + train=False, + random_state=random_state, + ) - def loaders(self, batch_size: int, shuffle_train=True, shuffle_test=False, num_workers: int = 0) -> ( - DataLoader, DataLoader): - train_loader = DataLoader(dataset=self.train_set, batch_size=batch_size, shuffle=shuffle_train, - num_workers=num_workers, drop_last=True) - test_loader = DataLoader(dataset=self.test_set, batch_size=batch_size, shuffle=shuffle_test, - num_workers=num_workers, drop_last=False) + def loaders( + self, + batch_size: int, + shuffle_train=True, + shuffle_test=False, + num_workers: int = 0, + ) -> (DataLoader, DataLoader): + train_loader = DataLoader( + dataset=self.train_set, + batch_size=batch_size, + shuffle=shuffle_train, + num_workers=num_workers, + drop_last=True, + ) + test_loader = DataLoader( + dataset=self.test_set, + batch_size=batch_size, + shuffle=shuffle_test, + num_workers=num_workers, + drop_last=False, + ) return train_loader, test_loader diff --git a/Deep-SAD-PyTorch/src/datasets/preprocessing.py b/Deep-SAD-PyTorch/src/datasets/preprocessing.py index 5d3612d..cc98b8c 100644 --- a/Deep-SAD-PyTorch/src/datasets/preprocessing.py +++ b/Deep-SAD-PyTorch/src/datasets/preprocessing.py @@ -2,10 +2,17 @@ import torch import numpy as np -def create_semisupervised_setting(labels, normal_classes, outlier_classes, known_outlier_classes, - ratio_known_normal, ratio_known_outlier, ratio_pollution): +def create_semisupervised_setting( + labels, + normal_classes, + outlier_classes, + known_outlier_classes, + ratio_known_normal, + ratio_known_outlier, + ratio_pollution, +): """ - Create a semi-supervised data setting. + Create a semi-supervised data setting. :param labels: np.array with labels of all dataset samples :param normal_classes: tuple with normal class labels :param outlier_classes: tuple with anomaly class labels @@ -17,15 +24,31 @@ def create_semisupervised_setting(labels, normal_classes, outlier_classes, known """ idx_normal = np.argwhere(np.isin(labels, normal_classes)).flatten() idx_outlier = np.argwhere(np.isin(labels, outlier_classes)).flatten() - idx_known_outlier_candidates = np.argwhere(np.isin(labels, known_outlier_classes)).flatten() + idx_known_outlier_candidates = np.argwhere( + np.isin(labels, known_outlier_classes) + ).flatten() n_normal = len(idx_normal) # Solve system of linear equations to obtain respective number of samples - a = np.array([[1, 1, 0, 0], - [(1-ratio_known_normal), -ratio_known_normal, -ratio_known_normal, -ratio_known_normal], - [-ratio_known_outlier, -ratio_known_outlier, -ratio_known_outlier, (1-ratio_known_outlier)], - [0, -ratio_pollution, (1-ratio_pollution), 0]]) + a = np.array( + [ + [1, 1, 0, 0], + [ + (1 - ratio_known_normal), + -ratio_known_normal, + -ratio_known_normal, + -ratio_known_normal, + ], + [ + -ratio_known_outlier, + -ratio_known_outlier, + -ratio_known_outlier, + (1 - ratio_known_outlier), + ], + [0, -ratio_pollution, (1 - ratio_pollution), 0], + ] + ) b = np.array([n_normal, 0, 0, 0]) x = np.linalg.solve(a, b) @@ -41,9 +64,13 @@ def create_semisupervised_setting(labels, normal_classes, outlier_classes, known perm_known_outlier = np.random.permutation(len(idx_known_outlier_candidates)) idx_known_normal = idx_normal[perm_normal[:n_known_normal]].tolist() - idx_unlabeled_normal = idx_normal[perm_normal[n_known_normal:n_known_normal+n_unlabeled_normal]].tolist() + idx_unlabeled_normal = idx_normal[ + perm_normal[n_known_normal : n_known_normal + n_unlabeled_normal] + ].tolist() idx_unlabeled_outlier = idx_outlier[perm_outlier[:n_unlabeled_outlier]].tolist() - idx_known_outlier = idx_known_outlier_candidates[perm_known_outlier[:n_known_outlier]].tolist() + idx_known_outlier = idx_known_outlier_candidates[ + perm_known_outlier[:n_known_outlier] + ].tolist() # Get original class labels labels_known_normal = labels[idx_known_normal].tolist() @@ -53,14 +80,32 @@ def create_semisupervised_setting(labels, normal_classes, outlier_classes, known # Get semi-supervised setting labels semi_labels_known_normal = np.ones(n_known_normal).astype(np.int32).tolist() - semi_labels_unlabeled_normal = np.zeros(n_unlabeled_normal).astype(np.int32).tolist() - semi_labels_unlabeled_outlier = np.zeros(n_unlabeled_outlier).astype(np.int32).tolist() + semi_labels_unlabeled_normal = ( + np.zeros(n_unlabeled_normal).astype(np.int32).tolist() + ) + semi_labels_unlabeled_outlier = ( + np.zeros(n_unlabeled_outlier).astype(np.int32).tolist() + ) semi_labels_known_outlier = (-np.ones(n_known_outlier).astype(np.int32)).tolist() # Create final lists - list_idx = idx_known_normal + idx_unlabeled_normal + idx_unlabeled_outlier + idx_known_outlier - list_labels = labels_known_normal + labels_unlabeled_normal + labels_unlabeled_outlier + labels_known_outlier - list_semi_labels = (semi_labels_known_normal + semi_labels_unlabeled_normal + semi_labels_unlabeled_outlier - + semi_labels_known_outlier) + list_idx = ( + idx_known_normal + + idx_unlabeled_normal + + idx_unlabeled_outlier + + idx_known_outlier + ) + list_labels = ( + labels_known_normal + + labels_unlabeled_normal + + labels_unlabeled_outlier + + labels_known_outlier + ) + list_semi_labels = ( + semi_labels_known_normal + + semi_labels_unlabeled_normal + + semi_labels_unlabeled_outlier + + semi_labels_known_outlier + ) return list_idx, list_labels, list_semi_labels diff --git a/Deep-SAD-PyTorch/src/main.py b/Deep-SAD-PyTorch/src/main.py index 1e3d78e..f81d237 100644 --- a/Deep-SAD-PyTorch/src/main.py +++ b/Deep-SAD-PyTorch/src/main.py @@ -14,66 +14,222 @@ from datasets.main import load_dataset # Settings ################################################################################ @click.command() -@click.argument('dataset_name', type=click.Choice(['mnist', 'fmnist', 'cifar10', 'arrhythmia', 'cardio', 'satellite', - 'satimage-2', 'shuttle', 'thyroid'])) -@click.argument('net_name', type=click.Choice(['mnist_LeNet', 'fmnist_LeNet', 'cifar10_LeNet', 'arrhythmia_mlp', - 'cardio_mlp', 'satellite_mlp', 'satimage-2_mlp', 'shuttle_mlp', - 'thyroid_mlp'])) -@click.argument('xp_path', type=click.Path(exists=True)) -@click.argument('data_path', type=click.Path(exists=True)) -@click.option('--load_config', type=click.Path(exists=True), default=None, - help='Config JSON-file path (default: None).') -@click.option('--load_model', type=click.Path(exists=True), default=None, - help='Model file path (default: None).') -@click.option('--eta', type=float, default=1.0, help='Deep SAD hyperparameter eta (must be 0 < eta).') -@click.option('--ratio_known_normal', type=float, default=0.0, - help='Ratio of known (labeled) normal training examples.') -@click.option('--ratio_known_outlier', type=float, default=0.0, - help='Ratio of known (labeled) anomalous training examples.') -@click.option('--ratio_pollution', type=float, default=0.0, - help='Pollution ratio of unlabeled training data with unknown (unlabeled) anomalies.') -@click.option('--device', type=str, default='cuda', help='Computation device to use ("cpu", "cuda", "cuda:2", etc.).') -@click.option('--seed', type=int, default=-1, help='Set seed. If -1, use randomization.') -@click.option('--optimizer_name', type=click.Choice(['adam']), default='adam', - help='Name of the optimizer to use for Deep SAD network training.') -@click.option('--lr', type=float, default=0.001, - help='Initial learning rate for Deep SAD network training. Default=0.001') -@click.option('--n_epochs', type=int, default=50, help='Number of epochs to train.') -@click.option('--lr_milestone', type=int, default=0, multiple=True, - help='Lr scheduler milestones at which lr is multiplied by 0.1. Can be multiple and must be increasing.') -@click.option('--batch_size', type=int, default=128, help='Batch size for mini-batch training.') -@click.option('--weight_decay', type=float, default=1e-6, - help='Weight decay (L2 penalty) hyperparameter for Deep SAD objective.') -@click.option('--pretrain', type=bool, default=True, - help='Pretrain neural network parameters via autoencoder.') -@click.option('--ae_optimizer_name', type=click.Choice(['adam']), default='adam', - help='Name of the optimizer to use for autoencoder pretraining.') -@click.option('--ae_lr', type=float, default=0.001, - help='Initial learning rate for autoencoder pretraining. Default=0.001') -@click.option('--ae_n_epochs', type=int, default=100, help='Number of epochs to train autoencoder.') -@click.option('--ae_lr_milestone', type=int, default=0, multiple=True, - help='Lr scheduler milestones at which lr is multiplied by 0.1. Can be multiple and must be increasing.') -@click.option('--ae_batch_size', type=int, default=128, help='Batch size for mini-batch autoencoder training.') -@click.option('--ae_weight_decay', type=float, default=1e-6, - help='Weight decay (L2 penalty) hyperparameter for autoencoder objective.') -@click.option('--num_threads', type=int, default=0, - help='Number of threads used for parallelizing CPU operations. 0 means that all resources are used.') -@click.option('--n_jobs_dataloader', type=int, default=0, - help='Number of workers for data loading. 0 means that the data will be loaded in the main process.') -@click.option('--normal_class', type=int, default=0, - help='Specify the normal class of the dataset (all other classes are considered anomalous).') -@click.option('--known_outlier_class', type=int, default=1, - help='Specify the known outlier class of the dataset for semi-supervised anomaly detection.') -@click.option('--n_known_outlier_classes', type=int, default=0, - help='Number of known outlier classes.' - 'If 0, no anomalies are known.' - 'If 1, outlier class as specified in --known_outlier_class option.' - 'If > 1, the specified number of outlier classes will be sampled at random.') -def main(dataset_name, net_name, xp_path, data_path, load_config, load_model, eta, - ratio_known_normal, ratio_known_outlier, ratio_pollution, device, seed, - optimizer_name, lr, n_epochs, lr_milestone, batch_size, weight_decay, - pretrain, ae_optimizer_name, ae_lr, ae_n_epochs, ae_lr_milestone, ae_batch_size, ae_weight_decay, - num_threads, n_jobs_dataloader, normal_class, known_outlier_class, n_known_outlier_classes): +@click.argument( + "dataset_name", + type=click.Choice( + [ + "mnist", + "fmnist", + "cifar10", + "arrhythmia", + "cardio", + "satellite", + "satimage-2", + "shuttle", + "thyroid", + ] + ), +) +@click.argument( + "net_name", + type=click.Choice( + [ + "mnist_LeNet", + "fmnist_LeNet", + "cifar10_LeNet", + "arrhythmia_mlp", + "cardio_mlp", + "satellite_mlp", + "satimage-2_mlp", + "shuttle_mlp", + "thyroid_mlp", + ] + ), +) +@click.argument("xp_path", type=click.Path(exists=True)) +@click.argument("data_path", type=click.Path(exists=True)) +@click.option( + "--load_config", + type=click.Path(exists=True), + default=None, + help="Config JSON-file path (default: None).", +) +@click.option( + "--load_model", + type=click.Path(exists=True), + default=None, + help="Model file path (default: None).", +) +@click.option( + "--eta", + type=float, + default=1.0, + help="Deep SAD hyperparameter eta (must be 0 < eta).", +) +@click.option( + "--ratio_known_normal", + type=float, + default=0.0, + help="Ratio of known (labeled) normal training examples.", +) +@click.option( + "--ratio_known_outlier", + type=float, + default=0.0, + help="Ratio of known (labeled) anomalous training examples.", +) +@click.option( + "--ratio_pollution", + type=float, + default=0.0, + help="Pollution ratio of unlabeled training data with unknown (unlabeled) anomalies.", +) +@click.option( + "--device", + type=str, + default="cuda", + help='Computation device to use ("cpu", "cuda", "cuda:2", etc.).', +) +@click.option( + "--seed", type=int, default=-1, help="Set seed. If -1, use randomization." +) +@click.option( + "--optimizer_name", + type=click.Choice(["adam"]), + default="adam", + help="Name of the optimizer to use for Deep SAD network training.", +) +@click.option( + "--lr", + type=float, + default=0.001, + help="Initial learning rate for Deep SAD network training. Default=0.001", +) +@click.option("--n_epochs", type=int, default=50, help="Number of epochs to train.") +@click.option( + "--lr_milestone", + type=int, + default=0, + multiple=True, + help="Lr scheduler milestones at which lr is multiplied by 0.1. Can be multiple and must be increasing.", +) +@click.option( + "--batch_size", type=int, default=128, help="Batch size for mini-batch training." +) +@click.option( + "--weight_decay", + type=float, + default=1e-6, + help="Weight decay (L2 penalty) hyperparameter for Deep SAD objective.", +) +@click.option( + "--pretrain", + type=bool, + default=True, + help="Pretrain neural network parameters via autoencoder.", +) +@click.option( + "--ae_optimizer_name", + type=click.Choice(["adam"]), + default="adam", + help="Name of the optimizer to use for autoencoder pretraining.", +) +@click.option( + "--ae_lr", + type=float, + default=0.001, + help="Initial learning rate for autoencoder pretraining. Default=0.001", +) +@click.option( + "--ae_n_epochs", + type=int, + default=100, + help="Number of epochs to train autoencoder.", +) +@click.option( + "--ae_lr_milestone", + type=int, + default=0, + multiple=True, + help="Lr scheduler milestones at which lr is multiplied by 0.1. Can be multiple and must be increasing.", +) +@click.option( + "--ae_batch_size", + type=int, + default=128, + help="Batch size for mini-batch autoencoder training.", +) +@click.option( + "--ae_weight_decay", + type=float, + default=1e-6, + help="Weight decay (L2 penalty) hyperparameter for autoencoder objective.", +) +@click.option( + "--num_threads", + type=int, + default=0, + help="Number of threads used for parallelizing CPU operations. 0 means that all resources are used.", +) +@click.option( + "--n_jobs_dataloader", + type=int, + default=0, + help="Number of workers for data loading. 0 means that the data will be loaded in the main process.", +) +@click.option( + "--normal_class", + type=int, + default=0, + help="Specify the normal class of the dataset (all other classes are considered anomalous).", +) +@click.option( + "--known_outlier_class", + type=int, + default=1, + help="Specify the known outlier class of the dataset for semi-supervised anomaly detection.", +) +@click.option( + "--n_known_outlier_classes", + type=int, + default=0, + help="Number of known outlier classes." + "If 0, no anomalies are known." + "If 1, outlier class as specified in --known_outlier_class option." + "If > 1, the specified number of outlier classes will be sampled at random.", +) +def main( + dataset_name, + net_name, + xp_path, + data_path, + load_config, + load_model, + eta, + ratio_known_normal, + ratio_known_outlier, + ratio_pollution, + device, + seed, + optimizer_name, + lr, + n_epochs, + lr_milestone, + batch_size, + weight_decay, + pretrain, + ae_optimizer_name, + ae_lr, + ae_n_epochs, + ae_lr_milestone, + ae_batch_size, + ae_weight_decay, + num_threads, + n_jobs_dataloader, + normal_class, + known_outlier_class, + n_known_outlier_classes, +): """ Deep SAD, a method for deep semi-supervised anomaly detection. @@ -90,150 +246,192 @@ def main(dataset_name, net_name, xp_path, data_path, load_config, load_model, et logging.basicConfig(level=logging.INFO) logger = logging.getLogger() logger.setLevel(logging.INFO) - formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') - log_file = xp_path + '/log.txt' + formatter = logging.Formatter( + "%(asctime)s - %(name)s - %(levelname)s - %(message)s" + ) + log_file = xp_path + "/log.txt" file_handler = logging.FileHandler(log_file) file_handler.setLevel(logging.INFO) file_handler.setFormatter(formatter) logger.addHandler(file_handler) # Print paths - logger.info('Log file is %s' % log_file) - logger.info('Data path is %s' % data_path) - logger.info('Export path is %s' % xp_path) + logger.info("Log file is %s" % log_file) + logger.info("Data path is %s" % data_path) + logger.info("Export path is %s" % xp_path) # Print experimental setup - logger.info('Dataset: %s' % dataset_name) - logger.info('Normal class: %d' % normal_class) - logger.info('Ratio of labeled normal train samples: %.2f' % ratio_known_normal) - logger.info('Ratio of labeled anomalous samples: %.2f' % ratio_known_outlier) - logger.info('Pollution ratio of unlabeled train data: %.2f' % ratio_pollution) + logger.info("Dataset: %s" % dataset_name) + logger.info("Normal class: %d" % normal_class) + logger.info("Ratio of labeled normal train samples: %.2f" % ratio_known_normal) + logger.info("Ratio of labeled anomalous samples: %.2f" % ratio_known_outlier) + logger.info("Pollution ratio of unlabeled train data: %.2f" % ratio_pollution) if n_known_outlier_classes == 1: - logger.info('Known anomaly class: %d' % known_outlier_class) + logger.info("Known anomaly class: %d" % known_outlier_class) else: - logger.info('Number of known anomaly classes: %d' % n_known_outlier_classes) - logger.info('Network: %s' % net_name) + logger.info("Number of known anomaly classes: %d" % n_known_outlier_classes) + logger.info("Network: %s" % net_name) # If specified, load experiment config from JSON-file if load_config: cfg.load_config(import_json=load_config) - logger.info('Loaded configuration from %s.' % load_config) + logger.info("Loaded configuration from %s." % load_config) # Print model configuration - logger.info('Eta-parameter: %.2f' % cfg.settings['eta']) + logger.info("Eta-parameter: %.2f" % cfg.settings["eta"]) # Set seed - if cfg.settings['seed'] != -1: - random.seed(cfg.settings['seed']) - np.random.seed(cfg.settings['seed']) - torch.manual_seed(cfg.settings['seed']) - torch.cuda.manual_seed(cfg.settings['seed']) + if cfg.settings["seed"] != -1: + random.seed(cfg.settings["seed"]) + np.random.seed(cfg.settings["seed"]) + torch.manual_seed(cfg.settings["seed"]) + torch.cuda.manual_seed(cfg.settings["seed"]) torch.backends.cudnn.deterministic = True - logger.info('Set seed to %d.' % cfg.settings['seed']) + logger.info("Set seed to %d." % cfg.settings["seed"]) # Default device to 'cpu' if cuda is not available if not torch.cuda.is_available(): - device = 'cpu' + device = "cpu" # Set the number of threads used for parallelizing CPU operations if num_threads > 0: torch.set_num_threads(num_threads) - logger.info('Computation device: %s' % device) - logger.info('Number of threads: %d' % num_threads) - logger.info('Number of dataloader workers: %d' % n_jobs_dataloader) + logger.info("Computation device: %s" % device) + logger.info("Number of threads: %d" % num_threads) + logger.info("Number of dataloader workers: %d" % n_jobs_dataloader) # Load data - dataset = load_dataset(dataset_name, data_path, normal_class, known_outlier_class, n_known_outlier_classes, - ratio_known_normal, ratio_known_outlier, ratio_pollution, - random_state=np.random.RandomState(cfg.settings['seed'])) + dataset = load_dataset( + dataset_name, + data_path, + normal_class, + known_outlier_class, + n_known_outlier_classes, + ratio_known_normal, + ratio_known_outlier, + ratio_pollution, + random_state=np.random.RandomState(cfg.settings["seed"]), + ) # Log random sample of known anomaly classes if more than 1 class if n_known_outlier_classes > 1: - logger.info('Known anomaly classes: %s' % (dataset.known_outlier_classes,)) + logger.info("Known anomaly classes: %s" % (dataset.known_outlier_classes,)) # Initialize DeepSAD model and set neural network phi - deepSAD = DeepSAD(cfg.settings['eta']) + deepSAD = DeepSAD(cfg.settings["eta"]) deepSAD.set_network(net_name) # If specified, load Deep SAD model (center c, network weights, and possibly autoencoder weights) if load_model: deepSAD.load_model(model_path=load_model, load_ae=True, map_location=device) - logger.info('Loading model from %s.' % load_model) + logger.info("Loading model from %s." % load_model) - logger.info('Pretraining: %s' % pretrain) + logger.info("Pretraining: %s" % pretrain) if pretrain: # Log pretraining details - logger.info('Pretraining optimizer: %s' % cfg.settings['ae_optimizer_name']) - logger.info('Pretraining learning rate: %g' % cfg.settings['ae_lr']) - logger.info('Pretraining epochs: %d' % cfg.settings['ae_n_epochs']) - logger.info('Pretraining learning rate scheduler milestones: %s' % (cfg.settings['ae_lr_milestone'],)) - logger.info('Pretraining batch size: %d' % cfg.settings['ae_batch_size']) - logger.info('Pretraining weight decay: %g' % cfg.settings['ae_weight_decay']) + logger.info("Pretraining optimizer: %s" % cfg.settings["ae_optimizer_name"]) + logger.info("Pretraining learning rate: %g" % cfg.settings["ae_lr"]) + logger.info("Pretraining epochs: %d" % cfg.settings["ae_n_epochs"]) + logger.info( + "Pretraining learning rate scheduler milestones: %s" + % (cfg.settings["ae_lr_milestone"],) + ) + logger.info("Pretraining batch size: %d" % cfg.settings["ae_batch_size"]) + logger.info("Pretraining weight decay: %g" % cfg.settings["ae_weight_decay"]) # Pretrain model on dataset (via autoencoder) - deepSAD.pretrain(dataset, - optimizer_name=cfg.settings['ae_optimizer_name'], - lr=cfg.settings['ae_lr'], - n_epochs=cfg.settings['ae_n_epochs'], - lr_milestones=cfg.settings['ae_lr_milestone'], - batch_size=cfg.settings['ae_batch_size'], - weight_decay=cfg.settings['ae_weight_decay'], - device=device, - n_jobs_dataloader=n_jobs_dataloader) + deepSAD.pretrain( + dataset, + optimizer_name=cfg.settings["ae_optimizer_name"], + lr=cfg.settings["ae_lr"], + n_epochs=cfg.settings["ae_n_epochs"], + lr_milestones=cfg.settings["ae_lr_milestone"], + batch_size=cfg.settings["ae_batch_size"], + weight_decay=cfg.settings["ae_weight_decay"], + device=device, + n_jobs_dataloader=n_jobs_dataloader, + ) # Save pretraining results - deepSAD.save_ae_results(export_json=xp_path + '/ae_results.json') + deepSAD.save_ae_results(export_json=xp_path + "/ae_results.json") # Log training details - logger.info('Training optimizer: %s' % cfg.settings['optimizer_name']) - logger.info('Training learning rate: %g' % cfg.settings['lr']) - logger.info('Training epochs: %d' % cfg.settings['n_epochs']) - logger.info('Training learning rate scheduler milestones: %s' % (cfg.settings['lr_milestone'],)) - logger.info('Training batch size: %d' % cfg.settings['batch_size']) - logger.info('Training weight decay: %g' % cfg.settings['weight_decay']) + logger.info("Training optimizer: %s" % cfg.settings["optimizer_name"]) + logger.info("Training learning rate: %g" % cfg.settings["lr"]) + logger.info("Training epochs: %d" % cfg.settings["n_epochs"]) + logger.info( + "Training learning rate scheduler milestones: %s" + % (cfg.settings["lr_milestone"],) + ) + logger.info("Training batch size: %d" % cfg.settings["batch_size"]) + logger.info("Training weight decay: %g" % cfg.settings["weight_decay"]) # Train model on dataset - deepSAD.train(dataset, - optimizer_name=cfg.settings['optimizer_name'], - lr=cfg.settings['lr'], - n_epochs=cfg.settings['n_epochs'], - lr_milestones=cfg.settings['lr_milestone'], - batch_size=cfg.settings['batch_size'], - weight_decay=cfg.settings['weight_decay'], - device=device, - n_jobs_dataloader=n_jobs_dataloader) + deepSAD.train( + dataset, + optimizer_name=cfg.settings["optimizer_name"], + lr=cfg.settings["lr"], + n_epochs=cfg.settings["n_epochs"], + lr_milestones=cfg.settings["lr_milestone"], + batch_size=cfg.settings["batch_size"], + weight_decay=cfg.settings["weight_decay"], + device=device, + n_jobs_dataloader=n_jobs_dataloader, + ) # Test model deepSAD.test(dataset, device=device, n_jobs_dataloader=n_jobs_dataloader) # Save results, model, and configuration - deepSAD.save_results(export_json=xp_path + '/results.json') - deepSAD.save_model(export_model=xp_path + '/model.tar') - cfg.save_config(export_json=xp_path + '/config.json') + deepSAD.save_results(export_json=xp_path + "/results.json") + deepSAD.save_model(export_model=xp_path + "/model.tar") + cfg.save_config(export_json=xp_path + "/config.json") # Plot most anomalous and most normal test samples - indices, labels, scores = zip(*deepSAD.results['test_scores']) + indices, labels, scores = zip(*deepSAD.results["test_scores"]) indices, labels, scores = np.array(indices), np.array(labels), np.array(scores) idx_all_sorted = indices[np.argsort(scores)] # from lowest to highest score - idx_normal_sorted = indices[labels == 0][np.argsort(scores[labels == 0])] # from lowest to highest score + idx_normal_sorted = indices[labels == 0][ + np.argsort(scores[labels == 0]) + ] # from lowest to highest score - if dataset_name in ('mnist', 'fmnist', 'cifar10'): + if dataset_name in ("mnist", "fmnist", "cifar10"): - if dataset_name in ('mnist', 'fmnist'): + if dataset_name in ("mnist", "fmnist"): X_all_low = dataset.test_set.data[idx_all_sorted[:32], ...].unsqueeze(1) X_all_high = dataset.test_set.data[idx_all_sorted[-32:], ...].unsqueeze(1) - X_normal_low = dataset.test_set.data[idx_normal_sorted[:32], ...].unsqueeze(1) - X_normal_high = dataset.test_set.data[idx_normal_sorted[-32:], ...].unsqueeze(1) + X_normal_low = dataset.test_set.data[idx_normal_sorted[:32], ...].unsqueeze( + 1 + ) + X_normal_high = dataset.test_set.data[ + idx_normal_sorted[-32:], ... + ].unsqueeze(1) - if dataset_name == 'cifar10': - X_all_low = torch.tensor(np.transpose(dataset.test_set.data[idx_all_sorted[:32], ...], (0,3,1,2))) - X_all_high = torch.tensor(np.transpose(dataset.test_set.data[idx_all_sorted[-32:], ...], (0,3,1,2))) - X_normal_low = torch.tensor(np.transpose(dataset.test_set.data[idx_normal_sorted[:32], ...], (0,3,1,2))) - X_normal_high = torch.tensor(np.transpose(dataset.test_set.data[idx_normal_sorted[-32:], ...], (0,3,1,2))) + if dataset_name == "cifar10": + X_all_low = torch.tensor( + np.transpose( + dataset.test_set.data[idx_all_sorted[:32], ...], (0, 3, 1, 2) + ) + ) + X_all_high = torch.tensor( + np.transpose( + dataset.test_set.data[idx_all_sorted[-32:], ...], (0, 3, 1, 2) + ) + ) + X_normal_low = torch.tensor( + np.transpose( + dataset.test_set.data[idx_normal_sorted[:32], ...], (0, 3, 1, 2) + ) + ) + X_normal_high = torch.tensor( + np.transpose( + dataset.test_set.data[idx_normal_sorted[-32:], ...], (0, 3, 1, 2) + ) + ) - plot_images_grid(X_all_low, export_img=xp_path + '/all_low', padding=2) - plot_images_grid(X_all_high, export_img=xp_path + '/all_high', padding=2) - plot_images_grid(X_normal_low, export_img=xp_path + '/normals_low', padding=2) - plot_images_grid(X_normal_high, export_img=xp_path + '/normals_high', padding=2) + plot_images_grid(X_all_low, export_img=xp_path + "/all_low", padding=2) + plot_images_grid(X_all_high, export_img=xp_path + "/all_high", padding=2) + plot_images_grid(X_normal_low, export_img=xp_path + "/normals_low", padding=2) + plot_images_grid(X_normal_high, export_img=xp_path + "/normals_high", padding=2) -if __name__ == '__main__': +if __name__ == "__main__": main() diff --git a/Deep-SAD-PyTorch/src/networks/__init__.py b/Deep-SAD-PyTorch/src/networks/__init__.py index 7c9d706..94b7774 100644 --- a/Deep-SAD-PyTorch/src/networks/__init__.py +++ b/Deep-SAD-PyTorch/src/networks/__init__.py @@ -1,10 +1,22 @@ from .main import build_network, build_autoencoder from .mnist_LeNet import MNIST_LeNet, MNIST_LeNet_Decoder, MNIST_LeNet_Autoencoder -from .fmnist_LeNet import FashionMNIST_LeNet, FashionMNIST_LeNet_Decoder, FashionMNIST_LeNet_Autoencoder -from .cifar10_LeNet import CIFAR10_LeNet, CIFAR10_LeNet_Decoder, CIFAR10_LeNet_Autoencoder +from .fmnist_LeNet import ( + FashionMNIST_LeNet, + FashionMNIST_LeNet_Decoder, + FashionMNIST_LeNet_Autoencoder, +) +from .cifar10_LeNet import ( + CIFAR10_LeNet, + CIFAR10_LeNet_Decoder, + CIFAR10_LeNet_Autoencoder, +) from .mlp import MLP, MLP_Decoder, MLP_Autoencoder from .layers.stochastic import GaussianSample from .layers.standard import Standardize -from .inference.distributions import log_standard_gaussian, log_gaussian, log_standard_categorical +from .inference.distributions import ( + log_standard_gaussian, + log_gaussian, + log_standard_categorical, +) from .vae import VariationalAutoencoder, Encoder, Decoder from .dgm import DeepGenerativeModel, StackedDeepGenerativeModel diff --git a/Deep-SAD-PyTorch/src/networks/cifar10_LeNet.py b/Deep-SAD-PyTorch/src/networks/cifar10_LeNet.py index 0bb6ac7..4174659 100644 --- a/Deep-SAD-PyTorch/src/networks/cifar10_LeNet.py +++ b/Deep-SAD-PyTorch/src/networks/cifar10_LeNet.py @@ -41,17 +41,27 @@ class CIFAR10_LeNet_Decoder(BaseNet): self.rep_dim = rep_dim - self.deconv1 = nn.ConvTranspose2d(int(self.rep_dim / (4 * 4)), 128, 5, bias=False, padding=2) - nn.init.xavier_uniform_(self.deconv1.weight, gain=nn.init.calculate_gain('leaky_relu')) + self.deconv1 = nn.ConvTranspose2d( + int(self.rep_dim / (4 * 4)), 128, 5, bias=False, padding=2 + ) + nn.init.xavier_uniform_( + self.deconv1.weight, gain=nn.init.calculate_gain("leaky_relu") + ) self.bn2d4 = nn.BatchNorm2d(128, eps=1e-04, affine=False) self.deconv2 = nn.ConvTranspose2d(128, 64, 5, bias=False, padding=2) - nn.init.xavier_uniform_(self.deconv2.weight, gain=nn.init.calculate_gain('leaky_relu')) + nn.init.xavier_uniform_( + self.deconv2.weight, gain=nn.init.calculate_gain("leaky_relu") + ) self.bn2d5 = nn.BatchNorm2d(64, eps=1e-04, affine=False) self.deconv3 = nn.ConvTranspose2d(64, 32, 5, bias=False, padding=2) - nn.init.xavier_uniform_(self.deconv3.weight, gain=nn.init.calculate_gain('leaky_relu')) + nn.init.xavier_uniform_( + self.deconv3.weight, gain=nn.init.calculate_gain("leaky_relu") + ) self.bn2d6 = nn.BatchNorm2d(32, eps=1e-04, affine=False) self.deconv4 = nn.ConvTranspose2d(32, 3, 5, bias=False, padding=2) - nn.init.xavier_uniform_(self.deconv4.weight, gain=nn.init.calculate_gain('leaky_relu')) + nn.init.xavier_uniform_( + self.deconv4.weight, gain=nn.init.calculate_gain("leaky_relu") + ) def forward(self, x): x = x.view(int(x.size(0)), int(self.rep_dim / (4 * 4)), 4, 4) diff --git a/Deep-SAD-PyTorch/src/networks/dgm.py b/Deep-SAD-PyTorch/src/networks/dgm.py index d8be582..6473754 100644 --- a/Deep-SAD-PyTorch/src/networks/dgm.py +++ b/Deep-SAD-PyTorch/src/networks/dgm.py @@ -97,7 +97,9 @@ class StackedDeepGenerativeModel(DeepGenerativeModel): :param features: a pre-trained M1 model of class 'VariationalAutoencoder' trained on the same dataset. """ [x_dim, y_dim, z_dim, h_dim] = dims - super(StackedDeepGenerativeModel, self).__init__([features.z_dim, y_dim, z_dim, h_dim]) + super(StackedDeepGenerativeModel, self).__init__( + [features.z_dim, y_dim, z_dim, h_dim] + ) # Be sure to reconstruct with the same dimensions in_features = self.decoder.reconstruction.in_features diff --git a/Deep-SAD-PyTorch/src/networks/inference/distributions.py b/Deep-SAD-PyTorch/src/networks/inference/distributions.py index f683048..8bfbba8 100644 --- a/Deep-SAD-PyTorch/src/networks/inference/distributions.py +++ b/Deep-SAD-PyTorch/src/networks/inference/distributions.py @@ -11,7 +11,7 @@ def log_standard_gaussian(x): :param x: point to evaluate :return: log N(x|0,I) """ - return torch.sum(-0.5 * math.log(2 * math.pi) - x ** 2 / 2, dim=-1) + return torch.sum(-0.5 * math.log(2 * math.pi) - x**2 / 2, dim=-1) def log_gaussian(x, mu, log_var): @@ -23,7 +23,11 @@ def log_gaussian(x, mu, log_var): :param log_var: log variance :return: log N(x|µ,σI) """ - log_pdf = -0.5 * math.log(2 * math.pi) - log_var / 2 - (x - mu)**2 / (2 * torch.exp(log_var)) + log_pdf = ( + -0.5 * math.log(2 * math.pi) + - log_var / 2 + - (x - mu) ** 2 / (2 * torch.exp(log_var)) + ) return torch.sum(log_pdf, dim=-1) diff --git a/Deep-SAD-PyTorch/src/networks/layers/standard.py b/Deep-SAD-PyTorch/src/networks/layers/standard.py index f7c6b3d..62ffa2e 100644 --- a/Deep-SAD-PyTorch/src/networks/layers/standard.py +++ b/Deep-SAD-PyTorch/src/networks/layers/standard.py @@ -21,7 +21,8 @@ class Standardize(Module): mu: the learnable translation parameter μ. std: the learnable scale parameter σ. """ - __constants__ = ['mu'] + + __constants__ = ["mu"] def __init__(self, in_features, bias=True, eps=1e-6): super(Standardize, self).__init__() @@ -32,7 +33,7 @@ class Standardize(Module): if bias: self.mu = Parameter(torch.Tensor(in_features)) else: - self.register_parameter('mu', None) + self.register_parameter("mu", None) self.reset_parameters() def reset_parameters(self): @@ -47,6 +48,6 @@ class Standardize(Module): return x def extra_repr(self): - return 'in_features={}, out_features={}, bias={}'.format( + return "in_features={}, out_features={}, bias={}".format( self.in_features, self.out_features, self.mu is not None ) diff --git a/Deep-SAD-PyTorch/src/networks/main.py b/Deep-SAD-PyTorch/src/networks/main.py index 5d6d4cf..1b3c833 100644 --- a/Deep-SAD-PyTorch/src/networks/main.py +++ b/Deep-SAD-PyTorch/src/networks/main.py @@ -9,78 +9,106 @@ from .dgm import DeepGenerativeModel, StackedDeepGenerativeModel def build_network(net_name, ae_net=None): """Builds the neural network.""" - implemented_networks = ('mnist_LeNet', 'mnist_DGM_M2', 'mnist_DGM_M1M2', - 'fmnist_LeNet', 'fmnist_DGM_M2', 'fmnist_DGM_M1M2', - 'cifar10_LeNet', 'cifar10_DGM_M2', 'cifar10_DGM_M1M2', - 'arrhythmia_mlp', 'cardio_mlp', 'satellite_mlp', 'satimage-2_mlp', 'shuttle_mlp', - 'thyroid_mlp', - 'arrhythmia_DGM_M2', 'cardio_DGM_M2', 'satellite_DGM_M2', 'satimage-2_DGM_M2', - 'shuttle_DGM_M2', 'thyroid_DGM_M2') + implemented_networks = ( + "mnist_LeNet", + "mnist_DGM_M2", + "mnist_DGM_M1M2", + "fmnist_LeNet", + "fmnist_DGM_M2", + "fmnist_DGM_M1M2", + "cifar10_LeNet", + "cifar10_DGM_M2", + "cifar10_DGM_M1M2", + "arrhythmia_mlp", + "cardio_mlp", + "satellite_mlp", + "satimage-2_mlp", + "shuttle_mlp", + "thyroid_mlp", + "arrhythmia_DGM_M2", + "cardio_DGM_M2", + "satellite_DGM_M2", + "satimage-2_DGM_M2", + "shuttle_DGM_M2", + "thyroid_DGM_M2", + ) assert net_name in implemented_networks net = None - if net_name == 'mnist_LeNet': + if net_name == "mnist_LeNet": net = MNIST_LeNet() - if net_name == 'mnist_DGM_M2': - net = DeepGenerativeModel([1*28*28, 2, 32, [128, 64]], classifier_net=MNIST_LeNet) + if net_name == "mnist_DGM_M2": + net = DeepGenerativeModel( + [1 * 28 * 28, 2, 32, [128, 64]], classifier_net=MNIST_LeNet + ) - if net_name == 'mnist_DGM_M1M2': - net = StackedDeepGenerativeModel([1*28*28, 2, 32, [128, 64]], features=ae_net) + if net_name == "mnist_DGM_M1M2": + net = StackedDeepGenerativeModel( + [1 * 28 * 28, 2, 32, [128, 64]], features=ae_net + ) - if net_name == 'fmnist_LeNet': + if net_name == "fmnist_LeNet": net = FashionMNIST_LeNet() - if net_name == 'fmnist_DGM_M2': - net = DeepGenerativeModel([1*28*28, 2, 64, [256, 128]], classifier_net=FashionMNIST_LeNet) + if net_name == "fmnist_DGM_M2": + net = DeepGenerativeModel( + [1 * 28 * 28, 2, 64, [256, 128]], classifier_net=FashionMNIST_LeNet + ) - if net_name == 'fmnist_DGM_M1M2': - net = StackedDeepGenerativeModel([1*28*28, 2, 64, [256, 128]], features=ae_net) + if net_name == "fmnist_DGM_M1M2": + net = StackedDeepGenerativeModel( + [1 * 28 * 28, 2, 64, [256, 128]], features=ae_net + ) - if net_name == 'cifar10_LeNet': + if net_name == "cifar10_LeNet": net = CIFAR10_LeNet() - if net_name == 'cifar10_DGM_M2': - net = DeepGenerativeModel([3*32*32, 2, 128, [512, 256]], classifier_net=CIFAR10_LeNet) + if net_name == "cifar10_DGM_M2": + net = DeepGenerativeModel( + [3 * 32 * 32, 2, 128, [512, 256]], classifier_net=CIFAR10_LeNet + ) - if net_name == 'cifar10_DGM_M1M2': - net = StackedDeepGenerativeModel([3*32*32, 2, 128, [512, 256]], features=ae_net) + if net_name == "cifar10_DGM_M1M2": + net = StackedDeepGenerativeModel( + [3 * 32 * 32, 2, 128, [512, 256]], features=ae_net + ) - if net_name == 'arrhythmia_mlp': + if net_name == "arrhythmia_mlp": net = MLP(x_dim=274, h_dims=[128, 64], rep_dim=32, bias=False) - if net_name == 'cardio_mlp': + if net_name == "cardio_mlp": net = MLP(x_dim=21, h_dims=[32, 16], rep_dim=8, bias=False) - if net_name == 'satellite_mlp': + if net_name == "satellite_mlp": net = MLP(x_dim=36, h_dims=[32, 16], rep_dim=8, bias=False) - if net_name == 'satimage-2_mlp': + if net_name == "satimage-2_mlp": net = MLP(x_dim=36, h_dims=[32, 16], rep_dim=8, bias=False) - if net_name == 'shuttle_mlp': + if net_name == "shuttle_mlp": net = MLP(x_dim=9, h_dims=[32, 16], rep_dim=8, bias=False) - if net_name == 'thyroid_mlp': + if net_name == "thyroid_mlp": net = MLP(x_dim=6, h_dims=[32, 16], rep_dim=4, bias=False) - if net_name == 'arrhythmia_DGM_M2': + if net_name == "arrhythmia_DGM_M2": net = DeepGenerativeModel([274, 2, 32, [128, 64]]) - if net_name == 'cardio_DGM_M2': + if net_name == "cardio_DGM_M2": net = DeepGenerativeModel([21, 2, 8, [32, 16]]) - if net_name == 'satellite_DGM_M2': + if net_name == "satellite_DGM_M2": net = DeepGenerativeModel([36, 2, 8, [32, 16]]) - if net_name == 'satimage-2_DGM_M2': + if net_name == "satimage-2_DGM_M2": net = DeepGenerativeModel([36, 2, 8, [32, 16]]) - if net_name == 'shuttle_DGM_M2': + if net_name == "shuttle_DGM_M2": net = DeepGenerativeModel([9, 2, 8, [32, 16]]) - if net_name == 'thyroid_DGM_M2': + if net_name == "thyroid_DGM_M2": net = DeepGenerativeModel([6, 2, 4, [32, 16]]) return net @@ -89,50 +117,59 @@ def build_network(net_name, ae_net=None): def build_autoencoder(net_name): """Builds the corresponding autoencoder network.""" - implemented_networks = ('mnist_LeNet', 'mnist_DGM_M1M2', - 'fmnist_LeNet', 'fmnist_DGM_M1M2', - 'cifar10_LeNet', 'cifar10_DGM_M1M2', - 'arrhythmia_mlp', 'cardio_mlp', 'satellite_mlp', 'satimage-2_mlp', 'shuttle_mlp', - 'thyroid_mlp') + implemented_networks = ( + "mnist_LeNet", + "mnist_DGM_M1M2", + "fmnist_LeNet", + "fmnist_DGM_M1M2", + "cifar10_LeNet", + "cifar10_DGM_M1M2", + "arrhythmia_mlp", + "cardio_mlp", + "satellite_mlp", + "satimage-2_mlp", + "shuttle_mlp", + "thyroid_mlp", + ) assert net_name in implemented_networks ae_net = None - if net_name == 'mnist_LeNet': + if net_name == "mnist_LeNet": ae_net = MNIST_LeNet_Autoencoder() - if net_name == 'mnist_DGM_M1M2': - ae_net = VariationalAutoencoder([1*28*28, 32, [128, 64]]) + if net_name == "mnist_DGM_M1M2": + ae_net = VariationalAutoencoder([1 * 28 * 28, 32, [128, 64]]) - if net_name == 'fmnist_LeNet': + if net_name == "fmnist_LeNet": ae_net = FashionMNIST_LeNet_Autoencoder() - if net_name == 'fmnist_DGM_M1M2': - ae_net = VariationalAutoencoder([1*28*28, 64, [256, 128]]) + if net_name == "fmnist_DGM_M1M2": + ae_net = VariationalAutoencoder([1 * 28 * 28, 64, [256, 128]]) - if net_name == 'cifar10_LeNet': + if net_name == "cifar10_LeNet": ae_net = CIFAR10_LeNet_Autoencoder() - if net_name == 'cifar10_DGM_M1M2': - ae_net = VariationalAutoencoder([3*32*32, 128, [512, 256]]) + if net_name == "cifar10_DGM_M1M2": + ae_net = VariationalAutoencoder([3 * 32 * 32, 128, [512, 256]]) - if net_name == 'arrhythmia_mlp': + if net_name == "arrhythmia_mlp": ae_net = MLP_Autoencoder(x_dim=274, h_dims=[128, 64], rep_dim=32, bias=False) - if net_name == 'cardio_mlp': + if net_name == "cardio_mlp": ae_net = MLP_Autoencoder(x_dim=21, h_dims=[32, 16], rep_dim=8, bias=False) - if net_name == 'satellite_mlp': + if net_name == "satellite_mlp": ae_net = MLP_Autoencoder(x_dim=36, h_dims=[32, 16], rep_dim=8, bias=False) - if net_name == 'satimage-2_mlp': + if net_name == "satimage-2_mlp": ae_net = MLP_Autoencoder(x_dim=36, h_dims=[32, 16], rep_dim=8, bias=False) - if net_name == 'shuttle_mlp': + if net_name == "shuttle_mlp": ae_net = MLP_Autoencoder(x_dim=9, h_dims=[32, 16], rep_dim=8, bias=False) - if net_name == 'thyroid_mlp': + if net_name == "thyroid_mlp": ae_net = MLP_Autoencoder(x_dim=6, h_dims=[32, 16], rep_dim=4, bias=False) return ae_net diff --git a/Deep-SAD-PyTorch/src/networks/mlp.py b/Deep-SAD-PyTorch/src/networks/mlp.py index 7754931..50ba768 100644 --- a/Deep-SAD-PyTorch/src/networks/mlp.py +++ b/Deep-SAD-PyTorch/src/networks/mlp.py @@ -12,7 +12,10 @@ class MLP(BaseNet): self.rep_dim = rep_dim neurons = [x_dim, *h_dims] - layers = [Linear_BN_leakyReLU(neurons[i - 1], neurons[i], bias=bias) for i in range(1, len(neurons))] + layers = [ + Linear_BN_leakyReLU(neurons[i - 1], neurons[i], bias=bias) + for i in range(1, len(neurons)) + ] self.hidden = nn.ModuleList(layers) self.code = nn.Linear(h_dims[-1], rep_dim, bias=bias) @@ -32,7 +35,10 @@ class MLP_Decoder(BaseNet): self.rep_dim = rep_dim neurons = [rep_dim, *h_dims] - layers = [Linear_BN_leakyReLU(neurons[i - 1], neurons[i], bias=bias) for i in range(1, len(neurons))] + layers = [ + Linear_BN_leakyReLU(neurons[i - 1], neurons[i], bias=bias) + for i in range(1, len(neurons)) + ] self.hidden = nn.ModuleList(layers) self.reconstruction = nn.Linear(h_dims[-1], x_dim, bias=bias) diff --git a/Deep-SAD-PyTorch/src/networks/vae.py b/Deep-SAD-PyTorch/src/networks/vae.py index 08f9104..963e6f0 100644 --- a/Deep-SAD-PyTorch/src/networks/vae.py +++ b/Deep-SAD-PyTorch/src/networks/vae.py @@ -22,7 +22,9 @@ class Encoder(nn.Module): [x_dim, h_dim, z_dim] = dims neurons = [x_dim, *h_dim] - linear_layers = [nn.Linear(neurons[i-1], neurons[i]) for i in range(1, len(neurons))] + linear_layers = [ + nn.Linear(neurons[i - 1], neurons[i]) for i in range(1, len(neurons)) + ] self.hidden = nn.ModuleList(linear_layers) self.sample = sample_layer(h_dim[-1], z_dim) @@ -48,7 +50,9 @@ class Decoder(nn.Module): [z_dim, h_dim, x_dim] = dims neurons = [z_dim, *h_dim] - linear_layers = [nn.Linear(neurons[i-1], neurons[i]) for i in range(1, len(neurons))] + linear_layers = [ + nn.Linear(neurons[i - 1], neurons[i]) for i in range(1, len(neurons)) + ] self.hidden = nn.ModuleList(linear_layers) self.reconstruction = nn.Linear(h_dim[-1], x_dim) diff --git a/Deep-SAD-PyTorch/src/optim/DeepSAD_trainer.py b/Deep-SAD-PyTorch/src/optim/DeepSAD_trainer.py index 44b1118..599fdc1 100644 --- a/Deep-SAD-PyTorch/src/optim/DeepSAD_trainer.py +++ b/Deep-SAD-PyTorch/src/optim/DeepSAD_trainer.py @@ -13,11 +13,29 @@ import numpy as np class DeepSADTrainer(BaseTrainer): - def __init__(self, c, eta: float, optimizer_name: str = 'adam', lr: float = 0.001, n_epochs: int = 150, - lr_milestones: tuple = (), batch_size: int = 128, weight_decay: float = 1e-6, device: str = 'cuda', - n_jobs_dataloader: int = 0): - super().__init__(optimizer_name, lr, n_epochs, lr_milestones, batch_size, weight_decay, device, - n_jobs_dataloader) + def __init__( + self, + c, + eta: float, + optimizer_name: str = "adam", + lr: float = 0.001, + n_epochs: int = 150, + lr_milestones: tuple = (), + batch_size: int = 128, + weight_decay: float = 1e-6, + device: str = "cuda", + n_jobs_dataloader: int = 0, + ): + super().__init__( + optimizer_name, + lr, + n_epochs, + lr_milestones, + batch_size, + weight_decay, + device, + n_jobs_dataloader, + ) # Deep SAD parameters self.c = torch.tensor(c, device=self.device) if c is not None else None @@ -36,39 +54,50 @@ class DeepSADTrainer(BaseTrainer): logger = logging.getLogger() # Get train data loader - train_loader, _ = dataset.loaders(batch_size=self.batch_size, num_workers=self.n_jobs_dataloader) + train_loader, _ = dataset.loaders( + batch_size=self.batch_size, num_workers=self.n_jobs_dataloader + ) # Set device for network net = net.to(self.device) # Set optimizer (Adam optimizer for now) - optimizer = optim.Adam(net.parameters(), lr=self.lr, weight_decay=self.weight_decay) + optimizer = optim.Adam( + net.parameters(), lr=self.lr, weight_decay=self.weight_decay + ) # Set learning rate scheduler - scheduler = optim.lr_scheduler.MultiStepLR(optimizer, milestones=self.lr_milestones, gamma=0.1) + scheduler = optim.lr_scheduler.MultiStepLR( + optimizer, milestones=self.lr_milestones, gamma=0.1 + ) # Initialize hypersphere center c (if c not loaded) if self.c is None: - logger.info('Initializing center c...') + logger.info("Initializing center c...") self.c = self.init_center_c(train_loader, net) - logger.info('Center c initialized.') + logger.info("Center c initialized.") # Training - logger.info('Starting training...') + logger.info("Starting training...") start_time = time.time() net.train() for epoch in range(self.n_epochs): scheduler.step() if epoch in self.lr_milestones: - logger.info(' LR scheduler: new learning rate is %g' % float(scheduler.get_lr()[0])) + logger.info( + " LR scheduler: new learning rate is %g" + % float(scheduler.get_lr()[0]) + ) epoch_loss = 0.0 n_batches = 0 epoch_start_time = time.time() for data in train_loader: inputs, _, semi_targets, _ = data - inputs, semi_targets = inputs.to(self.device), semi_targets.to(self.device) + inputs, semi_targets = inputs.to(self.device), semi_targets.to( + self.device + ) # Zero the network parameter gradients optimizer.zero_grad() @@ -76,7 +105,11 @@ class DeepSADTrainer(BaseTrainer): # Update network parameters via backpropagation: forward + backward + optimize outputs = net(inputs) dist = torch.sum((outputs - self.c) ** 2, dim=1) - losses = torch.where(semi_targets == 0, dist, self.eta * ((dist + self.eps) ** semi_targets.float())) + losses = torch.where( + semi_targets == 0, + dist, + self.eta * ((dist + self.eps) ** semi_targets.float()), + ) loss = torch.mean(losses) loss.backward() optimizer.step() @@ -86,12 +119,14 @@ class DeepSADTrainer(BaseTrainer): # log epoch statistics epoch_train_time = time.time() - epoch_start_time - logger.info(f'| Epoch: {epoch + 1:03}/{self.n_epochs:03} | Train Time: {epoch_train_time:.3f}s ' - f'| Train Loss: {epoch_loss / n_batches:.6f} |') + logger.info( + f"| Epoch: {epoch + 1:03}/{self.n_epochs:03} | Train Time: {epoch_train_time:.3f}s " + f"| Train Loss: {epoch_loss / n_batches:.6f} |" + ) self.train_time = time.time() - start_time - logger.info('Training Time: {:.3f}s'.format(self.train_time)) - logger.info('Finished training.') + logger.info("Training Time: {:.3f}s".format(self.train_time)) + logger.info("Finished training.") return net @@ -99,13 +134,15 @@ class DeepSADTrainer(BaseTrainer): logger = logging.getLogger() # Get test data loader - _, test_loader = dataset.loaders(batch_size=self.batch_size, num_workers=self.n_jobs_dataloader) + _, test_loader = dataset.loaders( + batch_size=self.batch_size, num_workers=self.n_jobs_dataloader + ) # Set device for network net = net.to(self.device) # Testing - logger.info('Starting testing...') + logger.info("Starting testing...") epoch_loss = 0.0 n_batches = 0 start_time = time.time() @@ -122,14 +159,22 @@ class DeepSADTrainer(BaseTrainer): outputs = net(inputs) dist = torch.sum((outputs - self.c) ** 2, dim=1) - losses = torch.where(semi_targets == 0, dist, self.eta * ((dist + self.eps) ** semi_targets.float())) + losses = torch.where( + semi_targets == 0, + dist, + self.eta * ((dist + self.eps) ** semi_targets.float()), + ) loss = torch.mean(losses) scores = dist # Save triples of (idx, label, score) in a list - idx_label_score += list(zip(idx.cpu().data.numpy().tolist(), - labels.cpu().data.numpy().tolist(), - scores.cpu().data.numpy().tolist())) + idx_label_score += list( + zip( + idx.cpu().data.numpy().tolist(), + labels.cpu().data.numpy().tolist(), + scores.cpu().data.numpy().tolist(), + ) + ) epoch_loss += loss.item() n_batches += 1 @@ -144,10 +189,10 @@ class DeepSADTrainer(BaseTrainer): self.test_auc = roc_auc_score(labels, scores) # Log results - logger.info('Test Loss: {:.6f}'.format(epoch_loss / n_batches)) - logger.info('Test AUC: {:.2f}%'.format(100. * self.test_auc)) - logger.info('Test Time: {:.3f}s'.format(self.test_time)) - logger.info('Finished testing.') + logger.info("Test Loss: {:.6f}".format(epoch_loss / n_batches)) + logger.info("Test AUC: {:.2f}%".format(100.0 * self.test_auc)) + logger.info("Test Time: {:.3f}s".format(self.test_time)) + logger.info("Finished testing.") def init_center_c(self, train_loader: DataLoader, net: BaseNet, eps=0.1): """Initialize hypersphere center c as the mean from an initial forward pass on the data.""" diff --git a/Deep-SAD-PyTorch/src/optim/SemiDGM_trainer.py b/Deep-SAD-PyTorch/src/optim/SemiDGM_trainer.py index b0269d0..b6b5ec3 100644 --- a/Deep-SAD-PyTorch/src/optim/SemiDGM_trainer.py +++ b/Deep-SAD-PyTorch/src/optim/SemiDGM_trainer.py @@ -14,11 +14,28 @@ import numpy as np class SemiDeepGenerativeTrainer(BaseTrainer): - def __init__(self, alpha: float = 0.1, optimizer_name: str = 'adam', lr: float = 0.001, n_epochs: int = 150, - lr_milestones: tuple = (), batch_size: int = 128, weight_decay: float = 1e-6, device: str = 'cuda', - n_jobs_dataloader: int = 0): - super().__init__(optimizer_name, lr, n_epochs, lr_milestones, batch_size, weight_decay, device, - n_jobs_dataloader) + def __init__( + self, + alpha: float = 0.1, + optimizer_name: str = "adam", + lr: float = 0.001, + n_epochs: int = 150, + lr_milestones: tuple = (), + batch_size: int = 128, + weight_decay: float = 1e-6, + device: str = "cuda", + n_jobs_dataloader: int = 0, + ): + super().__init__( + optimizer_name, + lr, + n_epochs, + lr_milestones, + batch_size, + weight_decay, + device, + n_jobs_dataloader, + ) self.alpha = alpha @@ -32,7 +49,9 @@ class SemiDeepGenerativeTrainer(BaseTrainer): logger = logging.getLogger() # Get train data loader - train_loader, _ = dataset.loaders(batch_size=self.batch_size, num_workers=self.n_jobs_dataloader) + train_loader, _ = dataset.loaders( + batch_size=self.batch_size, num_workers=self.n_jobs_dataloader + ) # Set device net = net.to(self.device) @@ -42,20 +61,27 @@ class SemiDeepGenerativeTrainer(BaseTrainer): elbo = SVI(net, likelihood=binary_cross_entropy, sampler=sampler) # Set optimizer (Adam optimizer for now) - optimizer = optim.Adam(net.parameters(), lr=self.lr, weight_decay=self.weight_decay) + optimizer = optim.Adam( + net.parameters(), lr=self.lr, weight_decay=self.weight_decay + ) # Set learning rate scheduler - scheduler = optim.lr_scheduler.MultiStepLR(optimizer, milestones=self.lr_milestones, gamma=0.1) + scheduler = optim.lr_scheduler.MultiStepLR( + optimizer, milestones=self.lr_milestones, gamma=0.1 + ) # Training - logger.info('Starting training...') + logger.info("Starting training...") start_time = time.time() net.train() for epoch in range(self.n_epochs): scheduler.step() if epoch in self.lr_milestones: - logger.info(' LR scheduler: new learning rate is %g' % float(scheduler.get_lr()[0])) + logger.info( + " LR scheduler: new learning rate is %g" + % float(scheduler.get_lr()[0]) + ) epoch_loss = 0.0 n_batches = 0 @@ -73,7 +99,9 @@ class SemiDeepGenerativeTrainer(BaseTrainer): u = inputs[semi_targets == 0] y = labels[semi_targets != 0] if y.nelement() > 1: - y_onehot = torch.Tensor(y.size(0), 2).to(self.device) # two labels: 0: normal, 1: outlier + y_onehot = torch.Tensor(y.size(0), 2).to( + self.device + ) # two labels: 0: normal, 1: outlier y_onehot.zero_() y_onehot.scatter_(1, y.view(-1, 1), 1) @@ -94,7 +122,9 @@ class SemiDeepGenerativeTrainer(BaseTrainer): # Add auxiliary classification loss q(y|x) logits = net.classify(x) eps = 1e-8 - classication_loss = torch.sum(y_onehot * torch.log(logits + eps), dim=1).mean() + classication_loss = torch.sum( + y_onehot * torch.log(logits + eps), dim=1 + ).mean() # Overall loss loss = L - self.alpha * classication_loss + U # J_alpha @@ -107,12 +137,14 @@ class SemiDeepGenerativeTrainer(BaseTrainer): # log epoch statistics epoch_train_time = time.time() - epoch_start_time - logger.info(f'| Epoch: {epoch + 1:03}/{self.n_epochs:03} | Train Time: {epoch_train_time:.3f}s ' - f'| Train Loss: {epoch_loss / n_batches:.6f} |') + logger.info( + f"| Epoch: {epoch + 1:03}/{self.n_epochs:03} | Train Time: {epoch_train_time:.3f}s " + f"| Train Loss: {epoch_loss / n_batches:.6f} |" + ) self.train_time = time.time() - start_time - logger.info('Training Time: {:.3f}s'.format(self.train_time)) - logger.info('Finished training.') + logger.info("Training Time: {:.3f}s".format(self.train_time)) + logger.info("Finished training.") return net @@ -120,7 +152,9 @@ class SemiDeepGenerativeTrainer(BaseTrainer): logger = logging.getLogger() # Get test data loader - _, test_loader = dataset.loaders(batch_size=self.batch_size, num_workers=self.n_jobs_dataloader) + _, test_loader = dataset.loaders( + batch_size=self.batch_size, num_workers=self.n_jobs_dataloader + ) # Set device net = net.to(self.device) @@ -130,7 +164,7 @@ class SemiDeepGenerativeTrainer(BaseTrainer): elbo = SVI(net, likelihood=binary_cross_entropy, sampler=sampler) # Testing - logger.info('Starting testing...') + logger.info("Starting testing...") epoch_loss = 0.0 n_batches = 0 start_time = time.time() @@ -147,7 +181,9 @@ class SemiDeepGenerativeTrainer(BaseTrainer): inputs = inputs.view(inputs.size(0), -1) u = inputs y = labels - y_onehot = torch.Tensor(y.size(0), 2).to(self.device) # two labels: 0: normal, 1: outlier + y_onehot = torch.Tensor(y.size(0), 2).to( + self.device + ) # two labels: 0: normal, 1: outlier y_onehot.zero_() y_onehot.scatter_(1, y.view(-1, 1), 1) @@ -157,17 +193,25 @@ class SemiDeepGenerativeTrainer(BaseTrainer): logits = net.classify(u) eps = 1e-8 - classication_loss = -torch.sum(y_onehot * torch.log(logits + eps), dim=1).mean() + classication_loss = -torch.sum( + y_onehot * torch.log(logits + eps), dim=1 + ).mean() loss = L + self.alpha * classication_loss + U # J_alpha # Compute scores - scores = logits[:, 1] # likelihood/confidence for anomalous class as anomaly score + scores = logits[ + :, 1 + ] # likelihood/confidence for anomalous class as anomaly score # Save triple of (idx, label, score) in a list - idx_label_score += list(zip(idx.cpu().data.numpy().tolist(), - labels.cpu().data.numpy().tolist(), - scores.cpu().data.numpy().tolist())) + idx_label_score += list( + zip( + idx.cpu().data.numpy().tolist(), + labels.cpu().data.numpy().tolist(), + scores.cpu().data.numpy().tolist(), + ) + ) epoch_loss += loss.item() n_batches += 1 @@ -182,7 +226,7 @@ class SemiDeepGenerativeTrainer(BaseTrainer): self.test_auc = roc_auc_score(labels, scores) # Log results - logger.info('Test Loss: {:.6f}'.format(epoch_loss / n_batches)) - logger.info('Test AUC: {:.2f}%'.format(100. * self.test_auc)) - logger.info('Test Time: {:.3f}s'.format(self.test_time)) - logger.info('Finished testing.') + logger.info("Test Loss: {:.6f}".format(epoch_loss / n_batches)) + logger.info("Test AUC: {:.2f}%".format(100.0 * self.test_auc)) + logger.info("Test Time: {:.3f}s".format(self.test_time)) + logger.info("Finished testing.") diff --git a/Deep-SAD-PyTorch/src/optim/ae_trainer.py b/Deep-SAD-PyTorch/src/optim/ae_trainer.py index c148b16..e612e25 100644 --- a/Deep-SAD-PyTorch/src/optim/ae_trainer.py +++ b/Deep-SAD-PyTorch/src/optim/ae_trainer.py @@ -13,10 +13,27 @@ import numpy as np class AETrainer(BaseTrainer): - def __init__(self, optimizer_name: str = 'adam', lr: float = 0.001, n_epochs: int = 150, lr_milestones: tuple = (), - batch_size: int = 128, weight_decay: float = 1e-6, device: str = 'cuda', n_jobs_dataloader: int = 0): - super().__init__(optimizer_name, lr, n_epochs, lr_milestones, batch_size, weight_decay, device, - n_jobs_dataloader) + def __init__( + self, + optimizer_name: str = "adam", + lr: float = 0.001, + n_epochs: int = 150, + lr_milestones: tuple = (), + batch_size: int = 128, + weight_decay: float = 1e-6, + device: str = "cuda", + n_jobs_dataloader: int = 0, + ): + super().__init__( + optimizer_name, + lr, + n_epochs, + lr_milestones, + batch_size, + weight_decay, + device, + n_jobs_dataloader, + ) # Results self.train_time = None @@ -27,30 +44,39 @@ class AETrainer(BaseTrainer): logger = logging.getLogger() # Get train data loader - train_loader, _ = dataset.loaders(batch_size=self.batch_size, num_workers=self.n_jobs_dataloader) + train_loader, _ = dataset.loaders( + batch_size=self.batch_size, num_workers=self.n_jobs_dataloader + ) # Set loss - criterion = nn.MSELoss(reduction='none') + criterion = nn.MSELoss(reduction="none") # Set device ae_net = ae_net.to(self.device) criterion = criterion.to(self.device) # Set optimizer (Adam optimizer for now) - optimizer = optim.Adam(ae_net.parameters(), lr=self.lr, weight_decay=self.weight_decay) + optimizer = optim.Adam( + ae_net.parameters(), lr=self.lr, weight_decay=self.weight_decay + ) # Set learning rate scheduler - scheduler = optim.lr_scheduler.MultiStepLR(optimizer, milestones=self.lr_milestones, gamma=0.1) + scheduler = optim.lr_scheduler.MultiStepLR( + optimizer, milestones=self.lr_milestones, gamma=0.1 + ) # Training - logger.info('Starting pretraining...') + logger.info("Starting pretraining...") start_time = time.time() ae_net.train() for epoch in range(self.n_epochs): scheduler.step() if epoch in self.lr_milestones: - logger.info(' LR scheduler: new learning rate is %g' % float(scheduler.get_lr()[0])) + logger.info( + " LR scheduler: new learning rate is %g" + % float(scheduler.get_lr()[0]) + ) epoch_loss = 0.0 n_batches = 0 @@ -74,12 +100,14 @@ class AETrainer(BaseTrainer): # log epoch statistics epoch_train_time = time.time() - epoch_start_time - logger.info(f'| Epoch: {epoch + 1:03}/{self.n_epochs:03} | Train Time: {epoch_train_time:.3f}s ' - f'| Train Loss: {epoch_loss / n_batches:.6f} |') + logger.info( + f"| Epoch: {epoch + 1:03}/{self.n_epochs:03} | Train Time: {epoch_train_time:.3f}s " + f"| Train Loss: {epoch_loss / n_batches:.6f} |" + ) self.train_time = time.time() - start_time - logger.info('Pretraining Time: {:.3f}s'.format(self.train_time)) - logger.info('Finished pretraining.') + logger.info("Pretraining Time: {:.3f}s".format(self.train_time)) + logger.info("Finished pretraining.") return ae_net @@ -87,17 +115,19 @@ class AETrainer(BaseTrainer): logger = logging.getLogger() # Get test data loader - _, test_loader = dataset.loaders(batch_size=self.batch_size, num_workers=self.n_jobs_dataloader) + _, test_loader = dataset.loaders( + batch_size=self.batch_size, num_workers=self.n_jobs_dataloader + ) # Set loss - criterion = nn.MSELoss(reduction='none') + criterion = nn.MSELoss(reduction="none") # Set device for network ae_net = ae_net.to(self.device) criterion = criterion.to(self.device) # Testing - logger.info('Testing autoencoder...') + logger.info("Testing autoencoder...") epoch_loss = 0.0 n_batches = 0 start_time = time.time() @@ -106,16 +136,24 @@ class AETrainer(BaseTrainer): with torch.no_grad(): for data in test_loader: inputs, labels, _, idx = data - inputs, labels, idx = inputs.to(self.device), labels.to(self.device), idx.to(self.device) + inputs, labels, idx = ( + inputs.to(self.device), + labels.to(self.device), + idx.to(self.device), + ) rec = ae_net(inputs) rec_loss = criterion(rec, inputs) scores = torch.mean(rec_loss, dim=tuple(range(1, rec.dim()))) # Save triple of (idx, label, score) in a list - idx_label_score += list(zip(idx.cpu().data.numpy().tolist(), - labels.cpu().data.numpy().tolist(), - scores.cpu().data.numpy().tolist())) + idx_label_score += list( + zip( + idx.cpu().data.numpy().tolist(), + labels.cpu().data.numpy().tolist(), + scores.cpu().data.numpy().tolist(), + ) + ) loss = torch.mean(rec_loss) epoch_loss += loss.item() @@ -130,7 +168,7 @@ class AETrainer(BaseTrainer): self.test_auc = roc_auc_score(labels, scores) # Log results - logger.info('Test Loss: {:.6f}'.format(epoch_loss / n_batches)) - logger.info('Test AUC: {:.2f}%'.format(100. * self.test_auc)) - logger.info('Test Time: {:.3f}s'.format(self.test_time)) - logger.info('Finished testing autoencoder.') + logger.info("Test Loss: {:.6f}".format(epoch_loss / n_batches)) + logger.info("Test AUC: {:.2f}%".format(100.0 * self.test_auc)) + logger.info("Test Time: {:.3f}s".format(self.test_time)) + logger.info("Finished testing autoencoder.") diff --git a/Deep-SAD-PyTorch/src/optim/vae_trainer.py b/Deep-SAD-PyTorch/src/optim/vae_trainer.py index 940214c..0c8e6e4 100644 --- a/Deep-SAD-PyTorch/src/optim/vae_trainer.py +++ b/Deep-SAD-PyTorch/src/optim/vae_trainer.py @@ -13,10 +13,27 @@ import numpy as np class VAETrainer(BaseTrainer): - def __init__(self, optimizer_name: str = 'adam', lr: float = 0.001, n_epochs: int = 150, lr_milestones: tuple = (), - batch_size: int = 128, weight_decay: float = 1e-6, device: str = 'cuda', n_jobs_dataloader: int = 0): - super().__init__(optimizer_name, lr, n_epochs, lr_milestones, batch_size, weight_decay, device, - n_jobs_dataloader) + def __init__( + self, + optimizer_name: str = "adam", + lr: float = 0.001, + n_epochs: int = 150, + lr_milestones: tuple = (), + batch_size: int = 128, + weight_decay: float = 1e-6, + device: str = "cuda", + n_jobs_dataloader: int = 0, + ): + super().__init__( + optimizer_name, + lr, + n_epochs, + lr_milestones, + batch_size, + weight_decay, + device, + n_jobs_dataloader, + ) # Results self.train_time = None @@ -27,26 +44,35 @@ class VAETrainer(BaseTrainer): logger = logging.getLogger() # Get train data loader - train_loader, _ = dataset.loaders(batch_size=self.batch_size, num_workers=self.n_jobs_dataloader) + train_loader, _ = dataset.loaders( + batch_size=self.batch_size, num_workers=self.n_jobs_dataloader + ) # Set device vae = vae.to(self.device) # Set optimizer (Adam optimizer for now) - optimizer = optim.Adam(vae.parameters(), lr=self.lr, weight_decay=self.weight_decay) + optimizer = optim.Adam( + vae.parameters(), lr=self.lr, weight_decay=self.weight_decay + ) # Set learning rate scheduler - scheduler = optim.lr_scheduler.MultiStepLR(optimizer, milestones=self.lr_milestones, gamma=0.1) + scheduler = optim.lr_scheduler.MultiStepLR( + optimizer, milestones=self.lr_milestones, gamma=0.1 + ) # Training - logger.info('Starting pretraining...') + logger.info("Starting pretraining...") start_time = time.time() vae.train() for epoch in range(self.n_epochs): scheduler.step() if epoch in self.lr_milestones: - logger.info(' LR scheduler: new learning rate is %g' % float(scheduler.get_lr()[0])) + logger.info( + " LR scheduler: new learning rate is %g" + % float(scheduler.get_lr()[0]) + ) epoch_loss = 0.0 n_batches = 0 @@ -76,12 +102,14 @@ class VAETrainer(BaseTrainer): # log epoch statistics epoch_train_time = time.time() - epoch_start_time - logger.info(f'| Epoch: {epoch + 1:03}/{self.n_epochs:03} | Train Time: {epoch_train_time:.3f}s ' - f'| Train Loss: {epoch_loss / n_batches:.6f} |') + logger.info( + f"| Epoch: {epoch + 1:03}/{self.n_epochs:03} | Train Time: {epoch_train_time:.3f}s " + f"| Train Loss: {epoch_loss / n_batches:.6f} |" + ) self.train_time = time.time() - start_time - logger.info('Pretraining Time: {:.3f}s'.format(self.train_time)) - logger.info('Finished pretraining.') + logger.info("Pretraining Time: {:.3f}s".format(self.train_time)) + logger.info("Finished pretraining.") return vae @@ -89,13 +117,15 @@ class VAETrainer(BaseTrainer): logger = logging.getLogger() # Get test data loader - _, test_loader = dataset.loaders(batch_size=self.batch_size, num_workers=self.n_jobs_dataloader) + _, test_loader = dataset.loaders( + batch_size=self.batch_size, num_workers=self.n_jobs_dataloader + ) # Set device vae = vae.to(self.device) # Testing - logger.info('Starting testing...') + logger.info("Starting testing...") epoch_loss = 0.0 n_batches = 0 start_time = time.time() @@ -104,7 +134,11 @@ class VAETrainer(BaseTrainer): with torch.no_grad(): for data in test_loader: inputs, labels, _, idx = data - inputs, labels, idx = inputs.to(self.device), labels.to(self.device), idx.to(self.device) + inputs, labels, idx = ( + inputs.to(self.device), + labels.to(self.device), + idx.to(self.device), + ) inputs = inputs.view(inputs.size(0), -1) @@ -113,9 +147,13 @@ class VAETrainer(BaseTrainer): scores = -likelihood # negative likelihood as anomaly score # Save triple of (idx, label, score) in a list - idx_label_score += list(zip(idx.cpu().data.numpy().tolist(), - labels.cpu().data.numpy().tolist(), - scores.cpu().data.numpy().tolist())) + idx_label_score += list( + zip( + idx.cpu().data.numpy().tolist(), + labels.cpu().data.numpy().tolist(), + scores.cpu().data.numpy().tolist(), + ) + ) # Overall loss elbo = likelihood - vae.kl_divergence @@ -133,7 +171,7 @@ class VAETrainer(BaseTrainer): self.test_auc = roc_auc_score(labels, scores) # Log results - logger.info('Test Loss: {:.6f}'.format(epoch_loss / n_batches)) - logger.info('Test AUC: {:.2f}%'.format(100. * self.test_auc)) - logger.info('Test Time: {:.3f}s'.format(self.test_time)) - logger.info('Finished testing variational autoencoder.') + logger.info("Test Loss: {:.6f}".format(epoch_loss / n_batches)) + logger.info("Test AUC: {:.2f}%".format(100.0 * self.test_auc)) + logger.info("Test Time: {:.3f}s".format(self.test_time)) + logger.info("Finished testing variational autoencoder.") diff --git a/Deep-SAD-PyTorch/src/optim/variational.py b/Deep-SAD-PyTorch/src/optim/variational.py index 7444fbc..229f031 100644 --- a/Deep-SAD-PyTorch/src/optim/variational.py +++ b/Deep-SAD-PyTorch/src/optim/variational.py @@ -41,7 +41,13 @@ class SVI(nn.Module): base_sampler = ImportanceWeightedSampler(mc=1, iw=1) - def __init__(self, model, likelihood=F.binary_cross_entropy, beta=repeat(1), sampler=base_sampler): + def __init__( + self, + model, + likelihood=F.binary_cross_entropy, + beta=repeat(1), + sampler=base_sampler, + ): super(SVI, self).__init__() self.model = model self.likelihood = likelihood diff --git a/Deep-SAD-PyTorch/src/utils/config.py b/Deep-SAD-PyTorch/src/utils/config.py index ee06a08..05d5994 100644 --- a/Deep-SAD-PyTorch/src/utils/config.py +++ b/Deep-SAD-PyTorch/src/utils/config.py @@ -10,7 +10,7 @@ class Config(object): def load_config(self, import_json): """Load settings dict from import_json (path/filename.json) JSON-file.""" - with open(import_json, 'r') as fp: + with open(import_json, "r") as fp: settings = json.load(fp) for key, value in settings.items(): @@ -19,5 +19,5 @@ class Config(object): def save_config(self, export_json): """Save settings dict to export_json (path/filename.json) JSON-file.""" - with open(export_json, 'w') as fp: + with open(export_json, "w") as fp: json.dump(self.settings, fp) diff --git a/Deep-SAD-PyTorch/src/utils/misc.py b/Deep-SAD-PyTorch/src/utils/misc.py index fd2c258..b1df107 100644 --- a/Deep-SAD-PyTorch/src/utils/misc.py +++ b/Deep-SAD-PyTorch/src/utils/misc.py @@ -38,7 +38,9 @@ def log_sum_exp(tensor, dim=-1, sum_op=torch.sum): :return: LSE """ max, _ = torch.max(tensor, dim=dim, keepdim=True) - return torch.log(sum_op(torch.exp(tensor - max), dim=dim, keepdim=True) + 1e-8) + max + return ( + torch.log(sum_op(torch.exp(tensor - max), dim=dim, keepdim=True) + 1e-8) + max + ) def binary_cross_entropy(x, y): diff --git a/Deep-SAD-PyTorch/src/utils/visualization/plot_images_grid.py b/Deep-SAD-PyTorch/src/utils/visualization/plot_images_grid.py index d982465..bd1f56d 100644 --- a/Deep-SAD-PyTorch/src/utils/visualization/plot_images_grid.py +++ b/Deep-SAD-PyTorch/src/utils/visualization/plot_images_grid.py @@ -1,26 +1,37 @@ import torch import matplotlib -matplotlib.use('Agg') # or 'PS', 'PDF', 'SVG' + +matplotlib.use("Agg") # or 'PS', 'PDF', 'SVG' import matplotlib.pyplot as plt import numpy as np from torchvision.utils import make_grid -def plot_images_grid(x: torch.tensor, export_img, title: str = '', nrow=8, padding=2, normalize=False, pad_value=0): +def plot_images_grid( + x: torch.tensor, + export_img, + title: str = "", + nrow=8, + padding=2, + normalize=False, + pad_value=0, +): """Plot 4D Tensor of images of shape (B x C x H x W) as a grid.""" - grid = make_grid(x, nrow=nrow, padding=padding, normalize=normalize, pad_value=pad_value) + grid = make_grid( + x, nrow=nrow, padding=padding, normalize=normalize, pad_value=pad_value + ) npgrid = grid.cpu().numpy() - plt.imshow(np.transpose(npgrid, (1, 2, 0)), interpolation='nearest') + plt.imshow(np.transpose(npgrid, (1, 2, 0)), interpolation="nearest") ax = plt.gca() ax.xaxis.set_visible(False) ax.yaxis.set_visible(False) - if not (title == ''): + if not (title == ""): plt.title(title) - plt.savefig(export_img, bbox_inches='tight', pad_inches=0.1) + plt.savefig(export_img, bbox_inches="tight", pad_inches=0.1) plt.clf()