DGL說明文檔


DGL源碼

https://docs.dgl.ai/en/0.5.x/_modules/index.html

一、DGL概述

Deep Graph Library(DGL)是一個Python軟件包,用於在現有DL框架(例如PyTorch,MXNet,Gluon等)之上實現圖神經網絡模型。

DGL將圖神經網絡的實現簡化為聲明一組函數。

此外,DGL還提供:

  • 消息傳遞的多功能控件,從低級別的操作(如沿選定邊緣發送和在特定節點上接收)到高層的控件(如圖形范圍的功能更新。
  • 具有自動批處理計算和稀疏矩陣乘法的透明速度優化。
  • 與現有深度學習框架的無縫集成。
  • 簡單易用的界面,用於節點/邊緣特征訪問和圖形結構操作。
  • 對具有數千萬個頂點的圖具有良好的可伸縮性。

二、安裝

官網安裝

DGL可與以下操作系統配合使用:

  • Ubuntu 16.04

  • mac OS X

  • Windows 10

DGL需要Python 3.6或更高版本。

DGL支持作為后端,例如PyTorch,MXNet。 有關后端的要求以及如何選擇后端的信息,請參閱使用不同的后端。

2.1、源碼下載

Download the source files from GitHub.

git clone --recurse-submodules https://github.com/dmlc/dgl.git

(Optional) Clone the repository first, and then run the following:

git submodule update --init --recursive

2.2、mac安裝

Installation on macOS is similar to Linux. But macOS users need to install build tools like clang, GNU Make, and cmake first. These installation steps were tested on macOS X with clang 10.0.0, GNU Make 3.81, and cmake 3.13.1.

Tools like clang and GNU Make are packaged in Command Line Tools for macOS. To install, run the following:

xcode-select --install

After you install Homebrew, install cmake.

brew install cmake

Go to root directory of the DGL repository, build a shared library, and install the Python binding for DGL.

mkdir build
cd build
cmake -DUSE_OPENMP=off ..
make -j4
cd ../python
python setup.py install

三、圖

圖表示實體(節點)和它們的關系(邊),其中節點和邊可以是有類型的 (例如,"用戶""物品" 是兩種不同類型的節點)。

DGL通過其核心數據結構 DGLGraph 提供了一個以圖為中心的編程抽象。

DGLGraph 提供了接口以處理圖的結構、節點/邊 的特征,以及使用這些組件可以執行的計算。

3.1、關於圖的基本概念

  • 圖是用以表示實體及其關系的結構,記為 𝐺=(𝑉,𝐸)。

  • 圖由兩個集合組成,一是節點的集合 𝑉 ,一個是邊的集合 𝐸 。 在邊集 𝐸中,一條邊 (𝑢,𝑣)連接一對節點 𝑢和𝑣,表明兩節點間存在關系。

  • 關系可以是無向的, 如描述節點之間的對稱關系;也可以是有向的,如描述非對稱關系。例如,若用圖對社交網絡中人們的友誼關系進行建模,因為友誼是相互的,則邊是無向的; 若用圖對Twitter用戶的關注行為進行建模,則邊是有向的。圖可以是 有向的無向的 ,這取決於圖中邊的方向性。

  • 圖可以是 加權的未加權的 。在加權圖中,每條邊都與一個標量權重值相關聯。例如,該權重可以表示長度或連接的強度。

  • 圖可以是 同構的 或是 異構的 。在同構圖中,所有節點表示同一類型的實體,所有邊表示同一類型的關系。例如,社交網絡的圖由表示同一實體類型的人及其相互之間的社交關系組成。相對地,在異構圖中,節點和邊的類型可以是不同的。例如,編碼市場的圖可以有表示”顧客”、”商家”和”商品”的節點, 它們通過“想購買”、“已經購買”、“是顧客”和“正在銷售”的邊互相連接。

  • 二分圖是一類特殊的、常用的異構圖, 其中的邊連接兩類不同類型的節點。例如,在推薦系統中,可以使用二分圖表示”用戶”和”物品”之間的關系。

  • 在多重圖中,同一對節點之間可以有多條(有向)邊,包括自循環的邊。例如,兩名作者可以在不同年份共同署名文章, 這就帶來了具有不同特征的多條邊。

3.2、圖、節點和邊

  • DGL使用一個唯一的整數來表示一個節點,稱為點ID;並用對應的兩個端點ID表示一條邊。

  • 同時,DGL也會根據邊被添加的順序, 給每條邊分配一個唯一的整數編號,稱為邊ID。節點和邊的ID都是從0開始構建的。

  • 在DGL的圖里,所有的邊都是有方向的, 即邊 (𝑢,𝑣)表示它是從節點 𝑢 指向節點 𝑣 的。對於多個節點,DGL使用一個一維的整型張量(如,PyTorch的Tensor類,TensorFlow的Tensor類或MXNet的ndarray類)來保存圖的點ID, DGL稱之為”節點張量”。

  • 為了指代多條邊,DGL使用一個包含2個節點張量的元組 (𝑈,𝑉),其中,用 (𝑈[𝑖],𝑉[𝑖]) 指代一條 𝑈[𝑖] 到 𝑉[𝑖] 的邊。創建一個 DGLGraph 對象的一種方法是使用 dgl.graph() 函數。它接受一個邊的集合作為輸入。

  • DGL也支持從其他的數據源來創建圖對象。

  • 下面的代碼段使用了 dgl.graph() 函數來構建一個 DGLGraph 對象,對應着下圖所示的包含4個節點的圖。 其中一些代碼演示了查詢圖結構的部分API的使用方法。

>>> import dgl
>>> import torch as th

>>> # 邊 0->1, 0->2, 0->3, 1->3
>>> u, v = th.tensor([0, 0, 0, 1]), th.tensor([1, 2, 3, 3])
>>> g = dgl.graph((u, v))
>>> print(g) # 圖中節點的數量是DGL通過給定的圖的邊列表中最大的點ID推斷所得出的
Graph(num_nodes=4, num_edges=4,
      ndata_schemes={}
      edata_schemes={})

img

>>> # 獲取節點的ID
>>> print(g.nodes())
tensor([0, 1, 2, 3])
>>> # 獲取邊的對應端點
>>> print(g.edges())
(tensor([0, 0, 0, 1]), tensor([1, 2, 3, 3]))
>>> # 獲取邊的對應端點和邊ID
>>> print(g.edges(form='all'))
(tensor([0, 0, 0, 1]), tensor([1, 2, 3, 3]), tensor([0, 1, 2, 3]))

>>> # 如果具有最大ID的節點沒有邊,在創建圖的時候,用戶需要明確地指明節點的數量。
>>> g = dgl.graph((u, v), num_nodes=8)

對於無向的圖,用戶需要為每條邊都創建兩個方向的邊。

可以使用 dgl.to_bidirected() 函數來實現這個目的。 如下面的代碼段所示,這個函數可以把原圖轉換成一個包含反向邊的圖。

>>> bg = dgl.to_bidirected(g)
>>> bg.edges()
(tensor([0, 0, 0, 1, 1, 2, 3, 3]), tensor([1, 2, 3, 0, 3, 0, 0, 1]))

DGL支持使用 32 位或 64 位的整數作為節點ID和邊ID。節點和邊ID的數據類型必須一致。如果使用 64 位整數, DGL可以處理最多 \(2^{63}−1\)個節點或邊。不過,如果圖里的節點或者邊的數量小於$ 2^{63}−1$,用戶最好使用 32 位整數。 這樣不僅能提升速度,還能減少內存的使用。

DGL提供了進行數據類型轉換的方法,如下例所示。

>>> edges = th.tensor([2, 5, 3]), th.tensor([3, 5, 0])  # 邊:2->3, 5->5, 3->0
>>> g64 = dgl.graph(edges)  # DGL默認使用int64
>>> print(g64.idtype)
torch.int64
>>> g32 = dgl.graph(edges, idtype=th.int32)  # 使用int32構建圖
>>> g32.idtype
torch.int32
>>> g64_2 = g32.long()  # 轉換成int64
>>> g64_2.idtype
torch.int64
>>> g32_2 = g64.int()  # 轉換成int32
>>> g32_2.idtype
torch.int32

3.3、節點和邊的特征

DGLGraph 對象的節點和邊可具有多個用戶定義的、可命名的特征,以儲存圖的節點和邊的屬性。

通過 ndataedata 接口可訪問這些特征。

以下代碼創建了2個節點特征(分別在第8、15行命名為 'x''y' )和1個邊特征(在第9行命名為 'x' )。

>>> import dgl
>>> import torch as th
>>> g = dgl.graph(([0, 0, 1, 5], [1, 2, 2, 0])) # 6個節點,4條邊
>>> g
Graph(num_nodes=6, num_edges=4,
      ndata_schemes={}
      edata_schemes={})
>>> g.ndata['x'] = th.ones(g.num_nodes(), 3)               # 長度為3的節點特征
>>> g.edata['x'] = th.ones(g.num_edges(), dtype=th.int32)  # 標量整型特征
>>> g
Graph(num_nodes=6, num_edges=4,
      ndata_schemes={'x' : Scheme(shape=(3,), dtype=torch.float32)}
      edata_schemes={'x' : Scheme(shape=(,), dtype=torch.int32)})
>>> # 不同名稱的特征可以具有不同形狀
>>> g.ndata['y'] = th.randn(g.num_nodes(), 5)
>>> g.ndata['x'][1]                  # 獲取節點1的特征
tensor([1., 1., 1.])
>>> g.edata['x'][th.tensor([0, 3])]  # 獲取邊0和3的特征
    tensor([1, 1], dtype=torch.int32)

關於 ndataedata 接口的重要說明:

  • 僅允許使用數值類型(如單精度浮點型、雙精度浮點型和整型)的特征。這些特征可以是標量、向量或多維張量。
  • 每個節點特征具有唯一名稱,每個邊特征也具有唯一名稱。節點和邊的特征可以具有相同的名稱(如上述示例代碼中的 'x' )。
  • 通過張量分配創建特征時,DGL會將特征賦給圖中的每個節點和每條邊。該張量的第一維必須與圖中節點或邊的數量一致。 不能將特征賦給圖中節點或邊的子集。
  • 相同名稱的特征必須具有相同的維度和數據類型。
  • 特征張量使用”行優先”的原則,即每個行切片儲存1個節點或1條邊的特征(參考上述示例代碼的第16和18行)。

對於加權圖,用戶可以將權重儲存為一個邊特征,如下。

>>> # 邊 0->1, 0->2, 0->3, 1->3
>>> edges = th.tensor([0, 0, 0, 1]), th.tensor([1, 2, 3, 3])
>>> weights = th.tensor([0.1, 0.6, 0.9, 0.7])  # 每條邊的權重
>>> g = dgl.graph(edges)
>>> g.edata['w'] = weights  # 將其命名為 'w'
>>> g
Graph(num_nodes=4, num_edges=4,
      ndata_schemes={}
      edata_schemes={'w' : Scheme(shape=(,), dtype=torch.float32)})

3.4、從外部源創建圖

1、從SciPy稀疏矩陣和NetworkX圖創建DGL圖

>>> import dgl
>>> import torch as th
>>> import scipy.sparse as sp
>>> spmat = sp.rand(100, 100, density=0.05) # 5%非零項
>>> dgl.from_scipy(spmat)                   # 來自SciPy
Graph(num_nodes=100, num_edges=500,
      ndata_schemes={}
      edata_schemes={})
>>> import networkx as nx
>>> nx_g = nx.path_graph(5) # 一條鏈路0-1-2-3-4
>>> g=dgl.from_networkx(nx_g) # 來自NetworkX
>>> bg = dgl.to_bidirected(g)
>>> bg.edges()
(tensor([0, 1, 1, 2, 2, 3, 3, 4]), tensor([1, 0, 2, 1, 3, 2, 4, 3]))

2、從磁盤加載圖

有多種文件格式可儲存圖,所以這里難以枚舉所有選項。本節僅給出一些常見格式的一般情況。

CSV是一種常見的格式,以表格格式儲存節點、邊及其特征:

參考: 從成對的邊 CSV 文件中加載 Karate Club Network 的教程

!ls -lh 'data'
total 24
-rw-r--r--@ 1 wzx  staff   3.7K 11  1 22:50 edges.csv
-rw-r--r--@ 1 wzx  staff   1.2K 11  1 22:50 gen_data.py
-rw-r--r--@ 1 wzx  staff   461B 11  1 22:50 nodes.csv
import pandas as pd

nodes_data = pd.read_csv('data/nodes.csv')
print(nodes_data)
edges_data = pd.read_csv('data/edges.csv')
print(edges_data)
import dgl

src = edges_data['Src'].to_numpy()
dst = edges_data['Dst'].to_numpy()

# Create a DGL graph from a pair of numpy arrays
g = dgl.graph((src, dst))

# Print a graph gives some meta information such as number of nodes and edges.
print(g)
Graph(num_nodes=34, num_edges=156,
      ndata_schemes={}
      edata_schemes={})

A DGL graph can be converted to a networkx graph, so to utilize its rich functionalities such as visualization.

import networkx as nx
# Since the actual graph is undirected, we convert it for visualization
# purpose.
nx_g = g.to_networkx().to_undirected()
# Kamada-Kawaii layout usually looks pretty for arbitrary graphs
pos = nx.kamada_kawai_layout(nx_g)
nx.draw(nx_g, pos, with_labels=True, node_color=[[.7, .7, .7]])

img

3.5、異構圖

相比同構圖,異構圖里可以有不同類型的節點和邊。這些不同類型的節點和邊具有獨立的ID空間和特征。

例如在下圖中,”用戶”和”游戲”節點的ID都是從0開始的,而且兩種節點具有不同的特征。

1、創建異構圖

在DGL中,一個異構圖由一系列子圖構成,一個子圖對應一種關系。每個關系由一個字符串三元組 定義 (源節點類型, 邊類型, 目標節點類型) 。由於這里的關系定義消除了邊類型的歧義,DGL稱它們為規范邊類型。

下面的代碼是一個在DGL中創建異構圖的示例。

>>> import dgl
>>> import torch as th

>>> # 創建一個具有3種節點類型和3種邊類型的異構圖
>>> graph_data = {
...    ('drug', 'interacts', 'drug'): (th.tensor([0, 1]), th.tensor([1, 2])),
...    ('drug', 'interacts', 'gene'): (th.tensor([0, 1]), th.tensor([2, 3])),
...    ('drug', 'treats', 'disease'): (th.tensor([1]), th.tensor([2]))
... }
>>> g = dgl.heterograph(graph_data)
>>> g.ntypes
['disease', 'drug', 'gene']
>>> g.etypes
['interacts', 'interacts', 'treats']
>>> g.canonical_etypes
[('drug', 'interacts', 'drug'),
 ('drug', 'interacts', 'gene'),
 ('drug', 'treats', 'disease')]
>>> g
Graph(num_nodes={'disease': 3, 'drug': 3, 'gene': 4},
      num_edges={('drug', 'interacts', 'drug'): 2, ('drug', 'interacts', 'gene'): 2, ('drug', 'treats', 'disease'): 1},
      metagraph=[('drug', 'drug', 'interacts'), ('drug', 'gene', 'interacts'), ('drug', 'disease', 'treats')])

注意,同構圖和二分圖只是一種特殊的異構圖,它們只包括一種關系。

>>> # 一個同構圖
>>> dgl.heterograph({('node_type', 'edge_type', 'node_type'): (u, v)})
>>> # 一個二分圖
>>> dgl.heterograph({('source_type', 'edge_type', 'destination_type'): (u, v)})

3.6、在GPU上使用DGLGraph

>>> import dgl
>>> import torch as th
>>> u, v = th.tensor([0, 1, 2]), th.tensor([2, 3, 4])
>>> g = dgl.graph((u, v))
>>> g.ndata['x'] = th.randn(5, 3)   # 原始特征在CPU上
>>> g.device
device(type='cpu')
>>> cuda_g = g.to('cuda:0')         # 接受來自后端框架的任何設備對象
>>> cuda_g.device
device(type='cuda', index=0)
>>> cuda_g.ndata['x'].device        # 特征數據也拷貝到了GPU上
device(type='cuda', index=0)

>>> # 由GPU張量構造的圖也在GPU上
>>> u, v = u.to('cuda:0'), v.to('cuda:0')
>>> g = dgl.graph((u, v))
>>> g.device
device(type='cuda', index=0)

任何涉及GPU圖的操作都是在GPU上運行的。

因此,這要求所有張量參數都已經放在GPU上,其結果(圖或張量)也將在GPU上。

此外,GPU圖只接受GPU上的特征數據。

>>> cuda_g.in_degrees()
tensor([0, 0, 1, 1, 1], device='cuda:0')
>>> cuda_g.in_edges([2, 3, 4])                          # 可以接受非張量類型的參數
(tensor([0, 1, 2], device='cuda:0'), tensor([2, 3, 4], device='cuda:0'))
>>> cuda_g.in_edges(th.tensor([2, 3, 4]).to('cuda:0'))  # 張量類型的參數必須在GPU上
(tensor([0, 1, 2], device='cuda:0'), tensor([2, 3, 4], device='cuda:0'))
>>> cuda_g.ndata['h'] = th.randn(5, 4)                  # ERROR! 特征也必須在GPU上!
DGLError: Cannot assign node feature "h" on device cpu to a graph on device
cuda:0. Call DGLGraph.to() to copy the graph to the same device.

四、消息傳遞范式

消息傳遞是實現GNN的一種通用框架和編程范式。它從聚合與更新的角度歸納總結了多種GNN模型的實現。

假設節點 𝑣 上的的特征為 \(𝑥_𝑣∈R^{𝑑_1}\),邊 (𝑢,𝑣) 上的特征為 \(𝑤_𝑒∈R^{𝑑_2}\)。 消息傳遞范式定義了以下逐節點和邊上的計算

在上面的等式中, 𝜙是定義在每條邊上的消息函數,它通過將邊上特征與其兩端節點的特征相結合來生成消息。

聚合函數 𝜌 會聚合節點接受到的消息。

更新函數 𝜓 會結合聚合后的消息和節點本身的特征來更新節點的特征。

4.1、內置函數和消息傳遞API*

  • 消息函數接受一個參數 edges,這是一個 EdgeBatch 的實例,在消息傳遞時,它被DGL在內部生成以表示一批邊。 edgessrcdstdata 共3個成員屬性, 分別用於訪問源節點、目標節點和邊的特征。聚合函數接受一個參數 nodes,這是一個 NodeBatch 的實例, 在消息傳遞時,它被DGL在內部生成以表示一批節點。 nodes 的成員屬性 mailbox 可以用來訪問節點收到的消息。 一些最常見的聚合操作包括 summaxmin等。
  • 更新函數 接受一個如上所述的參數 nodes。此函數對 聚合函數 的聚合結果進行操作, 通常在消息傳遞的最后一步將其與節點的特征相結合,並將輸出作為節點的新特征。
  • DGL在命名空間 dgl.function 中實現了常用的消息函數和聚合函數作為 內置函數。 一般來說,DGL建議 盡可能 使用內置函數,因為它們經過了大量優化,並且可以自動處理維度廣播。
  • 如果用戶的消息傳遞函數無法用內置函數實現,則可以實現自己的消息或聚合函數(也稱為 用戶定義函數 )。

內置消息函數可以是一元函數或二元函數。

對於一元函數,DGL支持 copy 函數。

對於二元函數, DGL現在支持 addsubmuldivdot 函數。

消息的內置函數的命名約定是 u 表示 src 節點, v 表示 dst 節點,e 表示 edge。這些函數的參數是字符串,指示相應節點和邊的輸入和輸出特征字段名。 關於內置函數的列表,請參見 DGL Built-in Function。例如,要對源節點的 hu特征和目標節點的 hv 特征求和, 然后將結果保存在邊的 he 特征上,用戶可以使用內置函數 dgl.function.u_add_v('hu', 'hv', 'he')。 而以下用戶定義消息函數與此內置函數等價。

def message_func(edges):
     return {'he': edges.src['hu'] + edges.dst['hv']}

DGL支持內置的聚合函數 summaxminmean 操作。 聚合函數通常有兩個參數,它們的類型都是字符串。一個用於指定 mailbox 中的字段名,一個用於指示目標節點特征的字段名, 例如, dgl.function.sum('m', 'h') 等價於如下所示的對接收到消息求和的用戶定義函數:

import torch
def reduce_func(nodes):
     return {'h': torch.sum(nodes.mailbox['m'], dim=1)}

在DGL中,也可以在不涉及消息傳遞的情況下,通過 apply_edges() 單獨調用逐邊計算。 apply_edges() 的參數是一個消息函數。並且在默認情況下,這個接口將更新所有的邊。例如:

import dgl.function as fn
graph.apply_edges(fn.u_add_v('el', 'er', 'e'))

對於消息傳遞, update_all() 是一個高級API。它在單個API調用里合並了消息生成、 消息聚合和節點特征更新,這為從整體上進行系統優化提供了空間。

update_all() 的參數是一個消息函數、一個聚合函數和一個更新函數。 更新函數是一個可選擇的參數,用戶也可以不使用它,而是在 update_all 執行完后直接對節點特征進行操作。

由於更新函數通常可以用純張量操作實現,所以DGL不推薦在 update_all 中指定更新函數。例如:

def updata_all_example(graph):
    # 在graph.ndata['ft']中存儲結果
    graph.update_all(fn.u_mul_e('ft', 'a', 'm'),fn.sum('m', 'ft'))
    # 在update_all外調用更新函數
    final_ft = graph.ndata['ft'] * 2
    return final_ft

此調用通過將源節點特征 ft 與邊特征 a 相乘生成消息 m, 然后對所有消息求和來更新節點特征 ft,再將 ft 乘以2得到最終結果 final_ft

調用后,中間消息 m 將被清除。上述函數的數學公式為:

image-20210216004228068

4.2、編寫高效的消息傳遞代碼

DGL優化了消息傳遞的內存消耗和計算速度,這包括:

  • 將多個內核合並到一個內核中:這是通過使用 update_all() 一次調用多個內置函數來實現的。(速度優化)
  • 節點和邊上的並行計算:DGL抽象了逐邊計算,將 apply_edges() 作為一種廣義抽樣稠密-稠密矩陣乘法 (gSDDMM) 運算,並實現了跨邊並行計算。同樣,DGL將逐節點計算 update_all() 抽象為廣義稀疏-稠密矩陣乘法(gSPMM)運算, 並實現了跨節點並行計算。(速度優化)
  • 避免不必要的從點到邊的內存拷貝:想要生成帶有源節點和目標節點特征的消息,一個選項是將源節點和目標節點的特征拷貝到邊上。 對於某些圖,邊的數量遠遠大於節點的數量。這個拷貝的代價會很大。DGL內置的消息函數通過使用條目索引對節點特征進行采集來避免這種內存拷貝。 (內存和速度優化)
  • 避免具體化邊上的特征向量:完整的消息傳遞過程包括消息生成、消息聚合和節點更新。 在調用 update_all() 時,如果消息函數和聚合函數是內置的,則它們會被合並到一個內核中, 從而避免存儲消息對象。(內存優化)

根據以上所述,利用這些優化的一個常見實踐是通過基於內置函數的 update_all() 來開發消息傳遞功能。

對於某些情況,比如 GATConv,計算必須在邊上保存消息, 那么用戶就需要調用基於內置函數的 apply_edges()。有時邊上的消息可能是高維的,這會非常消耗內存。 DGL建議用戶盡量減少邊的特征維數。

下面是一個如何通過對節點特征降維來減少消息維度的示例。該做法執行以下操作:拼接 節點和 目標 節點特征, 然后應用一個線性層,即 𝑊×(𝑢||𝑣)。 src 節點和 dst 節點特征維數較高,而線性層輸出維數較低。 一個直截了當的實現方式如下:

import torch
import torch.nn as nn

linear = nn.Parameter(torch.FloatTensor(size=(1, node_feat_dim * 2)))
def concat_message_function(edges):
     return {'cat_feat': torch.cat([edges.src.ndata['feat'], edges.dst.ndata['feat']])}
g.apply_edges(concat_message_function)
g.edata['out'] = g.edata['cat_feat'] * linear

建議的實現是將線性操作分成兩部分,一個應用於 節點特征,另一個應用於 目標 節點特征。 在最后一個階段,在邊上將以上兩部分線性操作的結果相加,即執行 𝑊𝑙×𝑢+𝑊𝑟×𝑣Wl×u+Wr×v, 因為 𝑊×(𝑢||𝑣)=𝑊𝑙×𝑢+𝑊𝑟×𝑣W×(u||v)=Wl×u+Wr×v,其中 𝑊𝑙Wl 和 𝑊𝑟Wr 分別是矩陣 𝑊W 的左半部分和右半部分:

import dgl.function as fn
linear_src = nn.Parameter(torch.FloatTensor(size=(1, node_feat_dim)))
linear_dst = nn.Parameter(torch.FloatTensor(size=(1, node_feat_dim)))
out_src = g.ndata['feat'] * linear_src
out_dst = g.ndata['feat'] * linear_dst
g.srcdata.update({'out_src': out_src})
g.dstdata.update({'out_dst': out_dst})
g.apply_edges(fn.u_add_v('out_src', 'out_dst', 'out'))

以上兩個實現在數學上是等價的。后一種方法效率高得多,因為不需要在邊上保存feat_src和feat_dst, 從內存角度來說是高效的。另外,加法可以通過DGL的內置函數 u_add_v 進行優化,從而進一步加快計算速度並節省內存占用。

4.3、在圖的一部分上進行消息傳遞

如果用戶只想更新圖中的部分節點,可以先通過想要囊括的節點編號創建一個子圖, 然后在子圖上調用 update_all() 方法。例如:

nid = [0, 2, 3, 6, 7, 9]
sg = g.subgraph(nid)
sg.update_all(message_func, reduce_func, apply_node_func)

這是小批量訓練中的常見用法。更多詳細用法請參考用戶指南 第6章:在大圖上的隨機(批次)訓練

4.4、在消息傳遞中使用邊的權重

一類常見的圖神經網絡建模的做法是在消息聚合前使用邊的權重, 比如在 圖注意力網絡(GAT) 和一些 GCN的變種 。 DGL的處理方法是:

  • 將權重存為邊的特征。
  • 在消息函數中用邊的特征與源節點的特征相乘。

例如:

import dgl.function as fn

graph.edata['a'] = affinity
graph.update_all(fn.u_mul_e('ft', 'a', 'm'),fn.sum('m', 'ft'))

在以上代碼中,affinity被用作邊的權重。邊權重通常是一個標量。

五、構建圖神經網絡(GNN)模塊*

DGL NN模塊是用戶構建GNN模型的基本模塊。根據DGL所使用的后端深度神經網絡框架, DGL NN模塊的父類取決於后端所使用的深度神經網絡框架。

在DGL NN模塊中,構造函數中的參數注冊和前向傳播函數中使用的張量操作與后端框架一樣。這種方式使得DGL的代碼可以無縫嵌入到后端框架的代碼中。 DGL和這些深度神經網絡框架的主要差異是其獨有的消息傳遞操作。

DGL已經集成了很多常用的 Conv LayersDense Conv LayersGlobal Pooling LayersUtility Modules。歡迎給DGL貢獻更多的模塊!

本章將使用PyTorch作為后端,用 SAGEConv 作為例子來介紹如何構建用戶自己的DGL NN模塊。

5.1、 DGL NN模塊的構造函數

構造函數完成以下幾個任務:

  1. 設置選項。
  2. 注冊可學習的參數或者子模塊。
  3. 初始化參數。
import torch.nn as nn

from dgl.utils import expand_as_pair

class SAGEConv(nn.Module):
    def __init__(self,
                 in_feats,
                 out_feats,
                 aggregator_type,
                 bias=True,
                 norm=None,
                 activation=None):
        super(SAGEConv, self).__init__()

        self._in_src_feats, self._in_dst_feats = expand_as_pair(in_feats)
        self._out_feats = out_feats
        self._aggre_type = aggregator_type
        self.norm = norm
        self.activation = activation
        # 聚合類型:mean、max_pool、lstm、gcn
        if aggregator_type not in ['mean', 'max_pool', 'lstm', 'gcn']:
            raise KeyError('Aggregator type {} not supported.'.format(aggregator_type))
        if aggregator_type == 'max_pool':
            self.fc_pool = nn.Linear(self._in_src_feats, self._in_src_feats)
        if aggregator_type == 'lstm':
            self.lstm = nn.LSTM(self._in_src_feats, self._in_src_feats, batch_first=True)
        if aggregator_type in ['mean', 'max_pool', 'lstm']:
            self.fc_self = nn.Linear(self._in_dst_feats, out_feats, bias=bias)
        self.fc_neigh = nn.Linear(self._in_src_feats, out_feats, bias=bias)
        self.reset_parameters()
    def reset_parameters(self):
        """重新初始化可學習的參數"""
        gain = nn.init.calculate_gain('relu')
        if self._aggre_type == 'max_pool':
            nn.init.xavier_uniform_(self.fc_pool.weight, gain=gain)
        if self._aggre_type == 'lstm':
            self.lstm.reset_parameters()
        if self._aggre_type != 'gcn':
            nn.init.xavier_uniform_(self.fc_self.weight, gain=gain)
        nn.init.xavier_uniform_(self.fc_neigh.weight, gain=gain)
  • 在構造函數中,用戶首先需要設置數據的維度。對於一般的PyTorch模塊,維度通常包括輸入的維度、輸出的維度和隱層的維度。 對於圖神經網絡,輸入維度可被分為源節點特征維度和目標節點特征維度。

  • 除了數據維度,圖神經網絡的一個典型選項是聚合類型(self._aggre_type)。對於特定目標節點,聚合類型決定了如何聚合不同邊上的信息。常用的聚合類型包括 meansummaxmin。一些模塊可能會使用更加復雜的聚合函數,比如 lstm

  • 上面代碼里的 norm 是用於特征歸一化的可調用函數。在SAGEConv論文里,歸一化可以是L2歸一化: \(ℎ_𝑣=ℎ_𝑣/‖ℎ_𝑣‖_2\)

  • 注冊參數和子模塊。

  • 在SAGEConv中,子模塊根據聚合類型而有所不同。這些模塊是純PyTorch NN模塊,例如 nn.Linearnn.LSTM 等。

  • 構造函數的最后調用了 reset_parameters() 進行權重初始化。

5.2、編寫DGL NN模塊的forward函數

5.3、異構圖上的GraphConv模塊

六、圖數據處理管道

DGL在 dgl.data 里實現了很多常用的圖數據集。

它們遵循了由 dgl.data.DGLDataset 類定義的標准的數據處理管道。

DGL推薦用戶將圖數據處理為 dgl.data.DGLDataset 的子類。該類為導入、處理和保存圖數據提供了簡單而干凈的解決方案。

本章介紹了如何為用戶自己的圖數據創建一個DGL數據集。以下內容說明了管道的工作方式,並展示了如何實現管道的每個組件。

6.1、DGLDataset類

為了處理位於遠程服務器或本地磁盤上的圖數據集,下面的例子中定義了一個類,稱為 MyDataset, 它繼承自 dgl.data.DGLDataset

from dgl.data import DGLDataset

class MyDataset(DGLDataset):
    """ 用於在DGL中自定義圖數據集的模板:

    Parameters
    ----------
    url : str
        下載原始數據集的url。
    raw_dir : str
        指定下載數據的存儲目錄或已下載數據的存儲目錄。默認: ~/.dgl/
    save_dir : str
        處理完成的數據集的保存目錄。默認:raw_dir指定的值
    force_reload : bool
        是否重新導入數據集。默認:False
    verbose : bool
        是否打印進度信息。
    """
    def __init__(self,
                 url=None,
                 raw_dir=None,
                 save_dir=None,
                 force_reload=False,
                 verbose=False):
        super(MyDataset, self).__init__(name='dataset_name',
                                        url=url,
                                        raw_dir=raw_dir,
                                        save_dir=save_dir,
                                        force_reload=force_reload,
                                        verbose=verbose)

    def download(self):
        # 將原始數據下載到本地磁盤
        pass

    def process(self):
        # 將原始數據處理為圖、標簽和數據集划分的掩碼
        pass

    def __getitem__(self, idx):
        # 通過idx得到與之對應的一個樣本
        pass

    def __len__(self):
        # 數據樣本的數量
        pass

    def save(self):
        # 將處理后的數據保存至 `self.save_path`
        pass

    def load(self):
        # 從 `self.save_path` 導入處理后的數據
        pass

    def has_cache(self):
        # 檢查在 `self.save_path` 中是否存有處理后的數據
        pass

DGLDataset 類有抽象函數 process()__getitem__(idx)__len__()

子類必須實現這些函數。同時DGL也建議實現保存和導入函數, 因為對於處理后的大型數據集,這么做可以節省大量的時間, 並且有多個已有的API可以簡化此操作(請參閱 4.4 保存和加載數據)。

請注意, DGLDataset 的目的是提供一種標准且方便的方式來導入圖數據。 用戶可以存儲有關數據集的圖、特征、標簽、掩碼,以及諸如類別數、標簽數等基本信息。 諸如采樣、划分或特征歸一化等操作建議在 DGLDataset 子類之外完成。

6.2、下載原始數據(可選)

6.3、處理數據

6.4、保存和加載數據

6.5、使用ogb包導入OGB數據集

七: 訓練圖神經網絡*

本章討論訓練用於節點分類,邊分類,鏈接預測和批圖的圖分類的圖神經網絡。

本章假設您的圖及其所有節點和邊都可以放入GPU。 如果不能的話, 參閱第6章:在大圖上的隨機(批次)訓練。。

以下文本假定已經准備好圖形和節點/邊特征。

如果您打算使用DGL提供的數據集或其他兼容的“ DGLDataset”,則可以使用以下內容獲取單圖形數據集的圖形:

import dgl

dataset = dgl.data.CiteseerGraphDataset()
graph = dataset[0]

Note: In this chapter we will use PyTorch as backend.

異構圖

有時您想處理異構圖。 這里我們以合成異構圖為例,演示節點分類,邊分類和鏈接預測任務。

合成異構圖hetero_graph具有以下邊類型:

  • ('user', 'follow', 'user')
  • ('user', 'followed-by', 'user')
  • ('user', 'click', 'item')
  • ('item', 'clicked-by', 'user')
  • ('user', 'dislike', 'item')
  • ('item', 'disliked-by', 'user')
import numpy as np
import torch

n_users = 1000
n_items = 500
n_follows = 3000
n_clicks = 5000
n_dislikes = 500
n_hetero_features = 10
n_user_classes = 5
n_max_clicks = 10

follow_src = np.random.randint(0, n_users, n_follows)
follow_dst = np.random.randint(0, n_users, n_follows)
click_src = np.random.randint(0, n_users, n_clicks)
click_dst = np.random.randint(0, n_items, n_clicks)
dislike_src = np.random.randint(0, n_users, n_dislikes)
dislike_dst = np.random.randint(0, n_items, n_dislikes)

hetero_graph = dgl.heterograph({
    ('user', 'follow', 'user'): (follow_src, follow_dst),
    ('user', 'followed-by', 'user'): (follow_dst, follow_src),
    ('user', 'click', 'item'): (click_src, click_dst),
    ('item', 'clicked-by', 'user'): (click_dst, click_src),
    ('user', 'dislike', 'item'): (dislike_src, dislike_dst),
    ('item', 'disliked-by', 'user'): (dislike_dst, dislike_src)})

hetero_graph.nodes['user'].data['feature'] = torch.randn(n_users, n_hetero_features)
hetero_graph.nodes['item'].data['feature'] = torch.randn(n_items, n_hetero_features)
hetero_graph.nodes['user'].data['label'] = torch.randint(0, n_user_classes, (n_users,))
hetero_graph.edges['click'].data['label'] = torch.randint(1, n_max_clicks, (n_clicks,)).float()
# randomly generate training masks on user nodes and click edges
hetero_graph.nodes['user'].data['train_mask'] = torch.zeros(n_users, dtype=torch.bool).bernoulli(0.6)
hetero_graph.edges['click'].data['train_mask'] = torch.zeros(n_clicks, dtype=torch.bool).bernoulli(0.6)

7.1 點分類/回歸

圖神經網絡最流行和廣泛采用的任務之一是節點分類,其中訓練/驗證/測試集中的每個節點都從一組預定義的類別中分配了一個地面真相類別。節點回歸是相似的,其中訓練/驗證/測試集中的每個節點都分配有地面真實數字。

概述

為了對節點進行分類,圖神經網絡執行了第2章:消息傳遞中討論的消息傳遞,以利用節點自身的功能以及相鄰節點和邊緣的功能。消息傳遞可以重復多次,以合並來自更大范圍鄰居的信息。

編寫神經網絡模型

DGL提供了一些內置的圖形卷積模塊,可以執行一輪消息傳遞。在本指南中,我們選擇dgl.nn.pytorch.SAGEConv(在MXNet和Tensorflow中也提供),這是GraphSAGE的圖形卷積模塊。

通常對於圖上的深度學習模型,我們需要一個多層圖神經網絡,在其中進行多輪消息傳遞。這可以通過如下堆疊圖卷積模塊來實現。

# Contruct a two-layer GNN model
import dgl.nn as dglnn
import torch.nn as nn
import torch.nn.functional as F
class SAGE(nn.Module):
    def __init__(self, in_feats, hid_feats, out_feats):
        super().__init__()
        self.conv1 = dglnn.SAGEConv(
            in_feats=in_feats, out_feats=hid_feats, aggregator_type='mean')
        self.conv2 = dglnn.SAGEConv(
            in_feats=hid_feats, out_feats=out_feats, aggregator_type='mean')

    def forward(self, graph, inputs):
        # inputs are features of nodes
        h = self.conv1(graph, inputs)
        h = F.relu(h)
        h = self.conv2(graph, h)
        return h

請注意,您不僅可以將上述模型用於節點分類,還可以為其他任務例如7.2邊分類/回歸,7.3鏈接預測或7.4圖形分類獲得隱藏的節點表示。

循環訓練

在完整圖上進行訓練僅涉及上述模型的正向傳播,並通過將預測與訓練節點上的地面真實標簽進行比較來計算損失。

本節使用DGL內置數據集dgl.data.CiteseerGraphDataset來顯示訓練循環。

The following is an example of evaluating your model by accuracy.

def evaluate(model, graph, features, labels, mask):
    model.eval()
    with torch.no_grad():
        logits = model(graph, features)
        logits = logits[mask]
        labels = labels[mask]
        _, indices = torch.max(logits, dim=1)
        correct = torch.sum(indices == labels)
        return correct.item() * 1.0 / len(labels)

You can then write our training loop as follows.

model = SAGE(in_feats=n_features, hid_feats=100, out_feats=n_labels)
opt = torch.optim.Adam(model.parameters())

for epoch in range(10):
    model.train()
    # forward propagation by using all nodes
    logits = model(graph, node_features)
    # compute loss
    loss = F.cross_entropy(logits[train_mask], node_labels[train_mask])
    # compute validation accuracy
    acc = evaluate(model, graph, node_features, node_labels, valid_mask)
    # backward propagation
    opt.zero_grad()
    loss.backward()
    opt.step()
    print(loss.item())

    # Save model if necessary.  Omitted in this example.

GraphSAGE 提供了一個端到端的同構圖節點分類示例。 您可以在示例中的GraphSAGE類中看到相應的模型實現,其中具有可調整的層數,loss以及可自定義的聚合函數和non-linear。

異構圖

如果圖是異構的,則您可能希望從所有邊的鄰居那里收集消息。 您可以使用模塊dgl.nn.pytorch.HeteroGraphConv 在所有邊上執行消息傳遞,然后為每種邊組合不同的圖卷積模塊。

以下代碼將定義一個異構圖卷積模塊,該模塊首先對每種邊緣類型執行單獨的圖卷積,然后將每種邊緣類型上的消息聚合求和,作為所有節點類型的最終結果。

# Define a Heterograph Conv model
import dgl.nn as dglnn

class RGCN(nn.Module):
    def __init__(self, in_feats, hid_feats, out_feats, rel_names):
        super().__init__()

        self.conv1 = dglnn.HeteroGraphConv({
            rel: dglnn.GraphConv(in_feats, hid_feats)
            for rel in rel_names}, aggregate='sum')
        self.conv2 = dglnn.HeteroGraphConv({
            rel: dglnn.GraphConv(hid_feats, out_feats)
            for rel in rel_names}, aggregate='sum')

    def forward(self, graph, inputs):
        # inputs are features of nodes
        h = self.conv1(graph, inputs)
        h = {k: F.relu(v) for k, v in h.items()}
        h = self.conv2(graph, h)
        return h

DGL為節點分類提供了RGCN 的端到端示例,您可以在 model implementation fileRelGraphConvLayer中看到異構圖卷積的定義。

7.2 邊分類/回歸

有時您希望預測圖邊上的屬性,甚至預測兩個給定節點之間是否存在邊。在這種情況下,您需要一個邊分類/回歸模型。在這里,我們生成用於邊預測的隨機圖作為演示。

src = np.random.randint(0, 100, 500)
dst = np.random.randint(0, 100, 500)
# make it symmetric
edge_pred_graph = dgl.graph((np.concatenate([src, dst]), np.concatenate([dst, src])))
# synthetic node and edge features, as well as edge labels
edge_pred_graph.ndata['feature'] = torch.randn(100, 10)
edge_pred_graph.edata['feature'] = torch.randn(1000, 10)
edge_pred_graph.edata['label'] = torch.randn(1000)
# synthetic train-validation-test splits
edge_pred_graph.edata['train_mask'] = torch.zeros(1000, dtype=torch.bool).bernoulli(0.6)

概述

從上一節中,學習了如何使用多層GNN進行節點分類。 可以將相同技術應用於計算任何節點的隱藏表示。 然后可以從其入射節點的表示中得出邊的預測。

在邊上計算預測的最常見情況是將其表示為其入射節點表示以及邊自身特征的參數化函數。

模型實現與節點分類的區別

假設您使用上一部分中的模型計算節點表示形式,則只需要編寫另一個使用 apply_edges() 方法計算邊緣預測的組件。

例如,如果您想計算每個邊緣的分數以進行邊回歸,則以下代碼將計算每個邊上入射節點表示的點積

import dgl.function as fn
class DotProductPredictor(nn.Module):
    def forward(self, graph, h):
        # h contains the node representations computed from the GNN defined
        # in the node classification section (Section 5.1).
        with graph.local_scope():
            graph.ndata['h'] = h
            graph.apply_edges(fn.u_dot_v('h', 'h', 'score'))
            return graph.edata['score']

還可以編寫一種預測函數,該函數使用MLP預測每個邊緣的向量。 這種載體可用於進一步的下游任務,例如 作為分類分布的對數。

class MLPPredictor(nn.Module):
    def __init__(self, in_features, out_classes):
        super().__init__()
        self.W = nn.Linear(in_features * 2, out_classes)

    def apply_edges(self, edges):
        h_u = edges.src['h']
        h_v = edges.dst['h']
        score = self.W(torch.cat([h_u, h_v], 1))
        return {'score': score}

    def forward(self, graph, h):
        # h contains the node representations computed from the GNN defined
        # in the node classification section (Section 5.1).
        with graph.local_scope():
            graph.ndata['h'] = h
            graph.apply_edges(self.apply_edges)
            return graph.edata['score']

訓練循環

給定節點表示計算模型和邊緣預測器模型,我們可以輕松編寫一個全圖訓練循環,在其中計算所有邊緣的預測。下例以上一節中的SAGE作為節點表示計算模型,以DotPredictor作為邊緣預測器模型。

class Model(nn.Module):
    def __init__(self, in_features, hidden_features, out_features):
        super().__init__()
        self.sage = SAGE(in_features, hidden_features, out_features)
        self.pred = DotProductPredictor()
    def forward(self, g, x):
        h = self.sage(g, x)
        return self.pred(g, h)

在此示例中,我們還假定訓練/驗證/測試邊緣集由邊緣上的布爾掩碼標識。 此示例也不包括提前停止和模型保存。

node_features = edge_pred_graph.ndata['feature']
edge_label = edge_pred_graph.edata['label']
train_mask = edge_pred_graph.edata['train_mask']
model = Model(10, 20, 5)
opt = torch.optim.Adam(model.parameters())
for epoch in range(10):
    pred = model(edge_pred_graph, node_features)
    loss = ((pred[train_mask] - edge_label[train_mask]) ** 2).mean()
    opt.zero_grad()
    loss.backward()
    opt.step()
    print(loss.item())

異構圖

異構圖上的邊分類與齊次圖上的邊分類沒有太大區別。 如果希望對一種邊緣類型執行邊緣分類,則只需要計算所有節點類型的節點表示形式,並使用apply_edges()方法對該邊緣類型進行預測。

例如,要使DotProductPredictor在異構圖的一種邊緣類型上工作,只需在apply_edges方法中指定邊緣類型。

class HeteroDotProductPredictor(nn.Module):
    def forward(self, graph, h, etype):
        # h contains the node representations for each edge type computed from
        # the GNN for heterogeneous graphs defined in the node classification
        # section (Section 5.1).
        with graph.local_scope():
            graph.ndata['h'] = h   # assigns 'h' of all node types in one shot
            graph.apply_edges(fn.u_dot_v('h', 'h', 'score'), etype=etype)
            return graph.edges[etype].data['score']

You can similarly write a HeteroMLPPredictor.

class MLPPredictor(nn.Module):
    def __init__(self, in_features, out_classes):
        super().__init__()
        self.W = nn.Linear(in_features * 2, out_classes)

    def apply_edges(self, edges):
        h_u = edges.src['h']
        h_v = edges.dst['h']
        score = self.W(torch.cat([h_u, h_v], 1))
        return {'score': score}

    def forward(self, graph, h, etype):
        # h contains the node representations for each edge type computed from
        # the GNN for heterogeneous graphs defined in the node classification
        # section (Section 5.1).
        with graph.local_scope():
            graph.ndata['h'] = h   # assigns 'h' of all node types in one shot
            graph.apply_edges(self.apply_edges, etype=etype)
            return graph.edges[etype].data['score']

預測單個邊緣類型上每個邊得分的端到端模型如下所示:

class Model(nn.Module):
    def __init__(self, in_features, hidden_features, out_features, rel_names):
        super().__init__()
        self.sage = RGCN(in_features, hidden_features, out_features, rel_names)
        self.pred = HeteroDotProductPredictor()
    def forward(self, g, x, etype):
        h = self.sage(g, x)
        return self.pred(g, h, etype)

使用模型僅涉及向模型提供節點類型和特征的字典。

model = Model(10, 20, 5, hetero_graph.etypes)
user_feats = hetero_graph.nodes['user'].data['feature']
item_feats = hetero_graph.nodes['item'].data['feature']
label = hetero_graph.edges['click'].data['label']
train_mask = hetero_graph.edges['click'].data['train_mask']
node_features = {'user': user_feats, 'item': item_feats}

然后,訓練循環看起來與齊次圖中的幾乎相同。 例如,如果您希望預測邊緣類型“ click”的邊緣標簽,則只需

opt = torch.optim.Adam(model.parameters())
for epoch in range(10):
    pred = model(hetero_graph, node_features, 'click')
    loss = ((pred[train_mask] - label[train_mask]) ** 2).mean()
    opt.zero_grad()
    loss.backward()
    opt.step()
    print(loss.item())

預測異構圖上現有邊的邊類型

有時您可能想預測現有邊屬於哪種類型。例如,在給出異構圖形示例的情況下, heterogeneous graph example指定了一條將用戶和項目連接起來的邊,以預測用戶是單擊還是不喜歡該項目。

可以使用異構圖卷積網絡來獲取節點表示。例如仍然可以為此使用先前定義的RGCN。要預測邊緣的類型,您可以簡單地重新使用上面的HeteroDotProductPredictor,以便它使用另一種僅包含一個邊類型的圖來“合並”所有要預測的邊類型,並為每個邊發出每種類型的得分。

在此處的示例中,您需要一個圖,該圖具有兩種節點類型:用戶和項目,以及一個單一的邊緣類型,用於“合並”用戶和項目中的所有邊緣類型,即單擊和不喜歡。可以使用以下語法方便地創建它:

dec_graph = hetero_graph['user', :, 'item']

它返回具有節點類型user和item的異構圖形,以及返回合並了介於兩者之間的所有邊緣類型(即單擊和不喜歡)的單個邊緣類型。

由於上面的語句還返回了原始邊緣類型作為名為dgl.ETYPE的功能,因此我們可以將其用作標簽。

edge_label = dec_graph.edata[dgl.ETYPE]

給定上圖作為邊緣類型預測器模塊的輸入,您可以如下編寫預測器模塊。

class HeteroMLPPredictor(nn.Module):
    def __init__(self, in_dims, n_classes):
        super().__init__()
        self.W = nn.Linear(in_dims * 2, n_classes)

    def apply_edges(self, edges):
        x = torch.cat([edges.src['h'], edges.dst['h']], 1)
        y = self.W(x)
        return {'score': y}

    def forward(self, graph, h):
        # h contains the node representations for each edge type computed from
        # the GNN for heterogeneous graphs defined in the node classification
        # section (Section 5.1).
        with graph.local_scope():
            graph.ndata['h'] = h   # assigns 'h' of all node types in one shot
            graph.apply_edges(self.apply_edges)
            return graph.edata['score']

結合了節點表示模塊和邊緣類型預測器模塊的模型如下

class Model(nn.Module):
    def __init__(self, in_features, hidden_features, out_features, rel_names):
        super().__init__()
        self.sage = RGCN(in_features, hidden_features, out_features, rel_names)
        self.pred = HeteroMLPPredictor(out_features, len(rel_names))
    def forward(self, g, x, dec_graph):
        h = self.sage(g, x)
        return self.pred(dec_graph, h)

訓練循環如下:

model = Model(10, 20, 5, hetero_graph.etypes)
user_feats = hetero_graph.nodes['user'].data['feature']
item_feats = hetero_graph.nodes['item'].data['feature']
node_features = {'user': user_feats, 'item': item_feats}

opt = torch.optim.Adam(model.parameters())
for epoch in range(10):
    logits = model(hetero_graph, node_features, dec_graph)
    loss = F.cross_entropy(logits, edge_label)
    opt.zero_grad()
    loss.backward()
    opt.step()
    print(loss.item())

DGL提供了圖卷積矩陣 作為rating預測的示例,該rating通過預測異構圖上現有邊的類型來制定。 model implementation file 中的節點表示模塊稱為GCMCLayer。 邊緣類型預測器模塊稱為BiDecoder。 兩者都比此處描述的設置復雜。

7.3 鏈接預測

總的來說,Link Prediction是一個Graph問題,它的目標是根據已知的節點和邊,得到新的邊的權值或特征。

在不同的 task 中,Link Prediction 被用於:

  1. 在社交網絡中,進行用戶/商品推薦

  2. 在生物學領域,進行相互作用發現

  3. 在知識圖譜中,進行實體關系學習

  4. 在基礎研究中,進行圖結構捕捉

從圖的角度出發,可以簡單理解為從一個點到其他點的鏈接概率,就好比說,一個社交網絡上,你只和其中很少一部分人建立了社交關系,剩下這么多用戶你都沒有關注啥的,那我現在要給你推薦一些用戶,讓你們互相關注,點贊啥的,推薦的這個過程就可以理解為一個鏈接預測。您可能想預測兩個給定節點之間是否存在邊。 這種模型稱為鏈接預測模型。

概述

基於GNN的鏈路預測模型將兩個節點 \(u\)\(v\) 之間的連通性的可能性表示為 \(ℎ^{𝐿}_𝑢\)\(ℎ^𝐿_𝑣\)的函數,節點是從多層GNN計算得出的。

\[𝑦_{𝑢,𝑣}=𝜙(ℎ^{𝐿}_𝑢,ℎ^𝐿_𝑣) \]

\(𝑦_{𝑢,𝑣}\) 代表了u和v節點之間的 \(score\)

訓練鏈路預測模型涉及將通過邊連接的節點之間的score與任意一對節點之間的 \(score\)進行比較。

例如,在給定連接𝑢和𝑣的邊的情況下,

我們鼓勵節點𝑢和𝑣之間得分高於節點 𝑢 和采樣節點 𝑣' 之間的得分,該得分來自任意噪聲分布\(v'〜P_𝑛(𝑣)\)。 這種方法稱為“負采樣”。如果最小化,有很多損失函數可以實現上述行為。 非詳盡清單包括:

image-20210222172253948

模型實現與邊分類的區別

用於計算 \(𝑢\)\(v\) 之間得分的神經網絡模型與上述邊緣回歸模型相同。

這是使用點積計算邊得分的示例.

class DotProductPredictor(nn.Module):
    def forward(self, graph, h):
        # h contains the node representations computed from the GNN defined
        # in the node classification section (Section 5.1).
        with graph.local_scope():
            graph.ndata['h'] = h
            graph.apply_edges(fn.u_dot_v('h', 'h', 'score'))
            return graph.edata['score']

訓練循環

因為我們的分數預測模型是在圖上運行的,所以我們需要negative examples表示為另一個圖。

該圖包含所有負節點對作為邊。下面顯示了將negative examples表示為圖的示例。 每個邊(𝑢,𝑣)取𝑘個negative examples \((𝑢,𝑣_𝑖)\),其中\(v_i\)從均勻分布中采樣。

def construct_negative_graph(graph, k):
    src, dst = graph.edges()
    neg_src = src.repeat_interleave(k)
    neg_dst = torch.randint(0, graph.number_of_nodes(), (len(src) * k,))
    return dgl.graph((neg_src, neg_dst), num_nodes=graph.number_of_nodes())

預測邊緣得分的模型與邊緣分類/回歸模型相同。

class Model(nn.Module):
    def __init__(self, in_features, hidden_features, out_features):
        super().__init__()
        self.sage = SAGE(in_features, hidden_features, out_features)
        self.pred = DotProductPredictor()
    def forward(self, g, neg_g, x):
        h = self.sage(g, x)
        return self.pred(g, h), self.pred(neg_g, h)

然后,訓練循環反復構造負圖並計算損失。

def compute_loss(pos_score, neg_score):
    # Margin loss
    n_edges = pos_score.shape[0]
    return (1 - neg_score.view(n_edges, -1) + pos_score.unsqueeze(1)).clamp(min=0).mean()

node_features = graph.ndata['feat']
n_features = node_features.shape[1]
k = 5
model = Model(n_features, 100, 100)
opt = torch.optim.Adam(model.parameters())
for epoch in range(10):
    negative_graph = construct_negative_graph(graph, k)
    pos_score, neg_score = model(graph, negative_graph, node_features)
    loss = compute_loss(pos_score, neg_score)
    opt.zero_grad()
    loss.backward()
    opt.step()
    print(loss.item())

經過訓練,可以得到node embeding

node_embeddings = model.sage(graph, node_features)

有多種使用node embeding方法。 示例包括訓練下游分類器,或對相關實體推薦進行最近鄰搜索或最大內積搜索。

7.4 圖分類

有時可能會有多個圖的數據,而不是一個大圖,例如一組不同類型人的社區。

通過用圖表描述同一社區中人們之間的友誼,就可以得到一系列圖表進行分類。

在這種情況下,圖分類模型可以幫助識別社區的類型,即根據結構和整體信息對每個圖進行分類。

概述

圖分類與節點分類或鏈接預測之間的主要區別在於,預測結果表征了整個輸入圖的特性。

可以像以前的任務一樣在節點/邊緣上執行消息傳遞,但是還需要檢索圖形級表示。

圖形分類管道的過程如下:

Graph Classification Process

  • 准備一批圖
  • 在批處理圖上執行消息傳遞以更新節點/邊緣功能
  • 將節點/邊緣特征聚合為圖表示
  • 根據圖表示對圖進行分類

批圖

通常,圖分類任務在許多圖上進行訓練,在訓練模型時,一次僅使用一個圖將非常低效。 從常見的深度學習實踐中借鑒了小批量培訓的想法,可以構建一批多個圖並將它們一起發送以進行一次培訓迭代。

在DGL中,可以從圖列表中構建單個批處理圖。

該批處理圖可以簡單地用作單個大圖,其連接的組件對應於原始小圖。

Batched Graph

圖讀數

數據中的每個圖都可能具有獨特的結構以及節點和邊特征。為了做出單個預測,通常會匯總可能的豐富信息。這種類型的操作稱為讀出。 常見的讀出操作包括所有節點或邊特征的求和,平均值,最大值或最小值。

給定一個圖 \(𝑔\),可以將平均節點特征讀數定義為

\[h_g=\dfrac{1}{|V|}\sum_{v\in V}h_v \]

其中 \(ℎ_𝑔\) 是的表示形式,𝑔是𝑔中節點的集合,\(ℎ_𝑣\) 是節點的功能,DGL為常見的讀取操作提供了內置支持。 例如,dgl.readout_nodes()實現了上述讀出操作,一旦 \(ℎ_𝑔\) 可用,就可以將其傳遞給MLP層以進行分類輸出。

編寫神經網絡模型

模型的輸入是具有節點和邊特征的批處理圖。

批處理圖上的計算

首先,一批不同圖完全分開,即任何兩個圖之間沒有邊。

有了這個不錯的屬性,所有消息傳遞函數仍然具有相同的結果。

其次,將對每個圖形分別進行批處理圖形的讀取功能。假設批次大小為𝐵,並且要聚合的特征的尺寸為𝐷,則讀取結果的形狀將為(𝐵,𝐷)。

import dgl
import torch

g1 = dgl.graph(([0, 1], [1, 0]))
g1.ndata['h'] = torch.tensor([1., 2.])
g2 = dgl.graph(([0, 1], [1, 2]))
g2.ndata['h'] = torch.tensor([1., 2., 3.])

dgl.readout_nodes(g1, 'h')
# tensor([3.])  # 1 + 2

bg = dgl.batch([g1, g2])
dgl.readout_nodes(bg, 'h')
# tensor([3., 6.])  # [1 + 2, 1 + 2 + 3]

最后,通過按順序將所有圖中的相應特征串聯在一起,可以獲得批處理圖中的每個節點/邊緣特征。

bg.ndata['h']
# tensor([1., 2., 1., 2., 3.])
模型定義

鑒於上述的計算規則,我們的模型可以定義為:

import dgl.nn.pytorch as dglnn
import torch.nn as nn

class Classifier(nn.Module):
    def __init__(self, in_dim, hidden_dim, n_classes):
        super(Classifier, self).__init__()
        self.conv1 = dglnn.GraphConv(in_dim, hidden_dim)
        self.conv2 = dglnn.GraphConv(hidden_dim, hidden_dim)
        self.classify = nn.Linear(hidden_dim, n_classes)

    def forward(self, g, h):
        # Apply graph convolution and activation.
        h = F.relu(self.conv1(g, h))
        h = F.relu(self.conv2(g, h))
        with g.local_scope():
            g.ndata['h'] = h
            # Calculate graph representation by average readout.
            hg = dgl.mean_nodes(g, 'h')
            return self.classify(hg)

循環訓練

讀取數據

一旦定義了模型,就可以開始訓練。 由於圖分類處理的是很多相對較小的圖,而不是一個大圖,因此人們可以在隨機mini-gragh上進行有效的訓練,而無需設計復雜的圖采樣算法。

假設有一個圖分類數據集如Graph Data Pipeline中介紹的一樣

import dgl.data
dataset = dgl.data.GINDataset('MUTAG', False)

圖分類數據集中的每個項目都是一對圖及其標簽。 可以通過使用DataLoader,自定義collate函數來批量處理圖形來加快數據加載過程

def collate(samples):
    graphs, labels = map(list, zip(*samples))
    batched_graph = dgl.batch(graphs)
    batched_labels = torch.tensor(labels)
    return batched_graph, batched_labels

然后,可以創建一個DataLoader,該數據在小批處理中迭代圖的數據集。

from torch.utils.data import DataLoader
dataloader = DataLoader(
    dataset,
    batch_size=1024,
    collate_fn=collate,
    drop_last=False,
    shuffle=True)
循環訓練

訓練循環僅涉及遍歷DataLoader並更新Model。

import torch.nn.functional as F

# Only an example, 7 is the input feature size
model = Classifier(7, 20, 5)
opt = torch.optim.Adam(model.parameters())
for epoch in range(20):
    for batched_graph, labels in dataloader:
        feats = batched_graph.ndata['attr'].float()
        logits = model(batched_graph, feats)
        loss = F.cross_entropy(logits, labels)
        opt.zero_grad()
        loss.backward()
        opt.step()

有關圖形分類的端到端示例,請參見DGL’s GIN example。 訓練循環位於main.py中的功能訓練中。 該模型實現位於gin.py中,具有更多組件,例如,使用dgl.nn.pytorch.GINConv作為圖卷積層,批處理規范化等。

八:大圖的隨機訓練

如果我們有一個包含數百萬甚至數十億個節點或邊的大規模圖形,那么通常無法訓練圖神經網絡中所述的全圖訓練。

考慮一個在節點圖上運行的,具有隱藏狀態大小𝐻的𝐿層圖卷積網絡,存儲中間隱藏狀態需要𝑂(𝑁𝐿𝐻)顯存,輕松超過一個GPU的容量N。

8-1、鄰域采樣方法概述

鄰域采樣方法通常按以下方式工作。 對於每個梯度下降步驟,我們選擇小批量圖的節點,其最終表示將在第 \(L\) 層進行計算。 然后我們將所有或部分鄰居作為 \(𝐿-1\) 層的成員。這個過程一直持續到我們輸入。 這個迭代過程將構建依賴關系圖,從輸出開始,一直到輸入,如下圖所示:

Imgur

這樣一來,可以節省用於在大型圖上訓練GNN的工作量和計算資源。DGL提供了一些鄰域采樣器和用於通過鄰域采樣訓練GNN的管道,以及自定義采樣策略的方式。

8-2、學習路線

本章從不同場景下隨機訓練GNN的部分開始。

其余部分涵蓋了更高級的主題,適合那些希望開發新的采樣算法,與小批量培訓兼容的新GNN模塊並了解如何在小批量中進行評估和推斷的人員。

九: 分布式訓練

DGL采用完全分布式的方法,可將數據和計算同時分布在一組計算資源中。在本節的上下文中,我們將假設一個集群。 DGL將圖划分為子圖,並且群集中的每台計算機負責一個子圖。

DGL在群集中的所有計算機上運行相同的訓練腳本以並行化計算,並在同一計算機上運行服務器以將分區數據提供給訓練程序。

對於訓練腳本,DGL提供了類似於mini-batch訓練的分布式API。這使得分布式培訓僅需要對單個機器上的小批量培訓進行少量代碼修改即可。

下面顯示了以分布式方式訓練GraphSage的示例。唯一的代碼修改位於4-7行:

  • 初始化DGL的分布式模塊。
  • 創建一個分布式圖形對象。
  • 拆分訓練集並計算本地過程的節點。

其余代碼(包括采樣器創建,模型定義,訓練循環)與小批量訓練相同。

import dgl
import torch as th

dgl.distributed.initialize('ip_config.txt', num_servers, num_workers)
th.distributed.init_process_group(backend='gloo')
g = dgl.distributed.DistGraph('graph_name', 'part_config.json')
pb = g.get_partition_book()
train_nid = dgl.distributed.node_split(g.ndata['train_mask'], pb, force_even=True)
# Create sampler
sampler = NeighborSampler(g, [10,25],
                          dgl.distributed.sample_neighbors,
                          device)

dataloader = DistDataLoader(
    dataset=train_nid.numpy(),
    batch_size=batch_size,
    collate_fn=sampler.sample_blocks,
    shuffle=True,
    drop_last=False)

# Define model and optimizer
model = SAGE(in_feats, num_hidden, n_classes, num_layers, F.relu, dropout)
model = th.nn.parallel.DistributedDataParallel(model)
loss_fcn = nn.CrossEntropyLoss()
optimizer = optim.Adam(model.parameters(), lr=args.lr)

# training loop
for epoch in range(args.num_epochs):
    for step, blocks in enumerate(dataloader):
        batch_inputs, batch_labels = load_subtensor(g, blocks[0].srcdata[dgl.NID],
                                                    blocks[-1].dstdata[dgl.NID])
        batch_pred = model(blocks, batch_inputs)
        loss = loss_fcn(batch_pred, batch_labels)
        optimizer.zero_grad()
        loss.backward()
        optimizer.step()

在計算機集群中運行訓練腳本時,DGL提供了一些工具,可將數據復制到集群的計算機上並在所有計算機上啟動培訓作業。

注意:當前的分布式培訓API僅支持Pytorch后端。

注意:當前實現僅支持具有一種節點類型和一種邊緣類型的圖形。

DGL實現了一些分布式組件以支持分布式培訓。 下圖顯示了組件及其相互作用。

Imgur

具體來說,DGL的分布式培訓具有三種類型的交互過程:服務器,采樣器和訓練器。

  • 服務器進程在存儲圖形分區(包括圖形結構和節點/邊緣功能)的每台計算機上運行。 這些服務器一起工作以將圖形數據提供給培訓師。 請注意,一台機器可以同時運行多個服務器進程,以並行化計算和網絡通信。

  • 采樣器進程與服務器以及采樣節點和邊緣進行交互,以生成用於訓練的迷你批次。

  • 訓練器包含多個與服務器交互的函數。 它具有 DistGraph來訪問分區圖形數據,並具有DistEmbeddingDistTensor來訪問節點/邊特征。 它具有DistDataLoader與采樣器進行交互以獲得mini-batch。

考慮到分布式組件,本節的其余部分將介紹以下分布式組件:


免責聲明!

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



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