前言
记录一下实现的细节。
benchmarks
原则
convlayer内部不加norm 和 activation。
特例
GIN的MLP部分多层时,中间层加BN和ReLU。
必须夸一下DGL的复现员还是相当尊重原始实现的。
1.gin
How Powerful are Graph Neural Networks? ICLR2019
https://arxiv.org/pdf/1810.00826.pdf
https://github.com/weihua916/powerful-gnns
这个东西的关键在于,区分 gin_conv_layer和 gin_readout _layer。
gin conv是一个非常类似gcn的东西。
可以把support写成C = (1+eps)I +A 的形式。(neighborpool =sum)
或者C= (1+eps)I + D^-1A 。(neighbor pool =mean)
区别在于它先做 agg 再做mlp。
显然当MLP只有一层时,就退化到了CHW的形态。
gin readout是一个非常类似jknet的东西。
sum时类似JK。
也可以选 mean。
GIN原文根据不同的dataset选择sum or mean。
sum readout on bioinformatics datasets and mean readout on social datasets due to better test performance
默认版本是
leaneps=True, neigh_pool=sum, graph_pool = sum。
DGL实现版本
https://github.com/dmlc/dgl/blob/master/examples/pytorch/gin/gin.py
OGB实现版本( base on PyG )
https://github.com/snap-stanford/ogb/tree/master/examples/graphproppred/mol
1.1 区别1 share linear predictor weight
注意OGB实现版本默认的JK='last’是只取convlayers[-1]的输出作为node embd。
而哪怕JK=‘sum’,OGB版本也跟DGL版本实现有些区别。
见下列伪代码
# OGB version
if JK=='last':
h_nodes = h_list[-1]
if JK=='sum':
h_nodes = sum([ h_list[i] for i in range(n_layers)])
graph_embd = graph_pool(h_nodes)
pred = fc(graph_embd)
作为对比,DGL-GIN的实现是
# DGL version
pred = 0
for i in range(n_layers):
g_embd = graph_pool(h_list[i])
pred = pred + self.linears[i](g_embd)
两者的差别,写成矩阵形式。
设Graph Pooling对应一个矩阵P。
P
r
e
d
D
G
L
−
G
I
N
=
∑
i
P
H
i
W
i
Pred_{DGL-GIN} = \sum_{i} P H_i W_i
PredDGL−GIN=∑iPHiWi
P
r
e
d
O
G
B
−
G
I
N
=
P
(
∑
i
H
i
)
W
,
if JK ==’sum’
Pred_{OGB-GIN} = P (\sum_{i} H_i) W , \text{if JK =='sum'}
PredOGB−GIN=P(∑iHi)W,if JK ==’sum’
将P提到最左边后对比,可以看到OGB版本share一个W,参数量更节约。
1.2 区别2 残差和末层激活
其他细节上。
#OGB version
h_list=[h]
for i in range(n_layers):
h = BN(Conv(..))
# 对h的最后一层不加relu
if not i==n_layers-1:
h = ReLU(h)
h=drop(h)
# 残差链接
if residual:
h += h_list[i]
h_list.append(h)
# DGL version
for i in range(n_layers):
h = BN(Conv(..))
h = ReLU(h)
#没有消除末层激活
#没有残差
对OGB版本来说,
因为h的最后一层不加relu,
如果JK又恰好为’last’。
相当于直接拿没有activation过的最后一层h去linear里做分类了。
这合理吗?
… 我觉得不合理。
QQ:结论,最后一层h还是要加激活。
再讨论残差连接合理吗?
我觉得也不合理。
用公式展开就知道了
假设下面的Conv操作符号已经融合了BN+ReLU
H
O
G
B
−
r
e
s
(
l
+
1
)
=
Conv
(
H
(
l
)
)
+
H
(
l
)
H_{OGB-res}^{(l+1)} = \text{Conv}(H^{(l)}) +H^{(l)}
HOGB−res(l+1)=Conv(H(l))+H(l)
在计算pred score的时候,可以将总分拆成几个子项的和。
P
r
e
d
O
G
B
−
G
I
N
=
P
(
∑
i
H
i
)
W
=
∑
i
(
P
H
i
W
)
Pred_{OGB-GIN} = P (\sum_{i} H_i) W = \sum_{i} (PH_i W)
PredOGB−GIN=P(∑iHi)W=∑i(PHiW)
设 pred ( i ) \text{pred}^{(i)} pred(i)表示第i项
pred
(
0
)
=
P
H
(
0
)
W
\text{pred}^{(0)} = P H^{(0)}W
pred(0)=PH(0)W
pred
(
1
)
=
P
(
Conv
(
H
(
0
)
)
+
H
(
0
)
)
W
\text{pred}^{(1)} = P (\text{Conv}(H^{(0)})+H^{(0)})W
pred(1)=P(Conv(H(0))+H(0))W
pred
(
2
)
=
P
(
Conv
(
H
(
1
)
)
+
H
(
1
)
)
W
=
P
(
Conv
(
H
(
1
)
)
W
+
H
(
1
)
W
)
=
P
(
Conv
(
H
(
1
)
)
W
+
Conv
(
H
(
0
)
)
W
+
H
(
0
)
W
)
\text{pred}^{(2)} = P (\text{Conv}(H^{(1)})+H^{(1)})W =P (\text{Conv}(H^{(1)})W+H^{(1)}W) = P (\text{Conv}(H^{(1)})W+\text{Conv}(H^{(0)})W+H^{(0)}W)
pred(2)=P(Conv(H(1))+H(1))W=P(Conv(H(1))W+H(1)W)=P(Conv(H(1))W+Conv(H(0))W+H(0)W)
最后要把这些pred全部加起来。
你发现了吗,在OGB版本里
if residual==True and JK=='sum'
H
(
0
)
W
H^{(0)}W
H(0)W 会被 add n_layers 次。
不同层的表征之间天然存在不平衡,相当于告诉模型重点在
H
(
0
)
H^{(0)}
H(0)上学。
直觉上这是非常不好的事情。
相反,DGL的版本就很好。
P
r
e
d
D
G
L
−
G
I
N
=
∑
i
P
H
i
W
i
Pred_{DGL-GIN} = \sum_{i} P H_i W_i
PredDGL−GIN=∑iPHiWi
每个层有自己的
W
i
W_i
Wi。
如果对模型来说,后面的深层表征真的不重要,而
H
(
0
)
H^{(0)}
H(0)重要,模型可以自适应地学到
W
i
→
0
,
i
≠
0
W_i \to 0 , i\ne0
Wi→0,i=0。
而不是像OGB版本一样,增加一个非常强的先验权重。
QQ: residual 和 JK sum 存在强相互作用。
if residual , 建议每层一个linear predictor,再JK。或者不要JK只取last做linear。
如果要节约参数,sum(h) 后再 linear predictor,建议不要residual。
这种trick少用…
懒人结论,应该保持跟原始repo一样的独立linear predictor,拒绝残差连接。
注意OGB版本(1.3.2)的默认参数是
JK=‘last’ ,residual=False。
所以直接按readme.md里的命令跑,不会出现上面说的问题。
总之,还是抄DGL版本为好。
1.3 区别3 Drop位置
OGB版本
h = Drop(ReLU(BN(Conv(h))))
pred = linear(graph_pool( sum(h) )) if JK==‘sum’
pred = linear(graph_pool( last_h )) if JK==‘last’ , default set
DGL版本
h= ReLU(BN(Conv(h)))
pred = sum( Drop(linear(graph_pool(h) ) ) )
这个应该不是很重要,因为我们主要只需要利用到GIN ConvLayer。
Conv之外的处理可以自定义的。
2.GCN
别人是怎么做的?
OGB官方实现的GCN Convlayer(base on PyG)
https://github.com/snap-stanford/ogb/blob/master/examples/graphproppred/mol/conv.py
QQ:
实现GC上的GCN的接口时遇到一个问题。
基于GIN写的第一个框架,model的input其实包含3个部分。
一个特别的adj (based on neighbor pooling type, raw_adj for sum or D^-1adj for mean) with no self-edge
但gcn的默认其实是 D-1adj/2 D-1/2 , with self-edge。
可以在config里写好需要的adj norm type , left right,both,none, 以及 selfedge。
但是这样就需要我自己去记model specific parameters。。
我需要找一个地方去记这些东西。
3.Deeper GCN (base PyG
https://github.com/lightaime/deep_gcns_torch/tree/master/examples/ogb
4.DGN (base DGL)
这个 repo给了非常多benchmark,非常好。
https://github.com/Saro00/DGN/tree/master/realworld_benchmark
实验设置非常细腻!
好评如潮!
Data
mol onehot embed
from ogb.graphproppred.mol_encoder import AtomEncoder,BondEncoder
from ogb.utils.features import get_atom_feature_dims, get_bond_feature_dims
full_atom_feature_dims = get_atom_feature_dims()
full_bond_feature_dims = get_bond_feature_dims()
#9项。[119, 4, 12, 12, 10, 6, 6, 2, 2]
#3项。[5, 6, 2]
Q要不要加JK
修改项
2处norm非L2时,应该分离。于是写出了 norm1,norm2。
调整了norm和relu的顺序,先norm后relu。
collate_fn
返回sparse_raw_adj, feat, label。
batch train
name = ‘ogbg-molhiv’
base_dir = ‘./Data/DatasetRaw/OGB/ogbg-molhiv’
train,val,test, 3.2w,4k,4k
bz=20
tr pred 0.0136
tr bp 0.0368
val pred+loss 0.008 *200 = 1.6
bz=100
tr pred+loss = 0.0044
tr bp=0.023
val pred+loss = 0.02*40 = 0.8s
bz=500
tr pred+loss =0.005
tr vp 0.029
val pred+loss 0.05*8 =0.5s
合并val和test之后,单次eval infer只需要0.01s
bz100
tr pred+loss 0.023 * 320 = 7s
bz20
tr pred_loss 0.004
tr bp 0.018
大概需要3.2w/20 = 1.6k iters
0.018*1600 = 28.8s
单词计算val rocauc开销0.03s