Name detection models¶
Person and organisation names are found by a named entity recognition model. A
category names the detectors to try, in order: the shipped configuration prefers
the multilingual Babelscape/wikineural-multilingual-ner and falls back to
spaCy's en_core_web_sm, so a plain install still finds names and an install
with the multilingual-names extra finds more of them. Each link names its own
model in sensitive.categories.<id>.model, and any setting can be changed with
Config.override.
complydoc doctor reports which model is available, and every report records
which one answered for each category.
Your own spaCy model¶
"""Use your own spaCy pipeline for names, with a second model for Portuguese pages."""
# requires: spacy
import tempfile
from pathlib import Path
import spacy
import complydoc as cd
def save_pipeline(folder: Path, lang: str, label: str) -> str:
"""A stand-in for a trained model: a pipeline with an entity ruler, saved to disk."""
nlp = spacy.blank(lang)
nlp.add_pipe("entity_ruler").add_patterns([{"label": label, "pattern": "Maria Silva"}])
nlp.to_disk(folder / lang)
return str(folder / lang)
with tempfile.TemporaryDirectory() as folder:
english = save_pipeline(Path(folder), "en", "PERSON")
portuguese = save_pipeline(Path(folder), "pt", "PER")
config = cd.load_config().override(
{
# The shipped configuration tries a transformer first and keeps spaCy
# as the fallback. This is about spaCy, so the category is pointed at
# it and the rest of the chain cleared: otherwise the pipeline built
# above is handed to a detector that cannot read it.
"sensitive.categories.person_name.detector": "ner",
"sensitive.categories.person_name.fallback": [],
"sensitive.categories.person_name.model": {
"name": english,
"entity_labels": ["PERSON"],
"by_language": {"pt": {"name": portuguese, "entity_labels": ["PER"]}},
},
}
)
text = (
"O contrato foi assinado por Maria Silva em nome do fornecedor, e a fatura foi "
"aprovada pela equipa financeira antes do final do mês."
)
for match in cd.scan_text(text, config=config).matches:
print(match.label, match.masked, match.evidence)
| Setting | Meaning | Default |
|---|---|---|
name |
An installed spaCy package, such as en_core_web_trf, or a path to a saved pipeline |
en_core_web_sm |
entity_labels |
Labels reported for the category, such as PERSON, PER, ORG |
[PERSON] or [ORG] |
by_language |
Models by ISO 639-1 code, each with name and optional entity_labels |
none |
spans_key |
Read entities and scores from doc.spans[spans_key] |
none: doc.ents, no score |
drop_short_acronyms |
Drop single all-caps tokens of up to five characters | true |
drop_multiline |
Drop entities that span a line break | true |
Label names depend on the model: spaCy's English pipelines use PERSON and ORG;
most other spaCy language pipelines and the multilingual xx_ent_wiki_sm use PER
and ORG.
Models per language¶
With by_language, each page's language is detected locally (py3langid) and the
matching model is used. Pages with fewer than 60 letters, or a language with no
entry, use name. A model name can appear under several languages.
Confidence¶
spaCy's doc.ents carries no score, so findings have confidence: null and the
category's min_confidence does not apply. A pipeline with a span categorizer
stores scores on a span group; set spans_key to its key (commonly sc) and
min_confidence to drop low-scoring spans. Findings are reported at the model
evidence tier either way.
Filters¶
The two filters are tuned for English business forms. drop_short_acronyms
removes field labels such as IBAN or VAT that the small English model tags as
organisations; drop_multiline removes entities that join the end of one line to
the start of the next. Turn them off for models that do not make those mistakes.
The multilingual model, which ships as the preferred one¶
en_core_web_sm is small and English. On the benchmark corpus it finds two
thirds of the names and reads field labels such as KUNDENDATEN as
organisations; the multilingual model finds all of them, at better precision.
On whole documents the gap is wider still. The numbers are in
Detection accuracy.
It is preferred by the shipped configuration, so this is what to install to get it. Without the extra the fallback runs instead and nothing breaks.
Install the extra and fetch the weights once. Fetching reaches the network, so it happens here rather than during a scan:
uv tool install --force "complydoc[multilingual-names]"
"$(uv tool dir)/complydoc/bin/python" -c "from transformers import pipeline; \
pipeline('token-classification', model='Babelscape/wikineural-multilingual-ner')"
With pip, pip install "complydoc[multilingual-names]" and run the second command
with your own python. In a checkout of this repository, uv sync --extra
multilingual-names and uv run python. complydoc doctor prints the command for
whichever piece is missing.
Then point the two categories at the token_classifier detector:
categories:
person_name:
detector: token_classifier
min_confidence: 0.9
model:
name: Babelscape/wikineural-multilingual-ner
entity_labels: [PER]
organisation_name:
detector: token_classifier
min_confidence: 0.9
model:
name: Babelscape/wikineural-multilingual-ner
entity_labels: [ORG]
A confidence floor is worth setting here, which it is not for spaCy's doc.ents:
this detector reports the model's own score, so min_confidence applies. On the
corpus 0.9 removes two wrong flags and costs no names.
The weights are read from files already on the machine. A model that is not there is reported as a category that could not be scanned, with how to fetch it, because a scan runs inside the network guard and downloads nothing. Pages are cut into windows first: the model reads a few hundred tokens at a time, and a name further down the page would otherwise never be seen.
complydoc doctor reports whichever model the configuration names.
Models from other libraries¶
A model from another library is added as a detector. The detector receives the
page text and the category configuration, and returns cd.Finding spans with a
confidence:
"""Detect names with a Hugging Face token-classification model saved on disk."""
# requires: transformers
import os
import sys
from transformers import pipeline
import complydoc as cd
model_path = os.environ.get("COMPLYDOC_NER_MODEL")
if not model_path:
print("set COMPLYDOC_NER_MODEL to a local token-classification model directory")
sys.exit(0)
classify = pipeline("token-classification", model=model_path, aggregation_strategy="simple")
class TokenClassifierDetector:
id = "token_classifier"
def find(self, text, context):
wanted = set(context.config.model.entity_labels)
return [
cd.Finding(start=entity["start"], end=entity["end"], confidence=float(entity["score"]))
for entity in classify(text)
if entity["entity_group"] in wanted
]
cd.register_detector(TokenClassifierDetector())
config = cd.load_config().override(
{
"sensitive.categories.person_name.detector": "token_classifier",
"sensitive.categories.person_name.model.entity_labels": ["PER"],
"sensitive.categories.person_name.min_confidence": 0.8,
}
)
for match in cd.scan_text("The agreement was signed by Maria Silva.", config=config).matches:
print(match.label, match.masked, match.confidence)
The model has to load from local files: scans run inside the network guard.
Registration applies to the current process, so audits with jobs above 1 do not
use it.
Availability¶
complydoc doctor lists every configured model and whether it loads. A category
whose model cannot be loaded is reported as not scanned, and run.ner_available is
true only when every configured model loads.
Where the count is read, the run says which categories nothing was looked for: a row in the CLI summary beside the item count, and a notice above the findings on the report's Sensitive information page. A category that was never scanned counts zero findings, which on its own reads the same as a category that is clean.