「JAXにおけるディープ強化学習の優しい入門」
「美とファッションの魔法:魅力的な入門ガイド」
1秒未満でDQNを使用したCartPole環境の解決方法
Waymoの自律タクシーやDeepMindの超人間チェスプレーヤーエージェントなど、最近の強化学習(RL)の進展は、ニューラルネットワークや勾配最適化手法などのディープラーニングコンポーネントと組み合わせて、古典的なRLを補完しています。
以前の記事で紹介した基礎とコーディングの原則に基づいて、OpenAIのCartPole環境を解決するために、ディープQネットワーク(DQN)とリプレイバッファの実装方法を発見し、学習します。それを実現するために、JAXを使用して1秒未満で解決します!
JAX、ベクトル化された環境、Q学習の紹介は、次の記事を参照してください:
JAXを使用してRL環境をベクトル化および並列化し、光速でQ学習を行う
GridWorld環境をベクトル化し、CPU上で30つのQ学習エージェントを並列にトレーニングする、180万ステップ/秒の方法を学ぶ
towardsdatascience.com
ディープラーニングのための私たちのフレームワークは、DeepMindのHaikuライブラリを選びました。これは、最近Transformersの文脈で紹介しました:
JAXとHaikuを使用してゼロからTransformerエンコーダを実装する 🤖
Transformerの基本的なビルディングブロックの理解
towardsdatascience.com
この記事では、以下のセクションをカバーします:
- なぜディープRLが必要なのか?
- ディープQネットワークの理論と実践
- リプレイバッファ
- CartPole環境をJAXに翻訳する
- 効率的なトレーニングループを記述するためのJAXの方法
いつものように、この記事で紹介されたすべてのコードはGitHubで入手できます:
GitHub – RPegoud/jym: RLアルゴリズムとベクトル化された環境のためのJAX実装
RLアルゴリズムとベクトル化された環境のためのJAX実装 – GitHub – RPegoud/jym: RLアルゴリズムとベクトル化された環境のためのJAX実装
github.com
なぜディープRLが必要なのか?
以前の記事では、時系列差分学習アルゴリズム、特にQ学習を紹介しました。
要するに、Q学習はオフポリシーアルゴリズム(ターゲットポリシーは意思決定に使用されるポリシーではない)であり、明示的なQテーブル、つまり状態に対応する行動価値のマッピングを維持し、更新します。
Q-学習は、離散的な行動空間と制約のある観測空間を持つ環境に対して実用的な解決策ですが、より複雑な環境に対してはスケーリングが難しいという課題があります。実際、Qテーブルを作成するには行動と観測空間を定義する必要があります。
例えば、自動運転の場合、観測空間はカメラ映像や他のセンサー入力から派生した無限の潜在的な構成要素で構成されています。一方、行動空間には、ハンドルの位置やブレーキとアクセルにかかる力のレベルなど、幅広いスペクトラムの要素が含まれます。
理論的には行動空間を離散化することができますが、可能な状態と行動の数が膨大なため、実世界のアプリケーションでは実用的なQテーブルではありません。
大きくて複雑な状態-行動空間で最適な行動を見つけるためには、強力な関数近似アルゴリズムが必要です。それがニューラルネットワークです。ディープ強化学習の場合、ニューラルネットワークはQテーブルの代わりとして使用され、大規模な状態空間によって導入される次元の呪いに対する効率的な解決策を提供します。さらに、明示的に観測空間を定義する必要もありません。
DQN(Deep Q-Networks)とリプレイバッファ
DQNでは、「オンライン」ネットワークと「ターゲット」ネットワークの2種類のニューラルネットワークを並列して使用し、まず「オンライン」ネットワークはQ値の予測と意思決定に使用されます。一方、「ターゲット」ネットワークは、オンラインネットを評価するための安定したQターゲットを作成するために使用されます。
Q学習と同様に、DQNエージェントはact
とupdate
の2つの関数で定義されます。
Act
act
関数は、Q値に関してε-グリーディポリシーを実装し、オンラインニューラルネットワークによって推定されたQ値に基づいて行動を選択します。つまり、与えられた状態に対して最大の予測Q値に対応する行動を一定の確率でランダムに行います。
Q学習では各ステップの後にQテーブルを更新することを覚えているかもしれませんが、ディープラーニングでは一般的には勾配降下法を使用してバッチの入力に対して更新を計算することが一般的です。
このため、DQNはstate, action, reward, next_state, done_flag
という経験をリプレイバッファに保存します。ネットワークのトレーニングでは、このバッファから一連の経験をサンプリングし、最後の経験だけを使用する代わりにバッチ処理します(詳細はリプレイバッファのセクションで説明します)。
以下はDQNのアクション選択部分のJAX実装です:
このスニペットの唯一の繊細な点は、model
属性には通常のPyTorchやTensorFlowのように内部パラメータが含まれていないことです。
ここでのモデルは、アーキテクチャを通過する順方向のパスを表す関数であり、変更可能な重みが外部に保持され、引数として渡されることを説明しています。これがself
引数を静的に(モデルは他のクラス属性と同様に状態を持たないため)渡してjit
を使用できる理由です。
アップデート
update
関数は、ネットワークのトレーニングを担当しています。これは、平均二乗誤差(MSE)損失を計算します。この損失は、時間差(TD)エラーに基づいています。
この損失関数では、θはオンラインネットワークのパラメータを表し、θ−はターゲットネットワークのパラメータを表します。ターゲットネットワークのパラメータは、Nステップごとにオンラインネットワークのパラメータに設定され、チェックポイントのような役割を果たします(Nはハイパーパラメータです)。
このパラメータの分離(現在のQ値にはθ、ターゲットQ値にはθ−を使用する)は、トレーニングの安定化において重要です。
両方のパラメータに同じ値を使用すると、移動する目標を目指しているようなものであり、ネットワークの更新によってターゲットの値がすぐに変動してしまうでしょう。一定のステップでθ−を定期的に更新することにより(つまり、これらのパラメータを一定のステップでフリーズすることにより)、オンラインネットワークが学習を継続する間に安定したQターゲットを確保します。
最後に、(1-done)の項は、終端状態に対するターゲットを調整します。実際に、エピソードが終了した場合(つまり、’done’が1と等しい場合)、次の状態は存在しません。したがって、次の状態のQ値は0に設定されます。
DQNのアップデート関数を実装するには、少し複雑ですが、以下のように分解できます:
- まず、
_loss_fn
関数は、前述の二乗誤差を単一のエクスペリエンスに対して実装します。 - 次に、
_batch_loss_fn
は_loss_fn
をラップし、vmap
を適用して一括のエクスペリエンスに対して損失関数を適用します。そして、このバッチに対する平均エラーを返します。 - 最後に、
update
は、損失関数に対するオンラインネットワークのパラメータ、ターゲットネットワークのパラメータ、およびバッチのエクスペリエンスに対して、その勾配を計算する最終層として機能します。それから、最適化のために一般的に使用されるJAXライブラリであるOptaxを使用して、オンラインパラメータを更新します。
リプレイバッファと同様に、モデルとオプティマイザは外部の状態を変更する純粋な関数です。以下の行は、この原則を良い例として示しています:
updates, optimizer_state = optimizer.update(grads, optimizer_state)
これはまた、パラメータが外部で格納および更新されるため、オンラインネットワークとターゲットネットワークの両方に単一のモデルを使用できる理由も説明しています。
# ターゲットネットワークの予測self.model.apply(target_net_params, None, state)# オンラインネットワークの予測self.model.apply(online_net_params, None, state)
文脈として、この記事で使用するモデルは、次のように定義されたマルチレイヤーパーセプトロンです:
N_ACTIONS = 2NEURONS_PER_LAYER = [64, 64, 64, N_ACTIONS]online_key, target_key = vmap(random.PRNGKey)(jnp.arange(2) + RANDOM_SEED)@hk.transformdef model(x): # シンプルなマルチレイヤーパーセプトロン mlp = hk.nets.MLP(output_sizes=NEURONS_PER_LAYER) return mlp(x)online_net_params = model.init(online_key, jnp.zeros((STATE_SHAPE,)))target_net_params = model.init(target_key, jnp.zeros((STATE_SHAPE,)))prediction = model.apply(online_net_params, None, state)
リプレイバッファ
まず、リプレイバッファについて詳しく見てみましょう。リプレイバッファは強化学習で広く使用され、さまざまな理由から以下の利点があります:
- 一般化:リプレイバッファからサンプリングすることで、連続した経験間の相関関係を破壊し、順番を入れ替えます。これにより、特定の経験のシーケンスに過剰適合することを避けます。
- 多様性:サンプリングは直近の経験に限定されないため、更新の分散が低くなり、最新の経験に過剰適合するのを防ぎます。
- サンプル効率の向上:各経験はバッファから複数回サンプリングできるため、モデルは個々の経験からより多くの学習を行うことができます。
最後に、リプレイバッファのためにいくつかのサンプリング方法を使用できます:
- 一様サンプリング:経験は一様にランダムにサンプリングされます。このタイプのサンプリングは実装が簡単であり、経験が収集されたタイムステップに依存せずにモデルが学習できます。
- 優先サンプリング:このカテゴリには、優先順位付き経験リプレイ(”PER”、Schaul et al. 2015)や勾配経験リプレイ(”GER”、Lahire et al., 2022)など、さまざまなアルゴリズムが含まれます。これらの方法は、経験の「学習ポテンシャル」に関連するメトリックに基づいて経験の選択を優先的に行います(PERの場合はTDエラーの振幅、GERの場合は経験の勾配のノルム)。
簡単のため、この記事では一様なリプレイバッファを実装します。しかし、優先サンプリングについても将来的に詳しく解説する予定です。
約束どおり、一様なリプレイバッファは実装が非常に簡単ですが、JAXと関数型プログラミングの使用に関連するいくつかの複雑さがあります。常にJAXでは、副作用のない純粋な関数を扱う必要があります。つまり、バッファを内部状態を持つクラスインスタンスとして定義することは許されません。
代わりに、buffer_state
という辞書を初期化し、キーを事前定義された形状の空の配列にマッピングします。JAXではコードをXLAにジットコンパイルする際に、定数サイズの配列が必要です。
buffer_state = { "states": jnp.empty((BUFFER_SIZE, STATE_SHAPE), dtype=jnp.float32), "actions": jnp.empty((BUFFER_SIZE,), dtype=jnp.int32), "rewards": jnp.empty((BUFFER_SIZE,), dtype=jnp.int32), "next_states": jnp.empty((BUFFER_SIZE, STATE_SHAPE), dtype=jnp.float32), "dones": jnp.empty((BUFFER_SIZE,), dtype=jnp.bool_),}
リプレイバッファの状態に対話するためにUniformReplayBuffer
クラスを使用します。このクラスには次の2つのメソッドがあります:
add
:経験タプルを展開し、その要素を特定のインデックスにマッピングします。idx = idx % self.buffer_size
は、バッファがいっぱいの場合、新しい経験を追加すると古いものが上書きされるようにします。sample
:一様なランダム分布からランダムなインデックスのシーケンスをサンプリングします。シーケンスの長さはbatch_size
によって設定され、インデックスの範囲は[0, current_buffer_size-1]
です。これにより、バッファがまだいっぱいになる前に空の配列をサンプリングしないようにします。最後に、JAXのvmap
とtree_map
を組み合わせて、経験のバッチを返します。
CartPole環境をJAXに変換する
DQNエージェントがトレーニングに使用できる状態になったので、以前の記事で紹介されたフレームワークと同じものを使用して、ベクトル化されたCartPole環境を簡単に実装します。CartPoleは、大きな連続した観測空間を持つ制御環境であり、DQNのテストに適しています。
プロセスは非常に簡単で、OpenAIのGymnasiumの実装を再利用しながら、PythonやNumPyの代替ではなく、JAXの配列とlax制御フローを使用するようにします。
# Pythonの実装force = self.force_mag if action == 1 else -self.force_mag# Jaxの実装force = lax.select(jnp.all(action) == 1, self.force_mag, -self.force_mag) )# Pythoncostheta, sintheta = math.cos(theta), math.sin(theta)# Jaxcos_theta, sin_theta = jnp.cos(theta), jnp.sin(theta)# Pythonif not terminated: reward = 1.0...else: reward = 0.0# Jaxreward = jnp.float32(jnp.invert(done))
簡潔さのために、完全な環境コードはこちらで利用できます:
jym/src/envs/control/cartpole.py at main · RPegoud/jym
JAXの実装によるRLアルゴリズムとベクトル化された環境 – jym/src/envs/control/cartpole.py at main ·…
github.com
効率的なトレーニングループを書くためのの方法
DQNの実装の最後の部分はトレーニングループ(ロールアウトとも呼ばれる)です。前の記事で説明したように、JAXの高速性を活かすために特定の形式を尊重する必要があります。
ロールアウト関数は最初は複雑に見えるかもしれませんが、その複雑さの大部分は純粋に構文上のものであり、ほとんどの構築ブロックはすでにカバー済みです。次に、擬似コードの手順を示します:
1. 初期化: * 各タイムステップの状態、アクション、報酬、完了フラグを格納する空の配列を作成します。ダミーの配列でネットワークとオプティマイザを初期化します。 * 初期化されたすべてのオブジェクトをvalタプルに包みます。 2. トレーニングループ(iステップの反復): * valタプルを展開する * (オプション)減衰関数を使用してεを減衰させる * 状態とモデルのパラメータに応じてアクションを選択する * 環境ステップを実行し、次の状態、報酬、完了フラグを観測する * 経験タプル(状態、アクション、報酬、新しい状態、完了)を作成し、リプレイバッファに追加する * 現在のバッファサイズに応じて一定の経験をサンプリングする(つまり、非ゼロの値を持つ経験のみからサンプリングする) * 経験バッチを使用してモデルパラメータを更新する * Nステップごとにターゲットネットワークの重みを更新する(target_params = online_paramsに設定する) * 現在のエピソードの経験の値を保存し、更新された `val` タプルを返す
今度はDQNを20,000ステップ実行してパフォーマンスを観察できます。約45エピソード後、エージェントは一貫して100ステップ以上のポールのバランスを取ることができるまっとうなパフォーマンスを達成します。
緑色の棒は、エージェントが200ステップ以上のポールのバランスを取ることに成功し、環境の解決を示しています。特に、エージェントは51エピソード目に393ステップの記録を樹立しました。
DQNのパフォーマンスレポート(著者作成)
20,000の訓練ステップは、わずか1秒弱で実行され、1秒あたり15,807ステップ(シングルCPU)の速度で実行されました!
これらのパフォーマンスは、JAXの印象的なスケーリング能力を示しており、最小限のハードウェア要件で大規模な並列化された実験を実行できるようにします。
20,000の繰り返しを実行中: 100%|██████████| 20000/20000 [00:01<00:00, 15807.81it/s]
次の記事では、統計的に有意な実験とハイパーパラメータの探索を実行するための並列化されたロールアウト手順について詳しく説明します!
それまでの間、このノートブックを使用して実験を再現し、ハイパーパラメータをいじってみてください:
jym/notebooks/control/cartpole/dqn_cartpole.ipynb at main · RPegoud/jym
RLアルゴリズムとベクトル化された環境のJAXによる実装 – jym/notebooks/control/cartpole/dqn_cartpole.ipynb at…
github.com
結論
いつものように、ここまでお読みいただきありがとうございます!この記事がJAXでのDeep RLの基本的な紹介に役立ったことを願っています。もし記事の内容に関する質問やフィードバックがございましたら、遠慮なくお知らせください。いつでもちょっとしたおしゃべりの相手になりますよ😉
次回までお待ちください👋
クレジット:
- Cartpole Gif、OpenAI Gymnasiumライブラリ(MITライセンス)
We will continue to update VoAGI; if you have any questions or suggestions, please contact us!
Was this article helpful?
93 out of 132 found this helpful
Related articles