16、图算法与机器学习:链接预测的实践探索

图算法与机器学习:链接预测的实践探索

1. 三角形计数与聚类系数在链接预测中的应用

在链接预测任务中,推荐解决方案常基于某种三角形度量进行预测。我们可以通过执行以下查询来计算节点参与的三角形数量及其聚类系数:

CALL algo.triangleCount('Author', 'CO_AUTHOR_EARLY', { write:true,
  writeProperty:'trianglesTrain', clusteringCoefficientProperty:
                'coefficientTrain'});
CALL algo.triangleCount('Author', 'CO_AUTHOR', { write:true,
  writeProperty:'trianglesTest', clusteringCoefficientProperty:
                'coefficientTest'});

下面的函数将这些特征添加到我们的 DataFrames 中:

def apply_triangles_features(data, triangles_prop, coefficient_prop):
    query = """
    UNWIND $pairs AS pair
    MATCH (p1) WHERE id(p1) = pair.node1
    MATCH (p2) WHERE id(p2) = pair.node2
    RETURN pair.node1 AS node1,
           pair.node2 AS node2,
           apoc.coll.min([p1[$trianglesProp], p2[$trianglesProp]])
                                              AS minTriangles,
           apoc.coll.max([p1[$trianglesProp], p2[$trianglesProp]])
                                              AS maxTriangles,
           apoc.coll.min([p1[$coefficientProp], p2[$coefficientProp]])
                                                AS minCoefficient,
           apoc.coll.max([p1[$coefficientProp], p2[$coefficientProp]])
                                                AS maxCoefficient
    """
    params = {
        "pairs": [{"node1": row["node1"], "node2": row["node2"]}
                            for row in data.collect()],
        "trianglesProp": triangles_prop,
        "coefficientProp": coefficient_prop
    }
    features = spark.createDataFrame(graph.run(query, params).to_data_frame())
    return data.join(features, ["node1", "node2"])

我们使用最小和最大前缀来处理三角形计数和聚类系数算法,以防止模型基于无向图中作者对的顺序进行学习。通过以下代码将此函数应用于训练和测试 DataFrames:

training_data = apply_triangles_features(training_data,
                                        "trianglesTrain", "coefficientTrain")
test_data = apply_triangles_features(test_data,
                                        "trianglesTest", "coefficientTest")

运行以下代码显示每个三角形特征的描述性统计信息:

(training_data.filter(training_data["label"]==1)
 .describe()
 .select("summary", "minTriangles", "maxTriangles",
                    "minCoefficient", "maxCoefficient")
 .show())
(training_data.filter(training_data["label"]==0)
 .describe()
 .select("summary", "minTriangles", "maxTriangles", "minCoefficient",
                                                    "maxCoefficient")
 .show())

结果如下表所示:
| summary | minTriangles | maxTriangles | minCoefficient | maxCoefficient |
| ---- | ---- | ---- | ---- | ---- |
| count | 81096 | 81096 | 81096 | 81096 |
| mean | 19.478260333431983 | 27.73590559337082 | 0.5703773654487051 | 0.8453786164620439 |
| stddev | 65.7615282768483 | 74.01896188921927 | 0.3614610553659958 | 0.2939681857356519 |
| min | 0 | 0 | 0.0 | 0.0 |
| max | 622 | 785 | 1.0 | 1.0 |

summary minTriangles maxTriangles minCoefficient maxCoefficient
count 81096 81096 81096 81096
mean 5.754661142349808 35.651980368945445 0.49048921333297446 0.860283935358397
stddev 20.639236521699 85.82843448272624 0.3684138346533951 0.2578219623967906
min 0 0 0.0 0.0
max 617 785 1.0 1.0

从对比中可以看出,合著和非合著数据之间的差异并不显著,这可能意味着这些特征的预测能力有限。

接下来,我们可以通过以下代码训练另一个模型:

fields = ["commonAuthors", "prefAttachment", "totalNeighbors",
          "minTriangles", "maxTriangles", "minCoefficient", "maxCoefficient"]
triangle_model = train_model(fields, training_data)

评估模型并显示结果:

triangle_results = evaluate_model(triangle_model, test_data)
display_results(triangle_results)

三角形模型的预测指标如下表所示:
| measure | score |
| ---- | ---- |
| accuracy | 0.992924 |
| recall | 0.965384 |
| precision | 0.958582 |

通过向先前模型添加新特征,我们的预测指标有了显著提升。使用以下代码将三角形模型添加到 ROC 曲线图表中:

plt, fig = create_roc_plot()
add_curve(plt, "Common Authors",
          basic_results["fpr"], basic_results["tpr"], basic_results["roc_auc"])
add_curve(plt, "Graphy",
          graphy_results["fpr"], graphy_results["tpr"],
                                 graphy_results["roc_auc"])
add_curve(plt, "Triangles",
          triangle_results["fpr"], triangle_results["tpr"],
                                   triangle_results["roc_auc"])
plt.legend(loc='lower right')
plt.show()

2. 社区检测在链接预测中的应用

我们假设处于同一社区的节点更有可能建立链接,并且社区越紧密,建立链接的可能性就越大。

首先,使用 Neo4j 中的标签传播算法计算更粗粒度的社区:

CALL algo.labelPropagation("Author", "CO_AUTHOR_EARLY", "BOTH",
  {partitionProperty: "partitionTrain"});
CALL algo.labelPropagation("Author", "CO_AUTHOR", "BOTH",
  {partitionProperty: "partitionTest"});

然后,使用 Louvain 算法计算更细粒度的组,并将最小的簇存储在相应属性中:

CALL algo.louvain.stream("Author", "CO_AUTHOR_EARLY",
                        {includeIntermediateCommunities:true})
YIELD nodeId, community, communities
WITH algo.getNodeById(nodeId) AS node, communities[0] AS smallestCommunity
SET node.louvainTrain = smallestCommunity;
CALL algo.louvain.stream("Author", "CO_AUTHOR",
                        {includeIntermediateCommunities:true})
YIELD nodeId, community, communities
WITH algo.getNodeById(nodeId) AS node, communities[0] AS smallestCommunity
SET node.louvainTest = smallestCommunity;

以下函数用于返回这些算法的结果:

def apply_community_features(data, partition_prop, louvain_prop):
    query = """
    UNWIND $pairs AS pair
    MATCH (p1) WHERE id(p1) = pair.node1
    MATCH (p2) WHERE id(p2) = pair.node2
    RETURN pair.node1 AS node1,
           pair.node2 AS node2,
           CASE WHEN p1[$partitionProp] = p2[$partitionProp] THEN
                     1 ELSE 0 END AS samePartition,
           CASE WHEN p1[$louvainProp] = p2[$louvainProp] THEN
                     1 ELSE 0 END AS sameLouvain
    """
    params = {
        "pairs": [{"node1": row["node1"], "node2": row["node2"]} for
                            row in data.collect()],
        "partitionProp": partition_prop,
        "louvainProp": louvain_prop
    }
    features = spark.createDataFrame(graph.run(query, params).to_data_frame())
    return data.join(features, ["node1", "node2"])

将此函数应用于训练和测试 DataFrames:

training_data = apply_community_features(training_data,
                                        "partitionTrain", "louvainTrain")
test_data = apply_community_features(test_data, "partitionTest", "louvainTest")

运行以下代码查看节点对是否属于同一分区:

plt.style.use('fivethirtyeight')
fig, axs = plt.subplots(1, 2, figsize=(18, 7), sharey=True)
charts = [(1, "have collaborated"), (0, "haven't collaborated")]
for index, chart in enumerate(charts):
    label, title = chart
    filtered = training_data.filter(training_data["label"] == label)
    values = (filtered.withColumn('samePartition',
             F.when(F.col("samePartition") == 0, "False")
                                  .otherwise("True"))
              .groupby("samePartition")
              .agg(F.count("label").alias("count"))
              .select("samePartition", "count")
              .toPandas())
    values.set_index("samePartition", drop=True, inplace=True)
    values.plot(kind="bar", ax=axs[index], legend=None,
                title=f"Authors who {title} (label={label})")
    axs[index].xaxis.set_label_text("Same Partition")
plt.tight_layout()
plt.show()

结果表明,合作过的作者更有可能处于同一分区,这一特征具有较强的预测能力。

对 Louvain 簇进行同样的操作:

plt.style.use('fivethirtyeight')
fig, axs = plt.subplots(1, 2, figsize=(18, 7), sharey=True)
charts = [(1, "have collaborated"), (0, "haven't collaborated")]
for index, chart in enumerate(charts):
    label, title = chart
    filtered = training_data.filter(training_data["label"] == label)
    values = (filtered.withColumn('sameLouvain',
              F.when(F.col("sameLouvain") == 0, "False")
                                  .otherwise("True"))
              .groupby("sameLouvain")
              .agg(F.count("label").alias("count"))
              .select("sameLouvain", "count")
              .toPandas())
    values.set_index("sameLouvain", drop=True, inplace=True)
    values.plot(kind="bar", ax=axs[index], legend=None,
                title=f"Authors who {title} (label={label})")
    axs[index].xaxis.set_label_text("Same Louvain")
plt.tight_layout()
plt.show()

结果显示,合作过的作者更有可能处于同一簇,而未合作过的作者处于同一簇的可能性极低,这一特征也具有较强的预测能力。

训练另一个模型:

fields = ["commonAuthors", "prefAttachment", "totalNeighbors",
          "minTriangles", "maxTriangles", "minCoefficient", "maxCoefficient",
          "samePartition", "sameLouvain"]
community_model = train_model(fields, training_data)

评估模型并显示结果:

community_results = evaluate_model(community_model, test_data)
display_results(community_results)

社区模型的预测指标如下表所示:
| measure | score |
| ---- | ---- |
| accuracy | 0.995771 |
| recall | 0.957088 |
| precision | 0.978674 |

部分指标有所提升,使用以下代码绘制所有模型的 ROC 曲线进行比较:

plt, fig = create_roc_plot()
add_curve(plt, "Common Authors",
          basic_results["fpr"], basic_results["tpr"], basic_results["roc_auc"])
add_curve(plt, "Graphy",
          graphy_results["fpr"], graphy_results["tpr"],
          graphy_results["roc_auc"])
add_curve(plt, "Triangles",
          triangle_results["fpr"], triangle_results["tpr"],
          triangle_results["roc_auc"])
add_curve(plt, "Community",
          community_results["fpr"], community_results["tpr"],
          community_results["roc_auc"])
plt.legend(loc='lower right')
plt.show()

查看最重要的特征:

rf_model = community_model.stages[-1]
plot_feature_importance(fields, rf_model.featureImportances)

3. 总结与建议

通过以上实践,我们发现简单的基于图的特征是一个良好的开端,随着添加更多基于图算法的特征,预测指标不断提升。我们现在拥有一个良好的、平衡的模型来预测合著链接。

使用图进行连接特征提取可以显著提高预测效果,但理想的图特征和算法会因数据的属性(包括网络领域和图的形状)而异。建议首先考虑数据中的预测元素,并使用不同类型的连接特征测试假设,然后进行微调。

4. 读者练习建议

  • 我们的模型对未包含的会议数据的预测能力如何?
  • 测试新数据时,移除某些特征会有什么影响?
  • 不同的训练和测试年份划分是否会影响预测结果?
  • 该数据集包含论文之间的引用,能否利用这些数据生成不同的特征或预测未来的引用?

5. 其他相关信息

5.1 其他算法

除了上述使用的算法,还有许多可用于图数据的算法。一些算法(如着色和启发式算法)由于更适用于学术场景或易于推导而未被涉及。而基于边的社区检测等算法虽有趣,但尚未在 Neo4j 或 Apache Spark 中实现。随着图分析的应用增长,预计这两个平台使用的图算法列表将不断增加。

此外,还有一些与图相关但并非严格意义上的图算法,如机器学习中的一些算法以及常用于推荐和链接预测的相似性算法。

5.2 Neo4j 批量数据导入和 Yelp 数据集

使用 Cypher 查询语言将数据导入 Neo4j 采用事务性方法,适用于增量数据加载或最多 1000 万条记录的批量加载。对于初始批量数据集的导入,Neo4j Import 工具是更好的选择,它可以直接创建存储文件,跳过事务日志。

Yelp 数据集较大,适合使用 Neo4j Import 工具。由于数据为 JSON 格式,需要先将其转换为该工具期望的 CSV 格式。可以使用 Python 编写简单脚本进行转换,详细说明可在相关资源库中找到。

5.3 APOC 和其他 Neo4j 工具

Awesome Procedures on Cypher (APOC) 是一个包含 450 多个过程和函数的库,可帮助完成常见任务,如数据集成、数据清理和数据转换等,是 Neo4j 的标准库。

Neo4j 还有其他工具,如用于无代码探索的算法“游乐场”应用程序,可在其图算法开发者网站上找到。

5.4 数据集来源

寻找符合测试目标或假设的图数据集可能具有挑战性。除了查阅研究论文,还可以探索以下网络数据集索引:
- 斯坦福网络分析项目 (SNAP):包含多个数据集以及相关论文和使用指南。
- 科罗拉多复杂网络索引 (ICON):可搜索来自网络科学各个领域的高质量研究网络数据集。
- 科布伦茨网络集合 (KONECT):包含各种类型的大型网络数据集,用于网络科学研究。

大多数数据集需要进行一些处理才能转换为更有用的格式。

5.5 平台帮助资源

Apache Spark 和 Neo4j 平台有许多在线资源。如果有特定问题,可以向各自的社区寻求帮助:
- 一般 Spark 问题:在 Spark 社区页面订阅 users@spark.apache.org。
- GraphFrames 问题:使用 GitHub 问题跟踪器。
- 所有 Neo4j 问题(包括图算法相关问题):访问 Neo4j 在线社区。

5.6 在线学习资源

有许多优秀的资源可用于开始图分析学习,以下是一些在线学习的好例子:
- Coursera 的 Python 应用社会网络分析课程
- Leonid Zhukov 的社会网络分析 YouTube 系列
- 斯坦福大学的网络分析课程,包括视频讲座、阅读列表和其他资源
- Complexity Explorer 提供的复杂性科学在线课程

6. 索引相关算法及概念

以下是一些常见的图算法和相关概念的简要介绍:
| 算法/概念 | 描述 |
| ---- | ---- |
| A* 算法 | 用于最短路径搜索,在 Neo4j 中有应用 |
| 所有对最短路径 (APSP) 算法 | 计算图中所有节点对之间的最短路径,可在 Apache Spark 和 Neo4j 中使用 |
| 广度优先搜索 (BFS) | 一种图搜索算法,在 Apache Spark 中有实现 |
| 接近中心性算法 | 衡量节点在图中的中心程度,有不同的变体,可在 Apache Spark 和 Neo4j 中使用 |
| 连通分量算法 | 用于找到图中的连通分量,可在 Apache Spark 和 Neo4j 中使用 |
| 度中心性算法 | 计算节点的度,可用于评估节点的重要性 |
| 标签传播算法 | 用于社区检测,可在 Apache Spark 和 Neo4j 中使用 |
| Louvain 模块化算法 | 用于社区检测,通过模块化进行基于质量的分组 |
| PageRank 算法 | 衡量网页的重要性,可在 Apache Spark 和 Neo4j 中使用 |
| 最短路径算法 | 包括加权和未加权的情况,有多种实现方式 |
| 单源最短路径算法 | 计算从单个源节点到其他所有节点的最短路径,可在 Apache Spark 和 Neo4j 中使用 |
| 强连通分量算法 | 用于找到图中的强连通分量,可在 Apache Spark 和 Neo4j 中使用 |
| 三角形计数和聚类系数算法 | 用于分析图的局部结构,可在 Apache Spark 和 Neo4j 中使用 |

这些算法和概念在图分析和机器学习中都有重要的应用,可以根据具体需求选择合适的算法进行分析和预测。

7. 图算法与机器学习的结合流程

下面通过 mermaid 流程图展示图算法与机器学习结合进行链接预测的整体流程:

graph LR
    classDef startend fill:#F5EBFF,stroke:#BE8FED,stroke-width:2px;
    classDef process fill:#E5F6FF,stroke:#73A6FF,stroke-width:2px;
    classDef decision fill:#FFF6CC,stroke:#FFBC52,stroke-width:2px;

    A([开始]):::startend --> B(数据准备):::process
    B --> C(特征提取):::process
    C --> D{特征选择}:::decision
    D -->|合适| E(模型训练):::process
    D -->|不合适| C
    E --> F(模型评估):::process
    F --> G{指标达标?}:::decision
    G -->|是| H([结束]):::startend
    G -->|否| C

这个流程清晰地展示了从数据准备到最终模型评估的整个过程。如果特征选择不合适或者模型评估指标未达标,都需要回到特征提取阶段进行调整。

8. 具体算法的操作步骤总结

8.1 三角形计数与聚类系数算法操作步骤
  1. 计算节点的三角形数量和聚类系数:
CALL algo.triangleCount('Author', 'CO_AUTHOR_EARLY', { write:true,
  writeProperty:'trianglesTrain', clusteringCoefficientProperty:
                'coefficientTrain'});
CALL algo.triangleCount('Author', 'CO_AUTHOR', { write:true,
  writeProperty:'trianglesTest', clusteringCoefficientProperty:
                'coefficientTest'});
  1. 添加特征到 DataFrames:
def apply_triangles_features(data, triangles_prop, coefficient_prop):
    query = """
    UNWIND $pairs AS pair
    MATCH (p1) WHERE id(p1) = pair.node1
    MATCH (p2) WHERE id(p2) = pair.node2
    RETURN pair.node1 AS node1,
           pair.node2 AS node2,
           apoc.coll.min([p1[$trianglesProp], p2[$trianglesProp]])
                                              AS minTriangles,
           apoc.coll.max([p1[$trianglesProp], p2[$trianglesProp]])
                                              AS maxTriangles,
           apoc.coll.min([p1[$coefficientProp], p2[$coefficientProp]])
                                                AS minCoefficient,
           apoc.coll.max([p1[$coefficientProp], p2[$coefficientProp]])
                                                AS maxCoefficient
    """
    params = {
        "pairs": [{"node1": row["node1"], "node2": row["node2"]}
                            for row in data.collect()],
        "trianglesProp": triangles_prop,
        "coefficientProp": coefficient_prop
    }
    features = spark.createDataFrame(graph.run(query, params).to_data_frame())
    return data.join(features, ["node1", "node2"])
  1. 应用到训练和测试数据:
training_data = apply_triangles_features(training_data,
                                        "trianglesTrain", "coefficientTrain")
test_data = apply_triangles_features(test_data,
                                        "trianglesTest", "coefficientTest")
  1. 显示描述性统计信息:
(training_data.filter(training_data["label"]==1)
 .describe()
 .select("summary", "minTriangles", "maxTriangles",
                    "minCoefficient", "maxCoefficient")
 .show())
(training_data.filter(training_data["label"]==0)
 .describe()
 .select("summary", "minTriangles", "maxTriangles", "minCoefficient",
                                                    "maxCoefficient")
 .show())
  1. 训练模型:
fields = ["commonAuthors", "prefAttachment", "totalNeighbors",
          "minTriangles", "maxTriangles", "minCoefficient", "maxCoefficient"]
triangle_model = train_model(fields, training_data)
  1. 评估模型:
triangle_results = evaluate_model(triangle_model, test_data)
display_results(triangle_results)
8.2 社区检测算法操作步骤
  1. 计算粗粒度社区(标签传播算法):
CALL algo.labelPropagation("Author", "CO_AUTHOR_EARLY", "BOTH",
  {partitionProperty: "partitionTrain"});
CALL algo.labelPropagation("Author", "CO_AUTHOR", "BOTH",
  {partitionProperty: "partitionTest"});
  1. 计算细粒度组(Louvain 算法):
CALL algo.louvain.stream("Author", "CO_AUTHOR_EARLY",
                        {includeIntermediateCommunities:true})
YIELD nodeId, community, communities
WITH algo.getNodeById(nodeId) AS node, communities[0] AS smallestCommunity
SET node.louvainTrain = smallestCommunity;
CALL algo.louvain.stream("Author", "CO_AUTHOR",
                        {includeIntermediateCommunities:true})
YIELD nodeId, community, communities
WITH algo.getNodeById(nodeId) AS node, communities[0] AS smallestCommunity
SET node.louvainTest = smallestCommunity;
  1. 添加社区特征到 DataFrames:
def apply_community_features(data, partition_prop, louvain_prop):
    query = """
    UNWIND $pairs AS pair
    MATCH (p1) WHERE id(p1) = pair.node1
    MATCH (p2) WHERE id(p2) = pair.node2
    RETURN pair.node1 AS node1,
           pair.node2 AS node2,
           CASE WHEN p1[$partitionProp] = p2[$partitionProp] THEN
                     1 ELSE 0 END AS samePartition,
           CASE WHEN p1[$louvainProp] = p2[$louvainProp] THEN
                     1 ELSE 0 END AS sameLouvain
    """
    params = {
        "pairs": [{"node1": row["node1"], "node2": row["node2"]} for
                            row in data.collect()],
        "partitionProp": partition_prop,
        "louvainProp": louvain_prop
    }
    features = spark.createDataFrame(graph.run(query, params).to_data_frame())
    return data.join(features, ["node1", "node2"])
  1. 应用到训练和测试数据:
training_data = apply_community_features(training_data,
                                        "partitionTrain", "louvainTrain")
test_data = apply_community_features(test_data, "partitionTest", "louvainTest")
  1. 查看节点对是否属于同一分区和簇:
plt.style.use('fivethirtyeight')
fig, axs = plt.subplots(1, 2, figsize=(18, 7), sharey=True)
charts = [(1, "have collaborated"), (0, "haven't collaborated")]
for index, chart in enumerate(charts):
    label, title = chart
    filtered = training_data.filter(training_data["label"] == label)
    values = (filtered.withColumn('samePartition',
             F.when(F.col("samePartition") == 0, "False")
                                  .otherwise("True"))
              .groupby("samePartition")
              .agg(F.count("label").alias("count"))
              .select("samePartition", "count")
              .toPandas())
    values.set_index("samePartition", drop=True, inplace=True)
    values.plot(kind="bar", ax=axs[index], legend=None,
                title=f"Authors who {title} (label={label})")
    axs[index].xaxis.set_label_text("Same Partition")
plt.tight_layout()
plt.show()

plt.style.use('fivethirtyeight')
fig, axs = plt.subplots(1, 2, figsize=(18, 7), sharey=True)
charts = [(1, "have collaborated"), (0, "haven't collaborated")]
for index, chart in enumerate(charts):
    label, title = chart
    filtered = training_data.filter(training_data["label"] == label)
    values = (filtered.withColumn('sameLouvain',
              F.when(F.col("sameLouvain") == 0, "False")
                                  .otherwise("True"))
              .groupby("sameLouvain")
              .agg(F.count("label").alias("count"))
              .select("sameLouvain", "count")
              .toPandas())
    values.set_index("sameLouvain", drop=True, inplace=True)
    values.plot(kind="bar", ax=axs[index], legend=None,
                title=f"Authors who {title} (label={label})")
    axs[index].xaxis.set_label_text("Same Louvain")
plt.tight_layout()
plt.show()
  1. 训练模型:
fields = ["commonAuthors", "prefAttachment", "totalNeighbors",
          "minTriangles", "maxTriangles", "minCoefficient", "maxCoefficient",
          "samePartition", "sameLouvain"]
community_model = train_model(fields, training_data)
  1. 评估模型:
community_results = evaluate_model(community_model, test_data)
display_results(community_results)

9. 总结

通过上述对图算法与机器学习结合进行链接预测的详细介绍,我们可以看到图算法在特征提取和模型优化方面发挥了重要作用。从三角形计数与聚类系数算法到社区检测算法,每一步都为提高模型的预测能力做出了贡献。

在实际应用中,我们需要根据具体的数据特点和问题需求选择合适的算法和特征。同时,不断尝试和调整模型,以达到最佳的预测效果。希望本文能为你在图算法和机器学习领域的实践提供有价值的参考。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符  | 博主筛选后可见
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值