diff --git a/.gitignore b/.gitignore
index 6959fd8..72e1822 100755
--- a/.gitignore
+++ b/.gitignore
@@ -122,4 +122,5 @@ data/
/.vs
runs/
logs/
-figures/
\ No newline at end of file
+figures/
+*.pickle
diff --git a/LICENSE.md b/LICENSE.md
new file mode 100644
index 0000000..553e30a
--- /dev/null
+++ b/LICENSE.md
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2020 Johann Brehmer, Sebastian Macaluso, Diccio Pappadopulo, and Kyle Cranmer
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/README.md b/README.md
index 77ce146..381b0fa 100644
--- a/README.md
+++ b/README.md
@@ -1,21 +1,45 @@
-# Hierarchical clustering in particle physics with reinforcement learning
+# Hierarchical clustering in particle physics through reinforcement learning
-## Getting started
+[Johann Brehmer](johann.brehmer@nyu.edu), [Sebastian Macaluso](sm4511@nyu.edu),
+[Duccio Pappadopulo](dpappadopulo@bloomberg.net), and [Kyle Cranmer](kyle.cranmer@nyu.edu)
-Need PyTorch + OpenAI gym + sacred with MongoDB backend
+[](https://arxiv.org/abs/2011.08191)
+[](https://ml4physicalsciences.github.io/2020/)
+[](https://opensource.org/licenses/MIT)
-## Running experiments
+Particle physics experiments often require the reconstruction of decay patterns through a hierarchical clustering of the observed final-state particles. We show that this task can be phrased as a Markov Decision Process and adapt reinforcement learning algorithms to solve it. In particular, we show that Monte-Carlo Tree Search guided by a neural policy can construct high-quality hierarchical clusterings and outperform established greedy and beam search baselines.
+Please see [our paper](https://arxiv.org/abs/2011.08191) for more details.
+
+### Getting started
+
+- A conda environment with all package dependencies can be installed with `conda env create -f environment.yml`
+- Ideally, also get a MongoDB to run and use OmniBoard to monitor experiments (though this is optional)
+- For the Ginkgo simulator, install [ToyJetsShower](https://github.com/johannbrehmer/ToyJetsShower) (`pip install -e .` works, it's missing the `pyro-ppl` dependency though)
+- For beam search and MLE estimates through the trellis, clone [ReclusterTreeAlgorithms](https://github.com/SebastianMacaluso/ReclusterTreeAlgorithms) and [hierarchical-trellis](https://github.com/iesl/hierarchical-trellis), and adapt the paths hard-coded in [evaluator.py](ginkgo_rl/eval/evaluator.py)
+
+
+### Running experiments
+
+To run individual experiments:
```
cd experiments
-./experiment.py with mcts_s # MCTS
-./experiment.py with lfd_s # Pure learning from demonstration
-./experiment.py with lfd_mcts_s # LfD policy in MCTS algorithm
+./experiment.py with truth # Ground truth
+./experiment.py with mle # Trellis MLE
./experiment.py with greedy_s # Greedy baseline
./experiment.py with beamsearch_s # Beam search baseline
-# For a full list of configurations, see experiments/config.py
+./experiment.py with mcts_s # MCTS
+./experiment.py with lfd_s # Pure learning from demonstration
+./experiment.py with lfd_mcts_s # BC policy in MCTS algorithm
+```
-# Monitor results live e.g. with Omniboard
-# Plot results with experiments/plot_results.ipynb
+For a full list of configurations, see the [experiments/config.py](experiments/config.py).
+
+To automate the whole process on a SLURM HPC system:
+```
+cd experiments/hpc
+sbatch --array 0-59 run.sh
```
+
+You can monitor the training live with Omniboard, and plot the final results with [experiments/plot_results.ipynb](experiments/plot_results.ipynb).
diff --git a/environment.yml b/environment.yml
new file mode 100644
index 0000000..b91a9b4
--- /dev/null
+++ b/environment.yml
@@ -0,0 +1,33 @@
+name: rl
+channels:
+ - pytorch
+ - conda-forge
+ - https://conda.anaconda.org/NLeSC
+ - defaults
+dependencies:
+ - jupyterlab
+ - line_profiler
+ - matplotlib>3.2
+ - nb_conda_kernels
+ - numpy>1.18
+ - pillow
+ - pip
+ - python=3.7.7
+ - pytorch=1.6.0
+ - requests
+ - scikit-learn
+ - sqlite
+ - torchvision=0.7.0
+ - tqdm
+ - werkzeug
+ - pip:
+ - atari-py
+ - black
+ - gym==0.17.2
+ - imageio
+ - pandas
+ - pyro-ppl
+ - pymongo==3.11.0
+ - sacred==0.8.1
+ - scipy
+ - stable-baselines==2.10.0
diff --git a/experiments/cluster_your_jets.ipynb b/experiments/cluster_your_jets.ipynb
new file mode 100644
index 0000000..4086e18
--- /dev/null
+++ b/experiments/cluster_your_jets.ipynb
@@ -0,0 +1,874 @@
+{
+ "cells": [
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "# Using an RL agent to cluster a given set of particles (BYOJ)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Johann Brehmer 2020"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## Preparations"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 1,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "import sys\n",
+ "import numpy as np\n",
+ "import logging\n",
+ "import torch\n",
+ "\n",
+ "sys.path.append(\"../\")\n",
+ "from ginkgo_rl import GinkgoRLInterface, PolicyMCTSAgent"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Let's read INFO-level output from the RL agent, but silence a few other libraries that like to produce lots of logging output. To check what's happening in more detail, use `logging.DEBUG` instead."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 2,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "logger = logging.getLogger(\"We\")\n",
+ "logging.basicConfig(format=\"%(asctime)s %(levelname).1s: %(message)s\", datefmt=\"%y-%m-%d %H:%M\")\n",
+ "\n",
+ "silence_list = [\"matplotlib\", \"showerSim\", \"hierarchical-trellis\"]\n",
+ "for key in logging.Logger.manager.loggerDict:\n",
+ " logging.getLogger(key).setLevel(logging.INFO)\n",
+ " for check_key in silence_list:\n",
+ " if check_key in key:\n",
+ " logging.getLogger(key).setLevel(logging.ERROR)\n",
+ " break\n",
+ " "
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## Set up GinkgoRLInterface instance"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "First, let's specify the setup for the environment and the agent. Note that these have to matched the setup in which the agent was trained.\n",
+ "\n",
+ "I'm working on documentation..."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 3,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "settings = {\n",
+ " \"n_max\": 20,\n",
+ " \"n_min\": 2,\n",
+ " \"w_jet\": True,\n",
+ " \"w_rate\": 3.0,\n",
+ " \"qcd_rate\": 1.5,\n",
+ " \"pt_min\": 4.0**2,\n",
+ " \"qcd_mass\": 30.0,\n",
+ " \"w_mass\": 80.0,\n",
+ " \"jet_momentum\": 400.0,\n",
+ " \"jetdir\": (1, 1, 1),\n",
+ " \"beamsize\": 20,\n",
+ " \"n_mc_target\": 2,\n",
+ " \"n_mc_max\": 50,\n",
+ " \"device\": torch.device(\"cpu\"),\n",
+ "}"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Path to the state dictionary of the RL agent."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 4,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "state_dict_filename = \"./data/runs/mcts_nn_m_20200930_092039_1010/model.pty\""
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "And finally..."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 5,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "grl = GinkgoRLInterface(state_dict_filename, **settings)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## If you don't have a jet, make one"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "The whole point of this interface is applying the RL agent to any tree of your choice. However, for convenience (and cross-checks), the `GinkgoRLInterface` class also provides a `generate` function that allows you to generate Ginkgo jets with the settings used."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 6,
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stderr",
+ "output_type": "stream",
+ "text": [
+ "20-12-15 12:56 I: Generating 3 jets\n",
+ "20-12-15 12:56 I: Done\n"
+ ]
+ }
+ ],
+ "source": [
+ "jets = grl.generate(n=3)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## Run clustering"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "`GinkgoRLInterface.cluster` is the main function to let the MCTS agent cluster the jets in a jet dictionary. These can be provided as a `dict` object (like we do here) or through the path of a pickled file.\n",
+ "\n",
+ "The function returns:\n",
+ "- the clustered jets (as a list of dict objects in the usual Ginkgo style)\n",
+ "- the overall log likelihoods of all reclustered jets\n",
+ "- the number of illegal actions that the RL algorithm picked (hopefully just zeros)\n",
+ "- the computational cost for each jet, measured as the number of evaluations of the splitting likelihood function\n",
+ "\n",
+ "If the keyword `filename` is used, the reclustered jets are also pickled to a file."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 7,
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stderr",
+ "output_type": "stream",
+ "text": [
+ "20-12-15 12:56 I: Clustering 3 jets\n",
+ "100%|██████████| 3/3 [02:00<00:00, 40.07s/it]\n",
+ "20-12-15 12:58 I: Saving clustered jets at reclustered_jets.pickle\n",
+ "20-12-15 12:58 I: Done\n"
+ ]
+ }
+ ],
+ "source": [
+ "clustered_jets, log_likelihoods, illegal_actions, costs = grl.cluster(jets, filename=\"reclustered_jets.pickle\")"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## Visualize results"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Let's visualize the true and clustered tree using Sebastian's code (make sure that the Ginkgo folder is in the PYTHONPATH)."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 8,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "from scripts import Tree1D_invM as Tree1D\n",
+ "\n",
+ "def visualize(jet_top, jet_bottom, label_top=\"Truth\", label_bottom=\"MCTS\"):\n",
+ " jet_top[\"node_id\"] = jet_top[\"root_id\"]\n",
+ " jet_bottom[\"node_id\"] = jet_bottom[\"root_id\"]\n",
+ " \n",
+ " tree1, tree2 = Tree1D.visualizeTreePair(\n",
+ " jet_top,\n",
+ " jet_bottom,\n",
+ " truthOrder=True, \n",
+ " label=True,\n",
+ " figFormat=\"jpg\",\n",
+ " )\n",
+ " tree1.attr(\n",
+ " rankdir='TB',\n",
+ " size=\"10\",\n",
+ " margin='0',\n",
+ " ratio=\"0.5\",\n",
+ " nodesep=\"0.01\",\n",
+ " label=label_top,\n",
+ " labelloc='t',\n",
+ " fontsize=\"36\"\n",
+ " )\n",
+ " tree2.attr(\n",
+ " rankdir='BT',\n",
+ " size=\"10\",\n",
+ " margin='0',\n",
+ " ratio=\"0.5\",\n",
+ " nodesep=\"0.01\",\n",
+ " label=label_bottom,\n",
+ " labelloc='b',\n",
+ " fontsize=\"36\"\n",
+ " )\n",
+ "\n",
+ " display(tree1)\n",
+ " display(tree2)\n"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 11,
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stderr",
+ "output_type": "stream",
+ "text": [
+ "Warning: node '12', graph '%3' size too small for label\n"
+ ]
+ },
+ {
+ "data": {
+ "image/svg+xml": [
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n"
+ ],
+ "text/plain": [
+ ""
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ },
+ {
+ "name": "stderr",
+ "output_type": "stream",
+ "text": [
+ "Warning: node '5', graph '%3' size too small for label\n"
+ ]
+ },
+ {
+ "data": {
+ "image/svg+xml": [
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n"
+ ],
+ "text/plain": [
+ ""
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ }
+ ],
+ "source": [
+ "visualize(jets[1], clustered_jets[1])"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "For comparison, here are `tree` and `content` fields from both jet dicts:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 26,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "def list_tree(jet, sort_by_energy=False):\n",
+ " data = list(enumerate(zip(jet[\"tree\"], jet[\"content\"])))\n",
+ " if sort_by_energy:\n",
+ " data = sorted(data, reverse=True, key=lambda x : x[1][1][0])\n",
+ " \n",
+ " for i, (children, momentum) in data:\n",
+ " logger.info(f\"i = {i:>2d}: children = {children}, p = {momentum}\")"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 27,
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stderr",
+ "output_type": "stream",
+ "text": [
+ "20-12-15 13:59 I: i = 0: children = [1 8], p = [407.92156109 230.94010768 230.94010768 230.94010768]\n",
+ "20-12-15 13:59 I: i = 8: children = [ 9 10], p = [349.57042955 199.3479812 197.89296817 207.79645725]\n",
+ "20-12-15 13:59 I: i = 10: children = [11 14], p = [246.45275334 141.46064359 139.48827859 145.64886977]\n",
+ "20-12-15 13:59 I: i = 11: children = [12 13], p = [210.39610406 120.76886713 118.59412674 124.78522421]\n",
+ "20-12-15 13:59 I: i = 12: children = [-1 -1], p = [126.34989637 72.02452326 71.20163054 75.50831308]\n",
+ "20-12-15 13:59 I: i = 9: children = [-1 -1], p = [103.1176915 57.88734634 58.40469824 62.14759657]\n",
+ "20-12-15 13:59 I: i = 13: children = [-1 -1], p = [84.04620769 48.74434386 47.39249619 49.27691112]\n",
+ "20-12-15 13:59 I: i = 1: children = [2 5], p = [58.35113154 31.59212647 33.04713951 23.14365043]\n",
+ "20-12-15 13:59 I: i = 2: children = [3 4], p = [41.82823887 31.69867118 22.42536816 13.00670542]\n",
+ "20-12-15 13:59 I: i = 14: children = [-1 -1], p = [36.05664928 20.69177646 20.89415185 20.86364556]\n",
+ "20-12-15 13:59 I: i = 4: children = [-1 -1], p = [31.8988728 23.9870116 18.17014735 10.51443981]\n",
+ "20-12-15 13:59 I: i = 5: children = [6 7], p = [16.52289467 -0.10654363 10.62177248 10.1369458 ]\n",
+ "20-12-15 13:59 I: i = 7: children = [-1 -1], p = [10.28785486 0.24507607 8.61276787 4.91484856]\n",
+ "20-12-15 13:59 I: i = 3: children = [-1 -1], p = [9.92936607 7.71165958 4.2552208 2.49226561]\n",
+ "20-12-15 13:59 I: i = 6: children = [-1 -1], p = [ 6.23504033 -0.35161971 2.00900495 5.22209757]\n"
+ ]
+ }
+ ],
+ "source": [
+ "list_tree(jets[1], sort_by_energy=True)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 28,
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stderr",
+ "output_type": "stream",
+ "text": [
+ "20-12-15 13:59 I: i = 14: children = [13 10], p = [407.92157889 230.94011747 230.94011779 230.94011787]\n",
+ "20-12-15 13:59 I: i = 10: children = [4 9], p = [349.57044484 199.34798992 197.89297682 207.79646634]\n",
+ "20-12-15 13:59 I: i = 9: children = [8 5], p = [246.45275334 141.46064359 139.48827859 145.64886977]\n",
+ "20-12-15 13:59 I: i = 5: children = [-1 -1], p = [126.34989637 72.02452326 71.20163054 75.50831308]\n",
+ "20-12-15 13:59 I: i = 8: children = [7 6], p = [120.10285697 69.43612032 68.28664804 70.14055668]\n",
+ "20-12-15 13:59 I: i = 4: children = [-1 -1], p = [103.1176915 57.88734634 58.40469824 62.14759657]\n",
+ "20-12-15 13:59 I: i = 6: children = [-1 -1], p = [84.04620769 48.74434386 47.39249619 49.27691112]\n",
+ "20-12-15 13:59 I: i = 13: children = [12 11], p = [58.35113405 31.59212755 33.04714097 23.14365154]\n",
+ "20-12-15 13:59 I: i = 11: children = [0 1], p = [41.82823887 31.69867118 22.42536816 13.00670542]\n",
+ "20-12-15 13:59 I: i = 7: children = [-1 -1], p = [36.05664928 20.69177646 20.89415185 20.86364556]\n",
+ "20-12-15 13:59 I: i = 1: children = [-1 -1], p = [31.8988728 23.9870116 18.17014735 10.51443981]\n",
+ "20-12-15 13:59 I: i = 12: children = [2 3], p = [16.52289519 -0.10654363 10.62177282 10.13694612]\n",
+ "20-12-15 13:59 I: i = 3: children = [-1 -1], p = [10.28785486 0.24507607 8.61276787 4.91484856]\n",
+ "20-12-15 13:59 I: i = 0: children = [-1 -1], p = [9.92936607 7.71165958 4.2552208 2.49226561]\n",
+ "20-12-15 13:59 I: i = 2: children = [-1 -1], p = [ 6.23504033 -0.35161971 2.00900495 5.22209757]\n"
+ ]
+ }
+ ],
+ "source": [
+ "list_tree(clustered_jets[1], sort_by_energy=True)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Finally, let's compare log likelihood numbers:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 29,
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stderr",
+ "output_type": "stream",
+ "text": [
+ "20-12-15 14:00 I: Jet 0: simulator log likelihood -112.9 vs MCTS log likelihood -107.6\n",
+ "20-12-15 14:00 I: Jet 1: simulator log likelihood -58.2 vs MCTS log likelihood -56.6\n",
+ "20-12-15 14:00 I: Jet 2: simulator log likelihood -112.6 vs MCTS log likelihood -109.7\n"
+ ]
+ }
+ ],
+ "source": [
+ "for i, (jet, log_likelihood) in enumerate(zip(jets, log_likelihoods)):\n",
+ " logger.info(f\"Jet {i}: simulator log likelihood {sum(jet['logLH']):>5.1f} vs MCTS log likelihood {log_likelihood:>5.1f}\")\n",
+ " "
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": []
+ }
+ ],
+ "metadata": {
+ "kernelspec": {
+ "display_name": "Python (rl)",
+ "language": "python",
+ "name": "rl"
+ },
+ "language_info": {
+ "codemirror_mode": {
+ "name": "ipython",
+ "version": 3
+ },
+ "file_extension": ".py",
+ "mimetype": "text/x-python",
+ "name": "python",
+ "nbconvert_exporter": "python",
+ "pygments_lexer": "ipython3",
+ "version": "3.7.7"
+ }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 4
+}
diff --git a/experiments/config.py b/experiments/config.py
index 25c2204..d221aa5 100644
--- a/experiments/config.py
+++ b/experiments/config.py
@@ -2,72 +2,71 @@
import datetime
from sacred import Experiment
from sacred.observers import FileStorageObserver, MongoObserver
+import logging
ex = Experiment(name="rl-ginkgo")
__all__ = ["ex", "config", "env_config", "agent_config", "train_config", "eval_config", "technical_config"]
+logger = logging.getLogger(__name__)
+
# noinspection PyUnusedLocal
@ex.config
def config():
- algorithm = "mcts"
- policy = "nn"
- teacher = "truth"
- env_type = "1d"
-
- if algorithm == "mcts":
- name = f"{algorithm}_{policy}"
- else:
- name = algorithm
- run_name = f"{name}_{datetime.datetime.now().strftime('%Y%m%d_%H%M%S')}"
-
- # Check config
- assert algorithm in ["mcts", "lfd", "lfd-mcts", "acer", "greedy", "random", "truth", "mle", "beamsearch"]
- if algorithm == "mcts":
- assert policy in ["nn", "random", "likelihood"]
- if algorithm in ["lfd", "lfd-mcts"]:
- assert teacher in ["truth", "mle"]
- assert env_type == "1d" # For now, 2d env is not supported
-
- # Set up observer
+ algorithm = "mcts" # {"mcts", "lfd", "lfd-mcts", "acer", "greedy", "random", "truth", "mle", "beamsearch"}
+ policy = "nn" # {"nn", "random", "likelihood"}
+ teacher = "truth" # {"truth", "mle"}
+
+ env_type = "1d" # for now, only {"1d"}
+ seed = 24927 # 1971248 was used for first round
+
+ name = f"{algorithm}_{policy}" if algorithm == "mcts" else algorithm
+ run_name = f"{name}_{datetime.datetime.now().strftime('%Y%m%d_%H%M%S')}_{seed}"
+
+ database = True
ex.observers.append(FileStorageObserver(f"./data/runs/{run_name}"))
- ex.observers.append(MongoObserver())
+ if database:
+ ex.observers.append(MongoObserver())
# noinspection PyUnusedLocal
@ex.config
def env_config():
- illegal_reward=-100.0
- illegal_actions_patience=3
+ illegal_reward = -100.0
+ illegal_actions_patience = 3
- n_max=20
- n_min=2
- n_target=1
+ n_max = 20
+ n_min = 2
+ n_target = 1
- min_reward=-100.0
- state_rescaling=0.01
- padding_value=-1.0
+ min_reward = -100.0
+ state_rescaling = 0.01
+ padding_value = -1.0
- w_jet=True
- w_rate=3.0
- qcd_rate=1.5
- pt_min=4.0 ** 2
- qcd_mass=30.0
- w_mass=80.0
- jet_momentum=400.0
- jetdir=(1, 1, 1)
- max_n_try=1000
+ w_jet = True
+ w_rate = 3.0
+ qcd_rate = 1.5
+ pt_min = 4.0 ** 2
+ qcd_mass = 30.0
+ w_mass = 80.0
+ jet_momentum = 400.0
+ jetdir = (1, 1, 1)
+ max_n_try = 1000
# noinspection PyUnusedLocal
@ex.config
def agent_config():
- initialize_mcts_with_beamsearch = True # TODO
- log_likelihood_policy_input = True # TODO
+ initialize_mcts_with_beamsearch = True
+ log_likelihood_policy_input = True
+ decision_mode = "max_reward" # {"max_reward", "max_puct", "mean_puct"}
- reward_range = (-500., 0.)
+ reward_range = (-500.0, 0.0)
history_length = None
- hidden_sizes = (100, 100,)
+ hidden_sizes = (
+ 100,
+ 100,
+ )
activation = torch.nn.ReLU()
@@ -79,7 +78,7 @@ def train_config():
pretrain_n_mc_min = 0
pretrain_n_mc_max = 10
pretrain_beamsize = 5
- pretrain_mcts_mode = "mean"
+ pretrain_planning_mode = "mean" # {"max", "mean"}, refers to PUCT
pretrain_c_puct = 1.0
train_steps = 10000
@@ -87,14 +86,15 @@ def train_config():
train_n_mc_min = 0
train_n_mc_max = 20
train_beamsize = 5
- train_mcts_mode = "mean"
+ train_planning_mode = "mean" # {"max", "mean"}, refers to PUCT
train_c_puct = 1.0
- imitation_steps = 500000
+ imitation_steps = 1000000
learning_rate = 1.0e-3
lr_decay = 0.01
weight_decay = 0.0
+ clip_gradient = None
# noinspection PyUnusedLocal
@@ -105,7 +105,7 @@ def eval_config():
eval_n_mc_max = 100
eval_beamsize = 10
- eval_mcts_mode = "mean"
+ eval_planning_mode = "mean"
eval_c_puct = 1.0
eval_jets = 500
@@ -119,7 +119,6 @@ def eval_config():
def technical_config():
device = torch.device("cpu")
dtype = torch.float
- seed = 24927 # 1971248 was used for first round
debug = False
debug_verbosity = 1
@@ -149,7 +148,7 @@ def debug_mcts():
def debug_lfd():
algorithm = "lfd"
name = "debug"
- debug = True
+ debug = False
imitation_steps = 10000
@@ -283,9 +282,23 @@ def mcts_l():
@ex.named_config
-def lfd_s():
+def lfd():
algorithm = "lfd"
- name = "lfd_s"
+ name = "lfd"
+
+
+@ex.named_config
+def lfd_mcts_xs():
+ algorithm = "lfd-mcts"
+ name = "lfd-mcts_xs"
+
+ train_beamsize = 3
+ train_n_mc_target = 1
+ train_n_mc_max = 10
+
+ eval_beamsize = 3
+ eval_n_mc_target = 1
+ eval_n_mc_max = 10
@ex.named_config
@@ -302,12 +315,151 @@ def lfd_mcts_s():
eval_n_mc_max = 20
+@ex.named_config
+def lfd_mcts_m():
+ algorithm = "lfd-mcts"
+ name = "lfd-mcts_m"
+
+ train_beamsize = 20
+ train_n_mc_target = 2
+ train_n_mc_max = 50
+
+ eval_beamsize = 20
+ eval_n_mc_target = 2
+ eval_n_mc_max = 50
+
+
+@ex.named_config
+def lfd_mcts_l():
+ algorithm = "lfd-mcts"
+ name = "lfd-mcts_l"
+
+ train_beamsize = 100
+ train_n_mc_target = 5
+ train_n_mc_max = 200
+
+ eval_beamsize = 100
+ eval_n_mc_target = 5
+ eval_n_mc_max = 200
+
+
+@ex.named_config
+def lfd_mleteacher():
+ algorithm = "lfd"
+ teacher = "mle"
+ name = "lfd_mleteacher"
+
+
+@ex.named_config
+def lfd_mcts_mleteacher_xs():
+ algorithm = "lfd-mcts"
+ teacher = "mle"
+ name = "lfd-mcts_mleteacher_xs"
+
+ train_beamsize = 3
+ train_n_mc_target = 1
+ train_n_mc_max = 10
+
+ eval_beamsize = 3
+ eval_n_mc_target = 1
+ eval_n_mc_max = 10
+
+
+@ex.named_config
+def lfd_mcts_mleteacher_s():
+ algorithm = "lfd-mcts"
+ teacher = "mle"
+ name = "lfd-mcts_mleteacher_s"
+
+ train_beamsize = 5
+ train_n_mc_target = 1
+ train_n_mc_max = 20
+
+ eval_beamsize = 5
+ eval_n_mc_target = 1
+ eval_n_mc_max = 20
+
+
+@ex.named_config
+def lfd_mcts_mleteacher_m():
+ algorithm = "lfd-mcts"
+ teacher = "mle"
+ name = "lfd-mcts_mleteacher_m"
+
+ train_beamsize = 20
+ train_n_mc_target = 2
+ train_n_mc_max = 50
+
+ eval_beamsize = 20
+ eval_n_mc_target = 2
+ eval_n_mc_max = 50
+
+
+@ex.named_config
+def lfd_mcts_mleteacher_l():
+ algorithm = "lfd-mcts"
+ teacher = "mle"
+ name = "lfd-mcts_mleteacher_l"
+
+ train_beamsize = 100
+ train_n_mc_target = 5
+ train_n_mc_max = 200
+
+ eval_beamsize = 100
+ eval_n_mc_target = 5
+ eval_n_mc_max = 200
+
+
@ex.named_config
def acer():
algorithm = "acer"
name = "acer"
+@ex.named_config
+def mcts_exploit():
+ algorithm = "mcts"
+ policy = "nn"
+ name = "mcts_s_exploit"
+
+ pretrain_beamsize = 3
+ pretrain_n_mc_target = 1
+ pretrain_n_mc_max = 10
+ pretrain_c_puct = 0.1
+
+ train_beamsize = 5
+ train_n_mc_target = 1
+ train_n_mc_max = 20
+ train_c_puct = 0.1
+
+ eval_beamsize = 5
+ eval_n_mc_target = 1
+ eval_n_mc_max = 20
+ eval_c_puct = 0.1
+
+
+@ex.named_config
+def mcts_explore():
+ algorithm = "mcts"
+ policy = "nn"
+ name = "mcts_s_explore"
+
+ pretrain_beamsize = 3
+ pretrain_n_mc_target = 1
+ pretrain_n_mc_max = 10
+ pretrain_c_puct = 10.0
+
+ train_beamsize = 5
+ train_n_mc_target = 1
+ train_n_mc_max = 20
+ train_c_puct = 10.0
+
+ eval_beamsize = 5
+ eval_n_mc_target = 1
+ eval_n_mc_max = 20
+ eval_c_puct = 10.0
+
+
@ex.named_config
def mcts_raw():
algorithm = "mcts"
@@ -328,6 +480,27 @@ def mcts_raw():
eval_beamsize = 5
+@ex.named_config
+def mcts_puct_decisions():
+ algorithm = "mcts"
+ policy = "nn"
+ name = "mcts_puct_decisions_s"
+
+ pretrain_beamsize = 3
+ pretrain_n_mc_target = 1
+ pretrain_n_mc_max = 10
+
+ train_n_mc_target = 1
+ train_n_mc_max = 20
+ train_beamsize = 5
+
+ eval_n_mc_target = 1
+ eval_n_mc_max = 20
+ eval_beamsize = 5
+
+ decision_mode = "mean_puct"
+
+
@ex.named_config
def mcts_no_beamsearch():
algorithm = "mcts"
diff --git a/experiments/debug/debug.py b/experiments/debug/debug.py
index f21f4b2..8f0a440 100644
--- a/experiments/debug/debug.py
+++ b/experiments/debug/debug.py
@@ -6,9 +6,7 @@
from ginkgo_rl import ImitationLearningPolicyMCTSAgent
logging.basicConfig(
- format='%(asctime)-5.5s %(name)-20.20s %(levelname)-7.7s %(message)s',
- datefmt='%H:%M',
- level=logging.DEBUG
+ format="%(asctime)-5.5s %(name)-20.20s %(levelname)-7.7s %(message)s", datefmt="%H:%M", level=logging.DEBUG
)
for key in logging.Logger.manager.loggerDict:
if "ginkgo_rl" not in key:
diff --git a/experiments/debug/line_profile.py b/experiments/debug/line_profile.py
index 110f50c..ed5bec4 100644
--- a/experiments/debug/line_profile.py
+++ b/experiments/debug/line_profile.py
@@ -13,9 +13,7 @@
if __name__ == "__main__":
# Logging setup
logging.basicConfig(
- format='%(asctime)-5.5s %(name)-20.20s %(levelname)-7.7s %(message)s',
- datefmt='%H:%M',
- level=logging.DEBUG
+ format="%(asctime)-5.5s %(name)-20.20s %(levelname)-7.7s %(message)s", datefmt="%H:%M", level=logging.DEBUG
)
for key in logging.Logger.manager.loggerDict:
if "ginkgo_rl" not in key:
diff --git a/experiments/experiment.py b/experiments/experiment.py
index ae1918b..42cb6c5 100755
--- a/experiments/experiment.py
+++ b/experiments/experiment.py
@@ -9,14 +9,36 @@
sys.path.append("../")
from experiments.config import ex, config, env_config, agent_config, train_config, technical_config
from ginkgo_rl import GinkgoLikelihood1DEnv, GinkgoLikelihoodEnv
-from ginkgo_rl import PolicyMCTSAgent, GreedyAgent, RandomAgent, RandomMCTSAgent, LikelihoodMCTSAgent, ImitationLearningPolicyMCTSAgent, BatchedACERAgent
+from ginkgo_rl import (
+ PolicyMCTSAgent,
+ GreedyAgent,
+ RandomAgent,
+ RandomMCTSAgent,
+ LikelihoodMCTSAgent,
+ ImitationLearningPolicyMCTSAgent,
+ BatchedACERAgent,
+)
from ginkgo_rl import GinkgoEvaluator
logger = logging.getLogger(__name__)
+@ex.capture
+def check_config(algorithm, policy, teacher, env_type):
+ """ Checks that the configuration is valid """
+
+ assert algorithm in ["mcts", "lfd", "lfd-mcts", "acer", "greedy", "random", "truth", "mle", "beamsearch"]
+ if algorithm == "mcts":
+ assert policy in ["nn", "random", "likelihood"]
+ if algorithm in ["lfd", "lfd-mcts"]:
+ assert teacher in ["truth", "mle"]
+ assert env_type == "1d" # For now, 2d env is not supported
+
+
@ex.capture
def setup_run(name, run_name, seed):
+ """ Sets up run, including the random seed """
+
logger.info(f"Setting up run {name}")
os.makedirs(f"./data/runs/{run_name}/", exist_ok=True)
@@ -26,6 +48,8 @@ def setup_run(name, run_name, seed):
@ex.capture
def setup_logging(debug):
+ """ Sets up logging """
+
silence_list = ["matplotlib", "showerSim", "hierarchical-trellis"]
for key in logging.Logger.manager.loggerDict:
@@ -55,8 +79,10 @@ def create_env(
w_mass,
jet_momentum,
jetdir,
- max_n_try
+ max_n_try,
):
+ """ Sets up environment """
+
logger.info(f"Creating environment")
if env_type == "1d":
@@ -87,8 +113,30 @@ def create_env(
@ex.capture
def create_agent(
- env, algorithm, policy, initialize_mcts_with_beamsearch, log_likelihood_policy_input, reward_range, history_length, train_n_mc_target, train_n_mc_min, train_n_mc_max, train_mcts_mode, train_c_puct, device, dtype, learning_rate, weight_decay, train_beamsize, debug, debug_verbosity
+ env,
+ algorithm,
+ policy,
+ initialize_mcts_with_beamsearch,
+ log_likelihood_policy_input,
+ reward_range,
+ history_length,
+ train_n_mc_target,
+ train_n_mc_min,
+ train_n_mc_max,
+ train_planning_mode,
+ train_c_puct,
+ device,
+ dtype,
+ learning_rate,
+ weight_decay,
+ train_beamsize,
+ debug,
+ debug_verbosity,
+ clip_gradient,
+ decision_mode,
):
+ """ Sets up agent """
+
logger.info(f"Setting up {algorithm} agent ")
if algorithm == "mcts" and policy == "nn":
@@ -98,7 +146,7 @@ def create_agent(
n_mc_target=train_n_mc_target,
n_mc_min=train_n_mc_min,
n_mc_max=train_n_mc_max,
- mcts_mode=train_mcts_mode,
+ planning_mode=train_planning_mode,
initialize_with_beam_search=initialize_mcts_with_beamsearch,
log_likelihood_feature=log_likelihood_policy_input,
c_puct=train_c_puct,
@@ -107,6 +155,8 @@ def create_agent(
device=device,
dtype=dtype,
verbose=debug_verbosity if debug else 0,
+ clip_gradient=clip_gradient,
+ decision_mode=decision_mode,
)
elif algorithm == "mcts" and policy == "likelihood":
agent = LikelihoodMCTSAgent(
@@ -115,7 +165,7 @@ def create_agent(
n_mc_target=train_n_mc_target,
n_mc_min=train_n_mc_min,
n_mc_max=train_n_mc_max,
- mcts_mode=train_mcts_mode,
+ planning_mode=train_planning_mode,
initialize_with_beam_search=initialize_mcts_with_beamsearch,
c_puct=train_c_puct,
device=device,
@@ -124,6 +174,7 @@ def create_agent(
weight_decay=weight_decay,
beam_size=train_beamsize,
verbose=debug_verbosity if debug else 0,
+ decision_mode=decision_mode,
)
elif algorithm == "mcts" and policy == "random":
agent = RandomMCTSAgent(
@@ -132,12 +183,13 @@ def create_agent(
n_mc_target=train_n_mc_target,
n_mc_min=train_n_mc_min,
n_mc_max=train_n_mc_max,
- mcts_mode=train_mcts_mode,
+ planning_mode=train_planning_mode,
initialize_with_beam_search=initialize_mcts_with_beamsearch,
c_puct=train_c_puct,
device=device,
dtype=dtype,
verbose=debug_verbosity if debug else 0,
+ decision_mode=decision_mode,
)
elif algorithm in ["lfd", "lfd-mcts"]:
agent = ImitationLearningPolicyMCTSAgent(
@@ -146,7 +198,7 @@ def create_agent(
n_mc_target=train_n_mc_target,
n_mc_min=train_n_mc_min,
n_mc_max=train_n_mc_max,
- mcts_mode=train_mcts_mode,
+ planning_mode=train_planning_mode,
initialize_with_beam_search=initialize_mcts_with_beamsearch,
log_likelihood_feature=log_likelihood_policy_input,
c_puct=train_c_puct,
@@ -155,14 +207,12 @@ def create_agent(
device=device,
dtype=dtype,
verbose=debug_verbosity if debug else 0,
+ clip_gradient=clip_gradient,
+ decision_mode=decision_mode,
)
elif algorithm == "acer":
agent = BatchedACERAgent(
- env,
- lr=learning_rate,
- weight_decay=weight_decay,
- device=device,
- dtype=dtype,
+ env, lr=learning_rate, weight_decay=weight_decay, device=device, dtype=dtype, clip_gradient=clip_gradient,
)
elif algorithm == "greedy":
agent = GreedyAgent(env, device=device, dtype=dtype, verbose=debug_verbosity if debug else 0)
@@ -178,11 +228,13 @@ def create_agent(
@ex.capture
def log_training(_run, callback_info):
- loss = callback_info.get('loss')
- reward = callback_info.get('reward')
- episode_length = callback_info.get('episode_length')
- likelihood_evaluations = callback_info.get('likelihood_evaluations')
- mean_abs_weight = callback_info.get('mean_abs_weight')
+ """ Callback for logging during training """
+
+ loss = callback_info.get("loss")
+ reward = callback_info.get("reward")
+ episode_length = callback_info.get("episode_length")
+ likelihood_evaluations = callback_info.get("likelihood_evaluations")
+ mean_abs_weight = callback_info.get("mean_abs_weight")
if loss is not None:
_run.log_scalar("training_loss", loss)
@@ -197,7 +249,30 @@ def log_training(_run, callback_info):
@ex.capture
-def train(env, agent, algorithm, policy, teacher, train_n_mc_target, train_n_mc_min, train_n_mc_max, train_mcts_mode, train_c_puct, train_steps, train_beamsize, pretrain_c_puct, pretrain_steps, pretrain_beamsize, pretrain_mcts_mode, pretrain_n_mc_max, pretrain_n_mc_min, pretrain_n_mc_target, imitation_steps):
+def train(
+ env,
+ agent,
+ algorithm,
+ policy,
+ teacher,
+ train_n_mc_target,
+ train_n_mc_min,
+ train_n_mc_max,
+ train_planning_mode,
+ train_c_puct,
+ train_steps,
+ train_beamsize,
+ pretrain_c_puct,
+ pretrain_steps,
+ pretrain_beamsize,
+ pretrain_planning_mode,
+ pretrain_n_mc_max,
+ pretrain_n_mc_min,
+ pretrain_n_mc_target,
+ imitation_steps,
+):
+ """ Trains an agent """
+
if algorithm in ["greedy", "random", "truth", "mle", "beamsearch"]:
logger.info(f"No training necessary for algorithm {algorithm}")
elif algorithm == "mcts" and policy in ["random", "likelihood"]:
@@ -205,13 +280,22 @@ def train(env, agent, algorithm, policy, teacher, train_n_mc_target, train_n_mc_
elif algorithm == "mcts":
# Pretraining
logger.info(f"Starting MCTS pretraining for {pretrain_steps} steps")
- agent.set_precision(pretrain_n_mc_target, pretrain_n_mc_min, pretrain_n_mc_max, pretrain_mcts_mode, pretrain_c_puct, pretrain_beamsize)
+ agent.set_precision(
+ pretrain_n_mc_target,
+ pretrain_n_mc_min,
+ pretrain_n_mc_max,
+ pretrain_planning_mode,
+ pretrain_c_puct,
+ pretrain_beamsize,
+ )
_ = env.reset()
agent.learn(total_timesteps=pretrain_steps, callback=log_training)
# Main training
logger.info(f"Starting MCTS training for {train_steps} steps")
- agent.set_precision(train_n_mc_target, train_n_mc_min, train_n_mc_max, train_mcts_mode, train_c_puct, train_beamsize)
+ agent.set_precision(
+ train_n_mc_target, train_n_mc_min, train_n_mc_max, train_planning_mode, train_c_puct, train_beamsize
+ )
_ = env.reset()
agent.learn(total_timesteps=train_steps, callback=log_training)
elif algorithm == "lfd":
@@ -227,7 +311,9 @@ def train(env, agent, algorithm, policy, teacher, train_n_mc_target, train_n_mc_
# RL training
logger.info(f"Starting MCTS training for {train_steps} steps")
_ = env.reset()
- agent.set_precision(train_n_mc_target, train_n_mc_min, train_n_mc_max, train_mcts_mode, train_c_puct, train_beamsize)
+ agent.set_precision(
+ train_n_mc_target, train_n_mc_min, train_n_mc_max, train_planning_mode, train_c_puct, train_beamsize
+ )
agent.learn(total_timesteps=train_steps, callback=log_training)
elif algorithm == "acer":
logger.info(f"Starting ACER training for {pretrain_steps + train_steps} steps")
@@ -240,19 +326,44 @@ def train(env, agent, algorithm, policy, teacher, train_n_mc_target, train_n_mc_
@ex.capture
-def eval(agent, env, name, algorithm, eval_n_mc_target, eval_n_mc_min, eval_n_mc_max, eval_mcts_mode, eval_c_puct, eval_repeats, eval_jets, eval_filename, redraw_eval_jets, run_name, eval_beamsize, _run):
+def eval(
+ agent,
+ env,
+ name,
+ algorithm,
+ eval_n_mc_target,
+ eval_n_mc_min,
+ eval_n_mc_max,
+ eval_planning_mode,
+ eval_c_puct,
+ eval_repeats,
+ eval_jets,
+ eval_filename,
+ redraw_eval_jets,
+ run_name,
+ eval_beamsize,
+ _run,
+):
+ """ Evaluates a trained agent """
+
# Set up evaluator
logger.info("Starting evaluation")
os.makedirs(os.path.dirname(eval_filename), exist_ok=True)
- evaluator = GinkgoEvaluator(env=env, filename=eval_filename, n_jets=eval_jets, redraw_existing_jets=redraw_eval_jets)
+ evaluator = GinkgoEvaluator(
+ env=env, filename=eval_filename, n_jets=eval_jets, redraw_existing_jets=redraw_eval_jets
+ )
jet_sizes = evaluator.get_jet_info()["n_leaves"]
# Evaluate
if algorithm in ["mcts", "lfd-mcts"]:
- agent.set_precision(eval_n_mc_target, eval_n_mc_min, eval_n_mc_max, eval_mcts_mode, eval_c_puct, eval_beamsize)
+ agent.set_precision(
+ eval_n_mc_target, eval_n_mc_min, eval_n_mc_max, eval_planning_mode, eval_c_puct, eval_beamsize
+ )
log_likelihood, errors, likelihood_evaluations = evaluator.eval(name, agent, n_repeats=eval_repeats)
elif algorithm == "lfd":
- log_likelihood, errors, likelihood_evaluations = evaluator.eval(name, agent, n_repeats=eval_repeats, mode="policy")
+ log_likelihood, errors, likelihood_evaluations = evaluator.eval(
+ name, agent, n_repeats=eval_repeats, mode="policy"
+ )
elif algorithm in ["acer", "random"]:
log_likelihood, errors, likelihood_evaluations = evaluator.eval(name, agent, n_repeats=eval_repeats)
elif algorithm == "greedy":
@@ -295,7 +406,9 @@ def eval(agent, env, name, algorithm, eval_n_mc_target, eval_n_mc_min, eval_n_mc
@ex.capture
def save_agent(agent, algorithm, policy, run_name):
- if algorithm == "mcts" and policy == "nn" and agent is not None:
+ """ Saves the state dict of an agent to file """
+
+ if algorithm in ["mcts", "lfd", "lfd-mcts"] and policy == "nn" and agent is not None:
filename = f"./data/runs/{run_name}/model.pty"
logger.info(f"Saving model at {filename}")
torch.save(agent.state_dict(), filename)
@@ -306,8 +419,11 @@ def save_agent(agent, algorithm, policy, run_name):
@ex.automain
def main():
+ """ Main entry point for experiments """
+
logger.info(f"Hi!")
+ check_config()
setup_run()
setup_logging()
env = create_env()
diff --git a/experiments/hpc/run1.sh b/experiments/hpc/run1.sh
new file mode 100755
index 0000000..a8e3bb6
--- /dev/null
+++ b/experiments/hpc/run1.sh
@@ -0,0 +1,29 @@
+#!/usr/bin/env bash
+
+#SBATCH --job-name=ginkgo-rl
+#SBATCH --output=log_ginkgo_rl_run1_%a.log
+#SBATCH --nodes=1
+#SBATCH --cpus-per-task=1
+#SBATCH --mem=16GB
+#SBATCH --time=5-00:00:00
+# #SBATCH --gres=gpu:1
+
+dir=/scratch/jb6504/ginkgo-rl/experiments
+seed=$((SLURM_ARRAY_TASK_ID + 1000))
+setup=$((SLURM_ARRAY_TASK_ID))
+
+cd $dir
+source activate rl
+
+case ${setup} in
+0) python -u experiment.py with truth "seed=$seed" "database=False";;
+1) python -u experiment.py with mle "seed=$seed" "database=False";;
+2) python -u experiment.py with greedy "seed=$seed" "database=False";;
+3) python -u experiment.py with beamsearch_s "seed=$seed" "database=False";;
+4) python -u experiment.py with beamsearch_m "seed=$seed" "database=False";;
+5) python -u experiment.py with beamsearch_l "seed=$seed" "database=False";;
+6) python -u experiment.py with beamsearch_xl "seed=$seed" "database=False";;
+7) python -u experiment.py with mcts_likelihood "seed=$seed" "database=False";;
+8) python -u experiment.py with mcts_only_beamsearch "seed=$seed" "database=False";;
+*) echo "Nothing to do for job ${SLURM_ARRAY_TASK_ID}" ;;
+esac
diff --git a/experiments/hpc/run2.sh b/experiments/hpc/run2.sh
new file mode 100755
index 0000000..e39ec04
--- /dev/null
+++ b/experiments/hpc/run2.sh
@@ -0,0 +1,41 @@
+#!/usr/bin/env bash
+
+#SBATCH --job-name=ginkgo-rl
+#SBATCH --output=log_ginkgo_rl_run2_%a.log
+#SBATCH --nodes=1
+#SBATCH --cpus-per-task=1
+#SBATCH --mem=16GB
+#SBATCH --time=5-00:00:00
+# #SBATCH --gres=gpu:1
+
+dir=/scratch/jb6504/ginkgo-rl/experiments
+seed=$((SLURM_ARRAY_TASK_ID + 1000))
+setup=$((SLURM_ARRAY_TASK_ID / 5))
+
+cd $dir
+source activate rl
+
+case ${setup} in
+0) python -u experiment.py with mcts_xs "seed=$seed" "database=False";;
+1) python -u experiment.py with mcts_s "seed=$seed" "database=False";;
+2) python -u experiment.py with mcts_m "seed=$seed" "database=False";;
+3) python -u experiment.py with mcts_l "seed=$seed" "database=False";;
+4) python -u experiment.py with lfd "seed=$seed" "database=False";;
+5) python -u experiment.py with lfd_mcts_s "seed=$seed" "database=False";;
+6) python -u experiment.py with mcts_exploit "seed=$seed" "database=False";;
+7) python -u experiment.py with mcts_explore "seed=$seed" "database=False";;
+8) python -u experiment.py with mcts_raw "seed=$seed" "database=False";;
+9) python -u experiment.py with mcts_puct_decisions "seed=$seed" "database=False";;
+10) python -u experiment.py with mcts_no_beamsearch "seed=$seed" "database=False";;
+11) python -u experiment.py with mcts_random "seed=$seed" "database=False";;
+12) python -u experiment.py with random "seed=$seed" "database=False";;
+13) python -u experiment.py with lfd_mcts_xs "seed=$seed" "database=False";;
+14) python -u experiment.py with lfd_mcts_m "seed=$seed" "database=False";;
+15) python -u experiment.py with lfd_mcts_l "seed=$seed" "database=False";;
+16) python -u experiment.py with lfd_mleteacher "seed=$seed" "database=False";;
+17) python -u experiment.py with lfd_mcts_mleteacher_xs "seed=$seed" "database=False";;
+18) python -u experiment.py with lfd_mcts_mleteacher_s "seed=$seed" "database=False";;
+19) python -u experiment.py with lfd_mcts_mleteacher_m "seed=$seed" "database=False";;
+20) python -u experiment.py with lfd_mcts_mleteacher_l "seed=$seed" "database=False";;
+*) echo "Nothing to do for job ${SLURM_ARRAY_TASK_ID}" ;;
+esac
diff --git a/experiments/plot_individual_runs.ipynb b/experiments/plot_individual_runs.ipynb
new file mode 100644
index 0000000..f3b0835
--- /dev/null
+++ b/experiments/plot_individual_runs.ipynb
@@ -0,0 +1,465 @@
+{
+ "cells": [
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "# Plot individual experiment results (deprecated)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 1,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "%matplotlib inline\n",
+ "from collections import OrderedDict\n",
+ "import numpy as np\n",
+ "from matplotlib import pyplot as plt\n",
+ "from matplotlib.ticker import MultipleLocator, AutoMinorLocator\n"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## Methods and filenames"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 2,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "runs = OrderedDict()\n",
+ "\n",
+ "runs[\"truth\"] = \"truth_20200911_091636\"\n",
+ "runs[\"mle\"] = \"mle_20200911_092100\"\n",
+ "runs[\"random\"] = \"random_20200911_092105\"\n",
+ "runs[\"greedy\"] = \"greedy_20200914_102725\"\n",
+ "runs[\"beamsearch_s\"] = \"beamsearch_s_20200911_173430\"\n",
+ "runs[\"beamsearch_m\"] = \"beamsearch_m_20200911_173434\"\n",
+ "runs[\"beamsearch_l\"] = \"beamsearch_l_20200911_173439\"\n",
+ "runs[\"beamsearch_xl\"] = \"beamsearch_xl_20200911_173443\"\n",
+ "runs[\"mcts_xs\"] = \"mcts_nn_xs_20200917_142539\" # old: \"mcts_nn_xs_20200914_110435\"\n",
+ "runs[\"mcts_s\"] = \"mcts_nn_s_20200916_165050\" # old: \"mcts_nn_s_20200911_170739\"\n",
+ "runs[\"mcts_m\"] = \"mcts_nn_m_20200916_215216\" # old: \"mcts_nn_m_20200911_172132\"\n",
+ "runs[\"mcts_l\"] = \"\" # old: \"mcts_nn_l_20200911_172057\"\n",
+ "\n",
+ "runs[\"mcts_raw\"] = \"\" # old: \"mcts_raw_s_20200911_173335\"\n",
+ "runs[\"mcts_puctdecisions\"] = \"mcts_puct_decisions_s\"\n",
+ "runs[\"mcts_onlybs\"] = \"mcts_only_bs_s_20200911_171817\"\n",
+ "runs[\"mcts_nobs\"] = \"mcts_nn_no_beamsearch_s_20200917_185603\" # old: \"mcts_nn_no_beamsearch_s_20200911_170827\"\n",
+ "runs[\"mcts_random\"] = \"mcts_random_s_20200911_173342\"\n",
+ "runs[\"mcts_likelihood\"] = \"mcts_likelihood_s_20200911_170510\"\n"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 3,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "labels = {}\n",
+ "\n",
+ "labels[\"truth\"] = \"Truth\"\n",
+ "labels[\"mle\"] = \"MLE\"\n",
+ "labels[\"random\"] = \"Random\"\n",
+ "labels[\"greedy\"] = \"Greedy\"\n",
+ "labels[\"beamsearch_s\"] = None\n",
+ "labels[\"beamsearch_m\"] = None\n",
+ "labels[\"beamsearch_l\"] = None\n",
+ "labels[\"beamsearch_xl\"] = \"Beam search\"\n",
+ "labels[\"mcts_xs\"] = None\n",
+ "labels[\"mcts_s\"] = None\n",
+ "labels[\"mcts_m\"] = None\n",
+ "labels[\"mcts_l\"] = \"MCTS\"\n"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 4,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "cost_labels = {}\n",
+ "\n",
+ "cost_labels[\"beamsearch_s\"] = \"Beam search\"\n",
+ "cost_labels[\"random\"] = \"Random\"\n",
+ "cost_labels[\"greedy\"] = \"Greedy\"\n",
+ "cost_labels[\"mcts_xs\"] = \"MCTS\""
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 5,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "colors = {}\n",
+ "\n",
+ "colors[\"random\"] = \"black\"\n",
+ "colors[\"truth\"] = \"black\"\n",
+ "colors[\"mle\"] = \"black\"\n",
+ "\n",
+ "colors[\"greedy\"] = \"#0A3D5C\"\n",
+ "colors[\"beamsearch_s\"] = \"#3D708F\"\n",
+ "colors[\"mcts_xs\"] = \"#F57547\"\n",
+ "\n",
+ "colors[\"beamsearch_m\"] = colors[\"beamsearch_s\"]\n",
+ "colors[\"beamsearch_l\"] = colors[\"beamsearch_s\"]\n",
+ "colors[\"beamsearch_xl\"] = colors[\"beamsearch_s\"]\n",
+ "colors[\"mcts_s\"] = colors[\"mcts_xs\"]\n",
+ "colors[\"mcts_m\"] = colors[\"mcts_xs\"]\n",
+ "colors[\"mcts_l\"] = colors[\"mcts_xs\"]\n"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 6,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "linestyles = {}\n",
+ "\n",
+ "linestyles[\"truth\"] = \"-.\"\n",
+ "linestyles[\"mle\"] = \":\"\n",
+ "linestyles[\"random\"] = \":\"\n",
+ "\n",
+ "linestyles[\"greedy\"] = \"--\"\n",
+ "\n",
+ "linestyles[\"beamsearch_s\"] = \"-.\"\n",
+ "linestyles[\"beamsearch_m\"] = \"-.\"\n",
+ "linestyles[\"beamsearch_l\"] = \"-.\"\n",
+ "linestyles[\"beamsearch_xl\"] = \"-.\"\n",
+ "\n",
+ "linestyles[\"mcts_xs\"] = \"-\"\n",
+ "linestyles[\"mcts_s\"] = \"-\"\n",
+ "linestyles[\"mcts_m\"] = \"-\"\n",
+ "linestyles[\"mcts_l\"] = \"-\"\n"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 7,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "markers = {}\n",
+ "\n",
+ "markers[\"beamsearch_s\"] = \"^\"\n",
+ "markers[\"greedy\"] = \"s\"\n",
+ "markers[\"mcts_xs\"] = \"o\"\n"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## Load results"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 8,
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "[Errno 2] No such file or directory: './data/runs/truth_20200911_091636/eval_likelihood_evaluations.npy'\n",
+ "[Errno 2] No such file or directory: './data/runs/mle_20200911_092100/eval_likelihood_evaluations.npy'\n",
+ "[Errno 2] No such file or directory: './data/runs/random_20200911_092105/eval_likelihood_evaluations.npy'\n",
+ "[Errno 2] No such file or directory: './data/runs//eval_log_likelihood.npy'\n",
+ "[Errno 2] No such file or directory: './data/runs//eval_likelihood_evaluations.npy'\n",
+ "[Errno 2] No such file or directory: './data/runs//eval_log_likelihood.npy'\n",
+ "[Errno 2] No such file or directory: './data/runs//eval_likelihood_evaluations.npy'\n",
+ "[Errno 2] No such file or directory: './data/runs/mcts_puct_decisions_s/eval_log_likelihood.npy'\n",
+ "[Errno 2] No such file or directory: './data/runs/mcts_puct_decisions_s/eval_likelihood_evaluations.npy'\n"
+ ]
+ }
+ ],
+ "source": [
+ "n_jets = 500\n",
+ "run_dir = \"./data/runs\"\n",
+ "\n",
+ "jet_sizes = np.nan * np.ones(n_jets)\n",
+ "log_likelihoods = {}\n",
+ "costs = {}\n",
+ "\n",
+ "for key, run in runs.items():\n",
+ " try:\n",
+ " log_likelihoods[key] = np.load(f\"{run_dir}/{run}/eval_log_likelihood.npy\").flatten()\n",
+ " jet_sizes = np.load(f\"{run_dir}/{run}/eval_jet_sizes.npy\")\n",
+ " except Exception as e:\n",
+ " print(e)\n",
+ " log_likelihoods[key] = np.nan * np.ones(n_jets)\n",
+ " \n",
+ " try:\n",
+ " costs[key] = np.load(f\"{run_dir}/{run}/eval_likelihood_evaluations.npy\").flatten()\n",
+ " except Exception as e:\n",
+ " print(e)\n",
+ " costs[key] = np.nan * np.ones(n_jets)\n"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## Normalize results"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 9,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "norm_key = \"greedy\"\n",
+ "rel_log_likelihoods = {}\n",
+ "\n",
+ "for key, val in log_likelihoods.items():\n",
+ " rel_log_likelihoods[key] = val - log_likelihoods[norm_key]\n"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## Bin results"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 10,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "# n_bins = 5\n",
+ "# bin_boundaries = np.percentile(jet_sizes, np.linspace(0., 100., n_bins + 1)).astype(np.float)\n",
+ "# bin_boundaries[0] -= 0.01\n",
+ "# bin_boundaries[-1] += 0.01\n",
+ "bin_boundaries = [1.5] + list(np.arange(7.5, 17, 2)) + [20.5]\n"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 11,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "bin_jet_size = {}\n",
+ "bin_log_likelihood = {}\n",
+ "bin_log_likelihood_err = {}\n",
+ "\n",
+ "for key in runs.keys():\n",
+ " bin_jet_size_ = []\n",
+ " bin_log_likelihood_ = []\n",
+ " bin_log_likelihood_err_ = []\n",
+ " \n",
+ " for bin_min, bin_max in zip(bin_boundaries[:-1], bin_boundaries[1:]):\n",
+ " x = jet_sizes[(jet_sizes >= bin_min) * (jet_sizes < bin_max)]\n",
+ " y = rel_log_likelihoods[key][(jet_sizes >= bin_min) * (jet_sizes < bin_max)]\n",
+ " bin_jet_size_.append(np.mean(x))\n",
+ " bin_log_likelihood_.append(np.mean(y))\n",
+ " bin_log_likelihood_err_.append(np.std(y) / (len(x)**0.5 + 1.e-9))\n",
+ " \n",
+ " bin_jet_size[key] = np.asarray(bin_jet_size_)\n",
+ " bin_log_likelihood[key] = np.asarray(bin_log_likelihood_)\n",
+ " bin_log_likelihood_err[key] = np.asarray(bin_log_likelihood_err_)\n"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## Plots"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 12,
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "image/png": "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\n",
+ "text/plain": [
+ ""
+ ]
+ },
+ "metadata": {
+ "needs_background": "light"
+ },
+ "output_type": "display_data"
+ }
+ ],
+ "source": [
+ "plot = [\n",
+ " [\"greedy\"],\n",
+ " [\"beamsearch_s\", \"beamsearch_m\", \"beamsearch_l\", \"beamsearch_xl\"],\n",
+ " [\"mcts_xs\", \"mcts_s\", \"mcts_m\", \"mcts_l\"],\n",
+ "]\n",
+ "\n",
+ "fig = plt.figure(figsize=(3.6,3.6))\n",
+ "ax = plt.gca()\n",
+ " \n",
+ "for keys in plot:\n",
+ " xs = [np.mean(costs[key]) for key in keys]\n",
+ " ys = [np.mean(log_likelihoods[key]) for key in keys]\n",
+ " c, m, ms, lbl = colors[keys[0]], markers[keys[0]], 6., cost_labels[keys[0]]\n",
+ " ls = linestyles[keys[0]] if len(xs) > 1 else \" \"\n",
+ " \n",
+ " # Include greedy in beam search\n",
+ " if keys[0] == \"beamsearch_s\":\n",
+ " plt.plot(\n",
+ " [np.mean(costs[\"greedy\"]), xs[0]],\n",
+ " [np.mean(log_likelihoods[\"greedy\"]), ys[0]],\n",
+ " c=c, lw=1.5, ls=ls, zorder=-1\n",
+ " )\n",
+ "\n",
+ " plt.plot(xs, ys, c=c, lw=1.5, ls=ls, marker=m, markersize=ms, label=lbl)\n",
+ "\n",
+ "plt.legend(loc=\"lower right\", frameon=False)\n",
+ "\n",
+ "plt.xscale(\"log\")\n",
+ "ax.yaxis.set_major_locator(MultipleLocator(1.))\n",
+ "ax.yaxis.set_minor_locator(MultipleLocator(0.2))\n",
+ "plt.xlabel(\"Cost (splitting likelihood evaluations)\")\n",
+ "plt.ylabel(f\"Tree log likelihood\", labelpad=0)\n",
+ "ax.xaxis.set_label_coords(0.5, -0.1)\n",
+ "ax.yaxis.set_label_coords(-0.12, 0.5)\n",
+ "\n",
+ "plt.subplots_adjust(left=0.15, bottom=0.15, right=0.99, top=0.99, wspace=0, hspace=0)\n",
+ "plt.savefig(\"figures/rl_ginkgo_log_likelihood_vs_cost.pdf\")\n"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 13,
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "image/png": "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\n",
+ "text/plain": [
+ ""
+ ]
+ },
+ "metadata": {
+ "needs_background": "light"
+ },
+ "output_type": "display_data"
+ }
+ ],
+ "source": [
+ "plot = [\"mle\", \"greedy\", \"beamsearch_xl\", \"mcts_l\"]\n",
+ "\n",
+ "fig = plt.figure(figsize=(3.6,3.6))\n",
+ "ax = plt.gca()\n",
+ " \n",
+ "for key in plot:\n",
+ " x, y, dy = bin_jet_size[key], bin_log_likelihood[key], bin_log_likelihood_err[key]\n",
+ " c, ls, lbl = colors[key], linestyles[key], labels[key]\n",
+ "\n",
+ " plt.fill_between(x, y-dy, y+dy, color=c, alpha=0.1)\n",
+ " plt.plot(x, y, c=c, lw=1.5, ls=ls, label=lbl)\n",
+ "\n",
+ "plt.legend(loc=\"upper left\", frameon=False)\n",
+ "\n",
+ "ax.yaxis.set_major_locator(MultipleLocator(1.))\n",
+ "ax.yaxis.set_minor_locator(MultipleLocator(0.5))\n",
+ "ax.xaxis.set_major_locator(MultipleLocator(5.))\n",
+ "ax.xaxis.set_minor_locator(MultipleLocator(1.))\n",
+ "plt.xlabel(\"Number of leaves\")\n",
+ "plt.ylabel(f\"Tree log likelihood relative to {norm_key}\")\n",
+ "ax.xaxis.set_label_coords(0.5, -0.1)\n",
+ "ax.yaxis.set_label_coords(-0.12, 0.5)\n",
+ "\n",
+ "plt.subplots_adjust(left=0.15, bottom=0.15, right=0.99, top=0.99, wspace=0, hspace=0)\n",
+ "plt.savefig(\"figures/rl_ginkgo_log_likelihood_vs_leaves.pdf\")\n"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## All results"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 14,
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ " truth: $ -99.3 & nan\n",
+ " mle: $ nan & nan\n",
+ " random: $-202.2 & nan\n",
+ " greedy: $ -96.0 & 452\n",
+ " beamsearch_s: $ -94.1 & 1914\n",
+ " beamsearch_m: $ -93.5 & 7397\n",
+ " beamsearch_l: $ -93.3 & 36641\n",
+ " beamsearch_xl: $ -93.3 & 365634\n",
+ " mcts_xs: $ -93.4 & 12119\n",
+ " mcts_s: $ -93.3 & 22371\n",
+ " mcts_m: $ -93.1 & 70586\n",
+ " mcts_l: $ nan & nan\n",
+ " mcts_raw: $ nan & nan\n",
+ " mcts_puctdecisions: $ nan & nan\n",
+ " mcts_onlybs: $ -93.9 & 7397\n",
+ " mcts_nobs: $ -94.6 & 5652\n",
+ " mcts_random: $ -93.9 & 26718\n",
+ " mcts_likelihood: $ -93.5 & 28015\n"
+ ]
+ }
+ ],
+ "source": [
+ "for key, val in log_likelihoods.items():\n",
+ " mean = np.mean(val)\n",
+ " err = np.std(val)/len(val)**0.5\n",
+ " cost = np.mean(costs[key])\n",
+ " print(\n",
+ " f\"{key:>20.20}: ${mean:6.1f}\"\n",
+ " # + f\" \\\\textcolor{{dark-grey}}{{\\pm {err:4.2f}}}$\"\n",
+ " + f\" & {cost:6.0f}\"\n",
+ " )\n",
+ " "
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": []
+ }
+ ],
+ "metadata": {
+ "kernelspec": {
+ "display_name": "Python (rl)",
+ "language": "python",
+ "name": "rl"
+ },
+ "language_info": {
+ "codemirror_mode": {
+ "name": "ipython",
+ "version": 3
+ },
+ "file_extension": ".py",
+ "mimetype": "text/x-python",
+ "name": "python",
+ "nbconvert_exporter": "python",
+ "pygments_lexer": "ipython3",
+ "version": "3.7.7"
+ }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 4
+}
diff --git a/experiments/plot_results.ipynb b/experiments/plot_results.ipynb
index 028491e..48daf46 100644
--- a/experiments/plot_results.ipynb
+++ b/experiments/plot_results.ipynb
@@ -4,7 +4,7 @@
"cell_type": "markdown",
"metadata": {},
"source": [
- "## Plot experiment results"
+ "# Plot experiment results"
]
},
{
@@ -17,14 +17,22 @@
"from collections import OrderedDict\n",
"import numpy as np\n",
"from matplotlib import pyplot as plt\n",
- "from matplotlib.ticker import MultipleLocator, AutoMinorLocator\n"
+ "from matplotlib.ticker import MultipleLocator, AutoMinorLocator\n",
+ "from mpl_toolkits.axes_grid1.inset_locator import inset_axes\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
- "## Methods and filenames"
+ "## Specify data"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "### Runs"
]
},
{
@@ -35,24 +43,21 @@
"source": [
"runs = OrderedDict()\n",
"\n",
- "runs[\"truth\"] = \"truth_20200911_091636\"\n",
- "runs[\"mle\"] = \"mle_20200911_092100\"\n",
- "runs[\"random\"] = \"random_20200911_092105\"\n",
- "runs[\"greedy\"] = \"greedy_20200914_102725\"\n",
- "runs[\"beamsearch_s\"] = \"beamsearch_s_20200911_173430\"\n",
- "runs[\"beamsearch_m\"] = \"beamsearch_m_20200911_173434\"\n",
- "runs[\"beamsearch_l\"] = \"beamsearch_l_20200911_173439\"\n",
- "runs[\"beamsearch_xl\"] = \"beamsearch_xl_20200911_173443\"\n",
- "runs[\"mcts_xs\"] = \"mcts_nn_xs_20200914_110435\"\n",
- "runs[\"mcts_s\"] = \"mcts_nn_s_20200911_170739\"\n",
- "runs[\"mcts_m\"] = \"mcts_nn_m_20200911_172132\"\n",
- "runs[\"mcts_l\"] = \"mcts_nn_l_20200911_172057\"\n",
+ "runs[\"truth\"] = [\"truth_20200930_092023_1000\"]\n",
+ "runs[\"mle\"] = [\"mle_20200930_092023_1001\"]\n",
+ "runs[\"random\"] = [\n",
+ " \"random_20200930_092429_1060\",\n",
+ " \"random_20200930_092430_1061\",\n",
+ " \"random_20200930_092430_1062\",\n",
+ " \"random_20200930_092430_1063\",\n",
+ " \"random_20200930_092430_1064\",\n",
+ "]\n",
+ "runs[\"greedy\"] = [\"greedy_20200930_092023_1003\"]\n",
"\n",
- "runs[\"mcts_raw\"] = \"mcts_raw_s_20200911_173335\"\n",
- "runs[\"mcts_onlybs\"] = \"mcts_only_bs_s_20200911_171817\"\n",
- "runs[\"mcts_nobs\"] = \"mcts_nn_no_beamsearch_s_20200911_170827\"\n",
- "runs[\"mcts_random\"] = \"mcts_random_s_20200911_173342\"\n",
- "runs[\"mcts_likelihood\"] = \"mcts_likelihood_s_20200911_170510\"\n"
+ "runs[\"beamsearch_s\"] = [\"beamsearch_s_20200930_092023_1004\"]\n",
+ "runs[\"beamsearch_m\"] = [\"beamsearch_m_20200930_092023_1005\"]\n",
+ "runs[\"beamsearch_l\"] = [\"beamsearch_l_20200930_092023_1006\"]\n",
+ "runs[\"beamsearch_xl\"] = [\"beamsearch_xl_20200930_092023_1007\"]\n"
]
},
{
@@ -60,6 +65,191 @@
"execution_count": 3,
"metadata": {},
"outputs": [],
+ "source": [
+ "runs[\"mcts_xs\"] = [\n",
+ " \"mcts_nn_xs_20200930_092038_1000\",\n",
+ " \"mcts_nn_xs_20200930_092038_1001\",\n",
+ " \"mcts_nn_xs_20200930_092038_1002\",\n",
+ " \"mcts_nn_xs_20200930_092038_1003\",\n",
+ " \"mcts_nn_xs_20200930_092039_1004\",\n",
+ "]\n",
+ "runs[\"mcts_s\"] = [\n",
+ " \"mcts_nn_s_20200930_092039_1005\",\n",
+ " \"mcts_nn_s_20200930_092039_1006\",\n",
+ " \"mcts_nn_s_20200930_092039_1007\",\n",
+ " \"mcts_nn_s_20200930_092039_1008\",\n",
+ " \"mcts_nn_s_20200930_092039_1009\",\n",
+ "]\n",
+ "runs[\"mcts_m\"] = [\n",
+ " \"mcts_nn_m_20200930_092039_1010\",\n",
+ " \"mcts_nn_m_20200930_092039_1011\",\n",
+ " \"mcts_nn_m_20200930_092039_1012\",\n",
+ " \"mcts_nn_m_20200930_092039_1013\",\n",
+ " \"mcts_nn_m_20200930_092039_1014\",\n",
+ " \"mcts_nn_m_20200916_215216\", # here we use one of the old runs as long as the new ones aren't done\n",
+ "]\n",
+ "runs[\"mcts_l\"] = [\n",
+ " \"mcts_nn_l_20200930_092039_1015\",\n",
+ " \"mcts_nn_l_20200930_092039_1019\",\n",
+ " \"mcts_nn_l_20200930_092040_1016\",\n",
+ " \"mcts_nn_l_20200930_092040_1017\",\n",
+ " \"mcts_nn_l_20200930_092040_1018\",\n",
+ " \"mcts_nn_l_20200911_172057\", # here we use one of the old runs as long as the new ones aren't done\n",
+ "]\n"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 4,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "runs[\"lfd-mcts_xs\"] = [\n",
+ " \"lfd-mcts_xs_20200930_172410_1065\",\n",
+ " \"lfd-mcts_xs_20200930_172410_1066\",\n",
+ " \"lfd-mcts_xs_20200930_172410_1067\",\n",
+ " \"lfd-mcts_xs_20200930_172410_1068\",\n",
+ " \"lfd-mcts_xs_20200930_172410_1069\",\n",
+ "]\n",
+ "runs[\"lfd-mcts_s\"] = [\n",
+ " \"lfd-mcts_s_20200930_172410_1025\",\n",
+ " \"lfd-mcts_s_20200930_172410_1026\",\n",
+ " \"lfd-mcts_s_20200930_172410_1027\",\n",
+ " \"lfd-mcts_s_20200930_172410_1028\",\n",
+ " \"lfd-mcts_s_20200930_172410_1029\",\n",
+ "]\n",
+ "runs[\"lfd-mcts_m\"] = [\n",
+ " \"lfd-mcts_m_20200930_172410_1070\",\n",
+ " \"lfd-mcts_m_20200930_172410_1071\",\n",
+ " \"lfd-mcts_m_20200930_172410_1072\",\n",
+ " \"lfd-mcts_m_20200930_172410_1073\",\n",
+ " \"lfd-mcts_m_20200930_172410_1074\",\n",
+ "]\n",
+ "runs[\"lfd-mcts_l\"] = [\n",
+ " \"lfd-mcts_l_20200930_172410_1075\",\n",
+ " \"lfd-mcts_l_20200930_172410_1076\",\n",
+ " \"lfd-mcts_l_20200930_172410_1077\",\n",
+ " \"lfd-mcts_l_20200930_172410_1078\",\n",
+ " \"lfd-mcts_l_20200930_172410_1079\",\n",
+ "]\n",
+ "\n",
+ "runs[\"lfd-mcts_mleteacher_xs\"] = [\n",
+ " \"lfd-mcts_mleteacher_xs_20200930_172410_1085\",\n",
+ " \"lfd-mcts_mleteacher_xs_20200930_172410_1086\",\n",
+ " \"lfd-mcts_mleteacher_xs_20200930_172410_1087\",\n",
+ " \"lfd-mcts_mleteacher_xs_20200930_172410_1088\",\n",
+ " \"lfd-mcts_mleteacher_xs_20200930_172410_1089\",\n",
+ "]\n",
+ "runs[\"lfd-mcts_mleteacher_s\"] = [\n",
+ " \"lfd-mcts_mleteacher_s_20200930_172410_1090\",\n",
+ " \"lfd-mcts_mleteacher_s_20200930_172410_1091\",\n",
+ " \"lfd-mcts_mleteacher_s_20200930_172551_1094\",\n",
+ " \"lfd-mcts_mleteacher_s_20200930_172552_1092\",\n",
+ " \"lfd-mcts_mleteacher_s_20200930_172552_1093\",\n",
+ "]\n",
+ "runs[\"lfd-mcts_mleteacher_m\"] = [\n",
+ " \"lfd-mcts_mleteacher_m_20200930_172552_1095\",\n",
+ " \"lfd-mcts_mleteacher_m_20200930_172552_1096\",\n",
+ " \"lfd-mcts_mleteacher_m_20200930_172552_1097\",\n",
+ " \"lfd-mcts_mleteacher_m_20200930_172552_1098\",\n",
+ " \"lfd-mcts_mleteacher_m_20200930_172552_1099\",\n",
+ "]\n",
+ "runs[\"lfd-mcts_mleteacher_l\"] = [\n",
+ " \"lfd-mcts_mleteacher_l_20200930_172553_1100\",\n",
+ " \"lfd-mcts_mleteacher_l_20200930_172553_1101\",\n",
+ " \"lfd-mcts_mleteacher_l_20200930_172553_1102\",\n",
+ " \"lfd-mcts_mleteacher_l_20200930_172553_1103\",\n",
+ " \"lfd-mcts_mleteacher_l_20200930_172553_1104\",\n",
+ "]\n",
+ "\n"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 5,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "runs[\"lfd\"] = [\n",
+ " \"lfd_20200930_172410_1021\",\n",
+ " \"lfd_20200930_172410_1022\",\n",
+ " \"lfd_20200930_172410_1023\",\n",
+ " \"lfd_20200930_172410_1024\",\n",
+ " \"lfd_20200930_174303_1020\",\n",
+ "]\n",
+ "runs[\"lfd_mleteacher\"] = [\n",
+ " \"lfd_mleteacher_20200930_172410_1080\",\n",
+ " \"lfd_mleteacher_20200930_172410_1081\",\n",
+ " \"lfd_mleteacher_20200930_172410_1082\",\n",
+ " \"lfd_mleteacher_20200930_172410_1083\",\n",
+ " \"lfd_mleteacher_20200930_172410_1084\",\n",
+ "]\n"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 6,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "runs[\"mcts_raw\"] = [\n",
+ " \"mcts_raw_s_20200930_092053_1040\",\n",
+ " \"mcts_raw_s_20200930_092113_1041\",\n",
+ " \"mcts_raw_s_20200930_092225_1042\",\n",
+ " \"mcts_raw_s_20200930_092225_1043\",\n",
+ " \"mcts_raw_s_20200930_092244_1044\",\n",
+ "]\n",
+ "runs[\"mcts_explore\"] = [\n",
+ " \"mcts_s_explore_20200930_092044_1035\",\n",
+ " \"mcts_s_explore_20200930_092539_1036\",\n",
+ " \"mcts_s_explore_20200930_092540_1037\",\n",
+ " \"mcts_s_explore_20200930_092540_1038\",\n",
+ " \"mcts_s_explore_20200930_092540_1039\",\n",
+ "]\n",
+ "runs[\"mcts_exploit\"] = [\n",
+ " \"mcts_s_exploit_20200930_092039_1030\",\n",
+ " \"mcts_s_exploit_20200930_092039_1031\",\n",
+ " \"mcts_s_exploit_20200930_092039_1032\",\n",
+ " \"mcts_s_exploit_20200930_092039_1033\",\n",
+ " \"mcts_s_exploit_20200930_092039_1034\",\n",
+ "]\n",
+ "runs[\"mcts_puctdecisions\"] = [\n",
+ " \"mcts_puct_decisions_s_20200930_092243_1045\",\n",
+ " \"mcts_puct_decisions_s_20200930_092243_1046\",\n",
+ " \"mcts_puct_decisions_s_20200930_092244_1047\",\n",
+ " \"mcts_puct_decisions_s_20200930_092324_1048\",\n",
+ " \"mcts_puct_decisions_s_20200930_092324_1049\",\n",
+ "]\n",
+ "runs[\"mcts_onlybs\"] = [\"mcts_only_bs_s_20200930_092023_1009\"]\n",
+ "runs[\"mcts_nobs\"] = [\n",
+ " \"mcts_nn_no_beamsearch_s_20200930_092324_1050\",\n",
+ " \"mcts_nn_no_beamsearch_s_20200930_092324_1051\",\n",
+ " \"mcts_nn_no_beamsearch_s_20200930_092324_1052\",\n",
+ " \"mcts_nn_no_beamsearch_s_20200930_092324_1053\",\n",
+ " \"mcts_nn_no_beamsearch_s_20200930_092324_1054\",\n",
+ "]\n",
+ "runs[\"mcts_random\"] = [\n",
+ " \"mcts_random_s_20200930_092324_1055\",\n",
+ " \"mcts_random_s_20200930_092324_1056\",\n",
+ " \"mcts_random_s_20200930_092324_1057\",\n",
+ " \"mcts_random_s_20200930_092324_1058\",\n",
+ " \"mcts_random_s_20200930_092324_1059\",\n",
+ "]\n",
+ "runs[\"mcts_likelihood\"] = [\"mcts_likelihood_s_20200930_092023_1008\"]\n"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "### Labels and style"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 7,
+ "metadata": {},
+ "outputs": [],
"source": [
"labels = {}\n",
"\n",
@@ -67,57 +257,92 @@
"labels[\"mle\"] = \"MLE\"\n",
"labels[\"random\"] = \"Random\"\n",
"labels[\"greedy\"] = \"Greedy\"\n",
- "labels[\"beamsearch_s\"] = None\n",
- "labels[\"beamsearch_m\"] = None\n",
- "labels[\"beamsearch_l\"] = None\n",
+ "\n",
+ "labels[\"beamsearch_s\"] = \"Beam search\"\n",
+ "labels[\"beamsearch_m\"] = \"Beam search\"\n",
+ "labels[\"beamsearch_l\"] = \"Beam search\"\n",
"labels[\"beamsearch_xl\"] = \"Beam search\"\n",
- "labels[\"mcts_xs\"] = None\n",
- "labels[\"mcts_s\"] = None\n",
- "labels[\"mcts_m\"] = None\n",
- "labels[\"mcts_l\"] = \"MCTS\"\n"
+ "\n",
+ "labels[\"mcts_xs\"] = \"MCTS\"\n",
+ "labels[\"mcts_s\"] = \"MCTS\"\n",
+ "labels[\"mcts_m\"] = \"MCTS\"\n",
+ "labels[\"mcts_l\"] = \"MCTS\"\n",
+ "\n",
+ "labels[\"lfd\"] = \"BC\"\n",
+ "labels[\"lfd_mleteacher\"] = \"MLE-BC\"\n",
+ "\n",
+ "labels[\"lfd-mcts_xs\"] = \"BC-MCTS\"\n",
+ "labels[\"lfd-mcts_s\"] = \"BC-MCTS\"\n",
+ "labels[\"lfd-mcts_m\"] = \"BC-MCTS\"\n",
+ "labels[\"lfd-mcts_l\"] = \"BC-MCTS\"\n",
+ "labels[\"lfd-mcts_mleteacher_xs\"] = \"BC-MCTS (MLE)\"\n",
+ "labels[\"lfd-mcts_mleteacher_s\"] = \"BC-MCTS (MLE)\"\n",
+ "labels[\"lfd-mcts_mleteacher_m\"] = \"BC-MCTS (MLE)\"\n",
+ "labels[\"lfd-mcts_mleteacher_l\"] = \"BC-MCTS (MLE)\"\n"
]
},
{
"cell_type": "code",
- "execution_count": 4,
+ "execution_count": 8,
"metadata": {},
"outputs": [],
"source": [
- "cost_labels = {}\n",
+ "colors = {}\n",
+ "\n",
+ "# https://coolors.co/053e61-337299-e6754c-e2bf9c-000000-808080\n",
+ "# blue0, blue1 = \"#053E61\", \"#337299\"\n",
+ "# red0, red1 = \"#E6754C\", \"#E2BF9C\"\n",
+ "# bw0, bw1 = \"#000000\", \"#808080\"\n",
+ "\n",
+ "# https://coolors.co/000000-064a75-4984ab-ffaa33-b8b8b8\n",
+ "# blue0, blue1 = \"#064A75\", \"#4984AB\"\n",
+ "# red0, red1 = \"#FFAA33\", \"#FFAA33\"\n",
+ "# bw0, bw1 = \"#000000\", \"#B8B8B8\"\n",
+ "\n",
+ "# https://coolors.co/000000-1d3557-457b9d-98CBCD-e63746\n",
+ "blue0, blue1, blue2 = \"#1d3557\", \"#457b9d\", \"#98CBCD\"\n",
+ "red0, red1 = \"#e63746\", \"#e63746\"\n",
+ "bw0, bw1 = \"#000000\", \"#000000\"\n",
+ "\n",
+ "colors[\"mle\"] = bw0\n",
+ "colors[\"random\"] = blue0\n",
"\n",
- "cost_labels[\"beamsearch_s\"] = \"Beam search\"\n",
- "cost_labels[\"random\"] = \"Random\"\n",
- "cost_labels[\"greedy\"] = \"Greedy\"\n",
- "cost_labels[\"mcts_xs\"] = \"MCTS\""
+ "colors[\"greedy\"] = blue1\n",
+ "colors[\"beamsearch_s\"] = blue2\n",
+ "\n",
+ "colors[\"mcts_xs\"] = red0\n",
+ "colors[\"lfd-mcts_xs\"] = red1\n",
+ "colors[\"lfd-mcts_mleteacher_xs\"] = red1\n",
+ "colors[\"lfd\"] = red0\n",
+ "colors[\"lfd_mleteacher\"] = red1\n",
+ "\n",
+ "colors[\"beamsearch_m\"] = colors[\"beamsearch_l\"] = colors[\"beamsearch_xl\"] = colors[\"beamsearch_s\"]\n",
+ "colors[\"mcts_s\"] = colors[\"mcts_m\"] = colors[\"mcts_l\"] = colors[\"mcts_xs\"]\n",
+ "colors[\"lfd-mcts_s\"] = colors[\"lfd-mcts_m\"] = colors[\"lfd-mcts_l\"] = colors[\"lfd-mcts_xs\"]\n",
+ "colors[\"lfd-mcts_mleteacher_s\"] = colors[\"lfd-mcts_mleteacher_m\"] = colors[\"lfd-mcts_mleteacher_l\"] = colors[\"lfd-mcts_mleteacher_xs\"]\n"
]
},
{
"cell_type": "code",
- "execution_count": 5,
+ "execution_count": 9,
"metadata": {},
"outputs": [],
"source": [
- "colors = {}\n",
- "\n",
- "colors[\"random\"] = \"black\"\n",
- "colors[\"truth\"] = \"black\"\n",
- "colors[\"mle\"] = \"black\"\n",
+ "alphas = {}\n",
"\n",
- "colors[\"greedy\"] = \"#0A3D5C\"\n",
- "colors[\"beamsearch_s\"] = \"#3D708F\"\n",
- "colors[\"mcts_xs\"] = \"#F57547\"\n",
- "\n",
- "colors[\"beamsearch_m\"] = colors[\"beamsearch_s\"]\n",
- "colors[\"beamsearch_l\"] = colors[\"beamsearch_s\"]\n",
- "colors[\"beamsearch_xl\"] = colors[\"beamsearch_s\"]\n",
- "colors[\"mcts_s\"] = colors[\"mcts_xs\"]\n",
- "colors[\"mcts_m\"] = colors[\"mcts_xs\"]\n",
- "colors[\"mcts_l\"] = colors[\"mcts_xs\"]\n"
+ "for key, color in colors.items():\n",
+ " if color == blue2:\n",
+ " alphas[key] = 0.18\n",
+ " elif color == blue0 or color == bw0:\n",
+ " alphas[key] = 0.08\n",
+ " else:\n",
+ " alphas[key] = 0.12\n",
+ " "
]
},
{
"cell_type": "code",
- "execution_count": 6,
+ "execution_count": 10,
"metadata": {},
"outputs": [],
"source": [
@@ -125,7 +350,7 @@
"\n",
"linestyles[\"truth\"] = \"-.\"\n",
"linestyles[\"mle\"] = \":\"\n",
- "linestyles[\"random\"] = \":\"\n",
+ "linestyles[\"random\"] = \"-.\"\n",
"\n",
"linestyles[\"greedy\"] = \"--\"\n",
"\n",
@@ -137,20 +362,36 @@
"linestyles[\"mcts_xs\"] = \"-\"\n",
"linestyles[\"mcts_s\"] = \"-\"\n",
"linestyles[\"mcts_m\"] = \"-\"\n",
- "linestyles[\"mcts_l\"] = \"-\"\n"
+ "linestyles[\"mcts_l\"] = \"-\"\n",
+ "\n",
+ "linestyles[\"lfd-mcts_xs\"] = \":\"\n",
+ "linestyles[\"lfd-mcts_s\"] = \":\"\n",
+ "linestyles[\"lfd-mcts_m\"] = \":\"\n",
+ "linestyles[\"lfd-mcts_l\"] = \":\"\n",
+ "linestyles[\"lfd-mcts_mleteacher_xs\"] = \"-.\"\n",
+ "linestyles[\"lfd-mcts_mleteacher_s\"] = \"-.\"\n",
+ "linestyles[\"lfd-mcts_mleteacher_m\"] = \"-.\"\n",
+ "linestyles[\"lfd-mcts_mleteacher_l\"] = \"-.\"\n",
+ "\n",
+ "linestyles[\"lfd\"] = \":\"\n",
+ "linestyles[\"lfd_mleteacher\"] = \"-\"\n"
]
},
{
"cell_type": "code",
- "execution_count": 7,
+ "execution_count": 11,
"metadata": {},
"outputs": [],
"source": [
"markers = {}\n",
"\n",
- "markers[\"beamsearch_s\"] = \"^\"\n",
- "markers[\"greedy\"] = \"s\"\n",
- "markers[\"mcts_xs\"] = \"o\"\n"
+ "markers[\"beamsearch_s\"] = \"v\"\n",
+ "markers[\"greedy\"] = \"^\"\n",
+ "markers[\"mcts_xs\"] = \"s\"\n",
+ "markers[\"lfd\"] = \"<\"\n",
+ "markers[\"lfd_mleteacher\"] = \">\"\n",
+ "markers[\"lfd-mcts_xs\"] = \"D\"\n",
+ "markers[\"lfd-mcts_mleteacher_xs\"] = \"D\"\n"
]
},
{
@@ -162,19 +403,9 @@
},
{
"cell_type": "code",
- "execution_count": 8,
+ "execution_count": 12,
"metadata": {},
- "outputs": [
- {
- "name": "stdout",
- "output_type": "stream",
- "text": [
- "[Errno 2] No such file or directory: './data/runs/truth_20200911_091636/eval_likelihood_evaluations.npy'\n",
- "[Errno 2] No such file or directory: './data/runs/mle_20200911_092100/eval_likelihood_evaluations.npy'\n",
- "[Errno 2] No such file or directory: './data/runs/random_20200911_092105/eval_likelihood_evaluations.npy'\n"
- ]
- }
- ],
+ "outputs": [],
"source": [
"n_jets = 500\n",
"run_dir = \"./data/runs\"\n",
@@ -183,51 +414,109 @@
"log_likelihoods = {}\n",
"costs = {}\n",
"\n",
- "for key, run in runs.items():\n",
- " try:\n",
- " log_likelihoods[key] = np.load(f\"{run_dir}/{run}/eval_log_likelihood.npy\").flatten()\n",
- " jet_sizes = np.load(f\"{run_dir}/{run}/eval_jet_sizes.npy\")\n",
- " except Exception as e:\n",
- " print(e)\n",
- " log_likelihoods[key] = np.nan * np.ones(n_jets)\n",
+ "for key, run_list in runs.items():\n",
+ " log_likelihoods[key] = []\n",
+ " costs[key] = []\n",
" \n",
- " try:\n",
- " costs[key] = np.load(f\"{run_dir}/{run}/eval_likelihood_evaluations.npy\").flatten()\n",
- " except Exception as e:\n",
- " print(e)\n",
- " costs[key] = np.nan * np.ones(n_jets)\n"
+ " for run in run_list:\n",
+ " try:\n",
+ " jet_sizes = np.load(f\"{run_dir}/{run}/eval_jet_sizes.npy\")\n",
+ " except Exception as e:\n",
+ " pass\n",
+ " # print(e)\n",
+ " \n",
+ " try:\n",
+ " log_likelihoods[key].append(\n",
+ " np.load(f\"{run_dir}/{run}/eval_log_likelihood.npy\").flatten()\n",
+ " )\n",
+ " except Exception as e:\n",
+ " # print(e)\n",
+ " log_likelihoods[key].append(np.nan * np.ones(n_jets))\n",
+ "\n",
+ " try:\n",
+ " costs[key].append(\n",
+ " np.load(f\"{run_dir}/{run}/eval_likelihood_evaluations.npy\").flatten()\n",
+ " )\n",
+ " except Exception as e:\n",
+ " # print(e)\n",
+ " costs[key].append(np.nan * np.ones(n_jets))\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
- "## Normalize results"
+ "## Process results"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "### Normalize wrt baseline"
]
},
{
"cell_type": "code",
- "execution_count": 9,
+ "execution_count": 13,
"metadata": {},
"outputs": [],
"source": [
"norm_key = \"greedy\"\n",
"rel_log_likelihoods = {}\n",
+ "norm_log_likelihoods = np.mean(log_likelihoods[norm_key], axis=0)\n",
"\n",
- "for key, val in log_likelihoods.items():\n",
- " rel_log_likelihoods[key] = val - log_likelihoods[norm_key]\n"
+ "for key, vals in log_likelihoods.items():\n",
+ " rel_log_likelihoods[key] = [val - norm_log_likelihoods for val in vals]\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
- "## Bin results"
+ "### Compute means and standard deviations"
]
},
{
"cell_type": "code",
- "execution_count": 10,
+ "execution_count": 14,
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stderr",
+ "output_type": "stream",
+ "text": [
+ "/Users/johannbrehmer/anaconda3/envs/rl/lib/python3.7/site-packages/ipykernel_launcher.py:2: RuntimeWarning: Mean of empty slice\n",
+ " \n",
+ "/Users/johannbrehmer/anaconda3/envs/rl/lib/python3.7/site-packages/numpy/lib/nanfunctions.py:1667: RuntimeWarning: Degrees of freedom <= 0 for slice.\n",
+ " keepdims=keepdims)\n"
+ ]
+ }
+ ],
+ "source": [
+ "def compute_means_stds(inputs):\n",
+ " means = {key: np.nanmean(val, axis=0) for key, val in inputs.items()}\n",
+ " stds = {key: np.nanstd(val, axis=0) / np.sum(np.isfinite(val), axis=0)**0.5 for key, val in inputs.items()}\n",
+ " \n",
+ " return means, stds\n",
+ "\n",
+ "\n",
+ "log_likelihood_means, log_likelihood_stds = compute_means_stds(log_likelihoods)\n",
+ "rel_log_likelihood_means, rel_log_likelihood_stds = compute_means_stds(rel_log_likelihoods)\n",
+ "cost_means, cost_stds = compute_means_stds(costs)\n",
+ "cost_stds = {}\n"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "### Bin results in number of jets"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 15,
"metadata": {},
"outputs": [],
"source": [
@@ -240,7 +529,7 @@
},
{
"cell_type": "code",
- "execution_count": 11,
+ "execution_count": 16,
"metadata": {},
"outputs": [],
"source": [
@@ -255,10 +544,13 @@
" \n",
" for bin_min, bin_max in zip(bin_boundaries[:-1], bin_boundaries[1:]):\n",
" x = jet_sizes[(jet_sizes >= bin_min) * (jet_sizes < bin_max)]\n",
- " y = rel_log_likelihoods[key][(jet_sizes >= bin_min) * (jet_sizes < bin_max)]\n",
+ " y = rel_log_likelihood_means[key][(jet_sizes >= bin_min) * (jet_sizes < bin_max)]\n",
+ " dy = rel_log_likelihood_stds[key][(jet_sizes >= bin_min) * (jet_sizes < bin_max)]\n",
+ " \n",
" bin_jet_size_.append(np.mean(x))\n",
" bin_log_likelihood_.append(np.mean(y))\n",
- " bin_log_likelihood_err_.append(np.std(y) / (len(x)**0.5 + 1.e-9))\n",
+ " # bin_log_likelihood_err_.append(np.std(y) / (len(x)**0.5 + 1.e-9))\n",
+ " bin_log_likelihood_err_.append(np.mean(dy))\n",
" \n",
" bin_jet_size[key] = np.asarray(bin_jet_size_)\n",
" bin_log_likelihood[key] = np.asarray(bin_log_likelihood_)\n",
@@ -272,16 +564,23 @@
"## Plots"
]
},
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "### Performance vs cost"
+ ]
+ },
{
"cell_type": "code",
- "execution_count": 24,
+ "execution_count": 17,
"metadata": {},
"outputs": [
{
"data": {
- "image/png": "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\n",
+ "image/png": "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4x//mIqKChYvXkwgEODDDz9k9erV/P73v6eiooJPfepT1NTUsH37dtauXcvVV1+N1+tl9OjRnHPOOQCICNdeey33338/dXV1rFu3jgsuuCAlP288DPmWVdhxKC8pZniODbM12HW0qkJhB4BQ2Ela62rGjBk88sgjne//7d/+jerqaiorKwHIzf1kpG9V5ZFHHmHKlClHbUNV+cUvfsHSpUuPmv70009HvYJ93XXXsWzZMrKysli+fDm+NC55NORbVpk+HwvHjqEs3+6tGuy6tqo6JKt1dc455xAIBPjlL3/ZOa2lpfsO/qVLl/KLX/wCjcT69ttvd07/5S9/STBSELKqqorm5mYWLVrEQw89RDgcZv/+/bz00kud2xo9ejSjR4/mRz/6EV/+8pcT9NMlR9qkWRFZBiybNCl+neCtwSA1La2MHpaPx+6tGvS2fHSos1XVIRR2eP+jxHcBiAiPPfYYt956K3fddRfDhw8nNzeXn/zkJ7S2th617Pe//32+9rWvMXv2bFSVCRMm8NRTT/HXf/3X7N69m7lz56KqDB8+nMcee4zLLruMF198kVmzZlFeXs7ZZ5991PauueYaDh8+zPTp0xP+cyaS6DH/aQa6yspKXb9+fVy29c6Bg7y+92OumjmdgqysuGzTmIHmlltu4dRTT+UrX/lKqkPplYhsUNXK7ualTcsq3lSVquoaRubmWqIyg9a8efPIzc3lpz/9aapDOWFDNllVt7RwJBDgrPHjUh2KMQmzYcOGVIcQN0O2g31bTS1eEU6xulXGpIUhmazCjsOOmlomFBWSmcaXco0ZSoZkstpTX09bOEx5SXGqQzHGxGhIJquq6lpy/H7GDBuW6lCMMTEacsmqJRjkw/p6JpcU271VJqm8Xi8VFRXMmTOHuXPn8pe//CXVISXEfffdxy233BL37Q65DpvGtnbyMzOZYqeAQ87D722h5pgbMAFKsrO5csa0hO8/Ozubd95x75Z/7rnn+M53vsPLL7+c8P0mSigUSurjO0OuZTUyL5erZk6nKDs71aGYJBuZl3tca9ojwsi83ChrJE5DQwNFXUb8vvvuuztLv/zDP/xD5/RLL72UefPmMWPGDFauXNk5PS8vj29961vMmzePc889lzfeeIPFixdz8skn88QTTxy3v/3797No0SIqKiqYOXMmr7zyCuA+NL1w4ULmzp3L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\n",
"text/plain": [
- ""
+ ""
]
},
"metadata": {
@@ -292,54 +591,146 @@
],
"source": [
"plot = [\n",
- " [\"greedy\"],\n",
- " [\"beamsearch_s\", \"beamsearch_m\", \"beamsearch_l\", \"beamsearch_xl\"],\n",
- " [\"mcts_xs\", \"mcts_s\", \"mcts_m\", \"mcts_l\"]\n",
+ " ([\"greedy\"], 6.),\n",
+ " ([\"beamsearch_s\", \"beamsearch_m\", \"beamsearch_l\", \"beamsearch_xl\"], 5.),\n",
+ " ([\"mcts_xs\", \"mcts_s\", \"mcts_m\", \"mcts_l\"], 8.),\n",
"]\n",
"\n",
- "fig = plt.figure(figsize=(3.6,3.6))\n",
+ "fig = plt.figure(figsize=(4.0, 4.0))\n",
"ax = plt.gca()\n",
" \n",
- "for keys in plot:\n",
- " xs = [np.mean(costs[key]) for key in keys]\n",
- " ys = [np.mean(log_likelihoods[key]) for key in keys]\n",
- " c, m, ms, lbl = colors[keys[0]], markers[keys[0]], 6., cost_labels[keys[0]]\n",
+ "for (keys, z) in plot:\n",
+ " xs = np.asarray([np.mean(cost_means[key]) for key in keys])\n",
+ " ys = np.asarray([np.mean(log_likelihood_means[key]) for key in keys])\n",
+ " dys = np.asarray([np.mean(log_likelihood_stds[key]) for key in keys])\n",
+ " c, m, ms, lbl, alpha = colors[keys[0]], markers[keys[0]], 6., labels[keys[0]], alphas[keys[0]]\n",
" ls = linestyles[keys[0]] if len(xs) > 1 else \" \"\n",
" \n",
" # Include greedy in beam search\n",
" if keys[0] == \"beamsearch_s\":\n",
+ " x0 = np.mean(cost_means[\"greedy\"])\n",
+ " y0 = np.mean(log_likelihood_means[\"greedy\"])\n",
" plt.plot(\n",
- " [np.mean(costs[\"greedy\"]), xs[0]],\n",
- " [np.mean(log_likelihoods[\"greedy\"]), ys[0]],\n",
+ " [x0, xs[0]], [y0, ys[0]],\n",
" c=c, lw=1.5, ls=ls, zorder=-1\n",
" )\n",
"\n",
- " plt.plot(xs, ys, c=c, lw=1.5, ls=ls, marker=m, markersize=ms, label=lbl)\n",
+ " # Uncertainty bands\n",
+ " if len(xs) > 1 and np.nanmax(dys) > 1.e-6:\n",
+ " plt.fill_between(xs, ys - dys, ys + dys, color=c, alpha=alpha)\n",
+ " elif len(xs) == 1 and np.nanmax(dys) > 1.e-6:\n",
+ " plt.fill_between([xs[0] / 1.15, xs[0] * 1.15], list(ys - dys)*2, list(ys + dys)*2, color=c, alpha=alpha)\n",
+ " \n",
+ " plt.plot(xs, ys, c=c, lw=1.5, ls=ls, marker=m, markersize=ms, label=lbl, zorder=z)\n",
"\n",
- "plt.legend(loc=\"lower right\", frameon=False)\n",
+ "plt.legend(loc=\"lower right\", frameon=False, handlelength=4)\n",
"\n",
"plt.xscale(\"log\")\n",
"ax.yaxis.set_major_locator(MultipleLocator(1.))\n",
"ax.yaxis.set_minor_locator(MultipleLocator(0.2))\n",
"plt.xlabel(\"Cost (splitting likelihood evaluations)\")\n",
- "plt.ylabel(f\"Tree log likelihood\", labelpad=0)\n",
+ "plt.ylabel(f\"Tree log likelihood\")\n",
"ax.xaxis.set_label_coords(0.5, -0.1)\n",
- "ax.yaxis.set_label_coords(-0.12, 0.5)\n",
+ "ax.yaxis.set_label_coords(-0.11, 0.5)\n",
+ "plt.ylim(-96.2,-92.6)\n",
"\n",
- "plt.subplots_adjust(left=0.15, bottom=0.15, right=0.99, top=0.99, wspace=0, hspace=0)\n",
+ "plt.subplots_adjust(left=0.13, bottom=0.13, right=0.99, top=0.99, wspace=0, hspace=0)\n",
"plt.savefig(\"figures/rl_ginkgo_log_likelihood_vs_cost.pdf\")\n"
]
},
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "### Performance vs # leaves"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 18,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "# plot = [\"mle\", \"greedy\", \"beamsearch_xl\", \"mcts_l\"]\n",
+ "\n",
+ "# fig = plt.figure(figsize=(4.0, 4.0))\n",
+ "# ax = plt.gca()\n",
+ " \n",
+ "# for key in plot:\n",
+ "# x, y, dy = bin_jet_size[key], bin_log_likelihood[key], bin_log_likelihood_err[key]\n",
+ "# c, ls, lbl, alpha = colors[key], linestyles[key], labels[key], alphas[key]\n",
+ " \n",
+ "# if np.max(dy) > 1.e-3:\n",
+ "# plt.fill_between(x, y-dy, y+dy, color=c, alpha=alpha)\n",
+ "# plt.plot(x, y, c=c, lw=1.5, ls=ls, label=lbl)\n",
+ "\n",
+ "# plt.legend(loc=\"upper left\", frameon=False)\n",
+ "\n",
+ "# ax.yaxis.set_major_locator(MultipleLocator(1.))\n",
+ "# ax.yaxis.set_minor_locator(MultipleLocator(0.2))\n",
+ "# ax.xaxis.set_major_locator(MultipleLocator(5.))\n",
+ "# ax.xaxis.set_minor_locator(MultipleLocator(1.))\n",
+ "# plt.xlabel(\"Number of leaves\")\n",
+ "# plt.ylabel(f\"Tree log likelihood relative to {norm_key}\")\n",
+ "# ax.xaxis.set_label_coords(0.5, -0.11)\n",
+ "# ax.yaxis.set_label_coords(-0.11, 0.5)\n",
+ "# plt.ylim(-0.2, 4.6)\n",
+ "\n",
+ "# plt.subplots_adjust(left=0.15, bottom=0.15, right=0.99, top=0.99, wspace=0, hspace=0)\n",
+ "# plt.savefig(\"figures/rl_ginkgo_log_likelihood_vs_leaves1.pdf\")\n"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 19,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "# plot = [\n",
+ "# (\"random\", 3),\n",
+ "# (\"greedy\", 4),\n",
+ "# (\"lfd\",6), \n",
+ "# (\"lfd_mleteacher\", 5),\n",
+ "# ]\n",
+ "\n",
+ "# fig = plt.figure(figsize=(4.0, 4.0))\n",
+ "# ax = plt.gca()\n",
+ " \n",
+ "# for key, z in plot:\n",
+ "# x, y, dy = bin_jet_size[key], bin_log_likelihood[key], bin_log_likelihood_err[key]\n",
+ "# c, ls, lbl, alpha = colors[key], linestyles[key], labels[key], alphas[key]\n",
+ " \n",
+ "# if np.max(dy) > 1.e-3:\n",
+ "# plt.fill_between(x, y-dy, y+dy, color=c, alpha=alpha)\n",
+ "# plt.plot(x, y, c=c, lw=1.5, ls=ls, label=lbl, zorder=z)\n",
+ "\n",
+ "# plt.legend(loc=\"lower left\", frameon=False)\n",
+ "\n",
+ "# ax.yaxis.set_major_locator(MultipleLocator(50.))\n",
+ "# ax.yaxis.set_minor_locator(MultipleLocator(10.))\n",
+ "# ax.xaxis.set_major_locator(MultipleLocator(5.))\n",
+ "# ax.xaxis.set_minor_locator(MultipleLocator(1.))\n",
+ "# plt.xlabel(\"Number of leaves\")\n",
+ "# plt.ylabel(f\"Tree log likelihood relative to {norm_key}\")\n",
+ "# ax.xaxis.set_label_coords(0.5, -0.11)\n",
+ "# ax.yaxis.set_label_coords(-0.12, 0.5)\n",
+ "# ax.yaxis.set_tick_params(pad=0.6) \n",
+ "# plt.ylim(-105, 5)\n",
+ "\n",
+ "# plt.subplots_adjust(left=0.15, bottom=0.15, right=0.99, top=0.99, wspace=0, hspace=0)\n",
+ "# plt.savefig(\"figures/rl_ginkgo_log_likelihood_vs_leaves2.pdf\")\n"
+ ]
+ },
{
"cell_type": "code",
- "execution_count": 25,
+ "execution_count": 20,
"metadata": {},
"outputs": [
{
"data": {
- "image/png": "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\n",
+ "image/png": "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\n",
"text/plain": [
- ""
+ ""
]
},
"metadata": {
@@ -349,30 +740,55 @@
}
],
"source": [
- "plot = [\"mle\", \"greedy\", \"beamsearch_xl\", \"mcts_l\"]\n",
+ "plot = [\"mle\", \"random\", \"greedy\", \"beamsearch_xl\", \"mcts_l\", \"lfd\"]\n",
+ "\n",
+ "fig = plt.figure(figsize=(4.0,4.0))\n",
"\n",
- "fig = plt.figure(figsize=(3.6,3.6))\n",
+ "# Larger plot\n",
"ax = plt.gca()\n",
" \n",
"for key in plot:\n",
" x, y, dy = bin_jet_size[key], bin_log_likelihood[key], bin_log_likelihood_err[key]\n",
- " c, ls, lbl = colors[key], linestyles[key], labels[key]\n",
- "\n",
- " plt.fill_between(x, y-dy, y+dy, color=c, alpha=0.1)\n",
+ " c, ls, lbl, alpha = colors[key], linestyles[key], labels[key], alphas[key]\n",
+ " if np.max(dy) > 1.e-3:\n",
+ " plt.fill_between(x, y-dy, y+dy, color=c, alpha=alpha)\n",
" plt.plot(x, y, c=c, lw=1.5, ls=ls, label=lbl)\n",
"\n",
"plt.legend(loc=\"upper left\", frameon=False)\n",
"\n",
"ax.yaxis.set_major_locator(MultipleLocator(1.))\n",
- "ax.yaxis.set_minor_locator(MultipleLocator(0.5))\n",
+ "ax.yaxis.set_minor_locator(MultipleLocator(0.2))\n",
"ax.xaxis.set_major_locator(MultipleLocator(5.))\n",
"ax.xaxis.set_minor_locator(MultipleLocator(1.))\n",
"plt.xlabel(\"Number of leaves\")\n",
"plt.ylabel(f\"Tree log likelihood relative to {norm_key}\")\n",
"ax.xaxis.set_label_coords(0.5, -0.1)\n",
- "ax.yaxis.set_label_coords(-0.12, 0.5)\n",
+ "ax.yaxis.set_label_coords(-0.1, 0.5)\n",
+ "plt.xlim(5.5,17.5)\n",
+ "plt.ylim(-0.15, 4.5)\n",
+ "\n",
+ "# Smaller plot\n",
+ "ax2 = inset_axes(\n",
+ " ax,\n",
+ " width=\"100%\", height=\"100%\",\n",
+ " bbox_to_anchor=(0.57, .13, .38, .38), \n",
+ " bbox_transform=ax.transAxes, loc=3, borderpad=0\n",
+ ")\n",
+ " \n",
+ "for key in plot:\n",
+ " x, y, dy = bin_jet_size[key], bin_log_likelihood[key], bin_log_likelihood_err[key]\n",
+ " c, ls, lbl, alpha = colors[key], linestyles[key], labels[key], alphas[key]\n",
+ " if np.max(dy) > 1.e-3:\n",
+ " plt.fill_between(x, y-dy, y+dy, color=c, alpha=alpha)\n",
+ " plt.plot(x, y, c=c, lw=1.5, ls=ls)\n",
+ "\n",
+ "ax2.yaxis.set_major_locator(MultipleLocator(50.))\n",
+ "ax2.xaxis.set_major_locator(MultipleLocator(5.))\n",
+ "plt.xlim(5.5, 17.5)\n",
+ "plt.ylim(-175,10)\n",
"\n",
- "plt.subplots_adjust(left=0.15, bottom=0.15, right=0.99, top=0.99, wspace=0, hspace=0)\n",
+ "\n",
+ "plt.subplots_adjust(left=0.13, bottom=0.13, right=0.99, top=0.99, wspace=0, hspace=0)\n",
"plt.savefig(\"figures/rl_ginkgo_log_likelihood_vs_leaves.pdf\")\n"
]
},
@@ -380,46 +796,62 @@
"cell_type": "markdown",
"metadata": {},
"source": [
- "## All results"
+ "## Print results"
]
},
{
"cell_type": "code",
- "execution_count": 14,
+ "execution_count": 21,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
- " truth: $ -99.26 \\textcolor{dark-grey}{\\pm 1.12}$\n",
- " mle: $ nan \\textcolor{dark-grey}{\\pm nan}$\n",
- " random: $-202.23 \\textcolor{dark-grey}{\\pm 4.95}$\n",
- " greedy: $ -95.96 \\textcolor{dark-grey}{\\pm 1.06}$\n",
- " beamsearch_s: $ -94.07 \\textcolor{dark-grey}{\\pm 1.04}$\n",
- " beamsearch_m: $ -93.49 \\textcolor{dark-grey}{\\pm 1.03}$\n",
- " beamsearch_l: $ -93.30 \\textcolor{dark-grey}{\\pm 1.02}$\n",
- " beamsearch_xl: $ -93.28 \\textcolor{dark-grey}{\\pm 1.02}$\n",
- " mcts_xs: $ -93.44 \\textcolor{dark-grey}{\\pm 1.02}$\n",
- " mcts_s: $ -93.25 \\textcolor{dark-grey}{\\pm 1.02}$\n",
- " mcts_m: $ -93.01 \\textcolor{dark-grey}{\\pm 1.02}$\n",
- " mcts_l: $ -92.79 \\textcolor{dark-grey}{\\pm 1.02}$\n",
- " mcts_raw: $ -93.76 \\textcolor{dark-grey}{\\pm 1.04}$\n",
- " mcts_onlybs: $ -93.85 \\textcolor{dark-grey}{\\pm 1.04}$\n",
- " mcts_nobs: $ -95.20 \\textcolor{dark-grey}{\\pm 1.07}$\n",
- " mcts_random: $ -93.85 \\textcolor{dark-grey}{\\pm 1.04}$\n",
- " mcts_likelihood: $ -93.47 \\textcolor{dark-grey}{\\pm 1.02}$\n"
+ " truth: $ -99.3_{\\textcolor{dark-grey}{\\hphantom{0} \\pm 0.0}}$ & 0\n",
+ " mle: & 0\n",
+ " random: $-198.3_{\\textcolor{dark-grey}{\\pm 23.8}}$ & 0\n",
+ " greedy: $ -96.0_{\\textcolor{dark-grey}{\\hphantom{0} \\pm 0.0}}$ & 452\n",
+ " beamsearch_s: $ -94.1_{\\textcolor{dark-grey}{\\hphantom{0} \\pm 0.0}}$ & 1914\n",
+ " beamsearch_m: $ -93.5_{\\textcolor{dark-grey}{\\hphantom{0} \\pm 0.0}}$ & 7397\n",
+ " beamsearch_l: $ -93.3_{\\textcolor{dark-grey}{\\hphantom{0} \\pm 0.0}}$ & 36641\n",
+ " beamsearch_xl: $ -93.3_{\\textcolor{dark-grey}{\\hphantom{0} \\pm 0.0}}$ & 365634\n",
+ " mcts_xs: $ -93.4_{\\textcolor{dark-grey}{\\hphantom{0} \\pm 0.1}}$ & 12272\n",
+ " mcts_s: $ -93.3_{\\textcolor{dark-grey}{\\hphantom{0} \\pm 0.1}}$ & 22592\n",
+ " mcts_m: $ -93.0_{\\textcolor{dark-grey}{\\hphantom{0} \\pm 0.1}}$ & 70315\n",
+ " mcts_l: $ -92.8_{\\textcolor{dark-grey}{\\hphantom{0} \\pm 0.1}}$ & 286233\n",
+ " lfd-mcts_xs: $ -93.5_{\\textcolor{dark-grey}{\\hphantom{0} \\pm 0.1}}$ & 13288\n",
+ " lfd-mcts_s: $ -93.3_{\\textcolor{dark-grey}{\\hphantom{0} \\pm 0.1}}$ & 24440\n",
+ " lfd-mcts_m: $ -93.0_{\\textcolor{dark-grey}{\\hphantom{0} \\pm 0.1}}$ & 75818\n",
+ " lfd-mcts_l: $ -92.8_{\\textcolor{dark-grey}{\\hphantom{0} \\pm 0.1}}$ & 293765\n",
+ "lfd-mcts_mleteacher_xs: $ -93.5_{\\textcolor{dark-grey}{\\hphantom{0} \\pm 0.1}}$ & 13382\n",
+ " lfd-mcts_mleteacher_s: $ -93.3_{\\textcolor{dark-grey}{\\hphantom{0} \\pm 0.1}}$ & 24895\n",
+ " lfd-mcts_mleteacher_m: $ -93.0_{\\textcolor{dark-grey}{\\hphantom{0} \\pm 0.1}}$ & 75432\n",
+ " lfd-mcts_mleteacher_l: & \n",
+ " lfd: $-108.5_{\\textcolor{dark-grey}{\\hphantom{0} \\pm 1.6}}$ & 452\n",
+ " lfd_mleteacher: $-108.3_{\\textcolor{dark-grey}{\\hphantom{0} \\pm 1.7}}$ & 452\n",
+ " mcts_raw: $ -93.8_{\\textcolor{dark-grey}{\\hphantom{0} \\pm 0.0}}$ & 30434\n",
+ " mcts_explore: $ -93.3_{\\textcolor{dark-grey}{\\hphantom{0} \\pm 0.1}}$ & 24210\n",
+ " mcts_exploit: $ -93.4_{\\textcolor{dark-grey}{\\hphantom{0} \\pm 0.1}}$ & 16525\n",
+ " mcts_puctdecisions: $ -94.6_{\\textcolor{dark-grey}{\\hphantom{0} \\pm 0.3}}$ & 15022\n",
+ " mcts_onlybs: $ -93.9_{\\textcolor{dark-grey}{\\hphantom{0} \\pm 0.0}}$ & 7397\n",
+ " mcts_nobs: $ -97.3_{\\textcolor{dark-grey}{\\hphantom{0} \\pm 1.3}}$ & 6475\n",
+ " mcts_random: $ -93.9_{\\textcolor{dark-grey}{\\hphantom{0} \\pm 0.0}}$ & 26718\n",
+ " mcts_likelihood: $ -93.5_{\\textcolor{dark-grey}{\\hphantom{0} \\pm 0.0}}$ & 28015\n"
]
}
],
"source": [
- "for key, val in log_likelihoods.items():\n",
- " mean = np.mean(val)\n",
- " err = np.std(val)/len(val)**0.5\n",
- " print(\n",
- " f\"{key:>20.20}: ${mean:7.2f} \"\n",
- " + f\"\\\\textcolor{{dark-grey}}{{\\pm {err:4.2f}}}$\"\n",
- " )\n",
+ "for key in runs.keys():\n",
+ " mean = np.mean(log_likelihood_means[key])\n",
+ " err = np.mean(log_likelihood_stds[key])\n",
+ " cost = np.mean(cost_means[key])\n",
+ " \n",
+ " result_str = (f\"${mean:6.1f}_{{\\\\textcolor{{dark-grey}}{{\\pm {err:04.1f}}}}}$\" if np.isfinite(mean) else \" \"*42)\n",
+ " result_str = result_str.replace(\"\\pm 0\", \"\\hphantom{0} \\pm \")\n",
+ " cost_str = (f\"{cost:6.0f}\" if np.isfinite(cost) else \" \"*6)\n",
+ " \n",
+ " print(f\"{key:>22.22}: {result_str} & {cost_str}\")\n",
" "
]
},
diff --git a/ginkgo_rl/__init__.py b/ginkgo_rl/__init__.py
index 069cf93..009babb 100644
--- a/ginkgo_rl/__init__.py
+++ b/ginkgo_rl/__init__.py
@@ -1,3 +1,3 @@
from .envs import GinkgoLikelihoodEnv, GinkgoLikelihood1DEnv
-from .eval import GinkgoEvaluator
+from .eval import GinkgoEvaluator, GinkgoRLInterface
from .agents import BatchedACERAgent, RandomMCTSAgent, PolicyMCTSAgent, GreedyAgent, RandomAgent, LikelihoodMCTSAgent, ImitationLearningPolicyMCTSAgent
diff --git a/ginkgo_rl/agents/acer.py b/ginkgo_rl/agents/acer.py
index 465d6a3..9fef9bc 100644
--- a/ginkgo_rl/agents/acer.py
+++ b/ginkgo_rl/agents/acer.py
@@ -22,7 +22,9 @@ class BatchedActorCriticAgent(Agent):
def __init__(
self,
*args,
- log_likelihood_feature=True, hidden_sizes=(100, 100,), activation=nn.ReLU(),
+ log_likelihood_feature=True,
+ hidden_sizes=(100, 100,),
+ activation=nn.ReLU(),
log_epsilon=-20.0,
**kwargs,
):
@@ -31,7 +33,13 @@ def __init__(
self.log_epsilon = log_epsilon
self.log_likelihood_feature = log_likelihood_feature
- self.actor_critic = MultiHeadedMLP(1 + self.state_length, hidden_sizes=hidden_sizes, head_sizes=(1, 1), activation=activation, head_activations=(None, None),)
+ self.actor_critic = MultiHeadedMLP(
+ 1 + self.state_length,
+ hidden_sizes=hidden_sizes,
+ head_sizes=(1, 1),
+ activation=activation,
+ head_activations=(None, None),
+ )
self.softmax = nn.Softmax(dim=0)
def _predict(self, state):
@@ -42,7 +50,15 @@ def _predict(self, state):
return (
legal_actions[action_id],
- {"legal_actions": legal_actions, "action_id": action_id, "log_probs": log_probs, "log_prob": log_probs[action_id], "values": qs, "value": qs[action_id], "likelihood_evaluations": 0},
+ {
+ "legal_actions": legal_actions,
+ "action_id": action_id,
+ "log_probs": log_probs,
+ "log_prob": log_probs[action_id],
+ "values": qs,
+ "value": qs[action_id],
+ "likelihood_evaluations": 0,
+ },
)
def _evaluate(self, states, legal_actions_list):
@@ -91,7 +107,7 @@ def _evaluate_batch_states(self, batch_states, pad=True):
return log_probs, qs
def _act(self, log_probs, legal_actions):
- probs = torch.exp(torch.clamp(log_probs, -20, 0.))
+ probs = torch.exp(torch.clamp(log_probs, -20, 0.0))
cat = Categorical(probs)
action_id = len(legal_actions)
while action_id >= len(legal_actions):
@@ -104,7 +120,10 @@ def _act(self, log_probs, legal_actions):
def _pad(self, inputs, value=None):
return torch.nn.functional.pad(
- inputs, (0, self.num_actions - inputs.size()[-1]), mode="constant", value=self.log_epsilon if value is None else value
+ inputs,
+ (0, self.num_actions - inputs.size()[-1]),
+ mode="constant",
+ value=self.log_epsilon if value is None else value,
)
def _parse_action(self, action, from_which_env="sim"):
@@ -133,7 +152,18 @@ def _parse_action(self, action, from_which_env="sim"):
class BatchedACERAgent(BatchedActorCriticAgent):
""" Largely following https://github.com/seungeunrho/minimalRL/blob/master/acer.py """
- def __init__(self, *args, rollout_len=10, minibatch=5, truncate=1.0, warmup=100, r_factor=1.0, actor_weight=1.0, critic_weight=1.0, **kwargs):
+ def __init__(
+ self,
+ *args,
+ rollout_len=10,
+ minibatch=5,
+ truncate=1.0,
+ warmup=100,
+ r_factor=1.0,
+ actor_weight=1.0,
+ critic_weight=1.0,
+ **kwargs,
+ ):
self.truncate = truncate
self.warmup = warmup
self.batchsize = minibatch
@@ -190,7 +220,9 @@ def _train(self, on_policy=True):
actor_loss = -rho_bar * log_prob_now_a * (q_ret - v)
actor_loss = actor_loss.mean()
- correction_loss = -correction_coeff * torch.exp(log_probs_then.detach()) * log_probs_now * (q.detach() - v) # bias correction term
+ correction_loss = (
+ -correction_coeff * torch.exp(log_probs_then.detach()) * log_probs_now * (q.detach() - v)
+ ) # bias correction term
correction_loss = correction_loss.sum(1).mean()
critic_loss = self.critic_weight * torch.nn.SmoothL1Loss()(q_a, q_ret)
loss = actor_loss + correction_loss + critic_loss
diff --git a/ginkgo_rl/agents/base.py b/ginkgo_rl/agents/base.py
index f7cfa42..cb191fb 100644
--- a/ginkgo_rl/agents/base.py
+++ b/ginkgo_rl/agents/base.py
@@ -5,6 +5,7 @@
from tqdm import trange
from ..utils.replay_buffer import History
+from ..utils.various import check_for_nans
logger = logging.getLogger(__name__)
@@ -12,7 +13,20 @@
class Agent(nn.Module):
""" Abstract base agent class """
- def __init__(self, env, gamma=1.00, lr=1.0e-3, lr_decay=0.01, weight_decay=0.0, history_length=None, dtype=torch.float, device=torch.device("cpu"), *args, **kwargs):
+ def __init__(
+ self,
+ env,
+ gamma=1.00,
+ lr=1.0e-3,
+ lr_decay=0.01,
+ weight_decay=0.0,
+ history_length=None,
+ clip_gradient=None,
+ dtype=torch.float,
+ device=torch.device("cpu"),
+ *args,
+ **kwargs,
+ ):
self.env = env
self.gamma = gamma
self.device = device
@@ -26,6 +40,7 @@ def __init__(self, env, gamma=1.00, lr=1.0e-3, lr_decay=0.01, weight_decay=0.0,
self.lr = lr
self.lr_decay = lr_decay
self.weight_decay = weight_decay
+ self.clip_gradient = clip_gradient
super().__init__()
@@ -37,24 +52,38 @@ def learn(self, total_timesteps, callback=None):
self.train()
if list(self.parameters()):
self.optimizer = torch.optim.Adam(params=self.parameters(), lr=self.lr, weight_decay=self.weight_decay)
- self.scheduler = torch.optim.lr_scheduler.ExponentialLR(self.optimizer, gamma=self.lr_decay**(1. / (total_timesteps + 1.0e-9)))
+ self.scheduler = torch.optim.lr_scheduler.ExponentialLR(
+ self.optimizer, gamma=self.lr_decay ** (1.0 / (total_timesteps + 1.0e-9))
+ )
else:
self.optimizer = None # For non-NN methods
self.scheduler = None
+ # Prepare episodes
state = self.env.reset()
- reward = 0.
+ reward = 0.0
rewards = []
- episode = 0
-
+ episode = -1
+ done = True
episode_loss = 0.0
episode_reward = 0.0
episode_length = 0
for steps in trange(total_timesteps):
+ # Initialize episode
+ if done:
+ episode += 1
+ episode_loss = 0.0
+ episode_reward = 0.0
+ episode_length = 0
+ state = self.env.reset()
+ self.init_episode()
+
+ # Agent and environment step
action, agent_info = self.predict(state)
next_state, next_reward, done, env_info = self.env.step(action)
+ # Learning
loss = self.update(
state=self._tensorize(state),
reward=reward,
@@ -63,9 +92,10 @@ def learn(self, total_timesteps, callback=None):
next_state=self._tensorize(next_state),
next_reward=next_reward,
num_episode=episode,
- **agent_info
+ **agent_info,
)
+ # Book keeping
episode_loss += loss
episode_reward += next_reward
episode_length += 1
@@ -73,15 +103,17 @@ def learn(self, total_timesteps, callback=None):
state = next_state
reward = next_reward
- if done:
- if callback is not None:
- callback(callback_info={"episode": episode, "episode_length": episode_length, "loss": episode_loss, "reward": episode_reward, "likelihood_evaluations": agent_info["likelihood_evaluations"], "mean_abs_weight":self.get_mean_weight()})
-
- episode += 1
- episode_loss = 0.0
- episode_reward = 0.0
- episode_length = 0
- state = self.env.reset()
+ if done and callback is not None:
+ callback(
+ callback_info={
+ "episode": episode,
+ "episode_length": episode_length,
+ "loss": episode_loss,
+ "reward": episode_reward,
+ "likelihood_evaluations": agent_info["likelihood_evaluations"],
+ "mean_abs_weight": self.get_mean_weight(),
+ }
+ )
def predict(self, state):
"""
@@ -105,6 +137,10 @@ def predict(self, state):
state = self._tensorize(state)
return self._predict(state)
+ def init_episode(self):
+ """ Is called at the beginning of an episode """
+ pass
+
def update(self, state, reward, action, done, next_state, next_reward, num_episode, **kwargs):
"""
Is called at the end of each step, gives the agent the chance to a) update the replay buffer and b) learn its weights.
@@ -117,11 +153,20 @@ def _init_replay_buffer(self, history_length):
def _tensorize(self, array):
tensor = array if isinstance(array, torch.Tensor) else torch.tensor(array)
tensor = tensor.to(self.device, self.dtype)
+ check_for_nans(f"Tensorizing state {array}", tensor)
return tensor
def _gradient_step(self, loss):
self.optimizer.zero_grad()
loss.backward()
+
+ if self.clip_gradient is not None:
+ if self.verbose > 2:
+ grad_norm = torch.nn.utils.clip_grad_norm_(self.parameters(), self.clip_gradient)
+ logger.debug(f"Gradient norm (clipping at {clip_gradient}): {grad_norm}")
+ else:
+ torch.nn.utils.clip_grad_norm_(self.parameters(), self.clip_gradient)
+
self.optimizer.step()
self.scheduler.step()
diff --git a/ginkgo_rl/agents/greedy.py b/ginkgo_rl/agents/greedy.py
index 800844f..102a8d2 100644
--- a/ginkgo_rl/agents/greedy.py
+++ b/ginkgo_rl/agents/greedy.py
@@ -7,12 +7,7 @@
class GreedyAgent(Agent):
- def __init__(
- self,
- *args,
- verbose=0,
- **kwargs
- ):
+ def __init__(self, *args, verbose=0, **kwargs):
super().__init__(*args, **kwargs)
self.verbose = verbose
self.sim_env = copy.deepcopy(self.env)
@@ -57,8 +52,5 @@ def _parse_action(self, action):
def _report_decision(self, legal_actions, log_likelihoods, chosen_action):
logger.debug(f"Greedy results:")
for i, (action_, log_likelihood) in enumerate(zip(legal_actions, log_likelihoods)):
- is_chosen = '*' if action_ == chosen_action else ' '
- logger.debug(
- f" {is_chosen} {action_:>2d}: "
- f"log likelihood = {log_likelihood:6.2f}"
- )
+ is_chosen = "*" if action_ == chosen_action else " "
+ logger.debug(f" {is_chosen} {action_:>2d}: " f"log likelihood = {log_likelihood:6.2f}")
diff --git a/ginkgo_rl/agents/imitation_learning.py b/ginkgo_rl/agents/imitation_learning.py
index 7733a2a..db1ac2f 100644
--- a/ginkgo_rl/agents/imitation_learning.py
+++ b/ginkgo_rl/agents/imitation_learning.py
@@ -31,11 +31,11 @@ def _predict_policy(self, state, demonstrator_action=None):
step_rewards = [self._parse_action(action, from_which_env="real") for action in legal_actions]
probs = self._evaluate_policy(state, legal_actions, step_rewards)
- probs = torch.clamp(probs, 1.e-6, 1.0)
+ probs = torch.clamp(probs, 1.0e-6, 1.0)
try:
cat = Categorical(probs)
action_id = cat.sample()
- except RuntimeError as e:
+ except RuntimeError:
logger.error(f"Error evaluating policy. Policy probabilities: {probs.detach().numpy()}")
raise
@@ -50,7 +50,7 @@ def _predict_policy(self, state, demonstrator_action=None):
"log_probs": log_probs,
"log_prob": log_probs[action_id],
"log_prob_demonstrator": log_prob_demo,
- "likelihood_evaluations": self.episode_likelihood_evaluations
+ "likelihood_evaluations": self.episode_likelihood_evaluations,
}
return action, info
@@ -63,25 +63,49 @@ def learn(self, total_timesteps, callback=None, mode="rl", teacher="truth"):
# Prepare training
self.train()
self.optimizer = torch.optim.Adam(params=self.parameters(), lr=self.lr, weight_decay=self.weight_decay)
- self.scheduler = torch.optim.lr_scheduler.ExponentialLR(self.optimizer, gamma=self.lr_decay**(1. / (total_timesteps + 1.0e-9)))
+ self.scheduler = torch.optim.lr_scheduler.ExponentialLR(
+ self.optimizer, gamma=self.lr_decay ** (1.0 / (total_timesteps + 1.0e-9))
+ )
- demonstration_actions = None
- while demonstration_actions is None:
- state = self.env.reset()
- demonstration_actions = deque(self._find_demonstration_actions(teacher=teacher))
-
- reward = 0.
+ demonstration_actions = []
rewards = []
- episode = 0
+ episode = -1
episode_loss = 0.0
episode_reward = 0.0
episode_length = 0
for _ in trange(total_timesteps):
- # Imitation learning
+ # Set up episode
+ if not demonstration_actions:
+ episode += 1
+ episode_loss = 0.0
+ episode_reward = 0.0
+ episode_length = 0
+ self.episode_likelihood_evaluations = 0
+
+ while not demonstration_actions:
+ state = self.env.reset()
+ demonstration_actions = deque(self._find_demonstration_actions(teacher=teacher))
+
+ # Find demonstrator action
demonstration_action = demonstration_actions.popleft()
+
+ # Make sure that demonstrator action is actually legal
+ # Rarely, this is not the case. I think it's due to ambiguous energy sorting, but haven't really been able to pin this down
+ legal_actions = self._find_legal_actions(state)
+ if demonstration_action not in legal_actions:
+ logger.error("Demonstrator action is not legal?!")
+ logger.error(f" State: {state}")
+ logger.error(f" Legal actions: {legal_actions}")
+ logger.error(f" Current demonstration action: {demonstration_action}")
+ logger.error(f" Jet: {self.env.jet}")
+
+ demonstration_actions = []
+ continue
+
+ # Imitation learning
_, agent_info = self._predict_policy(state, demonstrator_action=demonstration_action)
- loss = - agent_info["log_prob_demonstrator"]
+ loss = -agent_info["log_prob_demonstrator"]
self._gradient_step(loss)
# Transition to next step
@@ -92,22 +116,26 @@ def learn(self, total_timesteps, callback=None, mode="rl", teacher="truth"):
episode_length += 1
rewards.append(next_reward)
state = next_state
- reward = next_reward
-
- if done or not demonstration_actions:
- if callback is not None:
- callback(callback_info={"episode": episode, "episode_length": episode_length, "loss": episode_loss, "reward": episode_reward, "likelihood_evaluations": agent_info["likelihood_evaluations"], "mean_abs_weight":self.get_mean_weight()})
-
- episode += 1
- episode_loss = 0.0
- episode_reward = 0.0
- episode_length = 0
- self.episode_likelihood_evaluations = 0
- demonstration_actions = None
- while demonstration_actions is None:
- state = self.env.reset()
- demonstration_actions = deque(self._find_demonstration_actions(teacher=teacher))
+ # Episode is done but still demo actions? Episode not done, but no demo actions any more? Something's afoot!
+ if done == bool(demonstration_actions):
+ logger.warning(f"Inconsistent episode termination in imitation learning from teacher {teacher}.")
+ logger.warning(f" Done flag: {done}")
+ logger.warning(f" Demonstration actions left: {demonstration_actions}")
+ demonstration_actions = []
+ done = True
+
+ if done and callback is not None:
+ callback(
+ callback_info={
+ "episode": episode,
+ "episode_length": episode_length,
+ "loss": episode_loss,
+ "reward": episode_reward,
+ "likelihood_evaluations": agent_info["likelihood_evaluations"],
+ "mean_abs_weight": self.get_mean_weight(),
+ }
+ )
def _find_demonstration_actions(self, teacher="mle"):
""" From self.env.jet, find the sequence of true actions... or the MLE sequence of actions """
@@ -119,13 +147,15 @@ def _find_demonstration_actions(self, teacher="mle"):
elif teacher == "mle":
jet = self._get_maximum_likelihood_tree()
if jet is None:
- return None
+ jet = self.env.jet # We use MLE only for small jets, and otherwise stick to the truth jet
else:
raise ValueError(teacher)
- original_momenta = jet['content'] # original_momenta[i] are unmodified four-momenta of particle with ID i
- original_children = jet['tree'] # original_children[i] are original IDs of children of particle with ID i
- original_parents = {tuple(sorted([i, j])): parent for parent, (i, j) in enumerate(original_children) if i >= 0 and j >= 0} # original_parent[(i, j)] is parent ID of children IDs (i, j)
+ original_momenta = jet["content"] # original_momenta[i] are unmodified four-momenta of particle with ID i
+ original_children = jet["tree"] # original_children[i] are original IDs of children of particle with ID i
+ original_parents = {
+ tuple(sorted([i, j])): parent for parent, (i, j) in enumerate(original_children) if i >= 0 and j >= 0
+ } # original_parent[(i, j)] is parent ID of children IDs (i, j)
logger.debug("Children list:")
for parent, (i, j) in enumerate(original_children):
@@ -135,7 +165,9 @@ def _find_demonstration_actions(self, teacher="mle"):
for (i, j), parent in original_parents.items():
logger.debug(f" Children {i}, {j} -> parent {parent}")
- particles = [] # list of tuples (original ID, four-momenta) of all particles at current state, sorted by the energy
+ particles = (
+ []
+ ) # list of tuples (original ID, four-momenta) of all particles at current state, sorted by the energy
actions = [] # List of true actions
# Find leaves
@@ -145,8 +177,10 @@ def _find_demonstration_actions(self, teacher="mle"):
while len(particles) > 1:
# Sort particles by energy
- particles = sorted(particles, reverse=True, key=lambda x : x[1][0])
- particle_dict = {key : sorted_id for sorted_id, (key, _) in enumerate(particles)} # keys are particle ID, values are position in energy-sorted list
+ particles = sorted(particles, reverse=True, key=lambda x: x[1][0])
+ particle_dict = {
+ key: sorted_id for sorted_id, (key, _) in enumerate(particles)
+ } # keys are particle ID, values are position in energy-sorted list
logger.debug("Considering next clustering step.")
logger.debug(" Particle dictionary:")
@@ -184,13 +218,13 @@ def _find_demonstration_actions(self, teacher="mle"):
if idx == j:
del particles[pos]
break
- parent_ij = original_parents[(i,j)]
+ parent_ij = original_parents[(i, j)]
logger.debug(f" Removing particles {i} and {j} from list and adding {parent_ij}")
particles.append((parent_ij, original_momenta[parent_ij]))
return actions
- def _get_maximum_likelihood_tree(self, max_leaves=11):
+ def _get_maximum_likelihood_tree(self, max_leaves=10):
""" Based on Sebastian's code at https://github.com/iesl/hierarchical-trellis/blob/sebastian/src/Jet_Experiments_invM_exactTrellis.ipynb """
if len(self.env.jet["leaves"]) > max_leaves:
diff --git a/ginkgo_rl/agents/mcts.py b/ginkgo_rl/agents/mcts.py
index be49b2e..2798940 100644
--- a/ginkgo_rl/agents/mcts.py
+++ b/ginkgo_rl/agents/mcts.py
@@ -7,6 +7,7 @@
from ginkgo_rl.utils.mcts import MCTSNode
from .base import Agent
from ..utils.nets import MultiHeadedMLP
+from ..utils.various import check_for_nans, NanException
logger = logging.getLogger(__name__)
@@ -18,20 +19,22 @@ def __init__(
n_mc_target=5,
n_mc_min=5,
n_mc_max=100,
- mcts_mode="mean",
+ planning_mode="mean",
+ decision_mode="max_reward",
c_puct=1.0,
- reward_range=(-200., 0.),
+ reward_range=(-200.0, 0.0),
initialize_with_beam_search=True,
beam_size=10,
verbose=False,
- **kwargs
+ **kwargs,
):
super().__init__(*args, **kwargs)
self.n_mc_target = n_mc_target
self.n_mc_min = n_mc_min
self.n_mc_max = n_mc_max
- self.mcts_mode=mcts_mode
+ self.planning_mode = planning_mode
+ self.decision_mode = decision_mode
self.c_puct = c_puct
self.initialize_with_beam_search = initialize_with_beam_search
self.beam_size = beam_size
@@ -44,7 +47,7 @@ def __init__(
self.episode_reward = 0.0
self.episode_likelihood_evaluations = 0
- self._init_episode()
+ self.init_episode()
def set_env(self, env):
""" Sets current environment (and initializes episode) """
@@ -52,32 +55,38 @@ def set_env(self, env):
self.env = env
self.sim_env = copy.deepcopy(self.env)
self.sim_env.reset_at_episode_end = False # Avoids expensive re-sampling of jets every time we parse a path
- self._init_episode()
+ self.init_episode()
- def set_precision(self, n_mc_target, n_mc_min, n_mc_max, mcts_mode, c_puct, beam_size):
+ def set_precision(self, n_mc_target, n_mc_min, n_mc_max, planning_mode, c_puct, beam_size):
""" Sets / changes MCTS precision parameters """
self.n_mc_target = n_mc_target
self.n_mc_min = n_mc_min
self.n_mc_max = n_mc_max
self.n_mc_max = n_mc_max
- self.mcts_mode = mcts_mode
+ self.planning_mode = planning_mode
self.c_puct = c_puct
self.beam_size = beam_size
+ def init_episode(self):
+ """ Initializes MCTS tree and total reward so far """
+
+ self.mcts_head = MCTSNode(None, [], reward_min=self.reward_range[0], reward_max=self.reward_range[1])
+ self.episode_reward = 0.0
+ self.episode_likelihood_evaluations = 0
+
def update(self, state, reward, action, done, next_state, next_reward, num_episode, **kwargs):
""" Updates after environment reaction """
# Keep track of total reward
self.episode_reward += next_reward
- if self.verbose > 0: logger.debug(f"Agent acknowledges receiving a reward of {next_reward}, episode reward so far {self.episode_reward}")
+ if self.verbose > 0:
+ logger.debug(
+ f"Agent acknowledges receiving a reward of {next_reward}, episode reward so far {self.episode_reward}"
+ )
- # MCTS updates
- if done:
- # Reset MCTS when done with an episode
- self._init_episode()
- else:
- # Update MCTS tree when deciding on an action
+ # Update MCTS tree
+ if not done:
self.mcts_head = self.mcts_head.children[action]
self.mcts_head.prune() # This updates the node.path
@@ -151,13 +160,6 @@ def _parse_action(self, action, from_which_env="sim"):
self.episode_likelihood_evaluations += 1
return log_likelihood
- def _init_episode(self):
- """ Initializes MCTS tree and total reward so far """
-
- self.mcts_head = MCTSNode(None, [], reward_min=self.reward_range[0], reward_max=self.reward_range[1])
- self.episode_reward = 0.0
- self.episode_likelihood_evaluations = 0
-
def _mcts(self, state, max_steps=1000):
""" Run Monte-Carl tree search from state for n trajectories"""
@@ -166,7 +168,8 @@ def _mcts(self, state, max_steps=1000):
logger.debug(f"Starting MCTS with {n} trajectories")
for i in range(n):
- if self.verbose > 1: logger.debug(f"Initializing MCTS trajectory {i+1} / {n}")
+ if self.verbose > 1:
+ logger.debug(f"Initializing MCTS trajectory {i+1} / {n}")
node = self.mcts_head
total_reward = 0.0
@@ -175,40 +178,74 @@ def _mcts(self, state, max_steps=1000):
if len(node.path) == 0:
this_state, total_reward, terminal = self._parse_path(state, node.path)
else: # We can speed this up by just doing a single step in self.sim_env
- this_state, last_step_reward, terminal = self._parse_path(this_state, node.path[-1:], from_which_env="sim")
+ this_state, last_step_reward, terminal = self._parse_path(
+ this_state, node.path[-1:], from_which_env="sim"
+ )
total_reward += last_step_reward
node.set_terminal(terminal)
- if self.verbose > 1: logger.debug(f" Node {node.path}")
+ if self.verbose > 1:
+ logger.debug(f" Node {node.path}")
# Termination
if terminal:
- if self.verbose > 1: logger.debug(f" Node is terminal")
+ if self.verbose > 1:
+ logger.debug(f" Node is terminal")
break
# Expand
if not node.children:
actions = self._find_legal_actions(this_state)
- if self.verbose > 1: logger.debug(f" Expanding: {len(actions)} legal actions")
+ if self.verbose > 1:
+ logger.debug(f" Expanding: {len(actions)} legal actions")
step_rewards = [self._parse_action(action, from_which_env="sim") for action in actions]
node.expand(actions, step_rewards=step_rewards)
+ if not node.children:
+ logger.warning(
+ f"Did not find any legal actions even though state was not recognized as terminal. "
+ f"Node path: {node.path}. Children: {node.children}. State: {this_state}. Actions: {actions}."
+ )
+ node.set_terminal(True)
+ break
+
# Select
- policy_probs = self._evaluate_policy(this_state, node.children.keys(), step_rewards=node.children_q_steps())
- action = node.select_puct(policy_probs, mode=self.mcts_mode, c_puct=self.c_puct)
- if self.verbose > 1: logger.debug(f" Selecting action {action}")
+ policy_probs = self._evaluate_policy(
+ this_state, node.children.keys(), step_rewards=node.children_q_steps()
+ )
+ action = node.select_puct(policy_probs, mode=self.planning_mode, c_puct=self.c_puct)
+ if self.verbose > 1:
+ logger.debug(f" Selecting action {action}")
node = node.children[action]
# Backup
- if self.verbose > 1: logger.debug(f" Backing up total reward of {total_reward}")
+ if self.verbose > 1:
+ logger.debug(f" Backing up total reward of {total_reward}")
node.give_reward(self.episode_reward + total_reward, backup=True)
# Select best action
- action = self.mcts_head.select_best(mode="max")
- info = {"log_prob": torch.log(self._evaluate_policy(state, self._find_legal_actions(state), step_rewards=self.mcts_head.children_q_steps(), action=action))}
+ legal_actions = list(self.mcts_head.children.keys())
+ if not legal_actions:
+ legal_actions = self._find_legal_actions(state)
+ step_rewards = self.mcts_head.children_q_steps()
+
+ if self.decision_mode == "max_reward":
+ action = self.mcts_head.select_best(mode="max")
+ elif self.decision_mode == "max_puct":
+ policy_probs = self._evaluate_policy(state, legal_actions, step_rewards=step_rewards)
+ action = self.mcts_head.select_puct(policy_probs=policy_probs, mode="max", c_puct=self.c_puct)
+ elif self.decision_mode == "mean_puct":
+ policy_probs = self._evaluate_policy(state, legal_actions, step_rewards=step_rewards)
+ action = self.mcts_head.select_puct(policy_probs=policy_probs, mode="max", c_puct=self.c_puct)
+ else:
+ raise ValueError(self.decision_mode)
+
+ log_prob = torch.log(self._evaluate_policy(state, legal_actions, step_rewards=step_rewards, action=action))
+ info = {"log_prob": log_prob}
# Debug output
- if self.verbose > 0: self._report_decision(action, state)
+ if self.verbose > 0:
+ self._report_decision(action, state)
return action, info
@@ -216,30 +253,37 @@ def _greedy(self, state):
""" Expands MCTS tree using a greedy algorithm """
node = self.mcts_head
- if self.verbose > 1: logger.debug(f"Starting greedy algorithm.")
+ if self.verbose > 1:
+ logger.debug(f"Starting greedy algorithm.")
while not node.terminal:
# Parse current state
this_state, total_reward, terminal = self._parse_path(state, node.path)
node.set_terminal(terminal)
- if self.verbose > 1: logger.debug(f" Analyzing node {node.path}")
+ if self.verbose > 1:
+ logger.debug(f" Analyzing node {node.path}")
# Expand
if not node.terminal and not node.children:
actions = self._find_legal_actions(this_state)
step_rewards = [self._parse_action(action, from_which_env="sim") for action in actions]
- if self.verbose > 1: logger.debug(f" Expanding: {len(actions)} legal actions")
+ if self.verbose > 1:
+ logger.debug(f" Expanding: {len(actions)} legal actions")
node.expand(actions, step_rewards=step_rewards)
# If terminal, backup reward
if node.terminal:
- if self.verbose > 1: logger.debug(f" Node is terminal")
- if self.verbose > 1: logger.debug(f" Backing up total reward {total_reward}")
+ if self.verbose > 1:
+ logger.debug(f" Node is terminal")
+ if self.verbose > 1:
+ logger.debug(f" Backing up total reward {total_reward}")
node.give_reward(self.episode_reward + total_reward, backup=True)
# Debugging -- this should not happen
if not node.terminal and not node.children:
- logger.warning(f"Unexpected lack of children! Path: {node.path}, children: {node.children.keys()}, legal actions: {self._find_legal_actions(this_state)}, terminal: {node.terminal}")
+ logger.warning(
+ f"Unexpected lack of children! Path: {node.path}, children: {node.children.keys()}, legal actions: {self._find_legal_actions(this_state)}, terminal: {node.terminal}"
+ )
node.set_terminal(True)
# Greedily select next action
@@ -260,31 +304,37 @@ def _beam_search(self, state):
def format_beam():
return [node.path for _, node in beam]
- if self.verbose > 1: logger.debug(f"Starting beam search with beam size {self.beam_size}. Initial beam: {format_beam()}")
+ if self.verbose > 1:
+ logger.debug(f"Starting beam search with beam size {self.beam_size}. Initial beam: {format_beam()}")
while beam or next_beam:
for i, (_, node) in enumerate(beam):
# Parse current state
this_state, total_reward, terminal = self._parse_path(state, node.path)
node.set_terminal(terminal)
- if self.verbose > 1: logger.debug(f" Analyzing node {i+1} / {len(beam)} on beam: {node.path}")
+ if self.verbose > 1:
+ logger.debug(f" Analyzing node {i+1} / {len(beam)} on beam: {node.path}")
# Expand
if not node.terminal and not node.children:
actions = self._find_legal_actions(this_state)
step_rewards = [self._parse_action(action, from_which_env="sim") for action in actions]
- if self.verbose > 1: logger.debug(f" Expanding: {len(actions)} legal actions")
+ if self.verbose > 1:
+ logger.debug(f" Expanding: {len(actions)} legal actions")
node.expand(actions, step_rewards=step_rewards)
# If terminal, backup reward
if node.terminal:
- if self.verbose > 1: logger.debug(f" Node is terminal")
- if self.verbose > 1: logger.debug(f" Backing up total reward {total_reward}")
+ if self.verbose > 1:
+ logger.debug(f" Node is terminal")
+ if self.verbose > 1:
+ logger.debug(f" Backing up total reward {total_reward}")
node.give_reward(self.episode_reward + total_reward, backup=True)
# Did we already process this one? Then skip it
if node.n_beamsearch >= self.beam_size:
- if self.verbose > 1: logger.debug(f" Already beam searched this node sufficiently")
+ if self.verbose > 1:
+ logger.debug(f" Already beam searched this node sufficiently")
continue
# Beam search selection
@@ -297,8 +347,11 @@ def format_beam():
node.in_beam = True
# Just keep top entries for next step
- beam = sorted(next_beam, key=lambda x: x[0], reverse=True)[:self.beam_size]
- if self.verbose > 1: logger.debug(f"Preparing next step, keeping {self.beam_size} / {len(next_beam)} nodes in beam: {format_beam()}")
+ beam = sorted(next_beam, key=lambda x: x[0], reverse=True)[: self.beam_size]
+ if self.verbose > 1:
+ logger.debug(
+ f"Preparing next step, keeping {self.beam_size} / {len(next_beam)} nodes in beam: {format_beam()}"
+ )
next_beam = []
logger.debug(f"Finished beam search")
@@ -313,8 +366,8 @@ def _report_decision(self, chosen_action, state, label="MCTS"):
logger.debug(f"{label} results:")
for i, (action_, node_) in enumerate(self.mcts_head.children.items()):
- is_chosen = '*' if action_ == chosen_action else ' '
- is_greedy = 'g' if action_ == np.argmax(self.mcts_head.children_q_steps()) else ' '
+ is_chosen = "*" if action_ == chosen_action else " "
+ is_greedy = "g" if action_ == np.argmax(self.mcts_head.children_q_steps()) else " "
logger.debug(
f" {is_chosen}{is_greedy} {action_:>2d}: "
f"log likelihood = {node_.q_step:6.2f}, "
@@ -334,21 +387,49 @@ def _train(self, log_prob):
class PolicyMCTSAgent(BaseMCTSAgent):
- def __init__(self, *args, log_likelihood_feature=True, hidden_sizes=(100,100,), activation=nn.ReLU(), action_factor=0.01, log_likelihood_factor=0.1, **kwargs):
+ def __init__(
+ self,
+ *args,
+ log_likelihood_feature=True,
+ hidden_sizes=(100, 100,),
+ activation=nn.ReLU(),
+ action_factor=0.01,
+ log_likelihood_factor=0.1,
+ **kwargs,
+ ):
super().__init__(*args, **kwargs)
self.log_likelihood_feature = log_likelihood_feature
- self.actor = MultiHeadedMLP(1 + 8 + int(self.log_likelihood_feature) + self.state_length, hidden_sizes=hidden_sizes, head_sizes=(1,), activation=activation, head_activations=(None,))
+ self.actor = MultiHeadedMLP(
+ 1 + 8 + int(self.log_likelihood_feature) + self.state_length,
+ hidden_sizes=hidden_sizes,
+ head_sizes=(1,),
+ activation=activation,
+ head_activations=(None,),
+ )
self.softmax = nn.Softmax(dim=0)
self.action_factor = action_factor
self.log_likelihood_factor = log_likelihood_factor
def _evaluate_policy(self, state, legal_actions, step_rewards=None, action=None):
- batch_states = self._batch_state(state, legal_actions, step_rewards=step_rewards)
- (probs,) = self.actor(batch_states)
- probs = self.softmax(probs).flatten()
+ try:
+ policy_input = self._prepare_policy_input(state, legal_actions, step_rewards=step_rewards)
+ check_for_nans("Policy input", policy_input)
+ (probs,) = self.actor(policy_input)
+ check_for_nans("Policy probabilities", probs)
+ probs = self.softmax(probs).flatten()
+ except NanException:
+ logger.error("NaNs appeared when evaluating the policy.")
+ logger.error(f" state: {state}")
+ logger.error(f" legal actions: {legal_actions}")
+ logger.error(f" step rewards: {step_rewards}")
+ logger.error(f" action: {action}")
+ logger.error(f" policy weights: {list(self.parameters())}")
+ logger.error(f" mean weight: {self.get_mean_weight()}")
+
+ raise
if action is not None:
assert action in legal_actions
@@ -356,39 +437,53 @@ def _evaluate_policy(self, state, legal_actions, step_rewards=None, action=None)
return probs
- def _batch_state(self, state, legal_actions, step_rewards=None):
+ def _prepare_policy_input(self, state, legal_actions, step_rewards=None):
+ """ Prepares the input to the policy """
+ check_for_nans("Raw state", state)
state_ = state.view(-1)
- if step_rewards is None:
+ if step_rewards is None or not step_rewards:
step_rewards = [None for _ in legal_actions]
batch_states = []
+ assert legal_actions
+ assert step_rewards
+ assert len(legal_actions) == len(step_rewards)
+
for action, log_likelihood in zip(legal_actions, step_rewards):
action_ = self.action_factor * torch.tensor([action]).to(self.device, self.dtype)
i, j = self.env.unwrap_action(action)
pi = state[i, :]
pj = state[j, :]
+ check_for_nans("Individual momenta", pi, pj)
if self.log_likelihood_feature:
if log_likelihood is None:
log_likelihood = self._parse_action(action, from_which_env="real")
if not np.isfinite(log_likelihood):
- log_likelihood = 0.
+ log_likelihood = 0.0
log_likelihood = np.clip(log_likelihood, self.reward_range[0], self.reward_range[1])
- log_likelihood_ = self.log_likelihood_factor * torch.tensor([log_likelihood]).to(self.device, self.dtype)
+ log_likelihood_ = self.log_likelihood_factor * torch.tensor([log_likelihood]).to(
+ self.device, self.dtype
+ )
+ check_for_nans("Log likelihood as policy input", log_likelihood_)
combined_state = torch.cat((action_, pi, pj, log_likelihood_, state_), dim=0)
+ check_for_nans("Individual policy input entry", combined_state)
else:
combined_state = torch.cat((action_, pi, pj, state_), dim=0)
+ check_for_nans("Individual policy input entry", combined_state)
batch_states.append(combined_state.unsqueeze(0))
batch_states = torch.cat(batch_states, dim=0)
+ check_for_nans("Concatenated policy input", batch_states)
return batch_states
def _train(self, log_prob):
- loss = - log_prob
+ loss = -log_prob
+ check_for_nans("Loss", loss)
self._gradient_step(loss)
return loss.item()
@@ -401,9 +496,9 @@ class RandomMCTSAgent(BaseMCTSAgent):
def _evaluate_policy(self, state, legal_actions, step_rewards=None, action=None):
""" Evaluates the policy on the state and returns the probabilities for a given action or all legal actions """
if action is not None:
- return torch.tensor(1. / len(legal_actions), dtype=self.dtype)
+ return torch.tensor(1.0 / len(legal_actions), dtype=self.dtype)
else:
- return 1. / len(legal_actions) * torch.ones(len(legal_actions), dtype=self.dtype)
+ return 1.0 / len(legal_actions) * torch.ones(len(legal_actions), dtype=self.dtype)
def _train(self, log_prob):
return torch.tensor(0.0)
diff --git a/ginkgo_rl/agents/random.py b/ginkgo_rl/agents/random.py
index b446d85..3502875 100644
--- a/ginkgo_rl/agents/random.py
+++ b/ginkgo_rl/agents/random.py
@@ -11,7 +11,7 @@ class RandomAgent(Agent):
def _predict(self, state):
actions = self._find_legal_actions(state)
action = random.choice(actions)
- return action, {}
+ return action, {"likelihood_evaluations": 0}
def update(self, state, reward, action, done, next_state, next_reward, num_episode, **kwargs):
pass
diff --git a/ginkgo_rl/envs/__init__.py b/ginkgo_rl/envs/__init__.py
index 1853a95..cc5de68 100644
--- a/ginkgo_rl/envs/__init__.py
+++ b/ginkgo_rl/envs/__init__.py
@@ -1,15 +1,16 @@
from gym.envs.registration import register
-from .ginkgo_likelihood import GinkgoLikelihoodEnv, GinkgoLikelihood1DEnv # , GinkgoLikelihoodShuffledEnv, GinkgoLikelihoodShuffled1DEnv
+from .ginkgo_likelihood import (
+ GinkgoLikelihoodEnv,
+ GinkgoLikelihood1DEnv,
+) # , GinkgoLikelihoodShuffledEnv, GinkgoLikelihoodShuffled1DEnv
register(
- id='GinkgoLikelihood-v0',
- entry_point='ginkgo_rl.envs.ginkgo_likelihood:GinkgoLikelihoodEnv',
- max_episode_steps=100
+ id="GinkgoLikelihood-v0", entry_point="ginkgo_rl.envs.ginkgo_likelihood:GinkgoLikelihoodEnv", max_episode_steps=100
)
register(
- id='GinkgoLikelihood1D-v0',
- entry_point='ginkgo_rl.envs.ginkgo_likelihood:GinkgoLikelihood1DEnv',
- max_episode_steps=100
+ id="GinkgoLikelihood1D-v0",
+ entry_point="ginkgo_rl.envs.ginkgo_likelihood:GinkgoLikelihood1DEnv",
+ max_episode_steps=100,
)
# register(
# id='GinkgoLikelihoodShuffled-v0',
diff --git a/ginkgo_rl/envs/ginkgo_likelihood.py b/ginkgo_rl/envs/ginkgo_likelihood.py
index c99019f..02f6130 100644
--- a/ginkgo_rl/envs/ginkgo_likelihood.py
+++ b/ginkgo_rl/envs/ginkgo_likelihood.py
@@ -1,11 +1,10 @@
import numpy as np
from gym import Env
-from gym.spaces import Discrete, Box, Tuple, MultiDiscrete
+from gym.spaces import Discrete, Box, MultiDiscrete
import logging
from showerSim.invMass_ginkgo import Simulator as GinkgoSim
from showerSim.likelihood_invM import split_logLH as ginkgo_log_likelihood
import torch
-import itertools
import copy
logger = logging.getLogger(__name__)
@@ -101,8 +100,12 @@ def __init__(
# self._simulate()
# Spaces
- self.action_space = MultiDiscrete((self.n_max, self.n_max)) # Tuple((Discrete(self.n_max), Discrete(self.n_max)))
- self.observation_space = Box(low=self.padding_value, high=state_rescaling * max(self.jet_momentum), shape=(self.n_max, 4), dtype=np.float)
+ self.action_space = MultiDiscrete(
+ (self.n_max, self.n_max)
+ ) # Tuple((Discrete(self.n_max), Discrete(self.n_max)))
+ self.observation_space = Box(
+ low=self.padding_value, high=state_rescaling * max(self.jet_momentum), shape=(self.n_max, 4), dtype=np.float
+ )
def reset(self):
""" Resets the state of the environment and returns an initial observation. """
@@ -153,7 +156,9 @@ def step(self, action):
if self.illegal_action_counter > self.illegal_actions_patience:
new_action = self._draw_random_legal_action()
if self.verbose:
- logger.debug(f"This is the {self.illegal_action_counter}th illegal action in a row. That's enough. Executing random action {new_action} instead.")
+ logger.debug(
+ f"This is the {self.illegal_action_counter}th illegal action in a row. That's enough. Executing random action {new_action} instead."
+ )
reward += self._compute_log_likelihood(new_action)
self._merge(new_action)
@@ -168,7 +173,9 @@ def step(self, action):
self.illegal_action_counter = 0
else:
if self.verbose:
- logger.debug(f"This is the {self.illegal_action_counter}th illegal action in a row. Try again. (Environment state is unchanged.)")
+ logger.debug(
+ f"This is the {self.illegal_action_counter}th illegal action in a row. Try again. (Environment state is unchanged.)"
+ )
done = False
info = {
@@ -208,24 +215,42 @@ def _init_sim(self):
maxNTry=self.max_n_try,
)
- def _simulate(self):
+ def _simulate(self, max_tries=10):
""" Initiates an episode by simulating a new jet """
- rate = torch.tensor([self.w_rate, self.qcd_rate]) if self.w_jet else torch.tensor([self.qcd_rate, self.qcd_rate])
- jets = self.sim(rate)
- if not jets:
- raise RuntimeError(f"Could not generate any jets: {jets}")
+ rate = (
+ torch.tensor([self.w_rate, self.qcd_rate]) if self.w_jet else torch.tensor([self.qcd_rate, self.qcd_rate])
+ )
- self.jet = self.sim(rate)[0]
- self.n = len(self.jet["leaves"])
- self.state = self.padding_value * np.ones((self.n_max, 4))
- self.state[: self.n] = self.state_rescaling * self.jet["leaves"]
- self.is_leaf = [(i < self.n) for i in range(self.n_max)]
- self.illegal_action_counter = 0
- self._sort_state()
+ success = False
+ tries = 0
+ while not success:
+ tries += 1
+ if tries > max_tries:
+ raise RuntimeError(f"Failed to generate a consistent jet {max_tries} times")
+
+ jets = self.sim(rate)
+
+ if not jets:
+ logger.warning("Could not generate any jets, try {tries} / {max_tries}")
+ continue
+
+ self.jet = self.sim(rate)[0]
+ self.n = len(self.jet["leaves"])
+ self.state = self.padding_value * np.ones((self.n_max, 4))
+ self.state[: self.n] = self.state_rescaling * self.jet["leaves"]
+ self.is_leaf = [(i < self.n) for i in range(self.n_max)]
+ self.illegal_action_counter = 0
+ self._sort_state()
+
+ if not np.all(np.isfinite(self.state)):
+ logger.warning(f"NaNs in newly simulated state, try {tries} / {max_tries}:\n{self.state}")
+ continue
+
+ success = True
if self.verbose:
- logger.debug(f"Sampling new jet with {self.n} leaves")
+ logger.debug(f"Sampled new jet with {self.n} leaves")
def _check_acceptability(self, action):
i, j = action
@@ -237,7 +262,7 @@ def check_legality(self, action):
return self._check_acceptability(action) and i != j and i < self.n and j < self.n
def _sort_state(self):
- idx = sorted(list(range(self.n_max)), reverse=True, key=lambda i : self.state[i, 0])
+ idx = sorted(list(range(self.n_max)), reverse=True, key=lambda i: self.state[i, 0])
self.state = self.state[idx, :]
self.is_leaf = np.asarray(self.is_leaf, dtype=np.bool)[idx]
@@ -253,7 +278,9 @@ def _compute_log_likelihood(self, action):
if self.n == 2 and self.w_jet:
lam = self.jet["LambdaRoot"] # W jets have a different lambda for the first split
- log_likelihood = ginkgo_log_likelihood(self.state[i] / self.state_rescaling, ti, self.state[j] / self.state_rescaling, tj, t_cut=t_cut, lam=lam)
+ log_likelihood = ginkgo_log_likelihood(
+ self.state[i] / self.state_rescaling, ti, self.state[j] / self.state_rescaling, tj, t_cut=t_cut, lam=lam
+ )
try:
log_likelihood = log_likelihood.item()
except:
@@ -263,7 +290,9 @@ def _compute_log_likelihood(self, action):
log_likelihood = np.clip(log_likelihood, self.min_reward, None)
if self.verbose:
- logger.debug(f"Computing log likelihood of action {action}: ti = {ti}, tj = {tj}, t_cut = {t_cut}, lam = {lam} -> log likelihood = {log_likelihood}")
+ logger.debug(
+ f"Computing log likelihood of action {action}: ti = {ti}, tj = {tj}, t_cut = {t_cut}, lam = {lam} -> log likelihood = {log_likelihood}"
+ )
# logger.debug(f"Computing log likelihood of action {action}: log likelihood = {log_likelihood}")
return log_likelihood
@@ -279,11 +308,14 @@ def _merge(self, action):
for k in range(j, self.n_max - 1):
self.state[k, :] = self.state[k + 1, :]
- self.is_leaf[k] = self.is_leaf[k+1]
+ self.is_leaf[k] = self.is_leaf[k + 1]
self.state[-1, :] = self.padding_value * np.ones(4)
self.is_leaf[-1] = False
+ if not np.all(np.isfinite(self.jet["leaves"])):
+ logger.error(f"NaNs in simulator state after merging!\n{self.state}")
+
self.n -= 1
self._sort_state()
@@ -325,7 +357,7 @@ def __init__(self, *args, **kwargs):
def wrap_action(self, action_tuple):
assert self._check_acceptability(action_tuple)
i, j = max(action_tuple), min(action_tuple)
- return i * (i - 1) // 2 + j
+ return i * (i - 1) // 2 + j
def unwrap_action(self, action_int):
i = 1
diff --git a/ginkgo_rl/eval/__init__.py b/ginkgo_rl/eval/__init__.py
index 6ad794b..35f0e66 100644
--- a/ginkgo_rl/eval/__init__.py
+++ b/ginkgo_rl/eval/__init__.py
@@ -1 +1,2 @@
from .evaluator import GinkgoEvaluator
+from .cluster_interface import GinkgoRLInterface
diff --git a/ginkgo_rl/eval/cluster_interface.py b/ginkgo_rl/eval/cluster_interface.py
new file mode 100644
index 0000000..cf30c8e
--- /dev/null
+++ b/ginkgo_rl/eval/cluster_interface.py
@@ -0,0 +1,302 @@
+from tqdm import tqdm
+import pickle
+import logging
+import torch
+from copy import deepcopy
+import numpy as np
+
+from ginkgo_rl.envs import GinkgoLikelihood1DEnv
+from ginkgo_rl.agents import PolicyMCTSAgent
+
+logger = logging.getLogger(__name__)
+
+
+class GinkgoRLInterface:
+ def __init__(self, state_dict_filename, **kwargs):
+ self.env = self._make_env(**kwargs)
+ self.agent = self._make_agent(state_dict_filename, **kwargs)
+
+ def generate(self, n):
+ """ Generates a number of jets and returns the jet dictionary """
+
+ logger.info(f"Generating {n} jets")
+ jets = []
+
+ for _ in range(n):
+ self.env.reset()
+ jets.append(self.env.get_internal_state()[0])
+
+ logger.info(f"Done")
+
+ return jets
+
+ def cluster(self, jets, filename=None, mode=None):
+ """ Clusters all jets in a jet dictionary with the MCTS agent. """
+
+ jets = self._load_jets(jets)
+
+ logger.info(f"Clustering {len(jets)} jets")
+
+ reclustered_jets = []
+ log_likelihoods = []
+ illegal_actions = []
+ likelihood_evaluations = []
+
+ for jet in tqdm(jets):
+ with torch.no_grad():
+ reclustered_jet, log_likelihood, error, likelihood_evaluation = self._episode(jet, mode=mode)
+
+ reclustered_jets.append(reclustered_jet)
+ log_likelihoods.append(log_likelihood)
+ illegal_actions.append(error)
+ likelihood_evaluations.append(likelihood_evaluation)
+
+ if filename is not None:
+ self._save_jets(reclustered_jets, filename)
+
+ logger.info("Done")
+
+ return reclustered_jets, log_likelihoods, illegal_actions, likelihood_evaluations
+
+ def _episode(self, jet, mode=None):
+ """ Clusters a single jet """
+
+ # Initialize
+ self.agent.eval()
+ self.env.set_internal_state(self._jet_to_internal_state(jet))
+
+ state = self.env.get_state()
+ done = False
+ log_likelihood = 0.0
+ errors = 0
+ reward = 0.0
+ likelihood_evaluations = 0
+
+ reclustered_jet = self._init_reclustered_jet(jet)
+
+ # Point agent to correct env and initialize episode: this only works for *our* models, not the baselines
+ try:
+ self.agent.set_env(self.env)
+ self.agent.init_episode()
+ except:
+ pass
+
+ while not done:
+ # Agent step
+ if self.agent is None:
+ action = self.env.action_space.sample()
+ agent_info = {}
+ elif mode is None:
+ action, agent_info = self.agent.predict(state)
+ likelihood_evaluations = max(agent_info["likelihood_evaluations"], likelihood_evaluations)
+ else:
+ action, agent_info = self.agent.predict(state, mode=mode)
+ likelihood_evaluations = max(agent_info["likelihood_evaluations"], likelihood_evaluations)
+
+ # Environment step
+ next_state, next_reward, done, info = self.env.step(action)
+
+ # Keep track of clustered tree
+ if info["legal"]:
+ self._update_reclustered_jet_with_action(reclustered_jet, action, next_reward)
+
+ # Keep track of metrics
+ log_likelihood += next_reward
+ if not info["legal"]:
+ errors += 1
+
+ # Update model: this only works for *our* models, not the baselines
+ try:
+ self.agent.update(
+ state, reward, action, done, next_state, next_reward=next_reward, num_episode=0, **agent_info
+ )
+ except:
+ pass
+
+ reward, state = next_reward, next_state
+
+ self._finalize_reclustered_jet(reclustered_jet)
+
+ return reclustered_jet, float(log_likelihood), int(errors), int(likelihood_evaluations)
+
+ def _init_reclustered_jet(self, jet, delete_keys=("deltas", "draws", "dij", "ConstPhi", "PhiDelta", "PhiDeltaRel")):
+ reclustered_jet = deepcopy(jet)
+
+ reclustered_jet["content"] = list(deepcopy(reclustered_jet["leaves"]))
+ reclustered_jet["tree"] = [[-1, -1] for _ in reclustered_jet["content"]]
+ reclustered_jet["logLH"] = [0.0 for _ in reclustered_jet["content"]]
+ reclustered_jet["root_id"] = None
+ reclustered_jet["algorithm"] = "mcts"
+ reclustered_jet["current_particles"] = set(
+ range(len(reclustered_jet["content"]))
+ ) # dict IDs of current particles
+
+ for key in delete_keys:
+ try:
+ del reclustered_jet[key]
+ except:
+ logger.info(f"Jet dict did not contain field {key}")
+
+ return reclustered_jet
+
+ def _update_reclustered_jet_with_action(self, reclustered_jet, action, step_log_likelihood):
+ # Parse action
+ i_en, j_en = self.env.unwrap_action(action) # energy-sorted IDs of the particles to be merged
+ particles = [(dict_id, reclustered_jet["content"][dict_id]) for dict_id in reclustered_jet["current_particles"]]
+ particles = sorted(
+ particles, reverse=True, key=lambda x: x[1][0]
+ ) # (dict_ID, four_momentum) of current particles, sorted by E
+ i_dict, j_dict = particles[i_en][0], particles[j_en][0] # dict IDs of the particles to be merged
+
+ logger.debug(f"Parsing action {action}:")
+ logger.debug(" E-ranking | dict ID | momentum ")
+ for en_id, (dict_id, four_momentum) in enumerate(particles):
+ logger.debug(
+ f" {'x' if dict_id in (i_dict, j_dict) else ' '} {en_id:>7d} | {dict_id:>7d} | {four_momentum} "
+ )
+
+ # Perform action
+ new_momentum = reclustered_jet["content"][i_dict] + reclustered_jet["content"][j_dict]
+ reclustered_jet["content"].append(new_momentum)
+ reclustered_jet["tree"].append([i_dict, j_dict])
+ k_dict = len(reclustered_jet["content"]) - 1
+
+ reclustered_jet["root_id"] = k_dict
+ reclustered_jet["logLH"].append(step_log_likelihood)
+
+ reclustered_jet["current_particles"].remove(i_dict)
+ reclustered_jet["current_particles"].remove(j_dict)
+ reclustered_jet["current_particles"].add(k_dict)
+
+ def _finalize_reclustered_jet(self, reclustered_jet):
+ reclustered_jet["content"] = np.asarray(reclustered_jet["content"])
+ reclustered_jet["logLH"] = np.asarray(reclustered_jet["logLH"])
+ reclustered_jet["tree"] = np.asarray(reclustered_jet["tree"], dtype=np.int)
+
+ del reclustered_jet["current_particles"]
+
+ def _make_agent(
+ self,
+ state_dict,
+ initialize_mcts_with_beamsearch=True,
+ log_likelihood_policy_input=True,
+ decision_mode="max_reward",
+ reward_range=(-500.0, 0.0),
+ hidden_sizes=(100, 100),
+ activation=torch.nn.ReLU(),
+ n_mc_target=2,
+ n_mc_min=0,
+ n_mc_max=50,
+ beamsize=20,
+ planning_mode="mean",
+ c_puct=1.0,
+ device=torch.device("cpu"),
+ dtype=torch.float,
+ **kwargs,
+ ):
+ agent = PolicyMCTSAgent(
+ self.env,
+ reward_range=reward_range,
+ n_mc_target=n_mc_target,
+ n_mc_min=n_mc_min,
+ n_mc_max=n_mc_max,
+ planning_mode=planning_mode,
+ initialize_with_beam_search=initialize_mcts_with_beamsearch,
+ log_likelihood_feature=log_likelihood_policy_input,
+ c_puct=c_puct,
+ device=device,
+ dtype=dtype,
+ verbose=0,
+ decision_mode=decision_mode,
+ beam_size=beamsize,
+ hidden_sizes=hidden_sizes,
+ activation=activation,
+ )
+
+ try:
+ state_dict = torch.load(state_dict)
+ except:
+ pass
+ agent.load_state_dict(state_dict)
+
+ return agent
+
+ def _make_env(
+ self,
+ illegal_reward=-100.0,
+ illegal_actions_patience=3,
+ n_max=20,
+ n_min=2,
+ n_target=1,
+ min_reward=-100.0,
+ state_rescaling=0.01,
+ padding_value=-1.0,
+ w_jet=True,
+ w_rate=3.0,
+ qcd_rate=1.5,
+ pt_min=4.0 ** 2,
+ qcd_mass=30.0,
+ w_mass=80.0,
+ jet_momentum=400.0,
+ jetdir=(1, 1, 1),
+ max_n_try=1000,
+ **kwargs,
+ ):
+ env = GinkgoLikelihood1DEnv(
+ illegal_reward,
+ illegal_actions_patience,
+ n_max,
+ n_min,
+ n_target,
+ min_reward,
+ state_rescaling,
+ padding_value,
+ w_jet,
+ max_n_try,
+ w_rate,
+ qcd_rate,
+ pt_min,
+ qcd_mass,
+ w_mass,
+ jet_momentum,
+ jetdir,
+ )
+
+ return env
+
+ def _load_jets(self, jets):
+ try:
+ with open(jets, "rb") as f:
+ return pickle.load(f)
+ except:
+ return jets
+
+ def _save_jets(self, jets, filename):
+ logger.info(f"Saving clustered jets at {filename}")
+
+ with open(filename, "wb") as f:
+ pickle.dump(jets, f)
+
+ def _internal_state_to_jet(self, internal_state):
+ return internal_state[0]
+
+ def _jet_to_internal_state(self, jet):
+ """
+ Translates a jet dict to the environment internal state, a 5-tuple of the form (jet_dict, n_particles, state, is_leaf, illegal_action_counter).
+
+ Only works for "initial" states (no clustering so far, only observed particles).
+ """
+
+ n = len(jet["leaves"])
+ state = self.env.padding_value * np.ones((self.env.n_max, 4))
+ state[:n] = self.env.state_rescaling * jet["leaves"]
+ is_leaf = [(i < n) for i in range(self.env.n_max)]
+ illegal_action_counter = 0
+
+ # energy sorting
+ idx = sorted(list(range(self.env.n_max)), reverse=True, key=lambda i: state[i, 0])
+ state = state[idx, :]
+ is_leaf = np.asarray(is_leaf, dtype=np.bool)[idx]
+
+ internal_state = (jet, n, state, is_leaf, illegal_action_counter)
+ return internal_state
diff --git a/ginkgo_rl/eval/evaluator.py b/ginkgo_rl/eval/evaluator.py
index 4824810..b44fa5e 100644
--- a/ginkgo_rl/eval/evaluator.py
+++ b/ginkgo_rl/eval/evaluator.py
@@ -10,17 +10,28 @@
from ginkgo_rl import GinkgoLikelihoodEnv, GinkgoLikelihood1DEnv
-# Workaround for now until Trellis is better packaged
-sys.path.append("/Users/johannbrehmer/work/projects/shower_rl/hierarchical-trellis/src")
-from run_physics_experiment_invM import compare_map_gt_and_bs_trees as compute_trellis
-
-sys.path.insert(0, "/Users/johannbrehmer/work/projects/shower_rl/ReclusterTreeAlgorithms/scripts")
-import beamSearchOptimal_invM as beam_search
-
logger = logging.getLogger(__name__)
-
-class GinkgoEvaluator():
+# Workaround for now until Trellis is better packaged
+try:
+ sys.path.append("/Users/johannbrehmer/work/projects/shower_rl/hierarchical-trellis/src")
+ sys.path.append("/scratch/jb6504/hierarchical-trellis/src")
+ from run_physics_experiment_invM import compare_map_gt_and_bs_trees as compute_trellis
+except Exception:
+ logger.warning("Error importing hierarchical trellis code.")
+ compute_trellis = None
+
+try:
+ sys.path.insert(0, "/Users/johannbrehmer/work/projects/shower_rl/ReclusterTreeAlgorithms/scripts")
+ sys.path.insert(0, "/scratch/jb6504/ReclusterTreeAlgorithms/scripts")
+ sys.path.insert(0, "/scratch/jb6504/ReclusterTreeAlgorithms")
+ import beamSearchOptimal_invM as beam_search
+except Exception:
+ logger.warning("Error importing beam search code.")
+ beam_search = None
+
+
+class GinkgoEvaluator:
def __init__(self, filename, env, redraw_existing_jets=False, n_jets=100):
self.filename = filename
self.env = env
@@ -53,7 +64,9 @@ def eval_exact_trellis(self, method):
def eval_beam_search(self, method, beam_size):
log_likelihoods = [[self._compute_beam_search_log_likelihood(jet, beam_size)] for jet in self.jets]
illegal_actions = [[0] for _ in self.jets]
- likelihood_evaluations = [[self._compute_beam_search_likelihood_evaluations(jet, beam_size)] for jet in self.jets]
+ likelihood_evaluations = [
+ [self._compute_beam_search_likelihood_evaluations(jet, beam_size)] for jet in self.jets
+ ]
self._update_results(method, log_likelihoods, illegal_actions)
return log_likelihoods, illegal_actions, likelihood_evaluations
@@ -101,22 +114,36 @@ def __str__(self):
lines = []
lines.append("")
- lines.append("-"*(lengths[0] + lengths[1] + lengths[2] + (3-1)*3))
+ lines.append("-" * (lengths[0] + lengths[1] + lengths[2] + (3 - 1) * 3))
lines.append(f"{'Method':>{lengths[0]}s} | {'Log p':>{lengths[1]}s} | {'Err':>{lengths[2]}s}")
- lines.append("-"*(lengths[0] + lengths[1] + lengths[2] + (3-1)*3))
+ lines.append("-" * (lengths[0] + lengths[1] + lengths[2] + (3 - 1) * 3))
- for method, mean_log_likelihood, mean_illegals in sorted(results, key=lambda x : x[1], reverse=True):
- lines.append(f"{method:>{lengths[0]}s} | {mean_log_likelihood:>{lengths[1]}.{lengths[1] - 4}f} | {mean_illegals:>{lengths[2]}.{lengths[2] - 2}f}")
+ for method, mean_log_likelihood, mean_illegals in sorted(results, key=lambda x: x[1], reverse=True):
+ lines.append(
+ f"{method:>{lengths[0]}s} | {mean_log_likelihood:>{lengths[1]}.{lengths[1] - 4}f} | {mean_illegals:>{lengths[2]}.{lengths[2] - 2}f}"
+ )
- lines.append("-"*(lengths[0] + lengths[1] + lengths[2] + (3-1)*3))
+ lines.append("-" * (lengths[0] + lengths[1] + lengths[2] + (3 - 1) * 3))
lines.append("")
return "\n".join(lines)
- def plot_log_likelihoods(self, cols=2, rows=4, ymax=0.5, deltax_min=1., deltax_max=10., xbins=25, panelsize=4., filename=None, linestyles=["-", "--", ":", "-."], colors=[f"C{i}" for i in range(9)]):
+ def plot_log_likelihoods(
+ self,
+ cols=2,
+ rows=4,
+ ymax=0.5,
+ deltax_min=1.0,
+ deltax_max=10.0,
+ xbins=25,
+ panelsize=4.0,
+ filename=None,
+ linestyles=["-", "--", ":", "-."],
+ colors=[f"C{i}" for i in range(9)],
+ ):
colors = colors * 10
linestyles = linestyles * 10
- fig = plt.figure(figsize=(rows*panelsize, cols*panelsize))
+ fig = plt.figure(figsize=(rows * panelsize, cols * panelsize))
for j in range(self.n_jets):
if j > cols * rows:
@@ -132,14 +159,34 @@ def plot_log_likelihoods(self, cols=2, rows=4, ymax=0.5, deltax_min=1., deltax_m
ls_counter = 0
for i, (name, logp, _) in enumerate(self.get_results()):
- logp_ = np.clip(logp, xmin + 1.e-9, xmax - 1.e-9)
+ logp_ = np.clip(logp, xmin + 1.0e-9, xmax - 1.0e-9)
if len(logp[j]) == 1:
- plt.plot([logp_[j][0], logp_[j][0]], [0., ymax], color=colors[i], ls=linestyles[ls_counter], label=name)
+ plt.plot(
+ [logp_[j][0], logp_[j][0]], [0.0, ymax], color=colors[i], ls=linestyles[ls_counter], label=name
+ )
ls_counter += 1
else:
- plt.hist(logp_[j], histtype="stepfilled", range=(xmin, xmax), color=colors[i], bins=xbins, lw=1.5, density=True, alpha=0.15)
- plt.hist(logp_[j], histtype="step", range=(xmin, xmax), bins=xbins, color=colors[i], lw=1.5, density=True, label=name)
+ plt.hist(
+ logp_[j],
+ histtype="stepfilled",
+ range=(xmin, xmax),
+ color=colors[i],
+ bins=xbins,
+ lw=1.5,
+ density=True,
+ alpha=0.15,
+ )
+ plt.hist(
+ logp_[j],
+ histtype="step",
+ range=(xmin, xmax),
+ bins=xbins,
+ color=colors[i],
+ lw=1.5,
+ density=True,
+ label=name,
+ )
if j == 0:
plt.legend()
@@ -148,7 +195,7 @@ def plot_log_likelihoods(self, cols=2, rows=4, ymax=0.5, deltax_min=1., deltax_m
plt.ylabel("Histogram")
plt.xlim(xmin, xmax)
- plt.ylim(0., ymax)
+ plt.ylim(0.0, ymax)
plt.tight_layout()
if filename is not None:
@@ -161,11 +208,11 @@ def _update_results(self, method, log_likelihoods, illegal_actions):
def _save(self):
data = {"n_jets": self.n_jets, "jets": self.jets}
- with open(self.filename, 'wb') as file:
+ with open(self.filename, "wb") as file:
pickle.dump(data, file)
def _load(self):
- with open(self.filename, 'rb') as file:
+ with open(self.filename, "rb") as file:
data = pickle.load(file)
self.n_jets = data["n_jets"]
@@ -181,21 +228,24 @@ def _init_jets(self):
jets.append(self.env.get_internal_state())
sizes = np.array([len(jet[0]["leaves"]) for jet in jets])
- logger.info(f" Generated jets with min size {np.min(sizes)}, mean size {np.mean(sizes)}, max size {np.max(sizes)}")
+ logger.info(
+ f" Generated jets with min size {np.min(sizes)}, mean size {np.mean(sizes)}, max size {np.max(sizes)}"
+ )
return jets
def _episode(self, model, mode=None):
state = self.env.get_state()
done = False
- log_likelihood = 0.
+ log_likelihood = 0.0
errors = 0
reward = 0.0
likelihood_evaluations = 0
- # Point model to correct env: this only works for *our* models, not the baselines
+ # Point agent to correct env and initialize episode: this only works for *our* models, not the baselines
try:
model.set_env(self.env)
+ model.init_episode()
except:
pass
@@ -218,7 +268,9 @@ def _episode(self, model, mode=None):
# Update model: this only works for *our* models, not the baselines
try:
- model.update(state, reward, action, done, next_state, next_reward=next_reward, num_episode=0, **agent_info)
+ model.update(
+ state, reward, action, done, next_state, next_reward=next_reward, num_episode=0, **agent_info
+ )
except:
pass
diff --git a/ginkgo_rl/utils/mcts.py b/ginkgo_rl/utils/mcts.py
index f15d2c1..8f455b5 100644
--- a/ginkgo_rl/utils/mcts.py
+++ b/ginkgo_rl/utils/mcts.py
@@ -15,7 +15,11 @@ def __init__(self, parent, path, reward_normalizer=None, reward_min=None, reward
self.reward_min = reward_min
self.reward_max = reward_max
- self.reward_normalizer = AffineNormalizer(hard_min=reward_min, hard_max=reward_max) if reward_normalizer is None else reward_normalizer
+ self.reward_normalizer = (
+ AffineNormalizer(hard_min=reward_min, hard_max=reward_max)
+ if reward_normalizer is None
+ else reward_normalizer
+ )
self.terminal = None # None means undetermined
self.n = 0 # Total visit count
@@ -35,7 +39,14 @@ def expand(self, actions, step_rewards=None):
if action in self.children:
continue
- self.children[action] = MCTSNode(self, self.path + [action], self.reward_normalizer, reward_min=self.reward_min, reward_max=self.reward_max, q_step=reward)
+ self.children[action] = MCTSNode(
+ self,
+ self.path + [action],
+ self.reward_normalizer,
+ reward_min=self.reward_min,
+ reward_max=self.reward_max,
+ q_step=reward,
+ )
def set_terminal(self, terminal):
self.terminal = terminal
@@ -84,7 +95,7 @@ def select_puct(self, policy_probs=None, mode="mean", c_puct=1.0):
assert len(pucts) > 0
# Pick highest
- best_puct = - float("inf")
+ best_puct = -float("inf")
choice = None
for i, puct in enumerate(pucts):
if puct > best_puct or choice is None:
@@ -95,7 +106,7 @@ def select_puct(self, policy_probs=None, mode="mean", c_puct=1.0):
return list(self.children.keys())[choice]
def select_best(self, mode="max"):
- best_q = - float("inf")
+ best_q = -float("inf")
choice = None
for action, child in self.children.items():
@@ -108,7 +119,7 @@ def select_best(self, mode="max"):
return choice
def select_beam_search(self, beam_size):
- choices = sorted(list(self.children.keys()), key=lambda x : self.children[x].q_step, reverse=True)[:beam_size]
+ choices = sorted(list(self.children.keys()), key=lambda x: self.children[x].q_step, reverse=True)[:beam_size]
return choices
def select_greedy(self):
@@ -126,13 +137,15 @@ def prune(self):
def _compute_pucts(self, policy_probs=None, mode="mean", c_puct=1.0):
if policy_probs is None: # By default assume a uniform policy
- policy_probs = 1. / len(self)
+ policy_probs = 1.0 / len(self)
assert len(policy_probs) == len(self) > 0
n_children = torch.tensor([child.n for child in self.children.values()], dtype=policy_probs.dtype)
- q_children = torch.tensor([child.get_reward(mode=mode) for child in self.children.values()], dtype=policy_probs.dtype)
- pucts = q_children + c_puct * policy_probs * (self.n + 1.e-9) ** 0.5 / (1. + n_children)
+ q_children = torch.tensor(
+ [child.get_reward(mode=mode) for child in self.children.values()], dtype=policy_probs.dtype
+ )
+ pucts = q_children + c_puct * policy_probs * (self.n + 1.0e-9) ** 0.5 / (1.0 + n_children)
assert len(n_children) == len(self) > 0
assert len(q_children) == len(self) > 0
diff --git a/ginkgo_rl/utils/nets.py b/ginkgo_rl/utils/nets.py
index 722af98..fe39853 100644
--- a/ginkgo_rl/utils/nets.py
+++ b/ginkgo_rl/utils/nets.py
@@ -100,7 +100,9 @@ def forward(self, x):
class MultiHeadedMLP(nn.Module):
""" MLP with multiple heads """
- def __init__(self, input_size, hidden_sizes, head_sizes, activation, head_activations, linear=nn.Linear, init_sigma=1.0):
+ def __init__(
+ self, input_size, hidden_sizes, head_sizes, activation, head_activations, linear=nn.Linear, init_sigma=1.0
+ ):
super().__init__()
# print(hidden_sizes)
if linear != nn.Linear:
@@ -136,7 +138,9 @@ class DuellingDQNNet(nn.Module):
def __init__(self, input_size, hidden_sizes, out_size, activation, linear=nn.Linear, init_sigma=1.0):
super().__init__()
- self.mlp = MultiHeadedMLP(input_size, hidden_sizes, (1, out_size), activation, (None, None), linear=linear, init_sigma=init_sigma)
+ self.mlp = MultiHeadedMLP(
+ input_size, hidden_sizes, (1, out_size), activation, (None, None), linear=linear, init_sigma=init_sigma
+ )
def forward(self, inputs):
V, A = self.mlp(inputs)
diff --git a/ginkgo_rl/utils/normalization.py b/ginkgo_rl/utils/normalization.py
index c2b4b87..ac1be40 100644
--- a/ginkgo_rl/utils/normalization.py
+++ b/ginkgo_rl/utils/normalization.py
@@ -1,7 +1,7 @@
import numpy as np
-class AffineNormalizer():
+class AffineNormalizer:
def __init__(self, min_initial=None, max_initial=None, hard_min=None, hard_max=None, epsilon=0.001):
self._min = min_initial
self._max = max_initial
diff --git a/ginkgo_rl/utils/various.py b/ginkgo_rl/utils/various.py
index 06d9ab7..f3a18f7 100644
--- a/ginkgo_rl/utils/various.py
+++ b/ginkgo_rl/utils/various.py
@@ -70,3 +70,15 @@ def iter_flatten(iterable, max_depth=None):
yield f
else:
yield e
+
+
+class NanException(Exception):
+ pass
+
+
+def check_for_nans(label, *tensors):
+ for tensor in tensors:
+ if tensor is None:
+ continue
+ if torch.isnan(tensor).any():
+ raise NanException(f"{label} contains NaNs: {tensor}")
diff --git a/utils/cleanup.sh b/utils/cleanup.sh
new file mode 100755
index 0000000..a6b1ece
--- /dev/null
+++ b/utils/cleanup.sh
@@ -0,0 +1,12 @@
+#!/usr/bin/env bash
+
+echo "Converting code to Black style (see black.readthedocs.io)"
+
+black -l 120 ../experiments/*.py
+black -l 120 ../experiments/debug/*.py
+black -l 120 ../ginkgo_rl/agents/*.py
+black -l 120 ../ginkgo_rl/envs/*.py
+black -l 120 ../ginkgo_rl/eval/*.py
+black -l 120 ../ginkgo_rl/utils/*.py
+
+echo "All done, have a nice day!"