DeepWalk : DeepWalk: Online Learning of Social Representations发表于KDD 14年。
网络表示学习最近两年非常火热,这里的网络Network和图Graph意思相同,不再做区分。网络表示学习故名思义,是面向网络结构节点或者整个图的表示学习。
输入一个G=(V,E),V包含了网络中的节点,E包含了网络中的连边。传统的网络表表达方法,我们只需要,一个邻接矩阵A来表达整个网络的连边关系。网络表示学习的基础任务是给每个在V中的节点学习一个向量,当然也可以为E中的每条边学习一个向量表达。
网络表示学习是很早之前就有的任务,但是自从在顶级会议KDD 14年,DeepWalk提出来之后才渐渐火起来,到这两年火得一塌糊涂。如果你还不了解真的Out了。
话不多说,直接教你一步步实现DeepWalk模型,不用太久,只需要十分钟。
1.Word2vec
前提是需要了解Word2vec,不熟悉里面的公式也没关系,起码知道Word2vec的输入输出。输入是一堆训练的文字序列,输出是每个词语都有一个表示学习的向量。
例如:输入
[ ['I', 'Love', 'Food'],
['I', 'Like', 'Playing', 'Football']]
得到: 'Love'和‘Playing’的表示向量会相对较近。
2.Random Walk
随机游走,输入是一个图,这里用Networkx的图模型来表达,不熟悉Networkx的也没关系,可以看我写的Networkx系列。
我们就采用常用的BlogCatatlog数据,
导入数据
import math
import networkx as nx
import random
import numpy as np
file_name = 'data/blogCatalog/bc_edgelist.txt'
graph_format = 'edgelist'
if graph_format == 'edgelist':
g = nx.read_edgelist(file_name)
for edge in g.edges():
u, v = edge[0], edge[1]
g[u][v]['weight'] = 1.0
len(g)
# 10312
随机采样:每条长度 walk_length = 80,每个节点采样 number_walks = 15
def deepwalk_walk(g, walk_length=40, start_node=None):
walks = [start_node]
while len(walks) < walk_length:
cur = walks[-1]
cur_nbs = list(g.neighbors(cur))
if len(cur_nbs) > 0:
walks.append(random.choice(cur_nbs))
else:
raise ValueError('node with 0 in_degree')
return walks
def sample_walks(g, walk_length=40, number_walks=10, shuffle=True):
total_walks = []
print('Start sampling walks:')
nodes = list(g.nodes())
for iter_ in range(number_walks):
print('\t iter:{} / {}'.format(iter_+1, number_walks))
if shuffle:
random.shuffle(nodes)
for node in nodes:
total_walks.append(deepwalk_walk(g, walk_length, node))
return total_walks
walk_length = 80
number_walks = 15
total_walks = sample_walks(g, walk_length=walk_length, number_walks=number_walks)
Start sampling walks:
iter:1 / 15
iter:2 / 15
iter:3 / 15
iter:4 / 15
iter:5 / 15
iter:6 / 15
iter:7 / 15
iter:8 / 15
iter:9 / 15
iter:10 / 15
iter:11 / 15
iter:12 / 15
iter:13 / 15
iter:14 / 15
iter:15 / 15
3. 训练
采样完路径后开始训练,训练非常简单,这里使用gensim。注意新版的gensim在设置workers时可能会出错。例如把workers=-1.
from gensim.models import Word2Vec
emb_size = 128
window_size = 10
model_w2v = Word2Vec(sentences=total_sents, sg=1, hs=0, size=emb_size, window=window_size,\
min_count=0, workers=10, iter=10)
训练好了,可以检查任何节点在嵌入空间中的表达,例如:
model_w2v['0'],会输出一个128维的向量。
array([-2.4944900e-02, 8.3439365e-02, -3.0993205e-01, -1.3582650e-01,
5.8874243e-01, -1.2916335e-01, 1.1895838e-01, -6.6258423e-03,
7.1198274e-03, -3.4604724e-02, 2.7921599e-01, 2.0141172e-01,
2.5975418e-01, -1.3706718e-01, 1.4594914e-01, 3.4300745e-01,...])
4.测试效果
读入label,建立一个罗辑回归分类模型,并且嵌入一个multi-label的分类器中,测试效果:
def read_node_label(filename):
fin = open(filename, 'r')
X = []
Y = []
while 1:
l = fin.readline()
if l == '':
break
vec = l.strip().split(' ')
X.append(vec[0])
Y.append(vec[1:])
fin.close()
return X, Y
from sklearn.linear_model import LogisticRegression
file_name = 'data/blogCatalog/bc_labels.txt'
X, Y = read_node_label(file_name)
clf = Classifier(vectors=model_w2v.wv, clf=LogisticRegression(solver='liblinear'))
for split_ratio in [0.1, 0.3, 0.5]:
print(clf.split_train_evaluate(X, Y, split_ratio))
分别以取[0.1, 0.3, ....0.9] 的数据作为训练集,剩下的作为测试集。
'micro': 0.31360220825026836, 'macro': 0.16568788514916136
可以看到,结果和论文还是有差别的,有人也在文章中反应,实现不到原来论文里的效果。可能是由于gensim以及一些参数的原因,略有不同,也可以理解。例如设walk-length=40 ,但耗时太久。
这就是顶级会议KDD论文DeepWalk的全部工作,谢谢关注。下篇文章我要解释下DeepWalk的论文公式和Node2vec方法。
参考了代码:https://github.com/phanein/deepwalk
测试还需要这个类:
import numpy
from sklearn.multiclass import OneVsRestClassifier
from sklearn.metrics import f1_score
from sklearn.preprocessing import MultiLabelBinarizer
class TopKRanker(OneVsRestClassifier):
def predict(self, X, top_k_list):
probs = numpy.asarray(super(TopKRanker, self).predict_proba(X))
all_labels = []
for i, k in enumerate(top_k_list):
probs_ = probs[i, :]
labels = self.classes_[probs_.argsort()[-k:]].tolist()
probs_[:] = 0
probs_[labels] = 1
all_labels.append(probs_)
return numpy.asarray(all_labels)
class Classifier(object):
def __init__(self, vectors, clf):
self.embeddings = vectors
self.clf = TopKRanker(clf)
self.binarizer = MultiLabelBinarizer(sparse_output=True)
def train(self, X, Y, Y_all):
self.binarizer.fit(Y_all)
X_train = [self.embeddings[x] for x in X]
Y = self.binarizer.transform(Y)
self.clf.fit(X_train, Y)
def evaluate(self, X, Y):
top_k_list = [len(l) for l in Y]
Y_ = self.predict(X, top_k_list)
Y = self.binarizer.transform(Y)
averages = ["micro", "macro", "samples", "weighted"]
results = {}
for average in averages:
results[average] = f1_score(Y, Y_, average=average)
print(results)
return results
def predict(self, X, top_k_list):
X_ = numpy.asarray([self.embeddings[x] for x in X])
Y = self.clf.predict(X_, top_k_list=top_k_list)
return Y
def split_train_evaluate(self, X, Y, train_precent, seed=0):
state = numpy.random.get_state()
training_size = int(train_precent * len(X))
numpy.random.seed(seed)
shuffle_indices = numpy.random.permutation(numpy.arange(len(X)))
X_train = [X[shuffle_indices[i]] for i in range(training_size)]
Y_train = [Y[shuffle_indices[i]] for i in range(training_size)]
X_test = [X[shuffle_indices[i]] for i in range(training_size, len(X))]
Y_test = [Y[shuffle_indices[i]] for i in range(training_size, len(X))]
self.train(X_train, Y_train, Y)
numpy.random.set_state(state)
return self.evaluate(X_test, Y_test)