statrl 1.2609__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.
- statrl/__init__.py +0 -0
- statrl/experiments/__init__.py +0 -0
- statrl/experiments/analyzeruns.py +95 -0
- statrl/experiments/massiveruns.py +124 -0
- statrl/experiments/onerun.py +111 -0
- statrl/experiments/parallelruns.py +60 -0
- statrl/experiments/plotruns.py +144 -0
- statrl/experiments/utils.py +140 -0
- statrl/settings/__init__.py +0 -0
- statrl/settings/bandits/__init__.py +0 -0
- statrl/settings/bandits/adversarial/__init__.py +0 -0
- statrl/settings/bandits/adversarial/lipschitz/__init__.py +0 -0
- statrl/settings/bandits/adversarial/lipschitz/agent.py +53 -0
- statrl/settings/bandits/adversarial/lipschitz/agents/ALF.py +202 -0
- statrl/settings/bandits/adversarial/lipschitz/agents/__init__.py +0 -0
- statrl/settings/bandits/adversarial/lipschitz/environment.py +117 -0
- statrl/settings/bandits/adversarial/lipschitz/envs/__init__.py +0 -0
- statrl/settings/bandits/adversarial/lipschitz/interaction.py +62 -0
- statrl/settings/bandits/adversarial/lipschitz/wrappers/__init__.py +0 -0
- statrl/settings/bandits/stochastic/__init__.py +0 -0
- statrl/settings/bandits/stochastic/anytime/__init__.py +0 -0
- statrl/settings/bandits/stochastic/anytime/_test.py +74 -0
- statrl/settings/bandits/stochastic/anytime/agent.py +76 -0
- statrl/settings/bandits/stochastic/anytime/agents/IMED.py +156 -0
- statrl/settings/bandits/stochastic/anytime/agents/NPTS.py +136 -0
- statrl/settings/bandits/stochastic/anytime/agents/TS.py +119 -0
- statrl/settings/bandits/stochastic/anytime/agents/UCB.py +112 -0
- statrl/settings/bandits/stochastic/anytime/agents/_Oracle.py +66 -0
- statrl/settings/bandits/stochastic/anytime/agents/_Random.py +54 -0
- statrl/settings/bandits/stochastic/anytime/agents/__init__.py +0 -0
- statrl/settings/bandits/stochastic/anytime/environment.py +161 -0
- statrl/settings/bandits/stochastic/anytime/envs/__init__.py +0 -0
- statrl/settings/bandits/stochastic/anytime/envs/distributions.py +126 -0
- statrl/settings/bandits/stochastic/anytime/envs/parametric.py +145 -0
- statrl/settings/bandits/stochastic/anytime/interaction.py +110 -0
- statrl/settings/bandits/stochastic/anytime/renderers/__init__.py +0 -0
- statrl/settings/bandits/stochastic/anytime/renderers/textrenderer.py +69 -0
- statrl/settings/bandits/stochastic/anytime/wrappers/__init__.py +0 -0
- statrl/settings/bandits/stochastic/batch/__init__.py +0 -0
- statrl/settings/bandits/stochastic/batch/_test.py +60 -0
- statrl/settings/bandits/stochastic/batch/agent.py +110 -0
- statrl/settings/bandits/stochastic/batch/agents/BABA.py +687 -0
- statrl/settings/bandits/stochastic/batch/agents/BCB.py +255 -0
- statrl/settings/bandits/stochastic/batch/agents/BIMED.py +235 -0
- statrl/settings/bandits/stochastic/batch/agents/_Oracle.py +89 -0
- statrl/settings/bandits/stochastic/batch/agents/_Random.py +80 -0
- statrl/settings/bandits/stochastic/batch/agents/__init__.py +0 -0
- statrl/settings/bandits/stochastic/batch/agents/baba_schedule.py +246 -0
- statrl/settings/bandits/stochastic/batch/environment.py +121 -0
- statrl/settings/bandits/stochastic/batch/envs/__init__.py +0 -0
- statrl/settings/bandits/stochastic/batch/envs/parametric.py +206 -0
- statrl/settings/bandits/stochastic/batch/envs/renderers/__init__.py +0 -0
- statrl/settings/bandits/stochastic/batch/interaction.py +102 -0
- statrl/settings/bandits/stochastic/kernel/__init__.py +0 -0
- statrl/settings/bandits/stochastic/kernel/_test.py +27 -0
- statrl/settings/bandits/stochastic/kernel/agent.py +83 -0
- statrl/settings/bandits/stochastic/kernel/agents/_Oracle.py +62 -0
- statrl/settings/bandits/stochastic/kernel/agents/_Random.py +39 -0
- statrl/settings/bandits/stochastic/kernel/agents/__init__.py +0 -0
- statrl/settings/bandits/stochastic/kernel/environment.py +237 -0
- statrl/settings/bandits/stochastic/kernel/envs/__init__.py +0 -0
- statrl/settings/bandits/stochastic/kernel/envs/kernels.py +175 -0
- statrl/settings/bandits/stochastic/kernel/interaction.py +105 -0
- statrl/settings/bandits/stochastic/kernel/renderers/__init__.py +0 -0
- statrl/settings/bandits/stochastic/kernel/renderers/plotrenderer.py +256 -0
- statrl/settings/bandits/stochastic/kernel/renderers/textrenderer.py +100 -0
- statrl/settings/bandits/stochastic/knownhorizon/__init__.py +0 -0
- statrl/settings/bandits/stochastic/knownhorizon/agent.py +71 -0
- statrl/settings/bandits/stochastic/knownhorizon/agents/__init__.py +0 -0
- statrl/settings/bandits/stochastic/knownhorizon/environment.py +13 -0
- statrl/settings/bandits/stochastic/knownhorizon/envs/__init__.py +0 -0
- statrl/settings/bandits/stochastic/knownhorizon/interaction.py +141 -0
- statrl/settings/bandits/stochastic/knownhorizon/wrappers/__init__.py +0 -0
- statrl/settings/bandits/stochastic/knownhorizon/wrappers/wrapper_anytime_knownhorizon.py +143 -0
- statrl/settings/markovdecisionprocess/__init__.py +0 -0
- statrl/settings/markovdecisionprocess/discrete_nostructure/__init__.py +0 -0
- statrl/settings/markovdecisionprocess/discrete_nostructure/_test.py +68 -0
- statrl/settings/markovdecisionprocess/discrete_nostructure/agent.py +82 -0
- statrl/settings/markovdecisionprocess/discrete_nostructure/agents/Human.py +87 -0
- statrl/settings/markovdecisionprocess/discrete_nostructure/agents/IMED_RL.py +411 -0
- statrl/settings/markovdecisionprocess/discrete_nostructure/agents/PSRL.py +356 -0
- statrl/settings/markovdecisionprocess/discrete_nostructure/agents/PSRL_original.py +306 -0
- statrl/settings/markovdecisionprocess/discrete_nostructure/agents/_Oracle.py +260 -0
- statrl/settings/markovdecisionprocess/discrete_nostructure/agents/_Random.py +56 -0
- statrl/settings/markovdecisionprocess/discrete_nostructure/agents/__init__.py +0 -0
- statrl/settings/markovdecisionprocess/discrete_nostructure/environment.py +250 -0
- statrl/settings/markovdecisionprocess/discrete_nostructure/envs/__init__.py +0 -0
- statrl/settings/markovdecisionprocess/discrete_nostructure/envs/randomMDP.py +174 -0
- statrl/settings/markovdecisionprocess/discrete_nostructure/envs/riverswim.py +207 -0
- statrl/settings/markovdecisionprocess/discrete_nostructure/interaction.py +101 -0
- statrl/settings/markovdecisionprocess/discrete_nostructure/renderers/__init__.py +0 -0
- statrl/settings/markovdecisionprocess/discrete_nostructure/renderers/htmlRenderer.py +1744 -0
- statrl/settings/markovdecisionprocess/discrete_nostructure/renderers/textRenderer.py +89 -0
- statrl/settings/markovdecisionprocess/discrete_nostructure/wrappers/__init__.py +0 -0
- statrl/settings/markovdecisionprocess/gridworld/__init__.py +0 -0
- statrl/settings/markovdecisionprocess/gridworld/_test.py +62 -0
- statrl/settings/markovdecisionprocess/gridworld/agent.py +3 -0
- statrl/settings/markovdecisionprocess/gridworld/agents/_Oracle.py +168 -0
- statrl/settings/markovdecisionprocess/gridworld/agents/_Random.py +1 -0
- statrl/settings/markovdecisionprocess/gridworld/agents/__init__.py +0 -0
- statrl/settings/markovdecisionprocess/gridworld/environment.py +4 -0
- statrl/settings/markovdecisionprocess/gridworld/envs/__init__.py +0 -0
- statrl/settings/markovdecisionprocess/gridworld/envs/gridworlds.py +905 -0
- statrl/settings/markovdecisionprocess/gridworld/interaction.py +102 -0
- statrl/settings/markovdecisionprocess/gridworld/renderers/__init__.py +0 -0
- statrl/settings/markovdecisionprocess/gridworld/renderers/htmlrenderer.py +114 -0
- statrl/settings/markovdecisionprocess/gridworld/renderers/textRenderer.py +184 -0
- statrl/settings/utils.py +366 -0
- statrl/settings/validator.py +307 -0
- statrl-1.2609.dist-info/METADATA +129 -0
- statrl-1.2609.dist-info/RECORD +114 -0
- statrl-1.2609.dist-info/WHEEL +5 -0
- statrl-1.2609.dist-info/licenses/LICENSE +21 -0
- statrl-1.2609.dist-info/top_level.txt +1 -0
statrl/__init__.py
ADDED
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
|
|
2
|
+
import pickle
|
|
3
|
+
import time
|
|
4
|
+
import numpy as np
|
|
5
|
+
|
|
6
|
+
def computeScoreDiffs(names: list[str], dump_scores: list[list[str]], timeHorizon: int, envName: str, root_folder: str) -> tuple[list[np.ndarray], list[np.ndarray], list[np.ndarray],list[np.ndarray],list[np.ndarray], list[np.ndarray], list[int]]:
|
|
7
|
+
"""Turn per-replicate score dumps into regret statistics over time.
|
|
8
|
+
|
|
9
|
+
Loads every dump, subtracts each agent's cumulative score from the
|
|
10
|
+
oracle's averaged one to obtain regret, and summarizes the replicates by
|
|
11
|
+
their mean, median, and four quantiles.
|
|
12
|
+
|
|
13
|
+
Parameters
|
|
14
|
+
----------
|
|
15
|
+
names : list of str
|
|
16
|
+
Agent names, in the same order as ``dump_scores``. Used to name the
|
|
17
|
+
per-agent regret pickles.
|
|
18
|
+
dump_scores : list of list of str
|
|
19
|
+
One list of dump filenames per agent. **The last entry must be the
|
|
20
|
+
oracle's**, and it is what every other entry is compared against — the
|
|
21
|
+
function has no other way to tell which agent is the reference.
|
|
22
|
+
timeHorizon : int
|
|
23
|
+
Number of rounds each run played.
|
|
24
|
+
envName : str
|
|
25
|
+
Environment name, used in the output filenames.
|
|
26
|
+
root_folder : str
|
|
27
|
+
Directory the regret pickles are written to.
|
|
28
|
+
|
|
29
|
+
Returns
|
|
30
|
+
-------
|
|
31
|
+
mean, median : list of ndarray
|
|
32
|
+
Per-agent mean and median regret at each sampled time.
|
|
33
|
+
quantile1, quantile2, quantile3, quantile4 : list of ndarray
|
|
34
|
+
Per-agent regret quantiles at levels 0.1, 0.25, 0.75, and 0.9. The
|
|
35
|
+
plots shade 0.1-0.9 and 0.25-0.75 as nested bands.
|
|
36
|
+
times : list of int
|
|
37
|
+
Sampled time steps, shared by every returned series.
|
|
38
|
+
|
|
39
|
+
Notes
|
|
40
|
+
-----
|
|
41
|
+
Long runs are downsampled to at most ~1000 points
|
|
42
|
+
(``skip = timeHorizon // 1000``), which bounds both plot size and memory.
|
|
43
|
+
Each returned series therefore has ``len(times)`` entries, not
|
|
44
|
+
``timeHorizon``.
|
|
45
|
+
|
|
46
|
+
The oracle's score is averaged across its replicates *before* the
|
|
47
|
+
subtraction, so the result is regret against mean oracle performance
|
|
48
|
+
rather than a paired difference per replicate.
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
median = []
|
|
52
|
+
mean = []
|
|
53
|
+
quantile1 = []
|
|
54
|
+
quantile2 = []
|
|
55
|
+
quantile3 = []
|
|
56
|
+
quantile4 = []
|
|
57
|
+
nbAlgs = len(dump_scores) - 1
|
|
58
|
+
|
|
59
|
+
#Downsample the times, especially in case timeHorizon is huge.
|
|
60
|
+
skip = max(1, (timeHorizon // 1000))
|
|
61
|
+
times = [t for t in range(0,timeHorizon,skip)]
|
|
62
|
+
|
|
63
|
+
#file_oracle = open(dump_scores[-1], 'rb')
|
|
64
|
+
#scores_oracle = pickle.load(file_oracle)
|
|
65
|
+
# Comment the following line for BatchMabs:
|
|
66
|
+
#scores_oracle = scores_oracle[0]
|
|
67
|
+
#file_oracle.close()
|
|
68
|
+
|
|
69
|
+
data_o = []
|
|
70
|
+
for oracle_file in dump_scores[-1]:
|
|
71
|
+
with open(oracle_file, 'rb') as file:
|
|
72
|
+
scores_oi = pickle.load(file)
|
|
73
|
+
data_o.append([scores_oi[t] for t in times])
|
|
74
|
+
scores_oracle = np.mean(data_o, axis=0)
|
|
75
|
+
|
|
76
|
+
for j in range(nbAlgs):
|
|
77
|
+
data_j = []
|
|
78
|
+
for alg_file in dump_scores[j]:
|
|
79
|
+
with open(alg_file, 'rb') as file:
|
|
80
|
+
scores_ij = pickle.load(file)
|
|
81
|
+
data_j.append([scores_oracle[k] - scores_ij[t] for k, t in enumerate(times)])
|
|
82
|
+
|
|
83
|
+
filename = f"{root_folder}regret_{envName}_{names[j]}_{timeHorizon}_{j}_{time.time()}"
|
|
84
|
+
with open(filename, 'wb') as out_file:
|
|
85
|
+
pickle.dump(data_j, out_file)
|
|
86
|
+
|
|
87
|
+
mean.append(np.mean(data_j, axis=0))
|
|
88
|
+
median.append(np.quantile(data_j, 0.5, axis=0))
|
|
89
|
+
quantile1.append(np.quantile(data_j, 0.1, axis=0))
|
|
90
|
+
quantile2.append(np.quantile(data_j, 0.25, axis=0))
|
|
91
|
+
quantile3.append(np.quantile(data_j, 0.75, axis=0))
|
|
92
|
+
quantile4.append(np.quantile(data_j, 0.9, axis=0))
|
|
93
|
+
|
|
94
|
+
return mean,median,quantile1,quantile2, quantile3, quantile4,times
|
|
95
|
+
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
|
|
2
|
+
|
|
3
|
+
import statrl.experiments.onerun as oR
|
|
4
|
+
import statrl.experiments.parallelruns as pR
|
|
5
|
+
import statrl.experiments.analyzeruns as aR
|
|
6
|
+
import statrl.experiments.plotruns as plR
|
|
7
|
+
from statrl.experiments.utils import clear_auxiliaryfiles
|
|
8
|
+
|
|
9
|
+
import time
|
|
10
|
+
import os
|
|
11
|
+
from typing import Any
|
|
12
|
+
ROOT="results/"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def runLargeMulticoreExperiment(env: Any, agents: list[Any], oracle: Any, interact: Any, timeHorizon: int=1000, nbReplicates: int=100, root_folder: str=ROOT) -> None:
|
|
16
|
+
"""Benchmark several agents on one environment and plot their regret.
|
|
17
|
+
|
|
18
|
+
For each agent it runs ``nbReplicates`` independent interactions in parallel, runs the oracle for
|
|
19
|
+
the same number, computes regret as the oracle's cumulative score minus each agent's, and writes
|
|
20
|
+
a logfile and regret figures under ``root_folder``.
|
|
21
|
+
|
|
22
|
+
Parameters
|
|
23
|
+
----------
|
|
24
|
+
env : object
|
|
25
|
+
Environment to benchmark on. Must expose ``name``; an optional
|
|
26
|
+
``displayname`` is used as the figure title when present.
|
|
27
|
+
agents : list of object
|
|
28
|
+
Agents to compare. Their ``name`` attributes must be distinct — dump
|
|
29
|
+
filenames and plot legends are keyed on them, so duplicates silently
|
|
30
|
+
merge two agents' results.
|
|
31
|
+
oracle : object
|
|
32
|
+
Reference agent defining zero regret, and the only one required to
|
|
33
|
+
expose a ``policy`` (it is written to the logfile). Must belong to the
|
|
34
|
+
same setting as ``agents``.
|
|
35
|
+
interact : statrl.experiments.onerun.Interaction
|
|
36
|
+
Interaction loop of the setting, shared by every agent in the run.
|
|
37
|
+
timeHorizon : int, default=1000
|
|
38
|
+
Number of rounds per interaction.
|
|
39
|
+
nbReplicates : int, default=100
|
|
40
|
+
Number of independent runs per agent. Regret quantiles are taken
|
|
41
|
+
across these, so a handful of replicates gives a very rough band.
|
|
42
|
+
root_folder : str, default='results/'
|
|
43
|
+
Output directory, created if absent. Must end with a separator.
|
|
44
|
+
|
|
45
|
+
Returns
|
|
46
|
+
-------
|
|
47
|
+
None
|
|
48
|
+
Everything is written to disk. ``root_folder`` receives a
|
|
49
|
+
``logfile_*.txt``, one ``regret_*`` pickle per agent, and the figures
|
|
50
|
+
``Regrets_*.png`` / ``.pdf`` in linear and log-y scale. The
|
|
51
|
+
intermediate ``aux_*`` dumps are deleted on the way out.
|
|
52
|
+
|
|
53
|
+
See Also
|
|
54
|
+
--------
|
|
55
|
+
statrl.experiments.parallelruns.multicoreRuns : The parallel layer underneath.
|
|
56
|
+
statrl.experiments.analyzeruns.computeScoreDiffs : Turns the dumps into regret statistics.
|
|
57
|
+
statrl.experiments.plotruns.plotScoreDiffs : Draws the figures.
|
|
58
|
+
|
|
59
|
+
Notes
|
|
60
|
+
-----
|
|
61
|
+
Cost grows as ``(len(agents) + 1) * nbReplicates * timeHorizon``. Start
|
|
62
|
+
small — the defaults already amount to 100 000 rounds per agent.
|
|
63
|
+
|
|
64
|
+
Examples
|
|
65
|
+
--------
|
|
66
|
+
>>> from statrl.settings.bandits.stochastic.anytime.envs.parametric import BernoulliBandit
|
|
67
|
+
>>> from statrl.settings.bandits.stochastic.anytime.agents.IMED import IMED
|
|
68
|
+
>>> from statrl.settings.bandits.stochastic.anytime.agents._Oracle import Oracle
|
|
69
|
+
>>> from statrl.settings.bandits.stochastic.anytime.agents._Random import Random
|
|
70
|
+
>>> from statrl.settings.bandits.stochastic.anytime.interaction import BanditInteraction
|
|
71
|
+
>>> from statrl.settings.utils import klBern
|
|
72
|
+
>>> env = BernoulliBandit([0.2, 0.9, 0.5]) # doctest: +SKIP
|
|
73
|
+
>>> runLargeMulticoreExperiment( # doctest: +SKIP
|
|
74
|
+
... env,
|
|
75
|
+
... agents=[IMED(env.number_arms, klBern), Random(env)],
|
|
76
|
+
... oracle=Oracle(env),
|
|
77
|
+
... interact=BanditInteraction(),
|
|
78
|
+
... timeHorizon=1000, nbReplicates=50,
|
|
79
|
+
... )
|
|
80
|
+
"""
|
|
81
|
+
os.makedirs(root_folder, exist_ok=True)
|
|
82
|
+
|
|
83
|
+
envName = env.name
|
|
84
|
+
learners = agents
|
|
85
|
+
|
|
86
|
+
print("-"*30+"Massive Multicore Experiment"+"-"*30)
|
|
87
|
+
print(f'Environment: {envName}')
|
|
88
|
+
print(f'Learners: {[learner.name for learner in learners]}')
|
|
89
|
+
print(f'[INFO] Run {nbReplicates} many interactions of length {timeHorizon} for each learner:')
|
|
90
|
+
dump_scores = []
|
|
91
|
+
names = []
|
|
92
|
+
meanelapsedtimes = []
|
|
93
|
+
|
|
94
|
+
for learner in learners:
|
|
95
|
+
names.append(learner.name)
|
|
96
|
+
dump_scores_learner, meanelapsedtime_learner = pR.multicoreRuns(env, learner, interact, nbReplicates, timeHorizon, oR.oneRunWithDump, root_folder=root_folder)
|
|
97
|
+
dump_scores.append(dump_scores_learner)
|
|
98
|
+
meanelapsedtimes.append(meanelapsedtime_learner)
|
|
99
|
+
|
|
100
|
+
dump_scoresopt, meanelapsedtime = pR.multicoreRuns(env, oracle, interact, nbReplicates, timeHorizon,
|
|
101
|
+
oR.oneRunWithDump, root_folder=root_folder)
|
|
102
|
+
dump_scores.append(dump_scoresopt)
|
|
103
|
+
|
|
104
|
+
## Report statistics and compute regret:
|
|
105
|
+
timestamp = str(time.time())
|
|
106
|
+
logfilename = f"{root_folder}logfile_{envName}_{timestamp}.txt"
|
|
107
|
+
with open(logfilename, 'w') as logfile:
|
|
108
|
+
logfile.write("Environment " + envName + "\n")
|
|
109
|
+
logfile.write("Optimal policy is: " + str(oracle.policy) + "\n")
|
|
110
|
+
logfile.write("Learners " + str([learner.name for learner in learners]) + "\n")
|
|
111
|
+
logfile.write("Time horizon is " + str(timeHorizon) + ", nb of replicates is " + str(nbReplicates) + "\n")
|
|
112
|
+
for name, meanelapsedtime in zip(names, meanelapsedtimes):
|
|
113
|
+
logfile.write(f"{name} average runtime is {meanelapsedtime}\n")
|
|
114
|
+
print("[INFO] A log-file has been generated in ", logfilename)
|
|
115
|
+
print("[INFO] Compute Statistics...")
|
|
116
|
+
mean, median, quantile1, quantile2,quantile3,quantile4, times = aR.computeScoreDiffs(names, dump_scores, timeHorizon, envName, root_folder=root_folder)
|
|
117
|
+
print("[INFO] Plot results...")
|
|
118
|
+
|
|
119
|
+
title = env.displayname if hasattr(env, "displayname") else envName
|
|
120
|
+
labelx,labely = interact.plotlabels
|
|
121
|
+
plR.plotScoreDiffs(names, envName, (title,labelx,labely), mean, median, quantile1, quantile2,quantile3,quantile4, times, timeHorizon, logfile=logfile, timestamp=timestamp, root_folder=root_folder)
|
|
122
|
+
print("[INFO] Clean Auxiliary files...")
|
|
123
|
+
clear_auxiliaryfiles(env, root_folder)
|
|
124
|
+
print("[INFO] Massive multicore experiment successfully completed.")
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
|
|
2
|
+
from statrl.experiments.utils import dump
|
|
3
|
+
|
|
4
|
+
import time
|
|
5
|
+
|
|
6
|
+
from abc import ABC, abstractmethod
|
|
7
|
+
from typing import Any
|
|
8
|
+
import numpy as np
|
|
9
|
+
|
|
10
|
+
class Interaction(ABC):
|
|
11
|
+
"""Base class for the interaction loop of a setting.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
@abstractmethod
|
|
15
|
+
def run(self, env: Any, learner: Any, horizon: int)-> np.ndarray:
|
|
16
|
+
"""Run one interaction and return its cumulative expected score.
|
|
17
|
+
|
|
18
|
+
Parameters
|
|
19
|
+
----------
|
|
20
|
+
env : object
|
|
21
|
+
Environment of the setting. Reset by the implementation.
|
|
22
|
+
learner : object
|
|
23
|
+
Agent of the setting. Reset by the implementation, so one instance
|
|
24
|
+
can serve many replicates.
|
|
25
|
+
horizon : int
|
|
26
|
+
Number of rounds to play.
|
|
27
|
+
|
|
28
|
+
Returns
|
|
29
|
+
-------
|
|
30
|
+
ndarray of shape (horizon,)
|
|
31
|
+
Cumulative *expected* reward. Implementations must return exactly
|
|
32
|
+
``horizon`` entries — :func:`oneRunWithDump` asserts it.
|
|
33
|
+
"""
|
|
34
|
+
return np.array([])
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@abstractmethod
|
|
38
|
+
def renderrun(self, env: Any, learner: Any, horizon: int) -> None:
|
|
39
|
+
"""Run one interaction with rendering enabled, returning no score.
|
|
40
|
+
|
|
41
|
+
Parameters
|
|
42
|
+
----------
|
|
43
|
+
env : object
|
|
44
|
+
Environment of the setting; the implementation attaches renderers.
|
|
45
|
+
learner : object
|
|
46
|
+
Agent of the setting.
|
|
47
|
+
horizon : int
|
|
48
|
+
Number of rounds to play.
|
|
49
|
+
"""
|
|
50
|
+
...
|
|
51
|
+
|
|
52
|
+
@property
|
|
53
|
+
@abstractmethod
|
|
54
|
+
def plotlabels(self) -> tuple[str, str]:
|
|
55
|
+
"""tuple of (str, str): Axis labels ``(x, y)`` for the regret plots.
|
|
56
|
+
|
|
57
|
+
Read by
|
|
58
|
+
:func:`~statrl.experiments.massiveruns.runLargeMulticoreExperiment`
|
|
59
|
+
and forwarded to
|
|
60
|
+
:func:`~statrl.experiments.plotruns.plotScoreDiffs`. Settings whose
|
|
61
|
+
rounds are not time steps override it — the batch setting labels its
|
|
62
|
+
x-axis by episode.
|
|
63
|
+
"""
|
|
64
|
+
raise NotImplementedError
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def oneRunWithDump(env: Any, learner: Any, interact: Any, timeHorizon: int, root_folder: str) -> str:
|
|
70
|
+
"""Run one replicate and pickle its score series to disk.
|
|
71
|
+
|
|
72
|
+
The unit of work handed to :mod:`joblib` by
|
|
73
|
+
:func:`~statrl.experiments.parallelruns.multicoreRuns`. Results travel
|
|
74
|
+
back through the filesystem rather than through the return value, because
|
|
75
|
+
returning full score series from every worker would serialize
|
|
76
|
+
``nbReplicates x timeHorizon`` floats through the process pool.
|
|
77
|
+
|
|
78
|
+
Parameters
|
|
79
|
+
----------
|
|
80
|
+
env : object
|
|
81
|
+
Environment for this replicate; already a private deep copy.
|
|
82
|
+
learner : object
|
|
83
|
+
Agent for this replicate; already a private deep copy.
|
|
84
|
+
interact : Interaction
|
|
85
|
+
The interaction loop of the setting.
|
|
86
|
+
timeHorizon : int
|
|
87
|
+
Number of rounds to play.
|
|
88
|
+
root_folder : str
|
|
89
|
+
Directory the dump is written to. Must end with a separator, and must
|
|
90
|
+
already exist.
|
|
91
|
+
|
|
92
|
+
Returns
|
|
93
|
+
-------
|
|
94
|
+
str
|
|
95
|
+
Path of the pickle written, of the form
|
|
96
|
+
``{root_folder}aux_{env}_scores_{agent}_{horizon}_{timestamp}``. The
|
|
97
|
+
``aux_`` prefix is what
|
|
98
|
+
:func:`~statrl.experiments.utils.clear_auxiliaryfiles` deletes once
|
|
99
|
+
the statistics have been computed.
|
|
100
|
+
|
|
101
|
+
Raises
|
|
102
|
+
------
|
|
103
|
+
AssertionError
|
|
104
|
+
If the interaction returned fewer or more than ``timeHorizon`` scores.
|
|
105
|
+
"""
|
|
106
|
+
scoretimeseries=interact.run(env,learner,timeHorizon)
|
|
107
|
+
assert len(scoretimeseries) == timeHorizon
|
|
108
|
+
|
|
109
|
+
tag = f"{env.name}_scores_{learner.name}_{timeHorizon}_{time.time()}"
|
|
110
|
+
filename = dump(scoretimeseries,"aux",tag,root_folder)
|
|
111
|
+
return filename
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import time
|
|
2
|
+
import copy
|
|
3
|
+
from joblib import Parallel, delayed
|
|
4
|
+
from typing import Any, Callable
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
## Parallelization
|
|
8
|
+
def multicoreRuns(env: Any, learner: Any, interact: Any, nbReplicates: int, timeHorizon: int, oneRunFunction: Callable[..., Any], root_folder: str) -> tuple[Any, float]:
|
|
9
|
+
"""Run one agent for many independent replicates, spread across CPU cores.
|
|
10
|
+
|
|
11
|
+
Each replicate gets its own deep copy of the environment, agent, and
|
|
12
|
+
interaction.
|
|
13
|
+
|
|
14
|
+
Parameters
|
|
15
|
+
----------
|
|
16
|
+
env : object
|
|
17
|
+
Environment to replicate.
|
|
18
|
+
learner : object
|
|
19
|
+
Agent to replicate.
|
|
20
|
+
interact : statrl.experiments.onerun.Interaction
|
|
21
|
+
Interaction loop of the setting.
|
|
22
|
+
nbReplicates : int
|
|
23
|
+
Number of independent runs.
|
|
24
|
+
timeHorizon : int
|
|
25
|
+
Number of rounds per run.
|
|
26
|
+
oneRunFunction : callable
|
|
27
|
+
Function executing one replicate, called as
|
|
28
|
+
``oneRunFunction(env, learner, interact, timeHorizon, root_folder)``.
|
|
29
|
+
In practice :func:`~statrl.experiments.onerun.oneRunWithDump`.
|
|
30
|
+
root_folder : str
|
|
31
|
+
Directory the per-replicate dumps are written to.
|
|
32
|
+
|
|
33
|
+
Returns
|
|
34
|
+
-------
|
|
35
|
+
scores : list of str
|
|
36
|
+
One dump filename per replicate, in the order the jobs were created.
|
|
37
|
+
elapsed : float
|
|
38
|
+
Mean wall-clock seconds per replicate. Since the runs are concurrent
|
|
39
|
+
this is total elapsed time divided by ``nbReplicates``, so it measures
|
|
40
|
+
throughput rather than the cost of a single run.
|
|
41
|
+
|
|
42
|
+
Notes
|
|
43
|
+
-----
|
|
44
|
+
Uses all available cores (``n_jobs=-1``). Everything passed in must be
|
|
45
|
+
picklable, which is why
|
|
46
|
+
:class:`~statrl.settings.bandits.stochastic.batch.environment.BatchMAB` accepts a
|
|
47
|
+
plain list of batch sizes rather than only a callable.
|
|
48
|
+
"""
|
|
49
|
+
#FIXME Should be made more general? indep of gymnasium?
|
|
50
|
+
#envs.append(gymnasium.make(envRegisterName).unwrapped)
|
|
51
|
+
jobs = [
|
|
52
|
+
(copy.deepcopy(env), copy.deepcopy(learner), copy.deepcopy(interact), timeHorizon, root_folder)
|
|
53
|
+
for _ in range(nbReplicates)
|
|
54
|
+
]
|
|
55
|
+
|
|
56
|
+
t0 = time.time()
|
|
57
|
+
scores = Parallel(n_jobs=-1)(delayed(oneRunFunction)(*job) for job in jobs)
|
|
58
|
+
elapsed = time.time() - t0
|
|
59
|
+
|
|
60
|
+
return scores, elapsed / nbReplicates
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
|
|
2
|
+
import pylab as pl
|
|
3
|
+
import sys
|
|
4
|
+
from typing import Any
|
|
5
|
+
import numpy as np
|
|
6
|
+
|
|
7
|
+
ROOT= "results/"
|
|
8
|
+
def plotScoreDiffs(learnersName: list[str], envName: str, title, mean: list[np.ndarray], median: list[np.ndarray], quantile1: list[np.ndarray], quantile2: list[np.ndarray],quantile3: list[np.ndarray],quantile4: list[np.ndarray], times: list[int], timeHorizon: int, logfile: Any='', timestamp: Any=0, root_folder: str=ROOT) -> None:
|
|
9
|
+
"""Draw the regret figures and record final regrets in the logfile.
|
|
10
|
+
|
|
11
|
+
Each agent gets a mean curve with markers, a dashed median, and two nested
|
|
12
|
+
shaded bands (0.1-0.9 and 0.25-0.75).
|
|
13
|
+
|
|
14
|
+
Parameters
|
|
15
|
+
----------
|
|
16
|
+
learnersName : list of str
|
|
17
|
+
Agent names, used as legend labels and in the output filename.
|
|
18
|
+
envName : str
|
|
19
|
+
Environment name, used in the output filename.
|
|
20
|
+
title : tuple of (str, str, str)
|
|
21
|
+
``(figure_title, xlabel, ylabel)``. The two labels come from
|
|
22
|
+
:attr:`~statrl.experiments.onerun.Interaction.plotlabels`.
|
|
23
|
+
mean, median : list of ndarray
|
|
24
|
+
Per-agent mean and median regret over time.
|
|
25
|
+
quantile1, quantile2, quantile3, quantile4 : list of ndarray
|
|
26
|
+
Per-agent regret quantiles at levels 0.1, 0.25, 0.75, and 0.9.
|
|
27
|
+
times : list of int
|
|
28
|
+
Sampled time steps, shared by every series.
|
|
29
|
+
timeHorizon : int
|
|
30
|
+
Horizon of the runs, used for the x-limit and in the filename.
|
|
31
|
+
logfile : file-like, default=''
|
|
32
|
+
Where final regrets are written. The default empty string selects
|
|
33
|
+
:data:`sys.stdout`.
|
|
34
|
+
timestamp : str, default=0
|
|
35
|
+
Suffix making the filename unique across runs.
|
|
36
|
+
root_folder : str, default='results/'
|
|
37
|
+
Output directory. Must end with a separator.
|
|
38
|
+
|
|
39
|
+
Returns
|
|
40
|
+
-------
|
|
41
|
+
None
|
|
42
|
+
Writes ``Regrets_<agents>_<horizon>_<env>_<timestamp>`` as ``.png``
|
|
43
|
+
and ``.pdf``, plus an ``_ylog`` pair with a logarithmic ``y`` axis.
|
|
44
|
+
|
|
45
|
+
Notes
|
|
46
|
+
-----
|
|
47
|
+
Colours and markers cycle after 9 and 5 agents respectively, so beyond
|
|
48
|
+
that two agents become hard to tell apart.
|
|
49
|
+
"""
|
|
50
|
+
if (logfile==''):
|
|
51
|
+
logfile=sys.stdout
|
|
52
|
+
nbFigure = pl.gcf().number+1
|
|
53
|
+
pl.figure(nbFigure)
|
|
54
|
+
fig, ax = pl.subplots(layout="constrained")
|
|
55
|
+
textfile = root_folder+"Regrets_"
|
|
56
|
+
#colors= ['black', 'blue','gray', 'green', 'red']#['black', 'purple', 'blue','cyan','yellow', 'orange', 'red', 'chocolate']
|
|
57
|
+
colors = ['#377eb8', '#ff7f00', '#4daf4a',
|
|
58
|
+
'#f781bf', '#a65628', '#984ea3',
|
|
59
|
+
'#999999', '#e41a1c', '#dede00']
|
|
60
|
+
|
|
61
|
+
style = ['o','v','s','d','<']
|
|
62
|
+
m,M=0,0
|
|
63
|
+
|
|
64
|
+
fig_title,fig_xlabel,fig_ylabel=title
|
|
65
|
+
|
|
66
|
+
ax.set_title(fig_title)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
for i in range(len(median)):
|
|
70
|
+
m=min(m,min(quantile1[i]),min(mean[i]))
|
|
71
|
+
M=1.1*max(M,max(quantile4[i]),max(mean[i]))
|
|
72
|
+
ax.fill_between(
|
|
73
|
+
times,
|
|
74
|
+
quantile1[i],
|
|
75
|
+
quantile4[i],
|
|
76
|
+
color=colors[i% len(colors)],
|
|
77
|
+
alpha=0.18,
|
|
78
|
+
linewidth=0
|
|
79
|
+
)
|
|
80
|
+
ax.fill_between(
|
|
81
|
+
times,
|
|
82
|
+
quantile2[i],
|
|
83
|
+
quantile3[i],
|
|
84
|
+
color=colors[i % len(colors)],
|
|
85
|
+
alpha=0.18,
|
|
86
|
+
linewidth=0
|
|
87
|
+
)
|
|
88
|
+
ax.plot(
|
|
89
|
+
times,
|
|
90
|
+
median[i],
|
|
91
|
+
color=colors[i% len(colors)],
|
|
92
|
+
alpha=0.6,
|
|
93
|
+
linewidth=1.8,
|
|
94
|
+
linestyle='--'
|
|
95
|
+
)
|
|
96
|
+
ax.plot(
|
|
97
|
+
times,
|
|
98
|
+
mean[i],
|
|
99
|
+
style[i % len(style)],
|
|
100
|
+
markevery=0.15,
|
|
101
|
+
markersize=8,
|
|
102
|
+
color=colors[i% len(colors)],
|
|
103
|
+
linewidth=2.3,
|
|
104
|
+
linestyle='-',
|
|
105
|
+
label=learnersName[i]
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
#pl.plot(times, mean[i], style[i% len(style)], label=learnersName[i], color=colors[i % len(colors)], linewidth=2.0, linestyle='-.', markevery=0.05)
|
|
109
|
+
#pl.plot(times, median[i], style[i% len(style)], color=colors[i % len(colors)], linewidth=2.0, linestyle='--', markevery=0.05)
|
|
110
|
+
#pl.plot(times,quantile1[i], color=colors[i % len(colors)],linestyle=':',linewidth=0.6)
|
|
111
|
+
#pl.plot(times,quantile2[i], color=colors[i % len(colors)],linestyle=':',linewidth=0.6)
|
|
112
|
+
|
|
113
|
+
textfile += learnersName[i] + "_"
|
|
114
|
+
logfile.write(learnersName[i] + ' has regret ' + str(median[i][-1]) + ' after ' + str(timeHorizon) + ' time steps with quantiles ' +
|
|
115
|
+
str(quantile1[i][-1]) +' and '+ str(quantile2[i][-1])+"\n")
|
|
116
|
+
|
|
117
|
+
textfile+="_"+str(timeHorizon)+"_"+envName+"_"+timestamp
|
|
118
|
+
#fig.tight_layout()
|
|
119
|
+
ax.legend(loc=2)
|
|
120
|
+
|
|
121
|
+
ax.set_xlabel(fig_xlabel, fontsize=13, fontname = "Arial")
|
|
122
|
+
ax.set_ylabel(fig_ylabel, fontsize=13, fontname = "Arial")
|
|
123
|
+
|
|
124
|
+
ax.set_xlim(0,min(timeHorizon,len(mean[0]))-1)
|
|
125
|
+
#pl.xticks(times)
|
|
126
|
+
ax.ticklabel_format(axis='both', useMathText = True, useOffset = True, style='sci', scilimits=(0, 0))
|
|
127
|
+
ax.set_ylim([m,M])
|
|
128
|
+
fig.savefig(textfile+'.png')
|
|
129
|
+
fig.savefig(textfile+ '.pdf')
|
|
130
|
+
# pl.xscale('log')
|
|
131
|
+
# pl.savefig(textfile + '_xlog.png')
|
|
132
|
+
# pl.savefig(textfile + '_xlog.pdf')
|
|
133
|
+
# pl.ylim(1)
|
|
134
|
+
#if(timeHorizon>10):
|
|
135
|
+
ax.set_xscale('linear')
|
|
136
|
+
ax.set_yscale('log')
|
|
137
|
+
ax.set_ylim([max(m,1e-0),max(M,2e-0)])
|
|
138
|
+
fig.savefig(textfile + '_ylog.png')
|
|
139
|
+
fig.savefig(textfile + '_ylog.pdf')
|
|
140
|
+
fig.savefig(root_folder+"Regrets" + str(timeHorizon)+"_"+envName+'.pdf')
|
|
141
|
+
# pl.xscale('log')
|
|
142
|
+
# pl.savefig(textfile + '_loglog.png')
|
|
143
|
+
# pl.savefig(textfile + '_loglog.pdf')
|
|
144
|
+
logfile.write("\nPlots are depicted in files "+textfile + ".pdf/png, etc.")
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import importlib.util
|
|
2
|
+
import os
|
|
3
|
+
import pickle
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
import yaml
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def dump(values: Any, filename: str, tag: str, root_folder: str) -> str:
|
|
11
|
+
"""Pickle a value to ``{root_folder}{filename}_{tag}``.
|
|
12
|
+
|
|
13
|
+
Parameters
|
|
14
|
+
----------
|
|
15
|
+
values : object
|
|
16
|
+
Anything picklable; in practice a score time series.
|
|
17
|
+
filename : str
|
|
18
|
+
Filename prefix. ``"aux"`` marks a file
|
|
19
|
+
:func:`clear_auxiliaryfiles` may later delete.
|
|
20
|
+
tag : str
|
|
21
|
+
Suffix identifying the run, built from the environment, agent,
|
|
22
|
+
horizon, and a timestamp.
|
|
23
|
+
root_folder : str
|
|
24
|
+
Output directory. Must end with a separator and already exist.
|
|
25
|
+
|
|
26
|
+
Returns
|
|
27
|
+
-------
|
|
28
|
+
str
|
|
29
|
+
Path of the file written.
|
|
30
|
+
"""
|
|
31
|
+
filenameM = f"{root_folder}{filename}_{tag}"
|
|
32
|
+
with open(filenameM, 'wb') as file:
|
|
33
|
+
pickle.dump(values, file)
|
|
34
|
+
return filenameM
|
|
35
|
+
|
|
36
|
+
def clear_auxiliaryfiles(env: Any, root_folder: str) -> None:
|
|
37
|
+
"""Delete the intermediate per-replicate dumps of one environment.
|
|
38
|
+
|
|
39
|
+
Removes every file in ``root_folder`` whose name starts with
|
|
40
|
+
``aux_{env.name}``, i.e. the per-replicate score series, once the regret
|
|
41
|
+
statistics have been computed from them. The regret pickles, logfile, and
|
|
42
|
+
figures are kept.
|
|
43
|
+
|
|
44
|
+
Parameters
|
|
45
|
+
----------
|
|
46
|
+
env : object
|
|
47
|
+
Environment whose dumps to remove, identified by its ``name``.
|
|
48
|
+
root_folder : str
|
|
49
|
+
Directory to clean. Must end with a separator.
|
|
50
|
+
|
|
51
|
+
Warnings
|
|
52
|
+
--------
|
|
53
|
+
Deletes files unconditionally. Anything of yours in ``root_folder`` named
|
|
54
|
+
``aux_{env.name}*`` will be removed too.
|
|
55
|
+
"""
|
|
56
|
+
for file in os.listdir(root_folder):
|
|
57
|
+
if file.startswith("aux_" + env.name):
|
|
58
|
+
os.remove(root_folder + file)
|
|
59
|
+
|
|
60
|
+
#def load(filename):
|
|
61
|
+
# with open(filename, 'r') as file:
|
|
62
|
+
# return yaml.safe_load(file)
|
|
63
|
+
def load(filename: str) -> dict:
|
|
64
|
+
"""Read an ``environments.yaml`` registry.
|
|
65
|
+
|
|
66
|
+
Each entry maps an environment name to a spec with an ``entrypoint`` and
|
|
67
|
+
optional ``kwargs``. The spec's directory is recorded under ``_base_dir``
|
|
68
|
+
so :func:`make` can resolve the entrypoint relative to the YAML file
|
|
69
|
+
rather than the working directory.
|
|
70
|
+
|
|
71
|
+
Parameters
|
|
72
|
+
----------
|
|
73
|
+
filename : str or path-like
|
|
74
|
+
Path to the YAML registry.
|
|
75
|
+
|
|
76
|
+
Returns
|
|
77
|
+
-------
|
|
78
|
+
dict
|
|
79
|
+
The parsed registry, each spec augmented with ``_base_dir``.
|
|
80
|
+
|
|
81
|
+
See Also
|
|
82
|
+
--------
|
|
83
|
+
make : Instantiates one of the returned specs.
|
|
84
|
+
"""
|
|
85
|
+
path = Path(filename)
|
|
86
|
+
|
|
87
|
+
with path.open("r") as f:
|
|
88
|
+
envs = yaml.safe_load(f)
|
|
89
|
+
|
|
90
|
+
for spec in envs.values():
|
|
91
|
+
spec["_base_dir"] = path.parent
|
|
92
|
+
|
|
93
|
+
return envs
|
|
94
|
+
|
|
95
|
+
def make(spec: dict) -> Any:
|
|
96
|
+
"""Instantiate an environment from a registry spec.
|
|
97
|
+
|
|
98
|
+
Imports the module named by the spec's ``entrypoint`` from the file it
|
|
99
|
+
sits next to and calls the class it names with the spec's ``kwargs``.
|
|
100
|
+
Loading by file path rather than by import name is what lets a setting's
|
|
101
|
+
``envs/`` directory be a plain folder, with no package or installation.
|
|
102
|
+
|
|
103
|
+
Parameters
|
|
104
|
+
----------
|
|
105
|
+
spec : dict
|
|
106
|
+
A registry entry as returned by :func:`load`, with keys
|
|
107
|
+
``entrypoint`` (``"module:Class"``), ``_base_dir``, and optionally
|
|
108
|
+
``kwargs`` and ``displayname``.
|
|
109
|
+
|
|
110
|
+
Returns
|
|
111
|
+
-------
|
|
112
|
+
object
|
|
113
|
+
The constructed environment, carrying a ``displayname`` attribute —
|
|
114
|
+
the spec's if given, otherwise its ``name``. That value becomes the
|
|
115
|
+
title of the regret figures.
|
|
116
|
+
|
|
117
|
+
Raises
|
|
118
|
+
------
|
|
119
|
+
ImportError
|
|
120
|
+
If the module named by the entrypoint cannot be loaded.
|
|
121
|
+
|
|
122
|
+
See Also
|
|
123
|
+
--------
|
|
124
|
+
load : Reads the registry this spec comes from.
|
|
125
|
+
"""
|
|
126
|
+
module_name, class_name = spec["entrypoint"].split(":")
|
|
127
|
+
kwargs = spec.get("kwargs", {})
|
|
128
|
+
|
|
129
|
+
module_path = Path(spec["_base_dir"]) / f"{module_name}.py"
|
|
130
|
+
|
|
131
|
+
spec_module = importlib.util.spec_from_file_location(module_name, module_path)
|
|
132
|
+
if spec_module is None or spec_module.loader is None:
|
|
133
|
+
raise ImportError(f"cannot load module {module_name} from {module_path}")
|
|
134
|
+
module = importlib.util.module_from_spec(spec_module)
|
|
135
|
+
spec_module.loader.exec_module(module)
|
|
136
|
+
|
|
137
|
+
cls = getattr(module, class_name)
|
|
138
|
+
env = cls(**kwargs)
|
|
139
|
+
env.displayname = spec["displayname"] if "displayname" in spec else env.name
|
|
140
|
+
return env
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|