开启生长之旅!这是我参与「日新方案12月更文应战」的第2天,点击检查活动概况

1.ERNIESage运转实例介绍(1.8x版别)

本项目原链接:aistudio.baidu.com/aistudio/pr…

本项目首要是为了直接供给一个能够运转ERNIESage模型的环境

github.com/PaddlePaddl…

在许多工业应用中,往往呈现如下图所示的一种特别的图:Text Graph。顾名思义,图的节点属性由文本构成,而边的构建供给了结构信息。如查找场景下的Text Graph,节点可由查找词、网页标题、网页正文来表达,用户反应和超链信息则可构成边关系。

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

ERNIESage 由PGL团队提出,是ERNIE SAmple aggreGatE的简称,该模型能够同时建模文本语义与图结构信息,有效提升 Text Graph 的应用作用。其中 ERNIE 是百度推出的基于知识增强的继续学习语义了解结构。

ERNIESage 是 ERNIE 与 GraphSAGE 碰撞的成果,是 ERNIE SAmple aggreGatE 的简称,它的结构如下图所示,首要思维是经过 ERNIE 作为聚合函数(Aggregators),建模自身节点和街坊节点的语义与结构关系。ERNIESage 关于文本的建模是构建在街坊聚合的阶段,中心节点文本会与一切街坊节点文本进行拼接;然后经过预练习的 ERNIE 模型进行音讯会聚,捕捉中心节点以及街坊节点之间的相互关系;最终运用 ERNIESage 搭配共同的街坊互相看不见的 Attention Mask 和独立的 Position Embedding 系统,就能够轻松构建 TextGraph 中语句之间以及词之间的关系。

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

运用ID特征的GraphSAGE只能够建模图的结构信息,而独自的ERNIE只能处理文本信息。经过PGL建立的图与文本的桥梁,ERNIESage能够很简略的把GraphSAGE以及ERNIE的优点结合一同。以下面TextGraph的场景,ERNIESage的作用能够比独自的ERNIE以及GraphSAGE模型都要好。

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

ERNIESage能够很轻松地在PGL中的音讯传递范式中进行完结,现在PGL在github上供给了3个版别的ERNIESage模型:

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

首要会针对ERNIESageV1和ERNIESageV2版别进行一个介绍。

1.1算法完结

或许有同学关于整个项目代码文件都不太了解,因此这儿会做一个比较简略的解说。

中心部分包含:

  • 数据集部分
  1. data.txt – 简略的输入文件,格局为每行query \t answer,可作简略的运转实例运用。
  • 模型文件和装备部分
  1. ernie_config.json – ERNIE模型的装备文件。
  2. vocab.txt – ERNIE模型所运用的词表。
  3. ernie_base_ckpt/ – ERNIE模型参数。
  4. config/ – ERNIESage模型的装备文件,包含了三个版别的装备文件。
  • 代码部分
  1. local_run.sh – 进口文件,经过该进口可完结预处理、练习、infer三个步骤。
  2. preprocessing文件夹 – 包含dump_graph.py, tokenization.py。在预处理部分,咱们首要需求进行建图,将输入的文件构建成一张图。因为咱们所研讨的是Text Graph,因此节点都是文本,咱们将文本表明为该节点对应的node feature(节点特征),处理文本的时分需求进行切字,再映射为对应的token id。
  3. dataset/ – 该文件夹包含了数据ready的代码,以便于咱们在练习的时分将练习数据以batch的方式读入。
  4. models/ – 包含了ERNIESage模型中心代码。
  5. train.py – 模型练习进口文件。
  6. learner.py – 分布式练习代码,经过train.py调用。
  7. infer.py – infer代码,用于infer出节点对应的embedding。
  • 评价部分
  1. build_dev.py – 用于将咱们的验证集修正为需求的格局。
  2. 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的主模型结构图。

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

详细的代码解说能够直接看注释。

# 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阶段同时考虑了当时节点和其街坊节点的文本信息,到达更好的交互作用。