dice-group / Ontolearn

Ontolearn is an open-source software library for explainable structured machine learning in Python. It learns OWL class expressions from positive and negative examples.
https://ontolearn-docs-dice-group.netlify.app/index.html
MIT License
36 stars 9 forks source link

Mother example using father.owl - possible bug ? #426

Closed MagicFeeling closed 1 month ago

MagicFeeling commented 1 month ago

Thank you so much for this library. This seems to be really interesting.

I have taken a previous example (code below), which might have been removed from the repository and changed the learning problem to where Anna is the only positive class representing the "mother" instead of "father" which was the original example. One of the outputs I get is ∃ hasChild⁻.(∀ hasChild.(¬male)) which to me is "There exists someone who is a child of a person (has a parent) such that all the children of that person are female." From my understanding, this statement would be positive for Anna (Has a parent (markus), and that parent has only female as childs or actually one child which is Anna again) and negative for everyone else, yet, the reasoner classifies all individuals as negative. Could this be a bug or am I wrongly interpreting the expression.

Note that the example was published on a previous version of ontolearn. (See the commented imports). I had to change the imports as I am using the latest version of Ontolearn.

# from ontolearn.concept_learner import CELOE
# from ontolearn.knowledge_base import KnowledgeBase
# from ontolearn.learning_problem import PosNegLPStandard
# from ontolearn.search import EvoLearnerNode
# from owlapy.model import OWLClass, OWLClassAssertionAxiom, OWLNamedIndividual, IRI, OWLObjectProperty, OWLObjectPropertyAssertionAxiom
# from owlapy.render import DLSyntaxObjectRenderer
import os
from ontolearn.concept_learner import CELOE
from ontolearn.knowledge_base import KnowledgeBase
from ontolearn.learning_problem import PosNegLPStandard
from owlapy.class_expression import OWLClass 
from owlapy.iri import IRI
from owlapy.owl_property import OWLObjectProperty
from owlapy.owl_axiom import OWLClassAssertionAxiom, OWLObjectPropertyAssertionAxiom
from owlapy.owl_individual import OWLNamedIndividual

# (1) Load a knowledge graph.
kb = KnowledgeBase(path='father.owl')
# (2) Initialize a learner.
model = CELOE(knowledge_base=kb)
# (3) Define a description logic concept learning problem.
lp = PosNegLPStandard(neg={OWLNamedIndividual(IRI.create("http://example.com/father#stefan")),
                           OWLNamedIndividual(IRI.create("http://example.com/father#markus")),
                           OWLNamedIndividual(IRI.create("http://example.com/father#martin")),
                           OWLNamedIndividual(IRI.create("http://example.com/father#heinz")),
                           OWLNamedIndividual(IRI.create("http://example.com/father#michelle"))},
                      pos={OWLNamedIndividual(IRI.create("http://example.com/father#anna"))})
# (4) Learn description logic concepts best fitting (3).
dl_classifiers=model.fit(learning_problem=lp).best_hypotheses(2)

# (5) Inference over unseen individuals
namespace = 'http://example.com/father#'
# (6) New Individuals
julia = OWLNamedIndividual(IRI.create(namespace, 'julia'))
julian = OWLNamedIndividual(IRI.create(namespace, 'julian'))
thomas = OWLNamedIndividual(IRI.create(namespace, 'thomas'))
# (7) OWLClassAssertionAxiom  about (6)
male = OWLClass(IRI.create(namespace, 'male'))
female = OWLClass(IRI.create(namespace, 'female'))
axiom1 = OWLClassAssertionAxiom(individual=julia, class_expression=female)
axiom2 = OWLClassAssertionAxiom(individual=julian, class_expression=male)
axiom3 = OWLClassAssertionAxiom(individual=thomas, class_expression=male)
# (8) OWLObjectPropertyAssertionAxiom about (6)
has_child = OWLObjectProperty(IRI.create(namespace, 'hasChild'))
# Existing Individuals
anna = OWLNamedIndividual(IRI.create(namespace, 'anna'))
markus = OWLNamedIndividual(IRI.create(namespace, 'markus'))
michelle = OWLNamedIndividual(IRI.create(namespace, 'michelle'))
axiom4 = OWLObjectPropertyAssertionAxiom(subject=thomas, property_=has_child, object_=julian)
axiom5 = OWLObjectPropertyAssertionAxiom(subject=julia, property_=has_child, object_=julian)

# 4. Use loaded class expressions for predictions
predictions = model.predict(individuals=[julia, julian, thomas, anna, markus, michelle],
                            axioms=[axiom1, axiom2, axiom3, axiom4, axiom5],
                            hypotheses=dl_classifiers)
print(predictions)
Demirrr commented 1 month ago

Thank you for your interest @MagicFeeling !

from ontolearn.knowledge_base import KnowledgeBase
from owlapy.class_expression import OWLClass
from owlapy.iri import IRI
from owlapy.owl_property import OWLObjectProperty

from owlapy.class_expression import OWLObjectSomeValuesFrom, OWLObjectAllValuesFrom
from owlapy import owl_expression_to_dl

import ontolearn
import owlapy
print(ontolearn.__version__)
print(owlapy.__version__)

kb = KnowledgeBase(path='KGs/Family/father.owl')

forall_haschild_not_male = OWLObjectAllValuesFrom(
    property=OWLObjectProperty(IRI.create("http://example.com/father#", 'hasChild')),
    filler=OWLClass(IRI.create("http://example.com/father#", 'male')).get_object_complement_of())
assert owl_expression_to_dl(forall_haschild_not_male) == "∀ hasChild.(¬male)"

assert {i.str for i in kb.individuals(forall_haschild_not_male)} == {"http://example.com/father#heinz",
                                                                    "http://example.com/father#markus",
                                                                    "http://example.com/father#michelle"}
exist_inverse_haschild_forall_haschild_not_male = OWLObjectSomeValuesFrom(
    property=OWLObjectProperty(IRI.create("http://example.com/father#", 'hasChild')).get_inverse_property(),
    filler=forall_haschild_not_male)

assert owl_expression_to_dl(exist_inverse_haschild_forall_haschild_not_male) == "∃ hasChild⁻.(∀ hasChild.(¬male))"
assert {i.str for i in kb.individuals(exist_inverse_haschild_forall_haschild_not_male)} == {"http://example.com/father#anna"}

Hence, your assesment is correct. anna is the only instance of ∃ hasChild⁻.(∀ hasChild.(¬male))

Yet, by running your code snippet, we obtain the following output

          ∃ hasChild⁻.(∀ hasChild.(¬male))  female
julia                                  0.0     1.0
julian                                 0.0     0.0
thomas                                 0.0     0.0
anna                                   0.0     1.0
markus                                 0.0     0.0
michelle                               0.0     1.0

anna should be classifed as positive for ∃ hasChild⁻.(∀ hasChild.(¬male)) . Am I missing something @alkidbaci ?

versions ontolearn 0.7.2 owlapy 1.1.0

alkidbaci commented 1 month ago

Hello @MagicFeeling, your results were really intriguing so I took my time and investigated this a bit. First of all, your interpretation of the class expression is correct. The thing is that the method predict that you call, is using SyncReasoner to get the instances and apparently that reasoner is not identifying anna as an instance of ∃ hasChild⁻.(∀ hasChild.(¬male)).

The reason that anna is predicted in @Demirrr's example is because kb.individuals() by default uses another reasoner, called FastInstanceCheckerReasoner.

Now, this can indeed be considered a bug and I will take a better look on this and provide a fix, so thank you for pointing this out. In the meantime until we patch this, if you need a quick solution, I suggest using the FastInstanceCheckerReasoner.instance, which can be indirectly called by kb.individuals as in the Demir's example or if you prefer the predict method then the only suggestion I can make in this case, which i don't recommend, is to change the reasoner this method is using by editing your local ontolearn library.

For the second option you can edit this line to:

reasoner = FastInstanceCheckerReasoner(ontology, base_reasoner=OntologyReasoner(ontology))

Then if you run your script (with only 1 best hypothesis) you will get:

          ∃ hasChild⁻.(∀ hasChild.(¬male))
julia                                  0.0
julian                                 0.0
thomas                                 0.0
anna                                   1.0
markus                                 0.0
michelle                               0.0
Demirrr commented 1 month ago

Dear @MagicFeeling

with #430, the issue is solved! Thank you for opening this issue!