# Yksinkertainen OCR- ja NER-ominaisuuksien extrahointi C#:ssa ONNXin kanssa

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

Kuten I' on rakentanut [***ilmeinen*RAG**](https://www.lucidrag.com) Luen sosiaalisia tiedotusvälineitä, joissa ihmiset kysyvät jatkuvasti samaa asiaa.. 'How you get features from scanned text [syvään OCR-alueeseen](/blog/constrained-fuzzy-image-ocr-pipeline) Luulin, että olisin kirjoittanut ' alun perin ystävällisen ' lähestymistavan NON-lähestymistapaan-LLM-lähettiläiseen ±( tai LLM-lähentymismahdolliseen ± ) tapaan tehdä tämä.

Teillä on kuvia, joissa on tekstiä.

Tässä artiklassa esitetään **mahdollisimman yksinkertainen** putki **Tesseract** tekstin poistamiseksi, sitten **BERT NER** (ONNX-järjestelmän kautta)yksiköiden tunnistamiseksi . Kaikki paikallisetM SK3 Kaikki määritelmättömätMSC4 Kaikki C:ssä

Deterministinen merkitsee tässä yhteydessä pysyviä versioita, pysyviä kielitietoja , eikä mukautumiskoulutusta runtimessa

> **NuGet tulee pian** - IM SK1m pakkaan tämän yksinkertaiseksi `mostlylucid.ocrner` library. Tällä hetkellä, jäljempänä oleva koodi on kopioM SK2paste ready .

[TOC]

---


## Koko kaasuputki

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

Kaksi vaihetta, kaksi malliaM SK1 molemmat toimivat paikallisesti . Laaditaan molempien osapuolten rakentaminen

---


# Osa 1: OCR Tesseractin kanssa

[Tesseract](https://github.com/tesseract-ocr/tesseract) on standardi Open-ohjelmointiin perustuva OCR-moottori [Tesseract.NET](https://github.com/charlesw/tesseract), a CM SK1 wrapper

```bash
dotnet add package Tesseract
```

Tarvitaan myös koulutettuja tietokantoja. `eng.traineddata` peräisin [Tessdata](https://github.com/tesseract-ocr/tessdata) ja asettaa se `tessdata` kirjallinen.

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

Se' on . -puhe `ExtractText("invoice.png")` ja saat stringin.

> **Merkittävä**: `TesseractEngine` on kallis luoda. Todellisissa sovelluksissa, luoda se kerran ja käyttää sitä uudelleen

### Rajoitukset

Tesseract toimii hyvin **puhdasta, korkeaaM SK1vastatekstiä standardeihin kirjaimiin**. Se kamppailee :

- Stylisoituja tai dekoratiivisia fontejä
- Alhainen-laatuiset skansit tai valokuvat
- Ototettu tai kurottu teksti
- Teksti monimutkaisilla taustalla
- AnimaatioGIF-levyt, joissa on alaotsauksia
- Hyphenated line breaks (`inter-\nnational`) saattaa tarvita NER:n jälkeen tapahtuvaa siirtoa

Tuotantojärjestelmille, joilla on käsiteltävä kummallisia asioita, ks. [Kolmansien -Tier OCR-putken putki](/blog/constrained-fuzzy-image-ocr-pipeline)-, joka lisää Florencea-2 ONNX:n keskiluokkaan ja Vision LLM-eskalaatioon koville tapauksille

Tässä opituksessa otetaan huomioon, että teillä on puhtaita kuvia tai tekstiä toisesta lähteestä.

> Käytännössä, tavallisesti haluatte normalisoida OCR-tuloksen ( trim whitespace, collapse repeated newlinesMSC3 fix obvious hyphenationM SK4 ennen kuin siirrätte sen NER:lle

---


# Osa 2: NER ONNXin kanssa

## Miksi tämä lähestymistapa toimii

Ennen kuin aiomme syventyä koodiin, meidän on ymmärrettävä, mitä me teemme. Jos olette C:n kehittäjä, joka ei ole koskaan käsitellyt ML:tä, tämä jakso on tarkoitettu teille.

### Mikä on NER?

**Named Entity Recognition (NERM SK1** on ratkaistu ongelma.

- **PER** - Henkilöt SSK1John SmithM SK2 "Dr. Jane DoeMST5
- **ORG** - Hallintokunnat
- **LOC** - Locations ("LondonM SK2 "Mount Everest")
- **MISC** - Muut yksiköt

Malli ei ymmärrä tekstiä, miljoonista merkittyistä esimerkeistä opittuja tilastollisia malleja. **NER on ominaispiirteiden ekstrakointi, ei johdonmukaisuusM SK1** Se'muodon vastaavuus steroideihinM SK1

### Miksi ONNX?

**Onnix** (Open Neural Network Exchange **jäädytetty johtopäätös DLL** neutriittiverkkojen osalta: kiinteät painoarvotM SK1 käännyttelemät ulos , ei koulutuslogiikkaa, ei sattumanvaraisuuttaMSC4

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

Keskeinen käsitys: **joku muu teki kovasti töitä** (mallin kouluttaminen Pythonissa ). Te vain teette johtopäätökset C:ssä

### Miksi ei vain käyttää LLM:tä?

Voitte lähettää GPT:lle tekstin ja kysyä: "", etsitkö tässä tekstissä henkilöitä ja yrityksiä?"

| lähestymistapa | | | Nopeus || | Kustannus kutakin asiakirjaa kohti
|-------------------|-------|--------------|-----------------------|-------------|
| **ONNX NER** |
| **paikallinen LLM-API** |
| **LLM-API** |

LLM-järjestelmät ovat suureksi hyödyksi monimutkaisessa päätöksenteossa. Malleja voidaan extrahoida laajoissa mittakaavaissa , erityismalli on M SK2x nopeampi ja vapaampi.

---


## Rautatie

Tässä on se, mitä me rakennamme.

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

Jokainen askel on yksinkertainen.

---


## Askel 1: Laadi malli

Tarvitaan kolme HuggingFace-asiakirjaa. Laaditaan ne käsin luetteloon (eM SK2g., `./models/ner/`):

| Luettelo | | | Laajuus || | URL |
|------|------|-----|
| `model.onnx` |~430MB| [Laaditaan](https://huggingface.co/protectai/bert-base-NER-onnx/resolve/main/model.onnx) |
| `vocab.txt` | [Laaditaan](https://huggingface.co/protectai/bert-base-NER-onnx/resolve/main/vocab.txt) |
| `config.json` | [Laaditaan](https://huggingface.co/protectai/bert-base-NER-onnx/resolve/main/config.json) |

Malli on [Bert-baseM SK1NER](https://huggingface.co/dslim/bert-base-NER) vienti ONNX-formattiin [protectai](https://huggingface.co/protectai/bert-base-NER-onnx).

Luettelo pitäisi olla seuraava::

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

---


## Menettely 2: Hankkeiden perustaminen

Create a new console app and add the NuGet packages:

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

Se's se . Kaksi pakettiaM SK2

- **OnnxRuntime** - Käytetään mallia
- **ML.Tokenizerit** - Käsittelee tekstiä

---


## Askel 3: Tokenoinnin ymmärtäminen

Ennen kuin malli voi käsitellä tekstiä, meidän on muutettava se numeroiksi. **tokenointi**.

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

Keskeiset kohdat:

- `[CLS]` ja `[SEP]` ovat erityisiä tokenteja, jotka merkitsevät lauserajaa
- Jokaisesta sanasta tulee luku `vocab.txt`
- Malli näkee vain numerot, ei koskaan varsinaisen tekstin

### Tokenizerin laatiminen

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

Miksi? `LowerCaseBeforeTokenization = false`? Tämä malli on koulutettu kuormitukselliseen tekstiin.

> **Merkittävä**: Tokenizer *on* vastaamaan mallia tarkasti. käyttämällä erilaista vocab-vaihtoehtoa , kattovaihtokohtaaM SK2 tai erityisiä token ID:itä vähennetään äänettömästi tulosta `vocab.txt` laivastot, jotka käyttävät mallia.

### Tekstin merkitseminen

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

Tämän jälkeen meillä on

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

---


## Menettely 4: Mallin käyttö

Nyt syötämme nämä luvut ONNX-malliin.

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

### Laaditaan malli

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

> **Merkittävä**: Teki tokenizerin ja johdanto-oikeutta koskevan istunnon kerran.`InferenceSession` on kallis luoda.

### Tuloksia valmistellaan

Malli odottaa:

- **input_ids**: Our token IDs as `long[]`
- **huomio_maskki**: 1 reaalitokkien osalta

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

### Käynnissä oleva vihamielisyyttä

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

Tulos `logits` on muotoinen `[1, sequence_length, 9]`-9 mahdolliset merkinnän merkinnät kutakin token asemaa varten

---


## Askel 5: Tuloksen dekodiointi

Malli tuottaa raaka-arvot. Tarvitsemme

1. Voitaisiin löytää korkein merkki, -, joka merkitsee jokaisen token tulosta.
2. Siirtää nämä merkinnät todellisiin yksiköihin

### BIO-merkintöjen ymmärtäminen

Mallissa käytetään **BIO-merkintä**:

```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** = Henkilöyksikön alku
- **I-PER** = Henkilön yksikön sisäpuolella
- **O** = Mikään yksikön ulkopuolelle
- **B-ORG** = Järjestelmän perustaminen

Tämä antaa mallin käsitellä useita -sanayksikköjä, kuten "John SmithM SK2 tai SSK3Yhdistynyt kuningaskunta".

### Merkintökartoitus

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

> **Huomio**: Tässä erityisessä mallissa käytetään standardia 9-label CoNLL-schemaaM SK2 Jotkin ONNXin vientit sisältävät merkintöjen nimet `config.json` (`id2label` field). Jos vaihtatte malleja, lukee merkintöjä config -järjestelmästä mieluummin kuin hardcoding-järjestelmästä

### Parhaan merkin löytäminen

Jokaisesta tokensista valitaan merkki, jolla on suurin logit-tulos:

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

Euroopan unionin `Softmax` Funktio muuntaa raaka-arvot todennäköisyyteen (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;
}
```

> **Luottamusasiakirja**: Softmax-tulokset ovat *suhteellinen*, ei calibraoituja todennäköisyyksiä . NeM SK2 ovat hyödyllisiä luokitteluun ja kynnysarvon vahvistamiseen, `0.92` "92% oikeansuuntainen ". Käyttäkää niitä suodattaessa alhaisiaM SK2luottamusennusteita, ei pohjatodellisuuttaMSC4

---


## Menettely 6: Entiteettien poistaminen

Nyt meillä on per-token-ennusteet . Meidän on yhdistettävä ne toisiinsa

Ensinnäkin, WordPiece-yhdentämistyökalut. Nämä käsi kädet `##` alasanat ja täsmennyksen paikkaaminen oikein:

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

Nyt entiteettilähetys. Entity luottamus on **vähimmäismerkkiluottamus** across the span-conservativeM SK1 joten yksi heikko token ei ole peitelty keskitetyllä :

```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 hallinnoidaan kokonaan `MergeWordPieces`-ei tarvita erityistä valvontaa

---


## Täydentävä esimerkki

Tässä' on vähimmäistyöesimerkki, jonka voi jäljittää ja käyttää

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

**Tuotanto:**

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

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

---


## Käsittää WordPiece-nimen alasanat

BERT käyttää **WordPiece-tokenointi**,, joka jakaa tuntemattomat sanat alasanaksi `##` edeltäjä merkitsee "aiemman sanan jatkuminen

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

Euroopan unionin `MergeWordPieces` avustaja käsittelee tätä: `##` tokenit liitetään ilman kohtaa,, joka tuottaa `"Elasticsearch"` sen sijaan, että `"Elastic search"`.

---


## suorituskyvyn neuvot

### 1. Yhdenkertaiset tekstit

Jos teillä on monta tekstiä, käsitellä ne joukoittainM SK1

```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. Älykäs häkkikäyttö

' ei aina liitetä \512.\ Use the smallest bucket that fits

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

Käytännössä, ONNX NER toimii riittävän nopeasti **sisällytetään syömisen aikana**, ei vain jakelutehtävänä. Voitte extrahoida entiteet, kun asiakirjat saapuvat, sen sijaan että niissä on järjestys myöhemmälle

### 3.GPU:n nopeuttaminen

Korkean nopeuden osalta on käytettävä DirectML:tä ( Windows ) tai 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
```

---


## Milloin tätä käytetään LLM:n kanssa

| Käyttäkää ONNX-NER-järjestelmä | Käytkää LLM-järjestelmä
|--------------|------------------------|
| Korkean määrän (1000s lääkinnällisiä tutkimuksia
| Standard-yksiköt
|PrivaatiaM SK1Sensitiiviset tiedot | | | Kun tarvitsette selityksiä ||
| Deterministiset putkiputket
| Merkintöjen extrahointi | tulkinta / synteesi M|

Molemmat lähestymistavat toimivat. Ne ratkaisevat **erilaiset ongelmat**. NER-lähetys rakenteestaM SK1 LLM:n syyt merkitykselle.

---


## Suurimpi kuva

Tämä malli-**deterministinen ekstrahointi jäädytetyillä malleilla,, jota seuraa myöhemmin valinnainen synteesi**-laskentaa paljon paremmin kuin työntää raaka tekstiä LLM:hen ja toivoo, että se toimii

NER ei ole se, mitä te "agentifioitatteM SK1 Se'infrastruktuuriMSC3 Te hallinnoitte sitä syöttäessäMST4 tallennelette entiteetitMSV5 ja käytätte niitä loppusuoralla filtroimiseksiMSTO6liitännäisyydessäMSTU7 tai syötettäessä kehittyneisiin putkeihinMSZ8

Samaa lähestymistapaa sovelletaan muihin ominaisuuksien ekstrahointitehtäviin: sisällytykset, luokitteluMSC2 tunteetM SK3 junankäynti kerran ( tai käyttämällä ennalta ehkäisyäMST5 vienti ONNXiinMSV6 kaikkialla runnettavaMSP7 määrätietoisestiMSL8

> **Missä tämä sopii**: Tämä OCR + NER-putki on yksi rakenteellista osatekijääM SK2 kokonaiskuvaa varten- miten ekstrahoidut entiteet syötetään grafiikan rakentamiseen [Vähennetty RAG](/blog/reduced-rag-concept) ja [LucidRAG-asiakirjat](https://github.com/scottgal/lucidrag). Siihen liitetyt entiteet muuttuvat nodeiksi ; asiakirjat jäävät reunaksi M SK2 LLM näkee vain sen, mitä se tarvitsee

---


## Resurssit

**Luettelo kirjastoista &**:

- **[Tesseract.NET](https://github.com/charlesw/tesseract)** Tesseract OCR:n wrapper
- **[Tessdata](https://github.com/tesseract-ocr/tessdata)** - Tesseractin koulutetut tietokannat
- **[BERT-base-NER ONNX](https://huggingface.co/protectai/bert-base-NER-onnx)** - Käyttämämme NER-malli
- **[ONNX:n käyttöaika](https://onnxruntime.ai/docs/)** - Virallinen asiakirja
- **[ML.Tokenizerit](https://www.nuget.org/packages/Microsoft.ML.Tokenizers)** - Microsoft's tokenizer-kirjasto

**Related Articles**:

- **[Kolmansien -Tier OCR-putken putki](/blog/constrained-fuzzy-image-ocr-pipeline)** - Kun yksinkertainen OCR ei ole
- **[Vähennetty RAG](/blog/reduced-rag-concept)** - Missä ekstrahoidut entiteet sopivat kokonaiskuvaan
- **[LucidRAG](https://github.com/scottgal/lucidrag)** - Täsmällinen täytäntöönpano entiteetin päällekkäisyydellä ja grafiikan rakentamisella