## 前提条件 このレッスンでは、**OpenAI Gym**というライブラリを使用して、さまざまな**環境**をシミュレーションします。このレッスンのコードはローカル環境(例: Visual Studio Code)で実行することができ、その場合シミュレーションは新しいウィンドウで開きます。オンラインでコードを実行する場合は、[こちら](https://towardsdatascience.com/rendering-openai-gym-envs-on-binder-and-google-colab-536f99391cc7)に記載されているように、コードにいくつかの調整が必要になる場合があります。 ## OpenAI Gym 前回のレッスンでは、ゲームのルールと状態が自分で定義した`Board`クラスによって提供されていました。今回は、バランスを取るポールの物理をシミュレーションする特別な**シミュレーション環境**を使用します。強化学習アルゴリズムをトレーニングするための最も人気のあるシミュレーション環境の1つが、[Gym](https://gym.openai.com/)と呼ばれるもので、[OpenAI](https://openai.com/)によって管理されています。このGymを使用することで、カートポールのシミュレーションからアタリゲームまで、さまざまな**環境**を作成できます。 > **Note**: OpenAI Gymで利用可能な他の環境は[こちら](https://gym.openai.com/envs/#classic_control)で確認できます。 まず、Gymをインストールし、必要なライブラリをインポートしましょう(コードブロック1): ```python import sys !{sys.executable} -m pip install gym import gym import matplotlib.pyplot as plt import numpy as np import random ``` ## 演習 - カートポール環境を初期化する カートポールのバランス問題に取り組むには、対応する環境を初期化する必要があります。各環境には以下が関連付けられています: - **観測空間**: 環境から受け取る情報の構造を定義します。カートポール問題では、ポールの位置、速度、その他の値を受け取ります。 - **行動空間**: 可能な行動を定義します。この場合、行動空間は離散的で、**左**と**右**の2つの行動で構成されています。(コードブロック2) 1. 初期化するには、以下のコードを入力してください: ```python env = gym.make("CartPole-v1") print(env.action_space) print(env.observation_space) print(env.action_space.sample()) ``` 環境がどのように動作するかを確認するために、100ステップの短いシミュレーションを実行してみましょう。各ステップで、`action_space`からランダムに選択した行動を提供します。 1. 以下のコードを実行して、結果を確認してください。 ✅ このコードはローカルのPythonインストールで実行することを推奨します!(コードブロック3) ```python env.reset() for i in range(100): env.render() env.step(env.action_space.sample()) env.close() ``` 以下のような画像が表示されるはずです: ![バランスを取らないカートポール](../../../../8-Reinforcement/2-Gym/images/cartpole-nobalance.gif) 1. シミュレーション中に、行動を決定するために観測値を取得する必要があります。実際には、`step`関数は現在の観測値、報酬関数、およびシミュレーションを続行するかどうかを示す終了フラグを返します。(コードブロック4) ```python env.reset() done = False while not done: env.render() obs, rew, done, info = env.step(env.action_space.sample()) print(f"{obs} -> {rew}") env.close() ``` ノートブックの出力で以下のような結果が表示されるはずです: ```text [ 0.03403272 -0.24301182 0.02669811 0.2895829 ] -> 1.0 [ 0.02917248 -0.04828055 0.03248977 0.00543839] -> 1.0 [ 0.02820687 0.14636075 0.03259854 -0.27681916] -> 1.0 [ 0.03113408 0.34100283 0.02706215 -0.55904489] -> 1.0 [ 0.03795414 0.53573468 0.01588125 -0.84308041] -> 1.0 ... [ 0.17299878 0.15868546 -0.20754175 -0.55975453] -> 1.0 [ 0.17617249 0.35602306 -0.21873684 -0.90998894] -> 1.0 ``` シミュレーションの各ステップで返される観測ベクトルには以下の値が含まれています: - カートの位置 - カートの速度 - ポールの角度 - ポールの回転速度 1. これらの数値の最小値と最大値を取得してください。(コードブロック5) ```python print(env.observation_space.low) print(env.observation_space.high) ``` また、各シミュレーションステップでの報酬値が常に1であることに気付くかもしれません。これは、目標ができるだけ長く生き残ること、つまりポールをできるだけ垂直に保つことだからです。 ✅ 実際、カートポールのシミュレーションは、100回連続の試行で平均報酬が195に達した場合に解決されたと見なされます。 ## 状態の離散化 Q-Learningでは、各状態で何をすべきかを定義するQ-テーブルを構築する必要があります。これを行うためには、状態を**離散的**にする必要があります。つまり、有限の離散値を含む必要があります。そのため、観測値を**離散化**し、有限の状態セットにマッピングする必要があります。 これを行う方法はいくつかあります: - **ビンに分割する**: 特定の値の範囲がわかっている場合、その範囲をいくつかの**ビン**に分割し、その値を属するビン番号に置き換えることができます。これはnumpyの[`digitize`](https://numpy.org/doc/stable/reference/generated/numpy.digitize.html)メソッドを使用して行うことができます。この場合、選択したビンの数に応じて状態サイズを正確に把握できます。 ✅ 線形補間を使用して値を有限の範囲(例えば、-20から20)に持ってきて、丸めることで整数に変換することができます。この方法では、状態サイズを完全に制御することはできませんが、特に入力値の正確な範囲がわからない場合に便利です。例えば、この場合、4つの値のうち2つは値の上限/下限が定義されていないため、無限の状態数になる可能性があります。 この例では、2番目の方法を使用します。後で気付くかもしれませんが、上限/下限が定義されていないにもかかわらず、それらの値が特定の有限範囲外になることはめったにありません。そのため、極端な値を持つ状態は非常にまれです。 1. 以下は、モデルからの観測値を受け取り、4つの整数値のタプルを生成する関数です。(コードブロック6) ```python def discretize(x): return tuple((x/np.array([0.25, 0.25, 0.01, 0.1])).astype(np.int)) ``` 1. ビンを使用した別の離散化方法も試してみましょう。(コードブロック7) ```python def create_bins(i,num): return np.arange(num+1)*(i[1]-i[0])/num+i[0] print("Sample bins for interval (-5,5) with 10 bins\n",create_bins((-5,5),10)) ints = [(-5,5),(-2,2),(-0.5,0.5),(-2,2)] # intervals of values for each parameter nbins = [20,20,10,10] # number of bins for each parameter bins = [create_bins(ints[i],nbins[i]) for i in range(4)] def discretize_bins(x): return tuple(np.digitize(x[i],bins[i]) for i in range(4)) ``` 1. 短いシミュレーションを実行し、これらの離散化された環境値を観察してみましょう。`discretize`と`discretize_bins`の両方を試して、違いがあるかどうか確認してください。 ✅ `discretize_bins`はビン番号を返しますが、これは0ベースです。そのため、入力変数の値が0付近の場合、範囲の中央(10)の番号を返します。一方、`discretize`では出力値の範囲を気にせず、負の値を許容しているため、状態値はシフトされず、0は0に対応します。(コードブロック8) ```python env.reset() done = False while not done: #env.render() obs, rew, done, info = env.step(env.action_space.sample()) #print(discretize_bins(obs)) print(discretize(obs)) env.close() ``` ✅ 環境の実行を確認したい場合は、`env.render`で始まる行のコメントを解除してください。そうでない場合は、バックグラウンドで実行することができ、これにより高速化されます。この「非表示」実行をQ-Learningプロセス中に使用します。 ## Q-テーブルの構造 前回のレッスンでは、状態は0から8までの単純な数字のペアであり、そのためQ-テーブルを形状が8x8x2のnumpyテンソルで表現するのが便利でした。ビンによる離散化を使用する場合、状態ベクトルのサイズも既知であるため、同じアプローチを使用して状態を形状が20x20x10x10x2の配列で表現することができます(ここで2は行動空間の次元であり、最初の次元は観測空間内の各パラメータに使用するビンの数に対応します)。 ただし、観測空間の正確な次元がわからない場合があります。`discretize`関数の場合、元の値の一部が制限されていないため、状態が特定の制限内に留まることを保証することはできません。そのため、少し異なるアプローチを使用し、Q-テーブルを辞書で表現します。 1. ペア*(state,action)*を辞書のキーとして使用し、その値がQ-テーブルのエントリ値に対応します。(コードブロック9) ```python Q = {} actions = (0,1) def qvalues(state): return [Q.get((state,a),0) for a in actions] ``` ここでは、`qvalues()`という関数も定義しており、指定された状態に対応するすべての可能な行動に対するQ-テーブル値のリストを返します。Q-テーブルにエントリが存在しない場合、デフォルト値として0を返します。 ## Q-Learningを始めましょう さて、ピーターにバランスを取る方法を教える準備が整いました! 1. まず、いくつかのハイパーパラメータを設定しましょう。(コードブロック10) ```python # hyperparameters alpha = 0.3 gamma = 0.9 epsilon = 0.90 ``` ここで、`alpha`は**学習率**であり、各ステップでQ-テーブルの現在の値をどの程度調整するべきかを定義します。前回のレッスンでは1から始め、トレーニング中に`alpha`を低い値に減少させました。この例では簡単のために一定に保ちますが、後で`alpha`値を調整して実験することができます。 `gamma`は**割引率**であり、現在の報酬よりも将来の報酬をどの程度優先するべきかを示します。 `epsilon`は**探索/活用率**であり、探索を優先するか活用を優先するかを決定します。このアルゴリズムでは、`epsilon`の割合で次の行動をQ-テーブル値に従って選択し、残りの割合でランダムな行動を実行します。これにより、これまで見たことのない探索空間の領域を探索することができます。 ✅ バランスを取るという観点では、ランダムな行動(探索)を選択することは、間違った方向へのランダムなパンチのようなものであり、ポールはその「ミス」からバランスを回復する方法を学ぶ必要があります。 ### アルゴリズムを改善する 前回のレッスンからアルゴリズムを2つ改善することができます: - **平均累積報酬を計算する**: 複数のシミュレーションにわたって平均累積報酬を計算します。5000回のイテレーションごとに進捗を表示し、その期間の平均累積報酬を計算します。これにより、195ポイント以上を獲得した場合、問題が解決されたと見なすことができます。 - **最大平均累積結果を計算する**: `Qmax`を計算し、その結果に対応するQ-テーブルを保存します。トレーニングを実行すると、平均累積結果が時々低下し始めることに気付くでしょう。この場合、状況を悪化させる値でQ-テーブルの既に学習済みの値を「破壊」する可能性があります。 1. 各シミュレーションで累積報酬を`rewards`ベクトルに収集し、後でプロットします。(コードブロック11) ```python def probs(v,eps=1e-4): v = v-v.min()+eps v = v/v.sum() return v Qmax = 0 cum_rewards = [] rewards = [] for epoch in range(100000): obs = env.reset() done = False cum_reward=0 # == do the simulation == while not done: s = discretize(obs) if random.random() Qmax: Qmax = np.average(cum_rewards) Qbest = Q cum_rewards=[] ``` これらの結果から以下のことがわかります: - **目標に近い**: 100回以上の連続したシミュレーションで195の累積報酬を達成する目標に非常に近づいているか、実際に達成している可能性があります。たとえ小さい数値を得たとしても、5000回の実行で平均化しているため、正式な基準では100回の実行のみが必要です。 - **報酬が低下し始める**: 時々報酬が低下し始めることがあります。これは、状況を悪化させる値でQ-テーブルの既に学習済みの値を「破壊」する可能性があることを意味します。 この観察は、トレーニングの進捗をプロットするとより明確に見えます。 ## トレーニング進捗のプロット トレーニング中に、各イテレーションで累積報酬値を`rewards`ベクトルに収集しました。以下は、イテレーション番号に対してプロットした結果です: ```python plt.plot(rewards) ``` ![生の進捗](../../../../8-Reinforcement/2-Gym/images/train_progress_raw.png) このグラフからは何も判断できません。これは、確率的なトレーニングプロセスの性質上、トレーニングセッションの長さが大きく異なるためです。このグラフをより理解しやすくするために、例えば100回の実験にわたって**移動平均**を計算します。これを`np.convolve`を使用して便利に行うことができます。(コードブロック12) ```python def running_average(x,window): return np.convolve(x,np.ones(window)/window,mode='valid') plt.plot(running_average(rewards,100)) ``` ![トレーニング進捗](../../../../8-Reinforcement/2-Gym/images/train_progress_runav.png) ## ハイパーパラメータの調整 学習をより安定させるために、トレーニング中にいくつかのハイパーパラメータを調整することが理にかなっています。特に: - **学習率**`alpha`については、1に近い値から始め、その後パラメータを徐々に減少させることができます。時間が経つにつれて、Q-テーブルに良い確率値が得られるようになり、それらを完全に上書きするのではなく、わずかに調整するべきです。 - **epsilonを増加させる**: `epsilon`を徐々に増加させ、探索を減らし、活用を増やすことができます。低い値の`epsilon`から始め、ほぼ1に近づけるのが理にかなっているでしょう。 > **タスク 1**: ハイパーパラメータの値を調整して、より高い累積報酬を得られるか試してみましょう。195を超えていますか? > **タスク 2**: 問題を正式に解決するには、100回連続の実行で平均報酬195を達成する必要があります。トレーニング中にこれを測定し、問題を正式に解決したことを確認してください! ## 結果を実際に確認する トレーニングされたモデルがどのように動作するかを実際に見るのは興味深いでしょう。同じ行動選択戦略をトレーニング中と同様に使用し、Q-Tableの確率分布に従ってサンプリングしてシミュレーションを実行してみましょう: (コードブロック 13) ```python obs = env.reset() done = False while not done: s = discretize(obs) env.render() v = probs(np.array(qvalues(s))) a = random.choices(actions,weights=v)[0] obs,_,done,_ = env.step(a) env.close() ``` 以下のような結果が表示されるはずです: ![バランスを取るカートポール](../../../../8-Reinforcement/2-Gym/images/cartpole-balance.gif) --- ## 🚀チャレンジ > **タスク 3**: ここでは、Q-Tableの最終版を使用していましたが、これが最良のものとは限りません。最もパフォーマンスの良いQ-Tableを`Qbest`変数に保存していることを思い出してください!`Qbest`を`Q`にコピーして、最良のQ-Tableを使用した場合の違いを確認してみてください。 > **タスク 4**: ここでは各ステップで最良の行動を選択するのではなく、対応する確率分布に基づいてサンプリングしていました。Q-Tableの値が最も高い行動を常に選択する方が理にかなっているでしょうか?これは`np.argmax`関数を使用して、Q-Table値が最も高い行動番号を見つけることで実現できます。この戦略を実装して、バランスが改善されるかどうか確認してください。 ## [講義後のクイズ](https://ff-quizzes.netlify.app/en/ml/) ## 課題 [マウンテンカーをトレーニングする](assignment.md) ## 結論 私たちは、ゲームの望ましい状態を定義する報酬関数を提供し、探索空間を賢く探索する機会を与えることで、エージェントをトレーニングして良い結果を達成する方法を学びました。Q-Learningアルゴリズムを離散環境と連続環境のケースで適用し、離散的な行動を使用して成功を収めました。 行動状態も連続的であり、観測空間がより複雑である場合、例えばAtariゲーム画面の画像のような状況を研究することも重要です。そのような問題では、良い結果を達成するためにニューラルネットワークのようなより強力な機械学習技術を使用する必要があることがよくあります。これらのより高度なトピックは、今後のより高度なAIコースのテーマとなります。 --- **免責事項**: この文書は、AI翻訳サービス [Co-op Translator](https://github.com/Azure/co-op-translator) を使用して翻訳されています。正確性を期すよう努めておりますが、自動翻訳には誤りや不正確な表現が含まれる可能性があります。元の言語で記載された原文を公式な情報源としてご参照ください。重要な情報については、専門の人間による翻訳を推奨します。本翻訳の利用に起因する誤解や誤認について、当社は一切の責任を負いません。