diff --git a/kglib/kgcn/examples/diagnosis/diagnosis.py b/kglib/kgcn/examples/diagnosis/diagnosis.py index e2c20d00..319d12d9 100644 --- a/kglib/kgcn/examples/diagnosis/diagnosis.py +++ b/kglib/kgcn/examples/diagnosis/diagnosis.py @@ -19,14 +19,15 @@ import copy import inspect +import time from grakn.client import GraknClient from kglib.kgcn.pipeline.pipeline import pipeline from kglib.utils.grakn.synthetic.examples.diagnosis.generate import generate_example_graphs -from kglib.utils.graph.thing.queries_to_graph import build_graph_from_queries -from kglib.utils.graph.query.query_graph import QueryGraph from kglib.utils.graph.iterate import multidigraph_data_iterator +from kglib.utils.graph.query.query_graph import QueryGraph +from kglib.utils.graph.thing.queries_to_graph import build_graph_from_queries def diagnosis_example(num_graphs=200, @@ -58,8 +59,9 @@ def diagnosis_example(num_graphs=200, num_processing_steps_tr=num_processing_steps_tr, num_processing_steps_ge=num_processing_steps_ge, num_training_iterations=num_training_iterations, + continuous_attributes=CONTINUOUS_ATTRIBUTES, categorical_attributes=CATEGORICAL_ATTRIBUTES, - ) + output_dir=f"./events/{time.time()}/") with session.transaction().write() as tx: write_predictions_to_grakn(ge_graphs, tx) @@ -71,6 +73,7 @@ def diagnosis_example(num_graphs=200, CATEGORICAL_ATTRIBUTES = {'name': ['meningitis', 'flu', 'fever', 'light-sensitivity']} +CONTINUOUS_ATTRIBUTES = {'severity': (0, 1)} def create_concept_graphs(example_indices, grakn_session): @@ -119,18 +122,19 @@ def diagnosis_query(self, example_id): $p isa person, has example-id {example_id}; $s isa symptom, has name $sn; $d isa disease, has name $dn; - $sp(presented-symptom: $s, symptomatic-patient: $p) isa symptom-presentation; + $sp(presented-symptom: $s, symptomatic-patient: $p) isa symptom-presentation, has severity $sev; $c(cause: $d, effect: $s) isa causality; $diag(patient: $p, diagnosed-disease: $d) isa diagnosis; get;''') def base_query_graph(self): - p, s, sn, d, dn, sp, c = 'p', 's', 'sn', 'd', 'dn', 'sp', 'c' + vars = p, s, sn, d, dn, sp, sev, c = 'p', 's', 'sn', 'd', 'dn', 'sp', 'sev', 'c' g = QueryGraph() - g.add_vars(p, s, sn, d, dn, sp, c, **PREEXISTS) + g.add_vars(*vars, **PREEXISTS) g.add_has_edge(s, sn, **PREEXISTS) g.add_has_edge(d, dn, **PREEXISTS) g.add_role_edge(sp, s, 'presented-symptom', **PREEXISTS) + g.add_has_edge(sp, sev, **PREEXISTS) g.add_role_edge(sp, p, 'symptomatic-patient', **PREEXISTS) g.add_role_edge(c, s, 'effect', **PREEXISTS) g.add_role_edge(c, d, 'cause', **PREEXISTS) @@ -153,7 +157,7 @@ def candidate_diagnosis_query(self, example_id): $p isa person, has example-id {example_id}; $s isa symptom, has name $sn; $d isa disease, has name $dn; - $sp(presented-symptom: $s, symptomatic-patient: $p) isa symptom-presentation; + $sp(presented-symptom: $s, symptomatic-patient: $p) isa symptom-presentation, has severity $sev; $c(cause: $d, effect: $s) isa causality; $diag(candidate-patient: $p, candidate-diagnosed-disease: $d) isa candidate-diagnosis; get;''') diff --git a/kglib/kgcn/learn/learn.py b/kglib/kgcn/learn/learn.py index 713de384..f53757fb 100644 --- a/kglib/kgcn/learn/learn.py +++ b/kglib/kgcn/learn/learn.py @@ -70,20 +70,36 @@ def __call__(self, loss_ops_tr = loss_ops_preexisting_no_penalty(target_ph, output_ops_tr) # Loss across processing steps. loss_op_tr = sum(loss_ops_tr) / self._num_processing_steps_tr + + tf.summary.scalar('loss_op_tr', loss_op_tr) # Test/generalization loss. loss_ops_ge = loss_ops_preexisting_no_penalty(target_ph, output_ops_ge) loss_op_ge = loss_ops_ge[-1] # Loss from final processing step. + tf.summary.scalar('loss_op_ge', loss_op_ge) # Optimizer optimizer = tf.train.AdamOptimizer(learning_rate) - step_op = optimizer.minimize(loss_op_tr) + gradients, variables = zip(*optimizer.compute_gradients(loss_op_tr)) + + for grad, var in zip(gradients, variables): + try: + print(var.name) + tf.summary.histogram('gradients/' + var.name, grad) + except: + pass + + gradients, _ = tf.clip_by_global_norm(gradients, 5.0) + step_op = optimizer.apply_gradients(zip(gradients, variables)) input_ph, target_ph = make_all_runnable_in_session(input_ph, target_ph) sess = tf.Session() + merged_summaries = tf.summary.merge_all() + + train_writer = None if log_dir is not None: - tf.summary.FileWriter(log_dir, sess.graph) + train_writer = tf.summary.FileWriter(log_dir, sess.graph) sess.run(tf.global_variables_initializer()) @@ -105,16 +121,22 @@ def __call__(self, start_time = time.time() for iteration in range(num_training_iterations): feed_dict = create_feed_dict(input_ph, target_ph, tr_input_graphs, tr_target_graphs) - train_values = sess.run( - { - "step": step_op, - "target": target_ph, - "loss": loss_op_tr, - "outputs": output_ops_tr - }, - feed_dict=feed_dict) if iteration % log_every_epochs == 0: + + train_values = sess.run( + { + "step": step_op, + "target": target_ph, + "loss": loss_op_tr, + "outputs": output_ops_tr, + "summary": merged_summaries + }, + feed_dict=feed_dict) + + if train_writer is not None: + train_writer.add_summary(train_values["summary"], iteration) + feed_dict = create_feed_dict(input_ph, target_ph, ge_input_graphs, ge_target_graphs) test_values = sess.run( { @@ -140,6 +162,15 @@ def __call__(self, " {:.4f}, Cge {:.4f}, Sge {:.4f}".format( iteration, elapsed, train_values["loss"], test_values["loss"], correct_tr, solved_tr, correct_ge, solved_ge)) + else: + train_values = sess.run( + { + "step": step_op, + "target": target_ph, + "loss": loss_op_tr, + "outputs": output_ops_tr + }, + feed_dict=feed_dict) training_info = logged_iterations, losses_tr, losses_ge, corrects_tr, corrects_ge, solveds_tr, solveds_ge return train_values, test_values, training_info diff --git a/kglib/kgcn/models/attribute.py b/kglib/kgcn/models/attribute.py index 51aa47f9..a9a928d0 100644 --- a/kglib/kgcn/models/attribute.py +++ b/kglib/kgcn/models/attribute.py @@ -17,31 +17,57 @@ # under the License. # +import abc +from functools import partial + import sonnet as snt import tensorflow as tf -class CategoricalAttribute(snt.AbstractModule): +class Attribute(snt.AbstractModule, abc.ABC): + """ + Abstract base class for Attribute value embedding models + """ + def __init__(self, attr_embedding_dim, name='AttributeEmbedder'): + super(Attribute, self).__init__(name=name) + self._attr_embedding_dim = attr_embedding_dim + + +class ContinuousAttribute(Attribute): + def __init__(self, attr_embedding_dim, name='ContinuousAttributeEmbedder'): + super(ContinuousAttribute, self).__init__(attr_embedding_dim, name=name) + + def _build(self, attribute_value): + tf.summary.histogram('cont_attribute_value_histogram', attribute_value) + embedding = snt.Sequential([ + snt.nets.MLP([self._attr_embedding_dim] * 3, activate_final=True, use_dropout=True), + snt.LayerNorm(), + ])(tf.cast(attribute_value, dtype=tf.float32)) + tf.summary.histogram('cont_embedding_histogram', embedding) + return embedding + + +class CategoricalAttribute(Attribute): def __init__(self, num_categories, attr_embedding_dim, name='CategoricalAttributeEmbedder'): - super(CategoricalAttribute, self).__init__(name=name) + super(CategoricalAttribute, self).__init__(attr_embedding_dim, name=name) - self._attr_embedding_dim = attr_embedding_dim self._num_categories = num_categories - def _build(self, inputs): - int_inputs = tf.cast(inputs, dtype=tf.int32) - embedding = snt.Embed(self._num_categories, self._attr_embedding_dim)(int_inputs) + def _build(self, attribute_value): + int_attribute_value = tf.cast(attribute_value, dtype=tf.int32) + tf.summary.histogram('cat_attribute_value_histogram', int_attribute_value) + embedding = snt.Embed(self._num_categories, self._attr_embedding_dim)(int_attribute_value) + tf.summary.histogram('cat_embedding_histogram', embedding) return tf.squeeze(embedding, axis=1) -class BlankAttribute(snt.AbstractModule): +class BlankAttribute(Attribute): def __init__(self, attr_embedding_dim, name='BlankAttributeEmbedder'): - super(BlankAttribute, self).__init__(name=name) - self._attr_embedding_dim = attr_embedding_dim + super(BlankAttribute, self).__init__(attr_embedding_dim, name=name) - def _build(self, features): - shape = tf.stack([tf.shape(features)[0], self._attr_embedding_dim]) + def _build(self, attribute_value): + shape = tf.stack([tf.shape(attribute_value)[0], self._attr_embedding_dim]) encoded_features = tf.zeros(shape, dtype=tf.float32) return encoded_features diff --git a/kglib/kgcn/models/embedding.py b/kglib/kgcn/models/embedding.py index 511cba78..4df818fa 100644 --- a/kglib/kgcn/models/embedding.py +++ b/kglib/kgcn/models/embedding.py @@ -25,13 +25,17 @@ def common_embedding(features, num_types, type_embedding_dim): preexistance_feat = tf.expand_dims(tf.cast(features[:, 0], dtype=tf.float32), axis=1) type_embedder = snt.Embed(num_types, type_embedding_dim) - type_embedding = type_embedder(tf.cast(features[:, 1], tf.int32)) + norm = snt.LayerNorm() + type_embedding = norm(type_embedder(tf.cast(features[:, 1], tf.int32))) + tf.summary.histogram('type_embedding_histogram', type_embedding) return tf.concat([preexistance_feat, type_embedding], axis=1) def attribute_embedding(features, attr_encoders, attr_embedding_dim): typewise_attribute_encoder = TypewiseEncoder(attr_encoders, attr_embedding_dim) - return typewise_attribute_encoder(features[:, 1:]) + attr_embedding = typewise_attribute_encoder(features[:, 1:]) + tf.summary.histogram('attribute_embedding_histogram', attr_embedding) + return attr_embedding def node_embedding(features, num_types, type_embedding_dim, attr_encoders, attr_embedding_dim): diff --git a/kglib/kgcn/models/embedding_test.py b/kglib/kgcn/models/embedding_test.py index ac847f10..5fd6ac82 100644 --- a/kglib/kgcn/models/embedding_test.py +++ b/kglib/kgcn/models/embedding_test.py @@ -28,8 +28,10 @@ class TestCommonEmbedding(unittest.TestCase): - def test_embedding_output_shape_as_expected(self): + def setUp(self): tf.enable_eager_execution() + + def test_embedding_output_shape_as_expected(self): features = np.array([[1, 0, 0.7], [1, 2, 0.7], [0, 1, 0.5]], dtype=np.float32) type_embedding_dim = 5 output = common_embedding(features, 3, type_embedding_dim) @@ -38,11 +40,13 @@ def test_embedding_output_shape_as_expected(self): class TestAttributeEmbedding(unittest.TestCase): + def setUp(self): + tf.enable_eager_execution() def test_embedding_is_typewise(self): features = np.array([[1, 0, 0.7], [1, 2, 0.7], [0, 1, 0.5]]) - mock_instance = Mock() + mock_instance = Mock(return_value=tf.convert_to_tensor(np.array([[1, 0.7], [1, 0.7], [0, 0.5]]))) mock = Mock(return_value=mock_instance) patcher = patch('kglib.kgcn.models.embedding.TypewiseEncoder', spec=True, new=mock) mock_class = patcher.start() @@ -62,9 +66,10 @@ def test_embedding_is_typewise(self): class TestNodeEmbedding(unittest.TestCase): - def test_embedding_is_typewise(self): + def setUp(self): tf.enable_eager_execution() + def test_embedding_is_typewise(self): features = Mock() num_types = Mock() type_embedding_dim = Mock() diff --git a/kglib/kgcn/models/typewise.py b/kglib/kgcn/models/typewise.py index 26b5a3aa..3c2614df 100644 --- a/kglib/kgcn/models/typewise.py +++ b/kglib/kgcn/models/typewise.py @@ -52,6 +52,8 @@ def __init__(self, encoders_for_types, feature_length, name="typewise_encoder"): def _build(self, features): + tf.summary.histogram('typewise_encoder_features_histogram', features) + shape = tf.stack([tf.shape(features)[0], self._feature_length]) encoded_features = tf.zeros(shape, dtype=tf.float32) @@ -69,9 +71,12 @@ def _build(self, features): # Use this encoder when the feat_type matches any of the types applicable_types_mask = tf.reduce_any(elementwise_equality, axis=1) indices_to_encode = tf.where(applicable_types_mask) + feats_to_encode = tf.squeeze(tf.gather(features[:, 1:], indices_to_encode), axis=1) encoded_feats = encoder()(feats_to_encode) encoded_features += tf.scatter_nd(tf.cast(indices_to_encode, dtype=tf.int32), encoded_feats, shape) + tf.summary.histogram('typewise_encoder_encoded_features_histogram', encoded_features) + return encoded_features diff --git a/kglib/kgcn/pipeline/pipeline.py b/kglib/kgcn/pipeline/pipeline.py index 50bb1019..ecc725df 100644 --- a/kglib/kgcn/pipeline/pipeline.py +++ b/kglib/kgcn/pipeline/pipeline.py @@ -22,12 +22,13 @@ from graph_nets.utils_np import graphs_tuple_to_networkxs from kglib.kgcn.learn.learn import KGCNLearner -from kglib.kgcn.models.attribute import CategoricalAttribute, BlankAttribute +from kglib.kgcn.models.attribute import ContinuousAttribute, CategoricalAttribute, BlankAttribute from kglib.kgcn.models.core import softmax, KGCN from kglib.kgcn.pipeline.encode import encode_types, create_input_graph, create_target_graph from kglib.kgcn.pipeline.utils import apply_logits_to_graphs, duplicate_edges_in_reverse from kglib.kgcn.plot.plotting import plot_across_training, plot_predictions -from kglib.utils.graph.iterate import multidigraph_node_data_iterator, multidigraph_data_iterator +from kglib.utils.graph.iterate import multidigraph_node_data_iterator, multidigraph_data_iterator, \ + multidigraph_edge_data_iterator def pipeline(graphs, @@ -37,11 +38,13 @@ def pipeline(graphs, num_processing_steps_tr=10, num_processing_steps_ge=10, num_training_iterations=10000, + continuous_attributes=None, categorical_attributes=None, type_embedding_dim=5, attr_embedding_dim=6, edge_output_size=3, - node_output_size=3): + node_output_size=3, + output_dir=None): ############################################################ # Manipulate the graph data @@ -49,17 +52,23 @@ def pipeline(graphs, # Encode attribute values for graph in graphs: - - for data in multidigraph_data_iterator(graph): - data['encoded_value'] = 0 - for node_data in multidigraph_node_data_iterator(graph): typ = node_data['type'] - # Add the integer value of the category for each categorical attribute instance - for attr_typ, category_values in categorical_attributes.items(): - if typ == attr_typ: - node_data['encoded_value'] = category_values.index(node_data['value']) + if categorical_attributes is not None and typ in categorical_attributes.keys(): + # Add the integer value of the category for each categorical attribute instance + category_values = categorical_attributes[typ] + node_data['encoded_value'] = category_values.index(node_data['value']) + + elif continuous_attributes is not None and typ in continuous_attributes.keys(): + min_val, max_val = continuous_attributes[typ] + node_data['encoded_value'] = (node_data['value'] - min_val) / (max_val - min_val) + + else: + node_data['encoded_value'] = 0 + + for edge_data in multidigraph_edge_data_iterator(graph): + edge_data['encoded_value'] = 0 indexed_graphs = [nx.convert_node_labels_to_integers(graph, label_attribute='concept') for graph in graphs] graphs = [duplicate_edges_in_reverse(graph) for graph in indexed_graphs] @@ -78,29 +87,7 @@ def pipeline(graphs, # Build and run the KGCN ############################################################ - type_categories_list = [i for i, _ in enumerate(node_types)] - non_attribute_nodes = type_categories_list.copy() - - attr_embedders = dict() - - # Construct categorical attribute embedders - for attr_typ, category_values in categorical_attributes.items(): - num_categories = len(category_values) - - def make_embedder(): - return CategoricalAttribute(num_categories, attr_embedding_dim, name=attr_typ + '_cat_embedder') - attr_typ_index = node_types.index(attr_typ) - - # Record the embedder, and the index of the type that it should encode - attr_embedders[make_embedder] = [attr_typ_index] - - non_attribute_nodes.pop(attr_typ_index) - - # All entities and relations (non-attributes) also need an embedder with matching output dimension, which does - # nothing. This is provided as a list of their indices - def make_blank_embedder(): - return BlankAttribute(attr_embedding_dim) - attr_embedders[make_blank_embedder] = non_attribute_nodes + attr_embedders = configure_embedders(node_types, attr_embedding_dim, categorical_attributes, continuous_attributes) kgcn = KGCN(len(node_types), len(edge_types), @@ -118,10 +105,11 @@ def make_blank_embedder(): tr_target_graphs, ge_input_graphs, ge_target_graphs, - num_training_iterations=num_training_iterations) + num_training_iterations=num_training_iterations, + log_dir=output_dir) - plot_across_training(*tr_info) - plot_predictions(ge_input_graphs, test_values, num_processing_steps_ge) + plot_across_training(*tr_info, output_file=f'{output_dir}learning.png') + plot_predictions(ge_input_graphs, test_values, num_processing_steps_ge, output_file=f'{output_dir}graph.png') logit_graphs = graphs_tuple_to_networkxs(test_values["outputs"][-1]) @@ -136,3 +124,57 @@ def make_blank_embedder(): _, _, _, _, _, solveds_tr, solveds_ge = tr_info return ge_graphs, solveds_tr, solveds_ge + + +def configure_embedders(node_types, attr_embedding_dim, categorical_attributes, continuous_attributes): + + def construct_embedder_funcs(node_types, attribute_config, embedder_func): + + attr_embedders = dict() + + # Construct attribute embedders + for attribute_type, attribute_props in attribute_config.items(): + + attr_typ_index = node_types.index(attribute_type) + + # Record the embedder, and the index of the type that it should encode + attr_embedders[embedder_func(attribute_type, attribute_props)] = [attr_typ_index] + + return attr_embedders + + attr_embedders = dict() + + if categorical_attributes is not None: + + def embedder_func(attribute_type, category_values): + def make_embedder(): + return CategoricalAttribute(len(category_values), attr_embedding_dim, + name=attribute_type + '_cat_embedder') + return make_embedder + + attr_embedders.update(construct_embedder_funcs(node_types, categorical_attributes, embedder_func)) + + if continuous_attributes is not None: + + def embedder_func(attribute_type, _): + def make_embedder(): + return ContinuousAttribute(attr_embedding_dim, name=attribute_type + '_cat_embedder') + return make_embedder + + attr_embedders.update(construct_embedder_funcs(node_types, continuous_attributes, embedder_func)) + + attribute_nodes = [l for el in list(attr_embedders.values()) for l in el] + + non_attribute_nodes = [] + for i, _ in enumerate(node_types): + if i not in attribute_nodes: + non_attribute_nodes.append(i) + + # All entities and relations (non-attributes) also need an embedder with matching output dimension, which does + # nothing. This is provided as a list of their indices + def make_blank_embedder(): + return BlankAttribute(attr_embedding_dim) + + if len(non_attribute_nodes) > 0: + attr_embedders[make_blank_embedder] = non_attribute_nodes + return attr_embedders diff --git a/kglib/kgcn/pipeline/pipeline_test.py b/kglib/kgcn/pipeline/pipeline_test.py new file mode 100644 index 00000000..b9703192 --- /dev/null +++ b/kglib/kgcn/pipeline/pipeline_test.py @@ -0,0 +1,53 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +# +import unittest + +from kglib.kgcn.pipeline.pipeline import configure_embedders + + +class TestConfigureEmbedders(unittest.TestCase): + + def test_all_types_encoded(self): + node_types = ['a', 'b', 'c'] + attr_embedding_dim = 5 + categorical_attributes = {'a': ['option1', 'option2']} + continuous_attributes = {'b': (0, 1)} + attr_embedders = configure_embedders(node_types, attr_embedding_dim, categorical_attributes, continuous_attributes) + all_types = [l for el in list(attr_embedders.values()) for l in el] + + expected_types = [0, 1, 2] + + self.assertListEqual(expected_types, all_types) + + def test_multiple_categorical_embedders(self): + node_types = ['a', 'b', 'c'] + attr_embedding_dim = 5 + categorical_attributes = {'a': ['option1', 'option2'], 'c': ['option3', 'option4']} + continuous_attributes = {'b': (0, 1)} + attr_embedders = configure_embedders(node_types, attr_embedding_dim, categorical_attributes, continuous_attributes) + all_types = [l for el in list(attr_embedders.values()) for l in el] + all_types.sort() + + expected_types = [0, 1, 2] + print(attr_embedders) + + self.assertListEqual(expected_types, all_types) + + for types in attr_embedders.values(): + self.assertNotEqual(types, []) diff --git a/kglib/utils/grakn/synthetic/examples/diagnosis/generate.py b/kglib/utils/grakn/synthetic/examples/diagnosis/generate.py index 401cad68..7ebc8b6d 100644 --- a/kglib/utils/grakn/synthetic/examples/diagnosis/generate.py +++ b/kglib/utils/grakn/synthetic/examples/diagnosis/generate.py @@ -18,13 +18,11 @@ # import inspect -import os import numpy as np from grakn.client import GraknClient from kglib.utils.grakn.synthetic.statistics.pmf import PMF -import subprocess as sp def get_example_queries(pmf, example_id): @@ -49,21 +47,21 @@ def get_example_queries(pmf, example_id): (patient: $p, diagnosed-disease: $d) isa diagnosis;''')) - if variable_values['Light Sensitivity']: + if variable_values['Light Sensitivity'] is not False: queries.append(inspect.cleandoc(f'''match $p isa person, has example-id {example_id}; $s isa symptom, has name "light-sensitivity"; insert (presented-symptom: $s, symptomatic-patient: $p) isa - symptom-presentation;''')) + symptom-presentation, has severity {variable_values['Light Sensitivity']()};''')) - if variable_values['Fever']: + if variable_values['Fever'] is not False: queries.append(inspect.cleandoc(f'''match $p isa person, has example-id {example_id}; $s isa symptom, has name "fever"; insert (presented-symptom: $s, symptomatic-patient: $p) isa - symptom-presentation;''')) + symptom-presentation, has severity {variable_values['Fever']()};''')) return queries @@ -82,11 +80,14 @@ def generate_example_graphs(num_examples, keyspace="diagnosis", uri="localhost:4 pmf_array[0, 1, 1, 1] = 0.3 pmf_array[1, 0, 1, 1] = 0.05 + def normal_dist(mean, var): + return lambda: round(np.random.normal(mean, var, 1)[0], 2) + pmf = PMF({ 'Flu': [False, True], 'Meningitis': [False, True], - 'Light Sensitivity': [False, True], - 'Fever': [False, True] + 'Light Sensitivity': [False, normal_dist(0.3, 0.1)], + 'Fever': [False, normal_dist(0.5, 0.2)] }, pmf_array, seed=0) print(pmf.to_dataframe()) diff --git a/kglib/utils/grakn/synthetic/examples/diagnosis/schema.gql b/kglib/utils/grakn/synthetic/examples/diagnosis/schema.gql index 51340584..a5c13acc 100644 --- a/kglib/utils/grakn/synthetic/examples/diagnosis/schema.gql +++ b/kglib/utils/grakn/synthetic/examples/diagnosis/schema.gql @@ -6,6 +6,9 @@ example-id sub attribute, name sub attribute, datatype string; +severity sub attribute, + datatype double; + person sub entity, key example-id, plays patient, @@ -64,6 +67,7 @@ symptom sub entity, plays effect; symptom-presentation sub relation, + has severity, relates presented-symptom, relates symptomatic-patient;