SKSurrogate 0.1.0__py3-none-any.whl

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.
SKSurrogate/aml.py ADDED
@@ -0,0 +1,1156 @@
1
+ """
2
+ Optimized Pipeline Detector
3
+ ===============================
4
+
5
+ """
6
+ from sklearn.base import BaseEstimator, TransformerMixin
7
+ from typing import Any
8
+
9
+
10
+ class StackingEstimator(BaseEstimator, TransformerMixin):
11
+ """
12
+ Meta-transformer for adding predictions and/or class probabilities as synthetic feature(s).
13
+
14
+ :param estimator: object with fit, predict, and predict_proba methods. The estimator to generate
15
+ synthetic features from.
16
+ :param res: True (default), stacks the final result of estimator
17
+ :param probs: True (default), stacks probabilities calculated by estimator
18
+ :param decision: True (default), stacks the result of decision function of the estimator
19
+ :param cv: cross-validation splitter used to create out-of-fold training features; default=5
20
+ :param n_jobs: number of parallel jobs used for out-of-fold predictions; default=None
21
+ """
22
+
23
+ def __init__(self, estimator, res=True, probs=True, decision=True, cv=5, n_jobs=None):
24
+
25
+ self.estimator = estimator
26
+ self.res = res
27
+ self.probs = probs
28
+ self.decision = decision
29
+ self.cv = cv
30
+ self.n_jobs = n_jobs
31
+
32
+ def set_params(self, **params):
33
+ """
34
+ Sets the sklearn related parameters for the estimator
35
+
36
+ :param params: parameters to be bassed to the estimator
37
+ :return: `self`
38
+ """
39
+ estimator_params = {}
40
+ for key, value in params.items():
41
+ if key.startswith("estimator__"):
42
+ estimator_params[key[len("estimator__"):]] = value
43
+ elif key in {"estimator", "res", "probs", "decision"}:
44
+ setattr(self, key, value)
45
+ if estimator_params:
46
+ self.estimator.set_params(**estimator_params)
47
+ return self
48
+
49
+ def fit(self, X, y=None, **fit_params):
50
+ """
51
+ Fit the StackingEstimator meta-transformer.
52
+
53
+ :param X: array-like of shape (n_samples, n_features). The training input samples.
54
+ :param y: array-like, shape (n_samples,). The target values (integers that correspond to classes
55
+ in classification, real numbers in regression).
56
+ :param fit_params: Other estimator-specific parameters.
57
+ :return: self, object. Returns a copy of the estimator
58
+ """
59
+ self.estimator.fit(X, y, **fit_params)
60
+ return self
61
+
62
+ def transform(self, X):
63
+ """
64
+ Transform data by adding two synthetic feature(s).
65
+
66
+ :param X: numpy ndarray, {n_samples, n_components}. New data, where n_samples is the number of
67
+ samples and n_components is the number of components.
68
+ :return: X_transformed: array-like, shape (n_samples, n_features + 1) or
69
+ (n_samples, n_features + 1 + n_classes) for classifier with predict_proba attribute;
70
+ The transformed feature set.
71
+ """
72
+ import numpy as np
73
+ from sklearn.utils import check_array
74
+
75
+ X = check_array(X)
76
+ features = [X]
77
+ # add class probabilities as a synthetic feature
78
+ if self.probs and hasattr(self.estimator, "predict_proba"):
79
+ features.append(self.estimator.predict_proba(X))
80
+
81
+ # add class decision_function as a synthetic feature
82
+ if self.decision and hasattr(self.estimator, "decision_function"):
83
+ features.append(np.reshape(self.estimator.decision_function(X), (-1, 1)))
84
+
85
+ # add class prediction as a synthetic feature
86
+ if self.res:
87
+ features.append(np.reshape(self.estimator.predict(X), (-1, 1)))
88
+
89
+ return np.hstack(features)
90
+
91
+ def fit_transform(self, X, y=None, **fit_params):
92
+ """Fit the estimator and return leakage-safe out-of-fold features.
93
+
94
+ During pipeline training, predictions are generated by cloned estimators
95
+ on folds that did not contain each sample. The wrapped estimator is then
96
+ fitted on all training data for use by :meth:`transform` on later data.
97
+ """
98
+ import numpy as np
99
+ from sklearn.base import clone
100
+ from sklearn.model_selection import cross_val_predict
101
+
102
+ if y is None or self.cv is None:
103
+ return self.fit(X, y, **fit_params).transform(X)
104
+
105
+ predictions = {}
106
+ if self.probs and hasattr(self.estimator, "predict_proba"):
107
+ predictions["probs"] = cross_val_predict(
108
+ clone(self.estimator),
109
+ X,
110
+ y,
111
+ cv=self.cv,
112
+ method="predict_proba",
113
+ n_jobs=self.n_jobs,
114
+ )
115
+ if self.decision and hasattr(self.estimator, "decision_function"):
116
+ predictions["decision"] = cross_val_predict(
117
+ clone(self.estimator),
118
+ X,
119
+ y,
120
+ cv=self.cv,
121
+ method="decision_function",
122
+ n_jobs=self.n_jobs,
123
+ )
124
+ if self.res:
125
+ predictions["res"] = cross_val_predict(
126
+ clone(self.estimator),
127
+ X,
128
+ y,
129
+ cv=self.cv,
130
+ method="predict",
131
+ n_jobs=self.n_jobs,
132
+ )
133
+
134
+ self.fit(X, y, **fit_params)
135
+ features = [np.asarray(X)]
136
+ if self.probs and "probs" in predictions:
137
+ features.append(predictions["probs"])
138
+ if self.decision and "decision" in predictions:
139
+ decision = np.asarray(predictions["decision"])
140
+ features.append(np.reshape(decision, (-1, 1)) if decision.ndim == 1 else decision)
141
+ if self.res:
142
+ result = np.asarray(predictions["res"])
143
+ features.append(np.reshape(result, (-1, 1)))
144
+ return np.hstack(features)
145
+
146
+
147
+ class Words(object):
148
+ """
149
+ This class takes a set as alphabet and generates words of a given length accordingly.
150
+ A `Words` instant accepts the following parameters:
151
+
152
+ :param letters: is a set of letters (symbols) to make up the words
153
+ :param last: a subset of `letters` that are allowed to appear at the end of a word
154
+ :param first: a set of words that can only appear at the beginning of a word
155
+ :param repeat: whether consecutive occurrence of a letter is allowed
156
+ """
157
+
158
+ def __init__(self, letters, last=None, first=None, repeat=False):
159
+ self.letters = letters
160
+ self.last = last
161
+ self.first = first
162
+ self.words = []
163
+ self.repeat = repeat
164
+
165
+ def _check_cons_repeat(self, o):
166
+ lng = len(o)
167
+ for i in range(1, lng):
168
+ if self.repeat:
169
+ return True
170
+ if o[i - 1] == o[i]:
171
+ return False
172
+ return True
173
+
174
+ def _check_mid_first(self, o):
175
+ if (self.first is None) or (self.first == []):
176
+ return True
177
+ n_ = len(o)
178
+ for i in range(1, n_):
179
+ if o[i] in self.first:
180
+ return False
181
+ return True
182
+
183
+ def Generate(self, l):
184
+ """
185
+ Generates the set of legitimate words of length `l`
186
+
187
+ :param l: int, the length of words
188
+ :return: set of all legitimate words of length `l`
189
+ """
190
+ from itertools import product
191
+
192
+ words = []
193
+ for o in product(self.letters, repeat=l):
194
+ if self.last is not None:
195
+ if o[-1] in self.last:
196
+ if self._check_cons_repeat(o) and self._check_mid_first(o):
197
+ words.append(o)
198
+ else:
199
+ if self._check_cons_repeat(o) and self._check_mid_first(o):
200
+ words.append(o)
201
+ return words
202
+
203
+
204
+ try:
205
+ from .structsearch import Real, Integer, Categorical, HDReal
206
+ except ModuleNotFoundError:
207
+ Real = lambda a, b: None
208
+ Integer = lambda a, b: None
209
+ Categorical = lambda a: None
210
+ HDReal = lambda a, b: None
211
+ except ImportError:
212
+ Real = lambda a, b: None
213
+ Integer = lambda a, b: None
214
+ Categorical = lambda a: None
215
+ HDReal = lambda a, b: None
216
+
217
+ default_config = {
218
+ # Classifiers
219
+ "sklearn.naive_bayes.BernoulliNB": {
220
+ "alpha": Real(10.0e-5, 100.0),
221
+ "fit_prior": Categorical([True, False]),
222
+ },
223
+ "sklearn.naive_bayes.GaussianNB": {"var_smoothing": Real(1.0e-9, 2.0e-1)},
224
+ "sklearn.tree.DecisionTreeClassifier": {
225
+ "criterion": Categorical(["gini", "entropy"]),
226
+ "splitter": Categorical(["best", "random"]),
227
+ "min_samples_split": Integer(2, 10),
228
+ "min_samples_leaf": Integer(1, 10),
229
+ "class_weight": HDReal((1.0e-5, 1.0e-5), (20.0, 20.0)),
230
+ },
231
+ "sklearn.linear_model.LogisticRegression": {
232
+ "penalty": Categorical(["l1", "l2"]),
233
+ "C": Real(1.0e-6, 10.0),
234
+ "class_weight": HDReal((1.0e-5, 1.0e-5), (20.0, 20.0))
235
+ # 'dual': Categorical([True, False])
236
+ },
237
+ "sklearn.svm.SVC": {
238
+ "C": Real(1e-6, 20.0),
239
+ "gamma": Real(1e-6, 10.0),
240
+ "tol": Real(1e-6, 10.0),
241
+ "class_weight": HDReal((1.0e-5, 1.0e-5), (20.0, 20.0)),
242
+ },
243
+ "sklearn.ensemble.RandomForestClassifier": {
244
+ "n_estimators": Integer(10, 200),
245
+ "criterion": Categorical(["gini", "entropy"]),
246
+ "min_samples_split": Integer(2, 10),
247
+ "min_samples_leaf": Integer(1, 10),
248
+ "class_weight": HDReal((1.0e-5, 1.0e-5), (20.0, 20.0)),
249
+ },
250
+ "sklearn.ensemble.GradientBoostingClassifier": {
251
+ "loss": Categorical(["log_loss", "exponential"]),
252
+ "learning_rate": Real(1.0e-6, 1.0 - 1.0e-6),
253
+ "n_estimators": Integer(10, 500),
254
+ "subsample": Real(1.0e-6, 1.0),
255
+ "criterion": Categorical(["friedman_mse", "squared_error"]),
256
+ # "min_samples_split": Integer(2, 20),
257
+ # "min_samples_leaf": Integer(1, 20),
258
+ # "min_weight_fraction_leaf": Real(0., .5),
259
+ # "max_depth": Integer(2, 20),
260
+ # "min_impurity_decrease": Real(0., 10.),
261
+ # "min_impurity_split": Real(0., 10.),
262
+ # "max_features": Categorical(['auto', 'sqrt', 'log2', None]),
263
+ "tol": Real(1.0e-6, 0.1),
264
+ },
265
+ "lightgbm.LGBMClassifier": {
266
+ "boosting_type": Categorical(["gbdt", "dart", "goss", "rf"]),
267
+ "num_leaves": Integer(2, 100),
268
+ "learning_rate": Real(1.0e-7, 1.0 - 1.0e-6),
269
+ "n_estimators": Integer(5, 250),
270
+ "min_split_gain": Real(0.0, 1.0),
271
+ # "min_child_weight": Real(1.e-6, 1.),
272
+ # "min_child_samples": Integer(5, 200),
273
+ "subsample": Real(1.0e-6, 1.0),
274
+ # "subsample_freq": Integer(0, 10),
275
+ # "colsample_bytree": Real(1.e-5, 1.),
276
+ # "class_weight": HDReal((1.e-5, 1.e-5), (20., 20.)),
277
+ # "reg_alpha": Real(0., 10.),
278
+ # "reg_lambda": Real(0., 10.),
279
+ "importance_type": Categorical(["split", "gain"]),
280
+ },
281
+ "xgboost.XGBClassifier": {
282
+ "n_estimators": Integer(50, 400),
283
+ "max_depth": Integer(2, 20),
284
+ "max_leaves": Integer(0, 200),
285
+ "learning_rate": Real(1.0e-5, 1.0),
286
+ "objective": Categorical(
287
+ ["binary:logistic", "binary:logitraw", "binary:hinge"]
288
+ ),
289
+ "booster": Categorical(["gbtree", "gblinear", "dart"]),
290
+ "gamma": Real(0.0, 100.0),
291
+ # 'min_child_weight': Real(0., 100.),
292
+ # 'max_delta_step': Real(0., 100.),
293
+ "subsample": Real(1.0e-6, 1.0),
294
+ # 'colsample_bytree': Real(1.e-6, 1.),
295
+ # 'colsample_bylevel': Real(1.e-6, 1.),
296
+ # 'reg_alpha': Real(0., 10.),
297
+ # 'reg_lambda': Real(0., 10.),
298
+ # 'scale_pos_weight': Real(1.e-5, 1.e3),
299
+ # 'base_score': Real(1.e-5, .9999)
300
+ },
301
+ # 'sklearn.discriminant_analysis.QuadraticDiscriminantAnalysis': {
302
+ # 'tol': Real(1.e-5, 2.)
303
+ # },
304
+ # Feature Selectors
305
+ "sklearn.feature_selection.VarianceThreshold": {"threshold": Real(0.0, 0.3)},
306
+ "sklearn.decomposition.PCA": {
307
+ # 'svd_solver': Categorical(['randomized']),
308
+ "iterated_power": Integer(1, 11),
309
+ "n_components": Integer(2, 30),
310
+ },
311
+ "sklearn.decomposition.TruncatedSVD": {
312
+ "n_components": Integer(2, 30),
313
+ "algorithm": Categorical(["randomized", "arpack"]),
314
+ },
315
+ # Preprocesssors
316
+ "sklearn.preprocessing.StandardScaler": {
317
+ "with_mean": Categorical([True, False]),
318
+ "with_std": Categorical([True, False]),
319
+ },
320
+ "sklearn.preprocessing.Normalizer": {"norm": Categorical(["l1", "l2", "max"])},
321
+ # Transformers
322
+ # 'sklearn.preprocessing.PowerTransformer': {
323
+ # 'method': Categorical(['yeo-johnson', 'box-cox']),
324
+ # 'standardize': Categorical([True, False])
325
+ # },
326
+ "sklearn.kernel_approximation.Nystroem": {
327
+ "kernel": Categorical(["rbf", "poly", "sigmoid"]),
328
+ "gamma": Real(1.0e-2, 10.0),
329
+ "n_components": Integer(10, 100),
330
+ },
331
+ "sklearn.kernel_approximation.RBFSampler": {
332
+ "gamma": Real(1.0e-3, 10.0),
333
+ "n_components": Integer(10, 100),
334
+ },
335
+ "sklearn.kernel_approximation.AdditiveChi2Sampler": {"sample_steps": Integer(1, 3)},
336
+ "sklearn.kernel_approximation.SkewedChi2Sampler": {
337
+ "skewedness": Real(0.1, 5.0),
338
+ "n_components": Integer(10, 100),
339
+ },
340
+ # Sensitivity Analysis
341
+ "sksurrogate.SensAprx": {
342
+ "n_features_to_select": Integer(2, 30),
343
+ "method": Categorical(["sobol", "morris", "delta-mmnt"]),
344
+ # 'regressor': Categorical([None, regressor])
345
+ },
346
+ # Manifold Learning
347
+ # 'sklearn.manifold.Isomap': {
348
+ # 'n_neighbors': Integer(2, 12),
349
+ # 'n_components': Integer(1, 10),
350
+ # },
351
+ # 'sklearn.manifold.LocallyLinearEmbedding': {
352
+ # 'n_neighbors': Integer(2, 12),
353
+ # 'n_components': Integer(1, 10),
354
+ # 'reg ': Real(1.e-5, .2),
355
+ # 'method': Categorical(['standard', 'ltsa'])
356
+ # },
357
+ # 'sklearn.manifold.MDS': {
358
+ # 'n_components': Integer(1, 10),
359
+ # 'n_init': Integer(2, 8),
360
+ # },
361
+ # 'sklearn.manifold.SpectralEmbedding': {
362
+ # 'n_components': Integer(1, 10),
363
+ # 'affinity': Categorical(['nearest_neighbors', 'rbf', 'precomputed'])
364
+ # },
365
+ # 'sklearn.manifold.TSNE': {
366
+ # 'n_components': Integer(1, 10),
367
+ # 'perplexity': Real(5., 50.),
368
+ # 'early_exaggeration': Real(5., 25.),
369
+ # 'learning_rate ': Real(10., 500.)
370
+ # },
371
+ # Sampler
372
+ "imblearn.over_sampling.SMOTE": {"k_neighbors": Integer(3, 10)},
373
+ }
374
+
375
+
376
+
377
+ class AML(object):
378
+ """
379
+ A class that accepts a nested dictionary with machine learning libraries as its keys
380
+ and a dictionary of their parameters and their ranges as value of each key and finds
381
+ an optimum combination based on training data.
382
+
383
+ :param config: A dictionary whose keys are scikit-learn-style objects (as strings) and its corresponding
384
+ values are dictionaries of the parameters and their acceptable ranges/values
385
+ :param length: default=5; Maximum number of objects in generated pipelines
386
+ :param scoring: default='accuracy'; The scoring method to be optimized. Must follow the sklearn scoring signature
387
+ :param cat_cols: default=None; The list of indices of categorical columns
388
+ :param surrogates: default=None; A list of 4-tuples determining surrogates. The first entity of each tuple is
389
+ a scikit-learn regressor and the
390
+ 2nd entity is the number of iterations that this surrogate needs to be estimated and optimized.
391
+ The 3rd is the sampling strategy and the
392
+ 4th is the `scipy.optimize` solver
393
+ :param min_random_evals: default=15; Number of randomly sampled initial values for hyper parameters
394
+ :param cv: default=3; The cross-validation method. Integer values use
395
+ stratified folds for classification and regular folds for regression.
396
+ :param check_point: default='./'; The path where the optimization results will be stored
397
+ :param stack_res: default=True; `StackingEstimator`s `res`
398
+ :param stack_probs: default=True; `StackingEstimator`s `probs`
399
+ :param stack_decision: default=True; `StackingEstimator`s `decision`
400
+ :param stack_cv: default=5; cross-validation splitter used to build the out-of-fold
401
+ features of the automatic `StackingEstimator` wrappers
402
+ :param stack_n_jobs: default=None; parallel jobs used for those out-of-fold predictions
403
+ :param verbose: default=1; Level of output details
404
+ :param n_jobs: int, default=-1; number of processes to run in parallel
405
+ :param cpu_limit: Optional maximum CPU parallelism to respect for each surrogate search.
406
+ :param memory_limit: Optional memory limit descriptor carried in the summary metadata.
407
+ :param random_state: Optional seed for the default CV and evolutionary search.
408
+ :param time_limit: Optional wall-clock limit in seconds for each surrogate search.
409
+ :param max_evals: Optional maximum objective evaluations for each surrogate search.
410
+ :param forbidden: Iterable of forbidden parameter rules forwarded to every
411
+ ``SurrogateRandomCV`` (dicts mapping param name -> disallowed value, or callables).
412
+ :param conditional: Mapping of param name -> rule describing when the parameter is active;
413
+ inactive parameters are dropped from each candidate before evaluation.
414
+ """
415
+
416
+ def __init__(
417
+ self,
418
+ config=None,
419
+ length=5,
420
+ scoring="accuracy",
421
+ cat_cols=None,
422
+ surrogates=None,
423
+ min_random_evals=15,
424
+ cv=None,
425
+ check_point="./",
426
+ stack_res=True,
427
+ stack_probs=True,
428
+ stack_decision=True,
429
+ stack_cv=5,
430
+ stack_n_jobs=None,
431
+ verbose=1,
432
+ n_jobs=-1,
433
+ random_state=None,
434
+ time_limit=None,
435
+ max_evals=None,
436
+ cpu_limit=None,
437
+ memory_limit=None,
438
+ forbidden=(),
439
+ conditional=(),
440
+ ):
441
+ from collections import OrderedDict
442
+
443
+ self.config: dict = {}
444
+ if config is None:
445
+ self.config = default_config
446
+ else:
447
+ self.config = config
448
+ self.config_types = {}
449
+ self.couldBlast = []
450
+ self.couldBfirst = []
451
+ self.feature_selector = set()
452
+ self.known_feature_selectors = {
453
+ "sklearn.decomposition.FactorAnalysis",
454
+ "sklearn.decomposition.FastICA",
455
+ "sklearn.decomposition.IncrementalPCA",
456
+ "sklearn.decomposition.KernelPCA",
457
+ "sklearn.decomposition.LatentDirichletAllocation",
458
+ "sklearn.decomposition.MiniBatchDictionaryLearning",
459
+ "sklearn.decomposition.MiniBatchSparsePCA",
460
+ "sklearn.decomposition.NMF",
461
+ "sklearn.decomposition.PCA",
462
+ "sklearn.decomposition.SparsePCA",
463
+ "sklearn.decomposition.TruncatedSVD",
464
+ "sklearn.feature_selection.VarianceThreshold",
465
+ "sklearn.manifold.LocallyLinearEmbedding",
466
+ "sklearn.manifold.Isomap",
467
+ "sklearn.manifold.MDS",
468
+ "sklearn.manifold.SpectralEmbedding",
469
+ "sklearn.manifold.TSNE",
470
+ "sksurrogate.SensAprx",
471
+ "skrebate.ReliefF",
472
+ "skrebate.SURF",
473
+ "skrebate.SURFstar",
474
+ "skrebate.MultiSURF",
475
+ "skrebate.MultiSURFstar",
476
+ "skrebate.TuRF",
477
+ }
478
+ self.letters = self.config_types.keys()
479
+ self.length = length
480
+ self.scoring = scoring
481
+ self.cat_cols = cat_cols
482
+ self.surrogates = surrogates
483
+ self.check_point = check_point
484
+ self.min_random_evals = min_random_evals
485
+ self.stack_res = stack_res
486
+ self.stack_probs = stack_probs
487
+ self.stack_decision = stack_decision
488
+ self.stack_cv = stack_cv
489
+ self.stack_n_jobs = stack_n_jobs
490
+ self.verbose = verbose
491
+ self.num_features = 2
492
+ self.cpu_limit = cpu_limit
493
+ self.memory_limit = memory_limit
494
+ self.forbidden = tuple(forbidden)
495
+ self.conditional = dict(conditional)
496
+ self.n_jobs = n_jobs
497
+ self.random_state = random_state
498
+ self.time_limit = time_limit
499
+ self.max_evals = max_evals
500
+ self._apply_resource_limits()
501
+ if cv is None:
502
+ self.cv = 3
503
+ elif isinstance(cv, dict):
504
+ # Accept a JSON spec as produced by ``mltrace.cv_to_spec`` /
505
+ # ``mltrace.GetCVSpec`` and rebuild the splitter. A raw dict has no
506
+ # ``split`` method, so scikit-learn's ``check_cv`` would treat it as
507
+ # an iterable of pre-computed (train, test) folds and fail with a
508
+ # confusing "too many values to unpack" error.
509
+ from .mltrace import build_cv
510
+
511
+ rebuilt = build_cv(cv)
512
+ self.cv = 3 if rebuilt is None else rebuilt
513
+ else:
514
+ self.cv = cv
515
+ self.types()
516
+ self.words = Words(self.letters, last=self.couldBlast, first=self.couldBfirst)
517
+ self.models = OrderedDict([])
518
+ self.evaluation_history_ = []
519
+ self.last_candidate_duration_ = None
520
+ self.best_estimator_ = None
521
+ self.best_estimator_score = 0.0
522
+ self.termination_reason = None
523
+ self.summary_ = {}
524
+
525
+ def _apply_resource_limits(self):
526
+ """Clamp the effective parallelism to the configured CPU budget."""
527
+ if self.cpu_limit is None:
528
+ return
529
+ limit = int(self.cpu_limit)
530
+ if limit < 1:
531
+ raise ValueError("cpu_limit must be a positive integer")
532
+ if self.n_jobs == -1 or self.n_jobs > limit:
533
+ self.n_jobs = limit
534
+
535
+ def types(self):
536
+ """
537
+ Recognizes the type of each estimator to determine proper placement of each
538
+
539
+ :return: None
540
+ """
541
+ from importlib import import_module
542
+ from sklearn.base import is_classifier, is_regressor
543
+ from sklearn.feature_selection import SelectorMixin
544
+
545
+ for alg in self.config:
546
+ if alg != "sklearn.pipeline.FeatureUnion":
547
+ detail = alg.split(".")
548
+ module_str = ".".join(detail[:-1])
549
+ module = import_module(module_str)
550
+ clss = module.__getattribute__(detail[-1])
551
+ mdl = self._new_estimator(clss)
552
+ if is_classifier(mdl):
553
+ self.config_types[alg] = "classifier"
554
+ self.couldBlast.append(alg)
555
+ elif is_regressor(mdl):
556
+ self.config_types[alg] = "regressor"
557
+ self.couldBlast.append(alg)
558
+ elif hasattr(mdl, "_estimator_type") and mdl._estimator_type == "sampler":
559
+ self.config_types[alg] = "sampler"
560
+ self.couldBfirst.append(alg)
561
+ else:
562
+ self.config_types[alg] = "transformer"
563
+ if (
564
+ issubclass(clss, SelectorMixin)
565
+ or alg in self.known_feature_selectors
566
+ ):
567
+ self.feature_selector.add(alg)
568
+ else:
569
+ self.config_types[alg] = "transformer"
570
+
571
+ def _validate_sequence(self, seq):
572
+ """
573
+ Validates the structure of the building sequence
574
+
575
+ :param seq: a sequence of (genes) estimators
576
+ :return: True or False
577
+ """
578
+ # Check the validity of the final estimator
579
+ if self.config_types[seq[-1]] not in ["regressor", "classifier"]:
580
+ return False
581
+ n = len(seq) - 1
582
+ # The second to last estimator can't be `FeatureUnion`
583
+ if seq[n - 1] == "sklearn.pipeline.FeatureUnion":
584
+ return False
585
+ flag = False
586
+ cnt = 0
587
+ for idx in range(n):
588
+ gene = seq[idx]
589
+ if gene == "sklearn.pipeline.FeatureUnion":
590
+ flag = True
591
+ elif (
592
+ (gene in self.feature_selector)
593
+ or (self.config_types[gene] in ["regressor", "classifier"])
594
+ ) and flag:
595
+ cnt += 1
596
+ elif flag and not (
597
+ (gene in self.feature_selector)
598
+ or (self.config_types[gene] in ["regressor", "classifier"])
599
+ ):
600
+ if cnt == 0:
601
+ return False
602
+ else:
603
+ flag = False
604
+ cnt = 0
605
+ return True
606
+
607
+ def add_surrogate(self, estimator, itrs, sampling=None, optim="L-BFGS-B"):
608
+ """
609
+ Adding a regressor for surrogate optimization procedure.
610
+
611
+ :param estimator: A scikit-learn style regressor
612
+ :param itrs: Number of iterations the `estimator` needs to be fitted and optimized
613
+ :param sampling: default= BoxSample; The sampling strategy (`CompactSample`, `BoxSample` or `SphereSample`)
614
+ :param optim: default='L-BFGS-B';`scipy.optimize` solver
615
+ :return: None
616
+ """
617
+ if self.surrogates is None:
618
+ self.surrogates = []
619
+ if sampling is None:
620
+ from .structsearch import BoxSample
621
+
622
+ sampling = BoxSample
623
+ self.surrogates.append((estimator, itrs, sampling, optim))
624
+
625
+ def _cast(self, n, X, y):
626
+ """
627
+ Evaluates and optimizes all acceptable combinations of length `n`
628
+
629
+ :param n: The length of pipelines
630
+ :param X: Training data
631
+ :param y: Observed values
632
+ :return: None
633
+ """
634
+ from .structsearch import BoxSample, CompactSample
635
+
636
+ # if self.couldBfirst == []:
637
+ # from sklearn.pipeline import Pipeline
638
+ # else:
639
+ # from imblearn.pipeline import Pipeline
640
+ from sklearn.model_selection import RandomizedSearchCV
641
+
642
+ if self.surrogates is None:
643
+ from numpy import logspace
644
+ from sklearn.gaussian_process import GaussianProcessRegressor
645
+ from sklearn.kernel_ridge import KernelRidge
646
+ from sklearn.gaussian_process.kernels import (
647
+ Matern,
648
+ Sum,
649
+ ExpSineSquared,
650
+ WhiteKernel,
651
+ )
652
+
653
+ param_grid_gpr = {
654
+ "alpha": logspace(-8, 1, 20),
655
+ "kernel": [
656
+ Sum(Matern(length_scale=l, nu=p), WhiteKernel(noise_level=q))
657
+ for l in logspace(-3, 3, 20)
658
+ for p in [0.5, 1.5, 2.5]
659
+ for q in logspace(-3, 1.5, 20)
660
+ ],
661
+ }
662
+ GPR = RandomizedSearchCV(
663
+ GaussianProcessRegressor(),
664
+ param_distributions=param_grid_gpr,
665
+ n_iter=20,
666
+ cv=2,
667
+ )
668
+ param_grid_krr = {
669
+ "alpha": logspace(-2, 0, 10),
670
+ "kernel": [
671
+ Sum(Matern(), ExpSineSquared(l, p))
672
+ for l in logspace(-2, 2, 20)
673
+ for p in logspace(0, 2, 20)
674
+ ],
675
+ }
676
+ KRR = RandomizedSearchCV(
677
+ KernelRidge(), param_distributions=param_grid_krr, n_iter=30, cv=2
678
+ )
679
+ self.surrogates = [
680
+ (KRR, 35, CompactSample, "L-BFGS-B"),
681
+ (GPR, 50, BoxSample, "L-BFGS-B"),
682
+ ]
683
+ self.min_random_evals = 10
684
+ surrogates = self.surrogates
685
+ if surrogates is None:
686
+ raise RuntimeError("No surrogate regressors are configured")
687
+ Pop = []
688
+ candidates = self.words.Generate(n)
689
+ for cnddt in candidates:
690
+ if self._validate_sequence(cnddt):
691
+ Pop.append(cnddt)
692
+ for seq in Pop:
693
+ if not self._validate_sequence(seq):
694
+ continue
695
+ best_mdl, best_scr = self.optimize_pipeline(seq, X, y)
696
+ self.models[seq] = (best_mdl, best_scr)
697
+ self.evaluation_history_.append(
698
+ {"pipeline": seq, "estimator": best_mdl, "score": best_scr}
699
+ )
700
+ if self.verbose > 0:
701
+ print("score:%f" % best_scr)
702
+ print(best_mdl)
703
+
704
+ def fit(self, X: Any, y: Any):
705
+ """
706
+ Generates and optimizes all legitimate pipelines. The best pipeline can be retrieved from `self.best_estimator_`
707
+
708
+ :param X: Training data
709
+ :param y: Corresponding observations
710
+ :return: `self`
711
+ """
712
+ import numpy as np
713
+
714
+ self.models.clear()
715
+ self.evaluation_history_.clear()
716
+ self.termination_reason = None
717
+ self.summary_ = {}
718
+ _X, _y = X, y
719
+ if self.cat_cols is not None:
720
+ from category_encoders.one_hot import OneHotEncoder
721
+
722
+ enc = OneHotEncoder(
723
+ cols=self.cat_cols, return_df=False, handle_unknown="ignore"
724
+ )
725
+ enc.fit(X)
726
+ _X = enc.transform(X)
727
+
728
+ X_, y_ = _X, _y
729
+ self.num_features = np.asarray(X_).shape[1]
730
+ for l in range(1, self.length + 1):
731
+ self._cast(l, X_, y_)
732
+ self.best_estimator_ = list(self.get_top(1).items())[0][1][0]
733
+ self.best_estimator_score = list(self.get_top(1).items())[0][1][1]
734
+ return self
735
+
736
+ @staticmethod
737
+ def _get_class(library):
738
+ """
739
+ Takes a string that refers to a class in an accessible module and returns the associated class
740
+
741
+ :param library: string referring to a class
742
+ :return: the actual class
743
+ """
744
+ from importlib import import_module
745
+
746
+ detail = library.split(".")
747
+ module_str = ".".join(detail[:-1])
748
+ module = import_module(module_str)
749
+ clss = module.__getattribute__(detail[-1])
750
+ return clss
751
+
752
+ def _new_estimator(self, estimator_class):
753
+ """Instantiate an estimator and seed it when it exposes ``random_state``."""
754
+ estimator = estimator_class()
755
+ if estimator_class.__module__.startswith("lightgbm"):
756
+ estimator.set_params(verbosity=-1)
757
+ if self.random_state is not None and hasattr(estimator, "get_params"):
758
+ params = estimator.get_params(deep=False)
759
+ if "random_state" in params:
760
+ estimator.set_params(random_state=self.random_state)
761
+ return estimator
762
+
763
+ def eoa_fit(self, X: Any, y: Any, **kwargs):
764
+ """
765
+ Applies evolutionary optimization methods to find an optimum pipeline
766
+
767
+ :param X: Training data
768
+ :param y: Corresponding observations
769
+ :param kwargs: `EOA` parameters
770
+ :return: `self`
771
+ """
772
+ import numpy as np
773
+ from .structsearch import BoxSample, CompactSample
774
+ from .eoa import EOA
775
+
776
+ self.models.clear()
777
+ self.evaluation_history_.clear()
778
+ self.termination_reason = None
779
+ self.summary_ = {}
780
+ _X, _y = X, y
781
+ if self.cat_cols is not None:
782
+ from category_encoders.one_hot import OneHotEncoder
783
+
784
+ enc = OneHotEncoder(
785
+ cols=self.cat_cols, return_df=False, handle_unknown="ignore"
786
+ )
787
+ enc.fit(X)
788
+ _X = enc.transform(X)
789
+ X_, y_ = _X, _y
790
+ self.num_features = np.asarray(X_).shape[1]
791
+ Pop = []
792
+ for l in range(1, self.length + 1):
793
+ candidates = self.words.Generate(l)
794
+ for cnddt in candidates:
795
+ if self._validate_sequence(cnddt):
796
+ Pop.append(cnddt)
797
+
798
+ def _eval(ppl):
799
+ # if self.couldBfirst == []:
800
+ # from sklearn.pipeline import Pipeline
801
+ # else:
802
+ # from imblearn.pipeline import Pipeline
803
+ from sklearn.model_selection import RandomizedSearchCV
804
+
805
+ if self.surrogates is None:
806
+ from numpy import logspace
807
+ from sklearn.gaussian_process import GaussianProcessRegressor
808
+ from sklearn.kernel_ridge import KernelRidge
809
+ from sklearn.gaussian_process.kernels import (
810
+ Matern,
811
+ Sum,
812
+ ExpSineSquared,
813
+ WhiteKernel,
814
+ )
815
+
816
+ param_grid_gpr = {
817
+ "alpha": logspace(-8, 1, 20),
818
+ "kernel": [
819
+ Sum(Matern(length_scale=l_, nu=p), WhiteKernel(noise_level=q))
820
+ for l_ in logspace(-3, 3, 20)
821
+ for p in [0.5, 1.5, 2.5]
822
+ for q in logspace(-3, 1.5, 20)
823
+ ],
824
+ }
825
+ GPR = RandomizedSearchCV(
826
+ GaussianProcessRegressor(),
827
+ param_distributions=param_grid_gpr,
828
+ n_iter=20,
829
+ cv=2,
830
+ )
831
+ param_grid_krr = {
832
+ "alpha": logspace(-4, 0, 10),
833
+ "kernel": [
834
+ Sum(Matern(), ExpSineSquared(l_, p))
835
+ for l_ in logspace(-2, 2, 20)
836
+ for p in logspace(0, 2, 20)
837
+ ],
838
+ }
839
+ KRR = RandomizedSearchCV(
840
+ KernelRidge(), param_distributions=param_grid_krr, n_iter=30, cv=2
841
+ )
842
+ self.surrogates = [
843
+ (KRR, 35, CompactSample, "L-BFGS-B"),
844
+ (GPR, 50, BoxSample, "L-BFGS-B"),
845
+ ]
846
+ self.min_random_evals = 10
847
+ from collections import OrderedDict
848
+
849
+ fitted = OrderedDict([])
850
+ for seq in ppl:
851
+ best_mdl, best_scr = self.optimize_pipeline(seq, X_, y_)
852
+ if seq not in self.models:
853
+ self.models[seq] = (best_mdl, best_scr)
854
+ self.evaluation_history_.append(
855
+ {
856
+ "pipeline": seq,
857
+ "estimator": best_mdl,
858
+ "score": best_scr,
859
+ "duration": getattr(self, "last_candidate_duration_", None),
860
+ }
861
+ )
862
+ if self.verbose > 0:
863
+ print("score:%f" % best_scr)
864
+ print(best_mdl)
865
+ fitted[seq] = -best_scr
866
+ return fitted
867
+
868
+ num_parents = kwargs.pop("num_parents", 30)
869
+ mutation_prob = kwargs.pop("mutation_prob", 0.1)
870
+ _eoa = EOA(
871
+ population=Pop,
872
+ fitness=_eval,
873
+ num_parents=num_parents,
874
+ mutation_prob=mutation_prob,
875
+ term_genes=self.couldBlast,
876
+ init_genes=self.couldBfirst,
877
+ check_point=self.check_point,
878
+ random_state=self.random_state,
879
+ **kwargs
880
+ )
881
+ _eoa()
882
+ self.best_estimator_ = list(self.get_top(1).items())[0][1][0]
883
+ return self
884
+
885
+ def get_top(self, num=5):
886
+ """
887
+ Finds the top `n` pipelines
888
+
889
+ :param num: Number of pipelines to be returned
890
+ :return: An OrderedDict of top models
891
+ """
892
+ from collections import OrderedDict
893
+
894
+ return OrderedDict(sorted(self.models.items(), key=lambda x: x[1][1])[:num])
895
+
896
+ def pareto_frontier(self, score_key="score", cost_key="duration"):
897
+ """Return the non-dominated candidates across ``evaluation_history_``.
898
+
899
+ Scores are maximized and costs (per-candidate wall-clock duration) are
900
+ minimized, mirroring :meth:`SurrogateRandomCV.pareto_frontier`. Entries
901
+ missing either metric are skipped. Stored scores are the negated search
902
+ objective, so they are flipped back to the scorer's higher-is-better
903
+ units before dominance testing; the returned records carry that flipped
904
+ value under ``score_key``.
905
+ """
906
+ from types import SimpleNamespace
907
+
908
+ from .structsearch import SurrogateRandomCV
909
+
910
+ records = []
911
+ for entry in self.evaluation_history_:
912
+ raw_score = entry.get(score_key)
913
+ cost = entry.get(cost_key)
914
+ if raw_score is None or cost is None:
915
+ continue
916
+ record = dict(entry)
917
+ record[score_key] = -float(raw_score)
918
+ # AML history entries carry no status; mark them complete so the
919
+ # structsearch dominance filter (status == "complete") keeps them.
920
+ record.setdefault("status", "complete")
921
+ records.append(record)
922
+ holder = SimpleNamespace(evaluation_history_=records)
923
+ return SurrogateRandomCV.pareto_frontier(holder, score_key=score_key, cost_key=cost_key)
924
+
925
+ def _require_fitted(self):
926
+ if self.best_estimator_ is None:
927
+ raise RuntimeError("AML has not been fitted")
928
+ return self.best_estimator_
929
+
930
+ def predict(self, X):
931
+ """Predict targets with the best pipeline found by ``fit`` or ``eoa_fit``."""
932
+ return self._require_fitted().predict(X)
933
+
934
+ def predict_proba(self, X):
935
+ """Predict class probabilities with the best classifier pipeline."""
936
+ estimator = self._require_fitted()
937
+ if not hasattr(estimator, "predict_proba"):
938
+ raise AttributeError("The selected estimator does not provide predict_proba")
939
+ return estimator.predict_proba(X)
940
+
941
+ def score(self, X, y):
942
+ """Return the best pipeline's estimator score on ``X`` and ``y``."""
943
+ return self._require_fitted().score(X, y)
944
+
945
+ def get_params(self, deep=True):
946
+ """Return constructor parameters using the scikit-learn convention."""
947
+ return {
948
+ "config": self.config,
949
+ "length": self.length,
950
+ "scoring": self.scoring,
951
+ "cat_cols": self.cat_cols,
952
+ "surrogates": self.surrogates,
953
+ "min_random_evals": self.min_random_evals,
954
+ "cv": self.cv,
955
+ "check_point": self.check_point,
956
+ "stack_res": self.stack_res,
957
+ "stack_probs": self.stack_probs,
958
+ "stack_decision": self.stack_decision,
959
+ "stack_cv": self.stack_cv,
960
+ "stack_n_jobs": self.stack_n_jobs,
961
+ "verbose": self.verbose,
962
+ "n_jobs": self.n_jobs,
963
+ "cpu_limit": self.cpu_limit,
964
+ "memory_limit": self.memory_limit,
965
+ "random_state": self.random_state,
966
+ "time_limit": self.time_limit,
967
+ "max_evals": self.max_evals,
968
+ "forbidden": self.forbidden,
969
+ "conditional": self.conditional,
970
+ }
971
+
972
+ def set_params(self, **params):
973
+ """Set constructor parameters and return this AML instance."""
974
+ valid_params = self.get_params()
975
+ invalid = sorted(set(params) - set(valid_params))
976
+ if invalid:
977
+ raise ValueError("Invalid AML parameters: %s" % ", ".join(invalid))
978
+ for key, value in params.items():
979
+ setattr(self, key, value)
980
+ self._apply_resource_limits()
981
+ self.best_estimator_ = None
982
+ self.evaluation_history_ = []
983
+ self.termination_reason = None
984
+ self.summary_ = {}
985
+ return self
986
+
987
+ def optimize_pipeline(self, seq, X, y):
988
+ """
989
+ Constructs and optimizes a pipeline according to the steps passed through `seq` which is a tuple of
990
+ estimators and transformers.
991
+
992
+ :param seq: the tuple of steps of the pipeline to be optimized
993
+ :param X: numpy array of training features
994
+ :param y: numpy array of training values
995
+ :return: the optimized pipeline and its score
996
+ """
997
+ from .structsearch import SurrogateRandomCV
998
+ surrogates = self.surrogates
999
+ if surrogates is None:
1000
+ raise RuntimeError("No surrogate regressors are configured")
1001
+
1002
+ if self.couldBfirst == []:
1003
+ from sklearn.pipeline import Pipeline
1004
+ else:
1005
+ from imblearn.pipeline import Pipeline
1006
+ OPTIM = None
1007
+ n = len(seq)
1008
+ idx = 0
1009
+ ent_idx = 0
1010
+ steps = []
1011
+ config = {}
1012
+ task_name = self.check_point + "_".join(seq)
1013
+ while ent_idx < n:
1014
+ est = seq[ent_idx]
1015
+ clss = self._get_class(est)
1016
+ pre = "stp_%d" % idx
1017
+ step_param_prefix = ""
1018
+ if (
1019
+ self.config_types[est] in ["regressor", "classifier"]
1020
+ and ent_idx < n - 1
1021
+ ):
1022
+ mdl = self._new_estimator(clss)
1023
+ steps.append(
1024
+ (
1025
+ pre,
1026
+ StackingEstimator(
1027
+ mdl,
1028
+ res=self.stack_res,
1029
+ probs=self.stack_probs,
1030
+ decision=self.stack_decision,
1031
+ cv=self.stack_cv,
1032
+ n_jobs=self.stack_n_jobs,
1033
+ ),
1034
+ )
1035
+ )
1036
+ step_param_prefix = "estimator__"
1037
+ ent_idx += 1
1038
+ elif est == "sklearn.pipeline.FeatureUnion":
1039
+ self.config[est] = dict()
1040
+ int_idx = 1
1041
+ int_steps = []
1042
+ next_est = seq[ent_idx + int_idx]
1043
+ # Find the rest of the sequence before the last or the next feature selector
1044
+ while (
1045
+ (self.config_types[next_est] in ["regressor", "classifier"])
1046
+ or (next_est in self.known_feature_selectors)
1047
+ ) and (ent_idx + int_idx < n - 1):
1048
+ int_pre = "int_%d" % int_idx
1049
+ if next_est in self.known_feature_selectors:
1050
+ int_mdl = self._new_estimator(self._get_class(next_est))
1051
+ # set the parameter's dictionary
1052
+ for kw in self.config[next_est]:
1053
+ self.config[est][int_pre + "__" + kw] = self.config[
1054
+ next_est
1055
+ ][kw]
1056
+ else:
1057
+ from eli5.sklearn import PermutationImportance
1058
+ from sklearn.feature_selection import SelectFromModel
1059
+ from numpy import inf
1060
+
1061
+ int_est = self._new_estimator(self._get_class(next_est))
1062
+ int_mdl = SelectFromModel(
1063
+ PermutationImportance(int_est, scoring=self.scoring, cv=3), # type: ignore[arg-type]
1064
+ threshold=-inf,
1065
+ )
1066
+ self.config[est][int_pre + "__" + "max_features"] = Integer(
1067
+ 1, self.num_features
1068
+ )
1069
+ for kw in self.config[next_est]:
1070
+ self.config[est][
1071
+ int_pre + "__" + "estimator__estimator__" + kw
1072
+ ] = self.config[next_est][kw]
1073
+ int_steps.append((int_pre, int_mdl))
1074
+ int_idx += 1
1075
+ next_est = seq[ent_idx + int_idx]
1076
+ if int_steps != []:
1077
+ mdl = clss(int_steps)
1078
+ steps.append((pre, mdl))
1079
+ ent_idx += int_idx
1080
+ else:
1081
+ mdl = self._new_estimator(clss)
1082
+ steps.append((pre, mdl))
1083
+ ent_idx += 1
1084
+ idx += 1
1085
+ for kw in self.config[est]:
1086
+ config[pre + "__" + step_param_prefix + kw] = self.config[est][kw]
1087
+ ppln = Pipeline(steps)
1088
+ if self.verbose > 0:
1089
+ print("=" * 90)
1090
+ print(seq)
1091
+ print("-" * 90)
1092
+ from time import perf_counter
1093
+
1094
+ started = perf_counter()
1095
+ if not config:
1096
+ from sklearn.model_selection import cross_val_score
1097
+
1098
+ scores = cross_val_score(
1099
+ ppln,
1100
+ X,
1101
+ y,
1102
+ scoring=self.scoring,
1103
+ cv=self.cv,
1104
+ n_jobs=self.n_jobs,
1105
+ )
1106
+ ppln.fit(X, y)
1107
+ score = float(scores.mean())
1108
+ self.termination_reason = "completed"
1109
+ self.summary_ = {
1110
+ "termination_reason": "completed",
1111
+ "status": "completed",
1112
+ "budget": {
1113
+ "time_limit": self.time_limit,
1114
+ "max_evals": self.max_evals,
1115
+ "cpu_limit": self.cpu_limit,
1116
+ "memory_limit": self.memory_limit,
1117
+ "elapsed_seconds": 0.0,
1118
+ "iterations": 0,
1119
+ "evaluations": 0,
1120
+ },
1121
+ "current_value": None,
1122
+ }
1123
+ self.last_candidate_duration_ = perf_counter() - started
1124
+ return ppln, score
1125
+ for srgt in surrogates:
1126
+ OPTIM = SurrogateRandomCV(
1127
+ ppln,
1128
+ params=config,
1129
+ max_iter=srgt[1],
1130
+ min_evals=self.min_random_evals,
1131
+ scoring=self.scoring,
1132
+ cv=self.cv,
1133
+ verbose=max(self.verbose - 1, 0),
1134
+ sampling=srgt[2],
1135
+ regressor=srgt[0],
1136
+ scipy_solver=srgt[3],
1137
+ task_name=task_name,
1138
+ Continue=True,
1139
+ warm_start=True,
1140
+ n_jobs=self.n_jobs,
1141
+ time_limit=self.time_limit,
1142
+ max_evals=self.max_evals,
1143
+ random_state=self.random_state,
1144
+ forbidden=self.forbidden,
1145
+ conditional=self.conditional,
1146
+ )
1147
+ OPTIM.fit(X, y)
1148
+ if getattr(OPTIM, "summary_", None):
1149
+ self.termination_reason = getattr(
1150
+ OPTIM, "termination_reason", self.termination_reason
1151
+ )
1152
+ self.summary_ = dict(OPTIM.summary_)
1153
+ if OPTIM is None:
1154
+ raise RuntimeError("No surrogate search was executed")
1155
+ self.last_candidate_duration_ = perf_counter() - started
1156
+ return OPTIM.best_estimator_, OPTIM.best_estimator_score