PineBioML 1.2.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.
@@ -0,0 +1 @@
1
+
@@ -0,0 +1,414 @@
1
+ import sklearn.metrics as metrics
2
+ import optuna
3
+ from abc import ABC, abstractmethod
4
+ from sklearn.model_selection import StratifiedKFold
5
+ from sklearn.utils.class_weight import compute_sample_weight
6
+ from sklearn.preprocessing import LabelEncoder
7
+ from optuna.samplers import TPESampler
8
+ from numpy.random import RandomState, randint
9
+ from pandas import Series
10
+
11
+ from sklearn.exceptions import ConvergenceWarning
12
+
13
+ optuna.logging.set_verbosity(optuna.logging.WARNING)
14
+ ConvergenceWarning('ignore')
15
+
16
+
17
+ class Basic_tuner(ABC):
18
+ """
19
+ The base class of tuner. A tuner is a wrapper of optuna + models
20
+ What the tuners do:
21
+ 1. interface of optuna and models with sklearn api style
22
+ 2. randomity management.
23
+ 3. providing a uniform interface to regression and classiciation(binary and multi-class)
24
+
25
+ To conserve the reproducibility and to reduce the hyper parameter overfitting along the process of hyper parameter tuning,
26
+ we first using valid_seed to randomly initialize a tape of integers and it will sequentially be used in optuna trials.
27
+
28
+
29
+ """
30
+
31
+ def __init__(self,
32
+ n_try: int,
33
+ n_cv: int,
34
+ target: str,
35
+ validate_penalty: bool,
36
+ kernel_seed: int = None,
37
+ valid_seed: int = None,
38
+ optuna_seed: int = None):
39
+ """
40
+
41
+ Args:
42
+ n_try (int): The number of trials optuna should try.
43
+ n_cv (int): The number of folds to execute cross validation evaluation in iteration of optuna optimization.
44
+ target (str): The target of optuna optimization. Notice that is different from the training loss of model.
45
+ validate_penalty (bool): True to penalty the overfitting by difference between training score and cv score.
46
+ kernel_seed (int, optional): Random seed for model. Defaults to None.
47
+ valid_seed (int, optional): Random seed for cross validation. Defaults to None.
48
+ optuna_seed (int, optional): Random seed for optuna's hyperparameter sampling. Defaults to None.
49
+
50
+ ToDo:
51
+ 1. transfer the initialization of seed tape from __init__ to fit.
52
+ 2. optuna pruner: See the section Acticating Pruners in https://optuna.readthedocs.io/en/stable/tutorial/10_key_features/003_efficient_optimization_algorithms.html
53
+ 3. Winner's curse
54
+
55
+ """
56
+ self.y_mapping = LabelEncoder()
57
+
58
+ self.validate_penalty = validate_penalty
59
+
60
+ self.n_cv = n_cv
61
+ self.n_try = n_try
62
+
63
+ # initialize the random seeds
64
+ if kernel_seed is None:
65
+ self.kernel_seed = randint(16384)
66
+ else:
67
+ self.kernel_seed = kernel_seed
68
+
69
+ if valid_seed is None:
70
+ self.valid_seed = randint(16384)
71
+ else:
72
+ self.valid_seed = valid_seed
73
+
74
+ if optuna_seed is None:
75
+ self.optuna_seed = randint(16384)
76
+ else:
77
+ self.optuna_seed = optuna_seed
78
+
79
+ # The random seed tapes for cross validation along the optuna's optimization trials.
80
+ self.valid_seed_tape = RandomState(self.valid_seed).randint(low=0,
81
+ high=16384,
82
+ size=n_try)
83
+ self.kernel_seed_tape = RandomState(self.kernel_seed).randint(
84
+ low=0, high=16384, size=n_try)
85
+
86
+ # Get the scorer
87
+ self.metric = self.get_scorer(target)
88
+
89
+ self.optuna_model = None
90
+ self.default_model = None
91
+ self.best_model = None
92
+
93
+ def reference(self) -> dict[str, str]:
94
+ """
95
+ This function will return reference of this method in python dict.
96
+ If you want to access it in PineBioML api document, then click on the >Expand source code
97
+
98
+ Returns:
99
+ dict[str, str]: a dict of reference.
100
+ """
101
+ refers = {
102
+ "optuna publication":
103
+ "https://dl.acm.org/doi/10.1145/3292500.3330701",
104
+ "optuna document": "https://optuna.org/",
105
+ "sklearn publication":
106
+ "https://dl.acm.org/doi/10.5555/1953048.2078195"
107
+ }
108
+
109
+ return refers
110
+
111
+ @abstractmethod
112
+ def name(self) -> str:
113
+ """
114
+ Returns:
115
+ str: Name of this tuner.
116
+ """
117
+ pass
118
+
119
+ @abstractmethod
120
+ def is_regression(self) -> bool:
121
+ """
122
+ Returns:
123
+ bool: True if the task of tuner is a regression task.
124
+ """
125
+ pass
126
+
127
+ @abstractmethod
128
+ def create_model(self, trial, default):
129
+ """
130
+ Create model based on default setting or optuna trial
131
+
132
+ Args:
133
+ trial (optuna.trial.Trial): optuna trial in this call.
134
+ default (bool): set True to use default hyper parameter
135
+
136
+ Returns :
137
+ sklearn.base.BaseEstimator: A sklearn style model object.
138
+ """
139
+ pass
140
+
141
+ def evaluate(self, trial, default=False) -> float:
142
+ """
143
+ To evaluate the score of this trial. you should call create_model instead of creating model manually in this function.
144
+
145
+ Args:
146
+ trial (optuna.trial.Trial): optuna trial in this call.
147
+ default (bool): To use default hyper parameter. This argument will be passed to creat_model
148
+ Returns :
149
+ float: The score. Decided by optimization target.
150
+ """
151
+ # create the model using from this trial
152
+ classifier_obj = self.create_model(trial, default)
153
+
154
+ # create cross validation
155
+ cv = StratifiedKFold(n_splits=self.n_cv,
156
+ shuffle=True,
157
+ random_state=self.valid_seed_tape[trial.number])
158
+
159
+ # do cv
160
+ score = []
161
+ for i, (train_ind, test_ind) in enumerate(cv.split(self.x, self.y)):
162
+ # train test split
163
+ x_train = self.x.iloc[train_ind]
164
+ y_train = self.y.iloc[train_ind]
165
+ x_test = self.x.iloc[test_ind]
166
+ y_test = self.y.iloc[test_ind]
167
+
168
+ # sample_weight for imbalanced class.
169
+ if self.is_regression():
170
+ sample_weight = None
171
+ else:
172
+ sample_weight = compute_sample_weight(class_weight="balanced",
173
+ y=y_train)
174
+
175
+ # fit the model on training fold
176
+ classifier_obj.fit(x_train, y_train, sample_weight=sample_weight)
177
+
178
+ # evaluate on testing fold
179
+ test_score = self.metric(classifier_obj, x_test, y_test)
180
+ train_score = self.metric(classifier_obj, x_train, y_train)
181
+
182
+ if self.validate_penalty:
183
+ score.append(test_score + 0.1 * (test_score - train_score))
184
+ else:
185
+ score.append(test_score)
186
+
187
+ # averaging cv scores
188
+ score = sum(score) / self.n_cv
189
+ #print(score)
190
+ return score
191
+
192
+ def tune(self, x, y) -> None:
193
+ """
194
+ this function tunes the hyperparameters of a given kernel.
195
+
196
+ Args:
197
+ x (pandas.DataFrame or 2D-array): feature to extract information from.
198
+ y (pandas.Series or 1D-array): The property we interested in.
199
+
200
+ """
201
+ # input data
202
+ self.x = x
203
+ self.y = y
204
+
205
+ # the total number of samples
206
+ self.n_sample = x.shape[0]
207
+
208
+ # check task
209
+ self.check_task()
210
+
211
+ # Make the sampler behave in a deterministic way.
212
+ sampler = TPESampler(seed=self.optuna_seed)
213
+ self.study = optuna.create_study(direction="maximize", sampler=sampler)
214
+
215
+ print(
216
+ "optuna seed {self.optuna_seed} | validation seed {self.valid_seed} | model seed {self.kernel_seed}"
217
+ .format(self=self))
218
+ print(" {} start tuning. it will take a while.".format(self.name()))
219
+ # using optuna tuning hyper parameter
220
+ self.study.optimize(self.evaluate,
221
+ n_trials=self.n_try,
222
+ show_progress_bar=False)
223
+ self.optuna_model = self.create_model(self.study.best_trial,
224
+ default=False)
225
+
226
+ # using default hyper parameter
227
+ # the best_trial here is only a placeholder. It's not functional.
228
+ self.default_performance = self.evaluate(self.study.best_trial,
229
+ default=True)
230
+ self.default_model = self.create_model(self.study.best_trial,
231
+ default=True)
232
+ #print(" default performance: {:.3f} | best performance: {:.3f}".
233
+ # format(self.default_performance, self.study.best_trial.value))
234
+ if self.default_performance > self.study.best_trial.value:
235
+ # default better
236
+ print(" default is better.")
237
+ self.best_model = self.default_model
238
+ else:
239
+ # optuna better
240
+ print(" optuna is better, best trial: ",
241
+ self.study.best_trial.number)
242
+ self.best_model = self.optuna_model
243
+
244
+ def fit(self, x, y, retune=True):
245
+ """
246
+ The sklearn.base.BaseEstimator fit api.
247
+ Set retune to True to retune the model using the given x and y, else using the previously tuned model to fit on given x and y.
248
+
249
+ Args:
250
+ x (pandas.DataFrame or 2D-array): feature to extract information from.
251
+ y (pandas.Series or 1D-array): ground true.
252
+ retune (bool): True to retune the model using given x and y, else using the tuned model to fit on given x, y.
253
+ """
254
+ self.label_name = y.name
255
+
256
+ # label encoding
257
+ if not self.is_regression():
258
+ y = Series(self.y_mapping.fit_transform(y),
259
+ index=y.index,
260
+ name=y.name)
261
+
262
+ # tune the model.
263
+ if retune:
264
+ self.tune(x, y)
265
+
266
+ # fit the model.
267
+ if self.is_regression():
268
+ sample_weight = None
269
+ else:
270
+ sample_weight = compute_sample_weight(class_weight="balanced", y=y)
271
+ self.best_model.fit(x, y, sample_weight=sample_weight)
272
+
273
+ return self
274
+
275
+ def predict(self, x):
276
+ """
277
+ The sklearn.base.BaseEstimator predict api.
278
+
279
+ Args:
280
+ x (pandas.DataFrame or 2D-array): feature to extract information from.
281
+
282
+ Returns:
283
+ 1D-array: prediction
284
+ """
285
+ # using the model.
286
+ y_pred = self.best_model.predict(x)
287
+ # label decoding
288
+ if not self.is_regression():
289
+ y_pred = self.y_mapping.inverse_transform(y_pred)
290
+ y_pred = Series(y_pred, index=x.index, name=self.label_name)
291
+
292
+ return y_pred
293
+
294
+ def get_scorer(self, scorer_name: str):
295
+ """
296
+ A lazy function to call sklearn scorers.
297
+
298
+ Args:
299
+ scorer_name (str): abbreviation or formal name of sklearn scorers.
300
+
301
+ Returns:
302
+ scorer: A callable object that returns score.
303
+ """
304
+
305
+ ### polymorphism
306
+ scorer_name = scorer_name.lower().replace("-", "_").replace(" ", "_")
307
+
308
+ ### common abbreviation
309
+ nicknames = {
310
+ "acc": "accuracy",
311
+ "auc": "roc_auc",
312
+ "f1_score": "f1",
313
+ "macro_f1": "f1_macro",
314
+ "mcc": "matthews_corrcoef",
315
+ "log_loss": "neg_log_loss",
316
+ "cross_entropy": "neg_log_loss",
317
+ "ce": "neg_log_loss",
318
+ "bce": "neg_log_loss",
319
+ "mse": "neg_mean_squared_error",
320
+ "mean_squared_error": "neg_mean_squared_error",
321
+ "mae": "neg_mean_absolute_error",
322
+ "mean_absolute_error": "neg_mean_absolute_error",
323
+ "mape": "neg_mean_absolute_percentage_error",
324
+ "mean_absolute_percentage_error":
325
+ "neg_mean_absolute_percentage_error",
326
+ "rmse": "neg_root_mean_squared_error",
327
+ "root_mean_squared_error": "neg_root_mean_squared_error",
328
+ "qwk": "quadratic_weighted_kappa",
329
+ "kappa": "cohen_kappa"
330
+ }
331
+ if scorer_name in nicknames:
332
+ scorer_name = nicknames[scorer_name]
333
+
334
+ self.metric_name = scorer_name
335
+
336
+ # get the scorer
337
+ ### kappa
338
+ if scorer_name == "quadratic_weighted_kappa":
339
+ return metrics.make_scorer(metrics.cohen_kappa_score,
340
+ weights="quadratic",
341
+ response_method="predict",
342
+ greater_is_better=True)
343
+ elif scorer_name == "cohen_kappa":
344
+ return metrics.make_scorer(metrics.cohen_kappa_score,
345
+ weights=None,
346
+ response_method="predict",
347
+ greater_is_better=True)
348
+ ### others
349
+ return metrics.get_scorer(scorer_name)
350
+
351
+ def check_task(self) -> bool:
352
+ """
353
+ Uh.... this functions will do
354
+ 1. some check for mis-using of method and types.
355
+ 2. sparsing the task and target.
356
+
357
+ It will be nasty and full of dirty code. Make yourself at home.
358
+
359
+
360
+ Raises:
361
+ ValueError: Check auc score only be used in binary classification
362
+
363
+ Returns:
364
+ bool: _description_
365
+ """
366
+ # Check auc score only be used in binary classification
367
+ is_auc = self.metric_name == "roc_auc"
368
+ is_binary = len(self.y.value_counts()) == 2
369
+ if is_auc and not is_binary:
370
+ # roc_auc only can be used on binary classification. Do not try ovr, ovo. forget them.
371
+ raise ValueError(
372
+ "auc only support binary classification, but more than 2 values are detected in y. Try 'f1_macro'"
373
+ )
374
+
375
+ # Sparse the metric
376
+ scorer_kargs = {}
377
+ for arguments in self.metric.__str__()[12:-1].split(", ")[1:]:
378
+ if "(" in arguments: # for roc auc scorer: response_method=('decision_function', 'predict_proba')
379
+ scorer_kargs["response_method"] = "predict_proba"
380
+ else:
381
+ arg = arguments.split("=")
382
+ if len(arg) == 2:
383
+ scorer_kargs[arg[0]] = arg[1]
384
+
385
+ ### Response method
386
+ self.metric_using_proba = scorer_kargs["response_method"].find(
387
+ "_proba") != -1
388
+ scorer_kargs.pop("response_method")
389
+
390
+ ### Greater is better
391
+ if 'greater_is_better' in scorer_kargs:
392
+ self.metric_great_better = not scorer_kargs[
393
+ "greater_is_better"] == "False"
394
+ scorer_kargs.pop('greater_is_better')
395
+ else:
396
+ self.metric_great_better = True
397
+
398
+ ### pos_label for f1 socres
399
+ if "pos_label" in scorer_kargs:
400
+ scorer_kargs.pop("pos_label")
401
+
402
+ self.scorer_kargs = scorer_kargs
403
+
404
+ return True
405
+
406
+ def plot(self):
407
+ from plotly import io
408
+ fig = optuna.visualization.plot_optimization_history(
409
+ self.study, target_name=self.metric_name)
410
+ fig.add_hline(y=self.default_performance,
411
+ line_dash="dot",
412
+ annotation_text="Default setting",
413
+ annotation_position="bottom right")
414
+ io.show(fig)