やってみた!

やってみた!

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

PyBullet-HumanoidFlagrunHarderBulletEnv-v0(2)

2020/1/25改正
 学習継続時に早くalphaが収束するようalpha、log_alpha、alpha_optimizerを保存するように変更しました。gpu有、無しの両環境で保存データを共有できるようモデル読み込み時にmap_location=deviceを追加しました。

2020/1/23改正
 BATCH_SIZEを128から論文と同じ256に変更したら学習が飛躍的に改善しました。あわせてSIGMA_DECAY を 3000から10に、num_episodesを9000に変更しました。

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

 DDPGではHumonoidFlagrunをうまく学習させられなかったので、SAC(Soft Actor-Critic)で学習させてみました。以下まとめ。

  •  SAC動かすのにかなり手間取ったので、まだHumanoidFlagrunHarderを学習させることができていません(できないかも・・・汗)。本記事は、やっと学習するようになったHumanoidBulletEnv-v0を例に紹介します。
  • 学習に8時間かけて、やっと、すぐには転ばず、足を出す動作を覚えました。さらに時間をかければもっと安定するようになるでしょう。学習しているのを確認するだけなら2,3時間だけでもいいと思いますが、なにせ時間がかかります。

1.SAC(Soft Actor-Critic) 

 SAC(Soft Actor-Critic)は2018年に発表された比較的新しい方式で、DDPGと同様連続値を扱うことができる強化学習法のひとつです。以下の論文は初期のSACの計算手順の改良版について書かれたもので、今回はこれをベースにしました。本論文にはSACはDDPGや他の手法と比較して学習の進みが早いといった特長があることが示されています。

 本論文はGoogleカルフォルニアバークレー校の共同作業のようです。

 こちらも参考にさせてもらいました。なおSpinning Upの内容は改良前のSAC計算方法なので若干異なりますが、SACの式の解説等、基本的考え方は参考になります。

Soft Actor-Critic — Spinning Up documentation

GitHub - openai/spinningup: An educational resource to help anyone learn deep reinforcement learning.

GitHub - ku2482/soft-actor-critic.pytorch: A PyTorch Implementation of Soft Actor-Critic.

 次の章でとにかくコードを動かしてみますが、その前にSACのイメージだけでも。

  • 方策policyの出力(行動)を確率的なものとします。policyの出力は中心値に対してgauss分布でばらつくものとし、中心値とばらつきの大きさエントロピーニューラルネットワークで推定します。
  • 価値関数は、報酬に加え方策のエントロピー(ばらつきの大きさ)の大きさを加えます。これにより、より大きなエントロピーを持つ行動を学習するようになります。不安定な極所的最適解が学習から排除され、学習が安定して進むものと思われます。

     例えば水上の飛び石の上を渡っているとすると、DDPGでは単純に乱数で選んで学習するので小さな石を選び、うまくいったらそれを学習してしまうのに対し、SACはある幅をもった行動の価値を高いと考えるので、常に大きな石を選ぶように学習します。

    f:id:akifukka:20200121192727j:plain
     この積み重ねでSACはより安定した行動を学習し、極所最適解にはまったりせず学習が進むということだと思われます。

2.とにかく動かしてみる

 SACの細かい話は次回以降にして、とにかく動かしてみましょう。なお、ある程度動きが改善されてるなと感じるくらいまで学習させるには8~10時間くらいの覚悟が必要です。

(1) Colaboratoryを立ち上げる

 ランタイム-ランタイムのタイプを変更でGPUにします。

(2)ライブラリをインストールします。

 MountainCarContinuousも動かせるようBox2Dもインストールしています。

!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
!pip3 install pybullet

!pip3 install 'gym[Box2D]'
!pip3 install  'gym[classic_control]'

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

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

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

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

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

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

(5) ネットワーク、オプティマイザ、リプレイメモリ等を生成し初期化します。

#初期化コード
import gym
import pybullet_envs
import math
import random
import numpy as np
import matplotlib
import matplotlib.pyplot as plt
from collections import namedtuple
#from itertools import count

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
os.environ["DISPLAY"] = ":" + str(display.display) + "." + str(display.screen)

env = gym.make('HumanoidBulletEnv-v0')
#env = gym.make('HumanoidFlagrunBulletEnv-v0')
#env = gym.make('HumanoidFlagrunHarderBulletEnv-v0')
#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 squashed Gaussian Policy
LOG_STD_MAX = 2
LOG_STD_MIN = -20
EPS = 1e-8

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)

        #mu 中心値算出用
        self.muln = nn.Linear(NumHidden, NumAct)

        #log_std エントロピー×ー1算出用
        self.log_stdln = nn.Linear(NumHidden, NumAct)

    def forward(self, x):
        x = F.relu(self.ln1(x))
        x = F.relu(self.ln2(x))
#層数が多いと収束が遅いことがあるので層数減らした。収束が早くなったかは未確認。
#        x = F.relu(self.ln3(x))

        mu = self.muln(x)
        log_std = torch.tanh(self.log_stdln(x))
        log_std = LOG_STD_MIN + (LOG_STD_MAX - LOG_STD_MIN) * (log_std + 1) * 0.5

        log_std_exp = log_std.exp()
        #πの計算(action) ノイズはガウス分布
        noise = torch.randn_like(mu)
        pi = mu + noise * log_std_exp
        #Gaussian Policy類推度の計算(Log-Likelihood)
        pre_sum = -0.5 * (((pi-mu)/(log_std_exp + EPS))**2 + 2 * log_std + np.log(2 * np.pi))
        logp_pi = torch.sum(pre_sum,1,keepdim=True)

        #squash tanhで範囲をー1~1につぶす
        mu, pi, logp_pi = apply_squashing_func(mu, pi, logp_pi)

        return mu,pi,logp_pi

#傾きを保持したままクリッピングだけを行う
def clip_but_pass_gradient(x, low=-1., high=1.):
    clip_high = (x > high).float()
    clip_low = (x < low).float()
    return x + ((high - x)*clip_high + (low - x)*clip_low).detach()

def apply_squashing_func(mu, pi, logp_pi):
    mu = mu.tanh()
    pi = pi.tanh()

    logp_pi = logp_pi - torch.sum((clip_but_pass_gradient(1 - pi**2,low = 0, high = 1) + 1e-6).log(),1,keepdim=True)
    return mu, pi, logp_pi

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

    def forward(self, input):
        x = F.leaky_relu(self.ln1(input))
        x = F.leaky_relu(self.ln2(x))
#層数が多いと収束が遅いことがあるので層数減らした。収束が早くなったかは未確認。
#        x = F.leaky_relu(self.ln3(x))
        y = self.ln4(x)
        return y

def optimize_model():
    global alpha

    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ターゲット
    next_mus,next_actions,next_logp_pis = policy_net(next_state_batch)
    next_q1s = q1_target_net(torch.cat([next_state_batch, next_actions], 1))
    next_q2s = q2_target_net(torch.cat([next_state_batch, next_actions], 1))
    next_qs = torch.min(next_q1s, next_q2s)

    q_targets = reward_batch + GAMMA * (1.0-done_batch) * (next_qs - alpha * next_logp_pis)

    #Q1ネット
    #loss関数
#    q1_loss = 0.5 * F.mse_loss(q1_net(torch.cat([state_batch, action_batch],1)),q_targets) 中身がわかるような記述に変更
    q1_loss = 0.5 * torch.mean((q1_net(torch.cat([state_batch, action_batch],1)) - q_targets)**2)
    #ネットの学習
    q1_optimizer.zero_grad()
    #誤差逆伝搬
    q1_loss.backward(retain_graph=True)
    #重み更新
    q1_optimizer.step()

    #Q2ネット
    #loss関数
#    q2_loss = 0.5 * F.mse_loss(q2_net(torch.cat([state_batch, action_batch],1)),q_targets) 中身がわかるような記述に変更
    q2_loss = 0.5 * torch.mean((q2_net(torch.cat([state_batch, action_batch],1)) - q_targets)**2)
    #ネットの学習
    q2_optimizer.zero_grad()
    #誤差逆伝搬
    q2_loss.backward(retain_graph=True)
    #重み更新
    q2_optimizer.step()

    #policyネットのloss関数
    mus,actions,logp_pis = policy_net(state_batch)
    q1s = q1_net(torch.cat([state_batch,actions],1))
    q2s = q2_net(torch.cat([state_batch,actions],1))
    qs = torch.min(q1s, q2s)
    p_loss = torch.mean(alpha * logp_pis - qs)

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

    alpha_loss = -torch.mean(log_alpha*(target_entropy + logp_pis))
    #log_alpha更新
    alpha_optimizer.zero_grad()
    #誤差逆伝搬
    alpha_loss.backward(retain_graph=True)
    #log_alpha更新
    alpha_optimizer.step()
    #alpha更新
    alpha = log_alpha.exp()

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

    for target_param, local_param in zip(q2_target_net.parameters(), q2_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]
target_entropy = -torch.Tensor([n_actions]).to(device)
print('num actions,observations,target_entropy',n_actions,n_observations,target_entropy)

#ネットワーク
#policyネットとそのtargetネット
policy_net = Policy_net(n_observations, n_actions,256).to(device)
#Qネット,Vネットとそのtargetネット
q1_net = Q_net(n_observations + n_actions, 256).to(device)
q2_net = Q_net(n_observations + n_actions, 256).to(device)
q1_target_net = Q_net(n_observations + n_actions, 256).to(device)
q2_target_net = Q_net(n_observations + n_actions, 256).to(device)
#Qターゲットネット初期化(コピー)
q1_target_net.load_state_dict(q1_net.state_dict())
q2_target_net.load_state_dict(q1_net.state_dict())

#学習用optimizer生成
p_optimizer = optim.Adam(policy_net.parameters(),lr=3e-4)
q1_optimizer = optim.Adam(q1_net.parameters(),lr=3e-4)
q2_optimizer = optim.Adam(q2_net.parameters(),lr=3e-4)
log_alpha = torch.zeros(1,requires_grad=True,device=device)
alpha = log_alpha.exp()
alpha_optimizer = optim.Adam([log_alpha], lr=3e-4)

#学習用リプレイメモリ生成
memory = ReplayMemory(1000000)
memory.reset()

#総数カウント用
total_episode = 0
total_step = 0

 (6)学習

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

 num_epsode=20000を書き換えて学習episode数を変更します。最初は1000~5000くらいにして、様子を見た方がいいかも知れません。この学習コードだけ繰り返して実行すると、前の状態から学習を継続してくれます。

 また、後で説明するモデル読み込んだ後、学習コードを実行するとそこから継続して学習できます。

 学習を途中で止めても、再実行で継続できます(ごくたまに失敗しますが)。

 (5)の初期化コードを実行すると直前までの学習モデルや、ファイルから読み込んだモデルが初期化されてしまうので注意が必要です。

 なおColaboratoryは90分セッションが切れているとリセットされてしまうということで、次の記事の通りChromeのアドオンAuto Refreshを使います。

Google Colaboratoryの90分セッション切れ対策【自動接続】 - Qiita

#初期化コードを再実行せず、このコードだけを繰り返し実行すると直前の学習状態、
#もしくはファイルから読み込んだモデルを続けて学習できる。

#BATCH_SIZE = 128
BATCH_SIZE = 256
#Qネット学習時の報酬の割引率
GAMMA = 0.99
#alphaは自動計算のためコメントアウト
#alpha = 0.05

#episode数に対するノイズの減少係数
#OUNoise オリジナルのSACでは使わないが試行錯誤でいれてみた。
#ちゃんと確認できていないがMountainCarでは効果あるかも。
SIGMA_START = 1.0 #最初
SIGMA_END = 0.0 #最後
SIGMA_DECAY = 10 #このepisode回数で約30%まで減衰
SIGMA_DECAY_END = SIGMA_DECAY * 1.2 #このepisode以降はOUNoiseを使用しない
theta = 0.08

#50 フレーム/sec グラフ描画用
FPS = 50
#学習エピソード数。num_epsode=20000を変更する。
num_episodes = 9000
display_on = 1 # num_episodeの最後の回の動画データ保存
save_model_on = 1 # 0 モデル自動セーブしない 1:モデル自動セーブする
t_max = 1400 # 1エピソードでの最大ステップ数。ステップ数がこの値以上になったら強制的にenvをリセットして次のエピソードに移行

noise =np.array([random.uniform(-0.5,0.5) for i in range(n_actions)],dtype = np.float)

for i_episode in range(num_episodes):
    #whileループ内初期化
    observation = env.reset()
    done = False
    reward_total = 0.0
    logp_pi_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([])

    sigma = SIGMA_END + (SIGMA_START - SIGMA_END) * math.exp(-1. * total_episode / SIGMA_DECAY)

    total_episode += 1

    while not done and t < t_max:

        with torch.no_grad():
          mu,action,logp_pi = policy_net(torch.from_numpy(observation).float().reshape(1,-1).to(device))

          mu = mu.cpu().data.numpy().reshape(-1)
          action = action.cpu().data.numpy().reshape(-1)
          logp_pi = logp_pi.cpu().data.numpy().reshape(-1)[0]
          logp_pi_total += logp_pi

          #OUNoise 最初だけ。total_episode > SIGMA_DECAY_ENDではOUNoiseを加算しない。
          noise = noise - theta * noise + sigma * np.array([random.uniform(-1.0,1.0) for i in range(len(noise))])
          if total_episode < SIGMA_DECAY_END :
            action = mu + noise

          #episodeの最後は純粋にネットワークの性能を取得するためノイズ無し(中心値mu)-------------------
          if (i_episode == num_episodes -1 ):
            action = mu
          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

        #学習 現バージョンは毎ステップごとに学習。
        #episode終了毎にまとめて学習させることもできるが計算が遅くなったような気がしたので。
#        if ((t == t_max -1) or done) and (i_episode != num_episodes -1):
#          for j in range(t):
        optimize_model()

        #データ保存------------------------------------
        if (i_episode == num_episodes -1) and (display_on == 1) :
#          print(i_episode,observation,mu,action,_logp_pi)
          #動画
          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
        total_step += 1
        # end while loop ------------------------------

    #10episode毎に、最後のepisodeの報酬、エントロピーを表示
    if (total_episode % 10 == 0) or (i_episode == num_episodes -1 ):
      print('episode,step,reward,entropy=',total_episode,total_step,reward_total,-logp_pi_total/t)
    
    #1000episodeごとにモデルをGoogleドライブに保存
    if (total_episode % 1000 == 0) and (save_model_on == 1):
      torch.save({
        'epoch':total_episode,
        'step':total_step,
        'log_alpha':log_alpha,
        'alpha':alpha,
        'policy':policy_net.state_dict(),
        'q1':q1_net.state_dict(),
        'q2':q2_net.state_dict(),
        'q1_target':q1_target_net.state_dict(),
        'q2_target':q2_target_net.state_dict(),
        'p_optimizer':p_optimizer.state_dict(),
        'q1_optimizer':q1_optimizer.state_dict(),
        'q2_optimizer':q2_optimizer.state_dict(),
        'alpha_optimizer':alpha_optimizer.state_dict(),
      },'/content/drive/My Drive/model_Humanoid_SAC' + str(total_episode) )
# end for loop --------------------------------------

実行すると10episodeごとに次のように表示されます。

episode,step,reward,entropy= 10 188 -63.53772342780692 0.4764575527773963
episode,step,reward,entropy= 20 374 -53.021724941738086 10.449289242426554
episode,step,reward,entropy= 30 581 -67.98715715128392 8.465339357202703
 

 ここで、各数値の意味は次の通りです。

episode:総エピソード数、step:総ステップ数、entropy:エントロピー

 最初はすぐに転倒してしまうため、1episodeのstepが少ないので、stepはあまり増えません(上の例では、10episodeで200stepしか進まないので平均すると1episode=20step程度で転倒しています)。

 なお、humanoidでは、エントロピーが-17になるようalpha(エントロピー調整係数(Qを求める際に、報酬にエントロピーを加算する時にエントロピーに乗ずる係数)、 inverse of reward scaleとも呼ばれる)を自動調整しているので、学習が進むとエントロピーもー17近辺に近づいていきます。

 (7)画像を表示

 画像を生成します。またgif動画ファイルを作ってGoogleドライブに保存します。

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

fig = plt.figure(figsize=(5, 4))
plt.axis('off')
plt.subplots_adjust(left=0, right=1, bottom=0, top=1)

images = []
for f in frames:
  image = plt.imshow(f)
  images.append([image])
ani = matplotlib.animation.ArtistAnimation(fig, images, interval=30, repeat_delay=1)
ani.save("/content/drive/My Drive/images.gif", writer="pillow")

HTML(ani.to_jshtml())

  (8)モデルを読み込み

 Googleドライブに自動保存されたモデルを読み込みます。そこから学習を継続することも可能です。ただしリプレイメモリの内容は保存されていないので、過去の試行例は引き継がれないので、純粋な意味では条件は変わります。

#Googleドライブからモデルを読み込む。ファイル名は都度変更すること。
checkpoint = torch.load('/content/drive/My Drive/model_Humanoid_SAC16000', map_location=device)
total_episode = checkpoint['epoch']
total_step = checkpoint['step']
log_alpha = checkpoint['log_alpha']
alpha = checkpoint['alpha']
policy_net.load_state_dict(checkpoint['policy'])
q1_net.load_state_dict(checkpoint['q1'])
q2_net.load_state_dict(checkpoint['q2'])
q1_target_net.load_state_dict(checkpoint['q1_target'])
q2_target_net.load_state_dict(checkpoint['q2_target'])
p_optimizer.load_state_dict(checkpoint['p_optimizer'])
q1_optimizer.load_state_dict(checkpoint['q1_optimizer'])
q2_optimizer.load_state_dict(checkpoint['q2_optimizer'])
alpha_optimizer.load_state_dict(checkpoint['alpha_optimizer'])

3.結果です。

  約連続98時間学習した結果です。論文では100万stepの学習で報酬5000だとか・・・。当方、137万stepで報酬291・・・。
 BATCH_SIZEを128から256に変更したところ大きく改善しました。9000 episodesで約10時間かかります。ちなみに最大ステップ数1400を変更するとrewardも増えます。

BATCH_SIZEが128:episode:16352、step:1378063 reward:291
BATCH_SIZEが256:episode:9002、step:1626911 reward:563

f:id:akifukka:20200125075100g:plain
つづく
 SACについて少し解説を書いた後、HumanoidFlagrunに挑戦する予定です。まだ全く手が付いていないので時間かかりそうですが・・。

 なにせ今回、最初はSACの初期の論文ベースで実装して、いきなりFlagrunをやってみたのですが全然ダメで。バグなのか課題が難しすぎるのか判断つかず2週間ほど迷走して、やっと現状までたどりついたところなので・・・。では!

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