diff --git a/cyclegan/models/test_model.py b/cyclegan/models/test_model.py index fe15f40..ea97866 100644 --- a/cyclegan/models/test_model.py +++ b/cyclegan/models/test_model.py @@ -64,6 +64,59 @@ class TestModel(BaseModel): """Run forward pass.""" self.fake = self.netG(self.real) # G(real) + def forward_noattack(self): + """Run forward pass.""" + self.fake_noattack = self.netG(self.real) # G(real) + + def attack(self): + image = self.real + # Attack + pgd_attack = attacks.LinfPGDAttack(model=self.netG) + black = np.zeros((1, 3, image.size(2), image.size(3))) + black = torch.FloatTensor(black).cuda() + input_adv, perturb = pgd_attack.perturb(image, black) + + return input_adv, perturb + + def forward_attack(self, perturb): + self.real = torch.clamp(self.real + perturb, min=-1, max=1) + self.fake = self.netG(self.real) # G(real) + + def compute_errors(self): + generated = self.fake + generated_noattack = self.fake_noattack + l1 = F.l1_loss(generated, generated_noattack) + l2 = F.mse_loss(generated, generated_noattack) + l0 = (generated - generated_noattack).norm(0) + d = (generated - generated_noattack).norm(float('-inf')) + return l1, l2, l0, d + + def attack(self, label, inst, image=None): + # Encode Inputs + image = Variable(image) if image is not None else None + input_label, inst_map, real_image, _ = self.encode_input(Variable(label), Variable(inst), image, infer=True) + + # Fake Generation + if self.use_features: + if self.opt.use_encoded_image: + # encode the real image to get feature map + feat_map = self.netE.forward(real_image, inst_map) + else: + # sample clusters from precomputed features + feat_map = self.sample_features(inst_map) + input_concat = torch.cat((input_label, feat_map), dim=1) + else: + input_concat = input_label + + # Attack + pgd_attack = attacks.LinfPGDAttack(model=self.netG) + black = np.zeros((1, 3, input_concat.size(2), input_concat.size(3))) + black = torch.FloatTensor(black).cuda() + # print(input_concat.size()) + input_adv, perturb = pgd_attack.perturb(input_concat, black) + + return input_adv, perturb + def optimize_parameters(self): """No optimization for test model.""" pass diff --git a/cyclegan/options/test_options.py b/cyclegan/options/test_options.py index c85c996..cf8f082 100644 --- a/cyclegan/options/test_options.py +++ b/cyclegan/options/test_options.py @@ -15,7 +15,7 @@ class TestOptions(BaseOptions): parser.add_argument('--phase', type=str, default='test', help='train, val, test, etc') # Dropout and Batchnorm has different behavioir during training and test. parser.add_argument('--eval', action='store_true', help='use eval mode during test time.') - parser.add_argument('--num_test', type=int, default=50, help='how many test images to run') + parser.add_argument('--num_test', type=int, default=100, help='how many test images to run') # rewrite devalue values parser.set_defaults(model='test') # To avoid cropping, the load_size should be the same as crop_size diff --git a/cyclegan/test.py b/cyclegan/test.py index 9281a99..56a5dbd 100644 --- a/cyclegan/test.py +++ b/cyclegan/test.py @@ -56,14 +56,34 @@ if __name__ == '__main__': # For [CycleGAN]: It should not affect CycleGAN as CycleGAN uses instancenorm without dropout. if opt.eval: model.eval() + + # Initialize Metrics + l1_error, l2_error, min_dist, l0_error, perceptual_error = 0.0, 0.0, 0.0, 0.0, 0.0 + n_samples = 0 + for i, data in enumerate(dataset): if i >= opt.num_test: # only apply our model to opt.num_test images. break model.set_input(data) # unpack data from data loader - model.test() # run inference + model.forward_noattack() + input_adv, perturb = model.attack() + with torch.no_grad(): + model.forward_attack(perturb) + model.compute_visuals() + + # Compute metrics + l1, l2, l0, d = model.compute_errors() + l1_error += l1 + l2_error += l2 + l0_error += l0 + min_dist += d + n_samples += 1 + # model.test() # run inference visuals = model.get_current_visuals() # get image results img_path = model.get_image_paths() # get image paths if i % 5 == 0: # save images to an HTML file print('processing (%04d)-th image... %s' % (i, img_path)) save_images(webpage, visuals, img_path, aspect_ratio=opt.aspect_ratio, width=opt.display_winsize) webpage.save() # save the HTML + + diff --git a/cyclegan/util/attacks.py b/cyclegan/util/attacks.py new file mode 100644 index 0000000..b6296ff --- /dev/null +++ b/cyclegan/util/attacks.py @@ -0,0 +1,50 @@ +import copy +import numpy as np +from collections import Iterable +from scipy.stats import truncnorm + +import torch +import torch.nn as nn + +class LinfPGDAttack(object): + def __init__(self, model=None, epsilon=0.05, k=1, a=0.05): + self.model = model + self.epsilon = epsilon + self.k = k + self.a = a + self.loss_fn = nn.MSELoss() + + def perturb(self, X_nat, y): + """ + Given examples (X_nat, y), returns adversarial + examples within epsilon of X_nat in l_infinity norm. + """ + X = X_nat.clone().detach_() + + for i in range(self.k): + print('test', i) + X.requires_grad = True + output = self.model(X) + + self.model.zero_grad() + loss = -self.loss_fn(output, y) + loss.backward() + grad = X.grad + + X_adv = X + self.a * grad.sign() + + eta = torch.clamp(X_adv - X_nat, min=-self.epsilon, max=self.epsilon) + X = torch.clamp(X_nat + eta, min=-1, max=1).detach_() + eta = None + X_adv = None + + return X, X - X_nat + +def clip_tensor(X, Y, Z): + # Clip X with Y min and Z max + X_np = X.data.cpu().numpy() + Y_np = Y.data.cpu().numpy() + Z_np = Z.data.cpu().numpy() + X_clipped = np.clip(X_np, Y_np, Z_np) + X_res = torch.FloatTensor(X_clipped) + return X_res \ No newline at end of file