From af5d2ee6d7d5b4c3109bdc005531f8d8df7205f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Lamercerie?= <aurelien.lamercerie@tetras-libre.fr> Date: Fri, 11 Aug 2023 19:17:04 +0200 Subject: [PATCH] Update metrics computing for entities --- main.py | 2 +- ontoScorer/metric_score.py | 4 ++ ontoScorer/metrics.py | 90 +++++++++++++++++++++--------------- ontoScorer/ontology.py | 5 ++ ontoScorer/report.py | 95 ++++++++++++++++++++++++++++---------- ontoScorer/scorer.py | 5 +- tests/test_metrics.py | 6 +-- 7 files changed, 138 insertions(+), 69 deletions(-) diff --git a/main.py b/main.py index fe59030..4c53ecd 100644 --- a/main.py +++ b/main.py @@ -21,7 +21,7 @@ def main(): scorer = OntoScorer(REFERENCE_ONTOLOGY_PATH, GENERATED_ONTOLOGY_PATH) # Compare the ontologies - scorer.compare() + scorer.compute_metrics() # Generate a report scorer.generate_report() diff --git a/ontoScorer/metric_score.py b/ontoScorer/metric_score.py index d25555f..20027ad 100644 --- a/ontoScorer/metric_score.py +++ b/ontoScorer/metric_score.py @@ -25,6 +25,8 @@ class Score: self.f1 = None self.total_elements = 0 self.matched_elements = 0 + self.y_true = [] # Nouveau champ + self.y_pred = [] # Nouveau champ #-------------------------------------------------------------------------- @@ -42,6 +44,8 @@ class Score: Returns: None """ + self.y_true = y_true + self.y_pred = y_pred self.precision = precision_score(y_true, y_pred) self.recall = recall_score(y_true, y_pred) self.f1 = f1_score(y_true, y_pred) diff --git a/ontoScorer/metrics.py b/ontoScorer/metrics.py index b3c2b38..3c4936f 100644 --- a/ontoScorer/metrics.py +++ b/ontoScorer/metrics.py @@ -15,6 +15,7 @@ matches the reference. from ontoScorer.ontology import Ontology from ontoScorer.metric_score import Score + class Metrics: """ Metrics class provides functionalities to compute scores for ontology @@ -26,59 +27,73 @@ class Metrics: #-------------------------------------------------------------------------- def __init__(self): - """ - Initializes score categories for various ontology elements. - """ self.scores = { - "class": Score(), - "object_property": Score(), - "data_property": Score(), - "restriction": Score(), - "individual": Score(), - "annotation": Score(), - "overall": Score() + "entities": { + "classes": Score(), + "object_properties": Score(), + "individuals": Score(), + "synthesis": Score() # Synthesis score for entities axis + }, + "taxonomic_relations": { + "subclass": Score(), + "instanciation": Score(), + "synthesis": Score() # Synthesis score for taxonomic relations axis + }, + "non_taxonomic_relations": { + "object_properties": Score(), + "data_properties": Score(), + "domains": Score(), + "ranges": Score(), + "synthesis": Score() # Synthesis score for non-taxonomic relations axis + }, + "axioms": { + "restriction_axioms": Score(), + "synthesis": Score() # Synthesis score for axioms axis + } } + #-------------------------------------------------------------------------- # Computing Method(s) #-------------------------------------------------------------------------- - def calculate(self, reference_ontology, generated_ontology): - """ - Compute scores (precision, recall, f1) for each ontology element category. - - Args: - - reference_ontology: Ontology object representing the reference ontology. - - generated_ontology: Ontology object representing the generated ontology. - """ - methods = [ - ("class", "get_classes"), - ("object_property", "get_object_properties"), - # Additional methods can be uncommented as needed - #("data_property", "get_data_properties"), - #("restriction", "get_restrictions"), - ("individual", "get_individuals"), - #("annotation", "get_annotations") - ] + @staticmethod + def deduplicate_elements(elements, comparison_function): + unique_elements = [] + for elem in elements: + if not any([comparison_function(elem, unique_elem) for unique_elem in unique_elements]): + unique_elements.append(elem) + return unique_elements + + def compute_entity_scores(self, reference_ontology, generated_ontology): + entity_methods = { + "classes": ("get_classes", Ontology.compare_entity_names), + "object_properties": ("get_object_properties", Ontology.compare_entity_names), + "individuals": ("get_individuals", Ontology.compare_entity_names) + } y_true_overall = [] y_pred_overall = [] - for score_name, method_name in methods: - reference_elements = set([elem.name() for elem in getattr(reference_ontology, method_name)()]) - generated_elements = set([elem.name() for elem in getattr(generated_ontology, method_name)()]) + for score_name, (method_name, comparison_function) in entity_methods.items(): + reference_elements_raw = getattr(reference_ontology, method_name)() + generated_elements_raw = getattr(generated_ontology, method_name)() + + reference_elements = Metrics.deduplicate_elements(reference_elements_raw, comparison_function) + generated_elements = Metrics.deduplicate_elements(generated_elements_raw, comparison_function) - all_elements = reference_elements.union(generated_elements) - y_true = [1 if elem in reference_elements else 0 for elem in all_elements] - y_pred = [1 if elem in generated_elements else 0 for elem in all_elements] + all_elements = list(set(reference_elements + generated_elements)) + all_elements = Metrics.deduplicate_elements(all_elements, comparison_function) + y_true = [1 if any([comparison_function(elem, ref_elem) for ref_elem in reference_elements]) else 0 for elem in all_elements] + y_pred = [1 if any([comparison_function(elem, gen_elem) for gen_elem in generated_elements]) else 0 for elem in all_elements] - self.scores[score_name].compute(y_true, y_pred) + self.scores["entities"][score_name].compute(y_true, y_pred) y_true_overall.extend(y_true) y_pred_overall.extend(y_pred) - self.scores["overall"].compute(y_true_overall, y_pred_overall) + self.scores["entities"]["synthesis"].compute(y_true_overall, y_pred_overall) @@ -90,7 +105,8 @@ class Metrics: """ Prints the scores (precision, recall, f1) for each ontology element category. """ - for element, score in self.scores.items(): - print(f"Metrics for {element.capitalize()}:") + entity_scores = self.scores["entities"] + for element, score in entity_scores.items(): + print(f"Metrics for {element.capitalize()} (Entity axis):") print(score) print("----------------------------") diff --git a/ontoScorer/ontology.py b/ontoScorer/ontology.py index 942993d..64fff47 100644 --- a/ontoScorer/ontology.py +++ b/ontoScorer/ontology.py @@ -122,6 +122,11 @@ class Ontology: # Comparison Method(s) #-------------------------------------------------------------------------- + @staticmethod + def compare_entity_names(entity1, entity2): + return entity1.name() == entity2.name() + + def compare_to(self, other_ontology) -> tuple: """Compare classes of the current ontology with another.""" self_classes = {c.name() for c in self.classes} diff --git a/ontoScorer/report.py b/ontoScorer/report.py index a5df58a..9cc27ef 100644 --- a/ontoScorer/report.py +++ b/ontoScorer/report.py @@ -1,42 +1,87 @@ #!/usr/bin/python3.10 # -*-coding:Utf-8 -* -#============================================================================== -# ontoScorer: [brief description of the module] -#------------------------------------------------------------------------------ -# Detailed module description, if needed -#============================================================================== +""" +Ontology Report Module +------------------------------------------------------------------------------ +This module provides a means to generate detailed reports that compare a +reference ontology with a generated ontology. It uses various metrics to +highlight the similarities and differences between the two ontologies, +facilitating a comprehensive evaluation of ontology generation techniques. + +The reports can be used for debugging, optimization, and academic purposes, +helping developers and researchers to better understand the quality and +characteristics of generated ontologies in comparison to a reference. + +Classes: + - Report: Main class that represents an ontology comparison report. +""" class Report: - def __init__(self, reference_ontology, generated_ontology, comparison_result, metrics): + """ + A class used to generate detailed reports on the comparison of a + reference ontology with a generated ontology. + + Attributes: + ------------ + reference_ontology : obj + The reference ontology against which the generated ontology is compared. + generated_ontology : obj + The ontology that has been generated and needs to be evaluated. + metrics : obj + The metrics used to evaluate the generated ontology against the reference. + + Methods: + --------- + generate() -> str: + Produces a string representation of the report, detailing the comparison + results and associated metrics. + """ + + def __init__(self, reference_ontology, generated_ontology, metrics): + """ + Initializes the Report with a reference ontology, generated ontology, + and comparison metrics. + """ self.reference_ontology = reference_ontology self.generated_ontology = generated_ontology - self.comparison_result = comparison_result self.metrics = metrics - def generate(self): - report_str = "=== Ontology Evaluation Report ===\n" - report_str += f"\nReference Ontology: {self.reference_ontology.path}" - report_str += f"\nNumber of classes in Reference Ontology: {len(self.reference_ontology.classes)}" + def _generate_evaluation_metrics(self, category_name): + scores_str = f"\n\n== {category_name.replace('_', ' ').capitalize()} ==\n" + + category_scores = self.metrics.scores[category_name] + for element, score in category_scores.items(): + if element != "synthesis": # We'll print synthesis separately + scores_str += f"\nMetrics for {element.replace('_', ' ').capitalize()}:\n" + scores_str += f'Precision: {score._format_metric(score.precision)}\n' + scores_str += f'Recall: {score._format_metric(score.recall)}\n' + scores_str += f'F1 Score: {score._format_metric(score.f1)}\n' + scores_str += f'Total Elements: {score.total_elements}\n' + scores_str += f'Matched Elements: {score.matched_elements}\n' + + # Print synthesis score at the end for the category + synthesis_score = category_scores["synthesis"] + scores_str += "\nOverall Metrics (Synthesis):\n" + scores_str += f'Precision: {synthesis_score._format_metric(synthesis_score.precision)}\n' + scores_str += f'Recall: {synthesis_score._format_metric(synthesis_score.recall)}\n' + scores_str += f'F1 Score: {synthesis_score._format_metric(synthesis_score.f1)}\n' + + return scores_str - report_str += f"\n\nGenerated Ontology: {self.generated_ontology.path}" - report_str += f"\nNumber of classes in Generated Ontology: {len(self.generated_ontology.classes)}" - # Comparison of the number of classes - report_str += "\n\nComparison Result: " - if len(self.reference_ontology.classes) > len(self.generated_ontology.classes): - report_str += "The generated ontology has fewer classes than the reference ontology." - elif len(self.reference_ontology.classes) < len(self.generated_ontology.classes): - report_str += "The generated ontology has more classes than the reference ontology." - else: - report_str += "The generated ontology and the reference ontology have the same number of classes." + def generate(self): + report_str = "=== Ontology Evaluation Report ===\n" - report_str += "\n\nEvaluation Metrics:" - report_str += f'\nPrecision: {self.metrics.scores["overall"]["precision"]}' - report_str += f'\nRecall: {self.metrics.scores["overall"]["recall"]}' - report_str += f'\nF1 Score: {self.metrics.scores["overall"]["f1"]}' + # Introduction + report_str += "\nComparing Reference Ontology with Generated Ontology.\n" + + # Detailed Evaluation Metrics + for category in self.metrics.scores.keys(): + report_str += self._generate_evaluation_metrics(category) return report_str + diff --git a/ontoScorer/scorer.py b/ontoScorer/scorer.py index 763d930..f1e6b8d 100644 --- a/ontoScorer/scorer.py +++ b/ontoScorer/scorer.py @@ -17,14 +17,13 @@ class OntoScorer: self.generated_ontology = Ontology(generated_ontology_path) self.metrics = Metrics() - def compare(self): + def compute_metrics(self): self.comparison_result = self.reference_ontology.compare_to(self.generated_ontology) - self.metrics.calculate(self.reference_ontology, self.generated_ontology) + self.metrics.compute_entity_scores(self.reference_ontology, self.generated_ontology) def generate_report(self): report = Report(self.reference_ontology, self.generated_ontology, - self.comparison_result, self.metrics) print(report.generate()) diff --git a/tests/test_metrics.py b/tests/test_metrics.py index 0eb3365..de92d2f 100644 --- a/tests/test_metrics.py +++ b/tests/test_metrics.py @@ -27,8 +27,8 @@ class TestMetrics(unittest.TestCase): self.metrics = Metrics() - def test_calculate_scores(self): - self.metrics.calculate(self.onto1, self.onto2) + def test_computes_scores(self): + self.metrics.compute_entity_scores(self.onto1, self.onto2) for element, score in self.metrics.scores.items(): if score.total_elements == 0: self.assertIsNone(score.precision, f"Precision for {element} should be None when no elements are present") @@ -41,7 +41,7 @@ class TestMetrics(unittest.TestCase): def test_print_scores(self): - self.metrics.calculate(self.onto1, self.onto2) + self.metrics.compute_entity_scores(self.onto1, self.onto2) print() self.metrics.print_scores() -- GitLab