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/DataProcess.py +409 -0
- SKSurrogate/NpyProximation.py +457 -0
- SKSurrogate/__init__.py +110 -0
- SKSurrogate/aml.py +1156 -0
- SKSurrogate/ci.py +93 -0
- SKSurrogate/deployment.py +31 -0
- SKSurrogate/eoa.py +381 -0
- SKSurrogate/execution.py +416 -0
- SKSurrogate/inference.py +224 -0
- SKSurrogate/mltrace.py +2138 -0
- SKSurrogate/modelbundle.py +600 -0
- SKSurrogate/monitoring.py +360 -0
- SKSurrogate/retraining.py +49 -0
- SKSurrogate/sensapprx.py +250 -0
- SKSurrogate/structsearch.py +1411 -0
- SKSurrogate/synthdat.py +503 -0
- sksurrogate-0.1.0.dist-info/METADATA +139 -0
- sksurrogate-0.1.0.dist-info/RECORD +21 -0
- sksurrogate-0.1.0.dist-info/WHEEL +5 -0
- sksurrogate-0.1.0.dist-info/entry_points.txt +2 -0
- sksurrogate-0.1.0.dist-info/top_level.txt +1 -0
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
|