やってみた!

やってみた!

試したことを中心に、書評や興味のあること、思ったこととか

Open AI Gym Box2D BipedalWalkerをColaboratoryで動かしてみる(4)

改正2019.12.26

 ソースリスト中でsteps_done +=1の位置をwhileループ(各ステップ計算ループ)からepsodeのforループに移動しました(バグ)。このバグのためSIGMA_DECAYがほとんど効かず、すぐにノイズが小さくなっていました。あわせてSIGMA_DECAYの設定値も150→30に見直しました。

-----------------------------------

 前回は歩行パターンのスケジュールを作り、PD制御で脚の角度を制御し何とか2,3歩歩くことができました。

 ここからはpytorchで深層強化学習を実装していきます。が、BipedalWalkerはあまりに手強いので、手始めにMountainCarContinuousで試してみます。

 主に次のサイトを参考にさせてもらいました。

  実装はDDPG(Deep Deterministic Policy Gradient)です。元の論文はこちら。

 また、こちらにOPEN AIによる解説があります。

Deep Deterministic Policy Gradient — Spinning Up documentation

1.DDPG 

 DDPGは強化学習の手法のひとつDPG(Deterministic Policy Gradient)を発展させ、DeepLerningを適用したものです。元となったDPGの論文はこちら。

Deterministic Policy Gradient Algorithms,Silver et al., 2014

 DDPGの特徴は次の通りです。

  • action(制御の指令値)に連続値(discreteではなく)を扱うことができますDQN(Deep Q Network)の制御指令値はDiscrete値なので応用範囲が限定されますが、DDPGは連続値なので一般的な制御に応用することができます。
  • actor-critic アルゴリズムを採用します。actor(policy)は行動方策を推定し、critic(Q関数)は行動の価値を推定するものです。これらをディープネットワークで定義して同時に学習していきます。

2.動かしてみます

 DDPGの詳細は後回しにして、まずはどんなものか動かしてみます。

(1)Corabolatoryの準備

 Colaboratoryを立ち上げます。「ランタイム」ー「ランタイムのタイプを変更」でハードウェアアクセラレータ「GPU」を選択します。

(2)必要なライブラリのインストール

必要なライブラリをインストールします。

!apt-get update
!pip3 install box2d-py
!pip3 install 'gym[Box2D]'
!pip3 install  'gym[classic_control]'
!apt-get -qq -y install xvfb freeglut3-dev ffmpeg
!pip3 -q install pyglet
!pip3 -q install pyopengl
!pip3 -q install pyvirtualdisplay
!apt-get install x11-utils

(3)ランタイムを再起動します。

 インルトールした内容を反映させるため、一旦ランタイムを再起動します。
「ランタイム」-「ランタイムの再起動」

(4)学習前の処理(リプレイメモリ、ネットワークの定義、初期化)

 次のコードを実行します。

import gym
import math
import random
import numpy as np
import matplotlib
import matplotlib.pyplot as plt
from collections import namedtuple
from itertools import count
from PIL import Image

import torch
import torch.nn as nn
import torch.optim as optim
import torch.nn.functional as F
import torchvision.transforms as T

from pyvirtualdisplay import Display
from scipy.interpolate import interp1d

display = Display(visible=0, size=(1024, 768))
display.start()
import os
#pyvirtualdisplayの仕様変更で次の文はエラーになるのでコメントアウトします(2021/6/5変更)
#os.environ["DISPLAY"] = ":" + str(display.display) + "." + str(display.screen)

#Bipedalwalkerのv2は無くなったのでv3に変更(2021/6/5)
#env = gym.make('BipedalWalker-v3')
env = gym.make('MountainCarContinuous-v0')

# if gpu is to be used
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")

#リプレイメモリー
Transition = namedtuple('Transition',
                        ('state', 'action', 'next_state', 'reward','done'))

class ReplayMemory(object):
    def __init__(self, capacity):
        self.capacity = capacity
        self.memory = []
        self.position = 0

    def push(self, *args):
        """Saves a transition."""
        if len(self.memory) < self.capacity:
            self.memory.append(None)
        self.memory[self.position] = Transition(*args)
        self.position = (self.position + 1) % self.capacity

    def sample(self, batch_size):
        return random.sample(self.memory, batch_size)

    def __len__(self):
        return len(self.memory)
   
    def reset(self):
        self.memory = []
        self.position = 0   

#Actor(Polycy) Network
class Policy_net(nn.Module):
    def __init__(self, NumObs, NumAct,NumHidden):
        super(Policy_net, self).__init__()
        self.ln1 = nn.Linear(NumObs, NumHidden)
        self.ln2 = nn.Linear(NumHidden, NumHidden)
        self.ln3 = nn.Linear(NumHidden, NumHidden)
        self.ln4 = nn.Linear(NumHidden, NumAct)
        self.ln4.weight.data.uniform_(-3e-3, 3e-3)

    def forward(self, x):
        x = F.relu(self.ln1(x))
        x = F.relu(self.ln2(x))
        x = F.relu(self.ln3(x))
        y = torch.tanh(self.ln4(x))
        return y

#Critic(Q) Network Q値の計算
class Q_net(nn.Module):
    def __init__(self, NumObs, NumAct,NumHidden1,NumHidden2):
        super(Q_net, self).__init__()
        self.ln1 = nn.Linear(NumObs, NumHidden1)
        self.ln2 = nn.Linear(NumHidden1+NumAct, NumHidden2)
        self.ln3 = nn.Linear(NumHidden2, NumHidden2)
        self.ln4 = nn.Linear(NumHidden2, 1)
        self.ln4.weight.data.uniform_(-3e-3, 3e-3)

    def forward(self, state, action):
        x = F.leaky_relu(self.ln1(state))
        x = torch.cat((x, action), dim=1)
        x = F.leaky_relu(self.ln2(x))
        x = F.leaky_relu(self.ln3(x))
        y = self.ln4(x)
        return y

def optimize_model():
    if len(memory) < BATCH_SIZE:
        return
    transitions = memory.sample(BATCH_SIZE)
    batch = Transition(*zip(*transitions))

    #batchを一括して処理するためtensorのリストを2次元テンソルに変換
    state_batch = torch.stack(batch.state,dim=0)
    action_batch = torch.stack(batch.action,dim=0)
    reward_batch = torch.stack(batch.reward,dim=0)
    next_state_batch = torch.stack(batch.next_state,dim=0)
    done_batch = torch.stack(batch.done,dim=0)

    #Qネットのloss関数計算
    next_actions = policy_target_net(next_state_batch)
    next_Q_values =q_target_net(next_state_batch ,next_actions)
    expected_Q_values = (next_Q_values * GAMMA)*(1.0-done_batch) + reward_batch

    Q_values =  q_net(state_batch ,action_batch)

    #Qネットのloss関数
    q_loss = F.mse_loss(Q_values,expected_Q_values)

    #Qネットの学習
    q_optimizer.zero_grad()
    #誤差逆伝搬
    q_loss.backward()
    #重み更新
    q_optimizer.step()

    #policyネットのloss関数
    actions = policy_net(state_batch)
    p_loss = -q_net(state_batch,actions).mean()

    #policyネットの学習
    p_optimizer.zero_grad()
    #誤差逆伝搬
    p_loss.backward()
    #重み更新
    p_optimizer.step()

    tau = 0.001
    #targetネットのソフトアップデート
    #学習の収束を安定させるためゆっくり学習するtarget netを作りloss関数の計算に使う。
    #学習後のネット重み×tau分を反映させ、ゆっくり追従させる。
    for target_param, local_param in zip(policy_target_net.parameters(), policy_net.parameters()):
      target_param.data.copy_(tau*local_param.data + (1.0-tau)*target_param.data)

    for target_param, local_param in zip(q_target_net.parameters(), q_net.parameters()):
      target_param.data.copy_(tau*local_param.data + (1.0-tau)*target_param.data)

#action,observationの要素数取得
n_actions = env.action_space.shape[0]
n_observations = env.observation_space.shape[0]
print('num actions,observations',n_actions,n_observations )

#ネットワーク
#policyネットとそのtargetネット
policy_net = Policy_net(n_observations, n_actions,128).to(device)
policy_target_net = Policy_net(n_observations, n_actions,128).to(device)
#Qネットとそのtargetネット
q_net = Q_net(n_observations, n_actions, 64, 128).to(device)
q_target_net = Q_net(n_observations, n_actions, 64, 128).to(device)
#学習用optimizer生成
p_optimizer = optim.Adam(policy_net.parameters(),lr=1e-3)
q_optimizer = optim.Adam(q_net.parameters(),lr=3e-3, weight_decay=0.0001)
#学習用リプレイメモリ生成
memory = ReplayMemory(10000)
memory.reset()
#ノイズの大きさ計算用、最初は大きくして学習が進んだら小さくするためのカウンタ
steps_done = 0

(5)学習します

 所要時間は10分くらいでしょうか。表示されるrewardは報酬の合計値で、山登り成功の場合は80~95点くらいになります。学習中は制御指令値にノイズを加算しているので完全な実力値とは言えません。epsodeの最後の回はノイズ無しなので、ほぼ実力値になります。

 学習epsodeが200回の場合、0から始めて199回目のrewardが実力相当の値です。学習数は必要に応じて変更してください。

#BATCH_SIZE = 128
BATCH_SIZE = 128
#Qネット学習時の報酬の割引率
GAMMA = 0.999
#step_doneに対するノイズの減少係数
SIGMA_START = 1.0 #最初
SIGMA_END = 0.3 #最後
SIGMA_DECAY = 30 #この回数で約30%まで減衰

#50 フレーム/sec グラフ描画用
FPS = 50
#学習数。num_epsode=200を変更する。
num_episodes = 200

for i_episode in range(num_episodes):
    #whileループ内初期化
    observation = env.reset()
    done = False
    reward_total = 0.0
    t = 0
    #グラフ用データ保存領域初期化
    frames = []
    observations = []
    actions = []
    ts = []
    for i in range(n_observations):
      observations.append([])
    for i in range(n_actions):
      actions.append([])

    noise =np.array([random.uniform(-0.5,0.5) for i in range(n_actions)],dtype = np.float)
    theta = 0.08
    sigma = SIGMA_END + (SIGMA_START - SIGMA_END) * math.exp(-1. * steps_done / SIGMA_DECAY)
    steps_done += 1
while not done and t < 1400: #指令値生成------------------------------------ sample = random.random() with torch.no_grad(): action = policy_net(torch.from_numpy(observation).float().to(device)) action = action.cpu().data.numpy() #最後は純粋にネットワークのデータを取得するためノイズ無し------------------- if (i_episode != num_episodes -1 ): #OUNoise noise = noise - theta * noise + sigma * np.array([random.uniform(-1.0,1.0) for i in range(len(noise))]) action += noise action = np.clip(action, -1, 1) #物理モデル1ステップ--------------------------- next_observation, reward, done, info = env.step(action) reward_total = reward_total + reward #学習用にメモリに保存-------------------------- tensor_observation = torch.tensor(observation,device=device,dtype=torch.float) tensor_action = torch.tensor(action,device=device,dtype=torch.float) tensor_next_observation = torch.tensor(next_observation,device=device,dtype=torch.float) tensor_reward = torch.tensor([reward],device=device,dtype=torch.float) tensor_done = torch.tensor([done],device=device,dtype=torch.float) memory.push(tensor_observation, tensor_action, tensor_next_observation, tensor_reward,tensor_done) #observation(state)更新------------------------ observation = next_observation #学習してpolicy_net,target_neを更新 optimize_model() #データ保存------------------------------------ if i_episode == num_episodes -1 : #動画 frames.append(env.render(mode = 'rgb_array')) #グラフ for i in range(n_observations): observations[i].append(observation[i]) for i in range(n_actions): actions[i].append(action[i]) ts.append(t/FPS) #時間を進める---------------------------------- t += 1 # end while loop ------------------------------ print('episode,reward=',i_episode,reward_total) # end for loop --------------------------------------

 (6)学習結果を見てみる

次を実行させると動画を見ることができます。

import matplotlib.pyplot as plt
import matplotlib.animation
import numpy as np
from IPython.display import HTML

fig2 = plt.figure(figsize=(6, 6))
plt.axis('off')

images = []
for f in frames:
  image = plt.imshow(f)
  images.append([image])
ani = matplotlib.animation.ArtistAnimation(fig2, images, interval=30, repeat_delay=1)

HTML(ani.to_jshtml()) 

f:id:akifukka:20191222113715j:plain


(7)ネットワークを可視化してみる

 デバッグのためQネット(critic)、policyネット(actor)を可視化してみました。少し謎のところもありますが・・・。

import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D
import numpy as np

X, Y = np.mgrid[-1:1.1:0.1, -1:1.1:0.1]

#Q map
Z = np.zeros((21,21,21),dtype = float)
Za = np.zeros((21,21),dtype = float)
position = -1.0
for i in range(21):#position loop
  speed = -1.0
  for j in range(21):#speed loop
    a = -1.0
    a_qmax = -1.0
    qmax = -10000.0
    for k in range(21):#a loop
      q = q_net( torch.tensor([[position,speed]]).float().to(device), torch.tensor([[a]]).float().to(device)).to("cpu").detach().numpy()[0][0]
      Z[i][j][k] = q
      if (qmax < q) :
        qmax = q 
        a_qmax = a
      a = a + 0.1
    Za[j][i] = a_qmax
    speed = speed + 0.1
  position = position + 0.1

#Qmap
fig = plt.figure(figsize=(18, 6), facecolor="w")
ax = fig.add_subplot(121, projection="3d")
surf = ax.plot_surface(X, Y, Z[10])
ax.set_xlabel("speed",fontsize = 14)
ax.set_ylabel("a",fontsize = 14)
ax.set_zlabel("Q",fontsize = 14)
ax.set_title("position 0.0")

ax = fig.add_subplot(122, projection="3d")
surf = ax.plot_surface(X, Y, Za)
ax.set_ylabel("position",fontsize = 14)
ax.set_xlabel("speed",fontsize = 14)
ax.set_zlabel("a",fontsize = 14)
ax.set_title("calc from Qmap")

plt.show()

#policy map
Zpolicy = np.zeros((21,21),dtype = float)
speed = -1.0
for i in range(21):
  position = -1.0
  for j in range(21):
    Zpolicy[i][j] = policy_net(torch.tensor([[position,speed]]).float().to(device)).to("cpu").detach().numpy()[0][0]
    position = position + 0.1
  speed = speed + 0.1

fig = plt.figure(figsize=(9, 6), facecolor="w")
ax = fig.add_subplot(111, projection="3d")
ax.set_xlabel("speed",fontsize = 14)
ax.set_ylabel("position",fontsize = 14)
ax.set_zlabel("a",fontsize = 14)
surf = ax.plot_surface(X, Y, Zpolicy,color="green")
ax.set_title("policy")
plt.show()

 

f:id:akifukka:20191222114605j:plain

 左上の図はposition=0.0(図の真ん中なので谷より若干右より?)における価値関数Qネットの計算値です。

 Q = Q_net(observation, action)

 observation=[position,speed]

 a=action (+で右に押す、-で左に押す)

 aが変わっても以外にQの値は大きくは変わりませんでした。

 右上の図は行動aを変化させながらQを計算して、最もQが大きくなるaを描画したものです。

 左下の図は行動aを決めるpolicyネットの計算値です。この図から次のことがわかります。

・速度が-の時(台車が左に動いている)は-に押す(左に押す)

・速度が+の時(台車が右に動いている)は+に押す(右に押す)

 右上のQネットから算出した行動予測の図とpolicyネットを計算した左下の図はほぼ一致すると思っていたのですが、どうも一致しないようです。学習の初期ではほぼ一致してますが、学習が進むにつれ違いが出てきます。

 今のところ、この現象は理解できていません・・・。が、学習自体は上手くいっているようなので・・。

ーーーーーーーーーーーーーーーーーーーーーーーー

つづく!

 次回はDDPGの概要を理解できる様、ざっくり解説をしてみます。BipedalWalkerはその後に挑戦ということで(なんせ先に中身を理解しておかないと、太刀打ちできなさそうなので)。

 強化学習 カテゴリーの記事一覧 - やってみた!