Werkplaats / De werkplaats
In jouw eigen tempo
← RAG
MODULE 01 · LES 02

Vergelijk passages in dezelfde embeddingruimte

Pool tokenvectoren met een masker, normaliseer hun lengte en rangschik passages met cosine similarity.

60 min3 checkpoints
DIT GA JE ONTDEKKEN
  • Je berekent masked mean pooling zonder padding mee te tellen.
  • Je gebruikt cosinussimilariteit en benoemt wat een hoge overeenkomst niet bewijst.

Probeer ook echte tekstvectoren in het embedding-lab.

Twee vragen kunnen hetzelfde bedoelen zonder dezelfde woorden te gebruiken. Een zinsembedding (sentence embedding) zet tekst met een getrainde encoder om in een vector. Een passende encoder kan verwante passages dichter bij een vraag plaatsen.

De oefeningen gebruiken handgemaakte verborgen vectoren om de berekening te begrijpen. Voor echte geleerde vectoren is er het embedding-lab. Model, revisie, tokenizer, pooling en normalisatie moeten bij dezelfde embeddingruimte horen; dezelfde dimensie alleen maakt encoders niet compatibel.

1. Middel tokenvectoren zonder padding

In de les embedding-tabellen kreeg masked_mean een tabel, IDs en padding_idx. Hier heeft die naam een ander contract: je krijgt al verborgen toestanden H met vorm batch×tokens×dimensie en een booleaans masker. Er worden dus geen rijen uit een embeddingtabel opgezocht.

Neem twee echte tokenvectoren [1,2] en [3,4], plus padding [999,999]. De bedoelde som is [4,6], met twee geldige posities. Het gemiddelde wordt [2,3], zonder bijdrage van de padding.

hidden = np.array([[
    [1,2],[3,4],[999,999]]])
mask = np.array([[True,True,False]])
print(hidden.shape)  # (1, 3, 2)
print(mask[:,:,None].shape)  # (1, 3, 1)
print((hidden*mask[:,:,None])
      .sum(axis=1))  # [[4 6]]

De extra laatste as laat hetzelfde masker op alle dimensies van een token werken. Voor één voorbeeld is de gepoolde vector v:

v=∑tmtHt∑tmt

t loopt over tokenposities; m is nul of één en Hₜ is de vector op die positie. Sommeer over tokens en behoud batch en dimensie.

print(masked_mean(hidden,mask))
# [[2. 3.]]

Maak masked_mean met minstens één geldige positie per voorbeeld. Hiddenwaarden zijn reëel, eindig en begrensd tot absolute waarde een miljoen. Het modelcontract bepaalt welke posities geldig zijn: verwijder speciale tokens niet willekeurig.

De modelkaart van all-MiniLM-L6-v2 toont masked mean pooling en daarna normalisatie. Andere encoders kunnen andere pooling of verschillende query-/documentinstructies gebruiken. Dit gemiddelde is daarom geen universele definitie van een tekstembedding.

01
CHECKPOINTMiddel alleen de bedoelde tokenposities

2. Bewaar richting met een eenheidsvector

Een vector [3,4] heeft lengte vijf. Delen door vijf geeft [.6,.8], met dezelfde richting en lengte één. Alle dimensies tellen mee: [1,2,2] heeft lengte drie.

v^=v‖v‖2‖v‖2=∑jvj2
print(normalize_rows([[3,4]]))
# [[0.6 0.8]]
print(np.round(
    normalize_rows([[1,2,2]]),4))
# [[0.3333 0.6667 0.6667]]

Maak normalize_rows met een nieuwe array per matrix en zonder invoermutatie. Een nulvector heeft geen richting; onze oefening weigert ook normen kleiner dan 10⁻¹². Voeg niet stilzwijgend een willekeurige richting toe.

Positief schalen verandert de richting niet. Normalisatie verwijdert wel lengte-informatie. Gebruik haar daarom alleen wanneer ze bij het getrainde scorecontract past.

02
CHECKPOINTEen eenheidsvector bewaart richting

3. Zoek op richting en evalueer relevantie apart

Cosinussimilariteit deelt het inproduct door beide lengtes. Na normalisatie is dat gewoon het inproduct van de twee eenheidsvectoren:

cos(q,d)=q^·d^

q is de vraagvector en d een passagevector. Bij query [1,0,0] scoren dezelfde richting één, een loodrechte richting nul en de tegengestelde richting min één. Exact gelijke scores krijgen alfabetische IDs als tie-break.

x = [[1,0,0],[2,0,0],
     [-1,0,0],[0,1,0]]
ids = ['b','a','c','d']
hits = cosine_search(x,[1,0,0],ids,4)
for hit in hits:
    score = round(hit['score'],1)
    print(hit['id'],score)
# a 1.0
# b 1.0
# d 0.0
# c -1.0

Maak cosine_search met passende dimensies, unieke IDs en een geldige k. Bewaar negatieve scores; bijna gelijke floating-pointwaarden zijn geen exacte ties. De score is geen kans dat de passage waar of relevant is: tegengestelde uitspraken kunnen over hetzelfde onderwerp gaan.

03
CHECKPOINTRangschik op richting, niet op waarheidskans

Wat neem je mee?

  • Pooling en masker bepalen welke tokenvectoren meetellen.
  • Normalisatie bewaart richting en gebruikt alle dimensies.
  • Cosinus rangschikt vectoren; relevante en juiste bronnen vereisen evaluatie.

Hierna sla je de vectoren op en vergelijk je exact met benaderend zoeken.

DE VOLGENDE STAPBouw eerst exact zoeken en meet daarna de benadering
JOUW EXPERIMENT

Probeer het zelf

Python
main.py

Python start zodra jij code uitvoert.

UITVOER

Je uitvoer verschijnt hier.
Alles rekent in je eigen browser.

Jouw checkpoints0 / 3
01
Middel alleen de bedoelde tokenposities

Schrijf masked_mean(hidden,mask) voor 1–32 voorbeelden, 1–128 tokens en 1–512 dimensies. hidden is eindig met |waarde|≤10⁶; mask heeft vorm batch×tokens, echte booleans en minstens één True per rij. Geef batch×dimensie terug; anders ValueError.

Een hint

Vermenigvuldig met mask[:,:,None] en som langs de tokenas. Deel door mask.sum(axis=1)[:,None].

02
Een eenheidsvector bewaart richting

Schrijf normalize_rows(vectors). Geef een 2D-matrix met 1–1000 rijen en 1–512 kolommen, eindige waarden |x|≤10⁶ en elke norm≥10⁻¹². Retourneer een nieuwe genormaliseerde array en verander invoer niet. Anders ValueError.

Een hint

np.linalg.norm(...,axis=1,keepdims=True) houdt de noemer uitgelijnd met iedere rij.

03
Rangschik op richting, niet op waarheidskans

Schrijf cosine_search(vectors,query,ids,k) met een lijst {id,score}. Gebruik normalize_rows voor corpus en query; query is 1D met dezelfde dimensie. IDs is een evenlange lijst unieke niet-lege strings ≤100 tekens. k is exacte int 1–N. Sorteer op aflopende score en dan ID; anders ValueError.

Een hint

Normaliseer query[None,:] en neem vectors @ query. Sorteer op (-score,identifier); laat negatieve scores zichtbaar.

Vastgelopen? Bekijk de voorbeeldoplossing
import numpy as np

def masked_mean(hidden, mask):
    try:
        raw_hidden = np.asarray(hidden)
        if np.iscomplexobj(raw_hidden):
            raise ValueError('Verborgen waarden moeten reëel zijn')
        hidden, mask = np.asarray(raw_hidden, dtype=float), np.asarray(mask)
    except (TypeError, OverflowError) as error:
        raise ValueError('Geen geldige reële toestanden') from error
    if hidden.ndim != 3 or not 1 <= hidden.shape[0] <= 32 or not 1 <= hidden.shape[1] <= 128 or not 1 <= hidden.shape[2] <= 512:
        raise ValueError('Verborgen toestanden hebben vorm batch×tokens×dimensie')
    if not np.isfinite(hidden).all() or np.any(np.abs(hidden) > 1e6) or mask.shape != hidden.shape[:2] or mask.dtype != bool or not mask.any(axis=1).all():
        raise ValueError('Eindige begrensde waarden en minstens één geldig token per rij vereist')
    return (hidden * mask[:, :, None]).sum(axis=1) / mask.sum(axis=1)[:, None]

def normalize_rows(vectors):
    try:
        raw_vectors = np.asarray(vectors)
        if np.iscomplexobj(raw_vectors):
            raise ValueError('Vectorwaarden moeten reëel zijn')
        vectors = np.asarray(raw_vectors, dtype=float)
    except (TypeError, OverflowError) as error:
        raise ValueError('Geen geldige reële vectorinvoer') from error
    if vectors.ndim != 2 or not 1 <= vectors.shape[0] <= 1000 or not 1 <= vectors.shape[1] <= 512 or not np.isfinite(vectors).all() or np.any(np.abs(vectors) > 1e6):
        raise ValueError('Geef een eindige begrensde matrix met maximaal 1000×512 waarden')
    norms = np.linalg.norm(vectors, axis=1, keepdims=True)
    if np.any(norms < 1e-12):
        raise ValueError('Een nulvector heeft geen richting; vrijwel nul wordt hier geweigerd')
    return vectors / norms

def cosine_search(vectors, query, ids, k):
    vectors = normalize_rows(vectors)
    try:
        raw_query = np.asarray(query)
        if np.iscomplexobj(raw_query):
            raise ValueError('Vectorwaarden moeten reëel zijn')
        query = np.asarray(raw_query, dtype=float)
    except (TypeError, OverflowError) as error:
        raise ValueError('Geen geldige reële vectorinvoer') from error
    if query.shape != (vectors.shape[1],):
        raise ValueError('Query en corpus moeten dezelfde dimensie hebben')
    query = normalize_rows(query[None, :])[0]
    if not isinstance(ids, list) or len(ids) != len(vectors) or any(not isinstance(i, str) or not 1 <= len(i) <= 100 for i in ids) or len(set(ids)) != len(ids):
        raise ValueError('Unieke niet-lege IDs vereist')
    if type(k) is not int or not 1 <= k <= len(ids):
        raise ValueError('Kies 1 tot N resultaten')
    scores = vectors @ query
    order = sorted(range(len(ids)), key=lambda i: (-scores[i], ids[i]))[:k]
    return [{'id': ids[i], 'score': float(scores[i])} for i in order]

Voer de code uit en verklaar voor jezelf waarom elke stap werkt.