やってみた!

やってみた!

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

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

 今回はおまけということで、DDPGに教師を追加してみました。記事の最初の方で作成したPD制御を教師として、DDPGの経験処理中に行動をアシストすると、学習に何か効果があるかを試しました。

 結果、最初は教師の影響を受けて大股で歩こうとしていたものの、最終的にはちょこちょこ走りになってしまいました。学習回数も教師無しより余計にかかってしまいました。これは教師自体がへたくそ(すぐつまづく)だったこともあると思いますが。。

 その他、Colaboratoryが動画保存中に異常終了することが多かったので、学習だけ実行してネットワークをGoogleドライブに保存し、後でネットワークを読み込んで動画を作るように等、少し改良しました。

 BipedalWalkerは本記事で最後なので、これまでの記事の繰り返しになりますが、やり方を一通り最初からまとめておきます。

1. 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.Colaboratoryのランタイムを再起動します。

 インストールしたライブラリを有効にするためランタイムを再起動します。

 ランタイム-ランタイムの再起動

4.Googleドライブをマウントします。

 学習したモデルを保存するためGoogleドライブをマウントします。
 実行するとリンクが表示されるのでリンクをクリックして、アカウントを選択して表示されたコードをCtrl-Cでコピーして、Enter your authorization code:の四角のエリアにCtrl-Pでペーストして、Entキーを押します。

from google.colab import drive
drive.mount('/content/drive')

 5.学習前の処理

 ネットワーク、オプティマイザ、リプレイメモリ等を生成し初期化します。ちなみに、BatchNorm処理は試したのですが結局使っていません(コメントアウトしています)。

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)
        self.bn1 = nn.BatchNorm1d(num_features=NumHidden2)

    def forward(self, state, action):
        x = F.leaky_relu(self.ln1(state))
        x = torch.cat((x, action), dim=1)
#        x = F.leaky_relu(self.bn1(self.ln2(x)))
        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(1000000)
memory.reset()
#ノイズの大きさ計算用、最初は大きくして学習が進んだら小さくするためのカウンタ
steps_done = 0

6.学習

 学習します。1000エピソードごとにネットワークをGoogleドライブに保存します。ファイル名はmodelxxxxtで、xxxxはエピソード数です。

 教師の影響を徐々に減衰するようにしてあって、減衰の仕方はSIGMA_T_START、SIGMA_T_END、SIGMA_T_DECAYで調整します。行動actionは次の式で計算します。

action += sigma_t * (action1 - action)

 sigma_tがそのEPSODEにおける減衰率、action1は教師値、actionはpolicyネットワークの予想する最適行動です。教師とpolicyネットワークの予想値との差を、sigma_tの割合だけ、actionを教師側に近い値に修正しています。教師が手を取って介助しているイメージです。

 ノイズはそのままだと教師の影響が薄れてしまうため若干小さめにしました。

#角度制御ゲイン
 #P(比例)項
pgain = np.array([4,4,4,4], dtype = 'float')
 #D(微分)項
dgain = np.array([0.2,0.2,0.2,0.2], dtype = 'float')

#脚の指令角スケジュール
xsch = np.array([0,1,2,3,4,5,6,7,8,9,10],dtype = 'float')
yhip1  = np.array([ 0.021, -0.39, -0.58, -0.54, 0.45,    0.18, 0.88,  0.78, 0.60,  0.20, 0.021],dtype = 'float')
yknee1 = np.array([  0.17,  0.85,  0.95,  0.92, -0.38, -0.05,  -0.34,  -0.090,  0.074, 0.043,  0.17],dtype = 'float')
yhip2  = np.array([ 0.18,  0.88,  0.78,  0.60,  0.20, 0.021, -0.39, -0.58, -0.54, 0.45,    0.18],dtype = 'float')
yknee2 = np.array([ -0.05,-0.34,-0.090,  0.074,  0.043 ,  0.17,  0.85, 0.95,  0.92, -0.38, 0.05],dtype = 'float')
numx = xsch.shape[0]

hip1demmand = interp1d(xsch, yhip1)
knee1demmand = interp1d(xsch, yknee1)
hip2demmand = interp1d(xsch, yhip2)
knee2demmand = interp1d(xsch, yknee2)

legangle = np.array([0,0,0,0], dtype = 'float')
leganglespeed = np.array([0,0,0,0], dtype = 'float')
legangledemand = np.array([0,0,0,0], dtype = 'float')

speed = 4.2

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

#steps_doneに対する教師の減少係数
SIGMA_T_START = 0.4 #最初
SIGMA_T_END = 0.02 #最後
SIGMA_T_DECAY = 3000 #この回数で約30%まで減衰

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

for i_episode in range(num_episodes):
    #whileループ内初期化
    x = 0.0
    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
    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)
    #TEACHER
    sigma_t = SIGMA_T_END + (SIGMA_T_START - SIGMA_T_END) * math.exp(-1. * steps_done / SIGMA_T_DECAY)
    steps_done += 1
    display_on = 0
    
    while not done and t < 700:
        #指令値生成------------------------------------
        x += speed/FPS
        if x>10.0 :x-=10
        legangledemand[0] = hip1demmand(x)
        legangledemand[1] = knee1demmand(x)
        legangledemand[2] = hip2demmand(x)
        legangledemand[3] = knee2demmand(x)

        #脚の角度 PD制御
        action1 = pgain * (legangledemand - legangle) - dgain * leganglespeed
        action1 = np.clip(action1, -0.6, 0.6)

        #----------------------------------------------
        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 ):
          action += sigma_t * (action1 - action)

        #最後は純粋にネットワークのデータを取得するためノイズ無し-------------------
        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

        #脚の角度,角速度,胴体角取り出し----------------
        legangle[0] = observation[4] #HIP1
        leganglespeed[0] = observation[5]
        legangle[1] = observation[6] #KNEE1
        leganglespeed[1] = observation[7]
        legangle[2] = observation[9] #HIP2
        leganglespeed[2] = observation[10]
        legangle[3] = observation[11] #KNEE2
        leganglespeed[3] = observation[11]

        #学習用にメモリに保存--------------------------
        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) and (display_on == 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 ------------------------------
    if (steps_done % 10 == 0) or (i_episode == num_episodes -1 ):
#      print('episode,reward=',i_episode,reward_total)
      print('episode,reward=',steps_done,reward_total)
    if (steps_done % 1000 == 0):
      torch.save({
        'epoch':steps_done,
        'policy':policy_net.state_dict(),
        'policy_target':policy_target_net.state_dict(),
        'p_optimizer':p_optimizer.state_dict(),
        'q':q_net.state_dict(),
        'q_target':q_target_net.state_dict(),
        'q_optimizer':q_optimizer.state_dict(),
      },'/content/drive/My Drive/model' + str(steps_done) + 't' )
  # end for loop --------------------------------------

7.ネットワーク読み込み

  読み込みたいネットワークにあわせて赤字は都度変更してください。

checkpoint = torch.load('/content/drive/My Drive/model4000t')
steps_done = checkpoint['epoch']
policy_net.load_state_dict(checkpoint['policy'])
policy_target_net.load_state_dict(checkpoint['policy_target'])
p_optimizer.load_state_dict(checkpoint['p_optimizer'])
q_net.load_state_dict(checkpoint['q'])
q_target_net.load_state_dict(checkpoint['q_target'])
q_optimizer.load_state_dict(checkpoint['q_optimizer'])

8.学習したネットワークを動かして動画を保存

 6.学習のソースを一部変更して動かします。

①num_episodesを1にする。

 #学習数。num_epsode=200を変更する。
 num_episodes = 1

②画像保存スイッチを入れる。

 display_on = 1

 ③実行します。

9.画像を表示

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())

10.まとめ

 教師をつけて試しにDDPGで学習をしてみました。結果は次の通りです。

  • 学習したpolicyネットワークは最初は教師の影響を受けて、教師に近い動き(大股歩き)をしようとしました。
  • 教師の影響が強いと、もともと教師自体がすぐにつまづくためか、学習したpolicyもすぐにつまづいてしまいました(10000回も学習すると教師よりは多少ましでしたが・・)
  • SIGMA_T_ENDを0.2→0.02に変更して教師の影響を小さくすると、徐々にちょこちょこ走りになっていきました。
  • 結局、教師無しと比較して、学習回数が減ったとか、大股で歩けるようになったとかいう具体的なメリットは得られませんでした

 今回はあまりいい結果ではありませんでしたが、DDPG学習に教師の影響を受けさせられることを確かめられたので、どこかで使えることもあるかもしれません。ということで前向きに考えることにします!(大分試すのに時間をとられましたが・・・)。

おしまい

 BipedalWalkerの記事はこれでおしまいです!。今回の記事は長く続きましたが、最後までお付き合い頂きありがとうございました。

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