大约一年前,当我在杂项信息数据库中查找用户输入的有关石油钻井平台的信息时,我遇到了这个问题。目标是进行某种模糊字符串搜索,以识别具有最常见元素的数据库条目。
部分研究涉及实施Levenshtein 距离算法,该算法确定必须对字符串或短语进行多少更改才能将其转换为另一个字符串或短语。
我想出的实现比较简单,包括对两个短语的长度、每个短语之间的变化次数以及每个单词是否可以在目标条目中找到的加权比较。
该文章在私人网站上,因此我会尽力在此处附加相关内容:
模糊字符串匹配是对两个单词或短语的相似性进行类人估计的过程。在许多情况下,它涉及识别彼此最相似的单词或短语。本文描述了模糊字符串匹配问题的内部解决方案及其在解决各种问题中的有用性,这些问题可以让我们自动化以前需要繁琐的用户参与的任务。
介绍
在开发墨西哥湾验证器工具时,最初需要进行模糊字符串匹配。现有的是一个已知墨西哥湾石油钻井平台和平台的数据库,购买保险的人会给我们一些关于他们资产的错误输入信息,我们必须将其与已知平台的数据库相匹配。当提供的信息很少时,我们能做的最好的事情就是依靠承销商来“识别”他们所指的承销商并调用适当的信息。这就是这个自动化解决方案派上用场的地方。
我花了一天时间研究模糊字符串匹配的方法,最终偶然发现了维基百科上非常有用的 Levenshtein 距离算法。
执行
在阅读了它背后的理论之后,我实施并找到了优化它的方法。这就是我的代码在 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
简单、快速且非常有用的指标。使用它,我创建了两个单独的指标来评估两个字符串的相似性。一种我称之为“valuePhrase”,一种我称之为“valueWords”。valuePhrase 只是两个短语之间的 Levenshtein 距离,valueWords 根据分隔符(例如空格、破折号和您想要的任何其他内容)将字符串拆分为单个单词,并将每个单词与其他单词进行比较,总结出最短的连接任意两个单词的 Levenshtein 距离。本质上,它衡量一个“短语”中的信息是否真的包含在另一个“短语”中,就像逐字排列一样。我花了几天时间作为一个附带项目,提出了基于分隔符拆分字符串的最有效方法。
valueWords、valuePhrase 和 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
相似度测量
使用这两个指标,以及简单计算两个字符串之间距离的第三个指标,我有一系列变量,我可以运行优化算法来实现最大数量的匹配。模糊字符串匹配本身就是一门模糊科学,因此通过创建用于测量字符串相似性的线性独立指标,并拥有一组我们希望彼此匹配的已知字符串,我们可以找到适合我们特定风格的参数字符串,给出最好的模糊匹配结果。
最初,该度量的目标是为精确匹配提供较低的搜索值,并为越来越多的排列度量增加搜索值。在一个不切实际的情况下,这很容易使用一组定义明确的排列来定义,并设计最终公式,以便它们根据需要增加搜索值结果。

在上面的屏幕截图中,我调整了我的启发式方法,以提出一些我觉得可以很好地适应搜索词和结果之间的感知差异的东西。Value Phrase
我在上述电子表格中使用的启发式方法是=valuePhrase(A2,B2)-0.8*ABS(LEN(B2)-LEN(A2))
. 我有效地将 Levenstein 距离的惩罚减少了两个“短语”长度差异的 80%。这样,具有相同长度的“短语”会受到全部惩罚,但包含“附加信息”(更长)但除此之外仍然大部分共享相同字符的“短语”会受到减少的惩罚。我按原样使用该Value Words
函数,然后我的最终SearchVal
启发式定义为=MIN(D2,E2)*0.8+MAX(D2,E2)*0.2
- 加权平均值。两个分数中较低者的权重为 80%,较高分数的权重为 20%。这只是适合我的用例以获得良好匹配率的启发式方法。然后可以调整这些权重,以获得与测试数据的最佳匹配率。


正如您所看到的,最后两个指标,即模糊字符串匹配指标,已经自然倾向于将低分给要匹配的字符串(对角线下方)。这是非常好的。
应用
为了优化模糊匹配,我对每个度量进行加权。因此,模糊字符串匹配的每个应用程序都可以对参数进行不同的加权。定义最终分数的公式是指标及其权重的简单组合:
value = Min(phraseWeight*phraseValue, wordsWeight*wordsValue)*minWeight
+ Max(phraseWeight*phraseValue, wordsWeight*wordsValue)*maxWeight
+ lengthWeight*lengthValue
使用优化算法(神经网络在这里最好,因为它是一个离散的多维问题),现在的目标是最大化匹配的数量。我创建了一个函数来检测每组正确匹配的数量,如最终屏幕截图所示。如果将最低分数分配给要匹配的字符串,则列或行得到一个分数,如果最低分数相同,则给出部分分数,并且正确匹配在并列匹配的字符串中。然后我对其进行了优化。您可以看到绿色单元格是与当前行最匹配的列,而单元格周围的蓝色方块是与当前列最匹配的行。底角的分数大致是成功匹配的数量,这就是我们告诉优化问题最大化的值。

该算法取得了巨大的成功,解决方案参数对这类问题说了很多。您会注意到优化后的分数是 44,最好的分数是 48。最后的 5 列是诱饵,与行值完全不匹配。诱饵越多,自然就越难找到最佳匹配。
在这个特定的匹配情况下,字符串的长度是无关紧要的,因为我们期望代表更长单词的缩写,所以长度的最佳权重是 -0.3,这意味着我们不会惩罚长度不同的字符串。我们降低了对这些缩写的预期分数,为部分单词匹配提供了更多空间,以取代非单词匹配,因为字符串更短,因此只需要更少的替换。
单词权重为 1.0,而短语权重仅为 0.5,这意味着我们会惩罚从一个字符串中丢失的整个单词,并且更重视整个短语的完整性。这很有用,因为许多这些字符串都有一个共同的词(危险),真正重要的是是否保持组合(区域和危险)。
最后,最小权重优化为 10,最大权重优化为 1。这意味着如果两个分数(价值短语和价值词)中最好的不是很好,则匹配会受到很大的惩罚,但我们不会不要严重惩罚两个分数中最差的一个。从本质上讲,这强调要求valueWord或 valuePhrase 有一个好分数,但不能两者兼而有之。一种“拿走我们能得到的”的心态。
这 5 个权重的优化值说明了正在发生的模糊字符串匹配,这真是令人着迷。对于完全不同的模糊字符串匹配的实际情况,这些参数是非常不同的。到目前为止,我已经将它用于 3 个单独的应用程序。
虽然在最终优化中未使用,但建立了一个基准表,该表将列与其自身匹配以获得对角线下的所有完美结果,并允许用户更改参数以控制分数偏离 0 的速率,并注意搜索短语之间的先天相似性(理论上可以用来抵消结果中的误报)

进一步的应用
该解决方案有可能用于用户希望计算机系统识别一组字符串中不存在完美匹配的字符串的任何地方。(就像字符串的近似匹配vlookup)。
因此,您应该从中得到的,是您可能希望结合使用高级启发式算法(从另一个短语中的一个短语中查找单词、两个短语的长度等)以及 Levenshtein 距离算法的实现。因为决定哪个是“最佳”匹配是一种启发式(模糊)确定 - 您必须为您提出的任何指标提出一组权重以确定相似性。
使用适当的启发式方法和权重,您可以让您的比较程序快速做出您可能做出的决定。