Arraymancer + Cifar10

# Nim + Arraymancer + Cifar10

Nim Advent Calendar 18日目を担当します．

## はじめに
この記事では，Nimで使える深層学習フレームワーク[Arraymancer](https://github.com/mratsim/Arraymancer)を使ってCifar10の分類を行います．
私自身は深層学習に詳しいわけではないので，この記事では主に実装とその結果についてのみまとめます．

!! **2019/1/31 - 記事を更新しました** !!
Arraymancer-v0.5.0("Sign of the Unicorn")がリリースされたので，再度同様の実験を行いました．精度が向上し，他フレームワークと比較して遜色ない結果を得ています．

## Nimと深層学習フレームワーク
Nimから使える深層学習フレームワークは，2018年12月17日時点で確認したところ[Arraymancer](https://github.com/mratsim/Arraymancer)と[NimTorch](https://github.com/fragcolor-xyz/nimtorch)の2つが存在しています．NimTorchはまだまだ開発段階でMNISTデモすらありません．一方，Arraymancerはいくつかデモを備えており，工夫次第ではデモを超えて実用的な学習を行えるのではないかと感じました (とあるコンペにArraymancerで参加しようと思ったのですが，その前にちゃんと動くかの確認がしたかった) 
そこで，ArraymancerのデモにCifar10が無かったため，それを実装することにしました．

## 実装

以下のリポジトリに置きました．
https://github.com/cashiwamochi/nim_dl
Cifar10のダウンローダもLoss/Accのグラフ描画スクリプトも用意したので，是非いいモデルを探してみてください．


### Cifar10読み込み
PythonならCifar10の読み込みは適当なライブラリの関数を呼べば一発です．Nimにはもちろんそんなものは無いので，自分で書きます．Ciar10のバイナリの中身については[Cifar10公式](https://www.cs.toronto.edu/~kriz/cifar.html)が詳しいのでそちらを参照してください．
実装は簡単です．`readUint8()`を使ってバイナリを読み込み，ラベルは`cifar10_labels`へ，画像は`patchImageData`へと格納し`cifar10_image_patches`へ追加していきます．最後に`Tensor`に変換して，データローダは完成です．

```nim

type 
  cifar10 = tuple[
    train_images: Tensor[uint8],
    test_images: Tensor[uint8],
    train_labels: Tensor[uint8],
    test_labels: Tensor[uint8]
    ]

  cifar10Temp = tuple[
    images: Tensor[uint8],
    labels: Tensor[uint8]
    ]

  patchImageData = array[0 .. c-1, array[0 .. height-1, array[0 .. width-1, uint8]]]

 var
    cifar10_image_patches = newSeq[patchImageData](10000*len(file_names)) # patch-num * c * h * w
    cifar10_labels = newSeq[uint8](10000*len(file_names)) # batch * 1

```

### モデル定義
モデルの定義は以下の通り．書き方が独特ですが，簡潔です．簡潔すぎて複雑なネットワークをどう書くのか，やや気になりますが今回は放置します．

```nim
let
  ctx = newContext Tensor[float32] # Autograd/neural network graph
  n = 32                           # Batch size
  c = 3
  h = 32
  w = 32

network ctx, DemoNet:
  layers:
    x:          Input([c, h, w])
    cv1_1:      Conv2D(x.out_shape, 32, 3, 1)
    cv1_2:      Conv2D(cv1_1.outshape, 32, 3, 1)
    mp1:        MaxPool2D(cv1_2.outshape, (2,2), (0,0), (2,2))
    cv2_1:      Conv2D(mp1.out_shape, 64, 3, 1)
    cv2_2:      Conv2D(cv2_1.out_shape, 64, 3, 1)
    mp2:        MaxPool2D(cv2_2.outshape, (2,2), (0,0), (2,2))
    fl:         Flatten(mp2.out_shape)
    hidden:     Linear(fl.out_shape, 500)
    classifier: Linear(500, 10)
  forward x:
    x.cv1_1.relu.cv1_2.relu.mp1.cv2_1.relu.cv2_2.relu.mp2.fl.hidden.relu.classifier
```

### 学習
[ArraymancerのMNISTデモ](https://github.com/mratsim/Arraymancer/blob/master/examples/ex02_handwritten_digits_recognition.nim)とほぼ同じです．

```nim

let 
  model = ctx.init(DemoNet)
  optim = model.optimizerSGD(learning_rate = 0.001'f32)

# Learning loop
echo "|| Learning Start !"
for epoch in 0 ..< 1000:
  # Back-Propagation-Part
  var sum_train_loss = 0.0
  for batch_id in 0 ..< data.train_images.shape[0] div n:
    let 
      offset = batch_id * n
      batch_data_x = X_train[offset ..< offset+n]
      batch_data_y = y_train[offset ..< offset+n]
      clf = model.forward(batch_data_x)
      train_loss = clf.sparse_softmax_cross_entropy(batch_data_y)

    sum_train_loss = sum_train_loss + train_loss.value.data[0]

    train_loss.backprop()
    optim.update()

  # Validation
  ctx.no_grad_mode:

    var 
      score = 0.0
      val_loss = 0.0
    for i in 0 ..< 100:
      let y_pred = model.forward(X_test[i*100 ..< (i+1)*100]).value.softmax.argmax(axis = 1).squeeze
      score += accuracy_score(y_test[i*100 ..< (i+1)*100], y_pred)
      val_loss += model.forward(X_test[i*100 ..< (i+1)*100]).sparse_softmax_cross_entropy(y_test[i*100 ..< (i+1)*100]).value.data[0]
    score /= 100.0
    val_loss /= 100.0

    echo "||############## Epoch " & $epoch & " done. ##############"
    echo "|| Accuracy:     " & $(score * 100.0) & "%"
    echo "|| Val-Loss:     " & $val_loss
    echo "|| Train-Loss:   " & $(sum_train_loss / float(data.train_images.shape[0] div n))

```

## 結果
学習経過のグラフは以下のとおりです．Arraymancer v0.4.0での実験結果です．
執念で1000epoch回しました．最終的な精度はおよそ65％です．

![Figure_1.png](https://qiita-image-store.s3.amazonaws.com/0/324309/fe80959f-ea79-9554-c824-b6e832c2311a.png)

次に，Arraymancer v0.5.0での実験結果を紹介します．
前バージョンと比べ圧倒的に性能が向上し，わずか10epochで7割近い精度を発揮しています．正直，このモデルでなら他のフレームワークと比べても遜色ない結果だと思います．

![Figure_1.png](https://qiita-image-store.s3.amazonaws.com/0/324309/eb48f94d-4bfe-d3f0-dcf9-deaae5649f7f.png)


## 使ってみた感想
### v0.4.0
まず，期待した精度が出せませんでした．開発者が実装済みのMNISTデモのモデルでは全然駄目でした．他のライブラリのデモを真似てネットワークの構造を構築してもスコアが遠く及びません．色々と試行錯誤を繰り返して到達したのが現状です．今回の実装をArraymancerの開発者に報告したところ，「より深いネットワークを使うのが良いよ，あるいは学習済みモデルを使ってみるとか．もしかしたらバグがあるかもね！後で調べてみるよ」とのことでした．期待した精度を出せなかった原因が，モデルか，ハイパーパラメータか，Arraymancerにあるのか切り分けが出来てないのですが，今後は開発者の報告を待ちつついいモデルを探そうかなぁと思っています．

関数の充実具合を考慮しても，やはりまだ開発段階なので，これを携えてコンペに参加するとかいうのはちょっとむずかしそうです．~~コンペは参加できずに終了した~~

### v0.5.0
v0.4.0と比較して大きく改善されています．
行なった実験がCifar10のみなので，まだまだ知見が足りていませんが時間を見つけて試してみようと思います．

## 改めてNimと深層学習について
Nimコミュニティの開発者たちが深層学習にキャッチアップしようとしていることは，NimTorchやArraymancerから察しがつきます．しかし，やはりまだまだNimを使って深層学習で実際にあれこれやろうとは思えません．
深層学習ライブラリだけではなく，OpenCVのような便利なライブラリがNimから使えるようになることが重要だと感じています．もちろん，以下に挙げるような魅力的なライブラリも幾つかあるので，これらを使って手元でちょっとしたおもちゃを作ることは現状でも可能です．

* [neo](https://github.com/unicredit/neo)
* [nimPNG](https://github.com/jangko/nimPNG)
* [NimData](https://github.com/bluenote10/NimData)

線形代数ライブラリであるneoとPNG画像読み込みライブラリであるnimPNGがあればオプティカルフローの計算も出来たりするので思いつくところから作っていますが，自分の趣味が画像に偏っているのは理解しているので，もうちょっと別な側面からNimでなんかおもちゃをつくれないかなぁと思案し続けています．

## 終わりに
Nimで深層学習はまだ厳しいという否定的な見解を書いてしまいましたが，私自身は今後に期待しています．また先述したように，便利なライブラリはあるので現状でもある程度何かしらを作ることは可能ですし，これくらい道具が不足していたほうが勉強にも都合が良いというのもあります．Pythonは便利ですが，その分既存のツール群に甘えてしまうので，勉強のためにもNimに積極的に触れていこうと思っています．