数据集采用的是Cora数据集
首先导入所需的库
import itertools
import os
import os.path as osp
import pickle
import urllib
from collections import namedtuple
import numpy as np
import scipy.sparse as sp
import torch
import torch.nn as nn
import torch.nn.functional as F
import torch.nn.init as init
import torch.optim as optim
import matplotlib.pyplot as plt
创建一个类CoraData对数据进行预处理
Data = namedtuple('Data', ['x', 'y', 'adjacency','train_mask', 'val_mask', 'test_mask'])
def tensor_from_numpy(x,device):
return torch.from_numpy(x).to(device)
class CoraData(object):
download_url = "https://raw.githubusercontent.com/kimiyoung/planetoid/master/data"
filenames = ["ind.cora.{}".format(name) for name in ['x', 'tx', 'allx', 'y', 'ty', 'ally', 'graph', 'test.index']]
def __init__(self, data_root="cora", rebuild=False):
self.data_root = data_root
save_file = osp.join(self.data_root, "processed_cora.pkl")
if osp.exists(save_file) and not rebuild:
print("Using Cached file: {}".format(save_file))
self._data = pickle.load(open(save_file, "rb"))
else:
self.maybe_download()
self._data = self.process_data()
with open(save_file, "wb") as f:
pickle.dump(self.data, f)
print("Cached file: {}".format(save_file))
def data(self):
return self._data
def process_data(self):
print("Process data ...")
_, tx, allx, y, ty, ally, graph, test_index = [self.read_data(osp.join(self.data_root, "raw", name)) for name in self.filenames]
train_index = np.arange(y.shape[0])
val_index = np.arange(y.shape[0], y.shape[0] + 500)
sorted_test_index = sorted(test_index)
x = np.concatenate((allx, tx), axis=0)
y = np.concatenate((ally, ty), axis=0).argmax(axis=1)
x[test_index] = x[sorted_test_index]
y[test_index] = y[sorted_test_index]
num_nodes = x.shape[0]
train_mask = np.zeros(num_nodes, dtype=np.bool)
val_mask = np.zeros(num_nodes, dtype=np.bool)
test_mask = np.zeros(num_nodes, dtype=np.bool)
train_mask[train_index] = True
val_mask[val_index] = True
test_mask[test_index] = True
adjacency = self.build_adjacency(graph)
print("Node's feature shape: ", x.shape)
print("Node's label shape: ", y.shape)
print("Adjacency's shape: ", adjacency.shape)
print("Number of training nodes: ", train_mask.sum())
print("Number of validation nodes: ", val_mask.sum())
print("Number of test nodes: ", test_mask.sum())
return Data(x=x, y=y, adjacency=adjacency,train_mask=train_mask, val_mask=val_mask, test_mask=test_mask)
def maybe_download(self):#返回最后文件名,最后的/后面的
save_path = os.path.join(self.data_root,"raw")
for name in self.filenames:
if not osp.exists(osp.join(save_path,name)):
self.download_data("{}/{}".format(self.download_url,name),save_path)
def build_adjacency(adj_dict):
edge_index = []
num_nodes = len(adj_dict)
for src,dst in adj_dict.items():
edge_index.extend([src,v] for v in dst)
edge_index.extend([v,src] for v in dst) #去除重复的边
edge_index = list(k for k, _ in itertools.groupby(sorted(edge_index)))#相同的归为一组
edge_index = np.asarray(edge_index)#变成数组
adjacency = sp.coo_matrix((np.ones(len(edge_index)),(edge_index[:,0],edge_index[:,1])),shape = (num_nodes,num_nodes),dtype = "float32")#构造稀疏矩阵
return adjacency
def read_data(path):
name = osp.basename(path)
if name == "ind.cora.test.index":
out = np.genfromtxt(path,dtype="int64")
return out
else:
out = pickle.load(open(path,"rb"),encoding = "latin1")
out = out.toarray() if hasattr(out,"toarray") else out
return out
def download_data(url, save_path):
if not os.path.exists(save_path):
os.makedirs(save_path)
data = urllib.request.urlopen(url)
filename = os.path.split(url)[-1]
with open(os.path.join(save_path, filename), 'wb') as f:
f.write(data.read())
return True
def normalization(adjacency):
adjacency += sp.eye(adjacency.shape[0])
degree = np.array(adjacency.sum(1)) #此时的度矩阵的对角线的值 为 邻接矩阵 按行求和
d_hat = sp.diags(np.power(degree, -0.5).flatten()) #对度矩阵对角线的值取-0.5次方 再转换为对角矩阵
return d_hat.dot(adjacency).dot(d_hat).tocoo()
图卷积层定义
class GraphConvolution(nn.Module):
def __init__(self, input_dim, output_dim, use_bias=True):
super(GraphConvolution,self).__init__() self.input_dim = input_dim
self.output_dim = output_dim self.use_bias = use_bias
self.weight = nn.Parameter(torch.Tensor(input_dim,output_dim))
if self.use_bias:
self.bias = nn.Parameter(torch.Tensor(output_dim))
else:
self.register_parameter('bias',None)
self.reset_parameters()
def reset_parameters(self):
init.kaiming_uniform_(self.weight)
if self.use_bias:
init.zeros_(self.bias)
def forward(self,adjacency,input_feature):
support = torch.mm(input_feature,self.weight)
output = torch.sparse.mm(adjacency,support)
if self.use_bias:
output += self.bias
return output
def __repr__(self):
return self.__class__.__name__ + ' (' + str(self.in_features) + ' -> ' + str(self.output_dim) + ')'
模型定义
class GcnNet(nn.Module):
""" 定义一个包含两层GraphConvolution的模型 """
def __init__(self,input_dim = 1433):
super(GcnNet,self).__init__()
self.gcn1 = GraphConvolution(input_dim, 16)
self.gcn2 = GraphConvolution(16,7)
def forwaer(self,adjacency,feature):
h = F.relu(self.gcn1(adjacency,feature))
logits = self.gcn2(adjacency,h)
return logits
模型训练
learning_rate = 0.1
weight_decay = 5e-4#权重衰减
8epochs = 200
device = "cuda" if torch.cuda.is_available() else "cpu"
model = GcnNet().to(device)
criterion = nn.CrossEntropyLoss().to(device)
optimizer = optim.Adam(model.parameters(),lr = learning_rate,weight_decay = weight_decay)
dataset = CoraData().datax = dataset.x / dataset.x.sum(1,keepdims = True)# 归一化数据,使得每一行和为1,数据归一化后,最优解的寻优过程明显会变得平缓,更容易正确的收敛到最优解。,sum(1),1是按行,0是按列
tensor_x = torch.from_numpy(x).to(device)、
tensor_y = torch.from_numpy(dataset.y).to(device)
tensor_train_mask = torch.from_numpy(dataset.train_mask).to(device)
tensor_val_mask = torch.from_numpy(dataset.val_mask).to(device)
tensor_test_mask = torch.from_numpy(dataset.test_mask).to(device)
normalize_adjacency = CoraData.normalization(dataset.adjacency)# 规范化邻接矩阵
indices = torch.from_numpy(np.asarray([normalize_adjacency.row,normalize_adjacency.col]).astype('int64')).long
values = torch.from_numpy(normalize_adjacency.data.astype(np.float32))
tensor_adjacency = torch.sparse.FloatTensor(indices,values,(2708,2708)).to(device)
def train():
loss_history = []
val_acc_history = []
model.train()
train_y = tensor_y[tensor_train_mask]
for epoch in range(epochs):
logits = model(tensor_adjacency, tensor_x) # 前向传播
train_mask_logits = logits[tensor_train_mask] # 只选择训练节点进行监督
loss = criterion(train_mask_logits,train_y)
optimizer.zero_grad()
loss.backward()# 反向传播计算参数的梯度
optimizer.step() # 使用优化方法进行梯度更新,更新学习率
train_acc, _, _ = tt(tensor_train_mask) # 计算当前模型训练集上的准确率
val_acc,_,_ = tt(tensor_val_mask) # 记录训练过程中损失值和准确率的变化,用于画图
loss_history.append(loss.item())
val_acc_history.append(val_acc.item())
print("Epoch {:03d}: Loss {:.4f}, TrainAcc {:.4}, ValAcc {:.4f}".format(epoch, loss.item(), train_acc.item(), val_acc.item()))
def tt(mask):
model.eval()
with torch.no_grad():
logits = model(tensor_adjacency,tensor_x)
test_mask_logits = logits[mask]
predict_y = test_mask_logits.max(1)[1]
accuarcy = torch.eq(predict_y,tensor_y[mask]).float.mean()
return accuarcy,test_mask_logits.cpu().numpy(),tensor_y[mask].cpu().numpy()
loss,val_acc = train()
print(tensor_test_mask)
test_acc,test_logits,test_label = tt(tensor_test_mask)
print("Test accuarcy: ", test_acc.item())
实验结果