Google ColaboratoryでChainerを使ってCNNをやってみる (CIFAR-10の分類)

#はじめに
[Colaboratory](https://colab.research.google.com/notebooks/welcome.ipynb)とは、Jupyter Notebook環境で機械学習の勉強ができる研究ツールです。
何が嬉しいのかというと、色々ありますが、GPUが使えるところです!!!
これで深層学習の勉強をすぐに始めれます。また、環境構築とかも不要です！

今回作ったものは[こちら](https://github.com/hogezero/cifar10/blob/master/cifar10.ipynb)。
`cifar10.ipynb`を上から順序よく実行すると上手くできると思います。

#Chainerセットアップ
ColaboratoryでChainerGPUのセットアップは下の通りです。

> [Colaboratoryで10秒で起動できる、ChainerのGitHubレポジトリ作ってみた] (https://qiita.com/keisuke-umezawa/items/4e99c2a4f5f07567444c)

```python:cifar10.ipynb
# chainerセットアップ
# https://qiita.com/keisuke-umezawa/items/4e99c2a4f5f07567444c

# Install Chainer and CuPy!

!apt -y install libcusparse8.0 libnvrtc8.0 libnvtoolsext1
!ln -snf /usr/lib/x86_64-linux-gnu/libnvrtc-builtins.so.8.0 /usr/lib/x86_64-linux-gnu/libnvrtc-builtins.so
!pip install https://github.com/kmaehashi/chainer-colab/releases/download/2018-02-06/cupy_cuda80-4.0.0b3-cp36-cp36m-linux_x86_64.whl
!pip install 'chainer==4.0.0b3'
!apt-get install graphviz
!pip install 'chaineripy'
```
# 必要なパッケージをインポート


```python:cifar10.ipynb
import sys
import numpy as np
import matplotlib.pyplot as plt

# chainer
import numpy as np
import chainer
from chainer import training
from chainer import iterators, optimizers, serializers
import chainer.functions as F
import chainer.links as L
from chainer.training import extensions
from chainer.datasets import split_dataset
import pandas as pd
pd.options.display.precision = 3      # pandasの有効桁数を指定
```
ここでもGPUの確認をしましょう。

```python:cifar10.ipynb
print('GPU availability:', chainer.cuda.available)
print('cuDNN availablility:', chainer.cuda.cudnn_enabled)
```

出力は以下の通りになると思います。

```python:実行結果
GPU availability: True
cuDNN availablility: True
```

# データセット
## CIFAR-10
今回は、[CIFAR-10](https://www.cs.toronto.edu/~kriz/cifar.html)を使います。
このデータセットは、飛行機(airplane), 自動車(automobile), 鳥(bird), 猫(cat), 鹿(deer), 犬(dog), 蛙(frog), 馬(horse), 船(ship), トラック(truck)でラベル分けされています。
chainerでは簡単にCIFAR-10を取得できます。

```python:cifar10.ipynb
(train_original, test_original) = chainer.datasets.get_cifar10()
```

## データセット分割
ここでは`train_original`を分割して、
学習用データセット`train`と
検証用データセット`test`に分けます。
`test_original`は後の学習済みモデルを使用する時使います。

 ```python:cifar10.ipynb
train, test = split_dataset(train_original, int(len(train_original) * 0.8))
```
## ラベルの名前を準備 

```python:cifar10.ipynb
cifar10_labels = np.array([
    'airplane',
    'automobile',
    'bird',
    'cat',
    'deer',
    'dog',
    'frog',
    'horse',
    'ship',
    'truck'])
```

## 画像表示
せっかく画像を使っているので表示してみます。
表示は[こちら](http://labs.timedia.co.jp/2017/05/chainercifar-10-1.html)を参考にさせていただきました。

```python:cifar10.ipynb
plt.rcParams['figure.figsize'] = (12.0,12.0)
fig,ax = plt.subplots(nrows=3,ncols=10)
ax = ax.flatten()
for i in range(30):
    img = train[i][0]
    newimg = np.dstack((img[0],img[1],img[2]))
    ax[i].imshow(newimg,interpolation='none')
    ax[i].title.set_text(cifar10_labels[train[i][1]])
ax[0].set_xticks([])
ax[0].set_yticks([])
plt.tight_layout()
plt.show()
```

`実行結果`
![cifar10.png](https://qiita-image-store.s3.amazonaws.com/0/177560/9991978b-d01a-4de7-94cd-5c740c5cacdd.png)

## データセットの構成確認
データセットの詳細がわかないので確認します。

### 学習用データセット

```python:cifar10.ipynb
print('train total data : {:<5}'.format(len(train)))
print("------------------------")
train_category = []
for data, label_number in train:
    train_category.append(label_number)
  

for number, name in enumerate(cifar10_labels):
  num = train_category.count(number)
  print("{:<15} :{:>4}".format(name, num))
```

```python:実行結果
train total data : 40000
------------------------
airplane        :3986
automobile      :3986
bird            :4048
cat             :3984
deer            :4003
dog             :3975
frog            :4020
horse           :4023
ship            :3997
truck           :3978
```
### 検証用データセット

```python:cifar10.ipynb
print('test total data  : {:<5}'.format(len(test)))
print('------------------------')
test_category = []
for data, label_number in test:
    test_category.append(label_number)
  

for number, name in enumerate(cifar10_labels):
  num = test_category.count(number)
  print("{:<15} : {:>4}".format(name, num))
```

```python:実行結果
test total data  : 10000
------------------------
airplane        : 1014
automobile      : 1014
bird            :  952
cat             : 1016
deer            :  997
dog             : 1025
frog            :  980
horse           :  977
ship            : 1003
truck           : 1022
```
#　学習
## AlexNet
今回はAlexNetを使います。
[こちら](http://nocotan.github.io/chainer/2017/08/04/chainercnn-copy.html)を使用させていただきました。
AlexNetは2012年に画像認識コンテストで優勝したCNNです。

```python:cifar10.ipynb
class AlexNet(chainer.Chain):
    def __init__(self, num_class, train=True):
        super(AlexNet, self).__init__()
        with self.init_scope():
            self.conv1=L.Convolution2D(None, 96, 11, stride=2)
            self.conv2=L.Convolution2D(None, 256, 5, pad=2)
            self.conv3=L.Convolution2D(None, 384, 3, pad=1)
            self.conv4=L.Convolution2D(None, 384, 3, pad=1)
            self.conv5=L.Convolution2D(None, 256, 3, pad=1)
            self.fc6=L.Linear(None, 4096)
            self.fc7=L.Linear(None, 4096)
            self.fc8=L.Linear(None, num_class)

    def __call__(self, x):
        h = F.max_pooling_2d(F.local_response_normalization(
            F.relu(self.conv1(x))), 3, stride=2)
        h = F.max_pooling_2d(F.local_response_normalization(
            F.relu(self.conv2(h))), 3, stride=2)
        h = F.relu(self.conv3(h))
        h = F.relu(self.conv4(h))
        h = F.max_pooling_2d(F.relu(self.conv5(h)), 3, stride=2)
        h = F.dropout(F.relu(self.fc6(h)))
        h = F.dropout(F.relu(self.fc7(h)))
        h = self.fc8(h)

        return h
```

モデルを定義します。
`L.Classifier`では予測した値とラベルとの誤差を計算します。デフォルトでは、`softmax_cross_entropy`です。

```python:cifar10.ipynb
model = L.Classifier(AlexNet(len(cifar10_labels)))
```

## GPUの使用
GPUを使う場合は`gpu_id = 0`
CPUを使う場合は`gpu_id = -1`

```python:cifar10.ipynb
gpu_id = 0
if gpu_id >= 0:
  model.to_gpu(gpu_id)
```

## 最適化
```python:cifar10.ipynb
optimizer = optimizers.MomentumSGD()
optimizer.setup(model)
```
## イテレーション
`max_epoch`は最大のepoch数です。
ここを値を変更するとepoch数を変更でます。
`mytrigger`は`model`を保存する間隔を決めています。
iteratorsは学習ループです。

```python:cifar10.ipynb
batchsize = 20
max_epoch = 50
mytrigger = 10
train_iter = iterators.SerialIterator(train, batchsize)
test_iter  = iterators.SerialIterator(test, batchsize, False, False)
```
#Trainer
updaterに`train_iter`と`optimizer`を渡します。
`training.Trainer(updater, (max_epoch, 'epoch'), out='result')`でepoch数と結果保存ディレクトリを決めます。
結果は`result/`に出力されます。
`extensions.Evaluator(test_iter, model, device=gpu_id)`では、検証用データセットを評価します。
`extensions.dump_graph('main/loss', out_name="cg.dot")`では、ネットワークモデル図をdot言語で出力します。
`extensions.snapshot(), trigger=(mytrigger, 'epoch')`では、`mytrigger`で指定したepochごとに、snapshotを取ります。snapshotを呼び出せば、途中から学習が再スタートできます。
`extensions.snapshot_object(), trigger=(mytrigger, 'epoch')`では、`mytrigger`で指定したepochごとにmodelを保存します。
`extensions.LogReport()`では、logを取ります。
`extensions.PlotReport()`では、lossグラフとaccuracyグラフを`1epoch`ごとにpngで保存しています。
`extensions.PrintReport()`ではlossやaccuracyを出力しています。

```python:cifar10.ipynb
updater = training.StandardUpdater(train_iter, optimizer, device=gpu_id)
# iteration回数またはepochの回数を定義 #
trainer = training.Trainer(updater, (max_epoch, 'epoch'), out='result')
trainer.extend(extensions.Evaluator(test_iter, model, device=gpu_id))
trainer.extend(extensions.dump_graph('main/loss', out_name="cg.dot"))
# snapshotする間隔を決める #
trainer.extend(extensions.snapshot(filename='snapshot_iter_{.updater.epoch}'), trigger=(mytrigger, 'epoch'))
# modelの保存 #
trainer.extend(extensions.snapshot_object(model, 'model_snapshot_{.updater.epoch}', savefun=serializers.save_npz), trigger=(mytrigger, 'epoch'))
trainer.extend(extensions.LogReport())      # lossを 1 epochごとに出力
trainer.extend(extensions.PlotReport(['main/loss', 'validation/main/loss'], x_key='epoch', file_name='loss.png', marker=""))
trainer.extend(extensions.PlotReport(['main/accuracy', 'validation/main/accuracy'], x_key='epoch', file_name='accuracy.png', marker=""))
trainer.extend(extensions.PrintReport(['epoch', 'main/loss', 'main/accuracy', 'validation/main/loss', 'validation/main/accuracy', 'elapsed_time']))
```

## 学習開始

```python:cifar10.ipynb
trainer.run()
```

学習終了時にモデルを保存。

```python:cifar10.ipynb
print('save the model')
serializers.save_npz('./result/end.model', model)
```


# 各種グラフ表示
## lossグラフ

```python:cifar10.ipynb
from IPython.display import Image
Image('result/loss.png')
```

![loss.png](https://qiita-image-store.s3.amazonaws.com/0/177560/0e2e3dde-4f37-d6b4-2fe7-76af3261e88d.png)


## accuracyグラフ
```python:cifar10.ipynb
Image('result/accuracy.png')
```
![accuracy.png](https://qiita-image-store.s3.amazonaws.com/0/177560/5ffdb93b-9b65-3807-d6f2-7e66f99dbf7d.png)


## ネットワークモデル図

```python:cifar10.ipynb
!dot -Tpng result/cg.dot -o result/net.png
Image('result/net.png', width=1500, height=2000)
```
<img src=https://qiita-image-store.s3.amazonaws.com/0/177560/717e7b33-dc79-069c-87e9-305f033c9b73.png width=65%>

# テスト
## モデルの保存先を確認
```python:cifar10.ipynb
!ls result/
```

```python:実行結果
accuracy.png  loss.png		 model_snapshot_40  snapshot_iter_20
cg.dot	      model_snapshot_10  model_snapshot_50  snapshot_iter_30
end.model     model_snapshot_20  net.png	    snapshot_iter_40
log	      model_snapshot_30  snapshot_iter_10   snapshot_iter_50
```

## モデル呼び出し
```python:cifar10.ipynb
chainer.config.train = False
test_model =L.Classifier(AlexNet(len(cifar10_labels), train=False))
serializers.load_npz("./result/end.model", test_model)
```

## テスト
```python:cifar10.ipynb
test_num = 10000
test_label = []
pred = []
ok = []
no = []
for data, label in test_original[0:test_num][0:test_num]:
  test_label.append(label)
  redata = np.reshape(data, (1, 3, 32, 32))
  x = test_model.predictor(redata)
  cls = F.argmax(x)
  pred.append(cls.data.tolist())
  if(cls.data == label):
    ok.append(label)
  else:
    no.append(label)
```
## 正解数

```python:cifar10.ipynb
print('total ok data: {}'.format(len(ok)))
for number, name in enumerate(cifar10_labels):
  num = ok.count(number)
  print("{:<15} : {:>4}".format(name, num))
```

```python:実行結果
total ok data: 6736
airplane        :  705
automobile      :  757
bird            :  554
cat             :  508
deer            :  643
dog             :  547
frog            :  767
horse           :  733
ship            :  771
truck           :  751
```

## 不正解数
```python:cifar10.ipynb
print('total no data: {}'.format(len(no)))
for number, name in enumerate(cifar10_labels):
  num = no.count(number)
  print("{:<15} : {:>4}".format(name, num))
```

```python:実行結果
total no data: 3264
airplane        :  295
automobile      :  243
bird            :  446
cat             :  492
deer            :  357
dog             :  453
frog            :  233
horse           :  267
ship            :  229
truck           :  249
```

## 結果
結果を`DataFrame`に格納します。

```python:cifar10.ipynb
df_result = pd.DataFrame(index=[cifar10_labels], columns=['Correct', 'Incorrect', 'Accuracy','TotalData'])
for number, name in enumerate(cifar10_labels):
  df_result['Correct'][name] = ok.count(number)
  df_result['Incorrect'][name] = no.count(number)
  df_result['TotalData'][name] = total = ok.count(number) + no.count(number)
  df_result['Accuracy'][name] = accuracy = ok.count(number) / total
#合計のDataFrameを一番下の行に追加
df_result_total = pd.DataFrame(index=['Total'], columns=['Correct', 'Incorrect', 'Accuracy','TotalData'])
df_result_total['Correct'] = len(ok)
df_result_total['Incorrect'] = len(no)
df_result_total['Accuracy'] = len(ok) / (len(ok) + len(no))
df_result_total['TotalData'] = len(ok) + len(no)
df_result = pd.concat([df_result,df_result_total])
df_result
```

|  | Correct | Incorrect | Accuracy | TotalData |
|:--|--:|--:|--:|--:|
| (airplane,) | 705 | 295 | 0.705 | 1000 |
| (automobile,) | 757 | 243 | 0.757 | 1000 |
| (bird,) | 554 | 446 | 0.554 | 1000 |
| (cat,) | 508 | 492 | 0.508 | 1000 |
| (deer,) | 643 | 357 | 0.643 | 1000 |
| (dog,) | 547 | 453 | 0.547 | 1000 |
| (frog,) | 767 | 233 | 0.767 | 1000 |
| (horse,) | 733 | 267 | 0.733 | 1000 |
| (ship,) | 771 | 229 | 0.771 | 1000 |
| (truck,) | 751 | 249 | 0.751 | 1000 |
| Total | 6736 | 3264 | 0.674 | 10000 |




最後に`scikit-learn`を使って評価します。
`classification_report`というとても便利なものがありました。(知らなかった)

```python:cifar10.ipynb
from sklearn.metrics import classification_report
performance = classification_report(test_label, pred, target_names=list(cifar10_labels))
print(performance)
```

|  |precision|    recall | f1-score  | support|
|:--:|:-------:|:---------:|:---------:|:------:|
|airplane|       0.75|      0.70|      0.73|      1000|
|automobile|       0.87|      0.76|      0.81|      1000|
|       bird|       0.58|      0.55|      0.57|      1000|
|        cat|      0.46|      0.51|      0.48|      1000|
|       deer|       0.60|      0.64|      0.62|      1000|
|        dog|       0.58|      0.55|      0.56|      1000|
|       frog|       0.72|      0.77|      0.75|      1000|
|      horse|       0.71|      0.73|      0.72|      1000|
|       ship|       0.81|      0.77|      0.79|      1000|
|      truck|       0.71|      0.75|      0.73|      1000|
||
|avg / total|       0.68|      0.67|      0.68|     10000|

# おわりに
今回はGoogle Colaboratoryを使ってCNNをやってみました。
学習時間は`epoch 50`で65分ぐらいでした。
今後もColaboratoryを使って行こうと思います。
Colaboratory便利すぎる。
結果は検証用データセット・テスト用データセットに対して良くなかったので、他のネットワークモデルも試してみたいと思います。