Mi è stato presentato questo problema circa un anno fa, quando si trattava di cercare le informazioni inserite dall'utente su una piattaforma petrolifera in un database di informazioni varie. L'obiettivo era fare una sorta di ricerca di stringhe fuzzy in grado di identificare la voce del database con gli elementi più comuni.
Parte della ricerca ha riguardato l'implementazione dell'algoritmo a distanza di Levenshtein , che determina quante modifiche devono essere apportate a una stringa o frase per trasformarla in un'altra stringa o frase.
L'implementazione che mi è venuta in mente è stata relativamente semplice e ha comportato un confronto ponderato della lunghezza delle due frasi, il numero di modifiche tra ogni frase e se ogni parola potesse essere trovata nella voce di destinazione.
L'articolo è su un sito privato, quindi farò del mio meglio per aggiungere qui i contenuti pertinenti:
Fuzzy String Matching è il processo per eseguire una stima di tipo umano della somiglianza di due parole o frasi. In molti casi, comporta l'identificazione di parole o frasi che sono più simili tra loro. Questo articolo descrive una soluzione interna al problema della corrispondenza delle stringhe fuzzy e la sua utilità nel risolvere una varietà di problemi che possono permetterci di automatizzare le attività che in precedenza richiedevano un noioso coinvolgimento dell'utente.
introduzione
La necessità di eseguire la corrispondenza fuzzy delle stringhe era originariamente nata durante lo sviluppo dello strumento di convalida del Golfo del Messico. Ciò che esisteva era un database di piattaforme e piattaforme petrolifere note del Golfo del Messico, e le persone che acquistavano un'assicurazione ci avrebbero fornito alcune informazioni mal digitate sui loro beni e dovevamo abbinarli al database delle piattaforme note. Quando venivano fornite pochissime informazioni, il meglio che potevamo fare era affidarci a un sottoscrittore per "riconoscere" quello a cui si riferivano e richiamare le informazioni appropriate. È qui che questa soluzione automatizzata è utile.
Ho trascorso una giornata alla ricerca di metodi di corrispondenza fuzzy delle stringhe e alla fine mi sono imbattuto nell'utilissimo algoritmo di distanza Levenshtein su Wikipedia.
Implementazione
Dopo aver letto la teoria alla base, ho implementato e trovato il modo di ottimizzarlo. Ecco come appare il mio codice in VBA:
'Calculate the Levenshtein Distance between two strings (the number of insertions,
'deletions, and substitutions needed to transform the first string into the second)
Public Function LevenshteinDistance(ByRef S1 As String, ByVal S2 As String) As Long
Dim L1 As Long, L2 As Long, D() As Long 'Length of input strings and distance matrix
Dim i As Long, j As Long, cost As Long 'loop counters and cost of substitution for current letter
Dim cI As Long, cD As Long, cS As Long 'cost of next Insertion, Deletion and Substitution
L1 = Len(S1): L2 = Len(S2)
ReDim D(0 To L1, 0 To L2)
For i = 0 To L1: D(i, 0) = i: Next i
For j = 0 To L2: D(0, j) = j: Next j
For j = 1 To L2
For i = 1 To L1
cost = Abs(StrComp(Mid$(S1, i, 1), Mid$(S2, j, 1), vbTextCompare))
cI = D(i - 1, j) + 1
cD = D(i, j - 1) + 1
cS = D(i - 1, j - 1) + cost
If cI <= cD Then 'Insertion or Substitution
If cI <= cS Then D(i, j) = cI Else D(i, j) = cS
Else 'Deletion or Substitution
If cD <= cS Then D(i, j) = cD Else D(i, j) = cS
End If
Next i
Next j
LevenshteinDistance = D(L1, L2)
End Function
Metrica semplice, veloce e molto utile. Usando questo, ho creato due metriche separate per valutare la somiglianza di due stringhe. Uno che chiamo "valuePhrase" e uno che chiamo "valueWords". valuePhrase è solo la distanza di Levenshtein tra le due frasi e valueWords divide la stringa in singole parole, sulla base di delimitatori come spazi, trattini e qualsiasi altra cosa tu voglia, e confronta ogni parola con un'altra, riassumendo la più breve Distanza di Levenshtein che collega due parole qualsiasi. In sostanza, misura se l'informazione in una "frase" è realmente contenuta in un'altra, proprio come una permutazione in termini di parole. Ho trascorso alcuni giorni come un progetto secondario per trovare il modo più efficiente possibile di dividere una stringa in base ai delimitatori.
valueWords, valuePhrase e funzione Split:
Public Function valuePhrase#(ByRef S1$, ByRef S2$)
valuePhrase = LevenshteinDistance(S1, S2)
End Function
Public Function valueWords#(ByRef S1$, ByRef S2$)
Dim wordsS1$(), wordsS2$()
wordsS1 = SplitMultiDelims(S1, " _-")
wordsS2 = SplitMultiDelims(S2, " _-")
Dim word1%, word2%, thisD#, wordbest#
Dim wordsTotal#
For word1 = LBound(wordsS1) To UBound(wordsS1)
wordbest = Len(S2)
For word2 = LBound(wordsS2) To UBound(wordsS2)
thisD = LevenshteinDistance(wordsS1(word1), wordsS2(word2))
If thisD < wordbest Then wordbest = thisD
If thisD = 0 Then GoTo foundbest
Next word2
foundbest:
wordsTotal = wordsTotal + wordbest
Next word1
valueWords = wordsTotal
End Function
''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
' SplitMultiDelims
' This function splits Text into an array of substrings, each substring
' delimited by any character in DelimChars. Only a single character
' may be a delimiter between two substrings, but DelimChars may
' contain any number of delimiter characters. It returns a single element
' array containing all of text if DelimChars is empty, or a 1 or greater
' element array if the Text is successfully split into substrings.
' If IgnoreConsecutiveDelimiters is true, empty array elements will not occur.
' If Limit greater than 0, the function will only split Text into 'Limit'
' array elements or less. The last element will contain the rest of Text.
''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
Function SplitMultiDelims(ByRef Text As String, ByRef DelimChars As String, _
Optional ByVal IgnoreConsecutiveDelimiters As Boolean = False, _
Optional ByVal Limit As Long = -1) As String()
Dim ElemStart As Long, N As Long, M As Long, Elements As Long
Dim lDelims As Long, lText As Long
Dim Arr() As String
lText = Len(Text)
lDelims = Len(DelimChars)
If lDelims = 0 Or lText = 0 Or Limit = 1 Then
ReDim Arr(0 To 0)
Arr(0) = Text
SplitMultiDelims = Arr
Exit Function
End If
ReDim Arr(0 To IIf(Limit = -1, lText - 1, Limit))
Elements = 0: ElemStart = 1
For N = 1 To lText
If InStr(DelimChars, Mid(Text, N, 1)) Then
Arr(Elements) = Mid(Text, ElemStart, N - ElemStart)
If IgnoreConsecutiveDelimiters Then
If Len(Arr(Elements)) > 0 Then Elements = Elements + 1
Else
Elements = Elements + 1
End If
ElemStart = N + 1
If Elements + 1 = Limit Then Exit For
End If
Next N
'Get the last token terminated by the end of the string into the array
If ElemStart <= lText Then Arr(Elements) = Mid(Text, ElemStart)
'Since the end of string counts as the terminating delimiter, if the last character
'was also a delimiter, we treat the two as consecutive, and so ignore the last elemnent
If IgnoreConsecutiveDelimiters Then If Len(Arr(Elements)) = 0 Then Elements = Elements - 1
ReDim Preserve Arr(0 To Elements) 'Chop off unused array elements
SplitMultiDelims = Arr
End Function
Misure di somiglianza
Utilizzando queste due metriche e una terza che calcola semplicemente la distanza tra due stringhe, ho una serie di variabili che posso eseguire un algoritmo di ottimizzazione per ottenere il maggior numero di corrispondenze. La corrispondenza delle stringhe fuzzy è, di per sé, una scienza fuzzy, e quindi creando metriche linearmente indipendenti per misurare la somiglianza delle stringhe e avendo un insieme noto di stringhe che desideriamo abbinare tra loro, possiamo trovare i parametri che, per i nostri stili specifici di stringhe, dai i migliori risultati di corrispondenza fuzzy.
Inizialmente, l'obiettivo della metrica era avere un valore di ricerca basso per una corrispondenza esatta e aumentare i valori di ricerca per misure sempre più permutate. In un caso poco pratico, questo era abbastanza facile da definire usando una serie di permutazioni ben definite e ingegnerizzare la formula finale in modo tale che avessero risultati di valori di ricerca crescenti come desiderato.
Nello screenshot sopra, ho modificato la mia euristica per inventare qualcosa che mi è sembrato ben ridimensionato alla mia differenza percepita tra il termine di ricerca e il risultato. L'euristica che ho usato Value Phrase
nel foglio di calcolo sopra era =valuePhrase(A2,B2)-0.8*ABS(LEN(B2)-LEN(A2))
. Stavo effettivamente riducendo la penalità della distanza di Levenstein dell'80% della differenza nella lunghezza delle due "frasi". In questo modo, le "frasi" che hanno la stessa lunghezza subiscono la penalità completa, ma le "frasi" che contengono "informazioni aggiuntive" (più lunghe) ma a parte ciò condividono per lo più gli stessi personaggi subiscono una penalità ridotta. Ho usato ilValue Words
funzione così com'è, e quindi la mia SearchVal
euristica finale è stata definita come=MIN(D2,E2)*0.8+MAX(D2,E2)*0.2
- una media ponderata. Qualunque dei due punteggi più bassi è stato pesato l'80% e il 20% del punteggio più alto. Questa era solo un'euristica adatta al mio caso d'uso per ottenere un buon tasso di corrispondenza. Questi pesi sono qualcosa che si potrebbe quindi modificare per ottenere il miglior tasso di corrispondenza con i loro dati di test.
Come puoi vedere, le ultime due metriche, che sono metriche di corrispondenza delle stringhe sfocate, hanno già una tendenza naturale a dare punteggi bassi alle stringhe che devono corrispondere (in diagonale). Questo va molto bene.
Applicazione
Per consentire l'ottimizzazione della corrispondenza fuzzy, peso ogni metrica. Pertanto, ogni applicazione della corrispondenza della stringa fuzzy può ponderare i parametri in modo diverso. La formula che definisce il punteggio finale è semplicemente una combinazione delle metriche e dei loro pesi:
value = Min(phraseWeight*phraseValue, wordsWeight*wordsValue)*minWeight
+ Max(phraseWeight*phraseValue, wordsWeight*wordsValue)*maxWeight
+ lengthWeight*lengthValue
Utilizzando un algoritmo di ottimizzazione (la rete neurale è la migliore qui perché si tratta di un problema discreto e multidimensionale), l'obiettivo è ora quello di massimizzare il numero di corrispondenze. Ho creato una funzione che rileva il numero di corrispondenze corrette di ciascun set tra loro, come si può vedere in questo screenshot finale. Una colonna o riga ottiene un punto se al punteggio più basso viene assegnata la stringa che doveva essere abbinata, e vengono assegnati punti parziali se esiste un pareggio per il punteggio più basso e la corrispondenza corretta è tra le stringhe abbinate legate. L'ho quindi ottimizzato. Puoi vedere che una cella verde è la colonna che meglio corrisponde alla riga corrente e un quadrato blu attorno alla cella è la riga che meglio corrisponde alla colonna corrente. Il punteggio nell'angolo in basso è approssimativamente il numero di partite riuscite e questo è ciò che diciamo per massimizzare il nostro problema di ottimizzazione.
L'algoritmo è stato un successo meraviglioso e i parametri della soluzione dicono molto su questo tipo di problema. Noterai che il punteggio ottimizzato era 44 e il miglior punteggio possibile è 48. Le 5 colonne alla fine sono esche e non hanno alcuna corrispondenza con i valori delle righe. Più esche ci sono, più difficile sarà naturalmente trovare la migliore corrispondenza.
In questo particolare caso di corrispondenza, la lunghezza delle stringhe è irrilevante, poiché prevediamo abbreviazioni che rappresentano parole più lunghe, quindi il peso ottimale per la lunghezza è -0,3, il che significa che non penalizziamo le stringhe che variano di lunghezza. Riduciamo il punteggio in previsione di queste abbreviazioni, dando più spazio alle corrispondenze di parole parziali per sostituire le corrispondenze non di parole che richiedono semplicemente meno sostituzioni perché la stringa è più corta.
Il peso della parola è 1,0 mentre il peso della frase è solo 0,5, il che significa che penalizziamo parole intere mancanti da una stringa e valutiamo di più l'intera frase intatta. Questo è utile perché molte di queste stringhe hanno una parola in comune (il pericolo) dove ciò che conta davvero è se la combinazione (regione e pericolo) viene mantenuta o meno.
Infine, il peso minimo è ottimizzato a 10 e il peso massimo a 1. Ciò significa che se il migliore dei due punteggi (frase valore e parole valore) non è molto buono, la corrispondenza è fortemente penalizzata, ma noi non penalizzare notevolmente il peggio dei due punteggi. In sostanza, questo pone l'accento sul fatto che sia valueWord o valuePhrase per avere un buon punteggio, ma non entrambi. Una sorta di mentalità "prendi ciò che possiamo ottenere".
È davvero affascinante ciò che dice il valore ottimizzato di questi 5 pesi sul tipo di corrispondenza fuzzy delle stringhe in corso. Per casi pratici completamente diversi di corrispondenza delle stringhe fuzzy, questi parametri sono molto diversi. Finora l'ho usato per 3 applicazioni separate.
Sebbene non utilizzato nell'ottimizzazione finale, è stato creato un foglio di benchmark che abbina le colonne a se stessi per tutti i risultati perfetti lungo la diagonale e consente all'utente di modificare i parametri per controllare la frequenza con cui i punteggi divergono da 0 e annotare somiglianze innate tra le frasi di ricerca ( che potrebbe in teoria essere utilizzato per compensare i falsi positivi nei risultati)
Ulteriori applicazioni
Questa soluzione può essere utilizzata ovunque l'utente desideri che un sistema informatico identifichi una stringa in una serie di stringhe in cui non esiste una corrispondenza perfetta. (Come una corrispondenza approssimativa vlookup per le stringhe).
Quindi ciò che dovresti prendere da questo, è che probabilmente vuoi usare una combinazione di euristica di alto livello (trovare parole da una frase nell'altra frase, lunghezza di entrambe le frasi, ecc.) Insieme all'implementazione dell'algoritmo a distanza di Levenshtein. Perché decidere quale sia la corrispondenza "migliore" è una determinazione euristica (sfocata) - dovrai determinare un insieme di pesi per qualsiasi metrica che ti viene in mente per determinare la somiglianza.
Con il set appropriato di euristica e pesi, il tuo programma di confronto prenderà rapidamente le decisioni che avresti preso.