phasic_policy_gradient/ppo.py (223 lines of code) (raw):
"""
Mostly copied from ppo.py but with some extra options added that are relevant to phasic
"""
import numpy as np
import torch as th
from mpi4py import MPI
from .tree_util import tree_map
from . import torch_util as tu
from .log_save_helper import LogSaveHelper
from .minibatch_optimize import minibatch_optimize
from .roller import Roller
from .reward_normalizer import RewardNormalizer
import math
from . import logger
INPUT_KEYS = {"ob", "ac", "first", "logp", "vtarg", "adv", "state_in"}
def compute_gae(
*,
vpred: "(th.Tensor[1, float]) value predictions",
reward: "(th.Tensor[1, float]) rewards",
first: "(th.Tensor[1, bool]) mark beginning of episodes",
γ: "(float)",
λ: "(float)"
):
orig_device = vpred.device
assert orig_device == reward.device == first.device
vpred, reward, first = (x.cpu() for x in (vpred, reward, first))
first = first.to(dtype=th.float32)
assert first.dim() == 2
nenv, nstep = reward.shape
assert vpred.shape == first.shape == (nenv, nstep + 1)
adv = th.zeros(nenv, nstep, dtype=th.float32)
lastgaelam = 0
for t in reversed(range(nstep)):
notlast = 1.0 - first[:, t + 1]
nextvalue = vpred[:, t + 1]
# notlast: whether next timestep is from the same episode
delta = reward[:, t] + notlast * γ * nextvalue - vpred[:, t]
adv[:, t] = lastgaelam = delta + notlast * γ * λ * lastgaelam
vtarg = vpred[:, :-1] + adv
return adv.to(device=orig_device), vtarg.to(device=orig_device)
def log_vf_stats(comm, **kwargs):
logger.logkv(
"VFStats/EV", tu.explained_variance(kwargs["vpred"], kwargs["vtarg"], comm)
)
for key in ["vpred", "vtarg", "adv"]:
logger.logkv_mean(f"VFStats/{key.capitalize()}Mean", kwargs[key].mean())
logger.logkv_mean(f"VFStats/{key.capitalize()}Std", kwargs[key].std())
def compute_advantage(model, seg, γ, λ, comm=None):
comm = comm or MPI.COMM_WORLD
finalob, finalfirst = seg["finalob"], seg["finalfirst"]
vpredfinal = model.v(finalob, finalfirst, seg["finalstate"])
reward = seg["reward"]
logger.logkv("Misc/FrameRewMean", reward.mean())
adv, vtarg = compute_gae(
γ=γ,
λ=λ,
reward=reward,
vpred=th.cat([seg["vpred"], vpredfinal[:, None]], dim=1),
first=th.cat([seg["first"], finalfirst[:, None]], dim=1),
)
log_vf_stats(comm, adv=adv, vtarg=vtarg, vpred=seg["vpred"])
seg["vtarg"] = vtarg
adv_mean, adv_var = tu.mpi_moments(comm, adv)
seg["adv"] = (adv - adv_mean) / (math.sqrt(adv_var) + 1e-8)
def compute_losses(
model,
ob,
ac,
first,
logp,
vtarg,
adv,
state_in,
clip_param,
vfcoef,
entcoef,
kl_penalty,
):
losses = {}
diags = {}
pd, vpred, aux, _state_out = model(ob=ob, first=first, state_in=state_in)
newlogp = tu.sum_nonbatch(pd.log_prob(ac))
# prob ratio for KL / clipping based on a (possibly) recomputed logp
logratio = newlogp - logp
ratio = th.exp(logratio)
if clip_param > 0:
pg_losses = -adv * ratio
pg_losses2 = -adv * th.clamp(ratio, 1.0 - clip_param, 1.0 + clip_param)
pg_losses = th.max(pg_losses, pg_losses2)
else:
pg_losses = -adv * th.exp(newlogp - logp)
diags["entropy"] = entropy = tu.sum_nonbatch(pd.entropy()).mean()
diags["negent"] = -entropy * entcoef
diags["pg"] = pg_losses.mean()
diags["pi_kl"] = kl_penalty * 0.5 * (logratio ** 2).mean()
losses["pi"] = diags["negent"] + diags["pg"] + diags["pi_kl"]
losses["vf"] = vfcoef * ((vpred - vtarg) ** 2).mean()
with th.no_grad():
diags["clipfrac"] = (th.abs(ratio - 1) > clip_param).float().mean()
diags["approxkl"] = 0.5 * (logratio ** 2).mean()
return losses, diags
def learn(
*,
venv: "(VecEnv) vectorized environment",
model: "(ppo.PpoModel)",
interacts_total: "(float) total timesteps of interaction" = float("inf"),
nstep: "(int) number of serial timesteps" = 256,
γ: "(float) discount" = 0.99,
λ: "(float) GAE parameter" = 0.95,
clip_param: "(float) PPO parameter for clipping prob ratio" = 0.2,
vfcoef: "(float) value function coefficient" = 0.5,
entcoef: "(float) entropy coefficient" = 0.01,
nminibatch: "(int) number of minibatches to break epoch of data into" = 4,
n_epoch_vf: "(int) number of epochs to use when training the value function" = 1,
n_epoch_pi: "(int) number of epochs to use when training the policy" = 1,
lr: "(float) Adam learning rate" = 5e-4,
default_loss_weights: "(dict) default_loss_weights" = {},
store_segs: "(bool) whether or not to store segments in a buffer" = True,
verbose: "(bool) print per-epoch loss stats" = True,
log_save_opts: "(dict) passed into LogSaveHelper" = {},
rnorm: "(bool) reward normalization" = True,
kl_penalty: "(int) weight of the KL penalty, which can be used in place of clipping" = 0,
grad_weight: "(float) relative weight of this worker's gradients" = 1,
comm: "(MPI.Comm) MPI communicator" = None,
callbacks: "(seq of function(dict)->bool) to run each update" = (),
learn_state: "dict with optional keys {'opts', 'roller', 'lsh', 'reward_normalizer', 'curr_interact_count', 'seg_buf'}" = None,
):
if comm is None:
comm = MPI.COMM_WORLD
learn_state = learn_state or {}
ic_per_step = venv.num * comm.size * nstep
opt_keys = (
["pi", "vf"] if (n_epoch_pi != n_epoch_vf) else ["pi"]
) # use separate optimizers when n_epoch_pi != n_epoch_vf
params = list(model.parameters())
opts = learn_state.get("opts") or {
k: th.optim.Adam(params, lr=lr)
for k in opt_keys
}
tu.sync_params(params)
if rnorm:
reward_normalizer = learn_state.get("reward_normalizer") or RewardNormalizer(venv.num)
else:
reward_normalizer = None
def get_weight(k):
return default_loss_weights[k] if k in default_loss_weights else 1.0
def train_with_losses_and_opt(loss_keys, opt, **arrays):
losses, diags = compute_losses(
model,
entcoef=entcoef,
kl_penalty=kl_penalty,
clip_param=clip_param,
vfcoef=vfcoef,
**arrays,
)
loss = sum([losses[k] * get_weight(k) for k in loss_keys])
opt.zero_grad()
loss.backward()
tu.warn_no_gradient(model, "PPO")
tu.sync_grads(params, grad_weight=grad_weight)
diags = {k: v.detach() for (k, v) in diags.items()}
opt.step()
diags.update({f"loss_{k}": v.detach() for (k, v) in losses.items()})
return diags
def train_pi(**arrays):
return train_with_losses_and_opt(["pi"], opts["pi"], **arrays)
def train_vf(**arrays):
return train_with_losses_and_opt(["vf"], opts["vf"], **arrays)
def train_pi_and_vf(**arrays):
return train_with_losses_and_opt(["pi", "vf"], opts["pi"], **arrays)
roller = learn_state.get("roller") or Roller(
act_fn=model.act,
venv=venv,
initial_state=model.initial_state(venv.num),
keep_buf=100,
keep_non_rolling=log_save_opts.get("log_new_eps", False),
)
lsh = learn_state.get("lsh") or LogSaveHelper(
ic_per_step=ic_per_step, model=model, comm=comm, **log_save_opts
)
callback_exit = False # Does callback say to exit loop?
curr_interact_count = learn_state.get("curr_interact_count") or 0
curr_iteration = 0
seg_buf = learn_state.get("seg_buf") or []
while curr_interact_count < interacts_total and not callback_exit:
seg = roller.multi_step(nstep)
lsh.gather_roller_stats(roller)
if rnorm:
seg["reward"] = reward_normalizer(seg["reward"], seg["first"])
compute_advantage(model, seg, γ, λ, comm=comm)
if store_segs:
seg_buf.append(tree_map(lambda x: x.cpu(), seg))
with logger.profile_kv("optimization"):
# when n_epoch_pi != n_epoch_vf, we perform separate policy and vf epochs with separate optimizers
if n_epoch_pi != n_epoch_vf:
minibatch_optimize(
train_vf,
{k: seg[k] for k in INPUT_KEYS},
nminibatch=nminibatch,
comm=comm,
nepoch=n_epoch_vf,
verbose=verbose,
)
train_fn = train_pi
else:
train_fn = train_pi_and_vf
epoch_stats = minibatch_optimize(
train_fn,
{k: seg[k] for k in INPUT_KEYS},
nminibatch=nminibatch,
comm=comm,
nepoch=n_epoch_pi,
verbose=verbose,
)
for (k, v) in epoch_stats[-1].items():
logger.logkv("Opt/" + k, v)
lsh()
curr_interact_count += ic_per_step
curr_iteration += 1
for callback in callbacks:
callback_exit = callback_exit or bool(callback(locals()))
return dict(
opts=opts,
roller=roller,
lsh=lsh,
reward_normalizer=reward_normalizer,
curr_interact_count=curr_interact_count,
seg_buf=seg_buf,
)