# NER Models for PII Redaction This document describes the **Named Entity Recognition (NER)** ONNX models at the core of the PII Redaction POC. Person-name detection is the only NER responsibility in this solution; structured identifiers (email, phone, PAN, domain IDs) are handled by regex and domain-rule detectors. For pipeline placement, trust boundaries, and routing diagrams, see [architecture.md](architecture.md). For the Tamil/Tanglish implementation plan and success metrics, see [tamil-tanglish-ner-plan.md](tamil-tanglish-ner-plan.md). --- ## 1. Executive Summary The POC uses **dual-model ONNX NER routing** to redact **person-name PII** before prompts reach an LLM: | Script in prompt | Model invoked | Typical use case | |------------------|---------------|------------------| | Latin only (`LatinOnly`) | English (`dslim/bert-base-NER`) | English names, Indian names in Roman script, **Tanglish** | | Tamil only (`TamilOnly`) | Tamil (`prachuryyaIITG/SampurNER_Tamil_IndicBERTv2`) | Tamil-script customer names | | Mixed (`Mixed`) | **Both** models on the full text; spans merged | Code-mixed Indian CS prompts | | No letters (`NoLetters`) | Neither | Digits-only or symbol-only text | `RoutingOnnxNerModelRunner` classifies script via `ScriptRouter`, delegates to `EnglishOnnxNerRunner` and/or `TamilOnnxNerRunner`, and merges overlapping PERSON spans (longer span wins). Only **PERSON** entities are emitted to the redaction pipeline; all other NER labels are discarded. --- ## 2. English Model ### Hugging Face model ID **[`dslim/bert-base-NER`](https://huggingface.co/dslim/bert-base-NER)** ### Architecture | Property | Value | |----------|-------| | Base | BERT-base (uncased), ~110M parameters | | Task | Token classification (NER) | | Tokenizer | **WordPiece** via `vocab.txt` (`BertWordPieceEncoder`) | | Runtime | ONNX via Microsoft.ML.OnnxRuntime | | Export | Hugging Face Optimum (`ORTModelForTokenClassification`) or pre-exported ONNX from HF | ### Labels (BIO) The English model uses standard CoNLL-style BIO tags. The POC maps only **person** labels to `PiiEntityType.Person`: | Label | Mapped to PERSON | |-------|------------------| | `O` | No | | `B-PER`, `I-PER` | Yes | | `B-PERSON`, `I-PERSON` | Yes | | `B-ORG`, `I-ORG`, `B-LOC`, `I-LOC`, `B-MISC`, `I-MISC` | No | Full label list is written to `ner-labels.txt` at download time from the model `config.json` `id2label` map (typically 9 labels for this model). Label matching is implemented in `NerLabelConfig.English`: ```19:22:src/PiiRedaction.Infrastructure/Onnx/NerLabelConfig.cs private static bool IsEnglishPersonLabel(string label) => label is "B-PER" or "I-PER" or "B-PERSON" or "I-PERSON" || (label.EndsWith("-PER", StringComparison.Ordinal) && (label.StartsWith("B-", StringComparison.Ordinal) || label.StartsWith("I-", StringComparison.Ordinal))); ``` ### Asset paths | File | Primary path (`appsettings.json`) | Legacy fallback | |------|----------------------------------|-----------------| | ONNX model | `models/en/ner-model.onnx` | `models/ner-model.onnx` (`OnnxModelPath`) | | Vocabulary | `models/en/vocab.txt` | `models/vocab.txt` | | Labels | `models/en/ner-labels.txt` | `models/ner-labels.txt` | `EnglishOnnxNerRunner` resolves the model path with primary + legacy fallback: ```15:22:src/PiiRedaction.Infrastructure/Onnx/EnglishOnnxNerRunner.cs var modelPath = OnnxAssetPathResolver.ResolveModelPath( options.Value.EnglishOnnxModelPath, options.Value.OnnxModelPath); var modelDirectory = Path.GetDirectoryName(modelPath) ?? Environment.CurrentDirectory; var labels = OnnxAssetPathResolver.LoadLabels(modelDirectory); var encoder = new BertWordPieceEncoder(modelDirectory, logger); _runner = new OnnxTokenClassifierRunner(modelPath, encoder, NerLabelConfig.English, labels, logger); ``` > **Note:** `scripts/download-ner-model.ps1` writes assets to `models/` (repository root). For the configured primary path, copy or move them into `models/en/`, or rely on the `OnnxModelPath` fallback. ### Download script ```powershell .\scripts\download-ner-model.ps1 ``` Or with Python directly: ```bash python scripts/download-ner-model.py ``` **Behavior:** 1. If Python + Optimum are available → exports `dslim/bert-base-NER` to ONNX under `models/`. 2. Otherwise → downloads pre-exported ONNX from `https://huggingface.co/dslim/bert-base-NER/resolve/main/onnx/` (`model.onnx`, `vocab.txt`, `config.json` → `ner-labels.txt`). --- ## 3. Tamil Model ### Hugging Face model ID **[`prachuryyaIITG/SampurNER_Tamil_IndicBERTv2`](https://huggingface.co/prachuryyaIITG/SampurNER_Tamil_IndicBERTv2)** (SampurNER Tamil IndicBERTv2 — fine-grained NER for Tamil script.) ### Why SampurNER IndicBERTv2 vs MuRIL | Criterion | SampurNER Tamil IndicBERTv2 | MuRIL (fallback candidate) | |-----------|----------------------------|----------------------------| | Tamil NER training | Fine-grained SampurNER dataset (Tamil-specific labels) | General multilingual; NER requires separate fine-tune | | Model size | ~0.3B parameters (IndicBERTv2, ~278M base) | ~0.6B parameters | | POC fit | Lighter memory footprint; ONNX export path validated in this repo | Reserved for Phase 5 if Tamil recall is insufficient | | Indian financial context | Trained on Indian-language NER corpus; person subtypes map cleanly to PERSON | Heavier; eval-driven swap only | See [tamil-tanglish-ner-plan.md](tamil-tanglish-ner-plan.md) §3 for the original selection rationale. ### Architecture | Property | Value | |----------|-------| | Base | IndicBERTv2 (AI4Bharat), ~0.3B parameters | | Task | Fine-grained token classification | | Tokenizer | **WordPiece** when `vocab.txt` is present (this repo's export path); SentencePiece fallback if `sentencepiece.bpe.model` / `spiece.model` exists | | Runtime | Same shared `OnnxTokenClassifierRunner` as English | ### Tokenizer: WordPiece, not SentencePiece (in practice) The Hugging Face repo for this model does **not** ship a SentencePiece model file. The download scripts extract **WordPiece** assets from `tokenizer.json` → `vocab.txt`. `TokenClassifierEncoderFactory` prefers `vocab.txt`: ```10:19:src/PiiRedaction.Infrastructure/Onnx/TokenClassifierEncoderFactory.cs public static ITokenClassifierEncoder Create(string modelDirectory, ILogger logger) { var vocabPath = OnnxAssetPathResolver.ResolveAssetPath(Path.Combine(modelDirectory, "vocab.txt")); if (File.Exists(vocabPath)) { logger.LogInformation( "Using WordPiece tokenizer (vocab.txt) from {ModelDirectory}.", modelDirectory); return new BertWordPieceEncoder(modelDirectory, logger); } ``` The PowerShell Tamil download script emits an explicit warning when WordPiece assets are saved instead of SentencePiece. ### Labels (fine-grained person tags) SampurNER uses fine-grained BIO tags (e.g. `B-person-politician`, `I-person-artist`, `B-location`, `O`). The POC treats **any label containing `person`** (case-insensitive) as a person span: ```24:25:src/PiiRedaction.Infrastructure/Onnx/NerLabelConfig.cs private static bool IsTamilPersonLabel(string label) => label.Contains("person", StringComparison.OrdinalIgnoreCase); ``` Unit tests lock this behavior: ```20:27:tests/PiiRedaction.Infrastructure.Tests/Onnx/NerLabelConfigTests.cs [TestCase("B-person-politician", true)] [TestCase("I-person-artist", true)] [TestCase("B-location", false)] [TestCase("O", false)] public void Tamil_IsPersonLabel_MatchesFineGrainedTags(string label, bool expected) { NerLabelConfig.Tamil.IsPersonLabel(label).Should().Be(expected); } ``` ### Asset paths | File | Path | |------|------| | ONNX model | `models/ta/model.onnx` | | Tokenizer | `models/ta/vocab.txt` (WordPiece, preferred) **or** `models/ta/sentencepiece.bpe.model` | | Labels | `models/ta/ner-labels.txt` | | Optional | `models/ta/tokenizer.json` (intermediate export artifact) | ### Download script ```powershell .\scripts\download-tamil-ner-model.ps1 ``` Or with Python directly: ```bash python scripts/download-tamil-ner-model.py ``` **Behavior:** 1. Python + Optimum → full export to `models/ta/` including ONNX, labels, and tokenizer assets. 2. PowerShell fallback → downloads `onnx/model.onnx` from Hugging Face when published; otherwise requires Python export (pre-exported ONNX may return 404). `TamilOnnxNerRunner` wiring: ```15:19:src/PiiRedaction.Infrastructure/Onnx/TamilOnnxNerRunner.cs var modelPath = OnnxAssetPathResolver.ResolveModelPath(options.Value.TamilOnnxModelPath); var modelDirectory = Path.GetDirectoryName(modelPath) ?? Environment.CurrentDirectory; var labels = OnnxAssetPathResolver.LoadLabels(modelDirectory); var encoder = TokenClassifierEncoderFactory.Create(modelDirectory, logger); _runner = new OnnxTokenClassifierRunner(modelPath, encoder, NerLabelConfig.Tamil, labels, logger); ``` --- ## 4. Why These Models Evidence-based rationale for this Indian financial POC: | Requirement | Decision | |-------------|----------| | **English + Indian Latin names** | `dslim/bert-base-NER` is industry-standard, pre-integrated, and handles many Indian names in Roman script (e.g. `Ravi Kumar`, `Anita Sharma`) | | **Tamil script names** | English BERT is out-of-vocabulary for Tamil letters (U+0B80–U+0BFF); a Tamil-trained NER model is required | | **Tanglish (Roman-script Tamil)** | Routed to the **English** model only (`ScriptComposition.LatinOnly`); no Tamil ONNX on Latin-only text | | **Code-mixed prompts** | Both models run on the full prompt; `MergePersonSpans` deduplicates overlaps | | **Deployable size** | English ~431 MB ONNX (FP32); Tamil IndicBERTv2 ~0.3B params — smaller than MuRIL ~0.6B | | **ONNX export path** | Both models export via Hugging Face Optimum; English has pre-exported ONNX on HF; Tamil may require local Python export | | **PERSON-only scope** | POC redacts person names via NER; org/location/misc labels are intentionally ignored to limit false positives | --- ## 5. How They Integrate ### Configuration (`appsettings.json`) ```1:8:src/PiiRedaction.ConsoleApp/appsettings.json { "PiiRedaction": { "OnnxModelPath": "models/ner-model.onnx", "EnglishOnnxModelPath": "models/en/ner-model.onnx", "TamilOnnxModelPath": "models/ta/model.onnx", "EnableTamilNer": true } } ``` Options type: ```3:14:src/PiiRedaction.Core/Configuration/PiiRedactionOptions.cs public sealed class PiiRedactionOptions { public const string SectionName = "PiiRedaction"; public string OnnxModelPath { get; set; } = "models/ner-model.onnx"; public string EnglishOnnxModelPath { get; set; } = "models/en/ner-model.onnx"; public string TamilOnnxModelPath { get; set; } = "models/ta/model.onnx"; public bool EnableTamilNer { get; set; } = true; } ``` Set `EnableTamilNer` to `false` for English-only routing. ### Dependency injection ```34:36:src/PiiRedaction.ConsoleApp/DependencyInjection/ServiceCollectionExtensions.cs services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); ``` `OnnxNerPiiDetector` consumes `IOnnxNerModelRunner` (the router) and returns `[]` when no model is available — **fail-open** for person detection. ### Script routing (`ScriptRouter`) ```11:41:src/PiiRedaction.Core/Detection/ScriptRouter.cs public ScriptComposition GetComposition(string text) { ArgumentNullException.ThrowIfNull(text); var hasLatin = false; var hasTamil = false; foreach (var character in text) { if (IsTamilLetter(character)) { hasTamil = true; } else if (char.IsAsciiLetter(character)) { hasLatin = true; } if (hasLatin && hasTamil) { return ScriptComposition.Mixed; } } // ... return hasTamil ? ScriptComposition.TamilOnly : ScriptComposition.LatinOnly; } ``` ### Routing runner (`RoutingOnnxNerModelRunner`) ```39:79:src/PiiRedaction.Infrastructure/Onnx/RoutingOnnxNerModelRunner.cs public IReadOnlyList PredictEntities(string text) { var composition = _scriptRouter.GetComposition(text); var entities = new List(); switch (composition) { case ScriptComposition.LatinOnly: if (_englishRunner.IsModelAvailable) { entities.AddRange(_englishRunner.PredictEntities(text)); } break; case ScriptComposition.TamilOnly: if (_enableTamilNer && _tamilRunner.IsModelAvailable) { entities.AddRange(_tamilRunner.PredictEntities(text)); } break; case ScriptComposition.Mixed: if (_englishRunner.IsModelAvailable) { entities.AddRange(_englishRunner.PredictEntities(text)); } if (_enableTamilNer && _tamilRunner.IsModelAvailable) { entities.AddRange(_tamilRunner.PredictEntities(text)); } break; case ScriptComposition.NoLetters: break; } return MergePersonSpans(entities); } ``` ### Shared inference (`OnnxTokenClassifierRunner`) Both runners share: - **Encode** → `input_ids`, `attention_mask`, optional `token_type_ids` - **Argmax** over per-token logits - **BIO decode** → `PiiEntityType.Person` with `PiiDetectionSource.Ner` - **Max sequence length: 128 tokens** ```13:13:src/PiiRedaction.Infrastructure/Onnx/OnnxTokenClassifierRunner.cs private const int MaxSequenceLength = 128; ``` ### End-to-end flow ``` Prompt → CompositePiiDetector → OnnxNerPiiDetector → RoutingOnnxNerModelRunner → ScriptRouter → EnglishOnnxNerRunner / TamilOnnxNerRunner → OnnxTokenClassifierRunner → PERSON entities → PlaceholderPiiRedactor → ``` --- ## 6. Evidence from Codebase ### Real-model tests (`Category=RealModel`) English direct inference — `RealNerModelRunnerTests`: ```15:47:tests/PiiRedaction.Infrastructure.Tests/Onnx/RealNerModelRunnerTests.cs [Category("RealModel")] public sealed class RealNerModelRunnerTests : RealNerModelFixture { [TestCase("Customer Ravi Kumar called about billing.", "Ravi", "Ravi Kumar")] [TestCase("Mr. John Smith called about a duplicate debit.", "John", "John Smith")] public void PredictEntities_DetectsPersonWithCorrectSpan(...) ``` English pipeline — `RealNerPipelineTests` (`Category=RealModel`): canonical `FullFinancialWithCustomer`, multi-person, clean-ticket negative. Fixture skips when model missing: ```5:6:tests/TestSupport.Shared/RealNerModelPaths.cs public const string ModelMissingMessage = "ONNX model not found. Run scripts/download-ner-model.ps1 from the repository root."; ``` Resolves `models/en/ner-model.onnx` then `models/ner-model.onnx`. ### Tamil tests (`Category=TamilNer`) Direct Tamil runner — `RealTamilNerModelRunnerTests`: ```9:69:tests/PiiRedaction.Infrastructure.Tests/Onnx/RealTamilNerModelRunnerTests.cs [Category("TamilNer")] public sealed class RealTamilNerModelRunnerTests : RealTamilNerModelFixture { [TestCase("வாடிக்கையாளர் ராஜேஷ் குமார் அழைத்தார்.", "ராஜேஷ்", "ராஜேஷ் குமார்")] public void PredictEntities_TamilScript_DetectsPersonEntity(...) // ... [TestCase("Customer Senthil phone 9876543210", "Senthil")] public void PredictEntities_TanglishLatinScript_DoesNotInvokeTamilRunner(...) ``` Routed pipeline — `RealTamilPipelineTests` covers Tamil-only, Tanglish (English path), mixed, full financial, and clean Tamil negative. ### Console samples (`SamplePromptCatalog`) Tamil/Tanglish/mixed samples (indices 10–14): | Sample | Category | Input excerpt | |--------|----------|---------------| | `TamilCustomerNameOnly` | NER (Tamil) | `வாடிக்கையாளர் ராஜேஷ் குமார் …` | | `TamilWithPhonePan` | NER (Tamil) + Regex | Tamil person + phone + PAN | | `TanglishCustomer` | NER (English/Tanglish) | `Customer Senthil phone 9876543210 …` | | `MixedTamilEnglish` | NER (Mixed) | `வாடிக்கையாளர் Ravi Kumar phone …` | | `TamilFullFinancial` | NER (Tamil) + Regex + Domain | Tamil canonical demo | Run all samples (including Tamil) with no flags: ```bash dotnet run --project src/PiiRedaction.ConsoleApp ``` Or a single Tamil sample: ```bash dotnet run --project src/PiiRedaction.ConsoleApp -- --name TamilCustomerNameOnly ``` ### Expected console output (English canonical) When models are loaded, person names appear as `[PERSON]` with source `Ner`: ``` Detected PII: [PERSON ] Ravi Kumar (Ner) [EMAIL ] ravi.kumar@gmail.com (Regex) ... Sanitized Prompt: Customer with email ... ``` --- ## 7. Model Assets Table All model binaries are **gitignored**; only `.gitkeep` placeholders are committed. | Directory | File | Approx. size | Gitignored | Purpose | |-----------|------|--------------|------------|---------| | `models/` or `models/en/` | `ner-model.onnx` | ~431 MB | Yes | English BERT NER (FP32 ONNX from HF) | | `models/` or `models/en/` | `vocab.txt` | ~213 KB | Yes | WordPiece vocabulary | | `models/` or `models/en/` | `ner-labels.txt` | < 1 KB | Yes | BIO label index (one per line) | | `models/ta/` | `model.onnx` | ~1.2 GB (FP32 export, varies) | Yes | Tamil IndicBERT NER | | `models/ta/` | `vocab.txt` | varies | Yes | WordPiece vocab (preferred tokenizer) | | `models/ta/` | `tokenizer.json` | varies | Yes | HF tokenizer export (optional) | | `models/ta/` | `sentencepiece.bpe.model` | varies | Yes | SentencePiece (if present instead of vocab) | | `models/ta/` | `ner-labels.txt` | few KB | Yes | Fine-grained SampurNER labels | `.gitignore` entries: ``` models/*.onnx models/vocab.txt models/ner-labels.txt models/*.json models/en/* models/ta/* ``` English size reference: [docs/git-xenovex-setup.md](git-xenovex-setup.md) notes ~431 MB for the English ONNX file. --- ## 8. Limitations | Limitation | Detail | |------------|--------| | **Tanglish on English model only** | Roman-script Tanglish (`Customer Senthil`) is classified `LatinOnly` and handled by English BERT. Recall is best-effort and inconsistent for non-standard spellings. Tamil ONNX is **not** invoked on Latin-only text. | | **Fail-open if model missing** | `OnnxNerPiiDetector` and routing runners return `[]` when models are unavailable. Person names are **not** redacted; regex/domain layers still run. No regex fallback for names. | | **128 token limit** | `OnnxTokenClassifierRunner` truncates encoding at 128 tokens. Very long prompts may miss person names beyond the window. | | **PERSON-only NER scope** | Organization, location, and misc NER labels are ignored. Only person spans become ``. | | **Mixed-script merge** | When both models run, overlapping spans are deduped by length; shorter overlapping spans are dropped. | | **Tamil ONNX availability** | Pre-exported Tamil ONNX may not exist on Hugging Face; local Python export is often required. | | **No fail-closed mode** | Missing NER does not block sanitization or LLM calls (optional Phase 5 enhancement). | --- ## 9. How to Reproduce ### Download models From the repository root: ```powershell .\scripts\download-ner-model.ps1 .\scripts\download-tamil-ner-model.ps1 ``` If Tamil PowerShell download fails with a 404 on `onnx/model.onnx`, install Python 3.12+ and re-run: ```powershell winget install Python.Python.3.12 --accept-package-agreements --accept-source-agreements .\scripts\download-tamil-ner-model.ps1 -Python "$env:LOCALAPPDATA\Programs\Python\Python312\python.exe" ``` Optional: copy English assets from `models/` to `models/en/` to match `EnglishOnnxModelPath`. ### Verify with tests ```bash dotnet build dotnet test dotnet test --filter "Category=RealModel" dotnet test --filter "Category=TamilNer" dotnet test --logger "console;verbosity=detailed" ``` Tests skip gracefully when the corresponding ONNX files are absent. ### Verify with console ```bash dotnet run --project src/PiiRedaction.ConsoleApp -- --name CustomerNameOnly dotnet run --project src/PiiRedaction.ConsoleApp -- --name TamilCustomerNameOnly dotnet run --project src/PiiRedaction.ConsoleApp -- --name TanglishCustomer ``` ### Disable Tamil routing (English-only) Set in `appsettings.json`: ```json "EnableTamilNer": false ``` --- ## Related Documentation - [README.md](../README.md) — build, run, and test overview - [architecture.md](architecture.md) — dual-model routing diagrams and trust boundary - [tamil-tanglish-ner-plan.md](tamil-tanglish-ner-plan.md) — implementation phases and Tanglish expectations