# Enkel OCR och NER Feature Extraction i C# med ONNX

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

I ” ' ” har byggt [***klar*RAG**](https://www.lucidrag.com) Jag läser sociala medier där folk frågar samma sak. [djupt i OCR-rummet](/blog/constrained-fuzzy-image-ocr-pipeline) Jag tänkte att jag skulle skriva ett 'beginner friendly ' sätt att göra detta på icke-

Du har bilder med text.

Detta artikel visar **så enkel som möjligt** Pipeline: **Tesseract** för textuttvingning, så **BERT NER** ( via ONNX) för enhetens utmärkelse

Deterministisk betyder här fixera versioner , fixera språkdata, och inget adaptivt inlärning på starttid

> **NuGet kommer snart** - I'm förpackar detta till en enkel `mostlylucid.ocrner` bibliotek. För tillfället

[TOC]

---


## Den fulla Pipelinen

```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
```

Två steg: ,, två modeller, ,, båda igång lokalt. ., Låt ' bygga varje del.

---


# Del 1: OCR med Tesseract

[Tesseract](https://github.com/tesseract-ocr/tesseract) är standardopen, -, källkods OCR-motor. [Tesseract.NET](https://github.com/charlesw/tesseract), en C

```bash
dotnet add package Tesseract
```

Du behöver också de tränade datafilerna. ladda ner `eng.traineddata` från [tessdata](https://github.com/tesseract-ocr/tessdata) och placera den i en `tessdata` folder.

```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();
}
```

Det var "'", det var . `ExtractText("invoice.png")` och du får en string.

> **Nyckel**: `TesseractEngine` är dyrt att skapa

### Begränsningar

Tesseract fungerar bra för **ren, hög- kontrasttext i standardfonter**. Den kämpar med

- Stiliserade eller dekorativa fonter
- Low-kvalitélla skanningar eller foton
- Roterade eller böjda text
- Text på komplexa bakgrunder
- Animaerade GIFs med undertitel
- Hyfenerade linjer bryter (`inter-\nnational`) kan behöva processing efter - innan NER

För produktionssystem som behöver hantera konstiga saker [Det tre---Tier-OCR-röret](/blog/constrained-fuzzy-image-ocr-pipeline)- som lägger till Florence -2 ONNX som en medelklass och Vision LLM uppgradering för hårda fall

För detta inlärning, antar vi att du har ren bild eller text från en annan källa.

> I praktiken, vill du vanligtvis normalisera OCR-uttrycket ( trim whitespace , kollapsa upprepade nya linjer , fixa uppenbart hyphenation M SK4 innan du skickar ut det till NERM SK5

---


# Del 2: NER med ONNX

## Varför den här metoden fungerar

Innan vi ska dyka in i kod, låt oss förstå vad vi gör. Om du är en C-utvecklare som aldrig rört ML, så är det här för dig.

### Vad är NER?

**Namerade entitetserkänning (NER)** är ett löst problem.

- **PER** - Personen namn "John Smith", "DrM SK4 Jane DoeMST5
- **ORG** - Organiseringar
- **LOC** - Lokalisering | " | London | ", | МSK3 | Mount Everest
- **MISC** - Andra entityder "COVID-19",

Modellet förstår inte texten, utan lär sig statistiska mönster från miljontals märkta exempel. **NER är dragning av dragningar** Det matchar mönster på steroider

### Varför ONNX?

**NX** (, Open Neural Network Exchange, ) är ett standardformat för ML-modeller. **frozen inference DLL** för nervnätverk

```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
```

Den viktigaste insikten **någon annan gjorde hårt arbete** (trening av modellen i Python ). Du kör bara slutsatsen i C

### Varför inte bara använda en LLM

Du skulle kunna skicka ett sms till GPT, och fråga: "" Hitta människorna och företagen i det här smset.

| tillvägagångspunkt | | | Rymden || | kostnaderna per |1000 | dokument | | | integritet | | | konsistens ||
|-------------------|-------|--------------|-----------------------|-------------|
| **Northeastern** | ~50ms | ♫ ♫ $0 ♫ | ♫ Lokalt | ♫ Högt ♫
| **Lokal LLM API** | 4-30s | | | $0 ♫ ♫ | ♫ Små modeller kan vara fläckiga ♫
| **LLM API** |

LLMs är bra för komplext resonemang. För mönster extraktion på skalaM SK1 en dedikerad modell är 40x snabbare och gratis .

---


## Pipelinet

Här är vad vi bygger

```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
```

Varje steg är enkelt.

---


## steg 1: ladda ner modellen

Du behöver tre filer från HuggingFace. ladda ner dem manuell till en ordning (e .gM SK3 `./models/ner/`):

| Att filera | Storleken | | | URL |
|------|------|-----|
| `model.onnx` |  ~430MB | [ladda ner](https://huggingface.co/protectai/bert-base-NER-onnx/resolve/main/model.onnx) |
| `vocab.txt` | ~230KB  | [ladda ner](https://huggingface.co/protectai/bert-base-NER-onnx/resolve/main/vocab.txt) |
| `config.json` | ~1KB  | [ladda ner](https://huggingface.co/protectai/bert-base-NER-onnx/resolve/main/config.json) |

Modellet är [bert-bas-NER](https://huggingface.co/dslim/bert-base-NER) exporterade till ONNX format av: [skyddai](https://huggingface.co/protectai/bert-base-NER-onnx).

Din folder borde se ut som:

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

---


## steg 2: Projektuppsättning

Att skapa en ny konsolapp och lägga till NuGet paket

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

Det är det. Två paket.

- **OnnxRuntime** - kör modellen
- **ML.Tokenizer** - Handler text → token omvandling

---


## steg 3: förståndet av tokenisering

Innan modellen kan bearbeta texten, måste vi konvertera den till siffror. **symbolisering**.

```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
```

Nyckelpunkter:

- `[CLS]` och `[SEP]` är speciella token som markerar meningsgränser
- Varje ord blir ett tal från `vocab.txt`
- Modellet ser bara siffror , aldrig den faktiska texten

### Att ladda ut tokenisatorn

```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);
```

Varför? `LowerCaseBeforeTokenization = false`? Den här modellen undervisades på kasserade texter . | | " | John · " | och " | john · M SK5 | har olika betydelser - | en | ' | är troligtvis ett namn

> **Nyckel**: Markifieren *måste* matchar modellen exakt. Med hjälp av en annan vocab `vocab.txt` som skepper med modellen.

### Att tokenisera text

```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]");
```

Efter detta har vi

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

---


## steg 4: Att köra modellen

Nu lägger vi in dessa siffror i den ONNX-modellen.

```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
```

### Lade upp modellen

```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);
```

> **Nyckel**: Att skapa tokenisatorn och inferenssessionen en gång ♫ (singleton ♫`InferenceSession` är dyrt att skapa

### Att förbereda inmatningar

modellen förväntar sig:

- **input_ids**: Våra tokensidentifikationer som `long[]`
- **uppmärksamhet_ masker**: 1 för riktiga tokens

```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;
    }
}
```

### Running Inference

```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>();
```

Utgången `logits` har form `[1, sequence_length, 9]`-9 möjliga etiketter för varje tokenposition

---


## steg 5: Dekodering av utgången

Modellet ger ut råresultat. Vi behöver

1. Hitta den högsta tärningsetikern för varje token
2. Omvandla dessa etiketter till faktiska enheter

### Understanding BIO Tags

Modellet använder **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
```

- **B-PER** = Början av en Person
- **I-PER** = Inuti ( Kontinuation | ) | en Person
- **O** = Utanför någon enhet ( inte intressant
- **B-ORG** = Början av en organisation

Detta låter modellen hantera flera ordbeståndsdelar som "John Smith

### Etikettens kartläggning

```csharp
// These are the 9 labels the model was trained on (CoNLL-2003 dataset)
string[] labels =
{
    "O",       // 0: Outside any entity
    "B-PER",   // 1: Beginning of Person
    "I-PER",   // 2: Inside Person
    "B-ORG",   // 3: Beginning of Organization
    "I-ORG",   // 4: Inside Organization
    "B-LOC",   // 5: Beginning of Location
    "I-LOC",   // 6: Inside Location
    "B-MISC",  // 7: Beginning of Miscellaneous
    "I-MISC"   // 8: Inside Miscellaneous
};
```

> **Beteckning**: Den här specifika modellen använder standarden 9-label CoNLL-schemat `config.json` (`id2label` fält). Om du byter modeller, läsa etiketter från konfiguration snarare än att koda

### Att hitta den bästa etiketten

För varje token, väljer vi etiketten med den högsta logit poängenM SK1

```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));
}
```

Den `Softmax` funktionen konverterar rå poäng till sannolikheter (0-1):

```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;
}
```

> **Tillåtelse**: Softmax poäng är *relaterande*, inte kalibrerade sannolikheter `0.92` som "92% korrekt ". Använd dem för att filtrera låga ufdomsprognoser

---


## steg 6: Extrahera enheter

Nu har vi per- -token förutsägelser.

Först, WordPiece sammanfogade hjälper. De här vingarna `##` underord och punktspacing 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();
}
```

Nu är enhetens extraktion **minimalt märkeförtroende** över hela skalan.

```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;
}
```

Nota: WordPiece hanteras helt av `MergeWordPieces`- inga särskilda kontrollen behövs

---


## Ett komplett exempel

Här är ett minimalt fungerande exempel som du kan kopiera och köra.

```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-PER", "I-PER", "B-ORG", "I-ORG", "B-LOC", "I-LOC", "B-MISC", "I-MISC"];

// 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)}");
```

**utgång:**

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

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

---


## Understanding WordPiece Subwords

BERT använder **WordPiece-tokenisering**, som delar ut okända ord i underord `##` prefix betyder " Fortsättning av det tidigare ordet

```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
```

Den `MergeWordPieces` hjälparbetare hanterar detta: `##` symboler är tillskrivna utan ett utrymme, som producerar `"Elasticsearch"` istället för `"Elastic search"`.

---


## Performance-tips

### 1. Batch flera texter

Om du har många texter, bearbeta dem i lager.

```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]);
```

### 2. Använd Smart Bucketing

Använd den minsta burken som passar

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

I praktiken, ONNX NER är tillräckligt snabb för att fungera **iline under intag**, inte bara som ett slagjobb. Du kan extrahera enheter när dokument kommer in snarare än att ställa upp dem i queue för senare .

### 3. GPU: s accelerering

För hög hastighet använder DirectML (Windows ) eller CUDA

```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
```

---


## När man ska använda detta mot LLM

| Använd ONNX NER
|--------------|------------------------|
| Hög volymen | 1000 | Doser | | 2 | 3 | 1 | 4 | analys | 5
| Standardbeståndsdelar | ( | människor | , | organisationer | МSK3 | ställen |) |
| Privatsphäre
| Deterministiska ledningar | Eksploratorisk analys
| Extraktion av egenskaper | Interpretering | / | Synthes |

Båda tillvägagångssätten fungerar **olika problem**. NER extraherar struktur

---


## Den större bilden

Detta mönster-**deterministisk extraktion med frysta modeller, följt av optionell syntektion senare**- räknar mycket bättre än att trycka in råtext i en LLM och hoppas att den beter sig

NER är inte något du gör.

Samma tillvägagångssätt gäller även för andra dragningar av egenskaper.

> **Där det passar**: Den här OCR: n | | +| NER: s ledning är en byggsten | [Reduzerad RAG](/blog/reduced-rag-concept) och [LucidRAG-dokumentation](https://github.com/scottgal/lucidrag). De varelser du extraherar här blir noder ; dokumenten blir rander ; LLM ser bara vad den behöver

---


## resurser

**Bibliotek & Modeller**:

- **[Tesseract.NET](https://github.com/charlesw/tesseract)** - CM SK1 ompackare för Tesseract OCR
- **[tessdata](https://github.com/tesseract-ocr/tessdata)** - Tränade datafiler för Tesseract
- **[BERT-bas-NER ONNX](https://huggingface.co/protectai/bert-base-NER-onnx)** - NER-modellen vi använder
- **[Runtime på ONNX](https://onnxruntime.ai/docs/)** - officiella dokumentation
- **[ML.Tokenizer](https://www.nuget.org/packages/Microsoft.ML.Tokenizers)** - Microsofts tokeniseringsbibliotek

**Related Articles**:

- **[Det tre---Tier-OCR-röret](/blog/constrained-fuzzy-image-ocr-pipeline)** - När enkel OCR inte är
- **[Reduzerad RAG](/blog/reduced-rag-concept)** - Där extraherade enheter passar in i större sammanhang
- **[LucidRAG](https://github.com/scottgal/lucidrag)** - Full implementation with entity deduplication and graph construction