Skip to content

Commit 0ff76be

Browse files
committed
Refactor exploration to use new strategy approach and deprecate prefix appraoch
1 parent c47fe78 commit 0ff76be

14 files changed

Lines changed: 94 additions & 70 deletions

experiments/alaro/trap_bandit/run.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -188,9 +188,7 @@ def make_agent(
188188
prior=np.array([1.0]),
189189
reward_function=REWARD_FUNCTION,
190190
policy_discretisation=0,
191-
exploration_prefix=0,
192191
exploration_strategy=strategy,
193-
epsilon=0.0,
194192
)
195193

196194

experiments/fllor2/ib_validate_classical.ipynb

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
"from ibrl.utils import construct_environment,dump_array,sample_action\n",
2323
"from ibrl.environments import BernoulliBanditEnvironment\n",
2424
"from ibrl.agents import InfraBayesianAgent, DiscreteBayesianAgent\n",
25+
"from ibrl.exploration import EpsilonGreedy\n",
2526
"from ibrl.infrabayesian import AMeasure,Infradistribution,MultiBernoulliWorldModel"
2627
]
2728
},
@@ -58,7 +59,8 @@
5859
" # per-arm Bayesian inference — no joint enumeration needed.\n",
5960
" params = wm.make_params([grid] * num_actions)\n",
6061
" hypotheses = [Infradistribution([AMeasure(params)], world_model=wm)]\n",
61-
" return InfraBayesianAgent(num_actions=num_actions, hypotheses=hypotheses, exploration_prefix=None, **kwargs)"
62+
" epsilon = kwargs.pop(\"epsilon\", 0.1)\n",
63+
" return InfraBayesianAgent(num_actions=num_actions, hypotheses=hypotheses, exploration_strategy=EpsilonGreedy(epsilon), **kwargs)"
6264
]
6365
},
6466
{

experiments/fllor2/ibtest.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
"seed": 42,
2222
"verbose": 2,
2323
}
24-
shared = dict(num_actions=n, seed=options["seed"] + 0x01234567, verbose=options["verbose"], epsilon=0.1)
24+
shared = dict(num_actions=n, seed=options["seed"] + 0x01234567, verbose=options["verbose"])
2525

2626

2727
def make_bernoulli(alpha, beta):

experiments/fllor2/newcomb.ipynb

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,6 @@
4040
" dist = Infradistribution([m],wm)\n",
4141
" agent = InfraBayesianAgent(\n",
4242
" num_actions=2,\n",
43-
" epsilon=0.,\n",
4443
" hypotheses=[dist],\n",
4544
" reward_function=wm.agent_reward_matrix(),\n",
4645
" policy_discretisation=5,\n",
@@ -161,7 +160,6 @@
161160
" ], np.ones(6)/6)\n",
162161
" agent = InfraBayesianAgent(\n",
163162
" num_actions=2,\n",
164-
" epsilon=0.,\n",
165163
" hypotheses=[dist],\n",
166164
" reward_function=wm.agent_reward_matrix(),\n",
167165
" policy_discretisation=5)\n",

experiments/fllor2/validate_classical.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414

1515
from ibrl.agents import DiscreteBayesianAgent, InfraBayesianAgent
1616
from ibrl.environments import BernoulliBanditEnvironment
17+
from ibrl.exploration import EpsilonGreedy
1718
from ibrl.infrabayesian import AMeasure, Infradistribution, MultiBernoulliWorldModel
1819
from ibrl.simulators import simulate
1920

@@ -32,10 +33,11 @@ def make_classical_ib_agent(
3233
grid = [np.array([1.0 - p, p]) for p in np.linspace(0.0, 1.0, num_hypotheses)]
3334
params = wm.make_params([grid] * num_actions)
3435
hypothesis = Infradistribution([AMeasure(params)], world_model=wm)
36+
epsilon = kwargs.pop("epsilon", 0.1)
3537
return InfraBayesianAgent(
3638
num_actions=num_actions,
3739
hypotheses=[hypothesis],
38-
exploration_prefix=None,
40+
exploration_strategy=EpsilonGreedy(epsilon),
3941
**kwargs,
4042
)
4143

ibrl/agents/base_greedy.py

Lines changed: 21 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,36 @@
11
import numpy as np
22

33
from . import BaseAgent
4+
from ..exploration import EpsilonGreedy, ExplorationStrategy, Softmax
45

56

67
class BaseGreedyAgent(BaseAgent):
78
"""
8-
Base class for agents that use either an epsilon-greedy or softmax policy to encourage exploration.
9+
Base class for agents that use an exploration strategy over action values.
910
1011
Arguments:
12+
exploration_strategy: Optional strategy object for converting values to a policy
1113
epsilon: For epsilon-greedy policy
1214
temperature: For softmax policy
1315
decay_type: Select formula for decreasing rate (0: exponential, 1: linear)
1416
1517
Both epsilon and temperature can be either a fixed float or a tuple (start,decay constant,min) for decreasing exploration.
1618
"""
1719
def __init__(self, *,
20+
exploration_strategy : ExplorationStrategy | None = None,
1821
epsilon : float | tuple[float] | None = None,
1922
temperature : float | tuple[float] | None = None,
2023
decay_type : float = 0,
2124
**kwargs):
2225
super().__init__(**kwargs)
2326

24-
if epsilon is not None and temperature is not None:
25-
raise RuntimeError("Cannot specify both epsilon and temperature")
26-
if epsilon is None and temperature is None:
27-
epsilon = 0.1 # default value
27+
if exploration_strategy is not None and (epsilon is not None or temperature is not None):
28+
raise RuntimeError("Cannot specify exploration_strategy with epsilon or temperature")
29+
if exploration_strategy is None:
30+
if epsilon is not None and temperature is not None:
31+
raise RuntimeError("Cannot specify both epsilon and temperature")
32+
if epsilon is None and temperature is None:
33+
epsilon = 0.1 # default value
2834

2935
assert epsilon is None or isinstance(epsilon,float) or (isinstance(epsilon,tuple) and len(epsilon)==3)
3036
assert temperature is None or isinstance(temperature,float) or (isinstance(temperature,tuple) and len(temperature)==3)
@@ -33,34 +39,20 @@ def __init__(self, *,
3339
self.temperature = temperature
3440
self.decay_type = int(decay_type)
3541
assert self.decay_type in [0,1]
42+
if exploration_strategy is not None:
43+
self.exploration_strategy = exploration_strategy
44+
elif epsilon is not None:
45+
self.exploration_strategy = EpsilonGreedy(epsilon, self.decay_type)
46+
elif temperature is not None:
47+
self.exploration_strategy = Softmax(temperature, self.decay_type)
48+
else:
49+
raise RuntimeError("Invalid state")
3650

3751
def build_greedy_policy(self, values : np.ndarray) -> np.ndarray:
3852
"""
39-
Construct probabilities based on given reward estimates and selected policy
53+
Construct probabilities from reward estimates using the configured strategy.
4054
"""
41-
if self.epsilon is not None:
42-
return self.build_epsilon_greedy_policy(values)
43-
if self.temperature is not None:
44-
return self.build_softmax_policy(values)
45-
raise RuntimeError("Invalid state")
46-
47-
def build_epsilon_greedy_policy(self, values : np.ndarray) -> np.ndarray:
48-
# Exploitation: sample uniformly across actions with highest value
49-
best_actions = np.isclose(values, values.max())
50-
exploit = np.ones_like(values)*best_actions / best_actions.sum()
51-
52-
# Exploration: sample uniformly across all actions
53-
explore = np.ones_like(values) / self.num_actions
54-
55-
epsilon = self.parse_parameter(self.epsilon)
56-
return (1 - epsilon) * exploit + epsilon * explore
57-
58-
def build_softmax_policy(self, values : np.ndarray) -> np.ndarray:
59-
temperature = self.parse_parameter(self.temperature)
60-
61-
# Numerically stable softmax
62-
exp = np.exp((values - values.max()) / temperature)
63-
return exp / exp.sum()
55+
return self.exploration_strategy.get_probabilities(self, values)
6456

6557
def parse_parameter(self, parameter : float | tuple[float]) -> float:
6658
"""

ibrl/agents/infrabayesian.py

Lines changed: 4 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import numpy as np
44

55
from . import BaseGreedyAgent
6-
from ..exploration import ExplorationStrategy
6+
from ..exploration import ExplorationStrategy, Greedy
77
from ..infrabayesian.a_measure import AMeasure
88
from ..infrabayesian.infradistribution import Infradistribution
99

@@ -21,20 +21,17 @@ class InfraBayesianAgent(BaseGreedyAgent):
2121
prior: distribution over hypotheses; default: uniform
2222
reward_function: reward_function[a,o] is reward upon seeing outcome o from action a
2323
policy_discretisation: number of mixed policies to consider per action; default: 0 (i.e. only pure policies)
24-
exploration_prefix: parameter to control exploration
25-
=0 no exploration
26-
>0 forced exploration prefix for given number of steps, then no exploration
27-
None greedy exploration (epsilon or softmax; breaks regret bounds)
24+
exploration_strategy: optional strategy object for Bayesian single-measure exploration
2825
"""
2926
def __init__(self, *,
3027
hypotheses : list[Infradistribution],
3128
prior : np.ndarray | None = None, # shape (len(hypotheses),)
3229
reward_function : np.ndarray | None = None, # shape (num_actions, num_outcomes)
3330
policy_discretisation : int = 0,
34-
exploration_prefix : int | None = 0,
3531
exploration_strategy : ExplorationStrategy | None = None,
3632
**kwargs):
37-
super().__init__(**kwargs)
33+
base_strategy = exploration_strategy if exploration_strategy is not None else Greedy()
34+
super().__init__(exploration_strategy=base_strategy, **kwargs)
3835
assert len(hypotheses) > 0
3936
assert all(isinstance(h.world_model, type(hypotheses[0].world_model))
4037
for h in hypotheses), "All hypotheses must share the same WorldModel type"
@@ -43,7 +40,6 @@ def __init__(self, *,
4340
# default: reward_function[a,o] = o with o ∈ {0,1}
4441
self.reward_function = (reward_function if reward_function is not None
4542
else np.linspace(np.zeros(self.num_actions),np.ones(self.num_actions),2).T)
46-
self.exploration_prefix = exploration_prefix
4743
self.exploration_strategy = exploration_strategy
4844

4945
# Discretise policy space:
@@ -75,15 +71,6 @@ def get_probabilities(self) -> np.ndarray:
7571
)
7672
return self.exploration_strategy.get_probabilities(self, self._action_values())
7773

78-
# Greedy policy: reproduces classical agent, breaks regret bounds
79-
if self.exploration_prefix is None:
80-
return self.build_greedy_policy(self._action_values())
81-
82-
# Forced exploration prefix: regret bounds asymptotically preserved
83-
if self.step <= self.exploration_prefix:
84-
return np.ones(self.num_actions) / self.num_actions
85-
86-
# Use optimal policy: no exploration, almost no learning
8774
return self._optimal_policy()
8875

8976
def dump_state(self) -> str:

ibrl/exploration/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
ExplorationStrategy,
77
Greedy,
88
HypothesisThompsonSampling,
9+
Softmax,
910
ThompsonSampling,
1011
UniformPrefixThen,
1112
)
@@ -16,6 +17,7 @@
1617
"ExplorationStrategy",
1718
"Greedy",
1819
"HypothesisThompsonSampling",
20+
"Softmax",
1921
"ThompsonSampling",
2022
"UniformPrefixThen",
2123
]

ibrl/exploration/strategies.py

Lines changed: 27 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -22,20 +22,40 @@ def get_probabilities(self, agent, values: np.ndarray) -> np.ndarray:
2222
class EpsilonGreedy(ExplorationStrategy):
2323
"""Epsilon-greedy exploration with a fixed or scheduled epsilon."""
2424

25-
def __init__(self, epsilon: float | tuple[float, float, float]):
25+
def __init__(self, epsilon: float | tuple[float, float, float], decay_type: int = 0):
2626
self.epsilon = epsilon
27+
self.decay_type = int(decay_type)
2728

2829
def get_probabilities(self, agent, values: np.ndarray) -> np.ndarray:
2930
greedy = Greedy().get_probabilities(agent, values)
30-
eps = self._epsilon(agent)
31+
eps = _scheduled_value(self.epsilon, agent.step, self.decay_type)
3132
uniform = np.ones(agent.num_actions) / agent.num_actions
3233
return (1 - eps) * greedy + eps * uniform
3334

34-
def _epsilon(self, agent) -> float:
35-
if isinstance(self.epsilon, tuple):
36-
start, rate, end = self.epsilon
37-
return max(start / (agent.step ** rate), end)
38-
return float(self.epsilon)
35+
36+
class Softmax(ExplorationStrategy):
37+
"""Softmax action selection with a fixed or scheduled temperature."""
38+
39+
def __init__(self, temperature: float | tuple[float, float, float], decay_type: int = 0):
40+
self.temperature = temperature
41+
self.decay_type = int(decay_type)
42+
43+
def get_probabilities(self, agent, values: np.ndarray) -> np.ndarray:
44+
temperature = _scheduled_value(self.temperature, agent.step, self.decay_type)
45+
exp = np.exp((values - values.max()) / temperature)
46+
return exp / exp.sum()
47+
48+
49+
def _scheduled_value(parameter: float | tuple[float, float, float], step: int, decay_type: int) -> float:
50+
if isinstance(parameter, float):
51+
return parameter
52+
if decay_type == 0:
53+
start, rate, end = parameter
54+
return max(start / (step ** rate), end)
55+
if decay_type == 1:
56+
start, last_step, end = parameter
57+
return end if step >= last_step else (start + (end - start) * (step / last_step))
58+
raise RuntimeError("Invalid decay_type")
3959

4060

4161
class UniformPrefixThen(ExplorationStrategy):

ibrl/infrabayesian/world_model.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,3 +76,15 @@ def compute_expected_reward(self, belief_state, reward_function: np.ndarray,
7676
Returns a scalar.
7777
"""
7878
pass
79+
80+
def get_posterior_component_weights(self, belief_state, params) -> np.ndarray:
81+
"""Posterior weights for finite component-mixture exploration strategies."""
82+
raise NotImplementedError(
83+
f"{type(self).__name__} does not expose posterior component weights"
84+
)
85+
86+
def get_component_expected_rewards(self, component, reward_function: np.ndarray) -> np.ndarray:
87+
"""Per-action expected rewards for one finite posterior component."""
88+
raise NotImplementedError(
89+
f"{type(self).__name__} does not expose component expected rewards"
90+
)

0 commit comments

Comments
 (0)