Learn to Pay Attentionを使ってみる

# 0. 概要
頃日流行りのDeepLearningのマルチタスクラーニングの一環で、あらゆるタスクに適用され始めているアテンション機構。
最初はRNNやLSTMの時系列データに対する重みづけであったが、最近では画像識別にも用いられている。
簡単な構造としてはSemantic Segmentationを行った後にClassificationを行うような構造のものが多い。

<img width="1063" alt="figure2.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/95636/083a4541-cdcc-ef29-df77-e41b2cf48e6f.png">

代表的な手法といえば、この`Learn to Pay Attention`だろうか。
https://arxiv.org/abs/1804.02391

今回は、これを動かしてみる。

# 1. Installation
Gitには4つのレポジトリが存在しているが、一先ずは以下のKeras版を動かしてみる。
https://github.com/DadianisBidza/LearnToPayAttention-Keras

``` bash
$ git clone https://github.com/DadianisBidza/LearnToPayAttention-Keras
```

README等は一切ないが、ソースコードを読んでいくとどうやらSVHN、CIFAR、CUB等のデータセットに対応しているようである。
今回はこの中でも一番難しそうなCUBデータセットを学習させたいと思う。

以下URLからCUBデータセットをダウンロードし、解凍する。
http://www.vision.caltech.edu/visipedia/CUB-200-2011.html

``` bash
$ wget http://www.vision.caltech.edu/visipedia-data/CUB-200-2011/CUB_200_2011.tgz
$ tar -xvzf CUB_200_2011.tgz
```

これで必要なデータは揃った。

# 2. Generating Dataset
どうやらSVHN、CIFAR10、CUB等を統一のNUMPYデータセット形式に書き換えてから使っているようである。
まず`CUB200_preprocess.py`を開いて以下のように書き直す。

``` python
import cv2
import numpy as np
import os

os.chdir("<YOUR PATH>/CUB_200_2011")
image = open('images.txt', 'r')
bbox = open('bounding_boxes.txt', 'r')

images = []
classes = []
i = 1
for line in image:
  # Extract bounding box conditions
  split = [int(float(x))for x in bbox.readline().split()]
  x, y, w, h = split[1], split[2], split[3], split[4]

  # Read and process image #line = image.readline()
  path = 'images/'+line.split()[1]
  classnumber = int(line.split('/')[0].split()[1].split('.')[0])
  img = cv2.imread(path)
  img = img[y: y+h, x: x+w]
  img = cv2.resize(img, (80,80))

  #Append to list
  classes.append(classnumber)
  images.append(img)
  if i % 500 == 0:
    print(i)
  i = i+1

#Convert to numpy array and save to file
images = np.array(images)
clases = np.array(classes)

os.chdir("<YOUR PATH>/LearnToPayAttention-Keras")
np.save("datasets/cubimgArr", images)
np.save("datasets/cubclassArr", classes)
```

これを保存したら実行する。

```bash
$ python CUB200_preprocess.py
```

これでGit cloneしたフォルダ直下に`datasets`フォルダが出来て、その中にCUB DatasetがNumpy形式に変換されて保存される。
なお、`<YOUR PATH>/CUB_200_2011`はダウンロードして解凍したCUB Datasetのフォルダを指している。

次に、これをTraining/Testingデータとして分割する。`cubdivide.py`を開き、以下のように編集する。

``` python
import numpy as np
import os

traintestsplit = open('<YOUR_PATH>/CUB_200_2011/train_test_split.txt', 'r')
os.chdir('datasets')
images = np.load('cubimgArr.npy')
labels = np.load('cubclassArr.npy')

xcub = np.empty([5994, 80, 80, 3])
ycub = np.empty([5994])

xcubtest = np.empty([5794, 80, 80, 3])
ycubtest = np.empty([5794])

traini=0
testi=0
i=0
for l in traintestsplit:
    if l.split()[1] == '1':
        xcub[traini] = images[i]
        ycub[traini] = labels[i]
        traini = traini+1
    else:
        xcubtest[testi] = images[i]
        ycubtest[testi] = labels[i]
        testi=testi+1
    i=i+1
np.save('xcub',xcub)
np.save('ycub',ycub)
np.save('xcubtest',xcubtest)
np.save('ycubtest',ycubtest)
```

これを保存したら実行する。

```bash
$ python cubdivide.py
```

ここまで終えれば、以下のようなファイル構造が出来上がっているはずである。

```
+ datasets/
  - cubclassArr.npy
  - cubimgArr.npy
  - xcub.npy
  - xcubtest.npy
  - ycub.npy
  - ycubtest.npy
```

# 3. Training
Trainingを行うにあたって、`Train.py`を実行するのであるが、初期状態だとエラーが沢山でる。
というのもデータセットにSVHNやCIFARがあることが前提だからである。
このため、そこらの処理を取り除いて`Train.py`を以下のようにする。

``` python
import numpy as np
import scipy.io as sio
from keras.datasets import cifar10
from keras.datasets import cifar100

xcub = np.load("datasets/xcub.npy")
xcubtest = np.load("datasets/xcubtest.npy")
ycub = np.load("datasets/ycub.npy")
ycubtest = np.load("datasets/ycubtest.npy")
vggatt2concatpcCUB = AttentionVGG(att='att2', height=80, width=80, outputclasses=200).StandardFit("cub2002011",xcub,ycub,True,beep=False, min_delta=0.01, validation_data=(xcubtest,ycubtest))
```

また学習のたびにBeep音を鳴らしてくれる`winsound`も使われているが、Linuxだと使えないので、全て削除する。
次に、学習モデルが書かれている`LearnToPayAttention.py`を以下のように書き換える。261行目～からである。

``` python
...

def StandardFit(self, datasetname=None, X=[], Y=[], transfer=False, beep=False, initial_lr=0.01, min_delta=None, patience=7, validation_data=None, lrplateaufactor=None, lrplateaupatience=4):
        Y = Y - 1 
        Y = keras.utils.to_categorical(Y,self.outputclasses)
        if datasetname==None:
            datasetname=self.datasetname
        if os.path.isfile("weights/"+self.name+"-"+datasetname+" early.hdf5"):
            print("Found early-stopped weights for "+self.name+"-"+datasetname)
            return
        scheduler = LearningRateScaler(25, 0.5, initial_lr)
        startingepoch = 0
        tboardcb = TensorBoard(log_dir='./logs', histogram_freq=0, batch_size=3, write_graph=True, write_grads=False, write_images=False, embeddings_freq=0, embeddings_layer_names=None, embeddings_metadata=None)
        checkpoint = ModelCheckpoint("weights/"+self.name+"-"+datasetname+" {epoch}.hdf5", save_weights_only=True)
        epochprint = LambdaCallback(on_epoch_end=lambda epoch, logs: print("Passed epoch "+str(epoch)))
        callbackslist = [scheduler, checkpoint, epochprint, tboardcb]
        if beep:
            callbackslist.append(Beeper(1))
        if validation_data == None:
            self.model.fit(X, Y, 128, 300, callbacks=callbackslist, initial_epoch=startingepoch,shuffle=True)
        else:
            if min_delta != None:
                callbackslist.append(EarlyStopping(monitor='val_acc', min_delta=min_delta, patience=patience))
            if lrplateaufactor != None:
                callbackslist.append(ReduceLROnPlateau(monitor='loss', factor = lrplateaufactor, patience = lrplateaupatience))
            self.model.fit(X, Y, 32, 100, callbacks=callbackslist, initial_epoch=startingepoch,shuffle=True,validation_data=(validation_data[0], keras.utils.to_categorical(validation_data[1] - 1,self.outputclasses)))
            self.model.save_weights("weights/"+self.name+"-"+datasetname+" early.hdf5")
        pastepochs = list(map(int, [x.replace(".hdf5", "").replace(self.name+"-"+datasetname, "").replace(" ", "") for x in os.listdir("weights") if (self.name+"-"+datasetname in x) & ("early" not in x)]))
        if max(pastepochs) > 290:
            for filenum in range(1,297):  #delete most of the lower weight files
                try:
                    os.remove("weights/"+self.name+"-"+datasetname+" "+str(filenum)+".hdf5")
                except OSError:
                    pass
        return self.model

...
```

主に、転移学習を行う部分、またクラス数のOne-hotベクトル化の行列数があっていない点等を修正した。
というか、元のソースコードはよくこれで動いていたな…本当に動いていたのか(?)

ここまできてようやく学習が出来る。

``` pthon
$ python Train.py
```

こんな形で学習が始まれば上手くいっている。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/95636/e6219fbb-43a6-bd5e-542a-d8e89df43ca4.png)

# 4. Prediction
学習は出来るものの識別機がないので、作る。
`Eval.py`を作り、以下のようにする。

``` python
from LearnToPayAttention import AttentionVGG,AttentionRN,StandardVGG
import numpy as np
import scipy.io as sio
from keras.datasets import cifar10
from keras.datasets import cifar100


xcub = np.load("datasets/xcub.npy")
xcubtest = np.load("datasets/xcubtest.npy")
ycub = np.load("datasets/ycub.npy")
ycubtest = np.load("datasets/ycubtest.npy")

AttentionVGG(att='att2', height=80, width=80, outputclasses=200).Test("cub2002011",xcub,ycub,"<YOUR_WEIGHT_PATH>")
```

次に`LearnToPayAttention`を開き300行目くらいに以下testメソッドを追加する。

``` python
    def Test(self, datasetname=None, X=[], Y=[], weight_name=""):
        Y = Y - 1
        Y = keras.utils.to_categorical(Y,self.outputclasses)
        if datasetname==None:
            datasetname=self.datasetname
        startingepoch = 0
        self.model.load_weights(weight_name)
        result = self.model.predict(X)
        print(result)
        return self.model
```

多少、雑ではあるが、これで識別結果が返ってくる。
もちろんSoftmaxなので200の行列である。このため、`np.argmax`でも使ってあげよう。

上手く行けば以下のようになる。

```
Generated (VGG-att2)-concat-pc
[[9.8632556e-01 6.2270486e-04 5.5064459e-04 ... 3.7251905e-06
  1.9230667e-08 3.4075290e-10]
 [9.5530415e-01 1.8338986e-05 5.5904808e-03 ... 1.2014980e-02
  1.8671205e-06 1.0310343e-08]
 [9.9984050e-01 4.6071386e-07 2.4222306e-06 ... 1.3283548e-07
  9.5381003e-10 1.5707118e-12]
 ...
 [2.4886667e-08 1.4843293e-05 7.2611817e-07 ... 1.2306998e-07
  5.0252122e-08 2.4842610e-08]
 [9.5263233e-12 1.1329794e-07 1.6467885e-08 ... 3.6465042e-10
  1.6405279e-08 6.7638486e-09]
 [2.0605858e-08 1.8327040e-04 1.4463135e-06 ... 4.2653479e-08
  2.4670322e-07 1.0294924e-09]]
[0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1
 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2
 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3]
[[1. 0. 0. ... 0. 0. 0.]
 [1. 0. 0. ... 0. 0. 0.]
 [1. 0. 0. ... 0. 0. 0.]
```

因みに、多少難しいが、ソースコードを変更してアテンションを表示するとこのようになる。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/95636/d3691695-dd75-2a10-033d-4bc41dd8894c.png)
元画像
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/95636/b8929328-d5af-918e-e318-80c2dfb66627.png)
アテンション画像


![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/95636/fca3241a-a8ee-9419-1527-efb1023c6b59.png)
元画像

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/95636/64f96f1d-c6eb-ad89-f844-c5b164f042f0.png)
アテンション画像

中々よいのでは！