強化学習｜Q学習｜入門

# はじめに

強化学習は、エージェントが環境とのインタラクションを通じて最適な行動方針（ポリシー）を学習する技術です。大規模言語モデルにおいては、人間からのフィードバックによる強化学習（RLHF）という手法が使用されており、RLHFを理解するために、まずは基礎的なQ学習を理解したく、サンプルコードを実装しました。本稿では、強化学習の中でも基本的な手法である「Q学習」を利用して、エージェントがゴールに到達するための経路探索をどのように学習するかを説明します。

## GridWorldとは？

GridWorldは、エージェントがグリッド上を移動し、特定のゴール地点に到達することを目指すシンプルな環境です。本稿では5x5のグリッドを使用し、エージェントは左上の位置から右下のゴールを目指します。この環境は、強化学習の基本を学ぶ上で理想的なモデルであり、エージェントが取ることのできる行動（上、下、左、右）と、それに伴う報酬がシンプルに設定されています。こうした環境は、強化学習の初学者にとってアルゴリズムの挙動を理解しやすく、またエージェントの学習効果を視覚的に確認できるため、教育目的やアルゴリズムの検証に適しています。

### グリッドの定義

以下は使用する5x5のグリッドの具体例です。

|       | 0     | 1     | 2     | 3     | 4     |
| ----- | ----- | ----- | ----- | ----- | ----- |
| **0** | (0,0)<br>開始 | (0,1) | (0,2) | (0,3) | (0,4) |
| **1** | (1,0) | (1,1) | (1,2) | (1,3) | (1,4) |
| **2** | (2,0) | (2,1) | (2,2) | (2,3) | (2,4) |
| **3** | (3,0) | (3,1) | (3,2) | (3,3) | (3,4) |
| **4** | (4,0) | (4,1) | (4,2) | (4,3) | (4,4)<br>ゴール |

このグリッドでは、左上の位置 (0, 0) から右下の位置 (4, 4) がゴールです。各セルが異なる状態を表し、エージェントはこれらの状態を移動しながらゴールを目指します。

### 状態（State）、インデックス

エージェントの位置を「状態」として定義します。この状態は、グリッド上の各位置に対応し、それぞれの状態はインデックス（index）として表現されます。

|       | 0   | 1   | 2   | 3   | 4   |
|-------|-----|-----|-----|-----|-----|
| **0** | 0   | 1   | 2   | 3   | 4   |
| **1** | 5   | 6   | 7   | 8   | 9   |
| **2** | 10  | 11  | 12  | 13  | 14  |
| **3** | 15  | 16  | 17  | 18  | 19  |
| **4** | 20  | 21  | 22  | 23  | 24  |

このように、各グリッドの位置は一意のインデックスに対応しています。このインデックスはQ学習において、状態をQテーブル内で管理するために重要な役割を果たします。

### 行動（Action）

エージェントは、グリッド上で上下左右に移動することが可能です。

- 行動の例:
  - 上（UP）
  - 下（DOWN）
  - 左（LEFT）
  - 右（RIGHT）

これらの行動を通じて、エージェントはゴールを目指します。行動の選択はエージェントのポリシーに依存しており、ポリシーはQ学習を通じて学習されるQテーブルによって導かれます。エージェントがどの行動を選択するかは、ε-greedy法などの行動選択戦略により決定されます。

### ゴール（Goal）

エージェントの目標地点は右下のマス (4, 4) です。この位置に到達すると、エピソードが終了し、報酬が与えられます。このゴール報酬は、エージェントにとって非常に重要なフィードバックであり、最適な経路を学習するための指針となります。エージェントは、この報酬を最大化するように行動ポリシーを改善します。

### GridWorldのコード解説

以下は、GridWorldの環境を定義するPythonクラス `GridWorldClass` の実装です。

```python
import numpy as np
from typing import List, Tuple, Dict

class GridWorldClass:
    def __init__(self, size: Tuple[int, int] = (5,5)) -> None:
        self.size: Tuple[int, int] = size
        self.state: Tuple[int, int] = (0, 0) # エージェントの初期位置
        self.goal: Tuple[int, int] = (size[0]-1, size[1]-1) # ゴールの位置. 例：(4, 4)
        self.action: Dict[str, Tuple[int, int]] = {
            "UP": (-1, 0), # 上に1マス移動
            "DOWN": (1, 0), # 下に1マス移動
            "LEFT": (0, -1),
            "RIGHT": (0, 1)
        }
        self.action_space: int = len(self.action) # 行動の数. 例: 4
        self.state_space: int = size[0] * size[1] # 状態の数. 例: 25

    def reset(self) -> int:
        "環境をリセットして、エージェントの初期位置を返す"
        self.state = (0, 0)
        return self.state_to_index(self.state)

    def step(self, action: int) -> Tuple[int, float, bool]:
        """
        エージェントの行動を実行し、次の状態、報酬、終了フラグを返す
        Args: action (int): エージェントの行動のインデックス
        Returns: Tuple[int, float, bool]: 次の状態、報酬、終了フラグ
        """
        move: Tuple[int, int] = list(self.action.values())[action] # 行動を取得
        new_state: Tuple[int, int] = (
            max(0, min(self.size[0] -1, self.state[0] + move[0])), # 次の状態のy座標
            max(0, min(self.size[1] -1, self.state[1] + move[1])) # 次の状態のx座標
        )
        self.state = new_state # 状態を更新

        # ゴールに到達した場合
        if self.state == self.goal:
            return self.state_to_index(self.state), 1.0, True # ゴールに到達した場合、報酬1.0と終了フラグを返す
        else:
            return self.state_to_index(self.state), 0.0, False # ゴールに到達していない場合、報酬0.0と終了フラグを返す

    def state_to_index(self, state: Tuple[int, int]) -> int:
        "グリッドの状態(state)をインデックスに変換する"
        return state[0] * self.size[1] + state[1] # 例: (2, 3) -> 2*5+3 = 13
```

上記のコードでは、エージェントが行動（上、下、左、右）を取るたびに次の状態と報酬を返します。ゴールに到達すると報酬を受け取り、エピソードが終了します。こうした処理は、エージェントが環境から受け取るフィードバックの基礎となります。

## Q学習によるエージェントの訓練

次に、Q学習アルゴリズムを使用してエージェントが最適な行動を学習するプロセスを見ていきます。Q学習は、強化学習の代表的な手法であり、エージェントがどの行動が最も有益であるかを試行錯誤を通じて学習します。

### Q学習の理論的背景

Q学習では、各状態と行動のペアに対して「Q値」と呼ばれる価値を学習します。このQ値は、その状態で特定の行動を選択した場合に将来得られる累積報酬の期待値を表しています。エージェントは、各エピソードを通じて状態と行動のペアを更新し、次第に最適な行動ポリシーを構築します。

エージェントは、ε-greedy戦略を用いて行動を選択します。ε-greedy戦略では、確率εでランダムな行動を選択し、残りの確率1-εで現在のQ値が最大になる行動を選択します。これにより、探索と活用のバランスが取られ、未知の環境を探索しつつ最適な行動を学習することができます。

### Q学習のコード実装

以下にQ学習を実行するためのコードを示します。

```python
def train_q_learning(
        env: GridWorldClass, # GridWorldのインスタンス
        total_episodes: int = 500, # エピソード数
        learning_rate: float = 0.1, # 学習率
        discount_factor: float = 0.99, # 割引率
        epsilon: float = 0.9, # ε-greedy法のε
        max_epsilon: float = 1.0, # εの最大値
        min_epsilon: float = 0.01, # εの最小値
        decay_rate: float = 0.005 # εの減衰率
        ) -> np.ndarray:
    """グリッドワールド環境でQ学習を行い、Qテーブルを返す"""
    q_table: np.ndarray = np.zeros((env.state_space, env.action_space)) # Qテーブルの初期化

    for episode in range(total_episodes):
        state: int = env.reset() # 初期状態にリセット
        done: bool = False

        while not done:
            # ε-greedy法で行動（Action）を選択
            if np.random.uniform(0, 1) < epsilon:
                action: int = np.random.choice(env.action_space) # ランダムに行動を選択
            else:
                action: int = np.argmax(q_table[state, :]) # Q値が最大となる行動を選択

            # 行動（Action）を実行し、次の状態、報酬、終了フラグを取得
            next_state, reward, done = env.step(action)

            # Q値の更新
            q_table[state, action] = q_table[state, action] + learning_rate * (
                reward + discount_factor * np.max(q_table[next_state, :]) - q_table[state, action]
            )

            # 状態を更新
            state = next_state

        # εの減衰
        epsilon = min_epsilon + (max_epsilon - min_epsilon) * np.exp(-decay_rate * episode)

    return q_table
```

このコードでは、エージェントが環境を探索しながらQテーブルを更新し、最適な行動を学習します。各エピソードの終了時には、探索率εが減衰し、最終的には最適行動を選択する頻度が高まります。

### 学習結果のポリシーを表示

学習が完了した後、エージェントが各状態でどの行動を取るべきかのポリシーを表示する関数を実装します。

```python
def display_policy(env: GridWorldClass, q_table: np.ndarray) -> None:
    policy_symbols: Dict[int, str] = {
        0: '↑',  # up
        1: '↓',  # down
        2: '←',  # left
        3: '→'   # right
    }

    for i in range(env.size[0]):
        for j in range(env.size[1]):
            state: int = env.state_to_index((i, j))
            if (i, j) == env.goal:
                print(' G ', end='')
            else:
                action: int = np.argmax(q_table[state, :])
                print(f' {policy_symbols[action]} ', end='')
        print()
```

この関数を使うことで、エージェントが各位置で取るべき行動（上、下、左、右）が視覚的に示されます。最終的に、ゴールを目指して効率的に移動するための最適なポリシーを確認することが可能です。

## 実行結果

`main_gridworld` 関数を呼び出すことで、学習済みのQテーブルを使用してポリシーを表示します。

### 学習結果の一例

以下は、学習されたポリシーの一例です。エージェントがどのように移動すべきかが矢印で示されています。

```
 →  →  ↓  ↓  ↓
 →  →  →  ↓  ↓
 ↓  ↓  →  →  ↓
 →  →  →  →  G
```

このポリシーから、エージェントはゴールに到達するために最適な経路を学習し、効率的にゴールに向かって移動していることがわかります。

```python
def main_gridworld() -> None:
    "GridWorld環境でのQ学習の実行と、ポリシーの表示を行う"
    env: GridWorldClass = GridWorldClass(size=(5,5))
    q_table: np.ndarray = train_q_learning(env)
    print("学習済のPolicy:")
    display_policy(env, q_table)

if __name__ == "__main__":
    main_gridworld()
```

これにより、エージェントが学習してどのように移動するべきかを確認でき、強化学習の基礎的なプロセスを理解することができます。

### フローチャート
```mermaid
flowchart TD
    A[Start] --> B["Qテーブルの初期化<br>np.zeros()"]
    B --> C[エピソードでForループ]
    C --> D["環境の初期化. <br>env.reset()"]
    D --> E[行動の選択. <br>例:貪欲法]
    E --> G["行動を実行し、<br>次の状態・報酬を取得. <br>env.step()"]
    G --> J[Qテーブル、状態を更新]
    J --> F{全エピソード終了?}
    F -- No --> C
    F -- Yes --> Z[END]
```

## まとめ

今回のブログでは、強化学習の基本概念とQ学習アルゴリズムを使用したシンプルなGridWorldの例を紹介しました。Q学習は強化学習の中でも代表的な手法で、エージェントが自ら環境からの報酬を基に学習していくという流れを理解することができました。

Q学習の特徴は、エージェントが環境とのインタラクションを通じて試行錯誤を重ねることで、報酬を最大化する行動を学習する点にあります。また、ε-greedy法を使用して探索と活用のバランスを取ることで、未知の環境を効果的に探索し、最適な行動ポリシーを見つけ出すことができます。