图卷积网络(Graph Convolutional Network,GCN)是将卷积神经网络(Convolutional Neural Network,CNN)扩展到图结构数据的一种方法。在图结构数据中,节点和边都可以被视为数据点,而传统的神经网络只对向量或矩阵进行处理。因此,GCN成为了解决图结构数据上的分类、聚类和预测等问题的有效工具。本文将介绍使用PyTorch实现GCN的过程。
环境搭建
首先,需要安装Python 3.6及以上版本和PyTorch框架。可以通过以下命令安装:
pip install torch
然后还需要安装以下库:
- networkx:用于生成图结构数据。
- numpy:用于进行数据处理。
- scipy:用于处理稀疏矩阵。
pip install networkx numpy scipy
数据准备
在这里,我们将使用Cora数据集来进行实验。Cora是一个引文网络,其中每个节点代表一篇论文,每个边代表两篇论文之间的引用关系。每篇论文都有一个标签,表示它属于哪个领域,如计算机科学、物理等。我们的目标是根据节点特征和它们之间的连接模式,对每个节点进行分类。
下载Cora数据集并放到与脚本文件相同的目录下,数据集包含以下文件:
- cora.content:包含节点和特征信息。
- cora.cites:包含边的信息。
接下来,我们需要将数据加载到Python中。可以使用以下代码块实现:
import os
import networkx as nx
import numpy as np
import scipy.sparse as sp
def load_data(path="./cora"):
"""
加载Cora数据集
"""
# 读取特征和标签
features = np.genfromtxt(os.path.join(path, "cora.content"), dtype=np.float32, usecols=range(1, 1434))
labels = np.genfromtxt(os.path.join(path, "cora.content"), dtype=np.int64, usecols=-1)
# 创建图结构
edges_unordered = np.genfromtxt(os.path.join(path, "cora.cites"), dtype=np.int64)
edges = np.array(list(map(sorted, edges_unordered))))
idx_features_labels = np.genfromtxt(os.path.join(path, "cora.content"), dtype=np.dtype(str))
# 构建索引
idx = np.array(idx_features_labels[:, 0], dtype=np.int64)
# 构建特征矩阵
features = sp.csr_matrix(features[idx])
# 构建标签向量
labels = np.array(labels[idx])
# 构建邻接矩阵
adj = sp.coo_matrix((np.ones(edges.shape[0]), (edges[:, 0], edges[:, 1])), shape=(labels.shape[0], labels.shape[0]), dtype=np.float32)
# 对称归一化邻接矩阵
adj = normalize_adj(adj + sp.eye(adj.shape[0]))
# 将稀疏矩阵转换为张量
features = torch.FloatTensor(np.array(features.todense()))
labels = torch.LongTensor(labels)
adj = sparse_mx_to_torch_sparse_tensor(adj)
return adj, features, labels
def normalize_adj(mx):
"""
对称归一化邻接矩阵
"""
rowsum = np.array(mx.sum(1))
r_inv = np.power(rowsum, -1).flatten()
r_inv[np.isinf(r_inv)] = 0.
r_mat_inv = sp.diags(r_inv)
mx = r_mat_inv.dot(mx)
mx = mx.dot(r_mat_inv)
return mx
def sparse_mx_to_torch_sparse_tensor(sparse_mx):
"""
将稀疏矩阵转换为PyTorch的稀疏张量
"""
sparse_mx = sparse_mx.tocoo().astype(np.float32)
indices = torch.from_numpy(np.vstack((sparse_mx.row, sparse_mx.col))).long()
values = torch.from_numpy(sparse_mx.data)
shape = torch.Size(sparse_mx.shape)
return torch.sparse.FloatTensor(indices, values, shape)
模型定义
在Graph Convolutional Networks中,每个节点都会沿着边接收相邻节点的信息,并通过池化聚合这些信息。使用GCN时需要注意以下几点:
- 对输入进行归一化处理,以保证每层输出的范围和输入相同。
- 对每个节点的邻居节点进行池化操作,以获得相邻节点的聚合信息。
- 通过权重矩阵和输入特征计算节点的激活状态。
- 对节点激活状态进行归一化处理或使用激活函数来调整范围。
使用PyTorch实现GCN的代码:
首先,让我们导入所需的库:
import dgl
import numpy as np
import scipy.sparse as sp
import torch
import torch.nn as nn
import torch.nn.functional as F
from dgl.data import citation_graph as citegrh
from dgl.nn.pytorch import GraphConv
from sklearn.metrics import f1_score
接下来,我们将加载引文网络数据集,并将其转换为DGL图形对象:
data = citegrh.load_cora()
features = torch.FloatTensor(data.features)
labels = torch.LongTensor(data.labels)
mask = torch.BoolTensor(data.train_mask)
g = dgl.from_scipy(sp.coo_matrix(data.graph))
然后,我们创建模型类,并定义了一个包含两个GraphConv层的GCN模型:
class GCN(nn.Module):
def __init__(self, in_feats, h_feats, num_classes):
super(GCN, self).__init__()
self.conv1 = GraphConv(in_feats, h_feats)
self.conv2 = GraphConv(h_feats, num_classes)
def forward(self, g, x):
h = F.relu(self.conv1(g, x))
h = self.conv2(g, h)
return h
在这个模型中,我们将输入通过两个GraphConv层传递,其中第一个层将特征投影到隐藏维度(h_feats
),并使用ReLU作为激活函数。第二个层将输出投影到类别数(num_classes
),不使用激活函数。
接下来,我们定义模型的一些超参数,以及创建模型实例、优化器和损失函数:
in_feats = features.shape[1]
h_feats = 16
num_classes = data.num_labels
model = GCN(in_feats, h_feats, num_classes)
optimizer = torch.optim.Adam(model.parameters(), lr=0.01)
criterion = nn.CrossEntropyLoss()
然后,我们将数据传递到GPU,并开始训练模型:
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
model.to(device)
for epoch in range(200):
model.train()
logits = model(g.to(device), features.to(device))
loss = criterion(logits[mask], labels[mask])
optimizer.zero_grad()
loss.backward()
optimizer.step()
model.eval()
with torch.no_grad():
logits = model(g.to(device), features.to(device))
pred = logits.argmax(1).cpu().numpy()
val_acc = f1_score(labels[data.val_mask].numpy(), pred[data.val_mask], average='micro')
print(f'Epoch {epoch + 1}: Loss {loss.item():.4f}, Val F1: {val_acc:.4f}')
在每个epoch中,我们首先将模型的训练状态设置为“训练”,然后计算出当前批次的输出(即网络预测)。然后,我们计算该批次的交叉熵损失和梯度,然后执行反向传播并使用Adam优化器更新模型权重。最后,我们将模型状态设置为“评估”,计算模型的验证集F1分数,并记录训练过程中的损失和F1分数。
最后,我们可以使用测试集评估模型:
model.eval()
with torch.no_grad():
logits = model(g.to(device), features.to(device))
pred = logits.argmax(1).cpu().numpy()
test_acc = f1_score(labels[data.test_mask].numpy(), pred[data.test_mask], average='micro')
print(f'Test F1: {test_acc:.4f}')