在2008年,Franco Scarselli和Marco Gori等人最早提出了图神经网络(GNN)的概念,并将研究成果汇总撰写了The Graph Neural Network Model一文,发表在IEEE的期刊上。
在这一部分,我们就先来看看这个最早的图神经网络模型是怎样的。这一章节的安排是这样的:我们先了解GNN这个模型的最终目标,进而把它抽象成一个数学问题,最后将问题归结到一点并应用一定的理论知识对其进行建模、求解。
GNN的目标
本质上讲GNN也是一个embedding方法,那么什么是embedding呢?借用维基百科对word embedding的定义:Word embedding is the collective name for a set of
language modeling and feature learning techniques in natural language
processing (NLP) where words or phrases from the vocabulary are mapped
to vectors of real numbers. Conceptually it involves a mathematical
embedding from a space with many dimensions per word to a continuous
vector space with a much lower dimension。
这段话的意思是一个从具有许多维的空间到具有低得多的维的连续向量空间的数学嵌入。说简单点,就是用一个定长的向量对某一个客观对象进行表示。比如在GNN中,我们要表示的graph中的节点,在embedding之后可能就是一个长度为100的实数向量,这里说可能是因为这个向量的长度是可以自定义的,长度越长表示的效果越好,相应的训练代价也就越大。
从形式上来说,GNN最终的目标应该是实现图的节点的嵌入 h v \boldsymbol{h}_v hv,也就是上文所解释的embedding。然后基于这个嵌入计算一些有用的输出信息 o v \boldsymbol{o}_v ov。注意这个图指的图论中的图,是graph而不是image,由边和节点构成,节点一般是某个具体的物体,边一般代表了与之相连的节点之间具有某种关系,而这种关系又是认为定义的。比如一个社交网络,可以是一个图,图上的每一个节点代表一个人,图中的每一个边表示朋友关系,那么在这个图中,如果某两个节点之间有一条边的话,就意味着这两个节点对应的人是朋友。当然,这个例子是图的一种,叫做无向图,因为朋友关系是相互的,如果a是b的朋友,那么b也一定是a的朋友,当然也存在单向的关系,比如父子关系,这个时候,我们称相应的图为有向图。关于图论的知识,读者可以从离散数学这门课中学到更多更详细的知识,这并非这本书的重点,在这里就不再赘述了。
回来看GNN中的两个变量 h v \boldsymbol{h}_v hv和 o v \boldsymbol{o}_v ov。在GNN模型中, h v \boldsymbol{h}_v hv被称作节点 v v v的隐藏状态。当最终GNN模型计算完成时, h v \boldsymbol{h}_v hv应该包含了当前节点 v v v的及其相邻节点的信息。因此这是一个聚合计算的过程,也就是当前节点 v v v需要不断地从相邻节点收集信息然后用这些信息更新自己的隐藏状态。
而从 h v \boldsymbol{h}_v hv计算 o v \boldsymbol{o}_v ov就很简单了。以神经网络的实现方式为例,这其实是一个简单的前向传播的过程, o v \boldsymbol{o}_v ov作为神经网络的输入,该网络的输出就是 o o \boldsymbol{o}_o oo。当然你可能会问了,难道这个神经网络不用进行一定的训练么?答案是:肯定需要一定的训练,我们在这里说的是已经训练好的网络该如何使用,当然巡礼男的过程也很重要,这在后面会讲到,请耐心读下去。
经过上面的分析,我们可以得到结论,在这个GNN模型中, h v \boldsymbol{h}_v hv的计算是最主要的。因此接下来我们就先来分析一下 o v \boldsymbol{o}_v ov的计算方法。
计算 h v \boldsymbol{h}_v hv
首先,我们通过一个抽象的公式来表示
h
v
\boldsymbol{h}_v
hv的计算过程,需要注意的是,这个公式并不能直接用于计算
h
v
\boldsymbol{h}_v
hv,因为在这里我们还没有指出它的具体形式,但是这个公式可以告诉我们在计算时需要用到哪些信息,并将有助于我们进行更加形象具体的思考。具体的公式如下:
h
v
=
f
(
x
v
,
x
c
o
[
v
]
,
h
n
e
[
v
]
,
x
n
e
[
v
]
)
\boldsymbol{h}_v = f(\boldsymbol{x}_v,\boldsymbol{x}_{co[v]},\boldsymbol{h}_{ne[v]},\boldsymbol{x}_{ne[v]})
hv=f(xv,xco[v],hne[v],xne[v])
我们先来解释一下公式中的符号, f f f 被称为局部转移函数, f f f的四个输入分别是: x v \boldsymbol{x}_v xv, x c o [ v ] \boldsymbol{x}_{co[v]} xco[v], h n e [ v ] \boldsymbol{h}_{ne[v]} hne[v], x n e [ v ] \boldsymbol{x}_{ne[v]} xne[v]。更进一步地, x v \boldsymbol{x}_v xv代表节点 v v v的特征, x c o [ v ] \boldsymbol{x}_{co[v]} xco[v]指的是与节点 v v v 相连的边的特征 , h n e [ v ] \boldsymbol{h}_{ne[v]} hne[v]是与节点 v v v 相邻的节点的隐藏状态, x n e [ v ] \boldsymbol{x}_{ne[v]} xne[v]是与节点 v v v 相邻的节点的特征。简单解释一下特征的含义,所谓特征,可以理解为节点或者边的属性,举个例子,两个节点 u u u和 v v v之间具有一定的距离 d d d,那么这个时候这个距离 d d d就是 u u u的一个属性,具体的可以记为 ( v , d ) (v,d) (v,d),表示自己到 v v v这个节点的距离是 d d d。
在这里容易混淆的是节点的特征和节点的隐藏状态这两个概念,节点的特征指的是节点的固有属性,比如节点的位置、节点的大小等等,就像刚刚举的例子,这些在GNN模型的计算中是已知的。而节点的隐藏状态指的是当前节点在全局下的一个状态,仅仅从单一节点的属性中无法得到。举个例子,比如在一个社交网络图上,每个节点表示一个账号或者说一个用户,每个边表示following关系,现在我们要判断某个账号是不是水军账号,则我们可能还需要用到那些follow当前账号的其它账号是不是水军账号,那么这个时候我们其实要用到当前节点的相邻节点的一些属性,只依靠当前节点的属性比如有多少人follow是肯定不够的。但是通过聚合相邻节点的信息得到的隐藏状态却可以做到这一点,因为它已经包含了计算所需要的全部信息。
通过上面两段的分析,我们已经知道了计算隐藏状态的需要的数据,但是仅仅通过这个公式不足以计算 h v \boldsymbol{h}_v hv。两个问题:第一, f f f 的具体形式并没有给出;第二,输入中的 h n e [ v ] \boldsymbol{h}_{ne[v]} hne[v]与 h v \boldsymbol{h}_v hv是同一类型的变量,在一开始也是不知道的。针对第一个问题,可以通过一个神经网络来解决,也就是说把函数 f f f的输入作为神经网络的输入, h v \boldsymbol{h}_v hv作为神经网络的输出,从而在神经网络上 f f f 有了具体的形式。对于第二个问题,我们需要接著一个定理来解决这个事情,那就是巴拿赫不动点定理。接下来,我们就先来介绍一下这个定理,并且将这个定理与我们的GNN模型的求解关联起来,从而计算出每个节点的隐藏状态 h v \boldsymbol{h}_v hv。
但是在此之前,我们先来解决读者心中可能存在的一个小的疑问,那就是:神经网络虽然赋予了函数 f f f具体的形式,但它也有自己的参数,那么这些参数是怎么训练得到的呢?在这里我想说的是,通过不动点算法并不能得到神经网络应该有的参数,这个参数只能从神经网络的训练中得到,请读者耐心继续阅读,在后面你一定会得到你想要的答案。
好了,那么接下来就让我们先一起学习一下不动点算法,以及它是如何被应用到GNN模型中的。
利用不动点迭代算法求解隐藏状态
巴拿赫不动点迭代算法
值得注意的一点是,不动点算法有很多推论,但是并不是所有的这些结论对我们的讲解都是有用的,因此这里的不动点算法只包含了原有理论的一部分内容,如果想要了解更多相关的内容,可以去看The
Contraction Mapping Theorem这篇论文。
现在我将后面的讲解会用到的所有结论列举如下:
-
定义映射
f : X → X f:X \rightarrow X f:X→X,如果 ∃ 0 ≤ c < 1 \exists 0 \leq c < 1 ∃0≤c<1,使得 ∀ x , x ′ ∈ X , d ( f ( x ) , f ( x ′ ) ) ≤ c ∗ d ( x , x ′ ) \forall x,x' \in X, d(f(x),f(x')) \leq c * d(x,x') ∀x,x′∈X,d(f(x),f(x′))≤c∗d(x,x′),则函数 f f f在 X X X上有唯一不动点。其中 d d d是在该空间中距离的衡量方式,不限于欧式空间。 -
得到的不动点是 f ( x ) = x f(x) = x f(x)=x 的解。
-
任意 x 0 ∈ X x_0 \in X x0∈X,序列 x 0 , f ( x 0 ) , f ( f ( x 0 ) ) , … x_0,f(x_0),f(f(x_0)),\dots x0,f(x0),f(f(x0)),…收敛至这个不动点。
-
算法可以很快的收敛。
-
算法中的 x 0 x_0 x0可以是一个值,也可以是一个向量,即高维点,只要合理定义距离的计算方法即可 ∀ x , x ′ d ( x , x ′ ) ≥ 0 & ∀ a , b , c , d ( a , b ) ≤ d ( a , c ) + d ( b , c ) \forall x,x' d(x,x') \geq 0\ \&\ \forall a,b,c,d(a,b) \leq d(a,c) + d(b,c) ∀x,x′d(x,x′)≥0 & ∀a,b,c,d(a,b)≤d(a,c)+d(b,c)。一般用范数来定义向量之间的距离。
接下来,我们对这些结论进行依次说明。
首先是存在性,对应的是第一条中的从某个点出发一定能收敛到某个点。我们可以假设一个数列 a n = f n ( x 0 ) , a 0 = x 0 a_n = f^n(x_0),a_0 = x_0 an=fn(x0),a0=x0。则存在性问题相当于是数列的收敛性问题,在这里,我们通过证明数列中相邻位置的两个元素的距离是有上界的,并且这个上界的极限是0,从而证明数列是收敛的,也就是不动点是存在的。
我们计算 d ( a n + 1 , a n ) ≤ c ∗ d ( a n , a n − 1 ) ≤ ⋯ ≤ c n ∗ d ( a 1 , a 0 ) d(a_{n + 1},a_n) \leq c * d(a_{n},a_{n-1}) \leq \dots \leq c^n * d(a_1,a_0) d(an+1,an)≤c∗d(an,an−1)≤⋯≤cn∗d(a1,a0)。当 n n n趋于正无穷时,也就是经过了很多轮的迭代之后, c n c^n cn是趋于0的,因为在条件中我们已经声明 0 ≤ c < 1 0 \leq c < 1 0≤c<1了,同时 d ( a 1 , a 0 ) d(a_1,a_0) d(a1,a0)又是一个常数,因此 c n ∗ d ( a 1 , a 0 ) c^n * d(a_1,a_0) cn∗d(a1,a0)的极限也是0,因此数列 a n a_n an一定收敛,即不动点一定是存在的。
其次,我们证明在同一个域中不动点的唯一性,也就是说是在相同的 X X X内,从任意的 x 0 ∈ X x_0 \in X x0∈X出发,经过迭代,最后都会收敛到同一个值。这对应的是第一点,同时将存在性和唯一性结合起来看,也可以很直观地理解第三点中的结论。
为了证明这一点,我们采取反证法,假设 a a a 和 a ′ a' a′ 都是 f f f 的不动点,并且 a ≠ a ′ a \neq a' a=a′,也就是说在函数的域内有两个不同的不动点。考虑两个不动点之间的距离,如果能够证明两个不动点之间的距离不可能不为0,那么我们就认为假设错误,从而证明了唯一性。为此,我们计算 d ( a , a ′ ) d(a,a') d(a,a′), d ( a , a ′ ) = d ( f ( a ) , f ( a ′ ) ) d(a,a') = d(f(a),f(a')) d(a,a′)=d(f(a),f(a′)),这一步是因为既然 a a a是函数的不动点,根据刚才得到的存在性,我们知道最后 f n ( x 0 ) = f n − 1 ( x 0 ) f^n(x_0) = f^{n - 1}(x_0) fn(x0)=fn−1(x0),因此 a = f ( a ) a = f(a) a=f(a),同理 a ′ = f ( a ′ ) a' = f(a') a′=f(a′),因此有 d ( a , a ′ ) = d ( f ( a ) , f ( a ′ ) ) d(a,a') = d(f(a),f(a')) d(a,a′)=d(f(a),f(a′))。更进一步地, d ( f ( a ) , f ( a ′ ) ) ≤ c ∗ d ( a , a ′ ) d(f(a),f(a')) \leq c * d(a,a') d(f(a),f(a′))≤c∗d(a,a′),这个就是第一点中给出的条件,在证明存在性时也在反复利用这一条件。最后我们得到 d ( a , a ′ ) ≤ c ∗ d ( a , a ′ ) d(a,a') \leq c * d(a,a') d(a,a′)≤c∗d(a,a′)。分析这个不等式,如果 a ≠ a ′ a \neq a' a=a′,那么 d ( a , a ′ ) > 0 d(a,a') > 0 d(a,a′)>0,则 c ≥ 1 c \geq 1 c≥1,这显然与条件矛盾,因此假设错误,不动点的唯一性得到证明。
接下来我们来证明第二点,不动点是方程 f ( x ) = x f(x) = x f(x)=x的解。这个其实很好理解,我们来看。最终收敛时 f n ( x 0 ) = f ( f n − 1 ( x 0 ) ) = f n − 1 ( x 0 ) f^{n}(x_0) = f(f^{n - 1}(x_0)) = f^{n - 1}(x_0) fn(x0)=f(fn−1(x0))=fn−1(x0),从形式上当不动点是 f ( x ) = x f(x) = x f(x)=x的解的时候能够满足这一条件,再根据唯一性,这一结论不言而喻。
然后,我们来证明快速收敛性,也就是第四个结论。
关于这一点,其实很好证明,在存在性中我们已经指出了从任意初始点计算出不动点的迭代步数上界,即 n ≤ l o g c ( ϵ d ( a , a ′ ) ) n \leq log_c(\frac{\epsilon}{d(a,a')}) n≤logc(d(a,a′)ϵ),这个不等式是 c n ∗ d ( a 1 , a 0 ) ≤ ϵ c^n * d(a_1,a_0) \leq \epsilon cn∗d(a1,a0)≤ϵ的解。其中 ϵ \epsilon ϵ指当相邻两次的迭代结果小于这个值即认为收敛,所以 ϵ \epsilon ϵ可以认为是一个无穷小。因为是 l o g log log的,所以可以很快收敛。一般最多迭代30次左右,即可收敛到很小的值。
最后,其实是关于点的理解,这个点可以是一个普通意义上数轴上的一个点,也可以是一个向量,通常被称作高维点,甚至可以是一个矩阵,在后面的应用中,我们其实就是以矩阵的形式,对不动点算法进行应用。不管是什么样的点,只要我们定义好距离
d
d
d的计算方法,使他满足三角不等式以及其它的一些限制,如下面的公式所列出来的,就能够满足我们上面说的所哟结论,也自然可以应用不动点算法。
∀
a
,
b
,
c
d
(
a
,
b
)
≤
d
(
a
,
c
)
+
d
(
b
,
c
)
∀
a
,
b
d
(
a
,
b
)
≥
0
∀
a
≠
b
d
(
a
,
b
)
≠
0
\begin{aligned} & \forall a,b,c\ d(a,b) \leq d(a,c) + d(b,c)\\ & \forall a,b\ d(a,b) \geq 0\\ & \forall a \neq b\ d(a,b) \neq 0\end{aligned}
∀a,b,c d(a,b)≤d(a,c)+d(b,c)∀a,b d(a,b)≥0∀a=b d(a,b)=0
以上是我们会用到的所有的关于不动点理论的结论,下面,我们就开始基于这些结论,对 h v = f ( x v , x c o [ v ] , h n e [ v ] , x n e [ v ] ) \boldsymbol{h}_v = f(\boldsymbol{x}_v,\boldsymbol{x}_{co[v]},\boldsymbol{h}_{ne[v]},\boldsymbol{x}_{ne[v]}) hv=f(xv,xco[v],hne[v],xne[v])进行求解。
将不动点算法上应用于求解隐藏状态上
现在我们开始不动点算法的应用,首先,我们把所有的节点的计算公式都列出来,这里的所有节点指的是图中的所有结点,我们给所有的节点记为
v
i
v_i
vi,其中
1
≤
i
≤
n
1 \leq i \leq n
1≤i≤n,
n
n
n是节点的总的个数。
h
v
1
=
f
(
x
v
1
,
x
c
o
[
v
1
]
,
h
n
e
[
v
1
]
,
x
n
e
[
v
1
]
)
h
v
2
=
f
(
x
v
2
,
x
c
o
[
v
2
]
,
h
n
e
[
v
2
]
,
x
n
e
[
v
2
]
)
…
h
v
n
=
f
(
x
v
n
,
x
c
o
[
v
n
]
,
h
n
e
[
v
n
]
,
x
n
e
[
v
n
]
)
\begin{aligned} & \boldsymbol{h}_{v_1} = f(\boldsymbol{x}_{v_1},\boldsymbol{x}_{co[{v_1}]},\boldsymbol{h}_{ne[{v_1}]},\boldsymbol{x}_{ne[{v_1}]})\\ & \boldsymbol{h}_{v_2} = f(\boldsymbol{x}_{v_2},\boldsymbol{x}_{co[{v_2}]},\boldsymbol{h}_{ne[{v_2}]},\boldsymbol{x}_{ne[{v_2}]})\\ & \dots\\ & \boldsymbol{h}_{v_n} = f(\boldsymbol{x}_{v_n},\boldsymbol{x}_{co[{v_n}]},\boldsymbol{h}_{ne[{v_n}]},\boldsymbol{x}_{ne[{v_n}]})\end{aligned}
hv1=f(xv1,xco[v1],hne[v1],xne[v1])hv2=f(xv2,xco[v2],hne[v2],xne[v2])…hvn=f(xvn,xco[vn],hne[vn],xne[vn])
现在我们定义两个集合, { h v i } , { h n e [ v i ] } \{\boldsymbol{h}_{v_i}\}, \{\boldsymbol{h}_{ne[v_i]}\} {hvi},{hne[vi]}。 { h v i } \{\boldsymbol{h}_{v_i}\} {hvi}表示包含图中的所有的节点的集合, { h n e [ v i ] } \{\boldsymbol{h}_{ne[v_i]}\} {hne[vi]}是所有节点的邻接节点的集合。很显然, { h v i } = { h n e [ v i ] } \{\boldsymbol{h}_{v_i}\} = \{\boldsymbol{h}_{ne[v_i]}\} {hvi}={hne[vi]},因为它们都是图里面的节点。所以,我们不妨把公式统一表达为
H = F ( H , X ) \boldsymbol{H} = F(\boldsymbol{H},\boldsymbol{X}) H=F(H,X)
其中
H
\boldsymbol{H}
H指的是所有节点的隐藏状态,是一个矩阵,其实就是直接把所有的隐藏状态对应的向量进行简单拼接。
X
\boldsymbol{X}
X也是一个矩阵,包含了所有的节点的输入信息,在GNN模型中是已知量。因此
H
\boldsymbol{H}
H
就是要求的不动点,现在就可以利用不动点算法求解隐藏状态了。
在解决了隐藏状态的求解之后,我们已经打通了所有的技术关节,下面可以开始讲解GNN模型的结构了。
GNN 网络结构
直接截取的论文中的图片,图中的 l \boldsymbol{l} l就是上面说的 X \boldsymbol{X} X。
先说 f W f_{\boldsymbol{W}} fW。每一个 f W f_{\boldsymbol{W}} fW 是一个单独的神经网络,对应了一个节点的 f f f 的计算,每一列 f W f_{\boldsymbol{W}} fW对应了 F F F。从右向左,对应的是迭代计算,注意这个图中还没有包含反向传播的过程,仅仅是不动点迭代算法的进行,而且在不同列的 f W f_{\boldsymbol{W}} fW的连接是要与 图 对应的。举个例子,如果两个节点 u u u 和 v v v 之间有一条从 u u u 到 v v v的边,则 v v v 对应的 f W f_{\boldsymbol{W}} fW 的输入中应该包含 u u u 对应的 f W f_{\boldsymbol{W}} fW 的输出。在具体实现时,其实并没有搭建这么多列的 f W f_{\boldsymbol{W}} fW网络,而是用一个循环神经网络代替。
再说
g
W
\mathbb{g}_{\boldsymbol{W}}
gW。
f
W
f_{\boldsymbol{W}}
fW的计算结果,输出到
g
W
\mathbb{g}_{\boldsymbol{W}}
gW 中进行计算。而
g
W
\mathbb{g}_{\boldsymbol{W}}
gW对应的就是
o
v
\boldsymbol{o}_v
ov的计算,公式为
o
v
=
g
(
h
v
,
x
v
)
\boldsymbol{o}_v = g(\boldsymbol{h}_v,\boldsymbol{x}_v)
ov=g(hv,xv) 然后,输出的
o
v
\boldsymbol{o}_v
ov 与
t
v
\boldsymbol{t}_v
tv (
t
v
\boldsymbol{t}_v
tv是训练集中的标签),共同构成损失函数
l o s s = ∑ i ∣ ∣ o i − t i ∣ ∣ loss = \sum_i{||\boldsymbol{o}_i - \boldsymbol{t}_i}|| loss=i∑∣∣oi−ti∣∣
最后,从整体的结构来看,在进行不动点迭代的时候,神经网络的参数不能变。因此GNN的训练过程应该是多个过程 r r r 的序列,在每一个过程 r r r 中分成两步:第一步不动点迭代算法,直到 h v \boldsymbol{h}_v hv收敛;第二步,监督学习反向传播,更新神经网络参数。
欢迎关注公众号BBIT
让我们共同学习共同进步!