Нечеткое совпадение текста (предложений / названий) в C # - PullRequest
22 голосов
/ 10 сентября 2008

Эй, я использую алгоритм Левенштейнов , чтобы получить расстояние между исходной и целевой строками.

также у меня есть метод, который возвращает значение от 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 .. также некоторые файлы имеют только 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;
}

Это работает нормально, но в некоторых случаях многие названия, которые должны совпадать, не совпадают ... Я думаю, мне нужна какая-то формула для игры с весами и т. Д., Но я не могу придумать одно. .

Идеи? Предложения? Algos

кстати, я уже знаю эту тему (мой коллега уже опубликовал ее, но мы не можем найти правильное решение этой проблемы.): Приблизительные алгоритмы сопоставления строк

Ответы [ 5 ]

12 голосов
/ 10 апреля 2012

Вид старого, но может быть полезным для будущих посетителей. Если вы уже используете алгоритм Левенштейна и вам нужно пойти немного лучше, я опишу некоторые очень эффективные эвристики в этом решении:

Получение наиболее близкого совпадения строк

Суть в том, что вы придумали 3 или 4 (или больше ) метода измерения сходства между вашими фразами (расстояние Левенштейна - это всего лишь один метод) - и затем использовали реальные примеры строк, которые вы хотите чтобы совпадения были похожими, вы корректируете веса и комбинации этих эвристик, пока не получите что-то, что максимизирует количество положительных совпадений. Затем вы используете эту формулу для всех будущих матчей, и вы должны увидеть отличные результаты.

Если в процессе участвует пользователь, также лучше, если вы предоставите интерфейс, позволяющий пользователю видеть дополнительные совпадения, которые имеют высокий уровень сходства, если они не согласны с первым выбором.

Вот выдержка из связанного ответа. Если вы в конечном итоге захотите использовать любой из этих кодов как есть, заранее прошу прощения за необходимость конвертировать VBA в C #.


Простой, быстрый и очень полезный показатель. Используя это, я создал две отдельные метрики для оценки сходства двух строк. Один я называю «valuePhrase», а другой - «ValueWords». valuePhrase - это просто расстояние Левенштейна между двумя фразами, а valueWords разбивает строку на отдельные слова на основе разделителей, таких как пробелы, тире и все, что вы хотите, и сравнивает каждое слово с каждым другим словом, суммируя самое короткое Расстояние Левенштейна, соединяющее любые два слова. По сути, он измеряет, действительно ли информация в одной «фразе» содержится в другой, просто как перестановка слов. Я провел несколько дней в качестве побочного проекта, придумывая наиболее эффективный способ разделения строки на разделители.

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

Меры сходства

Используя эти две метрики и третью, которая просто вычисляет расстояние между двумя строками, у меня есть ряд переменных, с помощью которых я могу запустить алгоритм оптимизации для достижения наибольшего числа совпадений. Нечеткое сопоставление строк само по себе является нечеткой наукой, и поэтому, создав линейно независимые метрики для измерения сходства строк и имея известный набор строк, которые мы хотим сопоставить друг с другом, мы можем найти параметры, которые для наших конкретных стилей строки, дают лучшие результаты нечетких матчей.

Первоначально целью метрики было низкое значение поиска для точного соответствия и увеличение значений поиска для все более изменяемых показателей. В непрактичном случае это было довольно легко определить, используя набор четко определенных перестановок и разработав окончательную формулу так, чтобы они имели желаемые результаты при увеличении значений поиска.

enter image description here

Как видите, последние две метрики, которые являются метриками нечеткого сопоставления строк, уже имеют естественную тенденцию давать низкие оценки строкам, которые должны совпадать (по диагонали). Это очень хорошо.

Применение Для оптимизации нечеткого сопоставления я взвешиваю каждую метрику. Таким образом, каждое применение нечеткого совпадения строк может по-разному взвешивать параметры. Формула, которая определяет итоговую оценку, представляет собой простую комбинацию метрик и их весов:

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

Используя алгоритм оптимизации (нейронная сеть лучше всего подходит здесь, потому что это дискретная многомерная задача), теперь цель состоит в том, чтобы максимизировать количество совпадений. Я создал функцию, которая определяет количество правильных совпадений каждого набора друг к другу, как видно на этом окончательном снимке экрана. Столбец или строка получают балл, если наименьший балл присваивается той строке, которая должна была быть сопоставлена, и частичные баллы присваиваются, если есть галстук для наименьшего балла, и правильное совпадение находится среди связанных сопоставленных строк. Я тогда оптимизировал это. Вы можете видеть, что зеленая ячейка - это столбец, который лучше всего соответствует текущей строке, а синий квадрат вокруг ячейки - это строка, которая лучше всего соответствует текущему столбцу. Счет в нижнем углу - это примерно количество успешных совпадений, и это то, что мы говорим нашей задаче оптимизации, чтобы максимизировать.

enter image description here

6 голосов
/ 10 сентября 2008

Ваша проблема здесь может быть в различении шумовых слов и полезных данных:

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

Возможно, вам придется создать словарь шумовых слов, которые можно игнорировать. Это кажется неуклюжим, но я не уверен, что есть алгоритм, который может различать названия групп / альбомов и шум.

6 голосов
/ 10 сентября 2008

Похоже, что вы хотите, может быть самое длинное совпадение подстроки. То есть в вашем примере два файла типа

trash..thash..song_name_mp3.mp3 а также garbage..spotch..song_name_mp3.mp3

будет выглядеть так же.

Тебе, конечно, понадобится немного эвристики. Одна вещь, которую вы можете попробовать, - это вставить строку через конвертер soundex. Soundex - это «кодек», используемый для проверки того, звучат ли вещи «одинаково» (как вы могли бы сказать телефонному оператору). Это более или менее грубая фонетическая и нецензурная полу-доказательная транслитерация. Это определенно меньше, чем расстояние редактирования, но намного, намного дешевле. (Официальное использование для имен, и использует только три символа. Однако нет никаких оснований останавливаться на этом, просто используйте сопоставление для каждого символа в строке. Подробнее см. wikipedia )

Так что я бы предложил озвучить ваши струны, нарезать каждый из них на несколько длинных траншей (скажем, 5, 10, 20), а затем просто посмотреть на кластеры. В кластерах вы можете использовать что-то более дорогое, например, расстояние редактирования или максимальную подстроку.

3 голосов
/ 11 сентября 2008

Была проделана большая работа по несколько связанной проблеме выравнивания последовательностей ДНК (поиск «локальное выравнивание последовательностей») - классический алгоритм «Needleman-Wunsch» и более сложные современные также легко найти. Идея заключается в том, чтобы - подобно ответу Грега - вместо того, чтобы идентифицировать и сравнивать ключевые слова, попытаться найти самые длинные слабо совпадающие подстроки в длинных строках.

Как это ни печально, если единственной целью является сортировка музыки, ряд регулярных выражений, охватывающих возможные схемы именования, вероятно, будет работать лучше, чем любой общий алгоритм.

0 голосов
/ 17 апреля 2019

Существует GitHub репо , реализующий несколько методов.

...