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 | welcome | to | read | my | blog |
welcome | 1 | ||||
to | 1 | ||||
read | 1 | ||||
my | 1 | ||||
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)
可以看到,在训练的过程中我们的点逐渐变得有序了起来,右下角的小黑点最明显,都靠在了一起
=========================================================================
参考:
感谢阅读,有疑问欢迎讨论,本人同为初学者!
如有错误,欢迎指正!
最后感谢某位苏大佬的帮助()