Rust机器学习之petgraph

news2024/11/8 18:36:53

Rust机器学习之petgraph

图作为一种重要的数据结构和表示工具在科学和技术中无处不在。因此,许多软件项目会以各种形式用到图。尤其在知识图谱和因果AI领域,图是最基础的表达和研究工具。Python有著名的NetworksX库,便于用户对复杂网络进行创建、操作和学习。Rust有对应的petgraph库——一个用 Rust 开发的通用图形库。本文将用简短易懂的代码向大家介绍petgraph的主要功能特性。

本文是“Rust替代Python进行机器学习”系列文章的第六篇,其他教程请参考下面表格目录:

Python库Rust替代方案教程
numpyndarrayRust机器学习之ndarray
pandasPolarsRust机器学习之Polars
scikit-learnLinfaRust机器学习之Linfa
matplotlibplottersRust机器学习之plotters
pytorchtch-rsRust机器学习之tch-rs
networkspetgraphRust机器学习之petgraph

数据和算法工程师偏爱Jupyter,为了跟Python保持一致的工作环境,文章中的示例都运行在Jupyter上。因此需要各位搭建Rust交互式编程环境(让Rust作为Jupyter的内核运行在Jupyter上),相关教程请参考 Rust交互式编程环境搭建

在这里插入图片描述

文章目录

    • 关于petgraph
    • 安装petgraph
    • 基本用法
    • 图的构造
      • 变量
      • 列表
      • 环形缓冲器
      • 字典
    • 图的实现
      • `Graph`
      • `StableGraph`
      • `GraphMap`
      • `Csr`
    • 图的遍历
      • 广度优先
      • 深度优先
      • 后序深度优先
    • 图算法
      • 寻路算法
      • 其他算法
    • 结论

关于petgraph

petgraph是Rust中最受欢迎的图算法库,它有着不输NetworkX的功能,但性能远超纯Python实现的NetworkX,因此很多项目都依赖petgraph库。

petgraph包含三个主要部分:

  • 数据结构:包含4个不同的图实现,他们各有侧重,有的专注功能,有的专注性能,开发者可以根据实际需要权衡选择;
  • 遍历:支持深度优先和广度优先遍历算法;
  • 图算法:包括常见的图算法,如寻路算法。

安装petgraph

安装petgraph很简单,如果是标准rust项目,只需要在项目目录下运行:

cargo add petgraph

或在Cargo.toml文件中加入

petgraph = "0.6.2"

在机器学习中,我们更喜欢使用Jupyter。如果你已经搭建好Rust交互式编程环境(可以参考 《Rust交互式编程环境搭建》),可以直接通过下面代码引入petgraph :

:dep petgraph = "0.6.2"
:dep petgraph-evcxr = "*"

extern crate petgraph;
use petgraph_evcxr::draw_graph;

这里我们需要用到petgraph-evcxrdraw_graph方法让petgraph在Jupyter环境中绘制图。

基本用法

我们先来手动创建一个图。

在petgraph中手动建图很容易,首先初始化Graph结构,然后向其添加节点和边,例如:

use petgraph::graph::Graph;

let mut g : Graph<&str, &str> = Graph::new();
let a = g.add_node("Node A");
let b = g.add_node("Node B");
g.add_edge(a, b, "edge A->B");
draw_graph(&g);

输出结果如下图:

在这里插入图片描述

⚠注意:petgraph_evcxr::draw_graph不支持显示中文字符,因此图中所有显示文本都用英文。

petgraph有多种内存实现(详见图的实现),上面的例子用的是Graph结构。Graph又分有向图和无向图。我们用Graph::new()创建有向图,用Graph::new_undirected()创建无向图。

⚠注意:有向图和无向图的内存结构中是一样,都是有向图,区别在于某些算法行为会不同。

你可能留意到,Graph是支持泛型的,上面例子中我们给定Graph的两个参数为&str&str类型。第一个参数表示节点的权重,第二个参数表示边的权重。这里权重在不同场景下含义不同,上面例子可以简单地将权重理解为标签,在寻路算法中权重表示路径的cost(详见图算法)。权重的数据类型完全可以灵活地自定义。

图的构造

图是一种通用数据结构,绝大多数常见数据结构都可以表示为图的形式。下面列举一些常见数据结构如何表示成图:

变量

理论上,变量可以表示为单例图(只有一个节点的图)。

let mut singleton : Graph<&str, &str, petgraph::Undirected> = Graph::new_undirected();
let singleton_node = singleton.add_node("Single Node");
draw_graph(&singleton);

在这里插入图片描述

列表

let mut list : Graph<&str, &str, petgraph::Undirected> = Graph::new_undirected();
let item1 = list.add_node("a");
let item2 = list.add_node("b");
let item3 = list.add_node("c");
list.add_edge(item1, item2, "");
list.add_edge(item2, item3, "");
draw_graph(&list);

在这里插入图片描述

let mut table : Graph<&str, &str, petgraph::Undirected> = Graph::new_undirected();
let cellA1 = table.add_node("A1");
let cellA2 = table.add_node("A2");
let cellA3 = table.add_node("A3");

let cellB1 = table.add_node("B1");
let cellB2 = table.add_node("B2");
let cellB3 = table.add_node("B3");

let cellC1 = table.add_node("C1");
let cellC2 = table.add_node("C2");
let cellC3 = table.add_node("C3");

// 列
table.add_edge(cellA1, cellA2, "");
table.add_edge(cellA2, cellA3, "");

table.add_edge(cellB1, cellB2, "");
table.add_edge(cellB2, cellB3, "");

table.add_edge(cellC1, cellC2, "");
table.add_edge(cellC2, cellC3, "");

// 行
table.add_edge(cellA1, cellB1, "");
table.add_edge(cellB1, cellC1, "");

table.add_edge(cellA2, cellB2, "");
table.add_edge(cellB2, cellC2, "");

table.add_edge(cellA3, cellB3, "");
table.add_edge(cellB3, cellC3, "");

draw_graph(&table);

在这里插入图片描述

let mut tree : Graph<&str, &str, petgraph::Directed> = Graph::new();
let tree_item1 = tree.add_node("a");
let tree_item2 = tree.add_node("b");
let tree_item3 = tree.add_node("c");
let tree_item4 = tree.add_node("d");
let tree_item5 = tree.add_node("e");
tree.add_edge(tree_item1, tree_item2, "");
tree.add_edge(tree_item1, tree_item3, "");
tree.add_edge(tree_item2, tree_item4, "");
tree.add_edge(tree_item2, tree_item5, "");
draw_graph(&tree);

在这里插入图片描述

环形缓冲器

let mut ring : Graph<&str, &str> = Graph::new();
let ring_item1 = ring.add_node("a");
let ring_item2 = ring.add_node("b");
let ring_item3 = ring.add_node("c");
let ring_item4 = ring.add_node("d");
ring.add_edge(ring_item1, ring_item2, "");
ring.add_edge(ring_item2, ring_item3, "");
ring.add_edge(ring_item3, ring_item4, "");
ring.add_edge(ring_item4, ring_item1, "");
draw_graph(&ring);

在这里插入图片描述

字典

let mut dict : Graph<&str, &str> = Graph::new();
let core = dict.add_node("dict");

let key1 = dict.add_node("key 1");
let key2 = dict.add_node("key 2");
let key3 = dict.add_node("key 3");

let value1 = dict.add_node("value 1");
let value2 = dict.add_node("value 2");
let value3 = dict.add_node("value 3");

dict.add_edge(core, key1, "");
dict.add_edge(core, key2, "");
dict.add_edge(core, key3, "");

dict.add_edge(key1, value1, "");
dict.add_edge(key2, value2, "");
dict.add_edge(key3, value3, "");
draw_graph(&dict);

在这里插入图片描述

图的实现

petgraph中的图有4种实现方式,它们的主要区别在于图在内存中的数据结构不同。根据场景不同,不同的实现可能带来不同的时间和空间性能。我们有必要了解这4种实现的区别,从而在使用时根据场景选择恰当的实现。

Graph

Graph背后的内存模型是邻接表,其对节点和边的类型无任何限制。节点和边分别通过 NodeIndexEdgeIndex 的值访问。这些值可以访问背后的数值索引,这些索引在删除操作下不稳定。另外,Graph支持最多petgraph图算法。

StableGraph

Graph类似,StableGraph背后的内存模型也是邻接表;但与Graph不同的是,从 StableGraph 中删除节点或边不会使现有索引失效。

GraphMap

GraphMap背后的内存模型是Map。其用节点作为Key,因此节点必须实现Copy, Eq, Ord, 和Hash。与其他三种实现不同,GraphMap 可以直接操作节点和边标签,无需中间操作。

Csr

CsrCompressed Sparse Row(压缩稀疏行或稀疏矩阵)的简称,它是表示稀疏矩阵数据(大部分图都是稀疏数据)的有效方法,通过快速边查询可以降低内存消耗。虽然Csr对节点或边类型没有限制,但是,Csr 的 API 是所有图中限制最多的。

图的遍历

petgraph支持三种遍历形式:

  • 广度优先
  • 深度优先
  • 后序深度优先

每种形式都实现为迭代器,且都考虑了边的方向性。

广度优先

广度优先需要用到petgraph::visit::Bfs,广度优先遍历实现方式如下:

use petgraph::visit::Bfs;

let mut bfs_graph = Graph::<(),(), petgraph::Undirected>::new_undirected();

//      0
//     /|\
//    1 2 3
//        |
//        4
bfs_graph.extend_with_edges(&[
    (0, 1), (0, 2), (0, 3), (3, 4)
]);


for start in bfs_graph.node_indices() {
    let mut bfs = Bfs::new(&bfs_graph, start);

    print!("[{}] ", start.index());

    while let Some(visited) = bfs.next(&bfs_graph) {
        print!(" {}", visited.index());
    }

    println!();
};

上面代码会输出广度优先遍历顺序:

[0]  0 3 2 1 4
[1]  1 0 3 2 4
[2]  2 0 3 1 4
[3]  3 4 0 2 1
[4]  4 3 0 2 1 

深度优先

深度优先需要用到petgraph::visit::Dfs,用法跟广度优先一样,只需要将Bfs替换成Dfs即可。

use petgraph::visit::Dfs;

let mut dfs_graph = Graph::<(),(), petgraph::Undirected>::new_undirected();

//      0
//     /|\
//    1 2 3
//        |
//        4
dfs_graph.extend_with_edges(&[
    (0, 1), (0, 2), (0, 3), (3, 4)
]);


for start in dfs_graph.node_indices() {
    let mut dfs = Dfs::new(&dfs_graph, start);

    print!("[{}] ", start.index());

    while let Some(visited) = dfs.next(&dfs_graph) {
        print!(" {}", visited.index());
    }

    println!();
};

上面代码会输出深度优先遍历顺序:

[0]  0 1 2 3 4
[1]  1 0 2 3 4
[2]  2 0 1 3 4
[3]  3 0 1 2 4
[4]  4 3 0 1 2

后序深度优先

有时我们可能需要先迭代节点的邻居,然后是节点本身。petgraph为这种遍历顺序提供了DfsPostOrder。使用方式与深度遍历相同:

use petgraph::visit::DfsPostOrder;

let mut dfs_graph = Graph::<(),(), petgraph::Undirected>::new_undirected();

//      0
//     /|\
//    1 2 3
//        |
//        4
dfs_graph.extend_with_edges(&[
    (0, 1), (0, 2), (0, 3), (3, 4)
]);


for start in dfs_graph.node_indices() {
    let mut dfs = DfsPostOrder::new(&dfs_graph, start);

    print!("[{}] ", start.index());

    while let Some(visited) = dfs.next(&dfs_graph) {
        print!(" {}", visited.index());
    }

    println!();
};

上面代码会输出后序深度优先遍历顺序:

[0]  1 2 4 3 0
[1]  2 4 3 0 1
[2]  1 4 3 0 2
[3]  1 2 0 4 3
[4]  1 2 0 3 4

图算法

petgraph支持许多图算法,这些算法都封装在algo包中。

寻路算法

在所有图算法中,日常用的最多的是寻路算法,比如被学校广泛教授的优雅的Dijkstra算法。下面列举一些需要用到寻路算法的场景:

  • 计算机网络中两个节点之间边权重为ping时长
  • 计算机网络中两个节点之间边权重为数据传输成本
  • 计算机网络中两个节点之间边权重为ping时长和数据传输成本之和
  • 地图上两个地点间的权重为两点间的距离
  • 交易市场中边权重为两样商品或证券的兑换成本

上述列举的场景都可以通过 petgraph 建模,然而,petgraph 可能不适合建模地理空间图,我们需要使用专门的程序库或数据库来完成地理空间建模,例如Postgres,Postgres 可以使用 postgis 插件存储地理空间数据,并使用 pgRouting 查找最短路径。专用的地理空间路由引擎能够使用空间启发式算法(例如方向)来显著加速路径查找。

petgraph内置了3个寻路算法:

  • Dijkstra算法:经典寻路算法
  • A*算法:向Dijkstra算法中加入启发式算法
  • Bellman_Ford算法:让Dijkstra算法支持负权重

这里拿最经典的Dijkstra算法给大家演示petgraph如何寻找最短路径:

use petgraph::algo;

let mut graph = Graph::<(),()>::new();

//      0
//     /|\
//    1 2 3
//        |
//        4
graph.extend_with_edges(&[
    (0, 1), (0, 2), (0, 3), (3, 4)
]);

for start in graph.node_indices() {
    println!("--- {:?} ---", start.index());
    println!("{:?}", algo::dijkstra(&graph, start, None, |_| 1));
};

上面的代码以图上每一点作为起点,分别调用algo::dijkstra方法计算所能到达最终节点的最短路径。algo::dijkstra的接口定义如下:

pub fn dijkstra<G, F, K>(
    graph: G, 
    start: G::NodeId, 
    goal: Option<G::NodeId>, 
    edge_cost: F
) -> HashMap<G::NodeId, K> where
    G: IntoEdges + Visitable,
    G::NodeId: Eq + Hash,
    F: FnMut(G::EdgeRef) -> K,
    K: Measure + Copy,

其中start代表起始节点;edge_cost是一个函数,返回节点间路径的cost,用于计算路径消耗,cost一定为非负数,在上面代码中我们恒定返回1;goal是个可选值,如果goal不为空,则一旦计算出目标节点的成本,算法就会终止。

注意,这里的返回值不是一条路径,而是一个Map,它包含了起始节点到每一个可达节点的最短路径成本。

上面的代码会输出如下信息:

--- 0 ---
{NodeIndex(1): 1, NodeIndex(3): 1, NodeIndex(2): 1, NodeIndex(4): 2, NodeIndex(0): 0}
--- 1 ---
{NodeIndex(1): 0}
--- 2 ---
{NodeIndex(2): 0}
--- 3 ---
{NodeIndex(3): 0, NodeIndex(4): 1}
--- 4 ---
{NodeIndex(4): 0}

0节点可以到达{0,1,2,3,4}节点,到达每个节点的最短路径消耗分别为{0, 1, 1, 1, 2};

1节点只能到达{1}节点,最短路径消耗为0;

2节点只能到达{2}节点,最短路径消耗为0;

3节点可以到达{3,4}节点,到达每个节点的最短路径消耗分别为{0, 1};

4节点只能到达{4}节点,最短路径消耗为0;

其他算法

除了寻路算法,petgraph还有很多其他算法,这里简单列举一些常用的算法:

  • all_simple_paths: 返回给定节点上所有路径的迭代器
  • condensation: 将每个强连接节点归并到一个节点
  • connected_components:返回连通节点数
  • has_path_connecting:如果两个节点间存在连通路径则返回True
  • is_cyclic_directed:如果图中包含至少一个有向环则返回True
  • is_cyclic_undirected: 如果图中包含至少一个环则返回
  • kosaraju_scc:用 Kosaraju 算法返回强连通分量的向量
  • min_spanning_tree:以树的形式返回每一个连通部分
  • tarjan_scc:用 Tarjan 算法返回强连通分量向量
  • toposort:返回按拓扑顺序排列的节点向量

结论

petgraph几乎涵盖了日常项目中所需的大部分功能,4种图实现能够满足绝大部分场景需求,并且使用Rust迭代器进行遍历带来了极大的灵活性和强大的功能,Rust的性能优势也非常凸显。但不幸的是,4种图实现缺乏通用接口,并且某些算法与某些图实现不兼容。如果所需的算法petgraph有实现,那么用petgraph是一个不错的选择。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/185476.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

apt命令详解

apt&#xff08;Advanced Packaging Tool&#xff09;是一个在 Debian 和 Ubuntu 中的 Shell 前端软件包管理器。 apt 命令提供了查找、安装、升级、删除某一个、一组甚至全部软件包的命令&#xff0c;而且命令简洁而又好记。 apt 命令执行需要超级管理员权限(root)。前些日子…

基于java ssm springboot宠物用品商城系统

基于java ssm springboot宠物用品商城系统 博主介绍&#xff1a;5年java开发经验&#xff0c;专注Java开发、定制、远程、文档编写指导等,csdn特邀作者、专注于Java技术领域 作者主页 超级帅帅吴 Java毕设项目精品实战案例《500套》 欢迎点赞 收藏 ⭐留言 文末获取源码联系方式…

Python 基础语法介绍(一)

文章目录一、概述二、变量1&#xff09;变量定义2&#xff09;定义变量的规则3&#xff09;变量命名规范4&#xff09;变量类型转换三、注释1&#xff09;单行注释2&#xff09;多行注释1、单引号&#xff08;&#xff09;注释2、双引号&#xff08;"""&#xf…

Kubernetes 体验 kubecolor

Kubernetes 体验 kubecolorkubecolor 概述Github 地址安装 kubecolor设置.bashrc使用 kubecolorkubecolor 概述 对你的kubectl输出进行着色。 kubecolor在内部调用kubectl命令并尝试对输出进行着色&#xff0c;因此你可以将kubecolor作为kubectl的一个完整的替代品。这意味着…

JAVA经典面试题带答案(一)

目录 1、JDK 和 JRE 有什么区别&#xff1f; 2、 和 equals 的区别是什么&#xff1f; 3、final 在 java 中有什么作用&#xff1f; 4、java 中的 Math.round(-1.5) 等于多少&#xff1f; 5、String 属于基础的数据类型吗&#xff1f; 不属于。 6、String str"i&quo…

51单片机学习笔记-13直流电机

13 直流电机 [toc] 注&#xff1a;笔记主要参考B站江科大自化协教学视频“51单片机入门教程-2020版 程序全程纯手打 从零开始入门”。 注&#xff1a;工程及代码文件放在了本人的Github仓库。 13.1 直流电机与PWM波 13.1.1 直流电机 直流电机是一种将电能转换为机械能的装置…

Docker -- 部署Mysql主从服务

以下是配置一主两从的Mysql服务的具体流程。 文章目录创建用于挂载的目录修改cnf配置拉取mysql服务镜像自定义docker网络启动容器主库配置查看主库状态创建从库备份用户从库配置修改Master信息启动slave服务查看slave服务状态是否正常创建用于挂载的目录 保证数据的持久化&…

Databend 内幕大揭秘第二弹 - Data Source

本篇是 minibend 系列的第二期&#xff0c;将会介绍 Data Source 部分的设计与实现&#xff0c;当然&#xff0c;由于是刚开始涉及到编程的部分&#xff0c;也会提到包括 类型系统 和 错误处理 之类的一些额外内容。 前排指路视频和 PPT 地址 视频&#xff08;哔哩哔哩&#xf…

23种设计模式之趣味学习篇

23种设计模式之趣味学习篇1. 设计模式概述1.1 什么是设计模式1.2 设计模式的好处2. 设计原则分类3. 详解3.1 单一职责原则3.2 开闭原则3.3 里氏代换原则3.4 依赖倒转原则3.5 接口隔离原则3.6 合成复用原则3.7 迪米特法则4. Awakening1. 设计模式概述 我们的软件开发技术也包括一…

【1669. 合并两个链表】

来源&#xff1a;力扣&#xff08;LeetCode&#xff09; 描述&#xff1a; 给你两个链表 list1 和 list2 &#xff0c;它们包含的元素分别为 n 个和 m 个。 请你将 list1 中下标从 a 到 b 的全部节点都删除&#xff0c;并将list2 接在被删除节点的位置。 下图中蓝色边和节点…

【算法竞赛学习】csoj:寒假第二场

文章目录前言红包接龙最后一班勇者兔兔兔爱消除吃席兔知识拓展std::greater | 堆优化参考iota函数参考并查集参考sort自定义函数参考树形dp参考使用auto时控制分隔符前言 由于本人菜鸡&#xff0c;所以大多都是使用出题人的代码和思路 如有侵权&#xff0c;麻烦联系up删帖&…

pytorch_sparse教程

pytorch_sparse教程 Coalesce torch_sparse.coalesce(index, value, m, n, op"add") -> (torch.LongTensor, torch.Tensor) 逐行排序index并删除重复项。通过将重复项映射到一起来删除重复项。对于映射&#xff0c;可以使用任何一种torch_scatter操作。 参数 i…

来回修改的投标文件怎么做版本管理?1个工具搞定!

投标是公司市场活动中非常重要的事情&#xff0c;每次投标文件的编写像打仗一样&#xff0c;要修改很多次&#xff0c;不保存每个版本就只能在需要的时候后悔&#xff0c;多个文件、多人编写、多种方案要再最后的几个小时才能定&#xff0c;每次都是弄得鸡飞狗跳的&#xff0c;…

Python卷积神经网络CNN

Python卷积神经网络CNN 提示&#xff1a;前言 Python卷积神经网络CNN 提示&#xff1a;写完文章后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录Python卷积神经网络CNN前言一、导入包二、介绍三、卷积过滤四、权重五、展示特征图六、用 ReLU…

一文快速入门哈希表

目录一、基本概念1.1 哈希冲突二、整数哈希2.1 哈希函数的设计2.2 解决哈希冲突2.2.1 开放寻址法2.2.2 拉链法三、字符串哈希3.1 应用&#xff1a;重复的DNA序列References一、基本概念 哈希表又称散列表&#xff0c;一种以「key-value」形式存储数据的数据结构。所谓以「key-…

RA4M2开发(1)----使用串口进行打印

为什么使用Cube进行FreeRTOS配置 本篇文章主要介绍如何使用e2studio对瑞萨RA4M2开发板进行串口打印配置。 硬件准备 首先需要准备一个开发板&#xff0c;这里我准备的是芯片型号R7FAM2AD3CFP的开发板&#xff1a; 新建工程 工程模板 保存工程路径 芯片配置 本文中使用R7F…

【GlobalMapper精品教程】043:图片自动矢量化

本文讲解Globalmapper自动矢量化教程,配套案例数据。 参考教程:ArcGIS实验教程——实验三十三:ArcScan自动矢量化完整案例教程 文章目录 一、加载实验数据二、启动矢量化工具三、矢量化栅格四、矢量化结果五、注意事项一、加载实验数据 打开配套实验数据包中的data043.rar…

参数检验与非参数检验

综述 假设检验 参数检验 T检验 T检验是通过比较不同数据的均值&#xff0c;研究两组数据之间是否存在显著差异。 单总体检验&#xff1a;单总体t检验是检验一个样本平均数与一个已知的总体平均数的差异是否显著。当总体分布是正态分布&#xff0c;如总体标准差未知且样本容量小…

算法——垃圾回收算法——标记清除

标记清除简介算法过程1.标记阶段2.清除阶段3.缺点3.1内存碎片化简介 标记清除算法简介。 文章中使用的动画网站地址&#xff1a; 限 pc: 标记清除动画 &#xff1a;http://www.donghuasuanfa.com/platform/portal?pcmark-sweep 算法一览表&#xff1a;https://blog.csdn.net…

23种设计模式之面向对象的设计原则

23种设计模式之面向对象的设计原则1. 设计模式概述1.1 什么是设计模式1.2 设计模式的好处2. 设计原则分类3. 详解3.1 单一职责原则3.2 开闭原则3.3 里氏代换原则3.4 依赖倒转原则3.5 接口隔离原则3.6 合成复用原则3.7 迪米特法则4. Awakening1. 设计模式概述 我们的软件开发技术…