Unity ML-Agentsで作ったターン制ゲームを鍛える

強化学習を使ったリバーシを鍛えました。

# 前提
### 環境
- Unity 2019.4.9f1 (LTS)
- ML Agents 1.3.0 (Preview)
- ML Agents Extension 0.0.1 (Preview)
- Anaconda Python 3.8
- Windows 10

### 記事の範囲
- 前回作成したリバーシで、トレーニング構成(`~.yaml`)を調整して思考を鍛えます。
    - ML-Agentsの基礎やリバーシについては、この記事では扱いません。
- 下敷きとなる記事
    - 「[Unity ML-Agentsを試してみた](https://qiita.com/tetr4lab/items/cb698b1171ae52430ba9)」
    - 「[Unity ML-Agentsでターン制ゲームを作る
](https://qiita.com/tetr4lab/items/b02eca793336ed63429f)」
    - リソースについては、上記の記事を参照してください。
- 公式ドキュメント
    - [Using TensorBoard to Observe Training (GitHub)](https://github.com/Unity-Technologies/ml-agents/blob/master/docs/Using-Tensorboard.md)
    - [Training Configuration File (GitHub)](https://github.com/Unity-Technologies/ml-agents/blob/master/docs/Training-Configuration-File.md)
    - [ML-Agents のセルフプレイを使用したインテリジェントな対戦相手のトレーニング (公式 Unity Blog)](https://blogs.unity3d.com/jp/2020/02/28/training-intelligent-adversaries-using-self-play-with-ml-agents/)
- その他の情報
    - [イロレーティング (Wikipedia)](https://ja.wikipedia.org/wiki/%E3%82%A4%E3%83%AD%E3%83%AC%E3%83%BC%E3%83%86%E3%82%A3%E3%83%B3%E3%82%B0)

# 鍛錬の方針
- 執筆者はド素人なので、理論的な考察なしに試行錯誤で進めます。
- エージェントのインスタンスは二つ作られ、それぞれは別のチームです。
    - 先手と後手を一局毎に交代させます。
    - これを行わない場合、弱くなりました。
- テンソルボード(TensorBoard)のSelf-play/ELO(イロレーティング)を指標とします。
- トレーニング構成ファイル(Training Configuration File)の項目に対して仮説を立て、`max_steps`=`10000000`(一日に二巡できる程度)として検証します。

# トレーニング構成ファイル (Training Configuration File)
以下の「説明」部分は、公式ドキュメントからの(ほぼ)機械翻訳です。

- 構成要素
    - `trainer_type`
        - 説明
            -  トレーナーのタイプ
            -  `ppo`または`sac`
        - 設定
            - 強化学習なので、`ppo`ですね。
    - `hyperparameters`
        - `batch_size`
            - 説明
                - 勾配降下の各反復における経験数。
                - これは常に`buffer_size`の何倍も小さくなければなりません。
                - 連続アクションスペース(`Continuous`)を使用している場合、この値は大きくする必要があります (およそ1000台)。
                - 離散アクションスペース(`Discrete`)を使用している場合、この値は小さくする必要があります (10台)。
                - 標準的な範囲:
                    - 連続-PPO: `512~5120`
                    - 連続-SAC: `128~1024`
                    - 離散-PPO、SAC: `32~512`
            - 検証
                - 離散アクションなので、小さめにします。
                - `32`でも`128`でも、ELOは上昇するが弱くなります。
                -  ⇒ `64`
        - `buffer_size`
            - 説明
                - PPO
                    - ポリシーモデルを更新する前に収集する経験数。
                    - モデルの学習または更新を行う前に収集する必要がある経験の数に対応します。
                    - これは、`batch_size`よりも数倍大きくする必要があります。
                        - 通常、大きな`buffer_size`は、より安定したトレーニング更新に対応します。
                    - デフォルト: `10240`
                    - 標準的な範囲: `2048~409600`
                - SAC
                    - 経験バッファーの最大サイズ-エピソードよりも数千倍長いため、SACは古い経験と新しい経験から学習できます。
                    - デフォルト: `50000`
                    - 標準的な範囲: `50000~1000000`
            - 設定
                - 大きくて悪いことはないようなので、大きめにしておきます。
                -  ⇒ `20480`
        - `learning_rate`
            - 説明
                - 勾配降下法の初期学習率。
                - 各勾配降下法の更新ステップの強度に対応します。
                - トレーニングが不安定で、報酬が常に増加しない場合は、通常、これを減らす必要があります。
                - デフォルト: `3e-4`
                - 標準的な範囲: `1e-5~1e-3`
            - 設定
                - `3e-4`から`7.5e-5`にすると、ELOが若干上がり僅かに強くなります。
                - `1e-4`だと、`7.5e-5`よりELOが僅かに上がり強くなります。
                - `3e-5`にすると、`3e-4`に比べELOは下がり弱くなります。
                - `2e-4`だと、`1e-4`より僅かに弱くなりました。
                -  ⇒ `1e-4`
        - `beta`
            - 説明
                - 「よりランダム」なポリシーにする、エントロピー正則化の強さ。
                - これにより、エージェントはトレーニング中にアクションスペースを適切に探索できます。
                - これを増やすと、より多くのランダムアクションが実行されます。
                - これは、報酬の増加とともにエントロピー(TensorBoardから測定可能)がゆっくりと減少するように調整する必要があります。
                - エントロピーが急激に低下する場合は、`beta`を増やします。
                - エントロピーの低下が遅すぎる場合は、`beta`を減らします。
                - デフォルト: `5.0e-3`
                - 標準的な範囲: `1e-4~1e-2`
            - 検証
                - 大きめにして奇手を打たせた方が学びは多くなるのかもと試しましたが、弱くなりました。
                -  ⇒ `5.0e-3`
        - `epsilon`
            - 説明
                - トレーニング中にポリシーがどれだけ迅速に進化するかに影響します。
                - 勾配降下法更新中の新旧のポリシー間の許容可能な相違のしきい値に対応します。
                - この値を小さく設定すると、更新がより安定しますが、トレーニングプロセスが遅くなります。
                - デフォルト: `0.2`
                - 標準的な範囲: `0.1~0.3`
            - 設定
                - 解らないのでデフォルトに。
                -  ⇒ `0.2`
        - `lambd`
            - 説明
                - Generalized Advantage Estimate (GAE)を計算するときに使用される正則化パラメーター(lambda)。
                - これは、更新された値の見積もりを計算するときに、エージェントが現在の値の見積もりにどれだけ依存しているかと考えることができます。
                - 低い値は、(より高いバイアスになる可能性がある)現在値の見積もりにさらに依存することになり、高い値は、(分散が大きくなる可能性がある)環境で受け取る実際の報酬にさらに依存することになります。
                - パラメータは2つの間のトレードオフを提供し、適切な値はより安定したトレーニングプロセスにつながります。
                - デフォルト: `0.95`
                - 標準的な範囲: `0.9~0.95`
            - 検証
                - `0.9`にすると、ELOが下がって弱くなりました。
                -  ⇒ `0.95`
        - `num_epoch`
            - 説明
                - 勾配降下最適化を実行するときに経験バッファーを通過するパスの数。
                - `batch_size`が大きいほど、これを許容できます。これを減らすと、学習が遅くなりますが、より安定した更新が保証されます。
                - デフォルト: `3`
                - 標準的な範囲: `3~10`
            - 設定
                - 解らないのでデフォルトに。
                -  ⇒ `3`
        - `learning_rate_schedule`
            - 説明
                - 時間とともに学習率がどのように変化するかを決定します。
                - `PPO`の場合、学習がより安定して収束するように、`max_steps`まで学習率を減衰させることをお勧めします。
                - ただし、場合によっては(トレーニング時間が解らないなど)、この機能を無効にすることができます。
                - SACの場合、Q機能が自然に収束するまでエージェントが学習を続けることができるように、学習率を一定に保つことをお勧めします。
                - `linear`は、`learning_rate`を線形的に減衰させ、`max_steps`で`0`に到達します。一方、`constant`は、トレーニング全体の学習率を一定に保ちます。
                - デフォルト: PPOの場合は `linear`、SACの場合は `constant`
            - 設定
                - `ppo`なので
                - ⇒ `linear`
    - `network_settings`
        - `normalize`
            - 説明
                - 正規化がベクトル観測入力に適用されるかどうか。
                - この正規化は、ベクトル観測の移動平均と分散に基づいています。
                - 正規化は、複雑な連続制御の問題がある場合に役立ちますが、より単純な離散制御の問題には有害な場合があります。
                - デフォルト： `false`
            - 設定
                - 自前で正規化してるのでここでは`false`にしておきます。
                - 自前でするのをやめればコンパイルし直さずに切り替えができて良いのでしょうか?
                -  ⇒ `false`
        - `hidden_units`
            - 説明
                - ニューラルネットワークの非表示層のユニット数。
                - ニューラルネットワークの完全に接続された各層にあるユニットの数に対応します。
                - 正しいアクションが観測入力の単純な組み合わせである単純な問題の場合、これは小さいはずです。
                - アクションが観測変数間の非常に複雑な相互作用である問題の場合、これは大きくする必要があります。
                - デフォルト: `128`
                - 標準的な範囲： `32〜512`
            - 設定
                - 最大限複雑にしておきます。
                -  ⇒ `512`
        - `num_layers`
            - 説明
                - ニューラルネットワークの非表示レイヤーの数。
                - 観測入力後、または視覚観測のCNNエンコード後に存在する非表示層の数に対応します。
                - 単純な問題の場合、より少ない層がより速くより効率的にトレーニングする可能性があります。
                - より複雑な制御問題には、より多くの層が必要になる場合があります。
                - デフォルト: `2`
                - 標準的な範囲: `1~3`
            - 検証
                - 最大限複雑にします。`4`にして長期学習させても強くなりませんでした。
                -  ⇒ `3`
        - `vis_encode_type`
            - 説明
                - 視覚的観察をエンコードするためのエンコーダタイプ。
                - `simple`（デフォルト）は2つの畳み込み層で構成されるシンプルなエンコーダーを使用し、`nature_cnn`は3つの畳み込み層で構成されるMnihらによって提案されたCNN実装を使用し、resnetは3つのスタック層で構成される 'IMPALA Resnet'を使用します 、それぞれに2つの残差ブロックがあり、他の2つよりもはるかに大きなネットワークを構成しています。
                - `match3`は、ボードゲーム用に最適化された小さなCNN（Gudmundsoon他）であり、5x5の目視観察サイズまで使用できます。
                - デフォルト： `simple`
            - 設定
                - 視覚的観測ではないので関係ないのでしょうか。
                -  ⇒ `simple`
        - `reward_signals`
            - `extrinsic`
                - `gamma`
                    - 説明
                        - 環境から来る将来の報酬の割引係数。
                        - これは、エージェントが可能な限りの報酬を気遣う必要がある未来と考えることができます。
                        - 遠い将来に報酬を準備するためにエージェントが現在行動している必要がある状況では、この値は大きくなければなりません。 報酬がより迅速である場合、それはより少なくなる可能性があります。
                        - 厳密に1より小さくなければなりません。
                        - デフォルト: `0.99`
                        - 標準的な範囲: `0.8~0.995`
                    - 設定
                        - 解らないのでデフォルトに。
                        -  ⇒ `0.99`
                - `strength`
                    - 説明
                        - 環境によって与えられる報酬を乗算する係数。
                        - 通常の範囲は、報酬信号によって異なります。
                        - デフォルト: `1.0`
                        - 標準的な範囲: `1.00`
                    - 設定
                        - 解らないのでデフォルトに。
                        -  ⇒ `1.0`
        - `keep_checkpoints`
            - 説明
                - 保持するモデルチェックポイントの最大数。
                - チェックポイントは、`checkpoint_interval`オプションで指定された数のステップの後に保存されます。
                - チェックポイントの最大数に達すると、新しいチェックポイントを保存するときに最も古いチェックポイントが削除されます。
                - デフォルト： `5`
            - 設定
                - 解らないのでデフォルトに。
                - ⇒ `5`
        - `checkpoint_interval`
            - 説明
                - トレーナーが各チェックポイントの間に収集した経験の数。
                - 古いものを削除する前に、最大`keep_checkpoints`個のチェックポイントが保存されます。
                - 各チェックポイントは、`.nn`(および該当する場合は`.onnx`)ファイルを`results/`フォルダーに保存します。
                - デフォルト: `500000`
            - 設定
                - 解らないのでデフォルトに。
                - ⇒ `500000`
        - `max_steps`
            - 説明
                - トレーニングプロセスを終了する前に、環境で(または複数を並行して使用する場合はすべての環境にわたって)実行する必要があるステップ(つまり、収集された観察と実行されたアクション)の総数。
                - 環境内に同じ動作名のエージェントが複数ある場合、それらのエージェントが実行するすべてのステップは、同じ`max_steps`カウントに寄与します。
                - デフォルト: `500000`
                - 標準的な範囲: `5e5~1e7`
            - 検証
                - 大きすぎると過学習になるし、少ないと学習不足になるのでしょうか。
                - ELOが伸びてエントロピーが減少しているのに弱くなることも多く、単純ではないようです。
                -  ⇒ `10000000`
        - `time_horizon`
            - 説明
                - 経験バッファーに追加する前にエージェントごとに収集する経験のステップ数。
                - エピソードが終了する前にこの制限に達すると、エージェントの現在の状態から予想される全体的な報酬を予測するために値の見積もりが使用されます。
                - そのため、このパラメーターは、偏りは少ないが分散の推定値が高い(長時間の地平線)と、偏りはあるが変動の小さい推定値(短時間の地平線)の間でトレードオフします。
                - エピソード内で頻繁に報酬が発生する場合、またはエピソードが非常に大きい場合は、数値が小さいほど理想的です。
                - この数は、エージェントの一連のアクション内のすべての重要な動作をキャプチャするのに十分な大きさである必要があります。
                - デフォルト: `64`
                - 標準的な範囲: `32~2048`
            - 検証
                - `128`、`32`、`48`だと`64`よりELOは伸びるものの、弱くなりました。
                -  ⇒ `64`
        - `summary_freq`
            - 説明
                - トレーニング統計を生成して表示する前に収集する必要がある経験の数。これにより、Tensorboardのグラフの粒度が決まります。
                - デフォルト: `50000`
            - 設定
                - グラフを細かくします。
                -  ⇒ `10000`
        - `threaded`
            - 説明
                - デフォルトでは、環境のステップ実行中にモデルの更新が発生する可能性があります。
                - これは、トレーニングのスピードアップと引き換えに、PPOのポリシー上の想定に少し違反しています。
                - PPOの厳格なオンポリシーを維持するために、threadedをfalseに設定することで並列更新を無効にすることができます。
                - 通常、SACのスレッドをオフにする理由はありません。
                - デフォルト: `true`
            - 検証
                - `true`だとターン制御が面倒になります。
                -  ⇒ `false`
        - `self_play`
            - `save_steps`
                - 説明
                    - スナップショット間のトレーナーステップの数。
                    - たとえば、 `save_steps = 10000`の場合、現在のポリシーのスナップショットは10000トレーナーステップごとに保存されます。
                    - トレーナーのステップはエージェントごとにカウントされることに注意してください。
                    - 詳細については、v0.13以降の移行ドキュメントをご覧ください。
                    - `save_steps`の値を大きくすると、ポリシーがより多くのトレーニングを受けるため、幅広いスキルレベルをカバーし、場合によってはプレイスタイルをカバーする一連の対戦相手が生成されます。
                    - その結果、エージェントはさまざまな対戦相手に対してトレーニングを行います。
                    - より多様な対戦相手を倒すためのポリシーを学ぶことは難しい問題です。そのため、全体的なトレーニング手順が必要になる可能性がありますが、トレーニングの終わりにより一般的で堅固なポリシーにつながる可能性もあります。
                    - この値は、エージェントにとって環境が本質的にどれほど難しいかにも依存します。
                    - 標準的な範囲: `10000~100000`
                    - デフォルト： `20000`
                - 設定
                    - ⇒ `team_change / 5`
            - `team_change`
                - 説明
                    - 学習チームを切り替える間の`trainer_steps`の数。
                    - これは、別のチームが新しい学習チームになる前に、特定のゴーストトレーナーに関連付けられているチームがトレーニングするトレーナーステップの数です。
                    - 非対称ゲームでは、同様のパフォーマンスを得るために、反対側のチームが必要とするトレーナーのステップが少なくなる可能性があります。
                    - これにより、ユーザーは、チームスイッチごとの単純なエージェントチームよりも複雑なエージェントチームをより多くのトレーナーステップにトレーニングできます。
                    - チーム変更の値が大きいほど、エージェントは対戦相手に対してより長くトレーニングできるようになります。
                    - エージェントが同じ対戦相手を訓練する時間が長いほど、敵を倒す能力が高くなります。
                    - ただし、それらに対するトレーニングが長すぎると、特定の対戦相手の戦略に適合しすぎて、エージェントが対戦相手の次のバッチに対して失敗する可能性があります。
                    - `team-change`の値によって、他のチームの対戦相手として使用するために保存されるエージェントのポリシーのスナップショットの数が決まります。
                    - したがって、前述のsave_stepsパラメータの関数としてこの値を設定することをお勧めします。
                    - デフォルト: `5 * save_steps`
                    - 標準な範囲: `4x~10x ただし、x = save_steps`
                - 検証
                    - 一局に必要なステップが少ないので、もっと小さくて良いのではないだろうかと考えて、`50000`にしたところ、ELOは伸びましたが弱くなりました。
                    - 逆に、`150000`に増やしても同様でした。
                    -  ⇒ `100000`
            - `swap_steps`
                - 説明
                    - 異なるスナップショットでの対戦相手ポリシーのスワップ間のゴーストステップ(トレーナーステップではない)の数。
                    - 「ゴーストステップ」とは、固定されたポリシーに従っており、学習していないエージェントによって実行されるステップを指します。
                    - この区別の理由は、非対称ゲームでは、「Strikers Vs Goalie」サンプル環境の2v1シナリオのように、エージェントの数が等しくないチームが存在する可能性があるためです。
                    - 2つのエージェントを持つチームは、1つのエージェントを持つチームの2倍の環境ステップあたりのエージェントステップを収集します。
                    - したがって、これら2つの値は、同じ数のトレーナーステップが各チームの同じ数の対戦相手の交換に対応することを保証するために区別する必要があります。
                    - ユーザーが`team_change`の合計ステップ中に`num_agents`エージェントを持つチームと`num_opponent_agents`エージェントを持つ対戦相手チームの`x`スワップを希望する場合の`swap_steps`の式は次のとおりです：`(num_agents / num_opponent_agents) * (team_change / x)`
                    - デフォルト: `10000`
                    - 標準的な範囲： `10000-100000`
                - 設定
                    -  ⇒ `team_change / 10`
            - `window`
                - 説明
                    - エージェントの対戦相手がサンプリングされた過去のスナップショットのスライディングウィンドウのサイズ。
                    - たとえば、ウィンドウサイズが`5`の場合、最後に取得した5つのスナップショットが保存されます。
                    - 新しいスナップショットが作成されるたびに、最も古いものが破棄されます。
                    - ウィンドウの値が大きいほど、エージェントの対戦相手のプールには、トレーニング実行の初期のポリシーが含まれているため、より多様な行動が含まれます。
                    - `save_steps`ハイパーパラメーターと同様に、エージェントはさまざまな対戦相手に対してトレーニングを行います。
                    - より多様な対戦相手を倒すためのポリシーを学ぶことは難しい問題です。そのため、全体的なトレーニング手順が必要になる可能性がありますが、トレーニングの終わりにより一般的で堅固なポリシーにつながる可能性もあります。
                    - デフォルト: `10`
                    - 標準的な範囲: `5~30`
                - 検証
                    - 多様性を重視します。
                    - `10`、`20`に比して、`30`にすると明確に強くなりました。
                    - 試みに`40`にしたところ、ELOは伸びましたが弱くなりました。
                    -  ⇒ `30`
            - `play_against_latest_model_ratio`
                - 説明
                    - エージェントが最新の対戦相手のポリシーに反する確率。
                    - 確率が`1 - play_against_latest_model_ratio`の場合、エージェントは過去の反復からの対戦相手のスナップショットと対戦します。
                    - `play_against_latest_model_ratio`の値が大きいほど、エージェントが現在の対戦相手とより頻繁に対戦することを示しています。
                    - エージェントはそのポリシーを更新しているため、対戦相手は反復ごとに異なります。
                    - これは不安定な学習環境につながる可能性がありますが、より厳しい最終的なポリシーにつながる可能性がある、ますます困難な状況の自動カリキュラムをエージェントにもたらします。
                    - デフォルト: `0.5`
                    - 標準的な範囲: `0.0~1.0`
                - 検証
                    - 多様性を重視します。
                    - `0.5`と`0.3`では後者の方がELOは上昇しますが、前者の方が強いです。
                    - 後者は前者に比べて、過去の自分と戦うことがより多いと解釈しました。
                    - 過去の自分は現在より弱いだろうと考えると、レーティングが高くなる道理と受け取れます。
                    - ELOの上昇傾向は重要ですが、他より高いことが他より強いことを意味しないと学びました。
                    - ⇒ `0.5`
            - `initial_elo`
                - 説明
                    - ELOの初期値。
                - 設定
                    -  ⇒ `1200.0`

# 結果
- 初期に比べて多少は強くなりましたが、未だ初心者レベルに留まっています。

# 考察
- [公式 Unity Blog の記事](https://blogs.unity3d.com/jp/2020/02/28/training-intelligent-adversaries-using-self-play-with-ml-agents/)を読むと、自分自身を相手に強化学習を行うことの難しさを感じます。
- 限られた相手と戦うので戦略的な幅が生まれないということなのだろうと考えて、戦略に幅が出るような設定を模索したのですが、効果的な設定を見いだせませんでした。
- ソースを公開していますので、セルフプレイでの学習設定に詳しい諸兄のご助言をいただければ幸いです。