详解Graph-Embedding之DeepWalk、Line、Struct2vec

网易的几篇关于GAT和GCN的模型改造有点难,准备先拿其用过的Graph Embedding作为入门练手。

目录

前言

算法详解


前言

原文链接:Line -GraphEmbedding

本来想从DeepWalk写起的,感觉最近事情有点多,这个方法又有点简单,就不想写了,具体可以看以下几个链接:一言以蔽之,采用深度遍历的方法,进行随机游走,得到许多句子作为训练库,然后使用skip-gram学习表示向量,这个表示向量就是embedding。

DeepWalk:DeepWalk生动解释通俗理解代码讲解

下面步入正题,Line,同样一言以蔽之,使用广度遍历的方法,同时学习first-order和second-order两个相似度,用这两个相似度来做embedding。first-oredr相似度是指与节点相邻的节点与其相似(相似的含义是指embedding后的空间维度内这两个节点相近),second-order相似度是指与某节点具有相同neighbors的节点相似,如下图所示:6和7相似是因为直接相连及具有较大的weights(first),5和6相似是因为二者具有相同的邻居(second)。最后将这个两个相似度concat起来作为最后表示。

Line的适用范围:据作者所说是all in。但存在两个局限,第一不能很好处理新加入节点(尤其是这个新加入结点与其他结点没有关系的时候);第二,不能很好处理多阶相似度(没有考虑neighbor的neighbors)

先验知识:在进入正题之前,有几个先验知识必不可少,不然会一知半解,KL散度(用于衡量两个分布之间的相似程度),Alias Method(按照给定的概率分布进行采样,原文中防止梯度爆炸),负采样(随机选取一些负样本而非全部的负样本优化计算)

算法详解

优化目标一:使得embedding后的一阶相似度与真实共现的概率分布相同,即下面两个公式的KL散度(公式3)尽量的小:

# KL散度计算公式
def line_loss(y_true, y_pred):
    return -K.mean(K.log(K.sigmoid(y_true*y_pred)))

优化目标二:使得embedding后的二阶相似度与真实条件概率分布相同(即在给定i下出现j的概率),即下面两个公式的KL散度尽量的小(这个λ代表节点的重要程度,可以通过PageRank事先得到),为了统一形式,化为第4个公式:

创新点1:由于在优化目标2中需要计算条件概率,会导致算法复杂程度显著增加,就使用了负采样(简单来说我们仅采集几个中心词对应的负样本对目标函数进行训练)的方法,加快模型训练速度。

def get_negatives(all_contexts, sampling_weights, K):
    all_negatives, neg_candidates, i = [], [], 0
    population = list(range(len(sampling_weights)))
    for contexts in all_contexts:
        negatives = []
        while len(negatives) < len(contexts) * K:
            if i == len(neg_candidates):
                # 根据每个词的权重(sampling_weights)随机生成k个词的索引作为噪声词。
                # 为了高效计算,可以将k设得稍大一点
                i, neg_candidates = 0, random.choices(
                    population, sampling_weights, k=int(1e5))
            neg, i = neg_candidates[i], i + 1
            # 噪声词不能是背景词
            if neg not in set(contexts):
                negatives.append(neg)
        all_negatives.append(negatives)
    return all_negatives

sampling_weights = [counter[w]**0.75 for w in idx_to_token]
all_negatives = get_negatives(all_contexts, sampling_weights, 5)

创新点2:无论是优化目标1还是优化目标2,都有一个w直接乘在目标函数上,这会导致梯度爆炸现象(假如这个weights很大),作者一开始想将所有的带权边变成N*1(比如w为5,我就变成5个w为1的边),但这样存储消耗太大,作者就使用了Edge Sampling,只Sample出一部分带权边进行梯度下降(当然需要保证Sample的概率分布与原分布近似,也就是Alias Method。)

def _gen_sampling_table(self):
# 顶点采样和负采样的采样表制作
    # create sampling table for vertex
    power = 0.75
    numNodes = self.node_size
    node_degree = np.zeros(numNodes)  # out degree
    node2idx = self.node2idx

    for edge in self.graph.edges():
        node_degree[node2idx[edge[0]]
                    ] += self.graph[edge[0]][edge[1]].get('weight', 1.0)

    total_sum = sum([math.pow(node_degree[i], power)
                        for i in range(numNodes)])
    norm_prob = [float(math.pow(node_degree[j], power)) /
                    total_sum for j in range(numNodes)]
    # 主要就是为了计算某个节点出现的概率
    self.node_accept, self.node_alias = create_alias_table(norm_prob)

    # create sampling table for edge
    numEdges = self.graph.number_of_edges()
    total_sum = sum([self.graph[edge[0]][edge[1]].get('weight', 1.0)
                        for edge in self.graph.edges()])
    norm_prob = [self.graph[edge[0]][edge[1]].get('weight', 1.0) *
                    numEdges / total_sum for edge in self.graph.edges()]

    self.edge_accept, self.edge_alias = create_alias_table(norm_prob)
import numpy as np

def create_alias_table(area_ratio):
    """
    # 其实这里就是维护两个队列,使得采样符合原概率分布
    :param area_ratio: sum(area_ratio)=1
    :return: accept,alias
    """
    l = len(area_ratio)
    accept, alias = [0] * l, [0] * l
    small, large = [], []
    area_ratio_ = np.array(area_ratio) * l
    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]

本来还想再看看Struct2vec的,读完论文发现超级难,看了别人的博客弄懂了个大概,主要是通过节点的度数识别其neighbours,作者认为degree越相似的节点结构相似度越高,同时,作者认为如果其neighbors的度数还是很相似的话,其结构相似度应该更高。在随机游走那块本质和Deepwalk一样,也是构造出很多的序列,然后使用skip-gram进行训练,得到节点的embedding。

具体分为四步:①计算节点之间的结构相似度;②生成多层次的带权网络(一开始我也不是很理解,后来顿悟,第一层节点之间的权重是1-hop neibours序列之间的相似度,第二层节点之间的权重是2-hop neighbours序列之间的相似度,每一层都是完全图,但是权重不同);③在多层次的带权网络中随机游走,既可以再同层游走,也可以去下一层(这个可以自己学习得到);④根据得到的sequence使用word2vec学习node的向量表示;

具体学习资料如下所示:代码实现;生动诠释

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Data_Designer

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值