Ich wurde vor ungefähr einem Jahr mit diesem Problem konfrontiert, als ich nach vom Benutzer eingegebenen Informationen über eine Bohrinsel in einer Datenbank mit verschiedenen Informationen suchte. Das Ziel war eine Art Fuzzy-String-Suche, die den Datenbankeintrag mit den häufigsten Elementen identifizieren konnte.
Ein Teil der Forschung umfasste die Implementierung des Levenshtein-Distanzalgorithmus , der bestimmt, wie viele Änderungen an einer Zeichenfolge oder Phrase vorgenommen werden müssen, um sie in eine andere Zeichenfolge oder Phrase umzuwandeln.
Die Implementierung, die ich mir ausgedacht habe, war relativ einfach und beinhaltete einen gewichteten Vergleich der Länge der beiden Phrasen, der Anzahl der Änderungen zwischen den einzelnen Phrasen und der Frage, ob jedes Wort im Zieleintrag gefunden werden konnte.
Der Artikel befindet sich auf einer privaten Website, daher werde ich mein Bestes tun, um die relevanten Inhalte hier anzuhängen:
Fuzzy String Matching ist der Prozess der Durchführung einer menschenähnlichen Schätzung der Ähnlichkeit zweier Wörter oder Phrasen. In vielen Fällen geht es darum, Wörter oder Phrasen zu identifizieren, die einander am ähnlichsten sind. Dieser Artikel beschreibt eine interne Lösung für das Fuzzy-String-Matching-Problem und seine Nützlichkeit bei der Lösung einer Vielzahl von Problemen, mit denen wir Aufgaben automatisieren können, für die zuvor eine mühsame Benutzerbeteiligung erforderlich war.
Einführung
Die Notwendigkeit, einen Fuzzy-String-Abgleich durchzuführen, entstand ursprünglich bei der Entwicklung des Validator-Tools für den Golf von Mexiko. Was existierte, war eine Datenbank mit bekannten Golfbohrinseln und Plattformen in Mexiko, und die Leute, die Versicherungen kauften, gaben uns einige schlecht getippte Informationen über ihr Vermögen, und wir mussten sie mit der Datenbank bekannter Plattformen abgleichen. Wenn nur sehr wenige Informationen gegeben wurden, können wir uns am besten darauf verlassen, dass ein Versicherer denjenigen, auf den er sich bezieht, "erkennt" und die richtigen Informationen aufruft. Hier bietet sich diese automatisierte Lösung an.
Ich habe einen Tag lang nach Methoden des Fuzzy-String-Matchings gesucht und bin schließlich auf den sehr nützlichen Levenshtein-Distanzalgorithmus auf Wikipedia gestoßen.
Implementierung
Nachdem ich über die Theorie dahinter gelesen hatte, implementierte ich und fand Wege, sie zu optimieren. So sieht mein Code in VBA aus:
'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
Einfach, schnell und eine sehr nützliche Metrik. Auf diese Weise habe ich zwei separate Metriken erstellt, um die Ähnlichkeit zweier Zeichenfolgen zu bewerten. Eine nenne ich "valuePhrase" und eine nenne ich "valueWords". valuePhrase ist nur der Levenshtein-Abstand zwischen den beiden Phrasen, und valueWords teilt die Zeichenfolge in einzelne Wörter auf, basierend auf Trennzeichen wie Leerzeichen, Bindestrichen und allem, was Sie möchten, und vergleicht jedes Wort mit dem anderen Wort, wobei es das kürzeste zusammenfasst Levenshtein Abstand, der zwei beliebige Wörter verbindet. Im Wesentlichen wird gemessen, ob die Informationen in einer 'Phrase' tatsächlich in einer anderen enthalten sind, ebenso wie eine wortweise Permutation. Ich habe ein paar Tage als Nebenprojekt verbracht, um die effizienteste Methode zum Teilen einer Zeichenfolge anhand von Trennzeichen zu finden.
valueWords-, valuePhrase- und Split-Funktion:
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
Ähnlichkeitsmaße
Mit diesen beiden Metriken und einer dritten, die einfach den Abstand zwischen zwei Zeichenfolgen berechnet, habe ich eine Reihe von Variablen, mit denen ich einen Optimierungsalgorithmus ausführen kann, um die größte Anzahl von Übereinstimmungen zu erzielen. Fuzzy-String-Matching ist selbst eine Fuzzy-Wissenschaft. Wenn wir also linear unabhängige Metriken zur Messung der String-Ähnlichkeit erstellen und über einen bekannten Satz von Strings verfügen, die wir miteinander abgleichen möchten, können wir die Parameter finden, die für unsere spezifischen Stile von Saiten, geben Sie die besten Fuzzy-Match-Ergebnisse.
Anfänglich bestand das Ziel der Metrik darin, einen niedrigen Suchwert für eine genaue Übereinstimmung zu haben und die Suchwerte für zunehmend permutierte Kennzahlen zu erhöhen. In einem unpraktischen Fall war es ziemlich einfach, dies unter Verwendung eines Satzes gut definierter Permutationen zu definieren und die endgültige Formel so zu konstruieren, dass sie nach Wunsch steigende Suchwertergebnisse hatten.
Im obigen Screenshot habe ich meine Heuristik angepasst, um etwas zu finden, das meiner Meinung nach gut auf meinen wahrgenommenen Unterschied zwischen Suchbegriff und Ergebnis skaliert ist. Die Heuristik, für Value Phrase
die ich in der obigen Tabelle verwendet habe, war =valuePhrase(A2,B2)-0.8*ABS(LEN(B2)-LEN(A2))
. Ich habe die Strafe für die Levenstein-Distanz effektiv um 80% des Unterschieds in der Länge der beiden "Phrasen" reduziert. Auf diese Weise erleiden "Phrasen", die die gleiche Länge haben, die volle Strafe, aber "Phrasen", die "zusätzliche Informationen" (länger) enthalten, aber ansonsten meistens dieselben Zeichen haben, werden weniger bestraft. Ich habe die Value Words
Funktion so verwendet , wie sie ist, und dann wurde meine endgültige SearchVal
Heuristik definiert als=MIN(D2,E2)*0.8+MAX(D2,E2)*0.2
- ein gewichteter Durchschnitt. Welche der beiden Bewertungen niedriger war, wurde mit 80% und 20% der höheren Bewertung gewichtet. Dies war nur eine Heuristik, die zu meinem Anwendungsfall passte, um eine gute Übereinstimmungsrate zu erzielen. Diese Gewichte können dann angepasst werden, um die beste Übereinstimmungsrate mit ihren Testdaten zu erzielen.
Wie Sie sehen können, haben die letzten beiden Metriken, bei denen es sich um Fuzzy-String-Matching-Metriken handelt, bereits die natürliche Tendenz, Strings, die übereinstimmen sollen (in der Diagonale), niedrige Punktzahlen zu verleihen. Das ist sehr gut.
Anwendung
Um die Optimierung des Fuzzy-Matchings zu ermöglichen, gewichte ich jede Metrik. Daher kann jede Anwendung der Fuzzy-String-Übereinstimmung die Parameter unterschiedlich gewichten. Die Formel, die das Endergebnis definiert, ist eine einfache Kombination der Metriken und ihrer Gewichte:
value = Min(phraseWeight*phraseValue, wordsWeight*wordsValue)*minWeight
+ Max(phraseWeight*phraseValue, wordsWeight*wordsValue)*maxWeight
+ lengthWeight*lengthValue
Mithilfe eines Optimierungsalgorithmus (neuronales Netzwerk ist hier am besten geeignet, da es sich um ein diskretes, mehrdimensionales Problem handelt) besteht das Ziel nun darin, die Anzahl der Übereinstimmungen zu maximieren. Ich habe eine Funktion erstellt, die die Anzahl der korrekten Übereinstimmungen zwischen den einzelnen Sätzen erkennt, wie in diesem letzten Screenshot zu sehen ist. Eine Spalte oder Zeile erhält einen Punkt, wenn die niedrigste Punktzahl der Zeichenfolge zugewiesen wird, die abgeglichen werden soll, und Teilpunkte werden vergeben, wenn für die niedrigste Punktzahl ein Gleichstand besteht und die richtige Übereinstimmung unter den gebundenen übereinstimmenden Zeichenfolgen liegt. Ich habe es dann optimiert. Sie können sehen, dass eine grüne Zelle die Spalte ist, die am besten zur aktuellen Zeile passt, und ein blaues Quadrat um die Zelle die Zeile, die am besten zur aktuellen Spalte passt. Die Punktzahl in der unteren Ecke entspricht in etwa der Anzahl der erfolgreichen Spiele, und dies ist es, was wir unserem Optimierungsproblem mitteilen, um es zu maximieren.
Der Algorithmus war ein wunderbarer Erfolg, und die Lösungsparameter sagen viel über diese Art von Problem aus. Sie werden feststellen, dass die optimierte Punktzahl 44 und die bestmögliche Punktzahl 48 beträgt. Die 5 Spalten am Ende sind Lockvögel und stimmen überhaupt nicht mit den Zeilenwerten überein. Je mehr Köder es gibt, desto schwieriger wird es natürlich, die beste Übereinstimmung zu finden.
In diesem speziellen Übereinstimmungsfall ist die Länge der Zeichenfolgen irrelevant, da wir Abkürzungen erwarten, die längere Wörter darstellen. Das optimale Gewicht für die Länge beträgt also -0,3, was bedeutet, dass Zeichenfolgen mit unterschiedlicher Länge nicht bestraft werden. Wir reduzieren die Punktzahl in Erwartung dieser Abkürzungen und geben mehr Platz für Teilwortübereinstimmungen, um Nichtwortübereinstimmungen zu ersetzen, die einfach weniger Substitutionen erfordern, da die Zeichenfolge kürzer ist.
Das Wortgewicht beträgt 1,0, während das Phrasengewicht nur 0,5 beträgt. Dies bedeutet, dass wir ganze Wörter, die in einer Zeichenfolge fehlen, bestrafen und mehr Wert darauf legen, dass die gesamte Phrase intakt ist. Dies ist nützlich, da viele dieser Zeichenfolgen ein Wort gemeinsam haben (die Gefahr), bei dem es wirklich darauf ankommt, ob die Kombination (Region und Gefahr) beibehalten wird oder nicht.
Schließlich wird das Mindestgewicht auf 10 und das Höchstgewicht auf 1 optimiert. Dies bedeutet, dass wenn die beste der beiden Bewertungen (Wertphrase und Wertwörter) nicht sehr gut ist, die Übereinstimmung stark bestraft wird, aber wir ziehen nicht an Das schlechteste der beiden Ergebnisse wird nicht stark bestraft. Im Wesentlichen wird dabei betont, dass entweder das valueWord oder die valuePhrase eine gute Punktzahl haben müssen, aber nicht beide. Eine Art "nimm was wir bekommen können" Mentalität.
Es ist wirklich faszinierend, was der optimierte Wert dieser 5 Gewichte über die Art des Fuzzy-String-Matchings aussagt. Für völlig unterschiedliche praktische Fälle der Fuzzy-String-Anpassung sind diese Parameter sehr unterschiedlich. Ich habe es bisher für 3 verschiedene Anwendungen verwendet.
Während es in der endgültigen Optimierung nicht verwendet wurde, wurde ein Benchmarking-Blatt erstellt, das die Spalten für alle perfekten Ergebnisse in der Diagonale mit sich selbst vergleicht und es dem Benutzer ermöglicht, Parameter zu ändern, um die Rate zu steuern, mit der die Punktzahlen von 0 abweichen, und angeborene Ähnlichkeiten zwischen Suchphrasen festzustellen ( was theoretisch verwendet werden könnte, um falsch positive Ergebnisse auszugleichen)
Weitere Anwendungen
Diese Lösung kann überall dort eingesetzt werden, wo der Benutzer möchte, dass ein Computersystem eine Zeichenfolge in einer Reihe von Zeichenfolgen identifiziert, bei denen keine perfekte Übereinstimmung vorliegt. (Wie eine ungefähre Übereinstimmungsansicht für Zeichenfolgen).
Was Sie daraus ziehen sollten, ist, dass Sie wahrscheinlich eine Kombination von Heuristiken auf hoher Ebene (Suchen von Wörtern aus einer Phrase in der anderen Phrase, Länge beider Phrasen usw.) zusammen mit der Implementierung des Levenshtein-Distanzalgorithmus verwenden möchten. Da die Entscheidung, welches die "beste" Übereinstimmung ist, eine heuristische (unscharfe) Bestimmung ist, müssen Sie für alle Metriken, die Sie erstellen, eine Reihe von Gewichten erstellen, um die Ähnlichkeit zu bestimmen.
Mit den entsprechenden Heuristiken und Gewichten können Sie mit Ihrem Vergleichsprogramm schnell die Entscheidungen treffen, die Sie getroffen hätten.