{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## How Neural Network sees a Cat\n",
    "\n",
    "A neural network pre-trained on ImageNet is capable of recognizing any of 1000 different classes of objects, such as cats of different breeds. It would be interesting to see, what does the **ideal siamese cat** looks like for a neural network.\n",
    "\n",
    "> Of course, you can replace *siamese cat* with any other ImageNet class.\n",
    "\n",
    "To start, let's load VGG network:  "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "metadata": {},
   "outputs": [
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "2022-06-17 13:28:58.931467: I tensorflow/core/platform/cpu_feature_guard.cc:151] This TensorFlow binary is optimized with oneAPI Deep Neural Network Library (oneDNN) to use the following CPU instructions in performance-critical operations:  AVX2 FMA\n",
      "To enable them in other operations, rebuild TensorFlow with the appropriate compiler flags.\n",
      "2022-06-17 13:28:59.603736: I tensorflow/core/common_runtime/gpu/gpu_device.cc:1525] Created device /job:localhost/replica:0/task:0/device:GPU:0 with 15401 MB memory:  -> device: 0, name: Tesla P100-PCIE-16GB, pci bus id: 0001:00:00.0, compute capability: 6.0\n"
     ]
    }
   ],
   "source": [
    "import tensorflow as tf\n",
    "from tensorflow import keras\n",
    "import matplotlib.pyplot as plt\n",
    "import numpy as np\n",
    "from IPython.display import clear_output\n",
    "from PIL import Image\n",
    "import json\n",
    "np.set_printoptions(precision=3,suppress=True)\n",
    "\n",
    "model = keras.applications.VGG16(weights='imagenet',include_top=True)\n",
    "classes = json.loads(open('imagenet_classes.json','r').read())"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Optimizing for Result\n",
    "\n",
    "To visualize the ideal cat, we will start with a random noise image, and will try to use the gradient descent optimization technique to adjust the image to make a network recognize a cat.\n",
    "\n",
    "![Optimization Loop](images/ideal-cat-loop.png)\n",
    "\n",
    "Here is our starting image:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "<matplotlib.image.AxesImage at 0x7f90a0277ac0>"
      ]
     },
     "execution_count": 2,
     "metadata": {},
     "output_type": "execute_result"
    },
    {
     "data": {
      "image/png": "[encoded data, 193544 characters]\n",
      "text/plain": [
       "<Figure size 432x288 with 1 Axes>"
      ]
     },
     "metadata": {
      "needs_background": "light"
     },
     "output_type": "display_data"
    }
   ],
   "source": [
    "x = tf.Variable(tf.random.normal((1,224,224,3)))\n",
    "\n",
    "def normalize(img):\n",
    "    return (img-tf.reduce_min(img))/(tf.reduce_max(img)-tf.reduce_min(img))\n",
    "\n",
    "plt.imshow(normalize(x[0]))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "> We use `normalize` function to bring our values into 0-1 range.\n",
    "\n",
    "If we call our VGG network on this image, we will get more or less random distribution of probabilities:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "metadata": {},
   "outputs": [
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "2022-06-17 13:29:02.100818: I tensorflow/stream_executor/cuda/cuda_dnn.cc:368] Loaded cuDNN version 8303\n",
      "2022-06-17 13:29:02.570980: I tensorflow/core/platform/default/subprocess.cc:304] Start cannot spawn child process: No such file or directory\n"
     ]
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Predicted class: 669 (mosquito net)\n",
      "Probability of predicted class = 0.05466596782207489\n"
     ]
    },
    {
     "data": {
      "image/png": "[encoded data, 66652 characters]\n",
      "text/plain": [
       "<Figure size 1080x180 with 2 Axes>"
      ]
     },
     "metadata": {
      "needs_background": "light"
     },
     "output_type": "display_data"
    }
   ],
   "source": [
    "def plot_result(x):\n",
    "    res = model(x)[0]\n",
    "    cls = tf.argmax(res)\n",
    "    print(f\"Predicted class: {cls} ({classes[cls]})\")\n",
    "    print(f\"Probability of predicted class = {res[cls]}\")\n",
    "    fig,ax = plt.subplots(1,2,figsize=(15,2.5),gridspec_kw = { \"width_ratios\" : [1,5]} )\n",
    "    ax[0].imshow(normalize(x[0]))\n",
    "    ax[0].axis('off')\n",
    "    ax[1].bar(range(1000),res,width=3)\n",
    "    plt.show()\n",
    "\n",
    "plot_result(x)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "> Even though it may look like the probability of one of the classes is much higher than the others, it is still very low - look at the scale to see that actual probability is still around 5%.\n",
    "\n",
    "Now let's chose one target category (eg., **siamese cat**), and start adjusting the image using gradient descent. If $x$ is the input image, and $V$ is the VGG network, we will calculate the loss function $\\mathcal{L} = \\mathcal{L}(c,V(x))$ (where $c$ is the target category), and adjust $x$ using the following formula:\n",
    "$$\n",
    "x^{(i+1)} = x^{(i)} - \\eta{\\partial \\mathcal{L}\\over\\partial x}\n",
    "$$\n",
    "Loss function would be cross-entropy loss, because we are comparing two probability distributions. In our case, because the class is represented by a number, and not by one-hot encoded vector, we will use *sparse categorical cross-entropy*.\n",
    "\n",
    "We will repeat this process for several epochs, printing the image as we go.\n",
    "\n",
    "> It is better to execute this code on GPU-enabled compute, or reduce the number of epochs in order to minimize waiting time."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Epoch: 900, loss: 0.5220473408699036\n"
     ]
    },
    {
     "data": {
      "image/png": "[encoded data, 193416 characters]\n",
      "text/plain": [
       "<Figure size 432x288 with 1 Axes>"
      ]
     },
     "metadata": {
      "needs_background": "light"
     },
     "output_type": "display_data"
    }
   ],
   "source": [
    "target = [284] # Siamese cat\n",
    "\n",
    "def cross_entropy_loss(target,res):\n",
    "    return tf.reduce_mean(keras.metrics.sparse_categorical_crossentropy(target,res))\n",
    "\n",
    "def optimize(x,target,epochs=1000,show_every=None,loss_fn=cross_entropy_loss, eta=1.0):\n",
    "    if show_every is None:\n",
    "        show_every = epochs // 10\n",
    "    for i in range(epochs):\n",
    "        with tf.GradientTape() as t:\n",
    "            res = model(x)\n",
    "            loss = loss_fn(target,res)\n",
    "            grads = t.gradient(loss,x)\n",
    "            x.assign_sub(eta*grads)\n",
    "            if i%show_every == 0:\n",
    "                clear_output(wait=True)\n",
    "                print(f\"Epoch: {i}, loss: {loss}\")\n",
    "                plt.imshow(normalize(x[0]))\n",
    "                plt.show()\n",
    "\n",
    "optimize(x,target)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Predicted class: 284 (Siamese cat, Siamese)\n",
      "Probability of predicted class = 0.6449142098426819\n"
     ]
    },
    {
     "data": {
      "image/png": "[encoded data, 64232 characters]\n",
      "text/plain": [
       "<Figure size 1080x180 with 2 Axes>"
      ]
     },
     "metadata": {
      "needs_background": "light"
     },
     "output_type": "display_data"
    }
   ],
   "source": [
    "plot_result(x)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "We now have obtained an image that looks like a cat for a neural network, even though it still looks like a noise for us. If we optimize for a little bit longer - we are likeley to get the image of **ideal noisy cat**, which has probability close to 1.\n",
    "\n",
    "## Making Sense of Noise\n",
    "\n",
    "This noise does not make a lot of sense for us, but most probably it contains a lot of low-level filters that are typical for a cat. However, because there are very many ways to optimize input for the ideal result, the optimization algorithm is not motivated to find patterns that are visually comprehensible.\n",
    "\n",
    "To make this look a little bit less like a noise, we can introduce an additional term to the loss function - **variation loss**. It measures how similar neighboring pixels of the image are. If we add this term to our *loss function*, it will force the optimizer to find solutions with less noise, and thus having more recognizable details.\n",
    "\n",
    "> In practice, we need to balance between cross-entropy loss and variation loss to obtain good results. In our function, we introduce some numeric coefficients, and you can play with them and observe how image changes."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Epoch: 900, loss: [27.257]\n"
     ]
    },
    {
     "data": {
      "image/png": "[encoded data, 124576 characters]\n",
      "text/plain": [
       "<Figure size 432x288 with 1 Axes>"
      ]
     },
     "metadata": {
      "needs_background": "light"
     },
     "output_type": "display_data"
    }
   ],
   "source": [
    "def total_loss(target,res):\n",
    "    return 10*tf.reduce_mean(keras.metrics.sparse_categorical_crossentropy(target,res)) + \\\n",
    "           0.005*tf.image.total_variation(x,res)\n",
    "\n",
    "optimize(x,target,loss_fn=total_loss)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "This is the ideal image of a cat for our neural network, and we can also see some of the familiar features, such as eyes and ears. There are many of them, which makes neural network even more certain that this is a cat."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Predicted class: 284 (Siamese cat, Siamese)\n",
      "Probability of predicted class = 0.9201651215553284\n"
     ]
    },
    {
     "data": {
      "image/png": "[encoded data, 47552 characters]\n",
      "text/plain": [
       "<Figure size 1080x180 with 2 Axes>"
      ]
     },
     "metadata": {
      "needs_background": "light"
     },
     "output_type": "display_data"
    }
   ],
   "source": [
    "plot_result(x)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Let's also see how some other object looks like for the VGG:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Epoch: 900, loss: [29.59]\n"
     ]
    },
    {
     "data": {
      "image/png": "[encoded data, 120432 characters]\n",
      "text/plain": [
       "<Figure size 432x288 with 1 Axes>"
      ]
     },
     "metadata": {
      "needs_background": "light"
     },
     "output_type": "display_data"
    }
   ],
   "source": [
    "x = tf.Variable(tf.random.normal((1,224,224,3)))\n",
    "optimize(x,[340],loss_fn=total_loss) # zebra"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Adversarial Attacks\n",
    "\n",
    "Since *ideal cat* image can look like a random noise, it suggests that we can maybe tweak any image in a little way so that it changes it's class. Let's experiment with this a little bit. We will start with an image of a dog:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "<matplotlib.image.AxesImage at 0x7f8fd816e0a0>"
      ]
     },
     "execution_count": 9,
     "metadata": {},
     "output_type": "execute_result"
    },
    {
     "data": {
      "image/png": "[encoded data, 83124 characters]\n",
      "text/plain": [
       "<Figure size 432x288 with 1 Axes>"
      ]
     },
     "metadata": {
      "needs_background": "light"
     },
     "output_type": "display_data"
    }
   ],
   "source": [
    "img = Image.open('images/dog-from-unsplash.jpg')\n",
    "img = img.crop((200,20,600,420)).resize((224,224))\n",
    "img = np.array(img)\n",
    "plt.imshow(img)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "We can see that this image is clearly recognized as a dog:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 10,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Predicted class: 171 (Italian greyhound)\n",
      "Probability of predicted class = 0.9281901121139526\n"
     ]
    },
    {
     "data": {
      "image/png": "[encoded data, 39576 characters]\n",
      "text/plain": [
       "<Figure size 1080x180 with 2 Axes>"
      ]
     },
     "metadata": {
      "needs_background": "light"
     },
     "output_type": "display_data"
    }
   ],
   "source": [
    "plot_result(np.expand_dims(img,axis=0))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Now, we will use this image a starting point, and try to optimize it to become a cat:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 11,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Epoch: 90, loss: 0.15769274532794952\n"
     ]
    },
    {
     "data": {
      "image/png": "[encoded data, 110672 characters]\n",
      "text/plain": [
       "<Figure size 432x288 with 1 Axes>"
      ]
     },
     "metadata": {
      "needs_background": "light"
     },
     "output_type": "display_data"
    }
   ],
   "source": [
    "x = tf.Variable(np.expand_dims(img,axis=0).astype(np.float32)/255.0)\n",
    "optimize(x,target,epochs=100)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 12,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Predicted class: 284 (Siamese cat, Siamese)\n",
      "Probability of predicted class = 0.8651191592216492\n"
     ]
    },
    {
     "data": {
      "image/png": "[encoded data, 48388 characters]\n",
      "text/plain": [
       "<Figure size 1080x180 with 2 Axes>"
      ]
     },
     "metadata": {
      "needs_background": "light"
     },
     "output_type": "display_data"
    }
   ],
   "source": [
    "plot_result(x)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "So, this image above is a perfect cat, from the point of view of VGG network!\n",
    "\n",
    "## Experimenting with ResNet\n",
    "\n",
    "Let's now see how this same image is classified by a different model, say, ResNet:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 13,
   "metadata": {},
   "outputs": [],
   "source": [
    "model = keras.applications.ResNet50(weights='imagenet',include_top=True)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "> Since we used `model` as a global variable, from now on all functions will use ResNet instead of VGG"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 14,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Predicted class: 111 (nematode, nematode worm, roundworm)\n",
      "Probability of predicted class = 0.13089127838611603\n"
     ]
    },
    {
     "data": {
      "image/png": "[encoded data, 50572 characters]\n",
      "text/plain": [
       "<Figure size 1080x180 with 2 Axes>"
      ]
     },
     "metadata": {
      "needs_background": "light"
     },
     "output_type": "display_data"
    }
   ],
   "source": [
    "plot_result(x)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Apparenlty, the result is quite different. This is quite expected, because when optimizing for a cat we took into account the nature of VGG network, it's low-level filters, etc. Since ResNet has different filters, it gives different results. This gives us the idea of how we can protect ourselves from adversarial attacks - by using ensemble of different models.\n",
    "\n",
    "Let's see how the ideal zebra looks like for ResNet:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 15,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Epoch: 450, loss: [46.166]\n"
     ]
    },
    {
     "data": {
      "image/png": "[encoded data, 77328 characters]\n",
      "text/plain": [
       "<Figure size 432x288 with 1 Axes>"
      ]
     },
     "metadata": {
      "needs_background": "light"
     },
     "output_type": "display_data"
    }
   ],
   "source": [
    "x = tf.Variable(tf.random.normal((1,224,224,3)))\n",
    "optimize(x,target=[340],epochs=500,loss_fn=total_loss)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 16,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Predicted class: 340 (zebra)\n",
      "Probability of predicted class = 0.8876020312309265\n"
     ]
    },
    {
     "data": {
      "image/png": "[encoded data, 33320 characters]\n",
      "text/plain": [
       "<Figure size 1080x180 with 2 Axes>"
      ]
     },
     "metadata": {
      "needs_background": "light"
     },
     "output_type": "display_data"
    }
   ],
   "source": [
    "plot_result(x)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "This picture is quite different, which tells us that the architecture of a neural network probably plays quite an important role in the way it recognizes objects.\n",
    "\n",
    "> **Task**: Try to perform adversarial attach on ResNet, and compare the results.\n",
    "\n",
    "## Using Different Optimizers\n",
    "\n",
    "In our example, we have been using the simplest optimization technique - gradient descent. However, Keras framework contains different built-in [optimizers](https://www.tensorflow.org/api_docs/python/tf/keras/optimizers), and we can use them instead of gradient descent. This will require very little change to our code - we will replace the part where we adjust input image `x.assign_sub(eta*grads)` with a call to `apply_gradients` function of the optimizer:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 17,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Epoch: 900, loss: [41.451]\n"
     ]
    },
    {
     "data": {
      "image/png": "[encoded data, 68656 characters]\n",
      "text/plain": [
       "<Figure size 432x288 with 1 Axes>"
      ]
     },
     "metadata": {
      "needs_background": "light"
     },
     "output_type": "display_data"
    }
   ],
   "source": [
    "def optimize(x,target,epochs=1000,show_every=None,loss_fn=cross_entropy_loss,optimizer=keras.optimizers.SGD(learning_rate=1)):\n",
    "    if show_every is None:\n",
    "        show_every = epochs // 10\n",
    "    for i in range(epochs):\n",
    "        with tf.GradientTape() as t:\n",
    "            res = model(x)\n",
    "            loss = loss_fn(target,res)\n",
    "            grads = t.gradient(loss,x)\n",
    "            optimizer.apply_gradients([(grads,x)])\n",
    "            if i%show_every == 0:\n",
    "                clear_output(wait=True)\n",
    "                print(f\"Epoch: {i}, loss: {loss}\")\n",
    "                plt.imshow(normalize(x[0]))\n",
    "                plt.show()\n",
    "\n",
    "x = tf.Variable(tf.random.normal((1,224,224,3)))\n",
    "\n",
    "optimize(x,[898],loss_fn=total_loss) # water bottle"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Conclusion\n",
    "\n",
    "We were able to visualize the ideal image of a cat (as well as any other objects) within pre-trained CNN, using gradient descent optimization to adjust the input image instead of weights. The main trick to get the image that makes some sense was to use variation loss as an additional loss function, which enforces the image to look smoother. "
   ]
  }
 ],
 "metadata": {
  "interpreter": {
   "hash": "16af2a8bbb083ea23e5e41c7f5787656b2ce26968575d8763f2c4b17f9cd711f"
  },
  "kernelspec": {
   "display_name": "ai4beg",
   "language": "python",
   "name": "conda-env-ai4beg-py"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.8.12"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 4
}
