| @@ -0,0 +1,460 @@ | |||
| { | |||
| "nbformat": 4, | |||
| "nbformat_minor": 0, | |||
| "metadata": { | |||
| "colab": { | |||
| "name": "hw10_adversarial_attack.ipynb", | |||
| "provenance": [], | |||
| "collapsed_sections": [], | |||
| "toc_visible": true | |||
| }, | |||
| "kernelspec": { | |||
| "name": "python3", | |||
| "display_name": "Python 3" | |||
| }, | |||
| "accelerator": "GPU" | |||
| }, | |||
| "cells": [ | |||
| { | |||
| "cell_type": "markdown", | |||
| "metadata": { | |||
| "id": "Q-n2e0BkhEKS" | |||
| }, | |||
| "source": [ | |||
| "# **Homework 10 - Adversarial Attack**\n", | |||
| "\n", | |||
| "Slides: https://reurl.cc/v5kXkk\n", | |||
| "\n", | |||
| "Videos:\n", | |||
| "\n", | |||
| "TA: ntu-ml-2021spring-ta@googlegroups.com" | |||
| ] | |||
| }, | |||
| { | |||
| "cell_type": "markdown", | |||
| "metadata": { | |||
| "id": "9RX7iRXrhMA_" | |||
| }, | |||
| "source": [ | |||
| "## Enviroment & Download\n", | |||
| "\n", | |||
| "We make use of [pytorchcv](https://pypi.org/project/pytorchcv/) to obtain CIFAR-10 pretrained model, so we need to set up the enviroment first. We also need to download the data (200 images) which we want to attack." | |||
| ] | |||
| }, | |||
| { | |||
| "cell_type": "code", | |||
| "metadata": { | |||
| "id": "d4Lw7urignqP" | |||
| }, | |||
| "source": [ | |||
| "# set up environment\n", | |||
| "!pip install pytorchcv\n", | |||
| "\n", | |||
| "# download\n", | |||
| "!gdown --id 1fHi1ko7wr80wXkXpqpqpOxuYH1mClXoX -O data.zip\n", | |||
| "\n", | |||
| "# unzip\n", | |||
| "!unzip ./data.zip\n", | |||
| "!rm ./data.zip" | |||
| ], | |||
| "execution_count": null, | |||
| "outputs": [] | |||
| }, | |||
| { | |||
| "cell_type": "markdown", | |||
| "metadata": { | |||
| "id": "hkQQf0l1hbBs" | |||
| }, | |||
| "source": [ | |||
| "## Global Settings\n", | |||
| "\n", | |||
| "* $\\epsilon$ is fixed to be 8. But on **Data section**, we will first apply transforms on raw pixel value (0-255 scale) **by ToTensor (to 0-1 scale)** and then **Normalize (subtract mean divide std)**. $\\epsilon$ should be set to $\\frac{8}{255 * std}$ during attack.\n", | |||
| "\n", | |||
| "* Explaination (optional)\n", | |||
| " * Denote the first pixel of original image as $p$, and the first pixel of adversarial image as $a$.\n", | |||
| " * The $\\epsilon$ constraints tell us $\\left| p-a \\right| <= 8$.\n", | |||
| " * ToTensor() can be seen as a function where $T(x) = x/255$.\n", | |||
| " * Normalize() can be seen as a function where $N(x) = (x-mean)/std$ where $mean$ and $std$ are constants.\n", | |||
| " * After applying ToTensor() and Normalize() on $p$ and $a$, the constraint becomes $\\left| N(T(p))-N(T(a)) \\right| = \\left| \\frac{\\frac{p}{255}-mean}{std}-\\frac{\\frac{a}{255}-mean}{std} \\right| = \\frac{1}{255 * std} \\left| p-a \\right| <= \\frac{8}{255 * std}.$\n", | |||
| " * So, we should set $\\epsilon$ to be $\\frac{8}{255 * std}$ after ToTensor() and Normalize()." | |||
| ] | |||
| }, | |||
| { | |||
| "cell_type": "code", | |||
| "metadata": { | |||
| "id": "ACghc_tsg2vE" | |||
| }, | |||
| "source": [ | |||
| "import torch\n", | |||
| "import torch.nn as nn\n", | |||
| "\n", | |||
| "device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')\n", | |||
| "\n", | |||
| "batch_size = 8\n", | |||
| "\n", | |||
| "# the mean and std are the calculated statistics from cifar_10 dataset\n", | |||
| "cifar_10_mean = (0.491, 0.482, 0.447) # mean for the three channels of cifar_10 images\n", | |||
| "cifar_10_std = (0.202, 0.199, 0.201) # std for the three channels of cifar_10 images\n", | |||
| "\n", | |||
| "# convert mean and std to 3-dimensional tensors for future operations\n", | |||
| "mean = torch.tensor(cifar_10_mean).to(device).view(3, 1, 1)\n", | |||
| "std = torch.tensor(cifar_10_std).to(device).view(3, 1, 1)\n", | |||
| "\n", | |||
| "epsilon = 8/255/std\n", | |||
| "# TODO: iterative fgsm attack\n", | |||
| "# alpha (step size) can be decided by yourself\n", | |||
| "alpha = 0.8/255/std\n", | |||
| "\n", | |||
| "root = './data' # directory for storing benign images\n", | |||
| "# benign images: images which do not contain adversarial perturbations\n", | |||
| "# adversarial images: images which include adversarial perturbations" | |||
| ], | |||
| "execution_count": null, | |||
| "outputs": [] | |||
| }, | |||
| { | |||
| "cell_type": "markdown", | |||
| "metadata": { | |||
| "id": "lhBJBAlKherZ" | |||
| }, | |||
| "source": [ | |||
| "## Data\n", | |||
| "\n", | |||
| "Construct dataset and dataloader from root directory. Note that we store the filename of each image for future usage." | |||
| ] | |||
| }, | |||
| { | |||
| "cell_type": "code", | |||
| "metadata": { | |||
| "id": "VXpRAHz0hkDt" | |||
| }, | |||
| "source": [ | |||
| "import os\n", | |||
| "import glob\n", | |||
| "import shutil\n", | |||
| "import numpy as np\n", | |||
| "from PIL import Image\n", | |||
| "from torchvision.transforms import transforms\n", | |||
| "from torch.utils.data import Dataset, DataLoader\n", | |||
| "\n", | |||
| "transform = transforms.Compose([\n", | |||
| " transforms.ToTensor(),\n", | |||
| " transforms.Normalize(cifar_10_mean, cifar_10_std)\n", | |||
| "])\n", | |||
| "\n", | |||
| "class AdvDataset(Dataset):\n", | |||
| " def __init__(self, data_dir, transform):\n", | |||
| " self.images = []\n", | |||
| " self.labels = []\n", | |||
| " self.names = []\n", | |||
| " '''\n", | |||
| " data_dir\n", | |||
| " ├── class_dir\n", | |||
| " │ ├── class1.png\n", | |||
| " │ ├── ...\n", | |||
| " │ ├── class20.png\n", | |||
| " '''\n", | |||
| " for i, class_dir in enumerate(sorted(glob.glob(f'{data_dir}/*'))):\n", | |||
| " images = sorted(glob.glob(f'{class_dir}/*'))\n", | |||
| " self.images += images\n", | |||
| " self.labels += ([i] * len(images))\n", | |||
| " self.names += [os.path.relpath(imgs, data_dir) for imgs in images]\n", | |||
| " self.transform = transform\n", | |||
| " def __getitem__(self, idx):\n", | |||
| " image = self.transform(Image.open(self.images[idx]))\n", | |||
| " label = self.labels[idx]\n", | |||
| " return image, label\n", | |||
| " def __getname__(self):\n", | |||
| " return self.names\n", | |||
| " def __len__(self):\n", | |||
| " return len(self.images)\n", | |||
| "\n", | |||
| "adv_set = AdvDataset(root, transform=transform)\n", | |||
| "adv_names = adv_set.__getname__()\n", | |||
| "adv_loader = DataLoader(adv_set, batch_size=batch_size, shuffle=False)\n", | |||
| "\n", | |||
| "print(f'number of images = {adv_set.__len__()}')" | |||
| ], | |||
| "execution_count": null, | |||
| "outputs": [] | |||
| }, | |||
| { | |||
| "cell_type": "markdown", | |||
| "metadata": { | |||
| "id": "LnszlTsYrTQZ" | |||
| }, | |||
| "source": [ | |||
| "## Utils -- Benign Images Evaluation" | |||
| ] | |||
| }, | |||
| { | |||
| "cell_type": "code", | |||
| "metadata": { | |||
| "id": "5c_zZLzkrceE" | |||
| }, | |||
| "source": [ | |||
| "# to evaluate the performance of model on benign images\n", | |||
| "def epoch_benign(model, loader, loss_fn):\n", | |||
| " model.eval()\n", | |||
| " train_acc, train_loss = 0.0, 0.0\n", | |||
| " for x, y in loader:\n", | |||
| " x, y = x.to(device), y.to(device)\n", | |||
| " yp = model(x)\n", | |||
| " loss = loss_fn(yp, y)\n", | |||
| " train_acc += (yp.argmax(dim=1) == y).sum().item()\n", | |||
| " train_loss += loss.item() * x.shape[0]\n", | |||
| " return train_acc / len(loader.dataset), train_loss / len(loader.dataset)" | |||
| ], | |||
| "execution_count": null, | |||
| "outputs": [] | |||
| }, | |||
| { | |||
| "cell_type": "markdown", | |||
| "metadata": { | |||
| "id": "_YJxK7YehqQy" | |||
| }, | |||
| "source": [ | |||
| "## Utils -- Attack Algorithm" | |||
| ] | |||
| }, | |||
| { | |||
| "cell_type": "code", | |||
| "metadata": { | |||
| "id": "F_1wKfKyhrQW" | |||
| }, | |||
| "source": [ | |||
| "# perform fgsm attack\n", | |||
| "def fgsm(model, x, y, loss_fn, epsilon=epsilon):\n", | |||
| " x_adv = x.detach().clone() # initialize x_adv as original benign image x\n", | |||
| " x_adv.requires_grad = True # need to obtain gradient of x_adv, thus set required grad\n", | |||
| " loss = loss_fn(model(x_adv), y) # calculate loss\n", | |||
| " loss.backward() # calculate gradient\n", | |||
| " # fgsm: use gradient ascent on x_adv to maximize loss\n", | |||
| " x_adv = x_adv + epsilon * x_adv.grad.detach().sign()\n", | |||
| " return x_adv\n", | |||
| "\n", | |||
| "# TODO: perform iterative fgsm attack\n", | |||
| "# set alpha as the step size in Global Settings section\n", | |||
| "# alpha and num_iter can be decided by yourself\n", | |||
| "def ifgsm(model, x, y, loss_fn, epsilon=epsilon, alpha=alpha, num_iter=20):\n", | |||
| " # initialize x_adv as original benign image x\n", | |||
| " # write a loop of num_iter to represent the iterative times\n", | |||
| " # for each loop\n", | |||
| " # call fgsm with (epsilon = alpha) to obtain new x_adv\n", | |||
| " # clip new x_adv back to [x-epsilon, x+epsilon]\n", | |||
| " # return x_adv\n", | |||
| " pass" | |||
| ], | |||
| "execution_count": null, | |||
| "outputs": [] | |||
| }, | |||
| { | |||
| "cell_type": "markdown", | |||
| "metadata": { | |||
| "id": "fYCEQwmcrmH6" | |||
| }, | |||
| "source": [ | |||
| "## Utils -- Attack\n", | |||
| "\n", | |||
| "* Recall\n", | |||
| " * ToTensor() can be seen as a function where $T(x) = x/255$.\n", | |||
| " * Normalize() can be seen as a function where $N(x) = (x-mean)/std$ where $mean$ and $std$ are constants.\n", | |||
| "\n", | |||
| "* Inverse function\n", | |||
| " * Inverse Normalize() can be seen as a function where $N^{-1}(x) = x*std+mean$ where $mean$ and $std$ are constants.\n", | |||
| " * Inverse ToTensor() can be seen as a function where $T^{-1}(x) = x*255$.\n", | |||
| "\n", | |||
| "* Special Noted\n", | |||
| " * ToTensor() will also convert the image from shape (height, width, channel) to shape (channel, height, width), so we also need to transpose the shape back to original shape.\n", | |||
| " * Since our dataloader samples a batch of data, what we need here is to transpose **(batch_size, channel, height, width)** back to **(batch_size, height, width, channel)** using np.transpose." | |||
| ] | |||
| }, | |||
| { | |||
| "cell_type": "code", | |||
| "metadata": { | |||
| "id": "w5X_9x-7ro_w" | |||
| }, | |||
| "source": [ | |||
| "# perform adversarial attack and generate adversarial examples\n", | |||
| "def gen_adv_examples(model, loader, attack, loss_fn):\n", | |||
| " model.eval()\n", | |||
| " adv_names = []\n", | |||
| " train_acc, train_loss = 0.0, 0.0\n", | |||
| " for i, (x, y) in enumerate(loader):\n", | |||
| " x, y = x.to(device), y.to(device)\n", | |||
| " x_adv = attack(model, x, y, loss_fn) # obtain adversarial examples\n", | |||
| " yp = model(x_adv)\n", | |||
| " loss = loss_fn(yp, y)\n", | |||
| " train_acc += (yp.argmax(dim=1) == y).sum().item()\n", | |||
| " train_loss += loss.item() * x.shape[0]\n", | |||
| " # store adversarial examples\n", | |||
| " adv_ex = ((x_adv) * std + mean).clamp(0, 1) # to 0-1 scale\n", | |||
| " adv_ex = (adv_ex * 255).clamp(0, 255) # 0-255 scale\n", | |||
| " adv_ex = adv_ex.detach().cpu().data.numpy().round() # round to remove decimal part\n", | |||
| " adv_ex = adv_ex.transpose((0, 2, 3, 1)) # transpose (bs, C, H, W) back to (bs, H, W, C)\n", | |||
| " adv_examples = adv_ex if i == 0 else np.r_[adv_examples, adv_ex]\n", | |||
| " return adv_examples, train_acc / len(loader.dataset), train_loss / len(loader.dataset)\n", | |||
| "\n", | |||
| "# create directory which stores adversarial examples\n", | |||
| "def create_dir(data_dir, adv_dir, adv_examples, adv_names):\n", | |||
| " if os.path.exists(adv_dir) is not True:\n", | |||
| " _ = shutil.copytree(data_dir, adv_dir)\n", | |||
| " for example, name in zip(adv_examples, adv_names):\n", | |||
| " im = Image.fromarray(example.astype(np.uint8)) # image pixel value should be unsigned int\n", | |||
| " im.save(os.path.join(adv_dir, name))" | |||
| ], | |||
| "execution_count": null, | |||
| "outputs": [] | |||
| }, | |||
| { | |||
| "cell_type": "markdown", | |||
| "metadata": { | |||
| "id": "r_pMkmPytX3k" | |||
| }, | |||
| "source": [ | |||
| "## Model / Loss Function\n", | |||
| "\n", | |||
| "Model list is available [here](https://github.com/osmr/imgclsmob/blob/master/pytorch/pytorchcv/model_provider.py). Please select models which has _cifar10 suffix. Some of the models cannot be accessed/loaded. You can safely skip them since TA's model will not use those kinds of models." | |||
| ] | |||
| }, | |||
| { | |||
| "cell_type": "code", | |||
| "metadata": { | |||
| "id": "jwto8xbPtYzQ" | |||
| }, | |||
| "source": [ | |||
| "from pytorchcv.model_provider import get_model as ptcv_get_model\n", | |||
| "\n", | |||
| "model = ptcv_get_model('resnet110_cifar10', pretrained=True).to(device)\n", | |||
| "loss_fn = nn.CrossEntropyLoss()\n", | |||
| "\n", | |||
| "benign_acc, benign_loss = epoch_benign(model, adv_loader, loss_fn)\n", | |||
| "print(f'benign_acc = {benign_acc:.5f}, benign_loss = {benign_loss:.5f}')" | |||
| ], | |||
| "execution_count": null, | |||
| "outputs": [] | |||
| }, | |||
| { | |||
| "cell_type": "markdown", | |||
| "metadata": { | |||
| "id": "uslb7GPchtMI" | |||
| }, | |||
| "source": [ | |||
| "## FGSM" | |||
| ] | |||
| }, | |||
| { | |||
| "cell_type": "code", | |||
| "metadata": { | |||
| "id": "wQwPTVUIhuTS" | |||
| }, | |||
| "source": [ | |||
| "adv_examples, fgsm_acc, fgsm_loss = gen_adv_examples(model, adv_loader, fgsm, loss_fn)\n", | |||
| "print(f'fgsm_acc = {fgsm_acc:.5f}, fgsm_loss = {fgsm_loss:.5f}')\n", | |||
| "\n", | |||
| "create_dir(root, 'fgsm', adv_examples, adv_names)" | |||
| ], | |||
| "execution_count": null, | |||
| "outputs": [] | |||
| }, | |||
| { | |||
| "cell_type": "markdown", | |||
| "metadata": { | |||
| "id": "WXw6p0A6shZm" | |||
| }, | |||
| "source": [ | |||
| "## I-FGSM" | |||
| ] | |||
| }, | |||
| { | |||
| "cell_type": "code", | |||
| "metadata": { | |||
| "id": "fUEsT06Iskt2" | |||
| }, | |||
| "source": [ | |||
| "# TODO: iterative fgsm attack\n", | |||
| "# adv_examples, ifgsm_acc, ifgsm_loss = gen_adv_examples(model, adv_loader, ifgsm, loss_fn)\n", | |||
| "# print(f'ifgsm_acc = {ifgsm_acc:.5f}, ifgsm_loss = {ifgsm_loss:.5f}')\n", | |||
| "\n", | |||
| "# create_dir(root, 'ifgsm', adv_examples, adv_names)" | |||
| ], | |||
| "execution_count": null, | |||
| "outputs": [] | |||
| }, | |||
| { | |||
| "cell_type": "markdown", | |||
| "metadata": { | |||
| "id": "DQ-nYkkYexEE" | |||
| }, | |||
| "source": [ | |||
| "## Compress the images" | |||
| ] | |||
| }, | |||
| { | |||
| "cell_type": "code", | |||
| "metadata": { | |||
| "id": "ItRo_S0M264N" | |||
| }, | |||
| "source": [ | |||
| "%cd fgsm\n", | |||
| "!tar zcvf ../fgsm.tgz *\n", | |||
| "%cd ..\n", | |||
| "\n", | |||
| "# %cd ifgsm\n", | |||
| "# !tar zcvf ../ifgsm.tgz *\n", | |||
| "# %cd .." | |||
| ], | |||
| "execution_count": null, | |||
| "outputs": [] | |||
| }, | |||
| { | |||
| "cell_type": "markdown", | |||
| "metadata": { | |||
| "id": "0FM_S886kFd8" | |||
| }, | |||
| "source": [ | |||
| "## Visualization" | |||
| ] | |||
| }, | |||
| { | |||
| "cell_type": "code", | |||
| "metadata": { | |||
| "id": "2FCuE2njkH1O" | |||
| }, | |||
| "source": [ | |||
| "import matplotlib.pyplot as plt\n", | |||
| "\n", | |||
| "classes = ['airplane', 'automobile', 'bird', 'cat', 'deer', 'dog', 'frog', 'horse', 'ship', 'truck']\n", | |||
| "\n", | |||
| "plt.figure(figsize=(10, 20))\n", | |||
| "cnt = 0\n", | |||
| "for i, cls_name in enumerate(classes):\n", | |||
| " path = f'{cls_name}/{cls_name}1.png'\n", | |||
| " # benign image\n", | |||
| " cnt += 1\n", | |||
| " plt.subplot(len(classes), 4, cnt)\n", | |||
| " im = Image.open(f'./data/{path}')\n", | |||
| " logit = model(transform(im).unsqueeze(0).to(device))[0]\n", | |||
| " predict = logit.argmax(-1).item()\n", | |||
| " prob = logit.softmax(-1)[predict].item()\n", | |||
| " plt.title(f'benign: {cls_name}1.png\\n{classes[predict]}: {prob:.2%}')\n", | |||
| " plt.axis('off')\n", | |||
| " plt.imshow(np.array(im))\n", | |||
| " # adversarial image\n", | |||
| " cnt += 1\n", | |||
| " plt.subplot(len(classes), 4, cnt)\n", | |||
| " im = Image.open(f'./fgsm/{path}')\n", | |||
| " logit = model(transform(im).unsqueeze(0).to(device))[0]\n", | |||
| " predict = logit.argmax(-1).item()\n", | |||
| " prob = logit.softmax(-1)[predict].item()\n", | |||
| " plt.title(f'adversarial: {cls_name}1.png\\n{classes[predict]}: {prob:.2%}')\n", | |||
| " plt.axis('off')\n", | |||
| " plt.imshow(np.array(im))\n", | |||
| "plt.tight_layout()\n", | |||
| "plt.show()" | |||
| ], | |||
| "execution_count": null, | |||
| "outputs": [] | |||
| } | |||
| ] | |||
| } | |||