# Einfache OCR- und NER-Feature-Extraktion mit ONNX

<!-- category -- AI,OCR,NER,ONNX,Docker,CSharp,Tutorial -->
<datetime class="hidden">2026-01-21T12:00</datetime>

[![NuGet](https://img.shields.io/nuget/v/Mostlylucid.OcrNer)](https://www.nuget.org/packages/Mostlylucid.OcrNer/) [![NuGet Downloads](https://img.shields.io/nuget/dt/Mostlylucid.OcrNer)](https://www.nuget.org/packages/Mostlylucid.OcrNer/) [![GitHub-Freisetzung](https://img.shields.io/github/v/release/scottgal/mostlylucidweb?filter=ocrner-*&label=CLI)](https://github.com/scottgal/mostlylucidweb/releases?q=ocrner)

Wie I''ve gebaut wurde [***klar*RAG**](https://www.lucidrag.com) IM'-m Lesen von sozialen Medien, wo die Leute halten fragen das gleiche Ding .-'-wie Sie Funktionen aus gescannten Text erhalten .?'-m die Kategorie Fehler ist immer . . '-m benutzen Sie einfach ein LLM-'...-what WORKS aber ist sehr teuer . [tief im OCR-Raum](/blog/constrained-fuzzy-image-ocr-pipeline) Ich dachte mir, dass I'-D schreiben Sie eine kostenlose '-Beginner freundlich '-Annäherung an die NON---LLM oder LLM optional )-Wege zu tun

Sie haben Bilder mit Texten, die Sie extrahieren möchten, dass texten, ,, dann finden Sie die nützliche Struktur in ihr Namen, die ,, Unternehmen, Orte, ohne einen LLM-,, Versand von Daten in die Cloud oder zahlen per Token, .

Dieser Artikel zeigt die **einfachste Möglichkeit** Pipeline-Ausrüstung **Tesseract** für Textextraktion, dann **BERT NER** Alle lokalen, alle deterministischen, alle in C.#.

Deterministisch bedeutet hier feste Versionen.,.Festsprachdaten.,. und kein adaptives Lernen zur Laufzeit.

> **Sie wollen die verpackte Version.** Diese Pipeline ist jetzt ein NuGet-Paket mit automatischem Download von Modellen, , und einem CLI-Tool. **[Teil Das NuGet-Paket](/blog/simple-ocr-ner-nuget)** für die fertige Version von --to---use

[TOC]

---


## Die vollständige Pipeline

```mermaid
flowchart LR
    subgraph OCR["Part 1: OCR"]
        IMG[Image]
        TESS[Tesseract]
        TXT[Raw Text]
    end

    subgraph NER["Part 2: NER"]
        TOK[Tokenize]
        BERT[BERT NER<br/>ONNX]
        ENT[Entities]
    end

    IMG --> TESS
    TESS --> TXT
    TXT --> TOK
    TOK --> BERT
    BERT --> ENT

    style TESS stroke:#f60,stroke-width:3px
    style BERT stroke:#f60,stroke-width:3px
    style ENT stroke:#090,stroke-width:3px
```

Zwei Schritte: zwei Modelle: beide, die lokal laufen, ., Let.', bauen jedes Teil:

---


# OCR mit Tesseract

[Tesseract](https://github.com/tesseract-ocr/tesseract) ist die Standard-Open---Source-OCR-Engine, die wir verwenden [Tesseract-.-NET](https://github.com/charlesw/tesseract)Ein Wrapper mit einem Gewicht von mehr als 1 kg

```bash
dotnet add package Tesseract
```

Sie benötigen auch die trainierten Daten-Dateien. `eng.traineddata` von [tessdata](https://github.com/tesseract-ocr/tessdata) und legen Sie es in einem `tessdata` Ordner„ ."

```csharp
using Tesseract;

public static string ExtractText(string imagePath)
{
    using var engine = new TesseractEngine("./tessdata", "eng", EngineMode.Default);
    using var img = Pix.LoadFromFile(imagePath);
    using var page = engine.Process(img);

    return page.GetText();
}
```

Dass ,''s es ruft `ExtractText("invoice.png")` und du erhältst eine Zeichenkette

> **Wichtig**: `TesseractEngine` ist teuer, um zu erstellen. In realen Anwendungen...... erstellen Sie es einmal und verwenden Sie es wieder..

### Einschränkungen

Tesseract funktioniert gut für **clean-,-High---Kontrasttext in Standard Fonts**Es kämpft mit...

- Stylisierte oder dekorative Schriftarten
- Scans oder Fotos in niedriger Qualität
- rotierter oder gekrümmter Text
- Text auf komplexen Hintergründen
- Animierte GIF mit Untertiteln
- Hyphenierte Zeilenumbrüche`inter-\nnational`Vor der NER-Verarbeitung kann es erforderlich sein, dass -

Für Produktionssysteme, die mit dem seltsamen Zeug umgehen müssen, siehe [Die Pipeline von Three--.Tier OCR](/blog/constrained-fuzzy-image-ocr-pipeline)-- , die Florence .-2 ONNX als mittlere Ebene und Vision LLM Eskalation für harte Fälle hinzufügt .

Für dieses Tutorial Nehmen wir an, dass Sie saubere Bilder oder Text aus einer anderen Quelle haben.

> In der Praxis werden Sie normalerweise OCR-Ausgang normalisieren möchten, bevor Sie es an NER-. übergeben.

---


# Teil 2:-NER mit ONNX

## Warum dieser Ansatz funktioniert

Bevor du in Code eintauchst, lass 's verstehen, was wir tatsächlich tun, wenn du einen Entwickler von C.# bist, der 's nie berührt hat.

### Was ist NER-??

**Genannte Entitätserkennung** ist ein gelöstes Problem, das Forscher neuronale Netzwerke trainiert haben, die Text lesen können und die interessanten Bits hervorheben.

- **GESAMTBETRAG** Personennamen: John Smith, Mr. Smith, Dr. ., Jane Doe, Mrs.
- **ORG** Organisationen von ("Microsoft, ",, ", NHS, MSC4, MCSK5, ACME Corp,"),
- **LOC** Standort von ("London",
- **MISC** Sonstige Einrichtungen

Das Modell doesn't .'-t ?"-versteht den Text ,--it-'s lernte statistische Muster aus Millionen von gekennzeichneten Beispielen **NER ist Feature-Extraktion. ,, nicht argumentieren.** Es ist ein Muster, das auf Steroiden passt

### Warum ONNX-??

**ONNX** Öffnen Sie Neural Network Exchange.) ist ein Standardformat für ML-Modelle. **Gefrorene Inferenz DLL** für neuronale Netzwerke, feste Gewichte in den Tensoren von ,, ohne Trainingslogik, ohne Zufälligkeit

```mermaid
flowchart LR
    subgraph Training["Training (Python)"]
        PT[PyTorch Model]
        TF[TensorFlow Model]
    end

    subgraph Export["Export Once"]
        ONNX[model.onnx]
    end

    subgraph Runtime["Run Anywhere"]
        CS[C# App]
        CPP[C++ App]
        JS[JavaScript App]
    end

    PT --> ONNX
    TF --> ONNX
    ONNX --> CS
    ONNX --> CPP
    ONNX --> JS

    style ONNX stroke:#f60,stroke-width:4px
    style CS stroke:#090,stroke-width:3px
```

Die wichtigsten Erkenntnisse **jemand anderes hat die harte Arbeit gemacht** Schulung des Modells in Python, )., Du führst die Inferenz einfach in C.#.,

### Warum nicht einfach ein LLM-??

Sie konnten Text an GPT--4 schicken und bitten, die Leute und Firmen in diesem Text zu finden.

Anfluggeschwindigkeit Geschwindigkeit Kosten pro Stunde Datenschutz Datenkonsistenz
|-------------------|-------|--------------|-----------------------|-------------|
| **ONNX NER** |-~50-|-M-$0-M|-M -|-Hoch-|-
| **Lokale LLM-API** Kleine Modelle können flockig sein.
| **LLM API** Daten, die extern gesendet werden

LLMs sind ideal für komplexe Argumentationen..-Für Musterextraktion im Maßstab.,-Ein dediziertes Modell ist schneller und frei.

---


## Die Pipeline

Hier ist das, was wir bauen

```mermaid
flowchart LR
    subgraph Input
        TEXT[Raw Text]
    end

    subgraph Tokenization["Step 1: Tokenization"]
        TOK[Split into tokens]
        IDS[Convert to IDs]
    end

    subgraph Model["Step 2: ONNX Inference"]
        BERT[BERT Model]
        LOGITS[Logits Output]
    end

    subgraph Output["Step 3: Decode"]
        LABELS[BIO Labels]
        ENT[Entities]
    end

    TEXT --> TOK
    TOK --> IDS
    IDS --> BERT
    BERT --> LOGITS
    LOGITS --> LABELS
    LABELS --> ENT

    style BERT stroke:#f60,stroke-width:4px
    style ENT stroke:#090,stroke-width:3px
```

Jeder Schritt ist einfach.

---


## Schritt Download des Modells

Sie benötigen drei Dateien von HuggingFace-. . Laden Sie sie manuell in einen Ordner herunter `./models/ner/`):

Datei Datei Größe Adresse Adresse Adresse
|------|------|-----|
| `model.onnx` |-MB-|-Mb [Herunterladen](https://huggingface.co/protectai/bert-base-NER-onnx/resolve/main/model.onnx) |
| `vocab.txt` |" , , ~230, KB , |" [Herunterladen](https://huggingface.co/protectai/bert-base-NER-onnx/resolve/main/vocab.txt) |
| `config.json` |" , , ~1, KB , |" [Herunterladen](https://huggingface.co/protectai/bert-base-NER-onnx/resolve/main/config.json) |

Das Modell ist [BERT---base---NER](https://huggingface.co/dslim/bert-base-NER) in das ONNX-Format exportiert durch [Schutz](https://huggingface.co/protectai/bert-base-NER-onnx).

Dein Ordner sollte so aussehen

```
models/
  ner/
    model.onnx      (the neural network)
    vocab.txt       (word → ID mapping)
    config.json     (label definitions)
```

---


## Schritt Projekt-Einrichtung

Erstellen Sie eine neue Konsolen-App und fügen Sie die NuGet Pakete.

```bash
dotnet new console -n NerDemo
cd NerDemo
dotnet add package Microsoft.ML.OnnxRuntime
dotnet add package Microsoft.ML.Tokenizers
```

Dass-''s es -.' Zwei Pakete

- **OnnxRuntime** Führt das Modell
- **ML-.-Tokenisatoren** Behandelt Text-Token-Konvertierung

---


## Schritt zum Verständnis der Tokenisierung

Bevor das Modell Text verarbeiten kann, müssen wir es in Zahlen umwandeln. **Tokenisierung**.

```mermaid
flowchart TD
    subgraph Input
        TEXT["John works at Microsoft"]
    end

    subgraph Tokenize["Tokenization"]
        T1["[CLS]"]
        T2["John"]
        T3["works"]
        T4["at"]
        T5["Microsoft"]
        T6["[SEP]"]
    end

    subgraph IDs["Token IDs"]
        I1["101"]
        I2["1287"]
        I3["2573"]
        I4["1120"]
        I5["7513"]
        I6["102"]
    end

    TEXT --> T1 & T2 & T3 & T4 & T5 & T6
    T1 --> I1
    T2 --> I2
    T3 --> I3
    T4 --> I4
    T5 --> I5
    T6 --> I6

    style T1 stroke:#c00,stroke-width:3px
    style T6 stroke:#c00,stroke-width:3px
    style I2 stroke:#090,stroke-width:3px
    style I5 stroke:#090,stroke-width:3px
```

Wesentliche Punkte

- `[CLS]` und `[SEP]` sind spezielle Token, die Satzgrenzen markieren
- Jedes Wort wird zu einer Zahl aus `vocab.txt`
- Das Modell sieht nur Zahlen nie den tatsächlichen Text.

### Laden des Tokenizers

```csharp
using Microsoft.ML.Tokenizers;

// Load the vocabulary file
var vocabPath = "./models/ner/vocab.txt";

var options = new BertOptions
{
    LowerCaseBeforeTokenization = false,  // BERT-NER is case-sensitive!
    UnknownToken = "[UNK]",
    ClassificationToken = "[CLS]",
    SeparatorToken = "[SEP]",
    PaddingToken = "[PAD]"
};

using var stream = File.OpenRead(vocabPath);
var tokenizer = BertTokenizer.Create(stream, options);
```

Warum? `LowerCaseBeforeTokenization = false`Dieses Modell wurde auf kassierten Text trainiert ,. , , " , John , und ," , John , " , haben unterschiedliche Bedeutungen , - , ein Name wahrscheinlich nicht

> **Wichtig**Der Tokenizer *muss* Passen Sie das Modell genau mit einem anderen Vokabeln , , , Gehäuse-Option oder spezielle Token-IDs wird lautlos degradieren Ergebnisse ,. , immer verwenden die `vocab.txt` dass Schiffe mit dem Modell

### Text markieren

```csharp
var text = "John Smith works at Microsoft in London.";

// Tokenize (splits into subwords)
var encoded = tokenizer.EncodeToTokens(text, out _);

// Get special token IDs
var clsId = 101;  // [CLS] token
var sepId = 102;  // [SEP] token

// Build the full sequence: [CLS] + tokens + [SEP]
var tokenIds = new List<int> { clsId };
tokenIds.AddRange(encoded.Select(t => t.Id));
tokenIds.Add(sepId);

// Also keep the text tokens for later
var tokens = new List<string> { "[CLS]" };
tokens.AddRange(encoded.Select(t => t.Value));
tokens.Add("[SEP]");
```

Nach diesem haben wir

- `tokenIds`: `[101, 1287, 3455, 2573, 1120, 7513, 1999, 2414, 119, 102]`
- `tokens`: `["[CLS]", "John", "Smith", "works", "at", "Microsoft", "in", "London", ".", "[SEP]"]`

---


## Schritt Das Modell laufen lassen

Jetzt geben wir diese Zahlen in das ONNX-Modell ein Das Modell gibt an jeder Position die Punktzahlen für jedes mögliche Label zurück.

```mermaid
flowchart LR
    subgraph Inputs
        IDS["Token IDs<br/>[101, 1287, 3455, ...]"]
        MASK["Attention Mask<br/>[1, 1, 1, ...]"]
    end

    subgraph Model
        ONNX["BERT NER<br/>model.onnx"]
    end

    subgraph Outputs
        LOG["Logits<br/>[batch, seq_len, 9]"]
    end

    IDS --> ONNX
    MASK --> ONNX
    ONNX --> LOG

    style ONNX stroke:#f60,stroke-width:4px
```

### Laden des Modells

```csharp
using Microsoft.ML.OnnxRuntime;
using Microsoft.ML.OnnxRuntime.Tensors;

// Configure for best performance
var sessionOptions = new SessionOptions
{
    GraphOptimizationLevel = GraphOptimizationLevel.ORT_ENABLE_ALL,
    IntraOpNumThreads = Math.Min(4, Environment.ProcessorCount)
};

// Load the model (takes ~2 seconds first time)
var session = new InferenceSession("./models/ner/model.onnx", sessionOptions);
```

> **Wichtig**Erstellen Sie die Tokenizer- und Inferenzsession einmal.(.Singleton, /.service, ). und verwenden Sie sie wieder.MS K4. Don.'.t reload pro document.`InferenceSession` ist teuer zu erstellen.

### Eingaben vorbereiten

Das Modell erwartet

- **Eingabe-_-IDs**Unsere Token-IDs als `long[]`
- **Achtung_ Mask**1 für echte Tokens, ,, MSC3 für Polsterung

```csharp
// Pad to a fixed length (BERT requires fixed shapes; powers of 2 are cache-friendly)
int sequenceLength = 64;  // or 128, 256, 512

var inputIds = new long[sequenceLength];
var attentionMask = new long[sequenceLength];

for (int i = 0; i < sequenceLength; i++)
{
    if (i < tokenIds.Count)
    {
        inputIds[i] = tokenIds[i];
        attentionMask[i] = 1;
    }
    else
    {
        inputIds[i] = 0;   // PAD token
        attentionMask[i] = 0;
    }
}
```

### Ausführen von Schlussfolgerungen

```csharp
// Create tensors (shape: [batch_size=1, sequence_length])
var inputIdsTensor = new DenseTensor<long>(inputIds, [1, sequenceLength]);
var attentionMaskTensor = new DenseTensor<long>(attentionMask, [1, sequenceLength]);

// Build inputs
var inputs = new List<NamedOnnxValue>
{
    NamedOnnxValue.CreateFromTensor("input_ids", inputIdsTensor),
    NamedOnnxValue.CreateFromTensor("attention_mask", attentionMaskTensor)
};

// Run the model
using var results = session.Run(inputs);

// Get output logits
var output = results.First(r => r.Name == "logits");
var logits = output.AsTensor<float>();
```

Die Ausgabe `logits` hat Form `[1, sequence_length, 9]`Mögliche Beschriftungen für jede Tokenposition

---


## Schritt - Dekodierung der Ausgabe

Die Modellausgänge rohe Punktzahlen... Wir brauchen zu...

1. Finden Sie für jeden Token das höchste ,-'scoring Label
2. Diese Etiketten in tatsächliche Entities umrechnen

### BIO-Tags verstehen

Das Modell verwendet **BIO-Notation**:

```mermaid
flowchart LR
    subgraph Tokens
        T1["John"]
        T2["Smith"]
        T3["works"]
        T4["at"]
        T5["Microsoft"]
    end

    subgraph Labels
        L1["B-PER"]
        L2["I-PER"]
        L3["O"]
        L4["O"]
        L5["B-ORG"]
    end

    T1 --> L1
    T2 --> L2
    T3 --> L3
    T4 --> L4
    T5 --> L5

    style L1 stroke:#090,stroke-width:3px
    style L2 stroke:#090,stroke-width:3px
    style L5 stroke:#00f,stroke-width:3px
```

- **BESCHÄFTIGUNG** Beginn einer Person Entität
- **EINLEITUNG** Innerhalb einer Person
- **O Die** Ausserhalb irgendeiner Entität nicht interessant
- **B.--ORG** Beginn einer Organisation

Dies lässt das Modell mit Multi---Wort-Entitäten wie z.B. ", John Smith, ", oder ", United Kingdom, MKK4, umgehen

### Das Mapping des Etiketts

```csharp
// These are the 9 labels from the protectai/bert-base-NER-onnx config.json (id2label field)
string[] labels =
{
    "O",       // 0: Outside any entity
    "B-MISC",  // 1: Beginning of Miscellaneous
    "I-MISC",  // 2: Inside Miscellaneous
    "B-PER",   // 3: Beginning of Person
    "I-PER",   // 4: Inside Person
    "B-ORG",   // 5: Beginning of Organization
    "I-ORG",   // 6: Inside Organization
    "B-LOC",   // 7: Beginning of Location
    "I-LOC"    // 8: Inside Location
};
```

> **Anmerkung**Dieses spezifische Modell verwendet das Standard-Zeichen CoNLL schema... Einige ONNX-Exporte enthalten Etikettennamen in `config.json` (`id2label` Wenn Sie Modelle tauschen, lesen Sie die Etiketten von config statt hardcoding.

### Das beste Label finden

Für jedes Zeichen wählen wir das Etikett mit dem höchsten Logit-Score.

```csharp
var predictions = new List<(string Token, string Label, float Confidence)>();

int numLabels = 9;

for (int i = 0; i < tokens.Count; i++)
{
    // Skip special tokens
    if (tokens[i] is "[CLS]" or "[SEP]" or "[PAD]")
        continue;

    // Find highest scoring label
    float maxScore = float.MinValue;
    int maxIndex = 0;

    for (int j = 0; j < numLabels; j++)
    {
        float score = logits[0, i, j];
        if (score > maxScore)
        {
            maxScore = score;
            maxIndex = j;
        }
    }

    // Convert logit to probability (softmax)
    float confidence = Softmax(logits, i, numLabels, maxIndex);

    predictions.Add((tokens[i], labels[maxIndex], confidence));
}
```

Das `Softmax` Funktion wandelt Rohpunkte in Wahrscheinlichkeiten um

```csharp
static float Softmax(Tensor<float> logits, int position, int numLabels, int targetIndex)
{
    // Find max for numerical stability
    float maxLogit = float.MinValue;
    for (int j = 0; j < numLabels; j++)
        maxLogit = Math.Max(maxLogit, logits[0, position, j]);

    // Compute softmax
    float sumExp = 0f;
    for (int j = 0; j < numLabels; j++)
        sumExp += MathF.Exp(logits[0, position, j] - maxLogit);

    return MathF.Exp(logits[0, position, targetIndex] - maxLogit) / sumExp;
}
```

> **Hinweis zum Vertrauen**Die Softmax-Werte sind *relativ*Nicht kalibrierte Wahrscheinlichkeiten von .... Sie sind nützlich für Ranking und Schwellenbildung, aber nicht behandeln `0.92` Verwenden Sie sie, um niedrig zu filtern......

---


## Schritt zur Extraktion von Unternehmen

Jetzt haben wir per-- ,token Vorhersagen ,. , Wir müssen sie in Entities zusammenführen , . ,

Erstes, das WordPiece zusammenführen Helfer... diese Handle `##` Subwords und Interpunktionsabstand korrekt

```csharp
static void AppendWordPiece(StringBuilder sb, string token)
{
    if (string.IsNullOrEmpty(token)) return;

    // WordPiece continuation: "##soft" → append without space
    if (token.StartsWith("##", StringComparison.Ordinal))
    {
        sb.Append(token.AsSpan(2));
        return;
    }

    // No leading space if first token or if punctuation
    if (sb.Length > 0 && !IsPunctuationToken(token))
        sb.Append(' ');

    sb.Append(token);
}

static bool IsPunctuationToken(string token) =>
    token.Length == 1 && char.IsPunctuation(token[0]);

static string MergeWordPieces(IEnumerable<string> tokens)
{
    var sb = new StringBuilder();
    foreach (var t in tokens)
        AppendWordPiece(sb, t);
    return sb.ToString();
}
```

Nun ist das Entity-Konfidenzgefühl der Entity **Mindestsicherheit** über die Spanne ,- ,konservativ , , , so dass ein einzelner schwacher Token wird versteckt durch eine Durchschnittung , : ,

```csharp
public sealed class Entity
{
    public required string Text { get; init; }
    public required string Type { get; init; }  // PER, ORG, LOC, MISC
    public required float Confidence { get; init; }
}

static List<Entity> ExtractEntities(
    IReadOnlyList<(string Token, string Label, float Confidence)> predictions)
{
    var entities = new List<Entity>();

    string? currentType = null;
    var currentTokens = new List<string>();
    float currentConfidence = 1.0f;

    void Flush()
    {
        if (currentType == null || currentTokens.Count == 0) return;

        entities.Add(new Entity
        {
            Type = currentType,
            Text = MergeWordPieces(currentTokens),
            Confidence = currentConfidence
        });

        currentType = null;
        currentTokens.Clear();
        currentConfidence = 1.0f;
    }

    foreach (var (token, label, conf) in predictions)
    {
        // Continue current entity if model says I-<same type>
        if (currentType != null && label == $"I-{currentType}")
        {
            currentTokens.Add(token);  // keep ## form, merge handles it
            currentConfidence = Math.Min(currentConfidence, conf);
            continue;
        }

        // New entity begins
        if (label.StartsWith("B-", StringComparison.Ordinal))
        {
            Flush();
            currentType = label[2..];
            currentTokens.Add(token);
            currentConfidence = conf;
            continue;
        }

        // Anything else (O, or I- without matching current type) ends the entity
        Flush();
    }

    Flush();
    return entities;
}
```

Anmerkung: WordPiece wird vollständig von `MergeWordPieces`Keine spezielle Steuerungsströmung erforderlich

---


## Vollständiges Beispiel

Hier ist ein minimales Arbeitsbeispiel, das Sie kopieren und ausführen können.

```csharp
using System.Text;
using Microsoft.ML.OnnxRuntime;
using Microsoft.ML.OnnxRuntime.Tensors;
using Microsoft.ML.Tokenizers;

// === Configuration ===
var modelPath = "./models/ner/model.onnx";
var vocabPath = "./models/ner/vocab.txt";

// === Load tokenizer ===
var bertOptions = new BertOptions
{
    LowerCaseBeforeTokenization = false,
    UnknownToken = "[UNK]",
    ClassificationToken = "[CLS]",
    SeparatorToken = "[SEP]"
};

using var vocabStream = File.OpenRead(vocabPath);
var tokenizer = BertTokenizer.Create(vocabStream, bertOptions);

// === Load model ===
var sessionOptions = new SessionOptions
{
    GraphOptimizationLevel = GraphOptimizationLevel.ORT_ENABLE_ALL
};
using var session = new InferenceSession(modelPath, sessionOptions);

// === Process text ===
var text = "John Smith, CEO of Microsoft, announced the acquisition in London yesterday.";

// Tokenize
var encoded = tokenizer.EncodeToTokens(text, out _);

// Build sequence with special tokens
var tokens = new List<string> { "[CLS]" };
tokens.AddRange(encoded.Select(t => t.Value));
tokens.Add("[SEP]");

var rawIds = new List<int> { 101 };  // [CLS]
rawIds.AddRange(encoded.Select(t => t.Id));
rawIds.Add(102);  // [SEP]

// Pad to fixed length
int seqLen = 64;
var inputIds = new long[seqLen];
var attentionMask = new long[seqLen];

for (int i = 0; i < seqLen; i++)
{
    inputIds[i] = i < rawIds.Count ? rawIds[i] : 0;
    attentionMask[i] = i < rawIds.Count ? 1 : 0;
}

// === Run inference ===
var inputs = new List<NamedOnnxValue>
{
    NamedOnnxValue.CreateFromTensor("input_ids",
        new DenseTensor<long>(inputIds, [1, seqLen])),
    NamedOnnxValue.CreateFromTensor("attention_mask",
        new DenseTensor<long>(attentionMask, [1, seqLen]))
};

using var results = session.Run(inputs);
var logits = results.First().AsTensor<float>();

// === Decode predictions ===
string[] labels = ["O", "B-MISC", "I-MISC", "B-PER", "I-PER", "B-ORG", "I-ORG", "B-LOC", "I-LOC"];

// WordPiece merge helper
static string MergeWordPieces(List<string> tokens)
{
    var sb = new StringBuilder();
    foreach (var t in tokens)
    {
        if (t.StartsWith("##", StringComparison.Ordinal))
            sb.Append(t.AsSpan(2));
        else if (sb.Length > 0 && t.Length > 0 && !char.IsPunctuation(t[0]))
            sb.Append(' ').Append(t);
        else
            sb.Append(t);
    }
    return sb.ToString();
}

Console.WriteLine($"Input: {text}\n");
Console.WriteLine("Entities found:");

string? currentType = null;
var currentTokens = new List<string>();

for (int i = 1; i < tokens.Count - 1; i++)  // Skip [CLS] and [SEP]
{
    var token = tokens[i];

    // Find best label
    int bestIdx = 0;
    float bestScore = float.MinValue;
    for (int j = 0; j < 9; j++)
    {
        if (logits[0, i, j] > bestScore)
        {
            bestScore = logits[0, i, j];
            bestIdx = j;
        }
    }

    var label = labels[bestIdx];

    // Continue current entity if model says I-<same type>
    if (currentType != null && label == $"I-{currentType}")
    {
        currentTokens.Add(token);
        continue;
    }

    // New entity begins
    if (label.StartsWith("B-"))
    {
        if (currentType != null)
            Console.WriteLine($"  [{currentType}] {MergeWordPieces(currentTokens)}");

        currentType = label[2..];
        currentTokens = [token];
        continue;
    }

    // Anything else ends the current entity
    if (currentType != null)
        Console.WriteLine($"  [{currentType}] {MergeWordPieces(currentTokens)}");
    currentType = null;
    currentTokens.Clear();
}

// Output last entity
if (currentType != null)
    Console.WriteLine($"  [{currentType}] {MergeWordPieces(currentTokens)}");
```

**Ausgabe„ :"**

```
Input: John Smith, CEO of Microsoft, announced the acquisition in London yesterday.

Entities found:
  [PER] John Smith
  [ORG] Microsoft
  [LOC] London
```

---


## WordPiece-Unterwörter verstehen

BERT-Verwendung **WordPiece Tokenisierung**, die unbekannte Wörter in Subworte aufspaltet , , `##` Präfix bedeutet " Fortbestehen des vorherigen Wortes "MSC1"

```mermaid
flowchart LR
    subgraph Original
        W1["Elasticsearch"]
    end

    subgraph Tokenized
        T1["Elastic"]
        T2["##search"]
    end

    subgraph Merged
        M1["Elasticsearch"]
    end

    W1 --> T1 & T2
    T1 --> M1
    T2 --> M1

    style T2 stroke:#c00,stroke-width:3px
```

Das `MergeWordPieces` Helfer handhabt diese `##` Token werden ohne einen Raum angehängt. `"Elasticsearch"` statt `"Elastic search"`.

---


## Leistungstipps

### Mehrsprachige Texte für die Batch

Wenn du viele Texte hast, dann verarbeite sie in Chargen.

```csharp
// Instead of: 1 text × 1 inference = 50ms
// Do: 16 texts × 1 inference = 100ms (6ms per text)

int batchSize = 16;
var batchInputIds = new long[batchSize, seqLen];
// ... fill batch ...
var tensor = new DenseTensor<long>(batchInputIds, [batchSize, seqLen]);
```

### Verwenden Sie Smart Bucketing

Verwenden Sie immer den kleinsten Eimer, der passt.

```csharp
int[] buckets = [32, 64, 128, 256, 512];
int targetLength = buckets.FirstOrDefault(b => b >= tokenIds.Count);
if (targetLength == 0) targetLength = 512;
```

In der Praxis ist ONNX NER schnell genug zum Laufen **Inline während der Einnahme**Sie können Entitäten als Dokumente extrahieren, anstatt sie für später anzusprechen.

### GPU-Beschleunigung

Für einen hohen Durchsatz verwenden Sie DirectML (.Windows.). oder CUDAM:.

```bash
dotnet add package Microsoft.ML.OnnxRuntime.DirectML  # Windows GPU
# or
dotnet add package Microsoft.ML.OnnxRuntime.Gpu       # NVIDIA CUDA
```

```csharp
var options = new SessionOptions();
options.AppendExecutionProvider_DML();  // Use GPU
```

---


## Wann dies gegen LLM zu verwenden ist

Verwendung von ONNX-NER-LLM-L(-GPT-S-4/-Claude-)-D|
|--------------|------------------------|
Hohes Volumen der Doktrinen der ) Aus einer Analyse der - Aus
Standard-Entitäten - (Peoples -,Porgs - ,Pords - )Pendants-|Pongs - Benutzerdefinierte Entitätstypen
Datenschutz-Sensitive Daten Wenn Sie Erklärungen benötigen
Deterministische Pipelines - Sondierungsanalyse - |
Beschreibung des Merkmals Extraktion der | Interpretation der Synthese der MSC3

Beide Ansätze arbeiten – .-Sie lösen **unterschiedliche Probleme**NER extrahiert Struktur, LLMs Grund für Bedeutung

---


## Das größere Bild

Dieses Muster**Deterministische Extraktion mit gefrorenen Modellen, gefolgt von optionaler Synthese später**- ist viel besser als Rohtext in einen LLM zu schieben und zu hoffen, dass er sich verhält.

NER ist nicht etwas, das du "-agentify -".- It-'-s-Infrastruktur -.- Sie laufen es auf Ingestion -,- speichern die Entitäten und verwenden sie stromabwärts für die Filterung - ,-Verknüpfung von , oder Fütterung in anspruchsvollere Pipelines -MSC8-

Der gleiche Ansatz gilt für andere Feature-Extraktionsaufgaben.:.Embeddings.,.Klassifikation.MSC2.Sentiment... Train einmal, oder verwenden Sie vortrainiert ),. Export nach ONNX.,. Laufen überall. ,. deterministisch.....

> **Wo das passt**Diese OCR + NER-Pipeline ist ein Baustein für das gesamte Bild.- how extrahierte Entitäten fließen in den Graphenbau ein. [Reduzierte RAG](/blog/reduced-rag-concept) und der [***klar*RAG** Dokumentation](https://github.com/scottgal/lucidrag)Die Entitäten, die Sie hier extrahieren, werden zu Knoten. Die Dokumente werden zu Kanten. Die LLM sieht nur, was sie braucht.

---


## Aufkommen

**Bibliotheken Modelle**:

- **[Tesseract-.-NET](https://github.com/charlesw/tesseract)** Wrapper für Tesseract OCR
- **[tessdata](https://github.com/tesseract-ocr/tessdata)** Ausgebildete Datendateien für Tesseract
- **[BERT---Basis ONNX --NER](https://huggingface.co/protectai/bert-base-NER-onnx)** Das NER-Modell, das wir verwenden
- **[ONNX Laufzeit](https://onnxruntime.ai/docs/)** Offizielle Dokumentation
- **[ML-.-Tokenisatoren](https://www.nuget.org/packages/Microsoft.ML.Tokenizers)** Microsoft®'s tokenizer Bibliothek

**Nächster Schritt**:

- **[Teil Das NuGet-Paket](/blog/simple-ocr-ner-nuget)** Diese Pipeline als One---line NuGet installieren mit OpenCV Preprocessing-,. Florence.-2. Vision,. Microsoft, ..Recognisers, ,. und einem CLI-Tool

**Verwandte Artikel**:

- **[Die Pipeline von Three--.Tier OCR](/blog/constrained-fuzzy-image-ocr-pipeline)** Wenn die einfache OCR ausreichend ist
- **[Reduzierte RAG](/blog/reduced-rag-concept)** Wo extrahierte Entitäten in das größere Bild passen
- **[*klar*RAG](https://github.com/scottgal/lucidrag)** Vollständige Implementierung mit Entity-Deduplizierung und Graphenkonstruktion