关于算法:图神经网络之预训练大模型结合ERNIESage在链接预测任务应用

3次阅读

共计 9008 个字符,预计需要花费 23 分钟才能阅读完成。

1.ERNIESage 运行实例介绍(1.8x 版本)

本我的项目原链接:https://aistudio.baidu.com/aistudio/projectdetail/5097085?contributionType=1

本我的项目次要是为了间接提供一个能够运行 ERNIESage 模型的环境,

https://github.com/PaddlePadd…

在很多工业利用中,往往呈现如下图所示的一种非凡的图:Text Graph。顾名思义,图的节点属性由文本形成,而边的构建提供了构造信息。如搜寻场景下的 Text Graph,节点可由搜索词、网页题目、网页注释来表白,用户反馈和超链信息则可形成边关系。

ERNIESage 由 PGL 团队提出,是 ERNIE SAmple aggreGatE 的简称,该模型能够同时建模文本语义与图构造信息,无效晋升 Text Graph 的利用成果。其中 ERNIE 是百度推出的基于常识加强的继续学习语义了解框架。

ERNIESage 是 ERNIE 与 GraphSAGE 碰撞的后果,是 ERNIE SAmple aggreGatE 的简称,它的构造如下图所示,次要思维是通过 ERNIE 作为聚合函数(Aggregators),建模本身节点和街坊节点的语义与构造关系。ERNIESage 对于文本的建模是构建在街坊聚合的阶段,核心节点文本会与所有街坊节点文本进行拼接;而后通过预训练的 ERNIE 模型进行音讯汇聚,捕获核心节点以及街坊节点之间的互相关系;最初应用 ERNIESage 搭配独特的街坊相互看不见的 Attention Mask 和独立的 Position Embedding 体系,就能够轻松构建 TextGraph 中句子之间以及词之间的关系。

应用 ID 特色的 GraphSAGE 只可能建模图的构造信息,而独自的 ERNIE 只能解决文本信息。通过 PGL 搭建的图与文本的桥梁,ERNIESage可能很简略的把 GraphSAGE 以及 ERNIE 的长处联合一起。以上面 TextGraph 的场景,ERNIESage的成果可能比独自的 ERNIE 以及 GraphSAGE 模型都要好。

ERNIESage能够很轻松地在 PGL 中的消息传递范式中进行实现,目前 PGL 在 github 上提供了 3 个版本的 ERNIESage 模型:

  • ERNIESage v1: ERNIE 作用于 text graph 节点上;
  • ERNIESage v2: ERNIE 作用在 text graph 的边上;
  • ERNIESage v3: ERNIE 作用于一阶街坊及起边上;

次要会针对 ERNIESageV1 和 ERNIESageV2 版本进行一个介绍。

1.1 算法实现

可能有同学对于整个我的项目代码文件都不太理解,因而这里会做一个比较简单的解说。

外围局部蕴含:

  • 数据集局部
  • data.txt – 简略的输出文件,格局为每行 query \t answer,可作简略的运行实例应用。
  • 模型文件和配置局部
  • ernie_config.json – ERNIE 模型的配置文件。
  • vocab.txt – ERNIE 模型所应用的词表。
  • ernie_base_ckpt/ – ERNIE 模型参数。
  • config/ – ERNIESage 模型的配置文件,蕴含了三个版本的配置文件。
  • 代码局部
  • local_run.sh – 入口文件,通过该入口可实现预处理、训练、infer 三个步骤。
  • preprocessing 文件夹 – 蕴含 dump_graph.py, tokenization.py。在预处理局部,咱们首先须要进行建图,将输出的文件构建成一张图。因为咱们所钻研的是 Text Graph,因而节点都是文本,咱们将文本示意为该节点对应的 node feature(节点特色),解决文本的时候须要进行切字,再映射为对应的 token id。
  • dataset/ – 该文件夹蕴含了数据 ready 的代码,以便于咱们在训练的时候将训练数据以 batch 的形式读入。
  • models/ – 蕴含了 ERNIESage 模型外围代码。
  • train.py – 模型训练入口文件。
  • learner.py – 分布式训练代码,通过 train.py 调用。
  • infer.py – infer 代码,用于 infer 出节点对应的 embedding。
  • 评估局部
  • build_dev.py – 用于将咱们的验证集批改为须要的格局。
  • mrr.py – 计算 MRR 值。

要在这个我的项目中运行模型其实很简略,只有运行下方的入口命令就 ok 啦!然而,须要留神的是,因为 ERNIESage 模型比拟大,所以如果 AIStudio 中的 CPU 版本运行模型容易出问题。因而,在运行部署环境时,倡议抉择 GPU 的环境。

另外,如果提醒呈现了 GPU 空间有余等问题,咱们能够通过调小对应 yaml 文件中的 batch_size 来调整,也能够批改 ERNIE 模型的配置文件 ernie_config.json,将 num_hidden_layers 设小一些。在这里,我仅提供了 ERNIESageV2 版本的 gpu 运行过程,如果同学们想运行其余版本的模型,能够依据须要批改下方的命令。

运行结束后,会产生较多的文件,这里进行简略的解释。

  1. workdir/ – 这个文件夹次要会存储和图相干的数据信息。
  2. output/ – 次要的输入文件夹,蕴含了以下内容:(1)模型文件,依据 config 文件中的 save_per_step 可调整保留模型的频率,如果设置得比拟大则可能训练过程中不会保留模型; (2)last 文件夹,保留了进行训练时的模型参数,在 infer 阶段咱们会应用这部分模型参数;(3)part- 0 文件,infer 之后的输出文件中所有节点的 Embedding 输入。

为了能够比较清楚地晓得 Embedding 的成果,咱们间接通过 MRR 简略判断一下 data.txt 计算出来的 Embedding 后果,此处将 data.txt 同时作为训练集和验证集。

1.2 外围模型代码解说

首先,咱们能够通过查看 models/model_factory.py 来判断在本我的项目有多少种 ERNIESage 模型。

from models.base import BaseGNNModel
from models.ernie import ErnieModel
from models.erniesage_v1 import ErnieSageModelV1
from models.erniesage_v2 import ErnieSageModelV2
from models.erniesage_v3 import ErnieSageModelV3

class Model(object):
    @classmethod
    def factory(cls, config):
        name = config.model_type
        if name == "BaseGNNModel":
            return BaseGNNModel(config)
        if name == "ErnieModel":
            return ErnieModel(config)
        if name == "ErnieSageModelV1":
            return ErnieSageModelV1(config)
        if name == "ErnieSageModelV2":
            return ErnieSageModelV2(config)
        if name == "ErnieSageModelV3":
            return ErnieSageModelV3(config)
        else:
            raise ValueError

能够看到一共有 ERNIESage 模型一共有 3 个版本,另外咱们也提供了根本的 GNN 模型和 ERNIE 模型,感兴趣的同学能够自行查阅。

接下来,我次要会针对 ERNIESageV1 和 ERNIESageV2 这两个版本的模型进行要害局部的解说,次要的不同其实就是 消息传递机制(Message Passing)局部的不同。

1.2.1 ERNIESageV1 要害代码

# ERNIESageV1 的 Message Passing 代码
# 查找门路:erniesage_v1.py(__call__中的 self.gnn_layers) -> base.py(BaseNet 类中的 gnn_layers 办法) -> message_passing.py

# erniesage_v1.py
def __call__(self, graph_wrappers):
    inputs = self.build_inputs()
    feature = self.build_embedding(graph_wrappers, inputs[-1])  # 将节点的文本信息利用 ERNIE 模型建模,生成对应的 Embedding 作为 feature
    features = self.gnn_layers(graph_wrappers, feature)  # GNN 模型的次要不同,消息传递机制入口
    outputs = [self.take_final_feature(features[-1], i, "final_fc") for i in inputs[:-1]]
    src_real_index = L.gather(graph_wrappers[0].node_feat['index'], inputs[0])
    outputs.append(src_real_index)
    return inputs, outputs

# base.py -> BaseNet
def gnn_layers(self, graph_wrappers, feature):
    features = [feature]
    initializer = None
    fc_lr = self.config.lr / 0.001
    for i in range(self.config.num_layers):
        if i == self.config.num_layers - 1:
            act = None
        else:
            act = "leaky_relu"
        feature = get_layer(  
            self.config.layer_type, # 对于 ERNIESageV1, 其 layer_type="graphsage_sum",能够到 config 文件夹中查看
            graph_wrappers[i],
            feature,
            self.config.hidden_size,
            act,
            initializer,
            learning_rate=fc_lr,
            name="%s_%s" % (self.config.layer_type, i))
        features.append(feature)
    return features

# message_passing.py
def graphsage_sum(gw, feature, hidden_size, act, initializer, learning_rate, name):
    """doc"""
    msg = gw.send(copy_send, nfeat_list=[("h", feature)]) # Send
    neigh_feature = gw.recv(msg, sum_recv)                # Recv
    self_feature = feature
    self_feature = fluid.layers.fc(self_feature,
                                   hidden_size,
                                   act=act,
                                   param_attr=fluid.ParamAttr(name=name + "_l.w_0", initializer=initializer,
                                   learning_rate=learning_rate),
                                    bias_attr=name+"_l.b_0"
                                   )
    neigh_feature = fluid.layers.fc(neigh_feature,
                                    hidden_size,
                                    act=act,
                                    param_attr=fluid.ParamAttr(name=name + "_r.w_0", initializer=initializer,
                                   learning_rate=learning_rate),
                                    bias_attr=name+"_r.b_0"
                                    )
    output = fluid.layers.concat([self_feature, neigh_feature], axis=1)
    output = fluid.layers.l2_normalize(output, axis=1)
    return output

通过上述代码片段能够看到,要害的消息传递机制代码就是 graphsage_sum 函数,其中 send、recv 局部如下。

def copy_send(src_feat, dst_feat, edge_feat):
    """doc"""
    return src_feat["h"]
    
msg = gw.send(copy_send, nfeat_list=[("h", feature)]) # Send
neigh_feature = gw.recv(msg, sum_recv)                # Recv

通过代码能够看到,ERNIESageV1 版本,其次要是针对节点街坊,间接将以后节点的街坊节点特色求和。再看到 graphsage_sum 函数中,将街坊节点特色进行求和后,失去了 neigh_feature。随后,咱们将节点自身的特色 self_feature 和街坊聚合特色 neigh_feature 通过 fc 层后,间接 concat 起来,从而失去了以后 gnn layer 层的 feature 输入。

1.2.2ERNIESageV2 要害代码

ERNIESageV2 的消息传递机制代码次要在 erniesage_v2.py 和 message_passing.py,绝对 ERNIESageV1 来说,代码会绝对长了一些。

为了使得大家对上面无关 ERNIE 模型的局部可能有所理解,这里先贴出 ERNIE 的主模型框架图。

具体的代码解释能够间接看正文。

# ERNIESageV2 的 Message Passing 代码

# 上面的函数都在 erniesage_v2.py 的 ERNIESageV2 类中
# ERNIESageV2 的调用函数
def __call__(self, graph_wrappers):
    inputs = self.build_inputs()
    feature = inputs[-1]
    features = self.gnn_layers(graph_wrappers, feature) 
    outputs = [self.take_final_feature(features[-1], i, "final_fc") for i in inputs[:-1]]
    src_real_index = L.gather(graph_wrappers[0].node_feat['index'], inputs[0])
    outputs.append(src_real_index)
    return inputs, outputs

# 进入 self.gnn_layers 函数
def gnn_layers(self, graph_wrappers, feature):
    features = [feature]

    initializer = None
    fc_lr = self.config.lr / 0.001

    for i in range(self.config.num_layers):
        if i == self.config.num_layers - 1:
            act = None
        else:
            act = "leaky_relu"

        feature = self.gnn_layer(graph_wrappers[i],
            feature,
            self.config.hidden_size,
            act,
            initializer,
            learning_rate=fc_lr,
            name="%s_%s" % ("erniesage_v2", i))
        features.append(feature)
    return features
接下来会进入 ERNIESageV2 次要的代码局部。能够看到,在 ernie_send 函数用于将咱们的街坊信息发送到以后节点。在 ERNIESageV1 中,咱们在 Send 阶段对街坊节点通过 ERNIE 模型失去 Embedding 后,再间接求和,实际上以后节点和街坊节点之间的文本信息在消息传递过程中是没有间接交互的,直到最初才 **concat** 起来;而 ERNIESageV2 中,在 Send 阶段,源节点和指标节点的信息会间接 concat 起来,通过 ERNIE 模型失去一个对立的 Embedding,这样就失去了源节点和指标节点的一个信息交互过程,这个局部能够查看上面的 ernie_send 函数。gnn_layer 函数中蕴含了三个函数:1. ernie_send: 将 src 和 dst 节点对应文本 concat 后,过 Ernie 后失去须要的 msg,更加具体的解释能够看下方代码正文。2. build_position_ids: 次要是为了创立地位 ID,提供给 Ernie,从而能够产生 position embeddings。3. erniesage_v2_aggregator: gnn_layer 的入口函数,蕴含了消息传递机制,以及聚合后的音讯 feature 处理过程。
# 进入 self.gnn_layer 函数
def gnn_layer(self, gw, feature, hidden_size, act, initializer, learning_rate, name):
    def build_position_ids(src_ids, dst_ids): # 此函数用于创立地位 ID,能够对应到 ERNIE 框架图中的 Position Embeddings
        # ...
        pass
    def ernie_send(src_feat, dst_feat, edge_feat): 
        """doc"""
        # input_ids,能够对应到 ERNIE 框架图中的 Token Embeddings
        cls = L.fill_constant_batch_size_like(src_feat["term_ids"], [-1, 1, 1], "int64", 1)
        src_ids = L.concat([cls, src_feat["term_ids"]], 1)
        dst_ids = dst_feat["term_ids"]
        term_ids = L.concat([src_ids, dst_ids], 1)

        # sent_ids,能够对应到 ERNIE 框架图中的 Segment Embeddings
        sent_ids = L.concat([L.zeros_like(src_ids), L.ones_like(dst_ids)], 1)
        
        # position_ids,能够对应到 ERNIE 框架图中的 Position Embeddings
        position_ids = build_position_ids(src_ids, dst_ids)

        term_ids.stop_gradient = True
        sent_ids.stop_gradient = True
        ernie = ErnieModel( # ERNIE 模型
            term_ids, sent_ids, position_ids,
            config=self.config.ernie_config)
        feature = ernie.get_pooled_output() # 失去发送过去的 msg,该 msg 是由 src 节点和 dst 节点的文本特色一起过 ERNIE 后失去的 embedding
        return feature
    def erniesage_v2_aggregator(gw, feature, hidden_size, act, initializer, learning_rate, name):
        feature = L.unsqueeze(feature, [-1])
        msg = gw.send(ernie_send, nfeat_list=[("term_ids", feature)]) # Send
        neigh_feature = gw.recv(msg, lambda feat: F.layers.sequence_pool(feat, pool_type="sum")) # Recv,间接将发送来的 msg 依据 dst 节点来相加。# 接下来的局部和 ERNIESageV1 相似,将 self_feature 和 neigh_feature 通过 concat、normalize 后失去须要的输入。term_ids = feature
        cls = L.fill_constant_batch_size_like(term_ids, [-1, 1, 1], "int64", 1)
        term_ids = L.concat([cls, term_ids], 1)
        term_ids.stop_gradient = True
        ernie = ErnieModel(term_ids, L.zeros_like(term_ids),
            config=self.config.ernie_config)
        self_feature = ernie.get_pooled_output()
        self_feature = L.fc(self_feature,
                                        hidden_size,
                                        act=act,
                                        param_attr=F.ParamAttr(name=name + "_l.w_0",
                                        learning_rate=learning_rate),
                                        bias_attr=name+"_l.b_0"
                                        )
        neigh_feature = L.fc(neigh_feature,
                                        hidden_size,
                                        act=act,
                                        param_attr=F.ParamAttr(name=name + "_r.w_0",
                                        learning_rate=learning_rate),
                                        bias_attr=name+"_r.b_0"
                                        )
        output = L.concat([self_feature, neigh_feature], axis=1)
        output = L.l2_normalize(output, axis=1)
        return output
    return erniesage_v2_aggregator(gw, feature, hidden_size, act, initializer, learning_rate, name)
    

2. 总结

通过以上两个版本的模型代码简略的解说,咱们能够晓得他们的不同点,其实次要就是在消息传递机制的局部有所不同。ERNIESageV1 版本只作用在 text graph 的节点上,在传递音讯 (Send 阶段) 时只思考了街坊自身的文本信息;而 ERNIESageV2 版本则作用在了边上,在 Send 阶段同时思考了以后节点和其街坊节点的文本信息,达到更好的交互成果。

正文完
 0