🧠

【Tensorflow】自作ゲームの強化学習をする①

2024/08/06に公開

概要

ここでは、簡単なゲームを作って、強化学習をしていきます。コードはPythonで書いて、ゲームの構築はOpenAI開発のGymをベースにしていきます。立ちとるにもある通り、AIの学習アルゴリズムはTensorflowを使います。

ゲームの作成

ゲーム(学習する環境)は単純なものにします。キャラクターが空中で飛び続ける、といったゲームを作ります。FlappyBirdの障害物がないバージョンを想像してもらえると分かりやすいかもしれません。概要で説明した通り、コードはGymベースで書きます。GymとはChatGPTで有名なOpenAIが開発した強化学習用のシミュレーション用プラットフォームです。Tensorflowの公式ドキュメントではこれのサンプルにあるCart-Poleを学習環境として説明しています。

Gymのインストール:

$ pip install gym

インストールが完了したら、gymをインポートしてゲームの枠組みを作っていきます。

FlappyBird.py
import gym
from gym import spaces
import pygame as pg

import numpy as np
import time
import os

class FlappyBird(gym.Env):
    metadata = {"render.modes": ["human"]}

    def __init__(self) -> None:
        super().__init__()

        self.action_space = spaces.Discrete(2)  # ジャンプするかしないかの選択
        self.observation_space = spaces.Box(-float("inf"), float("inf"), (2,))  # 観測データ
        self.reward_range = (-float("inf"), float("inf"))  # 報酬の範囲

        self.reward = 0   # 報酬
        self.st = 0
        self.et = 0
        self.elapsed_time = 0
        self.delta = 0.01  # 前フレームから現在のフレームへ変わるのにかかる時間(FPSみたいなもの)

    def def Get_obs(self):
        self.distance = 0       #止まるべき点からの差(y座標)
        self.player_gravity = 0   #プレイヤーにかかっているY方向の力
        return np.array([self.player_gravity, self.distance], dtype=np.float32)

    def reset(self):
        self.reward = 0
        self.elapsed_time = 0

        self.obs = self.Get_obs()
        self.step_d = 0
        
        return self.obs, {}

    def step(self, action: np.ndarray):
        obs = self.Get_obs()
        self.elapsed_time += self.delta
        done = False
        info = {}

        return obs, self.reward, done, info

    def render(self, mode="human"):
        if mode == "human":
            self.st = time.perf_counter()

            if os.name == "posix":
                pg.display.update()

            self.screen.fill((255, 255, 255))

            if os.name == "nt":
                pg.display.update()

            self.clock.tick(30)
            self.et = time.perf_counter()
            self.delta = (self.et - self.st) * self.game_speed

            return arr
        else:
            super(FlappyBird, self).render(mode=mode)
            return

    def close(self):
        pg.quit()

これはとりあえず書いたものなので実行しても何も起こらないかバグって終わります。とりあえず枠組みだけ、ということです。ではコードについて説明していきます。

Gym環境ではreset()step()render()close()の関数を使います。
reset()ではプレイヤーが死んだときやゲームに成功したときに次の学習環境をゼロから再構築するために使います。環境のリセットです。
step()ではプレイヤーの入力を受け取ってゲームを進めます。ゲームの更新をするので、ゲームの挙動を書いていきます。
render()ではゲーム画面の描画を行います。当たり前ですが描画は比較的時間のかかる処理なので、学習時とテスト時で処理の有無を選択できるようにします。
close()ではクラスの終了処理を書きます。ここではpygameの終了処理を書きます。

プレイヤークラス

ゲーム枠組みができましたのでプレイヤーの処理を書いていきます。以下のコードを同じファイルに追加してください。

FlappyBird.py
class Player:
    def __init__(self, cl) -> None:
        self.parent = cl
        self.size = np.array([100, 90])
        self.position = [SIZE[0]*0.3, SIZE[1]/2]
        self.pos = [self.position[0] + self.size[0]/2, self.position[1] + self.size[1]/2]
        self.step_distance = 0

        self.image = pg.image.load("bird.png")
        self.image = pg.transform.scale(self.image, (self.size[0], self.size[1]))
        #self.image = pg.transform.flip(self.image, True, False)

        self.gravity = 0
        self.acceleration = 9.8*2     #重力加速度(/frame)
        self.dead = False

    def jump(self):
        self.gravity = -9.2

    def jump_human(self):
        self.key = pg.key.get_pressed()
        if self.key[pg.K_SPACE]:
            self.jump()
        if self.key[pg.K_m]:
            self.acceleration = 0
            self.gravity = 0

    def draw(self):
        self.parent.screen.blit(self.image, (self.position[0], self.position[1]))

    def update(self):
        self.step_distance = self.parent.speed * self.parent.delta
        self.measurement_step_distance += self.parent.speed * self.parent.delta
        self.gravity += self.acceleration * self.parent.delta

        self.position[1] += self.gravity
    
    def is_dead(self) -> bool:
        if self.position[1] > SIZE[1]:      # プレイヤーが画面下へ落下した
            self.parent.couse_of_death = "drop"
            return True
        elif self.position[1] + self.size[1] / 2 < 0:          # プレイヤーが上へ行き過ぎた
            self.parent.couse_of_death = "up"
            return True

        return False

これによりクラスオブジェクトを作ることでゲームにプレイヤーを追加することができます。クラス内の関数は以下のものがあります。

  • jump() ・・・ プレイヤーのジャンプ処理
  • jump_human() ・・・ 人間が操作する用の処理
  • draw() ・・・ プレイヤーの描画処理
  • update() ・・・ プレイヤーの更新処理
  • is_dead() ・・・ プレイヤーが死亡したかどうかを判定する

プレイヤーをゲームに追加する

プレイヤークラスが書けたのでゲームに追加します。FlappyBird.pyを以下のように書き変えて下さい。

FlappyBird.py
class FlappyBird(gym.Env):
    metadata = {"render.modes": ["human"]}

    def __init__(self) -> None:
        super().__init__()

        self.action_space = spaces.Discrete(2)  # ジャンプするかしないかの選択
        self.observation_space = spaces.Box(-float("inf"), float("inf"), (2,))  # 観測データ
        self.reward_range = (-float("inf"), float("inf"))  # 報酬の範囲

        #pygame設定
+       pg.init()
+       self.screen = pg.display.set_mode(SIZE)
+       pg.display.set_caption("AC Learning")
+       self.clock = pg.time.Clock()

        self.reward = 0   # 報酬
        self.st = 0
        self.et = 0
        self.elapsed_time = 0
        self.delta = 0.01  # 前フレームから現在のフレームへ変わるのにかかる時間(FPSみたいなもの)
+       self.player = Player(self)  # プレイヤー

    def def Get_obs(self):
+       self.distance = self.player.position[1] - SIZE[1]       #止まるべき点からの差(y座標)
+       self.player_gravity = self.player.gravity   #プレイヤーにかかっているY方向の力
        return np.array([self.player_gravity, self.distance], dtype=np.float32)

    def reset(self):
        self.reward = 0
        self.elapsed_time = 0
+       self.player = Player(self)

        self.obs = self.Get_obs()
        self.step_d = 0
        
        return self.obs, {}

    def step(self, action: np.ndarray):
        obs = self.Get_obs()

+       if action == 1:
+           self.player.jump()
+           self.jump_count += 1

+       if self.human:
+           self.player.jump_human()

        self.elapsed_time += self.delta
+       done = self.player.is_dead()

+       if not done:
+           self.reward = 1
+       else:
+           self.reward = 0
        info = {}

        return obs, self.reward, done, info

    def render(self, mode="human"):
        if mode == "human":
            self.st = time.perf_counter()

            if os.name == "posix":
                pg.display.update()

            self.screen.fill((255, 255, 255))
+           self.player.draw()

            if os.name == "nt":
                pg.display.update()

            self.clock.tick(30)
            self.et = time.perf_counter()
            self.delta = (self.et - self.st) * self.game_speed

            return arr
        else:
            super(FlappyBird, self).render(mode=mode)
            return

    def close(self):
        pg.quit()

これでプレイヤーを追加することができました。では実際に動くかテストしてみましょう。以下のコードを一番下に追加して動かしてみましょう。

FlappyBird.py
def check():
    env = FlappyBird()
    env.render_set_up()
    obs = env.reset()
    for i in range(5):
        state = env.reset()[0]
        #print(state)
        done = False
        
        while not done:
            action = random.randint(0, 1)
            state, reward, done, info = env.step(action)

            env.render()
        # 1エピソードが終わる
        print(f"step: {env.step_d}")
        print(f"reward: {reward}")

if __name__ == "__main__":
    check()

このコードではプレイヤーはランダムな行動を毎回選択します。1の場合はジャンプ、0の場合はジャンプをしません。
実行結果は以下のようになります。

次回へ

今回は学習環境のコードまで書きました。次回からは学習ループのコードを書いていきます。
次回➞ https://zenn.dev/y4shimounitydev/articles/2406eb352e814f

Discussion