Back to "DocSummarizer Part 3 - Gevorderde Concepten: De " Ik ging te ver 🤦" Deep Dive"

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

.NET AI BERT C# Embeddings LLM ONNX RAG

DocSummarizer Part 3 - Gevorderde Concepten: De " Ik ging te ver 🤦" Deep Dive

Sunday, 21 December 2025

Inleiding

Dit is Deel 3 van de DocSummarizer-serie:

  1. Part 1: Building a Document Summarizer with RAG - De architectuur en waarom de pijpleiding aanpak naïeve LLM-roepen overtreft
  2. Deel 2: Met het gereedschap - Quick-startgids voor de CLI
  3. Deel 3: Gevorderde concepten ( dit artikel) - De " Ik ging te verM SK4 technisch diepduiken.
  4. Part 4: Building RAG Pipelines - Gebruik de NuGet bibliotheek om je eigen RAG-apps te bouwen

Dit is een deel van mijn "Time-Boxed Tools " benaderingM SK3 geef mezelf een vaste venster om iets functioneels te bouwenMSC4 Het dwingt tot beslissingen en produceert werkcode in plaats van theoretische ontwerpen.

DocSummarizer begon als een demonstratie van hoe je zou moeten Build document summarizers with LLMs - de pijplijn-aanpak die ik in Part uitgelegd heb 1. De meeste tutorialen laten zien hoe je tekst in een LLM schuift en hoopt op het beste. Ik wilde de juiste architectuur tonenMSC3 chunkingM SK4embeddingsMST5 retrievalM ST6 citation validationMst7

Maar zoals ik altijd doe, raakte ik geïnteresseerd in de probleemruimte. Vier dagen laterM SK2 ging ik in het onderzoek duikenMSC4 Hoe verwerken kwaliteitssystemen eigenlijk samenvattingen?

Ik implementeerde versies van deze benaderingen. Wat begon als "HierM SK2 het juiste patroon was " werd ONNX-embeddings die lokaal draaidenMSC4 hybride zoekopdrachten combineren BMMske5 met dichte opsporingM Ske6 Maksimum marginale relevantie voor diversiteitMsche7 en Reciprocale Rank Fusion om signalen te combinerenM Sche8

Gewone waarschuwing: Dit is de " Ik ging te ver" diep duikenM SK3 Als je het gereedschap alleen maar wilt gebruikenMSC4 lees deel 2. als je het wil begrijpen Waarom? Het werkt. Hoe? De onderdelen passen samen, blijven lezen.

Dit artikel gaat over:

  • Sententie-inbedings: Hoe transformatormodellen (MiniLM/BGEM SK3GTEMSC4 tekst in vektoren omzetten
  • Runtime: ML-modellen lokaal uitvoeren zonder Python of cloud API's
  • RAG (Retrieval-Augmented GenerationM SK2: Grounding LLM output in bronmateriaal
  • Hybride Soektog: Kombineren van semantische en lexische extractie met RRF

De architectuur in een blik

Voordat we in het detail duiken, hier ' is hoe de stukken samenpassen:

flowchart TB
    subgraph Input["Document Input"]
        DOC[/"Document<br/>(PDF, MD, URL)"/]
    end

    subgraph Parse["Parsing Layer"]
        DOCLING["Docling<br/>(PDF/DOCX)"]
        MARKDIG["Markdig<br/>(Markdown)"]
    end

    subgraph Extract["Extraction Layer"]
        CHUNK["Document Chunker"]
        SEGMENT["Segment Extractor"]
    end

    subgraph Embed["Embedding Layer"]
        ONNX["ONNX Runtime<br/>(Sentence Transformers)"]
        OLLAMA_EMB["Ollama<br/>(Optional)"]
    end

    subgraph Store["Vector Storage"]
        QDRANT["Qdrant<br/>(Vector DB)"]
        MEMORY["In-Memory<br/>(Small Docs)"]
    end

    subgraph Retrieve["Retrieval Layer"]
        DENSE["Dense Search<br/>(Semantic)"]
        BM25["BM25<br/>(Lexical)"]
        RRF["RRF Fusion"]
    end

    subgraph Synthesize["Synthesis Layer"]
        OLLAMA["Ollama LLM<br/>(Local)"]
        TEMPLATES["Summary Templates"]
    end

    subgraph Output["Output"]
        SUMMARY[/"Summary with<br/>Citations [chunk-N]"/]
    end

    DOC --> DOCLING & MARKDIG
    DOCLING & MARKDIG --> CHUNK & SEGMENT
    CHUNK --> ONNX & OLLAMA_EMB
    SEGMENT --> ONNX
    ONNX & OLLAMA_EMB --> QDRANT & MEMORY
    QDRANT & MEMORY --> DENSE
    CHUNK --> BM25
    DENSE & BM25 --> RRF
    RRF --> OLLAMA
    OLLAMA --> TEMPLATES
    TEMPLATES --> SUMMARY

Begrijpen van inbedingen

Het probleem: Hoe vind je relevante inhoud zonder sleutelwoorden?

Als je een 500-bladhandleiding samenvat, moet je relevante delen vinden.

  • De gebruiker vraagt: "Hoe reset ik het toestel?
  • Handleiding zegt: "Om fabrieksinstellings te herstellen..."
  • Keyword search misses it ( geen gedeelde woorden)

Je hebt het nodig. Semantische zoekopdracht - matching by meaning, not just wordsM SK2

Wat zijn Embeddings?

Embeddings lossen dit op door tekst te veranderen in dichte vektoren ( reeksen van getallen) die semantische betekenis vastleggenM SK2 Vergelijkbare betekenissen = vergelijkbare vektorsMska4 ongeacht de exacte woordenschatMske5

Hier,', is de intuïtie, :, stel je een dimensionale ruimte voor waar elk stuk tekst een positie heeft.

graph LR
    subgraph "Embedding Space (simplified to 2D)"
        A["🚗 car"]
        B["🚙 automobile"]
        C["🏎️ vehicle"]
        D["🍎 apple"]
        E["🍊 orange"]
        F["🍌 fruit"]
    end
    
    A -.->|"close"| B
    B -.->|"close"| C
    A -.->|"close"| C
    
    D -.->|"close"| E
    E -.->|"close"| F
    D -.->|"close"| F
    
    A -.-|"far"| D

Waarom Sentence Transformers (Niet Rou BERT)?

Probleem: Ik had ingebouwingen nodig die werkten voor semantische gelijkenissen . Rou BERT was ontworpen voor classificatietaakjes, geen gelijkenigtezoekingM SK3

Oplossing: Gebruik zintransformatoren - modellen die specifiek getraind zijn op gelijkenissentaakjes met behulp van contrastief leren. BERT architectuur Maar goed-anders afgestemd.

Modellen als: all-MiniLM-L6-v2 en bge-small-en-v1.5 Ze trainden zich op miljarden tekstpars zoals :.

  • "Hoe herstel je het toestel" ↔ | | "Herstellen van fabrieksopstellings |
  • "Hoe reset je toestel" ↔ | | "Productspecificaties

De training leert ze:: gelijkaardige betekenissen = nauwe vektoren (hoge kosinusvergelijkingM SK3

Verwant: Als je wilt begrijpen hoe transformatormodellen werken op een dieper niveau - inclusief aandachtsmechanismenM SK2 encoder-decoderarchitektuurMSC4 en waaromembeddings werken - kijk mijn artikel op Hoe neural Machine Translation werkt. Het omvat dezelfde transformatorconcepten vanuit een vertalingsperspectief.

Implementatie: We nemen de outputlaag van het model' en passen ze toe. Medium pooling - met een gemiddelde inbouw van alle tekens om één enkele vektor voor de hele tekst te krijgen

flowchart LR
    subgraph Input
        TEXT["The quick brown fox"]
    end
    
    subgraph Tokenization
        CLS["[CLS]"]
        T1["the"]
        T2["quick"]
        T3["brown"]
        T4["fox"]
        SEP["[SEP]"]
    end
    
    subgraph "BERT Encoder"
        direction TB
        L1["Layer 1: Self-Attention"]
        L2["Layer 2: Self-Attention"]
        L3["..."]
        L6["Layer 6: Self-Attention"]
    end
    
    subgraph Output
        E1["E[CLS]"]
        E2["E[the]"]
        E3["E[quick]"]
        E4["E[brown]"]
        E5["E[fox]"]
        E6["E[SEP]"]
    end
    
    subgraph Pooling
        MEAN["Mean Pool<br/>(with attention mask)"]
        VEC["384-dim Vector"]
    end
    
    TEXT --> CLS & T1 & T2 & T3 & T4 & SEP
    CLS & T1 & T2 & T3 & T4 & SEP --> L1
    L1 --> L2 --> L3 --> L6
    L6 --> E1 & E2 & E3 & E4 & E5 & E6
    E1 & E2 & E3 & E4 & E5 & E6 --> MEAN
    MEAN --> VEC

ONNX: Met lokale ML-modellen

Het probleem: Python Dependency Hell

Ik wilde embeddings in "just work" wanneer iemand het gereedschap bestuurtM SK2 De standaard aanpak :

  1. Installeer Python + PyTorch + transformatoren
  2. Download modellen met de hand.
  3. Hope-versie conflicten don' niet alles breken

Dit sucks. Gebruikers willen docsummarizer -f doc.pdf, geen 30-stap-opstellingsgids.

Waarom?

ONNX (Open Neural Network Exchange) is een open formaat voor ML-modellen . De killer featureM SK3 looptijdveronderstelling zonder Python.

Wat krijg ik met ONNX Runtime:

  1. Geen externe afhankelijkheden. - Geen Python-treiders, PyTorchM SK2 CUDA
  2. Auto-download modellen - Laten downloads eerst uit HuggingFace lopen (~23-34MB), dan gecacheerd
  3. Pure .NET - Werk overal .NET draait Windows, LinuxM SK4 macOSMska5 ARMMske6
  4. CPU-inderving - Geen GPU nodig, werkt op goedkope hardware

Trade-off: Een beetje langzamer dan GPU PyTorch , maar veel sneller dan gebruikers vragen om Python te installerenM SK3

Het modelregister.

DocSummarizer bevat meerdere ingebouwde modellen, elk met een andere handelM SK1offs:

Modell Afmetingen Max tekens Grootte Kwantitaal МSK5 Mijzer Gebruik case Behoeft instructie
AllMiniLmL6V2 384 ♫ ♫
BgeSmallEnV15 384 ♫ ♫
GteSmall 384 ≥ ≤512 ~34 MB Gemeenschappelijk doel Nee
MultiQaMiniLm 384 ♫ ♫

Nota: Alle inskrywings van het registru wijzen op WordPiece-compatible ONNX-exporten (gebruik vocab.txt). BPE/Unigrammodellen worden nog niet ondersteundM SK2

BGE-instructieformat: Sommige modellen, zoals BGE), hebben voor een optimale prestatie prefixen nodig . Het exacte formaat hangt af van het modelM SK4

// Query embedding (what the user asks)
var queryText = "Represent this sentence for searching relevant passages: " + userQuery;
var queryEmbedding = await EmbedAsync(queryText);

// Passage embedding (document chunks)
// Some BGE variants prefix passages, others don't - check model documentation
var passageEmbedding = await EmbedAsync(chunkText);

Het register volgt welke modellen instructies nodig hebben via RequiresInstruction en QueryInstruction velden. Als je met instructies werkt, moet je altijd de kwaliteit van het terughalen van de referentie vinden-gebaseerde modellenM SK2

Hier is hoe het modelregister werkt'

public static class OnnxModelRegistry
{
    public static EmbeddingModelInfo GetEmbeddingModel(OnnxEmbeddingModel model, bool quantized = true)
    {
        return model switch
        {
            OnnxEmbeddingModel.AllMiniLmL6V2 => new EmbeddingModelInfo
            {
                Name = "all-MiniLM-L6-v2",
                HuggingFaceRepo = "Xenova/all-MiniLM-L6-v2",
                ModelFile = quantized ? "onnx/model_quantized.onnx" : "onnx/model.onnx",
                VocabFile = "vocab.txt",
                EmbeddingDimension = 384,
                MaxSequenceLength = 256,
                SizeBytes = quantized ? 23_000_000 : 90_000_000,
                RequiresInstruction = false
            },
            // ... other models
        };
    }
}

Tokenisatie: WordPiece vs BPE

Verschillende modellen gebruiken verschillende tokenizers. De all-MiniLM-L6-v2 Het model gebruikt WordPiece-tokenisatie ( zoals BERT), die onbekende woorden in subwoordtokens splitstMSC2 Andere modellen kunnen BPE gebruiken (ByteM SK4Pair EncodingMska5 of Unigram tokenizersMske6

Belangrijk: Modell-tokenizers moeten overeenkomen met de trainingstokenizerM SK1 Ons registreer volgt welke tokenizer elk model nodig heeft. Huidig geïnstalleerd: WordPiece (via vocab.txt). BPE/Unigram-onderstel via tokenizer.json is gepland maar nog niet geïnstalleerd - blijf bij de WordPiece-modellen in het register staan voor nu.

public class BertTokenizer
{
    private readonly Dictionary<string, int> _vocab;
    private const int ClsTokenId = 101;  // [CLS] - start of sequence
    private const int SepTokenId = 102;  // [SEP] - end of sequence
    private const int PadTokenId = 0;    // [PAD] - padding
    private const int UnkTokenId = 100;  // [UNK] - unknown token

    public BertTokenizer(string vocabPath)
    {
        // Load vocabulary: word -> token ID
        _vocab = File.ReadAllLines(vocabPath)
            .Select((word, index) => (word, index))
            .ToDictionary(x => x.word, x => x.index);
    }

    public (long[] InputIds, long[] AttentionMask, long[] TokenTypeIds) 
        Encode(string text, int maxLength)
    {
        // Split text into words, then apply WordPiece to each word
        var words = text.ToLowerInvariant()
            .Split(new[] { ' ', '\t', '\n', '\r' }, StringSplitOptions.RemoveEmptyEntries);
        var tokens = words.SelectMany(WordPieceTokenize).ToList();
        
        // Truncate to fit [CLS] and [SEP] tokens
        if (tokens.Count > maxLength - 2)
            tokens = tokens.Take(maxLength - 2).ToList();

        // Build input: [CLS] + tokens + [SEP] + [PAD]...
        var inputIds = new List<long> { ClsTokenId };
        inputIds.AddRange(tokens.Select(t => (long)GetTokenId(t)));
        inputIds.Add(SepTokenId);

        // Pad to maxLength
        var padCount = maxLength - inputIds.Count;
        inputIds.AddRange(Enumerable.Repeat((long)PadTokenId, padCount));

        // Attention mask: 1 for real tokens, 0 for padding
        var attentionMask = inputIds.Select(id => id != PadTokenId ? 1L : 0L).ToArray();
        
        // Token type IDs: all zeros for single sentence
        var tokenTypeIds = new long[maxLength];

        return (inputIds.ToArray(), attentionMask, tokenTypeIds);
    }

    private IEnumerable<string> WordPieceTokenize(string word)
    {
        // If the whole word is in vocabulary, return it
        if (_vocab.ContainsKey(word))
        {
            yield return word;
            yield break;
        }

        // Otherwise, split into subwords with "##" prefix
        int start = 0;
        while (start < word.Length)
        {
            int end = word.Length;
            string? curSubstr = null;

            while (start < end)
            {
                var substr = word[start..end];
                if (start > 0) substr = "##" + substr;  // Continuation marker

                if (_vocab.ContainsKey(substr))
                {
                    curSubstr = substr;
                    break;
                }
                end--;
            }

            if (curSubstr == null)
            {
                yield return "[UNK]";
                yield break;
            }

            yield return curSubstr;
            start = end;
        }
    }
}

Voorbeeld tokenisatie:

Invoer
"embedding" ["em", "##bed", "##ding"]
"DocSummarizer" ["doc", "##su", "##mm", "##ari", "##zer"]
"the quick brown" ["the", "quick", "brown"]

Mean Pooling met aandachtsmask

Nadat BERT de tekens verwerkt, krijgen we een verborgen staat voor elk teken.

private static float[] MeanPool(Tensor<float> hiddenStates, long[] attentionMask, int hiddenSize)
{
    // Assumes last_hidden_state shape: [batch=1, seq_len, hidden_size]
    // Note: Many sentence-transformer models export a pooled output directly,
    // but we use mean pooling for consistency across all ONNX exports.
    var result = new float[hiddenSize];
    var dims = hiddenStates.Dimensions.ToArray();
    var seqLen = (int)dims[1];
    
    // Count real tokens (not padding)
    float maskSum = attentionMask.Count(x => x == 1);
    if (maskSum == 0) maskSum = 1; // Avoid division by zero

    // Average each dimension, weighted by attention mask
    for (int h = 0; h < hiddenSize; h++)
    {
        float sum = 0;
        for (int s = 0; s < seqLen; s++)
        {
            if (attentionMask[s] == 1)
                sum += hiddenStates[0, s, h];
        }
        result[h] = sum / maskSum;
    }

    // L2 normalize for cosine similarity
    float norm = MathF.Sqrt(result.Sum(x => x * x));
    if (norm > 0)
    {
        for (int i = 0; i < result.Length; i++)
            result[i] /= norm;
    }

    return result;
}

De Complete Embedding Pipeline

Hier' is de volledige stroom van tekst naar inbeding:

public class OnnxEmbeddingService : IEmbeddingService, IDisposable
{
    private InferenceSession? _session;
    private BertTokenizer? _tokenizer;

    public async Task<float[]> EmbedAsync(string text, CancellationToken ct = default)
    {
        await InitializeAsync(ct);  // Downloads model if needed
        
        // Prepend instruction for models that need it (like BGE)
        if (_modelInfo.RequiresInstruction)
            text = _modelInfo.QueryInstruction + text;

        // Tokenize
        var (inputIds, attentionMask, tokenTypeIds) = 
            _tokenizer.Encode(text, _maxSequenceLength);

        // Create ONNX tensors
        var inputIdsTensor = new DenseTensor<long>(inputIds, new[] { 1, inputIds.Length });
        var attentionMaskTensor = new DenseTensor<long>(attentionMask, new[] { 1, attentionMask.Length });
        var tokenTypeIdsTensor = new DenseTensor<long>(tokenTypeIds, new[] { 1, tokenTypeIds.Length });

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

        // Run inference
        using var results = _session.Run(inputs);
        
        // Get hidden states output
        var output = results.First(r => r.Name == "last_hidden_state");
        var outputTensor = output.AsTensor<float>();

        // Mean pooling with attention mask
        return MeanPool(outputTensor, attentionMask, _modelInfo.EmbeddingDimension);
    }
}

RAG: Onttreking-Beperkte Genereering

Het probleem: LLMs kunnen' niet lezen M SK2Pagedocumenten

De naïeve aanpak mislukt:

var text = File.ReadAllText("500-page-manual.txt"); // 2MB of text
var summary = await llm.GenerateAsync($"Summarize: {text}"); // ❌ Doesn't fit in context

Zelfs met 128K context-windows, kun je niet zomaar enorme documenten in de : dumpen.

  • Truncation: Slechts de eerste 100 pagina's passen, blijven geignoreerd.
  • HallucinatieLLM uitvindt "facts" om gaten te vullen.
  • Kost: Verwerking 2MB van tekstkosten ≥$$$ per vraag
  • Kwaliteit: LLM's worden verwarren met massale context.

De oplossing: RAG M SK1Retrieval-Augmented Generation )

In plaats van alles te sturen, stuur alleen wat relevant is.

  1. Engeland: Skei het document in segmenten
  2. Inbedden: Segmentelen omzetten in vektoren (semantische weergave)
  3. Onttrek: Vind de meest relevante segmenten voor de zoekopdracht.
  4. Synthese: LLM vat alleen die segmenten samen.

Waarom dit werkt: De LLM ziet 10KB met zeer relevante inhoud in plaats van 2MB met vooral onrelevante tekst..

flowchart LR
    subgraph "Without RAG"
        DOC1[/"500-page PDF"/]
        LLM1["LLM<br/>(32K context)"]
        OUT1["❌ Truncated or<br/>Hallucinated"]
    end
    
    subgraph "With RAG"
        DOC2[/"500-page PDF"/]
        CHUNKS["100 Chunks"]
        VDB["Vector DB"]
        QUERY["Query"]
        TOP["Top 10 Chunks"]
        LLM2["LLM"]
        OUT2["✅ Grounded<br/>Summary"]
    end
    
    DOC1 --> LLM1 --> OUT1
    
    DOC2 --> CHUNKS --> VDB
    QUERY --> VDB --> TOP --> LLM2 --> OUT2

Dokumentenstrategieën

DocSummarizer ondersteun multiple chunking strategieën gebaseerd op documentstructuur:

public class DocumentChunker
{
    public List<DocumentChunk> ChunkByHeadings(string markdown, int maxHeadingLevel = 2)
    {
        var chunks = new List<DocumentChunk>();
        var lines = markdown.Split('\n');
        var currentChunk = new StringBuilder();
        var currentHeading = "";
        var headingLevel = 0;
        var order = 0;

        foreach (var line in lines)
        {
            // Detect heading (# to ######)
            var headingMatch = Regex.Match(line, @"^(#{1,6})\s+(.+)$");
            
            if (headingMatch.Success && 
                headingMatch.Groups[1].Length <= maxHeadingLevel)
            {
                // Flush current chunk
                if (currentChunk.Length > 0)
                {
                    chunks.Add(new DocumentChunk(
                        Order: order++,
                        Heading: currentHeading,
                        HeadingLevel: headingLevel,
                        Content: currentChunk.ToString().Trim(),
                        Hash: ComputeHash(currentChunk.ToString())
                    ));
                }
                
                // Start new chunk
                currentHeading = headingMatch.Groups[2].Value;
                headingLevel = headingMatch.Groups[1].Length;
                currentChunk.Clear();
            }
            else
            {
                currentChunk.AppendLine(line);
            }
        }
        
        // Don't forget the last chunk
        if (currentChunk.Length > 0)
        {
            chunks.Add(new DocumentChunk(
                Order: order,
                Heading: currentHeading,
                HeadingLevel: headingLevel,
                Content: currentChunk.ToString().Trim(),
                Hash: ComputeHash(currentChunk.ToString())
            ));
        }
        
        return chunks;
    }
}

Segmentextractie voor Fine-Grained Retrieval

Voor langere documenten, DocSummarizer extraheert individuele segmenten (woorden , lijst itemsM SK3 codeblocksMSC4 met saliëntie scoringMスク5

public class SegmentExtractor
{
    public async Task<ExtractionResult> ExtractAsync(string docId, string markdown)
    {
        // 1. Parse into typed segments
        var segments = ParseToSegments(docId, markdown);
        
        // 2. Generate embeddings
        await GenerateEmbeddingsAsync(segments);
        
        // 3. Calculate document centroid (average embedding)
        var centroid = CalculateCentroid(segments);
        
        // 4. Score by salience using MMR (Maximal Marginal Relevance)
        ComputeSalienceScores(segments, centroid);
        
        return new ExtractionResult
        {
            AllSegments = segments,
            TopBySalience = segments.OrderByDescending(s => s.SalienceScore).Take(50).ToList(),
            Centroid = centroid
        };
    }
}

Het probleem: Semantische zoekresultaten Duplikaten

Zonder MMR, onttrek voor "Hoe werkt het opslagwerk?" is teruggestuurdM SK3

  1. "Caching overview" (0.95 overeenkomstenM SK3
  2. "Introduction tot cachingM SK1 (0.94 overeenkomsten)
  3. "Wat is cachingM SK1 (0.93 overeenkomsten)
  4. "De details van de cache-implementatieM SK1 (0.85 overeenkomsten)

De bovenste 3-resultaten zeggen allemaal hetzelfde.

De oplossing: Maksimale marginale relevantie (MMRM SK2

MMR-bilances relevantie (similariteit tot de vraag) met diversiteit. (dissimilariteit tot al-uitgewählte items).

Formule: $$MMR = \lambda \cdot \text(sMSC7vraagM SK8 ♫ ♫ - | ♫_{s' \in Gekose}

Wat het doet: Bestrijdt kandidaten die vergelijkbaar zijn met eerdere segmenten.

flowchart TB
    subgraph "MMR Selection"
        S1["Segment 1<br/>Score: 0.95"]
        S2["Segment 2<br/>Score: 0.90"]
        S3["Segment 3<br/>Score: 0.88"]
        S4["Segment 4<br/>Score: 0.85"]
    end
    
    subgraph "Selected"
        SEL1["✓ Seg 1<br/>(highest)"]
        SEL2["✓ Seg 3<br/>(most diverse)"]
        SEL3["✓ Seg 4"]
    end
    
    S1 -->|"Select"| SEL1
    S2 -->|"Skip - too similar to Seg 1"| X["❌"]
    S3 -->|"Select"| SEL2
    S4 -->|"Select"| SEL3

De formule:

$$MMR = \lambda \cdot sim (s, centroide ) ♫ ♫ - ♫_{s' \in Gekose} sim

private List<Segment> SelectSentencesMMR(
    List<Segment> segments,
    float[] centroid,
    int targetCount)
{
    var selected = new List<Segment>();
    var candidates = new HashSet<Segment>(segments.Where(s => s.Embedding != null));
    
    // Pre-calculate centroid similarities
    foreach (var segment in candidates)
    {
        segment.Score = CosineSimilarity(segment.Embedding!, centroid) 
                      * segment.PositionWeight;
    }
    
    while (selected.Count < targetCount && candidates.Count > 0)
    {
        Segment? best = null;
        double bestScore = double.MinValue;
        
        foreach (var candidate in candidates)
        {
            // Relevance: similarity to centroid
            var relevance = candidate.Score;
            
            // Diversity: max similarity to already selected
            double maxSimToSelected = 0;
            foreach (var sel in selected)
            {
                var sim = CosineSimilarity(candidate.Embedding!, sel.Embedding!);
                maxSimToSelected = Math.Max(maxSimToSelected, sim);
            }
            
            // MMR score: balance relevance and diversity
            var mmrScore = _config.Lambda * relevance 
                         - (1 - _config.Lambda) * maxSimToSelected;
            
            if (mmrScore > bestScore)
            {
                bestScore = mmrScore;
                best = candidate;
            }
        }
        
        if (best != null)
        {
            selected.Add(best);
            candidates.Remove(best);
        }
    }
    
    return selected;
}

Hybride Suche met RRF

Het probleem: Semantische zoekopdracht misst Exact Matches

Ik kwam dit tegen tijdens de test.:

Navraag: "Wat is de API-endpunt voor authenticatie?"

Semantische zoekopdracht kwam terug:

  1. "User login flow overview" (hoge overeenkomstenM SK3
  2. "VeerveiligheidsbestrijdingsmaatregelenM SK1 (hoge gelijkenissen)
  3. "Sessiemanagement" (hoge overeenkomstenM SK3

Wat het mislukte: De actuale API-endpoint begraven in code voorbeelden: POST /api/v1/auth/login

Waarom?: Inbeddingsmodellen worden op natuurlijke taal getraind, geen code / URL's / exacte termen M SK4 Het eindpunt POST /api/v1/auth/login doesn't semantisch overeenstemt met "authentication endpointM SK2 - het is een letterlijke technische referentie

De oplossing: Hybride zoekopdracht (Semantice + LexicaleM SK3

Combineer twee extractiemethoden met komplementaire sterktes:

Soektype sterktes zwaktes M
Digt (Embedding) Semantische begrip, Synonymen Er kunnen exacte gelijkenissen ontbrekenM SK3 zeldzame termen
Sparse (BM25) Extreem passende sleutelwoorden, zeldzame termen Geen semantische begrip

Hybride zoekopdracht combineert beide met Reciprocal Rank Fusion (RRF):

flowchart TB
    QUERY["Query: 'authentication security'"]
    
    subgraph Dense["Dense Search (Semantic)"]
        D1["1. OAuth 2.0 implementation"]
        D2["2. User login flow"]
        D3["3. Password hashing"]
    end
    
    subgraph Sparse["BM25 Search (Lexical)"]
        S1["1. Authentication middleware"]
        S2["2. Security headers"]
        S3["3. OAuth 2.0 implementation"]
    end
    
    subgraph RRF["RRF Fusion (Illustrative)"]
        R1["OAuth 2.0 implementation<br/>RRF = 1/(60+1) + 1/(60+3) ≈ 0.032"]
        R2["Authentication middleware<br/>RRF = (not in dense) + 1/(60+1) ≈ 0.016"]
        R3["User login flow<br/>RRF = 1/(60+2) + (not in BM25) ≈ 0.016"]
    end
    
    QUERY --> Dense & Sparse
    Dense --> RRF
    Sparse --> RRF

Nota: De RRF-scores zijn illustrerend . De constante k=60 is standaardM SK3 de werkelijke rangorde hangt af van het volledige kandidatenstelMSC4

RRF-implementatie

public static class HybridRRF
{
    /// <summary>
    /// Reciprocal Rank Fusion: combine multiple rankings into one.
    /// 
    /// Formula: RRF(d) = Σ 1/(k + rank_i(d))
    /// 
    /// Where k = 60 (standard constant to prevent division by small numbers)
    /// </summary>

    public static List<Segment> Fuse(
        List<Segment> segments,
        string query,
        BM25Scorer bm25,
        int k = 60,
        int topK = 20)
    {
        // Rank by dense similarity
        var byDense = segments
            .Where(s => s.Embedding != null)
            .OrderByDescending(s => s.QuerySimilarity)
            .ToList();
        
        // Rank by BM25 (scorer is built over the same ordered segment list)
        var bm25Scores = segments
            .Select((s, i) => (segment: s, score: bm25.Score(i, query)))
            .OrderByDescending(x => x.score)
            .Select(x => x.segment)
            .ToList();
        
        // Rank by salience (pre-computed importance)
        var bySalience = segments
            .OrderByDescending(s => s.SalienceScore)
            .ToList();
        
        // Compute RRF scores
        var rrfScores = new Dictionary<Segment, double>();
        
        void AddRRFScore(List<Segment> ranking)
        {
            for (int i = 0; i < ranking.Count; i++)
            {
                var segment = ranking[i];
                var rrfContribution = 1.0 / (k + i + 1);  // 1-based rank
                
                if (!rrfScores.TryAdd(segment, rrfContribution))
                    rrfScores[segment] += rrfContribution;
            }
        }
        
        AddRRFScore(byDense);
        AddRRFScore(bm25Scores);
        AddRRFScore(bySalience);
        
        // Return top-K by fused score
        return rrfScores
            .OrderByDescending(kv => kv.Value)
            .Take(topK)
            .Select(kv => kv.Key)
            .ToList();
    }
}

BM25: De Sparse Retrieval Workhorse

BM25 M SK1Best Matching 25) is de klassieke informatieherhaling-algoritme. Het combineert term frequentieMSC4 omgekeerde document frequentie~, en documentlengtenormalisatie~:

public class BM25Scorer
{
    private const double K1 = 1.5;  // Term frequency saturation
    private const double B = 0.75;  // Length normalization factor
    
    public double Score(int docIndex, string query)
    {
        var queryTerms = Tokenize(query);
        var docTermFreq = _docTermFreqs[docIndex];
        var docLength = _docLengths[docIndex];
        
        double score = 0;
        
        foreach (var term in queryTerms.Distinct())
        {
            if (!docTermFreq.TryGetValue(term, out var tf)) continue;
            if (!_docFreqs.TryGetValue(term, out var df)) continue;
            
            // IDF with smoothing
            var idf = Math.Log((_corpusSize - df + 0.5) / (df + 0.5) + 1);
            
            // BM25 TF component with length normalization
            var tfNorm = (tf * (K1 + 1)) / 
                (tf + K1 * (1 - B + B * docLength / _avgDocLength));
            
            score += idf * tfNorm;
        }
        
        return score;
    }
}

TF-IDF voor Content Centrality

Het probleem: Hoe onderscheid je kerninhoud van Trivia?

Als ik een roman samenvat, krijg ik resultaten als:

"De hoofdpersoon droeg een blauwe jas . Watson merkte dat het weer mild was.

Dit zijn nauwkeurige extracties, maar ze zijn ook nauwkespelijk. Kleur (scene Kernbodempunten.

De uitdaging: Hoe kun je het verschil zien tussen

  • Kerninhoud: Verschijnt overal in het document ( karakternamen, hoofdthema'sM SK3 sleutelmomenten
  • Ondersteunende details: Verschijnt in sommige delen (subplots
  • Kleur: RareM SK1 specifieke details ( wat iemand droeg, beschrijvingen van meubelsMSC4

De oplossing: TF-IDF voor de centrale classificatie

TF-IDF M SK1Term-Frequentie - Omgekeerde documentfrequentie) schattingen Hoe belangrijk een term is voor het document., niet zijn waarheidswaarde.

Logic:

  • Hoge DF (>50% van stukjes): "Sherlock",
  • Medium DF (20-50%): "Baker Street", | | "investiging |
  • Lae DF (<20%): "blauwe mantel", ≥"eikenmöbelsM SK4 | | → Incidentele kleur

Dit gaat niet over waarheid. (een herhaalde claim kan vals zijn. centraliteit aan het document..

flowchart LR
    subgraph "TF-IDF Classification"
        CLAIM["Claim text"]
        TERMS["Extract terms"]
        TFIDF["Compute TF-IDF"]
        CLASS["Classify"]
    end
    
    subgraph "Term Types"
        COMMON["High DF (>50%)<br/>→ Core content"]
        MODERATE["Medium DF (20-50%)<br/>→ Supporting detail"]
        RARE["Low DF (<20%)<br/>→ Incidental colour"]
    end
    
    CLAIM --> TERMS --> TFIDF --> CLASS
    CLASS --> COMMON & MODERATE & RARE
public class TextAnalysisService
{
    private readonly Dictionary<string, int> _documentFrequency = new();
    private int _totalDocuments;

    public void BuildTfIdfIndex(IEnumerable<string> documents)
    {
        _documentFrequency.Clear();
        _totalDocuments = 0;
        
        foreach (var doc in documents)
        {
            _totalDocuments++;
            var terms = Tokenize(doc).Distinct();
            
            foreach (var term in terms)
            {
                _documentFrequency.TryGetValue(term, out var count);
                _documentFrequency[term] = count + 1;
            }
        }
    }

    /// <summary>
    /// Classify term centrality (not epistemic truth):
    /// - High DF (>50%): appears across most chunks = core content
    /// - Medium DF (20-50%): supporting detail
    /// - Low DF (<20%): rare = likely incidental ("colour")
    /// 
    /// Note: This estimates centrality, not factuality. A repeated 
    /// claim can be false; a rare fact can be true.
    /// </summary>

    public ClaimType ClassifyTermImportance(string term)
    {
        var df = _documentFrequency.GetValueOrDefault(term.ToLowerInvariant(), 0);
        
        if (_totalDocuments == 0 || df == 0)
            return ClaimType.Colour;
        
        var documentRatio = (double)df / _totalDocuments;
        
        // High centrality = appears widely
        if (documentRatio > 0.5)
            return ClaimType.Core;
        
        // Medium centrality = supporting themes
        if (documentRatio > 0.2)
            return ClaimType.Supporting;
        
        // Low centrality = incidental detail
        return ClaimType.Colour;
    }
}

De volledige BertRag-pijpleiding

DocSummarizer's productie pijpleiding (BertRagSummarizer) combineert al deze concepten

public class BertRagSummarizer
{
    /// <summary>
    /// Full pipeline: Extract → Retrieve → Synthesize
    /// 
    /// Key properties:
    /// - LLM only at synthesis (no LLM-in-the-loop evaluation)
    /// - Deterministic extraction (reproducible, debuggable)
    /// - Validated citations (every claim traceable to source segment)
    /// - Scales to any document size
    /// - Cost-optimal (cheap CPU work first, expensive LLM last)
    /// </summary>

    public async Task<DocumentSummary> SummarizeAsync(
        string docId,
        string markdown,
        string? focusQuery = null)
    {
        // === Phase 1: Extract ===
        // Parse document → segments with embeddings + salience scores
        var extraction = await _extractor.ExtractAsync(docId, markdown);
        
        // === Phase 2: Retrieve ===
        // Hybrid search: Dense + BM25 + Salience via RRF
        var retrieved = await RetrieveAsync(extraction, focusQuery);
        
        // === Phase 3: Synthesize ===
        // LLM generates fluent summary from retrieved segments
        var summary = await SynthesizeAsync(docId, retrieved, extraction, focusQuery);
        
        return summary;
    }
}

Gemeenmatige Mislukkingsmodi

Als je DocSummarizer bouwt en gebruikt, heb ik deze problemen gehad.

  1. Tokenizer mismatch → nonsensembeddings: Laaien van een WordPiece vocab voor een BPE-geschoold model produceert geldigeM SK2aangeziene, maar semantisch zinloos vektoren . Bevestig altijd dat de tokenizer met het model overeenkomtMSC4s trainingsregimeMスク5

  2. Dominerende-topische vooroordelen in een enkele-centroïde scoring: Met één document centroïd systematisch naar beneden te gebruiken- rangschikt minderheidsthema's ( beperkingen , uitzonderingen

  3. BM25 verslaat dichte zoekopdracht op zeldzame termen: Als je zoekopdracht technische jargon of goede naamwoorden niet goed bevat -representeerd in het inbedingsmodel's traininggegevensM SK3 lexische matching (BMMSC5 zal meer succesvol zijn dan semantische zoekop zoekMska6 Daarom is hybride zoek belangrijkMske7

  4. OCR afval in gescand PDF's: Docling is goed, maar OCR-fouten zijn samengevoegd . Als je onzin in samenvattingen zietM SK3 kijk dan eerst naar de afvaluitvoer van Docling.

  5. Low-coverage samenvattingen moeten taal beschermen.: Als je' alleen 3% van een document ziet, dan zijn zinnen als "in het eindeM SK5 of "in de conclusieMSC7 onjuist.

  6. Citation-hallucinatie: Kleine LLM's (1.5B-3B paramsM SK3 soms vind je plausibele uitvindingenMSC4geluidelijke citatiesMスク5 We bevestigen het door de output te parsen voor [chunk-N], het verifiëren dat N bestaat in de bronstukjes , en het afwijzen of repareren van claims die ontbrekende stukken citeren . Als je ziet [chunk-999] voor een 10-chunk document, je LLM worstelt met de taak

Dit zijn geen bugs, maar inherente spanningen in de ontwerpruimte. Goede productiesystemen herkennen en verlichten ze.

Praktische overwegingen

Omslag en steekproef eerlijkheid

Bij het verwerken van zeer grote documenten, DocSummarizer probeert niet' alles te inbedden M SK2 het gebruikt semantische pre-filtering om vertegenwoordigde segmenten te selecteren . Dit betekent dat de samenvatting gebaseerd is op een voorbeeld, niet het volledige document.

Het systeem manipuleert dit transparant:

// If coverage is low (<5%), prepend disclaimer and use cautious language
if (coverage < 0.05)
{
    var disclaimer = $"WARNING: Summary (sampled ~{coverage:P1} of document)";
    summary = $"{disclaimer}\n\n{CleanAndHedge(summary)}";
}

// Append coverage footer to every summary
var footer = $"\n\n---\nCoverage: {coverage:P1} ({scope})\nConfidence: {confidence}";

Belangrijk: Dit is een samenvatting van verkregen bewijsmateriaal., geen garantie van volledige documentbeperking -documentbeperkning . Als we zeggen "verampte 3%", dat ' precies wat er gebeurde -, zag het systeem SMk7 van het document en samenvatte dat

De steekproef is niet willekeurig. - it 's Semantisch.. We gebruiken multi--anchorclustering om ervoor te zorgen dat minderheidsthema's niet uitgesloten zijn 'M SK3 Een willekeurige 3% kan alle beperkingen en randgevallen verliezen . Een semantice M3% probeert een vertegenwoordigend segment van elk groot thema vast te leggen MSC7 Het is nog steeds gedeeltelijk behandeld, maar het is intentioneel divers.

Aanpasbare steekproefingen met meerdere themaankers: De voor--filter maakt gebruik van meerdere ankeren (k-betekent de-stijlclustering van een ge stratificeerde steekproef,) om ervoor te zorgen dat minderheidsthema's niet systematisch uitgesloten zijn.

Van SegmentExtractor.cs:

// Multi-anchor approach prevents single-centroid bias
var topicAnchors = ComputeTopicAnchors(embeddedSample, k: 5);

// Score by max similarity to ANY anchor (catches minority topics)
var score = topicAnchors.Max(anchor => CosineSimilarity(segment.Embedding, anchor));

Dit is onderzoek.

Waarom niet alles inbedden?

Voor een document van 500- pagina's, segmenten of (2,000+, zou alles inbedden werken, maar is het niet optimal?

  • Kost: OM SK1N) inbedingen domineren de looptijd . Op ~150 segmenten M/sec МSK6 dat 's m13+ seconden alleen voor inbedingstijd voordat je zelfs begint met herhalen.
  • Kwaliteit: Alles inbedden verhoogt het lawaai in de opsporingspool. Je hebt nog steeds een rangorde nodigM SK2 dus waarom segmenten inbeden die nooit hoog zullen staan?
  • Praktiek: Geheugen- en latencybeperkingen zijn belangrijk. Het houden van 2,000 384-dim-vektors in geheugen en berekeningen \2,000 cosinevergelijkingen per zoekopdracht is verspillend als je alleen de bovenste nodig hebt.

De multi--anchor-sampling geeft je het beste van beide onderwerpen.

Kontext trekken: Waarom onttrek niet is' Alleen maar zoeken

Wat ik eerder beschreef, is niet alleen ', maar ook ", herhalen en ", - het is een specifiek patroon dat ik ' noem. Beperkte fuzzy-context-dragen (CFCD). De inzichten:

De meeste samenvatters blijven context toevoegen. DocSummarizer Draagt vooruit. Alleen wat de deterministische selectie overleeft, let het model vloeiend in die grenzen schrijven.

Hier ' zie je hoe de DocSummarizer pijpleiding naar CFCD gaat.

CFCD-concept DocSummarizer Implementatie
Salistieherkenning (fuzzyM SK1 Embeddings, centroide overeenkomstenMska4 TFMske5 IDF-centraliteit mska6
Deterministische promotie MMRMSC1 BM25, RRF fusieM SK3 bovenaanMST4K-selectie MST5
Ankerblad Het geonttrekte segment met citatie-ID's ≥
Beperkte generatie Synthese-prompt omringd door opgenomen bewijs

Waarom dit belangrijk is?: Het model beslist niet wat de retrievingspijplijn doet. Hetmodel genereert alleen vloeiend binnen de grenzen die we hebben ingesteld.

In de praktijk ziet het "anchorleader" er zo uit.

{
  "coverage": "3.2% semantic sample",
  "anchors": [
    { "id": "chunk-12", "text": "Reset requires holding button 10s", "salience": 0.92 },
    { "id": "chunk-45", "text": "Factory reset clears all settings", "salience": 0.88 }
  ],
  "constraints": {
    "terms": { "factory reset": "restore factory settings" },
    "hedging": "sampled 3% - avoid definitive conclusions"
  }
}

Dan in de synthese prompt:

  • "Elk claim moet een inbegrepen stuk Id citeren"
  • "Als de omvang ≥< 5%, hedge-taal"
  • "Gebruik deze termen consistently"

Dit is waarom:

  • Langere contextvensters zijn een rode herring. "-, het probleem is: ', niet meer tekst passend, ,, het beslist wat het waard is om te overleven.
  • Recursieve samenvatting faalt - het behandelt de tussenuitgang als tekst , geen beperkte structuur
  • De LLM kan de beperkingen promoteren. -, ze, ', de structuur van ,, geen prose die eruit kan drijven.

CFCD is dezelfde filosofische splitsing als Beperkte duisternis, Beperkte fuzzy MoM, en Beeldsomvattender - Waarschijnlijkheid voorstelt, Determinisme blijft bestaan.

Kwantisering

OnNX-modellen kunnen kwantificeerd worden (reduceerde precisie) voor kleinere grootte en snellere conclusiesM SK2 Het handelen - is een minimale kwaliteitsverlies

Modell Volgrote Precisie Kwantitair Kwaliteitsverschillen
alle -MiniLM-LM SK3vMSC4 90MB
bge-small -enM SK3vMSC4 133MB

Batchverwerking en concurrentie

Voor grote documenten is batchembedding cruciaal voor de prestatie. ONNX Runtime's InferenceSession Het kan in het algemeen veilig over threads worden gedeeld, maar de prestaties hangen af van de sessieopstelling:

public async Task<float[][]> EmbedBatchAsync(IEnumerable<string> texts, CancellationToken ct)
{
    var textList = texts.ToList();
    var results = new float[textList.Count][];
    
    // InferenceSession is safe to share for inference in most cases
    // Tune SessionOptions.IntraOpNumThreads and InterOpNumThreads for your workload
    var maxParallel = Math.Min(Environment.ProcessorCount, 8);
    
    await Parallel.ForEachAsync(
        textList.Select((text, index) => (text, index)),
        new ParallelOptions { MaxDegreeOfParallelism = maxParallel },
        async (item, token) =>
        {
            results[item.index] = await EmbedSingleAsync(item.text, token);
        });
    
    return results;
}

Performance-tip: Opstel SessionOptions bij het creëren van de sessie:

var sessionOptions = new SessionOptions
{
    IntraOpNumThreads = 4,  // Threads within a single operation
    InterOpNumThreads = 2   // Threads across operations
};
var session = new InferenceSession(modelPath, sessionOptions);

Geheuebeheersing voor grote documenten

Vroege grote documenten (novels, juridische documentenM SK2 vereist speciale manipulatie om te voorkomen.

// For documents > MaxSegmentsToEmbed, use hierarchical extraction
if (segments.Count > _config.MaxSegmentsToEmbed)
{
    // Process in batches, keeping only top-K per batch
    // Then re-rank globally
    return await ExtractHierarchicalAsync(segments);
}

Werkbaarheidskenmerken

Echte performance op een typische ontwikkelaarmachine.

Operatie Snelheidsbreedte ♫ ♫ Notas ♫
Inbedden ~150 segmenten/sec
Digt onttreken
BM25 scoring <5ms SMK2 In-geheugenomgekeerde index MMK4
RRF fusie
Eind-to-uitgang M SK2pagina PDF )

Toetsomgeving: Ryzen 5600X (6-core), M32GB RAMM SK5 geen GPUMSC6 Embedding maakt gebruik van allesMST7MiniLM-LMST9vM ST10M stst11quantiseerdMSt12Mst13tokenmaxMstr14Mt15draadparallele batchingM St16Retrieval corpusMSST17MsST18segmentenM SST19 Je mileage varieert met verschillende modellenMSS20HardwareMTS21 en documentcomplexiteitM SS22

De belangrijkste drywer inbedt de doorvoer. (modelkeuze + tokenlengte + stapelgrootte). Onttreken en fusie zijn in principe gratis ♫- ze kosten milliseconden ♫ . Dit versterkt het ♫

Skaleren: De hiërarchische extractie verwerkt 500+ paginadocumenten (novels, handleidingenM SK4 door in lotjes te verwerken en alleen bovenaan te houdenMST5K per lot in geheugenMst6

Opsomming

DocSummarizer toont aan dat geavanceerde NLP-capaciteiten geen cloud API's of Python-afhangen nodig hebben. Met ONNX Runtime voor ingebouwingen en Ollama voor de generatie, kun je een volledige RAG-pijplijn bouwen.

  • Loop volledig lokaal
  • Produceert traceerbare, aangehaalde samenvattingen
  • Handt documenten van elke grootte.
  • Werkt offline

De belangrijkste inzichten uit het bouwen van dit gereedschap:

  1. Embeddings zijn de basis. - Goede terugvinding hangt af van goede inbouwingen
  2. Hybride zoekopdracht beatt ofwel alleen. - Combineer semantisch en lexisch voor robuustheid
  3. MMR voorkomt herhaling - Diversiteit is net zo belangrijk als belang
  4. Structuurkwesties - De documentstructuur respecteert.
  5. LLMs zouden de laatste moeten zijn. - Doe goedkope CPU-werk eerst, dure LLM-werk alleen op gefilterde inhoud

Volgende lees

Papieren en technische referenties

Verwante Deep Dives

  • Hoe neural Machine Translation werkt - Bevat transformatorarchitektuur, aandachtsmechanismen , en ingebouwingen vanuit een vertalingsperspectief velen van dezelfde concepten passen toe op documentverstandenM SK4

Opvouw de reeks

Dit is het resultaat van de DocSummarizer-serie. Hier' zie je hoe de drie onderdelen samen passenM SK2

Deel 1 Ze legt uit hoe het werkt. Waarom? De pijplijn aanpak overkomt naïeve LLM-roepen. Het omvat de architectuurpatronen (chunkingM SK2 hiërarchische vermindering , citatie-validatie

Deel 2 is je snelle-startgids~.InstallatieM SK2modusenMST3templates MST4common-use casesMst5~ Als je alleen maar het gereedschap wil gebruiken, dan is dat alles wat je nodig hebt.

Deel 3 ( dit artikel ) is de diepe duik voor mensen die willen begrijpen Hoe? Het werkt echt: BERT tegen zinstransformatoren, waarom ONNX belangrijk is , tokenisatie gokkenMSC3 hybride zoekhandelM SK4 afwijkingenMST5 en wat breukt in de productieMst6

Als je je eigen pijpleiding bouwt, lees alle drie.

Verwant

logo

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