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 34 kB

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