GCN每一层的输入都是节点特征矩阵H和邻接矩阵A,直接将这两个做内积,再乘以一个参数矩阵W,用激活函数激活,就形成一个简单的神经网络层。
1. 邻接矩阵标准化
import numpy as np
import scipy.sparse as sp
if FLAGS.model == 'gcn':
support = [preprocess_adj(adj)]
num_supports = 1
model_func = GCN
1.1 邻接矩阵+单位矩阵
但是因为邻接矩阵的对角都是0,和特征矩阵内积相当于将邻接矩阵做了加权和,节点特征的值成为了邻接矩阵的权,自身的特征被忽略。为避免这种情况,可以先给A加上一个单位矩阵I,单位矩阵。它是个方阵,从左上角到右下角的对角线(称为主对角线)上的元素均为1。除此以外全都为0。这样,使得邻接矩阵的对角元素变成1。
1.2 归一化处理
def normalize_adj(adj):
"""Symmetrically normalize adjacency matrix."""
adj = sp.coo_matrix(adj) # 采用三元组(row, col, data)的形式存储稀疏邻接矩阵
rowsum = np.array(adj.sum(1)) # 按行求和得到rowsum, 即每个节点的度
d_inv_sqrt = np.power(rowsum, -0.5).flatten() # (行和rowsum)^(-1/2)
d_inv_sqrt[np.isinf(d_inv_sqrt)] = 0. # isinf部分赋值为0
d_mat_inv_sqrt = sp.diags(d_inv_sqrt) # 对角化; 将d_inv_sqrt 赋值到对角线元素上, 得到度矩阵^-1/2
return adj.dot(d_mat_inv_sqrt).transpose().dot(d_mat_inv_sqrt).tocoo() # (度矩阵^-1/2)*邻接矩阵*(度矩阵^-1/2)
2. 切比雪夫多项式近似
def chebyshev_polynomials(adj, k):
"""Calculate Chebyshev polynomials up to order k. Return a list of sparse matrices (tuple representation)."""
print("Calculating Chebyshev polynomials up to order {}...".format(k))
adj_normalized = normalize_adj(adj)
laplacian = sp.eye(adj.shape[0]) - adj_normalized # L=I-D^{-1/2} A D^{-1/2}
largest_eigval, _ = eigsh(laplacian, 1, which='LM') # 得到laplacian矩阵的最大特征值
scaled_laplacian = (2. / largest_eigval[0]) * laplacian - sp.eye(adj.shape[0]) # 对laplacian矩阵进行scale处理
t_k = list()
t_k.append(sp.eye(adj.shape[0]))
t_k.append(scaled_laplacian)
def chebyshev_recurrence(t_k_minus_one, t_k_minus_two, scaled_lap):
s_lap = sp.csr_matrix(scaled_lap, copy=True)
return 2 * s_lap.dot(t_k_minus_one) - t_k_minus_two
for i in range(2, k+1):
t_k.append(chebyshev_recurrence(t_k[-1], t_k[-2], scaled_laplacian))
return sparse_to_tuple(t_k)
GCN中一个重要的矩阵是邻接矩阵,其中一个重要操作步骤是邻接矩阵标准化,这里介绍两种建立标准化邻接矩阵。
import torch
import numpy as np
impoer scipy.sparse as sp
def normalize_adj(adj):
adj = adj.to_dense().cpu().numpy()
adj = sp.coo_matrix(adj) #构建张量
rowsum = np.array(adj.sum(1))#每行的数加在一起
d_inv_sqrt = np.power(rowsum, -0.5).flatten() #输出rowsum ** -1/2
d_inv_sqrt[np.isinf(d_inv_sqrt)] = 0. #溢出部分赋值为0
d_mat_inv_sqrt = sp.diags(d_inv_sqrt) #对角化
adj = adj.dot(d_mat_inv_sqrt).transpose().dot(d_mat_inv_sqrt).tocoo() #转置后相乘
return torch.FloatTensor(adj.todense()).cuda()
第二种方法是利用torch来构建标准化邻接矩阵:
import torch
import numpy as np
impoer scipy.sparse as sp
def normalize_adj_torch(mx):
mx = mx.to_dense() #构建张量
rowsum = mx.sum(1) #每行的数加在一起
r_inv_sqrt = torch.pow(rowsum, -0.5).flatten() #输出rowsum ** -1/2
r_inv_sqrt[torch.isinf(r_inv_sqrt)] = 0. #溢出部分赋值为0
r_mat_inv_sqrt = ttorch.diag(r_inv_sqrt) #对角化
mx = torch.matmul(mx, r_mat_inv_sqrt)
mx = torch.transpose(mx, 0, 1) #转置
mx = torch.matmul(mx, r_mat_inv_sqrt)
return mx
附:
torch.mm(mat1, mat2) 只能进行矩阵乘法,也就是输入的两个 tensor 维度只能是 (n x m) 和 (m x p)
torch.bmm(batch1, batch2) 是两个三维张量相乘,两个输入tensor维度是 (b x n x m) 和 (b x m x p),第一维 b 代表 batch size,输出为 (b x n x p)
torch.matmul(tesnsor1, tensor2) 可以进行张量乘法,输入可以是高维。比如 tensor1 是 (j x 1 x n x m), tensor2 是 (k x m x p),输出为 (j x k x n x p)