圖神經網路七日打卡營學習筆記及心得
學習來源百度AIstudio:https://aistudio.baidu.com/aistudio/index
Day-1 圖學習
Part1 什麼是圖
圖的兩個基本元素:點、邊
圖是一種統一描述複雜事物的語言
常見的圖:社交網路、推薦系統、化學分子結構…
Part2 什麼是圖學習
圖學習: Graph Learning。深度學習中的一個子領域,強調處理的資料物件為圖。
與一般深度學習的區別:能夠方便地處理不規則資料(樹、圖),同時也可以處理規則資料(如影像)。
Part3 圖學習的應用
我們可以把圖學習的應用分為節點級別任務、邊級別任務、圖級別任務。 課程中介紹了以下幾種任務。
節點級別任務:金融詐騙檢測(典型的節點分類)、自動駕駛中的3D點雲目標檢測
邊級別任務:推薦系統(典型的邊預測)
圖級別任務:氣味識別(典型的圖分類)、發現“宇宙”
Part4 圖學習是怎麼做的
圖遊走類演算法:通過在圖上的遊走,獲得多個節點序列,再利用 Skip Gram 模型訓練得到節點表示(下節課內容)
圖神經網路演算法:端到端模型,利用訊息傳遞機制實現。
知識圖譜嵌入演算法:專門用於知識圖譜的相關演算法。
Part5 PGL 圖學習庫簡介
Github 連結:https://github.com/PaddlePaddle/PGL
API文件: https://pgl.readthedocs.io/en/latest/
Part6 熟悉 PGL 使用
- 環境安裝 !pip install pgl
- 使用以下程式碼來構圖:
import pgl
from pgl import graph # 匯入 PGL 中的圖模組
import paddle.fluid as fluid # 匯入飛槳框架
import numpy as np
def build_graph():
# 定義圖中的節點數目,我們使用數字來表示圖中的每個節點
num_nodes = 10
# 定義圖中的邊集
edge_list = [(2, 0), (2, 1), (3, 1),(4, 0), (5, 0),
(6, 0), (6, 4), (6, 5), (7, 0), (7, 1),
(7, 2), (7, 3), (8, 0), (9, 7)]
# 隨機初始化節點特徵,特徵維度為 d
d = 16
feature = np.random.randn(num_nodes, d).astype("float32")
# 隨機地為每條邊賦值一個權重
edge_feature = np.random.randn(len(edge_list), 1).astype("float32")
# 建立圖物件,最多四個輸入
g = graph.Graph(num_nodes = num_nodes,
edges = edge_list,
node_feat = {'feature':feature},
edge_feat ={'edge_feature': edge_feature})
return g
g = build_graph()
接下來我們列印圖的一些資訊:
print('圖中共計 %d 個節點' % g.num_nodes)
print('圖中共計 %d 條邊' % g.num_edges)
- 定義圖模型
我們可以定義下面的一個簡單圖模型層,這裡的結構是新增了邊權重資訊的類 GCN 層。
# 定義一個同時傳遞節點特徵和邊權重的簡單模型層。
def model_layer(gw, nfeat, efeat, hidden_size, name, activation):
'''
gw: GraphWrapper 圖資料容器,用於在定義模型的時候使用,後續訓練時再feed入真實資料
nfeat: 節點特徵
efeat: 邊權重
hidden_size: 模型隱藏層維度
activation: 使用的啟用函式
'''
# 定義 send 函式
def send_func(src_feat, dst_feat, edge_feat):
# 將源節點的節點特徵和邊權重共同作為訊息傳送
return src_feat['h'] * edge_feat['e']
# 定義 recv 函式
def recv_func(feat):
# 目標節點接收源節點訊息,採用 sum 的聚合方式
return fluid.layers.sequence_pool(feat, pool_type='sum')
# 觸發訊息傳遞機制
msg = gw.send(send_func, nfeat_list=[('h', nfeat)], efeat_list=[('e', efeat)])
output = gw.recv(msg, recv_func)
output = fluid.layers.fc(output,
size=hidden_size,
bias_attr=False,
act=activation,
name=name)
return output
- 模型定義
這裡我們簡單的把上述定義好的模型層堆疊兩層,作為我們的最終模型。
class Model(object):
def __init__(self, graph):
"""
graph: 我們前面建立好的圖
"""
# 建立 GraphWrapper 圖資料容器,用於在定義模型的時候使用,後續訓練時再feed入真實資料
self.gw = pgl.graph_wrapper.GraphWrapper(name='graph',
node_feat=graph.node_feat_info(),
edge_feat=graph.edge_feat_info())
# 作用同 GraphWrapper,此處用作節點標籤的容器
self.node_label = fluid.layers.data("node_label", shape=[None, 1],
dtype="float32", append_batch_size=False)
def build_model(self):
# 定義兩層model_layer
output = model_layer(self.gw,
self.gw.node_feat['feature'],
self.gw.edge_feat['edge_feature'],
hidden_size=8,
name='layer_1',
activation='relu')
output = model_layer(self.gw,
output,
self.gw.edge_feat['edge_feature'],
hidden_size=1,
name='layer_2',
activation=None)
# 對於二分類任務,可以使用以下 API 計算損失
loss = fluid.layers.sigmoid_cross_entropy_with_logits(x=output,
label=self.node_label)
# 計算平均損失
loss = fluid.layers.mean(loss)
# 計算準確率
prob = fluid.layers.sigmoid(output)
pred = prob > 0.5
pred = fluid.layers.cast(prob > 0.5, dtype="float32")
correct = fluid.layers.equal(pred, self.node_label)
correct = fluid.layers.cast(correct, dtype="float32")
acc = fluid.layers.reduce_mean(correct)
return loss, acc
- 訓練前準備
# 是否在 GPU 或 CPU 環境執行
use_cuda = False
place = fluid.CUDAPlace(0) if use_cuda else fluid.CPUPlace()
# 定義程式,也就是我們的 Program
startup_program = fluid.Program() # 用於初始化模型引數
train_program = fluid.Program() # 訓練時使用的主程式,包含前向計算和反向梯度計算
test_program = fluid.Program() # 測試時使用的程式,只包含前向計算
with fluid.program_guard(train_program, startup_program):
model = Model(g)
# 建立模型和計算 Loss
loss, acc = model.build_model()
# 選擇Adam優化器,學習率設定為0.01
adam = fluid.optimizer.Adam(learning_rate=0.01)
adam.minimize(loss) # 計算梯度和執行梯度反向傳播過程
# 複製構造 test_program,與 train_program的區別在於不需要梯度計算和反向過程。
test_program = train_program.clone(for_test=True)
# 定義一個在 place(CPU)上的Executor來執行program
exe = fluid.Executor(place)
# 引數初始化
exe.run(startup_program)
# 獲取真實圖資料
feed_dict = model.gw.to_feed(g)
# 獲取真實標籤資料
# 由於我們是做節點分類任務,因此可以簡單的用0、1表示節點類別。其中,黃色點標籤為0,綠色點標籤為1。
y = [0,1,1,1,0,0,0,1,0,1]
label = np.array(y, dtype="float32")
label = np.expand_dims(label, -1)
feed_dict['node_label'] = label
- 開始訓練
for epoch in range(30):
train_loss = exe.run(train_program,
feed=feed_dict, # feed入真實訓練資料
fetch_list=[loss], # fetch出需要的計算結果
return_numpy=True)[0]
print('Epoch %d | Loss: %f' % (epoch, train_loss))
Day-2 圖遊走類模型
1.生成單條 DeepWalk 遊走序列
import numpy as np
%matplotlib inline
import matplotlib.pyplot as plt
import networkx as nx # networkx是一個常用的繪製複雜圖形的Python包。
import pgl
構建graph
在進行deepwalk遊走之前,我們需要構建一個圖網路。
圖網路的構建需要用到Graph類,Graph類的具體實現可以參考 PGL/pgl/graph.py
簡單展示一下如果構建一個圖網路:
def build_graph():
# 定義節點的個數;每個節點用一個數字表示,即從0~9
num_node = 10
# 新增節點之間的邊,每條邊用一個tuple表示為: (src, dst)
edge_list = [(2, 0), (2, 1), (3, 1),(4, 0), (0, 5),
(6, 0), (6, 4), (5, 6), (7, 0), (1, 7),
(2, 7), (7, 3), (8, 0), (9, 7)]
g = pgl.graph.Graph(num_nodes = num_node, edges = edge_list)
return g
# 建立一個圖物件,用於儲存圖網路的各種資料。
g = build_graph()
def display_graph(g):
nx_G = nx.Graph()
nx_G.add_nodes_from(range(g.num_nodes))
nx_G.add_edges_from(g.edges)
pos = nx.spring_layout(nx_G, iterations=50)
nx.draw(nx_G,
pos,
with_labels=True,
node_color=['y','y','g','g','g','y','y','g','y','g'],
node_size=1000)
plt.show()
display_graph(g)
Deepwalk 取樣
DeepWalk會等概率的選取下一個相鄰節點加入路徑,直至達到最大路徑長度,或者沒有下一個節點可選。
因此, 假如我們想要得到一條walk, 我們需要輸入一個graph, 起始節點ID, 遊走的深度walk_len。
def deepwalk(graph, start_node, walk_len):
walk = [start_node] # 初始化遊走序列
for d in range(walk_len): # 最大長度範圍內進行取樣
current_node = walk[-1]
successors = graph.successor(np.array([current_node])) # graph.successor: 獲取當前節點的後繼鄰居
print("當前節點: %d" % current_node)
print("後繼鄰居", successors[0])
succ = successors[0]
if len(succ) == 0:
break
next_node = np.random.choice(succ, 1)
walk.extend(next_node)
return walk
walk = deepwalk(g, 2, 4)
print(walk)
Day-3 圖神經網路模型(一)
今天課堂主要講解了三個部分:GCN 演算法、GAT 演算法、Message Passing 訊息傳遞機制。
GCN引數補充解釋
主要是幫助大家理解訊息傳遞機制的一些引數型別。
這裡我們給出一個簡化版本的 GCN 模型,幫助大家理解PGL框架實現訊息傳遞的流程。
import paddle.fluid.layers as L
def gcn_layer(gw, feature, hidden_size, activation, name, norm=None):
"""
描述:通過GCN層計算新的節點表示
輸入:gw - GraphWrapper物件
feature - 節點表示 (num_nodes, feature_size)
hidden_size - GCN層的隱藏層維度 int
activation - 啟用函式 str
name - GCN層名稱 str
norm - 標準化tensor float32 (num_nodes,),None表示不標準化
輸出:新的節點表示 (num_nodes, hidden_size)
"""
# send函式
def send_func(src_feat, dst_feat, edge_feat):
"""
描述:用於send節點資訊。函式名可自定義,引數列表固定
輸入:src_feat - 源節點的表示字典 {name:(num_edges, feature_size)}
dst_feat - 目標節點表示字典 {name:(num_edges, feature_size)}
edge_feat - 與邊(src, dst)相關的特徵字典 {name:(num_edges, feature_size)}
輸出:儲存傳送資訊的張量或字典 (num_edges, feature_size) or {name:(num_edges, feature_size)}
"""
return src_feat["h"] # 直接返回源節點表示作為資訊
# send和recv函式是搭配實現的,send的輸出就是recv函式的輸入
# recv函式
def recv_func(msg):
"""
描述:對接收到的msg進行聚合。函式名可自定義,引數列表固定
輸出:新的節點表示張量 (num_nodes, feature_size)
"""
return L.sequence_pool(msg, pool_type='sum') # 對接收到的訊息求和
### 訊息傳遞機制執行過程
# gw.send函式
msg = gw.send(send_func, nfeat_list=[("h", feature)])
"""
描述:觸發message函式,傳送訊息並將訊息返回
輸入:message_func - 自定義的訊息函式
nfeat_list - list [name] or tuple (name, tensor)
efeat_list - list [name] or tuple (name, tensor)
輸出:訊息字典 {name:(num_edges, feature_size)}
"""
# gw.recv函式
output = gw.recv(msg, recv_func)
"""
描述:觸發reduce函式,接收並處理訊息
輸入:msg - gw.send輸出的訊息字典
reduce_function - "sum"或自定義的reduce函式
輸出:新的節點特徵 (num_nodes, feature_size)
如果reduce函式是對訊息求和,可以直接用"sum"作為引數,使用內建函式加速訓練,上述語句等價於 \
output = gw.recv(msg, "sum")
"""
# 通過以activation為啟用函式的全連線輸出層
output = L.fc(output, size=hidden_size, bias_attr=False, act=activation, name=name)
return output
Day-4 Graphsage 取樣程式碼實踐
GraphSage的PGL完整程式碼實現位於 PGL/examples/graphsage/
本次實踐將帶領大家嘗試實現一個簡單的graphsage 取樣程式碼實現
1. 構建graph
在實現graphsage取樣之前,我們需要構建一個圖網路。
圖網路的構建需要用到Graph類,Graph類的具體實現可以參考 PGL/pgl/graph.py
下面我們簡單展示一下如何構建一個圖網路:
import random
import numpy as np
import pgl
import display
def build_graph():
# 定義節點的個數;每個節點用一個數字表示,即從0~9
num_node = 16
# 新增節點之間的邊,每條邊用一個tuple表示為: (src, dst)
edge_list = [(2, 0), (1, 0), (3, 0),(4, 0), (5, 0),
(6, 1), (7, 1), (8, 2), (9, 2), (8, 7),
(10, 3), (4, 3), (11, 10), (11, 4), (12, 4),
(13, 5), (14, 5), (15, 5)]
g = pgl.graph.Graph(num_nodes = num_node, edges = edge_list)
return g
# 建立一個圖物件,用於儲存圖網路的各種資料。
g = build_graph()
display.display_graph(g)
2. GraphSage取樣函式實現
GraphSage的作者提出了取樣演算法來使得模型能夠以Mini-batch的方式進行訓練,演算法虛擬碼見論文附錄A。
1.假設我們要利用中心節點的k階鄰居資訊,則在聚合的時候,需要從第k階鄰居傳遞資訊到k-1階鄰居,並依次傳遞到中心節點。
2.取樣的過程剛好與此相反,在構造第t輪訓練的Mini-batch時,我們從中心節點出發,在前序節點集合中取樣Nt個鄰居節點加入取樣集合。
3.接著將鄰居節點作為新的中心節點繼續進行第t-1輪訓練的節點 取樣,以此類推。
4.最後將取樣到的節點和邊一起構造得到子圖。
def traverse(item):
"""traverse
"""
if isinstance(item, list) or isinstance(item, np.ndarray):
for i in iter(item):
for j in traverse(i):
yield j
else:
yield item
def flat_node_and_edge(nodes):
"""這個函式的目的是為了將 list of numpy array 扁平化成一個list
例如: [array([7, 8, 9]), array([11, 12]), array([13, 15])] --> [7, 8, 9, 11, 12, 13, 15]
"""
nodes = list(set(traverse(nodes)))
return nodes
def graphsage_sample(graph, start_nodes, sample_num):
subgraph_edges = []
# pre_nodes: a list of numpy array,
pre_nodes = graph.sample_predecessor(start_nodes, sample_num)
# 根據取樣的子節點, 恢復邊
for dst_node, src_nodes in zip(start_nodes, pre_nodes):
for node in src_nodes:
subgraph_edges.append((node, dst_node))
# flat_node_and_edge: 這個函式的目的是為了將 list of numpy array 扁平化成一個list
# [array([7, 8, 9]), array([11, 12]), array([13, 15])] --> [7, 8, 9, 11, 12, 13, 15]
subgraph_nodes = flat_node_and_edge(pre_nodes)
return subgraph_nodes, subgraph_edges
seed = 458
np.random.seed(seed)
random.seed(seed)
start_nodes = [0]
layer1_nodes, layer1_edges = graphsage_sample(g, start_nodes, sample_num=3)
print('layer1_nodes: ', layer1_nodes)
print('layer1_edges: ', layer1_edges)
display.display_subgraph(g, {'orange': layer1_nodes}, {'orange': layer1_edges})
layer2_nodes, layer2_edges = graphsage_sample(g, layer1_nodes, sample_num=2)
print('layer2_nodes: ', layer2_nodes)
print('layer2_edges: ', layer2_edges)
display.display_subgraph(g, {'orange': layer1_nodes, 'Thistle': layer2_nodes}, {'orange': laye
Day-5 ERNIESage程式碼解析
本專案主要是為了直接提供一個可以執行ERNIESage模型的程式碼介紹,以便同學們能夠直觀感受到ERNIESage的魅力,同時也會對ERNIESage中的部分關鍵程式碼進行必要講解。Let’s enjoy!
ERNIESage可以很輕鬆地在PGL中的訊息傳遞正規化中進行實現,目前PGL在github上提供了3個版本的ERNIESage模型:
ERNIESage v1: ERNIE 作用於text graph節點上;
ERNIESage v2: ERNIE 作用在text graph的邊上;
ERNIESage v3: ERNIE 作用於一階鄰居及起邊上;
講解流程
資料
模型
訓練
# 拉取PGL程式碼,由於github拉取較慢,已經提前拉取完畢了
# !git clone https://github.com/PaddlePaddle/PGL
# !cd PGL/example/erniesage
# 為了正常執行程式碼,首先我們需要安裝以下依賴
!pip install pgl
!pip install easydict
!python3 -m pip install --no-deps paddle-propeller
!pip install paddle-ernie
!pip uninstall -y colorlog
!export CUDAV_VISIBLE_DEVICES=0
資料
輸入example資料集
example_data/link_predict/graph_data.txt - 簡單的輸入檔案,格式為每行query \t answer,可作簡單的執行例項使用,link predict任務一般直接用圖中的邊作為訓練目標。
```javascript
! head -n 3 example_data/link_predict/graph_data.txt
! wc -l example_data/link_predict/graph_data.txt
如何表達一個文字圖
1.出現過的每一個文字段當作一個節點,比如“黑緣粗角肖葉甲觸角有多大?”就是一個節點
2.一行兩個節點作為一條邊
3.節點的文字段逐字轉成id,形成id序列,作為節點特徵
from preprocessing.dump_graph import dump_graph
from preprocessing.dump_graph import dump_node_feat
from preprocessing.dump_graph import download_ernie_model
from preprocessing.dump_graph import load_config
from pgl.graph_wrapper import BatchGraphWrapper
import propeller.paddle as propeller
import paddle.fluid as F
import paddle.fluid.layers as L
import numpy as np
from preprocessing.dump_graph import load_config
from models.pretrain_model_loader import PretrainedModelLoader
from pgl.graph import MemmapGraph
from models.encoder import linear
from ernie import ErnieModel
np.random.seed(123)
config = load_config("./config/erniesage_link_predict.yaml")
from preprocessing.dump_graph import dump_graph
from preprocessing.dump_graph import dump_node_feat
from preprocessing.dump_graph import download_ernie_model
from preprocessing.dump_graph import load_config
from pgl.graph_wrapper import BatchGraphWrapper
import propeller.paddle as propeller
import paddle.fluid as F
import paddle.fluid.layers as L
import numpy as np
from preprocessing.dump_graph import load_config
from models.pretrain_model_loader import PretrainedModelLoader
from pgl.graph import MemmapGraph
from models.encoder import linear
from ernie import ErnieModel
np.random.seed(123)
config = load_config("./config/erniesage_link_predict.yaml")
# 將原始QA資料產出一個文字圖,並使用grpah.dump存放到 workdir 目錄下
dump_graph(config)
dump_node_feat(config)
# MemmapGraph可以將PGL中graph.dump的模型,重新load回來
graph = MemmapGraph("./workdir/")
# 看一下圖基礎資訊
print("節點", graph.num_nodes,"個")
print("邊", graph.edges, graph.edges.shape)
# 看一下節點特徵
print([("%s shape is %s" % (key, str(graph.node_feat[key].shape))) for key in graph.node_feat])
print(graph.node_feat) # 按字的粒度轉成ID,每段文字為一個節點,文字全部保留40長度
# 1021個節點,每個節點有長度為40的id序列
模型
ERNIESage V1 模型核心流程
ERNIE提取節點語義 -> GNN聚合
# ERNIESage V1,ERNIE作用在節點上
class ERNIESageV1Encoder():
def __init__(self, config):
self.config = config
def __call__(self, graph_wrappers, inputs):
# step1. ERNIE提取節點語義
# 輸入每個節點的文字的id序列
term_ids = graph_wrappers[0].node_feat["term_ids"]
cls = L.fill_constant_batch_size_like(term_ids, [-1, 1], "int64",
self.config.cls_id) # cls [B, 1]
term_ids = L.concat([cls, term_ids], 1) # term_ids [B, S]
# [CLS], id1, id2, id3 .. [SEP]
ernie_model = ErnieModel(self.config.ernie_config)
# 獲得ERNIE的[CLS]位置的表達
cls_feat, _ = ernie_model(term_ids) # cls_feat [B, F]
# step2. GNN聚合
feature = graphsage_sum(cls_feat, graph_wrappers[0], self.config.hidden_size, "v1_graphsage_sum", "leaky_relu")
final_feats = [
self.take_final_feature(feature, i, "v1_final_fc") for i in inputs
]
return final_feats
def take_final_feature(self, feature, index, name):
"""take final feature"""
feat = L.gather(feature, index, overwrite=False)
feat = linear(feat, self.config.hidden_size, name)
feat = L.l2_normalize(feat, axis=1)
return feat
def graphsage_sum(feature, gw, hidden_size, name, act):
# copy_send
msg = gw.send(lambda src, dst, edge: src["h"], nfeat_list=[("h", feature)])
# sum_recv
neigh_feature = gw.recv(msg, lambda feat: L.sequence_pool(feat, pool_type="sum"))
self_feature = linear(feature, hidden_size, name+"_l", act)
neigh_feature = linear(neigh_feature, hidden_size, name+"_r", act)
output = L.concat([self_feature, neigh_feature], axis=1) # [B, 2H]
output = L.l2_normalize(output, axis=1)
return output
# 隨機構造些資料
feat_size = 40
feed_dict = {
"num_nodes": np.array([4]),
"num_edges": np.array([6]),
"edges": np.array([[0,1],[1,0],[0,2],[2,0],[0,3],[3,0]]),
"term_ids": np.random.randint(4, 10000, size=(4, feat_size)),
"inputs": np.array([0])}
place = F.CUDAPlace(0)
exe = F.Executor(place)
# 模型v1
erniesage_v1_encoder = ERNIESageV1Encoder(config)
main_prog, start_prog = F.Program(), F.Program()
with F.program_guard(main_prog, start_prog):
with F.unique_name.guard():
num_nodes = L.data("num_nodes", [1], False, 'int64')
num_edges = L.data("num_edges", [1], False, 'int64')
edges = L.data("edges", [-1, 2], False, 'int64')
node_feat = L.data("term_ids", [-1, 40], False, 'int64')
inputs = L.data("inputs", [-1], False, 'int64')
# 輸入圖的基本資訊(邊、點、特徵)構造一個graph
gw = BatchGraphWrapper(num_nodes, num_edges, edges, {"term_ids": node_feat})
outputs = erniesage_v1_encoder([gw], [inputs])
exe.run(start_prog)
outputs_np = exe.run(main_prog, feed=feed_dict, fetch_list=[outputs])[0]
print(outputs_np)
ERNIESage V2 核心程式碼
GNN send 文字id -> ERNIE提取邊語義 -> GNN recv 聚合鄰居語義 -> ERNIE提取中心節點語義並concat
圖片替換文字
為了使得大家對下面有關ERNIE模型的部分能夠有所瞭解,這裡先貼出ERNIE的主模型框架圖。
# ERNIESage V2,ERNIE作用在邊上
class ERNIESageV2Encoder():
def __init__(self, config):
self.config = config
def __call__(self, graph_wrappers, inputs):
gw = graph_wrappers[0]
term_ids = gw.node_feat["term_ids"] # term_ids [B, S]
# step1. GNN send 文字id
def ernie_send(src_feat, dst_feat, edge_feat):
def build_position_ids(term_ids):
input_mask = L.cast(term_ids > 0, "int64")
position_ids = L.cumsum(input_mask, axis=1) - 1
return position_ids
# src_ids, dst_ids 為傳送src和接收dst節點分別的文字ID序列
src_ids, dst_ids = src_feat["term_ids"], dst_feat["term_ids"]
# 生成[CLS]對應的id列, 並與前半段concat
cls = L.fill_constant_batch_size_like(
src_feat["term_ids"], [-1, 1], "int64", self.config.cls_id) # cls [B, 1]
src_ids = L.concat([cls, src_ids], 1) # src_ids [B, S+1]
# 將src與dst concat在一起作為完整token ids
term_ids = L.concat([src_ids, dst_ids], 1) # term_ids [B, 2S+1]
# [CLS], src_id1, src_id2.. [SEP], dst_id1, dst_id2..[SEP]
sent_ids = L.concat([L.zeros_like(src_ids), L.ones_like(dst_ids)], 1)
# 0, 0, 0 .. 0, 1, 1 .. 1
position_ids = build_position_ids(term_ids)
# 0, 1, 2, 3 ..
# step2. ERNIE提取邊語義
ernie_model = ErnieModel(self.config.ernie_config)
cls_feat, _ = ernie_model(term_ids, sent_ids, position_ids)
# cls_feat 為ERNIE提取的句子級隱向量表達
return cls_feat
msg = gw.send(ernie_send, nfeat_list=[("term_ids", term_ids)])
# step3. GNN recv 聚合鄰居語義
# 接收了鄰居的CLS語義表達,sum聚合在一起
neigh_feature = gw.recv(msg, lambda feat: F.layers.sequence_pool(feat, pool_type="sum"))
# 為每個節點也拼接一個CLS表達
cls = L.fill_constant_batch_size_like(term_ids, [-1, 1],
"int64", self.config.cls_id)
term_ids = L.concat([cls, term_ids], 1)
# [CLS], id1, id2, ... [SEP]
# step4. ERNIE提取中心節點語義並concat
# 對中心節點過一次ERNIE
ernie_model = ErnieModel(self.config.ernie_config)
# 獲取中心節點的語義CLS表達
self_cls_feat, _ = ernie_model(term_ids)
hidden_size = self.config.hidden_size
self_feature = linear(self_cls_feat, hidden_size, "erniesage_v2_l", "leaky_relu")
neigh_feature = linear(neigh_feature, hidden_size, "erniesage_v2_r", "leaky_relu")
output = L.concat([self_feature, neigh_feature], axis=1)
output = L.l2_normalize(output, axis=1)
final_feats = [
self.take_final_feature(output, i, "v2_final_fc") for i in inputs
]
return final_feats
def take_final_feature(self, feature, index, name):
"""take final feature"""
feat = L.gather(feature, index, overwrite=False)
feat = linear(feat, self.config.hidden_size, name)
feat = L.l2_normalize(feat, axis=1)
return feat
In [10]
# 直接run一下
erniesage_v2_encoder = ERNIESageV2Encoder(config)
main_prog, start_prog = F.Program(), F.Program()
with F.program_guard(main_prog, start_prog):
with F.unique_name.guard():
num_nodes = L.data("num_nodes", [1], False, 'int64')
num_edges = L.data("num_edges", [1], False, 'int64')
edges = L.data("edges", [-1, 2], False, 'int64')
node_feat = L.data("term_ids", [10, 40], False, 'int64')
inputs = L.data("inputs", [2], False, 'int64')
gw = BatchGraphWrapper(num_nodes, num_edges, edges, {"term_ids": node_feat})
outputs = erniesage_v2_encoder([gw], [inputs])
exe = F.Executor(place)
exe.run(start_prog)
outputs_np = exe.run(main_prog, feed=feed_dict, fetch_list=[outputs])[0]
print(outputs_np)
ERNIESage V3 核心過程
GNN send 文字id序列 -> GNN recv 拼接文字id序列 -> ERNIE同時提取中心和多個鄰居語義表達
from models.encoder import v3_build_sentence_ids
from models.encoder import v3_build_position_ids
class ERNIESageV3Encoder():
def __init__(self, config):
self.config = config
def __call__(self, graph_wrappers, inputs):
gw = graph_wrappers[0]
term_ids = gw.node_feat["term_ids"]
# step1. GNN send 文字id序列
# copy_send
msg = gw.send(lambda src, dst, edge: src["h"], nfeat_list=[("h", term_ids)])
# step2. GNN recv 拼接文字id序列
def ernie_recv(term_ids):
"""doc"""
num_neighbor = self.config.samples[0]
pad_value = L.zeros([1], "int64")
# 這裡使用seq_pad,將num_neighbor個鄰居節點的文字id序列拼接在一下
# 對於不足num_neighbor個鄰居的將會pad到num_neighbor個
neighbors_term_ids, _ = L.sequence_pad(
term_ids, pad_value=pad_value, maxlen=num_neighbor) # [B, N*S]
neighbors_term_ids = L.reshape(neighbors_term_ids, [0, self.config.max_seqlen * num_neighbor])
return neighbors_term_ids
neigh_term_ids = gw.recv(msg, ernie_recv)
neigh_term_ids = L.cast(neigh_term_ids, "int64")
# step3. ERNIE同時提取中心和多個鄰居語義表達
cls = L.fill_constant_batch_size_like(term_ids, [-1, 1], "int64",
self.config.cls_id) # [B, 1]
# 將中心與多個鄰居的文字全部拼接在一起,形成超長的文字(num_nerghbor+1) * seqlen
multi_term_ids = L.concat([cls, term_ids[:, :-1], neigh_term_ids], 1) # multi_term_ids [B, (N+1)*S]
# [CLS], center_id1, center_id2..[SEP]n1_id1, n1_id2..[SEP]n2_id1, n2_id2..[SEP]..[SEP]
slot_seqlen = self.config.max_seqlen
final_feats = []
for index in inputs:
term_ids = L.gather(multi_term_ids, index, overwrite=False)
position_ids = v3_build_position_ids(term_ids, slot_seqlen)
sent_ids = v3_build_sentence_ids(term_ids, slot_seqlen)
# 將需要計算的超長文字,使用Ernie提取CLS位置的語義表達
ernie_model = ErnieModel(self.config.ernie_config)
cls_feat, _ = ernie_model(term_ids, sent_ids, position_ids)
feature = linear(cls_feat, self.config.hidden_size, "v3_final_fc")
feature = L.l2_normalize(feature, axis=1)
final_feats.append(feature)
return final_feats
# 直接run一下
erniesage_v3_encoder = ERNIESageV3Encoder(config)
main_prog, start_prog = F.Program(), F.Program()
with F.program_guard(main_prog, start_prog):
num_nodes = L.data("num_nodes", [1], False, 'int64')
num_edges = L.data("num_edges", [1], False, 'int64')
edges = L.data("edges", [-1, 2], False, 'int64')
node_feat = L.data("term_ids", [-1, 40], False, 'int64')
inputs = L.data("inputs", [-1], False, 'int64')
gw = BatchGraphWrapper(num_nodes, num_edges, edges, {"term_ids": node_feat})
outputs = erniesage_v3_encoder([gw], [inputs])
exe.run(start_prog)
outputs_np = exe.run(main_prog, feed=feed_dict, fetch_list=[outputs])[0]
print(outputs_np)
下面展示一些 `內聯程式碼片`。
訓練
link predict任務
以一個link predict的任務為例,讀取一個語義圖,以上面的邊為目標進行無監督的訓練
In [17]
class ERNIESageLinkPredictModel(propeller.train.Model):
def __init__(self, hparam, mode, run_config):
self.hparam = hparam
self.mode = mode
self.run_config = run_config
def forward(self, features):
num_nodes, num_edges, edges, node_feat_index, node_feat_term_ids, user_index, \
pos_item_index, neg_item_index, user_real_index, pos_item_real_index = features
node_feat = {"index": node_feat_index, "term_ids": node_feat_term_ids}
graph_wrapper = BatchGraphWrapper(num_nodes, num_edges, edges,
node_feat)
#encoder = ERNIESageV1Encoder(self.hparam)
encoder = ERNIESageV2Encoder(self.hparam)
#encoder = ERNIESageV3Encoder(self.hparam)
# 中心節點、鄰居節點、隨機取樣節點 分別提取特徵
outputs = encoder([graph_wrapper],
[user_index, pos_item_index, neg_item_index])
user_feat, pos_item_feat, neg_item_feat = outputs
if self.mode is not propeller.RunMode.PREDICT:
return user_feat, pos_item_feat, neg_item_feat
else:
return user_feat, user_real_index
def loss(self, predictions, labels):
user_feat, pos_item_feat, neg_item_feat = predictions
pos = L.reduce_sum(user_feat * pos_item_feat, -1, keep_dim=True) #
#neg = L.reduce_sum(user_feat * neg_item_feat, -1, keep_dim=True)# 60.
neg = L.matmul(user_feat, neg_item_feat, transpose_y=True) # 80.
# 距離(中心,鄰居)> 距離(中心,隨機負)
loss = L.reduce_mean(L.relu(neg - pos + self.hparam.margin))
return loss
def backward(self, loss):
adam = F.optimizer.Adam(learning_rate=self.hparam['learning_rate'])
adam.minimize(loss)
def metrics(self, predictions, label):
return {}
from link_predict import train
from link_predict import predict
train(config, ERNIESageLinkPredictModel)
predict(config, ERNIESageLinkPredictModel)
! head output/part-0
如何評價
為了可以比較清楚地知道Embedding的效果,我們直接通過MRR簡單判斷一下graphp_data.txt計算出來的Embedding結果,此處將graph_data.txt同時作為訓練集和驗證集。
!python build_dev.py --path "./example_data/link_predict/graph_data.txt" # 此命令用於將訓練資料輸出為需要的格式,產生的檔案為dev_out.txt
# 接下來,計算MRR得分。
# 注意,執行此程式碼的前提是,我們已經將config對應的yaml配置檔案中的input_data引數修改為了:"data.txt"
# 並且注意訓練的模型是針對data.txt的,如果不符合,請重新訓練模型。
!python mrr.py --emb_path output/part-0
總結
通過以上三個版本的模型程式碼簡單的講解,我們可以知道他們的不同點,其實主要就是在訊息傳遞機制的部分有所不同。ERNIESageV1版本只作用在text graph的節點上,在傳遞訊息(Send階段)時只考慮了鄰居本身的文字資訊;而ERNIESageV2版本則作用在了邊上,在Send階段同時考慮了當前節點和其鄰居節點的文字資訊,達到更好的互動效果, ERNIESageV3則作用在中心和全部鄰居上,使節點之間能夠互相attention。
希望通過這一執行例項,可以幫助同學們對ERNIESage有更好的瞭解和認識,大家快快用起來吧!
相關文章
- 圖神經網路7日打卡營-學習心得-20201129神經網路
- 圖神經網路7日打卡心得神經網路
- 圖神經網路7日打卡營--day1安裝筆記神經網路筆記
- 卷積神經網路學習筆記——Siamese networks(孿生神經網路)卷積神經網路筆記
- 【機器學習】搭建神經網路筆記機器學習神經網路筆記
- 深度學習筆記------卷積神經網路深度學習筆記卷積神經網路
- 全連線神經網路學習筆記神經網路筆記
- 卷積神經網路學習筆記——SENet卷積神經網路筆記SENet
- 深度學習卷積神經網路筆記深度學習卷積神經網路筆記
- 【菜鳥筆記|機器學習】神經網路筆記機器學習神經網路
- 機器學習筆記(3): 神經網路初步機器學習筆記神經網路
- 幾種型別神經網路學習筆記型別神經網路筆記
- 深度學習筆記8:利用Tensorflow搭建神經網路深度學習筆記神經網路
- 深度學習與圖神經網路深度學習神經網路
- 吳恩達機器學習筆記 —— 9 神經網路學習吳恩達機器學習筆記神經網路
- 吳恩達《神經網路與深度學習》課程筆記(4)– 淺層神經網路吳恩達神經網路深度學習筆記
- 吳恩達《神經網路與深度學習》課程筆記(5)– 深層神經網路吳恩達神經網路深度學習筆記
- 《手寫數字識別》神經網路 學習筆記神經網路筆記
- 吳恩達機器學習筆記——八、神經網路吳恩達機器學習筆記神經網路
- 深度學習與圖神經網路學習分享:CNN 經典網路之-ResNet深度學習神經網路CNN
- 深度學習入門筆記(十八):卷積神經網路(一)深度學習筆記卷積神經網路
- 深度學習之上,圖神經網路(GNN )崛起深度學習神經網路GNN
- 機器學習導圖系列(5):機器學習模型及神經網路模型機器學習模型神經網路
- 深度學習與圖神經網路學習分享:Graph Embedding 圖嵌入深度學習神經網路
- 吳恩達機器學習課程 筆記5 神經網路吳恩達機器學習筆記神經網路
- 吳恩達《神經網路與深度學習》課程筆記(1)– 深度學習概述吳恩達神經網路深度學習筆記
- 【深度學習篇】--神經網路中的卷積神經網路深度學習神經網路卷積
- 深入淺出圖神經網路 第5章 圖訊號處理與圖卷積神經網路 讀書筆記神經網路卷積筆記
- TensorFlow筆記-07-神經網路優化-學習率,滑動平均筆記神經網路優化
- 神經網路學習參考神經網路
- Ng深度學習筆記——卷積神經網路基礎深度學習筆記卷積神經網路
- 吳恩達《神經網路與深度學習》課程筆記(2)– 神經網路基礎之邏輯迴歸吳恩達神經網路深度學習筆記邏輯迴歸
- 吳恩達《神經網路與深度學習》課程筆記(3)– 神經網路基礎之Python與向量化吳恩達神經網路深度學習筆記Python
- 深度學習四從迴圈神經網路入手學習LSTM及GRU深度學習神經網路
- nndl-復旦-神經網路與深度學習筆記第二章習題神經網路深度學習筆記
- 再聊神經網路與深度學習神經網路深度學習
- AI之(神經網路+深度學習)AI神經網路深度學習
- 【深度學習】神經網路入門深度學習神經網路