├── .github
└── FUNDING.yml
├── .gitignore
├── LICENSE
├── Pipfile
├── Pipfile.lock
├── README.md
├── exp
├── __init__.py
├── __version__.py
├── args.py
├── gopt.py
├── params.py
└── run.py
├── extras
├── convergence.png
├── exp.png
└── getting_started.gif
├── setup.py
└── test
├── __init__.py
├── assets
├── __init__.py
├── dummy.conf
├── dummy_runnable.py
├── multiple_params.conf
└── runnable_tensorflow.py
└── test_params.py
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | ko_fi: davidenunes
2 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Byte-compiled / optimized / DLL files
2 | __pycache__/
3 | *.py[cod]
4 | *$py.class
5 |
6 | # C Extensions
7 | *.so
8 |
9 | # swap files for vim
10 | *.swp
11 |
12 | # Packaging / Distribution
13 | .Python
14 | env/
15 | build/
16 | develop-eggs/
17 | dist/
18 | eggs/
19 | .eggs/
20 | lib/
21 | lib64/
22 | sdist/
23 | var/
24 | *.egg-info/
25 | .installed.cfg
26 | *.egg
27 |
28 | # PyInstaller
29 | # Usually these files are written by a python script from a template
30 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
31 | *.manifest
32 | *.spec
33 |
34 | # Installer Logs
35 | pip-log.txt
36 | pip-delete-this-directory.txt
37 |
38 | # Tests
39 | htmlcov/
40 | .tox/
41 | .coverage
42 | .coverage.*
43 | .cache
44 | nosetests.xml
45 | coverage.xml
46 | *,cover
47 |
48 | # PyBuilder
49 | target/
50 | # IPythonNotebook template
51 | ## Temporary data
52 | .ipynb_checkpoints/
53 |
54 | # sphinx
55 | _build/
56 |
57 |
58 | .vscode
59 |
60 |
61 | # Idea
62 | .idea
63 |
--------------------------------------------------------------------------------
/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 [yyyy] [name of copyright owner]
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 |
--------------------------------------------------------------------------------
/Pipfile:
--------------------------------------------------------------------------------
1 | [[source]]
2 | url = "https://pypi.org/simple"
3 | verify_ssl = true
4 | name = "pypi"
5 |
6 | [packages]
7 | gputil = "*"
8 | click = "*"
9 | matplotlib = "*"
10 | scikit-optimize = "*"
11 | toml = "*"
12 | tqdm = "*"
13 |
14 | [requires]
15 | python_version = "3.6"
16 |
--------------------------------------------------------------------------------
/Pipfile.lock:
--------------------------------------------------------------------------------
1 | {
2 | "_meta": {
3 | "hash": {
4 | "sha256": "9ad6c768704115a440294837bebf50d15e51f837034db9ab61610aa9b9417f74"
5 | },
6 | "pipfile-spec": 6,
7 | "requires": {
8 | "python_version": "3.6"
9 | },
10 | "sources": [
11 | {
12 | "name": "pypi",
13 | "url": "https://pypi.org/simple",
14 | "verify_ssl": true
15 | }
16 | ]
17 | },
18 | "default": {
19 | "click": {
20 | "hashes": [
21 | "sha256:2335065e6395b9e67ca716de5f7526736bfa6ceead690adf616d925bdc622b13",
22 | "sha256:5b94b49521f6456670fdb30cd82a4eca9412788a93fa6dd6df72c94d5a8ff2d7"
23 | ],
24 | "index": "pypi",
25 | "version": "==7.0"
26 | },
27 | "cycler": {
28 | "hashes": [
29 | "sha256:1d8a5ae1ff6c5cf9b93e8811e581232ad8920aeec647c37316ceac982b08cb2d",
30 | "sha256:cd7b2d1018258d7247a71425e9f26463dfb444d411c39569972f4ce586b0c9d8"
31 | ],
32 | "version": "==0.10.0"
33 | },
34 | "gputil": {
35 | "hashes": [
36 | "sha256:9afc56eb7ada21888673cf3ab2046932c6207fb2396797b9736b3d02a59532c6"
37 | ],
38 | "index": "pypi",
39 | "version": "==1.3.0"
40 | },
41 | "kiwisolver": {
42 | "hashes": [
43 | "sha256:0ee4ed8b3ae8f5f712b0aa9ebd2858b5b232f1b9a96b0943dceb34df2a223bc3",
44 | "sha256:0f7f532f3c94e99545a29f4c3f05637f4d2713e7fd91b4dd8abfc18340b86cd5",
45 | "sha256:1a078f5dd7e99317098f0e0d490257fd0349d79363e8c923d5bb76428f318421",
46 | "sha256:1aa0b55a0eb1bd3fa82e704f44fb8f16e26702af1a073cc5030eea399e617b56",
47 | "sha256:2874060b91e131ceeff00574b7c2140749c9355817a4ed498e82a4ffa308ecbc",
48 | "sha256:379d97783ba8d2934d52221c833407f20ca287b36d949b4bba6c75274bcf6363",
49 | "sha256:3b791ddf2aefc56382aadc26ea5b352e86a2921e4e85c31c1f770f527eb06ce4",
50 | "sha256:4329008a167fac233e398e8a600d1b91539dc33c5a3eadee84c0d4b04d4494fa",
51 | "sha256:45813e0873bbb679334a161b28cb9606d9665e70561fd6caa8863e279b5e464b",
52 | "sha256:53a5b27e6b5717bdc0125338a822605084054c80f382051fb945d2c0e6899a20",
53 | "sha256:574f24b9805cb1c72d02b9f7749aa0cc0b81aa82571be5201aa1453190390ae5",
54 | "sha256:66f82819ff47fa67a11540da96966fb9245504b7f496034f534b81cacf333861",
55 | "sha256:79e5fe3ccd5144ae80777e12973027bd2f4f5e3ae8eb286cabe787bed9780138",
56 | "sha256:83410258eb886f3456714eea4d4304db3a1fc8624623fc3f38a487ab36c0f653",
57 | "sha256:8b6a7b596ce1d2a6d93c3562f1178ebd3b7bb445b3b0dd33b09f9255e312a965",
58 | "sha256:9576cb63897fbfa69df60f994082c3f4b8e6adb49cccb60efb2a80a208e6f996",
59 | "sha256:95a25d9f3449046ecbe9065be8f8380c03c56081bc5d41fe0fb964aaa30b2195",
60 | "sha256:a424f048bebc4476620e77f3e4d1f282920cef9bc376ba16d0b8fe97eec87cde",
61 | "sha256:aaec1cfd94f4f3e9a25e144d5b0ed1eb8a9596ec36d7318a504d813412563a85",
62 | "sha256:acb673eecbae089ea3be3dcf75bfe45fc8d4dcdc951e27d8691887963cf421c7",
63 | "sha256:b15bc8d2c2848a4a7c04f76c9b3dc3561e95d4dabc6b4f24bfabe5fd81a0b14f",
64 | "sha256:b1c240d565e977d80c0083404c01e4d59c5772c977fae2c483f100567f50847b",
65 | "sha256:c595693de998461bcd49b8d20568c8870b3209b8ea323b2a7b0ea86d85864694",
66 | "sha256:ce3be5d520b4d2c3e5eeb4cd2ef62b9b9ab8ac6b6fedbaa0e39cdb6f50644278",
67 | "sha256:e0f910f84b35c36a3513b96d816e6442ae138862257ae18a0019d2fc67b041dc",
68 | "sha256:ea36e19ac0a483eea239320aef0bd40702404ff8c7e42179a2d9d36c5afcb55c",
69 | "sha256:efabbcd4f406b532206b8801058c8bab9e79645b9880329253ae3322b7b02cd5",
70 | "sha256:f923406e6b32c86309261b8195e24e18b6a8801df0cfc7814ac44017bfcb3939"
71 | ],
72 | "version": "==1.0.1"
73 | },
74 | "matplotlib": {
75 | "hashes": [
76 | "sha256:66a6b7264fb200dd217ebc95c53d59b5e5fa8cac6b8a650a50ed05438667ff32",
77 | "sha256:69ff0d7139f3886be552ff29478c886b461081c0afb3a3ad46afb1a445bae722",
78 | "sha256:70f8782c50ac2c7617aad0fa5ba59fc49f690a851d6afc0178813c49767644dd",
79 | "sha256:716caa55ebfb82d66f7a5584ad818b349998d9cf7e6282e5eda5fdddf4752742",
80 | "sha256:91bf4be2477aa7408131ae1a499b1c8904ea8eb1eb3f88412b4809ebe0698868",
81 | "sha256:d1bd008db1e389d14523345719c30fd0fb3c724b71ae098360c3c8e85b7c560f",
82 | "sha256:d419a5fb5654f620756ad9883bc3f1db6875f6f2760c367bee775357d1bbb38c",
83 | "sha256:dc5b097546eeadc3a91eee35a1dbbf876e78ebed83b934c391f0f14605234c76",
84 | "sha256:de25d893f54e1d50555e4a4babf66d337917499c33c78a24216838b3d2c6bf3b",
85 | "sha256:e4ad891787ad2f181e7582997520a19912990b5d0644b1fdaae365b6699b953f",
86 | "sha256:e69ab0def9b053f4ea5800306ff9c671776a2d151ec6b206465309bb468c0bcc",
87 | "sha256:e9d37b22467e0e4d6f989892a998db5f59ddbf3ab811b515585dfdde9aacc5f9",
88 | "sha256:ee4471dd1c5ed03f2f46149af351b7a2e6618eced329660f1b4b8bf573422b70"
89 | ],
90 | "index": "pypi",
91 | "version": "==3.0.1"
92 | },
93 | "numpy": {
94 | "hashes": [
95 | "sha256:032df9b6571c5f1d41ea6f6a189223208cb488990373aa686aca55570fcccb42",
96 | "sha256:094f8a83e5bd0a44a7557fa24a46db6ba7d5299c389ddbc9e0e18722f567fb63",
97 | "sha256:1c0c80e74759fa4942298044274f2c11b08c86230b25b8b819e55e644f5ff2b6",
98 | "sha256:2aa0910eaeb603b1a5598193cc3bc8eacf1baf6c95cbc3955eb8e15fa380c133",
99 | "sha256:2f5ebc7a04885c7d69e5daa05208faef4db7f1ae6a99f4d36962df8cd54cdc76",
100 | "sha256:32a07241cb624e104b88b08dea2851bf4ec5d65a1f599d7735041ced7171fd7a",
101 | "sha256:3c7959f750b54b445f14962a3ddc41b9eadbab00b86da55fbb1967b2b79aad10",
102 | "sha256:3d8f9273c763a139a99e65c2a3c10f1109df30bedae7f011b10d95c538364704",
103 | "sha256:63bca71691339d2d6f8a7c970821f2b12098a53afccc0190d4e1555e75e5223a",
104 | "sha256:7ae9c3baff3b989859c88e0168ad10902118595b996bf781eaf011bb72428798",
105 | "sha256:866a7c8774ccc7d603667fad95456b4cf56d79a2bb5a7648ac9f0082e0b9416e",
106 | "sha256:8bc4b92a273659e44ca3f3a2f8786cfa39d8302223bcfe7df794429c63d5f5a1",
107 | "sha256:919f65e0732195474897b1cafefb4d4e7c2bb8174a725e506b62e9096e4df28d",
108 | "sha256:9d1598573d310104acb90377f0a8c2319f737084689f5eb18012becaf345cda5",
109 | "sha256:9fff90c88bfaad2901be50453d5cd7897a826c1d901f0654ee1d73ab3a48cd18",
110 | "sha256:a245464ddf6d90e2d6287e9cef6bcfda2a99467fdcf1b677b99cd0b6c7b43de2",
111 | "sha256:a988db28f54e104a01e8573ceb6f28202b4c15635b1450b2e3b2b822c6564f9b",
112 | "sha256:b12fe6f31babb9477aa0f9692730654b3ee0e71f33b4568170dfafd439caf0a2",
113 | "sha256:b7599ff4acd23f5de983e3aec772153b1043e131487a5c6ad0f94b41a828877a",
114 | "sha256:c9f4dafd6065c4c782be84cd67ceeb9b1d4380af60a7af32be10ebecd723385e",
115 | "sha256:ce3622b73ccd844ba301c1aea65d36cf9d8331e7c25c16b1725d0f14db99aaf4",
116 | "sha256:d0f36a24cf8061a2c03e151be3418146717505b9b4ec17502fa3bbdb04ec1431",
117 | "sha256:d263f8f14f2da0c079c0297e829e550d8f2c4e0ffef215506bd1d0ddd2bff3de",
118 | "sha256:d8837ff272800668aabdfe70b966631914b0d6513aed4fc1b1428446f771834d",
119 | "sha256:ef694fe72a3995aa778a5095bda946e0d31f7efabd5e8063ad8c6238ab7d3f78",
120 | "sha256:f1fd1a6f40a501ba4035f5ed2c1f4faa68245d1407bf97d2ee401e4f23d1720b",
121 | "sha256:fa337b6bd5fe2b8c4e705f4102186feb9985de9bb8536d32d5129a658f1789e0",
122 | "sha256:febd31cd0d2fd2509ca2ec53cb339f8bf593c1bd245b9fc55c1917a68532a0af"
123 | ],
124 | "version": "==1.15.3"
125 | },
126 | "pyparsing": {
127 | "hashes": [
128 | "sha256:bc6c7146b91af3f567cf6daeaec360bc07d45ffec4cf5353f4d7a208ce7ca30a",
129 | "sha256:d29593d8ebe7b57d6967b62494f8c72b03ac0262b1eed63826c6f788b3606401"
130 | ],
131 | "version": "==2.2.2"
132 | },
133 | "python-dateutil": {
134 | "hashes": [
135 | "sha256:063df5763652e21de43de7d9e00ccf239f953a832941e37be541614732cdfc93",
136 | "sha256:88f9287c0174266bb0d8cedd395cfba9c58e87e5ad86b2ce58859bc11be3cf02"
137 | ],
138 | "version": "==2.7.5"
139 | },
140 | "scikit-learn": {
141 | "hashes": [
142 | "sha256:1ca280bbdeb0f9950f9427c71e29d9f14e63b2ffa3e8fdf95f25e13773e6d898",
143 | "sha256:33ad23aa0928c64567a24aac771aea4e179fab2a20f9f786ab00ca9fe0a13c82",
144 | "sha256:344bc433ccbfbadcac8c16b4cec9d7c4722bcea9ce19f6da42e2c2f805571941",
145 | "sha256:35ee532b5e992a6e8d8a71d325fd9e0b58716894657e7d3da3e7a1d888c2e7d4",
146 | "sha256:37cbbba2d2a3895bba834d50488d22268a511279e053135bb291f637fe30512b",
147 | "sha256:40cf1908ee712545f4286cc21f3ee21f3466c81438320204725ab37c96849f27",
148 | "sha256:4130760ac54f5946523c1a1fb32a6c0925e5245f77285270a8f6fb5901b7b733",
149 | "sha256:46cc8c32496f02affde7abe507af99cd752de0e41aec951a0bc40c693c2a1e07",
150 | "sha256:4a364cf22be381a17c05ada9f9ce102733a0f75893c51b83718cd9358444921e",
151 | "sha256:56aff3fa3417cd69807c1c74db69aee34ce08d7161cbdfebbff9b4023d9d224b",
152 | "sha256:58debb34a15cfc03f4876e450068dbd711d9ec36ae5503ed2868f2c1f88522f7",
153 | "sha256:7bcf7ade62ef3443470af32afb82646640d653f42502cf31a13cc17d3ff85d57",
154 | "sha256:7d4eab203ed260075f47e2bf6a2bd656367e4e8683b3ad46d4651070c5d1e9aa",
155 | "sha256:86697c6e4c2d74fbbf110c6d5979d34196a55108fa9896bf424f9795a8d935ad",
156 | "sha256:911115db6669c9b11efd502dcc5483cd0c53e4e3c4bcdfe2e73bbb27eb5e81da",
157 | "sha256:97d1d971f8ec257011e64b7d655df68081dd3097322690afa1a71a1d755f8c18",
158 | "sha256:99f22c3228ec9ab3933597825dc7d595b6c8c7b9ae725cfa557f16353fac8314",
159 | "sha256:a2e18e5a4095b3ca4852eb087d28335f3bb8515df4ccf906d380ee627613837f",
160 | "sha256:a3070f71a4479a9827148609f24f2978f10acffa3b8012fe9606720d271066bd",
161 | "sha256:a6a197499429d2eaa2ae922760aa3966ef353545422d5f47ea2ca9369cbf7d26",
162 | "sha256:a7f6f5b3bc7b8e2066076098788579af12bd507ccea8ca6859e52761aa61eaca",
163 | "sha256:a82b90b6037fcc6b311431395c11b02555a3fbf96921a0667c8f8b0c495991cb",
164 | "sha256:ab2c4266b8cd159a266eb03c709ad5400756dca9c45aa48fb523263344475093",
165 | "sha256:b983a2dfdb9d707c78790608bcfd63692e5c2d996865a9689f3db768d0a2978d",
166 | "sha256:bb33d447f4c6fb164d426467d7bf8a4901c303333c5809b85319b2e0626763cd",
167 | "sha256:bc2a0116a67081167f1fbfed731d361671e5925db291b70e65fa66170045c53f",
168 | "sha256:bd189f6d0c2fdccb7c0d3fd1227c6626dc17d00257edbb63dd7c88f31928db61",
169 | "sha256:d393f810da9cd4746cad7350fb89f0509c3ae702c79d2ba8bd875201be4102d1"
170 | ],
171 | "version": "==0.20.0"
172 | },
173 | "scikit-optimize": {
174 | "hashes": [
175 | "sha256:1d7657a4b8ef9aa6d81e49b369c677c584e83269f11710557741d3b3f8fa0a75",
176 | "sha256:a2304413f7b66b27dfaed64271c370e5e2c926fbc1cbecdb67fe47cd847f9d5c"
177 | ],
178 | "index": "pypi",
179 | "version": "==0.5.2"
180 | },
181 | "scipy": {
182 | "hashes": [
183 | "sha256:0611ee97296265af4a21164a5323f8c1b4e8e15c582d3dfa7610825900136bb7",
184 | "sha256:08237eda23fd8e4e54838258b124f1cd141379a5f281b0a234ca99b38918c07a",
185 | "sha256:0e645dbfc03f279e1946cf07c9c754c2a1859cb4a41c5f70b25f6b3a586b6dbd",
186 | "sha256:0e9bb7efe5f051ea7212555b290e784b82f21ffd0f655405ac4f87e288b730b3",
187 | "sha256:108c16640849e5827e7d51023efb3bd79244098c3f21e4897a1007720cb7ce37",
188 | "sha256:340ef70f5b0f4e2b4b43c8c8061165911bc6b2ad16f8de85d9774545e2c47463",
189 | "sha256:3ad73dfc6f82e494195144bd3a129c7241e761179b7cb5c07b9a0ede99c686f3",
190 | "sha256:3b243c77a822cd034dad53058d7c2abf80062aa6f4a32e9799c95d6391558631",
191 | "sha256:404a00314e85eca9d46b80929571b938e97a143b4f2ddc2b2b3c91a4c4ead9c5",
192 | "sha256:423b3ff76957d29d1cce1bc0d62ebaf9a3fdfaf62344e3fdec14619bb7b5ad3a",
193 | "sha256:42d9149a2fff7affdd352d157fa5717033767857c11bd55aa4a519a44343dfef",
194 | "sha256:625f25a6b7d795e8830cb70439453c9f163e6870e710ec99eba5722775b318f3",
195 | "sha256:698c6409da58686f2df3d6f815491fd5b4c2de6817a45379517c92366eea208f",
196 | "sha256:729f8f8363d32cebcb946de278324ab43d28096f36593be6281ca1ee86ce6559",
197 | "sha256:8190770146a4c8ed5d330d5b5ad1c76251c63349d25c96b3094875b930c44692",
198 | "sha256:878352408424dffaa695ffedf2f9f92844e116686923ed9aa8626fc30d32cfd1",
199 | "sha256:8b984f0821577d889f3c7ca8445564175fb4ac7c7f9659b7c60bef95b2b70e76",
200 | "sha256:8f841bbc21d3dad2111a94c490fb0a591b8612ffea86b8e5571746ae76a3deac",
201 | "sha256:c22b27371b3866c92796e5d7907e914f0e58a36d3222c5d436ddd3f0e354227a",
202 | "sha256:d0cdd5658b49a722783b8b4f61a6f1f9c75042d0e29a30ccb6cacc9b25f6d9e2",
203 | "sha256:d40dc7f494b06dcee0d303e51a00451b2da6119acbeaccf8369f2d29e28917ac",
204 | "sha256:d8491d4784aceb1f100ddb8e31239c54e4afab8d607928a9f7ef2469ec35ae01",
205 | "sha256:dfc5080c38dde3f43d8fbb9c0539a7839683475226cf83e4b24363b227dfe552",
206 | "sha256:e24e22c8d98d3c704bb3410bce9b69e122a8de487ad3dbfe9985d154e5c03a40",
207 | "sha256:e7a01e53163818d56eabddcafdc2090e9daba178aad05516b20c6591c4811020",
208 | "sha256:ee677635393414930541a096fc8e61634304bb0153e4e02b75685b11eba14cae",
209 | "sha256:f0521af1b722265d824d6ad055acfe9bd3341765735c44b5a4d0069e189a0f40",
210 | "sha256:f25c281f12c0da726c6ed00535ca5d1622ec755c30a3f8eafef26cf43fede694"
211 | ],
212 | "version": "==1.1.0"
213 | },
214 | "six": {
215 | "hashes": [
216 | "sha256:70e8a77beed4562e7f14fe23a786b54f6296e34344c23bc42f07b15018ff98e9",
217 | "sha256:832dc0e10feb1aa2c68dcc57dbb658f1c7e65b9b61af69048abc87a2db00a0eb"
218 | ],
219 | "version": "==1.11.0"
220 | },
221 | "toml": {
222 | "hashes": [
223 | "sha256:229f81c57791a41d65e399fc06bf0848bab550a9dfd5ed66df18ce5f05e73d5c",
224 | "sha256:235682dd292d5899d361a811df37e04a8828a5b1da3115886b73cf81ebc9100e"
225 | ],
226 | "index": "pypi",
227 | "version": "==0.10.0"
228 | },
229 | "tqdm": {
230 | "hashes": [
231 | "sha256:3c4d4a5a41ef162dd61f1edb86b0e1c7859054ab656b2e7c7b77e7fbf6d9f392",
232 | "sha256:5b4d5549984503050883bc126280b386f5f4ca87e6c023c5d015655ad75bdebb"
233 | ],
234 | "index": "pypi",
235 | "version": "==4.28.1"
236 | }
237 | },
238 | "develop": {}
239 | }
240 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Experiment design, deployment, and optimization
7 |
8 |
9 | [](http://www.apache.org/licenses/LICENSE-2.0.html)
10 |
11 |
12 | EXP is a python experiment management toolset created to simplify two simple use cases: design and deploy experiments in the form of python modules/files.
13 |
14 | An experiment is a series of runs of a given configurable module for a specified set of parameters. This tool covers one of the most prevalent experiment deployment scenarios: testing a set of parameters in parallel in a local machine or homogeneous cluster. EXP also supports [global optimization](https://www.cs.ox.ac.uk/people/nando.defreitas/publications/BayesOptLoop.pdf) using **gaussian processes** or other surrogate models such as **random forests**. This can be used for instance as a tool for **hyperoparameter tuning** for machine learning models.
15 |
16 | ## Features
17 | * **parameter space design** based on configuration files ([TOML](https://github.com/toml-lang/toml) format);
18 | * **parallel experiment deployment** using ``multiprocessing`` processes;
19 | * **CUDA gpu workers** one parallel process per available GPUs: uses the variable [CUDA_VISIBLE_DEVICES](https://devblogs.nvidia.com/cuda-pro-tip-control-gpu-visibility-cuda_visible_devices);
20 | * **global optimization** from parameter spaces (e.g. for hyperparameter tunning) using [scikit-optimize](https://scikit-optimize.github.io/).
21 |
22 | ##
23 |
24 |
25 | ## Installation
26 | ``pip install exp``
27 |
28 | ``pipenv install exp`` with [pipenv](https://pipenv.readthedocs.io/en/latest/install/#pragmatic-installation-of-pipenv)
29 |
30 | ## Available CLI tools
31 | EXP provides two CLI modules:
32 | * exp.run: ``python -m exp.run -p basic.conf -m runnable.py --workers 10``
33 | * exp.gopt:``python -m exp.gopt -p basic.conf -m runnable.py --workers 4 -n 100 --plot``
34 |
35 | for more information check each commands help:
36 |
37 | ``python -m exp.run -h``
38 |
39 | ## Getting Started: Optimization
40 |
41 | ### 1. Runnable Module
42 | The first step is to create a module to use in our experiments. A basic configurable module ``runnable.py`` looks like this:
43 |
44 | ```python
45 | def run(x=1, **kwargs):
46 | return x ** 2
47 | ```
48 |
49 | This module computes the square of a parameter ``x``. Note that ``kwargs`` is included to capture other parameters that the experiment runner might use (even if they are not used by your module). Since run receives a dictionary, you could also define it as follows.
50 |
51 | ```python
52 | def run(**kwargs):
53 | x = kwargs.get('x',1)
54 | return x ** 2
55 | ```
56 |
57 | ### 2. Parameter Space Definition
58 | Next, we need a configuration file ``basic.conf`` were the parameters are specified:
59 | ```markdown
60 | [x]
61 | type = "range"
62 | bounds = [-10,10]
63 | ```
64 | This defines a parameter space with a single parameter ``x`` with values in the range ``[-10,10]``. For how to specify parameter spaces, see the [Parameter Space Specification](#parameter-space-specification).
65 |
66 | ### 3. Module Optimization
67 | Our simple module returns the ``x**2``, the optimizer tries to find the minimum value of this function based on the parameter space given by the configuration file. In this case, the optimizer will look at values of ``x`` between ``[-10,10]`` and try to find the minimum value.
68 |
69 | ```bash
70 | python -m exp.gopt --params basic.conf --module runnable.py --n 20 --workers 4
71 | ```
72 |
73 |
74 |
75 |
76 |
77 | finds a solution very close to ``0``. By default, the optimizer assumes a range defines the boundaries of a real-valued variable. If you wish to optimize discrete integers use the following specification:
78 |
79 | ```markdown
80 | [x]
81 | type = "range"
82 | bounds = [-10,10]
83 | dtype = "int"
84 | ```
85 | The optimizer will explore discrete values between -10 and 10 inclusively. Also, using the ``--plot`` flag displays a real-time **convergence plot** for the optimization process.
86 |
87 |
88 |
89 |
90 |
91 | which in this case converges immediately because the function to be optimized is quite simple, but the goal is to optimize complex models and choosing from a large set of parameters without having to run an exhaustive search through all the possible parameter combinations.
92 |
93 | ## Parameter Space Specification
94 | Parameter space files use [TOML](https://github.com/toml-lang/toml) format, I recommend taking a look at the specification and getting familiar with how to define values, arrays, etc. ParamSpaces in EXP has **4 types of parametes**, namely:
95 | * **value**: single value parameter;
96 | * **range**: a range of numbers between bounds;
97 | * **random**: a random *real/int* value between bounds;
98 | * **list**: a list of values (used for example to specify categorical parameters);
99 |
100 | Bellow, I supply an example for each type of parameter:
101 |
102 | ### Value
103 | Single value parameter.
104 | ```python
105 | # this a single valued parameter with a boolean value
106 | [some_param]
107 | type = "value"
108 | value = true
109 | ```
110 | ### Range
111 | A parameter with a set of values within a range.
112 | ```python
113 | # TOML files can handle comments which is useful to document experiment configurations
114 | [some_range_param]
115 | type = "range"
116 | bounds = [-10,10]
117 | step = 1 # this is optional and assumed to be 1
118 | dtype = "float" # also optional and assumed to be float
119 | ```
120 | The commands ``run`` and ``gopt`` will treat this parameter definition differently. The optimizer will explore values within the bounds including the end-points. The runner will take values between ``bounds[0]`` and ``bounds[1]`` excluding the last end-point (much like a python range or numpy arange).
121 |
122 | The ``dtype`` also influences how the optimizer looks for values in the range, if set to ``"int"``, it explores discrete integer values within the bounds; if set to ``"float"``, it assumes the parameter takes a continuous value between the specified bounds.
123 |
124 | ### Random
125 | A parameter with ``n`` random values sampled from "uniform" or "log-uniform" between the given bounds. If used with ``run``, a parameter space will be populated with a list of random values according to the specification. If used with ``gopt``, ``n`` is ignored and bounds are used instead, along with the prior.
126 |
127 | For optimization purposes, this works like range, except that you can specify the prior which can be "uniform" or "log-uniform", range assumes that the values are generated from "uniform" prior, when the parameter is used for optimization.
128 |
129 | The other difference between parameter grids and optimization is that the bounds do not include the end-points when generating parameter values for grid search. The optimizer will explore random values within the bounds specified, including the high end-point.
130 |
131 | ```python
132 | [random_param]
133 | type="random"
134 | bounds=[0,3] # optional, default range is [0,1]
135 | prior="uniform" # optional, default value is "uniform"
136 | dtype="float" # optional, default value is "float"
137 | n=1 # optional, default value is 1 (number of random parameters to be sampled)
138 | ```
139 | ### List
140 | A list is just an homogeneous series of values a parameter can take.
141 | ```python
142 | [another_param]
143 | type="list"
144 | value = [1,2,3]
145 | ```
146 | The array in ``"value"`` must be homogenous, something like ``value=[1,2,"A"]`` would throw a *Not a homogeneous array* error. List parameters are treated by ``gopt`` command as a **categorical** parameter. This is encoded using a *one-hot-encoding* for optimization.
147 |
148 | Also, for optimization purposes, a list is treated like a set, if you provide duplicate values it will only explore the unique values. For example if you want to specify a boolean parameter, use a list:
149 |
150 | ```python
151 | [some_boolean_decision]
152 | type="list"
153 | value = [true,false]
154 | ```
155 |
156 | # Library Modules
157 | EXP also provides different tools to specify param spaces programmatically
158 | ## ParamSpace
159 | The ``exp.params.ParamSpace`` class provides a way to create parameter spaces and iterate over all the possible
160 | combinations of parameters as follows:
161 | ```python
162 | >>>from exp.params import ParamSpace
163 | >>>ps = ParamSpace()
164 | >>>ps.add_value("p1",1)
165 | >>>ps.add_list("p2",[True,False])
166 | >>>ps.add_range("p3",low=0,high=10,dtype=int)
167 | >>>ps.size
168 | 20
169 | ```
170 |
171 | ```python
172 | grid = ps.param_grid(runs=2)
173 | ```
174 | ``grid`` has ``2*ps.size`` configurations because we repeat each configuration ``2`` times (number of runs). Each configuration dictionary includes 2 additional parameters ``"id"`` and ``"run"`` which are the unique configuration id and run id respectively.
175 |
176 | ```python
177 | for config in grid:
178 | # config is a dictionary with the params of a unique configuration in the parameter space
179 | do_something(config)
180 | ```
181 |
182 | ## ParamDict & Namespace
183 | ``ParamDict`` from ``exp.args`` module is a very simple dictionary where you can specify default values for different parameters. ``exp.args.Param`` is a named tuple: ``(typefn,default,options)`` where ``typefn`` is a type function like ``int`` or ``float`` that transforms strings into values of the given type if necessary, ``default`` is a default value, ``options`` is a list of possible values for the parameter.
184 |
185 | This is just a very simple alternative to using argparse with a lot of of parameters. Example of usage:
186 |
187 | ```python
188 | from exp.args import ParamDict,Namespace
189 |
190 | # these are interpreted by a ParamDict as a exp.args.Param named tuple
191 | param_spec = {
192 | 'x': (float, None),
193 | 'id': (int, 0),
194 | 'run': (int, 1),
195 | 'cat': (str, "A", ["A","B"])
196 | }
197 |
198 | def run(**kargs):
199 | args = ParamDict(param_spec) # creates a param dict from default values and options
200 | args.from_dict(kargs) # updates the dictionary with new values where the parameter name overlaps
201 | ns = args.to_namespace() # creates a namespace object so you can access ns.x, ns.run etc
202 | ...
203 | ```
204 |
205 | Another nice thing is that there is basic type conversions from string to boolean, int, float, etc. Depending
206 | on the arguments received in ``kwargs``, ``ParamDict`` converts the values automatically according to the parameter
207 | specifications.
208 |
209 | ## Created by
210 | **[Davide Nunes](https://github.com/davidenunes)**
211 |
212 | ## If you find this useful
213 |
214 |
215 | ## Licence
216 |
217 | [Apache License 2.0](LICENSE)
218 |
--------------------------------------------------------------------------------
/exp/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/davidenunes/exp/091dba3b7ef60b463c9be7ad2e9c1458f06a1e38/exp/__init__.py
--------------------------------------------------------------------------------
/exp/__version__.py:
--------------------------------------------------------------------------------
1 | __version__ = "0.5.1"
2 |
--------------------------------------------------------------------------------
/exp/args.py:
--------------------------------------------------------------------------------
1 | """ args is a simple module to allow for quick
2 | specification of function parameters with default arguments,
3 | option restriction and type conversion based on type classes
4 |
5 | Param: namedtuple for specification parameters from tuples
6 | Namespace: simple object that maps param["param"] parameter
7 | dictionaries to namespace.param access.
8 | ParamDict: stores Param instances with default values
9 | """
10 | from collections import namedtuple
11 |
12 |
13 | class Param(namedtuple('Param', ["typefn", "value", "options"])):
14 | """ Simple class representing parameters
15 |
16 | with values, validated/restricted by options
17 | and be converted from other values using a type function
18 | used to read parameter tuples that serve as entries to :obj:`ParamDict`
19 |
20 | """
21 |
22 | def __new__(cls, typefn, value=None, options=None):
23 | if options is not None:
24 | options = set(options)
25 | typefn = convert_type(typefn)
26 |
27 | if value is not None:
28 | value = typefn(value)
29 |
30 | if options is not None and value is not None:
31 | if value not in options:
32 | raise ValueError("Invalid Param Value: {} not in options {}".format(value, options))
33 |
34 | return super().__new__(cls, typefn, value, options)
35 |
36 |
37 | class Namespace(object):
38 | def __init__(self, dict_attr):
39 | if not isinstance(dict_attr, dict):
40 | raise TypeError("Namespace requires a dict, {} found".format(dict_attr))
41 |
42 | for k, v in dict_attr.items():
43 | self.__setattr__(k, v)
44 |
45 | def __str__(self):
46 | attr = ["{}={}".format(k, v) for k, v in self.__dict__.items()]
47 |
48 | return "Namespace({s})".format(s=','.join(attr))
49 |
50 | def __setattr__(self, key, value):
51 | super().__setattr__(key, value)
52 |
53 |
54 | class ParamDict(dict):
55 | """ Dictionary of parameters with default values
56 |
57 | Attributes:
58 | defaults = a dictionary :py:`str` -> :obj:`Param`
59 |
60 | """
61 |
62 | def __init__(self, defaults):
63 | self.defaults = dict()
64 | self.add_params(defaults)
65 |
66 | for arg in self.defaults:
67 | param = self.defaults[arg]
68 | dict.__setitem__(self, arg, param.value)
69 |
70 | # self.__dict__ .update(self)
71 |
72 | def __setitem__(self, key, val):
73 | if key in self.defaults:
74 | default = self.defaults[key]
75 | if val is None:
76 | val = default.value
77 | else:
78 | val = default.typefn(val)
79 | if default.options is not None:
80 | if val not in default.options:
81 | raise ValueError("Invalid Param Value: {} not in options {}".format(val, default.options))
82 |
83 | dict.__setitem__(self, key, val)
84 |
85 | def add_params(self, param_dict):
86 | """ Adds a set of parameter values from a given dictionary to the current values
87 | overwrites the default values for the parameters that already exist in the defaults
88 |
89 | Args:
90 | param_dict: a dictionary with param_name -> (type,vale,options) :obj:`Param`
91 | """
92 | for arg in param_dict:
93 | param = Param(*param_dict[arg])
94 | self.defaults[arg] = param
95 |
96 | def from_dict(self, args):
97 | for arg in args:
98 | self.__setitem__(arg, args[arg])
99 |
100 | def to_namespace(self):
101 | """ Converts the ParamDict to a :obj:`Namespace` object
102 | which allows you to access ``namespace.param1``
103 |
104 | Returns:
105 | a :obj:`Namespace` object with the current values of this parameter dictionary
106 | """
107 | return Namespace(self)
108 |
109 |
110 | def as_bool(v):
111 | """ Converts a given value to a boolean
112 |
113 | Args:
114 | v (int,str,bool): and integer, string, or boolean to be converted to boolean value.
115 | Returns:
116 | (bool): if the value is an int any value <= 0 returns False, else True
117 | if the value is a boolean simply forwards this value
118 | if the value is a string, ignores case and converts any (yes,true,t,y,1) to True
119 | and ('no', 'false', 'f', 'n', '0') to False
120 | """
121 | if v is None:
122 | return False
123 | elif isinstance(v, bool):
124 | return v
125 | elif isinstance(v, str):
126 | if v.lower() in ('yes', 'true', 't', 'y', '1'):
127 | return True
128 | elif v.lower() in ('no', 'false', 'f', 'n', '0'):
129 | return False
130 | else:
131 | raise TypeError('Boolean value expected.')
132 | elif isinstance(v, int) or isinstance(v, float):
133 | if v <= 0:
134 | return False
135 | else:
136 | return True
137 |
138 |
139 | def as_int(v):
140 | """ Converts a value to int by converting it first to float
141 |
142 | because calling ``int("2.3")`` raises a ValueError since 2.3 is
143 | not an integer. ``as_int("2.3")`` returns the same as ``int(2.3)``
144 |
145 | Args:
146 | v: a value convertible to numerical
147 |
148 | Returns:
149 | (int) the integer value of the given value
150 |
151 | """
152 | return int(float(v))
153 |
154 |
155 | def convert_type(type_class):
156 | """ Maps classes to convert functions present in this module
157 | Args:
158 | type_class: some class that can also be called to convert values into its types
159 |
160 |
161 | Returns:
162 | a type conversion function for the given class capable of converting more than literal values, for instance,
163 | requesting a type conversion class for boolean, returns a function capable of converting strings, or integers
164 | to a boolean value (see :obj:`as_bool`)
165 | """
166 | if type_class == bool:
167 | return as_bool
168 | elif type_class == int:
169 | return as_int
170 | else:
171 | return type_class
172 |
--------------------------------------------------------------------------------
/exp/gopt.py:
--------------------------------------------------------------------------------
1 | """ CLI for Hyper parameter optimisation
2 |
3 | prompt_toolkit run sequential model algorithmic optimisation
4 | based on gaussian processes, random forests etc.
5 | """
6 | import sys
7 |
8 | import GPUtil
9 | import click
10 | import csv
11 | import importlib
12 | import logging
13 | import multiprocessing as mp
14 | import os
15 | from matplotlib import pyplot as plt
16 | from multiprocessing import Event
17 | from multiprocessing import Queue, Process
18 | from multiprocessing.queues import Empty
19 | from skopt import Optimizer, plots
20 | from skopt.space import Integer, Real, Categorical
21 | from tqdm import tqdm
22 | from tqdm._utils import _term_move_up
23 | import traceback
24 | from toml import TomlDecodeError
25 | from exp.params import ParamSpace, DTypes, ParamDecodeError
26 |
27 |
28 | def params_to_skopt(param_space: ParamSpace):
29 | """ Converts a parameter space to a list of Dimention objects that can be used with
30 | a skopt Optimizer.
31 |
32 | A skopt Optimizer only receives 3 types of Dimensions: Categorical, Real, or Integer
33 | we convert parameters from our parameter space into one of those 3 types. Note that we only
34 | convert parameters that have either bounds or with a categorical domain with more than 1 value.
35 | If we have constant values in our parameter space, these don't need to be optimized anyway.
36 |
37 | Another function is provided to convert skopt output values back into a dictionary with
38 | a full configuration according to the parameter space (@see values_to_params).
39 |
40 | Args:
41 | param_space: a ParameterSpace where we can get the domain of each parameter
42 |
43 | Returns:
44 | a list of Dimension that can be passed to a skopt Optimizer
45 |
46 | """
47 | dimensions = []
48 | for param_name in param_space.param_names():
49 | domain_param = param_space.domain(param_name)
50 | domain = domain_param["domain"]
51 | dtype = DTypes.from_type(domain_param["dtype"])
52 | if len(domain) > 1:
53 | if dtype == DTypes.INT:
54 | low = min(domain)
55 | high = max(domain)
56 | dimensions.append(Integer(low, high, name=param_name))
57 | elif dtype == DTypes.FLOAT:
58 | low = min(domain)
59 | high = max(domain)
60 | prior = domain_param.get("prior", None)
61 | dimensions.append(
62 | Real(low, high, prior=prior, name=param_name))
63 | elif dtype == DTypes.CATEGORICAL:
64 | prior = domain_param.get("prior", None)
65 | dimensions.append(Categorical(
66 | domain, prior, transform="onehot", name=param_name))
67 | return dimensions
68 |
69 |
70 | def values_to_params(param_values, param_space):
71 | """
72 | We don't need to optimize parameters with a single value in their domain so we filter
73 | them out with params to skopt and convert back to configuration using this method
74 |
75 | Args:
76 | param_values: dict {param_name: value}
77 | param_space: rest of the parameter space used to complete this configuration
78 |
79 | """
80 | cfg = {}
81 | for param_name in param_space.param_names():
82 | if param_name in param_values:
83 | cfg[param_name] = param_values[param_name]
84 | else:
85 | # complete with value from parameter space
86 | value = param_space.get_param(param_name)
87 |
88 | if isinstance(value, (tuple, list)) and len(value) > 1:
89 | raise ValueError(
90 | "don't know how to complete a configuration that might have multiple values")
91 | else:
92 | if isinstance(value, (tuple, list)):
93 | value = value[0]
94 | cfg[param_name] = value
95 |
96 | return cfg
97 |
98 |
99 | def load_module(runnable_path):
100 | """ Loads a python file with the module to be evaluated.
101 |
102 | The module is a python file with a run function member. Each worker then
103 | passes each configuration it receives from a Queue to run as keyword arguments
104 |
105 | Args:
106 | runnable_path:
107 |
108 | Raises:
109 | TypeError: if the loaded module doesn't have a run function
110 |
111 | Returns:
112 | a reference to the newly loaded module so
113 |
114 | """
115 | runnable_path = os.path.abspath(runnable_path)
116 | spec = importlib.util.spec_from_file_location(
117 | "runnable", location=runnable_path)
118 | runnable = importlib.util.module_from_spec(spec)
119 | spec.loader.exec_module(runnable)
120 |
121 | try:
122 | getattr(runnable, "run")
123 | except AttributeError:
124 | raise TypeError(
125 | "module in {} does not contain a \"run\" method".format(runnable_path))
126 |
127 | return runnable
128 |
129 |
130 | def submit(n, optimizer: Optimizer, opt_param_names, current_configs, param_space: ParamSpace, queue: Queue):
131 | """ Generate and submit n new configurations to a queue.
132 |
133 | Asks the optimizer for n new values to explore, creates configurations for those points and puts them
134 | in the given queue.
135 |
136 | Args:
137 | n: the number of configurations to be generated
138 | optimizer: the optimiser object from skopt with the model used for the suggested points to explore
139 | opt_param_names: the names for the parameters using the same order of the dimensions in the optimizer
140 | current_configs: current list of configurations (updated with the newly generated ones)
141 | param_space: parameter space which we can use to convert optimizer points to fully specified configurations
142 | queue: que multiprocessing queue in which we put the new configurations
143 | """
144 | dims = opt_param_names
145 | xs = optimizer.ask(n_points=n)
146 | cfgs = [values_to_params(dict(zip(dims, x)), param_space) for x in xs]
147 | for i, c in enumerate(cfgs):
148 | c["id"] = i + len(current_configs)
149 | queue.put(c)
150 | current_configs += cfgs
151 |
152 |
153 | def worker(pid: int,
154 | module_path: str,
155 | config_queue: Queue,
156 | result_queue: Queue,
157 | error_queue: Queue,
158 | terminated: Event):
159 | """ Worker to be executed in its own process
160 |
161 | Args:
162 | module_path: path to model runnable that must be imported and runned on a given configuration
163 | terminated: each worker should have its own flag
164 | pid: (int) with worker id
165 | config_queue: configuration queue used to receive the parameters for this worker, each configuration is a task
166 | result_queue: queue where the worker deposits the results
167 |
168 | Returns:
169 | each time a new result is returned from calling the run function on the module, the worker puts this in to its
170 | result multiprocessing Queue in the form (worker_id, configuration_id, result)
171 |
172 | If an exception occurs during run(...) the worker puts that exception as the result into the queue instead
173 |
174 | """
175 | # ids should be 0, 1, 2, n where n is the maximum number of gpus available
176 | os.environ["CUDA_VISIBLE_DEVICES"] = str(pid)
177 | module = load_module(module_path)
178 |
179 | while not terminated.is_set():
180 | try:
181 | kwargs = config_queue.get(timeout=0.5)
182 | cfg_id = kwargs["id"]
183 | kwargs["pid"] = pid
184 | # e.g. model score
185 | result = module.run(**kwargs)
186 | result_queue.put((pid, cfg_id, result))
187 | except Empty:
188 | # nothing to do, check if it's time to terminate
189 | pass
190 | except Exception as e:
191 | terminated.set()
192 | error_queue.put((pid, cfg_id, traceback.format_exc()))
193 | result_queue.put((pid, cfg_id, e))
194 |
195 |
196 | def update_progress_kuma(progress):
197 | tqdm.write(_term_move_up() * 10) # move cursor up
198 | offset = " " * int(progress.n / progress.total * (progress.ncols - 40))
199 |
200 | tqdm.write(offset + ' _______________')
201 | tqdm.write(offset + ' | |')
202 | tqdm.write(offset + ' | KUMA-SAN IS |')
203 | tqdm.write(offset + ' | OPTIMIZING! |')
204 | tqdm.write(
205 | offset + ' | {:>3}/{:<3} |'.format(progress.n, progress.total))
206 | tqdm.write(offset + ' |________|')
207 | tqdm.write(offset + ' ( ) ( )||')
208 | tqdm.write(offset + ' ( •(エ)•)|| ')
209 | tqdm.write(offset + ' / づ')
210 |
211 |
212 | @click.command(help='optimizes the hyperparameters for a given function',
213 | context_settings=dict(help_option_names=['-h', '--help'])
214 | )
215 | @click.option('-p', '--params', required=True, type=click.Path(exists=True), help='path to parameter space file')
216 | @click.option('-m', '--module', required=True, type=click.Path(exists=True), help='path to python module file')
217 | @click.option('-w', '--workers', default=1, type=int, help="number of workers: limited to CPU core count or GPU "
218 | "count, cannot be <=0.")
219 | @click.option('-g', '--gpu', is_flag=True,
220 | help="bounds the number of workers to the number of available GPUs (not under load)."
221 | "Each process only sees a single GPU.")
222 | @click.option('-n', '--n', default=1, type=int, help='number of configuration runs')
223 | @click.option('--name', default="opt", type=str,
224 | help='optimization experiment name: used as prefix for some output files')
225 | @click.option('-s', '--surrogate', default="GP",
226 | type=click.Choice(["GP", "RF", "ET"], case_sensitive=False),
227 | help='surrogate model for global optimisation: '
228 | '(GP) gaussian process, '
229 | '(RF) random forest, or'
230 | '(ET) extra trees.')
231 | @click.option('-a', '--acquisition', default="EI",
232 | type=click.Choice(["LCB", "EI", "PI"], case_sensitive=False),
233 | help='acquisition function: '
234 | '(LCB) Lower Confidence Bound, '
235 | '(EI) Expected Improvement, or '
236 | '(PI) Probability of Improvement.')
237 | @click.option('--plot', is_flag=True, help='shows a convergence plot during the optimization process and saves it at'
238 | 'the current working dir')
239 | @click.option('-o', '--out', type=click.Path(), help="output directory for the results file. If plotting "
240 | "convergence, the plot is also saved in the first directory in the"
241 | "path.")
242 | @click.option('--sync/--async', default=False, help="--async (default) submission, means that we submit a new "
243 | "configuration to a worker for each new result the optimizer gets."
244 | "--sync mode makes the optimizer wait for all workers before "
245 | "submitting new configurations to all of them")
246 | @click.option('--kappa', type=float, default=1.96, help="(default=1.96) Used when the acquisition is LCB."
247 | "Controls how much of the variance in the "
248 | "predicted values should be taken into account. High values "
249 | "favour exploration vs exploitation")
250 | @click.option('--xi', type=float, default=0.01, help="(default=0.01) Used with EI acquisition: controls how much "
251 | "improvement we want over previous best.")
252 | @click.option('--kuma', is_flag=True, help='kuma-san will display the progress on your global optimization procedure.')
253 | def run(params, module, workers, gpu, n, surrogate, acquisition, name, plot, out, sync, kappa, xi, kuma):
254 | logger = logging.getLogger(__name__)
255 | handler = logging.FileHandler('{name}.log'.format(name=name), delay=True)
256 | handler.setLevel(logging.ERROR)
257 | formatter = logging.Formatter(
258 | '%(asctime)s - %(name)s - %(levelname)s - %(message)s')
259 | handler.setFormatter(formatter)
260 | logger.addHandler(handler)
261 |
262 | opt_results = None
263 | out_file = None
264 | try:
265 | if gpu:
266 | # detecting available gpus with load < 0.1
267 | gpu_ids = [g.id for g in GPUtil.getGPUs() if g.load < 0.2]
268 | num_workers = min(workers, len(gpu_ids))
269 |
270 | if num_workers <= 0:
271 | sys.exit(1)
272 | else:
273 | num_workers = min(workers, mp.cpu_count())
274 |
275 | logger.log(logging.DEBUG, "Spawning {} workers".format(num_workers))
276 | if num_workers <= 0:
277 | logger.log(logging.ERROR, "--workers cannot be 0")
278 | sys.exit(1)
279 |
280 | # prepare output file
281 | out_file_name = '{}_configurations.csv'.format(name)
282 | out = out_file_name if out is None else out
283 | if out is not None and os.path.isdir(out):
284 | out_file_path = os.path.join(out, out_file_name)
285 | else:
286 | out_file_path = out
287 |
288 | out_dir = os.path.abspath(os.path.join(out_file_path, os.pardir))
289 | out_file_path = os.path.join(out_dir, out_file_name)
290 |
291 | param_space = ParamSpace(params)
292 |
293 | dimensions = params_to_skopt(param_space)
294 | optimizer_dims = [d.name for d in dimensions]
295 | acquisition_kwargs = None
296 | if acquisition == "LCB":
297 | acquisition_kwargs = {'kappa': kappa}
298 | elif acquisition == "EI":
299 | acquisition_kwargs = {'xi': xi}
300 |
301 | optimizer = Optimizer(dimensions=dimensions,
302 | acq_func_kwargs=acquisition_kwargs,
303 | base_estimator=surrogate,
304 | acq_func=acquisition)
305 |
306 | out_file = open(out_file_path, 'w')
307 | out_writer = csv.DictWriter(
308 | out_file, fieldnames=param_space.param_names() + ["id", "evaluation"])
309 | out_writer.writeheader()
310 |
311 | # setup process pool and queues
312 | # manager = mp.Manager()
313 | config_queue = Queue()
314 | result_queue = Queue()
315 | error_queue = Queue()
316 |
317 | terminate_flags = [Event() for _ in range(num_workers)]
318 | processes = [
319 | Process(target=worker, args=(i, module, config_queue,
320 | result_queue, error_queue, terminate_flags[i]))
321 | for i in range(num_workers)]
322 |
323 | configs = []
324 | scores = {}
325 | # get initial points at random and submit one job per worker
326 | submit(num_workers, optimizer, optimizer_dims,
327 | configs, param_space, config_queue)
328 | # cfg_if: score
329 |
330 | num_completed = 0
331 | pending = len(configs)
332 | cancel = False
333 |
334 | for p in processes:
335 | p.daemon = True
336 | p.start()
337 |
338 | if plot:
339 | fig = plt.gcf()
340 | fig.show()
341 | fig.canvas.draw()
342 |
343 | progress_bar = tqdm(total=n, leave=True)
344 |
345 | if kuma:
346 | update_progress_kuma(progress_bar)
347 |
348 | while num_completed < n and not cancel:
349 | try:
350 | res = result_queue.get(timeout=1)
351 | pid, cfg_id, result = res
352 | if not isinstance(result, Exception):
353 | cfg = configs[cfg_id]
354 | # convert dictionary to x vector that optimizer takes
355 | x = [cfg[param] for param in optimizer_dims]
356 | # store scores for each config
357 | scores[cfg_id] = result
358 |
359 | out_row = dict(cfg)
360 | out_row["evaluation"] = result
361 | out_writer.writerow(out_row)
362 | # make sure we can see the results in the file as we run the optimizer
363 | out_file.flush()
364 | opt_results = optimizer.tell(x, result)
365 |
366 | num_completed += 1
367 | pending -= 1
368 |
369 | if plot:
370 | plots.plot_convergence(opt_results)
371 | fig.canvas.draw()
372 |
373 | # sync submission of jobs means we wait for all workers to finish
374 | if sync and pending == 0:
375 | if num_completed != n:
376 | num_submit = min(num_workers, n - num_completed)
377 | submit(num_submit, optimizer, optimizer_dims,
378 | configs, param_space, config_queue)
379 | pending = num_submit
380 | else:
381 | terminate_flags[pid].set()
382 |
383 | # async submission of jobs: as soon as we receive one result we submit the next
384 | if not sync:
385 | if (num_completed + pending) != n:
386 | submit(1, optimizer, optimizer_dims,
387 | configs, param_space, config_queue)
388 | pending += 1
389 | else:
390 | # signal the current worker for termination
391 | terminate_flags[pid].set()
392 |
393 | progress_bar.update()
394 | progress_bar.set_postfix(
395 | {"best solution ": opt_results["fun"]})
396 |
397 | if kuma:
398 | update_progress_kuma(progress_bar)
399 |
400 | else:
401 | _, cfg_id_err, err = error_queue.get()
402 | logger.error("configuration {} failed".format(cfg_id_err))
403 | logger.error(err)
404 |
405 | cancel = True
406 | except Empty:
407 | pass
408 |
409 | # try to wait for process termination
410 | for process in processes:
411 | process.join(timeout=0.5)
412 |
413 | if process.is_alive():
414 | process.terminate()
415 |
416 | progress_bar.close()
417 |
418 | except TomlDecodeError as e:
419 | logger.error(traceback.format_exc())
420 | print("\n\n[Invalid parameter file] TOML decode error:\n {}".format(
421 | e), file=sys.stderr)
422 | except ParamDecodeError as e:
423 | logger.error(traceback.format_exc())
424 | print("\n\n[Invalid parameter file]\n {}".format(e), file=sys.stderr)
425 | except Exception as e:
426 | logger.error(traceback.format_exc())
427 | raise e
428 | except KeyboardInterrupt:
429 | pass
430 | finally:
431 | # debugging
432 | if opt_results is not None and plot:
433 | plt_file = '{}_convergence.pdf'.format(name)
434 | out_path = os.path.join(out_dir, plt_file)
435 | plt.savefig(out_path, bbox_inches='tight')
436 |
437 | if out_file is not None:
438 | out_file.close()
439 |
440 |
441 | if __name__ == '__main__':
442 | run()
443 |
--------------------------------------------------------------------------------
/exp/params.py:
--------------------------------------------------------------------------------
1 | """ Parameter Space definition writing and loading
2 |
3 | :obj:`ParamSpace` uses TOML as the underlying format to write and load
4 | configurations to and from
5 | """
6 | import csv
7 | import itertools
8 | import math
9 | import os
10 | from enum import Enum
11 |
12 | import numpy as np
13 |
14 | import toml
15 |
16 |
17 | def _repeat_it(iterable, n):
18 | return itertools.chain.from_iterable(itertools.repeat(x, n) for x in iterable)
19 |
20 |
21 | class ParamError(Exception):
22 | pass
23 |
24 |
25 | class ParamDecodeError(ParamError):
26 | pass
27 |
28 |
29 | class Types(Enum):
30 | """ Enum with valid parameter types supported by :obj:`ParamSpace`
31 |
32 | """
33 | VALUE = "value"
34 | LIST = "list"
35 | RANGE = "range"
36 | RANDOM = "random"
37 |
38 | @staticmethod
39 | def from_str(value, case_sensitive=False):
40 | if not case_sensitive:
41 | value.lower()
42 | if value == Types.VALUE.value:
43 | return Types.VALUE
44 | elif value == Types.LIST.value:
45 | return Types.LIST
46 | elif value == Types.RANGE.value:
47 | return Types.RANGE
48 | elif value == Types.RANDOM.value:
49 | return Types.RANDOM
50 | else:
51 | raise ValueError("invalid parameter type: {}\n"
52 | "supported values: {}".format(value,
53 | ",".join([t.name for t in Types])))
54 |
55 |
56 | class DTypes(Enum):
57 | """ Enum with valid parameter dtypes supported by :obj:`ParamSpace`
58 |
59 | Useful to convert parameter spaces into scikit-optimization Dimensions
60 | """
61 | INT = "int"
62 | FLOAT = "float"
63 | CATEGORICAL = "categorical"
64 |
65 | @staticmethod
66 | def from_type(dtype, case_sensitive=False):
67 | if not case_sensitive and isinstance(dtype, str):
68 | dtype.lower()
69 |
70 | if dtype in (DTypes.FLOAT.value, float):
71 | return DTypes.FLOAT
72 | elif dtype in (DTypes.INT.value, int):
73 | return DTypes.INT
74 | elif dtype == DTypes.CATEGORICAL.value:
75 | return DTypes.CATEGORICAL
76 | else:
77 | raise ValueError("invalid parameter dtype: {}\n"
78 | "supported values: {}".format(dtype,
79 | ",".join([t.name for t in DTypes])))
80 |
81 |
82 | class ParamSpace:
83 | """ ParamSpace
84 |
85 | Create parameter spaces from configuration files.
86 |
87 | ParamSpace creates and read from parameter configuration files
88 | to create parameter spaces used for hyperparameter grid search
89 | (:py:mod:`exp.run`) or Global optimization procedures (:py:mod:`exp.gopt`)
90 |
91 | Args:
92 | filename (str): [optional] path to a configuration file. If not specified, creates an empty ParamSpace.
93 | """
94 |
95 | def __init__(self, filename=None):
96 | if filename is not None:
97 | self.params = toml.load(filename)
98 | else:
99 | self.params = {}
100 | self.size = self._compute_size()
101 |
102 | def _compute_size(self):
103 | """ Returns the size of the parameter space in terms of number of
104 | unique configurations
105 |
106 | Returns:
107 | (int) number of unique configurations
108 |
109 | """
110 | params = self.params.keys()
111 | if len(params) > 0:
112 | size = 1
113 | else:
114 | return 0
115 |
116 | for param in params:
117 | param_type = Types.from_str(self.params[param]["type"])
118 | param_value = self.get_param(param, param_type)
119 |
120 | if param_type == Types.VALUE:
121 | param_value = [param_value]
122 | size *= len(param_value)
123 | return size
124 |
125 | def param_names(self):
126 | """ param_names.
127 |
128 | Returns:
129 | (list) list of strings with the names of the parameters in this space
130 |
131 | """
132 | return list(self.params.keys())
133 |
134 | def _update_space_size(self, n):
135 | """ Updates the static grid size each instance maintains so it's cheap to return it
136 |
137 | Args:
138 | n: number of values a new parameter being added has
139 | """
140 | if self.size == 0:
141 | self.size = n
142 | else:
143 | self.size *= n
144 |
145 | def _get_param(self, name, param_type):
146 | """ get parameter
147 |
148 | gets a parameter and checks if a parameter is of a given type
149 |
150 | Args:
151 | name: name of the parameter to be returned
152 | param_type: (Type) parameter type
153 |
154 | Raises:
155 | LookupError: if parameter is not found in parameter space
156 | TypeError: if parameter found is not of the type specified
157 |
158 | Returns:
159 | (dict) dictionary with the parameter value and configurations
160 |
161 | """
162 | if name not in self.params:
163 | raise LookupError("Parameter not found: {}".format(name))
164 |
165 | param = self.params[name]
166 | actual_type = Types.from_str(param['type'])
167 | if actual_type != param_type:
168 | raise TypeError("expected {param} to be a {expected} but got {actual}".format(param=name,
169 | expected=param_type,
170 | actual=actual_type))
171 |
172 | return param
173 |
174 | def add_random(self, name, low=0., high=1., prior="uniform", dtype=float, n=None, persist=False):
175 | """ Specify random params within bounds and a given prior distribution
176 |
177 | Args:
178 | name: name for the parameter
179 | low: lower bound for the distribution (optional)
180 | high: higher bound for the distribution (optional)
181 | prior: distribution to be used for the random sampling, one of the following values:
182 | -uniform
183 | -log-uniform
184 | n: number of random values to be sampled if persist
185 | persist:
186 | Returns:
187 |
188 | """
189 | if dtype not in (float, int):
190 | raise TypeError(
191 | """\n Unknown dtype "{}", valid dtypes are:\n \t-float, \n \t-int """.format(dtype))
192 |
193 | param = self.params[name] = {}
194 | param["type"] = Types.RANDOM.value
195 | param["dtype"] = dtype.__name__
196 | if n:
197 | param["n"] = n
198 | self._update_space_size(n)
199 | else:
200 | self._update_space_size(1)
201 |
202 | param["bounds"] = [low, high]
203 |
204 | if prior not in ("uniform", "log-uniform"):
205 | raise TypeError(
206 | """\n Unknown prior "{}", valid priors are:\n \t-"uniform", \n \t-"log-uniform" """.format(prior))
207 | param["prior"] = prior
208 |
209 | if persist:
210 | value = self.get_random(name)
211 | del self.params[name]
212 | self.add_list(name, value)
213 |
214 | def get_random(self, name):
215 | """ get random parameter
216 |
217 | Args:
218 | name: parameter name
219 |
220 | Returns:
221 | array with one or more random parameter values according to the prior distribution
222 | and the given bounds, if no bounds are found defaults to [0,1), if no priors
223 | are found uniform is used
224 |
225 | """
226 | param: dict = self._get_param(name, Types.RANDOM)
227 | n = param.get("n", 1)
228 | bounds = param.get("bounds", [0, 1])
229 | prior = param.get("prior", "uniform")
230 | dtype = param.get("dtype", "float")
231 |
232 | if prior == "uniform":
233 | if dtype == "float":
234 | rvs = np.random.uniform(low=bounds[0], high=bounds[1], size=n)
235 | else:
236 | rvs = np.random.randint(low=bounds[0], high=bounds[1], size=n)
237 | elif prior == "log-uniform":
238 | if dtype == "float":
239 | if bounds[0] == 0:
240 | raise ParamDecodeError(
241 | "Invalid bounds for parameter [{}] lower bound on a log space cannot be 0".format(name))
242 | low = np.log10(bounds[0])
243 | high = np.log10(bounds[1])
244 | rvs = np.power(10, np.random.uniform(low, high, size=n))
245 | else:
246 | raise ParamDecodeError(
247 | """\n Invalid prior value "{}" for parameter [{}] with dtype="float":
248 | random integer only supports "uniform" prior """.format(prior, name))
249 | else:
250 | raise ParamDecodeError(
251 | """\n Invalid prior value "{}" for parameter [{}], valid priors are:\n
252 | \t"uniform" \n
253 | \t"log-uniform" """.format(prior, name))
254 |
255 | return rvs
256 |
257 | def add_value(self, name, value):
258 | """ Create a single value parameter
259 |
260 | Args:
261 | name: parameter name
262 | value: value to be attributed to param
263 | """
264 | param = self.params[name] = {}
265 | param["type"] = Types.VALUE.value
266 | param["value"] = value
267 | self._update_space_size(1)
268 |
269 | def get_value(self, name):
270 | """ get single value parameter
271 |
272 | Args:
273 | name: parameter name
274 |
275 | Returns:
276 | obj some value for the single value parameter
277 |
278 | """
279 | param = self._get_param(name, Types.VALUE)
280 | return param["value"]
281 |
282 | def add_list(self, name, values):
283 | """ Create list parameter
284 |
285 | Args:
286 | name: parameter name
287 | values: list of values
288 | """
289 | values = list(values)
290 | param = self.params[name] = {}
291 | param["type"] = Types.LIST.value
292 | param["value"] = values
293 | self._update_space_size(len(values))
294 |
295 | def add_range(self, name, low=0, high=1, step=1, dtype=float):
296 | """ Creates range parameter
297 |
298 | Args:
299 | name: name for the parameter
300 | low: where the range starts
301 | high: where the range stops (not included in the values)
302 | step: distance between each point in the range
303 | dtype: float or int if int the points in the range are rounded
304 | """
305 | param = self.params[name] = {}
306 | param["type"] = Types.RANGE.value
307 | param["bounds"] = [low, high]
308 | param["step"] = step
309 | param["dtype"] = dtype.__name__
310 | self._update_space_size(math.ceil((high - low) / step))
311 |
312 | def get_range(self, name):
313 | """ get range value for given range parameter
314 |
315 | works like numpy.arange
316 |
317 | Args:
318 | name: parameter name associated with the range
319 |
320 | Returns:
321 | an array with n numbers according to the range specification
322 | """
323 | param = self._get_param(name, Types.RANGE)
324 | if "bounds" not in param:
325 | raise ParamDecodeError(""" "bounds" not found for parameter {}""".format(name))
326 | low = float(param["bounds"][0])
327 | # high = float(param["bounds"]["high"])
328 | high = float(param["bounds"][1])
329 |
330 | step = float(param.get("step", 1.0))
331 | if "dtype" not in param:
332 | dtype = float
333 | else:
334 | dtype = param["dtype"]
335 | if dtype not in ("int", "float"):
336 | raise ParamDecodeError("""invalid "dtype" for parameter "{}":
337 | expected int or float, got {}""".format(name, dtype))
338 |
339 | dtype = int if dtype == "int" else float
340 |
341 | return np.arange(low, high, step, dtype=dtype)
342 |
343 | def get_list(self, name, unique=False):
344 | """ get list parameter
345 |
346 | Args:
347 | name: parameter name
348 |
349 | Returns:
350 | a list with the parameter values
351 |
352 | """
353 | param = self._get_param(name, Types.LIST)
354 |
355 | # return list of unique items
356 | value = param["value"]
357 | if not unique:
358 | return value
359 | else:
360 | _, idx = np.unique(value, return_index=True)
361 | return np.array(value)[np.sort(idx)].tolist()
362 |
363 | def get_param(self, param, type=None):
364 | if param not in self.params:
365 | raise KeyError("Parameter {} not found".format(param))
366 |
367 | if "type" not in self.params[param]:
368 | raise ValueError("Parameter found but not specified properly: missing \"type\" property")
369 | actual_type = Types.from_str(self.params[param]["type"])
370 |
371 | if type is not None and actual_type != type:
372 | raise ValueError("Parameter {p} has type {ta}, you requested {t}".format(p=param, ta=actual_type, t=type))
373 |
374 | if actual_type == Types.RANGE:
375 | return self.get_range(param)
376 | elif actual_type == Types.VALUE:
377 | return self.get_value(param)
378 | elif actual_type == Types.LIST:
379 | return self.get_list(param)
380 | elif actual_type == Types.RANDOM:
381 | return self.get_random(param)
382 | else:
383 | raise TypeError("Unknown Parameter Type")
384 |
385 | def domain(self, param_name):
386 |
387 | param = self.params[param_name]
388 | param_type = Types.from_str(param["type"])
389 | prior = param.get("prior", None)
390 |
391 | if param_type == Types.LIST:
392 | return {"domain": self.get_list(param_name, unique=True),
393 | "dtype": DTypes.CATEGORICAL.value}
394 |
395 | elif param_type == Types.RANDOM:
396 | bounds = param.get("bounds", [0., 1.])
397 | dtype = param.get("dtype", DTypes.FLOAT.value)
398 |
399 | if prior is None:
400 | prior = "uniform"
401 | return {"domain": bounds, "dtype": dtype, "prior": prior}
402 |
403 | elif param_type == Types.RANGE:
404 | dtype = param.get("dtype", DTypes.FLOAT.value)
405 | bounds = param.get("bounds", [0., 1.])
406 | if prior is None:
407 | prior = "uniform"
408 | return {"domain": bounds, "dtype": dtype, "prior": prior}
409 |
410 | else:
411 | dtype = param.get("dtype", DTypes.CATEGORICAL.value)
412 | bounds = [self.get_param(param_name)]
413 | return {"domain": bounds, "dtype": dtype}
414 |
415 | def sample_param(self, name):
416 | """ draws a sample from a single parameter
417 |
418 | If this is a value it returns the value itself
419 | for a list or range draws one element uniformly at random
420 | for a random parameter, respects the distribution specified
421 |
422 | Args:
423 | name: parameter name to be sampled
424 |
425 | Returns:
426 | returns the sampled value for the given parameter name
427 |
428 | """
429 | if name in self.params:
430 | param = self.params[name]
431 | param_type = Types.from_str(param["type"])
432 | value = self.get_param(name, param_type)
433 | if param_type == Types.VALUE:
434 | return value
435 | elif param_type in (Types.LIST, Types.RANGE):
436 | randi = np.random.randint(0, len(value))
437 | return value[randi]
438 | elif param_type == Types.RANDOM:
439 | return value[0]
440 | else:
441 | raise KeyError("{} not in parameter space".format(name))
442 |
443 | def sample_space(self):
444 | """ Samples a configuration from the parameter space
445 |
446 | Returns:
447 | a dictionary with the sampled configuration
448 | """
449 | return {param: self.sample_param(param) for param in self.params.keys()}
450 |
451 | def param_grid(self, runs=1):
452 | """ Returns a generator of dictionaries with all the possible parameter combinations
453 | the keys are the parameter names the values are the current value for each parameter.
454 |
455 | Warnings:
456 | you shouldn't use id and run as parameter names, param grid automatically uses those names to identify each
457 | parameter combination.
458 |
459 | Args:
460 | runs(int): number of repeats for each unique configuration
461 | """
462 | if runs < 1:
463 | raise ValueError("runs must be >0: runs set to {}".format(runs))
464 |
465 | param_values = []
466 | params = list(self.params.keys())
467 |
468 | for param in self.params.keys():
469 | param_type = self.params[param]["type"]
470 | param_type = Types.from_str(param_type)
471 | param_value = self.get_param(param, param_type)
472 | if param_type == Types.VALUE:
473 | param_value = [param_value]
474 | param_values.append(param_value)
475 |
476 | if runs < 1:
477 | raise ValueError("runs must be >0: runs set to {}".format(runs))
478 | # add run to parameter names and run number to parameters
479 | params.append("run")
480 | run_ids = np.linspace(1, runs, runs, dtype=np.int32)
481 | param_values.append(run_ids)
482 |
483 | param_product = itertools.product(*param_values)
484 | param_product = (dict(zip(params, values)) for values in param_product)
485 |
486 | # create ids for each unique configuration
487 | ids = np.linspace(0, self.size - 1, self.size, dtype=np.int32)
488 | ids = list(_repeat_it(ids, runs))
489 | id_param_names = ["id"] * (self.size * runs)
490 |
491 | id_params = [{k: v} for k, v in zip(id_param_names, ids)]
492 |
493 | param_product = ({**p, **i} for i, p in zip(id_params, param_product))
494 | return param_product
495 |
496 | def write(self, filename):
497 | with open(filename, "w") as f:
498 | toml.dump(self.params, f)
499 |
500 | def write_configs(self, output_path="params.csv"):
501 | """ Writes a csv file with each line containing a configuration value with a unique id for each configuration
502 |
503 | Args:
504 | output_path: the output path for the summary file
505 | """
506 | summary_header = ["id", "run"]
507 | summary_header += self.param_names()
508 |
509 | with open(output_path, mode="w", newline='') as outfile:
510 | writer = csv.DictWriter(outfile, fieldnames=summary_header)
511 | writer.writeheader()
512 |
513 | param_grid = self.param_grid()
514 | for param_row in param_grid:
515 | writer.writerow(param_row)
516 |
517 | def write_config_files(self, output_path="params", file_prefix="params"):
518 | """ Writes one configuration file per unique configuration in the grid space
519 | Args:
520 | output_path:
521 | file_prefix:
522 | """
523 | param_grid = self.param_grid()
524 | conf_id = 0
525 | for current_config in param_grid:
526 | conf_file = "{prefix}_{id}.conf".format(prefix=file_prefix, id=conf_id)
527 | conf_file = os.path.join(output_path, conf_file)
528 |
529 | with open(conf_file, "w") as f:
530 | toml.dump(current_config, f)
531 |
532 | conf_id += 1
533 |
--------------------------------------------------------------------------------
/exp/run.py:
--------------------------------------------------------------------------------
1 | import sys
2 |
3 | import GPUtil
4 |
5 | import importlib
6 | import logging
7 | import multiprocessing as mp
8 | import os
9 | import traceback
10 | import click
11 | from toml import TomlDecodeError
12 | from tqdm import tqdm
13 | from multiprocessing import Queue, Event, Process
14 | from multiprocessing.queues import Empty as QueueEmpty
15 | from exp.params import ParamSpace, ParamDecodeError
16 |
17 |
18 | def load_module(runnable_path):
19 | """ Loads a python file with the module to be evaluated.
20 |
21 | The module is a python file with a run function member. Each worker then
22 | passes each configuration it receives from a Queue to run as keyword arguments
23 |
24 | Args:
25 | runnable_path:
26 |
27 | Raises:
28 | TypeError: if the loaded module doesn't have a run function
29 |
30 | Returns:
31 | a reference to the newly loaded module so
32 |
33 | """
34 | runnable_path = os.path.abspath(runnable_path)
35 | spec = importlib.util.spec_from_file_location("runnable", location=runnable_path)
36 | runnable = importlib.util.module_from_spec(spec)
37 | spec.loader.exec_module(runnable)
38 | try:
39 | getattr(runnable, "run")
40 | except AttributeError:
41 | raise TypeError("module in {} does not contain a \"run\" method".format(runnable_path))
42 |
43 | return runnable
44 |
45 |
46 | def worker(pid: int,
47 | module_path: str,
48 | config_queue: Queue,
49 | result_queue: Queue,
50 | error_queue: Queue,
51 | terminated: QueueEmpty,
52 | cancel):
53 | """ Worker to be executed in its own process
54 |
55 | Args:
56 | cancel: if true terminates when an error is encountered, otherwise keeps running
57 | error_queue: used to pass formatted stack traces to the main process
58 | module_path: path to model runnable that is imported. It's method run is called on a given configuration
59 | terminated: each worker should have its own flag
60 | pid: (int) with worker id
61 | config_queue: configuration queue used to receive the parameters for this worker, each configuration is a task
62 | result_queue: queue where the worker deposits the results
63 |
64 | Returns:
65 | each time a new result is returned from calling the run function on the module, the worker puts this in to its
66 | result multiprocessing Queue in the form (worker_id, configuration_id, result)
67 |
68 | If an exception occurs during run(...) the worker puts that exception as the result into the queue instead
69 |
70 | """
71 |
72 | os.environ["CUDA_VISIBLE_DEVICES"] = str(pid)
73 | module = load_module(module_path)
74 |
75 | while not terminated.is_set():
76 | try:
77 | kwargs = config_queue.get(timeout=0.5)
78 | cfg_id = kwargs["id"]
79 | kwargs["pid"] = pid
80 | result = module.run(**kwargs)
81 | result_queue.put((pid, cfg_id, result))
82 | except QueueEmpty:
83 | pass
84 | except Exception as e:
85 | if cancel:
86 | terminated.set()
87 | error_queue.put((pid, cfg_id, traceback.format_exc()))
88 | result_queue.put((pid, cfg_id, e))
89 |
90 |
91 | @click.command(help='runs all the configurations in a defined space')
92 | @click.option('-p', '--params', required=True, type=click.Path(exists=True), help='path to parameter space file')
93 | @click.option('-m', '--module', required=True, type=click.Path(exists=True), help='path to python module file')
94 | @click.option('-r', '--runs', default=1, type=int, help='number of configuration runs')
95 | @click.option('--name', default="exp", type=str, help='experiment name: used as prefix for some output files')
96 | @click.option('-w', '--workers', default=1, type=int, help="number of workers: limited to CPU core count or GPU "
97 | "count, cannot be <=0.")
98 | @click.option('-g', '--gpu', is_flag=True,
99 | help="bounds the number of workers to the number of available GPUs (not under load)."
100 | "Each process only sees a single GPU.")
101 | @click.option('-c', '--config-ids', type=int, multiple=True)
102 | @click.option('--cancel', is_flag=True, help="cancel all tasks if one fails")
103 | def main(params, module, runs, name, workers, gpu, config_ids, cancel):
104 | logger = logging.getLogger(__name__)
105 | handler = logging.FileHandler('{name}.log'.format(name=name), delay=True)
106 | handler.setLevel(logging.ERROR)
107 | formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
108 | handler.setFormatter(formatter)
109 | logger.addHandler(handler)
110 |
111 | try:
112 | if gpu:
113 | # detecting available gpus with load < 0.1
114 | worker_ids = [g.id for g in GPUtil.getGPUs() if g.load < 0.2]
115 | num_workers = min(workers, len(worker_ids))
116 |
117 | if num_workers <= 0:
118 | logger.log(logging.ERROR, "no gpus available")
119 | sys.exit(1)
120 | else:
121 | num_workers = min(workers, mp.cpu_count())
122 | if num_workers <= 0:
123 | logger.log(logging.ERROR, "--workers cannot be 0")
124 | sys.exit(1)
125 |
126 | ps = ParamSpace(filename=params)
127 | ps.write_configs('{}_params.csv'.format(name))
128 |
129 | param_grid = ps.param_grid(runs=runs)
130 | n_tasks = ps.size * runs
131 |
132 | if len(config_ids) > 0:
133 | n_tasks = len(config_ids) * runs
134 | param_grid = [p for p in param_grid if p["id"] in config_ids]
135 | param_grid = iter(param_grid)
136 |
137 | num_workers = min(n_tasks, num_workers)
138 |
139 | print("----------Parameter Space Runner------------")
140 | print(":: tasks: {}".format(n_tasks))
141 | print(":: workers: {}".format(num_workers))
142 | print("--------------------------------------------")
143 |
144 | config_queue = Queue()
145 | result_queue = Queue()
146 | error_queue = Queue()
147 | progress_bar = tqdm(total=n_tasks, leave=True)
148 |
149 | terminate_flags = [Event() for _ in range(num_workers)]
150 | processes = [
151 | Process(target=worker,
152 | args=(i, module, config_queue, result_queue, error_queue, terminate_flags[i], cancel))
153 | for i in range(num_workers)]
154 |
155 | scores = {}
156 | configs = {}
157 |
158 | # submit num worker jobs
159 | for _ in range(num_workers):
160 | next_cfg = next(param_grid)
161 | configs[next_cfg["id"]] = next_cfg
162 | config_queue.put(next_cfg)
163 |
164 | for p in processes:
165 | p.daemon = True
166 | p.start()
167 |
168 | num_completed = 0
169 | pending = num_workers
170 | done = False
171 | successful = set()
172 |
173 | while num_completed < n_tasks and not done:
174 | try:
175 | res = result_queue.get(timeout=1)
176 | pid, cfg_id, result = res
177 | if not isinstance(result, Exception):
178 | successful.add(cfg_id)
179 | # cfg = configs[cfg_id]
180 | scores[cfg_id] = result
181 | num_completed += 1
182 | pending -= 1
183 |
184 | if (num_completed + pending) != n_tasks:
185 | next_cfg = next(param_grid)
186 | configs[next_cfg["id"]] = next_cfg
187 | config_queue.put(next_cfg)
188 |
189 | pending += 1
190 | else:
191 | # signal the current worker for termination no more work to be done
192 | terminate_flags[pid].set()
193 |
194 | progress_bar.update()
195 | else:
196 | # retrieve one error from queue, might not be exactly the one that failed
197 | # since other worker can write to the queue, but we will have at least one error to retrieve
198 | _, cfg_id_err, err = error_queue.get()
199 | logger.error("configuration {} failed".format(cfg_id_err))
200 | logger.error(err)
201 |
202 | if cancel:
203 | done = True
204 | else:
205 | num_completed += 1
206 | pending -= 1
207 |
208 | if (num_completed + pending) != n_tasks:
209 | next_cfg = next(param_grid)
210 | configs[next_cfg["id"]] = next_cfg
211 | config_queue.put(next_cfg)
212 | pending += 1
213 | else:
214 | # signal the current worker for termination no more work to be done
215 | terminate_flags[pid].set()
216 | progress_bar.update()
217 |
218 | except QueueEmpty:
219 | pass
220 | # try to wait for process termination
221 | for process in processes:
222 | process.join(timeout=0.5)
223 |
224 | if process.is_alive():
225 | process.terminate()
226 |
227 | if len(config_ids) > 0:
228 | all_ids = set(config_ids)
229 | else:
230 | all_ids = set(range(ps.size))
231 | failed_tasks = all_ids.difference(successful)
232 | if len(failed_tasks) > 0:
233 | ids = " ".join(map(str, failed_tasks))
234 | fail_runs = "failed runs: {}".format(ids)
235 | print(fail_runs, file=sys.stderr)
236 | logger.warn(fail_runs)
237 |
238 | progress_bar.close()
239 |
240 | except TomlDecodeError as e:
241 | logger.error(traceback.format_exc())
242 | print("\n\n[Invalid parameter file] TOML decode error:\n {}".format(e), file=sys.stderr)
243 | except ParamDecodeError as e:
244 | logger.error(traceback.format_exc())
245 | print("\n\n[Invalid parameter file]\n {}".format(e), file=sys.stderr)
246 |
247 |
248 | if __name__ == '__main__':
249 | main()
250 |
--------------------------------------------------------------------------------
/extras/convergence.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/davidenunes/exp/091dba3b7ef60b463c9be7ad2e9c1458f06a1e38/extras/convergence.png
--------------------------------------------------------------------------------
/extras/exp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/davidenunes/exp/091dba3b7ef60b463c9be7ad2e9c1458f06a1e38/extras/exp.png
--------------------------------------------------------------------------------
/extras/getting_started.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/davidenunes/exp/091dba3b7ef60b463c9be7ad2e9c1458f06a1e38/extras/getting_started.gif
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | import os
3 | from setuptools import find_packages, setup, Command
4 | import codecs
5 | import sys
6 | from shutil import rmtree
7 |
8 | here = os.path.abspath(os.path.dirname(__file__))
9 |
10 | about = {}
11 |
12 | with open(os.path.join(here, "exp", "__version__.py")) as f:
13 | exec(f.read(), about)
14 |
15 | with codecs.open(os.path.join(here, "README.md"), encoding="utf-8") as f:
16 | long_description = "\n" + f.read()
17 |
18 | if sys.argv[-1] == "publish":
19 | os.system("python setup.py sdist bdist_wheel upload")
20 | sys.exit()
21 |
22 | required = [
23 | 'toml',
24 | 'click',
25 | 'numpy',
26 | 'matplotlib',
27 | 'GPUtil',
28 | 'scikit-optimize'
29 | ]
30 |
31 |
32 | class UploadCommand(Command):
33 | """Support setup.py publish."""
34 |
35 | description = "Build and publish the package."
36 | user_options = []
37 |
38 | @staticmethod
39 | def status(s):
40 | """Prints things in bold."""
41 | print("\033[1m{0}\033[0m".format(s))
42 |
43 | def initialize_options(self):
44 | pass
45 |
46 | def finalize_options(self):
47 | pass
48 |
49 | def run(self):
50 | try:
51 | self.status("Removing previous builds…")
52 | rmtree(os.path.join(here, "dist"))
53 | except FileNotFoundError:
54 | pass
55 | self.status("Building Source distribution…")
56 | os.system("{0} setup.py sdist bdist_wheel".format(sys.executable))
57 | self.status("Uploading the package to PyPI via Twine…")
58 | os.system("twine upload dist/*")
59 | self.status("Pushing git tags…")
60 | os.system("git tag v{0}".format(about["__version__"]))
61 | os.system("git push --tags")
62 | sys.exit()
63 |
64 | setup(name='exp',
65 | version=about["__version__"],
66 | description='Python tool do design and run experiments with global optimisation and grid search',
67 | long_description=long_description,
68 | long_description_content_type="text/markdown",
69 | author='Davide Nunes',
70 | author_email='mail@davidenunes.com',
71 | url='https://github.com/davidenunes/exp',
72 | packages=find_packages(exclude=["tests", "tests.*"]),
73 | install_requires=required,
74 | python_requires=">=3.6",
75 | license="Apache 2.0",
76 | package_data={
77 | "": ["LICENSE"],
78 | },
79 | include_package_data=True,
80 | classifiers=[
81 | 'Programming Language :: Python',
82 | 'Programming Language :: Python :: 3.6',
83 | 'Programming Language :: Python :: 3.7',
84 | ],
85 | cmdclass={"upload": UploadCommand},
86 | )
87 |
--------------------------------------------------------------------------------
/test/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/davidenunes/exp/091dba3b7ef60b463c9be7ad2e9c1458f06a1e38/test/__init__.py
--------------------------------------------------------------------------------
/test/assets/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/davidenunes/exp/091dba3b7ef60b463c9be7ad2e9c1458f06a1e38/test/assets/__init__.py
--------------------------------------------------------------------------------
/test/assets/dummy.conf:
--------------------------------------------------------------------------------
1 | [x]
2 | type="range"
3 | bounds=[-10,10]
4 |
5 | [param2]
6 | type = "random"
7 | bounds = [2, 1, 3]
8 |
9 |
--------------------------------------------------------------------------------
/test/assets/dummy_runnable.py:
--------------------------------------------------------------------------------
1 | import time
2 | import random
3 |
4 |
5 | def run(x=1, **kwargs):
6 | time.sleep(random.uniform(0, 0.5))
7 | if x == 3:
8 | raise ValueError("oops!!")
9 | return x ** 2
10 |
--------------------------------------------------------------------------------
/test/assets/multiple_params.conf:
--------------------------------------------------------------------------------
1 | [x]
2 | type = "range"
3 | bounds = [0,3]
4 |
5 | [param1]
6 | type = "value"
7 | value = "param 1 value"
8 | [param2]
9 | type = "list"
10 | value = ["param 2 value 1", 1]
--------------------------------------------------------------------------------
/test/assets/runnable_tensorflow.py:
--------------------------------------------------------------------------------
1 | import sys
2 | import os
3 | import tensorflow as tf
4 | from exp.args import ParamDict,Namespace
5 |
6 | defaults = {
7 | 'x': (float, None),
8 | 'id': (int, 0),
9 | 'run': (int, 1)
10 | }
11 |
12 | os.environ['TF_CPP_MIN_LOG_LEVEL'] = '3'
13 |
14 |
15 | def run(**kargs):
16 | args = ParamDict(defaults)
17 | args.from_dict(kargs)
18 | ns = args.to_namespace()
19 | #args = Namespace(args)
20 | # print(dargs)
21 |
22 | gpu = os.environ["CUDA_VISIBLE_DEVICES"]
23 | # test exceptions
24 | # if random.random() < 0.3:
25 | # raise Exception("failled with params: \n{}".format(kargs))
26 |
27 | a = tf.random_uniform([100000, 3])
28 | b = tf.constant([1.0, 2.0, 3.0, 4.0, 5.0, 6.0], shape=[3, 2], name='b')
29 | c = tf.matmul(a, b)
30 | d = tf.multiply(c, ns.x)
31 | #d = tf.matmul(c, ns.x)
32 |
33 | # cfg = tf.ConfigProto(log_device_placement=True)
34 | # sess = tf.Session(config=cfg)
35 | sess = tf.Session()
36 | res = sess.run(d)
37 | sess.close()
38 |
39 | debug = "INSIDE GPU WORKER ---------------\n" \
40 | "params: {params}\n" \
41 | "using GPU: {env}\n " \
42 | "result: \n {res}" \
43 | "-----------------------------------".format(params=args, env=gpu, res=res)
44 |
45 | tf.reset_default_graph()
46 | return debug
47 |
48 |
49 | if __name__ == "__main__":
50 | # note I can use argparse in the scripts to run directly from main
51 | run()
52 |
--------------------------------------------------------------------------------
/test/test_params.py:
--------------------------------------------------------------------------------
1 | import unittest
2 | from exp.params import ParamSpace, Types, DTypes
3 |
4 | import csv
5 | import os
6 |
7 |
8 | class MyTestCase(unittest.TestCase):
9 | def test_param_grid(self):
10 | ps = ParamSpace()
11 | ps.add_value("p1", True)
12 | ps.add_list("p2", ["A", "B"])
13 | ps.add_random("p3", low=0, high=4, prior="uniform", n=3)
14 | # print("param space size ", ps.grid_size)
15 |
16 | grid = ps.param_grid()
17 |
18 | # for params in grid:
19 | # print(params)
20 |
21 | grid = ps.param_grid()
22 | grid = list(grid)
23 | self.assertEqual(len(grid), 1 * 2 * 3)
24 | self.assertEqual(len(grid), ps.size)
25 |
26 | def test_write_recover(self):
27 | """ There is one issue with writing the param assets which is the fact that these do not preserve the
28 | value types, this is expected, the only issue was that we need to ensure that we can use np.random.uniform
29 | so regardless of the add_random and add_range arg types, they will be converted to float parameters
30 | """
31 | ps = ParamSpace()
32 | ps.add_value("p1", True)
33 | ps.add_list("p2", ["A", "B"])
34 | ps.add_random("p3", low=0, high=4, prior="uniform", n=3)
35 |
36 | param_filename = "test.conf"
37 | ps.write(param_filename)
38 | self.assertTrue(os.path.exists(param_filename))
39 | ParamSpace(param_filename)
40 | os.remove(param_filename)
41 |
42 | def test_write_summary(self):
43 | summary_file = "params.csv"
44 |
45 | ps = ParamSpace()
46 | ps.add_value("p1", True)
47 | ps.add_list("p2", ["A", "B"])
48 | ps.add_random("p3", low=0, high=4, prior="uniform", n=3)
49 | # print("param space size ", ps.grid_size)
50 |
51 | ps.write_configs(summary_file)
52 |
53 | written_summary = open(summary_file)
54 | reader = csv.DictReader(written_summary)
55 |
56 | params = [dict(config) for config in reader]
57 | # print("read parameters")
58 | # for config in params:
59 | # print(config)
60 |
61 | written_summary.close()
62 | os.remove(summary_file)
63 |
64 | self.assertEqual(len(params), ps.size)
65 |
66 | def test_add_random(self):
67 | """ If persist is not set to True for add_random
68 | each time we call param_grid, it samples new random values
69 | this is because persist = True saves the parameter as a list
70 | or randomly generated parameters
71 | """
72 | ps = ParamSpace()
73 | name = "param1"
74 | ps.add_random(name, low=2, high=4, persist=False, n=10, prior="uniform")
75 |
76 | params1 = ps.param_grid()
77 | self.assertTrue(ps.size, 1)
78 | r1 = next(params1)[name]
79 |
80 | params2 = ps.param_grid()
81 | r2 = next(params2)[name]
82 |
83 | ps.write("test.cfg")
84 |
85 | self.assertNotEqual(r1, r2)
86 |
87 | def test_add_range(self):
88 | filename = "test.cfg"
89 | ps = ParamSpace()
90 | ps.add_range("range_param", 0, 10, 1, dtype=int)
91 |
92 | ps.write(filename)
93 |
94 | ps = ParamSpace(filename)
95 | # print(ps.params["range_param"])
96 | # print(ps.get_range("range_param"))
97 |
98 | os.remove(filename)
99 |
100 | def test_domain(self):
101 | ps = ParamSpace()
102 | ps.add_value("value", True)
103 | domain = ps.domain("value")
104 | self.assertIn("domain", domain)
105 | self.assertIn("dtype", domain)
106 | self.assertEqual(DTypes.CATEGORICAL.value, domain["dtype"])
107 |
108 | ps.add_list("bool", [True, False, True])
109 | domain = ps.domain("bool")
110 | self.assertIn("domain", domain)
111 | self.assertIn("dtype", domain)
112 | self.assertEqual(DTypes.CATEGORICAL.value, domain["dtype"])
113 | self.assertListEqual([True, False], domain["domain"])
114 |
115 | ps.add_range("bounds", 0, 10, dtype=float)
116 | domain = ps.domain("bounds")
117 | self.assertIn("domain", domain)
118 | self.assertIn("dtype", domain)
119 | self.assertIn("prior", domain)
120 | self.assertEqual("float", domain["dtype"])
121 | self.assertEqual("uniform", domain["prior"])
122 |
123 | ps.add_random("random", 0, 10, prior="log-uniform", dtype=float)
124 | domain = ps.domain("bounds")
125 | self.assertIn("domain", domain)
126 | self.assertIn("dtype", domain)
127 | self.assertIn("prior", domain)
128 | self.assertEqual("float", domain["dtype"])
129 | self.assertEqual("uniform", domain["prior"])
130 |
131 | def test_param_grid_with_id(self):
132 | ps = ParamSpace()
133 | ps.add_value("p1", True)
134 | ps.add_list("p2", ["A", "B"])
135 |
136 | params1 = ps.param_grid(runs=5)
137 |
138 | self.assertEqual(len(list(params1)), 1 * 2 * 5)
139 |
140 | def test_write_grid_files(self):
141 | ps = ParamSpace()
142 | ps.add_value("p1", True)
143 | ps.add_list("p2", ["A", "B"])
144 | ps.add_random("p3", n=2, prior="uniform", low=1, high=3)
145 | # print("param space size ", ps.grid_size)
146 |
147 | out_path = "/tmp/test_params/"
148 | if not os.path.exists(out_path) or not os.path.isdir(out_path):
149 | os.makedirs(out_path)
150 | ps.write_config_files(out_path)
151 |
152 | def test_sample_params(self):
153 | ps = ParamSpace()
154 | ps.add_value("p1", True)
155 | ps.add_list("p2", ["A", "B"])
156 | ps.add_random("p3", n=1, prior="uniform", low=1, high=3)
157 |
158 | x = ps.sample_space()
159 | self.assertIsInstance(x, dict)
160 |
161 |
162 | if __name__ == '__main__':
163 | unittest.main()
164 |
--------------------------------------------------------------------------------