源代码:GitHub - tkipf/pygcn: Graph Convolutional Networks in PyTorch
依据记忆,自己敲了源代码,可能与源代码有细微差别,但也可以正常运行。
注释中给出了一些debug时的流程。
主要由两部分构成:数据预处理load_data()+精度计算accuracy()
提取featues -> 转化为csr_matrix存储 -> 归一化 -> 转变为张量torch.from_numpy()
提取labels -> onehot编码-> 找到节点的标签np.where(labels) -> 转变为张量
注:代码中第20行将前两步合成一步,43行将后两步合成了一步。
论文的idx是杂乱无章的,表示起来不方便,所以构建字典给论文编号。
建立图的过程较复杂,要生成idx, edges和adj.
图的组成 | 步骤 |
idx | 提取idx -> 生成字典,给节点编号 |
edges | 读取边 -> 用节点编号表示边 |
adj | 生成adj -> 对称化 -> 归一化 -> 转变为稀疏张量 |
计算精度, 在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)
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.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)。
'''
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) + ')'
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))
关键代码:
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()为例
- 定义可学习参数的网络结构(堆叠各层和层的设计)前面已经定义好了
- 数据集输入 output=model(features, labels)
- 对输入进行处理(由定义的网络层进行处理),主要体现在网络的前向传播
- 计算loss ,由Loss层计算(本代码还计算了accuracy)
- 反向传播求梯度
- 根据梯度改变参数值,最简单的实现方式(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()
它有各种优化算法,可以使用优化器的
step
来进行前向传播,而不用人工的更新所有参数
import torch.optim as optim
'''将所有的梯度置为 0,需要在下个批次计算梯度之前调用'''
optim.step()
'''使用优化器的 step 来进行前向传播,而不用人工的更新所有参数'''
optim.zero_grad()
参考文章:
Pygcn源码注释——知乎 这篇文章十分详细