网络分析:从基础图算法到分布式图计算
1. 最小生成树
在之前的算法中,我们考虑了如何在两点间遍历图。但当我们想找到连接图中所有节点的路径时,最小生成树就派上用场了。可以将最小生成树看作是全图遍历算法和最短路径算法的结合。
最小生成树在加权图中特别有用。如果权重代表连接两个顶点的成本,那么最小生成树就能找到连接整个图的最小成本。例如在网络设计问题中,如果节点代表办公室,边的权重代表办公室之间电话线的成本,最小生成树就能提供连接所有办公室且总成本最低的电话线集合。
Loom通过Prim算法实现了最小生成树,可使用
prim-mst
函数:
(defn ex-8-10 []
(let [graph (->> (load-edges "twitter/396721965.edges")
(apply loom/weighted-graph))]
(-> (alg/prim-mst graph)
(lio/view))))
若将顶点28719244和163629705之间的边更新为权重100,可观察到对最小生成树的影响:
(defn ex-8-11 []
(let [graph (->> (load-edges "twitter/396721965.edges")
(apply loom/weighted-graph))]
(-> (loom/add-edges graph [28719244 163629705 100])
(alg/prim-mst)
(lio/view))))
此时树会重新配置以绕过成本最高的边。
2. 子图和连通分量
最小生成树仅适用于连通图,即所有节点至少通过一条路径相互连接。对于非连通图,显然无法构建最小生成树,但可以构建最小生成森林。
如果图包含一组内部连通但彼此不相连的子图,这些子图被称为连通分量。加载更复杂的网络可以观察连通分量:
(defn ex-8-12 []
(->> (load-edges "twitter/15053535.edges")
(apply loom/graph)
(lio/view)))
通过图的布局,我们能轻松看到有三个连通分量,Loom的
connected-components
函数可计算这些连通分量:
(defn ex-8-13 []
(->> (load-edges "twitter/15053535.edges")
(apply loom/graph)
(alg/connected-components)))
有向图若从每个节点到其他每个节点都有路径,则为强连通;若仅将所有边视为无向边时,从每个节点到其他每个节点都有路径,则为弱连通。
加载相同的图作为有向图,查看是否有强连通分量:
(defn ex-8-14 []
(->> (load-edges "twitter/15053535.edges")
(apply loom/digraph)
(lio/view)))
通过Kosaraju算法可计算有向图中的强连通分量数量,Loom将其实现为
alg/scc
函数:
(defn ex-8-15 []
(->> (load-edges "twitter/15053535.edges")
(apply loom/digraph)
(alg/scc)
(count)))
若按长度降序排序,第一个分量将是最大的强连通分量:
(defn ex-8-16 []
(->> (load-edges "twitter/15053535.edges")
(apply loom/digraph)
(alg/scc)
(sort-by count >)
(first)))
3. 强连通分量与网络的蝴蝶结结构
弱连通和强连通分量能为理解有向图的结构提供有价值的信息。例如,对互联网链接结构的研究表明,强连通分量可能会变得非常大。
曾有研究显示,网络中心有一个由5600万页组成的大强连通分量,这意味着在该强连通分量内的任何页面,都可通过出站超链接到达其他任何页面。此外,有4400万页链接到该强连通分量,但未从其链接回来;还有4400万页从该强连通分量链接出去,但未链接回来,只有极少数链接完全绕过该强连通分量。
4. 全图分析
接下来我们将注意力转向
twitter_combined.txt
文件提供的更大的关注者图,该图包含超过240万条边。
全图最简单的度量指标之一是其密度。对于有向图,密度定义为边的数量
|E|
除以顶点数量
|V|
乘以
|V| - 1
:
[D = \frac{|E|}{|V|(|V| - 1)}]
对于连通图,密度为1;对于非连通图,密度为0。Loom通过
alg/density
函数实现图密度计算:
(defn ex-8-17 []
(->> (load-edges "twitter_combined.txt")
(apply loom/digraph)
(alg/density)
(double)))
计算结果表明该图非常稀疏,因为密度为1意味着每个账户关注其他每个账户,这在社交网络中显然不现实。
为了查看边在节点间的分布情况,我们可以使用Loom的
out-degree
函数计算每个节点的出边数量,并绘制分布直方图:
(defn ex-8-18 []
(let [graph (->> (load-edges "twitter_combined.txt")
(apply loom/digraph))
out-degrees (map #(loom/out-degree graph %)
(loom/nodes graph))]
(-> (c/histogram out-degrees :nbins 50
:x-label "Twitter Out Degrees")
(i/view))))
出度分布类似于指数分布,大多数人的出度很少,但有少数人超过一千。
同样,我们可以绘制入度直方图,在Twitter中,入度对应账户的关注者数量:
(defn ex-8-19 []
(let [graph (->> (load-edges "twitter_combined.txt")
(apply loom/digraph))
in-degrees (map #(loom/in-degree graph %)
(loom/nodes graph))]
(-> (c/histogram in-degrees :nbins 50
:x-label "Twitter In Degrees")
(i/view))))
入度分布更加极端,尾部延伸得更远,第一个条形更高,这意味着大多数账户关注者很少,但有少数账户有数千个关注者。
对比随机生成的图,使用Loom的
gen-rand
函数生成一个有10000个节点和1000000条边的随机图:
(defn ex-8-20 []
(let [graph (generate/gen-rand (loom/graph) 10000 1000000)
out-degrees (map #(loom/out-degree graph %)
(loom/nodes graph))]
(-> (c/histogram out-degrees :nbins 50
:x-label "Random out degrees")
(i/view))))
随机图的出度分布近似正态分布,平均出度约为200,这表明Twitter图并非由随机过程生成。
5. 无标度网络
Twitter的度直方图呈现幂律度分布的特征。与正态分布的随机生成图不同,Twitter直方图显示少数顶点连接了大部分边。
“无标度网络”这一术语由圣母大学的研究人员在1999年提出,用于描述万维网上观察到的结构。在模拟人类交互的图中,经常会观察到连通性的幂律,也称为Zipf标度,它体现了“优先连接定律”,即受欢迎的顶点更有可能发展额外的连接。社交媒体网站就是这种过程的典型例子,新用户倾向于关注已经受欢迎的用户。
我们可以通过在对数 - 对数坐标轴上寻找直线来确定幂律关系:
(defn ex-8-21 []
(let [graph (->> (load-edges "twitter_combined.txt")
(apply loom/digraph))
out-degrees (map #(loom/out-degree graph %)
(loom/nodes graph))
points (frequencies out-degrees)]
(-> (c/scatter-plot (keys points) (vals points))
(c/set-axis :x (c/log-axis :label "log(out-degree)"))
(c/set-axis :y (c/log-axis :label "log(frequency)"))
(i/view))))
虽然并非完全线性,但该图足以表明Twitter图中存在幂律分布。无标度网络在可视化时,由于其特征性的“聚类”形状,受欢迎的顶点周围往往有一圈其他顶点。
然而,处理完整的Twitter组合数据集时,之前的示例运行速度会变得非常慢,尽管与许多社交网络相比,这个图规模很小。因此,接下来我们将介绍基于Spark框架的图库GraphX,它可以利用Spark的分布式计算模型处理更大的图。
6. 使用GraphX进行分布式图计算
GraphX(https://spark.apache.org/graphx/)是一个分布式图处理库,旨在与Spark配合使用。与之前使用的MLlib库类似,GraphX提供了一组基于Spark的RDD构建的抽象,通过将图的顶点和边表示为RDD,GraphX能够以可扩展的方式处理非常大的图。
在之前的章节中,我们了解了如何使用MapReduce和Hadoop处理大型数据集,Hadoop和Spark都是数据并行系统,将数据集分成多个组进行并行处理。但图具有复杂的内部结构,以表格形式表示图效率不高,尽管可以将图表示为边列表,但处理以这种方式存储的图可能涉及复杂的连接和集群周围过多的数据移动。
随着图数据规模和重要性的增长,催生了许多新的图并行系统。这些系统通过限制可表达的计算类型,并引入图分区和分布技术,能够比一般的数据并行系统更高效地执行复杂的图算法。一些将图并行计算引入Hadoop的库包括Hama(https://hama.apache.org/)和Giraph(http://giraph.apache.org/)。
GraphX将图并行计算引入Spark,使用Spark作为图处理引擎的一个优点是其内存计算模型非常适合许多图算法的迭代性质。GraphX通过引入弹性分布式图(Resilient Distributed Graph,RDG)扩展了Spark的RDD抽象,并提供了一组函数以在结构感知的方式下查询和转换图。
7. 使用Glittering创建RDG
Spark和GraphX主要用Scala编写,我们将使用Clojure库Glittering(https://github.com/henrygarner/glittering)与GraphX交互,它为GraphX提供了一个薄的Clojure包装器。
创建图有两种方式:一是提供两个RDD表示(一个包含边,另一个包含顶点);二是仅提供边的RDD,此时需要为每个节点提供默认值。
以下是使用Glittering的图构造函数创建一个仅包含三条边的小图的示例:
(defn ex-8-22 []
(spark/with-context sc (-> (g/conf)
(conf/master "local")
(conf/app-name "ch8"))
(let [vertices [[1 "A"] [2 "B"] [3 "C"]]
edges [(g/edge 1 2 0.5)
(g/edge 2 1 0.5)
(g/edge 3 1 1.0)]]
(g/graph (spark/parallelize sc vertices)
(spark/parallelize sc edges)))))
另一种构造图的方式是使用
g/graph-from-edges
构造函数,它仅基于边的RDD返回一个图。Twitter数据以边列表格式提供,因此我们可以使用此函数加载数据:
(defn line->edge [line]
(let [[from to] (map to-long (str/split line #" "))]
(g/edge from to 1.0)))
(defn load-edgelist [sc path]
(let [edges (->> (spark/text-file sc path)
(spark/map line->edge))]
(g/graph-from-edges edges 1.0)))
(defn ex-8-23 []
(spark/with-context sc (-> (g/conf)
(conf/master "local")
(conf/app-name "ch8"))
(load-edgelist sc "data/twitter_combined.txt")))
graph-from-edges
函数的第二个参数是用作每个顶点属性的默认值,因为边列表中无法提供顶点属性。
8. 使用三角形计数测量图密度
GraphX自带了一些内置的图算法,Glittering在
glittering.algorithms
命名空间中提供了这些算法。在详细介绍Glittering的API之前,我们将在Twitter关注图上运行其中一个算法——三角形计数。
三角形计数算法用于测量每个节点附近的图密度,它在原理上类似于计算度数,但还考虑了邻居之间的连接情况。在社交网络分析中,三角形计数可以衡量朋友的朋友之间相互认识的程度,在紧密联系的社区中,三角形的数量应该较高。
GraphX实现了三角形计数算法,可通过
glittering.algorithms
命名空间中的
triangle-count
函数访问。在使用此算法之前,GraphX要求我们完成两件事:
1. 将边指向“规范”方向。
2. 确保图被分区。
这两个步骤是GraphX实现三角形计数算法的必要步骤,因为GraphX允许两个顶点之间存在多条边,而三角形计数只计算不同的边。这两个步骤确保GraphX在执行算法之前能够有效地计算不同的边。
边的规范方向是从较小的节点ID指向较大的节点ID,我们可以在构造边的RDD时确保所有边都按此方向创建:
(defn line->canonical-edge [line]
(let [[from to] (sort (map to-long (str/split line #" ")))]
(glitter/edge from to 1.0)))
(defn load-canonical-edgelist [sc path]
(let [edges (->> (spark/text-file sc path)
(spark/map line->canonical-edge))]
(glitter/graph-from-edges edges 1.0)))
通过在创建边之前对
from
和
to
ID进行排序,我们确保
from
ID始终小于
to
ID,这是提高重复边删除效率的第一步,第二步是为图选择分区策略。
网络分析:从基础图算法到分布式图计算
9. 图分区策略
为了让图处理更加高效,需要为图选择合适的分区策略。分区的目的是将图的顶点和边合理地分布在集群的不同节点上,减少数据的移动和重复计算。以下是几种常见的图分区策略:
| 分区策略 | 描述 |
|---|---|
| 随机分区(Random Partitioning) | 随机地将边分配到不同的分区中。这种策略简单,但可能导致数据分布不均匀,某些节点的负载过重。 |
| 哈希分区(Hash Partitioning) | 根据边的源顶点或目标顶点的哈希值将边分配到不同的分区。哈希分区可以使数据更均匀地分布,但可能会破坏图的局部性。 |
| 范围分区(Range Partitioning) | 根据顶点的ID范围将边分配到不同的分区。范围分区可以保留图的局部性,但需要对顶点ID进行排序,可能会增加预处理的开销。 |
在GraphX中,可以通过相应的函数来指定分区策略。例如,使用
repartition
函数可以对图进行重新分区:
(defn repartition-graph [graph]
(g/repartition graph :partition-strategy :random))
上述代码将图按照随机分区策略进行重新分区。
10. 三角形计数的完整流程
结合前面提到的将边指向规范方向和图分区的步骤,我们可以总结出使用三角形计数算法测量图密度的完整流程:
graph LR
A[加载边列表文件] --> B[将边转换为规范方向]
B --> C[创建图]
C --> D[对图进行分区]
D --> E[执行三角形计数算法]
E --> F[获取每个节点的三角形计数结果]
以下是实现该流程的代码示例:
(defn line->canonical-edge [line]
(let [[from to] (sort (map to-long (str/split line #" ")))]
(glitter/edge from to 1.0)))
(defn load-canonical-edgelist [sc path]
(let [edges (->> (spark/text-file sc path)
(spark/map line->canonical-edge))]
(glitter/graph-from-edges edges 1.0)))
(defn repartition-graph [graph]
(g/repartition graph :partition-strategy :random))
(defn triangle-count-analysis [sc path]
(let [graph (load-canonical-edgelist sc path)
partitioned-graph (repartition-graph graph)
triangle-counts (alg/triangle-count partitioned-graph)]
triangle-counts))
通过调用
triangle-count-analysis
函数,传入Spark上下文和边列表文件的路径,就可以得到每个节点的三角形计数结果。
11. GraphX其他内置算法
除了三角形计数算法,GraphX还提供了许多其他有用的内置算法,这些算法可以帮助我们更好地分析图的结构和性质。以下是一些常见的算法:
| 算法名称 | 描述 |
|---|---|
| PageRank | 用于计算图中每个顶点的重要性得分,常用于网页排名等场景。 |
| Connected Components | 计算图中的连通分量,即相互连接的顶点集合。 |
| Strongly Connected Components | 计算有向图中的强连通分量。 |
以下是使用PageRank算法的代码示例:
(defn pagerank-analysis [sc path]
(let [graph (load-canonical-edgelist sc path)
partitioned-graph (repartition-graph graph)
pagerank-result (alg/pagerank partitioned-graph 0.15)]
pagerank-result))
上述代码使用PageRank算法计算图中每个顶点的重要性得分,其中
0.15
是随机跳转的概率。
12. 图数据的可视化
在分析图数据时,可视化可以帮助我们更直观地理解图的结构和性质。可以使用一些可视化工具,如Graphviz、D3.js等,将图数据转换为图形。
以下是一个使用Graphviz进行图可视化的简单示例:
(defn visualize-graph [graph]
(let [edges (g/edges graph)
dot-str (reduce (fn [acc edge]
(let [from (g/src-id edge)
to (g/dst-id edge)]
(str acc from " -> " to ";")))
"digraph G {" edges)
dot-str (str dot-str "}")]
(spit "graph.dot" dot-str)
(sh "dot" "-Tpng" "graph.dot" "-o" "graph.png")))
上述代码将图的边信息转换为Graphviz的DOT格式,并生成PNG图像。
13. 总结
本文介绍了从基础图算法到分布式图计算的相关知识。首先,我们学习了最小生成树、子图和连通分量等基础图算法,这些算法可以帮助我们分析图的局部结构。然后,我们探讨了全图分析的指标,如密度、出度和入度分布等,以及无标度网络的特征。
接着,我们引入了分布式图处理库GraphX,它基于Spark的RDD抽象,能够以可扩展的方式处理大规模图数据。我们学习了如何使用Glittering与GraphX交互,创建图、加载数据,并使用三角形计数算法测量图密度。最后,我们介绍了图分区策略、GraphX的其他内置算法以及图数据的可视化方法。
通过这些知识和技术,我们可以更好地处理和分析大规模的图数据,挖掘图中隐藏的信息和模式,为社交网络分析、推荐系统、生物信息学等领域的应用提供支持。
在未来的研究和应用中,随着图数据规模的不断增大和图算法的不断发展,分布式图计算将变得越来越重要。我们可以进一步探索更高效的图分区策略、更复杂的图算法,以及如何将图计算与其他数据处理技术相结合,以满足不同领域的需求。
超级会员免费看

被折叠的 条评论
为什么被折叠?



