目录
这真是我读过的最难的一篇图池化了。有些没有理解的地方等日后再来拾遗。
本文作者来自Michigan State University,Pennsylvania State University以及IBM T. J. Watson Research Center。
图池化把节点分组为子图(对应池化之后的超节点),根据这些子图对图进行粗化,然后通过子图中对应的节点生成超节点的特征,将整个图信息简化为粗化图。在对超节点特征进行池化时,通常会忽略这些组节点的结构(局部结构),而采用平均池化或最大池化。但是对于不同的子图来说,子图可能包含不同数量的节点,因此固定大小的池操作符不能适用于所有子图;子图可以有非常不同的结构,这可能需要不同的方法来总结超节点表示的信息。为此,本文提出了一种适应不同子图结构的池化方法——基于傅里叶变换的池化方法,EigenPool,并以此提出了EigenGCN。
github传送门:https://github.com/alge24/eigenpooling
1. EigenPool
从具体的架构来看,DiffPool比较类似。对于池化的任务,普遍分为两个部分:
- 图粗化,即将图划分为一组子图,并将子图视为超节点,从而形成粗化图;
- 利用特征池将原始图信号信息转化为粗化图上定义的图信号。
1.1 Graph Coarsening
图粗化的过程是一个子图划分的过程,将原图划分为一组没有重叠节点的一组子图。给定一个图 G G G,包含 K K K个子图 G ( k ) G^{(k)} G(k),使用A∈RN×N表示邻接矩阵,X∈RN×d表示特征, N k N_k Nk表示子图 k k k中的结点数, Γ ( k ) Γ^{(k)} Γ(k)表示子图 G ( k ) G^{(k)} G(k)中的所有结点的列表。定义一个采样操作 C ( k ) ∈ R N × N K C^{(k)}∈R^{N×N_K} C(k)∈RN×NK:
C
(
k
)
[
i
,
j
]
C^{(k)}[i,j]
C(k)[i,j]表示i,j位置上的元素,
Γ
(
k
)
(
j
)
Γ^{(k)}(j)
Γ(k)(j)则表示
Γ
(
k
)
Γ^{(k)}
Γ(k)中的第
j
j
j个元素。首先需要明确
C
(
k
)
C^{(k)}
C(k)是一个比较特殊的矩阵,我们把
i
i
i视为行,表示
N
N
N个结点其中的一个,
j
j
j视为列表示结点
i
i
i属于哪个子图,那么
C
(
k
)
C^{(k)}
C(k)本身就是一个行只能为one-hot向量或者是0向量的矩阵,因为按照本文的定义,一个结点不可能同属两个子图。而对于不同的子图,因为子图的结点数量不同,对应的
C
(
k
)
C^{(k)}
C(k)的大小也不尽相同(对此会有一个补全操作,到特征池那里再说)。
这样,子图
k
k
k的邻接矩阵
A
(
k
)
∈
R
N
k
×
N
k
A^{(k)}∈R^{N_k×N_k}
A(k)∈RNk×Nk就可以表示为:
由于
C
(
k
)
C^{(k)}
C(k)只表示子图
k
k
k的采样,因此只有在子图中节点对应的行处才存在非0值,因此计算得出的结果只由每个子图内部的边组成。对其进行逆变换,就可以得到子图在原图中的结构特征,但是由于
C
(
k
)
C^{(k)}
C(k)无法表示跨越子图之间的关系,因此做逆变换(up-sampling):
之后,得到的
A
i
n
i
t
A_{init}
Ainit仅仅是子图内部结点之间的关系,这样跨越子图的关系也就可以计算:
A
e
x
t
=
A
−
A
i
n
i
t
A_{ext}=A-A_{init}
Aext=A−Ainit。这个操作可以理解为:原图的边A-(所有子图之内的边),就得到了子图之间的边。(这部分不好理解,可以试着构建一个K=2的简单矩阵自己推一下)
但是此时得到的
A
e
x
t
A_{ext}
Aext是一个
N
×
N
N×N
N×N的矩阵,因此还需要通过assignment matrix(对,就是与DiffPool里的那个assignment matrix差不多)缩放到大小为
N
K
N_K
NK。定义
S
∈
R
N
×
K
S∈R^{N×K}
S∈RN×K:
粗化之后的矩阵表示为:
至此,粗化邻接矩阵的步骤结束。呼,如此麻烦。但是从代码上来看,由于直接采用了封装好的谱聚类方法,所以还是比较好理解。看代码:
def _coarserning_pooling_(self, adjacency_matrix, pooling_size, normalize=False):
num_nodes = adjacency_matrix[:,0].shape[0]
A_dense = adjacency_matrix.todense()
num_clusters = int(num_nodes/pooling_size)
if num_clusters == 0:
num_clusters = num_clusters + 1
sc = SpectralClustering(n_clusters = num_clusters, affinity= 'precomputed', n_init=10) # 谱聚类,需要指明cluster的数量
sc.fit(A_dense)
clusters = dict()
for inx, label in enumerate(sc.labels_): # 从聚类的结果里解析出不同聚类的邻接矩阵
if label not in clusters:
clusters[label] = []
clusters[label].append(inx)
num_clusters = len(clusters)
num_nodes_in_largest_clusters = 0
for label in clusters:
if len(clusters[label])>=num_nodes_in_largest_clusters:
num_nodes_in_largest_clusters = len(clusters[label])
if num_nodes_in_largest_clusters <=5:
num_nodes_in_largest_clusters = 5
num_nodes_in_largest_clusters = 5 # 这个最后把最大cluster中的节点数量设置为了5我没咋理解
Adjacencies_per_cluster = [adjacency_matrix[clusters[label],:][:,clusters[label]] for label in range(len(clusters))]
######## Get inter matrix
A_int = sp.lil_matrix(adjacency_matrix)
for i in range(len(clusters)):
zero_list = list(set(range(num_nodes)) - set(clusters[i]))
for j in clusters[i]:
A_int[j,zero_list] = 0
A_int[zero_list,j] = 0
######## Getting adjacenccy matrix wuith only external links
A_ext = adjacency_matrix - A_int
######## Getting cluster vertex indicate matrix
row_inds = []
col_inds = []
data = []
for i in clusters:
for j in clusters[i]:
row_inds.append(j)
col_inds.append(i)
data.append(1)
Omega = sp.coo_matrix((data,(row_inds,col_inds))) # Omega就是公式中的S
A_coarsened = np.dot( np.dot(np.transpose(Omega),A_ext), Omega)
Eigenvector-Based Pooling
对每个已经划分好的子图,需要通过傅里叶变换提取特征。因为傅里叶变换是以拉普拉斯矩阵为基础的,所以可以说傅里叶变换之后既考虑了结构又考虑了特征。首先,同GCN一样,先定义拉普拉斯矩阵:
L
=
D
−
A
L = D−A
L=D−A。
D
D
D是度矩阵。原文的表示法为(顺便吐槽一下这篇论文写得真是拖泥带水,连简单的度矩阵公式看着都别扭,嗨呀):
拉普拉斯矩阵对应一组特征向量
{
u
l
}
l
=
1
N
\lbrace u_l \rbrace^N_{l=1}
{ul}l=1N以及特征值
{
λ
l
}
l
=
1
N
\lbrace λ_l \rbrace^N_{l=1}
{λl}l=1N,给定一个图信号,则对应傅里叶变换为:
U
U
U是所有特征向量构成的矩阵。那么傅里叶变换的逆变换为:
让
L
(
k
)
L^{(k)}
L(k)为子图k的拉普拉斯矩阵,其对应的特征为
u
1
(
k
)
u^{(k)}_{1}
u1(k)…
u
N
k
(
k
)
u^{(k)}_{N_k}
uNk(k)。通过上采样
C
(
k
)
C^{(k)}
C(k)得到特征在整个图中的表示:
然后将整个上采样之后的特征向量拼接成矩阵作为池操作符
θ
l
∈
R
N
×
K
θ_l∈R^{N×K}
θl∈RN×K:
也就是说,不同的子图在
l
l
l的位置都会对应一个特征,把这个特征拼接起来做成全局的特征。但是,这样又出现一个新的问题,就是每个子图的大小可能不确定,将其补全成最大子图的大小
N
m
a
x
N_{max}
Nmax,对于那些较小的,则在后边补0向量。最终,
l
t
h
l^{th}
lth的池化符
θ
l
θ_l
θl下的池化被定义为:
这里,需要对傅里叶变换的物理意义进行一波理解,我之前也写过这个,但是还是在这里复习一遍。首先,我们就不区分时域和频域,只考虑这样一个事实:傅里叶变换将信号转换为一组正弦波的叠加,每一组正弦波都对应一组特定的基底,在EigenPool中,这个基底就是
θ
l
θ_l
θl。拿Kipf简化之后的GCN来举例,GCN可以理解为一个高通滤波器(也就是让高频率的波通过),在公式中就表示为高频波的基底的叠加。对于GCN来说,只允许零阶和一阶的滤波通过(也就是下面公式里的
θ
I
θI
θI和
θ
L
θL
θL)。而相比于低通滤波器ChebNet,高通滤波器让更少的波通过却取得了更好的效果,这从物理意义上来理解,就是高通滤波器只聚合邻近的结点的信息,而ChebNet则会让更远的节点的信息参与到卷积的过程中来,后者会造成过平滑。
那么回到论文中去,在不同的基底作用下的池化就变成了:
小伙伴们可能会问了,这里也没有基底
θ
l
θ_l
θl啊,那是因为都借助公式(14)转化为X了。而参照GCN对切比雪夫多项式截断的思路,这个
X
p
o
o
l
e
d
X_{pooled}
Xpooled也可以用一个
H
H
H阶的截断来代替,也就是下面的公式:
其中,
H
<
<
N
m
a
x
H<<N_{max}
H<<Nmax,仍然能够保留大量的特征信息(因为高通滤波器)。
THEORETICAL ANALYSIS OF EigenPooling
接下来是对EigenPool进行的原理以及公式的推导,鉴于理解EigenPool已经让我的脑细胞大量死亡,因此这部分留到以后有时间再看。
EXPERIMENT
首先,这几个常用的数据集我也不多说了:
Baseline:
GCN,GraphSAGE,Set2Set,DGCNN,DiffPool,然后再加上EigenGCN-H,
H
=
1
,
2
,
3
H=1,2,3
H=1,2,3。对于每个数据集,我们随机将其分为3个部分,即:80%作为训练集,10%作为验证集,10%作为测试集。我们将随机分割过程重复10次,给出10次不同分割的平均性能。运行结果如下:
- 在大多数情况下,Diff-pool和EigenGCN框架比那些不使用分层池过程的方法执行得更好。分层聚合节点信息有助于更好地学习图的表示。
- 提出的框架EigenGCN与GCN、GraphSage和SET2SET使用的是同一个卷积放肆。然而,提出的框架在大多数数据集优于它们。这进一步表明了分层池程序的必要性。换句话说,所提出的特征池确实可以提高图的分类性能。
- 在大多数数据集中,H越大效果越好。包含更多的特征向量,这表明我们可以在池中保存更多的信息,在大多数情况下可以帮助学习更好的图表示。在一些数据集中,加入更多的特征向量并不会对性能带来任何提高,甚至会使性能变差。理论上,我们可以通过使用更多的特征向量来保存更多的信息。同时,利用较少的特征向量可以对噪声信号进行过滤。
下集预告:基于CRF的池化。