Skip to content

Commit ff1994e

Browse files
committed
Make actor critic a submodule and add custom implementation
1 parent 1b8d20e commit ff1994e

4 files changed

Lines changed: 179 additions & 6 deletions

File tree

oryx/policies/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1-
from .actor_critic import AbstractActorCriticPolicy
1+
from .actor_critic import AbstractActorCriticPolicy, CustomActorCriticPolicy
22
from .base_policy import AbstractPolicy
33

44
__all__ = [
55
"AbstractPolicy",
66
"AbstractActorCriticPolicy",
7+
"CustomActorCriticPolicy",
78
]
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
from .actor_critic import AbstractActorCriticPolicy
2+
from .custom import CustomActorCriticPolicy
3+
4+
__all__ = [
5+
"AbstractActorCriticPolicy",
6+
"CustomActorCriticPolicy",
7+
]

oryx/policies/actor_critic.py renamed to oryx/policies/actor_critic/actor_critic.py

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,19 +7,22 @@
77
AbstractSampleLogProbDistribution,
88
AbstractTransformedDistribution,
99
)
10-
from oryx.spaces import AbstractSpace
1110

12-
from .base_policy import AbstractPolicy
11+
from ..base_policy import AbstractPolicy
1312

1413

14+
# TODO: Break out stateful and non-stateful policies
1515
class AbstractActorCriticPolicy[FeatureType, ActType, ObsType](
1616
AbstractPolicy[ActType, ObsType], strict=True
1717
):
18-
"""Base class for actor-critic policies."""
18+
"""
19+
Base class for actor-critic policies.
20+
21+
For now all policies are treated as stateful, meaning they maintain an internal
22+
state. Non-stateful policies can simplly ignore the state during implementation.
23+
"""
1924

2025
state_index: eqx.AbstractVar[eqx.nn.StateIndex]
21-
action_space: eqx.AbstractVar[AbstractSpace[ActType]]
22-
observation_space: eqx.AbstractVar[AbstractSpace[ObsType]]
2326

2427
@abstractmethod
2528
def extract_features(
Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
import equinox as eqx
2+
from jax import numpy as jnp
3+
from jax import random as jr
4+
from jaxtyping import Array, Float, Key
5+
6+
from oryx.distributions import (
7+
AbstractSampleLogProbDistribution,
8+
AbstractTransformedDistribution,
9+
Categorical,
10+
SquashedMultivariateNormalDiag,
11+
)
12+
from oryx.env import AbstractEnvLike
13+
from oryx.models import MLP, AbstractModel, AbstractStatefulModel, Flatten
14+
from oryx.spaces import Box, Discrete
15+
16+
from .actor_critic import AbstractActorCriticPolicy
17+
18+
19+
# TODO: Support other action spaces
20+
class CustomActorCriticPolicy(
21+
AbstractActorCriticPolicy[Float, Float, Float], strict=True
22+
):
23+
"""
24+
Actor-critic policy with custom feature extractor, value model, and action model.
25+
26+
If no feature extractor is provided, the flatten model will be used.
27+
If no value model is provided, a default MLP will be used.
28+
If no action model is provided, a default MLP will be used.
29+
30+
Only Box and Discrete action spaces are supported currently.
31+
Only non-stochastic models are supported currently.
32+
"""
33+
34+
state_index: eqx.nn.StateIndex[None]
35+
36+
feature_extractor: AbstractModel[[Float], Float]
37+
value_model: AbstractModel[[Float], Float[Array, ""]]
38+
action_model: AbstractModel[[Float], Float]
39+
log_std: Float[Array, " action_size"]
40+
41+
env: AbstractEnvLike[Float, Float]
42+
43+
def __init__(
44+
self,
45+
env: AbstractEnvLike[Float, Float],
46+
*,
47+
feature_extractor: AbstractModel[[Float], Float] | None = None,
48+
feature_size: int | None,
49+
value_model: AbstractModel[[Float], Float[Array, ""]] | None = None,
50+
action_model: AbstractModel[[Float], Float] | None = None,
51+
key: Key,
52+
):
53+
self.env = env
54+
55+
if feature_extractor is None:
56+
feature_extractor = Flatten()
57+
feature_size = int(jnp.prod(jnp.asarray(env.observation_space.shape)))
58+
elif feature_size is None:
59+
raise ValueError("Custom feature extractor must specify feature_size")
60+
61+
self.feature_extractor = feature_extractor
62+
63+
if value_model is None:
64+
key, value_model_key = jr.split(key, 2)
65+
value_model = MLP(
66+
in_size=feature_size,
67+
out_size="scalar",
68+
width_size=128,
69+
depth=4,
70+
key=value_model_key,
71+
)
72+
73+
self.value_model = value_model
74+
75+
if action_model is None:
76+
key, action_model_key = jr.split(key, 2)
77+
action_model = MLP(
78+
in_size=feature_size,
79+
out_size=int(jnp.prod(jnp.asarray(env.action_space.shape))),
80+
width_size=128,
81+
depth=4,
82+
key=action_model_key,
83+
)
84+
self.log_std = jnp.zeros(env.action_space.shape)
85+
86+
self.action_model = action_model
87+
88+
self.state_index = eqx.nn.StateIndex(None)
89+
90+
def extract_features(
91+
self, state: eqx.nn.State, observation: Float
92+
) -> tuple[eqx.nn.State, Float]:
93+
if isinstance(self.feature_extractor, AbstractStatefulModel):
94+
substate = state.substate(self.feature_extractor)
95+
substate, features = self.feature_extractor(substate, observation)
96+
state = state.update(substate)
97+
98+
else:
99+
features = self.feature_extractor(observation)
100+
101+
return state, features
102+
103+
def action_dist_from_features(self, state: eqx.nn.State, features: Float) -> tuple[
104+
eqx.nn.State,
105+
AbstractSampleLogProbDistribution[Float]
106+
| AbstractTransformedDistribution[Float],
107+
]:
108+
if isinstance(self.action_model, AbstractStatefulModel):
109+
substate = state.substate(self.action_model)
110+
substate, action = self.action_model(substate, features)
111+
state = state.update(substate)
112+
113+
else:
114+
action = self.action_model(features)
115+
116+
if isinstance(self.env.action_space, Box):
117+
action_distribution = SquashedMultivariateNormalDiag(
118+
loc=action,
119+
scale_diag=jnp.exp(self.log_std),
120+
high=self.env.action_space.high,
121+
low=self.env.action_space.low,
122+
)
123+
elif isinstance(self.env.action_space, Discrete):
124+
action_distribution = Categorical(logits=action)
125+
else:
126+
raise NotImplementedError(
127+
f"Action space {self.env.action_space} not supported"
128+
)
129+
130+
return state, action_distribution
131+
132+
def value_from_features(
133+
self, state: eqx.nn.State, features: Float
134+
) -> tuple[eqx.nn.State, Float[Array, ""]]:
135+
if isinstance(self.value_model, AbstractStatefulModel):
136+
substate = state.substate(self.value_model)
137+
# FIX: Typing not working here for some reason
138+
substate, value = self.value_model(substate, features) # pyright: ignore
139+
state = state.update(substate)
140+
141+
else:
142+
value = self.value_model(features)
143+
144+
return state, value
145+
146+
def reset(self, state: eqx.nn.State) -> eqx.nn.State:
147+
if isinstance(self.feature_extractor, AbstractStatefulModel):
148+
substate = state.substate(self.feature_extractor)
149+
substate = self.feature_extractor.reset(substate)
150+
state = state.update(substate)
151+
152+
if isinstance(self.value_model, AbstractStatefulModel):
153+
substate = state.substate(self.value_model)
154+
substate = self.value_model.reset(substate)
155+
state = state.update(substate)
156+
157+
if isinstance(self.action_model, AbstractStatefulModel):
158+
substate = state.substate(self.action_model)
159+
substate = self.action_model.reset(substate)
160+
state = state.update(substate)
161+
162+
return state

0 commit comments

Comments
 (0)