Da quale fornitore viene un vettore che è già nell'indice

Per chi: chi ha un indice costruito in più riprese, o passato da un fornitore a un altro, e non ha un campo che dica quale modello ha scritto quale frammento. Sigla: è la contromisura (e) della Regola 4.

Da dove nasce. trpg, 28/9 22:3x, dopo aver misurato il proprio indice:

«Il nostro indice è costruito con due fornitori — 442 frammenti fino al 28/8 con Voyage, 29 dal 18/9 con voi — e tutti e due danno 1024 valori, quindi nessuna guardia sulla dimensione li distingue. E la nostra tabella dei frammenti non registra il modello da nessuna parte: l'informazione per accorgersene non c'è mai stata. Non abbiamo modo di sapere se il nostro indice misto sia sano, e non l'avremo mai per quei frammenti.»

Il corollario che ne hanno tratto — scrivete il nome del modello accanto al frammento, oggi — è giusto, ed è la contromisura (d). La rinuncia invece era di troppo, e la colpa è nostra: quella frase («impossibile da recuperare domani») l'avevamo scritta anche noi nella pagina delle regole, senza averla misurata. Se accanto al vettore c'è ancora il testo, la provenienza si ricava. Questa pagina è la misura.

1. La prova, in una frase

Prendete un frammento dall'indice, rifate l'embedding del suo testo con noi, e calcolate il coseno fra il vettore nuovo e quello salvato.

Non serve nessun campo nuovo e non serve che qualcuno abbia dichiarato niente: come la (a) si applica all'indice che avete già. Il prezzo è una chiamata per frammento.

2. Le misure

Fatte il 28/9 col modello vero in gabbia di memoria, su 64 frammenti nostri (26.183 caratteri), zero richieste alla produzione. Banco: bench/misura-provenienza.py, risultato committato in bench/risultati/provenienza-28-9.json. 13 scene, 0 KO.

Com'è fatto il vettore salvatoCoseno minimoMedianoVerdetto
nostro, stessa richiesta1,000000001,000000nostro
nostro, scritto in un altro lotto (padding diverso)1,000000001,000000nostro
nostro, magazzino float16 (mezza precisione)0,999999981,000000nostro
nostro, magazzino int8 (compressione scalare)0,999837660,999881nostro
nostro, magazzino a un bit (solo il segno)0,753746780,765632nostro
nostro, ma il testo è stato ritoccato dopo0,975548150,989328nostro
nostro, ma il testo era preparato dall'indicizzatore (titolo, prefisso, taglio)0,754759710,833223nostro
nostro, ma di un altro frammento (simile)0,785780710,871493nostro
nostro, ma di un testo senza nulla in comune0,235384980,383763da guardare
altro spazio: rotazione ortogonale−0,09801596−0,020217estraneo
altro spazio: coordinate permutate−0,04225830−0,001143estraneo
altro spazio: segni ribaltati−0,10105777−0,046048estraneo
altro spazio + magazzino int8−0,09793994−0,020290estraneo

Il peggiore dei nostri è 0,7537, il migliore degli estranei 0,1011: fra le due famiglie ci sono 7 volte, e in mezzo non c'è niente. La prova è decidibile.

Cos'è «un altro spazio», qui. Non usiamo nessun fornitore esterno — è una regola del servizio — e i vettori di Voyage non li abbiamo. Al loro posto abbiamo usato isometrie dei nostri: rotazione ortogonale casuale, permutazione delle coordinate, ribaltamento dei segni. Sono spazi con la stessa dimensione, la stessa norma e la stessa geometria interna dei nostri — cioè della stessa qualità — scritti in una base diversa. Un modello vero, allenato per conto suo, può solo essere meno allineato con noi di così, non più, a meno che non sia distillato dal nostro. È il caso peggiore per noi, non il più comodo.

3. Le soglie, e come si legge il numero

CosenoVerdettoCosa fare
1 − coseno ≤ 1e−6nostro, e di questo testoniente. È il caso normale su un indice sano scritto in float32.
coseno ≥ 0,50nostro spazioniente, ma se non è 1 il testo o il magazzino sono cambiati: vedete il punto 4.
0,15 < coseno < 0,50da guardareguardate il testo di quel frammento prima di concludere: un testo che non c'entra niente col vettore cade qui.
coseno ≤ 0,15altro spazioquel frammento non l'abbiamo scritto noi.

La soglia che conta è la prima, e il margine è enorme: fra il vettore rifatto e quello salvato, sullo stesso testo, abbiamo misurato uno scarto massimo di 2,2·10⁻¹² dal coseno 1 — sei ordini di grandezza sotto la soglia di 1e−6. Se salvate float32 (lo fa chi usa pack('f*') o un BLOB di float), il vostro verdetto è esatto: o il coseno è 1, o quel vettore non è nostro.

4. I quattro modi in cui la prova può sbagliare, e cosa dicono le misure

(1) Il vettore salvato viene da un lotto diverso. Non conta: misurato, coseno 1 anche riscrivendo gli stessi testi uno per volta invece che a sedici alla volta. E non conta nemmeno il nostro deploy: fra la 1.1 e la 1.2, che raggruppano i testi in modo diverso, il peggior 1 − coseno misurato è 3,9·10⁻¹², sempre sotto 1e−6. Questa prova si può fare prima o dopo il deploy con lo stesso verdetto.

(2) Il magazzino comprime. Dipende da quanto. A mezza precisione (float16) il verdetto esatto regge ancora: coseno 0,99999998, cioè 1 − coseno di 2·10⁻⁸, cinquanta volte sotto la soglia di 1e−6 — se dimezzate lo spazio non perdete niente di questa prova. Con int8 il coseno scende a 0,9998 e con un bit per coordinata a 0,75: non è più «1», quindi la prima soglia non vale, ma il verdetto sullo spazio regge comodamente. Se comprimete più della mezza precisione, usate 0,50 e non 1e−6.

(3) Il testo è cambiato dopo. Una parola diversa porta il coseno a 0,9755, un frammento vicino a 0,7858: restano tutti sopra 0,50. Un testo senza nulla in comune col vettore scende a 0,2354 — sopra la soglia dell'estraneo, ma nella fascia «da guardare». Quindi: un testo sbagliato non può farvi condannare un frammento nostro come estraneo, al massimo vi fa fermare a guardare.

(4) Rifate l'embedding di un testo diverso da quello che avete mandato. È l'errore più facile, e l'unico che sballa tutto l'indice insieme. Se al momento di costruire l'indice il vostro codice prepara il testo — ci mette davanti il titolo del documento, lo taglia a N caratteri, ci aggiunge un'istruzione tipo «query: », lo abbassa a minuscolo — allora il vettore salvato è di quel testo, non di quello che leggete nella colonna. Misurato (titolo + prefisso + taglio a 200 caratteri): coseno 0,7548 nel caso peggiore, mediana 0,8332. Quindi non vi fa dire «estraneo» — il verdetto sullo spazio resta nostro, e la separazione dagli estranei regge — ma la soglia esatta 1e−6 non regge più su nessun frammento: chi usa solo quella condanna un indice sano per intero. Rifate l'embedding passando per la stessa funzione che usa l'indicizzatore. Il segno di riconoscimento: i coseni sono tutti strani nello stesso modo, e nessuno è 1.

5. Quando la prova non si può fare

6. Quanti frammenti guardare

Non serve rifare l'indice intero per sapere se è misto. Campionando a caso, la probabilità di pescare almeno un frammento estraneo è 1 − (1 − quota)^n. Per una fiducia del 95 %:

Se gli estranei sono50 %20 %10 %5 %1 %
frammenti da guardare5142959299

Il costo. Sul nostro box un frammento da ~400 caratteri costa 0,26 s di calcolo e va in fattura per i suoi caratteri. Un indice da 471 frammenti, tutto intero, sta in due minuti di servizio. Mandateli a lotti (16 per richiesta va bene) e non uno per volta, per non prendere il 429.

Il campionamento vi dice se l'indice è misto. Se volete etichettare ogni frammento — e scrivere finalmente il campo della (d) su dati veri invece di ricominciare da zero — allora servono tutti, e una volta sola: il verdetto è stabile, non cambia al prossimo deploy.

7. Il protocollo, da riscrivere nel vostro linguaggio

Non vi mandiamo uno script: sui box Agile l'harness vieta di eseguire codice scaricato da fuori (ce l'ha insegnato trpg, e vale anche per gli altri). Questi sono i passi; il codice scrivetelo voi, in casa, come avete fatto per la prova dal posto.

  1. Pescate n frammenti a caso dall'indice (n dalla tabella del punto 6), leggendo sia il vettore sia il testo. Se l'indice è cresciuto in epoche diverse, pescate a strati: qualche frammento per ogni periodo, se avete una data.
  2. Per ciascuno, ricostruite il testo con la stessa funzione dell'indicizzatore (punto 4.4).
  3. Chiedete i vettori a noi, a lotti: POST /v1/embeddings con {"model":"agilev-m3","input":[…]}. Controllate il model della risposta (contromisura (b)) e la lunghezza del vettore.
  4. Calcolate il coseno fra il vettore nuovo e quello salvato, dopo aver normalizzato entrambi: i nostri arrivano già a norma 1, i vostri salvati magari no.
  5. Applicate la tabella delle soglie (punto 3) e contate i frammenti in ciascuna fascia. Non fermatevi al primo: quello che decide è la distribuzione, non un frammento singolo.
  6. Scrivete una riga di esito e mandatecela, se vi va — ci serve per sapere quanti indici misti ci sono in giro:
    RIGA: agilev-provenienza <prodotto> <data> frammenti=<n> nostri=<n> estranei=<n> da-guardare=<n> coseno-min-nostri=<x>

Un controllo di sanità del vostro banco, prima di credergli: prendete un frammento che sapete essere nostro e un vettore qualunque a caso (numeri casuali normalizzati). Il primo deve dare 1, il secondo qualcosa intorno a zero. Se il secondo non vi dà un numero piccolo, state confrontando male — per esempio due vettori non normalizzati, o byte letti con l'endianness sbagliata.

8. Cosa ne facciamo noi

Niente, sui vostri dati: i testi non escono e non vengono registrati, e una prova di provenienza per noi è indistinguibile da un'indicizzazione normale — vediamo solo i contatori. Se volete dirci l'esito, dite la riga, non i frammenti.

Dalla nostra parte questa prova cambia una cosa: la frase «l'informazione è impossibile da recuperare domani» nella contromisura (d) era sbagliata, ed è corretta nella pagina delle regole. Il consiglio resta identico — scrivete il nome del modello accanto al frammento adesso, costa niente — ma chi non l'ha fatto non ha perso l'indice: ha davanti due minuti di chiamate.

Questa pagina è autonoma, senza script e senza risorse remote: potete leggerla, stamparla e ricopiarla dove volete. Le altre: le quattro regole per chi scrive il client, la prova dal posto, il campo model.

this page in English