圖神經網絡06-基於Graph的傳統機器學習方法

1 圖學習任務

我們簡單回顧下,上一節我們介紹了,圖的機器學習任務主要是以下三種:

  • Node Level:節點級別
  • Link Level:邊級別
  • Graph Level:圖級別å
    並且三部分難度依次是由淺入深的


2 傳統ML流程

  • 定義和設計節點/邊/圖的特徵
  • 對所有訓練數據構造特徵


  • 訓練ML模型
    (1)隨機森林
    (2)支持向量機
    (3)神經網絡等
  • 應用模型
    給定一個新的節點、邊、圖,然後獲取特徵進行預測

我們總結下 基於Graph的機器學習相關概念和流程,首先明確下目標

目標:對一些對象集合進行預測,比如是分類或者回歸任務
特徵設計:

  • 特徵:d-dimensional向量
  • 對象:Nodes,edges,或者是graps
  • 目標函數:結合具體任務設計目標函數,如下圖所示給定一個圖G(V,E),其中V代表節點集合,E代表邊集合,然後學習節點到向量空間R的映射函數,也就是我們要學習權重參數W


爲了方便,我們下面的例子是基於無向圖(undirected grpah)進行解釋的。

3 節點級別的相關任務

基於圖中帶有標籤的節點訓練模型,然後預測未標註節點的標籤,



在這裏我們主要闡述下Node的四種特徵:

  • Node degree:節點的度
  • Node centrality:節點的中度
  • Clustering coefficient:相似性
  • Graphlets:圖元


節點的度

  • kv代表是節點v與鄰居節點相連邊的個數
  • 所有鄰居節點都是相等的

如下圖所示,A的度爲1,B的度爲2,C的度爲3,D的度爲4


節點的中心度 Node Centrality

  • 節點的度只計算了相連節點的個數,但是沒有評估節點的重要性
  • 節點的中心度c_v考慮了節點在圖中的重要程度
  • 節點的中心度有很多種計算方式:
    (1) Engienvector centrality:特徵向量中心性
    (2) Betweenness centrality:中介中心性
    (3) Closeness centrality:緊密中心性
    (4) 其他方式

Eigenvector centrality:特徵向量中心性

  • 如果一個節點的鄰居節點們u\in N(v)越重要,那麼該節點v就越重要
  • 我們將節點𝑣的中心性建模爲相鄰節點的中心性之和:
    c_v=\frac{1}{\lambda}\sum_{u \in N(v)}{c_u}
    其中\lambda爲一個正常數。
  • 我們注意到上面的等式是通過遞歸的方式來計算度度中心性的,所有我們應該怎麼求解

c_v=\frac{1}{\lambda}\sum_{u \in N(v)}{c_u} \iff \lambda c=Ac

其中\lambda爲正常數,A爲鄰接矩陣,如果u \in N(v) ,那麼A_{uv}=1

  • 我們可以看到度中心性是一個特徵向量(eigenvector),根據非負矩陣定理(Perron-Frobenius Theorem)我們可以知道\lambda_{max}是一個正值並且是唯一的,特徵向量c_{max}當做度中心性。

通常來說,有許多不同的特徵值\lambda 能使得一個特徵方程有非零解存在。然而,考慮到特徵向量中的所有項均爲非負值,根據佩倫-弗羅貝尼烏斯定理,只有特徵值最大時才能測量出想要的中心性。然後通過計算網絡中的節點v其特徵向量的相關分量v^{th}便能得出其對應的中心性的分數。

特徵向量的定義只有一個公因子,因此各節點中心性的比例可以很好確定。爲了確定一個絕對分數,必須將其中一個特徵值標準化,例如所有節點評分之和爲1或者節點數 n。冪次迭代是許多特徵值算法中的一種,該算法可以用來尋找這種主導特徵向量。此外,以上方法可以推廣,使得矩陣A中每個元素可以是表示連接強度的實數,例如隨機矩陣。---特徵向量中心性wiki

這裏其實涉及到比較多的線性代數的理論以及矩陣分析的算法,我特意查了一些文章幫大家去回顧和理解下這裏涉及到的知識:
(1) 線性代數之——特徵值和特徵向量
(2)非負矩陣之Perron-Frobenius定理
(3)非負矩陣
(4)乾貨 | 萬字長文帶你複習線性代數!

我們這裏再通過文章誰是社會網絡中最重要的人?解釋特徵向量中心性:
特徵向量中心性的基本思想是,一個節點的中心性是相鄰節點中心性的函數。也就是說,與你連接的人越重要,你也就越重要

特徵向量中心性和點度中心性不同,一個點度中心性高即擁有很多連接的節點特徵向量中心性不一定高,因爲所有的連接者有可能特徵向量中心性很低。同理,特徵向量中心性高並不意味着它的點度中心性高,它擁有很少但很重要的連接者也可以擁有高特徵向量中心性。

考慮下面的圖,以及相應的5x5的鄰接矩陣(Adjacency Matrix),A。

鄰接矩陣的含義是,如果兩個節點沒有直接連接,記爲0,否則記爲1。

現在考慮x,一個5x1的向量,向量的值對應圖中的每個點。在這種情況下,我們計算的是每個點的點度中心性(degree centrality),即以點的連接數來衡量中心性的高低。

矩陣A乘以這個向量的結果是一個5x1的向量:

結果向量的第一個元素是用矩陣A的第一行去“獲取”每一個與第一個點有連接的點的值(連接數,點度中心性),也就是第2個、第3個和第4個點的值,然後將它們加起來。

換句話說,鄰接矩陣做的事情是將相鄰節點的求和值重新分配給每個點。這樣做的結果就是“擴散了”點度中心性。你的朋友的朋友越多,你的特徵向量中心性就越高。

我們繼續用矩陣A乘以結果向量。如何理解呢?實際上,我們允許這一中心性數值再次沿着圖的邊界“擴散”。我們會觀察到兩個方向上的擴散(點既給予也收穫相鄰節點)。我們猜測,這一過程最後會達到一個平衡,特定點收穫的數量會和它給予相鄰節點的數量取得平衡。既然我們僅僅是累加,數值會越來越大,但我們最終會到達一個點,各個節點在整體中的比例會保持穩定。

現在把所有點的數值構成的向量用更一般的形式表示:

我們認爲,圖中的點存在一個數值集合,對於它,用矩陣A去乘不會改變向量各個數值的相對大小。也就是說,它的數值會變大,但乘以的是同一個因子。用數學符號表示就是:

滿足這一屬性的向量就是矩陣M的特徵向量。特徵向量的元素就是圖中每個點的特徵向量中心性。

特徵向量中心性的計算需要讀者具備矩陣乘法和特徵向量的知識,但不影響這裏讀者對特徵向量中心性思想的理解,不再贅述。

Betweenness centrality:中介中心性
如果一個節點位於很多條其他節點的最短路徑上,那麼改節點比較重要,定義如下:
c_{v}=\sum_{s\not=v\not=t}{\frac{\#(shortes\,paths \,betwen \,`𝑠 `\, and \, `𝑡` \, that \,contain \,`𝑣`)}{\#(shortest \,paths \,between \,'𝑠' \,and \,'𝑡')}}
我們可以看一個下面的例子:

假設我們要計算D的中介中心性:

  • 首先,我們計算節點D之外,所有節點對之間的最短路徑有多少條,這裏是1條(在5個節點中選擇兩個節點即節點對的個數)。
  • 然後,我們再看所有這些最短路徑中有多少條經過節點D,例如節點A要想找到節點E,必須經過節點D。經過節點D的最短路徑有3條(A-C-D-E,B-D-E,C-D-E)。
  • 最後,我們用經過節點D的最短路徑除以所有節點對的最短路徑總數,這個比率就是節點D的中介中心性。節點D的中介中心性是3/1=3。

4 PyTorch Geometric教程

官方文檔:https://pytorch-geometric.readthedocs.io/en/latest/index.html

PyTorch Geometric(PyG)是PyTorch的擴展庫。 它提供了有用的框架來開發圖深度學習模型,包括各種圖神經網絡層和大量基準數據集。

如果我們暫時不瞭解某些概念,例如“ GCNConv”,請不要擔心,之後我們將在以後的文章中介紹這些概念。

這個教程來自: https://colab.research.google.com/drive/1h3-vJGRVloF5zStxL5I0rSy4ZUPNsjy8?usp=sharing#scrollTo=ci-LpZWhRJoI by Matthias Fey

大家可以參照colab教程直接運行

4.1 PyG安裝

  • 查看當前torch和cuda版本
!python -c "import torch; print(torch.__version__)"

輸出:1.6.0

!python -c "import torch; print(torch.version.cuda)"

輸出:10.1

  • 安裝gpu版本
# 安裝GPU版本
!pip install --no-index torch-scatter -f https://pytorch-geometric.com/whl/torch-1.7.0+cu101.html
!pip install --no-index torch-sparse -f https://pytorch-geometric.com/whl/torch-1.7.0+cu101.html
!pip install --no-index torch-cluster -f https://pytorch-geometric.com/whl/torch-1.7.0+cu101.html
!pip install --no-index torch-spline-conv -f https://pytorch-geometric.com/whl/torch-1.7.0+cu101.html
!pip install torch-geometric

可能安裝gpu版本比較麻煩,大家可以多參照下官網進行安裝:
https://pytorch-geometric.readthedocs.io/en/latest/notes/installation.html

  • 安裝cuda版本
!pip install torch-scatter==latest+cpu -f https://pytorch-geometric.com/whl/torch-1.6.0.html
!pip install torch-sparse==latest+cpu -f https://pytorch-geometric.com/whl/torch-1.6.0.html
!pip install torch-cluster==latest+cpu -f https://pytorch-geometric.com/whl/torch-1.6.0.html
!pip install torch-spline-conv==latest+cpu -f https://pytorch-geometric.com/whl/torch-1.6.0.html
!pip install torch-geometric

4.2 PyG數據集

PyTorch Geometric 可以通過 torch_geometric.datasets 快速的獲取默認的數據集:

https://pytorch-geometric.readthedocs.io/en/latest/modules/datasets.html

一共有20+種數據,比較豐富~

# 導入空手道俱樂部數據集
from torch_geometric.datasets import KarateClub
dataset = KarateClub()
print(f'Dataset: {dataset}:')
print('======================')
print(f'Number of graphs: {len(dataset)}')
print(f'Number of features: {dataset.num_features}')
print(f'Number of classes: {dataset.num_classes}')

輸出:

Dataset: KarateClub():
======================
Number of graphs: 1
Number of features: 34
Number of classes: 4

初始化[KarateClub](https://pytorch-geometric.readthedocs.io/en/latest/modules/datasets.html#torch_geometric.datasets.KarateClub) 數據集之後,我們首先可以檢查其某些屬性。
例如,我們可以看到該數據集恰好有一個Graph,並且該數據集中的每個節點都被分配了** 34維特徵向量(唯一地描述了空手道俱樂部的成員)。
此外,該圖正好包含
4個類別**,代表每個節點所屬的社區。

現在讓我們更詳細地看一下這個Graph:

data = dataset[0]  # 獲取graph對象.

print(data)
print('==============================================================')

# 獲取一些graph的統計信息.
print(f'Number of nodes: {data.num_nodes}') # 節點的個數
print(f'Number of edges: {data.num_edges}') # 邊的個屬於
print(f'Average node degree: {data.num_edges / data.num_nodes:.2f}') # 節點的度平均數
print(f'Number of training nodes: {data.train_mask.sum()}')
print(f'Training node label rate: {int(data.train_mask.sum()) / data.num_nodes:.2f}')
print(f'Contains isolated nodes: {data.contains_isolated_nodes()}')
print(f'Contains self-loops: {data.contains_self_loops()}')
print(f'Is undirected: {data.is_undirected()}')

輸出如下:


Data(edge_index=[2, 156], train_mask=[34], x=[34, 34], y=[34])
==============================================================
Number of nodes: 34
Number of edges: 156
Average node degree: 4.59
Number of training nodes: 4
Training node label rate: 0.12
Contains isolated nodes: False
Contains self-loops: False
Is undirected: True

通過上面的信息我們可以發現,這個KarateClub網絡圖一共有34個節點,邊的個數爲1,每個節點度的平均數有4.59,是一個無向圖等等。

4.3 Data對象

PyTorch Geometric中的每個圖形都由單個[Data](https://pytorch-geometric.readthedocs.io/en/latest/modules/data.html#torch_geometric.data.Data)對象表示,該對象包含所有 描述其圖形表示的信息。
我們可以隨時通過print(data)打印數據對象,以接收有關其屬性及其形狀的簡短介紹:

Data(edge_index=[2, 156], x=[34, 34], y=[34], train_mask=[34])

我們可以看到這個data對象擁有4個屬性:
(1)“ edge_index”屬性保存有關“圖形連接性”(即)的信息,即每個邊緣的源節點和目標節點索引的元組。
PyG進一步將
(2)節點特徵
稱爲x(向34個節點中的每個節點分配了34維度的特徵向量),將
(3)節點標記
*稱爲y(每個節點僅分配給一個類別)。
(4)還有一個名爲“ train_mask”的附加屬性,它描述了我們已經知道其社區歸屬的節點。
總體而言,我們只知道4個節點的基本標籤(每個社區一個),任務是推斷其餘節點的社區分配。

數據對象還提供了一些“實用功能”來推斷基礎圖的一些基本屬性。
例如,我們可以輕鬆推斷圖中是否存在孤立的節點(* ie 任何節點都沒有邊),圖是否包含自環(i.e., (v, v) \in \mathcal{E}),或者圖形是否是無向的( (i.e.*, for each edge (v, w) \in \mathcal{E} there also exists the edge (w, v) \in \mathcal{E}).

from IPython.display import Javascript  # Restrict height of output cell.
display(Javascript('''google.colab.output.setIframeHeight(0, true, {maxHeight: 300})'''))

edge_index = data.edge_index
# 打印節點
print(edge_index.t())

輸出:

tensor([[ 0,  1],
        [ 0,  2],
        [ 0,  3],
        [ 0,  4],
        [ 0,  5],
        [ 0,  6],
        [ 0,  7],
        [ 0,  8],
        [ 0, 10],
        ....
        [33, 31],
        [33, 32]])

通過打印“ edge_index”,我們可以進一步瞭解PyG如何在內部表示圖形連接。
我們可以看到,對於每個邊緣,“ edge_index”都包含兩個節點索引的元組,其中第一個值描述源節點的節點索引,第二個值描述邊緣目標節點的節點索引。

這種表示稱爲** COO格式(座標格式)**,通常用於表示稀疏矩陣。
代替以密集表示形式保存鄰接信息 \mathbf{A} \in \{ 0, 1 \}^{|\mathcal{V}| \times |\mathcal{V}|},,PyG稀疏地表示圖形,這是指僅保存 \mathbf{A} 中的條目爲非零的座標/值。

最後,我們可以通過將圖形轉換爲networkx庫格式來進一步可視化圖形,該格式除了圖形操作功能之外,還實現了強大的可視化工具。我們創建一個專門用於展示Graph的函數

%matplotlib inline
import torch
import networkx as nx
import matplotlib.pyplot as plt

# Visualization function for NX graph or PyTorch tensor
def visualize(h, color, epoch=None, loss=None):
    plt.figure(figsize=(7,7))
    plt.xticks([])
    plt.yticks([])

    if torch.is_tensor(h):
        h = h.detach().cpu().numpy()
        plt.scatter(h[:, 0], h[:, 1], s=140, c=color, cmap="Set2")
        if epoch is not None and loss is not None:
            plt.xlabel(f'Epoch: {epoch}, Loss: {loss.item():.4f}', fontsize=16)
    else:
        nx.draw_networkx(G, pos=nx.spring_layout(G, seed=42), with_labels=False,
                         node_color=color, cmap="Set2")
    plt.show()
from torch_geometric.utils import to_networkx

G = to_networkx(data, to_undirected=True)
visualize(G, color=data.y)

4.4 實戰:基於GCN的Graph節點分類

接下來將通過Pytorch實現一個最基本的GCN網絡用語節點分類,基於帶有標籤的節點數據進行訓練模型,然後預測未帶有標籤的數據

在瞭解了PyG的數據處理之後,是時候實現我們的第一個Graph神經網絡了!我們在這裏將使用最簡單的GNN網絡之一,即GCN層**([Kipf等人(2017)](https://arxiv.org/abs/1609.02907))用於Graph節點分類。

PyG通過[GCNConv](https://pytorch-geometric.readthedocs.io/en/latest/modules/nn.html#torch_geometric.nn.conv.GCNConv)來實現此層,可以通過傳入 節點要素表示形式“ x”和COO圖形連接性表示形式“ edge_index”。

這樣,我們就可以通過在torch.nn.Module類中定義我們的網絡架構來創建我們的第一個圖形神經網絡:

import torch
from torch.nn import Linear
from torch_geometric.nn import GCNConv


class GCN(torch.nn.Module):
    def __init__(self):
        super(GCN, self).__init__()
        torch.manual_seed(12345)
        self.conv1 = GCNConv(dataset.num_features, 4)
        self.conv2 = GCNConv(4, 4)
        self.conv3 = GCNConv(4, 2)
        self.classifier = Linear(2, dataset.num_classes)

    def forward(self, x, edge_index):
        h = self.conv1(x, edge_index)
        h = h.tanh()
        h = self.conv2(h, edge_index)
        h = h.tanh()
        h = self.conv3(h, edge_index)
        h = h.tanh()  # Final GNN embedding space.
        
        # Apply a final (linear) classifier.
        out = self.classifier(h)

        return out, h

model = GCN()
print(model)

網絡結構輸出如下:

GCN(
  (conv1): GCNConv(34, 4)
  (conv2): GCNConv(4, 4)
  (conv3): GCNConv(4, 2)
  (classifier): Linear(in_features=2, out_features=4, bias=True)
)

在這裏,我們首先在__init__中初始化所有構建塊,並在“轉發”中定義網絡的計算流程。
我們首先定義並堆疊三層圖卷積層,這對應於在每個節點周圍(距離3個“跳”爲止的所有節點)彙總3跳鄰域信息。
另外,GCNConv層將節點特徵維數減小爲2, i.e., 34 \rightarrow 4 \rightarrow 4 \rightarrow 2.。每個[GCNConv]層都通過[tanh](https://pytorch.org/docs/stable/generation/torch.nn.Tanh.html?highlight=tanh#torch.nn.Tanh)非線性進行了增強。

之後,我們應用單個線性變換([torch.nn.Linear](https://pytorch.org/docs/stable/generated/torch.nn.Linear.html?highlight=linear#torch.nn。線性)),用作將我們的節點映射到4個類/社區中的1個的分類器。

我們返回最終分類器的輸出以及GNN生成的最終節點嵌入。
我們繼續通過GCN()初始化最終模型,並打印我們的模型以產生所有使用過的子模塊的概括。

獲取隱藏層的表示:

model = GCN()

_, h = model(data.x, data.edge_index)
print(f'Embedding shape: {list(h.shape)}')

visualize(h, color=data.y)

輸出:

Embedding shape: [34, 2]

在這裏值得注意的是,即使在訓練我們的模型權重之前,這個初始化的GCN網絡也會產生節點的嵌入,並且這些嵌入與圖的社區結構非常相似。儘管我們的模型的權重是“完全隨機地初始化”的,並且到目前爲止,我們還沒有進行任何訓練,但是相同顏色(社區)的節點已經在嵌入空間中緊密地聚集在一起。得出這樣的結論,即GNN引入了強烈的節點偏差,從而導致在輸入圖中彼此靠近的節點具有相似的嵌入

但是,節點表示並不是很完美,我們可以看出來四種類別的節點還是混在一塊了(對應途中是四種顏色),我們可以做得更好嗎?
讓我們看一個示例,該示例如何基於對圖中4四種節點的社區分配(每個社區一個)的知識來訓練我們的網絡參數:

由於模型中的所有內容都是可區分的和參數化的,因此我們可以添加一些標籤,訓練模型並觀察嵌入如何反應。
在這裏,我們使用一種半監督學習程序:我們僅針對每個類訓練一個節點,但是允許使用完整的輸入圖數據。

這個怎麼理解呢,我們其實可以輸出data.ydata.train_mask就明白了:

data.y
tensor([0, 0, 0, 0, 1, 1, 1, 0, 2, 2, 1, 0, 0, 0, 2, 2, 1, 0, 2, 0, 2, 0, 2, 2,
        3, 3, 2, 2, 3, 2, 2, 3, 2, 2])
data.train_mask
tensor([ True, False, False, False,  True, False, False, False,  True, False,
        False, False, False, False, False, False, False, False, False, False,
        False, False, False, False,  True, False, False, False, False, False,
        False, False, False, False])

大家可以看到相當於我們訓練網絡的時候只針對每一個類別下的token做優化,這樣可以加速網絡的訓練和收斂。

訓練我們的模型與任何其他PyTorch模型非常相似。
除了定義我們的網絡架構之外,我們還定義了損失函數在這裏[[CrossEntropyLoss]](https://pytorch.org/docs/stable/generate/torch.nn.CrossEntropyLoss.html)) 並初始化隨機梯度優化器(此處爲[Adam](https://pytorch.org/docs/stable/optim.html?highlight=adam#torch.optim.Adam))。
之後,我們執行多輪優化,其中每一輪都包含一個正向和反向傳遞,以計算模型參數w.r.t的梯度。從前向通行證產生的損失。
如果您對PyTorch並不陌生,則該方案應該對您來說很熟悉。
否則,PyTorch文檔會提供[有關如何在PyTorch中訓練神經網絡的很好的介紹](https://pytorch.org/tutorials/beginner/blitz/cifar10_tutorial.html#define-a-loss-function-and-optimizer )。

請注意,我們的半監督學習場景是通過以下行實現的:

loss = criterion(out[data.train_mask], data.y[data.train_mask])

在計算所有節點的節點嵌入時,我們“僅利用訓練節點來計算loss” **。
在這裏,這是通過過濾分類器“ out”和真實性標籤“ data.y”的輸出以僅包含“ train_mask”中的節點來實現的。

現在讓我們開始訓練,看看我們的節點嵌入隨時間如何演變(最好是顯式地運行代碼,打印出模型的訓練效果):

import time
from IPython.display import Javascript  #畫圖.
display(Javascript('''google.colab.output.setIframeHeight(0, true, {maxHeight: 430})'''))

model = GCN()
criterion = torch.nn.CrossEntropyLoss()  # 定義loss
optimizer = torch.optim.Adam(model.parameters(), lr=0.01)  # 優化器

def train(data):
    optimizer.zero_grad()  # 梯度清零.
    out, h = model(data.x, data.edge_index)  # GCN模型.
    loss = criterion(out[data.train_mask], data.y[data.train_mask])  # 計算真實loss.
    loss.backward()  # 反向求導.
    optimizer.step()  # 參數更新.
    return loss, h

for epoch in range(401):
    loss, h = train(data)
    # 每10個epochs打印Graph
    if epoch % 10 == 0:
        visualize(h, color=data.y, epoch=epoch, loss=loss)
        time.sleep(0.3)

過了一段時間之後,我們可以看到GCN強大的效果:

可以看到,我們的3層GCN模型可以有效地社區發現並正確地對大多數節點進行分類。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章