GCN代码

该博客详细介绍了图卷积网络(GCN)的实现过程,包括`layers.py`中定义的GraphConvolution层,以及`models.py`中的GCN模型。训练设置和数据加载在`train.py`中描述,使用PyTorch框架,通过`load_data`读取Cora数据集,进行预处理和归一化。模型包含两层GCN,训练过程中采用Adam优化器,执行随机梯度下降和dropout策略。
摘要由CSDN通过智能技术生成

代码可以在github上fork,本文主要是加了一些注释,并且搭配本人所作笔记【GCN代码笔记】
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))
        if bias:
            self.bias = Parameter(torch.FloatTensor(out_features))
        else:
            self.register_parameter('bias', None)
            #Parameter()与register_parameter()都是将一个不可训练的类型Tensor转换成可以训练的类型parameter,并将这个parameter绑定到这个module里面,相当于变成了模型的一部分,成为了模型中可以根据训练进行变化的参数。
        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)#将input函数和weight函数做了一个乘积;torch.mm(a, b)是矩阵a和b矩阵相乘
        output = torch.spmm(adj, support)#稀疏矩阵的相乘,是之前归一化之后的结果和上一步相乘
        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 pygcn.layers import GraphConvolution


class GCN(nn.Module):
    def __init__(self, nfeat, nhid, nclass, dropout):
        super(GCN, self).__init__()
        #构建第一层GCN,第一个参数是初始特征,第二个参数是隐藏层特征
        self.gc1 = GraphConvolution(nfeat, nhid)
        #构建第二层GCN,传入的nhid维度是16维,输出的nclass与最后要判定的类别数是一致的,是7个类别
        self.gc2 = GraphConvolution(nhid, nclass)
        self.dropout = dropout

    def forward(self, x, adj):
        x = F.relu(self.gc1(x, adj))
        x = F.dropout(x, self.dropout, training=self.training)
        #完成第一层GCN训练之后,x.shape是2708*16维的。16是hidden layer的维度,16是传参传进去的
        x = self.gc2(x, adj)
        return F.log_softmax(x, dim=1)

train.py

from __future__ import division
from __future__ import print_function

import time
import argparse
import numpy as np

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

from pygcn.utils import load_data, accuracy
from pygcn.models import GCN

# Training settings添加train set的一些参数
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=200,
                    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.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,
            nclass=labels.max().item() + 1,
            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()

#定义训练的一个函数
def train(epoch):
    t = time.time()
    # 如果模型中有BN层(Batch Normalization)和Dropout,需要在训练时添加model.train()。
    # model.train()是保证BN层能够用到每一批数据的均值和方差。对于Dropout,model.train()是随机取一部分网络连接来训练更新参数。
    model.train()
    optimizer.zero_grad()#把模型中参数的梯度设为0
    output = model(features, adj)#运行模型,输入参数(features,adj)即特征和邻接矩阵
    #output输出结果是2708*7的维度,值代表每一位可能输出的概率
    loss_train = F.nll_loss(output[idx_train], labels[idx_train])#等同于cross_entropy
    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))

#定义测试的一个函数
def test():
    # 如果模型中有BN层(BatchNormalization)和Dropout,在测试时添加model.eval()。
    # model.eval()是保证BN层能够用全部训练数据的均值和方差,即测试过程中要保证BN层的均值和方差不变。
    # 对于Dropout,model.eval()是利用到了所有网络连接,即不进行随机舍弃神经元。
    # 训练完train样本后,生成的模型model要用来测试样本。
    # 在model(test)之前,需要加上model.eval(),否则的话,有输入数据,即使不训练,它也会改变权值。这是model中含有BN层和Dropout所带来的的性质。

    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()

utils.py

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


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


def load_data(path="../data/cora/", dataset="cora"):
    """Load citation network dataset (cora only for now)"""
    print('Loading {} dataset...'.format(dataset))
    #得到cora.content中的数据
    idx_features_labels = np.genfromtxt("{}{}.content".format(path, dataset),
                                        dtype=np.dtype(str))
    #取特征,从第一个到倒数第二个
    #sp.csr_matrix就是按照列来压缩
    features = sp.csr_matrix(idx_features_labels[:, 1:-1], dtype=np.float32)
    #labels即最后一列 文章分类
    labels = encode_onehot(idx_features_labels[:, -1])
    # build graph
    #取索引,取的是cora.content中的第0列的数据
    idx = np.array(idx_features_labels[:, 0], dtype=np.int32)
    #构建节点的索引字典,就是转换为从0到索引长度-1的编号,例如将第一个元素31336转换为0,{31336:0,等等}
    idx_map = {j: i for i, j in enumerate(idx)}
    #导入边的一个数据 cora.cites文件中是边之间的联系
    edges_unordered = np.genfromtxt("{}{}.cites".format(path, dataset),
                                    dtype=np.int32)
    #将之前的转换成字典编号后的边,即假设23:9,23与31336之间有联系,现在转化为9与0之间有联系。
    edges = np.array(list(map(idx_map.get, edges_unordered.flatten())),
                     dtype=np.int32).reshape(edges_unordered.shape)
    #构建边的邻接矩阵(sp.coo_matrix构建的是稀疏矩阵)
    adj = sp.coo_matrix((np.ones(edges.shape[0]), (edges[:, 0], edges[:, 1])),
                        shape=(labels.shape[0], labels.shape[0]),
                        dtype=np.float32)   #2708*2708 因为有2708个节点

    # build symmetric adjacency matrix,计算转置矩阵,将有向图转化为无向图
    adj = adj + adj.T.multiply(adj.T > adj) - adj.multiply(adj.T > adj)

    features = normalize(features) #对特征做了归一化的操作,但不是一个必要的操作
    adj = normalize(adj + sp.eye(adj.shape[0])) #对(邻接矩阵+单位矩阵)做归一化的操作,这是必要的一个操作,这一步完成之后就是完成公式中的D的负1次*A
    #划分训练,验证,测试的样本
    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

#对节点做归一化
def normalize(mx):
    """Row-normalize sparse matrix"""
    rowsum = np.array(mx.sum(1))#矩阵按行求和
    r_inv = np.power(rowsum, -1).flatten() #求倒数,有的可能是0 然后求倒数之后出现无穷大,,此时是2708维
    r_inv[np.isinf(r_inv)] = 0. #如果是inf(无穷大),转换成0
    r_mat_inv = sp.diags(r_inv) #构建对角矩阵 ,转换成2708*2708维的矩阵
    #用转换之后的矩阵乘以之前的特征,即将特征按行做了归一化的一个操作。
    mx = r_mat_inv.dot(mx)
    return mx


def accuracy(output, labels):
    preds = output.max(1)[1].type_as(labels)#求output矩阵每一行的最大值,最大值即要输出的label
    correct = preds.eq(labels).double()
    correct = correct.sum()
    return correct / len(labels)


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))
    values = torch.from_numpy(sparse_mx.data)
    shape = torch.Size(sparse_mx.shape)
    return torch.sparse.FloatTensor(indices, values, shape)

  • 3
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值