22

嘿,我正在使用Levenshteins算法来获取源字符串和目标字符串之间的距离。

我也有返回值从 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年等年份等。还有一些文件只有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;
}

这正常工作,但只是在某些情况下,很多标题应该匹配,不匹配......我想我需要某种公式来玩权重等,但我想不出一个......

想法?建议?算法?

顺便说一句,我已经知道这个话题(我的同事已经发布了,但我们无法为这个问题提供适当的解决方案。): 近似字符串匹配算法

4

5 回答 5

14

有点旧,但它可能对未来的访客有用。如果您已经在使用 Levenshtein 算法并且需要做得更好,我将在此解决方案中描述一些非常有效的启发式方法:

获取最接近的字符串匹配

关键是你想出 3 或 4 种(或更多)方法来衡量你的短语之间的相似性(Levenshtein 距离只是一种方法) - 然后使用你想要匹配为相似的字符串的真实示例,你调整权重以及这些启发式的组合,直到你得到最大化正匹配数量的东西。然后,您将该公式用于所有未来的比赛,您应该会看到很好的结果。

如果用户参与了该过程,那么最好提供一个界面,该界面允许用户查看相似度高的其他匹配项,以防他们不同意第一选择。

这是链接答案的摘录。如果您最终想按原样使用任何此代码,我提前为必须将 VBA 转换为 C# 表示歉意。


简单、快速且非常有用的指标。使用它,我创建了两个单独的指标来评估两个字符串的相似性。一种我称之为“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 = Min(phraseWeight*phraseValue, wordsWeight*wordsValue)*minWeight + 
        Max(phraseWeight*phraseValue, wordsWeight*wordsValue)*maxWeight + lengthWeight*lengthValue

使用优化算法(神经网络在这里最好,因为它是一个离散的多维问题),现在的目标是最大化匹配的数量。我创建了一个函数来检测每组正确匹配的数量,如最终屏幕截图所示。如果将最低分数分配给要匹配的字符串,则列或行得到一个分数,如果最低分数相同,则给出部分分数,并且正确匹配在并列匹配的字符串中。然后我对其进行了优化。您可以看到绿色单元格是与当前行最匹配的列,而单元格周围的蓝色方块是与当前列最匹配的行。底角的分数大致是成功匹配的数量,这就是我们告诉优化问题最大化的值。

在此处输入图像描述

于 2012-04-09T20:17:26.430 回答
7

听起来您想要的可能是最长的子字符串匹配。也就是说,在您的示例中,有两个文件,例如

垃圾..thash..song_name_mp3.mp3 和垃圾..spotch..song_name_mp3.mp3

最终会看起来一样。

当然,你需要一些启发式方法。您可能会尝试的一件事是将字符串通过 soundex 转换器。Soundex 是用于查看事物“听起来”是否相同的“编解码器”(您可能会告诉电话接线员)。它或多或少是一个粗略的语音和错误发音的半证明音译。它绝对比编辑距离差,但便宜得多。(官方使用的是名称,只使用三个字符。不过,没有理由停在那里,只需对字符串中的每个字符使用映射即可。有关详细信息,请参阅wikipedia

所以我的建议是对你的琴弦进行soundex,将每根琴弦切成几段(比如5、10、20),然后只看簇。在集群中,您可以使用更昂贵的东西,例如编辑距离或最大子字符串。

于 2008-09-10T06:37:49.960 回答
6

您的问题可能是区分干扰词和有用数据:

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

您可能需要生成要忽略的噪声词词典。这看起来很笨拙,但我不确定是否有一种算法可以区分乐队/专辑名称和噪音。

于 2008-09-10T06:59:10.390 回答
4

在 DNA 序列比对(搜索“局部序列比对”)的一些相关问题上做了很多工作——经典算法是“Needleman-Wunsch”,更复杂的现代算法也很容易找到。这个想法是 - 类似于 Greg 的答案 - 而不是识别和比较关键字,而是尝试在长字符串中找到最长的松散匹配子字符串。

可悲的是,如果唯一的目标是对音乐进行排序,那么一些覆盖可能命名方案的正则表达式可能会比任何通用算法更好。

于 2008-09-11T06:44:57.703 回答
0

有一个GitHub repo实现了几种方法。

于 2019-04-17T16:42:55.103 回答