こんにちは.changです. 今回は強化学習がスプリントトレインを学習できるか試してみました.
0. トレインとは?
自転車ロードレースにおいて,ゴール前でチームで隊列を組んでエースを最終スプリントに導く戦略です. 映像*1の様に,エースを最後尾にして一列に並び,アシストが一人一人オールアウトしながら集団の先頭をキープします. 最後から2番目に位置してエースを送り出す(=リードアウトする)選手を,発射台と呼びます.
高度にシステマチックになった現代レースにおいて,勝利を手にする為の必須のテクニックの一つです.
補足用語説明
エース: チームの勝利を託され,最終局面での勝負を担う選手
アシスト: 自らのリザルトを犠牲にして,エースを補助する選手
オールアウト: 力を出し切ってそれ以後のレースには絡めない状態
その他,自転車レースのポイントについては過去記事*2*3*4をご覧下さい.
1. プログラム
前回からかなり変えたので,詳しく記録していきます.
(1) フィールド
32×12の画像の上を上から下方向にレースさせます. 右端にあるバーは,後述するエネルギー残量を表しています.
(2) プレーヤー
エースとアシスト1名づつから成るチーム編成としました. エースとアシストのアクションがそれぞれ5パターンなので,5×5=25パターンのアクションを決定する強化学習を作ります.
カ○ンディッシュ(エース)
- アクションは5パターン
- 1ダッシュなら1,3ダッシュなら3だけエネルギーを消費する
- エネルギー残量不足で(スーパー)スプリントした場合は,エネルギー残量分だけ進む
- エネルギーが0になると,スプリント(0)とスーパースプリント(4)は前進(2)と同じ動きになる
- 初期エネルギーは5
レ○ショー(アシスト)
- アクションは5パターン
- 1ダッシュ毎にエネルギー1を消費する
- エネルギーが0になると,スプリント(0)は前進(2)と同じ動きになる
- 初期エネルギーは5
(3) コンペティター
名目上コンペティターという位置づけにしますが,今回はプレーヤーとの競争を目的にはしていません. 事前の調査で,コンペティターがいるとアシストを使ったスプリントを学習しにくくなることがわかりました. このため,今回はコンペティターを敢えて弱く,無個性に設定しました.
ペ○ッキ & ツァ○ル
- アクションは5パターン
- 1ダッシュ毎にエネルギー1を消費する
- エネルギーが0になると,スプリント(0)は前進(2)と同じ動きになる
- 初期エネルギーは0
Note: 競争させないという意味ではフィールドから消しても良いのですが,次のステップで2つのトレイン同士を対戦させる為の準備として存在させています. また,今回はカ○ンディッシュを主役にしたため,前回と配色が違います. 紛らわしくてすみません.
(4) スリップストリーム
これまでと同様に簡単なルールで模擬しました. プレーヤーとコンペティターの間にも,スリップが効くようにしています.
(5) レース展開の表現
方針は変えていないのですが,前回までのソースに大きな間違いがあることに気付きました(汗). ツメが甘くてすみません. 修正しました.
- レース開始時に32枚(チャンネル)の画像を0初期化する
- 1ステップ毎にチャンネルをずらしながら,画像を更新する
- 勝敗が決まった時点で画像を確定し,ネットワークに入力する(数チャンネルは初期(0)画像のまま)
- レース終了時点から遡り,過去のアクションに報酬を与える
train.py
# store experience buf1.append((state_t.reshape(env.screen_n_cols*env.screen_n_rows*env.max_time), action_t_1, state_t_1.reshape(env.screen_n_cols*env.screen_n_rows*env.max_time))) if terminal: reward_t_1 = reward_t[0] agent1.store_experience(buf1, reward_t_1) agent1.experience_replay() buf1.clear()
致命的に間違っていたのが,過去の行動に報酬を与える処理です. 前回までのソースは,複数ゲームの経験の束に対して線形に報酬を割り当てていました. これでは,序盤のアクションに対して,レースの勝敗に応じた報酬を与えることが出来ません. このため,今回は1ゲーム毎の経験を中間バッファに貯め,勝負が決した時点で(ゲームに要したステップ数がわかる状態で)経験を積むようにしました.
dqn_agent.py
for j in range(minibatch_size): # minibatch_indexes: game_length = len(self.D[j][0]) reward_j = self.D[j][1] for i in range(game_length): ... reward_minibatch.append(reward_j * i/(game_length - 1))
1ゲーム毎に遡って,報酬を与えます.
(6) 報酬
今回苦労したところです.
前回まではAll or Nothingの思想の下で実践形式の報酬を与えていました. 色々と調査したところ,All or Nothing報酬では,チームトレインを組む動きが全く発生しませんでした. 勝利をつかむパターンが多岐に及び,トレインに成功した場合の報酬が際立たないからだと考えられます. このため,今回はゴールにより速く辿り着く為のトレーニングと位置づけ,ゴールまでに要したステップ数に応じて報酬が決まる様にしました.
また,エースを勝たせる動き(=リードアウト)が発生することを期待して,チームの成績で報酬を決めるケースと,エースの成績で報酬を決めるケースの二通りを試しました.
- チーム成績: エースまたはアシストで,ゴールに速くたどり着いた側のゴールタイムで報酬が決まる
- エース成績: エースのゴールタイムで報酬が決まり,アシストの速さは報酬に反映されない
slipstream.py
self.terminal = False for i in range(self.n_players): if self.player_row[i] >= self.field_n_rows - 1 and self.player_team[i] == 0: self.terminal = True self.reward[0] = ((self.field_n_rows - 7) - self.time)*0.1 self.reward[2] = ((self.field_n_rows - 7) - self.time)*0.1 break
抜粋はチーム成績の場合のソースです. スプリントを全くせずに最遅でゴールした場合が30ステップで報酬-0.5,スリップ無しでスプリントを使い切った場合が25ステップで報酬0.0になります.
Note: 前回までの実験についても,ライバルの背後からスプリントする様にはなるものの,ライバルのスリップに入る為に横の動きを使っているのか疑問に思っていました. 前にライバルがいる(=結果として,スリップが作用する)かは偶然だった可能性があります.
(7) 乱数
アクションに乱数を混ぜ込む割合にも工夫を加えました.
dqn_agent.py
def select_action(self, state, epsilon): if np.random.rand() <= epsilon: # random return np.random.choice(self.enable_actions) else: # max_action Q(state, action) return self.enable_actions[np.argmax(self.Q_values(state))]
前回までは乱数の割合(上記epsilonの値)を0.1で固定していましたが,今回は0.1と0.3の2条件を試しました.
(8) 計算条件
以上を総合すると,今回は下記の3条件での学習となります.
条件 | 乱数の割合 | 報酬 |
---|---|---|
1 | 0.1 | チームの成績 |
2 | 0.3 | チームの成績 |
3 | 0.3 | エースの成績 |
2. 結果
今回も計算に時間がかかりました. 計算に要する時間もケース毎に異なった為(学習が進んでレースが速く進めば,計算も速く終わる),結果を得られた件数もバラバラになってしまいました. 本来は統一するべきですが,今回は正直ベースで,現段階で得られている全ての結果を載せます.
学習終了後に300レースさせた際の結果をまとめました. 3列目以降の数値はそのステップ数で走りきった件数です.
条件1
条件 | 平均報酬 | 18ステップ | 17ステップ | 16ステップ | 15ステップ | 14ステップ |
---|---|---|---|---|---|---|
0 | 0.21 | 6 | 0 | 0 | 0 | 0 |
1 | 0.22 | 3 | 2 | 0 | 0 | 0 |
2 | 0.27 | 0 | 2 | 0 | 0 | 0 |
3 | 0.29 | 2 | 1 | 0 | 0 | 0 |
4 | 0.26 | 4 | 7 | 1 | 0 | 0 |
5 | 0.17 | 4 | 0 | 0 | 0 | 0 |
6 | 0.27 | 20 | 6 | 0 | 0 | 0 |
7 | 0.15 | 9 | 3 | 0 | 0 | 0 |
条件2
条件 | 平均報酬 | 18ステップ | 17ステップ | 16ステップ | 15ステップ | 14ステップ |
---|---|---|---|---|---|---|
0 | 0.21 | 5 | 0 | 0 | 0 | 0 |
1 | 0.19 | 21 | 0 | 0 | 0 | 0 |
2 | 0.31 | 9 | 1 | 1 | 0 | 0 |
3 | 0.20 | 0 | 0 | 0 | 0 | 0 |
4 | 0.19 | 7 | 0 | 0 | 0 | 0 |
5 | 0.30 | 12 | 2 | 4 | 2 | 0 |
6 | 0.28 | 14 | 9 | 0 | 0 | 0 |
7 | 0.25 | 7 | 0 | 0 | 0 | 0 |
8 | 0.24 | 12 | 0 | 0 | 0 | 0 |
条件3
条件 | 平均報酬 | 18ステップ | 17ステップ | 16ステップ | 15ステップ | 14ステップ |
---|---|---|---|---|---|---|
0 | 0.23 | 11 | 0 | 0 | 0 | 0 |
1 | 0.19 | 1 | 1 | 0 | 0 | 0 |
2 | 0.21 | 9 | 2 | 0 | 0 | 0 |
3 | 0.15 | 12 | 0 | 0 | 0 | 0 |
4 | 0.22 | 0 | 0 | 0 | 0 | 0 |
5 | 0.08 | 1 | 1 | 0 | 0 | 0 |
6 | 0.13 | 4 | 0 | 0 | 0 | 0 |
全体的な傾向としては条件2が一番速く,ついで条件1という結果でした. 条件2で15ステップでゴールしたケースが,最速となりました.
高速でゴールしたケースの代表例を,それぞれ3レースずづ抜粋してgifアニメにしました.
3. 考察
(1) トレイン
条件1で頻繁に発生したのが,カ○ンディッシュとレ○ショーが交互にスプリントしてゴールに突っ込んでいく「先頭交代トレイン」でした. AIならではのトリッキーな動きで面白いです. 実はこれ,両者は交互にスプリントしているわけでは無く,単純にダッシュを繰り返しています. 普通は直ぐにエネルギー切れになりますが,スリップの恩恵でエネルギーが交互に回復して延々とスプリントできるのです. スプリントと言うより,チームタイムトライアルに近い戦術だと思います.
先頭交代トレインよりも更に速かったのが,カベンディッシュのスーパーダッシュを利用した「リードアウトトレイン」です. これは,乱数を大きく入れ込んだ条件2のみで見られました. より多くの試行錯誤させたほうが方が,戦略の幅が広がるということでしょう. とはいえ,リードアウトが発生したのはごく僅かなケースでした. 2名のプレーヤーから成る5×5のアクションパターンを学習させているため,網羅的に戦術を試すのは難しかったようです.
以外だったのはエースの速度に報酬を与えた条件3の成績が振るわなかったです. リードアウトトレインが発生しやすいと予想していたのですが... 推測ですが,学習開始直後で未だ戦略が整わないフェーズにおいて,レ○ショーのアクションが全て徒労に終わるからだと思います. ゴールに報酬がつけば,レ○ショーもスプリントするようになる筈です. 条件1と条件2においては,カ○ンディッシュとレ○ショーが共にゴールを目指す中で,先頭交代やリードアウトが偶発したと考えています. 対して,自らのゴールに報酬がつかない条件3のレ○ショーはスプリントすら覚えず,結果的にカ○ンディッシュを孤立させてしまうのでしょう. 実際のレース現場においても,練習時にはエース・アシストを固定せず,本番直前にオーダーを決める場合があるようです. アシストにも,個の利益に繋がる様なモチベーションを与える必要があるということかも知れません.
(2) 人工知能を用いた戦術生成
前回までの様な実戦形式ではトレインは全く発生しませんでした. トレインが組まれる様な報酬やコンペティターとのパワーバランスを綿密にお膳立てする必要がありました. 高度な戦術は,レース当日に一堂を介した選手達からは生まれないということだと思います. 実際,シーズン前のチーム合宿等でトレインの練習をするという話を頻繁に耳にします.
では,新たな戦略はどの様に生まれるのでしょう. 完全な無から発生することは無いはずです. 同時に,(飛躍した命題にはなりますが)人工知能を使って戦術を発想するプロセスを考えてみようと思います.
トレインの歴史を語るの程の知識はないので,単なる思い出話になります. レースを見始めた当時,ツァベルがペタッキをリードアウトするシーンをよく見ました. その少し後で頭角を現したのがチームHigh Roadのカベンディッシュでした. 綿密に組まれたトレインとリードアウトで勝利を量産する姿がとても印象的でした. チームHigh Road以前には,あそこまでシステマチックなトレインを組むチームは無かったと思います.
想像ですが,カベンディッシュの監督はペタッキやツァベルの走りから,より理想的なトレインを想起して練習方法を練り上げたではないでしょうか. 一昔前までは先輩の後について実学で身につけていた技術が,緻密な教育プログラムによってより高度に標準化されるのは良く在ることです. つまり,戦術のキッカケは実践の中で得られたとしても,それを成熟させるのは戦略家の思考と綿密なトレーニングプランなのだと思います.
強化学習は,他者が採った戦術を真似たり,破れた経験から戦術を考察することは(一般的には)出来ません. あくまで,経験ベースです. 今回の様に高度な戦術を学ばせるには,報酬やルールを細かくお膳立てしてやる必要があります. 現実の選手の育成にも通じるところがあるかも知れませんね. 同時に,人工知能の限界とも言えます. プログラマが意図した戦術しか発生しないならば,現存しない新しい戦術を強化学習から生み出すことは出来ないことになります. これに抗いたいのです.
より綺麗なトレインを組んだり,高い頻度でトレインを発生させるためには,「同じチームの選手と近くにいたら高評価」とか「スリップをより多く使ってゴールしたら高評価」といった直接的な報酬を与える必要が在ると思います. それは面白くないと思っています. 報酬を直接的にする程,意外性が無くなってしまうからです.
今回,先頭交代トレインという戦術を人工知能がとってきました. 落車が存在せず,かつ他の選手の真後ろでも斜め後ろでも同等のスリップが発生するという,計算機レース特有の現象と言えるでしょう. 現実の物理法則下で,例えばカベンディッシュとペタッキが交互に先頭を入れ替わりながらスプリントしたら危険だし,速くも無い気がします. 現実との差を直視すれば,バグでしょう. ただ,冗長性が意外性を生んだと言うこともできます.
先頭交代トレインをバグと一蹴せずに「二人逃げって可能性あるのかな?」とか,「左右から交互に抜く先頭交代って可能なのかな(常に風下から抜くのが一般的)?」みたいは思考実験をやったとします. 可能性があると思ったならば,練習で試してみます. この繰り返しがイノベーションにつながるのではないでしょうか. 雲をつかむ様な話ですが,僕は,人工知能を使う意味がここに在ると思っています.
4.むすび
試行錯誤が大変で一つ一つの記事が重くなってきました. この辺で一度気を緩めたいと思っています.
余談なんですが,メインマシンをGPU重視で作ってしまった*5為,強化学習に時間が掛かります(泣). i9を買おうかな...
今回書いたソースここ*6です.
*1:https://www.youtube.com/watch?v=_xTQRdPkxd4
*2:https://changlikesdesktop.hatenablog.com/entry/2021/01/26/070425
*3:https://changlikesdesktop.hatenablog.com/entry/2021/02/18/061534
*4:https://changlikesdesktop.hatenablog.com/entry/2021/03/04/104754
*5:https://changlikesdesktop.hatenablog.com/entry/2020/05/01/101334