Vanilla Graph Neural Networks(GNN)

概要:在本文中,我将介绍图形数据集Cora,同时利用Gephi处理数据集中的节点特征。之后,我们尝试在神经网络中包含拓扑信息(topological infomation),并比较加入拓扑信息前后的神经网络的性能。


一、Cora数据集介绍

Cora数据集包括2708份科学出版物,分为7类。引文网络由5429个链接组成。数据集中的每个发布都用一个0/1值的词向量来描述,该词向量表示字典中相应词的缺失/存在。这部词典由1433个独特的单词组成。
Cora_nodes

图1 Cora nodes

Cora Graph Photo

图2 Cora Graph Visualize

提示:以下是本篇文章正文内容,下面案例可供参考

二、GNN架构

理解GNN架构首先需要理解多层感知机(MLP)在简单线性层的定义。
一个基本的神经网络对应于一个线性变换:
h A = x A W T h_A=x_AW^T hA=xAWT
式中, x A x_A xA是输入的节点向量, W W W是权重矩阵。
对于图数据集,输入向量是节点特征,这意味着节点之间是完全分离的。但是,我们需要考虑节点的上下文对节点的作用。因此,我们需要查看节点的邻域来帮助我们更好的进行节点分类。
定义 N A N_A NA为节点 A A A的邻域。进而,我们图线性变换层可以写为:
h A = ∑ i ∈ N A x i W T h_A=\sum_{i\in N_A}{x_iW^T} hA=iNAxiWT
将其转变为神经网络的矩阵运算格式,则为:
H = A ~ T X W T H=\tilde{A}^TXW^T H=A~TXWT
式中: A ~ = A + I \tilde{A}=A+I A~=A+I 表示邻接矩阵和自循环矩阵之和

三、代码解析

首先,我们利用MLP架构对节点进行分类。之后,利用GNN架构进行分类,并比较加入邻接矩阵前后模型的性能。

1、数据库导入

import torch
import torch.nn.functional as F
import pandas as pd
import csv
from torch.nn import Linear
from torch_geometric.datasets import Planetoid
from torch_geometric.datasets import FacebookPagePage
from torch_geometric.utils import to_dense_adj

torch.manual_seed(0)

2、MLP架构

定义准确度函数以及构建MLP架构。

# Model Create
def accuracy(y_pred, y_true):
    """Calculate accuracy."""
    return torch.sum(y_pred == y_true) / len(y_true)


class MLP(torch.nn.Module):
    """Multilayer Perceptron"""
    # the number of neurons in the input, hidden and output layers
    def __init__(self, dim_in, dim_h, dim_out):
        super().__init__()
        self.linear1 = Linear(dim_in, dim_h)
        self.linear2 = Linear(dim_h, dim_out)

    def forward(self, x):
        x = self.linear1(x)
        x = torch.relu(x)
        x = self.linear2(x)
        return F.log_softmax(x, dim=1)

    def fit(self, data, epochs):
        # initialize a loss function and an optimizer
        criterion = torch.nn.CrossEntropyLoss()
        optimizer = torch.optim.Adam(self.parameters(),
                                          lr=0.01,
                                          weight_decay=5e-4)

        self.train()

        losses = []
        accs = []
        val_losses = []
        val_accs = []
        for epoch in range(epochs+1):
            optimizer.zero_grad()
            out = self(data.x)
            loss = criterion(out[data.train_mask], data.y[data.train_mask])
            acc = accuracy(out[data.train_mask].argmax(dim=1),
                          data.y[data.train_mask])
            loss.backward()
            optimizer.step()

            losses.append(loss)
            accs.append(acc)

            val_loss = criterion(out[data.val_mask], data.y[data.val_mask])
            val_acc = accuracy(out[data.val_mask].argmax(dim=1),
                              data.y[data.val_mask])
            print(f'Epoch {epoch:>3} | Train Loss: {loss:.3f} | Train Acc:'
                  f' {acc*100:>5.2f}% | Val Loss: {val_loss:.2f} | '
                  f'Val Acc: {val_acc*100:.2f}%')

            val_losses.append(val_loss)
            val_accs.append(val_acc)

        self.train_loss = losses
        self.train_acc = accs
        self.val_loss = val_losses
        self.val_acc = val_accs

    @torch.no_grad()
    def test(self, data):
        self.eval()
        out = self(data.x)
        acc = accuracy(out.argmax(dim=1)[data.test_mask], data.y[data.test_mask])
        return acc

在上述MLP类中,首先初始化了两层线性层,并分别定义了它们的输入输出大小。其次在 f o r w a r d forward forward函数中,分别定义激活函数为 r e l u relu relu l o g _ s o f t m a x log\_softmax log_softmax,最后在 f i t fit fit函数中,定义了损失函数和优化器。

3、训练MLP模型

# Create MLP model
mlp = MLP(dataset.num_features, 16, dataset.num_classes)
print(mlp)

# Train
mlp.fit(data, epochs=100)

# Test
acc = mlp.test(data)
print(f'\nMLP test accuracy: {acc*100:.2f}%')

# plot
plt.plot(range(101), np.array(mlp.train_loss), label="Training loss")
plt.plot(range(101), np.array(mlp.val_loss), ":", label="Val loss")
plt.title("Training and validation loss")
plt.style.use('seaborn-colorblind')
plt.xlabel("Epochs")
plt.ylabel("Loss")
plt.legend()
plt.show()

plt.plot(range(101), np.array(mlp.train_acc), label="Training acc")
plt.plot(range(101), np.array(mlp.val_acc), ':', label="Val acc")
plt.style.use('seaborn-colorblind')
plt.title("Training and validation acc")
plt.xlabel("Epochs")
plt.ylabel("acc")
plt.legend()
plt.show()

保存训练过程中的 l o s s loss loss a c c acc acc以便于可视化,通过观察验证集 l o s s loss loss a c c acc acc的变化防止过拟合的发生。
训练后得到的结果如图3所示:

图3 训练集和验证集的loss和val曲线

从图3中可以看出,MLP架构的训练结果较差,验证集的准确率只能达到45%,同时在训练次数达到18次左右的时候,训练集的准确率就不在变化。
综上,模型在一定程度上有过拟合的现象发生,模型参数在后续的训练中变化幅度较小。

4、GNN架构

首先建立GNN架构中关于邻接矩阵运算的函数。

class VanillaGNNLayer(torch.nn.Module):
    def __init__(self, dim_in, dim_out):
        super().__init__()
        # bias=False: which means without bias
        self.linear = Linear(dim_in, dim_out, bias=False)

    def forward(self, x, adjacency):
        x = self.linear(x)
        x = torch.sparse.mm(adjacency, x)
        return x


# adjacency matrix
# The two lines correspond one to one
# [[[ ]]] --> [0] --> [[ ]]
adjacency = to_dense_adj(data.edge_index)[0]
# self loops
# sparse matrix
adjacency += torch.eye(len(adjacency))

在上述类中,我们构建了线性模型,并将线性偏置去除,同时用 t o r c h . s p a r s e . m m torch.sparse.mm torch.sparse.mm将邻接矩阵加入到运算中。
在程序运行部分,我们用 t o r c h torch torch中的 t o _ d e n s e _ a d j to\_dense\_adj to_dense_adj函数将相邻节点的对应关系转化为邻接矩阵的形式,并且通过 e y e eye eye函数引入自循环。

GNN模型的建立

class VanillaGNN(torch.nn.Module):
    """Vanilla Graph Neural Network"""
    def __init__(self, dim_in, dim_h, dim_out):
        super().__init__()
        # without bias and connect the adjacency matrix
        self.gnn1 = VanillaGNNLayer(dim_in, dim_h)
        self.gnn2 = VanillaGNNLayer(dim_h, dim_out)

    def forward(self, x, adjacency):
        h = self.gnn1(x, adjacency)
        h = torch.relu(h)
        h = self.gnn2(h, adjacency)
        return F.log_softmax(h, dim=1)

    def fit(self, data, epochs):
        criterion = torch.nn.CrossEntropyLoss()
        optimizer = torch.optim.Adam(self.parameters(),
                                      lr=0.01,
                                      weight_decay=5e-4)

        self.train()
        losses = []
        accs = []
        val_losses = []
        val_accs = []
        
        for epoch in range(epochs+1):
            optimizer.zero_grad()
            out = self(data.x, adjacency)
            loss = criterion(out[data.train_mask], data.y[data.train_mask])
            acc = accuracy(out[data.train_mask].argmax(dim=1),
                          data.y[data.train_mask])
            loss.backward()
            optimizer.step()

            val_loss = criterion(out[data.val_mask], data.y[data.val_mask])
            val_acc = accuracy(out[data.val_mask].argmax(dim=1),
                              data.y[data.val_mask])
            print(f'Epoch {epoch:>3} | Train Loss: {loss:.3f} | Train Acc:'
                  f' {acc*100:>5.2f}% | Val Loss: {val_loss:.2f} | '
                  f'Val Acc: {val_acc*100:.2f}%')
            
            losses.append(loss)
            accs.append(acc)
            val_losses.append(val_loss)
            val_accs.append(val_acc)
            
        self.train_loss = losses
        self.train_acc = accs
        self.val_loss = val_losses
        self.val_acc = val_accs

    @torch.no_grad()
    def test(self, data):
        self.eval()
        out = self(data.x, adjacency)
        acc = accuracy(out.argmax(dim=1)[data.test_mask], data.y[data.test_mask])
        return acc

GNN模型同MLP模型相类似,在此不多赘述。

5、训练GNN模型

# Create the Vanilla GNN model
gnn = VanillaGNN(dataset.num_features, 16, dataset.num_classes)
print(gnn)

# Train
gnn.fit(data, epochs=100)

# Test
acc = gnn.test(data)
print(f'\nGNN test accuracy: {acc*100:.2f}%')

# plot
plt.plot(range(101), np.array(gnn.train_loss), label="Training loss")
plt.plot(range(101), np.array(gnn.val_loss), ":", label="Val loss")
plt.title("GNN Training and validation loss")
plt.style.use('seaborn-colorblind')
plt.xlabel("Epochs")
plt.ylabel("Loss")
plt.legend()
plt.show()

plt.plot(range(101), np.array(gnn.train_acc), label="Training acc")
plt.plot(range(101), np.array(gnn.val_acc), ':', label="Val acc")
plt.style.use('seaborn-colorblind')
plt.title("GNN Training and validation acc")
plt.xlabel("Epochs")
plt.ylabel("acc")
plt.legend()
plt.show()

模型训练结果如图4所示:

图4 GNN训练集和验证集的loss和val曲线
从图4中可以看出,训练次数在18次左右的时候,模型的性能是最优的,因此我们可以通过早停或者手动设置训练次数的方法来停止训练,避免模型过度训练导致的过拟合。将图3和图4比较可以发现,加入邻接矩阵后,模型在验证集上的准确率由45%提高到了75%,模型性能有较大的改善。

6、Tensorflow库

前文的MLP模型,我们采用 t o r c h torch torch库中关于神经网络的架构。在此,我们更改为 t e n s o r f l o w tensorflow tensorflow库,比较模型的结果。

# 二分类用交叉熵作为损失函数
model.compile(optimizer="rmsprop",
              loss="categorical_crossentropy",
              metrics=["accuracy"])

# 训练模型
history = model.fit(X_train,
                    y_train,
                    epochs=40,
                    batch_size=128,
                    validation_data=(X_val, y_val))

上述是 t e n s o r f l o w tensorflow tensorflow神经网络模型的主要架构。
模型的训练结果如图5所示:

图5 训练集和验证集的loss和val曲线

从图5中我们可以看出,同样是MLP模型,利用 t e n s o r f l o w tensorflow tensorflow库训练的神经网络其验证集的准确率可以达到70%。
利用上述训练好的模型可以得到测试集的混淆矩阵如图6所示。

图6 测试集混淆矩阵
同时测试集的准确率为75%。

总结

在本章中,我们了解了MLP和GNN之间不同的环节。我们用我们的直觉和一点线性代数建立了我们自己的GNN架构。我们从科学文献中探索了流行的图数据集来比较我们的两种架构。最后,我们在Python中实现了它们,并评估了它们的性能。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值