AdverTorchで騙されないモデルを作る

## はじめに
PyTorch Ecosystemの一つである[AdverTorch](https://github.com/BorealisAI/advertorch)を使って，Adversarial Examplesに対してRobustな画像分類機を作ってみようと思います．
installに始まり，FGSMを使ったAdversarial Trainingを行ない，学習方法と制度の関係についての実験まで，順を追って解説していこうと思います．
実装した全コードのリンクは以下です．

- [code](https://github.com/tanimutomo/advertorch_sample/blob/master/mnist_lenet.py)
- [notebook](https://github.com/tanimutomo/advertorch_sample/blob/master/mnist_lenet.ipynb)

## 目次
- AdverTorchとは
- Adversarial Examplesとは
- Install
- Tutorialのコードをベースとしてモデル構築
- 実験
- まとめ

## AdverTorchとは
[AdverTorch](https://github.com/BorealisAI/advertorch)とは，[PyTorch](https://pytorch.org/)ベースで作られているwrapperライブラリ，[PyTorchのEcosystem](https://pytorch.org/ecosystem)の一つで，Adversarial Examplesに特化したライブラリです．
他にもAdversarial Examplesのライブラリだと，他には[foolbox](https://github.com/bethgelab/foolbox)などが有名かと思います．

## Adversarial Examplesとは
知らない方のために，Adversarial Examplesについて，最初に少しだけ説明しておきます．（知っている方は，読み飛ばしていただいて構いません）

Deep Neural Networks (DNNs）はCVやNLPの様々なタスクで，高い精度を達成してきました．一方で，DNNsは特定のノイズがのった入力に対しては，高確率で誤った出力をしてしまうという脆弱性があることが明らかになってきました．これらの特定のノイズがのった入力がAdversarial Examplesと呼ばれています．このAdversarial Examplesの作成方法をAdversarial Attack Method，様々なAdversarial Examplesに対して頑健なモデルを作る方法をAdversarial Defencesと呼んだりします．

イメージを掴んでもらうために，以下の代表的な画像を使って説明しようと思います．
![image](https://research.preferred.jp/wp-content/uploads/2018/04/66394cbbab54c1bfe802897b4ee4b6f4.png)
これは，左の「パンダ」の画像に，微小なノイズを加えて，右の画像を作り出しているという意味です．人間の我々からすると，左も右もどちらも「パンダ」の画像に見えます．**しかし，モデルは右の画像を「テナガザル(gibbon)」として分類しています．**これが，「Adversarial Examples」と呼ばれるもので，モデルが誤った出力をしてしまうような入力ということになります．

そこで，気になるのが，ノイズとはなんなんだということだと思います．ここで，右の画像に加わっているノイズというのは，**Loss関数を最大化するような微小な摂動（ノイズのようなもの）**になっています．つまり，モデルが誤分類するように，デザインされた特殊なノイズです．

この特殊なノイズを作り，Adversarial Examplesを作りだすことをAttack，作り出されたAdversarial Examplesに対してモデルが誤分類しないように学習をモデルのアーキテクチャや学習方法等を工夫することをDefenceと呼んだりします．

そして，今回行うAdversarial Trainingというのは，Defence手法の一つでAdversarial Examplesに対して頑健なモデルを作るための学習方法です．仕組みは単純で，単に訓練データにAdversarial Examplesを混ぜるだけです．これによって，通常の画像もノイズが加わった画像も同じように分類することができるという方法です．
学習中に訓練データからのサンプルに対して，ノイズを計算してAdversarial Examplesを作成するという手順があるため，それを行うためのAttack方法を定義しておく必要はあります．

Adversarial Examplesは人間にはわからない形でモデルを騙せることから，顔による本人認証や，スマートスピーカー，自動運転での標識識別などの社会実装の際に問題となるため，解決すべき課題として，近年盛んに研究が行われています．
また，Adversarial Examplesの特性とDNNsの特性は密接に関係していることから，DNNｓの解釈という観点からも，多くの研究がなされています．

詳しく知りたい方は，[NeurIPS Tutorial](https://adversarial-ml-tutorial.org/introduction/)とかを読むことをお勧めします．

長々と説明してしまいましたが，ここから本題に入っていこうと思います．

## Install
### Environment
READMEに以下のように記載されていたため，pythoｎ3.6を使用し，pytorchのversionは1.0.0 or 0.4にしておきましょう．
可能であれば，仮想環境で行うことをお勧めします．
> We developed AdverTorch under Python 3.6 and PyTorch 1.0.0 & 0.4.1.

私は，pipenvを使ってinstallしてきますが，ここにはpipでinstallする場合のコードを記します

### Install dependencies
以下の3つのライブラリをinstallしていきます

- torch==1.0.0
- torchvision==0.2.2
- numpy

```
pip install torch==1.0.0 torchvision==0.2.2 numpy
```

### Install advertorch
[README](https://github.com/BorealisAI/advertorch)に書いてある通りの手順でinstallしていきます．

```
pip install advertorch
```
終わり．簡単ですね．


## Tutorialのコードをベースとしてモデル構築
advertorchのREADMEでも紹介されている[tutorial_train_mnist.py](https://github.com/BorealisAI/advertorch/blob/master/advertorch_examples/tutorial_train_mnist.py)を実験しやすいように改変して，jupyterで動かしてみようと思います．
完成形のnotebook or codeへのリンクは，本ページの上部に記載しております．

### import libraries
まず，必要なライブラリ郡をimportします

```python
import os
import argparse
import random

import torch
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim

from advertorch.attacks import GradientSignAttack
from advertorch.context import ctx_noparamgrad_and_eval
from advertorch.test_utils import LeNet5
from advertorch_examples.utils import get_mnist_train_loader
from advertorch_examples.utils import get_mnist_test_loader
from advertorch_examples.utils import TRAINED_MODEL_PATH
```

### Define variables
まず，実行の際のオプションとなる変数を定義していきます．

```python
seed = 0
mode = 'adv' # 'cln' or 'half' or 'adv'
train_bs = 50 # train batch size
test_bs = 1000 # test batch size
log_interval = 200
```

次に，device (cuda or cpu)と，modeに合わせて各変数を定義していきます．

```python
torch.manual_seed(seed)
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')

if mode == 'cln':
    nb_epoch = 10
    model_filename = 'mnist_lenet5_clntrained.pth'
elif mode == 'half':
    nb_epoch = 50
    model_filename = "mnist_lenet5_halftrained.pth"
elif mode == 'adv':
    nb_epoch = 50
    model_filename = "mnist_lenet5_advtrained.pth"
else:
    raise RuntimeError('mode must be "cls" or "adv"')
```

dataloaderとmodelの定義
このTutorialではdatasetとしてMNISTを使用して，modelはLeNetを使用します．
実行すると，dataのdownloadが始まって，`~/.advertorch/data/data/mnist`以下にdatasetが保存されると思います．

```python
train_loader = get_mnist_train_loader(
    batch_size=train_bs, shuffle=True)
test_loader = get_mnist_test_loader(
    batch_size=test_bs, shuffle=False)

model = LeNet5()
model.to(device)
optimizer = optim.Adam(model.parameters(), lr=1e-4)
```

ここまでは，通常のPyTorchでのコードと大差ないです．
最も大きな違いは，次のattack方法を定義するところです．

### Define Attack Method
元のTutorialのcodeでは$L_\infty$ノルムのPGDを使用していますが，今回はFGSMを使用します．
ここでModelとLossとParameterを渡し，学習中にこれらの情報を利用してAdversarial Examplesを計算していく．

```python
adversary = GradientSignAttack(
    model, loss_fn=nn.CrossEntropyLoss(reduction="sum"),
    eps=0.3, clip_min=0.0, clip_max=1.0, targeted=False)
```


### Model training
Attack方法を定義したところで，実際にRobustなModelを学習してみましょう

*Note: 以下の水平線で囲まれた領域は，インデントはそのままにして，同じセルに貼り付けてください（一つのfor文の中で動作させなければならないコードです）*

***
#### Simple epoch iteration

```python
for epoch in range(nb_epoch):
    model.train()
```

#### Model training iteration

```python
    for batch_idx, (data, target) in enumerate(train_loader):
        data, target = data.to(device), target.to(device)
        ori = data
        if mode == 'adv' or (mode == 'half' and random.random() > 0.5):
            # when performing attack, the model needs to be in eval mode
            # also the parameters should be accumulating gradients
            with ctx_noparamgrad_and_eval(model):
                data = adversary.perturb(data, target)

        optimizer.zero_grad()
        output = model(data)
        loss = F.cross_entropy(
            output, target, reduction='elementwise_mean')
        loss.backward()
        optimizer.step()
        if batch_idx % log_interval == 0:
            print('Train Epoch: {} [{}/{} ({:.0f}%)]\tLoss: {:.6f}'.format(
                epoch, batch_idx *
                len(data), len(train_loader.dataset),
                100. * batch_idx / len(train_loader), loss.item()))
```

これも基本的には，通常のPyTorchの学習と同じだが，一点だけ異なる点がある．それが`data = adversary.perturb(data, target)`の行．ここで，入力画像に対してAttackをしてAdversarial Examplesを作成している．これによって入力される`data`という変数がattackされたものに入れ替わる．


#### Model evaluation for original testset
```python

    model.eval()
    test_clnloss = 0
    clncorrect = 0

    test_advloss = 0
    advcorrect = 0

    for clndata, target in test_loader:
        clndata, target = clndata.to(device), target.to(device)
        with torch.no_grad():
            output = model(clndata)
        test_clnloss += F.cross_entropy(
            output, target, reduction='sum').item()
        pred = output.max(1, keepdim=True)[1]
        clncorrect += pred.eq(target.view_as(pred)).sum().item()

```
ここは通常のTestsetを利用したModelの評価と同じ．

#### Model evaluation for adversarial testset
```python
        advdata = adversary.perturb(clndata, target)
        with torch.no_grad():
            output = model(advdata)
        test_advloss += F.cross_entropy(
            output, target, reduction='sum').item()
        pred = output.max(1, keepdim=True)[1]
        advcorrect += pred.eq(target.view_as(pred)).sum().item()

```
Adversarial Exampleｓに対してのRobustさを測るために，テストセットに対してのAdversarial Attackを行い，それによって作成されたAdversarial Examplesに対しての精度を検証している．
学習時と同様に，テストセットの入力である`clndata`を変換した`advdata`を利用してmodelの性能を評価している．

#### Report loss and accuracy
```python
    test_clnloss /= len(test_loader.dataset)
    print('\nTest set: avg cln loss: {:.4f},'
          ' cln acc: {}/{} ({:.0f}%)\n'.format(
              test_clnloss, clncorrect, len(test_loader.dataset),
              100. * clncorrect / len(test_loader.dataset)))
    
    test_advloss /= len(test_loader.dataset)
    print('Test set: avg adv loss: {:.4f},'
          ' adv acc: {}/{} ({:.0f}%)\n'.format(
              test_advloss, advcorrect, len(test_loader.dataset),
              100. * advcorrect / len(test_loader.dataset)))
```
通常のテストセットに対しての結果と，Adversarial Attackされたテストセットに対してのLossと精度を出力

***
ここから別セルで大丈夫です．

### Saving the trained model (optional)
```python
torch.save(
    model.state_dict(),
    os.path.join(TRAINED_MODEL_PATH, model_filename))
```
最後に，作成したモデルを保存して終了．

## 実験
Adversarial Trainingを利用することで，モデルがどれほどAdversarial Examplesに対してRobustになるのかを検証する．
### 評価指標
- cln loss と cln acc
    - 通常のテストセットに対してのLossと精度
- adv loss と adv acc
Adversarial Examplesに変換されたテストセットに対してのLossと精度

### 学習方法
以下の3つの学習方法(mode)を試す．

- mode = 'cln'
    - 通常の学習
- mode = 'half'
    - 訓練データの半分をAdversarial Examplesに変えた学習
- ｍode = 'adv'
    - 訓練データの全てをAdversarial Examplesに変えた学習

### 結果
|mode|cln loss|cln acc|adv loss|adv acc|
|:--|--:|--:|--:|--:|
|cln|0.0325|99%|8.7803%|3%|
|half|0.0234|99%|0.0181|99%|
|adv|3.4272|68%|0.0094|100%|

### 考察
- cln: 通常の学習手法
    - 通常のテストセットに対しては，99%の精度を達成している．
    - 一方で，Adversarial Attackがされたテストセットに対しては，わずか3%しか正しく分類できていない．
    - MNISTは10クラス分類問題であるため，ランダムよりも精度が著しく低い．
- half: 半分をAdversarial Examplesに変換して学習
    - 通常のテストセットと，Adversarial Examplesのみのテストセットの両方に対して，99%の精度で分類できている．
- adv: 全てをAdversarial Examplesに変換して学習
    - Adversarial Examplesのみのテストセットに対しては，高い確率で分類できている．
    - 一方で，逆に通常のテストセットでは，分類性能が低下している．

以上から，通常の画像も正しく分類することができ，Adversarial Examplesに対してもRobustであるModelを構築するためには，0.5の確率で入力をAdversarial Examplesに変換して学習すると良い．（これが最適な解法ではない）


## まとめ
AdverTorchを使って，簡単にAdversarial Attackを実装して，RobustなModelを作ることができた．
一方で，新しい論文を実装する際などに，AdverTorchにどの程度の拡張性があるのかなどは，今後使っていって検証していきたい．