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.

treeletKernel.py 21 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
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489
  1. """
  2. @author: linlin
  3. @references:
  4. [1] Gaüzère B, Brun L, Villemin D. Two new graphs kernels in
  5. chemoinformatics. Pattern Recognition Letters. 2012 Nov 1;33(15):2038-47.
  6. """
  7. import sys
  8. sys.path.insert(0, "../")
  9. import time
  10. from collections import Counter
  11. from itertools import chain
  12. from functools import partial
  13. from multiprocessing import Pool
  14. from tqdm import tqdm
  15. import networkx as nx
  16. import numpy as np
  17. from gklearn.utils.graphdataset import get_dataset_attributes
  18. from gklearn.utils.parallel import parallel_gm
  19. def treeletkernel(*args,
  20. sub_kernel,
  21. node_label='atom',
  22. edge_label='bond_type',
  23. parallel='imap_unordered',
  24. n_jobs=None,
  25. verbose=True):
  26. """Calculate treelet graph kernels between graphs.
  27. Parameters
  28. ----------
  29. Gn : List of NetworkX graph
  30. List of graphs between which the kernels are calculated.
  31. G1, G2 : NetworkX graphs
  32. Two graphs between which the kernel is calculated.
  33. sub_kernel : function
  34. The sub-kernel between 2 real number vectors. Each vector counts the
  35. numbers of isomorphic treelets in a graph.
  36. node_label : string
  37. Node attribute used as label. The default node label is atom.
  38. edge_label : string
  39. Edge attribute used as label. The default edge label is bond_type.
  40. parallel : string/None
  41. Which paralleliztion method is applied to compute the kernel. The
  42. Following choices are available:
  43. 'imap_unordered': use Python's multiprocessing.Pool.imap_unordered
  44. method.
  45. None: no parallelization is applied.
  46. n_jobs : int
  47. Number of jobs for parallelization. The default is to use all
  48. computational cores. This argument is only valid when one of the
  49. parallelization method is applied.
  50. Return
  51. ------
  52. Kmatrix : Numpy matrix
  53. Kernel matrix, each element of which is the treelet kernel between 2 praphs.
  54. """
  55. # pre-process
  56. Gn = args[0] if len(args) == 1 else [args[0], args[1]]
  57. Gn = [g.copy() for g in Gn]
  58. Kmatrix = np.zeros((len(Gn), len(Gn)))
  59. ds_attrs = get_dataset_attributes(Gn,
  60. attr_names=['node_labeled', 'edge_labeled', 'is_directed'],
  61. node_label=node_label, edge_label=edge_label)
  62. labeled = False
  63. if ds_attrs['node_labeled'] or ds_attrs['edge_labeled']:
  64. labeled = True
  65. if not ds_attrs['node_labeled']:
  66. for G in Gn:
  67. nx.set_node_attributes(G, '0', 'atom')
  68. if not ds_attrs['edge_labeled']:
  69. for G in Gn:
  70. nx.set_edge_attributes(G, '0', 'bond_type')
  71. start_time = time.time()
  72. # ---- use pool.imap_unordered to parallel and track progress. ----
  73. if parallel == 'imap_unordered':
  74. # get all canonical keys of all graphs before calculating kernels to save
  75. # time, but this may cost a lot of memory for large dataset.
  76. pool = Pool(n_jobs)
  77. itr = zip(Gn, range(0, len(Gn)))
  78. if len(Gn) < 100 * n_jobs:
  79. chunksize = int(len(Gn) / n_jobs) + 1
  80. else:
  81. chunksize = 100
  82. canonkeys = [[] for _ in range(len(Gn))]
  83. get_partial = partial(wrapper_get_canonkeys, node_label, edge_label,
  84. labeled, ds_attrs['is_directed'])
  85. if verbose:
  86. iterator = tqdm(pool.imap_unordered(get_partial, itr, chunksize),
  87. desc='getting canonkeys', file=sys.stdout)
  88. else:
  89. iterator = pool.imap_unordered(get_partial, itr, chunksize)
  90. for i, ck in iterator:
  91. canonkeys[i] = ck
  92. pool.close()
  93. pool.join()
  94. # compute kernels.
  95. def init_worker(canonkeys_toshare):
  96. global G_canonkeys
  97. G_canonkeys = canonkeys_toshare
  98. do_partial = partial(wrapper_treeletkernel_do, sub_kernel)
  99. parallel_gm(do_partial, Kmatrix, Gn, init_worker=init_worker,
  100. glbv=(canonkeys,), n_jobs=n_jobs, verbose=verbose)
  101. # ---- do not use parallelization. ----
  102. elif parallel == None:
  103. # get all canonical keys of all graphs before calculating kernels to save
  104. # time, but this may cost a lot of memory for large dataset.
  105. canonkeys = []
  106. for g in (tqdm(Gn, desc='getting canonkeys', file=sys.stdout) if verbose else Gn):
  107. canonkeys.append(get_canonkeys(g, node_label, edge_label, labeled,
  108. ds_attrs['is_directed']))
  109. # compute kernels.
  110. from itertools import combinations_with_replacement
  111. itr = combinations_with_replacement(range(0, len(Gn)), 2)
  112. for i, j in (tqdm(itr, desc='getting canonkeys', file=sys.stdout) if verbose else itr):
  113. Kmatrix[i][j] = _treeletkernel_do(canonkeys[i], canonkeys[j], sub_kernel)
  114. Kmatrix[j][i] = Kmatrix[i][j] # @todo: no directed graph considered?
  115. else:
  116. raise Exception('No proper parallelization method designated.')
  117. run_time = time.time() - start_time
  118. if verbose:
  119. print("\n --- treelet kernel matrix of size %d built in %s seconds ---"
  120. % (len(Gn), run_time))
  121. return Kmatrix, run_time
  122. def _treeletkernel_do(canonkey1, canonkey2, sub_kernel):
  123. """Calculate treelet graph kernel between 2 graphs.
  124. Parameters
  125. ----------
  126. canonkey1, canonkey2 : list
  127. List of canonical keys in 2 graphs, where each key is represented by a string.
  128. Return
  129. ------
  130. kernel : float
  131. Treelet Kernel between 2 graphs.
  132. """
  133. keys = set(canonkey1.keys()) & set(canonkey2.keys()) # find same canonical keys in both graphs
  134. vector1 = np.array([(canonkey1[key] if (key in canonkey1.keys()) else 0) for key in keys])
  135. vector2 = np.array([(canonkey2[key] if (key in canonkey2.keys()) else 0) for key in keys])
  136. kernel = sub_kernel(vector1, vector2)
  137. return kernel
  138. def wrapper_treeletkernel_do(sub_kernel, itr):
  139. i = itr[0]
  140. j = itr[1]
  141. return i, j, _treeletkernel_do(G_canonkeys[i], G_canonkeys[j], sub_kernel)
  142. def get_canonkeys(G, node_label, edge_label, labeled, is_directed):
  143. """Generate canonical keys of all treelets in a graph.
  144. Parameters
  145. ----------
  146. G : NetworkX graphs
  147. The graph in which keys are generated.
  148. node_label : string
  149. node attribute used as label. The default node label is atom.
  150. edge_label : string
  151. edge attribute used as label. The default edge label is bond_type.
  152. labeled : boolean
  153. Whether the graphs are labeled. The default is True.
  154. Return
  155. ------
  156. canonkey/canonkey_l : dict
  157. For unlabeled graphs, canonkey is a dictionary which records amount of
  158. every tree pattern. For labeled graphs, canonkey_l is one which keeps
  159. track of amount of every treelet.
  160. """
  161. patterns = {} # a dictionary which consists of lists of patterns for all graphlet.
  162. canonkey = {} # canonical key, a dictionary which records amount of every tree pattern.
  163. ### structural analysis ###
  164. ### In this section, a list of patterns is generated for each graphlet,
  165. ### where every pattern is represented by nodes ordered by Morgan's
  166. ### extended labeling.
  167. # linear patterns
  168. patterns['0'] = G.nodes()
  169. canonkey['0'] = nx.number_of_nodes(G)
  170. for i in range(1, 6): # for i in range(1, 6):
  171. patterns[str(i)] = find_all_paths(G, i, is_directed)
  172. canonkey[str(i)] = len(patterns[str(i)])
  173. # n-star patterns
  174. patterns['3star'] = [[node] + [neighbor for neighbor in G[node]] for node in G.nodes() if G.degree(node) == 3]
  175. patterns['4star'] = [[node] + [neighbor for neighbor in G[node]] for node in G.nodes() if G.degree(node) == 4]
  176. patterns['5star'] = [[node] + [neighbor for neighbor in G[node]] for node in G.nodes() if G.degree(node) == 5]
  177. # n-star patterns
  178. canonkey['6'] = len(patterns['3star'])
  179. canonkey['8'] = len(patterns['4star'])
  180. canonkey['d'] = len(patterns['5star'])
  181. # pattern 7
  182. patterns['7'] = [] # the 1st line of Table 1 in Ref [1]
  183. for pattern in patterns['3star']:
  184. for i in range(1, len(pattern)): # for each neighbor of node 0
  185. if G.degree(pattern[i]) >= 2:
  186. pattern_t = pattern[:]
  187. # set the node with degree >= 2 as the 4th node
  188. pattern_t[i], pattern_t[3] = pattern_t[3], pattern_t[i]
  189. for neighborx in G[pattern[i]]:
  190. if neighborx != pattern[0]:
  191. new_pattern = pattern_t + [neighborx]
  192. patterns['7'].append(new_pattern)
  193. canonkey['7'] = len(patterns['7'])
  194. # pattern 11
  195. patterns['11'] = [] # the 4th line of Table 1 in Ref [1]
  196. for pattern in patterns['4star']:
  197. for i in range(1, len(pattern)):
  198. if G.degree(pattern[i]) >= 2:
  199. pattern_t = pattern[:]
  200. pattern_t[i], pattern_t[4] = pattern_t[4], pattern_t[i]
  201. for neighborx in G[pattern[i]]:
  202. if neighborx != pattern[0]:
  203. new_pattern = pattern_t + [ neighborx ]
  204. patterns['11'].append(new_pattern)
  205. canonkey['b'] = len(patterns['11'])
  206. # pattern 12
  207. patterns['12'] = [] # the 5th line of Table 1 in Ref [1]
  208. rootlist = [] # a list of root nodes, whose extended labels are 3
  209. for pattern in patterns['3star']:
  210. if pattern[0] not in rootlist: # prevent to count the same pattern twice from each of the two root nodes
  211. rootlist.append(pattern[0])
  212. for i in range(1, len(pattern)):
  213. if G.degree(pattern[i]) >= 3:
  214. rootlist.append(pattern[i])
  215. pattern_t = pattern[:]
  216. pattern_t[i], pattern_t[3] = pattern_t[3], pattern_t[i]
  217. for neighborx1 in G[pattern[i]]:
  218. if neighborx1 != pattern[0]:
  219. for neighborx2 in G[pattern[i]]:
  220. if neighborx1 > neighborx2 and neighborx2 != pattern[0]:
  221. new_pattern = pattern_t + [neighborx1] + [neighborx2]
  222. # new_patterns = [ pattern + [neighborx1] + [neighborx2] for neighborx1 in G[pattern[i]] if neighborx1 != pattern[0] for neighborx2 in G[pattern[i]] if (neighborx1 > neighborx2 and neighborx2 != pattern[0]) ]
  223. patterns['12'].append(new_pattern)
  224. canonkey['c'] = int(len(patterns['12']) / 2)
  225. # pattern 9
  226. patterns['9'] = [] # the 2nd line of Table 1 in Ref [1]
  227. for pattern in patterns['3star']:
  228. for pairs in [ [neighbor1, neighbor2] for neighbor1 in G[pattern[0]] if G.degree(neighbor1) >= 2 \
  229. for neighbor2 in G[pattern[0]] if G.degree(neighbor2) >= 2 if neighbor1 > neighbor2 ]:
  230. pattern_t = pattern[:]
  231. # move nodes with extended labels 4 to specific position to correspond to their children
  232. pattern_t[pattern_t.index(pairs[0])], pattern_t[2] = pattern_t[2], pattern_t[pattern_t.index(pairs[0])]
  233. pattern_t[pattern_t.index(pairs[1])], pattern_t[3] = pattern_t[3], pattern_t[pattern_t.index(pairs[1])]
  234. for neighborx1 in G[pairs[0]]:
  235. if neighborx1 != pattern[0]:
  236. for neighborx2 in G[pairs[1]]:
  237. if neighborx2 != pattern[0]:
  238. new_pattern = pattern_t + [neighborx1] + [neighborx2]
  239. patterns['9'].append(new_pattern)
  240. canonkey['9'] = len(patterns['9'])
  241. # pattern 10
  242. patterns['10'] = [] # the 3rd line of Table 1 in Ref [1]
  243. for pattern in patterns['3star']:
  244. for i in range(1, len(pattern)):
  245. if G.degree(pattern[i]) >= 2:
  246. for neighborx in G[pattern[i]]:
  247. if neighborx != pattern[0] and G.degree(neighborx) >= 2:
  248. pattern_t = pattern[:]
  249. pattern_t[i], pattern_t[3] = pattern_t[3], pattern_t[i]
  250. new_patterns = [ pattern_t + [neighborx] + [neighborxx] for neighborxx in G[neighborx] if neighborxx != pattern[i] ]
  251. patterns['10'].extend(new_patterns)
  252. canonkey['a'] = len(patterns['10'])
  253. ### labeling information ###
  254. ### In this section, a list of canonical keys is generated for every
  255. ### pattern obtained in the structural analysis section above, which is a
  256. ### string corresponding to a unique treelet. A dictionary is built to keep
  257. ### track of the amount of every treelet.
  258. if labeled == True:
  259. canonkey_l = {} # canonical key, a dictionary which keeps track of amount of every treelet.
  260. # linear patterns
  261. canonkey_t = Counter(list(nx.get_node_attributes(G, node_label).values()))
  262. for key in canonkey_t:
  263. canonkey_l[('0', key)] = canonkey_t[key]
  264. for i in range(1, 6): # for i in range(1, 6):
  265. treelet = []
  266. for pattern in patterns[str(i)]:
  267. canonlist = list(chain.from_iterable((G.node[node][node_label], \
  268. G[node][pattern[idx+1]][edge_label]) for idx, node in enumerate(pattern[:-1])))
  269. canonlist.append(G.node[pattern[-1]][node_label])
  270. canonkey_t = canonlist if canonlist < canonlist[::-1] else canonlist[::-1]
  271. treelet.append(tuple([str(i)] + canonkey_t))
  272. canonkey_l.update(Counter(treelet))
  273. # n-star patterns
  274. for i in range(3, 6):
  275. treelet = []
  276. for pattern in patterns[str(i) + 'star']:
  277. canonlist = [tuple((G.node[leaf][node_label],
  278. G[leaf][pattern[0]][edge_label])) for leaf in pattern[1:]]
  279. canonlist.sort()
  280. canonlist = list(chain.from_iterable(canonlist))
  281. canonkey_t = tuple(['d' if i == 5 else str(i * 2)] +
  282. [G.node[pattern[0]][node_label]] + canonlist)
  283. treelet.append(canonkey_t)
  284. canonkey_l.update(Counter(treelet))
  285. # pattern 7
  286. treelet = []
  287. for pattern in patterns['7']:
  288. canonlist = [tuple((G.node[leaf][node_label],
  289. G[leaf][pattern[0]][edge_label])) for leaf in pattern[1:3]]
  290. canonlist.sort()
  291. canonlist = list(chain.from_iterable(canonlist))
  292. canonkey_t = tuple(['7'] + [G.node[pattern[0]][node_label]] + canonlist
  293. + [G.node[pattern[3]][node_label]]
  294. + [G[pattern[3]][pattern[0]][edge_label]]
  295. + [G.node[pattern[4]][node_label]]
  296. + [G[pattern[4]][pattern[3]][edge_label]])
  297. treelet.append(canonkey_t)
  298. canonkey_l.update(Counter(treelet))
  299. # pattern 11
  300. treelet = []
  301. for pattern in patterns['11']:
  302. canonlist = [tuple((G.node[leaf][node_label],
  303. G[leaf][pattern[0]][edge_label])) for leaf in pattern[1:4]]
  304. canonlist.sort()
  305. canonlist = list(chain.from_iterable(canonlist))
  306. canonkey_t = tuple(['b'] + [G.node[pattern[0]][node_label]] + canonlist
  307. + [G.node[pattern[4]][node_label]]
  308. + [G[pattern[4]][pattern[0]][edge_label]]
  309. + [G.node[pattern[5]][node_label]]
  310. + [G[pattern[5]][pattern[4]][edge_label]])
  311. treelet.append(canonkey_t)
  312. canonkey_l.update(Counter(treelet))
  313. # pattern 10
  314. treelet = []
  315. for pattern in patterns['10']:
  316. canonkey4 = [G.node[pattern[5]][node_label], G[pattern[5]][pattern[4]][edge_label]]
  317. canonlist = [tuple((G.node[leaf][node_label],
  318. G[leaf][pattern[0]][edge_label])) for leaf in pattern[1:3]]
  319. canonlist.sort()
  320. canonkey0 = list(chain.from_iterable(canonlist))
  321. canonkey_t = tuple(['a'] + [G.node[pattern[3]][node_label]]
  322. + [G.node[pattern[4]][node_label]]
  323. + [G[pattern[4]][pattern[3]][edge_label]]
  324. + [G.node[pattern[0]][node_label]]
  325. + [G[pattern[0]][pattern[3]][edge_label]]
  326. + canonkey4 + canonkey0)
  327. treelet.append(canonkey_t)
  328. canonkey_l.update(Counter(treelet))
  329. # pattern 12
  330. treelet = []
  331. for pattern in patterns['12']:
  332. canonlist0 = [tuple((G.node[leaf][node_label],
  333. G[leaf][pattern[0]][edge_label])) for leaf in pattern[1:3]]
  334. canonlist0.sort()
  335. canonlist0 = list(chain.from_iterable(canonlist0))
  336. canonlist3 = [tuple((G.node[leaf][node_label],
  337. G[leaf][pattern[3]][edge_label])) for leaf in pattern[4:6]]
  338. canonlist3.sort()
  339. canonlist3 = list(chain.from_iterable(canonlist3))
  340. # 2 possible key can be generated from 2 nodes with extended label 3,
  341. # select the one with lower lexicographic order.
  342. canonkey_t1 = tuple(['c'] + [G.node[pattern[0]][node_label]] + canonlist0
  343. + [G.node[pattern[3]][node_label]]
  344. + [G[pattern[3]][pattern[0]][edge_label]]
  345. + canonlist3)
  346. canonkey_t2 = tuple(['c'] + [G.node[pattern[3]][node_label]] + canonlist3
  347. + [G.node[pattern[0]][node_label]]
  348. + [G[pattern[0]][pattern[3]][edge_label]]
  349. + canonlist0)
  350. treelet.append(canonkey_t1 if canonkey_t1 < canonkey_t2 else canonkey_t2)
  351. canonkey_l.update(Counter(treelet))
  352. # pattern 9
  353. treelet = []
  354. for pattern in patterns['9']:
  355. canonkey2 = [G.node[pattern[4]][node_label], G[pattern[4]][pattern[2]][edge_label]]
  356. canonkey3 = [G.node[pattern[5]][node_label], G[pattern[5]][pattern[3]][edge_label]]
  357. prekey2 = [G.node[pattern[2]][node_label], G[pattern[2]][pattern[0]][edge_label]]
  358. prekey3 = [G.node[pattern[3]][node_label], G[pattern[3]][pattern[0]][edge_label]]
  359. if prekey2 + canonkey2 < prekey3 + canonkey3:
  360. canonkey_t = [G.node[pattern[1]][node_label]] \
  361. + [G[pattern[1]][pattern[0]][edge_label]] \
  362. + prekey2 + prekey3 + canonkey2 + canonkey3
  363. else:
  364. canonkey_t = [G.node[pattern[1]][node_label]] \
  365. + [G[pattern[1]][pattern[0]][edge_label]] \
  366. + prekey3 + prekey2 + canonkey3 + canonkey2
  367. treelet.append(tuple(['9'] + [G.node[pattern[0]][node_label]] + canonkey_t))
  368. canonkey_l.update(Counter(treelet))
  369. return canonkey_l
  370. return canonkey
  371. def wrapper_get_canonkeys(node_label, edge_label, labeled, is_directed, itr_item):
  372. g = itr_item[0]
  373. i = itr_item[1]
  374. return i, get_canonkeys(g, node_label, edge_label, labeled, is_directed)
  375. def find_paths(G, source_node, length):
  376. """Find all paths with a certain length those start from a source node.
  377. A recursive depth first search is applied.
  378. Parameters
  379. ----------
  380. G : NetworkX graphs
  381. The graph in which paths are searched.
  382. source_node : integer
  383. The number of the node from where all paths start.
  384. length : integer
  385. The length of paths.
  386. Return
  387. ------
  388. path : list of list
  389. List of paths retrieved, where each path is represented by a list of nodes.
  390. """
  391. if length == 0:
  392. return [[source_node]]
  393. path = [[source_node] + path for neighbor in G[source_node] \
  394. for path in find_paths(G, neighbor, length - 1) if source_node not in path]
  395. return path
  396. def find_all_paths(G, length, is_directed):
  397. """Find all paths with a certain length in a graph. A recursive depth first
  398. search is applied.
  399. Parameters
  400. ----------
  401. G : NetworkX graphs
  402. The graph in which paths are searched.
  403. length : integer
  404. The length of paths.
  405. Return
  406. ------
  407. path : list of list
  408. List of paths retrieved, where each path is represented by a list of nodes.
  409. """
  410. all_paths = []
  411. for node in G:
  412. all_paths.extend(find_paths(G, node, length))
  413. if not is_directed:
  414. # For each path, two presentations are retrieved from its two extremities.
  415. # Remove one of them.
  416. all_paths_r = [path[::-1] for path in all_paths]
  417. for idx, path in enumerate(all_paths[:-1]):
  418. for path2 in all_paths_r[idx+1::]:
  419. if path == path2:
  420. all_paths[idx] = []
  421. break
  422. all_paths = list(filter(lambda a: a != [], all_paths))
  423. return all_paths

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