You can not select more than 25 topics Topics must start with a chinese character,a letter or number, can include dashes ('-') and can be up to 35 characters long.

node_classifier.py 27 kB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701
  1. """
  2. Auto Classfier for Node Classification
  3. """
  4. import time
  5. import json
  6. from copy import deepcopy
  7. import torch
  8. import numpy as np
  9. import yaml
  10. from .base import BaseClassifier
  11. from ...module.feature import FEATURE_DICT
  12. from ...module.model import MODEL_DICT
  13. from ...module.train import TRAINER_DICT, get_feval
  14. from ...module import BaseModel
  15. from ..utils import Leaderboard, set_seed
  16. from ...datasets import utils
  17. from ...utils import get_logger
  18. LOGGER = get_logger("NodeClassifier")
  19. class AutoNodeClassifier(BaseClassifier):
  20. """
  21. Auto Multi-class Graph Node Classifier.
  22. Used to automatically solve the node classification problems.
  23. Parameters
  24. ----------
  25. feature_module: autogl.module.feature.BaseFeatureEngineer or str or None
  26. The (name of) auto feature engineer used to process the given dataset. Default ``deepgl``.
  27. Disable feature engineer by setting it to ``None``.
  28. graph_models: list of autogl.module.model.BaseModel or list of str
  29. The (name of) models to be optimized as backbone. Default ``['gat', 'gcn']``.
  30. hpo_module: autogl.module.hpo.BaseHPOptimizer or str or None
  31. The (name of) hpo module used to search for best hyper parameters. Default ``anneal``.
  32. Disable hpo by setting it to ``None``.
  33. ensemble_module: autogl.module.ensemble.BaseEnsembler or str or None
  34. The (name of) ensemble module used to ensemble the multi-models found. Default ``voting``.
  35. Disable ensemble by setting it to ``None``.
  36. max_evals: int (Optional)
  37. If given, will set the number eval times the hpo module will use.
  38. Only be effective when hpo_module is ``str``. Default ``None``.
  39. trainer_hp_space: list of dict (Optional)
  40. trainer hp space or list of trainer hp spaces configuration.
  41. If a single trainer hp is given, will specify the hp space of trainer for every model.
  42. If a list of trainer hp is given, will specify every model with corrsponding
  43. trainer hp space.
  44. Default ``None``.
  45. model_hp_spaces: list of list of dict (Optional)
  46. model hp space configuration.
  47. If given, will specify every hp space of every passed model. Default ``None``.
  48. size: int (Optional)
  49. The max models ensemble module will use. Default ``None``.
  50. device: torch.device or str
  51. The device where model will be running on. If set to ``auto``, will use gpu when available.
  52. You can also specify the device by directly giving ``gpu`` or ``cuda:0``, etc.
  53. Default ``auto``.
  54. """
  55. # pylint: disable=W0102
  56. def __init__(
  57. self,
  58. feature_module="deepgl",
  59. graph_models=["gat", "gcn"],
  60. hpo_module="anneal",
  61. ensemble_module="voting",
  62. max_evals=50,
  63. trainer_hp_space=None,
  64. model_hp_spaces=None,
  65. size=4,
  66. device="auto",
  67. ):
  68. super().__init__(
  69. feature_module=feature_module,
  70. graph_models=graph_models,
  71. hpo_module=hpo_module,
  72. ensemble_module=ensemble_module,
  73. max_evals=max_evals,
  74. trainer_hp_space=trainer_hp_space,
  75. model_hp_spaces=model_hp_spaces,
  76. size=size,
  77. device=device,
  78. )
  79. # data to be kept when fit
  80. self.data = None
  81. def _init_graph_module(
  82. self,
  83. graph_models,
  84. num_classes,
  85. num_features,
  86. *args,
  87. **kwargs,
  88. ) -> "AutoNodeClassifier":
  89. # load graph network module
  90. self.graph_model_list = []
  91. if isinstance(graph_models, list):
  92. for model in graph_models:
  93. if isinstance(model, str):
  94. if model in MODEL_DICT:
  95. self.graph_model_list.append(
  96. MODEL_DICT[model](
  97. num_classes=num_classes,
  98. num_features=num_features,
  99. *args,
  100. **kwargs,
  101. init=False,
  102. )
  103. )
  104. else:
  105. raise KeyError("cannot find model %s" % (model))
  106. elif isinstance(model, type) and issubclass(model, BaseModel):
  107. self.graph_model_list.append(
  108. model(
  109. num_classes=num_classes,
  110. num_features=num_features,
  111. *args,
  112. **kwargs,
  113. init=False,
  114. )
  115. )
  116. elif isinstance(model, BaseModel):
  117. # setup the hp of num_classes and num_features
  118. model.set_num_classes(num_classes)
  119. model.set_num_features(num_features)
  120. self.graph_model_list.append(model)
  121. else:
  122. raise KeyError("cannot find graph network %s." % (model))
  123. else:
  124. raise ValueError(
  125. "need graph network to be (list of) str or a BaseModel class/instance, get",
  126. graph_models,
  127. "instead.",
  128. )
  129. # wrap all model_cls with specified trainer
  130. for i, model in enumerate(self.graph_model_list):
  131. if self._model_hp_spaces is not None:
  132. if self._model_hp_spaces[i] is not None:
  133. model.hyper_parameter_space = self._model_hp_spaces[i]
  134. trainer = TRAINER_DICT["NodeClassification"](
  135. model=model,
  136. num_features=num_features,
  137. num_classes=num_classes,
  138. *args,
  139. **kwargs,
  140. init=False,
  141. )
  142. if self._trainer_hp_space is not None:
  143. if isinstance(self._trainer_hp_space[0], list):
  144. current_hp_for_trainer = self._trainer_hp_space[i]
  145. else:
  146. current_hp_for_trainer = self._trainer_hp_space
  147. trainer.hyper_parameter_space = (
  148. current_hp_for_trainer + model.hyper_parameter_space
  149. )
  150. self.graph_model_list[i] = trainer
  151. return self
  152. # pylint: disable=arguments-differ
  153. def fit(
  154. self,
  155. dataset,
  156. time_limit=-1,
  157. inplace=False,
  158. train_split=None,
  159. val_split=None,
  160. balanced=True,
  161. evaluation_method="infer",
  162. seed=None,
  163. ) -> "AutoNodeClassifier":
  164. """
  165. Fit current solver on given dataset.
  166. Parameters
  167. ----------
  168. dataset: torch_geometric.data.dataset.Dataset
  169. The dataset needed to fit on. This dataset must have only one graph.
  170. time_limit: int
  171. The time limit of the whole fit process (in seconds). If set below 0,
  172. will ignore time limit. Default ``-1``.
  173. inplace: bool
  174. Whether we process the given dataset in inplace manner. Default ``False``.
  175. Set it to True if you want to save memory by modifying the given dataset directly.
  176. train_split: float or int (Optional)
  177. The train ratio (in ``float``) or number (in ``int``) of dataset. If you want to
  178. use default train/val/test split in dataset, please set this to ``None``.
  179. Default ``None``.
  180. val_split: float or int (Optional)
  181. The validation ratio (in ``float``) or number (in ``int``) of dataset. If you want
  182. to use default train/val/test split in dataset, please set this to ``None``.
  183. Default ``None``.
  184. balanced: bool
  185. Wether to create the train/valid/test split in a balanced way.
  186. If set to ``True``, the train/valid will have the same number of different classes.
  187. Default ``True``.
  188. evaluation_method: (list of) str or autogl.module.train.evaluation
  189. A (list of) evaluation method for current solver. If ``infer``, will automatically
  190. determine. Default ``infer``.
  191. seed: int (Optional)
  192. The random seed. If set to ``None``, will run everything at random.
  193. Default ``None``.
  194. Returns
  195. -------
  196. self: autogl.solver.AutoNodeClassifier
  197. A reference of current solver.
  198. """
  199. set_seed(seed)
  200. if time_limit < 0:
  201. time_limit = 3600 * 24
  202. time_begin = time.time()
  203. # initialize leaderboard
  204. if evaluation_method == "infer":
  205. if hasattr(dataset, "metric"):
  206. evaluation_method = [dataset.metric]
  207. else:
  208. num_of_label = dataset.num_classes
  209. if num_of_label == 2:
  210. evaluation_method = ["auc"]
  211. else:
  212. evaluation_method = ["acc"]
  213. assert isinstance(evaluation_method, list)
  214. evaluator_list = get_feval(evaluation_method)
  215. self.leaderboard = Leaderboard(
  216. [e.get_eval_name() for e in evaluator_list],
  217. {e.get_eval_name(): e.is_higher_better() for e in evaluator_list},
  218. )
  219. # set up the dataset
  220. if train_split is not None and val_split is not None:
  221. size = dataset.data.x.shape[0]
  222. if balanced:
  223. train_split = (
  224. train_split if train_split > 1 else int(train_split * size)
  225. )
  226. val_split = val_split if val_split > 1 else int(val_split * size)
  227. utils.random_splits_mask_class(
  228. dataset,
  229. num_train_per_class=train_split // dataset.num_classes,
  230. num_val_per_class=val_split // dataset.num_classes,
  231. seed=seed,
  232. )
  233. else:
  234. train_split = train_split if train_split < 1 else train_split / size
  235. val_split = val_split if val_split < 1 else val_split / size
  236. utils.random_splits_mask(
  237. dataset, train_ratio=train_split, val_ratio=val_split
  238. )
  239. else:
  240. assert hasattr(dataset.data, "train_mask") and hasattr(
  241. dataset.data, "val_mask"
  242. ), (
  243. "The dataset has no default train/val split! Please manually pass "
  244. "train and val ratio."
  245. )
  246. LOGGER.info("Use the default train/val/test ratio in given dataset")
  247. # feature engineering
  248. if self.feature_module is not None:
  249. dataset = self.feature_module.fit_transform(dataset, inplace=inplace)
  250. self.dataset = dataset
  251. assert self.dataset[0].x is not None, (
  252. "Does not support fit on non node-feature dataset!"
  253. " Please add node features to dataset or specify feature engineers that generate"
  254. " node features."
  255. )
  256. # initialize graph networks
  257. self._init_graph_module(
  258. self.gml,
  259. num_features=self.dataset[0].x.shape[1],
  260. num_classes=dataset.num_classes,
  261. feval=evaluator_list,
  262. device=self.runtime_device,
  263. loss="cross_entropy" if not hasattr(dataset, "loss") else dataset.loss,
  264. )
  265. # train the models and tune hpo
  266. result_valid = []
  267. names = []
  268. for idx, model in enumerate(self.graph_model_list):
  269. time_for_each_model = (time_limit - time.time() + time_begin) / (
  270. len(self.graph_model_list) - idx
  271. )
  272. if self.hpo_module is None:
  273. model.initialize()
  274. model.train(self.dataset, True)
  275. optimized = model
  276. else:
  277. optimized, _ = self.hpo_module.optimize(
  278. trainer=model, dataset=self.dataset, time_limit=time_for_each_model
  279. )
  280. # to save memory, all the trainer derived will be mapped to cpu
  281. optimized.to(torch.device("cpu"))
  282. name = optimized.get_name_with_hp() + "_idx%d" % (idx)
  283. names.append(name)
  284. performance_on_valid, _ = optimized.get_valid_score(return_major=False)
  285. result_valid.append(optimized.get_valid_predict_proba().cpu().numpy())
  286. self.leaderboard.insert_model_performance(
  287. name,
  288. dict(
  289. zip(
  290. [e.get_eval_name() for e in evaluator_list],
  291. performance_on_valid,
  292. )
  293. ),
  294. )
  295. self.trained_models[name] = optimized
  296. # fit the ensemble model
  297. if self.ensemble_module is not None:
  298. performance = self.ensemble_module.fit(
  299. result_valid,
  300. self.dataset[0].y[self.dataset[0].val_mask].cpu().numpy(),
  301. names,
  302. evaluator_list,
  303. n_classes=dataset.num_classes,
  304. )
  305. self.leaderboard.insert_model_performance(
  306. "ensemble",
  307. dict(zip([e.get_eval_name() for e in evaluator_list], performance)),
  308. )
  309. return self
  310. def fit_predict(
  311. self,
  312. dataset,
  313. time_limit=-1,
  314. inplace=False,
  315. train_split=None,
  316. val_split=None,
  317. balanced=True,
  318. evaluation_method="infer",
  319. use_ensemble=True,
  320. use_best=True,
  321. name=None,
  322. ) -> np.ndarray:
  323. """
  324. Fit current solver on given dataset and return the predicted value.
  325. Parameters
  326. ----------
  327. dataset: torch_geometric.data.dataset.Dataset
  328. The dataset needed to fit on. This dataset must have only one graph.
  329. time_limit: int
  330. The time limit of the whole fit process (in seconds).
  331. If set below 0, will ignore time limit. Default ``-1``.
  332. inplace: bool
  333. Whether we process the given dataset in inplace manner. Default ``False``.
  334. Set it to True if you want to save memory by modifying the given dataset directly.
  335. train_split: float or int (Optional)
  336. The train ratio (in ``float``) or number (in ``int``) of dataset. If you want to
  337. use default train/val/test split in dataset, please set this to ``None``.
  338. Default ``None``.
  339. val_split: float or int (Optional)
  340. The validation ratio (in ``float``) or number (in ``int``) of dataset. If you want
  341. to use default train/val/test split in dataset, please set this to ``None``.
  342. Default ``None``.
  343. balanced: bool
  344. Wether to create the train/valid/test split in a balanced way.
  345. If set to ``True``, the train/valid will have the same number of different classes.
  346. Default ``False``.
  347. evaluation_method: (list of) str or autogl.module.train.evaluation
  348. A (list of) evaluation method for current solver. If ``infer``, will automatically
  349. determine. Default ``infer``.
  350. use_ensemble: bool
  351. Whether to use ensemble to do the predict. Default ``True``.
  352. use_best: bool
  353. Whether to use the best single model to do the predict. Will only be effective when
  354. ``use_ensemble`` is ``False``.
  355. Default ``True``.
  356. name: str or None
  357. The name of model used to predict. Will only be effective when ``use_ensemble`` and
  358. ``use_best`` both are ``False``.
  359. Default ``None``.
  360. Returns
  361. -------
  362. result: np.ndarray
  363. An array of shape ``(N,)``, where ``N`` is the number of test nodes. The prediction
  364. on given dataset.
  365. """
  366. self.fit(
  367. dataset=dataset,
  368. time_limit=time_limit,
  369. inplace=inplace,
  370. train_split=train_split,
  371. val_split=val_split,
  372. balanced=balanced,
  373. evaluation_method=evaluation_method,
  374. )
  375. return self.predict(
  376. dataset=dataset,
  377. inplaced=inplace,
  378. inplace=inplace,
  379. use_ensemble=use_ensemble,
  380. use_best=use_best,
  381. name=name,
  382. )
  383. def predict_proba(
  384. self,
  385. dataset=None,
  386. inplaced=False,
  387. inplace=False,
  388. use_ensemble=True,
  389. use_best=True,
  390. name=None,
  391. mask="test",
  392. ) -> np.ndarray:
  393. """
  394. Predict the node probability.
  395. Parameters
  396. ----------
  397. dataset: torch_geometric.data.dataset.Dataset or None
  398. The dataset needed to predict. If ``None``, will use the processed dataset passed
  399. to ``fit()`` instead. Default ``None``.
  400. inplaced: bool
  401. Whether the given dataset is processed. Only be effective when ``dataset``
  402. is not ``None``. If you pass the dataset to ``fit()`` with ``inplace=True``, and
  403. you pass the dataset again to this method, you should set this argument to ``True``.
  404. Otherwise ``False``. Default ``False``.
  405. inplace: bool
  406. Whether we process the given dataset in inplace manner. Default ``False``. Set it to
  407. True if you want to save memory by modifying the given dataset directly.
  408. use_ensemble: bool
  409. Whether to use ensemble to do the predict. Default ``True``.
  410. use_best: bool
  411. Whether to use the best single model to do the predict. Will only be effective when
  412. ``use_ensemble`` is ``False``. Default ``True``.
  413. name: str or None
  414. The name of model used to predict. Will only be effective when ``use_ensemble`` and
  415. ``use_best`` both are ``False``. Default ``None``.
  416. mask: str
  417. The data split to give prediction on. Default ``test``.
  418. Returns
  419. -------
  420. result: np.ndarray
  421. An array of shape ``(N,C,)``, where ``N`` is the number of test nodes and ``C`` is
  422. the number of classes. The prediction on given dataset.
  423. """
  424. if dataset is None:
  425. dataset = self.dataset
  426. assert dataset is not None, (
  427. "Please execute fit() first before" " predicting on remembered dataset"
  428. )
  429. elif not inplaced and self.feature_module is not None:
  430. dataset = self.feature_module.transform(dataset, inplace=inplace)
  431. if use_ensemble:
  432. LOGGER.info("Ensemble argument on, will try using ensemble model.")
  433. if not use_ensemble and use_best:
  434. LOGGER.info(
  435. "Ensemble argument off and best argument on, will try using best model."
  436. )
  437. if (use_ensemble and self.ensemble_module is not None) or (
  438. not use_best and name == "ensemble"
  439. ):
  440. # we need to get all the prediction of every model trained
  441. predict_result = []
  442. names = []
  443. for model_name in self.trained_models:
  444. predict_result.append(
  445. self._predict_proba_by_name(dataset, model_name, mask)
  446. )
  447. names.append(model_name)
  448. return self.ensemble_module.ensemble(predict_result, names)
  449. if use_ensemble and self.ensemble_module is None:
  450. LOGGER.warning(
  451. "Cannot use ensemble because no ensebmle module is given."
  452. "Will use best model instead."
  453. )
  454. if use_best or (use_ensemble and self.ensemble_module is None):
  455. # just return the best model we have found
  456. name = self.leaderboard.get_best_model()
  457. return self._predict_proba_by_name(dataset, name, mask)
  458. if name is not None:
  459. # return model performance by name
  460. return self._predict_proba_by_name(dataset, name, mask)
  461. LOGGER.error(
  462. "No model name is given while ensemble and best arguments are off."
  463. )
  464. raise ValueError(
  465. "You need to specify a model name if you do not want use ensemble and best model."
  466. )
  467. def _predict_proba_by_name(self, dataset, name, mask="test"):
  468. self.trained_models[name].to(self.runtime_device)
  469. predicted = (
  470. self.trained_models[name].predict_proba(dataset, mask=mask).cpu().numpy()
  471. )
  472. self.trained_models[name].to(torch.device("cpu"))
  473. return predicted
  474. def predict(
  475. self,
  476. dataset=None,
  477. inplaced=False,
  478. inplace=False,
  479. use_ensemble=True,
  480. use_best=True,
  481. name=None,
  482. mask="test",
  483. ) -> np.ndarray:
  484. """
  485. Predict the node class number.
  486. Parameters
  487. ----------
  488. dataset: torch_geometric.data.dataset.Dataset or None
  489. The dataset needed to predict. If ``None``, will use the processed dataset passed
  490. to ``fit()`` instead. Default ``None``.
  491. inplaced: bool
  492. Whether the given dataset is processed. Only be effective when ``dataset``
  493. is not ``None``. If you pass the dataset to ``fit()`` with ``inplace=True``,
  494. and you pass the dataset again to this method, you should set this argument
  495. to ``True``. Otherwise ``False``. Default ``False``.
  496. inplace: bool
  497. Whether we process the given dataset in inplace manner. Default ``False``.
  498. Set it to True if you want to save memory by modifying the given dataset directly.
  499. use_ensemble: bool
  500. Whether to use ensemble to do the predict. Default ``True``.
  501. use_best: bool
  502. Whether to use the best single model to do the predict. Will only be effective
  503. when ``use_ensemble`` is ``False``. Default ``True``.
  504. name: str or None
  505. The name of model used to predict. Will only be effective when ``use_ensemble``
  506. and ``use_best`` both are ``False``. Default ``None``.
  507. mask: str
  508. The data split to give prediction on. Default ``test``.
  509. Returns
  510. -------
  511. result: np.ndarray
  512. An array of shape ``(N,)``, where ``N`` is the number of test nodes.
  513. The prediction on given dataset.
  514. """
  515. proba = self.predict_proba(
  516. dataset, inplaced, inplace, use_ensemble, use_best, name, mask
  517. )
  518. return np.argmax(proba, axis=1)
  519. @classmethod
  520. def from_config(cls, path_or_dict, filetype="auto") -> "AutoNodeClassifier":
  521. """
  522. Load solver from config file.
  523. You can use this function to directly load a solver from predefined config dict
  524. or config file path. Currently, only support file type of ``json`` or ``yaml``,
  525. if you pass a path.
  526. Parameters
  527. ----------
  528. path_or_dict: str or dict
  529. The path to the config file or the config dictionary object
  530. filetype: str
  531. The filetype the given file if the path is specified. Currently only support
  532. ``json`` or ``yaml``. You can set to ``auto`` to automatically detect the file
  533. type (from file name). Default ``auto``.
  534. Returns
  535. -------
  536. solver: autogl.solver.AutoGraphClassifier
  537. The solver that is created from given file or dictionary.
  538. """
  539. assert filetype in ["auto", "yaml", "json"], (
  540. "currently only support yaml file or json file type, but get type "
  541. + filetype
  542. )
  543. if isinstance(path_or_dict, str):
  544. if filetype == "auto":
  545. if path_or_dict.endswith(".yaml"):
  546. filetype = "yaml"
  547. elif path_or_dict.endswith(".json"):
  548. filetype = "json"
  549. else:
  550. LOGGER.error(
  551. "cannot parse the type of the given file name, "
  552. "please manually set the file type"
  553. )
  554. raise ValueError(
  555. "cannot parse the type of the given file name, "
  556. "please manually set the file type"
  557. )
  558. if filetype == "yaml":
  559. path_or_dict = yaml.load(
  560. open(path_or_dict, "r").read(), Loader=yaml.FullLoader
  561. )
  562. else:
  563. path_or_dict = json.load(open(path_or_dict, "r"))
  564. path_or_dict = deepcopy(path_or_dict)
  565. solver = cls(None, [], None, None)
  566. fe_list = path_or_dict.pop("feature", [{"name": "deepgl"}])
  567. if fe_list is not None:
  568. fe_list_ele = []
  569. for feature_engineer in fe_list:
  570. name = feature_engineer.pop("name")
  571. if name is not None:
  572. fe_list_ele.append(FEATURE_DICT[name](**feature_engineer))
  573. if fe_list_ele != []:
  574. solver.set_feature_module(fe_list_ele)
  575. models = path_or_dict.pop("models", {"gcn": None, "gat": None})
  576. model_list = list(models.keys())
  577. model_hp_space = [models[m] for m in model_list]
  578. trainer_space = path_or_dict.pop("trainer", None)
  579. if model_hp_space:
  580. # parse lambda function
  581. for space in model_hp_space:
  582. if space is not None:
  583. for keys in space:
  584. if "cutFunc" in keys and isinstance(keys["cutFunc"], str):
  585. keys["cutFunc"] = eval(keys["cutFunc"])
  586. if trainer_space:
  587. for space in trainer_space:
  588. if (
  589. isinstance(space, dict)
  590. and "cutFunc" in space
  591. and isinstance(space["cutFunc"], str)
  592. ):
  593. space["cutFunc"] = eval(space["cutFunc"])
  594. elif space is not None:
  595. for keys in space:
  596. if "cutFunc" in keys and isinstance(keys["cutFunc"], str):
  597. keys["cutFunc"] = eval(keys["cutFunc"])
  598. solver.set_graph_models(model_list, trainer_space, model_hp_space)
  599. hpo_dict = path_or_dict.pop("hpo", {"name": "anneal"})
  600. if hpo_dict is not None:
  601. name = hpo_dict.pop("name")
  602. solver.set_hpo_module(name, **hpo_dict)
  603. ensemble_dict = path_or_dict.pop("ensemble", {"name": "voting"})
  604. if ensemble_dict is not None:
  605. name = ensemble_dict.pop("name")
  606. solver.set_ensemble_module(name, **ensemble_dict)
  607. return solver