深度学习 - 34.GraphEmbedding Line 图文详解

本文介绍了LINE算法,它通过一阶和二阶相似度扩展了word2vec的embedding空间,尤其关注网络中无向图和有向图的顶点邻域关系。LINE通过一阶近似处理无向图,二阶近似用于捕捉邻域相似性,包括负采样、ASGD和AliasSample技巧。实践部分展示了如何生成带权图、构建模型和优化,以及可视化二阶相似度实例。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

一.引言

前面介绍了 DeepWalk,Node2vec,通过不同游走方法获取游走序列,然后通过 word2vec 进行 embedding 训练, 在 word2vec 训练中,所有向量共用了同一个 embedding 空间即同一个 Variable,Line 提出了一阶和二阶相似度的概念,从而丰富了 embedding 空间。

二.Line 算法

1.Line 简介

如上是一个信息网络的例子,在上例中 6 和 7 存在直连关系,可以判定为有较强联系,按照传统方法, 5 和 6 没有相连一定意义上联系不大,但是可以看到 5,6 有相似的邻居 1,2,3,4,所以根据 LookAlike 的思想, 5 和 6 有很大的相似性或者相关性,只不过没有体现在边的直连上。

A.一阶相似度

一阶相似度描述了网络中两个临近顶点之间的相关性,对于边 (u,v),该边上的权重为 W_{uv} 表示其一阶相似度,如果没有边直连则一阶相似度为0。如上例所示,直连边只体现出了两个顶点之间的一部分信息量,如果只考虑一阶相似度,则无法捕捉到两个顶点拥有很多相似邻居的信息,所以需要通过二阶相似度补充丢失的信息量,有点类似于 LR 模型和 FM 模型 延伸到 DNN,DeepFM 一样。

B.二阶相似度

网络中一对顶点(u,v)之间的二阶相似度描述顶点的邻域的相似度。令 P_u=(w_{u,1},...,w_{u,m})表示顶点 u 与其他顶点间的一阶相似度,则 u 与 v 的二阶相似度可以通过比较 P_u 与 P_v 的相似度,若 u,v 没有公共邻居顶点,则 u,v 的二阶相似度为0。

2.Line 算法

A.一阶相似度

一阶相似度描述网络间相似顶点的相似性,对于每个无向边 (i,j),定义 Vi 与 Vj 之间的联合概率:

\overrightarrow{u}_i 为顶点的低维向量表示,在深度模型中可以理解为一个 Variable 中初始化的向量,同时定义经验分布

一阶相似度的优化目标即为:

衡量两个分布的近似度考虑K-L散度,通过对数的极大似然方法将优化目标转换为: 

论文中提到一阶近似值只适用于无向图,不用于有向图,通过最小化上述函数得到的向量 embedding 可以标识 d-dimensional 空间中的每个顶点。 

B.二阶相似度

二阶相似度同时适用于有向图和无向图,二阶近似假设共享多个相同邻域顶点的两个顶点相似,该情况下需要对向量引入两个向量表征,一个是顶点本身的 embedding,另一个是作为其他顶点上下文即邻域的表征,所以问前提到,对于一个顶点,有两个向量空间表征,也是其和 word2vec 不同的地方。对于任意边 (i,j),定义条件概率 vi 存在情况下 vj 存在的概率,这里 \overrightarrow{u_i} 代表其本身的向量,\overrightarrow{​{u_i}'} 代表其作为上下文的表征, |V| 代表上下文顶点的数量:

同样的有经验分布,wij为带权边的权重,di为顶点vi的出度,非带权图即等权图出度为顶点邻点个数,带权图时为权重求和,其中 N(I) 为 vi 的外邻域集:

                                             \widehat{p_2}=\frac{w_ij}{d_i} \ ,where \ d_i=\sum_{k\in N(I)} W_{ik} 

二阶优化目标为:

通过K-L散度计算分布距离,并设置 λi = di 并省略常数项得到:

通过学习自身向量 \overrightarrow{u_i} 和上下文向量  {\overrightarrow{u_i}}' 最小化目标函数可以得到每个顶点的向量表征。

3.Line 技巧

A.负采样

计算 O2 函数时分母需要遍历所有 |V| 个顶点,非常低效,Line 通过引入负采样提高迭代效率,对于每条边 (i,j),修改后的目标函数为:

\sigma (x) 为 sigmoid 函数,前面函数优化正样本,后面的部分优化负样本,其中 K 为负样本个数。

B.ASGD (Mini-Batch) 

在每个步骤中ASGD算法对一小批边进行采样,然后更新模型参数,mini-batch 可以提高训练效率。注意这里 w_{ij} 与 edge 相乘,当 w_{ij} 具有较大方差时,模型的更新会出现偏好,可能侧向于学习次数多且权重大的样本。

C.Alias Sample

通过 Alias Sample 将采样的时间复杂度优化至 O(1),关于 AliasSample 可以参考: AliasSample 图文详解https://blog.csdn.net/BIT_666/article/details/119865106https://blog.csdn.net/BIT_666/article/details/119865106

D.Discuss

关于低度顶点的精准嵌入:

基于二阶相似度的方法严重依赖"上下文"节点的数量,对于节点邻域非常少,难以学习或精确推断时,可以考虑扩充更高的顶点为该类样本作为邻居,例如 dx=2,dx=3 等等,dx代表两点之间距离,,其中顶点i与其二阶邻居j的权重值可以表示为:

E.优化目标:

对于一个新的顶点 i,如果他与现有顶点的连接是已知的,则可以获取经验分布 P1(·,vi) 和 P2(·,vi),为了获得新顶点的嵌入,根据上述目标函数方程,一种直接的方法是最小化下面任一目标函数:

三.Line 实践

1.生成带权图

这里生成1000条随机权重的边,通过 networkx 生成并读取,也可以调用 nx.draw(G) 查看随机生成的图。

# 1.生成带权图
file_name = ".../data/test_edges.txt"
G = createAndLoadGraph(file_name)

Tip: 辅助函数

A.随机生成 SampleNum 个样本并加载为 Graph

def createAndLoadGraph(file_name):
    f = open(file_name, "w")
    sample_num = 1000
    for i in range(sample_num):
        a = random.randint(0, 100)
        b = random.randint(0, 100)
        weight = random.random()
        if a != b:
            f.write("%s %s %s\n" % (a, b, weight))
    f.close()
    G = nx.read_edgelist(file_name, create_using=nx.DiGraph(), nodetype=None, data=[('weight', float)])
    return G

2.构建AliasTable

这里 sample_per_epoch 为一次迭代的全部样本数,每条边作为一个正样本,负采样生成 K 个负样本,所以一个 epoch 的样本为 edge_size x (1 + K)

    # 2.获取映射关系与AliasTable
    # node-id的映射 node2idx = {} 通过node获取对应id
    # 构建id-node的数组 idx2node = [] 通过id获取对应node
    idx2node, node2idx = preprocess_nxgraph(G)

    negative_ratio = 15  # 负采样比例

    # 点与边的数量以及每个epoch的样本
    node_size = G.number_of_nodes()
    edge_size = G.number_of_edges()
    # 1个正样本 negative个负样本
    samples_per_epoch = edge_size * (1 + negative_ratio)

    # 生成 Alias Table
    node_accept, node_alias, edge_accept, edge_alias = gen_sampling_table(G, node2idx)

A.生成映射关系

通过 graph 获取 node-id,id-node 的映射关系

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

B.生成 Node 和 Edge 采样表

参考之前讲到的 Node2vechttps://blog.csdn.net/BIT_666/article/details/119890792?spm=1001.2014.3001.5501https://blog.csdn.net/BIT_666/article/details/119890792?spm=1001.2014.3001.5501这里需要对 Node 和 Edge 各生成一个 AliasTable 来应对起点是 Node 还是 Edge 的情况,

def gen_sampling_table(graph, node2idx):
    # create sampling table for vertex
    power = 0.75
    numNodes = node_size
    node_degree = np.zeros(numNodes)  # out degree
    node2idx = node2idx

    for edge in graph.edges():
        # node2idx[edge[0]] 获取边的起始节点 并累加对应起始节点的 degree 度
        node_degree[node2idx[edge[0]]] += graph[edge[0]][edge[1]].get('weight', 1.0)

    # 对全部节点的 degree 做归一化
    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)]

    # 构建 Node 的 Alias Table
    _node_accept, _node_alias = create_alias_table(norm_prob)

    # 构建 Edge 的 Alias Table
    numEdges = graph.number_of_edges()
    total_sum = sum([graph[edge[0]][edge[1]].get('weight', 1.0)
                     for edge in graph.edges()])
    norm_prob = [graph[edge[0]][edge[1]].get('weight', 1.0) *
                 numEdges / total_sum for edge in graph.edges()]

    _edge_accept, _edge_alias = create_alias_table(norm_prob)
    return _node_accept, _node_alias, _edge_accept, _edge_alias

C.构建AliasTable

将转移概率归一化后送给 create_alias_table 生成 accept 和 alias ,具体的流程可参考:

Alias 采样图文详解https://blog.csdn.net/BIT_666/article/details/119865106?spm=1001.2014.3001.5501https://blog.csdn.net/BIT_666/article/details/119865106?spm=1001.2014.3001.5501

def create_alias_table(transform_prob):
    # 当前node的邻居的nbr的转移概率之和
    norm_const = sum(transform_prob)
    # 当前node的邻居的nbr的归一化转移概率
    normalized_prob = [float(u_prob) / norm_const for u_prob in transform_prob]
    length = len(transform_prob)
    # 建表复杂度o(N)
    accept, alias = [0] * length, [0] * length
    # small,big 存放比1小和比1大的索引
    small, big = [], []
    # 归一化转移概率 * 转移概率数
    transform_N = np.array(normalized_prob) * length
    # 根据概率放入small large
    for i, prob in enumerate(transform_N):
        if prob < 1.0:
            small.append(i)
        else:
            big.append(i)

    while small and big:
        small_idx, large_idx = small.pop(), big.pop()
        accept[small_idx] = transform_N[small_idx]
        alias[small_idx] = large_idx
        transform_N[large_idx] = transform_N[large_idx] - (1 - transform_N[small_idx])
        if np.float32(transform_N[large_idx]) < 1.:
            small.append(large_idx)
        else:
            big.append(large_idx)

    while big:
        large_idx = big.pop()
        accept[large_idx] = 1
    while small:
        small_idx = small.pop()
        accept[small_idx] = 1
    return accept, alias

3.初始化模型

    # 3.构建模型
    embedding_size = 8  # embedding size
    order = "all"  # 选择组合方式 first second all
    embeddings = {}  # 全部 embedding
    model, embedding_dict = create_model(node_size, embedding_size, order)
    opt = "adam"
    model.compile(opt, line_loss)

A.构建模型

模型输入就是一个边对应的两个 Node,输出是二者 embedding 内积的结果,embedding 通过 Embedding 层 lookup 得到

First:

i,j 共享一个向量空间 first_emb,描述二者一阶相似度

Second:

这里初始化的意思是每个向量都有两个属性,自身向量 v_i 和环境向量 c_i,v_i 来自 second——emb,c_i 来自 context_emb 标识当前 Node 的上下文向量, 如果向量 i 的自身向量 v_i 和向量 j 的上下文向量 c_j 契合度很高,可以理解为 j 的邻居和 i 也有很多边连接,这就是二阶相似度提到判断无直连边但相似节点的判断方法,根据 order 的不同,模型可以支持多输出。不过这里 context_emb 我觉得用当前节点的邻居的 embedding 池化得到也是一种实现方案,这样可以更好地获取邻居的信息,有兴趣可以实现以下。

def create_model(numNodes, embedding_size, order='second'):
    # 两个端点
    v_i = Input(shape=(1,))
    v_j = Input(shape=(1,))

    first_emb = Embedding(numNodes, embedding_size, name='first_emb')
    second_emb = Embedding(numNodes, embedding_size, name='second_emb')
    context_emb = Embedding(numNodes, embedding_size, name='context_emb')

    # i,j 都拿一阶 embedding
    v_i_emb = first_emb(v_i)
    v_j_emb = first_emb(v_j)

    # i 拿二阶 embedding j 拿 context embedding
    v_i_emb_second = second_emb(v_i)
    v_j_context_emb = context_emb(v_j)

    # 考虑两个点的一阶和二阶
    first = Lambda(lambda x: tf.compat.v1.reduce_sum(
        x[0] * x[1], axis=-1, keep_dims=False), name='first_order')([v_i_emb, v_j_emb])
    second = Lambda(lambda x: tf.compat.v1.reduce_sum(
        x[0] * x[1], axis=-1, keep_dims=False), name='second_order')([v_i_emb_second, v_j_context_emb])

    # 考虑不同的参数带来的不同模型
    if order == 'first':
        output_list = [first]
    elif order == 'second':
        output_list = [second]
    else:
        output_list = [first, second]

    model = Model(inputs=[v_i, v_j], outputs=output_list)
    print("ModelFormat: %s" % order)
    model.summary()

    return model, {'first': first_emb, 'second': second_emb}

B.损失函数

最后提高可以优化如下两个目标函数:

P1(i,j) 为 sigmoid 的形式,前面加上 log ,即为 line_loss 样式:

P2(i,j) 优化后也具有 line_loss 的样式,这里 \sigma 为 sigmoid 函数:

def line_loss(y_true, y_pred):
    return -K.mean(K.log(K.sigmoid(y_true * y_pred)))

4.采样样本

构建样本迭代器供模型训练使用

    # 4.构建样本
    batch_it = batch_iter(G, node2idx, batch_size, order)

下面通过 mod 和 batch_size 控制生成每个 batch 的样本,h,t 代表一个 edge 的两个 Node,如果 mod 为0,则生成一个正样本,随后 mod += 1,随机 Alias 生成negative_ratio 个负样本,该逻辑直到一个 batch 的样本生成在重新置 mod 为0,重新开始。需要注意如果使用多输出模型,则对应的标记 sigh 也需要多份,这里正例为 +1,负例为 -1。

def batch_iter(graph, node2idx, batch_size, order: str):
    # 获取全部边 (Node1, Node2)构成
    edges = [(node2idx[x[0]], node2idx[x[1]]) for x in graph.edges()]

    # 获取全部边的数量
    data_size = graph.number_of_edges()
    shuffle_indices = np.random.permutation(np.arange(data_size))

    # positive or negative mod
    mod = 0
    mod_size = 1 + negative_ratio
    h = []
    # 一个 batch 取 batch_size 个数据
    count = 0
    start_index = 0
    end_index = min(start_index + batch_size, data_size)
    while True:
        if mod == 0:
            h = []
            t = []
            for i in range(start_index, end_index):
                # < prob 保留 > prob 选取 alias
                if random.random() >= edge_accept[shuffle_indices[i]]:
                    shuffle_indices[i] = edge_alias[shuffle_indices[i]]
                # 边的起始点 h->t
                cur_h = edges[shuffle_indices[i]][0]
                cur_t = edges[shuffle_indices[i]][1]
                h.append(cur_h)
                t.append(cur_t)
            # label ±1
            sign = np.ones(len(h))
        else:
            sign = np.ones(len(h)) * -1
            t = []
            for i in range(len(h)):
                t.append(alias_sample(node_accept, node_alias))

        # all 情况下需要两个 label 同时训练
        if order == 'all':
            yield [np.array(h), np.array(t)], [sign, sign]
        else:
            yield [np.array(h), np.array(t)], [sign]
        mod += 1
        mod %= mod_size

        # 结束一个循环
        if mod == 0:
            start_index = end_index
            end_index = min(start_index + batch_size, data_size)

        if start_index >= data_size:
            count += 1
            mod = 0
            h = []
            shuffle_indices = np.random.permutation(np.arange(data_size))
            start_index = 0
            end_index = min(start_index + batch_size, data_size)

5.训练模型

提供一些基本参数和迭代器即可开始训练

    # 5.训练模型
    epochs = 10
    verbose = 1
    batch_size = 1024
    times = 5
    steps_per_epoch = ((samples_per_epoch - 1) // batch_size + 1) * times

    hist = model.fit_generator(batch_it, epochs=epochs,
                               steps_per_epoch=steps_per_epoch,
                               verbose=verbose)

6.向量可视化

使用 TSNE 将得到的 Embedding 压缩至两维

    embs = get_embeddings()
    embedding_list = [embs[k] for k in embs]

    model = TSNE(n_components=2)
    compress_embedding = model.fit_transform(embedding_list)
    keys = list(embs.keys())

    plt.scatter(compress_embedding[:, 0], compress_embedding[:, 1], s=10)
    for x, y, key in zip(compress_embedding[:, 0], compress_embedding[:, 1], keys):
        plt.text(x, y, key, ha='left', rotation=0, c='black', fontsize=8)
    plt.title("T-SNE")
    plt.colorbar()
    plt.show()

First 和 Second 直接获取 Embedding,All 会用 hstack 将一阶二阶 Embedding 拼接在一起

def get_embeddings():
    _embeddings = {}
    if order == 'first':
        embeddings = embedding_dict['first'].get_weights()[0]
    elif order == 'second':
        embeddings = embedding_dict['second'].get_weights()[0]
    else:
        embeddings = np.hstack((embedding_dict['first'].get_weights()[
                                    0], embedding_dict['second'].get_weights()[0]))
    for i, embedding in enumerate(embeddings):
        _embeddings[idx2node[i]] = embedding

    return _embeddings

  

7.查看二阶相似度

首先添加全部节点的邻居,然后遍历寻找 node_i,node_j 不在各自邻居且拥有超过 N 个邻居,即可得到对应节点,随后通过模型 get_embeddings 方法得到的 embedding 即可获取该类节点的内积。

def getCommonNode(graph):
    node_neighbor_dict = {}
    # 添加全部节点的邻居
    for node in graph:
        neighbors = graph.neighbors(node)
        node_neighbor_dict[node] = set([node for node in neighbors])

    # 暴力循环交集
    common_node = {}
    for node_i in graph:
        for node_j in graph:
            if node_i != node_j and node_j not in node_neighbor_dict[node_i] and node_i not in node_neighbor_dict[node_j]:
                common_num = len(node_neighbor_dict[node_i] & node_neighbor_dict[node_j])
                if common_num >= 5:
                    common_node[node_i + "_" + node_j] = common_num

    return common_node
{'14_31': 6, '20_5': 5, '15_69': 5, '69_15': 5, '83_31': 5, '5_20': 5,'4_54': 5,
 '89_21': 5, '54_4': 5, '21_89': 5, '31_14': 6, '31_83': 5, '31_92': 5, '92_31': 5}

上述节点的公共邻居数目 >= 5,查看对应 dot:

    print(getCommonNode(G))
    emb1 = embs['14']
    emb2 = embs['31']
    print(np.dot(emb1, emb2))

四.总结

LINE 理论+实践大概就写这么多,结合前面提到的几种方法,可以看到 GraphEmbedding 在采样,序列生成以及模型结构上的变化,后续有机会了解更多算法类似 EGES,SDNE 等等。关于 LINE 更多的推理与实践可以参考原论文。

更多推荐算法相关深度学习:深度学习导读专栏 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

BIT_666

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

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

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

打赏作者

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

抵扣说明:

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

余额充值