序章

強化学習は、制御理論内のサブフィールドであり、時間の経過とともに変化する制御システムに関係し、自動運転車、ロボット工学、ゲーム用ボットなどのアプリケーションを幅広く含みます。 このガイド全体を通して、強化学習を使用してAtariビデオゲーム用のボットを構築します。 このボットには、ゲームに関する内部情報へのアクセスは許可されていません。 代わりに、ゲームのレンダリングされたディスプレイへのアクセスとそのディスプレイの報酬のみが与えられます。つまり、人間のプレイヤーが見るものだけを見ることができます。

機械学習では、ボットは正式にはエージェントと呼ばれます。 このチュートリアルの場合、エージェントは、ポリシーと呼ばれる意思決定機能に従って機能するシステム内の「プレーヤー」です。 主な目標は、強力なポリシーで武装させることにより、強力なエージェントを開発することです。 言い換えれば、私たちの目的は、強力な意思決定機能を備えたインテリジェントボットを開発することです。

このチュートリアルは、比較のベースラインとして機能する、古典的なAtariアーケードゲームであるスペースインベーダーをプレイするときにランダムなアクションを実行する基本的な強化学習エージェントをトレーニングすることから始めます。 これに続いて、スペースインベーダーやOpenAIからリリースされた強化学習ツールキットであるGymに含まれるシンプルなゲーム環境であるFrozenLake。 このチュートリアルに従うことで、機械学習におけるモデルの複雑さの選択を左右する基本的な概念を理解できます。

前提条件

このチュートリアルを完了するには、次のものが必要です。

または、ローカルマシンを使用している場合は、 Pythonインストールおよびセットアップシリーズからオペレーティングシステムに適したチュートリアルを読んで、Python3をインストールしてローカルプログラミング環境をセットアップできます。

ステップ1—プロジェクトの作成と依存関係のインストール

ボットの開発環境をセットアップするには、ゲーム自体と計算に必要なライブラリをダウンロードする必要があります。

このプロジェクトのワークスペースを作成することから始めます。 AtariBot:

  1. mkdir ~/AtariBot

新しいに移動します AtariBot ディレクトリ:

  1. cd ~/AtariBot

次に、プロジェクトの新しい仮想環境を作成します。 この仮想環境には、任意の名前を付けることができます。 ここでは、名前を付けます ataribot:

  1. python3 -m venv ataribot

環境をアクティブ化します。

  1. source ataribot/bin/activate

Ubuntuでは、バージョン16.04以降、OpenCVが機能するには、さらにいくつかのパッケージをインストールする必要があります。 これには、ソフトウェアビルドプロセスを管理するアプリケーションであるCMakeのほか、セッションマネージャー、その他の拡張機能、デジタル画像の合成が含まれます。 次のコマンドを実行して、これらのパッケージをインストールします。

  1. sudo apt-get install -y cmake libsm6 libxext6 libxrender-dev libz-dev

注: MacOSを実行しているローカルマシンでこのガイドに従っている場合、インストールする必要がある追加のソフトウェアはCMakeだけです。 Homebrew(前提条件のMacOSチュートリアルに従った場合にインストールされます)を使用して、次のように入力してインストールします。

  1. brew install cmake

次に、 pip インストールするには wheel パッケージ、ホイールパッケージング標準のリファレンス実装。 Pythonライブラリであるこのパッケージは、ホイールを構築するための拡張機能として機能し、操作するためのコマンドラインツールが含まれています .whl ファイル:

  1. python -m pip install wheel

に加えて wheel、次のパッケージをインストールする必要があります。

  • Gym 、さまざまなゲームを研究に利用できるようにするPythonライブラリ、およびAtariゲームのすべての依存関係。 OpenAI によって開発されたGymは、さまざまなエージェントとアルゴリズムのパフォーマンスを均一に/評価できるように、各ゲームの公開ベンチマークを提供します。
  • Tensorflow 、ディープラーニングライブラリ。 このライブラリを使用すると、計算をより効率的に実行できます。 具体的には、GPU上で排他的に実行されるTensorflowの抽象化を使用して数学関数を構築することでこれを行います。
  • OpenCV 、前述のコンピュータービジョンライブラリ。
  • SciPy 、効率的な最適化アルゴリズムを提供する科学コンピューティングライブラリ。
  • NumPy 、線形代数ライブラリ。

次のコマンドを使用して、これらの各パッケージをインストールします。 このコマンドは、インストールする各パッケージのバージョンを指定することに注意してください。

  1. python -m pip install gym==0.9.5 tensorflow==1.5.0 tensorpack==0.8.0 numpy==1.14.0 scipy==1.1.0 opencv-python==3.4.1.15

これに続いて、 pip スペースインベーダーを含むさまざまなアタリビデオゲームを含むジムのアタリ環境をもう一度インストールします。

  1. python -m pip install gym[atari]

のインストールの場合 gym[atari] パッケージが成功した場合、出力は次のように終了します。

Output
Installing collected packages: atari-py, Pillow, PyOpenGL Successfully installed Pillow-5.4.1 PyOpenGL-3.1.0 atari-py-0.1.7

これらの依存関係をインストールすると、次に進んで、比較のベースラインとして機能するランダムに再生されるエージェントを構築する準備が整います。

ステップ2—ジムでベースラインランダムエージェントを作成する

必要なソフトウェアがサーバー上にあるので、古典的なAtariゲームの簡易バージョンであるスペースインベーダーをプレイするエージェントをセットアップします。 どの実験でも、モデルのパフォーマンスを理解するのに役立つベースラインを取得する必要があります。 このエージェントはフレームごとにランダムなアクションを実行するため、ランダムなベースラインエージェントと呼びます。 この場合、このベースラインエージェントと比較して、後のステップでエージェントのパフォーマンスを理解します。

Gymを使用すると、独自のゲームループを維持できます。 これは、ゲームの実行のすべてのステップを処理することを意味します。すべてのタイムステップで、 gym 新しい行動と質問 gym ゲーム状態の場合。 このチュートリアルでは、ゲームの状態は特定のタイムステップでのゲームの外観であり、ゲームをプレイしている場合に表示されるものとまったく同じです。

お好みのテキストエディタを使用して、という名前のPythonファイルを作成します bot_2_random.py. ここでは、 nano:

  1. nano bot_2_random.py

注:このガイド全体を通して、ボットの名前は、表示される順序ではなく、表示されるステップ番号に揃えられています。 したがって、このボットの名前は bot_2_random.py それよりも bot_1_random.py.

次の強調表示された行を追加して、このスクリプトを開始します。 これらの行には、このスクリプトが何をするかを説明するコメントブロックと2つの行が含まれています import このスクリプトが機能するために最終的に必要となるパッケージをインポートするステートメント:

/AtariBot/bot_2_random.py
"""
Bot 2 -- Make a random, baseline agent for the SpaceInvaders game.
"""

import gym
import random

追加する main 関数。 この関数では、ゲーム環境を作成します— SpaceInvaders-v0 —次に、を使用してゲームを初期化します env.reset:

/AtariBot/bot_2_random.py
. . .
import gym
import random

def main():
    env = gym.make('SpaceInvaders-v0')
    env.reset()

次に、 env.step 関数。 この関数は、次の種類の値を返すことができます。

  • state:提供されたアクションを適用した後のゲームの新しい状態。
  • reward:州が被るスコアの増加。 例として、これは弾丸がエイリアンを破壊し、スコアが50ポイント増加した場合です。 それで、 reward = 50. スコアベースのゲームをプレイする場合、プレーヤーの目標はスコアを最大化することです。 これは、総報酬を最大化することと同義です。
  • done:エピソードが終了したかどうか。これは通常、プレーヤーがすべての命を失ったときに発生します。
  • info:今のところ取っておくことになる無関係な情報。

使用します reward あなたの総報酬を数えるために。 また、使用します done プレイヤーがいつ死ぬかを決定するために、それはいつになるでしょう done 戻り値 True.

次のゲームループを追加します。これは、プレーヤーが死ぬまでループするようにゲームに指示します。

/AtariBot/bot_2_random.py
. . .
def main():
    env = gym.make('SpaceInvaders-v0')
    env.reset()

    episode_reward = 0
    while True:
        action = env.action_space.sample()
        _, reward, done, _ = env.step(action)
        episode_reward += reward
        if done:
            print('Reward: %s' % episode_reward)
            break

最後に、 main 関数。 含める __name__ 次のことを確認してください main で直接呼び出した場合にのみ実行されます python bot_2_random.py. 追加しない場合 if 小切手、 main Pythonファイルが実行されると、ファイルをインポートしても、は常にトリガーされます。 したがって、コードを main 関数、次の場合にのみ実行 __name__ == '__main__'.

/AtariBot/bot_2_random.py
. . .
def main():
    . . .
    if done:
        print('Reward %s' % episode_reward)
        break

if __name__ == '__main__':
    main()

ファイルを保存して、エディターを終了します。 使用している場合 nano、を押してそうします CTRL+X, Y、 それから ENTER. 次に、次のように入力してスクリプトを実行します。

  1. python bot_2_random.py

プログラムは、次のような数値を出力します。 ファイルを実行するたびに、異なる結果が得られることに注意してください。

Output
Making new env: SpaceInvaders-v0 Reward: 210.0

これらのランダムな結果には問題があります。 他の研究者や開業医が恩恵を受けることができる仕事を生み出すために、あなたの結果と試験は再現可能でなければなりません。 これを修正するには、スクリプトファイルを再度開きます。

  1. nano bot_2_random.py

import random、 追加 random.seed(0). 後 env = gym.make('SpaceInvaders-v0')、 追加 env.seed(0). これらの線が一緒になって、一貫した開始点で環境を「シード」し、結果が常に再現可能であることを保証します。 最終的なファイルは、次のファイルと正確に一致します。

/AtariBot/bot_2_random.py
"""
Bot 2 -- Make a random, baseline agent for the SpaceInvaders game.
"""

import gym
import random
random.seed(0)


def main():
    env = gym.make('SpaceInvaders-v0')
    env.seed(0)

    env.reset()
    episode_reward = 0
    while True:
        action = env.action_space.sample()
        _, reward, done, _ = env.step(action)
        episode_reward += reward
        if done:
            print('Reward: %s' % episode_reward)
            break


if __name__ == '__main__':
    main()

ファイルを保存してエディターを閉じ、ターミナルに次のように入力してスクリプトを実行します。

  1. python bot_2_random.py

これにより、正確に次の報酬が出力されます。

Output
Making new env: SpaceInvaders-v0 Reward: 555.0

これはあなたの最初のボットですが、決定を下すときに周囲の環境を考慮していないため、かなりインテリジェントではありません。 ボットのパフォーマンスをより確実に見積もるには、エージェントに一度に複数のエピソードを実行させ、複数のエピソードで平均された報酬を報告することができます。 これを構成するには、最初にファイルを再度開きます。

  1. nano bot_2_random.py

random.seed(0)、次の強調表示された行を追加して、エージェントに10エピソードのゲームをプレイするように指示します。

/AtariBot/bot_2_random.py
. . .
random.seed(0)

num_episodes = 10
. . .

直後の env.seed(0)、報酬の新しいリストを開始します。

/AtariBot/bot_2_random.py
. . .
    env.seed(0)
    rewards = []
. . .

からのすべてのコードをネストする env.reset() の終わりまで main()for ループ、反復 num_episodes 回数。 から各行をインデントしてください env.reset()break 4つのスペースで:

/AtariBot/bot_2_random.py
. . .
def main():
    env = gym.make('SpaceInvaders-v0')
    env.seed(0)
    rewards = []

    for _ in range(num_episodes):
        env.reset()
        episode_reward = 0

        while True:
            ...

直前 break、現在メインゲームループの最後の行で、現在のエピソードの報酬をすべての報酬のリストに追加します。

/AtariBot/bot_2_random.py
. . .
        if done:
            print('Reward: %s' % episode_reward)
            rewards.append(episode_reward)
            break
. . .

の終わりに main 関数、平均報酬を報告します:

/AtariBot/bot_2_random.py
. . .
def main():
    ...
            print('Reward: %s' % episode_reward)
            break
    print('Average reward: %.2f' % (sum(rewards) / len(rewards)))
    . . .

これで、ファイルは次のように整列します。 次のコードブロックには、スクリプトの重要な部分を明確にするためのコメントがいくつか含まれていることに注意してください。

/AtariBot/bot_2_random.py
"""
Bot 2 -- Make a random, baseline agent for the SpaceInvaders game.
"""

import gym
import random
random.seed(0)  # make results reproducible

num_episodes = 10


def main():
    env = gym.make('SpaceInvaders-v0')  # create the game
    env.seed(0)  # make results reproducible
    rewards = []

    for _ in range(num_episodes):
        env.reset()
        episode_reward = 0
        while True:
            action = env.action_space.sample()
            _, reward, done, _ = env.step(action)  # random action
            episode_reward += reward
            if done:
                print('Reward: %d' % episode_reward)
                rewards.append(episode_reward)
                break
    print('Average reward: %.2f' % (sum(rewards) / len(rewards)))


if __name__ == '__main__':
    main()

ファイルを保存し、エディターを終了して、スクリプトを実行します。

  1. python bot_2_random.py

これにより、次の平均報酬が正確に出力されます。

Output
Making new env: SpaceInvaders-v0 . . . Average reward: 163.50

これで、打ち負かすベースラインスコアのより信頼性の高い推定値が得られました。 ただし、優れたエージェントを作成するには、強化学習のフレームワークを理解する必要があります。 「意思決定」の抽象的な概念をより具体的にするにはどうすればよいでしょうか。

強化学習を理解する

どのゲームでも、プレーヤーの目標はスコアを最大化することです。 このガイドでは、プレーヤーのスコアを報酬と呼びます。 報酬を最大化するには、プレーヤーは意思決定能力を磨くことができなければなりません。 正式には、決定とは、ゲームを見る、またはゲームの状態を観察し、アクションを選択するプロセスです。 私たちの意思決定機能はポリシーと呼ばれます。 ポリシーは状態を入力として受け入れ、アクションを「決定」します。

policy: state -> action

このような関数を作成するために、Q学習アルゴリズムと呼ばれる強化学習の特定のアルゴリズムセットから始めます。 これらを説明するために、ゲームの初期状態を考えてみましょう。これを呼び出します。 state0:あなたの宇宙船とエイリアンはすべて彼らの開始位置にいます。 次に、各アクションが獲得する報酬の量を示す魔法の「Qテーブル」にアクセスできると仮定します。

アクション 褒美
state0 シュート 10
state0 3
state0 3

The shoot アクションはあなたの報酬を最大化します、それは最高の価値を持つ報酬をもたらすので:10。 ご覧のとおり、Qテーブルは、観察された状態に基づいて決定を下すための簡単な方法を提供します。

policy: state -> look at Q-table, pick action with greatest reward

ただし、ほとんどのゲームには状態が多すぎてテーブルにリストできません。 このような場合、Q学習エージェントはQテーブルの代わりにQ関数を学習します。 このQ関数は、以前のQテーブルの使用方法と同様に使用します。 テーブルエントリを関数として書き直すと、次のようになります。

Q(state0, shoot) = 10
Q(state0, right) = 3
Q(state0, left) = 3

特定の状態を考えると、決定を下すのは簡単です。考えられる各アクションとその報酬を確認し、期待される最高の報酬に対応するアクションを実行するだけです。 以前のポリシーをより正式に再定式化すると、次のようになります。

policy: state -> argmax_{action} Q(state, action)

これは、意思決定機能の要件を満たします。ゲーム内の状態が与えられると、アクションを決定します。 ただし、この解決策は知っていることに依存します Q(state, action) すべての状態とアクションに対して。 見積もる Q(state, action)、次のことを考慮してください。

  1. エージェントの状態、アクション、および報酬の多くの観察を考えると、移動平均を取ることによって、すべての状態およびアクションの報酬の見積もりを取得できます。
  2. スペースインベーダーは報酬が遅れるゲームです。プレイヤーが撃ったときではなく、エイリアンが爆破されたときにプレイヤーに報酬が与えられます。 ただし、射撃によってアクションを実行するプレーヤーは、報酬の真の推進力です。 どういうわけか、Q関数は割り当てる必要があります (state0, shoot) ポジティブな報酬。

これらの2つの洞察は、次の方程式で体系化されています。

Q(state, action) = (1 - learning_rate) * Q(state, action) + learning_rate * Q_target
Q_target = reward + discount_factor * max_{action'} Q(state', action')

これらの方程式は、次の定義を使用します。

  • state:現在のタイムステップでの状態
  • action:現在のタイムステップで実行されたアクション
  • reward:現在のタイムステップに対する報酬
  • state':アクションを実行した場合の次のタイムステップの新しい状態 a
  • action':すべての可能なアクション
  • learning_rate:学習率
  • discount_factor:割引係数、それを伝播するときに報酬が「低下」する量

これら2つの方程式の完全な説明については、Q学習の理解に関するこの記事を参照してください。

強化学習のこの理解を念頭に置いて、残っているのは、実際にゲームを実行し、新しいポリシーのこれらのQ値の推定値を取得することだけです。

ステップ3—FrozenLake用のシンプルなQ学習エージェントを作成する

ベースラインエージェントができたので、新しいエージェントの作成を開始して、元のエージェントと比較できます。 このステップでは、 Q-learning を使用するエージェントを作成します。これは、特定の状態でどのアクションを実行するかをエージェントに教えるために使用される強化学習手法です。 このエージェントは新しいゲームFrozenLakeをプレイします。 このゲームのセットアップは、ジムのWebサイトで次のように説明されています。

冬が来た。 湖の真ん中にフリスビーを置き去りにしたワイルドスローをしたとき、あなたとあなたの友達は公園でフリスビーの周りを投げていました。 水はほとんど凍っていますが、氷が溶けた穴がいくつかあります。 これらの穴の1つに足を踏み入れると、氷点下の水に落ちます。 現時点では、国際的なフリスビーが不足しているため、湖を渡ってディスクを回収することが絶対に必要です。 ただし、氷は滑りやすいので、必ずしも意図した方向に動くとは限りません。

サーフェスは、次のようなグリッドを使用して記述されます。

SFFF       (S: starting point, safe)
FHFH       (F: frozen surface, safe)
FFFH       (H: hole, fall to your doom)
HFFG       (G: goal, where the frisbee is located)

プレーヤーは左上から開始します。 S、右下のゴールに向かって進みます。 G. 利用可能なアクションは、、およびで、目標に到達するとスコアが1になります。 示されているいくつかの穴があります H、および1つに分類されると、スコアは0になります。

このセクションでは、簡単なQ学習エージェントを実装します。 以前に学んだことを使用して、探索探索の間でトレードオフするエージェントを作成します。 このコンテキストでは、探索とはエージェントがランダムに行動することを意味し、搾取とはエージェントがQ値を使用して最適な行動であると信じるものを選択することを意味します。 また、Q値を保持するテーブルを作成し、エージェントが行動して学習するたびにQ値を段階的に更新します。

手順2のスクリプトのコピーを作成します。

  1. cp bot_2_random.py bot_3_q_table.py

次に、この新しいファイルを開いて編集します。

  1. nano bot_3_q_table.py

スクリプトの目的を説明するファイルの上部にあるコメントを更新することから始めます。 これは単なるコメントであるため、スクリプトが正しく機能するためにこの変更は必要ありませんが、スクリプトの機能を追跡するのに役立ちます。

/AtariBot/bot_3_q_table.py
"""
Bot 3 -- Build simple q-learning agent for FrozenLake
"""

. . .

スクリプトに機能的な変更を加える前に、インポートする必要があります numpy その線形代数ユーティリティのために。 真下 import gym、強調表示された行を追加します。

/AtariBot/bot_3_q_table.py
"""
Bot 3 -- Build simple q-learning agent for FrozenLake
"""

import gym
import numpy as np
import random
random.seed(0)  # make results reproducible
. . .

下に random.seed(0)、のシードを追加します numpy:

/AtariBot/bot_3_q_table.py
. . .
import random
random.seed(0)  # make results reproducible
np.random.seed(0)
. . .

次に、ゲームの状態にアクセスできるようにします。 を更新します env.reset() ゲームの初期状態を変数に格納する次のように言う行 state:

/AtariBot/bot_3_q_table.py
. . .
    for _ in range(num_episodes):
        state = env.reset()
        . . .

を更新します env.step(...) 次の状態を格納する次のように言う行、 state2. 現在の両方が必要になります state そして次のもの— state2 —Q関数を更新します。

/AtariBot/bot_3_q_table.py
        . . .
        while True:
            action = env.action_space.sample()
            state2, reward, done, _ = env.step(action)
            . . .

episode_reward += reward、変数を更新する行を追加します state. これは変数を保持します state ご想像のとおり、次のイテレーションのために更新されました state 現在の状態を反映するには:

/AtariBot/bot_3_q_table.py
. . .
        while True:
            . . .
            episode_reward += reward
            state = state2
            if done:
                . . .

の中に if done ブロック、削除 print 各エピソードの報酬を印刷するステートメント。 代わりに、多くのエピソードの平均報酬を出力します。 The if done ブロックは次のようになります。

/AtariBot/bot_3_q_table.py
            . . .
            if done:
                rewards.append(episode_reward)
                break
                . . .

これらの変更後、ゲームループは次のように一致します。

/AtariBot/bot_3_q_table.py
. . .
    for _ in range(num_episodes):
        state = env.reset()
        episode_reward = 0
        while True:
            action = env.action_space.sample()
            state2, reward, done, _ = env.step(action)
            episode_reward += reward
            state = state2
            if done:
                rewards.append(episode_reward))
                break
                . . .

次に、エージェントが探索と活用の間でトレードオフする機能を追加します。 メインのゲームループの直前( for...)、Q値テーブルを作成します。

/AtariBot/bot_3_q_table.py
. . .
    Q = np.zeros((env.observation_space.n, env.action_space.n))
    for _ in range(num_episodes):
      . . .

次に、 for エピソード番号を公開するためのループ:

/AtariBot/bot_3_q_table.py
. . .
    Q = np.zeros((env.observation_space.n, env.action_space.n))
    for episode in range(1, num_episodes + 1):
      . . .

内部 while True: 内側のゲームループ、作成 noise. Noise 、または無意味なランダムデータは、モデルのパフォーマンスと精度の両方を向上させることができるため、ディープニューラルネットワークをトレーニングするときに導入されることがあります。 ノイズが高いほど、の値が少なくなることに注意してください Q[state, :] 案件。 その結果、ノイズが高いほど、エージェントがゲームの知識とは無関係に行動する可能性が高くなります。 言い換えると、ノイズが高いと、エージェントはランダムなアクションを探索するようになります。

/AtariBot/bot_3_q_table.py
        . . .
        while True:
            noise = np.random.random((1, env.action_space.n)) / (episode**2.)
            action = env.action_space.sample()
            . . .

として注意してください episodes 増加すると、ノイズの量は二乗的に減少します。時間が経つにつれて、エージェントはゲームの報酬の独自の評価を信頼し、その知識を活用し始めることができるため、探索する探索はますます少なくなります。

を更新します action Q値テーブルに従ってエージェントにアクションを選択させるための行。いくつかの調査が組み込まれています。

/AtariBot/bot_3_q_table.py
            . . .
            noise = np.random.random((1, env.action_space.n)) / (episode**2.)
            action = np.argmax(Q[state, :] + noise)
            state2, reward, done, _ = env.step(action)
            . . .

メインのゲームループは次のように一致します。

/AtariBot/bot_3_q_table.py
. . .
    Q = np.zeros((env.observation_space.n, env.action_space.n))
    for episode in range(1, num_episodes + 1):
        state = env.reset()
        episode_reward = 0
        while True:
            noise = np.random.random((1, env.action_space.n)) / (episode**2.)
            action = np.argmax(Q[state, :] + noise)
            state2, reward, done, _ = env.step(action)
            episode_reward += reward
            state = state2
            if done:
                rewards.append(episode_reward)
                break
                . . .

次に、 Bellman更新式を使用してQ値テーブルを更新します。これは、機械学習で広く使用されている式で、特定の環境内で最適なポリシーを見つけます。

ベルマン方程式には、このプロジェクトに非常に関連性のある2つのアイデアが組み込まれています。 まず、特定の状態から特定のアクションを何度も実行すると、その状態とアクションに関連付けられたQ値の適切な見積もりが得られます。 この目的のために、より強力なQ値の推定値を返すために、このボットが再生する必要のあるエピソードの数を増やします。 次に、報酬は時間の経過とともに伝播する必要があるため、元のアクションにはゼロ以外の報酬が割り当てられます。 このアイデアは、報酬が遅れるゲームで最も明確です。 たとえば、スペースインベーダーでは、プレイヤーが撃ったときではなく、エイリアンが爆破されたときにプレイヤーに報酬が与えられます。 ただし、プレイヤーの射撃は報酬の真の推進力です。 同様に、Q関数は(を割り当てる必要がありますstate0, shoot)前向きな報酬。

まず、更新します num_episodes 4000に等しい:

/AtariBot/bot_3_q_table.py
. . .
np.random.seed(0)

num_episodes = 4000
. . .

次に、必要なハイパーパラメータをさらに2つの変数の形式でファイルの先頭に追加します。

/AtariBot/bot_3_q_table.py
. . .
num_episodes = 4000
discount_factor = 0.8
learning_rate = 0.9
. . .

を含む行の直後に、新しいターゲットQ値を計算します env.step(...):

/AtariBot/bot_3_q_table.py
            . . .
            state2, reward, done, _ = env.step(action)
            Qtarget = reward + discount_factor * np.max(Q[state2, :])
            episode_reward += reward
            . . .

直後の行 Qtarget、新旧のQ値の加重平均を使用してQ値テーブルを更新します。

/AtariBot/bot_3_q_table.py
            . . .
            Qtarget = reward + discount_factor * np.max(Q[state2, :])
            Q[state, action] = (1-learning_rate) * Q[state, action] + learning_rate * Qtarget
            episode_reward += reward
            . . .

メインのゲームループが次のように一致することを確認します。

/AtariBot/bot_3_q_table.py
. . .
    Q = np.zeros((env.observation_space.n, env.action_space.n))
    for episode in range(1, num_episodes + 1):
        state = env.reset()
        episode_reward = 0
        while True:
            noise = np.random.random((1, env.action_space.n)) / (episode**2.)
            action = np.argmax(Q[state, :] + noise)
            state2, reward, done, _ = env.step(action)
            Qtarget = reward + discount_factor * np.max(Q[state2, :])
            Q[state, action] = (1-learning_rate) * Q[state, action] + learning_rate * Qtarget
            episode_reward += reward
            state = state2
            if done:
                rewards.append(episode_reward)
                break
                . . .

これで、エージェントをトレーニングするためのロジックが完成しました。 残っているのは、レポートメカニズムを追加することだけです。

Pythonは厳密な型チェックを強制していませんが、クリーンさのために関数宣言に型を追加してください。 ファイルの先頭、最初の行の読み取り前 import gym、インポート List タイプ:

/AtariBot/bot_3_q_table.py
. . .
from typing import List
import gym
. . .

直後の learning_rate = 0.9、外 main 関数、レポートの間隔と形式を宣言します。

/AtariBot/bot_3_q_table.py
. . .
learning_rate = 0.9
report_interval = 500
report = '100-ep Average: %.2f . Best 100-ep Average: %.2f . Average: %.2f ' \
         '(Episode %d)'

def main():
  . . .

の前に main 関数、これを設定する新しい関数を追加します report 文字列、すべての報酬のリストを使用:

/AtariBot/bot_3_q_table.py
. . .
report = '100-ep Average: %.2f . Best 100-ep Average: %.2f . Average: %.2f ' \
         '(Episode %d)'

def print_report(rewards: List, episode: int):
    """Print rewards report for current episode
    - Average for last 100 episodes
    - Best 100-episode average across all time
    - Average for all episodes across time
    """
    print(report % (
        np.mean(rewards[-100:]),
        max([np.mean(rewards[i:i+100]) for i in range(len(rewards) - 100)]),
        np.mean(rewards),
        episode))


def main():
  . . .

ゲームをに変更します FrozenLake それ以外の SpaceInvaders:

/AtariBot/bot_3_q_table.py
. . .
def main():
    env = gym.make('FrozenLake-v0')  # create the game
    . . .

rewards.append(...)、過去100エピソードの平均報酬を印刷し、すべてのエピソードの平均報酬を印刷します。

/AtariBot/bot_3_q_table.py
            . . .
            if done:
                rewards.append(episode_reward)
                if episode % report_interval == 0:
                    print_report(rewards, episode)
                . . .

の終わりに main() 関数、両方の平均をもう一度報告します。 これを行うには、次の行を置き換えます print('Average reward: %.2f' % (sum(rewards) / len(rewards))) 次の強調表示された行で:

/AtariBot/bot_3_q_table.py
. . .
def main():
    ...
                break
    print_report(rewards, -1)
. . .

最後に、Qラーニングエージェントを完了しました。 スクリプトが以下と一致していることを確認してください。

/AtariBot/bot_3_q_table.py
"""
Bot 3 -- Build simple q-learning agent for FrozenLake
"""

from typing import List
import gym
import numpy as np
import random
random.seed(0)  # make results reproducible
np.random.seed(0)  # make results reproducible

num_episodes = 4000
discount_factor = 0.8
learning_rate = 0.9
report_interval = 500
report = '100-ep Average: %.2f . Best 100-ep Average: %.2f . Average: %.2f ' \
         '(Episode %d)'


def print_report(rewards: List, episode: int):
    """Print rewards report for current episode
    - Average for last 100 episodes
    - Best 100-episode average across all time
    - Average for all episodes across time
    """
    print(report % (
        np.mean(rewards[-100:]),
        max([np.mean(rewards[i:i+100]) for i in range(len(rewards) - 100)]),
        np.mean(rewards),
        episode))


def main():
    env = gym.make('FrozenLake-v0')  # create the game
    env.seed(0)  # make results reproducible
    rewards = []

    Q = np.zeros((env.observation_space.n, env.action_space.n))
    for episode in range(1, num_episodes + 1):
        state = env.reset()
        episode_reward = 0
        while True:
            noise = np.random.random((1, env.action_space.n)) / (episode**2.)
            action = np.argmax(Q[state, :] + noise)
            state2, reward, done, _ = env.step(action)
            Qtarget = reward + discount_factor * np.max(Q[state2, :])
            Q[state, action] = (1-learning_rate) * Q[state, action] + learning_rate * Qtarget
            episode_reward += reward
            state = state2
            if done:
                rewards.append(episode_reward)
                if episode % report_interval == 0:
                    print_report(rewards, episode)
                break
    print_report(rewards, -1)

if __name__ == '__main__':
    main()

ファイルを保存し、エディターを終了して、スクリプトを実行します。

  1. python bot_3_q_table.py

出力は次のように一致します。

Output
100-ep Average: 0.11 . Best 100-ep Average: 0.12 . Average: 0.03 (Episode 500) 100-ep Average: 0.25 . Best 100-ep Average: 0.24 . Average: 0.09 (Episode 1000) 100-ep Average: 0.39 . Best 100-ep Average: 0.48 . Average: 0.19 (Episode 1500) 100-ep Average: 0.43 . Best 100-ep Average: 0.55 . Average: 0.25 (Episode 2000) 100-ep Average: 0.44 . Best 100-ep Average: 0.55 . Average: 0.29 (Episode 2500) 100-ep Average: 0.64 . Best 100-ep Average: 0.68 . Average: 0.32 (Episode 3000) 100-ep Average: 0.63 . Best 100-ep Average: 0.71 . Average: 0.36 (Episode 3500) 100-ep Average: 0.56 . Best 100-ep Average: 0.78 . Average: 0.40 (Episode 4000) 100-ep Average: 0.56 . Best 100-ep Average: 0.78 . Average: 0.40 (Episode -1)

これで、ゲーム用の最初の重要なボットができましたが、この平均的な報酬を 0.78 視点に。 Gym FrozenLakeページによると、ゲームを「解決する」とは、100エピソードの平均を達成することを意味します。 0.78. 非公式には、「解決する」とは「ゲームを上手にプレイする」ことを意味します。 記録的な速さではありませんが、Q-tableエージェントは4000回のエピソードでFrozenLakeを解決することができます。

ただし、ゲームはもっと複雑な場合があります。 ここでは、テーブルを使用して144の可能な状態すべてを格納しましたが、19,683の可能な状態がある三目並べを検討してください。 同様に、数えきれないほど多くの可能性のある状態があるスペースインベーダーを考えてみてください。 ゲームがますます複雑になるにつれて、Qテーブルは持続可能ではありません。 このため、Qテーブルを概算する方法が必要です。 次のステップで実験を続けると、状態とアクションを入力として受け入れ、Q値を出力できる関数を設計します。

ステップ4—FrozenLakeのディープQ学習エージェントを構築する

強化学習では、ニューラルネットワークは以下に基づいてQの値を効果的に予測します。 stateaction 可能なすべての値を格納するためにテーブルを使用して入力しますが、これは複雑なゲームでは不安定になります。 深層強化学習は、代わりにニューラルネットワークを使用してQ関数を近似します。 詳細については、ディープQ学習についてを参照してください。

手順1でインストールしたディープラーニングライブラリであるTensorflowに慣れるには、これまでに使用されていたすべてのロジックをTensorflowの抽象化で再実装し、ニューラルネットワークを使用してQ関数を近似します。 ただし、ニューラルネットワークは非常に単純になります。出力 Q(s) 行列です W 入力を掛けたもの s. これは、1つの完全に接続されたレイヤーを持つニューラルネットワークとして知られています。

Q(s) = Ws

繰り返しになりますが、目標は、Tensorflowの抽象化を使用してすでに構築したボットからすべてのロジックを再実装することです。 これにより、TensorflowがGPUですべての計算を実行できるため、操作がより効率的になります。

ステップ3のQテーブルスクリプトを複製することから始めます。

  1. cp bot_3_q_table.py bot_4_q_network.py

次に、で新しいファイルを開きます nano またはお好みのテキストエディタ:

  1. nano bot_4_q_network.py

まず、ファイルの上部にあるコメントを更新します。

/AtariBot/bot_4_q_network.py
"""
Bot 4 -- Use Q-learning network to train bot
"""

. . .

次に、Tensorflowパッケージを追加してインポートします import すぐ下のディレクティブ import random. さらに、追加 tf.set_radon_seed(0) 真下 np.random.seed(0). これにより、このスクリプトの結果がすべてのセッションで再現可能になります。

/AtariBot/bot_4_q_network.py
. . .
import random
import tensorflow as tf
random.seed(0)
np.random.seed(0)
tf.set_random_seed(0)
. . .

ファイルの先頭にあるハイパーパラメータを次のように再定義し、次の関数を追加します。 exploration_probability、各ステップでの探索の確率を返します。 このコンテキストでは、「探索」とは、Q値の見積もりで推奨されているアクションを実行するのではなく、ランダムなアクションを実行することを意味することを忘れないでください。

/AtariBot/bot_4_q_network.py
. . .
num_episodes = 4000
discount_factor = 0.99
learning_rate = 0.15
report_interval = 500
exploration_probability = lambda episode: 50. / (episode + 10)
report = '100-ep Average: %.2f . Best 100-ep Average: %.2f . Average: %.2f ' \
         '(Episode %d)'
. . .

次に、ワンホットエンコーディング関数を追加します。 要するに、ワンホットエンコーディングは、変数が機械学習アルゴリズムがより良い予測を行うのに役立つ形式に変換されるプロセスです。 ワンホットエンコーディングについて詳しく知りたい場合は、コンピュータービジョンの敵対的な例:感情ベースの犬のフィルターを構築してからかう方法を確認してください。

真下 report = ...、を追加します one_hot 関数:

/AtariBot/bot_4_q_network.py
. . .
report = '100-ep Average: %.2f . Best 100-ep Average: %.2f . Average: %.2f ' \
         '(Episode %d)'

def one_hot(i: int, n: int) -> np.array:
    """Implements one-hot encoding by selecting the ith standard basis vector"""
    return np.identity(n)[i].reshape((1, -1))

def print_report(rewards: List, episode: int):
. . .

次に、Tensorflowの抽象化を使用してアルゴリズムロジックを書き直します。 ただし、その前に、まずデータのプレースホルダーを作成する必要があります。

あなたの中で main 機能、真下 rewards=[]、次の強調表示されたコンテンツを挿入します。 ここでは、時間 t での観測のプレースホルダーを定義します(として obs_t_ph)と時間 t + 1 (as obs_tp1_ph)、およびアクション、報酬、Qターゲットのプレースホルダー:

/AtariBot/bot_4_q_network.py
. . .
def main():
    env = gym.make('FrozenLake-v0')  # create the game
    env.seed(0)  # make results reproducible
    rewards = []

    # 1. Setup placeholders
    n_obs, n_actions = env.observation_space.n, env.action_space.n
    obs_t_ph = tf.placeholder(shape=[1, n_obs], dtype=tf.float32)
    obs_tp1_ph = tf.placeholder(shape=[1, n_obs], dtype=tf.float32)
    act_ph = tf.placeholder(tf.int32, shape=())
    rew_ph = tf.placeholder(shape=(), dtype=tf.float32)
    q_target_ph = tf.placeholder(shape=[1, n_actions], dtype=tf.float32)

    Q = np.zeros((env.observation_space.n, env.action_space.n))
    for episode in range(1, num_episodes + 1):
        . . .

で始まる線の真下 q_target_ph =、次の強調表示された行を挿入します。 このコードは、すべての aに対してQ(s、a)を計算することにより、計算を開始します。 q_current およびQ(s’、a’)すべてのa’を作成する q_target:

/AtariBot/bot_4_q_network.py
    . . .
    rew_ph = tf.placeholder(shape=(), dtype=tf.float32)
    q_target_ph = tf.placeholder(shape=[1, n_actions], dtype=tf.float32)

    # 2. Setup computation graph
    W = tf.Variable(tf.random_uniform([n_obs, n_actions], 0, 0.01))
    q_current = tf.matmul(obs_t_ph, W)
    q_target = tf.matmul(obs_tp1_ph, W)

    Q = np.zeros((env.observation_space.n, env.action_space.n))
    for episode in range(1, num_episodes + 1):
        . . .

追加した最後の行のすぐ下に、次の強調表示されたコードを挿入します。 最初の2行は、ステップ3で追加された次の行と同等です。 Qtarget、 どこ Qtarget = reward + discount_factor * np.max(Q[state2, :]). 次の2行は損失を設定し、最後の行はQ値を最大化するアクションを計算します。

/AtariBot/bot_4_q_network.py
    . . .
    q_current = tf.matmul(obs_t_ph, W)
    q_target = tf.matmul(obs_tp1_ph, W)

    q_target_max = tf.reduce_max(q_target_ph, axis=1)
    q_target_sa = rew_ph + discount_factor * q_target_max
    q_current_sa = q_current[0, act_ph]
    error = tf.reduce_sum(tf.square(q_target_sa - q_current_sa))
    pred_act_ph = tf.argmax(q_current, 1)

    Q = np.zeros((env.observation_space.n, env.action_space.n))
    for episode in range(1, num_episodes + 1):
        . . .

アルゴリズムと損失関数を設定した後、オプティマイザーを定義します。

/AtariBot/bot_4_q_network.py
    . . .
    error = tf.reduce_sum(tf.square(q_target_sa - q_current_sa))
    pred_act_ph = tf.argmax(q_current, 1)

    # 3. Setup optimization
    trainer = tf.train.GradientDescentOptimizer(learning_rate=learning_rate)
    update_model = trainer.minimize(error)

    Q = np.zeros((env.observation_space.n, env.action_space.n))
    for episode in range(1, num_episodes + 1):
        . . .

次に、ゲームループの本体を設定します。 これを行うには、データをTensorflowプレースホルダーに渡します。そうすると、Tensorflowの抽象化によってGPUでの計算が処理され、アルゴリズムの結果が返されます。

古いQテーブルとロジックを削除することから始めます。 具体的には、定義する行を削除します Q (直前 for ループ)、 noise (の中に while ループ)、 action, Qtarget、 と Q[state, action]. 名前を変更 stateobs_tstate2obs_tp1 以前に設定したTensorflowプレースホルダーに合わせます。 終了したら、 for ループは次のように一致します。

/AtariBot/bot_4_q_network.py
    . . .
    # 3. Setup optimization
    trainer = tf.train.GradientDescentOptimizer(learning_rate=learning_rate)
    update_model = trainer.minimize(error)

    for episode in range(1, num_episodes + 1):
        obs_t = env.reset()
        episode_reward = 0
        while True:

            obs_tp1, reward, done, _ = env.step(action)

            episode_reward += reward
            obs_t = obs_tp1
            if done:
                ...

真上 for ループし、次の2つの強調表示された行を追加します。 これらの行はTensorflowセッションを初期化し、TensorflowセッションはGPUで操作を実行するために必要なリソースを管理します。 2行目は、計算グラフのすべての変数を初期化します。 たとえば、重みを更新する前に、重みを0に初期化します。 さらに、ネストします for 内のループ with ステートメントなので、全体をインデントします for 4つのスペースでループします。

/AtariBot/bot_4_q_network.py
    . . .
    trainer = tf.train.GradientDescentOptimizer(learning_rate=learning_rate)
        update_model = trainer.minimize(error)

    with tf.Session() as session:
        session.run(tf.global_variables_initializer())

        for episode in range(1, num_episodes + 1):
            obs_t = env.reset()
            ...

行の読み取り前 obs_tp1, reward, done, _ = env.step(action)、次の行を挿入して計算します action. このコードは、対応するプレースホルダーを評価し、アクションをランダムなアクションに置き換えます。

/AtariBot/bot_4_q_network.py
            . . .
            while True:
                # 4. Take step using best action or random action
                obs_t_oh = one_hot(obs_t, n_obs)
                action = session.run(pred_act_ph, feed_dict={obs_t_ph: obs_t_oh})[0]
                if np.random.rand(1) < exploration_probability(episode):
                    action = env.action_space.sample()
                . . .

を含む行の後 env.step(action)、以下を挿入して、Q値関数を推定するニューラルネットワークをトレーニングします。

/AtariBot/bot_4_q_network.py
                . . .
                obs_tp1, reward, done, _ = env.step(action)

                # 5. Train model
                obs_tp1_oh = one_hot(obs_tp1, n_obs)
                q_target_val = session.run(q_target, feed_dict={obs_tp1_ph: obs_tp1_oh})
                session.run(update_model, feed_dict={
                    obs_t_ph: obs_t_oh,
                    rew_ph: reward,
                    q_target_ph: q_target_val,
                    act_ph: action
                })
                episode_reward += reward
                . . .

最終的なファイルは、GitHubでホストされているこのファイルと一致します。 ファイルを保存し、エディターを終了して、スクリプトを実行します。

  1. python bot_4_q_network.py

出力は、正確に次のように終了します。

Output
100-ep Average: 0.11 . Best 100-ep Average: 0.11 . Average: 0.05 (Episode 500) 100-ep Average: 0.41 . Best 100-ep Average: 0.54 . Average: 0.19 (Episode 1000) 100-ep Average: 0.56 . Best 100-ep Average: 0.73 . Average: 0.31 (Episode 1500) 100-ep Average: 0.57 . Best 100-ep Average: 0.73 . Average: 0.36 (Episode 2000) 100-ep Average: 0.65 . Best 100-ep Average: 0.73 . Average: 0.41 (Episode 2500) 100-ep Average: 0.65 . Best 100-ep Average: 0.73 . Average: 0.43 (Episode 3000) 100-ep Average: 0.69 . Best 100-ep Average: 0.73 . Average: 0.46 (Episode 3500) 100-ep Average: 0.77 . Best 100-ep Average: 0.79 . Average: 0.48 (Episode 4000) 100-ep Average: 0.77 . Best 100-ep Average: 0.79 . Average: 0.48 (Episode -1)

これで、最初の深いQ学習エージェントをトレーニングしました。 FrozenLakeのような単純なゲームの場合、深いQ学習エージェントはトレーニングに4000エピソードを必要としました。 ゲームがはるかに複雑だったと想像してみてください。 トレーニングに必要なトレーニングサンプルはいくつですか? 結局のところ、エージェントは数百万のサンプルを必要とする可能性があります。 必要なサンプルの数は、サンプルの複雑さと呼ばれ、次のセクションでさらに詳しく説明する概念です。

バイアスと分散のトレードオフを理解する

一般的に、サンプルの複雑さは、機械学習のモデルの複雑さと対立しています。

  1. モデルの複雑さ:問題を解決するために十分に複雑なモデルが必要です。 たとえば、線のように単純なモデルは、車の軌道を予測するのに十分なほど複雑ではありません。
  2. サンプルの複雑さ:多くのサンプルを必要としないモデルが必要です。 これは、ラベル付けされたデータへのアクセスが制限されている、計算能力が不十分である、メモリが制限されているなどが原因である可能性があります。

単純なモデルと非常に複雑なモデルの2つのモデルがあるとします。 両方のモデルが同じパフォーマンスを達成するために、バイアス分散は、非常に複雑なモデルがトレーニングするために指数関数的に多くのサンプルを必要とすることを示しています。 適切な例:ニューラルネットワークベースのQ学習エージェントは、FrozenLakeを解決するために4000回のエピソードを必要としました。 ニューラルネットワークエージェントに2番目のレイヤーを追加すると、必要なトレーニングエピソードの数が4倍になります。 ますます複雑になるニューラルネットワークでは、この分裂は拡大するだけです。 同じエラー率を維持するために、モデルの複雑さを増やすと、サンプルの複雑さが指数関数的に増加します。 同様に、サンプルの複雑さを減らすと、モデルの複雑さが減ります。 したがって、モデルの複雑さを最大化し、サンプルの複雑さを最小化することはできません。

ただし、このトレードオフに関する知識を活用することはできます。 偏りと分散の分解の背後にある数学の視覚的な解釈については、偏りと分散のトレードオフについてを参照してください。 大まかに言えば、偏りと分散の分解は、「真の誤差」をバイアスと分散の2つの要素に分解したものです。 「真の誤差」を平均二乗誤差(MSE)と呼びます。これは、予測されたラベルと真のラベルの予想される差です。 以下は、モデルの複雑さが増すにつれて「真のエラー」が変化することを示すプロットです。

ステップ5—冷凍湖の最小二乗エージェントを構築する

最小二乗法は、線形回帰とも呼ばれ、数学やデータサイエンスの分野で広く使用されている回帰分析の手段です。 機械学習では、2つのパラメーターまたはデータセットの最適な線形モデルを見つけるためによく使用されます。

ステップ4では、Q値を計算するためのニューラルネットワークを構築しました。 ニューラルネットワークの代わりに、このステップでは、最小二乗法の変形であるリッジ回帰を使用して、このQ値のベクトルを計算します。 最小二乗法のように単純なモデルを使用すると、ゲームを解くために必要なトレーニングエピソードが少なくなることが期待されます。

ステップ3のスクリプトを複製することから始めます。

  1. cp bot_3_q_table.py bot_5_ls.py

新しいファイルを開きます。

  1. nano bot_5_ls.py

繰り返しますが、このスクリプトが何をするかを説明するファイルの上部にあるコメントを更新します。

/AtariBot/bot_4_q_network.py
"""
Bot 5 -- Build least squares q-learning agent for FrozenLake
"""

. . .

ファイルの先頭近くにあるインポートのブロックの前に、タイプチェック用にさらに2つのインポートを追加します。

/AtariBot/bot_5_ls.py
. . .
from typing import Tuple
from typing import Callable
from typing import List
import gym
. . .

ハイパーパラメータのリストに、別のハイパーパラメータを追加します。 w_lr、2番目のQ関数の学習率を制御します。 さらに、エピソード数を5000に更新し、割引係数をに更新します 0.85. 両方を変更することによって num_episodesdiscount_factor ハイパーパラメータをより大きな値にすると、エージェントはより強力なパフォーマンスを発行できるようになります。

/AtariBot/bot_5_ls.py
. . .
num_episodes = 5000
discount_factor = 0.85
learning_rate = 0.9
w_lr = 0.5
report_interval = 500
. . .

あなたの前に print_report 関数には、次の高階関数を追加します。 モデルを抽象化するラムダ(無名関数)を返します。

/AtariBot/bot_5_ls.py
. . .
report_interval = 500
report = '100-ep Average: %.2f . Best 100-ep Average: %.2f . Average: %.2f ' \
         '(Episode %d)'

def makeQ(model: np.array) -> Callable[[np.array], np.array]:
    """Returns a Q-function, which takes state -> distribution over actions"""
    return lambda X: X.dot(model)

def print_report(rewards: List, episode: int):
    . . .

makeQ、別の関数を追加し、 initialize、正規分布の値を使用してモデルを初期化します。

/AtariBot/bot_5_ls.py
. . .
def makeQ(model: np.array) -> Callable[[np.array], np.array]:
    """Returns a Q-function, which takes state -> distribution over actions"""
    return lambda X: X.dot(model)

def initialize(shape: Tuple):
    """Initialize model"""
    W = np.random.normal(0.0, 0.1, shape)
    Q = makeQ(W)
    return W, Q

def print_report(rewards: List, episode: int):
    . . .

後に initialize ブロック、追加 train リッジ回帰の閉形式の解を計算し、古いモデルを新しいモデルで重み付けする方法。 モデルと抽象化されたQ関数の両方を返します。

/AtariBot/bot_5_ls.py
. . .
def initialize(shape: Tuple):
    ...
    return W, Q

def train(X: np.array, y: np.array, W: np.array) -> Tuple[np.array, Callable]:
    """Train the model, using solution to ridge regression"""
    I = np.eye(X.shape[1])
    newW = np.linalg.inv(X.T.dot(X) + 10e-4 * I).dot(X.T.dot(y))
    W = w_lr * newW + (1 - w_lr) * W
    Q = makeQ(W)
    return W, Q

def print_report(rewards: List, episode: int):
    . . .

train、最後の関数を1つ追加します。 one_hot、状態とアクションに対してワンホットエンコーディングを実行するには:

/AtariBot/bot_5_ls.py
. . .
def train(X: np.array, y: np.array, W: np.array) -> Tuple[np.array, Callable]:
    ...
    return W, Q

def one_hot(i: int, n: int) -> np.array:
    """Implements one-hot encoding by selecting the ith standard basis vector"""
    return np.identity(n)[i]

def print_report(rewards: List, episode: int):
    . . .

これに続いて、トレーニングロジックを変更する必要があります。 前に作成したスクリプトでは、Qテーブルは反復ごとに更新されていました。 ただし、このスクリプトは、タイムステップごとにサンプルとラベルを収集し、10ステップごとに新しいモデルをトレーニングします。 さらに、Qテーブルまたはニューラルネットワークを保持する代わりに、最小二乗モデルを使用してQ値を予測します。

に移動します main 関数を作成し、Qテーブルの定義を置き換えます(Q = np.zeros(...))次のように:

/AtariBot/bot_5_ls.py
. . .
def main():
    ...
    rewards = []

    n_obs, n_actions = env.observation_space.n, env.action_space.n
    W, Q = initialize((n_obs, n_actions))
    states, labels = [], []
    for episode in range(1, num_episodes + 1):
        . . .

の前に下にスクロールします for ループ。 このすぐ下に、次の行を追加して、 stateslabels 保存されている情報が多すぎる場合のリスト:

/AtariBot/bot_5_ls.py
. . .
def main():
    ...
    for episode in range(1, num_episodes + 1):
        if len(states) >= 10000:
            states, labels = [], []
            . . .

この行の直後の行を変更します。 state = env.reset()、次のようになります。 すべての使用法でワンホットベクトルが必要になるため、これにより状態がすぐにワンホットエンコードされます。

/AtariBot/bot_5_ls.py
. . .
    for episode in range(1, num_episodes + 1):
        if len(states) >= 10000:
            states, labels = [], []
        state = one_hot(env.reset(), n_obs)
. . .

あなたの最初の行の前 while メインゲームループ、リストを修正 states:

/AtariBot/bot_5_ls.py
. . .
    for episode in range(1, num_episodes + 1):
        ...
        episode_reward = 0
        while True:
            states.append(state)
            noise = np.random.random((1, env.action_space.n)) / (episode**2.)
            . . .

の計算を更新します action、ノイズの確率を減らし、Q関数の評価を変更します。

/AtariBot/bot_5_ls.py
. . .
        while True:
            states.append(state)
            noise = np.random.random((1, n_actions)) / episode
            action = np.argmax(Q(state) + noise)
            state2, reward, done, _ = env.step(action)
            . . .

のワンホットバージョンを追加する state2 定義内のQ関数呼び出しを次のように修正します。 Qtarget 次のように:

/AtariBot/bot_5_ls.py
. . .
        while True:
            ...
            state2, reward, done, _ = env.step(action)

            state2 = one_hot(state2, n_obs)
            Qtarget = reward + discount_factor * np.max(Q(state2))
            . . .

更新する行を削除します Q[state,action] = ... 次の行に置き換えます。 このコードは、現在のモデルの出力を取得し、実行された現在のアクションに対応するこの出力の値のみを更新します。 結果として、他のアクションのQ値は損失を被りません。

/AtariBot/bot_5_ls.py
. . .
            state2 = one_hot(state2, n_obs)
            Qtarget = reward + discount_factor * np.max(Q(state2))
            label = Q(state)
            label[action] = (1 - learning_rate) * label[action] + learning_rate * Qtarget
            labels.append(label)

            episode_reward += reward
            . . .

直後の state = state2、モデルに定期的な更新を追加します。 これにより、10タイムステップごとにモデルがトレーニングされます。

/AtariBot/bot_5_ls.py
. . .
            state = state2
            if len(states) % 10 == 0:
                W, Q = train(np.array(states), np.array(labels), W)
            if done:
            . . .

ファイルがソースコードと一致することを再確認してください。 次に、ファイルを保存し、エディターを終了して、スクリプトを実行します。

  1. python bot_5_ls.py

これにより、次のように出力されます。

Output
100-ep Average: 0.17 . Best 100-ep Average: 0.17 . Average: 0.09 (Episode 500) 100-ep Average: 0.11 . Best 100-ep Average: 0.24 . Average: 0.10 (Episode 1000) 100-ep Average: 0.08 . Best 100-ep Average: 0.24 . Average: 0.10 (Episode 1500) 100-ep Average: 0.24 . Best 100-ep Average: 0.25 . Average: 0.11 (Episode 2000) 100-ep Average: 0.32 . Best 100-ep Average: 0.31 . Average: 0.14 (Episode 2500) 100-ep Average: 0.35 . Best 100-ep Average: 0.38 . Average: 0.16 (Episode 3000) 100-ep Average: 0.59 . Best 100-ep Average: 0.62 . Average: 0.22 (Episode 3500) 100-ep Average: 0.66 . Best 100-ep Average: 0.66 . Average: 0.26 (Episode 4000) 100-ep Average: 0.60 . Best 100-ep Average: 0.72 . Average: 0.30 (Episode 4500) 100-ep Average: 0.75 . Best 100-ep Average: 0.82 . Average: 0.34 (Episode 5000) 100-ep Average: 0.75 . Best 100-ep Average: 0.82 . Average: 0.34 (Episode -1)

Gym FrozenLakeページによると、ゲームを「解決する」とは、100エピソードの平均で0.78を達成することを意味することを思い出してください。 ここで、エージェントは平均0.82を達成します。これは、5000回のエピソードでゲームを解決できたことを意味します。 これは少数のエピソードでゲームを解決するわけではありませんが、この基本的な最小二乗法は、トレーニングエピソードの数がほぼ同じである単純なゲームを解決することができます。 ニューラルネットワークは複雑になる可能性がありますが、FrozenLakeには単純なモデルで十分であることを示しました。

これで、3つのQ学習エージェントを探索しました。1つはQテーブルを使用し、もう1つはニューラルネットワークを使用し、3つ目は最小二乗法を使用します。 次に、より複雑なゲームであるスペースインベーダーのための深い強化学習エージェントを構築します。

ステップ6—宇宙侵略者のための深いQ学習エージェントを作成する

ニューラルネットワークまたは最小二乗法のどちらを選択したかに関係なく、以前のQ学習アルゴリズムのモデルの複雑さとサンプルの複雑さを完全に調整したとします。 結局のところ、このインテリジェントでないQ学習エージェントは、トレーニングエピソードの数が特に多い場合でも、より複雑なゲームではパフォーマンスが低下します。 このセクションでは、パフォーマンスを向上させることができる2つの手法について説明し、次に、これらの手法を使用してトレーニングされたエージェントをテストします。

人間の介入なしにその行動を継続的に適応させることができる最初の汎用エージェントは、さまざまなAtariゲームをプレイするようにエージェントをトレーニングしたDeepMindの研究者によって開発されました。 DeepMindのオリジナルのディープQラーニング(DQN)ペーパーは、2つの重要な問題を認識しました。

  1. 相関状態:時間0でのゲームの状態を取得します。これを、s0と呼びます。 以前に導出したルールに従って、 Q(s0)を更新するとします。 ここで、 s1 と呼ばれる時間1の状態を取得し、同じルールに従って Q(s1)を更新します。 時間0でのゲームの状態は、時間1での状態と非常に似ていることに注意してください。 たとえば、スペースインベーダーでは、エイリアンがそれぞれ1ピクセルずつ移動した可能性があります。 もっと簡潔に言うと、s0s1は非常に似ています。 同様に、 Q(s0) Q(s1)も非常に似ていると予想されるため、一方を更新すると他方に影響します。 Q(s0)への更新は、実際には Q(s1)への更新に対抗する可能性があるため、これによりQ値が変動します。 より正式には、s0s1相関です。 Q関数は決定論的であるため、 Q(s1) Q(s0)と相関しています。
  2. Q関数の不安定性 Q 関数は、トレーニングするモデルであり、ラベルのソースでもあることを思い出してください。 ラベルは、分布Lを真に表すランダムに選択された値であるとします。 Q を更新するたびに、 L を変更します。これは、モデルが移動するターゲットを学習しようとしていることを意味します。 使用するモデルは固定分布を想定しているため、これは問題です。

相関状態と不安定なQ関数と戦うには:

  1. リプレイバッファと呼ばれる状態のリストを保持することができます。 タイムステップごとに、観察したゲームの状態をこのリプレイバッファーに追加します。 また、このリストから状態のサブセットをランダムにサンプリングし、それらの状態でトレーニングします。
  2. DeepMindのチームは、 Q(s、a)を複製しました。 1つはQ_current(s、a)と呼ばれ、更新するQ関数です。 後続の状態には別のQ関数Q_target(s’、a’)が必要ですが、これは更新されません。 Q_target(s’、a’)がラベルの生成に使用されていることを思い出してください。 Q_currentQ_targetから分離し、後者を修正することで、ラベルのサンプリング元の分布を修正します。 次に、深層学習モデルは、この分布の学習に短期間を費やすことができます。 しばらくすると、Q_currentを新しいQ_targetに再複製します。

これらを自分で実装することはありませんが、これらのソリューションでトレーニングされた事前トレーニング済みモデルをロードします。 これを行うには、これらのモデルのパラメータを保存する新しいディレクトリを作成します。

  1. mkdir models

次に、 wget 事前にトレーニングされたスペースインベーダーモデルのパラメーターをダウンロードするには:

  1. wget http://models.tensorpack.com/OpenAIGym/SpaceInvaders-v0.tfmodel -P models

次に、ダウンロードしたパラメーターに関連付けられたモデルを指定するPythonスクリプトをダウンロードします。 この事前トレーニング済みモデルには、覚えておく必要のある入力に対する2つの制約があることに注意してください。

  • 状態は、84 x 84にダウンサンプリングするか、サイズを縮小する必要があります。
  • 入力は、スタックされた4つの状態で構成されます。

これらの制約については、後で詳しく説明します。 今のところ、次のように入力してスクリプトをダウンロードします。

  1. wget https://github.com/alvinwan/bots-for-atari-games/raw/master/src/bot_6_a3c.py

次に、この事前トレーニング済みのスペースインベーダーエージェントを実行して、そのパフォーマンスを確認します。 これまで使用してきたいくつかのボットとは異なり、このスクリプトは最初から作成します。

新しいスクリプトファイルを作成します。

  1. nano bot_6_dqn.py

このスクリプトを開始するには、ヘッダーコメントを追加し、必要なユーティリティをインポートして、メインのゲームループを開始します。

/AtariBot/bot_6_dqn.py
"""
Bot 6 - Fully featured deep q-learning network.
"""

import cv2
import gym
import numpy as np
import random
import tensorflow as tf
from bot_6_a3c import a3c_model


def main():

if __name__ == '__main__':
    main()

インポートの直後に、ランダムなシードを設定して、結果を再現可能にします。 また、ハイパーパラメータを定義します num_episodes これにより、エージェントを実行するエピソードの数がスクリプトに通知されます。

/AtariBot/bot_6_dqn.py
. . .
import tensorflow as tf
from bot_6_a3c import a3c_model
random.seed(0)  # make results reproducible
tf.set_random_seed(0)

num_episodes = 10

def main():
  . . .

宣言後2行 num_episodes、定義 downsample すべての画像を84×84のサイズにダウンサンプリングする関数。 事前トレーニング済みモデルは84×84画像でトレーニングされているため、事前トレーニング済みニューラルネットワークに渡す前に、すべての画像をダウンサンプリングします。

/AtariBot/bot_6_dqn.py
. . .
num_episodes = 10

def downsample(state):
    return cv2.resize(state, (84, 84), interpolation=cv2.INTER_LINEAR)[None]

def main():
  . . .

開始時にゲーム環境を作成します main 結果が再現可能になるように、環境を機能させてシードします。

/AtariBot/bot_6_dqn.py
. . .
def main():
    env = gym.make('SpaceInvaders-v0')  # create the game
    env.seed(0)  # make results reproducible
    . . .

環境シードの直後に、報酬を保持するために空のリストを初期化します。

/AtariBot/bot_6_dqn.py
. . .
def main():
    env = gym.make('SpaceInvaders-v0')  # create the game
    env.seed(0)  # make results reproducible
    rewards = []
    . . .

このステップの最初にダウンロードした事前トレーニング済みモデルパラメーターを使用して、事前トレーニング済みモデルを初期化します。

/AtariBot/bot_6_dqn.py
. . .
def main():
    env = gym.make('SpaceInvaders-v0')  # create the game
    env.seed(0)  # make results reproducible
    rewards = []
    model = a3c_model(load='models/SpaceInvaders-v0.tfmodel')
    . . .

次に、スクリプトに反復するように指示する行をいくつか追加します num_episodes 平均パフォーマンスを計算し、各エピソードの報酬を0に初期化するための時間。 さらに、環境をリセットする行を追加します(env.reset())、プロセスで新しい初期状態を収集し、この初期状態を次のようにダウンサンプリングします。 downsample()、およびを使用してゲームループを開始します while ループ:

/AtariBot/bot_6_dqn.py
. . .
def main():
    env = gym.make('SpaceInvaders-v0')  # create the game
    env.seed(0)  # make results reproducible
    rewards = []
    model = a3c_model(load='models/SpaceInvaders-v0.tfmodel')
    for _ in range(num_episodes):
        episode_reward = 0
        states = [downsample(env.reset())]
        while True:
        . . .

新しいニューラルネットワークは、一度に1つの状態を受け入れる代わりに、一度に4つの状態を受け入れます。 結果として、あなたはのリストまで待たなければなりません states 事前トレーニング済みモデルを適用する前に、少なくとも4つの状態が含まれています。 行の読み取り値の下に次の行を追加します while True:. これらは、状態が4つ未満の場合はランダムなアクションを実行するか、状態を連結して少なくとも4つある場合は事前トレーニング済みモデルに渡すようにエージェントに指示します。

/AtariBot/bot_6_dqn.py
        . . .
        while True:
            if len(states) < 4:
                action = env.action_space.sample()
            else:
                frames = np.concatenate(states[-4:], axis=3)
                action = np.argmax(model([frames]))
                . . .

次に、アクションを実行して、関連するデータを更新します。 観察された状態のダウンサンプリングされたバージョンを追加し、このエピソードの報酬を更新します。

/AtariBot/bot_6_dqn.py
        . . .
        while True:
            ...
                action = np.argmax(model([frames]))
            state, reward, done, _ = env.step(action)
            states.append(downsample(state))
            episode_reward += reward
            . . .

次に、エピソードがであるかどうかを確認する次の行を追加します done もしそうなら、エピソードの合計報酬を印刷し、すべての結果のリストを修正して、 while 早くループする:

/AtariBot/bot_6_dqn.py
        . . .
        while True:
            ...
            episode_reward += reward
            if done:
                print('Reward: %d' % episode_reward)
                rewards.append(episode_reward)
                break
                . . .

の外 whilefor ループ、平均報酬を印刷します。 これをあなたの最後に置いてください main 関数:

/AtariBot/bot_6_dqn.py
def main():
    ...
                break
    print('Average reward: %.2f' % (sum(rewards) / len(rewards)))

ファイルが以下と一致することを確認してください。

/AtariBot/bot_6_dqn.py
"""
Bot 6 - Fully featured deep q-learning network.
"""

import cv2
import gym
import numpy as np
import random
import tensorflow as tf
from bot_6_a3c import a3c_model
random.seed(0)  # make results reproducible
tf.set_random_seed(0)

num_episodes = 10


def downsample(state):
    return cv2.resize(state, (84, 84), interpolation=cv2.INTER_LINEAR)[None]

def main():
    env = gym.make('SpaceInvaders-v0')  # create the game
    env.seed(0)  # make results reproducible
    rewards = []

    model = a3c_model(load='models/SpaceInvaders-v0.tfmodel')
    for _ in range(num_episodes):
        episode_reward = 0
        states = [downsample(env.reset())]
        while True:
            if len(states) < 4:
                action = env.action_space.sample()
            else:
                frames = np.concatenate(states[-4:], axis=3)
                action = np.argmax(model([frames]))
            state, reward, done, _ = env.step(action)
            states.append(downsample(state))
            episode_reward += reward
            if done:
                print('Reward: %d' % episode_reward)
                rewards.append(episode_reward)
                break
    print('Average reward: %.2f' % (sum(rewards) / len(rewards)))


if __name__ == '__main__':
    main()

ファイルを保存して、エディターを終了します。 次に、スクリプトを実行します。

  1. python bot_6_dqn.py

出力は次のように終了します。

Output
. . . Reward: 1230 Reward: 4510 Reward: 1860 Reward: 2555 Reward: 515 Reward: 1830 Reward: 4100 Reward: 4350 Reward: 1705 Reward: 4905 Average reward: 2756.00

これを、SpaceInvadersのランダムエージェントを実行した最初のスクリプトの結果と比較してください。 その場合の平均報酬はわずか約150でした。つまり、この結果は20倍以上優れています。 ただし、コードはかなり遅いため、3つのエピソードに対してのみ実行し、3つのエピソードの平均は信頼できるメトリックではありません。 これを10エピソードにわたって実行すると、平均は2756です。 100話以上、平均は約2500です。 これらの平均によってのみ、エージェントのパフォーマンスが実際に1桁向上し、スペースインベーダーを適度に上手くプレイするエージェントができたと快適に結論付けることができます。

ただし、サンプルの複雑さに関して前のセクションで提起された問題を思い出してください。 結局のところ、このスペースインベーダーエージェントはトレーニングに何百万ものサンプルを取ります。 実際、このエージェントは、この現在のレベルまでトレーニングするために、4つのTitanXGPUで24時間を必要としました。 つまり、適切にトレーニングするにはかなりの量の計算が必要でした。 はるかに少ないサンプルで同様に高性能なエージェントをトレーニングできますか? 前の手順では、この質問の調査を開始するのに十分な知識を身に付ける必要があります。 はるかに単純なモデルと偏りと分散のトレードオフを使用すると、それが可能になる場合があります。

結論

このチュートリアルでは、ゲーム用のボットをいくつか作成し、偏りと分散と呼ばれる機械学習の基本的な概念を探りました。 次の自然な質問は次のとおりです。StarCraft2などのより複雑なゲーム用のボットを構築できますか? 結局のところ、これは保留中の調査の質問であり、Google、DeepMind、Blizzardの共同研究者によるオープンソースツールで補完されています。 これらが興味のある問題である場合、現在の問題については、OpenAIでの研究の公募を参照してください。

このチュートリアルの主なポイントは、バイアスと分散のトレードオフです。 モデルの複雑さの影響を検討するのは、機械学習の実践者次第です。 非常に複雑なモデルとレイヤーを過剰な量の計算、サンプル、および時間で活用することは可能ですが、モデルの複雑さを軽減すると、必要なリソースを大幅に削減できます。