重磅好文透徹理解,異構圖上 Node 分類理論與DGL原始碼實戰

語言: CN / TW / HK

重磅好文透徹理解,異構圖上 Node 分類理論與DGL原始碼實戰


書接上文,關注過作者歷史文章的讀者都知道,圖上機器學習/深度學習系列文章一文揭開圖機器學習的面紗,你確定不來看看嗎 開始,已經陸續和大家一起了解了 同構圖上的連結預測、節點分類與迴歸、邊分類與迴歸 等機器學習任務,不熟悉的同學可以去作者的歷史文章裡查詢哦。

如上所說,以前介紹 圖上機器學習任務 的文章, 均是在 同構圖 上進行的,忽略了圖上不同節點以及不同邊的獨特性質,而是把所有節點當作一種節點來看待的。這個雖然可以解決一部分問題,但是該關係建模能力也不足以覆蓋現實世界 中複雜多變的多種關係,所以就輪到我們的 異構圖關係建模 文章出馬了。

針對 異構圖 上關係的建模,因為其 工程實現的複雜性 ,目前的學術界和工業界均存在一定的 實現難度 。我知道的甚至很多圖深度學習框架在最新的版本里還 不支援 對異構圖的建模。好在亞馬遜的DGL框架在最新的幾個版本中,已經更新了對異構圖的工程實現,下面就讓我們結合DGL的實現原始碼來一起了解下 異構圖上節點分類/迴歸任務 吧 ~ go go go !!!

注意:我們的文章裡,把分類迴歸任務一起囊括了因為這來那個任務除了 輸入和損失不同 以外,網路結構並沒有別的不同,分類迴歸任務彼此修改互用也比較容易,這裡就不再進行區分了。本文說是節點分類任務,但是其實迴歸任務也差不太多。


(1) 異構圖節點分類任務理論基礎

按照慣例,我們還是先從基礎定義引出下文的話題。

在以前的文章 一文揭開圖機器學習的面紗,你確定不來看看嗎 中,我們說圖的分類的時候說到了異構圖,文中說:圖中節點型別和邊型別超過兩種的圖稱為異構圖。這意思就是說異構圖中的節點和同2個節點的邊可能有多種,例如:圖中包括使用者,商品,IP三種類型的節點,其中使用者和商品之間又有加購物車與購買這兩種關係的邊。本文所說的圖就是這種型別的 比較複雜 的圖。

同構圖推廣 來看,既然在異構圖中區分了 節點和邊 的不同型別,那我們在處理根據 異構圖的區域性與全域性結構特性 對某個節點進行 定性分析 或則 進行兩個節點之間 關係預測 的時候,就需要從 更細粒度 上去對不同的節點和邊的關係進行 區分 。既然2個節點的某一種關係決定了一種型別的邊,一種比較好的方式是: 根據關係(邊)型別去組織不同型別的節點 ,然後進行異構圖卷積操作,得到對各個型別的節點的 Embeding,在基於此最終完成 異構圖上的機器學習任務 ,就像DGL官方原始碼實現的那樣。

所謂 異構圖卷積,顧名思義: 就是對 各種邊的關係各自分別進行卷積 ,然後將這些關係對應的各種型別的同類型節點進行融合,預設是Sum , 得到各種同類型節點的Embeding, 注意這裡每種型別節點只有一個Embeding。 對於 節點分類 任務,最後在異構圖卷積層結束的時候,可以直接接啟用函式,然後分別對每種型別的節點計算出一個Logit, 和有監督的某種型別的 label 計算損失進行回傳即可。感興趣的同學,可以看 DGL實現的RGCB節點分類任務的原始碼驗證明晰 以上所說的邏輯。

這裡需要特別強調注意 的是: 在異構圖RGCN取樣的時候,取樣了幾層鄰居節點,異構圖卷積層就有幾層異構卷積layer, 分別有每個異構卷積layer去處理每一層的鄰居節點

因為取樣是由內向外取樣的,而聚合是由外向內聚合的。這裡要引入DGL實現取樣得到的Block的概念,通俗理解 Block其實就是取樣得到的子圖,而這些子圖裡的邊也有對應這開始節點和結束節點以及邊型別等和 全Graph同等 的一些屬性

我們可以這樣理解DGL實現的Block可以把看作一個數組,數組裡的每一個元素是圖上一層鄰居的取樣,Block內部節點是 從遠到近的順序排列內部的Block的,Block陣列的下標從小到大對應著取樣範圍由外到內、覆蓋範圍由遠及近,並且 blocks[i+1]的 source node 和 blocks[i]的target node是可以對應上的。我們知道鄰居節點取樣其實是按照邊的關係去採來確認鄰居的,所以在DGL的取樣過程中,讓 blocks[0]的 src node 包含了 blocks[0]的所有dst node,並且dst 節點出現在src 節點序列的前面若干位置

所以我們在程式碼實現的時候,將 外層對應節點的Embeding作為內層節點的輸入,構成兩個互相挨著的卷積層 ,這裡取樣與工程實現是 完美互相契合 的。有疑問的同學,可以去看原始碼驗證哦 ~

好吧,整體對異構圖的節點分類任務 抽象 一下: 既然我們要對異構圖上某節點進行分類,那我們就需要綜合異構圖上該節點鄰居節點的資訊,得出所求節點的Embeding 資訊。 而該節點周圍有多種類別關係的節點,則我們就對各個關係分別進行卷積,求得各個關係裡面各個節點的Embeding, 然後將多種關係涵蓋的多類同類節點 Embeding進行聚合,後面可以接全連結層,也可以不接全連結層直接接啟用函式,得到各個節點型別的結果作為輸出。對於異構圖,最終 節點分類任務的 Logit 也是 按照節點類別的個數有多個

當然針對異構圖,我們可以採用 GraphSage還是HAN ?吐血力作綜述Graph Embeding 經典好文 文章後半部分裡介紹的,使用 MetaPath 結合 Attention 進行 Node 節點級別 與 path語義級別的融合,類似於 HAN 的處理方式。但是 萬丈高樓平地起 ,寫程式碼和寫文章,也得慢慢來一點一點兒實現不是~

異構圖RGCN節點分類任務 整體的流程解析就到這裡吧,感覺這個地方,還是得看原始碼才能說清楚。因為整個原始碼流程比較長,也為了讓最後整個程式碼demo能夠完美的執行起來,本篇文章的程式碼將從 講述一個工程的實現 開始。

所以,本文 就讓我們一起實現 基於DGL和異構圖的RGCN來進行節點分類迴歸任務 。下面就讓我們開始 coding 吧 ~


(2) 程式碼時光

開篇先吼一嗓子 , talk is cheap , show me the code !!!

本文的程式碼講的是 基於DGL和RGCN實現的異構圖上節點分類任務,整個原始碼流程是一個 小型的工業可用的工程,基於dgl實現,覺得有用趕緊收藏轉發吧~

life is short , i use python !!!

(2.1) 資料準備

我們假設可以輸入類似於這樣的資料, 其中每2列對應這一種關係,例如 使用者2352193 購買了商品CEEC9EBF7,使用者用了IP 174.74.201.9登入了賬號,使用者用IP 174.74.201.9 購買了商品 CEEC9EBF7, label 表示著該使用者真的購買商品,最終的節點分類任務是預測使用者的購買意願,是否是我們的高意圖潛在使用者,二分類。

我們可以把這樣一份資料存入 source_data.csv 檔案中,用 pandas 介面把資料讀入: raw_pdf = pd.read_csv('./source_data.csv')

因為對於 異構圖 模型,節點和邊的型別均有多種,為了處理方便,我們可以把各種型別的節點進行編碼,再到後期對其進行解碼,對 pandas 的 dataframe 資料結構的編解碼,我們可以使用下面的程式碼:

``` @ 歡迎關注微信公眾號:演算法全棧之路

編碼方法

def encode_map(input_array):     p_map={}     length=len(input_array)     for index, ele in zip(range(length),input_array):         # print(ele,index)         p_map[str(ele)] = index     return p_map

解碼方法

def decode_map(encode_map):     de_map={}     for k,v in encode_map.items():         # index,ele          de_map[v]=k     return de_map ```

然後用其中的各列node 進行 編碼

``` @ 歡迎關注微信公眾號:演算法全棧之路

userid_encode_map=encode_map(set(graph_features_pdf['user_id'].values))

解碼map

userid_decode_map=decode_map(userid_encode_map) graph_features_pdf['user_id_encoded'] = graph_features_pdf['user_id'].apply(lambda e: userid_encode_map.get(str(e),-1))

print unique值的個數

userid_count=len(set(graph_features_pdf['user_id_encoded'].values)) print(userid_count) ```

這裡僅僅以 使用者節點編碼 為例,itemId和 IP同理編解碼即可。 最後我們可以把圖資料儲存,供以後的異構圖程式碼 demo使用。

``` @ 歡迎關注微信公眾號:演算法全棧之路

final_graph_pdf=graph_features_pdf[['user_id_encoded','ip_encoded','item_id_encoded','label']].sort_values(by='user_id_encoded', ascending=True) final_graph_pdf.to_csv('result_label.csv',index=False) ```

基於此,異構圖的基礎準備資料就結束了,下面開始正式的coding了。


(2.2) 導包

老規矩,先導包,基於DGL和RGCN實現的異構圖上節點分類任務只需要這些包就可以了。

``` @ 歡迎關注微信公眾號:演算法全棧之路

import argparse import torch import torch.nn as nn import dgl import torch.optim as optim from dgl.dataloading import MultiLayerFullNeighborSampler, EdgeDataLoader from dgl.dataloading.negative_sampler import Uniform import numpy as np import pandas as pd import itertools import os import tqdm from dgl import save_graphs, load_graphs import dgl.function as fn import torch import dgl import torch.nn.functional as F from dgl.nn.pytorch import GraphConv, SAGEConv, HeteroGraphConv from dgl.utils import expand_as_pair import tqdm from collections import defaultdict import torch as th import dgl.nn as dglnn from dgl.data.utils import makedirs, save_info, load_info from sklearn.metrics import roc_auc_score import gc gc.collect() ```

推薦一個工具,tqdm 很好用 哦,結合 dataloading介面 , 可以看到模型訓練以及資料處理執行的進度,趕緊用起來吧~

這裡的 sklearn 工具 的匯入,僅僅是為了呼叫他來進行分類模型的離線指標評估,得到AUC等指標而已。

各種模型工具無所謂分類,能解決問題的就是好工具,混用又有何不可呢? 實用就行


(2.3) 構圖

資料有了,接下來就是構圖了,我們構建的是包含 三種節點的異構圖

``` @ 歡迎關注微信公眾號:演算法全棧之路

user 登入 ip

u_e_ip_src = final_graph_pdf['user_id_encoded'].values u_e_ip_dst = final_graph_pdf['ip_encoded'].values

user 購買 item

u_e_item_src = final_graph_pdf['user_id_encoded'].values u_e_item_dst = final_graph_pdf['item_id_encoded'].values

item和ip 共同出現

ip_e_item_src = final_graph_pdf['ip_encoded'].values ip_e_item_dst = final_graph_pdf['item_id_encoded'].values

user 購買 label

user_node_buy_label = final_graph_pdf['label'].values

hetero_graph = dgl.heterograph({     ('user', 'u_e_ip', 'ip'): (u_e_ip_src, u_e_ip_dst),     ('ip', 'u_eby_ip', 'user'): (u_e_ip_dst, u_e_ip_src),     ('user', 'u_e_item', 'item'): (u_e_item_src, u_e_item_dst),     ('item', 'u_eby_item', 'user'): (u_e_item_dst, u_e_item_src),     ('ip', 'ip_e_item', 'item'): (ip_e_item_src, ip_e_item_dst),     ('item', 'item_eby_ip', 'ip'): (ip_e_item_dst, ip_e_item_src) })

給 user node 新增標籤

hetero_graph.nodes['user'].data['label'] = torch.tensor(user_node_buy_label) print(hetero_graph) ```

這裡的 異構圖是 無向圖 ,因為無向,所以雙向。 構圖的時候就需要構建 雙向的邊。 程式碼很好理解,就不再贅述了哈。


(2.4) 模型的自定義函式

這裡定義了 異構圖上RGCN 會用到的模型的一系列自定義函式,綜合看程式碼註釋,結合上文第一小節的抽象理解,希望能理解的更加深入哦。

``` @ 歡迎關注微信公眾號:演算法全棧之路

class RelGraphConvLayer(nn.Module):

def init(self,                  in_feat,                  out_feat,                  rel_names,                  num_bases,                  *,                  weight=True,                  bias=True,                  activation=None,                  self_loop=False,                  dropout=0.0):         super(RelGraphConvLayer, self).init()         self.in_feat = in_feat         self.out_feat = out_feat         self.rel_names = rel_names         self.num_bases = num_bases         self.bias = bias         self.activation = activation         self.self_loop = self_loop

# 這個地方只是起到計算的作用, 不儲存資料         self.conv = HeteroGraphConv({             # graph conv 裡面有模型引數weight,如果外邊不傳進去的話,裡面新建             # 相當於模型加了一層全連結, 對每一種型別的邊計算卷積             rel: GraphConv(in_feat, out_feat, norm='right', weight=False, bias=False)             for rel in rel_names         })

self.use_weight = weight         self.use_basis = num_bases < len(self.rel_names) and weight         if self.use_weight:             if self.use_basis:                 self.basis = dglnn.WeightBasis((in_feat, out_feat), num_bases, len(self.rel_names))             else:                 # 每個關係,又一個weight,全連線層                 self.weight = nn.Parameter(th.Tensor(len(self.rel_names), in_feat, out_feat))                 nn.init.xavier_uniform_(self.weight, gain=nn.init.calculate_gain('relu'))

# bias         if bias:             self.h_bias = nn.Parameter(th.Tensor(out_feat))             nn.init.zeros_(self.h_bias)

# weight for self loop         if self.self_loop:             self.loop_weight = nn.Parameter(th.Tensor(in_feat, out_feat))             nn.init.xavier_uniform_(self.loop_weight,                                     gain=nn.init.calculate_gain('relu'))

self.dropout = nn.Dropout(dropout)

def forward(self, g, inputs):                  g = g.local_var()         if self.use_weight:             weight = self.basis() if self.use_basis else self.weight             # 這每個關係對應一個權重矩陣對應輸入維度和輸出維度             wdict = {self.rel_names[i]: {'weight': w.squeeze(0)}                      for i, w in enumerate(th.split(weight, 1, dim=0))}         else:             wdict = {}

if g.is_block:             inputs_src = inputs             inputs_dst = {k: v[:g.number_of_dst_nodes(k)] for k, v in inputs.items()}         else:             inputs_src = inputs_dst = inputs

# 多型別的邊結點卷積完成後的輸出         # 輸入的是blocks 和 embeding         hs = self.conv(g, inputs, mod_kwargs=wdict)

def _apply(ntype, h):             if self.self_loop:                 h = h + th.matmul(inputs_dst[ntype], self.loop_weight)             if self.bias:                 h = h + self.h_bias             if self.activation:                 h = self.activation(h)             return self.dropout(h)

#         return {ntype: _apply(ntype, h) for ntype, h in hs.items()}

class RelGraphEmbed(nn.Module):     r"""Embedding layer for featureless heterograph."""

def init(self,                  g,                  embed_size,                  embed_name='embed',                  activation=None,                  dropout=0.0):         super(RelGraphEmbed, self).init()         self.g = g         self.embed_size = embed_size         self.embed_name = embed_name         self.activation = activation         self.dropout = nn.Dropout(dropout)

# create weight embeddings for each node for each relation         self.embeds = nn.ParameterDict()         for ntype in g.ntypes:             embed = nn.Parameter(torch.Tensor(g.number_of_nodes(ntype), self.embed_size))             nn.init.xavier_uniform_(embed, gain=nn.init.calculate_gain('relu'))             self.embeds[ntype] = embed

def forward(self, block=None):                  return self.embeds

class EntityClassify(nn.Module):     def init(self,                  g,                  h_dim, out_dim,                  num_bases=-1,                  num_hidden_layers=1,                  dropout=0,                  use_self_loop=False):         super(EntityClassify, self).init()         self.g = g         self.h_dim = h_dim         self.out_dim = out_dim         self.rel_names = list(set(g.etypes))         self.rel_names.sort()         if num_bases < 0 or num_bases > len(self.rel_names):             self.num_bases = len(self.rel_names)         else:             self.num_bases = num_bases         self.num_hidden_layers = num_hidden_layers         self.dropout = dropout         self.use_self_loop = use_self_loop

self.embed_layer = RelGraphEmbed(g, self.h_dim)         self.layers = nn.ModuleList()         # i2h         self.layers.append(RelGraphConvLayer(             self.h_dim, self.h_dim, self.rel_names,             self.num_bases, activation=F.relu, self_loop=self.use_self_loop,             dropout=self.dropout, weight=False))

# h2h , 這裡不新增隱層,只用2層卷積         # for i in range(self.num_hidden_layers):         #    self.layers.append(RelGraphConvLayer(         #        self.h_dim, self.h_dim, self.rel_names,         #        self.num_bases, activation=F.relu, self_loop=self.use_self_loop,         #        dropout=self.dropout))         # h2o

self.layers.append(RelGraphConvLayer(             self.h_dim, self.out_dim, self.rel_names,             self.num_bases, activation=None,             self_loop=self.use_self_loop))

# 輸入 blocks,embeding     def forward(self, h=None, blocks=None):         if h is None:             # full graph training             h = self.embed_layer()         if blocks is None:             # full graph training             for layer in self.layers:                 h = layer(self.g, h)         else:             # minibatch training             # 輸入 blocks,embeding             for layer, block in zip(self.layers, blocks):                 h = layer(block, h)         return h

def inference(self, g, batch_size, device="cpu", num_workers=0, x=None):

if x is None:             x = self.embed_layer()

for l, layer in enumerate(self.layers):             y = {                 k: th.zeros(                     g.number_of_nodes(k),                     self.h_dim if l != len(self.layers) - 1 else self.out_dim)                 for k in g.ntypes}

sampler = dgl.dataloading.MultiLayerFullNeighborSampler(1)             dataloader = dgl.dataloading.NodeDataLoader(                 g,                 {k: th.arange(g.number_of_nodes(k)) for k in g.ntypes},                 sampler,                 batch_size=batch_size,                 shuffle=True,                 drop_last=False,                 num_workers=num_workers)                          for input_nodes, output_nodes, blocks in tqdm.tqdm(dataloader):                 # print(input_nodes)                 block = blocks[0].to(device)                                          h = {k: x[k][input_nodes[k]].to(device) for k in input_nodes.keys()}                 h = layer(block, h)

for k in h.keys():                     y[k][output_nodes[k]] = h[k].cpu()

x = y         return y ```

上面的程式碼主要分為三大塊:分別是 RelGraphConvLayerRelGraphEmbed 以及 EntityClassify

首先就是:RelGraphConvLayer 。我們可以看到 RelGraphConvLayer 就是我們的 異構圖卷積層layer , 其主要是呼叫了DGL實現的 HeteroGraphConv運算元,從上面第一小節我們也詳細闡述了異構圖卷積運算元其實就是: 對各種關係分別進行卷積然後進行同型別的節點的融合

這裡我們需要重點關注的是:RelGraphConvLayer層的返回,從程式碼中,我們可以看到,對於每種節點型別是返回了一個Embeding, 維度是 out_feat。如果是帶了啟用函式的,則是返回啟用後的一定維度的一個tensor。

過來是 RelGraphEmbed。 從程式碼中可以看到: 這個python類僅僅返回了一個字典,但是這個字典裡卻包括了 多個 Embeding Variable, 注意這裡的 Variable 均是可以 隨著網路訓練變化更新 的。我們可以根據節點型別,節點ID取得對應元素的 Embeding 。 這種實現方法是不是解決了 前文 GraphSage與DGL實現同構圖 Link 預測,通俗易懂好文強推基於GCN和DGL實現的圖上 node 分類, 值得一看!!! 所提到的 動態更新的Embeding 的問題呢。

最後就是 EntityClassify類 了,我們可以看到 這個就是最終的 模型RGCN結構 了,包括了 模型訓練的 forward 和用於推斷的inference方法

。這裡的 inference 可以用於 各個節點的embedding的匯出, 我們在後文有例項程式碼,接著看下去吧~

注意看 forword 方法裡 的 for layer, block in zip(self.layers, blocks) 這個位置, 這裡就是我們前一小節所說的 取樣層數和模型的卷積層數目是相同的說法的由來,可以結合上文說明理解原始碼哦。


(2.5) 模型取樣超參與節點取樣介紹

先上程式碼。

``` @ 歡迎關注微信公眾號:演算法全棧之路

根據節點型別和節點ID抽取embeding 參與模型訓練更新

def extract_embed(node_embed, input_nodes):     emb = {}     for ntype, nid in input_nodes.items():         nid = input_nodes[ntype]         emb[ntype] = node_embed[ntype][nid]     return emb

取樣定義,有監督取樣和無監督取樣不一樣

batch_size = 20480 neg_sample_count = 1

取樣2層全部節點

sampler = MultiLayerFullNeighborSampler(2)

使用者節點取樣,這裡是對使用者的所有鄰居取樣了2層節點

hetero_graph.nodes['user'].data['train_mask'] = torch.zeros(unique_userid_count, dtype=torch.bool).bernoulli(1.0) all_userid_idx = torch.nonzero(hetero_graph.nodes['user'].data['train_mask'], as_tuple=False).squeeze() user_loader = dgl.dataloading.NodeDataLoader(hetero_graph, {"user": train_userid_nodeids}, sampler,batch_size=batch_size, shuffle=True, num_workers=0)

訓練集和測試集split

train_count=(int)(len(all_userid_idx) * 0.9) print(train_count) train_userid_nodeids = all_userid_idx[:train_count] test_userid_nodeids = all_userid_idx[train_count:]

IP節點的鄰居取樣

hetero_graph.nodes['ip'].data['train_mask'] = torch.zeros(unique_ip_count, dtype=torch.bool).bernoulli(1.0) train_ip_nodeids = hetero_graph.nodes['ip'].data['train_mask'].nonzero(as_tuple=True)[0] ip_loader = dgl.dataloading.NodeDataLoader(hetero_graph, {"ip": train_ip_nodeids}, sampler,                                            batch_size=batch_size, shuffle=True, num_workers=0)

item 鄰居節點取樣

hetero_graph.nodes['item'].data['train_mask'] = torch.zeros(unique_ip_prefix_count, dtype=torch.bool).bernoulli(1.0) train_ipprefix_nodeids = hetero_graph.nodes['item'].data['train_mask'].nonzero(as_tuple=True)[0] ipprefix_loader = dgl.dataloading.NodeDataLoader(hetero_graph, {"item": train_ipprefix_nodeids}, sampler,batch_size=batch_size, shuffle=True, num_workers=0) ```

這裡的程式碼作者花了大量時間進行優化,註釋和組織形式 儘量寫的非常清晰,非常容易理解。

我們這裡選擇了 NodeDataLoader 來進行訓練資料的讀入,這其實是一種 分batch訓練 的方法,而 不是一次性把圖全讀入記憶體 進行訓練,而是每次選擇 batch的種子節點以及他們取樣的鄰居節點 讀入記憶體參與訓練,這也讓大的圖神經網路訓練成為了可能,是 DGL圖深度框架 非常優秀 的實現 !!! 大讚 !

需要 注意的是 : extract_embed 這個方法可以抽取出對應類別對應節點的 Embeding。 我們這裡用了 MultiLayerFullNeighborSampler 這個介面,對每個種子節點取樣了2層的全部鄰居參與訓練,中間因為是節點分類任務,這裡需要將該鄰居取樣運算元 和 dgl.dataloading.NodeDataLoader 結合使用。

NodeDataLoader 的第二個引數屬於一個字典,其中可以放多個 節點型別以及對應的種子nids , 這裡為了方便理解,把拆解成了多個 data_loader,來分別對多個型別的節點在圖上進行全部鄰居的取樣,這裡的 實現是等價 的。

作者親測,圖訓練的 batch_size 能選擇大盡可能大一些 吧,不然訓練模型會非常慢的~


(2.6) 模型訓練超參與單epoch訓練

``` @ 歡迎關注微信公眾號:演算法全棧之路

模型定義

num_class = 2 n_hetero_features = 16 labels = hetero_graph.nodes['user'].data['label']

hidden_feat_dim = n_hetero_features

embed_layer = RelGraphEmbed(hetero_graph, hidden_feat_dim) all_node_embed = embed_layer()

model = EntityClassify(hetero_graph, hidden_feat_dim, num_class)

優化模型所有引數,主要是weight以及輸入的embeding引數

all_params = itertools.chain(model.parameters(), embed_layer.parameters()) optimizer = torch.optim.Adam(all_params, lr=0.01, weight_decay=0)

def train_nodetype_one_epoch(ntype, spec_dataloader):     losses = []     # input_nodes 代表計算 output_nodes 的表示所需的節點,input_nodes包含了output_nodes。     # 塊 包含了每個GNN層要計算哪些節點表示作為輸出,要將哪些節點表示作為輸入,以及來自輸入節點的表示如何傳播到輸出節點。     for input_nodes, output_nodes, blocks in tqdm.tqdm(spec_dataloader):         emb = extract_embed(all_node_embed, input_nodes)         batch_tic = time.time()         seeds = output_nodes[ntype]         lbl = labels[seeds]  # 只取output_nodes部分結點參與訓練         logits = model(emb, blocks)[ntype]                  loss = F.cross_entropy(logits, lbl)         loss.backward()         optimizer.step()                   train_acc = torch.sum(logits.argmax(dim=1) == lbl).item() / len(seeds)                  print('AUC', roc_auc_score(lbl, logits.argmax(dim=1) ))         print("Epoch {:05d}  | Train Acc: {:.4f} | Train Loss: {:.4f} | Time: {:.4f}".               format(epoch, train_acc, loss.item(), time.time() - batch_tic)) ```

從上面的程式碼我們可以看到: 最終我們是進行了 2分類 ,中間的呼叫了上面模型定義類 EntityClassify 來定義 異構圖上RGCN的模型 結構,因為是分類問題,損失函式選擇了 交叉熵損失

需要注意的是: all_params = itertools.chain(model.parameters(), embed_layer.parameters())這一行程式碼,我們定義優化器的引數時,將我們自定義的 可隨網路更新的 Variable 加入了itertools.chain 參與模型的訓練。

另一個需要注意的點是: spec_dataloader 這個地方,它的返回是 input_nodes, output_nodes和 blocks 這三個元素的tuple 。 其中,input_nodes 代表計算 output_nodes 的表示所需的節點,input_nodes包含了output_nodes。塊 包含了每個GNN層要計算哪些節點表示作為輸出,要將哪些節點表示作為輸入,以及來自輸入節點的表示如何傳播到輸出節點

這就有了我們進行模型訓練所需要的圖上結構的全部資訊了。


(2.6) 模型多種節點訓練

``` @ 歡迎關注微信公眾號:演算法全棧之路

開始train 模型

for epoch in range(20):     print("start epoch:", epoch)     model.train()     train_nodetype_one_epoch('user', user_loader)     train_nodetype_one_epoch('user', user_loader)     train_nodetype_one_epoch('user', user_loader) ```

從程式碼中我們可以知道: 對於異構圖,其實我們也是以 各種型別的節點作為種子節點, 然後進行圖上的鄰居取樣,分別進行訓練然後更新整個模型結構 的。


(2.7) 模型儲存與節點Embeding匯出

``` @ 歡迎關注微信公眾號:演算法全棧之路

圖資料和模型儲存

save_graphs("graph.bin", [hetero_graph]) torch.save(model.state_dict(), "model.bin")

每個結點的embeding,自己初始化,因為參與了訓練,這個就是最後每個結點輸出的embeding

print("node_embed:", all_node_embed['user'][0])

模型預估的結果,最後應該使用 inference,這裡得到的是logit

注意,這裡傳入 all_node_embed,選擇0,選1可能會死鎖,最終程式不執行

inference_out = model.inference(hetero_graph, batch_size, 'cpu', num_workers=0, all_node_embed) print(inference_out["user"].shape) print(inference_out['user'][0]) ```

這裡我們可以看到, 我們使用了 model.inference 介面進行模型的節點 Embeding匯出。

這裡需要注意的是: 這個地方 num_workers應該設定0 ,即為不用多執行緒, 不然會互鎖,導致預估任務不執行。這裡是 深坑 啊,反正經過很長時間的糾結和查詢,最終發現是這個原因,希望讀者可以避免遇到相似的問題 ~

其實對於異構圖,要寫出對它的一些應用的理解,我也是怯生生的。但是,凡事必先騎上虎背 。管它呢,上吧,能寫到哪一步是哪一步吧! 歡迎關注作者並留言和我一起討論,彼此一起學習交流 ~

到這裡,重磅好文透徹理解, 異構圖上 Node 分類理論與DGL原始碼實戰 的全文就寫完了。上面的程式碼demo 在環境沒問題的情況下,全部複製到一個python檔案裡,就可以完美執行起來。本文的 程式碼是一個小型的商業可以用的工程專案,希望可以對你有參考作用 ~


碼字不易,覺得有收穫就動動小手轉載一下吧,你的支援是我寫下去的最大動力 ~

更多更全更新內容,歡迎關注作者的公眾號: 演算法全棧之路

  • END -