Back to "Extrazione semplice di OCR e NER in C# con ONNX"

This is a viewer only at the moment see the article on how this works.

To update the preview hit Ctrl-Alt-R (or ⌘-Alt-R on Mac) or Enter to refresh. The Save icon lets you save the markdown file to disk

This is a preview from the server running through my markdig pipeline

AI CSharp Docker NER OCR ONNX Tutorial

Extrazione semplice di OCR e NER in C# con ONNX

Wednesday, 21 January 2026

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

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

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

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

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


La Pipeline completa

flowchart LR
    subgraph OCR["Part 1: OCR"]
        IMG[Image]
        TESS[Tesseract]
        TXT[Raw Text]
    end

    subgraph NER["Part 2: NER"]
        TOK[Tokenize]
        BERT[BERT NER<br/>ONNX]
        ENT[Entities]
    end

    IMG --> TESS
    TESS --> TXT
    TXT --> TOK
    TOK --> BERT
    BERT --> ENT

    style TESS stroke:#f60,stroke-width:3px
    style BERT stroke:#f60,stroke-width:3px
    style ENT stroke:#090,stroke-width:3px

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


Parte 1: OCR con Tesseract

Tesseract è il motore OCR open standard. Tesseract.NET, a CM SK1 wrapper.

dotnet add package Tesseract

Servono anche i file di dati addestrati. scaricare eng.traineddata a partire da tessdata. E lo mettiamo in una tessdata folder.

using Tesseract;

public static string ExtractText(string imagePath)
{
    using var engine = new TesseractEngine("./tessdata", "eng", EngineMode.Default);
    using var img = Pix.LoadFromFile(imagePath);
    using var page = engine.Process(img);

    return page.GetText();
}

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

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

Limiti

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

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

Per i sistemi di produzione che devono gestire le cose strane, vedete Il Pipeline Three-Tier OCR- che aggiunge Florence-2 ONNX come escalazione intermedia e Vision LLM per i casi rigidi .

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

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


Parte 2: NER con ONNX

Perché questo approccio funziona?

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

Cos'è NER?

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

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

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

Perché ONNX?

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

flowchart LR
    subgraph Training["Training (Python)"]
        PT[PyTorch Model]
        TF[TensorFlow Model]
    end

    subgraph Export["Export Once"]
        ONNX[model.onnx]
    end

    subgraph Runtime["Run Anywhere"]
        CS[C# App]
        CPP[C++ App]
        JS[JavaScript App]
    end

    PT --> ONNX
    TF --> ONNX
    ONNX --> CS
    ONNX --> CPP
    ONNX --> JS

    style ONNX stroke:#f60,stroke-width:4px
    style CS stroke:#090,stroke-width:3px

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

Perché non usare solo un LLM?

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

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

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


Il Pipeline

Questo è quello che stiamo costruendo.

flowchart LR
    subgraph Input
        TEXT[Raw Text]
    end

    subgraph Tokenization["Step 1: Tokenization"]
        TOK[Split into tokens]
        IDS[Convert to IDs]
    end

    subgraph Model["Step 2: ONNX Inference"]
        BERT[BERT Model]
        LOGITS[Logits Output]
    end

    subgraph Output["Step 3: Decode"]
        LABELS[BIO Labels]
        ENT[Entities]
    end

    TEXT --> TOK
    TOK --> IDS
    IDS --> BERT
    BERT --> LOGITS
    LOGITS --> LABELS
    LABELS --> ENT

    style BERT stroke:#f60,stroke-width:4px
    style ENT stroke:#090,stroke-width:3px

Ogni passo è semplice.


Step 1: scaricare il modello

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

File Size URL
model.onnx ~430MB Lavorare
vocab.txt ~230KB Lavorare
config.json ~1KB Lavorare

Il modello è Bert-base-NER L'export è stato fatto nel formato ONNX. Proteggerai..

La vostra cartella dovrebbe sembrare:

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

Preparazione del progetto 2:

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

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

Che's it. Due pacchettiM SK2

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

Step 3: Comprendere la tokenizzazione

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

flowchart TD
    subgraph Input
        TEXT["John works at Microsoft"]
    end

    subgraph Tokenize["Tokenization"]
        T1["[CLS]"]
        T2["John"]
        T3["works"]
        T4["at"]
        T5["Microsoft"]
        T6["[SEP]"]
    end

    subgraph IDs["Token IDs"]
        I1["101"]
        I2["1287"]
        I3["2573"]
        I4["1120"]
        I5["7513"]
        I6["102"]
    end

    TEXT --> T1 & T2 & T3 & T4 & T5 & T6
    T1 --> I1
    T2 --> I2
    T3 --> I3
    T4 --> I4
    T5 --> I5
    T6 --> I6

    style T1 stroke:#c00,stroke-width:3px
    style T6 stroke:#c00,stroke-width:3px
    style I2 stroke:#090,stroke-width:3px
    style I5 stroke:#090,stroke-width:3px

punti chiave:

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

Lavorare il Tokenizer

using Microsoft.ML.Tokenizers;

// Load the vocabulary file
var vocabPath = "./models/ner/vocab.txt";

var options = new BertOptions
{
    LowerCaseBeforeTokenization = false,  // BERT-NER is case-sensitive!
    UnknownToken = "[UNK]",
    ClassificationToken = "[CLS]",
    SeparatorToken = "[SEP]",
    PaddingToken = "[PAD]"
};

using var stream = File.OpenRead(vocabPath);
var tokenizer = BertTokenizer.Create(stream, options);

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

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

Tokenizzando il testo

var text = "John Smith works at Microsoft in London.";

// Tokenize (splits into subwords)
var encoded = tokenizer.EncodeToTokens(text, out _);

// Get special token IDs
var clsId = 101;  // [CLS] token
var sepId = 102;  // [SEP] token

// Build the full sequence: [CLS] + tokens + [SEP]
var tokenIds = new List<int> { clsId };
tokenIds.AddRange(encoded.Select(t => t.Id));
tokenIds.Add(sepId);

// Also keep the text tokens for later
var tokens = new List<string> { "[CLS]" };
tokens.AddRange(encoded.Select(t => t.Value));
tokens.Add("[SEP]");

Dopo questo , abbiamo :.

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

Step 4: Condurre il modello

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

flowchart LR
    subgraph Inputs
        IDS["Token IDs<br/>[101, 1287, 3455, ...]"]
        MASK["Attention Mask<br/>[1, 1, 1, ...]"]
    end

    subgraph Model
        ONNX["BERT NER<br/>model.onnx"]
    end

    subgraph Outputs
        LOG["Logits<br/>[batch, seq_len, 9]"]
    end

    IDS --> ONNX
    MASK --> ONNX
    ONNX --> LOG

    style ONNX stroke:#f60,stroke-width:4px

Lavorare il modello

using Microsoft.ML.OnnxRuntime;
using Microsoft.ML.OnnxRuntime.Tensors;

// Configure for best performance
var sessionOptions = new SessionOptions
{
    GraphOptimizationLevel = GraphOptimizationLevel.ORT_ENABLE_ALL,
    IntraOpNumThreads = Math.Min(4, Environment.ProcessorCount)
};

// Load the model (takes ~2 seconds first time)
var session = new InferenceSession("./models/ner/model.onnx", sessionOptions);

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

Preparare le input

Il modello si aspetta:

  • input_ids: Le nostre ID di tocco come long[]
  • attenzione_mask: 1 per i token realiM SK2 \0 per la compensazione.
// Pad to a fixed length (BERT requires fixed shapes; powers of 2 are cache-friendly)
int sequenceLength = 64;  // or 128, 256, 512

var inputIds = new long[sequenceLength];
var attentionMask = new long[sequenceLength];

for (int i = 0; i < sequenceLength; i++)
{
    if (i < tokenIds.Count)
    {
        inputIds[i] = tokenIds[i];
        attentionMask[i] = 1;
    }
    else
    {
        inputIds[i] = 0;   // PAD token
        attentionMask[i] = 0;
    }
}

Inferenza in corso

// Create tensors (shape: [batch_size=1, sequence_length])
var inputIdsTensor = new DenseTensor<long>(inputIds, [1, sequenceLength]);
var attentionMaskTensor = new DenseTensor<long>(attentionMask, [1, sequenceLength]);

// Build inputs
var inputs = new List<NamedOnnxValue>
{
    NamedOnnxValue.CreateFromTensor("input_ids", inputIdsTensor),
    NamedOnnxValue.CreateFromTensor("attention_mask", attentionMaskTensor)
};

// Run the model
using var results = session.Run(inputs);

// Get output logits
var output = results.First(r => r.Name == "logits");
var logits = output.AsTensor<float>();

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


Step 5: Decodificare il risultato

Il modello produce punteggi grezzi. Dobbiamo:

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

Capire le targhette BIO

Il modello usa Notazione BIO:

flowchart LR
    subgraph Tokens
        T1["John"]
        T2["Smith"]
        T3["works"]
        T4["at"]
        T5["Microsoft"]
    end

    subgraph Labels
        L1["B-PER"]
        L2["I-PER"]
        L3["O"]
        L4["O"]
        L5["B-ORG"]
    end

    T1 --> L1
    T2 --> L2
    T3 --> L3
    T4 --> L4
    T5 --> L5

    style L1 stroke:#090,stroke-width:3px
    style L2 stroke:#090,stroke-width:3px
    style L5 stroke:#00f,stroke-width:3px
  • B-PER = Inizia dell'entità di una persona
  • I-PER = all'interno (continuazione) di un entità di una persona
  • O. = fuori da qualsiasi entità (non interessanteM SK2
  • B-ORG = Iniziare un'organizzazione

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

La mappatura di etichette

// These are the 9 labels the model was trained on (CoNLL-2003 dataset)
string[] labels =
{
    "O",       // 0: Outside any entity
    "B-PER",   // 1: Beginning of Person
    "I-PER",   // 2: Inside Person
    "B-ORG",   // 3: Beginning of Organization
    "I-ORG",   // 4: Inside Organization
    "B-LOC",   // 5: Beginning of Location
    "I-LOC",   // 6: Inside Location
    "B-MISC",  // 7: Beginning of Miscellaneous
    "I-MISC"   // 8: Inside Miscellaneous
};

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

Trovare l'etichetta migliore

Per ogni tocco, scegliamo l'etichetta con il punteggio logit più altoM SK1

var predictions = new List<(string Token, string Label, float Confidence)>();

int numLabels = 9;

for (int i = 0; i < tokens.Count; i++)
{
    // Skip special tokens
    if (tokens[i] is "[CLS]" or "[SEP]" or "[PAD]")
        continue;

    // Find highest scoring label
    float maxScore = float.MinValue;
    int maxIndex = 0;

    for (int j = 0; j < numLabels; j++)
    {
        float score = logits[0, i, j];
        if (score > maxScore)
        {
            maxScore = score;
            maxIndex = j;
        }
    }

    // Convert logit to probability (softmax)
    float confidence = Softmax(logits, i, numLabels, maxIndex);

    predictions.Add((tokens[i], labels[maxIndex], confidence));
}

L'informazione figura nella parte dispositiva. Softmax Funzione converte i punteggi grezzi alle probabilità (0-1):

static float Softmax(Tensor<float> logits, int position, int numLabels, int targetIndex)
{
    // Find max for numerical stability
    float maxLogit = float.MinValue;
    for (int j = 0; j < numLabels; j++)
        maxLogit = Math.Max(maxLogit, logits[0, position, j]);

    // Compute softmax
    float sumExp = 0f;
    for (int j = 0; j < numLabels; j++)
        sumExp += MathF.Exp(logits[0, position, j] - maxLogit);

    return MathF.Exp(logits[0, position, targetIndex] - maxLogit) / sumExp;
}

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


Step 6: Extrazione delle entità

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

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

static void AppendWordPiece(StringBuilder sb, string token)
{
    if (string.IsNullOrEmpty(token)) return;

    // WordPiece continuation: "##soft" → append without space
    if (token.StartsWith("##", StringComparison.Ordinal))
    {
        sb.Append(token.AsSpan(2));
        return;
    }

    // No leading space if first token or if punctuation
    if (sb.Length > 0 && !IsPunctuationToken(token))
        sb.Append(' ');

    sb.Append(token);
}

static bool IsPunctuationToken(string token) =>
    token.Length == 1 && char.IsPunctuation(token[0]);

static string MergeWordPieces(IEnumerable<string> tokens)
{
    var sb = new StringBuilder();
    foreach (var t in tokens)
        AppendWordPiece(sb, t);
    return sb.ToString();
}

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

public sealed class Entity
{
    public required string Text { get; init; }
    public required string Type { get; init; }  // PER, ORG, LOC, MISC
    public required float Confidence { get; init; }
}

static List<Entity> ExtractEntities(
    IReadOnlyList<(string Token, string Label, float Confidence)> predictions)
{
    var entities = new List<Entity>();

    string? currentType = null;
    var currentTokens = new List<string>();
    float currentConfidence = 1.0f;

    void Flush()
    {
        if (currentType == null || currentTokens.Count == 0) return;

        entities.Add(new Entity
        {
            Type = currentType,
            Text = MergeWordPieces(currentTokens),
            Confidence = currentConfidence
        });

        currentType = null;
        currentTokens.Clear();
        currentConfidence = 1.0f;
    }

    foreach (var (token, label, conf) in predictions)
    {
        // Continue current entity if model says I-<same type>
        if (currentType != null && label == $"I-{currentType}")
        {
            currentTokens.Add(token);  // keep ## form, merge handles it
            currentConfidence = Math.Min(currentConfidence, conf);
            continue;
        }

        // New entity begins
        if (label.StartsWith("B-", StringComparison.Ordinal))
        {
            Flush();
            currentType = label[2..];
            currentTokens.Add(token);
            currentConfidence = conf;
            continue;
        }

        // Anything else (O, or I- without matching current type) ends the entity
        Flush();
    }

    Flush();
    return entities;
}

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


Esempio completo

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

using System.Text;
using Microsoft.ML.OnnxRuntime;
using Microsoft.ML.OnnxRuntime.Tensors;
using Microsoft.ML.Tokenizers;

// === Configuration ===
var modelPath = "./models/ner/model.onnx";
var vocabPath = "./models/ner/vocab.txt";

// === Load tokenizer ===
var bertOptions = new BertOptions
{
    LowerCaseBeforeTokenization = false,
    UnknownToken = "[UNK]",
    ClassificationToken = "[CLS]",
    SeparatorToken = "[SEP]"
};

using var vocabStream = File.OpenRead(vocabPath);
var tokenizer = BertTokenizer.Create(vocabStream, bertOptions);

// === Load model ===
var sessionOptions = new SessionOptions
{
    GraphOptimizationLevel = GraphOptimizationLevel.ORT_ENABLE_ALL
};
using var session = new InferenceSession(modelPath, sessionOptions);

// === Process text ===
var text = "John Smith, CEO of Microsoft, announced the acquisition in London yesterday.";

// Tokenize
var encoded = tokenizer.EncodeToTokens(text, out _);

// Build sequence with special tokens
var tokens = new List<string> { "[CLS]" };
tokens.AddRange(encoded.Select(t => t.Value));
tokens.Add("[SEP]");

var rawIds = new List<int> { 101 };  // [CLS]
rawIds.AddRange(encoded.Select(t => t.Id));
rawIds.Add(102);  // [SEP]

// Pad to fixed length
int seqLen = 64;
var inputIds = new long[seqLen];
var attentionMask = new long[seqLen];

for (int i = 0; i < seqLen; i++)
{
    inputIds[i] = i < rawIds.Count ? rawIds[i] : 0;
    attentionMask[i] = i < rawIds.Count ? 1 : 0;
}

// === Run inference ===
var inputs = new List<NamedOnnxValue>
{
    NamedOnnxValue.CreateFromTensor("input_ids",
        new DenseTensor<long>(inputIds, [1, seqLen])),
    NamedOnnxValue.CreateFromTensor("attention_mask",
        new DenseTensor<long>(attentionMask, [1, seqLen]))
};

using var results = session.Run(inputs);
var logits = results.First().AsTensor<float>();

// === Decode predictions ===
string[] labels = ["O", "B-PER", "I-PER", "B-ORG", "I-ORG", "B-LOC", "I-LOC", "B-MISC", "I-MISC"];

// WordPiece merge helper
static string MergeWordPieces(List<string> tokens)
{
    var sb = new StringBuilder();
    foreach (var t in tokens)
    {
        if (t.StartsWith("##", StringComparison.Ordinal))
            sb.Append(t.AsSpan(2));
        else if (sb.Length > 0 && t.Length > 0 && !char.IsPunctuation(t[0]))
            sb.Append(' ').Append(t);
        else
            sb.Append(t);
    }
    return sb.ToString();
}

Console.WriteLine($"Input: {text}\n");
Console.WriteLine("Entities found:");

string? currentType = null;
var currentTokens = new List<string>();

for (int i = 1; i < tokens.Count - 1; i++)  // Skip [CLS] and [SEP]
{
    var token = tokens[i];

    // Find best label
    int bestIdx = 0;
    float bestScore = float.MinValue;
    for (int j = 0; j < 9; j++)
    {
        if (logits[0, i, j] > bestScore)
        {
            bestScore = logits[0, i, j];
            bestIdx = j;
        }
    }

    var label = labels[bestIdx];

    // Continue current entity if model says I-<same type>
    if (currentType != null && label == $"I-{currentType}")
    {
        currentTokens.Add(token);
        continue;
    }

    // New entity begins
    if (label.StartsWith("B-"))
    {
        if (currentType != null)
            Console.WriteLine($"  [{currentType}] {MergeWordPieces(currentTokens)}");

        currentType = label[2..];
        currentTokens = [token];
        continue;
    }

    // Anything else ends the current entity
    if (currentType != null)
        Console.WriteLine($"  [{currentType}] {MergeWordPieces(currentTokens)}");
    currentType = null;
    currentTokens.Clear();
}

// Output last entity
if (currentType != null)
    Console.WriteLine($"  [{currentType}] {MergeWordPieces(currentTokens)}");

Output:

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

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

Capire le frasi sottostanti di WordPiece

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

flowchart LR
    subgraph Original
        W1["Elasticsearch"]
    end

    subgraph Tokenized
        T1["Elastic"]
        T2["##search"]
    end

    subgraph Merged
        M1["Elasticsearch"]
    end

    W1 --> T1 & T2
    T1 --> M1
    T2 --> M1

    style T2 stroke:#c00,stroke-width:3px

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


I consigli per la performance

1. Batch Multiple Texts

Se avete molti messaggi, processarli in lotti:

// Instead of: 1 text × 1 inference = 50ms
// Do: 16 texts × 1 inference = 100ms (6ms per text)

int batchSize = 16;
var batchInputIds = new long[batchSize, seqLen];
// ... fill batch ...
var tensor = new DenseTensor<long>(batchInputIds, [batchSize, seqLen]);

2. Usare un boccone intelligente

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

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

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

3. Accelerazione GPU (Opzionale)

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

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

Quando usare questo contro LLM

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

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


Il quadro più grande

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

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

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

Dove questo va bene?: Questo OCR + Il tubo del NER è un blocco di costruzione. Per il quadro completoM SK3 come le entità extraite entrano nella costruzione dei graficiMSC4 la deduplicazioneMNK5 e la ripresaMZK6 vediamo RAG ridotto e la Documentazione LucidRAG. Le entità che si estraggono qui diventano nodi; i documenti diventano bordi ; il LLM vede solo quello di cui ha bisognoM SK3


Ressource

Le biblioteche & Modelli:

Article connessi:

  • Il Pipeline Three-Tier OCR - Quando un semplice OCR non è sufficiente
  • RAG ridotto - Dove le entità estrattoe si inseriscono nel quadro generale
  • LucidRAG - Full implementation with entity deduplication and graph construction
logo

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