Unity + Python + 箱庭でロボットを強化学習させてみよう！

# 概要
[TOPPERS/箱庭](https://toppers.github.io/hakoniwa/)では、様々なロボットがありますが、これらのロボットを強化学習できる環境を準備しました。

Python使って、Unity上のロボットの強化学習で試してみたいと思われる方にはお役に立てる環境と思います！

ちなみに、PythonからChatGPTのAPI使って、箱庭ロボットを動かすこともできます。

https://qiita.com/kanetugu2018/items/8bcaa2e4ceb464262c19

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/244147/7d03087d-c476-2317-3762-337a5b8132a4.png)

# ロボットの種類
箱庭で利用できるロボットとしては以下があります。

* 荷物運搬ロボット
* ETロボコン競技用ロボット(HackEV)
* EV3 ベースの電車モデル
* EV3 ベースの信号モデル
* TurtleBot3
  * TurtleBot3 Burger
  * TurtleBot3 waffle
* ドローンモデル

## 荷物運搬ロボット
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/244147/142f57fe-7a61-3d5f-52db-09c2504f9f4d.png)

本記事で、強化学習用に用意したもので、利用可能です。

## ETロボコン競技用ロボット(HackEV)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/244147/ba6bf663-eb7b-98ab-c9e5-bb6970d73870.png)

ET ロボコン競技用ロボットと同じモデルです。荷物運搬ロボットと同じ通信インタフェースですので、強化学習可能です。

## EV3 ベースの電車・信号モデル
こちらは、電車のレール上を走るロボットと信号用のロボットです。通信インタフェースはEV3ベースですので、これまでのものと同じです。


### 電車モデル
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/244147/74be844b-d744-7e27-c06f-edbb76c4212e.png)

### 信号モデル
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/244147/fcfc544b-bb5b-1558-1f03-0563cf63139e.png)

## TurtleBot3 
ROSで制御するロボットとして有名なロボットです。
通信インタフェースはROS2ベースです。

こちらのロボットになると、カメラデータやレーザスキャナがついているので、強化学習の対象の幅が広がりそうです。

解説記事は[こちら](https://qiita.com/kanetugu2018/items/c292ea92ef087bc5e68a)。

https://qiita.com/kanetugu2018/items/c292ea92ef087bc5e68a

### TurtleBot3 Burger
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/244147/5b0b4b57-fc36-b463-d42d-6969f5d6192f.png)

### TurtleBot3 Waffle
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/244147/849d201d-362a-38b8-128f-6baf0999aeb6.png)

## ドローンモデル

こちらは、簡単なドローンのモデルです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/244147/aa142f6a-9bf3-6181-dbe6-cc6651d17cb2.png)

https://qiita.com/kanetugu2018/items/cacbc0b3632e070fb777

# 前提とする環境
現時点では、以下の環境を想定しております。

# Unity
* Unity Hub
  * Unity Hub 3.4.1
* Unity
  * Unity 2021.3.7f1
* [Blender](https://www.blender.org/)
  * Blender v2.9.3以降

なお、Unityおよび Unity Hub, Blender はインストールされていることを前提として解説します。

## Windows環境
* Windows10 Home, Windows 11
* WSL2/WSLg/Ubuntu20.0.4
* WSL2/Docker Engine

## Mac環境
* Intel系 Mac：対応済み
* Arm系 Mac：対応済み
* Python：v3.10
 
## Linux環境

* Ubuntu: 対応済み
  * 導入手順は[こちら](https://qiita.com/Shintaro_Hosoai/items/4937f99be20baccbed4c#_reference-e5a100fcc88b9b793499)です！

https://qiita.com/Shintaro_Hosoai/items/4937f99be20baccbed4c#_reference-e5a100fcc88b9b793499

# 箱庭強化学習アーキテクチャ
箱庭上での強化学習のアーキテクチャは下図の通りです。
エージェント側をPythonプログラムで作成して、ロボットを強化学習します。
環境側は、「箱庭環境」です。箱庭環境に対して、エージェントが[アクション]を実行すると、アクション実行した結果として、[観測]と[報酬]が返ります。


![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/244147/0e2ee4cb-4950-1d13-2899-6d0b76b1f130.png)

このオペレーションを繰り返すことで強化学習ができます。なお、エージェント側と箱庭環境は、シミュレーション時間同期をしていますので、ここで学習したプログラムを実機ロボット用の開発言語(C言語等)に変換すれば、タイミングレベルでの精度が保証された学習済みプログラムを実機ロボットに適用できます（はずです）。

Windows版の実装レベルのアーキテクチャは下図の通りです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/244147/8285ff8c-726b-222c-52e1-d5242c62905d.png)


Mac／Linux版のアーキテクチャは下図の通りです。

![スクリーンショット 2023-02-05 19.14.47.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/244147/d72b5e9a-0d83-53cc-747a-76e7347de2bc.png)


以下、各コンポーネントの役割です。

* Pythonプログラム
  * 強化学習用のプログラムです
* 箱庭コンダクター
  * 箱庭アセット管理および箱庭アセット間のプロキシ通信を行います
  * Windows版では、Unityと箱庭コンダクターは UDP/gRPC 通信で接続していますが、
  * Linux/Mac版では、箱庭コア機能と関数コールさせることで通信オーバーヘッドを低減させています。
* 箱庭アセット(Unity)
  * Unity上のロボットのシミュレーションを実現します
  * Unity上のロボット配置やロボット周辺環境をUnity機能を利用して自由に変更できます



# インストール手順

OS 毎に若干インストール手順が異なりますので、以下をご参照ください。

## [Windows の方向け](https://qiita.com/kanetugu2018/items/65a57b6bc4bbab7e43d5#%E3%82%A4%E3%83%B3%E3%82%B9%E3%83%88%E3%83%BC%E3%83%AB%E6%89%8B%E9%A0%86)

https://qiita.com/kanetugu2018/items/65a57b6bc4bbab7e43d5#%E3%82%A4%E3%83%B3%E3%82%B9%E3%83%88%E3%83%BC%E3%83%AB%E6%89%8B%E9%A0%86

## [Mac の方向け](https://qiita.com/kanetugu2018/items/2268403549832966f26d#%E3%82%A4%E3%83%B3%E3%82%B9%E3%83%88%E3%83%BC%E3%83%AB%E6%89%8B%E9%A0%86) 

https://qiita.com/kanetugu2018/items/2268403549832966f26d#%E3%82%A4%E3%83%B3%E3%82%B9%E3%83%88%E3%83%BC%E3%83%AB%E6%89%8B%E9%A0%86



# 箱庭環境
さて、ここからUnity上で実現している箱庭環境の説明をします。

まず、Unity上のロボットは下図のものです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/244147/99a62d54-51ac-766c-8ee3-bd4be0688a43.png)

今回の強化学習用のサンプルとして利用するセンサは以下の２個にしています。

* 超音波センサ
  * 中央の円柱との距離を計測するために利用します。
  * ここで計測された値をもとにして、報酬計算します。
    * ライントレース上近くにロボットが走っている場合に最大の報酬になるようにします。
* カラーセンサ
  * ライントレース用のセンサです。
  * カラーセンサの値は連続値で0～100の値をとります。
  * 白色に近いと 0 の値を返し、黒色だと 100 の値を返します。

また、ロボットを移動させるために、モーターを使います。
モーター操作としては、簡略化して以下のオペレーションを用意することにしました。

* 前に進む(スピードは2段階程度)
* 右に曲がる(スピードは2段階程度)
* 左に曲がる(スピードは2段階程度)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/244147/8498f650-0f3d-1644-91bc-08a07f1eb300.png)


# 強化学習プログラム

強化学習用のサンプルプログラムを以下に用意しました。

https://github.com/toppers/hakoniwa-base/blob/ai/workspace/dev/ai/ai_qtable.py

強化学習としては、Qテーブルを利用しています(以下、参考にした情報です)。

* [【入門】Q学習の解説とpythonでの実装 〜シンプルな迷路問題を例に〜](https://www.tcom242242.net/entry/ai-2/%E5%BC%B7%E5%8C%96%E5%AD%A6%E7%BF%92/%E3%80%90%E5%BC%B7%E5%8C%96%E5%AD%A6%E7%BF%92%E3%80%81%E5%85%A5%E9%96%80%E3%80%91q%E5%AD%A6%E7%BF%92_%E8%BF%B7%E8%B7%AF%E3%82%92%E4%BE%8B%E3%81%AB/)
* [OpenAIGymとPythonを使って簡単なQ学習を実装](https://qiita.com/God_KonaBanana/items/20268e3ac92e40c791a4)

サンプルプログラム(ai_qtable.py)の骨子部分は以下のもので、学習モデル(model変数)を別の学習アルゴリズムに変更すれば、箱庭環境上で様々なアルゴリズムをお試しすることができると思います。

* 2023/01/29 午後
  * QテーブルをCSVファイルとして保存する機能を追加しました
  * また、起動時にQテーブルファイルがあった場合は、ロードする機能も追加しました
  * ※学習したモデルを保存しておきたくなりました

```python
#get ai model
model = qtable_model2.get_model(env.robo().num_states(), env.robo().num_actions())
model.load('./dev/ai/qtable_model.csv')

#do simulation
robo = env.robo()
for episode in range(100):
  total_time = 0
  done = False
  state = 0
  total_reward = 0
  while not done and total_time < 4000:
      action = model.get_action(state)
      next_state, reward, done, _ = env.step(action)
      total_reward = total_reward + reward
      model.learn(state, action, reward, next_state)
      state = next_state    
      total_time = total_time + 1
  env.reset()
  model.save('./dev/ai/qtable_model.csv')
  print("episode=" + str(episode) + " total_time=" + str(total_time) + " total_reward=" + str(total_reward))
```

なお、「報酬」、「観測」、「アクション」を変更したい場合は、以下の[プログラム](https://github.com/toppers/hakoniwa-base/blob/ai/workspace/dev/ai/hako_robomodel_ev3.py)を編集してください。

https://github.com/toppers/hakoniwa-base/blob/ai/workspace/dev/ai/hako_robomodel_ev3.py

* 報酬
  * reward()
    * 超音波センサの計測結果から報酬を計算します
    * 超音波センサは、0～255の値を返します。
    * ライントレース上にロボットがいる場合は、120くらいの値になります。
* 観測
  * state()
    * 観測した状態を返します
  * num_states()
    * 観測状態数を返します
* アクション
  * action()
    * ロボットのモーター操作を実行します
  * num_actions()
    * アクション数を返します
  
# 強化学習の実行手順

OS 毎に実ル手順が異なりますので、以下をご参照ください。

## [Windows の方向け](https://qiita.com/kanetugu2018/items/65a57b6bc4bbab7e43d5#%E5%BC%B7%E5%8C%96%E5%AD%A6%E7%BF%92%E3%81%AE%E5%AE%9F%E8%A1%8C%E6%89%8B%E9%A0%86)

https://qiita.com/kanetugu2018/items/65a57b6bc4bbab7e43d5#%E5%BC%B7%E5%8C%96%E5%AD%A6%E7%BF%92%E3%81%AE%E5%AE%9F%E8%A1%8C%E6%89%8B%E9%A0%86

## [Mac の方向け](https://qiita.com/kanetugu2018/items/2268403549832966f26d#%E5%BC%B7%E5%8C%96%E5%AD%A6%E7%BF%92%E3%81%AE%E5%AE%9F%E8%A1%8C%E6%89%8B%E9%A0%86) 

https://qiita.com/kanetugu2018/items/2268403549832966f26d#%E5%BC%B7%E5%8C%96%E5%AD%A6%E7%BF%92%E3%81%AE%E5%AE%9F%E8%A1%8C%E6%89%8B%E9%A0%86

# デモ
以下、デモです。

## 荷物運搬ロボット
### 学習中の状態

https://twitter.com/kanetugu2020/status/1619251860157927425

### 学習済み状態

https://twitter.com/kanetugu2020/status/1619251954521350144

## HackEV
こちらは、報酬をカラーセンサにして強化学習させたものです。

hako_robomodel_ev3.py の変更点は以下の２関数です。

```python
    def rewaord(self, obserbation):
        o = obserbation[1]['color_sensors'][0]['reflect']
        min = 00.0
        size = 100.0
        #0 -1
        ret_o = (o - min) / size
        if ret_o <= 0.5:
            return 10 * (1 - ret_o), False
        else:
            self.out_count = self.out_count + 1
            if self.out_count < 300:
                return -1, False
            else:
                self.out_count = 0
                return -10, True

    def state(self, obserbation):
        o = obserbation[1]['color_sensors'][0]['reflect']
        min = 0.0
        size = 100.0
        #0 -1
        ret_o = (o - min) / size
        #print("ret_o=" + str(ret_o))
        return (int)((ret_o * 30.0) / 10.0)
```

https://twitter.com/kanetugu2020/status/1624331702326931456

# トラブルシューティング

## [Windows の方向け](https://qiita.com/kanetugu2018/items/65a57b6bc4bbab7e43d5#%E3%83%88%E3%83%A9%E3%83%96%E3%83%AB%E3%82%B7%E3%83%A5%E3%83%BC%E3%83%86%E3%82%A3%E3%83%B3%E3%82%B0)

https://qiita.com/kanetugu2018/items/65a57b6bc4bbab7e43d5#%E3%83%88%E3%83%A9%E3%83%96%E3%83%AB%E3%82%B7%E3%83%A5%E3%83%BC%E3%83%86%E3%82%A3%E3%83%B3%E3%82%B0

## [Mac の方向け](https://qiita.com/kanetugu2018/items/2268403549832966f26d#%E3%83%88%E3%83%A9%E3%83%96%E3%83%AB%E3%82%B7%E3%83%A5%E3%83%BC%E3%83%86%E3%82%A3%E3%83%B3%E3%82%B0) 

https://qiita.com/kanetugu2018/items/2268403549832966f26d#%E3%83%88%E3%83%A9%E3%83%96%E3%83%AB%E3%82%B7%E3%83%A5%E3%83%BC%E3%83%86%E3%82%A3%E3%83%B3%E3%82%B0