这里再给大家推荐主要基于 Neo4J实现的案例算法书《Graph Algorithms》,其作者 Amy Holder 和 Mark Needham也是 Neo4j的员工。Neo4j Desktop 地址:
https://neo4j.com/download/
图数据库对于分析异构数据点之间的关系特别的有用,例如防欺诈或Facebook的好友关系图,以在社交网络关系的预测任务为例,复杂的(社交)网络一个最重要的基本构成是链接,在社交关系网络中基于已有节点和链接构成的网络信息,预测潜在关系,这背后一个核心的算法就是链路预测算法。这也是我们今天文章中的核心算法,Neo4J图算法库支持了多种链路预测算法,在初识Neo4J 后,我们就开始步入链路预测算法的学习,以及如何将数据导入Neo4J中,通过Scikit-Learning与链路预测算法,搭建机器学习预测任务模型。在线阅读地址:
https://neo4j.com/docs/graph-algorithms/current/
随后,Kleinberg 和 Liben-Nowell 提出从社交网络的角度来解决链路预测问题,如下所述:论文地址:
https://www.cs.cornell.edu/home/kleinber/link-pred.pdf
后来,Jim Webber 博士在 GraphConnect San Francisco 2015 大会上介绍了图算法的发展历程,他用图理论讲解了第二次世界大战。若给定一个社交网络的快照,我们能预测出该网络中的成员在未来可能出现哪些新的关系吗?我们可以把这个问题看作链路预测问题,然后对网络中各节点的相似度进行分析,从而得出预测链路的方法。
除了预测世界大战和社交网络中的朋友关系,我们还可能在什么场景用到关系预测呢?我们可以预测恐怖组织成员之间的关系,生物网络中分子间的关系,引文网络中潜在的共同创作关系,对艺术家或艺术品的兴趣等等,这些场景都可能用得上链路预测。 链路的预测都意味着对未来可能发生的行为进行预测,比如在一个引文网络中,我们是在对两个人是否可能合作写一篇论文进行预测。演讲视频:
https://youtu.be/kVHdMD-XT9s
Kleinberg 和 Liben-Nowell 在论文中所介绍的算法
这些方法都是计算一对节点的分数,该分数可看作为那些节点基于拓扑网络的“近似度”。两个节点越相近,它们之间存在联系的可能性就越大。 下面我们来看看几个评估标准,以便于我们理解算法的原理。这个度量标准计算了一对节点所共享的相同邻居数目。如下图所示,节点 A 和 D 有两个共同邻居(节点 B 和 C),而节点 A 和 E 只有一个共同邻居(节点 B)。因此,我们认为节点 A 和 D 更相近,未来更有可能产生关联。作为预测因子,共同邻居数可以捕捉到拥有同一个朋友的两个陌生人,而这两个人可能会被这个朋友介绍认识(图中出现一个闭合的三角形)。
UNWIND [["A", "C"], ["A", "B"], ["B", "D"],
["B", "C"], ["B", "E"], ["C", "D"]] AS pair
MERGE (n1:Node {name: pair[0]})
MERGE (n2:Node {name: pair[1]})
MERGE (n1)-[:FRIENDS]-(n2)
然后用下面的函数来计算节点 A 和 D 的共同邻居数:
neo4j> MATCH (a:Node {name: 'A'}) MATCH (d:Node {name: 'D'}) RETURN algo.linkprediction.commonNeighbors(a, d);+-------------------------------------------+| algo.linkprediction.commonNeighbors(a, d) |+-------------------------------------------+| 2.0 |+-------------------------------------------+1 row available after 97 ms, consumed after another 15 ms
这些节点有两个共同邻居,所以它们的得分为2。现在对节点 A 和 E 进行同样的计算。因为它们只有一个共同邻居,不出意外我们得到的分数应该为1。
neo4j> MATCH (a:Node {name: 'A'})
MATCH (e:Node {name: 'E'})
RETURN algo.linkprediction.commonNeighbors(a, e);
+-------------------------------------------+
| algo.linkprediction.commonNeighbors(a, e) |
+-------------------------------------------+
| 1.0 |
+-------------------------------------------+
如我们所料,得分确实为1。该函数默认的计算方式涵盖任意的类型以及指向。我们也可以通过传入特定的参数来进行计算:
neo4j> WITH {direction: "BOTH", relationshipQuery: "FRIENDS"}
AS config
MATCH (a:Node {name: 'A'})
MATCH (e:Node {name: 'E'})
RETURN algo.linkprediction.commonNeighbors(a, e, config)
AS score;
+-------+
| score |
+-------+
| 1.0 |
+-------+
为了确保得到准确的结果,我们再试试另一种算法。 优先连接函数返回的是两个节点度数的乘积。如果我们对节点 A 和 D 进行计算,会得到 2*2=4 的结果,因为节点 A 和 D 都有两个邻居。下面来试一试:
neo4j> MATCH (a:Node {name: 'A'})
MATCH (d:Node {name: 'D'})
RETURN algo.linkprediction.preferentialAttachment(a, d)
AS score;
+-------+
| score |
+-------+
| 4.0 |
+-------+
在这个系列教程中,我们会重点介绍有监督学习的方法。参考阅读文章《Link Prediction In Large-Scale Networks》中有对这两种方法的详细介绍:
https://hackernoon.com/link-prediction-in-large-scale-networks-f836fcb05c88?gi=b86a42e1c8d4
# negative examples = (# nodes)² - (# relationships) - (# nodes)
如果我们将训练集中的全部负样本都代入模型,就会导致严重的类别不均衡问题,即负样本数远大于正样本数。 若基于这种不均衡数据集进行模型的训练,只要我们预测任何节点对都不存在关联,就可以得到非常不错的准确度,但这当然不是我们想要的。 所以我们需要尽量减少负样本的数目。有一种方法被多篇论文提及过,那就是选择那些彼此间距相等的节点对。这种方法可以有效地减少负样本数,虽然负样本数仍然远大于正样本数。 为了解决样本不均衡的问题,我们也可以对负样本进行欠采样,或者对正样本进行过采样。
// Create constraints
CREATE CONSTRAINT ON (a:Article) ASSERT a.index IS UNIQUE;
CREATE CONSTRAINT ON (a:Author) ASSERT a.name IS UNIQUE;
CREATE CONSTRAINT ON (v:Venue) ASSERT v.name IS UNIQUE;
// Import data from JSON files using the APOC library
CALL apoc.periodic.iterate(
'UNWIND ["dblp-ref-0.json", "dblp-ref-1.json", "dblp-ref-2.json", "dblp-ref-3.json"] AS file
CALL apoc.load.json("https://github.com/mneedham/link-prediction/raw/master/data/" + file)
YIELD value WITH value
RETURN value',
'MERGE (a:Article {index:value.id})
SET a += apoc.map.clean(value,["id","authors","references", "venue"],[0])
WITH a, value.authors as authors, value.references AS citations, value.venue AS venue
MERGE (v:Venue {name: venue})
MERGE (a)-[:VENUE]->(v)
FOREACH(author in authors |
MERGE (b:Author{name:author})
MERGE (a)-[:AUTHOR]->(b))
FOREACH(citation in citations |
MERGE (cited:Article {index:citation})
MERGE (a)-[:CITED]->(cited))',
{batchSize: 1000, iterateList: true});
下图是数据导入到Neo4j后的显示:
2、搭建共同作者图
MATCH (a1)(a2:Author)
WITH a1, a2, paper
ORDER BY a1, paper.year
WITH a1, a2, collect(paper)[0].year AS year,
count(*) AS collaborations
MERGE (a1)-[coauthor:CO_AUTHOR {year: year}]-(a2)
SET coauthor.collaborations = collaborations;
即使在多篇文章中进行过合作,我们也只能在合作的作者之间创建一种CO_AUTHOR关系。我们在这些关系上创建几个属性: (1)年份属性,指合作者们共同完成的第一篇文章的出版年份 (2)合作属性,指作者们合作过多少篇文章
Neo4j 中的共同作者
现在已经有了合著者关系图表,我们需要弄清楚如何预测作者之间未来合作的可能性,我们将构建一个二进制分类器来执行此操作,因此下一步是创建训练图和测试图。训练子图
MATCH (a)-[r:CO_AUTHOR]->(b)
WHERE r.year < 2006
MERGE (a)-[:CO_AUTHOR_EARLY {year: r.year}]-(b);
测试子图
MATCH (a)-[r:CO_AUTHOR]->(b)
WHERE r.year >= 2006
MERGE (a)-[:CO_AUTHOR_LATE {year: r.year}]-(b);
这样分组使我们在2005年之前的早期图表中有81,096个关系,在2006年之后的后期图表中有74,128个关系,形成了52-48的比例。这个比例比通常测试中使用的比例高很多,但这没关系。这些子图中的关系将作为训练和测试集中的正例,但我们也需要一些负例。使用否定示例可以让我们的模型学习如何区分在它们之间链接节点和不在它们之间链接节点。 与链接预测问题一样,否定示例比肯定的示例多得多。否定示例的最大数量等于:
# negative examples = (# nodes)² - (# relationships) - (# nodes)
即节点的平方数减去图形所具有的关系再减去自身关系。 除了使用几乎所有可能的配对以外,我们也将彼此之间相距2至3跳的节点进行配对,这将为我们提供更多可管理的数据。我们可以通过运行以下代码来生成和查询配对:
MATCH (author:Author)
WHERE (author)-[:CO_AUTHOR_EARLY]-()
MATCH (author)-[:CO_AUTHOR_EARLY*2..3]-(other)
WHERE not((author)-[:CO_AUTHOR_EARLY]-(other))
RETURN id(author) AS node1, id(other) AS node2
此查询返回4,389,478个否定示和81,096个肯定示,这意味着否定示是肯定示的54倍之多。 但仍然存在很大的不平衡,这意味着用于预测每对节点链接的模型将非常不准确。为了解决这个问题,我们可以对正例进行升采样或对负例进行降采样,可以使用下采样方法。
pip install py2neo==4.1.3 pandas sklearn
(1)py2neo驱动程序使数据科学家能够轻松地将Neo4j与Python数据科学生态系统中的工具相结合。我们将使用该库对Neo4j执行Cypher查询。 (2)pandas是BSD许可的开放源代码库,为Python编程语言提供了高性能、易于使用的数据结构和数据分析工具。 (3)scikit-learn是一个非常受欢迎的机器学习库。我们将使用该库来构建我们的机器学习模型。 (Scikit-Learn workflow 拓展版,来源网络) 安装完这些库后,导入所需的程序包,并创建数据库连接:
from py2neo import Graphimport pandas as pdgraph = Graph("bolt://localhost", auth=("neo4j", "neo4jPassword"))
# Find positive examplestrain_existing_links = graph.run("""MATCH (author:Author)-[:CO_AUTHOR_EARLY]->(other:Author)RETURN id(author) AS node1, id(other) AS node2, 1 AS label""").to_data_frame()# Find negative examplestrain_missing_links = graph.run("""MATCH (author:Author)WHERE (author)-[:CO_AUTHOR_EARLY]-()MATCH (author)-[:CO_AUTHOR_EARLY*2..3]-(other)WHERE not((author)-[:CO_AUTHOR_EARLY]-(other))RETURN id(author) AS node1, id(other) AS node2, 0 AS label""").to_data_frame()# Remove duplicatestrain_missing_links = train_missing_links.drop_duplicates()# Down sample negative examplestrain_missing_links = train_missing_links.sample( n=len(train_existing_links))# Create DataFrame from positive and negative examplestraining_df = train_missing_links.append( train_existing_links, ignore_index=True)training_df['label'] = training_df['label'].astype('category')
一个测试数据集的例子 执行相同的操作来创建测试数据框架,但是这次仅考虑后期图形中的关系:
# Find positive examplestest_existing_links = graph.run("""MATCH (author:Author)-[:CO_AUTHOR_LATE]->(other:Author)RETURN id(author) AS node1, id(other) AS node2, 1 AS label""").to_data_frame()# Find negative examplestest_missing_links = graph.run("""MATCH (author:Author)WHERE (author)-[:CO_AUTHOR_LATE]-()MATCH (author)-[:CO_AUTHOR_LATE*2..3]-(other)WHERE not((author)-[:CO_AUTHOR_LATE]-(other))RETURN id(author) AS node1, id(other) AS node2, 0 AS label""").to_data_frame()# Remove duplicates test_missing_links = test_missing_links.drop_duplicates()# Down sample negative examplestest_missing_links = test_missing_links.sample(n=len(test_existing_links))# Create DataFrame from positive and negative examplestest_df = test_missing_links.append( test_existing_links, ignore_index=True)test_df['label'] = test_df['label'].astype('category')
接下来,开始创建机器学习模型。
from sklearn.ensemble import RandomForestClassifierclassifier = RandomForestClassifier(n_estimators=30, max_depth=10, random_state=0)
现在是时候设计一些用来训练模型的特征。特征提取是一种将大量数据和属性提取为一组具有代表性的数值(特征)的方法。这些特征会作为输入的数据,以便我们区分学习任务的类别/值。
def apply_graphy_features(data, rel_type):
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,
algo.linkprediction.commonNeighbors(
p1, p2, {relationshipQuery: $relType}) AS cn,
algo.linkprediction.preferentialAttachment(
p1, p2, {relationshipQuery: $relType}) AS pa,
algo.linkprediction.totalNeighbors(
p1, p2, {relationshipQuery: $relType}) AS tn
"""
pairs = [{"node1": pair[0], "node2": pair[1]}
for pair in data[["node1", "node2"]].values.tolist()]
params = {"pairs": pairs, "relType": rel_type}
features = graph.run(query, params).to_data_frame()
return pd.merge(data, features, on = ["node1", "node2"])
此功能发起一个查询,该查询从提供的DataFrame中获取配对的节点,并对每一对节点进行以下计算:共同邻居(cn)、优先附件(pa)以及邻居总数(tn) 如下所示,我们可以将其应用于我们的训练并测试DataFrame:
training_df = apply_graphy_features(training_df, "CO_AUTHOR_EARLY")test_df = apply_graphy_features(test_df, "CO_AUTHOR")
对于训练数据框架,仅根据早期图形来计算这些指标,而对于测试数据框架,将在整个图形中进行计算。也可以使用整个图形来计算这些功能,因为图形的演变取决于所有时间,而不仅取决于2006年及以后的情况。 测试训练集 使用以下代码训练模型:
columns = ["cn", "pa", "tn"]X = training_df[columns]y = training_df["label"]classifier.fit(X, y)
现在的模型已经经过训练了,但还需要对它进行评估。
from sklearn.metrics import recall_scorefrom sklearn.metrics import precision_scorefrom sklearn.metrics import accuracy_scoredef evaluate_model(predictions, actual): accuracy = accuracy_score(actual, predictions) precision = precision_score(actual, predictions) recall = recall_score(actual, predictions) metrics = ["accuracy", "precision", "recall"] values = [accuracy, precision, recall] return pd.DataFrame(data={'metric': metrics, 'value': values})def feature_importance(columns, classifier): features = list(zip(columns, classifier.feature_importances_)) sorted_features = sorted(features, key = lambda x: x[1]*-1) keys = [value[0] for value in sorted_features] values = [value[1] for value in sorted_features] return pd.DataFrame(data={'feature': keys, 'value': values})
评估模型执行代码:
predictions = classifier.predict(test_df[columns])y_test = test_df["label"]evaluate_model(predictions, y_test)
(准确率,精准度,召回度) 在各个方面的得分都很高。现在可以运行以下代码来查看哪个特征扮演了最重要的角色:
feature_importance(columns, classifier)
(特征重要度) 在上面我们可以看到,公共邻居(cn)是模型中的主要支配特征。共同邻居意味着作者拥有的未闭合的协同者三角的数量的计数,因此数值这么高并不奇怪。 接下来,添加一些从图形算法生成的新特征。
CALL algo.triangleCount('Author', 'CO_AUTHOR_EARLY', { write:true, writeProperty:'trianglesTrain', clusteringCoefficientProperty:'coefficientTrain'});
然后执行以下Cypher查询以在测试图上运行:
CALL algo.triangleCount('Author', 'CO_AUTHOR', { write:true, writeProperty:'trianglesTest', clusteringCoefficientProperty:'coefficientTest'});
现在节点上有4个新属性:三角训练,系数训练,三角测试和系数测试。现在,在以下功能的帮助下,将它们添加到我们的训练和测试DataFrame中:
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[$triangles], p2[$triangles]]) AS minTriangles, apoc.coll.max([p1[$triangles], p2[$triangles]]) AS maxTriangles, apoc.coll.min([p1[$coefficient], p2[$coefficient]]) AS minCoeff, apoc.coll.max([p1[$coefficient], p2[$coefficient]]) AS maxCoeff """ pairs = [{"node1": pair[0], "node2": pair[1]} for pair in data[["node1", "node2"]].values.tolist()] params = {"pairs": pairs, "triangles": triangles_prop, "coefficient": coefficient_prop} features = graph.run(query, params).to_data_frame() return pd.merge(data, features, on = ["node1", "node2"])
这些参数与我们到目前为止使用的不同,它们不是特定于某个节点配对的,而是针对某个单一节点的参数。不能简单地将这些值作为节点三角或节点系数添加到我们的DataFrame中,因为无法保证节点配对的顺序,我们需要一种与顺序无关的方法。这里可以通过取平均值、值的乘积或通过计算最小值和最大值来实现此目的,如此处所示:
training_df = apply_triangles_features(training_df, "trianglesTrain", "coefficientTrain")test_df = apply_triangles_features(test_df, "trianglesTest", "coefficientTest")
现在可以训练与评估:
columns = [ "cn", "pa", "tn", "minTriangles", "maxTriangles", "minCoeff", "maxCoeff"]X = training_df[columns]y = training_df["label"]classifier.fit(X, y)predictions = classifier.predict(test_df[columns])y_test = test_df["label"]display(evaluate_model(predictions, y_test))
(准确率,精准度,召回度) 这些特征很有帮助!我们的每项参数都比初始模型提高了约4%。哪个特征最重要?
display(feature_importance(columns, classifier))
(特征重要度) 共同邻居还是最具有影响力的特征,但三角特征的重要性也提升了不少。 这篇教程即将结束,基于整个工作流程,希望还可以激发大家更多的思考: (1)还有其他可添加的特征吗?这些特征能帮助我们创建更高准确性的模型吗?也许其他社区检测甚至中心算法也可能会有所帮助? (2)目前,图形算法库中的链接预测算法仅适用于单零件图(两个节点的标签相同的图),该算法基于节点的拓扑;如果我们尝试将其应用于具有不同标签的节点(这些节点可能具有不同的拓扑),这就意味着此算法无法很好地发挥作用,所以目前也在考虑添加适用于其他图表的链接预测算法的版本,也欢迎大家在Github上一起交流。
Github地址:
https://github.com/neo4j-contrib
原文链接:
https://medium.com/neo4j/link-prediction-with-neo4j-part-1-an-introduction-713aa779fd9
https://towardsdatascience.com/link-prediction-with-neo4j-part-2-predicting-co-authors-using-scikit-learn-78b42w356b44c
(*本文为AI科技大本营编译文章,转载请微信联系 1092722531)
◆
精彩推荐
◆
开幕倒计时 2 天!2019 中国大数据技术大会(BDTC)即将震撼来袭!豪华主席阵容及百位技术专家齐聚,十余场精选专题技术和行业论坛,超强干货+技术剖析+行业实践立体解读。
推荐阅读你点的每个“在看”,我都认真当成了AI