22import logging
33from typing import List
44from Adversarial_Observation .BirdParticle import BirdParticle
5-
5+ from tqdm import tqdm
66import tensorflow as tf
77import numpy as np
88import matplotlib .pyplot as plt
@@ -15,10 +15,10 @@ class ParticleSwarm:
1515 to misclassify it into the target class.
1616 """
1717
18- def __init__ (self , model : tf .keras .Model , input_set : np .ndarray , target_class : int ,
19- num_iterations : int = 20 , epsilon : float = 0.8 , save_dir : str = 'results' ,
20- inertia_weight : float = 0 .5 , cognitive_weight : float = .5 , social_weight : float = .5 ,
21- momentum : float = 0.9 , velocity_clamp : float = 0.1 ):
18+ def __init__ (self , model : tf .keras .Model , input_set : np .ndarray , starting_class : int , target_class : int ,
19+ num_iterations : int = 20 , save_dir : str = 'results' , inertia_weight : float = 0.5 ,
20+ cognitive_weight : float = .5 , social_weight : float = .5 , momentum : float = 0.9 ,
21+ clip_value_position : float = 0.2 , enable_logging : bool = False , device : str = 'cpu' ):
2222 """
2323 Initialize the Particle Swarm Optimization (PSO) for adversarial attacks.
2424
@@ -27,44 +27,48 @@ def __init__(self, model: tf.keras.Model, input_set: np.ndarray, target_class: i
2727 input_set (np.ndarray): The batch of input images to attack as a NumPy array.
2828 target_class (int): The target class for misclassification.
2929 num_iterations (int): The number of optimization iterations.
30- epsilon (float): The perturbation bound.
3130 save_dir (str): The directory to save output images and logs.
3231 inertia_weight (float): The inertia weight for the velocity update.
3332 cognitive_weight (float): The cognitive weight for the velocity update.
3433 social_weight (float): The social weight for the velocity update.
3534 momentum (float): The momentum for the velocity update.
36- velocity_clamp (float): The velocity clamp to limit the velocity.
35+ clip_value_position (float): The velocity clamp to limit the velocity.
36+ device (str): The device for computation ('cpu' or 'gpu'). Default is 'cpu'.
3737 """
3838 self .model = model
3939 self .input_set = tf .convert_to_tensor (input_set , dtype = tf .float32 ) # Convert NumPy array to TensorFlow tensor
40+ self .start_class = starting_class # The starting class index
4041 self .target_class = target_class # The target class index
4142 self .num_iterations = num_iterations
42- self .epsilon = epsilon # Perturbation bound
4343 self .save_dir = save_dir # Directory to save perturbed images
44-
44+ self .enable_logging = enable_logging
45+ self .device = device # Device ('cpu' or 'gpu')
46+
4547 self .particles : List [BirdParticle ] = [
46- BirdParticle (model , self .input_set [i :i + 1 ], target_class , epsilon ,
47- inertia_weight = inertia_weight , cognitive_weight = cognitive_weight ,
48- social_weight = social_weight , momentum = momentum , velocity_clamp = velocity_clamp )
48+ BirdParticle (model , self .input_set [i :i + 1 ], target_class ,
49+ inertia_weight = inertia_weight , cognitive_weight = cognitive_weight , social_weight = social_weight ,
50+ momentum = momentum , clip_value_position = clip_value_position )
4951 for i in range (len (input_set ))
5052 ]
5153
5254 self .global_best_position = tf .zeros_like (self .input_set [0 ]) # Global best position
5355 self .global_best_score = - float ('inf' ) # Initialize with a very low score
5456
5557 self .fitness_history : List [float ] = [] # History of fitness scores to track progress
56- self .setup_logging () # Set up logging
57- self .log_progress (- 1 ) # Log initial state (before optimization)
58+
59+ # Make output folder
60+ iteration_dir = self .save_dir
61+ os .makedirs (iteration_dir , exist_ok = True )
62+ if self .enable_logging :
63+ self .setup_logging ()
64+ self .log_progress (- 1 )
5865
5966 def setup_logging (self ):
6067 """
6168 Set up logging for each iteration. Each iteration will have a separate log file.
6269 Also prints logs to the terminal.
6370 """
64- iteration_dir = self .save_dir
65- os .makedirs (iteration_dir , exist_ok = True )
66-
67- log_file = os .path .join (iteration_dir , f'iteration_log.log' )
71+ log_file = os .path .join (self .save_dir , f'iteration_log.log' )
6872 self .logger = logging .getLogger ()
6973
7074 # Create a file handler to save logs to a file
@@ -86,7 +90,6 @@ def setup_logging(self):
8690 self .logger .info (f"Model: { self .model .__class__ .__name__ } " )
8791 self .logger .info (f"Target Class: { self .target_class } (This is the class we want to misclassify the image into)" )
8892 self .logger .info (f"Number of Iterations: { self .num_iterations } (Optimization steps)" )
89- self .logger .info (f"Epsilon (perturbation bound): { self .epsilon } (Maximum perturbation allowed)" )
9093 self .logger .info (f"Save Directory: { self .save_dir } " )
9194 self .logger .info (f"{ '*' * 60 } " )
9295
@@ -97,13 +100,16 @@ def log_progress(self, iteration: int):
97100 Args:
98101 iteration (int): The current iteration of the optimization process.
99102 """
103+ if not self .enable_logging :
104+ return
105+
100106 # Log the header for the iteration
101107 self .logger .info (f"\n { '-' * 60 } " )
102108 self .logger .info (f"Iteration { iteration + 1 } /{ self .num_iterations } " )
103109 self .logger .info (f"{ '=' * 60 } " )
104110
105111 # Table header
106- header = f"{ 'Particle' :<10} { 'Original Pred' :<15} { 'Perturbed Pred' :<18} { 'Orig Target Prob' :<20} " \
112+ header = f"{ 'Particle' :<10} { 'Original Pred' :<15} { 'Perturbed Pred' :<18} { 'Orig Start Prob' :<20 } { 'Pert Start Prob' :<20 } { 'Orig Target Prob' :<20} " \
107113 f"{ 'Pert Target Prob' :<20} { 'Personal Best' :<20} { 'Global Best' :<20} "
108114 self .logger .info (header )
109115 self .logger .info (f"{ '-' * 60 } " )
@@ -121,51 +127,96 @@ def log_progress(self, iteration: int):
121127 # Get softmax probabilities
122128 original_probs = tf .nn .softmax (original_output , axis = 1 )
123129 perturbed_probs = tf .nn .softmax (perturbed_output , axis = 1 )
124-
130+
131+ # Get starting class probabilities (how far away we've moved)
132+ original_prob_start = original_probs [0 , self .start_class ].numpy ().item ()
133+ perturbed_prob_start = perturbed_probs [0 , self .start_class ].numpy ().item ()
134+
125135 # Get target class probabilities
126136 original_prob_target = original_probs [0 , self .target_class ].numpy ().item ()
127137 perturbed_prob_target = perturbed_probs [0 , self .target_class ].numpy ().item ()
128138
129139 # Log each particle's data in a formatted row
130- self .logger .info (f"{ i + 1 :<10} { original_pred :<15} { perturbed_pred :<18} { original_prob_target :<20.4f} "
131- f" { perturbed_prob_target :<20.4f} { particle .best_score :<20.4f} { self .global_best_score :<20.4f} " )
140+ self .logger .info (f"{ i + 1 :<10} { original_pred :<15} { perturbed_pred :<18} { original_prob_start :<20.4f } { perturbed_prob_start :<20.4f} "
141+ f" { original_prob_target :<20.4f } { perturbed_prob_target :<20.4f} { particle .best_score :<20.4f} { self .global_best_score :<20.4f} " )
132142
133143 self .logger .info (f"{ '=' * 60 } " )
134144
135- def save_images (self , iteration : int ):
145+ def optimize (self ):
136146 """
137- Save the perturbed images for the current iteration.
138-
139- Args:
140- iteration (int): The current iteration number.
147+ Run the Particle Swarm Optimization process to optimize the perturbations.
141148 """
142- iteration_folder = os .path .join (self .save_dir , f"iteration_{ iteration + 1 } " )
143- os .makedirs (iteration_folder , exist_ok = True )
144-
145- for i , particle in enumerate (self .particles ):
146- # Convert TensorFlow tensor to NumPy array
147- position_numpy = particle .position .numpy ()
148- # Remove extra batch dimension (if it exists)
149- position_numpy = np .squeeze (position_numpy ) # Now shape is (28, 28)
150- plt .imsave (os .path .join (iteration_folder , f"perturbed_image_{ i + 1 } .png" ), position_numpy , cmap = "gray" , vmin = 0 , vmax = 1 )
149+ with tf .device (f"/{ self .device } :0" ): # Use the GPU/CPU based on the flag
150+ for iteration in tqdm (range (self .num_iterations ), desc = "Running Swarm" ):
151+ # Update particles and velocities, evaluate them, and track global best
152+ for particle in self .particles :
153+ particle .evaluate ()
154+ particle .update_velocity (self .global_best_position ) # No need to pass inertia_weight explicitly
155+ particle .update_position ()
156+
157+ # Update the global best based on the personal best scores of particles
158+ best_particle = max (self .particles , key = lambda p : p .best_score )
159+ if best_particle .best_score > self .global_best_score :
160+ self .global_best_score = best_particle .best_score
161+ self .global_best_position = tf .identity (best_particle .best_position )
162+
163+ self .log_progress (iteration )
151164
152- def optimize (self ) :
165+ def reduce_excess_perturbations (self , original_img : np . ndarray , target_label : int , model_shape : tuple = ( 1 , 28 , 28 , 1 )) -> np . ndarray :
153166 """
154- Run the Particle Swarm Optimization process to optimize the perturbations.
167+ Reduces excess perturbations in adversarial images while ensuring the misclassification remains.
168+
169+ Args:
170+ original_img (np.ndarray): The original (clean) image.
171+ target_label (int): The label we want the adversarial image to produce.
172+ model_shape (tuple): The expected shape of the model input.
173+
174+ Returns:
175+ list[np.ndarray]: A list of denoised adversarial images.
155176 """
156- for iteration in range (self .num_iterations ):
157-
158- # Update particles and velocities, evaluate them, and track global best
159- for particle in self .particles :
160- particle .evaluate ()
161- particle .update_velocity (self .global_best_position ) # No need to pass inertia_weight explicitly
162- particle .update_position ()
163-
164- # Update the global best based on the personal best scores of particles
165- best_particle = max (self .particles , key = lambda p : p .best_score )
166- if best_particle .best_score > self .global_best_score :
167- self .global_best_score = best_particle .best_score
168- self .global_best_position = tf .identity (best_particle .best_position )
169-
170- self .save_images (iteration )
171- self .log_progress (iteration )
177+ denoised_adv = []
178+ total_pixels = np .prod (original_img .shape ) # Total pixels in the image
179+
180+ for adv_particle in tqdm (self .particles , desc = "Processing Particles" , unit = "particle" ):
181+ adv_img = np .copy (adv_particle .position ).reshape (original_img .shape ) # Copy to avoid modifying the original
182+
183+ if original_img .shape != adv_img .shape :
184+ raise ValueError ("original_img and adv_img must have the same shape." )
185+
186+ # Iterate over every pixel coordinate in the image with tqdm progress bar
187+ with tqdm (total = total_pixels , desc = "Processing Pixels" , unit = "pixel" , leave = False ) as pbar :
188+ for idx in np .ndindex (original_img .shape ):
189+ if original_img [idx ] == adv_img [idx ]: # Ignore unchanged pixels
190+ pbar .update (1 )
191+ continue
192+
193+ # Store old adversarial value
194+ old_val = adv_img [idx ]
195+
196+ # Try restoring the pixel to original
197+ adv_img [idx ] = original_img [idx ]
198+
199+ # Check if the label is still the target label
200+ output = self .model (adv_img .reshape (model_shape ))
201+ softmax_output = tf .nn .softmax (tf .squeeze (output ), axis = 0 ).numpy ()
202+ current_label = np .argmax (softmax_output )
203+
204+ if current_label != target_label :
205+ # If misclassification is lost, try halfway adjustment
206+ adv_img [idx ] = old_val
207+ adv_img [idx ] += (original_img [idx ] - adv_img [idx ]) * 0.5
208+
209+ # Recheck if the label is still the target
210+ output = self .model (adv_img .reshape (model_shape ))
211+ softmax_output = tf .nn .softmax (tf .squeeze (output ), axis = 0 ).numpy ()
212+ current_label = np .argmax (softmax_output )
213+
214+ if current_label != target_label :
215+ # If misclassification is still lost, revert back
216+ adv_img [idx ] = old_val
217+
218+ pbar .update (1 ) # Update pixel progress
219+
220+ denoised_adv .append (adv_img )
221+
222+ return denoised_adv
0 commit comments