treequest 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- treequest/__init__.py +15 -0
- treequest/algos/ab_mcts_a/algo.py +383 -0
- treequest/algos/ab_mcts_a/prob_state.py +472 -0
- treequest/algos/ab_mcts_m/algo.py +248 -0
- treequest/algos/ab_mcts_m/pymc_interface.py +639 -0
- treequest/algos/base.py +46 -0
- treequest/algos/best_first_search.py +149 -0
- treequest/algos/multi_armed_bandit_ucb.py +144 -0
- treequest/algos/standard_mcts.py +240 -0
- treequest/algos/tree.py +121 -0
- treequest/algos/tree_of_thought_bfs.py +204 -0
- treequest/imports.py +97 -0
- treequest/py.typed +0 -0
- treequest/ranker.py +29 -0
- treequest/types.py +17 -0
- treequest/visualization.py +82 -0
- treequest-0.1.0.dist-info/METADATA +210 -0
- treequest-0.1.0.dist-info/RECORD +20 -0
- treequest-0.1.0.dist-info/WHEEL +4 -0
- treequest-0.1.0.dist-info/licenses/LICENSE +201 -0
treequest/__init__.py
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
from .algos.ab_mcts_a.algo import ABMCTSA
|
|
2
|
+
from .algos.ab_mcts_m.algo import ABMCTSM
|
|
3
|
+
from .algos.base import Algorithm
|
|
4
|
+
from .algos.standard_mcts import StandardMCTS
|
|
5
|
+
from .algos.tree_of_thought_bfs import TreeOfThoughtsBFSAlgo
|
|
6
|
+
from .ranker import top_k
|
|
7
|
+
|
|
8
|
+
__all__ = [
|
|
9
|
+
"StandardMCTS",
|
|
10
|
+
"top_k",
|
|
11
|
+
"TreeOfThoughtsBFSAlgo",
|
|
12
|
+
"ABMCTSA",
|
|
13
|
+
"ABMCTSM",
|
|
14
|
+
"Algorithm",
|
|
15
|
+
]
|
|
@@ -0,0 +1,383 @@
|
|
|
1
|
+
import copy
|
|
2
|
+
from collections import defaultdict
|
|
3
|
+
from collections.abc import Mapping
|
|
4
|
+
from dataclasses import dataclass, field
|
|
5
|
+
from logging import getLogger
|
|
6
|
+
from typing import Dict, Generic, List, Literal, Optional, Tuple, TypeVar, Union
|
|
7
|
+
|
|
8
|
+
from treequest.algos.ab_mcts_a.prob_state import NodeProbState, PriorConfig
|
|
9
|
+
from treequest.algos.base import Algorithm
|
|
10
|
+
from treequest.algos.tree import Node, Tree
|
|
11
|
+
from treequest.types import GenerateFnType, StateScoreType
|
|
12
|
+
|
|
13
|
+
# Type variable for state
|
|
14
|
+
StateT = TypeVar("StateT")
|
|
15
|
+
|
|
16
|
+
logger = getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass
|
|
20
|
+
class ABMCTSAStateManager(Generic[StateT]):
|
|
21
|
+
"""Manager for ABMCTSAState instances associated with expand_idx values."""
|
|
22
|
+
|
|
23
|
+
states: Dict[int, NodeProbState] = field(default_factory=dict)
|
|
24
|
+
default_prior_config: Optional["PriorConfig"] = None
|
|
25
|
+
default_reward_average_priors: Optional[Union[float, Dict[str, float]]] = None
|
|
26
|
+
default_model_selection_strategy: str = "multiarm_bandit_thompson"
|
|
27
|
+
|
|
28
|
+
def __contains__(self, expand_idx: int) -> bool:
|
|
29
|
+
"""Check if a thompson state exists for the given expand_idx."""
|
|
30
|
+
return expand_idx in self.states
|
|
31
|
+
|
|
32
|
+
def __len__(self) -> int:
|
|
33
|
+
return len(self.states)
|
|
34
|
+
|
|
35
|
+
def get(self, node: Node) -> Optional[NodeProbState]:
|
|
36
|
+
"""Get thompson state for the given expand_idx if it exists."""
|
|
37
|
+
return self.states.get(node.expand_idx)
|
|
38
|
+
|
|
39
|
+
def create(
|
|
40
|
+
self,
|
|
41
|
+
node: Node,
|
|
42
|
+
actions: List[str],
|
|
43
|
+
prior_config: Optional["PriorConfig"] = None,
|
|
44
|
+
reward_average_priors: Optional[Union[float, Dict[str, float]]] = None,
|
|
45
|
+
model_selection_strategy: Optional[str] = None,
|
|
46
|
+
) -> NodeProbState:
|
|
47
|
+
"""Create a new thompson state for the given expand_idx with optional prior configuration."""
|
|
48
|
+
# Use provided configs or fall back to defaults
|
|
49
|
+
prior_config = prior_config or self.default_prior_config
|
|
50
|
+
reward_average_priors = (
|
|
51
|
+
reward_average_priors or self.default_reward_average_priors
|
|
52
|
+
)
|
|
53
|
+
model_selection_strategy = (
|
|
54
|
+
model_selection_strategy or self.default_model_selection_strategy
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
state = NodeProbState(
|
|
58
|
+
actions=actions,
|
|
59
|
+
prior_config=prior_config,
|
|
60
|
+
reward_average_priors=reward_average_priors,
|
|
61
|
+
model_selection_strategy=model_selection_strategy,
|
|
62
|
+
)
|
|
63
|
+
self.states[node.expand_idx] = state
|
|
64
|
+
return state
|
|
65
|
+
|
|
66
|
+
def get_or_create(
|
|
67
|
+
self,
|
|
68
|
+
node: Node,
|
|
69
|
+
actions: List[str],
|
|
70
|
+
prior_config: Optional["PriorConfig"] = None,
|
|
71
|
+
reward_average_priors: Optional[Union[float, Dict[str, float]]] = None,
|
|
72
|
+
model_selection_strategy: Optional[str] = None,
|
|
73
|
+
) -> NodeProbState:
|
|
74
|
+
"""Get existing thompson state or create a new one if it doesn't exist."""
|
|
75
|
+
if node.expand_idx in self.states:
|
|
76
|
+
return self.states[node.expand_idx]
|
|
77
|
+
return self.create(
|
|
78
|
+
node,
|
|
79
|
+
actions,
|
|
80
|
+
prior_config=prior_config,
|
|
81
|
+
reward_average_priors=reward_average_priors,
|
|
82
|
+
model_selection_strategy=model_selection_strategy,
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def build_default_dict_of_list() -> Dict[str, List[float]]:
|
|
87
|
+
"""
|
|
88
|
+
For pickle we define it at the top-module level as named function.
|
|
89
|
+
Ref: https://docs.python.org/3.13/library/pickle.html#what-can-be-pickled-and-unpickled
|
|
90
|
+
"""
|
|
91
|
+
return defaultdict(list)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
@dataclass
|
|
95
|
+
class ABMCTSAAlgoState(Generic[StateT]):
|
|
96
|
+
"""State for ABMCTSA algorithm."""
|
|
97
|
+
|
|
98
|
+
tree: Tree[StateT]
|
|
99
|
+
thompson_states: ABMCTSAStateManager = field(default_factory=ABMCTSAStateManager)
|
|
100
|
+
all_rewards_store: Dict[str, List[float]] = field(
|
|
101
|
+
default_factory=build_default_dict_of_list
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
class ABMCTSA(Algorithm[StateT, ABMCTSAAlgoState[StateT]]):
|
|
106
|
+
"""
|
|
107
|
+
Adaptive Monte Carlo Tree Search algorithm with Node Aggregation.
|
|
108
|
+
|
|
109
|
+
This algorithm uses Thompson Sampling to decide whether to generate a new node (GEN)
|
|
110
|
+
or continue exploring from an existing one (CONT).
|
|
111
|
+
"""
|
|
112
|
+
|
|
113
|
+
def __init__(
|
|
114
|
+
self,
|
|
115
|
+
dist_type: Literal["gaussian", "beta"] = "gaussian",
|
|
116
|
+
reward_average_priors: Optional[Union[float, Dict[str, float]]] = None,
|
|
117
|
+
prior_config: Optional[PriorConfig] = None,
|
|
118
|
+
model_selection_strategy: str = "multiarm_bandit_thompson",
|
|
119
|
+
):
|
|
120
|
+
"""
|
|
121
|
+
Initialize the AB-MCTS-A algorithm.
|
|
122
|
+
|
|
123
|
+
Args:
|
|
124
|
+
dist_type: Type of Probability distribution. Either "gaussian" or "beta".
|
|
125
|
+
reward_average_priors: Optional prior reward average values (global or per-action)
|
|
126
|
+
prior_config: Optional prior configuration for Thompson sampling distributions. If specified, dist_type will not be used.
|
|
127
|
+
model_selection_strategy: Strategy for model selection. One of:
|
|
128
|
+
- "stack": Perform separate fits for each model (traditional approach)
|
|
129
|
+
- "multiarm_bandit_thompson": Use Thompson Sampling for joint selection
|
|
130
|
+
- "multiarm_bandit_ucb": Use UCB for joint selection
|
|
131
|
+
"""
|
|
132
|
+
if prior_config is None:
|
|
133
|
+
prior_config = PriorConfig(dist_type=dist_type)
|
|
134
|
+
else:
|
|
135
|
+
if dist_type != prior_config.dist_type:
|
|
136
|
+
logger.warning(
|
|
137
|
+
f"dist_type argument {dist_type} is different from the one specified by prior_config {prior_config.dist_type}. {dist_type} is ignored, and {prior_config.dist_type} will be used."
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
self.prior_config = prior_config
|
|
141
|
+
self.reward_average_priors = reward_average_priors
|
|
142
|
+
|
|
143
|
+
# Strategy for model selection:
|
|
144
|
+
# "stack": Perform separate fits for each model (traditional approach)
|
|
145
|
+
# "multiarm_bandit_thompson": Use Thompson Sampling for joint selection
|
|
146
|
+
# "multiarm_bandit_ucb": Use UCB for joint selection
|
|
147
|
+
if model_selection_strategy not in [
|
|
148
|
+
"stack",
|
|
149
|
+
"multiarm_bandit_thompson",
|
|
150
|
+
"multiarm_bandit_ucb",
|
|
151
|
+
]:
|
|
152
|
+
raise ValueError(
|
|
153
|
+
f"Invalid model_selection_strategy: {model_selection_strategy}. "
|
|
154
|
+
f"Must be one of: 'stack', 'multiarm_bandit_thompson', 'multiarm_bandit_ucb'"
|
|
155
|
+
)
|
|
156
|
+
self.model_selection_strategy = model_selection_strategy
|
|
157
|
+
|
|
158
|
+
def init_tree(self) -> ABMCTSAAlgoState:
|
|
159
|
+
"""
|
|
160
|
+
Initialize the algorithm state with an empty tree.
|
|
161
|
+
|
|
162
|
+
Returns:
|
|
163
|
+
Initial algorithm state
|
|
164
|
+
"""
|
|
165
|
+
tree: Tree = Tree.with_root_node()
|
|
166
|
+
state_manager = ABMCTSAStateManager[StateT](
|
|
167
|
+
default_prior_config=self.prior_config,
|
|
168
|
+
default_reward_average_priors=self.reward_average_priors,
|
|
169
|
+
default_model_selection_strategy=self.model_selection_strategy,
|
|
170
|
+
)
|
|
171
|
+
return ABMCTSAAlgoState(tree=tree, thompson_states=state_manager)
|
|
172
|
+
|
|
173
|
+
def step(
|
|
174
|
+
self,
|
|
175
|
+
state: ABMCTSAAlgoState,
|
|
176
|
+
generate_fn: Mapping[str, GenerateFnType[StateT]],
|
|
177
|
+
inplace: bool = False,
|
|
178
|
+
) -> ABMCTSAAlgoState:
|
|
179
|
+
"""
|
|
180
|
+
Perform one step of the Thompson Sampling MCTS algorithm.
|
|
181
|
+
|
|
182
|
+
Args:
|
|
183
|
+
state: Current algorithm state
|
|
184
|
+
generate_fn: Mapping of action names to generation functions
|
|
185
|
+
|
|
186
|
+
Returns:
|
|
187
|
+
Updated algorithm state
|
|
188
|
+
"""
|
|
189
|
+
if not inplace:
|
|
190
|
+
state = copy.deepcopy(state)
|
|
191
|
+
|
|
192
|
+
# initialize all_rewards_store
|
|
193
|
+
if len(state.all_rewards_store) == 0:
|
|
194
|
+
for action in generate_fn:
|
|
195
|
+
state.all_rewards_store[action] = []
|
|
196
|
+
|
|
197
|
+
# If the tree is empty (only root), expand the root
|
|
198
|
+
if not state.tree.root.children:
|
|
199
|
+
self._expand_node(state, state.tree.root, generate_fn)
|
|
200
|
+
return state
|
|
201
|
+
|
|
202
|
+
# Run one simulation step
|
|
203
|
+
node = state.tree.root
|
|
204
|
+
|
|
205
|
+
# Selection phase: traverse tree until we reach a leaf node or need to create a new node
|
|
206
|
+
while node.children:
|
|
207
|
+
node, action_used = self._select_child(state, node, generate_fn)
|
|
208
|
+
|
|
209
|
+
# If action is not None, it means we've generated a new node
|
|
210
|
+
if action_used is not None:
|
|
211
|
+
return state
|
|
212
|
+
|
|
213
|
+
# Expansion phase: expand leaf node
|
|
214
|
+
self._expand_node(state, node, generate_fn)
|
|
215
|
+
|
|
216
|
+
return state
|
|
217
|
+
|
|
218
|
+
def _select_child(
|
|
219
|
+
self,
|
|
220
|
+
state: ABMCTSAAlgoState,
|
|
221
|
+
node: Node,
|
|
222
|
+
generate_fn: Mapping[str, GenerateFnType[StateT]],
|
|
223
|
+
) -> Tuple[Node, Optional[str]]:
|
|
224
|
+
"""
|
|
225
|
+
Select a child node using Thompson Sampling.
|
|
226
|
+
|
|
227
|
+
Args:
|
|
228
|
+
state: Current algorithm state
|
|
229
|
+
node: Node to select child from
|
|
230
|
+
generate_fn: Mapping of action names to generation functions
|
|
231
|
+
|
|
232
|
+
Returns:
|
|
233
|
+
Tuple of (selected node, action if new node was generated)
|
|
234
|
+
"""
|
|
235
|
+
# Get or create thompson state for this node
|
|
236
|
+
thompson_state = state.thompson_states.get_or_create(
|
|
237
|
+
node,
|
|
238
|
+
list(generate_fn.keys()),
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
# Ask for next node or action using Thompson Sampling
|
|
242
|
+
selection = thompson_state.select_next(state.all_rewards_store)
|
|
243
|
+
|
|
244
|
+
# If string returned, we need to generate a new node with that action
|
|
245
|
+
if isinstance(selection, str):
|
|
246
|
+
new_node = self._generate_new_child(state, node, generate_fn, selection)
|
|
247
|
+
return new_node, selection
|
|
248
|
+
else:
|
|
249
|
+
# Otherwise, we return the existing child with that index
|
|
250
|
+
if selection >= len(node.children):
|
|
251
|
+
raise RuntimeError(
|
|
252
|
+
f"Something went wrong in ABMCTSA algorithm: selected index {selection} is out of bounds."
|
|
253
|
+
)
|
|
254
|
+
return node.children[selection], None
|
|
255
|
+
|
|
256
|
+
def _expand_node(
|
|
257
|
+
self,
|
|
258
|
+
state: ABMCTSAAlgoState,
|
|
259
|
+
node: Node,
|
|
260
|
+
generate_fn: Mapping[str, GenerateFnType[StateT]],
|
|
261
|
+
) -> Tuple[Node, str]:
|
|
262
|
+
"""
|
|
263
|
+
Expand a leaf node by generating a new child.
|
|
264
|
+
|
|
265
|
+
Args:
|
|
266
|
+
state: Current algorithm state
|
|
267
|
+
node: Node to expand
|
|
268
|
+
generate_fn: Mapping of action names to generation functions
|
|
269
|
+
|
|
270
|
+
Returns:
|
|
271
|
+
Tuple of (new node, action used)
|
|
272
|
+
"""
|
|
273
|
+
# Create thompson state for this node if it doesn't exist
|
|
274
|
+
thompson_state = state.thompson_states.get_or_create(
|
|
275
|
+
node,
|
|
276
|
+
list(generate_fn.keys()),
|
|
277
|
+
)
|
|
278
|
+
|
|
279
|
+
# Get action to use for generating child
|
|
280
|
+
selection = thompson_state.select_next(state.all_rewards_store)
|
|
281
|
+
|
|
282
|
+
# Ensure we get a string action name, not an index
|
|
283
|
+
if not isinstance(selection, str):
|
|
284
|
+
raise RuntimeError(
|
|
285
|
+
f"Something went wrong in ABMCTSA algorithm: selection should always be str when the expansion is from the leaf node, whle got {selection}"
|
|
286
|
+
)
|
|
287
|
+
|
|
288
|
+
new_node = self._generate_new_child(state, node, generate_fn, selection)
|
|
289
|
+
return new_node, selection
|
|
290
|
+
|
|
291
|
+
def _generate_new_child(
|
|
292
|
+
self,
|
|
293
|
+
state: ABMCTSAAlgoState,
|
|
294
|
+
node: Node,
|
|
295
|
+
generate_fn: Mapping[str, GenerateFnType[StateT]],
|
|
296
|
+
action: str,
|
|
297
|
+
) -> Node:
|
|
298
|
+
"""
|
|
299
|
+
Generate a new child node using the specified action.
|
|
300
|
+
|
|
301
|
+
Args:
|
|
302
|
+
state: Current algorithm state
|
|
303
|
+
node: Parent node
|
|
304
|
+
generate_fn: Mapping of action names to generation functions
|
|
305
|
+
action: Name of action to use for generation
|
|
306
|
+
|
|
307
|
+
Returns:
|
|
308
|
+
Newly created node
|
|
309
|
+
"""
|
|
310
|
+
# Generate new state and score using the selected action
|
|
311
|
+
node_state = None if node.is_root() else node.state
|
|
312
|
+
new_state, new_score = generate_fn[action](node_state)
|
|
313
|
+
|
|
314
|
+
# Add new node to the tree
|
|
315
|
+
new_node = state.tree.add_node((new_state, new_score), node)
|
|
316
|
+
|
|
317
|
+
# Update Thompson state with the new node
|
|
318
|
+
thompson_state = state.thompson_states.get(node)
|
|
319
|
+
if thompson_state:
|
|
320
|
+
thompson_state.register_new_child_node(
|
|
321
|
+
action, new_node, self.model_selection_strategy
|
|
322
|
+
)
|
|
323
|
+
else:
|
|
324
|
+
raise RuntimeError(
|
|
325
|
+
f"Internal Error in ABMCTSA: thompson_state should not be None for node {node}"
|
|
326
|
+
)
|
|
327
|
+
|
|
328
|
+
# Backpropagate the score through the parents
|
|
329
|
+
self._backpropagate(state, new_node, new_score, action)
|
|
330
|
+
|
|
331
|
+
return new_node
|
|
332
|
+
|
|
333
|
+
def _backpropagate(
|
|
334
|
+
self, state: ABMCTSAAlgoState, node: Node, score: float, action: str
|
|
335
|
+
) -> None:
|
|
336
|
+
"""
|
|
337
|
+
Update Thompson Sampling statistics for all nodes in the path from node to root.
|
|
338
|
+
|
|
339
|
+
Args:
|
|
340
|
+
state: Current algorithm state
|
|
341
|
+
node: Leaf node to start backpropagation from
|
|
342
|
+
score: Score to backpropagate
|
|
343
|
+
action: The action which have generated node
|
|
344
|
+
"""
|
|
345
|
+
# Update all_rewards_score for multiarm bandit use
|
|
346
|
+
state.all_rewards_store[action].append(score)
|
|
347
|
+
|
|
348
|
+
# NOTE: For the newly created node, we always update the score for GEN node
|
|
349
|
+
assert node.parent is not None
|
|
350
|
+
thompson_state = state.thompson_states.get(node.parent)
|
|
351
|
+
if thompson_state is None:
|
|
352
|
+
raise RuntimeError(
|
|
353
|
+
"Internal Error in ABMCTSA: ThompsonState should have been already initialized"
|
|
354
|
+
)
|
|
355
|
+
thompson_state.update_action_reward(action=action, reward=score)
|
|
356
|
+
|
|
357
|
+
current: Optional[Node] = node.parent
|
|
358
|
+
while current is not None and current.parent is not None:
|
|
359
|
+
thompson_state = state.thompson_states.get(current.parent)
|
|
360
|
+
if thompson_state is None:
|
|
361
|
+
raise RuntimeError(
|
|
362
|
+
"Internal Error in ABMCTSA: ThompsonState should have been already initialized"
|
|
363
|
+
)
|
|
364
|
+
|
|
365
|
+
# Update the Thompson state with the score
|
|
366
|
+
thompson_state.update_node_reward(current, score)
|
|
367
|
+
|
|
368
|
+
# Move up to the parent
|
|
369
|
+
current = current.parent
|
|
370
|
+
|
|
371
|
+
def get_state_score_pairs(
|
|
372
|
+
self, state: ABMCTSAAlgoState
|
|
373
|
+
) -> List[StateScoreType[StateT]]:
|
|
374
|
+
"""
|
|
375
|
+
Get all the state-score pairs from the tree.
|
|
376
|
+
|
|
377
|
+
Args:
|
|
378
|
+
state: Current algorithm state
|
|
379
|
+
|
|
380
|
+
Returns:
|
|
381
|
+
List of (state, score) pairs
|
|
382
|
+
"""
|
|
383
|
+
return state.tree.get_state_score_pairs()
|