node2vec&deepwalk_checked_v0.0 & alias samp detail

1. fix deepwalk; 2. fix deepwalk; 3. try to explain alias sampling method based abrw (see code comments therein); 4. add a method in graph.py for adding weights to networkx graph (in order to fix node2vec); 5. format graph.py;
This commit is contained in:
Chengbin HOU 2018-11-19 20:23:45 +00:00 committed by GitHub
commit 57d670f786
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 144 additions and 109 deletions

View File

@ -22,9 +22,9 @@ class Graph(object):
#--------------------commonly used APIs that will modify graph-------------------------
#--------------------------------------------------------------------------------------
def node_mapping(self):
""" node id and index mapping;
based on the order given by networkx G.nodes();
NB: updating is needed if any node is added/removed;
""" node id and index mapping; \n
based on the order given by networkx G.nodes(); \n
NB: updating is needed if any node is added/removed; \n
"""
i = 0 #node index
self.look_up_dict = {} #init
@ -35,10 +35,10 @@ class Graph(object):
i += 1
def read_adjlist(self, path, directed=False):
""" read adjacency list format graph;
support unweighted and (un)directed graph;
format: see https://networkx.github.io/documentation/stable/reference/readwrite/adjlist.html
NB: not supoort weighted graph
""" read adjacency list format graph; \n
support unweighted and (un)directed graph; \n
format: see https://networkx.github.io/documentation/stable/reference/readwrite/adjlist.html \n
NB: not supoort weighted graph \n
"""
if directed:
self.G = nx.read_adjlist(path, create_using=nx.DiGraph())
@ -47,9 +47,9 @@ class Graph(object):
self.node_mapping() #update node id index mapping
def read_edgelist(self, path, weighted=False, directed=False):
""" read edge list format graph;
support (un)weighted and (un)directed graph;
format: see https://networkx.github.io/documentation/stable/reference/readwrite/edgelist.html
""" read edge list format graph; \n
support (un)weighted and (un)directed graph; \n
format: see https://networkx.github.io/documentation/stable/reference/readwrite/edgelist.html \n
"""
if directed:
self.G = nx.read_edgelist(path, create_using=nx.DiGraph())
@ -57,10 +57,19 @@ class Graph(object):
self.G = nx.read_edgelist(path, create_using=nx.Graph())
self.node_mapping() #update node id index mapping
def add_edge_weight(self, equal_weight=1.0):
''' add weights to networkx graph; \n
currently only support adding 1.0 to all existing edges; \n
some NE method may require 'weight' attribute spcified in networkx graph; \n
to do... support user-specified weights e.g. from file (similar to read_node_attr): node_id1 node_id2 weight \n
https://networkx.github.io/documentation/stable/reference/generated/networkx.classes.function.set_edge_attributes.html#networkx.classes.function.set_edge_attributes
'''
nx.set_edge_attributes(self.G, equal_weight, 'weight') #check the url and use dict to assign diff weights to diff edges
def read_node_attr(self, path):
""" read node attributes and store as NetworkX graph {'node_id': {'attr': values}}
input file format: node_id1 attr1 attr2 ... attrM
node_id2 attr1 attr2 ... attrM
""" read node attributes and store as NetworkX graph {'node_id': {'attr': values}} \n
input file format: node_id1 attr1 attr2 ... attrM \n
node_id2 attr1 attr2 ... attrM \n
"""
with open(path, 'r') as fin:
for l in fin.readlines():
@ -68,20 +77,20 @@ class Graph(object):
self.G.nodes[vec[0]]['attr'] = np.array([float(x) for x in vec[1:]])
def read_node_label(self, path):
""" todo... read node labels and store as NetworkX graph {'node_id': {'label': values}}
input file format: node_id1 labels
node_id2 labels
with open(path, 'r') as fin:
for l in fin.readlines():
vec = l.split()
self.G.nodes[vec[0]]['label'] = np.array([float(x) for x in vec[1:]])
""" todo... read node labels and store as NetworkX graph {'node_id': {'label': values}} \n
input file format: node_id1 labels \n
node_id2 labels \n
with open(path, 'r') as fin: \n
for l in fin.readlines(): \n
vec = l.split() \n
self.G.nodes[vec[0]]['label'] = np.array([float(x) for x in vec[1:]]) \n
"""
pass #to do...
def remove_edge(self, ratio=0.0):
""" randomly remove edges/links
ratio: the percentage of edges to be removed
edges_removed: return removed edges, each of which is a pair of nodes
""" randomly remove edges/links \n
ratio: the percentage of edges to be removed \n
edges_removed: return removed edges, each of which is a pair of nodes \n
"""
num_edges_removed = int( ratio * self.G.number_of_edges() )
#random.seed(2018)
@ -92,13 +101,13 @@ class Graph(object):
return edges_removed
def remove_node_attr(self, ratio):
""" todo... randomly remove node attributes;
""" todo... randomly remove node attributes; \n
"""
pass #to do...
def remove_node(self, ratio):
""" todo... randomly remove nodes;
#self.node_mapping() #update node id index mapping is needed
""" todo... randomly remove nodes; \n
#self.node_mapping() #update node id index mapping is needed \n
"""
pass #to do...
@ -106,8 +115,8 @@ class Graph(object):
#--------------------commonly used APIs that will not modify graph-------------------------
#------------------------------------------------------------------------------------------
def get_adj_mat(self, is_sparse=True):
""" return adjacency matrix;
use 'csr' format for sparse matrix
""" return adjacency matrix; \n
use 'csr' format for sparse matrix \n
"""
if is_sparse:
return nx.to_scipy_sparse_matrix(self.G, nodelist=self.look_back_list, format='csr', dtype='float64')
@ -115,8 +124,8 @@ class Graph(object):
return nx.to_numpy_matrix(self.G, nodelist=self.look_back_list, dtype='float64')
def get_attr_mat(self, is_sparse=True):
""" return attribute matrix;
use 'csr' format for sparse matrix
""" return attribute matrix; \n
use 'csr' format for sparse matrix \n
"""
attr_dense_narray = np.vstack([self.G.nodes[self.look_back_list[i]]['attr'] for i in range(self.get_num_nodes())])
if is_sparse:
@ -132,6 +141,10 @@ class Graph(object):
""" return the number of edges """
return nx.number_of_edges(self.G)
def get_density(self):
""" return the density of a graph """
return nx.density(self.G)
def get_num_isolates(self):
""" return the number of isolated nodes """
return len(list(nx.isolates(self.G)))
@ -153,8 +166,8 @@ class Graph(object):
return list(nx.common_neighbors(self.G, node1, node2))
def get_centrality(self, centrality_type='degree'):
""" todo... return specified type of centrality
see https://networkx.github.io/documentation/stable/reference/algorithms/centrality.html
""" todo... return specified type of centrality \n
see https://networkx.github.io/documentation/stable/reference/algorithms/centrality.html \n
"""
pass #to do...

View File

@ -1,3 +1,11 @@
"""
NE method: DeepWalk and Node2Vec
modified by Chengbin Hou and Zeyu Dong 2018
originally from https://github.com/thunlp/OpenNE/blob/master/src/openne/node2vec.py
"""
from __future__ import print_function
import time
import warnings
@ -7,9 +15,7 @@ from . import walker
class Node2vec(object):
def __init__(self, graph, path_length, num_paths, dim, p=1.0, q=1.0, dw=False, **kwargs):
kwargs["workers"] = kwargs.get("workers", 1)
if dw:
kwargs["hs"] = 1
@ -18,9 +24,9 @@ class Node2vec(object):
self.graph = graph
if dw:
self.walker = walker.BasicWalker(graph, workers=kwargs["workers"])
self.walker = walker.BasicWalker(graph, workers=kwargs["workers"]) #walker for deepwalk
else:
self.walker = walker.Walker(graph, p=p, q=q, workers=kwargs["workers"])
self.walker = walker.Walker(graph, p=p, q=q, workers=kwargs["workers"]) #walker for node2vec
print("Preprocess transition probs...")
self.walker.preprocess_transition_probs()
sentences = self.walker.simulate_walks(num_walks=num_paths, walk_length=path_length)

View File

@ -14,12 +14,7 @@ import numpy as np
from networkx import nx
def deepwalk_walk_wrapper(class_instance, walk_length, start_node):
class_instance.deepwalk_walk(walk_length, start_node)
# ===========================================ABRW-weighted-walker============================================
class WeightedWalker:
''' Weighted Walker for Attributed Biased Randomw Walks (ABRW) method
'''
@ -28,52 +23,59 @@ class WeightedWalker:
self.look_back_list = node_id_map
self.T = transition_mat
self.workers = workers
# self.G = nx.to_networkx_graph(self.T, create_using=nx.Graph()) # wrong... will return symt transition mat
self.G = nx.to_networkx_graph(self.T, create_using=nx.DiGraph()) # reconstructed graph based on transition matrix
# print(nx.adjacency_matrix(self.G).todense()[0:6, 0:6])
# self.rec_G = nx.to_networkx_graph(self.T, create_using=nx.Graph()) # wrong... will return symt transition mat
self.rec_G = nx.to_networkx_graph(self.T, create_using=nx.DiGraph()) # reconstructed "directed" "weighted" graph based on transition matrix
# print(nx.adjacency_matrix(self.rec_G).todense()[0:6, 0:6])
# print(transition_mat[0:6, 0:6])
# print(nx.adjacency_matrix(self.G).todense()==transition_mat)
# print(nx.adjacency_matrix(self.rec_G).todense()==transition_mat)
# alias sampling for ABRW-------------------------
def simulate_walks(self, num_walks, walk_length):
t1 = time.time()
self.preprocess_transition_probs(G=self.G) # construct alias table; adapted from node2vec
self.preprocess_transition_probs(weighted_G=self.rec_G) # construct alias table; adapted from node2vec
t2 = time.time()
print(f'Time for construct alias table: {(t2-t1):.2f}')
walks = []
nodes = list(self.G.nodes())
print('Walk iteration:')
nodes = list(self.rec_G.nodes())
for walk_iter in range(num_walks):
print(str(walk_iter+1), '/', str(num_walks))
t1 = time.time()
# random.seed(2018)
random.shuffle(nodes)
for node in nodes:
walks.append(self.node2vec_walk(G=self.G, walk_length=walk_length, start_node=node))
walks.append(self.weighted_walk(weighted_G=self.rec_G, walk_length=walk_length, start_node=node))
t2 = time.time()
print(f'Walk iteration: {walk_iter+1}/{num_walks}; time cost: {(t2-t1):.2f}')
for i in range(len(walks)): # use ind to retrive orignal node ID
for i in range(len(walks)): # use ind to retrive orignal node ID
for j in range(len(walks[0])):
walks[i][j] = self.look_back_list[int(walks[i][j])]
return walks
def node2vec_walk(self, G, walk_length, start_node): # more efficient way instead of copy from node2vec
alias_nodes = self.alias_nodes
def weighted_walk(self, weighted_G, walk_length, start_node): # more efficient way instead of copy from node2vec
G = weighted_G
walk = [start_node]
while len(walk) < walk_length:
cur = walk[-1]
cur_nbrs = list(G.neighbors(cur))
if len(cur_nbrs) > 0:
walk.append(cur_nbrs[alias_draw(alias_nodes[cur][0], alias_nodes[cur][1])])
else:
break
if len(cur_nbrs) > 0: # if non-isolated node
walk.append(cur_nbrs[alias_draw(self.alias_nodes[cur][0], self.alias_nodes[cur][1])]) # alias sampling in O(1) time to get the index of
else: # if isolated node # 1) randomly choose a nbr; 2) judege if use nbr or its alias
break
return walk
def preprocess_transition_probs(self, G):
def preprocess_transition_probs(self, weighted_G):
''' reconstructed G mush be weighted; \n
return a dict of alias table for each node
'''
G = weighted_G
alias_nodes = {} # unlike node2vec, the reconstructed graph is based on transtion matrix
for node in G.nodes(): # no need to normalize again
probs = [G[node][nbr]['weight'] for nbr in G.neighbors(node)]
alias_nodes[node] = alias_setup(probs)
self.alias_nodes = alias_nodes
probs = [G[node][nbr]['weight'] for nbr in G.neighbors(node)] #pick prob of neighbors with non-zero weight --> sum up to 1.0
#print(f'sum of prob: {np.sum(probs)}')
alias_nodes[node] = alias_setup(probs) #alias table format {node_id: (array1, array2)}
self.alias_nodes = alias_nodes #where array1 gives alias node indexes; array2 gives its prob
#print(self.alias_nodes)
'''
@ -127,20 +129,23 @@ class WeightedWalker:
return self.walks
'''
def deepwalk_walk_wrapper(class_instance, walk_length, start_node):
class_instance.deepwalk_walk(walk_length, start_node)
# ===========================================deepWalk-walker============================================
class BasicWalker:
def __init__(self, G, workers):
self.G = G.G
self.node_size = G.get_num_nodes()
self.look_up_dict = G.look_up_dict
def __init__(self, g, workers):
self.g = g
self.node_size = g.get_num_nodes()
self.look_up_dict = g.look_up_dict
def deepwalk_walk(self, walk_length, start_node):
'''
Simulate a random walk starting from start node.
'''
G = self.G
G = self.g.G
look_up_dict = self.look_up_dict
node_size = self.node_size
@ -159,37 +164,48 @@ class BasicWalker:
'''
Repeatedly simulate random walks from each node.
'''
G = self.G
G = self.g.G
walks = []
nodes = list(G.nodes())
print('Walk iteration:')
for walk_iter in range(num_walks):
t1 = time.time()
# pool = multiprocessing.Pool(processes = 4)
print(str(walk_iter+1), '/', str(num_walks))
random.shuffle(nodes)
for node in nodes:
# walks.append(pool.apply_async(deepwalk_walk_wrapper, (self, walk_length, node, )))
walks.append(self.deepwalk_walk(walk_length=walk_length, start_node=node))
# pool.close()
# pool.join()
t2 = time.time()
print(f'Walk iteration: {walk_iter+1}/{num_walks}; time cost: {(t2-t1):.2f}')
# print(len(walks))
return walks
# ===========================================node2vec-walker============================================
class Walker:
def __init__(self, G, p, q, workers):
self.G = G.G
def __init__(self, g, p, q, workers):
self.g = g
self.p = p
self.q = q
self.node_size = G.node_size
self.look_up_dict = G.look_up_dict
if self.g.get_isweighted():
#print('is weighted graph: ', self.g.get_isweighted())
#print(self.g.get_adj_mat(is_sparse=False)[0:6,0:6])
pass
else: #otherwise, add equal weights 1.0 to all existing edges
#print('is weighted graph: ', self.g.get_isweighted())
self.g.add_edge_weight(equal_weight=1.0) #add 'weight' to networkx graph
#print(self.g.get_adj_mat(is_sparse=False)[0:6,0:6])
self.node_size = g.get_num_nodes()
self.look_up_dict = g.look_up_dict
def node2vec_walk(self, walk_length, start_node):
'''
Simulate a random walk starting from start node.
'''
G = self.G
G = self.g.G
alias_nodes = self.alias_nodes
alias_edges = self.alias_edges
look_up_dict = self.look_up_dict
@ -205,9 +221,7 @@ class Walker:
walk.append(cur_nbrs[alias_draw(alias_nodes[cur][0], alias_nodes[cur][1])])
else:
prev = walk[-2]
pos = (prev, cur)
next = cur_nbrs[alias_draw(alias_edges[pos][0],
alias_edges[pos][1])]
next = cur_nbrs[alias_draw(alias_edges[(prev, cur)][0], alias_edges[(prev, cur)][1])]
walk.append(next)
else:
break
@ -217,22 +231,23 @@ class Walker:
'''
Repeatedly simulate random walks from each node.
'''
G = self.G
G = self.g.G
walks = []
nodes = list(G.nodes())
print('Walk iteration:')
for walk_iter in range(num_walks):
print(str(walk_iter+1), '/', str(num_walks))
t1 = time.time()
random.shuffle(nodes)
for node in nodes:
walks.append(self.node2vec_walk(walk_length=walk_length, start_node=node))
t2 = time.time()
print(f'Walk iteration: {walk_iter+1}/{num_walks}; time cost: {(t2-t1):.2f}')
return walks
def get_alias_edge(self, src, dst):
'''
Get the alias edge setup lists for a given edge.
'''
G = self.G
G = self.g.G
p = self.p
q = self.q
@ -246,18 +261,16 @@ class Walker:
unnormalized_probs.append(G[dst][dst_nbr]['weight']/q)
norm_const = sum(unnormalized_probs)
normalized_probs = [float(u_prob)/norm_const for u_prob in unnormalized_probs]
return alias_setup(normalized_probs)
def preprocess_transition_probs(self):
'''
Preprocessing of transition probabilities for guiding the random walks.
'''
G = self.G
G = self.g.G
alias_nodes = {}
for node in G.nodes():
unnormalized_probs = [G[node][nbr]['weight'] for nbr in G.neighbors(node)]
unnormalized_probs = [G[node][nbr]['weight'] for nbr in G.neighbors(node)] #pick prob of neighbors with non-zero weight
norm_const = sum(unnormalized_probs)
normalized_probs = [float(u_prob)/norm_const for u_prob in unnormalized_probs]
alias_nodes[node] = alias_setup(normalized_probs)
@ -266,16 +279,20 @@ class Walker:
triads = {}
look_up_dict = self.look_up_dict
node_size = self.node_size #to do... node2vec directed and undirected
for edge in G.edges(): #https://github.com/aditya-grover/node2vec/blob/master/src/node2vec.py
alias_edges[edge] = self.get_alias_edge(edge[0], edge[1])
node_size = self.node_size
if self.g.get_isdirected():
for edge in G.edges():
alias_edges[edge] = self.get_alias_edge(edge[0], edge[1])
else: #if undirected, duplicate the reverse direction; otherwise may get key error
for edge in G.edges():
alias_edges[edge] = self.get_alias_edge(edge[0], edge[1])
alias_edges[(edge[1], edge[0])] = self.get_alias_edge(edge[1], edge[0])
self.alias_nodes = alias_nodes
self.alias_edges = alias_edges
return
#========================================= utils: alias sampling method ====================================================
def alias_setup(probs):
'''
Compute utility lists for non-uniform sampling from discrete distributions.
@ -295,7 +312,7 @@ def alias_setup(probs):
else:
larger.append(kk)
while len(smaller) > 0 and len(larger) > 0:
while len(smaller) > 0 and len(larger) > 0: #it is all about use large prob to compensate small prob untill reach the average
small = smaller.pop()
large = larger.pop()
@ -306,7 +323,7 @@ def alias_setup(probs):
else:
larger.append(large)
return J, q
return J, q #the values in J are indexes; it is possible to have repeated indexes if that that index have large prob to compensate others
def alias_draw(J, q):
@ -315,8 +332,8 @@ def alias_draw(J, q):
'''
K = len(J)
kk = int(np.floor(np.random.rand()*K))
if np.random.rand() < q[kk]:
return kk
kk = int(np.floor(np.random.rand()*K)) #randomly choose a nbr (an index)
if np.random.rand() < q[kk]: #use alias table to choose
return kk #either that nbr node (an index)
else:
return J[kk]
return J[kk] #or the nbr's alias node (an index)

View File

@ -86,7 +86,7 @@ def parse_args():
help='balance struc and attr info; ranging [0, inf]')
parser.add_argument('--AttrComb-mode', default='concat', type=str,
help='choices of mode: concat, elementwise-mean, elementwise-max')
parser.add_argument('--Node2Vec-p', default=0.5, type=float,
parser.add_argument('--Node2Vec-p', default=0.5, type=float, #if p=q=1.0 node2vec = deepwalk
help='trade-off BFS and DFS; rid search [0.25; 0.50; 1; 2; 4]')
parser.add_argument('--Node2Vec-q', default=0.5, type=float,
help='trade-off BFS and DFS; rid search [0.25; 0.50; 1; 2; 4]')
@ -179,20 +179,12 @@ def main(args):
elif args.method == 'attrcomb':
model = attrcomb.ATTRCOMB(graph=g, dim=args.dim, comb_with='deepwalk', number_walks=args.number_walks, walk_length=args.walk_length,
window=args.window_size, workers=args.workers, comb_method=args.AttrComb_mode) #comb_method: concat, elementwise-mean, elementwise-max
elif args.method == 'asne':
if args.task == 'nc':
model = asne.ASNE(graph=g, dim=args.dim, alpha=args.ASNE_lamb, epoch=args.epochs, learning_rate=args.learning_rate, batch_size=args.batch_size,
X_test=None, Y_test=None, task=args.task, nc_ratio=args.label_reserved, lp_ratio=args.link_reserved, label_file=args.label_file)
else:
model = asne.ASNE(graph=g, dim=args.dim, alpha=args.ASNE_lamb, epoch=args.epochs, learning_rate=args.learning_rate, batch_size=args.batch_size,
X_test=test_node_pairs, Y_test=test_edge_labels, task=args.task, nc_ratio=args.label_reserved, lp_ratio=args.link_reserved, label_file=args.label_file)
elif args.method == 'deepwalk':
model = node2vec.Node2vec(graph=g, path_length=args.walk_length,
num_paths=args.number_walks, dim=args.dim,
model = node2vec.Node2vec(graph=g, path_length=args.walk_length, num_paths=args.number_walks, dim=args.dim,
workers=args.workers, window=args.window_size, dw=True)
elif args.method == 'node2vec':
model = node2vec.Node2vec(graph=g, path_length=args.walk_length, num_paths=args.number_walks, dim=args.dim,
workers=args.workers, p=args.Node2Vec_p, q=args.Node2Vec_q, window=args.window_size)
workers=args.workers, window=args.window_size, p=args.Node2Vec_p, q=args.Node2Vec_q)
elif args.method == 'grarep':
model = GraRep(graph=g, Kstep=args.GraRep_kstep, dim=args.dim)
elif args.method == 'line':
@ -201,6 +193,13 @@ def main(args):
label_file=args.label_file, clf_ratio=args.label_reserved)
else:
model = line.LINE(g, epoch = args.epochs, rep_size=args.dim, order=args.LINE_order)
elif args.method == 'asne':
if args.task == 'nc':
model = asne.ASNE(graph=g, dim=args.dim, alpha=args.ASNE_lamb, epoch=args.epochs, learning_rate=args.learning_rate, batch_size=args.batch_size,
X_test=None, Y_test=None, task=args.task, nc_ratio=args.label_reserved, lp_ratio=args.link_reserved, label_file=args.label_file)
else:
model = asne.ASNE(graph=g, dim=args.dim, alpha=args.ASNE_lamb, epoch=args.epochs, learning_rate=args.learning_rate, batch_size=args.batch_size,
X_test=test_node_pairs, Y_test=test_edge_labels, task=args.task, nc_ratio=args.label_reserved, lp_ratio=args.link_reserved, label_file=args.label_file)
elif args.method == 'graphsage':
model = graphsageAPI.graphsage_unsupervised_train(graph=g, graphsage_model = 'graphsage_mean')
#we follow the default parameters, see __inti__.py in graphsage file