Struc2Vec论文浅见

u与v两个节点之间是不相邻的,但是结构相似

1. Abstract

在过往很多的Graph embedding都是通过节点的相似度组织语料,如node2vec,deepwalk都是基于根据邻居节点的相似度来组织语料,然后使用word2vec来进行训练.这种无监督的训练方式将相似的节点进行聚合,然而他们更加关注"相邻性",而忽视了"结构相似性".虽然node2vec也考虑到结构性(structural),但实际上它仍然使用一种相邻节点的游走策略组织语料,它使用p, q两个参数来平衡着两个属性.

以v节点为例子,判断下一跳的概率为多少

本文介绍的Struc2Vec主要考虑节点的结构性并且使用多层将不同跳数的节点组织起来,使用alias算法进行采样和转移概率的方式组织训练所需要的语料,最后通过word2vec将其训练.当然代码和算法逻辑相对于node2vec和deepwalk更加复杂,抱着学习的心态我们一起来学习一下struc2Vec的算法核心和代码逻辑吧

2. Main Work

struc2vec利用节点的度来衡量两个节点之间的节点相似性,再通过节点的跳数对节点进行分层.进而构建了多层的结构相似度拓扑图.

这其实与node2veec以及deepwalk是很不同的,前者是关注于拓扑图的结构相似度,后者则是关注于节点的相邻性.因此虽然两种方法都是进行无监督学习,但是聚类的目的是不同,不能绝对地说两者优劣.那么接下来我们先说一下struc2vec是如何构建的.

2.1 weight compute

论文当中给出以下的公式来计算两个节点直接的距离和权值


u,v两个节点的结构距离

上面参数有一下解释

  1. f_{k}(u, v): u, v两个节点的在第k跳的结构相似度
  2. R_k(u): u节点的第k跳的相邻节点集合
  3. s(D): 节点集合D的节点度的有序序列,如节点集合D每个节点的度为[2, 34, 5, 1, 2],那么s(D)为[1, 2, 2, 5, 34]
  4. g(S1, S2): 计算两个节点度的有序序列的距离,考虑到S1, S2两个序列的长度不一定一样,因此使用DTW进行对齐,有趣去的朋友可以看一下这个链接(Dynamic Time Warping)动态时间规整,这里就不详细介绍了

对于g(S1, S2),我们会计算序列当中对齐的每一个值来运算其序列的距离.

d函数计算根据度计算两个节点的距离

S1和S2对齐后,可以得到一对值a属于S1, b属于S2,以便后续求和得到两个集合的实际距离.回顾之前这一堆繁杂的公式,其本质上是:计算u, v两个节点的结构距离就是找出每一跳的邻居节点集合R_k(第k跳的邻居节点结合),根据两个节点的邻居节点集合R_k计算出集合当中节点的度,接着排序好每个度值再对齐,接着计算出u, v两个节点在第k跳的距离相似度.
有了两个节点的距离就可以求解两个节点之间的权值了:
节点之间权值

相对于求距离的计算公式,这个更加容易理解.到这里存在一个问题:假如要计算每对节点的权值可是十分耗时,而且k跳也是增加计算量.因此如何将该算法简化成为论文的另外一个亮点.因此struc2vec作者提出了优化点:

  1. 缩短order degree sequences的长度(前文提到的节点度的有序序列)
  2. 限制计算节点的对数(reduce the number of pairwise)
  3. 限制k的值,也就是layers的数量

在实际当中,order degree sequences会出现大量重复的值因为节点的度分布是长尾分布,因此使用(degree, the number of node)作为pair来缩短序列长度.node的数量其实是节点度数为degree的节点数量.然后使用另外一种距离函数来计算pairwise的距离

dist函数计算根据度计算两个节点的距离

我们用(a0, a1), (a1, b1)作为两个节点的pair值,函数当中打括号部分跟前面的距离函数一样的,但是多出max(a1, b1)这是因为相对于之前order degree sequences存在大量重复的值,我们通过统计dergee的节点个数压缩了其长度那么计算的时候就要乘以其最大的重复数量.接着就是限制了计算的节点对数,假如所有的数量都进入计算,那么计算个数是n*(n-1)/2.因此论文作者设置了限制计算节点的个数为: log(n).同时也限制了k的个数,也是后面的待会会谈到的layers数量.
下面是计算结构距离的函数_compute_structural_distance

    def _compute_structural_distance(self, max_num_layers, workers=1, verbose=0):
        # return: 
        #     dtw_dict = {
        #         (v1, v2): {
        #             layer1: dist1, 
        #             layer2: dist2,
        #             ...
        #         },
        #         (v3, v4): {
        #             layer1: dist1, 
        #             layer2: dist2,
        #             ...
        #         }, ...
        #     }
        if os.path.exists(self.temp_path + 'structural_dist.pkl'):
            structural_dist = pd.read_pickle(self.temp_path+'structural_dist.pkl')
        else:
            if self.opt1_reduce_len:
                dist_func = cost_max
            else:
                dist_func = cost
            if os.path.exists(self.temp_path + 'degreelist.pkl'):
                degree_list = pd.read_pickle(self.temp_path + 'degreelist.pkl')
            else:
                # 构建每个节点,max_layers跳(keys: 第i跳,value: [(degree, #node)])
                degree_list = self._compute_ordered_degreelist(max_num_layers)
                pd.to_pickle(degree_list, self.temp_path + 'degreelist.pkl')
            if self.opt2_reduce_sim_calc:
                # 创建节点相邻的degrees dict,以degree为key,其value有vertices,before,after
                degrees = self._create_vectors()
                degree_list_selected = {}
                vertices = {}
                n_nodes = len(self.graph.nodes())
                for v in self.idx:
                    # 获取相邻的节点(度数越相近越相邻),主要考虑结构相似度
                    node = self.idx2node[v]
                    nbs = get_vertices(v, len(self.graph[node]), degrees, n_nodes)
                    vertices[v] = nbs
                    # 递归获取一定量的相邻节点
                    # 获取v节点下,不同层的dergee信息(按照degree进行排序的节点个数)
                    degree_list_selected[v] = degree_list[v]
                    for n in nbs:
                        degree_list_selected[n] = degree_list[n]
            else:
                vertices = {}
                for v in degree_list:
                    vertices[v] = [vd for vd in degree_list.keys() if vd > v]
            results = Parallel(n_jobs=workers, verbose=verbose,)(delayed(compute_dtw_dist)(part_list, degree_list, dist_func) for part_list in partition_dict(vertices, workers))
            dtw_dist = dict((ChainMap(*results)))
            structural_dist = convert_dtw_struc_dist(dtw_dist)
            pd.to_pickle(structural_dist, self.temp_path + 'structural_dist.pkl')
        return structural_dist

构建不同度的值的节点列表_create_vectors

    def _create_vectors(self):
        # 获取每个度值的节点,并按照degree进行排序,记录下该degree下的节点有哪些,前后两个度值的节点有哪些
        # degrees = {
        #   degree_value1: {
        #       'vertices': [v1, v2, ...], 
        #       'before': [v1, v2, ...], 
        #       'after': [v1, v2, ...]} 
        #   },
        #   degree_value2: {
        #       'vertices': [v1, v2, ...], 
        #       'before': [v1, v2, ...], 
        #       'after': [v1, v2, ...]} 
        #   }, 
        #   ....
        # }
        degrees = {} # key: graph中的度数,value: 度数的节点
        degrees_sorted = set() # 存放graph当中的度数
        for v in self.idx:
            degree = len(self.graph[self.idx2node[v]])
            degrees_sorted.add(degree)
            if (degree not in degrees):
                degrees[degree] = {}
                degrees[degree]['vertices'] = []
            degrees[degree]['vertices'].append(v)
        degrees_sorted = np.array(list(degrees_sorted), dtype='int')
        degrees_sorted = np.sort(degrees_sorted)

        l = len(degrees_sorted)
        for index, degree in enumerate(degrees_sorted):
            if (index > 0):
                # 记录比该度数小的前一个度数值
                degrees[degree]['before'] = degrees_sorted[index - 1]
            if (index < (l - 1)):
                # 记录比该度数大的后一个度数值
                degrees[degree]['after'] = degrees_sorted[index + 1]
        return degrees

下面是_compute_ordered_degreelist是构建节点的度和对应的节点个数,_get_order_degreelist_node是构建节点的有序度序列,它本质上层次遍历.

    def _compute_ordered_degreelist(self, max_num_layers):
        # 构建多层关于度排序的节点数量,用于计算节点之间的距离
        degree_list = {}
        vertices = self.idx
        for v in vertices:
            # 根据不同的node建立不同的树
            # degree_list =
            # v : {
            #   level_1: {
            #       (degree_1, number of node), 
            #       (degree_2, number of node)
            #   }, 
            #   level_2: {....}}
            degree_list[v] = self._get_order_degreelist_node(v, max_num_layers)
        return degree_list

    def _get_order_degreelist_node(self, root, max_num_layers=None):
        # 顶点对距离计算
        # 用一个循环去计算每个顶点对应的有序度序列。
        if max_num_layers is None:
            max_num_layers = float('inf')
        ordered_degree_sequence_dict = {}
        visited = [False] * len(self.graph.nodes())
        queue = deque()
        level = 0
        queue.append(root)
        visited[root] = True
        # 进行层次遍历
        while (len(queue) > 0 and level <= max_num_layers):
            count = len(queue) # 当前layer节点个数
            if self.opt1_reduce_len:
                degree_list = {}
            else:
                degree_list = []
            while (count > 0):
                top = queue.popleft()
                # 获取邻居个数(度)
                node = self.idx2node[top]
                degree = len(self.graph[node])
                if self.opt1_reduce_len:
                    # 统计当前层下的同样度的节点个数
                    degree_list[degree] = degree_list.get(degree, 0) + 1
                else:
                    degree_list.append(degree)
                # 将结论邻居放入队列当中
                for i in self.graph[node]:
                    if not visited[int(i)]:
                        visited[int(i)] = True
                        queue.append(int(i))
                count -= 1
            if self.opt1_reduce_len:
                # 按照degree进行排序
                order_degree_list = [(degree, freq) for degree, freq in degree_list.items()]
                order_degree_list.sort(key=lambda x: x[0])
            else:
                order_degree_list = sorted(degree_list)
            # level作为key保存order degree序列
            ordered_degree_sequence_dict[level] = order_degree_list
            level += 1
        return ordered_degree_sequence_dict

使用fastdtw计算compute_dtw_dist节点之间结构性的距离

def compute_dtw_dist(part_list, degree_list, dist_func):
    # part_list: {v: nbs}
    # degree_list =
    # v : {
    #   level_1: {
    #            degree_1, number of node, 
    #            degree_2, number of node
    #            }, 
    #   level_2: {....}}
    # return: 
    # dtw_dict = {
    #   (v1, v2): {
    #       layer1: dist1, 
    #       layer2: dist2,
    #       ...
    #   },
    #   (v3, v4): {
    #       layer1: dist1, 
    #       layer2: dist2,
    #       ...
    #   }, ...
    # }
    dtw_dict = {}
    # 获取v1的degree_list和v1邻居节点degree_list
    for v1, nbs in part_list:
        lists_v1 = degree_list[v1] # orderd degree list of v1
        for v2 in nbs:
            lists_v2 = degree_list[v2] # orderd degree list of v2
            max_layer = min(len(lists_v1), len(lists_v2))
            dtw_dict[v1, v2] = {}
            for layer in range(0, max_layer):
                # v1节点与邻居节点的距离(按照layer计算)
                dist, path = fastdtw(lists_v1[layer], lists_v2[layer], radius=1, dist=dist_func)
                dtw_dict[v1, v2][layer] = dist
    return dtw_dict

上面一大段又长又丑的代码肯定是很难看的,后面我会给github的地址让大家可以看看完整的代码.

2.2 multi layers construct

有了权值,那么就需要构建一个用于游走的多层图.刚刚我们的计算方式是用于后续计算层内的节点相似度,那么对于层外呢?层与层之间的节点的转移呢,我们之所以考虑构建多层图就是为了构建一个用于不同跳的结构相似度.所以在这一小节我们需要考虑层与层之间的转移权值.


同一节点在不同层的权值

gamma函数

下面解释一下公式当中每个符号的含义:

  1. gamma函数: 统计在第k层的u节点与其他节点之间的权值大于平均权值的节点个数
  2. w(u_k, u_{k + 1}): 是从k层u节点跳到去k+1层的u节点的权值,使用log函数是为了避免gamma函数值过大

好了到这里多层的layers结构就构建出来了,根据不同层构建gamma函数的映射

    def prepare_biased_walk(self,):
        # 论文当中3.2节构建graph上下文
        # 构建每一层内部的平均权值和每层每个节点的gamma函数映射,在后续随机游走的时候计算层与层之间的转移概率
        sum_weights = {} # sum layer edge weights
        sum_edges = {}    # number of edges
        average_weight = {} # each layer average weight of edges
        gamma = {}
        layer = 0
        while (os.path.exists(self.temp_path + 'norm_weights_distance-layer-' + str(layer) + '.pkl')):
            # 读取每层每个节点与邻居节点的权值
            probs = pd.read_pickle(self.temp_path + 'norm_weights_distance-layer-' + str(layer) + '.pkl')
            for v, list_weights in probs.items():
                sum_weights.setdefault(layer, 0)
                sum_edges.setdefault(layer, 0)
                sum_weights[layer] += sum(list_weights)
                sum_edges[layer] += len(list_weights)

            average_weight[layer] = sum_weights[layer] / sum_edges[layer]
            # 构建gamma函数: 计算当前层的节点v,在当前层的相邻节点的权值大于全局平均权值的节点个数
            gamma.setdefault(layer, {})
            for v, list_weights in probs.items():
                num_neighbours = 0
                # 筛选权值,统计大于该层的平均权值的权值节点个数(符合论文当中的优化)
                for w in list_weights:
                    if (w > average_weight[layer]):
                        num_neighbours += 1
                gamma[layer][v] = num_neighbours
            layer += 1
        pd.to_pickle(average_weight, self.temp_path + 'average_weight')
        pd.to_pickle(gamma, self.temp_path + 'gamma.pkl')

2.3 compute transition probability

这是对于同一层的不同节点的转移概率:


转移到其他节点的转移概率

这是对于同一节点转移到不同层转移概率:


转移到不同层转移概率

设置一个stay_prob来判断游走是否留在同一层,若保留同一层我们使用alias sample算法进行权值采样游走到其他节点,起始alias sample算法是为了加速采样速度,假如有n个节点,一个均匀采样算法的时间复杂度为O(n),但是alias sample的时间复杂度为O(1).我这里就不详细解释alias sample,有兴趣的朋友可以进入这个link看看时间复杂度O(1)的离散采样算法—— Alias method/别名采样方法.随机游走的代码如下:

    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): # 下一步在同一层当中游走
                v = chooseNeighbor(v, graphs, layers_alias, layers_accept, layer)
                path.append(self.idx2node[v])
            else: # 下一步前往上一层或者下一层
                r = random.random()
                try:
                    # x: 转移到H+1层的权值大小
                    x = math.log(gamma[layer][v] + math.e)
                    # p_moveup: 转移到H+1层的概率
                    p_moveup = (x / (x + 1))
                except:
                    print(layer, v)
                    raise ValueError()
                if (r > p_moveup and layer > initialLayer):
                    layer = layer - 1
                elif (r <= p_moveup and (layer + 1) in graphs and v in graphs[layer + 1]):
                    layer = layer + 1
        return path

这时候语料就组织好了.那么剩下的是训练了,训练的方法就是word2vec.算法介绍大概完成了.

3 Experiment and conclusion

这里我实验了论文当中的europe-airports和usa-airports两份数据,但实验起来发现并没有论文上面说得accuracy这么好,可能是论文没有公布它使用的分类器是什么.我只是简单使用LogisticRegression进行分类.

usa-airports二维分布图
brazil-airports二维分布图
dataset name micro macro acc
usa-airports 52.10% 51.86% 52.10%
brazil-airports 71.43% 73.57% 71.43%

文章目的是使用拓扑图当中的结构相似性,进行无监督学习.但假如task并不是偏向于structure,那么做这种无监督的学习是无用的.我这里使用另外一个数据集合wiki去证明这件事.

dataset name micro macro acc
wiki 18.26% 9.89% 18.26%
wiki二维分布图

显然效果差得不行,也证明了wiki的label数据并不是根据节点的结构性来进行划分.所以请各位使用每个模型的时候,请认真分析模型是否真的适合当前的场景,而不是看到某个task不行就说它毫无用地.模型研究其实也是一种特征工程和分析.

最后放出github地址:https://github.com/Salon-sai/learning-tensorflow/tree/master/lesson11

4. Reference

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,132评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,802评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,566评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,858评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,867评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,695评论 1 282
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,064评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,705评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,915评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,677评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,796评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,432评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,041评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,992评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,223评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,185评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,535评论 2 343

推荐阅读更多精彩内容

  • 转载自 https://mp.weixin.qq.com/s/OXXtPoBrCADbwxVyEbfbYg 25....
    _龙雀阅读 1,647评论 0 0
  • 前面的文章主要从理论的角度介绍了自然语言人机对话系统所可能涉及到的多个领域的经典模型和基础知识。这篇文章,甚至之后...
    我偏笑_NSNirvana阅读 13,862评论 2 64
  • 主要内容 自然语言输入编码 前馈网络 卷积网络 循环网络(recurrent networks ) 递归网络(re...
    JackHorse阅读 4,100评论 0 2
  • 官网 中文版本 好的网站 Content-type: text/htmlBASH Section: User ...
    不排版阅读 4,365评论 0 5
  • 机器学习术语表 本术语表中列出了一般的机器学习术语和 TensorFlow 专用术语的定义。 A A/B 测试 (...
    yalesaleng阅读 1,958评论 0 11