大约一年前,我遇到了这个问题,它涉及在杂项信息数据库中查找用户输入的有关石油钻井平台的信息。目的是进行某种模糊字符串搜索,以识别具有最常见元素的数据库条目。
研究的一部分涉及实施Levenshtein距离算法,该算法确定必须对字符串或短语进行多少更改才能将其转换为另一个字符串或短语。
我想出的实现方式相对简单,涉及两个词组的长度,每个词组之间的更改次数以及是否可以在目标条目中找到每个词的加权比较。
该文章位于私人站点上,因此我将尽力在此处附加相关内容:
模糊字符串匹配是对两个单词或短语进行相似度估算的过程。在许多情况下,它涉及识别彼此最相似的单词或短语。本文介绍了模糊字符串匹配问题的内部解决方案及其在解决各种问题中的实用性,这些问题可以使我们能够自动化以前需要乏味用户参与的任务。
介绍
在开发墨西哥湾验证程序工具时,最初就需要进行模糊字符串匹配。那里存在着一个已知的墨西哥石油钻井平台和平台的数据库,购买保险的人会给我们一些关于他们资产的错误键入信息,我们必须将其与已知平台的数据库进行匹配。当给出的信息很少时,我们所能做的最好的就是依靠承销商“认出”他们所指的信息,并调出适当的信息。这是该自动化解决方案派上用场的地方。
我花了一天的时间研究模糊字符串匹配的方法,最终偶然发现了Wikipedia上非常有用的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距离算法的实现。因为确定哪个是“最佳”匹配是一种启发式(模糊)确定,所以您必须为要得出的任何度量标准提供一组权重才能确定相似性。
借助适当的试探法和权重,您将使您的比较程序快速做出您将要做出的决定。