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_classification_het.py 15 kB

4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442
  1. """
  2. Node classification Het Trainer Implementation
  3. """
  4. from . import register_trainer
  5. from .base import BaseNodeClassificationHetTrainer, EarlyStopping
  6. import torch
  7. from torch.optim.lr_scheduler import (
  8. StepLR,
  9. MultiStepLR,
  10. ExponentialLR,
  11. ReduceLROnPlateau,
  12. )
  13. import torch.nn.functional as F
  14. from ..model import BaseAutoModel
  15. from .evaluation import get_feval, Logloss
  16. from typing import Union
  17. from copy import deepcopy
  18. from sklearn.metrics import f1_score
  19. from ...utils import get_logger
  20. from ...backend import DependentBackend
  21. LOGGER = get_logger("node classification het trainer")
  22. def score(logits, labels):
  23. _, indices = torch.max(logits, dim=1)
  24. prediction = indices.long().cpu().numpy()
  25. labels = labels.cpu().numpy()
  26. accuracy = (prediction == labels).sum() / len(prediction)
  27. micro_f1 = f1_score(labels, prediction, average='micro')
  28. macro_f1 = f1_score(labels, prediction, average='macro')
  29. return accuracy, micro_f1, macro_f1
  30. @register_trainer("NodeClassificationHet")
  31. class NodeClassificationHetTrainer(BaseNodeClassificationHetTrainer):
  32. """
  33. The node classification trainer.
  34. Used to automatically train the node classification problem.
  35. Parameters
  36. ----------
  37. model: ``BaseAutoModel`` or ``str``
  38. The (name of) model used to train and predict.
  39. optimizer: ``Optimizer`` of ``str``
  40. The (name of) optimizer used to train and predict.
  41. lr: ``float``
  42. The learning rate of node classification task.
  43. max_epoch: ``int``
  44. The max number of epochs in training.
  45. early_stopping_round: ``int``
  46. The round of early stop.
  47. device: ``torch.device`` or ``str``
  48. The device where model will be running on.
  49. init: ``bool``
  50. If True(False), the model will (not) be initialized.
  51. """
  52. def __init__(
  53. self,
  54. model: Union[BaseAutoModel, str] = None,
  55. dataset = None,
  56. num_features=None,
  57. num_classes=None,
  58. optimizer=torch.optim.AdamW,
  59. lr=1e-4,
  60. max_epoch=100,
  61. early_stopping_round=100,
  62. weight_decay=1e-4,
  63. device="auto",
  64. init=False,
  65. feval=[Logloss],
  66. loss="nll_loss",
  67. lr_scheduler_type=None,
  68. *args,
  69. **kwargs
  70. ):
  71. super().__init__(
  72. model,
  73. dataset,
  74. num_features,
  75. num_classes,
  76. device=device,
  77. feval=feval,
  78. loss=loss,
  79. )
  80. self.opt_received = optimizer
  81. if isinstance(optimizer, str):
  82. if optimizer.lower() == "adam": self.optimizer = torch.optim.Adam
  83. elif optimizer.lower() == "sgd": self.optimizer = torch.optim.SGD
  84. else: raise ValueError("Currently not support optimizer {}".format(optimizer))
  85. elif isinstance(optimizer, type) and issubclass(optimizer, torch.optim.Optimizer):
  86. self.optimizer = optimizer
  87. else:
  88. raise ValueError("Currently not support optimizer {}".format(optimizer))
  89. self.lr_scheduler_type = lr_scheduler_type
  90. self.lr = lr
  91. self.max_epoch = max_epoch
  92. self.early_stopping_round = early_stopping_round
  93. self.args = args
  94. self.kwargs = kwargs
  95. self.weight_decay = weight_decay
  96. self.early_stopping = EarlyStopping(
  97. patience=early_stopping_round, verbose=False
  98. )
  99. self.valid_result = None
  100. self.valid_result_prob = None
  101. self.valid_score = None
  102. self.pyg_dgl = DependentBackend.get_backend_name()
  103. self.hyper_parameter_space = [
  104. {
  105. "parameterName": "max_epoch",
  106. "type": "INTEGER",
  107. "maxValue": 500,
  108. "minValue": 10,
  109. "scalingType": "LINEAR",
  110. },
  111. {
  112. "parameterName": "early_stopping_round",
  113. "type": "INTEGER",
  114. "maxValue": 30,
  115. "minValue": 10,
  116. "scalingType": "LINEAR",
  117. },
  118. {
  119. "parameterName": "lr",
  120. "type": "DOUBLE",
  121. "maxValue": 1e-1,
  122. "minValue": 1e-4,
  123. "scalingType": "LOG",
  124. },
  125. {
  126. "parameterName": "weight_decay",
  127. "type": "DOUBLE",
  128. "maxValue": 1e-2,
  129. "minValue": 1e-4,
  130. "scalingType": "LOG",
  131. },
  132. ]
  133. self.hyper_parameters = {
  134. "max_epoch": self.max_epoch,
  135. "early_stopping_round": self.early_stopping_round,
  136. "lr": self.lr,
  137. "weight_decay": self.weight_decay,
  138. }
  139. if init is True:
  140. self.initialize()
  141. def _initialize(self):
  142. self.encoder.initialize()
  143. @classmethod
  144. def get_task_name(cls):
  145. return "NodeClassificationHet"
  146. def _train_only(self, dataset, train_mask="train"):
  147. G = dataset[0].to(self.device)
  148. field = dataset.schema["target_node_type"]
  149. labels = G.nodes[field].data['label'].to(self.device)
  150. train_mask = self._get_mask(dataset, train_mask).to(self.device)
  151. val_mask = self._get_mask(dataset, "val").to(self.device)
  152. model = self.encoder.model.to(self.device)
  153. optimizer = self.optimizer(
  154. model.parameters(), lr=self.lr, weight_decay=self.weight_decay
  155. )
  156. lr_scheduler_type = self.lr_scheduler_type
  157. if type(lr_scheduler_type) == str and lr_scheduler_type == "steplr":
  158. scheduler = StepLR(optimizer, step_size=100, gamma=0.1)
  159. elif type(lr_scheduler_type) == str and lr_scheduler_type == "multisteplr":
  160. scheduler = MultiStepLR(optimizer, milestones=[30, 80], gamma=0.1)
  161. elif type(lr_scheduler_type) == str and lr_scheduler_type == "exponentiallr":
  162. scheduler = ExponentialLR(optimizer, gamma=0.1)
  163. elif (
  164. type(lr_scheduler_type) == str and lr_scheduler_type == "reducelronplateau"
  165. ):
  166. scheduler = ReduceLROnPlateau(optimizer, "min")
  167. else:
  168. scheduler = None
  169. for epoch in range(1, self.max_epoch):
  170. model.train()
  171. optimizer.zero_grad()
  172. logits = model(G)
  173. if hasattr(F, self.loss):
  174. loss = getattr(F, self.loss)(logits[train_mask], labels[train_mask])
  175. else:
  176. raise TypeError(
  177. "PyTorch does not support loss type {}".format(self.loss)
  178. )
  179. loss.backward()
  180. optimizer.step()
  181. if self.lr_scheduler_type:
  182. scheduler.step()
  183. if val_mask is not None:
  184. if type(self.feval) is list:
  185. feval = self.feval[0]
  186. else:
  187. feval = self.feval
  188. val_loss = self.evaluate(dataset, mask=val_mask, feval=feval)
  189. if feval.is_higher_better() is True:
  190. val_loss = -val_loss
  191. self.early_stopping(val_loss, model)
  192. if self.early_stopping.early_stop:
  193. LOGGER.debug("Early stopping at %d", epoch)
  194. break
  195. if val_mask is not None:
  196. self.early_stopping.load_checkpoint(model)
  197. def _predict_only(self, dataset, mask=None):
  198. model = self.encoder.model.to(self.device)
  199. model.eval()
  200. G = dataset[0].to(self.device)
  201. with torch.no_grad():
  202. res = model(G)
  203. if mask is None:
  204. return res
  205. else:
  206. return res[mask]
  207. def train(self, dataset, keep_valid_result=True, train_mask="train"):
  208. """
  209. The function of training on the given dataset and keeping valid result.
  210. Parameters
  211. ----------
  212. dataset: The node classification dataset used to be trained.
  213. keep_valid_result: ``bool``
  214. If True(False), save the validation result after training.
  215. train_mask: The mask for training data
  216. Returns
  217. -------
  218. self: ``autogl.train.NodeClassificationTrainer``
  219. A reference of current trainer.
  220. """
  221. self._train_only(dataset, train_mask)
  222. G = dataset[0].to(self.device)
  223. if keep_valid_result:
  224. # generate labels
  225. val_mask = G.nodes[dataset.schema["target_node_type"]].data["val_mask"]
  226. self.valid_result = self._predict_only(dataset)[val_mask].max(1)[1]
  227. self.valid_result_prob = self._predict_only(dataset)[val_mask]
  228. self.valid_score = self.evaluate(
  229. dataset, mask=val_mask, feval=self.feval
  230. )
  231. # print(self.valid_score)
  232. def predict(self, dataset, mask="test"):
  233. """
  234. The function of predicting on the given dataset.
  235. Parameters
  236. ----------
  237. dataset: The node classification dataset used to be predicted.
  238. mask: ``train``, ``val``, or ``test``.
  239. The dataset mask.
  240. Returns
  241. -------
  242. The prediction result of ``predict_proba``.
  243. """
  244. return self.predict_proba(dataset, mask=mask, in_log_format=True).max(1)[1]
  245. def predict_proba(self, dataset, mask="test", in_log_format=False):
  246. """
  247. The function of predicting the probability on the given dataset.
  248. Parameters
  249. ----------
  250. dataset: The node classification dataset used to be predicted.
  251. mask: ``train``, ``val``, ``test``, or ``Tensor``.
  252. The dataset mask.
  253. in_log_format: ``bool``.
  254. If True(False), the probability will (not) be log format.
  255. Returns
  256. -------
  257. The prediction result.
  258. """
  259. G = dataset[0].to(self.device)
  260. if mask in ["train", "val", "test"]:
  261. mask = G.nodes[dataset.schema["target_node_type"]].data[f"{mask}_mask"]
  262. ret = self._predict_only(dataset, mask)
  263. if in_log_format is True:
  264. return ret
  265. else:
  266. return torch.exp(ret)
  267. def get_valid_predict(self):
  268. # """Get the valid result."""
  269. return self.valid_result
  270. def get_valid_predict_proba(self):
  271. # """Get the valid result (prediction probability)."""
  272. return self.valid_result_prob
  273. def get_valid_score(self, return_major=True):
  274. """
  275. The function of getting the valid score.
  276. Parameters
  277. ----------
  278. return_major: ``bool``.
  279. If True, the return only consists of the major result.
  280. If False, the return consists of the all results.
  281. Returns
  282. -------
  283. result: The valid score in training stage.
  284. """
  285. if isinstance(self.feval, list):
  286. if return_major:
  287. return self.valid_score[0], self.feval[0].is_higher_better()
  288. else:
  289. return self.valid_score, [f.is_higher_better() for f in self.feval]
  290. else:
  291. return self.valid_score, self.feval.is_higher_better()
  292. def __repr__(self) -> str:
  293. import yaml
  294. return yaml.dump(
  295. {
  296. "trainer_name": self.__class__.__name__,
  297. "optimizer": self.optimizer,
  298. "learning_rate": self.lr,
  299. "max_epoch": self.max_epoch,
  300. "early_stopping_round": self.early_stopping_round,
  301. "model": repr(self.model.model),
  302. }
  303. )
  304. def _get_mask(self, dataset, mask):
  305. if mask in ["train", "val", "test"]:
  306. return dataset[0].nodes[dataset.schema["target_node_type"]].data[f"{mask}_mask"]
  307. return mask
  308. def evaluate(self, dataset, mask='val', feval = None):
  309. """
  310. The function of training on the given dataset and keeping valid result.
  311. Parameters
  312. ----------
  313. dataset: The node classification dataset used to be evaluated.
  314. mask: ``train``, ``val``, or ``test``.
  315. The dataset mask.
  316. feval: ``str``.
  317. The evaluation method used in this function.
  318. Returns
  319. -------
  320. res: The evaluation result on the given dataset.
  321. """
  322. G = dataset[0].to(self.device)
  323. mask = self._get_mask(dataset, mask)
  324. label = G.nodes[dataset.schema["target_node_type"]].data['label'].to(self.device)
  325. if feval is None:
  326. feval = self.feval
  327. else:
  328. feval = get_feval(feval)
  329. y_pred_prob = self.predict_proba(dataset, mask)
  330. y_true = label[mask] if mask is not None else label
  331. if not isinstance(feval, list):
  332. feval = [feval]
  333. return_signle = True
  334. else:
  335. return_signle = False
  336. res = []
  337. for f in feval:
  338. try:
  339. res.append(f.evaluate(y_pred_prob, y_true))
  340. except:
  341. res.append(f.evaluate(y_pred_prob.cpu().numpy(), y_true.cpu().numpy()))
  342. if return_signle:
  343. return res[0]
  344. return res
  345. def to(self, new_device):
  346. self.device = new_device
  347. if self.model is not None:
  348. self.model.to(self.device)
  349. def duplicate_from_hyper_parameter(self, hp: dict, model=None, restricted=True):
  350. """
  351. The function of duplicating a new instance from the given hyperparameter.
  352. Parameters
  353. ----------
  354. hp: ``dict``.
  355. The hyperparameter used in the new instance.
  356. model: The model used in the new instance of trainer.
  357. restricted: ``bool``.
  358. If False(True), the hyperparameter should (not) be updated from origin hyperparameter.
  359. Returns
  360. -------
  361. self: ``autogl.train.NodeClassificationTrainer``
  362. A new instance of trainer.
  363. """
  364. trainer_hp = hp["trainer"]
  365. model_hp = hp["encoder"]
  366. if not restricted:
  367. origin_hp = deepcopy(self.hyper_parameters)
  368. origin_hp.update(trainer_hp)
  369. trainer_hp = origin_hp
  370. if model is None:
  371. model = self.model
  372. model = model.from_hyper_parameter(model_hp)
  373. ret = self.__class__(
  374. model=model,
  375. dataset=self._dataset,
  376. num_features=self.num_features,
  377. num_classes=self.num_classes,
  378. optimizer=self.opt_received,
  379. lr=trainer_hp["lr"],
  380. max_epoch=trainer_hp["max_epoch"],
  381. early_stopping_round=trainer_hp["early_stopping_round"],
  382. device=self.device,
  383. weight_decay=trainer_hp["weight_decay"],
  384. feval=self.feval,
  385. loss=self.loss,
  386. lr_scheduler_type=self.lr_scheduler_type,
  387. init=True,
  388. *self.args,
  389. **self.kwargs
  390. )
  391. return ret