Spaces:
Running
Running
| """ | |
| Evaluation scripts for the extraction system. | |
| Provides entity-level evaluation of NER (precision / recall / F1 via seqeval) | |
| and recall-oriented evaluation of the anonymizer. These scripts produce the | |
| numbers used in the "Testing" chapter of the thesis. | |
| Usage: | |
| python evaluate.py ner # evaluate NER on a small annotated sample | |
| python evaluate.py anonymize # evaluate PII recall (NER vs regex) | |
| The samples here are tiny illustrative examples. For the thesis, replace them | |
| with a real annotated dataset (e.g. a subset of CoNLL-2003 for NER, or | |
| ai4privacy/pii-masking-200k for anonymization). | |
| """ | |
| from __future__ import annotations | |
| import sys | |
| # --------------------------------------------------------------------------- | |
| # NER evaluation (entity level, seqeval) | |
| # --------------------------------------------------------------------------- | |
| def evaluate_ner(): | |
| from seqeval.metrics import classification_report, f1_score, precision_score, recall_score | |
| from processors import ner | |
| # Each example: (tokens, gold BIO tags). Illustrative only. | |
| samples = [ | |
| ( | |
| ["My", "name", "is", "John", "Smith", "from", "London"], | |
| ["O", "O", "O", "B-PER", "I-PER", "O", "B-LOC"], | |
| ), | |
| ( | |
| ["Sarah", "works", "at", "Google", "in", "Berlin"], | |
| ["B-PER", "O", "O", "B-ORG", "O", "B-LOC"], | |
| ), | |
| ] | |
| y_true, y_pred = [], [] | |
| for tokens, gold in samples: | |
| text = " ".join(tokens) | |
| ents = ner.extract_entities(text) | |
| # Build predicted BIO tags aligned to whitespace tokens. | |
| pred = ["O"] * len(tokens) | |
| # character offset of each token | |
| offsets = [] | |
| pos = 0 | |
| for tok in tokens: | |
| start = text.index(tok, pos) | |
| offsets.append((start, start + len(tok))) | |
| pos = start + len(tok) | |
| for ent in ents: | |
| etype = ent["type"] | |
| first = True | |
| for i, (s, e) in enumerate(offsets): | |
| # token overlaps the entity span | |
| if s >= ent["start"] and e <= ent["end"] + 1: | |
| pred[i] = ("B-" if first else "I-") + etype | |
| first = False | |
| y_true.append(gold) | |
| y_pred.append(pred) | |
| print("=== NER evaluation (entity level) ===") | |
| print(classification_report(y_true, y_pred)) | |
| print(f"Precision: {precision_score(y_true, y_pred):.4f}") | |
| print(f"Recall: {recall_score(y_true, y_pred):.4f}") | |
| print(f"F1: {f1_score(y_true, y_pred):.4f}") | |
| # --------------------------------------------------------------------------- | |
| # Anonymization evaluation (PII recall: NER+Presidio vs regex only) | |
| # --------------------------------------------------------------------------- | |
| def evaluate_anonymize(): | |
| from processors import anonymizer | |
| import re | |
| # (text, list of PII substrings that MUST be removed) | |
| samples = [ | |
| ("My name is John Smith, email john@example.com", ["John Smith", "john@example.com"]), | |
| ("Call Sarah at +1 202 555 0143", ["Sarah", "+1 202 555 0143"]), | |
| ("I live in Berlin and work at Google", ["Berlin", "Google"]), | |
| ] | |
| def recall(anon_fn): | |
| found, total = 0, 0 | |
| for text, pii_list in samples: | |
| anon = anon_fn(text) | |
| for pii in pii_list: | |
| total += 1 | |
| # PII counts as removed if it no longer appears verbatim | |
| if pii.lower() not in anon.lower(): | |
| found += 1 | |
| return found / total if total else 0.0 | |
| # regex-only baseline (the original approach) | |
| def regex_only(text): | |
| from processors.anonymizer import _regex_anonymize | |
| return _regex_anonymize(text) | |
| print("=== Anonymization evaluation (PII recall) ===") | |
| print(f"Active backend: {anonymizer.backend_name()}") | |
| print(f"Recall (regex only): {recall(regex_only):.2%}") | |
| print(f"Recall (full system): {recall(anonymizer.anonymize_text):.2%}") | |
| print("\nNote: full system requires Presidio + spaCy model for name/location recall.") | |
| if __name__ == "__main__": | |
| mode = sys.argv[1] if len(sys.argv) > 1 else "ner" | |
| if mode == "ner": | |
| evaluate_ner() | |
| elif mode == "anonymize": | |
| evaluate_anonymize() | |
| else: | |
| print("Usage: python evaluate.py [ner|anonymize]") | |