Skip to content

Commit bab40d1

Browse files
committed
Add curriculum learning example and docs
1 parent 3af1332 commit bab40d1

6 files changed

Lines changed: 274 additions & 54 deletions

File tree

docs/api/callback/curriculum.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
---
2+
title: Curriculum
3+
---
4+
5+
::: lerax.curriculum.ScheduledCurriculum
6+
options:
7+
members: true
8+
9+
::: lerax.curriculum.AbstractAdaptiveCurriculum
10+
options:
11+
members: true
12+
13+
::: lerax.curriculum.LevelCurriculum
14+
options:
15+
members: true
16+
17+
::: lerax.curriculum.linear_schedule
18+
19+
::: lerax.curriculum.step_schedule
20+
21+
::: lerax.curriculum.cosine_schedule

docs/curriculum.md

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
---
2+
title: Curriculum Learning
3+
description: How to vary environment parameters during training using scheduled and adaptive curricula.
4+
---
5+
6+
# Curriculum Learning
7+
8+
Curricula modify environment parameters over the course of training. In Lerax, curricula are callbacks that update fields on `state.env` between iterations.
9+
10+
## Scheduled Curriculum
11+
12+
A `ScheduledCurriculum` modifies an environment field on a fixed schedule based on iteration count.
13+
14+
```py
15+
from jax import random as jr
16+
17+
from lerax.algorithm import PPO
18+
from lerax.curriculum import ScheduledCurriculum, linear_schedule
19+
from lerax.env.classic_control import Pendulum
20+
from lerax.policy import MLPActorCriticPolicy
21+
22+
env = Pendulum()
23+
policy = MLPActorCriticPolicy(env=env, key=jr.key(0))
24+
algo = PPO()
25+
26+
curriculum = ScheduledCurriculum(
27+
where=lambda env: env.m, # (1)!
28+
schedule_fn=linear_schedule(start=0.5, end=2.0, total=500), # (2)!
29+
)
30+
31+
policy = algo.learn(
32+
env, policy, total_timesteps=2**18, key=jr.key(1), callback=curriculum
33+
)
34+
```
35+
36+
1. `where` selects which field on the environment to modify. Any array-valued field works.
37+
2. `linear_schedule` linearly interpolates from `start` to `end` over `total` iterations, clamped outside the range.
38+
39+
### Multiple Fields
40+
41+
Compose multiple schedules with `CallbackList`:
42+
43+
```py
44+
from lerax.callback import CallbackList
45+
from lerax.curriculum import ScheduledCurriculum, linear_schedule, step_schedule
46+
47+
curriculum = CallbackList(callbacks=[
48+
ScheduledCurriculum(
49+
where=lambda env: env.m,
50+
schedule_fn=linear_schedule(start=0.5, end=2.0, total=500),
51+
),
52+
ScheduledCurriculum(
53+
where=lambda env: env.g,
54+
schedule_fn=step_schedule(
55+
values=[5.0, 7.0, 9.8],
56+
boundaries=[200, 400],
57+
),
58+
),
59+
])
60+
```
61+
62+
### Schedule Functions
63+
64+
| Function | Behavior |
65+
|---|---|
66+
| `linear_schedule(start, end, total)` | Linear interpolation, clamped outside `[0, total]` |
67+
| `step_schedule(values, boundaries)` | Discrete jumps at iteration boundaries |
68+
| `cosine_schedule(start, end, total)` | Cosine annealing from `start` to `end` |
69+
70+
All schedule functions return a JAX-compatible callable `iteration_count -> value`.
71+
72+
## Adaptive Curriculum
73+
74+
Adaptive curricula track a user-defined performance metric and modify the environment based on it. `AbstractAdaptiveCurriculum` handles the metric tracking; subclasses implement `apply_curriculum` to decide how the metric drives parameter changes.
75+
76+
### LevelCurriculum
77+
78+
`LevelCurriculum` is the built-in concrete implementation. It steps through a sequence of parameter values, advancing to the next when the metric exceeds a threshold.
79+
80+
```py
81+
from jax import numpy as jnp
82+
83+
from lerax.curriculum import LevelCurriculum
84+
85+
curriculum = LevelCurriculum(
86+
where=lambda env: env.max_speed,
87+
levels=jnp.array([4.0, 6.0, 8.0]), # (1)!
88+
metric_fn=lambda done, reward, locals: reward, # (2)!
89+
threshold=100.0, # (3)!
90+
smoothing=0.05, # (4)!
91+
)
92+
```
93+
94+
1. Array of parameter values for each level. Training starts at index 0.
95+
2. Called every step with `(done, reward, locals_dict)`. The return value is accumulated per episode and tracked as an exponential moving average.
96+
3. When the running metric exceeds this value, the curriculum advances to the next level.
97+
4. EMA smoothing factor. Higher values respond faster to recent performance.
98+
99+
### Custom Adaptive Curricula
100+
101+
Subclass `AbstractAdaptiveCurriculum` to implement custom adaptation logic. The base class handles metric tracking in `on_step` and EMA smoothing in `on_iteration`. You only need to implement `apply_curriculum`:
102+
103+
```py
104+
from lerax.curriculum import AbstractAdaptiveCurriculum
105+
106+
class MyCurriculum(AbstractAdaptiveCurriculum):
107+
def apply_curriculum(self, state, callback_state):
108+
# callback_state.running_metric has the EMA of your metric
109+
# callback_state.level tracks the current level
110+
# Modify state.env however you like via eqx.tree_at
111+
return state, callback_state
112+
```
113+
114+
### Custom Metrics
115+
116+
The `metric_fn` receives three arguments at every step:
117+
118+
- `done`: boolean, whether the episode just ended
119+
- `reward`: scalar reward at this step
120+
- `locals`: dictionary with full transition details (`observation`, `action`, `next_env_state`, etc.)
121+
122+
Examples:
123+
124+
```py
125+
# Episode return (sum of rewards)
126+
metric_fn = lambda done, reward, locals: reward
127+
128+
# Binary success (reward > 0 at episode end)
129+
metric_fn = lambda done, reward, locals: jnp.where(done, (reward > 0).astype(float), 0.0)
130+
```

examples/curriculum.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
"""Curriculum learning example: gradually increase pendulum mass during PPO training."""
2+
3+
from jax import numpy as jnp
4+
from jax import random as jr
5+
6+
from lerax.algorithm import PPO
7+
from lerax.callback import CallbackList, ConsoleBackend, LoggingCallback
8+
from lerax.curriculum import ScheduledCurriculum, linear_schedule
9+
from lerax.env.classic_control import Pendulum
10+
from lerax.policy import MLPActorCriticPolicy
11+
12+
policy_key, learn_key = jr.split(jr.key(0), 2)
13+
14+
env = Pendulum()
15+
policy = MLPActorCriticPolicy(env=env, key=policy_key)
16+
algo = PPO(num_envs=4, num_steps=2048)
17+
18+
# Schedule pendulum mass from 0.5 to 2.0 over 500 iterations
19+
curriculum = ScheduledCurriculum(
20+
where=lambda env: env.m,
21+
schedule_fn=linear_schedule(start=0.5, end=2.0, total=500),
22+
)
23+
24+
logger = LoggingCallback([ConsoleBackend()], env=env, policy=policy)
25+
callbacks = CallbackList(callbacks=[curriculum, logger])
26+
27+
policy = algo.learn(
28+
env, policy, total_timesteps=2**20, key=learn_key, callback=callbacks
29+
)
30+
31+
print(f"Final mass: {jnp.array(2.0)}")
32+
logger.close()

src/lerax/curriculum/__init__.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
from .adaptive import (
2-
AdaptiveCurriculum,
2+
AbstractAdaptiveCurriculum,
33
AdaptiveCurriculumState,
44
AdaptiveCurriculumStepState,
5+
LevelCurriculum,
56
)
67
from .scheduled import (
78
ScheduledCurriculum,
@@ -11,9 +12,10 @@
1112
)
1213

1314
__all__ = [
14-
"AdaptiveCurriculum",
15+
"AbstractAdaptiveCurriculum",
1516
"AdaptiveCurriculumState",
1617
"AdaptiveCurriculumStepState",
18+
"LevelCurriculum",
1719
"ScheduledCurriculum",
1820
"cosine_schedule",
1921
"linear_schedule",

0 commit comments

Comments
 (0)