当前位置:   article > 正文

图神经网络 | (4) GCN实战_dataset = coradata().data

dataset = coradata().data

近期买了一本图神经网络的入门书,接下里的几篇博客对书中的一些实战案例进行整理,具体的理论和原理部分可以自行查阅该书,该书购买链接:《深入浅出的图神经网络》

该书配套代码

本节我们通过一个完整的例子来理解如何通过GCN来实现对节点的分类。

目录

1. 数据集及预处理

2. 图卷积层定义

3. 模型定义

4. 模型训练


1. 数据集及预处理

我们使用的是Cora数据集,该数据集由2708篇论文,以及它们之间的引用关系构成的5429条边构成。这些论文根据主题划分为7类,分别是神经网络、强化学习、规则学习、概率方法、遗传算法、理论研究、案例相关。每篇论文的特征(向量)通过词袋模型得到,维度为1433(词典大小),每一维表示一个词,1表示该词在该论文中出现,0表示未出现。

首先定义类CoraData来对数据进行预处理,主要包括下载数据、规范化数据并进行缓存以备重复使用。最终得到的数据形式包括如下几个部分:

1)X:图中节点的特征,维度为N*D,即2708*1433(每个节点表示一条数据/一篇论文)

2)Y:节点对应的标签,包括7个类别。

3)adjacency:邻接矩阵,维度N*N(2708*2708),类型为scipy.sparse.coo_matrix

4)train_mask、val_mask、test_mask:与节点数相同的掩码,用于划分训练集、验证集、 测试集。

注意:我们把每条数据/每篇论文表示为图中的一个节点,和之前的深度学习数据集不同,以前我们假设数据之间是独立同分布的,在这里论文间都有引用关系,也就是每个数据都是有关联的,之前的假设不再适用。所以,我们把这种有关联的数据表示为图中节点,边表示数据之间的关系。这是一个典型的图数据。

  1. #导入必要的库
  2. import itertools
  3. import os
  4. import os.path as osp
  5. import pickle
  6. import urllib
  7. from collections import namedtuple
  8. import numpy as np
  9. import scipy.sparse as sp #邻接矩阵用稀疏矩阵形式存储 节省空间
  10. import torch
  11. import torch.nn as nn
  12. import torch.nn.functional as F
  13. import torch.nn.init as init
  14. import torch.optim as optim
  15. import matplotlib.pyplot as plt
  16. %matplotlib inline
  • CoraData类定义

A为邻接矩阵 N*N;

\widetilde{A} = A + I(添加自连接 不仅考虑邻接节点的特征信息,还考虑自身节点的特征信息)

\widetilde{D}_{ii} = \sum_j \widetilde{A}_{ij}      此时的度矩阵\widetilde{D}为邻接矩阵\widetilde{A}按行求和。

归一化拉普拉斯矩阵:\widetilde{L}_{sym} = \widetilde{D}^{-1/2}\widetilde{A}\widetilde{D}^{-1/2}(解决邻接节点较多的节点,有更大影响的问题)

  1. Data = namedtuple('Data', ['x', 'y', 'adjacency',
  2. 'train_mask', 'val_mask', 'test_mask'])
  3. def tensor_from_numpy(x, device): #将数据从数组格式转换为tensor格式 并转移到相关设备上
  4. return torch.from_numpy(x).to(device)
  5. class CoraData(object):
  6. #数据集下载链接
  7. download_url = "https://raw.githubusercontent.com/kimiyoung/planetoid/master/data"
  8. #数据集中包含的文件名
  9. filenames = ["ind.cora.{}".format(name) for name in
  10. ['x', 'tx', 'allx', 'y', 'ty', 'ally', 'graph', 'test.index']]
  11. def __init__(self, data_root="cora", rebuild=False):
  12. """Cora数据,包括数据下载,处理,加载等功能
  13. 当数据的缓存文件存在时,将使用缓存文件,否则将下载、进行处理,并缓存到磁盘
  14. 处理之后的数据可以通过属性 .data 获得,它将返回一个数据对象,包括如下几部分:
  15. * x: 节点的特征,维度为 2708 * 1433,类型为 np.ndarray
  16. * y: 节点的标签,总共包括7个类别,类型为 np.ndarray
  17. * adjacency: 邻接矩阵,维度为 2708 * 2708,类型为 scipy.sparse.coo.coo_matrix
  18. * train_mask: 训练集掩码向量,维度为 2708,当节点属于训练集时,相应位置为True,否则False
  19. * val_mask: 验证集掩码向量,维度为 2708,当节点属于验证集时,相应位置为True,否则False
  20. * test_mask: 测试集掩码向量,维度为 2708,当节点属于测试集时,相应位置为True,否则False
  21. Args:
  22. -------
  23. data_root: string, optional
  24. 存放数据的目录,原始数据路径: {data_root}/raw
  25. 缓存数据路径: {data_root}/processed_cora.pkl
  26. rebuild: boolean, optional
  27. 是否需要重新构建数据集,当设为True时,如果存在缓存数据也会重建数据
  28. """
  29. self.data_root = data_root
  30. save_file = osp.join(self.data_root, "processed_cora.pkl")
  31. if osp.exists(save_file) and not rebuild: #使用缓存数据
  32. print("Using Cached file: {}".format(save_file))
  33. self._data = pickle.load(open(save_file, "rb"))
  34. else:
  35. self.maybe_download() #下载或使用原始数据集
  36. self._data = self.process_data() #数据预处理
  37. with open(save_file, "wb") as f: #把处理好的数据保存为缓存文件.pkl 下次直接使用
  38. pickle.dump(self.data, f)
  39. print("Cached file: {}".format(save_file))
  40. @property
  41. def data(self):
  42. """返回Data数据对象,包括x, y, adjacency, train_mask, val_mask, test_mask"""
  43. return self._data
  44. def process_data(self):
  45. """
  46. 处理数据,得到节点特征和标签,邻接矩阵,训练集、验证集以及测试集
  47. 引用自:https://github.com/rusty1s/pytorch_geometric
  48. """
  49. print("Process data ...")
  50. #读取下载的数据文件
  51. _, tx, allx, y, ty, ally, graph, test_index = [self.read_data(
  52. osp.join(self.data_root, "raw", name)) for name in self.filenames]
  53. train_index = np.arange(y.shape[0]) #训练集索引
  54. val_index = np.arange(y.shape[0], y.shape[0] + 500)#验证集索引
  55. sorted_test_index = sorted(test_index) #测试集索引
  56. x = np.concatenate((allx, tx), axis=0) #节点特征 N*D 2708*1433
  57. y = np.concatenate((ally, ty), axis=0).argmax(axis=1)#节点对应的标签 2708
  58. x[test_index] = x[sorted_test_index]
  59. y[test_index] = y[sorted_test_index]
  60. num_nodes = x.shape[0] #节点数/数据量 2708
  61. #训练、验证、测试集掩码
  62. #初始化为0
  63. train_mask = np.zeros(num_nodes, dtype=np.bool)
  64. val_mask = np.zeros(num_nodes, dtype=np.bool)
  65. test_mask = np.zeros(num_nodes, dtype=np.bool)
  66. train_mask[train_index] = True
  67. val_mask[val_index] = True
  68. test_mask[test_index] = True
  69. #构建邻接矩阵
  70. adjacency = self.build_adjacency(graph)
  71. print("Node's feature shape: ", x.shape) #(N*D)
  72. print("Node's label shape: ", y.shape)#(N,)
  73. print("Adjacency's shape: ", adjacency.shape) #(N,N)
  74. #训练、验证、测试集各自的大小
  75. print("Number of training nodes: ", train_mask.sum())
  76. print("Number of validation nodes: ", val_mask.sum())
  77. print("Number of test nodes: ", test_mask.sum())
  78. return Data(x=x, y=y, adjacency=adjacency,
  79. train_mask=train_mask, val_mask=val_mask, test_mask=test_mask)
  80. def maybe_download(self):
  81. #原始数据保存路径
  82. save_path = os.path.join(self.data_root, "raw")
  83. #下载相应的文件
  84. for name in self.filenames:
  85. if not osp.exists(osp.join(save_path, name)):
  86. self.download_data(
  87. "{}/{}".format(self.download_url, name), save_path)
  88. @staticmethod
  89. def build_adjacency(adj_dict):
  90. """根据下载的邻接表创建邻接矩阵"""
  91. edge_index = []
  92. num_nodes = len(adj_dict)
  93. for src, dst in adj_dict.items():
  94. edge_index.extend([src, v] for v in dst)
  95. edge_index.extend([v, src] for v in dst)
  96. # 去除重复的边
  97. edge_index = list(k for k, _ in itertools.groupby(sorted(edge_index)))
  98. edge_index = np.asarray(edge_index)
  99. #稀疏矩阵 存储非0值 节省空间
  100. adjacency = sp.coo_matrix((np.ones(len(edge_index)),
  101. (edge_index[:, 0], edge_index[:, 1])),
  102. shape=(num_nodes, num_nodes), dtype="float32")
  103. return adjacency
  104. @staticmethod
  105. def read_data(path):
  106. """使用不同的方式读取原始数据以进一步处理"""
  107. name = osp.basename(path)
  108. if name == "ind.cora.test.index":
  109. out = np.genfromtxt(path, dtype="int64")
  110. return out
  111. else:
  112. out = pickle.load(open(path, "rb"), encoding="latin1")
  113. out = out.toarray() if hasattr(out, "toarray") else out
  114. return out
  115. @staticmethod
  116. def download_data(url, save_path):
  117. """数据下载工具,当原始数据不存在时将会进行下载"""
  118. if not os.path.exists(save_path):
  119. os.makedirs(save_path)
  120. data = urllib.request.urlopen(url)
  121. filename = os.path.split(url)[-1]
  122. with open(os.path.join(save_path, filename), 'wb') as f:
  123. f.write(data.read())
  124. return True
  125. @staticmethod
  126. def normalization(adjacency):
  127. """计算 L=D^-0.5 * (A+I) * D^-0.5"""
  128. adjacency += sp.eye(adjacency.shape[0]) # 增加自连接 不仅考虑邻接节点特征 还考虑节点自身的特征
  129. degree = np.array(adjacency.sum(1)) #此时的度矩阵的对角线的值 为 邻接矩阵 按行求和
  130. d_hat = sp.diags(np.power(degree, -0.5).flatten()) #对度矩阵对角线的值取-0.5次方 再转换为对角矩阵
  131. return d_hat.dot(adjacency).dot(d_hat).tocoo() #归一化的拉普拉斯矩阵 稀疏存储 节省空间

2. 图卷积层定义

根据GCN的定义X' = \sigma(\widetilde{L}_{sym}XW)来定义图卷积层,代码直接根据定义来实现,需要特别注意的是邻接矩阵是稀疏矩阵,为了提高运算效率,使用了稀疏矩阵的乘法。

注意:之前我们在PyTorch使用图像卷积时,可以直接调用nn.Conv函数,是因为图像卷积相关的操作都已经在PyTorch中封装好了,可以直接用;对于图卷积,目前没有封装好的现成函数使用,所以需要自定义图卷积层,包括参数定义,初始化方式,前向传播过程等。

  1. class GraphConvolution(nn.Module):
  2. def __init__(self, input_dim, output_dim, use_bias=True):
  3. """图卷积:L*X*\theta
  4. Args:
  5. ----------
  6. input_dim: int
  7. 节点输入特征的维度 D
  8. output_dim: int
  9. 输出特征维度 D‘
  10. use_bias : bool, optional
  11. 是否使用偏置
  12. """
  13. super(GraphConvolution, self).__init__()
  14. self.input_dim = input_dim
  15. self.output_dim = output_dim
  16. self.use_bias = use_bias
  17. #定义GCN层的权重矩阵
  18. self.weight = nn.Parameter(torch.Tensor(input_dim, output_dim))
  19. if self.use_bias:
  20. self.bias = nn.Parameter(torch.Tensor(output_dim))
  21. else:
  22. self.register_parameter('bias', None)
  23. self.reset_parameters() #使用自定义的参数初始化方式
  24. def reset_parameters(self):
  25. #自定义参数初始化方式
  26. #权重参数初始化方式
  27. init.kaiming_uniform_(self.weight)
  28. if self.use_bias: #偏置参数初始化为0
  29. init.zeros_(self.bias)
  30. def forward(self, adjacency, input_feature):
  31. """邻接矩阵是稀疏矩阵,因此在计算时使用稀疏矩阵乘法
  32. Args:
  33. -------
  34. adjacency: torch.sparse.FloatTensor
  35. 邻接矩阵
  36. input_feature: torch.Tensor
  37. 输入特征
  38. """
  39. support = torch.mm(input_feature, self.weight) #XW (N,D');X (N,D);W (D,D')
  40. output = torch.sparse.mm(adjacency, support) #(N,D')
  41. if self.use_bias:
  42. output += self.bias
  43. return output
  44. def __repr__(self):
  45. return self.__class__.__name__ + ' (' \
  46. + str(self.input_dim) + ' -> ' \
  47. + str(self.output_dim) + ')'

 

3. 模型定义

有了数据和GCN层,就可以构建模型进行训练了。定义一个两层的GCN,其中输入维度为1433(输入特征维度),隐藏层维度设为16,最后一层GCN将输出维度设置为7(分类类别数),激活函数使用ReLU。

读者可以自己对GCN模型结构进行修改和实验。

  1. class GcnNet(nn.Module):
  2. """
  3. 定义一个包含两层GraphConvolution的模型
  4. """
  5. def __init__(self, input_dim=1433):
  6. super(GcnNet, self).__init__()
  7. self.gcn1 = GraphConvolution(input_dim, 16)
  8. self.gcn2 = GraphConvolution(16, 7)
  9. def forward(self, adjacency, feature):
  10. h = F.relu(self.gcn1(adjacency, feature)) #(N,1433)->(N,16)
  11. logits = self.gcn2(adjacency, h) #(N,16)->(N,7)
  12. return logits

4. 模型训练

  • 超参数定义
  1. # 超参数定义
  2. LEARNING_RATE = 0.1 #学习率
  3. WEIGHT_DACAY = 5e-4 #正则化系数
  4. EPOCHS = 200 #完整遍历训练集的次数
  5. DEVICE = "cuda" if torch.cuda.is_available() else "cpu" #设备
  • 数据加载 
  1. # 加载数据,并转换为torch.Tensor
  2. dataset = CoraData().data
  3. node_feature = dataset.x / dataset.x.sum(1, keepdims=True) # 归一化数据,使得每一行和为1
  4. tensor_x = tensor_from_numpy(node_feature, DEVICE)
  5. tensor_y = tensor_from_numpy(dataset.y, DEVICE)
  6. tensor_train_mask = tensor_from_numpy(dataset.train_mask, DEVICE)
  7. tensor_val_mask = tensor_from_numpy(dataset.val_mask, DEVICE)
  8. tensor_test_mask = tensor_from_numpy(dataset.test_mask, DEVICE)
  9. normalize_adjacency = CoraData.normalization(dataset.adjacency) # 规范化邻接矩阵
  10. num_nodes, input_dim = node_feature.shape #(N,D)
  11. #转换为稀疏表示 加速运算 节省空间
  12. indices = torch.from_numpy(np.asarray([normalize_adjacency.row,
  13. normalize_adjacency.col]).astype('int64')).long()
  14. values = torch.from_numpy(normalize_adjacency.data.astype(np.float32))
  15. tensor_adjacency = torch.sparse.FloatTensor(indices, values,
  16. (num_nodes, num_nodes)).to(DEVICE)
  • 模型定义 
  1. # 模型定义:Model, Loss, Optimizer
  2. model = GcnNet(input_dim).to(DEVICE) #如果gpu>1 用DataParallel()包裹 单机多卡 数据并行
  3. criterion = nn.CrossEntropyLoss().to(DEVICE) #多分类交叉熵损失
  4. optimizer = optim.Adam(model.parameters(),
  5. lr=LEARNING_RATE,
  6. weight_decay=WEIGHT_DACAY) #Adam优化器
  • 训练函数
  1. # 训练主体函数
  2. def train():
  3. loss_history = []
  4. val_acc_history = []
  5. model.train() #训练模式
  6. train_y = tensor_y[tensor_train_mask] #训练节点的标签
  7. for epoch in range(EPOCHS): #完整遍历一遍训练集 一个epoch做一次更新
  8. logits = model(tensor_adjacency, tensor_x) # 所有数据前向传播 (N,7)
  9. train_mask_logits = logits[tensor_train_mask] # 只选择训练节点进行监督
  10. loss = criterion(train_mask_logits, train_y) # 计算损失值
  11. optimizer.zero_grad() #清空梯度
  12. loss.backward() # 反向传播计算参数的梯度
  13. optimizer.step() # 使用优化方法进行梯度更新
  14. train_acc, _, _ = test(tensor_train_mask) # 计算当前模型训练集上的准确率
  15. val_acc, _, _ = test(tensor_val_mask) # 计算当前模型在验证集上的准确率
  16. # 记录训练过程中损失值和准确率的变化,用于画图
  17. loss_history.append(loss.item())
  18. val_acc_history.append(val_acc.item())
  19. print("Epoch {:03d}: Loss {:.4f}, TrainAcc {:.4}, ValAcc {:.4f}".format(
  20. epoch, loss.item(), train_acc.item(), val_acc.item()))
  21. return loss_history, val_acc_history
  • 测试函数
  1. # 测试函数
  2. def test(mask):
  3. model.eval() #测试模式
  4. with torch.no_grad(): #关闭求导
  5. logits = model(tensor_adjacency, tensor_x) #所有数据作前向传播
  6. test_mask_logits = logits[mask] #取出相应数据集对应的部分
  7. predict_y = test_mask_logits.max(1)[1] #按行取argmax 得到预测的标签
  8. accuarcy = torch.eq(predict_y, tensor_y[mask]).float().mean() #计算准确率
  9. return accuarcy, test_mask_logits.cpu().numpy(), tensor_y[mask].cpu().numpy()
  • 可视化函数
  1. #可视化训练集损失和验证集准确率变化
  2. def plot_loss_with_acc(loss_history, val_acc_history):
  3. fig = plt.figure()
  4. ax1 = fig.add_subplot(111)
  5. ax1.plot(range(len(loss_history)), loss_history,
  6. c=np.array([255, 71, 90]) / 255.)
  7. plt.ylabel('Loss')
  8. ax2 = fig.add_subplot(111, sharex=ax1, frameon=False)
  9. ax2.plot(range(len(val_acc_history)), val_acc_history,
  10. c=np.array([79, 179, 255]) / 255.)
  11. ax2.yaxis.tick_right()
  12. ax2.yaxis.set_label_position("right")
  13. plt.ylabel('ValAcc')
  14. plt.xlabel('Epoch')
  15. plt.title('Training Loss & Validation Accuracy')
  16. plt.show()
  • 训练
  1. loss, val_acc = train()#每个epoch 模型在训练集上的loss 和验证集上的准确率
  2. #计算最后训练好的模型在测试集上准确率
  3. test_acc, test_logits, test_label = test(tensor_test_mask)
  4. print("Test accuarcy: ", test_acc.item())
plot_loss_with_acc(loss, val_acc)

 

 

 

 

声明:本文内容由网友自发贡献,转载请注明出处:【wpsshop】
推荐阅读
相关标签
  

闽ICP备14008679号