原理
论文:“Convolutional Neural Networks on Graphs with Fast Localized Spectral Filtering”
卷积公式:
参数说明:
L
^
\hat{L}
L^ 表示缩放和标准化的拉普拉斯矩阵:
2
L
λ
m
a
x
−
I
\frac{2L}{\lambda_{max}} - I
λmax2L−I
λ
m
a
x
\lambda_{max}
λmax是L分解出的最大特征值,
I
I
I是单位矩阵。
思想:其实仔细观察
Z
(
k
)
Z^{(k)}
Z(k),可以发现当K=1时,chebnet的卷积公式就退化成了GCN:
也就是说GCN是K=1的chebnet,是一种chebbnet的一种简化。而chebnet,来自于拉普拉斯的切比雪夫多项式形式,有兴趣可以自行了解,学过高数的都知道,对一个函数进行级数分解,往往只需要前面1~3项就可以很精确了,因此实际使用中,我们的K也不会很大,基本就是2-3的样子。
说明
对于PyG类:torch_geometric.nn.chebConv
只实现核心功能。
我的输入为:我的输入:data={“graph”:adj[N,N],“flow_x”:features[B,N,D,H]} B和H 可以是1
代码
class ChebConv1(nn.Module):
"""
The ChebNet convolution operation.
:param in_c: int, number of input channels.
:param out_c: int, number of output channels.
:param K: int, the order of Chebyshev Polynomial.
"""
def __init__(self, in_c, out_c, K, bias=True, normalize=True):
super(ChebConv, self).__init__()
self.normalize = normalize
self.weight = nn.Parameter(torch.Tensor(K + 1, 1, in_c, out_c)) # [K+1, 1, in_c, out_c]
init.xavier_normal_(self.weight)
if bias:
self.bias = nn.Parameter(torch.Tensor(1, 1, out_c))
init.zeros_(self.bias)
else:
self.register_parameter("bias", None)
self.K = K + 1
def forward(self, inputs, graph):
"""
:param inputs: the input data, [B, N, C]
:param graph: the graph structure, [N, N]
:return: convolution result, [B, N, D]
"""
L = ChebConv1.get_laplacian(graph, self.normalize) # [N, N]
mul_L = self.cheb_polynomial(L).unsqueeze(1) # [K, 1, N, N]
result = torch.matmul(mul_L, inputs) # [K, B, N, C]
result = torch.matmul(result, self.weight) # [K, B, N, D]
result = torch.sum(result, dim=0) + self.bias # [B, N, D]
return result
# 将Z一次性计算出来,存到列表中备用
def cheb_polynomial(self, laplacian):
"""
Compute the Chebyshev Polynomial, according to the graph laplacian.
:param laplacian: the graph laplacian, [N, N].
:return: the multi order Chebyshev laplacian, [K, N, N].
"""
N = laplacian.size(0) # [N, N]
multi_order_laplacian = torch.zeros([self.K, N, N], device=laplacian.device, dtype=torch.float) # [K, N, N]
multi_order_laplacian[0] = torch.eye(N, device=laplacian.device, dtype=torch.float)
if self.K == 1:
return multi_order_laplacian
else:
multi_order_laplacian[1] = laplacian
if self.K == 2:
return multi_order_laplacian
else:
for k in range(2, self.K):
multi_order_laplacian[k] = 2 * torch.mm(laplacian, multi_order_laplacian[k-1]) - \
multi_order_laplacian[k-2]
return multi_order_laplacian
@staticmethod
def get_laplacian(graph, normalize):
"""
return the laplacian of the graph.
:param graph: the graph structure without self loop, [N, N].
:param normalize: whether to used the normalized laplacian.
:return: graph laplacian.
"""
if normalize:
D = torch.diag(torch.sum(graph, dim=-1) ** (-1 / 2))
L = torch.eye(graph.size(0), device=graph.device, dtype=graph.dtype) - torch.mm(torch.mm(D, graph), D)
else:
D = torch.diag(torch.sum(graph, dim=-1))
L = D - graph
return L
class ChebNet1(nn.Module):
def __init__(self, in_c, hid_c, out_c, K):
"""
:param in_c: int, number of input channels.
:param hid_c: int, number of hidden channels.
:param out_c: int, number of output channels.
:param K:
"""
super(ChebNet1, self).__init__()
self.conv1 = ChebConv1(in_c=in_c, out_c=hid_c, K=K)
self.conv2 = ChebConv1(in_c=hid_c, out_c=out_c, K=K)
self.act = nn.ReLU()
def forward(self, data, device):
graph_data = data["graph"].to(device)[0] # [N, N]
flow_x = data["flow_x"].to(device) # [B, N, H, D]
B, N = flow_x.size(0), flow_x.size(1)
flow_x = flow_x.view(B, N, -1) # [B, N, H*D]
output_1 = self.act(self.conv1(flow_x, graph_data))
output_2 = self.act(self.conv2(output_1, graph_data))
return output_2.unsqueeze(2)
总结
chebNet 比GCN稍微复杂一些,主要是Z值得计算比较麻烦一些。