# Extrazione semplice di OCR e NER in C# con ONNX

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

Come l'IMSC ha costruito. [***lucido.*RAG**](https://www.lucidrag.com) Sto leggendo i social media dove le persone continuano a chiedere la stessa cosa. 'Come si ottengono caratteristiche dal testo scansionatoM SK3 l'errore nella categoria è sempre ' usate solo un LLMMST5 che funziona ma è molto costosoMst6 Quindi come io [in profondità nello spazio OCR.](/blog/constrained-fuzzy-image-ocr-pipeline) Ho pensato che avrei scritto un 'applicabile per i principianti 'approccio al non-non--LLM (o LLM opzionale) modo di fare questo

Avete immagini con un testo. volete estrarre quel testo , poi trovate la struttura utile all'interno (i nomiM SK3 aziendeMSC4 postiMST5 senza chiamare un LLMMSSK6 spedire i dati alla nuvolaMSM7 o pagare per un tokenMSV8

Questo articolo mostra il **La più semplice possibile.** Pipeline: **Tesseract** per l'estrazione del testo, allora **BERT NER** (via ONNXM SK1 per il riconoscimento delle entità. Tutti localiMST3 Tutti deterministiciMSS4 Tutti in CMS5

Determinista qui significa versioni fixe, dati linguistici fissati, e nessuna apprendimento adattativo al runtimeM SK2

> **NuGet sta per arrivare.** - IM SK1m imballare questo in un semplice `mostlylucid.ocrner` biblioteca. Per ora, il codice sotto è copiareM SK2fare la pastaMSC3

[TOC]

---


## La Pipeline completa

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

Due fasi , due modelli, entrambi funzionano localmente. LasciateM SK3 costruire ogni parteMSC4

---


# Parte 1: OCR con Tesseract

[Tesseract](https://github.com/tesseract-ocr/tesseract) è il motore OCR open standard. [Tesseract.NET](https://github.com/charlesw/tesseract), a CM SK1 wrapper.

```bash
dotnet add package Tesseract
```

Servono anche i file di dati addestrati. scaricare `eng.traineddata` a partire da [tessdata.](https://github.com/tesseract-ocr/tessdata) E lo mettiamo in una `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();
}
```

Quella è la chiamata. `ExtractText("invoice.png")` E ottenete una stringa.

> **Importante**: `TesseractEngine` È costoso creare. Nelle applicazioni reali, lo crea una volta e lo riutilizzaM SK2

### Limiti

Tesseract funziona bene per **pulito, alto- testo contrastante nelle font standard**. Si batte con :

- Font stilizzati o decorativi
- Low- scansioni o foto di qualità
- testo ruotato o curvato
- Il testo su sfondo complesso
- GIF animati con sottotitoli
- Interruzioni di linee signate (`inter-\nnational`) potrebbe avere bisogno di un processo post- prima del NER.

Per i sistemi di produzione che devono gestire le cose strane, vedete [Il Pipeline Three-Tier OCR](/blog/constrained-fuzzy-image-ocr-pipeline)- che aggiunge Florence-2 ONNX come escalazione intermedia e Vision LLM per i casi rigidi .

Per questo tutorial, noi' supponiamo che abbiate immagini pulite o un testo da un'altra fonte M SK2Parsing PDF , copyMSC4pasteMST5 eccMSSK6

> In pratica, di solito vorrete normalizzare il risultato OCR M SK1 trim whitespace, collapse repetite newlinesMSC3 correggere l'ifenazione ovvia ) prima di trasferirlo a NERMNK5

---


# Parte 2: NER con ONNX

## Perché questo approccio funziona?

Prima di approfondire il codice, lasciate che ' capisca cosa stiamo facendo.

### Cos'è NER?

**Riconoscere le entità chiamate (NER)** è un problema risolto. I ricercatori hanno addestrato reti neurali che possono leggere il testo e evidenziare i "bit interessantiM SK2

- **PER** - Nome delle persone "John Smith", MSC3DrM SK4 Jane DoeMSC5
- **ORG** - Organizzazioni "Microsoft", "NHSM SK4 ≥"Acme CorpMSC6
- **LOC** - Locations ("LondonM SK2 SSK3Mount Everest")
- **MISC** - Altre entità

Il modello non capisce il testo. **NER è l'estrazione delle caratteristiche, non il ragionamento.** E' il modello che corrisponde ai steroidi.

### Perché ONNX?

**NX** (Open Neural Network Exchange) è un formato standard per i modelli MLM SK2 Pensate a questo come ad una **Frozen inference DLL** per le reti neurali: peso fisso in , tensioni fuori , nessuna logica per l'addestramento, nessuna casualità:

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

Il punto di vista chiave: **qualcun altro ha fatto il duro lavoro.** (struire il modello in Python). Si fa solo l'inferenza in CM SK2

### Perché non usare solo un LLM?

Potreste mandare un messaggio al GPT-4 e chiedere " trovare le persone e le aziende in questo messaggioM SK2 Funziona ! MaMSC4

| Approco | Velocità | Costo per S1000 dossiers M| Privacy R| Consistenza D|
|-------------------|-------|--------------|-----------------------|-------------|
| **NER ONNX** |  ~50ms
| **LLM API locale** | 4-30s | \$0 | | | I modelli piccoli possono essere impermeabili || | Variabile ||
| **LLM API** |

I LLM sono fantastici per un ragionamento complesso. Per l'estrazione di schemi su larga scalaM SK1 un modello dedicato è 40x più veloce e libero .

---


## Il Pipeline

Questo è quello che stiamo costruendo.

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

Ogni passo è semplice.

---


## Step 1: scaricare il modello

Avete bisogno di tre file da HuggingFace. Fatela scaricare manualmente in un folder M SK1e .g., `./models/ner/`):

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

Il modello è [Bert-base-NER](https://huggingface.co/dslim/bert-base-NER) L'export è stato fatto nel formato ONNX. [Proteggerai.](https://huggingface.co/protectai/bert-base-NER-onnx).

La vostra cartella dovrebbe sembrare:

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

---


## Preparazione del progetto 2:

Creare una nuova app per la console e aggiungere i pacchetti NuGet:

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

Che's it. Due pacchettiM SK2

- **OnnxRuntime** - Funziona il modello
- **ML.Tokenizer** - Gestisce il testo → conversione di tocco

---


## Step 3: Comprendere la tokenizzazione

Prima che il modello possa elaborare il testo, dobbiamo convertirlo in numeri. **Tokenizzazione**.

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

punti chiave:

- `[CLS]` e `[SEP]` Sono dei simboli speciali che marcano i confini delle frasi.
- Ogni parola diventa un numero da. `vocab.txt`
- Il modello vede solo i numeri, mai il testo reale.

### Lavorare il Tokenizer

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

Perché? `LowerCaseBeforeTokenization = false`? Questo modello è stato addestrato su un testo con casetta. "JohnM SK3 e MSC4johnMSC5 hanno significati diversiMST6unoMSS7 è probabile che sia un nomeMTS8 unoMSSS9 probabilmente non sia

> **Importante**: Il tokenizzatore *deve* Corresponde esattamente al modello. Utilizzando un altro vocab, opzione di cabinaM SK2 o degli identificatori di tocco speciali diminuisce silentmente i risultati . Usate sempre `vocab.txt` che nave con il modello.

### Tokenizzando il testo

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

Dopo questo , abbiamo :.

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

---


## Step 4: Condurre il modello

Ora inseriamo quei numeri nel modello ONNX. Il modello restituisce "logitiM SK2 punteggi reali per ogni possibile etichetta in ogni posizione .

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

### Lavorare il modello

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

> **Importante**: Creare una volta il tokenizzatore e la sessione di deduzione (singletonM SK2service) e riutilizzarliMST4 DonMSSK5 non riscarciare per ogni documentoMSC6`InferenceSession` È costoso creare.

### Preparare le input

Il modello si aspetta:

- **input_ids**: Le nostre ID di tocco come `long[]`
- **attenzione_mask**: 1 per i token realiM SK2 \0 per la compensazione.

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

### Inferenza in corso

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

Il risultato. `logits` Ha una forma. `[1, sequence_length, 9]`-9 possibili etichette per ogni posizione di toccoM SK1

---


## Step 5: Decodificare il risultato

Il modello produce punteggi grezzi. Dobbiamo:

1. Trovare l'etichetta di punteggio più alta- per ogni tocco.
2. Converti queste etichette in entità reali.

### Capire le targhette BIO

Il modello usa **Notazione BIO**:

```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** = Inizia dell'entità di una persona
- **I-PER** = all'interno (continuazione) di un entità di una persona
- **O.** = fuori da qualsiasi entità (non interessanteM SK2
- **B-ORG** = Iniziare un'organizzazione

Questo permette al modello di gestire molte entità di parole come "John SmithM SK2 o "United Kingdom".

### La mappatura di etichette

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

> **Nota.**: Questo modello specifico usa il schema CoNLL standard 9-label. Alcuni esportazioni ONNX includono i nomi delle etichette in `config.json` (`id2label` campo). Se scambiate modelli, leggete le etichette dalla configurazione piuttosto che dal hardcoding

### Trovare l'etichetta migliore

Per ogni tocco, scegliamo l'etichetta con il punteggio logit più altoM 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));
}
```

L'informazione figura nella parte dispositiva. `Softmax` Funzione converte i punteggi grezzi alle probabilità (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;
}
```

> **Nota sulla fiducia**: I punteggi della Softmax sono *relative.*, probabilità non calibrateM SK1 Sono utili per la classificazione e il tasso di soglia , ma non sono usati `0.92` come "92% corretto". Usarli per filtrare le previsioni di fiducia bassaM SK2 non come verità di fondo .

---


## Step 6: Extrazione delle entità

Ora abbiamo proiezioni per -, "token predictions" e .. Dobbiamo fusionarle in entità.

Primo, gli aiuti per la fusione di WordPiece. Queste manipolazioni `##` parole sottostantive e spazi per la punteggiatura corretti:

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

Ora l'estrazione dell'entità. La fiducia nell'entity è la **La fiducia minima nel tocco** In questo modo, un singolo tocco debole non è nascosto dall'evidenza media.

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

Note: WordPiece è gestito interamente da `MergeWordPieces`-non c'è bisogno di un flusso di controllo specialeM SK1

---


## Esempio completo

Qui' è un esempio di lavoro minimo che potete copiare e far funzionare:

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

**Output:**

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

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

---


## Capire le frasi sottostanti di WordPiece

BERT usa **Tokenizzazione di WordPiece**, che divide parole sconosciute in sotto parole. `##` Prefixo significa "continuazione della parola precedente":

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

L'informazione figura nella parte dispositiva. `MergeWordPieces` l'aiuto gestisce questo: `##` i token sono attaccati senza uno spazio, che produca `"Elasticsearch"` invece di `"Elastic search"`.

---


## I consigli per la performance

### 1. Batch Multiple Texts

Se avete molti messaggi, processarli in lotti:

```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. Usare un boccone intelligente

Don' non immergersi sempre a 512. Usare il contenitore più piccolo che si adatti.

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

In pratica, ONNX NER è abbastanza veloce da funzionare **inline durante l'ingestione.**, non solo come un lavoro di lotto. Si possono estrarre le entità quando arrivano i documenti piuttosto che metterli in fila per un ulteriore periodo.

### 3. Accelerazione GPU (Opzionale)

Per un alto throughput, usa DirectML (Windows) o CUDAM SK3

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

---


## Quando usare questo contro LLM

| Utilizza ONNX NER | Utilizzi LLM (GPTM SK3Claude)
|--------------|------------------------|
| High volume (1000s of docs) | OneM SK4off analysis SSK5
| Entità standard (persone, organiM SK3 luoghiMSC4 | Tipi di entità personalizzati SSK6
|PrivatezzaM SK1dati sensibili |Quando avete bisogno di spiegazioni |
| Pipeline deterministici | Analisi esplorative PSK2
| Extrazione delle caratteristiche | Interprezione PES2 sintesi PES3

Entrambi approcci funzionano. Risolvono **diversi problemi.**. NER extrae la strutturaM SK1 LLMs ragione sul significato.

---


## Il quadro più grande

Questo schema-**Extrazione deterministica con modelli congelati, seguito da una sintesi opzionale dopo.**-scalle molto meglio di spingere il testo grezzo in un LLM e sperare che si comporte.

Il NER non è qualcosa che voi "agentificiate". Ma l'infrastruttura di 'You run it on ingestionM SK4 store the entitiesM Sk5 and use them downstream for filteringM sk6 linking M Sk7 or feeding into more sophisticated pipelinesMtk8

Lo stesso approccio si applica ad altre attività di extrazione delle caratteristiche: embeddings, classificazioneM SK2 sentimenti . Treno una volta MSC4 o usare pre-preservatoMSL5 esportazione a ONNXMST6 andare ovunqueMst7 deterministamenteM st8

> **Dove questo va bene?**: Questo OCR + Il tubo del NER è un blocco di costruzione. Per il quadro completoM SK3 come le entità extraite entrano nella costruzione dei graficiMSC4 la deduplicazioneMNK5 e la ripresaMZK6 vediamo [RAG ridotto](/blog/reduced-rag-concept) e la [Documentazione LucidRAG](https://github.com/scottgal/lucidrag). Le entità che si estraggono qui diventano nodi; i documenti diventano bordi ; il LLM vede solo quello di cui ha bisognoM SK3

---


## Ressource

**Le biblioteche & Modelli**:

- **[Tesseract.NET](https://github.com/charlesw/tesseract)** - CM SK1 wrapper per Tesseract OCR
- **[tessdata.](https://github.com/tesseract-ocr/tessdata)** - Date addestrate per Tesseract
- **[BERT-base-NER ONNX](https://huggingface.co/protectai/bert-base-NER-onnx)** - Il modello NER che usiamo
- **[Il tempo di esecuzione di ONNX](https://onnxruntime.ai/docs/)** - Documentazione ufficiale
- **[ML.Tokenizer](https://www.nuget.org/packages/Microsoft.ML.Tokenizers)** - La biblioteca del tokenizzatore Microsoft'

**Article connessi**:

- **[Il Pipeline Three-Tier OCR](/blog/constrained-fuzzy-image-ocr-pipeline)** - Quando un semplice OCR non è sufficiente
- **[RAG ridotto](/blog/reduced-rag-concept)** - Dove le entità estrattoe si inseriscono nel quadro generale
- **[LucidRAG](https://github.com/scottgal/lucidrag)** - Full implementation with entity deduplication and graph construction