2008-09-10 78 views
22

嗨,我使用Levenshteins算法来获得源和目标字符串之间的距离。C#中匹配的模糊文本(句子/标题)

还我有方法,它返回的值从0到1:

/// <summary> 
/// Gets the similarity between two strings. 
/// All relation scores are in the [0, 1] range, 
/// which means that if the score gets a maximum value (equal to 1) 
/// then the two string are absolutely similar 
/// </summary> 
/// <param name="string1">The string1.</param> 
/// <param name="string2">The string2.</param> 
/// <returns></returns> 
public static float CalculateSimilarity(String s1, String s2) 
{ 
    if ((s1 == null) || (s2 == null)) return 0.0f; 

    float dis = LevenshteinDistance.Compute(s1, s2); 
    float maxLen = s1.Length; 
    if (maxLen < s2.Length) 
     maxLen = s2.Length; 
    if (maxLen == 0.0F) 
     return 1.0F; 
    else return 1.0F - dis/maxLen; 
} 

,但是这对我来说是不够的。因为我需要更复杂的方式来匹配两个句子。

例如,我想自动标记一些音乐,我有原创歌曲的名字,和我有歌曲垃圾,像超强,品质,年像2007年,2008年, etc..etc ..也有一些文件只有http://trash..thash..song_name_mp3.mp3,其他都很正常。我想创建一个算法,它会比我现在的工作更完美..也许任何人都可以帮助我?

这里是我目前的算法中:

/// <summary> 
/// if we need to ignore this target. 
/// </summary> 
/// <param name="targetString">The target string.</param> 
/// <returns></returns> 
private bool doIgnore(String targetString) 
{ 
    if ((targetString != null) && (targetString != String.Empty)) 
    { 
     for (int i = 0; i < ignoreWordsList.Length; ++i) 
     { 
      //* if we found ignore word or target string matching some some special cases like years (Regex). 
      if (targetString == ignoreWordsList[i] || (isMatchInSpecialCases(targetString))) return true; 
     } 
    } 

    return false; 
} 

/// <summary> 
/// Removes the duplicates. 
/// </summary> 
/// <param name="list">The list.</param> 
private void removeDuplicates(List<String> list) 
{ 
    if ((list != null) && (list.Count > 0)) 
    { 
     for (int i = 0; i < list.Count - 1; ++i) 
     { 
      if (list[i] == list[i + 1]) 
      { 
       list.RemoveAt(i); 
       --i; 
      } 
     } 
    } 
} 

/// <summary> 
/// Does the fuzzy match. 
/// </summary> 
/// <param name="targetTitle">The target title.</param> 
/// <returns></returns> 
private TitleMatchResult doFuzzyMatch(String targetTitle) 
{ 
    TitleMatchResult matchResult = null; 

    if (targetTitle != null && targetTitle != String.Empty) 
    { 
     try 
     { 
      //* change target title (string) to lower case. 
      targetTitle = targetTitle.ToLower(); 

      //* scores, we will select higher score at the end. 
      Dictionary<Title, float> scores = new Dictionary<Title, float>(); 

      //* do split special chars: '-', ' ', '.', ',', '?', '/', ':', ';', '%', '(', ')', '#', '\"', '\'', '!', '|', '^', '*', '[', ']', '{', '}', '=', '!', '+', '_' 
      List<String> targetKeywords = new List<string>(targetTitle.Split(ignoreCharsList, StringSplitOptions.RemoveEmptyEntries)); 

      //* remove all trash from keywords, like super, quality, etc.. 
      targetKeywords.RemoveAll(delegate(String x) { return doIgnore(x); }); 
      //* sort keywords. 
      targetKeywords.Sort(); 
     //* remove some duplicates. 
     removeDuplicates(targetKeywords); 

     //* go through all original titles. 
     foreach (Title sourceTitle in titles) 
     { 
      float tempScore = 0f; 
      //* split orig. title to keywords list. 
      List<String> sourceKeywords = new List<string>(sourceTitle.Name.Split(ignoreCharsList, StringSplitOptions.RemoveEmptyEntries)); 
      sourceKeywords.Sort(); 
      removeDuplicates(sourceKeywords); 

      //* go through all source ttl keywords. 
      foreach (String keyw1 in sourceKeywords) 
      { 
       float max = float.MinValue; 
       foreach (String keyw2 in targetKeywords) 
       { 
        float currentScore = StringMatching.StringMatching.CalculateSimilarity(keyw1.ToLower(), keyw2); 
        if (currentScore > max) 
        { 
         max = currentScore; 
        } 
       } 
       tempScore += max; 
      } 

      //* calculate average score. 
      float averageScore = (tempScore/Math.Max(targetKeywords.Count, sourceKeywords.Count)); 

      //* if average score is bigger than minimal score and target title is not in this source title ignore list. 
      if (averageScore >= minimalScore && !sourceTitle.doIgnore(targetTitle)) 
      { 
       //* add score. 
       scores.Add(sourceTitle, averageScore); 
      } 
     } 

     //* choose biggest score. 
     float maxi = float.MinValue; 
     foreach (KeyValuePair<Title, float> kvp in scores) 
     { 
      if (kvp.Value > maxi) 
      { 
       maxi = kvp.Value; 
       matchResult = new TitleMatchResult(maxi, kvp.Key, MatchTechnique.FuzzyLogic); 
      } 
     } 
    } 
    catch { } 
} 
//* return result. 
return matchResult; 
} 

这工作正常,但只是在某些情况下,很多应该匹配称号,不符合...我想我需要某种配方打与重量等,但我想不到一个..

想法?建议?交易算法?

通过我已经知道这个话题的方式(我的同事已经发布,但我们不能跟这个问题的妥善解决。): Approximate string matching algorithms

回答

6

你的问题在这里可以干扰词和有用的数据之间区分:

  • Rolling_Stones.Best_of_2003.Wild_Horses.mp3
  • Super.Quality.Wild_Horses.mp3
  • Tori_Amos.Wild_Horses.mp3

您可能需要生成一个要忽略的噪音字词字典。这似乎很笨重,但我不确定是否有算法可以区分乐队/专辑名称和噪音。

+0

我有乐队列表,我忽略了他们的关键字。 – 2008-09-10 07:05:30

6

这听起来像你想要的东西可能是最长的子字符串匹配。也就是说,在你的例子,两个文件就像

trash..thash..song_name_mp3.mp3 和 garbage..spotch..song_name_mp3.mp3

最终会寻找相同。

当然你需要一些启发式的。你可能会尝试的一件事是把字符串通过soundex转换器。 Soundex是用于查看“声音”是否相同的“编解码器”(可能会告诉电话运营商)。这或多或少是一个粗略的拼音和发音错误的半音音译。它肯定比编辑距离差,但更便宜得多。 (官方使用的是名字,只使用三个字符,没有理由停下来,但是只使用字符串中每个字符的映射关系,详见wikipedia

所以我的建议是soundex你的琴弦,将每个琴弦切成几段(如5,10,20),然后看看琴弦。在群集内,你可以使用更昂贵的东西,如编辑距离或最大子串。

+1

莱文斯坦的距离是一个更好的算法比这儿的拼音一个像同音,这也只是着眼于一个单词的开始。 – Keith 2008-09-10 06:49:17

3

有很多关于DNA序列比对的有些相关问题所做的工作(搜索“本地序列比对”) - 经典的算法是“尼德曼 - 翁施”,更复杂的现代的也很容易找到。这个想法 - 与Greg的答案类似 - 不是识别和比较关键字,而是尝试在长字符串中查找最长松散匹配的子字符串。

这是可悲的,如果唯一的目标是排序的音乐,一些正则表达式来支付可能的命名方案可能会工作比任何通用算法更好。

12

的老种,但它可能是未来的游客非常有用。如果您已经使用Levenshtein算法,你需要去好一点,我将介绍一些非常有效的启发式算法在这个解决方案:

Getting the closest string match

的关键是,你拿出3或4(或more)测量短语之间的相似性的方法(Levenshtein距离只是一种方法) - 然后使用您想要匹配的字符串的实例作为相似的,您调整这些启发式的权重和组合,直到获得最大化数量的正面匹配。然后你对所有未来的比赛都使用这个公式,你会看到很棒的结果。

如果用户参与这一进程,这也是最好的,如果你提供了一个接口,使用户可以看到,在与他们的第一选择不同意的情况下高度排在相似的其他比赛。

下面是从链接答案的摘录。如果你最终想要使用任何这样的代码,我必须提前将VBA转换为C#。


简单,快速并且非常有用的指标。使用这个,我创建了两个单独的度量来评估两个字符串的相似度。一个我称之为“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 

措施

使用这两个指标,第三它简单地计算两个字符串之间的距离,我有一系列的我可以运行一个优化算法来实现最大数量的匹配。模糊字符串匹配本身就是一门模糊科学,因此通过为测量字符串相似度创建线性独立的度量标准,并且拥有一组我们希望彼此匹配的已知字符串集合,我们可以找到这些参数,对于我们的特定样式字符串,给出最好的模糊匹配结果。

最初,度量的目标是有精确匹配的低搜索值,并为日益置换措施提高搜索值。在一个不切实际的情况下,使用一组明确定义的排列很容易定义,并且设计最终的公式,使得它们具有所期望的增加的搜索值结果。

enter image description here

正如你所看到的,最后两个指标,这是模糊的字符串匹配的指标,已经有一种自然倾向给予较低的分数到是为了匹配字符串(下对角线)。这是非常好的。

应用 为了允许模糊匹配的优化,我加权每个度量。因此,模糊字符串匹配的每个应用都可以对参数进行不同的加权。定义最终分数的公式是一个简单的指标及其权重的组合:

value = Min(phraseWeight*phraseValue, wordsWeight*wordsValue)*minWeight + 
     Max(phraseWeight*phraseValue, wordsWeight*wordsValue)*maxWeight + lengthWeight*lengthValue 

利用优化算法(神经网络是最好的位置,因为它是一个独立的,多维问题),我们的目标是现在要最大限度地提高比赛的数量。我创建了一个函数,用于检测每个集合相互之间的正确匹配数量,如最终截图所示。如果最低分数被指定为要匹配的字符串,则列或行获得一个分数,并且如果最低分数存在平局,则给出部分分数,并且匹配匹配字符串中的正确匹配。然后我优化了它。您可以看到,绿色单元格是与当前行最匹配的列,并且单元格周围的蓝色方形表示与当前列最匹配的行。底角的得分大致是成功比赛的数量,这就是我们告诉优化问题最大化的原因。 (已被使用)

enter image description here