「実践！ Chainrとロボットで学ぶディープラーニング」をシミュレーション環境で動かしてみた

## 概要
「実践！ Chainerとロボットで学ぶディープラーニング」には、実はシミュレーション環境が用意[[1][1]]されており、実機を購入しなくても教材の内容を楽しむことができます！ 今回は、そのシミュレーション環境でどんな事ができるか試してみました。

![画面収録-2019-11-29-17.54.44.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/111482/e92b5afe-0800-9aac-b26d-23a8abe591cf.gif)

## 「実践！ Chainerとロボットで学ぶディープラーニング」とは

「実践！ Chainerとロボットで学ぶディープラーニング」[[2][2]]は、Preferred Networks、株式会社アフレル、山梨大学が共同で開発した教材で、教育版レゴマインドストームEV3を使って、Pythonによるプログラミングから、オープンソースの深層学習フレームワークChainerまで学習することができる教材です。

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">【発表】プログラミング教材 「実践！Chainerとロボットで学ぶディープラーニング」をアフレルさんと共同開発しました。<br>深層学習の正しい知識の学習機会をより広く提供し、実務領域への活用を促進するため無料公開しています。高校・大学の授業や自主学習にご利用ください！

<a href="https://t.co/5OTEXWNRsu">https://t.co/5OTEXWNRsu</a></p>&mdash; Preferred Networks JP (@PreferredNetJP) <a href="https://twitter.com/PreferredNetJP/status/1181039123932430336?ref_src=twsrc%5Etfw">October 7, 2019</a></blockquote>
<script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

この教材自体は無料なのですが、動かすための実機を購入するとなると結構なお値段になります(税込 ¥78,980)。気軽に試すにはちょっとお高いお値段で私も購入するのをためらっていました。

しかし！！！実は以下のようなシミュレーション環境も教材には用意されています。

<img width="793" alt="スクリーンショット 2019-11-29 14.52.38.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/111482/909cb2ef-bce9-4441-56ed-3d170d6ff67e.png">

[pfnet-research/chainer-ev3](https://github.com/pfnet-research/chainer-ev3#%E3%82%B7%E3%83%9F%E3%83%A5%E3%83%AC%E3%83%BC%E3%82%BF%E3%83%BC%E3%81%AE%E7%92%B0%E5%A2%83%E6%A7%8B%E7%AF%89)

これを使えばサンプルを動かして、購入するか検討できますね。

## 環境設定
シミュレーション環境は[pfnet-research/chainer-ev3/document/simulator_setup.md][1]に用意されております。リンク先の手順に従って設定してみましょう。

## サンプル実行
### 実行
環境が設定できたら早速サンプルコードを動かしてみましょう。
`linetrace_controller1.ipynb`にルールベースでライントレースするnotebookが用意されています。ドキュメント[[1][1]]に書いてあるとおり、以下の手順でサンプルコードを実行してみましょう。

1. Jupyter Labの左側のリストから`linetrace_controller1.ipynb`を開く。

2. Jupyter Lab上で`linetrace_controller1.ipynb`を実行。

3. シミュレーターのウインドウ上で、`t`キーを押し、シミュレーションを開始。

すると以下のようにルールベースのライントレースのシミュレーションが動くはずです。

<img width="641" alt="スクリーンショット 2019-11-29 14.32.00.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/111482/1877662a-fe55-7b86-5707-bd07be61dc01.png">

シミュレーションを停止したいときは再度、`t`キーを押しましょう。

### コードの説明

```python
# タッチセンサーを押して離すとスタート.
ev3.lcd_draw_string('Push to start.', 0)
while not ev3.touch_sensor_is_pressed(touch_port):
    pass
while ev3.touch_sensor_is_pressed(touch_port):
    pass

ev3.lcd_draw_string('Go!', 0)
```

- ここを見ると、タッチセンサーが押されるまで後続の処理に進まないことがわかります。
- シミュレーターでは、`t`キーがタッチセンサーに相当するので、`t`キーを押すことで動き出すことがわかります。

```python
# 制御ループ
while True:
    # タッチセンサーが押されたら終了.
    if ev3.touch_sensor_is_pressed(touch_port):
        break
    # P制御でステアリング値を計算.
    white = 70  # 白面上の反射値.
    black = 5   # 黒面上の反射値.
    midpoint = (white + black) / 2  # 平均値を計算.
    color = ev3.color_sensor_get_reflect(color_port)
    steer = 1.5 * (color - midpoint)  # p * (測定値 - 平均値)
    # EV3へ制御値を送信.
    ev3.motor_steer(lmotor_port, rmotor_port, 10, int(steer))
```
- ここは、実際のライントレースの処理部分になります。
- ここで、`ev3.color_sensor_get_reflect(ポート番号)]`はカラーセンサーから反射光の値を取得する関数です。
    - 引数： 右表のポート番号を指定(今回は変数内に指定)
    - 戻り値： 反射光の値(0~100)
    - ここの値が大きいほど白いものがカラーセンサーに写っていることがわかります。
- なので、カラーセンサーに写っている色が黒いほど正の方向（左）、白いほど負の方向(右)に舵を切ることがわかります。

## Chainerを使った学習と推論
テキスト自体もこちら[pfnet-research/chainer-robotcar-text][3]で公開されています。現在の最新版は[chainer-robotcar-text-v1.1.0.pdf](https://github.com/pfnet-research/chainer-robotcar-text/blob/master/chainer-robotcar-text-v1.1.0.pdf)のようです。ここの手順にしたがって、Chainerをつかったライントレースをしてみます。

上記テキストのp.133から課題の説明がありますが、以下の順序で機械学習ベースのライントレースを行います。

1. 訓練データセットの作成
    - ルールベースのライントレースプログラムを動かすことで、カメラ画像(特徴量)と制御角度（ラベル）の組を作成する
2. モデルの訓練
    - 作成したデータセットを使用して、モデルを訓練します。
3. モデルによるライントレースの実行
    - 訓練したモデルを使用して、カメラ画像から制御角度を予測し、ｅｖ３を動かします。

### 訓練データセットの作成
機械学習を行うには入力に使う特徴量とそれの出力に対応するラベルが必要になります。ルールベースのライントレースプログラムを動かし、その時のカメラ画像(特徴量)と制御角度（ラベル）をロギングすることで訓練に使用するデータセットを作成します。

#### 実行
プログラムは`ml_linetrace_logger.ipynb`に用意されています。中身を見てみると、上記で説明した`linetrace_controller1.ipynb`とほとんど変わらないことがわかりますね。違いは以下です。

1. 白と黒の反射光の値をカラーセンサーから取得して定義していること
1. カメラ画像と制御角度を保存していること

大体３０００サンプルのデータが必要なので５周くらいさせてみましょう。データセットは`ml_linetrace_data/(日付)-(時刻)`ディレクトリ以下に保存されます。

### モデルの訓練
プログラムは`ml_linetrace_trainer.ipynb`に用意されています。実行する前に、`input_dir`を先程作成したデータセットの場所にしましょう。
だいたい、30epochほど訓練すると十分なようです。作成したモデルは`ml_linetrace_model/mychain.model`に保存されます。

```python
import os
import numpy as np
from PIL import Image
import chainer
from chainer.datasets import LabeledImageDataset
from chainer.datasets import TransformDataset
import chainer.links as L
import chainer.functions as F
from chainer import serializers
from chainer import training
from chainer.training import extensions


# Network definition
class MLP(chainer.Chain):

    def __init__(self):
        super(MLP, self).__init__()
        with self.init_scope():
            self.l1 = L.Linear(300, 256)  # 300(20*15) -> 256 units
            self.l2 = L.Linear(256, 256)  # 256 units -> 256 units
            self.l3 = L.Linear(256, 1)    # 256 units -> 1

    def forward(self, x):
        h1 = F.relu(self.l1(x))
        h2 = F.relu(self.l2(h1))
        return self.l3(h2)


# Preprocess data
def preprocess(in_data):
    img, label = in_data
    img = img / 256.  # normalization
    img = img.reshape(300)  # 2-dim (20,15) -> 1-dim (300)
    label = label.reshape(1)   # scalar -> 1-dim
    return img, label


input_dir = 'ml_linetrace_data/20191129-173344'
out_dir = 'ml_linetrace_model'
batchsize = 100
epoch = 30

# Set up a neural network to train
model = L.Classifier(MLP(),
                    lossfun=F.mean_squared_error,
                    accfun=F.mean_absolute_error)

# Setup an optimizer
optimizer = chainer.optimizers.Adam()
optimizer.setup(model)

# Load the dataset
dataset = LabeledImageDataset(os.path.join(input_dir, 'list.txt'),
                              os.path.join(input_dir, 'images'),
                              label_dtype=np.float32)

# Divide dataset into training and validation
threshold = np.int32(len(dataset) * 0.8)
train = TransformDataset(dataset[0:threshold], preprocess)
val = TransformDataset(dataset[threshold:], preprocess)

# Set iterators
train_iter = chainer.iterators.SerialIterator(train, batchsize)
val_iter = chainer.iterators.SerialIterator(val, batchsize,
                                            repeat=False, shuffle=False)

# Setup an Updater
updater = training.updaters.StandardUpdater(train_iter, optimizer)

# Setup a Trainer
trainer = training.Trainer(updater, (epoch, 'epoch'), out='result')
trainer.extend(extensions.LogReport())
trainer.extend(extensions.Evaluator(val_iter, model))
trainer.extend(extensions.PrintReport(['epoch', 'main/loss', 'main/accuracy', 'validation/main/loss', 'validation/main/accuracy', 'elapsed_time']))

# Run the Trainer
trainer.run()

# Save the model, the optimizer and config.                                                                                                                         
if not os.path.exists(out_dir):
    os.makedirs(out_dir)
print('save the model')
serializers.save_npz('{}/mlp.model'.format(out_dir), model.predictor)
print('save the optimizer')
serializers.save_npz('{}/mlp.state'.format(out_dir), optimizer)
```

### モデルによるライントレースの実行
プログラムは`ml_linetrace_controller.ipynb`に用意されています。`ml_linetrace_model/mychain.model`に保存されたモデルを使って実行します。
すると、こんな感じでちゃんと学習できているような様子が見られます。
![画面収録-2019-11-29-17.54.44.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/111482/e92b5afe-0800-9aac-b26d-23a8abe591cf.gif)

```python
import time
import numpy as np
from PIL import Image

import chainer
from chainer import configuration
import chainer.links as L
import chainer.functions as F
from chainer import serializers

from lib.ev3 import EV3
from lib.vstream import VideoStream


touch_port = EV3.PORT_2
lmotor_port = EV3.PORT_B
rmotor_port = EV3.PORT_C


# Network definition
class MLP(chainer.Chain):

    def __init__(self):
        super(MLP, self).__init__()
        with self.init_scope():
            self.l1 = L.Linear(300, 256)  # 300(20*15) -> 256 units
            self.l2 = L.Linear(256, 256)  # 256 units -> 256 units
            self.l3 = L.Linear(256, 1)      # 256 units -> 1

    def forward(self, x):
        h1 = F.relu(self.l1(x))
        h2 = F.relu(self.l2(h1))
        return self.l3(h2)


# Set up a neural network of trained model
predictor = MLP()

# Load the model
serializers.load_npz('ml_linetrace_model/mlp.model', predictor)

# Run VideoStream by setting image_size and fps
vs = VideoStream(resolution=(20, 15),
                 framerate=10,
                 colormode='binary').start()

ev3 = EV3()
ev3.enable_watchdog_task()
ev3.motor_config(lmotor_port, EV3.LARGE_MOTOR)
ev3.motor_config(rmotor_port, EV3.LARGE_MOTOR)
ev3.sensor_config(touch_port, EV3.TOUCH_SENSOR)

print("Push the touch sensor to start the linetracer")
while not ev3.touch_sensor_is_pressed(touch_port):
    pass

# Confirm the touch sensor is released.
while ev3.touch_sensor_is_pressed(touch_port):
    pass

# Enable evaluation mode for faster inference.
with configuration.using_config('train', False), chainer.using_config('enable_backprop', False):
    while True:
        # Break this loop when the touch sensor was pressed.
        if ev3.touch_sensor_is_pressed(touch_port):
            break
        im = vs.read()  # Get a current image in PIL format.
        im = np.asarray(im, dtype=np.float32)  # Convert to numpy array.
        x = im / 255.  # Normalization
        x = x.reshape(1, 300)  # (20, 15) -> (1, 300)
        y = predictor(x)  # Predict steer value from x.
        steer = y.data[0, 0]
        print("predicted steer = {}".format(steer))
        ev3.motor_steer(lmotor_port, rmotor_port, 10, int(steer))

vs.stop()
ev3.motor_steer(lmotor_port, rmotor_port, 0, 0)
ev3.close()
```

## おまけ
シミュレータで作ったモデルを実機で動かしたところ、無事動きました！

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">Chainerとシミュレーターを使ってモデルを作り、それを実機で動かしたバージョン <a href="https://t.co/hoCpQAeYZ4">pic.twitter.com/hoCpQAeYZ4</a></p>&mdash; くめざわ (Chainer Evangelist) (@kumezawa_) <a href="https://twitter.com/kumezawa_/status/1200665300061933568?ref_src=twsrc%5Etfw">November 30, 2019</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>


## Reference
- [1] [pfnet-research/chainer-ev3/document/simulator_setup.md][1]
- [2] [実践！ Chainerとロボットで学ぶ
ディープラーニング][2]
- [3] [pfnet-research/chainer-robotcar-text][3]

[1]:https://github.com/pfnet-research/chainer-ev3/blob/master/document/simulator_setup.md
[2]:https://ai.afrel.co.jp/chainer.html
[3]:https://github.com/pfnet-research/chainer-robotcar-text