diff --git a/.gitignore b/.gitignore index c0f0b7e..853aed0 100644 --- a/.gitignore +++ b/.gitignore @@ -22,3 +22,9 @@ TransX/Tf/result/ Graph/data/cora/README gat/__pycache__/ + +EGES/__pycache__/ + +EGES/data_cache/ + +EGES/data/ diff --git a/EGES/__init__.py b/EGES/__init__.py new file mode 100644 index 0000000..6929806 --- /dev/null +++ b/EGES/__init__.py @@ -0,0 +1,5 @@ +# -*- coding:utf-8 -*- +# @Time : 2021/9/12 12:26 上午 +# @Author : huichuan LI +# @File : __init__.py.py +# @Software: PyCharm diff --git a/EGES/alias.py b/EGES/alias.py new file mode 100644 index 0000000..26c489c --- /dev/null +++ b/EGES/alias.py @@ -0,0 +1,55 @@ +import numpy as np + + +def create_alias_table(area_ratio): + """ + + :param area_ratio: sum(area_ratio)=1 + :return: accept,alias + """ + l = len(area_ratio) + area_ratio = [prop * l for prop in area_ratio] + accept, alias = [0] * l, [0] * l + small, large = [], [] + + for i, prob in enumerate(area_ratio): + if prob < 1.0: + small.append(i) + else: + large.append(i) + + while small and large: + small_idx, large_idx = small.pop(), large.pop() + accept[small_idx] = area_ratio[small_idx] + alias[small_idx] = large_idx + area_ratio[large_idx] = area_ratio[large_idx] - \ + (1 - area_ratio[small_idx]) + if area_ratio[large_idx] < 1.0: + small.append(large_idx) + else: + large.append(large_idx) + + while large: + large_idx = large.pop() + accept[large_idx] = 1 + while small: + small_idx = small.pop() + accept[small_idx] = 1 + + return accept, alias + + +def alias_sample(accept, alias): + """ + + :param accept: + :param alias: + :return: sample index + """ + N = len(accept) + i = int(np.random.random()*N) + r = np.random.random() + if r < accept[i]: + return i + else: + return alias[i] diff --git a/EGES/data_process.py b/EGES/data_process.py new file mode 100644 index 0000000..d1f1394 --- /dev/null +++ b/EGES/data_process.py @@ -0,0 +1,126 @@ +import pandas as pd +import numpy as np +from itertools import chain +import pickle +import time +import networkx as nx + +from sklearn.preprocessing import LabelEncoder +import argparse +from walker import RandomWalker + + +def cnt_session(data, time_cut=30, cut_type=2): + sku_list = data['sku_id'] + time_list = data['action_time'] + type_list = data['type'] + session = [] + tmp_session = [] + for i, item in enumerate(sku_list): + if type_list[i] == cut_type or ( + i < len(sku_list) - 1 and (time_list[i + 1] - time_list[i]).seconds / 60 > time_cut) or i == len( + sku_list) - 1: + tmp_session.append(item) + session.append(tmp_session) + tmp_session = [] + else: + tmp_session.append(item) + return session + + +def get_session(action_data, use_type=None): + if use_type is None: + use_type = [1, 2, 3, 5] + action_data = action_data[action_data['type'].isin(use_type)] + action_data = action_data.sort_values(by=['user_id', 'action_time'], ascending=True) + group_action_data = action_data.groupby('user_id').agg(list) + session_list = group_action_data.apply(cnt_session, axis=1) + return session_list.to_numpy() + + +def get_graph_context_all_pairs(walks, window_size): + all_pairs = [] + for k in range(len(walks)): + for i in range(len(walks[k])): + for j in range(i - window_size, i + window_size + 1): + if i == j or j < 0 or j >= len(walks[k]): + continue + else: + all_pairs.append([walks[k][i], walks[k][j]]) + return np.array(all_pairs, dtype=np.int32) + + +if __name__ == '__main__': + parser = argparse.ArgumentParser(description='manual to this script') + parser.add_argument("--data_path", type=str, default='./data/') + parser.add_argument("--p", type=float, default=0.25) + parser.add_argument("--q", type=float, default=2) + parser.add_argument("--num_walks", type=int, default=10) + parser.add_argument("--walk_length", type=int, default=10) + parser.add_argument("--window_size", type=int, default=5) + args = parser.parse_known_args()[0] + + action_data = pd.read_csv(args.data_path + 'action_head.csv', parse_dates=['action_time']).drop('module_id', + axis=1).dropna() + all_skus = action_data['sku_id'].unique() + all_skus = pd.DataFrame({'sku_id': list(all_skus)}) + sku_lbe = LabelEncoder() + all_skus['sku_id'] = sku_lbe.fit_transform(all_skus['sku_id']) + action_data['sku_id'] = sku_lbe.transform(action_data['sku_id']) + + print('make session list\n') + start_time = time.time() + session_list = get_session(action_data, use_type=[1, 2, 3, 5]) + session_list_all = [] + for item_list in session_list: + for session in item_list: + if len(session) > 1: + session_list_all.append(session) + + print('make session list done, time cost {0}'.format(str(time.time() - start_time))) + + # session2graph + node_pair = dict() + for session in session_list_all: + for i in range(1, len(session)): + if (session[i - 1], session[i]) not in node_pair.keys(): + node_pair[(session[i - 1], session[i])] = 1 + else: + node_pair[(session[i - 1], session[i])] += 1 + + in_node_list = list(map(lambda x: x[0], list(node_pair.keys()))) + out_node_list = list(map(lambda x: x[1], list(node_pair.keys()))) + weight_list = list(node_pair.values()) + graph_df = pd.DataFrame({'in_node': in_node_list, 'out_node': out_node_list, 'weight': weight_list}) + graph_df.to_csv('./data_cache/graph.csv', sep=' ', index=False, header=False) + + G = nx.read_edgelist('./data_cache/graph.csv', create_using=nx.DiGraph(), nodetype=None, data=[('weight', int)]) + walker = RandomWalker(G, p=args.p, q=args.q) + print("Preprocess transition probs...") + walker.preprocess_transition_probs() + + session_reproduce = walker.simulate_walks(num_walks=args.num_walks, walk_length=args.walk_length, workers=4, + verbose=1) + session_reproduce = list(filter(lambda x: len(x) > 2, session_reproduce)) + + # add side info + product_data = pd.read_csv(args.data_path + 'jdata_product.csv').drop('market_time', axis=1).dropna() + + all_skus['sku_id'] = sku_lbe.inverse_transform(all_skus['sku_id']) + print("sku nums: " + str(all_skus.count())) + sku_side_info = pd.merge(all_skus, product_data, on='sku_id', how='left').fillna(0) + + # id2index + for feat in sku_side_info.columns: + if feat != 'sku_id': + lbe = LabelEncoder() + sku_side_info[feat] = lbe.fit_transform(sku_side_info[feat]) + else: + sku_side_info[feat] = sku_lbe.transform(sku_side_info[feat]) + + sku_side_info = sku_side_info.sort_values(by=['sku_id'], ascending=True) + sku_side_info.to_csv('./data_cache/sku_side_info.csv', index=False, header=False, sep='\t') + + # # get pair + all_pairs = get_graph_context_all_pairs(session_reproduce, args.window_size) + np.savetxt('./data_cache/all_pairs', X=all_pairs, fmt="%d", delimiter=" ") diff --git a/EGES/eges.py b/EGES/eges.py new file mode 100644 index 0000000..44f9852 --- /dev/null +++ b/EGES/eges.py @@ -0,0 +1,99 @@ +# -*- coding:utf-8 -*- +# @Time : 2021/9/12 12:26 上午 +# @Author : huichuan LI +# @File : eges.py +# @Software: PyCharm +import numpy as np +import tensorflow as tf +from tensorflow import keras + + +class EGES_Model(keras.Model): + def __init__(self, num_nodes, num_feat, feature_lens, n_sampled=100, embedding_dim=128, lr=0.001, **kwargs): + self.n_samped = n_sampled + self.num_feat = num_feat + self.feature_lens = feature_lens + self.embedding_dim = embedding_dim + self.num_nodes = num_nodes + self.lr = lr + self.num_nodes = num_nodes + self.embedding_dim = embedding_dim + super(EGES_Model, self).__init__(**kwargs) + + def build(self, input_shapes): + # noise-contrastive estimation + self.nce_w = self.add_weight( + name="nce_w", shape=[self.num_nodes, self.embedding_dim], + initializer=keras.initializers.TruncatedNormal(0., 0.1)) # [n_vocab, emb_dim] + self.nce_b = self.add_weight( + name="nce_b", shape=(self.num_nodes,), + initializer=keras.initializers.Constant(0.1)) # [n_vocab, ] + + cat_embedding_vars = [] + for i in range(self.num_feat): + embedding_var = self.add_weight( + shape=[self.feature_lens[i], self.embedding_dim] + , initializer=keras.initializers.TruncatedNormal(0., 0.1), + name='embedding' + str(i), + trainable=True) + cat_embedding_vars.append(embedding_var) + self.cat_embedding = cat_embedding_vars + self.alpha_embedding = self.add_weight( + name="nce_b", shape=(self.num_nodes, self.num_feat), + initializer=keras.initializers.Constant(0.1)) + + def attention_merge(self): + embed_list = [] + for i in range(self.num_feat): + cat_embed = tf.nn.embedding_lookup(self.cat_embedding[i], self.batch_features[:, i]) + embed_list.append(cat_embed) + stack_embed = tf.stack(embed_list, axis=-1) + # attention merge + alpha_embed = tf.nn.embedding_lookup(self.alpha_embedding, self.batch_features[:, 0]) + alpha_embed_expand = tf.expand_dims(alpha_embed, 1) + alpha_i_sum = tf.reduce_sum(tf.exp(alpha_embed_expand), axis=-1) + merge_emb = tf.reduce_sum(stack_embed * tf.exp(alpha_embed_expand), axis=-1) / alpha_i_sum + return merge_emb + + def make_skipgram_loss(self, labels): + loss = tf.reduce_mean(tf.nn.sampled_softmax_loss( + weights=self.nce_w, + biases=self.nce_b, + labels=tf.expand_dims(labels, axis=1), + inputs=self.merge_emb, + num_sampled=self.n_samped, + num_classes=self.num_nodes)) + + return loss + + def call(self, side_info, batch_index, batch_labels): + self.side_info = tf.convert_to_tensor(side_info) + self.batch_features = tf.nn.embedding_lookup(self.side_info, batch_index) + + embed_list = [] + for i in range(self.num_feat): + cat_embed = tf.nn.embedding_lookup(self.cat_embedding[i], self.batch_features[:, i]) + embed_list.append(cat_embed) + stack_embed = tf.stack(embed_list, axis=-1) + # attention merge + alpha_embed = tf.nn.embedding_lookup(self.alpha_embedding, self.batch_features[:, 0]) + alpha_embed_expand = tf.expand_dims(alpha_embed, 1) + alpha_i_sum = tf.reduce_sum(tf.exp(alpha_embed_expand), axis=-1) + self.merge_emb = tf.reduce_sum(stack_embed * tf.exp(alpha_embed_expand), axis=-1) / alpha_i_sum + + return self.make_skipgram_loss(batch_labels) + + def get_embedding(self, batch_index): + self.batch_features = tf.nn.embedding_lookup(self.side_info, batch_index) + + embed_list = [] + for i in range(self.num_feat): + cat_embed = tf.nn.embedding_lookup(self.cat_embedding[i], self.batch_features[:, i]) + embed_list.append(cat_embed) + stack_embed = tf.stack(embed_list, axis=-1) + # attention merge + alpha_embed = tf.nn.embedding_lookup(self.alpha_embedding, self.batch_features[:, 0]) + alpha_embed_expand = tf.expand_dims(alpha_embed, 1) + alpha_i_sum = tf.reduce_sum(tf.exp(alpha_embed_expand), axis=-1) + merge_emb = tf.reduce_sum(stack_embed * tf.exp(alpha_embed_expand), axis=-1) / alpha_i_sum + return merge_emb diff --git a/EGES/run_eges.py b/EGES/run_eges.py new file mode 100644 index 0000000..223a938 --- /dev/null +++ b/EGES/run_eges.py @@ -0,0 +1,87 @@ +# -*- coding:utf-8 -*- +# @Time : 2021/9/12 12:11 下午 +# @Author : huichuan LI +# @File : run_eges.py +# @Software: PyCharm + + +import pandas as pd +import numpy as np +import tensorflow as tf +import time +import argparse + +from eges import EGES_Model + +if __name__ == '__main__': + parser = argparse.ArgumentParser(description='manual to this script') + parser.add_argument("--batch_size", type=int, default=2048) + parser.add_argument("--n_sampled", type=int, default=10) + parser.add_argument("--epochs", type=int, default=1) + parser.add_argument("--lr", type=float, default=0.001) + parser.add_argument("--root_path", type=str, default='./data_cache/') + parser.add_argument("--num_feat", type=int, default=4) + parser.add_argument("--embedding_dim", type=int, default=128) + parser.add_argument("--outputEmbedFile", type=str, default='./embedding/EGES.embed') + args = parser.parse_args() + + # read train_data + print('read features...') + start_time = time.time() + side_info = np.loadtxt(args.root_path + 'sku_side_info.csv', dtype=np.int32, delimiter='\t') + feature_lens = [] + for i in range(side_info.shape[1]): + tmp_len = len(set(side_info[:, i])) + feature_lens.append(tmp_len) + end_time = time.time() + print('time consumed for read features: %.2f' % (end_time - start_time)) + + + # read data_pair by tf.dataset + def decode_data_pair(line): + columns = tf.strings.split([line], ' ') + x = tf.strings.to_number(columns.values[0], out_type=tf.int32) + y = tf.strings.to_number(columns.values[1], out_type=tf.int32) + return x, y + + + dataset = tf.data.TextLineDataset(args.root_path + 'all_pairs').map(decode_data_pair, + num_parallel_calls=tf.data.AUTOTUNE).prefetch( + 500000) + # dataset = dataset.shuffle(256) + dataset = dataset.repeat(args.epochs) + dataset = dataset.batch(args.batch_size) # Batch size to use + iterator = tf.compat.v1.data.make_one_shot_iterator( + dataset + ) + + print('read embedding...') + start_time = time.time() + EGES = EGES_Model(len(side_info), args.num_feat, feature_lens, + n_sampled=args.n_sampled, embedding_dim=args.embedding_dim, lr=args.lr) + end_time = time.time() + print('time consumed for read embedding: %.2f' % (end_time - start_time)) + opt = tf.keras.optimizers.Adam(0.01) + print_every_k_iterations = 100 + iteration = 0 + start = time.time() + while iterator: + iteration += 1 + + batch_index, batch_labels = iterator.get_next() + with tf.GradientTape() as tape: + loss = EGES(side_info, batch_index, batch_labels) + gradients = tape.gradient(loss, EGES.trainable_variables) + opt.apply_gradients(zip(gradients, EGES.trainable_variables)) + # 计算梯度 + # 根据梯度值更新参数值 + if iteration % print_every_k_iterations == 0: + end = time.time() + print("Iteration: {}".format(iteration), + "Avg. Training loss: {:.4f}".format(loss / print_every_k_iterations), + "{:.4f} sec/batch".format((end - start) / print_every_k_iterations)) + start = time.time() + + + + print(EGES.get_embedding(side_info[:, 0])) diff --git a/EGES/utils.py b/EGES/utils.py new file mode 100644 index 0000000..d5f17db --- /dev/null +++ b/EGES/utils.py @@ -0,0 +1,105 @@ +import numpy as np +import matplotlib.pyplot as plt +from sklearn.manifold import TSNE + + +def plot_embeddings(embebed_mat, side_info_mat): + model = TSNE(n_components=2) + node_pos = model.fit_transform(embebed_mat) + brand_color_idx, shop_color_idx, cate_color_idx = {}, {}, {} + for i in range(len(node_pos)): + brand_color_idx.setdefault(side_info_mat[i, 1], []) + brand_color_idx[side_info_mat[i, 1]].append(i) + shop_color_idx.setdefault(side_info_mat[i, 2], []) + shop_color_idx[side_info_mat[i, 2]].append(i) + cate_color_idx.setdefault(side_info_mat[i, 3], []) + cate_color_idx[side_info_mat[i, 3]].append(i) + + plt.figure() + for c, idx in brand_color_idx.items(): + plt.scatter(node_pos[idx, 0], node_pos[idx, 1], label=c) # c=node_colors) + plt.title('brand distribution') + plt.savefig('./data_cache/brand_dist.png') + + plt.figure() + for c, idx in shop_color_idx.items(): + plt.scatter(node_pos[idx, 0], node_pos[idx, 1], label=c) # c=node_colors) + plt.title('shop distribution') + plt.savefig('./data_cache/shop_dist.png') + + plt.figure() + for c, idx in cate_color_idx.items(): + plt.scatter(node_pos[idx, 0], node_pos[idx, 1], label=c) # c=node_colors) + plt.title('cate distribution') + plt.savefig('./data_cache/cate_dist.png') + + + +def write_embedding(embedding_result, outputFileName): + f = open(outputFileName, 'w') + for i in range(len(embedding_result)): + s = " ".join(str(f) for f in embedding_result[i].tolist()) + f.write(s + "\n") + f.close() + + +def graph_context_batch_iter(all_pairs, batch_size, side_info, num_features): + while True: + start_idx = np.random.randint(0, len(all_pairs) - batch_size) + batch_idx = np.array(range(start_idx, start_idx + batch_size)) + batch_idx = np.random.permutation(batch_idx) + batch = np.zeros((batch_size, num_features), dtype=np.int32) + labels = np.zeros((batch_size, 1), dtype=np.int32) + batch[:] = side_info[all_pairs[batch_idx, 0]] + labels[:, 0] = all_pairs[batch_idx, 1] + yield batch, labels + + +def preprocess_nxgraph(graph): + node2idx = {} + idx2node = [] + node_size = 0 + for node in graph.nodes(): + node2idx[node] = node_size + idx2node.append(node) + node_size += 1 + return idx2node, node2idx + + +def partition_dict(vertices, workers): + batch_size = (len(vertices) - 1) // workers + 1 + part_list = [] + part = [] + count = 0 + for v1, nbs in vertices.items(): + part.append((v1, nbs)) + count += 1 + if count % batch_size == 0: + part_list.append(part) + part = [] + if len(part) > 0: + part_list.append(part) + return part_list + + +def partition_list(vertices, workers): + batch_size = (len(vertices) - 1) // workers + 1 + part_list = [] + part = [] + count = 0 + for v1, nbs in enumerate(vertices): + part.append((v1, nbs)) + count += 1 + if count % batch_size == 0: + part_list.append(part) + part = [] + if len(part) > 0: + part_list.append(part) + return part_list + + +def partition_num(num, workers): + if num % workers == 0: + return [num//workers]*workers + else: + return [num//workers]*workers + [num % workers] diff --git a/EGES/walker.py b/EGES/walker.py new file mode 100644 index 0000000..dba5277 --- /dev/null +++ b/EGES/walker.py @@ -0,0 +1,217 @@ +import itertools +import math +import random + +import numpy as np +import pandas as pd +from joblib import Parallel, delayed +from tqdm import trange + +from alias import alias_sample, create_alias_table +from utils import partition_num + + +class RandomWalker: + def __init__(self, G, p=1, q=1): + """ + :param G: + :param p: Return parameter,controls the likelihood of immediately revisiting a node in the walk. + :param q: In-out parameter,allows the search to differentiate between “inward” and “outward” nodes + """ + self.G = G + self.p = p + self.q = q + + def deepwalk_walk(self, walk_length, start_node): + + walk = [start_node] + + while len(walk) < walk_length: + cur = walk[-1] + cur_nbrs = list(self.G.neighbors(cur)) + if len(cur_nbrs) > 0: + walk.append(random.choice(cur_nbrs)) + else: + break + return walk + + def node2vec_walk(self, walk_length, start_node): + + G = self.G + alias_nodes = self.alias_nodes + alias_edges = self.alias_edges + + walk = [start_node] + + while len(walk) < walk_length: + cur = walk[-1] + cur_nbrs = list(G.neighbors(cur)) + if len(cur_nbrs) > 0: + if len(walk) == 1: + walk.append( + cur_nbrs[alias_sample(alias_nodes[cur][0], alias_nodes[cur][1])]) + else: + prev = walk[-2] + edge = (prev, cur) + next_node = cur_nbrs[alias_sample(alias_edges[edge][0], + alias_edges[edge][1])] + walk.append(next_node) + else: + break + + return walk + + def simulate_walks(self, num_walks, walk_length, workers=1, verbose=0): + + G = self.G + + nodes = list(G.nodes()) + + results = Parallel(n_jobs=workers, verbose=verbose, )( + delayed(self._simulate_walks)(nodes, num, walk_length) for num in + partition_num(num_walks, workers)) + + walks = list(itertools.chain(*results)) + + return walks + + def _simulate_walks(self, nodes, num_walks, walk_length,): + walks = [] + for _ in range(num_walks): + random.shuffle(nodes) + for v in nodes: + if self.p == 1 and self.q == 1: + walks.append(self.deepwalk_walk( + walk_length=walk_length, start_node=v)) + else: + walks.append(self.node2vec_walk( + walk_length=walk_length, start_node=v)) + return walks + + def get_alias_edge(self, t, v): + """ + compute unnormalized transition probability between nodes v and its neighbors give the previous visited node t. + :param t: + :param v: + :return: + """ + G = self.G + p = self.p + q = self.q + + unnormalized_probs = [] + for x in G.neighbors(v): + weight = G[v][x].get('weight', 1.0) # w_vx + if x == t: # d_tx == 0 + unnormalized_probs.append(weight/p) + elif G.has_edge(x, t): # d_tx == 1 + unnormalized_probs.append(weight) + else: # d_tx > 1 + unnormalized_probs.append(weight/q) + norm_const = sum(unnormalized_probs) + normalized_probs = [ + float(u_prob)/norm_const for u_prob in unnormalized_probs] + + return create_alias_table(normalized_probs) + + def preprocess_transition_probs(self): + """ + Preprocessing of transition probabilities for guiding the random walks. + """ + G = self.G + + alias_nodes = {} + for node in G.nodes(): + unnormalized_probs = [G[node][nbr].get('weight', 1.0) #保存start的邻居节点的权重 + for nbr in G.neighbors(node)] + norm_const = sum(unnormalized_probs) + normalized_probs = [ + float(u_prob)/norm_const for u_prob in unnormalized_probs] #计算从node到邻居的转移矩阵 + alias_nodes[node] = create_alias_table(normalized_probs) + + alias_edges = {} + + for edge in G.edges(): + alias_edges[edge] = self.get_alias_edge(edge[0], edge[1]) + + self.alias_nodes = alias_nodes + self.alias_edges = alias_edges + + return + + +class BiasedWalker: + def __init__(self, idx2node, temp_path): + + self.idx2node = idx2node + self.idx = list(range(len(self.idx2node))) + self.temp_path = temp_path + pass + + def simulate_walks(self, num_walks, walk_length, stay_prob=0.3, workers=1, verbose=0): + + layers_adj = pd.read_pickle(self.temp_path+'layers_adj.pkl') + layers_alias = pd.read_pickle(self.temp_path+'layers_alias.pkl') + layers_accept = pd.read_pickle(self.temp_path+'layers_accept.pkl') + gamma = pd.read_pickle(self.temp_path+'gamma.pkl') + walks = [] + initialLayer = 0 + + nodes = self.idx # list(self.g.nodes()) + + results = Parallel(n_jobs=workers, verbose=verbose, )( + delayed(self._simulate_walks)(nodes, num, walk_length, stay_prob, layers_adj, layers_accept, layers_alias, gamma) for num in + partition_num(num_walks, workers)) + + walks = list(itertools.chain(*results)) + return walks + + def _simulate_walks(self, nodes, num_walks, walk_length, stay_prob, layers_adj, layers_accept, layers_alias, gamma): + walks = [] + for _ in range(num_walks): + random.shuffle(nodes) + for v in nodes: + walks.append(self._exec_random_walk(layers_adj, layers_accept, layers_alias, + v, walk_length, gamma, stay_prob)) + return walks + + def _exec_random_walk(self, graphs, layers_accept, layers_alias, v, walk_length, gamma, stay_prob=0.3): + initialLayer = 0 + layer = initialLayer + + path = [] + path.append(self.idx2node[v]) + + while len(path) < walk_length: + r = random.random() + if(r < stay_prob): # same layer + v = chooseNeighbor(v, graphs, layers_alias, + layers_accept, layer) + path.append(self.idx2node[v]) + else: # different layer + r = random.random() + try: + x = math.log(gamma[layer][v] + math.e) + p_moveup = (x / (x + 1)) + except: + print(layer, v) + raise ValueError() + + if(r > p_moveup): + if(layer > initialLayer): + layer = layer - 1 + else: + if((layer + 1) in graphs and v in graphs[layer + 1]): + layer = layer + 1 + + return path + + +def chooseNeighbor(v, graphs, layers_alias, layers_accept, layer): + + v_list = graphs[layer][v] + + idx = alias_sample(layers_accept[layer][v], layers_alias[layer][v]) + v = v_list[idx] + + return v