【PyG】图神经网络GCN代码自学

本文考虑了一个具有分层传播规则的多层图卷积网络(GCN),图卷积网络(GCN)是一个对图数据进行操作的神经网络架构,它非常强大,即使是随机初始化的两层GCN也可以生成图网络中节点的有用特征表示。

本文引用参考GCN原作者论文及代码,供自学自用,原作者github网址如下: https://github.com/tkipf/pygcn

论文原图及核心公式

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

Data

cora.cites如下图所示:
在这里插入图片描述
content表示每个点的内容,第一列为序号,最后一列为所属类型。
在这里插入图片描述
在这里插入图片描述

utils相关代码分析

utils.py

代码解析见注释

load_data()函数

def load_data(path=os.path.join(os.getcwd(),'pygcn','data','cora'), dataset="cora"):
    """Load citation network dataset (cora only for now)"""
    print('Loading {} dataset...'.format(dataset))

    idx_features_labels = np.genfromtxt("{}/{}.content".format(path, dataset),
                                        dtype=np.dtype(str))
    features = sp.csr_matrix(idx_features_labels[:, 1:-1], dtype=np.float32)  # 取特征feature
    # 前闭后开的,相当于第二列到倒数第二列
    labels = encode_onehot(idx_features_labels[:, -1])  # one-hot label

    # build graph
    idx = np.array(idx_features_labels[:, 0], dtype=np.int32)  # 节点
    idx_map = {j: i for i, j in enumerate(idx)}   # 构建节点的索引字典
    # {31336: 0, 1061127: 1, 1106406: 2, 13195: 3},大概这样j=31336,i=0,enumerate可以把数组或者列表的数据和序号变成索引字典
    edges_unordered = np.genfromtxt("{}/{}.cites".format(path, dataset),  # 导入edge的数据
                                    dtype=np.int32)
    edges = np.array(list(map(idx_map.get, edges_unordered.flatten())),
                     dtype=np.int32).reshape(edges_unordered.shape)    # 将之前的转换成字典编号后的边
    # map函数用法https://blog.csdn.net/weixin_43641920/article/details/122111417
    # 如果找到了key值大概就是31336,用后面的edges_unordered替换
    adj = sp.coo_matrix((np.ones(edges.shape[0]), (edges[:, 0], edges[:, 1])),  # 构建边的邻接矩阵
                        shape=(labels.shape[0], labels.shape[0]),
                        dtype=np.float32)
    
    # build symmetric adjacency matrix,计算转置矩阵。将有向图转成无向图
    adj = adj + adj.T.multiply(adj.T > adj) - adj.multiply(adj.T > adj)
    #博客在这里 https://blog.csdn.net/iamjingong/article/details/97392571
    # 例子在test.py

    features = normalize(features)   # 对特征做了归一化的操作
    adj = normalize(adj + sp.eye(adj.shape[0]))   # 对A+I归一化
    # 训练,验证,测试的样本
    idx_train = range(140)
    idx_val = range(200, 500)
    idx_test = range(500, 1500)
    # 将numpy的数据转换成torch格式
    features = torch.FloatTensor(np.array(features.todense()))
    labels = torch.LongTensor(np.where(labels)[1])
    adj = sparse_mx_to_torch_sparse_tensor(adj)

    idx_train = torch.LongTensor(idx_train)
    idx_val = torch.LongTensor(idx_val)
    idx_test = torch.LongTensor(idx_test)

    return adj, features, labels, idx_train, idx_val, idx_test

map函数用法https://blog.csdn.net/weixin_43641920/article/details/122111417
非对称邻接矩阵转变为对称邻接矩阵 https://blog.csdn.net/iamjingong/article/details/97392571

normalize函数

def normalize(mx):
    """Row-normalize sparse matrix"""
    rowsum = np.array(mx.sum(1))  #  求矩阵每一行的度
    r_inv = np.power(rowsum, -1).flatten()  # 求和的-1次方
    r_inv[np.isinf(r_inv)] = 0.   # 如果是inf,转换成0
    r_mat_inv = sp.diags(r_inv)  # 构造对角戏矩阵
    mx = r_mat_inv.dot(mx)  # 构造D-1*A,非对称方式,简化方式
    return mx

encode_onehot函数

见下方onehot.py

def encode_onehot(labels):
    classes = set(labels)
    classes_dict = {c: np.identity(len(classes))[i, :] for i, c in
                    enumerate(classes)}
    labels_onehot = np.array(list(map(classes_dict.get, labels)),
                             dtype=np.int32)
    return labels_onehot

accuracy函数

def accuracy(output, labels):
    preds = output.max(1)[1].type_as(labels)
    correct = preds.eq(labels).double()
    correct = correct.sum()
    return correct / len(labels)

sparse_mx_to_torch_sparse_tensor函数

def sparse_mx_to_torch_sparse_tensor(sparse_mx):
    """Convert a scipy sparse matrix to a torch sparse tensor."""
    sparse_mx = sparse_mx.tocoo().astype(np.float32)
    indices = torch.from_numpy(
        np.vstack((sparse_mx.row, sparse_mx.col)).astype(np.int64))
        #vstack按行堆叠
    values = torch.from_numpy(sparse_mx.data)
    shape = torch.Size(sparse_mx.shape)
    return torch.sparse.FloatTensor(indices, values, shape)

coo稀疏矩阵 https://blog.csdn.net/yhb1047818384/article/details/78996906

变换前adj
在这里插入图片描述

变换后adj
变换后

test.py

import torch
import numpy as np
import scipy.sparse as sp

i=torch.LongTensor([[0,1,1],[2,0,2]])
j=i.t()

adj = sp.coo_matrix((np.ones(i.shape[0]), (i[:, 0], i[:, 1])),  # 构建边的邻接矩阵
                        shape=(5,5),
                        dtype=np.float32)
print(adj.A)
print('~~~~~~~~~~~')
print(adj.T > adj)
print('~~~~~~~~~~~')
print(adj.multiply(adj.T > adj))
print('~~~~~~~~~~~')
print(adj.T.multiply(adj.T > adj))
print('~~~~~~~~~~~')
adj = adj + adj.T.multiply(adj.T > adj) - adj.multiply(adj.T > adj)
print(adj.A)

在这里插入图片描述

where.py


import numpy as np
 
a = np.arange(12).reshape(3,4)
print('a:', a)
print('np.where(a > 5):', np.where(a > 5))
print('a[np.where(a > 5)]:', a[np.where(a > 5)])
print('np.where(a > 5)[0]:', np.where(a > 5)[0])
print('np.where(a > 5)[1]:', np.where(a > 5)[1])
print(a[np.where(a > 5)[0], np.where(a > 5)[1]])

https://blog.csdn.net/ysh1026/article/details/109559981
在这里插入图片描述

onehot.py

import numpy as np
import os

def encode_onehot(labels):
    classes = set(labels)#集合没有重复值,看看有多少个类型
    classes_dict = {c: np.identity(len(classes))[i, :] for i, c in
                    enumerate(classes)}#把classes变成字典
    #np.identity(len(classes))[i, :],变成对角矩阵,并切割成i行
    #for i, c in enumerate(classes),i是索引,c是元素
    labels_onehot = np.array(list(map(classes_dict.get, labels)),
                             dtype=np.int32)
    return labels_onehot


path=os.path.join(os.getcwd(),'pygcn','data','cora')
dataset="cora"
idx_features_labels = np.genfromtxt("{}/{}.content".format(path, dataset),
                                        dtype=np.dtype(str))
labels = encode_onehot(idx_features_labels[:, -1])  # one-hot label

在这里插入图片描述

train相关代码分析

代码解析见注释

train.py

from __future__ import division
from __future__ import print_function
from multiprocessing import cpu_count

import time
import argparse
import numpy as np

import torch
import torch.nn.functional as F
import torch.optim as optim

from utils import load_data, accuracy
from models import GCN

# Training settings
parser = argparse.ArgumentParser()
parser.add_argument('--no-cuda', action='store_true', default=False,
                    help='Disables CUDA training.')
parser.add_argument('--fastmode', action='store_true', default=False,
                    help='Validate during training pass.')
parser.add_argument('--seed', type=int, default=42, help='Random seed.')
parser.add_argument('--epochs', type=int, default=1000,
                    help='Number of epochs to train.')
parser.add_argument('--lr', type=float, default=0.01,
                    help='Initial learning rate.')
parser.add_argument('--weight_decay', type=float, default=5e-4,
                    help='Weight decay (L2 loss on parameters).')
parser.add_argument('--hidden', type=int, default=16,
                    help='Number of hidden units.')
parser.add_argument('--dropout', type=float, default=0.5,
                    help='Dropout rate (1 - keep probability).')

args = parser.parse_args()
args.cuda = (not args.no_cuda) and torch.cuda.is_available()

# np,cpu,gpu三个随机数种子
np.random.seed(args.seed)
torch.manual_seed(args.seed)
if args.cuda:
    torch.cuda.manual_seed(args.seed)


# Load data
adj, features, labels, idx_train, idx_val, idx_test = load_data()

# Model and optimizer,构造GCN,初始化参数。两层GCN
model = GCN(nfeat=features.shape[1],
            nhid=args.hidden,#2400->16->7
            nclass=labels.max().item() + 1,
#labels.max().item()
# 6
# labels.max()
# tensor(6)
            dropout=args.dropout)
optimizer = optim.Adam(model.parameters(),
                       lr=args.lr, weight_decay=args.weight_decay)#有时间可以康康

if args.cuda:
    model.cuda()
    features = features.cuda()
    adj = adj.cuda()
    labels = labels.cuda()
    idx_train = idx_train.cuda()
    idx_val = idx_val.cuda()
    idx_test = idx_test.cuda()
#是否使用cuda


def train(epoch):
    t = time.time()
    model.train()
    optimizer.zero_grad() # GraphConvolution forward
    output = model(features, adj)   # 运行模型,输入参数 (features, adj)
    loss_train = F.nll_loss(output[idx_train], labels[idx_train])
    acc_train = accuracy(output[idx_train], labels[idx_train])
    loss_train.backward()
    optimizer.step()

    if not args.fastmode:
        # Evaluate validation set performance separately,
        # deactivates dropout during validation run.
        model.eval()
        output = model(features, adj)

    loss_val = F.nll_loss(output[idx_val], labels[idx_val])
    acc_val = accuracy(output[idx_val], labels[idx_val])
    print('Epoch: {:04d}'.format(epoch+1),
          'loss_train: {:.4f}'.format(loss_train.item()),
          'acc_train: {:.4f}'.format(acc_train.item()),
          'loss_val: {:.4f}'.format(loss_val.item()),
          'acc_val: {:.4f}'.format(acc_val.item()),
          'time: {:.4f}s'.format(time.time() - t))

    print('Epoch: %d'%epoch)


def test():
    model.eval()
    output = model(features, adj)
    loss_test = F.nll_loss(output[idx_test], labels[idx_test])
    acc_test = accuracy(output[idx_test], labels[idx_test])
    print("Test set results:",
          "loss= {:.4f}".format(loss_test.item()),
          "accuracy= {:.4f}".format(acc_test.item()))


# Train model
t_total = time.time()
for epoch in range(args.epochs):
    train(epoch)
print("Optimization Finished!")
print("Total time elapsed: {:.4f}s".format(time.time() - t_total))

# Testing
test()

layers.py

import math
import torch
from torch.nn.parameter import Parameter
from torch.nn.modules.module import Module
class GraphConvolution(Module):
    """
    Simple GCN layer, similar to https://arxiv.org/abs/1609.02907
    """

    def __init__(self, in_features, out_features, bias=True):
        super(GraphConvolution, self).__init__()
        self.in_features = in_features
        self.out_features = out_features
        self.weight = Parameter(torch.FloatTensor(in_features, out_features))  # input_features, out_features组成的矩阵
        if bias:
            self.bias = Parameter(torch.FloatTensor(out_features))#向量
        else:
            self.register_parameter('bias', None)
        self.reset_parameters()

    def reset_parameters(self):
        stdv = 1. / math.sqrt(self.weight.size(1))
        self.weight.data.uniform_(-stdv, stdv)  # 随机化参数
        if self.bias is not None:
            self.bias.data.uniform_(-stdv, stdv)

    def forward(self, input, adj):
        support = torch.mm(input, self.weight)  # GraphConvolution forward。input*weight
        output = torch.spmm(adj, support)  # 稀疏矩阵的相乘,和mm一样的效果
        if self.bias is not None:
            return output + self.bias
        else:
            return output

    def __repr__(self):
        return self.__class__.__name__ + ' (' \
               + str(self.in_features) + ' -> ' \
               + str(self.out_features) + ')'

models.py

import torch.nn as nn
import torch.nn.functional as F
from layers import GraphConvolution


class GCN(nn.Module):
    def __init__(self, nfeat, nhid, nclass, dropout):#初始化
        super(GCN, self).__init__()

        self.gc1 = GraphConvolution(nfeat, nhid) # 构建第一层 GCN
        self.gc2 = GraphConvolution(nhid, nclass) # 构建第二层 GCN
        self.dropout = dropout

    def forward(self, x, adj):
        x = F.relu(self.gc1(x, adj))#第一层,并用relu激活
        x = F.dropout(x, self.dropout, training=self.training)#丢弃一部分特征
        x = self.gc2(x, adj)#第二层
        return F.log_softmax(x, dim=1)#softmax激活函数

debug调试

数据处理结果
在这里插入图片描述
layers.py的第一层初始化结果
在这里插入图片描述
在这里插入图片描述
layers.py的第二层初始化结果

在这里插入图片描述
优化结果
在这里插入图片描述
第一次训练,x = F.relu(self.gc1(x, adj))#第一层gcn,并用relu激活
在这里插入图片描述
x = F.dropout(x, self.dropout, training=self.training)#丢弃一部分特征
在这里插入图片描述
x = self.gc2(x, adj)#第二层gcn
在这里插入图片描述
第一次训练结果
在这里插入图片描述
在这里插入图片描述
第一次验证eval结果
第一层GCN
在这里插入图片描述
dropout之后
在这里插入图片描述
第二层gcn
在这里插入图片描述
验证eval结果输出
在这里插入图片描述
在这里插入图片描述
测试结果
在这里插入图片描述

实验结果

训练200次结果

训练1000次结果在这里插入图片描述

# GPF ## 一、GPF(Graph Processing Flow):利用神经网络处理问题的一般化流程 1、节点预表示:利用NE框架,直接获得全每个节点的Embedding; 2、正负样本采样:(1)单节点样本;(2)节点对样本; 3、抽取封闭子:可做类化处理,建立一种通用数据结构; 4、子特征融合:预表示、节点特征、全局特征、边特征; 5、网络配置:可以是输入、输出的网络;也可以是输入,分类/聚类结果输出的网络; 6、训练和测试; ## 二、主要文件: 1、graph.py:读入数据; 2、embeddings.py:预表示学习; 3、sample.py:采样; 4、subgraphs.py/s2vGraph.py:抽取子; 5、batchgraph.py:子特征融合; 6、classifier.py:网络配置; 7、parameters.py/until.py:参数配置/帮助文件; ## 三、使用 1、在parameters.py中配置相关参数(可默认); 2、在example/文件夹中运行相应的案例文件--包括链接预测、节点状态预测; 以链接预测为例: ### 1、导入配置参数 ```from parameters import parser, cmd_embed, cmd_opt``` ### 2、参数转换 ``` args = parser.parse_args() args.cuda = not args.noCuda and torch.cuda.is_available() torch.manual_seed(args.seed) if args.cuda: torch.cuda.manual_seed(args.seed) if args.hop != 'auto': args.hop = int(args.hop) if args.maxNodesPerHop is not None: args.maxNodesPerHop = int(args.maxNodesPerHop) ``` ### 3、读取数据 ``` g = graph.Graph() g.read_edgelist(filename=args.dataName, weighted=args.weighted, directed=args.directed) g.read_node_status(filename=args.labelName) ``` ### 4、获取全节点的Embedding ``` embed_args = cmd_embed.parse_args() embeddings = embeddings.learn_embeddings(g, embed_args) node_information = embeddings #print node_information ``` ### 5、正负节点采样 ``` train, train_status, test, test_status = sample.sample_single(g, args.testRatio, max_train_num=args.maxTrainNum) ``` ### 6、抽取节点对的封闭子 ``` net = until.nxG_to_mat(g) #print net train_graphs, test_graphs, max_n_label = subgraphs.singleSubgraphs(net, train, train_status, test, test_status, args.hop, args.maxNodesPerHop, node_information) print('# train: %d, # test: %d' % (len(train_graphs), len(test_graphs))) ``` ### 7、加载网络模型,并在classifier中配置相关参数 ``` cmd_args = cmd_opt.parse_args() cmd_args.feat_dim = max_n_label + 1 cmd_args.attr_dim = node_information.shape[1] cmd_args.latent_dim = [int(x) for x in cmd_args.latent_dim.split('-')] if len(cmd_args.latent_dim) == 1: cmd_args.latent_dim = cmd_args.latent_dim[0] model = classifier.Classifier(cmd_args) optimizer = optim.Adam(model.parameters(), lr=args.learningRate) ``` ### 8、训练和测试 ``` train_idxes = list(range(len(train_graphs))) best_loss = None for epoch in range(args.num_epochs): random.shuffle(train_idxes) model.train() avg_loss = loop_dataset(train_graphs, model, train_idxes, cmd_args.batch_size, optimizer=optimizer) print('\033[92maverage training of epoch %d: loss %.5f acc %.5f auc %.5f\033[0m' % (epoch, avg_loss[0], avg_loss[1], avg_loss[2])) model.eval() test_loss = loop_dataset(test_graphs, model, list(range(len(test_graphs))), cmd_args.batch_size) print('\033[93maverage test of epoch %d: loss %.5f acc %.5f auc %.5f\033[0m' % (epoch, test_loss[0], test_loss[1], test_loss[2])) ``` ### 9、运行结果 ``` average test of epoch 0: loss 0.62392 acc 0.71462 auc 0.72314 loss: 0.51711 acc: 0.80000: 100%|███████████████████████████████████| 76/76 [00:07<00:00, 10.09batch/s] average training of epoch 1: loss 0.54414 acc 0.76895 auc 0.77751 loss: 0.37699 acc: 0.79167: 100%|█████████████████████████████████████| 9/9 [00:00<00:00, 34.07batch/s] average test of epoch 1: loss 0.51981 acc 0.78538 auc 0.79709 loss: 0.43700 acc: 0.84000: 100%|███████████████████████████████████| 76/76 [00:07<00:00, 9.64batch/s] average training of epoch 2: loss 0.49896 acc 0.79184 auc 0.82246 loss: 0.63594 acc: 0.66667: 100%|█████████████████████████████████████| 9/9 [00:00<00:00, 28.62batch/s] average test of epoch 2: loss 0.48979 acc 0.79481 auc 0.83416 loss: 0.57502 acc: 0.76000: 100%|███████████████████████████████████| 76/76 [00:07<00:00, 9.70batch/s] average training of epoch 3: loss 0.50005 acc 0.77447 auc 0.79622 loss: 0.38903 acc: 0.75000: 100%|█████████████████████████████████████| 9/9 [00:00<00:00, 34.03batch/s] average test of epoch 3: loss 0.41463 acc 0.81132 auc 0.86523 loss: 0.54336 acc: 0.76000: 100%|███████████████████████████████████| 76/76 [00:07<00:00, 9.57batch/s] average training of epoch 4: loss 0.44815 acc 0.81711 auc 0.84530 loss: 0.44784 acc: 0.70833: 100%|█████████████████████████████████████| 9/9 [00:00<00:00, 28.62batch/s] average test of epoch 4: loss 0.48319 acc 0.81368 auc 0.84454 loss: 0.36999 acc: 0.88000: 100%|███████████████████████████████████| 76/76 [00:07<00:00, 10.17batch/s] average training of epoch 5: loss 0.39647 acc 0.84184 auc 0.89236 loss: 0.15548 acc: 0.95833: 100%|█████████████████████████████████████| 9/9 [00:00<00:00, 28.62batch/s] average test of epoch 5: loss 0.30881 acc 0.89623 auc 0.95132 ```
以下是使用PyG训练卷积网络GCN进行骨骼识别的代码参考: ```python import torch import torch.nn.functional as F from torch_geometric.nn import GCNConv from torch_geometric.datasets import ModelNet from torch_geometric.loader import DataLoader class GCN(torch.nn.Module): def __init__(self): super(GCN, self).__init__() self.conv1 = GCNConv(3, 16) self.conv2 = GCNConv(16, 32) self.conv3 = GCNConv(32, 64) self.fc1 = torch.nn.Linear(64, 128) self.fc2 = torch.nn.Linear(128, 10) def forward(self, x, edge_index): x = F.relu(self.conv1(x, edge_index)) x = F.relu(self.conv2(x, edge_index)) x = F.relu(self.conv3(x, edge_index)) x = global_max_pool(x, batch) x = F.relu(self.fc1(x)) x = F.dropout(x, training=self.training) x = self.fc2(x) return F.log_softmax(x, dim=1) device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') model = GCN().to(device) optimizer = torch.optim.Adam(model.parameters(), lr=0.01) dataset = ModelNet(root='path/to/dataset', name='10') loader = DataLoader(dataset, batch_size=32, shuffle=True) def train(): model.train() for data in loader: data = data.to(device) optimizer.zero_grad() out = model(data.x, data.edge_index) loss = F.nll_loss(out, data.y) loss.backward() optimizer.step() def test(loader): model.eval() correct = 0 for data in loader: data = data.to(device) with torch.no_grad(): out = model(data.x, data.edge_index) pred = out.argmax(dim=1) correct += int((pred == data.y).sum()) return correct / len(loader.dataset) for epoch in range(1, 201): train() train_acc = test(loader) test_acc = test(loader) print('Epoch: {:03d}, Train Acc: {:.4f}, Test Acc: {:.4f}'.format( epoch, train_acc, test_acc)) ``` 在这个例子中,我们创建了一个包含三个GCNConv层的GCN模型来处理3D点云数据。然后我们使用PyG中的ModelNet数据集进行训练和测试,并使用Adam优化器进行优化。在每个epoch中,我们都会计算训练集和测试集的准确率,并打印出来。 需要注意的是,这里的global_max_pool函数是从torch_geometric.nn中导入的,用于进行全局最大池化操作。这个函数会对每个batch中的节点特征进行最大池化,得到一个大小为(batch_size, num_channels)的特征向量。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值