ruby-dnnでディープラーニング

# はじめに
Ruby Advent Calendar 2019 4日目の記事です。 
今回は、Ruby用のディープラーニングライブラリ[「ruby-dnn」](https://github.com/unagiootoro/ruby-dnn)を紹介したいと思います。

# ruby-dnn
ruby-dnnは、Ruby用のディープラーニングライブラリです。
Python用の本格的なライブラリに比べると非常にシンプルですが、全結合ネットワーク、畳み込みネットワーク、再帰型ネットワークといった基本的な機能は実装されています。

特徴は、行列計算にNumo::NArrayを使用することで、全てのディープラーニングのアルゴリズムをRubyで実装していることです。そのため、可読性が高いコードになっている(はず)と思います。

# インストール
```
gem install ruby-dnn
```

# MNIST
機械学習のHello Worldこと、MNISTを用いた手書き数字の認識を通してruby-dnnの使い方を説明したいと思います。

## ライブラリの読み込み
```ruby
require "dnn"
require "dnn/datasets/mnist"
# require "numo/linalg/autoloader" # numo-linalgを入れると高速化できます。

# 名前空間のインクルード
include DNN::Models
include DNN::Layers
include DNN::Optimizers
include DNN::Losses
```

## データの前処理
```ruby
# MNISTデータの読み込み
x_train, y_train = DNN::MNIST.load_train
x_test, y_test = DNN::MNIST.load_test

# MNISTデータを[画像枚数, 28 * 28]の形状に変形する
x_train = x_train.reshape(x_train.shape[0], 784)
x_test = x_test.reshape(x_test.shape[0], 784)

# 画像データを0~1の範囲に正規化
x_train = Numo::SFloat.cast(x_train) / 255
x_test = Numo::SFloat.cast(x_test) / 255

# ラベルデータをone-hotベクトルに変換する
y_train = DNN::Utils.to_categorical(y_train, 10, Numo::SFloat)
y_test = DNN::Utils.to_categorical(y_test, 10, Numo::SFloat)
```

## モデルの作成
[784, 256, 256, 10]のシンプルな全結合ネットワークを作成します。

```ruby
# モデルの作成
model = Sequential.new

# 入力層を追加
model << InputLayer.new(784)

# 中間層を追加
model << Dense.new(256)
model << ReLU.new

# 中間層を追加
model << Dense.new(256)
model << ReLU.new

# 出力層を追加
model << Dense.new(10)

# モデルのセットアップ(最適化にAdamオプティマイザ、損失関数にSoftmaxCrossEntropyを使用)
model.setup(Adam.new, SoftmaxCrossEntropy.new)
```

## 学習開始
trainで学習を開始します。今回は10エポックをバッチサイズ128で学習します。

```ruby
model.train(x_train, y_train, 10, batch_size: 128, test: [x_test, y_test])

accuracy, loss = model.evaluate(x_test, y_test)
puts "loss: #{loss}"
puts "accuracy: #{accuracy}"
```

このプログラムを実行すると、次のような結果になります。
認識率は実行するたびに異なりますが、大体98%ぐらいになると思います。

```
【 epoch 1/10 】
========================================  60000/60000 loss: 0.1141  accuracy: 0.9604, test_loss: 0.1257
【 epoch 2/10 】
========================================  60000/60000 loss: 0.0660  accuracy: 0.9700, test_loss: 0.0920
【 epoch 3/10 】
========================================  60000/60000 loss: 0.0666  accuracy: 0.9736, test_loss: 0.0810
【 epoch 4/10 】
========================================  60000/60000 loss: 0.0441  accuracy: 0.9735, test_loss: 0.0799
【 epoch 5/10 】
========================================  60000/60000 loss: 0.0330  accuracy: 0.9797, test_loss: 0.0680
【 epoch 6/10 】
========================================  60000/60000 loss: 0.0415  accuracy: 0.9780, test_loss: 0.0760
【 epoch 7/10 】
========================================  60000/60000 loss: 0.0026  accuracy: 0.9805, test_loss: 0.0641
【 epoch 8/10 】
========================================  60000/60000 loss: 0.0468  accuracy: 0.9801, test_loss: 0.0707
【 epoch 9/10 】
========================================  60000/60000 loss: 0.0735  accuracy: 0.9796, test_loss: 0.0814
【 epoch 10/10 】
========================================  60000/60000 loss: 0.0396  accuracy: 0.9806, test_loss: 0.0780
loss: 0.9806
accuracy: 0.07885340588234832
```

## 学習結果の保存と読み込み
saveとloadを使用することで、学習したモデルの保存と読み込みができます。

```ruby
model.save("trained_mnist.marshal")
new_model = Sequential.load("trained_mnist.marshal")
accuracy, loss = new_model.evaluate(x_test, y_test)
puts "loss: #{loss}"
puts "accuracy: #{accuracy}"
```

# CIFAR10
次はMNISTから難易度を上げて、CIFAR10で動物や乗り物の画像認識にチャレンジします。
畳み込み層6 + 全結合層2の計8層のモデルを使用します。
このコードで学習させると、認識率は大体70%を超えるぐらいになると思います。

```ruby
require "dnn"
require "dnn/datasets/cifar10"
require "numo/linalg/autoloader"

include DNN::Models
include DNN::Layers
include DNN::Optimizers
include DNN::Losses

# CIFAR10データの読み込み
x_train, y_train = DNN::CIFAR10.load_train
x_test, y_test = DNN::CIFAR10.load_test

# 画像データを0~1の範囲に正規化
x_train = Numo::SFloat.cast(x_train) / 255
x_test = Numo::SFloat.cast(x_test) / 255

# ラベルデータをone-hotベクトルに変換する
y_train = DNN::Utils.to_categorical(y_train, 10, Numo::SFloat)
y_test = DNN::Utils.to_categorical(y_test, 10, Numo::SFloat)

# モデルの作成(畳み込み層6 + 全結合層2 = 計8層)
model = Sequential.new

model << InputLayer.new([32, 32, 3])

model << Conv2D.new(32, 3, padding: true)
model << Dropout.new(0.25)
model << ReLU.new

model << Conv2D.new(32, 3, padding: true)
model << BatchNormalization.new
model << ReLU.new
model << MaxPool2D.new(2)

model << Conv2D.new(64, 3, padding: true)
model << Dropout.new(0.25)
model << ReLU.new

model << Conv2D.new(64, 3, padding: true)
model << BatchNormalization.new
model << ReLU.new
model << MaxPool2D.new(2)

model << Conv2D.new(128, 3, padding: true)
model << Dropout.new(0.25)
model << ReLU.new

model << Conv2D.new(128, 3, padding: true)
model << BatchNormalization.new
model << ReLU.new

model << Flatten.new

model << Dense.new(512)
model << BatchNormalization.new
model << ReLU.new

model << Dense.new(10)

# モデルのセットアップ
model.setup(Adam.new, SoftmaxCrossEntropy.new)

# 学習を開始
model.train(x_train, y_train, 20, batch_size: 128, test: [x_test, y_test])

accuracy, loss = model.evaluate(x_test, y_test)
puts "loss: #{loss}"
puts "accuracy: #{accuracy}"

# 学習結果を保存
model.save("trained_cifar10.marshal")
```

# その他できること
## Chainer風のモデル定義
先ほどまで、モデルにレイヤーを追加する書き方をしましたが、Chainer風にモデルを定義することもできます。
この場合、define by runで実行することができるので、デバッグが楽になるメリットがあります。

```ruby
class MLP < Model
  def initialize
    super
    @l1 = Dense.new(256)
    @l2 = Dense.new(256)
    @l3 = Dense.new(10)
  end

  def forward(x)
    x = InputLayer.new(784).(x)
    x = @l1.(x)
    x = ReLU.(x)
    x = @l2.(x)
    x = ReLU.(x)
    x = @l3.(x)
    x
  end
end

model = MLP.new
model.setup(Adam.new, SoftmaxCrossEntropy.new)

model.train(x_train, y_train, 10, batch_size: 128, test: [x_test, y_test])
```

## 画像の読み込み/書き込み
ruby-dnnでは、C言語の画像処理ライブラリ「[stb_image](https://github.com/nothings/stb)」のラッパーが付属しており、それを使うことで簡単な画像の読み込み/書き込み/リサイズができます。

```ruby
require "dnn/image"

# 画像の読み込み
img = DNN::Image.read("image.png")
# 画像サイズを64*64に拡大
new_img = DNN::Image.resize(img, 64, 64)
# リサイズした画像を書き込み
DNN::Image.write("new_image.png", new_img)
```

# おわりに
ruby-dnnが実用的になる為には、まだまだ足りていない機能が数多くありますが、一応、今の時点でもDCGANで画像を作れるぐらいの性能はあります。そのため、Rubyでディープラーニングやってみたい人に試してもらえたら幸いです。