PPO
1. 概念
PPO:Policy Gradient不好确定Learning rate(step size)的问题,如果因为step size过大,学出来的Policy会一直乱动,不会收敛,但如果step size太小,对于完成训练,会很长时间,因此PPO利用了New Policy和Old Policy的比例,限制了New Policy的更新幅度,让Policy Gradient对稍微大的step size不那么敏感。
PPO是基于Actor Critic 的算法。
2.原理
从On-policy----->off-policy改进
公式说明:
本来是从p做sample,换成从q做sample。也就是说可以把p换成任何的q,虽然p和q的期望无偏,但是他们的方差有偏,现在我们可以改变它的奖励值,不用θ去跟环境做互动,假设有另外一个policy,另外一个policy的参数θ‘去跟环境做互动。我们要训练的是θ,而θ’是负责跟环境做互动的,实际上的τ是从θ‘(τ)sample出来的
目标:使用θ’采样的数据,训练θ这个actor,过程中θ’是fixed的所以可以重复使用用θ’的数据训练θ许多次,增加数据利用率,提高训练速度,为openAI的default方法。显然,此方法转为了off-policy。
PPO公式:
约束:由于θ’与θ不能差别太大,所以需要有个约束,比如在目标函数后加-βKL(θ,θ’)作为约束(PPO);或者外面引入KL(θ,θ’) < δ.(TRPO,PPO前身,用constraint计算上不好处理) 说明:因为可能参数小的变化,行为变化很大,衡量无意义。所以。KL(θ,θ’)不是参数上的距离,而是行为上的距离!π(a|s)的分布接近!
tip:adaptive KL Penalty,权衡β。定义KLmin和KLmax,当KL > KLmax,说明penalty作用没发挥,则增加β的值,反之亦然。
3.代码
3.1伪代码
1、将环境信息s输入到actor-new网络, 得到两个值, 一个是mu, 一个是sigma,
然后将这两个值分别当作正态分布的均值和方差构建正态分布(意义是表示action的分布),然后通过这个正态分布sample出来一个action,
再输入到环境中得到奖励r和下一步的状态s_,然后存储[(s,a,r),…], 再将s_输入到actor-new网络,循环步骤1,
直到存储了一定量的[(s, a, r), …], 注意这个过程中actor-new网络没有更新。
2、将1循环完最后一步得到的s_输入到critic-NN网络中, 得到状态的v_值, 然后计算折扣奖励:
R[t] = r[t] + γ*r[t+1] + γ2 * r[t+1] + … + γT-t+1 * r[T-1] + γT-t *
v_, 得到R = [R[0], R[1],…,R[t],…,R[T]],其中T为最后一个时间步。
3、将存储的所有s组合输入到critic-NN网络中, 得到所有状态的V_值, 计算At = R – V_ (论文公式10,
openai官网代码则实现的是论文公式11和12)
4、求c_loss = mean(square(At )), 然后反向传播更新critic-NN网络。
5、将存储的所有s组合输入actor-old和actor-new网络(网络结构一样), 分别得到正态分布Normal1和Normal2,
将存储的所有action组合为actions输入到正态分布Normal1和Normal2,
得到每个actions对应的prob1和prob2, 然后用prob2除以prob1得到important weight,
也就是ratio。
6、根据论文公式7计算a_loss = mean(min((ration* At, clip(ratio, 1-ξ, 1+ξ)*
At))), 然后反向传播, 更新actor-new网络。
7、循环5-6步骤, 一定步后, 循环结束, 用actor-new网络权重来更新actor-old网络(莫凡代码是在循环开始前更新的,
效果是一样的)。
8、循环1-7步骤。
3.2代码解析
源代码如下:
import argparse
import os
import time
import gym
import matplotlib.pyplot as plt
import numpy as np
import tensorflow as tf
import tensorlayer as tl
parser = argparse.ArgumentParser(description='Train or test neural net motor controller.')
parser.add_argument('--train', dest='train', action='store_true', default=True)
parser.add_argument('--test', dest='test', action='store_false')
args = parser.parse_args()
##################### hyper parameters ####################
ENV_NAME = 'Pendulum-v0' # environment name
RANDOMSEED = 1 # random seed
LR_A = 0.001 # learning rate for actor
LR_C = 0.002 # learning rate for critic
GAMMA = 0.9 # reward discount
TAU = 0.01 # soft replacement
MEMORY_CAPACITY = 10000 # size of replay buffer
BATCH_SIZE = 32 # update batchsize
MAX_EPISODES = 200 # total number of episodes for training
MAX_EP_STEPS = 200 # total number of steps for each episode
TEST_PER_EPISODES = 10 # test the model per episodes
VAR = 3 # control exploration
############################### DDPG ####################################
class DDPG(object):
"""
DDPG class
"""
def __init__(self, a_dim, s_dim, a_bound):
# memory用于储存跑的数据的数组:
# 保存个数MEMORY_CAPACITY,s_dim * 2 + a_dim + 1:分别是两个state,一个action,和一个reward
self.memory = np.zeros((MEMORY_CAPACITY, s_dim * 2 + a_dim + 1), dtype=np.float32)
self.pointer = 0
self.a_dim, self.s_dim, self.a_bound = a_dim, s_dim, a_bound
W_init = tf.random_normal_initializer(mean=0, stddev=0.3)
b_init = tf.constant_initializer(0.1)
# 建立actor网络,输入s,输出a
def get_actor(input_state_shape, name=''):
"""
Build actor network
:param input_state_shape: state
:param name: name
:return: act
"""
inputs = tl.layers.Input(input_state_shape, name='A_input')
x = tl.layers.Dense(n_units=30, act=tf.nn.relu, W_init=W_init, b_init=b_init, name='A_l1')(inputs)
x = tl.layers.Dense(n_units=a_dim, act=tf.nn.tanh, W_init=W_init, b_init=b_init, name='A_a')(x)
x = tl.layers.Lambda(lambda x: np.array(a_bound) * x)(x) # 注意这里,先用tanh把范围限定在[-1,1]之间,再进行映射
return tl.models.Model(inputs=inputs, outputs=x, name='Actor' + name)
# 建立Critic网络,输入s,a。输出Q值
def get_critic(input_state_shape, input_action_shape, name=''):
"""
Build critic network
:param input_state_shape: state
:param input_action_shape: act
:param name: name
:return: Q value Q(s,a)
"""
s = tl.layers.Input(input_state_shape, name='C_s_input')
a = tl.layers.Input(input_action_shape, name='C_a_input')
x = tl.layers.Concat(1)([s, a])
x = tl.layers.Dense(n_units=60, act=tf.nn.relu, W_init=W_init, b_init=b_init, name='C_l1')(x)
x = tl.layers.Dense(n_units=1, W_init=W_init, b_init=b_init, name='C_out')(x)
return tl.models.Model(inputs=[s, a], outputs=x, name='Critic' + name)
self.actor = get_actor([None, s_dim])
self.critic = get_critic([None, s_dim], [None, a_dim])
self.actor.train()
self.critic.train()
# 更新参数,只用于首次赋值,之后就没用了
def copy_para(from_model, to_model):
"""
Copy parameters for soft updating
:param from_model: latest model
:param to_model: target model
:return: None
"""
for i, j in zip(from_model.trainable_weights, to_model.trainable_weights):
j.assign(i)
# 建立actor_target网络,并和actor参数一致,不能训练
self.actor_target = get_actor([None, s_dim], name='_target')
copy_para(self.actor, self.actor_target)
self.actor_target.eval()
# 建立critic_target网络,并和actor参数一致,不能训练
self.critic_target = get_critic([None, s_dim], [None, a_dim], name='_target')
copy_para(self.critic, self.critic_target)
self.critic_target.eval()
self.R = tl.layers.Input([None, 1], tf.float32, 'r')
# 建立ema,滑动平均值
self.ema = tf.train.ExponentialMovingAverage(decay=1 - TAU) # soft replacement
self.actor_opt = tf.optimizers.Adam(LR_A)
self.critic_opt = tf.optimizers.Adam(LR_C)
def ema_update(self):
"""
滑动平均更新
"""
# 其实和之前的硬更新类似,不过在更新赋值之前,用一个ema.average。
paras = self.actor.trainable_weights + self.critic.trainable_weights # 获取要更新的参数包括actor和critic的
self.ema.apply(paras) # 主要是建立影子参数
for i, j in zip(self.actor_target.trainable_weights + self.critic_target.trainable_weights, paras):
i.assign(self.ema.average(j)) # 用滑动平均赋值
# 选择动作,把s带进入,输出a
def choose_action(self, s):
"""
Choose action
:param s: state
:return: act
"""
return self.actor(np.array([s], dtype=np.float32))[0]
def learn(self):
"""
Update parameters
:return: None
"""
indices = np.random.choice(MEMORY_CAPACITY, size=BATCH_SIZE) # 随机BATCH_SIZE个随机数
bt = self.memory[indices, :] # 根据indices,选取数据bt,相当于随机
bs = bt[:, :self.s_dim] # 从bt获得数据s
ba = bt[:, self.s_dim:self.s_dim + self.a_dim] # 从bt获得数据a
br = bt[:, -self.s_dim - 1:-self.s_dim] # 从bt获得数据r
bs_ = bt[:, -self.s_dim:] # 从bt获得数据s'
# Critic:
# Critic更新和DQN很像,不过target不是argmax了,是用critic_target计算出来的。
# br + GAMMA * q_
with tf.GradientTape() as tape:
a_ = self.actor_target(bs_)
q_ = self.critic_target([bs_, a_])
y = br + GAMMA * q_
q = self.critic([bs, ba])
td_error = tf.losses.mean_squared_error(y, q)
c_grads = tape.gradient(td_error, self.critic.trainable_weights)
self.critic_opt.apply_gradients(zip(c_grads, self.critic.trainable_weights))
# Actor:
# Actor的目标就是获取最多Q值的。
with tf.GradientTape() as tape:
a = self.actor(bs)
q = self.critic([bs, a])
a_loss = -tf.reduce_mean(q) # 【敲黑板】:注意这里用负号,是梯度上升!也就是离目标会越来越远的,就是越来越大。
a_grads = tape.gradient(a_loss, self.actor.trainable_weights)
self.actor_opt.apply_gradients(zip(a_grads, self.actor.trainable_weights))
self.ema_update()
# 保存s,a,r,s_
def store_transition(self, s, a, r, s_):
"""
Store data in data buffer
:param s: state
:param a: act
:param r: reward
:param s_: next state
:return: None
"""
# 整理s,s_,方便直接输入网络计算
s = s.astype(np.float32)
s_ = s_.astype(np.float32)
# 把s, a, [r], s_横向堆叠
transition = np.hstack((s, a, [r], s_))
# pointer是记录了曾经有多少数据进来。
# index是记录当前最新进来的数据位置。
# 所以是一个循环,当MEMORY_CAPACITY满了以后,index就重新在最底开始了
index = self.pointer % MEMORY_CAPACITY # replace the old memory with new memory
# 把transition,也就是s, a, [r], s_存进去。
self.memory[index, :] = transition
self.pointer += 1
def save_ckpt(self):
"""
save trained weights
:return: None
"""
if not os.path.exists('model'):
os.makedirs('model')
tl.files.save_weights_to_hdf5('model/ddpg_actor.hdf5', self.actor)
tl.files.save_weights_to_hdf5('model/ddpg_actor_target.hdf5', self.actor_target)
tl.files.save_weights_to_hdf5('model/ddpg_critic.hdf5', self.critic)
tl.files.save_weights_to_hdf5('model/ddpg_critic_target.hdf5', self.critic_target)
def load_ckpt(self):
"""
load trained weights
:return: None
"""
tl.files.load_hdf5_to_weights_in_order('model/ddpg_actor.hdf5', self.actor)
tl.files.load_hdf5_to_weights_in_order('model/ddpg_actor_target.hdf5', self.actor_target)
tl.files.load_hdf5_to_weights_in_order('model/ddpg_critic.hdf5', self.critic)
tl.files.load_hdf5_to_weights_in_order('model/ddpg_critic_target.hdf5', self.critic_target)
if __name__ == '__main__':
# 初始化环境
env = gym.make(ENV_NAME)
env = env.unwrapped
# reproducible,设置随机种子,为了能够重现
env.seed(RANDOMSEED)
np.random.seed(RANDOMSEED)
tf.random.set_seed(RANDOMSEED)
# 定义状态空间,动作空间,动作幅度范围
s_dim = env.observation_space.shape[0]
a_dim = env.action_space.shape[0]
a_bound = env.action_space.high
print('s_dim', s_dim)
print('a_dim', a_dim)
# 用DDPG算法
ddpg = DDPG(a_dim, s_dim, a_bound)
# 训练部分:
if args.train: # train
reward_buffer = [] # 用于记录每个EP的reward,统计变化
t0 = time.time() # 统计时间
for i in range(MAX_EPISODES):
t1 = time.time()
s = env.reset()
ep_reward = 0 # 记录当前EP的reward
for j in range(MAX_EP_STEPS):
# Add exploration noise
a = ddpg.choose_action(s) # 这里很简单,直接用actor估算出a动作
# 为了能保持开发,这里用了另外一种方式增加探索。
# 因此需要需要以a为均值,VAR为标准差,建立正态分布,再从正态分布采样出a
# 因为a是均值,所以a的概率是最大的。但a相对其他概率由多大,是靠VAR调整。这里我们其实可以增加更新VAR,动态调整a的确定性
# 然后进行裁剪
a = np.clip(np.random.normal(a, VAR), -2, 2)
# 与环境进行互动
s_, r, done, info = env.step(a)
# 保存s,a,r,s_
ddpg.store_transition(s, a, r / 10, s_)
# 第一次数据满了,就可以开始学习
if ddpg.pointer > MEMORY_CAPACITY:
ddpg.learn()
# 输出数据记录
s = s_
ep_reward += r # 记录当前EP的总reward
if j == MAX_EP_STEPS - 1:
print(
'\rEpisode: {}/{} | Episode Reward: {:.4f} | Running Time: {:.4f}'.format(
i, MAX_EPISODES, ep_reward,
time.time() - t1
), end=''
)
plt.show()
# test
if i and not i % TEST_PER_EPISODES:
t1 = time.time()
s = env.reset()
ep_reward = 0
for j in range(MAX_EP_STEPS):
a = ddpg.choose_action(s) # 注意,在测试的时候,我们就不需要用正态分布了,直接一个a就可以了。
s_, r, done, info = env.step(a)
s = s_
ep_reward += r
if j == MAX_EP_STEPS - 1:
print(
'\rEpisode: {}/{} | Episode Reward: {:.4f} | Running Time: {:.4f}'.format(
i, MAX_EPISODES, ep_reward,
time.time() - t1
)
)
reward_buffer.append(ep_reward)
if reward_buffer:
plt.ion()
plt.cla()
plt.title('DDPG')
plt.plot(np.array(range(len(reward_buffer))) * TEST_PER_EPISODES, reward_buffer) # plot the episode vt
plt.xlabel('episode steps')
plt.ylabel('normalized state-action value')
plt.ylim(-2000, 0)
plt.show()
plt.pause(0.1)
plt.ioff()
plt.show()
print('\nRunning time: ', time.time() - t0)
ddpg.save_ckpt()
# test
ddpg.load_ckpt()
while True:
s = env.reset()
for i in range(MAX_EP_STEPS):
env.render()
s, r, done, info = env.step(ddpg.choose_action(s))
if done:
break