Skip to content

Commit 12edcba

Browse files
default verbose initial design
1 parent f554f42 commit 12edcba

File tree

5 files changed

+382
-3
lines changed

5 files changed

+382
-3
lines changed

GPopt/BOstopping.py

Lines changed: 243 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,243 @@
1+
import numpy as np
2+
from scipy.stats import mannwhitneyu, norm
3+
from scipy.stats import wasserstein_distance
4+
from sklearn.gaussian_process import GaussianProcessRegressor
5+
from sklearn.gaussian_process.kernels import Matern
6+
from copy import deepcopy
7+
from tqdm import tqdm
8+
import xgboost as xgb
9+
from sklearn.datasets import make_classification
10+
from sklearn.model_selection import train_test_split, cross_validate, StratifiedKFold
11+
from sklearn.metrics import accuracy_score, log_loss
12+
from sklearn.preprocessing import StandardScaler
13+
import matplotlib.pyplot as plt
14+
15+
# BayesianOptimization class with robust early stopping
16+
class BOstopping:
17+
"""Bayesian Optimization with robust early stopping criteria."""
18+
19+
def __init__(self, f, bounds, n_init=5, kappa=1.96, early_stopping=True,
20+
stop_patience=20, stop_threshold=0.01, n_test_points=100,
21+
alpha=1e-6, n_restarts_optimizer=25, seed=123):
22+
23+
self.f = f
24+
self.bounds = np.array(bounds)
25+
self.n_init = n_init
26+
self.kappa = kappa
27+
self.early_stopping = early_stopping
28+
self.stop_patience = stop_patience
29+
self.stop_threshold = stop_threshold
30+
self.n_test_points = n_test_points
31+
self.alpha = alpha
32+
self.n_restarts_optimizer = n_restarts_optimizer
33+
self.seed = seed
34+
35+
np.random.seed(self.seed)
36+
self.test_points = self._sample_random(n_test_points)
37+
38+
# History tracking
39+
self.wasserstein_history = []
40+
self.X = []
41+
self.y = []
42+
self.best_values = []
43+
self.acquisition_values = []
44+
self.gp_variance = []
45+
self.phase = []
46+
47+
# GP setup
48+
self.gp = GaussianProcessRegressor(
49+
kernel=Matern(nu=2.5),
50+
alpha=self.alpha,
51+
normalize_y=True,
52+
n_restarts_optimizer=self.n_restarts_optimizer,
53+
random_state=self.seed,
54+
)
55+
56+
def _sample_random(self, n_samples):
57+
"""Uniform sampling within bounds."""
58+
return np.random.uniform(
59+
self.bounds[:, 0], self.bounds[:, 1],
60+
size=(n_samples, len(self.bounds)))
61+
62+
def _acquisition(self, X_candidate):
63+
"""Expected Improvement acquisition function."""
64+
mu, sigma = self.gp.predict(X_candidate, return_std=True)
65+
mu_sample = np.min(self.y) # Use actual observed minimum
66+
67+
sigma = np.maximum(sigma, 1e-6)
68+
gamma = (mu_sample - mu) / sigma
69+
ei = (mu_sample - mu) * norm.cdf(gamma) + sigma * norm.pdf(gamma)
70+
return ei
71+
72+
def _get_posterior_samples(self, gp, n_samples=100):
73+
"""Sample from GP posterior at test points."""
74+
mu, sigma = gp.predict(self.test_points, return_std=True)
75+
return np.random.normal(mu, sigma, size=(n_samples, len(self.test_points)))
76+
77+
def _compute_wasserstein(self, gp_prev, gp_current):
78+
"""Compute approximate Wasserstein distance between posteriors."""
79+
mu_prev, std_prev = gp_prev.predict(self.test_points, return_std=True)
80+
mu_curr, std_curr = gp_current.predict(self.test_points, return_std=True)
81+
82+
# 2-Wasserstein distance for independent 1D Gaussians, averaged
83+
w2_per_point = (mu_prev - mu_curr)**2 + (std_prev - std_curr)**2
84+
return np.sqrt(np.mean(w2_per_point))
85+
86+
def _should_stop(self, gp_prev):
87+
"""Early stopping based on improvement and posterior stability."""
88+
if len(self.best_values) < self.stop_patience + 1:
89+
return False
90+
91+
# 1. Improvement check
92+
recent_improvements = np.diff(self.best_values[-self.stop_patience:])
93+
improvement_stop = np.all(np.abs(recent_improvements) < self.stop_threshold)
94+
95+
# 2. Posterior stability
96+
current_w = self._compute_wasserstein(gp_prev, self.gp)
97+
self.wasserstein_history.append(current_w)
98+
99+
if len(self.wasserstein_history) >= 2 * self.stop_patience:
100+
recent_w = self.wasserstein_history[-self.stop_patience:]
101+
older_w = self.wasserstein_history[-2*self.stop_patience:-self.stop_patience]
102+
_, p_value = mannwhitneyu(recent_w, older_w, alternative='greater')
103+
mwu_stable = (p_value > 0.1)
104+
var_stable = (np.var(recent_w) < 1e-6)
105+
posterior_stable = mwu_stable or var_stable
106+
else:
107+
posterior_stable = False
108+
109+
return improvement_stop or posterior_stable
110+
111+
def optimize(self, n_iter=100):
112+
"""Run Bayesian optimization loop."""
113+
print("Starting Initial Design Phase...")
114+
self.X = self._sample_random(self.n_init)
115+
self.y = []
116+
117+
for i, x in enumerate(self.X):
118+
y_val = self.f(x)
119+
self.y.append(y_val)
120+
current_best = np.min(self.y)
121+
self.best_values.append(current_best)
122+
self.acquisition_values.append(0)
123+
self.gp_variance.append(0)
124+
self.phase.append('initial')
125+
print(f" Initial sample {i+1}/{self.n_init}: f(x) = {y_val:.6f}, best = {current_best:.6f}")
126+
127+
print(f"Initial Design Complete. Best value: {np.min(self.y):.6f}")
128+
print("\nStarting Bayesian Optimization Phase...")
129+
130+
gp_prev = None
131+
for i in tqdm(range(n_iter), desc="Bayesian Optimization"):
132+
if i > 0:
133+
gp_prev = deepcopy(self.gp)
134+
135+
self.gp.fit(self.X, self.y)
136+
137+
X_candidate = self._sample_random(1000)
138+
acq = self._acquisition(X_candidate)
139+
best_acq_idx = np.argmax(acq)
140+
x_next = X_candidate[best_acq_idx]
141+
max_acq_value = acq[best_acq_idx]
142+
143+
_, gp_std = self.gp.predict([x_next], return_std=True)
144+
145+
y_next = self.f(x_next)
146+
self.X = np.vstack((self.X, x_next))
147+
self.y.append(y_next)
148+
current_best = np.min(self.y)
149+
self.best_values.append(current_best)
150+
self.acquisition_values.append(max_acq_value)
151+
self.gp_variance.append(gp_std[0])
152+
self.phase.append('bayesian')
153+
154+
print(f" Iteration {i+1}: f(x) = {y_next:.6f}, best = {current_best:.6f}, "
155+
f"EI = {max_acq_value:.6f}, σ = {gp_std[0]:.6f}")
156+
157+
if gp_prev is not None:
158+
w_dist = self._compute_wasserstein(gp_prev, self.gp)
159+
self.wasserstein_history.append(w_dist)
160+
print(f" Wasserstein distance: {w_dist:.8f}")
161+
162+
if self.early_stopping and i > self.n_init and gp_prev is not None:
163+
if self._should_stop(gp_prev):
164+
print(f"Early stopping at iteration {i+1}")
165+
break
166+
167+
best_idx = np.argmin(self.y)
168+
return self.X[best_idx], self.y[best_idx]
169+
170+
171+
def plot_optimization_history(optimizer, title):
172+
"""Plot optimization convergence and diagnostics."""
173+
fig, axes = plt.subplots(2, 2, figsize=(15, 10))
174+
phase_colors = ['red' if p == 'initial' else 'blue' for p in optimizer.phase]
175+
iterations = range(len(optimizer.best_values))
176+
177+
# Plot 1: Convergence
178+
axes[0, 0].scatter(iterations, optimizer.best_values, c=phase_colors, alpha=0.7, s=30)
179+
axes[0, 0].plot(optimizer.best_values, 'k-', alpha=0.3, linewidth=1)
180+
axes[0, 0].set_xlabel('Iteration')
181+
axes[0, 0].set_ylabel('Best Objective Value')
182+
axes[0, 0].set_title(f'{title} - Convergence (Red=Initial, Blue=Bayesian)')
183+
axes[0, 0].grid(True, alpha=0.3)
184+
185+
n_initial = sum(1 for p in optimizer.phase if p == 'initial')
186+
axes[0, 0].axvline(x=n_initial-0.5, color='orange', linestyle='--', alpha=0.8, linewidth=2, label='Phase Boundary')
187+
axes[0, 0].legend()
188+
189+
# Plot 2: Acquisition
190+
bayesian_iters = [i for i, p in enumerate(optimizer.phase) if p == 'bayesian']
191+
bayesian_acq = [optimizer.acquisition_values[i] for i in bayesian_iters]
192+
if bayesian_acq:
193+
axes[0, 1].plot(bayesian_iters, bayesian_acq, 'g-', linewidth=2, marker='o', markersize=4)
194+
axes[0, 1].set_xlabel('Iteration')
195+
axes[0, 1].set_ylabel('Expected Improvement')
196+
axes[0, 1].set_title(f'{title} - Acquisition Function')
197+
axes[0, 1].grid(True, alpha=0.3)
198+
else:
199+
axes[0, 1].text(0.5, 0.5, 'No Bayesian iterations', ha='center', va='center', transform=axes[0, 1].transAxes)
200+
201+
# Plot 3: GP Uncertainty
202+
if bayesian_acq:
203+
bayesian_var = [optimizer.gp_variance[i] for i in bayesian_iters]
204+
axes[1, 0].plot(bayesian_iters, bayesian_var, 'purple', linewidth=2, marker='s', markersize=4)
205+
axes[1, 0].set_xlabel('Iteration')
206+
axes[1, 0].set_ylabel('GP Standard Deviation')
207+
axes[1, 0].set_title(f'{title} - GP Uncertainty')
208+
axes[1, 0].grid(True, alpha=0.3)
209+
else:
210+
axes[1, 0].text(0.5, 0.5, 'No Bayesian iterations', ha='center', va='center', transform=axes[1, 0].transAxes)
211+
212+
# Plot 4: Posterior Stability
213+
if optimizer.wasserstein_history:
214+
w_start_iter = n_initial + 1
215+
w_iterations = range(w_start_iter, w_start_iter + len(optimizer.wasserstein_history))
216+
axes[1, 1].plot(w_iterations, optimizer.wasserstein_history, 'r-', linewidth=2, marker='d', markersize=4)
217+
axes[1, 1].set_xlabel('Iteration')
218+
axes[1, 1].set_ylabel('Wasserstein Distance')
219+
axes[1, 1].set_title(f'{title} - Posterior Stability')
220+
axes[1, 1].grid(True, alpha=0.3)
221+
axes[1, 1].set_yscale('log')
222+
else:
223+
axes[1, 1].text(0.5, 0.5, 'No Wasserstein history\n(Need ≥2 Bayesian iterations)',
224+
ha='center', va='center', transform=axes[1, 1].transAxes)
225+
226+
plt.tight_layout()
227+
plt.show()
228+
229+
print(f"\n{title} Optimization Summary:")
230+
print("=" * 50)
231+
print(f"Total iterations: {len(optimizer.best_values)}")
232+
print(f"Initial design: {n_initial} samples")
233+
print(f"Bayesian optimization: {len(optimizer.best_values) - n_initial} iterations")
234+
print(f"Initial best: {optimizer.best_values[n_initial-1]:.6f}")
235+
print(f"Final best: {min(optimizer.best_values):.6f}")
236+
print(f"Improvement: {optimizer.best_values[n_initial-1] - min(optimizer.best_values):.6f}")
237+
if optimizer.wasserstein_history:
238+
print(f"Avg Wasserstein distance: {np.mean(optimizer.wasserstein_history):.8f}")
239+
print(f"Final Wasserstein distance: {optimizer.wasserstein_history[-1]:.8f}")
240+
if bayesian_acq:
241+
print(f"Avg Expected Improvement: {np.mean(bayesian_acq):.6f}")
242+
print(f"Final Expected Improvement: {bayesian_acq[-1]:.6f}")
243+

GPopt/GPOpt.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@ def __init__(
121121
n_restarts_optimizer=25,
122122
seed=123,
123123
save=None,
124-
n_jobs=None,
124+
n_jobs=1,
125125
acquisition="ei",
126126
method="bayesian",
127127
min_value=None,

GPopt/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
from .GPOpt import GPOpt
2+
from .BOstopping import BOstopping
23

3-
__all__ = ["GPOpt"]
4+
__all__ = ["GPOpt", "BOstopping"]

examples/bo_stopping.py

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
import numpy as np
2+
import GPopt as gp
3+
from sklearn.datasets import make_regression, make_classification, load_iris, load_breast_cancer, load_wine, load_diabetes
4+
from sklearn.model_selection import cross_val_score
5+
from sklearn.ensemble import (
6+
RandomForestRegressor, ExtraTreesRegressor, GradientBoostingRegressor,
7+
RandomForestClassifier, ExtraTreesClassifier, GradientBoostingClassifier
8+
)
9+
10+
# Objective function: negative CV score for a given model and hyperparameter
11+
def make_objective(model_class, X, y, scoring):
12+
def objective(x):
13+
max_depth = int(np.round(x[0]))
14+
model = model_class(max_depth=max_depth, random_state=42)
15+
scores = cross_val_score(model, X, y, cv=3, scoring=scoring)
16+
# For classifiers, maximize accuracy; for regressors, minimize MSE
17+
return -np.mean(scores) if 'neg_' in scoring else -np.mean(scores)
18+
return objective
19+
20+
# Regression example
21+
X_reg, y_reg = make_regression(n_samples=200, n_features=10, noise=0.2, random_state=42)
22+
regressors = {
23+
"RandomForestRegressor": RandomForestRegressor,
24+
"ExtraTreesRegressor": ExtraTreesRegressor,
25+
"GradientBoostingRegressor": GradientBoostingRegressor,
26+
}
27+
28+
# Classification example
29+
X_clf, y_clf = make_classification(n_samples=200, n_features=10, n_classes=2, random_state=42)
30+
classifiers = {
31+
"RandomForestClassifier": RandomForestClassifier,
32+
"ExtraTreesClassifier": ExtraTreesClassifier,
33+
"GradientBoostingClassifier": GradientBoostingClassifier,
34+
}
35+
36+
bounds = [(2, 20)] # max_depth
37+
38+
# Regression optimization
39+
for name, model_class in regressors.items():
40+
print(f"\n=== Optimizing {name} max_depth (regression) ===")
41+
bo = gp.BOstopping(
42+
f=make_objective(model_class, X_reg, y_reg, scoring='neg_mean_squared_error'),
43+
bounds=bounds,
44+
n_init=5,
45+
early_stopping=True,
46+
stop_patience=8,
47+
stop_threshold=0.01,
48+
n_test_points=50,
49+
seed=42
50+
)
51+
x_best, y_best = bo.optimize(n_iter=30)
52+
print(f"Best max_depth: {int(np.round(x_best[0]))}, Best CV MSE: {y_best:.4f}")
53+
54+
try:
55+
from GPopt.GPopt.BOstopping import plot_optimization_history
56+
plot_optimization_history(bo, f"{name} max_depth (regression)")
57+
except ImportError:
58+
pass
59+
60+
# Classification optimization
61+
for name, model_class in classifiers.items():
62+
print(f"\n=== Optimizing {name} max_depth (classification) ===")
63+
bo = gp.BOstopping(
64+
f=make_objective(model_class, X_clf, y_clf, scoring='accuracy'),
65+
bounds=bounds,
66+
n_init=5,
67+
early_stopping=True,
68+
stop_patience=8,
69+
stop_threshold=0.001,
70+
n_test_points=50,
71+
seed=42
72+
)
73+
x_best, y_best = bo.optimize(n_iter=30)
74+
print(f"Best max_depth: {int(np.round(x_best[0]))}, Best CV Accuracy: {-y_best:.4f}")
75+
76+
try:
77+
from GPopt.GPopt.BOstopping import plot_optimization_history
78+
plot_optimization_history(bo, f"{name} max_depth (classification)")
79+
except ImportError:
80+
pass
81+
82+
# Classification datasets
83+
iris = load_iris()
84+
breast_cancer = load_breast_cancer()
85+
wine = load_wine()
86+
87+
classification_datasets = [
88+
("Iris", iris.data, iris.target),
89+
("Breast Cancer", breast_cancer.data, breast_cancer.target),
90+
("Wine", wine.data, wine.target),
91+
]
92+
93+
for ds_name, X, y in classification_datasets:
94+
for name, model_class in classifiers.items():
95+
print(f"\n=== Optimizing {name} max_depth on {ds_name} ===")
96+
bo = gp.BOstopping(
97+
f=make_objective(model_class, X, y, scoring='accuracy'),
98+
bounds=bounds,
99+
n_init=5,
100+
early_stopping=True,
101+
stop_patience=8,
102+
stop_threshold=0.001,
103+
n_test_points=50,
104+
seed=42
105+
)
106+
x_best, y_best = bo.optimize(n_iter=30)
107+
print(f"Best max_depth: {int(np.round(x_best[0]))}, Best CV Accuracy: {-y_best:.4f}")
108+
109+
try:
110+
from GPopt.GPopt.BOstopping import plot_optimization_history
111+
plot_optimization_history(bo, f"{name} max_depth ({ds_name})")
112+
except ImportError:
113+
pass
114+
115+
# Regression dataset
116+
diabetes = load_diabetes()
117+
print(f"\n=== Optimizing RandomForestRegressor max_depth on Diabetes ===")
118+
bo = gp.BOstopping(
119+
f=make_objective(RandomForestRegressor, diabetes.data, diabetes.target, scoring='neg_mean_squared_error'),
120+
bounds=bounds,
121+
n_init=5,
122+
early_stopping=True,
123+
stop_patience=8,
124+
stop_threshold=0.01,
125+
n_test_points=50,
126+
seed=42
127+
)
128+
x_best, y_best = bo.optimize(n_iter=30)
129+
print(f"Best max_depth: {int(np.round(x_best[0]))}, Best CV MSE: {y_best:.4f}")
130+
131+
try:
132+
from GPopt.GPopt.BOstopping import plot_optimization_history
133+
plot_optimization_history(bo, "RandomForestRegressor max_depth (Diabetes)")
134+
except ImportError:
135+
pass

0 commit comments

Comments
 (0)