DonkeyCar simulatorで強化学習(その2)

DonkeyCar simulatorで強化学習のサンプルを実行してみる(PPO2編)

概要

DonkeyCar simulatorで強化学習(その1) ではポリシーにDDQNを使用したサンプルを実行してみたが、今回はもう一つのサンプル(PPO2を使用)を試してみる。

準備

python仮想環境の準備

# 作業ディレクトリ作成
mkdir -p /work2/donkey_sim2
cd /work2/donkey_sim2

# 仮想環境構築
pyenv install 3.7.12 
pyenv virtualenv 3.7.12 donkey_sim2
pyenv local donkey_sim2 
pip install --upgrade pip setuptools wheel

# 必要なライブラリのインストール
pip install opencv-python
pip install stable-baselines
pip install tensorflow==1.14.0

# 現時点での最新リリース v21.7.24 をcheckoutしておく
git clone https://github.com/tawnkramer/gym-donkeycar -b v21.07.24

# gym-donkeycarライブラリのインストール
pip install -e gym-donkeycar

[!NOTE] stable-baselines は tensorflow ~1.14.0 しかサポートしていないので、バージョン指定してインストールする。
tensorflow 1.14.0 は python ~3.7 しかサポートしていないので、3.7系の最新版を使用している。

[!NOTE] gym-donkeycar は -e (–editable) オプション付きでインストールしているので、編集も可能。 git cloneした先を参照しているので、インストール後もgym-donkeycarを削除しちゃダメ。

githubから直接インストールする場合は以下。
この場合、パッケージは通常のディレクトリにインストールされる。

pip install git+https://github.com/tawnkramer/gym-donkeycar@v21.07.24

でも、以下でサンプルプログラム使うので、git cloneもしないとダメ。

シミュレータのダウンロード&インストール

DonkeyCar simulatorで強化学習(その1) と同じ

プログラム

用意されているサンプルプログラムにパッチをあてようと思ったのだけど、
ppo_train.py はやっつけ感満載のイマイチソースなので いっそ全書き換えで。

主な対応内容は、

ppo.py

import sys
import os
import signal
import argparse
import uuid
import datetime

import typing
from typing import Union, List, Dict, Any, Optional

import gym
import gym_donkeycar

from stable_baselines import PPO2
# from stable_baselines.common import set_global_seeds
from stable_baselines.common.policies import CnnPolicy
from stable_baselines.common.vec_env import DummyVecEnv, VecEnv
from stable_baselines.common.callbacks import EventCallback
from stable_baselines.common.base_class import BaseRLModel

class MyCallback(EventCallback):
    def __init__(self, 
                 eval_env: Union[gym.Env, VecEnv],
                 save_freq: int = 1000,
                 save_file: Optional[str] = None,
                 verbose: int = 1):
        super(MyCallback, self).__init__(verbose=verbose)
        self.save_file = save_file
        self.save_freq = save_freq
        
        # Convert to VecEnv for consistency
        if not isinstance(eval_env, VecEnv):
            eval_env = DummyVecEnv([lambda: eval_env])
            
        assert eval_env.num_envs == 1, "You must pass only one environment for evaluation"
        
        self.eval_env = eval_env
    
    def init_callback(self, model: 'BaseRLModel') -> None:
        print("#### INIT ####")
        super(EventCallback, self).init_callback(model)
    
    def _init_callback(self):
        print("#### _INIT ####")
    
    def _on_step(self) -> bool:
        if self.save_freq > 0 and self.n_calls % self.save_freq == 0:
            now = datetime.datetime.now()
            if self.verbose > 0:
                print(f'{now} saving...')
            if self.save_file :
                now_str  = now.strftime('%y%m%d_%H%M%S')
                filename = self.save_file
                # filename = os.path.join(os.path.dirname(self.save_file), f'{now_str}_{os.path.basename(self.save_file)}')
                self.model.save(filename)
        return True
    

if __name__ == "__main__":
    # Initialize the donkey environment
    # where env_name one of:
    env_list = [
        "donkey-warehouse-v0",
        "donkey-generated-roads-v0",
        "donkey-avc-sparkfun-v0",
        "donkey-generated-track-v0",
        "donkey-roboracingleague-track-v0",
        "donkey-waveshare-v0",
        "donkey-minimonaco-track-v0",
        "donkey-warren-track-v0",
        "donkey-thunderhill-track-v0",
        "donkey-circuit-launch-track-v0",
    ]
    
    parser = argparse.ArgumentParser(description="ppo_train")
    parser.add_argument(
        "--sim",
        type=str,
        default="sim_path",
        help="path to unity simulator. maybe be left at manual if you would like to start the sim on your own.",
    )
    parser.add_argument("--model",     type=str, default="ppo_donkey",  help="path to model")
    parser.add_argument("--host",      type=str, default="127.0.0.1",   help="simulator address")
    parser.add_argument("--port",      type=int, default=9091,          help="port to use for tcp")
    parser.add_argument("--step",      type=int, default=10000,         help="port to use for tcp")
    parser.add_argument("--test_step", type=int, default=0,             help="port to use for tcp")
    parser.add_argument("--test",      action="store_true",             help="load the trained model and play")
    parser.add_argument("--env_name",  type=str, default="donkey-warehouse-v0",     help="name of donkey sim environment", choices=env_list)
    
    args = parser.parse_args()
    
    test_step = args.test_step
    if args.test and test_step == 0 :
        test_step = 1000
        
    # Complement the file extension
    if not args.model.endswith(".zip") :
        model_path = args.model + ".zip"
    else :
        model_path = args.model
    
    conf = {
        "exe_path": args.sim,
        "host": args.host,
        "port": args.port,
        "body_style": "car01",
        "body_rgb": (128, 255, 128),
        "car_name": "me",
        "font_size": 100,
        "racer_name": "PPO",
        "country": "USA",
        "bio": "Learning to drive w PPO RL",
        "guid": str(uuid.uuid4()),
        "max_cte": 10,
    }
    
    # Make an environment test our trained policy
    env = gym.make(args.env_name, conf=conf)
    env = DummyVecEnv([lambda: env])
    
    # hook terninate signal
    def signal_handler(signal, frame):
        print("catching ctrl+c")
        env.close()
        sys.exit(0)
    
    signal.signal(signal.SIGINT, signal_handler)
    signal.signal(signal.SIGTERM, signal_handler)
    signal.signal(signal.SIGABRT, signal_handler)
    
    try :
        # check model path
        if os.path.exists(model_path):
            # load model
            model = PPO2.load(model_path, env = env)
        else : 
            if not args.test:
                # create model
                print("create new model")
                model = PPO2(CnnPolicy, env, verbose=1)
            else :
                raise ValueError(f"Error: the file {model_path} could not be found")
        
        # change throttle lower limit
        env.action_space.low[1] = 0.1
        
        if not args.test:
            # in training mode
            
            callback = MyCallback(env, save_freq = 5000, save_file = model_path, verbose = 1)
            # callback = MyCallback(env, save_freq = 10, verbose = 1)
            
            # set up model in learning mode with goal number of timesteps to complete
            # model.learn(total_timesteps=10000)
            model.learn(total_timesteps=args.step, callback=callback)
            
            # save model
            model.save(model_path)
        
        print("stert testing...")
        obs = env.reset()
        prev_done_count = 0
        for i in range(test_step):
            action, _states = model.predict(obs)
            obs, rewards, dones, info = env.step(action)
            # print(f"cnt : {i}    rewards : {rewards[0]}    dones : {dones[0]}    pos : {info[0]['pos']}, CrossTrackError : {info[0]['cte']}, speed : {info[0]['speed']}")
            # print(f"+++ info: {info} +++")
            env.render()
            if (dones) :
                print(f'dones flag detected : {i - prev_done_count}  ({i})')
                prev_done_count = i
        print("done testing")
        
    except KeyboardInterrupt:
        print("stopping run...")
    
    env.close()

とりあえず学習

とりあえず学習。
リモートマシンでDonkeyCar シミュレータを実行しておき、以下のコマンドを実行します。 --simオプションにremoteを、実行マシンのIPアドレスを--hostオプションに指定します。

cd examples/reinforcement_learning
python ppo.py --sim=remote --host=192.168.78.200

[!NOTE] 学習回数を指定するには--stepオプションで指定します。
指定する回数はエピソード数ではなく、アクション数。
例えば、--step=100000

[!NOTE] ローカルマシンでシミュレータを実行する場合は
--simオプションにDonkeyCar シミュレータ起動コマンドをフルパスで指定します。
このとき、--hostオプションは不要です。
シミュレータをあらかじめ起動しておく必要はなく、自動的に起動されます。

モデルの保存間隔はMyCallbackのインスタンス生成時にsave_freq = 5000で指定していますので、必要なら変更してください。

学習結果でテストしてみる。

学習のときのコマンドに--testを指定するだけです。
テストを実行するステップ数を変更したい場合は--test_stepオプションで指定します。

python ppo.py --sim=remote --host=192.168.78.200 --test 

途中で止めたい場合はCTRL-Cで止めてください。

ソースを読んでみる

ほど複雑じゃないので省略。