Skip to content
Open
Show file tree
Hide file tree
Changes from 32 commits
Commits
Show all changes
61 commits
Select commit Hold shift + click to select a range
0a0a4c6
Change readme to trigger test
Mar 15, 2022
002683f
add dependencies for AG
Mar 15, 2022
60a847c
add user permission to test_notebook_example L81
Mar 15, 2022
60a9e27
add mlflow dependency to setup
Mar 16, 2022
bc7f38d
add textpredictor estimator and test
Mar 16, 2022
f9ca56b
new estimator, no test file
Mar 16, 2022
fe0ecbb
Update automl.py
Qiaochu-Song Mar 16, 2022
4a52ac7
Update automl.py
Qiaochu-Song Mar 16, 2022
30cc834
add test with gc, narrow down mxnet version
Mar 16, 2022
14e6720
Merge branch 'test_main' of github.com:Qiaochu-Song/FLAML into test_main
Mar 16, 2022
6b75a73
skip test for py3.6 and win+py3.8, loose mxnet ver
Mar 16, 2022
d10945e
no ag on windows, remove mlflow dependency
Mar 16, 2022
06f64b2
no ag on windows, remove mlflow dependency
Mar 16, 2022
c9ff3d4
test with direct return
Mar 17, 2022
e7b6f6d
debug without new test
Mar 17, 2022
2307b37
w/o os.environ setting in new test, direct return
Mar 17, 2022
bf3203b
debug, import only in new test
Mar 17, 2022
10c93b2
move new test to automl
Mar 17, 2022
53b5f09
move new test to test/nlp/
Mar 17, 2022
ee3cacb
pass data with X_train
Mar 21, 2022
8096a89
pr fixes, debugging
Mar 24, 2022
fed989b
update with upstream
Mar 24, 2022
c40af7d
Rename to MultimodalEstimator, pr fix
Mar 24, 2022
d0b3b11
remove comment
Mar 24, 2022
30e9f60
Update data.py
Qiaochu-Song Mar 25, 2022
d15dd60
fix bug
Mar 25, 2022
6c42839
Merge branch 'new-test2' of github.com:Qiaochu-Song/FLAML into new-test2
Mar 25, 2022
301eb16
remove useless import
Mar 25, 2022
c59a3b2
remove useless import
Mar 25, 2022
f04b69e
Merge branch 'new-test2' of github.com:Qiaochu-Song/FLAML into new-test2
Mar 25, 2022
2f07223
resolve conflict
Mar 28, 2022
ea515d2
remove task mapping for AG
Mar 28, 2022
6cc2f9e
use 0.5 threshold for text/cat inference
Apr 13, 2022
4cc2b4e
add MM_TASKS; no preprocess on X; pass val_data for early stopping
Apr 14, 2022
4fa136d
adjust testing data and raise budget
Apr 14, 2022
c5d9914
Merge remote-tracking branch 'upstream/main' into new-test2
Apr 14, 2022
25c1baf
shrink test toy data and budget
Apr 14, 2022
f9d3b22
change to regression test
Apr 14, 2022
c1568b4
add metric to kwargs for mm in train_estimator, raise test budget
Apr 14, 2022
1e4201d
use valid data if any for early stopping, raise test budget
Apr 15, 2022
9692d4e
return to the original budget
Apr 15, 2022
1b2cb28
fix valid DF checking
Apr 16, 2022
05941bc
simplify isinstance in ml.py
Apr 18, 2022
984d000
Merge remote-tracking branch 'upstream/main' into new-test2
Apr 18, 2022
74f27b5
reduce text column and budget
Apr 19, 2022
c8848c7
use only 4-row toy test data
Apr 19, 2022
7be2c5c
test 10s budget
Apr 19, 2022
1c7f7ad
minimize test toy dataset
Apr 19, 2022
be60fa6
shorter test sentence
Apr 19, 2022
3a29c5b
give enough test budget
Apr 20, 2022
543b660
give enough test budget
Apr 20, 2022
4296129
solve conflict
May 4, 2022
ca30eab
Merge branch 'mxtextpredictor' of github.com:Qiaochu-Song/FLAML into …
May 6, 2022
5bd061f
add pytorch backend support
May 12, 2022
2b150e7
set pytorch backend to default
May 19, 2022
505c894
pytorch backend support only
May 19, 2022
cd98daf
solve merge conflict
May 19, 2022
98ee138
test remove os and python ver constraints
May 19, 2022
ff8c078
no support for python 3.6
May 19, 2022
24a5333
no support for python 3.6 or windows
May 19, 2022
2aeb563
Merge branch 'main' into mxtextpredictor
Qiaochu-Song May 20, 2022
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .github/workflows/python-package.yml
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,10 @@ jobs:
run: |
pip install -e .[ray,forecast]
pip install 'tensorboardX<=2.2'
- name: If python version > 3.6 and not on windows, install autogluon
if: matrix.python-version >= '3.7' && (matrix.os == 'macOS-latest' || matrix.os == 'ubuntu-latest')
run: |
pip install -e .[autogluon]
- name: Lint with flake8
run: |
# stop the build if there are Python syntax errors or undefined names
Expand Down
27 changes: 17 additions & 10 deletions flaml/data.py
Original file line number Diff line number Diff line change
Expand Up @@ -272,7 +272,8 @@ def fit_transform(self, X: Union[DataFrame, np.array], y, task):
elif isinstance(X, DataFrame):
X = X.copy()
n = X.shape[0]
cat_columns, num_columns, datetime_columns = [], [], []
# NOTE: add str_columns here
str_columns, cat_columns, num_columns, datetime_columns = [], [], [], []
drop = False
if task in TS_FORECAST:
X = X.rename(columns={X.columns[0]: TS_TIMESTAMP_COL})
Expand All @@ -282,13 +283,17 @@ def fit_transform(self, X: Union[DataFrame, np.array], y, task):
for column in X.columns:
# sklearn\utils\validation.py needs int/float values
if X[column].dtype.name in ("object", "category"):
if (
X[column].nunique() == 1
or X[column].nunique(dropna=True)
== n - X[column].isnull().sum()
):
if X[column].nunique() == 1:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why removing X[column].nunique(dropna=True) == n - X[column].isnull().sum()?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

X[column].nunique(dropna=True) == n - X[column].isnull().sum() will remove the column if it does not contain not-null repeated values, e.g., text columns.

X.drop(columns=column, inplace=True)
drop = True
elif X[column].nunique(dropna=True) >= int((n - X[column].isnull().sum()) * 0.9):

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what does this if condition do?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it is a condition for a text column: if the #unique columns >=90% of #not-null columns. I added a NOTE comment for this condition. If we hope a text column to have all unique values, I can modify it to the X[column].nunique(dropna=True) == n - X[column].isnull().sum() version.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

2 TODOs: first, if the column type is already set to str, no need to run this if condition. If it's not set to str, but meet this condition, set it to str, and inform the user by logger.info()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

logger.info -> logger.warning

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

2 TODOs: first, if the column type is already set to str, no need to run this if condition. If it's not set to str, but meet this condition, set it to str, and inform the user by logger.info()

There is no "str" dtype for DataFrame column, but "object" instead. If I check whether a cell is str by isinstance(X[column].iloc[0], str), then a categorical column will also return True.

# NOTE: here a threshold is applied for distinguishing str vs. cat
# if no threshold wanted => requires every non-nan str entry to be different
# delete the line above and uncomment below
# elif X[column].nunique(dropna=True) == n - X[column].isnull().sum():
# NOTE: here detects str fields and do fillna with ""
X[column] = X[column].fillna("")
str_columns.append(column)
elif X[column].dtype.name == "category":
current_categories = X[column].cat.categories
if "__NAN__" not in current_categories:
Expand Down Expand Up @@ -330,7 +335,7 @@ def fit_transform(self, X: Union[DataFrame, np.array], y, task):
del tmp_dt
X[column] = X[column].fillna(np.nan)
num_columns.append(column)
X = X[cat_columns + num_columns]
X = X[str_columns + cat_columns + num_columns]
if task in TS_FORECAST:
X.insert(0, TS_TIMESTAMP_COL, ds_col)
if cat_columns:
Expand Down Expand Up @@ -359,7 +364,8 @@ def fit_transform(self, X: Union[DataFrame, np.array], y, task):
]
)
X[num_columns] = self.transformer.fit_transform(X_num)
self._cat_columns, self._num_columns, self._datetime_columns = (
self.str_columns, self._cat_columns, self._num_columns, self._datetime_columns = (

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
self.str_columns, self._cat_columns, self._num_columns, self._datetime_columns = (
self._str_columns, self._cat_columns, self._num_columns, self._datetime_columns = (

str_columns,
cat_columns,
num_columns,
datetime_columns,
Expand Down Expand Up @@ -400,7 +406,8 @@ def transform(self, X: Union[DataFrame, np.array]):
if len(self._str_columns) > 0:
X[self._str_columns] = X[self._str_columns].astype("string")
elif isinstance(X, DataFrame):
cat_columns, num_columns, datetime_columns = (
str_columns, cat_columns, num_columns, datetime_columns = (
self.str_columns,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
self.str_columns,
self._str_columns,

self._cat_columns,
self._num_columns,
self._datetime_columns,
Expand All @@ -426,7 +433,7 @@ def transform(self, X: Union[DataFrame, np.array]):
X[new_col_name] = new_col_value
X[column] = X[column].map(datetime.toordinal)
del tmp_dt
X = X[cat_columns + num_columns].copy()
X = X[str_columns + cat_columns + num_columns].copy()
if self._task in TS_FORECAST:
X.insert(0, TS_TIMESTAMP_COL, ds_col)
for column in cat_columns:
Expand Down
6 changes: 6 additions & 0 deletions flaml/ml.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
ARIMA,
SARIMAX,
TransformersEstimator,
MultiModalEstimator,
)
from .data import CLASSIFICATION, group_counts, TS_FORECAST, TS_VALUE_COL
import logging
Expand Down Expand Up @@ -121,6 +122,8 @@ def get_estimator_class(task, estimator_name):
estimator_class = SARIMAX
elif estimator_name == "transformer":
estimator_class = TransformersEstimator
elif estimator_name == "multimodal":
estimator_class = MultiModalEstimator
else:
raise ValueError(
estimator_name + " is not a built-in learner. "
Expand Down Expand Up @@ -575,6 +578,9 @@ def compute_estimator(
fit_kwargs["X_val"] = X_val
fit_kwargs["y_val"] = y_val

elif isinstance(estimator, MultiModalEstimator):
fit_kwargs["metric"] = eval_metric

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

metric is passed in ml.py not automl.py. Please ctrl+F fit_kwargs["metric"] in ml.py

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This modification is in ml.py. I added this elif so that the metric can be passed to the multimodal estimator.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see, thanks!


if "holdout" == eval_method:
val_loss, metric_for_logging, train_time, pred_time = get_val_loss(
config_dic,
Expand Down
123 changes: 123 additions & 0 deletions flaml/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -2099,6 +2099,129 @@ class XGBoostLimitDepth_TS(TS_SKLearn):
base_class = XGBoostLimitDepthEstimator


class MultiModalEstimator(BaseEstimator):
"""
The class for tuning AutoGluon TextPredictor
"""
@classmethod
def search_space(cls, **params):
"""
Add the possible search space configs here, e.g. 'optimization.lr'
reference:
https://auto.gluon.ai/stable/tutorials/text_prediction/customization.html#custom-hyperparameter-values
"""
# TODO: expand the search space
search_space_dict = {
"model.network.agg_net.mid_units": {
"domain": tune.choice(list(range(32, 129))),
"init_value": 128,
},
"optimization.lr": {
"domain": tune.loguniform(lower=1E-5, upper=1E-4),
"init_value": 1E-4,
},
"optimization.wd": {
"domain": tune.choice([1E-4, 1E-3, 1E-2]),
"init_value": 1E-4,
},
"optimization.warmup_portion": {
"domain": tune.choice([0.1, 0.2]),
"init_value": 0.1,
},
"optimization.layerwise_lr_decay": {
"domain": tune.choice([0.8, 0.9]),
"init_value": 0.8,
},
"optimization.nbest": {
"domain": tune.choice([2, 3, 4,]),
"init_value": 3,
},
"optimization.num_train_epochs": {
"domain": tune.choice([5, 10, 15,]),
"init_value": 10,
},
"optimization.per_device_batch_size": {
"domain": tune.choice([2, 4, 8,]),
"init_value": 10,
},
"optimization.batch_size": {
"domain": tune.choice([32, 64, 128,]),
"init_value": 128,
},
}
return search_space_dict

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There were only 4 hyperparameters and now there are 9. Which one was the search space used in your original experiment for autogluon?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The original four are "model.network.agg_net.mid_units", "optimization.warmup_portion", "optimization.lr", "optimization.wd".


def _init_ag_args(self, automl_fit_kwargs: dict = None):
from .nlp.utils import AGArgs

ag_args = AGArgs()
for key, val in automl_fit_kwargs["ag_args"].items():
assert (
key in ag_args.__dict__
), "The specified key {} is not in the argument list of flaml.nlp.utils::AGArgs".format(
key
)
setattr(ag_args, key, val)
self.ag_args = ag_args

def _set_seed(self, seed):
import random
import mxnet as mx
# NOTE: if support pytorch backend, uncomment below
# import torch as th
# th.manual_seed(seed)
mx.random.seed(seed)
np.random.seed(seed)
random.seed(seed)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please try if you can reproduce the result


def fit(self, X_train=None, y_train=None, budget=None, **kwargs):
from autogluon.text import TextPredictor

self._kwargs = kwargs
self._init_ag_args(kwargs)
seed = self._kwargs.get("seed", 123)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why 123?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

They used this value for their experiment.

self._set_seed(seed)

assert (self.ag_args.backend == "mxnet"), "the pytorch automm model is not supported. "
# get & set the hyperparameters, update with self.params
hyperparameters = self.ag_args.get_presets()
search_space = hyperparameters["models"]["MultimodalTextModel"]["search_space"]
for key, value in self.params.items():

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of a for loop, try implement using one line with dict.update()

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are two problems here:

  1. flaml uses np.float64 but AG uses float, need to convert or else will raise error;
  2. there will be a "n_jobs" in self.params, which should not be passed to "search_space" of "hyperparameters".

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

both problems can be solved using python conditional value assignment https://stackoverflow.com/questions/6402311/python-conditional-assignment-operator

# NOTE: FLAML uses np.float64 but AG uses float, need to transform
if key == "n_jobs":
continue
elif isinstance(value, np.float64):
search_space[key] = value.item()
else:
search_space[key] = value
start_time = time.time()
self._model = TextPredictor(path=self.ag_args.output_dir,
label="label",
problem_type=self._task,
eval_metric=kwargs["metric"],
backend=self.ag_args.backend)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

don't leave the self._model in trainable. Suppose to clean the self._model

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See the example in TransformersEstimator

train_data = TransformersEstimator._join(X_train, y_train)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
train_data = TransformersEstimator._join(X_train, y_train)
train_data = self._join(X_train, y_train)

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The MultimodalEstimator currently do not have a _join method (since it's a child class of the BaseEstimator). Shall I add one to it, or just use the TransformersEstimator's static method _join()?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

move TransformersEstimator._join -> BaseEstimator._join, keep it as a static function, and replace every TransformersEstimator._join with BaseEstimator._join

self._model.fit(train_data=train_data,
hyperparameters=hyperparameters,
time_limit=budget,
seed=seed)

training_time = time.time() - start_time
return training_time

def predict(self, X):
output = self._model.predict(X, as_pandas=False)
return output

def predict_proba(self, X):
# only works for classification tasks
assert (
self._task in CLASSIFICATION
), "predict_proba() only for classification tasks."
output = self._model.predict_proba(X, as_pandas=False)
return output


class suppress_stdout_stderr(object):
def __init__(self):
# Open a pair of null files
Expand Down
81 changes: 81 additions & 0 deletions flaml/nlp/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -632,3 +632,84 @@ def load_args_from_console():
)
console_args, unknown = arg_parser.parse_known_args()
return console_args


@dataclass
class AGArgs:
"""
The Autogluon configurations
Args:
output_dir (str): data root directory for outputing the log and intermediate data, model.
backend (str, optional, defaults to "mxnet"): currently only support to mxnet.
text_backbone (str, optional, defaults to "electra_base"): the text backbone model.
multimodal_fusion_strategy (str, optional, defaults to "fuse_late"): the fuse strategy.
"""
output_dir: str = field(
default="data/mm/output/", metadata={"help": "data dir", "required": True}
)
backend: str = field(default="mxnet", metadata={"help": "the backend of the multimodal model"})
text_backbone: str = field(default="electra_base", metadata={"help": "text backbone model"})
multimodal_fusion_strategy: str = field(default="fuse_late", metadata={"help": "fusion strategy"})
# TODO: determine whether to tune these HPs
# per_device_batch_size: int = field(default=8, metadata={"help": "per device batch size"})
# num_train_epochs: int = field(default=10, metadata={"help": "number of train epochs"})
# batch_size: int = field(default=128, metadata={"help": "batch size"})


def get_presets(self):
"""
Get the preset using the AGArgs.
{'models': {'MultimodalTextModel': {'backend': 'gluonnlp_v0',

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this an example of the return? If so please move it as an example after the "Returns"

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, will fix.

'search_space': {'model.backbone.name': 'google_electra_small',
'model.network.agg_net.agg_type': 'concat',
'model.network.agg_net.mid_units': 128, # [in HPO example]
'model.network.aggregate_categorical': True,
'model.use_avg_nbest': True,
'optimization.batch_size': 128,
'optimization.layerwise_lr_decay': 0.8,
'optimization.lr': Categorical[0.0001],
'optimization.nbest': 3,
'optimization.num_train_epochs': 10,
'optimization.per_device_batch_size': 8,
'optimization.wd': 0.0001,
'optimization.warmup_portion': 0.1, # [in HPO example]
'preprocessing.categorical.convert_to_text': False,
'preprocessing.numerical.convert_to_text': False}}},
'tune_kwargs': {'num_trials': 1,
'scheduler_options': None,
'search_options': None,
'search_strategy': 'local',
'searcher': 'random'}}
Ref: https://auto.gluon.ai/0.3.1/tutorials/text_prediction/customization.html
Return:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Return:
Returns:

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed

hyperparameters: a Dict of the preset hyperparameter settings.
"""
from autogluon.text.text_prediction.legacy_presets import ag_text_presets

base_key = f'{self.text_backbone}_{self.multimodal_fusion_strategy}'
hyperparameters = ag_text_presets.create(base_key)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you do a post_init function, set hyperparameters as an argument (append to after line 652), and replace this line and next with self.hyperparameters = ag_text_presets.create(base_key)

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can remove this get_presets method and use post_init instead to get the self.hyperparameters. Is that correct?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes. __post_init returns void, so just set hyperparameter to one data field so you can access it outside of AGArgs.

# NOTE: set anything else that would like to be set via ag_args here
return hyperparameters


@staticmethod
def load_args():
from dataclasses import fields

arg_parser = argparse.ArgumentParser()
for each_field in fields(AGArgs):
print(each_field)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

remove

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed

arg_parser.add_argument(
"--" + each_field.name,
type=each_field.type,
help=each_field.metadata["help"],
required=each_field.metadata["required"]
if "required" in each_field.metadata
else False,
choices=each_field.metadata["choices"]
if "choices" in each_field.metadata
else None,
default=each_field.default,
)
console_args, unknown = arg_parser.parse_known_args()
return console_args
5 changes: 5 additions & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,11 @@
"hcrystalball==0.1.10",
"seqeval",
],
"autogluon": [
"mxnet<2.0.0",
"autogluon.text==0.4.0",
"autogluon.features==0.4.0",
],
"catboost": ["catboost>=0.26"],
"blendsearch": ["optuna==2.8.0"],
"ray": [
Expand Down
Loading