Chainer v2 でSmoothGradを実装

# はじめに
画像認識において、識別器がどこを注目して認識したかを可視化することは重要です。
CNNでは、損失関数の勾配をバックプロパゲーションで入力画像まで伝搬し、その絶対値の強度を可視化する方法が提案されていますが、ノイズが多いという問題がありました。
SmoothGradでは、入力画像にガウシアンノイズを与え、複数の勾配を平均するだけで、きれいな可視化でできるという、とても簡単な方法です。


![cheetah.jpg](https://qiita-image-store.s3.amazonaws.com/0/137022/568a420b-f9db-cd08-bfbf-cb0153fe9ba7.jpeg)

平均化される様子
![cheetah.gif](https://qiita-image-store.s3.amazonaws.com/0/137022/77eec096-908d-ec62-852e-abe1720cfbbf.gif)


Smooth Grad
![MN099.jpg](https://qiita-image-store.s3.amazonaws.com/0/137022/0b330ef6-3de0-4c6a-096f-94a3626b81d6.jpeg)

Vanilla Grad（従来手法）
![VanillaMM.jpg](https://qiita-image-store.s3.amazonaws.com/0/137022/c9318dcc-e1c2-1589-d96f-e66a1d008035.jpeg)

TensorFlowのコードや論文は下記からダウンロードできるようです。
https://tensorflow.github.io/saliency/

Chainer v2の勉強を兼ねて、SmoothGradを実装してみました。
モデルには、学習済みのVGG16モデルを使っています。
環境は、
Windows7 64bit
Python 3.5.2 |Anaconda 4.2.0 (64-bit) 
chainerのversionは'2.0.0'です。
GPUには対応していません。


# 実装

## import
```python:smoothgrad.py
import chainer
import chainer.functions as F
from chainer.variable import Variable
from chainer.links import VGG16Layers
from PIL import Image
import numpy as np
import matplotlib.pyplot as plt
```

## config
テストモードで行いますが、バックプロパゲーションは行う必要があるので、
chainer.configを以下のように設定します。

```python:smoothgrad.py
chainer.config.train=False
chainer.config.enable_backprop=True
```
## VGG16モデルのロード
VGG16モデルをロードします。
モデルは500MB程度あるので、事前にダウンロードされていない場合は、それなりに時間がかかります。

```python:smoothgrad.py
model = VGG16Layers()
```

## 画像の読み込みと前処理
VGGは画像サイズが224x224なのでリサイズしておきます。

```python:smoothgrad.py
image = Image.open("cheetah.png")
image = image.resize((224,224))
```

## パラメータ
サンプリング数とノイズレベルを設定します。
サンプリング数は100、ノイズレベルは20%としています。

```python:smoothgrad.py
sampleSize = 100
noiseLevel = 0.2 # 20%
sigma = noiseLevel*255.0
```
## 勾配計算
使用メモリの関係上、今回は一枚ずつ行うことにします。
まず、VGG16ではチャネルの並びがBGRなので変換し、平均値を引きます。
次に画像のノイズを加えながら、順伝搬、ロスを計算し、逆伝搬することで勾配を計算します。
求めた勾配は、リストに追加しておきます。

```python:smoothgrad.py
gradList = []
for _ in range(sampleSize):
    x = np.asarray(image, dtype=np.float32)
    # RGB to BGR
    x = x[:,:,::-1]
    # 平均を引く
    x -= np.array([103.939, 116.779, 123.68], dtype=np.float32)
    x = x.transpose((2, 0, 1))
    x = x[np.newaxis]
    # ノイズを追加
    x += sigma*np.random.randn(x.shape[0],x.shape[1],x.shape[2],x.shape[3])    
    x = Variable(np.asarray(x))
    # FPして最終層を取り出す
    y = model(x, layers=['prob'])['prob']
    # 予測が最大のラベルでBP
    t = np.zeros((x.data.shape[0]),dtype=np.int32)
    t[:] = np.argmax(y.data)
    t = Variable(np.asarray(t))
    loss = F.softmax_cross_entropy(y,t)
    loss.backward()
    # 勾配をリストに追加
    grad = np.copy(x.grad)
    gradList.append(grad)
    # 勾配をクリア
    model.cleargrads()
```

## 可視化
勾配の各チャネルの絶対値の最大値を取り、画像に対して平均を取ります。

```python:smoothgrad.py
G = np.array(gradList)
M = np.mean(np.max(np.abs(G),axis=2),axis=0)
M = np.squeeze(M)
plt.imshow(M,"gray")
plt.show()
```
![M049.png](https://qiita-image-store.s3.amazonaws.com/0/137022/c1054ebe-0243-216f-d04c-9b04b6b100b7.png)


## 結果
平均する枚数を増やしながら、元画像とマップを画素ごとにかけて可視化してみました。

1サンプル
![MN000.jpg](https://qiita-image-store.s3.amazonaws.com/0/137022/ae76849e-edec-d05b-1354-dde86fb050fd.jpeg)

2サンプル
![MN001.jpg](https://qiita-image-store.s3.amazonaws.com/0/137022/41f77399-ed02-333b-650f-a8068af7ecb6.jpeg)

3サンプル
![MN003.jpg](https://qiita-image-store.s3.amazonaws.com/0/137022/6206a5a5-e291-ac1e-1c43-2540a62ace3b.jpeg)

10サンプル
![MN009.jpg](https://qiita-image-store.s3.amazonaws.com/0/137022/cb377310-f46a-987a-a945-c508b2c5a0d0.jpeg)

20サンプル
![MN019.jpg](https://qiita-image-store.s3.amazonaws.com/0/137022/058555f7-3e85-c5b5-ea26-bd2b1ef9df35.jpeg)

30サンプル
![MN029.jpg](https://qiita-image-store.s3.amazonaws.com/0/137022/65466b87-5ffd-79a0-1adc-ae43a7cef5c4.jpeg)

50サンプル
![MN049.jpg](https://qiita-image-store.s3.amazonaws.com/0/137022/bebec490-218f-8538-7bcd-7a7b052572e9.jpeg)

75サンプル
![MN074.jpg](https://qiita-image-store.s3.amazonaws.com/0/137022/9f731c94-3398-f7b3-1327-0599c4c57b95.jpeg)

100サンプル
![MN099.jpg](https://qiita-image-store.s3.amazonaws.com/0/137022/0b330ef6-3de0-4c6a-096f-94a3626b81d6.jpeg)

ノイズを与えずに、一枚のサンプルから可視化すると以下のようにノイズが多く見られます。
![VanillaMM.jpg](https://qiita-image-store.s3.amazonaws.com/0/137022/c9318dcc-e1c2-1589-d96f-e66a1d008035.jpeg)