PythonでPPOを実装してみた

#PPOについて
　PPOは深層強化学習のアルゴリズムの一つで、その中でも方策ベースのものです。方策ベースは環境を与えられた時に行動確率を出力する方策関数を直接最適化するようなアルゴリズムのことです。方策ベースのアルゴリズムには、他にA3CやTRPOなどがあります。方策ベース以外のアルゴリズムは、DQNなどに代表される価値ベースのアルゴリズムなどがあります。

##他の手法との比較
　まずは他の手法についての説明を行います。

###A3Cについて
　A3Cに用いられている代表的な技術として、Actor-Critic, Advantage, Asynchronousという三つの手法があります。

####Actor-Critic
　Actor-Criticはネットワークの構造に関する特徴です。A3Cでは方策の目的関数として



```math
L_{policy}=A(t)log\pi_{\theta}(a_{t}|s_{t})
```



　を用いています。このうちA(t)がアドバンテージ関数と呼ばれるもので



```math
A(t)=(R(t)-V(s_{t}))
```
　で表されます。このA(t)を求めるのに価値関数V(s)を用いるため、方策（行動確率分布）と同時に状態価値がネットワークの出力となるようにモデルを構成する手法です。これによってより早く学習が進みます。

####Advantage
 通常状態価値関数$V(t)$の更新には次の誤差を用います




```math
loss=r(s_{t})+\gamma V(s_{t+1})-V(s_{t})
```
　を満たすように$V(s)$を学習していきます。しかしこの方法では、1エピソードのステップ数が多い場合などは学習の影響が早いステップまで伝播するまでに必要な学習の回数が多くなり学習が遅くなります。そこでAdvantageでは次の誤差を用います。



```math
loss=\sum_{k=1}^n \gamma^{k-1} r(s_{t+k})+\gamma^n V(s_{t+2})-V(s_{t})
```
　この式の$n$を調節することで、より先の学習の影響が早く伝播しますが、あまり大きくしすぎると逆に学習速度が遅くなります。例えば、CartPoleではそれほど効果が無く場合によってはAdvantageを用いない方が早いこともあります。
####Asynchronous
　Asynchronousは学習方法に関する手法で、通常一つのエージェントで探索を行うと学習の方向に偏りが出やすいので、その対策として一つの共有のニューラルネットワークと複数のエージェントを用いて探索を行いそれぞれ一定ステップ数経つか、1エピソードが終わった際に各エージェントが目的関数のパラメータに対する勾配を求めて共有ネットワークのパラメータを更新します。

　それぞれのエージェントもニューラルネットワークを持っており、エピソードが始まる前に共有ネットワークからパラメータをコピーして探索を行います。これによって、DQNでいうreplay bufferと同じように学習の偏りを防ぐことができます。

####A3Cの目的関数
　A3Cの目的関数は方策、状態価値、正則化のためのエントロピーの三つを用いて


```math
\begin{align}
L_{policy} &= A(t)log\pi_{\theta}(a_{t}|s_{t})\\
L_{value} &= (R(t)-V(s_{t}))^2\\
L_{entropy} &= \pi_{\theta}(a_{t}|s_{t})log\pi_{\theta}(a_{t}|s_{t})
\end{align}
```
　と表され、最終的にはこれらを合わせた形で次式のようになります

```math
L_{all} = -L_{policy}+C_{value}L_{value}-C_{entropy}L_{entropy}
```
 $C_{value}$と$C_{entropy}$は定数を表しています。これを最小化することで学習を行います。

##PPOについて
 A3Cでは方策勾配が



```math
\Delta L_{policy} = \Delta log\pi_{\theta}(a_{t}|s_{t})A(t)
```
 
 として表され、式の中に$log$が入っているため更新の際に、とても大きくなってしまいます。そこでPPOでは、更新に制約をかけることで、更新しすぎてしまうことを防ぎます。また目的関数もA3Cとは大きく異なり、

```math
r_{t}(\theta)=\frac{\pi_{\theta_{new}}(a_{t}|s_{t})}{\pi_{\theta_{old}}(a_{t}|s_{t})}\\
L^{CPI}=\mathbb E \big[\,r_{t}(\theta)A(t)\, \big]
```

　これを代理の目的関数として、更新する際にclip関数を用いて方策の目的関数とします。clip関数は

```math
clip(x,a,b)=\left\{
\begin{array}{ll}
b & (x > b) \\
x & (a \leq x \leq b) \\
a & (x < a)
\end{array}
\right.
```

　このように表され、$x$がどのように変化しても$a$と$b$の間に収まります。この関数を用いて目的関数は

```math
L_{policy}=min \big(\, r_{t}(\theta)A(t),clip(r_{t}(\theta),1-\epsilon,1+\epsilon)\, \big)
```
　と表されます。状態価値関数に関しては、PPOとほとんど変わりません。

　またPPOでもA3Cと同じようにAdvantageを用いて学習を行います。


#実装
　実装の際には以下のサイトを参考にしました
[【強化学習】実装しながら学ぶPPO【CartPoleで棒立て：1ファイルで完結】](https://qiita.com/sugulu/items/8925d170f030878d6582#brain)

##処理の概観
main():スレッドを作り処理を行います

Worker(thread_type, thread_name, ppo_brain)
-run_thread():スレッドタイプによって処理を分けます
-env_run():環境内でエージェントに探索させます。

ppo_agent(ppo_brain)
-action(state):状態を受け取って行動を出力
-greedy_action(state):$\epsilon-greedy$法を使って行動を出力
-update(memory):探索際に保存したデータをアドバンテージを考慮して学習
ppo_brain()
-build_graph:ここでグラフの形を定義します
-update:更新します

##main
```python
def main(args):
    #スレッドを作る処理
    with tf.device("/cpu:0"):
        brain = ppo_brain()
        thread=[]
        for i in range(WORKER_NUM):
            thread_name = "local_thread"+str(i)
            thread.append(Worker(thread_type = "train",thread_name = thread_name,brain = brain))
    
    COORD = tf.train.Coordinator()
    SESS.run(tf.global_variables_initializer())
    saver = tf.train.Saver()
    
　　#前回の学習の処理をロード、基本的にモデルを定義した後にこの処理を行う
    if args.load:
        ckpt = tf.train.get_checkpoint_state(MODEL_DIR)
        if ckpt:
            saver.restore(SESS,MODEL_SAVE_PATH)

    runnning_thread=[]
    for worker in thread:
        job = lambda: worker.run_thread()
        t = threading.Thread(target=job)
        t.start()
        runnning_thread.append(t)
    COORD.join(runnning_thread)
　　　
    #学習が終わった際にtestを行う
    test = Worker(thread_type = "test",thread_name = "test_thread",brain=brain)
    test.run_thread()

    if args.save:
        saver.save(SESS,MODEL_SAVE_PATH)
        print("saved")
```

##Worker

```python
class Worker:
    def __init__(self,thread_type,thread_name,brain):
        self.thread_type = thread_type
        self.name = thread_name
        self.agent = ppo_agent(brain)
        self.env = gym.make(ENV_NAME)
        #testの時には動画を保存する
        if self.thread_type == "test" and args.video:
            self.env = wrappers.Monitor(self.env, VIDEO_DIR, force = True)
        self.leaning_memory = np.zeros(10)
        self.memory = []
        self.total_trial = 0

    def run_thread(self):
        while True:
            if self.thread_type == "train" and not isLearned:
                self.env_run()
            elif self.thread_type == "train" and isLearned:
                sleep(3)
                break
            elif self.thread_type == "test" and not isLearned:
                sleep(3)
            elif self.thread_type == "test" and isLearned:
                self.env_run()
                break

    def env_run(self):
        global isLearned
        global frame
        self.total_trial += 1

        step = 0
        observation = self.env.reset()

        while True:
            step += 1
            frame += 1
　　　　　　　　
　　　　　　　#行動の選択
            if self.thread_type == "train":
                action=self.agent.greedy_action(observation)
            elif self.thread_type == "test":
                self.env.render()
                sleep(0.01)
                action=self.agent.action(observation)
            
            next_observation,_,done,_ = self.env.step(action)

            reward = 0

            if done:
                if step >= 199:
                    reward = 1 #成功時
                else:
                    reward =- 1　#失敗時
            else:
                #終わっていない時
                reward+=0
            
　　　　　　　#結果をメモリに保存
            self.memory.append([observation,action,reward,done,next_observation])

            observation = next_observation

            if done:
                break

　　　　　#10回の平均スコアを算出
        self.leaning_memory = np.hstack((self.leaning_memory[1:],step))
        print("Thread:",self.name," Thread_trials:",self.total_trial," score:",step," mean_score:",self.leaning_memory.mean()," total_step:",frame)

　　　　　#学習終了時
        if self.leaning_memory.mean() >= 199:
            isLearned = True
            sleep(3)
        else:
            #パラメータの更新
            self.agent.update(self.memory)
            self.memory = []
        

```

##ppo_agent

```python
class ppo_agent:
    def __init__(self,brain):
        self.brain=brain
        self.memory=[]

    #ランダムな要素なしで行動を行う
    def action(self,state):
        prob,v = self.brain.predict(state)
        return np.random.choice(ACTION_LIST,p = prob)

    #一定確率でランダムに行動を行う
    def greedy_action(self,state):
        if frame >= EPS_STEPS:   
            eps = EPS_END
        else:
            eps = EPS_START + frame* (EPS_END - EPS_START) / EPS_STEPS  

        if np.random.random() <= eps:
            return np.random.choice(ACTION_LIST)
        else:
            return self.action(state)

    #探索結果を加工してppo_brainクラスに送る
    def update(self,memory):
        R = sum([memory[j][2] * (GAMMA ** j) for j in range(ADVANTAGE + 1)])
        self.memory.append([memory[0][0], memory[0][1], R,memory[0][3], memory[0][4], GAMMA ** ADVANTAGE])

        #アドバンテージを考慮
        for i in range(1, len(memory) - ADVANTAGE):
            R = ((R - memory[i-1][2]) / GAMMA) + memory[i + ADVANTAGE][2] * (GAMMA ** (ADVANTAGE - 1))
            self.memory.append([memory[i][0], memory[i][1], R,memory[i + ADVANTAGE][3], memory[i][4],GAMMA ** ADVANTAGE])
            
        for i in range(ADVANTAGE - 1):
            R = ((R - memory[len(memory) - ADVANTAGE + i][2]) / GAMMA)
            self.memory.append([memory[i][0], memory[i][1], R, True, memory[i][4], GAMMA ** (ADVANTAGE - i)])

        #ppo_brainクラスにデータを送って更新
        self.brain.update(self.memory)

        self.memory = []
```

##ppo_brain

```python
class ppo_brain:
    def __init__(self):
        self.build_model()
        self.name="brain"
        self.prob_old=1.0

    def build_model(self):
        self.input=tf.placeholder(dtype=tf.float32,shape=[None,STATE_NUM])
       
        #ここでは古いパラメータのモデルと新しいパラメータのモデルを定義し同じ入力に対して行動確率と状態価値を出力する
        #新しいネットワーク
        with tf.variable_scope("current_brain"):
            hidden1=tf.layers.dense(self.input,HIDDEN_LAYERE,activation=tf.nn.leaky_relu)
            self.prob=tf.layers.dense(hidden1,ACTION_NUM,activation=tf.nn.softmax)
            self.v=tf.layers.dense(hidden1,1)
        #古いネットワーク
        with tf.variable_scope("old_brain"):
            old_hiddend1=tf.layers.dense(self.input,HIDDEN_LAYERE,activation=tf.nn.leaky_relu)
            self.old_prob=tf.layers.dense(hidden1,ACTION_NUM,activation=tf.nn.softmax)
            self.old_v=tf.layers.dense(hidden1,1)

        self.reward=tf.placeholder(dtype=tf.float32,shape=(None,1))
        self.action=tf.placeholder(dtype=tf.float32,shape=(None,ACTION_NUM))

###########ここ以下は損失関数の定義部分############
        #アドバンテージ関数の定義
        advantage = self.reward-self.v

        #方策の損失関数の定義部分
        r_theta = tf.div(self.prob + 1e-10, tf.stop_gradient(self.old_prob) + 1e-10)
        action_theta = tf.reduce_sum(tf.multiply(r_theta, self.action), axis = 1, keepdims = True)
        #rのclipを計算
        r_clip = tf.clip_by_value(action_theta, 1 - EPSIRON, 1 + EPSIRON)
        #方策の目的関数としてアドバンテージ関数を用いる時には勾配を考えないのでstop_gradientを用いる
        advantage_cpi = tf.multiply(action_theta, tf.stop_gradient(advantage))
        advantage_clip = tf.multiply(r_clip , tf.stop_gradient(advantage))
        self.policy_loss = tf.minimum(advantage_clip , advantage_cpi)

        #状態価値の損失関数
        self.value_loss = tf.square(advantage)

        #エントロピーの定義
        self.entropy = tf.reduce_sum(self.prob*tf.log(self.prob+1e-10),axis = 1,keepdims = True)

        #最終的な損失関数の定義
        self.loss = tf.reduce_sum(-self.policy_loss + LOSS_V * self.value_loss - LOSS_ENTROPY * self.entropy)

##############ここ以下は更新の際に必要となる動作を定義##############

        #パラメータの更新（Adamを用いて最小化）
        self.opt = tf.train.AdamOptimizer(learning_rate = LEARNING_RATE)
        self.minimize = self.opt.minimize(self.loss)

        #新しいパラメータと古いパラメータをそれぞれのネットワークから取得
        self.weight_param = tf.get_collection(key = tf.GraphKeys.TRAINABLE_VARIABLES, scope = "current_brain")
        self.old_weight_param = tf.get_collection(key = tf.GraphKeys.TRAINABLE_VARIABLES, scope = "old_brain")

        #古いネットワークのパラメータに新しいネットワークのパラメータを代入
        self.insert = [g_p.assign(l_p) for l_p,g_p in zip(self.weight_param,self.old_weight_param)]

    #状態から行動確率と状態価値を出力
    def predict(self,state):
        state=np.array(state).reshape(-1,STATE_NUM)
        feed_dict={self.input:state}
        p,v=SESS.run([self.prob,self.v],feed_dict)
        return p.reshape(-1),v.reshape(-1)
    
    #データを入力する前の前処理を行いバッチを作成する
    #更新する
    def update(self,memory):
        length=len(memory)
       
        s_=np.array([memory[j][0] for j in range(length)]).reshape(-1,STATE_NUM)
        a_=np.eye(ACTION_NUM)[[memory[j][1] for j in range(length)]].reshape(-1,ACTION_NUM)
        R_=np.array([memory[j][2] for j in range(length)]).reshape(-1,1)
        d_=np.array([memory[j][3] for j in range(length)]).reshape(-1,1)
        s_mask=np.array([memory[j][5] for j in range(length)]).reshape(-1,1)
        _s=np.array([memory[j][4] for j in range(length)]).reshape(-1,STATE_NUM)

        #後の状態価値を推論
        _, v=self.predict(_s)
        #アドバンテージを考慮した報酬を算出
        R=(np.where(d_,0,1)*v.reshape(-1,1))*s_mask+R_
        #パラメータの更新
        feed_dict={self.input:s_, self.action:a_, self.reward:R}

        SESS.run(self.minimize,feed_dict)
        #ネットワークの更新
        SESS.run(self.insert)

```


##全体のコード
　コード全体は以下のようになります。

```python
import argparse
import tensorflow as tf
import numpy as np
import random
import threading
import gym
from time import sleep
from gym import wrappers
from os import path

parser=argparse.ArgumentParser(description="Reiforcement training with PPO",add_help=True)
parser.add_argument("--model",type=str,required=True,help="model base name. required")
parser.add_argument("--env_name",default="CartPole-v0",help="environment name. default is CartPole-v0")
parser.add_argument("--save",action="store_true",default=False,help="save command")
parser.add_argument("--load",action="store_true",default=False,help="load command")
parser.add_argument("--thread_num",type=int,default=5)
parser.add_argument("--video",action="store_true",default=False, help="write this if you want to save as video")
args=parser.parse_args()


ENV_NAME=args.env_name
WORKER_NUM=args.thread_num
#define constants
VIDEO_DIR="./train_info/video"
MODEL_DIR="./train_info/models"
MODEL_SAVE_PATH=path.join(MODEL_DIR,args.model)

ADVANTAGE=2
STATE_NUM=4
ACTION_LIST=[0,1]
ACTION_NUM=2
#epsiron parameter
EPS_START = 0.5
EPS_END = 0.1
EPS_STEPS = 200 * WORKER_NUM**2
#learning parameter
GAMMA=0.99
LEARNING_RATE=0.002
#loss constants
LOSS_V=0.5
LOSS_ENTROPY=0.02
HIDDEN_LAYERE=30

EPSIRON = 0.2

class ppo_brain:
    def __init__(self):
        self.build_model()
        self.name="brain"
        self.prob_old=1.0

    def build_model(self):
        self.input=tf.placeholder(dtype=tf.float32,shape=[None,STATE_NUM])
       
        #ここでは古いパラメータのモデルと新しいパラメータのモデルを定義し同じ入力に対して行動確率と状態価値を出力する
        with tf.variable_scope("current_brain"):
            hidden1=tf.layers.dense(self.input,HIDDEN_LAYERE,activation=tf.nn.leaky_relu)
            self.prob=tf.layers.dense(hidden1,ACTION_NUM,activation=tf.nn.softmax)
            self.v=tf.layers.dense(hidden1,1)
        with tf.variable_scope("old_brain"):
            old_hiddend1=tf.layers.dense(self.input,HIDDEN_LAYERE,activation=tf.nn.leaky_relu)
            self.old_prob=tf.layers.dense(hidden1,ACTION_NUM,activation=tf.nn.softmax)
            self.old_v=tf.layers.dense(hidden1,1)

        self.reward=tf.placeholder(dtype=tf.float32,shape=(None,1))
        self.action=tf.placeholder(dtype=tf.float32,shape=(None,ACTION_NUM))

###########ここ以下は損失関数の定義部分############
        #アドバンテージ関数の定義
        advantage = self.reward-self.v

        #方策の損失関数の定義部分
        r_theta = tf.div(self.prob + 1e-10, tf.stop_gradient(self.old_prob) + 1e-10)
        action_theta = tf.reduce_sum(tf.multiply(r_theta, self.action), axis = 1, keepdims = True)
        r_clip = tf.clip_by_value(action_theta, 1 - EPSIRON, 1 + EPSIRON)
        advantage_cpi = tf.multiply(action_theta, tf.stop_gradient(advantage))
        advantage_clip = tf.multiply(r_clip , tf.stop_gradient(advantage))
        self.policy_loss = tf.minimum(advantage_clip , advantage_cpi)

        #状態価値の損失関数
        self.value_loss = tf.square(advantage)

        #エントロピーの定義
        self.entropy = tf.reduce_sum(self.prob*tf.log(self.prob+1e-10),axis = 1,keepdims = True)

        #最終的な損失関数の定義
        self.loss = tf.reduce_sum(-self.policy_loss + LOSS_V * self.value_loss - LOSS_ENTROPY * self.entropy)

##############ここ以下は更新の際に必要となる動作を定義##############

        #パラメータの更新（Adamを用いて最小化）
        self.opt = tf.train.AdamOptimizer(learning_rate = LEARNING_RATE)
        self.minimize = self.opt.minimize(self.loss)

        #新しいパラメータと古いパラメータをそれぞれのネットワークから取得
        self.weight_param = tf.get_collection(key = tf.GraphKeys.TRAINABLE_VARIABLES, scope = "current_brain")
        self.old_weight_param = tf.get_collection(key = tf.GraphKeys.TRAINABLE_VARIABLES, scope = "old_brain")

        #古いネットワークのパラメータに新しいネットワークのパラメータを代入
        self.insert = [g_p.assign(l_p) for l_p,g_p in zip(self.weight_param,self.old_weight_param)]

    #状態から行動確率と状態価値を出力
    def predict(self,state):
        state=np.array(state).reshape(-1,STATE_NUM)
        feed_dict={self.input:state}
        p,v=SESS.run([self.prob,self.v],feed_dict)
        return p.reshape(-1),v.reshape(-1)
    
    #データを入力する前の前処理を行いバッチを作成する
    #更新する
    def update(self,memory):
        length=len(memory)
       
        s_=np.array([memory[j][0] for j in range(length)]).reshape(-1,STATE_NUM)
        a_=np.eye(ACTION_NUM)[[memory[j][1] for j in range(length)]].reshape(-1,ACTION_NUM)
        R_=np.array([memory[j][2] for j in range(length)]).reshape(-1,1)
        d_=np.array([memory[j][3] for j in range(length)]).reshape(-1,1)
        s_mask=np.array([memory[j][5] for j in range(length)]).reshape(-1,1)
        _s=np.array([memory[j][4] for j in range(length)]).reshape(-1,STATE_NUM)

        #後の状態価値を推論
        _, v=self.predict(_s)
        #アドバンテージを考慮した報酬を算出
        R=(np.where(d_,0,1)*v.reshape(-1,1))*s_mask+R_
        #パラメータの更新
        feed_dict={self.input:s_, self.action:a_, self.reward:R}

        SESS.run(self.minimize,feed_dict)
        #ネットワークの更新
        SESS.run(self.insert)


class ppo_agent:
    def __init__(self,brain):
        self.brain=brain
        self.memory=[]

    #ランダムな要素なしで行動を行う
    def action(self,state):
        prob,v = self.brain.predict(state)
        return np.random.choice(ACTION_LIST,p = prob)

    #一定確率でランダムに行動を行う
    def greedy_action(self,state):
        if frame >= EPS_STEPS:   
            eps = EPS_END
        else:
            eps = EPS_START + frame* (EPS_END - EPS_START) / EPS_STEPS  

        if np.random.random() <= eps:
            return np.random.choice(ACTION_LIST)
        else:
            return self.action(state)

    #探索結果を加工してppo_brainクラスに送る
    def update(self,memory):
        R = sum([memory[j][2] * (GAMMA ** j) for j in range(ADVANTAGE + 1)])
        self.memory.append([memory[0][0], memory[0][1], R,memory[0][3], memory[0][4], GAMMA ** ADVANTAGE])

        #アドバンテージを考慮
        for i in range(1, len(memory) - ADVANTAGE):
            R = ((R - memory[i-1][2]) / GAMMA) + memory[i + ADVANTAGE][2] * (GAMMA ** (ADVANTAGE - 1))
            self.memory.append([memory[i][0], memory[i][1], R,memory[i + ADVANTAGE][3], memory[i][4],GAMMA ** ADVANTAGE])
            
        for i in range(ADVANTAGE - 1):
            R = ((R - memory[len(memory) - ADVANTAGE + i][2]) / GAMMA)
            self.memory.append([memory[i][0], memory[i][1], R, True, memory[i][4], GAMMA ** (ADVANTAGE - i)])

        #ppo_brainクラスにデータを送って更新
        self.brain.update(self.memory)

        self.memory = []


class Worker:
    def __init__(self,thread_type,thread_name,brain):
        self.thread_type = thread_type
        self.name = thread_name
        self.agent = ppo_agent(brain)
        self.env = gym.make(ENV_NAME)
        #testの時には動画を保存する
        if self.thread_type == "test" and args.video:
            self.env = wrappers.Monitor(self.env, VIDEO_DIR, force = True)
        self.leaning_memory = np.zeros(10)
        self.memory = []
        self.total_trial = 0

    def run_thread(self):
        while True:
            if self.thread_type == "train" and not isLearned:
                self.env_run()
            elif self.thread_type == "train" and isLearned:
                sleep(3)
                break
            elif self.thread_type == "test" and not isLearned:
                sleep(3)
            elif self.thread_type == "test" and isLearned:
                self.env_run()
                break

    def env_run(self):
        global isLearned
        global frame
        self.total_trial += 1

        step = 0
        observation = self.env.reset()

        while True:
            step += 1
            frame += 1

            if self.thread_type == "train":
                action=self.agent.greedy_action(observation)
            elif self.thread_type == "test":
                self.env.render()
                sleep(0.01)
                action=self.agent.action(observation)
            
            next_observation,_,done,_ = self.env.step(action)

            reward = 0

            if done:
                if step >= 199:
                    reward = 1 #成功時
                else:
                    reward =- 1　#失敗時
            else:
                #終わっていない時
                reward+=0
            
　　　　　　　#結果をメモリに保存
            self.memory.append([observation,action,reward,done,next_observation])

            observation = next_observation

            if done:
                break

　　　　　#10回の平均スコアを算出
        self.leaning_memory = np.hstack((self.leaning_memory[1:],step))
        print("Thread:",self.name," Thread_trials:",self.total_trial," score:",step," mean_score:",self.leaning_memory.mean()," total_step:",frame)

　　　　　#学習終了時
        if self.leaning_memory.mean() >= 199:
            isLearned = True
            sleep(3)
        else:
            #パラメータの更新
            self.agent.update(self.memory)
            self.memory = []

def main(args):
    #スレッドを作る処理
    with tf.device("/cpu:0"):
        brain = ppo_brain()
        thread=[]
        for i in range(WORKER_NUM):
            thread_name = "local_thread"+str(i)
            thread.append(Worker(thread_type = "train",thread_name = thread_name,brain = brain))
    
    COORD = tf.train.Coordinator()
    SESS.run(tf.global_variables_initializer())
    saver = tf.train.Saver()
    
　　#前回の学習の処理をロード、基本的にモデルを定義した後にこの処理を行う
    if args.load:
        ckpt = tf.train.get_checkpoint_state(MODEL_DIR)
        if ckpt:
            saver.restore(SESS,MODEL_SAVE_PATH)

    runnning_thread=[]
    for worker in thread:
        job = lambda: worker.run_thread()
        t = threading.Thread(target=job)
        t.start()
        runnning_thread.append(t)
    COORD.join(runnning_thread)
　　　
    #学習が終わった際にtestを行う
    test = Worker(thread_type = "test",thread_name = "test_thread",brain=brain)
    test.run_thread()

    if args.save:
        saver.save(SESS,MODEL_SAVE_PATH)
        print("saved")

if __name__=="__main__":
    SESS=tf.Session()
    frame=0
    isLearned=False
    
    main(args)

print("end")
```

#まとめ
　というわけで今回はPPOを実装しました。PPOの大きな特徴として、仕組みが簡単にも関わらず高い成績を出すというところがあるらしいです。TRPOについて、少し調べてみたのですが結構仕組みが難しそうだったので今回は細かい説明は省いています。次は連続行動空間におけるPPOの実装か、他の手法についてまとめてみたいと思います。