- Je onderscheidt losse tekstembeddings van gezamenlijke query-passagebeoordeling.
- Je meet wat een reranker wel en niet kan terughalen uit een beperkte kandidatenlijst.
Probeer ook echte tekstvectoren in het embedding-lab.
Een zoekactie vindt een passage over een lamp, maar die passage geeft geen resetinstructie. Alleen woordoverlap is blijkbaar niet genoeg. We willen de gevonden kandidaten daarom opnieuw vergelijken met de vraag.
Een tweestapszoeker (retrieve-and-rerank) begint met een snelle kandidatenlijst. Daarna bekijkt een scorer ieder vraag-passagepaar. Een passage die nooit in de eerste lijst kwam, kan deze tweede stap niet kiezen.
1. Tel woorden én hun volgorde
Een bi-encoder berekent vraag en passage afzonderlijk; corpusvectoren kunnen vooraf klaarstaan. Een cross-encoder verwerkt het paar samen, met interactie tussen hun tokens. Dat vraagt een nieuwe berekening per paar.
Wij bouwen een transparante regel, geen geleerd transformer-model. De regel kijkt eerst welke zoekwoorden aanwezig zijn en daarna welke woorden direct na elkaar staan. Zo kun je iedere score narekenen.
Neem de drie zoekwoorden reset, lamp en veilig. Hun unieke opeenvolgende paren zijn reset→lamp en lamp→veilig. De passage “Reset lamp: niet veilig” bevat alle drie woorden, maar slechts het eerste paar.
Noem de unieke querywoorden Q en de passagewoorden D. P_Q en P_D bevatten de unieke opeenvolgende paren uit de oorspronkelijke tokenvolgorde, vóór het maken van woordsets. C is woorddekking en A paarsdekking.
In die passage is C=3/3=1 en A=1/2=.5. Bij een query van één woord ontbreken paren; A is dan volgens ons contract nul.
q = 'reset lamp veilig'
docs = {
'a': 'De lamp reset veilig.',
'b': 'Reset lamp veilig.',
'c': 'Reset lamp: niet veilig.'
}
for name, text in docs.items():
f = pair_features(q, text)
print(name, f['coverage'],
f['adjacency'])
# a 1.0 0.0
# b 1.0 1.0
# c 1.0 0.5
Tokeniseer met casefold en de Unicode-regex [^\W_]+. De dubbelepunt vormt geen woord; herhaalde woorden tellen voor coverage niet opnieuw. We gebruiken geen stemming of synoniemen.
De gewichten hieronder zijn gekozen oefenbeleid. Woorddekking krijgt .6 en paarsdekking .4.
Voor c geeft dit .6×1+.4×.5=.8. De ontkenning verhindert die hoge score niet: onze regel begrijpt de betekenis niet.
for name, text in docs.items():
f = pair_features(q, text)
score = (.6 * f['coverage']
+ .4 * f['adjacency'])
print(name, round(score, 2))
# a 0.6
# b 1.0
# c 0.8
Schrijf pair_features met beide kenmerken. Beide teksten zijn niet-leeg na strip en maximaal 2000 tekens; de query moet bovendien minstens één regexwoord bevatten. Ongeldig betekent ValueError.
012. Herordenen begint bij de kandidaten
Stel dat retrieval eerst a, daarna c en pas daarna b vindt. Met limit=2 scoor je alleen a en c; b is voor die run onbereikbaar. Met limit=3 kan b wel bovenaan komen.
candidates = ['a', 'c', 'b']
for limit in [2, 3]:
hits = rerank_candidates(
q, docs, candidates, limit)
print([(h['id'], round(
h['score'], 2)) for h in hits])
# [('c', 0.8), ('a', 0.6)]
# [('b', 1.0), ('c', 0.8), ('a', 0.6)]
Schrijf rerank_candidates: neem eerst de kandidaatprefix en sorteer daarna op aflopende score, met ID als alfabetische tie-break. Gebruik uitsluitend unieke bestaande document-IDs. Een lege kandidatenlijst levert geen hits.
Probeer in de visualisatie wat er gebeurt als de kandidaatlimiet groeit. Meer paarberekeningen kunnen een gemiste passage beschikbaar maken, maar een betere einduitkomst blijft afhankelijk van de scorer.
Vraag: reset lamp. B bevat de handmatig gekozen resetinstructie. Laat meer kandidaten toe en bekijk de nieuwe volgorde.
Retrievalvolgorde
- 1 · A
De reset van de lamp staat hier.
Gaat naar de scorer - 2 · C
Reset sensor: houd de knop vijf seconden vast.
Gaat naar de scorer - 3 · B
Reset lamp: houd de knop tien seconden vast.
Buiten de kandidaatgrens
Na paarscores
- A0.60
De reset van de lamp staat hier.
coverage 1.0 · adjacency 0.0 - C0.30
Reset sensor: houd de knop vijf seconden vast.
coverage 0.5 · adjacency 0.0
Score = 0,6 × coverage + 0,4 × adjacency. Deze regel begrijpt geen ontkenning; een hoge score of marge bewijst geen juistheid. Met één kandidaat laten we de margeregel passeren.
3. Beslis apart of je voldoende bewijs hebt
Onze selectie vraagt een minimumscore én een minimummarge ten opzichte van nummer twee. In het laatste voorbeeld heeft b score 1 en c .8: een marge-eis van .1 slaagt, .3 niet. None betekent dat dit beleid zich onthoudt.
print(decide_rerank(hits, .8, .1))
print(decide_rerank(hits, 1, .3))
tie = [dict(id='z', score=.8),
dict(id='a', score=.8)]
print(decide_rerank(tie, .8, 0))
print(decide_rerank(tie, .8, .01))
one = [dict(id='solo', score=.5)]
print(decide_rerank(one, .5, 1))
print(decide_rerank([], .5, 0))
# b
# None
# a
# None
# solo
# None
Bij marge nul mag een gelijkspel; alfabetisch wint a. Bij één kandidaat zetten we gap=1, zodat de margeregel passeert. Dit is een expliciete afspraak, geen bewijs dat één resultaat betrouwbaar is.
Schrijf decide_rerank met unieke IDs en gewone numerieke scores, drempel en marge binnen [0,1]. Kies beleidsgrenzen op dev en bevries ze vóór test. Een grote marge kan nog steeds een overtuigend fout resultaat kiezen.
03Wat neem je mee?
- Een paarscore kan meer informatie gebruiken dan losse vectorovereenkomst.
- De reranker kan uitsluitend binnen de opgehaalde kandidaten kiezen.
- Score, marge en betekenis zijn verschillende zaken; onze regel mist ontkenning.
Hierna houden we bronversie, exacte tekst en verwijzing bij elkaar.