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.

spectral_decomposition.py 9.5 kB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269
  1. #!/usr/bin/env python3
  2. # -*- coding: utf-8 -*-
  3. """
  4. Created on Thu Aug 20 16:12:45 2020
  5. @author: ljia
  6. @references:
  7. [1] S Vichy N Vishwanathan, Nicol N Schraudolph, Risi Kondor, and Karsten M Borgwardt. Graph kernels. Journal of Machine Learning Research, 11(Apr):1201–1242, 2010.
  8. """
  9. import sys
  10. from gklearn.utils import get_iters
  11. import numpy as np
  12. import networkx as nx
  13. from scipy.sparse import kron
  14. from gklearn.utils.parallel import parallel_gm, parallel_me
  15. from gklearn.kernels import RandomWalkMeta
  16. class SpectralDecomposition(RandomWalkMeta):
  17. def __init__(self, **kwargs):
  18. super().__init__(**kwargs)
  19. self._sub_kernel = kwargs.get('sub_kernel', None)
  20. def _compute_gm_series(self):
  21. self._check_edge_weight(self._graphs, self.verbose)
  22. self._check_graphs(self._graphs)
  23. if self.verbose >= 2:
  24. import warnings
  25. warnings.warn('All labels are ignored. Only works for undirected graphs.')
  26. # compute Gram matrix.
  27. gram_matrix = np.zeros((len(self._graphs), len(self._graphs)))
  28. if self._q is None:
  29. # precompute the spectral decomposition of each graph.
  30. P_list = []
  31. D_list = []
  32. iterator = get_iters(self._graphs, desc='spectral decompose', file=sys.stdout, verbose=(self.verbose >= 2))
  33. for G in iterator:
  34. # don't normalize adjacency matrices if q is a uniform vector. Note
  35. # A actually is the transpose of the adjacency matrix.
  36. A = nx.adjacency_matrix(G, self._edge_weight).todense().transpose()
  37. ew, ev = np.linalg.eig(A)
  38. D_list.append(ew)
  39. P_list.append(ev)
  40. # P_inv_list = [p.T for p in P_list] # @todo: also works for directed graphs?
  41. if self._p is None: # p is uniform distribution as default.
  42. q_T_list = [np.full((1, nx.number_of_nodes(G)), 1 / nx.number_of_nodes(G)) for G in self._graphs]
  43. # q_T_list = [q.T for q in q_list]
  44. from itertools import combinations_with_replacement
  45. itr = combinations_with_replacement(range(0, len(self._graphs)), 2)
  46. len_itr = int(len(self._graphs) * (len(self._graphs) + 1) / 2)
  47. iterator = get_iters(itr, desc='Computing kernels', file=sys.stdout, length=len_itr, verbose=(self.verbose >= 2))
  48. for i, j in iterator:
  49. kernel = self._kernel_do(q_T_list[i], q_T_list[j], P_list[i], P_list[j], D_list[i], D_list[j], self._weight, self._sub_kernel)
  50. gram_matrix[i][j] = kernel
  51. gram_matrix[j][i] = kernel
  52. else: # @todo
  53. pass
  54. else: # @todo
  55. pass
  56. return gram_matrix
  57. def _compute_gm_imap_unordered(self):
  58. self._check_edge_weight(self._graphs, self.verbose)
  59. self._check_graphs(self._graphs)
  60. if self.verbose >= 2:
  61. import warnings
  62. warnings.warn('All labels are ignored. Only works for undirected graphs.')
  63. # compute Gram matrix.
  64. gram_matrix = np.zeros((len(self._graphs), len(self._graphs)))
  65. if self._q is None:
  66. # precompute the spectral decomposition of each graph.
  67. P_list = []
  68. D_list = []
  69. iterator = get_iters(self._graphs, desc='spectral decompose', file=sys.stdout, verbose=(self.verbose >= 2))
  70. for G in iterator:
  71. # don't normalize adjacency matrices if q is a uniform vector. Note
  72. # A actually is the transpose of the adjacency matrix.
  73. A = nx.adjacency_matrix(G, self._edge_weight).todense().transpose()
  74. ew, ev = np.linalg.eig(A)
  75. D_list.append(ew)
  76. P_list.append(ev) # @todo: parallel?
  77. if self._p is None: # p is uniform distribution as default.
  78. q_T_list = [np.full((1, nx.number_of_nodes(G)), 1 / nx.number_of_nodes(G)) for G in self._graphs] # @todo: parallel?
  79. def init_worker(q_T_list_toshare, P_list_toshare, D_list_toshare):
  80. global G_q_T_list, G_P_list, G_D_list
  81. G_q_T_list = q_T_list_toshare
  82. G_P_list = P_list_toshare
  83. G_D_list = D_list_toshare
  84. do_fun = self._wrapper_kernel_do
  85. parallel_gm(do_fun, gram_matrix, self._graphs, init_worker=init_worker,
  86. glbv=(q_T_list, P_list, D_list), n_jobs=self.n_jobs, verbose=self.verbose)
  87. else: # @todo
  88. pass
  89. else: # @todo
  90. pass
  91. return gram_matrix
  92. def _compute_kernel_list_series(self, g1, g_list):
  93. self._check_edge_weight(g_list + [g1], self.verbose)
  94. self._check_graphs(g_list + [g1])
  95. if self.verbose >= 2:
  96. import warnings
  97. warnings.warn('All labels are ignored. Only works for undirected graphs.')
  98. # compute kernel list.
  99. kernel_list = [None] * len(g_list)
  100. if self._q is None:
  101. # precompute the spectral decomposition of each graph.
  102. A1 = nx.adjacency_matrix(g1, self._edge_weight).todense().transpose()
  103. D1, P1 = np.linalg.eig(A1)
  104. P_list = []
  105. D_list = []
  106. iterator = get_iters(g_list, desc='spectral decompose', file=sys.stdout, verbose=(self.verbose >= 2))
  107. for G in iterator:
  108. # don't normalize adjacency matrices if q is a uniform vector. Note
  109. # A actually is the transpose of the adjacency matrix.
  110. A = nx.adjacency_matrix(G, self._edge_weight).todense().transpose()
  111. ew, ev = np.linalg.eig(A)
  112. D_list.append(ew)
  113. P_list.append(ev)
  114. if self._p is None: # p is uniform distribution as default.
  115. q_T1 = 1 / nx.number_of_nodes(g1)
  116. q_T_list = [np.full((1, nx.number_of_nodes(G)), 1 / nx.number_of_nodes(G)) for G in g_list]
  117. iterator = get_iters(range(len(g_list)), desc='Computing kernels', file=sys.stdout, length=len(g_list), verbose=(self.verbose >= 2))
  118. for i in iterator:
  119. kernel = self._kernel_do(q_T1, q_T_list[i], P1, P_list[i], D1, D_list[i], self._weight, self._sub_kernel)
  120. kernel_list[i] = kernel
  121. else: # @todo
  122. pass
  123. else: # @todo
  124. pass
  125. return kernel_list
  126. def _compute_kernel_list_imap_unordered(self, g1, g_list):
  127. self._check_edge_weight(g_list + [g1], self.verbose)
  128. self._check_graphs(g_list + [g1])
  129. if self.verbose >= 2:
  130. import warnings
  131. warnings.warn('All labels are ignored. Only works for undirected graphs.')
  132. # compute kernel list.
  133. kernel_list = [None] * len(g_list)
  134. if self._q is None:
  135. # precompute the spectral decomposition of each graph.
  136. A1 = nx.adjacency_matrix(g1, self._edge_weight).todense().transpose()
  137. D1, P1 = np.linalg.eig(A1)
  138. P_list = []
  139. D_list = []
  140. if self.verbose >= 2:
  141. iterator = get_iters(g_list, desc='spectral decompose', file=sys.stdout)
  142. else:
  143. iterator = g_list
  144. for G in iterator:
  145. # don't normalize adjacency matrices if q is a uniform vector. Note
  146. # A actually is the transpose of the adjacency matrix.
  147. A = nx.adjacency_matrix(G, self._edge_weight).todense().transpose()
  148. ew, ev = np.linalg.eig(A)
  149. D_list.append(ew)
  150. P_list.append(ev) # @todo: parallel?
  151. if self._p is None: # p is uniform distribution as default.
  152. q_T1 = 1 / nx.number_of_nodes(g1)
  153. q_T_list = [np.full((1, nx.number_of_nodes(G)), 1 / nx.number_of_nodes(G)) for G in g_list] # @todo: parallel?
  154. def init_worker(q_T1_toshare, P1_toshare, D1_toshare, q_T_list_toshare, P_list_toshare, D_list_toshare):
  155. global G_q_T1, G_P1, G_D1, G_q_T_list, G_P_list, G_D_list
  156. G_q_T1 = q_T1_toshare
  157. G_P1 = P1_toshare
  158. G_D1 = D1_toshare
  159. G_q_T_list = q_T_list_toshare
  160. G_P_list = P_list_toshare
  161. G_D_list = D_list_toshare
  162. do_fun = self._wrapper_kernel_list_do
  163. def func_assign(result, var_to_assign):
  164. var_to_assign[result[0]] = result[1]
  165. itr = range(len(g_list))
  166. len_itr = len(g_list)
  167. parallel_me(do_fun, func_assign, kernel_list, itr, len_itr=len_itr,
  168. init_worker=init_worker, glbv=(q_T1, P1, D1, q_T_list, P_list, D_list), method='imap_unordered', n_jobs=self.n_jobs, itr_desc='Computing kernels', verbose=self.verbose)
  169. else: # @todo
  170. pass
  171. else: # @todo
  172. pass
  173. return kernel_list
  174. def _wrapper_kernel_list_do(self, itr):
  175. return itr, self._kernel_do(G_q_T1, G_q_T_list[itr], G_P1, G_P_list[itr], G_D1, G_D_list[itr], self._weight, self._sub_kernel)
  176. def _compute_single_kernel_series(self, g1, g2):
  177. self._check_edge_weight([g1] + [g2], self.verbose)
  178. self._check_graphs([g1] + [g2])
  179. if self.verbose >= 2:
  180. import warnings
  181. warnings.warn('All labels are ignored. Only works for undirected graphs.')
  182. if self._q is None:
  183. # precompute the spectral decomposition of each graph.
  184. A1 = nx.adjacency_matrix(g1, self._edge_weight).todense().transpose()
  185. D1, P1 = np.linalg.eig(A1)
  186. A2 = nx.adjacency_matrix(g2, self._edge_weight).todense().transpose()
  187. D2, P2 = np.linalg.eig(A2)
  188. if self._p is None: # p is uniform distribution as default.
  189. q_T1 = 1 / nx.number_of_nodes(g1)
  190. q_T2 = 1 / nx.number_of_nodes(g2)
  191. kernel = self._kernel_do(q_T1, q_T2, P1, P2, D1, D2, self._weight, self._sub_kernel)
  192. else: # @todo
  193. pass
  194. else: # @todo
  195. pass
  196. return kernel
  197. def _kernel_do(self, q_T1, q_T2, P1, P2, D1, D2, weight, sub_kernel):
  198. # use uniform distribution if there is no prior knowledge.
  199. kl = kron(np.dot(q_T1, P1), np.dot(q_T2, P2)).todense()
  200. # @todo: this is not needed when p = q (kr = kl.T) for undirected graphs.
  201. # kr = kron(np.dot(P_inv_list[i], q_list[i]), np.dot(P_inv_list[j], q_list[j])).todense()
  202. if sub_kernel == 'exp':
  203. D_diag = np.array([d1 * d2 for d1 in D1 for d2 in D2])
  204. kmiddle = np.diag(np.exp(weight * D_diag))
  205. elif sub_kernel == 'geo':
  206. D_diag = np.array([d1 * d2 for d1 in D1 for d2 in D2])
  207. kmiddle = np.diag(weight * D_diag)
  208. kmiddle = np.identity(len(kmiddle)) - weight * kmiddle
  209. kmiddle = np.linalg.inv(kmiddle)
  210. return np.dot(np.dot(kl, kmiddle), kl.T)[0, 0]
  211. def _wrapper_kernel_do(self, itr):
  212. i = itr[0]
  213. j = itr[1]
  214. return i, j, self._kernel_do(G_q_T_list[i], G_q_T_list[j], G_P_list[i], G_P_list[j], G_D_list[i], G_D_list[j], self._weight, self._sub_kernel)

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