ontolearn.lp_generator.complexity_lp_generator

Goal-directed, complexity-controlled learning-problem generation.

ontolearn.lp_generator.LPGen/KB2Data synthesize learning problems bottom-up: they refine owl:Thing with ExpressRefinement and keep whatever non-redundant concepts fall out, sorted by length only as a dedup tie-break. There is no way to ask for “a learning problem whose target uses exactly a nested existential restriction and an object intersection, depth <= 3”.

This module instead samples a target OWLClassExpression top-down from an explicit DL-construct grammar (ComplexityProfile): the caller names which constructors are allowed (intersection, union, existential/universal restriction, cardinality restrictions, has-value, negation, inverse roles) and bounds on length/depth, and the generator recursively builds concepts that respect that budget. Each candidate is turned into a learning problem via reasoner-based retrieval (kb.individuals_set).

Candidates are additionally ranked by an explicit difficulty score (DifficultyWeights) combining four signals: normalized target length/depth, how far the best “cheap” one-step hypothesis (an atomic class, its negation, or a bare exists r.Top) falls short of solving the LP outright, and the density of near-miss negatives – individuals that satisfy the target with exactly one top-level conjunct dropped (e.g. the right class but the wrong relation), which is exactly where a greedy top-down learner is most likely to overgeneralize. generate() oversamples candidates and keeps the hardest num_lps of them, and biases the negative sample towards near-misses via hard_negative_ratio, so the returned problems are pointed at the learner’s actual failure modes rather than generic random negatives.

The grammar and probes are built purely from the knowledge base’s own signature (kb.ontology.classes_in_signature(), kb.get_object_properties()), so the same ComplexityProfile curriculum runs unmodified across knowledge graphs with very different shapes – e.g. Family, Mutagenesis, Carcinogenesis, Biopax, Lymphography. Relation-based constructs (∃/∀/cardinality/has-value) are silently dropped from the grammar on a KB that exposes no object properties (e.g. Lymphography) rather than raising, so a curriculum degrades gracefully instead of crashing on such KBs.

A ComplexityProfile curriculum (e.g. atomic -> conjunctive -> nested existential -> cardinality -> full ALCQ) can then be run through GoalDirectedLPGenerator.generate_benchmark() to build a graded benchmark suite, e.g. to measure how a learner’s F1 degrades as target-concept complexity grows – see examples/benchmark_complexity_lp_generator_with_tdl.py.

Classes

DLConstruct

DL constructors a ComplexityProfile can allow/forbid.

DifficultyWeights

Weights combining four difficulty signals into one score in (roughly) [0, 1].

ComplexityProfile

A named point (or curriculum step) in the space of "how complex should the target be".

GeneratedLearningProblem

A goal-directed learning problem: a sampled target concept plus its (E+, E-).

GoalDirectedLPGenerator

Samples target concepts from an explicit DL-construct grammar and turns them into LPs.

Functions

save_benchmark(→ None)

Serialize a benchmark (as returned by GoalDirectedLPGenerator.generate_benchmark())

Module Contents

class ontolearn.lp_generator.complexity_lp_generator.DLConstruct(*args, **kwds)[source]

Bases: enum.Enum

DL constructors a ComplexityProfile can allow/forbid.

ATOMIC = 'atomic'
NEGATION = 'negation'
INTERSECTION = 'intersection'
UNION = 'union'
EXISTENTIAL = 'existential'
UNIVERSAL = 'universal'
MIN_CARDINALITY = 'min_cardinality'
MAX_CARDINALITY = 'max_cardinality'
EXACT_CARDINALITY = 'exact_cardinality'
HAS_VALUE = 'has_value'
class ontolearn.lp_generator.complexity_lp_generator.DifficultyWeights[source]

Weights combining four difficulty signals into one score in (roughly) [0, 1].

Parameters:
  • length – weight on length / profile.max_length (longer target -> harder).

  • depth – weight on depth / profile.max_depth (deeper nesting -> harder).

  • baseline_hardness – weight on 1 - baseline_f1 – how far the best cheap one-step hypothesis falls short of solving the LP outright.

  • near_miss – weight on the fraction of positives that have a matching near-miss negative (an individual satisfying the target with exactly one top-level conjunct dropped) – these are the negatives a greedy learner is most likely to accidentally cover.

length: float = 0.25
depth: float = 0.15
baseline_hardness: float = 0.4
near_miss: float = 0.2
class ontolearn.lp_generator.complexity_lp_generator.ComplexityProfile[source]

A named point (or curriculum step) in the space of “how complex should the target be”.

Parameters:
  • name – identifier used to tag generated problems and group benchmark results.

  • allowed_constructs – DL constructors GoalDirectedLPGenerator.generate() may use when sampling a target concept. Relation-based constructs (∃/∀/cardinality/has-value) are silently unavailable on a knowledge base exposing no object properties.

  • min_length/max_length – bounds on concept_len of the (NNF) target.

  • min_depth/max_depth – bounds on restriction-nesting depth (see GoalDirectedLPGenerator.concept_depth()); a bare named class has depth 0.

  • max_arity – maximum number of operands sampled under one intersection/union.

  • card_limit – cardinality restrictions sample n uniformly from [1, card_limit].

  • use_inverse_roles – if True, restrictions may use r- in place of r.

  • min_pos/min_neg – an LP is discarded unless the target has at least this many positive and negative instances in the knowledge base.

  • max_examples_per_side – positives/negatives are subsampled down to this cap.

  • max_baseline_f1 – an LP is discarded if the best of a pool of “cheap” one-step hypotheses (an atomic class, its negation, or a bare exists r.Top) already reaches this F1 – i.e. it is trivially solvable without the constructs being tested.

  • min_difficulty – an LP is discarded if its DifficultyWeights-weighted score falls below this bar. 0.0 (default) disables this filter.

  • oversample_factor – GoalDirectedLPGenerator.generate() collects up to num_lps * oversample_factor passing candidates (subject to max_attempts) before ranking by difficulty and keeping the hardest num_lps – i.e. it actively searches for hard problems instead of returning the first ones found.

  • hard_negative_ratio – fraction of the sampled negatives drawn from the near-miss pool (when one exists) rather than arbitrary non-instances.

  • difficulty_weights – see DifficultyWeights.

name: str
allowed_constructs: FrozenSet[DLConstruct]
min_length: int = 1
max_length: int = 10
min_depth: int = 0
max_depth: int = 3
max_arity: int = 3
card_limit: int = 5
use_inverse_roles: bool = False
min_pos: int = 3
min_neg: int = 3
max_examples_per_side: int = 200
max_baseline_f1: float = 0.9
min_difficulty: float = 0.0
oversample_factor: int = 4
hard_negative_ratio: float = 0.5
difficulty_weights: DifficultyWeights
class ontolearn.lp_generator.complexity_lp_generator.GeneratedLearningProblem[source]

A goal-directed learning problem: a sampled target concept plus its (E+, E-).

profile_name: str
target_concept: owlapy.class_expression.OWLClassExpression
dl: str
pos: Individuals
neg: Individuals
length: int
depth: int
construct_histogram: Dict[str, int]
baseline_f1: float
difficulty: float
num_near_miss_negatives: int
to_lp() → PosNegLPStandard[source]

Build the PosNegLPStandard consumed by Ontolearn learners (e.g. TDL).

class ontolearn.lp_generator.complexity_lp_generator.GoalDirectedLPGenerator(knowledge_base: KnowledgeBase, seed: int | None = None, probe_cap: int = 300)[source]

Samples target concepts from an explicit DL-construct grammar and turns them into LPs.

Usage:

kb = KnowledgeBase(path="KGs/Family/family-benchmark_rich_background.owl")
gen = GoalDirectedLPGenerator(kb, seed=1)
profile = ComplexityProfile(
    name="nested-existential",
    allowed_constructs=frozenset({DLConstruct.EXISTENTIAL, DLConstruct.INTERSECTION}),
    max_length=8, max_depth=3,
)
lps = gen.generate(profile, num_lps=10)
model = TDL(kb).fit(lps[0].to_lp())
kb
rng
all_individuals: Individuals
atomic_classes: List[owlapy.class_expression.OWLClass]
object_properties: List[owlapy.owl_property.OWLObjectProperty]
baseline_f1(pos: Individuals, neg: Individuals) → float[source]

Best F1 any single “cheap” one-step hypothesis reaches on (pos, neg).

near_miss_negatives(target: owlapy.class_expression.OWLClassExpression, pos: Individuals) → Individuals[source]

Individuals satisfying target with exactly one top-level conjunct dropped, minus pos.

E.g. for C = Compound ⊓ (∃ hasChild.Father) these are individuals that are a Compound but whose child (if any) isn’t a Father, or that have a Father child but aren’t a Compound – the negatives one ablated conjunct away from being positive, which is exactly what trips up a greedy top-down learner.

static difficulty_score(profile: ComplexityProfile, length: int, depth: int, base_f1: float, pos: Individuals, near_miss: Individuals) → float[source]

Combine length/depth/baseline-hardness/near-miss-density into one score in [0, 1].

static concept_depth(ce: owlapy.class_expression.OWLClassExpression) → int[source]

Restriction-nesting depth: a named class or has value atom has depth 0.

sample_target(profile: ComplexityProfile) → Tuple[owlapy.class_expression.OWLClassExpression, collections.Counter][source]

Sample one target concept (in NNF) respecting profile, plus its construct histogram.

generate(profile: ComplexityProfile, num_lps: int, max_attempts: int = 3000) → List[GeneratedLearningProblem][source]

Sample the num_lps hardest distinct, non-trivial learning problems matching profile: up to num_lps * profile.oversample_factor passing candidates are collected (subject to max_attempts sampling attempts total), ranked by difficulty_score() and the top num_lps are kept.

generate_benchmark(profiles: Sequence[ComplexityProfile], num_lps_per_profile: int = 5, max_attempts_per_profile: int = 3000) → Dict[str, List[GeneratedLearningProblem]][source]

Run generate() for each profile in a curriculum; returns {profile.name: […]}.

ontolearn.lp_generator.complexity_lp_generator.save_benchmark(benchmark: Dict[str, List[GeneratedLearningProblem]], path: str) → None[source]

Serialize a benchmark (as returned by GoalDirectedLPGenerator.generate_benchmark()) to JSON: {dl_string: {“profile”, “positive_examples”, “negative_examples”, “length”, “depth”, “construct_histogram”, “baseline_f1”, “difficulty”, “num_near_miss_negatives”}}, individuals kept as full IRIs.