Skip to content

Commit 8fd1ee2

Browse files
0.26.6 seed (random_state)
1 parent 8a9fec7 commit 8fd1ee2

5 files changed

Lines changed: 99 additions & 135 deletions

File tree

notebooks/00_spotPython_tests.ipynb

Lines changed: 17 additions & 41 deletions
Large diffs are not rendered by default.

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ build-backend = "setuptools.build_meta"
77

88
[project]
99
name = "spotpython"
10-
version = "0.26.5"
10+
version = "0.26.6"
1111
authors = [
1212
{ name="T. Bartz-Beielstein", email="tbb@bartzundbartz.de" }
1313
]

src/spotpython/gp/gp_sep.py

Lines changed: 69 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
from spotpython.utils.linalg import matrix_inversion_dispatcher
1515
from numpy.linalg import det
1616
from spotpython.utils.aggregate import select_distant_points
17+
from sklearn.base import BaseEstimator, RegressorMixin
1718

1819

1920
def crude_reset(theta, tmin, tmax, m):
@@ -114,12 +115,11 @@ def darg(d, X: np.ndarray = None, samp_size: int = 1000) -> dict:
114115
dict: Updated 'd' with fields 'start', 'min', 'max', 'mle', 'ab', etc.
115116
116117
Examples:
117-
>>> from spotpython.gp.gp_sep import GPsep
118+
>>> from spotpython.gp.gp_sep import darg
118119
>>> import numpy as np
119120
>>> X = np.array([[1, 2], [3, 4], [5, 6]])
120-
>>> gp = GPsep(m=2, n=3, X=X)
121121
>>> d = 2.5
122-
>>> result = gp.darg(d=d, X=X, samp_size=10)
122+
>>> result = darg(d=d, X=X, samp_size=10)
123123
>>> print(result)
124124
"""
125125
if X is None:
@@ -278,7 +278,7 @@ def garg(g, y: np.ndarray = None) -> dict:
278278
return g
279279

280280

281-
class GPsep:
281+
class GPsep(BaseEstimator, RegressorMixin):
282282
"""A class to represent a Gaussian Process with separable covariance.
283283
284284
Attributes:
@@ -303,22 +303,22 @@ class GPsep:
303303
verbosity: Verbosity level.
304304
auto_optimize: Whether to automatically optimize hyperparameters.
305305
max_points: Maximum number of points for model building.
306+
seed: Random seed for reproducibility.
306307
"""
307308

308309
def __init__(
309310
self,
310-
X: np.ndarray = None,
311-
y: np.ndarray = None,
312-
d: np.ndarray = None,
313-
g: float = None,
311+
d=None,
312+
g=None,
314313
nlsep_method="inv",
315314
gradnlsep_method="inv",
316315
n_restarts_optimizer=9,
317-
samp_size: int = 1000,
316+
samp_size=1000,
318317
maxit=100,
319318
verbosity=0,
320319
auto_optimize=True,
321320
max_points=None,
321+
seed=123,
322322
) -> None:
323323
"""
324324
Initialize the GP model with data and hyperparameters.
@@ -349,31 +349,9 @@ def __init__(
349349
max_points (int):
350350
Maximum number of points to use for the model building. Default is None, which means all points are used.
351351
"""
352-
if X is not None:
353-
# convert pandas dataframes or series to numpy arrays
354-
if hasattr(X, "to_numpy"):
355-
X = X.to_numpy()
356-
if y is not None:
357-
if hasattr(y, "to_numpy"):
358-
y = y.to_numpy()
359-
y = y.reshape(-1, 1)
360-
if X is not None and y is not None:
361-
if max_points is not None:
362-
X, y = select_distant_points(X, y, max_points)
363-
print(f"Selected {max_points} points for the model.")
364-
self.m = None # (int) number of input dimensions
365-
self.n = None # (int) number of observations
366-
self.X = X
367-
self.y = y
352+
# Hyperparameters (do not store training data)
368353
self.d = d
369354
self.g = g
370-
self.K = None
371-
self.Ki = None
372-
self.Kiy = None
373-
self.phi = None
374-
self.dK = None # boolean
375-
self.DK = None # matrix
376-
self.ldetK = None
377355
self.nlsep_method = nlsep_method
378356
self.gradnlsep_method = gradnlsep_method
379357
self.n_restarts_optimizer = n_restarts_optimizer
@@ -382,11 +360,27 @@ def __init__(
382360
self.verbosity = verbosity
383361
self.auto_optimize = auto_optimize
384362
self.max_points = max_points
363+
self.seed = seed
364+
365+
# Attributes set during fit
366+
self.m = None
367+
self.n = None
368+
self.X_ = None
369+
self.y_ = None
370+
self.dk = None # derivative flag
371+
self.K = None
372+
self.Ki = None
373+
self.Kiy = None
374+
self.phi = None
375+
self.dK = None
376+
self.DK = None
377+
self.ldetK = None
378+
379+
# Internal flag to check if fitted
380+
self._is_fitted = False
385381

386382
# need to store the initial parameters for the fit method (sklearn compatibility)
387383
self.init_params = {
388-
"X": X,
389-
"y": y,
390384
"d": d,
391385
"g": g,
392386
"nlsep_method": nlsep_method,
@@ -397,6 +391,7 @@ def __init__(
397391
"verbosity": verbosity,
398392
"auto_optimize": auto_optimize,
399393
"max_points": max_points,
394+
"seed": seed,
400395
}
401396

402397
# Add these two methods required by scikit-learn
@@ -413,8 +408,6 @@ def get_params(self, deep=True):
413408
dict: Parameter names mapped to their values.
414409
"""
415410
return {
416-
"X": self.X,
417-
"y": self.y,
418411
"d": self.d,
419412
"g": self.g,
420413
"nlsep_method": self.nlsep_method,
@@ -425,6 +418,7 @@ def get_params(self, deep=True):
425418
"verbosity": self.verbosity,
426419
"auto_optimize": self.auto_optimize,
427420
"max_points": self.max_points,
421+
"seed": self.seed,
428422
}
429423

430424
def set_params(self, **parameters):
@@ -450,8 +444,8 @@ def fit(self, X: np.ndarray, y: np.ndarray, d=None, g=None, dK: bool = True, aut
450444
"""Fit the GP model with training data and optionally auto-optimize hyperparameters.
451445
452446
Args:
453-
X: The input data matrix of shape (n, m).
454-
y: The output data vector of length n.
447+
X: array-like of shape (n_samples, n_features)
448+
y: array-like of shape (n_samples,)
455449
d: The length-scale parameters. If None, will be determined
456450
automatically. Defaults to None.
457451
g: The nugget parameter. If None, will be determined automatically.
@@ -475,10 +469,12 @@ def fit(self, X: np.ndarray, y: np.ndarray, d=None, g=None, dK: bool = True, aut
475469
if hasattr(y, "to_numpy"):
476470
y = y.to_numpy()
477471
y = y.reshape(-1, 1)
478-
print(f"X shape: {X.shape}, y shape: {y.shape}")
472+
if verbosity > 0:
473+
print(f"X shape: {X.shape}, y shape: {y.shape}")
479474
if self.max_points is not None:
480475
X, y = select_distant_points(X, y, self.max_points)
481-
print(f"Selected {self.max_points} points for the model.")
476+
if verbosity > 0:
477+
print(f"Selected {self.max_points} points for the model.")
482478
if auto_optimize is None:
483479
auto_optimize = self.auto_optimize
484480
n, m = X.shape
@@ -600,7 +596,9 @@ def objective(par):
600596
def gradient(par):
601597
return gradnlsep(par, X, y, self.gradnlsep_method)
602598

603-
result = run_minimize_with_restarts(objective=objective, gradient=gradient, x0=p, bounds=bounds, n_restarts_optimizer=self.n_restarts_optimizer, maxit=self.maxit, verb=self.verbosity)
599+
result = run_minimize_with_restarts(
600+
objective=objective, gradient=gradient, x0=p, bounds=bounds, n_restarts_optimizer=self.n_restarts_optimizer, maxit=self.maxit, verb=self.verbosity, random_state=self.seed
601+
)
604602

605603
d = result.x[:-1]
606604
g = result.x[-1]
@@ -611,7 +609,7 @@ def gradient(par):
611609
print(f"result: {result}")
612610
print(f"Optimized d: {d}, g: {g}")
613611
print(f"Updated d: {self.d}, g: {self.g}")
614-
self.build()
612+
self._build()
615613
new_theta = np.concatenate((self.get_d(), [self.get_g()]))
616614
if np.sqrt(np.mean((result.x - new_theta) ** 2)) > np.sqrt(np.finfo(float).eps):
617615
warnings.warn("stored theta not the same as theta-hat", RuntimeWarning)
@@ -622,10 +620,12 @@ def gradient(par):
622620
print(f"Optimized nugget (g): {self.get_g()}")
623621
print(f"Message: {result['msg']}")
624622
print(f"Iterations: {result['its']}")
623+
self._is_fitted = True
625624
return self
626625
else:
627626
# No optimization, just build the model with roughly estimated parameters using darg and garg
628-
self.build()
627+
self._build()
628+
self._is_fitted = True
629629
return self
630630
else:
631631
# Original behavior for explicitly provided parameters
@@ -634,7 +634,8 @@ def gradient(par):
634634
if len(self.d) != m:
635635
raise ValueError(f"Length of d ({len(self.d)}) does not match ncol(X) ({m})")
636636
self.g = g
637-
self.build()
637+
self._build()
638+
self._is_fitted = True
638639
return self
639640

640641
def calc_ytKiy(self) -> None:
@@ -656,7 +657,7 @@ def calc_ytKiy(self) -> None:
656657
self.phi = phi[0, 0]
657658
self.Kiy = Kiy
658659

659-
def build(self) -> None:
660+
def _build(self) -> None:
660661
"""
661662
Completes all correlation calculations after data is defined.
662663
"""
@@ -674,11 +675,15 @@ def build(self) -> None:
674675
# # raise RuntimeError("dK calculations have already been initialized.")
675676
# self.DK = diff_covar_sep_symm(self.m, self.X, self.n, self.d, self.K)
676677

677-
def predict(self, XX: np.ndarray, lite: bool = False, nonug: bool = False, return_full=False, return_std=False) -> float:
678+
def _check_is_fitted(self):
679+
if not self._is_fitted:
680+
raise ValueError("This GPsep instance is not fitted yet. Call 'fit' with " "appropriate arguments before using 'predict'.")
681+
682+
def predict(self, X: np.ndarray, lite: bool = False, nonug: bool = False, return_full=False, return_std=False) -> float:
678683
"""Predict the Gaussian Process output at new input points.
679684
680685
Args:
681-
XX: The predictive locations.
686+
X: The predictive locations.
682687
lite: Flag to indicate whether to compute only the diagonal
683688
of Sigma. Defaults to False.
684689
nonug: Flag to indicate whether to exclude nugget.
@@ -724,54 +729,55 @@ def predict(self, XX: np.ndarray, lite: bool = False, nonug: bool = False, retur
724729
plt.title("Predictive Distribution")
725730
plt.show()
726731
"""
727-
# if XX is a pandas dataframe, convert it to a numpy array
728-
if hasattr(XX, "to_numpy"):
729-
XX = XX.to_numpy()
732+
self._check_is_fitted()
733+
# if X is a pandas dataframe, convert it to a numpy array
734+
if hasattr(X, "to_numpy"):
735+
X = X.to_numpy()
730736
if lite:
731-
res = self._predict_lite(XX, nonug)
737+
res = self._predict_lite(X, nonug)
732738
if return_full:
733739
return res
734740
elif return_std:
735741
return (res["mean"], res["s2"])
736742
else:
737743
return res["mean"]
738744
else:
739-
res = self._predict_full(XX, nonug)
745+
res = self._predict_full(X, nonug)
740746
if return_full:
741747
return res
742748
elif return_std:
743749
return (res["mean"], res["Sigma"])
744750
else:
745751
return res["mean"]
746752

747-
def _predict_lite(self, XX: np.ndarray, nonug: bool) -> dict:
753+
def _predict_lite(self, X: np.ndarray, nonug: bool) -> dict:
748754
"""
749755
Predict only the diagonal of Sigma—optimized for speed.
750756
751757
Args:
752-
XX (np.ndarray): The predictive locations.
758+
X (np.ndarray): The predictive locations.
753759
nonug (bool): Flag to indicate whether to use nugget.
754760
755761
Returns:
756762
dict: A dictionary containing the mean, s2, df, and llik.
757763
"""
758-
nn = XX.shape[0]
759-
m = XX.shape[1]
760-
mean_out, s2_out, df_out, llik_out = predGPsep_lite(self, m, nn, XX, lite_in=True, nonug_in=nonug)
764+
nn = X.shape[0]
765+
m = X.shape[1]
766+
mean_out, s2_out, df_out, llik_out = predGPsep_lite(self, m, nn, X, lite_in=True, nonug_in=nonug)
761767
return {"mean": mean_out, "s2": s2_out, "df": df_out, "llik": llik_out}
762768

763-
def _predict_full(self, XX: np.ndarray, nonug: bool) -> dict:
769+
def _predict_full(self, X: np.ndarray, nonug: bool) -> dict:
764770
"""
765771
Compute full predictive covariance matrix.
766772
767773
Args:
768-
XX (np.ndarray): The predictive locations.
774+
X (np.ndarray): The predictive locations.
769775
nonug (bool): Flag to indicate whether to use nugget.
770776
771777
Returns:
772778
dict: A dictionary containing the mean, Sigma, df, and llik.
773779
"""
774-
nn, m = XX.shape
780+
nn, m = X.shape
775781
if m != self.m:
776782
raise ValueError(f"ncol(X)={m} does not match GPsep model ({self.m})")
777783

@@ -785,8 +791,8 @@ def _predict_full(self, XX: np.ndarray, nonug: bool) -> dict:
785791
df[0] = float(n)
786792
phidf = self.phi / df[0]
787793
llik[0] = -0.5 * (df[0] * np.log(0.5 * self.phi) + self.ldetK)
788-
k = covar_sep(self.m, self.X, n, XX, nn, self.d, 0.0)
789-
Sigma[...] = covar_sep_symm(self.m, XX, nn, self.d, g)
794+
k = covar_sep(self.m, self.X, n, X, nn, self.d, 0.0)
795+
Sigma[...] = covar_sep_symm(self.m, X, nn, self.d, g)
790796
ktKi = np.dot(k.T, self.Ki)
791797
mean[:] = np.dot(ktKi, self.y).reshape(-1)
792798
Sigma[...] = phidf * (Sigma - np.dot(ktKi, k))
@@ -880,7 +886,7 @@ def gradient(par):
880886
# set new parameters and build
881887
self.set_new_params(d, g)
882888
print(f"Updated d: {self.d}, g: {self.g}")
883-
self.build()
889+
self._build()
884890
return {"parameters": result.x, "iterations": result.nit, "convergence": result.status, "message": result.message}
885891

886892

src/spotpython/utils/optimize.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
from scipy.optimize import minimize
33

44

5-
def run_minimize_with_restarts(objective, gradient, x0, bounds, n_restarts_optimizer=5, method="L-BFGS-B", maxit=100, verb=0):
5+
def run_minimize_with_restarts(objective, gradient, x0, bounds, n_restarts_optimizer=5, method="L-BFGS-B", maxit=100, verb=0, random_state=None):
66
"""
77
Runs multiple restarts of the minimize() function and returns the best found result.
88
@@ -15,10 +15,14 @@ def run_minimize_with_restarts(objective, gradient, x0, bounds, n_restarts_optim
1515
method (str): Optimization method. Default "L-BFGS-B".
1616
maxit (int): Max iterations.
1717
verb (int): Verbosity level.
18+
random_state (int, optional): Seed for the random-number generator to ensure reproducibility.
1819
1920
Returns:
2021
OptimizeResult: The best optimization result among all restarts.
2122
"""
23+
if random_state is not None:
24+
np.random.seed(random_state)
25+
2226
best_result = None
2327
best_fun = float("inf")
2428

0 commit comments

Comments
 (0)