Deep Graph Library(DGL)
DGL是一個專門用於深度學習圖形的Python包, 一款面向圖神經網絡以及圖機器學習的全新框架, 簡化了基於圖形的神經網絡的實現。
在設計上,DGL 秉承三項原則:
DGL 必須和目前的主流的深度學習框架(PyTorch、MXNet、TensorFlow 等)無縫銜接。從而實現從傳統的 tensor 運算到圖運算的自由轉換。
DGL 應該提供最少的 API 以降低用戶的學習門檻。
在保證以上兩點的基礎之上,DGL 能高效並透明地並行圖上的計算,以及能很方便地擴展到巨圖上。
設計一:DGL 是一個「框架上的框架」
設計二:基於「消息傳遞」(message passing)編程模型
消息傳遞是圖計算的經典編程模型。原因在於圖上的計算往往可以表示成兩步:
發送節點根據自身的特征(feature)計算需要向外分發的消息。
接受節點對收到的消息進行累和並更新自己的特征。
比如常見的卷積圖神經網絡 GCN(Graph Convolutional Network)可以用下圖中的消息傳遞模式進行表示(圖中 h 表示節點各自的特征)。用戶可以定制化消息函數(message function),以及節點的累和更新函數(reduce function)來構造新的模型。事實上,在 Google 的 2017 年的論文中 [Gilmer et al. 2017] 將很多圖神經網絡都歸納到了這一體系內。
DGL 的編程模型正基於此。
設計三:DGL 的自動批處理(auto-batching)
好的系統設計應該是簡單透明的。在提供用戶最大的自由度的同時,將系統優化最大程度地隱藏起來。圖計算的主要難點在於並行。我們根據模型特點將問題划分為三類。
首先是處理單一靜態圖的模型(比如 citation graph),其重點是如何並行計算多個節點或者多條邊。DGL 通過分析圖結構能夠高效地將可以並行的節點分組,然后調用用戶自定義函數進行批處理。相比於現有的解決方案(比如 Dynet 和 TensorflowFold),DGL 能大大降低自動批處理的開銷從而大幅提高性能。
第二類是處理許多圖的模型(比如 module graph),其重點是如何並行不同圖間的計算。DGL 的解決方案是將多張圖合並為一張大圖的多個連通分量,從而將該類模型轉化為了第一類。
第三類是巨圖模型(比如 knowledge graph),對此,DGL 提供了高效的圖采樣接口,將巨圖變為小圖樣本,從而轉化為第一類問題。
目前 DGL 提供了 10 個示例模型,涵蓋了以上三種類別。其中除了 TreeLSTM,其余都是 2017 年以后新鮮出爐的圖神經網絡,其中包括幾個邏輯上相當復雜的生成模型(DGMG、JTNN)我們也嘗試用圖計算的方式重寫傳統模型比如 Capsue 和 Universal Transformer,讓模型簡單易懂,幫助進一步擴展思路。
可以看到,DGL 能在這些模型上都取得了相當好的效果。我們也對 DGL 在巨圖上的性能進行了測試。在使用 MXNet/Gluon 作為后端時,DGL 能支持在千萬級規模的圖上進行神經網絡訓練。
DGL 現已開源。
主頁地址:http://dgl.ai
項目地址:https://github.com/jermainewang/dgl
初學者教程:https://docs.dgl.ai/tutorials/basics/index.html
所有示例模型的詳細從零教程:https://docs.dgl.ai/tutorials/models/index.html
本教程目標:
了解DGL如何從高級別開始在圖形上進行計算。
在DGL中訓練一個簡單的圖形神經網絡來對圖形中的節點進行分類。
依賴庫
- DGL 0.2
- PyTorch 1.1.0
- networkX 2.3+
Step 0: 問題描述(Zachary’s karate club)
我們從“Zachary空手道俱樂部”問題開始。 空手道俱樂部是一個社交網絡,它捕獲34名成員並記錄在俱樂部外互動的成員之間的成對鏈接。 俱樂部分為由教練(節點0)和俱樂部主席(節點33)領導的兩個社區。 網絡可視化如下,顏色表示社區):
任務是根據社交網絡本身預測每個成員傾向於加入哪一方(0或33)。
Step 1:利用DGL構建圖
1 # -*- coding: utf-8 -*- 2 """ 3 @Date: 2019/5/27 4 @Author: ranran 5 @Summary: DGL graph 6 """ 7 8 import dgl 9 import torch 10 import networkx as nx 11 import matplotlib.pyplot as plt 12 13 14 'import dgl 創建 Zachary’s karate club 圖 :' 15 def build_karate_club_graph(): 16 g = dgl.DGLGraph() 17 # add 34 nodes into the graph; nodes are labeled from 0~33 18 g.add_nodes(34) 19 # all 78 edges as a list of tuples所有78條邊作為元組列表 元組屬於不可變序列, 元組的訪問和處理速度比列表快 20 edge_list = [(1, 0), (2, 0), (2, 1), (3, 0), (3, 1), (3, 2), 21 (4, 0), (5, 0), (6, 0), (6, 4), (6, 5), (7, 0), (7, 1), 22 (7, 2), (7, 3), (8, 0), (8, 2), (9, 2), (10, 0), (10, 4), 23 (10, 5), (11, 0), (12, 0), (12, 3), (13, 0), (13, 1), (13, 2), 24 (13, 3), (16, 5), (16, 6), (17, 0), (17, 1), (19, 0), (19, 1), 25 (21, 0), (21, 1), (25, 23), (25, 24), (27, 2), (27, 23), 26 (27, 24), (28, 2), (29, 23), (29, 26), (30, 1), (30, 8), 27 (31, 0), (31, 24), (31, 25), (31, 28), (32, 2), (32, 8), 28 (32, 14), (32, 15), (32, 18), (32, 20), (32, 22), (32, 23), 29 (32, 29), (32, 30), (32, 31), (33, 8), (33, 9), (33, 13), 30 (33, 14), (33, 15), (33, 18), (33, 19), (33, 20), (33, 22), 31 (33, 23), (33, 26), (33, 27), (33, 28), (33, 29), (33, 30), 32 (33, 31), (33, 32)] 33 34 # add edges two lists of nodes: src and dst添加兩個節點列表:src和dst 35 src, dst = tuple(zip(*edge_list)) #zip(*)解壓成列表形式,再構成兩個元組 36 print(src) 37 #(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) 38 print(dst) 39 #(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) 40 g.add_edges(src, dst) 41 # edges are directional in DGL; make them bi-directional 邊在DGL中是方向性的; 使它們雙向 42 g.add_edges(dst, src) 43 return g 44 45 46 if __name__ == '__main__': 47 48 '打印出剛剛創建的圖的節點數 nodes 和邊的個數 edges ' 49 G = build_karate_club_graph() 50 print('We have %d nodes.' % G.number_of_nodes()) 51 print('We have %d edges.' % G.number_of_edges()) 52 #We have 34 nodes. 53 #We have 156 edges. 54 55 fig, ax = plt.subplots() 56 fig.set_tight_layout(False) 57 58 'import networkx as nx import matplotlib.pyplot as plt 我們還可以通過將圖轉換為networkx圖來可視化它:' 59 #Since the actual graph is undirected, we convert it for visualization purpose. 60 nx_G = G.to_networkx().to_undirected() 61 # Position nodes using Kamada-Kawai path-length cost-function. Kamada-Kawaii layout usually looks pretty for arbitrary graphs 62 pos = nx.kamada_kawai_layout(nx_G) 63 #Draw the graph nx_G with Matplotlib. 64 nx.draw(nx_G, pos, with_labels=True, node_color=[[0.7, 0.7, 0.7]]) #node_color 節點顏色的深淺程度 65 plt.show() 66 67 68 69 'import torch 為節點或邊分配特征 assign features to nodes or edges' 70 #圖形神經網絡將特征與節點和邊緣相關聯以進行訓練。 對於我們的分類示例子,我們將每個節點的輸入特征指定為一個one-hot 向量: 71 # 節點vi的特征向量為[0,...,1,...,0],其中第i個位置是1。 72 #在DGL中,我們可以使用沿第一維批量節點特征的特征張量一次為所有節點添加特征。 下面的代碼為所有節點添加 one—hot特征: 73 G.ndata['feat'] = torch.eye(34) 74 75 #可以打印出一些節點的特征確定下: 76 print(G.nodes[2].data['feat']) 77 print(G.nodes[1, 3].data['feat']) 78 #tensor([[0., 0., 1., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 79 # 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.]]) 80 #tensor([[0., 1., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 81 # 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.], 82 # [0., 0., 0., 1., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 83 # 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.]])
構建的圖可視化:
Step 2: 定義 Graph Convolutional Network (GCN)
為了執行節點分類,我們使用由Kipf和Welling開發的圖形卷積網絡(GCN)。 在這里,我們提供了GCN框架的最簡單定義,但我們建議讀者閱讀原始文章以獲取更多詳細信息。
在 l 層,每個節點vli 帶有特征向量 hli
GCN的每一層都試圖聚合來自uil的特征(ui是v的鄰接節點)生成下一層ViL+ 1的特征。 接下來是具有一些非線性的仿射變換。an affine transformation with some non-linearity
GCN的上述定義適合於消息傳遞范例:每個節點將使用從相鄰節點發送的信息更新其自己的特征。 下面顯示了圖形演示。
# -*- coding: utf-8 -*- """ @Date: 2019/5/27 @Author: ranran @Summary: define a Graph Convolutional Network (GCN) """ import torch import torch.nn as nn '定義 message 和 reduce 函數' #注意:本教程忽略GCN標准化常量c_ij。 def gcn_message(edges): #參數是一批邊。它使用源節點的特征'h'計算一個名為'msg'的(批處理)消息。 """ compute a batch of message called 'msg' using the source nodes' feature 'h' :param edges: :return: """ return {'msg': edges.src['h']} def gcn_reduce(nodes): #參數是一批節點。這通過將每個節點收到的'msg'相加來計算新的'h'特征。 """ compute the new 'h' features by summing received 'msg' in each node's mailbox. :param nodes: :return: """ '??????' return {'h': torch.sum(nodes.mailbox['msg'], dim=1)} '定義GCNLayer Module (一層GCN) ' class GCNLayer(nn.Module): def __init__(self, in_feats, out_feats): super(GCNLayer, self).__init__() self.linear = nn.Linear(in_feats, out_feats) def forward(self, g, inputs): # g—graph #inputs—input node features # first set the node features首先設置節點特征 g.ndata['h'] = inputs # trigger message passing on all edges 在所有邊上觸發消息傳送 g.send(g.edges(), gcn_message) # trigger aggregation at all nodes 在所有節點上觸發聚合 g.recv(g.nodes(), gcn_reduce) # get the result node features 獲取結果節點特征 h = g.ndata.pop('h') # perform linear transformation 執行線性變換 return self.linear(h) #通常,節點發送通過message函數計算的信息,並使用reduce函數聚合傳入信息。 #然后,我們定義了一個包含兩個GCN層的更深層次的GCN模型: '定義一個2層的GCN 網絡' class GCN(nn.Module): def __init__(self, in_feats, hidden_size, num_classes): super(GCN, self).__init__() self.gcn1 = GCNLayer(in_feats, hidden_size) #兩層 self.gcn2 = GCNLayer(hidden_size, num_classes) def forward(self, g, inputs): h = self.gcn1(g, inputs) h = torch.relu(h) # h = self.gcn2(g, h) return h if __name__ == '__main__': net = GCN(34, 5, 2) #輸入特征34維向量,隱藏層5, 輸出層2(分兩類)
Step 4: 數據准備及初始化 訓練 可視化
可視化模塊我還沒學
# -*- coding: utf-8 -*- """ @Date: 2019/5/27 @Author: ranran @Summary: train semi-supervised setting """ import torch import torch.nn.functional as F import networkx as nx import matplotlib.animation as animation import matplotlib.pyplot as plt from model import GCN from build_graph import build_karate_club_graph import warnings warnings.filterwarnings('ignore') net = GCN(34, 5, 2) print(net) G = build_karate_club_graph() '數據准備和初始化' #我們使用one—hot向量來初始化節點特征。 由於這是半監督設置,因此僅向教練(節點0)和俱樂部主席(節點33)分配標簽。 實施如下 inputs = torch.eye(34) labeled_nodes = torch.tensor([0, 33]) # only the instructor and the president nodes are labeled labels = torch.tensor([0, 1]) '訓練' #訓練循環與其他PyTorch模型完全相同。 # (1)創建一個優化器 # (2)將輸入數據喂給模型 # (3)計算損失 # (4)使用autograd來優化模型。 optimizer = torch.optim.Adam(net.parameters(), lr=0.01) all_logits = [] for epoch in range(30): #訓練20次 logits = net(G, inputs) #返回的h保存在logits中 #為了之后的可視化保存logits 'detach()?????' all_logits.append(logits.detach()) #返回的是個概率? # 非線性函數torch.nn.functional.log_softmax(input, dim=None, _stacklevel=3, dtype=None) logp = F.log_softmax(logits, 1) # 只計算標記節點的損失 # logp-labels # torch.nn.functional.nll_loss(input, target, weight=None, size_average=True) # size_average (bool, optional) – 默認情況下,是mini-batchloss的平均值,然而,如果size_average=False,則是mini-batchloss的總和。 loss = F.nll_loss(logp[labeled_nodes], labels) optimizer.zero_grad() loss.backward() optimizer.step() print('Epoch %d | Loss: %.4f' % (epoch, loss)) #去掉.items()也正常運行 ''' 這是一個小示例,它沒有驗證或測試集。 由於模型為每個節點生成大小為2的輸出特征,我們可以通過在2D空間中繪制輸出特征來進行可視化。 以下代碼將訓練過程從初始猜測(其中節點根本沒有正確分類)到結束(節點可線性分離)進行動畫處理。 ''' def draw(i): cls1color = '#00FFFF' cls2color = '#FF00FF' 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.cla() #ax.axis('off') ax.set_title('Epoch: %d' % i) nx.draw_networkx(nx_G.to_undirected(), pos, node_color=colors, with_labels=True, node_size=300, ax=ax) nx_G = G.to_networkx().to_undirected() fig = plt.figure(dpi=150) fig.clf() ax = fig.subplots() draw(29) # draw the prediction of the last epoch# '以下動畫顯示了模型在一系列訓練時期之后如何正確預測社區?????顯示不了動畫' # ani = animation.FuncAnimation(fig, draw, frames=len(all_logits), interval=200) plt.show()