学习来源百度AIstudio:https://aistudio.baidu.com/aistudio/index
图的两个基本元素:点、边
图是一种统一描述复杂事物的语言
常见的图:社交网络、推荐系统、化学分子结构…
图学习: Graph Learning。深度学习中的一个子领域,强调处理的数据对象为图。
与一般深度学习的区别:能够方便地处理不规则数据(树、图),同时也可以处理规则数据(如图像)。
我们可以把图学习的应用分为节点级别任务、边级别任务、图级别任务。 课程中介绍了以下几种任务。
节点级别任务:金融诈骗检测(典型的节点分类)、自动驾驶中的3D点云目标检测
边级别任务:推荐系统(典型的边预测)
图级别任务:气味识别(典型的图分类)、发现“宇宙”
图游走类算法:通过在图上的游走,获得多个节点序列,再利用 Skip Gram 模型训练得到节点表示(下节课内容)
图神经网络算法:端到端模型,利用消息传递机制实现。
知识图谱嵌入算法:专门用于知识图谱的相关算法。
Github 链接:https://github.com/PaddlePaddle/PGL
API文档: https://pgl.readthedocs.io/en/latest/
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)
# 定义一个同时传递节点特征和边权重的简单模型层。
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))
import numpy as np
%matplotlib inline
import matplotlib.pyplot as plt
import networkx as nx # networkx是一个常用的绘制复杂图形的Python包。
import pgl
在进行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会等概率的选取下一个相邻节点加入路径,直至达到最大路径长度,或者没有下一个节点可选。
因此, 假如我们想要得到一条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)
今天课堂主要讲解了三个部分:GCN 算法、GAT 算法、Message Passing 消息传递机制。
主要是帮助大家理解消息传递机制的一些参数类型。
这里我们给出一个简化版本的 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
GraphSage的PGL完整代码实现位于 PGL/examples/graphsage/
本次实践将带领大家尝试实现一个简单的graphsage 采样代码实现
在实现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)
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
本项目主要是为了直接提供一个可以运行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_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序列
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)
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)
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有更好的了解和认识,大家快快用起来吧!