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.

ged_model.py 20 kB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724
  1. #!/usr/bin/env python3
  2. # -*- coding: utf-8 -*-
  3. """
  4. Created on Thu May 5 09:42:30 2022
  5. @author: ljia
  6. """
  7. import sys
  8. import multiprocessing
  9. import time
  10. import numpy as np
  11. import networkx as nx
  12. # from abc import ABC, abstractmethod
  13. from sklearn.base import BaseEstimator # , TransformerMixin
  14. from sklearn.utils.validation import check_is_fitted # check_X_y, check_array,
  15. from sklearn.exceptions import NotFittedError
  16. from gklearn.ged.model.distances import euclid_d
  17. from gklearn.ged.util import pairwise_ged, get_nb_edit_operations
  18. # from gklearn.utils import normalize_gram_matrix
  19. from gklearn.utils import get_iters
  20. class GEDModel(BaseEstimator): #, ABC):
  21. """The graph edit distance model class compatible with `scikit-learn`.
  22. Attributes
  23. ----------
  24. _graphs : list
  25. Stores the input graphs on fit input data.
  26. Default format of the list objects is `NetworkX` graphs.
  27. **We don't guarantee that the input graphs remain unchanged during the
  28. computation.**
  29. References
  30. ----------
  31. https://ysig.github.io/GraKeL/0.1a8/_modules/grakel/kernels/kernel.html#Kernel.
  32. """
  33. def __init__(self,
  34. ed_method='BIPARTITE',
  35. edit_cost_fun='CONSTANT',
  36. init_edit_cost_constants=[3, 3, 1, 3, 3, 1],
  37. optim_method='init',
  38. optim_options={'y_distance': euclid_d, 'mode': 'reg'},
  39. node_labels=[],
  40. edge_labels=[],
  41. parallel=None,
  42. n_jobs=None,
  43. chunksize=None,
  44. # normalize=True,
  45. copy_graphs=True, # make sure it is a full deep copy. and faster!
  46. verbose=2):
  47. """`__init__` for `GEDModel` object."""
  48. # @todo: the default settings of the parameters are different from those in the self.compute method.
  49. # self._graphs = None
  50. self.ed_method = ed_method
  51. self.edit_cost_fun = edit_cost_fun
  52. self.init_edit_cost_constants = init_edit_cost_constants
  53. self.optim_method=optim_method
  54. self.optim_options=optim_options
  55. self.node_labels=node_labels
  56. self.edge_labels=edge_labels
  57. self.parallel = parallel
  58. self.n_jobs = n_jobs
  59. self.chunksize = chunksize
  60. # self.normalize = normalize
  61. self.copy_graphs = copy_graphs
  62. self.verbose = verbose
  63. # self._run_time = 0
  64. # self._gram_matrix = None
  65. # self._gram_matrix_unnorm = None
  66. ##########################################################################
  67. # The following is the 1st paradigm to compute GED distance matrix, which is
  68. # compatible with `scikit-learn`.
  69. ##########################################################################
  70. def fit(self, X, y=None):
  71. """Fit a graph dataset for a transformer.
  72. Parameters
  73. ----------
  74. X : iterable
  75. DESCRIPTION.
  76. y : None, optional
  77. There is no need of a target in a transformer, yet the `scikit-learn`
  78. pipeline API requires this parameter.
  79. Returns
  80. -------
  81. object
  82. Returns self.
  83. """
  84. # self._is_tranformed = False
  85. # Clear any prior attributes stored on the estimator, # @todo: unless warm_start is used;
  86. self.clear_attributes()
  87. # Validate parameters for the transformer.
  88. self.validate_parameters()
  89. # Validate the input.
  90. self._graphs = self.validate_input(X)
  91. if y is not None:
  92. self._targets = y
  93. # self._targets = self.validate_input(y)
  94. # self._X = X
  95. # self._kernel = self._get_kernel_instance()
  96. # Return the transformer.
  97. return self
  98. def transform(self, X=None, return_dm_train=False):
  99. """Compute the graph kernel matrix between given and fitted data.
  100. Parameters
  101. ----------
  102. X : TYPE
  103. DESCRIPTION.
  104. Raises
  105. ------
  106. ValueError
  107. DESCRIPTION.
  108. Returns
  109. -------
  110. None.
  111. """
  112. # If `return_dm_train`, return the fitted GED distance matrix of training data.
  113. if return_dm_train:
  114. check_is_fitted(self, '_dm_train')
  115. self._is_transformed = True
  116. return self._dm_train # @todo: copy or not?
  117. # Check if method "fit" had been called.
  118. check_is_fitted(self, '_graphs')
  119. # Validate the input.
  120. Y = self.validate_input(X)
  121. # Transform: compute the graph kernel matrix.
  122. dis_matrix = self.compute_distance_matrix(Y)
  123. self._Y = Y
  124. # Self transform must appear before the diagonal call on normilization.
  125. self._is_transformed = True
  126. # if self.normalize:
  127. # X_diag, Y_diag = self.diagonals()
  128. # old_settings = np.seterr(invalid='raise') # Catch FloatingPointError: invalid value encountered in sqrt.
  129. # try:
  130. # kernel_matrix /= np.sqrt(np.outer(Y_diag, X_diag))
  131. # except:
  132. # raise
  133. # finally:
  134. # np.seterr(**old_settings)
  135. return dis_matrix
  136. def fit_transform(self, X, y=None, save_dm_train=False):
  137. """Fit and transform: compute GED distance matrix on the same data.
  138. Parameters
  139. ----------
  140. X : list of graphs
  141. Input graphs.
  142. Returns
  143. -------
  144. dis_matrix : numpy array, shape = [len(X), len(X)]
  145. The distance matrix of X.
  146. """
  147. self.fit(X, y)
  148. # Compute edit cost constants.
  149. self.compute_edit_costs()
  150. # Transform: compute Gram matrix.
  151. dis_matrix = self.compute_distance_matrix()
  152. # # Normalize.
  153. # if self.normalize:
  154. # self._X_diag = np.diagonal(gram_matrix).copy()
  155. # old_settings = np.seterr(invalid='raise') # Catch FloatingPointError: invalid value encountered in sqrt.
  156. # try:
  157. # gram_matrix /= np.sqrt(np.outer(self._X_diag, self._X_diag))
  158. # except:
  159. # raise
  160. # finally:
  161. # np.seterr(**old_settings)
  162. if save_dm_train:
  163. self._dm_train = dis_matrix
  164. return dis_matrix
  165. def get_params(self):
  166. pass
  167. def set_params(self):
  168. pass
  169. def clear_attributes(self): # @todo: update
  170. # if hasattr(self, '_X_diag'):
  171. # delattr(self, '_X_diag')
  172. if hasattr(self, '_graphs'):
  173. delattr(self, '_graphs')
  174. if hasattr(self, '_Y'):
  175. delattr(self, '_Y')
  176. if hasattr(self, '_run_time'):
  177. delattr(self, '_run_time')
  178. def validate_parameters(self):
  179. """Validate all parameters for the transformer.
  180. Returns
  181. -------
  182. None.
  183. """
  184. if self.parallel is not None and self.parallel != 'imap_unordered':
  185. raise ValueError('Parallel mode is not set correctly.')
  186. if self.parallel == 'imap_unordered' and self.n_jobs is None:
  187. self.n_jobs = multiprocessing.cpu_count()
  188. def validate_input(self, X):
  189. """Validate the given input and raise errors if it is invalid.
  190. Parameters
  191. ----------
  192. X : list
  193. The input to check. Should be a list of graph.
  194. Raises
  195. ------
  196. ValueError
  197. Raise if the input is not correct.
  198. Returns
  199. -------
  200. X : list
  201. The input. A list of graph.
  202. """
  203. if X is None:
  204. raise ValueError('Please add graphs before computing.')
  205. elif not isinstance(X, list):
  206. raise ValueError('Cannot detect graphs. The input must be a list.')
  207. elif len(X) == 0:
  208. raise ValueError('The graph list given is empty. No computation will be performed.')
  209. return X
  210. def compute_distance_matrix(self, Y=None):
  211. """Compute the distance matrix between a given target graphs (Y) and
  212. the fitted graphs (X / self._graphs) or the distance matrix for the fitted
  213. graphs (X / self._graphs).
  214. Parameters
  215. ----------
  216. Y : list of graphs, optional
  217. The target graphs. The default is None. If None kernel is computed
  218. between X and itself.
  219. Returns
  220. -------
  221. kernel_matrix : numpy array, shape = [n_targets, n_inputs]
  222. The computed kernel matrix.
  223. """
  224. if Y is None:
  225. # Compute Gram matrix for self._graphs (X).
  226. dis_matrix = self._compute_X_distance_matrix()
  227. # self._gram_matrix_unnorm = np.copy(self._gram_matrix)
  228. else:
  229. # Compute kernel matrix between Y and self._graphs (X).
  230. start_time = time.time()
  231. if self.parallel == 'imap_unordered':
  232. dis_matrix = self._compute_distance_matrix_imap_unordered(Y)
  233. elif self.parallel is None:
  234. Y_copy = ([g.copy() for g in Y] if self.copy_graphs else Y)
  235. graphs_copy = ([g.copy() for g in self._graphs] if self.copy_graphs else self._graphs)
  236. dis_matrix = self._compute_distance_matrix_series(Y_copy, graphs_copy)
  237. self._run_time = time.time() - start_time
  238. if self.verbose:
  239. print('Distance matrix of size (%d, %d) built in %s seconds.'
  240. % (len(Y), len(self._graphs), self._run_time))
  241. return dis_matrix
  242. def _compute_distance_matrix_series(self, X, Y):
  243. """Compute the GED distance matrix between two sets of graphs (X and Y)
  244. without parallelization.
  245. Parameters
  246. ----------
  247. X, Y : list of graphs
  248. The input graphs.
  249. Returns
  250. -------
  251. dis_matrix : numpy array, shape = [n_X, n_Y]
  252. The computed distance matrix.
  253. """
  254. dis_matrix = np.zeros((len(X), len(Y)))
  255. for i_x, g_x in enumerate(X):
  256. for i_y, g_y in enumerate(Y):
  257. dis_matrix[i_x, i_y], _ = self.compute_ged(g_x, g_y)
  258. return dis_matrix
  259. def _compute_kernel_matrix_imap_unordered(self, Y):
  260. """Compute the kernel matrix between a given target graphs (Y) and
  261. the fitted graphs (X / self._graphs) using imap unordered parallelization.
  262. Parameters
  263. ----------
  264. Y : list of graphs, optional
  265. The target graphs.
  266. Returns
  267. -------
  268. kernel_matrix : numpy array, shape = [n_targets, n_inputs]
  269. The computed kernel matrix.
  270. """
  271. raise Exception('Parallelization for kernel matrix is not implemented.')
  272. def diagonals(self):
  273. """Compute the kernel matrix diagonals of the fit/transformed data.
  274. Returns
  275. -------
  276. X_diag : numpy array
  277. The diagonal of the kernel matrix between the fitted data.
  278. This consists of each element calculated with itself.
  279. Y_diag : numpy array
  280. The diagonal of the kernel matrix, of the transform.
  281. This consists of each element calculated with itself.
  282. """
  283. # Check if method "fit" had been called.
  284. check_is_fitted(self, ['_graphs'])
  285. # Check if the diagonals of X exist.
  286. try:
  287. check_is_fitted(self, ['_X_diag'])
  288. except NotFittedError:
  289. # Compute diagonals of X.
  290. self._X_diag = np.empty(shape=(len(self._graphs),))
  291. graphs = ([g.copy() for g in self._graphs] if self.copy_graphs else self._graphs)
  292. for i, x in enumerate(graphs):
  293. self._X_diag[i] = self.pairwise_kernel(x, x) # @todo: parallel?
  294. try:
  295. # If transform has happened, return both diagonals.
  296. check_is_fitted(self, ['_Y'])
  297. self._Y_diag = np.empty(shape=(len(self._Y),))
  298. Y = ([g.copy() for g in self._Y] if self.copy_graphs else self._Y)
  299. for (i, y) in enumerate(Y):
  300. self._Y_diag[i] = self.pairwise_kernel(y, y) # @todo: parallel?
  301. return self._X_diag, self._Y_diag
  302. except NotFittedError:
  303. # Else just return both X_diag
  304. return self._X_diag
  305. # @abstractmethod
  306. def pairwise_distance(self, x, y):
  307. """Compute pairwise kernel between two graphs.
  308. Parameters
  309. ----------
  310. x, y : NetworkX Graph.
  311. Graphs bewteen which the kernel is computed.
  312. Returns
  313. -------
  314. kernel: float
  315. The computed kernel.
  316. # Notes
  317. # -----
  318. # This method is abstract and must be implemented by a subclass.
  319. """
  320. raise NotImplementedError('Pairwise kernel computation is not implemented!')
  321. def compute_edit_costs(self, Y=None, Y_targets=None):
  322. """Compute edit cost constants. When optimizing method is `fiited`,
  323. apply Jia2021's metric learning method by using a given target graphs (Y)
  324. the fitted graphs (X / self._graphs).
  325. Parameters
  326. ----------
  327. Y : TYPE, optional
  328. DESCRIPTION. The default is None.
  329. Returns
  330. -------
  331. None.
  332. """
  333. # Get or compute.
  334. if self.optim_method == 'random':
  335. self._edit_cost_constants = np.random.rand(6)
  336. elif self.optim_method == 'init':
  337. self._edit_cost_constants = self.init_edit_cost_constants
  338. elif self.optim_method == 'expert':
  339. self._edit_cost_constants = [3, 3, 1, 3, 3, 1]
  340. elif self.optim_method == 'fitted': # Jia2021 method
  341. # Get proper inputs.
  342. if Y is None:
  343. check_is_fitted(self, ['_graphs'])
  344. check_is_fitted(self, ['_targets'])
  345. graphs = ([g.copy() for g in self._graphs] if self.copy_graphs else self._graphs)
  346. targets = self._targets
  347. else:
  348. graphs = ([g.copy() for g in Y] if self.copy_graphs else Y)
  349. targets = Y_targets
  350. # Get optimization options.
  351. node_labels = self.node_labels
  352. edge_labels = self.edge_labels
  353. unlabeled = (len(node_labels) == 0 and len(edge_labels) == 0)
  354. from gklearn.ged.model.optim_costs import compute_optimal_costs
  355. self._edit_cost_constants = compute_optimal_costs(
  356. graphs, targets,
  357. node_labels=node_labels, edge_labels=edge_labels,
  358. unlabeled=unlabeled, ed_method=self.ed_method,
  359. verbose=(self.verbose >= 2),
  360. **self.optim_options)
  361. ##########################################################################
  362. # The following is the 2nd paradigm to compute kernel matrix. It is
  363. # simplified and not compatible with `scikit-learn`.
  364. ##########################################################################
  365. # def compute(self, *graphs, **kwargs):
  366. # self.parallel = kwargs.get('parallel', 'imap_unordered')
  367. # self.n_jobs = kwargs.get('n_jobs', multiprocessing.cpu_count())
  368. # self.normalize = kwargs.get('normalize', True)
  369. # self.verbose = kwargs.get('verbose', 2)
  370. # self.copy_graphs = kwargs.get('copy_graphs', True)
  371. # self.save_unnormed = kwargs.get('save_unnormed', True)
  372. # self.validate_parameters()
  373. # # If the inputs is a list of graphs.
  374. # if len(graphs) == 1:
  375. # if not isinstance(graphs[0], list):
  376. # raise Exception('Cannot detect graphs.')
  377. # elif len(graphs[0]) == 0:
  378. # raise Exception('The graph list given is empty. No computation was performed.')
  379. # else:
  380. # if self.copy_graphs:
  381. # self._graphs = [g.copy() for g in graphs[0]] # @todo: might be very slow.
  382. # else:
  383. # self._graphs = graphs
  384. # self._gram_matrix = self._compute_gram_matrix()
  385. # if self.save_unnormed:
  386. # self._gram_matrix_unnorm = np.copy(self._gram_matrix)
  387. # if self.normalize:
  388. # self._gram_matrix = normalize_gram_matrix(self._gram_matrix)
  389. # return self._gram_matrix, self._run_time
  390. # elif len(graphs) == 2:
  391. # # If the inputs are two graphs.
  392. # if self.is_graph(graphs[0]) and self.is_graph(graphs[1]):
  393. # if self.copy_graphs:
  394. # G0, G1 = graphs[0].copy(), graphs[1].copy()
  395. # else:
  396. # G0, G1 = graphs[0], graphs[1]
  397. # kernel = self._compute_single_kernel(G0, G1)
  398. # return kernel, self._run_time
  399. # # If the inputs are a graph and a list of graphs.
  400. # elif self.is_graph(graphs[0]) and isinstance(graphs[1], list):
  401. # if self.copy_graphs:
  402. # g1 = graphs[0].copy()
  403. # g_list = [g.copy() for g in graphs[1]]
  404. # kernel_list = self._compute_kernel_list(g1, g_list)
  405. # else:
  406. # kernel_list = self._compute_kernel_list(graphs[0], graphs[1])
  407. # return kernel_list, self._run_time
  408. # elif isinstance(graphs[0], list) and self.is_graph(graphs[1]):
  409. # if self.copy_graphs:
  410. # g1 = graphs[1].copy()
  411. # g_list = [g.copy() for g in graphs[0]]
  412. # kernel_list = self._compute_kernel_list(g1, g_list)
  413. # else:
  414. # kernel_list = self._compute_kernel_list(graphs[1], graphs[0])
  415. # return kernel_list, self._run_time
  416. # else:
  417. # raise Exception('Cannot detect graphs.')
  418. # elif len(graphs) == 0 and self._graphs is None:
  419. # raise Exception('Please add graphs before computing.')
  420. # else:
  421. # raise Exception('Cannot detect graphs.')
  422. # def normalize_gm(self, gram_matrix):
  423. # import warnings
  424. # warnings.warn('gklearn.kernels.graph_kernel.normalize_gm will be deprecated, use gklearn.utils.normalize_gram_matrix instead', DeprecationWarning)
  425. # diag = gram_matrix.diagonal().copy()
  426. # for i in range(len(gram_matrix)):
  427. # for j in range(i, len(gram_matrix)):
  428. # gram_matrix[i][j] /= np.sqrt(diag[i] * diag[j])
  429. # gram_matrix[j][i] = gram_matrix[i][j]
  430. # return gram_matrix
  431. # def compute_distance_matrix(self):
  432. # if self._gram_matrix is None:
  433. # raise Exception('Please compute the Gram matrix before computing distance matrix.')
  434. # dis_mat = np.empty((len(self._gram_matrix), len(self._gram_matrix)))
  435. # for i in range(len(self._gram_matrix)):
  436. # for j in range(i, len(self._gram_matrix)):
  437. # dis = self._gram_matrix[i, i] + self._gram_matrix[j, j] - 2 * self._gram_matrix[i, j]
  438. # if dis < 0:
  439. # if dis > -1e-10:
  440. # dis = 0
  441. # else:
  442. # raise ValueError('The distance is negative.')
  443. # dis_mat[i, j] = np.sqrt(dis)
  444. # dis_mat[j, i] = dis_mat[i, j]
  445. # dis_max = np.max(np.max(dis_mat))
  446. # dis_min = np.min(np.min(dis_mat[dis_mat != 0]))
  447. # dis_mean = np.mean(np.mean(dis_mat))
  448. # return dis_mat, dis_max, dis_min, dis_mean
  449. def _compute_X_distance_matrix(self):
  450. start_time = time.time()
  451. if self.parallel == 'imap_unordered':
  452. dis_matrix = self._compute_X_dm_imap_unordered()
  453. elif self.parallel is None:
  454. graphs = ([g.copy() for g in self._graphs] if self.copy_graphs else self._graphs)
  455. dis_matrix = self._compute_X_dm_series(graphs)
  456. else:
  457. raise Exception('Parallel mode is not set correctly.')
  458. self._run_time = time.time() - start_time
  459. if self.verbose:
  460. print('Distance matrix of size %d built in %s seconds.'
  461. % (len(self._graphs), self._run_time))
  462. return dis_matrix
  463. def _compute_X_dm_series(self, graphs):
  464. N = len(graphs)
  465. dis_matrix = np.zeros((N, N))
  466. for i, G1 in get_iters(enumerate(graphs), desc='Computing distance matrix', file=sys.stdout, verbose=(self.verbose >= 2)):
  467. for j, G2 in enumerate(graphs[i+1:], i+1):
  468. dis_matrix[i, j], _ = self.compute_ged(G1, G2)
  469. dis_matrix[j, i] = dis_matrix[i, j]
  470. return dis_matrix
  471. def _compute_X_dm_imap_unordered(self, graphs):
  472. pass
  473. def compute_ged(self, Gi, Gj, **kwargs):
  474. """
  475. Compute GED between two graph according to edit_cost.
  476. """
  477. ged_options = {'edit_cost': self.edit_cost_fun,
  478. 'method': self.ed_method,
  479. 'edit_cost_constants': self._edit_cost_constants}
  480. dis, pi_forward, pi_backward = pairwise_ged(Gi, Gj, ged_options, repeats=10)
  481. n_eo_tmp = get_nb_edit_operations(Gi, Gj, pi_forward, pi_backward,
  482. edit_cost=self.edit_cost_fun,
  483. node_labels=self.node_labels,
  484. edge_labels=self.edge_labels)
  485. return dis, n_eo_tmp
  486. # def _compute_kernel_list(self, g1, g_list):
  487. # start_time = time.time()
  488. # if self.parallel == 'imap_unordered':
  489. # kernel_list = self._compute_kernel_list_imap_unordered(g1, g_list)
  490. # elif self.parallel is None:
  491. # kernel_list = self._compute_kernel_list_series(g1, g_list)
  492. # else:
  493. # raise Exception('Parallel mode is not set correctly.')
  494. # self._run_time = time.time() - start_time
  495. # if self.verbose:
  496. # print('Graph kernel bewteen a graph and a list of %d graphs built in %s seconds.'
  497. # % (len(g_list), self._run_time))
  498. # return kernel_list
  499. # def _compute_kernel_list_series(self, g1, g_list):
  500. # pass
  501. # def _compute_kernel_list_imap_unordered(self, g1, g_list):
  502. # pass
  503. # def _compute_single_kernel(self, g1, g2):
  504. # start_time = time.time()
  505. # kernel = self._compute_single_kernel_series(g1, g2)
  506. # self._run_time = time.time() - start_time
  507. # if self.verbose:
  508. # print('Graph kernel bewteen two graphs built in %s seconds.' % (self._run_time))
  509. # return kernel
  510. # def _compute_single_kernel_series(self, g1, g2):
  511. # pass
  512. def is_graph(self, graph):
  513. if isinstance(graph, nx.Graph):
  514. return True
  515. if isinstance(graph, nx.DiGraph):
  516. return True
  517. if isinstance(graph, nx.MultiGraph):
  518. return True
  519. if isinstance(graph, nx.MultiDiGraph):
  520. return True
  521. return False
  522. @property
  523. def graphs(self):
  524. return self._graphs
  525. # @property
  526. # def parallel(self):
  527. # return self.parallel
  528. # @property
  529. # def n_jobs(self):
  530. # return self.n_jobs
  531. # @property
  532. # def verbose(self):
  533. # return self.verbose
  534. # @property
  535. # def normalize(self):
  536. # return self.normalize
  537. @property
  538. def run_time(self):
  539. return self._run_time
  540. @property
  541. def dis_matrix(self):
  542. return self._dis_matrix
  543. @dis_matrix.setter
  544. def dis_matrix(self, value):
  545. self._dis_matrix = value
  546. # @property
  547. # def gram_matrix_unnorm(self):
  548. # return self._gram_matrix_unnorm
  549. # @gram_matrix_unnorm.setter
  550. # def gram_matrix_unnorm(self, value):
  551. # self._gram_matrix_unnorm = value

A Python package for graph kernels, graph edit distances and graph pre-image problem.