├── .bumpversion.cfg ├── .editorconfig ├── .gitignore ├── .ignore ├── LICENSE ├── README.md ├── docs ├── examples │ ├── inventory.md │ └── knapsack.md ├── getting_started.md ├── index.md └── learners │ ├── optimal_trees.md │ ├── pytorch.md │ └── xgboost.md ├── examples ├── .gitignore ├── inventory.ipynb └── knapsack.ipynb ├── mkdocs.yml ├── mlopt ├── __init__.py ├── error.py ├── filter.py ├── kkt.py ├── learners │ ├── __init__.py │ ├── learner.py │ ├── optimal_tree │ │ ├── __init__.py │ │ ├── optimal_tree.py │ │ └── settings.py │ ├── pytorch │ │ ├── __init__.py │ │ ├── lightning.py │ │ ├── pytorch.py │ │ ├── settings.py │ │ └── utils.py │ └── xgboost │ │ ├── settings.py │ │ └── xgboost.py ├── optimizer.py ├── problem.py ├── sampling.py ├── settings.py ├── strategy.py ├── tests │ ├── __init__.py │ ├── data │ │ └── afiro.mat │ ├── settings.py │ ├── test_caching.py │ ├── test_filter.py │ ├── test_kkt_solver.py │ ├── test_matrix_parameters.py │ ├── test_parallel.py │ ├── test_parameters.py │ ├── test_problem.py │ ├── test_sampling.py │ ├── test_save.py │ └── test_solve_strategy.py └── utils.py ├── pytest.ini ├── requirements.txt └── setup.py /.bumpversion.cfg: -------------------------------------------------------------------------------- 1 | [bumpversion] 2 | current_version = 0.0.2 3 | commit = True 4 | tag = True 5 | 6 | [bumpversion:file:setup.py] 7 | 8 | [bumpversion:file:mlopt/__init__.py] 9 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*.jl] 4 | indent_style = space 5 | indent_size = 4 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | output_bak/ 2 | 3 | output.txt 4 | output/ 5 | .DS_Store 6 | .ropeproject 7 | tags 8 | 9 | 10 | 11 | # Python 12 | Pipfile.lock 13 | # Byte-compiled / optimized / DLL files 14 | __pycache__/ 15 | *.py[cod] 16 | *$py.class 17 | 18 | # C extensions 19 | *.so 20 | 21 | # Distribution / packaging 22 | .Python 23 | build/ 24 | develop-eggs/ 25 | dist/ 26 | downloads/ 27 | eggs/ 28 | .eggs/ 29 | lib/ 30 | lib64/ 31 | parts/ 32 | sdist/ 33 | var/ 34 | wheels/ 35 | *.egg-info/ 36 | .installed.cfg 37 | *.egg 38 | MANIFEST 39 | 40 | # PyInstaller 41 | # Usually these files are written by a python script from a template 42 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 43 | *.manifest 44 | *.spec 45 | 46 | # Installer logs 47 | pip-log.txt 48 | pip-delete-this-directory.txt 49 | 50 | # Unit test / coverage reports 51 | htmlcov/ 52 | .tox/ 53 | .nox/ 54 | .coverage 55 | .coverage.* 56 | .cache 57 | nosetests.xml 58 | coverage.xml 59 | *.cover 60 | .hypothesis/ 61 | .pytest_cache/ 62 | 63 | # Translations 64 | *.mo 65 | *.pot 66 | 67 | # Django stuff: 68 | *.log 69 | local_settings.py 70 | db.sqlite3 71 | 72 | # Flask stuff: 73 | instance/ 74 | .webassets-cache 75 | 76 | # Scrapy stuff: 77 | .scrapy 78 | 79 | # Sphinx documentation 80 | docs/_build/ 81 | 82 | # PyBuilder 83 | target/ 84 | 85 | # Jupyter Notebook 86 | .ipynb_checkpoints 87 | 88 | # IPython 89 | profile_default/ 90 | ipython_config.py 91 | 92 | # pyenv 93 | .python-version 94 | 95 | # celery beat schedule file 96 | celerybeat-schedule 97 | 98 | # SageMath parsed files 99 | *.sage.py 100 | 101 | # Environments 102 | .env 103 | .venv 104 | env/ 105 | venv/ 106 | ENV/ 107 | env.bak/ 108 | venv.bak/ 109 | 110 | # Spyder project settings 111 | .spyderproject 112 | .spyproject 113 | 114 | # Rope project settings 115 | .ropeproject 116 | 117 | # mkdocs documentation 118 | /site 119 | 120 | # mypy 121 | .mypy_cache/ 122 | .dmypy.json 123 | dmypy.json 124 | *.pkl 125 | .ray_tmp/ 126 | *.*~ 127 | 128 | # Emacs 129 | *~ 130 | \#*\# 131 | /.emacs.desktop 132 | /.emacs.desktop.lock 133 | *.elc 134 | auto-save-list 135 | tramp 136 | .\#* -------------------------------------------------------------------------------- /.ignore: -------------------------------------------------------------------------------- 1 | # Ignore files for ag searcher 2 | *.mps 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2018 Bartolomeo Stellato, Dimitris Bertsimas 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Machine Learning Optimizer 2 | 3 | `mlopt` is a package to learn how to solve numerical optimization problems from data. It relies on [cvxpy](https://cvxpy.org) for modeling and [gurobi](https://www.gurobi.com/) for solving the problem offline. 4 | 5 | `mlopt` learns how to solve programs using [pytorch](https://pytorch.org/) ([pytorch-lightning](https://github.com/PyTorchLightning/pytorch-lightning)), [xgboost](https://xgboost.readthedocs.io/en/latest/) or [optimaltrees](https://docs.interpretable.ai/stable). The machine learning hyperparameter optimization is performed using [optuna](https://optuna.org/). 6 | 7 | Online, `mlopt` only requires to predict the strategy and solve a linear system using [scikit-umfpack](https://github.com/scikit-umfpack/scikit-umfpack). 8 | 9 | ## Examples 10 | 11 | To see `mlopt` in action, have a look at the notebooks in the [examples/](./examples/) folder. 12 | 13 | ## Documentation 14 | 15 | Coming soon at [mlopt.org](https://mlopt.org)! 16 | 17 | ## Citing 18 | 19 | If you use `mlopt` for research, please cite the following papers: 20 | 21 | * [The Voice of Optimization](https://arxiv.org/pdf/1812.09991.pdf): 22 | 23 | ``` 24 | @Article{bertsimas2021, 25 | author = {{Bertsimas}, D. and {Stellato}, B.}, 26 | title = {The Voice of Optimization}, 27 | journal = {Machine Learning}, 28 | year = {2021}, 29 | month = {2}, 30 | volume = {110}, 31 | issue = {2}, 32 | pages = {249--277}, 33 | } 34 | ``` 35 | 36 | * [Online Mixed-Integer Optimization in Milliseconds](https://arxiv.org/pdf/1907.02206.pdf) 37 | 38 | ``` 39 | @article{stellato2019a, 40 | author = {{Bertsimas}, D. and {Stellato}, B.}, 41 | title = {Online Mixed-Integer Optimization in Milliseconds}, 42 | journal = {arXiv e-prints}, 43 | year = {2019}, 44 | month = jul, 45 | adsnote = {Provided by the SAO/NASA Astrophysics Data System}, 46 | adsurl = {https://ui.adsabs.harvard.edu/abs/2019arXiv190702206B}, 47 | archiveprefix = {arXiv}, 48 | eprint = {1907.02206}, 49 | keywords = {Mathematics - Optimization and Control}, 50 | pdf = {https://arxiv.org/pdf/1907.02206.pdf}, 51 | primaryclass = {math.OC}, 52 | } 53 | 54 | ``` 55 | 56 | 57 | The code to **reproduce the results in the papers** is available at [bstellato/mlopt_benchmarks](https://github.com/bstellato/mlopt_benchmarks). 58 | 59 | 60 | ## Projects using mlopt framework 61 | 62 | 63 | * [Learning Mixed-Integer Convex Optimization Strategies for Robot Planning and Control](https://arxiv.org/pdf/2004.03736.pdf) 64 | 65 | -------------------------------------------------------------------------------- /docs/examples/inventory.md: -------------------------------------------------------------------------------- 1 | # Inventory management example 2 | 3 | 4 | ```python 5 | import matplotlib.pyplot as plt 6 | from mlopt.sampling import uniform_sphere_sample 7 | import mlopt 8 | import pandas as pd 9 | import cvxpy as cp 10 | import numpy as np 11 | import os 12 | 13 | np.random.seed(0) 14 | ``` 15 | 16 | # Generate Optimizer 17 | 18 | 19 | ```python 20 | np.random.seed(1) 21 | T = 30 22 | M = 3. 23 | h = 1. 24 | c = 2. 25 | p = 3. 26 | 27 | # Define problem 28 | x = cp.Variable(T+1) 29 | u = cp.Variable(T) 30 | 31 | # Define parameters 32 | d = cp.Parameter(T, nonneg=True, name="d") 33 | x_init = cp.Parameter(1, name="x_init") 34 | 35 | # Constaints 36 | constraints = [x[0] == x_init] 37 | for t in range(T): 38 | constraints += [x[t+1] == x[t] + u[t] - d[t]] 39 | constraints += [u >= 0, u <= M] 40 | 41 | # Objective 42 | cost = cp.sum(cp.maximum(h * x, -p * x)) + c * cp.sum(u) 43 | 44 | # Define optimizer 45 | m = mlopt.Optimizer(cp.Minimize(cost), constraints) 46 | ``` 47 | 48 | # Sample points 49 | 50 | 51 | ```python 52 | # Average request 53 | theta_bar = np.concatenate(( 2 * np.ones(T), # d 54 | [10] # x_init 55 | )) 56 | radius = 1 57 | 58 | 59 | def sample_inventory(theta_bar, radius, n=100): 60 | 61 | # Sample points from multivariate ball 62 | X_d = uniform_sphere_sample(theta_bar[:-1], radius, n=n) 63 | X_x_init = uniform_sphere_sample([theta_bar[-1]], 3 * radius, 64 | n=n) 65 | 66 | df = pd.DataFrame({'d': X_d.tolist(), 67 | 'x_init': X_x_init.tolist()}) 68 | 69 | return df 70 | ``` 71 | 72 | # Train 73 | 74 | 75 | ```python 76 | # Training and testing data 77 | n_train = 1000 78 | n_test = 100 79 | theta_train = sample_inventory(theta_bar, radius, n=n_train) 80 | theta_test = sample_inventory(theta_bar, radius, n=n_test) 81 | 82 | # Train solver 83 | m.train(theta_train, 84 | learner=mlopt.OPTIMAL_TREE, 85 | n_best=3, 86 | max_depth=[1, 5, 10], 87 | minbucket=[1, 5, 10], 88 | parallel_trees=True, 89 | save_svg=True) 90 | # m.train(theta_train, learner=mlopt.PYTORCH) 91 | ``` 92 | 93 | 94 | ```python 95 | m._learner._lnr 96 | ``` 97 | 98 | 99 | ```python 100 | output = "oct_inv" 101 | 102 | # Save solver 103 | m.save(output, delete_existing=True) 104 | 105 | # Benchmark 106 | results_general, results_detail = m.performance(theta_test) 107 | results_general.to_csv(output + "_general.csv", header=True) 108 | results_detail.to_csv(output + "_detail.csv", header=True) 109 | 110 | results_general 111 | ``` 112 | 113 | # Plot behavior 114 | 115 | 116 | ```python 117 | # Solve with single value of theta 118 | theta_plot = sample_inventory(theta_bar, radius, n=1) 119 | 120 | # Get optimal solution 121 | result_plot = m.solve(theta_plot) 122 | 123 | t = np.arange(0, T, 1) 124 | fig, ax = plt.subplots(3, 1) 125 | ax[0].step(t, x.value[:-1], where="post") 126 | ax[0].set_ylabel('x') 127 | ax[1].step(t, u.value, where="post") 128 | ax[1].set_ylabel('u') 129 | ax[2].step(t, theta_plot['d'][0], where="post") 130 | ax[2].set_ylabel('d') 131 | plt.show() 132 | ``` 133 | 134 | 135 | ```python 136 | # Store values for plotting 137 | df_plot = pd.DataFrame({'t': t, 138 | 'x': x.value[:-1], 139 | 'u': u.value, 140 | 'd': theta_plot['d'][0]}) 141 | df_plot.to_csv(output + "_plot.csv") 142 | ``` 143 | -------------------------------------------------------------------------------- /docs/examples/knapsack.md: -------------------------------------------------------------------------------- 1 | # MLOPT Knapsack Example 2 | 3 | 4 | ```python 5 | import numpy as np 6 | import cvxpy as cp 7 | import pandas as pd 8 | import logging 9 | 10 | import mlopt 11 | from mlopt.sampling import uniform_sphere_sample 12 | from mlopt.learners.pytorch.pytorch import PytorchNeuralNet 13 | from mlopt.utils import n_features, pandas2array 14 | ``` 15 | 16 | ## Generate problem data 17 | 18 | 19 | ```python 20 | np.random.seed(1) # Reset random seed for reproducibility 21 | 22 | # Variable 23 | n = 10 24 | x = cp.Variable(n, integer=True) 25 | 26 | # Cost 27 | c = np.random.rand(n) 28 | 29 | # Weights 30 | a = cp.Parameter(n, nonneg=True, name='a') 31 | x_u = cp.Parameter(n, nonneg=True, name='x_u') 32 | b = 0.5 * n 33 | ``` 34 | 35 | ## Create optimizer object 36 | 37 | 38 | ```python 39 | # Problem 40 | cost = - c * x 41 | constraints = [a * x <= b, 42 | 0 <= x, x <= x_u] 43 | 44 | 45 | # Define optimizer 46 | # If you just want to remove too many messages 47 | # change INFO to WARNING 48 | m = mlopt.Optimizer(cp.Minimize(cost), constraints, 49 | log_level=logging.INFO) 50 | ``` 51 | 52 | ## Define training and testing parameters 53 | 54 | 55 | ```python 56 | # Average request 57 | theta_bar = 2 * np.ones(2 * n) 58 | radius = 1.0 59 | 60 | 61 | def sample(theta_bar, radius, n=100): 62 | 63 | # Sample points from multivariate ball 64 | ndim = int(len(theta_bar)/2) 65 | X_a = uniform_sphere_sample(theta_bar[:ndim], radius, n=n) 66 | X_u = uniform_sphere_sample(theta_bar[ndim:], radius, n=n) 67 | 68 | df = pd.DataFrame({ 69 | 'a': list(X_a), 70 | 'x_u': list(X_u) 71 | }) 72 | 73 | return df 74 | 75 | 76 | # Training and testing data 77 | n_train = 1000 78 | n_test = 100 79 | theta_train = sample(theta_bar, radius, n=n_train) 80 | theta_test = sample(theta_bar, radius, n=n_test) 81 | ``` 82 | 83 | ## Train predictor (Pytorch) 84 | 85 | 86 | ```python 87 | # Dictionary of different parameters. 88 | # The cross validation will try all of the possible 89 | # combinations 90 | params = { 91 | 'learning_rate': [0.001, 0.01, 0.1], 92 | 'batch_size': [32], 93 | 'n_epochs': [10] 94 | } 95 | m.train(theta_train, learner=mlopt.PYTORCH, params=params) 96 | ``` 97 | 98 | ## Benchmark on testing dataset 99 | 100 | 101 | ```python 102 | results = m.performance(theta_test) 103 | print("Accuracy: %.2f " % results[0]['accuracy']) 104 | ``` 105 | 106 | ## Save training data 107 | 108 | 109 | ```python 110 | m.save_training_data("training_data.pkl", delete_existing=True) 111 | ``` 112 | 113 | ## Create new solver and train passing loaded data 114 | 115 | 116 | ```python 117 | m = mlopt.Optimizer(cp.Minimize(cost), constraints) 118 | m.load_training_data("training_data.pkl") 119 | m.train(learner=mlopt.PYTORCH, params=params) # Train after loading samples 120 | 121 | results = m.performance(theta_test) 122 | print("Accuracy: %.2f " % results[0]['accuracy']) 123 | ``` 124 | 125 | ## Predict single point 126 | 127 | 128 | ```python 129 | # Predict single point 130 | theta = theta_test.iloc[0] 131 | root = logging.getLogger('mlopt') 132 | root.setLevel(logging.DEBUG) 133 | result_single_point = m.solve(theta) 134 | print(result_single_point) 135 | ``` 136 | 137 | ## Learn directly from points (talk directly to pytorch) 138 | 139 | 140 | ```python 141 | y = m.y_train 142 | X = m.X_train 143 | learner = PytorchNeuralNet(n_input=n_features(X), 144 | n_classes=len(np.unique(y)), 145 | n_best=3, 146 | params=params) 147 | # Train learner 148 | learner.train(pandas2array(X), y) 149 | 150 | # Predict 151 | X_pred = X.iloc[0] 152 | y_pred = learner.predict(pandas2array(X_pred)) # n_best most likely classes 153 | ``` 154 | 155 | 156 | ```python 157 | 158 | ``` 159 | -------------------------------------------------------------------------------- /docs/getting_started.md: -------------------------------------------------------------------------------- 1 | # Getting started 2 | 3 | You can directly install `mlopt` using pip 4 | 5 | ```bash 6 | pip install mlopt 7 | ``` 8 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | # Machine Learning Optimizer 2 | 3 | Welcome to the Machine Learning Optimizer documentation. 4 | -------------------------------------------------------------------------------- /docs/learners/optimal_trees.md: -------------------------------------------------------------------------------- 1 | # OptimalTrees 2 | 3 | `OptimalTrees` is a commercial machine learning package from [Interpretable AI](https://docs.interpretable.ai). 4 | They feature free Academic licenses. 5 | 6 | ## Installation 7 | 8 | `OptimalTrees` is written in Julia and can be called by `mlopt` using [pyjulia](https://pyjulia.readthedocs.io/). To install it you need to 9 | 10 | 1. Install Julia version of OptimalTrees. Instructions are available [here](https://docs.interpretable.ai/stable/installation/). 11 | 12 | 2. Install python interface. Instructions are available [here](https://docs.interpretable.ai/stable/IAI-Python/installation/). Note [this comment](https://docs.interpretable.ai/stable/IAI-Python/installation/#Python-distribution-is-incompatible-with-PyJulia-1) when using Anaconda version of python. If this is the case for you, please set the environment variable `IAI_DISABLE_COMPILED_MODULES` to `True` as explained at the link. 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /docs/learners/pytorch.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bstellato/mlopt/48cb9f8b004b648d0c6bbfb586623a6696ebbcca/docs/learners/pytorch.md -------------------------------------------------------------------------------- /docs/learners/xgboost.md: -------------------------------------------------------------------------------- 1 | # XGBoost 2 | -------------------------------------------------------------------------------- /examples/.gitignore: -------------------------------------------------------------------------------- 1 | *.md 2 | *.tar.gz 3 | *.csv 4 | -------------------------------------------------------------------------------- /mkdocs.yml: -------------------------------------------------------------------------------- 1 | site_name: "mlopt" 2 | site_description: 'Machine Learning Optimizer' 3 | site_author: "Bartolomeo Stellato" 4 | 5 | nav: 6 | - Home: index.md 7 | 8 | - Getting started: getting_started.md 9 | 10 | - Manual: 11 | - Learners: 12 | - XGBoost: learners/xgboost.md 13 | - OptimalTreees: learners/optimal_trees.md 14 | - Pytorch: learners/pytorch.md 15 | 16 | - Examples: 17 | - Knapsack: examples/knapsack.md 18 | - Inventory: examples/inventory.md 19 | 20 | theme: 21 | name: 'material' 22 | palette: 23 | primary: 'blue' 24 | accent: 'light blue' 25 | 26 | 27 | 28 | repo_name: 'bstellato/mlopt' 29 | repo_url: 'https://github.com/bstellato/mlopt' 30 | 31 | 32 | extra: 33 | social: 34 | - type: 'github' 35 | link: 'https://github.com/bstellato' 36 | 37 | 38 | 39 | copyright: 'Copyright © 2020 Bartolomeo Stellato' 40 | 41 | -------------------------------------------------------------------------------- /mlopt/__init__.py: -------------------------------------------------------------------------------- 1 | from mlopt.optimizer import Optimizer 2 | from mlopt.settings import PYTORCH, OPTIMAL_TREE, XGBOOST 3 | from mlopt.learners import installed_learners 4 | 5 | 6 | __version__ = '0.0.2' 7 | -------------------------------------------------------------------------------- /mlopt/error.py: -------------------------------------------------------------------------------- 1 | import mlopt.settings as stg 2 | 3 | 4 | def value_error(err, error_type=ValueError): 5 | stg.logger.error(err) 6 | raise error_type(err) 7 | 8 | 9 | def warning(message): 10 | stg.logger.warning(message) 11 | -------------------------------------------------------------------------------- /mlopt/filter.py: -------------------------------------------------------------------------------- 1 | from joblib import Parallel, delayed 2 | import mlopt.settings as stg 3 | import numpy as np 4 | import mlopt.utils as u 5 | from tqdm.auto import tqdm 6 | 7 | 8 | def best_strategy(theta, obj_train, encoding, problem): 9 | """Compute best strategy between the ones in encoding.""" 10 | 11 | problem.populate(theta) # Populate parameters 12 | 13 | # Serial solution over the strategies 14 | results = [problem.solve(strategy=strategy) for strategy in encoding] 15 | 16 | # Compute cost degradation 17 | degradation = [] 18 | for r in results: 19 | 20 | cost = r['cost'] 21 | 22 | if r['infeasibility'] > stg.INFEAS_TOL: 23 | cost = np.inf 24 | 25 | diff = np.abs(cost - obj_train) 26 | if np.abs(obj_train) > stg.DIVISION_TOL: # Normalize in case 27 | diff /= np.abs(obj_train) 28 | degradation.append(diff) 29 | 30 | # Find minimum one 31 | best_strategy = np.argmin(degradation) 32 | # if degradation[best_strategy] > stg.FILTER_SUBOPT: 33 | # stg.logger.warning("Sample assigned to strategy more " + 34 | # "than %.2e suboptimal." % stg.FILTER_SUBOPT) 35 | 36 | return best_strategy, degradation[best_strategy] 37 | 38 | 39 | class Filter(object): 40 | """Strategy filter.""" 41 | 42 | def __init__(self, 43 | X_train=None, 44 | y_train=None, 45 | obj_train=None, 46 | encoding=None, 47 | problem=None): 48 | """Initialize strategy condenser.""" 49 | self.X_train = X_train 50 | self.y_train = y_train 51 | self.encoding = encoding 52 | self.obj_train = obj_train 53 | self.problem = problem 54 | 55 | def assign_samples(self, discarded_samples, selected_strategies, 56 | batch_size, parallel=True): 57 | """ 58 | Assign samples to strategies choosing the ones minimizing the cost. 59 | """ 60 | 61 | # Backup strategies labels and encodings 62 | # self.y_full = self.y_train 63 | 64 | # Reassign y_labels 65 | # selected_strategies: find index where new labels are 66 | # discarded_strategies: -1 67 | self.y_train = np.array([np.where(selected_strategies == label)[0][0] 68 | if label in selected_strategies 69 | else -1 70 | for label in self.y_train]) 71 | 72 | # Assign discarded samples and compute degradation 73 | degradation = np.zeros(len(discarded_samples)) 74 | 75 | n_jobs = u.get_n_processes() if parallel else 1 76 | stg.logger.info("Assign samples to selected strategies (n_jobs = %d)" 77 | % n_jobs) 78 | 79 | results = Parallel(n_jobs=n_jobs, batch_size=batch_size)( 80 | delayed(best_strategy)(self.X_train.iloc[i], self.obj_train[i], 81 | self.encoding, self.problem) 82 | for i in tqdm(range(len(discarded_samples))) 83 | ) 84 | 85 | for i in range(len(discarded_samples)): 86 | sample_idx = discarded_samples[i] 87 | self.y_train[sample_idx], degradation[i] = results[i] 88 | 89 | return degradation 90 | 91 | def select_strategies(self, samples_fraction): 92 | """Select the most frequent strategies depending on the counts""" 93 | 94 | n_samples = len(self.X_train) 95 | n_strategies = len(self.encoding) 96 | n_samples_selected = int(samples_fraction * n_samples) 97 | 98 | stg.logger.info("Selecting most frequent strategies") 99 | 100 | # Select strategies with high frequency counts 101 | strategies, y_counts = np.unique(self.y_train, return_counts=True) 102 | assert n_strategies == len(strategies) # Sanity check 103 | 104 | # Sort from largest to smallest counts and pick 105 | # only the first ones covering up to samples_fraction samples 106 | idx_sort = np.argsort(y_counts)[::-1] 107 | selected_strategies = [] 108 | n_temp = 0 109 | for idx in idx_sort: 110 | n_temp += y_counts[idx] # count selected samples 111 | selected_strategies.append(strategies[idx]) 112 | if n_temp > n_samples_selected: 113 | break 114 | 115 | stg.logger.info("Selected %d strategies" % len(selected_strategies)) 116 | 117 | return selected_strategies 118 | 119 | def filter(self, 120 | samples_fraction=stg.FILTER_STRATEGIES_SAMPLES_FRACTION, 121 | max_iter=stg.FILTER_MAX_ITER, 122 | batch_size=stg.JOBLIB_BATCH_SIZE, 123 | parallel=True): 124 | """Filter strategies.""" 125 | n_samples = len(self.X_train) 126 | 127 | # Backup strategies labels and encodings 128 | self.y_full = self.y_train 129 | self.encoding_full = self.encoding 130 | 131 | degradation = [np.inf] 132 | for k in range(max_iter): 133 | 134 | selected_strategies = \ 135 | self.select_strategies(samples_fraction=samples_fraction) 136 | 137 | # Reassign encodings and labels 138 | self.encoding = [self.encoding[i] for i in selected_strategies] 139 | 140 | # Find discarded samples 141 | discarded_samples = np.array([i for i in range(n_samples) 142 | if self.y_train[i] 143 | not in selected_strategies]) 144 | 145 | stg.logger.info("Samples fraction at least %.3f %%" % (100 * samples_fraction)) 146 | stg.logger.info("Discarded strategies for %d samples (%.2f %%)" % 147 | (len(discarded_samples), 148 | (100 * len(discarded_samples) / n_samples))) 149 | 150 | # Reassign discarded samples to selected strategies 151 | degradation = self.assign_samples(discarded_samples, 152 | selected_strategies, 153 | batch_size=batch_size, 154 | parallel=parallel) 155 | 156 | if len(degradation) > 0: 157 | stg.logger.info("\nAverage cost degradation = %.2e %%" % 158 | (100 * np.mean(degradation))) 159 | stg.logger.info("Max cost degradation = %.2e %%" % 160 | (100 * np.max(degradation))) 161 | 162 | if np.mean(degradation) > stg.FILTER_SUBOPT: 163 | samples_fraction = 1 - (1 - samples_fraction)/2 164 | 165 | stg.logger.info("Mean degradation too high, " 166 | "trying samples_fraction = %.4f " % samples_fraction) 167 | self.y_train = self.y_full 168 | self.encoding = self.encoding_full 169 | else: 170 | stg.logger.info("Acceptable degradation found") 171 | break 172 | else: 173 | stg.logger.info("No more discarded points.") 174 | break 175 | 176 | if k == max_iter - 1: 177 | self.y_train = self.y_full 178 | self.encoding = self.encoding_full 179 | stg.logger.warning("No feasible filtering found.") 180 | 181 | return self.y_train, self.encoding 182 | -------------------------------------------------------------------------------- /mlopt/kkt.py: -------------------------------------------------------------------------------- 1 | # Define and solve equality constrained QP 2 | from cvxpy.reductions.solvers.qp_solvers.qp_solver import QpSolver 3 | import cvxpy.settings as cps 4 | import cvxpy.interface as intf 5 | import cvxpy.settings as s 6 | import scipy.sparse as spa 7 | from cvxpy.reductions import Solution 8 | from cvxpy.constraints import Zero 9 | import numpy as np 10 | 11 | # from pypardiso import spsolve 12 | # from pypardiso.pardiso_wrapper import PyPardisoError 13 | from scipy.sparse.linalg import spsolve 14 | from scipy.sparse.linalg import factorized 15 | from scikits.umfpack import UmfpackWarning 16 | import time 17 | import warnings 18 | import mlopt.settings as stg 19 | 20 | KKT = "KKT" 21 | 22 | 23 | class CatchSingularMatrixWarnings(object): 24 | 25 | def __init__(self): 26 | self.catcher = warnings.catch_warnings() 27 | 28 | def __enter__(self): 29 | self.catcher.__enter__() 30 | warnings.simplefilter("ignore", UmfpackWarning) 31 | 32 | warnings.filterwarnings( 33 | "ignore", 34 | message="divide by zero encountered in double_scalars" 35 | ) 36 | 37 | def __exit__(self, *args): 38 | self.catcher.__exit__() 39 | 40 | 41 | def create_kkt_matrix(data): 42 | """Create KKT matrix from data.""" 43 | A_con = data[cps.A + "_red"] 44 | n_con = A_con.shape[0] 45 | O_con = spa.csc_matrix((n_con, n_con)) 46 | 47 | # Create KKT linear system 48 | KKT = spa.vstack([spa.hstack([data[cps.P], A_con.T]), 49 | spa.hstack([A_con, O_con])], format='csc') 50 | return KKT 51 | 52 | 53 | def create_kkt_rhs(data): 54 | """Create KKT rhs from data.""" 55 | return np.concatenate((-data[cps.Q], data[cps.B + "_red"])) 56 | 57 | 58 | def create_kkt_system(data): 59 | """Create KKT linear system from data.""" 60 | 61 | KKT = create_kkt_matrix(data) 62 | rhs = create_kkt_rhs(data) 63 | 64 | return KKT, rhs 65 | 66 | 67 | def factorize_kkt_matrix(KKT): 68 | 69 | with CatchSingularMatrixWarnings(): 70 | return factorized(KKT) 71 | 72 | 73 | class KKTSolver(QpSolver): 74 | """KKT solver for equality constrained QPs""" 75 | 76 | SUPPORTED_CONSTRAINTS = [Zero] # Support only equality constraints 77 | 78 | def name(self): 79 | return KKT 80 | 81 | def import_solver(self): 82 | pass 83 | 84 | def invert(self, solution, inverse_data): 85 | attr = {s.SOLVE_TIME: solution['time']} 86 | 87 | status = solution['status'] 88 | 89 | if status in s.SOLUTION_PRESENT: 90 | opt_val = solution['cost'] 91 | primal_vars = { 92 | KKTSolver.VAR_ID: 93 | intf.DEFAULT_INTF.const_to_matrix(np.array(solution['x'])) 94 | } 95 | 96 | # Build dual variables 97 | n_eq, n_ineq = inverse_data['n_eq'], inverse_data['n_ineq'] 98 | # equalities 99 | y_eq = solution['y'][:n_eq] 100 | # only dual variables for inequalities (not integer variables) 101 | y_ineq = np.zeros(n_ineq) 102 | 103 | n_tight = np.sum(inverse_data['tight_constraints']) 104 | y_ineq[inverse_data['tight_constraints']] = \ 105 | solution['y'][n_eq:n_eq + n_tight] 106 | y = np.concatenate([y_eq, y_ineq]) 107 | 108 | dual_vars = {KKTSolver.DUAL_VAR_ID: y} 109 | else: 110 | primal_vars = None 111 | dual_vars = None 112 | opt_val = np.inf 113 | if status == s.UNBOUNDED: 114 | opt_val = -np.inf 115 | return Solution(status, opt_val, primal_vars, dual_vars, attr) 116 | 117 | def solve_via_data(self, data, warm_start, verbose, 118 | solver_opts, 119 | solver_cache=None): 120 | 121 | n_var = data[cps.P].shape[0] 122 | n_con = len(data[cps.B + "_red"]) # Only equality constraints 123 | 124 | stg.logger.debug("Solving %d x %d linear system A x = b " % 125 | (n_var + n_con, n_var + n_con)) 126 | 127 | if solver_cache is None: 128 | stg.logger.debug("Not using KKT solver cache") 129 | 130 | KKT, rhs = create_kkt_system(data) 131 | 132 | t_start = time.time() 133 | with CatchSingularMatrixWarnings(): 134 | x = spsolve(KKT, rhs, use_umfpack=True) 135 | t_end = time.time() 136 | 137 | else: 138 | stg.logger.debug("Using KKT solver cache") 139 | 140 | rhs = create_kkt_rhs(data) 141 | 142 | t_start = time.time() 143 | 144 | with CatchSingularMatrixWarnings(): 145 | x = solver_cache['factors'](rhs) 146 | t_end = time.time() 147 | 148 | # Get results 149 | results = {} 150 | results['x'] = x[:n_var] 151 | results['y'] = x[n_var:] 152 | 153 | if np.any(np.isnan(results['x'])): 154 | results['status'] = s.INFEASIBLE 155 | else: 156 | results['status'] = s.OPTIMAL 157 | results['cost'] = \ 158 | .5 * results['x'].T.dot(data['P'].dot(results['x'])) \ 159 | + data['q'].dot(results['x']) 160 | results['time'] = t_end - t_start 161 | 162 | return results 163 | 164 | 165 | # # Add solver to CVXPY solvers 166 | # QP_SOLVERS.insert(0, KKT) 167 | # SOLVER_MAP_QP[KKT] = KKTSolver() 168 | # INSTALLED_SOLVERS.append(KKT) 169 | -------------------------------------------------------------------------------- /mlopt/learners/__init__.py: -------------------------------------------------------------------------------- 1 | from mlopt.learners.pytorch.pytorch import PytorchNeuralNet 2 | from mlopt.learners.optimal_tree.optimal_tree import OptimalTree 3 | from mlopt.learners.xgboost.xgboost import XGBoost 4 | import mlopt.settings as s 5 | 6 | LEARNER_MAP = {s.PYTORCH: PytorchNeuralNet, 7 | s.OPTIMAL_TREE: OptimalTree, 8 | s.XGBOOST: XGBoost} 9 | 10 | 11 | def installed_learners(): 12 | """List the installed learners. 13 | """ 14 | installed = [] 15 | 16 | for name, learner in LEARNER_MAP.items(): 17 | if learner.is_installed(): 18 | installed.append(name) 19 | 20 | return installed 21 | -------------------------------------------------------------------------------- /mlopt/learners/learner.py: -------------------------------------------------------------------------------- 1 | from abc import ABC, abstractmethod 2 | import numpy as np 3 | import optuna 4 | import mlopt.settings as stg 5 | 6 | 7 | class Learner(ABC): 8 | """ 9 | Optimization strategy learner 10 | 11 | Attributes 12 | ---------- 13 | n_train : int 14 | Number of training samples. 15 | """ 16 | 17 | @classmethod 18 | @abstractmethod 19 | def is_installed(cls): 20 | """Is learner installed?""" 21 | return NotImplemented 22 | 23 | @property 24 | def n_train(self): 25 | """Number of training samples.""" 26 | return self._n_train 27 | 28 | @n_train.setter 29 | def n_train(self, value): 30 | self._n_train = value 31 | 32 | @abstractmethod 33 | def train(self, X, y): 34 | """Learn predictor form data.""" 35 | return NotImplemented 36 | 37 | @abstractmethod 38 | def predict(self, X): 39 | """Predict strategies from data.""" 40 | return NotImplemented 41 | 42 | @abstractmethod 43 | def save(self, file_name): 44 | """Save learner to file""" 45 | return NotImplemented 46 | 47 | @abstractmethod 48 | def load(self, file_name): 49 | """Load learner from file""" 50 | return NotImplemented 51 | 52 | def pick_best_class(self, y, n_best=None): 53 | """ 54 | Sort predictions and pick best points. 55 | 56 | Use n_best classes to choose classes that 57 | are most likely. 58 | """ 59 | n_points = y.shape[0] 60 | n_best = n_best if (n_best is not None) else self.options['n_best'] 61 | 62 | # Sort probabilities 63 | idx_probs = np.empty((n_points, n_best), dtype='int') 64 | for i in range(n_points): 65 | # Get best k indices 66 | # NB. Argsort sorts in reverse mode 67 | idx_probs[i, :] = np.argsort(y[i, :])[-n_best:] 68 | 69 | return idx_probs 70 | 71 | def print_trial_stats(self, study): 72 | """TODO: Docstring for print_trial_stats. 73 | 74 | Args: 75 | arg1 (TODO): TODO 76 | 77 | Returns: TODO 78 | 79 | """ 80 | best_params = study.best_trial.params 81 | 82 | pruned_trials = [t for t in study.trials 83 | if t.state == optuna.trial.TrialState.PRUNED] 84 | complete_trials = [t for t in study.trials 85 | if t.state == optuna.trial.TrialState.COMPLETE] 86 | 87 | stg.logger.info("Study statistics: ") 88 | stg.logger.info(" Number of finished trials: %d" % len(study.trials)) 89 | stg.logger.info(" Number of pruned trials: %d" % len(pruned_trials)) 90 | stg.logger.info(" Number of complete trials: %d" % 91 | len(complete_trials)) 92 | 93 | stg.logger.info("Best loss value: %.4f" % study.best_trial.value) 94 | stg.logger.info("Best parameters") 95 | for key, value in best_params.items(): 96 | print(" {}: {}".format(key, value)) 97 | -------------------------------------------------------------------------------- /mlopt/learners/optimal_tree/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bstellato/mlopt/48cb9f8b004b648d0c6bbfb586623a6696ebbcca/mlopt/learners/optimal_tree/__init__.py -------------------------------------------------------------------------------- /mlopt/learners/optimal_tree/optimal_tree.py: -------------------------------------------------------------------------------- 1 | from mlopt.learners.learner import Learner 2 | import mlopt.learners.optimal_tree.settings as octstg 3 | import mlopt.settings as stg 4 | from mlopt.utils import pandas2array, get_n_processes 5 | import mlopt.error as e 6 | import shutil 7 | from subprocess import call 8 | import time 9 | import os 10 | import sys 11 | 12 | class OptimalTree(Learner): 13 | 14 | def __init__(self, 15 | **options): 16 | """ 17 | Initialize OptimalTrees class. 18 | 19 | Parameters 20 | ---------- 21 | options : dict 22 | Learner options as a dictionary. 23 | """ 24 | if not OptimalTree.is_installed(): 25 | e.value_error("Interpretable AI not installed") 26 | 27 | # Import julia and IAI module 28 | from interpretableai import iai 29 | self.iai = iai 30 | from julia import Distributed 31 | self.nprocs = Distributed.nprocs 32 | 33 | # Define name 34 | self.name = stg.OPTIMAL_TREE 35 | 36 | # Assign settings 37 | self.n_input = options.pop('n_input') 38 | self.n_classes = options.pop('n_classes') 39 | self.options = {} 40 | self.options['hyperplanes'] = options.pop('hyperplanes', False) 41 | # self.options['fast_num_support_restarts'] = \ 42 | # options.pop('fast_num_support_restarts', [20]) 43 | self.options['parallel'] = options.pop('parallel_trees', True) 44 | self.options['cp'] = options.pop('cp', None) 45 | self.options['max_depth'] = options.pop('max_depth', 46 | octstg.DEFAULT_TRAINING_PARAMS['max_depth']) 47 | self.options['minbucket'] = options.pop('minbucket', 48 | octstg.DEFAULT_TRAINING_PARAMS['minbucket']) 49 | # Pick minimum between n_best and n_classes 50 | self.options['n_best'] = min(options.pop('n_best', stg.N_BEST), 51 | self.n_classes) 52 | self.options['save_svg'] = options.pop('save_svg', False) 53 | 54 | # Get fraction between training and validation 55 | self.options['frac_train'] = options.pop('frac_train', stg.FRAC_TRAIN) 56 | 57 | # Load Julia 58 | n_cpus = get_n_processes() 59 | 60 | n_cur_procs = self.nprocs() 61 | if n_cur_procs < n_cpus and self.options['parallel']: 62 | # Add processors to match number of cpus 63 | Distributed.addprocs((n_cpus - n_cur_procs)) 64 | 65 | # Assign optimaltrees options 66 | self.optimaltrees_options = {'random_seed': 1} 67 | self.optimaltrees_options['max_depth'] = self.options['max_depth'] 68 | self.optimaltrees_options['minbucket'] = self.options['minbucket'] 69 | if self.options['hyperplanes']: 70 | self.optimaltrees_options['hyperplane_config'] = \ 71 | {'sparsity': 'all'} 72 | 73 | if self.options['cp']: 74 | self.optimaltrees_options['cp'] = self.options['cp'] 75 | 76 | @classmethod 77 | def is_installed(cls): 78 | try: 79 | from interpretableai import iai 80 | except: 81 | return False 82 | return True 83 | 84 | def train(self, X, y): 85 | 86 | # Convert X to array 87 | self.n_train = len(X) 88 | # X = pandas2array(X) 89 | 90 | info_str = "Training trees " 91 | if self.options['parallel']: 92 | info_str += "on %d processors" % self.nprocs() 93 | else: 94 | info_str += "\n" 95 | stg.logger.info(info_str) 96 | 97 | # Start time 98 | start_time = time.time() 99 | 100 | # Create grid search 101 | self._grid = \ 102 | self.iai.GridSearch( 103 | self.iai.OptimalTreeClassifier( 104 | random_seed=self.optimaltrees_options['random_seed'] 105 | ), **self.optimaltrees_options) 106 | 107 | # Train classifier 108 | self._grid.fit(X, y, 109 | train_proportion=self.options['frac_train']) 110 | 111 | # Extract learner 112 | self._lnr = self._grid.get_learner() 113 | 114 | # End time 115 | end_time = time.time() 116 | stg.logger.info("Tree training time %.2f" % (end_time - start_time)) 117 | 118 | def predict(self, X): 119 | 120 | # Evaluate probabilities 121 | y = self._lnr.predict_proba(X) 122 | return self.pick_best_class(y.to_numpy(), n_best=self.options['n_best']) 123 | 124 | def save(self, file_name): 125 | # Save tree as json file 126 | self._lnr.write_json(file_name + ".json") 127 | 128 | # Save tree to dot file and convert it to 129 | # pdf for visualization purposes 130 | if self.options['save_svg']: 131 | if shutil.which("dot") is not None: 132 | self._lnr.write_dot(file_name + ".dot") 133 | call(["dot", "-Tsvg", "-o", 134 | file_name + ".svg", 135 | file_name + ".dot"]) 136 | else: 137 | stg.logger.warning("dot command not found in path") 138 | 139 | def load(self, file_name): 140 | # Check if file name exists 141 | if not os.path.isfile(file_name + ".json"): 142 | e.value_error("Optimal Tree json file does not exist.") 143 | 144 | # Load tree from file 145 | self._lnr = self.iai.read_json(file_name + ".json") 146 | -------------------------------------------------------------------------------- /mlopt/learners/optimal_tree/settings.py: -------------------------------------------------------------------------------- 1 | DEFAULT_TRAINING_PARAMS = { 2 | 'max_depth': [5, 10, 15], 3 | 'minbucket': [1, 5, 10], 4 | 'hyperplanes': False, 5 | } 6 | -------------------------------------------------------------------------------- /mlopt/learners/pytorch/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bstellato/mlopt/48cb9f8b004b648d0c6bbfb586623a6696ebbcca/mlopt/learners/pytorch/__init__.py -------------------------------------------------------------------------------- /mlopt/learners/pytorch/lightning.py: -------------------------------------------------------------------------------- 1 | import torch 2 | import torch.nn as nn 3 | import torch.nn.functional as F 4 | from pytorch_lightning.core.lightning import LightningModule 5 | from torch.utils.data import TensorDataset, DataLoader 6 | from torch.optim import Adam 7 | 8 | 9 | class LightningNet(LightningModule): 10 | def __init__(self, options, data=None): 11 | super(LightningNet, self).__init__() 12 | self.options = options 13 | self.data = data 14 | self.layers = nn.ModuleList() 15 | 16 | n_layers = options['n_layers'] 17 | dropout = options['dropout'] 18 | input_dim = options['n_input'] 19 | n_classes = options['n_classes'] 20 | 21 | # Add one linear and one dropout layer per layer 22 | for i in range(n_layers): 23 | output_dim = options['n_units_l{}'.format(i)] 24 | self.layers.append(nn.Linear(input_dim, output_dim)) 25 | self.layers.append(nn.Dropout(dropout)) 26 | input_dim = output_dim 27 | 28 | self.layers.append(nn.Linear(input_dim, n_classes)) 29 | 30 | def forward(self, x): 31 | for layer in self.layers: 32 | x = layer(x) 33 | if type(layer) != nn.Dropout: 34 | x = F.relu(x) 35 | return F.log_softmax(x, dim=1) 36 | 37 | def train_dataloader(self): 38 | X = torch.tensor(self.data['X_train'], dtype=torch.float) 39 | y = torch.tensor(self.data['y_train'], dtype=torch.long) 40 | 41 | return DataLoader(TensorDataset(X, y), 42 | batch_size=self.options['batch_size'], 43 | shuffle=False) 44 | 45 | def val_dataloader(self): 46 | X = torch.tensor(self.data['X_valid'], dtype=torch.float) 47 | y = torch.tensor(self.data['y_valid'], dtype=torch.long) 48 | 49 | return DataLoader(TensorDataset(X, y), 50 | batch_size=self.options['batch_size'], 51 | shuffle=False) 52 | 53 | def training_step(self, batch, batch_idx): 54 | inputs, labels = batch 55 | outputs = self(inputs) 56 | loss = F.nll_loss(outputs, labels) 57 | return {'loss': loss} 58 | 59 | def validation_step(self, batch, batch_idx): 60 | inputs, labels = batch 61 | outputs = self(inputs) 62 | loss = F.nll_loss(outputs, labels) 63 | return {"val_loss": loss} 64 | 65 | def validation_epoch_end(self, outputs): 66 | val_loss_mean = torch.stack([x['val_loss'] for x in outputs]).mean() 67 | return {'val_loss': val_loss_mean} 68 | 69 | def configure_optimizers(self): 70 | return Adam(self.parameters(), 71 | lr=self.options['learning_rate']) 72 | -------------------------------------------------------------------------------- /mlopt/learners/pytorch/pytorch.py: -------------------------------------------------------------------------------- 1 | import mlopt.settings as stg 2 | import mlopt.learners.pytorch.settings as pts 3 | from optuna.integration import PyTorchLightningPruningCallback 4 | from pytorch_lightning import Trainer 5 | from mlopt.learners.pytorch.lightning import LightningNet 6 | from pytorch_lightning import Callback 7 | import mlopt.error as e 8 | from mlopt.learners.learner import Learner 9 | from sklearn.model_selection import train_test_split 10 | import optuna 11 | from time import time 12 | import os 13 | import logging 14 | 15 | 16 | class MetricsCallback(Callback): 17 | """PyTorch Lightning metric callback.""" 18 | 19 | def __init__(self): 20 | super().__init__() 21 | self.metrics = [] 22 | 23 | def on_validation_end(self, trainer, pl_module): 24 | self.metrics.append(trainer.callback_metrics) 25 | 26 | 27 | class PytorchObjective(object): 28 | 29 | def __init__(self, data, bounds, n_input, n_classes, use_gpu=False): 30 | self.use_gpu = use_gpu 31 | self.bounds = bounds 32 | self.data = data 33 | self.n_input = n_input 34 | self.n_classes = n_classes 35 | 36 | def __call__(self, trial): 37 | 38 | # The default logger in PyTorch Lightning writes to event files 39 | # to be consumed by TensorBoard. We don't use any logger here as 40 | # it requires us to implement several abstract methods. Instead 41 | # we setup a simple callback, that saves metrics from each 42 | # validation step. 43 | metrics_callback = MetricsCallback() 44 | 45 | # Define parameters 46 | parameters = { 47 | 'n_input': self.n_input, 48 | 'n_classes': self.n_classes, 49 | 'n_layers': trial.suggest_int('n_layers', 50 | *self.bounds['n_layers']), 51 | 'dropout': trial.suggest_uniform('dropout', 52 | *self.bounds['dropout']), 53 | 'batch_size': trial.suggest_int('batch_size', 54 | *self.bounds['batch_size']), 55 | 'learning_rate': trial.suggest_float('learning_rate', 56 | *self.bounds['learning_rate'], 57 | log=True), 58 | 'max_epochs': trial.suggest_int('max_epochs', 59 | *self.bounds['max_epochs']) 60 | } 61 | for i in range(parameters['n_layers']): 62 | parameters['n_units_l{}'.format(i)] = trial.suggest_int( 63 | 'n_units_l{}'.format(i), *self.bounds['n_units_l'], log=True) 64 | 65 | # Construct trainer object and train 66 | trainer = Trainer( 67 | logger=False, 68 | checkpoint_callback=False, 69 | accelerator='dp', 70 | max_epochs=parameters['max_epochs'], 71 | gpus=-1 if self.use_gpu else None, 72 | callbacks=[metrics_callback, 73 | PyTorchLightningPruningCallback(trial, 74 | monitor="val_loss")], 75 | 76 | ) 77 | 78 | model = LightningNet(parameters, self.data) 79 | trainer.fit(model) 80 | 81 | return metrics_callback.metrics[-1]["val_loss"] 82 | 83 | 84 | class PytorchNeuralNet(Learner): 85 | """Pytorch Learner class. """ 86 | 87 | def __init__(self, **options): 88 | """Initialize Pytorch Learner class. 89 | 90 | Parameters 91 | ---------- 92 | options : dict 93 | Learner options as a dictionary. 94 | """ 95 | if not PytorchNeuralNet.is_installed(): 96 | e.value_error("Pytorch not installed") 97 | 98 | # import torch 99 | import torch 100 | self.torch = torch 101 | 102 | # Disable logging 103 | log = logging.getLogger("lightning") 104 | log.setLevel(logging.ERROR) 105 | 106 | self.name = stg.PYTORCH 107 | self.n_input = options.pop('n_input') 108 | self.n_classes = options.pop('n_classes') 109 | self.options = {} 110 | 111 | self.options['bounds'] = options.pop( 112 | 'bounds', pts.PARAMETER_BOUNDS) 113 | 114 | not_specified_bounds = \ 115 | [x for x in pts.PARAMETER_BOUNDS.keys() 116 | if x not in self.options['bounds'].keys()] 117 | for p in not_specified_bounds: # Assign remaining keys 118 | self.options['bounds'][p] = pts.PARAMETER_BOUNDS[p] 119 | 120 | # Pick minimum between n_best and n_classes 121 | self.options['n_best'] = min(options.pop('n_best', stg.N_BEST), 122 | self.n_classes) 123 | 124 | # Pick number of hyperopt_trials 125 | self.options['n_train_trials'] = options.pop('n_train_trials', 126 | stg.N_TRAIN_TRIALS) 127 | 128 | # Mute optuna 129 | optuna.logging.set_verbosity(optuna.logging.INFO) 130 | 131 | # Define device 132 | self.use_gpu = self.torch.cuda.is_available() 133 | if self.use_gpu: 134 | self.device = self.torch.device("cuda:0") 135 | stg.logger.info("Using CUDA GPU %s with Pytorch" % 136 | self.torch.cuda.get_device_name(self.device)) 137 | else: 138 | self.device = self.torch.device("cpu") 139 | stg.logger.info("Using CPU with Pytorch") 140 | 141 | @classmethod 142 | def is_installed(cls): 143 | try: 144 | import torch 145 | torch 146 | except ImportError: 147 | return False 148 | return True 149 | 150 | def train(self, X, y): 151 | """ 152 | Train model. 153 | 154 | Parameters 155 | ---------- 156 | X : pandas DataFrame 157 | Features. 158 | y : numpy int array 159 | Labels. 160 | """ 161 | 162 | self.n_train = len(X) 163 | 164 | # Split train and validation 165 | X_train, X_valid, y_train, y_valid = train_test_split( 166 | X, y, # stratify=y, 167 | test_size=(1 - stg.FRAC_TRAIN), random_state=0) 168 | 169 | data = {'X_train': X_train, 'y_train': y_train, 170 | 'X_valid': X_valid, 'y_valid': y_valid} 171 | stg.logger.info("Split dataset in %d training and %d validation" % 172 | (len(y_train), len(y_valid))) 173 | 174 | start_time = time() 175 | objective = PytorchObjective(data, self.options['bounds'], 176 | self.n_input, self.n_classes, 177 | self.use_gpu) 178 | 179 | sampler = optuna.samplers.TPESampler(seed=0) # Deterministic 180 | pruner = optuna.pruners.MedianPruner( 181 | # n_warmup_steps=5 182 | ) 183 | study = optuna.create_study(sampler=sampler, pruner=pruner, 184 | direction="minimize") 185 | study.optimize(objective, 186 | n_trials=self.options['n_train_trials'], 187 | # show_progress_bar=True 188 | ) 189 | 190 | # DEBUG 191 | # fig = optuna.visualization.plot_intermediate_values(study) 192 | # fig.show() 193 | 194 | self.best_params = study.best_trial.params 195 | self.best_params['n_input'] = self.n_input 196 | self.best_params['n_classes'] = self.n_classes 197 | 198 | self.print_trial_stats(study) 199 | 200 | # Train again 201 | stg.logger.info("Train with best parameters") 202 | 203 | self.trainer = Trainer( 204 | checkpoint_callback=False, 205 | accelerator='dp', 206 | logger=False, # ?? 207 | max_epochs=self.best_params['max_epochs'], 208 | gpus=-1 if self.use_gpu else None, 209 | ) 210 | self.model = LightningNet(self.best_params, data) 211 | self.trainer.fit(self.model) 212 | 213 | # Print timing 214 | end_time = time() 215 | stg.logger.info("Training time %.2f" % (end_time - start_time)) 216 | 217 | def predict(self, X): 218 | 219 | # Disable gradients computation 220 | # self.model.eval() # Put layers in evaluation mode 221 | # with self.torch.no_grad(): # Needed? 222 | # 223 | # X = self.torch.tensor(X, dtype=self.torch.float).to(self.device) 224 | # y = self.model(X).detach().cpu().numpy() 225 | 226 | X = self.torch.tensor(X, dtype=self.torch.float) 227 | with self.torch.no_grad(): 228 | y = self.model(X).detach().numpy() 229 | 230 | return self.pick_best_class(y, n_best=self.options['n_best']) 231 | 232 | def save(self, file_name): 233 | self.trainer.save_checkpoint(file_name + ".ckpt") 234 | 235 | # Save state dictionary to file 236 | # https://pytorch.org/tutorials/beginner/saving_loading_models.html 237 | # self.torch.save(self.model.state_dict(), file_name + ".pkl") 238 | 239 | def load(self, file_name): 240 | path = file_name + ".ckpt" 241 | # Check if file name exists 242 | if not os.path.isfile(path): 243 | e.value_error("Pytorch checkpoint file does not exist.") 244 | 245 | self.model = LightningNet.load_from_checkpoint( 246 | checkpoint_path=path, options=self.best_params 247 | ) 248 | self.model.eval() # Necessary to set the model to evaluation mode 249 | self.model.freeze() # Fix layers parameters 250 | 251 | # # Load state dictionary from file 252 | # # https://pytorch.org/tutorials/beginner/saving_loading_models.html 253 | # self.model.load_state_dict(self.torch.load(file_name + ".pkl")) 254 | # self.model.eval() # Necessary to set the model to evaluation mode 255 | -------------------------------------------------------------------------------- /mlopt/learners/pytorch/settings.py: -------------------------------------------------------------------------------- 1 | # DEFAULT_TRAINING_PARAMS = { 2 | # 'learning_rate': [1e-04, 1e-03, 1e-02], 3 | # 'n_epochs': [20], 4 | # 'batch_size': [32], 5 | # # 'n_layers': [5, 7, 10] 6 | # } 7 | 8 | 9 | PARAMETERS = { 10 | # TOFILL 11 | # 'objective': 'multi:softprob', 12 | # 'eval_metric': 'mlogloss', 13 | # 'booster': 'gbtree', 14 | } 15 | 16 | PARAMETER_BOUNDS = { 17 | 'n_layers': [3, 15], 18 | 'n_units_l': [4, 128], 19 | 'learning_rate': [1e-05, 1e1], 20 | 'dropout': [0.1, 0.5], 21 | 'max_epochs': [5, 30], 22 | 'batch_size': [32, 256], 23 | } 24 | 25 | N_FOLDS = 5 26 | -------------------------------------------------------------------------------- /mlopt/learners/pytorch/utils.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import mlopt.settings as stg 3 | 4 | 5 | def accuracy(outputs, labels): 6 | """ 7 | Compute the accuracy, given the outputs and labels for all images. 8 | 9 | Args: 10 | outputs: (np.ndarray) output of the model 11 | labels: (np.ndarray) batch labels 12 | 13 | Returns: (float) accuracy in [0,1] 14 | """ 15 | outputs = np.argmax(outputs, axis=1) 16 | return np.sum(outputs == labels) / float(labels.size) 17 | 18 | 19 | def log_metrics(metrics, string="Train"): 20 | # compute mean of all metrics in summary 21 | metrics_mean = {metric: np.mean([x[metric] for x in metrics]) 22 | for metric in metrics[0]} 23 | metrics_string = " ; ".join("{}: {:05.3f}".format(k, v) 24 | for k, v in metrics_mean.items()) 25 | stg.logger.info("- {} metrics: ".format(string) + metrics_string) 26 | 27 | return metrics_mean 28 | 29 | 30 | def eval_metrics(outputs, labels, loss): 31 | outputs = outputs.detach().cpu().numpy() 32 | labels = labels.detach().cpu().numpy() 33 | 34 | # compute all metrics on this batch 35 | summary = {metric: METRICS[metric](outputs, 36 | labels) 37 | for metric in METRICS} 38 | summary['loss'] = loss.item() 39 | 40 | return summary 41 | 42 | 43 | class RunningAverage(): 44 | """A simple class that maintains the running average of a quantity 45 | 46 | Example: 47 | ``` 48 | loss_avg = RunningAverage() 49 | loss_avg.update(2) 50 | loss_avg.update(4) 51 | loss_avg() # Returns 3 52 | ``` 53 | """ 54 | 55 | def __init__(self): 56 | self.steps = 0 57 | self.total = 0.0 58 | 59 | def update(self, val): 60 | self.total += val 61 | self.steps += 1 62 | 63 | def __call__(self): 64 | return self.total/float(self.steps) 65 | 66 | 67 | # maintain all metrics required in this dictionary- these are used in 68 | # the training and evaluation loops 69 | METRICS = { 70 | 'accuracy': accuracy, 71 | # could add more metrics such as accuracy for each token type 72 | } 73 | 74 | 75 | METRICS_STEPS = 100 76 | 77 | -------------------------------------------------------------------------------- /mlopt/learners/xgboost/settings.py: -------------------------------------------------------------------------------- 1 | # Parameters 2 | # https://github.com/dmlc/xgboost/blob/master/doc/parameter.rst 3 | 4 | # DEFAULT_TRAINING_PARAMS = { 5 | # 'max_depth': [1, 5, 10], 6 | # 'learning_rate' : [0.1, 1], 7 | # 'n_estimators': [10, 100], 8 | # 'random_state': [0], 9 | # 'objective':['multi:softmax'], 10 | # # https://xgboost.readthedocs.io/en/latest/tutorials/saving_model.html 11 | # # 'enable_experimental_json_serialization': [True] 12 | # } 13 | 14 | PARAMETERS = { 15 | 'objective': 'multi:softprob', 16 | 'eval_metric': 'mlogloss', 17 | 'booster': 'gbtree', 18 | } 19 | 20 | PARAMETER_BOUNDS = { 21 | 'lambda': [1e-8, 2.0], 22 | 'alpha': [1e-9, 2.0], 23 | 'max_depth': [1, 15], 24 | 'eta': [1e-9, 2.0], 25 | 'gamma': [1e-8, 2.0], 26 | 'n_boost_round': [50, 400], 27 | } 28 | -------------------------------------------------------------------------------- /mlopt/learners/xgboost/xgboost.py: -------------------------------------------------------------------------------- 1 | import optuna 2 | from mlopt.learners.learner import Learner 3 | import mlopt.learners.xgboost.settings as xgbs 4 | import mlopt.settings as stg 5 | import mlopt.error as e 6 | import time 7 | import copy 8 | 9 | 10 | class XGBoostObjective(object): 11 | def __init__(self, dtrain, bounds, n_classes): 12 | self.bounds = copy.deepcopy(bounds) 13 | self.n_classes = n_classes 14 | import xgboost as xgb 15 | self.xgb = xgb 16 | self.dtrain = dtrain 17 | 18 | def __call__(self, trial): 19 | params = xgbs.PARAMETERS 20 | params.update({ 21 | 'objective': 'multi:softprob', 22 | 'eval_metric': 'mlogloss', 23 | 'booster': 'gbtree', 24 | 'num_class': self.n_classes, 25 | 'lambda': trial.suggest_float( 26 | 'lambda', *self.bounds['lambda'], log=True), 27 | 'alpha': trial.suggest_float( 28 | 'alpha', *self.bounds['alpha'], log=True), 29 | 'max_depth': trial.suggest_int( 30 | 'max_depth', *self.bounds['max_depth']), 31 | 'eta': trial.suggest_float( 32 | 'eta', *self.bounds['eta'], log=True), 33 | 'gamma': trial.suggest_float( 34 | 'gamma', *self.bounds['gamma'], log=True), 35 | }) 36 | n_boost_round = trial.suggest_int( 37 | 'n_boost_round', *self.bounds['n_boost_round']) 38 | 39 | pruning_callback = optuna.integration.XGBoostPruningCallback( 40 | trial, "test-mlogloss") 41 | history = self.xgb.cv(params, self.dtrain, 42 | num_boost_round=n_boost_round, 43 | callbacks=[pruning_callback] 44 | ) 45 | 46 | mean_loss = history["test-mlogloss-mean"].values[-1] 47 | return mean_loss 48 | 49 | 50 | class XGBoost(Learner): 51 | """XGBoost Learner class. """ 52 | 53 | def __init__(self, 54 | **options): 55 | """ 56 | Initialize XGBoost Learner class. 57 | 58 | Parameters 59 | ---------- 60 | options : dict 61 | Learner options as a dictionary. 62 | """ 63 | if not XGBoost.is_installed(): 64 | e.value_error("XGBoost not installed") 65 | 66 | import xgboost as xgb 67 | self.xgb = xgb 68 | 69 | self.name = stg.XGBOOST 70 | self.n_input = options.pop('n_input') 71 | self.n_classes = options.pop('n_classes') 72 | self.options = {} 73 | 74 | self.options['bounds'] = options.pop( 75 | 'bounds', xgbs.PARAMETER_BOUNDS) 76 | 77 | not_specified_bounds = \ 78 | [x for x in xgbs.PARAMETER_BOUNDS.keys() 79 | if x not in self.options['bounds'].keys()] 80 | for p in not_specified_bounds: # Assign remaining keys 81 | self.options['bounds'][p] = xgbs.PARAMETER_BOUNDS[p] 82 | 83 | # Pick minimum between n_best and n_classes 84 | self.options['n_best'] = min(options.pop('n_best', stg.N_BEST), 85 | self.n_classes) 86 | 87 | # Pick number of hyperopt_trials 88 | self.options['n_train_trials'] = options.pop('n_train_trials', 89 | stg.N_TRAIN_TRIALS) 90 | 91 | # Mute optuna 92 | optuna.logging.set_verbosity(optuna.logging.INFO) 93 | 94 | @classmethod 95 | def is_installed(cls): 96 | try: 97 | import xgboost 98 | xgboost 99 | except ImportError: 100 | return False 101 | return True 102 | 103 | def train(self, X, y): 104 | 105 | self.n_train = len(X) 106 | dtrain = self.xgb.DMatrix(X, label=y) 107 | 108 | stg.logger.info("Train XGBoost") 109 | 110 | start_time = time.time() 111 | objective = XGBoostObjective(dtrain, self.options['bounds'], 112 | self.n_classes) 113 | 114 | sampler = optuna.samplers.TPESampler(seed=0) # Deterministic 115 | pruner = optuna.pruners.MedianPruner(n_warmup_steps=5) 116 | study = optuna.create_study(sampler=sampler, pruner=pruner, 117 | direction="minimize") 118 | study.optimize(objective, n_trials=self.options['n_train_trials'], 119 | # show_progress_bar=True 120 | ) 121 | self.best_params = study.best_trial.params 122 | 123 | self.print_trial_stats(study) 124 | 125 | # Train again 126 | stg.logger.info("Train with best parameters") 127 | params = xgbs.PARAMETERS # Fixed parameters 128 | params.update({k: v for k, v in self.best_params.items() 129 | if k != 'n_boost_round'}) 130 | self.bst = self.xgb.train( 131 | params=params, 132 | dtrain=dtrain, 133 | num_boost_round=self.best_params['n_boost_round'] 134 | ) 135 | 136 | # Print timing 137 | end_time = time.time() 138 | stg.logger.info("Training time %.2f" % (end_time - start_time)) 139 | 140 | def predict(self, X): 141 | y = self.bst.predict(self.xgb.DMatrix(X)) 142 | return self.pick_best_class(y, n_best=self.options['n_best']) 143 | 144 | def save(self, file_name): 145 | self.bst.save_model(file_name + ".json") 146 | 147 | def load(self, file_name): 148 | self.bst = self.xgb.Booster() 149 | self.bst.load_model(file_name + ".json") 150 | -------------------------------------------------------------------------------- /mlopt/optimizer.py: -------------------------------------------------------------------------------- 1 | import pandas as pd 2 | from mlopt.problem import Problem 3 | import mlopt.settings as stg 4 | from mlopt.learners import LEARNER_MAP, installed_learners 5 | from mlopt.sampling import Sampler 6 | from mlopt.strategy import encode_strategies 7 | from mlopt.filter import Filter 8 | import mlopt.error as e 9 | from mlopt.utils import n_features, accuracy, suboptimality 10 | import mlopt.utils as u 11 | from mlopt.kkt import create_kkt_matrix, factorize_kkt_matrix 12 | from mlopt.utils import pandas2array 13 | from cvxpy import Minimize, Maximize 14 | import numpy as np 15 | import os 16 | from glob import glob 17 | import tempfile 18 | import tarfile 19 | import pickle as pkl 20 | from joblib import Parallel, delayed 21 | from tqdm.auto import tqdm 22 | from time import time 23 | 24 | 25 | class Optimizer(object): 26 | """ 27 | Machine Learning Optimizer class. 28 | """ 29 | 30 | def __init__(self, 31 | cvxpy_problem, 32 | name="problem", 33 | log_level=None, 34 | parallel=True, 35 | tight_constraints=True, 36 | **solver_options): 37 | """ 38 | Inizialize optimizer. 39 | 40 | Parameters 41 | ---------- 42 | problem : cvxpy.Problem 43 | Problem in CVXPY format. 44 | name : str 45 | Problem name. 46 | solver_options : dict, optional 47 | A dict of options for the internal solver. 48 | """ 49 | 50 | if log_level is not None: 51 | stg.logger.setLevel(log_level) 52 | 53 | self._problem = Problem(cvxpy_problem, 54 | solver=stg.DEFAULT_SOLVER, 55 | tight_constraints=tight_constraints, 56 | **solver_options) 57 | self._solver_cache = None 58 | self.name = name 59 | self._learner = None 60 | self.encoding = None 61 | self.X_train = None 62 | self.y_train = None 63 | 64 | @property 65 | def n_strategies(self): 66 | """Number of strategies.""" 67 | if self.encoding is None: 68 | e.value_error("Model has been trained yet to " + 69 | "return the number of strategies.") 70 | 71 | return len(self.encoding) 72 | 73 | def variables(self): 74 | """Problem variables.""" 75 | return self._problem.variables() 76 | 77 | @property 78 | def parameters(self): 79 | """Problem parameters.""" 80 | return self._problem.parameters 81 | 82 | @property 83 | def n_parameters(self): 84 | """Number of parameters.""" 85 | return self._problem.n_parameters 86 | 87 | def samples_present(self): 88 | """Check if samples have been generated.""" 89 | return (self.X_train is not None) and \ 90 | (self.y_train is not None) and \ 91 | (self.encoding is not None) 92 | 93 | def sample(self, sampling_fn, parallel=False): 94 | """ 95 | Sample parameters. 96 | """ 97 | 98 | # Create sampler 99 | self._sampler = Sampler(self._problem, sampling_fn) 100 | 101 | # Sample parameters 102 | self.X_train, self.y_train, self.obj_train, self.encoding = \ 103 | self._sampler.sample(parallel=parallel) 104 | 105 | def save_training_data(self, file_name, delete_existing=False): 106 | """ 107 | Save training data to file. 108 | 109 | 110 | Avoids the need to recompute data. 111 | 112 | Parameters 113 | ---------- 114 | file_name : string 115 | File name of the compressed optimizer. 116 | delete_existing : bool, optional 117 | Delete existing file with the same name? 118 | Defaults to False. 119 | """ 120 | # Check if file already exists 121 | if os.path.isfile(file_name): 122 | if not delete_existing: 123 | p = None 124 | while p not in ['y', 'n', 'N', '']: 125 | p = input("File %s already exists. " % file_name + 126 | "Would you like to delete it? [y/N] ") 127 | if p == 'y': 128 | os.remove(file_name) 129 | else: 130 | return 131 | else: 132 | os.remove(file_name) 133 | 134 | if not self.samples_present(): 135 | e.value_error("You need to get the strategies " + 136 | "from the data first by training the model.") 137 | 138 | # Save to file 139 | with open(file_name, 'wb') \ 140 | as data: 141 | data_dict = {'X_train': self.X_train, 142 | 'y_train': self.y_train, 143 | 'obj_train': self.obj_train, 144 | '_problem': self._problem, 145 | 'encoding': self.encoding} 146 | 147 | # if hasattr(self, '_solver_cache'): 148 | # data_dict['_solver_cache'] = self._solver_cache 149 | 150 | # Store strategy filter 151 | if hasattr(self, '_filter'): 152 | data_dict['_filter'] = self._filter 153 | 154 | pkl.dump(data_dict, data) 155 | 156 | def load_training_data(self, file_name): 157 | """ 158 | Load pickled training data from file name. 159 | 160 | Parameters 161 | ---------- 162 | file_name : string 163 | File name of the data. 164 | """ 165 | 166 | # Check if file exists 167 | if not os.path.isfile(file_name): 168 | e.value_error("File %s does not exist." % file_name) 169 | 170 | # Load optimizer 171 | with open(file_name, "rb") as f: 172 | data_dict = pkl.load(f) 173 | 174 | # Store data internally 175 | self.X_train = data_dict['X_train'] 176 | self.y_train = data_dict['y_train'] 177 | self.obj_train = data_dict['obj_train'] 178 | self._problem = data_dict['_problem'] 179 | self.encoding = data_dict['encoding'] 180 | 181 | # Set n_train in learner 182 | if self._learner is not None: 183 | self._learner.n_train = len(self.y_train) 184 | 185 | stg.logger.info("Loaded %d points with %d strategies" % 186 | (len(self.y_train), len(self.encoding))) 187 | 188 | if ('_solver_cache' in data_dict): 189 | self._solver_cache = data_dict['_solver_cache'] 190 | 191 | # Full strategies backup after filtering 192 | if ('_filter' in data_dict): 193 | self._filter = data_dict['_filter'] 194 | 195 | # Compute Good turing estimates 196 | self._sampler = Sampler(self._problem, n_samples=len(self.X_train)) 197 | self._sampler.compute_good_turing(self.y_train) 198 | 199 | def get_samples(self, X=None, sampling_fn=None, 200 | parallel=True, 201 | filter_strategies=stg.FILTER_STRATEGIES): 202 | """Get samples either from data or from sampling function""" 203 | 204 | # Assert we have data to train or already trained 205 | if X is None and sampling_fn is None and not self.samples_present(): 206 | e.value_error("Not enough arguments to train the model") 207 | 208 | if X is not None and sampling_fn is not None: 209 | e.value_error("You can pass only one value between X " 210 | "and sampling_fn") 211 | 212 | # Check if data is passed, otherwise train 213 | # if (X is not None) and not self.samples_present(): 214 | if X is not None: 215 | stg.logger.info("Use new data") 216 | self.X_train = X 217 | self.y_train = None 218 | self.encoding = None 219 | 220 | # Encode training strategies by solving 221 | # the problem for all the points 222 | results = self._problem.solve_parametric(X, 223 | parallel=parallel, 224 | message="Compute " + 225 | "tight constraints " + 226 | "for training set") 227 | 228 | stg.logger.info("Checking for infeasible points") 229 | not_feasible_points = {i: x for i, x in tqdm(enumerate(results)) 230 | if np.isnan(x['x']).any()} 231 | if not_feasible_points: 232 | e.value_error("Infeasible points found. Number of infeasible " 233 | "points %d" % len(not_feasible_points)) 234 | stg.logger.info("No infeasible point found.") 235 | 236 | self.obj_train = [r['cost'] for r in results] 237 | train_strategies = [r['strategy'] for r in results] 238 | 239 | # Check if the problems are solvable 240 | # for r in results: 241 | # assert r['status'] in cps.SOLUTION_PRESENT, \ 242 | # "The training points must be feasible" 243 | 244 | # Encode strategies 245 | self.y_train, self.encoding = \ 246 | encode_strategies(train_strategies) 247 | 248 | # Compute Good turing estimates 249 | self._sampler = Sampler(self._problem, n_samples=len(self.X_train)) 250 | self._sampler.compute_good_turing(self.y_train) 251 | 252 | # Condense strategies 253 | if filter_strategies: 254 | self.filter_strategies(parallel=parallel) 255 | 256 | elif sampling_fn is not None and not self.samples_present(): 257 | stg.logger.info("Use iterative sampling") 258 | # Create X_train, y_train and encoding from 259 | # sampling function 260 | self.sample(sampling_fn, parallel=parallel) 261 | 262 | # Condense strategies 263 | if filter_strategies: 264 | self.filter_strategies(parallel=parallel) 265 | 266 | # Add factorization faching if 267 | # 1. Problem is MIQP 268 | # 2. Parameters do not enter in matrices 269 | if self._problem.is_qp() and \ 270 | (self._solver_cache is None) and \ 271 | not self._problem.parameters_in_matrices: 272 | self.cache_factors() 273 | 274 | def filter_strategies(self, parallel=True, **filter_options): 275 | # Store full non filtered strategies 276 | 277 | self.encoding_full = self.encoding 278 | self.y_train_full = self.y_train 279 | 280 | # Define strategies filter (not run it yet) 281 | self._filter = Filter(X_train=self.X_train, 282 | y_train=self.y_train, 283 | obj_train=self.obj_train, 284 | encoding=self.encoding, 285 | problem=self._problem) 286 | self.y_train, self.encoding = \ 287 | self._filter.filter(parallel=parallel, **filter_options) 288 | 289 | def train(self, X=None, 290 | sampling_fn=None, 291 | parallel=True, 292 | learner=stg.DEFAULT_LEARNER, 293 | filter_strategies=stg.FILTER_STRATEGIES, 294 | **learner_options): 295 | """ 296 | Train optimizer using parameter X. 297 | 298 | This function needs one argument between data points X 299 | or sampling function sampling_fn. It will raise an error 300 | otherwise because there is no way to sample data. 301 | 302 | Parameters 303 | ---------- 304 | X : pandas dataframe or numpy array, optional 305 | Data samples. Each row is a new sample points. 306 | sampling_fn : function, optional 307 | Function to sample data taking one argument being 308 | the number of data points to be sampled and returning 309 | a structure of the same type as X. 310 | parallel : bool 311 | Perform training in parallel. 312 | learner : str 313 | Learner to use. Learners are defined in :mod:`mlopt.settings` 314 | learner_options : dict, optional 315 | A dict of options for the learner. 316 | """ 317 | 318 | # Get training samples 319 | self.get_samples(X, sampling_fn, 320 | parallel=parallel, 321 | filter_strategies=filter_strategies) 322 | 323 | # Define learner 324 | if learner not in installed_learners(): 325 | e.value_error("Learner specified not installed. " 326 | "Available learners are: %s" % installed_learners()) 327 | self._learner = LEARNER_MAP[learner](n_input=n_features(self.X_train), 328 | n_classes=len(self.encoding), 329 | **learner_options) 330 | 331 | # Train learner 332 | self._learner.train(pandas2array(self.X_train), 333 | self.y_train) 334 | 335 | def cache_factors(self): 336 | """Cache linear system solver factorizations""" 337 | 338 | self._solver_cache = [] 339 | stg.logger.info("Caching KKT solver factors for each strategy ") 340 | for strategy_idx in tqdm(range(self.n_strategies)): 341 | 342 | # Get a parameter giving that strategy 343 | strategy = self.encoding[strategy_idx] 344 | idx_param = np.where(self.y_train == strategy_idx)[0] 345 | theta = self.X_train.iloc[idx_param[0]] 346 | 347 | # Populate 348 | self._problem.populate(theta) 349 | 350 | # Get problem data 351 | data, inverse_data, solving_chain = \ 352 | self._problem._get_problem_data() 353 | 354 | # Apply strategy 355 | strategy.apply(data, inverse_data[-1]) 356 | 357 | # Old 358 | # self._problem.populate(theta) 359 | # 360 | # self._problem._relax_disc_var() 361 | # 362 | # reduced_problem = \ 363 | # self._problem._construct_reduced_problem(strategy) 364 | # 365 | # data, full_chain, inv_data = \ 366 | # reduced_problem.get_problem_data(solver=KKT) 367 | 368 | # Get KKT matrix 369 | KKT_mat = create_kkt_matrix(data) 370 | solve_kkt = factorize_kkt_matrix(KKT_mat) 371 | 372 | cache = {} 373 | cache['factors'] = solve_kkt 374 | # cache['inverse_data'] = inverse_data 375 | # cache['chain'] = solving_chain 376 | 377 | self._solver_cache += [cache] 378 | 379 | def choose_best(self, problem_data, labels, parallel=False, 380 | batch_size=stg.JOBLIB_BATCH_SIZE, use_cache=True): 381 | """ 382 | Choose best strategy between provided ones 383 | 384 | Parameters 385 | ---------- 386 | labels : list 387 | Strategy labels to compare. 388 | parallel : bool, optional 389 | Perform `n_best` strategies evaluation in parallel. 390 | True by default. 391 | use_cache : bool, optional 392 | Use solver cache if available. True by default. 393 | 394 | Returns 395 | ------- 396 | dict 397 | Results as a dictionary. 398 | """ 399 | n_best = self._learner.options['n_best'] 400 | 401 | # For each n_best classes get x, y, time and store the best one 402 | x = [] 403 | time = [] 404 | infeas = [] 405 | cost = [] 406 | 407 | strategies = [self.encoding[label] for label in labels] 408 | 409 | # Cache is a list of solver caches to pass 410 | cache = [None] * n_best 411 | if self._solver_cache and use_cache: 412 | cache = [self._solver_cache[label] for label in labels] 413 | 414 | n_jobs = u.get_n_processes() if parallel else 1 415 | results = Parallel(n_jobs=n_jobs, batch_size=batch_size)( 416 | delayed(self._problem.solve)(problem_data, 417 | strategy=strategies[j], 418 | cache=cache[j]) 419 | for j in range(n_best)) 420 | 421 | x = [r["x"] for r in results] 422 | time = [r["time"] for r in results] 423 | infeas = [r["infeasibility"] for r in results] 424 | cost = [r["cost"] for r in results] 425 | 426 | # Pick best class between k ones 427 | infeas = np.array(infeas) 428 | cost = np.array(cost) 429 | idx_filter = np.where(infeas <= stg.INFEAS_TOL)[0] 430 | if len(idx_filter) > 0: 431 | # Case 1: Feasible points 432 | # -> Get solution with best cost 433 | # between feasible ones 434 | if self._problem.sense() == Minimize: 435 | idx_pick = idx_filter[np.argmin(cost[idx_filter])] 436 | elif self._problem.sense() == Maximize: 437 | idx_pick = idx_filter[np.argmax(cost[idx_filter])] 438 | else: 439 | e.value_error('Objective type not understood') 440 | else: 441 | # Case 2: No feasible points 442 | # -> Get solution with minimum infeasibility 443 | idx_pick = np.argmin(infeas) 444 | 445 | # Store values we are interested in 446 | result = {} 447 | result['x'] = x[idx_pick] 448 | result['time'] = np.sum(time) 449 | result['strategy'] = strategies[idx_pick] 450 | result['cost'] = cost[idx_pick] 451 | result['infeasibility'] = infeas[idx_pick] 452 | 453 | return result 454 | 455 | def solve(self, X, 456 | message="Predict optimal solution", 457 | use_cache=True, 458 | verbose=False, 459 | ): 460 | """ 461 | Predict optimal solution given the parameters X. 462 | 463 | Parameters 464 | ---------- 465 | X : pandas DataFrame or Series 466 | Data points. 467 | use_cache : bool, optional 468 | Use solver cache? Defaults to True. 469 | 470 | Returns 471 | ------- 472 | list 473 | List of result dictionaries. 474 | """ 475 | 476 | if isinstance(X, pd.Series): 477 | X = pd.DataFrame(X).transpose() 478 | n_points = len(X) 479 | 480 | if use_cache and not self._solver_cache: 481 | e.warning("Solver cache requested but the cache has " 482 | "not been computed for this problem. " 483 | "Possibly parameters in proble matrices.") 484 | 485 | # Change verbose setting 486 | if verbose: 487 | self._problem.verbose = True 488 | 489 | # Define array of results to return 490 | results = [] 491 | 492 | # Predict best n_best classes for all the points 493 | X_pred = pandas2array(X) 494 | t_start = time() 495 | classes = self._learner.predict(X_pred) 496 | t_predict = (time() - t_start) / n_points # Average predict time 497 | 498 | if n_points > 1: 499 | stg.logger.info(message) 500 | ran = tqdm(range(n_points)) 501 | else: 502 | # Do not print anything if just one point 503 | ran = range(n_points) 504 | 505 | for i in ran: 506 | 507 | # Populate problem with i-th data point 508 | self._problem.populate(X.iloc[i]) 509 | problem_data = self._problem._get_problem_data() 510 | results.append(self.choose_best(problem_data, 511 | classes[i, :], 512 | use_cache=use_cache)) 513 | 514 | # Append predict time 515 | for r in results: 516 | r['pred_time'] = t_predict 517 | r['solve_time'] = r['time'] 518 | r['time'] = r['pred_time'] + r['solve_time'] 519 | 520 | if len(results) == 1: 521 | results = results[0] 522 | 523 | return results 524 | 525 | def save(self, file_name, delete_existing=False): 526 | """ 527 | Save optimizer to a specific tar.gz file. 528 | 529 | Parameters 530 | ---------- 531 | file_name : string 532 | File name of the compressed optimizer. 533 | delete_existing : bool, optional 534 | Delete existing file with the same name? 535 | Defaults to False. 536 | """ 537 | if self._learner is None: 538 | e.value_error("You cannot save the optimizer without " + 539 | "training it before.") 540 | 541 | # Add .tar.gz if the file has no extension 542 | if not file_name.endswith('.tar.gz'): 543 | file_name += ".tar.gz" 544 | 545 | # Check if file already exists 546 | if os.path.isfile(file_name): 547 | if not delete_existing: 548 | p = None 549 | while p not in ['y', 'n', 'N', '']: 550 | p = input("File %s already exists. " % file_name + 551 | "Would you like to delete it? [y/N] ") 552 | if p == 'y': 553 | os.remove(file_name) 554 | else: 555 | return 556 | else: 557 | os.remove(file_name) 558 | 559 | # Create temporary directory to create the archive 560 | # and store relevant files 561 | with tempfile.TemporaryDirectory() as tmpdir: 562 | 563 | # Save learner 564 | self._learner.save(os.path.join(tmpdir, "learner")) 565 | 566 | # Save optimizer 567 | with open(os.path.join(tmpdir, "optimizer.pkl"), 'wb') \ 568 | as optimizer: 569 | file_dict = {'_problem': self._problem, 570 | # '_solver_cache': self._solver_cache, # Cannot pickle 571 | 'learner_name': self._learner.name, 572 | 'learner_options': self._learner.options, 573 | 'learner_best_params': self._learner.best_params, 574 | 'encoding': self.encoding 575 | } 576 | pkl.dump(file_dict, optimizer) 577 | 578 | # Create archive with the files 579 | tar = tarfile.open(file_name, "w:gz") 580 | for f in glob(os.path.join(tmpdir, "*")): 581 | tar.add(f, os.path.basename(f)) 582 | tar.close() 583 | 584 | @classmethod 585 | def from_file(cls, file_name): 586 | """ 587 | Create optimizer from a specific compressed tar.gz file. 588 | 589 | Parameters 590 | ---------- 591 | file_name : string 592 | File name of the exported optimizer. 593 | """ 594 | 595 | # Add .tar.gz if the file has no extension 596 | if not file_name.endswith('.tar.gz'): 597 | file_name += ".tar.gz" 598 | 599 | # Check if file exists 600 | if not os.path.isfile(file_name): 601 | e.value_error("File %s does not exist." % file_name) 602 | 603 | # Extract file to temporary directory and read it 604 | with tempfile.TemporaryDirectory() as tmpdir: 605 | with tarfile.open(file_name) as tar: 606 | tar.extractall(path=tmpdir) 607 | 608 | # Load optimizer 609 | optimizer_file_name = os.path.join(tmpdir, "optimizer.pkl") 610 | if not optimizer_file_name: 611 | e.value_error("Optimizer pkl file does not exist.") 612 | with open(optimizer_file_name, "rb") as f: 613 | optimizer_dict = pkl.load(f) 614 | 615 | name = optimizer_dict.get('name', 'problem') 616 | 617 | # Create optimizer using loaded dict 618 | problem = optimizer_dict['_problem'].cvxpy_problem 619 | optimizer = cls(problem, name=name) 620 | 621 | # Assign strategies encoding 622 | optimizer.encoding = optimizer_dict['encoding'] 623 | optimizer._sampler = optimizer_dict.get('_sampler', None) 624 | 625 | # Load learner 626 | learner_name = optimizer_dict['learner_name'] 627 | learner_options = optimizer_dict['learner_options'] 628 | learner_best_params = optimizer_dict['learner_best_params'] 629 | optimizer._learner = \ 630 | LEARNER_MAP[learner_name](n_input=optimizer.n_parameters, 631 | n_classes=len(optimizer.encoding), 632 | **learner_options) 633 | optimizer._learner.best_params = learner_best_params 634 | optimizer._learner.load(os.path.join(tmpdir, "learner")) 635 | 636 | return optimizer 637 | 638 | def performance(self, theta, 639 | results_test=None, 640 | results_heuristic=None, 641 | parallel=False, 642 | use_cache=True): 643 | """ 644 | Evaluate optimizer performance on data theta by comparing the 645 | solution to the optimal one. 646 | 647 | Parameters 648 | ---------- 649 | theta : DataFrame 650 | Data to predict. 651 | parallel : bool, optional 652 | Solve problems in parallel? Defaults to True. 653 | 654 | Returns 655 | ------- 656 | dict 657 | Results summarty. 658 | dict 659 | Detailed results summary. 660 | """ 661 | 662 | stg.logger.info("Performance evaluation") 663 | 664 | if results_test is None: 665 | # Get strategy for each point 666 | results_test = self._problem.solve_parametric( 667 | theta, parallel=parallel, message="Compute " + 668 | "tight constraints " + 669 | "for test set") 670 | 671 | if results_heuristic is None: 672 | # self._problem.solver_options['MIPGap'] = 0.1 # 10% MIP Gap 673 | # Focus on feasibility 674 | self._problem.solver_options['MIPFocus'] = 1 675 | 676 | # Limit time to one second 677 | self._problem.solver_options['TimeLimit'] = 1. 678 | 679 | # Get strategy for each point 680 | results_heuristic = self._problem.solve_parametric( 681 | theta, parallel=parallel, message="Compute " + 682 | "tight constraints " + 683 | "with heuristic Gurobi " + 684 | "for test set") 685 | 686 | # Remove options 687 | self._problem.solver_options.pop('MIPFocus') 688 | self._problem.solver_options.pop('TimeLimit') 689 | 690 | time_test = [r['time'] for r in results_test] 691 | cost_test = [r['cost'] for r in results_test] 692 | 693 | time_heuristic = [r['time'] for r in results_heuristic] 694 | cost_heuristic = [r['cost'] for r in results_heuristic] 695 | 696 | # Get predicted strategy for each point 697 | results_pred = self.solve(theta, 698 | message="Predict tight constraints for " + 699 | "test set", 700 | use_cache=use_cache) 701 | time_pred = [r['time'] for r in results_pred] 702 | solve_time_pred = [r['solve_time'] for r in results_pred] 703 | pred_time_pred = [r['pred_time'] for r in results_pred] 704 | cost_pred = [r['cost'] for r in results_pred] 705 | infeas = np.array([r['infeasibility'] for r in results_pred]) 706 | 707 | n_test = len(theta) 708 | n_train = self._learner.n_train # Number of training samples 709 | n_theta = n_features(theta) # Number of parameters 710 | n_strategies = len(self.encoding) # Number of strategies 711 | if hasattr(self, "encoding_full"): 712 | n_unpruned_strategies = len(self.encoding_full) 713 | else: 714 | n_unpruned_strategies = n_strategies 715 | 716 | # Compute comparative statistics 717 | time_comp = np.array([time_test[i] / time_pred[i] 718 | for i in range(n_test)]) 719 | 720 | time_comp_heuristic = np.array([time_heuristic[i] / time_pred[i] 721 | for i in range(n_test)]) 722 | 723 | subopt = np.array([suboptimality(cost_pred[i], cost_test[i], 724 | self._problem.sense()) 725 | for i in range(n_test)]) 726 | 727 | subopt_real = subopt[np.where(infeas <= stg.INFEAS_TOL)[0]] 728 | if any(subopt_real): 729 | max_subopt = np.max(subopt_real) 730 | avg_subopt = np.mean(subopt_real) 731 | std_subopt = np.std(subopt_real) 732 | else: 733 | max_subopt = np.nan 734 | avg_subopt = np.nan 735 | std_subopt = np.nan 736 | 737 | subopt_heuristic = np.array([suboptimality(cost_heuristic[i], 738 | cost_test[i], 739 | self._problem.sense()) 740 | for i in range(n_test)]) 741 | 742 | # accuracy 743 | test_accuracy, idx_correct = accuracy(results_pred, results_test, 744 | self._problem.sense()) 745 | 746 | # Create dataframes to return 747 | df = pd.Series( 748 | { 749 | "problem": self.name, 750 | "learner": self._learner.name, 751 | "n_best": self._learner.options['n_best'], 752 | "n_var": self._problem.n_var, 753 | "n_constr": self._problem.n_constraints, 754 | "n_test": n_test, 755 | "n_train": n_train, 756 | "n_theta": n_theta, 757 | "good_turing": self._sampler.good_turing, 758 | "good_turing_smooth": self._sampler.good_turing_smooth, 759 | "n_correct": np.sum(idx_correct), 760 | "n_strategies_unpruned": n_unpruned_strategies, 761 | "n_strategies": n_strategies, 762 | "accuracy": 100 * test_accuracy, 763 | "n_infeas": np.sum(infeas >= stg.INFEAS_TOL), 764 | "avg_infeas": np.mean(infeas), 765 | "std_infeas": np.std(infeas), 766 | "max_infeas": np.max(infeas), 767 | "avg_subopt": avg_subopt, 768 | "std_subopt": std_subopt, 769 | "max_subopt": max_subopt, 770 | "avg_subopt_heuristic": np.mean(subopt_heuristic), 771 | "std_subopt_heuristic": np.std(subopt_heuristic), 772 | "max_subopt_heuristic": np.max(subopt_heuristic), 773 | "mean_solve_time_pred": np.mean(solve_time_pred), 774 | "std_solve_time_pred": np.std(solve_time_pred), 775 | "mean_pred_time_pred": np.mean(pred_time_pred), 776 | "std_pred_time_pred": np.std(pred_time_pred), 777 | "mean_time_pred": np.mean(time_pred), 778 | "std_time_pred": np.std(time_pred), 779 | "max_time_pred": np.max(time_pred), 780 | "mean_time_full": np.mean(time_test), 781 | "std_time_full": np.std(time_test), 782 | "max_time_full": np.max(time_test), 783 | "mean_time_heuristic": np.mean(time_heuristic), 784 | "std_time_heuristic": np.std(time_heuristic), 785 | "max_time_heuristic": np.max(time_heuristic), 786 | } 787 | ) 788 | 789 | df_detail = pd.DataFrame( 790 | { 791 | "problem": [self.name] * n_test, 792 | "learner": [self._learner.name] * n_test, 793 | "n_best": [self._learner.options['n_best']] * n_test, 794 | "correct": idx_correct, 795 | "infeas": infeas, 796 | "subopt": subopt, 797 | "solve_time_pred": solve_time_pred, 798 | "pred_time_pred": pred_time_pred, 799 | "time_pred": time_pred, 800 | "time_full": time_test, 801 | "time_heuristic": time_heuristic, 802 | "time_improvement": time_comp, 803 | "time_improvement_heuristic": time_comp_heuristic, 804 | } 805 | ) 806 | 807 | return df, df_detail 808 | -------------------------------------------------------------------------------- /mlopt/problem.py: -------------------------------------------------------------------------------- 1 | from joblib import Parallel, delayed 2 | import numpy as np 3 | # Mlopt stuff 4 | from mlopt.strategy import Strategy 5 | import mlopt.settings as stg 6 | from mlopt.kkt import KKTSolver 7 | import mlopt.utils as u 8 | import mlopt.error as e 9 | # Import cvxpy and constraint types 10 | import cvxpy as cp 11 | import cvxpy.settings as cps 12 | from cvxpy.reductions.solvers.defines import INSTALLED_SOLVERS 13 | from cvxpy.reductions.solvers.solving_chain import SolvingChain 14 | # Progress bars 15 | from tqdm.auto import tqdm 16 | 17 | 18 | # DEBUG 19 | import pickle 20 | def is_pickleable(obj): 21 | try: 22 | pickle.dumps(obj) 23 | except (pickle.PicklingError, TypeError): 24 | return False 25 | return True 26 | 27 | 28 | def populate_and_solve(problem, theta): 29 | """Single function to populate the problem with 30 | theta and solve it with the solver. 31 | Useful for multiprocessing.""" 32 | problem.populate(theta) 33 | results = problem.solve() 34 | 35 | return results 36 | 37 | 38 | class Problem(object): 39 | 40 | def __init__(self, 41 | cvxpy_problem, 42 | solver=stg.DEFAULT_SOLVER, 43 | verbose=False, 44 | **solver_options): 45 | """ 46 | Initialize optimization problem. 47 | 48 | 49 | Parameters 50 | ---------- 51 | problem : cvxpy.Problem 52 | CVXPY problem. 53 | solver : str, optional 54 | Solver to solve internal problem. Defaults to DEFAULT_SOLVER. 55 | solver_options : dict, optional 56 | A dict of options for the internal solver. 57 | """ 58 | # Assign solver 59 | self.solver = solver 60 | self.verbose = verbose 61 | 62 | # Define problem 63 | if not cvxpy_problem.is_dcp(): 64 | e.value_error("CVXPY Problem is not DCP") 65 | 66 | if not cvxpy_problem.is_qp(): 67 | e.value_error("MLOPT supports only MIQP-based problems " + 68 | "LP/QP/MILP/MIQP") 69 | 70 | self.cvxpy_problem = cvxpy_problem 71 | 72 | # Canonicalize problem 73 | self._canonicalize() 74 | 75 | # Check if parameters in matrices (do it only once) 76 | self._parameters_in_matrices = self.check_parameters_in_matrices() 77 | 78 | self._x = None # Raw solution 79 | 80 | # Add default solver options to solver options 81 | if solver == stg.DEFAULT_SOLVER: 82 | solver_options.update(stg.DEFAULT_SOLVER_OPTIONS) 83 | 84 | # Set options 85 | self.solver_options = solver_options 86 | 87 | # # Set solver cache 88 | # self._solver_cache = None 89 | 90 | def _canonicalize(self): 91 | """Canonicalize optimizaton problem. 92 | It constructs CVXPY solving chains. 93 | """ 94 | data, solving_chain, inverse_data = \ 95 | self.cvxpy_problem.get_problem_data(self.solver, enforce_dpp=True) 96 | 97 | # Cache contains 98 | # - solving_chain 99 | # - inverse_data (not solver_inverse_data) 100 | # - param_prog (parametric_program) 101 | self._cache = self.cvxpy_problem._cache 102 | self._data = data 103 | 104 | def sense(self): 105 | return type(self.cvxpy_problem.objective) 106 | 107 | @property 108 | def solver(self): 109 | """Internal optimization solver""" 110 | return self._solver 111 | 112 | @solver.setter 113 | def solver(self, s): 114 | """Set internal solver.""" 115 | if s not in INSTALLED_SOLVERS: 116 | e.value_error('Solver %s not installed.' % s) 117 | self._solver = s 118 | 119 | @property 120 | def n_var(self): 121 | """Number of variables""" 122 | return self._cache.param_prog.x.size 123 | 124 | @property 125 | def n_constraints(self): 126 | """Number of constraints""" 127 | return self._cache.param_prog.constr_size 128 | 129 | @property 130 | def parameters(self): 131 | """Problem parameters.""" 132 | return self._cache.param_prog.parameters 133 | 134 | def variables(self): 135 | """Problem variables.""" 136 | return self._cache.param_prog.variables 137 | 138 | @property 139 | def n_parameters(self): 140 | """Number of parameters.""" 141 | return sum([x.size for x in self.parameters]) 142 | 143 | def populate(self, theta): 144 | """ 145 | Populate problem using parameter theta 146 | """ 147 | for p in self.parameters: 148 | p.value = theta[p.name()] 149 | 150 | @property 151 | def objective(self): 152 | """Inner problem objective""" 153 | return self.cvxpy_problem.objective 154 | 155 | @property 156 | def constraints(self): 157 | """Inner problem constraints""" 158 | return self.cvxpy_problem.constraints 159 | 160 | def cost(self): 161 | """Compute cost function value""" 162 | return self.objective.value 163 | 164 | def is_mip(self): 165 | """Check if problem has integer variables.""" 166 | return self.cvxpy_problem.is_mixed_integer() 167 | 168 | def is_qp(self): 169 | """Is problem QP representable (LP/QP/MILP/MIQP)""" 170 | return self.cvxpy_problem.is_qp() 171 | 172 | @property 173 | def parameters_in_matrices(self): 174 | """Do we have problem parameters in matrices or only in vectors? 175 | Returns: TODO 176 | 177 | """ 178 | return self._parameters_in_matrices 179 | 180 | def make_serializable(self): 181 | """ 182 | Remove unpickleable solver-specific elements 183 | Example: Gurobi model 184 | """ 185 | if self.cvxpy_problem._solution is not None: 186 | if cps.EXTRA_STATS in self.cvxpy_problem._solution.attr: 187 | self.cvxpy_problem._solution.attr.pop(cps.EXTRA_STATS) 188 | if self.cvxpy_problem._solver_cache is not None: 189 | self.cvxpy_problem._solver_cache = {} 190 | if hasattr(self.cvxpy_problem._solver_stats, "extra_stats"): 191 | del self.cvxpy_problem._solver_stats.extra_stats 192 | 193 | def check_parameters_in_matrices(self): 194 | """Check if parameters are in matrices. 195 | 196 | Cvxpy works by applying a mapping to the parameter vector such that 197 | 198 | .. code :: 199 | 200 | M_A @ (theta, 1) = vec([A | b]) 201 | M_P @ (theta, 1) = vec(P) 202 | 203 | Instead of calculating the full mapping :code:`M_A` and :code:`M_P`, 204 | we use :code:`reduced_A` and :code:`reduced_P`. See `the cvxpy source 205 | `_. 207 | 208 | We then reshape the results :code:`vec([A | b])` and :code:`vec(P)` 209 | into :code:`[A | b]` and :code:`P` by using the indices and 210 | 211 | 212 | Returns: 213 | True if parameters appear in the matrices :code:`A` or :code:`P`. 214 | False otherwise. 215 | 216 | """ 217 | 218 | param_prog = self._cache.param_prog 219 | 220 | # Check [A | b] 221 | M_A = param_prog.A 222 | n_row_A, n_col_A = M_A.shape 223 | n_con = param_prog.constr_size 224 | 225 | # -1 to ignore the (theta, 1) offset column 226 | for idx in range(n_col_A - 1): 227 | col_start = M_A.indptr[idx] 228 | col_end = M_A.indptr[idx+1] 229 | indices = M_A.indices[col_start:col_end] 230 | # Allow only elements in last row representing constraint vector 231 | if any(indices < n_row_A - n_con): 232 | return True 233 | 234 | # Check P 235 | M_P = param_prog.P.tocsc() 236 | n_row_P, n_col_P = M_P.shape 237 | # -1 to ignore the (theta, 1) offset column 238 | for idx in range(n_col_P - 1): 239 | col_start = M_P.indptr[idx] 240 | col_end = M_P.indptr[idx+1] 241 | indices = M_P.indices[col_start:col_end] 242 | if any(indices): # Any element => parameters in P 243 | return True 244 | 245 | return False 246 | 247 | def infeasibility(self, x, data): 248 | """Compute infeasibility for variables given internally stored solution. 249 | NB. Using conditions similar to: 250 | https://docs.mosek.com/9.0/pythonfusion/solving-conic.html#interior-point-termination-criterion 251 | TODO: Make it independent from the problem. 252 | 253 | Args: 254 | x (TODO): TODO 255 | data (TODO): TODO 256 | 257 | Returns: TODO 258 | 259 | """ 260 | 261 | A, b = data[cps.A], data[cps.B] 262 | F, g = data[cps.F], data[cps.G] 263 | 264 | eq_viol, ineq_viol = 0, 0 265 | if A.size: 266 | eq_viol = np.linalg.norm(A.dot(x) - b, np.inf) 267 | eq_viol /= 1 + np.linalg.norm(b, np.inf) 268 | if F.size: 269 | ineq_viol = np.linalg.norm(np.maximum(F.dot(x) - g, 0), np.inf) 270 | ineq_viol /= 1 + np.linalg.norm(g, np.inf) 271 | 272 | return np.maximum(eq_viol, ineq_viol) 273 | 274 | def _get_problem_data(self): 275 | """TODO: Docstring for _get_problem_data. 276 | Returns: TODO 277 | 278 | """ 279 | cache = self._cache 280 | solving_chain = cache.solving_chain 281 | inverse_data = cache.inverse_data 282 | param_prog = cache.param_prog 283 | 284 | # Compute raw solution using parametric program 285 | data, solver_inverse_data = solving_chain.solver.apply(param_prog) 286 | inverse_data = inverse_data + [solver_inverse_data] 287 | 288 | return data, inverse_data, solving_chain 289 | 290 | def solve(self, problem_data=None, solver_data=None, 291 | strategy=None, cache=None): 292 | """Solve optimization problem. 293 | 294 | Kwargs: 295 | solver (string): Solver to use. Defaults to 296 | strategy (Strategy): Strategy to apply. Default none. 297 | cache (dict): KKT solver cache 298 | 299 | Returns: Dictionary of results 300 | 301 | """ 302 | 303 | if problem_data is None: 304 | data, inverse_data, solving_chain = self._get_problem_data() 305 | else: 306 | data, inverse_data, solving_chain = problem_data 307 | 308 | if strategy is not None: 309 | if not strategy.accepts(data): 310 | e.value_error("Strategy incompatible for current problem") 311 | 312 | if strategy is not None: 313 | strategy.apply(data, inverse_data[-1]) 314 | solving_chain = \ 315 | SolvingChain(problem=self.cvxpy_problem, 316 | reductions=solving_chain.reductions[:-1] + 317 | [KKTSolver()]) 318 | solver_options = {} 319 | else: 320 | solver_options = self.solver_options 321 | cache = self.cvxpy_problem._solver_cache 322 | 323 | raw_solution = solving_chain.solver.solve_via_data( 324 | data, warm_start=True, verbose=self.verbose, 325 | solver_opts=solver_options, 326 | solver_cache=cache 327 | ) 328 | 329 | return self._parse_solution(raw_solution, data, self.cvxpy_problem, 330 | solving_chain, inverse_data) 331 | 332 | def _parse_solution(self, raw_solution, data, problem, 333 | solving_chain, inverse_data): 334 | """TODO: Docstring for _parse_solution. 335 | 336 | Args: 337 | raw_solution (TODO): TODO 338 | data (TODO): TODO 339 | problem (TODO): TODO 340 | solving_chain (TODO): TODO 341 | inverse_data (TODO): TODO 342 | 343 | Returns: TODO 344 | 345 | """ 346 | 347 | # Unpack raw solution 348 | self.cvxpy_problem.unpack_results(raw_solution, solving_chain, 349 | inverse_data) 350 | 351 | results = {} 352 | 353 | # Get time and status 354 | results['time'] = problem.solver_stats.solve_time 355 | results['status'] = problem.status 356 | results['cost'] = np.inf # Initialize infinite cost 357 | 358 | if results['status'] in cp.settings.SOLUTION_PRESENT: 359 | # Get raw solution 360 | # Invert solver solution and get raw x! 361 | solver = solving_chain.solver 362 | solver_solution = solver.invert(raw_solution, inverse_data[-1]) 363 | x = solver_solution.primal_vars[solver.VAR_ID] 364 | results['x'] = x 365 | results['cost'] = self.cvxpy_problem.objective.value 366 | results['infeasibility'] = self.infeasibility(x, data) 367 | results['strategy'] = Strategy(x, data) 368 | else: 369 | results['x'] = np.nan * np.ones(self.n_var) 370 | results['cost'] = np.inf 371 | results['infeasibility'] = np.inf 372 | results['strategy'] = None 373 | 374 | return results 375 | 376 | def solve_parametric(self, theta, 377 | batch_size=stg.JOBLIB_BATCH_SIZE, 378 | parallel=True, # Solve problems in parallel 379 | message="Solving for all theta", 380 | ): 381 | """ 382 | Solve parametric problems for each value of theta. 383 | 384 | Parameters 385 | ---------- 386 | theta : DataFrame 387 | Parameter values. 388 | parallel : bool, optional 389 | Solve problems in parallel. Default True. 390 | message : str, optional 391 | Message to be printed on progress bar. 392 | 393 | Returns 394 | ------- 395 | dict 396 | Results dictionary. 397 | """ 398 | n = len(theta) # Number of points 399 | 400 | n_jobs = u.get_n_processes() if parallel else 1 401 | stg.logger.info(message + " (n_jobs = %d)" % n_jobs) 402 | 403 | # Remove unpickleable objects 404 | self.make_serializable() 405 | 406 | results = Parallel(n_jobs=n_jobs, batch_size=batch_size)( 407 | delayed(populate_and_solve)(self, theta.iloc[i]) 408 | for i in tqdm(range(n)) 409 | ) 410 | 411 | # Remove unpickleable objects for storage 412 | self.make_serializable() 413 | 414 | return results 415 | -------------------------------------------------------------------------------- /mlopt/sampling.py: -------------------------------------------------------------------------------- 1 | from joblib import Parallel, delayed 2 | import numpy as np 3 | from scipy.special import gammainc 4 | import pandas as pd 5 | from mlopt.strategy import encode_strategies 6 | import mlopt.settings as stg 7 | import mlopt.utils as u 8 | from tqdm import tqdm 9 | 10 | 11 | def count_occurrences(labels, i): 12 | return len(np.where(labels == i)[0]) 13 | 14 | 15 | class Sampler(object): 16 | """ 17 | Optimization problem sampler. 18 | 19 | Parameters 20 | ---------- 21 | 22 | """ 23 | 24 | def __init__(self, 25 | problem, 26 | sampling_fn=None, 27 | n_samples_iter=5000, 28 | n_samples_strategy=200, 29 | max_iter=int(1e2), 30 | alpha=0.99, 31 | n_samples=0): 32 | self.problem = problem # Optimization problem 33 | self.sampling_fn = sampling_fn 34 | self.n_samples_iter = n_samples_iter 35 | self.n_samples_strategy = n_samples_strategy 36 | self.max_iter = max_iter 37 | self.alpha = alpha 38 | self.n_samples = n_samples # Initialize numer of samples 39 | self.good_turing_smooth = 1. # Initialize Good Turing estimator 40 | 41 | def frequencies(self, labels, batch_size=stg.JOBLIB_BATCH_SIZE, 42 | n_jobs=1): 43 | """ 44 | Get frequency for each unique strategy 45 | """ 46 | results = Parallel(n_jobs=n_jobs, batch_size=batch_size)( 47 | delayed(count_occurrences)(labels, i) 48 | for i in tqdm(np.unique(labels))) 49 | 50 | return np.array(results) 51 | 52 | def compute_good_turing(self, labels, 53 | batch_size=stg.JOBLIB_BATCH_SIZE, 54 | parallel=True): 55 | """Compute good turing estimator""" 56 | stg.logger.info("Computing Good Turing Estimator") 57 | 58 | n_jobs = u.get_n_processes() if parallel else 1 59 | 60 | stg.logger.info("Compute frequencies") 61 | # Get frequencies 62 | freq = self.frequencies(labels, batch_size, n_jobs=n_jobs) 63 | 64 | # Check if there are labels appearing only once 65 | if not any(np.where(freq == 1)[0]): 66 | stg.logger.info("No labels appearing only once") 67 | n1 = 0 68 | # n1 = np.inf 69 | else: 70 | stg.logger.info("Compute frequencies of frequencies") 71 | # Get frequency of frequencies 72 | freq_freq = self.frequencies(freq, batch_size=batch_size, n_jobs=n_jobs) 73 | n1 = freq_freq[0] 74 | 75 | # Get Good Turing estimator 76 | self.good_turing = n1/self.n_samples 77 | 78 | # Get Good Turing estimator 79 | self.good_turing_smooth = self.alpha * n1/self.n_samples + \ 80 | (1 - self.alpha) * self.good_turing_smooth 81 | 82 | def sample(self, parallel=True, epsilon=stg.SAMPLING_TOL, beta=1e-05): 83 | """ 84 | Iterative sampling. 85 | """ 86 | 87 | stg.logger.info("Iterative sampling") 88 | 89 | # Initialize dataframes 90 | theta = pd.DataFrame() 91 | s_theta = [] 92 | obj_theta = [] 93 | 94 | # Start with 100 samples 95 | for self.niter in range(self.max_iter): 96 | # Sample new points 97 | theta_new = self.sampling_fn(self.n_samples_iter) 98 | results = self.problem.solve_parametric(theta_new, 99 | parallel=parallel) 100 | s_theta_new = [r['strategy'] for r in results] 101 | obj_theta_new = [r['cost'] for r in results] 102 | theta = theta.append(theta_new, ignore_index=True) 103 | s_theta += s_theta_new 104 | obj_theta += obj_theta_new 105 | self.n_samples += self.n_samples_iter 106 | 107 | # Get unique strategies 108 | labels, encoding = encode_strategies(s_theta) 109 | 110 | # Get Good Turing Estimator 111 | self.compute_good_turing(labels) 112 | 113 | stg.logger.info("i: %d, gt: %.2e, gt smooth: %.2e, n: %d " % 114 | (self.niter+1, self.good_turing, 115 | self.good_turing_smooth, 116 | self.n_samples)) 117 | 118 | if (self.good_turing_smooth < epsilon): 119 | 120 | # Compute number of strategies 121 | n_strategies = len(encoding) 122 | 123 | # Compute ideal number of strategies 124 | n_samples_ideal = self.n_samples_strategy * n_strategies 125 | n_samples_todo = \ 126 | np.maximum(n_samples_ideal - self.n_samples, 0) 127 | 128 | if n_samples_todo > 0: 129 | # Sample new points 130 | theta_new = self.sampling_fn(n_samples_todo) 131 | results = self.problem.solve_parametric(theta_new, parallel=parallel) 132 | s_theta_new = [r['strategy'] for r in results] 133 | obj_theta_new = [r['cost'] for r in results] 134 | theta = theta.append(theta_new, ignore_index=True) 135 | s_theta += s_theta_new 136 | obj_theta += obj_theta_new 137 | self.n_samples += n_samples_todo 138 | 139 | # Get unique strategies 140 | labels, encoding = encode_strategies(s_theta) 141 | 142 | # Get Good Turing Estimator 143 | self.compute_good_turing(labels) 144 | 145 | break 146 | 147 | # # Get bound from theory 148 | # c = 2 * np.sqrt(2) + np.sqrt(3) 149 | # bound = good_turing_est 150 | # bound += c * np.sqrt((1 / n_samples) * np.log(3 / beta)) 151 | # print("Bound ", bound) 152 | # if bound < epsilon: 153 | # break 154 | 155 | return theta, labels, obj_theta, encoding 156 | 157 | 158 | def sample_around_points(df, 159 | n_total=10000, 160 | radius={}): 161 | """ 162 | Sample around points provided in the dataframe for a total of 163 | n_total points. We sample each parameter using a uniform 164 | distribution over a ball centered at the point in df row. 165 | """ 166 | n_samples_per_point = np.round(n_total / len(df), decimals=0).astype(int) 167 | 168 | df_samples = pd.DataFrame() 169 | 170 | for idx, row in df.iterrows(): 171 | df_row = pd.DataFrame() 172 | 173 | # For each column sample points and create series 174 | for col in df.columns: 175 | 176 | if col in radius: 177 | rad = radius[col] 178 | else: 179 | rad = 0.1 180 | 181 | samples = uniform_sphere_sample(row[col], rad, 182 | n=n_samples_per_point).tolist() 183 | if len(samples[0]) == 1: 184 | # Flatten list 185 | samples = [item for sublist in samples for item in sublist] 186 | 187 | df_row[col] = samples 188 | 189 | df_samples = df_samples.append(df_row) 190 | 191 | return df_samples 192 | 193 | 194 | def uniform_sphere_sample(center, radius, n=1): 195 | """ 196 | Generate a single vector sample to x 197 | 198 | The function initially samples the points using a Normal Distribution with 199 | `randn`. Then the incomplete gamma function is used to map the points 200 | radially to fit in the hypersphere of finite radius r with a uniform 201 | spatial distribution. 202 | 203 | In order to have a uniform distributions over the sphere we multiply the 204 | vectors by f(radius): f(radius)*radius is distributed with density 205 | proportional to radius^n on 206 | [0,1]. 207 | 208 | Parameters 209 | ---------- 210 | center : numpy array 211 | Center of the sphere. 212 | radius : float 213 | Radius of the sphere. 214 | n : int, optional 215 | Number of samples. Default 1. 216 | 217 | Returns 218 | ------- 219 | numpy array : 220 | Array of points with dimension m x n. n is the number of points and 221 | m the dimension. 222 | """ 223 | center = np.atleast_1d(center) 224 | n_dim = len(center) 225 | x = np.random.normal(size=(n, n_dim)) 226 | ssq = np.sum(x ** 2, axis=1) 227 | fr = radius * gammainc(n_dim / 2, ssq / 2) ** (1 / n_dim) / np.sqrt(ssq) 228 | frtiled = np.tile(fr.reshape(n, 1), (1, n_dim)) 229 | p = center + np.multiply(x, frtiled) 230 | return p 231 | 232 | # Debug: plot points 233 | # from matplotlib import pyplot as plt 234 | # fig1 = plt.figure(1) 235 | # ax1 = fig1.gca() 236 | # center = np.array([0, 0]) 237 | # radius = 1 238 | # p = uniform_sphere_sample(center, radius, 10000) 239 | # ax1.scatter(p[:, 0], p[:, 1], s=0.5) 240 | # ax1.add_artist(plt.Circle(center, radius, fill=False, color="0.5")) 241 | # ax1.set_xlim(-1.5, 1.5) 242 | # ax1.set_ylim(-1.5, 1.5) 243 | # ax1.set_aspect("equal") 244 | # plt.show() 245 | -------------------------------------------------------------------------------- /mlopt/settings.py: -------------------------------------------------------------------------------- 1 | import cvxpy as cp 2 | 3 | # Logger 4 | import logging 5 | import sys 6 | LOGGER_NAME = 'mlopt' 7 | logger = logging.getLogger(LOGGER_NAME) 8 | logger.setLevel(logging.INFO) 9 | 10 | # Stdout handler 11 | stdout_handler = logging.StreamHandler(sys.stdout) 12 | stdout_formatter = logging.Formatter('%(message)s') 13 | logger.addHandler(stdout_handler) 14 | logger.propagate = False # Disable double logging 15 | 16 | # Add file handler 17 | # file_handler = logging.FileHandler('mlopt.log') 18 | # file_handler.setLevel(logging.INFO) 19 | # logger.addHandler(file_handler) 20 | 21 | 22 | # Parallel 23 | JOBLIB_BATCH_SIZE = 'auto' 24 | # JOBLIB_BATCH_SIZE = 2 25 | 26 | 27 | # Define constants 28 | INFEAS_TOL = 1e-04 29 | SUBOPT_TOL = 1e-04 30 | TIGHT_CONSTRAINTS_TOL = 1e-4 31 | DIVISION_TOL = 1e-8 32 | 33 | # Define default solver 34 | DEFAULT_SOLVER = cp.GUROBI 35 | DEFAULT_SOLVER_OPTIONS = {'Method': 1, # Dual simplex 36 | } 37 | 38 | # DEFAULT_SOLVER = cp.CPLEX 39 | # DEFAULT_SOLVER = cp.MOSEK 40 | # DEFAULT_SOLVER = cp.ECOS 41 | 42 | # Define learners 43 | PYTORCH = "pytorch" 44 | TENSORFLOW = "tensorflow" 45 | OPTIMAL_TREE = "optimaltree" 46 | XGBOOST = "xgboost" 47 | DEFAULT_LEARNER = XGBOOST 48 | 49 | # Learners settings 50 | N_BEST = 10 51 | N_TRAIN_TRIALS = 300 52 | FRAC_TRAIN = 0.8 # Fraction dividing training and validation 53 | 54 | # Sampling 55 | SAMPLING_TOL = 5e-03 56 | 57 | # Filtering 58 | FILTER_STRATEGIES = True 59 | FILTER_STRATEGIES_SAMPLES_FRACTION = 0.80 60 | FILTER_SUBOPT = 2e-01 61 | FILTER_MAX_ITER = 10 62 | -------------------------------------------------------------------------------- /mlopt/strategy.py: -------------------------------------------------------------------------------- 1 | from joblib import Parallel, delayed 2 | import numpy as np 3 | import mlopt.settings as stg 4 | import mlopt.error as e 5 | import mlopt.utils as u 6 | import cvxpy.settings as cps 7 | import scipy.sparse as spa 8 | from time import time 9 | from tqdm import tqdm 10 | 11 | 12 | class Strategy(object): 13 | """ 14 | Solving strategy. 15 | 16 | Parameters 17 | ---------- 18 | tight_constraints : numpy bool array 19 | Set of tight constraints. The values are numpy bool arrays 20 | (True/False for tight/non tight). 21 | int_vars : numpy bool array 22 | Value of the integer variables. The values are numpy int arrays. 23 | """ 24 | 25 | def __init__(self, x, data): 26 | """Initialize strategy from problem data.""" 27 | 28 | self.tight_constraints = self.get_tight_constraints(x, data) 29 | self.int_vars = x[data[cps.INT_IDX]] 30 | 31 | # Store hash for comparisons 32 | self._hash = hash((frozenset(self.tight_constraints), 33 | frozenset(self.int_vars))) 34 | 35 | def get_tight_constraints(self, x, data): 36 | """Compute tight constraints for solution x 37 | 38 | Args: 39 | data (TODO): TODO 40 | x (TODO): TODO 41 | 42 | Returns: TODO 43 | 44 | """ 45 | # Check only inequalities 46 | F, g = data[cps.F], data[cps.G] 47 | 48 | tight_constraints = np.array([], dtype=np.bool) 49 | 50 | # Constraint is tight if ||F * x - g|| <= eps (1 + rel_tol) 51 | if F.size > 0: 52 | tight_constraints = np.abs(F.dot(x) - g) <= \ 53 | stg.TIGHT_CONSTRAINTS_TOL * (1 + np.linalg.norm(g, np.inf)) 54 | 55 | return tight_constraints 56 | 57 | def __hash__(self): 58 | """Overrides default hash implementation""" 59 | return self._hash 60 | 61 | def __eq__(self, other): 62 | """Overrides the default equality implementation""" 63 | if isinstance(other, Strategy): 64 | 65 | if np.any(self.tight_constraints != other.tight_constraints): 66 | return False 67 | 68 | if np.any(self.int_vars != other.int_vars): 69 | return False 70 | 71 | return True 72 | else: 73 | return False 74 | 75 | def accepts(self, data): 76 | """Check if strategy is compatible with current problem. 77 | If not, it raises an error. 78 | 79 | TODO: Add check to see if we match problem type 80 | 81 | Args: 82 | data (TODO): TODO 83 | 84 | """ 85 | 86 | if len(self.tight_constraints) != data['n_ineq']: 87 | e.warn("Tight constraints not compatible with problem. " + 88 | "Different than the number of inequality constraints.") 89 | return False 90 | 91 | if len(self.int_vars) != len(data['int_vars_idx']): 92 | e.warn("Integer variables not compatible " + 93 | "with problem. IDs not " + 94 | "matching an integer variable.") 95 | return False 96 | 97 | return True 98 | 99 | def apply(self, data, inverse_data): 100 | """TODO: Docstring for apply. 101 | 102 | Args: 103 | data (TODO): TODO 104 | inverse_data (TODO): TODO 105 | 106 | Returns: TODO 107 | 108 | """ 109 | n_eq, n_var = data[cps.A].shape 110 | n_ineq = data[cps.F].shape[0] 111 | 112 | # Edit data by increasing the dimension of A 113 | # 1. Fix tight constraints: F_active x = g_active 114 | A_active = data[cps.F][self.tight_constraints] 115 | b_active = data[cps.G][self.tight_constraints] 116 | 117 | # 2. Fix integer variables: F_fix x = g_fix 118 | A_fix = spa.eye(n_var, format='csc')[data[cps.INT_IDX]] 119 | b_fix = self.int_vars 120 | 121 | # Combine in A_ref and b_red 122 | data[cps.A + "_red"] = spa.vstack([data[cps.A], A_active, A_fix]) 123 | data[cps.B + "_red"] = np.concatenate([data[cps.B], b_active, b_fix]) 124 | 125 | # Store inverse data 126 | inverse_data['tight_constraints'] = self.tight_constraints 127 | inverse_data['int_vars'] = self.int_vars 128 | inverse_data['n_eq'] = n_eq 129 | inverse_data['n_ineq'] = n_ineq 130 | 131 | 132 | def unique_strategies(strategies): 133 | """ 134 | Extract unique strategies from array of strategies. 135 | 136 | Parameters 137 | ---------- 138 | strategies : Strategy list 139 | Strategies to be processed. 140 | 141 | Returns 142 | ------- 143 | Strategy set : 144 | Unique strategies. 145 | """ 146 | 147 | # Using set (we must define hash to use this) 148 | unique = list(set(strategies)) 149 | 150 | # Using list 151 | # unique = [] 152 | # # traverse for all elements 153 | # for x in strategies: 154 | # # check if x exists in unique_list or not 155 | # if x not in unique: 156 | # unique.append(x) 157 | 158 | return unique 159 | 160 | 161 | def assign_to_unique_strategy(strategy, unique_strategies): 162 | y = next((index for (index, s) in enumerate(unique_strategies) 163 | if strategy == s), -1) 164 | # y = -1 165 | # n_unique_strategies = len(unique_strategies) 166 | # for j in range(n_unique_strategies): 167 | # if unique_strategies[j] == strategy: 168 | # y = j 169 | # break 170 | if y == -1: 171 | e.value_error("Strategy not found") 172 | return y 173 | 174 | 175 | def encode_strategies(strategies, batch_size=stg.JOBLIB_BATCH_SIZE, 176 | parallel=True): 177 | """ 178 | Encode strategies 179 | 180 | 181 | Parameters 182 | ---------- 183 | strategies : Strategies array 184 | Array of strategies to be encoded. 185 | 186 | Returns 187 | ------- 188 | numpy array 189 | Encodings for each strategy in strategies. 190 | Strategies array 191 | Array of unique strategies. 192 | """ 193 | stg.logger.info("Encoding strategies") 194 | 195 | stg.logger.info("Getting unique set of strategies") 196 | start_time = time() 197 | unique = unique_strategies(strategies) 198 | end_time = time() 199 | stg.logger.info("Extraction time %.3f sec" % (end_time - start_time)) 200 | n_unique_strategies = len(unique) 201 | stg.logger.info("Found %d unique strategies" % n_unique_strategies) 202 | 203 | n_jobs = u.get_n_processes() if parallel else 1 204 | 205 | # Map strategies to number 206 | stg.logger.info("Assign samples to unique strategies (n_jobs = %d)" 207 | % n_jobs) 208 | 209 | results = Parallel(n_jobs=n_jobs, batch_size=batch_size)( 210 | delayed(assign_to_unique_strategy)(s, unique) 211 | for s in tqdm(strategies)) 212 | y = np.array(results) 213 | 214 | return y, unique 215 | 216 | 217 | def strategy2array(s): 218 | """Convert strategy to array""" 219 | return np.concatenate([s.tight_constraints, s.int_vars]) 220 | 221 | 222 | def strategy_distance(a, b): 223 | """Compute manhattan distance between strategy a and b.""" 224 | # Convert strategies to array 225 | a_array = strategy2array(a) 226 | b_array = strategy2array(b) 227 | 228 | return np.linalg.norm(a_array - b_array, 1) / len(a_array) 229 | -------------------------------------------------------------------------------- /mlopt/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bstellato/mlopt/48cb9f8b004b648d0c6bbfb586623a6696ebbcca/mlopt/tests/__init__.py -------------------------------------------------------------------------------- /mlopt/tests/data/afiro.mat: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bstellato/mlopt/48cb9f8b004b648d0c6bbfb586623a6696ebbcca/mlopt/tests/data/afiro.mat -------------------------------------------------------------------------------- /mlopt/tests/settings.py: -------------------------------------------------------------------------------- 1 | TEST_TOL = 1e-06 2 | -------------------------------------------------------------------------------- /mlopt/tests/test_caching.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import cvxpy as cp 3 | import numpy as np 4 | import mlopt 5 | from mlopt.sampling import uniform_sphere_sample 6 | import pandas as pd 7 | import numpy.testing as npt 8 | from mlopt.tests.settings import TEST_TOL as TOL 9 | 10 | 11 | class TestCaching(unittest.TestCase): 12 | 13 | def setUp(self): 14 | """Setup simple problem""" 15 | np.random.seed(1) 16 | # This needs to work for different 17 | p = 10 18 | n = 30 19 | F = np.random.randn(n, p) 20 | D = np.diag(np.random.rand(n)*np.sqrt(p)) 21 | Sigma = F.dot(F.T) + D 22 | gamma = 1.0 23 | mu = cp.Parameter(n, name='mu') 24 | x = cp.Variable(n) 25 | cost = - mu @ x + gamma * cp.quad_form(x, Sigma) + .5 * cp.norm(x, 1) 26 | constraints = [cp.sum(x) == 1, x >= 0] 27 | problem = cp.Problem(cp.Minimize(cost), constraints) 28 | 29 | # Define optimizer 30 | m = mlopt.Optimizer(problem, 31 | # log_level=logging.DEBUG 32 | ) 33 | 34 | ''' 35 | Sample points 36 | ''' 37 | theta_bar = 10 * np.random.rand(n) 38 | radius = 1.0 39 | 40 | ''' 41 | Train and solve 42 | ''' 43 | 44 | # Training and testing data 45 | n_train = 300 46 | n_test = 10 47 | 48 | # Sample points from multivariate ball 49 | X_d = uniform_sphere_sample(theta_bar, radius, n=n_train) 50 | df = pd.DataFrame({'mu': list(X_d)}) 51 | 52 | m.train(df, filter_strategies=True, parallel=False, 53 | learner=mlopt.PYTORCH, n_train_trials=10) 54 | # m.train(df, parallel=False, learner=mlopt.XGBOOST, n_train_trials=10) 55 | 56 | # Testing data 57 | X_d_test = uniform_sphere_sample(theta_bar, radius, n=n_test) 58 | df_test = pd.DataFrame({'mu': list(X_d_test)}) 59 | 60 | # Store stuff 61 | self.m = m 62 | self.df_test = df_test 63 | 64 | def test_solve(self): 65 | """Solve problem with or without caching""" 66 | caching = self.m.solve(self.df_test, use_cache=True) 67 | no_caching = self.m.solve(self.df_test, use_cache=False) 68 | 69 | for i in range(len(self.df_test)): 70 | npt.assert_array_almost_equal(caching[i]['x'], 71 | no_caching[i]['x'], 72 | decimal=TOL) 73 | 74 | # Compare cost 75 | npt.assert_array_almost_equal(caching[i]['cost'], 76 | no_caching[i]['cost'], 77 | decimal=TOL) 78 | -------------------------------------------------------------------------------- /mlopt/tests/test_filter.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import numpy as np 3 | import mlopt 4 | from mlopt.sampling import uniform_sphere_sample 5 | import pandas as pd 6 | import cvxpy as cp 7 | from mlopt.settings import logger 8 | 9 | 10 | class TestFilter(unittest.TestCase): 11 | 12 | def setUp(self): 13 | """Setup simple problem""" 14 | np.random.seed(1) 15 | # This needs to work for different 16 | p = 50 17 | n = 200 18 | F = np.random.randn(n, p) 19 | D = np.diag(np.random.rand(n)*np.sqrt(p)) 20 | Sigma = F.dot(F.T) + D 21 | gamma = 1.0 22 | mu = cp.Parameter(n, name='mu') 23 | x = cp.Variable(n) 24 | cost = - mu @ x + gamma * cp.quad_form(x, Sigma) + .5 * cp.norm(x, 1) 25 | constraints = [cp.sum(x) == 1, x >= 0] 26 | problem = cp.Problem(cp.Minimize(cost), constraints) 27 | 28 | # Define optimizer 29 | # Force mosek to be single threaded 30 | m = mlopt.Optimizer(problem, 31 | # log_level=logging.DEBUG, 32 | ) 33 | 34 | ''' 35 | Sample points 36 | ''' 37 | theta_bar = 10 * np.random.rand(n) 38 | radius = 0.8 39 | 40 | ''' 41 | Train and solve 42 | ''' 43 | 44 | # Training and testing data 45 | n_train = 100 46 | n_test = 10 47 | 48 | # Sample points from multivariate ball 49 | X_d = uniform_sphere_sample(theta_bar, radius, n=n_train) 50 | self.df_train = pd.DataFrame({'mu': list(X_d)}) 51 | 52 | # # Train and test using pytorch 53 | # params = { 54 | # 'learning_rate': [0.01], 55 | # 'batch_size': [100], 56 | # 'n_epochs': [200] 57 | # } 58 | # 59 | # m.train(df, parallel=False, learner=mlopt.PYTORCH, params=params) 60 | 61 | # Testing data 62 | X_d_test = uniform_sphere_sample(theta_bar, radius, n=n_test) 63 | df_test = pd.DataFrame({'mu': list(X_d_test)}) 64 | 65 | # Store stuff 66 | self.m = m 67 | self.df_test = df_test 68 | 69 | def test_filter_simple(self): 70 | 71 | self.m.get_samples(self.df_train, parallel=True, 72 | filter_strategies=False) 73 | logger.info("Number of original strategies %d" % 74 | len(self.m.encoding)) 75 | self.m.filter_strategies(parallel=True) 76 | logger.info("Number of condensed strategies (parallel): %d" % 77 | len(self.m.encoding)) 78 | n_filter_parallel = len(self.m.encoding) 79 | 80 | logger.info("Recompute samples to cleanup filtered ones") 81 | self.m.get_samples(self.df_train, parallel=False, 82 | filter_strategies=False) 83 | self.m.filter_strategies(parallel=False) 84 | logger.info("Number of condensed strategies (serial): %d" % 85 | len(self.m.encoding)) 86 | n_filter_serial = len(self.m.encoding) 87 | 88 | assert len(self.m._filter.encoding_full) >= n_filter_parallel 89 | assert n_filter_serial == n_filter_parallel 90 | -------------------------------------------------------------------------------- /mlopt/tests/test_kkt_solver.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import cvxpy as cp 3 | from cvxpy.error import SolverError 4 | from mlopt.kkt import KKTSolver 5 | from mlopt.tests.settings import TEST_TOL as TOL 6 | from mlopt.strategy import Strategy 7 | import mlopt.settings as stg 8 | import numpy as np 9 | import numpy.testing as npt 10 | import scipy.sparse as spa 11 | 12 | 13 | class TestKKT(unittest.TestCase): 14 | 15 | def setUp(self): 16 | np.random.seed(0) 17 | n = 10 18 | m = 10 19 | P = spa.random(n, n, density=0.5, 20 | data_rvs=np.random.randn, 21 | format='csc') 22 | self.P = P.dot(P.T).tocsc() 23 | self.q = np.random.randn(n) 24 | self.A = spa.random(m, n, density=0.5, 25 | data_rvs=np.random.randn, 26 | format='csc') 27 | self.b = np.random.randn(m) 28 | self.n = n 29 | self.m = m 30 | 31 | def test_solution(self): 32 | """Test solution of KKT solver 33 | compared to another solver 34 | """ 35 | 36 | # Define equality constrained QP 37 | x = cp.Variable(self.n) 38 | prob = cp.Problem(cp.Minimize(cp.quad_form(x, self.P) + self.q.T @ x), 39 | [self.A @ x == self.b]) 40 | obj_solver = prob.solve(solver=stg.DEFAULT_SOLVER) 41 | x_solver = np.copy(x.value) 42 | y_solver = np.copy(prob.constraints[0].dual_value) 43 | 44 | # Solve using KKT solver 45 | data, chain, inverse_data = \ 46 | prob.get_problem_data(solver=stg.DEFAULT_SOLVER) 47 | # Get Strategy 48 | strategy = Strategy(x_solver, data) 49 | 50 | # Apply strategy and solve with KKT solver 51 | strategy.apply(data, inverse_data[-1]) 52 | solver = KKTSolver() 53 | raw_solution = solver.solve_via_data(data, warm_start=True, 54 | verbose=True, solver_opts={}) 55 | inv_solution = solver.invert(raw_solution, inverse_data[-1]) 56 | x_kkt = inv_solution.primal_vars[KKTSolver.VAR_ID] 57 | y_kkt = inv_solution.dual_vars[KKTSolver.DUAL_VAR_ID] 58 | obj_kkt = raw_solution['cost'] 59 | 60 | # Assert matching 61 | npt.assert_almost_equal(x_solver, 62 | x_kkt, 63 | decimal=TOL) 64 | npt.assert_almost_equal(y_solver, 65 | y_kkt, 66 | decimal=TOL) 67 | npt.assert_almost_equal(obj_solver, 68 | obj_kkt, 69 | decimal=TOL) 70 | 71 | # def test_not_applicable(self): 72 | # """ 73 | # Test that it complains if problem is not an equality 74 | # constrained QP. 75 | # """ 76 | # x = cp.Variable(self.n) 77 | # prob = cp.Problem(cp.Minimize(cp.quad_form(x, self.P) + 78 | # self.q.T * x), 79 | # [self.A * x <= self.b]) 80 | # with npt.assert_raises(SolverError): 81 | # prob.solve(solver=KKT) 82 | -------------------------------------------------------------------------------- /mlopt/tests/test_matrix_parameters.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import numpy as np 3 | import cvxpy as cp 4 | import pandas as pd 5 | from mlopt.optimizer import Optimizer 6 | from mlopt.settings import PYTORCH 7 | from mlopt.sampling import uniform_sphere_sample 8 | 9 | 10 | def sample_portfolio(n, k, T=5, N=100): 11 | """Sample portfolio parameters.""" 12 | 13 | rad = 0.01 14 | 15 | # mean values 16 | np.random.seed(0) 17 | SigmaF_FT_bar = np.random.randn(k, n) 18 | sqrt_D_bar = np.random.rand(n) 19 | # Sigma_F_diag_bar = np.random.rand(k) 20 | hat_r_bar = np.random.rand(n) 21 | w_init_bar = np.random.rand(n) 22 | w_init_bar /= np.sum(w_init_bar) 23 | 24 | df = pd.DataFrame() 25 | for i in range(N): 26 | 27 | w_init = uniform_sphere_sample(w_init_bar, rad).flatten() 28 | w_init /= np.sum(w_init) 29 | 30 | SigmaF_FT = uniform_sphere_sample(SigmaF_FT_bar.flatten(), 31 | rad).reshape(k, n) 32 | 33 | x = pd.Series( 34 | { 35 | # "F": F, 36 | "sqrt_D": uniform_sphere_sample(sqrt_D_bar, rad).flatten(), 37 | "SigmaF_FT": SigmaF_FT, 38 | "w_init": w_init, 39 | } 40 | ) 41 | 42 | for t in range(1, T + 1): 43 | x["hat_r_%s" % str(t)] = uniform_sphere_sample(hat_r_bar, 44 | rad).flatten() 45 | 46 | df = df.append(x, ignore_index=True) 47 | 48 | return df 49 | 50 | 51 | class TestMatrixParams(unittest.TestCase): 52 | def test_matrix_multiperiod_portfolio(self): 53 | np.random.seed(1) 54 | 55 | k = 10 56 | n = 50 57 | T = 5 58 | borrow_cost = 0.0001 59 | lam = { 60 | "risk": 50, 61 | "borrow": 0.0001, 62 | "norm1_trade": 0.01, 63 | "norm0_trade": 1.0, 64 | } 65 | 66 | # Parameters 67 | hat_r = [ 68 | cp.Parameter(n, name="hat_r_%s" % str(t)) for t in range(1, T + 1) 69 | ] 70 | w_init = cp.Parameter(n, name="w_init") 71 | # F = cp.Parameter((n, k), name="F") 72 | sqrt_SigmaF_FT = cp.Parameter((k, n), name="SigmaF_FT") 73 | sqrt_D = cp.Parameter(n, name="sqrt_D") 74 | 75 | # Formulate problem 76 | w = [cp.Variable(n) for t in range(T + 1)] 77 | 78 | # Define cost components 79 | cost = 0 80 | constraints = [w[0] == w_init] 81 | for t in range(1, T + 1): 82 | 83 | risk_cost = lam["risk"] * ( 84 | # cp.quad_form(F.T * w[t], Sigma_F) 85 | cp.sum_squares(sqrt_SigmaF_FT @ w[t]) + 86 | cp.sum_squares(cp.multiply(sqrt_D, w[t])) 87 | ) 88 | 89 | holding_cost = lam["borrow"] * cp.sum(borrow_cost * cp.neg(w[t])) 90 | 91 | transaction_cost = lam["norm1_trade"] * cp.norm(w[t] - w[t - 1], 1) 92 | 93 | cost += ( 94 | hat_r[t - 1] @ w[t] 95 | - risk_cost 96 | - holding_cost 97 | - transaction_cost 98 | ) 99 | 100 | constraints += [cp.sum(w[t]) == 1.0] 101 | 102 | # Define optimizer 103 | problem = cp.Problem(cp.Maximize(cost), constraints) 104 | m = Optimizer(problem) 105 | 106 | self.assertTrue(m._problem.parameters_in_matrices) 107 | 108 | # Sample parameters 109 | df_train = sample_portfolio(n, k, N=1000) 110 | 111 | # Train and test using pytorch 112 | m.train(df_train, filter_strategies=True, parallel=True, 113 | n_train_trials=10, 114 | learner=PYTORCH) 115 | 116 | # Assert fewer strategies than training samples 117 | self.assertTrue(len(m.encoding) < len(df_train)) 118 | self.assertTrue(len(m.encoding) > 1) 119 | -------------------------------------------------------------------------------- /mlopt/tests/test_parallel.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import numpy as np 3 | import numpy.testing as npt 4 | from mlopt.optimizer import Optimizer 5 | from mlopt.settings import PYTORCH 6 | from mlopt.problem import Problem 7 | from mlopt.tests.settings import TEST_TOL as TOL 8 | from mlopt.sampling import uniform_sphere_sample 9 | import pandas as pd 10 | import cvxpy as cp 11 | 12 | 13 | class TestParallel(unittest.TestCase): 14 | 15 | def test_parallel_vs_serial_learning(self): 16 | """Test parallel VS serial learning""" 17 | 18 | # Generate data 19 | np.random.seed(1) 20 | T = 5 21 | M = 2. 22 | h = 1. 23 | c = 1. 24 | p = 1. 25 | x_init = 2. 26 | radius = 2. 27 | n_train = 1000 # Number of samples 28 | 29 | # Define problem 30 | x = cp.Variable(T+1) 31 | u = cp.Variable(T) 32 | 33 | # Define parameter and sampling points 34 | d = cp.Parameter(T, nonneg=True, name="d") 35 | d_bar = 3. * np.ones(T) 36 | X_d = uniform_sphere_sample(d_bar, radius, n=n_train) 37 | df = pd.DataFrame({'d': list(X_d)}) 38 | 39 | # Constaints 40 | constraints = [x[0] == x_init] 41 | for t in range(T): 42 | constraints += [x[t+1] == x[t] + u[t] - d[t]] 43 | constraints += [u >= 0, u <= M] 44 | 45 | # Objective 46 | cost = cp.sum(cp.maximum(h * x, -p * x)) + c * cp.sum(u) 47 | 48 | # Define problem 49 | cvxpy_problem = cp.Problem(cp.Minimize(cost), constraints) 50 | problem = Problem(cvxpy_problem) 51 | 52 | # Solve for all theta in serial 53 | results_serial = problem.solve_parametric(df, 54 | parallel=False) 55 | 56 | # Solve for all theta in parallel 57 | results_parallel = problem.solve_parametric(df, 58 | parallel=True) 59 | 60 | # Assert all results match 61 | for i in range(n_train): 62 | serial = results_serial[i] 63 | parallel = results_parallel[i] 64 | 65 | # Compare x 66 | npt.assert_array_almost_equal(serial['x'], 67 | parallel['x'], 68 | decimal=TOL) 69 | # Compare cost 70 | npt.assert_array_almost_equal(serial['cost'], 71 | parallel['cost'], 72 | decimal=TOL) 73 | 74 | # Compare strategy 75 | self.assertTrue(serial['strategy'] == parallel['strategy']) 76 | 77 | def test_parallel_resolve(self): 78 | """Test parallel resolve (to avoid hanging)""" 79 | 80 | np.random.seed(1) 81 | # This needs to work for different 82 | p = 10 83 | n = p * 10 84 | F = np.random.randn(n, p) 85 | D = np.diag(np.random.rand(n)*np.sqrt(p)) 86 | Sigma = F.dot(F.T) + D 87 | gamma = 1.0 88 | mu = cp.Parameter(n, name='mu') 89 | x = cp.Variable(n) 90 | cost = - mu @ x + gamma * cp.quad_form(x, Sigma) 91 | constraints = [cp.sum(x) == 1, x >= 0] 92 | 93 | # Define optimizer 94 | problem = cp.Problem(cp.Minimize(cost), constraints) 95 | m = Optimizer(problem, name="portfolio") 96 | 97 | ''' 98 | Sample points 99 | ''' 100 | theta_bar = np.random.randn(n) 101 | radius = 1.0 102 | 103 | ''' 104 | Train and solve 105 | ''' 106 | 107 | # Training and testing data 108 | n_train = 1000 109 | n_test = 1000 110 | # Sample points from multivariate ball 111 | X_d = uniform_sphere_sample(theta_bar, radius, n=n_train) 112 | X_d_test = uniform_sphere_sample(theta_bar, radius, n=n_test) 113 | df = pd.DataFrame({'mu': list(X_d)}) 114 | df_test = pd.DataFrame({'mu': list(X_d_test)}) 115 | 116 | # Train and test using pytorch 117 | m.train(df, 118 | parallel=True, 119 | filter_strategies=True, 120 | n_train_trials=10, 121 | learner=PYTORCH) 122 | m.performance(df_test, parallel=True) 123 | 124 | # Run parallel loop again to enforce instability 125 | # in multiprocessing 126 | m.performance(df_test, parallel=True) 127 | 128 | # DOES NOT WORK YET BECAUSE IT CANNOT PICKLE pardiso objects 129 | # def test_parallel_strategy_selection(self): 130 | # """Choose best strategy in parallel""" 131 | # np.random.seed(1) 132 | # # This needs to work for different 133 | # p = 10 134 | # n = p * 10 135 | # F = np.random.randn(n, p) 136 | # D = np.diag(np.random.rand(n)*np.sqrt(p)) 137 | # Sigma = F.dot(F.T) + D 138 | # gamma = 1.0 139 | # mu = cp.Parameter(n, name='mu') 140 | # x = cp.Variable(n) 141 | # cost = - mu * x + gamma * cp.quad_form(x, Sigma) 142 | # constraints = [cp.sum(x) == 1, x >= 0] 143 | # 144 | # # Define optimizer 145 | # # Force mosek to be single threaded 146 | # m = Optimizer(cp.Minimize(cost), constraints) 147 | # 148 | # ''' 149 | # Sample points 150 | # ''' 151 | # theta_bar = np.random.randn(n) 152 | # radius = 0.6 153 | # 154 | # ''' 155 | # Train and solve 156 | # ''' 157 | # 158 | # # Training and testing data 159 | # n_train = 100 160 | # n_test = 10 161 | # 162 | # # Sample points from multivariate ball 163 | # X_d = uniform_sphere_sample(theta_bar, radius, n=n_train) 164 | # df = pd.DataFrame({'mu': list(X_d)}) 165 | # X_d_test = uniform_sphere_sample(theta_bar, radius, n=n_test) 166 | # df_test = pd.DataFrame({'mu': list(X_d_test)}) 167 | # 168 | # # Train and test using pytorch 169 | # params = { 170 | # 'learning_rate': [0.01], 171 | # 'batch_size': [32], 172 | # 'n_epochs': [200] 173 | # } 174 | # 175 | # m.train(df, parallel=True, learner=PYTORCH, params=params) 176 | # 177 | # # Test 178 | # serial = m.solve(df_test, parallel=False) 179 | # parallel = m.solve(df_test, parallel=True) 180 | # 181 | # for i in range(n_test): 182 | # npt.assert_array_almost_equal(serial[i]['x'], 183 | # parallel[i]['x'], 184 | # decimal=TOL) 185 | # 186 | # npt.assert_array_almost_equal(serial[i]['cost'], 187 | # parallel[i]['cost'], 188 | # decimal=TOL) 189 | # 190 | -------------------------------------------------------------------------------- /mlopt/tests/test_parameters.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import cvxpy as cp 3 | import numpy as np 4 | import mlopt 5 | from mlopt.sampling import uniform_sphere_sample 6 | import pandas as pd 7 | import numpy.testing as npt 8 | import logging 9 | from mlopt.tests.settings import TEST_TOL as TOL 10 | 11 | 12 | class TestParameters(unittest.TestCase): 13 | 14 | def test_parameters_in_matrices(self): 15 | """Check if parameters in matrices are recognized 16 | """ 17 | # Problem data. 18 | m = 2 19 | n = 1 20 | np.random.seed(1) 21 | A = np.random.randn(m, n) 22 | b = np.random.randn(m) 23 | gamma = cp.Parameter(nonneg=True) 24 | gamma.value = 0.8 25 | theta = cp.Parameter(nonneg=True) 26 | theta.value = 8.9 27 | x = cp.Variable(n) 28 | objective = cp.Minimize(gamma * cp.sum_squares(A @ x - b) + 29 | cp.norm(x, 1)) 30 | constraints = [0 <= x, x <= theta] 31 | problem = cp.Problem(objective, constraints) 32 | m = mlopt.Optimizer(problem) 33 | 34 | self.assertTrue(m._problem.parameters_in_matrices) 35 | 36 | def test_parameters_in_matrices2(self): 37 | """Check if parameters in matrices are recognized 38 | """ 39 | # Problem data. 40 | m = 2 41 | n = 1 42 | np.random.seed(1) 43 | A = np.random.randn(m, n) 44 | b = np.random.randn(m) 45 | gamma = cp.Parameter(nonneg=True) 46 | gamma.value = 0.8 47 | x = cp.Variable(n) 48 | objective = cp.Minimize(cp.sum_squares(A @ x - b) + 49 | cp.norm(x, 1)) 50 | constraints = [0 <= x, gamma * x <= 1] 51 | problem = cp.Problem(objective, constraints) 52 | m = mlopt.Optimizer(problem) 53 | 54 | self.assertTrue(m._problem.parameters_in_matrices) 55 | 56 | def test_parameters_in_vectors(self): 57 | """Check if parameters not in matrices are recognized 58 | """ 59 | # Problem data. 60 | m = 2 61 | n = 1 62 | np.random.seed(1) 63 | A = np.random.randn(m, n) 64 | b = np.random.randn(m) 65 | theta = cp.Parameter(nonneg=True) 66 | theta.value = 8.9 67 | x = cp.Variable(n) 68 | objective = cp.Minimize(cp.sum_squares(A @ x - b) + cp.norm(x, 1)) 69 | constraints = [0 <= x, x <= theta] 70 | problem = cp.Problem(objective, constraints) 71 | m = mlopt.Optimizer(problem) 72 | 73 | self.assertFalse(m._problem.parameters_in_matrices) 74 | -------------------------------------------------------------------------------- /mlopt/tests/test_problem.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import numpy as np 3 | import numpy.testing as npt 4 | import cvxpy as cp 5 | from mlopt.problem import Problem 6 | from mlopt.settings import DEFAULT_SOLVER 7 | from mlopt.tests.settings import TEST_TOL as TOL 8 | from copy import deepcopy 9 | 10 | 11 | class TestProblem(unittest.TestCase): 12 | def setUp(self): 13 | pass 14 | 15 | def test_violation(self): 16 | """Test problem violation""" 17 | 18 | np.random.seed(1) 19 | 20 | # Define problem 21 | n = 5 22 | m = 5 23 | x = cp.Variable(n) 24 | c = np.random.randn(n) 25 | A = np.random.randn(m, n) 26 | b = np.random.randn(m) 27 | prob_cvxpy = cp.Problem(cp.Minimize(c @ x), [A @ x <= b]) 28 | mlprob = Problem(prob_cvxpy) 29 | data, _, _ = prob_cvxpy.get_problem_data(solver=DEFAULT_SOLVER) 30 | 31 | # Set variable value 32 | x_val = 10 * np.random.randn(n) 33 | x.value = x_val 34 | 35 | # Check violation 36 | viol_cvxpy = mlprob.infeasibility(x_val, data) 37 | viol_manual = np.linalg.norm(np.maximum(A.dot(x_val) - b, 0), np.inf)/(1 + np.linalg.norm(b, np.inf)) 38 | 39 | self.assertTrue(abs(viol_cvxpy - viol_manual) <= TOL) 40 | 41 | def test_solve_cvxpy(self): 42 | """Solve cvxpy problem vs optimizer problem. 43 | Expect similar solutions.""" 44 | np.random.seed(1) 45 | n = 5 46 | m = 15 47 | x = cp.Variable(n) 48 | c = np.random.randn(n) 49 | A = np.random.randn(m, n) 50 | b = np.random.randn(m) 51 | cost = c @ x 52 | constraints = [A @ x <= b] 53 | 54 | cvxpy_problem = cp.Problem(cp.Minimize(cost), constraints) 55 | cvxpy_problem.solve(solver=DEFAULT_SOLVER) 56 | x_cvxpy = deepcopy(x.value) 57 | cost_cvxpy = cost.value 58 | problem = Problem(cvxpy_problem) 59 | problem.solve() 60 | x_problem = x.value 61 | cost_problem = cost.value 62 | 63 | npt.assert_almost_equal(x_problem, x_cvxpy, decimal=TOL) 64 | npt.assert_almost_equal(cost_problem, cost_cvxpy, decimal=TOL) 65 | 66 | def test_warm_start(self): 67 | """Solve with Gurobi twice and check warm start.""" 68 | np.random.seed(0) 69 | m, n = 80, 30 70 | A = np.random.rand(m, n) 71 | b = np.random.randn(m) 72 | x = cp.Variable(n, integer=True) 73 | cost = cp.norm(A @ x - b, 1) 74 | cvxpy_problem = cp.Problem(cp.Minimize(cost)) 75 | problem = Problem(cvxpy_problem) 76 | results_first = problem.solve() 77 | results_second = problem.solve() 78 | 79 | npt.assert_array_less(results_second['time'], 80 | results_first['time']) 81 | 82 | -------------------------------------------------------------------------------- /mlopt/tests/test_sampling.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import numpy as np 3 | import numpy.testing as npt 4 | from mlopt import Optimizer, PYTORCH 5 | from mlopt.tests.settings import TEST_TOL as TOL 6 | from mlopt.sampling import uniform_sphere_sample 7 | import mlopt.settings as s 8 | import tempfile 9 | import os 10 | import pandas as pd 11 | import cvxpy as cp 12 | 13 | 14 | def sampling_function(n): 15 | """ 16 | Sample data points. 17 | """ 18 | theta_bar = 3. * np.ones(5) 19 | 20 | # Sample points from multivariate ball 21 | X = uniform_sphere_sample(theta_bar, 1, n=n) 22 | 23 | df = pd.DataFrame({'d': list(X)}) 24 | 25 | return df 26 | 27 | 28 | class TestSampling(unittest.TestCase): 29 | 30 | def setUp(self): 31 | # Generate data 32 | np.random.seed(1) 33 | T = 5 34 | M = 2. 35 | h = 1. 36 | c = 1. 37 | p = 1. 38 | x_init = 2. 39 | 40 | # Number of test points 41 | n_test = 10 42 | 43 | # Define problem 44 | x = cp.Variable(T+1) 45 | u = cp.Variable(T) 46 | 47 | # Define parameter and sampling points 48 | d = cp.Parameter(T, nonneg=True, name="d") 49 | 50 | # Constaints 51 | constraints = [x[0] == x_init] 52 | for t in range(T): 53 | constraints += [x[t+1] == x[t] + u[t] - d[t]] 54 | constraints += [u >= 0, u <= M] 55 | self.constraints = constraints 56 | 57 | # Objective 58 | self.cost = cp.sum(cp.maximum(h * x, -p * x)) + c * cp.sum(u) 59 | 60 | # Define problem 61 | problem = cp.Problem(cp.Minimize(self.cost), self.constraints) 62 | self.optimizer = Optimizer(problem) 63 | 64 | # Test set 65 | self.df_test = sampling_function(n_test) 66 | 67 | def test_sample(self): 68 | """Test sampling scheme""" 69 | 70 | # Train optimizer 71 | self.optimizer.train(sampling_fn=sampling_function, 72 | learner=PYTORCH, 73 | n_train_trials=10) 74 | 75 | # Check tolerance 76 | self.assertTrue(self.optimizer._sampler.good_turing_smooth 77 | < s.SAMPLING_TOL) 78 | 79 | # Check that smoothed is larger than unsmoothed 80 | self.assertTrue(self.optimizer._sampler.good_turing 81 | < self.optimizer._sampler.good_turing_smooth) 82 | 83 | 84 | if __name__ == '__main__': 85 | unittest.main() 86 | 87 | -------------------------------------------------------------------------------- /mlopt/tests/test_save.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import numpy as np 3 | import numpy.testing as npt 4 | from mlopt import Optimizer, installed_learners 5 | from mlopt.tests.settings import TEST_TOL as TOL 6 | from mlopt.sampling import uniform_sphere_sample 7 | import mlopt.settings as s 8 | import tempfile 9 | import os 10 | import pandas as pd 11 | import cvxpy as cp 12 | 13 | 14 | class TestSave(unittest.TestCase): 15 | 16 | def setUp(self): 17 | # Generate data 18 | np.random.seed(0) 19 | T = 5 20 | M = 2. 21 | h = 1. 22 | c = 1. 23 | p = 1. 24 | x_init = 2. 25 | self.radius = 2. 26 | n = 1000 # Number of points 27 | n_test = 100 28 | 29 | # Define problem 30 | x = cp.Variable(T+1) 31 | u = cp.Variable(T) 32 | 33 | # Define parameter and sampling points 34 | d = cp.Parameter(T, nonneg=True, name="d") 35 | self.d_bar = 2. * np.ones(T) 36 | X_d = uniform_sphere_sample(self.d_bar, self.radius, n=n) 37 | X_d_test = uniform_sphere_sample(self.d_bar, self.radius, n=n_test) 38 | self.df = pd.DataFrame({'d': list(X_d)}) 39 | self.df_test = pd.DataFrame({'d': list(X_d_test)}) 40 | 41 | # Constaints 42 | constraints = [x[0] == x_init] 43 | for t in range(T): 44 | constraints += [x[t+1] == x[t] + u[t] - d[t]] 45 | constraints += [u >= 0, u <= M] 46 | self.constraints = constraints 47 | 48 | # Objective 49 | self.cost = cp.sum(cp.maximum(h * x, -p * x)) + c * cp.sum_squares(u) + 0.001 * cp.sum_squares(x) 50 | 51 | # Define problem 52 | problem = cp.Problem(cp.Minimize(self.cost), self.constraints) 53 | self.optimizer = Optimizer(problem) 54 | 55 | # # Define learners 56 | # self.learners = [ 57 | # # s.OPTIMAL_TREE, # Disable. Too slow 58 | # s.PYTORCH 59 | # ] 60 | 61 | def test_save_load_data(self): 62 | """Test save load data""" 63 | m = self.optimizer 64 | 65 | for learner in installed_learners(): 66 | with tempfile.TemporaryDirectory() as tmpdir: 67 | data_file = os.path.join(tmpdir, "data.pkl") 68 | 69 | # Sample and store 70 | m.train(self.df, 71 | # sampling_fn=lambda n: sample(self.d_bar, 72 | # self.radius, 73 | # n), 74 | filter_strategies=True, 75 | parallel=True, 76 | learner=learner, 77 | n_train_trials=10, 78 | # params=nn_params 79 | ) 80 | store_general, store_detail = m.performance(self.df_test) 81 | 82 | # Save datafile 83 | m.save_training_data(data_file, delete_existing=True) 84 | 85 | # Create new optimizer, load data, train and 86 | # evaluate performance 87 | problem = cp.Problem(cp.Minimize(self.cost), self.constraints) 88 | self.optimizer = Optimizer(problem) 89 | 90 | m = self.optimizer 91 | m.load_training_data(data_file) 92 | m.train(parallel=True, 93 | learner=learner, 94 | n_train_trials=10) 95 | load_general, load_detail = m.performance(self.df_test) 96 | 97 | # test same things 98 | npt.assert_almost_equal(store_general['max_infeas'], 99 | load_general['max_infeas'], 100 | decimal=1e-8) 101 | npt.assert_almost_equal(store_general['avg_infeas'], 102 | load_general['avg_infeas'], 103 | decimal=1e-8) 104 | npt.assert_almost_equal(store_general['max_subopt'], 105 | load_general['max_subopt'], 106 | decimal=1e-8) 107 | npt.assert_almost_equal(store_general['avg_subopt'], 108 | load_general['avg_subopt'], 109 | decimal=1e-8) 110 | # 111 | def test_save_load(self): 112 | """Test save load""" 113 | 114 | for learner in installed_learners(): 115 | with tempfile.TemporaryDirectory() as tmpdir: 116 | data_file = os.path.join(tmpdir, "data.pkl") 117 | 118 | # Train optimizer 119 | self.optimizer.train(self.df, 120 | n_train_trials=10, 121 | learner=learner) 122 | 123 | # Create temporary directory where 124 | # to do stuff 125 | with tempfile.TemporaryDirectory() as tmpdir: 126 | 127 | # Archive name 128 | file_name = os.path.join(tmpdir, learner + ".tar.gz") 129 | 130 | # Save optimizer 131 | self.optimizer.save(file_name) 132 | 133 | # Create new optimizer and load 134 | new_optimizer = Optimizer.from_file(file_name) 135 | 136 | # Predict with optimizer 137 | res = self.optimizer.solve(self.df_test) 138 | 139 | # Predict with new_optimizer 140 | res_new = new_optimizer.solve(self.df_test) 141 | 142 | # Make sure predictions match 143 | for i in range(len(self.df_test)): 144 | npt.assert_almost_equal(res[i]['x'], 145 | res_new[i]['x'], 146 | decimal=TOL) 147 | npt.assert_almost_equal(res[i]['cost'], 148 | res_new[i]['cost'], 149 | decimal=TOL) 150 | self.assertTrue(res[i]['strategy'] == 151 | res_new[i]['strategy']) 152 | -------------------------------------------------------------------------------- /mlopt/tests/test_solve_strategy.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import mlopt 3 | import numpy as np 4 | import scipy.sparse as spa 5 | import numpy.testing as npt 6 | from mlopt.tests.settings import TEST_TOL as TOL 7 | from mlopt.problem import Problem 8 | import cvxpy as cp 9 | 10 | 11 | class TestSolveStrategy(unittest.TestCase): 12 | def test_small(self): 13 | """Test small continuous LP""" 14 | 15 | # Define problem 16 | c = np.array([-1, -2]) 17 | x = cp.Variable(2, integer=True) 18 | # x = cp.Variable(2) 19 | cost = c @ x 20 | constraints = [ 21 | x[1] <= 0.5 * x[0] + 1.5, 22 | x[1] <= -0.5 * x[0] + 3.5, 23 | x[1] <= -5.0 * x[0] + 10, 24 | x >= 0, x <= 1, 25 | ] 26 | cvxpy_problem = cp.Problem(cp.Minimize(cost), constraints) 27 | problem = Problem(cvxpy_problem) 28 | 29 | # Solve and compute strategy 30 | results = problem.solve() 31 | # violation1 = problem.infeasibility() 32 | 33 | # Solve just with strategy 34 | results_new = problem.solve(strategy=results["strategy"]) 35 | # violation2 = problem.infeasibility() 36 | 37 | # Verify both solutions are equal 38 | npt.assert_almost_equal(results["x"], results_new["x"], decimal=TOL) 39 | npt.assert_almost_equal( 40 | results["cost"], results_new["cost"], decimal=TOL 41 | ) 42 | # self.assertTrue(abs(violation1 - violation2) <= TOL) 43 | 44 | def test_random_cont(self): 45 | """Test random continuous LP test""" 46 | 47 | # Seed for reproducibility 48 | np.random.seed(1) 49 | 50 | # Define problem 51 | n = 100 52 | m = 250 53 | 54 | # Define constraints 55 | v = np.random.rand(n) # Solution 56 | A = spa.random( 57 | m, n, density=0.8, data_rvs=np.random.randn, format="csc" 58 | ) 59 | b = A.dot(v) + np.random.rand(m) 60 | 61 | # Split in 2 parts 62 | A1 = A[: int(m / 2), :] 63 | b1 = b[: int(m / 2)] 64 | A2 = A[int(m / 2):, :] 65 | b2 = b[int(m / 2):] 66 | 67 | # Cost 68 | c = np.random.rand(n) 69 | x = cp.Variable(n) # Variable 70 | cost = c @ x 71 | 72 | # Define constraints 73 | constraints = [A1 @ x <= b1, A2 @ x <= b2] 74 | 75 | # Problem 76 | cvxpy_problem = cp.Problem(cp.Minimize(cost), constraints) 77 | problem = Problem(cvxpy_problem) 78 | 79 | # Solve and compute strategy 80 | results = problem.solve() 81 | 82 | # Solve just with strategy 83 | results_new = problem.solve(strategy=results["strategy"]) 84 | 85 | # Verify both solutions are equal 86 | npt.assert_almost_equal(results["x"], results_new["x"], decimal=TOL) 87 | npt.assert_almost_equal( 88 | results["cost"], results_new["cost"], decimal=TOL 89 | ) 90 | 91 | def test_small_inventory(self): 92 | # Generate data 93 | np.random.seed(1) 94 | T = 5 95 | M = 2.0 96 | h = 1.0 97 | c = 1.0 98 | p = 1.0 99 | x_init = 2.0 100 | 101 | # Define problem 102 | x = cp.Variable(T + 1) 103 | u = cp.Variable(T) 104 | t = cp.Variable(T + 1) 105 | 106 | # Explicitly define parameter 107 | d = np.array( 108 | [3.94218985, 2.98861724, 2.48309709, 1.91226946, 2.33123841] 109 | ) 110 | 111 | # Constaints 112 | constraints = [x[0] == x_init] 113 | for t in range(T): 114 | constraints += [x[t + 1] == x[t] + u[t] - d[t]] 115 | constraints += [u >= 0, u <= M] 116 | 117 | # Maximum 118 | constraints += [t >= h * x, t >= -p * x] 119 | 120 | # Objective 121 | cost = cp.sum(t) + c * cp.sum(u) 122 | 123 | # Define problem 124 | cvxpy_problem = cp.Problem(cp.Minimize(cost), constraints) 125 | problem = Problem(cvxpy_problem) 126 | results = problem.solve() 127 | 128 | # Solve with strategy! 129 | results_strategy = problem.solve(strategy=results["strategy"]) 130 | 131 | # Verify both solutions are equal 132 | npt.assert_almost_equal( 133 | results["x"], results_strategy["x"], decimal=TOL 134 | ) 135 | npt.assert_almost_equal( 136 | results["cost"], results_strategy["cost"], decimal=TOL 137 | ) 138 | 139 | def test_random_integer(self): 140 | """Mixed-integer random LP test""" 141 | 142 | # Seed for reproducibility 143 | np.random.seed(1) 144 | 145 | # Define problem 146 | n = 20 147 | m = 70 148 | 149 | # Define constraints 150 | v = np.random.rand(n) # Solution 151 | A = spa.random( 152 | m, n, density=0.8, data_rvs=np.random.randn, format="csc" 153 | ) 154 | b = A.dot(v) + 10 * np.random.rand(m) 155 | 156 | # Split in 2 parts 157 | A1 = A[: int(m / 2), :] 158 | b1 = b[: int(m / 2)] 159 | A2 = A[int(m / 2):, :] 160 | b2 = b[int(m / 2):] 161 | 162 | # Cost 163 | c = np.random.rand(n) 164 | x = cp.Variable(n) # Variable 165 | y = cp.Variable(integer=True) # Variable 166 | cost = c @ x - cp.sum(y) + y 167 | 168 | # Define constraints 169 | constraints = [A1 @ x - y <= b1, A2 @ x + y <= b2, y >= 2] 170 | 171 | # Problem 172 | cvxpy_problem = cp.Problem(cp.Minimize(cost), constraints) 173 | problem = Problem(cvxpy_problem) 174 | 175 | # Solve and compute strategy 176 | results = problem.solve() 177 | 178 | # Solve just with strategy 179 | results_new = problem.solve(strategy=results["strategy"]) 180 | 181 | # Verify both solutions are equal 182 | npt.assert_almost_equal(results["x"], results_new["x"], decimal=TOL) 183 | npt.assert_almost_equal( 184 | results["cost"], results_new["cost"], decimal=TOL 185 | ) 186 | 187 | def test_random_reform_integer(self): 188 | """Mixed-integer random reformulated LP test""" 189 | 190 | # Seed for reproducibility 191 | np.random.seed(1) 192 | 193 | # Define problem 194 | n = 20 195 | m = 70 196 | 197 | # Define constraints 198 | v = np.random.rand(n) # Solution 199 | A = spa.random( 200 | m, n, density=0.8, data_rvs=np.random.randn, format="csc" 201 | ) 202 | b = A.dot(v) + 10 * np.random.rand(m) 203 | 204 | # Split in 2 parts 205 | A1 = A[: int(m / 2), :] 206 | b1 = b[: int(m / 2)] 207 | A2 = A[int(m / 2):, :] 208 | b2 = b[int(m / 2):] 209 | 210 | # Cost 211 | c = np.random.rand(n) 212 | x = cp.Variable(n) # Variable 213 | y = cp.Variable(integer=True) # Variable 214 | cost = c @ x - cp.sum(y) + y + 0.1 * cp.sum(cp.pos(x)) 215 | 216 | # Define constraints 217 | constraints = [A1 @ x - y <= b1, A2 @ x + y <= b2, y >= 2] 218 | 219 | # Problem 220 | cvxpy_problem = cp.Problem(cp.Minimize(cost), constraints) 221 | problem = Problem(cvxpy_problem) 222 | results = problem.solve() 223 | 224 | # Solve just with strategy 225 | results_new = problem.solve(strategy=results["strategy"]) 226 | 227 | # Verify both solutions are equal 228 | npt.assert_almost_equal(results["x"], results_new["x"], decimal=TOL) 229 | npt.assert_almost_equal( 230 | results["cost"], results_new["cost"], decimal=TOL 231 | ) 232 | 233 | def test_random_cont_qp_reform(self): 234 | """Test random continuous QP reform test""" 235 | 236 | # Seed for reproducibility 237 | np.random.seed(1) 238 | 239 | # Define problem 240 | n = 100 241 | m = 250 242 | 243 | # Define constraints 244 | v = np.random.rand(n) # Solution 245 | A = spa.random( 246 | m, n, density=0.8, data_rvs=np.random.randn, format="csc" 247 | ) 248 | b = A.dot(v) + np.random.rand(m) 249 | 250 | # Split in 2 parts 251 | A1 = A[: int(m / 2), :] 252 | b1 = b[: int(m / 2)] 253 | A2 = A[int(m / 2):, :] 254 | b2 = b[int(m / 2):] 255 | 256 | # Cost 257 | c = np.random.rand(n) 258 | x = cp.Variable(n) # Variable 259 | cost = cp.sum_squares(c @ x) + cp.norm(x, 1) 260 | 261 | # Define constraints 262 | constraints = [A1 @ x <= b1, A2 @ x <= b2] 263 | 264 | # Problem 265 | cvxpy_problem = cp.Problem(cp.Minimize(cost), constraints) 266 | problem = Problem(cvxpy_problem) 267 | 268 | # Solve and compute strategy 269 | results = problem.solve() 270 | 271 | # Solve just with strategy 272 | results_new = problem.solve(strategy=results["strategy"]) 273 | 274 | # Verify both solutions are equal 275 | npt.assert_almost_equal(results["x"], results_new["x"], decimal=TOL) 276 | npt.assert_almost_equal( 277 | results["cost"], results_new["cost"], decimal=TOL 278 | ) 279 | 280 | 281 | if __name__ == "__main__": 282 | unittest.main() 283 | -------------------------------------------------------------------------------- /mlopt/utils.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import cvxpy as cp 3 | import os 4 | import pandas as pd 5 | import mlopt.settings as stg 6 | import joblib 7 | 8 | 9 | # def args_norms(expr): 10 | # """Calculate norm of the arguments in a cvxpy expression""" 11 | # if expr.args: 12 | # norms = [] 13 | # # Expression contains arguments 14 | # for arg in expr.args: 15 | # # norms += args_norms(arg) 16 | # norms += [cp.norm(arg, np.inf).value] 17 | # else: 18 | # norms = [0.] 19 | # return norms 20 | 21 | 22 | # def tight_components(con): 23 | # """Return which components are tight in the constraints.""" 24 | # # rel_norm = np.amax([np.linalg.norm(np.atleast_1d(a.value), np.inf) 25 | # # for a in con.expr.args]) 26 | # # If Equality Constraint => all tight 27 | # if type(con) in [Equality, Zero]: 28 | # return np.full(con.shape, True) 29 | # 30 | # # Otherwise return violation 31 | # rel_norm = 1.0 32 | # return np.abs(con.expr.value) <= stg.TIGHT_CONSTRAINTS_TOL * (1 + rel_norm) 33 | 34 | 35 | def get_n_processes(max_n=np.inf): 36 | """Get number of processes from current cps number 37 | 38 | Parameters 39 | ---------- 40 | max_n: int 41 | Maximum number of processes. 42 | 43 | Returns 44 | ------- 45 | float 46 | Number of processes to use. 47 | """ 48 | 49 | try: 50 | # Check number of cpus if we are on a SLURM server 51 | n_cpus = int(os.environ["SLURM_CPUS_PER_TASK"]) 52 | except KeyError: 53 | n_cpus = joblib.cpu_count() 54 | 55 | n_proc = max(min(max_n, n_cpus), 1) 56 | 57 | return n_proc 58 | 59 | 60 | def n_features(df): 61 | """ 62 | Get number of features in dataframe 63 | where cells contain tuples. 64 | 65 | Parameters 66 | ---------- 67 | df : pandas DataFrame 68 | Dataframe. 69 | 70 | Returns 71 | ------- 72 | int 73 | Number of features. 74 | """ 75 | return sum(np.atleast_1d(x).size for x in df.iloc[0]) 76 | 77 | # n = 0 78 | # for c in df.columns.values: 79 | # 80 | # if isinstance(df[c].iloc[0], list): # If list add length 81 | # n += len(df[c].iloc[0]) 82 | # else: # If number add 1 83 | # n += 1 84 | # return n 85 | 86 | 87 | def pandas2array(X): 88 | """ 89 | Unroll dataframe elements to construct 2d array in case of 90 | cells containing tuples. 91 | """ 92 | 93 | if isinstance(X, np.ndarray): 94 | # Already numpy array. Return it. 95 | return X 96 | else: 97 | if isinstance(X, pd.Series): 98 | X = pd.DataFrame(X).transpose() 99 | # get number of datapoints 100 | n_data = len(X) 101 | 102 | x_temp_list = [] 103 | for i in range(n_data): 104 | x_temp_list.append( 105 | np.concatenate([np.atleast_1d(v).flatten() 106 | for v in X.iloc[i].values]) 107 | ) 108 | X_new = np.vstack(x_temp_list) 109 | 110 | return X_new 111 | 112 | 113 | def suboptimality(cost_pred, cost_test, sense): 114 | """Compute suboptimality""" 115 | if np.abs(cost_test) < stg.DIVISION_TOL: 116 | cost_norm = 1. 117 | else: 118 | cost_norm = np.abs(cost_test) 119 | 120 | if sense == cp.Minimize: 121 | return (cost_pred - cost_test)/cost_norm 122 | else: # Maximize 123 | return (cost_test - cost_pred)/cost_norm 124 | 125 | 126 | def accuracy(results_pred, results_test, sense): 127 | """ 128 | Accuracy comparison between predicted and test results. 129 | 130 | Parameters 131 | ---------- 132 | results_red : dictionary of predict results. 133 | List of predicted results. 134 | results_test : dictionary of test results. 135 | List of test results. 136 | 137 | Returns 138 | ------- 139 | float: 140 | Fraction of correct over total strategies compared. 141 | numpy array: 142 | Boolean vector indicating which strategy is correct. 143 | numpy array: 144 | Boolean vector indicating which strategy is exact. 145 | """ 146 | 147 | # Assert correctness by compariing solution cost and infeasibility 148 | n_points = len(results_pred) 149 | assert n_points == len(results_test) 150 | 151 | idx_correct = np.zeros(n_points, dtype=int) 152 | for i in range(n_points): 153 | r_pred = results_pred[i] 154 | r_test = results_test[i] 155 | # Check if prediction is correct 156 | if r_pred['strategy'] == r_test['strategy']: 157 | idx_correct[i] = 1 158 | else: 159 | # Check feasibility 160 | if r_pred['infeasibility'] <= stg.INFEAS_TOL: 161 | # Check cost function value 162 | subopt = suboptimality(r_pred['cost'], r_test['cost'], sense) 163 | if np.abs(subopt) <= stg.SUBOPT_TOL: 164 | idx_correct[i] = 1 165 | 166 | return np.sum(idx_correct) / n_points, idx_correct 167 | -------------------------------------------------------------------------------- /pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | log_cli = 1 3 | log_cli_level = INFO 4 | 5 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | appnope==0.1.0 2 | autopep8==1.5.2 3 | backcall==0.1.0 4 | cvxpy==1.1.1 5 | decorator==4.4.2 6 | ecos==2.0.7.post1 7 | flake8==3.8.1 8 | future==0.18.2 9 | greenlet==0.4.15 10 | gurobipy==9.0.2 11 | ipdb==0.13.2 12 | ipython==7.14.0 13 | ipython-genutils==0.2.0 14 | jedi==0.17.0 15 | joblib==0.15.1 16 | mccabe==0.6.1 17 | -e git+https://github.com/bstellato/mlopt.git@90ff0179d54b92ac907fef3e0df2dc7602627428#egg=mlopt 18 | msgpack==1.0.0 19 | numpy==1.18.4 20 | osqp==0.6.1 21 | pandas==1.0.3 22 | parso==0.7.0 23 | pexpect==4.8.0 24 | pickleshare==0.7.5 25 | prompt-toolkit==3.0.5 26 | ptyprocess==0.6.0 27 | pycodestyle==2.6.0 28 | pyflakes==2.2.0 29 | Pygments==2.6.1 30 | pynvim==0.4.1 31 | python-dateutil==2.8.1 32 | pytz==2020.1 33 | scikit-umfpack==0.3.2 34 | scipy==1.4.1 35 | scs==2.1.2 36 | six==1.14.0 37 | tqdm==4.46.0 38 | traitlets==4.3.3 39 | wcwidth==0.1.9 40 | xgboost==1.1.0 41 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | 4 | # Read README.rst file 5 | def readme(): 6 | with open('README.md') as f: 7 | return f.read() 8 | 9 | 10 | setup(name='mlopt', 11 | version='0.0.2', 12 | description='The Machine Learning Optimizer', 13 | long_description=readme(), 14 | long_description_content_type='text/markdown', 15 | author='Bartolomeo Stellato, Dimitris Bertsimas', 16 | author_email='bartolomeo.stellato@gmail.com', 17 | url='https://mlopt.org/', 18 | packages=['mlopt', 19 | 'mlopt.learners', 20 | 'mlopt.tests'], 21 | install_requires=["cvxpy", 22 | "optuna", 23 | "numpy", 24 | "scipy", 25 | "pandas", 26 | "joblib", 27 | "tqdm", 28 | "scikit-learn", 29 | "gurobipy", 30 | # TODO: Choose a default one to keep 31 | "xgboost", 32 | "torch", 33 | "torchvision", 34 | "pytorch-lightning", 35 | "scikit-umfpack" 36 | ], 37 | license='Apache License, Version 2.0', 38 | ) 39 | --------------------------------------------------------------------------------