Python
DeepLearning
強化学習
TensorFlow

[強化学習]連続した行動空間を扱えるPPO

強化学習】実装しながら学ぶPPO【CartPoleで棒立て:1ファイルで完結】
を参考にして、連続したaction空間を扱えるPPOを作ろうとしたのですが、今のところ上手くいっていません。
学習が進むようになりました! ただ、まだ上手くいかないときもあります。
pendulumではenvironmentが与える報酬は常に負で幅も少し大きめなのですが、これを調整して基本的に正の報酬を与えるようにすると学習が進みやすくなりました。
pendulum.gif

もし、何か修正点を見つけてくださった場合は、コメント欄でこっそり教えてください。

追記
※2017/11/19: コードを修正
学習の途中で重みがnanになっているようです... ゼロ割かlog(0)かな?
※2017/11/21: コードを修正

上記の記事からの主な変更点は、以下の通りです。

ベータ分布

連続したactionに対して確率を返してくれる分布を考えます。

そうなるとまず思い浮かぶのが正規分布ですが、actionに上限と下限がある場合、確率分布の区間にも上限と下限があるとよいと思います。

ベータ分布の場合の区間が0<=x<=1で、パラメータがα、βの二つ、適当にパラメータを調整すれば様々な形状の分布をとれます。以下の記事を参照
ベータ分布(Beta distribution)

Improving Stochastic Policy Gradients in Continuous Control with Deep Reinforcement Learning using the Beta Distribution
そこで、ニューラルネットのoutputを以下のように変更します。

def _build_model(self):     # Kerasでネットワークの形を定義します
    l_input = Input(batch_shape=(None, NUM_STATES))
    l_dense1 = Dense(NUM_HIDDENS[0], activation='relu')(l_input)
    out_alpha = Dense(NUM_ACTIONS, activation='softplus')(l_dense1)
    out_beta = Dense(NUM_ACTIONS, activation='softplus')(l_dense1)
    out_value = Dense(1, activation='linear')(l_dense1)
    model = Model(inputs=[l_input], outputs=[out_alpha, out_beta, out_value])

    A_BOUNDS = [env.action_space.low, env.action_space.high]

    alpha, beta, v = self.model(self.s_t)

    beta_dist = tf.contrib.distributions.Beta(alpha + 1, beta + 1)
    self.prob = beta_dist.prob( (self.a_t - A_BOUNDS[0]) / (-A_BOUNDS[0] + A_BOUNDS[1]) ) + 1e-10

    self.A = beta_dist.sample(1) * (-A_BOUNDS[0] + A_BOUNDS[1]) + A_BOUNDS[0]

全体のコード

PPOContinuous.py
# coding:utf-8
# -----------------------------------
# OpenGym Pendulum-v0 with PPO on CPU
# -----------------------------------
#
#参考にした記事
#https://qiita.com/sugulu/items/8925d170f030878d6582#ppo%E3%82%A2%E3%83%AB%E3%82%B4%E3%83%AA%E3%82%BA%E3%83%A0%E8%A7%A3%E8%AA%AC
#http://proceedings.mlr.press/v70/chou17a/chou17a.pdfs

import tensorflow as tf
import matplotlib.pyplot as plt
from scipy.stats import beta
import gym, time, threading
import numpy as np
import random as pyrandom
from sklearn.utils import shuffle
from gym import wrappers  # gymの画像保存
from keras.models import *
from keras.layers import *
from keras.utils import plot_model
from keras import backend as K
import os
os.environ['TF_CPP_MIN_LOG_LEVEL'] = '2'    # TensorFlow高速化用のワーニングを表示させない

# -- constants of Game
ENV = 'Pendulum-v0'
env = gym.make(ENV)
NUM_STATES = env.observation_space.shape[0]   
NUM_ACTIONS = env.action_space.high.size   
A_BOUNDS = [env.action_space.low, env.action_space.high]
NONE_STATE = np.zeros(NUM_STATES)

# -- constants of Brain
MIN_BATCH = 128
BUFFER_SIZE = MIN_BATCH * 60
MAX_STEPS = 200
EPOCH = 3
EPSILON = 0.2 # loss_CPIをCLIPする範囲を決めます
LOSS_V = 0.2  # v loss coefficient
LOSS_ENTROPY = 0.001  # entropy coefficient
LEARNING_RATE = 1e-3

# -- params of Advantage-ベルマン方程式
GAMMA = 0.995
N_STEP_RETURN = 5
GAMMA_N = GAMMA ** (N_STEP_RETURN)
LAMBDA = 0.95
NUM_HIDDENS = [400, 200, 200]

#TIME_HORIZON = 32
N_WORKERS = 16   # スレッドの数
#Tmax = 3*N_WORKERS   # 各スレッドの更新ステップ間隔      

# ε-greedyのパラメータ
EPS_START = 1
EPS_END = 0.01
EPS_STEPS = 500*N_WORKERS*MAX_STEPS

TARGET_SCORE = -250


# --各スレッドで共有するTensorFlowのDeep Neural Networkのクラスです -------
class Brain:
    def __init__(self):   # globalなparameter_serverをメンバ変数として持つ
        self.global_step = tf.Variable(0, trainable=False)
        self.learning_rate = tf.train.exponential_decay(LEARNING_RATE, self.global_step, 1000, 0.98, staircase=True)
        with tf.name_scope("old_brain"):
            self.old_model = self._build_model(trainable=False)
            self.old_weights = tf.contrib.framework.get_trainable_variables(scope='old_brain')
        with tf.name_scope("brain"):
            self.train_queue = [[], [], [], [], []]  # s, a, r, s', s' terminal mask
            K.set_session(SESS)
            self.model = self._build_model(trainable=True)  # ニューラルネットワークの形を決定
            self.weights = tf.contrib.framework.get_trainable_variables(scope='brain')
            self.opt = tf.train.AdamOptimizer(learning_rate=self.learning_rate)  # loss関数を最小化していくoptimizerの定義です
            self.assign_op = [self.old_weights[i].assign(self.weights[i]) for i in range(len(self.weights))]
            self.graph = self.build_graph()  # ネットワークの学習やメソッドを定義

    def _build_model(self, trainable):     # Kerasでネットワークの形を定義します
        l_input = Input(batch_shape=(None, NUM_STATES))
        l_dense1 = Dense(NUM_HIDDENS[0], activation='relu', kernel_initializer='he_normal', trainable=trainable)(l_input)
        #l_dense2 = Dense(NUM_HIDDENS[1], activation='relu')(l_input)
        #l_dense3 = Dense(NUM_HIDDENS[2], activation='relu')(l_dense1)
        out_alpha = Dense(NUM_ACTIONS, activation='softplus', kernel_initializer='he_normal', trainable=trainable)(l_dense1)
        out_beta = Dense(NUM_ACTIONS, activation='softplus', kernel_initializer='he_normal', trainable=trainable)(l_dense1)
        out_value = Dense(1, activation='linear', kernel_initializer='he_normal', trainable=trainable)(l_dense1)
        model = Model(inputs=[l_input], outputs=[out_alpha, out_beta, out_value])
        model._make_predict_function()  # have to initialize before threading
        plot_model(model, to_file='PPO.png', show_shapes=True)  # ネットワークの可視化
        return model

    def build_graph(self):      # TensorFlowでネットワークの重みをどう学習させるのかを定義します
        self.s_t = tf.placeholder(tf.float32, shape=(None, NUM_STATES))  # placeholderは変数が格納される予定地となります
        self.a_t = tf.placeholder(tf.float32, shape=(None, NUM_ACTIONS))
        self.r_t = tf.placeholder(tf.float32, shape=(None, 1))  # not immediate, but discounted n step reward

        #x = tf.clip_by_value((self.a_t - A_BOUNDS[0]) / (-A_BOUNDS[0] + A_BOUNDS[1]), 0 + 1e-8, 1 - 1e-8) 
        x = (self.a_t - A_BOUNDS[0]) / (-A_BOUNDS[0] + A_BOUNDS[1])

        self.alpha, self.beta, self.v = self.model(self.s_t)
        alpha_old, beta_old, v_old = self.old_model(self.s_t)

        beta_dist = tf.contrib.distributions.Beta(self.alpha + 1, self.beta + 1)
        self.prob = beta_dist.prob( x ) + 1e-8

        beta_dist_old = tf.contrib.distributions.Beta(alpha_old + 1, beta_old + 1)
        self.prob_old = tf.stop_gradient(beta_dist_old.prob( x ) + 1e-5)
        self.A = beta_dist.sample(1) * (-A_BOUNDS[0] + A_BOUNDS[1]) + A_BOUNDS[0]

        # loss関数を定義します
        self.advantage = self.r_t - self.v
        #mean, var = tf.nn.moments(self.advantage, axes=[1])
        #stand_adv = (self.advantage - mean) / (var + 1e-8)
        r_theta = self.prob / self.prob_old
        loss_CPI = r_theta * tf.stop_gradient(self.advantage)  # stop_gradientでadvantageは定数として扱います

        # CLIPした場合を計算して、小さい方を使用します。
        self.r_clip = tf.clip_by_value(r_theta, 1.0-EPSILON, 1.0+EPSILON)
        clipped_loss_CPI = self.r_clip * tf.stop_gradient(self.advantage)  # stop_gradientでadvantageは定数として扱います
        self.loss_CLIP = -tf.reduce_mean(tf.minimum(loss_CPI, clipped_loss_CPI))

        self.loss_value = LOSS_V * tf.reduce_mean(tf.square(self.advantage))  # minimize value error
        self.entropy = LOSS_ENTROPY * tf.reduce_mean(beta_dist.entropy())  # maximize entropy (regularization)
        self.loss_total = self.loss_CLIP + self.loss_value - self.entropy

        minimize = self.opt.minimize(self.loss_total, global_step=self.global_step)   # 求めた勾配で重み変数を更新する定義
        return minimize

    def update_parameter_server(self):     # 重みを学習・更新します
        if len(self.train_queue[0]) < BUFFER_SIZE:    # データがたまっていない場合は更新しない
            return
        queue = self.train_queue
        self.train_queue = [[], [], [], [], []]
        Buffer = np.array(queue).T
        [SESS.run(self.assign_op[i]) for i in range(len(self.assign_op))]
        for i in range(EPOCH):
            print("EPOCH:" + str(i+1))
            n_batches = int(BUFFER_SIZE / MIN_BATCH)
            batch = shuffle(Buffer)
            for n in range(n_batches):
                s, a, r, s_, s_mask = np.array(batch[n * MIN_BATCH: (n + 1) * MIN_BATCH]).T
                s = np.vstack(s)    # vstackはvertical-stackで縦方向に行列を連結、いまはただのベクトル転置操作
                a = np.vstack(a)
                r = np.vstack(r)
                s_ = np.vstack(s_)
                s_mask = np.vstack(s_mask)

                # Nステップあとの状態s_から、その先得られるであろう時間割引総報酬vを求めます
                _, _, v = self.model.predict(s_)

                # N-1ステップあとまでの時間割引総報酬rに、Nから先に得られるであろう総報酬vに割引N乗したものを足します
                r = r + LAMBDA * GAMMA_N * v * s_mask  # set v to 0 where s_ is terminal state

                feed_dict = {self.s_t: s, self.a_t: a, self.r_t: r}     # 重みの更新に使用するデータ

                minimize = self.graph
                SESS.run(minimize, feed_dict)   # Brainの重みを更新

        print("learning_rate" + str(SESS.run(self.learning_rate)))
        print("entropy:" + str(SESS.run(self.entropy, feed_dict={self.s_t:s}) / LOSS_ENTROPY))
        print("state:" + str(s[0]))
        print("alpha:" + str(SESS.run(self.alpha, feed_dict={self.s_t: s})[0]+ 1))
        print("beta:" + str(SESS.run(self.beta, feed_dict={self.s_t: s})[0] + 1))
        print("v:" + str(SESS.run(self.v, feed_dict={self.s_t: s}).mean()))
        print("r:" + str(r.mean()))
        print("advantage:" + str(SESS.run(self.advantage, feed_dict={self.s_t: s, self.r_t: r}).mean()))
        print("loss_value:" + str((SESS.run(self.loss_value, feed_dict={self.s_t: s, self.r_t: r}))))
        print("loss_actor: " + str(SESS.run(self.loss_CLIP, feed_dict)))
        print("r_clip:" + str(SESS.run(self.r_clip, feed_dict={self.s_t: s, self.a_t: a})[0]))
        print("loss_total: " + str(SESS.run(self.loss_total, feed_dict)))

    def predict_a(self, s):    # 状態sから各action
        a = SESS.run(self.A, feed_dict={self.s_t: s})
        return a

    def train_push(self, s, a, r, s_):
        self.train_queue[0].append(s)
        self.train_queue[1].append(a)
        self.train_queue[2].append(r)

        if s_ is None:
            self.train_queue[3].append(NONE_STATE)
            self.train_queue[4].append(0.)
        else:
            self.train_queue[3].append(s_)
            self.train_queue[4].append(1.)

class Agent:
    def __init__(self, brain):
        self.brain = brain   # 行動を決定するための脳(ニューラルネットワーク)
        self.memory = []        # s,a,r,s_の保存メモリ、 used for n_step return
        self.R = 0             # 時間割引した、「いまからNステップ分あとまで」の総報酬R
        self.count = 0

    def act(self, s):
        if frames >= EPS_STEPS:   # ε-greedy法で行動を決定します
            eps = EPS_END
        else:
            eps = EPS_START + frames * (EPS_END - EPS_START) / EPS_STEPS  # linearly interpolate

        if pyrandom.random() < eps:
            return np.random.uniform(A_BOUNDS[0], A_BOUNDS[1], NUM_ACTIONS)  # ランダムに行動
        else:
            s = np.array([s])
            a = self.brain.predict_a(s)
            return a

    def advantage_push_brain(self, s, a, r, s_):   # advantageを考慮したs,a,r,s_をbrainに与える
        def get_sample(memory, n):  # advantageを考慮し、メモリからnステップ後の状態とnステップ後までのRを取得する関数
            s, a, _, _ = memory[0]
            _, _, _, s_ = memory[n - 1]
            return s, a, self.R, s_

        self.memory.append((s, a, r, s_))

        # 前ステップの「時間割引Nステップ分の総報酬R」を使用して、現ステップのRを計算
        self.R = (self.R + LAMBDA * r * GAMMA_N) / GAMMA     # r0はあとで引き算している、この式はヤロミルさんのサイトを参照

        # advantageを考慮しながら、LocalBrainに経験を入力する
        if s_ is None:
            while len(self.memory) > 0:
                n = len(self.memory)
                self.R = self.R - LAMBDA * self.memory[0][2] + self.memory[0][2]
                s, a, r, s_ = get_sample(self.memory, n)
                self.brain.train_push(s, a, r, s_)
                self.R = (self.R - self.memory[0][2]) / GAMMA
                self.memory.pop(0)

            self.R = 0  # 次の試行に向けて0にしておく

        if len(self.memory) >= N_STEP_RETURN:
            self.R = self.R - LAMBDA * self.memory[0][2] + self.memory[0][2]
            s, a, r, s_ = get_sample(self.memory, N_STEP_RETURN)
            self.brain.train_push(s, a, r, s_)
            self.R = self.R - self.memory[0][2]     # # r0を引き算
            self.memory.pop(0)
            #print([self.memory[i][2] for i in range(len(self.memory))])


# --CartPoleを実行する環境です、TensorFlowのスレッドになります -------
class Environment:
    total_reward_vec = np.array([TARGET_SCORE - 100 for i in range(20)])  # 総報酬を20試行分格納して、平均総報酬をもとめる
    count_trial_each_thread = 0     # 各環境の試行数

    def __init__(self, name, thread_type, brain):
        self.name = name
        self.thread_type = thread_type
        self.env = gym.make(ENV)
        self.agent = Agent(brain)    # 環境内で行動するagentを生成
        self.memory = []
        self.thread_step = 0

    def run(self):
        global frames  # セッション全体での試行数、global変数を書き換える場合は、関数内でglobal宣言が必要です
        global isLearned
        global TRAIN

        if (self.thread_type is 'test') and (self.count_trial_each_thread == 0):
            self.env.reset()
            self.env = gym.wrappers.Monitor(self.env, './movie/PPO')  # 動画保存する場合

        s = self.env.reset().reshape(-1)
        R = 0
        step = 0
        while True:
            #if TRAIN:
             #   continue
            if self.thread_type is 'test':
                self.env.render()   # 学習後のテストでは描画する
                time.sleep(0.1)

            a = self.agent.act(s)   # 行動を決定)
            s_, r, done, info = self.env.step(a)   # 行動を実施
            a = a.reshape(-1)
            r = r.reshape(-1)
            s_ = s_.reshape(-1)

            step += 1
            self.thread_step += 1
            frames += 1     # セッショントータルの行動回数をひとつ増やします

            if step > MAX_STEPS or done:  # terminal state
                s_ = None

            # 報酬と経験を、Brainにプッシュ
            #self.memory.append((s, a, r /10 + 2.2, s_))
            #if self.thread_step % TIME_HORIZON == 0:
             #   while len(self.memory) > 0:
              #      s1, a1, r1, s_1 = self.memory[0]
               #     self.agent.advantage_push_brain(s1, a1, r1, s_1)
                #    self.memory.pop(0)
            self.agent.advantage_push_brain(s, a, r /4 + 2.2, s_)

            s = s_
            R += r
            if len(self.agent.brain.train_queue[0]) >= BUFFER_SIZE:
                if not(isLearned) and self.thread_type is 'learning':
                    #TRAIN = True
                    self.agent.brain.update_parameter_server()
                    #TRAIN = False

            if step > MAX_STEPS or done:
                self.total_reward_vec = np.hstack((self.total_reward_vec[1:], R))  # トータル報酬の古いのを破棄して最新10個を保持
                self.count_trial_each_thread += 1  # このスレッドの総試行回数を増やす
                break
        # 総試行数、スレッド名、今回の報酬を出力
        print("スレッド:"+self.name + "、試行数:"+str(self.count_trial_each_thread) + "、今回の報酬:" + str(R)+"、平均報酬:"+str(self.total_reward_vec.mean()))

        # スレッドで平均報酬が一定を越えたら終了
        if self.total_reward_vec.mean() > TARGET_SCORE:
            isLearned = True
            time.sleep(2.0)     # この間に他のlearningスレッドが止まります

# --スレッドになるクラスです -------
class Worker_thread:
    # スレッドは学習環境environmentを持ちます
    def __init__(self, thread_name, thread_type, brain):
        self.environment = Environment(thread_name, thread_type, brain)
        self.thread_type = thread_type

    def run(self):
        while True:
            if not(isLearned) and self.thread_type is 'learning':     # learning threadが走る
                self.environment.run()

            if not(isLearned) and self.thread_type is 'test':    # test threadを止めておく
                time.sleep(1.0)

            if isLearned and self.thread_type is 'learning':     # learning threadを止めておく
                time.sleep(3.0)

            if isLearned and self.thread_type is 'test':     # test threadが走る
                global frames
                frames = EPS_STEPS
                global SAVED
                if not(SAVED): #パラメータをセーブ
                    saver = tf.train.Saver(self.environment.agent.brain.weights)
                    saver.save(SESS, '.\PPOModel\ckpt\model.ckpt') #ファイルと同じディレクトリにフォルダー'PPOModel'がいります。
                    SAVED = True
                time.sleep(3.0)
                self.environment.run()


# -- main ここからメイン関数です------------------------------
# M0.global変数の定義と、セッションの開始です
frames = 0              # 全スレッドで共有して使用する総ステップ数
isLearned = False        # 学習が終了したことを示すフラグ
config = tf.ConfigProto(allow_soft_placement = True)
SESS = tf.Session(config = config)
#TRAIN = False
SAVED = False

# M1.スレッドを作成します
with tf.device("/cpu:0"):
#with tf.device("/gpu:0"):
    brain = Brain()     # ディープニューラルネットワークのクラスです
    threads = []     # 並列して走るスレッド
    # 学習するスレッドを用意
    for i in range(N_WORKERS):
        thread_name = "local_thread"+str(i+1)
        threads.append(Worker_thread(thread_name=thread_name, thread_type="learning", brain=brain))

# 学習後にテストで走るスレッドを用意
threads.append(Worker_thread(thread_name="test_thread", thread_type="test", brain=brain))

# M2.TensorFlowでマルチスレッドを実行します
COORD = tf.train.Coordinator()                  # TensorFlowでマルチスレッドにするための準備です
SESS.run(tf.global_variables_initializer())     # TensorFlowを使う場合、最初に変数初期化をして、実行します

#saver = tf.train.Saver(brain.weights)
#saver.restore(SESS, '.\PPOModel\ckpt\model.ckpt') #セーブしたパラメータを再利用

running_threads = []
for worker in threads:
    job = lambda: worker.run()      # この辺は、マルチスレッドを走らせる作法だと思って良い
    t = threading.Thread(target=job)
    t.start()
    #running_threads.append(t)

# M3.スレッドの終了を合わせます
#COORD.join(running_threads)