[論文実装_その1] 畳み込みニューラルネットワークは何を見ているか

## 1. はじめに
　PyTorchの使い方にも少し慣れてきたので、arXivに公開されているディープラーニング関連の論文の実装にチャレンジをはじめました。まず今回は[Visualizing and understanding convolutional networks](https://arxiv.org/abs/1311.2901)を実装してみました。本論文はディープラーニングの可視化の文献としてよく参照されているので[^1]、以前から興味がありました。

　本論文の流れは、前半でCNNの可視化手法を中心に取り扱い、後半では構築したモデルのパフォーマンスを取り扱います。有名な論文ということもあり、Web上で日本語の解説も[こちら](https://qiita.com/knao124/items/fdb47674ada389e70c6e)に公開されています。

　本論文の概要に関しては上記リンクの解説の通りなので、本記事では上記解説であまり触れられていない提案された可視化手法の実装に重点を置いて解説してみようと思います。

## 2. 本論文の概要
　CNNは画像分類において高い精度を実現する一方、なぜそれが可能なのかがはっきりしていないという課題があります。本論文の前半では、中間層や分類器の動作を理解する手段として、**CNNの各中間層で入力画像の最も反応している部分を可視化する方法**が提案されています。

　CNNでは入力画像から特徴マップが生成されますが、本論文ではその逆のプロセスとして**任意の中間層の特徴マップから入力画像を復元する手法**を利用します。

　上記の手法では**特徴マップ全体が復元対象**ですが、ここではさらに工夫を加えます。復元したいCNNの各中間層の特徴マップに対して、**活性度が最も高いpixelはそのままにし、それ以外の全てのpixelはすべて0**となるように特徴マップを修正します。そしてこの修正された特徴マップに対して、上記手法を適用して入力画像の復元します。

　この復元された入力画像は、中間層で最も反応している部分（＝特徴マップにおいてもっとも大きな値を持つpixel）をもとに生成されています。つまり、これは**入力画像から対象の中間層でもっとも反応した部分のみを抽出すること**に相当しています。本プロセスを入力層に近い中間層から順に適用していくことで、**入力層側の浅い中間層から出力層側の深い中間層に進むにつれて、入力画像のどの部分に最も反応したか**を確認することができます。

　本論文では入力層に近い浅い中間層ほどコーナやエッジなど**具体的な形状**に強く反応する一方、出力層に近い深い中間層では犬やキーボードや人の顔などの**抽象的な形状**に反応していることが示されています。

　下図Fig.2(本論文から抜粋)は、入力層に近い浅い中間層のLayer1から出力層に近い深い中間層のLayer5まで上記プロセスを行った結果です。左側の画像が復元された入力画像、右側が元の入力画像になります。**深い層ほど抽象的なパターンが抽出**できていることがわかります。

![68747470733a2f2f71696974612d696d6167652d73746f72652e73332e616d617a6f6e6177732e636f6d2f302f33323738332f64393339373363342d323235392d626263632d323038372d3066653433313538343930612e706e67.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/246451/bf5225f3-1c79-7932-ec58-7fb2d69c1896.png)

## 3. 取り組んでみたこと
　次の4節では、中間層の特徴マップから入力画像を復元する提案手法で用いる**MaxUnpooling処理およびDeconvolution処理の概要**を記載しています。5節では本論文の提案手法を持ちいて**各中間層の最も活性度が高いpixelを元に入力画像を復元**してみました。どちらもPyTorchの充実したライブラリを利用することで極めて容易に実装できます。

## 4. MaxUnpooling処理およびDeconvolution処理
　中間層の特徴マップから入力画像を復元する提案手法は、下図のFig.1に概要が記載されています。入力画像にConvolution(畳み込み)処理→ReLU関数→MaxPooling処理を適用して得た特徴マップを入力として、**MaxUnPooling処理→ReLU関数→Deconvolution処理**を順に適用することで入力画像を再現しています。すなわち復元処理では**Convolution処理にはDeconvolution処理を、ReLU関数には同じReLU関数を、MaxPooling処理にはMaxUnpooling処理を対応**させています。

![68747470733a2f2f71696974612d696d6167652d73746f72652e73332e616d617a6f6e6177732e636f6d2f302f33323738332f61393663356239302d346435342d363066382d373137322d3537336164363931613435362e706e67.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/246451/a20d3a2b-09e8-413d-5b40-a7e02c9cf837.png)

　ここからは下の入力画像[^2]を元にMaxUnpooling処理およびDeconvolution処理を見てみます。
<img src=https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/246451/3cdf1356-c271-5f60-740b-520c31cb8c62.png               width=224>

### 4-1. MaxUnpooling処理
　MaxUnpooling処理の概要を下図に載せました。左の3x3の入力に対して、カーネルサイズ2x2・ストライド1・パディング0としてMaxPool処理を適用すると中心の2x2の出力が得られます。これにカーネルサイズ2x2・ストライド1・パディング0のMaxUnpooling処理を適用すると、右のような3x3の出力が得られます。

<img width="767" alt="スクリーンショット 2019-08-04 15.49.39.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/246451/18988c7e-5ace-8515-3d39-dae6e9543087.png">

　MaxUnpooling処理はMaxPool処理の逆転に相当する処理です。しかし**最大値以外の値は0となるため一部の情報は消失**しまいます。MaxUnpoolingはPyTorchには既に[MaxUnpool2d](https://pytorch.org/docs/stable/nn.html#maxunpool2d)として実装済みです。注意点としては、MaxUnpooling処理を行うには、**MaxPool処理の適用時に最大値が存在したインデックスをindicesとして取得**しておく必要があります。

　MaxUnpooling処理の動作を確認するために、**MaxPooling処理を行った画像にMaxUnpooling処理を行い元画像を復元**してみました。下に処理の概要を抜粋しました。全体のソースコードは[こちら](https://github.com/okn-yu/Visualizing-and-understanding-convolutional-networks/blob/master/test/test_maxunpooling.py)にあります。

```python
    # MaxUnpooling処理の抜粋

    # VGG16モデルの取得
    model = models.vgg16(pretrained=True).eval()
    # indicesを取得するためにreturn_indicesを設定
    for i, layer in enumerate(model.features):
        if isinstance(layer, torch.nn.MaxPool2d):
            layer.return_indices = True

    # VGG16のプーリング層の取得
    maxpooling_layer = model.features[4]
    # MaxPooling処理の実施およびindicesの取得
    maxpooling_result, indices = maxpooling_layer(input_img)
    # 入力画像→MaxPooling処理の可視化
    visualize(maxpooling_result)

    # MaxUnpooling用のパラメータの取得
    kernel_size = maxpooling_layer.kernel_size
    stride = maxpooling_layer.stride
    padding = maxpooling_layer.padding

    # MaxUnpooling層の作成
    unpooling_layer = torch.nn.MaxUnpool2d(kernel_size, stride, padding)
    # indicesを指定してMaxUnpooling処理の実施
    unpooling_result = unpooling_layer(maxpooling_result, indices)
    # 入力画像→MaxPooling処理→MaxUnpooling処理結果の可視化
    visualize(unpooling_result)
```

　下が**入力画像→MaxPooling処理**を適用した結果です。MaxPooling処理の最大値のみを取得するという動作のため、**全体的に画像が粗くなっている**のがわかります。
<img src=https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/246451/d49f13e0-2185-7809-67bd-9826924b3aa5.png               width=224>

　下が**入力画像→MaxPooling処理→MaxUnpooling処理**を続けて行い、入力画像を復元した結果です。**元画像の再現とならず画像に欠損が存在**しています。
![maxunpooling_img.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/246451/ec24855d-0079-bd52-3d03-7dafd15a50e6.png)

### 4-2. Deconvolution処理
　Deconvolutionの説明は[こちら](https://qiita.com/shngt/items/9c86e69e16ce6d61a0c6)や[こちら](https://techblog.nhn-techorus.com/archives/12879)にあります。DeconvolutionはPyTorchには既に[ConvTranspose2d](https://pytorch.org/docs/stable/nn.html#convtranspose2d)として実装済みです。CNNにおいて畳み込み処理(Convolution)を行うと、一般的に出力結果の画像の高さや幅は小さくなりチャネル数が増大するのですが、Deconvolution処理ではその逆になります。

　特に下のように特徴マップの生成に用いたConvolution処理と同じパラメータを用いてDeconvolution処理を行うことで、**特徴マップの入力画像と同じチャネル数と高さと幅を持つ画像を取得**できます。

　Deconvolution処理の動作を確認するために、**Convlution処理を行った画像にDeconvolution処理を行い元画像を復元**してみました。下に処理の概要を抜粋しました。全体のソースコードは[こちら](https://github.com/okn-yu/Visualizing-and-understanding-convolutional-networks/blob/master/test/test_deconvolution.py)にあります。


```python
    # Deconvolution処理の抜粋

    # VGG16モデルの取得
    model = models.vgg16(pretrained=True).eval()

    # VGG16の畳み込み層の取得
    conv_layer = model.features[0]
    # 畳み込み処理の実施
    conv_result = conv_layer(input_img)

    # Deconvolution用のパラメータの取得
    in_channels = conv_layer.out_channels
    out_channels = input_img.shape[1]    
    kernel_size = conv_layer.kernel_size
    stride = conv_layer.stride
    padding = conv_layer.padding

    # Deconvolution層の作成
    deconv_layer = torch.nn.ConvTranspose2d(in_channels, out_channels, kernel_size, stride, padding)
    deconv_layer.weight = conv_layer.weight
    # Deconvolution処理の実施
    deconv_result = deconv_layer(conv_result)

    # 入力画像→Convolution処理→Deconvolution処理結果の可視化
    visualize(deconv_result)
```

　MaxUnPooling処理と同じく、入力に対してConvolution処理→DeConvolution処理を続けた結果は、**元の入力の完全な再現とならず情報の欠落が発生**します。

　下が**入力画像→Convolution処理→Deconvolution処理**を適用して入力画像を復元した結果です。**全体的に画像が薄くなっている**のがわかります。

![deconvolution_img.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/246451/d7418d4f-983f-6c61-98ba-e76753aedd23.png)

## 5. 再現確認
　実際に本論文の提案手法を試してみました。ソースコードは[こちら](https://github.com/okn-yu/Visualizing-and-understanding-convolutional-networks/blob/master/main.py)にあります。今回の実装ではPyTorchの学習済みモデルのVGG-16を利用しました。VGG−16には下のように全部で5つのMaxPooling層があります。**各MaxPooling層経由後に特徴マップから元の入力画像を再現**してみました。

![vgg16.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/246451/547bb857-d5c9-32dc-909d-eb00fd51fbb9.png)

　入力画像は下のアメリカン・ショートヘアーの画像を利用しました。
<img src=https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/246451/ded7e6fc-0f4f-f5ac-ca6c-cdd5604598f1.jpeg width=224>

　入力画像の再現において、**A.特徴マップ全体を用いた入力画像の再現**と、**B.特徴マップ中から値が最大となるpixelのみを用いた入力画像を再現**を記載しています。以降の5-1から5-4において上の画像がAに下の画像がBに対応しています。

　AとBのどちらにおいても再現する層が深くなるほど、**MaxUnpooling処理やDeconvolution処理を行う回数が多くなるため再現された画像が不鮮明**となっています。また**CNNの各層において入力画像のどこに最も強く反応したのか**を意味する下側のBに着目すると5-3.では**目**に、5-4.では**耳**に、5-5.では**顔**に反応しているのがわかります。

　これらから**CNNの層が深くなるほど、入力画像においてより複雑で抽象度の高いパターンに強く反応している**ことが可視化されています。

### 5-1. 1つめのMaxPooling層経由後

![4th-layer.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/246451/1f8d690e-1c91-8a51-8d75-2e901736e784.png)

![4th-layer-filtered.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/246451/6e3c35f9-df3d-ab08-d988-0f4bb0c93258.png)

### 5-2. 2つめのMaxPooling層経由後

![9th-layer.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/246451/5b2bd9c0-ed9e-48e2-5ce6-32db0ebaf102.png)

![9th-layer-filtered.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/246451/769388c1-df6c-d22a-f332-03be31f543ed.png)

### 5-3. 3つめのMaxPooling層経由後

![16th-layer.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/246451/075c8060-5217-238d-8450-b95186c06a39.png)

![16th-layer-filtered.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/246451/b172cdf0-8851-ec86-bc31-d77f58f1a5f6.png)

### 5-4. 4つめのMaxPooling層経由後

![23th-layer.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/246451/170c4df2-f9c9-dabc-c72b-9bf70b78306e.png)

![23th-layer-filtered.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/246451/85f834b8-81df-b2f2-9b15-5265bf6c49a8.png)

### 5-5. 5つめのMaxPooling層経由後
![30th-layer.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/246451/f6c4cf19-08ac-2b0a-f6db-4abe0a155c21.png)

![30th-layer-filtered.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/246451/4642a8c5-3d36-9667-9c06-109f5fb60411.png)

[^1]: なんと本論文の被引用数は2019年7月18日時点で6550もあります！
[^2]: 飼い猫のノルウェイジャンです。我が家の長女。