# Прості OCR і NER Funkції Extraction в C# з ONNX

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

Так, як я будую [***прозора*РАГ**](https://www.lucidrag.com) Я читаю в соціальних медіа, де люди питають одне й те саме. МСК1 МСК2 Як ви отримуєте характеристики від сканованого тексту? МСК3 помилка категорії завжди. ' просто користуйтесь LLM '... яка працює, але дуже дорого. [глибоко в просторі OCR](/blog/constrained-fuzzy-image-ocr-pipeline) Я подумав, що я напишу 'напрочуд дружній для początkujących ' підхід до NON

У вас є зображення з текстом. Ви хочете виокремити цей текстM SK1 тоді знаходити корисний структуру всередині нього ( назви , компаніїMska4 місцяMска5 без виклику LLMM Ska6 відправлення даних до хмариMSKA7 або платити за один токенМska8

Ця стаття показує **найпростіше** трубка: **Тесеракт** для видобутку тексту, тоді **БЕРТ NER** (via ONNXM SK1 для розпізнавання об 'єктів . Все локальне МSK3 Все детерміністичне M SK4 Все в C #.

Детерміністично тут означає фіксовані версії, фіксоване значення мовиM SK1 і відсутність адаптивного навчання в часі запуску

> **NuGet скоро прийде** - IM SK1m упакуємо це в простий `mostlylucid.ocrner` бібліотека. На даний момент

[TOC]

---


## Цілковита труба

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

Два кроки, два моделі , обидва локально працюютьM SK2 Дозвольте' збудувати кожну частинуMSC4

---


# Частина 1: ОCR з Tesseract

[Тесеракт](https://github.com/tesseract-ocr/tesseract) це стандартний відкритий -ソース OCR двигун . Ми МSK2 використаємо [Теsseract.NET](https://github.com/charlesw/tesseract), упаковка CM SK1

```bash
dotnet add package Tesseract
```

Вам також потрібні треновані файли даних. завантажити `eng.traineddata` від [тести](https://github.com/tesseract-ocr/tessdata) і помістити його в `tessdata` папка.

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

Це - МСК0, це - МSK1 `ExtractText("invoice.png")` і ви отримуєте струну.

> **Важливо**: `TesseractEngine` це дорого створити. В реальному програмуванніМSK1 створити його один раз і відновити

### Ограничення

Теsseract працює добре для **чистий, високийM SK1 контрастний текст в стандартних шрифтах**. Він бореться з

- Стилізовані чи декоративні шрифти
- Низка якість сканів або фотографій
- Розвернутий або вигнутий текст
- Текст на складних фонах
- Анімовані GIF з субтитрами
- Похибні лінії (`inter-\nnational`) може потребувати обробки післяM SK1 перед NER

Для систем виробництва, які мають справу з дивними речами, см. [Трійна труба МСК0Tier](/blog/constrained-fuzzy-image-ocr-pipeline)-, що додає Флоренції -2 ONNX як середній рівень і ескалацію Vision LLM для жорстких корпусів

Для цього уроку ми припускаємо, що ви маєте чисті зображення або текст з іншого джерела.

> У практиці, ви зазвичай хочете нормалізувати OCR вихідні коди ( trim whitespace, collapse repeated newlinesM SK3 fix obvious hyphenationMska4 before passing it to NERM Ska5

---


# Частина 2: NER з ONNX

## Чому цей підхід працює?

Перед тим, як зануритися у код, треба зрозуміти, що ми насправді робимо. Якщо ви - програміст, який ніколи не торкався ML,

### Що таке NER?

**Визначання наименованих об 'єктів (NERM SK1** є розв 'язаною проблемою. Дослідники навчили нейронні мережі, які можуть читати текст і виділяти цікаві біти

- **PER** - Назви людей МSK1Джон Сміт МSK2 " Доктор мSK4 Джейн Доe "
- **ORG** - Організації МSK1Microsoft", "NHSM SK4 | | 5 | Acme Corp | МSK6 |
- **LOC** - Местоположення
- **MISC** - Інші об 'єкти МSK1COVIDM SK2 МSK3iPhone 15"

Модель не розуміє тексту **NER - це виділення властивостей, не обґрунтуванняM SK1** Імовірність співпадіння шаблону на стероидах

### Чому ONNX?

**NX** ( Open Neural Network Exchange ) це стандартний формат для моделей ML МSK2 Подумайте про це як про **заморожена деференція DLL** для нейронних мереж: фіксовані вага вM SK1 витяжники назовні, без тренувальної логіки МSK3 без випадковості :

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

Найголовніша думка: **хтось інший зробив важку роботу** (викладання моделі в PythonіM SK1 Ви просто виконуєте висновки у C#.

### Чому б не просто використовувати LLM?

Ви можете надіслати текст на GPT-4 і запитати: M SK1знайомте людей та компанії в цьому текстіMSC2 Це працює! АлеMST4

| Погляньмо на це. МSK1 Швидкість.
|-------------------|-------|--------------|-----------------------|-------------|
| **NER** МSK0 ~50ms МSK2 ♫ ♫ $0 ♫
| **Місцевий LLM API** МSK0 4-30s МSK2 | | $0 \ | МSK4 Малі моделі можуть бути склоподібними
| **LLM API** МSK0 1-5s МSK2 | | $20-50 \ | МSK4 | Дані надсилаються назовні | Варіабель M|

ЛЛМ чудові для складного обґрунтування. Для видобутку шаблонів в масштабіM SK1 спеціальною моделью є 40x швидше і вільніше.

---


## Трубопровод

Ось що ми будуємо

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

Кожен крок простий.

---


## Шаг 1: завантажити модель

Вам потрібні три файли з HuggingFace. завантажити їх ręczно до папки M SK1e .g., `./models/ner/`):

| Файлі МSK1 Размер МSK2 यूआरएल |
|------|------|-----|
| `model.onnx` МSK0 ~430MB  МSK2 [Загрузити](https://huggingface.co/protectai/bert-base-NER-onnx/resolve/main/model.onnx) |
| `vocab.txt` МSK0 ~230KB  МSK2 [Загрузити](https://huggingface.co/protectai/bert-base-NER-onnx/resolve/main/vocab.txt) |
| `config.json` МSK0 ~1KB  МSK2 [Загрузити](https://huggingface.co/protectai/bert-base-NER-onnx/resolve/main/config.json) |

Модель [bert-baseM SK1NER](https://huggingface.co/dslim/bert-base-NER) exported to ONNX format by [захист](https://huggingface.co/protectai/bert-base-NER-onnx).

Ваша папка має виглядати як:

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

---


## Шаг 2: Установка проекту

Створити нову консольну програму і додати пакети NuGet:

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

Це – МСК0, це – МSK1, два пакети, МSK2

- **Час trwania Onnx** - запускає модель
- **ML.Tokenizers** - Рухає текстом МSK1 конвертація टोकена

---


## Шаг 3: Усвідомлення символізації

Перед тим, як модель зможе обробляти текст, ми повинні конвертувати його в цифриM SK1 Це називається **символізація**.

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

Ключевые моменти:

- `[CLS]` і `[SEP]` є особливими символами, які позначають межі речення
- Кожне слово стає числом від `vocab.txt`
- Модель бачить тільки числа, ніколи не справжній текст

### Завантаження токенізера

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

Чому? `LowerCaseBeforeTokenization = false`? Ця модель була навчена на корпусному тексті . МSK2Джон МSK3 і СМСК4Джун ММСК5 мають різні значення СМСК6 один МПСК7 скоріш за все ім 'я

> **Важливо**: Токенізер *повинна* точно співпадуть з моделлю. Використовуючи інший vocabM SK1 опцію обробки , або індивідуальні значки символів тиші погіршують результати `vocab.txt` що судна з моделью.

### Це символізує текст

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

Після цього ми маємо

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

---


## Шаг 4: Запуск моделі

Тепер ми додаємо ці цифри до моделі ONNX. Модель повертає "логітиM SK2raw результати для кожного можливого ярлика на кожному місціMSC3

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

### Завантаження моделі

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

> **Важливо**: Створити токенізер і сеанс висновків один раз.`InferenceSession` дуже дорого створити.

### Подготування вхідних даних

Модель очікує:

- **вхід_іди**: Наші торгові марки `long[]`
- **увагу_маска**МSK0 1 для реальних токенівM SK2 \0 для підкладки

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

### Рухна інференція

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

Виход `logits` має форму `[1, sequence_length, 9]`-9 можливі ярлики для кожного місця символуM SK1

---


## Шаг 5: Декодування вихідного коду

Модель виводить сирові очки. Нам потрібноM SK1

1. Найти найвищий - ярлик оцінки для кожного символу
2. Перетворити ці ярлики на справжні речі

### Усвідомлення BIO Tags

Модель використовує **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** = Початок Person entity
- **I-PER** МSK0 Всередині (продовжуванняM SK2 особи
- **О** = За межами будь-якого об 'єкта
- **B-ORG** = Початок організації

Це дозволяє моделлю працювати з багатьма-іменними об 'єктами, такими як M SK1Джон Сміт " або МSK3Великобританія МSK4

### Мапування ярликів

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

> **Примітка**: Це специфічна модель, яка використовує стандартну 9-схему CoNLL зображень `config.json` (`id2label` поле). Якщо ви поміняєте моделіM SK1 читайте ярлики з конфігурації, а не за допомогою кодування

### Найти найкращий ярлик

Для кожного символу ми вибираємо ярлик з найвищим logit балом:

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

І `Softmax` функція перетворює чисті партитури на ймовірність (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;
}
```

> **Довідка про довіру**: Оцінки Softmax *родичі*, не калібровані вірогідності . Вони МSK2 є корисними для рейтингу та порогового значення M SK3 але їх не лікують `0.92` як "92% правильнийM SK1 Використовуйте їх для того, щоб відфільтрувати низькі - прогнози впевненості МSK3 не як базову правду

---


## Шаг 6: Витягування об 'єктів

Тепер ми маємо прогнози на% 1,% 2,% 3,% 4,% 5. Нам потрібно об 'єднати їх в один ціль.

Перші, допоміжні дозволяти поєднати WordPiece. Ці ручки `##` слова-заголовки та відмітки правильно:

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

Тепер виділення об 'єкту. Довіра об' єкту **мінімальний рівень довіру** через весь діапазон, так що один слабкий символ не приховується середнім показником

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

Примітка: WordPiece повністю обробляється `MergeWordPieces`-не потрібно спеціального управління потоком

---


## Повний приклад

Тут' є мінімальним прикладом, який ви можете скопіювати і запустити

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

**Виход:**

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

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

---


## Усвідомлення підслов WordPiece

БЕРТ використовує **Токенізація WordPiece**,, що розділяє невідомі слова на підслова `##` попереднє слово означає "продовжування попереднього словаM SK1

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

І `MergeWordPieces` допомічник керує цим: `##` टोकони прикріплені без простору,, що виробляє `"Elasticsearch"` замість `"Elastic search"`.

---


## Наполегшення розвитку

### 1. Множество текстів в пакеті

Якщо ви маєте багато текстів, обробляйте їх в партіяхM 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. Використовуйте розумний букетинг

' не завжди прикріплятись до МSK1 Використовуйте найменший контейнер, який вписується в :

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

На практиці, ONNX NER досить швидко працює **встромлений під час поглинання**, не просто як пакетна роботаM SK1 Ви можете витягати сутності, коли приходять документи, а не переміщувати їх в чергах на пізніше.

### 3. ГПЗ Швидшення OptionalM SK2

Для високої пропускної спроможності, використовуйте DirectML (WindowsM SK2 або 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
```

---


## Коли використовувати це проти LLM

| Використовуйте ONNX NER
|--------------|------------------------|
| високий об 'єм МSK1 шл. доків МSK2 | один мSK4 аналіз ззовні M|
| Стандартні об 'єкти МSK1 люди МSK2 організації , місця
| конфіденциальністьM SK1чутливі дані | Коли вам потрібні пояснения МSK3
| Детерміністичні труби МSK1 Дослідковий аналіз МSK2
| Видобуток властивостей МSK1 інтерпретація МSK2 синтез |

Обидві методи працюють. Вони розв 'язують **різні проблеми**. NER виділяє структуру ; LLM - причину значущості МSK2

---


## Велика картина

Ця модель-**детерміністичне видобуток з замороженими моделями,, потім опціональна синтеза**- набагато краще, ніж втиснути чистий текст у LLM і сподіватися, що він буде вестись так

NER - це не те, що ви робите.

Те ж саме стосується і інших завдань по видобутку властивостей.

> **Де це підходить?**: Це OCR + NER труба є одним з будівельних блоків МSK2 Для повної картини МSK3 як виокремлені об 'єкти входять у графову конструкцію [Reduced RAG](/blog/reduced-rag-concept) і [Документація LucidRAG](https://github.com/scottgal/lucidrag). Втіли, які ви тут витягуєте, стають вузлими ; документи стають крапочками МSK2 LLM бачить тільки те, що йому потрібно

---


## Ресурс

**Кітапники & Моделі**:

- **[Теsseract.NET](https://github.com/charlesw/tesseract)** - CM SK1 упаковка для Tesseract OCR
- **[тести](https://github.com/tesseract-ocr/tessdata)** - Треновані файли даних для Tesseract
- **[BERT-base-NER ONNX](https://huggingface.co/protectai/bert-base-NER-onnx)** - Модель NER, яку ми використовуємо
- **[Пропускний час на NX](https://onnxruntime.ai/docs/)** - офіційна документація
- **[ML.Tokenizers](https://www.nuget.org/packages/Microsoft.ML.Tokenizers)** - Бібліотека токенізерів Microsoft'

**Подібні статті**:

- **[Трійна труба МСК0Tier](/blog/constrained-fuzzy-image-ocr-pipeline)** - Коли простий ОCR недостатньо
- **[Reduced RAG](/blog/reduced-rag-concept)** - Де виділені об 'єкти вписуються у загальну картину
- **[LucidRAG](https://github.com/scottgal/lucidrag)** - Полне введення з дедукцією об 'єктів та конструюванням графів