-
Notifications
You must be signed in to change notification settings - Fork 558
Euler 2.0 在图分类上的应用
本章的章节安排如下:
在本章,我们将介绍euler2.0是如何在图分类任务上应用。
图分类相比于之前的节点分类(比如前文的在有/无属性图、异质图上的应用)而言区别是,图分类研究的对象是每一张图,节点分类研究的对象是每一个节点。在图分类中,需要对每一张图学习出一个embedding向量,然后根据图的embedding来预测该图所对应的类别。因此,每一个batch中,处理的是多张图,而不是多个节点。图分类中所研究的图往往比较小,比如化合物网络,每一个图表示一个化合物中所包含的原子与原子之间的化学键关系。通过对于化合物网络的表示学习,可以刻画出该化合物本身的性质,获得的表示学习向量往往可以被用来预测该化合物对于某种疾病的治疗是否产生正向作用等
本章以图分类模型GatedGraph,多图数据集MUTAG为例,介绍如何利用Euler2.0构建图神经网络来解决图分类问题,完整的代码见这里。
Note:
在章会对相应的一些句子加粗或者在代码做注释来显式地区别与其他场景的应用的一些不同处理。与其他场景的联系和区别的详细对比见这里。
整体流程也是分为两步:
- 生成多图数据
- 对多图数据进行加载
与前文的在有/无属性图、异质图的数据准备不同的是,这里是要生成多张图数据。如何处理多个图数据一个简单的方法是,首先将数据集中所有的图都合到一张大图来,通过对大图中的每一个节点添加属于哪一张图的标识属性来区分不同图中的节点,然后与之前生成一张图数据的方式一样来处理这张大图。
由于要处理的原始数据MUTAG是多张图,一个简单的方法是将多张图合成一张大图,在生成大图的JSON文件的时候,在JSON中添加每一个节点属于哪一张图的标识属性(即graph_label)来区分不同图中的节点。同时生成Meta文件,定义基于graph_label来建立属性索引,以此来快速找到同一个图中的所有节点。完整的代码见这里。
def gen_graph_json(edge_list, indicator_list, graph_labels, node_labels):
graph = {}
graph['nodes'] = []
graph['edges'] = []
print("Total nodes: {}.".format(len(node_labels)))
for i in range(len(node_labels)):
node_id = i
node_label = [node_labels[i]]
node_graph_label = [graph_labels[indicator_list[i]]]
graph_indicator = str(indicator_list[i])
features = [{'name': 'f1',
'type': 'sparse',
'value': node_label},
{'name': 'label',
'type': 'dense',
'value': node_graph_label},
{'name': 'graph_label',
'type': 'binary',
'value': graph_indicator}]
node = {'id': node_id,
'type': node_labels[i],
'weight': 1,
'features': features}
graph['nodes'].append(node)
for one_edge in edge_list:
src = one_edge[0]
dst = one_edge[1]
edge = {'src': src,
'dst': dst,
'type': 0,
'weight': 1,
'features': []}
graph['edges'].append(edge)
return json.dumps(graph)
def convert2json(self, convert_dir, out_dir):
prefix = os.path.join(self.data_dir, 'MUTAG_')
adj_list = read_adj(prefix + 'A.txt')
graph_indicator = read_graph_indicator(prefix + 'graph_indicator.txt')
graph_label = read_graph_label(prefix + 'graph_labels.txt')
node_label = read_node_label(prefix + 'node_labels.txt')
self.generate_index_meta()
with open(out_dir, 'w') as out:
out.write(gen_graph_json(adj_list, graph_indicator,
graph_label, node_label))
with open(self.id_file, 'w') as id_out:
start_idx = int(self.total_size * self.train_rate)
for i in range(start_idx, self.total_size):
id_out.write(str(i) + '\n')
def generate_index_meta(self):
meta = {}
meta['node'] = {}
meta['node']['features'] = {}
meta['node']['features']['graph_label'] = \
"graph_label:string:uint64_t:hash_index"
meta['edge'] = {}
meta_json = json.dumps(meta)
print(meta_json)
with open(self.meta_file ,'w') as out:
out.write(meta_json)
与一张图的处理方式类似,这里利用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()
与一张图数据加载类似,多图数据生成之后,用户需要在训练的时候加载对应的数据,并在每个Batch获取具体的数据(详见这里),方式如下:
import tf_euler
#加载图数据
euler_graph = tf_euler.dataset.get_dataset('mutag')
euler_graph.load_graph()
#通过tf_euler.get_graph_by_label采样训练的图样本,生成Batch data,
def get_train_from_input(self, inputs, params):
inputs = tf_euler.sample_graph_label(inputs)
sample_graph = tf_euler.get_graph_by_label(inputs)
node_idx = sample_graph.values
node_graph_idx = sample_graph.indices[:, 0]
graph_label = self.get_graph_label(sample_graph)
graph_idx = inputs
return {'node_idx': node_idx,
'graph_label': graph_label,
'node_graph_idx': node_graph_idx,
'graph_idx': graph_idx}
在图分类任务中需要学习得到图的embedding,一个最常用的解法是首先学习得到图中每一个节点的embedding,然后通过readout(类似于pooling)的操作,将一个图中所有节点的embedding整合成图的embedding表达向量。
因此,图分类模型大体仍然与之前的GNN类算法类似(在Euler-2.0抽象成Message Passing接口范式),只不过多了一个pooling操作。
这里以GatedGraph为例,来介绍如何利用Euler2.0来实现一个图分类算法。
整体上分为两步:
1.实现GNN Encoder
2.实现GNN Model
对于图分类而言,与节点分类任务中的GNN算法一样(比如在无属性图,有属性图,异质图上的应用等等),首先构建GNN Encoder,可以通过继承BaseGNNNet基类实现。其作用与之前类似,这里的GNN Encoder仍然是针对节点粒度来进行多层图卷积的。
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
与之前类似,在实现自己的GraphEncoder的时候,用户需要继承这个BaseGNNNet类,并实现to_x函数来表示每一个节点H0层embedding的构建过程,这里是将节点的spares特征进行embedding后的结果作为H0层embedding。
class GNN(BaseGNNNet):
def __init__(self, conv, flow,
dims, fanouts, metapath,
feature_idx, feature_max_id,
add_self_loops=True):
super(GNN, self).__init__(conv=conv,
flow=flow,
dims=dims,
fanouts=fanouts,
metapath=metapath,
add_self_loops=add_self_loops)
if not isinstance(feature_idx, list):
feature_idx = [feature_idx]
self.feature_idx = feature_idx
self.encoder = \
tf_euler.utils.layers.SparseEmbedding(feature_max_id, dims[0])
def to_x(self, n_id):
x, = tf_euler.get_sparse_feature(n_id, self.feature_idx)
x = self.encoder(x)
return x
参数:
- conv:使用的卷积方法名称,参考message passing接口中的convolution
- flow:使用的子图抽样方法名称,参考message passing接口中的dataflow
- dims:一个列表,元素个数为[卷积层数+1],表示图卷积中每一个convolution的输出embedding维度和最后一个全链接层输出embedding的维度
- fanouts:一个列表,对graphsage类算法有效,元素个数为[卷积层数],表示每层子图采样中邻居采样的个数
- metapath:一个列表,元素个数为[卷积层数],表示每层子图采样的采样边类型
- feature_idx:一个列表,表示H0层使用的sparse feature名字集合
- feature_max_id:一个列表,表示H0层使用的sparse feature的最大值,和feature_idx一一对应
- add_self_loops:表示是否在子图采样的过程中添加自环
该示例中to_x函数定义了,H0层node的embedding为节点 sparse 特征的 embedding。
与节点分类任务中的GNN算法(比如在无属性图,有属性图,异质图上的应用等等)不同的是,在实现GNN模型的时候,不在继承基类SuperviseModel或者UnsuperviseModel,而是需要继承GraphModel(详见这里),具体来讲:
通过实现Graph Encoder,用户便可以得到每个节点图卷积后的embed向量。用户需要定义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
用户需要实现embed(),该方法用来定义节点embedding通过pooling生成图embedding过程。其中Euler2.0已经内置了多种graph pooling的方法,详见这里。
用户继承GraphModel之后,GraphModel自身已经分装了图分类的损失函数,因此不需要用户额外去定义损失,详见这里
class GatedGraph(GraphModel):
def __init__(self, dims, metapath, label_dim,
feature_idx, feature_max_id,
processing_steps=4,
lstm_layers=2):
super(GatedGraph, self).__init__(label_dim)
self.gnn = GNN('gated', 'full', dims, None, metapath,
feature_idx, feature_max_id)
self.pool = tf_euler.graph_pool.AttentionPool()
def embed(self, n_id, graph_index):
node_emb = self.gnn(n_id)
graph_emb = self.pool(node_emb, graph_index)
return graph_emb
euler_graph = tf_euler.dataset.get_dataset('mutag')
euler_graph.load_graph()
model = GatedGraph(dims, metapath,
euler_graph.num_classes,
euler_graph.sparse_fea_idx,
euler_graph.sparse_fea_max_id,
processing_steps=flags_obj.process_steps,
lstm_layers=flags_obj.lstm_layers)
Euler-2.0提供了NodeEstimator GraphEstimator EdgeEstimator类和相应接口(详见这里),方便用户快速的完成模型训练,预测,embedding导出任务。其中NodeEstimator为点分类模型,GraphEstimator为图分类模型,EdgeEstimator为边分类模型(link prediction任务)。
这里利用GraphEstimator来训练GatedGraph。
params = {
'num_classes': euler_graph.num_classes,
'optimizer': flags_obj.optimizer,
'learning_rate': flags_obj.learning_rate,
'log_steps': flags_obj.log_steps,
'label': ['label'],
'train_rate': euler_graph.train_rate,
'id_file': euler_graph.id_file,
'model_dir': flags_obj.model_dir,
'total_size': euler_graph.total_size,
'infer_dir': flags_obj.model_dir,
'batch_size': flags_obj.batch_size,
'total_step': num_steps}
config = tf.estimator.RunConfig(log_step_count_steps=None)
model_estimator = GraphEstimator(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!')