Training an agent¶
Every agent trains the same way: build it with an environment and its
hyperparameters, then call train. This page covers the arguments every
agent shares, what train does with them and what it gives back.
Building an agent¶
An agent's constructor takes the environment first, then its paper's
hyperparameters, then extensions=():
from ajax import SAC
agent = SAC(
"Pendulum-v1", # an environment id, or a prebuilt environment
n_envs=4, # parallel copies of the environment
actor_architecture=("256", "relu", "256", "relu"),
critic_architecture=("256", "relu", "256", "relu"),
gamma=0.99,
learning_starts=1_000,
)
The defaults follow each paper or its reference implementation; the
algorithm pages list them and every deviation.
Most agents accept the arguments below; DQN and PQN take one learning_rate
and one architecture for their Q-network instead of an actor and a critic.
| Argument | What it sets | Page |
|---|---|---|
n_envs |
parallel environment copies per seed | below |
actor_learning_rate, critic_learning_rate |
a number, or a schedule of the optimiser step | below |
actor_architecture, critic_architecture |
hidden layers, or a PID policy for the actor | Networks |
env_params |
gymnax environment parameters | Environments |
episode_length, action_repeat |
episode length and repeat of a Brax or Playground id | Environments |
normalize_observations, normalize_rewards |
running normalisation | Environments |
max_grad_norm |
global-norm gradient clipping (None: off); twin and ensemble critics are each clipped on their own |
|
weight_decay |
decoupled (AdamW) weight decay of the network optimisers, multiplied by the learning rate: each step multiplies the parameters by 1 - lr * weight_decay (0: plain Adam) |
|
memory |
a recurrent memory block | Memory |
extensions |
research features: experts, exploration, diagnostics | Extensions |
An agent refuses what it cannot do when you build it, not halfway through a
run: memory on an agent without memory support, SAC on a discrete action
space, action_repeat on a gymnax environment, an extension that needs a
step the agent's loop never runs or data its replay does not keep (the
extension checks list them all).
Each agent also records its run configuration in agent.config: every
constructor argument by name (TDMPC2MultiTask leaves out its dataset and
evaluation environments), plus algo_name. The loggers store it with each
run (Logging).
train¶
from ajax import SAC
agent = SAC("Pendulum-v1", learning_starts=1_000)
state, out = agent.train(
seed=[1, 2, 3], # one run per seed, all in one program
n_timesteps=2_000, # environment steps this call trains for
num_episode_test=10, # test episodes per evaluation
logging_config=None, # None: no evaluation, and out is None
initial_state=None, # or a state to continue from
total_timesteps=None, # the run's horizon; None: this call's end
)
The budget¶
n_timesteps counts environment steps summed over the n_envs copies. An
off-policy agent collects one step in each copy per iteration, so 100,000
steps with n_envs=4 are 25,000 iterations. An on-policy agent (PPO, APO,
PQN) collects a rollout of n_steps per copy per iteration and trains in
whole rollouts: it runs n_timesteps // (n_envs * n_steps) + 1 of them, so
it can stop a little past the budget.
The off-policy agents act uniformly at random and do not update until
learning_starts steps are collected: 10,000 by default for SAC, TD3, REDQ
and ASAC, 5,000 for CrossQ, 1,000 for DQN, 257 for TQC (whose policy acts
from the first step), 0 for AVG. A short test run must lower it, or the
agent never trains.
Seeds¶
seed is one integer or a list. Each seed starts from jax.random.PRNGKey(seed),
and train runs all of them at once with jax.vmap: one compilation, one
program, every seed in step. More seeds cost little extra time until the
device is full (Performance has the measurements), so run
the seeds you need in one call rather than one call per seed.
Every array in the returned state has the seed axis first. Seed i's part
is jax.tree.map(lambda x: x[i], state).
The same seed on the same machine and library versions gives the same run. Two programs compiled apart, or on different hardware, may round differently, so do not expect identical numbers across machines.
Parallel environments¶
n_envs copies of the environment step together inside each seed. They
collect more data per iteration, and each copy starts its own episodes. For
an off-policy agent each iteration still makes one update (or the number its
own settings ask for), so at a fixed budget more copies mean fewer updates.
The defaults differ by agent:
n_envs default |
Agents |
|---|---|
| 1 | SAC, ASAC, REDQ, CrossQ, TQC, TD3, IQL, AVG, DQN, UDRL, DAgger, TDMPC2 |
| 4 | PPO, APO |
| 8 | PQN, APG |
| 16 | DreamerV3 |
Evaluation¶
With a logging_config, the agent is evaluated every log_frequency
steps on num_episode_test episodes, run in parallel in a separate copy of
the environment. The policy acts deterministically: the mean action of a
continuous policy, the most likely one of a discrete policy.
Each evaluation draws fresh initial states and noise. Its key is folded from the number of evaluations made so far, out of an evaluation key split apart from the training key when the run starts. So two evaluations of a seed see different episodes, the same seed always sees the same ones, and evaluating repeats none of the random draws the training makes. Every agent follows this, the world models included.
UDRL evaluates through the same code, with the command (desired return and horizon) it would collect with; DreamerV3 and TD-MPC2 act through their own latent policy or planner. Their pages say what they log.
What train returns¶
train returns (state, out).
- Without a
logging_config,outisNone. The evaluation is not even compiled into the program. - With one,
outholds the evaluations: a dictionary from each logged name to an array of shape(seeds, evaluations).out["timestep"]says when each evaluation ran. A call that evaluates fewer times than its maximum pads the rest with -1 (the timestep) and NaN (everything else). - APG returns its per-update metrics instead. See APG.
The logged names are listed in Logging.
state is the agent's whole state: actor_state and critic_state (the
networks, their optimisers and parameters), collector_state (the
environments, the replay buffer, the step counter timestep), the random
keys (rng for training, eval_rng for evaluation), the evaluation count
n_logs, and each extension's state.
Continuing a run¶
Pass a returned state back as initial_state to train further:
from ajax import PPO
agent = PPO("CartPole-v1", n_envs=8, n_steps=128)
state, _ = agent.train(seed=[0, 1], n_timesteps=8_192)
state, _ = agent.train(seed=[0, 1], n_timesteps=8_192, initial_state=state)
print(state.collector_state.timestep) # [18432 18432]
The second call continues from the first call's timestep. Its schedules
(the extensions', the training fraction, the exploration, the logging's
end) continue where they stopped instead of restarting, and run to the
horizon total_timesteps. By default that is the call's own end, the
timestep reached plus n_timesteps, so a schedule set as a fraction of
training, such as DQN's epsilon decay, has already finished at the end of a
first call. To split a run and keep the schedules of one call, pass the
whole run's horizon to every call. A call whose n_timesteps would take it
past the horizon is refused, so the last call trains the rest. An on-policy
call trains in whole rollouts plus one and ends up to a rollout past its
budget (the last call past the horizon, as one call does), so take the rest
from the timestep reached:
total = 16_384
state, _ = agent.train(seed=[0, 1], n_timesteps=8_192, total_timesteps=total)
reached = int(state.collector_state.timestep[0]) # 9216: one rollout past
state, _ = agent.train(
seed=[0, 1],
n_timesteps=total - reached,
initial_state=state,
total_timesteps=total,
)
print(state.collector_state.timestep) # [17408 17408], as one call of 16_384
To resume in another process, save the state first: Checkpoints and resuming.
The call may reuse the memory of the state you pass in. Read or save anything you need from it before the call.
Training offline¶
A replay agent (SAC, TD3, REDQ, ASAC, DQN, IQL) can train on a
transition dataset alone, with no environment step.
train_offline(dataset, n_updates) puts the dataset's rows in the replay
and in the normalisers, then runs the agent's own update n_updates times.
With a logging_config it evaluates every log_frequency updates and logs
the updates done as the timestep. The state it returns is back at
timestep 0, so train(initial_state=state, ...) then fine-tunes it online,
its replay keeping the dataset's rows:
import jax
import jax.numpy as jnp
from ajax import TD3
from ajax.buffers.dataset import record
from ajax.logging.wandb_logging import LoggingConfig
def pd_expert(obs):
theta = jnp.arctan2(obs[..., 1], obs[..., 0])
return jnp.clip(-(2.0 * theta + 0.5 * obs[..., 2]), -2.0, 2.0)[..., None]
agent = TD3("Pendulum-v1", learning_starts=0) # online: update from the first step
data = record(pd_expert, agent.env_args, jax.random.PRNGKey(0), n_steps=1_000)
logging = LoggingConfig(config={}, log_frequency=500, use_wandb=False)
state, offline = agent.train_offline(data, n_updates=1_000, seed=[0], logging_config=logging)
print(offline["timestep"]) # [[ 500 1000]]: updates done
state, online = agent.train(seed=[0], n_timesteps=1_000, initial_state=state)
train_offline runs the agent's own update, with its extensions' terms:
TD3+BC is TD3 with a cloning term, and
IQL an agent of its own.
PriorData(dataset, offline_updates=N) runs the same updates inside the
pre-training of an ordinary train call, without evaluations.
Learning-rate schedules¶
A learning rate can be a function of the optimiser step, such as an optax schedule:
import optax
from ajax import PPO
# Decays linearly to zero over the first 1,000 optimiser steps.
schedule = optax.linear_schedule(init_value=3e-4, end_value=0.0, transition_steps=1_000)
agent = PPO(
"CartPole-v1",
n_envs=8,
n_steps=128,
actor_learning_rate=schedule,
critic_learning_rate=schedule,
)
state, _ = agent.train(seed=0, n_timesteps=8_192)
The step counts gradient updates, not environment steps: here 9 rollouts of
10 epochs of 2 minibatches, 180 steps. ajax.schedule.warmup_cosine_schedule
builds the linear warm-up then cosine decay used by APG's in-context
controller.
The shared training loop¶
Most agents run on one loop, TrainLoop in
agents/loop.py. An agent brings only its
algorithm: how to build a fresh state and how to make one update. The loop
does everything else, the same way for every agent:
fresh run: build the agent's state, then each extension's state
and its one-shot pre-training (if any)
resumed: take the state passed as initial_state
repeat: collect one step per environment (off-policy)
or one rollout (on-policy)
once learning_starts steps are collected:
update, then let the extensions act after the update
every log_frequency steps: evaluate and log
The world models (DreamerV3, TD-MPC2) act through their own planner or latent policy and decide how many updates to make per step, but they plug those pieces into the same loop, in the same order. UDRL runs its own iteration (a segment, its replay of segments, its command statistics, its updates) inside the loop's train function. APG and the offline TDMPC2MultiTask, whose iterations differ more, reuse the loop's pieces (the fresh state, the extensions' folds, the evaluation) rather than the loop itself.
Why one loop: collection, the learning_starts gate, the extensions,
evaluation, logging and resuming then behave identically across agents and
are fixed in one place. A bug fixed there is fixed for every agent, and an
extension written for one agent works on the others.
The whole loop is one jax.lax.scan inside one jax.jit, vmapped over the
seeds. Python runs once, to build the program; after that the CPU or GPU runs
the entire training without returning to Python, except to hand the logs
to the logging worker.
Architecture describes the loop for
contributors.