Pygcn源码解读

源代码:GitHub - tkipf/pygcn: Graph Convolutional Networks in PyTorch

依据记忆,自己敲了源代码,可能与源代码有细微差别,但也可以正常运行。 

注释中给出了一些debug时的流程。

一、utils代码分析

主要由两部分构成:数据预处理load_data()+精度计算accuracy()

load_data()

1)features

提取featues -> 转化为csr_matrix存储 -> 归一化 -> 转变为张量torch.from_numpy()

2)labels

提取labels -> onehot编码-> 找到节点的标签np.where(labels) -> 转变为张量

注:代码中第20行将前两步合成一步,43行将后两步合成了一步。

3)建立图

论文的idx是杂乱无章的,表示起来不方便,所以构建字典给论文编号。

建立图的过程较复杂,要生成idx, edges和adj.

图的组成 步骤
idx

提取idx -> 生成字典,给节点编号 

edges 读取边 -> 用节点编号表示边
adj 生成adj -> 对称化 -> 归一化 -> 转变为稀疏张量

accuracy()

计算精度, 在train()中被调用。

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

def encode_onehot(labels):
    classes=set(labels)     #求出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'):
    print("Loading {} data...".format(dataset))
    idx_features_labels = np.genfromtxt('{}{}.content'.format(path, dataset), dtype=np.dtype(str))
    #idx_features_labels=np.array(np.genfromtxt('{}{}.content'.format(path, dataset)), dtype=np.dtype(str))
    #error:得到的features是浮点型,得到的labels是nan
    '''
    也可以写作sp.coo_matrix,只不过变成了col,row作为指示
    '''
    features=sp.csr_matrix(idx_features_labels[:, 1:-1], dtype=np.float32)  #提取第一列至倒数第二列
    labels=encode_onehot(idx_features_labels[:,-1])

    '''build graph'''
    '''①给结点编号'''
    idx=np.array(idx_features_labels[:,0], dtype=np.int32)
    #idx=idx_features_labels[:,0]有bug,原因:提取出来的是str类型的数据,idx:['31336' '1061127']
    # 得到的idx_dict:{'31336': 0,'1061127': 1,...}在第30行map()会出错!
    idx_dict={j:i for i,j in enumerate(idx)}
    '''②处理边,把边的关系用编号表示'''
    edges_unordered=np.genfromtxt('{}{}.cites'.format(path, dataset), dtype=np.int32)
    edges=np.array(list(map(idx_dict.get, edges_unordered.flatten())),
                   dtype=np.int32).reshape(edges_unordered.shape)
    '''③生成邻接矩阵'''
    adj=sp.csr_matrix((np.ones(edges.shape[0]),(edges[:,0], edges[:,1])),
                      shape=(labels.shape[0], labels.shape[0]), dtype=np.int32)
    # row, column, and data array must all be the same length
    '''④处理邻接矩阵:原始邻接矩阵->对称化(加上自连接的邻接矩阵)->归一化'''
    adj=adj+adj.T.multiply(adj.T>adj)-adj.multiply(adj.T>adj)  #adj的类型csr_matrix:(2708, 2708)
    adj=normalize(adj+np.identity(labels.shape[0]))     #传入normalize中的类型是matrix,输出是matrix

    features=normalize(features)
    '''变为张量'''
    features=torch.from_numpy(np.array(features.todense()))   #形状定义?.reshape()
    labels=torch.from_numpy(np.where(labels)[1])
    #np.where(labels)返回索引p.where(labels)[1]返回所在列  np.where(labels)返回所在行
    adj=sparse_mx_to_torch_sparse_matrix(adj)

    idx_train=range(140)
    idx_val=range(200, 500)
    idx_test=range(500, 1500)

    idx_train=torch.LongTensor(idx_train)
    idx_val=torch.LongTensor(idx_val)
    idx_test=torch.LongTensor(idx_test)
    return features, labels, adj, idx_train, idx_val, idx_test

def normalize(mx):
    rowsum=mx.sum(1)
    #rowsum=mx.sum(1)有bug,原因:这样得到的r_inv是matrix:(1, 2708)类型的
    # 运行61行sp.diags()会出现“Different number of diagonals and offsets”
    rowsum=np.array(rowsum)
    r_inv=np.power(rowsum, -1).flatten()
    r_inv[np.isinf(r_inv)]=0
    r_mat_inv=sp.diags(r_inv)   #r_mat_inv类型是dia_matrix:(2708, 2708)
    mx=r_mat_inv.dot(mx)        #矩阵相乘
    return mx

def sparse_mx_to_torch_sparse_matrix(sparse_mx):
    sparse_mx=sp.csr_matrix(sparse_mx)
    sparse_mx=sparse_mx.tocoo()
    '''下面可当成固定模板'''
    indices=torch.LongTensor(np.vstack((sparse_mx.row, sparse_mx.col)))
    #vstack()里面只有一个元素,所以要加()
    values=torch.FloatTensor(sparse_mx.data)
    shape=torch.Size(sparse_mx.shape)
    return torch.sparse.FloatTensor(indices, values, shape)

'''可当作模板使用'''
def accuracy(output, labels):
    preds=output.max(1)[1]       #找到每行的最大值(负数)所在列
    correct=preds.eq(labels)    #equ()两者相等置1,不等值0
    correct=correct.sum()
    return correct/len(labels)

二、model代码分析

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

class GCN(nn.Module):   #torch.nn.Module
    def __init__(self, nfeat, nhid, nclass, dropout):
        super(GCN, self).__init__()
        self.gc1= GraphConvolution(nfeat, nhid)     #执行了__init__和reset_parameter()
        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)    #training 在前向训练的过程中指定当前模型是在训练还是在验证
        x=self.gc2(x, adj)
        return F.log_softmax(x, dim=1)  #softmax()->取log,得到的是1433*7的向量

在定义网络的时候,如果层内有Variable,那么用nn定义,反之,则用nn.functional定义。

全连接层,卷积层等,有Variable,用nn定义。

Pooling层,Relu层等,没有Variable,用nn.function定义。

所有放在构造函数__init__里面的层的都是这个模型的“固有属性”。

nn 与 nn.functional 的区别

nn.functional.xxx 是函数接口,nn.Xxx .nn.functional.xxx 的类封装,并且nn.Xxx 都继承于一个共同祖先 nn.Module
nn.Xxx 除了具有 nn.functional.xxx 功能之外,内部附带 nn.Module 相关的属性和方法,eg. train(), eval(), load_state_dict, state_dict

两者的调用方式不同 

nn.Xxx ,实例化 ->函数调用 -> 传入数据

'''实例化'''
model=GCN(nfeat=features.shape[1],
          nhid=args.hidden,
          nclass=labels.max().item()+1,
          dropout=args.dropout)    #model是实例
'''函数调用'''
在Module类里的__call__实现了forward()函数的调用
当执行model(features, adj)的时候,底层自动调用forward方法计算结果
'''传入数据'''
output=model(features, adj)        #此时model()可看成函数

 nn.functional.xxx 传入数据 和 weight、bias 等其他参数。

x=F.relu(self.gc1(x, adj))    #例1
x=F.dropout(x, self.dropout, training=self.training)    #例2
'''
关于 dropout,强烈推荐使用 nn.Xxx 方式.
因为一般情况下只有训练阶段才进行 dropout,在 eval 阶段不会进行 dropout。
使用nn.Xxx 方法定义 dropout,在调用 model.eval() 之后,model中所有的 dropout layer 都关闭,
但以 nn.functional.dropout 方式定义 dropout,在调用 model.eval()之后并不能关闭 dropout。
需要使用 F.dropout(x, trainig=self.training)。
'''

三、layers代码分析

import math
import torch
from torch.nn.parameter import Parameter
from torch.nn.modules.module import Module  #不太理解


class GraphConvolution(Module):
    def __init__(self, in_features, out_features, bias=True):
        #gc1 in_features: 1433 out_features: 16
        #gc1 in_features: 16   out_features: 7
        super(GraphConvolution, self).__init__()
        self.in_features=in_features
        self.out_features=out_features
        '''
        先转化为张量,再转化为可训练的Parameter对象,并绑定到module里面。
        net.parameter()中就有了这个绑定的parameter,所以在参数优化的时候可以进行优化。
        Parameter()用于将参数自动加入到参数列表,让某些变量在学习过程中不断修改其值以达到最优化。
        '''
        self.weight=Parameter(torch.FloatTensor(in_features, out_features))
        if bias:
            self.bias=Parameter(torch.FloatTensor(out_features))
        else:
            self.register_parameter('bias', None)
        self.reset_parameter()

    def reset_parameter(self):  #debug时 self: GraghConvolution(1433->16)
        stv = 1./math.sqrt(self.weight.size(1))     #stv: 0.25
        self.weight.data.uniform_(-stv, stv) #weight={Parameter:(1433, 16)} self.weight.shape={Size:2}torch.Size([1433, 16])
        if self.bias is not None:
            self.bias.data.uniform_(-stv, stv)

    def forward(self, input, adj):

        support=torch.mm(input, self.weight)
        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) + ')'

nn.parameter

torch.nn.parameter是继承自torch.Tensor的子类

主要作用:作为nn.Module中的可训练参数使用。

与torch.Tensor的区别:nn.parameter会自动被认为是module的可训练参数,即加入到parameter()这个迭代器中去

下面的代码使用了nn.Parameter()对weights进行了初始化

import torch.nn.parameter as Parameter
self.weight = Parameter(torch.Tensor(out_features, in_features))

四、train代码分析

关键代码:

optimizer.zero_grad()   
output=model(features, adj)                                     '''数据集输入'''
loss_train=F.nll_loss(output[idx_train], labels[idx_train])     '''计算loss'''
acc_train=accuracy(output[idx_train], labels[idx_train])
loss_train.backward()                                           '''反向传播求梯度'''
optimizer.step()                                                '''前向传播'''

神经网络的典型处理如下所示:以train()为例

  1.  定义可学习参数的网络结构(堆叠各层和层的设计)前面已经定义好了
  2. 数据集输入  output=model(features, labels)
  3. 对输入进行处理(由定义的网络层进行处理),主要体现在网络的前向传播
  4. 计算loss ,由Loss层计算(本代码还计算了accuracy)
  5. 反向传播求梯度
  6. 根据梯度改变参数值,最简单的实现方式(SGD)为:
    weight = weight - learning_rate * gradient

:红色代表注释

from __future__ import division
from __future__ import print_function
import numpy as np
import time
import argparse

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

from mycode_utils import load_data, accuracy
from mycode_models import GCN

parser=argparse.ArgumentParser()
parser.add_argument('--no-cuda', action='store_true', default=False, help='Disables CUDA training') #不用cuda训练
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 training')
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')
parser.add_argument('--lr', default=0.01, type=float, help='learning rate')
parser.add_argument('--weight_decay', default=5e-4, type=float, help='Weight decay(L2 loss on parameter)')
args=parser.parse_args()

args.cuda=not args.no_cuda and torch.cuda.is_available() #确定是否可以使用cuda
np.random.seed(args.seed)
torch.manual_seed(args.seed)
if args.cuda:
    torch.cuda.manual_seed(args.seed)

features, labels, adj, idx_train, idx_val, idx_test=load_data()
model=GCN(nfeat=features.shape[1],
          nhid=args.hidden,
          nclass=labels.max().item()+1,
          dropout=args.dropout)
#初始化 GCN __init__()—> GraghConvolution __init()__ -> GraghConvolution reset_parameter()
'''
构造一个优化器对象optimizer,用来保存当前状态,并能根据计算得到的梯度更新参数。
Adam优化器
model.parameter()获取网络的参数
'''
optimizer=optim.Adam(model.parameters(), lr=args.lr, weight_decay=args.weight_decay)
#如果gpu可以,放到gpu上跑
if args.cuda:
    features=features.cuda()
    labels=labels.cuda()
    adj=adj.cuda()
    idx_train=idx_train.cuda()
    idx_val=idx_val.cuda()
    idx_test=idx_test.cuda()

def train(epoch):
    t=time.time()
    model.train()   #固定语句,将模型转为训练模式
    '''将所有的梯度置为 0,需要在下个批次计算梯度之前调用'''
    optimizer.zero_grad()   #也就是把loss关于weight的导数变成0

    output=model(features, adj)
    #output:{Tensor:(2708, 7)}
    loss_train=F.nll_loss(output[idx_train], labels[idx_train])
    acc_train=accuracy(output[idx_train], labels[idx_train])
    loss_train.backward()   #每次训练迭代时,使用完整的数据集来执行批量梯度下降
    '''使用优化器的 step 来进行前向传播,而不用人工的更新所有参数'''
    optimizer.step()
    '''是否在训练时验证'''
    if not args.fastmode:
        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),
          'loss_train:{:.4f}'.format(loss_train),
          'acc_train:{:.4f}'.format(acc_train),
          'loss_val:{:.4f}'.format(loss_val),
          'acc_val:{:.4f}'.format(acc_val),
          'time:{:.4f}s'.format(time.time()-t))

def test():
    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_test:{:.4f}'.format(loss_test),
          'acc_test:{:4f}'.format(acc_test))

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

torch.optim

 它有各种优化算法,可以使用优化器的 step 来进行前向传播,而不用人工的更新所有参数

import torch.optim as optim
'''将所有的梯度置为 0,需要在下个批次计算梯度之前调用'''
optim.step()
'''使用优化器的 step 来进行前向传播,而不用人工的更新所有参数'''
optim.zero_grad()

运行结果

Pygcn源码解读_第1张图片


参考文章:

Pygcn源码注释——知乎 这篇文章十分详细

  1. pytorch框架下—GCN代码详细解读
  2. PyTorch(1) torch.nn与torch.nn.functional之间的区别和联系_GZHermit的博客-CSDN博客
  3. Pytorch笔记 之 torch.nn 模块简介_子耶-CSDN博客

你可能感兴趣的:(python学习,python,gcn,源码)