【GNN】第二章:图论、及其工具NetworkX
离散数学的一个重要分支叫图论,在实际应用中图论也是运筹学的一个经典而重要的分支,是交通运输、经济管理和工业工程等领域研究的基础工具。所以我们需要先了解一下前人对图数据的研究成果:图论,为后面学习打一个基础认知。
一、图的定义
1、定义1——图中的节点和边的表示
图(Graph)是一种表示对象之间关系的基本结构。具体的,我们用G=(V, E) 来表示图,其中V表示节点Nodes(也叫‘实体entities’) 的集合、E表示边Edges(也叫‘关系’relations) 的集合:
(1)在深度学习中,节点和边本身是要embedding成一个特征向量的。这是自然喽,如果不是特征向量,神经网络怎么计算嘛。所以,一个图有三部分特征:
一是,点特征。点是由特征向量来表示的。
二是,边特征。边也是用特征向量来表示的。比如如果要预测两个点之间的关系(朋友关系、亲人关系、同事关系)就是这两个点的边的特征。所以边也是一个向量。
三是,图特征。就是上图的U。图是一个全局的概念。图是由所有的点和所有的边组成的。所以比如所有的点的均值就可以表示图的特征。所有的边的特征的均值就也是图的特征。
(2)一般情况下,节点和边还要有自己的标签:
节点和边的信息可以是类别型的(categorical),类别型数据的取值只能是哪一类别。一般称类别型的信息为标签(label)。
节点和边的信息可以是数值型的(numeric),数值型数据的取值范围为实数。一般称数值型的信息为属性(attribute)。
为什么要有标签?有标签就可以分类或回归了呀。所以,图神经网络是可以完成不同的任务:
比如有的任务是求点的(比如对点进行分类、回归等任务),有的是求边的(比如对边进行分类、回归等任务),有的还是求全局的就是Graph级别的任务(比如设计分子结构等任务)。
为什么“一般情况下”有标签?意思就是可以只有部分标签,就是可以仅仅是一些重要的节点和重要的边才有标签,就是图神经网络可以进行半监督学习的,不是必须所有的节点和边都需要标签的。但是当然还是标签越多越好嘛。也所以后面讲数据集的时候,你会碰到mask,mask就是在求loss时mask住那些没有标签的节点。
但是无论数据多么随意,我们使用图神经网络的目的就是整合特征,或者说是重构特征。为什么要重构特征?因为每个节点都不是孤立的,它都是和一些别的节点是有联系的,别的节点也是和别别的节点有联系的。所以一个图中的某个节点的特征变化是要受到图中其他所有节点的影响的。我们的图神经网络就是拟合(或者说‘捕获’)节点之间的相互影响关系,从而重构出最合适的特征向量来表示这个节点。
2、定义2——图的拓扑结构:邻接矩阵(Adjacency Matrix)、邻接表(Adjacency List)
一个图数据,所有的节点特征、节点标签、边特征、边标签就可以表示一个图数据了嘛?显然是不够的!图的拓扑结构也是有决定作用的:
上面的两个图,它们同样都是有俩黄、俩蓝、俩绿,共6个节点,因此它们的节点信息相同;假如边只表示一种关系,那么它们的边信息也相同。但这两个图显然是不一样的图,因为它们的拓扑结构不一样!所以图的拓扑结构同样也包含了这个图的重要信息,这些拓扑信息后面还要辅助深度学习网络计算节点表征的,就是上面提到的是要辅助“重构特征”的。所以我们还得把图的拓扑结构也用数字表示出来。
那如何表示一个图的拓扑结构呢?其实有很多种表示方法,最常见的是用邻接矩阵或者邻接表来表示。
表示图的拓扑结构就是用规则数据表示图中哪两个节点之间有边。就是用矩阵或者列表的形式告诉神经网络节点之间的关系,方便神经网络学习这些节点之间的影响规律。
最常见的邻接矩阵是NxN矩阵的形式,有时是coordinate format(COO Format)形式。这两种形式都叫邻接矩阵。
最常见的邻接表是以 (source, target)对儿 的形式存储的,是一种顺序分配和链式分配相结合的存储结构。邻接表也有2种表示形式。
下面用图直观展示一下邻接矩阵和邻接表:
为什么如此五花八门?因为我们不是手动计算啊,都是计算机帮我们计算,数据结构就得符合计算机底层的计算原理。所以每种表示形式都无好坏优劣之分,只有合适不合适之分。
(1)上图是一个无向图(Undirected Graph),就是节点v1到节点v2的边存在,就意味着从节点v2到v1的边也是存在的。所以无向图的NxN邻接矩阵是对称的,有向图(Directed Graph)的邻接矩阵是不对称的。
(2)上图还是一个无权图,就是各条边的权重被认为是等价的,即认为各条边的权重都是1。所以如果是一个有权图,那它对应的NxN邻接矩阵Aij=wij,就是从节点vi到vj的边的权重,若边不存在时,边的权重为0。
(3)邻接矩阵(邻接表)作为图数据的重要信息,是要作为输入一并传入GNN模型的,GNN在计算节点和边的特征时,邻接矩阵(邻接表)是要参与计算的,具体怎么参与计算,后面讲图深度学习网络架构原理时会详细展开讲。
二、图的基本属性
1、节点的度(Degree)
节点的度就是指一个节点有多少条边和它连接。但是在有权有向图中,边就不仅有方向还有权重,所以一个节点的度(Degree)就被细分为这个节点的出度(out degree)和入度(in degree)。
显然,出度(dout(vi))就是从节点vi出发的边的权重之和;入度(din(vi))就是所有连向节点vi的边的权重之和。
无向图是有向图的特殊情况,所以无向图的节点的出度和入度相等。
无权图又是有权图的特殊情况,无权图的权重为1,所以无权图的节点vi的出度=从vi出发的边的数量,节点vi的入度=所有连向vi的边的数量。
下面是计算一个图的节点的度矩阵(Degree Matrix):
度矩阵也非常非常重要,以后我们讲网络数据传播时,度矩阵也是要和邻接矩阵一样,二者一起要参与节点表征的计算的。
2、邻接节点(neighbors)
与节点vi直接相连的节点,就是节点vi的邻接节点。比如上图的节点A的邻接节点就是E,而节点E的邻接节点是ABCD四个节点。
vi节点k跳远的邻接节点(neighbors with k-hop),指的是到节点vi走k步的节点(一个节点的2跳远的邻接节点包含了自身)。
3、行走(walk)、路径(path)、最短路径(shortest path)
4、子图(subgraph)、连通分量(connected component)、连通图(connected graph)、直径(diameter)
5、拉普拉斯矩阵 (Laplacian Matrix)
说明:上面图论的相关概念仅仅是非常非常少的一部分,也是最常用的一些概念。如果你想知道的更多,可以参考: https://cse.msu.edu/~mayao4/dlg_book/chapters/chapter2.pdf
图论的知识是非常深也非常难的,我只能写个皮毛也算不上的几个点,还有比如关键路径、拓扑排序、聚集系数、标签传播、概率图模型等都是入门图论的基础,有感兴趣的自己找资料学吧,我这里就是抛砖引玉一下。
三、如何创建自己的图数据
首先我们先了解一下图的分类:
然后就是根据图的分类,结合我们的项目目标,把你的原始数据转化为本体图,根据本体图再转化成一张张图结构数据。用一个例子来说明,比如你要做医疗领域的知识图谱,你可能只是有一堆医学文献,或者一堆病历单啥的,现在的任务是让你从这一堆文本中抽取“症状和疾病的关系、疾病和食物的关系、食物和症状的关系”等关系,那这个过程就是:
有了图结构数据后,不管是根据图的定义还是图的属性,我们还得把图结构数据表示为矩阵的形式。因为我们要用算法或者模型进行图计算的,而算法或模型,或者说计算机,它们是不认识图的,它们只能处理比如矩阵这样的结构化数据。也所以不管是深度学习、科学计算、并行计算,还是numpy\pytorch\cuda,它们底层解决的都是矩阵运算、矩阵乘法加速、矩阵分解等问题。所以后续我们还得把图表示成计算机和算法能处理的数据结构、数据形式,才能进行后续的数据计算。也就是比如节点和边的嵌入问题、邻居矩阵的表示、节点的度的衡量、中心性的衡量等问题。最终这些数据才是我们可以直接喂入模型训练的数据。
四、图分析工具:NetworkX
NetworkX是一个图论和网络分析的Python软件包,提供丰富的图数据结构和算法,可以创建、操作和分析各种类型的图。这个工具直接pip install networkx就安装好了。
NetworkX可以创建各种类型的图对象,比如无向图、有向图、有权图等;
NetworkX可以设置节点和边的属性、获取图的邻接矩阵、度、平均度、度分布、度矩阵等;
NetworkX可以进行图分析,因为它内置了很多图论算法,比如图的遍历、最短路径、社区发现、中心性等;
NetworkX可以绘制图对象,支持图数据的可视化,经常用于绘制网络结构。
1、举几个使用例子:
import networkx as nx #导包
#创建一个无向图
G = nx.Graph([('li', 'yuan'), ('hi'), (6, 'nihao'), ('yuan', 6)], attr="Undirected Graph")
G #G是个图对象
type(G) #G的数据类型为networkx.classes.graph.Graph
G.graph #查看图属性
G.graph['attr']='mygraph' #还可以修改图属性
G.graph['name']='Undirected_Graph' #也可以这样添加图属性
G.graph
G.nodes #获取图上的所有节点
#colors = ['r', 'r', 'b', 'b', 'y', 'g'] #根据节点设置每个节点的颜色
colors = [1, 1, 2, 2, 3, 4] #或者随机设置节点的颜色
nx.draw(G, with_labels=True, font_weight='bold', node_size=2000, node_color=colors, pos=nx.circular_layout(G))
#circular_layout random_layout shell_layout spring_layout
import networkx as nx #导包
#创建一个空的有向图
G = nx.DiGraph()
G
type(G)
G.graph
#在图上添加节点
G.add_node('hello') #添加单个节点
G.add_node(6, attr1='age', attr2='height', attr3='female') #添加单个节点,并且设置这个节点的属性
G.add_nodes_from(['ni', 'hao']) #一次添加多个节点
G.add_nodes_from('hi') #一次添加多个节点
#在图上添加边
G.add_edge('li','yuan', attr1='name', value=1.5)
G.add_edges_from([('ni', 'hao'), ('h', 'i')])
G.nodes #获取图上的所有节点
G.edges #获取图上的所有边
colors_nodes = ['r', 'b', 'y', 'y', 'g', 'g', 'pink', 'pink'] #根据节点设置每个节点的颜色
colors_edges = [1, 2, 3] #根据边设置边的颜色
nx.draw(G, with_labels=True, font_weight='bold', node_size=2000, node_color=colors_nodes, edge_color=colors_edges, pos=nx.circular_layout(G))
import networkx as nx #导包
#创建一个空的有向图
G = nx.DiGraph()
#在图上添加节点
G.add_node('hello') #添加单个节点
G.add_node(6, attr1='age', attr2='height', attr3='female') #添加单个节点,并且设置这个节点的属性
G.add_nodes_from(['ni', 'hao', 'li', 'yuan']) #一次添加多个节点
#在图上添加边
G.add_edge('li','yuan', length=1.5, attr1='name') #自己定义length属性为边的权重
G.add_edges_from([('ni', 'hao', {'length':2.0, 'attr1':10}), ('hello', 6, {'length':0.6})]) #添加多个边,并且每个边都有属性
#检查图上的节点和边属性
G.nodes #获取图上的所有节点
G.nodes[6] #查看节点6的属性
G.number_of_nodes() #获取图上所有节点的个数
G.edges #获取图上的所有边
G['li']['yuan'] #获取节点li和节点yuan之间的边的属性
G.number_of_edges() #获取边的个数
colors_nodes = ['b', 'b', 'g', 'g', 'r', 'r'] #根据节点设置每个节点的颜色
colors_edges = [1, 2, 3] #根据边设置边的颜色
nx.draw(G, with_labels=True, font_weight='bold', node_size=2000, node_color=colors_nodes, edge_color=colors_edges, pos=nx.circular_layout(G))
nx.draw_networkx_edge_labels(G, edge_labels=nx.get_edge_attributes(G, 'length'), pos=nx.circular_layout(G), font_weight='bold')
import networkx as nx #导包
#创建一个空的有向图
G = nx.DiGraph([('Hi','Li'), ('Li', 'Yuan'), ('Yuan', 'yuan'), ('yuan', '!'),
('Li', 'Ming'), ('Ming', '!')])
G.nodes
node_color = [1, 2, 3, 3, 1, 3]
nx.draw(G, with_labels=True, font_weight='bold', node_size=2000, pos=nx.spring_layout(G), node_color=node_color)
#获取图的邻接矩阵
adj_matrix = nx.adjacency_matrix(G)
adj_matrix
adj_matrix.toarray()
#获取图的度矩阵
degrees = G.degree()
dict(degrees)
degree_matrix = np.diag(list(dict(degrees).values()))
degree_matrix
out_degree = G.out_degree() #出度
dict(out_degree)
in_degree = G.in_degree() #入度
dict(in_degree)
#获取图的拉普拉斯矩阵(Laplacian Matrix)=度矩阵-邻接矩阵
L = degree_matrix - adj_matrix.toarray()
L
#查找两个节点之间的最短路径
path=nx.shortest_path(G, source='Hi', target='!')
path
#查找一个节点的邻接节点
neighbors = list(G.neighbors('Li'))
neighbors
上面只是入门一下这个工具包,其实这个工具包是非常强大的,经常用于复杂网络拓扑结构统计指标计算、典型复杂网络建模(随机图、小世界、无标度等)以及复杂网络可视化的方法等,值得花时间熟悉一下。
2、举一个交通网络优化的小案例:提升城市交通效率的路径优化
假设我们有一个城市的交通网络,由多个路口(节点)和道路(边)组成。我们的目标是从城市的某个起点(比如A点)到达某个终点(比如B点),我们希望:
一是,找到最短路径:从A到B的最短路径。
二是,找到最大流路径:在交通高峰期间,如何最大化从A到B的车辆通过量。
三是,避免拥堵:找到一条尽量避开拥堵区域的路径。
import networkx as nx
# 创建一个有向图来表示交通网络
G = nx.DiGraph()
# 添加节点(路口)
nodes = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H']
G.add_nodes_from(nodes)
# 添加带权重的边(道路)及其通行能力
edges = [('A', 'B', {'weight': 2, 'capacity': 100}), #就是给边设置属性,一个属性是weight,一个属性是capacity
('A', 'C', {'weight': 5, 'capacity': 50}), #weight你可以理解为大家走这条路的“平均用时”
('B', 'C', {'weight': 2, 'capacity': 100}), #capacity你可以理解为这条路的“最大通行量”
('B', 'D', {'weight': 4, 'capacity': 70}),
('C', 'E', {'weight': 1, 'capacity': 120}),
('D', 'F', {'weight': 3, 'capacity': 80}),
('E', 'F', {'weight': 1, 'capacity': 150}),
('F', 'G', {'weight': 2, 'capacity': 90}),
('E', 'H', {'weight': 3, 'capacity': 60}),
('H', 'G', {'weight': 2, 'capacity': 70}),]
G.add_edges_from(edges)
# 可视化交通网络
plt.rcParams['font.sans-serif'] = ['Microsoft YaHei'] #这行代码是用来让图上显示汉字的
plt.figure(figsize=(10, 6)) #搞一个画布
pos=nx.spring_layout(G) #这种模式是随机的,所以别让它运行两次!
nx.draw(G, with_labels=True, node_size=1500, font_size=20, font_weight='bold', pos=pos)
nx.draw_networkx_edge_labels(G, pos=pos, font_size=15,
edge_labels={(i,j): f"w:{k['weight']}, c:{k['capacity']}" for i,j,k in G.edges(data=True)}) #这个参数只接收字典的形式
G.edges(data=True)
plt.title("城市交通网络图") #弄一个标题
plt.show()
#1、基于权重,也就是基于时间,找到从A到G的最短路径
shortest_path = nx.shortest_path(G, source='A', target='G', weight='weight')
shortest_path_length = nx.shortest_path_length(G, source='A', target='G', weight='weight') #这是最短路径用时
shortest_path, shortest_path_length
#2、找到最大流量路径
max_flow_value, max_flow_dict = nx.maximum_flow(G, 'A', 'G',capacity='capacity')
max_flow_value, max_flow_dict
#3、避免拥堵的路径(通过增加某些边的权重来模拟拥堵路径)
# 我们假设边 ('B', 'C') 和 ('E', 'F') 在高峰期很拥堵,因此增加它们的权重
G['B']['C']['weight'] += 10
G['E']['F']['weight'] += 10
# 重新计算最短路径
congestion_avoidance_path = nx.shortest_path(G, source='A', target='G', weight='weight')
congestion_avoidance_path_length = nx.shortest_path_length(G, source='A', target='G', weight='weight')
congestion_avoidance_path, congestion_avoidance_path_length
在没有深度学习之前,人们已经用图论中的定义定理解决实际生活中的很多问题,比如好友推荐、商品推荐、潜在客户分类、金融领域的反欺诈、消费能力预测、社交圈检测等。所以图论的定义定理以及networkx工具是我们进行深度学习的基础。深度学习是要和传统解决方案相互补充、相得益彰,这样你才能游刃有余的解决生活中的实际问题。
PyG是进行图数据计算的,没有可视化功能,所以后期我们还经常要用这个工具对图进行可视化,所以这里补充一些可视化的代码: