图神经网络初步实验

实验复现来源

https://zhuanlan.zhihu.com/p/603486955

该文章主要解决问题:

1.加深对图神经网络数据集的理解

2.加深对图神经网络模型中喂数据中维度变化的理解

原理问题在另一篇文章分析:
介绍数据集:cora数据集

其中的主要内容表示为一堆文章,有自己的特征内容,有自己的编号,有自己的类别(标签),相互引用的关系构成了图。

cora.content:包含特征编号,特征内容,特征类别(标签)

31336	0	0	0	0	0	0 ....0 Neural_Networks
1061127	0	0	0	0	0	0 ....0 Rule_Learning
1106406	0	0	0	0	0	0 ....0 Reinforcement_Learning
13195	0	0	0	0	0	0 ....0 Reinforcement_Learning
37879	0	0	0	0	0	0 ....0 Probabilistic_Methods

1.其中左面第一列表示特征编号

2.中间的内容表示特征内容(1433维)

3.右面的最后一列表示标签

cora.cite:引用关系,也称作边

35	1033
35	103482
35	103515
35	1050679
35	1103960
35	1103985
35	1109199
35	1112911

左面第一列表示起始点(序号),右面表示终止点(序号),其中一行表示一个边,表示两个点的连接

以点作为主要特征进行分类

首先先看一下GCN网络的参数部分

self.conv1 = GCNConv(in_channels=16, out_channels=32, add_self_loops=True, normalize=True)

主要参数就是输入的维度,输出的维度

# 前向传播时调用
output = self.conv1(x, edge_index, edge_weight)

主要的参数为结点的特征矩阵与图的连接关系

也就是说数据需要预处理成结点的特征矩阵,然后单独的标签,再预处理出图的连接关系

分为三个部分。

1.数据预处理

from plistlib import Data
from torch_geometric.data import Data
import torch
#print(torch.__version__)
import torch.nn.functional as F
# import sys
# print(sys.executable)
# import torch_geometric
# print(torch_geometric.__version__)
datasetPath = 'E:/pytorch/pytorch exercise/Graph neural network/Cora dataset/cora'
node_feature_file = 'E:/pytorch/pytorch exercise/Graph neural network/Cora dataset/cora/Cora.content'
edge_file = 'E:/pytorch/pytorch exercise/Graph neural network/Cora dataset/cora/Cora.cites'
label_mapping = {}
node_features = []
node_labels = []
node_ids = {}  #特征数
# 定义一个计数器,遍历所有可能的标签
current_label = 0

with open(node_feature_file,'r') as f:
    for line in f:
        parts = line.strip().split('\t')
        node_id = int(parts[0])
        features = list(map(float, parts[1:-1]))  # 特征
        label_str = parts[-1]
        if label_str not in label_mapping:
            label_mapping[label_str] = current_label
            current_label +=1
            # 将标签转换为整数
        label = label_mapping[label_str]
        node_ids[node_id] = len(node_features) #补充结点索引
        node_features.append(features)     #将节点特征依次按照数量拼接在一起
        node_labels.append(label)
#print(node_ids)
# 将节点特征和标签转换为 tensor
node_features = torch.tensor(node_features, dtype=torch.float)
# 输出张量的形状
print(node_features.shape)
# 或者使用 .size() 也能得到相同的结果
print(node_features.size())

node_labels = torch.tensor(node_labels, dtype=torch.long)
print("node_labels size = ",node_labels.size())
edge_index = []
with open(edge_file, 'r') as f:
    for line in f:
        parts = line.strip().split('\t')
        source = int(parts[0])  # 源节点
        target = int(parts[1])  # 目标节点

        source_idx = node_ids[source]  # 获取节点ID的索引
        target_idx = node_ids[target]
        edge_index.append([source_idx, target_idx])#引用边的信息,生成边的索引集合
# print(source_idx)
# print(target_idx)
edge_index = torch.tensor(edge_index, dtype=torch.long).t().contiguous()
print("edge_index size = ",edge_index.size())
#print(edge_index.shape())
data = Data(x=node_features, edge_index=edge_index, y=node_labels)
# 输出数据的一些信息
print(f'节点特征矩阵 shape: {data.x.shape}')
print(f'边的连接关系 (edge_index) shape: {data.edge_index.shape}')
print(f'节点标签 shape: {data.y.shape}')

# 输出第一个节点的特征和标签
print(f'节点 0 的特征: {data.x[0]}')
print(f'节点 0 的标签: {data.y[0]}')









其中

node_features表示所有点的特征结合在一起
node_labels表示所有标签集中在一起
node_ids表示特征点的个数

首先是从数据集中抽取特征矩阵的过程

with open(node_feature_file,'r') as f:  #打开文件
    for line in f:                      #按照行为单位,开始进行遍历
        parts = line.strip().split('\t')#删除其他空格与回车
        node_id = int(parts[0])        #将第一个元素放入node_id
        features = list(map(float, parts[1:-1]))  # 将第二个到倒数第二个元素一并放入features
        label_str = parts[-1]                #最后一个元素放入标签
        if label_str not in label_mapping:    #处理标签为null的情况
            label_mapping[label_str] = current_label
            current_label +=1
            # 将标签转换为整数
        label = label_mapping[label_str]    
        node_ids[node_id] = len(node_features) #补充结点索引
#为新的node_id分配一个新的整数索引,比如第一个元素node-id=35422,那么就是node_ids[35422] = 1
#也就是为第一个名字为35422的节点编辑了一个序号1,表示第一个元素


        node_features.append(features)     #将节点特征依次按照数量拼接在一起
        node_labels.append(label)           #拼接标签到一个集合中 

从数据集中提取边的集合

edge_index = []
with open(edge_file, 'r') as f:
    for line in f:
        parts = line.strip().split('\t')
        source = int(parts[0])  # 源节点
        target = int(parts[1])  # 目标节点

        source_idx = node_ids[source]  # 获取节点ID的索引
        target_idx = node_ids[target]
        edge_index.append([source_idx, target_idx])#引用边的信息,生成边的索引集合

转换成data对象

edge_index = torch.tensor(edge_index, dtype=torch.long).t().contiguous()
data = Data(x=node_features, edge_index=edge_index, y=node_labels)

简易的模型

class Net(torch.nn.Module):
    def __init__(self):
        super(Net, self).__init__()
        self.conv1 = GCNConv(data.x.size(1), 16)  # 输入特征维度是 data.x.size(1),输出 16 个特征

        # 计算类别数,假设 data.y 是节点标签
        num_classes = data.y.max().item() + 1  # 获取类别数

        # 第二层卷积层,输出类别数个特征
        self.conv2 = GCNConv(16, num_classes)
    def forward(self,x,edge_index):
        x = self.conv1(x, edge_index)        #输入特征矩阵与边的索引集合
        x = F.relu(x)                        #卷积后激活
        x = self.conv2(x, edge_index)
        return F.log_softmax(x, dim=1)

第一种:主要关注结点的特征,所以不需要手工的对结点与边的特征进行融合,直接输入到卷积层即可。

训练测试过程

device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
model = Net().to(device)
data = data.to(device)
optimizer = torch.optim.Adam(model.parameters(), lr=0.01, weight_decay=5e-4)
#模型训练,此处应该将模型的训练模式放入到循环当中,放入外部方便演示。
model.train()

train_mask = torch.zeros(data.x.size(0), dtype=torch.bool)
train_mask[:1400] = 1  # 前 140 个节点作为训练集
# 假设你划分了验证集和测试集
val_mask = torch.zeros(data.x.size(0), dtype=torch.bool)
test_mask = torch.zeros(data.x.size(0), dtype=torch.bool)

# 例如,验证集为 140-170,测试集为 170-2708
val_mask[1400:2000] = 1
test_mask[2000:] = 1

data.train_mask = train_mask
data.val_mask = val_mask
data.test_mask = test_mask
for epoch in range(200):
    optimizer.zero_grad()
    out = model(data.x, data.edge_index)    #模型的输入有节点特征还有边特征,使用的是全部数据
    loss = F.nll_loss(out[data.train_mask], data.y[data.train_mask])   #损失仅仅计算的是训练集的损失
    loss.backward()
    optimizer.step()
#测试:
model.eval()
test_predict = model(data.x, data.edge_index)[data.test_mask]
max_index = torch.argmax(test_predict, dim=1)
test_true = data.y[data.test_mask]
correct = 0
for i in range(len(max_index)):
    if max_index[i] == test_true[i]:
        correct += 1
print('测试集准确率为:{}%'.format(correct*100/len(test_true)))

数据集划分

train_mask = torch.zeros(data.x.size(0), dtype=torch.bool)
train_mask[:1400] = 1  # 前 140 个节点作为训练集
# 假设你划分了验证集和测试集
val_mask = torch.zeros(data.x.size(0), dtype=torch.bool)
test_mask = torch.zeros(data.x.size(0), dtype=torch.bool)

# 例如,验证集为 140-170,测试集为 170-2708
val_mask[1400:2000] = 1
test_mask[2000:] = 1

data.train_mask = train_mask
data.val_mask = val_mask
data.test_mask = test_mask

训练

for epoch in range(200):
    optimizer.zero_grad()  # 清除梯度
    out = model(data.x, data.edge_index)    #模型的输入有节点特征还有边特征,使用的是全部数据
    loss = F.nll_loss(out[data.train_mask], data.y[data.train_mask])   #损失仅仅计算的是训练集的损失
    loss.backward()     # 反向传播
    optimizer.step()    # 更新参数

测试

#测试:
model.eval()
test_predict = model(data.x, data.edge_index)[data.test_mask]
max_index = torch.argmax(test_predict, dim=1)
test_true = data.y[data.test_mask]
correct = 0
for i in range(len(max_index)):
    if max_index[i] == test_true[i]:
        correct += 1
print('测试集准确率为:{}%'.format(correct*100/len(test_true)))

第二种:主要关注边的特征,该模型主要是将边的两端的两个结点特征进行拼接

class EdgeClassifier(torch.nn.Module):
    def __init__(self,in_channels,out_channels):
        super(EdgeClassifier, self).__init__()
        self.conv= GCNConv(in_channels,out_channels)
        self.classifier = torch.nn.Linear(2 * out_channels,2)

    def forward(self,x,edge_index):
        x = F.relu(self.conv(x,edge_index))
        pos_edge_index = edge_index
        total_edge_index = torch.cat([pos_edge_index, negative_sampling(edge_index, num_neg_samples=pos_edge_index.size(1))], dim=1)#生成负样本边,将正负进行拼接
        edge_features = torch.cat([x[total_edge_index[0]], x[total_edge_index[1]]], dim=1)#拼接边中两个结点的特征
        return self.classifier(edge_features)

整体代码块

from plistlib import Data
from torch_geometric.data import Data
import torch
from torch_geometric.nn import GCNConv
from Train import *
from torch_geometric.utils import negative_sampling
#print(torch.__version__)
import torch.nn.functional as F
datasetPath = 'E:/pytorch/pytorch exercise/Graph neural network/Cora dataset/cora'
node_feature_file = 'E:/pytorch/pytorch exercise/Graph neural network/Cora dataset/cora/Cora.content'
edge_file = 'E:/pytorch/pytorch exercise/Graph neural network/Cora dataset/cora/Cora.cites'
label_mapping = {}
node_features = []
node_labels = []
node_ids = {}  #特征数
# 定义一个计数器,遍历所有可能的标签
current_label = 0

with open(node_feature_file,'r') as f:
    for line in f:
        parts = line.strip().split('\t')
        node_id = int(parts[0])
        features = list(map(float, parts[1:-1]))  # 特征
        label_str = parts[-1]
        if label_str not in label_mapping:
            label_mapping[label_str] = current_label
            current_label +=1
            # 将标签转换为整数
        label = label_mapping[label_str]
        node_ids[node_id] = len(node_features) #补充结点索引
        node_features.append(features)     #将节点特征依次按照数量拼接在一起
        node_labels.append(label)
#print(node_ids)
# 将节点特征和标签转换为 tensor
node_features = torch.tensor(node_features, dtype=torch.float)
# 输出张量的形状
print(node_features.shape)
# 或者使用 .size() 也能得到相同的结果
print(node_features.size())

node_labels = torch.tensor(node_labels, dtype=torch.long)
print("node_labels size = ",node_labels.size())
edge_index = []
with open(edge_file, 'r') as f:
    for line in f:
        parts = line.strip().split('\t')
        source = int(parts[0])  # 源节点
        target = int(parts[1])  # 目标节点

        source_idx = node_ids[source]  # 获取节点ID的索引
        target_idx = node_ids[target]
        edge_index.append([source_idx, target_idx])#引用边的信息,生成边的索引集合
# print(source_idx)
# print(target_idx)
edge_index = torch.tensor(edge_index, dtype=torch.long).t().contiguous()
print("edge_index size = ",edge_index.size())
#print(edge_index.shape())
data = Data(x=node_features, edge_index=edge_index, y=node_labels)
# 输出数据的一些信息
print(f'节点特征矩阵 shape: {data.x.shape}')
print(f'边的连接关系 (edge_index) shape: {data.edge_index.shape}')
print(f'节点标签 shape: {data.y.shape}')

# 输出第一个节点的特征和标签
print(f'节点 0 的特征: {data.x[0]}')
print(f'节点 0 的标签: {data.y[0]}')


class EdgeClassifier(torch.nn.Module):
    def __init__(self,in_channels,out_channels):
        super(EdgeClassifier, self).__init__()
        self.conv= GCNConv(in_channels,out_channels)
        self.classifier = torch.nn.Linear(2 * out_channels,2)

    def forward(self,x,edge_index):
        x = F.relu(self.conv(x,edge_index))
        pos_edge_index = edge_index
        total_edge_index = torch.cat([pos_edge_index, negative_sampling(edge_index, num_neg_samples=pos_edge_index.size(1))], dim=1)#生成负样本边,将正负进行拼接
        edge_features = torch.cat([x[total_edge_index[0]], x[total_edge_index[1]]], dim=1)#拼接边中两个结点的特征
        return self.classifier(edge_features)

# 关注边的特征,划分数据集依照边的数量进行划分
edges = data.edge_index.t().cpu().numpy()  #提取边的矩阵
num_edges = edges.shape[0]                 #提取边的数量
print("-------num_edges---------",num_edges)
train_mask = torch.zeros(num_edges, dtype=torch.bool)
test_mask = torch.zeros(num_edges, dtype=torch.bool)
train_size = int(0.8 * num_edges)
train_indices = torch.randperm(num_edges)[:train_size]# 随机生成num_edges长度的索引,随机选择train_size数量的边,索引存储在train_indices
train_mask[train_indices] = True
test_mask[~train_mask] = True
# 定义模型和优化器/训练/测试
model = EdgeClassifier(data.num_features, 64)# 定义输入维度与输出维度
optimizer = torch.optim.Adam(model.parameters(), lr=0.01, weight_decay=5e-4)
def train():
    model.train()
    optimizer.zero_grad()
    logits = model(data.x, data.edge_index)
    pos_edge_index = data.edge_index
    pos_labels = torch.ones(pos_edge_index.size(1), dtype=torch.long)
    neg_labels = torch.zeros(pos_edge_index.size(1), dtype=torch.long)
    labels = torch.cat([pos_labels, neg_labels], dim=0).to(logits.device)
    new_train_mask = torch.cat([train_mask, train_mask], dim=0)
    loss = F.cross_entropy(logits[new_train_mask], labels[new_train_mask])
    loss.backward()
    optimizer.step()
    return loss.item()


def test():
    model.eval()
    with torch.no_grad():
        logits = model(data.x, data.edge_index)
        pos_edge_index = data.edge_index
        pos_labels = torch.ones(pos_edge_index.size(1), dtype=torch.long)
        neg_labels = torch.zeros(pos_edge_index.size(1), dtype=torch.long)
        labels = torch.cat([pos_labels, neg_labels], dim=0).to(logits.device)
        new_test_mask = torch.cat([test_mask, test_mask], dim=0)

        predictions = logits[new_test_mask].max(1)[1]
        correct = predictions.eq(labels[new_test_mask]).sum().item()
        return correct / len(predictions)


for epoch in range(1, 1001):
    loss = train()
    acc = test()
    print(f"Epoch: {epoch:03d}, Loss: {loss:.4f}, Acc: {acc:.4f}")




3.按照图分类的方式进行计算,输入多个小图进行分类

数据集:ENZYMES

包含文件:

在该代码中,主要用到了以下几个文件:

  1. ENZYMES_graph_indicator.txt:用于指示每个节点所属的图。PyTorch Geometric 会自动将每个节点分配到对应的图中,以便在训练和测试时能够分辨哪些节点属于同一个图。

  2. ENZYMES_A.txt:用于定义每个图的边(即节点之间的连接关系),这个文件中的数据会被转换成 edge_index 参数,用于图卷积层 GCNConv 的输入。

  3. ENZYMES_node_labels.txt:用于给每个节点分配标签(标签只有 1 和 0),在一些任务中可能作为节点特征或属性被使用。

  4. ENZYMES_graph_labels.txt:提供每个图的标签(类别)。这是最终分类任务的标签,也就是每个图所属的类别。在代码中,损失函数 CrossEntropyLoss 会用到该标签进行监督学习。

此外,如果数据集中包含以下文件,它们也可能被使用:

  • ENZYMES_node_attributes.txt:每个节点的属性或特征,作为输入特征矩阵 data.x,即 dataset.num_node_features。如果存在这个文件,它会被用作节点的特征数据。

  • ENZYMES_edge_attributes.txt:每条边的属性或特征。在该代码中没有直接用到这个文件,因为 GCNConv 层没有使用边特征

ENZYMES_A:表示边的集合

ENZYMES_graph_indicator:表示结点属于哪一个图

ENZYMES_graph_labels:图的标签

ENZYMES_node_attributes:结点特征

ENZYMES_node_labels:结点的标签

对于结点的标签。解释如下

1.加载数据集

import torch
import torch.nn.functional as F
from torch_geometric.nn import GCNConv, global_mean_pool
from torch_geometric.datasets import TUDataset
from torch_geometric.data import DataLoader

# 加载数据集
dataset = TUDataset(root='/tmp/ENZYMES', name='ENZYMES')
dataset = dataset.shuffle()


train_dataset = dataset[:540]
test_dataset = dataset[540:]

train_loader = DataLoader(train_dataset, batch_size=64, shuffle=True)
test_loader = DataLoader(test_dataset, batch_size=64, shuffle=False)

模型结构

# 定义图卷积网络模型
class GCN(torch.nn.Module):
    def __init__(self, hidden_channels):
        super(GCN, self).__init__()
        self.conv1 = GCNConv(dataset.num_node_features, hidden_channels)
        self.conv2 = GCNConv(hidden_channels, hidden_channels)
        self.conv3 = GCNConv(hidden_channels, hidden_channels)
        self.lin = torch.nn.Linear(hidden_channels, dataset.num_classes)
    def forward(self, x, edge_index, batch):
        x = self.conv1(x, edge_index)
        x = x.relu()
        x = self.conv2(x, edge_index)
        x = x.relu()
        x = self.conv3(x, edge_index)
        x = global_mean_pool(x, batch)    # 使用全局平均池化获得图的嵌入
        x = F.dropout(x, p=0.5, training=self.training)
        x = self.lin(x)
        return x

model = GCN(hidden_channels=64)
optimizer = torch.optim.Adam(model.parameters(), lr=0.01)
criterion = torch.nn.CrossEntropyLoss()

BATCH参数

在图神经网络(GNN)中,batch 参数用于指示每个节点属于哪个图。通常情况下,在图神经网络的训练过程中,模型会处理一批(batch)图数据。batch 参数在这种情况下非常重要,因为它帮助模型在批次(batch)处理中区分不同的图。

例如:

batch = tensor([0, 0, 0, 1, 1, 2, 2, 2, 2])

在这个示例中,batch 表示一共有 3 个图:

  • 前 3 个节点(索引 0、1、2)属于第一个图(标记为 0)。
  • 接下来的 2 个节点(索引 3、4)属于第二个图(标记为 1)。
  • 最后 4 个节点(索引 5、6、7、8)属于第三个图(标记为 2)。

2. batch 参数的作用

在图神经网络中,batch 参数的主要作用是帮助处理多个图的批量计算。在该代码中,batch 参数配合 global_mean_pool 函数使用。global_mean_pool 会按照 batch 参数,将属于同一个图的节点聚合起来,计算每个图的特征表示。

例如:

  • global_mean_pool(x, batch) 会根据 batch 将节点特征 x 按照图来划分,计算每个图的节点特征的平均值,从而得到图级别的表示。

3. batch 参数的生成

在使用 PyTorch Geometric 的 DataLoader 时,每次加载一批图时会自动生成 batch 参数。例如,代码中的 train_loadertest_loader 会将多个图放入一个批次,并自动生成 batch 参数。

总结

batch 参数的作用是标记每个节点属于哪个图,以便在批次处理中区分不同的图,特别是在使用聚合函数(如 global_mean_pool)时,用于生成每个图的全局特征表示。

简而言之,batch就是把不同图的特征聚合分别聚合在不同图上,这样每个图都有自己的特征。

训练与测试代码

def train():
    model.train()
    for data in train_loader:
        optimizer.zero_grad()
        out = model(data.x, data.edge_index, data.batch)
        loss = criterion(out, data.y)
        loss.backward()
        optimizer.step()

def test(loader):
    model.eval()
    correct = 0
    for data in loader:
        out = model(data.x, data.edge_index, data.batch)
        pred = out.argmax(dim=1)
        correct += int((pred == data.y).sum())
    return correct / len(loader.dataset)

for epoch in range(1, 1001):
    train()
    train_acc = test(train_loader)
    test_acc = test(test_loader)
    print(f'Epoch: {epoch:03d}, Train Acc: {train_acc:.4f}, Test Acc: {test_acc:.4f}')

注意区分不同图的话增加batch参数

5 总结

综合上面所有的内容,最重要的是以下两点:

①不同GNN的本质区别是他们的消息传递机制不同,如GCN/GraphSAGE/GIN/GAT等等,只需要修改层的名称即可,目前已经达到了高度的集成化,不需要进行手撸,除非你的研究需要。

②三种不同的任务,他们的本质区别就是:Output层的输入不一样

●对于节点层面的任务而言

可以直接self.conv = GCNConv(16, dataset.num_classes) ————这是直接把任务融合到卷积层

也可以在卷积获取特征之后,后面加几个线性层

●对于边层面的任务而言

通过GNN提取出节点信息,输入Output层之前需要进行边特征的融合(在这里是Concat节点特征)

边特征融合之后再跟几个线性层

edge_features = torch.cat([x[total_edge_index[0]], x[total_edge_index[1]]], dim=1)  

●对于图层面的任务而言

通过GNN提取出节点信息,输入Output层之前需要进行图特征的融合(在这里是对节点特征进行全局平均池化)

 x = global_mean_pool(x, batch)    

图特征融合之后再跟几个线性层

最后

如果你认真看完上述所有内容,你已经初步掌握了GNN的概念和使用方法。若想进阶,请使用自己的Graph_data进行尝试。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值