├── LICENSE ├── README.md ├── data └── dataset.txt ├── dataloader.py ├── dataset ├── fine_tune_dicriminator.py ├── imgs └── OGNet_architect.png ├── model.py ├── model_fine_tune_discriminator.py ├── network.py ├── opts.py ├── opts_fine_tune_discriminator.py ├── test.py ├── train.py └── utils.py /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Zaigham Zaheer 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Old is Gold: Redefining the Adversarially Learned One-Class Classifier Training Paradigm (CVPR 2020) 2 | 3 | 4 | 5 | [CVPR Presentation](https://www.youtube.com/watch?v=mAfAUwFlUpU) || [Paper](https://arxiv.org/abs/2004.07657) || [CVPR CVF Archive](http://openaccess.thecvf.com/content_CVPR_2020/html/Zaheer_Old_Is_Gold_Redefining_the_Adversarially_Learned_One-Class_Classifier_Training_CVPR_2020_paper.html) || [Supp material zip file](http://openaccess.thecvf.com/content_CVPR_2020/supplemental/Zaheer_Old_Is_Gold_CVPR_2020_supplemental.zip) || [Ped2 Results Video](https://youtu.be/59Lqkkyy9bQ) 6 | 7 | ![img1](https://github.com/xaggi/OGNet/blob/master/imgs/OGNet_architect.png) 8 | 9 | ## Requirements 10 | 11 | - Python2.7 12 | - torch 1.2.0 13 | - torchvision 0.4.0 14 | 15 | Previously, the code was built using Python3.5, but as the version has reached its EOL, this code is verified on Python 2.7 now. 16 | 17 | ## Code execution 18 | 19 | - Train.py is the entry point to the code. 20 | - Place training and testing images under the directory 'data' by following the instructions provided in 'dataset.txt' file. 21 | - Set necessary options in opts.py for phase one and opts_fine_tune_discriminator.py for phase two. 22 | - Execute Train.py 23 | 24 | Previously, only test codes were provided for which test.py file was needed to run the evaluation. For that, the instructions can be found below. Note that, for the current version. test.py is not required as the code calls the test function every iteration from within to visualize the performance difference between the baseline and the OGNet. 25 | 26 | - Download trained generator and discriminator models from [here](https://drive.google.com/drive/folders/1onNezvWJCfaKndvzOc3CXXnNidjosVvn?usp=sharing) and place inside the directory ./models/ 27 | - Download datasets [here](https://drive.google.com/drive/folders/1Cj28-1aV4AXtdm9j_CEs3UArLodc0GY4?usp=sharing) and place test images in the subdirectories of ./data/test/ 28 | - Example: 29 | - All images from inlier class (\*.png) should be placed as ./data/test/0/*.png 30 | - Similarly, all images from outlier class (* \*.png) should be placed as ./data/test/1/* \*.png 31 | - run test.py 32 | 33 | ## MNIST training and testing details 34 | The [models provided](https://drive.google.com/drive/folders/1onNezvWJCfaKndvzOc3CXXnNidjosVvn?usp=sharing) are trained on the training set of '0' class in MNIST dataset. For evaluation, the test dataset [provided](https://drive.google.com/drive/folders/1Cj28-1aV4AXtdm9j_CEs3UArLodc0GY4?usp=sharing) contains all test images from class '0' as inliers, whereas 100 images each from all other classes as outliers. 35 | 36 | 37 | 38 | ## Updates 39 | 40 | [17.6.2020] For the time being, test code (and some trained models) are being made available. Training code will be uploaded in some time. 41 | 42 | [05.3.2021] A mockup training code is uploaded which can be used for training and evaluation of the model. 43 | 44 | 45 | 46 | For any queries, please feel free to contact Zaigham through mzz . pieas @ gmail . com 47 | 48 | If you find this code helpful, please cite our paper: 49 | 50 | ``` 51 | @inproceedings{zaheer2020old, 52 | title={Old is Gold: Redefining the Adversarially Learned One-Class Classifier Training Paradigm}, 53 | author={Zaheer, Muhammad Zaigham and Lee, Jin-ha and Astrid, Marcella and Lee, Seung-Ik}, 54 | booktitle={Proceedings of the IEEE/CVF Conference on Computer Vision and Pattern Recognition}, 55 | pages={14183--14193}, 56 | year={2020} 57 | } 58 | ``` 59 | 60 | -------------------------------------------------------------------------------- /data/dataset.txt: -------------------------------------------------------------------------------- 1 | For training images: 2 | ./data/train/'normal_class_name'/'sub_directory'/*.png 3 | 4 | 5 | For testing images: 6 | Class 0: ./data/test/0/*.png 7 | Class 1: ./data/test/1/*.png 8 | -------------------------------------------------------------------------------- /dataloader.py: -------------------------------------------------------------------------------- 1 | import torchvision.datasets as dset 2 | import torchvision.transforms as transforms 3 | import torch 4 | def load_data(opt): 5 | dataset = dset.ImageFolder(root=opt.data_path, # opt.data_path 6 | transform=transforms.Compose([ 7 | transforms.Grayscale(), 8 | transforms.Resize((45, 45)), 9 | transforms.ToTensor(), 10 | transforms.Normalize([0.5], [0.5]), 11 | ])) 12 | dataloader = torch.utils.data.DataLoader(dataset, 13 | batch_size=opt.batch_size, 14 | shuffle=True, 15 | num_workers=opt.n_threads, 16 | drop_last=opt.drop_last) 17 | return dataloader 18 | 19 | def load_data_train(opt): 20 | dataset = dset.ImageFolder(root=opt.data_path+'/'+ opt.normal_class, # opt.data_path 21 | transform=transforms.Compose([ 22 | transforms.Grayscale(), 23 | transforms.Resize((45, 45)), 24 | transforms.ToTensor(), 25 | transforms.Normalize([0.5], [0.5]), 26 | ])) 27 | dataloader = torch.utils.data.DataLoader(dataset, 28 | batch_size=opt.batch_size, 29 | shuffle=True, 30 | num_workers=opt.n_threads, 31 | drop_last=opt.drop_last) 32 | return dataloader -------------------------------------------------------------------------------- /dataset: -------------------------------------------------------------------------------- 1 | For training images: 2 | ./data/train/'normal_class_name'/'sub_directory'/*.png 3 | 4 | 5 | For testing images: 6 | Class 0: ./data/test/0/*.png 7 | Class 1: ./data/test/1/*.png 8 | -------------------------------------------------------------------------------- /fine_tune_dicriminator.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | from model_fine_tune_discriminator import Fine_Tune_Disc 4 | 5 | from opts_fine_tune_discriminator import parse_opts_ft 6 | from dataloader import load_data_train 7 | 8 | 9 | 10 | def fine_tune(): #Phase two 11 | 12 | opt = parse_opts_ft() 13 | low_epoch = opt.low_epoch 14 | high_epoch = opt.high_epoch 15 | for i in range(high_epoch, high_epoch+1, 1): 16 | 17 | dataloader = load_data_train(opt) 18 | 19 | model = Fine_Tune_Disc(opt, dataloader) 20 | model.cuda() 21 | 22 | model_folder_path = './models/' 23 | load_model_epoch = ['epoch_{0}'.format(low_epoch), 'epoch_{0}'.format(i)] 24 | model.train(load_model_epoch, model_folder_path) 25 | -------------------------------------------------------------------------------- /imgs/OGNet_architect.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xaggi/OGNet/bb9741c6ef698366544d01b849f329cc1187c917/imgs/OGNet_architect.png -------------------------------------------------------------------------------- /model.py: -------------------------------------------------------------------------------- 1 | import torch 2 | import torch.nn.functional as F 3 | from torch import nn 4 | import torch.optim as optim 5 | import numpy as np 6 | from network import d_net, g_net 7 | import torchvision.utils as vutils 8 | from opts_fine_tune_discriminator import parse_opts_ft 9 | from opts import parse_opts 10 | from fine_tune_dicriminator import fine_tune 11 | from utils import gaussian 12 | from dataloader import load_data 13 | from sklearn import metrics 14 | 15 | def check_auc(g_model_path, d_model_path, i): 16 | opt_auc = parse_opts() 17 | opt_auc.batch_shuffle = False 18 | opt_auc.drop_last = False 19 | opt_auc.data_path = './data/test/' 20 | dataloader = load_data(opt_auc) 21 | model = OGNet(opt_auc, dataloader) 22 | model.cuda() 23 | d_results, labels = model.test_patches(g_model_path, d_model_path, i) 24 | d_results = np.concatenate(d_results) 25 | labels = np.concatenate(labels) 26 | fpr1, tpr1, thresholds1 = metrics.roc_curve(labels, d_results, pos_label=1) # (y, score, positive_label) 27 | fnr1 = 1 - tpr1 28 | eer_threshold1 = thresholds1[np.nanargmin(np.absolute((fnr1 - fpr1)))] 29 | EER1 = fpr1[np.nanargmin(np.absolute((fnr1 - fpr1)))] 30 | d_f1 = np.copy(d_results) 31 | d_f1[d_f1 >= eer_threshold1] = 1 32 | d_f1[d_f1 < eer_threshold1] = 0 33 | f1_score = metrics.f1_score(labels, d_f1, pos_label=0) 34 | print("AUC: {0}, EER: {1}, EER_thr: {2}, F1_score: {3}".format(metrics.auc(fpr1,tpr1), EER1, 35 | eer_threshold1,f1_score)) 36 | 37 | class OGNet(nn.Module): 38 | @staticmethod 39 | def name(): 40 | return 'Old is Gold: Redefining the adversarially learned one-class classification paradigm' 41 | 42 | def __init__(self, opt, dataloader): 43 | super(OGNet, self).__init__() 44 | self.adversarial_training_factor = opt.adversarial_training_factor 45 | self.g_learning_rate = opt.g_learning_rate 46 | self.d_learning_rate = opt.d_learning_rate 47 | self.epoch = opt.epoch 48 | self.batch_size = opt.batch_size 49 | self.n_threads = opt.n_threads 50 | self.sigma_noise = opt.sigma_noise 51 | self.dataloader = dataloader 52 | self.g = g_net().cuda() 53 | self.d = d_net().cuda() 54 | self.image_grids_numbers = opt.image_grids_numbers 55 | self.filename = '' 56 | self.n_row_in_grid = opt.n_row_in_grid 57 | 58 | def train(self, normal_class): 59 | self.g.train() 60 | self.d.train() 61 | 62 | # Set optimizators 63 | g_optim = optim.Adam(self.g.parameters(), lr=self.g_learning_rate) 64 | d_optim = optim.Adam(self.d.parameters(), lr=self.d_learning_rate) 65 | 66 | fake = torch.ones([self.batch_size], dtype=torch.float32).cuda() 67 | valid = torch.zeros([self.batch_size], dtype=torch.float32).cuda() 68 | print('Training until high epoch...') 69 | for num_epoch in range(self.epoch): 70 | # print("Epoch {0}".format(num_epoch)) 71 | for i, data in enumerate(self.dataloader): 72 | input, gt_label = data 73 | input = input.cuda() 74 | 75 | g_optim.zero_grad() 76 | d_optim.zero_grad() 77 | sigma = self.sigma_noise ** 2 78 | input_w_noise = gaussian(input, 1, 0, sigma) #Noise 79 | # Inference from generator 80 | g_output = self.g(input_w_noise) 81 | 82 | vutils.save_image(input[0:self.image_grids_numbers, :, :, :], 83 | './results/%03d_real_samples_epoch.png' % (num_epoch), nrow=self.n_row_in_grid, normalize=True) 84 | vutils.save_image(g_output[0:self.image_grids_numbers, :, :, :], 85 | './results/%03d_fake_samples_epoch.png' % (num_epoch), nrow=self.n_row_in_grid, normalize=True) 86 | vutils.save_image(input_w_noise[0:self.image_grids_numbers, :, :, :], 87 | './results/%03d_noise_samples_epoch.png' % (num_epoch), nrow=self.n_row_in_grid, normalize=True) 88 | 89 | ####################### 90 | ####################### 91 | d_fake_output = self.d(g_output) 92 | d_real_output = self.d(input) 93 | d_fake_loss = F.binary_cross_entropy(torch.squeeze(d_fake_output), fake) 94 | d_real_loss = F.binary_cross_entropy(torch.squeeze(d_real_output), valid) 95 | d_sum_loss = 0.5 * (d_fake_loss + d_real_loss) 96 | d_sum_loss.backward(retain_graph=True) 97 | d_optim.step() 98 | g_optim.zero_grad() 99 | 100 | ############################################## 101 | g_recon_loss = F.mse_loss(g_output, input) 102 | g_adversarial_loss = F.binary_cross_entropy(d_fake_output.squeeze(), valid) 103 | g_sum_loss = (1-self.adversarial_training_factor)*g_recon_loss + self.adversarial_training_factor*g_adversarial_loss 104 | g_sum_loss.backward() 105 | g_optim.step() 106 | 107 | if i%1 == 0: 108 | opts_ft = parse_opts_ft() #opts for phase two 109 | 110 | if num_epoch == opts_ft.low_epoch: 111 | g_model_name = 'g_low_epoch' 112 | model_save_path = './models/' + g_model_name 113 | torch.save({ 114 | 'epoch': num_epoch, 115 | 'g_model_state_dict': self.g.state_dict(), 116 | 'g_optimizer_state_dict': g_optim.state_dict(), 117 | }, model_save_path) 118 | 119 | if num_epoch >= opts_ft.high_epoch: 120 | g_model_name = 'g_high_epoch' 121 | d_model_name = 'd_high_epoch' 122 | model_save_path = './models/' + g_model_name 123 | torch.save({ 124 | 'epoch': num_epoch, 125 | 'g_model_state_dict': self.g.state_dict(), 126 | 'g_optimizer_state_dict': g_optim.state_dict(), 127 | }, model_save_path) 128 | 129 | model_save_path = './models/' + d_model_name 130 | torch.save({ 131 | 'epoch': num_epoch, 132 | 'd_model_state_dict': self.d.state_dict(), 133 | 'd_optimizer_state_dict': d_optim.state_dict(), 134 | }, model_save_path) 135 | 136 | print('Epoch {0} / Iteration {1}, before phase two: '.format(num_epoch, i)) 137 | high_epoch_g_model_name = 'g_high_epoch' 138 | high_epoch_d_model_name = 'd_high_epoch' 139 | g_model_save_path = './models/' + high_epoch_g_model_name 140 | d_model_save_path = './models/' + high_epoch_d_model_name 141 | check_auc(g_model_save_path, d_model_save_path,1) 142 | fine_tune() #Phase two 143 | print('After phase two: ') 144 | high_epoch_g_model_name = 'g_high_epoch' 145 | high_epoch_d_model_name = 'd_high_epoch' 146 | g_model_save_path = './models/' + high_epoch_g_model_name 147 | d_model_save_path = './models/' + high_epoch_d_model_name 148 | 149 | check_auc(g_model_save_path, d_model_save_path,1) 150 | 151 | def test_patches(self,g_model_path, d_model_path,i): #test all images/patches present inside a folder on given g and d models. Returns d score of each patch 152 | checkpoint_epoch_g = -1 153 | g_checkpoint = torch.load(g_model_path) 154 | self.g.load_state_dict(g_checkpoint['g_model_state_dict']) 155 | checkpoint_epoch_g = g_checkpoint['epoch'] 156 | if checkpoint_epoch_g is -1: 157 | raise Exception("g_model not loaded") 158 | else: 159 | pass 160 | d_checkpoint = torch.load(d_model_path) 161 | self.d.load_state_dict(d_checkpoint['d_model_state_dict']) 162 | checkpoint_epoch_d = d_checkpoint['epoch'] 163 | if checkpoint_epoch_g == checkpoint_epoch_d: 164 | pass 165 | else: 166 | raise Exception("d_model not loaded or model mismatch between g and d") 167 | 168 | self.g.eval() 169 | self.d.eval() 170 | labels = [] 171 | d_results = [] 172 | count = 0 173 | for input, label in self.dataloader: 174 | input = input.cuda() 175 | g_output = self.g(input) 176 | d_fake_output = self.d(g_output) 177 | count +=1 178 | d_results.append(d_fake_output.cpu().detach().numpy()) 179 | labels.append(label) 180 | return d_results, labels 181 | 182 | -------------------------------------------------------------------------------- /model_fine_tune_discriminator.py: -------------------------------------------------------------------------------- 1 | import torch 2 | import torch.nn.functional as F 3 | from torch import nn 4 | import torch.optim as optim 5 | from network import d_net, g_net 6 | 7 | class Fine_Tune_Disc(nn.Module): 8 | @staticmethod 9 | def name(): 10 | return 'Old is Gold: Redefining the adversarially learned one-class classification paradigm' 11 | 12 | def __init__(self, opt, dataloader): 13 | super(Fine_Tune_Disc, self).__init__() 14 | self.d_learning_rate = opt.d_learning_rate 15 | self.epoch = opt.epoch 16 | self.batch_size = opt.batch_size 17 | self.n_threads = opt.n_threads 18 | self.high_epoch_fake_loss_contribution = opt.high_epoch_fake_loss_contribution 19 | self.psuedo_anomaly_contribution = opt.psuedo_anomaly_contribution 20 | 21 | self.dataloader = dataloader 22 | self.iterations = opt.iterations 23 | 24 | self.g_low_epoch = g_net().cuda() 25 | self.g_high_epoch = g_net().cuda() 26 | self.d_high_epoch = d_net().cuda() 27 | 28 | self.filename = '' 29 | self.test_anomaly_threshold = opt.test_anomaly_threshold 30 | 31 | def train(self, load_model_epoch, model_folder_path): 32 | load_g_low_epoch_model = model_folder_path + 'g_low_epoch' 33 | g_low_epoch_checkpoint = torch.load(load_g_low_epoch_model) 34 | self.g_low_epoch.load_state_dict(g_low_epoch_checkpoint['g_model_state_dict']) 35 | checkpoint_epoch1 = g_low_epoch_checkpoint['epoch'] 36 | load_g_high_epoch_model = model_folder_path + 'g_high_epoch' 37 | g_high_epoch_checkpoint = torch.load(load_g_high_epoch_model) 38 | self.g_high_epoch.load_state_dict(g_high_epoch_checkpoint['g_model_state_dict']) 39 | checkpoint_epoch2 = g_high_epoch_checkpoint['epoch'] 40 | load_d_high_epoch_model = model_folder_path + 'd_high_epoch' 41 | d_high_epoch_checkpoint = torch.load(load_d_high_epoch_model) 42 | self.d_high_epoch.load_state_dict(d_high_epoch_checkpoint['d_model_state_dict']) 43 | checkpoint_epoch3 = d_high_epoch_checkpoint['epoch'] 44 | self.g_low_epoch.eval() 45 | self.g_high_epoch.eval() 46 | self.d_high_epoch.train() 47 | 48 | # Set optimizator(s) 49 | d_optim = optim.Adam(self.d_high_epoch.parameters(), lr=self.d_learning_rate) 50 | label_real_input = torch.ones([self.batch_size], dtype=torch.float32).cuda() * 0 51 | label_fake_high_quality = torch.ones([self.batch_size], dtype=torch.float32).cuda() * 0 52 | label_fake_low_quality = torch.ones([self.batch_size], dtype=torch.float32).cuda() * 1 53 | label_fake_augmented = torch.ones([self.batch_size], dtype=torch.float32).cuda() * 1 54 | 55 | 56 | it = 0 57 | for num_epoch in range(self.epoch): 58 | train_loader_iter = iter(self.dataloader) 59 | total_batches_pulled = 0 60 | for i, data in enumerate(train_loader_iter): 61 | input, gt_label = data 62 | input = input.cuda() 63 | d_optim.zero_grad() 64 | input_w_noise = input 65 | 66 | # Inference from generators 67 | g_low_epoch_output = self.g_low_epoch(input_w_noise) 68 | g_high_epoch_output = self.g_high_epoch(input_w_noise) 69 | total_batches_pulled += 3 # to avoid the iterator to crash 70 | if total_batches_pulled >= len(self.dataloader): 71 | # print('breaking because the batches are finished') 72 | break 73 | b1 = next(train_loader_iter) 74 | b2 = next(train_loader_iter) 75 | low_fake_augmented_1 , gar = b1 76 | low_fake_augmented_2 , gar = b2 77 | 78 | g_low_fake_augmented_1 = self.g_low_epoch(low_fake_augmented_1.cuda()) 79 | g_low_fake_augmented_2 = self.g_low_epoch(low_fake_augmented_2.cuda()) 80 | augmented_image = (g_low_fake_augmented_1 + g_low_fake_augmented_2) / 2 81 | g_augmented_data = self.g_high_epoch(augmented_image) 82 | 83 | ####################### 84 | ####################### 85 | d_low_epoch_fake_output = self.d_high_epoch(g_low_epoch_output) #low epoch recon 86 | d_high_epoch_fake_output = self.d_high_epoch(g_high_epoch_output) #high epoch recon 87 | d_real_output = self.d_high_epoch(input) #real image 88 | d_augmented_g = self.d_high_epoch(g_augmented_data) #psuedo anomaly 89 | loss_1 = F.binary_cross_entropy(torch.squeeze(d_low_epoch_fake_output), label_fake_low_quality) * self.psuedo_anomaly_contribution + F.binary_cross_entropy(torch.squeeze(d_augmented_g), label_fake_augmented) * (1-self.psuedo_anomaly_contribution) 90 | loss_2_1 = F.binary_cross_entropy(torch.squeeze(d_high_epoch_fake_output), label_fake_high_quality) 91 | loss_2_2 = F.binary_cross_entropy(torch.squeeze(d_real_output), label_real_input) 92 | loss_2 = (loss_2_1 * self.high_epoch_fake_loss_contribution) + (loss_2_2 * (1-self.high_epoch_fake_loss_contribution)) 93 | d_sum_loss = loss_1 + loss_2 94 | d_sum_loss.backward() 95 | d_optim.step() 96 | 97 | if it == self.iterations: 98 | high_epoch_g_model_name = 'g_high_epoch' 99 | high_epoch_d_model_name = 'd_high_epoch' 100 | g_model_save_path = './models/' + high_epoch_g_model_name 101 | torch.save({ 102 | 'epoch': num_epoch, 103 | 'g_model_state_dict': self.g_high_epoch.state_dict(), 104 | }, g_model_save_path) 105 | 106 | d_model_save_path = './models/' + high_epoch_d_model_name 107 | torch.save({ 108 | 'epoch': num_epoch, 109 | 'd_model_state_dict': self.d_high_epoch.state_dict(), 110 | 'd_optimizer_state_dict': d_optim.state_dict(), 111 | }, d_model_save_path) 112 | 113 | if it == self.iterations: 114 | return 115 | it += 1 116 | return d_model_save_path, g_model_save_path -------------------------------------------------------------------------------- /network.py: -------------------------------------------------------------------------------- 1 | 2 | from torch import nn 3 | 4 | 5 | class g_net(nn.Module): 6 | def __init__(self): 7 | super(g_net, self).__init__() 8 | self.encoder = nn.Sequential( 9 | 10 | nn.Conv2d(1, 64, 5, stride=1), 11 | nn.BatchNorm2d(64), 12 | nn.ReLU(True), 13 | nn.Conv2d(64, 128, 5, stride=1), 14 | nn.BatchNorm2d(128), 15 | nn.ReLU(True), 16 | nn.Conv2d(128, 256, 5, stride=1), 17 | nn.ReLU(True), 18 | nn.BatchNorm2d(256), 19 | nn.Conv2d(256, 512, 5, stride=1), 20 | nn.ReLU(True), 21 | nn.BatchNorm2d(512), 22 | ) 23 | self.decoder = nn.Sequential( 24 | 25 | nn.ConvTranspose2d(512, 256, 5, stride=1), 26 | nn.BatchNorm2d(256), 27 | nn.ReLU(True), 28 | nn.ConvTranspose2d(256, 128, 5, stride=1), 29 | nn.BatchNorm2d(128), 30 | nn.ReLU(True), 31 | nn.ConvTranspose2d(128, 64, 5, stride=1), 32 | nn.BatchNorm2d(64), 33 | nn.ReLU(True), 34 | nn.ConvTranspose2d(64, 1, 5, stride=1), 35 | nn.Tanh() 36 | ) 37 | 38 | def forward(self, x): 39 | x = self.encoder(x) 40 | x = self.decoder(x) 41 | return x 42 | 43 | class Flatten(nn.Module): 44 | def forward(self, input): 45 | return input.view(input.size(0), -1) 46 | 47 | class d_net(nn.Module): 48 | def __init__(self): 49 | super(d_net, self).__init__() 50 | self.discriminator = nn.Sequential( 51 | 52 | nn.Conv2d(1, 64, 5, stride=2, padding=2), 53 | nn.BatchNorm2d(64), 54 | nn.ReLU(True), 55 | nn.Conv2d(64, 128, 5, stride=2, padding=2), 56 | nn.BatchNorm2d(128), 57 | nn.ReLU(True), 58 | nn.Conv2d(128, 256, 5, stride=2, padding=2), 59 | nn.BatchNorm2d(256), 60 | nn.ReLU(True), 61 | nn.Conv2d(256, 512, 5, stride=2, padding=2), 62 | nn.ReLU(True), 63 | Flatten(), 64 | nn.Linear(4608, 1), 65 | nn.Sigmoid() 66 | ) 67 | 68 | def forward(self, x): 69 | x = self.discriminator(x) 70 | return x 71 | -------------------------------------------------------------------------------- /opts.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | 3 | 4 | #Phase one / baseline related options 5 | 6 | def parse_opts(): 7 | parser = argparse.ArgumentParser() 8 | parser.add_argument('--data_path', default='./data/train', type=str, help='Input file path') 9 | parser.add_argument('--normal_class', default='0', type=str, help='normal_class_folder_name') 10 | parser.add_argument('--g_learning_rate', default='0.001', type=float, help='g_learning_rate') 11 | parser.add_argument('--d_learning_rate', default='0.0001', type=float, help='d_learning_rate') 12 | parser.add_argument('--adversarial_training_factor', default='0.5', type=float, help='loss parameter for generator (reconstruction and adversaria)') 13 | parser.add_argument('--sigma_noise', default='0.9', type=float, help='sigma of noise added to the iamges') 14 | parser.add_argument('--epoch', default=100, type=int, help='Epoch for training') 15 | parser.add_argument('--batch_size', default=64, type=int, help='Batch size') 16 | parser.add_argument('--n_threads', default=8, type=int, help='Number of threads for multi-thread loading') 17 | parser.add_argument('--batch_shuffle', default=True, type=bool, help='shuffle input batch or not') 18 | parser.add_argument('--drop_last', default=True, type=bool, help='drop the remaining of the batch if the size doesnt match minimum batch size') 19 | parser.add_argument('--image_grids_numbers', default=64, type=int, help='total number of grid squares to be saved every / few epochs') 20 | parser.add_argument('--n_row_in_grid', default=10, type=int, help=' Number of images displayed in each row of the grid images.') 21 | parser.add_argument('--frame_size', default=45, type=int, help='one side size of the square patch to be extracted from each frame') 22 | parser.add_argument('--final_d_path', default='', type=str, help='final d model save path') 23 | parser.add_argument('--final_g_path', default='', type=str, help='final g model save path') 24 | args = parser.parse_args() 25 | return args 26 | -------------------------------------------------------------------------------- /opts_fine_tune_discriminator.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | 3 | 4 | # Phase Two Related options 5 | def parse_opts_ft(): 6 | parser = argparse.ArgumentParser() 7 | parser.add_argument('--data_path', default='./data/train/', type=str, help='Input file path') 8 | parser.add_argument('--normal_class', default='0', type=str, help='normal_class') 9 | parser.add_argument('--d_learning_rate', default='0.00005', type=float, help='d_learning_rate') 10 | parser.add_argument('--high_epoch_fake_loss_contribution', default='0.9', type=float, help='the contribution of Gn fake loss being added compared with the loss of real images in total_normal_loss') 11 | parser.add_argument('--psuedo_anomaly_contribution', default='0.001', type=float, help='the contribution of pseudo anomalies loss being added compared with the loss of Go reconstructed images in total_normal_loss') 12 | parser.add_argument('--epoch', default=10, type=int, help='Epoch for training') 13 | parser.add_argument('--batch_size', default=64, type=int, help='Batch size') 14 | parser.add_argument('--n_threads', default=8, type=int, help='Number of threads for multi-thread loading') 15 | parser.add_argument('--batch_shuffle', default=True, type=bool, help='shuffle input batch or not') 16 | parser.add_argument('--test_anomaly_threshold', default='0.5', type=float, help='threshold to print anomaly or not') 17 | parser.add_argument('--drop_last', default=True, type=bool, help='drop the remaining of the batch if the size doesnt match minimum batch size') 18 | parser.add_argument('--frame_size', default=45, type=int, help='one side size of the square patch to be extracted from each frame') 19 | parser.add_argument('--low_epoch', default=0, type=int, help='low epoch i.e. Gold for phase two') 20 | parser.add_argument('--high_epoch', default=3, type=int, help='high epoch i.e. Gn for phase two') 21 | parser.add_argument('--iterations', default=75, type=int, help='iterations for phase two') 22 | args = parser.parse_args() 23 | return args 24 | 25 | -------------------------------------------------------------------------------- /test.py: -------------------------------------------------------------------------------- 1 | # from __future__ import print_function 2 | # import numpy as np 3 | # from sklearn import metrics 4 | # import re 5 | # lastNum = re.compile(r'(?:[^\d]*(\d+)[^\d]*)+') 6 | # from dataloader import load_data 7 | # from model import OGNet 8 | # from opts import parse_opts 9 | 10 | # # Test code for the CVPR 2020 paper -> Old is Gold: Redefining the Adversarially Learned One-Class Classifier Training Paradigm 11 | # # https://arxiv.org/abs/2004.07657 12 | # # http://openaccess.thecvf.com/content_CVPR_2020/html/Zaheer_Old_Is_Gold_Redefining_the_Adversarially_Learned_One-Class_Classifier_Training_CVPR_2020_paper.html 13 | # # Code written by Zaigham and Jin-ha. 14 | # def check_auc(g_model_path, d_model_path, opt, i): 15 | # opt.batch_shuffle = False 16 | # opt.drop_last = False 17 | # dataloader = load_data(opt) 18 | # model = OGNet(opt, dataloader) 19 | # model.cuda() 20 | # d_results, labels = model.test_patches(g_model_path, d_model_path, i) 21 | # d_results = np.concatenate(d_results) 22 | # labels = np.concatenate(labels) 23 | # fpr1, tpr1, thresholds1 = metrics.roc_curve(labels, d_results, pos_label=1) # (y, score, positive_label) 24 | # fnr1 = 1 - tpr1 25 | # eer_threshold1 = thresholds1[np.nanargmin(np.absolute((fnr1 - fpr1)))] 26 | # eer_threshold1 = eer_threshold1 27 | # d_f1 = np.copy(d_results) 28 | # d_f1[d_f1 >= eer_threshold1] = 1 29 | # d_f1[d_f1 < eer_threshold1] = 0 30 | # f1_score = metrics.f1_score(labels, d_f1, pos_label=0) 31 | # print("AUC: {0}, F1_score: {1}".format(metrics.auc(fpr1,tpr1), f1_score)) 32 | 33 | 34 | 35 | # if __name__ == '__main__': 36 | # opt = parse_opts() 37 | # opt.data_path = './data/test/' #test data path 38 | # g_model_path = './models/phase_two_g' #generator model path 39 | # d_model_path = './models/phase_two_d' #discriminator model path 40 | # print('working on :', g_model_path, d_model_path) 41 | # check_auc(g_model_path, d_model_path, opt, 0) 42 | 43 | -------------------------------------------------------------------------------- /train.py: -------------------------------------------------------------------------------- 1 | 2 | from model import OGNet 3 | from opts import parse_opts 4 | from dataloader import load_data_train 5 | 6 | # Code for the CVPR 2020 paper -> Old is Gold: Redefining the Adversarially Learned One-Class Classifier Training Paradigm 7 | # https://arxiv.org/abs/2004.07657 8 | # http://openaccess.thecvf.com/content_CVPR_2020/html/Zaheer_Old_Is_Gold_Redefining_the_Adversarially_Learned_One-Class_Classifier_Training_CVPR_2020_paper.html 9 | # If you use this code, or find it helpful, please cite our paper: 10 | # @inproceedings{zaheer2020old, 11 | # title={Old is Gold: Redefining the Adversarially Learned One-Class Classifier Training Paradigm}, 12 | # author={Zaheer, Muhammad Zaigham and Lee, Jin-ha and Astrid, Marcella and Lee, Seung-Ik}, 13 | # booktitle={Proceedings of the IEEE/CVF Conference on Computer Vision and Pattern Recognition}, 14 | # pages={14183--14193}, 15 | # year={2020} 16 | # } 17 | # 18 | # Please contact me through email, if you have any questions or need any help: mzz . pieas (at) gmailcom 19 | if __name__=="__main__": 20 | 21 | opt = parse_opts() 22 | train_loader = load_data_train(opt) 23 | model = OGNet(opt, train_loader) 24 | model.cuda() 25 | model.train(opt.normal_class) 26 | 27 | -------------------------------------------------------------------------------- /utils.py: -------------------------------------------------------------------------------- 1 | import torch 2 | 3 | def gaussian(ins, is_training, mean, stddev): 4 | if is_training: 5 | noise = ins.data.new(ins.size()).normal_(mean, stddev) 6 | noisy_image = ins + noise 7 | if noisy_image.max().data > 1 or noisy_image.min().data < -1: 8 | noisy_image = torch.clamp(noisy_image, -1, 1) 9 | if noisy_image.max().data > 1 or noisy_image.min().data < -1: 10 | raise Exception('input image with noise has values larger than 1 or smaller than -1') 11 | return noisy_image 12 | return ins --------------------------------------------------------------------------------