# استخراج خصائص الـ OCR و NER بسيطة في C# مع ONNX

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

كما كان I' يبني [***وضوح*RAG**](https://www.lucidrag.com) أنا أقرأ وسائل الإعلام الإجتماعية حيث يسأل الناس نفس الشيء. [عميقاً في مساحة الـ OCR](/blog/constrained-fuzzy-image-ocr-pipeline) اعتقدت انني سوف اكتب طريقة ' ملائمة للمبتدئين ' للطريقة غير الملائمة لتلك الطريقة

لديك صور مع نص . تريد استخراج ذلك النص , ثم تجد البنية المفيدة في داخله | ( | الأسماء |, | الشركات | МSK4 | الأماكن | ) | دون أن تتصل بـ LLM

هذه المقالة تظهر **أبسط ما يمكن** خط أنابيب: **تيسيراكت** من أجل استخراج النص, عندها **بيرت نيور** (via ONNX) للتعرف على الكياناتM SK2 جميعها محلية . جميعهم محددين . جميعهما في CMSC5

تعبر هنا عن نسخ ثابتة, بيانات لغة ثابتة

> **غويت قادم قريباً** - I'm أقوم بتغليف هذا إلى شيء بسيط `mostlylucid.ocrner` مكتبة. لحد الآن, الشفرة التالية هي النسخ -paste readyM SK3

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

خطوات اثنان , نماذج اثنتين , كلاهما يعمل محلياً | . | دع |' | يبني كل جزء |

---


# أجزاء 1: أو سي آر مع تيسيراكت

[تيسيراكت](https://github.com/tesseract-ocr/tesseract) هو نموذج مفتوح - source OCR engine . سنستخدم [Tesseract.NET](https://github.com/charlesw/tesseract), مُغلّم C# مَغلِم .

```bash
dotnet add package Tesseract
```

تحتاج أيضاً إلى حزم البيانات المدرّسة. تحميل `eng.traineddata` من [بيانات تيس](https://github.com/tesseract-ocr/tessdata) ووضعها في `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();
}
```

تلك هي .. المكالمة `ExtractText("invoice.png")` وتحصل على سلسلة

> **مهم**: `TesseractEngine` يكلف صنعه. في التطبيقات الحقيقية, صنعها مرة واحدة وإعادة استخدامها

### الحدود

تسيراكت تعمل بشكل جيد **نظيف, عالي-contrast text in standard fonts**. إنها تعاني مع:

- خطوط مصممة أو مرسومة
- منخفضة -دقة المسح أو الصور
- النص المدور أو المنحني
- النص على خلفيات معقدة
- الرسوم المتحركة GIFs مع الترجمات
- كسرات خطية مكتوبة (`inter-\nnational`) قد يحتاج إلى معالجة بعد - قبل NER

للأنظمة الإنتاجية التي تحتاج إلى التعامل مع الأشياء الغريبة , أنظر [أنابيب تير ثلاثية -](/blog/constrained-fuzzy-image-ocr-pipeline)- الذي يضيف فلورانس-2 ONNX كطبقة متوسطة وتسليط لرؤية LLM للمواد الصلبة .

لهذا الدروس , نحن' سنفترض أن لديكم صور أو نص نظيفة من مصدر آخر ( تحليل PDF

> في الواقع , سترغب عادة في توصيل خروجية الـOCR ( إحداث فضاء أبيض | , إنحدار خطوط جديدة مكررة |, إصلاح المصطلحات الواضحة ٬) قبل أن تنقلها إلى NER

---


# جزء 2: NER مع ONNX

## لماذا هذا النهج ينجح

قبل أن نغوص في البرمجة، دعونا نفهم ما نفعله بالفعل.

### ما هو NER?

**التعرف على الكائنات المسمى (NER)** هو مشكلة حلة. لقد قام الباحثون بتدريب شبكات عصبية يمكنها قراءة النص وتسليط الضوء على "الجزئيات المثيرة

- **PER** MSC0 أسماء الأشخاص MSC1 جون سميث MSc2 msc3 د.مsc4 جين دو MScs5
- **ORG** - منظمات (" مايكروسوفت", "NHSM SK4 |" |Acme Corp | ")
- **LOC** - المواقع ("لوندن", "شارع إيفريستM SK4
- **MISC** - كائنات أخرى ("COVID-19", "iPhone 15")

النموذج لا يفهم 't " understood" the textM SK3itMSC4s learned statistical patterns from millions of labelled examples **NER هو استخراج الخصائص, ليس استدلالًا.** إنها تطابق النمط على الاستيدروجين

### لماذا ONNX?

**أونكس** (تبادل شبكات عصبية مفتوحة) هو النموذج القياسي لنماذج الميل **. frozen inference DLL** لشبكات عصبية: ثقوب ثابتة في, مقبضات خارجيّةM SK2 لا منطق تدريبيّ

```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). تقوم فقط بتشغيل الاستنتاج في CM SK2

### لماذا لا نستخدم فقط LLM?

يمكنك إرسال رسالة نصية إلى GPT-4 وتسأل "إيجاد الأشخاص والشركات في هذا النصM SK2 إنه ينجح

| المقاربة | | | السرعة || | التكلفة لكل | 1000 | دوتات || | خصوصيية |مSK5 | التماسك | مSK6
|-------------------|-------|--------------|-----------------------|-------------|
| **أونكس نيور** | ~50ms | | | $0 |
| **واجهة LLM محلية** | 4-30s | | | $0 \| النماذج الصغيرة يمكنها أن تكون رقيقة || | متغير |
| **LLM API** |

LLMs great for complex reasoning. For pattern extraction at scaleM SK1 a dedicated model is 40x faster and freeMSC3

---


## الأنبوب

هذا ما نصنعه هنا

```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. تحمّلهم لاسيًا إلى папة M SK1e .g., `./models/ner/`):

| الملف | الحجم | | | URL ||
|------|------|-----|
| `model.onnx` | ~430MB  | [تنزيل](https://huggingface.co/protectai/bert-base-NER-onnx/resolve/main/model.onnx) |
| `vocab.txt` مسك0 ~230KB | [تنزيل](https://huggingface.co/protectai/bert-base-NER-onnx/resolve/main/vocab.txt) |
| `config.json` مسك0 ~1KB | [تنزيل](https://huggingface.co/protectai/bert-base-NER-onnx/resolve/main/config.json) |

النموذج هو [bert-base-NER](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
```

ذلك ' s it . بطاقتين :

- **OnnxRuntime** - يشغل النموذج
- **ML.توكينر** - يدوي النص → تحويل الرموز

---


## الخطوة 3: فهم التوجيه

قبل أن يستطيع النموذج معالجة النص , نحتاج إلى تحويله إلى أرقام . هذا يدعى **التدوين**.

```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`? تم تدريب هذا النماذج على نص مكعب. "JohnM SK3 و MSC4johnMSC5 لها معناه مختلفةMST6oneMS'محتمل أن يكون اسمًا

> **مهم**: الرمز *يجب أن* تتطابق بالنموذج بالضبط. بإستخدام حرف مختلف, إختيار القضبان , أو هوية رموز خاصة سوف تحطم النتائج بصمت `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

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

> **مهم**: إنشاء session of tokenizer and inference once (singleton`InferenceSession` من التكلفة أن تصنع

### إعداد المدخلات

النموذج يتوقع:

- **الدخل_ids**: إختبارات نصيبنا كـ `long[]`
- **الملاحظة_الغطاء**: 1 للرموز الحقيقية, |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 possibly labels for each token position.

---


## الخطوة 5: فك شفرة المخرج

النموذج ينتج نقاط خامة. نحن بحاجة إلى

1. أبحث عن أعلى علامة تقييم - لكل رموز
2. تحويل هذه العلامات إلى كائنات حقيقية

### فهم العلامات 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** = بداية كيان الشخص
- **I-PER** = داخل (استمرار) للكيان الشخصي
- **أو** = خارج أي كيان ( لا مثيرة للاهتمام
- **B-ORG** = بداية منظمة

هذا يسمح للنموذج بتعامل مع العديد من -كلمات مثل "جون سميث" أو "مملكة المتحدةM 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-label schema CoNLL. بعض निर्यातات ONNX تتضمن أسماء العلامات في `config.json` (`id2label` مجال). إذا قمت باستبدال النماذج, قراءة العلامات من konfigur بدلاً عن البرمجة القاسيةM SK2

### إيجاد أفضل علامة تجارية

لكل علامة نختار العلامة ذات التقييم الأعلى

```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` Function converts raw scores to probabilities (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 هي *النسيج*, احتمالات غير مقيَّدة. هي ' مفيدة لتصنيف وتقدير الدرجاتM SK3 لكنها لا تعالج `0.92` كـ "92% صحيحة". استخدمها لتصفية توقعات الثقة المنخفضة -M SK3 وليست حقاً أرضياً

---


## الخطوة 6: استخراج Entities

الآن لدينا توقعات للـ - ,token ,. .علينا أن ندمجها في كيانات ..

أولا, مساعدات دمج 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();
}
```

الآن استخراج الكائنات . الثقة الكائنة هي **الثقة المحدودة في الرموز** على امتداد المسافة - conservative , بحيث لا يتم إخفاء علامة ضعيفة واحدة .

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

**ال sortie:**

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

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

---


## فهم كلمات فرعية WordPiece

يستخدم BERT **تدوين WordPiece**, الذي يقسم الكلمات غير المعروفة إلى كلمات فرعية `##` prefix means "continuation of previous word":

```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. أجزاء متعددة من النص

إذا كان لديك الكثير من الرسائل , تعالجها في مجموعات :

```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. إستخدام مشبك ذكي

لا تقلصوا دائماً إلى 512. استخدموا أصغر سطل يلائم :

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

في الواقع , ONNX NER سريع بما فيه الكفاية لتشغيله **في الخطوط أثناء الإمتصاص**, ليس فقط كمهمة عمل. يمكنك استخراج الكائنات عند وصول الوثائق بدلاً من الاصطفاء لها في وقت لاحق

### 3. تسارع GPU ( إختياري)

للسرعة العالية , إستخدم 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 | إستخدام LLM (GPTM SK3Claude) |
|--------------|------------------------|
| حجم كبير (1000s من الأبحاث ) | واحد -تحليل خارجي |
| الوحدات القياسية ( الناس, المنظماتM SK3 الأماكنMNK4 | أنواع الوحدت المخصصة SNK6
|خصوصية-بيانات حساسة | عندما تحتاج إلى تفسير |
| خطوط أنابيب إختبارية | تحليل استكشافي |
| استخراج الخصائص | تفسير / استنساخ |

كلا النهجين يعملون. يحلون **مشاكل مختلفة**. يستخرج NER الهيكل; سبب LLM عن المعنىM SK2

---


## الصورة الأكبر

هذا النمط-**الاستخراج الإفتراضي مع نماذج متجمدة, تتبعها التركيبة البديلة لاحقاً**- يتقلص أفضل بكثير من دفع النص الخام إلى LLM ويأمل أن يتصرف

NER ليس شيئًا تقوم به "agentify". إنهM SK2بنية تحتية . تقوم بتشغيله عند الإدخالMSC4 تخزين الكائناتMNK5 وتستخدمها في الأسفل لصفيتهاMRK6 ربطهاMMK7 أو توصيلها في خطوط أنابيب أكثر تعقيدًا

نفس المقاربة تطبق على مهمات إستخراج الخصائص الأخرى..

> **أين يلائم هذا**: هذا الرسم البياني + خط أنابيب النطاق الشمالي هو قطعة بناء واحدة. للصورة الكاملةM SK3 كيف تتغذى الكائنات المستخرجة في بناء الرسوم البيانيةMSC4 التكرارMNK5 والإستخراجMMK6انظر [تقلل من RAG](/blog/reduced-rag-concept) و [وثائق LucidRAG](https://github.com/scottgal/lucidrag). الكائنات التي تخرجها هنا تصبح العقدة; الوثائق تصبح الحوافM SK2 يشاهد LLM فقط ما يحتاجه

---


## الموارد

**المكتبات & نماذج**:

- **[Tesseract.NET](https://github.com/charlesw/tesseract)** - C# غلاف لـ Tesseract OCR
- **[بيانات تيس](https://github.com/tesseract-ocr/tessdata)** - فايلات بيانات مدربة لـ Tesseract
- **[BERT-base-NER ONNX](https://huggingface.co/protectai/bert-base-NER-onnx)** - نموذج NER الذي نستخدمه
- **[أونكس وقت التشغيل](https://onnxruntime.ai/docs/)** - الوثائق الرسمية
- **[ML.توكينر](https://www.nuget.org/packages/Microsoft.ML.Tokenizers)** مكتبة توكينر - مايكروسوفت'

**المقالات المرتبطة**:

- **[أنابيب تير ثلاثية -](/blog/constrained-fuzzy-image-ocr-pipeline)** - عندما يكون الرسم البياني البسيط غير كاف
- **[تقلل من RAG](/blog/reduced-rag-concept)** - أين تلائم الكائنات المستخرجة الصورة الأكبر
- **[LucidRAG](https://github.com/scottgal/lucidrag)** - تنفيذ كامل مع تفكيك الوحدات وبناء الرسوم البيانية