File size: 9,761 Bytes
6953bd1 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 | """PPO training (torch only; BC warm-start via --init, see BUILD_REPORT.md).
Training: 16 parallel envs, random layouts regenerated on every episode
reset (trap probability ramped by --trap-prob/--trap-ramp-iters), fixed seed 0. Rollout =
128 simulator steps per iteration. Default 120 iterations =
245,760 environment steps (983,040 agent-steps). PPO: gamma 0.99,
GAE lambda 0.95, clip 0.2, lr 3e-4, 4 epochs, minibatch 2048,
entropy coef 0.005 (default; overridable), value coef 0.5, max-grad-norm 0.5.
Reward is the shaping reward returned by BatchedSim.step
(progress + success/collision bonuses + step/proximity penalties).
Outputs: ppo_policy.pt, training_curve.csv (per-iteration mean return,
success rate over finished agent-episodes, collision rate, entropy,
value loss, policy loss), train.log via stdout redirection by caller.
"""
import argparse
import csv
import json
import time
import numpy as np
import torch
import torch.nn as nn
from sim import AMAX, OBS_DIM, BatchedSim, generate_layout
from policies import ActorCritic
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--iterations", type=int, default=120)
ap.add_argument("--rollout", type=int, default=128)
ap.add_argument("--n-envs", type=int, default=16)
ap.add_argument("--seed", type=int, default=0)
ap.add_argument("--out", default="ppo_policy.pt")
ap.add_argument("--curve", default="training_curve.csv")
ap.add_argument("--n-agents", type=int, default=4)
ap.add_argument("--init", default="")
ap.add_argument("--ent-coef", type=float, default=0.005)
ap.add_argument("--trap-prob", type=float, default=0.5)
ap.add_argument("--trap-ramp-iters", type=int, default=150)
args = ap.parse_args()
torch.manual_seed(args.seed)
np.random.seed(args.seed)
torch.set_num_threads(2)
device = "cpu"
net = ActorCritic().to(device)
if args.init:
net.load_state_dict(torch.load(args.init, map_location=device, weights_only=True))
print(f"initialised from {args.init}", flush=True)
opt = torch.optim.Adam(net.parameters(), lr=3e-4)
layout_rng = np.random.default_rng(args.seed)
def sample_layout():
# Curriculum: trap probability ramps 0 -> trap_prob over the
# first trap_ramp_iters iterations, so the policy first learns
# steering around convex obstacles (short detours) before the
# wall-trap detour (long, progress-negative detour) appears.
p = args.trap_prob * min(1.0, (sample_layout.it - 1) / max(args.trap_ramp_iters, 1))
return generate_layout(layout_rng, args.n_agents, trap=bool(layout_rng.random() < p))
sample_layout.it = 1
sim = BatchedSim(args.n_envs, args.n_agents, seed=args.seed)
layouts = [sample_layout() for _ in range(args.n_envs)]
obs = sim.reset(layouts)
gamma, lam, clip, ent_coef, vf_coef = 0.99, 0.95, 0.2, args.ent_coef, 0.5
rows = []
total_env_steps = 0
t0 = time.time()
for it in range(1, args.iterations + 1):
sample_layout.it = it
obs_buf, act_buf, logp_buf, rew_buf, val_buf, done_buf = [], [], [], [], [], []
ep_ret, ep_succ, ep_coll, ep_n = [], [], [], 0
cur_ret = np.zeros((args.n_envs, args.n_agents))
cur_succ = np.zeros((args.n_envs, args.n_agents), dtype=bool)
cur_coll = np.zeros((args.n_envs, args.n_agents), dtype=bool)
prev_done = np.zeros((args.n_envs, args.n_agents), dtype=bool)
for t in range(args.rollout):
o = torch.as_tensor(obs)
with torch.no_grad():
# pre-tanh Gaussian sample (PPO logprob is on this sample)
mean, value = net.forward(o)
std = net.log_std.exp().expand_as(mean)
dist = torch.distributions.Normal(mean, std)
raw_s = dist.sample()
logp = dist.log_prob(raw_s).sum(-1)
action = torch.tanh(raw_s) * AMAX
act_np = action.numpy()
obs_buf.append(obs.copy()); act_buf.append(raw_s.numpy())
logp_buf.append(logp.numpy()); val_buf.append(value.numpy())
obs2, rew, ep_done, _ = sim.step(act_np)
rew_buf.append(rew)
# per-agent episode end: done_agent flips False->True once
just_done = sim.done_agent.copy()
done_buf.append(np.zeros_like(rew, dtype=bool))
cur_ret += rew
cur_succ |= sim.success
cur_coll |= sim.collision
# episode-level done for GAE: per-agent terminal when its
# own episode ends (done_agent) or the env timed out
obs = obs2
total_env_steps += args.n_envs
# flush finished agents: done_agent transitions False->True
newly = just_done & ~prev_done
if newly.any():
ep_ret.extend(cur_ret[newly].tolist())
ep_succ.extend(cur_succ[newly].tolist())
ep_coll.extend(cur_coll[newly].tolist())
ep_n += int(newly.sum())
cur_ret[newly] = 0.0
cur_succ[newly] = False
cur_coll[newly] = False
prev_done = just_done
# terminal mask for this step: agent done or env done
done_buf[-1] = just_done | np.broadcast_to(ep_done[:, None], just_done.shape)
# partial resets for finished envs
fin = np.where(ep_done)[0]
if len(fin):
new_layouts = [sample_layout() for _ in fin]
obs = sim.reset_envs(fin.tolist(), new_layouts)
prev_done[fin] = False
cur_ret[fin] = 0.0
cur_succ[fin] = False
cur_coll[fin] = False
obs_arr = np.stack(obs_buf) # (T,E,A,15)
act_arr = np.stack(act_buf) # (T,E,A,2) pre-tanh samples
logp_arr = np.stack(logp_buf) # (T,E,A)
rew_arr = np.stack(rew_buf).astype(np.float32)
val_arr = np.stack(val_buf) # (T,E,A)
done_arr = np.stack(done_buf) # (T,E,A)
# GAE
with torch.no_grad():
_, last_val = net.forward(torch.as_tensor(obs))
last_val = last_val.numpy()
adv = np.zeros_like(rew_arr)
gae = np.zeros(rew_arr.shape[1:])
for t in reversed(range(args.rollout)):
nxt_val = last_val if t == args.rollout - 1 else val_arr[t + 1]
nxt_nonterm = 1.0 - done_arr[t].astype(np.float32)
delta = rew_arr[t] + gamma * nxt_val * nxt_nonterm - val_arr[t]
gae = delta + gamma * lam * nxt_nonterm * gae
adv[t] = gae
ret_arr = adv + val_arr
# flatten (T*E*A, ...)
O = torch.as_tensor(obs_arr.reshape(-1, OBS_DIM))
A_ = torch.as_tensor(act_arr.reshape(-1, 2))
LP = torch.as_tensor(logp_arr.reshape(-1))
ADV = torch.as_tensor(adv.reshape(-1))
RET = torch.as_tensor(ret_arr.reshape(-1))
ADV = (ADV - ADV.mean()) / (ADV.std() + 1e-8)
n = O.shape[0]
idx = np.arange(n)
ploss_sum = vloss_sum = ent_sum = 0.0
nb = 0
for _epoch in range(4):
np.random.shuffle(idx)
for s in range(0, n, 2048):
b = idx[s:s + 2048]
logp_new, ent, val_new = net.evaluate(O[b], A_[b])
ratio = torch.exp(logp_new - LP[b])
s1 = ratio * ADV[b]
s2 = torch.clamp(ratio, 1 - clip, 1 + clip) * ADV[b]
ploss = -torch.min(s1, s2).mean()
vloss = ((val_new - RET[b]) ** 2).mean()
loss = ploss + vf_coef * vloss - ent_coef * ent.mean()
opt.zero_grad()
loss.backward()
nn.utils.clip_grad_norm_(net.parameters(), 0.5)
opt.step()
ploss_sum += float(ploss.detach()); vloss_sum += float(vloss.detach()); ent_sum += float(ent.detach().mean())
nb += 1
row = {
"iteration": it,
"env_steps_total": total_env_steps,
"agent_steps_total": total_env_steps * args.n_agents,
"mean_episode_return": float(np.mean(ep_ret)) if ep_ret else float("nan"),
"episodes_finished": ep_n,
"success_rate_finished": float(np.mean(ep_succ)) if ep_succ else float("nan"),
"collision_rate_finished": float(np.mean(ep_coll)) if ep_coll else float("nan"),
"policy_loss": ploss_sum / max(nb, 1),
"value_loss": vloss_sum / max(nb, 1),
"entropy": ent_sum / max(nb, 1),
"elapsed_s": round(time.time() - t0, 1),
}
rows.append(row)
print(f"iter {it}/{args.iterations} env_steps={total_env_steps} "
f"episodes={ep_n} mean_return={row['mean_episode_return']:.2f} "
f"success={row['success_rate_finished']:.3f} coll={row['collision_rate_finished']:.3f} "
f"ploss={row['policy_loss']:.3f} ent={row['entropy']:.3f} "
f"elapsed={row['elapsed_s']:.0f}s", flush=True)
if it % 20 == 0 or it == args.iterations:
torch.save(net.state_dict(), args.out)
with open(args.curve, "w", newline="") as f:
w = csv.DictWriter(f, fieldnames=list(rows[0].keys()))
w.writeheader(); w.writerows(rows)
torch.save(net.state_dict(), args.out)
with open(args.curve, "w", newline="") as f:
w = csv.DictWriter(f, fieldnames=list(rows[0].keys()))
w.writeheader(); w.writerows(rows)
print(f"DONE total_env_steps={total_env_steps} agent_steps={total_env_steps * args.n_agents} "
f"elapsed={time.time() - t0:.1f}s weights={args.out}")
if __name__ == "__main__":
main()
|