You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

main_DeepGMG.py 22KB

6 years ago
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594
  1. # an implementation for "Learning Deep Generative Models of Graphs"
  2. from main import *
  3. class Args_DGMG():
  4. def __init__(self):
  5. ### CUDA
  6. self.cuda = 2
  7. ### model type
  8. self.note = 'Baseline_DGMG' # do GCN after adding each edge
  9. # self.note = 'Baseline_DGMG_fast' # do GCN only after adding each node
  10. ### data config
  11. self.graph_type = 'caveman_small'
  12. # self.graph_type = 'grid_small'
  13. # self.graph_type = 'ladder_small'
  14. # self.graph_type = 'enzymes_small'
  15. # self.graph_type = 'barabasi_small'
  16. # self.graph_type = 'citeseer_small'
  17. self.max_num_node = 20
  18. ### network config
  19. self.node_embedding_size = 64
  20. self.test_graph_num = 200
  21. ### training config
  22. self.epochs = 2000 # now one epoch means self.batch_ratio x batch_size
  23. self.load_epoch = 2000
  24. self.epochs_test_start = 100
  25. self.epochs_test = 100
  26. self.epochs_log = 100
  27. self.epochs_save = 100
  28. if 'fast' in self.note:
  29. self.is_fast = True
  30. else:
  31. self.is_fast = False
  32. self.lr = 0.001
  33. self.milestones = [300, 600, 1000]
  34. self.lr_rate = 0.3
  35. ### output config
  36. self.model_save_path = 'model_save/'
  37. self.graph_save_path = 'graphs/'
  38. self.figure_save_path = 'figures/'
  39. self.timing_save_path = 'timing/'
  40. self.figure_prediction_save_path = 'figures_prediction/'
  41. self.nll_save_path = 'nll/'
  42. self.fname = self.note + '_' + self.graph_type + '_' + str(self.node_embedding_size)
  43. self.fname_pred = self.note + '_' + self.graph_type + '_' + str(self.node_embedding_size) + '_pred_'
  44. self.fname_train = self.note + '_' + self.graph_type + '_' + str(self.node_embedding_size) + '_train_'
  45. self.fname_test = self.note + '_' + self.graph_type + '_' + str(self.node_embedding_size) + '_test_'
  46. self.load = False
  47. self.save = True
  48. def train_DGMG_epoch(epoch, args, model, dataset, optimizer, scheduler, is_fast = False):
  49. model.train()
  50. graph_num = len(dataset)
  51. order = list(range(graph_num))
  52. shuffle(order)
  53. loss_addnode = 0
  54. loss_addedge = 0
  55. loss_node = 0
  56. for i in order:
  57. model.zero_grad()
  58. graph = dataset[i]
  59. # do random ordering: relabel nodes
  60. node_order = list(range(graph.number_of_nodes()))
  61. shuffle(node_order)
  62. order_mapping = dict(zip(graph.nodes(), node_order))
  63. graph = nx.relabel_nodes(graph, order_mapping, copy=True)
  64. # NOTE: when starting loop, we assume a node has already been generated
  65. node_count = 1
  66. node_embedding = [Variable(torch.ones(1,args.node_embedding_size)).cuda()] # list of torch tensors, each size: 1*hidden
  67. loss = 0
  68. while node_count<=graph.number_of_nodes():
  69. node_neighbor = graph.subgraph(list(range(node_count))).adjacency_list() # list of lists (first node is zero)
  70. node_neighbor_new = graph.subgraph(list(range(node_count+1))).adjacency_list()[-1] # list of new node's neighbors
  71. # 1 message passing
  72. # do 2 times message passing
  73. node_embedding = message_passing(node_neighbor, node_embedding, model)
  74. # 2 graph embedding and new node embedding
  75. node_embedding_cat = torch.cat(node_embedding, dim=0)
  76. graph_embedding = calc_graph_embedding(node_embedding_cat, model)
  77. init_embedding = calc_init_embedding(node_embedding_cat, model)
  78. # 3 f_addnode
  79. p_addnode = model.f_an(graph_embedding)
  80. if node_count < graph.number_of_nodes():
  81. # add node
  82. node_neighbor.append([])
  83. node_embedding.append(init_embedding)
  84. if is_fast:
  85. node_embedding_cat = torch.cat(node_embedding, dim=0)
  86. # calc loss
  87. loss_addnode_step = F.binary_cross_entropy(p_addnode,Variable(torch.ones((1,1))).cuda())
  88. # loss_addnode_step.backward(retain_graph=True)
  89. loss += loss_addnode_step
  90. loss_addnode += loss_addnode_step.data
  91. else:
  92. # calc loss
  93. loss_addnode_step = F.binary_cross_entropy(p_addnode, Variable(torch.zeros((1, 1))).cuda())
  94. # loss_addnode_step.backward(retain_graph=True)
  95. loss += loss_addnode_step
  96. loss_addnode += loss_addnode_step.data
  97. break
  98. edge_count = 0
  99. while edge_count<=len(node_neighbor_new):
  100. if not is_fast:
  101. node_embedding = message_passing(node_neighbor, node_embedding, model)
  102. node_embedding_cat = torch.cat(node_embedding, dim=0)
  103. graph_embedding = calc_graph_embedding(node_embedding_cat, model)
  104. # 4 f_addedge
  105. p_addedge = model.f_ae(graph_embedding)
  106. if edge_count < len(node_neighbor_new):
  107. # calc loss
  108. loss_addedge_step = F.binary_cross_entropy(p_addedge, Variable(torch.ones((1, 1))).cuda())
  109. # loss_addedge_step.backward(retain_graph=True)
  110. loss += loss_addedge_step
  111. loss_addedge += loss_addedge_step.data
  112. # 5 f_nodes
  113. # excluding the last node (which is the new node)
  114. node_new_embedding_cat = node_embedding_cat[-1,:].expand(node_embedding_cat.size(0)-1,node_embedding_cat.size(1))
  115. s_node = model.f_s(torch.cat((node_embedding_cat[0:-1,:],node_new_embedding_cat),dim=1))
  116. p_node = F.softmax(s_node.permute(1,0))
  117. # get ground truth
  118. a_node = torch.zeros((1,p_node.size(1)))
  119. # print('node_neighbor_new',node_neighbor_new, edge_count)
  120. a_node[0,node_neighbor_new[edge_count]] = 1
  121. a_node = Variable(a_node).cuda()
  122. # add edge
  123. node_neighbor[-1].append(node_neighbor_new[edge_count])
  124. node_neighbor[node_neighbor_new[edge_count]].append(len(node_neighbor)-1)
  125. # calc loss
  126. loss_node_step = F.binary_cross_entropy(p_node,a_node)
  127. # loss_node_step.backward(retain_graph=True)
  128. loss += loss_node_step
  129. loss_node += loss_node_step.data
  130. else:
  131. # calc loss
  132. loss_addedge_step = F.binary_cross_entropy(p_addedge, Variable(torch.zeros((1, 1))).cuda())
  133. # loss_addedge_step.backward(retain_graph=True)
  134. loss += loss_addedge_step
  135. loss_addedge += loss_addedge_step.data
  136. break
  137. edge_count += 1
  138. node_count += 1
  139. # update deterministic and lstm
  140. loss.backward()
  141. optimizer.step()
  142. scheduler.step()
  143. loss_all = loss_addnode + loss_addedge + loss_node
  144. if epoch % args.epochs_log==0:
  145. print('Epoch: {}/{}, train loss: {:.6f}, graph type: {}, hidden: {}'.format(
  146. epoch, args.epochs,loss_all[0], args.graph_type, args.node_embedding_size))
  147. # loss_sum += loss.data[0]*x.size(0)
  148. # return loss_sum
  149. def train_DGMG_forward_epoch(args, model, dataset, is_fast = False):
  150. model.train()
  151. graph_num = len(dataset)
  152. order = list(range(graph_num))
  153. shuffle(order)
  154. loss_addnode = 0
  155. loss_addedge = 0
  156. loss_node = 0
  157. for i in order:
  158. model.zero_grad()
  159. graph = dataset[i]
  160. # do random ordering: relabel nodes
  161. node_order = list(range(graph.number_of_nodes()))
  162. shuffle(node_order)
  163. order_mapping = dict(zip(graph.nodes(), node_order))
  164. graph = nx.relabel_nodes(graph, order_mapping, copy=True)
  165. # NOTE: when starting loop, we assume a node has already been generated
  166. node_count = 1
  167. node_embedding = [Variable(torch.ones(1,args.node_embedding_size)).cuda()] # list of torch tensors, each size: 1*hidden
  168. loss = 0
  169. while node_count<=graph.number_of_nodes():
  170. node_neighbor = graph.subgraph(list(range(node_count))).adjacency_list() # list of lists (first node is zero)
  171. node_neighbor_new = graph.subgraph(list(range(node_count+1))).adjacency_list()[-1] # list of new node's neighbors
  172. # 1 message passing
  173. # do 2 times message passing
  174. node_embedding = message_passing(node_neighbor, node_embedding, model)
  175. # 2 graph embedding and new node embedding
  176. node_embedding_cat = torch.cat(node_embedding, dim=0)
  177. graph_embedding = calc_graph_embedding(node_embedding_cat, model)
  178. init_embedding = calc_init_embedding(node_embedding_cat, model)
  179. # 3 f_addnode
  180. p_addnode = model.f_an(graph_embedding)
  181. if node_count < graph.number_of_nodes():
  182. # add node
  183. node_neighbor.append([])
  184. node_embedding.append(init_embedding)
  185. if is_fast:
  186. node_embedding_cat = torch.cat(node_embedding, dim=0)
  187. # calc loss
  188. loss_addnode_step = F.binary_cross_entropy(p_addnode,Variable(torch.ones((1,1))).cuda())
  189. # loss_addnode_step.backward(retain_graph=True)
  190. loss += loss_addnode_step
  191. loss_addnode += loss_addnode_step.data
  192. else:
  193. # calc loss
  194. loss_addnode_step = F.binary_cross_entropy(p_addnode, Variable(torch.zeros((1, 1))).cuda())
  195. # loss_addnode_step.backward(retain_graph=True)
  196. loss += loss_addnode_step
  197. loss_addnode += loss_addnode_step.data
  198. break
  199. edge_count = 0
  200. while edge_count<=len(node_neighbor_new):
  201. if not is_fast:
  202. node_embedding = message_passing(node_neighbor, node_embedding, model)
  203. node_embedding_cat = torch.cat(node_embedding, dim=0)
  204. graph_embedding = calc_graph_embedding(node_embedding_cat, model)
  205. # 4 f_addedge
  206. p_addedge = model.f_ae(graph_embedding)
  207. if edge_count < len(node_neighbor_new):
  208. # calc loss
  209. loss_addedge_step = F.binary_cross_entropy(p_addedge, Variable(torch.ones((1, 1))).cuda())
  210. # loss_addedge_step.backward(retain_graph=True)
  211. loss += loss_addedge_step
  212. loss_addedge += loss_addedge_step.data
  213. # 5 f_nodes
  214. # excluding the last node (which is the new node)
  215. node_new_embedding_cat = node_embedding_cat[-1,:].expand(node_embedding_cat.size(0)-1,node_embedding_cat.size(1))
  216. s_node = model.f_s(torch.cat((node_embedding_cat[0:-1,:],node_new_embedding_cat),dim=1))
  217. p_node = F.softmax(s_node.permute(1,0))
  218. # get ground truth
  219. a_node = torch.zeros((1,p_node.size(1)))
  220. # print('node_neighbor_new',node_neighbor_new, edge_count)
  221. a_node[0,node_neighbor_new[edge_count]] = 1
  222. a_node = Variable(a_node).cuda()
  223. # add edge
  224. node_neighbor[-1].append(node_neighbor_new[edge_count])
  225. node_neighbor[node_neighbor_new[edge_count]].append(len(node_neighbor)-1)
  226. # calc loss
  227. loss_node_step = F.binary_cross_entropy(p_node,a_node)
  228. # loss_node_step.backward(retain_graph=True)
  229. loss += loss_node_step
  230. loss_node += loss_node_step.data*p_node.size(1)
  231. else:
  232. # calc loss
  233. loss_addedge_step = F.binary_cross_entropy(p_addedge, Variable(torch.zeros((1, 1))).cuda())
  234. # loss_addedge_step.backward(retain_graph=True)
  235. loss += loss_addedge_step
  236. loss_addedge += loss_addedge_step.data
  237. break
  238. edge_count += 1
  239. node_count += 1
  240. loss_all = loss_addnode + loss_addedge + loss_node
  241. # if epoch % args.epochs_log==0:
  242. # print('Epoch: {}/{}, train loss: {:.6f}, graph type: {}, hidden: {}'.format(
  243. # epoch, args.epochs,loss_all[0], args.graph_type, args.node_embedding_size))
  244. return loss_all[0]/len(dataset)
  245. def test_DGMG_epoch(args, model, is_fast=False):
  246. model.eval()
  247. graph_num = args.test_graph_num
  248. graphs_generated = []
  249. for i in range(graph_num):
  250. # NOTE: when starting loop, we assume a node has already been generated
  251. node_neighbor = [[]] # list of lists (first node is zero)
  252. node_embedding = [Variable(torch.ones(1,args.node_embedding_size)).cuda()] # list of torch tensors, each size: 1*hidden
  253. node_count = 1
  254. while node_count<=args.max_num_node:
  255. # 1 message passing
  256. # do 2 times message passing
  257. node_embedding = message_passing(node_neighbor, node_embedding, model)
  258. # 2 graph embedding and new node embedding
  259. node_embedding_cat = torch.cat(node_embedding, dim=0)
  260. graph_embedding = calc_graph_embedding(node_embedding_cat, model)
  261. init_embedding = calc_init_embedding(node_embedding_cat, model)
  262. # 3 f_addnode
  263. p_addnode = model.f_an(graph_embedding)
  264. a_addnode = sample_tensor(p_addnode)
  265. # print(a_addnode.data[0][0])
  266. if a_addnode.data[0][0]==1:
  267. # print('add node')
  268. # add node
  269. node_neighbor.append([])
  270. node_embedding.append(init_embedding)
  271. if is_fast:
  272. node_embedding_cat = torch.cat(node_embedding, dim=0)
  273. else:
  274. break
  275. edge_count = 0
  276. while edge_count<args.max_num_node:
  277. if not is_fast:
  278. node_embedding = message_passing(node_neighbor, node_embedding, model)
  279. node_embedding_cat = torch.cat(node_embedding, dim=0)
  280. graph_embedding = calc_graph_embedding(node_embedding_cat, model)
  281. # 4 f_addedge
  282. p_addedge = model.f_ae(graph_embedding)
  283. a_addedge = sample_tensor(p_addedge)
  284. # print(a_addedge.data[0][0])
  285. if a_addedge.data[0][0]==1:
  286. # print('add edge')
  287. # 5 f_nodes
  288. # excluding the last node (which is the new node)
  289. node_new_embedding_cat = node_embedding_cat[-1,:].expand(node_embedding_cat.size(0)-1,node_embedding_cat.size(1))
  290. s_node = model.f_s(torch.cat((node_embedding_cat[0:-1,:],node_new_embedding_cat),dim=1))
  291. p_node = F.softmax(s_node.permute(1,0))
  292. a_node = gumbel_softmax(p_node, temperature=0.01)
  293. _, a_node_id = a_node.topk(1)
  294. a_node_id = int(a_node_id.data[0][0])
  295. # add edge
  296. node_neighbor[-1].append(a_node_id)
  297. node_neighbor[a_node_id].append(len(node_neighbor)-1)
  298. else:
  299. break
  300. edge_count += 1
  301. node_count += 1
  302. # save graph
  303. node_neighbor_dict = dict(zip(list(range(len(node_neighbor))), node_neighbor))
  304. graph = nx.from_dict_of_lists(node_neighbor_dict)
  305. graphs_generated.append(graph)
  306. return graphs_generated
  307. ########### train function for LSTM + VAE
  308. def train_DGMG(args, dataset_train, model):
  309. # check if load existing model
  310. if args.load:
  311. fname = args.model_save_path + args.fname + 'model_' + str(args.load_epoch) + '.dat'
  312. model.load_state_dict(torch.load(fname))
  313. args.lr = 0.00001
  314. epoch = args.load_epoch
  315. print('model loaded!, lr: {}'.format(args.lr))
  316. else:
  317. epoch = 1
  318. # initialize optimizer
  319. optimizer = optim.Adam(list(model.parameters()), lr=args.lr)
  320. scheduler = MultiStepLR(optimizer, milestones=args.milestones, gamma=args.lr_rate)
  321. # start main loop
  322. time_all = np.zeros(args.epochs)
  323. while epoch <= args.epochs:
  324. time_start = tm.time()
  325. # train
  326. train_DGMG_epoch(epoch, args, model, dataset_train, optimizer, scheduler, is_fast=args.is_fast)
  327. time_end = tm.time()
  328. time_all[epoch - 1] = time_end - time_start
  329. # print('time used',time_all[epoch - 1])
  330. # test
  331. if epoch % args.epochs_test == 0 and epoch >= args.epochs_test_start:
  332. graphs = test_DGMG_epoch(args,model, is_fast=args.is_fast)
  333. fname = args.graph_save_path + args.fname_pred + str(epoch) + '.dat'
  334. save_graph_list(graphs, fname)
  335. # print('test done, graphs saved')
  336. # save model checkpoint
  337. if args.save:
  338. if epoch % args.epochs_save == 0:
  339. fname = args.model_save_path + args.fname + 'model_' + str(epoch) + '.dat'
  340. torch.save(model.state_dict(), fname)
  341. epoch += 1
  342. np.save(args.timing_save_path + args.fname, time_all)
  343. ########### train function for LSTM + VAE
  344. def train_DGMG_nll(args, dataset_train,dataset_test, model,max_iter=1000):
  345. # check if load existing model
  346. fname = args.model_save_path + args.fname + 'model_' + str(args.load_epoch) + '.dat'
  347. model.load_state_dict(torch.load(fname))
  348. fname_output = args.nll_save_path + args.note + '_' + args.graph_type + '.csv'
  349. with open(fname_output, 'w+') as f:
  350. f.write('train,test\n')
  351. # start main loop
  352. for iter in range(max_iter):
  353. nll_train = train_DGMG_forward_epoch(args, model, dataset_train, is_fast=args.is_fast)
  354. nll_test = train_DGMG_forward_epoch(args, model, dataset_test, is_fast=args.is_fast)
  355. print('train', nll_train, 'test', nll_test)
  356. f.write(str(nll_train) + ',' + str(nll_test) + '\n')
  357. if __name__ == '__main__':
  358. args = Args_DGMG()
  359. os.environ['CUDA_VISIBLE_DEVICES'] = str(args.cuda)
  360. print('CUDA', args.cuda)
  361. print('File name prefix',args.fname)
  362. graphs = []
  363. for i in range(4, 10):
  364. graphs.append(nx.ladder_graph(i))
  365. model = DGM_graphs(h_size = args.node_embedding_size).cuda()
  366. if args.graph_type == 'ladder_small':
  367. graphs = []
  368. for i in range(2, 11):
  369. graphs.append(nx.ladder_graph(i))
  370. args.max_prev_node = 10
  371. # if args.graph_type == 'caveman_small':
  372. # graphs = []
  373. # for i in range(2, 5):
  374. # for j in range(2, 6):
  375. # for k in range(10):
  376. # graphs.append(nx.relaxed_caveman_graph(i, j, p=0.1))
  377. # args.max_prev_node = 20
  378. if args.graph_type=='caveman_small':
  379. graphs = []
  380. for i in range(2, 3):
  381. for j in range(6, 11):
  382. for k in range(20):
  383. graphs.append(caveman_special(i, j, p_edge=0.8))
  384. args.max_prev_node = 20
  385. if args.graph_type == 'grid_small':
  386. graphs = []
  387. for i in range(2, 5):
  388. for j in range(2, 6):
  389. graphs.append(nx.grid_2d_graph(i, j))
  390. args.max_prev_node = 15
  391. if args.graph_type == 'barabasi_small':
  392. graphs = []
  393. for i in range(4, 21):
  394. for j in range(3, 4):
  395. for k in range(10):
  396. graphs.append(nx.barabasi_albert_graph(i, j))
  397. args.max_prev_node = 20
  398. if args.graph_type == 'enzymes_small':
  399. graphs_raw = Graph_load_batch(min_num_nodes=10, name='ENZYMES')
  400. graphs = []
  401. for G in graphs_raw:
  402. if G.number_of_nodes()<=20:
  403. graphs.append(G)
  404. args.max_prev_node = 15
  405. if args.graph_type == 'citeseer_small':
  406. _, _, G = Graph_load(dataset='citeseer')
  407. G = max(nx.connected_component_subgraphs(G), key=len)
  408. G = nx.convert_node_labels_to_integers(G)
  409. graphs = []
  410. for i in range(G.number_of_nodes()):
  411. G_ego = nx.ego_graph(G, i, radius=1)
  412. if (G_ego.number_of_nodes() >= 4) and (G_ego.number_of_nodes() <= 20):
  413. graphs.append(G_ego)
  414. shuffle(graphs)
  415. graphs = graphs[0:200]
  416. args.max_prev_node = 15
  417. # remove self loops
  418. for graph in graphs:
  419. edges_with_selfloops = graph.selfloop_edges()
  420. if len(edges_with_selfloops) > 0:
  421. graph.remove_edges_from(edges_with_selfloops)
  422. # split datasets
  423. random.seed(123)
  424. shuffle(graphs)
  425. graphs_len = len(graphs)
  426. graphs_test = graphs[int(0.8 * graphs_len):]
  427. graphs_train = graphs[0:int(0.8 * graphs_len)]
  428. args.max_num_node = max([graphs[i].number_of_nodes() for i in range(len(graphs))])
  429. # args.max_num_node = 2000
  430. # show graphs statistics
  431. print('total graph num: {}, training set: {}'.format(len(graphs), len(graphs_train)))
  432. print('max number node: {}'.format(args.max_num_node))
  433. print('max previous node: {}'.format(args.max_prev_node))
  434. # save ground truth graphs
  435. # save_graph_list(graphs, args.graph_save_path + args.fname_train + '0.dat')
  436. # save_graph_list(graphs, args.graph_save_path + args.fname_test + '0.dat')
  437. # print('train and test graphs saved')
  438. ## if use pre-saved graphs
  439. # dir_input = "graphs/"
  440. # fname_test = args.graph_save_path + args.fname_test + '0.dat'
  441. # graphs = load_graph_list(fname_test, is_real=True)
  442. # graphs_test = graphs[int(0.8 * graphs_len):]
  443. # graphs_train = graphs[0:int(0.8 * graphs_len)]
  444. # graphs_validate = graphs[0:int(0.2 * graphs_len)]
  445. # print('train')
  446. # for graph in graphs_validate:
  447. # print(graph.number_of_nodes())
  448. # print('test')
  449. # for graph in graphs_test:
  450. # print(graph.number_of_nodes())
  451. ### train
  452. train_DGMG(args,graphs,model)
  453. ### calc nll
  454. # train_DGMG_nll(args, graphs_validate,graphs_test, model,max_iter=1000)
  455. # for j in range(1000):
  456. # graph = graphs[0]
  457. # # do random ordering: relabel nodes
  458. # node_order = list(range(graph.number_of_nodes()))
  459. # shuffle(node_order)
  460. # order_mapping = dict(zip(graph.nodes(), node_order))
  461. # graph = nx.relabel_nodes(graph, order_mapping, copy=True)
  462. # print(graph.nodes())