说明

没有自己构造GCNLayer,而已使用dgl的GraphConv

环境准备

安装的是GPU版本的DGL,但实际使用没有使用GPU

  1. import torch
  1. pip install dgl-cu110 -f https://data.dgl.ai/wheels/repo.html
  1. import dgl
  2. import numpy as np
  3. import networkx as nx
  4. import torch.nn as nn
  5. import torch.nn.functional as F
  6. from dgl.nn.pytorch import GraphConv
  7. import itertools
  8. import matplotlib.animation as animation
  9. import matplotlib.pyplot as plt
  10. %matplotlib inline

构造

  1. # 构造俱乐部人员关系
  2. def build_karate_club_graph():
  3. # 所有78条边都存储在两个numpy数组中。一个用于源端点
  4. # 另一个用于目标端点。
  5. src = np.array([1, 2, 2, 3, 3, 3, 4, 5, 6, 6, 6, 7, 7, 7, 7, 8, 8, 9, 10, 10,
  6. 10, 11, 12, 12, 13, 13, 13, 13, 16, 16, 17, 17, 19, 19, 21, 21,
  7. 25, 25, 27, 27, 27, 28, 29, 29, 30, 30, 31, 31, 31, 31, 32, 32,
  8. 32, 32, 32, 32, 32, 32, 32, 32, 32, 33, 33, 33, 33, 33, 33, 33,
  9. 33, 33, 33, 33, 33, 33, 33, 33, 33, 33])
  10. dst = np.array([0, 0, 1, 0, 1, 2, 0, 0, 0, 4, 5, 0, 1, 2, 3, 0, 2, 2, 0, 4,
  11. 5, 0, 0, 3, 0, 1, 2, 3, 5, 6, 0, 1, 0, 1, 0, 1, 23, 24, 2, 23,
  12. 24, 2, 23, 26, 1, 8, 0, 24, 25, 28, 2, 8, 14, 15, 18, 20, 22, 23,
  13. 29, 30, 31, 8, 9, 13, 14, 15, 18, 19, 20, 22, 23, 26, 27, 28, 29, 30,
  14. 31, 32])
  15. # 在DGL中,边是有方向性的;让他们双向。
  16. g=dgl.graph((src,dst))
  17. bg=dgl.to_bidirected(g)
  18. return bg
  1. g=build_karate_club_graph()
  2. g
  3. >>>
  4. Graph(num_nodes=34, num_edges=156,
  5. ndata_schemes={}
  6. edata_schemes={})

查看网络

  1. # 由于实际图形是无向的,因此我们去掉边的方向,以达到可视化的目的
  2. nx_G = g.to_networkx().to_undirected()
  3. # 为了图更加美观,我们使用Kamada-Kawaii layout
  4. pos = nx.kamada_kawai_layout(nx_G)
  5. nx.draw(nx_G, pos, with_labels=True, node_color=[[.2, .7, .7]])

image.png

每一个结点特征都是一个独热码one-hot。这里只是展示了如何给图增加特征。实际上,该例子的特征是以单独构建了一个inputs然后传入的

  1. #在DGL中,可以使用特征张量为所有节点一次性添加特征
  2. #使用一个沿着第一个维度批量节点特性的特性张量。
  3. g.ndata['feat'] = torch.eye(34)
  1. #查看特征
  2. print(g.nodes[2].data['feat'])
  3. # print out node 10 and 11's input features
  4. print(g.nodes[[10, 11]].data['feat'])
  5. #也可以这样查看 g.ndata['feat'][torch.tensor([2,10,11])]
  6. >>>
  7. tensor([[0., 0., 1., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,
  8. 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.]])
  9. tensor([[0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 1., 0., 0., 0., 0., 0., 0., 0.,
  10. 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.],
  11. [0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 1., 0., 0., 0., 0., 0., 0.,
  12. 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.]])
  1. #定义一个GCN模型,包含2个图卷积层
  2. #维度 34→5→2 最后只有2个维度,代表的是2个类别 0或33
  3. class GCN(nn.Module):
  4. def __init__(self, in_feats, hidden_size, num_classes):
  5. super(GCN, self).__init__()
  6. self.gcn1 = GraphConv(in_feats,hidden_size)
  7. self.gcn2 = GraphConv(hidden_size, num_classes)
  8. def forward(self, g, inputs):
  9. h = self.gcn1(g, inputs)
  10. h = torch.relu(h)
  11. h = self.gcn2(g, h)
  12. return h
  1. net=GCN(34,5,2)
  2. net
  3. >>>
  4. GCN(
  5. (gcn1): GraphConv(in=34, out=5, normalization=both, activation=None)
  6. (gcn2): GraphConv(in=5, out=2, normalization=both, activation=None)
  7. )

我们使用one-hot向量初始化节点。因为是一个半监督的设定,仅有指导员(节点0)和俱乐部主席(节点33)被分配了label,实现如下:

训练

  1. optimizer = torch.optim.Adam(net.parameters(), lr=0.01)
  2. all_logits = []
  3. for epoch in range(100):
  4. logits = net(g, inputs) # 前向传播 net() 是一个GCN模型 G是一个图数据 inputs 是节点 embedding (one-hot向量)
  5. #print(logits)
  6. #break
  7. #保存logits以便于接下来可视化
  8. all_logits.append(logits.detach())
  9. logp = F.softmax(logits, 1) #logp = F.log_softmax(logits, 1)
  10. #print(logp)
  11. # break
  12. # 仅为标记过的节点(0和33)计算loss
  13. loss = F.nll_loss(logp[labeled_nodes], labels)
  14. print(logp[labeled_nodes])
  15. break
  16. optimizer.zero_grad()
  17. loss.backward()
  18. optimizer.step()
  19. print('Epoch %d | Loss: %.4f' % (epoch, loss.item()))

结果

  1. optimizer = torch.optim.Adam(net.parameters(), lr=0.01)
  2. all_logits = []
  3. for epoch in range(100):
  4. logits = net(g, inputs) # 前向传播 net() 是一个GCN模型 G是一个图数据 inputs 是节点 embedding (one-hot向量)
  5. #保存logits以便于接下来可视化
  6. all_logits.append(logits.detach())
  7. logp = F.softmax(logits, 1)
  8. # 仅为标记过的节点(0和33)计算loss
  9. loss = F.nll_loss(logp[labeled_nodes], labels)
  10. optimizer.zero_grad()
  11. loss.backward()
  12. optimizer.step()
  13. print('Epoch %d | Loss: %.4f' % (epoch, loss.item()))

最后一轮的logits(全部)
image.png
归一化之后(这里只输出了0和33)
print(logp[labeled_nodes])
image.png
[0]号的标签概率接近为[1,0]
[33]号标签概率接近为[0,1]
这里不太好理解,所以我把logp = F.log_softmax(logits, 1)改成了logp = F.softmax(logits, 1)
image.png
这里不懂,因为我的标签设置的是[0,1]