PyG搭建异质图注意力网络HAN实现DBLP节点分类

20 篇文章 35 订阅

前言

HAN的原理请见:WWW 2019 | HAN:异质图注意力网络

数据处理

导入数据:

path = os.path.abspath(os.path.dirname(os.getcwd())) + '\data\DBLP'
dataset = DBLP(path)
graph = dataset[0]
print(graph)

输出如下:

HeteroData(
  author={
    x=[4057, 334],
    y=[4057],
    train_mask=[4057],
    val_mask=[4057],
    test_mask=[4057]
  },
  paper={ x=[14328, 4231] },
  term={ x=[7723, 50] },
  conference={ num_nodes=20 },
  (author, to, paper)={ edge_index=[2, 19645] },
  (paper, to, author)={ edge_index=[2, 19645] },
  (paper, to, term)={ edge_index=[2, 85810] },
  (paper, to, conference)={ edge_index=[2, 14328] },
  (term, to, paper)={ edge_index=[2, 85810] },
  (conference, to, paper)={ edge_index=[2, 14328] }
)

可以发现,DBLP数据集中有作者(author)、论文(paper)、术语(term)以及会议(conference)四种类型的节点。DBLP中包含14328篇论文(paper), 4057位作者(author), 20个会议(conference), 7723个术语(term)。作者分为四个领域:数据库、数据挖掘、机器学习、信息检索。

任务:对author节点进行分类,一共4类。

由于conference节点没有特征,因此需要预先设置特征:

graph['conference'].x = torch.ones((graph['conference'].num_nodes, 1))

所有conference节点的特征都初始化为[1]

获取一些有用的数据:

num_classes = torch.max(graph['author'].y).item() + 1
train_mask, val_mask, test_mask = graph['author'].train_mask, graph['author'].val_mask, graph['author'].test_mask
y = graph['author'].y

模型搭建

首先导入包:

from torch_geometric.nn import HANConv

模型参数:
在这里插入图片描述

  1. in_channels:输入通道,比如节点分类中表示每个节点的特征数,一般设置为-1。
  2. out_channels:输出通道,最后一层HANConv的输出通道为节点类别数(节点分类)。
  3. heads:多头注意力机制中的头数。值得注意的是,HANConv和HANConv不一样的地方在于,HANConv模型是把多头注意力的结果直接展平,而不是进行concat操作。
  4. negative_slope:LeakyReLU的参数。

于是模型搭建如下:

class HAN(nn.Module):
    def __init__(self, in_channels, hidden_channels, out_channels):
        super(HAN, self).__init__()
        # H, D = self.heads, self.out_channels // self.heads
        self.conv1 = HANConv(in_channels, hidden_channels, graph.metadata(), heads=8)
        self.conv2 = HANConv(hidden_channels, out_channels, graph.metadata(), heads=4)

    def forward(self, data):
        x_dict, edge_index_dict = data.x_dict, data.edge_index_dict
        x = self.conv1(x_dict, edge_index_dict)
        x = self.conv2(x, edge_index_dict)
        x = x['author']
        return x

输出一下模型:

model = HAN(-1, 64, num_classes).to(device)
HAN(
  (conv1): HANConv(64, heads=8)
  (conv2): HANConv(4, heads=4)
)

1. 前向传播

查看官方文档中HANConv的输入输出要求:
在这里插入图片描述
可以发现,HANConv中需要输入的是节点特征字典x_dict和邻接关系字典edge_index_dict

因此有:

x_dict, edge_index_dict = data.x_dict, data.edge_index_dict
x = self.conv1(x_dict, edge_index_dict)

此时我们不妨输出一下x['author']及其size:

tensor([[0.0000, 0.0000, 0.0000,  ..., 0.0000, 0.0000, 0.0000],
        [0.0969, 0.0601, 0.0000,  ..., 0.0000, 0.0000, 0.0251],
        [0.0000, 0.0000, 0.0000,  ..., 0.1288, 0.0000, 0.0602],
        ...,
        [0.0000, 0.0000, 0.0000,  ..., 0.0096, 0.0000, 0.0240],
        [0.0000, 0.0000, 0.0000,  ..., 0.0096, 0.0000, 0.0240],
        [0.0801, 0.0558, 0.0837,  ..., 0.0277, 0.0347, 0.0000]],
       device='cuda:0', grad_fn=<SumBackward1>)
torch.Size([4057, 64])

此时的x一共4057行,每一行表示一个author节点经过第一层卷积更新后的状态向量。

那么同理,由于:

x = self.conv2(x, edge_index_dict)

所以经过第二层卷积后得到的x['author']的size应该为:

torch.Size([4057, 4])

即每个author节点的维度为4的状态向量,表示4个类别各自的概率。

2. 反向传播

在训练时,我们首先利用前向传播计算出输出:

f = model(graph)

f即为最终得到的每个节点的4个概率值,但在实际训练中,我们只需要计算出训练集的损失,所以损失函数这样写:

loss = loss_function(f[train_mask], y[train_mask])

然后计算梯度,反向更新!

3. 训练

训练时返回验证集上表现最优的模型:

def train():
    model = HAN(-1, 64, num_classes).to(device)
    print(model)
    optimizer = torch.optim.Adam(model.parameters(), lr=0.01, weight_decay=1e-4)
    loss_function = torch.nn.CrossEntropyLoss().to(device)
    min_epochs = 5
    best_val_acc = 0
    final_best_acc = 0
    model.train()
    for epoch in range(100):
        f = model(graph)
        loss = loss_function(f[train_mask], y[train_mask])
        optimizer.zero_grad()
        loss.backward()
        optimizer.step()
        # validation
        val_acc, val_loss = test(model, val_mask)
        test_acc, test_loss = test(model, test_mask)
        if epoch + 1 > min_epochs and val_acc > best_val_acc:
            best_val_acc = val_acc
            final_best_acc = test_acc
        print('Epoch {:3d} train_loss {:.5f} val_acc {:.3f} test_acc {:.3f}'
              .format(epoch, loss.item(), val_acc, test_acc))

    return final_best_acc

4. 测试

def test(model, mask):
    model.eval()
    with torch.no_grad():
        out = model(graph)
        loss_function = torch.nn.CrossEntropyLoss().to(device)
        loss = loss_function(out[mask], y[mask])
    _, pred = out.max(dim=1)
    correct = int(pred[mask].eq(y[mask]).sum().item())
    acc = correct / int(test_mask.sum())

    return acc, loss.item()

实验结果

数据集采用DBLP网络,训练100轮,分类正确率为78.54%:

HAN Accuracy: 0.7853853239177156

完整代码

代码地址:GNNs-for-Node-Classification。原创不易,下载时请给个follow和star!感谢!!

  • 23
    点赞
  • 45
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 12
    评论
好的,以下是基于PYG搭建的GAT实现分类代码示例: ```python import torch import torch.nn.functional as F from torch_geometric.nn import GATConv class GAT(torch.nn.Module): def __init__(self, num_features, num_classes, num_heads): super(GAT, self).__init__() self.conv1 = GATConv(num_features, 8, heads=num_heads, dropout=0.6) self.conv2 = GATConv(8*num_heads, num_classes, dropout=0.6) def forward(self, x, edge_index): x = F.dropout(x, p=0.6, training=self.training) x = F.elu(self.conv1(x, edge_index)) x = F.dropout(x, p=0.6, training=self.training) x = self.conv2(x, edge_index) return F.log_softmax(x, dim=1) ``` 说明: - `num_features`:输入特征的维度; - `num_classes`:分类的类别数; - `num_heads`:GAT中的头数(默认为8); - `self.conv1`:GAT的第一层; - `self.conv2`:GAT的第二层; - `forward`:前向传播函数,其中包括两层GAT和一个log_softmax层。 使用方法: ```python import torch from torch_geometric.datasets import Planetoid import torch_geometric.transforms as T from torch_geometric.data import DataLoader from GAT import GAT # 加载数据集 dataset = Planetoid(root='data/Cora', name='Cora', transform=T.NormalizeFeatures()) loader = DataLoader(dataset, batch_size=32, shuffle=True) # 初始化模型和优化器 device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') model = GAT(num_features=dataset.num_features, num_classes=dataset.num_classes, num_heads=8).to(device) optimizer = torch.optim.Adam(model.parameters(), lr=0.005, weight_decay=5e-4) # 训练模型 model.train() for epoch in range(200): for batch in loader: batch = batch.to(device) optimizer.zero_grad() out = model(batch.x, batch.edge_index) loss = F.nll_loss(out[batch.train_mask], batch.y[batch.train_mask]) loss.backward() optimizer.step() # 测试模型 model.eval() correct = 0 for batch in loader: batch = batch.to(device) with torch.no_grad(): pred = model(batch.x, batch.edge_index).max(dim=1)[1] correct += pred.eq(batch.y).sum().item() print(f"Accuracy: {correct / len(dataset)}") ``` 说明: - `Planetoid`:PyG中一个内置的数据集,用于分类任务; - `T.NormalizeFeatures()`:用于归一化特征矩阵; - `DataLoader`:数据加载器,用于批量加载数据集; - `F.nll_loss`:负对数似然损失函数; - `batch.train_mask`:训练集节点的掩码; - `batch.y`:节点的真实标签; - `pred.eq(batch.y).sum().item()`:计算预测正确的节点数。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 12
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Cyril_KI

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值