《SEMI-SUPERVISED CLASSIFICATION WITH GRAPH CONVOLUTIONAL NETWORKS》論文閱讀(二)


GCN的定義

下面內容參考kipf博客,個人認為是告訴你從直覺上,我們怎么得到GCN圖上的定義而前面的大幅推導是從理論上一步一步來的,也就是說可以用來佐證我們的直覺

我們的網絡輸入\(\mathcal{G}=(\mathcal{V},\mathcal{E})\)

  • 即可以用\(N\times D\)的矩陣\(X\)表示,\(N\)為圖上結點個數,\(D\)是每個結點的特征維數
  • 同時表示一個圖還需要鄰接矩陣\(A\)

而一層的輸出記作\(Z_{\mathbb{R}^{N \times F}}\),其中\(N\)還是結點個數,\(F\)為每個結點的特征維數

那么非線性神經網絡就可以定義成如下形式:

\[H^{l+1}=f(H^{l}, A) \]

其中\(H^{0}=X\), \(H^{L}=Z\), \(L\) 表示網絡的層數,那么模型的關鍵是如何設計\(f(\cdot )\)

一種簡單的形式

\[f(H^{l}, A) = \sigma (AH^{l}W^{l}) \]

其中\(W^{l}\)\(l.th\)層的參數矩陣,\(\sigma ()\) 是激活函數。【PS:Despite its simplicity this model is already quite powerful】
仔細觀察上式就能發現幾個缺陷:

  • 其中\(A\)是鄰接矩陣,對角線上為\(0\),導致經過網絡中的一層,沒有加上自己本結點的信息,所以改造替換成 \(\hat{A} = A+I\)
  • 可是\(\hat {A}H\) 則是自己+相鄰結點特征總和,還需平均化,所以改成 \(D^{-1}\hat {A}H\)
  • 我們還可以更進一步,考慮到上篇說的拉普拉斯算子計算中周圍結點總和\(-\)中心點*相鄰結點個數,即相當於每個相鄰點能量\(-\)中心點能量。類比過來,相鄰點給我影響是:(相鄰點能量/相鄰點本身鄰居個數),所以有\(D^{-1/2}\hat{A}D^{-1/2}\)

\[f(H^{(l+1)}, A) = \sigma\left( D^{-\frac{1}{2}}\hat{A}D^{-\frac{1}{2}}H^{(l)}W^{(l)}\right) \, \]

這個形式已經和上篇利用譜理論推導處理的結果很相近了

\[\boldsymbol{g}_{\boldsymbol{\theta^{\prime}}} * \boldsymbol{x} = \theta(\boldsymbol{I_n} + \boldsymbol{D}^{-1/2} \boldsymbol{W} \boldsymbol{D}^{-1/2}) \boldsymbol{x} \]

但和最終的結果還不一樣,回顧論文給的 renormalization trick:

\[I_{N}+D^{-1/2}AD^{-1/2}=D^{-1/2}\hat{A}D^{-1/2}\rightarrow \tilde{D}^{-1/2}\hat{A}\tilde{D}^{-1/2}=(D+I_{N})^{-1/2}\hat{A}(D+I_{N})^{-1/2} \]

\[\hat{A}=A+I_{N} \]

\[\tilde{D}_{ii}= \sum_{}^{j}\hat{A}_{ij}=D+I_{N} \]

那么的確可以得到最終形式:

\[f(H^{(l+1)}, A) = \sigma\left(\tilde{D}^{-\frac{1}{2}}\hat{A}\tilde{D}^{-\frac{1}{2}}H^{(l)}W^{(l)}\right) \, \]

WEISFEILER-LEHMAN算法

作者試圖使用Weifeiler-Lehman算法來解釋GCN的表征能力。\(WL\)算法是用來判斷兩個graph是否同構(簡單說是兩圖拓撲結構相同)的,WL算法

算法包含兩個關鍵點

  • 聚合自己和鄰接結點信息,若記\(h\_aggregate^{t}_{i}\)\(t\)是第幾次迭代,\(i\) 是第幾個結點
  • 利用hash函數吐出唯一的值,\(h^{t+1}_{i}=hash(h\_aggregate^{t}_{i})\) 代替\(i\) 結點的特征

然后循環迭代

下面一個簡單小例子(默認各個結點信息是相同的,所以序號均標為1,主要判斷拓撲結構是否同構):

進行信息聚合,因為默認結點是一致的,所以主要通過鄰接關系判斷是否同構\(\{1,1,1\}\)即是該結點的相鄰結點是誰

輸入hash()函數,並且代替原有結點信息

重復上述操作(具體參見:https://www.davidbieber.com/post/2019-05-10-weisfeiler-lehman-isomorphism-test/#)
最后結果:

通過判斷 \(9,8,7\) 兩個圖個數相同,判斷是同構

WEISFEILER-LEHMAN算法反映什么

\(WL\)算法由於\(hash()\)函數的使用,使得通過不斷迭代,能夠表征不能不同結點的差異——即結點自身信息+結點的鄰居所帶來的差異

代替WL的hash()函數

\[h^{l+1}_{i}=\sigma (\sum_{j\in \mathcal{N_{i}}}^{}\frac{1}{c_{ij}}h_{j}^{l}W^{l}) \]

\(N_{i}\)是結點\(j\)的相鄰結點,上式可進一步化成矩陣形式

\[h^{l+1}_{i}=\sigma (D^{-1/2}AD^{-1/2}h_{j}^{l}W^{l}) \]

通過調整\(W^{l}\)參數,實現\(hash()\)的功能。也就是說這種形式的GCN對結點的表征能力可以達到hash()函數級別,作者借此來證明GCN的能力。
可是有個疑問吧,這里用的是\(D^{-1/2}AD^{-1/2}\),其實和上文提到的\(\tilde{D}^{-1/2}\hat{A}\tilde{D}^{-1/2}\),也還是有區別的,前者不是GCN的最終形式。。。。

Zachary karate club舉例

任務內容是醬紫的,一共有0~33位俱樂部成員,由於0號和33號兩位之間發生了沖突,導致其他成員進行圍繞這兩位進行了“拉幫結派”,不同成員之間有一定交流(用連線表示),所以任務具體來說就是需要我們對這些成員進行分類——歸屬於哪個小團體。

我們先看一下label的真實分類情況:

import matplotlib.pyplot as plt
import networkx as nx
from networkx import karate_club_graph, to_numpy_matrix
import matplotlib.pyplot as plt

def lable_graph(G):

    fig,ax = plt.subplots()
    pos = nx.kamada_kawai_layout(G) # 指定圖的美化排列方式

    cluter1 = []
    cluter2 = []
    for i in range(G.number_of_nodes()):
        if zkc.nodes[i]['club'] == 'Mr. Hi':
            cluter1.append(i)
        else:
            cluter2.append(i)
    nx.draw_networkx_nodes(G, pos, nodelist=cluter1,  node_color='orange')
    nx.draw_networkx_nodes(G, pos, nodelist=cluter2,  node_color='red')
    nx.draw_networkx_labels(G, pos, labels={i:str(i) for i in range(G.number_of_nodes())}, font_size=16)
    nx.draw_networkx_edges(G, pos, edgelist=G.edges())

zkc = karate_club_graph()
lable_graph(zkc)
plt.show()

假設暫時采用的形式進行研究:

\[D^{-1/2}\hat{A}D^{-1/2} \]

import numpy as np
import networkx as nx
import matplotlib.pyplot as plt
from networkx import karate_club_graph, to_numpy_matrix
# np.set_printoptions(threshold=np.inf)

zkc = karate_club_graph()
order = sorted(list(zkc.nodes()))
A = to_numpy_matrix(zkc, nodelist=order) # type=np.matrix

I = np.eye(A.shape[0])
A_hat = A + I

D_hat = np.diag(np.array(np.sum(A_hat, 0)).reshape(-1,))
# print(D_hat, D_hat.shape)
W1 = np.random.normal(loc=0, scale=1, size=(zkc.number_of_nodes(), 4))
W2 = np.random.normal(loc=0, scale=1, size=(W1.shape[1], 2))

def relu(x):
    return  1 / (1 + np.exp(-x))
    # return np.maximum(x, 0)

def gcn_layer(D_hat, A_hat, X, W):
    D_hat_1 = np.linalg.inv(D_hat)
    result = np.dot(D_hat_1, A).dot(X).dot(W)
    return relu(result)

H1 = gcn_layer(D_hat, A_hat, I, W1)
H2 = gcn_layer(D_hat, A_hat, H1, W2)
output = H2 # 34*2
# print(output, output.shape)

pos_weight = {
    node: (np.array(output)[node][0], np.array(output)[node][1])
    for node in zkc.nodes()}

def plot_graph_feature(G, pos_weight):
    fig, ax = plt.subplots()

    clsuter1 = []
    clsuter2 = []
    for i in range(G.number_of_nodes()):
        if G.nodes[i]['club'] == 'Mr. Hi':
            clsuter1.append(i)
        else:
            clsuter2.append(i)
    nx.draw_networkx_nodes(G, pos_weight, nodelist=clsuter1, node_color='orange')
    nx.draw_networkx_nodes(G, pos_weight, nodelist=clsuter2, node_color='red')
    nx.draw_networkx_labels(G, pos_weight, labels={i: str(i) for i in range(G.number_of_nodes())}, font_size=16)
    nx.draw_networkx_edges(G, pos_weight, edgelist=G.edges())


    ax.set_title('epoch')
    # x1_min = x1_max = pos_weight[0][0]
    # x2_min = x2_max = pos_weight[0][1]
    # for index,pos in pos_weight.items():
    #     x1_min = np.minimum(x1_min, pos[0])
    #     x1_max = np.maximum(x1_max, pos[0])
    #     x2_min = np.minimum(x2_min, pos[1])
    #     x2_max = np.maximum(x2_max, pos[1])
    # ax.set_xlim(x1_min, x1_max)
    # ax.set_ylim(x2_min, x2_max)

plot_graph_feature(zkc, pos_weight)
plt.show()

如上,采用隨機初始化參數,配套使用兩層GCN,根據\(WL\)理論,的確可以得到比較良好的分類結果(當然下圖也是隨機得到的比較理想的情況),但是我們都還沒開始反向傳播呢,效果有點閃瞎狗眼

使用DGL框架實現

實現如下,總的來說,只使用了兩個結點的label,最后的效果還是挺吃驚的,大有文章

import dgl
import numpy as np
import networkx as nx
import torch
import torch.nn as nn
import torch.nn.functional as F
from dgl.nn.pytorch import GraphConv
import itertools
import matplotlib.pyplot as plt


def build_karate_club_graph():
    src = np.array([1, 2, 2, 3, 3, 3, 4, 5, 6, 6, 6, 7, 7, 7, 7, 8, 8, 9, 10, 10,
                    10, 11, 12, 12, 13, 13, 13, 13, 16, 16, 17, 17, 19, 19, 21, 21,
                    25, 25, 27, 27, 27, 28, 29, 29, 30, 30, 31, 31, 31, 31, 32, 32,
                    32, 32, 32, 32, 32, 32, 32, 32, 32, 33, 33, 33, 33, 33, 33, 33,
                    33, 33, 33, 33, 33, 33, 33, 33, 33, 33])
    dst = np.array([0, 0, 1, 0, 1, 2, 0, 0, 0, 4, 5, 0, 1, 2, 3, 0, 2, 2, 0, 4,
                    5, 0, 0, 3, 0, 1, 2, 3, 5, 6, 0, 1, 0, 1, 0, 1, 23, 24, 2, 23,
                    24, 2, 23, 26, 1, 8, 0, 24, 25, 28, 2, 8, 14, 15, 18, 20, 22, 23,
                    29, 30, 31, 8, 9, 13, 14, 15, 18, 19, 20, 22, 23, 26, 27, 28, 29, 30,
                    31, 32])
    # Edges are directional in DGL; Make them bi-directional.
    u = np.concatenate([src, dst])
    v = np.concatenate([dst, src])
    # Construct a DGLGraph
    return dgl.DGLGraph((u, v))


def lable_gt_graph(G):
    fig, ax = plt.subplots()
    pos = nx.kamada_kawai_layout(G)  # 指定圖的美化排列方式

    cluter1 = []
    cluter2 = []
    for i in range(G.number_of_nodes()):
        if G.nodes[i]['club'] == 'Mr. Hi':
            cluter1.append(i)
        else:
            cluter2.append(i)
    nx.draw_networkx_nodes(G, pos, nodelist=cluter1, node_color='orange')
    nx.draw_networkx_nodes(G, pos, nodelist=cluter2, node_color='red')
    nx.draw_networkx_labels(G, pos, labels={i: str(i) for i in range(G.number_of_nodes())}, font_size=16)
    nx.draw_networkx_edges(G, pos, edgelist=G.edges())


G = build_karate_club_graph()
# print('We have %d nodes.' % G.number_of_nodes())
# print('We have %d edges.' % G.number_of_edges())

nx_G = G.to_networkx().to_undirected()
# pos = nx.kamada_kawai_layout(nx_G)
# nx.draw(nx_G, pos, with_labels=True) # 未分類的
# plt.show()

embed = nn.Embedding(34, 5)  # 隨機初始化
G.ndata['feat'] = embed.weight
# print(G.ndata['feat'][2])

class GCN(nn.Module):
    def __init__(self, in_feat, hidden_size, num_classes):
        super(GCN, self).__init__()
        self.conv1 = GraphConv(in_feat, hidden_size)
        self.conv2 = GraphConv(hidden_size, num_classes)

    def forward(self, g, inputs):
        h = self.conv1(g, inputs)
        h = torch.relu(h)
        h = self.conv2(g, h)
        return h


net = GCN(5, 5, 2)

inputs = embed.weight
labeled_nodes = torch.tensor([0, 33])
labels = torch.tensor([0, 1])

optimizer = torch.optim.Adam(itertools.chain(net.parameters(), embed.parameters()), lr=0.01)
all_logits = []

for epoch in range(50):
    logits = net(G, inputs)

    all_logits.append(logits.detach())

    logp = F.log_softmax(logits, 1)  # dimension

    loss = F.nll_loss(logp[labeled_nodes], labels)

    optimizer.zero_grad()
    loss.backward()
    optimizer.step()
   # print('Epoch %d | Loss: %.4f' % (epoch, loss.item()))

fig, ax = plt.subplots()
def draw(i):
    cls1color = 'orange'
    cls2color = 'red'
    pos = {}
    colors = []
    for v in range(34):
        pos[v] = all_logits[i][v].numpy()
        cls = pos[v].argmax()
        colors.append(cls1color if cls else cls2color)
    ax.set_title('Epoch: %d' % i)
    nx.draw_networkx(nx_G.to_undirected(), pos, node_color=colors,
                   node_size=300, with_labels=True)

    plt.show()

draw(5)
draw(49)


參考

https://www.zhihu.com/question/54504471

http://tkipf.github.io/graph-convolutional-networks/

https://www.davidbieber.com/post/2019-05-10-weisfeiler-lehman-isomorphism-test/#

https://towardsdatascience.com/how-to-do-deep-learning-on-graphs-with-graph-convolutional-networks-7d2250723780

https://arxiv.org/abs/1609.02907

https://docs.dgl.ai/en/0.4.x/index.html


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM