Skip to content

Commit

Permalink
Merge pull request #245 from dice-group/develop
Browse files Browse the repository at this point in the history
Preperation for the next release
  • Loading branch information
Demirrr authored Oct 16, 2022
2 parents e27cb6e + 3c24b5d commit 084d520
Show file tree
Hide file tree
Showing 19 changed files with 520 additions and 70 deletions.
49 changes: 41 additions & 8 deletions examples/concept_learning_with_ocel.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
import json
import os

from ontolearn import KnowledgeBase
from ontolearn.knowledge_base import KnowledgeBase
from ontolearn.concept_learner import OCEL
from ontolearn.learning_problem import PosNegLPStandard
from ontolearn.utils import setup_logging
from owlapy.model import OWLClass, IRI, OWLNamedIndividual

setup_logging()

try:
os.chdir("examples")
Expand All @@ -13,18 +18,46 @@
settings = json.load(json_file)

kb = KnowledgeBase(path=settings['data_path'])
model = OCEL(knowledge_base=kb, verbose=1)

for str_target_concept, examples in settings['problems'].items():
p = set(examples['positive_examples'])
n = set(examples['negative_examples'])
print('Target concept: ', str_target_concept)
concepts_to_ignore = set()
# lets inject more background info
if str_target_concept in ['Granddaughter', 'Aunt', 'Sister']:
concepts_to_ignore.update(
{'http://www.benchmark.org/family#Brother',
'Father', 'http://www.benchmark.org/family#Grandparent'}) # Use URI, or concept with length 1.
model.fit(pos=p, neg=n, ignore=concepts_to_ignore)
NS = 'http://www.benchmark.org/family#'
concepts_to_ignore = {
OWLClass(IRI(NS, 'Brother')),
OWLClass(IRI(NS, 'Sister')),
OWLClass(IRI(NS, 'Daughter')),
OWLClass(IRI(NS, 'Mother')),
OWLClass(IRI(NS, 'Grandmother')),
OWLClass(IRI(NS, 'Father')),
OWLClass(IRI(NS, 'Grandparent')),
OWLClass(IRI(NS, 'PersonWithASibling')),
OWLClass(IRI(NS, 'Granddaughter')),
OWLClass(IRI(NS, 'Son')),
OWLClass(IRI(NS, 'Child')),
OWLClass(IRI(NS, 'Grandson')),
OWLClass(IRI(NS, 'Grandfather')),
OWLClass(IRI(NS, 'Grandchild')),
OWLClass(IRI(NS, 'Parent')),
}
target_kb = kb.ignore_and_copy(ignored_classes=concepts_to_ignore)
else:
target_kb = kb

typed_pos = set(map(OWLNamedIndividual, map(IRI.create, p)))
typed_neg = set(map(OWLNamedIndividual, map(IRI.create, n)))
lp = PosNegLPStandard(pos=typed_pos, neg=typed_neg)

model = OCEL(knowledge_base=target_kb,
max_runtime=600,
max_num_of_concepts_tested=10_000_000_000,
iter_bound=10_000_000_000)
model.fit(lp)

model.save_best_hypothesis(n=3, path='Predictions_{0}'.format(str_target_concept))
hypotheses = model.best_hypotheses(n=1)
print(hypotheses[0])
hypotheses = model.best_hypotheses(n=3)
[print(_) for _ in hypotheses]
9 changes: 5 additions & 4 deletions ontolearn/concept_generator.py
Original file line number Diff line number Diff line change
Expand Up @@ -203,9 +203,9 @@ def most_general_object_properties(self, *, domain: OWLClassExpression, inverse:

func = self.get_object_property_ranges if inverse else self.get_object_property_domains

inds_domain = set(self._reasoner.instances(domain))
for prop in self._object_property_hierarchy.most_general_roles():
# Probably need to do this with instance checks
if domain.is_owl_thing() or domain == func(prop):
if domain.is_owl_thing() or inds_domain <= set(self._reasoner.instances(func(prop))):
yield prop

def _data_properties_for_domain(self, domain: OWLClassExpression, data_properties: Iterable[OWLDataProperty]) \
Expand All @@ -221,9 +221,10 @@ def _data_properties_for_domain(self, domain: OWLClassExpression, data_propertie
"""
assert isinstance(domain, OWLClassExpression)

inds_domain = set(self._reasoner.instances(domain))
for prop in data_properties:
# Probably need to do this with instance checks
if domain.is_owl_thing() or domain == self.get_data_property_domains(prop):
if domain.is_owl_thing() or inds_domain <= set(self._reasoner.instances(
self.get_data_property_domains(prop))):
yield prop

def most_general_data_properties(self, *, domain: OWLClassExpression) -> Iterable[OWLDataProperty]:
Expand Down
59 changes: 50 additions & 9 deletions ontolearn/concept_learner.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
from ontolearn.learning_problem import PosNegLPStandard, EncodedPosNegLPStandard
from ontolearn.metrics import Accuracy, F1
from ontolearn.refinement_operators import LengthBasedRefinement
from ontolearn.search import EvoLearnerNode, HeuristicOrderedNode, OENode, TreeNode, LengthOrderedNode, \
from ontolearn.search import EvoLearnerNode, HeuristicOrderedNode, LBLNode, OENode, TreeNode, LengthOrderedNode, \
QualityOrderedNode, RL_State, DRILLSearchTreePriorityQueue
from ontolearn.utils import oplogging, create_experiment_folder
from ontolearn.value_splitter import AbstractValueSplitter, BinningValueSplitter, EntropyValueSplitter
Expand Down Expand Up @@ -462,13 +462,40 @@ class OCEL(CELOE):

name = 'ocel_python'

def __init__(self, knowledge_base, quality_func=None, iter_bound=None, max_num_of_concepts_tested=None,
terminate_on_goal=None):
def __init__(self,
knowledge_base: KnowledgeBase,
refinement_operator: Optional[BaseRefinement[OENode]] = None,
quality_func: Optional[AbstractScorer] = None,
heuristic_func: Optional[AbstractHeuristic] = None,
terminate_on_goal: Optional[bool] = None,
iter_bound: Optional[int] = None,
max_num_of_concepts_tested: Optional[int] = None,
max_runtime: Optional[int] = None,
max_results: int = 10,
best_only: bool = False,
calculate_min_max: bool = True):

if heuristic_func is None:
heuristic_func = OCELHeuristic()

super().__init__(knowledge_base=knowledge_base,
refinement_operator=refinement_operator,
quality_func=quality_func,
heuristic_func=OCELHeuristic(),
heuristic_func=heuristic_func,
terminate_on_goal=terminate_on_goal,
iter_bound=iter_bound, max_num_of_concepts_tested=max_num_of_concepts_tested)
iter_bound=iter_bound,
max_num_of_concepts_tested=max_num_of_concepts_tested,
max_runtime=max_runtime,
max_results=max_results,
best_only=best_only,
calculate_min_max=calculate_min_max)

def make_node(self, c: OWLClassExpression, parent_node: Optional[OENode] = None, is_root: bool = False) -> OENode:
assert parent_node is None or isinstance(parent_node, LBLNode)
r = LBLNode(c, self.kb.concept_len(c), self.kb.individuals_set(c), parent_node=parent_node, is_root=is_root)
if parent_node is not None:
parent_node.add_child(r)
return r


class Drill(AbstractDrill, RefinementBasedConceptLearner):
Expand Down Expand Up @@ -1470,15 +1497,20 @@ def __build_toolbox(self) -> base.Toolbox:

def __set_splitting_values(self):
for p in self._dp_splits:
del self.pset.terminals[self._dp_to_prim_type[p]]
if len(self._dp_splits[p]) == 0:
if p in self.kb.get_numeric_data_properties():
self.pset.addTerminal(OWLLiteral(0), self._dp_to_prim_type[p],
name=owlliteral_to_primitive_string(OWLLiteral(0), p))
self._dp_splits[p].append(OWLLiteral(0))
else:
pass # TODO:

# Remove terminal for multiple fits, unfortunately there exists no better way in DEAP
# This removal is probably not needed, the important one is removal from the context below
self.pset.terminals.pop(self._dp_to_prim_type[p], None)
for split in self._dp_splits[p]:
self.pset.addTerminal(split, self._dp_to_prim_type[p], name=owlliteral_to_primitive_string(split, p))
terminal_name = owlliteral_to_primitive_string(split, p)
# Remove terminal for multiple fits, unfortunately there exists no better way in DEAP
self.pset.context.pop(terminal_name, None)
self.pset.addTerminal(split, self._dp_to_prim_type[p], name=terminal_name)

def register_op(self, alias: str, function: Callable, *args, **kargs):
self.toolbox.register(alias, function, *args, **kargs)
Expand Down Expand Up @@ -1562,4 +1594,13 @@ def _fitness_func(self, individual: Tree):

def clean(self):
self._result_population = None

# Resets classes if they already exist, names must match the ones that were created in the toolbox
try:
del creator.Fitness
del creator.Individual
del creator.Quality
except AttributeError:
pass

super().clean()
6 changes: 3 additions & 3 deletions ontolearn/heuristics.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ def apply(self, node, instances, learning_problem: EncodedPosNegUndLP):


class OCELHeuristic(AbstractHeuristic):
__slots__ = 'accuracy', 'gainBonusFactor', 'expansionPenaltyFactor'
__slots__ = 'accuracy_method', 'gainBonusFactor', 'expansionPenaltyFactor'

name: Final = 'OCEL_Heuristic'

Expand All @@ -103,12 +103,12 @@ def __init__(self, *, gainBonusFactor: float = 0.5,
self.gainBonusFactor = gainBonusFactor # called alpha in the paper and gainBonusFactor in the original code
self.expansionPenaltyFactor = expansionPenaltyFactor # called beta in the paper

def apply(self, node, instances, learning_problem: EncodedPosNegLPStandard):
def apply(self, node: LBLNode, instances, learning_problem: EncodedPosNegLPStandard):
assert isinstance(node, LBLNode), "OCEL Heuristic requires instances information of a node"

heuristic_val = 0
accuracy_gain = 0
_, accuracy = self.accuracy_method.score_elp(instances, learning_problem)
_, accuracy = self.accuracy_method.score_elp(node.individuals, learning_problem)

if node.parent_node is not None:
_, parent_accuracy = self.accuracy_method.score_elp(node.parent_node.individuals, learning_problem)
Expand Down
20 changes: 20 additions & 0 deletions ontolearn/knowledge_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -335,6 +335,26 @@ def all_individuals_set(self):
else:
return frozenset(self._ontology.individuals_in_signature())

def most_general_object_properties(self, *, domain: OWLClassExpression, inverse: bool = False) \
-> Iterable[OWLObjectProperty]:
assert isinstance(domain, OWLClassExpression)

func = self.get_object_property_ranges if inverse else self.get_object_property_domains

inds_domain = self.individuals_set(domain)
for prop in self._object_property_hierarchy.most_general_roles():
if domain.is_owl_thing() or inds_domain <= self.individuals_set(func(prop)):
yield prop

def _data_properties_for_domain(self, domain: OWLClassExpression, data_properties: Iterable[OWLDataProperty]) \
-> Iterable[OWLDataProperty]:
assert isinstance(domain, OWLClassExpression)

inds_domain = self.individuals_set(domain)
for prop in data_properties:
if domain.is_owl_thing() or inds_domain <= self.individuals_set(self.get_data_property_domains(prop)):
yield prop

def __repr__(self):
properties_count = iter_count(self.ontology().object_properties_in_signature()) + iter_count(
self.ontology().data_properties_in_signature())
Expand Down
8 changes: 4 additions & 4 deletions ontolearn/utils/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import pickle
import random
import time
from typing import Callable, Set, TypeVar
from typing import Callable, Set, TypeVar, Tuple

from ontolearn.utils.log_config import setup_logging # noqa: F401
from owlapy.model import OWLNamedIndividual, IRI, OWLClass, HasIRI
Expand Down Expand Up @@ -83,7 +83,7 @@ def apply_TSNE_on_df(df) -> None:
plt.show()


def balanced_sets(a: set, b: set) -> (set, set):
def balanced_sets(a: set, b: set) -> Tuple[Set, Set]:
"""
Balance given two sets through sampling without replacement.
Returned sets have the same length.
Expand All @@ -93,10 +93,10 @@ def balanced_sets(a: set, b: set) -> (set, set):
"""

if len(a) > len(b):
sampled_a = random.sample(a, len(b))
sampled_a = random.sample(list(a), len(b))
return set(sampled_a), b
elif len(b) > len(a):
sampled_b = random.sample(b, len(a))
sampled_b = random.sample(list(b), len(a))
return a, set(sampled_b)
else:
assert len(a) == len(b)
Expand Down
11 changes: 10 additions & 1 deletion ontolearn/value_splitter.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,9 @@ def _combine_values(self, a: Values, b: Values) -> Values:
else:
return a

def reset(self):
pass


class BinningValueSplitter(AbstractValueSplitter):
"""Calculate a number of bins of equal size as splits."""
Expand Down Expand Up @@ -112,15 +115,18 @@ class EntropyValueSplitter(AbstractValueSplitter):

def __init__(self, max_nr_splits: int = 2):
super().__init__(max_nr_splits)
self._prop_to_values = {}

def compute_splits_properties(self, reasoner: OWLReasoner, properties: List[OWLDataProperty],
pos: Set[OWLNamedIndividual] = None, neg: Set[OWLNamedIndividual] = None) \
-> Dict[OWLDataProperty, List[OWLLiteral]]:
assert pos is not None
assert neg is not None

self.reset()
properties = properties.copy()

dp_splits: Dict[OWLDataProperty, List[OWLLiteral]] = {}
self._prop_to_values = {}
for property_ in properties:
dp_splits[property_] = []
self._prop_to_values[property_] = IndividualValues(self._get_values_for_inds(reasoner, property_, pos),
Expand Down Expand Up @@ -221,3 +227,6 @@ def _get_values_for_inds(self, reasoner: OWLReasoner, property_: OWLDataProperty
except StopIteration:
pass
return inds_to_value

def reset(self):
self._prop_to_values = {}
2 changes: 1 addition & 1 deletion owlapy/fast_instance_checker.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ class OWLReasoner_FastInstanceChecker(OWLReasonerEx):
_negation_default: bool

def __init__(self, ontology: OWLOntology, base_reasoner: OWLReasoner, *,
property_cache=True, negation_default=False):
property_cache=True, negation_default=True):
"""Fast instance checker
Args:
Expand Down
30 changes: 18 additions & 12 deletions owlapy/owlready2/_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -229,22 +229,28 @@ def __init__(self, ontology: OWLOntology_Owlready2):
self._world = ontology._world

def data_property_domains(self, pe: OWLDataProperty, direct: bool = False) -> Iterable[OWLClassExpression]:
for ax in self.get_root_ontology().data_property_domain_axioms(pe):
yield ax.get_domain()
if not direct:
yield from self.super_classes(ax.get_domain())
domains = {d.get_domain() for d in self.get_root_ontology().data_property_domain_axioms(pe)}
# TODO: Remove if when super_classes is implemented for complex class expressions
super_domains = set(chain.from_iterable([self.super_classes(d) for d in domains if isinstance(d, OWLClass)]))
yield from domains - super_domains
if not direct:
yield from super_domains

def object_property_domains(self, pe: OWLObjectProperty, direct: bool = False) -> Iterable[OWLClassExpression]:
for ax in self.get_root_ontology().object_property_domain_axioms(pe):
yield ax.get_domain()
if not direct:
yield from self.super_classes(ax.get_domain())
domains = {d.get_domain() for d in self.get_root_ontology().object_property_domain_axioms(pe)}
# TODO: Remove if when super_classes is implemented for complex class expressions
super_domains = set(chain.from_iterable([self.super_classes(d) for d in domains if isinstance(d, OWLClass)]))
yield from domains - super_domains
if not direct:
yield from super_domains

def object_property_ranges(self, pe: OWLObjectProperty, direct: bool = False) -> Iterable[OWLClassExpression]:
for ax in self.get_root_ontology().object_property_range_axioms(pe):
yield ax.get_range()
if not direct:
yield from self.super_classes(ax.get_range())
ranges = {r.get_range() for r in self.get_root_ontology().object_property_range_axioms(pe)}
# TODO: Remove if when super_classes is implemented for complex class expressions
super_ranges = set(chain.from_iterable([self.super_classes(d) for d in ranges if isinstance(d, OWLClass)]))
yield from ranges - super_ranges
if not direct:
yield from super_ranges

def equivalent_classes(self, ce: OWLClassExpression) -> Iterable[OWLClass]:
"""Return the named classes that are directly equivalent to the class expression"""
Expand Down
3 changes: 3 additions & 0 deletions owlapy/owlready2/axioms.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ def _(axiom: OWLDeclarationAxiom, ontology: OWLOntology, world: owlready2.namesp

thing_x: owlready2.entity.ThingClass = conv.map_concept(OWLThing)
if isinstance(entity, OWLClass):
if entity.is_owl_thing() or entity.is_owl_nothing():
return
entity_x = types.new_class(name=entity.get_iri().get_remainder(), bases=(thing_x,))
elif isinstance(entity, OWLIndividual):
entity_x = thing_x(entity.get_iri().get_remainder())
Expand All @@ -53,6 +55,7 @@ def _(axiom: OWLDeclarationAxiom, ontology: OWLOntology, world: owlready2.namesp
else:
raise ValueError(f'Cannot add ({entity}). Not an atomic class, property, or individual.')
entity_x.namespace = ont_x.get_namespace(entity.get_iri().get_namespace())
entity_x.namespace.world._refactor(entity_x.storid, entity_x.iri)


@_add_axiom.register
Expand Down
4 changes: 2 additions & 2 deletions owlapy/parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -722,9 +722,9 @@ def visit_facet(self, node, children) -> OWLFacet:
def visit_class_iri(self, node, children) -> OWLClass:
top_bottom = _node_text(node)
if top_bottom == _DL_SYNTAX.TOP:
return OWLRDFVocabulary.OWL_THING.get_iri()
return OWLClass(OWLRDFVocabulary.OWL_THING.get_iri())
elif top_bottom == _DL_SYNTAX.BOTTOM:
return OWLRDFVocabulary.OWL_NOTHING.get_iri()
return OWLClass(OWLRDFVocabulary.OWL_NOTHING.get_iri())
else:
return OWLClass(children[0])

Expand Down
Loading

0 comments on commit 084d520

Please sign in to comment.