Google Colaboratory で Chainer を触ってみるvol.7 ~LeNet を嗜む~

# 前回
[Google Colaboratory で Chainer を触ってみるvol.6 ~optimizerの理解、いままでのまとめ~](https://qiita.com/4no3/items/46df75e2a16dfadf19d9) 

# 進捗
[00 Colaboratory で Chainer を動かしてみよう](https://chainer-colab-notebook.readthedocs.io/ja/latest/notebook/hands_on/chainer/begginers_hands_on/00_How_to_use_chainer_on_colaboratory.html)」の最後課題に挙げられていた LeNet-5 を試しました.

# LeNet-5 とは
LeNet5 とは, 5層の LeNet です. 3つの畳み込み(convolution)層と, 2つの全結合層を持つ, 計5層のネットワークです. 
![image.png](https://qiita-image-store.s3.amazonaws.com/0/284026/756640c9-8685-32d2-c786-ef726212d036.png)
(HandsOn のページから転載しました.)

LeNet は手書き数字認識を行うネットワークとして 1998 年に提案された, CNN の元祖です. 畳み込み層, プーリング層(要素を間引く層)を連続させ, 最後に全結合層を経て結果が出力されます. 昔は LeNet の活性化関数に sigmoid が使われていましたが, 最近では relu が用いられるそうです.

# LeNet-5 で Fashion MNIST をやってみる
サンプルコードを動かしつつ, 精度を 90% 以上にできないか試してみました. 使用したコードは, この記事の最後に記載しています.

* 精度 90% 以上
* 学習時間 200sec 以内

## 結果
### デフォルト設定(HandsOn 通りの設定)
* 活性化関数 : sigmoid
* epoch 数 : 10
* batchsize : 256
* optimizer : Adam

```
epoch       main/loss   main/accuracy  val/main/loss  val/main/accuracy  elapsed_time
1           1.83379     0.349968       1.16711        0.570508           3.91257       
2           0.98471     0.628926       0.89656        0.668945           7.99946       
3           0.810149    0.703085       0.777537       0.70498            11.9553       
4           0.711494    0.733          0.698332       0.732031           16.0126       
5           0.647929    0.74988        0.652298       0.74541            20.0139       
6           0.608307    0.76232        0.612736       0.754687           24.1201       
7           0.575744    0.772939       0.583578       0.767773           28.1152       
8           0.551792    0.78135        0.559339       0.773145           32.1578       
9           0.528406    0.790966       0.542836       0.788965           36.1562       
10          0.506318    0.800163       0.51899        0.789941           40.2234       

Test accuracy: 0.7987305
```
80% 行くか行かないか.

### 変更1 : batchsize
* 活性化関数 : sigmoid
* epoch 数 : 10
* batchsize : 128 ←
* optimizer : Adam

```
epoch       main/loss   main/accuracy  val/main/loss  val/main/accuracy  elapsed_time
1           1.63544     0.404412       0.946665       0.662579           7.17152       
2           0.779716    0.711297       0.703671       0.731013           14.6435       
3           0.640226    0.753405       0.620902       0.756527           22.077        
4           0.573759    0.775196       0.578544       0.768987           29.395        
5           0.530182    0.79144        0.529941       0.790645           36.7229       
6           0.497212    0.80621        0.50374        0.803699           44.0955       
7           0.47194     0.817915       0.479635       0.815071           51.6966       
8           0.450012    0.829187       0.458499       0.828422           59.1986       
9           0.43215     0.839494       0.447579       0.832872           66.6459       
10          0.416274    0.845908       0.430306       0.839695           74.0034       

Test accuracy: 0.84169924
```
batchsize を減らすといい感じで精度が上がりました.

### 変更2 : 活性化関数
* 活性化関数 : relu ←
* epoch 数 : 10
* batchsize : 256
* optimizer : Adam

```
epoch       main/loss   main/accuracy  val/main/loss  val/main/accuracy  elapsed_time
1           0.835276    0.696309       0.615126       0.760254           4.03892       
2           0.534279    0.79988        0.525212       0.798535           8.42688       
3           0.466187    0.83143        0.484503       0.814062           12.7898       
4           0.430691    0.844348       0.436197       0.837988           17.2373       
5           0.397923    0.857312       0.415125       0.844238           21.638        
6           0.379096    0.863762       0.383555       0.861328           26.017        
7           0.358667    0.871851       0.376659       0.859961           30.3771       
8           0.347551    0.8748         0.367091       0.866992           34.8929       
9           0.330936    0.880729       0.36706        0.864648           39.1901       
10          0.321007    0.883311       0.346736       0.873633           43.4323       

Test accuracy: 0.8751953
```
活性化関数を変えるだけで, だいぶ精度が上がりました.

### 変更3 : 活性化関数と batchsize
* 活性化関数 : relu ←
* epoch 数 : 10
* batchsize : 128 ←
* optimizer : Adam

```
epoch       main/loss   main/accuracy  val/main/loss  val/main/accuracy  elapsed_time
1           0.718779    0.736833       0.54409        0.797765           7.3917        
2           0.469592    0.829384       0.446577       0.839794           15.0238       
3           0.406688    0.853546       0.393267       0.857595           22.5937       
4           0.373509    0.866188       0.380327       0.862045           30.3102       
5           0.348192    0.874321       0.363642       0.86521            37.9552       
6           0.330103    0.879647       0.366197       0.865605           45.6231       
7           0.315076    0.88523        0.341243       0.87144            53.2682       
8           0.301359    0.890264       0.33667        0.876879           61.0204       
9           0.288146    0.89538        0.32718        0.878659           68.7567       
10          0.279945    0.897498       0.314154       0.88301            76.5967       

Test accuracy: 0.88427734
```
精度への影響は, やはり活性化関数が支配的ですね. さらにエポック数やバッチサイズを変えても, Test accuracy は 88% 以降ほぼ横ばいでした. 90% 以上の道は近そうで遠い...このあたりの最後のチューニングがノウハウなんでしょうかね.

# まとめ
LeNet は元祖 CNN. 精度を上げる最後のチューニングには, 何らかのノウハウが必要っぽい.

# 次回
[01 Chainerの基本的な使い方を学んでみよう](https://chainer-colab-notebook.readthedocs.io/ja/latest/notebook/hands_on/chainer/begginers_hands_on/01_Chainer_basic_tutorial.html)をやる.

# 参考：試したコード
```LeNet5.py
# colab の cuda に応じて、いい感じに chainer と Cupy をインストールするコマンド
!curl https://colab.chainer.org/install | sh -

# chainer のインストール確認
!python -c "import chainer; chainer.print_runtime_info()"

# graphviz のインストール
!apt -y -qq install graphviz > /dev/null 2> /dev/null
!pip install pydot

%matplotlib inline
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
import seaborn as sns
import chainer

# ラベル ID (label)からラベル名を取得する関数
LABEL_NAMES = [
    "T-shirt/top",
    "Trouser",
    "Pullover",
    "Dress",
    "Coat",
    "Sandal",
    "Shirt",
    "Sneaker",
    "Bag",
    "Ankle boot"
]

def get_label_name(label):
  return LABEL_NAMES[label]

# fashion MNIST のデータを取ってきて学習用, validation 用, test 用に分ける
from chainer.datasets.fashion_mnist import get_fashion_mnist

train, test = get_fashion_mnist(withlabel=True, ndim=1)
train, validation = chainer.datasets.split_dataset_random(train, 50000, seed=0)

# 学習
from chainer import optimizers, training
from chainer.training import extensions

def train_and_validate(
      model, optimizer, train, validation, n_epoch, batchsize, device):
  # 1.device が gpu なら、gpu にモデルデータを転送する
  if device >= 0:
    model.to_gpu(device)
  
  # 2. optimizer を設定する
  optimizer.setup(model)
  
  # 3. Dataset から Iterator を作成する
  train_iter = chainer.iterators.SerialIterator(train, batchsize)
  validation_iter = chainer.iterators.SerialIterator(
      validation, batchsize, repeat=False, shuffle=False)
  
  # 4. Updater, Trainer を作成する
  updater = training.StandardUpdater(train_iter, optimizer, device=device)
  trainer = chainer.training.Trainer(updater, (n_epoch, "epoch"), out="out")
  
  # 5. Trainer の機能を拡張する
  trainer.extend(extensions.LogReport())
  trainer.extend(extensions.Evaluator(validation_iter, model, device=device), name="val")
  trainer.extend(extensions.PrintReport(
      ["epoch", "main/loss", "main/accuracy", "val/main/loss", "val/main/accuracy", "elapsed_time"]))
  trainer.extend(extensions.PlotReport(
      ["main/loss", "val/main/loss"], x_key="epoch", file_name="loss.png"))
  trainer.extend(extensions.PlotReport(
      ["main/accuracy", "val/main/accuracy"], x_key="epoch", file_name="accuracy.png"))
  trainer.extend(extensions.dump_graph("main/loss"))
  
  # 6. 訓練を開始する
  trainer.run()

# グラフ表示
import pydot
from IPython.display import Image, display

def show_graph():
    graph = pydot.graph_from_dot_file('out/cg.dot') # load from .dot file
    graph[0].write_png('graph.png')
    display(Image('graph.png', width=600, height=600))

def show_loss_and_accuracy():
    display(Image(filename="out/loss.png"))
    display(Image(filename="out/accuracy.png"))

# test 結果計算
def show_test_performance(model, test, device, batchsize=256):
    if device >= 0:
        model.to_gpu()
    test_iter = chainer.iterators.SerialIterator(
        test, batchsize, repeat=False, shuffle=False
    )
    test_evaluator = extensions.Evaluator(test_iter, model, device=device)
    results = test_evaluator()
    print("Test accuracy:", results["main/accuracy"])

# LeNet-5
class LeNet5(Chain):
  def __init__(self):
    super(LeNet5, self).__init__()
    with self.init_scope():
      self.conv1 = L.Convolution2D(
          in_channels=1, out_channels=6, ksize=5, stride=1, pad=0)
      self.conv2 = L.Convolution2D(
          in_channels=6, out_channels=16, ksize=5, stride=1, pad=0)
      self.conv3 = L.Convolution2D(
          in_channels=16, out_channels=120, ksize=4, stride=1, pad=0)
      self.fc4 = L.Linear(None, 84)
      self.fc5 = L.Linear(84, 10)
 
  def forward(self, x):
#     h = F.sigmoid(self.conv1(x.reshape((-1, 1, 28, 28))))
#     h = F.max_pooling_2d(h, ksize=2, stride=2)
#     h = F.sigmoid(self.conv2(h))
#     h = F.max_pooling_2d(h, ksize=2, stride=2)
#     h = F.sigmoid(self.conv3(h))
#     h = F.sigmoid(self.fc4(h))

    h = F.relu(self.conv1(x.reshape((-1, 1, 28, 28))))
    h = F.max_pooling_2d(h, ksize=2, stride=2)
    h = F.relu(self.conv2(h))
    h = F.max_pooling_2d(h, ksize=2, stride=2)
    h = F.relu(self.conv3(h))
    h = F.relu(self.fc4(h))

    return self.fc5(h)

# 学習と test 実行
device = 0
n_epoch = 10
batchsize = 256

model = LeNet5()
classifier_model = L.Classifier(model)
optimizer = optimizers.Adam()
train_and_validate(
    classifier_model, optimizer, train, validation, n_epoch, batchsize, device)
show_test_performance(classifier_model, test, device)
show_loss_and_accuracy()
show_graph()
```