(转载)GNN教程:开山之作GCN代码理解(DGL框架)

大纲

本文为GNN教程的第七篇文章【使用DGL框架实现GCN算法】。图神经网络的计算模式大致相似,节点的Embedding需要汇聚其邻接节点Embedding以更新,从线性代数的角度来看,这就是邻接矩阵和特征矩阵相乘。然而邻接矩阵通常都会很大,因此另一种计算方法是将邻居的Embedding传递到当前节点上,再进行更新。很多图并行框架都采用详细传递的机制进行运算(比如Google的Pregel)。而图神经网络框架DGL也采用了这样的思路。

从本篇博文开始,我们使用DGL做一个系统的介绍,我们主要关注他的设计,尤其是应对大规模图计算的设计。这篇文章将会介绍DGL的核心概念 — 消息传递机制,并且使用DGL框架实现GCN算法。图片

DGL 核心 — 消息传递

DGL 的核心为消息传递机制(message passing),主要分为消息函数 (message function)和汇聚函数(reduce function)。如下图所示:

图片

  • 消息函数(message function):传递消息的目的是将节点计算时需要的信息传递给它,因此对每条边来说每个源节点将会将自身的Embedding(e.src.data)和边的Embedding(edge.data)传递到目的节点;对于每个目的节点来说,它可能会受到多个源节点传过来的消息,它会将这些消息存储在"邮箱"中。(gcn_msg:就是将边消息和邻居消息h传到中心节点,存到中心节点的邮箱mailbox中,记做m)

  • 汇聚函数(reduce function):汇聚函数的目的是根据邻居传过来的消息更新跟新自身节点Embedding,对每个节点来说,它先从邮箱(v.mailbox['m'])中汇聚消息函数所传递过来的消息(message),并清空邮箱(v.mailbox['m'])内消息(gcn_reduce每个节点从邮箱中汇聚所有传过来的信息m,并清空,记做h);然后该节点结合汇聚后的结果和该节点原Embedding,更新节点Embedding(需要做个线性变换NodeApplyModule,并且在线性变换的基础上加上自身信息来更新节点表示GCNupdate_all+apply_nodes)。

下面我们以GCN的算法为例,详细说明消息传递的机制是如何work的。

用消息传递的方式实现GCN

GCN 的线性代数表达

GCN 的逐层传播公式如下所示:

简而言之:每个节点拿到邻居节点信息汇聚到自身 embedding 上在进行一次变换。具体 GCN 内容介绍可参考之前的文章

从消息传递的角度分析

上面的数学描述可以利用消息传递的机制实现为:

具体实现

step 1,引入相关包

import dgl
import torch as th
import torch.nn as nn
import dgl.function as fn
import torch.nn.functional as F
from dgl import DGLGraph
import networkx as nx

step 2,需要定义 GCN 的 message 函数和 reduce 函数

 message 函数用于发送节点的Embedding,reduce 函数用来对收到的 Embedding 进行聚合。在这里,每个节点发送Embedding的时候不需要任何处理,所以可以通过内置的copy_scr实现,out='m'表示发送到目的节点后目的节点的mailbox用m来标识这个消息是源节点的Embedding。

目的节点的reduce函数很简单,因为按照GCN的数学定义,邻接矩阵和特征矩阵相乘,以为这更新后的特征矩阵的每一行是原特征矩阵某几行相加的形式,"某几行"是由邻接矩阵选定的,即对应节点的邻居所在的行。因此目的节点reduce只需要通过sum将接受到的信息相加就可以了。

#需要定义GCN的 message函数和reduce函数
#message 函数用于发送节点的Embedding,reduce 函数用来对收到的 Embedding 进行聚合
#传递节点特征h的message
gcn_msg = fn.copy_src(src='h', out='m') #将节点表示为h作为信息发出
gcn_reduce = fn.sum(msg='m',out='h') #累和函数

'''
#对所有邻居节点特征hu进行平均,并使用它来覆盖原始节点特征
def reduce(nodes): #定义消息类和函数,对收到的消息进行平均
    accum = torch.mean(nodes.mailbox['m'],1)
    return {'h': accum}
'''

step 3,定义一个应用于节点的 node UDF(user defined function)

#定义一个应用于节点的 node UDF(user defined function)
#实际上是定义一个全连接层
#使用ReLU(whv + b) 更新节点特征hv
class NodeApplyModule(nn.Module):#对收到的消息应用线性变换和激活函数,将节点特征 hv 更新为ReLU(whv + b)
    def __init__(self,in_feats,out_feats,activation):
        super(NodeApplyModule,self).__init__()
        self.linear=nn.Linear(in_feats,out_feats)
        self.activation=activation #还没有指定是什么激活函数
        
    def forward(self,node):
        h = self.linear(node.data['h'])
        h = self.activation(h)
        return {'h' : h}  #返回更新后的节点特征 h(l+1)

 step4 ,定义GCN的Embedding更新层

以实现在所有节点上进行消息传递,并利用 NodeAppleModule 对节点的信息进行计算

#定义GCN的Enbedding更新层
#我们把所有的小模块串联起来成为 GCNLayer
#GCN实际上是对所有节点进行 消息传递/聚合/更新
class GCN(nn.Module):
    def __init__(self,in_feats,out_feats,activation):
        super(GCN,self).__init__()
        self.apply_mod = NodeApplyModule(in_feats, out_feats, activation)

    def forward(self,g,feature): 
        g.ndata['h'] = feature#使用h初始化节点特征
        g.update_all(gcn_msg,gcn_reduce)#使用update_all接口和自定义的消息传递及累和函数更新节点表示
        g.apply_nodes(func=self.apply_mod)# 更新节点特征
        return g.ndata.pop('h') #pop() 函数用于移除列表中的一个元素(默认最后一个元素),并且返回该元素的值

step 5 定义一个包含两个GCN层的图神经网络分类器

我们通过向分类器输入特征大小为1433的训练样本,以获得该样本所属的类别编号,类别总共包含7类。

#定义一个包含两个GCN层的图神经网络分类器
class Net(nn.Module):
    #初始化网络参数
    def __init__(self):
        super (Net,self).__init__()
        self.gcn1 = GCN(1433,16,F.relu) #第一层GCN
        self.gcn2 = GCN(16,7,F.relu)
    #前向传播
    def forward(self,g,features):
        x = self.gcn1(g,features)
        x = self.gcn2(g,x)
        return x
net = Net()
print(net)

step 6 加载 cora 数据集,并进行数据预处理

#使用DGL内置模块加载cora数据集,并进行数据预处理
from dgl.data import citation_graph as citegrh
def load_cora_data():
    data = citegrh.load_cora() #加载数据集
    features = th.FloatTensor(data.features) #特征向量,张量的形式
    labels = th.LongTensor(data.labels)#所属的类别
    train_mask = th.BoolTensor(data.train_mask)#训练数据集
    test_mask = th.BoolTensor(data.test_mask) #哪些是测试集
    g = data.graph #
    g.remove_edges_from(nx.selfloop_edges(g)) #删除自循环的边
    g = DGLGraph(g) #构造
    g.add_edge(g.nodes(), g.nodes()) #第一个是起始点,第二个终点
    return g,features,labels,train_mask,test_mask

step7 训练GCN神经网络

#训练GCN神经网络
import time
import numpy as np
g,features,labels,train_mask,test_mask = load_cora_data()
optimizer = th.optim.Adam(net.parameters(),lr=1e-3)
dur = []
for epoch in range(30):
    if epoch >=3:
        t0 = time.time()
        
    logits = net(g,features)
    logp = F.log_softmax(logits,) #用来计算输出和梯度
    loss = F.nll_loss(logp[train_mask], labels[train_mask])
    
    optimizer.zero_grad()
    loss.backward()
    optimizer.step()
    
    if epoch >= 3:
        dur.append(time.time() - t0)
        
    print("Epoch {:05d} | Loss {:.4f} | Time(s) {:.4f}".format(
        epoch,loss.item(),np.mean(dur)))

补充:增加一个评估函数

#定义评估函数
def evaluate(model,g,features,labels,mask):
    model.eval() #会通知所有图层,您处于评估模式
    with th.no_grad():
        logits = model(g,features)
        logits = logits[mask]
        labels = labels[mask]
        _, indices = th.max(logits,dim=1)
        correct = th.sum(indices == labels)
        return correct.item() * 1.0 /len(labels) 

全部代码

import dgl
import torch as th
import torch.nn as nn
import dgl.function as fn
import torch.nn.functional as F
from dgl import DGLGraph
import torch
import networkx as nx

#需要定义GCN的 message函数和reduce函数
#message 函数用于发送节点的Embedding,reduce 函数用来对收到的 Embedding 进行聚合
#传递节点特征h的message
gcn_msg = fn.copy_src(src='h', out='m') #将节点表示为h作为信息发出
gcn_reduce = fn.sum(msg='m',out='h') #累和函数

'''
#对所有邻居节点特征hu进行平均,并使用它来覆盖原始节点特征
def reduce(nodes): #定义消息类和函数,对收到的消息进行平均
    accum = torch.mean(nodes.mailbox['m'],1)
    return {'h': accum}
'''
#定义一个应用于节点的 node UDF(user defined function)
#实际上是定义一个全连接层
#使用ReLU(whv + b) 更新节点特征hv
class NodeApplyModule(nn.Module):#对收到的消息应用线性变换和激活函数,将节点特征 hv 更新为ReLU(whv + b)
    def __init__(self,in_feats,out_feats,activation):
        super(NodeApplyModule,self).__init__()
        self.linear=nn.Linear(in_feats,out_feats)
        self.activation=activation #还没有指定是什么激活函数
        
    def forward(self,node):
        h = self.linear(node.data['h'])
        h = self.activation(h)
        return {'h' : h}  #返回更新后的节点特征 h(l+1)
    
#定义GCN的Enbedding更新层
#我们把所有的小模块串联起来成为 GCNLayer
#GCN实际上是对所有节点进行 消息传递/聚合/更新
class GCN(nn.Module):
    def __init__(self,in_feats,out_feats,activation):
        super(GCN,self).__init__()
        self.apply_mod = NodeApplyModule(in_feats, out_feats, activation)

    def forward(self,g,feature): 
        g.ndata['h'] = feature#使用h初始化节点特征
        g.update_all(gcn_msg,gcn_reduce)#使用update_all接口和自定义的消息传递及累和函数更新节点表示
        g.apply_nodes(func=self.apply_mod)# 更新节点特征
        return g.ndata.pop('h') #pop() 函数用于移除列表中的一个元素(默认最后一个元素),并且返回该元素的值
     
#定义一个包含两个GCN层的图神经网络分类器
class Net(nn.Module):
    #初始化网络参数
    def __init__(self):
        super (Net,self).__init__()
        self.gcn1 = GCN(1433,16,F.relu) #第一层GCN
        self.gcn2 = GCN(16,7,F.relu)
    #前向传播
    def forward(self,g,features):
        x = self.gcn1(g,features)
        x = self.gcn2(g,x)
        return x
net = Net()
print(net)

#使用DGL内置模块加载cora数据集,并进行数据预处理
from dgl.data import citation_graph as citegrh
def load_cora_data():
    data = citegrh.load_cora() #加载数据集
    features = th.FloatTensor(data.features) #特征向量,张量的形式
    labels = th.LongTensor(data.labels)#所属的类别
    train_mask = th.BoolTensor(data.train_mask)#训练数据集
    test_mask = th.BoolTensor(data.test_mask) #哪些是测试集
    g = data.graph #
    g.remove_edges_from(nx.selfloop_edges(g)) #删除自循环的边
    g = DGLGraph(g) #构造
    g.add_edge(g.nodes(), g.nodes()) #第一个是起始点,第二个终点
    return g,features,labels,train_mask,test_mask

g,features,labels,train_mask,test_mask = load_cora_data()

import matplotlib.pyplot as plt
nx.draw(g.to_networkx(),node_size=50,with_labels=True)
plt.show()

#定义评估函数
def evaluate(model,g,features,labels,mask):
    model.eval() #会通知所有图层,您处于评估模式
    with th.no_grad():
        logits = model(g,features)
        logits = logits[mask]
        labels = labels[mask]
        _, indices = th.max(logits,dim=1)
        correct = th.sum(indices == labels)
        return correct.item() * 1.0 /len(labels) 



#训练GCN神经网络
import time
import numpy as np
g,features,labels,train_mask,test_mask = load_cora_data()
optimizer = th.optim.Adam(net.parameters(),lr=1e-3)
dur = [] #时间
for epoch in range(30):
    if epoch >=3:
        t0 = time.time()
        
    logits = net(g,features)
    logp = F.log_softmax(logits,) #用来计算输出和梯度
    loss = F.nll_loss(logp[train_mask], labels[train_mask])
    
    optimizer.zero_grad()
    loss.backward()
    optimizer.step()
    
    if epoch >= 3:
        dur.append(time.time() - t0)
    acc = evaluate(net,g,features,labels,test_mask)    
    print("Epoch {:05d} | Loss {:.4f} | Test Acc {:.4f}| Time(s) {:.4f}".format(
        epoch,loss.item(),acc,np.mean(dur)))

后话

本篇博文介绍了如何利用图神经网络框架DGL编写GCN模型,接下来我们会介绍如何利用DGL实现GraphSAGE中的采样机制,以减少运算规模。

  • 1
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值