@@ -80,6 +80,10 @@ class GPOpt:
80
80
81
81
acquisition: a string;
82
82
acquisition function: "ei" (expected improvement) or "ucb" (upper confidence bound)
83
+
84
+ method: an str;
85
+ "bayesian" (default) for Gaussian posteriors, "mc" for Monte Carlo posteriors,
86
+ "splitconformal" for conformalized surrogates
83
87
84
88
min_value: a float;
85
89
minimum value of the objective function (default is None). For example,
@@ -114,6 +118,7 @@ def __init__(
114
118
save = None ,
115
119
n_jobs = None ,
116
120
acquisition = "ei" ,
121
+ method = "bayesian" ,
117
122
min_value = None ,
118
123
per_second = False , # /!\ very experimental
119
124
log_scale = False , # /!\ experimental
@@ -145,8 +150,10 @@ def __init__(
145
150
self .y_min = None
146
151
self .y_mean = None
147
152
self .y_std = None
153
+ self .y_lower = None
154
+ self .y_upper = None
148
155
self .best_surrogate = None
149
- self .acquisition = acquisition
156
+ self .acquisition = acquisition
150
157
self .min_value = min_value
151
158
self .acq = np .array ([])
152
159
self .max_acq = []
@@ -160,7 +167,12 @@ def __init__(
160
167
)
161
168
else :
162
169
self .surrogate_obj = surrogate_obj
163
- self .method = None
170
+ assert method in (
171
+ "bayesian" ,
172
+ "mc" ,
173
+ "splitconformal"
174
+ ), "method must be in ('bayesian', 'mc', 'splitconformal')"
175
+ self .method = method
164
176
self .posterior_ = None
165
177
166
178
# Sobol seqs for initial design and choices
@@ -299,23 +311,42 @@ def surrogate_fit_predict(
299
311
assert (
300
312
return_std == True and return_pi == True
301
313
) == False , "must have either return_std == True or return_pi == True"
314
+
302
315
if return_std == True :
316
+
303
317
self .posterior_ = "gaussian"
304
318
return self .surrogate_obj .fit (X_train , y_train ).predict (
305
319
X_test , return_std = True
306
320
)
307
- elif return_pi == True :
308
- self .posterior_ = "mc"
309
- res = self .surrogate_obj .fit (X_train , y_train ).predict (
310
- X_test , return_pi = True , method = "splitconformal"
311
- )
312
- self .y_sims = res .sims
313
- self .y_mean , self .y_std = (
314
- np .mean (self .y_sims , axis = 1 ),
315
- np .std (self .y_sims , axis = 1 ),
316
- )
317
- return self .y_mean , self .y_std , self .y_sims
321
+
322
+ elif return_pi == True : # here, self.surrogate_obj must have `replications` not None
323
+
324
+ if self .surrogate_obj .replications is not None :
325
+
326
+ self .posterior_ = "mc"
327
+ res = self .surrogate_obj .fit (X_train , y_train ).predict (
328
+ X_test , return_pi = True , method = "splitconformal"
329
+ )
330
+ self .y_sims = res .sims
331
+ self .y_mean , self .y_std = (
332
+ np .mean (self .y_sims , axis = 1 ),
333
+ np .std (self .y_sims , axis = 1 ),
334
+ )
335
+ return self .y_mean , self .y_std , self .y_sims
336
+
337
+ else : # self.surrogate_obj is conformalized (uses nnetsauce.PredictionInterval)
338
+
339
+ assert self .acquisition == "ucb" , "'acquisition' must be 'ucb' for conformalized surrogates"
340
+ self .posterior_ = None
341
+ res = self .surrogate_obj .fit (X_train , y_train ).predict (
342
+ X_test , return_pi = True )
343
+ self .y_mean = res .mean
344
+ self .y_lower = res .lower
345
+ self .y_upper = res .upper
346
+ return self .y_mean , self .y_lower , self .y_upper
347
+
318
348
else :
349
+
319
350
raise NotImplementedError
320
351
321
352
# fit predict timings
@@ -332,6 +363,7 @@ def timings_fit_predict(self, X_train, y_train, X_test):
332
363
def next_parameter_by_acq (self , i , acq = "ei" ):
333
364
334
365
if acq == "ei" :
366
+
335
367
if self .posterior_ == "gaussian" :
336
368
gamma_hat = (self .y_min - self .y_mean ) / self .y_std
337
369
self .acq = - self .y_std * (
@@ -343,7 +375,15 @@ def next_parameter_by_acq(self, i, acq="ei"):
343
375
)
344
376
345
377
if acq == "ucb" :
346
- self .acq = - (self .y_mean - 1.96 * self .y_std )
378
+
379
+ if self .posterior_ == "gaussian" :
380
+
381
+ self .acq = (self .y_mean - 1.96 * self .y_std )
382
+
383
+ elif self .posterior_ is None : # split conformal(ized) estimator
384
+
385
+ self .acq = self .y_lower
386
+
347
387
348
388
# find max index -----
349
389
@@ -404,8 +444,7 @@ def optimize(
404
444
n_more_iter = None ,
405
445
abs_tol = None , # suggested 1e-4, for n_iter = 200
406
446
min_budget = 50 , # minimum budget for early stopping
407
- func_args = None ,
408
- method = "bayesian" ,
447
+ func_args = None ,
409
448
):
410
449
"""Launch optimization loop.
411
450
@@ -426,22 +465,13 @@ def optimize(
426
465
minimum number of iterations before early stopping controlled by `abs_tol`
427
466
428
467
func_args: a list;
429
- additional parameters for the objective function (if necessary)
430
-
431
- method: an str;
432
- "bayesian" (default) for Gaussian posteriors or "mc" for Monte Carlo posteriors
468
+ additional parameters for the objective function (if necessary)
433
469
434
470
see also [Bayesian Optimization with GPopt](https://thierrymoudiki.github.io/blog/2021/04/16/python/misc/gpopt)
435
471
and [Hyperparameters tuning with GPopt](https://thierrymoudiki.github.io/blog/2021/06/11/python/misc/hyperparam-tuning-gpopt)
436
472
437
473
"""
438
474
439
- assert method in (
440
- "bayesian" ,
441
- "mc" ,
442
- ), "method must be in ('bayesian', 'mc')"
443
- self .method = method
444
-
445
475
# verbose = 0: nothing is printed
446
476
# verbose = 1: a progress bar is printed (longer than 0)
447
477
# verbose = 2: information about each iteration is printed (longer than 1)
@@ -554,7 +584,7 @@ def optimize(
554
584
555
585
# current gp mean and std on initial design
556
586
# /!\ if GP
557
- if self .method == "bayesian" :
587
+ if self .method == "bayesian" :
558
588
self .posterior_ = "gaussian"
559
589
try :
560
590
y_mean , y_std = self .surrogate_fit_predict (
@@ -573,12 +603,17 @@ def optimize(
573
603
return_pi = False ,
574
604
)
575
605
y_mean , y_std = preds_with_std [0 ], preds_with_std [1 ]
606
+ self .y_mean = y_mean
607
+ self .y_std = np .maximum (2.220446049250313e-16 , y_std )
608
+
576
609
577
610
elif self .method == "mc" :
578
611
self .posterior_ = "mc"
579
612
assert self .surrogate_obj .__class__ .__name__ .startswith (
580
613
"CustomRegressor"
581
- ), "for `method = 'mc'`, the surrogate must be a nnetsauce.CustomRegressor()"
614
+ ) or self .surrogate_obj .__class__ .__name__ .startswith (
615
+ "PredictionInterval"
616
+ ), "for `method = 'mc'`, the surrogate must be a nnetsauce.CustomRegressor() or nnetsauce.PredictionInterval()"
582
617
assert (
583
618
self .surrogate_obj .replications is not None
584
619
), "for `method = 'mc'`, the surrogate must be a nnetsauce.CustomRegressor() with a number of 'replications' provided"
@@ -590,9 +625,23 @@ def optimize(
590
625
return_pi = True ,
591
626
)
592
627
y_mean , y_std = preds_with_std [0 ], preds_with_std [1 ]
593
-
594
- self .y_mean = y_mean
595
- self .y_std = np .maximum (2.220446049250313e-16 , y_std )
628
+ self .y_mean = y_mean
629
+ self .y_std = np .maximum (2.220446049250313e-16 , y_std )
630
+
631
+ elif self .method == "splitconformal" :
632
+ self .posterior_ = None
633
+ assert self .surrogate_obj .__class__ .__name__ .startswith (
634
+ "PredictionInterval"
635
+ ), "for `method = 'splitconformal'`, the surrogate must be a nnetsauce.PredictionInterval()"
636
+ preds_with_pi = self .surrogate_fit_predict (
637
+ np .asarray (self .parameters ),
638
+ np .asarray (self .scores ),
639
+ self .x_choices ,
640
+ return_std = False ,
641
+ return_pi = True ,
642
+ )
643
+ y_lower = preds_with_pi [1 ]
644
+ self .lower = y_lower
596
645
597
646
# saving after initial design computation
598
647
if self .save is not None :
@@ -631,8 +680,8 @@ def optimize(
631
680
632
681
for i in range (n_iter ):
633
682
634
- # find next set of parameters (vector), maximizing ei
635
- next_param = self .next_parameter_by_acq (i = i , acq = "ei" )
683
+ # find next set of parameters (vector), maximizing acquisition function
684
+ next_param = self .next_parameter_by_acq (i = i , acq = self . acquisition )
636
685
637
686
try :
638
687
@@ -744,16 +793,17 @@ def optimize(
744
793
)
745
794
)
746
795
747
- elif self .posterior_ == "mc" and self .method == "mc" :
748
- self .y_mean , self .y_std , self .y_sims = (
796
+ elif self .posterior_ in ( None , "mc" ) and self .method in ( "mc" , "splitconformal" ) :
797
+ self .y_mean , self .y_lower , self .y_upper = (
749
798
self .surrogate_fit_predict (
750
799
np .asarray (self .parameters ),
751
800
np .asarray (self .scores ),
752
801
self .x_choices ,
753
802
return_std = False ,
754
803
return_pi = True ,
755
804
)
756
- )
805
+ )
806
+
757
807
else :
758
808
return NotImplementedError
759
809
@@ -808,9 +858,8 @@ def lazyoptimize(
808
858
abs_tol = None , # suggested 1e-4, for n_iter = 200
809
859
min_budget = 50 , # minimum budget for early stopping
810
860
func_args = None ,
811
- method = "bayesian" , # "bayesian" or "mc
812
861
estimators = "all" ,
813
- type_pi = "kde" , # for now, 'kde' or 'bootstrap'
862
+ type_pi = "kde" , # for now, 'kde', 'bootstrap', 'splitconformal '
814
863
type_exec = "independent" , # "queue" or "independent" (default)
815
864
):
816
865
"""Launch optimization loop.
@@ -834,15 +883,12 @@ def lazyoptimize(
834
883
func_args: a list;
835
884
additional parameters for the objective function (if necessary)
836
885
837
- method: an str;
838
- "bayesian" (default) for Gaussian posteriors or "mc" for Monte Carlo posteriors
839
-
840
886
estimators: an str or a list of strs (estimators names)
841
887
if "all", then 30 models are fitted. Otherwise, only those provided in the list
842
888
are adjusted; for example ["RandomForestRegressor", "Ridge"]
843
889
844
890
type_pi: an str;
845
- "kde" (default) or "bootstrap "; type of prediction intervals for the surrogate
891
+ "kde" (default) or, "splitconformal "; type of prediction intervals for the surrogate
846
892
model
847
893
848
894
type_exec: an str;
@@ -859,20 +905,40 @@ def lazyoptimize(
859
905
860
906
else :
861
907
862
- self .regressors = [
863
- (
864
- "CustomRegressor(" + est [0 ] + ")" ,
865
- ns .CustomRegressor (
866
- est [1 ](), replications = 150 , type_pi = type_pi
867
- ),
868
- )
869
- for est in all_estimators ()
870
- if (
871
- issubclass (est [1 ], RegressorMixin )
872
- and (est [0 ] not in REMOVED_REGRESSORS )
873
- and (est [0 ] in estimators )
874
- )
875
- ]
908
+ if type_pi == "kde" :
909
+
910
+ self .regressors = [
911
+ (
912
+ "CustomRegressor(" + est [0 ] + ")" ,
913
+ ns .CustomRegressor (
914
+ est [1 ](), replications = 150 , type_pi = type_pi
915
+ ),
916
+ )
917
+ for est in all_estimators ()
918
+ if (
919
+ issubclass (est [1 ], RegressorMixin )
920
+ and (est [0 ] not in REMOVED_REGRESSORS )
921
+ and (est [0 ] in estimators )
922
+ )
923
+ ]
924
+
925
+ elif type_pi == "splitconformal" :
926
+
927
+ self .regressors = [
928
+ (
929
+ est [0 ],
930
+ ns .PredictionInterval (
931
+ est [1 ](),
932
+ type_pi = "splitconformal"
933
+ ),
934
+ )
935
+ for est in all_estimators ()
936
+ if (
937
+ issubclass (est [1 ], RegressorMixin )
938
+ and (est [0 ] not in REMOVED_REGRESSORS )
939
+ and (est [0 ] in estimators )
940
+ )
941
+ ]
876
942
877
943
self .surrogate_fit_predict = partial (
878
944
self .surrogate_fit_predict , return_pi = True
@@ -908,6 +974,7 @@ def lazyoptimize(
908
974
seed = self .seed ,
909
975
n_jobs = self .n_jobs ,
910
976
acquisition = self .acquisition ,
977
+ method = self .method ,
911
978
min_value = self .min_value ,
912
979
surrogate_obj = copy .deepcopy (self .regressors [0 ][1 ]),
913
980
)
@@ -917,7 +984,6 @@ def lazyoptimize(
917
984
abs_tol = abs_tol , # suggested 1e-4, for n_iter = 200
918
985
min_budget = min_budget , # minimum budget for early stopping
919
986
func_args = func_args ,
920
- method = method ,
921
987
)
922
988
923
989
score_next_param = gp_opt_obj_prev .y_min
@@ -944,6 +1010,7 @@ def lazyoptimize(
944
1010
seed = self .seed ,
945
1011
n_jobs = self .n_jobs ,
946
1012
acquisition = self .acquisition ,
1013
+ method = self .method ,
947
1014
min_value = self .min_value ,
948
1015
surrogate_obj = copy .deepcopy (self .regressors [i ][1 ]),
949
1016
x_init = np .asarray (gp_opt_obj_prev .parameters ),
@@ -955,7 +1022,6 @@ def lazyoptimize(
955
1022
abs_tol = abs_tol , # suggested 1e-4, for n_iter = 200
956
1023
min_budget = min_budget , # minimum budget for early stopping
957
1024
func_args = func_args ,
958
- method = method ,
959
1025
)
960
1026
961
1027
score_next_param = gp_opt_obj .y_min
@@ -1030,6 +1096,7 @@ def lazyoptimize(
1030
1096
seed = self .seed ,
1031
1097
n_jobs = self .n_jobs ,
1032
1098
acquisition = self .acquisition ,
1099
+ method = self .method ,
1033
1100
min_value = self .min_value ,
1034
1101
surrogate_obj = copy .deepcopy (self .regressors [i ][1 ]),
1035
1102
)
@@ -1039,7 +1106,6 @@ def lazyoptimize(
1039
1106
abs_tol = abs_tol , # suggested 1e-4, for n_iter = 200
1040
1107
min_budget = min_budget , # minimum budget for early stopping
1041
1108
func_args = func_args ,
1042
- method = method ,
1043
1109
)
1044
1110
1045
1111
score_next_param = gp_opt_obj .y_min
@@ -1080,6 +1146,7 @@ def foo(i):
1080
1146
seed = self .seed ,
1081
1147
n_jobs = self .n_jobs ,
1082
1148
acquisition = self .acquisition ,
1149
+ method = self .method ,
1083
1150
min_value = self .min_value ,
1084
1151
surrogate_obj = copy .deepcopy (self .regressors [i ][1 ]),
1085
1152
)
@@ -1090,7 +1157,6 @@ def foo(i):
1090
1157
abs_tol = abs_tol , # suggested 1e-4, for n_iter = 200
1091
1158
min_budget = min_budget , # minimum budget for early stopping
1092
1159
func_args = func_args ,
1093
- method = method ,
1094
1160
)
1095
1161
1096
1162
return gp_opt_obj
0 commit comments