safe_rl/pg/agents.py (262 lines of code) (raw):
from copy import deepcopy
import numpy as np
from safe_rl.utils.mpi_tools import mpi_avg
from safe_rl.pg.utils import EPS
import safe_rl.pg.trust_region as tro
class Agent:
def __init__(self, **kwargs):
self.params = deepcopy(kwargs)
def set_logger(self, logger):
self.logger = logger
def prepare_update(self, training_package):
# training_package is a dict with everything we need (and more)
# to train.
self.training_package = training_package
def prepare_session(self, sess):
self.sess = sess
def update_pi(self, inputs):
raise NotImplementedError
def log(self):
pass
def ensure_satisfiable_penalty_use(self):
reward_penalized = self.params.get('reward_penalized', False)
objective_penalized = self.params.get('objective_penalized', False)
assert not(reward_penalized and objective_penalized), \
"Can only use either reward_penalized OR objective_penalized, " + \
"not both."
if not(reward_penalized or objective_penalized):
learn_penalty = self.params.get('learn_penalty', False)
assert not(learn_penalty), \
"If you are not using a penalty coefficient, you should " + \
"not try to learn one."
def ensure_satisfiable_optimization(self):
first_order = self.params.get('first_order', False)
trust_region = self.params.get('trust_region', False)
assert not(first_order and trust_region), \
"Can only use either first_order OR trust_region, " + \
"not both."
@property
def cares_about_cost(self):
return self.use_penalty or self.constrained
@property
def clipped_adv(self):
return self.params.get('clipped_adv', False)
@property
def constrained(self):
return self.params.get('constrained', False)
@property
def first_order(self):
self.ensure_satisfiable_optimization()
return self.params.get('first_order', False)
@property
def learn_penalty(self):
# Note: can only be true if "use_penalty" is also true.
self.ensure_satisfiable_penalty_use()
return self.params.get('learn_penalty', False)
@property
def penalty_param_loss(self):
return self.params.get('penalty_param_loss', False)
@property
def objective_penalized(self):
self.ensure_satisfiable_penalty_use()
return self.params.get('objective_penalized', False)
@property
def reward_penalized(self):
self.ensure_satisfiable_penalty_use()
return self.params.get('reward_penalized', False)
@property
def save_penalty(self):
# Essentially an override for CPO so it can save a penalty coefficient
# derived in its inner-loop optimization process.
return self.params.get('save_penalty', False)
@property
def trust_region(self):
self.ensure_satisfiable_optimization()
return self.params.get('trust_region', False)
@property
def use_penalty(self):
return self.reward_penalized or \
self.objective_penalized
class PPOAgent(Agent):
def __init__(self, clip_ratio=0.2,
pi_lr=3e-4,
pi_iters=80,
kl_margin=1.2,
**kwargs):
super().__init__(**kwargs)
self.clip_ratio = clip_ratio
self.pi_lr = pi_lr
self.pi_iters = pi_iters
self.kl_margin = kl_margin
self.params.update(dict(
clipped_adv=True,
first_order=True,
constrained=False
))
def update_pi(self, inputs):
# Things we need from training package
train_pi = self.training_package['train_pi']
d_kl = self.training_package['d_kl']
target_kl = self.training_package['target_kl']
# Run the update
for i in range(self.pi_iters):
_, kl = self.sess.run([train_pi, d_kl], feed_dict=inputs)
kl = mpi_avg(kl)
if kl > self.kl_margin * target_kl:
self.logger.log('Early stopping at step %d due to reaching max kl.'%i)
break
self.logger.store(StopIter=i)
def log(self):
self.logger.log_tabular('StopIter', average_only=True)
class TrustRegionAgent(Agent):
def __init__(self, damping_coeff=0.1,
backtrack_coeff=0.8,
backtrack_iters=10,
**kwargs):
super().__init__(**kwargs)
self.damping_coeff = damping_coeff
self.backtrack_coeff = backtrack_coeff
self.backtrack_iters = backtrack_iters
self.params.update(dict(
trust_region=True
))
class TRPOAgent(TrustRegionAgent):
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.params.update(dict(
constrained=False
))
def update_pi(self, inputs):
flat_g = self.training_package['flat_g']
v_ph = self.training_package['v_ph']
hvp = self.training_package['hvp']
get_pi_params = self.training_package['get_pi_params']
set_pi_params = self.training_package['set_pi_params']
pi_loss = self.training_package['pi_loss']
d_kl = self.training_package['d_kl']
target_kl = self.training_package['target_kl']
Hx = lambda x : mpi_avg(self.sess.run(hvp, feed_dict={**inputs, v_ph: x}))
g, pi_l_old = self.sess.run([flat_g, pi_loss], feed_dict=inputs)
g, pi_l_old = mpi_avg(g), mpi_avg(pi_l_old)
# Core calculations for TRPO or NPG
x = tro.cg(Hx, g)
alpha = np.sqrt(2*target_kl/(np.dot(x, Hx(x))+EPS))
old_params = self.sess.run(get_pi_params)
# Save lagrange multiplier
self.logger.store(Alpha=alpha)
def set_and_eval(step):
self.sess.run(set_pi_params, feed_dict={v_ph: old_params - alpha * x * step})
return mpi_avg(self.sess.run([d_kl, pi_loss], feed_dict=inputs))
# TRPO augments NPG with backtracking line search, hard kl constraint
for j in range(self.backtrack_iters):
kl, pi_l_new = set_and_eval(step=self.backtrack_coeff**j)
if kl <= target_kl and pi_l_new <= pi_l_old:
self.logger.log('Accepting new params at step %d of line search.'%j)
self.logger.store(BacktrackIters=j)
break
if j==self.backtrack_iters-1:
self.logger.log('Line search failed! Keeping old params.')
self.logger.store(BacktrackIters=j)
kl, pi_l_new = set_and_eval(step=0.)
def log(self):
self.logger.log_tabular('Alpha', average_only=True)
self.logger.log_tabular('BacktrackIters', average_only=True)
class CPOAgent(TrustRegionAgent):
def __init__(self, learn_margin=False, **kwargs):
super().__init__(**kwargs)
self.learn_margin = learn_margin
self.params.update(dict(
constrained=True,
save_penalty=True
))
self.margin = 0
self.margin_lr = 0.05
def update_pi(self, inputs):
flat_g = self.training_package['flat_g']
flat_b = self.training_package['flat_b']
v_ph = self.training_package['v_ph']
hvp = self.training_package['hvp']
get_pi_params = self.training_package['get_pi_params']
set_pi_params = self.training_package['set_pi_params']
pi_loss = self.training_package['pi_loss']
surr_cost = self.training_package['surr_cost']
d_kl = self.training_package['d_kl']
target_kl = self.training_package['target_kl']
cost_lim = self.training_package['cost_lim']
Hx = lambda x : mpi_avg(self.sess.run(hvp, feed_dict={**inputs, v_ph: x}))
outs = self.sess.run([flat_g, flat_b, pi_loss, surr_cost], feed_dict=inputs)
outs = [mpi_avg(out) for out in outs]
g, b, pi_l_old, surr_cost_old = outs
# Need old params, old policy cost gap (epcost - limit),
# and surr_cost rescale factor (equal to average eplen).
old_params = self.sess.run(get_pi_params)
c = self.logger.get_stats('EpCost')[0] - cost_lim
rescale = self.logger.get_stats('EpLen')[0]
# Consider the right margin
if self.learn_margin:
self.margin += self.margin_lr * c
self.margin = max(0, self.margin)
# The margin should be the same across processes anyhow, but let's
# mpi_avg it just to be 100% sure there's no drift. :)
self.margin = mpi_avg(self.margin)
# Adapt threshold with margin.
c += self.margin
# c + rescale * b^T (theta - theta_k) <= 0, equiv c/rescale + b^T(...)
c /= (rescale + EPS)
# Core calculations for CPO
v = tro.cg(Hx, g)
approx_g = Hx(v)
q = np.dot(v, approx_g)
# Determine optim_case (switch condition for calculation,
# based on geometry of constrained optimization problem)
if np.dot(b,b) <= 1e-8 and c < 0:
# feasible and cost grad is zero---shortcut to pure TRPO update!
w, r, s, A, B = 0, 0, 0, 0, 0
optim_case = 4
else:
# cost grad is nonzero: CPO update!
w = tro.cg(Hx, b)
r = np.dot(w, approx_g) # b^T H^{-1} g
s = np.dot(w, Hx(w)) # b^T H^{-1} b
A = q - r**2 / s # should be always positive (Cauchy-Shwarz)
B = 2*target_kl - c**2 / s # does safety boundary intersect trust region? (positive = yes)
if c < 0 and B < 0:
# point in trust region is feasible and safety boundary doesn't intersect
# ==> entire trust region is feasible
optim_case = 3
elif c < 0 and B >= 0:
# x = 0 is feasible and safety boundary intersects
# ==> most of trust region is feasible
optim_case = 2
elif c >= 0 and B >= 0:
# x = 0 is infeasible and safety boundary intersects
# ==> part of trust region is feasible, recovery possible
optim_case = 1
self.logger.log('Alert! Attempting feasible recovery!', 'yellow')
else:
# x = 0 infeasible, and safety halfspace is outside trust region
# ==> whole trust region is infeasible, try to fail gracefully
optim_case = 0
self.logger.log('Alert! Attempting infeasible recovery!', 'red')
if optim_case in [3,4]:
lam = np.sqrt(q / (2*target_kl))
nu = 0
elif optim_case in [1,2]:
LA, LB = [0, r /c], [r/c, np.inf]
LA, LB = (LA, LB) if c < 0 else (LB, LA)
proj = lambda x, L : max(L[0], min(L[1], x))
lam_a = proj(np.sqrt(A/B), LA)
lam_b = proj(np.sqrt(q/(2*target_kl)), LB)
f_a = lambda lam : -0.5 * (A / (lam+EPS) + B * lam) - r*c/(s+EPS)
f_b = lambda lam : -0.5 * (q / (lam+EPS) + 2 * target_kl * lam)
lam = lam_a if f_a(lam_a) >= f_b(lam_b) else lam_b
nu = max(0, lam * c - r) / (s + EPS)
else:
lam = 0
nu = np.sqrt(2 * target_kl / (s+EPS))
# normal step if optim_case > 0, but for optim_case =0,
# perform infeasible recovery: step to purely decrease cost
x = (1./(lam+EPS)) * (v + nu * w) if optim_case > 0 else nu * w
# save intermediates for diagnostic purposes
self.logger.store(Optim_A=A, Optim_B=B, Optim_c=c,
Optim_q=q, Optim_r=r, Optim_s=s,
Optim_Lam=lam, Optim_Nu=nu,
Penalty=nu, DeltaPenalty=0,
Margin=self.margin,
OptimCase=optim_case)
def set_and_eval(step):
self.sess.run(set_pi_params, feed_dict={v_ph: old_params - step * x})
return mpi_avg(self.sess.run([d_kl, pi_loss, surr_cost], feed_dict=inputs))
# CPO uses backtracking linesearch to enforce constraints
self.logger.log('surr_cost_old %.3f'%surr_cost_old, 'blue')
for j in range(self.backtrack_iters):
kl, pi_l_new, surr_cost_new = set_and_eval(step=self.backtrack_coeff**j)
self.logger.log('%d \tkl %.3f \tsurr_cost_new %.3f'%(j, kl, surr_cost_new), 'blue')
if (kl <= target_kl and
(pi_l_new <= pi_l_old if optim_case > 1 else True) and
surr_cost_new - surr_cost_old <= max(-c,0)):
self.logger.log('Accepting new params at step %d of line search.'%j)
self.logger.store(BacktrackIters=j)
break
if j==self.backtrack_iters-1:
self.logger.log('Line search failed! Keeping old params.')
self.logger.store(BacktrackIters=j)
kl, pi_l_new, surr_cost_new = set_and_eval(step=0.)
def log(self):
self.logger.log_tabular('Optim_A', average_only=True)
self.logger.log_tabular('Optim_B', average_only=True)
self.logger.log_tabular('Optim_c', average_only=True)
self.logger.log_tabular('Optim_q', average_only=True)
self.logger.log_tabular('Optim_r', average_only=True)
self.logger.log_tabular('Optim_s', average_only=True)
self.logger.log_tabular('Optim_Lam', average_only=True)
self.logger.log_tabular('Optim_Nu', average_only=True)
self.logger.log_tabular('OptimCase', average_only=True)
self.logger.log_tabular('Margin', average_only=True)
self.logger.log_tabular('BacktrackIters', average_only=True)