Translate

ラベル Stable Baselines の投稿を表示しています。 すべての投稿を表示
ラベル Stable Baselines の投稿を表示しています。 すべての投稿を表示

2020年12月2日水曜日

Stable Baselines の使いやすいインターフェイスを使ったままで、独自の方策ネットワークを定義する

 Stable Baselines の使いやすいインターフェイスを使ったままで、独自の方策ネットワークを定義することも可能です。

以下、Stable Baselinesドキュメントの該当箇所の翻訳です。

-----

本ドキュメントは、Stable Baselines3 ドキュメントの Custom Policy Network を翻訳したものです。

カスタム方策ネットワーク


Stable Baselines3 は、画像(CnnPolicy)及びその他タイプの入力機能(MlpPolicy)の方策ネットワークを提供します。
 

警告
A2CおよびPPOの場合、トレーニングやテスト中に(範囲外のエラーを回避するため)継続的な行動がクリップされます。SAC、DDPG、TD3は、`tanh()` 境界をより正確に処理する変換を使って、行動を押しつぶします。

カスタム方策アーキテクチャ


カスタム方策アーキテクチャをカスタマイズするひとつの方法として、モデルを生成する際の引数に policy_kwargs パラメータを渡すことです。


import gym
import torch as th

from stable_baselines3 import PPO

# 活性化関数にRelu を使う、サイズが32である2つのレイヤを備えたカスタムMLP(多層パーセプトロン)Policy
policy_kwargs = dict(activation_fn=th.nn.ReLU, net_arch=[32, 32])
# エージェントの生成
model = PPO("MlpPolicy", "CartPole-v1", policy_kwargs=policy_kwargs, verbose=1)
# 環境の取得
env = model.get_env()
# エージェントのトレーニング
model.learn(total_timesteps=100000)
# エージェントの保存
model.save("ppo-cartpole")

del model
# policy_kwargs も自動的にロードされる
model = PPO.load("ppo-cartpole")


方策(もしくは価値)ネットワークのカスタムアーキテクチャも簡単に定義することができます。
 

注意
カスタム方策クラスを定義することと、引数として policy_kwargs を渡すことは同じ意味をさしていますが、ポリシーに名前をつけることができるため、コード上では意図が明確に示しやすくなります。policy_kwargs は、ハイパーパラメータ検索を行う場合には便利です。

カスタム特徴抽出機(Feature Extractor)


(画像を使用する際のカスタムCNNなど)カスタム特徴抽出器が必要な場合は、 BaseFeatureExtractor を継承したクラスを定義して、トレーニング処理にてモデルを生成する差異に引数として渡します。


import gym
import torch as th
import torch.nn as nn

from stable_baselines3 import PPO
from stable_baselines3.common.torch_layers import BaseFeaturesExtractor


class CustomCNN(BaseFeaturesExtractor):
    """
    :param observation_space: (gym.Space) 観測空間
    :param features_dim: (int) 抽出される特徴量の次元(最後のレイヤのユニット数に相当)
    """

    def __init__(self, observation_space: gym.spaces.Box, features_dim: int = 256):
        super(CustomCNN, self).__init__(observation_space, features_dim)
        # (チャネルが最初にくる) C×H×W イメージを想定
        # 並べ直しは事前処理もしくはラッパクラスにより実行されることを想定
        n_input_channels = observation_space.shape[0]
        self.cnn = nn.Sequential(
            nn.Conv2d(n_input_channels, 32, kernel_size=8, stride=4, padding=0),
            nn.ReLU(),
            nn.Conv2d(32, 64, kernel_size=4, stride=2, padding=0),
            nn.ReLU(),
            nn.Flatten(),
        )

        # 観測空間のサンプルを1件取得して形式を算出
        with th.no_grad():
            n_flatten = self.cnn(
                th.as_tensor(observation_space.sample()[None]).float()
            ).shape[1]

        self.linear = nn.Sequential(nn.Linear(n_flatten, features_dim), nn.ReLU())

    def forward(self, observations: th.Tensor) -> th.Tensor:
        return self.linear(self.cnn(observations))

policy_kwargs = dict(
    features_extractor_class=CustomCNN,
    features_extractor_kwargs=dict(features_dim=128),
)
model = PPO("CnnPolicy", "BreakoutNoFrameskip-v4", policy_kwargs=policy_kwargs, verbose=1)
model.learn(1000)

 

オンポリシーアルゴリズム

共有ネットワーク


A2CやPPOの方策パラメータ `net_arch` を使用すると、非表示レイヤ数やサイズ、方策ネットワークと価値ネットワーク間で共有されるレイヤ数を指定することができます。これは次のようなリストであると想定されています。
 

  1. 0を含む任意の共有レイヤのユニット数を指定する任意の長さをあらわす整数(0を含む)。intの数が0の場合、共有レイヤはありません。
  2. 価値ネットワークや方策ネットワークの非共有レイヤを指定するための辞書(オプション)。dict(vf=[<価値ネットワークレイヤサイズ>], pi=[<方策ネットワークレイヤサイズ>]) のような形式。pi や vf のいずれかのキーが欠落している場合、非共有レイヤ(空のリスト)は想定されません。


要約すると、 [<共有レイヤ数>, dict(vf=[<非共有価値ネットワークレイヤ数>], pi=[<非共有方策ネットワークレイヤ数>])] という形式のリストを指します。
 

例


サイズが128である2層の共有レイヤの場合:net_arch=[128, 128]


    観測
     |
   <128>
     |
   <128>
   /   \
行動    価値


方策ネットワークより価値ネットワークのほうが層が厚く、第1層を共有する場合:net_arch=[128, dict(vf=[256, 256])]

    観測
     |
   <128>
   /   \
行動  <256>
        |
      <256>
        |
       価値


第1層を共有し、第2層目から分岐する場合:net_arch=[128, dict(vf=[256], pi=[16])]

    観測
     |
   <128>
   /   \
 <16> <256>
  |     |
行動   価値

 

高度な例


方策/価値アーキテクチャをさらに細かく制御する必要がある場合は、方策を直接再定義することができます。


from typing import Callable, Dict, List, Optional, Tuple, Type, Union

import gym
import torch as th
from torch import nn

from stable_baselines3 import PPO
from stable_baselines3.common.policies import ActorCriticPolicy


class CustomNetwork(nn.Module):
    """
    方策および価値関数のカスタムネットワークをあらわすクラス。
    特徴抽出器(Feature Extractor)によって抽出された特徴量を入力として受け取る。

    :param feature_dim: feature_extractor で抽出された特徴量の次元(例:CNNにより抽出された特徴量)
    :param last_layer_dim_pi: 方策ネットワーク最終層のユニット数(int)
    :param last_layer_dim_vf: 価値ネットワーク最終層のユニット数(int)
    """

    def __init__(
        self,
        feature_dim: int,
        last_layer_dim_pi: int = 64,
        last_layer_dim_vf: int = 64,
    ):
        super(CustomNetwork, self).__init__()

        # 重要:
        # 確率分布の作成に使用される出力層次元数を保存
        self.latent_dim_pi = last_layer_dim_pi
        self.latent_dim_vf = last_layer_dim_vf

        # 方策ネットワーク
        self.policy_net = nn.Sequential(
            nn.Linear(feature_dim, last_layer_dim_pi), nn.ReLU()
        )
        # 価値ネットワーク
        self.value_net = nn.Sequential(
            nn.Linear(feature_dim, last_layer_dim_vf), nn.ReLU()
        )

    def forward(self, features: th.Tensor) -> Tuple[th.Tensor, th.Tensor]:
        """
        :return: (th.Tensor, th.Tensor) 指定ネットワークの latent_policy(th.Tensor形式), latent_value(th.Tensor形式) 。
            すべての層が共有の場合、``latent_policy == latent_value`` となる。
        """
        return self.policy_net(features), self.value_net(features)


class CustomActorCriticPolicy(ActorCriticPolicy):
    def __init__(
        self,
        observation_space: gym.spaces.Space,
        action_space: gym.spaces.Space,
        lr_schedule: Callable[[float], float],
        net_arch: Optional[List[Union[int, Dict[str, List[int]]]]] = None,
        activation_fn: Type[nn.Module] = nn.Tanh,
        *args,
        **kwargs,
    ):

        super(CustomActorCriticPolicy, self).__init__(
            observation_space,
            action_space,
            lr_schedule,
            net_arch,
            activation_fn,
            # 残りの引数群はベースクラスに渡される
            *args,
            **kwargs,
        )
        # 直行初期化の無効化
        self.ortho_init = False

    def _build_mlp_extractor(self) -> None:
        self.mlp_extractor = CustomNetwork(self.features_dim)


model = PPO(CustomActorCriticPolicy, "CartPole-v1", verbose=1)
model.learn(5000)


 

オフポリシーアルゴリズム


SACやDDPG、TD3を使用する際に行動器(actor)と評価器(critic)が異なるネットワークアーキテクチャが必要な場合、次のような形式の辞書を引数に渡します: dict(qf=[<評価器ネットワークアーキテクチャ>], pi=[<行動器ネットワークアーキテクチャ>])

例えば、行動器ネットワーク(別名pi)と評価器ネットワーク(別名qf)を別のアーキテクチャにする場合、net_arch=dict(qf=[400, 300], pi=[64, 64]) のように指定します。

行動器ネットワークと評価器ネットワークを同一アーキテクチャを使う場合は、net_arch=[256, 256] のように指定します(ここでは256ユニットの2つの隠れ層を指定しています)。
 

注意
オンポリシーとは対照的に、行動器と評価器の間の(特徴抽出器以外の)共有ネットワークは許可されていません。


from stable_baselines3 import SAC

# それぞれ64ユニットの2層を持つカスタム行動器アーキテクチャ
# 400ユニットと300ユニットの2層を持つカスタム評価器アーキテクチャ
policy_kwargs = dict(net_arch=dict(pi=[64, 64], qf=[400, 300]))
# エージェントの生成
model = SAC("MlpPolicy", "Pendulum-v0", policy_kwargs=policy_kwargs, verbose=1)
model.learn(5000)

-----
カスタムSIで独自モデルを作ることは殆どないとは思いますが、ご参考まで。

p.s.
Donkeycar のモデルを強化学習化するには、最後の「高度な例」セクションの手段を取れば可能になります。

..というか、gym-donkeycar使うか..ふつー..


2020年9月14日月曜日

Stable Baselinesのコールバックの書き方を調べてみる

Donkeycar の OpenAI Gym 環境 gym-donkeycar を理解するためには OpenAI Gymのフレームワークを手っ取り早く把握する必要がある。

 ということで書店でみつくろい、比較的カラフルで分厚い「OpenAI Gym/Baselines 深層学習 強化学習 人工知能プログラミング 実践入門」という書籍を買った。

 

過去に一度さらっとTensorflo(v1の頃だけど)強化学習のチュートリアルをやって、だいぶ時間がたっていたが、必要な箇所しか説明していなかったので、すんなりすすめることができた。

 本に書いているコードに多少誤り(breakぬけとか)があり、治す必要はあるが Python を知っている人だったら問題ない程度..

 

..だったのだけど、Stable Baselinesのコールバックを使うところで、サンプルコードが動かな書くなった..

..動かなくなった時点でピンときたのが、後付で読んだアマゾンのレコメンド、情報が古い云々..ああ、Stable Baselinesのコールバック実装が変わったんじゃないの、とピンときた。

 

で公式ドキュメントの Callbacks を読んでみたら、案の定"レガシ"になってた..

 

以下、勝手翻訳した内容です。

(ただしレガシとかかれた旧呼び出しに関する箇所はomitしました)

参照はat your own riskでお願いします。

 ------

コールバック


コールバックは、トレーニング手順の特定の段階で呼び出される関数のセットです。トレーニング中にコールバックを使用して強化学習(RL)モデルの内部状態にアクセスできます。監視、自動保存、モデル操作、進行状況バーなどの機能を提供することができます。

独自 Callback


独自の Callback クラスを構築するには、BaseCallback を継承したクラスを作成する必要があります。これにより、イベント( _on_training_start 、_on_step )や便利な変数(RLモデルの self.model など)にアクセスできます。

ここではカスタムコールバックの2つのサンプルを紹介します。1つはトレーニングの報酬に応じて最適なモデルを保存するためのもの(セクション「サンプル」を参照)と、Tensorboardを使用して追加の値をログに記録するためのもの(「Tensorboard」セクションを参照)です。

from stable_baselines.common.callbacks import BaseCallback


class CustomCallback(BaseCallback):
    """
    BaseCallback クラスを継承したカスタムコールバック

    :param verbose: (int) Verboseレベル 0:出力なし 1: infoのみ 2: debugレベルまで
    """
    def __init__(self, verbose=0):
        super(CustomCallback, self).__init__(verbose)
        # 以下の変数にはコールバック内でアクセス可能
        # (ベースクラス上で定義済み)
        # 強化学習モデル
        # self.model = None         # タイプ: BaseRLModel
        # トレーニングに使用される環境のエイリアス
        # self.training_env = None  # タイプ: Union[gym.Env, VecEnv, None]
        # コールバックが呼び出された回数
        # self.n_calls = 0          # タイプ: int
        # self.num_timesteps = 0    # タイプ: int
        # ローカル変数辞書、グローバル変数辞書
        # self.locals = None        # タイプ: 辞書[文字列, Any]
        # self.globals = None       # タイプ: 辞書[文字列, Any]
        # レポートに使用されるロガーオブジェクト
        # self.logger = None        # タイプ: logger.Logger
        # # イベントコールバックでは、親オブジェクトにアクセスできると
        # # 便利な場合がある
        # self.parent = None        # タイプ: オプション[BaseCallback]

    def _on_training_start(self) -> None:
        """
        このメソッドは最初のロールアウトが始まる前に呼び出される。
        """
        pass

    def _on_rollout_start(self) -> None:
        """
        ロールアウトは、現在のポリシを使用した環境対話の集合である。
        このイベントは、新しいサンプルを収集する前にトリガされる。
        """
        pass

    def _on_step(self) -> bool:
        """
        このメソッドは env.step() が呼び出されるたびに
        モデルによって呼び出される。

        (EventCallback の) 子コールバックの場合、イベントがトリガされた
        ときに呼び出される。

         :return:(真偽値) コールバックが False を返した場合、トレーニングは早期中止される.
        """
        return True

    def _on_rollout_end(self) -> None:
        """
        このイベントは、ポリシを更新する前にトリガされる。
        """
        pass

    def _on_training_end(self) -> None:
        """
        このイベントは learn() メソッド終了前にトリガされる。
        """
        pass


注意
self.num_timesteps は、環境で実行されたステップの総数に対応する。つまり環境の数に env.step() が呼び出された時間の数を掛けたものである。
PPO1および TRPO は、 MPI に依存しているため、(各ステップではなく)各ロールアウト後に self.num_timesteps を知って更新する。
その他のアルゴリズムの場合、self.num_timesteps は n_envs、env.step() を呼び出すたびに、(環境数)ずつ増加する。


注意
SAC、DDPG、TD3、DQNなどのオフポリシアルゴリズムの場合、rollout の概念は、2つの更新の間の環境で実行されるステップに対応する。



EventCallback


Kerasと比較すると、Stable Baselinesは EventCallback というイベントをトリガするための BaseCallback のセカンドタイプを提供します。イベントがトリガされると、子コールバックが呼び出されます。

例として、EventCallbackのサブクラスのEvalCallback (後述セクション参照)は、新しい最適なモデルがある場合、その子のコールバックをトリガします。子コールバック StopTrainingOnRewardThreshold の場合、強化学習モデルによって達成された平均報酬がしきい値を超えた場合にトレーニングを停止します。

注意
EvalCallback と StopTrainingOnRewardThreshold のソースコードを確認して、この種のコールバックで実現できることの概要を理解することを推奨。


class EventCallback(BaseCallback):
    """
    イベント発生時にコールバックをトリガするためのベースクラス。

    :param callback: (オプション[BaseCallback]) イベントがトリガーされた時に呼び出されるコールバック
    :param verbose: (int)
    """
    def __init__(self, callback: Optional[BaseCallback] = None, verbose: int = 0):
        super(EventCallback, self).__init__(verbose=verbose)
        self.callback = callback
        # 親にアクセスする権限を与える
        if callback is not None:
            self.callback.parent = self
    ...

    def _on_event(self) -> bool:
        if self.callback is not None:
            return self.callback()
        return True


Callbackコレクション


安定したベースラインは、以下のような共通コールバックセットを提供します。
 

  • モデルを定期的に保存する (CheckpointCallback)
  • モデルを定期的に評価し最適なモデルを保存する (EvalCallback)
  • コールバックの連鎖 (CallbackList)
  • イベントのコールバックのトリガ(イベントコールバック、EveryNTimesteps)
  • 報酬のしきい値に基づいてトレーニングを早期に停止する (StopTrainingOnRewardThreshold)


CheckpointCallback


save_freq ステップごとにモデルを保存する Callback クラスです。ログフォルダ( save_path ) を引数に指定します。またオプションとしてチェックポイントファイルの接頭文字列を指定できます( デフォルトは rl_model )。

from stable_baselines import SAC
from stable_baselines.common.callbacks import CheckpointCallback
# 1000ステップごとにチェックポイントを保存する
checkpoint_callback = CheckpointCallback(save_freq=1000, save_path='./logs/',
                                         name_prefix='rl_model')

model = SAC('MlpPolicy', 'Pendulum-v0')
model.learn(2000, callback=checkpoint_callback)



StopTrainingOnRewardThreshold


一時的な報酬(評価に対する平均エピソード報酬)のしきい値に達したら(つまりモデルが十分に良いときに)、トレーニングを停止します。EvalCallback と一緒に使用し、新しい最良のモデルによってトリガされるイベントを使用する必要があります。

import gym

from stable_baselines import SAC
from stable_baselines.common.callbacks import EvalCallback, StopTrainingOnRewardThreshold

# 評価用環境
eval_env = gym.make('Pendulum-v0')
# モデルが報酬のしきい値に達したときにトレーニングを停止する
callback_on_best = StopTrainingOnRewardThreshold(reward_threshold=-200, verbose=1)
eval_callback = EvalCallback(eval_env, callback_on_new_best=callback_on_best, verbose=1)

model = SAC('MlpPolicy', 'Pendulum-v0', verbose=1)
# タイムステップの数はほぼ無限(1e10)だが、
# 報酬のしきい値に達するとすぐにトレーニングを停止する
model.learn(int(1e10), callback=eval_callback)



EveryNTimesteps


n_steps ステップごとに子コールバックをトリガするイベントコールバック。

注意
PPO1やTRPOはMPIに依存するため、s_stepは2つのイベント感の下限となる。


import gym

from stable_baselines import PPO2
from stable_baselines.common.callbacks import CheckpointCallback, EveryNTimesteps

# これは CheckpointCallback (save_freq = 500) を定義することと同等
# checkpoint_callback は 500 ステップごとにトリガされる
checkpoint_on_event = CheckpointCallback(save_freq=1, save_path='./logs/')
event_callback = EveryNTimesteps(n_steps=500, callback=checkpoint_on_event)

model = PPO2('MlpPolicy', 'Pendulum-v0', verbose=1)

model.learn(int(2e4), callback=event_callback)



レガシ:機能的アプローチ


警告
以下書かれているコールバックの実装方法は、オブジェクト指向アプローチを支持するため
廃止となった。
(以下略)


----


本読んだときのコールバック、たしかに実装がダサかった。

OpenAI Gymの環境クラスって、ビジネスアプリケーションの設計アプローチで書かれている。理由はシミュレータが業務アプリと科学技術計算のファサードだから。

そこが理解できるかできないかで、強化学習をさくっと一般業務アプリケーションへ適用できるかどうかのスピードが変わる。

 とはいえトレーニング実装は、いわゆる機械学習ライブラリ側の責務なので、無理にオブジェクト指向にシなくてもいいと思っている。..けど、実装しやすいPyTorchにTensorFlowが押されているのって、科学技術計算指向脳で設計されいたライブラリだからなので、今の風潮はこっちが正解なのかもしれない..

 

..まあ、使わせて頂く見としては、どうでもいいけど..

Google Colab相当の環境をローカルPC上のDocker Composeで実現する

 Docker Composeの有料版に、お金を払いたくない ^H^Hが使えない ため古いゲーミングPC(GeForce3080)にDocker Composeを入れてその上のコンテナでGoogle Colab相当のノート操作ができないかためしてみた: コンテナイメージとして a...