black formatted files before changes

This commit is contained in:
Jan Kowalczyk
2024-06-28 11:36:46 +02:00
parent d33c6b1e16
commit 71f9662022
40 changed files with 2938 additions and 1260 deletions

View File

@@ -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)

View File

@@ -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

View File

@@ -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)

View File

@@ -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

View File

@@ -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!")

View File

@@ -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

View File

@@ -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()

View File

@@ -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()

View File

@@ -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()

View File

@@ -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()

View File

@@ -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()

View File

@@ -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)

View File

@@ -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)

View File

@@ -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)

View File

@@ -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)

View File

@@ -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 <w,phi(x_i)> >= rho - xi_i
y_j<w,phi(x_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 <w,phi(x_i)> >= rho - xi_i
y_j<w,phi(x_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

View File

@@ -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)

View File

@@ -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

View File

@@ -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)

View File

@@ -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

View File

@@ -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)

View File

@@ -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

View File

@@ -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

View File

@@ -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()

View File

@@ -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

View File

@@ -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)

View File

@@ -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

View File

@@ -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)

View File

@@ -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
)

View File

@@ -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

View File

@@ -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)

View File

@@ -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)

View File

@@ -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."""

View File

@@ -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.")

View File

@@ -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.")

View File

@@ -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.")

View File

@@ -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

View File

@@ -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)

View File

@@ -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):

View File

@@ -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()