Sweeps¤
jaxnasium.eval.Sweep allows us to build a (parameter) sweep over any function by combining different
search stages into a flat list of jobs.
The primary use case here is to create any kind of evaluation function which accepts some parameters, and then perform a hyperparameter sweep over said function. Below is a simple example, testing various learning rates in two environments.
import jax
import jaxnasium as jym
from jaxnasium.eval import GridSearch, RandomSearch, Sweep
from jaxnasium.algorithms import PPO
SEED = jax.random.key(0)
def simple_eval_function(env, learning_rate_start):
env = jym.make(env)
ppo = PPO(learning_rate_start=learning_rate_start)
ppo, _metrics = ppo.train(SEED, env)
return ppo.evaluate(SEED, env).mean()
sweep = Sweep(
simple_eval_function,
GridSearch({"env": ["CartPole-v1", "Acrobot-v1"]}),
RandomSearch(
{"learning_rate_start": (1e-4, 1e-2, "log")},
num_samples=32,
seed=SEED,
),
)
for run in sweep:
result = run()
print(result.arguments, result.result)
A standard algorithm evaluation function is provided in AlgorithmEvaluation. It accepts env, algorithm and any leftover keywords as hyperparameters.
Typically, one may want to launch jobs through a slurm system:
parser = argparse.ArgumentParser()
parser.add_argument(
"index",
type=int,
nargs="?",
help="sweep job to run; omit to print the number of jobs",
)
args = parser.parse_args()
sweep = Sweep(...)
if args.index is None:
print(len(sweep))
return
result = sweep[args.index]()
print(result.arguments)
print("mean return:", float(result.result["evaluation"].mean()))
python sweep.py # prints number of jobs
#SBATCH --array=0-NUM_JOBS
python sweep.py $SLURM_ARRAY_TASK_ID
Full example¤
# sweep.py
import argparse
from pathlib import Path
import jax
from jaxnasium.algorithms import PPO, SAC
from jaxnasium.eval import AlgorithmEvaluation, GridSearch, SobolSearch, Sweep
OUTPUT_DIR = Path("results/sweep")
KEY = jax.random.key(0)
SEARCH_KEY = jax.random.PRNGKey(1)
DEFAULTS = {"total_timesteps": 1_000_000, "num_envs": 8, "log_function": None}
def main():
parser = argparse.ArgumentParser()
parser.add_argument(
"index",
type=int,
nargs="?",
help="sweep job to run; omit to print the number of jobs",
)
args = parser.parse_args()
evaluation = AlgorithmEvaluation(
jax.random.split(KEY, 10),
batch_size=5,
num_evaluations=50,
return_train_metrics=True,
save_path=OUTPUT_DIR,
)
sweep = Sweep(
evaluation,
GridSearch({"env": ["CartPole-v1", "Pendulum-v1"]}),
GridSearch(
{
"algorithm": {
"PPO": [
{"algorithm": PPO(**DEFAULTS)},
SobolSearch(
{
"clip_coef": (0.1, 0.3),
"ent_coef_start": (1e-3, 1e-1, "log"),
},
num_samples=32,
seed=SEARCH_KEY,
),
],
"SAC": [
{"algorithm": SAC(**DEFAULTS)},
SobolSearch(
{
"tau": (1e-3, 5e-2, "log"),
"init_alpha": (0.05, 0.5),
},
num_samples=32,
seed=SEARCH_KEY,
),
],
}
}
),
SobolSearch(
{"learning_rate_start": (1e-4, 3e-3, "log"), "gamma": (0.95, 0.999)},
num_samples=16,
seed=SEARCH_KEY,
),
)
# 2 envs × (32 PPO + 32 SAC) × 16 shared draws = 2048 jobs.
if args.index is None:
print(len(sweep))
return
result = sweep[args.index]()
print(result.arguments)
print("mean return:", float(result.result["evaluation"].mean()))
if __name__ == "__main__":
main()
python sweep.py # prints 2048
#SBATCH --array=0-2047
python sweep.py $SLURM_ARRAY_TASK_ID
jaxnasium.eval.Sweep
¤
Build a parameter sweep over function fn from one or more stages of parameter
space search, like GridSearch, OneAtATimeSearch, RandomSearch, or SobolSearch.
For a single stage, RandomSearch(...).sweep(fn, ...) is equivalent.
Arguments:
fn: The function to sweep. Must accept all swept params as keywords.
If it implements with_labels(labels) -> fn, Sweep calls that
before each job so branch labels can be recorded (as
AlgorithmEvaluation does).
*stages: GridSearch / OneAtATimeSearch / RandomSearch / SobolSearch stages to nest.
print_cost_estimate: After creation, print the cost estimate of the first job to provide a
rough estimate of the memory requirements of a job in the sweep. This triggers a compilation, so
adds some overhead to the creation of the sweep.
Attributes:
fn: Runs one SweepJob and returns its SweepResult.
jobs: All jobs, to run in a loop or to dispatch by index (e.g. over a slurm job array).
Example:
sweep = Sweep(
train,
GridSearch({"env_name": ["CartPole-v1", "Acrobot-v1"]}),
RandomSearch(
{"learning_rate": (1e-4, 1e-2, "log")},
num_samples=64,
seed=jax.random.PRNGKey(1), # the same 64 lrs in both envs
),
)
len(sweep) # 2 envs * 64 learning rates
for run in sweep:
result = run()
print(result.arguments, result.result)
# Or dispatch one job: sweep[0]()
Branching over algorithms, each with its own hyperparameters:
sweep = Sweep(
train,
GridSearch(
{
"algorithm": {
"PPO": [
{"algorithm": PPO()},
SobolSearch({"clip_coef": (0.1, 0.3)}, 32, seed=key),
],
"SAC": [
{"algorithm": SAC()},
SobolSearch({"tau": (1e-3, 5e-2)}, 32, seed=key),
],
}
}
),
)
# Results record algorithm="PPO"; the run is called with algorithm=PPO().
Or in a slurm job array:
#SBATCH --array=0-5
python create_jobs_and_run_id.py --batch_idx $SLURM_ARRAY_TASK_ID
fn: Callable[[SweepJob], SweepResult] = run_job
instance-attribute
¤
jobs: list[SweepJob] = _expand_space(list(stages), [SweepJob({}, {})])
instance-attribute
¤
__init__(fn: Callable, *stages: ParameterSpaceSearch, print_cost_estimate: bool = False)
¤
__len__()
¤
__iter__()
¤
__repr__()
¤
__getitem__(index: int | slice) -> partial[SweepResult] | Sweep
¤
jaxnasium.eval.SweepJob
dataclass
¤
One dispatchable unit of work: a single call to the swept function.
Attributes:
args: The keyword arguments the swept function is called with.
labels: Branch labels recorded in place of what they expanded into.
Overlay these on args to get the human-readable configuration.
jaxnasium.eval.SweepResult
dataclass
¤
Simple container for the results of a sweep job.
Most prominently, this contains the result and the arguments that produced it.
Attributes:
start_time: Unix time at which the job containing this run started.
end_time: Unix time at which the job containing this run finished.
arguments: The parameter values that produced this result. Branch
parameters hold their label, not the sub-space they expanded into.
result: Whatever the swept function returned, for this configuration.
Algorithm Evaluation¤
AlgorithmEvaluation trains a Jaxnasium algorithm on an environment and evaluates the
resulting agent, over one seed or many. It is an intended trial function for a
Sweep, but may be used on its own.
import jax
from jaxnasium.eval import AlgorithmEvaluation
evaluation = AlgorithmEvaluation(
seed=jax.random.split(jax.random.key(0), 10),
env="CartPole-v1",
algorithm="PPO",
batch_size=5,
save_path="results/",
)
returns = evaluation(learning_rate_start=3e-4) # shape (10, num_evaluations)
Any keyword arguments given to the call that aren't seed, env or algorithm are treated
as algorithm hyperparameters, which is what lets a sweep drive it directly.
jaxnasium.eval.AlgorithmEvaluation
dataclass
¤
Train and evaluate a jaxnasium algorithm on a given environment, for one
seed or for many. A possible trial function for a Sweep.
Arguments:
seed: Key for training and evaluation. Pass multiple keys to repeat the
training and evaluation for each (e.g. AlgorithmEvaluation(jax.random.split(key, 10))).
Optional at init: it can also be supplied when calling (e.g. from a Sweep),
but not both. A seed is required by the time the evaluation actually runs.
env: Environment to train on, or a name to pass to jym.make.
eval_env: Environment to evaluate on, or a name to pass to jym.make.
Defaults to None, in which case evaluation happens on env.
algorithm: RLAlgorithm instance or name (e.g. "PPO").
batch_size: the batch size to jax.lax.map in case multiple seeds are given.
num_evaluations: Episodes to evaluate the trained agent over.
deterministic_policy: Whether to act greedily (instead of sampling) during evaluation.
return_train_metrics: Also return the per-iteration training metrics.
save_path: Where to write the results, or None (the default) to write
nothing and leave saving to the caller.
This will save to results.npy: the evaluation results, shape (num_seeds, num_evaluations)
- train_curve.npy: the training metrics, shape (num_seeds, num_iterations, ...)
- input_parameters.json: the input parameters to the evaluation
- full_parameters.json: the full parameters used for the evaluation.
save_as_zip: If True, while save_path is not None, will zip the results folder
and write it to save_path.
Example:
key = jax.random.key(0)
evaluation = AlgorithmEvaluation(
jax.random.split(key, 10), env="CartPole-v1", batch_size=5
)
sweep = Sweep(
evaluation,
GridSearch({"algorithm": ["PPO", "DQN"]}),
RandomSearch(
{"learning_rate": (1e-4, 1e-2, "log")},
num_samples=64,
seed=jax.random.PRNGKey(1),
),
)
# 2 algorithms * 64 learning rates = 128 jobs, each of 10 seeds, 5 at a time.
__call__(**kwargs: Any) -> Any
¤
Train and evaluate, once per seed.
Returns a dict {"evaluation": evaluation_returns, "train_metrics": train_metrics} if
return_train_metrics is True, otherwise just the evaluation returns. With
several seeds, every leaf gains a leading axis over them.
context(**kwargs: Any) -> dict[str, Any]
¤
JSON-ready metadata for a configuration, to pair with its result. Used to retrieve the full algorithm and env parameters that were used for this run.
with_labels(labels: dict[str, Any]) -> AlgorithmEvaluation
¤
Attach recorded labels for the saved config that won't be passed as call arguments.