LightningBO 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- lightningbo-0.1.0/LICENSE +208 -0
- lightningbo-0.1.0/LightningBO/__init__.py +2 -0
- lightningbo-0.1.0/LightningBO/bayesian_core.py +460 -0
- lightningbo-0.1.0/LightningBO/bayesian_optimization.py +336 -0
- lightningbo-0.1.0/LightningBO.egg-info/PKG-INFO +15 -0
- lightningbo-0.1.0/LightningBO.egg-info/SOURCES.txt +13 -0
- lightningbo-0.1.0/LightningBO.egg-info/dependency_links.txt +1 -0
- lightningbo-0.1.0/LightningBO.egg-info/requires.txt +7 -0
- lightningbo-0.1.0/LightningBO.egg-info/top_level.txt +1 -0
- lightningbo-0.1.0/PKG-INFO +15 -0
- lightningbo-0.1.0/README.md +48 -0
- lightningbo-0.1.0/pyproject.toml +16 -0
- lightningbo-0.1.0/setup.cfg +4 -0
- lightningbo-0.1.0/tests/test_bayesian_core.py +62 -0
- lightningbo-0.1.0/tests/test_bayesian_optimization.py +48 -0
|
@@ -0,0 +1,208 @@
|
|
|
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.
|
|
67
|
+
|
|
68
|
+
Subject to the terms and conditions of this License, each Contributor
|
|
69
|
+
hereby grants to You a perpetual, worldwide, non-exclusive, no-charge,
|
|
70
|
+
royalty-free, irrevocable copyright license to reproduce, prepare
|
|
71
|
+
Derivative Works of, publicly display, publicly perform, sublicense,
|
|
72
|
+
and distribute the Work and such Derivative Works in Source or Object
|
|
73
|
+
form.
|
|
74
|
+
|
|
75
|
+
3. Grant of Patent License.
|
|
76
|
+
|
|
77
|
+
Subject to the terms and conditions of this License, each Contributor
|
|
78
|
+
hereby grants to You a perpetual, worldwide, non-exclusive, no-charge,
|
|
79
|
+
royalty-free, irrevocable (except as stated in this section) patent
|
|
80
|
+
license to make, have made, use, offer to sell, sell, import, and
|
|
81
|
+
otherwise transfer the Work, where such license applies only to those
|
|
82
|
+
patent claims licensable by such Contributor that are necessarily
|
|
83
|
+
infringed by their Contribution(s) alone or by combination of their
|
|
84
|
+
Contribution(s) with the Work to which such Contribution(s) was
|
|
85
|
+
submitted. If You institute patent litigation against any entity
|
|
86
|
+
(including a cross-claim or counterclaim in a lawsuit) alleging that
|
|
87
|
+
the Work or a Contribution incorporated within the Work constitutes
|
|
88
|
+
direct or contributory patent infringement, then any patent licenses
|
|
89
|
+
granted to You under this License for that Work shall terminate as of
|
|
90
|
+
the date such litigation is filed.
|
|
91
|
+
|
|
92
|
+
4. Redistribution.
|
|
93
|
+
|
|
94
|
+
You may reproduce and distribute copies of the Work or Derivative
|
|
95
|
+
Works thereof in any medium, with or without modifications, and in
|
|
96
|
+
Source or Object form, provided that You meet the following conditions:
|
|
97
|
+
|
|
98
|
+
(a) You must give any other recipients of the Work or Derivative Works
|
|
99
|
+
a copy of this License; and
|
|
100
|
+
|
|
101
|
+
(b) You must cause any modified files to carry prominent notices stating
|
|
102
|
+
that You changed the files; and
|
|
103
|
+
|
|
104
|
+
(c) You must retain, in the Source form of any Derivative Works that You
|
|
105
|
+
distribute, all copyright, patent, trademark, and attribution
|
|
106
|
+
notices from the Source form of the Work, excluding those notices
|
|
107
|
+
that do not pertain to any part of the Derivative Works; and
|
|
108
|
+
|
|
109
|
+
(d) If the Work includes a "NOTICE" text file as part of its distribution,
|
|
110
|
+
then any Derivative Works that You distribute must include a readable
|
|
111
|
+
copy of the attribution notices contained within such NOTICE file,
|
|
112
|
+
excluding those notices that do not pertain to any part of the
|
|
113
|
+
Derivative Works, in at least one of the following places: within a
|
|
114
|
+
NOTICE text file distributed as part of the Derivative Works; within
|
|
115
|
+
the Source form or documentation, if provided along with the
|
|
116
|
+
Derivative Works; or, within a display generated by the Derivative
|
|
117
|
+
Works, if and wherever such third-party notices normally appear. The
|
|
118
|
+
contents of the NOTICE file are for informational purposes only and
|
|
119
|
+
do not modify the License. You may add Your own attribution notices
|
|
120
|
+
within Derivative Works that You distribute, alongside or as an
|
|
121
|
+
addendum to the NOTICE text from the Work, provided that such
|
|
122
|
+
additional attribution notices cannot be construed as modifying the
|
|
123
|
+
License.
|
|
124
|
+
|
|
125
|
+
You may add Your own copyright statement to Your modifications and may
|
|
126
|
+
provide additional or different license terms and conditions for use,
|
|
127
|
+
reproduction, or distribution of Your modifications, or for any such
|
|
128
|
+
Derivative Works as a whole, provided Your use, reproduction, and
|
|
129
|
+
distribution of the Work otherwise complies with the conditions
|
|
130
|
+
stated in this License.
|
|
131
|
+
|
|
132
|
+
5. Submission of Contributions.
|
|
133
|
+
|
|
134
|
+
Unless You explicitly state otherwise, any Contribution intentionally
|
|
135
|
+
submitted for inclusion in the Work by You to the Licensor shall be
|
|
136
|
+
under the terms and conditions of this License, without any additional
|
|
137
|
+
terms or conditions. Notwithstanding the above, nothing herein shall
|
|
138
|
+
supersede or modify the terms of any separate license agreement you
|
|
139
|
+
may have executed with Licensor regarding such Contributions.
|
|
140
|
+
|
|
141
|
+
6. Trademarks.
|
|
142
|
+
|
|
143
|
+
This License does not grant permission to use the trade names,
|
|
144
|
+
trademarks, service marks, or product names of the Licensor, except as
|
|
145
|
+
required for describing the origin of the Work and reproducing the
|
|
146
|
+
content of the NOTICE file.
|
|
147
|
+
|
|
148
|
+
7. Disclaimer of Warranty.
|
|
149
|
+
|
|
150
|
+
Unless required by applicable law or agreed to in writing, Licensor
|
|
151
|
+
provides the Work (and each Contributor provides its Contributions) on
|
|
152
|
+
an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
|
|
153
|
+
express or implied, including, without limitation, any warranties or
|
|
154
|
+
conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR
|
|
155
|
+
A PARTICULAR PURPOSE. You are solely responsible for determining the
|
|
156
|
+
appropriateness of using or redistributing the Work and assume any
|
|
157
|
+
risks associated with Your exercise of permissions under this License.
|
|
158
|
+
|
|
159
|
+
8. Limitation of Liability.
|
|
160
|
+
|
|
161
|
+
In no event and under no legal theory, whether in tort (including
|
|
162
|
+
negligence), contract, or otherwise, unless required by applicable law
|
|
163
|
+
(such as deliberate and grossly negligent acts) or agreed to in writing,
|
|
164
|
+
shall any Contributor be liable to You for damages, including any direct,
|
|
165
|
+
indirect, special, incidental, or consequential damages of any character
|
|
166
|
+
arising as a result of this License or out of the use or inability to
|
|
167
|
+
use the Work (including but not limited to damages for loss of goodwill,
|
|
168
|
+
work stoppage, computer failure or malfunction, or any and all other
|
|
169
|
+
commercial damages or losses), even if such Contributor has been advised
|
|
170
|
+
of the possibility of such damages.
|
|
171
|
+
|
|
172
|
+
9. Accepting Warranty or Additional Liability.
|
|
173
|
+
|
|
174
|
+
While redistributing the Work or Derivative Works thereof, You may choose
|
|
175
|
+
to offer, and charge a fee for, acceptance of support, warranty, indemnity,
|
|
176
|
+
or other liability obligations and/or rights consistent with this License.
|
|
177
|
+
However, in accepting such obligations, You may act only on Your own behalf
|
|
178
|
+
and on Your sole responsibility, not on behalf of any other Contributor,
|
|
179
|
+
and only if You agree to indemnify, defend, and hold each Contributor
|
|
180
|
+
harmless for any liability incurred by, or claims asserted against, such
|
|
181
|
+
Contributor by reason of your accepting any such warranty or additional
|
|
182
|
+
liability.
|
|
183
|
+
|
|
184
|
+
END OF TERMS AND CONDITIONS
|
|
185
|
+
|
|
186
|
+
APPENDIX: How to apply the Apache License to your work.
|
|
187
|
+
|
|
188
|
+
To apply the Apache License to your work, attach the following boilerplate
|
|
189
|
+
notice, with the fields enclosed by brackets "[]" replaced with your own
|
|
190
|
+
identifying information. (Don't include the brackets!) The text should be
|
|
191
|
+
enclosed in the appropriate comment syntax for the file format. We also
|
|
192
|
+
recommend that a file or class name and description of purpose be included
|
|
193
|
+
on the same "printed page" as the copyright notice for easier
|
|
194
|
+
identification within third-party archives.
|
|
195
|
+
|
|
196
|
+
Copyright [2025] [s-ohtake]
|
|
197
|
+
|
|
198
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
199
|
+
you may not use this file except in compliance with the License.
|
|
200
|
+
You may obtain a copy of the License at
|
|
201
|
+
|
|
202
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
203
|
+
|
|
204
|
+
Unless required by applicable law or agreed to in writing, software
|
|
205
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
206
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
207
|
+
See the License for the specific language governing permissions and
|
|
208
|
+
limitations under the License.
|
|
@@ -0,0 +1,460 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Bayesian Optimization Core Library
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
This module provides core components for Bayesian optimization.
|
|
6
|
+
|
|
7
|
+
- Parameter space definitions (Continuous, Integer, ParamSpace)
|
|
8
|
+
- Gaussian Process utilities (kernels, prediction, hyperparameter fitting)
|
|
9
|
+
- Acquisition functions (EI, PI, UCB, LCB)
|
|
10
|
+
|
|
11
|
+
Design philosophy:
|
|
12
|
+
- Flexible abstraction of parameter space for use from Optimizer
|
|
13
|
+
- Fast numerical computation and automatic differentiation via JAX
|
|
14
|
+
- Reusable for both research and production
|
|
15
|
+
|
|
16
|
+
Main use cases:
|
|
17
|
+
- Black-box function optimization
|
|
18
|
+
- Hyperparameter search
|
|
19
|
+
- Experiment planning and automation
|
|
20
|
+
|
|
21
|
+
All components are designed to be reusable and extensible for other optimization algorithms.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
from collections import namedtuple
|
|
25
|
+
from functools import partial
|
|
26
|
+
from typing import Any
|
|
27
|
+
|
|
28
|
+
import jax
|
|
29
|
+
import jax.numpy as jnp
|
|
30
|
+
import optax
|
|
31
|
+
from jax.scipy.linalg import cholesky, solve_triangular
|
|
32
|
+
from jax.scipy.stats import norm
|
|
33
|
+
|
|
34
|
+
jax.config.update("jax_enable_x64", True)
|
|
35
|
+
from abc import ABC, abstractmethod
|
|
36
|
+
|
|
37
|
+
class ParameterDomain(ABC):
|
|
38
|
+
"""パラメータ空間の抽象基底クラス"""
|
|
39
|
+
|
|
40
|
+
def __init__(self, dtype):
|
|
41
|
+
self.dtype = dtype
|
|
42
|
+
|
|
43
|
+
def __hash__(self):
|
|
44
|
+
return hash((type(self), self.dtype))
|
|
45
|
+
|
|
46
|
+
def __eq__(self, other):
|
|
47
|
+
return type(self) == type(other) and self.dtype == other.dtype
|
|
48
|
+
|
|
49
|
+
@abstractmethod
|
|
50
|
+
def transform(self, x):
|
|
51
|
+
pass
|
|
52
|
+
|
|
53
|
+
@abstractmethod
|
|
54
|
+
def sample(self, key, shape):
|
|
55
|
+
pass
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class Continuous(ParameterDomain):
|
|
60
|
+
"""連続値パラメータ空間"""
|
|
61
|
+
|
|
62
|
+
def __init__(self, lower: float, upper: float):
|
|
63
|
+
if not isinstance(lower, (float, int)):
|
|
64
|
+
raise TypeError(f"lower must be float or int, got {type(lower)}")
|
|
65
|
+
if not isinstance(upper, (float, int)):
|
|
66
|
+
raise TypeError(f"upper must be float or int, got {type(upper)}")
|
|
67
|
+
if lower > upper:
|
|
68
|
+
raise ValueError("lower must be <= upper")
|
|
69
|
+
self.lower = float(lower)
|
|
70
|
+
self.upper = float(upper)
|
|
71
|
+
super().__init__(dtype=jnp.float32)
|
|
72
|
+
|
|
73
|
+
def __hash__(self):
|
|
74
|
+
return hash((type(self), self.lower, self.upper))
|
|
75
|
+
|
|
76
|
+
def __eq__(self, other):
|
|
77
|
+
return (
|
|
78
|
+
isinstance(other, Continuous)
|
|
79
|
+
and self.lower == other.lower
|
|
80
|
+
and self.upper == other.upper
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
def transform(self, x):
|
|
84
|
+
return jnp.clip(x, self.lower, self.upper)
|
|
85
|
+
|
|
86
|
+
def sample(self, key, shape):
|
|
87
|
+
samples = jax.random.uniform(key, shape, minval=self.lower, maxval=self.upper)
|
|
88
|
+
return self.transform(samples)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
class Integer(ParameterDomain):
|
|
92
|
+
"""
|
|
93
|
+
Discrete integer-valued domain with rounding and clipping.
|
|
94
|
+
|
|
95
|
+
Represents a parameter that can take integer values within [lower, upper].
|
|
96
|
+
"""
|
|
97
|
+
|
|
98
|
+
def __init__(self, lower, upper):
|
|
99
|
+
"""
|
|
100
|
+
Initialize an integer domain with bounds.
|
|
101
|
+
|
|
102
|
+
Args:
|
|
103
|
+
lower: Lower integer bound (inclusive).
|
|
104
|
+
upper: Upper integer bound (inclusive).
|
|
105
|
+
"""
|
|
106
|
+
if not isinstance(lower, int):
|
|
107
|
+
raise TypeError("Lower bound must be an integer")
|
|
108
|
+
if not isinstance(upper, int):
|
|
109
|
+
raise TypeError("Upper bound must be an integer")
|
|
110
|
+
if lower >= upper:
|
|
111
|
+
raise ValueError("Lower bound must be less than upper bound")
|
|
112
|
+
|
|
113
|
+
self.lower = int(lower)
|
|
114
|
+
self.upper = int(upper)
|
|
115
|
+
super().__init__(dtype=jnp.int32)
|
|
116
|
+
|
|
117
|
+
def __hash__(self):
|
|
118
|
+
"""Return hash based on bounds."""
|
|
119
|
+
return hash((self.lower, self.upper))
|
|
120
|
+
|
|
121
|
+
def __eq__(self, other):
|
|
122
|
+
"""Check equality based on bounds."""
|
|
123
|
+
return self.lower == other.lower and self.upper == other.upper
|
|
124
|
+
|
|
125
|
+
def transform(self, x: jax.Array):
|
|
126
|
+
"""
|
|
127
|
+
Round and clip values to the integer domain.
|
|
128
|
+
|
|
129
|
+
Args:
|
|
130
|
+
x: Input values.
|
|
131
|
+
|
|
132
|
+
Returns
|
|
133
|
+
-------
|
|
134
|
+
Rounded and clipped values as float32.
|
|
135
|
+
"""
|
|
136
|
+
return jnp.clip(jnp.round(x), self.lower, self.upper).astype(jnp.float32)
|
|
137
|
+
|
|
138
|
+
def sample(self, key: jax.Array, shape: tuple):
|
|
139
|
+
"""
|
|
140
|
+
Sample integers uniformly from the domain.
|
|
141
|
+
|
|
142
|
+
Args:
|
|
143
|
+
key: JAX PRNGKey.
|
|
144
|
+
shape: Desired output shape.
|
|
145
|
+
|
|
146
|
+
Returns
|
|
147
|
+
-------
|
|
148
|
+
Sampled values clipped to valid integer range.
|
|
149
|
+
"""
|
|
150
|
+
samples = jax.random.randint(
|
|
151
|
+
key, shape, minval=self.lower, maxval=self.upper + 1
|
|
152
|
+
)
|
|
153
|
+
return self.transform(samples)
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
class ParamSpace:
|
|
158
|
+
"""
|
|
159
|
+
Parameter space manager for collections of named parameter domains.
|
|
160
|
+
|
|
161
|
+
This utility encapsulates logic for sampling, transforming, and handling
|
|
162
|
+
structured parameter inputs defined by a mapping of variable names to ParameterDomain
|
|
163
|
+
instances (e.g., Continuous, Integer).
|
|
164
|
+
"""
|
|
165
|
+
|
|
166
|
+
def __init__(self, space: dict):
|
|
167
|
+
self.space = space
|
|
168
|
+
|
|
169
|
+
def sample_params(self, key: jax.Array, shape: tuple) -> dict:
|
|
170
|
+
"""Sample parameters from all domains."""
|
|
171
|
+
keys = jax.random.split(key, len(self.space))
|
|
172
|
+
return {
|
|
173
|
+
name: self.space[name].sample(k, shape)
|
|
174
|
+
for name, k in zip(self.space, keys)
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
def to_array(self, tree: dict) -> jax.Array:
|
|
178
|
+
"""
|
|
179
|
+
Transform a batch of parameter values into a 2D array suitable for GP input.
|
|
180
|
+
|
|
181
|
+
Args:
|
|
182
|
+
tree: A dictionary of parameter name → array of raw values.
|
|
183
|
+
|
|
184
|
+
Returns
|
|
185
|
+
-------
|
|
186
|
+
A JAX array of shape (batch_size, num_params) with transformed values.
|
|
187
|
+
"""
|
|
188
|
+
return jnp.stack([self.space[k].transform(tree[k]) for k in self.space], axis=1)
|
|
189
|
+
|
|
190
|
+
def to_dict(self, xs: jax.Array) -> dict:
|
|
191
|
+
"""
|
|
192
|
+
Convert a stacked parameter matrix back into named parameter trees.
|
|
193
|
+
|
|
194
|
+
Args:
|
|
195
|
+
xs: A 2D JAX array of shape (batch_size, num_params), with each column
|
|
196
|
+
corresponding to a parameter.
|
|
197
|
+
|
|
198
|
+
Returns
|
|
199
|
+
-------
|
|
200
|
+
A dictionary mapping parameter names to individual 1D arrays.
|
|
201
|
+
"""
|
|
202
|
+
return {k: self.space[k].transform(xs[:, i]) for i, k in enumerate(self.space)}
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
# =============================================================================
|
|
206
|
+
# Gaussian Process Components
|
|
207
|
+
# =============================================================================
|
|
208
|
+
|
|
209
|
+
MASK_VARIANCE = 1e12 # High variance for masked points to not affect the process.
|
|
210
|
+
|
|
211
|
+
GPParams = namedtuple("GPParams", ["noise", "amplitude", "lengthscale"])
|
|
212
|
+
GPState = namedtuple("GPState", ["params", "momentums", "scales"])
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def softplus(x):
|
|
216
|
+
"""Softplus activation function."""
|
|
217
|
+
return jnp.logaddexp(x, 0.0)
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def exp_quadratic(x1, x2, mask):
|
|
221
|
+
"""Exponential quadratic kernel function."""
|
|
222
|
+
distance = jnp.sum((x1 - x2) ** 2)
|
|
223
|
+
return jnp.exp(-distance) * mask
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def cov(x1, x2, mask1, mask2):
|
|
227
|
+
"""Covariance function with masking."""
|
|
228
|
+
M = jnp.outer(mask1, mask2)
|
|
229
|
+
k = exp_quadratic
|
|
230
|
+
return jax.vmap(jax.vmap(k, in_axes=(None, 0, 0)), in_axes=(0, None, 0))(x1, x2, M)
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def gaussian_process(
|
|
234
|
+
params,
|
|
235
|
+
x: jnp.ndarray,
|
|
236
|
+
y: jnp.ndarray,
|
|
237
|
+
mask,
|
|
238
|
+
xt: Any = None,
|
|
239
|
+
compute_ml: bool = False,
|
|
240
|
+
) -> Any:
|
|
241
|
+
"""Core Gaussian Process implementation with masking support."""
|
|
242
|
+
# Number of points in the prior distribution
|
|
243
|
+
n = x.shape[0]
|
|
244
|
+
|
|
245
|
+
noise, amp, ls = jax.tree_util.tree_map(softplus, params)
|
|
246
|
+
|
|
247
|
+
ymean = jnp.mean(y, where=mask.astype(bool))
|
|
248
|
+
y = (y - ymean) * mask
|
|
249
|
+
x = x / ls
|
|
250
|
+
K = amp * cov(x, x, mask, mask) + (jnp.eye(n) * (noise + 1e-6))
|
|
251
|
+
K += jnp.eye(n) * (1.0 - mask.astype(float)) * MASK_VARIANCE
|
|
252
|
+
L = cholesky(K, lower=True)
|
|
253
|
+
K_inv_y = solve_triangular(L.T, solve_triangular(L, y, lower=True), lower=False)
|
|
254
|
+
|
|
255
|
+
if compute_ml:
|
|
256
|
+
logp = 0.5 * jnp.dot(y.T, K_inv_y)
|
|
257
|
+
logp += jnp.sum(jnp.log(jnp.diag(L)))
|
|
258
|
+
logp -= jnp.sum(1.0 - mask) * 0.5 * jnp.log(MASK_VARIANCE)
|
|
259
|
+
logp += (jnp.sum(mask) / 2) * jnp.log(2 * jnp.pi)
|
|
260
|
+
logp += jnp.sum(-0.5 * jnp.log(2 * jnp.pi) - jnp.log(amp) - jnp.log(amp) ** 2)
|
|
261
|
+
return jnp.sum(logp)
|
|
262
|
+
|
|
263
|
+
if xt is None:
|
|
264
|
+
raise ValueError("xt cannot be None during prediction")
|
|
265
|
+
xt = xt / ls
|
|
266
|
+
|
|
267
|
+
# Compute the covariance with the new point xt
|
|
268
|
+
mask_t = jnp.ones(len(xt)) == 1
|
|
269
|
+
K_cross = amp * cov(x, xt, mask, mask_t)
|
|
270
|
+
|
|
271
|
+
K_inv_y = K_inv_y * mask # masking
|
|
272
|
+
pred_mean = jnp.dot(K_cross.T, K_inv_y) + ymean
|
|
273
|
+
v = solve_triangular(L, K_cross, lower=True)
|
|
274
|
+
pred_var = amp * cov(xt, xt, mask_t, mask_t) - v.T @ v
|
|
275
|
+
pred_std = jnp.sqrt(jnp.maximum(jnp.diag(pred_var), 1e-10))
|
|
276
|
+
return pred_mean, pred_std
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
marginal_likelihood = partial(gaussian_process, compute_ml=True)
|
|
280
|
+
grad_fun = jax.jit(jax.grad(marginal_likelihood))
|
|
281
|
+
predict = jax.jit(partial(gaussian_process, compute_ml=False))
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
def neg_log_likelihood(params, x, y, mask):
|
|
285
|
+
"""Negative log likelihood for GP hyperparameter optimization."""
|
|
286
|
+
ll = marginal_likelihood(params, x, y, mask)
|
|
287
|
+
|
|
288
|
+
# Weak priors to keep things sane
|
|
289
|
+
priors = GPParams(-8.0, 1.0, 1.0)
|
|
290
|
+
log_prior = jax.tree.map(lambda p, m: jnp.sum((p - m) ** 2), params, priors)
|
|
291
|
+
log_prior = sum(jax.tree.leaves(log_prior))
|
|
292
|
+
log_posterior = ll - 0.5 * log_prior
|
|
293
|
+
return -log_posterior
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
def posterior_fit(
|
|
297
|
+
y: jax.Array,
|
|
298
|
+
x: jax.Array,
|
|
299
|
+
mask: jax.Array,
|
|
300
|
+
params: GPParams,
|
|
301
|
+
lr: float = 1e-3,
|
|
302
|
+
trainsteps: int = 100,
|
|
303
|
+
) -> GPParams:
|
|
304
|
+
"""Fit GP hyperparameters using gradient descent."""
|
|
305
|
+
optimizer = optax.chain(optax.clip_by_global_norm(10.0), optax.adamw(lr))
|
|
306
|
+
opt_state = optimizer.init(params)
|
|
307
|
+
|
|
308
|
+
def train_step(carry, _):
|
|
309
|
+
params, opt_state = carry
|
|
310
|
+
grads = jax.grad(neg_log_likelihood)(params, x, y, mask)
|
|
311
|
+
updates, opt_state = optimizer.update(grads, opt_state, params=params)
|
|
312
|
+
params = optax.apply_updates(params, updates)
|
|
313
|
+
return (params, opt_state), None
|
|
314
|
+
|
|
315
|
+
(params, _), __ = jax.lax.scan(
|
|
316
|
+
train_step, (params, opt_state), None, length=trainsteps
|
|
317
|
+
)
|
|
318
|
+
return params
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
# =============================================================================
|
|
322
|
+
# Acquisition Functions
|
|
323
|
+
# =============================================================================
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
def expected_improvement(
|
|
327
|
+
x_pred: jnp.ndarray,
|
|
328
|
+
xs: jax.Array,
|
|
329
|
+
ys: jax.Array,
|
|
330
|
+
mask: jax.Array,
|
|
331
|
+
gp_params: GPParams,
|
|
332
|
+
xi: float = 0.01,
|
|
333
|
+
):
|
|
334
|
+
r"""
|
|
335
|
+
Compute Expected Improvement (EI) acquisition function.
|
|
336
|
+
|
|
337
|
+
Favors points with high improvement over the current best observed value,
|
|
338
|
+
balancing exploitation and exploration.
|
|
339
|
+
|
|
340
|
+
The formula is:
|
|
341
|
+
|
|
342
|
+
.. math::
|
|
343
|
+
|
|
344
|
+
EI(x) = (\mu(x) - y^* - \xi) \Phi(z) + \sigma(x) \phi(z)
|
|
345
|
+
|
|
346
|
+
where:
|
|
347
|
+
|
|
348
|
+
.. math::
|
|
349
|
+
|
|
350
|
+
z = \frac{\mu(x) - y^* - \xi}{\sigma(x)}
|
|
351
|
+
|
|
352
|
+
Args:
|
|
353
|
+
x_pred: Candidate input locations to evaluate.
|
|
354
|
+
xs: Observed inputs.
|
|
355
|
+
ys: Observed function values.
|
|
356
|
+
mask: Boolean mask indicating valid entries in `ys`.
|
|
357
|
+
gp_params: Gaussian Process hyperparameters.
|
|
358
|
+
xi: Exploration-exploitation tradeoff parameter.
|
|
359
|
+
|
|
360
|
+
Returns
|
|
361
|
+
-------
|
|
362
|
+
EI scores at `x_pred`.
|
|
363
|
+
"""
|
|
364
|
+
ymax = jnp.max(ys, where=mask.astype(bool), initial=-jnp.inf)
|
|
365
|
+
mu, std = predict(gp_params, xs, ys, mask, xt=x_pred)
|
|
366
|
+
a = mu - ymax - xi
|
|
367
|
+
z = a / (std + 1e-3)
|
|
368
|
+
ei = a * norm.cdf(z) + std * norm.pdf(z)
|
|
369
|
+
return ei
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
def probability_improvement(
|
|
373
|
+
x_pred: jnp.ndarray,
|
|
374
|
+
xs: jax.Array,
|
|
375
|
+
ys: jax.Array,
|
|
376
|
+
mask: jax.Array,
|
|
377
|
+
gp_params: GPParams,
|
|
378
|
+
xi: float = 0.01,
|
|
379
|
+
):
|
|
380
|
+
"""
|
|
381
|
+
Probability of Improvement (PI) acquisition function.
|
|
382
|
+
|
|
383
|
+
Estimates the probability that a candidate point will improve
|
|
384
|
+
over the current best observed value.
|
|
385
|
+
|
|
386
|
+
Args:
|
|
387
|
+
x_pred: Candidate input locations to evaluate.
|
|
388
|
+
xs: Observed inputs.
|
|
389
|
+
ys: Observed function values.
|
|
390
|
+
mask: Boolean mask indicating valid entries in `ys`.
|
|
391
|
+
gp_params: Gaussian Process hyperparameters.
|
|
392
|
+
xi: Improvement margin for sensitivity.
|
|
393
|
+
|
|
394
|
+
Returns
|
|
395
|
+
-------
|
|
396
|
+
PI scores at `x_pred`.
|
|
397
|
+
"""
|
|
398
|
+
y_max = ys.max()
|
|
399
|
+
mu, std = predict(gp_params, xs, ys, mask, xt=x_pred)
|
|
400
|
+
z = (mu - y_max - xi) / std
|
|
401
|
+
return norm.cdf(z)
|
|
402
|
+
|
|
403
|
+
|
|
404
|
+
def upper_confidence_bounds(
|
|
405
|
+
x_pred: jnp.ndarray,
|
|
406
|
+
xs: jax.Array,
|
|
407
|
+
ys: jax.Array,
|
|
408
|
+
mask: jax.Array,
|
|
409
|
+
gp_params: GPParams,
|
|
410
|
+
kappa: float = 0.01,
|
|
411
|
+
):
|
|
412
|
+
"""
|
|
413
|
+
Upper Confidence Bound (UCB) acquisition function.
|
|
414
|
+
|
|
415
|
+
Promotes exploration by favoring points with high predictive uncertainty.
|
|
416
|
+
|
|
417
|
+
Args:
|
|
418
|
+
x_pred: Candidate input locations to evaluate.
|
|
419
|
+
xs: Observed inputs.
|
|
420
|
+
ys: Observed function values.
|
|
421
|
+
mask: Boolean mask indicating valid entries in `ys`.
|
|
422
|
+
gp_params: Gaussian Process hyperparameters.
|
|
423
|
+
kappa: Weighting factor for uncertainty.
|
|
424
|
+
|
|
425
|
+
Returns
|
|
426
|
+
-------
|
|
427
|
+
UCB scores at `x_pred`.
|
|
428
|
+
"""
|
|
429
|
+
mu, std = predict(gp_params, xs, ys, mask, xt=x_pred)
|
|
430
|
+
return mu + kappa * std
|
|
431
|
+
|
|
432
|
+
|
|
433
|
+
def lower_confidence_bounds(
|
|
434
|
+
x_pred: jnp.ndarray,
|
|
435
|
+
xs: jax.Array,
|
|
436
|
+
ys: jax.Array,
|
|
437
|
+
mask: jax.Array,
|
|
438
|
+
gp_params: GPParams,
|
|
439
|
+
kappa: float = 2.576,
|
|
440
|
+
):
|
|
441
|
+
"""
|
|
442
|
+
Lower Confidence Bound (LCB) acquisition function.
|
|
443
|
+
|
|
444
|
+
Useful for minimization tasks. Encourages sampling in uncertain regions
|
|
445
|
+
with low predicted values.
|
|
446
|
+
|
|
447
|
+
Args:
|
|
448
|
+
x_pred: Candidate input locations to evaluate.
|
|
449
|
+
xs: Observed inputs.
|
|
450
|
+
ys: Observed function values.
|
|
451
|
+
mask: Boolean mask indicating valid entries in `ys`.
|
|
452
|
+
gp_params: Gaussian Process hyperparameters.
|
|
453
|
+
kappa: Weighting factor for uncertainty.
|
|
454
|
+
|
|
455
|
+
Returns
|
|
456
|
+
-------
|
|
457
|
+
LCB scores at `x_pred`.
|
|
458
|
+
"""
|
|
459
|
+
mu, std = predict(gp_params, xs, ys, mask, xt=x_pred)
|
|
460
|
+
return mu - kappa * std
|
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
|
|
2
|
+
"""
|
|
3
|
+
Bayesian Optimization High-Level Interface
|
|
4
|
+
|
|
5
|
+
This module provides a high-level API for Bayesian optimization using Gaussian Processes.
|
|
6
|
+
It manages the optimization loop, parameter space, and acquisition functions.
|
|
7
|
+
|
|
8
|
+
Main features:
|
|
9
|
+
- Optimizer class for black-box function optimization
|
|
10
|
+
- Flexible parameter space definition (Continuous, Integer, etc.)
|
|
11
|
+
- Acquisition functions: EI, PI, UCB, LCB
|
|
12
|
+
- Automatic state management and buffer expansion
|
|
13
|
+
- JAX-based fast computation and differentiation
|
|
14
|
+
|
|
15
|
+
Typical use cases:
|
|
16
|
+
- Hyperparameter tuning
|
|
17
|
+
- Experimental design
|
|
18
|
+
- Automated search for expensive functions
|
|
19
|
+
|
|
20
|
+
Designed for extensibility and integration with custom objective functions.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
from collections.abc import Callable
|
|
25
|
+
from functools import partial
|
|
26
|
+
from typing import NamedTuple
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
import jax
|
|
30
|
+
from typing import Any
|
|
31
|
+
import jax.numpy as jnp
|
|
32
|
+
import numpy as np
|
|
33
|
+
import optax
|
|
34
|
+
|
|
35
|
+
from .bayesian_core import (
|
|
36
|
+
GPParams,
|
|
37
|
+
ParamSpace,
|
|
38
|
+
expected_improvement,
|
|
39
|
+
lower_confidence_bounds,
|
|
40
|
+
posterior_fit,
|
|
41
|
+
probability_improvement,
|
|
42
|
+
upper_confidence_bounds,
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
UtilityFunctionType = Callable[[jnp.ndarray], float]
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class OptimizerState(NamedTuple):
|
|
51
|
+
"""
|
|
52
|
+
State container for Bayesian optimizer.
|
|
53
|
+
|
|
54
|
+
Attributes:
|
|
55
|
+
params: dict[str, jnp.ndarray] - Padded parameter arrays.
|
|
56
|
+
ys: jnp.ndarray | np.ndarray - Objective values (with padding).
|
|
57
|
+
best_score: float - Best observed objective value.
|
|
58
|
+
best_params: dict[str, float] - Parameter configuration for best_score.
|
|
59
|
+
mask: jnp.ndarray - Boolean mask for valid entries.
|
|
60
|
+
gp_params: GPParams - Fitted Gaussian Process parameters.
|
|
61
|
+
"""
|
|
62
|
+
params: dict[str, jnp.ndarray]
|
|
63
|
+
ys: jnp.ndarray | np.ndarray
|
|
64
|
+
best_score: float
|
|
65
|
+
best_params: dict[str, float]
|
|
66
|
+
mask: jnp.ndarray
|
|
67
|
+
gp_params: GPParams
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _optimize_suggestion(params: jnp.ndarray, fun: Callable[[jnp.ndarray], float], max_iter: int = 10) -> jnp.ndarray:
|
|
72
|
+
"""
|
|
73
|
+
Local optimization using L-BFGS (Optax).
|
|
74
|
+
|
|
75
|
+
Args:
|
|
76
|
+
params: Initial point in input space.
|
|
77
|
+
fun: Objective function to maximize (returns scalar, supports autodiff).
|
|
78
|
+
max_iter: Number of L-BFGS steps.
|
|
79
|
+
|
|
80
|
+
Returns:
|
|
81
|
+
Optimized point after `max_iter` iterations.
|
|
82
|
+
"""
|
|
83
|
+
opt = optax.lbfgs()
|
|
84
|
+
value_and_grad_fun = optax.value_and_grad_from_state(lambda x: -fun(x))
|
|
85
|
+
|
|
86
|
+
def step(carry, _):
|
|
87
|
+
params, state = carry
|
|
88
|
+
value, grad = value_and_grad_fun(params, state=state)
|
|
89
|
+
updates, state = opt.update(
|
|
90
|
+
grad, state, params=params, value=value, grad=grad, value_fn=fun
|
|
91
|
+
)
|
|
92
|
+
params = optax.apply_updates(params, updates)
|
|
93
|
+
params = jnp.clip(params, -1e6, 1e6)
|
|
94
|
+
return (params, state), None
|
|
95
|
+
|
|
96
|
+
init_carry = (params, opt.init(params))
|
|
97
|
+
(final_params, _), __ = jax.lax.scan(step, init_carry, None, length=max_iter)
|
|
98
|
+
return final_params
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
class Optimizer:
|
|
103
|
+
"""
|
|
104
|
+
Bayesian optimizer using Gaussian Processes and acquisition functions.
|
|
105
|
+
|
|
106
|
+
Manages the optimization loop for expensive black-box functions by modeling
|
|
107
|
+
them with a Gaussian Process and selecting samples via acquisition functions
|
|
108
|
+
(EI, PI, UCB, LCB).
|
|
109
|
+
"""
|
|
110
|
+
|
|
111
|
+
def __init__(self, domain: dict[str, Any], acq: str = "EI", maximize: bool = False):
|
|
112
|
+
"""
|
|
113
|
+
Initialize the optimizer.
|
|
114
|
+
|
|
115
|
+
Args:
|
|
116
|
+
domain: A dict mapping parameter names to domain objects (e.g., Real, Integer).
|
|
117
|
+
acq: Acquisition function ('EI', 'PI', 'UCB', 'LCB').
|
|
118
|
+
maximize: Whether to maximize or minimize the objective.
|
|
119
|
+
"""
|
|
120
|
+
self.domain = domain
|
|
121
|
+
self.sign = 1 if maximize else -1
|
|
122
|
+
self.param_space = ParamSpace(domain)
|
|
123
|
+
|
|
124
|
+
acq_map = {
|
|
125
|
+
"EI": expected_improvement,
|
|
126
|
+
"PI": probability_improvement,
|
|
127
|
+
"UCB": upper_confidence_bounds,
|
|
128
|
+
"LCB": lower_confidence_bounds,
|
|
129
|
+
}
|
|
130
|
+
if acq not in acq_map:
|
|
131
|
+
raise ValueError(f"Acquisition function {acq} is not implemented")
|
|
132
|
+
self.acq = jax.jit(acq_map[acq])
|
|
133
|
+
|
|
134
|
+
def init(self, ys: jax.Array, params: dict, noise_scale: float = -8.0):
|
|
135
|
+
"""
|
|
136
|
+
Initialize the optimizer state from initial data.
|
|
137
|
+
|
|
138
|
+
Args:
|
|
139
|
+
ys: Objective values for the initial parameters.
|
|
140
|
+
params: Dict of parameter arrays (same keys as domain).
|
|
141
|
+
noise_scale: Initial noise scale for GP.
|
|
142
|
+
|
|
143
|
+
Returns
|
|
144
|
+
-------
|
|
145
|
+
Initialized OptimizerState.
|
|
146
|
+
"""
|
|
147
|
+
# Create a padded jax array for each parameter and each score.
|
|
148
|
+
# In order to keep jax compilations at a bay.
|
|
149
|
+
num_entries = len(ys)
|
|
150
|
+
pad_value = int(np.ceil(len(ys) / 10) * 10)
|
|
151
|
+
|
|
152
|
+
# Convert to jax arrays if they are not already
|
|
153
|
+
ys = jnp.asarray(ys)
|
|
154
|
+
ys = self.sign * ys
|
|
155
|
+
params = jax.tree.map(lambda x: jnp.asarray(x), params)
|
|
156
|
+
|
|
157
|
+
# Define padded arrays for the inputs and the outputs
|
|
158
|
+
mask = jnp.zeros(shape=(pad_value,), dtype=jnp.bool_).at[:num_entries].set(True)
|
|
159
|
+
ys = jnp.zeros(shape=(pad_value,), dtype=ys.dtype).at[:num_entries].set(ys)
|
|
160
|
+
|
|
161
|
+
_params = {}
|
|
162
|
+
for key, entries in params.items():
|
|
163
|
+
# Assert that the parameter is in the domain dictionary
|
|
164
|
+
if key not in self.domain:
|
|
165
|
+
raise ValueError(f"Parameter {key} is not in the domain")
|
|
166
|
+
|
|
167
|
+
# Get dtype from the domain and create a padded array
|
|
168
|
+
dtype = self.domain[key].dtype
|
|
169
|
+
values = (
|
|
170
|
+
jnp.zeros(shape=(pad_value,), dtype=dtype).at[:num_entries].set(entries)
|
|
171
|
+
)
|
|
172
|
+
_params[key] = values
|
|
173
|
+
|
|
174
|
+
# From the given observation, find the better one (either maxima or minima) and return the
|
|
175
|
+
# initial optimizer state.
|
|
176
|
+
best_score = float(jnp.max(ys[mask]))
|
|
177
|
+
best_params_idx = jnp.argmax(ys[mask])
|
|
178
|
+
best_params = jax.tree.map(lambda x: x[mask][best_params_idx], _params)
|
|
179
|
+
|
|
180
|
+
# Initialize the gaussian processes state
|
|
181
|
+
gp_params = GPParams(
|
|
182
|
+
noise=jnp.full((1, 1), 1.0 * noise_scale),
|
|
183
|
+
amplitude=jnp.zeros((1, 1)),
|
|
184
|
+
lengthscale=jnp.zeros((1, len(_params))),
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
# Fit to the current observations
|
|
188
|
+
xs = jnp.stack(
|
|
189
|
+
[self.domain[key].transform(_params[key]) for key in _params], axis=1
|
|
190
|
+
)
|
|
191
|
+
gp_params = posterior_fit(ys, xs, mask=mask, params=gp_params)
|
|
192
|
+
|
|
193
|
+
ys = self.sign * ys
|
|
194
|
+
best_score = self.sign * best_score
|
|
195
|
+
opt_state = OptimizerState(
|
|
196
|
+
params=_params,
|
|
197
|
+
ys=ys,
|
|
198
|
+
best_score=best_score,
|
|
199
|
+
best_params=best_params,
|
|
200
|
+
mask=mask,
|
|
201
|
+
gp_params=gp_params,
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
return opt_state
|
|
205
|
+
|
|
206
|
+
@partial(jax.jit, static_argnames=("self", "size"))
|
|
207
|
+
def sample(self, key, state, size=10_000):
|
|
208
|
+
"""
|
|
209
|
+
Sample new parameters using the acquisition function.
|
|
210
|
+
|
|
211
|
+
Args:
|
|
212
|
+
key: JAX PseudoRandom key for random sampling.
|
|
213
|
+
state: Current optimizer state.
|
|
214
|
+
size: Number of samples to draw.
|
|
215
|
+
|
|
216
|
+
Returns
|
|
217
|
+
-------
|
|
218
|
+
Sampled parameters (dict).
|
|
219
|
+
"""
|
|
220
|
+
# Sample 'size' elements of each distribution.
|
|
221
|
+
samples = self.param_space.sample_params(key, (size,))
|
|
222
|
+
xs_samples = self.param_space.to_array(samples)
|
|
223
|
+
|
|
224
|
+
# Prepare the data for the Gaussian process prediction.
|
|
225
|
+
xs = self.param_space.to_array(state.params)
|
|
226
|
+
ys = self.sign * state.ys
|
|
227
|
+
mask = state.mask
|
|
228
|
+
gp_params = state.gp_params
|
|
229
|
+
|
|
230
|
+
# Compute the acquisition function values for the sampled points.
|
|
231
|
+
acq_vals = self.acq(xs_samples, xs, ys, mask, gp_params)
|
|
232
|
+
|
|
233
|
+
# Of those, find the best 50 points and optimize them using BFGS.
|
|
234
|
+
top_idxs = jnp.argsort(acq_vals)[-50:]
|
|
235
|
+
init_points = xs_samples[top_idxs]
|
|
236
|
+
|
|
237
|
+
def acquisition_objective(x):
|
|
238
|
+
return jnp.squeeze(self.acq(x[None, :], xs, ys, mask, gp_params))
|
|
239
|
+
|
|
240
|
+
optimized = jax.vmap(
|
|
241
|
+
lambda x: _optimize_suggestion(x, acquisition_objective, max_iter=10)
|
|
242
|
+
)(init_points)
|
|
243
|
+
opt_vals = self.acq(optimized, xs, ys, mask, gp_params)
|
|
244
|
+
|
|
245
|
+
# Return the best point from the optimized points and the sampled points.
|
|
246
|
+
all_points = jnp.concatenate((optimized, xs_samples), axis=0)
|
|
247
|
+
all_vals = jnp.concatenate((opt_vals, acq_vals), axis=0)
|
|
248
|
+
chosen_suggestion = jnp.argmax(all_vals)
|
|
249
|
+
|
|
250
|
+
best_params = self.param_space.to_dict(all_points[chosen_suggestion][None])
|
|
251
|
+
return best_params
|
|
252
|
+
|
|
253
|
+
def expand(self, opt_state: OptimizerState):
|
|
254
|
+
"""
|
|
255
|
+
Expand internal buffers if no space is available.
|
|
256
|
+
|
|
257
|
+
Args:
|
|
258
|
+
opt_state: Current optimizer state.
|
|
259
|
+
|
|
260
|
+
Returns
|
|
261
|
+
-------
|
|
262
|
+
OptimizerState with expanded storage.
|
|
263
|
+
"""
|
|
264
|
+
current = jnp.sum(opt_state.mask)
|
|
265
|
+
|
|
266
|
+
if current == len(opt_state.mask):
|
|
267
|
+
pad_value = int(np.ceil(len(opt_state.mask) * 2 / 10) * 10)
|
|
268
|
+
diff = pad_value - len(opt_state.mask)
|
|
269
|
+
mask = jnp.pad(opt_state.mask, (0, diff))
|
|
270
|
+
ys = jnp.pad(opt_state.ys, (0, diff))
|
|
271
|
+
params = {}
|
|
272
|
+
for key in opt_state.params:
|
|
273
|
+
params[key] = jnp.pad(opt_state.params[key], (0, diff))
|
|
274
|
+
else:
|
|
275
|
+
mask = opt_state.mask
|
|
276
|
+
ys = opt_state.ys
|
|
277
|
+
params = opt_state.params
|
|
278
|
+
|
|
279
|
+
opt_state = OptimizerState(
|
|
280
|
+
params=params,
|
|
281
|
+
ys=ys,
|
|
282
|
+
best_score=opt_state.best_score,
|
|
283
|
+
best_params=opt_state.best_params,
|
|
284
|
+
mask=mask,
|
|
285
|
+
gp_params=opt_state.gp_params,
|
|
286
|
+
)
|
|
287
|
+
return opt_state
|
|
288
|
+
|
|
289
|
+
def fit(self, opt_state, y, new_params):
|
|
290
|
+
"""
|
|
291
|
+
Update optimizer state with a new observation.
|
|
292
|
+
|
|
293
|
+
Args:
|
|
294
|
+
opt_state: Current optimizer state.
|
|
295
|
+
y: New objective value.
|
|
296
|
+
new_params: Parameters that produced y.
|
|
297
|
+
|
|
298
|
+
Returns
|
|
299
|
+
-------
|
|
300
|
+
Updated OptimizerState.
|
|
301
|
+
"""
|
|
302
|
+
opt_state = self.expand(opt_state) # Prompts recompilation
|
|
303
|
+
opt_state = self._fit(opt_state, y, new_params)
|
|
304
|
+
return opt_state
|
|
305
|
+
|
|
306
|
+
@partial(jax.jit, static_argnums=(0,))
|
|
307
|
+
def _fit(self, opt_state, y, new_params):
|
|
308
|
+
"""Update optimizer state with new observation (JIT-compiled)."""
|
|
309
|
+
last_idx = jnp.arange(len(opt_state.mask)) == jnp.argmin(opt_state.mask)
|
|
310
|
+
mask = jnp.asarray(jnp.where(last_idx, True, opt_state.mask))
|
|
311
|
+
ys = jnp.where(last_idx, y, opt_state.ys)
|
|
312
|
+
params = jax.tree_util.tree_map(
|
|
313
|
+
lambda x, y: jnp.where(last_idx, y, x), opt_state.params, new_params
|
|
314
|
+
)
|
|
315
|
+
|
|
316
|
+
xs = jnp.stack(
|
|
317
|
+
[self.domain[key].transform(params[key]) for key in params], axis=1
|
|
318
|
+
)
|
|
319
|
+
ys = self.sign * ys
|
|
320
|
+
gp_params = posterior_fit(ys, xs, mask=mask, params=opt_state.gp_params)
|
|
321
|
+
|
|
322
|
+
best_score = jnp.max(ys, where=mask, initial=-jnp.inf)
|
|
323
|
+
best_params_idx = jnp.argmax(jnp.where(mask, ys, -jnp.inf))
|
|
324
|
+
best_params = jax.tree_util.tree_map(lambda x: x[best_params_idx], params)
|
|
325
|
+
|
|
326
|
+
ys = self.sign * ys
|
|
327
|
+
best_score = self.sign * best_score
|
|
328
|
+
opt_state = OptimizerState(
|
|
329
|
+
params=params,
|
|
330
|
+
ys=ys,
|
|
331
|
+
best_score=best_score,
|
|
332
|
+
best_params=best_params,
|
|
333
|
+
mask=mask,
|
|
334
|
+
gp_params=gp_params,
|
|
335
|
+
)
|
|
336
|
+
return opt_state
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: LightningBO
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Bayesian optimization library
|
|
5
|
+
License-Expression: Apache-2.0
|
|
6
|
+
Requires-Python: >=3.12
|
|
7
|
+
License-File: LICENSE
|
|
8
|
+
Requires-Dist: jax
|
|
9
|
+
Requires-Dist: jaxlib
|
|
10
|
+
Requires-Dist: optax
|
|
11
|
+
Requires-Dist: numpy
|
|
12
|
+
Requires-Dist: absl-py
|
|
13
|
+
Requires-Dist: pytest
|
|
14
|
+
Requires-Dist: matplotlib
|
|
15
|
+
Dynamic: license-file
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
LightningBO/__init__.py
|
|
5
|
+
LightningBO/bayesian_core.py
|
|
6
|
+
LightningBO/bayesian_optimization.py
|
|
7
|
+
LightningBO.egg-info/PKG-INFO
|
|
8
|
+
LightningBO.egg-info/SOURCES.txt
|
|
9
|
+
LightningBO.egg-info/dependency_links.txt
|
|
10
|
+
LightningBO.egg-info/requires.txt
|
|
11
|
+
LightningBO.egg-info/top_level.txt
|
|
12
|
+
tests/test_bayesian_core.py
|
|
13
|
+
tests/test_bayesian_optimization.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
LightningBO
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: LightningBO
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Bayesian optimization library
|
|
5
|
+
License-Expression: Apache-2.0
|
|
6
|
+
Requires-Python: >=3.12
|
|
7
|
+
License-File: LICENSE
|
|
8
|
+
Requires-Dist: jax
|
|
9
|
+
Requires-Dist: jaxlib
|
|
10
|
+
Requires-Dist: optax
|
|
11
|
+
Requires-Dist: numpy
|
|
12
|
+
Requires-Dist: absl-py
|
|
13
|
+
Requires-Dist: pytest
|
|
14
|
+
Requires-Dist: matplotlib
|
|
15
|
+
Dynamic: license-file
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# LightningBO
|
|
2
|
+
|
|
3
|
+
LightningBOは、ベイズ最適化(Bayesian Optimization)を簡単に実装・拡張できるPythonライブラリです。
|
|
4
|
+
本ライブラリは、ブラックボックス関数の最適化やハイパーパラメータ探索など、試行回数を抑えつつ効率的に最適解を見つけたい場面で活用できます。
|
|
5
|
+
|
|
6
|
+
## 特徴
|
|
7
|
+
|
|
8
|
+
- JAXベースの高速な計算
|
|
9
|
+
- 連続・離散パラメータ両対応
|
|
10
|
+
- 期待値改善(EI)などの獲得関数
|
|
11
|
+
- 拡張性の高い設計
|
|
12
|
+
|
|
13
|
+
## 使い方例
|
|
14
|
+
|
|
15
|
+
```python
|
|
16
|
+
import LightningBO.bayesian_optimization as bo
|
|
17
|
+
import LightningBO.bayesian_core as bc
|
|
18
|
+
|
|
19
|
+
# 最適化したい関数
|
|
20
|
+
def f(x):
|
|
21
|
+
return x**2 - 4*x + 5
|
|
22
|
+
|
|
23
|
+
# 探索空間の定義
|
|
24
|
+
domain = {"x": bc.Continuous(-10, 10)}
|
|
25
|
+
|
|
26
|
+
# Optimizerの作成
|
|
27
|
+
optimizer = bo.Optimizer(domain=domain, maximize=False)
|
|
28
|
+
|
|
29
|
+
# 初期点
|
|
30
|
+
params = {"x": [0.0, 2.0, 5.0]}
|
|
31
|
+
ys = [f(x) for x in params["x"]]
|
|
32
|
+
state = optimizer.init(ys, params)
|
|
33
|
+
|
|
34
|
+
# 最適化ループ
|
|
35
|
+
for step in range(20):
|
|
36
|
+
new_params = optimizer.sample(None, state)
|
|
37
|
+
y = f(**new_params)
|
|
38
|
+
state = optimizer.fit(state, y, new_params)
|
|
39
|
+
|
|
40
|
+
print("最適値:", state.best_score)
|
|
41
|
+
print("最適パラメータ:", state.best_params)
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## 主な用途
|
|
45
|
+
|
|
46
|
+
- 機械学習モデルのハイパーパラメータ最適化
|
|
47
|
+
- 実験・シミュレーションのパラメータ探索
|
|
48
|
+
- その他ブラックボックス関数の最適化
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "LightningBO"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Bayesian optimization library"
|
|
5
|
+
requires-python = ">=3.12"
|
|
6
|
+
|
|
7
|
+
license = "Apache-2.0"
|
|
8
|
+
dependencies = [
|
|
9
|
+
"jax",
|
|
10
|
+
"jaxlib",
|
|
11
|
+
"optax",
|
|
12
|
+
"numpy",
|
|
13
|
+
"absl-py",
|
|
14
|
+
"pytest",
|
|
15
|
+
"matplotlib"
|
|
16
|
+
]
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"""Tests for the Bayesian core functionality including GP operations and acquisition functions."""
|
|
2
|
+
|
|
3
|
+
import jax
|
|
4
|
+
import jax.numpy as jnp
|
|
5
|
+
from absl.testing import absltest, parameterized
|
|
6
|
+
|
|
7
|
+
from LightningBO.bayesian_core import (
|
|
8
|
+
GPParams,
|
|
9
|
+
Integer,
|
|
10
|
+
Continuous,
|
|
11
|
+
cov,
|
|
12
|
+
exp_quadratic,
|
|
13
|
+
expected_improvement,
|
|
14
|
+
gaussian_process,
|
|
15
|
+
lower_confidence_bounds,
|
|
16
|
+
marginal_likelihood,
|
|
17
|
+
predict,
|
|
18
|
+
probability_improvement,
|
|
19
|
+
upper_confidence_bounds,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
class BayesianCoreDomainTest(parameterized.TestCase):
|
|
23
|
+
"""Test suite for Bayesian domain classes (Continuous, Integer)."""
|
|
24
|
+
|
|
25
|
+
@parameterized.parameters(
|
|
26
|
+
(0.0, 1.0),
|
|
27
|
+
(-5.5, 5.5),
|
|
28
|
+
)
|
|
29
|
+
def test_continuous_transform_clips_within_bounds(self, lower, upper):
|
|
30
|
+
"""Test that Continuous domain clips values within bounds."""
|
|
31
|
+
domain = Continuous(lower, upper)
|
|
32
|
+
x = jnp.array([lower - 1, (lower + upper) / 2, upper + 1])
|
|
33
|
+
out = domain.transform(x)
|
|
34
|
+
self.assertTrue(jnp.all(out >= lower) and jnp.all(out <= upper))
|
|
35
|
+
|
|
36
|
+
def test_continuous_sample_within_bounds(self):
|
|
37
|
+
"""Test that Continuous domain samples are within bounds."""
|
|
38
|
+
domain = Continuous(0.0, 1.0)
|
|
39
|
+
key = jax.random.PRNGKey(0)
|
|
40
|
+
shape = (100,)
|
|
41
|
+
samples = domain.sample(key, shape)
|
|
42
|
+
self.assertEqual(samples.shape, shape)
|
|
43
|
+
self.assertTrue(jnp.all(samples >= 0.0) and jnp.all(samples <= 1.0))
|
|
44
|
+
|
|
45
|
+
@parameterized.parameters(
|
|
46
|
+
(0, 5),
|
|
47
|
+
)
|
|
48
|
+
def test_integer_transform_clips_within_bounds(self, lower, upper):
|
|
49
|
+
domain = Integer(lower, upper)
|
|
50
|
+
x = jnp.array([lower - 1, lower, upper, upper + 1])
|
|
51
|
+
out = domain.transform(x)
|
|
52
|
+
self.assertTrue(jnp.all(out >= lower) and jnp.all(out <= upper))
|
|
53
|
+
|
|
54
|
+
def test_integer_sample_within_bounds(self):
|
|
55
|
+
domain = Integer(0, 5)
|
|
56
|
+
key = jax.random.PRNGKey(0)
|
|
57
|
+
shape = (100,)
|
|
58
|
+
samples = domain.sample(key, shape)
|
|
59
|
+
self.assertEqual(samples.shape, shape)
|
|
60
|
+
self.assertTrue(jnp.all(samples >= 0) and jnp.all(samples <= 5))
|
|
61
|
+
|
|
62
|
+
# ...他のテスト...
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"""Tests for Bayesian optimization functionality."""
|
|
2
|
+
|
|
3
|
+
import jax
|
|
4
|
+
import jax.numpy as jnp
|
|
5
|
+
import numpy as np
|
|
6
|
+
from absl.testing import absltest
|
|
7
|
+
|
|
8
|
+
import LightningBO.bayesian_core as bc
|
|
9
|
+
import LightningBO.bayesian_optimization as bo
|
|
10
|
+
|
|
11
|
+
KEY = jax.random.PRNGKey(42)
|
|
12
|
+
SEED = 42
|
|
13
|
+
|
|
14
|
+
class BayesianOptimizationTest(absltest.TestCase):
|
|
15
|
+
"""Test suite for Bayesian optimization functionality."""
|
|
16
|
+
|
|
17
|
+
def test_1D_optim(self):
|
|
18
|
+
"""Test 1D optimization with sin function."""
|
|
19
|
+
def f(x):
|
|
20
|
+
return jnp.sin(x) - 0.1 * x**2
|
|
21
|
+
|
|
22
|
+
TARGET = np.max(f(np.linspace(-2, 2, 1000)))
|
|
23
|
+
domain = {"x": bc.Continuous(-2, 2)}
|
|
24
|
+
opt = bo.Optimizer(domain=domain, maximize=True)
|
|
25
|
+
params = {"x": [0.0, 1.0, 2.0]}
|
|
26
|
+
ys = [f(x) for x in params["x"]]
|
|
27
|
+
opt_state = opt.init(ys, params)
|
|
28
|
+
self.assertEqual(opt_state.best_score, np.max(ys))
|
|
29
|
+
self.assertEqual(opt_state.best_params["x"], params["x"][np.argmax(ys)])
|
|
30
|
+
self.assertEqual(type(opt_state), bo.OptimizerState)
|
|
31
|
+
self.assertEqual(type(opt_state.gp_params), bc.GPParams)
|
|
32
|
+
self.assertEqual(opt_state.params["x"].shape, (10,))
|
|
33
|
+
self.assertEqual(opt_state.ys.shape, (10,))
|
|
34
|
+
self.assertEqual(opt_state.mask.shape, (10,))
|
|
35
|
+
self.assertTrue(np.allclose(opt_state.params["x"][:3], params["x"]))
|
|
36
|
+
self.assertTrue(np.allclose(opt_state.ys[:3], ys))
|
|
37
|
+
self.assertTrue(np.allclose(opt_state.mask[:3], [True, True, True]))
|
|
38
|
+
self.assertTrue(np.allclose(opt_state.mask[3:], [False] * 7))
|
|
39
|
+
key = jax.random.PRNGKey(SEED)
|
|
40
|
+
sample_fn = jax.jit(opt.sample)
|
|
41
|
+
for step in range(100):
|
|
42
|
+
key = jax.random.fold_in(key, step)
|
|
43
|
+
new_params = sample_fn(key, opt_state)
|
|
44
|
+
y = f(**new_params)
|
|
45
|
+
opt_state = opt.fit(opt_state, y, new_params)
|
|
46
|
+
if jnp.allclose(opt_state.best_score, TARGET, atol=1e-03):
|
|
47
|
+
break
|
|
48
|
+
# ...他のテスト...
|