Skip to content

Euler 2.0 在异质图上的应用

origin edited this page Jun 29, 2020 · 4 revisions

本章的章节安排如下:

在本章,我们将介绍euler2.0是如何在异质图上应用。

异质图一般指的是,在一张图当中有多种类型的节点和边。现实世界中,有很多图都是异质图,比如在推荐系统中存在多种类型的节点,如商品,用户和商店节点等,这些节点之间的边的关系也是多种多样的,比如用户和商品的购买关系,用户和商品的浏览关系等等。如何将多种不同类型的节点以及边关系同时有效的刻画出来是异质图表示学习的关键。

本章以图神经网络模型R-GCN,异质图Wordnet18为例,介绍如何利用Euler2.0构建图神经网络来解决异质图上的表示学习。

Note:

在章会对相应的一些句子加粗或者在代码做注释来显式地区别与其他场景的应用的一些不同处理。与其他场景的联系和区别的详细对比见这里

数据准备

在有属性图上的应用中的数据准备类似,也是先生成异质图数据,然后对异质图数据进行加载。

Euler2.0 异质图数据生成

1.构建图数据的JSON文件和Meta文件, 其中在JSON文件中可以创建不同的节点和边的类型

如下所示,为生成Wn18数据对应的JSON文件,其中Meta文件为可选文件,这里不提供Meta文件。完整的代码见这里

通过convert2json()函数生成相应的JSON文件,需要注意的是,wn18中节点的没有属性信息,边的属性用边的关系类型存储,节点和边的type多种多样。

def convert2json(self, convert_dir, out_dir):
    def add_node(id, type, weight):
        node_buf = {}
        node_buf["id"] = id
        node_buf["type"] = type
        node_buf["weight"] = weight
        node_buf["features"] = []
        return node_buf

    def add_edge(src, dst, id, type, weight):
        edge_buf = {}
        edge_buf["src"] = src
        edge_buf["dst"] = dst
        edge_buf["type"] = type
        edge_buf["weight"] = weight
        edge_buf["features"] = []
        
        #采用边的关系类型作为边的属性特征
        lab_buf = {}
        lab_buf["name"] = "id"
        lab_buf["type"] = "dense"
        lab_buf["value"] = [id]
        edge_buf["features"].append(lab_buf)
        return edge_buf

    out_test = open(self.edge_id_file, 'w')
    node_out_test = open(self.node_id_file, 'w')
    with open(out_dir, 'w') as out:
        buf = {}
        buf["nodes"] = []
        buf["edges"] = []
        entity_map = {}
        relation_map = {}
        entity_id = 0
        relation_id = 0
        for file_type in ['train', 'test', 'valid']:
            in_file = open(convert_dir + '/wordnet-mlj12/wordnet-mlj12-' +
                           file_type + '.txt', 'r')
            for line in in_file.readlines():
                triple = line.strip().split()
                if not triple[0] in entity_map:
                    entity_map[triple[0]] = entity_id
                    entity_id += 1
                    buf["nodes"].append(add_node(entity_map[triple[0]],
                                                 file_type, 1))
                    if not triple[2] in entity_map:
                        entity_map[triple[2]] = entity_id
                        entity_id += 1
                        buf["nodes"].append(add_node(entity_map[triple[2]],
                                                     file_type, 1))
                        if not triple[1] in relation_map:
                            relation_map[triple[1]] = relation_id
                            relation_id += 1
                            buf["edges"].append(add_edge(entity_map[triple[0]],
                                                         entity_map[triple[2]],
                                                         relation_map[triple[1]],
                                                         file_type, 1))
                            if file_type == 'test':
                                edge_line = str(entity_map[triple[0]]) + " " + \
                                str(entity_map[triple[2]]) + " " + "1" + "\n"
                                id_line = str(entity_map[triple[0]]) + "\n"
                                out_test.write(edge_line)
                                node_out_test.write(id_line)
                                in_file.close()
                                out.write(json.dumps(buf))
                                print("Total Entity: {}, Relation: {}, Node: {}, Edge: {}."
                                      .format(len(buf["nodes"]), relation_id,
                                              entity_id, len(buf["edges"])))
                                out_test.close()
                                node_out_test.close()

2.利用Euler 2.0的图数据转化工具将步骤1中的JSON文件和Meta文件转化成Euler2.0可以加载的二进制文件

与有属性图类似,这里利用Euler2.0 python工具(详细介绍见这里)将JSON和Meta文件转化成对应的二进制文件。

def convert2euler(self, convert_dir, out_dir):
    dir_name = os.path.dirname(os.path.realpath(__file__))
    convert_meta = self.meta_file
    g = EulerGenerator(convert_dir,
                       convert_meta,
                       out_dir,
                       self.partition_num)
    g.do()

Euler2.0 异质图数据加载

与有属性图类似,异质图数据生成之后,用户需要在训练的时候加载对应的数据,并在每个Batch获取具体的数据,方式如下:

import tf_euler
#加载图数据
euler_graph = tf_euler.dataset.get_dataset('wn18')
euler_graph.load_graph()



#通过tf_euler.sample_node采样训练的节点,生成Batch data,
def get_train_from_input(self, inputs, params):
    result = tf_euler.sample_node(inputs, params['train_node_type'])
    result.set_shape([self.params['batch_size']])
    return result

模型实现

Euler-2.0将GNN类算法抽象成Message Passing接口范式,这里将介绍如何通过Message Passing接口范式构建编写一个面向异质图的GNN模型,这里以R-GCN为例。

与之前的一样,整体上分为两步:

1.实现GNN Encoder

2.实现GNN Model

实现GNN Encoder

在构建GNN算法的时候,用户首先需要实现GNN Encoder,可以通过继承BaseGNNNet基类实现。GNN Encoder的作用是定义一个多层图神经网络的节点特征表达向量的多层图卷积过程。

BaseGNNNet

Euler-2.0提供了BaseGNNNet基类(详见这里)。该类已经封装好了一个多层图神经网络的节点特征表达向量的多层图卷积过程。其中图卷积在Message Passing接口范式下,会被抽象为一个子图抽样方法(flow)和一个卷积汇聚(conv)方法。

class BaseGNNNet(object):

    def __init__(self, conv, flow, dims,
                 fanouts, metapath,
                 add_self_loops=True,
                 max_id=-1,
                 **kwargs):
        conv_class = utils.get_conv_class(conv)
        flow_class = utils.get_flow_class(flow)
        if flow_class == 'whole':
            self.whole_graph = True
        else:
            self.whole_graph = False
        self.convs = []
        for dim in dims[:-1]:
            self.convs.append(self.get_conv(conv_class, dim))
        self.fc = tf.layers.Dense(dims[-1])
        self.sampler = flow_class(fanouts, metapath, add_self_loops, max_id=max_id)

    def get_conv(self, conv_class, dim):
        return conv_class(dim)

    def to_x(self, n_id):
        raise NotImplementedError

    def to_edge(self, n_id_src, n_id_dst, e_id):
        return e_id

    def get_edge_attr(self, block):
        n_id_dst = tf.cast(tf.expand_dims(block.n_id, -1),
                           dtype=tf.float32)
        n_id_src= mp_ops.gather(n_id_dst, block.res_n_id)
        n_id_src = mp_ops.gather(n_id_src,
                                 block.edge_index[0])
        n_id_dst = mp_ops.gather(n_id_dst,
                                 block.edge_index[1])
        n_id_src = tf.cast(tf.squeeze(n_id_src, -1), dtype=tf.int64)
        n_id_dst = tf.cast(tf.squeeze(n_id_dst, -1), dtype=tf.int64)
        edge_attr = self.to_edge(n_id_src, n_id_dst, block.e_id)
        return edge_attr



    def calculate_conv(self, conv, inputs, edge_index,
                       size=None, edge_attr=None):
        return conv(inputs, edge_index, size=size, edge_attr=edge_attr)

    def __call__(self, n_id):
        data_flow = self.sampler(n_id)
        num_layers = len(self.convs)
        x = self.to_x(data_flow[0].n_id)
        for i, conv, block in zip(range(num_layers), self.convs, data_flow):
            if block.e_id is None:
                edge_attr = None
            else:
                edge_attr = self.get_edge_attr(block)
            x_src = mp_ops.gather(x, block.res_n_id)
            x_dst = None if self.whole_graph else x
            x = self.calculate_conv(conv,
                                    (x_src, x_dst),
                                    block.edge_index,
                                    size=block.size,
                                    edge_attr=edge_attr)
            x = tf.nn.relu(x)
        x = self.fc(x)
        return x

继承BaseGNNNet,实现GNN Encoder

在实现自己的GraphEncoder的时候,用户需要继承这个BaseGNNNet类,并实现to_x函数来表示每一个节点H0层embedding的构建过程,这里由于wn18没有节点属性,所以利用ShallowEncoder将节点id进行embedding,作为h0层的embedding。此外,需要重写to_edge(),将边与边的关系类型作为edge作为初始化特征。

class GNN(BaseGNNNet):

    def __init__(self, conv, flow,
                 dims, fanouts, metapath,
                 rel_num, node_max_id,
                 feature_idx, feature_dim,
                 embedding_dim,
                 add_self_loops=False):
        self.fea_dim = embedding_dim
        self.relation_num = rel_num 
        super(GNN, self).__init__(conv=conv,
                                  flow=flow,
                                  dims=dims,
                                  fanouts=fanouts,
                                  metapath=metapath,
                                  add_self_loops=add_self_loops)

        self._encoder = tf_euler.utils.encoders.ShallowEncoder(
            dim=embedding_dim, feature_idx=-1,
            max_id=node_max_id,
            embedding_dim=embedding_dim)
        if not isinstance(feature_idx, list):
            feature_idx = [feature_idx]
        if not isinstance(feature_dim, list):
            feature_dim = [feature_dim]
        self.feature_idx = feature_idx
        self.feature_dim = feature_dim


    def to_x(self, n_id):
        x = self._encoder(n_id)
        return x

    def to_edge(self, n_id_src, n_id_dst, e_id):
        a = tf.expand_dims(n_id_src, -1)
        b = tf.expand_dims(n_id_dst, -1)
        c = tf.expand_dims(e_id, -1)
        c = tf.cast(c, dtype=tf.int64)
        edges = tf.concat([a,b,c], axis=1)
        rel = tf_euler.get_edge_dense_feature(edges, self.feature_idx, self.feature_dim)
        return tf.cast(tf.reshape(rel,[-1]), dtype=tf.int32)

    def get_conv(self, conv_class, dim):
        conv = conv_class(self.fea_dim, dim, None, self.relation_num)
        self.fea_dim = dim
        return conv

参数:

  • conv:使用的卷积方法名称,参考message passing接口中的convolution
  • flow:使用的子图抽样方法名称,参考message passing接口中的dataflow
  • dims:一个列表,元素个数为[卷积层数+1],表示图卷积中每一个convolution的输出embedding维度和最后一个全链接层输出embedding的维度
  • fanouts:一个列表,对graphsage类算法有效,元素个数为[卷积层数],表示每层子图采样中邻居采样的个数
  • metapath:一个列表,元素个数为[卷积层数],表示每层子图采样的采样边类型
  • rel_num:异质图中边的种类个数
  • node_max_id:最大节点的id
  • feature_idx:一个列表,表示H0层使用的dense feature名字集合
  • feature_dim:一个列表,表示H0层使用的dense feature的维度,和feature_idx一一对应
  • embedding_dim:ho层node id对应的embedding维度
  • add_self_loops:表示是否在子图采样的过程中添加自环

该示例中to_x函数定义了,H0层node的embedding为node id embedding,to_edge函数定义了,H0层edge的embedding为edge 的dense特征。

实现GNN模型

通过实现Graph Encoder,用户便可以得到每个节点图卷积后的embed向量。

为了实现GNN(R-GCN)模型,用户需要定义Graph Encoder所用的图抽样方法(dataflow)和卷积汇聚方法(convolution)以及模型的损失函数。

对于图抽样方法(dataflow)和卷积汇聚方法(convolution)而言,Euler2.0提供了:

  • 可选convolution(详见这里):gcn, sage, gat, tag, agnn, sgcn, graphgcn, appnp, arma, dna, gin, gated, relation
  • 可选dataflow(详见这里):full, sage, adapt, layerwise, whole, relation

对于模型的损失函数而言,Euler2.0针对GNN的无监督和有监督模型分别封装了SuperviseModel(详见这里)和UnsuperviseModel(详见这里)。

用户可以直接继承SuperviseModel或者UnsuperviseModel,并定义相应的dataflow和convolution来实现GNN模型,同时实现embed()方式,来定义具体获取节点embedding的方式。

对于R-GCN而言,其采用子图采样方式为‘relation’(具体实现详见这里),卷积方式为‘relation’(具体实现详见这里),损失为无监督损失函数,即需要中心节点的embedding以及context节点的embedding,实现如下

class UnsupervisedRGCN(UnsuperviseModel):
    def __init__(self, node_type, edge_type, max_id,
                       dims, metapath, relation_num,
                       feature_idx, feature_dim,
                       embedding_dim, num_negs, metric):
        super(UnsupervisedRGCN, self).__init__(node_type, edge_type, max_id, num_negs, metric)

        self.dim = dims[-1]

        self.gnn = GNN('relation', 'relation', dims, None,
                       metapath, relation_num, max_id,
                       feature_idx, feature_dim,
                       embedding_dim)

    def embed(self, n_id):
        shape = n_id.shape
        output_shape = shape.concatenate(self.dim)
        output_shape = [d if d is not None else -1 for d in output_shape.as_list()]
        res = self.gnn(tf.reshape(n_id, [-1]))
        return tf.reshape(res, output_shape)

    def embed_context(self, n_id):
        return self.embed(n_id)

模型训练

加载图数据

euler_graph = tf_euler.dataset.get_dataset('wn18')
euler_graph.load_graph()

创建R-GCN模型

model = UnsupervisedRGCN(euler_graph.all_node_type,
                         euler_graph.all_edge_type,
                         euler_graph.max_node_id,
                         dims, metapath,
                         euler_graph.max_edge_id,
                         euler_graph.edge_id_idx,
                         euler_graph.edge_id_dim,
                         flags_obj.embedding_dim,
                         flags_obj.num_negs,
                         flags_obj.metric)

利用NodeEstimator训练模型

Euler-2.0提供了NodeEstimator GraphEstimator EdgeEstimator类和相应接口(详见这里),方便用户快速的完成模型训练,预测,embedding导出任务。其中NodeEstimator为点分类模型,GraphEstimator为图分类模型,EdgeEstimator为边分类模型(link prediction任务)。

这里利用NodeEstimator来训练R-GCN。

params = {'train_node_type': euler_graph.train_node_type[0],
          'batch_size': flags_obj.batch_size,
          'optimizer': flags_obj.optimizer,
          'learning_rate': flags_obj.learning_rate,
          'log_steps': flags_obj.log_steps,
          'model_dir': flags_obj.model_dir,
          'id_file': euler_graph.node_id_file,
          'infer_dir': flags_obj.model_dir,
          'total_step': num_steps}
config = tf.estimator.RunConfig(log_step_count_steps=None)
model_estimator = NodeEstimator(model, params, config)

if flags_obj.run_mode == 'train':
    model_estimator.train()
elif flags_obj.run_mode == 'evaluate':
    model_estimator.evaluate()
elif flags_obj.run_mode == 'infer':
    model_estimator.infer()
else:
    raise ValueError('Run mode not exist!')
Clone this wiki locally