大约一年前,当我在一个杂项信息的数据库中查询用户输入的石油钻井平台信息时,出现了这个问题。目标是做一些模糊的字符串搜索,可以用最常见的元素来标识数据库条目。
部分研究涉及实现Levenshtein distance算法,该算法确定必须对字符串或短语进行多少更改才能将其转换为另一个字符串或短语。
我提出的实现相对简单,涉及两个短语长度的加权比较,每个短语之间的变化数量以及每个单词是否可以在目标条目中找到。
这篇文章是在一个私人网站,所以我会尽我所能来这里附加的相关内容:
模糊字符串匹配是执行的两个相似的类人估计的过程单词或短语。在很多情况下,它涉及识别彼此最相似的单词或短语。本文介绍了模糊字符串匹配问题的内部解决方案及其在解决各种问题方面的实用性,这些问题可以使我们自动执行之前需要繁琐的用户参与的任务。
介绍
需要做模糊字符串匹配原本是约在开发墨西哥湾验证工具。现存的是一个已知的墨西哥海湾钻井平台和平台的数据库,购买保险的人会给我们一些关于他们资产的错误信息,我们必须将其与已知平台的数据库相匹配。当没有足够的信息时,我们能做的最好的事情就是依靠保险公司来“认识”他们所指的那个人,并调出正确的信息。这就是这个自动化解决方案派上用场的地方。
我花了一天时间研究模糊字符串匹配的方法,最终偶然发现了维基百科上非常有用的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和斯普利特功能:类似的
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偏离的比率,并注意天生的相似性搜索短语(在理论上可以被用来抵消误报的结果)之间
进一步应用
该解决方案有潜力成为在用户希望有一个计算机系统识别一组字符串中没有完美匹配的字符串时使用。 (就像一个近似的匹配查找字符串)。
所以,你应该采取什么样的从这个,就是你可能要与执行一起使用的高级启发式组合(找到的话从其他词组一个词组,无论短语的长度等)的Levenshtein距离算法。因为决定哪一个是“最佳”匹配是一个启发式(模糊)决定 - 您必须为您提出的任何度量提供一组权重来确定相似度。
与适当的启发和权重的,你有你比较程序快速进行,你会作出的决定。
算法通常做这种类型的东西工作,对确定需要多少变化把一个检查字符串转换成目标字符串。在这种情况下,这些类型的算法根本无法正常工作。我认为让计算机把它关掉会非常困难。 – 2011-05-02 16:28:29
一般来说,什么称为“最接近的字符串”将取决于所使用的相似性度量以及用于引入对齐中的间隙的惩罚。例如,你认为“牛”和“鸡”比“牛”和“红”更相似(因为它们是相关的概念),或者是相反的(因为“鸡”比“牛” )?但是考虑到相似性度量和空位罚分,可以证明下面的Levenshtein算法可以保证找到最接近的字符串。 Needleman-Wunsch和Smith-Waterman也是如此(下面进一步介绍)。 – 2012-05-04 09:56:21
许多语言的Levenshtein距离源代码:Java,Ruby,Python,PHP等http://en.wikibooks.org/wiki/Algorithm_Implementation/Strings/Levenshtein_distance – joelparkerhenderson 2012-05-04 01:26:06