Note
Go to the end to download the full example code.
Intro to JaxDEM Reinforcement Learning.#
In this example, we train a simple agent with JaxDEM’s reinforcement learning tools.
The agent is a sphere that moves in a box with reflective boundaries. The objective is
to reach a target location. We train it with Proximal Policy Optimization (PPO)
(PPOTrainer) and a shared-parameters actor–critic MLP
(SharedActorCritic).
import tempfile
from pathlib import Path
Imports#
import jax
import jax.numpy as jnp
from flax import nnx
from jax._src.ad_util import stop_gradient_p
import jaxdem as jdem
import jaxdem.rl as rl
from jaxdem import utils
num_steps_epoch = 100
reset_every = 40
skip_frames = 50
num_envs = 32
Environment#
First, we create a single-agent navigation environment with reflective boundaries.
It uses default values for the domain and time step. See
SingleNavigator for details.
env = rl.Environment.create(
"single_navigator",
max_steps=num_steps_epoch * reset_every * skip_frames,
)
Model#
Next, we build a shared-parameters actor–critic MLP. We can use a bijector to constrain the action space.
Registry keys are case- and underscore-insensitive, so "max_norm" and "MaxNorm" are equivalent.
model = rl.Model.create(
"SharedActorCritic",
key=nnx.Rngs(jax.random.key(1)),
observation_space_size=env.observation_space_size,
action_space_size=env.action_space_size,
action_space=rl.ActionSpace.create("max_norm", max_norm=1.0),
)
Trainer (PPO)#
Then, we create the PPO trainer. You can change the learning rate, num_epochs, and other options
(PPOTrainer).
We choose these parameters so training runs fast, not for quality. With a bijector, we do not need to clip actions.
To clip actions anyway, pass that option to the trainer.
key = jax.random.key(6)
tr = rl.Trainer.create(
"PPO",
env=env,
model=model,
key=key,
num_steps_epoch=num_steps_epoch,
num_envs=num_envs,
num_epochs=1080, # We anneal the learning rate
stop_at_epoch=reset_every * 6,
skip_frames=skip_frames,
learning_rate=2e-3,
)
Training#
Train the policy. This returns the updated trainer with the learned parameters. This method is a
convenience training loop. To control the loop yourself, call
epoch() directly.
tmp_runs = Path(tempfile.gettempdir()) / "runs"
tr = tr.train(tr, directory=tmp_runs, verbose=False, log=False)
steps/s: 4.41e+06, final avg_score: 0.02
Testing the New Policy#
Now that we have a trained agent, we test it.
We reset the agent and move the target periodically, so the agent chases the objective. When we save the simulation state, we add a small sphere at the target to show where the agent must go.
tr.key, subkey = jax.random.split(tr.key)
env = env.reset(
env, subkey
) # re-seed and reset the serial env. The trainer used its own vectorized copy.
tmp_frames = Path(tempfile.gettempdir()) / "frames"
writer = jdem.VTKWriter(directory=tmp_frames)
state = env.state.add(env.state, pos=env.env_params["objective"], rad=env.state.rad / 5)
writer.save(state, env.system)
JaxDEM has utilities that drive the environment. To use them, we create a policy function.
Each env_step() call advances n logical steps. Each logical step runs
1 + skip_frames physics frames. The loop below calls env_step with n=10, so each saved
frame covers 10 logical steps, and the objective moves every 20 calls (200 logical steps).
@jax.jit
def policy_model(obs, key, graphstate, graphdef):
model = nnx.merge(graphdef, graphstate)
pi, _value = model(obs, sequence=False)
action = pi.sample(seed=key)
_, graphstate = nnx.split(model)
return action, graphstate
NOTE: With a recurrent model (like LSTMActorCritic or MinGRUActorCritic), we must reset its internal memory before we run the policy. It is good practice to always call reset, because non-recurrent models ignore it.
base_model = tr.model
base_model.reset(
shape=(env.max_num_agents, 1),
mask=None,
)
graphdef, graphstate = nnx.split(base_model)
for _ in range(5): # 1000 total steps / 200 steps per objective change
for _ in range(200 // 10):
env, tr.key, graphstate = utils.env_step(
env,
policy_model,
tr.key,
graphstate,
graphdef=graphdef,
n=10,
skip_frames=skip_frames,
)
state = env.state.add(
env.state,
pos=env.env_params["objective"],
rad=env.state.rad / 5,
)
writer.save(state, env.system)
tr.key, subkey = jax.random.split(tr.key)
min_pos = env.state.rad[0] * jnp.ones_like(env.system.domain.box_size)
objective = jax.random.uniform(
subkey,
(env.max_num_agents, env.state.dim),
minval=min_pos,
maxval=env.system.domain.box_size - min_pos,
dtype=float,
)
env.env_params["objective"] = objective
Total running time of the script: (0 minutes 25.187 seconds)