MATLAB x OpenAI Gym: 強化学習アプリでラクして強化学習!

# はじめに
<div align="center">
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/473841/3543b5e0-5802-2785-e8d8-43c35d6ef00f.gif",width=200px,title="車が山を登る"></div>

MATLAB R2021a でリリースされた強化学習を GUI から実行可能な**強化学習デザイナーアプリ**は、直感的に小難しい強化学習の

- 設定
- 学習
- シミュレーション

が実行できるとして、MATLAB ユーザーが乱舞狂乱したのは記憶に新しいことかと思います。本記事は、この機能を使って楽して OpenAI Gym の環境を強化学習するという試みをご披露します。

なお、当該アプリを使うと MATLAB/Simulink 下での強化学習はもはや Challenging でも何でもないため、ここではあえていばらの道、 **Python (OpenAI Gym) 連携** に取り組みたいと思います。

強化学習デザイナーアプリ:
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/473841/a4861299-69ee-5424-c97f-d05b50603111.gif" width=600px>
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/473841/6c648f60-fe80-0b44-64c8-615ed01dfb1c.jpeg" width=500px>

# 強化学習用に Python の仮想環境を用意する
デフォルトでの MATLAB の Python 設定は以下のような感じ:
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/473841/9ce761cd-69cc-fd8f-550d-e210cac59089.png" width=450px>

:::note warn
注意
MATLAB から Python を操作することが可能ですが、この準備ができているのが大大大前提です*
:::

[*Python の MATLAB からの利用についてはこちら](https://www.mathworks.com/help/matlab/call-python-libraries.html?s_tid=CRUX_lftnav)

この環境に OpenAI Gym ライブラリをインストールしても良いのですが、Python のライブラリ間の依存関係 (ヴァージョン揃えたり) の解決には時間がかかるので、ここではあっさり以下のようにターミナルから強化学習用に Python の仮想環境を用意します。因みに anaconda 使っています:

```console:Console
conda create --name matlab-rl python=3.7
```
Open AI Gym をインストールします。

   -  conda 
   -  pip 

がありますが、`conda` でインストールに苦戦したので、”えいや”で `pip` で以下のような感じでインストール:

```console:Console
pip install gym==[version]
pip install gym[atari]
```
ここで、MATLAB から Python 連携を行うために `pyenv` コマンドを使いますが、上で作成した Python 仮想環境を MATLAB で使うために:

```matlab:Code(Display)
rlPython = {'Version','C:\Users\HYCE\anaconda3\envs\matlab-rl\python.exe'};
pyenv(rlPython{:})
```
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/473841/97f2d6ef-365f-4ae7-303f-8ecd7f220f69.png" width=500px>
と Python を切り替えておきます。


# OpenAI Gym 環境
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/473841/b61f2812-f6f9-34b1-308c-0881c2a0af64.png",width=500px>
Open AI Gym から1つ強化学習用の環境を引っ張ってきます:

```matlab:Code
myenv = py.gym.make('MountainCar-v0')
```

Python での実装は [GitHub OpenAI Gym](https://github.com/openai/gym/blob/master/gym/envs/classic_control/mountain_car.py) をご覧ください。

:::note info
環境の絵を見てお分かりいただける通り、車を左右に加速させて、 山を登りきることを目指すための単純な環境です。観測は(x,y) 座標と、速度、そして報酬信号、と終了条件達成フラグ (isdone 信号)が得られると考えられます。
:::

## この環境の詳細
結局、環境の I/O だけ分かれば良いので情報を集めます:
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/473841/198fecc1-08b5-647d-d305-4a1e2ccbfd7a.png",width=500px>

### 環境の"観測"を行う
上の情報に依ると入手できる情報は以下の2つ:

- Car position
- Car velocity

確認しましょう。<br> Python オブジェクトのプロパティを表示するために、`details` 関数を使います。

```matlab:Code
details(myenv.reset())
```
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/473841/e5bc5e1a-567f-4b71-4407-bd40a2242049.png", width=300px>

アクション後のオブジェクトにある data プロパティが恐らく、観測データと睨む:

```matlab:Code
res = myenv.reset();
res.data
```
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/473841/f19d137a-81ce-e23e-f689-0c3ea83d8769.png",width=450px>

間違いない... こいつらが観測データの2つだ。

## ランダムな行動に対する応答をレンダリングして表示する
``myenv``オブジェクトを触っていると、"いかにも"なメソッドが見つかります

   -  step 
   -  render 

あたりのメソッドがアヤシイので、これを使ってランダムな行動に対して、
 
- Position (観測1: 位置)
- Velocity (観測2: 速度)
- Reward (報酬)
- isdone (終了条件達成フラグ)

を確認できるはずです。

```matlab:Code
for i = 1:1000
    action = int16(randi([0,2],1));
    myenv.step(action);
    myenv.render();
end
```

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/473841/3543b5e0-5802-2785-e8d8-43c35d6ef00f.gif",width=450px>
あ、動いた...
### 環境から戻ってきたデータを受け取る準備
動作確認ができたので、最後の1つの行動 (``action``) を使って出力を確認:

```matlab:Code
result = myenv.step(action) 
```

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/473841/b07bc3c6-e568-671e-6a42-ed7264cc7ee5.png",width=450px>


きっとこれが、[Position, Velocity, Reward, isdone] だ! これを MATLAB で受け取る。

ここで、**MATLAB <--> Python 技: "とりあえず cell で受ける"** を使う:

```matlab:Code
% Python の結果をとりあえず cell で受けておく
result = cell(result)
% 観測 (Observation) だけ MATLAB に渡すことができるか確認
Observation = double(result{1})'
% 報酬
Reward = double(result{2})
% isdone?
IsDone = boolean(double(result{3}))
```
これでとりあえず、MATLAB で扱うことができる変数型に変換することができました。ここまでくればあとは MATLAB World での作業なので大分楽になります。

```matlab:Code
% 最後に閉じる
myenv.close();
```
以上で実験終了。MATLAB で強化学習を実行する準備が整いました。

# カスタム環境ラッパークラスを作成する
今までやってきたことを念頭に置いて、Python で作成した"環境" >> MATLAB の"環境" とする必要があるので、カスタム環境を作成します。次のリンクから作り方を真似します：[Create Custom MATLAB Environment from Template](https://www.mathworks.com/help/reinforcement-learning/ug/create-custom-matlab-environment-from-template.html)

```matlab:Code(Display)
rlCreateEnvTemplate("MountainCar_v0");
```
と実行すると、``MountainCar_v0`` 環境クラスの**テンプレート**が作成されます。ここがスタートポイントです。上記の情報を引き抜く技をテンプレートに適用し、``MountainCar_v0.m``を次のように定義します。

<details><summary>MountainCar_v0 クラス</summary><div>

```matlab:Code
classdef MountainCar_v0 < rl.env.MATLABEnvironment
    %MOUNTAINCAR_V0: Template for defining custom environment in MATLAB.    
    
    %% Properties (set properties' attributes accordingly)
    properties
       open_env = py.gym.make('MountainCar-v0');
    end
    
    properties
        % Initialize system state [x,dx/dt]'
    end
    
    properties(Access = protected)
        % Initialize internal flag to indicate episode termination
        IsDone = false;
    end

    %% Necessary Methods
    methods              
        % Contructor method creates an instance of the environment
        % Change class name and constructor name accordingly
        function this = MountainCar_v0()
            % Initialize Observation settings
            ObservationInfo = rlNumericSpec([2 1]);
            ObservationInfo.Name = 'Mountain Car States';
            ObservationInfo.Description = 'Position x, Velocity dx/dt';
            
            % Initialize Action settings   
            ActionInfo = rlFiniteSetSpec([0 1 2]);
            ActionInfo.Name = 'Mountain Car Action';
            
            % The following line implements built-in functions of RL env
            this = this@rl.env.MATLABEnvironment(ObservationInfo,ActionInfo);
        end
        
        % Apply system dynamics and simulates the environment with the 
        % given action for one step.
        function [Observation,Reward,IsDone,LoggedSignals] = step(this,Action)
            LoggedSignals = [];
            
            % Observation
            result = cell(this.open_env.step(int16(Action)));
            Observation = double(result{1})'; % ndarray >> MATLAB double
            
            % Reward
            Reward = double(result{2});
            
            % is Done?
            IsDone = boolean(double(result{3}));
            
            if (Observation(1) >= 0.4)
                Reward = 0;
                IsDone = true;
            end
            this.IsDone = IsDone;
            
            % (optional) use notifyEnvUpdated to signal that the 
            % environment has been updated (e.g. to update visualization)
            notifyEnvUpdated(this);
        end
        
        % Reset environment to initial state and output initial observation
        function InitialObservation = reset(this)
            
            result = this.open_env.reset();
            InitialObservation = double(result)';
            
            % (optional) use notifyEnvUpdated to signal that the 
            % environment has been updated (e.g. to update visualization)
            notifyEnvUpdated(this);
        end
    end
    %% Optional Methods (set methods' attributes accordingly)
    methods               
        % Helper methods to create the environment
    end
    
    methods (Access = protected)
        % (optional) update visualization everytime the environment is updated 
        % (notifyEnvUpdated is called)
        %function envUpdatedCallback(this)
        %end
    end
end
```

</div></details>
最初のプロパティメンバと、コンストラクタ、step 関数、初期値の設定、あたりをお馴染みのコマンドで記述すれば良いだけです。Python のデータの渡し方を事前に検証しておいたので、スムースに出来ると思います。

# 環境インスタンスをカスタム環境クラスから作成する
ではカスタム環境クラスからインスタンスを作成します。これが今回学習する MATLAB での環境の定義になります。

```matlab:Code
matEnv = MountainCar_v0();
```

一応チェックしておきます。カスタムで作った場合は、この関数 ``validateEnvironment``を使います。

```matlab:Code
validateEnvironment(matEnv)
```

これは、

- 初期の観測と行動を作成
- 1－2 step だけ、環境をシミュレーション

を実行し、強化学習の学習が大丈夫か事前にチェックできます。問題が無ければそのまま進みます。


# 学習
以降は強化学習デザイナーアプリを使って実施。以下の流れです

1. 学習のセットアップ
1. 学習
1. アプリ内でのシミュレーション
1. Critic \& Agent のエクスポート
1. エクスポートしたものを使ってシミュレーション

1-4 までアプリ内で実行可能です。
### 設定
環境は先ほど作った ``matEnv``です。agent は以下のように指定:

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/473841/a5bd4020-98b9-12dc-ee23-10f46638738d.jpeg", width=400px>

- DQN
- Critic ネットワークの幅は 256

学習の設定は次のようにします:

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/473841/6c648f60-fe80-0b44-64c8-615ed01dfb1c.jpeg" width=500px>

### 学習実行
実行ボタンをクリックして、あとは学習終了まで待ちましょう .... 
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/473841/33dc991d-3aa4-d1f3-ad42-b9aa5c0e156f.gif", width=500px>

# シミュレーションと可視化
学習終了したら、アプリからシミュレーションを実行しても良いですが、この場合レンダリングされず、車が動いている様子が見えないためモデルをエクスポートして、マニュアルシミュレーションを実行します。

## 学習後にモデルをエクスポート
agent や agent の要素をエクスポートできます。深層強化学習のネットワークだけエクスポート:
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/473841/bfb6ce3e-f2fe-fe6b-4d8f-cecd9d54b05b.jpeg",width=100px>

Critic のネットワークがワークスペースに落ちてきます。この設定だと ``agent_criticNetwork`` のはずです。

:::note warn
R2021a ではエクスポートされたネットワークは DAGnetwork オブジェクトでしたが、R2021b では dlnetwork オブジェクトです。そのため、ネットワークに渡す変数型がR2021b のものでは dlarray が要求されます。以下の可視化プログラムではそこに配慮を入れて、どちらでも動作するようにしています。
:::

## マニュアルシミュレーションを実行して車が登るか確認
可視化するコードを用意して、無理やり Gym のレンダリングをさせます。このあたりは、MATLAB と Python を連携させる際に、一手間必要なところになってしまいますが、本丸の強化学習の方で楽できるので、Qiita ユーザーなら屁でもないでしょう。

- 環境
- 学習したネットワーク (Critic)
- シミュレーション回数

を渡します:

```matlab:Code
visSim(matEnv,trainedCritic,5);
```
<details><summary> visSim.m コード内容こちら </summary><div>

```matlab:Code
function visSim(env,net,numSim)
% Run Simulation using "step" method
%
%
if isa(net,"dlnetwork") % in case of dlnetwork
    dlnetFlag = true;
end

    for i=1:numSim
        state = single(env.open_env.reset()); % initial [x, dx/dt]
        
        if dlnetFlag % dlnetwork
            state = dlarray(state,"BC");
        end
        
        isDone = false; % Break criterion 
        steps = 0; % step counts
        
        while(~isDone && steps < 200)
            
            % Take the best action according to state
            % Note that the network accepts S --> Q(S,A)
            [~,action] = max(predict(net,state));
            if dlnetFlag
                action = extractdata(action);
            end
            
            % Recieve result from the environment: action \in {0,1,2}
            result = cell(env.open_env.step(int16(action-1)));
            
            % Display OpenAI Gym environment
            env.open_env.render(); 
            
            % new [x, dx/dt]
            new_state = single(result{1});
            
            % isDone signal
            isDone = boolean(single(result{3}));
            
            % stop criterion
            if (new_state(1) >=0.49)
                isDone = true;
            end
            
            state = new_state; % update [x, dx/dt]
            if dlnetFlag % dlnetwork
                state = dlarray(state,"BC");
            end
            steps = steps + 1;
        end
        env.open_env.close(); % Close OpenAI Gym environment
    end

end
```

</div></details>

おー上手く動いた動いた:
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/473841/b0175e75-64d5-53b0-9fcd-834113fcb0e7.gif", width=500>

# まとめ

- MATLAB の強化学習アプリを使って、OpenAI Gym の環境を学習しました
- Python のレンダリングを生かすには、マニュアルシミュレーションが必要でした
- 強化学習アプリのお陰で、かなり作業が楽になります。その分、Python での環境を使ってみようかなと、元気が出ました


では、みなさん **LGTMをしていただいて**、良いお年をお迎えください