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 +[![arXiv](http://img.shields.io/badge/arXiv-arXiv:2011.08191-B31B1B.svg)](https://arxiv.org/abs/2011.08191) +[![ML4PS](http://img.shields.io/badge/ML4PS-2020-000000.svg)](https://ml4physicalsciences.github.io/2020/) +[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](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", + "\n", + "\n", + "Truth\n", + "\n", + "\n", + "0\n", + "\n", + "pT:326.6\n", + " m:80.00 \n", + "\n", + "\n", + "\n", + "1\n", + "\n", + "pT:45.7\n", + " m:27.91 \n", + "\n", + "\n", + "\n", + "0->1\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "8\n", + "\n", + "pT:280.9\n", + " m:10.90 \n", + "\n", + "\n", + "\n", + "0->8\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "2\n", + "\n", + "pT:38.8\n", + " m:8.53 \n", + "\n", + "\n", + "\n", + "1->2\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "5\n", + "\n", + "pT:10.6\n", + " m:7.58 \n", + "\n", + "\n", + "\n", + "1->5\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "3\n", + "\n", + "pT:8.8\n", + " m:3.85 \n", + "\n", + "\n", + "\n", + "2->3\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "4\n", + "\n", + "pT:30.1\n", + " m:1.21 \n", + "\n", + "\n", + "\n", + "2->4\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "6\n", + "\n", + "pT:2.0\n", + " m:2.73 \n", + "\n", + "\n", + "\n", + "\n", + "5->6\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "7\n", + "\n", + "pT:8.6\n", + " m:2.73 \n", + "\n", + "\n", + "\n", + "5->7\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "9\n", + "\n", + "pT:82.2\n", + " m:2.98 \n", + "\n", + "\n", + "\n", + "\n", + "8->9\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "10\n", + "\n", + "pT:198.7\n", + " m:7.57 \n", + "\n", + "\n", + "\n", + "8->10\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "12\n", + "\n", + "pT:101.3\n", + " m:2.36 \n", + "\n", + "\n", + "\n", + "\n", + "11\n", + "\n", + "pT:169.3\n", + " m:6.74 \n", + "\n", + "\n", + "\n", + "10->11\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "14\n", + "\n", + "pT:29.4\n", + " m:0.27 \n", + "\n", + "\n", + "\n", + "10->14\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "11->12\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "13\n", + "\n", + "pT:68.0\n", + " m:3.67 \n", + "\n", + "\n", + "\n", + "11->13\n", + "\n", + "\n", + "\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", + "\n", + "\n", + "MCTS\n", + "\n", + "\n", + "14\n", + "\n", + "pT:326.6\n", + " m:80.00 \n", + "\n", + "\n", + "\n", + "13\n", + "\n", + "pT:45.7\n", + " m:27.91 \n", + "\n", + "\n", + "\n", + "14->13\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "10\n", + "\n", + "pT:280.9\n", + " m:10.90 \n", + "\n", + "\n", + "\n", + "14->10\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "12\n", + "\n", + "pT:10.6\n", + " m:7.58 \n", + "\n", + "\n", + "\n", + "13->12\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "11\n", + "\n", + "pT:38.8\n", + " m:8.53 \n", + "\n", + "\n", + "\n", + "13->11\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "2\n", + "\n", + "pT:2.0\n", + " m:2.73 \n", + "\n", + "\n", + "\n", + "12->2\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "3\n", + "\n", + "pT:8.6\n", + " m:2.73 \n", + "\n", + "\n", + "\n", + "12->3\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "0\n", + "\n", + "pT:8.8\n", + " m:3.85 \n", + "\n", + "\n", + "\n", + "\n", + "11->0\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "1\n", + "\n", + "pT:30.1\n", + " m:1.21 \n", + "\n", + "\n", + "\n", + "11->1\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "4\n", + "\n", + "pT:82.2\n", + " m:2.98 \n", + "\n", + "\n", + "\n", + "\n", + "10->4\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "9\n", + "\n", + "pT:198.7\n", + " m:7.57 \n", + "\n", + "\n", + "\n", + "10->9\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "7\n", + "\n", + "pT:29.4\n", + " m:0.27 \n", + "\n", + "\n", + "\n", + "\n", + "8\n", + "\n", + "pT:97.4\n", + " m:4.53 \n", + "\n", + "\n", + "\n", + "9->8\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "5\n", + "\n", + "pT:101.3\n", + " m:2.36 \n", + "\n", + "\n", + "\n", + "9->5\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "8->7\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "6\n", + "\n", + "pT:68.0\n", + " m:3.67 \n", + "\n", + "\n", + "\n", + "8->6\n", + "\n", + "\n", + "\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": "iVBORw0KGgoAAAANSUhEUgAAAQwAAAEHCAYAAABbS3hQAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAALEgAACxIB0t1+/AAAADh0RVh0U29mdHdhcmUAbWF0cGxvdGxpYiB2ZXJzaW9uMy4yLjIsIGh0dHA6Ly9tYXRwbG90bGliLm9yZy+WH4yJAAAgAElEQVR4nOydd5hV1bn/P+v0M2d6oxcLiDRBQJoiolIUUIhdjAFjudYYjfkZrybX5ObeaEw0aozmJqISUaOIChZAKaKIggUBCzGC4Axl+py+y/v7Y88MM0zbw5yh7s/zzAOn7L3WDJx31lrv9/2+SkRwcHBwsIPrYE/AwcHh8MEJGA4ODrZxAoaDg4NtnIDh4OBgGydgODg42MZzsCfQFvLz86V3794HexoODkc069evLxGRgqZeO6wCRu/evVm3bt3BnoaDwxGNUmpbc685WxIHBwfbOAHDwcHBNk7AcHBwsM1hETCUUtOUUk9UVlYe7Kk4OBzVHBYBQ0ReE5FrsrKyDvZUHByOag6LgOHg4HBo4AQMBwcH2zgBw8HBwTZOwEgBSimuuOKKuse6rlNQUMDUqVMBmDt3LjfeeGOj63r37s2gQYMYMmQIQ4YM4eabbz5gc3Y4uhA9iRmuREyzXfc5rJSehyqhUIiNGzcSi8UIBoMsXbqUbt262bp2+fLl5Ofnd/AMHY5WRNeQaBXEYyAmBNPbdT9nhZEipkyZwuLFiwGYP38+l1566UGekcPRjBg6ZlU5UrYTSSbAHwTV/o/7ERcwxo8fz9y5cwHQNI3x48czb948AKLRKOPHj+f5558HoLKykvHjx7NgwQIASkpKGD9+PK+99hoAO3futD3uJZdcwnPPPUc8HmfDhg2MHDnS1nVnnHFG3Zbkj3/8o+3xHByaQgwDM1yBlBYjiSjiC6K8/pTd39mSpIjBgwezdetW5s+fzznnnGP7OmdL4pAKxDCQWDVEw+BSVqBQKuXjHHEBY8WKFXV/93q9DR6npaU1eJyVldXgcX5+foPHnTt3btPY06dP5/bbb2fFihWUlpa2ceYODm1HTAOJRSBSBQrEF0ApRepDhcURFzAOJnPmzCErK4tBgwY1CDwODqlGTBOJRyBSCUhNoHB1WKCoxQkYKaR79+7ccsstTb42d+5cFi5cWPf4gw8+AKwzDLfbDVjbmqeffrrjJ+pw2CKmiSSiEK4JFF5/q4FCtCTG+qWYRd/g/+E97RpfHU59SYYPHy6OgY7D0YiI7A0UYiCeAMrVcs5CDB3js1XoqxdAdTmu3gPwzb4XVzDU4nVKqfUiMryp15wVhoPDIYwVKGLW1sPQrRWFy9/yikJMzM1r0Vf+EynfherWB+95N+Dq3BvlC7RrPq0GDKXUQBHZ2K5RHBwc2oSIgJZAwhWga1ag8PhaCRSC+c1n6CteQHZtQxX2wHvhbbj6DLUyJolYu+dlZ4XxF6WUD5gLPCsiFe0e1cHBoVkkGUcilaAlEY8P5U9r9TDT3P4V2vLnke1fobIL8Z53Pa7+o1vdtrSVVgOGiJyqlOoDzAHWKaU+BJ4UkaUpnYmDw1GOaAkkXGmtLOwGip1b0Vf8E/ObTyGUjWfybNxDxqPcHXPaYOuuIrJFKfWfwDrgT8BQZalCfiEiCzpkZg4ORwmiJZFIhRUoXF57gaJsJ/rKFzE3r4FACM8Zl+AeMTGlqs6msHOGMRiYDZwLLAWmicjHSqmuwBqgXQFDKeXGCkTfi8jU9tzLweFwQvQkEq6CZBxxu1G+1gOFVJWhr34Z49MV4PHiHnMentHnogItZz5ShZ0VxiPA/2GtJupOTUSkqGbV0V5uAb4AMlNwr4PGrl27uPXWW/nggw/IycnB5/Nxxx13MGPGjJSO86Mf/YipU6dywQUXpPS+DgeOvRWkUcTlQfmDrQeKaDX6mtcw1i0B08Q97Cw8Y89DpWcfkDnXYucMY1wLrz3TnsGVUt2xVi7/Dfy0Pfc6mIgI559/PldeeSXPPvssANu2bePVV19t8D5d1/F4nEz20YoYOhKphngYcbltbT0kEcP48A30DxZDMoFr0Kl4xs3ElV1of1wxUVoc3B5oZ31Js0eoSqnPlVIbmvtq16h7eRC4A2jW1UMpdY1Sap1Sat2ePXtSNGxqeeedd/D5fFx33XV1z/Xq1YubbrqJuXPncuGFFzJt2jQmTpxIJBJhzpw5jBgxgqFDh/LKK68AYBgGP/vZzxgxYgSDBw/m8ccfB6xgdOONN9K/f3/OPfdcdu/eDcDbb7/dYPWydOlSZs6ceQC/awe77E8FqehJ9A/fIPHnn6KvegnXMQPxXfO/+KZfZztYWKnZOCqZgFA2Kqew3VmTln7d1Z4n3FDzZ+1q4nIg2q5RAaXUVGC3iKxXSo1v7n0i8gTwBFhKz9buO/nm/2703MwzRnLNjLOIxhPMvOP3jV6/fMppXDFlHCUV1cy6508NXnvzT3e1NiSbNm3i5JNPbvb1NWvWsGHDBnJzc/nFL37BhAkT+Pvf/05FRQWnnHIKZ511Fv/4xz/Iysrio48+IpFIMHbsWCZOnMgnn3zCV199xeeff86uXbvo378/c+bMYcKECdxwww3s2bOHgoICnnzySWbPnt3qXB0OHHsrSKsR5QIbFaRiGhgb3kV/dwFUleLqPQDP+ItxdTuubWNrCZRpQFoGKpiBqik/aC/NBgwR2QaglBorImPrvfT/lFLvAfe2c+yxwHSl1DlAAMhUSs0TkVntvO9B54YbbmD16tX4fD5uuOEGzj77bHJzcwFYsmQJr776Kr//vRW44vE43333HUuWLGHDhg28+OKLgOXVsWXLFlatWsWll16K2+2ma9euTJgwAdhrCzhv3jxmz57NmjVrnDqUQ4TGFaQ2AoUI5pcfoq98ESktQnU5Fs/Ua3AfM7BtY+tJlGGgAmmoUGbK06t27hZSSp0qIqsBlFJjgHYfyYrIncCdNfccD9yeimDR0oogLeBv8fX87AxbK4p9GTBgAC+99FLd40cffZSSkhKGD7fk+KHQ3h+XiPDSSy9xwgknNLiHiPDwww8zadKkBs+//vrrzf5nmz17NtOmTSMQCHDhhRc65yMHmf2pIBURzG8/R1/+ArLzW1R+N7w/+AmuE4a3yc9CdA1laihvEJWVh/L42v39NIWdDc1VwKNKqa1KqW+BP2OJuBxqmDBhAvF4nMcee6zuuWi06V3bpEmTePjhh6kt+vvkk0/qnn/sscfQNA2Ar7/+mkgkwrhx43juuecwDIPi4mKWL19ed6+uXbvStWtXfvOb3/CjH/2og747h9YQ08SMhZHSYohUIl4/+NJQrVjimd9vQfvHf6PN/x0Sq8Y77Vp8V/8v7n4jbAcLMXRIRFEuhcouxJWd32HBAuxlSdYDJymlMrGqW1Per1BEVgArUn3fA4VSioULF3Lrrbdy3333UVBQQCgU4ne/+x2xWEP9/t13381PfvITBg8ejIjQu3dvFi1axI9//GO2bt3KySefjIhQUFDAwoULmTFjBu+88w6DBg2ib9++nH766Q3ud/nll7Nnzx769+9/IL9lB5qvIG1VdLV7O/qKFzC3fAyhTDwTf4h76ASUx2t/bNPKfCi3B5VdAF5/hzhs7Uur5e1KqU7Ab4GuIjJFKdUfGC0if+vw2e2DU97emBtvvJGhQ4dy1VVXHeypHDU0XUHa+qGiWb4bfdWLmBvfB38Az6ipuE+Z3KYKUitFmrAMfdOzrNRsigNFe8vb5wJPArWb+6+B54EDHjAcGjJs2DBCoRAPPPDAwZ7KUcH+VJACSHU5+nsLMT5ZDi4X7tHn4hk9DdUGy38RsVYUAKEsVCCU8sIyO9gJGPki8oJS6k4AEdGVUkYHz8vBBuvXrz/YUzhqEF1DqsusQOG2V+8hsQj6B69hfPgWmAbuIePxnDoDlZFjf9yaIKXEtFKkaRm2VjPN3au9qxE7ASOilMoDBEApNQpI+TmGg8OhimgJpGKPpc702ZBxJ+MYH72FvmYRJGK4BozGc/oFuHI6tXHcJMrUIZiBSkvf7xSpiBCNJ6mKxSnMzsDdjpWJnRn8FHgVOK5Gf1EAOIUMDkcFZjwCVWVWuXkrH1gxdIyP30F/byFEKnH1ORnP6Rfi6tSzTWOKrqEMHeUPoEL5bToM3RfdMCivjhFLJC1NSDsdOe1kST5WSp0OnAAo4CsR0do3rIPDoY2IINHqmjRpy/6ZYpqYG99DX/USUrkH1fNEvBf8BFf3vm0b09BRegLlC6AyC1He/U+PigiReIKy6hhulyIt4COWaP/H1k55exrWKqOXiFytlOqjlDpBRBa1e3QHh0MQMU3LyCYeblGlKSKYX6+3vDP37EB17o13yhxcxw5qm+jKNFBaAuX1obIL2+27qekGZeEI8YRG0OfD5UpdFsXOluRJYD0wuubxDuCfgBMwanC73QwaNAgRwe1288gjjzBmzJiDPa2UM3fuXNatW8cjjzxysKfSYYhpIFWloCWhhYNNY+sm9OXPI0XfoHK74J15M65+I1oVazUcy0TpcZTLA5l5Vpl7Ow4lRYTqaILySBSPy0UokHozHTsB4zgRuVgpdWnNpGLqQChEDiOCwSCffvopAG+99RZ33nknK1euPMiz2n+O1jJ80TUrWJgG+IJNvscs+sYSXX27ETJy8Zx7Ne7Bp7Upc1Fbbq5wQXqOlXFpZ4o0qeuUVUVJ6jpBrzelq4r62JllUikVZG+W5Dgg0SGzOQKoqqoiJ2dv2uz++++vK1n/5S9/Wff8+eefz7BhwxgwYABPPPFE3fPp6en8/Oc/Z9iwYZx11ll8+OGHjB8/nmOPPbaRvwZAcXEx48aNY8iQIQwcOJB3330XsIrcRo8ezcknn8yFF15IOBwG4N5772XEiBEMHDiQa665pk6iPn78eH7xi19w+umn89BDD/HRRx8xZswYTjrpJE455RSqq6sBKCoqYvLkyfTp04c77rgj9T/Ag4RoSaR8l/Xz8DbeEpglRSRffJDkk/dg7tqG56xZ+K9/AM+Q8baDRYNy87QsVF5nXMH0dgULEaEqEmdnWRWmaZLmT+0WZF/sKD3PBv4T6A8swaoy/VGNnPuAYkfpefMj/2z1PqMHHMulZwyre/+UU/oz5ZQBVIRj3DO34U7rTzde2Or9arck8Xic4uJi3nnnHYYNG8aSJUt48cUXefzxxxERpk+fzh133MG4ceMoKysjNzeXWCzGiBEjWLlyJXl5eSileP3115kyZQozZswgEomwePFiNm/ezJVXXlm3kqnlgQceIB6Pc9ddd2EYBtFolEQiwcyZM3njjTfqJOqJRIJ77rmnblyAK664gosuuohp06Yxfvx4+vfvz5///GeSyST9+vXj+eefZ8SIEVRVVZGWlsa8efO49957+eSTT/D7/ZxwwgmsXr2aHj16tPozOpQx41GoKm02E2JsWoP22uPgduMZeQ7ukVNQ/rQ2jdER5eZJXaes0lpVBHytrypiCY0ueVl43C0HqP1WeiprQ5YDzARGYWVJbhGRkhZHPMqovyVZs2YNP/zhD9m4cSNLlixhyZIlDB06FIBwOMyWLVsYN24cf/rTn3j55ZcB2L59O1u2bCEvLw+fz8fkyZMBGDRoEH6/H6/Xy6BBg9i6dWujsUeMGMGcOXPQNI3zzz+fIUOGsHLlSjZv3szYsZYrQTKZZPRo6whq+fLl3HfffUSjUcrKyhgwYADTpk0D4OKLLwbgq6++okuXLowYMQKAzMy97olnnnkmWVlZAPTv359t27Yd1gHDjFZDuKLJTIiIoL+7AOPdBageJ+CbeQsqPatN97fKzXVLmZmW0a4Uad2cTaE6lqAiHMXncZMWaD6boukGm7ft5KTjurV7XGglYIiIqZS6UUReABanZMQOxs6KoLn3Z6cH23z9vowePZqSkhL27NmDiHDnnXdy7bXXNnjPihUrWLZsGWvWrCEtLY3x48cTj8cBq+N87RGRy+XC7/fX/V3X9UbjjRs3jlWrVrF48WKuuOIKfvazn5GTk8PZZ5/N/PnzG7w3Ho9z/fXXs27dOnr06MGvfvWrunFhbxl+S4rA2vmAtbJqak6HAyKCVFc0mwkRLYm26HHMzR/gHjwOz5Q5bSsOM3SUkUR5AyktN09oOmVVETTDJM3vbfWQ9IWVH7PwvQ08etNFZKS1L/sC9s4wliqlbldK9VBK5dZ+tXvkI5Qvv/wSwzDIy8tj0qRJ/P3vf687P/j+++/ZvXs3lZWV5OTkkJaWxpdfflnXmHl/2LZtG4WFhVx99dVcddVVfPzxx4waNYr33nuPf/3rX4BVav/111/XBYf8/HzC4XCdWc++9OvXj6KiIj766CMAqqurD9vA0BRiGkhlKSQiViZk32ARriA57zeYm9fimXAJnqnX2A4WdeXmippy84KUBAvTFCrCUXaWVQG0GCw03aCkMgLAeWMGc+elEynMyWj3HMBelqTW++KGes8JcGxKZnAEEIvFGDJkCGD95nrqqadwu91MnDiRL774om47kJ6ezrx585g8eTJ/+ctfGDx4MCeccAKjRo3a77FXrFjB/fffj9frJT09naeffpqCggLmzp3LpZdeSiJhnU//5je/oW/fvlx99dUMGjSI3r1712059sXn8/H8889z0003EYvFCAaDLFu2bL/neCghhm5lQgy9yUyIuWsbyRcegFgY7wU/wX1Ck1v5xvftwHLzRFKnpCqMYUirq4pvi0v508KVuJTivmvOIz3o5+Q+qdsyOt3bHY4aREvW1IS4mvytb3y9Hm3hoxAI4bvoNlyde7d+zw4sNzdMk6ponKpIHJ/HjdfT/EGpbpi89O6nvLjqEzLSAlw39VRO6derwXs6/NCz5uKmrKgrgc9FZHdr1zs4HAqYiZiVCXF7G2VCRARj7evob89HdTkG34U/bbWitKPLzeNJjdKqCKbZ+qpi685SHl64im93lnLaoOP48ZTRKTmvaAo7W5KrsFSetd5w44EPgL5KqXvb25vEDkqpacC0448/vqOHcjgCaTETYujob/wd47OVuE4ciXfatS22AEhluXlTGKZJRThGOJbA7/Xg9zYfhAzD5OX3PuOFFZ8QCvq44+KzGHVi75TNpSnsBAwTOFFEdkGdA9djwEhgFXvbD3QYIvIa8Nrw4cOv7uixHI4cRMTqgh6trjPkbfB6tJrkSw8h332B+9QZeMbNbFHaXVduHkhHhTJS7sgdS2iUVkcQs+VDTYBtu8p4ZOEqviku4dSBx/LjKWPIDHXMqqI+dr7j3rXBoobdQF8RKVNKOVWrDockVk1IOWixJtOmZkkR2gu/R6rK8J53Pe6BY5u5U832IxmzvDDSU+/IrRsmlZEokVgSv9eDu4VVRS3/KiphT2WY2y88kzEDjknpfFrCTsB4Vym1CKvgDOAHwCqlVAio6LCZOTjsJw0zIY0LyIxvN6K99BB4PPhm3YWre5/m71WT/SAt0+rzkeIyqkg8QXm1ZRTdkgALYMeecr4vqWTkib2ZMKQPI/v1Ij3YeoGZiBBLaPj9HlwHwHHrBiyl56lYSs+ngZfESq+c0a7RHRxSjOhJpKIEQaGaqAnR1y9Df+spVH5XfBffjsoqaP5eho4yNMjMwxVomxS8NWpXFeFYkoDXg7uVzAXAU0s+ZPuecob17YnH7bIVLDTdIKkb5KSnkZHW/lSvHQMdAV6q+XJoAqUUs2bN4plnrOMcXdfp0qULI0eOZNEiqzbljTfe4O677yYSiSAiTJ06lZycHP75T2vh9vnnnzNo0CAA5syZw6RJk7j22mupqKggkUhw2mmnNShSc2iMJONW2rSJmhAxDfSl8zDWLcF1/BC859+I8jddkQo1tR9Y4qv2GNk0um+NXV5ZOIoCQq2sKopKKvF5PeRnhbhu2qm4lGo1LVo7Tiyp43O76ZKXiS9F1cdHXw1zBxAKhdi4cWOdyGnp0qV067ZXu79x40ZuvPFGFi9eTL9+/dB1nSeeeILrr7+eu+6yzNjT09MbFJZNmjSJW2+9lfPOOw+wAopD85ixMFSXN2n5L/Eo2ssPY/57A+6RU/BMuKzlFGgyZpnZZOSlrCcp7LXLiyaSBHyeFr01DdPk9bWb+Mfb6xh+Qk9uv/BM8jLtNRzUDZOEppMVCpCZFkxp9eqB9yk/QpkyZQqLF1vlNvPnz+fSSy+te+2+++7jrrvuol+/fgB4PB6uv/76Fu9XXFxM9+7d6x7Xrj4cGiIimOEKqC6zMiH7BAuzYjfJp36FuXUTnnOuwnvWrGaDhYgJiSj4Q6jM/JQFCxEhEktQVFpFUtMJBXwtBoui0krufnIxT761lsHHdmPO5NHNvnffcWIJDcM06ZyTSXZ6WspL3W2tMJRSPqDWoPCQ9fRMvvoXzKJvUnpPV9fj8E2/rtX3XXLJJdx7771MnTqVDRs2MGfOnDpvio0bN3Lbbbe1adxbb72VCRMmMGbMGCZOnMjs2bPJzs7er+/hSMWy0iuHeBTxNVZYmtu/IvniH8E08V76c9y9B7RwL8smj/QcXGn2+4W0Rn27vIDP22KgME3hjQ838cyyj/B63Nx0/umMP+l4W+cOhmES13TSg36y04PtcgZvCTtKz/HAU8BWrEPPHkqpK0VkVYfM6DBl8ODBbN26lfnz53POOee0+36zZ89m0qRJvPnmm7zyyis8/vjjfPbZZw2qRY9mxDCQqhLQtSat9IwN76K9/n+orAK8F92GK69L8/fSkyjTQGUXtNtPs+6eNSa8pdX27PJ2llXxyCur2LxtJyf36cF/TDvV9hYkltBRCjrlZBDwtb98viXsrDAeACaKyFcASqm+wHxgWEdObH+wsxLoSKZPn87tt9/OihUrKC0trXt+wIABrF+/npNOOqlN9+vatStz5sxhzpw5DBw4kI0bNzJs2CH3Yz/giJ5EKkstfcQ+BWQiJvqKf2K8/yqu3gPwzrwFFWzhg6fFUS43KqdTSrwqwFpVlFZHSGr27PLeWvcFc99ai9uluOG8cUwY0sfeqsI0iSd10oM+skJptg5D24udEby1wQJARL4GOjaMHabMmTOHe+65p9F5w89+9jN++9vf8vXXXwNgmiZ/+MMfWrzXm2++WdfJfefOnZSWljY4SD1akWQcKdtdkzb1N3pNe+lPGO+/invoBLyX3NFssBARq7zd47dWFikIFrV2ecVllZiGfbu8ykicE3t25sHrf8CZQ/vaChaJpE5SN8jPCpGXmX5AggXYW2GsU0r9jb0S8MuxXMQd9qF79+7ccsstjZ4fPHgwDz74IJdeeinRaBSlFOeee26L91qyZAm33HILgYC1RL7//vvp3Llzh8z7cKGuqVBTmZCqMpL/fADZtQ3P2bNwj5jcfHsA00RpsRoxVlZKxFhtscsTEZas/5KCrHRO7tODH5x2Ei6lbM3DNIVYMkma309ORhBPCrM4drDj6enHEm/VCrdWAY+KSLLjp9cQp7z96GRvTUhVjcy74W9Ts/hby8MiGcN7/o24+wxt/l6GjtKTkJmLK2DvjKAl9rXLa6kEvRZNN7jjiYX07pzHLTPH2x4rqelopkleRhqhQOr8Nvalvd3brxORPwB1a2il1C3AQyman4NDs7SWCTG+/BDtlccglInvyl/iKmy+LaFoSRRinVekQIyV0HTKq6IkDaPVYjERYcVn/+KUfr0IBXz86spzyLRZgm6aQlzT8Ps8FKRn2ApKHYWdjc+VTTz3oxTPw8GhEXWZkES0kZWeiKCvXoj20kOoTr3wz763xWBBMopyu1A57Vdu1trl7SqrRmjdr6KkMsKv573FwwtXsnT9lwBkhew1LdJ0g7imkR1KozDr4AYLaGGFUdO46DLgGKVU/YYYGUBp01c5OKQG0ZJIVW0mpGEdh+ga2uK/Ym58D9fAsXjP/XGzFaR1jlj+dFR6VrtNbhJJndLqMLouBP2eVlcV73y6hSffXINhClefM4ZJw0+0NU59aXfn3NRJu9tLS7N4HygG8rFSq7VUAxs6clIOBw/DMHC5XB22P24N0TWrCXI8irg9jVYDEqkk+eIfkR1b8Jx+Ae6x57dwuFkrxspGBdPb9T3ta5eXFmj5A1xaFeGx11bz8Zbt9O/VmRvPG0fn3MwWr6mltmCsI6Td7aXZ71pEtgHb2NtT1eEIxzRNduzYQWFhIcFg84VZHYEYBhKrtsxuXG6rz+i+89u9He2FB5BIJd6ZN+M+cWTz99M1qx9IO8VYhmkSjiWpisZAWje2qT2r+Puba9B0g6smj2LKKQNsfehFhHhSx+VSdM7NxO89NFYV9TmoM1JKBbCyLv6aubwoIr9s+SqHjmLPnj1UV1dTWFh4wMYU00BiEYhUgUs12y3d+NenaC8/DL4AvivuxtW1BdN6LY5SLlTu/ouxDNMkEktSGY0hJgR8nlY/9IZhct8Ly/joq+/o16MTN54/jq559hof1Uq7M9ICZIcOrVVFfQ52CEsAE0QkrJTyAquVUm+IyP436nDYL6qrqykvL8frPTCaPDFNJBGFcCVg1lno7fsxERGMj95CXzYPVdgL30U/RWXmNX1PEUtf4Q2iMnP2y2vTNC1Jd2UkjmmKrUBRi9vtojA7g9mTRnLOyAG26zliCR3lOjDS7vZit/gsCPSsr/hMBTVeG+Gah96ar8On78ERgqZpFBcXEwqFiEajHTqWiCCJGEQqwdQRj2XM29RHUgwd/a2nMD55B1ff4XjP+49mtxciJiq5/2Is0xSiiQQV4TimCH6v29YHvioS5y+LVjPz1JM4vlsBV02xv4M3TJNEUicU9JGdntZhBWOppNUZ1jh2fwq8WfN4yD5Zk3ahlHIrpT7F8gpdKiJr93n9GqXUOqXUuj179qRqWIcaRITi4mLcbjfuDlYNWgY3uy27f5fbss9rrtQ8FkF77j6MT97BPWYa3gtuaT5YGDoqGbecsdKz2xQsTLOm9LyskrLqGF6PizR/y1Wl9XG7FFt3lrF9T9vcKmul3Xk10u7DIViAPaXnemACsEJEhtY8t0FEBqd0IkplAy8DN4nIxqbe4yg9U09JSQmlpaVkZFit9Kqrq+nZs2dKDz1FSyDhStASzXZIr4/5/b/QXv0LUrEbzzlX4Tnp9BbunUSJicrObxQRjDsAACAASURBVLE9QKPrapyvKiIxDMPE34qhTX0qIzFeef9zLpswHI/bhaYbtvURtQVjaX4fORkHpmCsrbRX6amLSGVHp9lEpEIptQKYDDQZMBxSSywWo6SkpC5YpBrRk0i4CpIxq4FQE2XoDd4frkRf/hzGhlWQno3v8l/g6tmv+QuSMZTHi8ossG35LyJEE0kqwjEM08Tv8bQpG7Fm87c8vug9ookkI/r25MRenW0Hi4SmY5hCfmaItIDvoKWu24Odn9RGpdRlgFsp1Qe4GUuj0W6UUgWAVhMsgsBZwO9ScW+HltF1naKiIoJBe4rDtiCGjkSqIR6pSZG2EigMHWPdEvR3F4CWxD16Kp6x5zfruVlr+08gDZWeY0uMZQmhNCrCUXSj7YGiKhLnr6+/z3ub/s1xXfK5acbp9CxsuTtaLfWl3Z0yQge8YCyV2PmJ3QTchZXReBZ4C/hNisbvAjyllHJjnae8ICKLUnRvh2YQEXbt2oWINMiKvP322+Tn59OzZwsS65buW6uliIURFPgCrQYj49uN6EueQkqKcB07GM/EK3DldW1+jFrb/1CW1XWslfvXahsqwlE03cDn9ZDmtx8oqqNxFq/dxOtrNxNPalw2YTgzxg625fINlghLMwyyQ6lx7T7Y2PnJnSAid2EFjZQiIhuA5ksLHTqEqqoqqquryczcqzwUER599FHcbjfjx49v0/3ENJF4xMp8gNWSsJUPhlmxB33ZPzC/+giVXYj3wttw9RnasiiqttI0Kx9XC47ftd9PQtOpqI6R1HUrULTi0F2fsqoIr7z/OUvXf0lc0xlxQk8umzCcXp1ybV1/qEq724ud7+IPSqkuWI2MnhORTR08J4cOJJFIsHPnTtLTG/pWKqV49tlnKS4utn0vMU0kGYPqCsCsCRRNp0jrrtGS6Gtew1jzGigXnvEX4R45pdVuYpKMW+nX3E6tvjee1KgIx0hoeo2M236gEBGUUvy7uJTFazdx2qDjOH/sYNuBAmpWFbpJVnqQjKD/kBVh7Q92+pKcoZTqDFwEPKGUygSeF5FUbUscDhCmaVJUVITP58NVb9+/atUqRo4cSSAQID8/v9X7tEVLUf8a86t1aMvmQWUJrv6j8J55WbMirPrXKS1uy/Y/kdQpj0RJJnW8HnerPT/qY5gmDy1YQeecTC47czjD+vbgsVsupiDbviGw5dqt4/EoOuVmHJLS7vZiayMmIjtF5E/AdViajHs6dFYOHcKePXvQNK2BkfD27duZNWsWDz1kz96kTktRXdaqlqIWc88OtPn/i/bSgyh/EO+su/DNuMlGsKgRYwXTW7T9TyR1dlVUsau8CjGFtIDPduZi++5yANwuFwGfF5/Xuk4p1aZgoRsm0YRGRshP55ysIzJYgD3X8BOBi4ELsMranwPa5pnvcNCplX7vm0Lt0aMHc+fOZfjwJtPudYiWRCIVkIwjHr/VmLiVMSUeRX93Aca6JeDz45l4Je5hZ9qSbNedV2Tk4Ao2/cFNaDqVkRixhNamrYeIsH7Ldha8+xlfbt/Fg9f/gJ6FOVw//TRb1+/L4STtbi92wuCTWC7hE0WkqIPn49ABaJrGzp07CYVCDQ4V9+zZQ0FBARMmTGj2WtE1yx4vEUfcbpQ/1HqgEBNjw7voy5+DSDXuoWfgOf1CVKj18u66LYjLXWN201iMldR1KsNxYslkjYW/vUBhGCbvb/6WBas/Y9uuMvKz0vnxlNF0yt4/HYpR02Es1MG9QA4l7JxhjDoQE3HoGESEnTt34nK5Gki/X3nlFW6//XYWLFjQZFc1MXQkGoZYNeLyNFlu3hRm0Tdobz2FFH2D6tYH78V34OpyjL251q4q0jKslOk+K5GkrlMdjROOJfG4XaT57QWKpKaz/LMtLHxvA7vKq+men81N55/OaYOO22+lZTypAYqC7AyC/iN7VVGflhy3XhCRi5RSn9OwIExh1Y2lVBru0DGUl5cTjUYbbUWGDx/OZZddxokn7uMAZZpIpBKp1hDlgmbKzfdFIpXoy1/A+GwlhLLwTr8O18CxjQx7m7y2dlXh9jTpt6npBlXRGJGYhtut2nSY+fraTbz47qdUhGMc37WAH00cyYgTeu135qJW2h0KWAVjh6K0uyNpaYVR65c/9UBMxCH1xGIxdu/e3SBY1KYNu3Xrxn/91381vMA0cUfKIcOL2CziEkPHWL8MfdVLoCVwjzoHz6nno/xprV4LNV3HDN0SYgXTGxygWoEiTjiWwONykRaw95s8HEsQqpFef19aSc+CHH4yczyDjunaLuFUfWl3KHh0dqBryXGrNiF/vYj8vP5rSqnfAT9vfJXDoYJhGE1Kvx988EG+/fZbHnjggYbeFyIQqQDTQDXhzt3kGFs3oS95GtmzA9exg/Cc/UNc+c2rNOtjZUDiKK8flZXXQFuhG2ZNoIjjVq5WXa7q88V3O7n3mTf5z8snMaB3F+ZMGmVbldn0PIWkZmCISdDnOyi9QA4l7Bx6nk3j4DClieccDhFEhN27dwM0MsSp1V949lEeqmgVsaoKMvMKWu3fKpUlaG8/i/nFWlR2Ad4LbsXVd5jtD7VoCZRpQkYOKrD3INY0hXAsQUUkhkIR9NkLFN+XVFBeHWXgMV05tks+ZwzpQ26GtcLZ32BhmpZS1ERID/jJSPMfMWrN9tBsebtS6j+A64Fjgfot0TOA90RkVsdPryFOebs9Kisr2blzZ7NVqLXbkjriYfSKUkyPjx49ezZ77iB6EmPNYvT3LTsUz5jpuEeda9u2v64OxBdEZWTXVZjWlpqXR2KIKfi99lyuvikq4aV3P2XtF1vpXpDDg9fPbHethm6YJHUdl8tFZjBAWsB31J1T7G95+7PAG8D/AP+v3vPVIlKWwvk5pJBa6Xco1LCr1/3338/EiRM56aSTGn6oEjFUpIqEKLp37tJksBARzK/Xoy+bh1TswXXiSEulmdW6KrQOLY4SIDPPyrjUzCGe1Kx6D0PH7/Xg9rb84RQRNm0tZsHqz/j0m+9J8/uYedpJnDtyYPvPJwwTn8dDflY6ARtNlI9GWjrDqAQqgUsBlFKFQABIV0qli8h3B2aKDnYxTZPi4uJG0u+ysjKef/55lFINO8jrGq5IBdUJnbyCprciZkkR+tJnMP+9AVXQHe/lv8Dde4DtOVmp0gQEQqhQdp1aM6nrlIejJBKWjLu1FKlpCuu+/o4Fqz/j6x27yQ4FmXXWCCYNP7FNWZN975nQDASrcXJGZgC/z9l2tIQdpec0rDaJXbFs9HoBXwD2/9c4HBBKSkpIJpONCstyc3NZtmxZw+cNHRUuJW6YBNKCZGc3dLeWRBR99UKMD98Erx/PxCtwDzvbtrFuXapUuVDZhXX2erphUBmxMh9et8uWOtPqHPYm2/eUU5idzjXnjuGMIX33W35tGCYJ3UApyEjzkx70H9UHmW3Bzk/8N8AoYJmIDFVKnUHNqsPh0CEcDlNWVtbg3MIwDBYtWsS0adPIzs7e+2bTRFWXYZoKzRC6dCuEGlmWiIn5+Xto7zwHkUrcJ52O54yLUCF7dvnQtADL6u9hHWi6lWo185HUdL7bXc7x3QrIzUija14mM08dzKkDj9vvg8zaBkFej4u8jDQCbfDudLCwEzA0ESlVSrmUUi4RWV6TVnU4RKjv+l3/Q7ho0SKuv/56MjMzOeOMM6wnxUSFywEhmkzSqVMnvDWHlmbFHrSFjyLfb0F1PQ7vRT/F1fU42/NoKMCyZN2WyW6cinAcESHNZubjL4veY91X3/HETy8h4PPy80vObtPPpP6cEpqOaULA76FTRhp+b8stDh2ax07AqFBKpWM1HPqHUmo3oHfstBzs0pz0G2D69OlkZmbuNcQRQUWrwdCI6SahUKhumyJxy6VbIhV4pl6De/BptlSadfPYR4CFUsQSGuXhCLpuz2T3q+27yctMIz8rnfPGDOKMIX32f9thmiSSBihID/rJCPoPeiPjIwE7ruEhII61Zr0cyAL+ISIHrCFzzTnKtOOPP/7qLVu2HKhhDwvKysrYs2dPg61IJBIhHo+Tl9ewfFzFqlHRanS3j6SWpEePHrjdHsTQ0Z6/H3PbF/guuxNXL3sNg2GvAAuvD5WRg/L4SGg1B5pJK/PRWlqyvDrKvGUfsfyzLUwa3o9rp57ath9CPaxth47H7SYrFCDo9znbjjbSUlq11YBxKOHoMBoSi8XYtm0bGRkNvS3vuOMO3n77bVauXLn3oDMRxRWuwPT6iUSjdO3alWAwDRFBf/1vGJ8uxzvtWtyDx9keX7QkyjQgIxsVCNUoNGNE4km8bnerv9E13eD1DzfxwopP0AyD6aMH8YPThrS5mKu+GtPn9ZCVFiTgc7Yd+8t+6TCUUtU0UXTG3uIze62oHTqE5qTfAFdeeSX9+/ffGyz0JK5IBeLzE4slyM7OJhi0lJDG2tcxPl2Oe8x5toNFrQBL+YKojAIMXIQjMaqitVLu1jMfn/5rB397cw3fl1QyrE8PZk8eZbsPaS0N1Zg+0oOBI9a45lChJR1GxzSrcGg3tdLvfV2/dV3H4/EwYMAABgyoyXrrGqq6DPH40HQDt9tFbq7lT2l8tQ797fm4ThyJZ/wF9sZOxlEIZOYh3gDhuNWwGLEn5d5VXs2Tb33Ah19uo0tuJr+4bCLD+7bNpbxOjalcZIaChI5CNebBwm5v1VOBPiLypFIqH8gQkW87dmoOzVFVVUVlZWUD12/DMLjssssYM2YMP/nJT6wnTcPKiLjciHKRSMTo3r0HSrkwi79Fe+XPqK7H4p12XasHnGIa1qoiEIK0LGK6QUV5FYYpBGxKuSPxJLf9ZQGmKcw6cwTTRg9s00FkUtPRa9SYeZkhgj6fo8Y8wNgRbv0SGA6cgOW+5QPmAWM7dmoOTZFIJNi1a1cjcZamafTs2ZMuXbpYT4iJClcAAm4vkXCEvLx8/H4/UlVK8oUHIC0D34W3tVgLsq8AK4mH8sooCV0j4PXityHl/nL7Lk7s2ZlQwMe1U0/lxJ6dyc8KtXhdLbXbDkFI8/vIywzg87qd84mDhJ0Vxgys3iEfA4hIkVLK2a4cBGql316vt4H0GyAQCPD73//eeiCCilSCoYHXTyKRIBgIkJ2dhSTjVrBIxvBd+StUevPnBvUFWJovSFVUI5aI1jhy2/ODWPHZv3h44Up+/aNzGdC7C6cNsqfrMEyThOaoMQ817Gz8kmKlUgTq0qwOB4HS0lKSyWSDmo9oNMrNN9/M9u3b655TsWpIxsHrxzBMNE2noLAAMcUSZu3+Du/Mm3EV9mhyHBGx+paKiZGZT4X42FkeIanrthy5I7EE3xSVADB2wDHccN44+vXoZOt7rLXq13ST3IwgXfOyapytnGBxKGAnYLyglHocyFZKXQ0sA/7asdNy2JdwOExpaWmjKtQvvviCt99+e2/AiEdR8TDUmOfGYlE6dSrE6/Whv/0s5paPLffu407adwjAMv1VyRhGIJ1qfxbFVXEisQRpfm+rGQjTFJZ9/BU3PPxPfv/C21b1p9fDmUP72pJza7pBNKGRFvDSJS+T9GDA0VAcYrT4P0BZG8XngX5AFdY5xj0isvQAzM2hhlrpd1paYyesYcOG8cEHH1jCLS2BK1qJeP2W0jIWq1Nz6uuXYXz4Bu4Rk/EMb1pmLYYOpk4smEN5Qsc0EgR89g40v96xm7++/j7fFJXQr0cnfnzOaNs1H1azYh2v20XnnEynYvQQpsV/GRERpdRCERkGOEHiIFBf+l3fJau4uJhPPvmEc845xwoWuoaruhzxeEG5MHQDgIKCAox/f47+1lO4jh+C56zLmx7HNNETMcpd6SRjSVveFFCj0nz7I5Z/uoWc9DRumTmecYOOs30oaflkmmSH0kg/wtoKHonYCeUfKKVGiMhHHT4bh0Y05/r92GOPMX/+fE455RTyc7JR4TLE7bZSqCLE4jG6du2KKttJcsGfLC+L829sskuZiBAPV1FGAHfQQ5oN8ZNumLy+dhPPr/gYTTeYMXYwF4wbQtCm9X+t+3aa30d2etCp8zhMsBMwzgCuVUptAyI4bQYOGLFYjD179jRKoQLcfffdzJw5k/zcXFS4DFBQY3kXi8XJzs4mYGgknr8fPH58F9+OaqLjuYhQUVZGVPnxZWfYPjO4/4VlfPTVd5x8fHfmTB5N13x7Kk0RIZ60sh/5WSHS/D4nRXoYYSdgTOnwWTg0wjAMiouLCQQCDT5Q27Zto7CwkGAwyJCTTkLVOH1T47qtaRput4ucjHSSz/4PRKrwXXF3k31MNUOnvLQc3e0jmJMLrXxwd5dXk5EWIOj3Mm3UQM4+uR/DT7Cv0qz1o0g/ijqFHWnY6Xy27UBMxKEhu3fvxjRNAoFA3XO6rvPDH/6Qbt268eyzz9akTxPgszIiIkIikaBbt27oi/+KfP8N3h/cgqvrsfvc3TLdraisxOPxEsjNbzVYlFVHufnRF5k+ehCXnTmcgcfYaycAzqHmkYTzL3cI0pT0G6zWAL/+9a+t+pF4GBWPWBmRGiKRKHl5+bjXLsbY/AGeCZfg7jeiwT1MMamMxIhGogTcblR2PjTnEi7CtztLObZLPrkZacyePIphfdpW9+Ecah5ZOAHjEKPW9Xvfc4tEIoHf72fcuHGQiOGKlNelT2tfDwYCZHy3AW31y7hPGo97VMOmdUlNp7Q6AqZJ0K0gO7/u3GNfvttdzt/eWMOmrcU8cN0MenXKZdJw+z4ZtYeaAb+HwvQM51DzCMHZRB5CNCf93rlzJ2PHjmXRokV1Tt/i8detDGrVnPmJcrTF/4er9wA8U2bXO/sQqmMJ9lSG8SiFHx0ycurOPeoTiSX42xtr+OljC/i2uJSrpoyme352o/c1R32lZn5WiMIsJ1gcSbTFD6MBjh9G6iktLUXTtEZqTrfbzdChQ+nXtw8qXGppLeoFlFgsSie/wnzhYVROId6Zt9Q1CTJMg/JwlGTSIOjzQDIGoSxoovfp2i+28pdFq6mKxDl7WD8umzCczFCg0fuaQ9MNNMMgFHAONY9UWvXDUErdC+wEnmGvTZ9TfJZiIpEIpaWlTXYrKygo4K+PP46qLrV6oNaz+o/FYqR7FN7X/owA3ot/hgpaASee1CgLR3ChCPq9SCJuBYrAPpWuusHTSz9k8dpNHNcln7tnTebYLvabFNU/1OyU7RxqHsnY+RUwSUT+LCLVIlIlIo8BP0jF4EqpHkqp5UqpL5RSm5RSt7R+1ZGHpmkUFRU1kn5v3LiRG264gYryMsvXQkzw7DXMMXQDDJ3sVfORyhJ8F9yKK6cTIkJlNEZpVQSfy23VgGgJ8HohPbtBRmRPRZi7/r6IxWs3ce7IAfz2qmltChYJTSeuaWSHgnRyMiBHPHb+dQ2l1OXAc1hblEsBI0Xj68BtIvJxTcn8eqXUUhHZnKL7H/Lous7333/fSPoNsGnTJtavX49EqiHkrysog5qzgliUrhuXIt99gXf6f+Dq2Q/dMCiviqKZ1hZEKQWGbu0tM3IbZUS8HhexZJI7Lj6LUSf2tj1v51Dz6MSOa3hv4CH2GuasBn4iIltTPhmlXgEeaa647UgzATYMgx07dqDrOsFgYxUmQKKylKCeQPZRaUajMfK+WYtv7Wu4T52B9/QLiMUTlEVieJTCVyvvNk1ET0BWYd3qRNMNlqz/ksnDT8TtdmGYpu3zhvpKzZyMoKPUPALZ32bMANQEhvNSPal9qQlMQ4G1+zx/DXANQM+ebdMAHMqYpklRURGappGW1vAAcuXKlaSlpTFi8ACCRhLxNTx41DSNtO8341v7Gq7+o3GdNoPycJRoPInf69774RdBtDhk5DXYynzyrx387Y01dM7JZFjfHraDhaPUdLCzwugOPIy1whCsFcYtIrIjZZOwGiWtBP5bRBY0974jZYVRmz6NRqONMiIiwrRp0zANgzfm/R/4Ag22ESJC8ruvKVw+F9WpF1xyB+VRDVOEoK+hPb8kY5CWBUHrkLOkMkx+lvX3b4pKOK6rvbOK+oeauRkh55ziCKddKwwsH89ngQtrHs+qeW7/etc1npwXeAmrOVKzweJIQUTYtWsXkUikyaIypRTz5z1D5Y5vrTOLfc4c4ruL6PTec6hQFtq0G6gMJ/G6XY3MbSQZB38IAqG6LMjS9V/y+2tn0L0g23awcJSaDvWxs6YsEJEnRUSv+ZoLFKRi8BqDnr8BX4jIH1Jxz0OZ2mBRXV3dZLD49NNPMbQkmWj06NGjQfoUIFldSf7qZ1GGRnTqDVSKh4DX0/jAUUtYoqxQJjvLq/nF315j8dpNTBx+Ip1y7GXEDdMkGk/i8bjokptFZijgBAsHWwGjRCk1SynlrvmaBaSqTeJY4ApgglLq05qvc1J070OK2l4iFRUVTQaL7777jhkzZvCH3/0vltN3wxWDoWlkrJqPu2I31Wf/mHhmAWn+Jmz2dQ1BQUYOa77Yxu2Pv8zO8ip+fvFZzJk8qtVsRn2lZp6j1HTYBztbkjnAI8Afax6/V/NcuxGR1VhisCOekpISysvLGxWU1dKje3ce+M1/MX7UiCYl2973XyZQvIXwaZegeg0g0JT9nWkgpo4WyuOpN9fy+oeb6dOtgNsumEChjZVFbao0PegnKxR0mgM5NMJOluQ7YPoBmMsRS2lpabMqTrAMfjNcwg+mnG0dcu6DfPwOGVvWEhs0AdeQM5pOY4qJaEl2Gj4eeOpNvikuYdqogcw6a4StFUJS09FNIT8rZLuFgMPRR6u/QpRS3ZVSLyuldiuldimlXqrJnDjYoLy8vK67elMf9AULFjB69Ci++OzjBsKsWrQtn5H18eskew2C0y9qJlgIkkyQ9Kfzn/OW1m1BZtvYggBE4xou5aJLbqYTLBxa5KBnSY5kKisr2blzJ5mZmc1+0Ief2JcpZ4znuL79Gki2TRHiRVspXP0cRm5XjClXNyg4q48Wj+IOpuPLyOK6aafSoyDH1uGmaQqxpEZ60E9OeppzqOnQKgc1S3IkU1VVRVFRUbMri23ffouKVNCrUz73//e9+OqZ5+q6SdXu3eSteBp8AbTzbm5yqwJQUVnNnS+u5Z0tuwAY3renrWCh6QZxTSM/M0ReZsgJFg62ONhZkiOScDhcFyz2bWkI8OXmTUw480yemvcPy16vJqCICNGERmlFBYWr5+FJxkhOu9HyrmgK3VodFOZlk5HWtLS8KWIJHVOEzrmZhILOFsTBPnYCxhzgIqwS92LgAlKUJTkSiUaj7Nixg1Ao1GSwQNfo2ymHG388m6nn7M0gm6ZQFU1QFYnRdf1CAuVFRM+aDZ17N7qFphs8s+pzSqsjuLPz+NnFZ3FKv16tzs00hUg8QdDvoXNuJj6Po9h0aBtOliSFxGIxtm/fTlpaGu4meoH+64vN5Afd5OTk8tObb6x7XtNNKiJxBKHzlytI376JquFT8Z7QWJ1bXBHmgUUf8s2uCgryc5nU09voPU2hGyZJzSAnPURGmt8pGHPYL1oNGEqpAuBqoHf994uIs8qoRzweZ/v27QSDwUZl6gDJ6gpmXXklxx97DP/4v8eBmi1IUqc6Gsfj8ZC99ROyNq+k6tjhePbx4wR4/+vvefSt9biU4v/94DROGXSCrbklklZ5e6ecDKcOxKFd2Pnf8wrwLlYT5lT5YBxRJBIJtm/fjt/vbxwsRFCJCIFklPt//Ss6FRYC1vagOpYgntTxeT0Ed39L3kevEC44Bjnrhw06lCV1g7krP+eNT/9Nn05Z3HbeWAq7tm7zb6k2Nfx+D3kZ6Y4Qy6Hd2AkYaSLy8w6fyWFKMplk+/bteL1ey/6/PmJStmMbX238nDGnnsq4sWMAKwtSEY1jmoLf58FTVULB6mdJpmUTn3g1oXq9SIrLw/x+0Vr+vbuS6UOP4fLTB+PNar1wzDBM4ppOVihAVijobEEcUoKdgLFIKXWOiLze4bM5zNA0je3bt+N2u/H59pFzmyYqUsG9v/0f3lq+irXvvEVWZiaxpEZVJIHL48LndeOOVlG48mkExZ7xV5Kbu7dD2Xtf7eDRJR/jdinunDaCEcd1gczWO5QlNR1DhMLsDIJ+e2ccDg52sOMaroBfKKUSgMbe3qpHtWu4ruts374dpRR+/z6pSUOva2F47913cdnFF5GRkUF1NEEkkcTn9eBSCm/FLgpXPoUrGWP7mMvI7Xl8g5XAjtJqeuRlcNuUYRSk+6z0qqt55WZt4ZjP66YgM+QUjTmknFYNdA4lDhUDHV3X2bFjB4ZhNLbW0zWSZTt55vmXmP3DWbjdbgxDqIjGMQwDr8eNUorAzm8oWP0sptvLdyMvJPuEIQQDAYrLw5RH4vTvno9hCqZp4DGSkFXQpHS8FsM0SSR1Qo5q06Gd7JeBjlKqn4h8qZQ6uanXReTjVE3wcMIwDL7//vumg4WWwFVdzpvvrOSe/72ffv36MWL4cCojcZRrr89m6NtPyftwAVpGHltHXEhur+MJBgKICA+9sY5wQuOhK8/CrcBlJCE9t8VgUdsPJC8z5AixHDqUls4wbsNKpz7QxGsCTOiQGR3CtOTDSTyKK1qJeLycN/Vcjj3mGI459jjKwzG8XjdupUCEzM0ryNmwjFjhMWwbNoPczt3x+gPENZ2A18NNk4fh9bhxuxSSiEEwAwKNmw7VEkvouN04QiyHA0JLjYyurvnzjAM3nUOXWh/OeDze0IdTBBULQ6yKB/46lxnTptKzRw+69TqGSFzD77W2IJgGueteJeObdYR7ncR3AyeRW1BI2IBfzV9Bz/wsbpkynG65Vh2IJBPgD0Ja00dFpinEkxqhoI/s9DTHkNfhgNDSlmRmSxceSP9NpdQ0YNrxxx9/oIZsQLM+nGKiIpWQjFNUVsnfnnkW0xSuvPJHAHUiKaUlKHjvOYLFX1PR/3SKjhtDdk4OX++J8OAb63ApuGRM/7331ZPgdlstDZvIiOiGSULTyc2wwLTgDQAAGFhJREFUfDadlKnDgaLZQ0+l1JMtXCcHQ+l5MA49a631KisrGwYL07S6kRkaeP2ICN989z3pGVl4vR7cNYeO7lgVBSufwVexk7Lh09jddSDp6Rm8ubmIlz78mmMLs7hj+ig6ZdWsWgwdMQ3rkLOJzurxpAZKUZCV3sj418EhFezXoaeIzO64KR0e1PfhbOCWZeio6jJAePrFhYjA+eedR3pmNj6vG1fNb3xv5W4rbZqIsnvcLMqze4DHx8PvbGLDd3s4a2Av/n97dx4mZ1Ulfvx7au2upZfqNQshCSREIBCSQAgSCFsMjA7gJAjIiINIQCUODjj66CgijjrAoz9+6rDJICIIIoO4AcoWZA9hSQgQIhJJCEl632qvM3+8b3eq11TovXM+z9NPd9Wtuu+p6q7T73LvuZ89aR6BzsufuRyaSUNZ72Sh6tSuKA4EKI+GbNSmGRWFzCWpAf4TmKyqp4rIwcBiVf3psEc3yvqsw5lJI231gAf1+nnkiSdJZbJ85NTTKMpbFyS4829UrbkD9frYcdKFNBeV8357hpuf3EhLPMnnl83n5LnTd/fbtehQrFdNz85Rm2WRECU2ccyMokL2aW/DqbD1Nff2JuBunOUBJqyGhobedThTCTxtjajPj4qHRDrD1VdfjQe6JYvQO69Q+dyvSUdi7Dz+fNp9xbxd38FPHttIRbSY752zlJk1Zd225yw6VOKsrp4nmXZqV9SUR7ttw5jRUMh+baWq3gPkAFQ1wwSfhNbY2MjOnTu7V8tKtLnJIsATzzzHuRdezPZdjYSLgoSL3bkfqpRsfIKqZ+4hWTmNHaesot3nzF5dNGc6y+fN5NrzTuw7WQRDziXUzvvcYjpej1Nr05KFGQsKSRjtIlKBM/YCETkaaB7WqEZRZx3OSCTiJAtVpKMFT0cr6g+QVXhn6/vs2LULj2jX+YrOy6blrzxM+7TD2LH002xpTnLbM38lWlpOqCjIZ044nEhR3uGGKprscFYoi5R1XRHJ5nLEk2mioSDVZVF8fdTWMGY0FHJI8iXgAeAAEXkKp57nimGNapTk1+H0eDzdLpuqP0hbPElHKsNppy7nH5Yv65rKLukklU/fTei9N2n+0HE0HX4KqVSGpniat+va2NkaJxrqUZNTc85yhqESZ8/CTRbpTJZUNmvl/s2YVMgeRgNwPHAMsAo4BJhwf8m96nDmskhrI6STqD/I+jfe4uR/PJP1r23A7/V0JQtPvJWaR2+hePsm6hf+I/WHLWP91gZymmPJYbO44bPLOaCmR03OXNYZmBUpdxKGmyziyTSCMDlWasnCjEmFJIxfAzWq+pqqbgAWA7cOb1gjq1cdzmwGaamHXIasJ0BjewIVL1WVlcTKdp9/8LXsYtKfbsTfvItdS85j29Qj+MHDr/Ljx14n5Q8T8Psp6jlWIptBMykoqYAiZ+yFU2szRagoQHW5LU1oxq5CDkkuBu53R1vOx7nEOmHWP+1VhzOTcsZYeLyk1MN7O3YRCoeZtt8Ubr/lpq6ToMGd71D15B0gHnacdCEbs1Fu+t06OlIZLjllHgfWxnpvLJNGNecMynIvnaYzWVKZLBUlISLFfS8lYMxYUUgR4BdEZDXwMJAATlHVXcMe2QjorMNZVFTkHGIk43jam8h5fbSnc+yoa+SCVatYumQJl136+a5kEdryKpXP3ksmXM6O4z/FQ+8m+PWL64mFA3x7xYc5aGp1742lk6jHCyW7B2UlUxlUnIljNmrTjAcDzSX5Le6VEVcI5+rIT0UEVR3XlcTz63D6/X7nsmlHK1mPn5Z4ikQ6S1lJhGMXL+boo450nqRKyRt/ofzlB0lU7c+7i8/mtrXbeHFLHYdOLuWLyxdSWV7aa1uaTIB/dwEcq7VpxquB/q1dO2JRjLBudTh9PqSjBUm0kxI/Te1JkskE6VSKovIyrrjsi86Tcjli635H9K3naJ82lw1zTuOGP2/i/ZY4Hz10MmcdcyjRaKT7hlSdKyHBYveyqadrhXSrtWnGo4HmkjwxkoGMlG51OH0+p5ReKkl7zkNLIo7P6+UbV32bt/76V3595x0EAgEkk3Ium257g+Y5S2g47BR+/MA64ukslyyZxaI50/tIFjnnSkhxpOtKiFPoJmeXTM24NdAhyV9U9di82p5dTYzTmp7d6nD6fUhbI7l0kraM0JFKd00cO/eslfxtyxYCgQCeeBvVa24n0PgedfM/Suuso/F4hAuOPYiAZJk5pZaSaI+1THM5Z15IuMxJGDiFbnxeYVKsxK6CmHFroD2MY93ve17ZdxzorMOpqhT7fUhLHZmM0pRScrkcQb+XNzZt4kMHHcTCBfNZuGA+vpZdVD/+M7yJNrYfcw7feS3D7I4tnHHEdKpDXsrKKigt6ZE3sxlnxmk0BsGQu0J6ikhx0ArdmHGv379eEYkN9DWSQQ5WZx3OTCZDsc+DtNaTSOeoj6dRgYDfyy/uvpuzzvsUr7/5JgDBXVuo/dONeDIpdpz0GdLTDmFaRYQpZSE6OjooiUYpK+1xgjOTRnMZKK2EYIhsNkc8laY8EiYWDVuyMOPeQCc9X2T3MgM9KTBzWCIaYt3qcHqBljra0tCWznaV+wc482MfwyMe5syeTejvG6h85ldkwmXcN/kj7OeJMQU4+6gDiHd0EAqFicVi3U9YZlKo4ha+8ZNMZ1CF2vISW57QTBgDHZLMGMlAhkNXHc54nIgPsi3NNKWVbFadcxgiPPnU0xx91FGEw2HOPWsl0TefovylB4lXTOVa3yKe3lDPKbkiViycSTwep6i4mIqK7slCU0nw+SAaQ8VDPJkm4PNSWRq2iWNmQhnVfWQRuVVEdorIhqHuu6sOZ1srEcmSbmmiPpEjhxAIOMli01ubWXXpam6/807I5Shf93tiL/2R+pqDuLTtCJ7d1s7KhTP5pwUzSCaSBINBqiorneHjndtJxSEQgGiMHE6yiBTbLFMzMY32vvJtwI+A24ey087Sei1NTUQlQ1tLO61pIRBwy/27Zs86kOuvu5bjFx1J1VN3Etr6Om/WHsEV79VSHIB/WzaXA2tKSSaSeLye7slC1UkWRREIl5LO5khn07Y2iJnQRjVhqOoaEZk+1P02NDTQ1FBPWFM0t7aTUC9FAW/XYcTTzz5LbU0tM2dMZ9niI6le83MC9Vv5U8Uirnu3nNk1JVx43BxKiwOkkilEhJrqameuCThjLJIJiJRCUYREOosI1JTbEG8zsRVS07OvKyKtqpoehnj62v5FwEUA06ZNK+g5ibZWfB3NNKZzqNdPkX/3oUEqnebK73yX6dOmcev3vulcNu1o4cbQYu5/v4RTDp7CmfNn4PUI6VQKVaW2trZrOju5LJpOOucrgsVdhXljJXbJ1Ex8hfw7XAfsBzTiXDEpA7aLyE7gs6r64jDGh6reBNwEzjIDhTwn0dJAQ3uC4nAUX481RgN+Pzf/+EfUZFqoffhGEHh0zsd5cGM7q46fzfz9KwFnRGg2l6O2thZ/515DNoNm01BaRdbjJ5FMUxYupiRcZEO8zT6hkITxIPC/qvoQgIgsA5YD9wA/ARYNX3gfTCKVwefdvTYIwOa33+aFtS9yzlkrmSMtVK69h2QwSsOJ/8KsaAXfnp2ipNiZcp7JZMik09ROnkzA79bS7JqaXk1KhUwmS3VZlOKg1do0+45C9qEXdiYLAFV9GDhOVZ9lHFXeuuvuX3HDLT/F/8qjVP3lLnYEKriwbQHv4xSx6UwW2WyWVCpFTW0tQTdZaDqFikBpFfEseMQpzGvJwuxrCtnDaBCRfwd+6d7+BNAoIl7cSuIflIjcBSwFKkVkK/DN4Vrv5KuXX8ZVH57B5I2P0DH1YOqPOINT32uhIrI75+VyORKJBLW1tRQFnfs7p6bnwmXE0zkixUHKIyE8HjsEMfueQvYwzgWmAvcDvwGmufd5gbMGs3FVPUdVJ6mqX1WnDnWyaGhs5GtXfou2pkZqn7uXydteYU3xbN5ffDbRSJglsyd1nXtQVeLxONVVVRQXFbkVveMQLCITLiOeyRGLhohFLVmYfVchFbfqgEtFpATIqWpbXvPmYYtsCGx8/Q1efHoNtfOihOL13JCdw+uhw7k4p+QvF6SqdHR0UFlR6azM3lnRuzhK0hdCczbE2xgo7LLqXJyBVTH3dh1wvlsQeExbOnc2Z35uOb54I1dn5hE95Ej+dd7+3fYQVJX29nYqKiqcmha5HJpOouFS4p4iggGvVcUyxlXIv8wbgS+p6mMAIrIU5zLnMcMY1weWTCa57Mtf4aLlx3FS0ysk0xmuYhHHLF3M4ftVdHts52FIeXm5M03dnZqeDZeRFD/RUJDSULEdghjjKiRhhDuTBYCqPi4i4WGMaVBaWlqYkW7g5J3PU0+QH4VPYMWJR1EVLe712Hg8TklJiTNNPZNGNUs6HCPl8VJZYlWxjOmpkITxtoj8B/Bz9/Z5wN+GL6TBCb/xNNcdXcumXJTf7reMTx8zl0AfFa7iHR1EIxHKy8qQTApFiBfF8Pp9TCqNEPDZ+QpjeirkU3EB8C3gPpyRnmuAfxnOoAbDV17NS8GpvHTgMlbOndnnCMx4RwfFoRCxWAzSKXJeL/FglFBxEeVRG+JtTH8KuUrSCKwegViGRODQxYQTHha5k8Z6SiQSBIuKqIzFIJ0g5w0SD0Yoi0YoCQVtiLcxA9ibdUm6GcvrkvT3oU8kEvh8PqoqKpBMirQvSKa4hJqyKEUBG7VpzJ7sM+uSJBNJvF4v1ZUVeLIp4t4Q/kgpk8oiVujGmALtE+uSdNW0qKzAk0nTEYwSLiu3Id7G7KUJfykgnU6jqtRUVaDZLPFQGbFYuS18bMwHMKETRiaTIZPJUFtVQS6naEkFNRXlVhXLmA+o4E+OiIRVtX04gxlK2VyOXCpFbUU5WfUQqKiioqzMhngbMwh7/PSIyDEishF43b19uIj8ZNgjG6RsOk1NeQlZb5Bo7WSqY+WWLIwZpEI+QT8APgLUA6jqK8BxwxnUYAUCQSrKomhxCZVT9qOsJGrjK4wZAgUdkqjquz0+cNnhCWdolMcqSOdyVNROJmDnK4wZMoV8mt4VkWMAFZEAzqjP14c3rMGJVtfg83ptiLcxQ6yQT9TFwOeBKcBWYJ57e8SIyMdE5Kbm5uaCHh/0+y1ZGDMMRLWgyv1jwsKFC3Xt2rWjHYYxE5qIvKiqC/tqK+QqyWwReaRz/VMROUxEvj7UQRpjxr5C9ttvBr4KpAFU9VXg7OEMyhgzNhWSMEKq+nyP+zLDEYwxZmwrJGHUicgBuFPdRWQFsH1YozLGjEmFXFb9PE7R3zkisg2nPN8nhzUqY8yYNGDCcFc3u0RVT3YL/3pUtXVkQjPGjDUDVdzyqWpGRBYAjKeJZ8aY4THQHsbzwHzgJRF5APgV0JU0VPW+YY7NGDPGFHIOI4Yz8exEnBOf4n63hGHMPmaghFEtIl8CNrA7UXQaP8NDjTFDZqCE4QUidE8UnSxhGLMPGihhbFfVq0YsEmPMmDfQwC2rOGOM6WaghHHSiEVhjBkX+k0YqtowkoEYY8Y+qzJjjCmYJQxjTMFGPWGIyHIReVNENovIV0Y7HmNM/0Y1YbiT234MnAocDJwjIgePZkzGmP6Ndg3+o4DNqvo2gIj8Ejgd2DiYTpev/k6v+z5+wiIuOvNkOhJJPv7l3gvTf/LUJfzzqcdR19TKed+4vlf7haefxIqTjmbrjnou/M4NvdpXf+JUTvvwfDb9fTurr721V/uXP3U6Jy48lFfe2sK///87erVf+dmVHD13Ns+u38SVN/+qV/v3Lz2Pw2ftz6NrN/Bft/+mV/v1l1/A7GmT+MNT67j+7j/2ar/laxcztaaCex95llt+80iv9juuWk1lWZSf/3ENv/jjk73a7/uvywkVBbnpf//MfY8916v9weu/BsAP7/o9Dz7zcre2omCA+6+5AoDv/ex+Hn/xtW7tsZIId179RQC+cePdPP/a5m7tk6ti3PoflwBwxfV3sH7zlm7tB+5Xy4+u+AwAX7jmp2x+9/1u7XMP3J9rVp8HwAXf/m/e29X9fP5RhxzIVas+AcC5X/9/NLS0dWtfuuAQvnL+GQCcccU1JJKpbu3LF8/jX8/5B+fnMfy3NxRGO2FMAd7Nu70VWJT/ABG5CLgIYNq0aQV1mkz3LgjW3NbB9rpG4slU3+2tTntDS1uf7U2t7Wyva2RHY3Of7Q0tbWyva2RXf+3NTntdU0uf7fXNrWyva6S+ubXP9rqmFie+5r7j29XYTDRU1G/8Oxqb8Xo9NLW2993e0EQ6k6G5taPP9vfrmygOBmhu67t9e10jAK3t8V7tIjJgeyKV7mpviyd6tceTqa72jj7aOxJ57Ynev9+OeKKrva/ff1teeyKV7tXe2h7vak/uqX0M/+0BTKos79W+N0a1ariIrAQ+oqoXurf/GThKVS/t6/FWNdyY4TeoquHDbCuwX97tqcB7oxSLMWYPRjthvADMEpEZ7qpqZwMPjHJMxph+jOo5DLei1xeAh3Bmx96qqq/t4WnGmFEy2ic9UdU/AH8Y7TiMMXs22ockxphxxBKGMaZgljCMMQWzhGGMKdioDtzaWyKyC9gywENKgeYh2NRE7acSqBuCfsba6xpr/Yz393l/Va3qs0VVJ8wXcJP1M2A/a8dYPBO1nwn5PqvqhDsk+a31MyLG2usaa/0MlTH3usbVIYkZHBFZq/3METBDZyK/zxNtD8MM7KbRDmAfMWHfZ9vDMMYUzPYwjDEFs4RhjCmYJYwJSkRuFZGdIrIh776YiPxJRN5yvw+u/JLp732+UkS2icjL7tdpoxnjULKEMXHdBizvcd9XgEdUdRbwiHvbDM5t9H6fAX6gqvPcrwkzG9sSxgSlqmuAnqvXnQ78zP35Z8AZIxrUBNTP+zxhWcLYt9So6nYA93v1KMczkX1BRF51D1kmzKGfJQxjht5/AwcA84DtwHWjG87QsYSxb9khIpMA3O87RzmeCUlVd6hqVlVzwM046+9MCJYw9i0PAOe7P58P9F4RyQxaZ1J2nQls6O+x442N9JygROQuYCnOVOsdwDeB+4F7gGnA34GVqrrPnLAbDv28z0txDkcUeAdY1XnuaLyzhGGMKZgdkhhjCmYJwxhTMEsYxpiCWcIwxhTMEoYxpmCWMIwxBbOEMQ6JiIrIdXm3LxeRK4eo79tEZMVQ9LWH7awUkddF5LEe90/PnypuxhZLGONTEvi4iFSOdiD5RMS7Fw//DPA5VT1huOIxQ88SxviUwSk0e1nPhp57CCLS5n5fKiJPiMg9IrJJRL4nIp8UkedFZL2IHJDXzcki8qT7uI+6z/eKyDUi8oI7C3NVXr+PicidwPo+4jnH7X+DiHzfve8bwLHADSJyTX8vcoBtRkTkERFZ5/Z9unv/90Xkc3nPv1JE/s39+Yq8fr7l3hcWkd+LyCtufJ8o7O3fhw3VAif2NXJfQBtQgjPsuBS4HLjSbbsNWJH/WPf7UqAJmAQEgW3At9y2LwI/zHv+gzj/TGYBW4Ei4CLg6+5jgsBaYIbbbzswo484J+MMQa8CfMCjwBlu2+PAwj6eMx3Y4P7c3zZ9QIl7fyWwGRDgCOCJvL424gyDX4aTYMV9Xb8DjgP+Cbg57/Glo/27HetftocxTqlqC3A7sHovnvaCqm5X1STwV+Bh9/71OB/UTveoak5V3wLeBubgfOg+JSIvA88BFTgJBeB5Vf1bH9s7EnhcVXepagb4Bc4HtVD9bVOA/xSRV4E/A1Nwan28BFSLyGQRORxoVNW/u/0sA14C1rmvZ5b7uk9290yWqOpQLEs4oflGOwAzKD/E+QD8T959GdxDTRERIJDXlsz7OZd3O0f3v4WeE4wU50N6qao+lN8gIktx9jD6Int8BQPrb5ufxtlrWaCqaRF5B2cvCOBeYAVQC/wyr5/vquqNvTYgsgA4DfiuiDysqlcNMuYJzfYwxjF1Zpreg3MCsdM7wAL359MB/wfoeqWIeNzzGjOBN4GHgEtExA8gIrNFJLyHfp4DjheRSveE6DnAE3sRR3/bLAV2usniBGD/vOf8EjgbJ2ncm9fPBSIScfuZIiLVIjIZ6FDVO4Brgfl7Eds+yfYwxr/rgC/k3b4Z+I2IPI9T6Le///4DeRPng10DXKyqCRG5BeewZZ2757KLPdQEVdXtIvJV4DGc//J/UNW9qcHR3zZ/AfxWRNYCLwNv5G3zNRGJAtt0dznCh0XkQ8AzTje0AecBBwLXiEgOSAOX7EVs+ySb3m6MKZgdkhhjCmYJwxhTMEsYxpiCWcIwxhTMEoYxpmCWMIwxBbOEYYwp2P8BY/ulXiWkuesAAAAASUVORK5CYII=\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!"