PettingZooのチュートリアル「SB3: PPO for Knights-Archers-Zombies」を動かしてみる
Pythonのマルチエージェント強化学習ライブラリPettingZooのチュートリアルを触ってみました。
#python #強化学習 #pettingzoo #MARL
概要
マルチエージェント強化学習(MARL)のライブラリ"PettingZoo"の公式チュートリアル「SB3: PPO for Knights-Archers-Zombies」を動かしながらPettingZooおよびそのラッパーであるSuperSuitの動きを見てみます。
インストール
Python3.10.11にインストールしました。
今回使用した各ライブラリのバージョンは下記のとおりです。
pettingzoo 1.24.3
stable_baselines3 2.3.2
SuperSuit 3.9.3
※この記事を書いた2024年9月6日時点ではpettingzooはPython3.12にはまだ対応していないようです。
pip install pettingzoo[butterfly]
pip install stable-aselines3[extra]
pip install supersuit
エラーが出てインストールできない場合は下記を試してみてください
setuptoolsのダウングレード
新しすぎるとインストールできない依存ライブラリがあるようです。
(私は65.5.0でインストールできました)マイクロソフトC++ビルドツールのインストール
こちらからBuird Tools for Visual Studio 2022をダウンロードしインストールします。
途中チェックを求められたら「C++によるデスクトップ開発」にチェックを入れ、あとはデフォルトでインストールします。
環境(Knights-Archers-Zombies)の詳細
画面上部から迫りくるゾンビをアーチャーの放つ矢とナイトの振り回すメイスで倒してゾンビが画面下端にたどり着かないようにするゲームです。
デフォルト設定ではアーチャー2人とナイト2人です。各々、ゾンビを倒すと1点の報酬を獲得できます。ゾンビが画面下端に1匹でもたどり着いてしまうとゲーム終了です。
画面中の座標は左上隅を(x=0, y=0)としてそれぞれ右方向・下方向を正の向きとしており、画面右下を(x=1, y=1)として正規化されています。
上記に留意しながら、parallel_envおよびvector_state=Trueの場合のObservationの中身を確認します。
エージェントごとのアクションを下記のようにdictで適当に定めて環境に入力した場合のobservationはエージェントごとの2次元ndarrayのdictとなります。
actions = {'archer_0': 0, 'archer_1': 5, 'knight_0': 4, 'knight_1': 2}
observations, rewards, terminations, truncations, infos = env.step(actions)
# observations
{
'archer_0': array([[...]]),
'archer_1': array([[...]]),
'knight_0': array([[...]]),
'knight_1': array([[...]])
}
それぞれの2次元ndarrayの中身は公式ドキュメントの説明にある通り各行ごとに、
1列目 :current agentとの距離(current agent自身の行は必ず0になる)
2,3列目:各agentとcurrent agentとの相対座標(current agent自身の行は自分の座標)
4,5列目:各agentの向き(上記座標系で表した単位ベクトル)
を表しています。
その他の返り値もobsevationと同様のdict形式となっています。
ここまでは簡単に理解できましたがsupersuitでラップされてからの動きの理解に少し手間取りました。。。
サンプルコード動作確認
ここからいよいよ公式チュートリアルのサンプルコードの中に入っていきます。stablebaselines3のPPOを用いて学習を行い、ランダムで行動させたエージェント(archer_0)と学習させたエージェント(archer_0以外)の報酬を比較しています。
train
supersuitを活用して学習を行っています。
最初見たときは何をやっているのかさっぱりでしたが、ライブラリの中を追いながら入出力を見ていくと環境を複数動かして効率的に学習を行っていて、なるほどよくできてるなと感心しました。
pettingzoo_env_to_vec_env_v1でラップすることで入力がdictでなくてもよくなり、またobservationが(agent数, N, 5)のテンソルになります。
さらにconcat_vec_envs_v1でラップするとその環境をenv_num個動かせます。この時の入力は[(agent数 * env_num)個のactionのリスト]で、observationは(agent数 * env_num, N, 5)のテンソルになります。
このあたりの詳細はsupersuitのPyPIの説明文に記載されているのでそちらも併せてご覧ください。
下記はサンプルコードのtrainの部分を一部追記修正したものです。
コメントアウト部を(デバッグモードで)動かしながら変数の中身を見ると非常によく動きがわかるのでおすすめです。
def train(env_fn, steps: int = 10_000, seed: int | None = 0, **env_kwargs):
env = env_fn.parallel_env(**env_kwargs)
env = ss.black_death_v3(env)
env.reset(seed=seed)
print(f"Starting training on {str(env.metadata['name'])}.")
"""
# テスト用のaction
a_1 = {'archer_0': 2, 'archer_1': 5, 'knight_0': 0, 'knight_1': 2}
a_2 = [2, 5, 0, 2]
a_3 = [2, 5, 0, 2, 1, 1, 1, 1]
# black_death_v3適用後の入出力
env.reset(seed=seed)
test_1 = env.step(a_1)
# (21, 5)
print(test_1[0]['archer_0'].shape)
# pettingzoo_env_to_vec_env_v1適用後の入出力
env = ss.pettingzoo_env_to_vec_env_v1(env)
env.reset(seed=seed)
test_2 = env.step(a_2)
# (4, 21, 5)
print(test_2[0].shape)
# concat_vec_envs_v1適用後の入出力
env = ss.concat_vec_envs_v1(env, 2, num_cpus=1, base_class="stable_baselines3")
env.reset()
test_3 = env.step(a_3)
# (8, 21, 5)
print(test_3[0].shape)
"""
env = ss.pettingzoo_env_to_vec_env_v1(env)
env = ss.concat_vec_envs_v1(env, 8, num_cpus=1, base_class="stable_baselines3")
model = PPO(MlpPolicy, env, verbose=1, batch_size=256)
model.learn(total_timesteps=steps)
model.save(f"{env.unwrapped.metadata.get('name')}_{time.strftime('%Y%m%d-%H%M%S')}")
print("Model has been saved.")
print(f"Finished training on {str(env.unwrapped.metadata['name'])}.")
env.close()
学習自体を記述している部分はmodel.learnの1行のみです。
また、PPO(..., tensorboard_log=(logフォルダのパス))とすることでtensorboardでログ情報を見ることもできます。
tensorboardはtensorboard --logdir (logフォルダのパス)で起動できます。
eval
こちらはsupersuitによるラッピングを使用していないため特に迷うところはないと思います。parallelとAECの違いはPettingZoo公式ドキュメントの説明が詳しいので割愛します。
env.possible_agents[0] ⇔ archer_0 だけランダムで行動させるようにして、archer_0とその他エージェントの報酬の値を比較することが目的です。
動かしてみた結果
サンプルコードをそのまま動かすと標準出力に下記のようなログが表示されます。
学習したアーチャー(archer_1)はランダム行動のアーチャー(archer_0)の2倍のゾンビが倒せていることがわかります。
最後に2ゲーム分実際の画面でのエージェントの動きを見ることができますが、やはりarcher_0(初期位置で一番左側のアーチャー)は明らかに他3人とは異なるランダム行動をしている様子が見受けられました。
・・・が、ランダムでやみくもに撃った矢が結構いい感じでゾンビに当たっていそうな感じも見受けられました\(^o^)/
もう少し学習を増やしたりするともっと顕著に差が出るのかもしれません。
Starting training on knights_archers_zombies_v10.
Using cpu device
------------------------------
| time/ | |
| fps | 1652 |
| iterations | 1 |
| time_elapsed | 39 |
| total_timesteps | 65536 |
------------------------------
-----------------------------------------
| time/ | |
| fps | 1512 |
| iterations | 2 |
| time_elapsed | 86 |
| total_timesteps | 131072 |
| train/ | |
| approx_kl | 0.009279132 |
| clip_fraction | 0.0826 |
| clip_range | 0.2 |
| entropy_loss | -1.78 |
| explained_variance | -0.304 |
| learning_rate | 0.0003 |
| loss | -0.0141 |
| n_updates | 10 |
| policy_gradient_loss | -0.0043 |
| value_loss | 0.0324 |
-----------------------------------------
Model has been saved.
Finished training on knights_archers_zombies_v10.
Starting evaluation on knights_archers_zombies_v10 (num_games=10, render_mode=None)
Avg reward: 3.0
Avg reward per agent, per game: {'archer_0': 0.4, 'archer_1': 0.8, 'knight_0': 0.0, 'knight_1': 0.0}
Full rewards: {'archer_0': 4, 'archer_1': 8, 'knight_0': 0, 'knight_1': 0}
Starting evaluation on knights_archers_zombies_v10 (num_games=2, render_mode=human)
Avg reward: 0.75
Avg reward per agent, per game: {'archer_0': 0.5, 'archer_1': 1.0, 'knight_0': 0.0, 'knight_1': 0.0}
Full rewards: {'archer_0': 1, 'archer_1': 2, 'knight_0': 0, 'knight_1': 0}
参考資料
この記事が気に入ったらサポートをしてみませんか?