深入调试diffpool

1. 整体思路

  1. 利用原来的GCN,重写diffpool
  2. 测试效果(收敛状态下70左右)

2. 现有diffpool的参数调整

因为diffpool的临界简化模型是GCN,所以通过调整参数设置达到基础GCN的效果。下面是需要调整的参数:

  • max_num_nodes: 针对每个图计算。
  • embedding_dim: 选择恰当的值。
  • num_layers: 寻找恰当的值。
  • assign_hidden_dim: 寻找恰当的值。
  • assign_ratio: 需要优化。
  • assign_num_layers: 需要优化。
  • num_pooling:需要优化。
  • pred_hidden_dims: 需要优化。
  • concat: 改成False。
  • bn: 改成False。
  • dropout: 即dropout。保留。
  • linkpred: 只在loss里出现,忽略。
  • assign_input_dim: 就是input_dim,忽略。

3. 现有diffpool的函数调整

1. 目的

随意调整参数设置而网络结构自动变化。(之前functional_forward写得不好)

2. pytorch存储各层权重参数的命名规则
image-20200710182038074
image-20200710182038074
1. 对于__init__中使用self定义的变量会使用这个变量名作为存储时的名字.

卷积层有两个参数:权重和偏移项,对应名称为conv1.weight, conv1.bias

1
self.conv1 = nn.Conv2d(3,12,kernel_size=3,stride=1,padding=1)

BN层有5个参数: bn1.weight, bn1.bias, bn1.running_mean, bn1.running_var, bn1.num_batches_tracked.

1
self.bn1 = nn.BatchNorm2d(12)
2. 当使用nn.Sequential时会根据传入的list的顺序对其进行编号,从0开始。
1
2
3
4
conv1 = nn.Conv2d(3, 12, kernel_size=3, stride=1, padding=1)
bn1 = nn.BatchNorm2d(12)
s1 = [conv1, bn1]
self.stage1 = nn.Sequential(*s1)

注意此时的conv1和bn1都没有self,stage1有self,由于Sequential将conv1和bn1进行顺序封装,因此conv1会被编号为stage1.0,bn1会被编号为stage1.1,具体结果如下:

1
2
stage1.0.weight、stage1.0.bias
stage1.1.weight、stage1.1.bias、stage1.1.running_mean、stage1.1.running_var、stage1.1.num_batches_tracked
3. 当一个module被from torch.nn import DataParallel或者from torch.nn.paralledl import DistributedDataParalledl包围住后,会在这个变量名后加上module
1
2
3
4
5
conv1 = nn.Conv2d(3, 12, kernel_size=3, stride=1, padding=1)
bn1 = nn.BatchNorm2d(12)
s1 = [conv1, bn1]
stage1 = nn.Sequential(*s1)
self.stage2 = DataParallel(stage1)

注意只有stage2前面有self,输出结果如下:

1
2
stage2.module.0.weight, stage2.module.0.bias
stage2.module.1.weight, stage2.module.1.bias, stage2.module.1.running_mean, stage2.module.1.running_var, stage2.module.1.num_batches_tracked
3. 操作目标

直接针对diffpool进行修改,将模块里面的标号改成直接生成的。

  • 把所有的weights传入;
  • 然后在最底层根据需要进行挑选;
  • 知道weights的命名规则。
4. 对标号的修改

1 没问题,只在最开始出现。

4. 重写diffpool准备

1. 查看原本diffpool需要什么东西
1
2
3
4
self, max_num_nodes, input_dim, hidden_dim, embedding_dim, label_dim, num_layers,
assign_hidden_dim, assign_ratio=0.25, assign_num_layers=-1, num_pooling=1,
pred_hidden_dims=[50], concat=True, bn=True, dropout=0.0, linkpred=True,
assign_input_dim=-1, args=None
2. GCN类里带的参数
1
self, nfeat, nhid, nclass, dropout
3. 分析有无必要保留
  • max_num_nodes: GCN中没有,用于最开始计算assign_dim,保留。
  • input_dim: 即nfeat,保留。
  • hidden_dim: 即nhid, 保留
  • embedding_dim: diffpool特有参数,参与$Z$的运算,参见下图,应该保留。
    image-20200725183312050
    image-20200725183312050
  • label_dim: 就是类别数,对应nclass,保留。
    image-20200725183419107
    image-20200725183419107
  • num_layers: 就是两个diffpool之间夹的图卷积的层数,保留。暂时丢弃。
  • assign_hidden_dim: 就是$S$的hidden_dim,保留。暂时丢弃。
  • assign_ratio: 用最大节点数*assign_ratio得到assign_dim,保留。
  • assign_num_layers: 就是assign矩阵的层数,保留。暂时丢弃。
  • num_pooling:pooling的次数,保留。暂时丢弃。
  • pred_hidden_dims:中间层的维度,保留。暂时丢弃。
  • concat: True的话就是每一个embedding的out都会拼在一起预测结果,False就只有最后一层。丢弃。
  • bn: True,batch_normalization。丢弃。
  • dropout: 即dropout。保留。
  • linkpred: 只在loss里出现,忽略。丢弃。
  • assign_input_dim: 就是assign矩阵的inpud_dim。这里就是input_dim,丢弃。
4. pred_hidden_dims存在的价值是什么?为什么assign矩阵要pred一次?
1
self.assign_tensor = nn.Softmax(dim=-1)(self.assign_pred_modules[i](self.assign_tensor, 40+i, weights))

这里assign_dim其实就是d,然后assign_pred才是$n_{l+1}$。

5. 其实GCN并不是调用的forward,而是调用的GCN_forward,分析其作用。
image-20200725195431728
image-20200725195431728

不同点:

  1. 正常forward多一个mask。(忽略)
  2. 正常forward多了对x的max操作等。(忽略)
  3. 最后没有全连接层了。

5. 重写pool部分代码

1. process_sparse的作用和意义

还要看gnn_lib.py中的PrepareSparseMatrices函数。

2. 了解预测层的作用

其实x应该是3维的,但是这里的函数把它变成了2维,第2维是属性的长度,第1维应该是batch数和节点数的结合。是直接把10个图放到了一起。

3. 抉择

现在有两种方式,一种是大矩阵,一种是小矩阵。

大矩阵的话gcn_forward那里直接把x输出就可以了,30632维的矩阵,然后pool阶段和adj直接相乘。然后得到的x维度降低了,adj维度也降低了,但是后面gcn需要将graph_list和x输入,x有了,*graph_list没有

小矩阵的话gcn_forward那里输出数组形式的x,3个维度,然后用for循环对每个矩阵相乘(貌似可以改造成直接相乘),然后pool阶段和adj相乘,adj是大的,要变成小的。然后把小矩阵的x和adj算出graph_list,组合成x,然后继续算。(不能这么做,因为换成小矩阵阻断了反向传播。)

6. 写GraphPooling

输入:

输入应该是邻接矩阵、属性矩阵

输出:

更小的邻接矩阵,更小的属性矩阵。

论文

论文里的做法是我将输入的两个矩阵训练一个嵌入GNN,生成Z,再用输入的两个矩阵训练一个分配GNN,生成S,然后矩阵相乘得到输出。

代码

代码里的做法和论文中是一样的,里面都是gcn_forward.

考虑实现
实际输入:

给了graph_list, node_feat,然后可以生成出

  • graph_sizes: 是一个list,长度为图的数目,各个元素为图的节点数;
  • n2n_sp: 是一个稀疏的tensor,是十个图邻接矩阵合并起来形成的大邻接矩阵。
  • e2n_sp: 后续没有用到,一个维度是节点数,另一个维度应该是边数。
  • subg_sp: 稀疏tensor,后续没有用到,图数*节点数。
  • node_degs: tensor,每个节点的度数。
总体实现:

首先调用gcn_forward算出embedding矩阵和assignment矩阵,然后embedding矩阵和X计算得到下一层的X,然后assignment矩阵和呢n_sp计算得到下一层的A。然后这个过程都要在比较低的程度写,不能把graphlist泄露出来。

具体实现:

layers.py里面编写Pooling类,然后在里面不涉及利用gcn_forward,这个类输入Z和S大矩阵,计算并输出A和X。

7. 遇到的问题

大邻接矩阵的pool可能会导致某一个图的节点全消失:

每次传入一个图,在pool和maml中间再加一层。

8. 反向传播被阻断的机制?

应该是中间转换成了numpy的原因。

参考资料

  1. pytorch中存储各层权重参数时的命名规则,为什么有些层的名字中带module.