Node2vec Git源码解读与实战

Graph Embedding简史

传统意义上的 Graph Embedding 被看成是一个降维的过程,将网络投影到欧式空间,但是会造成结点特征的损失。主要的方法包括主成分分析(PCA)和多维缩放(MDS)。所有的方法都可以理解成运用一个 n × k 的矩阵来表示原始的 n × m 矩阵,其中 k << n。
在 2000 年代早期,又提出了其他方法,如 IsoMap 和 LLE,以保持非线性流形的整体结构。总的来说,这些方法都在小型网络上提供了良好的性能。然而这些方法的时间复杂性至少是二次的,这使得它们无法在大规模网络上运行。
在这里插入图片描述
随着深度学习的不断发展,DeepWalk第一个被提出来,使用表示学习(或深度学习)来进行网络的向量化表示。DeepWalk通过将节点视为单词并生成短随机游走(广度优先BFS)作为句子来弥补网络嵌入和单词嵌入之间的差距。然后,可以将诸如Skip-gram之类的神经语言模型应用于这些随机游走以获得网络嵌入。在这里插入图片描述
但是,DeepWalk中的实现是完全随机的,根据Random Walk的不同,后面又衍生出了node2vec算法,解决了DeepWalk定义的结点相似度不能很好反映原网络结构的问题。LINE在其基础上,探究一阶相似度(相邻结点)与二阶相似度(不相邻结点),在一定程度上超脱出了传统方式对网络拓扑结构的依赖。
在这里插入图片描述
但是,Word2vec一众方法都是浅层模型,并且需要根据不同的应用定义相似函数,不易延展。并且对拓扑结构的过度依赖导致鲁棒性不强。因此,GNN出现了。

Node2vec

在Node2vec之前,需要稍稍了解一下一下deepwalk。Deepwalk主要思想是在图上进行结点的序列采样,将采样的结果作为结点的特征,并使用word2vec对序列进行embedding。主要步骤分为两部分:

  1. 第一步为随机游走采样节点序列。
  2. 第二步为使用skip-gram model学习表达向量。

Node2vec就是在DeepWalk的基础上对游走的方式进行了更改,啊具体的原理暂时懒得写,大家可以看node2vec:算法原理,实现和应用。

源码解读

来源于Github的开源代码:https://github.com/eliorc/node2vec。pip安装:

1
pip install node2vec

安装之后的包中主要有三个文件:
在这里插入图片描述
其中,edges.py主要通过训练好的结点向量来生成边的embedding
node2vec.py是生成结点的embedding的向量。
parallel.py用于生成图上的随机游走序列,返回一个walk的list,每一步walk都是一个结点。
在node2vec.py中封装了Node2vec类,其包含三个函数,分别对应node2vec实现的三步:

  1. _precompute_probabilities:生成结点之间的转移概率。
  2. _generate_walks:根据转移概率进行随机游走。
  3. fit:借助gensim.models.Word2Vec对游走之后的结点序列进行拟合,生成结点的embedding。

代码实践

在使用之前,需要在模块安装的路径对文件进行更改,否则此模块名和内部的py文件名重复(都是node2vec),使用时会报错。
在这里插入图片描述
接下来就可以使用了:

1
2
import networkx as nx
from node2vec_.node2vec import Node2Vec  # 注意我的node2vec文件名改成了node2vec_

使用方法与word2vec类似,不过输入一定是一个networkx.graph的类,这个类根据自己实际的情况生成:

1
2
3
4
node2vec = Node2Vec(graph, dimensions=64, walk_length=30, num_walks=200, workers=4)  # Use temp_folder for big graphs

# Embed nodes
model = node2vec.fit(window=10, min_count=1, batch_words=4)  # Any keywords acceptable by gensim.Word2Vec can be passed, `diemnsions` and `workers` are automatically passed (from the Node2Vec constructor)

比如,我使用了movie rating公开数据集生成词之间的共现图:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
def term_graph():
    files = os.listdir('{}/'.format(dataset))
    if '{}_vocab-id.pkl'.format(dataset) not in files:
        print('please run process first')
        exit(0)
    vocabs2id = util.load_word_id(dataset)
    matrix = {}
    with open('{}/{}_cut_train.txt'.format(dataset,dataset), 'r', encoding='utf-8') as trainf:
        for line in trainf.readlines():
            line = line.strip().split()[1:]
            for i in range(0, len(line)-1):
                index1 = vocabs2id[line[i]]
                index2 = vocabs2id[line[i+1]]
                id2 = max(index1, index2)
                id1 = min(index1, index2)
                key = str(id1) + '-' + str(id2)
                if key in matrix.keys():
                    matrix[key] += 1
                else:
                    matrix[key] = 1
    with open('{}/{}_cut_test.txt'.format(dataset,dataset), 'r', encoding='utf-8') as trainf:
        for line in trainf.readlines():
            line = line.strip().split()[1:]
            for i in range(0, len(line)-1):
                try:
                    index1 = vocabs2id[line[i]]
                    index2 = vocabs2id[line[i+1]]
                    id2 = max(index1, index2)
                    id1 = min(index1, index2)
                    key = str(id1) + '-' + str(id2)
                    if key in matrix.keys():
                        matrix[key] += 1
                    else:
                        matrix[key] = 1
                except:
                    continue
    G = nx.Graph()  # 无向图
    size = len(vocabs2id.keys())
    for i in range(size):
        G.add_node(i)
    for key in matrix.keys():
        keys = key.split('-')
        row = keys[0]
        colum = keys[1]
        if row in G.nodes and colum in G.nodes:
            value = matrix[key]
            G.add_edge(row, colum)
    return G

使用如下代码保存训练好之后的向量与模型:

1
2
3
4
5
# Save embeddings for later use
model.wv.save_word2vec_format(EMBEDDING_FILENAME)

# Save model for later use
model.save(EMBEDDING_MODEL_FILENAME)

训练之后的结果是一堆代表向量的数字,起始数字代表结点的id:
在这里插入图片描述
通过node2vec得到的结点向量可以应用到许多下游任务:结点分类,链接预测等。

参考

图神经网络在线研讨会 报告课件 —— 北大宋国杰老师
node2vec:算法原理,实现和应用