😨

強化学習で作る最強のCCレモンAI~強化学習基礎編~

2024/12/10に公開

本記事は強化学習で作る最強のCCレモンAI~ルール編~の続編です。
ルール編に記載したCCレモンのルールを前提としていますので、ぜひ前の記事もご覧ください。

強化学習とは

強化学習とは、「収益」と呼ばれる数値が最大化するように「状態」に応じて「行動」を選択する学習手法です。(ref : https://www.youtube.com/watch?v=jwHVLrtkt5w)
強化学習の考え方をCCレモンに当てはめて解説をしていきます。

状態

状態とは、今がどうなっているのかを表すものです。
将棋や囲碁のAIであれば盤面と手番ですし、そば打ちAIであればその日の温度や気候と今時点でのそば生地の状態のことです。
CCレモンAIでは、自分と相手プレイヤーの「チャージ」がいくつ溜まっているかが状態に当たります。

行動

行動とは、AIが選択する相手プレイヤーもしくは状態に対して起こす行動のことです。
囲碁や将棋のAIであれば自分の手番で行う指し手・打ち手のことであり、そば打ちAIであれば生地をこねる強さや場所などのこね方にあたる部分です。

収益

収益とは、AIが行動を起こした結果どの程度収益が得られるのかを表します。
強化学習はこの収益を最大化することを目的に行動を選択するため、強化学習が説いている問題の最終目的の達成もしくは最終目的の達成に近づくと収益が得られるように収益を設計します。
囲碁や将棋のAIであれば最終目的は勝利なので勝利が一番大きい収益が得られるようにし盤面が自分に有利(将棋であれば駒得など)になれば収益が得られるように設計します。
CCレモンは相手に勝利するのが目的なので相手に勝利するのが一番収益が大きく得られるように設計し、自分が有利な状態になれば収益が得られるように設計します。

Q値

Q値とは、「状態sにおける行動aを起こしたときの報酬和」の推測値です。
「報酬」ではなく「報酬和」なのが重要な部分です。行動を起こした直後の短期的な報酬ではなく、問題終了までの長期的な報酬の合計の推測値になります。

Qテーブル

Qテーブルとは、状態に対して各行動の報酬和の推測値が書かれたテーブルです。
Qテーブルを用いた強化学習はQテーブルの更新によって学習を進めます。

0 1 2 3 4 5
0
1
2
3
4
5

図1 : CCレモンのQテーブルのイメージ
各マスには行動可能なコマンドを実行したときの報酬和の推測値が入る

ソースコード

Agent

AIとして状態をもとに行動するAgentClassです。

class QLearningAgent:
    def __init__(self, actions: List[int], alpha: float = 0.1, gamma: float = 0.9, epsilon: float = 0.1) -> None:
        self.actions: List[int] = actions  # 行動のリスト

        self.alpha: float = alpha  # 学習率

        self.gamma: float = gamma  # 割引率
        self.epsilon: float = epsilon  # 探索率
        self.q_table: dict[Tuple[int, int], np.ndarray] = {}  # Q値テーブル

    def get_q_value(self, state: Tuple[int, int], action: int) -> float:
        """Q値を取得"""
        if state not in self.q_table:
            self.q_table[state] = np.zeros(len(self.actions))
        return self.q_table[state][action]

    def update_q_value(self, state: Tuple[int, int], action: int, reward: float, next_state: Tuple[int, int]) -> None:
        """Q値を更新"""
        if next_state not in self.q_table:
            # 次の状態が初めて登場した場合、Q値を全てゼロで初期化
            self.q_table[next_state] = np.zeros(len(self.actions))
        # 次の状態における最大のQ値を取得
        best_next_action = np.max(self.q_table[next_state])
        # Q値の更新式
        current_q_value = self.get_q_value(state, action)
        new_q_value = current_q_value + self.alpha * (reward + self.gamma * best_next_action - current_q_value)
        # 更新されたQ値を保存
        self.q_table[state][action] = new_q_value

    def select_action(self, state: Tuple[int, int], show_flag = False) -> int:
        """ε-グリーディ方策による行動選択"""
        """ε-グリーディ方策とは一定確率でランダムなアクションを選択し、現在のQテーブルよりより良い行動を探索する手法"""
        charge = state[0]  # プレイヤーのチャージ数

        # 有効なアクションのリストをチャージ数に基づいて絞り込む
        available_actions = []
        # チャージ数に基づくアクション制限

        if charge >= 1:
            available_actions.append(1)  # ビーム
        if charge >= specium_cost:
            available_actions.append(2)  # スペシウム光線
        available_actions.append(0)  # チャージ(常に選択可能)
        available_actions.append(3)  # ガード(常に選択可能)
        # ε-グリーディ方策で行動選択

        if show_flag:
            valid_q_values = [self.get_q_value(state, action) for action in available_actions]
            return available_actions[np.argmax(valid_q_values)]  # 最大Q値のアクションを選択
        elif random.uniform(0, 1) < self.epsilon :
            # 探索: 有効なアクションからランダムに選択
            return random.choice(available_actions)
        else:
            # スペシウム光線を打てるなら必ず打つ
            if charge == specium_cost:
                return 2
            # 利用: Q値に基づいて有効なアクションの中から選択
            # 有効なアクションのQ値を取得
            valid_q_values = [self.get_q_value(state, action) for action in available_actions]
            return available_actions[np.argmax(valid_q_values)]  # 最大Q値のアクションを選択

    def show_agent(self):
        for my_state in range(6):
                for teki_state in range(6):
                    if self.select_action((my_state,teki_state), show_flag = True) == 0:
                        show_action = "チャージ"
                    elif self.select_action((my_state,teki_state), show_flag = True) == 1:
                        show_action = "ビーム"
                    elif self.select_action((my_state,teki_state), show_flag = True) == 2:
                        show_action = "スペシウム光線"
                    elif self.select_action((my_state,teki_state), show_flag = True) == 3:
                        show_action = "ガード"
                    print(f"自分のチャージが{my_state}, 相手のチャージが{teki_state} : {show_action}")

学習部分

状態から報酬を計算する関数と学習を実行する部分です。

def cal_reward(state : Tuple[int,int], win : int) -> float:
    # 勝利報酬
    win_reward = 0
    if win == 1:
        win_reward = 100000
    elif win == -1:
        win_reward = -100000
    return win_reward

def train_agent(episodes: int = 1000) -> Tuple[Any, Any]:
    env = CCGameEnv()  # 環境の初期化

    agent1 = QLearningAgent(actions=[0, 1, 2, 3])  # プレイヤー1のエージェント
    agent2 = QLearningAgent(actions=[0, 1, 2, 3])  # プレイヤー2のエージェント
    for episode in range(episodes):
        state = env.reset()  # ゲームの初期状態
        done = False
        total_reward = 0

        while not done:
            action1 = agent1.select_action(state)  # プレイヤー1の行動
            action2 = agent2.select_action(state)  # プレイヤー2の行動
            next_state, win, done = env.step(action1, action2)  # アクションを実行
            # 状態と報酬の更新
            state = next_state
            total_reward += cal_reward(state,win)
            # Q値の更新
            agent1.update_q_value(state, action1, total_reward, next_state)
            agent2.update_q_value(state, action2, -total_reward, next_state)
        if episode % 100 == 0:
            print(f"Episode {episode}, Total reward: {total_reward}")
    return agent1, agent2

精度評価

実行可能なコマンドをランダムに実行するAIと対戦し勝率を計算することで強いAIが作れているか確認します。

def buttle_random_agent(agent, episodes: int = 1000) -> int:
    env = CCGameEnv()  # 環境の初期化
    random_agent = QLearningAgent(actions=[0, 1, 2, 3])

    total_win = 0
    for episode in range(episodes):
        state = env.reset()  # ゲームの初期状態
        done = False
        while not done:
            action1 = agent.select_action(state)  # プレイヤー1の行動
            action2 = random_agent.select_action(state)  # プレイヤー2の行動
            next_state, win, done = env.step(action1, action2)
            state = next_state
        if episode % 100 == 0 and episode > 0:
            print(f"Episode {episode}, Total win: {total_win}, win ratio: {total_win / episode}")
        if win == 1:
            total_win += 1
    return total_win

精度評価結果

ランダムにコマンドを実行するAIとの勝率は以下の通りでした。

勝率
1回目 51.5%
2回目 50.2%
3回目 51.0%

微妙...!!!!
次回はこのAIをどんどん強くしていきます!

株式会社ZOZO

Discussion