基于Node2Vec的图嵌入实现过程

一、引言

  随着互联网的发展,社交网络、推荐系统以及知识图谱等应用中的图结构数据越来越多,如何从这些复杂的关系网中提取出有意义的信息,成为了机器学习中的一大挑战。图结构数据的关键在于节点(如用户、物品等)之间的关联关系,而要从图中有效提取节点的特征,我们需要一种能保留图的结构信息的嵌入方法。

  传统的机器学习方法难以直接处理图数据,因为图的结构不如图像和文本那样规则,图中的节点没有固定的顺序,也没有明确的拓扑形式。因此,近年来图嵌入(Graph Embedding)技术得到了广泛关注。通过图嵌入方法,我们可以将图中的节点映射为低维向量,使其能够被应用到分类、聚类、推荐等任务中。

  Node2Vec 是一种基于随机游走(Random Walk)的节点嵌入方法。它通过对图中的节点进行采样,并利用这些采样结果训练出能够表示节点的嵌入向量。这些向量不仅能保留节点的局部邻域信息,还能反映节点在整个图中的全局位置。本文的目的在于详细介绍如何通过代码实践使用Node2Vec进行图嵌入、对嵌入结果进行可视化,并最终将嵌入向量存储为JSON文件。

二、Node2Vec(原理)

2.1 随机游走(Random Walk)

  Node2Vec 基于图上的随机游走来捕捉节点的结构特性。随机游走是一种类似于在图上进行漫步的过程,即从一个节点出发,每次根据一定的概率选择下一个要访问的节点。通过多次游走,Node2Vec可以得到一系列的节点序列,这些序列可以看作是图中的“路径”,用于训练节点的嵌入表示。

  Node2Vec 提供了两个关键参数 p p p q q q,用来控制随机游走的方式:

  • 参数 p p p:回归系数,用于控制游走时返回前一个节点的概率。 p p p 越大,模型越倾向于向更远的节点扩展。

  • 参数 q q q:探索系数,用于控制模型深度优先搜索(DFS)和广度优先搜索(BFS)之间的平衡。 q q q 越大,游走更倾向于深度探索,即更多地访问图的深层节点; q q q 较小时,游走更倾向于广度探索,即优先访问节点的直接邻居。

  通过调节这两个参数,Node2Vec 能够适应不同的任务需求。例如,在社交网络中,我们可能希望重点关注与用户相邻的其他用户,此时较小的 q q q值能帮助我们更好地捕捉局部社区结构。而在知识图谱中,我们可能希望深入挖掘某个概念与远端概念的关系,此时较大的 q q q 值可以帮助捕获图的全局结构。

在这里插入图片描述

2.2 嵌入学习

  一旦通过随机游走得到节点序列,Node2Vec 将这些序列视为词语序列,采用类似 Word2Vec 的 Skip-Gram 模型进行嵌入学习。Skip-Gram 模型的目标是给定一个节点,预测其上下文节点(即在随机游走序列中与该节点相邻的节点)。

  在这个过程中,Node2Vec 学习到每个节点的嵌入向量。这些嵌入向量能够保留节点之间的相似性,邻近的节点将拥有相近的嵌入表示。最终,通过这种方式,图中的每个节点都被映射到一个固定维度的向量空间中,嵌入向量可以直接用于下游任务,如节点分类、边预测和社区检测。

2.3 Node2Vec 的优势

  Node2Vec 的最大优势在于它可以同时捕捉局部和全局的图结构信息。相比于传统的图算法(如PageRank)只考虑节点的全局属性,或者某些仅仅考虑局部连接的嵌入方法,Node2Vec 能够通过灵活的随机游走策略,在不同的场景下取得更好的表现。

  此外,Node2Vec 的随机游走策略可以高效处理大型图,尤其是对于节点和边非常多的复杂网络,Node2Vec 仍然能通过较少的计算时间获得高质量的嵌入向量。

三、使用 Node2Vec 进行图嵌入(实践)

3.1 读取和转换 JSON 文件为 Graph 对象

  在正式训练模型之前,首先我们需要将原始的数据加载为图结构。在很多实际应用场景中,图结构的输入数据并不是直接以图的形式给出,而是以诸如JSON等格式的文件存储了节点之间的关系。因此,第一步的任务是将JSON文件中的节点和边转化为可以用于训练的 Graph 对象。(本文使用的json文件可以从百度网盘下载)

import json
import networkx as nx

# 读取并转换JSON文件为Graph对象的函数
def read_and_convert_json_to_graph(json_file):
    with open(json_file, 'r') as file:
        json_list = json.load(file)

    G = nx.Graph()  # 创建一个空的无向图
    for item in json_list:
        G.add_edge(item['object1'], item['object2'])  # 添加边

    return G

  在上面的代码中,read_and_convert_json_to_graph 函数负责读取JSON文件,并将其中的节点(object1 和 object2)之间的关系转换为图的边(edge)。具体过程如下:

  • 通过 json.load() 将JSON文件中的内容加载为Python的列表格式。

  • 遍历该列表中的每个元素,并使用 networkx 库将两个节点之间的关系加入到无向图 G 中。

  最终,我们会得到一个完整的 Graph 对象,它包含了所有节点及它们之间的连接。这个 Graph 对象将作为Node2Vec 模型的输入,进一步进行节点嵌入训练。

3.2 训练 Node2Vec 模型

  接下来,利用构建好的图对象,我们将使用Node2Vec库进行节点嵌入的训练。Node2Vec 的核心思想是通过随机游走生成节点序列,并通过这些序列学习节点的嵌入表示。

from node2vec import Node2Vec

# Node2Vec模型训练函数
def train_node2vec(graph, dimensions=64, walk_length=30, num_walks=200, p=1, q=1, workers=4):
    node2vec = Node2Vec(graph, dimensions=dimensions, walk_length=walk_length, num_walks=num_walks, p=p, q=q, workers=workers)
    model = node2vec.fit(window=10, min_count=1, batch_words=4)
    return model

  在这个 train_node2vec 函数中,我们定义了几个关键参数来控制Node2Vec的训练过程:

  • dimensions:嵌入向量的维度,即我们希望每个节点被映射到多少维度的向量空间。通常,维度越高,模型能表达的信息越丰富,但训练时间也会增加。

  • walk_length:随机游走的长度,决定每次游走过程中访问的节点数目。

  • num_walks:每个节点的游走次数,即我们为每个节点采样的路径数量。

  • p 和 q:前面介绍的控制游走策略的两个参数,分别控制返回前一节点的概率和探索新节点的概率。

  • workers:并行计算的线程数,用于加速训练过程。

  训练完成后,fit() 函数返回的是一个包含了所有节点嵌入向量的模型对象。我们可以通过该模型获取每个节点的嵌入向量,并将这些向量用于后续的任务。

3.3 二维嵌入可视化

  在训练完Node2Vec模型并得到节点的高维嵌入向量后,直接查看这些高维向量并不直观,因此我们通常需要将其降维,以便通过图形展示这些节点在嵌入空间中的分布情况。

  二维可视化是最常用的手段之一。我们可以使用降维算法(如主成分分析 PCA)将嵌入向量从高维空间降至二维,从而通过平面图直观展示节点之间的相对位置。接下来是实现代码:

import numpy as np
import matplotlib.pyplot as plt
from sklearn.decomposition import PCA

# 二维可视化嵌入的函数
def visualize_embeddings(model, graph, save_path='embeddings_visualization.png'):
    # 获取所有节点的嵌入向量
    embeddings = np.array([model.wv[str(node)] for node in graph.nodes()])
    
    # 使用PCA将嵌入向量降维到2D
    pca = PCA(n_components=2)
    embeddings_2d = pca.fit_transform(embeddings)

    # 绘制图形
    plt.figure(figsize=(20, 16))  # 设置图形大小
    pos = {node: embeddings_2d[i] for i, node in enumerate(graph.nodes())}

    # 绘制节点,设置节点大小和颜色
    nx.draw_networkx_nodes(graph, pos=pos, node_size=30, node_color='blue')

    # 绘制图中的边,透明度alpha设置较低以减少边线干扰
    nx.draw_networkx_edges(graph, pos=pos, alpha=0.1)

    # 隐藏坐标轴
    plt.axis('off')

    # 保存结果并展示图像
    plt.savefig(save_path)
    plt.show()
  • 获取节点嵌入向量:从训练好的 Node2Vec 模型中提取节点的嵌入向量,这些嵌入向量被保存在 model.wv 中,其中每个节点的 ID 被转化为字符串格式。

  • PCA降维:使用 PCA 将高维嵌入向量降至二维。PCA 是一种常见的降维技术,它能够提取数据中最重要的两个主成分,使得降维后的数据尽可能保留原始高维数据中的信息。

  • 节点和边的可视化:通过 networkx 提供的 draw_networkx_nodes 和 draw_networkx_edges 方法来绘制图形中的节点和边。节点颜色为蓝色,边的透明度较低,以减少边线的干扰,使节点的位置关系更加清晰可见。

  • 图形保存与显示:绘制好的图形保存为 PNG 文件,并显示出来以供查看。plt.savefig() 可以保存结果,plt.show() 用于在执行程序时显示图像。

  通过这种方式,节点会根据其嵌入向量的相似性在二维平面上分布。距离较近的节点往往在嵌入空间中更为相似,说明它们在图结构中有更密切的联系。反之,距离较远的节点可能属于不同的社区或子图。

3.4 三维嵌入可视化

  虽然二维可视化已经能帮助我们理解节点嵌入,但在某些场景中,我们可能希望更丰富的维度来展示节点的分布。此时,三维可视化可以提供更直观的展示效果。通过降维到三维,我们能够通过三维图形进一步观察节点的相互关系。

from mpl_toolkits.mplot3d import Axes3D
from sklearn.decomposition import PCA

# 三维可视化嵌入的函数
def visualize_embeddings_3d(model, graph, save_path='embeddings_visualization_3d.png'):
    # 获取所有节点的嵌入向量
    embeddings = np.array([model.wv[str(node)] for node in graph.nodes()])
    
    # 使用PCA将嵌入向量降维到3D
    pca = PCA(n_components=3)
    embeddings_3d = pca.fit_transform(embeddings)

    # 创建3D绘图
    fig = plt.figure(figsize=(20, 16))
    ax = fig.add_subplot(111, projection='3d')

    # 获取嵌入向量的三个维度(x, y, z坐标)
    xs = embeddings_3d[:, 0]
    ys = embeddings_3d[:, 1]
    zs = embeddings_3d[:, 2]

    # 绘制节点
    ax.scatter(xs, ys, zs, c='blue', marker='o')

    # 设置轴标签
    ax.set_xlabel('PCA1')
    ax.set_ylabel('PCA2')
    ax.set_zlabel('PCA3')

    # 添加标题
    plt.title('3D visualization of Node Embeddings')

    # 保存并展示图像
    plt.savefig(save_path)
    plt.show()
  • PCA降维到三维:与二维可视化类似,这里我们将嵌入向量从高维空间降至三维。这三个维度能够提供更多的嵌入信息,从而帮助我们更好地观察节点的分布情况。

  • 三维绘图:通过 mpl_toolkits.mplot3d 的 Axes3D 类进行三维图形的绘制。我们提取PCA降维后的三个维度(x、y、z坐标),并将其用于三维散点图的绘制。

  • 散点绘制:使用 scatter() 函数绘制节点,每个节点显示为一个三维点。我们可以通过颜色、大小、标记等属性来调整节点的视觉效果。在本例中,节点颜色为蓝色,标记为圆形。

  • 图形轴标签与标题:为三维图形的各个坐标轴(PCA1、PCA2、PCA3)添加标签,以便清晰显示每个维度代表的含义。此外,还为图形添加了标题,方便理解可视化内容。

  三维图形相比于二维图形能够展示更多的信息。特别是在处理嵌入维度较高的情况下,三维可视化能够帮助我们更好地理解节点在嵌入空间中的位置关系。通过旋转三维图形,我们可以从不同角度观察节点的分布情况,发现潜在的社区结构或其他有趣的模式。

3.5 保存嵌入和关系到 JSON 文件

  在获得了节点嵌入向量后,我们往往需要将这些向量保存下来,方便后续的分析与应用。在实际项目中,我们可能需要将嵌入向量与原始的关系数据一起存储,以便后续模型或任务能够继续利用这些嵌入。

  下面的代码展示了如何将节点的嵌入向量与原始的JSON文件结合,并将结果保存为一个新的JSON文件。

def save_embeddings_to_json(graph, model, json_file_path, output_file):
    # 读取原始的JSON文件来获取对象间的关系
    with open(json_file_path, 'r') as file:
        json_list = json.load(file)

    output_data = []

    for item in json_list:
        object1, object2 = item['object1'], item['object2']

        # 对于每对对象,获取其嵌入向量
        properties1 = model.wv[object1].tolist() if object1 in model.wv else None
        properties2 = model.wv[object2].tolist() if object2 in model.wv else None

        # 构建新的字典项,包含嵌入向量和原始关系
        modified_item = {
            "object1": object1,
            "properties1": properties1,
            "object2": object2,
            "properties2": properties2,
            "relationship": item.get('relationship')  # 假设原始JSON中包含关系字段
        }
        output_data.append(modified_item)

    # 将修改后的数据写入新的JSON文件
    with open(output_file, 'w') as f:
        json.dump(output_data, f, indent=4)
  • 原始关系读取:首先,我们读取原始的JSON文件,获取其中包含的节点及其关系。在本例中,JSON文件中的每一条记录包含两个对象(object1 和 object2),代表它们之间的某种关系。

  • 嵌入向量提取:对于每个对象,我们通过模型中的 model.wv 获取其对应的嵌入向量。如果某个对象没有嵌入向量(例如由于它在训练中未出现),我们可以将其设置为 None。

  • 数据结构修改:将每对对象的嵌入向量(properties1 和 properties2)与它们的关系信息(relationship)一起构造一个新的数据项。

  • 结果保存:最终,将所有修改后的数据写入一个新的JSON文件,以便在后续的分析中使用。

3.6 完整代码

import json
import numpy as np
from node2vec import Node2Vec
import networkx as nx
import matplotlib.pyplot as plt


# 读取并转换JSON文件为Graph对象的函数
def read_and_convert_json_to_graph(json_file):
    with open(json_file, 'r') as file:
        json_list = json.load(file)

    G = nx.Graph()  # 创建一个空的无向图
    for item in json_list:
        G.add_edge(item['object1'], item['object2'])  # 添加边

    return G


# Node2Vec模型训练函数
def train_node2vec(graph, dimensions=64, walk_length=30, num_walks=200, p=1, q=1, workers=4):
    # 使用Node2Vec库
    node2vec = Node2Vec(graph, dimensions=dimensions, walk_length=walk_length, num_walks=num_walks, p=p, q=q,
                        workers=workers)

    # 训练模型
    model = node2vec.fit(window=10, min_count=1, batch_words=4)

    return model


# 可视化嵌入的函数,实现二维可视化
def visualize_embeddings(model, graph, save_path='embeddings_visualization.png'):
    # 获取嵌入向量
    embeddings = np.array([model.wv[str(node)] for node in graph.nodes()])

    # 使用PCA进行降维以便于可视化
    from sklearn.decomposition import PCA
    pca = PCA(n_components=2)
    embeddings_2d = pca.fit_transform(embeddings)

    # 绘制节点嵌入
    plt.figure(figsize=(20, 16))
    pos = {node: embeddings_2d[i] for i, node in enumerate(graph.nodes())}

    # 调整节点大小和边的透明度
    nx.draw_networkx_nodes(graph, pos=pos, node_size=30, node_color='blue')
    edges = nx.draw_networkx_edges(graph, pos=pos, alpha=0.1)  # 减少alpha以降低边的透明度

    plt.axis('off')  # 关闭坐标轴
    plt.savefig(save_path, format='PNG')  # 先保存可视化结果到文件
    plt.show()  # 然后显示图像
    plt.close()  # 关闭图形,防止重复显示

# 修改后的可视化嵌入函数,实现三维可视化
def visualize_embeddings_3d(model, graph, save_path='embeddings_visualization_3d.png'):
    # 获取嵌入向量
    embeddings = np.array([model.wv[str(node)] for node in graph.nodes()])

    # 使用PCA进行到3维的降维
    from sklearn.decomposition import PCA
    pca = PCA(n_components=3)
    embeddings_3d = pca.fit_transform(embeddings)

    # 创建3D绘图
    fig = plt.figure(figsize=(20, 16))
    ax = fig.add_subplot(111, projection='3d')

    # 获取节点的三维坐标
    xs = embeddings_3d[:, 0]
    ys = embeddings_3d[:, 1]
    zs = embeddings_3d[:, 2]

    # 绘制节点
    ax.scatter(xs, ys, zs, c='blue', marker='o')

    ax.set_xlabel('PCA1')
    ax.set_ylabel('PCA2')
    ax.set_zlabel('PCA3')
    plt.title('3D visualization of Node Embeddings')

    plt.savefig(save_path, format='PNG')  # 保存可视化结果到文件
    plt.show()  # 显示图像
    plt.close()  # 关闭图形,防止重复显示

def save_embeddings_to_json(graph, model, json_file_path, output_file):
    # 首先,读取原始的JSON文件来获取对象间的关系
    with open(json_file_path, 'r') as file:
        json_list = json.load(file)

    output_data = []

    for item in json_list:
        object1, object2 = item['object1'], item['object2']
        # 对于每对对象,获取其嵌入向量
        properties1 = model.wv[object1].tolist() if object1 in model.wv else None
        properties2 = model.wv[object2].tolist() if object2 in model.wv else None
        # 构建新的字典项
        modified_item = {
            "object1": object1,
            "properties1": properties1,
            "object2": object2,
            "properties2": properties2,
            "relationship": item.get('relationship')  # 假设原始JSON中包含关系字段
        }
        output_data.append(modified_item)

    # 将修改后的数据写入新的JSON文件
    with open(output_file, 'w') as f:
        json.dump(output_data, f, indent=4)

# 主程序
if __name__ == "__main__":
    json_file_path = 'Node2Vec_Input.json'
    output_file = 'Node2Vec_Embeddings_Dim768.json'

    G = read_and_convert_json_to_graph(json_file_path)

    #节点嵌入维度为dimensions
    model = train_node2vec(G, dimensions=768, walk_length=20, num_walks=200, p=1, q=2, workers=4)
    visualize_embeddings_3d(model, G, 'embeddings_visualization_Dim768_3D_20-200-1-2-4.png')

    # 调用新定义的函数,保存嵌入和关系到JSON
    save_embeddings_to_json(G, model, json_file_path, output_file)

四、结论

  通过以上的过程,我们完整地介绍了如何使用Node2Vec进行图嵌入、如何将嵌入结果可视化以及如何保存嵌入向量。Node2Vec 提供了一种灵活且高效的方式来处理图数据,并且能够通过参数调整适应不同的图结构。通过嵌入向量,我们可以将图结构信息转化为适合机器学习模型处理的低维向量形式,从而为分类、聚类、推荐系统等任务提供支持。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

晓shuo

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

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

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

打赏作者

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

抵扣说明:

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

余额充值