Il y a environ un an, ce problème m'a été présenté lors de la recherche d'informations saisies par l'utilisateur sur une plate-forme pétrolière dans une base de données d'informations diverses. Le but était de faire une sorte de recherche de chaîne floue qui pourrait identifier l'entrée de base de données avec les éléments les plus courants.
Une partie de la recherche a consisté à implémenter l' algorithme de distance de Levenshtein , qui détermine le nombre de modifications à apporter à une chaîne ou une phrase pour la transformer en une autre chaîne ou phrase.
L'implémentation que j'ai trouvée était relativement simple, et impliquait une comparaison pondérée de la longueur des deux phrases, du nombre de changements entre chaque phrase, et si chaque mot pouvait être trouvé dans l'entrée cible.
L'article est sur un site privé, je ferai de mon mieux pour ajouter le contenu pertinent ici:
L'appariement de chaînes floues est le processus consistant à effectuer une estimation de type humain de la similitude de deux mots ou expressions. Dans de nombreux cas, il s'agit d'identifier des mots ou des phrases qui se ressemblent le plus. Cet article décrit une solution interne au problème de correspondance de chaînes floues et son utilité pour résoudre une variété de problèmes qui peuvent nous permettre d'automatiser des tâches qui nécessitaient auparavant une implication fastidieuse des utilisateurs.
introduction
La nécessité de faire une correspondance de chaîne floue est apparue à l'origine lors du développement de l'outil de validation du golfe du Mexique. Ce qui existait était une base de données des plates-formes et plates-formes pétrolières connues du golfe du Mexique, et les personnes achetant une assurance nous fourniraient des informations mal tapées sur leurs actifs et nous devions les faire correspondre à la base de données des plates-formes connues. Lorsqu'il y avait très peu d'informations fournies, le mieux que nous puissions faire était de compter sur un souscripteur pour «reconnaître» celle à laquelle il faisait référence et appeler les informations appropriées. C'est là que cette solution automatisée est utile.
J'ai passé une journée à rechercher des méthodes d'appariement de chaînes floues, et je suis finalement tombé sur l'algorithme de distance de Levenshtein très utile sur Wikipédia.
la mise en oeuvre
Après avoir lu la théorie sous-jacente, j'ai implémenté et trouvé des moyens de l'optimiser. Voici à quoi ressemble mon code dans 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
Métrique simple, rapide et très utile. À l'aide de cela, j'ai créé deux mesures distinctes pour évaluer la similitude de deux chaînes. Un que j'appelle "valuePhrase" et un que j'appelle "valueWords". valuePhrase est juste la distance de Levenshtein entre les deux phrases, et valueWords divise la chaîne en mots individuels, en fonction de délimiteurs tels que les espaces, les tirets et tout ce que vous souhaitez, et compare chaque mot à l'autre, résumant le plus court Distance Levenshtein reliant deux mots quelconques. Essentiellement, il mesure si l'information dans une «phrase» est vraiment contenue dans une autre, tout comme une permutation mot à mot. J'ai passé quelques jours en tant que projet parallèle à trouver le moyen le plus efficace possible de fractionner une chaîne en fonction des délimiteurs.
Fonction valueWords, valuePhrase et 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
Mesures de similitude
En utilisant ces deux métriques, et une troisième qui calcule simplement la distance entre deux chaînes, j'ai une série de variables que je peux exécuter un algorithme d'optimisation pour obtenir le plus grand nombre de correspondances. L'appariement de chaînes floues est, en soi, une science floue, et donc en créant des métriques linéairement indépendantes pour mesurer la similitude des chaînes, et en ayant un ensemble connu de chaînes que nous souhaitons faire correspondre, nous pouvons trouver les paramètres qui, pour nos styles spécifiques de chaînes, donnent les meilleurs résultats de correspondance floue.
Initialement, l'objectif de la métrique était d'avoir une faible valeur de recherche pour une correspondance exacte et d'augmenter les valeurs de recherche pour des mesures de plus en plus permutées. Dans un cas peu pratique, cela était assez facile à définir en utilisant un ensemble de permutations bien définies et en concevant la formule finale de manière à ce que les résultats de recherche augmentent comme souhaité.
Dans la capture d'écran ci-dessus, j'ai modifié mon heuristique pour arriver à quelque chose qui me semblait bien adapté à ma différence perçue entre le terme de recherche et le résultat. L'heuristique que j'ai utilisée Value Phrase
dans la feuille de calcul ci-dessus était =valuePhrase(A2,B2)-0.8*ABS(LEN(B2)-LEN(A2))
. Je réduisais effectivement la pénalité de la distance de Levenstein de 80% de la différence de longueur des deux "phrases". De cette façon, les "phrases" qui ont la même longueur subissent la pénalité complète, mais les "phrases" qui contiennent des "informations supplémentaires" (plus longues) mais à part cela partagent toujours la plupart des mêmes personnages subissent une pénalité réduite. J'ai utilisé la Value Words
fonction telle quelle, puis mon SearchVal
heuristique finale a été définie comme=MIN(D2,E2)*0.8+MAX(D2,E2)*0.2
- une moyenne pondérée. Le plus faible des deux scores a obtenu une pondération de 80% et 20% du score le plus élevé. C'était juste une heuristique qui convenait à mon cas d'utilisation pour obtenir un bon taux de correspondance. Ces poids sont quelque chose que l'on pourrait ensuite modifier pour obtenir le meilleur taux de correspondance avec leurs données de test.
Comme vous pouvez le voir, les deux dernières mesures, qui sont des mesures d'appariement de chaînes floues, ont déjà tendance à donner des scores faibles aux chaînes qui doivent correspondre (en bas de la diagonale). C'est très bien.
Application
Pour permettre l'optimisation de la correspondance floue, je pondère chaque métrique. Ainsi, chaque application de correspondance de chaîne floue peut pondérer les paramètres différemment. La formule qui définit le score final est une simple combinaison des métriques et de leurs poids:
value = Min(phraseWeight*phraseValue, wordsWeight*wordsValue)*minWeight
+ Max(phraseWeight*phraseValue, wordsWeight*wordsValue)*maxWeight
+ lengthWeight*lengthValue
En utilisant un algorithme d'optimisation (le réseau de neurones est préférable ici car il s'agit d'un problème discret et multidimensionnel), l'objectif est maintenant de maximiser le nombre de correspondances. J'ai créé une fonction qui détecte le nombre de correspondances correctes de chaque ensemble, comme on peut le voir dans cette capture d'écran finale. Une colonne ou une ligne obtient un point si le score le plus bas est affecté à la chaîne qui devait être mise en correspondance, et des points partiels sont donnés s'il y a une égalité pour le score le plus bas, et la correspondance correcte est parmi les chaînes correspondantes liées. Je l'ai ensuite optimisé. Vous pouvez voir qu'une cellule verte est la colonne qui correspond le mieux à la ligne actuelle, et un carré bleu autour de la cellule est la ligne qui correspond le mieux à la colonne actuelle. Le score dans le coin inférieur est à peu près le nombre de matchs réussis et c'est ce que nous disons à notre problème d'optimisation de maximiser.
L'algorithme a été un formidable succès et les paramètres de la solution en disent long sur ce type de problème. Vous remarquerez que le score optimisé était de 44 et que le meilleur score possible est de 48. Les 5 colonnes à la fin sont des leurres et n'ont aucune correspondance avec les valeurs des lignes. Plus il y a de leurres, plus il sera naturellement difficile de trouver le meilleur match.
Dans ce cas de correspondance particulier, la longueur des chaînes n'est pas pertinente, car nous attendons des abréviations qui représentent des mots plus longs, donc le poids optimal pour la longueur est de -0,3, ce qui signifie que nous ne pénalisons pas les chaînes dont la longueur varie. Nous réduisons le score en prévision de ces abréviations, ce qui donne plus de place aux correspondances partielles de mots pour remplacer les correspondances non verbales qui nécessitent simplement moins de substitutions car la chaîne est plus courte.
Le poids du mot est de 1,0 tandis que le poids de la phrase n'est que de 0,5, ce qui signifie que nous pénalisons les mots entiers manquants dans une chaîne et valorisons davantage la phrase entière étant intacte. Ceci est utile car beaucoup de ces chaînes ont un mot en commun (le péril) où ce qui importe vraiment est de savoir si la combinaison (région et péril) est maintenue ou non.
Enfin, le poids minimum est optimisé à 10 et le poids maximum à 1. Ce que cela signifie, c'est que si le meilleur des deux scores (expression de valeur et mots de valeur) n'est pas très bon, le match est fortement pénalisé, mais nous ne 't pénaliser considérablement le pire des deux scores. Essentiellement, cela met l'accent sur l'exigence soit le valueWord ou valuePhrase pour avoir un bon score, mais pas les deux. Une sorte de mentalité de «prendre ce que nous pouvons obtenir».
C'est vraiment fascinant ce que la valeur optimisée de ces 5 poids dit sur le type de correspondance de chaîne floue qui a lieu. Pour des cas pratiques complètement différents de correspondance de chaînes floues, ces paramètres sont très différents. Je l'ai utilisé jusqu'à présent pour 3 applications distinctes.
Bien qu'elle ne soit pas utilisée dans l'optimisation finale, une feuille d'analyse comparative a été établie qui associe les colonnes à elles-mêmes pour tous les résultats parfaits dans la diagonale, et permet à l'utilisateur de modifier les paramètres pour contrôler la vitesse à laquelle les scores divergent de 0 et noter les similitudes innées entre les expressions de recherche ( qui pourrait en théorie être utilisé pour compenser les faux positifs dans les résultats)
Autres applications
Cette solution peut être utilisée partout où l'utilisateur souhaite qu'un système informatique identifie une chaîne dans un ensemble de chaînes où il n'y a pas de correspondance parfaite. (Comme une correspondance approximative avec vlookup pour les chaînes).
Donc, ce que vous devriez en déduire, c'est que vous voulez probablement utiliser une combinaison d'heuristiques de haut niveau (trouver des mots d'une phrase dans l'autre phrase, la longueur des deux phrases, etc.) avec la mise en œuvre de l'algorithme de distance Levenshtein. Parce que décider quelle est la «meilleure» correspondance est une détermination heuristique (floue) - vous devrez trouver un ensemble de pondérations pour toutes les mesures que vous proposez pour déterminer la similitude.
Avec l'ensemble d'heuristique et de pondération approprié, votre programme de comparaison prendra rapidement les décisions que vous auriez prises.