Extraction simple de la fonction OCR et NER dans C# avec ONNX (Français (French))

Extraction simple de la fonction OCR et NER dans C# avec ONNX

Wednesday, 21 January 2026

//

20 minute read

NuGet NuGet téléchargements Publication de GitHub (CLI)

Comme I' l’a construit lucideRAG I'm lire les médias sociaux où les gens continuent de demander la même chose . M SK2Comment obtenir des fonctionnalités du texte scanné ?' l'erreur de catégorie est toujours MSC4juste utiliser un LLMMST5qui fonctionne mais est très coûteux MST6 Alors que jeMst7m déjà en profondeur dans l'espace OCR J’ai pensé que j’aurais écrit une approche 'à l’intention des débutants ' à l’approche non-conventionnelle M SK3LLM (ou LLM facultative MSC5 pour le faire.

Vous avez des images avec un texte. Vous voulez extraire ce texte , puis trouver la structure utile à l'intérieur de celui-ci ( nomsM SK3 sociétés

Cet article montre les la plus simple possible pipeline: Tesseract pour extraction de texte, alors BERT NER (par le biais de l’ONNX) pour la reconnaissance des entités . Tous les locauxMSC3 Tous déterministesM SK4 Tous dans CCSK5

Le terme déterministe signifie ici des versions fixes, données linguistiques fixes≥, et aucun apprentissage adaptatif à l’heure d’exécution.

Besoin de la version emballée? Ce pipeline est maintenant un paquet NuGet avec l'auto-downloading models, OpenCV preprocessingMSC2 FlorenceM SK3 visionMST4 et un outil CLIMSL5 Voir Partie 2: Le paquet NuGet pour la version prête-àM SK1pour l'utilisation.


Le pipeline complet

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

Deux étapes, deux modèles , tous deux fonctionnent localementM SK2 Laissez' construire chaque partieMSC4


Partie 1: OCR avec Tesseract

Tesseract est le moteur OCR de source standard open - . Nous utilisons Tesseract.NET, un enveloppeur CM SK1

dotnet add package Tesseract

Vous avez également besoin des fichiers de données formés. Télécharger eng.traineddata de Les données de l'essai sont les suivantes : et le placer dans un tessdata dossier.

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

Il s’agit là d’un appel ExtractText("invoice.png") et vous obtiendrez une chaîne

Importance: TesseractEngine est coûteux de créer. Dans les applications réelles, le créer une fois et l’utiliser à nouveau

Limitations

Tesseract fonctionne bien pour nettoyer, hautM SK1 texte contrastant dans les caractères standard. Il se heurte à la difficulté :

  • Fonts stylisés ou décoratifs
  • Scans ou photos de faible qualité-
  • Texte tourné ou courbé
  • Texte sur des fonds complexes
  • GIF animés avec sous-titres
  • Frais de ligne (inter-\nnational) peut nécessiter un traitement après - avant le NER

Pour les systèmes de production qui doivent traiter des objets étranges, voir Le pipeline Three-Tier OCR-qui ajoute FlorenceM SK1 ONNX comme niveau moyen et escalation Vision LLM pour les cas durs.

Pour ce tutorial, nous présumons que vous avez des images ou un texte nettoyés d'une autre source.

Dans la pratique, vous voudrez normalement normaliser le résultat de l'OCR M SK1 trim whitespace , collapse repeated newlines, fix obvious hyphenationMNK4 avant de le transmettre à NERMSC5


Partie 2: NER avec ONNX

Pourquoi cette approche fonctionne?

Avant de se plonger dans le code, laissez-leur comprendre ce que nous faisons en réalité.

Qu'est-ce que NER?

Reconnaissance des entités désignées (NER) est un problème résolu. Les chercheurs ont formé des réseaux neuronaux capables de lire le texte et de mettre en évidence les "bits intéressants ":

  • PER - Nom de la personne "John Smith", MSC3DrMSC4 Jane DoeM SK5
  • ORG - Organisations "Microsoft", "NHSM SK4 |
  • LOC - Endroits "Londres", "Mount EverestM SK4
  • MISC - Autres entités

Le modèle ne comprend pas les modèles statistiques tirés de millions d’exemples étiquetés. NER est extraction de la caractéristique, non raisonnement. Il correspond à ce qui est observé sur les stéroïdes.

Pourquoi ONNX?

ANNEXE I (Open Neural Network Exchange) est un format standard pour les modèles M.L. frozen inference DLL pour les réseaux neuronaux: poids fixes dans l'enceinteM SK1 tenseurs hors de celle-ci , aucune logique d'entraînement, aucun caractère aléatoireMSC4

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

Le point de vue clé: un autre a fait le travail ardu (formation du modèle en Python). Vous faites simplement l'inférence dans CM SK2

Pourquoi ne pas simplement utiliser un LLM?

Vous pouvez envoyer un texte à GPT-4 et demander M SK1Trouver les personnes et entreprises dans ce texte ". Il fonctionne! MaisMSC4

Approche Accès rapide MSC2 Coût par dossier SMC3 Dossiers SMC4 Protection des renseignements personnels ESC5 Consistence ESC6
TÉU, TPAC, TPMD, TM, TMÉU, TC: ~50ms
LLM API local 4-30s $0 Les petits modèles peuvent être floqués \ Variable \
LLM API 1-5s $20-50 Données envoyées à l'extérieur S Variable M

Les LLM sont excellents pour des raisonnements complexes. Pour l’extraction de modèles à grande échelleM SK1 un modèle spécial est 40x plus rapide et gratuitMSC3


Le pipeline

Voici ce que nous construisons.

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

Chaque étape est simple.


Étape 1: Télécharger le modèle

Vous avez besoin de trois fichiers de HuggingFace. Téléchargez-les manuellement dans un dossier M SK1e .g., ./models/ner/):

Fichier Taille
model.onnx ~430MB Télécharger
vocab.txt ~230KB Télécharger
config.json ~1KB Télécharger

Le modèle est Bert-base-NER exporté au format ONNX par Protection.

Votre dossier devrait ressembler à:

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

Étape 2: Établissement du projet

Créer une nouvelle application de console et ajouter les paquets NuGet:

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

Il s’agit de deux paquets.

  • OnnxRuntime - Exécute le modèle
  • ML.Tokenizers - Génie le texte → conversion de cote

Étape 3: Comprendre la tokenisation

Avant que le modèle puisse traiter le texte, nous devons le convertir en chiffres. C'est appelé Tokenisation.

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

Points clés:

  • [CLS] et [SEP] sont des tokens spéciaux qui marquent les limites de la phrase
  • Chaque mot devient un nombre de vocab.txt
  • Le modèle ne voit que les chiffres, jamais le texte réel

Chargement du Tokenizer

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

Pourquoi? LowerCaseBeforeTokenization = false? Ce modèle a été formé sur un texte en case.

Importance: Le tokeniseur Il faut correspondre exactement au modèle. Utilisation d'un vocab différentM SK1 option de caisse, ou des identifiants spéciaux pour le symbole dégradera silencieusement les résultats . Utiliser toujours vocab.txt que les navires sont équipés du modèle

Tokeniser le texte

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

Après cela, nous avons

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

Étape 4: Faire fonctionner le modèle

Maintenant, nous envoyons ces chiffres dans le modèle ONNX. Le modèle retourne "logits"-cotes brutes pour chaque étiquette possible à chaque positionM SK3

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

Déchargement du modèle

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

Importance: Créer le tokenizer et la session d'inférence une fois (singletonMSC2service) et les réutiliserM SK4 DonMST5 ne recharger pas par documentInferenceSession est coûteux de créer.

Préparation des intrants

Le modèle prévoit:

  • input_ids: Nos numéros d'identification comme long[]
  • attention_masque: 1 pour les tokens réelsM SK2 ≥0 pour le padding
// 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;
    }
}

Inférence en cours

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

Le résultat logits a une forme [1, sequence_length, 9]-9 étiquettes possibles pour chaque position de coteM SK1


Étape 5: Décodage de la sortie

Le modèle produit des cotes brutes. Nous devons

  1. Trouver l'étiquette de cotation la plus élevée- pour chaque token
  2. Convertir ces étiquettes en entités réelles

Comprendre les étiquettes BIO

Le modèle utilise Notation BIO:

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 = Début d’une entité de personnes
  • I-PER = À l’intérieur d’une entité de Personne (suiteM SK2
  • O = À l’extérieur de toute entité
  • B-ORG = Début d’une organisation

Cela permet au modèle de manipuler des entités multiples de mots comme "John SmithM SK2 ou "United Kingdom".

La cartographie des étiquettes

// These are the 9 labels from the protectai/bert-base-NER-onnx config.json (id2label field)
string[] labels =
{
    "O",       // 0: Outside any entity
    "B-MISC",  // 1: Beginning of Miscellaneous
    "I-MISC",  // 2: Inside Miscellaneous
    "B-PER",   // 3: Beginning of Person
    "I-PER",   // 4: Inside Person
    "B-ORG",   // 5: Beginning of Organization
    "I-ORG",   // 6: Inside Organization
    "B-LOC",   // 7: Beginning of Location
    "I-LOC"    // 8: Inside Location
};

Remarque: Ce modèle spécifique utilise le schéma coNLL de l’étiquette standard 9-. config.json (id2label champ). Si vous échangez des modèlesM SK1 lire les étiquettes de la configuration plutôt que du codage dur.

Trouver le meilleur étiquette

Pour chaque token, nous choisissons l’étiquette ayant le score logit le plus élevé:

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'article 5 de la Loi sur l'immigration et le statut des réfugiés Softmax fonction convert les cotes brutes en probabilités (0-1):

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

Note sur la confiance: Les scores de Softmax sont relatives, probabilités non calibrées . EllesM SK2 sont utiles pour le classement et la détermination des seuils, mais ne traitent pas 0.92 en tant que "92% correct ". Utilisez-les pour filtrer les prédictions de confiance faiblesM SK2, non comme la vérité fondéeMSC4


Étape 6: Extraire les entités

Nous devons les fusionner en entités.

Premièrement, les aides à la fusion WordPiece. Ces manœuvres ## correctement les sous- mots et l’espacement de la ponctuation:

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

Maintenant, l’extraction de l’entité. La confiance de l'entité est la Confiance minimale en symboles sur l’ensemble de la gamme-conservatifM SK1 donc un seul symbole faible n’est pas caché par une évaluation moyenne

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 est géré entièrement par MergeWordPieces-aucun flux de contrôle spécial n'est nécessaireM SK1


Exemple complète

Voici un exemple de travail minimal que vous pouvez copier et exécuter:

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

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

Comprendre les sous- mots de WordPiece

BERT utilise Tokenisation WordPiece, qui divise les mots inconnus en sous- mots ## Avant-propos signifie "suite du mot précédent":

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'article 5 de la Loi sur l'immigration et le statut des réfugiés MergeWordPieces helper gère ce: ## les tokens sont ajoutés sans espace, produisant "Elasticsearch" au lieu de "Elastic search".


Conseils sur la performance

1. Batch de textes multiples

Si vous avez de nombreux textes, les traiter en lots:

// 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. Utilisation de bouclage intelligent

Utilisez le bol le plus petit qui convient.

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

Dans la pratique, ONNX NER est assez rapide pour fonctionner en ligne pendant l'ingestion, pas seulement comme un travail en lots. Vous pouvez extraire des entités à mesure que les documents arrivent plutôt qu’à les entrer dans la queue pour plus tardM SK2

3. Accélération GPU OptionnelleM SK2

Pour un haut débit, utiliser DirectML (WindowsM SK2 ou CUDA:

dotnet add package Microsoft.ML.OnnxRuntime.DirectML  # Windows GPU
# or
dotnet add package Microsoft.ML.OnnxRuntime.Gpu       # NVIDIA CUDA
var options = new SessionOptions();
options.AppendExecutionProvider_DML();  // Use GPU

Quand utiliser cela contre LLM

Utilisez ONNX NER
Volume élevé (1000s de dossiers)
Entités standards personnes, organismesM SK3 lieuxMSC4 Types d'entités personnalisés
PrivautéM SK1données sensibles Lorsque vous avez besoin d'explications
Pipelines déterministes Analyse exploratoire
Extraction des caractéristiques Interprétation

Les deux approches fonctionnent. Elles résolvent Différents problèmes. L'NER extrait la structure de ; La raison d'être des LLM quant au sens de .


Le plus grand tableau

Ce modèle-extraction déterministe avec des modèles congelés, suivie par une synthèse facultative plus tard-scales bien mieux que de pousser le texte brut dans un LLM et espérer qu’il se comporte

Le NER n’est pas quelque chose que vous "agentifiezM SK1 Il'l’infrastructure . Vous l’effectuez à l’ingestionMSC4 stockez les entitésMNK5 et les utilisez en aval pour le filtrageMRK6 la liaisonMMK7 ou l’alimentation dans des pipelines plus sophistiquésMEK8

La même approche s'applique aux autres tâches d'extraction des caractéristiques: embeddingsM SK1 classification , sentiment. Train une fois MSC4 ou utiliser préconiséMNK5 exportation à ONNXMRK6 courir partoutMST7 deterministementMst8

Lorsque cela convient: Ce OCR + Le pipeline de l’NER est un élément fondamentalM SK2 Pour le tableau complet- comment les entités extraites s’infiltrent dans la construction du graphe RAG réduit et le lucideRAG Documentation. Les entités que vous extraisez ici deviennent des nodes ; les documents deviennent des bords M SK2 le LLM ne voit qu'à ce qu'il a besoin .


Ressources

Bibliothèques & Modèles:

Étape suivante:

  • Partie 2: Le paquet NuGet - Ce pipeline en tant qu’un seul-line Installation NuGet avec le prétraitement OpenCVM SK2 Florence -2 visionMNK4 MicrosoftMRK5RecognizersMMK6 et un outil CLI

Articles connexes:

  • Le pipeline Three-Tier OCR - Lorsque le simple OCR n’est pas suffisant
  • RAG réduit - Lorsque les entités extraites correspondent à l’ensemble
  • lucideRAG - Mise en œuvre complète avec déduplication des entités et construction de graphiques
Finding related posts...
logo

© 2026 Scott Galloway — Unlicense — All content and source code on this site is free to use, copy, modify, and sell.