#PPOについて
PPOは深層強化学習のアルゴリズムの一つで、その中でも方策ベースのものです。方策ベースは環境を与えられた時に行動確率を出力する方策関数を直接最適化するようなアルゴリズムのことです。方策ベースのアルゴリズムには、他にA3CやTRPOなどがあります。方策ベース以外のアルゴリズムは、DQNなどに代表される価値ベースのアルゴリズムなどがあります。
##他の手法との比較
まずは他の手法についての説明を行います。
###A3Cについて
A3Cに用いられている代表的な技術として、Actor-Critic, Advantage, Asynchronousという三つの手法があります。
####Actor-Critic
Actor-Criticはネットワークの構造に関する特徴です。A3Cでは方策の目的関数として
L_{policy}=A(t)log\pi_{\theta}(a_{t}|s_{t})
を用いています。このうちA(t)がアドバンテージ関数と呼ばれるもので
A(t)=(R(t)-V(s_{t}))
で表されます。このA(t)を求めるのに価値関数V(s)を用いるため、方策(行動確率分布)と同時に状態価値がネットワークの出力となるようにモデルを構成する手法です。これによってより早く学習が進みます。
####Advantage
通常状態価値関数$V(t)$の更新には次の誤差を用います
loss=r(s_{t})+\gamma V(s_{t+1})-V(s_{t})
を満たすように$V(s)$を学習していきます。しかしこの方法では、1エピソードのステップ数が多い場合などは学習の影響が早いステップまで伝播するまでに必要な学習の回数が多くなり学習が遅くなります。そこでAdvantageでは次の誤差を用います。
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の目的関数は方策、状態価値、正則化のためのエントロピーの三つを用いて
\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}
と表され、最終的にはこれらを合わせた形で次式のようになります
L_{all} = -L_{policy}+C_{value}L_{value}-C_{entropy}L_{entropy}
$C_{value}$と$C_{entropy}$は定数を表しています。これを最小化することで学習を行います。
##PPOについて
A3Cでは方策勾配が
\Delta L_{policy} = \Delta log\pi_{\theta}(a_{t}|s_{t})A(t)
として表され、式の中に$log$が入っているため更新の際に、とても大きくなってしまいます。そこでPPOでは、更新に制約をかけることで、更新しすぎてしまうことを防ぎます。また目的関数もA3Cとは大きく異なり、
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関数は
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$の間に収まります。この関数を用いて目的関数は
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ファイルで完結】
##処理の概観
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
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
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
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
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)
##全体のコード
コード全体は以下のようになります。
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の実装か、他の手法についてまとめてみたいと思います。