ChatGPT-4 と始める強化学習：宇宙船着陸ゲームに挑戦しよう！

# 1. はじめに

本記事は、エンジニア向けにChatGPTの活用例を紹介する 「ChatGPTと始めるシリーズ」第5弾です。今回は強化学習に挑戦したいと思います！

今回の題材はこちらです。

![dqn_1700.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3130775/f4bc8bfb-5afe-f7f7-7f56-bf01a49fce03.gif)

OpenAI Gym のシミュレーション環境 LunarLander-v2 を使用して、宇宙船を着陸させるエージェントを強化学習を用いて作成します。

（過去のシリーズ記事はこちらです）

https://qiita.com/key353/items/d58b93b4c21a9cb44baf


# 2. 作業内容と問題

## 2.1. 作業内容について提案してもらう

少し複雑な作業になるので、まずは作業内容を ChatGPT と相談したいと思います。

![スクリーンショット 2023-05-27 121729.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3130775/cf6d524f-ad26-d976-9d26-2622b77a3669.png)

ChatGPT からの回答がこちらです。

![スクリーンショット 2023-05-27 081334.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3130775/d2aa67f6-4ea7-5924-faa8-7127440124f9.png)

本記事では、上記の作業内容に沿って、強化学習の基本である Q-Learning、 Deep Q-Learning の順に試していきます。

:::note info
以降は上記作業内容の順に、手順を聞いていく流れになります。プロンプトの多くは、過去のシリーズ記事と同じなので、今回は結果を中心に紹介します。
:::

## 2.2. 問題設定

今回使用する環境は冒頭で紹介したように、下記のリンクの LunarLander-v2 になります。

https://www.gymlibrary.dev/environments/box2d/lunar_lander/

問題の概要はこちらです。

![スクリーンショット 2023-05-27 081934.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3130775/4442c53c-0a3e-3443-5f40-8c186281bb3a.png)

宇宙船を墜落させることなく着陸できるように、エンジン出力をコントロールします。

# 3. Q-Learning 

## 3.1. Q-Learning の概要

Q-Learningについて、ChatGTP に簡単に概要を紹介してもらいます。

![スクリーンショット 2023-05-27 082146.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3130775/94c9dbd1-8030-13ab-0b9d-1b7d3dc07f95.png)

実際に作業していたときは、「Exploration vs Exploitation についてもっと詳しく」など理解が足りないところについて色々聞いていたのですが、すごく長いのでカットします。
ChatGPT は何を聞いても根気よく教えてくれるので、不明点はぜひお手元のGPTに相談してみてください！

## 3.2. 使用環境

今回使用した環境はこちらです。

* Windows 11
* python 3.8.10
* 使用したパッケージ (gymは最新バージョンがうまくインストールできなかったのでバージョンを落としています)

```txt: requirements.txt
wheel
gym==0.21.0
numpy
matplotlib
Box2D
pyglet==1.5.27
```

## 3.3. Q-Learningのコード

ChatGPTにより生成してもらった Q-Learning のコードを以下に示します。

<details><summary>長いので折りたたんでいます</summary><div>

```python: lunar_lander_qlearning.py
"""
強化学習（Q学習）によるLunarLanderの制御コードです。
"""

import gym
import numpy as np
import matplotlib.pyplot as plt
from gym import wrappers
import datetime

def create_env():
    """
    環境の作成と初期化を行います。

    Returns
    -------
    env : gym.Env
        'LunarLander-v2'環境
    initial_state : array
        初期状態
    """
    # 環境の初期化
    env = gym.make('LunarLander-v2')
    initial_state = env.reset()
    
    return env, initial_state

def initialize_q_table(env, binning_specs):
    """
    Qテーブルを初期化します。

    Parameters
    ----------
    env : gym.Env
        学習対象の環境
    binning_specs : list of array-like
        各状態変数を離散化する際のビンの境界値リスト

    Returns
    -------
    q_table : ndarray
        初期化されたQテーブル
    """
    # Q-Tableの初期化
    # binning_specs の各リストの長さを取得してタプルを作る
    state_space_size = tuple(len(specs) + 1 for specs in binning_specs)
    action_space_size = env.action_space.n
    
    # Q-Tableをゼロで初期化
    q_table = np.zeros(state_space_size + (action_space_size,))
    
    return q_table

def choose_action(state, q_table, action_space, epsilon):
    """
    ε-greedy法による行動選択を行います。

    Parameters
    ----------
    state : tuple
        離散化された現在の状態
    q_table : ndarray
        Qテーブル
    action_space : gym.Space
        行動空間
    epsilon : float
        ランダムな行動を選択する確率

    Returns
    -------
    action : int
        選択された行動
    """
    # ε-greedy法による行動選択
    if np.random.uniform(0, 1) < epsilon:
        # 探索：ランダムな行動を選択
        action = action_space.sample()
    else:
        # 活用：Q値が最大の行動を選択
        action = np.argmax(q_table[state])
        
    return action

def update_q_table(q_table, state, action, reward, next_state, alpha, gamma):
    """
    Q値の更新を行います。

    Parameters
    ----------
    q_table : ndarray
        Qテーブル
    state : tuple
        離散化された現在の状態
    action : int
        行動
    reward : float
        得られた報酬
    next_state : tuple
        離散化された次の状態
    alpha : float
        学習率
    gamma : float
        割引率

    Returns
    -------
    q_table : ndarray
        更新されたQテーブル
    """
    # Q値の更新
    # 古いQ値
    old_value = q_table[state][action]
    
    # 次の状態における最高のQ値
    next_max = np.max(q_table[next_state])
    
    # 新しいQ値の計算（Bellman方程式）
    new_value = (1 - alpha) * old_value + alpha * (reward + gamma * next_max)
    
    # Q-Tableの更新
    q_table[state][action] = new_value
    
    return q_table

def discretize_state(state, binning_specs):
    """
    離散化を行う関数です。

    Parameters
    ----------
    state: list
        離散化する前の状態変数のリスト
    binning_specs: list of tuples
        各状態変数に対するビンの境界値を定めるタプルのリスト

    Returns
    -------
    discretized_state: tuple
        離散化された状態変数
    """
    discretized_state = []
    for value, bins in zip(state, binning_specs):
        discretized_state.append(np.digitize(value, bins) - 1)

    return tuple(discretized_state)

def episode_step(env, q_table, max_steps, epsilon, alpha, gamma, binning_specs, is_recording):
    """
    一エピソードを実行し、報酬と終了フラグを返します。

    Parameters
    ----------
    env : gym.Env
        学習対象の環境
    q_table : ndarray
        Qテーブル
    max_steps : int
        1エピソードの最大ステップ数
    epsilon : float
        ランダムな行動を選択する確率
    alpha : float
        学習率
    gamma : float
        割引率
    binning_specs : list of array-like
        各状態変数を離散化する際のビンの境界値リスト
    is_recording : bool
        ビデオを録画するかどうか

    Returns
    -------
    episode_reward : float
        1エピソードの総報酬
    done : bool
        エピソードが終了したかどうか
    """
    # 状態の初期化
    state = env.reset()
    state = discretize_state(state, binning_specs)

    # 1エピソードの報酬
    episode_reward = 0

    done = False

    # for step in range(max_steps):
    while not done:
        # 行動の選択
        action = choose_action(state, q_table, env.action_space, epsilon)

        # 行動の実行とフィードバックの取得
        next_state, reward, done, _ = env.step(action)
        next_state = discretize_state(next_state, binning_specs)

        # Q-Tableの更新
        q_table = update_q_table(q_table, state, action, reward, next_state, alpha, gamma)

        # 状態の更新
        state = next_state

        # 報酬の加算
        episode_reward += reward

        # エピソード終了時
        if done:
            break

    if is_recording:
        env.close()

    return episode_reward, done

def plot_rewards(rewards):
    """
    報酬の移動平均を計算し、グラフを表示します。

    Parameters
    ----------
    rewards : list of float
        各エピソードの報酬のリスト
    """
    # 報酬の移動平均を計算
    moving_avg = np.convolve(rewards, np.ones((100,))/100, mode='valid')
    
    # グラフをプロット
    plt.plot(np.arange(len(moving_avg)), moving_avg)
    plt.title('Training Rewards')
    plt.xlabel('Episode')
    plt.ylabel('100 Episode Average')
    plt.savefig("result.png") 
    plt.show()

def main():
    """
    メインの関数。学習パラメータの設定、学習環境の準備、エピソードの実行、結果の表示などを行います。

    Parameters
    ----------
    num_episodes : int
        学習を行うエピソード数
    max_steps : int
        1エピソード当たりの最大ステップ数
    alpha : float
        学習率
    gamma : float
        割引率
    epsilon : float
        ε-greedy法における探索の確率
    """
    # パラメータの設定
    num_episodes = 50000
    max_steps = 300
    alpha = 0.5
    gamma = 0.95
    epsilon = 0.1

    # ビデオを保存するエピソードの間隔
    video_interval = 1000

    binning_specs = [
    np.linspace(-1, 1, 10),  # x座標
    np.linspace(0, 1, 10),  # y座標
    np.linspace(-1, 1, 10),  # x速度
    np.linspace(-1, 1, 10),  # y速度
    np.linspace(-1, 1, 10),  # ランダーの角度
    np.linspace(-1, 1, 10),  # ランダーの角速度
    np.linspace(0, 1, 10),  # 左脚が地面に接触しているかどうか
    np.linspace(0, 1, 10)  # 右脚が地面に接触しているかどうか
    ]

    # 環境の作成
    env = gym.make('LunarLander-v2')

    # 環境の初期化とQテーブルの初期化
    q_table = initialize_q_table(env, binning_specs)

    rewards = []
    
    # Q-Tableの初期化
    q_table = initialize_q_table(env, binning_specs)

    now = datetime.datetime.now()
    # エピソードごとの報酬を格納するリスト
    rewards = []

    # 学習のループ
    for episode in range(num_episodes):
        if episode % video_interval == 0:
            # Gymのラッパーを使用して環境をラップ
            env_wrapped = wrappers.Monitor(env, f"./videos_{now.strftime('%Y%m%d_%H%M%S')}/{episode}", force=True)
            episode_reward, done= episode_step(env_wrapped, q_table,  max_steps, epsilon, alpha, gamma, binning_specs, True)
            print(f"episode {episode}: {episode_reward}")
        else:
            # ビデオを保存しないエピソード
            episode_reward, done= episode_step(env, q_table,  max_steps, epsilon, alpha, gamma, binning_specs, False)
        rewards.append(episode_reward)
        # if episode % video_interval == 0:
        #     print(f"episode {episode}: {episode_reward}")
    
    # 学習の結果を表示
    print("Training finished.\n")

    # 報酬の変動をグラフで表示
    plot_rewards(rewards)

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

</div></details>

**補足**

ChatGPTがそのまま出してくれたコードでは少し不具合があったので、以下の修正を行っています。

* 対象の状態空間を連続のまま取り扱うとメモリが足りなかったため、離散化を実施
* 一定間隔ごとにmp4で動画を保存するように設定
* 完成したコードに対して、コメントの追加を依頼

## 3.4. 実行結果

上記のコードを実行して、100エピソードごとの報酬の平均値を出したものはこちらです。50000回学習を行うことで、スコアはだんだんよくなりますが、0 程度で飽和し、安定的に着陸したことを示すスコア200には届いていません。

![result.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3130775/154ecfc3-6280-2397-2ef6-473f8b05c6bf.png)

シミュレーション動画: 1エピソード

![qlearn_first.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3130775/175e558e-df6e-d964-0ac2-4dce034eb1e4.gif)

シミュレーション動画: 50000エピソード付近

![qlearn_last.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3130775/8d22b439-2ed5-385a-23f4-3620280a9b78.gif)


50000回学習しても着陸を学ぶという段階には進んでおらず、機体が反転することがないように一応はコントロールしながらも、ただフラフラと落ちているだけのように見えます。

ChatGPT にもさんざん言われたのですが、Q-Learning で解くには問題自体がちょっと複雑なため、Q-Learning でこれ以上のパラメータ調整を頑張らずに、ニューラルネットワークを使った Deep Q-Learning に進みたいと思います。

# 4. Deep Q-Learning

## 4.1. Deep Q-Learning の概要

Deep Q-Learning の概要説明はこちらです。

![スクリーンショット 2023-05-27 084612.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3130775/083defd7-1cfa-a460-c9d9-b8223003a482.png)

## 4.2. 事前準備

ここからは、Pytorch のインストールをお願いします（バージョン等については Pytorchの公式サイトを参考に使用環境に応じて変更してください）。

```sh
pip install torch==1.13.1+cpu torchvision==0.14.1+cpu torchaudio==0.13.1 --extra-index-url https://download.pytorch.org/whl/cpu
```

## 4.3. Deep Q-Learning のコード

Deep Q-Learning について、同様に ChatGPT にコードを出力してもらいました。

<details><summary>長いので折りたたみます</summary><div>

```python: lunar_lander_dqn.py
import torch
import torch.nn as nn
import torch.optim as optim
from collections import deque
import random
import torch.nn.functional as F
import numpy as np
import gym
import matplotlib.pyplot as plt
import datetime

device = torch.device("cuda" if torch.cuda.is_available() else "cpu")

class DQNetwork(nn.Module):
    """DQN (Deep Q-Network) モデルクラス.
    
    Parameters
    ----------
    state_size : int
        状態の次元数
    action_size : int
        行動の数
    hidden_size : int, default=64
        隠れ層のノード数
    hidden_size2 : int, default=16
        2つ目の隠れ層のノード数
    """

    def __init__(self, state_size, action_size, hidden_size=64, hidden_size2=16):
        super(DQNetwork, self).__init__()

        # 全結合層
        self.fc1 = nn.Linear(state_size, hidden_size)
        self.fc2 = nn.Linear(hidden_size, hidden_size2)
        self.fc3 = nn.Linear(hidden_size2, action_size)

        # Heの初期化を使って重みを初期化
        nn.init.kaiming_normal_(self.fc1.weight, nonlinearity='relu')
        nn.init.kaiming_normal_(self.fc2.weight, nonlinearity='relu')
        nn.init.kaiming_normal_(self.fc3.weight, nonlinearity='relu')
        
    def forward(self, state):
        """順伝播を定義します.

        Parameters
        ----------
        state : torch.Tensor
            状態

        Returns
        -------
        action_values : torch.Tensor
            各行動の価値
        """
        x = torch.relu(self.fc1(state))
        x = torch.relu(self.fc2(x))
        action_values = self.fc3(x)

        return action_values

class ReplayBuffer:
    """経験再生 (Experience Replay) メモリクラス.

    Parameters
    ----------
    buffer_size : int
        バッファの大きさ
    """

    def __init__(self, buffer_size):
        self.buffer_size = buffer_size
        self.buffer = deque(maxlen=buffer_size)

    def add(self, experience):
        """経験をメモリに追加します.

        Parameters
        ----------
        experience : tuple
            経験 (状態, 行動, 報酬, 次の状態, 終了フラグ)
        """
        self.buffer.append(experience)

    def sample(self, batch_size):
        """メモリからランダムに経験をサンプリングします.

        Parameters
        ----------
        batch_size : int
            サンプリングする経験の数

        Returns
        -------
        list
            バッチサイズ分の経験
        """
        return random.sample(self.buffer, batch_size)

    def __len__(self):
        """メモリの現在の長さを返します.

        Returns
        -------
        int
            メモリの長さ
        """
        return len(self.buffer)

class DQNAgent:
    """DQNエージェントクラス.

    Parameters
    ----------
    state_size : int
        状態の次元数
    action_size : int
        行動の数
    hidden_size : int, default=64
        隠れ層のノード数
    buffer_size : int, default=10000
        経験再生メモリの大きさ
    batch_size : int, default=64
        バッチサイズ
    gamma : float, default=0.95
        割引率
    lr : float, default=0.01
        学習率
    update_every : int, default=5
        何ステップごとにネットワークを更新するか
    target_update_every : int, default=10
        何ステップごとにターゲットネットワークを更新するか
    """

    def __init__(self, state_size, action_size, hidden_size=64, buffer_size=10000,
                 batch_size=64, gamma=0.95, lr=0.01, update_every=5, target_update_every=10):
        self.state_size = state_size
        self.action_size = action_size
        self.hidden_size = hidden_size
        self.buffer_size = buffer_size
        self.batch_size = batch_size
        self.gamma = gamma
        self.lr = lr
        self.update_every = update_every
        self.target_update_every = target_update_every

        # Qネットワークとターゲットネットワークの初期化
        self.qnetwork = DQNetwork(state_size, action_size, hidden_size).to(device)
        self.target_network = DQNetwork(state_size, action_size, hidden_size).to(device)
        self.optimizer = optim.Adam(self.qnetwork.parameters(), lr=lr)

        # メモリの初期化
        self.memory = ReplayBuffer(buffer_size)
        self.t_step = 0
        self.tq_step = 0

    def step(self, state, action, reward, next_state, done):
        """エージェントのステップを進めます.

        Parameters
        ----------
        state : array_like
            現在の状態
        action : int
            行動
        reward : float
            報酬
        next_state : array_like
            次の状態
        done : bool
            エピソードが終了したかどうか
        """
        # 経験を再生メモリに保存
        self.memory.add((state, action, reward, next_state, done))

        # `update_every` ステップごとに学習
        self.t_step = (self.t_step + 1) % self.update_every
        if self.t_step == 0:
            # メモリに十分なサンプルがある場合、ランダムにサブセットを取得して学習
            if len(self.memory) > self.batch_size:
                experiences = self.memory.sample(self.batch_size)
                self.learn(experiences, self.gamma)
    
        # DQNの全ての重みとバイアスをターゲットネットワークに更新する
        # self.tq_step = (self.tq_step + 1) % self.target_update_every
        # if self.tq_step == 0:
        #     self.update_target_network()

    def update_target_network(self):
        """ターゲットネットワークを更新します.
        """
        self.target_network.load_state_dict(self.qnetwork.state_dict())

    def act(self, state, eps=0.):
        """行動を選択します.

        Parameters
        ----------
        state : array_like
            現在の状態
        eps : float, default=0.
            イプシロン、イプシロン貪欲な行動選択に使用

        Returns
        -------
        int
            選択された行動
        """
        # 行動を選択
        state = torch.from_numpy(state).float().unsqueeze(0).to(device)
        self.qnetwork.eval()
        with torch.no_grad():
            action_values = self.qnetwork(state)
        self.qnetwork.train()

        # イプシロン貪欲な行動選択
        if random.random() > eps:
            return np.argmax(action_values.cpu().data.numpy())
        else:
            return random.choice(np.arange(self.action_size))

    def learn(self, experiences, gamma):
        """経験を元にネットワークを学習させます.

        Parameters
        ----------
        experiences : tuple of (array_like, array_like, array_like, array_like, array_like)
            経験のタプル, (states, actions, rewards, next_states, dones)
        gamma : float
            割引率
        """
        # 状態、行動、報酬、次の状態、終了情報を抽出
        states, actions, rewards, next_states, dones = zip(*experiences)

        states = torch.from_numpy(np.vstack(states)).float().to(device)
        actions = torch.from_numpy(np.vstack(actions)).long().to(device)
        rewards = torch.from_numpy(np.vstack(rewards)).float().to(device)
        next_states = torch.from_numpy(np.vstack(next_states)).float().to(device)
        dones = torch.from_numpy(np.vstack(dones)).float().to(device)

        # qnetworkからQ値を取得
        Q_expected = self.qnetwork(states).gather(1, actions)

        # ターゲットネットワークから次の状態の最大予測Q値を取得
        Q_targets_next = self.target_network(next_states).detach().max(1)[0].unsqueeze(1)
        
        # 現在の状態のQターゲットを計算
        Q_targets = rewards + (gamma * Q_targets_next * (1 - dones))

        # 損失を計算
        loss = F.mse_loss(Q_expected, Q_targets)

        # 損失を最小化
        self.optimizer.zero_grad()
        loss.backward()
        self.optimizer.step()

def dqn(env, agent, now, n_episodes=2000, max_t=1000, eps_start=1.0, eps_end=0.01, eps_decay=0.995):
    """深層Q学習.

    パラメータ
    ----------
    env : gym.Env
        環境
    agent : DQNAgent
        エージェント
    n_episodes : int, default=2000
        エピソードの最大数
    max_t : int, default=1000
        エピソード内の最大ステップ数
    eps_start : float, default=1.0
        イプシロンの初期値
    eps_end : float, default=0.01
        イプシロンの最小値
    eps_decay : float, default=0.995
        イプシロンの減衰率

    戻り値
    -------
    list
        各エピソードのスコア
    """
    scores = []                        # 各エピソードからのスコアを格納するリスト
    scores_window = deque(maxlen=100)  # 最新の100のスコア
    eps = eps_start                    # イプシロンを初期化
    video_interval = 100

    for i_episode in range(1, n_episodes+1):
        state = env.reset()
        score = 0
        done = False

        # 100エピソードごとに録画を開始
        if i_episode % video_interval == 0:
            env = gym.wrappers.Monitor(env, f"./dvideos_{now.strftime('%Y%m%d_%H%M%S')}/{i_episode}", force=True)
            # env.seed(0)
            env._max_episode_steps = 500
            env.reset()

        while not done:
            action = agent.act(state, eps)
            next_state, reward, done, _ = env.step(action)
            agent.step(state, action, reward, next_state, done)
            state = next_state
            score += reward
            if done:
                agent.update_target_network()
                break 

        # 100エピソードの終了時に録画を終了
        if i_episode % video_interval == 0:
            env.close()
            env = gym.make('LunarLander-v2')
            env._max_episode_steps = 500
            # env.seed(0)

        scores_window.append(score)       # 最新のスコアを保存
        scores.append(score)              # 最新のスコアを保存
        eps = max(eps_end, eps_decay*eps) # イプシロンを減少させる
        print('\rEpisode {}\tAverage Score: {:.2f}'.format(i_episode, np.mean(scores_window)), end="")
        if i_episode % 100 == 0:
            print('\rEpisode {}\tAverage Score: {:.2f}'.format(i_episode, np.mean(scores_window)))
            print(f'eps: {eps}')
        # if np.mean(scores_window)>=200.0:
        #     print('\nEnvironment solved in {:d} episodes!\tAverage Score: {:.2f}'.format(i_episode-100, np.mean(scores_window)))
        #     torch.save(agent.qnetwork.state_dict(), 'checkpoint.pth')
        #     break
    return scores

def plot_rewards(rewards):
    """
    報酬の移動平均を計算し、グラフを表示します。

    パラメータ
    ----------
    rewards : list of float
        各エピソードの報酬のリスト
    """
    # 報酬の移動平均を計算
    moving_avg = np.convolve(rewards, np.ones((100,))/100, mode='valid')
    
    # グラフをプロット
    plt.plot(np.arange(len(moving_avg)), moving_avg)
    plt.title('Training Rewards')
    plt.xlabel('Episode')
    plt.ylabel('100 Episode Average')
    plt.savefig("result.png") 
    plt.show()

def main():
    buffer_size = int(1e3)
    hidden_size = 32
    batch_size = 64
    gamma = 0.99
    n_episodes=5000
    update_every=5
    lr = 0.00047
    eps_decay = 0.998

    # Gym環境を初期化
    env = gym.make('LunarLander-v2')
    # env.seed(0)
    env._max_episode_steps = 500

    now = datetime.datetime.now()

    # 環境から状態サイズとアクションサイズを取得
    state_size = env.observation_space.shape[0]
    action_size = env.action_space.n

    print('状態の形状: ', state_size)
    print('アクションの数: ', action_size)
    
    # DQNエージェントを初期化
    agent = DQNAgent(state_size=state_size, action_size=action_size, buffer_size=buffer_size, batch_size=batch_size, gamma=gamma, hidden_size=hidden_size, update_every=update_every, lr=lr)

    # エージェントを訓練
    scores = dqn(env, agent, now, n_episodes, eps_decay=eps_decay)

    # スコアをプロット
    plot_rewards(scores)

if __name__ == '__main__':
    main()
```

</div></details>

**補足**

上記コードは完成版ですが、この完成版のコードを出してもらうまでに、色々と問題が発生しました...

1. 機械学習ライブラリの変更

    一番初めは機械学習ライブラリとして、PytorchではなくKerasとtensorflowを使用するコードを出力してくれていたのですが、私のPCでメモリリークが発生し、学習が中断する問題が発生しました。色々探ってみたのですが原因がはっきりしないため、使用する機械学習ライブラリを Pytorch に変更するようお願いしました。

2. コードのバグ
   
   ChatGPT が出してくれたコード上で、ターゲットネットワークが正しく更新されていませんでした。

:::note info
特に2.についてパラメータがの調整不足が原因かと思い2日くらい試行錯誤していたのですが、単なるコードのバグでした (別のスレッドのChatGPTに「コードに変なところはない？」と聞いてみて発覚しました)

ChatGPTのことを信用しすぎず、ちゃんとコードはチェックした方がいいですね...
:::

## 4.4. 学習結果

Q-Learningと同様に、100エピソードごとの報酬の平均値を出力したのが下記になります。

![result.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3130775/5ee89abf-7a81-c024-5cbd-61d420145f9c.png)

無事200点以上を達成し、着陸できるようになりました！
5000エピソードまで学習を行いましたが、3000くらいで切るのが良さそうですね。

## 4.5. 学習中のシミュレーション結果

学習初期の100エピソード目はこちら。$\epsilon$（強化学習における探索と利用のバランスを管理するパラメータ）が大きいので、とにかくランダムに色々な方法を試そうとします。

![dqn_100.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3130775/b2cc2c68-4924-70f5-7802-5c9f5db5ac62.gif)

1000エピソードあたりで、ホバリングで点を稼ぐ（墜落ペナルティをもらわないようにする）ようになりました。パラメータの設定が悪いと、これ以上改善されません。

![dqn_1000.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3130775/727ad9b4-d33d-4a01-77d0-24dd4f2cf013.gif)

2000エピソード付近で、きれいに着陸することができるようになりました。

![dqn_2200.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3130775/f7a128ce-fb42-7dbe-c329-2986489cd7a3.gif)

2000エピソードでは無駄な燃料消費があったり、ときどき墜落することがありましたが、3000エピソードあたりで安定して高速に着陸しています。

![dqn_best_3000.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3130775/069a1932-3547-ab4e-a141-0ec47b21deac.gif)

## 4.6. パラメータ調整について

ChatGPTにパラメータについて聞くと、概要やどのように調整した方がよいかはある程度指針は教えてくれますが、今回は最終的な微調整は自分で行う必要がありました。

* ニューラルネットワークの構成
  
  使用するニューラルネットワークについて、ChatGPTが提案してくれたのは隠れ層は1層だったのですが、少ないかなと思ったので2層に増やしました（あまり試行錯誤はできていないです）。

*  $\epsilon$ について

    $\epsilon$ をあまりに早く下げると、動画も貼りましたが、着陸動作を学習せず、ホバリングし続ける（墜落によるマイナスペナルティを避ける）ような収束をしました。`eps_decay=0.995` とすると $\epsilon$ の減少が早すぎてダメで、最終的に収束の速さとの兼ね合いから 0.998 としました。

色々と苦労はしましたが、初歩的な DQN でもちゃんと学習させることができました！

# 5. おわりに

今回、色々と苦労して強化学習は難しいなと改めて思いましたが、本を読むだけだとちゃんと理解できていないことが、手を動かすとわかってくるのでいいですね。
みんなも遊んでみてね。

# 関連記事
https://qiita.com/key353/items/b9b244f01d3820ba8461

https://qiita.com/key353/items/ae6131ffea178c960fdd

https://qiita.com/key353/items/d58b93b4c21a9cb44baf

https://qiita.com/key353/items/9b1a4855aa5bc198bb7f