【OpenAI Gym】深層強化学習でAcrobotを攻略してみた。

はじめに


はじめまして。最近機械学習を勉強している、ほぼプログラミング初心者の社会人です。今回は、深層強化学習というものに興味を持ったので、挑戦してみました。
手探り状態で至らない点もありますが、ご容赦ください。

目次


  1. 参考

  2. OpenAI Gymについて

  3. Acrobotについて

  4. 実行環境

  5. 学習条件の設定

  6. 学習の実行

  7. 学習の結果と確認

  8. まとめ

1.参考


今回、深層強化学習に挑戦するにあたって、下記URLの書籍を参考にさせてもらいました。
書籍内では「Cartpole」という、箱に乗った棒を倒さないようにするゲームの攻略を紹介していました。本記事では、別の「Acrobot」というゲームを深層強化学習によって攻略していきます。

https://www.borndigital.co.jp/book/14383.html

2.OpenAI Gymについて


非営利団体の「OpenAI」が提供する強化学習用のツールキットです。
OpenAI Gymには様々なゲーム環境が用意されており、Google Colaboratoryにも「OpenAI Gym」がインストールされているため、強化学習を簡単に始められました。

3.Acrobotについて


OpenAI GymのAcrobotは、2本の棒が端の黄色部分で結合しており、関節のように折れ曲がります。
片側の棒の端が固定されており、関節部分を振り子のように動かして、うまく棒が灰色の線にタッチしたら報酬が与えられます。
下記のGifは、何も学習させていないニューラルネットワークでAcrobotを実行してみた結果になります。

無学習でAcrobotを実行


4.実行環境


深層強化学習の実行はGoogle Colaboratory上で行いました。

5.学習条件の設定


学習の手法は、DQN(Deep Q-network)という深層強化学習のアルゴリズムを活用します。
各条件下での行動の価値を学習するQ学習に深層学習を組み合わせたもので、より複雑な条件の学習が可能になります。

①ライブラリのインポート

import gym
import numpy as np
from keras.models import Sequential
from keras.layers import Dense
from keras.optimizers import adam_v2
from collections import deque
from keras.losses import huber_loss

各バージョンは以下の通りです。
gym 0.17.3
numpy 1.21.6
keras 2.8.0

②各種パラメーターの設定
探索パラメータ:ε-greedy法に関わる設定で、はじめはランダムに行動(探索)し、徐々に学習した確率の高い行動をとるようになります。
メモリパラメータ:学習を安定させるために、蓄積した経験からランダムに抽出したものを学習します。

# パラメータの準備
NUM_EPISODES = 500 # エピソード数
MAX_STEPS = 200 # 最大ステップ数
GAMMA = 0.99 # 時間割引率
WARMUP = 10 # 無操作ステップ数

# 探索パラメータ
E_START = 1.0 # εの初期値
E_STOP = 0.01 # εの最終値
E_DECAY_RATE = 0.0005 # εの減衰率

# メモリパラメータ
MEMORY_SIZE = 10000 # 経験メモリのサイズ
BATCH_SIZE = 32 # バッチサイズ

③ニューラルネットワークの定義
中間層を3層にして、損失関数をhuber_loss、最適化関数をAdamにしています。

class QNetwork:
    # 初期化
    def __init__(self, state_size, action_size):
        # モデルの作成
        self.model = Sequential()
        self.model.add(Dense(16, activation='relu', input_dim=state_size))
        self.model.add(Dense(16, activation='relu'))
        self.model.add(Dense(16, activation='relu'))
        self.model.add(Dense(action_size, activation='linear'))
        
        # モデルのコンパイル
        self.model.compile(loss='huber_loss', optimizer='adam')

④経験メモリの定義
経験メモリをdequeに格納し、一定数以上の経験になると古いものから削除されます。

# 経験メモリの定義
class Memory():
    # 初期化
    def __init__(self, memory_size):
        self.buffer = deque(maxlen=memory_size)

    # 経験の追加
    def add(self, experience):
        self.buffer.append(experience)

    # バッチサイズ分の経験をランダムに取得
    def sample(self, batch_size):
        idx = np.random.choice(np.arange(len(self.buffer)), size=batch_size, replace=False)
        return [self.buffer[i] for i in idx]
    
    # 経験メモリのサイズ
    def __len__(self):
        return len(self.buffer)

⑤環境の作成
行動価値関数(Qnet)を更新用のmain_qnと計算用のtarget_qnに分けることで、学習を安定化します。

# 環境の作成
env = gym.make('Acrobot-v1')
state_size = env.observation_space.shape[0] # 状態数
action_size = env.action_space.n # 行動数

# main-networkの作成
main_qn = QNetwork(state_size, action_size)

# target-orkの作成
target_qn = QNetwork(state_size, action_size)

# 経験メモリの作成
memory = Memory(MEMORY_SIZE)

6.学習の実行


学習の流れは以下のようになります。

  1. ε-greedy法に従って、行動を選択する。

  2. 行動に応じて、次の状態と報酬を取得する。

  3. 190ステップ以内にクリアしたら報酬が1、200ステップでもクリアできなかったら強制終了して報酬は0。

  4. 1行動ごとに行動価値関数を更新する。

  5. 1.~4.を繰り返し、5回成功したら学習終了とする。

  6. 学習後のモデルは、自身のPCにダウンロードする。


# 環境の初期化
state = env.reset()
state = np.reshape(state, [1, state_size])

# エピソード数分のエピソードを繰り返す
total_step = 0 # 総ステップ数
success_count = 0 # 成功数
for episode in range(1, NUM_EPISODES+1):
    step = 0 # ステップ数
    
    # target-networkの更新
    target_qn.model.set_weights(main_qn.model.get_weights())
    
    # 1エピソードのループ
    for _ in range(1, MAX_STEPS+1):
        step += 1
        total_step += 1

        # εを減らす
        epsilon = E_STOP + (E_START - E_STOP)*np.exp(-E_DECAY_RATE*total_step)
        
        # ランダムな行動を選択
        if epsilon > np.random.rand():
            action = env.action_space.sample()
        # 行動価値関数で行動を選択
        else:
            action = np.argmax(main_qn.model.predict(state)[0])

        # 行動に応じて状態と報酬を得る
        next_state, _, done, _ = env.step(action)
        next_state = np.reshape(next_state, [1, state_size])

        # エピソード完了時
        if done:
            # 報酬の指定
            if step <= 190:
                success_count += 1
                reward = 1
            else:
                success_count = 0
                reward = 0
            
            # 次の状態に状態なしを代入
            next_state = np.zeros(state.shape)
            
            # 経験の追加
            if step > WARMUP:
                memory.add((state, action, reward, next_state))
                 
        # エピソード完了でない時
        else:
            # 報酬の指定
            reward = 0
                
            # 経験の追加
            if step > WARMUP:
                memory.add((state, action, reward, next_state))
            
            # 状態に次の状態を代入
            state = next_state

        # 行動価値関数の更新
        if len(memory) >= BATCH_SIZE:
            # ニューラルネットワークの入力と出力の準備
            inputs = np.zeros((BATCH_SIZE, 6)) # 入力(状態)
            targets = np.zeros((BATCH_SIZE, 3)) # 出力(行動ごとの価値)

            # バッチサイズ分の経験をランダムに取得
            minibatch = memory.sample(BATCH_SIZE)
            
            # ニューラルネットワークの入力と出力の生成
            for i, (state_b, action_b, reward_b, next_state_b) in enumerate(minibatch):
                
                # 入力に状態を指定
                inputs[i] = state_b
                
                # 採った行動の価値を計算
                if not (next_state_b == np.zeros(state_b.shape)).all(axis=1):
                    target = reward_b + GAMMA * np.amax(target_qn.model.predict(next_state_b)[0])
                else:
                    target = reward_b

                # 出力に行動ごとの価値を指定
                targets[i] = main_qn.model.predict(state_b)
                targets[i][action_b] = target # 採った行動の価値

            # 行動価値関数の更新
            main_qn.model.fit(inputs, targets, epochs=1, verbose=0)
        
        # エピソード完了時
        if done:
            # エピソードループを抜ける
            break
           
    # エピソード完了時のログ表示
    print('エピソード: {}, ステップ数: {}, epsilon: {:.4f}'.format(episode, step, epsilon))

    # 5回連続成功で学習終了
    if success_count >= 5:
        break

    # 環境のリセット
    state = env.reset()
    state = np.reshape(state, [1, state_size])

main_qn.model.save('Acrobot.h5')
from google.colab import files
#ファイルを自分のパソコンに保存
files.download('Acrobot.h5')

7.学習の結果と確認


①結果


23回のエピソードで学習が完了しました。(4時間くらいかかりました…)
はじめの12ステップまでは1回も成功せず、13ステップ目で偶然成功してから報酬による行動価値関数の更新が起きはじめたようです。
21ステップ目からは3連続で成功しているため、学習が進んでることが読み取れます。
エピソード: 1, ステップ数: 200, epsilon: 0.9058
エピソード: 2, ステップ数: 200, epsilon: 0.8205
エピソード: 3, ステップ数: 200, epsilon: 0.7434
エピソード: 4, ステップ数: 200, epsilon: 0.6736
エピソード: 5, ステップ数: 200, epsilon: 0.6105
エピソード: 6, ステップ数: 200, epsilon: 0.5533
エピソード: 7, ステップ数: 200, epsilon: 0.5016
エピソード: 8, ステップ数: 200, epsilon: 0.4548
エピソード: 9, ステップ数: 200, epsilon: 0.4125
エピソード: 10, ステップ数: 200, epsilon: 0.3742
エピソード: 11, ステップ数: 200, epsilon: 0.3395
エピソード: 12, ステップ数: 200, epsilon: 0.3082
エピソード: 13, ステップ数: 154, epsilon: 0.2861
エピソード: 14, ステップ数: 200, epsilon: 0.2598
エピソード: 15, ステップ数: 166, epsilon: 0.2399
エピソード: 16, ステップ数: 200, epsilon: 0.2180
エピソード: 17, ステップ数: 200, epsilon: 0.1982
エピソード: 18, ステップ数: 200, epsilon: 0.1803
エピソード: 19, ステップ数: 200, epsilon: 0.1641
エピソード: 20, ステップ数: 200, epsilon: 0.1494
エピソード: 21, ステップ数: 187, epsilon: 0.1370
エピソード: 22, ステップ数: 173, epsilon: 0.1265
エピソード: 23, ステップ数: 178, epsilon: 0.1166

②検証

下記コードで、学習したモデルをもとにAcrobotを実行し、framesにGif作成用のアニメーションフレームを保存します。


# 評価
frames = [] # アニメーションフレーム

# 環境のリセット
state = env.reset()
state = np.reshape(state, [1, state_size])

# 1エピソードのループ
step = 0 # ステップ数
for step in range(1, 200):
    step += 1
    
    # アニメーションフレームの追加
    frames.append(env.render(mode='rgb_array'))
    
    # 最適行動を選択
    action = np.argmax(main_qn.model.predict(state)[0])

    # 行動に応じて状態と報酬を得る
    next_state, reward, done, _ = env.step(action)
    next_state = np.reshape(next_state, [1, state_size])

    # エピソード完了時
    if done:
        # 次の状態に状態なしを代入
        next_state = np.zeros(state.shape)
        
        # エピソードループを抜ける
        break
    else:
        # 状態に次の状態を代入
        state = next_state

# エピソード完了時のログ表示
print('ステップ数: {}'.format(step))

学習したモデルを使用して、Acrobotを10回実行してみた結果が以下になります。8割の確率で成功しています。
ステップ数: 189
ステップ数: 163
ステップ数: 166
ステップ数: 161
ステップ数: 165
ステップ数: 200
ステップ数: 139
ステップ数: 132
ステップ数: 170
ステップ数: 200

③実行

下記コードでGif形式でアニメーションを保存しました。

# JSAnimationのインストール
!pip install JSAnimation
!apt-get update && apt-get install imagemagick

# パッケージのインポート
import matplotlib.pyplot as plt
from matplotlib import animation
from JSAnimation.IPython_display import display_animation
from IPython.display import HTML

# アニメーション再生の定義
plt.figure(figsize=(frames[0].shape[1]/72.0, frames[0].shape[0]/72.0), dpi=72)
patch = plt.imshow(frames[0])
plt.axis('off')

# アニメーションの定期処理
def animate(i):
    patch.set_data(frames[i])

# アニメーション再生
anim = animation.FuncAnimation(plt.gcf(), animate, frames=len(frames), interval=50)
HTML(anim.to_jshtml())

anim.save('/content/drive/MyDrive/ball.gif', writer='imagemagick')

163ステップで成功したAcrobotが下記Gifのようになります。
徐々にふり幅を大きくしていって、端の棒がしっかりと灰色の線にタッチしていることがわかります。

学習後にAcrobotを実行


8.まとめ


今回は、深層強化学習を活用して「Acrobot」の攻略を行いました。
学習の初期はなかなか成功せず運頼みのところもあり、なかなか学習が進みませんでした。
また、さらに学習を進めさせたり、報酬条件を厳しくするとより性能の高いモデルができるかなと思います。
今後はより効率的に学習させられるようなコーディングを追究していきたいと思います。

この記事が気に入ったらサポートをしてみませんか?