图神经网络GNN/GCN自学笔记&基础代码实现

本文介绍了图神经网络(GNN)的核心概念,包括图的基本构成(节点、边和图),以及GNN如何重构特征。重点讲解了邻接矩阵在GNN中的作用,探讨了GCN(图卷积网络)与普通卷积的区别,强调了半监督学习在GNN中的应用。文章还通过实例展示了GCN的代码实现和训练过程,以及注意事项和优化方法。

GNN

图的基本构成

跟数据结构里面的图基本一样的,没有别的,就是三大特征:

V Vertex(or node)arrtibutes:点 ,比如一个人的身高体重这样的特征

E Edge(or link)arrtibutes and directions: 边,比如你正在读小白我的博客,我们的边就是朋友啦hh

U Global(or master node)arrtibutes:图, 比如一个学校可以做成图,包含了同学,老师,教学楼本身的属性特征,和他们之间的关系特征

总之,三大特征:点,边,图

后续不同的任务可以根据不同的任务来针对某一个具体的特征进行改进;

现在我们知道了什么是特征了,其实GNN最核心的任务就是重构特征,三大特征都可以进行重构,专有名词就是Embedding,(由于本人是从nlp模型学过来的,初学的时候看见还是非常亲切熟悉的hh);

图东西不多,也就这三大特征,如果可以把这三大特征全部都做好的话,具体到实现的任务,其效效果也显然是最好的。

GNN的输出结果:三大特征的分类/回归

邻接矩阵

表示点之间的连接关系

如果一个点的数量n=25,拿笔拿矩阵大小就应该是n*n了;

这个不难理解,比如说每个点与其他所有点的关系都需要被表示,对于每个点的那一行,谁跟他有关系,就标记一个1,反之就是0了

邻接矩阵的特点:对称

一般我们搭建GNN网络的时候模板就是:model = GNN(a,x,......)

a:点与其他点的关系

x:每个点的特征

其实,只要你觉得满足结构,靠自己的感觉发挥一下,就可以做成一个邻接矩阵;

举个栗子,比如我们这里有一幅图:
Welcome->to->read->my->blog

ylxn

welcometoreadmyblog
welcome1

to

1
read1
my1
blog

邻接矩阵就是这样,非常简单,图结构当中,知道每个点的特征和关系,就可以进行应用了

应用领域的个人理解

cv,nlp方面其实用的比较少,不是效果不好,而是用不上,如果后续学习有些论文的想法,可以尝试加上GNN。

用的不多的原因:

cv里面的图数据格式是固定的,正常用卷积直接做即可,nlp也是如此,每个词向量的维度都是固定的。图神经网络可以用来干嘛呢?

当我们讲到分子结构,每个分子组成的原子不一样,数量也不一样,传统的神经网络就不能做了,因为传统神经网络(nn)有个特带你就是输入格式是固定的。而道路交通,GNN的三大特征点边图是千变万化的,带有一点随意的感觉,结构都是不一样的。

那我如果就是喜欢cv nlp呢?那好,现在我们拿到一份数据,里面有个原本很短的数据,为了与最长的那个数据格式匹配,强行扩充成就非常不好了,所以,GNN闪亮登场~,应用领域主要是数据不规则的时候

好处:输入结构无所谓,能做成图结构就行,都可以进行操作,无cv nlp的resize和固定大小一说

更何况,邻接矩阵是需要更新的,每个人的关系都会变化,传统nn更加不行了,GNN则可以解决这些问题;

其实,实际操作的时候,邻接矩阵往往做成别的形式而不是n*n,比如pytorch传过来的就是2*n的矩阵,具体落实到计算的时候,同样也提及到了权重w。

这与cv nlp中的模型训练类似,有一种加权的感觉,总之,每个点的更新都是由自身的特征和所有的邻接点的特征所共同决定的

对于权值w:针对平时的任务,什么方式合适,就选择什么,比如取最大最小,平均加权等

注意

        图神经网络输入一个图的结构,邻接矩阵永远是不变的,改变的是特征;

        既然要考虑自己和邻居(邻接点),效果最好的方式肯定是仅有一种;

        为什么公式里用到了很多层GNN?邻居的向量可能会发生迭代更新,点的特征又会相互影响,比如1更新用到了2和3,后面2的更新又用到了1,1又要受到其影响

感受野

随着卷积的进行,每个点的特征越来越明显,最终就看到了整个全局

当GNN不断进行,最后全局的感受也越来越大了,最后每个点就带上了全局的特征(这里有点Transformer中self-attention的味道)

输出特征能做什么?

对每个点特征组合,就可以进行图分类

各个节点也可以进行分类,边也是如此,

关键任务就是通过GNN得到每个点最终.最好的特征,之后就可以进行自己要做的事情了

GCN

图卷积,与一般的卷积有何不同?

一般的卷积就是把图像的小窗口做成特征,如此如此不断重复;

但是GCN完全不同,没有窗口,只有之前提到的图,所以两者差距还是很大的,个人觉得这样取名字并不perfect,GCN概念与卷积的不是很大,但代码有一定的相似;

输入格式

与GNN一样,输入也不需要是固定格式,只要交给GCN两种东西即可:

1:各个点的输入特征

2:网络结构图

值得一提的是,这并不是完全的监督学习,即不是所有的点都带标签;

这与实际情况也更加拟合,比如有一个非常重要的十字路口,大家可能给它取名叫做“老地方”,但是它旁边的小村庄就不一定会有外号(标签)了对吧;

半监督任务(Semi-supervised learning)

所以引出GCN的一大特点:半监督任务(Semi-supervised learning),就是不需要所有信息全都有标签,有一些玩的溜的都甚至不叫半监督,可以叫做1/10监督,因为他们的标签实在是太少了hh;

没有标签还怎么算损失?实际计算损失和的时候我们只计算有标签的,即可。

这种做法的个人理解:比如,在我的监督任务中,我只研究ylxn同学的情况,所以只关注他与他的社交朋友的特征,跟ylxn同学没有联系的人物圈子也随之没有关注的必要,用不上!

同时,要想研究透ylxn同学,我们就不仅仅要了解他自己,还要了解透他社交同学的所有信息,而周边的人又会认识更多不同的人,所以,标签肯定还是越多越好的

GCN的基本思想

与GNN类似

经过图卷积的洗礼,最后输出点的特征向量

目前图卷积暂时不用做深层,两层足够,不用太多

对于GCN层数不用太多的个人理解

不知是哪位哲学家说过:当你认识了几个人,你就已经认识了全世界

拿到这里来举例:

点1与2,3,4,5,6点有联系,而2~6这些点又会连接其他点

我们1的更新信息由本身与2~6得到,同时也间接地联系到了其他点,也就是带到了整个图的信息,所以一般图卷积层数会较少一点

除了邻接矩阵A,GCN还有: D各个节点的度 , F 每个结点的特征

特征计算方法:邻接矩阵和特征矩阵进行乘法操作,表示聚合邻居关系

其实D就是为了方便计算

更新的A = A + w Xi,

我们看这个公式,显然一个点的邻居如果足够多,那么往往就会结果很大,而那些邻居少的点则计算结果比较小,所以就可以取个平均值,联系了几个点就除几个,这里又用到了D矩阵,实际操作与取倒数,归一化有关。

左乘是对行归一化,右乘是对列进行归一化,(原因与矩阵乘法有关,里解释起来很麻烦,但是真的很好理解)

最终按照公式乘下来就得到了一个新的矩阵

但是如此下来每个值被归一化两次了,原本的数据更小了

因此再把公式改成:

还有个问题,GCN原本论文说的比较绕,

有些点的度非常大,跟谁都有点关系,有些点的度又非常小,只跟极个别有关系

但是这两种东西都不是同一个人,但是按照之前的方法取推断的话

原本度数小的人因为没有跟什么人接触,比如我出生贫民窟,这辈子就认识一个叫王多余的富二代,那王多余的米跟我又没关系,结果却预测出我也是富二代。。。。。。

最终的解决方案:

这里softmax和relu采用的都是tanh函数(不知道对不对???)

注意:GCN层数不能太多,这里还是要根据实际情况来,经验表明3~5层好,多了结果会容易发散(我跟市长认识,层数多了又会变成我就是市长)

下图中可以直观的看到:层数多了,效果反而差了

GCN的基础代码实现

数据集的解释

来自于torch_geometric.datasets 里面自带的经典一个包(KarateClub)

是一个Hello World级别的简单数据集

讲国外有个地方拳击俱乐部发生了口角冲突,然后各个俱乐部之间发生的事情

比如俱乐部之间的哥们和教练之间有点关系,帮派之间谁又不服谁干一架hh

有兴趣,具体的可以去包的官网看看别人的解释

包的内容只有一幅图,图里面有34个点,每个点是一个34维度的向量,向量内容就是别人内置的数据集。(咱们可以不用管)

如果你有兴趣了解一下这个数据,请点这里吧:

torch_geometric.datasets.KarateClub — pytorch_geometric documentation

代码与运行结果展示

networkx是用于数据化成图的一个包

本代码的邻接矩阵就对应前面说的,并不是n*n,而是2*n的

from torch_geometric.datasets import KarateClub
import matplotlib.pyplot as plt 
import networkx as nx #用于把数据化成图的一个包

dataset = KarateClub()

'''
print(dataset) 

print(f'Dataset:{dataset}:')
print('===========================')
print(f'Number of graphs:{len(dataset)}') 
print(f'Number of features:{dataset.num_features}') 
print(f'Number of classes:{dataset.num_classes}') 
''' 

data = dataset[0] 
print(data) 
#邻接矩阵并不是n*n的,而是2*边的个数
edge_index = data.edge_index
print(edge_index.t())

def visualize_graph(G,color):
    plt.figure(figsize = (7,7)) 
    plt.xticks([]) 
    plt.yticks([]) 
    nx.draw_networkx(G,pos = nx.spring_layout(G,seed = 42),with_labels = False,
                     node_color=color ,cmap="Set2")
    plt.show()
    
def visualize_embedding(h,color,epoch = None,loss = None):
    plt.figure(figsize=(7,7)) 
    plt.xticks([])
    plt.yticks([]) 
    h = h.detach().cpu().numpy()
    plt.scatter(h[:,0] ,h[:,1],s = 140,c = color,cmap = "Set2")
    if epoch is not None and loss is not None:
        plt.xlabel(f'Epoch:{epoch}.loss:{loss.item():.4f}',fontsize = 16) 
    plt.show()
    
    

Jupyter里部分的运行结果:

看一下这些数据:

from torch_geometric.utils import to_networkx
G = to_networkx(data,to_undirected = True) 
visualize_graph(G,color = data.y) 
#分成了四个不同的类别

GCN网络的构建:

#构造GCN函数
import torch 
from torch.nn import Linear 
from torch_geometric.nn import GCNConv 

class GCN(torch.nn.Module): 
    def __init__(self):
        super().__init__()
        torch.manual_seed(1234) 
        self.conv1 = GCNConv(dataset.num_features,4) 
        self.conv2 = GCNConv(4,4) 
        self.conv3 = GCNConv(4,2) #做成二维向量方便画图展示
        
        self.classifier = Linear(2,dataset.num_classes) 
        
    def forward(self, x, edge_index):#这里可以对应之前说的,变得只有特征,而邻接矩阵是不变的
        h = self.conv1(x, edge_index) #x图,edge_index矩阵
        h = h.tanh()
        h = self.conv2(h,edge_index) 
        h = h.tanh()
        h = self.conv3(h,edge_index) 
        h = h.tanh()
        #h是一个中间结果,两维向量
        
        #分类层
        out = self.classifier(h) 
        
        return out , h 

model = GCN()
print(model)     

运行结果:

输入特征的展示:

#输入特征的展示
model = GCN()
_,h = model(data.x, data.edge_index) 
print(f'Embedding shape:{list(h.shape)}') 
visualize_embedding(h,color = data.y) 

#开始是随机初始化的,看不出什么东西 

相同颜色的点就是一类的,可以看到,开始大家还是杂乱不规则的

模型的训练:

#训练模型(semi-supervised :半监督) 
#只是中间网络结构用到了其他包,模型的构建还是十分熟悉的
import time 
model = GCN()
cirterion = torch.nn.CrossEntropyLoss() # define loss criterion
optimizer = torch.optim.Adam(model.parameters(),lr = 0.01) #define optimizer

def train(data):
    optimizer.zero_grad()
    out,h = model(data.x, data.edge_index)#h是中间输出的二维向量,主要是为了方便画图
    loss = cirterion(out[data.train_mask],data.y[data.train_mask]) 
    #注意:我们loss只关注有标签的有没有做好,其他的对我们没有什么影响(半监督的体现)
    loss.backward()
    optimizer.step()
    return loss, h 

for epoch in range(401):
    loss, h = train(data) 
    if epoch % 10 == 0:
        visualize_embedding(h,color = data.y, epoch = epoch, loss = loss) 
        time.sleep(0.3) 
    

可以看到,在训练的过程中我们的点逐渐变得有序了起来,右下角的小黑点最明显,都靠在了一起

=========================================================================

参考:

GCN原论文

bilibili:图神经网络GNN/GCN教程

感谢阅读,有疑问欢迎讨论,本人同为初学者!

如有错误,欢迎指正!

最后感谢某位苏大佬的帮助()

人工智能(AI)最近经历了复兴,在视觉,语言,控制和决策等关键领域取得了重大进展。 部分原因在于廉价数据和廉价计算资源,这些资源符合深度学习的自然优势。 然而,在不同的压力下发展的人类智能的许多定义特征仍然是当前方法无法实现的。 特别是,超越一个人的经验 - 从婴儿期开始人类智能的标志 - 仍然是现代人工智能的一项艰巨挑战。 以下是部分立场文件,部分审查和部分统一。我们认为组合概括必须是AI实现类似人类能力的首要任务,结构化表示和计算是实现这一目标的关键。就像生物学利用自然和培养合作一样,我们拒绝“手工工程”和“端到端”学习之间的错误选择,而是倡导一种从其互补优势中获益的方法。我们探索如何在深度学习架构中使用关系归纳偏差来促进对实体,关系和组成它们的规则的学习。我们为AI工具包提供了一个新的构建模块,具有强大的关系归纳偏差 - 图形网络 - 它概括和扩展了在图形上运行的神经网络的各种方法,并为操纵结构化知识和生成结构化行为提供了直接的界面。我们讨论图网络如何支持关系推理和组合泛化,为更复杂,可解释和灵活的推理模式奠定基础。作为本文的配套文件,我们还发布了一个用于构建图形网络的开源软件库,并演示了如何在实践中使用它们。
# GPF ## 一、GPF(Graph Processing Flow):利用图神经网络处理问题的一般化流程 1、图节点预表示:利用NE框架,直接获得全图每个节点的Embedding; 2、正负样本采样:(1)单节点样本;(2)节点对样本; 3、抽取封闭子图:可做类化处理,建立一种通用图数据结构; 4、子图特征融合:预表示、节点特征、全局特征、边特征; 5、网络配置:可以是图输入、图输出的网络;也可以是图输入,分类/聚类结果输出的网络; 6、训练和测试; ## 二、主要文件: 1、graph.py:读入图数据; 2、embeddings.py:预表示学习; 3、sample.py:采样; 4、subgraphs.py/s2vGraph.py:抽取子图; 5、batchgraph.py:子图特征融合; 6、classifier.py:网络配置; 7、parameters.py/until.py:参数配置/帮助文件; ## 三、使用 1、在parameters.py中配置相关参数(可默认); 2、在example/文件夹中运行相应的案例文件--包括链接预测、节点状态预测; 以链接预测为例: ### 1、导入配置参数 ```from parameters import parser, cmd_embed, cmd_opt``` ### 2、参数转换 ``` args = parser.parse_args() args.cuda = not args.noCuda and torch.cuda.is_available() torch.manual_seed(args.seed) if args.cuda: torch.cuda.manual_seed(args.seed) if args.hop != 'auto': args.hop = int(args.hop) if args.maxNodesPerHop is not None: args.maxNodesPerHop = int(args.maxNodesPerHop) ``` ### 3、读取数据 ``` g = graph.Graph() g.read_edgelist(filename=args.dataName, weighted=args.weighted, directed=args.directed) g.read_node_status(filename=args.labelName) ``` ### 4、获取全图节点的Embedding ``` embed_args = cmd_embed.parse_args() embeddings = embeddings.learn_embeddings(g, embed_args) node_information = embeddings #print node_information ``` ### 5、正负节点采样 ``` train, train_status, test, test_status = sample.sample_single(g, args.testRatio, max_train_num=args.maxTrainNum) ``` ### 6、抽取节点对的封闭子图 ``` net = until.nxG_to_mat(g) #print net train_graphs, test_graphs, max_n_label = subgraphs.singleSubgraphs(net, train, train_status, test, test_status, args.hop, args.maxNodesPerHop, node_information) print('# train: %d, # test: %d' % (len(train_graphs), len(test_graphs))) ``` ### 7、加载网络模型,并在classifier中配置相关参数 ``` cmd_args = cmd_opt.parse_args() cmd_args.feat_dim = max_n_label + 1 cmd_args.attr_dim = node_information.shape[1] cmd_args.latent_dim = [int(x) for x in cmd_args.latent_dim.split('-')] if len(cmd_args.latent_dim) == 1: cmd_args.latent_dim = cmd_args.latent_dim[0] model = classifier.Classifier(cmd_args) optimizer = optim.Adam(model.parameters(), lr=args.learningRate) ``` ### 8、训练和测试 ``` train_idxes = list(range(len(train_graphs))) best_loss = None for epoch in range(args.num_epochs): random.shuffle(train_idxes) model.train() avg_loss = loop_dataset(train_graphs, model, train_idxes, cmd_args.batch_size, optimizer=optimizer) print('\033[92maverage training of epoch %d: loss %.5f acc %.5f auc %.5f\033[0m' % (epoch, avg_loss[0], avg_loss[1], avg_loss[2])) model.eval() test_loss = loop_dataset(test_graphs, model, list(range(len(test_graphs))), cmd_args.batch_size) print('\033[93maverage test of epoch %d: loss %.5f acc %.5f auc %.5f\033[0m' % (epoch, test_loss[0], test_loss[1], test_loss[2])) ``` ### 9、运行结果 ``` average test of epoch 0: loss 0.62392 acc 0.71462 auc 0.72314 loss: 0.51711 acc: 0.80000: 100%|███████████████████████████████████| 76/76 [00:07<00:00, 10.09batch/s] average training of epoch 1: loss 0.54414 acc 0.76895 auc 0.77751 loss: 0.37699 acc: 0.79167: 100%|█████████████████████████████████████| 9/9 [00:00<00:00, 34.07batch/s] average test of epoch 1: loss 0.51981 acc 0.78538 auc 0.79709 loss: 0.43700 acc: 0.84000: 100%|███████████████████████████████████| 76/76 [00:07<00:00, 9.64batch/s] average training of epoch 2: loss 0.49896 acc 0.79184 auc 0.82246 loss: 0.63594 acc: 0.66667: 100%|█████████████████████████████████████| 9/9 [00:00<00:00, 28.62batch/s] average test of epoch 2: loss 0.48979 acc 0.79481 auc 0.83416 loss: 0.57502 acc: 0.76000: 100%|███████████████████████████████████| 76/76 [00:07<00:00, 9.70batch/s] average training of epoch 3: loss 0.50005 acc 0.77447 auc 0.79622 loss: 0.38903 acc: 0.75000: 100%|█████████████████████████████████████| 9/9 [00:00<00:00, 34.03batch/s] average test of epoch 3: loss 0.41463 acc 0.81132 auc 0.86523 loss: 0.54336 acc: 0.76000: 100%|███████████████████████████████████| 76/76 [00:07<00:00, 9.57batch/s] average training of epoch 4: loss 0.44815 acc 0.81711 auc 0.84530 loss: 0.44784 acc: 0.70833: 100%|█████████████████████████████████████| 9/9 [00:00<00:00, 28.62batch/s] average test of epoch 4: loss 0.48319 acc 0.81368 auc 0.84454 loss: 0.36999 acc: 0.88000: 100%|███████████████████████████████████| 76/76 [00:07<00:00, 10.17batch/s] average training of epoch 5: loss 0.39647 acc 0.84184 auc 0.89236 loss: 0.15548 acc: 0.95833: 100%|█████████████████████████████████████| 9/9 [00:00<00:00, 28.62batch/s] average test of epoch 5: loss 0.30881 acc 0.89623 auc 0.95132 ```
评论 2
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值