<jupyter_start><jupyter_text>This notebook experiments with the idea of **discriminator fine-tuning**. The idea is simple - * First, train a GAN on the *unlabeled* version of the dataset with which you would like to conduct adversarial training as well. * We then construct a perturbed version of the original dataset as shown in [this notebook](https://github.com/dipanjanS/adversarial-learning-robustness/blob/main/tutorials/adversarial_training/01_1_adv_training_scratch.ipynb).* We then use the pre-trained discriminator to test its generalizability to this newly created dataset. A pictorial depiction of these steps has been shown in the figure below -   Initial Setup<jupyter_code># To prevent unnecessary warnings
import warnings
warnings.simplefilter(action='ignore', category=FutureWarning)

# TensorFlow and tf.keras
import tensorflow as tf

# Helper libraries
from sklearn.metrics import confusion_matrix, classification_report
from tqdm import tqdm
import matplotlib.pyplot as plt
import pandas as pd
import numpy as np

print(tf.__version__)<jupyter_output>2.3.0<jupyter_text>Dataset Collection and Preprocessing<jupyter_code>fashion_mnist = tf.keras.datasets.fashion_mnist
(train_images, train_labels), (test_images, test_labels) = fashion_mnist.load_data()

class_names = ['T-shirt/top', 'Trouser', 'Pullover', 'Dress', 'Coat',
               'Sandal', 'Shirt', 'Sneaker', 'Bag', 'Ankle boot']

print('\nTrain_images.shape: {}, of {}'.format(train_images.shape, train_images.dtype))
print('Test_images.shape: {}, of {}'.format(test_images.shape, test_images.dtype))
# Add channel dimension
train_images = np.expand_dims(train_images, -1) 
test_images = np.expand_dims(test_images, -1) 

# Our GAN was trained on images that were normalized to the
# range of [-1, 1], so we need to follow same preprocessing steps
train_images = train_images - 127.5 / 127.5
test_images = test_images - 127.5 / 127.5<jupyter_output><empty_output><jupyter_text>Model Building Utilities<jupyter_code># define input shape
INPUT_SHAPE = (28, 28, 1)

def get_training_model(main_activation='relu'):
    inputs = tf.keras.Input(shape=INPUT_SHAPE)
    x = tf.keras.layers.Conv2D(32, (3, 3), strides=2, padding="same", activation=main_activation)(inputs)
    x = tf.keras.layers.MaxPool2D((2,2))(x)
    x = tf.keras.layers.Conv2D(62, (3, 3), strides=2, padding="same", activation=main_activation)(x)
    x = tf.keras.layers.MaxPool2D((2,2))(x)
    x = tf.keras.layers.Flatten()(x)
    x = tf.keras.layers.Dense(256)(x)
    outputs = tf.keras.layers.Dense(10)(x)
    model = tf.keras.Model(inputs=inputs, outputs=outputs)
    return model

get_training_model().summary()
def plot_progress(hist):
    plt.plot(hist.history["loss"], label="train_loss")
    plt.plot(hist.history["val_loss"], label="validation_loss")
    plt.plot(hist.history["accuracy"], label="training_accuracy")
    plt.plot(hist.history["val_accuracy"], label="validation_accuracy")
    plt.title("Training Progress")
    plt.ylabel("accuracy/loss")
    plt.xlabel("epoch")
    plt.legend(loc="upper left")
    plt.show()<jupyter_output><empty_output><jupyter_text>Train a Regular Image Classification Model<jupyter_code>optimizer = tf.keras.optimizers.Adam(learning_rate=1e-3)
loss_fn = tf.keras.losses.SparseCategoricalCrossentropy(from_logits=True)

regular_model = get_training_model()
regular_model.compile(loss=loss_fn, optimizer=optimizer, metrics=["accuracy"])

BATCH_SIZE = 64

train_dataset = tf.data.Dataset.from_tensor_slices(
    (train_images, train_labels)
).shuffle(buffer_size=4096).batch(BATCH_SIZE)

test_dataset = tf.data.Dataset.from_tensor_slices(
    (test_images, test_labels)
).batch(BATCH_SIZE)
history = regular_model.fit(train_dataset, 
                            validation_data=test_dataset, 
                            epochs=10)
plot_progress(history)
predictions = regular_model.predict(test_images)
predictions = tf.nn.softmax(predictions)
prediction_labels = np.argmax(predictions, axis=1)
print(classification_report(test_labels, prediction_labels, target_names=class_names))
pd.DataFrame(confusion_matrix(test_labels, prediction_labels), index=class_names, columns=class_names)<jupyter_output>precision    recall  f1-score   support

 T-shirt/top       0.85      0.79      0.82      1000
     Trouser       0.92      0.98      0.95      1000
    Pullover       0.87      0.76      0.81      1000
       Dress       0.91      0.83      0.87      1000
        Coat       0.75      0.87      0.80      1000
      Sandal       0.95      0.98      0.97      1000
       Shirt       0.67      0.71      0.69      1000
     Sneaker       0.95      0.93      0.94      1000
         Bag       0.98      0.95      0.97      1000
  Ankle boot       0.95      0.96      0.96      1000

    accuracy                           0.88     10000
   macro avg       0.88      0.88      0.88     10000
weighted avg       0.88      0.88      0.88     10000<jupyter_text>Creating the  Datasets (train + eval) Adversarial Training with FGSM<jupyter_code>def get_model_preds(input_image, class_names_map, model):
        logits = model.predict(input_image)
        preds = tf.nn.softmax(logits)
        top_idx = np.argsort(-preds)[0][0]
        top_prob = -np.sort(-preds)[0][0]
        top_class = np.array(class_names_map)[top_idx]
        return top_class, top_prob


def generate_adverarial_pattern(input_image, image_label_idx, model, loss_func):
    with tf.GradientTape() as tape:
        tape.watch(input_image)
        prediction = model(input_image)
        #prediction = tf.nn.softmax(prediction)
        loss = loss_func(image_label_idx, prediction)
    # Get the gradients of the loss w.r.t to the input image.
    gradient = tape.gradient(loss, input_image)
    # Get the sign of the gradients to create the perturbation
    signed_grad = tf.sign(gradient)
    return signed_grad
# We want maximize the cross-entropy loss
scc = tf.keras.losses.SparseCategoricalCrossentropy(from_logits=True)
def generate_perturbed_images(input_images, image_label_idxs, model, loss_func, eps=0.01):
  perturbed_images = []
  # don't use list on large data - used just to view fancy progress-bar
  for image, label in tqdm(list(zip(input_images, image_label_idxs))): 
      image = tf.convert_to_tensor(np.array([image]))
      adv_pattern = generate_adverarial_pattern(image, label, model, loss_func)
      clip_adv_pattern = tf.clip_by_value(adv_pattern, clip_value_min=-1, clip_value_max=1.)
      perturbed_img = image + (eps * clip_adv_pattern)
      perturbed_img = tf.clip_by_value(perturbed_img, clip_value_min=-1, clip_value_max=1.)[0]
      perturbed_images.append(perturbed_img)

  return tf.convert_to_tensor(perturbed_images)
perturbed_test_imgs = generate_perturbed_images(input_images=test_images, 
                                                image_label_idxs=test_labels, model=regular_model, 
                                                loss_func=scc, eps=0.05)<jupyter_output>100%|██████████| 10000/10000 [00:55<00:00, 178.69it/s]<jupyter_text>Evaluating the Current Model on the Perturbed Test Set<jupyter_code>predictions = regular_model.predict(perturbed_test_imgs)
predictions = tf.nn.softmax(predictions)
prediction_labels = np.argmax(predictions, axis=1)
print(classification_report(test_labels, prediction_labels, target_names=class_names))
pd.DataFrame(confusion_matrix(test_labels, prediction_labels), index=class_names, columns=class_names)<jupyter_output>precision    recall  f1-score   support

 T-shirt/top       0.85      0.27      0.41      1000
     Trouser       0.04      0.01      0.01      1000
    Pullover       0.00      0.00      0.00      1000
       Dress       0.46      0.38      0.41      1000
        Coat       0.00      0.00      0.00      1000
      Sandal       0.20      0.81      0.32      1000
       Shirt       0.00      0.00      0.00      1000
     Sneaker       0.24      0.96      0.38      1000
         Bag       0.04      0.01      0.02      1000
  Ankle boot       0.70      0.14      0.23      1000

    accuracy                           0.26     10000
   macro avg       0.25      0.26      0.18     10000
weighted avg       0.25      0.26      0.18     10000<jupyter_text>We see a performance drop from 88% to 26% when we introduced perturbation.   Create the Final Training Dataset: Pertubed + Original<jupyter_code>perturbed_train_imgs = generate_perturbed_images(input_images=train_images, 
                                                image_label_idxs=train_labels, model=regular_model, 
                                                loss_func=scc, eps=0.05)
train_dataset = tf.data.Dataset.from_tensor_slices(
    (train_images, train_labels)
)

adv_train_dataset = tf.data.Dataset.from_tensor_slices(
    (perturbed_train_imgs, train_labels)
)

final_dataset = adv_train_dataset.concatenate(train_dataset).shuffle(buffer_size=120000).batch(BATCH_SIZE)<jupyter_output><empty_output><jupyter_text>Loading the Pre-trained Weights of Discriminator<jupyter_code># Discriminator
# Reference: https://colab.research.google.com/github/sayakpaul/Image-Adversaries-101/blob/master/GANs_w_Adversaries.ipynb
def disc_network():
    disc = tf.keras.Sequential()
    inputShape = (28, 28, 1)

    disc.add(tf.keras.layers.Conv2D(32, (5, 5), padding="same", strides=(2, 2),
        input_shape=inputShape))
    disc.add(tf.keras.layers.LeakyReLU(alpha=0.2))

    disc.add(tf.keras.layers.Conv2D(64, (5, 5), padding="same", strides=(2, 2)))
    disc.add(tf.keras.layers.LeakyReLU(alpha=0.2))

    disc.add(tf.keras.layers.Flatten())
    disc.add(tf.keras.layers.Dense(512))
    disc.add(tf.keras.layers.LeakyReLU(alpha=0.2))
    disc.add(tf.keras.layers.Dropout(0.3))
    disc.add(tf.keras.layers.Dense(1))
    disc.add(tf.keras.layers.Activation("sigmoid"))
    
    return disc
# Get the discriminator weights (model training  was done in the notebook mentioned above)
!wget -q https://www.dropbox.com/s/n32doh46k72lw8r/disc_fashionmnist.h5
discriminator = disc_network()
discriminator.load_weights("disc_fashionmnist.h5")
discriminator.summary()<jupyter_output>Model: "sequential"
_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
conv2d_4 (Conv2D)            (None, 14, 14, 32)        832       
_________________________________________________________________
leaky_re_lu (LeakyReLU)      (None, 14, 14, 32)        0         
_________________________________________________________________
conv2d_5 (Conv2D)            (None, 7, 7, 64)          51264     
_________________________________________________________________
leaky_re_lu_1 (LeakyReLU)    (None, 7, 7, 64)          0         
_________________________________________________________________
flatten_2 (Flatten)          (None, 3136)              0         
_________________________________________________________________
dense_4 (Dense)              (None, 512)               1606144   
________________________________________________________[...]<jupyter_text>Create a Custom Model with the Pre-trained Discriminator Weights<jupyter_code>custom_model = tf.keras.Model(discriminator.input, discriminator.layers[-4].output)
custom_model.summary()
# First, we just warm up the training keeping the pre-trained weights
# of the discriminator to non-trainable
custom_model.trainable = False
inputs = tf.keras.layers.Input((28, 28, 1))
x = custom_model(inputs, training=False)
outputs = tf.keras.layers.Dense(10)(x)
model = tf.keras.Model(inputs, outputs)
model.summary()<jupyter_output>Model: "functional_9"
_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
input_6 (InputLayer)         [(None, 28, 28, 1)]       0         
_________________________________________________________________
functional_5 (Functional)    (None, 512)               1658240   
_________________________________________________________________
dense_8 (Dense)              (None, 10)                5130      
=================================================================
Total params: 1,663,370
Trainable params: 5,130
Non-trainable params: 1,658,240
_________________________________________________________________<jupyter_text>Training our Custom Model and Evaluation<jupyter_code>optimizer = tf.keras.optimizers.Adam(learning_rate=1e-3)
loss_fn = tf.keras.losses.SparseCategoricalCrossentropy(from_logits=True)

model.compile(loss=loss_fn, optimizer=optimizer, metrics=["accuracy"])
# Set up an early stopping callback to prevent overfitting
es = tf.keras.callbacks.EarlyStopping(monitor='val_loss', patience=5, restore_best_weights=True)

# We resume the training of our regular model
history = model.fit(final_dataset, 
                            validation_data=test_dataset, 
                            epochs=10,
                            callbacks=[es])
plot_progress(history)
predictions = model.predict(perturbed_test_imgs)
predictions = tf.nn.softmax(predictions)
prediction_labels = np.argmax(predictions, axis=1)
print(classification_report(test_labels, prediction_labels, target_names=class_names))
pd.DataFrame(confusion_matrix(test_labels, prediction_labels), index=class_names, columns=class_names)<jupyter_output>precision    recall  f1-score   support

 T-shirt/top       0.84      0.64      0.73      1000
     Trouser       0.98      0.95      0.97      1000
    Pullover       0.61      0.69      0.65      1000
       Dress       0.71      0.91      0.80      1000
        Coat       0.62      0.64      0.63      1000
      Sandal       0.97      0.90      0.93      1000
       Shirt       0.54      0.41      0.47      1000
     Sneaker       0.90      0.90      0.90      1000
         Bag       0.87      0.96      0.91      1000
  Ankle boot       0.90      0.96      0.93      1000

    accuracy                           0.80     10000
   macro avg       0.80      0.80      0.79     10000
weighted avg       0.80      0.80      0.79     10000<jupyter_text>Let's see if performance can be improved with further fine-tuning.<jupyter_code>custom_model.trainable = True
model.compile(loss=loss_fn, optimizer=optimizer, metrics=["accuracy"])
model.summary()
# Set up an early stopping callback to prevent overfitting
es = tf.keras.callbacks.EarlyStopping(monitor='val_loss', patience=5, restore_best_weights=True)

# We resume the training of our regular model
history = model.fit(final_dataset, 
                            validation_data=test_dataset, 
                            epochs=15,
                            callbacks=[es])
plot_progress(history)<jupyter_output>Epoch 1/15
1875/1875 [==============================] - 10s 5ms/step - loss: 4.7519 - accuracy: 0.8127 - val_loss: 2.7032 - val_accuracy: 0.8375
Epoch 2/15
1875/1875 [==============================] - 6s 3ms/step - loss: 1.2320 - accuracy: 0.8456 - val_loss: 1.8730 - val_accuracy: 0.8491
Epoch 3/15
1875/1875 [==============================] - 6s 3ms/step - loss: 0.9010 - accuracy: 0.8531 - val_loss: 1.3753 - val_accuracy: 0.8247
Epoch 4/15
1875/1875 [==============================] - 6s 3ms/step - loss: 0.7421 - accuracy: 0.8561 - val_loss: 0.9206 - val_accuracy: 0.8563
Epoch 5/15
1875/1875 [==============================] - 6s 3ms/step - loss: 0.6476 - accuracy: 0.8581 - val_loss: 0.8823 - val_accuracy: 0.8603
Epoch 6/15
1875/1875 [==============================] - 6s 3ms/step - loss: 0.5346 - accuracy: 0.8620 - val_loss: 0.9884 - val_accuracy: 0.8398
Epoch 7/15
1875/1875 [==============================] - 6s 3ms/step - loss: 0.5492 - accuracy: 0.8604 - val_loss: 0.9556 - val_accuracy[...]<jupyter_text>Notice that the training has stabilized quite a bit after unfreezing the pre-trained weights of the discriminator.<jupyter_code>predictions = model.predict(perturbed_test_imgs)
predictions = tf.nn.softmax(predictions)
prediction_labels = np.argmax(predictions, axis=1)
print(classification_report(test_labels, prediction_labels, target_names=class_names))
pd.DataFrame(confusion_matrix(test_labels, prediction_labels), index=class_names, columns=class_names)<jupyter_output>precision    recall  f1-score   support

 T-shirt/top       0.71      0.90      0.79      1000
     Trouser       0.96      0.97      0.96      1000
    Pullover       0.67      0.83      0.74      1000
       Dress       0.88      0.86      0.87      1000
        Coat       0.74      0.74      0.74      1000
      Sandal       0.92      0.98      0.95      1000
       Shirt       0.76      0.37      0.49      1000
     Sneaker       0.97      0.89      0.92      1000
         Bag       0.95      0.97      0.96      1000
  Ankle boot       0.94      0.97      0.95      1000

    accuracy                           0.85     10000
   macro avg       0.85      0.85      0.84     10000
weighted avg       0.85      0.85      0.84     10000
{'fraction_non_alphanumeric': 0.11742957746478873, 'fraction_numerical': 0.06889671361502347, 'mean_word_length': 2.9364749364749363, 'pattern_counts': {'":': 0, '<': 41, '<?xml version=': 0, '>': 40, 'https://': 3, 'lorem ipsum': 0, 'www.': 1, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 12, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '12', 'dir': 'jupyter-structured-clean-dedup', 'id': '137120', 'license': 'Apache-2.0', 'n_tokens_mistral': 6048, 'n_tokens_neox': 5332, 'n_words': 1146, 'path': 'tutorials/experiments/03_experiments_disc_finetuning.ipynb', 'repo_name': 'dipanjanS/adversarial-learning-robustness', 'size': '17040'}