Processing math: 100%

Word Embedding e operazioni sullo Spazio Vettoriale

Abbiamo visto come attraverso modelli di Latent Semantic Analysis , iterativi/deep learning (Word2Vec) o ibridi (GloVe), possiamo ricavare una rappresentazione delle parole presenti in un corpus mediante word embedding, ovvero vettori xRn  le cui componenti sono intrinsecamente portatrici di specifici significati.
Ciò ci permette di operare sugli embedding con gli strumenti propri dell'algebra vettoriale per ottenere risultati semanticamente rilevanti in ambito di analogie, similitudini e in tutti quegli ambiti dove sia utile sfruttare le relazioni tra le parole in termini di significato.

Vediamo nell'esempio che per semplicità utilizza degli embedding in 2 dimensioni, come possiamo utilizzare le relazioni tra rappresentazioni di parole note per estrarre componenti di significato ed individuare altre parole correlate.

regalità=xrexuomo   individua una componente che rappresenta il concetto di regalità. 
Applicando questa componente alla rappresentazione nota di "donna"  otterremo un nuovo embedding che ci attendiamo essere molto vicino a quello di "regina"
xreginaxdonna+regalità     

Procedendo analogamente possiamo estrarre la componente di genere genere=xdonnaxuomo   ed applicarla alla rappresentazione di "re" per ottenere un embedding che ci attendiamo essere (il più) vicino a quello di  xreginaxre+genere

Operando linearmente sullo spazio vettoriale degli embedding è ad esempio possibile strutturare semplici sistemi di Q&A sfruttando  analogie e similitudini.
Un esempio classico è l'individuazione delle capitali, partendo dalle rappresentazioni note di una nazione e della relativa capitale: 
capitale=xRomaxItalia   rappresenterà la componente "capitale" che applicata ad una qualsiasi nazione potrà approssimare al meglio la della relativa capitale.
Quindi se calcoliamo y=xFrancia+capitale   e ricerchiamo tra gli embedding del nostro dizionario V quello con distanza minore da y , otterremo auspicabilmente xParigi   
E' importante rimarcare che non andremo ad operare in termini esatti, in quanto la determinazione stessa degli embedding è un processo di modellizzazione che tende verso la migliore rappresentazione in relazione al metodo adottato, alla dimensione degli embedding e alle caratteristiche qualitative/quantitative del corpus di training.

Traduzione come Trasformazione lineare tra Spazi Vettoriali

Immaginiamo di avere due differenti insiemi di word embedding, uno per la lingua A dove ogni parola nel corpus è rappresentata da un vettore   xRn ,  e uno per la lingua B dove ogni parola nel corpus è rappresentata da un vettore yRm .

Visto che gli word embedding sono una rappresentazione semantica delle parole, risulta corretto attenderci che le relazioni lineari già viste tra parole che rappresentano lo stesso significato nelle due lingue rimangano valide in entrambi gli spazi. Riprendendo l'esempio e considerando per comodità di visualizzazione n=m=2 

Vediamo che, a prescindere dalle componenti dei singoli vettori, nelle 2 lingue continueranno a valere le relazioni tra i termini corrispondenti e quindi:

xreginaxrexuomo+xdonnayqueenykingyman+ywoman

Pertanto, tale linearità deve preservarsi nell'operazione di traduzione, che consisterà nell'associare ad un vettore xRn nella lingua A un vettore ˆyRm il più possibile vicino alla esatta traduzione yRm .  
E' immediato verificare, considerando i vettori riga per comodità, che tale operazione è soddisfatta da una trasformazione lineare attraverso la moltiplicazione per una opportuna matrice R(n×m)   .
Ipotizzando di avere già individuato una matrice R ottimale tale che xreRyking , xuomoRyman , xdonnaRywoman  , otteniamo dalle relazioni cui sopra:

xreginaR(xrexuomo+xdonna)RxreginaRxreRxuomoR+xdonnaRxreginaRykingyman+ywoman       

pertanto dalle relazioni di partenza  yqueenxreginaR   , ovvero siamo in grado di individuare l'embedding yRm  che rappresenta la migliore corrispondenza nella lingua di destinazione dell'embedding xRn nella lingua di partenza, il tutto attraverso una matrice R(n×m)  costruita in modo da meglio approssimare la trasformazione tra un certo numero di embedding noti nelle due lingue.

Calcolo della matrice R

Per calcolare la matrice R è necessario disporre di k parole nella lingua A di cui sia nota la traduzione nella lingua B, ovviamente con i relativi embedding  x1xk e  i corrispondenti y1yk
La matrice R(n×m) dovrà essere calcolata in modo da minimizzare complessivamente l'errore tra i vari  xiR e yi .  Disponendo i vettori in 2 matrici:

X=[x1xk](k×n)  ,  Y=[y1yk](k×m)     e pertanto possiamo definire il problema come XRY

Una delle metriche più utilizzate per il calcolo di distanze tra matrici è la norma di Frobenius ||A||F=ija2ij  che ci permette di impostare il problema come

argminR||XRY||2F   , ovvero come ricerca del minimo della funzione di costo rispetto all'incognita R 

Il metodo storicamente più utilizzato per la risoluzione numerica di problemi di minimo è l'algoritmo della discesa del gradiente : dopo aver inizializzato la matrice R a 0 o con valori casuali, si procede ad applicare le regole del calcolo differenziale matriciale e calcolare il gradiente rispetto alla matrice R come  dR=2X(XRY)    

A questo punto si procederà ad aggiornare la nostra matrice R=Rα dR  utilizzando un opportuno coefficiente di apprendimento α .
Ripeteremo iterativamente il calcolo del gradiente e l'aggiornamento della matrice, sino a quando il valore della funzione di costo si assesterà attorno a quello che identificheremo come un minimo.

In alternativa alla risoluzione per via numerica è ovviamente possibile anche la risoluzione analitica del problema di minimo ponendo il gradiente X(XRY)=0  , da cui   XXR=XY  e infine R=(XX)1XY
Sebbene il calcolo di una matrice inversa di dimensione n×n possa rilevarsi rilevante al crescere di n , per le attuali capacità computazionali e le dimensioni degli embedding correntemente utilizzate inferiori a 1000 componenti, si ritiene generalmente preferibile procedere direttamente con questa soluzione esatta.

A questo punto avremo individuato la nostra migliore matrice di traduzione R che potremo utilizzare per ottenere un vettore  ˆy=Rx a partire dall'embedding x di una qualunque parola nella lingua A,  e successivamente individuare l'embedding più vicino yˆy nella lingua B corrispondente alla traduzione più probabile della parola.

Distanza e hashing degli embedding

L'individuazione nel nostro dizionario dell'embedding più vicino al vettore ˆyRm  può essere computazionalmente onerosa, a seconda della dimensione |V| del dizionario stesso, potenzialmente nell'ordine dei 105~10elementi.

La metrica generalmente più efficace in queste applicazioni non è quella della distanza euclidea ma il coseno di similarità:
d(v,w)=cos(^vw)=vw||v|| ||w||    , che quindi attribuisce rilevanza alla direzione e verso degli embedding e non al modulo, meno significativo ai fini del contributo semantico, specie in spazi ad alta dimensionalità come quelli degli embedding.

Una tecnica utilizzata frequentemente per ottimizzare i tempi di ricerca è quella dell'hashing degli embedding, che nel caso della distanza vista può essere realizzata dividendo lo spazio Rm in 2p  parti attraverso p iperpiani orientati di dimensione m1 passanti per l'origine, identificati dai loro vettori normali niRm .
La generazione dei vettori normali può essere casuale, pseudocasuale o seguire algoritmi precisi in relazione alla conoscenza dell'insieme degli embedding - possiamo assumere una generazione casuale senza perdere di generalità. 

L'attribuzione di un vettore v alla una delle parti dello spazio separate da un iperpiano con normale n può essere codificata come 0 od 1  ponendo  hi=min(0,sign(vni)) .

E' quindi possibile generare un hash h=pi=1hi 2i1  che identifichi univocamente la zona di appartenza del vettore nello spazio partizionato dagli iperpiani.

Pertanto, per trovare l'embedding più vicino ad un dato vettore, sarà opportuno misurarne la distanza dagli embedding che hanno stesso hash, senza confrontarlo con l'intero insieme degli embedding.

Tuttavia si evidenzia che questo non è un metodo esatto, non assicurando che l'embedding più vicino a v nel relativo bucket sia l'embedding più vicino in assoluto: il vettore v potrebbe ad esempio trovarsi vicino alla frontiera delimitata da un iperpiano oltre il quale si trova un embedding ancora più vicino.
Per ovviare a questo si procede solitamente a generare k insiemi distinti di p iperpiani, e quindi k hash per ogni embedding. 
Si andrà quindi a confrontare la distanza tra il vettore e tutti gli embedding nei k bucket,  aumentando sensibilmente la probabilità di individuare l'embedding più vicino in assoluto. 

Limiti e osservazioni

L'osservazione preliminare è che il metodo esposto opera in uno scenario ideale e circoscritto di una traduzione parola per parola, nel quale si cerca di associare una parola della lingua B ad una parola nella lingua A, alla stregua di un dizionario.
Sebbene sia possibile con alcuni accorgimenti utilizzare anche embedding composti da più parole per rappresentare contesti più articolati, l'utilizzo di questo approccio per la traduzione di intere frasi risulta scarsamente efficiente.
E' inoltre di limitata efficacia per la traduzione di parole che presentano molteplici significati a seconda del contesto, o tra lingue con costruzioni concettuali/semantiche estremamente diverse tra loro. Pertanto la facilità di training del sistema va valutata alla luce dello specifico obiettivo che si vuole ottenere e dell'accuratezza reputata accettabile.

E' bene ricordare che è sempre rilevante, in ogni applicazione che faccia uso di più embedding per traduzioni e mappature, utilizzare per quanto possibile degli embedding addestrati con un metodo uniforme e ricavati da corpus omogenei per estensione, tematiche, contesti (es: pagine di wikipedia , news, etc.) , in modo da avere una ragionevole confidenza nel fatto che siano rappresentati quantitativamente e qualitativamente embedding riconducibili allo stesso universo. 

E' infine opportuno fornire al modello quanti più embedding di corrispondenze note per costruire le matrici X e Y, in modo da imporre più punti di riferimento ed ottenere una matrice R efficace in grado di trasporre al meglio tutte le varie componenti di significato,  considerando anche le consuete dimensioni m,n>200 dei vettori in gioco.

Una delle applicazioni della NLP che da decenni ci assiste in ogni software di elaborazione testi, e in generale nella digitazione sui dispositivi mobili, è l'autocorrect.

Sebbene i sistemi attuali vadano ben oltre la semplice soluzione presentata e utilizzino ampiamente tecniche predittive legate al contesto della frase nella quale è effettuata la correzione, questo metodo su base probabilistica e di immediata implementazione può risultare un compromesso accettabile per un buon numero di applicazioni.

Rilevazione dell'errore

Il trigger del meccanismo di autocorrect deve ovviamente essere la rilevazione di un errore.
Il più elementare dei criteri per decidere se un token sia o meno da considerarsi un errore di digitazione, è quello di verificarne la presenza tra i |V| termini di un corpus di riferimento.

E' chiaramente un approccio diretto che può essere integrato con accorgimenti di preprocessing, specie per corpus di riferimento non particolarmente estesi.

Generazione di potenziali alternative

Rilevato l'errore, possiamo generare potenziali alternative in modo iterativo, in particolare tra quelle che abbiano una distanza di edit pari ad 1 dal token digitato.  

Con distanza di edit indichiamo il numero minimo di operazioni (inserimenti, cancellazioni, sostituzioni) necessarie per trasformare un testo di partenza in uno di destinazione.

Supponendo di attribuire peso unitario ai 3 tipi di operazioni, le potenziali alternative generabili iterativamente a distanza 1 per una parola formata da n caratteri da un alfabeto di |c| caratteri ammissibili sono (2n+1)×|c| , ovvero:
- inserimento:  si generano (n+1)×|c| potenziali parole, inserendo 1 carattere in una delle n+1 possibili posizioni
- cancellazione:  si generano n potenziali parole, rimuovedo 1 carattere da una delle n possibili posizioni
- sostituzione:  si generano n×(|c|1) potenziali parole, sostituendo 1 carattere in una delle n possibili posizioni

Validazione delle potenziali alternative e attribuzione probabilità

Chiaramente solo una ridotta frazione delle potenziali alternative risulteranno corrispondenti a parole ammissibili  wkV, ed è proprio tra queste che andremo a selezionare quella con maggiore probabilità, calcolata come frequenza relativa  P(wk)=freq (wk)|V|  nel corpus di riferimento - oppure proponendo all'utente la scelta tra un numero k di alternative ammissibili in ordine discendente di probabilità.

Distanza di Levenshtein

La distanza di edit sopra utilizzata, nota anche come distanza di Levenshtein, ci offre una misura della distanza tra 2 sequenze arbitrarie di caratteri in termini di operazioni necessarie per trasformare una sequenza nell'altra.

Sebbene la verifica e la generazione di sequenze a distanza unitaria sia pressochè immediata, il calcolo della distanza relativa a sequenze arbitrarie può risultare tutt'altro che intuitivo.

A tal fine si utilizza l'approccio della programmazione dinamica scomponendo il problema in sottoproblemi dei quali trovare in modo sistematico la migliore soluzione.  

Nel caso della distanza di Levenshtein andiamo a creare una tabella che conterrà la distanza tra sottostringhe delle sequenze di partenza e di destinazione. 

Ipotizziamo ad esempio di voler calcolare la distanza tra "avere" e "neve": collochiamo innanzitutto la stringa di destinazione sopra la prima riga e la stringa di partenza a sinistra della  prima colonna, anteponendo il carattere "#" a rappresentare la sottostringa vuota.

#neve#avere

Andiamo quindi a calcolare le distanze, iniziando dalla cella di coordinate (1,1) corrispondente ad una stringa di partenza nulla "#" e a una stringa di destinazione nulla "#",  In ragione dell'identità la distanza è pari a 0.

In generale per ogni cella della prima riga (1,i), la distanza sarà pari proprio ad i, infatti sarà necessario aggiungere i caratteri alla stringa vuota # di partenza per giungere alla stringa di destinazione. Analogamente per ogni cella della prima colonna (j,1), la distanza sarà pari proprio ad j, infatti sarà necessario eliminare caratteri alla substringa di partenza per giungere alla stringa vuota # di destinazione.

#neve#01234a1v2e3r4e5

Per una generica cella (i,j) per la quale siano noti sia i valori della cella della colonna precedente (i,j-1), della cella della riga precedente (i-1,j) , della cella precedente sulla diagonale (i-1,j-1)  è possibile trovare la distanza ipotizzando che la substringa di destinazione sia ottenuta in uno dei tre modi possibili:
- aggiungendo un carattere alla substringa di destinazione della colonna precedente, per cui la distanza sarà  d(i,j1)+1

- eliminando un carattere rispetto alla substringa di partenza della riga precedente, per cui la distanza sarà  d(i1,j)+1

- (eventualmente) sostituendo un carattere rispetto allo stato della riga e della colonna precedente (i-1,j-1):
 in particolare se il carattere della substringa di partenza in posizione è uguale al carattere della substringa di partenza in posizione significa che la distanza sarà proprio d(i1,j1) in quanto il nuovo carattere è aggiunto senza  sostituzione e senza alcun ulteriore costo di "edit";
se invece il carattere è diverso significa che la distanza sarà d(i1,j1)+1  in quanto il nuovo carattere della stringa di destinazione è ottenuto sostituendo quello della stringa di partenza;

Ovviamente trattandosi di distanza si selezionerà il minore dei 3 possibili costi.  In sintesi:

d(i,j)=min{d(i1,j)+1d(i,j1)+1d(i1,j1)+{0 se partenza(i)=destinazione(j)1 se partenza(i)destinazione(j)

che applicata in modo iterativo ci consente di completare la tabella e di ottenere la distanza tra le due stringhe nell'ultima cella, come distanza tra la stringa di partenza completa e la stringa di destinazione completa.
Nel caso in esame la distanza di Levenshtein è 3:

#neve#01234a11234v22223e33232r44333e55443

Come già accennato, si segnala che è possibile assegnare un costo non unitario e diverso per i 3  tipi di operazioni di inserimento, eliminazione e cancellazione,  anche per adattarsi al contesto della specifica applicazione nella quale viene utilizzata la distanza di edit.

©2023 - me@enricogiannini.com -