Skip to content

Commit 16b1708

Browse files
committed
First demo
1 parent 0a31ca3 commit 16b1708

File tree

10 files changed

+692
-3
lines changed

10 files changed

+692
-3
lines changed

LICENSE

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
MIT License
22

3-
Copyright (c) 2024 loriskong
3+
Copyright (c) 2024 Loris Kong <[email protected]>
44

55
Permission is hereby granted, free of charge, to any person obtaining a copy
66
of this software and associated documentation files (the "Software"), to deal

README.md

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,32 @@
1-
# BayesianSafetyValidation
2-
A Python implementation of bayesian safety validation with gaussian process
1+
# Bayesian Safety Validation
2+
A Python implementation of bayesian safety validation.
3+
4+
## Usage
5+
```pyhton
6+
from bayesian_safety_validation import BayesianSafetyValidation
7+
8+
# Define a black box function
9+
def black_box_func(params) -> float:
10+
return float(
11+
(
12+
(params["x1"] + 2 * params["x2"] - 7) ** 2
13+
+ (2 * params["x1"] + params["x2"] - 5) ** 2
14+
)
15+
<= 200
16+
)
17+
18+
# Create BSV with a parameter space
19+
bsv = BayesianSafetyValidation(param_space={"x1": (-10, 5), "x2": (-10, 5)})
20+
21+
# Run BSV loop.
22+
for i in range(10):
23+
suggestions = bsv.suggest()
24+
evaluations = [black_box_func(suggestion) for suggestion in suggestions]
25+
print(f"suggestions: {suggestions}, evaluations: {evaluations}")
26+
bsv.refit(suggestions, evaluations)
27+
28+
# Display results
29+
bsv.falsification()
30+
```
31+
You will get this graph after running forementioned code
32+
![](./statics/bsv_example.png)
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
"""A Python implementation of bayesian safety validation."""
2+
from .bayesian_safety_validation import BayesianSafetyValidation
3+
4+
import importlib.metadata
5+
__version__ = importlib.metadata.version('bayesian-safety-validation')
6+
7+
8+
__all__ = [
9+
"BayesianSafetyValidation",
10+
]

bayesian_safety_validation/acq_max.py

Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
"""General acquisition function maxize algo.
2+
The acq_max implementation in bayes_opt 1.4.3 is not good enough,
3+
so I copied the implementation from commit 383cb29.
4+
"""
5+
import json
6+
import warnings
7+
8+
import numpy as np
9+
from scipy.optimize import minimize
10+
from scipy.stats import norm
11+
12+
13+
def acq_max(
14+
ac,
15+
gp,
16+
y_max,
17+
bounds,
18+
random_state,
19+
constraint=None,
20+
n_warmup=10000,
21+
n_iter=10,
22+
y_max_params=None,
23+
):
24+
"""Find the maximum of the acquisition function.
25+
26+
It uses a combination of random sampling (cheap) and the 'L-BFGS-B'
27+
optimization method. First by sampling `n_warmup` (1e5) points at random,
28+
and then running L-BFGS-B from `n_iter` (10) random starting points.
29+
30+
Parameters
31+
----------
32+
ac : callable
33+
Acquisition function to use. Should accept an array of parameters `x`,
34+
an from sklearn.gaussian_process.GaussianProcessRegressor `gp` and the
35+
best current value `y_max` as parameters.
36+
37+
gp : sklearn.gaussian_process.GaussianProcessRegressor
38+
A gaussian process regressor modelling the target function based on
39+
previous observations.
40+
41+
y_max : number
42+
Highest found value of the target function.
43+
44+
bounds : np.ndarray
45+
Bounds of the search space. For `N` parameters this has shape
46+
`(N, 2)` with `[i, 0]` the lower bound of parameter `i` and
47+
`[i, 1]` the upper bound.
48+
49+
random_state : np.random.RandomState
50+
A random state to sample from.
51+
52+
constraint : ConstraintModel or None, default=None
53+
If provided, the acquisition function will be adjusted according
54+
to the probability of fulfilling the constraint.
55+
56+
n_warmup : int, default=10000
57+
Number of points to sample from the acquisition function as seeds
58+
before looking for a minimum.
59+
60+
n_iter : int, default=10
61+
Points to run L-BFGS-B optimization from.
62+
63+
y_max_params : np.array
64+
Function parameters that produced the maximum known value given by `y_max`.
65+
66+
:param y_max_params:
67+
Function parameters that produced the maximum known value given by `y_max`.
68+
69+
Returns
70+
-------
71+
Parameters maximizing the acquisition function.
72+
73+
"""
74+
# We need to adjust the acquisition function to deal with constraints when there is some
75+
if constraint is not None:
76+
77+
def adjusted_ac(x):
78+
"""Acquisition function adjusted to fulfill the constraint when necessary.
79+
80+
Parameters
81+
----------
82+
x : np.ndarray
83+
Parameter at which to sample.
84+
85+
86+
Returns
87+
-------
88+
The value of the acquisition function adjusted for constraints.
89+
"""
90+
# Transforms the problem in a minimization problem, this is necessary
91+
# because the solver we are using later on is a minimizer
92+
values = -ac(x.reshape(-1, bounds.shape[0]), gp=gp, y_max=y_max)
93+
p_constraints = constraint.predict(x.reshape(-1, bounds.shape[0]))
94+
95+
# Slower fallback for the case where any values are negative
96+
if np.any(values > 0):
97+
# TODO: This is not exactly how Gardner et al do it.
98+
# Their way would require the result of the acquisition function
99+
# to be strictly positive, which is not the case here. For a
100+
# positive target value, we use Gardner's version. If the target
101+
# is negative, we instead slightly rescale the target depending
102+
# on the probability estimate to fulfill the constraint.
103+
return np.array(
104+
[
105+
value / (0.5 + 0.5 * p) if value > 0 else value * p
106+
for value, p in zip(values, p_constraints)
107+
]
108+
)
109+
110+
# Faster, vectorized version of Gardner et al's method
111+
return values * p_constraints
112+
113+
else:
114+
# Transforms the problem in a minimization problem, this is necessary
115+
# because the solver we are using later on is a minimizer
116+
adjusted_ac = lambda x: -ac(x.reshape(-1, bounds.shape[0]), gp=gp, y_max=y_max)
117+
118+
# Warm up with random points
119+
x_tries = random_state.uniform(
120+
bounds[:, 0], bounds[:, 1], size=(n_warmup, bounds.shape[0])
121+
)
122+
ys = -adjusted_ac(x_tries)
123+
x_max = x_tries[ys.argmax()]
124+
max_acq = ys.max()
125+
126+
# Explore the parameter space more thoroughly
127+
x_seeds = random_state.uniform(
128+
bounds[:, 0],
129+
bounds[:, 1],
130+
size=(1 + n_iter + int(not y_max_params is None), bounds.shape[0]),
131+
)
132+
# Add the best candidate from the random sampling to the seeds so that the
133+
# optimization algorithm can try to walk up to that particular local maxima
134+
x_seeds[0] = x_max
135+
if not y_max_params is None:
136+
# Add the provided best sample to the seeds so that the optimization
137+
# algorithm is aware of it and will attempt to find its local maxima
138+
x_seeds[1] = y_max_params
139+
140+
for x_try in x_seeds:
141+
# Find the minimum of minus the acquisition function
142+
res = minimize(adjusted_ac, x_try, bounds=bounds, method="L-BFGS-B")
143+
144+
# See if success
145+
if not res.success:
146+
continue
147+
148+
# Store it if better than previous minimum(maximum).
149+
if max_acq is None or -np.squeeze(res.fun) >= max_acq:
150+
x_max = res.x
151+
max_acq = -np.squeeze(res.fun)
152+
153+
# Clip output to make sure it lies within the bounds. Due to floating
154+
# point technicalities this is not always the case.
155+
return np.clip(x_max, bounds[:, 0], bounds[:, 1])

0 commit comments

Comments
 (0)